Introduction

MAX7219CNG is a programmable display driver integrated circuit that is used to control a large array of LEDs while consuming a relatively small number of GPIO pins. It receives input commands via four SPI pins, and uses these commands to drive either an 8x8 matrix of LEDs, or an eight-digit seven-segment display.

MAX7219CNG DIP chips can be purchased as standalone components, but I find it a lot cheaper to buy them as a part of an LED matrix kit for substantially lower cost. Buying the chip with an LED matrix also makes it easier to get started since the kit means you only have to worry about power, ground, and three SPI input pins.

On this page, I will demonstrate some of the basics of MAX7219CNG using an 8x8 matrix kit, which can be purchased very inexpensively (e.g., the chip and the LED matrix for $3).

Communicating with MAX7219CNG via SPI

The MAX7219CNG has a bona fide command protocol, built on SPI, where you send 12-bit packet that encodes both a 4-bit command and an 8-bit value. In this packet, the Most Significant Bits (MSBs), or the ones that are sent first, encode the command, and the Least Significant Bits (LSBs) encode the data. The specific packet is constructed such that

  • bits 0-3 encode a command
  • bits 4-11 encode the value that should be passed to the command

This chip has the following commands:

  • Set a specific column (or "digit") of LEDs to a specific combination of on and off. This MAX7219CNG chip supports eight columns of LEDs, so there are 8 different command codes that are used to set each of them.
  • Set the "decode mode" for each column
  • Set the brightness of the illuminated LEDs
  • Enable or disable entire columns
  • Enable or disable "shutdown mode," where all LEDs are turned off
  • Enable or disable "test mode," where all LEDs are turned on at the highest brightness

The very first thing I did after soldering the components together was to enable test mode to make sure all the LEDs did light up. The packet we send to the MAX7219 should look like this:

Depiction of the MAX7219 command packet

where the 1111 command code corresponds with test mode, and the 00000001 value means enable. Python has a really easy way of encoding these bits into variables that don't require bit shift operators:

cmd = int("1111", 2)
value = int("0000001", 2)
packet = cmd | value

Alternatively, you can just encode the whole packet directly:

packet = int("11110000001", 2)

Then sending these bits down the MOSI pin will enable test mode, and all 64 LEDs should turn on at the highest brightness. To disable test mode, send the same command (1111) but a zero value (00000000), or

packet = int("111100000000", 2)

This test mode also overrides the values of the other settings, so it should illuminate all LEDs at full brightness no matter what. If any LEDs are flickering, dim, or unlit, there is a physical problem with either the LED (i.e., it is damaged) or a solder joint somewhere.

For reference, all of these registers and their function are documented in the MAX7219 datasheet provided by the manufactuerer.

Initializing the MAX7219CNG

It's a bit misleading to refer to the 4-bit command codes as such; they're actually register addresses, and the 12-bit packets we send actually program (write to) these special-purpose registers. I mention this now because the contents of these registers at any given time determine how the LEDs are illuminated; it follows that you need to be sure you know the contents of those registers when your program first starts. Since there is no way to read the contents of these registers off of the MAX7219CNG (because there is no MISO pin), this means explicitly setting all of the registers.

So, let's first zero out all registers:

# range(16) because there are 16 command registers, 0000 through 1111
for cmd in range(16):
    packet = cmd << 8
    # e.g., if cmd is 0101, 0101 << 8 becomes 010100000000
    spi.put(packet,12)

Then there are two specific registers that must have nonzero values for all LEDs to work correctly:

  1. The scan limit register (1011) must have its three least significant bits set to 1, so the packet should be 101100000111. This register provides a way to disable columns of LEDs, so by setting it to 111 (i.e., 8), we are enabling all eight columns.
  2. The shutdown register (1100) must be set to 1, so the packet should be 110000000001. When set to 0, this register shuts off all of the LEDs but doesn't forget their on/off state. Thus, when you set this register, the LEDs light back up just as you had them before shutting down.

Once the MAX7219 is initialized, we can set LED patterns.

Programming the MAX7219

The MAX2719 chip divides a set of 64 LEDs into eight columns ("digits") of eight LEDs each. The columns are numbered 1 through 8, and each LED within a column is represented by a single bit in that column's register.

the MAX7219CNG addressing layout

The 4-bit command sequence to address a column is just that column's index. In the above photo, column two can be programmed using 0010, which is the binary representation of the 2. Similarly, column 8 can be addressed using 1000.

The 8-bit value portion of the SPI packet represents whether each LED in that column should be on (1) or off (0). The most-significant bit addresses the bottom-most LED, and the least-significant bit addresses the top-most. So in the above photo, column 2's register contains 00100111. Thus, to get column 2 lit up as shown, you would send an SPI packet that was 001000100111.

A complete Python script that does this using my simple Python SPI package might look like

You can then alter another column, and the value of this column remains the same. For example, we can then fill columns 5-8 with a random pattern:

import random
for column in (5, 6, 7, 8):
    register_addr = column << 8
    value = random.randint(0, 2**8-1)
    max7219.put(register_addr|value,12)

or even more, put this random pattern generator in a loop:

to demonstrate that each column can be configured separately:

Filling columns 5-8 with random values

This covers the basics of lighting up an 8x8 matrix of LEDs using the MAX7219.

Practical Programming

While the MAX7219 does provide a very simple way to control all 64 LEDs, you must program LEDs in terms of entire columns of 8 LEDs at a time. As a result, it's not very easy to change the state of a single LED within a column, or to change a row. And, because there is no way to ask the MAX7219 what the current state of its registers are, you wind up having to make your application keep track of the state of every LED.

The data structure you use to save the on/off state of each LED can be as simple or complicated as you want. The simplest possible way is using a 64-element list:

led_state = [0] * 64
def update_state(led_state, column, row, state):
    return led_state[column * 8 + row] = state

update_state(led_state, column=2, row=3, state=1)
update_state(led_state, column=2, row=3, state=0)

However, this highlights a mild annoyance with addressing specific LEDs in MAX7219:

  • columns are numbered 1-8 (i.e., register address 0001 through 1000)
  • Python numbers indices from 0-7

This means that the first element in our list (led_state[0]) corresponds to the column at register 0001. Not the end of the world, but something to bear in mind when converting user input to column register addresses.

To convert a single column from our led_state list into an SPI command, we have to do some bitwise manipulations. For example,

register = (column+1) << 8
value = 0
for row in range(8):
    value <<= 1
    if led_state[column*8 + row]:
        value |= 1

packet = register | value

which

  1. sets the register address of the column we are modifying. column+1 allows us to specify columns 0-7 (to match rows 0-7) but still get the correct register addresses (1-8)
  2. initializes our value to 00000000
  3. probes each value in led_state that corresponds to our value
  4. shift all previous bits in our value over by one
  5. if the value is true/on, set the zero value we just used to shift our value to "on" (1); otherwise, leave it as "off" (0)
  6. combines the 4-bit register address and 8-bit value to generate the packet which we send to the MAX7219

A complete Python program which uses this approach to set individual LEDs on or off might look like this:

#!/usr/bin/env python
"""Change the state of a single LED in an 8x8 matrix.
"""

import spi

### Initialize an SPI connection using BCM-mode pins 21, 20, and 16
max7219 = spi.SPI(clk=21, cs=20, mosi=16, miso=None, verbose=True)

### Zero out all registers
for cmd in range(16):
    packet = cmd << 8
    max7219.put(packet,12)

### Set the scan limit register and disable shutdown mode
max7219.put(int("101100000111",2),12)
max7219.put(int("110000000001",2),12)

### We zeroed out all registers, so all LEDs are off (0)
led_state = [0]*64

def set_led(row, column, state):
    ### update our saved state
    led_state[column*8 + row] = state

    ### convert the new column into an SPI command
    register = (column+1) << 8
    value = 0
    for row in range(8):
        value <<= 1
        if led_state[column*8+row]:
            value |= 1

    max7219.put(register|value,12)

while True:
    row, col, state = input("Enter a row, col, state: ")
    set_led(row, col, state)

Of course, using Python lists is not always the best way to store and update the state of each LED; for example, using an 8x8 numpy matrix may be a more logical data structure to use.

Other Features

The MAX7219 datasheet presents all of the features of the chip, and many are worth playing with. For example, playing with the LED intensity register allows you to make the LEDs pulse in different ways:

Pulsing the LED intensity register

The code to create the beating heart pattern above is quite simple, and it is available in my GitHub repository.