Round watch face on an Arduino Uno
An analog watch face on a GC9A01 round display: hour, minute and second hands from a DS3231 clock, redrawn each second. The whole build, an Arduino Uno and 2 more parts, 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 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $38. On Mokxi they're free.
What you'll learn
- Reading a DS3231 clock over I2C
- Converting BCD to normal numbers
- Finding hand tips with a sine table
- Erasing by redrawing in the background color
- Driving a round SPI display
- Difficulty: Intermediate
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 4
How it works
A DS3231 real-time clock keeps the time and a round GC9A01 display shows it as an analog watch face. The Uno reads seconds, minutes and hours from the clock's first three registers over I2C on A4 and A5. The display uses SPI: SCL on 13, SDA on 11, CS on 10 and DC on 9.
The sketch draws 60 tick marks once. Each new second, it erases the old hands by drawing them in black and draws the new hands. The hour and minute hands are white and the second hand is red. A table of sines replaces floating point math.
Parts list
4 parts, plus the jumper wires. They retail for about $38; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Round display, GC9A01In Desk Buddy Kit.
- 1 × Real-time clock, DS3231In Christmas Lights Kit.
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 pin | Connects to |
|---|---|
| Round display, GC9A01 pin SCL | Arduino Uno R3 pin 13 |
| Round display, GC9A01 pin SDA | Arduino Uno R3 pin 11 |
| Round display, GC9A01 pin CS | Arduino Uno R3 pin 10 |
| Round display, GC9A01 pin DC | Arduino Uno R3 pin 9 |
| Round display, GC9A01 pin VCC | Arduino Uno R3 pin 5V |
| Real-time clock, DS3231 pin VCC | Arduino Uno R3 pin 5V |
| Round display, GC9A01 pin GND | Arduino Uno R3 pin GND |
| Real-time clock, DS3231 pin GND | Arduino Uno R3 pin GND |
| Real-time clock, DS3231 pin SDA | Arduino Uno R3 pin A4 |
| Real-time clock, DS3231 pin SCL | Arduino Uno R3 pin A5 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Round watch face: an analog clock on a 1.28 inch GC9A01 round display,
// with the time from a DS3231, the way a desk watch or a smartwatch face is
// built.
//
// GC9A01 SCL -> 13 SDA -> 11 CS -> 10 DC -> 9 VCC -> 5 V GND -> GND (RES left free)
// DS3231 SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
//
// The display is a 240x240 square of memory with round glass on it, so the
// face is centered at (120, 120) with a radius of 120. Each second the sketch
// rubs out the hands where they were, by drawing them again in black, and
// draws them where they are now: redrawing the whole face would take this
// board a long time over its SPI bus.
//
// The hands are straight lines from the center, and their ends come from a
// table of sines in steps of 6 degrees (one minute of the dial), because this
// board has no floating point. The time is the DS3231's own registers, read
// over Wire: three bytes of BCD from register 0.
#include <SPI.h>
#include <Wire.h>
#include "Adafruit_GFX.h"
#include "Adafruit_GC9A01A.h"
Adafruit_GC9A01A tft(10, 9); // CS, DC; no reset pin
const uint8_t DS3231 = 0x68;
uint8_t fromBcd(uint8_t b) { return (b >> 4) * 10 + (b & 0x0f); }
// Seconds, minutes and hours: the DS3231's first three registers, in BCD.
// False when the clock does not answer.
uint8_t now[3];
bool readClock() {
Wire.beginTransmission(DS3231);
Wire.write(0x00);
Wire.endTransmission();
if (Wire.requestFrom(DS3231, 3) != 3) return false;
for (uint8_t i = 0; i < 3; i++) now[i] = fromBcd(Wire.read() & 0x7f);
return true;
}
const int16_t CX = 120;
const int16_t CY = 120;
const uint16_t FACE = GC9A01A_BLACK;
// sin(6 degrees x i) x 127 for i = 0 to 15, a quarter of the dial.
const int8_t SINE[16] = {0, 13, 26, 39, 52, 64, 75, 85, 94, 103, 110, 116, 121, 124, 126, 127};
int16_t sine60(uint8_t i) {
i %= 60;
if (i <= 15) return SINE[i];
if (i <= 30) return SINE[30 - i];
if (i <= 45) return -SINE[i - 30];
return -SINE[60 - i];
}
// The point `radius` pixels from the center at dial position `i` (0 is 12 o'clock).
int16_t dialX(uint8_t i, int16_t radius) { return CX + radius * sine60(i) / 127; }
int16_t dialY(uint8_t i, int16_t radius) { return CY - radius * sine60(i + 15) / 127; }
void hand(uint8_t i, int16_t radius, uint16_t color) {
tft.drawLine(CX, CY, dialX(i, radius), dialY(i, radius), color);
}
void drawFace() {
tft.fillScreen(FACE);
// A tick a minute, longer every five.
for (uint8_t i = 0; i < 60; i++) {
int16_t inner = (i % 5 == 0) ? 100 : 108;
tft.drawLine(dialX(i, inner), dialY(i, inner), dialX(i, 112), dialY(i, 112), GC9A01A_WHITE);
}
}
uint8_t lastSecond = 255;
uint8_t hourPos, minutePos, secondPos;
void setup() {
tft.begin();
drawFace();
Wire.begin();
}
void loop() {
if (!readClock() || now[0] == lastSecond) {
delay(20);
return;
}
bool first = lastSecond == 255;
lastSecond = now[0];
// Rub out the old hands, then draw the new ones, longest last.
if (!first) {
hand(secondPos, 95, FACE);
hand(minutePos, 85, FACE);
hand(hourPos, 55, FACE);
}
hourPos = (now[2] % 12) * 5 + now[1] / 12;
minutePos = now[1];
secondPos = now[0];
hand(hourPos, 55, GC9A01A_WHITE);
hand(minutePos, 85, GC9A01A_WHITE);
hand(secondPos, 95, GC9A01A_RED);
}
Code walkthrough
The key functions of the sketch, and what each one does.
readClock sketch.ino
Asks the DS3231 for three bytes starting at register 0, then converts each one from BCD. It returns false if the clock does not answer, and the loop just tries again.
bool readClock() {
Wire.beginTransmission(DS3231);
Wire.write(0x00);
Wire.endTransmission();
if (Wire.requestFrom(DS3231, 3) != 3) return false;
for (uint8_t i = 0; i < 3; i++) now[i] = fromBcd(Wire.read() & 0x7f);
return true;
}fromBcd sketch.ino
Converts a BCD byte to a normal number. The upper four bits are the tens and the lower four are the ones. For example, 0x42 becomes 42.
uint8_t fromBcd(uint8_t b) { return (b >> 4) * 10 + (b & 0x0f); }hand sketch.ino
Draws a line from the center at CX and CY to a point on the dial. The point comes from dialX and dialY, and the color can be the face color to erase.
void hand(uint8_t i, int16_t radius, uint16_t color) {
tft.drawLine(CX, CY, dialX(i, radius), dialY(i, radius), color);
}loop sketch.ino
Does nothing until the second changes. Then it erases the old hands, works out the new positions, and draws the hour, minute and second hands, longest last.
void loop() {
if (!readClock() || now[0] == lastSecond) {
delay(20);
return;
}
bool first = lastSecond == 255;
lastSecond = now[0];
// Rub out the old hands, then draw the new ones, longest last.
if (!first) {
hand(secondPos, 95, FACE);
hand(minutePos, 85, FACE);
hand(hourPos, 55, FACE);
}
hourPos = (now[2] % 12) * 5 + now[1] / 12;
minutePos = now[1];
secondPos = now[0];
hand(hourPos, 55, GC9A01A_WHITE);
hand(minutePos, 85, GC9A01A_WHITE);
hand(secondPos, 95, GC9A01A_RED);
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The hands do not move. This project
Check the DS3231's SDA on A4 and SCL on A5, plus power. The loop waits for a good read. - The display stays blank. This project
Check CS on pin 10, DC on pin 9, and SDA and SCL on pins 11 and 13. - Hands leave trails. This project
Erase them with the same radius used to draw them, as the loop does. - The display stays blank or shows garbage. Round display
Check SCK, MOSI, CS, DC and RST wiring, and use 3.3 V logic. Pick a library that supports the GC9A01 and set its pins to match. Reset the display on start. - Colors look swapped or the image is shifted. Round display
Try the library's RGB or BGR color order option and inversion option. The visible area is a circle, so keep important content near the center. - The time shows odd numbers, such as 0x23 printed as 35, when you read it. Real-time clock
All time registers are BCD, not plain binary. Convert each byte with (b >> 4) * 10 + (b & 0x0F) to read, and the reverse to write. A library such as RTClib does this for you. - The clock does not respond, or the I2C bus hangs. Real-time clock
The DS3231 address is 0x68. Wire SDA and SCL to the right pins with pull-ups, about 4.7k, to the supply. Many breakout boards already have them. Join the grounds. - Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.