Making an ESP8266-based LED clock

Index, feed.

[ Date | 2020-02-23 15:43 -0500 ]
[ Mod. | 2020-12-20 19:57 -0500 ]
32x16 LED display, ESP8266-MAX7129-LED-CLOCK Wemos clone

32x16 LED display, ESP8266-MAX7129-LED-CLOCK Wemos clone

This is more or less a follow-up to "Tinkering with a LED matrix: first steps."


Compared to last time's project that was based on Arduino Uno and a Chip single-board computer, I wanted to improve a few things:


The former is a microcontroller with both UDP-capable Wi-Fi and GPIO pins. It is programmable using the Arduino IDE, through it USB port, which also provides a serial console when the sketch runs; a useful debugging feature. It can be powered from USB (5V), and its I/O voltage is 3.3V.

The latter uses a simple serial protocol to let any device capable of digital output control an array of four square 64-LED displays (daisy-chaining allows using fewer or more modules, using one chip for each). It operates at 5V.

Together, these can form a display that can be controlled remotely through UDP over WiFi. While the logic levels are theoretically incompatible and should require a level shifter, as the ESP outputs 3.3V for logic high and the MAX expects at least 3.5V, the combination appears to work well in practice, perhaps because the actual implementations are more tolerant than the datasheets guarantee.


Wires connected to the ESP8266 module

Wires connected to the ESP8266 module

Wires connected to the MAX7219-based display

Wires connected to the MAX7219-based display

Both modules are highly-integrated, and therefore very few connections are required. The connections are as follows:

ESP pin (GPIO#) MAX display pin Wire color on prototype
5V VCC purple
G GND blue
D4 (2) DIN green
D2 (4) CS yellow
D1 (5) CLK orange

Note that my prototype uses relatively long wires between the two modules: for a more compact result, much shorter wires could be used, or even near-direct soldering of pins.

The prototype also uses non-contiguous pins D1, D2, and D4, perhaps because some documentation somewhere convinced me not to use D3 on my first attempt. I'm 80% certain that there is no good reason to avoid that pin and I should test that theory someday.


The high-level view is that the ESP module exposes a UDP server, listening for a simple home-brewed language of instructions on port 4444, and translates them to MAX7219 instructions, so that their effects can be seen on the LED displays:

Code walk-through

Arduino sketch source code: esp8266-max7219-relay.ino.

The preamble defines constants for easy reference to pins and dimensions, sets up an object of type WiFiUDP, a static buffer for incoming UDP packets, and two buffers representing the current and upcoming states of the display. A static font array is defined, holding a byte for each supported character's line (in the prototype, only digits 0-9, colon and space are supported).

The standard Arduino setup function initializes output pins, initializes the display, starts the Wi-Fi connection (blinking some of the display while connection is being established), and then starts the UDP server.

void setup() {

    pinMode(outData,   OUTPUT); // MAX7219 DIN
    pinMode(outClock,  OUTPUT); // MAX7219 CLK
    pinMode(outCommit, OUTPUT); // MAX7219 LOAD / ¬CS

    pinMode(outLed,    OUTPUT);


    WiFi.begin(ssid, pass);
    int i = 0;
    while (WiFi.status() != WL_CONNECTED) {
        Serial.println("Not connected.");
        // Toggle first square.
        for (int l = 0; l < LINES; ++l) {
            scratch[i + l * DISPLAYS] ^= 0xff;


The main loop listens for incoming commands and dispatches them to onReceive, which is recycled from the previous project, as is all of the MAX7219-specific code. (That code is a straightforward implementation of the language described in the chip's spec sheet.)

void loop() {
    if (Udp.parsePacket() == 0) return;
    int count =, sizeof buf);
    Serial.printf("Received %d bytes; [0] = 0x%02x.\r\n",
                  count, buf[0]);
static void onReceive(int bytes) {
    if (bytes < 1) return;

    byte instr = buf[0];

    switch (instr) {
    case 0x01: display_init(); break;     // Init
    case 0x02: memset(scratch, 0, BYTES); // Clear
    case 0x03: refresh_display(); break;  // Commit

    case 0x80: {                // Write (text)
        if (bytes < CHARACTERS) return;

        memset(scratch, 0, BYTES);
        for (int i = 0; i < CHARACTERS; ++i) {
            byte *c = fontAt(buf[i + 1]);
            compositeDigit(c, i);


    case 0x81: {                // Set font
        if (bytes < LINES + 1) return;
        byte c = buf[1];             // Character to write
        byte *fontPoint = fontAt(c); // Never fails

        for (int i = 0; i < LINES; ++i) {
            fontPoint[i] = buf[i + 2];

Simple code to push the current time to the ESP/MAX display

This is as simple as writing the time in HH:MM:SS format, preceded by the 0x80 instruction byte, through UDP to port 4444 on the correct host, several times a second for freshness. Assuming your ESP8266's hostname is esp0, the following Bash code will do:

while true; do
  date $'+\x80%T' >/dev/udp/esp0/4444
  sleep 0.1

%T is a Posix date equivalent for %H:%M:%S. Users of GNU date may prefer %_H:%M:%S, which prints the hour space-padded instead of zero-padded.

Future work

Possible improvements:


Quick links:

Camp info 2007
Camp Faécum 2007
Japanese adjectives
Couleurs LTP
French English words
Petites arnaques
DSC-W17 patch
Scarab: dictionnaire de Scrabble
Omelette soufflée au sirop d'érable
Camembert fondu au sirop d'érable
La Mona de Tata Zineb
Cake aux bananes, au beurre de cacahuètes et aux pépites de chocolat