MAX7219 ← ATtiny85 Interface for LED Matrix 8×8

MAX7219 Module

What’s our goal?

To connect a MAX7219 based LED Matrix module to an ATtiny85 microcontroller and control the LEDs.

Doesn’t sound very complicated, right?

The Hardware

MAX7219 IC

The MAX7219 controller manufactured by Maxim Integrated is a compact, serial input/output common-cathode display driver that could interface microcontrollers to 64 individual LEDs, 7-segment numeric LED displays of up to 8 digits, bar-graph displays, etc. Included on-chip are a BCD code-B decoder, multiplex scan circuitry, segment and digit drivers, and an 8×8 static RAM that stores each digit. 

The MAX7219 modules are very convenient to use with microcontrollers such as ATtiny85, or, in our case with the Tinusaur Boards.

They have an input bus on one side and an output bus on the other. This allows you to daisy chain 2, 3, and more modules, i.e. one after another, to create more complicated setups.

The modules that we are using are capable of connecting in a chain using 5 small jumpers. See the picture below.

Pinout and Signals

MAX7219 Module Pinout

MAX7219 module that we are going to use has input 5 pins:

  • VCC – power (+)
  • GND – ground (-)
  • DIN – Data input
  • CS – Chip select
  • CLK – Clock

That means that we will need 3 ATtiny85  pins to control the module. Those will be:

  • PB0 connected to CS
  • PB1 connected to DIN
  • PB2 connected to CLK

Why did we choose those ATtiny85 pins to connect to those MAX7219?

If we look at the ATtiny85 pinout (picture below) we will notice the following:

ATtiny85 Pinout
  • The PB2 pin is often used to output a clock signal ( SCK / USCK / SCL ), so it will be a good idea to use it for the same thing in our setup.
  • The PB1 pin is commonly used to output data ( MISO / DO ) so we could use it the same way.
  • The PB0 pin is often used for data input. We don’t need to input data so we will use it for the CS signal.

These are all the signals that we need to connect to the MAX7219 module.

How to Connect?

For the MAX7219 module to work we need 5 connections in total: 3 for the DIN/CS/CLK signals and 2 for the VCC/GND power lines.

You can use I/O ports different from the ones specified here, but you will have to make changes in the code examples in this article.

Tinusaur MAX7219 ATtiny85 Connection

The Communication Protocol

Communicating with the MAX7219 is relatively easy – it uses a simple synchronous protocol which means that for every data bit we send we also send a clock cycle that signifies the presence of that data bit. In other words, we send 2 parallel sequences to bits – one for the clock and another one for the data.

MAX7219 Timing Diagram

This is how it works:

In a single module setup: We put the CS signal to low and we start the transmission of the data bits. This is a sequence of putting CLK to low, followed by putting DIN to low/high (depending on the data), followed by putting CLK to high. In the end, we put CS to high to mark the end of the transmission.

To control the module we send commands that consist of 2 bytes of data which means that we must send the 2 bytes of data before we bring the CS signal back to high.

The Software

The way the MAX7219 modules work is this:

  • We send a sequence of bytes to the MAX7219 internal registers.
  • MAX7219 interprets the data.
  • MAX7219 controls the LEDs in the matrix (or matrices).

That also means that we don’t have to circle through the array of LEDs all the time in order to light them up – the MAX7219 controller takes care of that. It could also manage the intensity of the LEDs.

So, to use the MAX7219 modules in a convenient way we need a library of functions to serve that purpose.

First, we need some basic functions in order to write bytes to the various MAX7219 registers.

Let’s define our pins

#define MAX7219_CS		PB0	// CS,	pin 4 on the MAX7219 Board
#define MAX7219_DIN		PB1	// DIN,	pin 3 on the MAX7219 Board
#define MAX7219_CLK		PB2	// CLK,	pin 5 on the MAX7219 Board

For convenience, we will create 6 macros that will put those 3 pins at high and low.

#define MAX7219_CS_HI()		PORTB |= (1 << MAX7219_CS)
#define MAX7219_CS_LO() 	PORTB &= ~(1 << MAX7219_CS)
#define MAX7219_CLK_HI()	PORTB |= (1 << MAX7219_CLK)
#define MAX7219_CLK_LO()	PORTB &= ~(1 << MAX7219_CLK)
#define MAX7219_DIN_HI()	PORTB |= (1 << MAX7219_DIN)
#define MAX7219_DIN_LO()	PORTB &= ~(1 << MAX7219_DIN)

The function that writes one byte to the controller would look like this:

void max7219_byte(uint8_t data) {
	for(uint8_t i = 8; i >= 1; i--) {
		MAX7219_CLK_LO();		// Set CLK to LOW
		if (data & 0x80)		// Mask the MSB of the data
			MAX7219_DIN_HI();	// Set DIN to HIGH
			MAX7219_DIN_LO();	// Set DIN to LOW
		MAX7219_CLK_HI();		// Set CLK to HIGH
		data <<= 1;				// Shift to the left

Now that we can send bytes to the MAX7219 we can start sending commands. This is done by sending 2 bytes – 1st for the address of the internal register and the 2nd for the data we’d like to send.

Sending commands, or words is basically sending 2 consecutive bytes. The function implementing that is very simple.

void max7219_word(uint8_t address, uint8_t data) {
	MAX7219_CS_LO();		// Set CS to LOW
	max7219_byte(address);	// Sending the address
	max7219_byte(data);		// Sending the data
	MAX7219_CS_HI();		// Set CS to HIGH
	MAX7219_CLK_LO();		// Set CLK to LOW
MAX7219 Register Address Map

It is important to note here the source code line where we bring the CS signal back to HIGH – this marks the end of the sequence – in this case, the end of the command. A similar technique is used when controlling multiple matrices connected in a chain.

Let’s now take a look at the internal MAX7219 registers. There are more than a dozen registers in the controller.

The next step, before we start turning on and off the LEDs, is to initialize the MAX7219 controller. This is done by writing specific values to certain registers. For convenience, while coding it, we could put the initialization sequence in an array.

const uint8_t max7219_initseq[] PROGMEM = {
	0x09, 0x00,	// Decode-Mode Register, 00 = No decode
	0x0a, 0x01,	// Intensity Register, 0x00 .. 0x0f
	0x0b, 0x07,	// Scan-Limit Register, 0x07 to show all lines
	0x0c, 0x01,	// Shutdown Register, 0x01 = Normal Operation
	0x0f, 0x00,	// Display-Test Register, 0x01, 0x00 = Normal

Note the use of const and PROGMEM. This will ensure that those values are put in the Flash memory and read directly from there while our program runs. For the same reason, we will have to use the pgm_read_byte() function to access the array values.

This initialization function sends the 5 commands to the MAX7219 controller.

void max7219_init(void) {
	DDRB |= (1 << MAX7219_CLK);	// Set CLK port as output
	DDRB |= (1 << MAX7219_CS);	// Set CS port as output
	DDRB |= (1 << MAX7219_DIN);	// Set DIN port as output
	for (uint8_t i = 0; i < sizeof (max7219_initseq);) {
		uint8_t opcode = pgm_read_byte(&max7219_initseq[i++]);
		uint8_t opdata = pgm_read_byte(&max7219_initseq[i++]);
		max7219_word(opcode, opdata);

The next step – lighting up a row of LEDs.

How it works

At this point, It is important to understand how addressing the individual pixels with MAX7219 works and what happens when we send the data to it.

When we write one byte of data to any of the 8 digits’ registers (Digit 0 through Digit 7) the bits of that byte light on or of one LED on the matrix.

The pixels are filled out left-to-right, bottom-to-top.

Pixel on and off

This is very simple – we just write one command where the 1st byte is the address (from 1 to 8) and the 2nd byte is the 8 bits data representing the 8 LEDs in the row. We also need to check, just in case, if the address is within the boundaries.

void max7219_row(uint8_t address, uint8_t data) {
	if (address >= 1 && address <= 8) max7219_word(address, data);

It is important to note that this will work for 1 matrix only. If we connect more matrices in a chain, they will all show the same data. The reason for that is that after sending the command we bring the CS signal back to HIGH which causes all the MAX7219 controllers in the chain to latch and show whatever the last command was.

To control multiple matrices we need a bit more complex procedure of sending out the data. That will be the subject of another article.


This is a simple testing program that lights up a LED on the first row (r=1) on the right-most position, then moves the lit-up LED on the left (by shifting the bits to the left) until it reaches the left-most position. Then it does the same on the next row up (r=2) until it reaches the top (r=8).

for (uint8_t r = 1; r <= 8; r++) {
	uint8_t d = 1;
	for (uint8_t i = 9; i > 0; i--) {
		max7219_row(r, d);
		d = d << 1;

Note that the inner loop executes 9 times, so the last value written in the row will be 0 – all LEDs off.

This testing code doesn’t do much but it demonstrates how to communicate with the MAX7219 controller.

MAX7219tiny MAX7219 ATtiny85

The MAX7219tiny Library

We wrote a simple library called MAX7219tiny, based on the code above, that could be used to work with MAX7219 modules. It actually does more than what was shown in this article.

The Tinusaur Shield GAMEx3

If you already have a Tinusaur Board we have the Shield GAMEx3 (Generation-4) for it. It is a great addition to our boards and gives you a convenient way of connecting the MAX7219 modules to your microcontroller projects.

Tinusaur Shield GAMEx3 MAX7219


The Gametinu Project

The Gametinu is a small game platform that you could build yourself using the Tinusaur Shield GAMEx3 and a few more parts and tools.


Final Words


MAX7219 specification and datasheet:

Older Articles

This article is a rewritten version of 2 older articles:

Comments and Suggestions

Please, leave a comment or make a suggestion below this blog post: https://tinusaur.com/2022/01/16/how-to-connect-max7219-attiny85-8×8-led-matrix/.

Item added to cart.
0 items - $0.00