Scanning LED displays
Another staple of embedded user interfaces is scanning LED displays using firmware. The simple process of lighting each display, one at a time, and using the principle of persistence of vision to blur the individual digits into one seemingly continuously lit display. Originally, the practice was used to reduce cost by cutting down on the number of BCD to 7 segment converter chips, it now accomplishes the same saving by limiting the number of microcontroller GPIO used to drive the display.
So, what’s involved? Well, there are a couple of things that have to happen;
- The amount of time each display is lit must be equal to the lit time for all the other displays, if the intensity is to be consistent.
- The amount of current driven through the display must be correspondingly larger to maintain the correct intensity. Specifically, if the display is multiplexed 4:1, (4 displays) then the current must be 4x the continuous drive current.
- To prevent flicker, each display must be driven at least 2-3x the power line frequency. If it is not, then the beat frequency between the flashes in a fluorescent lamp, and the display, will be visible.
- To provide the higher drive current, the digit select must be capable of delivering 8x the individual segment current, for a total of 16-24x the DC segment drive current. This typically requires a discrete transistor for each digit drive.
- If a current limiting driver is not used, then a current limiting resistor will be needed, one for each common segment. Don’t try to use a single resistor on the digit common, it will vary the intensity of the display based on the number of segments lit.
Alright, so we have a group of digits configured for scanning, with individual GPIO for each of the segments, and additional GPIO (with transistor drivers) for each of the display commons. What’s involved in generating the software?
The following listing is a basic display scanning routine.
#define CH0 0b00111111 // THIS SECTION BUILDS A SET OF DEFINES FOR THE
#define CH1 0b00000110 // BCD TO 7 SEGMENT DECODER
#define CH2 0b01011011 // IT GENERATES DECODING FOR 0-9 PLUS HELP AND A BLANK
#define CH3 0b01001111
#define CH4 0b01100110 // NOTE: THE DECODER IS DONE THIS WAY TO COMPENSATE FOR
#define CH5 0b01101101 // THE NORMAL PIN SCRAMBLING THAT OCCURS DURING THE BOARD
#define CH6 0b01111101 // LAYOUT
#define CH7 0b00000111
#define CH8 0b01111111
#define CH9 0b01101111
#define CHH 0b01110110
#define CHE 0b01111001
#define CHL 0b00111000
#define CHP 0b01110011
#define CHb 0b00000000
#define DIG0 0b00001000 // THIS SECTION BUILDS ANOTHER SET OF DEFINES FOR THE
#define DIG1 0b00000100 // INDIVIDUAL DIGIT DRIVERS
#define DIG2 0b00000010
#define DIG3 0b00000001
// THIS SECTION TAKES THE DEFINES AND CREATES CONSTANT ARRAYS FOR ACCESSING THE DATA
const unsigned char SEGMENT[] = {CH0, CH1, CH2, CH3, CH4, CH5, CH6, CH7, CH8, CH9, CHH, CHE, CHL, CHP, CHb};
const unsigned char DRIVE[] = {DIG0, DIG1, DIG2, DIG3};
// THIS SECTION DEFINES THE DIGIT COUNTER TO KEEP TRACK OF WHICH DISPLAY IS CURRENT
// AND ANOTHER DATA ARRAY FOR THE ACTUAL DISPLAY, IN THIS CASE 4 DIGITS
static signed char DIGIT; // Digit counter
static unsigned char VALUES[4] = {0,0,0,0}; // VALUES TO BE DISPLAYED
void init(void)
{
DIGIT = 3; // THE DIGIT COUNTER STARTS AT 3 AND COUNTS DOWN
PORTD = 0xFF; // PORT D IS THE SEGMENT DRIVE FOR THE DISPLAY
TRISD = 0×00;
PORTA = 0×00; // PORT A IS THE DIGIT DRIVE FOR THE DISPLAY
TRISA = 0xF0;
OPTION = 0b10000011; // TIMER0 PROVIDES A CONSISTEN TIMEBASE FOR SCANNING
TMR0 = 0; // THE DISPLAY
T0IE = 1;
T0IF = 0;
GIE = 1;
}
static void interrupt isr(void) // THIS IS THE TIMER INTERRUPT FUNCTIONS
{
T0IF = 0; // FIRST OF ALL CLEAR THE INTERRUPT
PORTA = 0b00000000; // BLANK THE DISPLAY TO PREVENT GHOSTING
PORTD = SEGMENT[VALUES[DIGIT]]; // PUT OUT THE SEGMENT INFO FOR NEXT DIGIT
PORTA = DRIVE[DIGIT]; // TURN ON THE NEXT DISPLAY
DIGIT–; // DECREMENT TO THE NEXT DISPLAY RIGHT
if (DIGIT < 0) DIGIT = 3; // IF ROLL OVER, RESET THE DIGIT VALUE
}
Let’s take a look at the various sections;
First of all are the #defines that give us a label for the specific bit combination for each display and each number. This is done to make the function more portable, if the individual bit combinations are hard-coded into the routine, then it would require editing the routines for each new system. Using the #defines, all we have to do is update them for the new bit order and the compiler takes care of the rest.
The same is true of the digit drivers. The #defines allow us to redefine which bits in the port are tied to the display, and it allows us to change the polarity of the drive with only a simple change of the #define.
Note: The port used for both the segments and digits assumes that all unused bits are inputs. If this is not the case, then you will have to AND off the old bits and OR on the new ones instead of just assigning a new value.
OK, so we have a basic scanning interrupt setup, what else do we need? Well, one additional feature that is nice to have is leading zero blanking. You know, instead of 0021, we would much rather have just 21. This is accomplished by using a simple flag, flags.blank. At roll over, the flag is set to indicate all zero values are blanked, and each time the interrupt is called, if the flags is set and if the digit to be displayed is zero, then the segments are left off. Now you know why we started at digit 3 and counted down, this allows us to blank the display of leading zeros until we hit a non-zero value.
If the value of the digit to be displayed is not zero, then the flag flags.blank is reset and all subsequent digits are displayed even they are zero. This allows us to display 100 and 1 0 0 without the blanking routine turning it into just 1 and two blank spaces. Finally, digit zero is always displayed even if the total number is zero. The modified code is shown below;
Struct // THIS STRUCTURE HOLDS THE LEADING ZERO FLAG
{
char blank:1;
char balance:7;
} flags;
static void interrupt isr(void)
{
T0IF = 0;
PORTA = 0b00000000;
PORTD = SEGMENT[VALUES[DIGIT]];
if (VALUES[DIGIT] > 0) flags.blank = 0; // IF A NON-ZERO VALUE, CLEAR BLANKING
if (DIGIT == 0) flags.blank = 0; // IF DIGIT 0, CLEAR BLANKING
if (flags.blank == 1) PORTD = 0; // IF BLANKING IS STILL SET, BLANK DIGIT
PORTA = DRIVE[DIGIT];
DIGIT–;
if (DIGIT < 0)
{
DIGIT = 3;
flags.blank = 1; // AT ROLL OVER SET THE BLANKING FLAG AGAIN
}
}
Alright, now we have an Timer0 interrupt triggering an LED display scanning routine, and it has leading zero suppression; what if I have to display a decimal point? That affects leading zero suppression and my segment drive, how do we fold that into the routine. And, we have to contend with a decimal point drive that shares the GPIO of the segment drive.
Well, first of all, we need a couple of bits to determine where the decimal point lies in the display, remember to account for left or right hand locations. They are both driven off the same digit common, but one leads the display and one lags. The routines below assume a lagging, or right handed decimal point display.
Struct // THIS STRUCTURE HOLDS THE LEADING 0 FLAG & decimal point
{
char ` blank:1;
char decpnt:2;
char balance:5;
} flags;
static void interrupt isr(void)
{
T0IF = 0;
PORTA = 0b00000000;
PORTD = SEGMENT[VALUES[DIGIT]];
if (flags.decpnt == DIGIT) // HERE WE CHECK THE LOCATION OF DECIMAL PT
{
PORTD |=0×80; // IF IT IS HERE, THEN ADD TO THE SEGMENT
flags.blank = 0; // ALSO TURN OFF BLANKING FOR 0.XX
}
if (VALUES[DIGIT] > 0) flags.blank = 0;
if (DIGIT == 0) flags.blank = 0;
if (flags.blank == 1) PORTD = 0;
PORTA = DRIVE[DIGIT];
DIGIT–;
if (DIGIT < 0)
{
DIGIT = 3;
flags.blank = 1;
}
}
The result is essentially the same interrupt service routine, we just have to turn on the decimal point at the right time. Note, we also have to turn off the blanking for any zero leading the decimal point.
For routines with left hand decimal points, we have to check for both the condition of when the decimal point value equal DIGIT-1, and when the decimal point equals DIGIT. At DIGIT we have to turn off the blanking, and at DIGIT+1 we have to turn on the decimal point for the digit to the left.
OK, there you have it, a simple display driver routine that handles scanning the displays, leading zero suppression, and the decimal point, all in less than 60 lines of C code. It is reasonably portable, and once it is initialized, it is basically invisible to the main program.
