Before considering the code, we need to consider what we are trying to accomplish. Commands for the TM1638 consist of a command byte followed by zero or more data bytes. Sometimes the microcontroller sends the data, other times it receives data, and in either case there is a single data wire used (MOSI and MISO connected together). The STB line (in SPI-speak, SS) must be brought low at the start of the command and brought high at the end of each command.
There are also some timing considerations which are important to us since we are communicating with the TM1638 much faster than the conventional drivers and could end up trying to run faster than spec. What’s of importance here is that the clock frequency must not be greater than 1.25MHz, there must be 1µs from the trailing edge of the last clock pulse to the rising edge of STB, and there must be at least 1µs between the rising edge of STB and the next falling edge of STB.
There are five commands, although the Set Address command is often ignored in articles as a command and just considered part of the data. Here’s a table of the commands:
The Display Control Command is typically issued just once to turn on the display and set its intensity. There are two commands for writing to the display, the Single Address Command or the Auto-address Add Command. With the former, a single 7-segment display digit or LED is written to at a time, using the Set Address Command for each location. The latter command uses a single following Set Address Command and a string of data bytes to write to consecutive locations.
In my application I want the display update to occur in the background. Because it is so fast I use the Auto-address Add Command and write to all 16 locations, 8 7-segment display digits and 8 LEDs. I don’t use the Single Address command at all. When writing the LED bytes, a value of 0 is off and a value of 1 is on.
The final command, Read Keys, is used to read the 8 push buttons. This requires changing the direction of data, and the data is read in four bytes, two push buttons per byte.
The source code for the test program can be downloaded here https://almy.us/files/TM1638_test.zip and you will also need https://almy.us/Pins.zip. Both of these are copyrighted but are distributed as free software under the GNU General Public License. While it has conditional compilations to use on the Uno/Nano/Nano Every/Mega/Leonardo/Micro/Pro Micro boards (and it probably will work on other AVR-based Arduino boards as well) the discussion that follows will show only the Uno code. Frankly, the only significant difference is for the Nano Every with the ATmega4809 microcontroller. Not all the source code is shown below, only the more interesting parts!
The program has the following global variables:
uint8_t buffer_LED = 0; // LSB is leftmost LED
uint8_t buffer_display[8] = {10, 11, 12, 13+16, 5, 6, 7, 8};
volatile uint8_t button_values = 0; // LSB is leftmost LED
// These are private variables for the SPI state machine
volatile uint8_t spi_index = 0;
volatile uint8_t tm1638_command = 0;
The first two variables are the buffers for the LED and 7-segment display values. Here all 8 LED values are in a single byte. for convenience. The initial display value is arbitrary. In operation the program sets these variables and calls updateDisplay(). Because it isn’t double buffered (to save memory) the program may not change the values until the transfer is complete, which is indicated by the spi_idle() function returning non-zero.
When the readButtons() function is called, the button_values variable is updated with the states of all eight buttons. Again, the values will only be valid after spi_idle() returns non-zero.
UpdateDisplay() and readButtons() are non-blocking if the SPI is idle when they are called. This will give optimum system performance in programs that can take advantage of the non-blocking operation. If SPI is running, the functions will block until the SPI is available for the new command.
Initialization is done in the setup() function. TM1638_STB has been defined to be digital_2. We set all the pin directions and set STB high. Then we configure the SPI, enabling it and its interrupt, setting Master mode, CPOL=1, CPHA=1, and DORD=1 (LSB sent first). Clock speed is set to 1MHz, the fastest available without going over 1.25MHz. Finally we issue the Activate Display Command. The delay insures it completes before we might try to issue another command.
ddr.digital_SS = 1; // This pin must be output
ddr.TM1638_STB = 1;
port.TM1638_STB = 1;
ddr.digital_MOSI = 1; // Output for now
ddr.digital_SCK = 1;
// Enable all but interrupt, LSB first, 1MHz clock
// Negative clock (CPOL=1) and reversed phase (CPHA=1)
// so that we can do the direction change.
SPCR = (1 << SPE) + (1 << SPIE) + (1 << MSTR) + (1 << CPOL) + (1 << CPHA) + (1 << DORD) + 1;
// Do initialization of TM1638
port.TM1638_STB = 0;
SPDR = 0x89; // Activate display, low (level 1) brightness
delay(1); // Time to execute above
The command is completed in the ISR by raising STB.
The only functions directly accessed by the application are spi_idle(), updateDisplay(), and readButtons(). After starting the sending of the first command, all further activity is handled by state machines in the SPI ISR. In particular the variable spi_index is the state variable for the state machines and there is effectively one state machine per command, where the variable tm1638_command has the command.
inline bool spi_idle(void) {
return (tm1638_command == 0);
}
void updateDisplay(void) {
while (!spi_idle()); // block if SPI still running
port.TM1638_STB = 0; // start command
tm1638_command = 0x40;
SPDR = 0x40; // command to write data incrementing address
}
void readButtons(void) {
while (!spi_idle()); // block if SPI still running
port.TM1638_STB = 0; // start command
tm1638_command = 0x42;
SPDR = 0x42;
}
Inside the ISR for writing the entire display, it must end the first command, issue a second, address command, and send 16 bytes of data, all one byte per interrupt. After the final byte has been sent it must raise STB and reset the variables. Note the function delay1us() provides the necessary delay for the timing requirements of the tm1638.
if (tm1638_command == 0x40) { // write all display
if (spi_index == 0) { // start
// End command byte and set the address
port.TM1638_STB = 1;
delay1us();
port.TM1638_STB = 0;
SPDR = 0xc0;
spi_index = 2; // index plus 2 when writing
} else if (spi_index == 18) { // we are done
delay1us();
port.TM1638_STB = 1;
delay1us();
spi_index = 0;
tm1638_command = 0;
} else {
uint8_t index = (spi_index >> 1) - 1;
if (spi_index & 1) { // LED
SPDR = (buffer_LED >> index) & 1;
} else { // 7-segment display
SPDR = pgm_read_byte(&SEGMENT_MAP[buffer_display[index]]);
}
spi_index++;
}
}
And, finally, for reading the buttons we need to change the bus direction by changing the MOSI pin to an input. We still read from the MISO pin, though. We have to send something to start each read, so we send zeros.
if (tm1638_command == 0x42) { // Read switches/buttons
if (spi_index == 0) { // start
// switch direction and only read
ddr.digital_MOSI = 0;
spi_index = 1;
button_values = 0;
SPDR = 0; // start read
} else if (spi_index < 4) { // read first three button bytes
button_values |= SPDR << (spi_index - 1);
SPDR = 0; // start next read
spi_index++;
} else { // last button byte
button_values |= SPDR << 3;
delay1us();
port.TM1638_STB = 1;
delay1us();
ddr.digital_MOSI = 1; // Do last, after SS is high
tm1638_command = 0;
spi_index = 0;
}
}