multiplexing 14 segment display Arduino code - Page 1
Forum section: electronics / general

author: dentaku
author_rank: Frequent Contributor
timestamp: March 27, 2020, 12:49:59 am
content: I salvaged some common anode 14 segment displays and Allegro A6276ELW 16-BIT SERIAL-INPUT, CONSTANTCURRENT LATCHED LED DRIVERS and decided to figure out how to use them without relying on copying someone elses code or using a pre-made library, just using datasheets. Considering it's just a big shift register and a 3 to 8 decoder it isn't anything I haven't seen before.The circuit works so that's not what my question is about.At the moment I wrote some Arduino code to see how fast it needs to be multiplexed before it stops being annoyingly flickery. I hate flickery multiplexed displays, slow PWM, etc...It just uses a big for loop and even a delay() so it's not like it's code you would use for an actual product because the Arduino spends all it's time doing nothing but shifting out data and turning on the anodes in sequence then delaying for 1ms.QUESTION:If I wanted to be able to say, for example, print14Seg("Hello!!!");OR maybe even type it in the Serial Monitor and have it show up on the displays once a buffer has 8 characters in it, does anyone have any links or suggestions on how to go to the next level and make this more friendly to use?I'm assuming Timer Interrupts or counting millis/micros would be involved?Here's the code I wrote to test how fast/slow I can multiplex it before it gets annoying. (with lots of comments to remind myself why I did certain things)I'm also including a probably not prefect schematic just to give an idea of how it's all wired up. It works fine on my breadboard though.Code://shifting out 16 bits to an Allegro A6276 LED driver#include "ASCII_table_14seg.h" //array containing 127 characters/fonts for 14seg displays as type uint16_t in ASCII table order#include <SPI.h>// SDI..... MOSI (e.g. 11 at UNO, 51 on Mega 2560, ICSP/10 on Leonardo)// CLK..... SCK (e.g. 13 at UNO, 52 on Mega 2560, ICSP/9 on Leonardo)const int OE = 7; //connect to Output Enable of A6276 / PWM ~OE to set brightnessconst int latchPin = 6; //connected to Latch Enable of A6276const int displayLength = 8; //how many digits long is your display?int anodeDriver = 0; //start at first digit by turning on anode 0 firstvoid setup() {SPI.begin();// Serial.begin(9600);pinMode(latchPin, OUTPUT);pinMode(OE, OUTPUT);pinMode(A3, OUTPUT); pinMode(A2, OUTPUT); pinMode(A1, OUTPUT); //PORTF 4,5,6 on LeonardodigitalWrite(OE, LOW); //set output enable low / ONpinMode(A0, OUTPUT);//making PORTF7/A0 do something to prove I can count on PORTF4,5,6 without changing the rest of the pins on PORTFdigitalWrite(A0, HIGH); //set PORTF7/pinA0 high to light led}void loop() {for (int i = 48; i <= 55; i++) //0 to 7 in ASCII table{print14SegChar(i);PORTF = (PORTF & 0B10001111) | (anodeDriver << 4); //make pins 4,5,6 be 0 then OR with anodeDriver shifted 4 left //used () because I don't know the operator precedencedelay(1); //keep display lit for x millisecondsanodeDriver++; //increment this number so the 74138 turns on the next display's anodeif (anodeDriver == displayLength) //remember... anodeDriver starts at 0{anodeDriver = 0;//switch the state of PORTF7/pinA0 every time anodeDriver resets to 0, blinking an LEDdigitalWrite(A0, ~PINF & 0B10000000);//anything other than 00000000 is considered a 1 or True or High?}}}void print14SegChar (char ASCIIcharacter) //shift out a single char using SPI.transfer16(){// digitalWrite(OE, HIGH);digitalWrite(latchPin, LOW); //take the latchPin low during serial data entrySPI.transfer16 (ASCIItable); //transfer 16 bits al at once (instead of highByte/lowByte in two steps)digitalWrite(latchPin, HIGH); //take the latch pin high so the LEDs will light up// digitalWrite(OE, LOW);}// print14SegChar('z'); //don't forget the single quotes ''// for example... 'B' = number66 in the ASCII table so  from the array is displayed// Serial.print('B', DEC); //and you will get 66// delay (250);

author: Peabody
author_rank: Super Contributor
timestamp: March 27, 2020, 05:53:49 am
content: I guess I would divide up the process. You could have the 16-byte buffer containing the actual segment patterns of the 8 characters currently being displayed, including nulls (or FFs) for blank/empty characters. The display multiplexing would simply display all 8, one at a time, over and over again, with the update occurring whenever the current millis() value is greater than or equal to the previous value of millis() when the last update occurred, plus X. X would probably be 1.Code:currentmillis = millis();if ((currentmillis - lastmillis) => 1) {// update display - next characterlastmillis = currentmillis;}else {//do other stuff}Then it would be up to the rest of the code outside that IF to receive the new characters to be displayed and update the 16-byte buffer accordingly. I don't think you would need to wait for all 8 new characters to be processed before displaying any of them. I suspect you can just update each character in real time, and however you are receiving them would be fast enough that you wouldn't notice that the display is actually updating sequentially, left to right, or whatever. But if receiving individual characters over serial, you might want to zero out the 16 bytes on the first received character.I find myself wishing your display was common cathode. That would allow you to replace the 3-to-8 with a 74HC4017 which would just select the next digit when triggered by a single clock pin rather than 3 decoded pins.Do you know the maximun number of segments that would ever be lit at the same time? I'm thinking it would be 8. If so, is there any chance you wouldn't need the transistors?

author: cv007
author_rank: Frequent Contributor
timestamp: March 27, 2020, 10:49:26 am
content: You have c++ on your side, so take advantage.simple example-https://godbolt.org/z/5UiDzeMost likely not perfect, but it compiles and gives an idea how you can contain something in its own world (struct/class). Now you have a black box where you can allow any method of access to it you want. Give it the info it requires to setup/run, give it methods for outside access, and if done right you really do a get a reliable black box. It also becomes easily reusable, and although you have fixed spi pins many of the other pins can be anything you want. You also get the ability to inherit the Print class in this case by simply providing your own write function which Print will use to feed you bytes one at a time.It becomes awkward to refresh the display manually unless you are doing basically nothing else, so getting an isr to do the display update/refresh would be next on the list. I don't really know arduino so not sure what they already have going on for isr's and what is available.

author: Peabody
author_rank: Super Contributor
timestamp: March 27, 2020, 02:18:43 pm
content: One further thought. You are using SPI.h, which uses the SPI peripheral in the processor. To transmit two bytes, it appears the library initiates transmission of the first byte, but then has to wait for that to complete before initiating the second byte.However, you can also use the USART peripheral in SPI mode, which is double-buffered in transmit. So you would start the first byte, but then immediately write the second byte into the buffer, and you're done. No waiting. Then you could do without SPI.h, but you would need to write your own code for the USART (unless there's a library for that).Here's a quote from Section 19.4 of the datasheet:"16-bit data transfer can be achieved by writing two data bytes to UDRn."

author: cv007
author_rank: Frequent Contributor
timestamp: March 27, 2020, 06:44:12 pm
content: >So you would start the first byte, but then immediately write the second byte into the buffer, and you're done. No waiting.You have to stick around to latch the data anyway, so may as well just skip the whole spi thing and pick out any two pins for clk and dat, then clock out the data manually. Then the spi can be left to do other things and you get to use any pins for clk/dat and become totally pin independent.Shifting out 16bits is probably not ideal in an isr in any case (not terrible either), but is has to be done sometime. The hardware versions spi/uart-spi would let you do other things while the data is going out, but then you have to exit the isr and come back to latch the data when finished (2 times for spi, once for uart). Probably easiest to keep it simple and just clock out the data in the isr as fast as the shift ic will allow (manually will most likely be faster than hardware, although arduino digitalWrite is no help for speed), and if there are other higher priority isr's that need to be done in that time, you can re-enable irq's in the shift routine with some care and thought.slight mod to clock out data manually-https://godbolt.org/z/-Rn7Dh

author: Peabody
author_rank: Super Contributor
timestamp: March 27, 2020, 08:33:16 pm
content: Quote from: cv007 on March 27, 2020, 06:44:12 pmI don't think there's any input to latch. The SPI only does output from the Leonardo to the A6276. Nothing is coming back in via SPI, so I don't think you have to hang around.

author: cv007
author_rank: Frequent Contributor
timestamp: March 27, 2020, 10:30:26 pm
content: >Nothing is coming back in via SPI, so I don't think you have to hang around.I guess it depends if you want to watch the data being clocked in. Its most likely not going to look ok.good-1. disable outputs via OE - no led's are on while new data being changed and next digit selected2. setup '138 to drive next digit3. send new data (LE probably can just be left high since OE in use)4. all done, now OE back on (low), and we see the next digitnot so good-1. setup '138 to drive next digit - now are seeing the previous digit data on this digit2. send digit data, and watch it go from bit to bit/segment to segment (yes, its fast but you will see it)another good, with 2 isr's-1. disable outputs via OE - no led's are on while new data being changed and next digit selected2. setup '138 to drive next digit3. start sending new data via hardware, exit this isranother isr, triggered when data sending is done-4. data sent, now OE back on (low), and we see the next digit

author: Buriedcode
author_rank: Super Contributor
timestamp: March 28, 2020, 03:18:37 pm
content: I would use a timer interrupt to fire at (display rate * no. of digits). All segment displays "flicker" to some degree, and this becomes more noticeable with more digits, because it is down to the duty cycle of a digit, more digits = smaller fraction of total display refresh time. Clocks with 4 digits (25%) tend to be less noticeable with say 6 digits (16.7%).A good experiment would be a quick timer interrupt routine that follows pretty much what cv007's reply states:- disable all digits- write SPI data for next digit - say make all digits '8' for now.- set '138 to enable next digit- re-enable all digitsYou could then experiment with the interrupt period to see at what point the flicker becomes less pronounced. Waiting for the hardware SPI port to spit out a byte isn't really going to use up much time - AFAIK the SPI port can run at half the main clock, which is 8MHz. For 8 bit per transfer thats 1us. 2us for 16-bits. What might slow things down are things like "digitalwrite" but these can just be replaced by directly writing to the ports.This project would be a good opportunity to learn about timers, interrupts, writing code directly for the peripherals/hardware etc.. then you could start to develop your own library for it.Nothing wrong with Arduinos built-in stuff, but it was meant for convenience and simplicity, not speed.

author: cv007
author_rank: Frequent Contributor
timestamp: March 28, 2020, 08:17:06 pm
content: > All segment displays "flicker" to some degree, and this becomes more noticeable with more digits,You pick your desired refresh Hz for a single digit and multiply by the number of digits. You want 8 digits at 100Hz, then refresh at 800Hz so each digit gets its 100Hz. What you lose with more digits is max brightness, but leds have datasheet's like anything else and you can check if more current is allowed for brief periods to compensate (current vs duty cycle for what is allowed, and luminous intensity vs current to see if it helps).There is also another alternative to the previous to decrease the off time (or to make uart-spi use better)- use LE and preload the next digit data so the off time is only the time it takes to set the '138 and toggle LE-1. disable outputs via OE - no led's are on while new data being changed and next digit selected2. setup '138 to drive next digit2b. LE high, LE low - now the previous data is latched in2c. enable output via OE, new digit now displaying3. send new data for the NEXT digit (preload data, but not latched)4. all done and ready to latch new data next time throughMay not be worth giving up a pin to drive LE, but its another option and if using something like the uart-spi you can dump the 2 bytes and be done as the isr will do the latch of the data next time through.

author: dentaku
author_rank: Frequent Contributor
timestamp: March 29, 2020, 02:00:19 am
content: At first I was thinking about something like a 4017 for the anodes but of course there's the problem of having it miss a clock pulse and then everything would be shifted to the left by one.Also, like you said, it wouldn't be bad to have it display each character right away but it would also be nice to just have it update only once there are 8 new characters. Not for performance reasons but because I like it that way.Thanks for the little bit of sample code.Quote from: Peabody on March 27, 2020, 05:53:49 am

author: dentaku
author_rank: Frequent Contributor
timestamp: March 29, 2020, 02:22:55 am
content: You calling a it a "black box" is a perfect description of what I was thinking about. I want to be able to interface with the display without ever having to think about how it works once I have it set up.I was recently looking at some sample code on how to use Timer Interrupts and it also uses a Class and a Constructor.https://learn.adafruit.com/multi-tasking-the-arduino-part-2/timers#I'm not a programmer but I've seen code like that before. I just have to learn more before using more advanced things like that.I definitely want to do some experiments with timer interrupts.I might use the Timer1 or Timer3 libraries from PJRC.https://www.pjrc.com/teensy/td_libs_TimerOne.htmlI wouldn't want to shift out 16 bits and increment a counter for the anodes every 1.024 ms in an interrupt though. That seems like an awful lot to do for an ISR which in my experience (very limited experience) has to be as short as possible.Quote from: cv007 on March 27, 2020, 10:49:26 am

author: dentaku
author_rank: Frequent Contributor
timestamp: March 29, 2020, 02:29:18 am
content: I just used SPI because SPI.transfer16 looked neater than using shiftOut twice to get 16 bits.The original code I wrote looked like this and it also works.I'm not sure which one is more efficient.Code:void shiftOut16MSB (int twoByteValue) //using two shiftouts{digitalWrite(latchPin, LOW);shiftOut(dataPin, clockPin, MSBFIRST, highByte(twoByteValue)); // shift out the first 8 bitsshiftOut(dataPin, clockPin, MSBFIRST, lowByte(twoByteValue)); // shift out the next 8 bitsdigitalWrite(latchPin, HIGH);//take the latch pin high so the LEDs will light up}Quote from: Peabody on March 27, 2020, 08:33:16 pm

author: dentaku
author_rank: Frequent Contributor
timestamp: March 29, 2020, 02:54:30 am
content: Interesting. I like that sequence.I originally was setting the OE on and off like you would for a 74HC595 shift register but ended up not bothering while using this LED driver because I wasn't seeing any difference so it was just adding more complication.I was just using digitalWrite(OE, HIGH); but I can replace that easily enough by doing something like PORTE = PORTE | 0B01000000; and PORTE = PORTE & 0B10111111;The OE pin can also be used to PWM the brightness of the display.I also just noticed that in my code I shift out the bits THEN update the 138. I'm going to change the order and see if there's any noticeable flickering or ghosting.I might bring out my $10 logic analyzer and see if one SPI.transfer16 is actually faster than doing shiftOut highByte then shiftOut lowByte in two steps.Quote from: cv007 on March 28, 2020, 08:17:06 pm

author: Peabody
author_rank: Super Contributor
timestamp: March 29, 2020, 04:27:02 am
content: I believe the following is the actual code for shiftOut():Code:void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val){uint8_t i;for (i = 0; i < 8; i++) {if (bitOrder == LSBFIRST)digitalWrite(dataPin, !!(val & (1 << i)));elsedigitalWrite(dataPin, !!(val & (1 << (7 - i))));digitalWrite(clockPin, HIGH);digitalWrite(clockPin, LOW);}}So that's just bit banging, which I suspect is pretty slow.But it's not just a question of how fast the 16 bits are shifted out. It's also a question of whether you can automate the process so you don't have to stick around in the ISR or loop waiting for it to complete.The advantage of using SPI is that the SPI peripheral shifts out all 8 bits automatically once you've started it going. But with SPI you do have to wait for the first byte to complete before initiating the second byte because the SPI peripheral doesn't use a transmit buffer. It just has the transmit shift register.USART SPI lets you initiate the first byte, then immediately write the second byte to the transmit buffer, and the USART will automatically send the second byte when the first has completed. The actual shifting out will take place long after you've returned from the ISR and resumed doing other things. So there would be no waiting at all. You would use cv007's algorithm where the last thing you do is start the transmission of the NEXT digit's segments.

author: cv007
author_rank: Frequent Contributor
timestamp: March 29, 2020, 09:14:18 am
content: >I wouldn't want to shift out 16 bits and increment a counter for the anodes every 1.024 ms in an interrupt though. That seems like an awful lot to do for an ISR which in my experience (very limited experience) has to be as short as possible.If you bypass the arduino digitalwrites, it wouldn't be so bad and you would probably be left with under 20us of work to shift out manually (assuming 16MHz).Short isr's are generally good, but general rules apply generally. If you delayed a pin interrupt for a user switch by 30us because of your display isr, the user will not know their switch press could have been serviced 30us sooner. It just depends what is more important and if the work can easily be deferred out of the isr.I don't know what isr's arduino claims for its own use or if you want to give up the uart for display use, but if using the uart-spi you could use the transmit-complete irq/isr. Set the baud rate so 2 bytes take whatever time you want for refresh, then your txc isr also becomes the refresh timer.I would probably just shift out the bits manually so any pins can be used and the important peripherals can be left to do the work they were designed to do if needed. Bit banging the shift data will be easier than bit banging a uart, and a uart will be wanted sooner or later.Here is a mod of previous code that bypasses digitalWrite and ends up with cbi/sbi-https://godbolt.org/z/8wriXV(also slight mod to load data first with latch disabled, so the shift time is done with display on)this requires an enum for the pins (using the 'real' names here), and instantiation via a template (just as easy as before).I have no idea if this is correct, and arduino use makes the online assembly a little more difficult to read but the cbi/sbi are there for pin writes so assume the rest is close.edit- I had arduino on the pc for esp32 so tried to compile and hook into the millis isr-https://godbolt.org/z/FeYsa5I modified platform.txt to use c++17 (or c++14), created an extern C function for the isr to access display.refresh, added the display_refresh c function call to the timer0 isr in wiring.c, and Bob's your uncle. Probably.The arduino asm output shows the display.refresh as looking ok. I would probably forget using the LE when doing the shift register work manually as it will only make a 0.36% difference in led on time- ~30us shift time /( 1.024ms*8 ).The final step would be to get OE hooked up to a pwm if wanted, where you could control brightness and even have each digit with its own brightness value (pwm value).

author: dentaku
author_rank: Frequent Contributor
timestamp: March 29, 2020, 09:38:08 pm
content: It just happens that I was hacking a speaking smoke detector last year and now that you mention it, I remember seeing the shiftOut code and modifying it to make is much slower because the chip I wanted to control only worked if you communicated with it super slowly.Quote from: Peabody on March 29, 2020, 04:27:02 am

author: dentaku
author_rank: Frequent Contributor
timestamp: March 29, 2020, 09:44:51 pm
content: I wasn't expecting anyone in the forums would write code for thisI'm not a programmer so that would take quite a bit of studying and learning new stuff just to begin understanding it.I'm going to look into timer interrupts first and do a few simple experiments. Maybe sometime I'll understand your code.and yes... I will almost certainly need the UART for something but I don't mind using SPI for this.Quote from: cv007 on March 29, 2020, 09:14:18 am

author: Peabody
author_rank: Super Contributor
timestamp: March 30, 2020, 04:26:43 am
content: If you use SPI, then you will either have to wait for the End of Transmission flag to be set before starting the second byte, or you can enable the interrupt on that flag. Then when that interrupt occurs, its ISR would intitiate transmission of the second byte and disable the end-of-transmission interrupt (so it won't interrupt again when the second byte completes). That's a bit more work than using the UART, but if you need the UART for something else, SPI should work well enough.If you are transmitting at maximum speed, there may not be much difference between waiting and using the interrupt. Normally the interrupt would be faster, but you have a lot of overhead pushing and pulling registers to/from the stack with an ISR, so it might be about the same either way. Basically, since you're probably going to be updating every millisecond or so, you just need to minimize the processor time devoted to an update so you can do other things.Either way, you do want to use cv007's algorithm so the last thing your update routine does is send the NEXT digit's segment bytes. That way you don't need to wait for the second byte to complete.

author: kripton2035
author_rank: Super Contributor
timestamp: March 30, 2020, 06:44:37 am
content: @dentaku: just out of curiosity, what software did you use to make the schematic on the first post ? thanks.

author: dentaku
author_rank: Frequent Contributor
timestamp: March 30, 2020, 11:36:16 pm
content: It's kind of a hacked together thing done with Circuit Wizard and some extra images pasted in it. It's just a simple little program so it doesn't have stuff like the big display and the LED driver in it's library of parts.Quote from: kripton2035 on March 30, 2020, 06:44:37 am

author: obiwanjacobi
author_rank: Super Contributor
timestamp: March 31, 2020, 07:59:49 am
content: Your diagram shows Pin 16 of the 74138 to GND. That should be +5V.