mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 05:46:12 +00:00
[time] Make the BCD calendar helpers a template over the chip's layout
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user