Stepping down to 8 bits
Perhaps you've already asked yourself whether you should jump from a 16- to a 32-bit or a 64-bit processor to accommodate the extra features and complex requirements of the next-generation design. The question is: Have you seriously thought about stepping down to 8 bits?
By Nicholas Cravotta, Technical Editor -- EDN, May 11, 2000
At least two factors suggest that moving to a more powerful processor doesn't necessarily mean taking on more bits. As applications mature, powerful, function-specific processors, or engines, become available to take on most of the grunt work. With an MP3 player, for example, you can buy complete digital-audio decoding engines. You need only a user
interface and an I/O interface to a PC to flesh out your digital player. An 8-bit processor can efficiently handle these functions. And, for tasks that require a bit more horsepower, you might be surprised at the powerful workhorses those little 8-bit processors have become with their 40-MHz clocks, expandable program memory, vectored interrupt tables, floating-point libraries, C compilers, and range of peripherals. You may find that selecting a processor that uses fewer bits may better help you meet cost and performance constraints with headroom to spare.
I spent more than 10 years bit-fiddling with 16-and 32-bit x86 assembly in an industrial-control application. For this hands-on project, I wanted to see what it would be like, with all of my 16/32-bit biases, to take a step back and design with an 8-bit processor. After all, because the processor is less complex, the design process should be simpler, shouldn'tit?
One's stupid mistakes often generate the best learning material. Some design experience, therefore, can be somewhat embarrassing to write about. Fortunately, the consolation always exists that, having completed this project, I am no longer the same programmer who made those mistakes. If you're considering moving to an 8-bit processor, reading about this project may save you from some of the pain and suffering that I experienced.
The project
For my project, I decided to make a sequencer. A number of my fellow engineers and I recently discovered "Cool-Neon," also known as electroluminescent wire (Reference 1). You can use an inverter to convert a 9V power supply to 300V ac, which drives the wire to create multicolored light sculptures. We first built a 2-D fish that we could mount on bicycles to create the illusion of a swimming school of fish. For our next-generation project, we needed a sequencer that could cycle through patterns of light to create an illusion of movement for either moving sculptures or an electronic kaleidoscope in which different strands of light lit at different times represent frames in a short and cyclic animated sequence.
An 8-bit processor seemed perfect for this project. I needed one output pin to drive each strand of wire; for a 10-strand capacity, I would need 10 pins. A button (using another pin) would allow someone to cycle through pattern options. To the features list, I quickly added an input for a sensor. For example, a sensor placed around a bicycle wheel could trigger the pattern based on an external event rather than on an internal timer. Deciding to go with the PIC family from Microchip, I found that the 18-pin package processors have 13 I/O pins, so I had one pin to spare, which didn't stay spare for long.
Limited resources: memory
You can often solve a problem by throwing resources, such as memory, processing, and I/O, at the more powerful processors. The difference between family members in the 8-bit world, however, can lead to direct cost savings because of the variety of configurations. For example, a processor with 2 kbytes of onboard memory costs less than one with 4 kbytes. I remember having banks of 64 kbytes available; you can burn memory by using a large table to speed a critical calculation—a viable and sometimes vital option.
Early on, I made what was to become a critical design decision for my sequencer. Instead of representing active lights with 1 bit (10 lights equals 10 bits), I created a table 10 bytes long that contained the physical port code for the appropriate logical light; that is, light 1=bit 3 on I/O Port A). I made this table because, as I tested to see whether a light was populated, I had to calculate the physical port. At the time, it seemed easier to store this precalculated value. Given my old mindset, this choice was intelligent: Perform the calculation once at the beginning (outside the main processing loop) and save that many cycles every time you later use the calculation.
Unfortunately, this choice was a terrible one. First, the sequencer spends most of its time in a polling loop, waiting to see whether a key has been pressed or a timer had been triggered. Thus, Ilost the few cycles I saved (maximum 10 lights times 10 instructions=100 cycles) in the polling loop. Second and more important, I used 8 more bytes of RAM than I needed to. I'm used to throwing away hundreds of bytes over-padding buffers to cover conceivable worst-case scenarios. However, when 8 bytes represents almost one-eighth of your total data memory, it's a bad decision.
I didn't realize the magnitude of this choice until I ran out of data memory. I added a variable, and the program failed to run because another variable was pushed into the zone of undefined 0xFFs. My project hadn't seemed complicated enough to use this much memory, but I forgot to keep an eye on memory as I added more functions. Hitting the top of data memory brought my development to a temporary halt as I struggled to recover bytes. I then made the foolish mistake of haphazardly trying to double temporary variables. For example, certain functions need loop counters. However, you can use these loop counters to count things other than those functions. To pull off this effort, I needed to keep a tight rein on the scope of my variables to ensure that one function did not call another function using the same variable.
My first intuition directed me to give double-duty variables only one name. I could just imagine the debugging nightmare of failing to remember that two distinct variables were in fact the same variable and changing each other. At least if they had the same name, I'd have a better chance of catching myself if I accidentally violated the scope of a variable.
As I tried to group the variables by scope, I learned that I could more easily perform this task before a variable is peppered throughout a program. For example, the variable Temp had an immediate scope, meaning that if a function called another function, the first function should assume that Temp was destroyed. At the other extreme were system or global variables, such as Flags, which all functions can use. Then comes the gray area. Cycle, for example, counts which step in a light pattern is currently being processed. Because only one pattern can execute at a time, each pattern function can use Cycle. Additionally, when a user edits a user-programmable pattern, no predefined patterns are running, so you can use Cycle here as well. However, I did have a potential scope violation when I wanted to display the user-programmable pattern as it was being edited. Either I had to store Cycle before display and restore it after for editing, or I could let the user reset Cycle by stopping the display at the Cycle he or she wanted to edit, which is the choice I implemented.
To store Cycle in this case would have required a global variable, given that displaying a pattern used the main program loop, but dedicating a variable to this purpose defeats the point of doubling up the variable in the first place. I could also have chosen to implement a data stack. In the 16- and 32-bit worlds, the stack is a handy friend. However, the microprocessor I was using had an internal stack for program addresses only. (I'm sure there's some way of tricking the processor into storing data, but with only eight levels, I would risk blowing the stack if I wanted to save more than one or two values.) Stacks take at least an extra byte for tail management, and you have to buffer them for worst-case use. I avoided implementing a stack because I thought I had too few values to save.
The scope of variables is critical to keeping track of and creating dependencies for a variable by doubling its use. This task should be a careful decision. I tried to segment variables into functional blocks, but, even with careful planning, it always seemed as if there could be a case for simultaneously using both blocks and, thus, all the variables. I would then find myself chasing down a bug of my own making.
Chances are you'll be able to significantly reduce the processor's cost once you decide to put it on the board. It makes sense to give yourself some headroom for reprogrammability when you develop; however, you may find yourself just a few bytes shy of fitting your design into a much less expensive processor. Less expensive options are not always equivalent and affect development in that they define memory and performance thresholds. By understanding these thresholds when you start development rather than when you finish, you can determine just how tightly you have to constrain your available resources. In any case, you should probably never hire a byte for a bit role.
Limited resources: processing
As "feature creep"—extra features that complicate a design and throw off the schedule—continued to add options and enhancements to the sequencer, I realized that I might top off in program memory as well. In the 16- and 32-bit world, I often threw memory at a problem to reduce total computation time. I found myself making the reverse trade-off with my sequencer.
When I designed the user-programmable pattern, which would be stored in EEPROM, I used bits to represent each light. However, 10 bits is 1 byte with 2 bits left over, which you can pack in groups of four to fully use another byte. This decision created a problem because the pattern would be stored in a different format from the one I used to turn on the lights. (The EEPROM format used a logical format to keep track of lights that were lit by: lights zero to nine. The light format used a physical format: a byte for each port, A and B, and each bit represented a corresponding I/O pin, out of order due to board-layout considerations.) Two formats exist because the physical format was the actual mask for driving the output ports. However, the physical format used 16 bits instead of only 10 like the logical format. Thus, I would need a conversion function.
I would typically write both directions of a conversion function simultaneously. To minimize total processing cycles, I would convert the EEPROM logical format to the light/physical format. As the user edited this cycle of the pattern, I would make and display changes to the light/physical format. When the user was done editing this cycle, I would convert the light/physical format back to the EEPROM/logical format and save the cycle.
I decided instead to favor memory over processing performance. By making changes directly to the EEPROM/logical format, I could then call the conversion function to create the light/physical format and display the revised cycle. Running this conversion every time took more processing cycles, but, again, these cycles were coming only from a polling loop. I gained the option of scrapping the inverse conversion function because it was never called. Thus, I saved program memory and some development time. I also gained the benefit of having all changes immediately saved in EEPROM as they were made, freeing me from having to manage saving the changes whenever the user moved to a new cycle, wanted to display the pattern, left editing mode, and so on.
Timers
Throwing processing power instead of memory at my problems almost backfired on me. The PIC16F84 has an internal timer, TMR0, with a granularity of 256 cycles and a prescaler as large as 28 (256×256=65,536 cycles). Running with no prescaler, a TMR0 interrupt occurs every 256 cycles. Because I tied the cycling of each pattern to TMR0, each cycle had to complete execution within 256 cycles, or I would've encountered a TMR0 event before I had finished processing the last one, occasionally losing a cycle during a pattern. Increasing the prescaler offers the option of increasing the TMR0 gap by a power of two to prevent overruns. It's also important to keep your interrupt-handling routines clean and simple by processing outside the interrupt as much as possible to avoid delay of interrupts during interrupt handling.
I used a potentiometer to allow users to adjust the speed of the sequencer. Given that you need to do only a little processing between cycles of a pattern, I opted to use an RC clock rather than a crystal. This decision saved the cost of the crystal, system complexity, and an I/O pin; the potentiometer drives the RC clock instead of telling the PIC how fast it should be running and internally managing this speed. If I ever want to enable two sequencers to communicate with each other, I will have to create a protocol that connects two nodes with independent and possibly varying clock speeds.
I wanted several speed options—from fractions of a second to several seconds—for cycling through patterns. The potentiometer gave me a range, so I created two user-selectable speed zones that used different prescalers for TMR0. My design originally supported three zones, but two simplified user selection, cut the speed parameter to 1 bit, and simplified internal management of the TMR0 event.
The difference between a prescaler of 3 (24=16) and 4 (25=32) can get wide, especially with the RC clock cranked to 5 versus 50 kHz. When my design supported three modes, I found that adding (TMR0_COUNT prescaler gave me more control over the interrupt. If you're running the PIC with, say, a 10-MHz clock, you may find this extra prescaler necessary to let you measure useful lengths of time.
Debugging
Given this project to repeat, I would have obtained an emulator for software development. The time saved trying to solve difficult programming would have quickly made up for the expense. Instead, I relied heavily on the MPLAB simulator.
The simulator ran much slower than I expected. Sometimes, I thought the system had frozen as it struggled to get to my breakpoint. Sometimes, I caused a delay, such as when I placed the breakpoint at a point in the code after a polling loop, and the simulator was stuck in the polling loop waiting for me to respond. My design had a main loop shorter than 256 clocks, so I could quickly test iterations and situations. Designs running at 10 MHz probably have a much longer main loop or much more polling.
I found it useful to use Define to create a Simulator label. On the real board, I wanted real delays. However, there's usually no reason to wait forever in a polling or delay loop in the simulator. For example, I had a Flash function that would flash a particular wire W times. In the simulator, I had no need to "see" this flashing. Therefore, I added the following code to skip the useless function and speed simulation:
IFDEF SIMULATOR
return
ENDIF
TMR0 also triggered events. I made all delay amounts named constants so that I could set all simulator constants to zero. I also added bits of code to simulate button presses or other events. Sometimes, waiting for the TMR0 interrupt became insufferable. In my polling loop, then, I added simulator-only code that would set my TMR0 event flag (and then reset TMR0 to zero, just so that it wouldn't eventually trigger on me).
Another useful place for simulator code is at the start of the interrupt handler. My interrupt handler was short enough to leave it at the front of the code, as opposed to calling a function placed elsewhere in memory, which adds latency to the interrupts. However, when I simulated inputs, an interrupt would be called, and I had to step through all of the interrupt code. I added the code:
IFDEF SIMULATOR
call interrupt_handler
return
interrupt_handler
ENDIF
This code created a call that I could step over with a keystroke instead.
I generally found that the MPLAB environment contained many good features, but it pained me to chase down an interface problem. For example, I wanted to test code that required two button presses to reach. MPLAB has a "stimulus-simulator" window through which I could activate input ports. Unfortunately, when the stimulus window is open, the simulator runs significantly slower, so that I usually had to close the window and reopen it a few seconds later. The option of clocking in stimuli seemed more trouble than it was worth because I had to figure out at which clock cycle I wanted the input to trigger. I also had the luxury of seeing whether a light turned on by looking at the appropriate output pin. With any complex output sequence, however, verifying accurate feedback could be difficult.
As useful as the MPLAB environment is, I recommend having access to a quick- and-dirty development environment, such as Basic, to test algorithms. For example, the random-number-generator function in the microchip application-notes library appeared to have unevenly distributed numbers in values of 0 to 255. Testing this function in the Microchip environment seemed difficult because of memory constraints. For example, if I had 256 bytes of RAM, I could easily count the distribution over a long time period. With less than 40H bytes, however, I would have to come up with a clever scheme for counting and perhaps run my simulation several times to see that the spread was even. In Basic, I could pop this code out in minutes and almost immediately see a result.
I encountered other frustrating bugs; if I had used an emulator, I would have saved hours of debugging time.
Because I used a reprogrammable device, I kept having to insert and remove the device into and from the board to put it into the device programmer. I had a socket on the board, but putting the device into the socket meant that I would have to put a socket into a socket, yielding a loose and consequently poor electrical fit. I eventually broke a lead on one device, rendering it useless. I also needed to occasionally bend the lead outward, because pins bent inward make poor connections, and I/O pins would mysteriously stop working. Using an emulator would have eliminated most of my socket problems.
I installed a new inverter that could drive more wire than my current inverter, thus enabling me to simultaneously light several wires. While the design was in program mode, I would flash a strand to represent a parameter. Parameter One worked fine, but Parameter Two wouldn't flash. Because I focused on user-programmable mode and had disabled the other parameters, I thought that some of my changes might have altered the code's ability to flash—say, through using of a variable. After about an hour of chasing phantom code bugs, I finally narrowed down the problem to the actual wire, which I couldn't get to turn off unless I lit another wire. Suddenly, I realized that the new inverter might be the problem. When I hooked up the old inverter, the wires flashed correctly.
The problem occurred because the inverter tended to keep wire lit; some wires—but not others—overdrove the phase gap in the triac driving the wire. I hunted through the code because one wire worked, and the other didn't. I suspected a logical problem and disregarded the possibility of the wire's being at fault because it had always worked before. With an emulator, I would have seen that the port had gone low and that I was facing a hardware, not a logic, problem. I also learned that when I ran a test with a new piece of equipment that I should have checked to see whether a bug existed with the previous setup before I changed the hardware.
It's worth mentioning that I kept a "sanity burn"—several chips that had been burned with old versions of the code. Whenever the prototype started to freak out, I would put in one of these sanity burns. If the prototype worked, then my latest version of the code had broken something critical. If the sanity burn failed to work, something, such as a disconnected wire, was wrong with the hardware.
A nasty set of bugs arose from power-supply issues. I originally powered my prototype using wall-outlet power. However, I got some strange flashing, which I quickly evaluated as a power-source problem. Because the board should operate from a 9V battery, I switched to battery power. Lit wire, however, consumes a lot of power. I also made the unfortunate assumption that my wire would die when the battery was too low. The PIC actually became power-starved first and then acted erratically. I discovered this fact only after chasing down imaginary bugs for a day. Using a fresh battery took care of everything. I wish that I had saved a special version of the code before I tried finding the bug and commented out sections, set up unnecessary debugging hooks, and so on.
To avoid the battery issue, I went back to the wall supply. I then discovered that the flickering was not caused by the power source, as I had assumed. (The wall outlet gave me enough power to see the flicker over my weakened batteries.) Rather, it was a matter of my briefly turning off the lights and displaying them before setting up the next cycle. I changed the code to just move from cycle to cycle without displaying all lights off in between. The strange flicker disappeared.
Another prototype bug I created involved the use of a reset button. I added a reset button on the breadboard prototype so that I could easily reset the board without having to physically disconnect then reconnect the 9V battery. An emulator would have offered the same ability. However, the end product would not have a reset button. Using the prototype, when I wanted to leave program mode, I would press the reset button to restart the board. This reset, however, was not a full-power reset, which meant that flags and memory were not cleared; the only significant change in this case was that the program counter was reset to 0x00. When I finally moved to a manufactured prototype, which a real user would have, I left programming mode by turning the board off and then on again, but the board registered none of my changes. This problem occurred because power-down reset also clears memory.
I list these examples because they illustrate some of the dangers of working with prototypes. On the one hand, you want a clean environment in which to develop so you're not chasing down phantom bugs. You also want access to all the hardware and should be able to probe at will. On the other hand, you need to work with the same hardware that your customer will have to see what's really going to happen.
Delays
I encountered one of the biggest delays in my development cycle when I fried my only controller. I first went to Microchip's Web site to secure a few more chips. Fastest delivery service was three days, which meant I could use the simulator only to develop. I found multiple Internet sites selling PIC chips for less and with optional faster delivery. Although the other sites offered a limited choice of PICs, I did find the PIC16F84, which is nice to develop on and then plan to trade down to a less expensive chip. Given the general availability of PIC microprocessors, I bought a handful of parts without having to establish a relationship with a distributor. I also learned the first rule of inventory: have some. Your time and your product's time to market deserve the insurance of a few extra parts for when something goes wrong.
One of the more destructive sources of delay was feature creep. Before I could finish the basic functions, I had decided upon a range of new features, such as user-programmable patterns, that found their way into the spec. I conceived user-programmable patterns for expert users who needed nonstandard patterns. These patterns also required as much coding-development time as that of the rest of the project. Suddenly, time to market took a hit as I struggled to work out bugs in the expert code. Ironically, beta testers were not interested in the user-programmable modes. In other words, I could have delivered the board to beta testers much sooner and tested the fundamental robustness of the sequencer if I had closed the spec and postponed the expert features to a second release (see sidebar "Classic mix-ups and errors").
The killer NOP
I always feel a thrill of excitement whenever I get a new tool. After I install it, the last thing I want to do is pay special attention to the instructions. I read enough to get started and then promise myself that I'll return and finish later.
Early on, I discovered a strange problem when I tried to implement demo mode: The board would light only six lights. When I removed the code, the board worked correctly. At this point, I started putting pieces of the code back in, testing each time that the board worked. Finally, all but one piece of the code was back in. When I commented out the code, the board ran. Oddly enough, this code should have executed only if I pressed a button. Somehow, I thought, the event is erroneously triggering. When I removed all code but a Goto and Return, the code worked. Adding in a CLRF flag caused failure. But how? Then, I decided to add in a nonoperation (NOP) instruction instead of the CLRF Flag. With the NOP, the board failed. Without the NOP, the code ran.
When a NOP instruction causes a critical error, it usually means that a memory misunderstanding exists. Sure enough, one of my tables crossed the 100H boundary as I slowly added program code in front of it. In this case, a problem arises because the table address becomes 9 bits long; the 14-bit PIC instructions can accommodate extra bits for Goto addresses, but they drop the ninth and 10th bit on arithmetic instructions, such as Add. Because the code I tested was 5 bytes long, the table overran by 4 bytes. (Hence, only six lights showed before light seven set PIC to 00H (Reset) instead of 100H.)
I solved the problem by moving all tables to the front of the code section and then carefully reading the instruction set to see whether any more commands had sneaky nuances the assembler wouldn't catch. Later, when I read Design with PIC Microcontrollers by John Peatman, I discovered that placing tables at the front of memory is standard practice for PICs for this very reason (Reference 2). I quickly read on to see whether there were any other standard practices I should follow. In the area of interrupts, the book saved me a great deal of grief. In the 16- and 32-bit world, for example, flags are automatically saved, and there's a stack to save registers. With the PIC family, you have to save the flags yourself, but you first have to save the accumulator in a tricky way (using Swap) without affecting the flags (Listing 1). I found the disable-interrupt command to be particularly deceiving. Because the PIC has a two-stage pipeline, a problem can occur if an interrupt thus occurs at the same time as the request to disable interrupts. The PIC clears the interrupt flag, as per your instruction, but then executes the pending interrupt. When you return from the interrupt, thus enabling the interrupt flag, interrupts are enabled in your main code, even if you thought you just disabled them (Listing 2).
In the end, I found the PIC spec sheets, although technically complete, to need some clarifications. At least buy a book like Peatman's. He suggests a strong structure for programming that can help you avoid many problems. However, his question sections annoyed me because you have to read a chapter or two ahead to eventually discover the answers. In any case, read the book before you find out that, as with most bugs, you failed to understand the nuances of the architecture.
You may ask how complicated 35 instructions can be. Unfortunately, the reduced instruction set actually results in more complex programs. For a loose analogy, imagine that I tried to write this article without using the letter T. Fewer words make for a less complex vocabulary, but having fewer words available means that it takes longer to say certain things. For example, higher order languages have complex vocabularies that let you describe complex assembly tasks, such as printing or multiplication, in one instruction. With a pared-down instruction set, such as that of the PIC family, even straightforward functions, such as bit-shifting, become more complex. Assembly for the x86, for example, offers commands such as rotate without carry and rotate CL times (Listing 3). When I needed to implement bit-shifting for bit-packing into this project, I had to think the process through. There is no rotate-without-carry instruction, so I had to set the carry flag to the right-most bit before each rotate in the case of right rotate.
Creating functions such as bit-shifting is not difficult; however, you still have to create them. If you've never worked at this level—for example, if you've always had a Print instruction available—it can take awhile to create a fundamental-functions library. You'll also find that, the first time you try, you may be unable to nail the most efficient or flexible way of implementing a function. Thus, my project took longer than I thought as I coded out "fundamental" functions I was used to implementing in the less complex 16- and 32-bit architectures.
I also found that building my fundamental library of functions early increased my ability to lay out the architecture of my sequencer. For example, once I coded basic functions for reading and writing to EEPROM, I started to consider the EEPROM as a resource to use, not just something I later needed to code. This change of perspective prompted me to consider the idea of creating static user parameters that remained set after power-down. EEPROM also opened a bank of usable data memory that was much slower to access but could conceivably get me out of a memory jam.
The hardware/software disconnection
During my years programming 16- and 32-bit architectures, I've always worked with a completed hardware system; that is, I was working on either a PC or a board that had already been designed, printed, stuffed, and tested. Under these conditions, one of my first tasks was to determine the feasibility of a new function. Generally, you can implement any function in software, given that the function meets the constraints of processing speed, memory, and interface. If it weren't for these three constraints, an 8-bit µP could perform videoconferencing, albeit slowly. Once I determined that I had enough processing power and I could perform the task in real time if necessary, enough memory, and the right interfaces, I would write the function.
On this project, however, I was a part of the design team that built the board. Although I have limited hardware skills, I provided a perspective of what was reasonably possible to implement in code. That is, it would have been ludicrous to include hardware interfaces that the PIC simply couldn't process effectively. One function the team developed was an autodetect circuit that determined whether a port actually attached to a wire. Without such a circuit, the PIC could not know how many strands of wire a user wanted to light and cycle through unless he entered this value every time he powered up the board. In contrast, because an RC circuit with a potentiometer clocks the PIC, the PIC never knows how fast it is running. (PIC's designers did not consider this feature necessary or useful.)
Note that we didn't lock in the hardware spec until we paid for the first batch of boards. Given time-to-market pressures, we sent the board out to be screened even though we still had to write code. We finished enough code to satisfy the team, and the hardware worked as we expected, but we hadn't yet put all the bells and whistles in place.
Once we locked in the hardware spec, the entire design atmosphere changed: a disconnection occurred between hardware and software. Until now, the hardware design was the bottleneck. Software was always ready and waiting to test the board. Once we finished the hardware, software suddenly became the bottleneck. Additionally, we no longer discussed what we could do in hardware or software; now, it was all software. The hardware team walked away from the project because there was no more anyone could do. Any hardware "problems," such as inversion of I/O pins or pin layout, were now software problems. One frustrating day, as the hardware team struggled to solve a ground problem, they declared that the board was done and walked away, leaving me with a prototype that failed to run the same code as it had at the beginning of the session. Someone, I discovered, had inverted the input buttons to drive low instead of high but failed to give me this information.
Further, everyone had great ideas for how to improve the functions of the board, which created another challenge. We had a simple hardware foundation that gave software great flexibility. Now that I was under the gun, spec changes and feature creep became epidemic. Two classes of spec changes exist: changes to features and changes to how to offer these features to a user (interface). Of these two, designers usually perceive interface changes as simpler, but they are actually the harder of the two. For example, the sequencer had no display and only two buttons. After some reflection, we realized that we could use the wire itself to communicate information to a user, so I now had a primitive 10-bit display. However, I also had to create a library of functions for "printing" to this display. Having only two buttons created the challenge of trying to provide user access for as many as 10 commands in the user-programmable sequence mode. We came up with the idea of an extended shift key; that is, holding one button down while repeatedly pressing another button. Although this plan let us access many functions, it created complexity in the evaluation of button events. Originally, I flagged a Button 1 or Button 2 event when a button was pressed. Now, I still had to support this mechanism for the code I had written while creating a shifted state that counted Button 2 presses and triggered an even upon Button 1's release. Given that I had to debug this problem in the simulation environment, the fact that extended shifting functioned as an interrupt, and that I had to support the old way of triggering an event, this function was the most complex part of my design.
One of the most annoying aspects of the PIC assembly language is that you must remember bit combinations. For example, documentation for the PIC16F84 says that you can store a calculation in either the accumulator W (0) or the file register in use (1). Or is it the other way around? I didn't realize until I read Peatman's book that, in the F84 include file, PIC's designers have already set the constants W=0 and F=1 for you.
The lure of using constants and macros to simplify code tempted me. For the W/F distinction, it makes sense to use constants. However, I hesitated to use macros wherever possible. For example, I found myself always thrown off by SUBLW and SUBWF. First, the mnemonics are inconsistent: SUBLW subtracts W from a literal (LW), and SUBWF subtracts W from F (should be SUBWF?). I kept having to go back to the manual to get it straight. Additionally, using subtraction to set flags during a comparison always required careful review. In x86 assembly, the flags JA (jump if above), JB (jump if below), and JZ (jump if zero) clearly define the action after a compare. These commands test both the zero and the carry flag in one instruction. You can also test for above, below, and zero in one instruction using the PIC (Listing 4). However, you have to switch between subtracting A from B or B from A to check only the carry flag and not the zero flag. (Swapping the order shifts whether the carry flag accounts for the zero case.)
I considered writing a macro to do my comparisons but realized this task could become complicated given that values could be either data or constants, thus requiring MOVLW or SUBLW. Additionally, I considered one of the detrimental side effects of macros: They create "proprietary" commands. The problem with proprietary commands is that someone reading the code initially finds them confusing and must learn them. Additionally, you'll have trouble writing code in other applications in which you can't use your macros for some reason or have to use someone else's.
Microchip could ease development in this area by managing standard macro libraries. For example, virtually every program uses a compare macro (Reference 3). If you use a standard macro library, the macro becomes a command across the industry instead of a stumbling block between programmers. In its defense, Microchip does provide an impressive application-notes library, (Reference 4) showing how programmers have implemented various functions on PICs. However, it took time to sort through these notes, leaving me with the trade-off of possibly saving time by developing the code myself.
Thinking outside the box
The most important lesson I learned when we took the sequencer to production is that design is not a linear process. Teams usually break into functional groups: for hardware; software; packaging, manufacturing, and testing; and so on. Unfortunately, design dependencies are less neatly segmented. Packaging, for example, proved a rude awakening. The plastic box seemed to cost more than the board, and we would have to mount most of the components on the backside of the board to fit the board into the off-the-shelf box we selected. Each stage of design, regardless of its seeming simplicity, has dependencies and effects on the other stages. If we had looked at boxes before rushing to manufacture the board, we could have laid out the board with a different form factor and widened our possible box options.
All in all, this experience enlightened us. I naively assumed that coding would only take a few days, so I was shocked by how long we overran our projected schedule. Granted, each of us had other full-time jobs and personal lives. However, I seriously underestimated the amount of time it would take me to get up to speed with the PIC. I forgot that I had years of experience with x86 assembly and had already spent the plethora of hours required to understand the intricacies and nuances of that architecture, as well as common logical structures, such as tables. This knowledge gave me a head start with my PIC development, but I still had to rebuild many of the functions I could code without thinking in x86, as well as overcome my biases and assumptions. The nasty thing about assumptions is that it is easy to forget that you ever made them.
The reality is that 8-bit µPs are here to stay and are finding their way into more and more complex applications (Reference 5). In some respects, they are as specialized for certain tasks, such as interface management, sensor monitoring, and serial communication, as audio decoders are for MP3 players. And, in a world where devices continue to become more intelligent, they offer a fair chunk of intelligence for a low cost.
|
Classic mix-ups and errors Bugs are nasty little fellows, especially if they're your own fault. Here are some classic mix-ups and errors I made that I hope you can avoid: Hexadecimal/decimal mix-up: Example: When you use 10 for 10 instead of 0´0a. I found it's best to use one radix and numbering convention throughout the program to avoid ambiguity. Literal/address mix-up: I had plenty of mix-ups with literals and addresses because both are defined as constants. In the 16- and 32-bit world, the assembler usually flags a misuse of a constant. For example, instead of using ADDLW posxw_table, I used ADDWF posxw_table, W. Clearly, this error is mine because I tried to add the byte at that address instead of the offset. With the unfamiliar mnemonics, however, it was easier to make this mistake because I was used to having to use an Offset command instead of explicitly stating that I was using a literal in the add instruction. Crutch functions: Early on, my design needed a delay function. I first wrote one that counted cycles. The problem with making such a quick-and-dirty function was that I continued to use it much longer than I should have. The more useful function, which I knew I was going to eventually have to write anyway, was a TMR0 delay. Because I relied on the crutch function, I avoided writing the TMR0 delay, which defined my interrupt structure and event-flagging mechanism. When I did write the TMR0 delay, I had to adjust and redebug portions that it affected. You should write fundamental and structural functions earlier rather than later. Always clear memory or assume it is undefined: At one point, I encountered what looked like a parameter problem that made me wonder how parameters stored in EEPROM were getting set. When I checked the EEPROM, the parameters were correct, but the program failed to execute according to the parameters. The problem resulted from a shift from physical parameters to logical parameters. EEPROM stores power-up parameters but you can change them in RAM during run mode). For example, you can toggle speed from slow to fast. I had been developing code for demo mode, in which the logical parameters worked (started in slow mode), but they failed when I ran in normal mode (started in fast mode). I caused this problem because I never cleared the logical parameters before I masked in bits; I erroneously assumed that memory started at 0 (especially silly when the chip powers up with 0xffs)! Confirm that all modes still work after a major code change: I often made changes in demo mode or programming mode and then found errors when I returned to run mode. Modes make different assumptions about the total system state, and it pays to confirm that your code violates none of these assumptions after you've spent time making major changes in other sections of the code. Understand the device's architecture: There's nothing as embarrassing or frustrating as realizing that the chip doesn't work the way you assumed it would. I've illustrated several of the surprises the PIC family had for me. Although I may plead that Iwas using inadequate documentation, I didn't understand the chip, and I lost valuable development time. Many of the lessons I learned in the 16- and 32-bit world didn't carry over into the 8-bit realm. Referencing the spec sheet only when I couldn't figure out something was not conducive to learning the PIC architecture. Understand your own hardware: Once or twice, the hardware guys did a task differently from how they said they would. Early on, I wished that I had created a dummy program that simply tested characteristics of the hardware, such as whether it drove outputs high or low, so I could confirm that no one had "fixed" or "improved" the circuit on my prototype board. This program is also similar to a "sanity burn" to confirm that a failed test run is a result of your code and not because of a hardware failure or change. |
Author info
REFERENCE
1.Cool Neon Wire,www.coolneon.com.
2. Peatman, John, Design with PIC Microcontrollers, Prentice Hall
3. Nagel, Dennis, "Comparison macro for PIC processors," EDN, March 2, 2000, pg 122.
4. Microchip Application Notes, www.microchip.com.
5. Levy, Markus, "C for yourself: programming 8-bitters" EDN, Jan 6, 2000 pg 101.





















