fix: an alarm's interrupt could be turned on and never off, and nothing tested any of it
`set_alarm1/2`'s `enable_interrupt = false` did nothing at all - it skipped the control-register write rather than clearing A1IE/A2IE - and there was no other way to clear them, so an alarm armed once with its interrupt on drove INT/SQW for good. It is `alarm_interrupt::off` now and it clears the bit, with `listen_alarm1/2()` and `mute_alarm1/2()` beside it for a caller that arms once and changes its mind later. The bare bool went with it (rule 37): `set_alarm1(at, rate, false)` said nothing at the point of use. `bool SetWeekday` was the same shape one level up - `device<bus, false>` names nothing - and its two writers disagreed about what it meant: `write_date` skipped the weekday register, `write_clock` stamped a placeholder 1 into it, so a program that turned the maintenance off still had the register overwritten and no way to set it. `weekday_source::external` now means the register is the program's, in both writers. test/consteval.cpp is the battery this driver never had, over the arithmetic that has no bus in it: the BCD round trip across every representable value and its nibble layout, the hours register in both formats including all four noon/midnight cases, Sakamoto's weekday against six calendar dates (leap day, the day after it, and 2100's missing leap), and both alarms' rate encodings transcribed against 19-5170 Table 2 - including that alarm 2's masks sit one place below alarm 1's, which is the property that makes one distribution loop wrong for the other. Red-green: two assertions fire on a flipped DY bit. Beside it, one decode rather than two: `read_clock` and `read_time` spelled the seconds/minutes/hours triplet out separately (rule 6). Every image byte-identical on both chips in both modes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
7
test/CMakeLists.txt
Normal file
7
test/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# The battery is a compile: a static_assert that fails is the failure. It is a
|
||||
# target rather than only a ctest so a plain build catches a regression too.
|
||||
add_library(consteval_tests OBJECT consteval.cpp)
|
||||
target_link_libraries(consteval_tests PRIVATE ds3231)
|
||||
|
||||
add_test(NAME ds3231.consteval
|
||||
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target consteval_tests)
|
||||
82
test/consteval.cpp
Normal file
82
test/consteval.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
// Compile-only battery, built with the cross compiler so the target's 16-bit
|
||||
// int is exercised. Everything in this driver that is arithmetic rather than a
|
||||
// bus transaction lives here: the BCD both ways, the hours register's two
|
||||
// formats, Sakamoto's weekday, and the alarm rate encodings as the datasheet
|
||||
// tabulates them.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <ds3231/ds3231.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
using ds3231::detail::from_bcd;
|
||||
using ds3231::detail::hours_from_reg;
|
||||
using ds3231::detail::to_bcd;
|
||||
|
||||
// BCD is a round trip over every value the registers hold, and the encoding is
|
||||
// the datasheet's: tens in the high nibble, units in the low one.
|
||||
consteval bool bcd_round_trips()
|
||||
{
|
||||
for (std::uint8_t value = 0; value < 100; ++value) {
|
||||
const std::uint8_t packed = to_bcd(value);
|
||||
if (from_bcd(packed) != value) {
|
||||
return false;
|
||||
}
|
||||
if ((packed >> 4) != value / 10 || (packed & 0x0f) != value % 10) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static_assert(bcd_round_trips());
|
||||
|
||||
// The hours register (19-5170 Table 1): bit 6 selects 12-hour mode and bit 5
|
||||
// is then PM. Both noons and both midnights are the cases a naive decode gets
|
||||
// wrong - 12 AM is hour 0 and 12 PM is hour 12, neither of which is 12 + 12.
|
||||
static_assert(hours_from_reg(to_bcd(0)) == 0); // 24-hour midnight
|
||||
static_assert(hours_from_reg(to_bcd(13)) == 13); // 24-hour afternoon
|
||||
static_assert(hours_from_reg(to_bcd(23)) == 23);
|
||||
static_assert(hours_from_reg(0x40 | to_bcd(12)) == 0); // 12 AM
|
||||
static_assert(hours_from_reg(0x40 | 0x20 | to_bcd(12)) == 12); // 12 PM
|
||||
static_assert(hours_from_reg(0x40 | to_bcd(1)) == 1); // 1 AM
|
||||
static_assert(hours_from_reg(0x40 | 0x20 | to_bcd(1)) == 13); // 1 PM
|
||||
static_assert(hours_from_reg(0x40 | 0x20 | to_bcd(11)) == 23); // 11 PM
|
||||
|
||||
// Sakamoto's method, against dates a calendar can be checked against rather
|
||||
// than against this implementation run twice. Sunday is 1.
|
||||
using ds3231::detail::weekday;
|
||||
static_assert(weekday(2000, 1, 1) == 7); // Saturday
|
||||
static_assert(weekday(2024, 1, 1) == 2); // Monday
|
||||
static_assert(weekday(2024, 2, 29) == 5); // Thursday, the leap day
|
||||
static_assert(weekday(2024, 3, 1) == 6); // Friday, the day after it
|
||||
static_assert(weekday(2100, 3, 1) == 2); // Monday - 2100 is not a leap year
|
||||
static_assert(weekday(2026, 8, 12) == 4); // Wednesday
|
||||
|
||||
// The alarm rate encodings (19-5170 Table 2), transcribed: the low bits are
|
||||
// A1M4..A1M1 in register order and the high one is DY/DT. A rate is the mask
|
||||
// pattern its row names, and a transcription slip here arms the wrong alarm.
|
||||
using ds3231::alarm1_rate;
|
||||
static_assert(static_cast<std::uint8_t>(alarm1_rate::once_per_second) == 0b01111);
|
||||
static_assert(static_cast<std::uint8_t>(alarm1_rate::seconds_match) == 0b01110);
|
||||
static_assert(static_cast<std::uint8_t>(alarm1_rate::minutes_seconds_match) == 0b01100);
|
||||
static_assert(static_cast<std::uint8_t>(alarm1_rate::time_match) == 0b01000);
|
||||
static_assert(static_cast<std::uint8_t>(alarm1_rate::date_time_match) == 0b00000);
|
||||
static_assert(static_cast<std::uint8_t>(alarm1_rate::weekday_time_match) == 0b10000);
|
||||
|
||||
using ds3231::alarm2_rate;
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::once_per_minute) == 0b0111);
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::minutes_match) == 0b0110);
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::time_match) == 0b0100);
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::date_time_match) == 0b0000);
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::weekday_time_match) == 0b1000);
|
||||
|
||||
// Alarm 2 has no seconds register, so its mask bits are one place lower than
|
||||
// alarm 1's throughout - the property that makes one distribution loop wrong
|
||||
// for the other.
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::once_per_minute) ==
|
||||
static_cast<std::uint8_t>(alarm1_rate::once_per_second) >> 1);
|
||||
static_assert(static_cast<std::uint8_t>(alarm2_rate::weekday_time_match) ==
|
||||
static_cast<std::uint8_t>(alarm1_rate::weekday_time_match) >> 1);
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user