[core][time] Add BCD helpers and a shared RTC BCD calendar codec (#20860)

This commit is contained in:
J. Nick Koston
2026-10-17 16:32:13 -10:00
committed by GitHub
parent 1307faef06
commit 968d57c56b
5 changed files with 121 additions and 0 deletions
@@ -107,4 +107,37 @@ 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,4 +44,10 @@ 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);
} // namespace esphome::time
+5
View File
@@ -1062,6 +1062,11 @@ constexpr std::array<uint8_t, sizeof(T)> decode_value(T val) {
return ret;
}
/// Decode a two-digit packed BCD byte (tens in the high nibble) to its value, e.g. 0x59 to 59.
constexpr uint8_t bcd_to_uint8(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }
/// Encode a value from 0 to 99 as a two-digit packed BCD byte, e.g. 59 to 0x59.
constexpr uint8_t uint8_to_bcd(uint8_t value) { return ((value / 10) << 4) | (value % 10); }
/// Reverse the order of 8 bits.
inline uint8_t reverse_bits(uint8_t x) {
x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
+26
View File
@@ -0,0 +1,26 @@
#include <gtest/gtest.h>
#include "esphome/core/helpers.h"
namespace esphome::core::testing {
static_assert(bcd_to_uint8(0x59) == 59);
static_assert(uint8_to_bcd(59) == 0x59);
TEST(Bcd, RoundTripsEveryTwoDigitValue) {
for (uint8_t value = 0; value < 100; value++) {
const uint8_t bcd = uint8_to_bcd(value);
EXPECT_EQ(bcd >> 4, value / 10);
EXPECT_EQ(bcd & 0x0F, value % 10);
EXPECT_EQ(bcd_to_uint8(bcd), value);
}
}
TEST(Bcd, DecodesBoundaryBytes) {
EXPECT_EQ(bcd_to_uint8(0x00), 0);
EXPECT_EQ(bcd_to_uint8(0x09), 9);
EXPECT_EQ(bcd_to_uint8(0x10), 10);
EXPECT_EQ(bcd_to_uint8(0x99), 99);
}
} // namespace esphome::core::testing
+51
View File
@@ -0,0 +1,51 @@
#include <gtest/gtest.h>
#include <algorithm>
#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};
TEST(BcdCalendar, DecodesEveryField) {
const ESPTime time = decode_bcd_calendar(RAW);
EXPECT_EQ(time.second, 58);
EXPECT_EQ(time.minute, 59);
EXPECT_EQ(time.hour, 23);
EXPECT_EQ(time.day_of_week, 5);
EXPECT_EQ(time.day_of_month, 16);
EXPECT_EQ(time.month, 4);
EXPECT_EQ(time.year, 2026);
EXPECT_EQ(time.timestamp, 1776383998);
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);
EXPECT_EQ(time.second, 58);
EXPECT_EQ(time.minute, 59);
EXPECT_EQ(time.hour, 23);
EXPECT_EQ(time.day_of_week, 5);
EXPECT_EQ(time.day_of_month, 16);
EXPECT_EQ(time.month, 4);
}
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);
}
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;
}
} // namespace esphome::time::testing