Using the MAX7219 in your Projects – Part 2

In the first part we explored the functions of this MAX7219 and how the SPI link is the key to making the device work for us.

In this part we’ll develop code to efficiently display numeric data using 7-segment and LED matrix displays.

Making it all work

Once the underlying hardware functionality is understood, it is up to the microcontroller software manage the data and type of display required.

We’ll consider two use cases – running a 7-segment display and a LED matrix display. In both cases the example software will just display a running counter incremented every second.

7-Segment Displays

The datasheet has the model circuit for seven segment displays (shown below for a 2 digit 7-segment display), so there is not much to add to the hardware requirements explanation, and there is only one logical way to wire the MAX7219 and the display modules so the IC signal names match the LED segment names.

The capacitors in the circuit minimize power-supply ripple due to the peak digit driver currents, so never leave them out. This ensures that you get reliable hardware operation when building your version.

The first thing to do is declare some constants for the MAX7219 registers given in Table 2 of the datasheet.

// Opcodes for the MAX7221 and MAX7219
const uint16_t OP_NOOP = 0;
const uint16_t OP_DIGIT0 = 1;
// note: all OP_DIGITn are +n offsets from OP_DIGIT0
const uint16_t OP_DIGIT1 = 2;
const uint16_t OP_DIGIT2 = 3;
const uint16_t OP_DIGIT3 = 4;
const uint16_t OP_DIGIT4 = 5;
const uint16_t OP_DIGIT5 = 6;
const uint16_t OP_DIGIT6 = 7;
const uint16_t OP_DIGIT7 = 8;
const uint16_t OP_DECODEMODE = 9;
const uint16_t OP_INTENSITY = 10;
const uint16_t OP_SCANLIMIT = 11;
const uint16_t OP_SHUTDOWN = 12;
const uint16_t OP_DISPLAYTEST = 15;

This example implementation can be used for up to the maximum 8 digits connected to one MAX7219. This means we can write a simple comms function with the command and data as parameters, sending these out as a single 16-bit packet.

void sendData(uint16_t cmd, uint8_t data)
// Send a simple command to the MAX7219
// using the hardware SPI interface
{
  uint16_t x = (cmd << 8) | data;
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  digitalWrite(pinSS, LOW);
  SPI.transfer16(x);
  digitalWrite(pinSS, HIGH);
  SPI.endTransaction();
}

In this code the SPI object is managed in accordance with the library reference. As discussed in the first part, the SS output is toggled LOW to activate the device and then back HIGH to load the transmitted data into the correct register.

Finally, we need to initialize the hardware in setup():

  • The datasheet states that all the registers are set to zero and the device is initialised in ‘shutdown’ mode. So we need release the shutdown state to start displaying data.
  • To simplify display of numeric digits, we also set the ‘decode’ mode for all digits.
  • The scan limit (number of digits scanned) is set to minimum required to maximize the refresh rate for the number of digits displayed.
  • Intensity is set to a mid level value (7 of maximum 15).
  • Then, for good measure, we’ll invoke the hardware LED test mode for a short time (because we can!).
void setup(void) 
{
  pinMode(pinSS, OUTPUT);
  SPI.begin();
  // Set the MAX7219 parameters
  sendData(OP_SHUTDOWN, 1);
  sendData(OP_DECODEMODE, 0xff);
  sendData(OP_SCANLIMIT, NUM_DIGITS-1);
  sendData(OP_INTENSITY, 7);

  // turn on all LED for short time
  sendData(OP_DISPLAYTEST, 1);
  delay(500);
  sendData(OP_DISPLAYTEST, 0);

  // initialize the display
  update(0);
}

The full sketch is listed below.

// Assumes one MAX7219 IC with up to 8 
// 7-segment LED displays attached.
// The sketch displays a counter increasing
// once per second.
// SPI interface is through the SPI 
// library and so uses the native hardware 
// interface pin for the target MCU.
#include "Arduino.h"
#include "SPI.h"

// Define the number of 7-seg digits 
// in the display. Valid range [1..8]
const uint8_t NUM_DIGITS = 2;

// Define the SS pin
const uint8_t pinSS = 10;

// Opcodes for the MAX7221 and MAX7219
const uint16_t OP_NOOP = 0;
const uint16_t OP_DIGIT0 = 1;
// note: all OP_DIGITn are +n offsets from OP_DIGIT0
const uint16_t OP_DIGIT1 = 2;
const uint16_t OP_DIGIT2 = 3;
const uint16_t OP_DIGIT3 = 4;
const uint16_t OP_DIGIT4 = 5;
const uint16_t OP_DIGIT5 = 6;
const uint16_t OP_DIGIT6 = 7;
const uint16_t OP_DIGIT7 = 8;
const uint16_t OP_DECODEMODE = 9;
const uint16_t OP_INTENSITY = 10;
const uint16_t OP_SCANLIMIT = 11;
const uint16_t OP_SHUTDOWN = 12;
const uint16_t OP_DISPLAYTEST = 15;

void sendData(uint16_t cmd, uint8_t data)
// Send a simple command to the MAX7219
// using the hardware SPI interface
{
  uint16_t x = (cmd << 8) | data;
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  digitalWrite(pinSS, LOW);
  SPI.transfer16(x);
  digitalWrite(pinSS, HIGH);
  SPI.endTransaction();
}
 
void update(uint32_t n)
// Work out the individual digits to
// be displayed and show them.
{
  for (uint8_t i = 0; i < NUM_DIGITS; i++)
  {
    uint8_t digit = n % 10;
    sendData(OP_DIGIT0 + i, digit);
    n /= 10;
  }
}

void setup(void) 
{
  pinMode(pinSS, OUTPUT);
  SPI.begin();
  // Set the MAX7219 parameters
  sendData(OP_SHUTDOWN, 1);
  sendData(OP_DECODEMODE, 0xff);
  sendData(OP_SCANLIMIT, NUM_DIGITS-1);
  sendData(OP_INTENSITY, 7);

  // turn on all LED for short time
  sendData(OP_DISPLAYTEST, 1);
  delay(500);
  sendData(OP_DISPLAYTEST, 0);

  // initialize the display
  update(0);
}

void loop(void) 
{
  static uint32_t timeLast = 0;
  static uint32_t counter = 0;

  if (millis() - timeLast >= 1000)
  {
    timeLast = millis();
    counter++;
    update(counter);
  }
}

LED Matrix modules

The circuit for a LED matrix is the essentially same control as for the 7-segment display (8 LEDs and 8 digits) but looks different because the LEDs are no longer in in the same ‘8.’ configuration. See the example Parola matrix module circuit below for a typical circuit arrangement.

For this type of matrix there are a few different ways to connect the signals which will change the way the software needs to work with LED rows and columns (see this previous article on the subject). This example code is for modules identifiable as FC-16, available either as individual units or, more commonly, a set of four attached units as in this example.

We’ll also simplify the program logic by assuming that each counter digit is displayed in a separate module by itself (see the photo above).

A major difference from the 7-segment example is that communications will need to be structured differently because have multiple MAX7219 ICs daisy-chained DOUT to DIN. We can think of needing 2 different types of communications.

The first type of comms is used to set module based parameters for each MAX7219. In a multi-module display it is not unreasonable to want to keep the same ‘look and feel’ for all modules, so the commands for decoding, intensity, system test and scan limit (although this makes no sense for a LED matrix) should be sent to all the modules at the same time. This is easily accomplished by sending the identical command registers along the entire serial pipeline:

void sendCmd(uint16_t cmd, uint8_t data)
// Send a simple command to the MAX7219
// chain using the hardware SPI interface
{
  uint16_t x = (cmd << 8) | data;
  SPI.beginTransaction(SPI_PARAMETER);
  digitalWrite(pinSS, LOW);
  for (uint8_t i = 0; i < NUM_MODULES; i++)
    SPI.transfer16(x);
  digitalWrite(pinSS, HIGH);
  SPI.endTransaction();
} 

The second type of comms are for setting the LEDs on the display, and this requires a bit more thought.

As we are no longer able to rely on the MAX7219 to decode the digits for us, we’ll need to define what LEDs are turned on in the matrix. What this data looks like depends on how the matrix rows/columns are wired to the digits/segments.

In this example, looking at the FC-16 module with DIN on the left, the columns are the segments (lsb on the left, msb on the right) and the rows are the digits (increasing row number bottom to top). So the ‘font’ definition in the code works for the FC-16 but will be inverted and/or reversed for other matrix types. Fully featured libraries like MD_MAX72xx will automatically manage the right transformations required for different module wiring configurations.

Also, to refresh a module we need to update the data for every digit. Because the data is set when SS goes HIGH, it means that the most efficient way to do this is to send the same digit for all the modules, update, then send the next digit to all the modules, update, etc. It is also good to do this as quickly as possible to avoid disjointed partial display updates. This requires a display memory buffer so that an update can be done as a contiguous batch operation. This is implemented in the code below:

void sendData(void)
// Send display data the MAX7219 chain
// using the hardware SPI interface
{
  SPI.beginTransaction(SPI_PARAMETER);
  for (uint8_t j = 0; j < MAX_ROWS; j++)
  {
    digitalWrite(pinSS, LOW);
    for (uint8_t i = 0; i < NUM_MODULES; i++)
    {
      uint16_t x = ((OP_DIGIT0 + j) << 8) | moduleData[i][j];
      SPI.transfer16(x);
    }
    digitalWrite(pinSS, HIGH);
  }
  SPI.endTransaction();
}

Finally, the initialisation in setup() is similar, but shorter, than the corresponding routine for the 7-segment example:

void setup(void) 
{
  pinMode(pinSS, OUTPUT);
  SPI.begin();

  // Set the MAX7219 parameters
  sendCmd(OP_SHUTDOWN, 1);
  sendCmd(OP_DECODEMODE, 0);
  sendCmd(OP_INTENSITY, 7);

  // turn on all LED for a short time
  sendCmd(OP_DISPLAYTEST, 1);
  delay(500);
  sendCmd(OP_DISPLAYTEST, 0);

  // initialise the display
  update(0);
} 

The full sketch is listed below.

// Assumes 4 FC-16 type MAX7219 IC
// LED matrix modules with 64 LEDs 
// per module.
// The sketch displays a counter increasing
// once per second.
// SPI interface is through the SPI 
// library and so uses the native hardware 
// interface pin for the target MCU.
#include "Arduino.h"
#include "SPI.h"

// Define the number of modules in the 
// display and rows/columns per module
const uint8_t NUM_MODULES = 4;
const uint8_t MAX_ROWS = 8;

// Define the SS pin
const uint8_t pinSS = 10;

// Opcodes for the MAX7221 and MAX7219
const uint16_t OP_NOOP = 0;
const uint16_t OP_DIGIT0 = 1;
// note: all OP_DIGITn are +n offsets from OP_DIGIT0
const uint16_t OP_DIGIT1 = 2;
const uint16_t OP_DIGIT2 = 3;
const uint16_t OP_DIGIT3 = 4;
const uint16_t OP_DIGIT4 = 5;
const uint16_t OP_DIGIT5 = 6;
const uint16_t OP_DIGIT6 = 7;
const uint16_t OP_DIGIT7 = 8;
const uint16_t OP_DECODEMODE = 9;
const uint16_t OP_INTENSITY = 10;
const uint16_t OP_SCANLIMIT = 11;
const uint16_t OP_SHUTDOWN = 12;
const uint16_t OP_DISPLAYTEST = 15;

uint8_t digitFont[10][MAX_ROWS] = 
{
  { 0x00, 0xf8, 0x88, 0x88, 0x88, 0x88, 0x88, 0xf8 }, // 0
  { 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, // 1
  { 0x00, 0xf8, 0x08, 0x08, 0xf8, 0x80, 0x80, 0xf8 }, // 2
  { 0x00, 0xf8, 0x80, 0x80, 0xf8, 0x80, 0x80, 0xf8 }, // 3
  { 0x00, 0x80, 0x80, 0x80, 0xf8, 0x88, 0x88, 0x88 }, // 4
  { 0x00, 0xf8, 0x80, 0x80, 0xf8, 0x08, 0x08, 0xf8 }, // 5
  { 0x00, 0xf8, 0x88, 0x88, 0xf8, 0x08, 0x08, 0xf8 }, // 6
  { 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xf8 }, // 7
  { 0x00, 0xf8, 0x88, 0x88, 0xf8, 0x88, 0x88, 0xf8 }, // 8
  { 0x00, 0xf8, 0x80, 0x80, 0xf8, 0x88, 0x88, 0xf8 }, // 9
};

uint8_t moduleData[NUM_MODULES][MAX_ROWS];
#define SPI_PARAMETER SPISettings(8000000, MSBFIRST, SPI_MODE0)
 
void sendCmd(uint16_t cmd, uint8_t data)
// Send a simple command to the MAX7219
// chain using the hardware SPI interface
{
  uint16_t x = (cmd << 8) | data;
  SPI.beginTransaction(SPI_PARAMETER);
  digitalWrite(pinSS, LOW);
  for (uint8_t i = 0; i < NUM_MODULES; i++)
    SPI.transfer16(x);
  digitalWrite(pinSS, HIGH);
  SPI.endTransaction();
}

void sendData(void)
// Send display data the MAX7219 chain
// using the hardware SPI interface
{
  SPI.beginTransaction(SPI_PARAMETER);
  for (uint8_t j = 0; j < MAX_ROWS; j++)
  {
    digitalWrite(pinSS, LOW);
    for (uint8_t i = 0; i < NUM_MODULES; i++)
    {
      uint16_t x = ((OP_DIGIT0 + j) << 8) | moduleData[i][j];
      SPI.transfer16(x);
    }
    digitalWrite(pinSS, HIGH);
  }
  SPI.endTransaction();
}

void update(uint32_t n)
// Work out the individual digits to
// be displayed and show them.
{
  for (uint8_t i = 0; i < NUM_MODULES; i++)
  {
    uint8_t digit = n % 10;
    n /= 10;
    // assume one digit per module and copy 
    // the bitmap into the module buffer 
    for (uint8_t j = 0; j < MAX_ROWS; j++)
      moduleData[i][j] = digitFont[digit][j];
  }
  sendData();
}

void setup(void) 
{
  pinMode(pinSS, OUTPUT);
  SPI.begin();

  // Set the MAX7219 parameters
  sendCmd(OP_SHUTDOWN, 1);
  sendCmd(OP_DECODEMODE, 0);
  sendCmd(OP_INTENSITY, 7);

  // turn on all LED for a short time
  sendCmd(OP_DISPLAYTEST, 1);
  delay(500);
  sendCmd(OP_DISPLAYTEST, 0);

  // initialise the display
  update(0);
}

void loop(void) 
{
  static uint32_t timeLast = 0;
  static uint32_t counter = 0;

  if (millis() - timeLast >= 1000)
  {
    timeLast = millis();
    counter++;
    update(counter);
  }
}

3 thoughts on “Using the MAX7219 in your Projects – Part 2

  1. Hi
    I have a weird phenomenon . When I load this program on a FC-16 display connected to a UNO board it doesn’t do nothing other then lighting up the full display for a second and then go blank. When I load first the Majid Scoll program which uses the SPI interface and let that one run and then load this program it shows me the four digits and starts counting.
    Futher more the the line “if (millis() – timeLast >= 1000)” 7th from the bottom the “>=” is wrong and must be “>=” otherwise it generates a compiler error

    What is the reason that i need to start with one program first to make it run? or what is missing in this program for me

    Like

    1. Not sure what the Majid Scoll program is. I can’t explain what you are seeing. I have seen something like this a couple of times with my matrices and it usually requires a power off (not just reset) and restart for the matrices to recover. I have put this down to the hardware getting into some weird lock-up state but I don’t know.

      > Futher more the the line “if (millis() – timeLast >= 1000)” 7th from the bottom the “>=” is wrong and must be “>=” otherwise it generates a compiler error

      As you can see from your comment, this is a problem with the WordPress display widget. I have changed the type of display and it looks right now.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s