Files
ds3231/README.md
BlackMark 2e9c929a19 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>
2026-08-12 15:40:52 +02:00

95 lines
4.0 KiB
Markdown

# 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<bus>;
auto now = rtc::read_clock(); // result<date_time>
auto armed = rtc::set_alarm1({}, ds3231::alarm1_rate::once_per_second);
```
`example/main.cpp` is the full tour. libavr rides as the `libavr/` submodule,
pinned to the commit this driver builds against; `LIBAVR_ROOT` (cache or
environment) overrides it for development against a working tree:
```sh
git submodule update --init libavr
cmake --preset attiny85-generated
cmake --build --preset attiny85-generated
```
Presets cover attiny85/atmega328p in both libavr modes (generated and
reflect); `ctest` runs the consteval battery over the BCD, the hours
register's two formats, the weekday and the alarm rate encodings. The legacy
yazoalfa-based driver lives on the `master` branch.
The weekday register is derived from the date by default. A program that
assigns its own meaning to the device's 1..7 takes ownership of it, and no
write here touches it:
```cpp
using rtc = ds3231::device<bus, ds3231::weekday_source::external>;
```
An alarm arms its INT/SQW output unless told otherwise, and can be muted and
re-wired without re-arming:
```cpp
auto quiet = rtc::set_alarm2(at, ds3231::alarm2_rate::minutes_match,
ds3231::alarm_interrupt::off);
auto again = rtc::listen_alarm2();
```
## Atmel Studio
`master` carries a Studio solution, so this branch does too: `ide/ds3231.atsln`
builds `example/main.cpp` for both parts the presets cover, each to a
**byte-identical `.text`** against the CMake build (`check-flags.py` below is
what holds the flag sets equal, so the sizes are the presets' own), with the
flags mirrored by hand. CMake remains the build system.
`avrdevice` is a project-level property in Studio, so a part means a project, not
a configuration: `ide/atmega328p/` and `ide/attiny85/`. They need separate
directories rather than separate names — Studio builds into `<project
dir>/<Configuration>` whatever `OutputDirectory` says, so two projects sharing
one directory would also share one `example/main.o`, and building one after the
other without a rebuild would link the other part's object.
Studio finds libavr in the **submodule** at
`$(MSBuildProjectDirectory)\..\..\libavr\include` — correct by construction,
and anchored to the project because a plain relative path is resolved against
the generated makefile's directory (the configuration's output directory), not
the project's. There is no `LIBAVR_ROOT` escape hatch: a variable exported in a
shell is invisible to Studio launched from the Start menu — which is what the
submodule answers. It also needs a GCC 16.1 toolchain registered as flavour
`avr-g++-16.1.0`, nothing older reaching `-std=c++26`.
One generated file is required before the project will load, and one command
checks the flags have not drifted (both from libavr's `tools/atmelstudio/`):
```sh
for mcu in atmega328p attiny85; do
case $mcu in atmega328p) device=ATmega328P;; attiny85) device=ATtiny85;; esac
python libavr/tools/atmelstudio/componentinfo.py \
"ide/$mcu/ds3231-$mcu.componentinfo.xml" --device "$device"
python libavr/tools/atmelstudio/check-flags.py --solution ide/ds3231.atsln \
--project "ds3231-$mcu" \
--compile-commands "build/$mcu-generated/compile_commands.json" \
--log "build/as-$mcu.log"
done
```
One reference describes one part, hence `--project`. Release is what the gate
compares, the presets defining no debug build; Debug carries the `-Og
-gdwarf-4` pair libavr's own debug preset uses.
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.