Scrolling Text On LED Matrices with an AVR MCU

The 8×8 LED matrix is a fun place to begin learning about how to control LEDs in a way that expresses something meaningful. You can create static images such as smiley faces, sprites, characters, and with some coding magic even create scrolling text and animations.

In this posting I will detail how to control 8×8 LED matrices with the MAX7219 Driver chip to create a cascaded scrolling text display. I will be using an AVR ATmega328P Microcontroller programed in C with the avr-gcc compiler.


 

The 8×8 LED matrix has two sets of 8 pins. Consider the pinout arrangement shown below, taken from a datasheet for a common cathode LED matrix.

In the diagram, “COL. #” and “ROW. #” refer to the column and row number for an LED, while the “PIN. NO.” number refers to the pin number on the actual pinout of the device. A certain group of pins control the anodes (+) for each specific column of LEDs , while the other group of pins control the cathodes (-) for each specific row of LEDs.

As an example, if we want to turn on the LED at column 4 and row 7, we would supply pin number 4 with the supply voltage, say 5V, and connect pin number 7 to ground.


Of course we would need to add in series resistors for either the group of row or column pins to control the current through each LED when they are on.

There are many ways to control the 16 pins on this device in order to display what we wish at any time. First we will be using a microcontroller, so the obvious brute force way to start would be to use 16 GPIO lines to either pull high or low each pin on the device in order to display a pattern on the LEDs. We could do better by using two 8-bit shift registers and shifting out two bytes to control the 16 pins, thus reducing the number of GPIO lines needed. We would also need to consider that we can only display either a single column or row at any time, as trying to control more than one column or row at the same time will lead to unintended LEDs being on as a side effect. We could try and handle the multiplexing in code, or we can pass that on to a dedicated piece of hardware.

The 8×8 LED matrix often comes packaged with a PCB that contains a MAX 7219 LED Display Driver. This package has become very affordable though online retailers, selling for around $2. I will be using these devices to achieve the end goal of a scrolling text display. The datasheet for the MAX7219 can be found here. Be sure to check the datasheet out, as it is well written and has everything you need to know about the device.

The MAX7219 chip allows you to shift in two bytes, one selecting a column address, and the second selecting which LEDs in the column to be on or off. We can achieve very high rates of data transfer by utilizing our microcontroller’s SPI communication hardware, thereby allowing us to update the led pattern very rapidly and efficiently.

The MAX7219 chips have a carry-out data line for chips to be cascaded, allowing many 8×8 displays to be controlled by just 3 of the SPI lines from the microcontroller.



Above is a schematic for the SPI setup for 3 cascaded devices. Click to enlarge.

SPI communication uses 4 lines from the microcontroller to control one device, MOSI, MISO, SS, and SCK. From our ATmega328P we use only 3 of these lines. MOSI (master out, slave in) is the line that the microcontroller sends data through to the slave (our first MAX7219). We don’t use MISO (master in, slave out) for this setup as we wont be reading data into the microcontroller from the MAX7219. We use SS (slave select) to let each MAX7219 know when we are ready to pass in data by holding the line LOW to signal we are about to send data, and by holding the line HIGH to signal that we are done sending data. SCK is the clock line from the master (our microcontroller) to the slave (MAX7219), and determines the speed of communication between the devices.

Our circuit has both SCK and SS routed to each MAX7219 device’s SCK and CS line, while MOSI is routed to the first MAX7219’s MISO (labelled DIN on PCB), with each cascaded device’s MISO routed to the MOSI (labelled DOUT on PCB) of the device before it. This chain of MOSI to MISO for each successive device will allow us to control many 8×8 LED matrices with only the 3 lines from the microcontroller.

Let’s now take a look at the C code, first with the header file.

#include <avr/io.h>
#include <util/delay.h>
#include <avr/pgmspace.h>

// Macros to set SS line LOW (selected) or HIGH (deselected)
#define SLAVE_SELECT    PORTB &= ~(1 << PB2)
#define SLAVE_DESELECT  PORTB |= (1 << PB2)

// Array holding arrays of 5 Bytes for each representation of an ASCII character, stored in flash 
char characters[96][5] PROGMEM = { 
 {0b00000000,0b00000000,0b00000000,0b00000000,0b00000000}, // space
 {0b00000000,0b00000000,0b01001111,0b00000000,0b00000000}, // !
 {0b00000000,0b00000111,0b00000000,0b00000111,0b00000000}, // "
 {0b00010100,0b01111111,0b00010100,0b01111111,0b00010100}, // #
 {0b00100100,0b00101010,0b01111111,0b00101010,0b00010010}, // $
 {0b00100011,0b00010011,0b00001000,0b01100100,0b01100010}, // %
 {0b00110110,0b01001001,0b01010101,0b00100010,0b01010000}, // &
 {0b00000000,0b00000101,0b00000011,0b00000000,0b00000000}, // '
 {0b00000000,0b00011100,0b00100010,0b01000001,0b00000000}, // (
 {0b00000000,0b01000001,0b00100010,0b00011100,0b00000000}, // )
 {0b00010100,0b00001000,0b00111110,0b00001000,0b00010100}, // *
 {0b00001000,0b00001000,0b00111110,0b00001000,0b00001000}, // +
 {0b00000000,0b01010000,0b00110000,0b00000000,0b00000000}, // ,
 {0b00001000,0b00001000,0b00001000,0b00001000,0b00001000}, // -
 {0b00000000,0b01100000,0b01100000,0b00000000,0b00000000}, // .
 {0b00100000,0b00010000,0b00001000,0b00000100,0b00000010}, // /
 {0b00111110,0b01010001,0b01001001,0b01000101,0b00111110}, // 0
 {0b00000000,0b01000010,0b01111111,0b01000000,0b00000000}, // 1
 {0b01000010,0b01100001,0b01010001,0b01001001,0b01000110}, // 2
 {0b00100001,0b01000001,0b01000101,0b01001011,0b00110001}, // 3
 {0b00011000,0b00010100,0b00010010,0b01111111,0b00010000}, // 4
 {0b00100111,0b01000101,0b01000101,0b01000101,0b00111001}, // 5 
 {0b00111100,0b01001010,0b01001001,0b01001001,0b00110000}, // 6
 {0b00000011,0b01110001,0b00001001,0b00000101,0b00000011}, // 7
 {0b00110110,0b01001001,0b01001001,0b01001001,0b00110110}, // 8
 {0b00000110,0b01001001,0b01001001,0b00101001,0b00011110}, // 9
 {0b00000000,0b01101100,0b01101100,0b00000000,0b00000000}, // :
 {0b00000000,0b01010110,0b00110110,0b00000000,0b00000000}, // ;
 {0b00001000,0b00010100,0b00100010,0b01000001,0b00000000}, // <
 {0b00010100,0b00010100,0b00010100,0b00010100,0b00010100}, // =
 {0b00000000,0b01000001,0b00100010,0b00010100,0b00001000}, // >
 {0b00000010,0b00000001,0b01010001,0b00001001,0b00000110}, // ?
 {0b00110010,0b01001001,0b01111001,0b01000001,0b00111110}, // @
 {0b01111110,0b00010001,0b00010001,0b00010001,0b01111110}, // A
 {0b01111111,0b01001001,0b01001001,0b01001001,0b00111110}, // B
 {0b00111110,0b01000001,0b01000001,0b01000001,0b00100010}, // C
 {0b01111111,0b01000001,0b01000001,0b01000001,0b00111110}, // D
 {0b01111111,0b01001001,0b01001001,0b01001001,0b01001001}, // E
 {0b01111111,0b00001001,0b00001001,0b00001001,0b00000001}, // F
 {0b00111110,0b01000001,0b01001001,0b01001001,0b00111010}, // G
 {0b01111111,0b00001000,0b00001000,0b00001000,0b01111111}, // H
 {0b01000001,0b01000001,0b01111111,0b01000001,0b01000001}, // I
 {0b00110000,0b01000001,0b01000001,0b00111111,0b00000001}, // J
 {0b01111111,0b00001000,0b00010100,0b00100010,0b01000001}, // K
 {0b01111111,0b01000000,0b01000000,0b01000000,0b01000000}, // L
 {0b01111111,0b00000010,0b00001100,0b00000010,0b01111111}, // M
 {0b01111111,0b00000100,0b00001000,0b00010000,0b01111111}, // N
 {0b00111110,0b01000001,0b01000001,0b01000001,0b00111110}, // O
 {0b01111111,0b00001001,0b00001001,0b00001001,0b00000110}, // P
 {0b00111110,0b01000001,0b01010001,0b00100001,0b01011110}, // Q
 {0b01111111,0b00001001,0b00001001,0b00011001,0b01100110}, // R
 {0b01000110,0b01001001,0b01001001,0b01001001,0b00110001}, // S 
 {0b00000001,0b00000001,0b01111111,0b00000001,0b00000001}, // T
 {0b00111111,0b01000000,0b01000000,0b01000000,0b00111111}, // U
 {0b00001111,0b00110000,0b01000000,0b00110000,0b00001111}, // V
 {0b00111111,0b01000000,0b00111000,0b01000000,0b00111111}, // W
 {0b01100011,0b00010100,0b00001000,0b00010100,0b01100011}, // X 
 {0b00000011,0b00000100,0b01111000,0b00000100,0b00000011}, // Y
 {0b01100001,0b01010001,0b01001001,0b01000101,0b01000011}, // Z
 {0b01111111,0b01000001,0b01000001,0b00000000,0b00000000}, // [
 {0b00000010,0b00000100,0b00001000,0b00010000,0b00100000}, // '\'
 {0b00000000,0b00000000,0b01000001,0b01000001,0b01111111}, // ]
 {0b00000100,0b00000010,0b00000001,0b00000010,0b00000100}, // ^
 {0b01000000,0b01000000,0b01000000,0b01000000,0b01000000}, // _
 {0b00000000,0b00000001,0b00000010,0b00000100,0b00000000}, // `
 {0b00100000,0b01010100,0b01010100,0b01010100,0b01111000}, // a
 {0b01111111,0b01001000,0b01000100,0b01000100,0b00111000}, // 0b
 {0b00111000,0b01000100,0b01000100,0b01000100,0b00100000}, // c
 {0b00111000,0b01000100,0b01000100,0b01001000,0b01111111}, // d
 {0b00111000,0b01010100,0b01010100,0b01010100,0b00011000}, // e
 {0b00001000,0b01111110,0b00001001,0b00000001,0b00000010}, // f
 {0b00001100,0b01010010,0b01010010,0b01010010,0b00111110}, // g
 {0b01111111,0b00001000,0b00000100,0b00000100,0b01111000}, // h
 {0b00000000,0b01000100,0b01111101,0b01000000,0b00000000}, // i
 {0b00100000,0b01000000,0b01000100,0b00111101,0b00000000}, // j
 {0b01111111,0b00010000,0b00101000,0b01000100,0b00000000}, // k
 {0b00000000,0b01000001,0b01111111,0b01000000,0b00000000}, // l
 {0b01111000,0b00000100,0b00001000,0b00000100,0b01111000}, // m
 {0b01111100,0b00001000,0b00000100,0b00000100,0b01111000}, // n
 {0b00111000,0b01000100,0b01000100,0b01000100,0b00111000}, // o
 {0b01111100,0b00010100,0b00010100,0b00010100,0b00001000}, // p
 {0b00001000,0b00010100,0b00010100,0b01111100,0b00000000}, // q
 {0b01111100,0b00001000,0b00000100,0b00000100,0b00001000}, // r
 {0b01001000,0b01010100,0b01010100,0b01010100,0b00100000}, // s
 {0b00000100,0b00111111,0b01000100,0b01000000,0b00100000}, // t
 {0b00111100,0b01000000,0b01000000,0b00100000,0b01111100}, // u
 {0b00011100,0b00100000,0b01000000,0b00100000,0b00011100}, // v
 {0b00111100,0b01000000,0b00110000,0b01000000,0b00111100}, // w
 {0b01000100,0b00101000,0b00010000,0b00101000,0b01000100}, // x
 {0b00001100,0b01010000,0b01010000,0b01010000,0b00111100}, // y
 {0b01000100,0b01100100,0b01010100,0b01001100,0b01000100}, // z          
 {0b00000000,0b00001000,0b00110110,0b01000001,0b00000000}, // {
 {0b00000000,0b00000000,0b01111111,0b00000000,0b00000000}, // |
 {0b00000000,0b01000001,0b00110110,0b00001000,0b00000000}, // }
 {0b00001000,0b00000100,0b00000100,0b00001000,0b00000100} // ~    
}; // characters[95]

// Message to be displayed, stored in flash 
const char message[] PROGMEM = "This is the message we want to scroll by.";

// Function Prototypes

void initSPI(void);

void writeByte(uint8_t byte);

void writeWord(uint8_t address, uint8_t data);
  
void initMatrix(void);

void clearMatrix(void);

void initBuffer(void);

void pushBuffer(uint8_t x);

void pushCharacter(uint8_t c);

void displayMessage(const char *arrayPointer, uint16_t arraySize);

void displayBuffer(void);

 

The #include’s provide AVR IO macros, delay functions, and functions to store and fetch data in program flash memory.

The two defines SLAVE_SELECT and SLAVE_DESELECT pull the SS line on the microcontroller LOW to tell the slave device we are ready to communicate, or HIGH to tell them we are done communicating. In case you haven’t seen code like this before I will explain how it works.

PORTB AND PB2 are both defined in in the avr/io.h file that was included in the beginning of the header. PORTB is a byte that determines the state of each pin located on the port containing pins PB0- PB7. The byte contains 1’s and 0’s, with 1 being high and 0 being low. To set a bit in the byte (or a pin in the port) high, we OR the byte with a byte containing all zeros except for the bit we wish to turn on. PB2 equates to the number 2, and (1 << 2) is the byte 00000100, which when OR’d with PORTB sets pin PB2, or SS, high. See the pinout diagram below.

When we want to set the same pin on the port low, we AND the negation (NOT) of the same byte.

Next we consider the characters[][] array, which contains 95 indices, with 5 chars (bytes) each. This array stores the column data that represents each ASCII character on the 8×8 matrices. It is stored in PROGMEM, or program flash memory, rather than RAM because this data is constant and doesn’t belong in the limited RAM space of our mircocontroller where dynamic variables belong.

If we take the entry for ‘T’ and stack the bytes vertically, and tilt our head to the right, we can see how the 1’s correspond to the bits that turn on LEDs to represent the letter ‘T’.

{0b00000001
,0b00000001
,0b01111111
,0b00000001
,0b00000001}

 

Below we find the function prototypes for each function in the main .c file, which we detail next.

#include "max7219_8x8.h"

#define NUM_DEVICES     3	 // Number of cascaded max7219's, or just 1
#define DEL 		14000    // Delay for scrolling speed in microseconds

// Buffer array of bytes to store current display data for each column in each cascaded device
uint8_t buffer [NUM_DEVICES*8];		

 

Now in the main .c source code, we include the contents of the header and define a couple of constants. NUM_DEVICES holds the number of MAX7219 chips we have cascaded, while DEL stores a number in microseconds we will use later to affect the speed of our scrolling text.

The buffer[] array will contain 3*8 bytes for our three 8×8 matrices. The buffer[] is stored in RAM, as its contents will be changing all the time as bytes pass in and text is scrolling across the display.

// Initializes SPI
void initSPI(void) 
{
  DDRB  |= (1 << PB2);	    // Set SS output 
  PORTB |= (1 << PB2);      // Begin high (unselected)

  DDRB |= (1 << PB3);       // Output on MOSI 
  DDRB |= (1 << PB5);       // Output on SCK 

  SPCR |= (1 << MSTR);      // Clockmaster 
  SPCR |= (1 << SPE);       // Enable SPI
}

 

The initSPI() function initializes the DDR (data direction register) for PORTB which contains our SPI pins. We set the line as an output by setting a 1 bit in the DDR for each line we will use. SPCR is the SPI Control Register, and we set the bits in this register that tell it to be the clockmaster, controller of SCK, and to enable the SPI hardware.

// Send byte through SPI
void writeByte(uint8_t byte)
{
  SPDR = byte;                      // SPI starts sending immediately  
  while(!(SPSR & (1 << SPIF)));     // Loop until complete bit set
}

 

The writeByte() function takes a byte to send over the SPI lines and writes it to the SPI Data Register. We then wait in a while loop until the SPIF bit in the SPI Status Register is set to 1, signaling that the SPI data transfer is complete. Here I chose to wait in a while loop, but we could chose to not wait and have an interrupt let us know when the SPI data transfer is complete, thus allowing to do something else in the meantime.

// Sends word through SPI
void writeWord(uint8_t address, uint8_t data) 
{
  writeByte(address);	// Write first byte
  writeByte(data);      // Write Second byte
}

 

The writeWord() function takes two bytes, address and data and sends then over the SPI lines. This function will be used to send a byte representing an address to a MAX7219 register, mainly column registers, as well as a second byte for the data to be written to the register addressed by the initial byte.

// Initializes all cascaded devices
void initMatrix() 
{
	uint8_t i;	// Var used in for loops

	// Set display brighness
	SLAVE_SELECT;
	for(i = 0; i < NUM_DEVICES; i++)   // Loop through number of cascaded devices
	{
		writeByte(0x0A); // Select Intensity register
		writeByte(0x07); // Set brightness
	}
	SLAVE_DESELECT;

	
	// Set display refresh
	SLAVE_SELECT;
	for(i = 0; i < NUM_DEVICES; i++)
	{
		writeByte(0x0B); // Select Scan-Limit register
		writeByte(0x07); // Select columns 0-7
	}
	SLAVE_DESELECT;

	 
	// Turn on the display
	SLAVE_SELECT;
	for(i = 0; i < NUM_DEVICES; i++)
	{
		writeByte(0x0C); // Select Shutdown register
		writeByte(0x01); // Select Normal Operation mode
	}
	SLAVE_DESELECT;

	 
	// Disable Display-Test
	SLAVE_SELECT;
	for(i = 0; i < NUM_DEVICES; i++)
	{
		writeByte(0x0F); // Select Display-Test register
		writeByte(0x00); // Disable Display-Test
	}
	SLAVE_DESELECT;
}

 

The initMatrix() function initializes the relevant registers in all of the cascaded MAX7219 devices. When we set each register type, we first SELECT_SLAVE, loop through all the devices and send the register address and data bytes for each device, then SLAVE_DESELECT to complete the transaction.

First we set the Intensity register which sets the brightness of the LEDs. The MAX7219 uses one resistor to set the maximum current through all LEDs controlled by the chip. The chip also contains a PWM register and circuit that can reduce this maximum current even more depending on the status of its register, thereby allowing further control of LED brightness. Here we set an arbitrary brightness of 7.

Next we set the Scan-Limit register to 7 which will allow us to use all of the LEDs in columns 0-7.

We then turn on the display by selecting the Shutdown Register and writing the normal operation code, and finally disable the Display-Test which turns on all LEDs as a test case.

// Clears all columns on all devices
void clearMatrix(void)
{
	for(uint8_t x = 1; x < 9; x++) // for all columns
	{   
        SLAVE_SELECT;
        for(uint8_t i = 0; i < NUM_DEVICES; i++)
		{
			writeByte(x);    // Select column x
			writeByte(0x00); // Set column to 0
		}
		SLAVE_DESELECT;
	}
}

 

The clearMatrix() function begins to give an idea as to how we will write bytes to columns in our display to set the LED states. We have an outer for loop that loops through the 8 columns in each device. Inside the loop we first SLAVE_SELECT to begin communicating, and then enter an inner for loop that loops through each device ‘i’ and writes a column address byte and then an empty byte to turn each column ‘x’ off. We then SLAVE_DESELECT to complete our action and repeat this action 7 more times for each remaining column in each 8×8 matrix.

// Initializes buffer empty
void initBuffer(void)
{   
	for(uint8_t i = 0; i < NUM_DEVICES*8; i++)
		buffer[i] = 0x00;
}       

 

The initBuffer() function initializes our buffer[] of bytes to hold all empty bytes.

// Moves each byte forward in the buffer and adds next byte in at the end
void pushBuffer(uint8_t x)
{
	for(uint8_t i = 0; i < NUM_DEVICES*8 - 1; i++)
		buffer[i] = buffer[i+1];
	
	buffer[NUM_DEVICES*8 - 1] = x;
}

 

The pushBuffer() function is what allows us to scroll bytes across the display.  It first shifts each byte forward in the display buffer and then puts the byte we pass to it at the end of the buffer.

// Pushes in 5 characters columns into the buffer.
void pushCharacter(uint8_t c)
{
		for(uint8_t j = 0; j < 5; j++)				// For 5 bytes representing each character
		{
			pushBuffer(pgm_read_byte(&characters[c][j]));   // Push the byte from the characters array to the display buffer
			displayBuffer();				// Display the current buffer on the devices
			_delay_us(DEL);					// and delay
		}
}

 

The pushCharacter() function allows us to scroll characters across the display. Recall that in the characters[i][j] array, each index i contains j bytes that represent an ASCII character. We pass pushCharacter() a char c, and it pushes the corresponding 5 bytes to the display buffer, updating the display and delaying between each byte push.

The pgm_read_byte function is from the avr/pgmspace.h include and allows us to retrieve a byte stored in program flash memory. We have to pass this function an the address of the byte we want to send to pushBuffer() and use the address (&) operator to specify the address of the byte to push to the buffer.

// Takes a pointer to the beginning of a char array holding message, and array size, scrolls message.
void displayMessage(const char *arrayPointer, uint16_t arraySize)
{
	for(uint16_t i = 0; i < arraySize; i++)
	{
		pushCharacter(pgm_read_byte_near(arrayPointer + i) - 32);	// Send converted ASCII value of character in message to index in characters array (-32 sends corrent index to characters array)
		pushBuffer(0x00);						// Add empty column after character for letter spacing
		displayBuffer();						// Display &
		_delay_us(DEL); 						// Delay
	}
	
}

 

The displayMessage() function allows us to finally scroll a message across the display. We pass it a pointer to the beginning (index 0) of our message’s char array, and the size of the array. We fetch each character from program flash memory with the pgm_read_byte_near() that takes the ith character address starting at the initial arrayPointer address. We then subtract 32 from this char (byte) to align it with the slice of ASCII characters we provide in the characters[][] array (compare the characters[][] array with an ASCII table). We then push this char to the pushCharacter() function that then pushes the appropriate bytes from the characters[][] array to the display buffer, while setting the display and delaying between each byte pushed in. We finally push to the buffer an empty byte which acts as a space between characters on the display.

// Displays current buffer on the cascaded devices
void displayBuffer()
{   
   for(uint8_t i = 0; i < NUM_DEVICES; i++) // For each cascaded device
   {
	   for(uint8_t j = 1; j < 9; j++) // For each column
	   {
		   SLAVE_SELECT;
		   
		   for(uint8_t k = 0; k < i; k++) // Write Pre No-Op code
			   writeWord(0x00, 0x00);
		   
		   writeWord(j, buffer[j + i*8 - 1]); // Write column data from buffer
		   
		   for(uint8_t k = NUM_DEVICES-1; k > i; k--) // Write Post No-Op code
			   writeWord(0x00, 0x00);
		   
		   SLAVE_DESELECT;
	   }
   }
}

 

Now we consider displayBuffer(), where we actually set our physical display to display the contents of buffer[]. To write each byte to each column in the cascaded 8×8 displays we have to send in appropriate No-Op codes before and after the byte data we actually care about. For our setup we have 3 cascaded displays, so if I want to set column 1 in the middle display I need to SLAVE_SELECT, send in two empty bytes for the last MAX7219, the column 1 address and corresponding byte to write to the middle MAX7219, and then two empty bytes for the first MAX7219, and finally SLAVE_DESELECT to finish the transaction.

The two for loops with variable k are generalized to send the correct number of No-Op codes before and after the data we wish to send to a device between any number of MAX7219’s in a cascaded arrangement.

The two outer loops cycle through all devices, and the 8 columns for each device.

With the above functionality we can finally move on to the main() function.

// Main Loop
int main(void) 
{
  // Inits
  initSPI();
  initMatrix();
  clearMatrix();
  initBuffer();
  
  // Pointer to beginning of message
  const char *messagePointer = &message[0];
  
  // Size of message matrix
  uint16_t messageSize = sizeof(message);
  
  // Event loop
  while (1) 
  {
   
   displayMessage(messagePointer, messageSize);	// Display the message
   
  }                                                 
  return (0);                          
}

 

We first call our initialization functions we wrote, to initialize SPI, initialize the MAX7219 registers, clear the display, and initialize the buffer[].

We create a pointer to the first address in message[] char array, and store the sizeof() our message[].

With the complex modular functions we created before, the event loop simply passes the message pointer and message size to displayMessage() function and our message scrolls across the display!

Using an ATmega328P, I was able to cascade 8 devices, and store the first two chapters of Alice’s Adventures In Wonderland (~22,600 bytes) in program flash memory to scroll across the display. The gif above was taken in 1/8th slow motion to remove any choppiness from the animation at the normal scrolling speed. The actual speed of the scrolling text with this setup can be very fast, even with a sizable number of cascaded matrices. I only have 8 units at the moment, but would be interested in buying more to extend this display.

Here is a link to the source code.

13 thoughts on “Scrolling Text On LED Matrices with an AVR MCU

  1. MY FINAL CODE COPIED FROM THIS SIDE IS with all function definations compiled it successuflly .generated hex file but the circuit didnot give output on max7219 ics output port ,,, please check the code if there is any mistake … thankyou
    [code]

    Like

  2. ok finally i successfully did this project thank you very much ! but i have a problem ,, all the matrixes are cascaded serially ,, first one donot glow but the rest of the 3 matrix are glow successuflly it happens 1 times in every 20 times i connect the power source ,, do u have any idea how to display character in first led matrix ,, all are similar matrixes and i dont think that the connection is broken if connection is broken then how the other two matrixes displayed but not 1st one ! please give me the solution

    Like

    1. Hi, I am glad you got it working and found this helpful. I have experienced the behavior you have explained, so it is normal given this setup. I believe the reason has to do with noise from the power source from not putting in proper bypass capacitors across the power rails. I have also had a matrix go blank after moving the jumper wires around, which probably introduced noise or corrupted the signal.

      Liked by 2 people

  3. how to use a dynamic string for the text input?

    lets say i attach lm35 to the ADC and want to display the temperature

    Thankyou.

    Like

    1. I did a project with that sensor, but displaying in an LCD. I think all you have to do is save the information in the ‘message’ array using ‘sprintf(message[], type of variable, variableWhereADCMeausreIsSaved)’.

      Like

  4. Can this be used to make an snake game? If so, how can i stop it from scrolling. I’m trying to make it in one 8×8 matrix.

    Like

  5. Hi, thanks for sharing. It really helped me with understanding how to interface the matrix. My text cascades vertically and not horizontally like the video you posted. Any idea on how to change this?

    Like

  6. I think what can be done with the least amount of effort is to rotate the matrix sideways, leaving the matrix control program code unchanged. To do this, you would only need to change the code of the symbols, as if to stack them sideways. In code: char characters[96][5] PROGMEM = {

    Like

Leave a comment