Newer
Older
pic-leddrvr / readme.txt
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! ...