leddrvr - Copyright (c) 2002 Tundraware Inc., All Rights Reserved $Id: readme.txt,v 1.4 2002/03/25 17:12:37 tundra Exp tundra $ ************************************************************************ PLEASE NOTE: Before using anything in this archive, you must read, and agree to abide by, the licensing terms found in: leddrvr-license.txt ************************************************************************ FILE DESCRIPTIONS ================= Makefile Unix-style makefile which runs under DJGPP and should also work under MKS and Cygwin. README.txt This file. leddrvr-license.txt Licensing terms for using this software. leddrvr-sch.gif leddrvr schematic in GIF format. leddrvr-sch.png leddrvr schematic in png format. leddrvr-sch.ps leddrvr schematic in PostScript format. leddrvr-sch.sch leddrvr schematic in Eagle format. leddrvr.asm leddrvr software written in PIC assembler. leddrvr.hex The assembled leddrvr software. leddrvr.lst leddrvr listing file generated by assembler. leddrvr.xrf leddrvr cross-reference file generated by assmbler. WHAT IS 'leddrvr'? ================== 'leddrvr' is a simple 7-segment LED display driver system using PIC technology. The idea is to use minimum parts count to drive up to 4, 7-segment LEDs. This is also a useful introduction to some of the PIC microprocessor programming techniques insofar as 'leddrvr' exercises many of the basic features of these chips including: - Programming & use of the TMR0 timer & prescaler - Timer-based interrupt handling - Asynchronous application/interrupt interaction. - Display multiplexing HOW DOES THE HARDWARE WORK? =========================== Like many PIC-based projects, this system is realized with a minimum parts count. Besides the usual clock and MCLR pull up circuitry, 'leddrvr' is realized by having PORTA connected to the individual LED segments and PORTB connected to the LED selects (cathodes). Several notes: - The convention used in both the hardware and software design is that 'LED0' is the Least Significant Digit (LSD or rightmost) and 'LED3' is the Most Significant Digit (MSD or leftmost). - The schematic and software assume a "common cathode" type 7-segment display, whose individual segments are acceptably bright at less than 25ma current. - R2-R9 should be selected so that the PIC is never required to deliver more than its rated current (25ma) to an LED segment. A typical value here is probably between 150-330 ohms. - Although not shown on the schematic, a given segment position on each LED is wired to the corresponding position on the other LEDs and then connected to the PIC. For example, all the 'a' segments are connected to each other and RB0. In effect, this means when a segment pattern is output by the PIC it is sent to *all* the displays. However, this pattern only actually appears on those displays selected via an 'on' bit on RA0-RA3. - RA0-RA3 are used to select which of the digits are to display the segment pattern currently presented on RB0-RB7. This is how the software is able to multiplex the display. Each time a TMR0 overflow interrupt occurs (1ms intervals), the software determines the digit to update, presents its current segment pattern, and then selects that digit as 'on' via its corresponding cathode transitor. Because this update takes places very rapidly, all digits appear to be displayed simultaneously when, in fact, only one is on at a time. HOW DOES THE SOFTWARE WORK? =========================== The best way to understand the software is to read the code which contains a fair number of explanatory comments. At a high-level, here's what's going on: - At startup, the chip jumps to memory location 0 which contains a jump to the mainline code ('main:'). This is necessary because the PIC chip expects to find an interrupt handler at location 4 and we have to jump around that handler. - The first thing done is to call the 'init:' subroutine which initializes the hardware, the timer, and various program variables to run properly. - Then we sit in a loop running our 'application software'. In this case, our 'application' does nothing more than increment at 16-bit count stored 'dsply_val' array. It then calls the 'wait:' subroutine to kill some time. This is done so that you can actually watch the count on the display. Try removing the call to 'wait:' and see what happens - the rightmost digits are changing so fast you cannot distinguish the individual count. - 'init:' setup the prescaler and TMR0 so that the timer count overflows every 1ms - i.e., A TMR0 interrupt is generated 1000/sec. The Interrupt Service Routine (ISR) which services these interrupts is found beginning at location 4. - Before the ISR can do anything useful, it must save the current state or "context" of the system. This is then restored when we exit the ISR so that we do not clobber any registers in use by the main program that was interrupted. - The TMR0 ISR does two things: 1) It increments a wait count which is used by the 'wait:' subroutine to kill time. You can set how long each call to 'wait:' will be by changing the 'count_delay' constant. 2) It determines whose turn (which digit) it is to be displayed and does so via 'display_led'. - Notice that the main application and ISR are 'asynchronous' to each other. That is, they never call each other directly or wait for one another. They work independently in time from one another. So, they have to have a way to communicate. They do so by sharing the 'dsply_val' array and 'wait_count' variable. The 'led_display' routine assumes that the value it is expected to display is stored (in binary) in the 'dsply_val' array, starting with the Least Significant Digit and moving towards the Most Significant Digit as it moves down the array. Any application is free to update this array as it sees fit - in our example case, we just count up in binary - and the Interrupt Service Routine will see to it that the display is updated accordingly. Similarly, the 'wait:' subroutine primes the 'wait_count' variable with the desired waiting time and then periodically inspects it to see if it has decremented to 0. However, it is logic in the Interrupt Service Routine that actually does the decrementing countdown. TIMING ISSUES ============= There are several places where timing can be tricky, which, although not a problem here, probably should be mentioned: - It is important that the 'wait:' subroutine be short enough so that any change in 'wait_count' can be read before the next TMR0 interrupt happens and the variable is updated again. If this is not the case, 'wait:' may not detect 'wait_count' decrementing to 0 and end up waiting longer than it should. In the worst case, 'wait:' could be just long enough so that it ends up in sync with the code in the ISR that modifies 'wait_count'. In that case 'wait:' would *never* see the decrement to 0 and it would thus end up becoming an infinite wait. So long as 'wait:' runs in much less time than the TMR0 interrupt interval, AND it is not being interrupted so frequently by other interrupt sources so as to miss the TMR0 interrupt which updates the count, this is not a problem. To avoid this problem entirely, the approach used here is to prevent 'wait_count' count from rolling over from 0x00 to 0xff. Even if 'wait:' misses one or more interrupts, it will eventually detect 'wait_count' == 0 and end the timing loop. - Commonly, the TMR0 interrupt rate (in this case 1000 per second) is not exactly the rate at which its service routine runs. This is because interrupts are disabled during the ISR and only enabled again *after* the interrupt has been serviced. This means that the effective TMR0 interrupt service interval is slightly *more* than 1ms. It ends up being 1ms + Time To Service TMR0 Interrupt because the TMR0 timer is not running during the ISR. It is possible to enable selected interrupts while an ISR is running, but this makes things much more complex because you have to do a context save *per interrupt*. This is hard to do on a PIC because it is not a stack-based architecture and there are very limited amounts of dynamic memory for storing such things. So, it is highly preferred that interrupts stay off during an ISR thereby guaranteeing that we only need to save a single context at a time. Having said this, it is possible to selectively enable interrupts while running an ISR is you are certain that: * You will *never* see another interrupt among those you are enabling while you are still in the ISR. This guarantees that you only need a single context save area. OR * You parse for the various kinds of interrupts coming in and provide a separate context save area for each. In 'lddrvr' we *enable* the TMR0 interrupt during the ISR because we know the ISR execution time (about 70us) is far less than the TMR0 interrupt interval (1ms). We restart the TMR0 timer before actually servicing the interrupt to keep the timer interval as close to 1ms as possible. If we did not do this, the TMR0 interrupt interval would end up being 1ms + ISR Computation Time. Try it - stub out the interrupt enabling code in the TMR0 ISR and you'll see that the effective timer rate ends up being something around 1.07ms. (You can measure this by connecting an oscilloscope to any one of the 2N700 gates and watching the pulse train there. Just remember to divide those timings by 4 since you only see every 4th interrupt at a given transistor.) HOW COULD 'leddrvr' BE IMPROVED? ================================ The big problem with this design is that it uses up all but one of the available I/O pins on the 16F84A PIC. This really limits how useful 'leddrvr' is in its current form (although it can be used to build timers and stopwatches pretty much as-is). There are a number of ways we could solve this problem with some hardware changes: - Use a PIC with more I/O pins. - Use a decoder to select the LED cathode enable. With 4 LEDs we only actually need 2 bits to select them uniquely, rather the 4 we are currently using. - Use an 8-bit shift register to select the segments for display. This can be done with only 2 I/O pins instead of the 8 we currently use. - Use a dedicated LED driver system like the Maxim MAX7219 to offload all this work from the PIC. This requires only 3 I/O pins total because the MAX7219 uses a serial interface. There are also some things that could be improved in the software: - There are probably some small optimizations possible to reduce code size and improve speed slightly. - There are probably bugs in this code. If you find any, let me know. Me email address is at the end of this file. I'D LIKE TO THANK THE ACADEMY... ================================ Lots of nice people on Usenet answered my stupid questions as I've been learning the PIC. Their help is very much appreciated. You should feel free to poke around the code, change things, and generally experiment with what is there. It's the best way I know to learn something new. If you have any other thoughts / fixes / questions / improvements for 'leddrvr', please let tundra@tundraware.com know. Happy Hacking! ...