Initially I had hooked up shift register and 4 BJTs in order to drive 7-segment displays, but then I found out there's an issue with I2C and disconnected everything except RTC.
// This sketch reads RTC output and drives multiplexed 7-segment 4-digit LED display with 4 common cathodes.
// Between GND and each common cathode a small-signal NPN BJT is required.
#include <Adafruit_BusIO_Register.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_I2CRegister.h>
#include <RTClib.h>
// GENERIC GLOBAL VARIABLES
// This sketch works with both DS3231 and DS1307 RTCs. Applicable variant must be uncommented to create an object.
// RTC_DS3231 rtc;
RTC_DS1307 rtc;
// For storing numbers from RTC output.
uint8_t RTC_hours;
uint8_t RTC_minutes;
uint8_t RTC_seconds;
// For storing digits to be displayed.
uint8_t hours_first_digit;
uint8_t hours_second_digit;
uint8_t minutes_first_digit;
uint8_t minutes_second_digit;
uint8_t seconds_first_digit;
uint8_t seconds_second_digit;
// GLOBAL VARIABLES RELATED TO USER-DEFINED FUNCTIONS
// dispay_digit()
// Pins which drive transistors connecting common cathodes to GND
// and thus switch the active display section ("D" is for "digit").
const uint8_t D1_PIN = 12;
const uint8_t D2_PIN = 11;
const uint8_t D3_PIN = 10;
const uint8_t D4_PIN = 9;
// Pins which interact with 74HC595 (known as simply "595") latched shift register IC.
// The 595 is used to create a set of 8 parallel output signals ("byte mask") that lights up only necessary segments at any given time.
const uint8_t DATA_PIN = 8;
const uint8_t LATCH_PIN = 4;
const uint8_t CLOCK_PIN = 7;
uint8_t dot_byte; // A byte which, being added to a byte mask, turns decimal point ("dot") bit into 1.
bool dot_state; // Makes decimal point blink.
// serial_output()
uint8_t previous_RTC_seconds; // Reference point for finding out if seconds output has been changed, which entails sending data via UART.
bool initial_RTC_seconds_was_stored = 0; // Makes the code which finds out initial previous_RTC_seconds run only once.
// compensate()
const int COMPENSATION_VALUE = 0; // Number of seconds added to or subtracted from RTC_seconds in order to keep up with the actual time. Zero value means no compensation.
// Value should be negative if RTC runs ahead of time, positive otherwise. Valid value ranges from -59 to 59.
const uint8_t COMPENSATION_THRESHOLD = 12; // How many hours will pass until compensation occurs.
uint8_t compensation_counter = 0; // Hours counter.
uint8_t previous_RTC_hours; // Reference point for finding out if hours output has been changed, which entails raising hours counter.
bool initial_RTC_hours_was_stored = 0; // Makes the code which finds out initial previous_RTC_hours run only once.
bool compensation_loaded = 0; // Enables compensation and puts it "on hold" until RTC_seconds takes appropriate value.
bool compensation_all_clear; // Confirms that RTC_seconds holds appropriate value. It prevents driving RTC_seconds below 0 or above 59.
// manual_settime()
bool settime_mode = 0; // Used to begin and end while() loops during which time may be manually set.
// Pins for active-low manual input buttons.
const uint8_t SETTIME_HOURS_PIN = 2;
const uint8_t SETTIME_MINUTES_PIN = 3;
// For button debounce.
bool settime_hours_button_is_pressed;
bool settime_hours_button_wasnt_pressed;
bool settime_minutes_button_is_pressed;
bool settime_minutes_button_wasnt_pressed;
unsigned long previous_millis;
bool holding_toggle_available = 0;
int holding_toggle_timing = 500;
// For storing temporary numbers to be loaded into RTC when settime mode is turned off.
uint8_t settime_hours;
uint8_t settime_minutes;
uint8_t settime_seconds;
void setup() {
rtc.begin();
// If necessary, uncomment following line to set RTC time, upload the sketch, comment the following line once again and then re-upload the sketch.
// rtc.adjust(DateTime(2022, 3, 14, 1, 53, 0)); // year, month, date, hours, minutes, seconds
pinMode(D1_PIN, OUTPUT);
pinMode(D2_PIN, OUTPUT);
pinMode(D3_PIN, OUTPUT);
pinMode(D4_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(DATA_PIN, OUTPUT);
pinMode(SETTIME_HOURS_PIN, INPUT_PULLUP);
pinMode(SETTIME_MINUTES_PIN, INPUT_PULLUP);
// Prevents artifacts from being displayed immediately after boot.
digitalWrite(D1_PIN, 0);
digitalWrite(D2_PIN, 0);
digitalWrite(D3_PIN, 0);
digitalWrite(D4_PIN, 0);
// May be handy for debugging purposes etc.
Serial.begin(9600);
}
void loop() {
if(millis() - previous_millis >= 1000) {
previous_millis = millis();
Serial.println("test!");
}
DateTime now = rtc.now();
RTC_hours = now.month(), DEC;
RTC_minutes = now.month(), DEC;
RTC_seconds = now.month(), DEC;
// Division and modulo operators are used
// to separate numbers into first and second digit.
hours_first_digit = RTC_hours % 10;
hours_second_digit = RTC_hours / 10;
minutes_first_digit = RTC_minutes % 10;
minutes_second_digit = RTC_minutes / 10;
seconds_first_digit = RTC_seconds % 10;
seconds_second_digit = RTC_seconds / 10;
// Display function callers.
display_digit(D1_PIN, hours_first_digit, 0);
display_digit(D2_PIN, hours_second_digit, 1); // Decimal point is used to visually separate hours from minutes and indicate that the clock is running.
display_digit(D3_PIN, minutes_first_digit, 0);
display_digit(D4_PIN, minutes_second_digit, 0);
// Serial output function caller.
serial_output();
// Time drift compensation function caller.
compensate();
// Turn on settime mode.
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (settime_hours_button_is_pressed && settime_hours_button_wasnt_pressed) {
delay(10);
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (settime_hours_button_is_pressed) {
holding_toggle_available = true;
previous_millis = millis();
while (holding_toggle_available) {
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (!settime_hours_button_is_pressed) {
holding_toggle_available = false;
}
if (millis() - previous_millis >= holding_toggle_timing) {
holding_toggle_available = false; // Not really necessary, but it feels right to drop this flag to zero befor settime sequence begins.
manual_settime(); // Settime function caller.
}
}
}
}
settime_hours_button_wasnt_pressed = !settime_hours_button_is_pressed;
}
void display_digit(int current_cathode, int digit_to_display, bool whether_dot_is_used) { // User-defined function dedicated to actually displaying digits.
// Defines which display section (digit) is to be turned on right now.
digitalWrite(D1_PIN, current_cathode == D1_PIN ? 1 : 0);
digitalWrite(D2_PIN, current_cathode == D2_PIN ? 1 : 0);
digitalWrite(D3_PIN, current_cathode == D3_PIN ? 1 : 0);
digitalWrite(D4_PIN, current_cathode == D4_PIN ? 1 : 0);
// Byte masks for digits from 0 to 9. May vary depending on the order
// in which 595 output pins are wired to the display input pins.
uint8_t output_matrix[] = {
0b11101110, // 0
0b00000110, // 1
0b11100011, // 2
0b01100111, // 3
0b00001111, // 4
0b01101101, // 5
0b11101101, // 6
0b00100110, // 7
0b11101111, // 8
0b01101111 // 9
};
// If third argument of function is 1, decimal point will blink once per second.
dot_byte = 0;
if (whether_dot_is_used) {
dot_state = seconds_second_digit % 2; // Every other second the dot blinks.
if (dot_state) {
dot_byte = 0b00010000;
} else dot_byte = 0;
}
// Displaying.
digitalWrite(LATCH_PIN, 0);
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, output_matrix[digit_to_display] + dot_byte);
digitalWrite(LATCH_PIN, 1);
// Anti-ghosting sequence.
delay(4); // Anti-ghosting delay
digitalWrite(LATCH_PIN, 0);
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, 0b00000000); // Turns all display sections off for one clock period.
digitalWrite(LATCH_PIN, 1);
}
void serial_output() { // Optional user-defined function. Used for sending RTC output via UART for debugging purposes etc.
if (!initial_RTC_seconds_was_stored) {
previous_RTC_seconds = RTC_seconds;
initial_RTC_seconds_was_stored = 1;
}
if (RTC_seconds != previous_RTC_seconds) {
Serial.print(RTC_hours);
Serial.print(":");
Serial.print(RTC_minutes);
Serial.print(":");
Serial.println(RTC_seconds);
previous_RTC_seconds = RTC_seconds;
}
}
void compensate() { // User-defined function dedicated to software compensation for time drift.
if (!initial_RTC_hours_was_stored) { // Begin counting hours.
previous_RTC_hours = RTC_hours;
initial_RTC_hours_was_stored = 1;
}
if (RTC_hours != previous_RTC_hours) {
++compensation_counter;
previous_RTC_hours = RTC_hours;
}
if (compensation_counter >= COMPENSATION_THRESHOLD) {
compensation_loaded = 1; // When a set number of hours has passed, a compensation is ready to take place.
compensation_counter = 0;
}
if (COMPENSATION_VALUE > 0 || RTC_seconds >= abs(COMPENSATION_VALUE)) // Prevents RTC_seconds from being driven below 0 or above 59
compensation_all_clear = 1;
else compensation_all_clear = 0;
if (compensation_loaded && compensation_all_clear && COMPENSATION_VALUE >= -59 && COMPENSATION_VALUE <= 59) {
rtc.adjust(DateTime(2022, 3, 14, RTC_hours, RTC_minutes, RTC_seconds + COMPENSATION_VALUE)); // year, month, date, hours, minutes, seconds with compensation value added.
compensation_loaded = 0;
}
}
void manual_settime() { // Settime mode allows setting time manually without neither using IDE nor resetting the MCU.
// This stuff is executed just once, after that function gets caught into a while() loop.
settime_mode = true;
settime_hours = 10;
settime_minutes = 0;
settime_seconds = 0;
Serial.print(settime_hours);
Serial.print(":");
Serial.print(settime_minutes);
Serial.print(":");
Serial.println(settime_seconds);
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
settime_hours_button_wasnt_pressed = !settime_hours_button_is_pressed;
// While settime mode is on.
while (settime_mode) {
// Set hours or turn off settime mode.
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (settime_hours_button_is_pressed && settime_hours_button_wasnt_pressed) {
delay(10);
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (settime_hours_button_is_pressed) {
holding_toggle_available = true;
previous_millis = millis();
while (holding_toggle_available) {
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (!settime_hours_button_is_pressed) {
holding_toggle_available = false;
++settime_hours;
if (settime_hours > 23) {
settime_hours = 0;
}
Serial.print(settime_hours);
Serial.print(":");
Serial.print(settime_minutes);
Serial.print(":");
Serial.println(settime_seconds);
}
if (millis() - previous_millis >= holding_toggle_timing) {
holding_toggle_available = false;
settime_mode = false;
rtc.adjust(DateTime(2022, 3, 14, settime_hours, settime_minutes, settime_seconds)); // year, month, date, hours, minutes, seconds
}
}
}
}
settime_hours_button_wasnt_pressed = !settime_hours_button_is_pressed;
// Set minutes.
settime_minutes_button_is_pressed = !digitalRead(SETTIME_MINUTES_PIN);
if (settime_minutes_button_is_pressed && settime_minutes_button_wasnt_pressed) {
delay(10);
settime_minutes_button_is_pressed = !digitalRead(SETTIME_MINUTES_PIN);
if (settime_minutes_button_is_pressed) {
++settime_minutes;
if (settime_minutes > 59) {
settime_minutes = 0;
}
Serial.print(settime_hours);
Serial.print(":");
Serial.print(settime_minutes);
Serial.print(":");
Serial.println(settime_seconds);
}
}
settime_minutes_button_wasnt_pressed = !settime_minutes_button_is_pressed;
// Same as outside settime while() loop, but calculated from temporary settime numbers, not actual RTC output.
hours_first_digit = settime_hours / 10;
hours_second_digit = settime_hours % 10;
minutes_first_digit = settime_minutes / 10;
minutes_second_digit = settime_minutes % 10;
seconds_first_digit = settime_seconds / 10;
seconds_second_digit = settime_seconds % 10;
// Same as outside settime while() loop.
display_digit(D1_PIN, hours_first_digit, 0);
display_digit(D2_PIN, hours_second_digit, 0); // Decimal point is off to indicate that settime mode is on.
display_digit(D3_PIN, minutes_first_digit, 0);
display_digit(D4_PIN, minutes_second_digit, 0);
}
}