ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
rtc.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file rtc.cpp
6 * @brief DS1307/DS3231 RTC driver - implementation. See rtc.h.
7 *
8 * The date math is H. Hinnant's proleptic-Gregorian days<->civil algorithms, so it is exact
9 * for any date and needs no lookup tables or stdlib time functions.
10 */
11
13#include "protocore_config.h"
14
15#if PC_ENABLE_RTC
16
17#if defined(ARDUINO)
19#include <Wire.h>
20#endif
21namespace
22{
23int bcd2int(uint8_t b)
24{
25 return (b >> 4) * 10 + (b & 0x0F);
26}
27uint8_t int2bcd(int v)
28{
29 return (uint8_t)(((v / 10) << 4) | (v % 10));
30}
31
32// Days from 1970-01-01 for a civil (y, m, d), and its inverse.
33long days_from_civil(int y, int m, int d)
34{
35 y -= m <= 2;
36 long era = (y >= 0 ? y : y - 399) / 400; // GCOVR_EXCL_BR_LINE y<0 unreachable: the only caller
37 // feeds year = 2000 + bcd2int(r[6]) (uint8_t, so
38 // bcd2int >= 0) minus at most 1 for m<=2, so y >= 1999
39 unsigned yoe = (unsigned)(y - era * 400);
40 unsigned doy = (153u * (unsigned)(m + (m > 2 ? -3 : 9)) + 2u) / 5u + (unsigned)d - 1u;
41 unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
42 return era * 146097L + (long)doe - 719468L;
43}
44void civil_from_days(long z, int *y, int *m, int *d)
45{
46 z += 719468;
47 long era = (z >= 0 ? z : z - 146096) / 146097; // GCOVR_EXCL_BR_LINE z<0 unreachable: the only
48 // caller passes days = epoch/86400u, and epoch
49 // is uint32_t so days is always >= 0
50 unsigned doe = (unsigned)(z - era * 146097);
51 unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
52 long yy = (long)yoe + era * 400;
53 unsigned doy = doe - (365u * yoe + yoe / 4u - yoe / 100u);
54 unsigned mp = (5u * doy + 2u) / 153u;
55 *d = (int)(doy - (153u * mp + 2u) / 5u + 1u);
56 *m = (int)(mp < 10 ? mp + 3 : mp - 9);
57 *y = (int)(yy + (*m <= 2));
58}
59} // namespace
60
61bool pc_rtc_regs_to_epoch(const uint8_t r[RTC_REG_COUNT], uint32_t *epoch)
62{
63 if (!r || !epoch)
64 {
65 return false;
66 }
67 int sec = bcd2int(r[0] & 0x7F); // mask the DS1307 clock-halt bit
68 int min = bcd2int(r[1] & 0x7F);
69 int hour;
70 if (r[2] & 0x40) // 12-hour mode
71 {
72 int h12 = bcd2int(r[2] & 0x1F);
73 if (h12 < 1 || h12 > 12)
74 {
75 return false;
76 }
77 bool pm = (r[2] & 0x20) != 0;
78 hour = (h12 % 12) + (pm ? 12 : 0);
79 }
80 else
81 {
82 hour = bcd2int(r[2] & 0x3F);
83 }
84 int date = bcd2int(r[4] & 0x3F);
85 int month = bcd2int(r[5] & 0x1F); // mask the DS3231 century bit
86 int year = 2000 + bcd2int(r[6]);
87 if (sec > 59 || min > 59 || hour > 23 || date < 1 || date > 31 || month < 1 || month > 12)
88 {
89 return false;
90 }
91 // int64: days*86400 exceeds a 32-bit long (Windows host and ESP32 both) past ~2038.
92 int64_t t = (int64_t)days_from_civil(year, month, date) * 86400 + hour * 3600 + min * 60 + sec;
93 if (t < 0 || t > 0xFFFFFFFFLL) // GCOVR_EXCL_BR_LINE t<0 unreachable: year = 2000 + bcd2int(r[6])
94 // is always >= 2000, so days_from_civil (and t) is always positive;
95 // t > 0xFFFFFFFF (year rollover past 2106) is real and tested below
96 {
97 return false;
98 }
99 *epoch = (uint32_t)t;
100 return true;
101}
102
103void pc_rtc_epoch_to_regs(uint32_t epoch, uint8_t r[RTC_REG_COUNT])
104{
105 long days = (long)(epoch / 86400u);
106 int rem = (int)(epoch % 86400u);
107 int y = 0;
108 int m = 0;
109 int d = 0;
110 civil_from_days(days, &y, &m, &d);
111 r[0] = int2bcd(rem % 60);
112 r[1] = int2bcd((rem % 3600) / 60);
113 r[2] = int2bcd(rem / 3600); // 24-hour mode (bit 6 clear)
114 r[3] = (uint8_t)((((days % 7) + 3) % 7) + 1); // 1 = Mon .. 7 = Sun
115 r[4] = int2bcd(d);
116 r[5] = int2bcd(m); // century bit clear
117 r[6] = int2bcd(y - 2000);
118}
119
120// ---------------------------------------------------------------------------
121// I2C binding
122// ---------------------------------------------------------------------------
123
124#if defined(ARDUINO)
125
126bool pc_rtc_begin()
127{
128 pc_i2c_begin();
129 return true;
130}
131
132uint32_t pc_rtc_read_epoch()
133{
134 Wire.beginTransmission(PC_RTC_I2C_ADDR);
135 Wire.write((uint8_t)0x00); // point at register 0 (seconds)
136 if (Wire.endTransmission() != 0)
137 {
138 return 0; // no RTC on the bus
139 }
140 if (Wire.requestFrom((int)PC_RTC_I2C_ADDR, (int)RTC_REG_COUNT) != RTC_REG_COUNT)
141 {
142 return 0;
143 }
144 uint8_t r[RTC_REG_COUNT];
145 for (int i = 0; i < RTC_REG_COUNT; i++)
146 {
147 r[i] = (uint8_t)Wire.read();
148 }
149 uint32_t e = 0;
150 return pc_rtc_regs_to_epoch(r, &e) ? e : 0;
151}
152
153bool pc_rtc_set_epoch(uint32_t epoch)
154{
155 uint8_t r[RTC_REG_COUNT];
156 pc_rtc_epoch_to_regs(epoch, r);
157 Wire.beginTransmission(PC_RTC_I2C_ADDR);
158 Wire.write((uint8_t)0x00);
159 for (int i = 0; i < RTC_REG_COUNT; i++)
160 {
161 Wire.write(r[i]);
162 }
163 return Wire.endTransmission() == 0;
164}
165
166uint32_t pc_rtc_time_source()
167{
168 return pc_rtc_read_epoch();
169}
170
171#else // host build: no I2C. The BCD<->epoch conversions above are host-tested.
172
173bool pc_rtc_begin()
174{
175 return true;
176}
177uint32_t pc_rtc_read_epoch()
178{
179 return 0;
180}
181bool pc_rtc_set_epoch(uint32_t)
182{
183 return false;
184}
185uint32_t pc_rtc_time_source()
186{
187 return 0;
188}
189
190#endif // ARDUINO
191
192#endif // PC_ENABLE_RTC
The one owner of the shared I2C bus bring-up for the peripheral drivers.
void pc_i2c_begin()
Bring up the shared I2C bus on PC_I2C_SDA_PIN / PC_I2C_SCL_PIN (-1 = default).
Definition i2c.h:33
User-facing configuration for ProtoCore.
#define PC_RTC_I2C_ADDR
I2C address of the RTC (DS1307/DS3231 are fixed at 0x68).
I2C real-time-clock driver (DS1307 / DS3231) - a battery-backed time source.
#define RTC_REG_COUNT
Number of time registers read from the RTC (seconds..year).
Definition rtc.h:28
void pc_rtc_epoch_to_regs(uint32_t epoch, uint8_t regs[RTC_REG_COUNT])
Convert a Unix timestamp to the 7 RTC time registers (BCD, 24-hour). Pure - no I2C....
bool pc_rtc_begin()
Initialize the I2C bus for the RTC.
bool pc_rtc_regs_to_epoch(const uint8_t regs[RTC_REG_COUNT], uint32_t *epoch)
Convert the 7 raw RTC time registers (BCD: sec, min, hour, dow, date, month, year) to a Unix timestam...
uint32_t pc_rtc_read_epoch()
Read the current time from the RTC over I2C.
uint32_t pc_rtc_time_source()
A TimeSourceFn wrapper (returns pc_rtc_read_epoch()) to register with pc_time_source_add().
bool pc_rtc_set_epoch(uint32_t epoch)
Set the RTC to epoch over I2C.