DS3231 currently seems to be an ultimate solution for my RTC needs, but I have plenty of DS1307 in my coffers (some of them are pretty inaccurate, like 90 ppm ahead of time), so I'd like to find a way to use them effectlively.
There are similar topics on the web, but I'd like to ask to check my code so that I could understand if I'd been able to find a proper solution by myself. The sketch works, I've tried that out, but nevertheless I feel the need to draw some feedback. Can I do something better? Didn't I overlook something?, etc.
Time drift compensation statements are at the very end of the sketch. Basically sketch counts up every hour and each 12 hours it subtracts 4 seconds from current RTC time. The value (4 sec) was found experimentally. This sketch is written in mind with following:
– it's made for room clock which don't get turned off too often;
– using another DS1307 module will require measuring its item-specific drift experimentally;
– overall compensation is not very accurate, it can suffer from temperature etc. But if time drift will be more or less consistent, constant 4 sec offset still makes things somewhat better.
So yes, in this case it's an attempt to turn terrible case (90+ ppm) into a mediocre one (around 20 ppm).
One more thing. I'm just beginning to learn how to write readable and consistent code, therefore any general input on my sketch is appreciated. At the very least I have a feeling I'm overusing global variables, but I'm not sure if it's really bad.
// This RTC sketch 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 <Adafruit_SPIDevice.h>
#include <RTClib.h>
// This sketch works for both DS3231 and DS1307 RTCs. Applicable variant must be uncommented to create an object.
// RTC_DS3231 rtc;
RTC_DS1307 rtc;
// Pins which drive transistors connecting common cathodes to GND
// and thus switch the active display section ("D" is for "digit").
int D1_PIN = 9;
int D2_PIN = 8;
int D3_PIN = 7;
int D4_PIN = 6;
// Pins for interaction with 74HC595 latched shift register IC.
// The 595 is used to create an output parallel set of signals ("byte mask")
// that lights up only necessary segments at any given time.
int DATA_PIN = 4;
int LATCH_PIN = 2;
int CLOCK_PIN = 3;
// For storing numbers from RTC output.
int RTC_hours;
int RTC_minutes;
int RTC_seconds;
// For storing digits to be displayed.
int hours_first_digit;
int hours_second_digit;
int minutes_first_digit;
int minutes_second_digit;
int seconds_first_digit;
int seconds_second_digit;
// Optional. Used to start a counter which sends RTC output through UART once per second.
int previous_RTC_seconds;
bool initial_RTC_seconds_was_stored = 0;
// GLOBAL VARIABLES USED IN SETTIME MODE
// Settime mode allows setting time manually without neither using IDE nor resetting the MCU.
// Used to begin and end "while" loops during which time may be set.
bool settime_mode = 0;
// Pins for active-low manual input buttons.
int SETTIME_TOGGLE_PIN = 10;
int SETTIME_HOURS_PIN = 11;
int SETTIME_MINUTES_PIN = 12;
// For button debounce.
bool settime_toggle_button_is_pressed;
bool settime_toggle_button_wasnt_pressed;
bool settime_minutes_button_is_pressed;
bool settime_minutes_button_wasnt_pressed;
bool settime_hours_button_is_pressed;
bool settime_hours_button_wasnt_pressed;
// For storing temporary numbers to be loaded into RTC when settime mode is turned off.
int settime_hours;
int settime_minutes;
int settime_seconds;
// GLOBAL VARIABLES USED FOR TIME DRIFT COMPENSATION
int previous_RTC_hours;
bool initial_RTC_hours_was_stored = 0;
int compensation_counter = 0;
int compensation_threshold = 12;
bool compensation_cocked = 0;
int compensation_value = 4;
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, 2, 20, 3, 39, 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_TOGGLE_PIN, INPUT_PULLUP);
pinMode(SETTIME_HOURS_PIN, INPUT_PULLUP);
pinMode(SETTIME_MINUTES_PIN, INPUT_PULLUP);
// Prevents artifacts from being displayed during 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);
}
// User-defined function dedicated to actually displaying digits.
void display_digit(int current_cathode, int digit_to_display, bool whether_dot_is_used) {
// Defines which display section (digit) is to be turned on right now.
bool cathode_1;
if (current_cathode == D1_PIN) {
cathode_1 = 1;
} else cathode_1 = 0;
digitalWrite(D1_PIN, cathode_1);
bool cathode_2;
if (current_cathode == D2_PIN) {
cathode_2 = 1;
} else cathode_2 = 0;
digitalWrite(D2_PIN, cathode_2);
bool cathode_3;
if (current_cathode == D3_PIN) {
cathode_3 = 1;
} else cathode_3 = 0;
digitalWrite(D3_PIN, cathode_3);
bool cathode_4;
if (current_cathode == D4_PIN) {
cathode_4 = 1;
} else cathode_4 = 0;
digitalWrite(D4_PIN, cathode_4);
// Byte maska 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[] = {
0b11011101,
0b00001100,
0b11000111,
0b11001110,
0b00011110,
0b11011010,
0b11011011,
0b01001100,
0b11011111,
0b11011110
};
// If third argument of function is 1, decimal point ("dot") will blink once per second.
bool dot_state;
uint8_t dot_byte = 0; // An addition to an appropriate matrix byte which makes (or doesn't make) decimal point bit become 1.
if (whether_dot_is_used) {
dot_state = seconds_second_digit % 2; // Every other second the dot blinks.
if (dot_state) {
dot_byte = 0b00100000;
} 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 loop() {
DateTime now = rtc.now();
RTC_hours = now.hour(), DEC;
RTC_minutes = now.minute(), DEC;
RTC_seconds = now.second(), DEC;
// Division and modulo operators are used
// to divide 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);
// Optional. Begin counting seconds as a clock for sending data via UART.
if (!initial_RTC_seconds_was_stored) {
previous_RTC_seconds = RTC_seconds;
initial_RTC_seconds_was_stored = 1;
}
// Optional. Used for sending RTC output via UART.
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;
}
// SETTIME MODE
// Turn on settime mode
settime_toggle_button_is_pressed = !digitalRead(SETTIME_TOGGLE_PIN);
if (settime_toggle_button_is_pressed && settime_toggle_button_wasnt_pressed) {
delay(10);
if (settime_toggle_button_is_pressed) {
settime_seconds = 0;
settime_minutes = 0;
settime_hours = 0;
Serial.print(settime_hours);
Serial.print(":");
Serial.print(settime_minutes);
Serial.print(":");
Serial.println(settime_seconds);
settime_mode = !settime_mode;
}
}
settime_toggle_button_wasnt_pressed = !settime_toggle_button_is_pressed;
// While settime mode is on.
while (settime_mode) {
settime_hours_button_is_pressed = !digitalRead(SETTIME_HOURS_PIN);
if (settime_hours_button_is_pressed && settime_hours_button_wasnt_pressed) {
delay(10);
if (settime_hours_button_is_pressed) {
++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);
}
}
settime_hours_button_wasnt_pressed = !settime_hours_button_is_pressed;
settime_minutes_button_is_pressed = !digitalRead(SETTIME_MINUTES_PIN);
if (settime_minutes_button_is_pressed && settime_minutes_button_wasnt_pressed) {
delay(10);
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 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);
// Turn off settime mode.
settime_toggle_button_is_pressed = !digitalRead(SETTIME_TOGGLE_PIN);
if (settime_toggle_button_is_pressed && settime_toggle_button_wasnt_pressed) {
delay(10);
if (settime_toggle_button_is_pressed) {
rtc.adjust(DateTime(2022, 2, 20, settime_hours, settime_minutes, settime_seconds)); // year, month, date, hours, minutes, seconds
settime_mode = !settime_mode;
}
}
settime_toggle_button_wasnt_pressed = !settime_toggle_button_is_pressed;
}
// TIME DRIFT COMPENSATION
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_cocked = 1; // When set number of hours has passed, a compensation is ready to take place.
compensation_counter = 0;
}
if (RTC_seconds >= compensation_value && compensation_cocked) { // Wait until current RTC_seconds equals compensation value to avoid setting negative RTC time.
rtc.adjust(DateTime(2022, 2, 20, RTC_hours, RTC_minutes, RTC_seconds - compensation_value)); // year, month, date, hours, minutes, seconds with compensation value subtracted.
compensation_cocked = 0;
}
}