Update of Arduino Led Control Library for MAX7219

I love the 7 segment LED displays from the '80s. They are clearly readable, night and day. Combined with a dedicated driver chip - for example the Maxim MAX7219/MAX7221 - the usage with an Arduino gets pretty easy. Fortunatly there is a library exisiting called "LED Control". On this page I want to present some extensions for easier writing of numbers and text using the "print" method.

Introduction Led Control

There are lot of cheap modules avalaible from China (obviously with counterfeight chips). The price might be the reason, why these displays have still fans nowadays. Each MAX7219/MAX7221 can drive up to 8 seven segment digits. And if 8 digits are not enough - you can daisy chain chips.

The original Arduino library LED Control written by Eberhard Fahle is dating back to 2007 and - according to the files - seems to be last updated 2015. It provides a good starting point when you want to use the MAX7219 (or MAX7221). It's easy to show one character or one digit, but gets quite complex if you want to print larger numbers or plain text on the display. And this is the point where my new modified library jumps in.

Modifications in the NoiascaLedControl Library

In the Arduino world there is existing a so called "print" class. You will use it for Serial output (Serial.print), printing to LCDs, OLEDs or even for the output of a webserver. I added this "print" method to an updated library called "NoiascaLedControl". The new library should be fully compatible to the original library - and offers easier output with print (and some other helpers).

One capability of C++ is, that a class can derive properties and characteristics from another class. This is called inheritance. Exactly this method was used: the new library inherits the print method. To be precise: I needed to implemet the "write" method. This method writes one digit on the display. The new class inherites from the print class (which uses the write method) and now we are able to "print" integers, floats, C-strings (char arrays) and Arduino-Strings to the display like you are used to from other libraries. Additionally I needed one additional byte variable to store the current position where to write the next character.

As you might know from other libraries they offer some helper methods. The method

lc.setCursor(addr, positionWithinDevice);

is such an example, it will set the cursor to a defined position. More or less compareable to some parameters in the .setDigit method. Please see the next chapter for more details.

Understanding Digit Numbering and Position of Cursor

The original library uses "digit" - numbered from 7 to 0 (left to right) to set where to print. To keep in line with other displays I decided to use "positions" - starting with 0 (left) to 7 (right).

Let's see a short comparision of devices (the chips), digits and the new positions:

device   0                 1                 2                   7
addr     0 0 0 0 0 0 0 0   1 1 1 1 1 1 1 1   2 2 2 2 2 2 2 2     7 7 7 7 7 7 7 7
digit    7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0 ... 7 6 5 4 3 2 1 0
position 0 1 2 3 4 5 6 7   8 9 10  . . . .   16  . . . . . . ... 56            63

Example: if you have 2 MAX7219 and you want to write on the second chip - just remember that C++ starts counting at zero and define

lc.setCursor(1, 0);    // second chip (=address), first character (=position)
lc.print("2nd IC");    // print text to LED Display

The method can be used also with one parameter to set the absolute position:

lc.setCursor(8);       // second chip, first character
lc.print("2nd IC");    // print text to LED Display

and the same can be used, if you have only one chip:

lc.setCursor(0);       // first chip, first character
lc.print("1st IC");    // print text to LED Display

Hint: Be careful when you migrate existing code which used "setDigit". If you have used setDigit(int addr, int digit, byte value, boolean dp) like in

lc.setDigit(0, 1, 3, 0);
lc.setDigit(0, 0, 4, 0);

to print 34 on the two most right digits, you will need

lc.setCursor(0, 6);
lc.print("34");

to get the same result with the print method.

If for any reason, you have wired your display in the inversed order of 0 1 2 3 4 5 6 7 - you will find an example with an adopted class to write on displays which have digits in inversed order (instead of 7 6 5 4 3 2 1 0). You need library version 1.2.0 (or higher) for this.

Limitations of the 7 Segment Character Set

Does it need an explanation, that it is not possible to print all characters with 7 segments? Even I tried to cover as many characters as possible, I was limited by the fact, of just having 7 straight lines availale for the Alphabet.

Characters like K W X are definitely not printable and are replaced with a blank. V is replaced by U. The small "r" needs quite good phantasie to be identified as r. The "a" is  "o." with dot. The "Q" uses a similar aproach, it's sent as "O." That reads quite complicated, but doesn't looks as badly at all. Just give it a try. Sometimes there needs a mix of small and capital letters. If one has good ideas for improvements of the character table - I appreciate your feedback.

Brackets - parentheses ( ), square brackets [ ], braces { }, and angle brackets - are always printed as square brackets.

There is

  • a degree sign ° (which will also be used for %),
  • the dash -
  • the underscore _
  • and the dot .

All together the character set could have 128 characters (= 7bit). However only character 37-127 are printable, other values will cause a blank. Technically you can also use 0-10 which will print the respective numbers 0-9 and the degree sign ° for character 10.

Points and Dots

Points and dots need some special attention. As printing to the display is done character by character we have to find a way how to "activate" the decimal point of a previous printed digit. This is done by using an (existing) status array. The library stores each digit (up to 64) in RAM. Therefore we can "reprint" the previous digit with an activated dot.

This works quite well - up to the point when the previous character is a dot. It might break the print out. This means you can easily print a dot after another character - but not a dot after a dot. So this is can not be printed:

...

instead print a blank before the next dot, like following:

. . .

This will be shown up correctly on the display - and if you carefully check your display it is exactly what you want: 7 segments must not be enlighted (all off = blank) but the decimal point does. So it makes absolutly sense to print blanks between the dots on a 7 segment display, doesn't it?

End of Line

If we come to the 8th position we have to decide what should happen with the next character. By default the library jumps to the "NEXT_DEVICE". If we have written the last postion on the last device, we wrap around to the first device. This behaviour can be changed with the method:

void setEndOfDevice(optionEndOfDevice newOption);

The available options are

NEXT_DEVICE:  the default behaviour as described. Jump to the next adress - or if no more devices are available, jump to the first chip (adress=0).

THIS_DEVICE: only uses the same chip/adress, so wrap around within the same chip.

FIRST_DEVICE: always jumps to adress 0. There are seldom usecases where you need this, use NEXT_DEVICE instead.

SHIFT_LEFT: whould be nice, but is not implemented yet.

The Linefeed and Carriage Return

I decided to use the linefeed (LF, 0d10, 0x0A, \n) to force a linefeed. To be precise the linefeed will trigger an event causing the same behaviour as when the cursor reaches the end of the device, i.e. per default the cursor will jump to the next device.

The carriage return (CR, 0d13, 0x0C, \r) will have NO EFFECT. It will even not print a blank.

By the way: as we inherited from print class we can also use println. The println prints text (or numbers) followed by CRLF. As described the library ignores the carriage return (CR) and the linefeed (LF) gets recognized. Therefore a println will cause a linefeed.

Hardware SPI for the MAX7219

Up from version 1.1.0 the NoiascaLedControl supports the hardware SPI for the MAX7219/MAX7221. Hardware SPI is much more faster than the common "software bitbang". Further more - if you are using already another SPI device, you can reuse the exisiting pins for MOSI and SCK and don't need these separte pins for your display. As with other SPI devices on a shared bus, your MAX7219 only needs a separate "Chip Select" pin.

In the sketch you need two changes:

  • The library comes with a separte .h file which needs to be included if you want to use Hardware SPI
  • The object constructor needs only two arguments: the ChipSelect pin and how many modules are connected
#include <NoiascaLedControlSpi.h>
const uint8_t LEDCS_PIN = 8;           // 8 LED CS or LOAD -  8 CS
LedControlSpi lc = LedControlSpi (LEDCS_PIN, LED_MODULES); // Hardware SPI

Beside CS you have to connect MOSI and SCK. MISO is not used, as the MAX7219 doesn't send back information to the Arduino. The pins for Hardware SPI differ from controller to controller. Following table shows the used pins for UNO/Nano and Mega in Hardware SPI mode:

MAX72.. SPI Uno/Nano Mega
DIN MOSI 11 51
CLK SCK 13 52
CS SS 8 53
 - MISO 12 50

In my examples I'm using pin 8 for CS, but you can use any other pin you have available. There is an additional example sketch using the SPI class: LCDemoNoiascaHelloWorldSPI.

I've have tested the Hardware SPI together with several ethernet, SD and NFC/RFID shields and all are working.

Hardware SPI vs. Software bitbang of the MAX7219/MAX7221

I did a small test to campare the speed of Hardware SPI vs. Software bitbang of the MAX72xx using this code:

  uint32_t start = 0;
  uint32_t end = 0;
  start = micros();

  lc.setDigit(0, 0, 8, false); // low level .setDigit
  end = micros();
  Serial.println(end-start);

  start = micros();
  lc.write('8');    // .write like used by the Print class
  end = micros();
  Serial.println(end-start);

  start = micros();
  lc.print("8");     // .print calls implicit the write method
  end = micros();
  Serial.println(end-start);

  start = micros();  
  lc.setDigit(0, 0, 8, false);  // send a total of 8 digits
  lc.setDigit(0, 1, 8, false);
  lc.setDigit(0, 2, 8, false);
  lc.setDigit(0, 3, 8, false);
  lc.setDigit(0, 4, 8, false);
  lc.setDigit(0, 5, 8, false);
  lc.setDigit(0, 6, 8, false);
  lc.setDigit(0, 7, 8, false);
  end = micros();
  Serial.println(end-start);

  start = micros();      
  lc.print("88888888");  // print a total of 8 digits
  end = micros();
  Serial.println(end-start);

I measured following times (in microseconds):

used code bitbang SPI 4000000 SPI 800000
.setDigit(0,0,8,false) 240 28 28
.write('8') 264 36 32
.print("8") 280 40 40
8 x .setDigit 1920 244 228
.print("88888888") 1992 316 292

Learnings:

  • Hardware SPI for the MAX7219 is approx. 7 times faster than software bitbang.
  • The write method is sligthly slower than setDigit. This is due to the more complex code (decimal points, shifting, line feed...).
  • The print method adds some overhead as it calls .write
  • The difference between SPI 4000000 and SPI 8000000 is not very significant. On all my displays the faster rate works stable. Nevertheless I decided to implement SPI 4000000. If you want to speed up SPI, you have to patch the .cpp file.

Conclusion:

As conclusion: use the Hardware SPI for your MAX7219/MAX7221 if you can use the SPI pins!

Installation and usage of the new Noiasca Led Control Library

Download the ZIP (at the end of this page) and unzip it to your libraries folder. It might be that you will need a restart of your Arduino IDE before you can use the library.

There is an additional example sketch called LCDemoNoiascaHelloWorld. It shows all available methods and some special things you should know about the library.

If you want to migrate an existing sketch you have to follow these steps:

a) Modify the #include of the existing library to the new NoiascaLedControl

 #include <NoiascaLedControl.h>

b) The object needs a call of the .begin() method in your setup(). So if your object is named lc, add following line in your setup:

 lc.begin();

c) Before any additional changes do a test compile of your sketch. The sketch should still compile without any new errors. The NoiascaLedControl should be backward compatible.

d) Adopt your print outs to the new print method, replace setting of cursor position and adopt your text to be shown as correctly as possible.

Version

  • 2022-02-28 V1.2.4: fixed to make it compile also for the Nano 33 BLE (several naming conflicts for expansion of macro ‘SPI_MOSI’)
  • 2022-01-09 V1.2.3: SPI should be included in the user sketch, SPI.begin musst be called in the user sketch, adopted examples
  • 2021-11-14 V1.2.2: fix for printing dots at position 0
  • 2020-12-08 V1.2.0: internal: split into abstract base class and hw implementation, enabling the inheritance for your own implementation
  • 2020-08-08 V1.1.1: no hardware calls in constructor, begin() method has to be called separatly, soft bit bang and hardware SPI splitted
  • 2020-03-07 V1.1.0: add hardware SPI (better performance than software bit bang)
  • 2020-03-06 V1.0.1: internal improvement rebuild int to uint8_t
  • 2019-04-25 V1.0.0: initial version, inheritance from print class

Licence

I was not able to contact Eberhard Fahle regarding licence topics. Therefore I keept his name as author in the header of .cpp/.h.

(*) Disclosure: Some of the links above are affiliate links, meaning, at no additional cost to you I will earn a (little) comission if you click through and make a purchase. I only recommend products I own myself and I'm convinced they are useful for other makers.

History

First upload: 2019-04-25 | Version: 2024-09-17