[time] Make the BCD calendar helpers a template over the chip's layout

This commit is contained in:
J. Nick Koston
2026-10-20 03:57:55 -10:00
parent c0cbf9cdfd
commit e12e49119d
7 changed files with 149 additions and 60 deletions
+2 -2
View File
@@ -212,8 +212,8 @@ file does, and it is the authority when they disagree. The most useful starting
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`); pick the helper's
arguments instead of writing a loop. Epson style 7-byte RTC calendars (one-hot weekday) use
`time::decode_bcd_calendar`.
arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>` and
`encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
+2 -2
View File
@@ -59,7 +59,7 @@ void RX8025TComponent::read_time() {
return;
}
ESPTime rtc_time = time::decode_bcd_calendar(date);
ESPTime rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(date);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
@@ -77,7 +77,7 @@ void RX8025TComponent::write_time() {
}
uint8_t buff[7];
time::encode_bcd_calendar(now, buff);
time::encode_bcd_calendar<time::EPSON_BCD_CALENDAR>(now, buff);
if (!this->write_bytes(RX8025T_REG_SEC, buff, sizeof(buff))) {
ESP_LOGE(TAG, "Can't write I2C data.");
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
+2 -2
View File
@@ -60,7 +60,7 @@ void RX8111Component::read_time() {
this->status_set_warning(LOG_STR("Invalid RTC time"));
return;
}
ESPTime rtc_time = time::decode_bcd_calendar(buf);
ESPTime rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
if (!rtc_time.is_valid(true, false) || rtc_time.second >= 60) {
ESP_LOGW(TAG, "Invalid RTC time, not syncing to system clock");
this->status_set_warning(LOG_STR("Invalid RTC time"));
@@ -88,7 +88,7 @@ void RX8111Component::write_time() {
}
uint8_t buf[7];
time::encode_bcd_calendar(now, buf);
time::encode_bcd_calendar<time::EPSON_BCD_CALENDAR>(now, buf);
const bool written = this->write_bytes(RX8111_REG_SEC, buf, sizeof(buf));
// Always try to restart the counter, even after a failed calendar write.
const bool restarted = this->set_stop_bit_(false);
+2 -2
View File
@@ -67,7 +67,7 @@ void RX8130Component::read_time() {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
ESPTime rtc_time = time::decode_bcd_calendar(date);
ESPTime rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(date);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
@@ -84,7 +84,7 @@ void RX8130Component::write_time() {
return;
}
uint8_t buff[7];
time::encode_bcd_calendar(now, buff);
time::encode_bcd_calendar<time::EPSON_BCD_CALENDAR>(now, buff);
this->stop_(true);
if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
@@ -107,37 +107,4 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
this->time_sync_callback_.call();
}
// Bits of each calendar byte that hold the value, byte 0 lowest; byte 3 is the one-hot weekday.
// Packed into one constant so it stays in code rather than RAM on ESP8266.
static constexpr uint64_t BCD_CALENDAR_MASKS = 0xFF1F3F7F3F7F7FULL;
ESPTime decode_bcd_calendar(const uint8_t *raw) {
uint8_t value[7];
for (size_t i = 0; i < 7; i++)
value[i] = bcd_to_uint8(raw[i] & static_cast<uint8_t>(BCD_CALENDAR_MASKS >> (8 * i)));
const uint8_t weekday = raw[3] & static_cast<uint8_t>(BCD_CALENDAR_MASKS >> 24);
ESPTime time{
.second = value[0],
.minute = value[1],
.hour = value[2],
.day_of_week = static_cast<uint8_t>(weekday ? __builtin_ctz(weekday) + 1 : 1),
.day_of_month = value[4],
.day_of_year = 1,
.month = value[5],
.year = static_cast<uint16_t>(value[6] + 2000),
.is_dst = false,
.timestamp = 0,
};
time.recalc_timestamp_utc(false);
return time;
}
void encode_bcd_calendar(const ESPTime &time, uint8_t *raw) {
const uint8_t value[7] = {
time.second, time.minute, time.hour, 0, time.day_of_month, time.month, static_cast<uint8_t>(time.year % 100)};
for (size_t i = 0; i < 7; i++)
raw[i] = uint8_to_bcd(value[i]);
raw[3] = 1 << (time.day_of_week - 1);
}
} // namespace esphome::time
+90 -5
View File
@@ -44,10 +44,95 @@ class RealTimeClock : public PollingComponent {
LazyCallbackManager<void()> time_sync_callback_;
};
/// Decode the 7-byte BCD calendar (second, minute, hour, one-hot weekday, day, month, year since 2000)
/// shared by Epson RTCs such as the RX8025T, RX8111 and RX8130, and fill in the UTC timestamp.
ESPTime decode_bcd_calendar(const uint8_t *raw);
/// Encode @p time into that 7-byte layout; @p time must be valid (is_valid()), with the year 2000 to 2099.
void encode_bcd_calendar(const ESPTime &time, uint8_t *raw);
/// How the weekday byte of a BCD calendar names the day, Sunday first.
enum class BcdWeekday : uint8_t {
ONE_HOT, ///< one bit per day, bit 0 is Sunday
FROM_1, ///< 1 is Sunday
FROM_0, ///< 0 is Sunday
};
/// Layout of a 7-byte BCD calendar: second, minute, hour, weekday and day of the month in either order,
/// month and year since 2000.
struct BcdCalendarLayout {
BcdWeekday weekday;
bool weekday_before_day{true};
/// Bit 7 of the month byte is set from the year 2100 on. Chips without the bit read it as 0.
bool century_bit{false};
/// Bit 6 of the hour byte selects 12-hour mode, with PM in bit 5; decoded only, encoding writes 24-hour mode.
bool hour_12h{false};
constexpr size_t weekday_index() const { return this->weekday_before_day ? 3 : 4; }
constexpr size_t day_index() const { return this->weekday_before_day ? 4 : 3; }
};
/// Epson RX8025T, RX8111, RX8130.
inline constexpr BcdCalendarLayout EPSON_BCD_CALENDAR{.weekday = BcdWeekday::ONE_HOT};
/// DS1307, DS3231.
inline constexpr BcdCalendarLayout DS_BCD_CALENDAR{
.weekday = BcdWeekday::FROM_1, .century_bit = true, .hour_12h = true};
/// PCF8563, PCF85063, BM8563.
inline constexpr BcdCalendarLayout PCF_BCD_CALENDAR{.weekday = BcdWeekday::FROM_0, .weekday_before_day = false};
/// Decode the 7-byte BCD calendar at @p raw in the given layout and fill in the UTC timestamp. Bits each byte
/// does not use for its value, such as a chip's flag bits, are dropped; day_of_year is not worked out.
/// Kept out of line so that drivers sharing a layout share one copy.
template<BcdCalendarLayout LAYOUT> __attribute__((noinline)) ESPTime decode_bcd_calendar(const uint8_t *raw) {
uint8_t day_of_week;
if constexpr (LAYOUT.weekday == BcdWeekday::ONE_HOT) {
const uint8_t weekday = raw[LAYOUT.weekday_index()] & 0x7F;
day_of_week = weekday ? __builtin_ctz(weekday) + 1 : 1;
} else if constexpr (LAYOUT.weekday == BcdWeekday::FROM_1) {
day_of_week = raw[LAYOUT.weekday_index()] & 0x07;
} else {
day_of_week = (raw[LAYOUT.weekday_index()] & 0x07) + 1;
}
uint8_t hour = bcd_to_uint8(raw[2] & 0x3F);
if constexpr (LAYOUT.hour_12h) {
if ((raw[2] & 0x40) != 0) {
// 12-hour mode, e.g. set by another library: 12 AM reads as 12 with PM clear, 12 PM as 12 with PM set.
hour = bcd_to_uint8(raw[2] & 0x1F) % 12 + ((raw[2] & 0x20) != 0 ? 12 : 0);
}
}
uint16_t year = 2000 + bcd_to_uint8(raw[6]);
if constexpr (LAYOUT.century_bit) {
if ((raw[5] & 0x80) != 0)
year += 100;
}
ESPTime time{
.second = bcd_to_uint8(raw[0] & 0x7F),
.minute = bcd_to_uint8(raw[1] & 0x7F),
.hour = hour,
.day_of_week = day_of_week,
.day_of_month = bcd_to_uint8(raw[LAYOUT.day_index()] & 0x3F),
.day_of_year = 1,
.month = bcd_to_uint8(raw[5] & 0x1F),
.year = year,
.is_dst = false,
.timestamp = 0,
};
time.recalc_timestamp_utc(false);
return time;
}
/// Encode @p time into the 7-byte BCD calendar at @p raw in the given layout, always in 24-hour mode.
/// @p time must be valid (is_valid()); the year must be 2000 to 2099 unless the layout has a century bit.
template<BcdCalendarLayout LAYOUT>
__attribute__((noinline)) void encode_bcd_calendar(const ESPTime &time, uint8_t *raw) {
uint8_t value[7] = {time.second, time.minute, time.hour, 0, 0, time.month, static_cast<uint8_t>(time.year % 100)};
value[LAYOUT.day_index()] = time.day_of_month;
for (size_t i = 0; i < 7; i++)
raw[i] = uint8_to_bcd(value[i]);
if constexpr (LAYOUT.weekday == BcdWeekday::ONE_HOT) {
raw[LAYOUT.weekday_index()] = 1 << (time.day_of_week - 1);
} else if constexpr (LAYOUT.weekday == BcdWeekday::FROM_1) {
raw[LAYOUT.weekday_index()] = time.day_of_week;
} else {
raw[LAYOUT.weekday_index()] = time.day_of_week - 1;
}
if constexpr (LAYOUT.century_bit) {
if (time.year >= 2100)
raw[5] |= 0x80;
}
}
} // namespace esphome::time
+51 -14
View File
@@ -1,16 +1,38 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include "esphome/components/time/real_time_clock.h"
namespace esphome::time::testing {
// 2026-04-16 23:59:58, a Thursday (day_of_week 5, one-hot bit 4).
static constexpr uint8_t RAW[7] = {0x58, 0x59, 0x23, 0x10, 0x16, 0x04, 0x26};
// 2026-04-16 23:59:58, a Thursday (day_of_week 5), in each layout.
static constexpr uint32_t TIMESTAMP = 1776383998;
static constexpr uint8_t EPSON_RAW[7] = {0x58, 0x59, 0x23, 0x10, 0x16, 0x04, 0x26};
static constexpr uint8_t DS_RAW[7] = {0x58, 0x59, 0x23, 0x05, 0x16, 0x04, 0x26};
static constexpr uint8_t PCF_RAW[7] = {0x58, 0x59, 0x23, 0x16, 0x04, 0x04, 0x26};
static std::array<uint8_t, 7> patched(const uint8_t (&src)[7], size_t index, uint8_t value) {
std::array<uint8_t, 7> raw;
std::copy(src, src + 7, raw.begin());
raw[index] = value;
return raw;
}
template<BcdCalendarLayout LAYOUT> static void expect_round_trip(const uint8_t (&raw)[7]) {
const ESPTime time = decode_bcd_calendar<LAYOUT>(raw);
EXPECT_EQ(time.day_of_week, 5);
EXPECT_EQ(time.day_of_month, 16);
EXPECT_EQ(time.timestamp, TIMESTAMP);
uint8_t out[7];
encode_bcd_calendar<LAYOUT>(time, out);
for (size_t i = 0; i < 7; i++)
EXPECT_EQ(out[i], raw[i]) << "byte " << i;
}
TEST(BcdCalendar, DecodesEveryField) {
const ESPTime time = decode_bcd_calendar(RAW);
const ESPTime time = decode_bcd_calendar<EPSON_BCD_CALENDAR>(EPSON_RAW);
EXPECT_EQ(time.second, 58);
EXPECT_EQ(time.minute, 59);
EXPECT_EQ(time.hour, 23);
@@ -18,14 +40,14 @@ TEST(BcdCalendar, DecodesEveryField) {
EXPECT_EQ(time.day_of_month, 16);
EXPECT_EQ(time.month, 4);
EXPECT_EQ(time.year, 2026);
EXPECT_EQ(time.timestamp, 1776383998);
EXPECT_EQ(time.timestamp, TIMESTAMP);
EXPECT_TRUE(time.is_valid(true, false));
}
TEST(BcdCalendar, MasksStatusBits) {
// Bit 7 of seconds/minutes, the top two bits of hour and day and the top three of month are not time.
const uint8_t raw[7] = {0xD8, 0xD9, 0xE3, 0x90, 0xD6, 0xE4, 0x26};
const ESPTime time = decode_bcd_calendar(raw);
const ESPTime time = decode_bcd_calendar<EPSON_BCD_CALENDAR>(raw);
EXPECT_EQ(time.second, 58);
EXPECT_EQ(time.minute, 59);
EXPECT_EQ(time.hour, 23);
@@ -35,17 +57,32 @@ TEST(BcdCalendar, MasksStatusBits) {
}
TEST(BcdCalendar, EmptyWeekdayDecodesAsSunday) {
uint8_t raw[7];
std::copy(RAW, RAW + 7, raw);
raw[3] = 0;
EXPECT_EQ(decode_bcd_calendar(raw).day_of_week, 1);
EXPECT_EQ(decode_bcd_calendar<EPSON_BCD_CALENDAR>(patched(EPSON_RAW, 3, 0).data()).day_of_week, 1);
}
TEST(BcdCalendar, EncodeRoundTrips) {
uint8_t raw[7];
encode_bcd_calendar(decode_bcd_calendar(RAW), raw);
for (size_t i = 0; i < 7; i++)
EXPECT_EQ(raw[i], RAW[i]) << "byte " << i;
TEST(BcdCalendar, EveryLayoutRoundTrips) {
expect_round_trip<EPSON_BCD_CALENDAR>(EPSON_RAW);
expect_round_trip<DS_BCD_CALENDAR>(DS_RAW);
expect_round_trip<PCF_BCD_CALENDAR>(PCF_RAW);
}
TEST(BcdCalendar, DsLayoutTwelveHourMode) {
EXPECT_EQ(decode_bcd_calendar<DS_BCD_CALENDAR>(patched(DS_RAW, 2, 0x40 | 0x20 | 0x11).data()).hour, 23); // 11 PM
EXPECT_EQ(decode_bcd_calendar<DS_BCD_CALENDAR>(patched(DS_RAW, 2, 0x40 | 0x12).data()).hour, 0); // 12 AM
EXPECT_EQ(decode_bcd_calendar<DS_BCD_CALENDAR>(patched(DS_RAW, 2, 0x40 | 0x20 | 0x12).data()).hour, 12); // 12 PM
}
TEST(BcdCalendar, DsLayoutCenturyBit) {
const ESPTime time = decode_bcd_calendar<DS_BCD_CALENDAR>(patched(DS_RAW, 5, 0x84).data());
EXPECT_EQ(time.year, 2126);
uint8_t out[7];
encode_bcd_calendar<DS_BCD_CALENDAR>(time, out);
EXPECT_EQ(out[5], 0x84);
EXPECT_EQ(out[6], 0x26);
}
TEST(BcdCalendar, PcfLayoutIgnoresMonthBit7) {
EXPECT_EQ(decode_bcd_calendar<PCF_BCD_CALENDAR>(patched(PCF_RAW, 5, 0x84).data()).year, 2026);
}
} // namespace esphome::time::testing