ESP8266: Controlling a LED matrix with the 74HC595 ICs

The objective of this post is to explain how to control the LEDs of a LED matrix using 2 74HC595 ICs and a ESP8266 microcontroller.

Introduction

The objective of this post is to explain how we can control a LED matrix using 2 74HC595 ICs and a ESP8266. With this method, we will only use 3 pins of the microcontroller, leaving the others free for different uses.

This will be a simple example that will allow to control if a LED of a predefined position is on at a given time. We will assume that only one LED will be on, at most, at each time. In future posts, we will cover more advanced control techniques.

This tutorial is based on 2 previous posts that explain how to create a LED matrix and how to control 2 chained 74HC595 with the ESP8266. Thus, we will not cover the basic details here, but rather concentrate on the code and methods used. You can check those previous posts here:

The hardware for this tutorial can be decomposed in two main blocks: the 2 chained 74HC595 ICs and the LED matrix.

As we can remember from the previous post, a LED matrix is decomposed in controlable rows and columns and, to turn on a given LED, we activate the corresponding column and row. So, with N (number of rows) + M (number of columns) pins, we can control M*N LEDs. Nevertheless, the limitation is that we can’t control the individual state of every LED at same time (to do so, we would need M*N pins).

In this example, we will use a 5X5 LED matrix (which means a total of 25 LEDs). In order to keep the schematic simple, we will draw this matrix as a black box with only 5 inputs for controlling the rows and 5 inputs for controlling the columns, and the power pins. To see how to design the actual matrix, please check the previously mentioned post, which details the components needed and how to connect them.

Although with the matrix technique we reduced the number of pins to control the LEDs to 10, that’s still a very high number for a microcontroller such as the ESP8266, which only has 17 GPIOs [1] that may even not be all available in certain prototyping boards.

So, the objective is to reduce the number of pins needed even more, in this case to 3. To do so, we will use 2 chained 74HC595 ICs, which are 8-bit Serial-In, Parallel-Out Shift Registers.

Basically, each IC can extend the number of controllable IO pins of a microcontroller. The 74HC595 can be controlled with only 3 pins but exposes 8 output pins. The state of those 8 output pins depends on the value of a byte previously transferred by the microcontroller via a serial interface (the 3 pins mentioned).

The best part is that we can chain multiple of these devices and keep controlling the chain with the same 3 pins we use for an individual IC, further enhancing the IO pin expansion.

This device was covered in greater detail in a series of previous posts about controlling it with the ESP8266. One of the methods used was controlling it via SPI, which we will also use here.

Since we have a total of 10 inputs of the matrix to control and each 74HC595 has 8 output pins, we need 2 of these ICs. But, to keep things easy, we will use one of the ICs to control the rows and the other to control the columns of the matrix, which will make the code and the connections a little bit simpler.

So, the diagram is illustrated bellow, in figure 1.

Figure 1 – Schematic diagram for controlling a 5X5 LED matrix with 2 74HC595 ICs and the ESP8266.

Since this schematic is a little bit complex, I suggest wiring and testing each block (the LED matrix and the 2 chained 74HC595) individually and just connect them in the end. If you don’t want to build the whole LED matrix, there are some already soldered at eBay.

Setup

In this case, we need to include the SPI library to control the 74HC595, which provides easy to use functions to interact with devices using this interface.

#include <SPI.h>

Since we are using a GPIO pin for as Slave Select (SS), we declare it in a global variable.

const int SSpin = 15;

Finally, in the setup function, we define the SS pin as an output pin and we initialize the SPI bus with the begin method.

void setup() {

pinMode(SSpin, OUTPUT);SPI.begin();

}

Since the microcontroller will only control the 74HC595 ICs, there are no more initializations needed.

Main loop

In this tutorial, we will start by controlling the matrix with two for loops and then create a function for a more reusable approach.

First of all, it’s important to take into consideration how the hardware for the LED matrix was designed, so we know how to control it. In this case, I’ve created my LED matrix with a particularity: the columns are activated by a VCC signal and the rows are activated by a GND signal.

So, to activate a row, the output pin of the controlling 74HC595 will need to have a LOW value (digital “0”) and to activate a column a HIGH value (digital “1”).

Nevertheless, the conceptual approach used here works just fine if all the control pins of the matrix are active LOW or active HIGH.

Since the 74HC595 have both 8 output pins, we will model them as 2 variables. In this case, we initialize them assuming that both the rows and the columns are inactive. We are going to specify the variables in binary format so it’s easier to map with the outputs of the shift registers.

byte rows = B11111111; //Rows are active LOW, so we initialize all bits as HIGHbyte columns = B00000000; //Columns are active HIGH, so we will initialize all bits as LOW

In order to keep thinking in terms of binary data, we can use the bitWrite function to define the value of a specific bit of a variable. So, let’s assume that we want to turn on the LED in column 2 and line 2.

The bitWrite function receives as input parameters, from left to right, the variable where to write, the position of the bit (starting at 0 for the least-significant) and the binary value (“1” or “0”) to write.

Just as a note, we are specifying rows and columns from 1 to 5 and the first bit of the variable corresponds to the bit 0. This is why we passed the position 1 and not 2 for the row and column to activate.

Now, we just need to write the value on both shift registers using SPI.

digitalWrite(SSpin, LOW); //Disable any internal transference in the SN74HC595SPI.transfer(columns); //Transfer columns data to the SN74HC595SPI.transfer(rows); //Transfer rows data to the SN74HC595digitalWrite(SSpin, HIGH); //Start the internal data transference in the SN74HC595

It’s very important to follow the correct order of writing to the shift registers. As explained in a previous post, when we chain 2 shift registers, the transference is propagated from the first to the second. So, the first byte we transfer will end in the last 74HC595 of the chain. Since, as seen in figure 1 of the previous section, the IC that controls the columns is the last one in the chain, we need to send its byte first.

The main loop function is shown bellow. The code used just iterates through each row and column of the matrix connecting an individual LED at each iteration.

digitalWrite(SSpin, LOW); //Disable any internal transference in the SN74HC595 SPI.transfer(columns); //Transfer data to the SN74HC595 SPI.transfer(rows); //Transfer data to the SN74HC595 digitalWrite(SSpin, HIGH); //Start the internal data transference in the SN74HC595 delay(3000);

}

}

}

You can see the result in the video bellow. In this case, only the matrix is shown, but the 74HC595 ICs were in a breadboard that can be seen in the beginning of the video.

Although this was omitted in the wiring diagram for simplicity, I was using an ULN2803A to control the columns (is an active HIGH IC) and some transistors to control the rows of the column (in an active LOW configuration).

Just to finish, we will encapsulate this concepts in a easy to use function called matrixWriteBit. This function will receive as parameter the row and column where to turn on the LED. In this case, let’s assume that both columns and rows are specified from 0 to 4, so we don’t need to subtract 1 to the parameters received.

Basically, we also start by declaring 2 variables to hold the data to control both the rows and the columns. They are initialized to keep rows and columns inactive. Then, we use again the bitWrite function to map the row and column received as parameters of the function with the bits that will control the matrix.

Finally, we send the data to the shift registers in the correct order, as we did before. The function is shown bellow.

void matrixWriteBit(int row, int column) {

byte rowData = B11111111;byte columnData = B00000000;

bitWrite(rowData, row, 0); bitWrite(columnData, column, 1);

digitalWrite(SSpin, LOW); //Disable any internal transference in the SN74HC595 SPI.transfer(columnData); //Transfer data to the SN74HC595 SPI.transfer(rowData); digitalWrite(SSpin, HIGH); //Start the internal data transference in the SN74HC595

}

Just take into consideration that every time we call this function, we will erase the previously stored values of the shift registers, and thus only a led will be on at each time. This happens because the function is stateless and we always initialize rows and columns as inactive. An easy way to not erase the previous values would be storing them, for example, in global variables and write the bits on those global variables before sending.