Driving the 4 digit 7 segment display

Here is the documentation of my next step: driving the LED display. The design and the C code were not really difficult; the steps to put the results here were.

As I noted before I use a PIC 16F887, but CadSoft Eagle didn’t have this component in its library, so I drew a 877 which is pin compatible. Now I wanted to get it right and so I started my own library. Copying the 877 to it and then renaming it to 887 was simple enough. The next step was to get my 4 digit 7 segment display (datasheet in PDF)  in Eagle. I did find a 3 segment display that came close and it took me a bit of time to add the fourth digit. Now my custom library contains two components!


Ok, all that was necessary so I could draw this:

I stole heavily from all around the web to put this together. Most examples I saw were using common cathode displays, whereas mine is a common anode variant.

The schematic uses the octal latch 74HCT573 from my previous step. Whilst it is really not necessary in this example, I still kept it because I think I will need it later on. Plus it was on my breadboard anyway. PIC pin RC5 drives the latch.

The output of port D (through the latch) is split in two: RD0..RD3 contain the binary coded number (BCD) for a single digit, RD4..RD7 are the pins that select the display to be displayed. We need a mechanism called multiplexing to drive the display. David Meiklejohn at Gooligum really explains it very well in a tutorial, that he made freely available on GitHub.

The 4543 (datasheet, PDF) translates the BCD number to outputs for each of the segments of a digit. The ULN2803A (datasheet, PDF) is a darlington transistor array that allows much more current to flow through the display. The discrete darlington transistors BC516 are the switches that select the digit.

I wrote a small program that just counts each 0.5 second from 0 to 9999. The interrupt service routine does two things. Every 0.5 second it increments the number to display and splits this number in its parts – thousands, hundreds, tens and ones. Every millisecond a digit displays its number. We have four digits to drive, so the refresh rate for each digit is 250Hz.

I have heavily commented the code, so I hope you understand what is going on.

// First PIC experiments by Bart van der Velden
// Based on tutorials written by Devlin Thyne and gooligum.com.au
//
// This program increments a counter every 500 ms and puts the
// value on a four digit 7 segment display.
//
// Port D is used as output port connected to two octal latches.
// RC5 is used to select the octal latch to output to.
//
// Delay routines by Tyler Montbriand at burningsmell.org, but not
// needed in this example.
//
// The timer for the interrupt is set as explained by the tutorials
// at www.gooligum.com.au
//
// To compile:
// sdcc -mpic14 -p16f887 uartrx.c
//
// To program the chip using pk2cmd:
// pk2cmd -M -W -R -PPIC16f887 -Fuartrx.hex

#include "pic16f887.h"

#include "tsmtypes.h"

#define TRUE 1
#define FALSE 0

//Set the configuration words:
Uint16 __at _CONFIG1 configWord1 =
	_DEBUG_OFF & // In circuit debugging off (I don't have an ICD 2)
	_LVP_OFF &   // Low voltage programming off
	_FCMEN_ON &  // Fail-safe clock monitor is enabled
	_IESO_ON &   // Internal/external switchover mode is enabled
	_BOR_ON &    // Brown-out reset enabled
	_CPD_OFF &   // Data memory code protection is disabled
	_CP_OFF &    // Program memory code protection is disabled
	_MCLRE_ON &  // MCLR pin is MCLR (not a digital input)
	_PWRTE_ON &  // Power-up timer is disabled
	_WDT_OFF &   // Watchdog timer is disabled
	_HS_OSC;     // External high speed crystal on OSC1 and OSC2

// No write protection and brown-out reset at 4.0V
Uint16 __at _CONFIG2 configWord2 = 0x3fff;

// Setup variables
volatile Uint8 sPortC = 0; // shadow variable for PORTC
volatile Uint8 sPortD = 0; // shadow variable for PORTD

Uint16 bcdCount = 0;
Uint8 bcdThousands = 0;      // The thousands part of the value to display
Uint8 bcdHundreds = 0;       // The hundreds part of the value to display
Uint8 bcdTens = 0;           // The tens part of the value to display
Uint8 bcdOnes = 0;           // The ones part of the value to display
Uint8 bcdDigitToDisplay = 0; // The digit to show

// Set the output on the second octal latch
// The 4 bits on the left control the segment to light at any point. The bit that goes low
// selects the segment.
void Set7SegmentDisplay()
{
	Uint8 pattern = 0;
	if (bcdDigitToDisplay == 0) // 1st digit from the right
	{
		pattern = bcdOnes; // Set the value for the ones
		pattern |= 0xE0; // Select the 1st digit from the right
	}
	if (bcdDigitToDisplay == 1) // 2nd digit from the right
	{
		pattern = bcdTens; // Set the value for the tens
		// If this is a leading zero, set a pattern that blanks the digit
		if (bcdTens == 0 && bcdHundreds == 0 && bcdThousands == 0)
			pattern = 0x0A;
		pattern |= 0xD0; // Select the 2nd digit from the right
	}
	if (bcdDigitToDisplay == 2) // 3rd digit from the right
	{
		pattern = bcdHundreds; // Set the value for the hundred
		// If this is a leading zero, set a pattern that blanks the digit
		if (bcdHundreds == 0 && bcdThousands == 0)
			pattern = 0x0A;
		pattern |= 0xB0; // Select the 3rd digit from the right
	}
	if (bcdDigitToDisplay == 3) // 4th digit from the right
	{
		pattern = bcdThousands; // Set the value for the thousands
		// If this is a leading zero, set a pattern that blanks the digit
		if (bcdThousands == 0)
			pattern = 0x0A;
		pattern |= 0x70; // Select the 4th digit from the right
	}
	// Prepare the next digit to display
	bcdDigitToDisplay++;
	if (bcdDigitToDisplay == 4)
	{
		bcdDigitToDisplay = 0;
	}
	// Set the actual outputs
	sPortC |= 0x10;    // Enable latch on RC4;
	PORTC = sPortC;

	PORTD = pattern;   // Set output on PORTD

	sPortC &= 0xEF;    // Disable latch on RC4;
	PORTC = sPortC;
}

void isr(void) __interrupt
{
	// Handle Timer0 interrupt
	static Uint16 tmr0Cnt = 0; // counts Timer0 overflows
	static Uint16 displayCount = 0;

	GIE = 0;

	// Handle Timer0 overflow interrupt if it happened
	if (T0IF)
	{
		// Now make the Timer0 interrupt fire every 250 cycles, which
		// is every 50 us with my 20MHz oscillator. See details at
		// http://gooligum.com.au/tutorials/midrange/PIC_Mid_C_3.pdf
		// around page 8.
		TMR0 += 256-250+3;
		T0IF = 0;

		++tmr0Cnt; // increment interrupt count (every 50 us)
		if (tmr0Cnt == 500000/50) // every 0.5 sec
		{
			Uint16 tmp = 0;
			tmr0Cnt = 0;
			sPortD++;
			// Count from 0 to 9999 and the start over
			bcdCount++;
			if (bcdCount == 9999)
			{
				bcdCount = 0;
			}
			// Split the number in its BCD components
			bcdThousands = bcdCount / 1000;
			tmp = bcdCount % 1000;
			bcdHundreds = tmp / 100;
			tmp = tmp % 100;
			bcdTens = tmp / 10;
			bcdOnes = tmp % 10;
		}

		// Update the 4 digit 7 segment display every 1 ms.
		// This gives a refresh rate per digit of 250 Hz.
		++displayCount;
		if (displayCount == 20) // every 1 ms
		{
			displayCount = 0;
			Set7SegmentDisplay();
		}
	}
	GIE = 1;
}

void main(void)
{
	PORTC = 0;
	PORTD = 0;

	// Set PORTC and PORTD to all outputs
	TRISC = 0b10000000;
	TRISD = 0x00;
	ANSEL = 0;
	TRISA = 0b11110000;

	OPTION_REG = 0b11011111; // Setup Timer0
	             //--0-----  timer mode (TOCS = 0)
	             //----1---  no prescaler (assigned to WDT, PSA = 1)

	// Setup Interrupt Control Register
	INTCON = 0b10100000;
	         //1------- GIE - enable global interrupts
	         //--1----- TOIE - enable Timner0 interrupt

	TMR0 = 0; // Set Timer0 to 0

	// Loop forever, everything relevant is in the interrupt routine
	while (1)
	{
	}
}

I hope this is of use for someone, as it took me a bit of time to collect the information, and – more importantly – to understand how this all works.

4 thoughts on “Driving the 4 digit 7 segment display”

    1. I don’t have a libray for the display. All the code to drive the led segments is in the code of the article. Of course you could extract that and make a library out of it, but I never did.

Comments are closed.