ARM Architecture

Introduction to Microcontrollers – 7-segment shows & Multiplexing


Fast Hyperlinks

Doing the 7 Phase Shuffle

The 7 section show is ubiquitous within the fashionable world.  Nearly each digital clock, calculator and film bomb has one.  The treadmills at my health club have 6 or 7, each displaying 3 or 4 digits.  What makes the 7-seg attention-grabbing is that it presents a chance to make a commerce off between GPIO (output pins) for time.  Each 7-seg show requires 8 outputs (the 7 segments and often both a decimal level or a colon).  Thus a 4 digit show requires 32 outputs, and a 6 digit show requires 48 outputs.  We microcontroller customers usually guard our provide of GPIO pins jealously.  In case you are ready to dedicate 48 GPIO pins to a 7-seg show, you are most likely paying extra for the uC than you might want to.  As well as, you will want 48 current-limiting resistors and, except your uC can immediately drive all these segments, you will want 48 driver transistors to get the right present drive via the show LEDs (LCD 7-seg shows are one other beast totally).

You can use particular 7-seg driver chips to scale back the variety of output traces and exchange the transistors, but it surely’s onerous to think about when these could be economical.  So the standard answer is to “multiplex” the digits of the show.  This implies to activate one after the other, further brightly, and scan via all of the digits quick sufficient that the attention blurs the ON and OFF intervals collectively and does not see any flicker.  Now you must have simply one set of section drive traces (8 GPIO) since you’re solely ever driving one set of segments at a time, and one set of digit drive traces (4 or 6 in our instance).  This implies you want 12 GPIO vs. 32 for a 4 digit show, and 14 GPIO vs. 48 for a 6 digit.  You additionally solely want 8 present limiting resistors (and maybe driver transistors) together with, most often, a driver transistor for every digit.  The general financial savings are appreciable.

What does that “further brightly” imply, precisely?  Properly, it is all a operate of how the attention perceives brightness.  You may roughly assume that if a relentless section present of X mA offers you your required brightness, then N*X mA (the place N is the variety of digits) is the present you need to drive via a section, for 1/N of the whole time.  In actual life you will most likely simply begin enjoying with the present limiting resistor values searching for the best values (lowest present) that provides you acceptable brightness.  After all your uC GPIO pins or driver transistors should be rated to produce that N*X mA present, in order that units an higher restrict to how brilliant you possibly can drive the show.  Equally vital, your digit driver has to have the ability to supply or sink 8*N*X mA, for the case that each one 8 segments are turned on.  Fortunately, you will discover very environment friendly shows nowadays, the place even simply 1 or 2mA of common present per section could also be sufficient.

So, how briskly do now we have to scan the digits?  I discover 100/sec to be a very good quantity for the show replace charge.  Some folks wish to go sooner, however going sooner than you might want to simply burns up CPU cycles for no goal.  Typically you will get by with a decrease quantity like 50-80/sec, however then some folks might begin to discover flicker, particularly in sure lighting circumstances.  So let’s use 100/sec and run the numbers.  To show all 4 digits 100 occasions/sec means we show every digit in flip for 1/400 of a second or each 2.5ms.  For six digits the quantity is 1/600 of a second or each 1.67ms.

Now each digit time now we have to do the next:

  • Flip off the earlier digit
  • Advance our bookeeping to the subsequent digit (now the present digit)
  • Get the section sample for the present digit
  • Output the section sample
  • Activate the present digit

Word specifically that each one digits are off when the segments are modified.  In case you do not do that you’re going to get ghosting on the show.  This complete replace course of may find yourself being a pair dozen CPU cycles.  Maintaining in thoughts {that a} 20MHz microcontroller may execute almost 50,000 directions in 2.5ms, and a 100MHz uC almost 250,000, we see that a few dozen cycles is a really low CPU load, lower than 0.1%.  This low CPU load, together with the financial savings in {hardware}, is what makes multiplexing engaging.

It is vital at this level to grasp how NOT to do that digit updating.  Some exterior (to the show replace) course of may have generated an integer worth between 0 and 9999 (let’s assume 4 digits, optimistic values solely).  What we do NOT need to do in our digit replace is to transform this integer worth into separate digits after which seize the section sample for the digit we’re updating.  Changing an integer to separate digits takes a while, and it solely must be accomplished when the worth is up to date (possibly each quarter second or much less typically), definitely not each 2.5ms.  What this implies for our code is that the conversion of integer to digits needs to be part of the code that gives the show worth to the show code.  To find out the very best encoding for these digit values, we’d like to consider the section patterns and the way we’ll symbolize them.  The segments are universally labeled A to G, together with DP if that’s included.  Right here is the section mapping (courtesy Wikipedia):

Thus for instance, to show the worth 7, you’d activate segments A, B and C.  Worth 4 could be B, C, F and G, and so forth.  Very conveniently, we will retailer the section patterns as 1 byte per worth.  This implies that the section patterns be saved in a byte array, both 10 components (for the decimal digits) or 16 components( hexadecimal digits).  We’d additionally need to add one other worth to symbolize all segments off, in order to clean your entire show, or clean main digits.  Lastly, we might need to add one other worth to indicate a minus signal (section G).

Now we simply should assign a bit place to every section.  The 2 apparent decisions are to go in A-G,DP order beginning with D7, or go to in the identical order beginning with D0.  For the board I designed, I selected to begin with D0, so the section array would appear like this (in digits 0-9 order):

static u8 Seg_array[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66,
                         0x6D, 0x7D, 0x07, 0x7F, 0x6F};

Since we would like every digit worth to be an index into our Seg_array, it is sensible (no less than from a C background) to make the digit values 0 via 9.  Word that this isn’t the identical because the character values ‘0’ via ‘9’, which you’d get from an itoa-type operate.  For comfort in changing the integer values to digit values, our first try makes use of sprintf, which does produce ASCII digit values.  We convert these ASCII digit values to indexes by subtracting ‘0’ from them.  Later we’ll take a look at just a few choices which are sooner and smaller than sprintf.

Our show is a 4-digit frequent cathode show, that means we drive the section traces excessive and the digit traces low to gentle up the chosen segments.  The show is “clock format” that means it has a colon between the 2nd and third digits like a clock.  It does not have any decimal factors, sadly.  The show is mounted on a board that connects to my motherboard by way of a 16-pin ribbon cable.

To indicate the multiplexed show in motion we’ll do a 1/10 second counter on an AVR in C (want I had these decimal factors!).  Then we’ll take a look at just a few Ada approaches within the subsequent tutorial.

Mission Creep 

Our C model might be (was going to be) filth easy, besides that I hold pondering of enjoyable issues so as to add.  Begin with a 2.5ms digit replace interrupt (100/sec show replace charge).  The 1/10 second counter may clock off the digit replace interrupt, with 40 interrupts yielding precisely 100ms.  On the whole all the time search for methods to make use of any timer interrupt for extra functions.  However I’ve determined so as to add the aptitude to decelerate the digit replace charge so you possibly can see the glint your self.  In that case we have to divorce the digit replace charge from the 1/10 second counter, so we’ll use T3 for the latter, the tenths interrupt (the AVR is the ATmega1281).  One small however vital level is that the sooner interrupt (T1) has greater precedence than the slower interrupt (T3).  That is constructed into the AVR design and can’t be modified – you possibly can see the precedence order within the datasheet.  As a basic rule, the upper the speed of a repetitive interrupt, the upper its precedence needs to be.  In our explicit instance, you are extra more likely to discover a delay within the service of the two.5ms interrupt (digit flicker) than within the service of the 100ms interrupt.

Within the tenths ISR, 4 buttons are additionally learn (that is an instance of utilizing a timer ISR for a number of functions).  Button 1 (high left) clears the timer and resets the digit replace charge to 100/sec.  The following button, button 2, pauses the timer.  Button 3 restarts the timer.  Button 4 is the brand new addition, the “flicker button”.  Every press (it repeats on the interrupt charge) lowers the show replace charge by 1/sec, and shows the brand new show replace charge.  This fashion you possibly can decrease it till you begin to discover the results in several lighting and by way of peripheral imaginative and prescient, after which reset all the things by urgent button 1.  Barrels of enjoyable, and instructive too!

The one large unknown is the time it takes to transform the 1/10 second rely to the proper 4-digit array index values.  We’d consider doing the conversion within the 100ms ISR, however that would yield issues.  Whereas our 100ms ISR is operating, our 2.5ms interrupt is blocked, and if that occurs we might even see flicker within the show, as one digit stays on longer than it ought to, and the subsequent one goes on shorter than it ought to.  For that reason, we’ll select to determine a method for it to execute within the background when wanted, whereas the two.5ms interrupts hold updating the show.  This retains the 100ms ISR as quick as doable.  We will use our basic ISR-set, background-read flag to inform the background code every time there is a new worth to transform – that’s, every time the worth is cleared or incremented.

No Downside, We’ll Repair It In Software program

Run and conceal everytime you hear this (and you’ll hear it).  It means there’s an issue with the {hardware}, and so they’re hoping (anticipating, actually) the software program folks can make all of it higher.  Properly, now we have a very good instance of that right here.  The 7-segment board connects to the motherboard by way of a 16-pin connector that, for the STM32VLDiscovery board, sensibly offered PA0-7 and PB12-15 – excellent for driving a 7-segment show.  However I selected to show the C code with the AVR board, and the AVR board is a group of compromises, trying to match up the STM32 board comms interfaces (USART, SPI, I2C) first, then matching up any remaining GPIO with no matter AVR GPIO is left.  The result’s that the PA0-7 pins on the connector map to AVR PD4,PE1,PE0,PE7,PF0,PF1,PF2,PF3.  Sure, it truly is that unhealthy.  Moreover, the digit bits PB12-15 match up as PB0,PB1,PB3,PB2.  Did you catch that?  Hey, no downside, we’ll repair it in software program, proper?

Properly, our digit ISR will take extra cycles than a clear {hardware} design, however we undoubtedly can repair this in software program.  We simply should map our section and digit information bits to the suitable AVR port bits.  Meaning numerous masking and shifting, anding and oring.  It seems now we have the identical kind of mapping downside with the STM32F4 board that runs Ada, so within the subsequent tutorial we will see an Ada method of dealing with it.  Within the meantime the code has a PROPER_HW_DESIGN flag to indicate how a lot easier it will be if the segments have been all sensibly mapped to 1 port, and if the digit traces have been consecutive.

And now, our 1/10 second counter with 4 digit multiplexed show:

/*
 * _7Seg.cpp
 */ 


#embody "defs.h"
#embody <avr/io.h>
#embody <avr/interrupt.h>

#outline DIGIT_RATE		400		// 2.5ms interrupts
#outline PRESCALE1		64		// T1 prescale of 64
#outline PRESCALE3		64		// T3 prescale of 64
#outline DIG_OCR			(F_CPU/PRESCALE1/DIGIT_RATE)	// 625 for our values
#outline NUM_DIGITS		4
#outline SCAN_RATE		(DIGIT_RATE/NUM_DIGITS)

#outline DIG_VAL			(SCAN_RATE*DIG_OCR)				// 100 refreshes/sec @ DIGIT_RATE and 4 digits

static u8 Dig_array[NUM_DIGITS];
static unstable u8 Update_flag = 0;
static unstable u16 Tenths = 0;
static unstable u16 Price = SCAN_RATE;		// preliminary worth


#outline PROPER_HW_DESIGN	0		// 1 if the {hardware} folks listened to the software program folks throughout the hw design

// digit replace timer
ISR(TIMER1_COMPA_vect)
{
	static u8 Seg_array[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66,
                                              0x6D, 0x7D, 0x07, 0x7F, 0x6F};
	static u8 Dig_mask_array[] = {0x01, 0x02, 0x08, 0x04};
	static u8 Digit = 0;
	
	u8 segments;
	u8 dig_index;
	
#if PROPER_HW_DESIGN
	PORTB &= ~(1 << Digit);
#else
	PORTB &= ~Dig_mask_array[Digit];	        // flip off earlier digit
#endif
	
	Digit = (Digit == 3 ) ? 0 : Digit + 1;
	dig_index = Dig_array[Digit];
	if (dig_index < 10)						   // legitimate digit index
		segments = Seg_array[dig_index];        // first we fetch the worth of the chosen digit, then we get the section sample for that worth
	else
		segments = 0;                             // some other index leads to clean digit
	
#if PROPER_HW_DESIGN
	PORTA = segments;
	PORTB |= (1 << Digit);
#else
	PORTD = (PORTD & ~(1<<4)) | ((segments & 1) << 4);	    // put segments(0) into PORTD(4)
	PORTE = (PORTE & ~(0x83)) | ((segments & 2) << 6) | ((segments & 4) >> 1) | ((segments & 8) >> 3);	        // segments(1)->PE7, (2)->PE1, (3)->PE0
	PORTF = (PORTF & ~(0x0F)) | ((segments & 0xF0) >> 4);	// segments(4-7) -> PF0-3
	PORTB |= Dig_mask_array[Digit];				// flip off earlier digit
#endif
}

// 1/10 second timer
ISR(TIMER3_COMPA_vect)
{
	static u8 Run = 1;
	
	u8 keys;
	
	if (Run)
	{
		if (++Tenths > 9999)
		Tenths = 0;
	}
	Update_flag = 1;
	
	keys = ~(PINC | 0xF0);						// energetic switches on PC0-3 = 1, all the things else = 0
	if (keys & 1)
	{
		Tenths = 0;
		Price = SCAN_RATE;
		OCR1A = DIG_OCR - 1;					// authentic digit replace charge
	}
	if (keys & 2)
		Run = 0;								// cease
	if (keys & 4)
		Run = 1;								// run
	if ((keys & 8) && (Price > 1))
	{
		Price--;								// decelerate 1/sec
		OCR1A = DIG_VAL / Price;
	}
}

void GPIO_init(void)
{
	DDRB = 0x0F;								// PB0-3 drive digits
	DDRC = 0x10;								// PC4 = pseudo PC10, swap row output
	DDRD = 0x10;								// PD4 = PA0 (section)
	DDRE = 0x8B;								// PE0, 1, 7 = PA3, 2, 1 (segments), PE3 (PB0) is debug
	DDRF = 0x0F;								// PF0-3 = pseudo-PA4-7 (segments)
	
	PORTC = 0x0F;							// pullups on PC0-3, and PC4=0
}

void T1_init(void)
 (3 << CS10);	// CTC mode, 64 prescale, timer begins right here


void T3_init(void)
 (3 << CS30);	// CTC mode, 64 prescale, timer begins right here


int primary(void)
{
	GPIO_init();
	T1_init();
	T3_init();
	sei();
	
    whereas(1)
    {
        //TODO:: Please write your utility code 
		if (Update_flag)
		{
			char buf[6];						// to carry 5 digits and EOS
			// utilizing sprintf as a result of it is easy and provides us formatted output that is straightforward to make use of
			if (Price == SCAN_RATE)				// show both timer worth or show replace charge
				sprintf(buf, "%5u", Tenths);		// no main zeros
			else
				sprintf(buf, "%05u", Price);		// hold main zeros
				
			Dig_array[0] = buf[1] - '0';			// convert ASCII to digit indexes
			Dig_array[1] = buf[2] - '0';		
			Dig_array[2] = buf[3] - '0';		
			Dig_array[3] = buf[4] - '0';		
				
			Update_flag = 0;
		}
    }
}

So now we have the T1 ISR doing the digit updating (the multiplexing), T3 ISR updating the 1/10 sec counter and studying the buttons, and the primary loop changing the 1/10 sec worth (or the digit replace charge) into the format utilized by the T1 ISR.  Initially I am utilizing sprintf to do the formatting as a result of it could format into a set variety of characters, e.g. it could format the integer 7 into ”   7″ or “0007”.  That is very handy, and I do not need to write the code to duplicate this functionality.  Later we’ll take a look at different conversion choices.

Hey, How Do You Convert 1234 To 1234?

Changing information from a reminiscence or computationally environment friendly format (e.g. 16-bit integers) right into a format appropriate for show is a standard embedded requirement.  The appropriate format relies upon after all on the necessities of the show, however on this case we have settled on an array of digit indexes, one byte per digit.  We have seen methods to use sprintf to do that (adopted by the conversion of the ASCII digits to digit indexes by subtraction of ‘0’).  Listed here are just a few different methods, if you wish to try to roll your individual.  This is likely to be as a result of your uC does not help {hardware} division and even multiplication, and people routines can take up quite a lot of cycles of code.  Listed here are two routines to offer you an concept of the chances.

#outline BLANK_DIGIT  10 // something outdoors of 0-9

#if 1
void utoi(u8 * p, u16 val, u8 blank_lz)
{
	u8 digit;
	u8 blank_this_z = blank_lz;
	
	whereas (val > 9999)
	val -= 10000;							// toss out any main ten-thousands

	digit = 0;
	whereas (val > 999)
	{
		val -= 1000;
		digit++;
	}
	if (blank_this_z && (digit == 0))
	    digit = BLANK_DIGIT;
	else
	    blank_this_z = 0;
	*p++ = digit;
	
	digit = 0;
	whereas (val > 99)
	{
		val -= 100;
		digit++;
	}
	if (blank_this_z && (digit == 0))
	    digit = BLANK_DIGIT;
	else
	    blank_this_z = 0;
	*p++ = digit;
	
	digit = 0;
	whereas (val > 9)
	{
		val -= 10;
		digit++;
	}
	if (blank_this_z && (digit == 0))
	    digit = BLANK_DIGIT;
	*p++ = digit;
	*p = val;
}

#else

void utoi(u8 * p, u16 val, u8 blank_lz)
{
	div_t outcome;
	bool blank_this_z = blank_lz;
	
	outcome = div(val, 10000);	// strip out ten 1000's
	
	outcome = div(outcome.rem, 1000);
	if (blank_this_z && (outcome.quot == 0))
	    *p++ = BLANK_DIGIT;
	else
	{
		blank_this_z = 0;
		*p++ = outcome.quot;
	}
	
	outcome = div(outcome.rem, 100);
	if (blank_this_z && (outcome.quot == 0))
	    *p++ = BLANK_DIGIT;
	else
	{
		blank_this_z = 0;
		*p++ = outcome.quot;
	}
	
	outcome = div(outcome.rem, 10);
	if (blank_this_z && (outcome.quot == 0))
	    *p++ = BLANK_DIGIT;
	else
	{
		blank_this_z = 0;
		*p++ = outcome.quot;
	}
	*p = outcome.rem;
}
#endif

int primary(void)
{
	GPIO_init();
	T1_init();
	T3_init();
	sei();
	
	whereas(1)
	{
		//TODO:: Please write your utility code
		if (Update_flag)
		{
			// utilizing sprintf as a result of it is easy and provides us formatted output that is straightforward to make use of
			if (Price == SCAN_RATE)				// show both timer worth or show replace charge
			    utoi(Dig_array, Tenths, 1);
			else
			    utoi(Dig_array, Price, 0);
			
			Update_flag = 0;
		}
	}
}

The 2 capabilities utoi (for unsigned-to-index) take the vacation spot u8 array, the integer worth to be transformed, and a flag indicating whether or not main zeros are to be blanked.  As you possibly can see, solely one of many capabilities is outlined at anybody time, relying on the #if state (0 or 1).  The primary utoi operate avoids any multiplication or division, so it is a good selection in case your uC does not have these operations in {hardware}.  The second utoi operate makes use of the neat library “div” operate, which returns each quotient and the rest – very helpful!  I feel the code is extra elegant, but it surely does require divisions.  The main zero blanking code is identical in each capabilities.

Listed here are some numbers for the AVR, which doesn’t have {hardware} division.  sprintf code is 2508 bytes (complete program dimension, not simply the sprintf half) and the conversions took 80us.  utoi#1 code (not utilizing “div”) is 1110 bytes complete program dimension, and took between 3 and 16us, relying on the quantity being transformed.  utoi#2 code, utilizing “div”, is 1152 bytes complete program dimension and took 60us.  Maybe unintuitively, on a component that doesn’t help {hardware} division, the brute power subtract and loop strategy takes much less code house, and is about 6 to 10 occasions as quick as the opposite approaches.

Two Timers is Too Many

Timers are valuable sources.  Possibly we do not have two to dedicate to our little experiment.  That is OK, we solely want one, with two output evaluate registers.  We’ll use the “leapfrog” method, advancing the OCR registers contained in the ISRs, primarily utilizing two output evaluate registers to get the equal of two timers.  Listed here are the modifications required:  T3_init and the TIMER3_COMPA ISR have disappeared.  T1_init now begins the timer in “regular” mode, and TIMER1_COMPA ISR simply provides the OCR1A += leapfrog line.  TIMER1_COMPB ISR is added, performing the identical operate as TIMER3_COMPA did.  Our interrupt priorities are nonetheless appropriate, because the precedence of T1 COMPA is greater than T1 COMPB.  Listed here are the modified sections of code:

static unstable u16 Dig_ocr = DIG_OCR;		// digit replace charge


#outline PROPER_HW_DESIGN	0				// 1 if the {hardware} folks listened to the software program folks throughout the hw design

// digit replace timer
ISR(TIMER1_COMPA_vect)
{
<< similar as earlier than, with one extra line >>
	OCR1A += Dig_ocr;							// advance to subsequent interrupt time
}

// 1/10 second timer
ISR(TIMER1_COMPB_vect)
{
	static u8 Run = 1;
	
	u8 keys;
	
	if (Run)
	{
		if (++Tenths > 9999)
		Tenths = 0;
	}
	Update_flag = 1;
	
	keys = ~(PINC | 0xF0);						// energetic switches on PC0-3 = 1, all the things else = 0
	if (keys & 1)
	{
		Tenths = 0;
		Price = SCAN_RATE;
		Dig_ocr = DIG_OCR;					// authentic digit replace charge
	}
	if (keys & 2)
		Run = 0;								// cease
	if (keys & 4)
		Run = 1;								// run
	if ((keys & 8) && (Price > 1))
	{
		Price--;								// decelerate 1/sec
		Dig_ocr = DIG_VAL / Price;				// COMPA ISR will use this.  No collisions as a result of each makes use of are ISRs
	}
	OCR1B += TENTH_OCR;					// advance to subsequent tenths time
}

....

void T1_init(void)
 (1 << OCIE1B);	// for TIMER1_COMPA_vect and TIMER1_COMPB_vect
	TCCR1A = 0;
	TCCR1B = (3 << CS10);					// regular mode, 64 prescale, timer begins right here


So here is a video of the code operating.  After all the scanning of the video document and playback completely interferes with the scanning of the show, so you actually cannot see what is going on on.  I seen that for myself, I couldn’t discover flicker wanting straight on the show, in daytime indoor lighting, till it received all the way down to 42/sec.  Utilizing peripheral imaginative and prescient I began to note flicker round 48/sec.

 

Subsequent time we’ll attempt it in Ada, and in addition see how Ada catches nasty runtime bugs and might report the precise file and line quantity the place the bug occurred.

You may additionally like… (promoted content material)

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button