#include <I2C_RTC.h>
/*
Lee's Clock: Hopefully the final revision.
In order to use the buttons:
Hold 'set' to adjust the 'minutes' and 'brightness'.
Press 'set' to change between twelve and twenty four hour time.
As of right now the 'hours' button doesn't work so i forced the minutes to cycle to the next hour.
If you hold any of the first three buttons; it will begin to add faster.
Set the brightness first and then minutes because holding 'set' pauses the clock.
Make the minutes one higher than the current time and let go of the 'set' button when the time on the standard you are using hits zero seconds.
I advise against putting any battery on the RTC module as it will consistently overcharge
*/
#include <Wire.h>
#include <RTClib.h>
DS3231 rtc;
// Shift Register Hardware Pin Definitions
const int reg_clock = 13; // SRCLK
const int reg_latch = 10; // RCLK
const int reg_data = 11; // SER (Data In)
const int reg_OE = 9; // OE (Output Enable - Hardware PWM Control)
// Exact physical pinout matching your custom PCB panel traces
const int btn_bright = 2; // J6 Pin 5 -> Arduino Pin 2
const int btn_min = 3; // J6 Pin 4 -> Arduino Pin 3 (Minutes Button with Hour Rollover)
const int btn_set = 5; // J6 Pin 2 -> Arduino Pin 5 (Set Button)
// Common Anode segment patterns (1 = Segment ON / Sink to GND)
const byte digitPatterns[] = {
0b01111110, // 0
0b00110000, // 1
0b01101101, // 2
0b01111001, // 3
0b00110011, // 4
0b01011011, // 5
0b01011111, // 6
0b01110000, // 7
0b01111111, // 8
0b01111011 // 9
};
// Global Time variables
int hours = 12, minutes = 14, seconds = 0; // Pre-set to 12:14:00 for easy testing
bool is24Hour = false;
// Chime Flags
bool chimeDoneThisMinute = false;
int lastCheckedMinute = -1;
// Asynchronous Chime Engine State Variables (Zero Delays)
bool introMelodyActive = false;
unsigned long chimeStateTimer = 0;
int strikeCount = 0;
int totalStrikesNeeded = 0;
// Your exact custom active-low brightness PWM levels
const int brightnessPWM[] = {255, 192, 128, 64, 0};
int currentBrightStage = 2;
// Non-blocking button and chime tracking state counters
int setHoldCounter = 0;
int minHoldCounter = 0;
int rtcRefreshTimer = 0;
bool setModeActive = false;
// Tracking variables to prevent display lag and electrical noise
int lastDispSec = -1, lastDispMin = -1, lastDispHr = -1;
void updateDisplayRouting() {
int displayHours = hours;
bool isPM = (displayHours >= 12);
if (!is24Hour) {
displayHours = displayHours % 12;
if (displayHours == 0) displayHours = 12;
}
int onesSec = seconds % 10;
int tensSec = seconds / 10;
int onesMin = minutes % 10;
int tensMin = minutes / 10;
int onesHr = displayHours % 10;
int tensHr = displayHours / 10;
byte seg0 = digitPatterns[onesSec];
byte seg1 = digitPatterns[tensSec];
byte seg2 = (tensHr == 0) ? 0b00000000 : digitPatterns[tensHr];
byte seg3 = digitPatterns[onesHr];
byte seg4 = digitPatterns[tensMin];
byte seg5 = digitPatterns[onesMin];
if (setModeActive) {
seg2 |= 0b10000000;
seg3 |= 0b10000000;
}
if (!is24Hour && isPM) {
seg5 |= 0b10000000;
}
digitalWrite(reg_latch, LOW);
shiftOut(reg_data, reg_clock, MSBFIRST, seg5);
shiftOut(reg_data, reg_clock, MSBFIRST, seg4);
shiftOut(reg_data, reg_clock, MSBFIRST, seg3);
shiftOut(reg_data, reg_clock, MSBFIRST, seg2);
shiftOut(reg_data, reg_clock, MSBFIRST, seg1);
shiftOut(reg_data, reg_clock, MSBFIRST, seg0);
digitalWrite(reg_latch, HIGH);
}
void readRTC() {
hours = now.hour();
minutes = now.minute();
seconds = now.second();
}
// RESTORED: Uses Native Hardware Serial for your pins 0 and 1 connection
void triggerTrack(byte trackNumber) {
Serial.write((uint8_t)'t');
delayMicroseconds(50);
Serial.write((uint8_t)trackNumber);
}
void checkChimesAsynchronous() {
if (minutes != lastCheckedMinute) {
chimeDoneThisMinute = false;
lastCheckedMinute = minutes;
}
if (!chimeDoneThisMinute) {
if (minutes == 15) {
triggerTrack(1);
chimeDoneThisMinute = true;
}
else if (minutes == 30) {
triggerTrack(2);
chimeDoneThisMinute = true;
}
else if (minutes == 45) {
triggerTrack(3);
chimeDoneThisMinute = true;
}
else if (minutes == 0) {
triggerTrack(4); // Start hourly intro melody immediately
introMelodyActive = true;
chimeStateTimer = millis();
totalStrikesNeeded = hours % 12;
if (totalStrikesNeeded == 0) totalStrikesNeeded = 12;
strikeCount = 0;
chimeDoneThisMinute = true;
}
}
if (introMelodyActive) {
unsigned long elapsed = millis() - chimeStateTimer;
// Step 1: Wait out the 26-second intro song
if (strikeCount == 0 && elapsed >= 26000) {
triggerTrack(5); // First hour strike
strikeCount = 1;
chimeStateTimer = millis();
}
// Step 2: Cycle through individual hourly strikes every 3 seconds
else if (strikeCount > 0 && strikeCount < totalStrikesNeeded && elapsed >= 3000) {
triggerTrack(5); // Consecutive hour strike
strikeCount++;
chimeStateTimer = millis();
}
// Step 3: Turn off tracking engine when total count completes
else if (strikeCount >= totalStrikesNeeded) {
introMelodyActive = false;
}
}
}
void handleInputsNoMillis() {
bool setPressed = (digitalRead(btn_set) == LOW);
bool minPressed = (digitalRead(btn_min) == LOW);
bool brightPressed = (digitalRead(btn_bright) == LOW);
// --- SET BUTTON INITIAL PRESS AND MODE TRANSITION ---
if (setPressed) {
if (!setModeActive) {
setHoldCounter++;
if (setHoldCounter > 30) { // ~600ms hold requirement reached
setModeActive = true;
}
}
if (setModeActive) {
// 1. Brightness Toggle
if (brightPressed) {
currentBrightStage = (currentBrightStage + 1) % 5;
analogWrite(reg_OE, brightnessPWM[currentBrightStage]);
delay(250); // Small mechanical debounce
}
// 2. Minutes Step with Automatic Hour Rollover
if (minPressed) {
minHoldCounter++;
// Advance instantly on first press. If held past 25 frames (~500ms), scroll every 6 frames (~120ms)
if (minHoldCounter == 1 || (minHoldCounter > 25 && minHoldCounter % 6 == 0)) {
seconds = 0;
minutes++;
// Rollover minutes into hours smoothly
if (minutes >= 60) {
minutes = 0;
hours++;
if (hours >= 24) hours = 0;
}
updateDisplayRouting();
}
} else {
minHoldCounter = 0; // Reset scroll acceleration instantly on button release
}
}
}
else {
// --- SET BUTTON RELEASE DETECTED ---
if (setHoldCounter > 0) {
if (setModeActive) {
setModeActive = false;
rtc.adjust(DateTime(2026, 8, 13, hours, minutes, 0)); // Save strictly on release
}
else if (setHoldCounter > 1 && setHoldCounter <= 30) {
is24Hour = !is24Hour; // Short tap changes 12/24 mode formatting
}
setHoldCounter = 0;
minHoldCounter = 0;
updateDisplayRouting();
}
}
}
void setup() {
pinMode(reg_clock, OUTPUT); pinMode(reg_latch, OUTPUT); pinMode(reg_data, OUTPUT); pinMode(reg_OE, OUTPUT);
analogWrite(reg_OE, brightnessPWM[currentBrightStage]);
pinMode(btn_bright, INPUT_PULLUP);
pinMode(btn_min, INPUT_PULLUP);
pinMode(btn_set, INPUT_PULLUP);
Wire.begin();
Serial.begin(38400); // Start native Hardware Serial on pins 0/1 at 38400 baud
if (!rtc.begin()) {
while (1);
}
// TESTING FEATURE: Forces the RTC memory directly to 12:14:00 on every boot/reset
// so you can instantly verify the 12:15:00 chime sound!
rtc.adjust(DateTime(2026, 8, 13, 12, 14, 0));
readRTC();
updateDisplayRouting();
}
void loop() {
handleInputsNoMillis();
if (!setModeActive) {
checkChimesAsynchronous();
// Query the RTC hardware via I2C once every 10 loops (~200ms)
rtcRefreshTimer++;
if (rtcRefreshTimer >= 10) {
readRTC();
rtcRefreshTimer = 0;
}
}
// Conditional refresh gate blocks high-frequency trace noise from locking up buttons
if (seconds != lastDispSec || minutes != lastDispMin || hours != lastDispHr || setModeActive) {
updateDisplayRouting();
lastDispSec = seconds;
lastDispMin = minutes;
lastDispHr = hours;
}
delay(20);
}
Giving the source and details of your hardware might help.
I can't compile because I am not sure which of the libraries you are using for,
RTClib.h
.
https://dronebotworkshop.com/real-time-clock-arduino/
Or whatever source you have used for the hardware?
Ardruino uno rev 3. When i got the ino file and loaded it into other Ardruino, i couldn’t complete it either. I will try to copy it straight from the clock Ardruino to the programmer to see if that helps.
lee
I’m checking with James, and asked him what version of RTClib.h he used. When I get answer, l’ll let you know…. Lee
By hardware I meant a link to the hardware/software project are you using to make the clock?
I see it is a popular project and different versions can be found on the internet.
Is there any reason James cannot get his software working?
According to ChatGPT the reason your hour set switch doesn't work is because your hour button isn't actually implemented.
It seems like a simple enough project but I haven't done a real time clock project myself and I would have to spend time learning about it which is why I referenced the dronebot tutorials. You can then use that knowledge to tailor your own solutions.
Are you wanting someone to do the software for you? Or are you wanting to learn how to program yourself?
To robotbuilder:
one. I did all the hardware design, schematic & pcb layout. Two, code never has been my expertize. Ican read it a d understand it,but i totally suck at writing it. I know my limitations.
i would appreciate the code help. If you would like to see my schematic and pcb layout. I haven’t made a BOM of the the clock since I was only doing this for the wife. This is version 3 over fifty years. Hopefully this is my last one.
i just talked to James and he will send me what libraries he used, as soon as he can look at the code. His studies take priority, so i understand.
lee
Yes I would find the schematic interesting it might help me picture the project better.
I don't have time to work all this code out myself but for what it is worth the code below written by chatGPT compiles for me and is followed by an explanation of the code. Usually I avoid posting AI code as I have no way for sure that it is correct for I haven't used this module or studied the logic of the program.
#include <Wire.h>
#include <RTClib.h>
RTC_DS3231 rtc;
// ============================================================
// HARDWARE
// ============================================================
// Shift register pins
const uint8_t REG_CLOCK = 13; // SRCLK
const uint8_t REG_LATCH = 10; // RCLK
const uint8_t REG_DATA = 11; // SER
const uint8_t REG_OE = 9; // Output Enable / PWM
// Buttons - active LOW
const uint8_t BTN_BRIGHT = 2;
const uint8_t BTN_MIN = 3;
const uint8_t BTN_SET = 5;
// ============================================================
// 7-SEGMENT DISPLAY
// ============================================================
// Common-anode display patterns.
// Bit 7 is the decimal point / indicator LED.
const byte digitPatterns[10] = {
0b01111110, // 0
0b00110000, // 1
0b01101101, // 2
0b01111001, // 3
0b00110011, // 4
0b01011011, // 5
0b01011111, // 6
0b01110000, // 7
0b01111111, // 8
0b01111011 // 9
};
const byte SET_INDICATOR = 0b10000000;
// ============================================================
// BRIGHTNESS
// ============================================================
// Your custom active-low PWM levels.
const uint8_t brightnessPWM[5] = {
255,
192,
128,
64,
0
};
uint8_t currentBrightStage = 2;
// ============================================================
// CLOCK STATE
// ============================================================
int hours = 12;
int minutes = 14;
int seconds = 0;
bool is24Hour = false;
// ============================================================
// BUTTON STATE
// ============================================================
const unsigned long SET_HOLD_TIME = 600;
const unsigned long MIN_REPEAT_START = 500;
const unsigned long MIN_REPEAT_INTERVAL = 120;
bool previousSetPressed = false;
bool previousBrightPressed = false;
bool previousMinPressed = false;
unsigned long setPressedAt = 0;
unsigned long minPressedAt = 0;
unsigned long lastMinStep = 0;
bool setModeActive = false;
// ============================================================
// RTC TIMING
// ============================================================
unsigned long lastRTCRead = 0;
const unsigned long RTC_READ_INTERVAL = 200;
// ============================================================
// DISPLAY CACHE
// ============================================================
int lastDispSec = -1;
int lastDispMin = -1;
int lastDispHr = -1;
bool lastDispSetMode = false;
bool lastDisp24Hour = false;
// ============================================================
// CHIME ENGINE
// ============================================================
int lastCheckedMinute = -1;
bool chimeDoneThisMinute = false;
bool introMelodyActive = false;
unsigned long chimeStateTimer = 0;
int strikeCount = 0;
int totalStrikesNeeded = 0;
// ============================================================
// READ RTC
// ============================================================
void readRTC() {
DateTime now = rtc.now();
hours = now.hour();
minutes = now.minute();
seconds = now.second();
}
// ============================================================
// SAVE TIME TO RTC
// ============================================================
void saveTimeToRTC() {
DateTime current = rtc.now();
rtc.adjust(DateTime(
current.year(),
current.month(),
current.day(),
hours,
minutes,
0
));
}
// ============================================================
// DISPLAY
// ============================================================
void updateDisplay() {
int displayHours = hours;
bool isPM = (displayHours >= 12);
// Convert to 12-hour format when required.
if (!is24Hour) {
displayHours %= 12;
if (displayHours == 0) {
displayHours = 12;
}
}
// Break time into individual digits.
uint8_t onesSec = seconds % 10;
uint8_t tensSec = seconds / 10;
uint8_t onesMin = minutes % 10;
uint8_t tensMin = minutes / 10;
uint8_t onesHr = displayHours % 10;
uint8_t tensHr = displayHours / 10;
// Build display data.
byte seg0 = digitPatterns[onesSec];
byte seg1 = digitPatterns[tensSec];
byte seg2 = (tensHr == 0)
? 0b00000000
: digitPatterns[tensHr];
byte seg3 = digitPatterns[onesHr];
byte seg4 = digitPatterns[tensMin];
byte seg5 = digitPatterns[onesMin];
// SET mode indicator.
if (setModeActive) {
seg2 |= SET_INDICATOR;
seg3 |= SET_INDICATOR;
}
// PM indicator in 12-hour mode.
if (!is24Hour && isPM) {
seg5 |= SET_INDICATOR;
}
// Send data to shift registers.
digitalWrite(REG_LATCH, LOW);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg5);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg4);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg3);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg2);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg1);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg0);
digitalWrite(REG_LATCH, HIGH);
}
// ============================================================
// DISPLAY REFRESH
// ============================================================
void refreshDisplayIfNeeded() {
if (
seconds != lastDispSec ||
minutes != lastDispMin ||
hours != lastDispHr ||
setModeActive != lastDispSetMode ||
is24Hour != lastDisp24Hour
) {
updateDisplay();
lastDispSec = seconds;
lastDispMin = minutes;
lastDispHr = hours;
lastDispSetMode = setModeActive;
lastDisp24Hour = is24Hour;
}
}
// ============================================================
// AUDIO
// ============================================================
void triggerTrack(uint8_t trackNumber) {
Serial.write('t');
// Very short pause required by your audio device.
delayMicroseconds(50);
Serial.write(trackNumber);
}
// ============================================================
// CHIMES
// ============================================================
void checkChimes() {
// Detect a new minute.
if (minutes != lastCheckedMinute) {
lastCheckedMinute = minutes;
chimeDoneThisMinute = false;
}
// Don't start another chime if this minute already triggered.
if (!chimeDoneThisMinute) {
if (minutes == 15) {
triggerTrack(1);
chimeDoneThisMinute = true;
}
else if (minutes == 30) {
triggerTrack(2);
chimeDoneThisMinute = true;
}
else if (minutes == 45) {
triggerTrack(3);
chimeDoneThisMinute = true;
}
else if (minutes == 0) {
// Start hourly intro.
triggerTrack(4);
introMelodyActive = true;
chimeStateTimer = millis();
totalStrikesNeeded = hours % 12;
if (totalStrikesNeeded == 0) {
totalStrikesNeeded = 12;
}
strikeCount = 0;
chimeDoneThisMinute = true;
}
}
// ==========================================================
// HOURLY STRIKE ENGINE
// ==========================================================
if (!introMelodyActive) {
return;
}
unsigned long elapsed = millis() - chimeStateTimer;
// Wait 26 seconds for intro melody.
if (strikeCount == 0) {
if (elapsed >= 26000) {
triggerTrack(5);
strikeCount = 1;
chimeStateTimer = millis();
}
return;
}
// Additional strikes every 3 seconds.
if (
strikeCount < totalStrikesNeeded &&
elapsed >= 3000
) {
triggerTrack(5);
strikeCount++;
chimeStateTimer = millis();
return;
}
// Finished.
if (strikeCount >= totalStrikesNeeded) {
introMelodyActive = false;
}
}
// ============================================================
// CHANGE MINUTE
// ============================================================
void advanceMinute() {
seconds = 0;
minutes++;
if (minutes >= 60) {
minutes = 0;
hours++;
if (hours >= 24) {
hours = 0;
}
}
}
// ============================================================
// HANDLE BRIGHTNESS BUTTON
// ============================================================
void handleBrightnessButton(bool pressed) {
// Detect a new press rather than continuous holding.
if (pressed && !previousBrightPressed) {
currentBrightStage++;
if (currentBrightStage >= 5) {
currentBrightStage = 0;
}
analogWrite(
REG_OE,
brightnessPWM[currentBrightStage]
);
}
previousBrightPressed = pressed;
}
// ============================================================
// HANDLE MINUTE BUTTON
// ============================================================
void handleMinuteButton(bool pressed, unsigned long now) {
if (!pressed) {
minPressedAt = 0;
previousMinPressed = false;
return;
}
// New press.
if (!previousMinPressed) {
advanceMinute();
minPressedAt = now;
lastMinStep = now;
previousMinPressed = true;
return;
}
// Button is being held.
unsigned long heldTime = now - minPressedAt;
if (
heldTime >= MIN_REPEAT_START &&
now - lastMinStep >= MIN_REPEAT_INTERVAL
) {
advanceMinute();
lastMinStep = now;
}
}
// ============================================================
// HANDLE SET BUTTON
// ============================================================
void handleSetButton(bool pressed, unsigned long now) {
// ----------------------------------------------------------
// BUTTON JUST PRESSED
// ----------------------------------------------------------
if (pressed && !previousSetPressed) {
setPressedAt = now;
previousSetPressed = true;
return;
}
// ----------------------------------------------------------
// BUTTON BEING HELD
// ----------------------------------------------------------
if (pressed && previousSetPressed) {
// Enter setting mode after 600 ms.
if (
!setModeActive &&
now - setPressedAt >= SET_HOLD_TIME
) {
setModeActive = true;
// Stop any chime activity while setting.
introMelodyActive = false;
}
return;
}
// ----------------------------------------------------------
// BUTTON RELEASED
// ----------------------------------------------------------
if (!pressed && previousSetPressed) {
unsigned long pressDuration = now - setPressedAt;
// Long press.
if (setModeActive) {
setModeActive = false;
// Save the newly adjusted time.
saveTimeToRTC();
}
// Short press.
else if (pressDuration < SET_HOLD_TIME) {
is24Hour = !is24Hour;
}
previousSetPressed = false;
}
}
// ============================================================
// HANDLE ALL BUTTONS
// ============================================================
void handleButtons() {
unsigned long now = millis();
bool setPressed =
digitalRead(BTN_SET) == LOW;
bool minPressed =
digitalRead(BTN_MIN) == LOW;
bool brightPressed =
digitalRead(BTN_BRIGHT) == LOW;
// SET button controls mode.
handleSetButton(setPressed, now);
// Brightness and minute buttons only work in setting mode.
if (setModeActive) {
handleBrightnessButton(brightPressed);
handleMinuteButton(minPressed, now);
}
else {
// Reset their state when leaving setting mode.
previousBrightPressed = brightPressed;
previousMinPressed = false;
minPressedAt = 0;
}
}
// ============================================================
// SETUP
// ============================================================
void setup() {
// Shift registers.
pinMode(REG_CLOCK, OUTPUT);
pinMode(REG_LATCH, OUTPUT);
pinMode(REG_DATA, OUTPUT);
pinMode(REG_OE, OUTPUT);
// Buttons.
pinMode(BTN_BRIGHT, INPUT_PULLUP);
pinMode(BTN_MIN, INPUT_PULLUP);
pinMode(BTN_SET, INPUT_PULLUP);
// Initial brightness.
analogWrite(
REG_OE,
brightnessPWM[currentBrightStage]
);
// I2C.
Wire.begin();
// Audio device.
Serial.begin(38400);
// RTC.
if (!rtc.begin()) {
// RTC not found.
while (true) {
// Stop here.
}
}
// Only set the RTC if it has lost power.
if (rtc.lostPower()) {
rtc.adjust(
DateTime(F(__DATE__), F(__TIME__))
);
}
// Get initial time.
readRTC();
updateDisplay();
}
// ============================================================
// MAIN LOOP
// ============================================================
void loop() {
unsigned long now = millis();
// Buttons are checked continuously.
handleButtons();
// Don't update RTC while manually setting the time.
if (!setModeActive) {
// Refresh RTC approximately every 200 ms.
if (now - lastRTCRead >= RTC_READ_INTERVAL) {
lastRTCRead = now;
readRTC();
}
// Check quarter-hour/hour chimes.
checkChimes();
}
// Only shift display data when something changed.
refreshDisplayIfNeeded();
}
This is essentially a digital clock program for Arduino that combines four pieces of hardware:
- A DS3231 real-time clock (RTC) to keep accurate time.
- Six 7-segment digits, controlled through shift registers.
- Three buttons for setting time, changing 12/24-hour mode, and brightness.
- An audio device that plays quarter-hour and hourly chimes.
The easiest way to understand it is to follow the program from hardware → state → loop.
1. The libraries and RTC
#include <Wire.h>
#include <RTClib.h>
RTC_DS3231 rtc;
Wire.h provides I²C communication, which is how the Arduino talks to the DS3231.
RTClib.h provides convenient functions for reading and setting the DS3231.
The important object is:
RTC_DS3231 rtc;
You can then do things like:
DateTime now = rtc.now();
to get the current date and time.
2. Hardware pins
You define the Arduino pins here:
const uint8_t REG_CLOCK = 13;
const uint8_t REG_LATCH = 10;
const uint8_t REG_DATA = 11;
const uint8_t REG_OE = 9;
These are connected to your shift register(s).
Conceptually:
Arduino
│
├── DATA ──────► Shift register
├── CLOCK ──────► Shift register
├── LATCH ──────► Shift register
└── OE ──────► Brightness control
The three buttons are:
BTN_BRIGHT = 2
BTN_MIN = 3
BTN_SET = 5
They're configured with:
INPUT_PULLUP
That means the button logic is active LOW.
So:
digitalRead(BTN_SET) == LOW
means:
"The SET button is currently pressed."
3. The 7-segment digit patterns
This is one of the most important parts:
const byte digitPatterns[10] = {
0b01111110, // 0
0b00110000, // 1
...
};
Each byte represents which segments of a 7-segment display should be illuminated.
For example:
a
---
f | | b
-g-
e | | c
---
d
A digit such as 8 needs essentially all seven segments.
Because your display is common-anode, the bit patterns are arranged for the way your hardware is wired.
You also have:
const byte SET_INDICATOR = 0b10000000;
Bit 7 is being used as an additional indicator LED/decimal-point output.
So this:
seg3 |= SET_INDICATOR;
means:
Keep the digit segments, but also turn on the indicator LED.
4. Brightness
You have five brightness levels:
const uint8_t brightnessPWM[5] = {
255,
192,
128,
64,
0
};
And initially:
uint8_t currentBrightStage = 2;
So the clock starts at:
brightnessPWM[2] = 128
The brightness is controlled through:
analogWrite(REG_OE, ...);
Pressing the brightness button cycles:
Stage 0 → Stage 1 → Stage 2 → Stage 3 → Stage 4 → Stage 0
Because OE is active-low, you've deliberately chosen PWM values that work with your particular circuit.
5. The clock variables
These hold the time currently being displayed:
int hours = 12;
int minutes = 14;
int seconds = 0;
You also have:
bool is24Hour = false;
So initially the display is in 12-hour mode.
For example, if the RTC says:
14:37:25
then:
hours = 14
minutes = 37
seconds = 25
But because:
is24Hour == false
the display converts 14 to:
2
and turns on the PM indicator.
6. Reading the RTC
This function is very simple:
void readRTC() {
DateTime now = rtc.now();
hours = now.hour();
minutes = now.minute();
seconds = now.second();
}
It asks the DS3231:
"What time is it?"
and copies the result into your three variables.
The program calls this approximately every 200 ms:
const unsigned long RTC_READ_INTERVAL = 200;
So although the display only visibly changes once per second, the Arduino is checking the RTC five times per second.
7. Saving manually adjusted time
When you change the minutes using the button, the program changes its local variables.
Eventually it calls:
saveTimeToRTC();
This gets the existing date:
DateTime current = rtc.now();
and writes the newly selected time back:
rtc.adjust(DateTime(
current.year(),
current.month(),
current.day(),
hours,
minutes,
0
));
Notice that seconds are deliberately set to:
0
So if you set the clock to:
3:42
the RTC becomes:
3:42:00
8. How the display is constructed
This function does the actual work:
updateDisplay();
First it decides what hour should be shown:
int displayHours = hours;
Then:
bool isPM = (displayHours >= 12);
So:
0–11 → AM
12–23 → PM
If you're in 12-hour mode:
displayHours %= 12;
if (displayHours == 0) {
displayHours = 12;
}
That converts:
00 → 12
01 → 1
...
12 → 12
13 → 1
14 → 2
...
23 → 11
9. Breaking the time into digits
Suppose the time is:
14:37:25
The code extracts:
onesSec = 5
tensSec = 2
onesMin = 7
tensMin = 3
onesHr = 2
tensHr = 0
Because you're in 12-hour mode, 14 becomes 2.
So the six display positions are effectively:
[hour tens] [hour ones] [minute tens] [minute ones] [second tens] [second ones]
0 2 3 7 2 5
10. Why seg2 can be blank
This:
byte seg2 = (tensHr == 0)
? 0b00000000
: digitPatterns[tensHr];
means:
If the hour has no tens digit, don't display anything.
So instead of:
02:37:25
you get:
2:37:25
That's useful for a 12-hour clock.
In 24-hour mode, however, 02 will display both digits.
11. SET mode indicator
When setting the clock:
if (setModeActive) {
seg2 |= SET_INDICATOR;
seg3 |= SET_INDICATOR;
}
The indicator bit gets added to the hour digits.
So the hour section tells the user:
"You're currently in SET mode."
12. PM indicator
Similarly:
if (!is24Hour && isPM) {
seg5 |= SET_INDICATOR;
}
If you're in 12-hour mode and it's PM, the indicator on the last digit turns on.
So something conceptually like:
2:37:25 •
means PM.
13. Sending the digits to the shift registers
This is the part that physically updates the display:
digitalWrite(REG_LATCH, LOW);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg5);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg4);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg3);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg2);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg1);
shiftOut(REG_DATA, REG_CLOCK, MSBFIRST, seg0);
digitalWrite(REG_LATCH, HIGH);
The Arduino sends six bytes, one for each digit.
Importantly, you're sending:
seg5
seg4
seg3
seg2
seg1
seg0
rather than seg0 through seg5.
That's because the shift registers are chained, so the first byte you send ends up at the far end of the chain.
The latch works roughly like this:
LATCH LOW
↓
send six bytes
↓
LATCH HIGH
↓
all six outputs change simultaneously
This prevents the display from visibly changing one digit at a time while the data is being shifted in.
14. The display cache
This is a nice optimization.
You have:
lastDispSec
lastDispMin
lastDispHr
and similar variables for mode.
Then:
refreshDisplayIfNeeded()
checks whether anything changed.
For example, if the RTC is repeatedly reporting:
14:37:25
14:37:25
14:37:25
14:37:25
the program doesn't repeatedly send the same six bytes to the shift registers.
Only when something changes does it call:
updateDisplay();
This reduces unnecessary work.
15. The audio system
Your audio device is controlled through Serial:
Serial.begin(38400);
To play a track:
triggerTrack(5);
runs:
Serial.write('t');
delayMicroseconds(50);
Serial.write(trackNumber);
So the device receives something equivalent to:
't' + track number
For example:
triggerTrack(3);
sends:
t
3
The exact meaning is determined by your audio hardware's protocol.
16. Quarter-hour chimes
This section:
if (minutes == 15) {
triggerTrack(1);
}
means:
XX:15 → Track 1
XX:30 → Track 2
XX:45 → Track 3
At exactly the top of the hour:
else if (minutes == 0) {
triggerTrack(4);
Track 4 is your hourly introduction melody.
17. The hourly strike system
After playing track 4, this happens:
introMelodyActive = true;
chimeStateTimer = millis();
The program remembers:
"I just started the hourly melody."
Then:
totalStrikesNeeded = hours % 12;
determines how many strikes are required.
For example:
1:00 → 1 strike
2:00 → 2 strikes
3:00 → 3 strikes
...
11:00 → 11 strikes
12:00 → 12 strikes
This:
if (totalStrikesNeeded == 0) {
totalStrikesNeeded = 12;
}
handles 12 o'clock.
18. Why millis() is used for the strikes
Instead of doing:
delay(26000);
the program uses:
millis()
This is important because delay() would freeze the rest of your program.
Instead:
unsigned long elapsed = millis() - chimeStateTimer;
calculates how long it's been since the previous event.
The sequence is:
00:00
│
├── Play intro
│
├── wait 26 seconds
│
├── Play strike
│
├── wait 3 seconds
│
├── Play strike
│
├── wait 3 seconds
│
└── etc.
So at 3:00, for example:
3:00:00 Intro
3:00:26 Strike 1
3:00:29 Strike 2
3:00:32 Strike 3
Then:
introMelodyActive = false;
ends the chime sequence.
19. The minute button
advanceMinute() does exactly what its name says.
seconds = 0;
minutes++;
If minutes reach 60:
if (minutes >= 60) {
minutes = 0;
hours++;
}
And if hours reach 24:
if (hours >= 24) {
hours = 0;
}
So:
12:59 → 13:00
23:59 → 00:00
20. Why the minute button repeats
A short press advances one minute.
A long hold behaves like a "repeat" button.
Initially:
MIN_REPEAT_START = 500;
so the button must be held for 500 ms before repeating.
Then:
MIN_REPEAT_INTERVAL = 120;
means another minute is added approximately every 120 ms.
So:
Press
↓
+1 minute immediately
↓
hold 500 ms
↓
+1 minute
↓
120 ms
↓
+1 minute
↓
120 ms
↓
+1 minute
...
This makes rapidly setting the time possible.
21. The SET button has two functions
This is probably the most interesting button logic.
A short press changes:
is24Hour = !is24Hour;
So:
12-hour → 24-hour
24-hour → 12-hour
A long press enters setting mode.
The threshold is:
SET_HOLD_TIME = 600;
So:
press SET
│
├── release before 600 ms
│ ↓
│ toggle 12/24h
│
└── hold for ≥600 ms
↓
enter SET mode
22. What happens in SET mode
Once this happens:
setModeActive = true;
the program allows:
handleBrightnessButton()
handleMinuteButton()
to operate.
So while setting:
- Brightness button → changes brightness.
- Minute button → advances minutes.
- SET button → exits setting mode.
When you release SET:
if (setModeActive) {
setModeActive = false;
saveTimeToRTC();
}
The newly adjusted time is written to the DS3231.
So the workflow is:
Hold SET
↓
Enter setting mode
↓
Press MIN repeatedly/hold
↓
Adjust time
↓
Release SET
↓
Save time to RTC
↓
Return to normal operation
23. Why chimes stop during setting
When entering setting mode:
introMelodyActive = false;
And in the main loop:
if (!setModeActive) {
...
checkChimes();
}
So while you're adjusting the clock, the chime system is disabled.
That's sensible—you don't want the clock suddenly starting an hourly sequence while you're setting it.
24. setup() — what happens when the Arduino starts
The Arduino runs setup() once.
It first configures the shift-register pins:
pinMode(REG_CLOCK, OUTPUT);
...
Then the buttons:
pinMode(BTN_BRIGHT, INPUT_PULLUP);
...
Then it sets the initial brightness.
Next:
Wire.begin();
starts I²C.
Then:
Serial.begin(38400);
starts communication with the audio device.
Then:
if (!rtc.begin()) {
while (true) {
}
}
checks whether the DS3231 exists.
If it doesn't, the Arduino gets stuck there permanently.
Then:
if (rtc.lostPower()) {
rtc.adjust(
DateTime(F(__DATE__), F(__TIME__))
);
}
handles a dead/disconnected RTC battery.
It sets the RTC to the date/time the Arduino sketch was compiled.
Finally:
readRTC();
updateDisplay();
gets the current time and immediately displays it.
25. loop() — the heart of the program
Arduino repeatedly executes:
void loop()
forever.
Your loop is essentially:
┌──────────────────────────────┐
│ loop() │
│ │
│ Check buttons │
│ ↓ │
│ Are we setting time? │
│ / \ │
│ YES NO │
│ ↓ ↓ │
│ Handle buttons Read RTC │
│ ↓ │
│ Check chimes │
│ │
│ ↓ │
│ Refresh display │
│ ↓ │
│ repeat │
└──────────────────────────────┘
More specifically:
handleButtons();
runs every loop.
Then, only when you're not in setting mode:
if (!setModeActive) {
the Arduino reads the RTC every ~200 ms and checks the chimes.
Finally:
refreshDisplayIfNeeded();
updates the display if something changed.
26. One subtle thing worth noticing
There are actually two versions of the time in this program.
The DS3231 has the authoritative time:
DS3231 RTC
while these variables are a temporary copy:
hours
minutes
seconds
Normally:
RTC → readRTC() → hours/minutes/seconds → display
During setting:
buttons → hours/minutes → display
↓
saveTimeToRTC()
↓
RTC
That's a good architecture because you don't continuously write to the RTC while the user is pressing the button.
27. The whole system in one picture
Putting everything together:
┌──────────────┐
│ DS3231 │
│ RTC │
└──────┬───────┘
│ I²C
↓
┌──────────────┐
│ Arduino │
│ │
Buttons ──────────►│ Program │
│ │
└───┬──────┬───┘
│ │
shift │ │ Serial
register │ │
↓ ↓
┌──────────┐ ┌──────────┐
│ 6-digit │ │ Audio │
│ 7-seg │ │ device │
│ display │ │ │
└──────────┘ └──────────┘
The Arduino is basically acting as the controller/orchestrator:
RTC
│
├── current time ──► Display
│
└── minute/hour ───► Chime engine ──► Audio
Buttons
│
├── SET ───────────► Setting mode
├── MIN ───────────► Change time
└── BRIGHT ────────► Display brightness
One thing I'd pay particular attention to
There is a potential edge case in the chime logic around setting the time across a quarter-hour/hour. The program intentionally stops checkChimes() during setting, but lastCheckedMinute isn't explicitly synchronized afterward. Usually the next RTC read will establish the new minute correctly, but depending on exactly how the time is changed, you can get behavior that's worth testing.
Also, the hourly chime uses:
totalStrikesNeeded = hours % 12;
which is correct for a 12-hour strike count, but importantly it uses the 24-hour hours variable, not displayHours. That's actually a good choice: both 00 and 12 produce 12 strikes.
Very impressive work!
You would certainly be able to incorporate the IR sensor remote into your project.
https://dronebotworkshop.com/using-ir-remote-controls-with-arduino/
I remember playing around with IR remote control some years back now but don't need it in my current project.
Although I thought it seemed a simple project it is in fact a major project and you have done very well so far with the hardware. To help with the code I would have to spend a lot of time getting up to speed with all the details and unfortunately I am flat out in my spare time with my own project at the moment. Hopefully someone will come on board.
You say you can read code and understand it, but totally suck at writing it. But if you understand it why is it hard to write? I have the opposite problem. Deconstructing someone else's code is hard but translating my ideas clearly stated and understood is easy.
Did the AI code run or make sense? If you have an issue with using AI that is fine just say so. I use it as an educational tool and mentor for my own code but wouldn't use AI code without fully understanding how it worked.
AI can explain and comment on other people's code.
For example in the code you posted above it noted:
There is a little oddity here: you include both:
#include <I2C_RTC.h>
#include <RTClib.h>
but the code actually uses the RTClib DS3231 interface.
You have line
DS3231 rtc;
When you needed,
RTC_DS3231 rtc;
And so on ...
Wil load your your into my test Ardruino to see if it complies.
thanks for your help.
mom wants her new clock back with chimes up on the wall, so I might have to build another clock up to test your code.
i’ll let you how I make out in a few days.
i’m going to try to use the second clock to test your code and maybe try to correct the hour set and looking into possibly doing the ir remote.
i will add a picture of her new blue clock running, when i get upon the wall.
Ok. Let me know how it goes.
You can adjust code to set the hour setting and also add IR remote code. You need to find out what the codes are for each button your IR remote sends. Run this program on an Arduino board with the IR sensor connected to to pin 4 to get the codes.
#include <IRremote.hpp>
#define IR_RECEIVE_PIN 4
void setup() {
IrReceiver.begin(IR_RECEIVE_PIN, DISABLE_LED_FEEDBACK);
// IMPORTANT:
// Your normal clock uses Serial for the MP3/audio device.
// This temporary sketch uses it for IR code learning instead.
Serial.begin(115200);
Serial.println(F("IR LEARNING MODE"));
Serial.println(F("Press each remote button..."));
}
void loop() {
if (IrReceiver.decode()) {
IrReceiver.printIRResultShort(&Serial);
IrReceiver.printIRSendUsage(&Serial);
Serial.println();
IrReceiver.resume();
}
}
The find IR remote code failed to compile for me.
Fixed by,
In the Arduino IDE got to:
Tools -> Manage Libraries ...
In search box type IRremote
and install
maybe there is hope for me. I was able to compline the code you gave me. The first picture is an Arduino uno rev3 ic chip on com 4. The second is an SMD chip, it complied and loaded fine, but couldn’t open a COM port. I don’t know why the SMD didn’t see the com port.
I am building up a new clock for test purposes. Tomorrow , I will move my clock to the basement and test the new arduino in the clock circuit.
if it works, then I’ m done for awhile. I am redesigning the amp circuit, I had a hum when it wasn’t playing a tune. Waiting for parts.
I would still like to get a remote working with the test circuit once I get it built. I won’t move anything to the real clock it everything works.
thanks for your hopeless so far. First, my testing, then maybe I can real about the remote, with help of course.
I will let you know how things go.
so why can’t I get the SMD to see the com port.
thanks, Lee




