From cdca5219d007fb8e2828fe302e2bdb97316890d0 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sat, 18 Jul 2026 01:09:43 +0200 Subject: [PATCH] Rewrite on libavr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same driver surface as the yazoalfa version — clock and alarm get/set, alarm interrupts, flag check/clear — plus oscillator-stop detection and die temperature. One source for tiny85 (software I2C) and mega328P (TWI), built against libavr in both generated and reflect mode, byte-identical .text across modes. Errors surface as std::expected instead of being dropped; weekday-rate alarms now really set the DY bit (legacy cleared it); multi-register access is one coherent bus transaction. Legacy stays on master. Co-Authored-By: Claude --- .gitignore | 13 +- CMakeLists.txt | 25 +++ CMakePresets.json | 43 ++++ README.md | 29 +++ alarms.hpp | 24 --- ds3231.hpp | 392 ------------------------------------ example/CMakeLists.txt | 3 + example/main.cpp | 32 +++ flags.hpp | 88 -------- include/ds3231/ds3231.hpp | 304 ++++++++++++++++++++++++++++ registers.hpp | 413 -------------------------------------- 11 files changed, 438 insertions(+), 928 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 README.md delete mode 100644 alarms.hpp delete mode 100644 ds3231.hpp create mode 100644 example/CMakeLists.txt create mode 100644 example/main.cpp delete mode 100644 flags.hpp create mode 100644 include/ds3231/ds3231.hpp delete mode 100644 registers.hpp diff --git a/.gitignore b/.gitignore index 9f584cc..a4fb4fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,2 @@ -.vs -Release -Debug -*.componentinfo.xml -*.elf -*.o -*.hex -*.srec -*.eeprom -*.lss -*.map +build/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ba2217a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.28) + +project(ds3231 LANGUAGES CXX) + +# libavr comes from a local checkout (LIBAVR_ROOT cache/env variable) or, +# failing that, straight from the forge. The toolchain file also lives in +# that checkout — see CMakePresets.json. +include(FetchContent) +if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT}) + set(LIBAVR_ROOT $ENV{LIBAVR_ROOT}) +endif() +if(LIBAVR_ROOT) + FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT}) +else() + FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main) +endif() +FetchContent_MakeAvailable(libavr) + +add_library(ds3231 INTERFACE) +target_include_directories(ds3231 INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_link_libraries(ds3231 INTERFACE libavr) + +if(PROJECT_IS_TOP_LEVEL) + add_subdirectory(example) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..f088ba3 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,43 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_COLOR_DIAGNOSTICS": "ON" + } + }, + { + "name": "attiny85-generated", + "inherits": "base", + "cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "OFF" } + }, + { + "name": "attiny85-reflect", + "inherits": "base", + "cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "ON" } + }, + { + "name": "atmega328p-generated", + "inherits": "base", + "cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" } + }, + { + "name": "atmega328p-reflect", + "inherits": "base", + "cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" } + } + ], + "buildPresets": [ + { "name": "attiny85-generated", "configurePreset": "attiny85-generated" }, + { "name": "attiny85-reflect", "configurePreset": "attiny85-reflect" }, + { "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" }, + { "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6f184b --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# ds3231 + +Maxim DS3231 RTC driver on [libavr](https://git.blackmark.me/avr/libavr): +clock and alarm read/write, alarm interrupts, oscillator-stop detection, +die temperature. One source runs on every libavr chip — TWI hardware on +the mega328P, open-drain software I2C on the tinies. All bus errors +surface as `std::expected`. + +```cpp +using bus = dev::i2c<{.frequency = 100_kHz}>; +using rtc = ds3231::device; + +auto now = rtc::read_clock(); // result +(void)rtc::set_alarm1({}, ds3231::alarm1_rate::once_per_second); +``` + +`example/main.cpp` is the full tour. Build with a libavr checkout: + +```sh +LIBAVR_ROOT=/path/to/libavr cmake --preset attiny85-generated +cmake --build --preset attiny85-generated +``` + +Presets cover attiny85/atmega328p in both libavr modes (generated and +reflect). The legacy yazoalfa-based driver lives on the `master` branch. + +Differences from legacy: weekday-rate alarms now actually set the DY bit +(the old `setAlarmHelper` always cleared it), reads/writes are single +coherent bus transactions, and errors are reported instead of ignored. diff --git a/alarms.hpp b/alarms.hpp deleted file mode 100644 index e4de941..0000000 --- a/alarms.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -namespace rtc { - -enum class Alarm1Rate : uint8_t { - ONCE_PER_S = 0b1111, - WHEN_S_MATCH = 0b1110, - WHEN_M_S_MATCH = 0b1100, - WHEN_H_M_S_MATCH = 0b1000, - WHEN_DATE_H_M_S_MATCH = 0b00000, - WHEN_DAY_H_N_S_MATCH = 0b10000, -}; - -enum class Alarm2Rate : uint8_t { - ONCE_PER_M = 0b111, - WHEN_M_MATCH = 0b110, - WHEN_H_M_MATCH = 0b100, - WHEN_DATE_H_M_MATCH = 0b0000, - WHEN_DAY_H_N_MATCH = 0b1000, -}; - -} // namespace rtc diff --git a/ds3231.hpp b/ds3231.hpp deleted file mode 100644 index a38ada2..0000000 --- a/ds3231.hpp +++ /dev/null @@ -1,392 +0,0 @@ -#pragma once - -#include "../clock.hpp" - -#include - -#include "../i2c/i2c.hpp" -#include "../util/type.hpp" - -#include "registers.hpp" - -namespace rtc { - -struct Date { - uint16_t year; - uint8_t month; - uint8_t day; - - inline bool operator==(const Date &rhs) const - { - if (day != rhs.day) - return false; - if (month != rhs.month) - return false; - if (year != rhs.year) - return false; - return true; - } - - inline bool operator!=(const Date &rhs) - { - return !(*this == rhs); - } -}; - -struct Time { - uint8_t hour; - uint8_t minute; - uint8_t second; - - inline bool operator==(const Time &rhs) const - { - if (second != rhs.second) - return false; - if (minute != rhs.minute) - return false; - if (hour != rhs.hour) - return false; - return true; - } - - inline bool operator!=(const Time &rhs) - { - return !(*this == rhs); - } -}; - -struct DateTime : Date, Time { - inline bool operator==(const DateTime &rhs) const - { - if (second != rhs.second) - return false; - if (minute != rhs.minute) - return false; - if (hour != rhs.hour) - return false; - if (day != rhs.day) - return false; - if (month != rhs.month) - return false; - if (year != rhs.year) - return false; - return true; - } - - inline bool operator!=(const DateTime &rhs) - { - return !(*this == rhs); - } -}; - -template -class DS3231 { - using i2c_t = i2c::I2c; - - public: - static constexpr auto I2C_ADDRESS = 0x68; - - static constexpr auto TIME_REG_ADDR = 0x00; - static constexpr auto ALARM1_REG_ADDR = 0x07; - static constexpr auto ALARM2_REG_ADDR = 0x0B; - static constexpr auto CONTROL_REG_ADDR = 0x0E; - static constexpr auto CONTROL_STATUS_REG_ADDR = 0x0F; - static constexpr auto AGING_OFFSET_REG_ADDR = 0x10; - static constexpr auto TEMP_REG_ADDR = 0x11; - - // Construction does not call init and is only available for convenience - DS3231() = default; - - // Moving and copying ds3231 objects is not supported - DS3231(const DS3231 &) = delete; - DS3231(DS3231 &&) = delete; - DS3231 &operator=(const DS3231 &) = delete; - DS3231 &operator=(DS3231 &&) = delete; - - static inline void init() - { - i2c_t::init(); - } - - static auto getDate() - { - const auto timeReg = readRegister(); - - Date date; - date.year = timeReg.getYear(); - date.month = timeReg.getMonth(); - date.day = timeReg.getDate(); - return date; - } - static auto getTime() - { - const auto timeReg = readRegister(); - - Time time; - time.hour = timeReg.getHours(); - time.minute = timeReg.getMinutes(); - time.second = timeReg.getSeconds(); - return time; - } - static auto getDateTime() - { - const auto timeReg = readRegister(); - - DateTime dateTime; - dateTime.year = timeReg.getYear(); - dateTime.month = timeReg.getMonth(); - dateTime.day = timeReg.getDate(); - dateTime.hour = timeReg.getHours(); - dateTime.minute = timeReg.getMinutes(); - dateTime.second = timeReg.getSeconds(); - return dateTime; - } - - static DateTime getAlarm1() - { - return getAlarmHelper(); - } - static DateTime getAlarm2() - { - return getAlarmHelper(); - } - - static void setDate(const Date &date) - { - detail::TimeReg timeReg; - timeReg.setYear(date.year); - timeReg.setMonth(date.month); - timeReg.setDate(date.day); - if constexpr (SetDayOfWeek) - timeReg.setDay(calcDayOfWeek(date.year, date.month, date.day) + 1); - - constexpr auto DATE_START_OFFSET = offsetof(detail::TimeReg, day); - constexpr auto DATE_END_OFFSET = offsetof(detail::TimeReg, year); - - writePartialRegister(timeReg); - } - static void setTime(const Time &time) - { - detail::TimeReg timeReg; - timeReg.setHours(time.hour); - timeReg.setMinutes(time.minute); - timeReg.setSeconds(time.second); - - constexpr auto TIME_START_OFFSET = offsetof(detail::TimeReg, seconds); - constexpr auto TIME_END_OFFSET = offsetof(detail::TimeReg, hours); - - writePartialRegister(timeReg); - } - static void setDateTime(const DateTime &dateTime) - { - detail::TimeReg timeReg; - timeReg.setYear(dateTime.year); - timeReg.setMonth(dateTime.month); - timeReg.setDate(dateTime.day); - if constexpr (SetDayOfWeek) - timeReg.setDay(calcDayOfWeek(dateTime.year, dateTime.month, dateTime.day) + 1); - - timeReg.setHours(dateTime.hour); - timeReg.setMinutes(dateTime.minute); - timeReg.setSeconds(dateTime.second); - - constexpr auto START_OFFSET = offsetof(detail::TimeReg, seconds); - constexpr auto END_OFFSET = offsetof(detail::TimeReg, year); - - writePartialRegister(timeReg); - } - - static void setAlarm1(const DateTime &alarmTime, const Alarm1Rate &alarmRate, bool enableInterrupt = true) - { - setAlarmHelper(alarmTime, alarmRate); - if (enableInterrupt) - enableInterruptHelper(); - } - static void setAlarm2(const DateTime &alarmTime, const Alarm2Rate &alarmRate, bool enableInterrupt = true) - { - setAlarmHelper(alarmTime, alarmRate); - if (enableInterrupt) - enableInterruptHelper(); - } - - static bool checkAlarm1() - { - return checkAlarmHelper(); - } - static bool checkAlarm2() - { - return checkAlarmHelper(); - } - - static void clearAlarm1() - { - clearAlarmHelper(); - } - static void clearAlarm2() - { - clearAlarmHelper(); - } - - private: - template - static Register readRegisterHelper() - { - i2c_t::template start(false); - i2c_t::write(Address); - i2c_t::stop(); - - Register reg; - i2c_t::template start(true); - i2c_t::template readBytes(reinterpret_cast(®)); - i2c_t::stop(); - return reg; - } - - template - static auto readRegister() - { - if constexpr (Address == TIME_REG_ADDR) { - return readRegisterHelper(); - } else if constexpr (Address == ALARM1_REG_ADDR) { - return readRegisterHelper(); - } else if constexpr (Address == ALARM2_REG_ADDR) { - return readRegisterHelper(); - } else if constexpr (Address == CONTROL_REG_ADDR) { - return readRegisterHelper(); - } else if constexpr (Address == CONTROL_STATUS_REG_ADDR) { - return readRegisterHelper(); - } else if constexpr (Address == AGING_OFFSET_REG_ADDR) { - return readRegisterHelper(); - } else if constexpr (Address == TEMP_REG_ADDR) { - return readRegisterHelper(); - } else { - static_assert(util::always_false_v, "Invalid register address"); - } - } - - template - static void writePartialRegister(const Register ®) - { - constexpr auto getRegisterAddress = []() { - if constexpr (util::is_same_v) { - return TIME_REG_ADDR; - } else if constexpr (util::is_same_v) { - return ALARM1_REG_ADDR; - } else if constexpr (util::is_same_v) { - return ALARM2_REG_ADDR; - } else if constexpr (util::is_same_v) { - return CONTROL_REG_ADDR; - } else if constexpr (util::is_same_v) { - return CONTROL_STATUS_REG_ADDR; - } else if constexpr (util::is_same_v) { - return AGING_OFFSET_REG_ADDR; - } else if constexpr (util::is_same_v) { - return TEMP_REG_ADDR; - } else { - static_assert(util::always_false_v, "Invalid register type"); - } - }; - - constexpr auto ADDRESS = getRegisterAddress(); - - static_assert(StartOffset <= EndOffset, "Invalid offset range"); - static_assert(StartOffset < sizeof(Register), "Start offset out of bounds"); - static_assert(EndOffset < sizeof(Register), "End offset out of bounds"); - - constexpr auto WRITE_SIZE = EndOffset + 1 - StartOffset; - static_assert(StartOffset + WRITE_SIZE <= sizeof(Register), "Writing out of bounds"); - - i2c_t::template start(false); - i2c_t::write(ADDRESS + StartOffset); - i2c_t::template writeBytes(reinterpret_cast(®) + StartOffset); - i2c_t::stop(); - } - - template - static void writeRegister(const Register ®) - { - writePartialRegister<0, sizeof(Register) - 1>(reg); - } - - template - static inline auto getAlarmHelper() - { - constexpr auto IsAlarm1 = Address == ALARM1_REG_ADDR; - static_assert(IsAlarm1 || Address == ALARM2_REG_ADDR, "Must use valid alarm address"); - - const auto alarmReg = readRegister
(); - - DateTime alarmTime = {}; - alarmReg.getDate(alarmTime.day); - alarmTime.hour = alarmReg.getHours(); - alarmTime.minute = alarmReg.getMinutes(); - if constexpr (IsAlarm1) - alarmTime.second = alarmReg.getSeconds(); - - return alarmTime; - } - - template - static inline void setAlarmHelper(const DateTime &alarmTime, const AlarmRate &alarmRate) - { - constexpr auto IsAlarm1 = util::is_same_v; - static_assert(IsAlarm1 || util::is_same_v, "Must use valid alarm rate"); - - using alarm_reg_t = util::conditional_t; - - alarm_reg_t alarmReg; - alarmReg.setAlarmRate(alarmRate); - alarmReg.setDate(alarmTime.day); - alarmReg.setHours(alarmTime.hour); - alarmReg.setMinutes(alarmTime.minute); - if constexpr (IsAlarm1) - alarmReg.setSeconds(alarmTime.second); - writeRegister(alarmReg); - } - - template - static inline void enableInterruptHelper() - { - constexpr auto IsAlarm1Flag = AlarmFlag == detail::ControlRegFlags::A1IE; - constexpr auto IsAlarm2Flag = AlarmFlag == detail::ControlRegFlags::A2IE; - static_assert(IsAlarm1Flag || IsAlarm2Flag, "Must use valid alarm flag"); - - auto controlReg = readRegister(); - using Flags = typename decltype(controlReg)::Flags; - controlReg &= ~Flags::BBSQW; - controlReg |= Flags::INTCN | AlarmFlag; - writeRegister(controlReg); - } - - template - static inline bool checkAlarmHelper() - { - constexpr auto IsAlarm1Flag = AlarmFlag == detail::ControlStatusRegFlags::A1F; - constexpr auto IsAlarm2Flag = AlarmFlag == detail::ControlStatusRegFlags::A2F; - static_assert(IsAlarm1Flag || IsAlarm2Flag, "Must use valid alarm flag"); - - const auto alarmStatus = readRegister(); - return alarmStatus == AlarmFlag; - } - - template - static inline void clearAlarmHelper() - { - constexpr auto IsAlarm1Flag = AlarmFlag == detail::ControlStatusRegFlags::A1F; - constexpr auto IsAlarm2Flag = AlarmFlag == detail::ControlStatusRegFlags::A2F; - static_assert(IsAlarm1Flag || IsAlarm2Flag, "Must use valid alarm flag"); - - auto controlStatusReg = readRegister(); - controlStatusReg &= ~AlarmFlag; - writeRegister(controlStatusReg); - } - - static uint8_t calcDayOfWeek(uint16_t year, uint8_t month, uint16_t day) - { - day += month < 3 ? year-- : year - 2; - const auto dayOfWeek = (23 * month / 9 + day + 4 + year / 4 - year / 100 + year / 400); - return dayOfWeek % 7; - } -}; - -} // namespace rtc diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..4f29f7b --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(clock main.cpp) +target_link_libraries(clock PRIVATE ds3231) +add_custom_command(TARGET clock POST_BUILD COMMAND ${CMAKE_SIZE} $) diff --git a/example/main.cpp b/example/main.cpp new file mode 100644 index 0000000..57bd9b0 --- /dev/null +++ b/example/main.cpp @@ -0,0 +1,32 @@ +#include +#include + +using namespace avr::literals; + +// One source for the tiny85 (open-drain software I2C on the USI pins) and +// the mega328P (TWI hardware). On first power-up the clock is seeded and +// alarm 1 armed; then the LED mirrors the seconds parity — a stuck LED +// means bus errors. +using dev = avr::device<{.clock = 8_MHz}>; +using bus = dev::i2c<{.frequency = 100_kHz}>; +using rtc = ds3231::device; +using led = dev::output; + +int main() +{ + avr::init(); + + if (auto stopped = rtc::oscillator_stopped(); stopped.value_or(false)) { + (void)rtc::write_clock({{2026, 1, 1}, {12, 0, 0}}); + (void)rtc::set_alarm1({}, ds3231::alarm1_rate::once_per_second); + (void)rtc::clear_oscillator_stopped(); + } + + while (true) { + if (auto now = rtc::read_clock(); now.has_value()) + led::write(now->second % 2 == 0); + if (auto fired = rtc::alarm1_fired(); fired.value_or(false)) + (void)rtc::clear_alarm1(); + dev::delay<100_ms>(); + } +} diff --git a/flags.hpp b/flags.hpp deleted file mode 100644 index 1371dc7..0000000 --- a/flags.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -namespace rtc { - -namespace detail { - -template -struct [[gnu::packed]] FlagsImpl -{ - static_assert(sizeof(FlagsT) == sizeof(uint8_t), "Must use uint8_t enum class flags"); - - uint8_t data = 0; - using Flags = FlagsT; - - FlagsImpl() : data(0) {} - FlagsImpl(const Flags &flag) : data(static_cast(flag)) {} - - FlagsImpl(const FlagsImpl &other) : data(other.data) {} - FlagsImpl(const FlagsImpl &&) = delete; - - FlagsImpl &operator=(const FlagsImpl &rhs) - { - data = rhs.data; - return *this; - } - FlagsImpl &operator=(const FlagsImpl &&) = delete; - - FlagsImpl &operator=(const FlagsT &rhs) - { - data = static_cast(rhs); - return *this; - } - - FlagsImpl &operator|=(const FlagsT &flag) - { - data |= static_cast(flag); - return *this; - } - - FlagsImpl &operator|=(const uint8_t &flag) - { - data |= flag; - return *this; - } - - FlagsImpl &operator&=(const FlagsT &flag) - { - data &= static_cast(flag); - return *this; - } - - FlagsImpl &operator&=(const uint8_t &flag) - { - data &= flag; - return *this; - } - - FlagsImpl &operator~() - { - data = ~data; - return *this; - } - - bool operator==(const FlagsT &flag) const - { - return data & static_cast(flag); - } -}; - -template -FlagsT operator|(const FlagsT &lhs, const FlagsT &rhs) -{ - const auto lhsInt = static_cast(lhs); - const auto rhsInt = static_cast(rhs); - return static_cast(lhsInt | rhsInt); -} - -template -FlagsT operator&(const FlagsT &lhs, const FlagsT &rhs) -{ - const auto lhsInt = static_cast(lhs); - const auto rhsInt = static_cast(rhs); - return static_cast(lhsInt & rhsInt); -} - -} // namespace detail - -} // namespace rtc diff --git a/include/ds3231/ds3231.hpp b/include/ds3231/ds3231.hpp new file mode 100644 index 0000000..8e94e5d --- /dev/null +++ b/include/ds3231/ds3231.hpp @@ -0,0 +1,304 @@ +#pragma once + +#include +#include +#include + +#include + +namespace ds3231 { + +using avr::i2c::error; +using avr::i2c::status; +template +using result = std::expected; + +struct date { + std::uint16_t year; // 2000..2099 + std::uint8_t month; // 1..12 + std::uint8_t day; // 1..31 (weekday 1..7 in weekday-alarm reads) + friend constexpr bool operator==(const date &, const date &) = default; +}; + +struct time_of_day { + std::uint8_t hour; // 0..23 + std::uint8_t minute; + std::uint8_t second; + friend constexpr bool operator==(const time_of_day &, const time_of_day &) = default; +}; + +struct date_time : date, time_of_day { + friend constexpr bool operator==(const date_time &, const date_time &) = default; +}; + +// Alarm match rates (datasheet Table 2). Alarm 2 has no seconds register +// and fires at :00 of the matching minute. +enum class alarm1_rate : std::uint8_t { + once_per_second = 0b01111, + seconds_match = 0b01110, + minutes_seconds_match = 0b01100, + time_match = 0b01000, + date_time_match = 0b00000, + weekday_time_match = 0b10000, +}; + +enum class alarm2_rate : std::uint8_t { + once_per_minute = 0b0111, + minutes_match = 0b0110, + time_match = 0b0100, + date_time_match = 0b0000, + weekday_time_match = 0b1000, +}; + +namespace detail { + +constexpr std::uint8_t to_bcd(std::uint8_t value) +{ + return static_cast(((value / 10) << 4) | (value % 10)); +} + +constexpr std::uint8_t from_bcd(std::uint8_t value) +{ + return static_cast((value >> 4) * 10 + (value & 0x0f)); +} + +// Hours register: bit 6 selects 12-hour mode, bit 5 is then PM. Writes +// always use 24-hour form; reads accept either. +constexpr std::uint8_t hours_from_reg(std::uint8_t reg) +{ + if (reg & 0x40) { + auto hour = from_bcd(reg & 0x1f); + bool pm = reg & 0x20; + if (hour == 12) + return pm ? 12 : 0; + return static_cast(pm ? hour + 12 : hour); + } + return from_bcd(reg & 0x3f); +} + +// Sakamoto's method; the device counts weekdays 1..7 with a free epoch, +// this maps Sunday to 1. +constexpr std::uint8_t weekday(std::uint16_t year, std::uint8_t month, std::uint8_t day) +{ + constexpr std::uint8_t offsets[]{0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + if (month < 3) + --year; + return static_cast((year + year / 4 - year / 100 + year / 400 + offsets[month - 1] + day) % 7 + 1); +} + +} // namespace detail + +// Maxim DS3231 RTC on any libavr i2c master (register map: datasheet +// 19-5170 Table 1). Every call is one bus transaction; errors surface as +// std::expected. Weekday registers maintain themselves from the date when +// SetWeekday is on. +template +class device { + using dev = avr::i2c::device; + + static constexpr std::uint8_t reg_clock = 0x00; + static constexpr std::uint8_t reg_alarm1 = 0x07; + static constexpr std::uint8_t reg_alarm2 = 0x0b; + static constexpr std::uint8_t reg_control = 0x0e; + static constexpr std::uint8_t reg_status = 0x0f; + static constexpr std::uint8_t reg_temp = 0x11; + + static constexpr std::uint8_t a1ie = 0x01, a2ie = 0x02, intcn = 0x04, bbsqw = 0x40; + static constexpr std::uint8_t a1f = 0x01, a2f = 0x02, osf = 0x80; + + public: + [[nodiscard]] static result read_clock() + { + std::array raw; + if (auto s = dev::read_regs(reg_clock, raw); !s) + return std::unexpected(s.error()); + date_time now; + now.second = detail::from_bcd(raw[0] & 0x7f); + now.minute = detail::from_bcd(raw[1] & 0x7f); + now.hour = detail::hours_from_reg(raw[2]); + now.day = detail::from_bcd(raw[4] & 0x3f); + now.month = detail::from_bcd(raw[5] & 0x1f); + now.year = static_cast(2000 + detail::from_bcd(raw[6])); + return now; + } + + [[nodiscard]] static result read_date() + { + auto now = read_clock(); + if (!now) + return std::unexpected(now.error()); + return static_cast(*now); + } + + [[nodiscard]] static result read_time() + { + std::array raw; + if (auto s = dev::read_regs(reg_clock, raw); !s) + return std::unexpected(s.error()); + return time_of_day{detail::hours_from_reg(raw[2]), detail::from_bcd(raw[1] & 0x7f), + detail::from_bcd(raw[0] & 0x7f)}; + } + + [[nodiscard]] static status write_clock(const date_time &now) + { + std::array raw{detail::to_bcd(now.second), + detail::to_bcd(now.minute), + detail::to_bcd(now.hour), + SetWeekday ? detail::weekday(now.year, now.month, now.day) : std::uint8_t{1}, + detail::to_bcd(now.day), + detail::to_bcd(now.month), + detail::to_bcd(static_cast(now.year % 100))}; + return dev::write_regs(reg_clock, raw); + } + + [[nodiscard]] static status write_date(const date &value) + { + if constexpr (SetWeekday) { + std::array raw{detail::weekday(value.year, value.month, value.day), + detail::to_bcd(value.day), detail::to_bcd(value.month), + detail::to_bcd(static_cast(value.year % 100))}; + return dev::write_regs(reg_clock + 3, raw); + } else { + std::array raw{detail::to_bcd(value.day), detail::to_bcd(value.month), + detail::to_bcd(static_cast(value.year % 100))}; + return dev::write_regs(reg_clock + 4, raw); + } + } + + [[nodiscard]] static status write_time(const time_of_day &value) + { + std::array raw{detail::to_bcd(value.second), detail::to_bcd(value.minute), + detail::to_bcd(value.hour)}; + return dev::write_regs(reg_clock, raw); + } + + // Alarm times use .day as the date of month, or as weekday 1..7 with + // the weekday_time_match rates. + [[nodiscard]] static status set_alarm1(const date_time &at, alarm1_rate rate, bool enable_interrupt = true) + { + auto m = static_cast(rate); + std::array raw{ + static_cast(detail::to_bcd(at.second) | ((m & 1) << 7)), + static_cast(detail::to_bcd(at.minute) | (((m >> 1) & 1) << 7)), + static_cast(detail::to_bcd(at.hour) | (((m >> 2) & 1) << 7)), + static_cast(day_date(at.day, rate == alarm1_rate::weekday_time_match) | + (((m >> 3) & 1) << 7))}; + if (auto s = dev::write_regs(reg_alarm1, raw); !s) + return s; + return enable_interrupt ? enable_alarm_interrupt(a1ie) : status{}; + } + + [[nodiscard]] static status set_alarm2(const date_time &at, alarm2_rate rate, bool enable_interrupt = true) + { + auto m = static_cast(rate); + std::array raw{ + static_cast(detail::to_bcd(at.minute) | ((m & 1) << 7)), + static_cast(detail::to_bcd(at.hour) | (((m >> 1) & 1) << 7)), + static_cast(day_date(at.day, rate == alarm2_rate::weekday_time_match) | + (((m >> 2) & 1) << 7))}; + if (auto s = dev::write_regs(reg_alarm2, raw); !s) + return s; + return enable_interrupt ? enable_alarm_interrupt(a2ie) : status{}; + } + + [[nodiscard]] static result read_alarm1() + { + std::array raw; + if (auto s = dev::read_regs(reg_alarm1, raw); !s) + return std::unexpected(s.error()); + date_time at{}; + at.second = detail::from_bcd(raw[0] & 0x7f); + at.minute = detail::from_bcd(raw[1] & 0x7f); + at.hour = detail::hours_from_reg(raw[2] & 0x7f); + at.day = (raw[3] & 0x40) ? (raw[3] & 0x0f) : detail::from_bcd(raw[3] & 0x3f); + return at; + } + + [[nodiscard]] static result read_alarm2() + { + std::array raw; + if (auto s = dev::read_regs(reg_alarm2, raw); !s) + return std::unexpected(s.error()); + date_time at{}; + at.minute = detail::from_bcd(raw[0] & 0x7f); + at.hour = detail::hours_from_reg(raw[1] & 0x7f); + at.day = (raw[2] & 0x40) ? (raw[2] & 0x0f) : detail::from_bcd(raw[2] & 0x3f); + return at; + } + + [[nodiscard]] static result alarm1_fired() + { + return flag_set(a1f); + } + + [[nodiscard]] static result alarm2_fired() + { + return flag_set(a2f); + } + + [[nodiscard]] static status clear_alarm1() + { + return clear_flag(a1f); + } + + [[nodiscard]] static status clear_alarm2() + { + return clear_flag(a2f); + } + + // True after the oscillator was ever stopped (first power-up, battery + // ran out): the clock needs to be set. + [[nodiscard]] static result oscillator_stopped() + { + return flag_set(osf); + } + + [[nodiscard]] static status clear_oscillator_stopped() + { + return clear_flag(osf); + } + + // Die temperature in quarter °C (updated every 64 s by the device). + [[nodiscard]] static result temperature_quarters() + { + std::array raw; + if (auto s = dev::read_regs(reg_temp, raw); !s) + return std::unexpected(s.error()); + return static_cast((static_cast(static_cast(raw[0])) << 2) | + (raw[1] >> 6)); + } + + private: + static constexpr std::uint8_t day_date(std::uint8_t day, bool weekday_mode) + { + if (weekday_mode) + return static_cast(0x40 | (day & 0x0f)); + return detail::to_bcd(day) & std::uint8_t{0x3f}; + } + + static status enable_alarm_interrupt(std::uint8_t enable_bit) + { + auto control = dev::read_reg(reg_control); + if (!control) + return std::unexpected(control.error()); + return dev::write_reg(reg_control, static_cast((*control & ~bbsqw) | intcn | enable_bit)); + } + + static result flag_set(std::uint8_t bit) + { + auto flags = dev::read_reg(reg_status); + if (!flags) + return std::unexpected(flags.error()); + return (*flags & bit) != 0; + } + + static status clear_flag(std::uint8_t bit) + { + auto flags = dev::read_reg(reg_status); + if (!flags) + return std::unexpected(flags.error()); + return dev::write_reg(reg_status, static_cast(*flags & ~bit)); + } +}; + +} // namespace ds3231 diff --git a/registers.hpp b/registers.hpp deleted file mode 100644 index 22d4d1a..0000000 --- a/registers.hpp +++ /dev/null @@ -1,413 +0,0 @@ -#pragma once - -#include - -#include "alarms.hpp" -#include "flags.hpp" - -namespace rtc { - -namespace detail { - -////////////////////////////////////////////////////////////////////////// - -template -static inline uint8_t toBcd(const uint8_t &data) -{ - return ((data / 10 * 16) + (data % 10)) & Mask; -} - -template -static inline uint8_t fromBcd(const uint8_t &data) -{ - const auto maskedData = data & Mask; - return ((maskedData / 16 * 10) + (maskedData % 16)); -} - -static inline uint8_t convertTo24Hour(const uint8_t &hoursReg) -{ - constexpr auto FLAG_12_HOUR = 6; - constexpr auto FLAG_PM = 5; - - const bool time12HourFormat = (hoursReg >> FLAG_12_HOUR) & 1; - - if (time12HourFormat) { - const auto pmFlag = (hoursReg >> FLAG_PM) & 1; - constexpr auto HOUR_12_MASK = 0b00011111; - const auto hour12 = fromBcd(hoursReg); - if (hour12 == 12 && !pmFlag) - return 0; - return hour12 + pmFlag ? 12 : 0; - } else // 24 hour format - { - constexpr auto HOUR_MASK = 0b00111111; - return fromBcd(hoursReg); - } -} - -template -static inline uint8_t getMaskedBcd(const uint8_t ®) -{ - return fromBcd(reg); -} - -template -static inline void setMaskedBcd(uint8_t ®, const uint8_t &value) -{ - reg &= ~Mask; - reg |= toBcd(value); -} - -static inline bool getEncodedDay(uint8_t &value, const uint8_t ®) -{ - constexpr auto DAY_FLAG = 6; - if ((reg >> DAY_FLAG) & 1) { - constexpr auto DAY_MASK = 0b00001111; - value = reg & DAY_MASK; - return true; - } - - return false; -} - -static inline bool getEncodedDate(uint8_t &value, const uint8_t ®) -{ - constexpr auto DAY_FLAG = 6; - if (!((reg >> DAY_FLAG) & 1)) { - value = getMaskedBcd<0b00111111>(reg); - return true; - } - - return false; -} - -static inline void setEncodedDay(uint8_t ®, const uint8_t &value) -{ - constexpr auto DAY_MASK = 0b11110000; - reg &= DAY_MASK; - reg |= value & DAY_MASK; - constexpr auto DAY_FLAG = 6; - reg |= (1 << DAY_FLAG); -} - -static inline void setEncodedDate(uint8_t ®, const uint8_t &value) -{ - setMaskedBcd<0b00111111>(reg, value); - constexpr auto DAY_FLAG = 6; - reg &= ~(1 << DAY_FLAG); -} - -////////////////////////////////////////////////////////////////////////// - -struct [[gnu::packed]] TimeReg -{ - uint8_t seconds = 0; - uint8_t minutes = 0; - uint8_t hours = 0; - uint8_t day = 1; // Range 1-7 according to datasheet - uint8_t date = 0; - uint8_t month_century = 0; - uint8_t year = 0; - - ////////////////////////////////////////////////////////////////////////// - - inline uint8_t getSeconds() const - { - return getMaskedBcd(seconds); - } - inline uint8_t getMinutes() const - { - return getMaskedBcd(minutes); - } - inline uint8_t getHours() const - { - return convertTo24Hour(hours); - } - inline uint8_t getDay() const - { - return getMaskedBcd<0b00000111>(day); - } - inline uint8_t getDate() const - { - return getMaskedBcd<0b00111111>(date); - } - inline uint8_t getMonth() const - { - return getMaskedBcd<0b00011111>(month_century); - } - inline bool getCentury() const - { - constexpr auto CENTURY_FLAG = 7; - return (month_century >> CENTURY_FLAG) & 1; - } - inline uint16_t getYear() const - { - return 2000 + fromBcd(year); - } - - ////////////////////////////////////////////////////////////////////////// - - inline void setSeconds(uint8_t seconds) - { - setMaskedBcd(this->seconds, seconds); - } - inline void setMinutes(uint8_t minutes) - { - setMaskedBcd(this->minutes, minutes); - } - inline void setHours(uint8_t hours) - { - setMaskedBcd(this->hours, hours); - } - inline void setDay(uint8_t day) - { - this->day = day & 0b111; - } - inline void setDate(uint8_t date) - { - setMaskedBcd<0b00111111>(this->date, date); - } - inline void setMonth(uint8_t month) - { - setMaskedBcd<0b00011111>(month_century, month); - } - inline void setCentury(bool century) - { - constexpr auto CENTURY_POS = 7; - month_century &= ~(1 << CENTURY_POS); - month_century |= (century << CENTURY_POS); - } - inline void setYear(uint16_t year) - { - year = year % 100; - this->year = toBcd(year); - } -}; - -static_assert(sizeof(TimeReg) == 7, "Invalid time register size"); - -////////////////////////////////////////////////////////////////////////// - -struct [[gnu::packed]] Alarm1Reg -{ - uint8_t seconds = 0; - uint8_t minutes = 0; - uint8_t hours = 0; - uint8_t day_date = 0; - - ////////////////////////////////////////////////////////////////////////// - - inline uint8_t getSeconds() const - { - return getMaskedBcd(seconds); - } - inline uint8_t getMinutes() const - { - return getMaskedBcd(minutes); - } - inline uint8_t getHours() const - { - return convertTo24Hour(hours); - } - inline bool getDay(uint8_t & day) const - { - return getEncodedDay(day, day_date); - } - inline bool getDate(uint8_t & date) const - { - return getEncodedDate(date, day_date); - } - inline Alarm1Rate getAlarmRate() const - { - constexpr auto M_FLAG = 7; - const auto m1 = (seconds >> M_FLAG) & 1; - const auto m2 = (minutes >> M_FLAG) & 1; - const auto m3 = (hours >> M_FLAG) & 1; - const auto m4 = (day_date >> M_FLAG) & 1; - const auto m = (m4 << 3) | (m3 << 2) | (m2 << 1) | (m1 << 0); - if (m == 0) { - constexpr auto DAY_FLAG = 6; - const auto dayFormat = ((day_date >> DAY_FLAG) & 1) << 4; - return static_cast(dayFormat); - } - return static_cast(m); - } - - ////////////////////////////////////////////////////////////////////////// - - inline void setSeconds(uint8_t seconds) - { - setMaskedBcd(this->seconds, seconds); - } - inline void setMinutes(uint8_t minutes) - { - setMaskedBcd(this->minutes, minutes); - } - inline void setHours(uint8_t hours) - { - setMaskedBcd(this->hours, hours); - } - inline void setDay(uint8_t day) - { - setEncodedDay(day_date, day); - } - inline void setDate(uint8_t date) - { - setEncodedDate(day_date, date); - } - inline void setAlarmRate(const Alarm1Rate &alarmRate) - { - const auto alarmRateFlags = static_cast(alarmRate); - constexpr auto M_FLAG = 7; - seconds &= ~(1 << M_FLAG); - seconds |= (alarmRateFlags & 1) << M_FLAG; - minutes &= ~(1 << M_FLAG); - minutes |= ((alarmRateFlags >> 1) & 1) << M_FLAG; - hours &= ~(1 << M_FLAG); - hours |= ((alarmRateFlags >> 2) & 1) << M_FLAG; - day_date &= ~(1 << M_FLAG); - day_date |= ((alarmRateFlags >> 3) & 1) << M_FLAG; - } -}; - -static_assert(sizeof(Alarm1Reg) == 4, "Invalid alarm1 register size"); - -////////////////////////////////////////////////////////////////////////// - -struct [[gnu::packed]] Alarm2Reg -{ - uint8_t minutes = 0; - uint8_t hours = 0; - uint8_t day_date = 0; - - ////////////////////////////////////////////////////////////////////////// - - inline uint8_t getMinutes() const - { - return getMaskedBcd(minutes); - } - inline uint8_t getHours() const - { - return convertTo24Hour(hours); - } - inline bool getDay(uint8_t & day) const - { - return getEncodedDay(day, day_date); - } - inline bool getDate(uint8_t & date) const - { - return getEncodedDate(date, day_date); - } - inline Alarm2Rate getAlarmRate() const - { - constexpr auto M_FLAG = 7; - const auto m2 = (minutes >> M_FLAG) & 1; - const auto m3 = (hours >> M_FLAG) & 1; - const auto m4 = (day_date >> M_FLAG) & 1; - const auto m = (m4 << 2) | (m3 << 1) | (m2 << 0); - if (m == 0) { - constexpr auto DAY_FLAG = 6; - const auto dayFormat = ((day_date >> DAY_FLAG) & 1) << 3; - return static_cast(dayFormat); - } - return static_cast(m); - } - - ////////////////////////////////////////////////////////////////////////// - - inline void setMinutes(uint8_t minutes) - { - setMaskedBcd(this->minutes, minutes); - } - inline void setHours(uint8_t hours) - { - setMaskedBcd(this->hours, hours); - } - inline void setDay(uint8_t day) - { - setEncodedDay(day_date, day); - } - inline void setDate(uint8_t date) - { - setEncodedDate(day_date, date); - } - inline void setAlarmRate(const Alarm2Rate &alarmRate) - { - const auto alarmRateFlags = static_cast(alarmRate); - constexpr auto M_FLAG = 7; - minutes &= ~(1 << M_FLAG); - minutes |= (alarmRateFlags & 1) << M_FLAG; - hours &= ~(1 << M_FLAG); - hours |= ((alarmRateFlags >> 1) & 1) << M_FLAG; - day_date &= ~(1 << M_FLAG); - day_date |= ((alarmRateFlags >> 2) & 1) << M_FLAG; - } -}; - -static_assert(sizeof(Alarm2Reg) == 3, "Invalid alarm2 register size"); - -////////////////////////////////////////////////////////////////////////// - -enum class ControlRegFlags : uint8_t { - N_EOSC = 1 << 7, - BBSQW = 1 << 6, - CONV = 1 << 5, - RS2 = 1 << 4, - RS1 = 1 << 3, - INTCN = 1 << 2, - A2IE = 1 << 1, - A1IE = 1 << 0, -}; - -static inline uint8_t operator~(const ControlRegFlags &flag) -{ - return ~static_cast(flag); -} - -struct [[gnu::packed]] ControlReg : FlagsImpl{}; - -static_assert(sizeof(ControlReg) == 1, "Invalid control register size"); - -////////////////////////////////////////////////////////////////////////// - -enum class ControlStatusRegFlags : uint8_t { - OSF = 1 << 7, - EN32KHZ = 1 << 3, - BSY = 1 << 2, - A2F = 1 << 1, - A1F = 1 << 0, -}; - -static inline uint8_t operator~(const ControlStatusRegFlags &flag) -{ - return ~static_cast(flag); -} - -struct [[gnu::packed]] ControlStatusReg : FlagsImpl{}; - -static_assert(sizeof(ControlStatusReg) == 1, "Invalid control/status register size"); - -////////////////////////////////////////////////////////////////////////// - -struct [[gnu::packed]] AgingOffsetReg -{ - uint8_t data = 0; -}; - -static_assert(sizeof(AgingOffsetReg) == 1, "Invalid aging offset register size"); - -////////////////////////////////////////////////////////////////////////// - -struct [[gnu::packed]] TempReg -{ - uint8_t msb_temp = 0; - uint8_t lsb_temp = 0; -}; - -static_assert(sizeof(TempReg) == 2, "Invalid temperature register size"); - -////////////////////////////////////////////////////////////////////////// - -} // namespace detail - -} // namespace rtc