From e32ae14b6c3e5cb86c6b8675917843b4a7f015dc Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 08:02:37 -0700 Subject: [PATCH 1/5] Adopt an RTC only if its time registers read like that chip AutoDiscoverRTCClock::begin() adopted each RTC on an I2C ACK alone, so any device answering at 0x68, 0x52, 0x51 or 0x32 (an IMU, a 24-series EEPROM) became the clock: read as the time, and written on every sync. Upstream already worked around this per board with DISABLE_DS3231_PROBE for IMUs at 0x68. Read the seven time registers and skip the device if they cannot belong to that chip, per its datasheet: bits documented as always 0 for the DS3231, RV3028 and RX8130CE; documented field ranges for the PCF8563, whose unused bits are undefined, accepting it while VL is set since its time is then undefined. Two reads must both rule a device out; a failed read keeps today's behaviour. Co-Authored-By: Claude Opus 5.5 --- src/helpers/AutoDiscoverRTCClock.cpp | 82 ++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index af08fddb03..067ea9229d 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -26,14 +26,86 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) { return (error == 0); } +// An ACK only says that something answers at an RTC's address. An IMU at 0x68 or an EEPROM in +// 0x50-0x57 answers too, and would then be read as the clock and written on every time sync. +// So each RTC is adopted only if its seven time registers read like that chip's, per the +// datasheets cited below. + +static bool rtcReadTime(TwoWire& wire, uint8_t addr, uint8_t reg, uint8_t t[7]) { + wire.beginTransmission(addr); + wire.write(reg); + if (wire.endTransmission() != 0) return false; + if (wire.requestFrom(addr, (uint8_t)7) != 7) return false; + for (int i = 0; i < 7; i++) t[i] = wire.read(); + return true; +} + +// True if two reads of the time registers both rule the device out. A failed read proves +// nothing, so the device is adopted as before, and one corrupted read cannot hide a real RTC. +static bool rtcRuledOut(TwoWire& wire, uint8_t addr, uint8_t reg, + bool (*ruled_out)(const uint8_t t[7])) { + for (int i = 0; i < 2; i++) { + uint8_t t[7]; + if (!rtcReadTime(wire, addr, reg, t) || !ruled_out(t)) return false; + } + MESH_DEBUG_PRINTLN("RTC: device at 0x%02X does not read like the RTC expected there", addr); + return true; +} + +static bool anyBitSet(const uint8_t t[7], const uint8_t zero[7]) { + for (int i = 0; i < 7; i++) { + if (t[i] & zero[i]) return true; + } + return false; +} + +// DS3231, 00h-06h: bits shown as 0 in Figure 1 (Maxim 19-5170 Rev 10, p. 11) +static bool ds3231RuledOut(const uint8_t t[7]) { + static const uint8_t zero[7] = { 0x80, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }; + return anyBitSet(t, zero); +} + +// RV3028, 00h-06h: bits that always read 0 (RV-3028-C7 App Manual Rev 1.4, 3.2-3.3, pp. 12-14) +static bool rv3028RuledOut(const uint8_t t[7]) { + static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }; + return anyBitSet(t, zero); +} + +// RX8130CE, 10h-16h: bits whose "read value is always 0" (Epson ETM50E-10, 13.2.1, p. 22) +static bool rx8130ceRuledOut(const uint8_t t[7]) { + static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }; + return anyBitSet(t, zero); +} + +static bool bcdInRange(uint8_t v, uint8_t lo, uint8_t hi) { + if ((v & 0x0F) > 9 || (v >> 4) > 9) return false; + uint8_t d = (v >> 4) * 10 + (v & 0x0F); + return d >= lo && d <= hi; +} + +// PCF8563, 02h-08h. Its unused bits are "not relevant" rather than 0, so this checks the field +// ranges instead (NXP PCF8563 Rev 11.1, Table 4, p. 10). With VL set (02h bit 7) the time is +// undefined (Table 27, p. 24), so a chip that lost power is still adopted and can be set. +static bool pcf8563RuledOut(const uint8_t t[7]) { + bool all_ff = true; + for (int i = 0; i < 7; i++) all_ff = all_ff && t[i] == 0xFF; + if (all_ff) return true; // an erased EEPROM, not a chip whose VL is set + if (t[0] & 0x80) return false; + return !(bcdInRange(t[0] & 0x7F, 0, 59) && bcdInRange(t[1] & 0x7F, 0, 59) + && bcdInRange(t[2] & 0x3F, 0, 23) && bcdInRange(t[3] & 0x3F, 1, 31) + && (t[4] & 0x07) <= 6 && bcdInRange(t[5] & 0x1F, 1, 12) && bcdInRange(t[6], 0, 99)); +} + void AutoDiscoverRTCClock::begin(TwoWire& wire) { #if !defined(DISABLE_DS3231_PROBE) - if (i2c_probe(wire, DS3231_ADDRESS)) { + if (i2c_probe(wire, DS3231_ADDRESS) + && !rtcRuledOut(wire, DS3231_ADDRESS, 0x00, ds3231RuledOut)) { ds3231_success = rtc_3231.begin(&wire); } #endif - if (i2c_probe(wire, RV3028_ADDRESS)) { + if (i2c_probe(wire, RV3028_ADDRESS) + && !rtcRuledOut(wire, RV3028_ADDRESS, 0x00, rv3028RuledOut)) { rtc_rv3028.initI2C(wire); rtc_rv3028.writeToRegister(0x35, 0x00); rtc_rv3028.writeToRegister(0x37, 0xB4); // Direct Switching Mode (DSM): when VDD < VBACKUP, switchover occurs from VDD to VBACKUP @@ -41,12 +113,14 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) { rv3028_success = true; } - if (i2c_probe(wire, PCF8563_ADDRESS)) { + if (i2c_probe(wire, PCF8563_ADDRESS) + && !rtcRuledOut(wire, PCF8563_ADDRESS, 0x02, pcf8563RuledOut)) { MESH_DEBUG_PRINTLN("PCF8563: Found"); rtc_8563_success = rtc_8563.begin(&wire); } - if (i2c_probe(wire, RX8130CE_ADDRESS)) { + if (i2c_probe(wire, RX8130CE_ADDRESS) + && !rtcRuledOut(wire, RX8130CE_ADDRESS, 0x10, rx8130ceRuledOut)) { MESH_DEBUG_PRINTLN("RX8130CE: Found"); rtc_8130.begin(&wire); rtc_8130_success = true; From 58319ad7a38f64b67fe6c973cd0a2f9e83cc9806 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 08:17:42 -0700 Subject: [PATCH 2/5] Require valid time fields unless the RTC's power-loss flag is set Hardware testing showed the always-zero bits alone pass a register block of zeros, such as an LPS22HB pressure sensor's. Also require seconds, minutes, date and month to be valid BCD in range, unless the chip's documented power-loss flag (DS3231 OSF, RV3028 PORF, PCF8563 VL, RX8130CE VLF) is set, as each sets it at power-up when its time is undefined. From review: read with a repeated start, the only form the RX8130CE manual documents; never check the year, which MeshCore itself can write out of range from a bad epoch; compile the DS3231 table out with DISABLE_DS3231_PROBE; cite the RV3028 overview on p. 12; and state that a device whose bytes happen to fit is still adopted. Co-Authored-By: Claude Opus 5.5 --- src/helpers/AutoDiscoverRTCClock.cpp | 123 ++++++++++++++------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index 067ea9229d..73d266bd9c 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -26,86 +26,91 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) { return (error == 0); } -// An ACK only says that something answers at an RTC's address. An IMU at 0x68 or an EEPROM in +// An ACK only says that something answers at an RTC's address: an IMU at 0x68 or an EEPROM in // 0x50-0x57 answers too, and would then be read as the clock and written on every time sync. -// So each RTC is adopted only if its seven time registers read like that chip's, per the -// datasheets cited below. - -static bool rtcReadTime(TwoWire& wire, uint8_t addr, uint8_t reg, uint8_t t[7]) { +// So an RTC is adopted only if its time registers read like that chip's, per its datasheet: +// - bits documented as always 0 must read 0 (the PCF8563 documents none); +// - seconds, minutes, date and month must be valid BCD in range, unless the chip's power-loss +// flag is set, which every one of these chips sets at power-up, when its time is undefined. +// The year is not checked, as MeshCore can write an out-of-range one from a bad epoch. This +// cannot catch every device: one whose bytes happen to fit is still adopted, as before. +struct RtcId { + uint8_t time_reg; // seconds register; the seven time registers follow it + uint8_t zero[7]; // bits that always read 0 + uint8_t date_idx; // index of the date register within the seven + uint8_t flag_reg; // power-loss flag register and bit + uint8_t flag_bit; +}; + +#if !defined(DISABLE_DS3231_PROBE) +// DS3231 (Maxim 19-5170 Rev 10): Figure 1, p. 11; OSF in Status (0Fh) bit 7, p. 14 +static const RtcId DS3231_ID = + { 0x00, { 0x80, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }, 4, 0x0F, 0x80 }; +#endif +// RV3028 (RV-3028-C7 App Manual Rev 1.4): 3.2, p. 12; PORF in Status (0Eh) bit 0, 3.7, p. 22 +static const RtcId RV3028_ID = + { 0x00, { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }, 4, 0x0E, 0x01 }; +// PCF8563 (NXP Rev 11.1): unused bits are "not relevant", not 0, Table 4, p. 10; VL in +// VL_seconds (02h) bit 7, Table 8, p. 13, and set at power-up, Table 27, p. 24 +static const RtcId PCF8563_ID = { 0x02, { 0 }, 3, 0x02, 0x80 }; +// RX8130CE (Epson ETM50E-10): read value always 0, 13.2.1, p. 22; VLF in Flag (1Dh) bit 1, +// 13.3.5, p. 25, and set at power-up, 13.2.2, p. 23 +static const RtcId RX8130CE_ID = + { 0x10, { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }, 4, 0x1D, 0x02 }; + +// Reads n registers from reg, with a repeated start as every one of these datasheets documents +static bool rtcRead(TwoWire& wire, uint8_t addr, uint8_t reg, uint8_t* buf, uint8_t n) { wire.beginTransmission(addr); wire.write(reg); - if (wire.endTransmission() != 0) return false; - if (wire.requestFrom(addr, (uint8_t)7) != 7) return false; - for (int i = 0; i < 7; i++) t[i] = wire.read(); - return true; -} - -// True if two reads of the time registers both rule the device out. A failed read proves -// nothing, so the device is adopted as before, and one corrupted read cannot hide a real RTC. -static bool rtcRuledOut(TwoWire& wire, uint8_t addr, uint8_t reg, - bool (*ruled_out)(const uint8_t t[7])) { - for (int i = 0; i < 2; i++) { - uint8_t t[7]; - if (!rtcReadTime(wire, addr, reg, t) || !ruled_out(t)) return false; - } - MESH_DEBUG_PRINTLN("RTC: device at 0x%02X does not read like the RTC expected there", addr); + if (wire.endTransmission(false) != 0) return false; + if (wire.requestFrom(addr, n) != n) return false; + for (uint8_t i = 0; i < n; i++) buf[i] = wire.read(); return true; } -static bool anyBitSet(const uint8_t t[7], const uint8_t zero[7]) { - for (int i = 0; i < 7; i++) { - if (t[i] & zero[i]) return true; - } - return false; -} - -// DS3231, 00h-06h: bits shown as 0 in Figure 1 (Maxim 19-5170 Rev 10, p. 11) -static bool ds3231RuledOut(const uint8_t t[7]) { - static const uint8_t zero[7] = { 0x80, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }; - return anyBitSet(t, zero); -} - -// RV3028, 00h-06h: bits that always read 0 (RV-3028-C7 App Manual Rev 1.4, 3.2-3.3, pp. 12-14) -static bool rv3028RuledOut(const uint8_t t[7]) { - static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }; - return anyBitSet(t, zero); -} - -// RX8130CE, 10h-16h: bits whose "read value is always 0" (Epson ETM50E-10, 13.2.1, p. 22) -static bool rx8130ceRuledOut(const uint8_t t[7]) { - static const uint8_t zero[7] = { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }; - return anyBitSet(t, zero); -} - static bool bcdInRange(uint8_t v, uint8_t lo, uint8_t hi) { if ((v & 0x0F) > 9 || (v >> 4) > 9) return false; uint8_t d = (v >> 4) * 10 + (v & 0x0F); return d >= lo && d <= hi; } -// PCF8563, 02h-08h. Its unused bits are "not relevant" rather than 0, so this checks the field -// ranges instead (NXP PCF8563 Rev 11.1, Table 4, p. 10). With VL set (02h bit 7) the time is -// undefined (Table 27, p. 24), so a chip that lost power is still adopted and can be set. -static bool pcf8563RuledOut(const uint8_t t[7]) { +// 1 if this read rules the device out, 0 if not, -1 if a read failed +static int rtcCheck(TwoWire& wire, uint8_t addr, const RtcId& id) { + uint8_t t[7]; + if (!rtcRead(wire, addr, id.time_reg, t, 7)) return -1; bool all_ff = true; - for (int i = 0; i < 7; i++) all_ff = all_ff && t[i] == 0xFF; - if (all_ff) return true; // an erased EEPROM, not a chip whose VL is set - if (t[0] & 0x80) return false; - return !(bcdInRange(t[0] & 0x7F, 0, 59) && bcdInRange(t[1] & 0x7F, 0, 59) - && bcdInRange(t[2] & 0x3F, 0, 23) && bcdInRange(t[3] & 0x3F, 1, 31) - && (t[4] & 0x07) <= 6 && bcdInRange(t[5] & 0x1F, 1, 12) && bcdInRange(t[6], 0, 99)); + for (int i = 0; i < 7; i++) { + if (t[i] & id.zero[i]) return 1; + all_ff = all_ff && t[i] == 0xFF; + } + if (all_ff) return 1; // an erased EEPROM + if (bcdInRange(t[0] & 0x7F, 0, 59) && bcdInRange(t[1] & 0x7F, 0, 59) + && bcdInRange(t[id.date_idx] & 0x3F, 1, 31) && bcdInRange(t[5] & 0x1F, 1, 12)) { + return 0; + } + uint8_t flag; + if (!rtcRead(wire, addr, id.flag_reg, &flag, 1)) return -1; + return (flag & id.flag_bit) ? 0 : 1; // power was lost, so the time is undefined +} + +// True if two reads both rule the device out. A failed read proves nothing, so the device is +// adopted as before, and one corrupted read cannot hide a real RTC. +static bool rtcRuledOut(TwoWire& wire, uint8_t addr, const RtcId& id) { + if (rtcCheck(wire, addr, id) != 1 || rtcCheck(wire, addr, id) != 1) return false; + MESH_DEBUG_PRINTLN("RTC: device at 0x%02X does not read like the RTC expected there", addr); + return true; } void AutoDiscoverRTCClock::begin(TwoWire& wire) { #if !defined(DISABLE_DS3231_PROBE) if (i2c_probe(wire, DS3231_ADDRESS) - && !rtcRuledOut(wire, DS3231_ADDRESS, 0x00, ds3231RuledOut)) { + && !rtcRuledOut(wire, DS3231_ADDRESS, DS3231_ID)) { ds3231_success = rtc_3231.begin(&wire); } #endif if (i2c_probe(wire, RV3028_ADDRESS) - && !rtcRuledOut(wire, RV3028_ADDRESS, 0x00, rv3028RuledOut)) { + && !rtcRuledOut(wire, RV3028_ADDRESS, RV3028_ID)) { rtc_rv3028.initI2C(wire); rtc_rv3028.writeToRegister(0x35, 0x00); rtc_rv3028.writeToRegister(0x37, 0xB4); // Direct Switching Mode (DSM): when VDD < VBACKUP, switchover occurs from VDD to VBACKUP @@ -114,13 +119,13 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) { } if (i2c_probe(wire, PCF8563_ADDRESS) - && !rtcRuledOut(wire, PCF8563_ADDRESS, 0x02, pcf8563RuledOut)) { + && !rtcRuledOut(wire, PCF8563_ADDRESS, PCF8563_ID)) { MESH_DEBUG_PRINTLN("PCF8563: Found"); rtc_8563_success = rtc_8563.begin(&wire); } if (i2c_probe(wire, RX8130CE_ADDRESS) - && !rtcRuledOut(wire, RX8130CE_ADDRESS, 0x10, rx8130ceRuledOut)) { + && !rtcRuledOut(wire, RX8130CE_ADDRESS, RX8130CE_ID)) { MESH_DEBUG_PRINTLN("RX8130CE: Found"); rtc_8130.begin(&wire); rtc_8130_success = true; From caa8def8bce93a3a5f7f22910d0050a750fdf7d2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 08:22:34 -0700 Subject: [PATCH 3/5] Skip the time-field check for the RX8130CE RTC_RX8130CE::begin() clears the flag register, VLF included, on every boot without setting the time. A chip powered up with undefined time was adopted on the first boot, then ruled out on the next one if no sync came in between, and never adopted again. Check only its always-zero bits. Also correct the comment: the RV3028's time is defined at reset, and an all-0xFF block is ruled out even with VL set. Co-Authored-By: Claude Opus 5.5 --- src/helpers/AutoDiscoverRTCClock.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index 73d266bd9c..b08cb244a1 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -29,16 +29,17 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) { // An ACK only says that something answers at an RTC's address: an IMU at 0x68 or an EEPROM in // 0x50-0x57 answers too, and would then be read as the clock and written on every time sync. // So an RTC is adopted only if its time registers read like that chip's, per its datasheet: -// - bits documented as always 0 must read 0 (the PCF8563 documents none); +// - bits documented as always 0 must read 0 (the PCF8563 documents none), and the seven +// registers must not all read 0xFF, as an erased EEPROM does; // - seconds, minutes, date and month must be valid BCD in range, unless the chip's power-loss -// flag is set, which every one of these chips sets at power-up, when its time is undefined. +// flag is set: each of these chips sets it at power-up, when its time may be undefined. // The year is not checked, as MeshCore can write an out-of-range one from a bad epoch. This // cannot catch every device: one whose bytes happen to fit is still adopted, as before. struct RtcId { uint8_t time_reg; // seconds register; the seven time registers follow it uint8_t zero[7]; // bits that always read 0 uint8_t date_idx; // index of the date register within the seven - uint8_t flag_reg; // power-loss flag register and bit + uint8_t flag_reg; // power-loss flag register and bit; flag_bit 0 skips the field check uint8_t flag_bit; }; @@ -53,10 +54,11 @@ static const RtcId RV3028_ID = // PCF8563 (NXP Rev 11.1): unused bits are "not relevant", not 0, Table 4, p. 10; VL in // VL_seconds (02h) bit 7, Table 8, p. 13, and set at power-up, Table 27, p. 24 static const RtcId PCF8563_ID = { 0x02, { 0 }, 3, 0x02, 0x80 }; -// RX8130CE (Epson ETM50E-10): read value always 0, 13.2.1, p. 22; VLF in Flag (1Dh) bit 1, -// 13.3.5, p. 25, and set at power-up, 13.2.2, p. 23 +// RX8130CE (Epson ETM50E-10): read value always 0, 13.2.1, p. 22. No field check: its power-loss +// flag VLF (1Dh bit 1) cannot vouch for the fields, as RTC_RX8130CE::begin() clears it on every +// boot without setting the time. static const RtcId RX8130CE_ID = - { 0x10, { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }, 4, 0x1D, 0x02 }; + { 0x10, { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }, 4, 0x1D, 0x00 }; // Reads n registers from reg, with a repeated start as every one of these datasheets documents static bool rtcRead(TwoWire& wire, uint8_t addr, uint8_t reg, uint8_t* buf, uint8_t n) { @@ -84,6 +86,7 @@ static int rtcCheck(TwoWire& wire, uint8_t addr, const RtcId& id) { all_ff = all_ff && t[i] == 0xFF; } if (all_ff) return 1; // an erased EEPROM + if (id.flag_bit == 0) return 0; if (bcdInRange(t[0] & 0x7F, 0, 59) && bcdInRange(t[1] & 0x7F, 0, 59) && bcdInRange(t[id.date_idx] & 0x3F, 1, 31) && bcdInRange(t[5] & 0x1F, 1, 12)) { return 0; From 0aacafa4894c78c0e90671c54eb00f6e96bb7c90 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 08:23:24 -0700 Subject: [PATCH 4/5] Note the RX8130CE exception in the RTC identity comment Co-Authored-By: Claude Opus 5.5 --- src/helpers/AutoDiscoverRTCClock.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index b08cb244a1..9ffae1e4f9 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -32,7 +32,8 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) { // - bits documented as always 0 must read 0 (the PCF8563 documents none), and the seven // registers must not all read 0xFF, as an erased EEPROM does; // - seconds, minutes, date and month must be valid BCD in range, unless the chip's power-loss -// flag is set: each of these chips sets it at power-up, when its time may be undefined. +// flag is set: each of these chips sets it at power-up, when its time may be undefined +// (not checked for the RX8130CE, see its entry below). // The year is not checked, as MeshCore can write an out-of-range one from a bad epoch. This // cannot catch every device: one whose bytes happen to fit is still adopted, as before. struct RtcId { From c36cb9ee5d51f884cbbdda2e6d322bdfd65080ce Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 4 Oct 2026 09:17:36 -0700 Subject: [PATCH 5/5] Do not mask 00h bit 7 at 0x68, so a new DS1307 is still adopted A DS1307 also answers at 0x68 and is driven by the DS3231 code. Its 00h bit 7 is CH, clock halt, set on first application of power (Maxim DS1307 Rev 3/15, p. 8), so the DS3231 mask ruled out every new DS1307 and its clock was never started. Drop that bit; the DS1307's other fixed bits read as 0 (its Table 2, p. 8) and match the DS3231's. Found by cross-checking the ZephCore implementation. Co-Authored-By: Claude Opus 5.5 --- src/helpers/AutoDiscoverRTCClock.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index 9ffae1e4f9..53e80e15a3 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -45,9 +45,11 @@ struct RtcId { }; #if !defined(DISABLE_DS3231_PROBE) -// DS3231 (Maxim 19-5170 Rev 10): Figure 1, p. 11; OSF in Status (0Fh) bit 7, p. 14 +// DS3231 (Maxim 19-5170 Rev 10): Figure 1, p. 11; OSF in Status (0Fh) bit 7, p. 14. 00h bit 7 +// is left out: a DS1307 at 0x68, which this code also drives, powers up with its clock-halt bit +// there set (Maxim DS1307 Rev 3/15, Table 2 and text, p. 8). Its other bits match. static const RtcId DS3231_ID = - { 0x00, { 0x80, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }, 4, 0x0F, 0x80 }; + { 0x00, { 0x00, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }, 4, 0x0F, 0x80 }; #endif // RV3028 (RV-3028-C7 App Manual Rev 1.4): 3.2, p. 12; PORF in Status (0Eh) bit 0, 3.7, p. 22 static const RtcId RV3028_ID =