Tutorial: Arduino and the NXP SAA1064 4-digit LED display driver
Welcome back fellow arduidans!
In this article we investigate controlling the NXP (formerly Philips) SAA1064 4-digit LED display driver IC with Arduino and the I2C bus interface. If you are not familiar with using the I2C bus, please read tutorials (parts one and two) before moving on. Although the SAA1064 is not the newest on the market, it is still popular, quite inexpensive and easy to source. Furthermore as it is controlled over the I2C bus – you don’t waste any digital I/O pins on your Arduino, and you can also operate up to four SAA1064s at once (allowing 16 digits!). Finally, it has a constant-current output – keeping all the segments of your LED display at a constant brightness (which is also adjustable). So let’s get started…
Here is an example of the SAA1064 in SOIC surface mount packaging:
It measures around 15mm in length. For use in a solderless breadboard, I have soldered the IC onto a through-hole adaptor:
The SAA1064 is also available in a regular through-hole DIP package. At this point, please download the data sheet (.pdf) as you will need to refer to it during the article. Next, our LED display examples. We need common-anode displays, and for this article we use two Agilent HDSP521G two-digit modules (data sheet [.pdf]) as shown below:
For the uninitiated – a common anode display has all the segments’ anodes connected together, with the cathodes terminated separately. For example, our LED displays are wired as such:
Notice the anodes for the left digit are pin 14, and the right digit pin 13. A device that is connected to all the cathodes (e.g. our SAA1064) will control the current flow through each element – thereby turning each segment on (and controlling the brightness) or off. Our SAA1064 is known as acurrent-sink as the current flows through the LED, and then sinks into the IC.
Now, let’s get it connected. There is an excellent demonstration circuit on page twelve of the data sheet that we will follow for our demonstrations:
It looks pretty straight-forward, and it is. The two transistors are standard NPN-type, such as PN2222. The two transistors are used to each turn on or off a pair of digits – as the IC can only drive digits 1+3 or 2+4 together. (When presented in real life the digits are numbered 4-3-2-1). So the pairs are alternatively turned on and off at a rapid rate, which is controlled by the capacitor between pin 2 and GND. The recommended value is 2.7 nF. At the time of writing, I didn’t have that value in stock, so chose a 3.3 nF instead. However due to the tolerance of the ceramic capacitor it was actually measured to be 2.93 nF:
So close enough to 2.7 nF will be OK. The other capacitor shown between pins 12 and 13 is a standard 0.1 uF smoothing capacitor. Pin 1 on the SAA1064 is used to determine the I2C bus address – for our example we have connected it straight to GND (no resistors at all) resulting in an address of 0×70. See the bottom page five of the data sheet for other address options. Power for the circuit can be taken from your Arduino’s 5V pin – and don’t forget to connect the circuit GND to Arduino GND. You will also use 4.7k ohm pull-up resistors on the SDA and SCL lines of the I2C bus.
The last piece of the schematic puzzle is how to connect the cathodes of the LED displays to the SAA1064. Display pins 14 and 13 are the common anodes of the digits.
The cathodes for the left-hand display module:
- LED display pins 4, 16, 15, 3, 2, 1, 18 and 17 connect to SAA1064 pins 22, 21, 20, 19, 18, 17, 16 and 15 respectively (that is, LED pin 4 to IC pin 22, etc.);
- LED display pins 9, 11, 10, 8, 6, 5, 12 and 7 also connect to SAA1064 pins 22, 21, 20, 19, 18, 17, 16 and 15 respectively.
- LED display pins 4, 16, 15, 3, 2, 1, 18 and 17 connect to SAA1064 pins 3, 4, 5, 6, 7, 8, 9 and 10 respectively;
- LED display pins 9, 11, 10, 8, 6, 5, 12 and 7 also connect to SAA1064 pins 3, 4, 5, 6, 7, 8, 9 and 10 respectively.
Wire.beginTransmission(saa1064);
Wire.send(B00000000);
Wire.send(B01000111);
Wire.beginTransmission(saa1064);
Wire.send(B00000001);
The digit bytes describe which digit elements to turn on or off. The bytes are described as such: Bpgfedcba. (p is the decimal point). So if you wanted to display the number 7, you would send B00000111 – as this would turn on elements a, b and c. To add the decimal point with 7 you would send B10000111. You can also send the byte as a decimal number. So to send the digit 7 as a decimal, you would send 7 – as 00000111 in base-10 is 7. To include the decimal point, send 135 – as 100000111 in base-10 is 135. Easy! You can also create other characters such as A~F for hexadecimal. In fact let’s do that now in the following example sketch (download):/*
NXP SAA1064 I2C LED Driver IC Demo I
Demonstrating display of digits
*/
#include "Wire.h" // enable I2C bus
byte saa1064 = 0x70 >> 1; // define the I2C bus address for our SAA1064 (pin 1 to GND)
int digits[16]={63, 6, 91, 79, 102, 109, 125,7, 127, 111, 119, 124, 57, 94, 121, 113};
// these are the byte representations of pins required to display each digit 0~9 then A~F
void setup()
{
Wire.begin(); // start up I2C bus
delay(500);
initDisplay();
}
void initDisplay()
// turns on dynamic mode and adjusts segment current to 12mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B01000111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 12mA segment current
Wire.endTransmission();
}
void displayDigits()
// show all digits 0~9, A~F on all digits of display
{
for (int z=0; z<16; z++)
{
Wire.beginTransmission(saa1064);
Wire.send(1); // instruction byte - first digit to control is 1 (right hand side)
Wire.send(digits[z]); // digit 1 (RHS)
Wire.send(digits[z]); // digit 2
Wire.send(digits[z]); // digit 3
Wire.send(digits[z]); // digit 4 (LHS)
Wire.endTransmission();
delay(500);
}// now repeat but with decimal point
for (int z=0; z<16; z++)
{
Wire.beginTransmission(saa1064);
Wire.send(1); // instruction byte - first digit to control is 1 (right hand side)
Wire.send(digits[z]+128); // digit 1 (RHS)
Wire.send(digits[z]+128); // digit 2
Wire.send(digits[z]+128); // digit 3
Wire.send(digits[z]+128); // digit 4 (LHS)
Wire.endTransmission();
delay(500);
}
}
void clearDisplay()
// clears all digits
{
Wire.beginTransmission(saa1064);
Wire.send(1); // instruction byte - first digit to control is 1 (right hand side)
Wire.send(0); // digit 1 (RHS)
Wire.send(0); // digit 2
Wire.send(0); // digit 3
Wire.send(0); // digit 4 (LHS)
Wire.endTransmission();
}
void loop()
{
displayDigits();
clearDisplay();
delay(1000);
}In the function initDisplay() you can see an example of using the instruction then the control byte. In the function clearDisplay() you can see the simplest form of sending digits to the display – we send 0 for each digit to turn off all elements in each digit. The bytes that define the digits 0~9 and A~F are stored in the array digits[]. For example, the digit zero is 63 in decimal, which is B00111111 in binary – which turns on elements a,b,c,d,e and f. Finally, notice the second loop in displayDigits() – 128 is added to each digit value to turn on the decimal point. Before moving on, let’s see it in action:
Our next example revisits the instruction and control byte – we change the brightness of the digits by setting bits 4~6 in the control byte. Each level of brightness is separated into a separate function, and should be self-explanatory. Here is the sketch (download):
/*
NXP SAA1064 I2C LED Driver IC Demo II
Demonstrating brightness levels via adjusting segment current
*/
#include "Wire.h" // enable I2C bus
byte saa1064 = 0x70 >> 1; // define the I2C bus address for our SAA1064 (pin 1 to GND)
int digits[16]={63, 6, 91, 79, 102, 109, 125,7, 127, 111, 119, 124, 57, 94, 121, 113};
// these are the byte representations of pins required to display each digit 0~9 then A~F
void setup()
{
Wire.begin(); // start up I2C bus
delay(500);
init12ma();
}
void init3ma()
// turns on dynamic mode and adjusts segment current to 3mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B00010111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 3mA segment current
Wire.endTransmission();
}
void init6ma()
// turns on dynamic mode and adjusts segment current to 6mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B00100111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 6mA segment current
Wire.endTransmission();
}
void init9ma()
// turns on dynamic mode and adjusts segment current to 9mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B00110111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 9mA segment current
Wire.endTransmission();
}
void init12ma()
// turns on dynamic mode and adjusts segment current to 12mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B01000111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 12mA segment current
Wire.endTransmission();
}
void init18ma()
// turns on dynamic mode and adjusts segment current to 12mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B01100111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 18mA segment current
Wire.endTransmission();
}
void init21ma()
// turns on dynamic mode and adjusts segment current to 12mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B01110111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 21mA segment current
Wire.endTransmission();
}
void loop()
{
int d=250; // for delay
// first, light up all segments
Wire.beginTransmission(saa1064);
Wire.send(0); // instruction byte - Zero means the next byte is the control byte
Wire.send(B01001111); // set current and bit 3 to 1 for all segments on
Wire.endTransmission();
// now loop through the brightness levels
do
{
init3ma();
delay(d);
init6ma();
delay(d);
init9ma();
delay(d);
init12ma();
delay(d);
init18ma();
delay(d);
init21ma();
delay(d);
}
while (1>0);
}And again, see it in action:
For our final example, there is a function displayInteger(a,b) which can be used to easily display numbers from 0~9999 on the 4-digit display. The parameter a is the number to display, and b is the leading-zero control – zero – off, one – on. The function does some maths on the integet to display and separates the digits for each column, then sends them to the SAA1064 in reverse order. By now you should be able to understand the following sketch (download):
/*
NXP SAA1064 I2C LED Driver IC Demo III
Displaying numbers on command
*/
#include "Wire.h" // enable I2C bus
byte saa1064 = 0x70 >> 1; // define the I2C bus address for our SAA1064 (pin 1 to GND)
int digits[17]={
63, 6, 91, 79, 102, 109, 125,7, 127, 111, 119, 124, 57, 94, 121, 113, 0};
// these are the byte representations of pins required to display each digit 0~9 then A~F, and blank digit
void setup()
{
Wire.begin(); // start up I2C bus
delay(500);
initDisplay();
}
void initDisplay()
// turns on dynamic mode and adjusts segment current to 12mA
{
Wire.beginTransmission(saa1064);
Wire.send(B00000000); // this is the instruction byte. Zero means the next byte is the control byte
Wire.send(B01000111); // control byte (dynamic mode on, digits 1+3 on, digits 2+4 on, 12mA segment current
Wire.endTransmission();
}
void clearDisplay()
{
Wire.beginTransmission(saa1064);
Wire.send(1); // start with digit 1 (right-hand side)
Wire.send(0); // blank digit 1
Wire.send(0); // blank digit 2
Wire.send(0); // blank digit 3
Wire.send(0); // blank digit 4
Wire.endTransmission();
}
void displayInteger(int num, int zero)
// displays the number 'num'
// zero = 0 - no leading zero
// zero = 1 - leading zero
{
int thousand, hundred, ten, one;
// breakdown number into columns
thousand = num/1000;
hundred = (num-(thousand*1000))/100;
ten = (num-((thousand*1000)+(hundred*100)))/10;
one = num-((thousand*1000)+(hundred*100)+(ten*10));
if (zero==1) // yes to leading zero
{
Wire.beginTransmission(saa1064);
Wire.send(1);
Wire.send(digits[one]);
Wire.send(digits[ten]);
Wire.send(digits[hundred]);
Wire.send(digits[thousand]);
Wire.endTransmission();
delay(10);
}
else
if (zero==0) // no to leading zero
{
if (thousand==0) { thousand=16; }
if (hundred==0 && num<100) { hundred=16; }
if (ten==0 && num<10) { ten=16; }
Wire.beginTransmission(saa1064);
Wire.send(1);
Wire.send(digits[one]);
Wire.send(digits[ten]);
Wire.send(digits[hundred]);
Wire.send(digits[thousand]);
Wire.endTransmission();
delay(10);
}
}
void loop()
{
for (int a=0; a<251; a++)
// display 0~250 without leading zero
{
displayInteger(a,0);
delay(20);
}
delay(1000);
clearDisplay();
delay(1000);
for (int a=0; a<1000; a++)
// display 0~999 with leading zero
{
displayInteger(a,1);
delay(5);
}
}And the final example in action:
So there you have it – another useful IC that can be used in conjunction with our Arduino systems to make life easier and reduce the required digital output pins. I hope you enjoyed reading this as much as I did writing it for you. If you have any suggestions with regards to our next article, leave a comment below and we’ll look into it. Furthermore, don’t be shy in pointing out errors or places that could use improvement. Why not follow us on twitter and facebook to keep up with new articles, news and other items of interest.
Otherwise, have fun, stay safe, be good to each other – and make something!






