You are on page 1of 18

© Gooligum Electronics 2014 www.gooligum.com.

au

Introduction to PIC Programming


Programming Enhanced Mid-Range PICs in C

by David Meiklejohn, Gooligum Electronics

Lesson 10: Driving 7-Segment Displays

Up until now, we have only used a small number of individual LEDs as outputs. LEDs are adequate as
simple indicators, but many applications need to be able to display information in numeric, alphanumeric or
graphical form. Although LCD and OLED displays are becoming more common, there is still a place, when
displaying numeric (or sometimes hexadecimal) information, for 7-segment LED displays.
This lesson shows how lookup tables can be used to translate a numeric digit into the specific pattern of
segments needed to display that digit. And we’ll see how multiplexing makes it possible for a device with
only 14-pins, such as the PIC16F1824, to drive multiple 7-segment displays.
In summary, this lesson covers:
 Using lookup tables to drive a single 7-segment display
 Using multiplexing to drive multiple displays
with examples implemented using XC8 (running in “Free mode”).

Driving a 7-segment LED Display


A 7-segment LED display is simply a collection of LEDs, typically one per segment (but often having two or
more LEDs per segment for large displays), arranged in a “figure 8” pattern. 7-segment display modules
also commonly include one or two LEDs for decimal points.

7-segment LED display modules typically come in one of two varieties: common-anode or common-cathode.

In a common-cathode module, the cathodes belonging to each segment are wired together within the module,
and brought out through one or two (or sometimes more) pins. The anodes for each segment are brought out
separately, each to its own pin. Typically, each segment would be connected to a separate output pin on the
PIC, as shown in the circuit diagram on the next page1.
The common cathode pins are connected together and grounded.

1
The segment anodes are connected to PIC pins in the (apparently) haphazard way shown, because this reflects the
connections on the Gooligum training board. You’ll often find that, by rearranging your PIC pin assignments, you can
simplify your PCB layout and routing – even if it makes your schematic messier!

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 1


© Gooligum Electronics 2014 www.gooligum.com.au

To light a given segment in a


common-cathode display, the
corresponding PIC output is set
high. Current flows from the
output and through the given
segment (limited by a series
resistor) to ground.
In a common-anode module, this
is reversed; the anodes for each
segment are wired together and
the cathodes are connected
separately.
In that case, the common anode
pins are connected to the positive
supply and each cathode is
connected to a separate PIC
output.
To light a segment in a common-anode display, the corresponding PIC output is set low; current flows from
the positive supply, through the segment and into the PIC’s output.

Although a single pin can source or sink up to 25 mA, the combined maximum for all the pins on the
16F1824 (industrial version) is 170 mA and since all segments may be lit at once (when displaying ‘8’), we
need to limit the current per pin to 170 mA ÷ 7 = 24 mA. The 330 Ω resistors limit the current to 10 mA,
well within spec while giving a bright display, and allowing plenty of overhead for adding LEDs or other
outputs on the unused pins, in future.

Lookup tables
To display each digit, a corresponding pattern of segments must be lit, as follows:

Segment: A B C D E F G
Pin: RC1 RC2 RC4 RC3 RA4 RA1 RA0
0 on on on on on on off
1 off on on off off off off
2 on on off on on off on
3 on on on on off off on
4 off on on off off on on
5 on off on on off on on
6 on off on on on on on
7 on on on off off off off
8 on on on on on on on
9 on on on on off on on

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 2


© Gooligum Electronics 2014 www.gooligum.com.au

We need a way to determine the pattern corresponding to the digit to be displayed, and that is most
effectively done using a lookup table.

In C, a lookup table would usually be implemented as an initialised array. For example:


uint8_t days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

The problem with such a declaration for XC8 is that the compiler has no way to know whether the array
contents will change, so it is forced to place such an array in data memory (which even in large 8-bit PICs is
a very limited resource) and add code to initialise the array on program start-up – wasteful of both data and
program space.
If instead the array is declared as ‘const’, the compiler knows that the contents of the array will never
change, and so can be placed in ROM (program memory).

For example, to store the binary patterns to be applied to PORTA and PORTC, corresponding to each digit,
we could use the following array declarations:
// pattern table for 7 segment display on port A
const uint8_t pat7segA[10] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011 // 9
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[10] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110 // 9
};

Looking up the display patterns is easy; the digit to be displayed is used as the array index.
To set the port pins for a given digit, we then have:
LATA = pat7segA[digit]; // lookup port A and C patterns
LATC = pat7segC[digit];

Alternatively, we could use a single lookup table with patterns specifying all seven segments of the display,
and to then extract the parts of each pattern corresponding to various pins.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 3


© Gooligum Electronics 2014 www.gooligum.com.au

For example:
// pattern table for 7 segment display on ports A and C
const uint8_t pat7seg[10] = {
// RC4:1,RA4,RA1:0 = CDBAEFG
0b1111110, // 0
0b1010000, // 1
0b0111101, // 2
0b1111001, // 3
0b1010011, // 4
0b1101011, // 5
0b1101111, // 6
0b1011000, // 7
0b1111111, // 8
0b1111011 // 9
};

Bits 6:3 of each pattern provide the PORTC bits 4:1, so to get the value for PORTC, shift the pattern two
bits to the right, and mask off bit 0:
LATC = (pat7seg[digit] >> 2) & 0b011110;

Extracting the bits for PORTA is a little more difficult.


Pattern bit 2 gives the value for RA4. To extract that bit (by ANDing with a single-bit mask) and shift it to
position 4 (corresponding to RA4), we can use the expression:
(pat7seg[digit] & 1<<2) << 2

Pattern bits 1:0 give the values of PORTA bits 1:0 (RA1 and RA0). We don’t need to do any shifting; the
bit positions already align, so to extract these bits, we can simply AND them with a mask:
(pat7seg[digit] & 0b00000011)

Finally, we need to OR these two expressions together, to build the value to load into PORTA:
LATA = (pat7seg[digit] & 1<<2) << 2 |
(pat7seg[digit] & 0b00000011);

Whether you would choose to do this in practice (it seems quite clumsy here) is partly a matter of personal
style, and also a question of whether the space savings, from using only one pattern array, are worth it; we’ll
compare the program space used by both approaches after the following example.

Example 1: Single-digit seconds counter


To demonstrate these table lookup and display digit routines, we will implement a seconds counter, using the
single-digit circuit shown on page two.
It will simply count repeatedly from 0 to 9 on a single 7-segment display, with approximately 1 second
between each count.

If you are using the Gooligum training board, you can build the circuit by:
 placing shunts (six of them) across every position in jumper block JP4, connecting segments A-D, F
and G to pins RA0-1 and RC1-4
 placing a single shunt in position 1 (“RA/RB4”) of JP5, connecting segment E to pin RA4
 placing a shunt across pins 1 and 2 (“GND”) of JP6, connecting digit 1 to ground.
All other shunts should be removed.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 4


© Gooligum Electronics 2014 www.gooligum.com.au

If you are using Microchip’s Low Pin Count Demo Board, you will have to supply your own 7-segment
display module, and connect it (and the current-limiting resistors) to the board. This can be done via the 14-
pin header on the Low Pin Count Demo Board.
Be careful, because your 7-segment display module may have a different pin-out to that shown above.
If you have a common-anode display, you will need to wire it correctly and make appropriate changes to the
code presented here, but the techniques for driving the display are essentially the same.

Complete program
The following program incorporates the code fragments presented above, along with code and techniques
from previous lessons:
/************************************************************************
* *
* Description: Lesson 10, example 1b *
* *
* Demonstrates use of lookup tables to drive a 7-segment display *
* *
* Single digit 7-segment display counts repeating 0 -> 9 *
* 1 second per count, with timing derived from int RC oscillator *
* (single pattern lookup array) *
* *
*************************************************************************
* *
* Pin assignments: *
* RA0-1,RA4, RC1-4 = 7-segment display bus (common cathode) *
* *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 500000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, internal oscillator (no clock out), 4xPLL off
#pragma config MCLRE = ON, FOSC = INTOSC, CLKOUTEN = OFF, PLLEN = OFF
// no watchdog timer, brownout resets enabled, low brownout voltage
#pragma config WDTE = OFF, BOREN = ON, BORV = LO
// no power-up timer, no failsafe clock monitor, two-speed start-up disabled
#pragma config PWRTE = OFF, FCMEN = OFF, IESO = OFF
// no code or data protect, no write protection
#pragma config CP = OFF, CPD = OFF, WRT = OFF
// stack resets on, high-voltage programming
#pragma config STVREN = ON, LVP = OFF

/***** LOOKUP TABLES *****/

// pattern table for 7 segment display on ports A and C


const uint8_t pat7seg[10] = {
// RC4:1,RA4,RA1:0 = CDBAEFG
0b1111110, // 0
0b1010000, // 1
0b0111101, // 2
0b1111001, // 3
0b1010011, // 4

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 5


© Gooligum Electronics 2014 www.gooligum.com.au

0b1101011, // 5
0b1101111, // 6
0b1011000, // 7
0b1111111, // 8
0b1111011 // 9
};

/***** MAIN PROGRAM *****/


void main()
{
uint8_t digit; // digit to be displayed

/*** Initialisation ***/

// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b0111; // internal oscillator = 500 kHz

/*** Main loop ***/


for (;;)
{
// display each digit from 0 to 9 for 1 sec
for (digit = 0; digit < 10; digit++)
{
// display digit by extracting pattern bits for all pins
LATA = (pat7seg[digit] & 1<<2) << 2 | // RA4
(pat7seg[digit] & 0b00000011); // RA0-1
LATC = (pat7seg[digit] >> 2) & 0b011110; // RC1-4

// delay 1 sec
__delay_ms(1000);
}
}
}

The following table compares the source code length and resource usage for the version of this example with
two lookup tables with direct port updates, versus the version (above) that used a single combined lookup
array with more complex pattern extraction for each port.
Count_7seg_x1

Source code Program memory Data memory


Compiler Lookup tables
(lines) (words) (bytes)

XC8 v1.30 (Free mode) per-port 41 69 5


XC8 v1.30 (Free mode) combined 30 82 5

As you can see, the single combined lookup table version is shorter (fewer lines of source code) than the
table-per-port version. But even with only one table in memory, the XC8 compiler still generates larger code
than the two-table version – due to the instructions needed to extract the patterns from each array entry.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 6


© Gooligum Electronics 2014 www.gooligum.com.au

In this case, the added complexity of the code needed to extract bit patterns from a combined lookup array
isn’t worth it – the generated code becomes bigger overall, and the combined-table version is arguably harder
to understand. Nevertheless, if the lookup tables were much longer (say 50 entries instead of 10), it would
be a different story – the space saved by storing only a single table in memory would more than make up for
the extra instructions needed to decode it. Sometimes you simply need to try both ways, to see what’s best.

Interrupt-driven Multiplexing
To display multiple digits, as in (say) a digital clock, the obvious approach is to extend the method used
above for a single digit. That is, where one digit requires 7 outputs, two digits would apparently need 14
outputs; four digits would need 28 outputs, etc. At that rate, you would very quickly run out of output pins,
even on the bigger PICs!
A technique commonly used to conserve pins is to multiplex a number of displays (and/or inputs – a topic
we’ll look at another time).
Display multiplexing relies on speed, and human persistence of vision, to create an illusion that a number of
displays are on at once, whereas in fact they are being lit rapidly in sequence, so quickly that it appears that
they are all on continuously.
To multiplex 7-segment displays, it is usual to connect each display in parallel, so that one set of output pins
on the PIC drives every display at once, the connections between the modules and to the PIC forming a bus.
If the common cathodes were all grounded, every module would display the same digit (feebly, since the
output current would be shared between them).
To enable a different digit to be displayed on each module, the individual displays need to be switched on or
off under software control, and for that, transistors are usually used, as illustrated below:

Note that it is not possible to connect the common cathodes directly to the PIC’s outputs; the combined
current from all the segments in a module will be up to 70 mA – too high for a single pin to sink. Instead,
the output pin is used to switch a transistor on or off.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 7


© Gooligum Electronics 2014 www.gooligum.com.au

Almost any NPN transistor2 could be used for this, as is it not a demanding application. It’s also possible to
use FETs; for example, MOSFETs are usually used to switch high-power devices.
When the output pin is set ‘high’, the transistor’s base is pulled high, turning it ‘on’. The 1 kΩ resistors are
used to limit the base current to around 4.4 mA – enough to saturate the transistor, effectively grounding the
module’s common cathode connection, allowing the display connected to that transistor to light.
These transistors are then used to switch each module on, in sequence, for a short time, while the pattern for
that digit is output on the display bus. This is repeated for each digit in the display, quickly enough to avoid
visible flicker (preferably at least 70 times per second).

To implement this circuit using the Gooligum training board:


 keep the six shunts in every position of jumper block JP4, connecting segments A-D, F and G to pins
RA0-1 and RC1-4
 keep the shunt in position 1 (“RA/RB4”) of JP5, connecting segment E to pin RA4
 move the shunt in JP6 to across pins 2 and 3 (“RC5”), connecting digit 1 to its transistor
 place shunts in jumpers JP8, JP9 and JP10, connecting pins RC5, RA5 and RC0 to their respective
transistors
All other shunts should be removed.

The approach taken in the single-digit example above – set the outputs and then delay for 1 sec – won’t
work, since the display multiplexing has to continue throughout the delay.
Ideally the display multiplexing would be a “background task”; one that continues steadily while the main
program is free to perform tasks such as responding to changing inputs. As we saw in lesson 5, that’s an
ideal application for timer-based interrupts.
A timer, such as Timer0, would be used to trigger a regular interrupt. The interrupt service routine displays
each digit, one at a time, in succession. If the interrupt is triggered at (say) 1 ms intervals, one digit would be
displayed for 1 ms, then the next digit for another 1 ms, and so on. If there are three digits, as in the circuit
above, each digit will be on for 1 ms, then off for 2 ms, with the whole display being refreshed every 3 ms.

But before going all the way to a three-digit, multiplexed example, let’s start by converting the previous
single-digit example, so that a timer-based interrupt, running every 2 ms, is used to maintain the display.
The main loop will do nothing more than increment the ‘digit’ variable, which the ISR will display3.
This means that ‘digit’ has to be declared as a global variable, before main() or the interrupt function, so
that it can be accessed by both the ISR and the main program:
/***** GLOBAL VARIABLES *****/
uint8_t digit = 0; // digit to be displayed by ISR

The variable is initialised to ensure that it holds a defined, legal value (between 0 and 9) when the ISR,
which references it, runs.

2
If you had common-anode displays, you would normally use PNP transistors as high-side switches (between VDD and
each common anode), instead of the NPN low-side switches shown here.
3
It is usually a good idea, when developing an application, to build it up one piece at a time, so that you can test each
section, concept or algorithm, as you go.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 8


© Gooligum Electronics 2014 www.gooligum.com.au

We’ll use the digit labelled ‘DIGIT3’ in the circuit above. To enable that digit, we need to raise RC0.

Note, however, that when we write a looked-up pattern value to LATC, the whole of LATC is overwritten,
not only the bits used for the display.
That means that the following sequence will not work:
LATCbits.LATC0 = 1; // raise RC0 to enable display
LATA = pat7segA[digit]; // output port A and C patterns
LATC = pat7segC[digit];

This is because the final ‘LATC = pat7segC[digit]’ overwrites every bit of LATC, including RC0.

If you want the digit enable operation to stick, you have to put it after the write to LATC:
LATA = pat7segA[digit]; // output port A and C patterns
LATC = pat7segC[digit];
LATCbits.LATC0 = 1; // raise RC0 to enable display

This will work correctly.

It is good practice to encapsulate the pattern table definitions, along with the code responsible for outputting
those patterns, as a function:
void set7seg(uint8_t digit); // display digit on 7-segment display

The function is then self-contained – only the function needs to “know” about the pattern tables; they are
never accessed directly from other parts of the program, and if we ever wanted to change the way that a digit
is displayed, we’d only have to modify this function. This approach also makes it easier to reuse the code in
other programs.
So we have:
/***** Display digit on 7-segment display *****/
void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[10] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011 // 9
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[10] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 9


© Gooligum Electronics 2014 www.gooligum.com.au

0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110 // 9
};

// lookup pattern bits and output them (via port latch registers)
LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

Because we’re writing the whole of PORTA and PORTC (via their output latches) in this function, the
“digit enable” bits, corresponding to RA5, RC0 and RC5, are always cleared (because the digit pattern
tables contain ‘0’s for these bits). Thus, all the displays are blanked whenever a new digit is output.
So, after calling this function, we must enable the display:
// display digit
set7seg(digit); // output digit
DISP_EN = 1; // enable display

Where the symbol ‘DISP_EN’ has been defined by:


#define DISP_EN LATCbits.LATC0 // display enable (RC0)

You can see that this will be easy to extend to multiple digits; all we need do is enable different displays,
after outputting the appropriate digit pattern on the 7-segment bus.

Placing this code into an interrupt service routine (see lesson 5), we have:
/***** INTERRUPT SERVICE ROUTINE *****/
void interrupt isr(void)
{
// *** Service Timer0 interrupt
//
// TMR0 overflows every 2.048 ms
//
// Displays single digit on 7-segment display
//
// (only Timer0 interrupts are enabled)
//
INTCONbits.TMR0IF = 0; // clear interrupt flag

// display current value of 'digit'


set7seg(digit); // output digit
DISP_EN = 1; // enable display
}

Assuming that we’re using the internal RC oscillator, running at the default 500 kHz, Timer0 is set up to
overflow (generating an interrupt) every 2.048 ms, and the timer interrupt enabled, by:
// configure Timer0
OPTION_REGbits.TMR0CS = 0; // select timer mode
OPTION_REGbits.PSA = 1; // no prescaling
// -> increment TMR0 every 8 us
// -> TMR0 overflows every 2.048 ms

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 10


© Gooligum Electronics 2014 www.gooligum.com.au

// enable interrupts
INTCONbits.TMR0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

With this interrupt code running in the background, taking care of displaying the current contents of the
‘digit’ variable, all the main loop code has to do is update the value of ‘digit’ every second:
/*** Main loop ***/
for (;;)
{
// display each digit from 0 to 9 for 1 sec
// (digit is displayed by ISR)
for (digit = 0; digit < 10; digit++)
{
// delay 1 sec
__delay_ms(1000);
}
}

That’s one of the main advantages of using a timer-based “background” interrupt to maintain the display;
your main code only has to update the display contents, without worrying about the mechanics of how it is
displayed, making the main code easier to follow.

Example 2: Three-digit minutes and seconds counter


To demonstrate display multiplexing, we’ll extend the techniques introduced above so that the first digit will
count minutes and the next two digits will count seconds (00 to 59).

Whenever you use display (or other) multiplexing, you must decide on a multiplexing rate – in this case, how
quickly the digits are updated, corresponding to the tick speed (how often then timer-based interrupt runs).
Making the rate faster (shorter ticks), means less perceptible flicker, but the faster the multiplexing rate, the
greater the proportion of time spent updating the display. So you need to find a sensible balance – generally
speaking it’s a good idea to update the display as slowly as you can get away with, to maximise the time
available for other tasks.
In this case, if each of three digits is updated at a rate of 2 ms per digit, the whole 3-digit display is updated
every 6 ms, so the display rate is 1 ÷ 6 ms = 167 Hz – fast enough to avoid perceptible flicker.
Since the exact display refresh rate doesn’t matter, we can use an “easy to generate” 2.048 ms tick, as in the
single-digit example above.

To represent the time count, we can store the minutes and seconds as integer variables:
uint8_t mins = 0; // time counters (displayed by ISR)
uint8_t secs = 0;

Then to extract the tens digit (by dividing seconds by ten) and display it, using the function developed above,
we can simply write:
set7seg(secs/10); // output tens digit
TENS_EN = 1; // enable tens display

Similarly, the ones digit is returned by the expression ‘secs%10’, which gives the remainder after dividing
seconds by ten:
set7seg(secs%10); // output ones digit
ONES_EN = 1; // enable ones display

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 11


© Gooligum Electronics 2014 www.gooligum.com.au

This code assumes that the symbols ‘TENS_EN’ and ‘ONES_EN’ have been defined:
// Pin assignments
#define MINS_EN LATCbits.LATC5 // minutes digit enable (RC5)
#define TENS_EN LATAbits.LATA5 // tens digit enable (RA5)
#define ONES_EN LATCbits.LATC0 // ones digit enable (RC0)

Although using arithmetical operators such as ‘/’ and ‘%’ seems natural when programming in C, their use
can greatly increase the size of the generated code. The XC8 compiler deploys general integer division and
modulus routines to evaluate the expressions ‘secs/10’ and ‘secs%10’, which are not optimised for the
specific “divide by 10” and “find remainder after dividing by 10” tasks that they are used for here.
The size of the generated code is not a problem in this example, because the PIC16F1824 has more than
enough program memory available.
However, the time that these integer division and modulus operations take to complete is a problem, if they
are performed within an ISR which is run every 2 ms – if any of these operations take 2 ms or more to run,
the ISR won’t complete before the next interrupt. The multiplexing won’t work correctly, and there will be
little time available for the main loop to do anything.
It is possible to reduce the code size, and the time needed to run it, by using the expressions ‘secs/10U’ and
‘secs%10U’ (with the ‘U’ telling the compiler that these are unsigned integer constants – which you might be
excused for thinking was already obvious...). This causes the XC8 compiler to use unsigned integer division
and modulus routines, which are somewhat shorter than the general (signed) versions. But they are still large
and slow, and make the multiplexing routine run poorly.

A possible solution is to avoid the use of division and modulus operators altogether, by using separate
variables to store “tens” and “ones”, instead of a single variable for seconds.
However, in this example we can reduce the impact of the slow execution of these ‘/’ and ‘%’ operators by
running the PIC more quickly – configuring the internal RC oscillator to run at 8 MHz (you could go up to
32 MHz if you wish – see lesson 7), instead of the default 500 kHz.
Sometimes increasing the processor speed isn’t an option, and you may need to find a more efficient (if less
straightforward) way to implement your program – or consider writing parts of it in assembly language.

The multiplexing algorithm can be expressed in pseudo code as:


; display next digit in sequence
; (determined by current value of mpx_cnt)
if mpx_cnt = 0
display ones digit
if mpx_cnt = 1
display tens digit
if mpx_cnt = 2
display minutes digit
; increment mpx_cnt, to select next digit for next time
mpx_cnt = mpx_cnt + 1
if mpx_cnt = 3 ; reset count if at end of digit sequence
mpx_cnt = 0

The “multiplex counter” variable, ‘mpx_cnt’, is used to keep track of which digit to display, so that they can
be displayed correctly in sequence, each time the ISR is called.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 12


© Gooligum Electronics 2014 www.gooligum.com.au

Since this variable has to retain its value, from one invocation of the ISR to the next, we need to declare it as
static variable within the ISR:
static uint8_t mpx_cnt = 0; // multiplex counter

It is then straightforward to translate the pseudo code presented above into C:


if (mpx_cnt == 0) {
set7seg(secs%10); // output ones digit
ONES_EN = 1; // enable ones display
}
if (mpx_cnt == 1) {
set7seg(secs/10); // output tens digit
TENS_EN = 1; // enable tens display
}
if (mpx_cnt == 2) {
set7seg(mins); // output minutes digit
MINS_EN = 1; // enable minutes display
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;

However, when selecting between blocks of code, based on various possible values of a single variable, it is
more normal to use the C ‘switch’ statement:
switch (mpx_cnt)
{
case 0:
set7seg(secs%10); // output ones digit
ONES_EN = 1; // enable ones display
break;
case 1:
set7seg(secs/10); // output tens digit
TENS_EN = 1; // enable tens display
break;
case 2:
set7seg(mins); // output minutes digit
MINS_EN = 1; // enable minutes display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;

Note that the multiplex count is updated in a separate increment and test (in case the end of the sequence has
been reached) operation, after the ‘switch’ statement.

An alternative is to update mpx_cnt within the switch statement:


switch (mpx_cnt)
{
case 0:
set7seg(secs%10); // output ones digit
ONES_EN = 1; // enable ones display

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 13


© Gooligum Electronics 2014 www.gooligum.com.au

mpx_cnt = 1; // display tens next


break;
case 1:
set7seg(secs/10); // output tens digit
TENS_EN = 1; // enable tens display
mpx_cnt = 2; // display minutes next
break;
case 2:
set7seg(mins); // output minutes digit
MINS_EN = 1; // enable minutes display
mpx_cnt = 0; // display ones next
break;
}

This is in the form of a state machine, where for each current state, we explicitly state what the next state
will be. It is particularly appropriate when the states are not purely sequential, as they are here. As ever, you
can choose whichever approach seems best.

It is a good idea to blank the display, by clearing the digit enable lines, before outputting each new digit
pattern on the display bus – this avoids “ghosting” (visible in low light) due to PORTA being updated while
the pattern for the previous digit is still being output on PORTC.
So the ‘set7seg’ function becomes:
/***** Display digit on 7-segment display *****/
void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
...

// pattern table for 7 segment display on port C


...

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup and output digit pattern


LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

Once again, with the display being updated by the interrupt code, in the background, the main loop code has
nothing to do except increment the counters, once per second:
for (;;)
{
// count minutes:seconds from 0:00 to 9:59
// (displayed by ISR)
for (mins = 0; mins < 10; mins++)
{
for (secs = 0; secs < 60; secs++)
{
__delay_ms(1000); // delay 1 sec
}
}
}

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 14


© Gooligum Electronics 2014 www.gooligum.com.au

Complete program
Here is the complete program, incorporating the above code fragments.
One point to note is that TMR0 is never initialised; there’s no need, as it simply means that there may be a
delay of up to 2 ms before the display begins for the first time, which isn’t at all noticeable.
/************************************************************************
* Description: Lesson 10, example 2b *
* *
* Demonstrates use of timer-based interrupt-driven multiplexing *
* to drive multiple 7-seg displays *
* *
* 3 digit 7-segment LED display: 1 digit minutes, 2 digit seconds *
* counts in seconds 0:00 to 9:59 then repeats, *
* with timing derived from int RC oscillator *
* *
*************************************************************************
* Pin assignments: *
* RA0-1,RA4, RC1-4 = 7-segment display bus (common cathode) *
* RC5 = minutes digit enable (active high) *
* RA5 = tens digit enable *
* RC0 = ones digit enable *
* *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, internal oscillator (no clock out), 4xPLL off
#pragma config MCLRE = ON, FOSC = INTOSC, CLKOUTEN = OFF, PLLEN = OFF
// no watchdog timer, brownout resets enabled, low brownout voltage
#pragma config WDTE = OFF, BOREN = ON, BORV = LO
// no power-up timer, no failsafe clock monitor, two-speed start-up disabled
#pragma config PWRTE = OFF, FCMEN = OFF, IESO = OFF
// no code or data protect, no write protection
#pragma config CP = OFF, CPD = OFF, WRT = OFF
// stack resets on, high-voltage programming
#pragma config STVREN = ON, LVP = OFF

// Pin assignments
#define MINS_EN LATCbits.LATC5 // minutes digit enable (RC5)
#define TENS_EN LATAbits.LATA5 // tens digit enable (RA5)
#define ONES_EN LATCbits.LATC0 // ones digit enable (RC0)

/***** PROTOTYPES *****/


void set7seg(uint8_t digit); // display digit on 7-segment display

/***** GLOBAL VARIABLES *****/


uint8_t mins = 0; // time counters (displayed by ISR)
uint8_t secs = 0;

/***** MAIN PROGRAM *****/


void main()
{

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 15


© Gooligum Electronics 2014 www.gooligum.com.au

/*** Initialisation ***/

// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

// configure oscillator
OSCCONbits.SCS1 = 1; // select internal clock
OSCCONbits.IRCF = 0b1110; // internal oscillator = 8 MHz

// configure Timer0
OPTION_REGbits.TMR0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b011; // prescale = 16
// -> increment TMR0 every 8 us
// -> TMR0 overflows every 2.048 ms

// enable interrupts
INTCONbits.TMR0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// count minutes:seconds from 0:00 to 9:59
// (displayed by ISR)
for (mins = 0; mins < 10; mins++)
{
for (secs = 0; secs < 60; secs++)
{
__delay_ms(1000); // delay 1 sec
}
}
}
}

/***** INTERRUPT SERVICE ROUTINE *****/


void interrupt isr(void)
{
static uint8_t mpx_cnt = 0; // multiplex counter

// *** Service Timer0 interrupt


//
// TMR0 overflows every 2.048 ms
//
// Displays current count on 7-segment displays
//
// (only Timer0 interrupts are enabled)
//
INTCONbits.TMR0IF = 0; // clear interrupt flag

// Display current count on 3 x 7-segment displays


// mpx_cnt determines current digit to diplay
//
switch (mpx_cnt)
{
case 0:
set7seg(secs%10); // output ones digit
ONES_EN = 1; // enable ones display

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 16


© Gooligum Electronics 2014 www.gooligum.com.au

break;
case 1:
set7seg(secs/10); // output tens digit
TENS_EN = 1; // enable tens display
break;
case 2:
set7seg(mins); // output minutes digit
MINS_EN = 1; // enable minutes display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;
}

/***** FUNCTIONS *****/

/***** Display digit on 7-segment display *****/


void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[10] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011 // 9
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[10] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110 // 9
};

// disable displays
LATA = 0; // clear all digit enable lines on PORTA
LATC = 0; // and PORTC

// lookup and output digit pattern


LATA = pat7segA[digit];
LATC = pat7segC[digit];
}

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 17


© Gooligum Electronics 2014 www.gooligum.com.au

Conclusion
We have seen in this lesson that lookup tables can be effectively implemented in C as initialised arrays
qualified as ‘const’. We also saw that C bit-manipulation expressions make it reasonably easy to extract
more than one segment display pattern from a single table entry, making it seem natural to use a single
lookup table – but that the extra instructions that the compiler generates to perform this pattern extraction
may not be worth the savings in lookup table size.
We also saw that it was quite straightforward to use timer-based interrupt-driven multiplexing to implement
a multi-digit display, without needing to be as concerned (as we were in the assembler versions) about how
to store the values being displayed. This allowed us to use simple arithmetic expressions such as ‘secs/10’
and as ‘secs%10’, but at a significant cost in generated code size and execution time – demonstrating that
what seems easy or natural in C, may not always the most efficient way to do something.
Of course it is useful, when using C, to be aware of which program structures use more memory or need
more instructions to implement than others (such as including floating point calculations when it is not
necessary). But if you really need efficiency, as you often will with these small devices, it’s difficult to beat
assembler.

The next lesson makes use of our new ability to display numeric values, covering analog-to-digital
conversion and simple arithmetic and arrays.

Enhanced Mid-Range PIC C, Lesson 10: Driving 7-segment Displays Page 18

You might also like