MAX7219 tube clock/The clock/11. A clock in a tube
The clock · 11 of 12

A clock in a tube

An ESP32, a time server and eight digits. The interesting problem is not the network — it is that the colons are soldered at digits 1 and 5, so HH:MM:SS in one run is a layout this display physically cannot draw.

Where the seconds go

Where the seconds go
08:30:57
Layout
The colon
Digits used
6 of 8
Colons available
1 and 5
Writes a second
2
HH:MM:SS in one run is the layout this display cannot draw. The colons are soldered where they are — digit 1 and digit 5 — and a separator between the minutes and the seconds would need one at digit 3. So the seconds go on the far right with the two middle digits blanked, which reads cleanly and has the pleasant side effect that the gap lines up with the seam between the panels. Blanking a digit is code B 0x0F, not a zero. Either way the sketch only rewrites the digits that changed — the other six are still sitting in the chip’s RAM being scanned, and rewriting them costs nothing but does nothing.

Press Let it run. Then switch the layout and notice what is not on offer.

HH:MM:SS with a separator in both places is not something this display can draw. The colons are LEDs soldered into the middle of each four-digit panel, so they sit at digits 1 and 5, and a separator between the minutes and the seconds would need one at digit 3. There is no LED there and no register that could light it.

What the geometry does give you is two NN:NN fields, which is a genuinely useful shape:

  • hours and minutes on the left, seconds pushed to the right-hand edge;
  • hours and minutes on the left, day and month on the right;
  • two time zones;
  • a time and a temperature.

The sketch below takes the first. The two middle digits are blanked, which lines the gap up with the seam between the panels and looks deliberate rather than like a display with a fault.

Blanking is a character

Worth its own sentence because it is the one non-obvious call in the sketch. There is no "turn this digit off" function in LedControl. What there is, is code B character 0x0F, which the decoder draws as a blank:

lc.setDigit(0, 4, 0x0F, false);   // digit 4 shows nothing

lc.clearDisplay(0) blanks all eight, which is the wrong tool inside a loop that runs every second — it writes eight registers to undo work you are about to redo.

Only what changed

The chip is holding all eight bytes and scanning them itself, so a digit you do not rewrite keeps showing what it was showing. That means the loop only has to write what moved:

  • the seconds change every second — two digits, sometimes one;
  • the colon blinks — one digit, and it is a digit that is already being written;
  • the minutes change once a minute;
  • the hours, once an hour.

The sketch here rewrites eight digits every second anyway, because eight register writes is sixteen bytes and costs nothing measurable. It is worth knowing that you could write two, though — it is the difference between a clock and something that has to keep talking to stay lit.

Setting itself

configTzTime does two jobs in one call: it starts an NTP client and it installs a POSIX timezone rule. The rule is the part worth getting right.

const char* TZ = "AEST-10AEDT,M10.1.0,M4.1.0/3";

That string says "standard time is UTC+10, daylight time starts on the first Sunday in October and ends on the first Sunday in April at 3 am". With it, the clock in the tube changes itself twice a year and you never touch the sketch. An offset in hours does not, and is wrong for half the year.

After that, getLocalTime fills a struct tm from the ESP32's own system clock, which keeps running from the crystal whether or not the network is there. The board resyncs about once an hour in the background.

The code

tube_clock.ino

A clock that sets itself over Wi-Fi and never drifts: HH:MM on the left panel with a colon that blinks once a second, and the seconds on the right-hand edge. Written for an ESP32; the display half is identical on any board.

// A clock in a glass tube: eight 0.28-inch digits on a MAX7219, set from NTP.
//
// Wiring, driver board to ESP32:
//   GND -> GND
//   VCC -> 5V or VIN     (the chip needs 4.0 V minimum; NOT the 3V3 pin)
//   DIN -> GPIO 23
//   CLK -> GPIO 18
//   CS  -> GPIO 5
//
// The ESP32's 3.3 V outputs are below the MAX7219's guaranteed 3.5 V input
// threshold. It works on most parts; for anything permanent put a level
// converter in DIN, CLK and CS.
//
// Arduino IDE: board "ESP32 Dev Module", and the "LedControl" library by
// Eberhard Fahle from Sketch > Include Library > Manage Libraries.

#include <WiFi.h>
#include <time.h>
#include <LedControl.h>

const char* SSID = "your-network";
const char* PASS = "your-password";

// POSIX timezone string. This one is Melbourne / Sydney, DST rule included.
const char* TZ = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const int DIN_PIN = 23;
const int CLK_PIN = 18;
const int CS_PIN  = 5;

LedControl lc = LedControl(DIN_PIN, CLK_PIN, CS_PIN, 1);

// Code B sends 0x0F as "blank". The two middle digits are never used.
const byte BLANK = 0x0F;

void setup() {
  Serial.begin(115200);

  lc.shutdown(0, false);       // the chip powers up blanked
  lc.setScanLimit(0, 7);       // all eight digits
  lc.setIntensity(0, 6);
  lc.clearDisplay(0);

  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    Serial.print(".");
  }
  Serial.println(WiFi.localIP());

  // Fetch the time and apply the timezone rule in one call.
  configTzTime(TZ, "pool.ntp.org", "time.nist.gov");

  struct tm t;
  while (!getLocalTime(&t, 500)) Serial.println("waiting for NTP");
}

void loop() {
  struct tm t;
  if (!getLocalTime(&t)) return;

  static int lastSecond = -1;
  if (t.tm_sec == lastSecond) return;   // only redraw when it changes
  lastSecond = t.tm_sec;

  bool colon = (t.tm_sec % 2) == 0;     // blink once a second

  // Digits 0-3: HH:MM. The colon is the point of digit 1.
  lc.setDigit(0, 0, t.tm_hour / 10, false);
  lc.setDigit(0, 1, t.tm_hour % 10, colon);
  lc.setDigit(0, 2, t.tm_min  / 10, false);
  lc.setDigit(0, 3, t.tm_min  % 10, false);

  // Digits 4-5 blank, seconds on the right-hand edge. There is no colon
  // between the minutes and the seconds because digit 3 has no LED there.
  lc.setDigit(0, 4, BLANK, false);
  lc.setDigit(0, 5, BLANK, false);
  lc.setDigit(0, 6, t.tm_sec / 10, false);
  lc.setDigit(0, 7, t.tm_sec % 10, false);
}

configTzTime takes a POSIX timezone string, and the one here is Australian eastern time with its daylight-saving rule built in — so the clock changes itself in October and April with nothing to update. Look yours up rather than adding an offset by hand; an offset goes wrong twice a year.

When it does not work

It shows 00:00 and never changes

NTP has not answered. The board joined Wi-Fi — otherwise setup would still be printing dots — but nothing came back from the time server, which is usually a network that blocks outbound UDP on port 123 or a captive portal. getLocalTime returning false is how you see it; the sketch prints that.

The time is right but the hour is wrong

The timezone string. configTzTime applies a POSIX TZ rule, not an offset, and the one in the sketch is Australian eastern. Look yours up rather than adding hours by hand: a plain offset is right for half the year and an hour out for the other half.

The colon blinks but the digits do not update

Both come from the same getLocalTime call, so if one is moving the other should be. Check you have not left a delay in loop that is longer than a second — the second-change test only fires when loop actually runs.

Can I put the date on the right instead?

Yes, and it is the better use of that half: the right panel has its own colon at digit 5, so DD:MM reads properly there where the seconds do not. t.tm_mday and t.tm_mon + 1 are the two numbers, and tm_mon counts from zero.

It loses time when the Wi-Fi drops

It does not. configTzTime sets the ESP32's own system clock and the chip keeps counting from its crystal, so a dropped network costs accuracy over days, not the time itself. It resyncs on its own about once an hour.

Can I do this without Wi-Fi?

Yes, with a real-time clock module on I2C, and the display half of the sketch does not change. A bare ESP32 with no RTC and no network has no idea what time it is at power-up, which is the one thing a clock cannot work around.

Where this goes next

Six symptoms, and the single register write that tells you which half of the problem you are in.

When a digit goes missing

Edit this page — content/books/max7219-tube-clock/a-clock-in-a-tube.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

MAX7219 Vintage Tube LED Clock Kit

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum