Programming an 8 digit 7-segment display, the easy way, using a Max7219

In today’s blog, I am going to show you the easy way to interface 7-segment displays – Using the MAX7219. Each 7219 chip can be used to interface eight 7-segment digits, or 64 individual LEDs.

Previously, in part 1 of my blog posting Programming a 7-segment Display, using just Arduino digital pins (the hard way), we had demonstrated how to hook up a pair of 7-segment displays to an Arduino, treating each individual segment as a separate LED. There was a bit of tricky logic to translate each digit into the segments A, B, C, D, E, F, G plus additional logic to turn the digital pins on or off. Although I tried to code it as efficiently as possible, the logic may have been difficult to understand.

Also, constructing the project was fairly tedious, with dozens of resistors and hook-up wires.

Instead, lets do this the easy way. A typical MAX7219 module comprised of a single MAX7219 chip and eight 7-segment displays. It looks like this:

This module can be purchased for a dollar or two from eBay or Ali Express. These modules interface to the Arduino using only 3 digit signals. Therefore, they are super easy to wire, and, as you will see, easy to program.

How is the MAX7219 Controlled by the Arduino?

Let’s start with the DATASHEET. Download it here:

max7219_datasheet.pdf

Like many driver chips, the MAX7219 has a register model. The application processor – the Arduino in our case – writes values into the MAX7219 registers. Thus, the MAX7219 register values control the operation of the chip. All interactions with the chip are defined in terms of setting one or more registers.

The MAX7219 has relatively few registers. We can understand how to do all operations by studying this register set. Pages 7 through 10 of the datasheet explain, sometimes in excruciating detail, all about this register set. However, a simple summary of the register semantics will explain the chip sufficiently to get you going:

Register Name Register Address Meaning
DIGIT0 1 Right-most digit value
DIGIT1 2 Next digit value
DIGIT2 3 digit value
DIGIT3 4 digit value
DIGIT4 5 digit value
DIGIT5 6 digit value
DIGIT6 7 digit value
DIGIT7 8 Left-most digit value
Decode Mode 9 With 1 bit for each of digit 0-7; if set, interpret the digit as a BCD digit to display. If not set, the bits in the digit register directly light up the 7-segments
Intensity 0x0A The proportion of time the display segments are on and how bright is the display
Scan Limit 0x0B How many of DIGIT0 through DIGIT7 are displayed
Shutdown 0x0C If 0, turn display off; If 1, turn display on
Display Test 0x0F If 0, turn test mode off; If 1, turn test mode on

So, creating any display is simply setting these registers with values.

An example of setting the Registers

Here is an example. To write a 3-digit value “123” into the right-most three digits, we do the following

  • Set the SHUTDOWN register to off
  • Set the DIGIT0 register to “3”
  • Set the DIGIT1 register to “2”
  • Set the DIGIT2 register to “1”
  • Set the DECODEMODE register to binary 00000111, so the 3 right-most digits are decoded.
  • Set the SCANLIMIT register to 2, so digits 0, 1, 2 and 3 are displayed
  • Set the SHUTDOWN register to on, to turn the display back on

That’s it! We can certainly create an Arduino-C function for these simple steps.

How do we communicate with the MAX7219 from the Arduino? How do we send these “set register” commands?

The MAX7219 is serially interfaced to the Arduino. This means that the data is sent serially or one bit at a time. Although this sounds complicated, it is really quite straight forward.

Figure 1, on page 6 of the datasheet, describes sending a register set command, as sending the serial data like this:

Someone who has no experience looking at these Timing Diagrams will be lost. However, not to worry. The Timing Diagram shows an industry-standard method of serial data transfer. In Arduino C, we implement this as follows:

// ... write a value into a max7219 register 
// See MAX7219 Datasheet, Table 1, page 6
void set_register(byte reg, byte value)  
{
    digitalWrite(MAX7219_CS, LOW);
    shiftOut(MAX7219_DIN, MAX7219_CLK, MSBFIRST, reg);
    shiftOut(MAX7219_DIN, MAX7219_CLK, MSBFIRST, value);
    digitalWrite(MAX7219_CS, HIGH);
}

It is simply those 4 lines of code. The Arduino-C built-in function shiftOut() does the magic of serially sending the data on pin DIN one bit at a time, while cycling the CLK pin.

The Timing Diagram above shows all sorts of timing constraints – however, not to worry, the shiftOut() function is fully compliant to those timing constraints. Also, the Timing Diagram specifies that the first bit transferred is D15, then D14, … finally D1 and D0. This is accomplished with the keyword MSBFIRST in the call to shiftOut().

Putting this together into a higher-level function to display the Time

Now, using this set_register() function, let’s create a function called display_time() which will take the time in a string, such as “18:35:04” (or hh:mm:ss format), and display it like this:

The code to accomplish this is as follows.

// ... display the TIME on the 7-segment display
void displayTime(String timeString)  
{
    set_register(MAX7219_REG_SHUTDOWN, OFF);      // turn off display
    set_register(MAX7219_REG_SCANLIMIT, 7);       // limit to 8 digits
    set_register(MAX7219_REG_DECODE, 0b11111111); // decode all digits

    set_register(1, timeString.charAt(7));
    set_register(2, timeString.charAt(6));
    set_register(3, timeString.charAt(5));
    set_register(4, timeString.charAt(4));
    set_register(5, timeString.charAt(3));
    set_register(6, timeString.charAt(2));
    set_register(7, timeString.charAt(1));
    set_register(8, timeString.charAt(0));

    set_register(MAX7219_REG_SHUTDOWN, ON);       // Turn on display
}

I think this is very straight forward code.

The only difficult aspect of writing this function was the order of setting the registers. Through some trial and error, we discovered that the display was more stable if we modified the SCANLIMIT and DECODE registers only when the display was off, or in shutdown mode. Therefore, this function begins with turning off the display and ends with turning the display back on.

How do I create a character display other than a numeral?

In our application, we will have the requirement to display a Temperature either in Celsius or Fahrenheit, like this:

To create a character C or the character F, we have to use the no-decode mode of the MAX7219. In this case, the segments, A, B, C, D, E, F and G are controlled individually. We can refer to Table 6, from Page 8 of the data sheet:

In order to display the letter C, we light segments A, F, E, D. For the letter F, we light segments A, F, G, E. Here is a table to show how we construct the binary values representing symbols C and F:

DpABC DEFG
Symbol C 0100 1110
Symbol F 0100 0111

In Arduino-C, we can express these values as const bytes:

const byte C  = 0b01001110;  
const byte F  = 0b01000111;  

And the line of code to set DIGIT0 to C would be

    set_register(0x01, C); 

or to set DIGIT0 to F would be

    set_register(0x01, F);

Once we have figured out that little trick, we are ready to write the function to display the Temperature in the 7-segment display:

// ... display the TEMP on the 7-segment display
void displayTemp(String tempString, char C_or_F )  
{
    set_register(MAX7219_REG_SHUTDOWN, OFF);      // turn off display
    set_register(MAX7219_REG_SCANLIMIT, 5);       // limit to 6 digits
    set_register(MAX7219_REG_DECODE, 0b00111100); // decode 4 digits

    set_register(1, C_or_F);
    set_register(2, 0);                           // blank
    set_register(3, tempString.charAt(4));
    set_register(4, tempString.charAt(3));
    set_register(5, tempString.charAt(1) | DP);   // plus dec. point
    set_register(6, tempString.charAt(0));

    set_register(MAX7219_REG_SHUTDOWN, ON);       // Turn On display
}

A Useful Application for the Max7219 module

Now we are ready to describe the entire application. The purpose of the application is to display the date, time and temperature, using the 7-segment display. The date, time and temperature are read from the DS3231 chip, which is attached to the Arduino’s I2C bus.

We are going to grab a DS3231 driver from the Arduino contributed libraries. I like the one at rinkydink.com. After installing it our our personal Arduino libraries, we import it into our code using

#include <DS3231.h>           // import library from rinkydink.com

We will use the methods

String rtc.getDateStr()  
String rtc.getTimeStr()  
Float rtc.getTemp()  

which return those values. We are not going to describe the DS3231 chip and this library in detail at this time, although, perhaps I will in another blog. We are using this chip as a sort of “black-box”, which magically returns either the Date, the Time, or the current Temperature (of the DS3231 chip itself).

Because the 8-digit display is limited, we have to make design decisions as to how to present this information. My WhatIMadeToday partner Mike gave me the requirements:

  • Display the DATE for a few seconds in the format: DD.MM.YYYY
  • Then display the TIME for a few seconds in the format HH-MM-SS
  • Then display the Temperature, first in Celsius, then in Fahrenheit for a few seconds.
  • And repeat.

Here is the entire Arduino Application:

/**
 * @file        max7219_7segment_date_time_temp.c
 *
 * @brief       view DATE/TIME/TEMPERATURE using an 8-digit 
 *              7-Segment display
 *
 * @history     Feb 6th, 2017
 *
 * @author      Allan Schwartz <allans@CodeValue.net>
 *              Michael Diamond <m.diamond.il@gmail.com>
 *
 * @notes       The following Arduino Pins are attached:
 *
 *              name    Arduino   7 Segment display
 *              -----   ----      -----
 *                      +5V       VCC
 *                      GND       GND
 *              DIN     D11/MOSI  DIN
 *              CS      D10/SS    CS
 *              CLK     D13/SCK   CLK
 *
 *              name    Arduino   RTC module
 *              -----   ----      -----
 *                      nc        32K
 *                      nc        SQW
 *              SCL     SCL/A5    SCL
 *              SDA     SDA/A4    SDA
 *                      3v3       VCC
 *                      GND       GND
 */

#include <DS3231.h>           // import library from rinkydink.com

// define pins attached to MAX7219 (and also see notes above)
#define MAX7219_DIN           11
#define MAX7219_CS            10
#define MAX7219_CLK           13

// enumerate the MAX7219 registers
// See MAX7219 Datasheet, Table 2, page 7
enum {  MAX7219_REG_DECODE    = 0x09,  
        MAX7219_REG_INTENSITY = 0x0A,
        MAX7219_REG_SCANLIMIT = 0x0B,
        MAX7219_REG_SHUTDOWN  = 0x0C,
        MAX7219_REG_DISPTEST  = 0x0F };

// enumerate the SHUTDOWN modes
// See MAX7219 Datasheet, Table 3, page 7
enum  { OFF = 0,  
        ON  = 1 };

const byte DP = 0b10000000;  
const byte C  = 0b01001110;  
const byte F  = 0b01000111;


// create an instance of the DS3231 called 'rtc', 
// and specify the hardware interface PINS
DS3231 rtc(SDA, SCL);


// ... setup code here, to run once
void setup()  
{
    // initialize the serial port:
    Serial.begin(115200);           // initialize serial communication
    Serial.println("max7219_7segment_date_time_temp");

    // define type of pin
    pinMode(MAX7219_DIN, OUTPUT);   // serial data-in
    pinMode(MAX7219_CS, OUTPUT);    // chip-select, active low    
    pinMode(MAX7219_CLK, OUTPUT);   // serial clock
    digitalWrite(MAX7219_CS, HIGH);

    resetDisplay();                 // reset the MAX2719 display

    rtc.begin();                    // init. the DS3231 RTC interface

    // Uncomment the following lines to set the RTC
    //rtc.setDOW(WEDNESDAY);        // Set Day-of-Week to WEDNESDAY
    //rtc.setTime(16, 20, 0);       // Set the time to 4:20pm
    //rtc.setDate(8, 2, 2017);      // Set the date to 8-Feb-2017
}


void loop()  
{
    String str;                     // scratch or working string

    // ... display Date, dd.mm.yyyy
    // read the date as a string from the RTC
    str = rtc.getDateStr(FORMAT_LONG);
    Serial.println(str);            // debug
    displayDate(str);               // display on the 7-segment 
    delay(6000);

    // ... display Time, hh-mm-ss
    for ( int i = 0; i < 6; i++ ) {
        // read the time as a string from the RTC
        str = rtc.getTimeStr(FORMAT_LONG);
        Serial.println(str);        // debug
        displayTime(str);           // display on the 7-segment 
        delay(1000);
    }

    // ... display Temperature in Celsius, xx.xx C
    // read the temperature, as a float, from the RTC
    float t = rtc.getTemp();
    str = String(t, 2);             // format that value
    Serial.println(str);            // debug
    displayTemp(str, C);            // display on the 7-segment
    delay(3000);

    // ... display Temperature in Fahrenheit, xx.xx F
    t = t * 1.8 + 32.0;             // convert the value to Fahrenheit
    str = String(t, 1);             // format that value
    Serial.println(str);            // debug
    displayTemp(str, F);            // display on the 7-segment
    delay(3000);
}


// ... write a value into a max7219 register 
// See MAX7219 Datasheet, Table 1, page 6
void set_register(byte reg, byte value)  
{
    digitalWrite(MAX7219_CS, LOW);
    shiftOut(MAX7219_DIN, MAX7219_CLK, MSBFIRST, reg);
    shiftOut(MAX7219_DIN, MAX7219_CLK, MSBFIRST, value);
    digitalWrite(MAX7219_CS, HIGH);
}


// ... reset the max7219 chip
void resetDisplay()  
{
    set_register(MAX7219_REG_SHUTDOWN, OFF);   // turn off display
    set_register(MAX7219_REG_DISPTEST, OFF);   // turn off test mode
    set_register(MAX7219_REG_INTENSITY, 0x0D); // display intensity
}


// ... display the DATE on the 7-segment display
void displayDate(String dateString)  
{
    set_register(MAX7219_REG_SHUTDOWN, OFF);  // turn off display
    set_register(MAX7219_REG_SCANLIMIT, 7);   // scan limit 8 digits
    set_register(MAX7219_REG_DECODE, 0b11111111); // decode all digits

    set_register(1, dateString.charAt(9));
    set_register(2, dateString.charAt(8));
    set_register(3, dateString.charAt(7));
    set_register(4, dateString.charAt(6));
    set_register(5, dateString.charAt(4) | DP); // plus decimal point
    set_register(6, dateString.charAt(3));
    set_register(7, dateString.charAt(1) | DP);
    set_register(8, dateString.charAt(0));

    set_register(MAX7219_REG_SHUTDOWN, ON);   // Turn on display
}


// ... display the TIME on the 7-segment display
void displayTime(String timeString)  
{
    set_register(MAX7219_REG_SHUTDOWN, OFF);  // turn off display
    set_register(MAX7219_REG_SCANLIMIT, 7);   // scan limit 8 digits
    set_register(MAX7219_REG_DECODE, 0b11111111); // decode all digits

    set_register(1, timeString.charAt(7));
    set_register(2, timeString.charAt(6));
    set_register(3, timeString.charAt(5));
    set_register(4, timeString.charAt(4));
    set_register(5, timeString.charAt(3));
    set_register(6, timeString.charAt(2));
    set_register(7, timeString.charAt(1));
    set_register(8, timeString.charAt(0));

    set_register(MAX7219_REG_SHUTDOWN, ON);   // Turn on display
}


// ... display the TEMP on the 7-segment display
void displayTemp(String tempString, char C_or_F )  
{
    set_register(MAX7219_REG_SHUTDOWN, OFF);  // turn off display
    set_register(MAX7219_REG_SCANLIMIT, 5);   // scan limit 6 digits
    set_register(MAX7219_REG_DECODE, 0b00111100); // decode 4 digits

    set_register(1, C_or_F);
    set_register(2, 0);                       // blank
    set_register(3, tempString.charAt(4));
    set_register(4, tempString.charAt(3));
    set_register(5, tempString.charAt(1) | DP); // plus decimal point
    set_register(6, tempString.charAt(0));

    set_register(MAX7219_REG_SHUTDOWN, ON);   // Turn On display
}
Enclosure

Here is my finished project box, with the display attached to outside of the enclosure with double-sided tape:

Mike has made several creative enclosures. Here is the display attached to his monitor:

Here is one in a clear plastic box:

There is a lot you can do with this simple display. Have fun!