Components · 17 min read

MAX7219 Arduino LED Matrix: Scrolling Text with MD_Parola

MAX7219 Arduino guide: wire 8x8 and 4-in-1 8x32 LED matrix modules, scroll text with MD_Parola, fix mirrored text with FC16_HW, plus clock and ESP32 code.

The MAX7219 is a cheap driver chip that runs 64 LEDs on its own, and it's the easiest way to use an LED matrix with Arduino. You'll usually find it in two forms: a module with a single 8x8 dot matrix and a 4-in-1 8x32 dot matrix module with four matrices side by side. The MAX7219 needs only 3 signal wires from the Arduino (DIN, CS, CLK); it scans the rows hundreds of times per second, sets the brightness and remembers the state of every LED by itself. That leaves the Arduino free for scrolling text, clocks, scoreboards or animations, and you can make the display as long as you like by chaining modules end to end.

In this guide you'll learn how the MAX7219 works, how to connect the module to an Arduino Uno, ESP32 and Raspberry Pi Pico over SPI, how to chain modules and how to show your first scrolling text with the MD_Parola library. Then you'll fix the upside-down or mirrored text problem that trips up most beginners by choosing the right hardware type (FC16_HW), work out brightness and power consumption, display accented and custom characters, and build a clock with an RTC module and a pixel-by-pixel animation. The final sections cover sending messages from your phone with an ESP32, MicroPython code for the Pico and fixes for the most common mistakes.

MAX7219 4-in-1 8x32 dot matrix LED module
4-in-1 MAX7219 dot matrix module: four 8x8 matrices with a MAX7219 chip under each one; it's fed from the IN side and connects to the next module from the OUT side.

What is the MAX7219 and how does it work?

You can't wire 64 LEDs to individual Arduino pins, so LED matrices use multiplexing. In an 8x8 matrix the LEDs sit on a grid of 8 rows and 8 columns. At any moment the MAX7219 enables just one group of LEDs (a "digit" in the datasheet, which is a row or a column on a matrix), drives the other lines to choose which of the eight LEDs in that group light up, then moves on to the next group. All eight groups are scanned about 800 times per second; your eye can't follow that speed, so the image looks steady. Meanwhile the chip keeps each group's pattern in its own 8x8-bit RAM, limits the LED current with a single resistor and sets the brightness in 16 steps.

The Arduino talks to the MAX7219 over a 3-wire serial interface that works much like SPI. Every command is a 16-bit packet: the upper byte selects the register to write and the lower byte carries the value. While CS (LOAD in the datasheet) is LOW, bits shift in from DIN on every rising edge of CLK; when CS goes HIGH again, the packet is written to the register. It's the same idea as the 74HC595 shift register, but because the MAX7219 also handles scanning and current limiting, it's far less work. You'll find how SPI works in general in What is SPI?.

FeatureValue
Supply voltage4.0–5.5 V (5 V in practice)
Interface3-wire serial (DIN, CLK, CS), up to 10 MHz
LEDs driven64 (an 8x8 matrix or eight 7-segment digits)
Scan rate800 Hz typical (8 digits)
Segment currentSet by the RSET resistor, 40 mA max (usually 10 kΩ on modules)
Brightness16 steps, duty cycle from 1/32 to 31/32
Shutdown currentAbout 150 µA
Input HIGH thresholdAt least 3.5 V (with a 5 V supply)
Daisy chainingDOUT output to the DIN input of the next chip

The MAX7219 has 14 addressable registers, but the ones you need for an LED matrix are in the table below. The libraries set them for you; still, knowing what they do helps a lot when you're debugging. For example, when the chip is first powered it starts in shutdown mode with a dark display, so if the initialisation code doesn't run, the module seems completely dead. Its sibling, the MAX7221, has the same registers but adds a true SPI-compatible CS pin and slew-rate-limited segment drivers that reduce electromagnetic interference.

AddressRegisterPurpose
0x00No-OpLeaves this chip unchanged while a packet goes to another chip in the chain
0x01–0x08Digit 0–7The 8-bit LED pattern of each of the eight rows (or columns)
0x09Decode ModeBCD decoding for 7-segment digits; off (0x00) for a matrix
0x0AIntensityBrightness, from 0x0 to 0xF
0x0BScan LimitNumber of digits to scan; 7 (all eight) for a matrix
0x0CShutdown0: shutdown mode, 1: normal operation
0x0FDisplay Test1: every LED lights at full brightness

Single 8x8 module or 4-in-1 8x32 module?

The two modules you'll meet most often use the same circuit; the difference is how the LED matrix is wired to the MAX7219 and the shape of the board. The single module on a green board has a DIP (24-pin) MAX7219 and an 8x8 matrix such as the 1088AS, with input pins (VCC, GND, DIN, CS, CLK) at one end and output pins (VCC, GND, DOUT, CS, CLK) at the other. On the blue 4-in-1 module, four SMD MAX7219 chips sit under the matrices and the four blocks are already chained at the factory; the board is usually marked FC-16. For long scrolling text, the 4-in-1 module is both cheaper and tidier.

Single 8x8 MAX7219 LED matrix module
Single 8x8 MAX7219 module: green board, DIP MAX7219 and an 8x8 dot matrix; it usually displays correctly with the GENERIC_HW hardware type in code.
FeatureSingle 8x8 module4-in-1 8x32 module (FC-16)
Resolution8x8 = 64 LEDs32x8 = 256 LEDs
Number of MAX7219s1 (DIP package)4 (SMD package)
Hardware type in codeUsually GENERIC_HWUsually FC16_HW
Characters visible at once14–6 (depending on letter width)
Maximum current at full brightnessAbout 330 mAAbout 1.3 A
Best forIcons, dice, smiley faces, small gamesScrolling text, clocks, scoreboards

MAX7219 wiring: Arduino Uno, ESP32 and Pico

The module has five pins: VCC and GND for power, DIN for data, CLK for the clock and CS for chip select (load). By default the MD_Parola and MD_MAX72XX libraries use hardware SPI, so DIN must go to your board's MOSI pin and CLK to its SCK pin, while CS can be any digital pin. You can check where the hardware SPI pins are on your board with the Pinout tool. Always plug the wires into the module's input (IN) side, the end marked DIN; the end marked DOUT is for the next module.

Module pinArduino UnoESP32Raspberry Pi Pico
VCC5V5V (VIN)VBUS (5 V)
GNDGNDGNDGND
DIND11 (MOSI)GPIO 23 (MOSI)GP19 (SPI0 TX)
CSD10GPIO 5GP17
CLKD13 (SCK)GPIO 18 (SCK)GP18 (SPI0 SCK)
Don't power a MAX7219 module from the 3.3 V pin; the chip needs at least 4 V. Even at 5 V, a single 8x8 module can draw more than 300 mA at full brightness and a 4-in-1 module up to 1.3 A. A computer's USB port and the Arduino's USB fuse are limited to 500 mA. If the Arduino resets or the image flickers when you turn up the brightness, power the module from an external 5 V adapter (at least 2 A for a 4-in-1 module) and always connect the adapter's GND to the Arduino's GND.

The ESP32 and Pico have 3.3 V pins, while the MAX7219 datasheet asks for at least 3.5 V as a HIGH level with a 5 V supply. So 3.3 V signals are technically just below the limit. Most modules still work, but with long wires or certain chip batches you may see random dots and corrupted images. The reliable fix is a buffer such as the 74HCT125, which accepts 3.3 V as HIGH and outputs 5 V, or a fast logic level converter on the DIN, CLK and CS lines. Keeping the signal wires under 20 cm often solves the problem too.

Chaining modules (daisy chain)

The MAX7219's DOUT pin outputs the data that entered DIN 16.5 clock cycles later. When you connect the OUT side of one module to the IN side of the next, DOUT goes to the next chip's DIN, while CLK and CS are shared by all chips. That way only 3 signal wires still leave the Arduino, but the display can be 8, 16 or 32 blocks long. On every update the library sends packets down the whole chain, and when CS rises they all update at the same moment. On 4-in-1 modules this wiring is already on the board; to make an 8-block (64x8) display from two 4-in-1 modules, simply connect the OUT pins of the first module to the IN pins of the second and set MAX_DEVICES to 8 in your code.

In the library, block numbers start at the module closest to the DIN input and increase to the left; for MD_MAX72XX the origin is the top-right corner of the display. That's why the DIN input must stay on the right-hand side for the text to read correctly. As the chain grows, power becomes the real limit: feed 5 V and GND back into the chain with thick wires every 4–8 blocks, and add a 10 µF electrolytic and a 100 nF ceramic capacitor at each module's supply input. The datasheet recommends this pair too, to smooth out the ripple caused by the LEDs' current pulses.

Installing the libraries: MD_MAX72XX and MD_Parola

The two most popular MAX7219 libraries come from the same author (majicDesigns) and work together. MD_MAX72XX does the low-level work: it initialises the modules, writes pixels and columns and manages the font. MD_Parola builds on it with scrolling text, effects, alignment and zones. The LedControl library you'll see in older examples works too, but you have to write the scrolling yourself. If you've never installed a library, read How to add a library in Arduino first.

  1. In the Arduino IDE, open the Library Manager (Tools > Manage Libraries) and search for MD_Parola.
  2. Install the MD_Parola library by majicDesigns. When it asks about dependencies, choose Install all so MD_MAX72XX is installed too.
  3. Wire the module as shown in the table above, and for a 4-in-1 module set HARDWARE_TYPE to FC16_HW and MAX_DEVICES to 4 in your code.
  4. Upload the sketch. If the text looks upside down, mirrored or rotated, don't worry; you'll fix it with a single line in the hardware type section below.

First program: scrolling text with MD_Parola

Arduino (C++)
1// MAX7219 4'lü modülde kayan yazı (MD_Parola) / Scrolling text on a 4-in-1 MAX7219 module (MD_Parola)
2#include <MD_Parola.h>
3#include <MD_MAX72xx.h>
4#include <SPI.h>
5
6#define HARDWARE_TYPE MD_MAX72XX::FC16_HW   // mavi PCB'li 4'lü modül / blue-PCB 4-in-1 module
7#define MAX_DEVICES 4                       // zincirdeki 8x8 blok sayısı / number of 8x8 blocks in the chain
8#define CS_PIN 10                           // DIN = D11 (MOSI), CLK = D13 (SCK)
9
10MD_Parola display = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
11
12void setup() {
13  display.begin();
14  display.setIntensity(4);                  // parlaklık 0–15 / brightness 0–15
15  display.displayClear();
16  // metin, hizalama, hız (ms/kare), bekleme (ms), giriş efekti, çıkış efekti
17  // text, alignment, speed (ms/frame), pause (ms), entry effect, exit effect
18  display.displayText("Merhaba BlueGrays!", PA_CENTER, 50, 1000, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
19}
20
21void loop() {
22  if (display.displayAnimate()) {           // efekt bittiğinde true döner / returns true when the effect ends
23    display.displayReset();                 // aynı yazıyı baştan başlat / restart the same text
24  }
25}

The most important line is displayText(): it takes the text, the alignment (PA_LEFT, PA_CENTER, PA_RIGHT), the time between frames in milliseconds, the pause after the text settles and the entry and exit effects, in that order. The smaller the speed value, the faster the text moves; 25–60 ms gives readable scrolling. displayAnimate() advances at most one frame each time it's called and returns straight away. That's why it has to be called constantly in loop() and why you shouldn't use delay() in the rest of your code; otherwise the text stutters. You'll find how to wait without blocking the program in millis() vs delay().

FunctionWhat it does
begin()Initialises the modules, wakes them from shutdown and clears the display
displayText(text, align, speed, pause, effectIn, effectOut)Sets the text and effects to show and starts the animation
displayAnimate()Draws the next frame; returns true when the effect has finished
displayReset()Plays the same text and effect again from the start
print("...")Shows static text with no effect
setIntensity(0–15)Sets the brightness of all modules
setInvert(true)Swaps lit and unlit LEDs (negative image)
displayShutdown(true)Puts the display into shutdown mode; false wakes it up
begin(2) and setZone(0, 0, 1)Splits the display into independent zones, each with its own text and effect

Apart from PA_SCROLL_LEFT there are many entry and exit effects: PA_PRINT (text appears at once), PA_SCROLL_UP and PA_SCROLL_DOWN (vertical scrolling), PA_WIPE, PA_OPENING and PA_CLOSING (opening from and closing to the centre), PA_FADE (fading through brightness), PA_DISSOLVE, PA_BLINDS, PA_RANDOM and PA_GROW_UP. To see them all, upload the Parola_Animation_Catalog sketch from File > Examples > MD_Parola; don't forget to change the HARDWARE_TYPE and MAX_DEVICES lines to match your module.

Choosing the hardware type (FC16_HW) and fixing mirrored text

Manufacturers wire the 8x8 matrix to the MAX7219 in different ways: on some boards the chip's "digit" lines go to the rows, on others to the columns, and the row or column order may be reversed. The library makes up for this with the HARDWARE_TYPE setting. If you pick the wrong type the module isn't faulty, but the letters in every 8x8 block look mirrored, upside down or rotated by 90 degrees. Because most older code online uses PAROLA_HW or GENERIC_HW, this is the most common problem with the blue 4-in-1 modules sold today. The table below shows what to change for each symptom; it's based on the 4-in-1 FC-16 module.

What you seeLikely causeFix
Letters are mirrored left to right in every blockPAROLA_HW selected, column order reversedUse FC16_HW
Letters are rotated 90 degrees in every blockGENERIC_HW selected, rows and columns swappedUse FC16_HW
Letters are upside down in every blockICSTATION_HW selected, row and column order reversedUse FC16_HW
The whole text is upside down and scrolls left to rightThe module is the wrong way round, DIN input on the leftTurn the module 180 degrees so the DIN input is on the right
Only some blocks are correct, the rest are blank or scrambledMAX_DEVICES doesn't match the real number of blocksSet MAX_DEVICES to the number of blocks
On a single green 8x8 module try GENERIC_HW first; on a blue 4-in-1 module try FC16_HW first. If the image is still wrong, try the four types in turn; only one line changes each time. To find your module's type for certain, upload the MD_MAX72xx_HW_Mapper sketch from the MD_MAX72XX examples: it lights the LEDs one by one and shows in the Serial Monitor which row and column is wired where.

Brightness and power consumption

Brightness depends on two things. The first is the RSET resistor on the module: the chip sends about 100 times the current flowing out of the ISET pin to the LEDs. Modules usually use 10 kΩ, which means a peak current slightly above 40 mA for red LEDs and about 40 mA for blue and white ones. The second is the Intensity register you set in software: a value n from 0 to 15 keeps the LEDs on for (2n+1)/32 of each scan interval, so 0 is the lowest (1/32) and 15 the highest (31/32) brightness. And because each row is only lit for one eighth of the time, even at full brightness a single LED averages about 5 mA. You can look up forward voltages by LED colour in the LED voltages tool.

You can roughly estimate a module's average current like this: I ≈ 8 mA + 5 mA × (2n+1)/32 × L. Here 8 mA is the chip's own consumption, n is the brightness value and L is the number of lit LEDs on that module (0–64); 5 mA is one eighth of the 40 mA peak current. For example, at brightness 15 with all 64 LEDs on you get 8 + 5 × 0.97 × 64 ≈ 318 mA, which agrees with the 330 mA in the datasheet. Scrolling text usually lights only 25–35 percent of the LEDs, so real consumption is much lower. The table below shows typical values for a 4-in-1 module.

ConditionPer module4-in-1 module (32x8)
All LEDs on, brightness 15≈ 320 mA≈ 1.3 A
All LEDs on, brightness 7≈ 160 mA≈ 630 mA
Scrolling text (~30% of LEDs), brightness 7≈ 55 mA≈ 210 mA
Scrolling text (~30% of LEDs), brightness 2≈ 25 mA≈ 90 mA
Shutdown mode (displayShutdown)≈ 0.15 mA≈ 0.6 mA

To dim the display at night you can adjust the brightness automatically with an LDR light sensor, and put the display to sleep with displayShutdown(true) when nobody is around. If you're designing a battery-powered sign, enter the values above into the Battery life tool to work out how many hours it will last. Indoors, a brightness of around 2–4 instead of 15 is usually enough; it's easier on the eyes and cuts the LED current to roughly a third to a sixth.

Accented and special characters (UTF-8)

The Arduino IDE saves your code as UTF-8, so a letter such as "é" or "ü" isn't one byte in the text but two. MD_Parola looks up every byte as a character number in the font, so accented letters turn into meaningless symbols. The library's default font contains the full Latin-1 set (é, è, à, ñ, ç, ä, ö, ü, ß and more), so all you need is a small function that converts UTF-8 into the font's codes. Letters outside Latin-1, such as the Turkish ğ, ı and ş in the example below, have to be drawn pixel by pixel and added with addChar(). The sketch does both steps.

Arduino (C++)
1// MAX7219 + MD_Parola: Türkçe karakterli kayan yazı / Scrolling text with Turkish letters
2#include <MD_Parola.h>
3#include <MD_MAX72xx.h>
4#include <SPI.h>
5
6#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
7#define MAX_DEVICES 4
8#define CS_PIN 10
9
10MD_Parola display = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
11
12// Fontta olmayan 6 harf: ilk bayt genişlik, sonrakiler sütunlar (bit 0 = en üst satır)
13// 6 letters missing from the font: first byte is the width, then the columns (bit 0 = top row)
14const uint8_t trG[] = {5, 56, 69, 70, 85, 116};  // Ğ
15const uint8_t trg[] = {4, 25, 166, 166, 125};    // ğ
16const uint8_t trI[] = {3, 68, 125, 68};          // İ
17const uint8_t tri[] = {1, 124};                  // ı
18const uint8_t trS[] = {5, 70, 73, 201, 73, 49};  // Ş
19const uint8_t trs[] = {4, 88, 212, 84, 52};      // ş
20
21// UTF-8 metni fontun kodlarına çevirir / converts UTF-8 text to the font's codes
22void utf8ToFont(const char *src, char *dst, size_t size) {
23  size_t n = 0;
24  while (*src && n < size - 1) {
25    uint8_t c = *src++;
26    if (c < 0x80) { dst[n++] = c; continue; }            // ASCII aynen kalır / ASCII stays as is
27    if ((c & 0xE0) == 0xC0 && *src) {                    // 2 baytlık harf / 2-byte letter
28      uint16_t u = ((c & 0x1F) << 6) | (*src++ & 0x3F);
29      switch (u) {
30        case 0x11E: dst[n++] = (char)0x80; break;        // Ğ
31        case 0x11F: dst[n++] = (char)0x81; break;        // ğ
32        case 0x130: dst[n++] = (char)0x82; break;        // İ
33        case 0x131: dst[n++] = (char)0x83; break;        // ı
34        case 0x15E: dst[n++] = (char)0x84; break;        // Ş
35        case 0x15F: dst[n++] = (char)0x85; break;        // ş
36        default: dst[n++] = (u < 0x100) ? (char)u : '?'; // ç ö ü Ç Ö Ü ° fontta var / already in the font
37      }
38      continue;
39    }
40    while ((*src & 0xC0) == 0x80) src++;                 // desteklenmeyen karakteri atla / skip an unsupported character
41    dst[n++] = '?';
42  }
43  dst[n] = '\0';
44}
45
46char message[96];
47
48void setup() {
49  display.begin();
50  display.setIntensity(3);
51  display.addChar(0x80, trG);                // addChar begin()'den sonra / addChar after begin()
52  display.addChar(0x81, trg);
53  display.addChar(0x82, trI);
54  display.addChar(0x83, tri);
55  display.addChar(0x84, trS);
56  display.addChar(0x85, trs);
57  utf8ToFont("Günaydın İstanbul! Şehirde hava 22°C, çok güzel bir gün.", message, sizeof(message));
58  display.displayText(message, PA_LEFT, 40, 1000, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
59}
60
61void loop() {
62  if (display.displayAnimate()) display.displayReset();
63}

In each custom character array, the first byte is the width of the letter in columns and the following bytes are the columns from left to right; bit 0 of each byte is the top row. For example, the dotless ı is a single column, and the value 124 (0b01111100) lights rows 2 to 6; the only difference from a normal i is the missing dot on top. On the capital Ğ and İ the body of the letter was made two rows shorter (5 rows instead of 7) to make room for the accent mark. addChar() doesn't copy the data, it only stores its address, so the arrays must be global and added after begin(). The codes 0x80–0x85 were chosen because they never appear in normal text. In the same way you can add any symbol you like, such as a degree sign, an arrow or a heart.

Clock example: a digital clock with an RTC module

DS1307 RTC clock module
DS1307 RTC module: thanks to its coin cell it keeps counting time even when the Arduino is off; it connects to the I2C bus via the SDA and SCL pins.

The Arduino's millis() counter resets when power is lost, and because of the Uno's ceramic resonator it can drift by up to several minutes a day. For a real clock, use an RTC clock module. Connect the DS1307 module to the I2C bus (SDA → A4, SCL → A5, VCC → 5V, GND → GND); the MAX7219 uses the SPI pins, so the two modules don't clash. For the code, install Adafruit's RTClib library from the Library Manager.

Arduino (C++)
1// MAX7219 + DS1307 RTC ile dijital saat / Digital clock with MAX7219 + DS1307 RTC
2#include <MD_Parola.h>
3#include <MD_MAX72xx.h>
4#include <SPI.h>
5#include <RTClib.h>                         // Adafruit RTClib
6
7#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
8#define MAX_DEVICES 4
9#define CS_PIN 10
10
11MD_Parola display = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
12RTC_DS1307 rtc;                             // SDA = A4, SCL = A5
13
14char timeText[8];                           // "12:34" + sonlandırıcı / + terminator
15uint8_t lastSecond = 99;
16
17void setup() {
18  display.begin();
19  display.setIntensity(2);
20  display.setTextAlignment(PA_CENTER);      // print() bu hizalamayı kullanır / print() uses this alignment
21  if (!rtc.begin()) {
22    display.print("RTC?");                  // modül bulunamadı / module not found
23    while (true) {}
24  }
25  if (!rtc.isrunning()) {
26    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));   // ilk kurulumda derleme saati / compile time on first run
27  }
28}
29
30void loop() {
31  DateTime now = rtc.now();
32  if (now.second() != lastSecond) {         // saniyede bir güncelle / update once a second
33    lastSecond = now.second();
34    // ':' ve boşluk aynı genişlikte, yazı kaymaz / ':' and space are equally wide, nothing shifts
35    sprintf(timeText, (lastSecond % 2) ? "%02d %02d" : "%02d:%02d", now.hour(), now.minute());
36    display.print(timeText);
37  }
38}

The sketch reads the time once a second and blinks the colon on and off. In the default font the colon (:) and the space are the same width (2 columns), so the digits don't jump around. When the RTC runs for the first time, it's set to the moment the sketch was compiled. A DS1307 can drift by about a minute a month; for a more accurate clock, get a DS3231 module and use RTC_DS3231 instead of RTC_DS1307 and change the condition if (!rtc.isrunning()) to if (rtc.lostPower()). Thanks to its temperature-compensated crystal, the DS3231 drifts only about a minute a year. You can also split the display into two zones to show the time on one side and the temperature on the other.

Animation: drawing pixel by pixel with MD_MAX72XX

To go beyond scrolling text, you can use the MD_MAX72XX library directly. It treats the whole chain as a single pixel display: a 4-in-1 module has 32 columns and 8 rows. setPoint(row, column, state) turns a single LED on or off, setColumn() writes all 8 bits of a column at once and clear() blanks the display. The origin (0, 0) is the top-right corner; column numbers increase to the left and row numbers increase downwards. The code below draws a ball that bounces off the edges of the display:

Arduino (C++)
1// MD_MAX72XX ile seken top animasyonu / Bouncing ball animation with MD_MAX72XX
2#include <MD_MAX72xx.h>
3#include <SPI.h>
4
5#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
6#define MAX_DEVICES 4
7#define CS_PIN 10
8
9MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
10
11int x = 0, y = 0;                           // topun konumu (sütun, satır) / ball position (column, row)
12int dx = 1, dy = 1;                         // hareket yönü / direction of travel
13
14void setup() {
15  mx.begin();
16  mx.control(MD_MAX72XX::INTENSITY, 2);
17  mx.clear();
18}
19
20void loop() {
21  const int lastCol = mx.getColumnCount() - 1;          // 4 modülde 31 / 31 with 4 modules
22  mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);      // çizim bitene kadar gönderme / hold updates while drawing
23  mx.setPoint(y, x, false);                             // eski konumu sil / erase the old position
24  x += dx;
25  y += dy;
26  if (x <= 0 || x >= lastCol) dx = -dx;                 // kenara çarpınca dön / bounce off the edge
27  if (y <= 0 || y >= 7) dy = -dy;
28  mx.setPoint(y, x, true);                              // yeni konumu yak / light the new position
29  mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);       // hepsini tek seferde gönder / send everything at once
30  delay(40);
31}

In every frame, automatic updating (UPDATE) is switched off first, the old pixel is erased and the new one drawn, then UPDATE is switched back on so the changes go to the modules in one go. This stops a half-drawn frame from appearing on the display. With the same approach you can draw dice, smiley faces, a snake game or audio level bars. The MD_MAX72XX examples also include ready-made animations such as Pac-Man, robot eyes and an hourglass. When you're using MD_Parola, you can reach the same drawing functions through getGraphicObject().

Messages over the internet with an ESP32: scrolling text from your phone

With an ESP32 you can make your scrolling sign wireless. The code below connects to your home network, first scrolls its own IP address across the display, then shows the time it gets from the internet (NTP). Open that IP address in the browser of a phone on the same Wi-Fi and you'll see a text box; the message you type starts scrolling on the next round and alternates with the clock. The module connects to the VSPI pins as in the table: CLK to GPIO 18, DIN to GPIO 23 and CS to GPIO 5. If you're new to the ESP32, read What is the ESP32? first.

Arduino (C++)
1// ESP32: telefondan mesaj + internet saati / ESP32: message from your phone + internet time
2#include <WiFi.h>
3#include <WebServer.h>
4#include <time.h>
5#include <MD_Parola.h>
6#include <MD_MAX72xx.h>
7#include <SPI.h>
8
9#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
10#define MAX_DEVICES 4
11#define CS_PIN 5                            // VSPI: CLK = GPIO 18, DIN = GPIO 23
12
13const char* ssid = "AgAdi";                 // Wi-Fi adın / your Wi-Fi name
14const char* password = "Sifre123";          // Wi-Fi şifren / your Wi-Fi password
15
16MD_Parola display = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
17WebServer server(80);
18
19char message[128];
20char nextMessage[128];
21char clockText[8] = "--:--";
22bool newMessage = false;
23bool showClock = false;
24
25const char PAGE[] = R"rawliteral(<!DOCTYPE html><html><head><meta charset="utf-8">
26<meta name="viewport" content="width=device-width"><title>MAX7219</title></head><body>
27<h2>Kayan yazı / Scrolling text</h2>
28<form action="/"><input name="m" maxlength="100"> <button>Gönder / Send</button></form>
29</body></html>)rawliteral";
30
31void handleRoot() {
32  if (server.hasArg("m")) {                 // formdan gelen mesaj / message from the form
33    server.arg("m").toCharArray(nextMessage, sizeof(nextMessage));
34    newMessage = true;
35  }
36  server.send(200, "text/html; charset=utf-8", PAGE);
37}
38
39void setup() {
40  display.begin();
41  display.setIntensity(3);
42  display.print("WiFi..");
43  WiFi.begin(ssid, password);
44  while (WiFi.status() != WL_CONNECTED) delay(500);
45  configTime(3 * 3600, 0, "pool.ntp.org");  // Türkiye UTC+3 / Turkey is UTC+3
46  snprintf(message, sizeof(message), "IP: %s", WiFi.localIP().toString().c_str());
47  server.on("/", handleRoot);
48  server.begin();
49  display.displayText(message, PA_LEFT, 40, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
50}
51
52void loop() {
53  server.handleClient();
54  if (display.displayAnimate()) {           // önceki gösterim bitti / the previous show has ended
55    showClock = !showClock;
56    if (newMessage) {                       // yeni mesaj sırayı keser / a new message jumps the queue
57      strlcpy(message, nextMessage, sizeof(message));
58      newMessage = false;
59      showClock = false;
60    }
61    if (showClock) {
62      struct tm t;
63      if (getLocalTime(&t, 10)) strftime(clockText, sizeof(clockText), "%H:%M", &t);
64      display.displayText(clockText, PA_CENTER, 40, 3000, PA_SCROLL_DOWN, PA_SCROLL_UP);
65    } else {
66      display.displayText(message, PA_LEFT, 40, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
67    }
68  }
69}

handleRoot() doesn't push the message from the form straight to the display; it parks it in nextMessage. The message is copied only when the current animation has finished (when displayAnimate() returns true), so the text MD_Parola is reading never changes halfway through. The 3 × 3600 value in configTime() is the UTC+3 time zone used in Turkey; for the UK, replace that line with configTzTime("GMT0BST,M3.5.0/1,M10.5.0", "pool.ntp.org"), which also handles daylight saving time. For accented messages, add the addChar() lines and the utf8ToFont() function from above to this sketch and replace the strlcpy() line with utf8ToFont(nextMessage, message, sizeof(message)). For internet time and real-time web dashboards, see ESP32 NTP clock and ESP32 web server and WebSocket.

MAX7219 with the Raspberry Pi Pico and MicroPython

On the Pico with MicroPython, the most practical approach is to copy the open-source max7219.py driver (micropython-max7219) to the Pico with Thonny. Because the driver uses MicroPython's framebuf module, drawing functions such as text(), pixel(), line() and rect() come ready to use, and it runs in the right orientation on 4-in-1 FC-16 modules. Power the module from VBUS (5 V) and connect DIN to GP19, CLK to GP18 and CS to GP17. MicroPython also claims SPI0's MISO pin (GP16), so don't use that pin for anything else. framebuf's font is 8x8 pixels and ASCII only, so a 4-in-1 module shows 4 letters at a time and accented letters aren't displayed. For setup, see What is the Raspberry Pi Pico? and MicroPython basics.

MicroPython
1# Pico + 4'lü MAX7219 modül: kayan yazı / Pico + 4-in-1 MAX7219 module: scrolling text
2from machine import Pin, SPI
3import time
4import max7219                        # max7219.py Pico'da olmalı / must be on the Pico
5
6# GP18 = SCK -> CLK, GP19 = MOSI -> DIN, GP17 -> CS; GP16 (MISO) boş kalır / GP16 (MISO) stays free
7spi = SPI(0, baudrate=5_000_000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(19))
8display = max7219.Matrix8x8(spi, Pin(17, Pin.OUT), 4)
9display.brightness(3)                 # parlaklık 0–15 / brightness 0–15
10
11def scroll(text, delay_ms=35):
12    width = len(text) * 8             # her harf 8 piksel / each letter is 8 px wide
13    for x in range(32, -width, -1):   # sağdan girip solda kaybolur / enters right, leaves left
14        display.fill(0)
15        display.text(text, x, 0, 1)
16        display.show()
17        time.sleep_ms(delay_ms)
18
19while True:
20    scroll("BlueGrays + Pico")
21    display.fill(0)
22    display.text("Pico", 0, 0, 1)     # 4 harf tam sığar / exactly 4 letters fit
23    display.show()
24    time.sleep(1)
25    display.fill(0)
26    display.rect(0, 0, 32, 8, 1)      # ekran çerçevesi / display frame
27    display.show()
28    time.sleep_ms(500)

MAX7219 project ideas

  • Scrolling message sign: build a message board for a shop, classroom or bedroom door, and change the message from your phone with an HC-05 Bluetooth module.
  • Clock, calendar and temperature: show the time and date from an RTC and the temperature and humidity from a DHT11 sensor in turn.
  • Internet clock and weather: scroll the NTP time and the temperature from a weather API with an ESP32.
  • Scoreboard: run a two-zone display for a table tennis or basketball match with an IR remote.
  • Mini games: play snake, Pong or an obstacle dodger on the 32x8 display with a joystick; more ideas in Arduino game projects.
  • Audio level meter: draw the readings from a sound sensor as a 32-column bar graph.
  • 32x32 animation panel: stack four 4-in-1 modules on top of each other and play animations such as rain, stars and fireworks.

Common mistakes and how to fix them

SymptomLikely causeFix
No LEDs light upNo power, the wires are on the module's OUT side, or the CS pin doesn't match the codePlug the wires into the IN side marked DIN and check CS_PIN and the 5 V supply
All LEDs are fully lit and never changeNo data arrives: DIN and CLK are swapped, or the chip is stuck in test modeConnect DIN to MOSI and CLK to SCK, then power-cycle the module
Letters are mirrored, upside down or rotated 90 degreesWrong HARDWARE_TYPETry FC16_HW on a 4-in-1 module and GENERIC_HW on a single module
The whole text is upside down and scrolls left to rightThe module is mounted the wrong way round, DIN input on the leftTurn the module 180 degrees; the DIN input must be on the right
Only one block works or the text looks cut offWrong MAX_DEVICES or a broken link between modulesSet MAX_DEVICES to the number of blocks and check the OUT → IN wires
The Arduino resets or the image flickers at higher brightnessThe 500 mA USB limit is exceeded and the voltage dropsUse an external 5 V 2 A adapter, join the grounds and lower the brightness
Random dots and corrupted characters on an ESP32The 3.3 V signal is below the 3.5 V HIGH threshold, and the wires are longShift the level up with a 74HCT125 and shorten the wires
Accented letters show up as meaningless symbolsThe UTF-8 bytes go straight to the fontUse the utf8ToFont() and addChar() approach
The scrolling text stutters and jerksThere's a delay() or a long task in loop()Call displayAnimate() often and handle waits with millis()
When something doesn't work, start with the simplest test: leave only display.begin() and display.print("8888") in the code. If no LEDs light up, the problem is the wiring or the power supply; use a multimeter to check for 4.5–5 V between the module's VCC and GND pins. If the LEDs light but the digits are garbled, the problem is the hardware type; if only the first block works, look at MAX_DEVICES or the connections between modules.

The MAX7219 is one of the cheapest ways to get an eye-catching display with just a few wires: the chip handles scanning, current limiting and brightness, so you can focus on what to show. Start by drawing icons on a single 8x8 module, then move on to scrolling text, a clock and wireless messages with an ESP32 on a 4-in-1 module. If you want colourful, individually controlled LEDs, WS2812B NeoPixel LEDs are a good choice, and for a project that only shows numbers, a 7-segment display. In the BlueGrays app you'll find MAX7219 projects such as a dot matrix clock and calendar, a weather station, 32x8 animations and scrolling text, step by step with circuit diagrams and code.

Frequently asked questions

Why does the text on my MAX7219 module look upside down or mirrored?

Manufacturers wire the 8x8 matrix to the MAX7219 in different ways, and the library compensates with the HARDWARE_TYPE setting. Most blue 4-in-1 modules sold today display correctly with FC16_HW, and single green modules with GENERIC_HW. If the image is still wrong, try PAROLA_HW and ICSTATION_HW as well; only one line of code changes.

How many MAX7219 modules can be daisy-chained?

The chip itself has no limit, and because MD_MAX72XX stores the number of modules in a byte, the software allows up to 255. In practice, power and signal quality set the limit: feed 5 V and GND back into the chain every 4–8 blocks, keep the wires short and add a 10 µF and a 100 nF capacitor to each module. The longer the chain, the longer each update takes and the more RAM it uses.

Does the MAX7219 work with an ESP32 at 3.3 V?

The MAX7219 needs at least a 4 V supply, so power the module from the ESP32's 5V (VIN) pin. The datasheet asks for 3.5 V as a HIGH level with a 5 V supply; the ESP32's 3.3 V signals still work on most modules, but they're below that limit. For reliable operation, put a level shifter such as a 74HCT125 on the DIN, CLK and CS lines.

How much current does a MAX7219 LED matrix draw?

A single 8x8 module with all LEDs on at full brightness draws about 330 mA, and a 4-in-1 8x32 module up to 1.3 A. With scrolling text only some of the LEDs are lit and brightness is usually kept between 2 and 7, so a 4-in-1 module typically draws around 100–250 mA. In shutdown mode each chip uses only about 150 µA.

How do I show accented or special characters on a MAX7219?

The MD_Parola default font contains the Latin-1 characters such as é, ñ, ä, ö, ü and ß, but the Arduino IDE stores them as two UTF-8 bytes. Convert the text into the font's codes with a small function before displaying it. Characters outside Latin-1 can be drawn yourself and added with addChar().

Can a MAX7219 drive a 7-segment display?

Yes. The MAX7219 was actually designed for common-cathode 7-segment displays with up to 8 digits, and it has a BCD mode that decodes the digits by itself. You can easily use ready-made 8-digit MAX7219 display modules with the setDigit() function of the LedControl library. Common-anode displays can't be connected to this chip directly.

See all projects
Dot Matrix Clock and Calendar (MAX7219, DS1307)
#84Intermediate
Premium

Dot Matrix Clock and Calendar (MAX7219, DS1307)

A glowing board for your desk or wall that shows the time and date in scrolling text. The 8x32 LED display holds a big-digit clock whose two dots blink every second; at the start of each minute the da

5.0(37)
58
Dot Matrix Clock, Calendar and Weather Station (MAX7219, DS1307)
#85Intermediate
Premium

Dot Matrix Clock, Calendar and Weather Station (MAX7219, DS1307)

A scrolling-text board for your desk or wall that shows the time, the date and the room's weather in turn. On the 8x32 LED display you first see the temperature in both Celsius and Fahrenheit, then th

5.0(50)
66
4x 32x8 Matrix LED Animation and Text
#90Intermediate
Premium

4x 32x8 Matrix LED Animation and Text

Stack four long LED matrix modules on top of each other and you get a square screen with more than a thousand dots of light. First, letters pour down from the top like rain in the movies, then a curta

4.6(40)
66
4x 32x8 LED Matrix - 6-Animation Pack
#91Intermediate
Premium

4x 32x8 LED Matrix - 6-Animation Pack

Four 32x8 LED panels stacked on top of each other form a square screen where six animations play in turn: falling digital rain, a growing spiral, cells that are born and die all by themselves, ripplin

4.5(35)
48
Pixel Snake 8x8 Matrix
#88Advanced
Premium

Pixel Snake 8x8 Matrix

Build the classic snake game with your own hands and play it. On a colorful 8x8 light matrix, you steer a green snake with a joystick and eat red apples; with every apple the snake grows, speeds up a

4.7(39)
57
Real-Time Clock from the Internet via ESP32 NTP
#128Advanced
Premium

Real-Time Clock from the Internet via ESP32 NTP

The ESP32 connects to Wi-Fi and retrieves real time with millisecond precision from NTP (Network Time Protocol) servers. Thanks to the `configTzTime()` function, the Turkey time zone (TRT, UTC+3) is automatically applied, and DST transitions in countries that observe daylight saving time are automatically managed using a POSIX TZ string. The time is displayed both on the Serial Monitor and on an optional I2C LCD. `configTzTime()` starts the SNTP service; periodic resynchronization takes place wh

4.5(13)
21
Digital Clock with ESP32 and DS1307 RTC (I2C + LCD)
#120Intermediate
Premium

Digital Clock with ESP32 and DS1307 RTC (I2C + LCD)

In this project, we display the time and date on a 16x2 I2C LCD screen using the DS1307 real-time clock (RTC) module. The DS1307 is a battery-backed RTC that maintains the time even if the ESP32 is powered off. Both modules share the same I2C bus (SDA/SCL), allowing us to learn how to control multiple devices using just two signal wires. Through this project, you will gain practical knowledge of the I2C communication protocol, RTC module operation, and dynamic data display on an LCD screen.

4.6(26)
40
Digital Clock with 4-Digit 7-Segment Display
#48Intermediate
Premium

Digital Clock with 4-Digit 7-Segment Display

A four-digit glowing digital clock, much like the one on a kitchen oven. The DS1302 clock module keeps the time, and every second the Arduino reads the hours and minutes and shows them on the four-dig

4.8(56)
73