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! ...