Skip to main content

MAX7219 LED Dot Matrix Display

Introduction

The MAX7219 is one of the most popular drivers for 8x8 LED matrix modules. It makes dot matrix displays much easier to use because it handles the multiplexing for you. Instead of manually driving rows and columns yourself, you send data over SPI and let the MAX7219 take care of the display updates.

With Raspberry Pi Pico and MicroPython, this makes it easy to build:

  • scrolling text displays
  • scoreboards
  • counters and timers
  • simple animations
  • menu or status indicators

In this tutorial, you can cover three useful versions of the same display family:

  • a single 8x8 module
  • a chained 4 x 8x8 display
  • a larger 8 x 8x8 display for long scrolling text

Why Use This Module?

Advantages:

  • Simple SPI wiring
  • Works well with MicroPython
  • Easy to chain multiple modules together

Components Needed

ComponentQuantity
Raspberry Pi Pico or Pico W1
MAX7219 8x8 LED matrix module1 or more
Jumper wiresSeveral
USB cable1

Wiring

MAX7219 wiring

Common MAX7219 module pins:

  • VCC
  • GND
  • DIN
  • CS or LOAD
  • CLK

Example Pico wiring:

  • VCC -> VBUS / 5V
  • GND -> GND
  • DIN -> GP3
  • CS -> GP5
  • CLK -> GP2

Library

We will use this library based on - mcauser/micropython-max7219

max7219.py
from micropython import const
import framebuf
import time

_NOOP = const(0)
_DIGIT0 = const(1)
_DECODEMODE = const(9)
_INTENSITY = const(10)
_SCANLIMIT = const(11)
_SHUTDOWN = const(12)
_DISPLAYTEST = const(15)


class Matrix8x8:
def __init__(self, spi, cs, num):
self.spi = spi
self.cs = cs
self.cs.init(cs.OUT, True)
self.buffer = bytearray(8 * num)
self.num = num

fb = framebuf.FrameBuffer(self.buffer, 8 * num, 8, framebuf.MONO_HLSB)
self.framebuf = fb

# expose framebuffer functions
self.fill = fb.fill
self.pixel = fb.pixel
self.hline = fb.hline
self.vline = fb.vline
self.line = fb.line
self.rect = fb.rect
self.fill_rect = fb.fill_rect
self.text = fb.text
self.blit = fb.blit

self.init()

# -----------------------
# LOW LEVEL
# -----------------------
def _write(self, command, data):
self.cs(0)
for _ in range(self.num):
self.spi.write(bytearray([command, data]))
self.cs(1)

def init(self):
for command, data in (
(_SHUTDOWN, 0),
(_DISPLAYTEST, 0),
(_SCANLIMIT, 7),
(_DECODEMODE, 0),
(_SHUTDOWN, 1),
):
self._write(command, data)

def brightness(self, value):
if not 0 <= value <= 15:
raise ValueError("Brightness out of range")
self._write(_INTENSITY, value)

def show(self):
for y in range(8):
self.cs(0)
for m in range(self.num):
self.spi.write(bytearray([_DIGIT0 + y, self.buffer[(y * self.num) + m]]))
self.cs(1)

# -----------------------
# ANIMATION TOOLKIT
# -----------------------

def scroll(self, message, speed=0.08, loop=False):
width = self.num * 8
while True:
for pos in range(len(message) * 8 + width):
self.fill(0)
self.text(message, width - pos, 0, 1)
self.show()
time.sleep(speed)
if not loop:
break

def scroll_reverse(self, message, speed=0.08):
width = self.num * 8
for pos in range(len(message) * 8 + width):
self.fill(0)
self.text(message, -width + pos, 0, 1)
self.show()
time.sleep(speed)

def blink(self, message, times=5, speed=0.3):
for _ in range(times):
self.fill(0)
self.show()
time.sleep(speed)

self.fill(0)
self.text(message, 0, 0, 1)
self.show()
time.sleep(speed)

def fade_in(self, message):
for b in range(16):
self.brightness(b)
self.fill(0)
self.text(message, 0, 0, 1)
self.show()
time.sleep(0.05)

def fade_out(self, message):
for b in range(15, -1, -1):
self.brightness(b)
self.fill(0)
self.text(message, 0, 0, 1)
self.show()
time.sleep(0.05)

def bounce(self, message, speed=0.05):
width = self.num * 8
text_width = len(message) * 8

for pos in range(width - text_width):
self.fill(0)
self.text(message, pos, 0, 1)
self.show()
time.sleep(speed)

for pos in range(width - text_width, 0, -1):
self.fill(0)
self.text(message, pos, 0, 1)
self.show()
time.sleep(speed)

def wipe(self, speed=0.02):
width = self.num * 8
for x in range(width):
self.vline(x, 0, 8, 1)
self.show()
time.sleep(speed)

for x in range(width):
self.vline(x, 0, 8, 0)
self.show()
time.sleep(speed)

def draw_sprite(self, sprite, x_offset=0):
for y in range(8):
for x in range(8):
if sprite[y] & (1 << (7 - x)):
px = x + x_offset
if 0 <= px < self.num * 8:
self.pixel(px, y, 1)

Save that file as max7219.py, then import it like this:

from max7219 import Matrix8x8

Basic Setup

from machine import Pin, SPI
from max7219 import Matrix8x8

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(1)

If you chain multiple modules:

  • single module -> create_display(1)
  • 4 modules -> create_display(4)
  • 8 modules -> create_display(8)

If your wiring changes, you only need to edit the din, cs_pin, and clk values in one place.

Code Explanation

def create_display(modules=1, din=3, cs_pin=5, clk=2):

This helper function keeps the display setup in one place so the later examples stay short and easier to copy.

sck=Pin(clk), mosi=Pin(din)

These values define the CLK and DIN wiring for the MAX7219.

cs = Pin(cs_pin, Pin.OUT)

This sets the CS pin used by the display.

display = create_display(1)

The final 1 means one 8x8 matrix module is connected. If you chain four modules together, you would use create_display(4).

Example 1 - Single 8x8 Module

This is the simplest setup and a good first test to confirm your wiring and library are working.

from machine import Pin, SPI
from max7219 import Matrix8x8
import time

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(1)
display.brightness(5)
display.fill(0)
display.text('A', 0, 0, 1)
display.show()

This clears the display, writes the character A, and sends it to the matrix.

Example 2 - Drawing Basics

This version shows one drawing function at a time so it is easier to understand what each one does.

from machine import Pin, SPI
from max7219 import Matrix8x8
import time

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(1)

# Clear display
display.fill(0)
display.show()

# -----------------------
# Draw a horizontal line
# -----------------------
display.hline(0, 3, 8, 1)
display.show()
time.sleep(1)

# Clear
display.fill(0)
display.show()

# -----------------------
# Draw a vertical line
# -----------------------
display.vline(3, 0, 8, 1)
display.show()
time.sleep(1)

# Clear
display.fill(0)
display.show()

# -----------------------
# Draw a diagonal line
# -----------------------
display.line(0, 0, 7, 7, 1)
display.show()
time.sleep(1)

# Clear
display.fill(0)
display.show()

# -----------------------
# Draw a rectangle
# -----------------------
display.rect(1, 1, 6, 6, 1)
display.show()
time.sleep(1)

# Clear
display.fill(0)
display.show()

# -----------------------
# Draw pixels (corners)
# -----------------------
display.pixel(0, 0, 1)
display.pixel(7, 0, 1)
display.pixel(0, 7, 1)
display.pixel(7, 7, 1)
display.show()

Drawing Function Arguments

pixel(x, y, color)

display.pixel(0, 0, 1)
  • x -> horizontal position from 0 to 7
  • y -> vertical position from 0 to 7
  • color -> 1 = on, 0 = off

hline(x, y, length, color)

display.hline(0, 3, 8, 1)
  • x, y -> starting point
  • length -> how long the line is
  • color -> 1 or 0

This draws a horizontal line.

vline(x, y, length, color)

display.vline(3, 0, 8, 1)

This draws a vertical line.

line(x1, y1, x2, y2, color)

display.line(0, 0, 7, 7, 1)
  • (x1, y1) -> start point
  • (x2, y2) -> end point

This lets you draw a line in any direction.

rect(x, y, width, height, color)

display.rect(1, 1, 6, 6, 1)
  • x, y -> top-left corner
  • width, height -> size

This draws an outline rectangle.

fill(0 or 1)

display.fill(0)
  • 0 -> clear the screen
  • 1 -> fill the whole screen

Example 3 - 4 x 8x8 Chain

If you connect four modules together, you get a 32x8 display. That is much better for short words, counters, and mini banners.

from machine import Pin, SPI
from max7219 import Matrix8x8

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(4)
display.brightness(5)
display.fill(0)
display.text('PICO', 0, 0, 1)
display.show()

Example 4 - 4 x 8x8 Short Scroll

This is a good example if you want one moving message across four chained modules.

from machine import Pin, SPI
from max7219 import Matrix8x8

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(4)
display.scroll(" PICO ", 0.08, loop=True)

Example 5 - 8 x 8x8 Long Scrolling Text

With eight modules chained together, you get a 64x8 display, which is much better for long text. This is probably the best showcase example for the tutorial.

from machine import Pin, SPI
from max7219 import Matrix8x8

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(8)
display.brightness(4)
display.scroll(" NERDCAVE - RASPBERRY PI PICO - MICROPYTHON ", 0.05, loop=True)

Example 6 - Static Text Across 8 Modules

If you do not want scrolling yet, you can still use eight modules like one wide banner.

from machine import Pin, SPI
from max7219 import Matrix8x8

def create_display(modules=1, din=3, cs_pin=5, clk=2):
spi = SPI(
0,
baudrate=10000000,
polarity=1,
phase=0,
sck=Pin(clk),
mosi=Pin(din),
)
cs = Pin(cs_pin, Pin.OUT)
return Matrix8x8(spi, cs, modules)

display = create_display(8)
display.brightness(5)
display.fill(0)
display.text('MAX7219', 0, 0, 1)
display.show()

Brightness Control

You can change the display brightness:

display.brightness(1)

or

display.brightness(10)

Lower values are often easier to read indoors and use less power.

Built-In Animation Helpers

If you use the updated library above, you also get a small animation toolkit built into Matrix8x8.

1. Scroll Text

Scroll text smoothly from right to left across the display.

display.scroll("HELLO", 0.05)

Arguments:

  • message -> text to display
  • speed -> delay between frames, where smaller values scroll faster

Good for messages, names, or notifications.

2. Reverse Scroll

Scroll text from left to right.

display.scroll_reverse("WORLD", 0.05)

This is useful when you want a different motion direction or want to alternate animations.

Make text flash on and off.

display.blink("HI", times=6, speed=0.2)

Arguments:

  • times -> number of blinks
  • speed -> on/off delay

This works well for alerts or attention-grabbing messages.

4. Fade In / Fade Out

Use MAX7219 brightness control for a smoother effect.

display.fade_in("A")
display.fade_out("A")

This is a simple way to add visual polish without changing the wiring or setup.

5. Bounce Text

Move short text across the display and back again.

display.bounce("OK", 0.05)

This works best with short words or status indicators.

6. Wipe Effect

Fill and clear the screen column by column.

display.wipe()

This makes a good transition between other animations.

Common Problems

Nothing lights up

  • check that the module is powered correctly
  • confirm DIN, CLK, and CS are connected to the expected Pico pins
  • verify SPI pin assignments in the code

Display is mirrored or text seems backwards

Some MAX7219 modules are chained in different physical directions. You may need to adjust your text position or use a different module orientation.

Random flickering

  • lower the SPI speed if needed
  • check jumper wires
  • make sure the power supply is stable

Summary

The MAX7219 dot matrix display is one of the easiest ways to add visual output to a Raspberry Pi Pico project. With MicroPython and a ready-made driver, you can quickly display characters, draw patterns, and build scrolling text effects.

The key ideas are:

  • use SPI
  • use a MAX7219 driver such as Matrix8x8
  • call show() after drawing
  • use 1, 4, or 8 modules depending on how much text you want to display