Built-in project · ESP32-C3-DevKitM-1

Binary clock on an ESP32-C3

An ESP32-C3 writes the hours, minutes and seconds as three rows of binary lamps on a 128x64 OLED. The whole build, an ESP32-C3 and 1 more part, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $17. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Reading binary numbers as lamps
  • Bit shifting and masking with >> and &
  • Drawing shapes on an SSD1306 OLED
  • Keeping time from millis
  • Redrawing only when something changes
  • Difficulty: Intermediate
  • Time: About 20 minutes
  • Board: ESP32-C3-DevKitM-1
  • Parts: 3

How it works

The ESP32-C3 draws three rows of circles on a 128x64 OLED. The top row is hours in 5 bits, the middle is minutes in 6 bits and the bottom is seconds in 6 bits. A filled circle is a 1 and an empty ring is a 0, with the biggest bit on the left.

There is no clock chip. The sketch counts seconds with millis from a starting time of 9:41:00 and redraws only when the second changes. It also prints the time as plain digits at the top right so you can check your reading.

Parts list

3 parts, plus the jumper wires. They retail for about $17; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
OLED display, 128x64 pin VCCESP32-C3-DevKitM-1 pin 3V3
OLED display, 128x64 pin GNDESP32-C3-DevKitM-1 pin GND
OLED display, 128x64 pin SDAESP32-C3-DevKitM-1 pin 4
OLED display, 128x64 pin SCLESP32-C3-DevKitM-1 pin 5

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · ESP32-C3-DevKitM-1
// Binary clock: the time in ones and zeros on a 128x64 OLED.
//
// Wiring, module to board:
//   SCL -> GPIO 5      SDA -> GPIO 4      VCC -> 3V3      GND -> GND
//
// Three rows of lamps: five bits of hours, six of minutes, six of seconds,
// most significant on the left. A filled circle is a one. Read a row as a
// binary number and that is the number under it, which the sketch also prints
// in decimal so the reading can be checked.
//
// The time comes from millis() and nothing else. There is no clock chip here,
// so the board starts at the time set below and counts from there; a real one
// would read a DS3231 over the same two wires this display is on.
//
// A whole second is a long time for a board, so the loop sleeps in 50 ms steps
// and only repaints when the second changes. The driver sends the
// eight-column blocks that changed rather than the whole kilobyte, so a tick
// that moves three lamps costs a few dozen bytes on the bus.

#include "mokxi_i2c.h"
#include "mokxi_ssd1306.h"

const uint8_t SCL_PIN = 5;
const uint8_t SDA_PIN = 4;

SoftI2c bus(SCL_PIN, SDA_PIN, 400);
Ssd1306 oled(bus, SSD1306_ADDRESS);

// Where the clock starts, since nothing here keeps time across a reset.
const uint32_t START_HOUR = 9;
const uint32_t START_MINUTE = 41;
const uint32_t START_SECOND = 0;

const int16_t ROW_Y[3] = {22, 38, 54};
const uint8_t ROW_BITS[3] = {5, 6, 6};
const char *const ROW_NAME[3] = {"H", "M", "S"};

const int16_t DOT_R = 5;
const int16_t DOT_STEP = 16;
const int16_t DOT_RIGHT = 118;

uint32_t shown = 0xffffffffu;

static void furniture() {
  oled.clear();
  oled.text(0, 2, "MOKXI", 1);
  oled.hLine(0, 12, 128);
  for (int r = 0; r < 3; r++) {
    oled.text(0, (int16_t)(ROW_Y[r] - 3), ROW_NAME[r], 1);
  }
}

// One row of lamps, most significant bit on the left.
static void row(int r, uint32_t value) {
  uint8_t bits = ROW_BITS[r];
  for (uint8_t i = 0; i < bits; i++) {
    int16_t cx = (int16_t)(DOT_RIGHT - i * DOT_STEP);
    bool on = ((value >> i) & 1u) != 0u;
    oled.fillCircle(cx, ROW_Y[r], DOT_R, false);
    if (on) {
      oled.fillCircle(cx, ROW_Y[r], DOT_R, true);
    } else {
      oled.circle(cx, ROW_Y[r], DOT_R, true);
    }
  }
}

static void twoDigits(int16_t x, int16_t y, uint32_t value) {
  oled.number(x, y, (long)(value / 10u), 1, true, true);
  oled.number((int16_t)(x + 6), y, (long)(value % 10u), 1, true, true);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi ESP32-C3: binary clock on an SSD1306");
  if (!oled.begin()) {
    Serial.println("no ACK from the display: check SCL, SDA and the supply");
    return;
  }
  furniture();
  oled.display();
}

void loop() {
  uint32_t elapsed = millis() / 1000u;
  uint32_t total =
      (START_HOUR * 3600u + START_MINUTE * 60u + START_SECOND + elapsed) % 86400u;
  if (total == shown) {
    delay(50);
    return;
  }
  shown = total;

  uint32_t hours = total / 3600u;
  uint32_t minutes = (total / 60u) % 60u;
  uint32_t seconds = total % 60u;

  row(0, hours);
  row(1, minutes);
  row(2, seconds);

  twoDigits(72, 2, hours);
  oled.text(84, 2, ":", 1, true, true);
  twoDigits(90, 2, minutes);
  oled.text(102, 2, ":", 1, true, true);
  twoDigits(108, 2, seconds);
  oled.display();

  if (seconds == 0u) {
    Serial.print("tick ");
    Serial.print(hours);
    Serial.print(":");
    Serial.println(minutes);
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

furniture sketch.ino

Draws the parts that never change: the MOKXI title, a line under it and the H, M and S labels. It runs once, so the loop only has to repaint the lamps and digits.

static void furniture() {
  oled.clear();
  oled.text(0, 2, "MOKXI", 1);
  oled.hLine(0, 12, 128);
  for (int r = 0; r < 3; r++) {
    oled.text(0, (int16_t)(ROW_Y[r] - 3), ROW_NAME[r], 1);
  }
}

row sketch.ino

Draws one row of lamps. For each bit it shifts the value right, checks the lowest bit, and draws a filled circle for 1 or an outline for 0. Bit 0 sits on the far right.

// One row of lamps, most significant bit on the left.
static void row(int r, uint32_t value) {
  uint8_t bits = ROW_BITS[r];
  for (uint8_t i = 0; i < bits; i++) {
    int16_t cx = (int16_t)(DOT_RIGHT - i * DOT_STEP);
    bool on = ((value >> i) & 1u) != 0u;
    oled.fillCircle(cx, ROW_Y[r], DOT_R, false);
    if (on) {
      oled.fillCircle(cx, ROW_Y[r], DOT_R, true);
    } else {
      oled.circle(cx, ROW_Y[r], DOT_R, true);
    }
  }
}

loop sketch.ino

Turns elapsed seconds into hours, minutes and seconds, wrapping at 24 hours. If the second has not changed it waits 50 ms and tries again. Otherwise it redraws the three rows and the digits, then updates the display.

void loop() {
  uint32_t elapsed = millis() / 1000u;
  uint32_t total =
      (START_HOUR * 3600u + START_MINUTE * 60u + START_SECOND + elapsed) % 86400u;
  if (total == shown) {
    delay(50);
    return;
  }
  shown = total;

  uint32_t hours = total / 3600u;
  uint32_t minutes = (total / 60u) % 60u;
  uint32_t seconds = total % 60u;

  row(0, hours);
  row(1, minutes);
  row(2, seconds);

  twoDigits(72, 2, hours);
  oled.text(84, 2, ":", 1, true, true);
  twoDigits(90, 2, minutes);
  oled.text(102, 2, ":", 1, true, true);
  twoDigits(108, 2, seconds);
  oled.display();

  if (seconds == 0u) {
    Serial.print("tick ");
    Serial.print(hours);
  // ... 4 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The screen stays blank This project
    Check that SDA is on pin 4 and SCL is on pin 5, and that VCC is on 3V3. The Serial Monitor prints a message if the display does not answer.
  • The time is not what I expect This project
    The clock starts at the values in START_HOUR, START_MINUTE and START_SECOND every time it resets. Change them to set a different start.
  • The OLED stays blank. OLED display
    Check SDA and SCL are not swapped, and power it with 3.3 V or 5 V as its board allows. Most use address 0x3C, some 0x3D. Call display.begin() with the matching address.
  • Text never shows up even though the code runs. OLED display
    Drawing only writes to a buffer. Call display.display() after drawing to push it out. Also set a text color with setTextColor(WHITE) and clear the buffer between frames.
  • Upload fails or the board is not found by the computer. ESP32-C3-DevKitM-1
    Pick the ESP32-C3 board in your tools and the right port. If it still fails, hold BOOT, tap RESET, then release BOOT to enter download mode and try again.
  • GPIO pins act strangely or the board resets. ESP32-C3-DevKitM-1
    GPIO is 3.3 V only, so never feed it 5 V. GPIO 18 and 19 are USB, and GPIO 2, 8 and 9 are boot strapping pins. Avoid loading them at reset.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.