Compare commits

..

33 Commits

Author SHA1 Message Date
50c424d3d2 deps: the pin advances to the identity check that reads its own sibling
libavr 93d8b0e. The check is handed the sibling build tree now and asks its build system whether the image is current, so a comparison against a tree the run did not build skips rather than passing. Gated at the pin. Its own gate builds both modes, so the comparison still happens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:23:57 +02:00
de894e6216 deps: the pin advances again, and the line ending stops being the editing machine's
The libavr pin moves to the preset loop's own fix. `tools/check-presets.sh` -
which this repository's gate calls - parsed `cmake --list-presets` by anchoring
a preset name's closing quote to the end of the line, and a preset carrying a
`displayName` is listed as `"name" - description`. The presets here are
undescribed, so the loop found them either way; one `displayName` away it would
have found none and said the repository had no presets. The loop has a test
aimed at it now, which it did not before - libavr runs none of its own presets
through it.

`.gitattributes` gains `* text=auto eol=lf`. Naming the source extensions left
Markdown, Python, shell and CMake to whatever wrote them, and on the Windows
side of this bench that is CRLF - which turns an eighty-line edit into a rewrite
of every file it touched and buries the change in the diff. Rule 50 carries the
sentence now, so it is the repository's default rather than a habit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 03:26:58 +02:00
52faaf9920 deps: the libavr pin advances, and this repository gets a committed gate
The pin moves to libavr's USI I2C rate fix and to the two override surfaces the
fan-controller port filed - a PWM solve that can be pinned, and a capture edge
that is a value rather than a template argument. Nothing here names any of
them; four presets green and no image moved.

`tools/check.sh` is new, and its absence was the defect: this repository had no
committed gate at all, so "run the suite" was a snippet somebody remembered -
and the order in a snippet is the one thing nobody re-derives. Every preset
built before any is tested is subtle and load-bearing, because the
mode-identity check reads the sibling mode's tree and a build-then-test-per-
preset run holds a fresh image against a stale sibling. The loop is libavr's
`tools/check-presets.sh`; nothing is written after the call, there being nothing
beyond the presets this repository knows to ask.

And `.vscode/settings.json` stops naming a toolchain prefix. It named
`D:/dev/libavr/local/toolchain/avr-gcc-16.1.0-mingw`: a path true of one machine
(libavr guidance rule 50) and, since the in-repo toolchain copies went, true of
none. `local/machine.cmake` is where a checkout says that, and it already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 02:39:15 +02:00
d778383d88 deps: the libavr pin advances, and the tiny85 image grows 24 B
The growth is not this bump's. It entered at libavr 32f8167, where the
software master started clocking the rate it is told: the phase padding has to
cover the shortest bit of a byte, and paying for it costs `shift_out` and
`read_acked` twelve bytes each on the tiny85. Those two symbols are the whole
of the move - an avr-nm diff across the two images names them and nothing else.

So the claim went unheld through two pin bumps. 1246 B was last measured at
libavr e697920; the tiny85 preset has read 1270 since 32f8167, which both
5a042a9 and f2205a7 pinned past without re-recording it. The number is stated
in test/CMakeLists.txt and in README.md, and this is what keeps them one fact -
a check that only ever ran on one of the two chips keeps neither.

The suite also stops depending on its host: libavr's launcher and skip marker
register every check everywhere, and one this machine cannot run reads as
Skipped rather than going missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:00:31 +02:00
f2205a7c53 deps: the libavr pin advances to the compare-debug fix
One commit, and it is the host check script this repo runs on every target
plus its own new test: `check-compare-debug.py` was splitting each recorded
compile command with POSIX rules, which eat a Windows compiler path, so that
check died here rather than reporting. Nothing a device image is compiled from
moved, and the comparison says so - every image this repo builds is
byte-for-byte the one it built at the previous pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:47:14 +02:00
5a042a99a4 deps: the libavr pin advances 53 commits
The furthest behind of the fleet, and the least affected: the clock example is
**byte-identical at 1480 B** across all fifty-three, so nothing this driver
emits moved. Four checks green.

Built at the old pin first for the comparison, which needed the host's tool
suffix named in the working tree - that pin predates the toolchain file
learning it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:15:51 +02:00
e105fd11f3 build: the libavr pin advances 93 commits
Built and tested at the new pin: 4/4, mode identity skipped for want of a
sibling reflect tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 22:38:02 +02:00
925106394b build: the filed i2c cost comes back, -70 B on the tiny85
1316 -> 1246. libavr took the finding this repo filed when its pin last moved:
the shared transaction ladder's cost was never the ladder but the argument
list reaching it - AVR passes eight bytes in call-clobbered registers and the
rest in call-saved ones, so twelve bytes made every forwarder save four it
never reads. A write has no read span, so the write path now passes eight.

Worth recording that the candidate this repo suggested was measured and
rejected upstream, and the numbers filed from here are why: it wins 66 B on a
driver like this one and charges a single-byte reader 20, which is a class
rather than one program.

The mega328P does not move at 1480 - its TWI backend has its own ladder and
was never charged. Net over the whole 22-commit advance the tiny85 is +8 B,
and all of it is the bit-period fix that made the wire rate stop following the
inliner.

Sixteen tests green, cross-mode identity held on both chips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:22:23 +02:00
bd604ec288 build: the libavr pin advances, and the tiny85 image grows 78 B
43fc479 -> aec9955. The mega328P does not move at 1480 B - it drives the RTC
from the TWI block, which neither change touches. The tiny85 has no TWI, so
dev::i2c resolves to the bit-banged master there and takes both:

  cb40a13, the bit period                      1238 -> 1262  (+24)
  943ffbe, one call shape over every master    1262 -> 1316  (+54)

The first is a bug being paid for. software_master delayed a fixed quarter
period and let its own instructions make up the rest of the bit, so the rate
on the wire followed the inliner; each bit now delays a low and a high phase
net of what that phase already spends. The DS3231 is a 400 kHz part and this
example asks for 100, so nothing here was out of spec - the bytes buy a rate
that no longer moves when the image does.

The second is a trade, and it is worth writing down which way it went. The
shared ladder is smaller than the two it replaced - run_transfer is 232 B
against read_regs 162 + write_regs 102 + stop 30 - but the call-site thunks
that marshal spans into it cost 116 B where the specialised ladders were
inlined, so a driver making both calls nets +54. Filed upstream with these
numbers rather than worked around here: which way the trade falls is a
property of the consumer's mix, and that is the library owner's call.

Size re-recorded in the two places that state it. Four presets green,
cross-mode identity held on both chips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 02:17:44 +02:00
176f78c2e9 test: the README's cross-mode claim is now checked, not just stated
The pin advances to the commit that gives consumers the gate's own image
comparison, and the claim written into this README a few commits ago - 1480 B
and 1238 B, byte-identical across the two modes - is registered against it.
Both chips compare clean over every loadable section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 20:49:59 +02:00
6e179fbac6 test: the example's size stops being a number nobody holds
Every other port in the fleet states its image size and is held to it; this
one stated none, so a library advance could move `clock` and no check would
say a word - which is exactly what the size claim was built after. The two
numbers are what the presets build today, byte-identical across generated
and reflect, and the README now says both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:20:24 +02:00
53ff3f1258 build: the libavr pin advances over the inlined vector
The vector that inlines its handler is the one advance that could reach a
driver on the two-wire bus, and it does not reach this one: `clock` drives
the bus from the foreground, so it is byte-identical across the advance in
all four modes - 1238 B on the attiny85, 1480 B on the atmega328p.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:08:07 +02:00
a02b051ab4 chore: machine-local state has a home here too
Guidance rule 15 puts every machine-local artefact under a gitignored local/,
and this repo could only have committed one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 03:32:52 +02:00
923c118c6d test: the format and ASCII rules stop being a habit, and the stated sizes stop drifting
libavr's guidance binds this repo too, and until now nothing here checked it -
`ctest` runs `libavr_format_test()` over this tree's own sources now (rules 11
and 33), skipping rather than passing where clang-format is absent. It caught
drift on its first run: a file written this week and edited after formatting.

Where the README states a measured size, `libavr_size_claim_test()` holds it to
the image and holds the image to the prose: advancing the library pin moved
three of these across the fleet with nothing saying so, and re-recording one
now requires the sentence that quotes it to move too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:02:40 +02:00
5b6bff8346 build: the libavr pin advances to the sweep's own record
Documentation only - the guideline sweep's condensed entry, the three measured
facts about class-type constants it produced, and the port filings it left
open. No header, tool or generated input moves, so every image is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:02:55 +02:00
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
572d38feef build: the libavr pin advances past the audit sweep, and the extents get counted
The pin crosses libavr's phase-6 close and the guideline sweep behind it;
the example is byte-identical on both chips in both modes.

The port's own sweep found rule 36 in seven places, and the fix had to be
measured rather than applied. Sakamoto's month offsets as a `std::to_array`
local cost **+348 bytes** on a 1480-byte image - an automatic class-type
constant inside a `constexpr` function is given an address and built per
call, which outlined `weekday()` whole and pulled in `__do_copy_data` and
`__udivmodhi4`; `static constexpr` is what folds it, and emits no symbol at
all. The six register buffers cost +4 as `to_array`, which materialises its
argument array before copying where an aggregate initialiser stores each
element as it is computed - so those take CTAD, which counts the extent and
keeps the direct initialisation. Both spellings are rule 36; only one is
free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:40:20 +02:00
bea81c97e0 editor: clangd and cmake work from a committed vscode workspace
The three files libavr's consumers carry: .vscode/settings.json names the
atmega328p-generated database and passes --query-driver, .clangd holds the
stand-ins clang needs for GCC's AVR dialect -- and no database, because this
driver is made to be vendored and the file travels with it -- and
extensions.json names the two extensions. The libavr pin advances to the
editor-audit fixes, without which every TU inherits device.hpp's
alias-shadowing errors. The preset builds green from the pin and clangd
reports zero errors on the example TU.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:48:26 +02:00
87e1f5e8a1 build: the libavr pin advances past phase 6, and the driver takes the bus whole
The register sugar now rides the bus's own master role - device<Bus>
resolves avr::i2c::device<typename Bus::master, bus_address>, so a
consumer keeps handing over its declared dev::i2c<...> unchanged - and
the DS3231's hardwired 0x68 is a named constant. The example's four
discards became the LED's own error signal (a failed seed or a failed
alarm clear holds it dark, the same word a stuck bus says), the tree is
reformatted under InsertBraces, the sources are ASCII, and the README's
stale Studio byte counts are replaced by the claim its check-flags gate
holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:46:10 +02:00
2909d0bc6c build: the libavr pin advances past the consumer-report fixes
timer::engine gains stop()/start() and a runtime TOP, adc gains
disable()/enable(), and libavr_programming_targets() stops leaving .fuse bytes
in the flash HEX. Every one of them is additive, and this port adopts none of
them yet: its 1 built image comes out byte-identical across the pin change,
which is what the advance is here to keep true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:18:55 +02:00
c49ada8a71 build: the libavr pin advances to the v9 era
Built and tested against it in a clean checkout of this port, through its own
submodule rather than a working-tree override, so the pin is what was proved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:46:36 +02:00
5c14c340e0 build: the libavr pin advances over the trait projection
The de-string-2 pass upstream: every peripheral block behind generated
instance traits, the string layer gone. Nothing here changes — both
examples rebuild byte-identically across modes at their recorded sizes
(tiny85 1212 B, mega328P 1242 B).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:59:11 +02:00
52ef7cb791 build: the libavr pin advances to current main
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:24:09 +02:00
c023b5ed31 build: the libavr pin advances to current main
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 06:48:36 +02:00
d219b299f4 build: libavr rides as the pinned submodule
The submodule replaces FetchContent and the unpinned forge fallback;
LIBAVR_ROOT stays as the tandem-development override, the presets take the
toolchain file from the submodule, and the Studio project's include path
anchors there — correct by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 00:29:38 +02:00
0ecd53856b ide: the attiny85 beside the atmega328p, one directory each
The presets cover both parts, so the solution should too. Studio carries
avrdevice as a project property, so a part is a project rather than a
configuration, and both build example/main.cpp to a .text byte-identical to
their CMake counterpart: 1242 B on the mega, 1212 B on the tiny, each with the
architecture its own object file says.

They need separate directories, not just separate names. Studio builds into
<project dir>/<Configuration> whatever OutputDirectory claims — setting it per
project creates the directory and then ignores it — so two projects in one
directory share example/main.o. Verified rather than assumed: with both in ide/,
the single shared object read avr:25 while the mega's ELF read avr:5, so a
build of one after the other without a rebuild links the wrong part's object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:51:50 +02:00
dc89d0779a ide: the Atmel Studio solution master has, on the libavr port
master opens in Studio, so this branch should too. The solution builds
example/main.cpp — the port's only executable, the driver itself being an
INTERFACE library — for the ATmega328P that solution targeted, and the
acceptance is not that it builds but that it builds the same firmware: .text
comes out byte-identical to CMake's at 1242 B.

One project, one chip. Studio carries avrdevice as a project property, so the
attiny85 half of the presets would need a second project rather than a second
configuration; CMake stays the build system and covers both.

libavr is found beside this repo, anchored to the project directory rather than
written relative to the generated makefile, which runs from the configuration's
output directory. No LIBAVR_ROOT: a variable exported in a shell is invisible to
Studio launched from the Start menu.

Both configurations verified headless with LIBAVR_ROOT unset, and the flag
mirror checked against the CMake build's own compile_commands.json.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:33:39 +02:00
cdca5219d0 Rewrite on libavr
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 <noreply@anthropic.com>
2026-07-18 01:09:43 +02:00
8a6170cb10 Add get alarm helper to reduce code duplication 2020-05-17 20:11:35 +02:00
21f9215bba Implement getting alarm times 2020-05-17 20:06:13 +02:00
dfd2289aef Reduce code duplication 2020-05-17 19:54:46 +02:00
14e608d397 Implement setting alarms 2020-05-17 19:41:53 +02:00
1bc7e66389 Move alarm rate from inside details to visible namespace 2020-05-17 19:19:28 +02:00
23 changed files with 1046 additions and 854 deletions

View File

@@ -1,13 +1,15 @@
--- ---
BasedOnStyle: LLVM BasedOnStyle: LLVM
Standard: Latest
ColumnLimit: 120 ColumnLimit: 120
IndentWidth: 4 IndentWidth: 4
TabWidth: 4 TabWidth: 4
UseTab: ForIndentation UseTab: ForIndentation
AlignEscapedNewlines: DontAlign AlignEscapedNewlines: DontAlign
AllowShortFunctionsOnASingleLine: Empty AllowShortFunctionsOnASingleLine: Empty
AlwaysBreakTemplateDeclarations: true BreakTemplateDeclarations: Yes
BreakBeforeBraces: Custom BreakBeforeBraces: Custom
BraceWrapping: BraceWrapping:
AfterFunction: true AfterFunction: true
InsertBraces: true
... ...

32
.clangd Normal file
View File

@@ -0,0 +1,32 @@
# Editor accommodations for the second frontend. No compilation database is
# named here: this driver is made to be vendored, and this file travels with
# it — a consumer's own database then covers these headers, at the consumer's
# chip. The checkout that is opened as a folder names its build tree in
# .vscode/settings.json instead.
CompileFlags:
Add:
# clang has no 24-bit integer and GCC's are keywords, not macros, so the
# editor needs a stand-in for avr::uint24_t. The next width up is the only
# one available — clang rejects _BitInt(24) on this target.
- -D__uint24=unsigned long
- -D__int24=long
# clangd forwards the driver's system includes but not its own header
# directory, so <stdint.h> resolves to avr-libc's, which still gates the
# limit and constant macros on the C++98 opt-in.
- -D__STDC_LIMIT_MACROS
- -D__STDC_CONSTANT_MACROS
# isr::emit spells a vector number into [[gnu::signal(N)]], which clang
# rejects rather than ignores — enough of them in one TU to reach the
# default limit of 19 inside the headers and truncate the parse.
- -ferror-limit=0
Remove:
# The build promotes warnings for the compiler that has to be right about
# them; in the editor the flag paints a second frontend's opinions in the
# colour reserved for things that do not compile.
- -Werror
Diagnostics:
Suppress:
# clang's AVR `signal` attribute takes no arguments and it knows none of
# progmem, naked or OS_main. A misspelling is what the build is for.
- attribute_wrong_number_arguments
- unknown-attributes

13
.gitattributes vendored
View File

@@ -1,8 +1,11 @@
*.h eol=lf # Line endings are the repository's, not the editing machine's: this checkout
*.hpp eol=lf # is reached from two hosts, and a file rewritten by a Windows tool comes back
*.c eol=lf # with every line changed unless something says otherwise. Naming the source
*.cpp eol=lf # extensions left Markdown, Python, shell and CMake to whatever the writing
.git* eol=lf # tool defaulted to, which is CRLF on one of the two.
* text=auto eol=lf
# Atmel Studio writes these and expects them back.
*.vcxproj* eol=crlf *.vcxproj* eol=crlf
*.cppproj eol=crlf *.cppproj eol=crlf
*.sln eol=crlf *.sln eol=crlf

21
.gitignore vendored
View File

@@ -1,11 +1,10 @@
.vs build/
Release local/
Debug .cache/
*.componentinfo.xml
*.elf # Atmel Studio: generated per machine, its build outputs, and per-user state
*.o ide/*/*.componentinfo.xml
*.hex ide/*/Debug/
*.srec ide/*/Release/
*.eeprom ide/.vs/
*.lss ide/*.log
*.map

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "libavr"]
path = libavr
url = ../libavr.git

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"llvm-vs-code-extensions.vscode-clangd",
"ms-vscode.cmake-tools"
]
}

36
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,36 @@
{
// clangd is the language server; the cpptools engine would parse every file
// a second time and disagree, since nothing tells it about a cross
// compiler.
"C_Cpp.intelliSenseEngine": "disabled",
// --query-driver lets clangd ask the cross compiler for its own system
// includes and target. The database is named here rather than in .clangd
// because that file travels with the driver into a consumer's submodule,
// where a build tree of this repo's own need not exist.
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated",
"--query-driver=**avr-g++*",
"--header-insertion=never"
],
// The presets are the build interface, and the toolchain file inside the
// libavr submodule is the one place the compiler is chosen. **No prefix is
// named here**: a committed file may not name a path that is true of one
// machine (libavr guidance rule 50), so the gitignored local/machine.cmake
// at this repository's root is where a checkout says where its toolchain
// is - one file, and it answers for both hosts.
"cmake.useCMakePresets": "always",
"cmake.configureOnOpen": true,
"cmake.options.statusBarVisibility": "compact",
"files.watcherExclude": {
"**/build/**": true,
"**/libavr/**": true
},
"files.associations": {
".clangd": "yaml",
".clang-format": "yaml"
}
}

29
CMakeLists.txt Normal file
View File

@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.28)
project(ds3231 LANGUAGES CXX)
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
# overrides it for tandem development against a working tree. The toolchain
# file comes from the submodule via CMakePresets.json either way.
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
endif()
if(NOT LIBAVR_ROOT)
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
endif()
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
endif()
add_subdirectory(${LIBAVR_ROOT} libavr-build)
add_library(ds3231 INTERFACE)
target_include_directories(ds3231 INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(ds3231 INTERFACE libavr)
include(${LIBAVR_ROOT}/cmake/checks.cmake)
if(PROJECT_IS_TOP_LEVEL)
add_subdirectory(example)
enable_testing()
add_subdirectory(test)
endif()

43
CMakePresets.json Normal file
View File

@@ -0,0 +1,43 @@
{
"version": 8,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "${sourceDir}/libavr/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" }
]
}

96
README.md Normal file
View File

@@ -0,0 +1,96 @@
# 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, and holds
the example to its stated size — **1480 B** on the mega328P and **1270 B**
on the tiny85, byte-identical across the two modes. 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.

View File

@@ -1,320 +0,0 @@
#pragma once
#include "../clock.hpp"
#include <stddef.h>
#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 <typename I2cDriver, bool SetDayOfWeek = true>
class DS3231 {
using i2c_t = i2c::I2c<I2cDriver>;
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<TIME_REG_ADDR>();
Date date;
date.year = timeReg.getYear();
date.month = timeReg.getMonth();
date.day = timeReg.getDate();
return date;
}
static auto getTime()
{
const auto timeReg = readRegister<TIME_REG_ADDR>();
Time time;
time.hour = timeReg.getHours();
time.minute = timeReg.getMinutes();
time.second = timeReg.getSeconds();
return time;
}
static auto getDateTime()
{
const auto timeReg = readRegister<TIME_REG_ADDR>();
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 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<DATE_START_OFFSET, DATE_END_OFFSET>(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<TIME_START_OFFSET, TIME_END_OFFSET>(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<START_OFFSET, END_OFFSET>(timeReg);
}
static bool checkAlarm1()
{
return checkAlarmHelper<detail::ControlStatusRegFlags::A1F>();
}
static bool checkAlarm2()
{
return checkAlarmHelper<detail::ControlStatusRegFlags::A2F>();
}
static void clearAlarm1()
{
clearAlarmHelper<detail::ControlStatusRegFlags::A1F>();
}
static void clearAlarm2()
{
clearAlarmHelper<detail::ControlStatusRegFlags::A2F>();
}
private:
template <uint8_t Address, typename Register>
static Register readRegisterHelper()
{
i2c_t::template start<I2C_ADDRESS>(false);
i2c_t::write(Address);
i2c_t::stop();
Register reg;
i2c_t::template start<I2C_ADDRESS>(true);
i2c_t::template readBytes<sizeof(Register)>(reinterpret_cast<uint8_t *>(&reg));
i2c_t::stop();
return reg;
}
template <uint8_t Address>
static auto readRegister()
{
if constexpr (Address == TIME_REG_ADDR) {
return readRegisterHelper<Address, detail::TimeReg>();
} else if constexpr (Address == ALARM1_REG_ADDR) {
return readRegisterHelper<Address, detail::Alarm1Reg>();
} else if constexpr (Address == ALARM2_REG_ADDR) {
return readRegisterHelper<Address, detail::Alarm2Reg>();
} else if constexpr (Address == CONTROL_REG_ADDR) {
return readRegisterHelper<Address, detail::ControlReg>();
} else if constexpr (Address == CONTROL_STATUS_REG_ADDR) {
return readRegisterHelper<Address, detail::ControlStatusReg>();
} else if constexpr (Address == AGING_OFFSET_REG_ADDR) {
return readRegisterHelper<Address, detail::AgingOffsetReg>();
} else if constexpr (Address == TEMP_REG_ADDR) {
return readRegisterHelper<Address, detail::TempReg>();
} else {
static_assert(util::always_false_v<decltype(Address)>, "Invalid register address");
}
}
template <uint8_t StartOffset, uint8_t EndOffset, typename Register>
static void writePartialRegister(const Register &reg)
{
constexpr auto getRegisterAddress = []() {
if constexpr (util::is_same_v<Register, detail::TimeReg>) {
return TIME_REG_ADDR;
} else if constexpr (util::is_same_v<Register, detail::Alarm1Reg>) {
return ALARM1_REG_ADDR;
} else if constexpr (util::is_same_v<Register, detail::Alarm2Reg>) {
return ALARM2_REG_ADDR;
} else if constexpr (util::is_same_v<Register, detail::ControlReg>) {
return CONTROL_REG_ADDR;
} else if constexpr (util::is_same_v<Register, detail::ControlStatusReg>) {
return CONTROL_STATUS_REG_ADDR;
} else if constexpr (util::is_same_v<Register, detail::AgingOffsetReg>) {
return AGING_OFFSET_REG_ADDR;
} else if constexpr (util::is_same_v<Register, detail::TempReg>) {
return TEMP_REG_ADDR;
} else {
static_assert(util::always_false_v<Register>, "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<I2C_ADDRESS>(false);
i2c_t::write(ADDRESS + StartOffset);
i2c_t::template writeBytes<WRITE_SIZE>(reinterpret_cast<const uint8_t *>(&reg) + StartOffset);
i2c_t::stop();
}
template <typename Register>
static void writeRegister(const Register &reg)
{
writePartialRegister<0, sizeof(Register) - 1>(reg);
}
template <detail::ControlStatusRegFlags AlarmFlag>
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<CONTROL_STATUS_REG_ADDR>();
return alarmStatus == AlarmFlag;
}
template <detail::ControlStatusRegFlags AlarmFlag>
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<CONTROL_STATUS_REG_ADDR>();
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

3
example/CMakeLists.txt Normal file
View File

@@ -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} $<TARGET_FILE:clock>)

37
example/main.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include <ds3231/ds3231.hpp>
#include <libavr/libavr.hpp>
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<bus>;
using led = dev::output<avr::pb3>;
int main()
{
avr::init<bus, led>();
if (auto stopped = rtc::oscillator_stopped(); stopped.value_or(false)) {
// A failed seed leaves the LED dark before the loop ever runs - the
// same signal a stuck bus gives it below.
const bool seeded = rtc::write_clock({{2026, 1, 1}, {12, 0, 0}}) &&
rtc::set_alarm1({}, ds3231::alarm1_rate::once_per_second) &&
rtc::clear_oscillator_stopped();
led::write(seeded);
}
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) && !rtc::clear_alarm1()) {
led::clear(); // a clear that failed holds the LED dark, like any bus error
}
dev::delay<100_ms>();
}
}

View File

@@ -1,88 +0,0 @@
#pragma once
namespace rtc {
namespace detail {
template <typename FlagsT>
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<uint8_t>(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<uint8_t>(rhs);
return *this;
}
FlagsImpl &operator|=(const FlagsT &flag)
{
data |= static_cast<uint8_t>(flag);
return *this;
}
FlagsImpl &operator|=(const uint8_t &flag)
{
data |= flag;
return *this;
}
FlagsImpl &operator&=(const FlagsT &flag)
{
data &= static_cast<uint8_t>(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<uint8_t>(flag);
}
};
template <typename FlagsT>
FlagsT operator|(const FlagsT &lhs, const FlagsT &rhs)
{
const auto lhsInt = static_cast<uint8_t>(lhs);
const auto rhsInt = static_cast<uint8_t>(rhs);
return static_cast<FlagsT>(lhsInt | rhsInt);
}
template <typename FlagsT>
FlagsT operator&(const FlagsT &lhs, const FlagsT &rhs)
{
const auto lhsInt = static_cast<uint8_t>(lhs);
const auto rhsInt = static_cast<uint8_t>(rhs);
return static_cast<FlagsT>(lhsInt & rhsInt);
}
} // namespace detail
} // namespace rtc

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>7.0</ProjectVersion>
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
<ProjectGuid>7b3e5a41-9c26-4d18-8f52-6a0d31c74be9</ProjectGuid>
<avrdevice>ATmega328P</avrdevice>
<avrdeviceseries>none</avrdeviceseries>
<OutputType>Executable</OutputType>
<Language>CPP</Language>
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
<OutputFileExtension>.elf</OutputFileExtension>
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
<AssemblyName>ds3231-atmega328p</AssemblyName>
<Name>ds3231-atmega328p</Name>
<RootNamespace>ds3231</RootNamespace>
<ToolchainFlavour>avr-g++-16.1.0</ToolchainFlavour>
<KeepTimersRunning>true</KeepTimersRunning>
<OverrideVtor>false</OverrideVtor>
<CacheFlash>true</CacheFlash>
<ProgFlashFromRam>true</ProgFlashFromRam>
<RamSnippetAddress>0x20000000</RamSnippetAddress>
<UncachedRange />
<preserveEEPROM>true</preserveEEPROM>
<OverrideVtorValue>exception_table</OverrideVtorValue>
<BootSegment>2</BootSegment>
<ResetRule>0</ResetRule>
<eraseonlaunchrule>0</eraseonlaunchrule>
<EraseKey />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<ToolchainSettings>
<AvrGccCpp>
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>NDEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\..\include</Value>
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<ToolchainSettings>
<AvrGccCpp>
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>DEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\..\include</Value>
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize debugging experience (-Og)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\example\main.cpp">
<SubType>compile</SubType>
<Link>example\main.cpp</Link>
</Compile>
<Compile Include="..\..\include\ds3231\ds3231.hpp">
<SubType>compile</SubType>
<Link>include\ds3231\ds3231.hpp</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="example" />
<Folder Include="include" />
<Folder Include="include\ds3231" />
</ItemGroup>
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
</Project>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>7.0</ProjectVersion>
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
<ProjectGuid>1d84c0f7-52b9-4a63-b7e1-9f2c48d5a306</ProjectGuid>
<avrdevice>ATtiny85</avrdevice>
<avrdeviceseries>none</avrdeviceseries>
<OutputType>Executable</OutputType>
<Language>CPP</Language>
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
<OutputFileExtension>.elf</OutputFileExtension>
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
<AssemblyName>ds3231-attiny85</AssemblyName>
<Name>ds3231-attiny85</Name>
<RootNamespace>ds3231</RootNamespace>
<ToolchainFlavour>avr-g++-16.1.0</ToolchainFlavour>
<KeepTimersRunning>true</KeepTimersRunning>
<OverrideVtor>false</OverrideVtor>
<CacheFlash>true</CacheFlash>
<ProgFlashFromRam>true</ProgFlashFromRam>
<RamSnippetAddress>0x20000000</RamSnippetAddress>
<UncachedRange />
<preserveEEPROM>true</preserveEEPROM>
<OverrideVtorValue>exception_table</OverrideVtorValue>
<BootSegment>2</BootSegment>
<ResetRule>0</ResetRule>
<eraseonlaunchrule>0</eraseonlaunchrule>
<EraseKey />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<ToolchainSettings>
<AvrGccCpp>
<avrgcc.common.Device>-mmcu=attiny85</avrgcc.common.Device>
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>NDEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\..\include</Value>
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<ToolchainSettings>
<AvrGccCpp>
<avrgcc.common.Device>-mmcu=attiny85</avrgcc.common.Device>
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>DEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\..\include</Value>
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize debugging experience (-Og)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\example\main.cpp">
<SubType>compile</SubType>
<Link>example\main.cpp</Link>
</Compile>
<Compile Include="..\..\include\ds3231\ds3231.hpp">
<SubType>compile</SubType>
<Link>include\ds3231\ds3231.hpp</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="example" />
<Folder Include="include" />
<Folder Include="include\ds3231" />
</ItemGroup>
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
</Project>

28
ide/ds3231.atsln Normal file
View File

@@ -0,0 +1,28 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Atmel Studio Solution File, Format Version 11.00
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "ds3231-atmega328p", "atmega328p\ds3231-atmega328p.cppproj", "{7B3E5A41-9C26-4D18-8F52-6A0D31C74BE9}"
EndProject
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "ds3231-attiny85", "attiny85\ds3231-attiny85.cppproj", "{1D84C0F7-52B9-4A63-B7E1-9F2C48D5A306}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|AVR = Debug|AVR
Release|AVR = Release|AVR
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7B3E5A41-9C26-4D18-8F52-6A0D31C74BE9}.Debug|AVR.ActiveCfg = Debug|AVR
{7B3E5A41-9C26-4D18-8F52-6A0D31C74BE9}.Debug|AVR.Build.0 = Debug|AVR
{7B3E5A41-9C26-4D18-8F52-6A0D31C74BE9}.Release|AVR.ActiveCfg = Release|AVR
{7B3E5A41-9C26-4D18-8F52-6A0D31C74BE9}.Release|AVR.Build.0 = Release|AVR
{1D84C0F7-52B9-4A63-B7E1-9F2C48D5A306}.Debug|AVR.ActiveCfg = Debug|AVR
{1D84C0F7-52B9-4A63-B7E1-9F2C48D5A306}.Debug|AVR.Build.0 = Debug|AVR
{1D84C0F7-52B9-4A63-B7E1-9F2C48D5A306}.Release|AVR.ActiveCfg = Release|AVR
{1D84C0F7-52B9-4A63-B7E1-9F2C48D5A306}.Release|AVR.Build.0 = Release|AVR
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

375
include/ds3231/ds3231.hpp Normal file
View File

@@ -0,0 +1,375 @@
#pragma once
#include <array>
#include <cstdint>
#include <expected>
#include <span>
#include <libavr/i2c.hpp>
namespace ds3231 {
using avr::i2c::error;
using avr::i2c::status;
template <typename T>
using result = std::expected<T, error>;
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,
};
// Who owns the weekday register. The device counts 1..7 with a free epoch and
// maintains nothing itself, so either this driver derives it from the date on
// every write, or the register belongs to the program and no write here
// touches it - which is what a caller that assigns its own meaning to those
// seven values needs.
enum class weekday_source : std::uint8_t { computed, external };
// Whether arming an alarm also wires its INT/SQW output. `off` is not
// silence about the bit - it clears A1IE/A2IE, so an alarm armed with the
// interrupt on and re-armed with it off stops driving the pin.
enum class alarm_interrupt : std::uint8_t { off, on };
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<std::uint8_t>(((value / 10) << 4) | (value % 10));
}
constexpr std::uint8_t from_bcd(std::uint8_t value)
{
return static_cast<std::uint8_t>((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<std::uint8_t>(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)
{
static constexpr auto offsets = std::to_array<std::uint8_t>({0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4});
if (month < 3) {
--year;
}
return static_cast<std::uint8_t>((year + year / 4 - year / 100 + year / 400 + offsets[month - 1] + day) % 7 + 1);
}
} // namespace detail
// The one bus address a DS3231 answers on - hardwired in the die, no
// address pins (datasheet 19-5170, "I2C interface").
inline constexpr std::uint8_t bus_address = 0x68;
// Maxim DS3231 RTC on any libavr i2c bus (register map: datasheet
// 19-5170 Table 1): the master role is taken from the bus's own
// resolution, so a declared `dev::i2c<...>` is handed over whole. Every
// call is one bus transaction; errors surface as std::expected. Weekday
// registers are derived from the date unless the program owns them.
template <typename Bus, weekday_source Weekday = weekday_source::computed>
class device {
static constexpr bool computes_weekday = Weekday == weekday_source::computed;
using dev = avr::i2c::device<typename Bus::master, bus_address>;
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<date_time> read_clock()
{
std::array<std::uint8_t, 7> raw;
if (auto s = dev::read_regs(reg_clock, raw); !s) {
return std::unexpected(s.error());
}
date_time now;
static_cast<time_of_day &>(now) = decode_time(raw);
now.day = detail::from_bcd(raw[4] & 0x3f);
now.month = detail::from_bcd(raw[5] & 0x1f);
now.year = static_cast<std::uint16_t>(2000 + detail::from_bcd(raw[6]));
return now;
}
[[nodiscard]] static result<date> read_date()
{
auto now = read_clock();
if (!now) {
return std::unexpected(now.error());
}
return static_cast<date>(*now);
}
[[nodiscard]] static result<time_of_day> read_time()
{
std::array<std::uint8_t, 3> raw;
if (auto s = dev::read_regs(reg_clock, raw); !s) {
return std::unexpected(s.error());
}
return decode_time(raw);
}
[[nodiscard]] static status write_clock(const date_time &now)
{
if constexpr (!computes_weekday) {
// Two transactions, because the weekday register sits between the
// time and the date and this mode may not write it.
if (auto s = write_time(now); !s) {
return s;
}
return write_date(now);
} else {
std::array raw{detail::to_bcd(now.second),
detail::to_bcd(now.minute),
detail::to_bcd(now.hour),
detail::weekday(now.year, now.month, now.day),
detail::to_bcd(now.day),
detail::to_bcd(now.month),
detail::to_bcd(static_cast<std::uint8_t>(now.year % 100))};
return dev::write_regs(reg_clock, raw);
}
}
[[nodiscard]] static status write_date(const date &value)
{
if constexpr (computes_weekday) {
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<std::uint8_t>(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<std::uint8_t>(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,
alarm_interrupt interrupt = alarm_interrupt::on)
{
auto m = static_cast<std::uint8_t>(rate);
std::array raw{static_cast<std::uint8_t>(detail::to_bcd(at.second) | ((m & 1) << 7)),
static_cast<std::uint8_t>(detail::to_bcd(at.minute) | (((m >> 1) & 1) << 7)),
static_cast<std::uint8_t>(detail::to_bcd(at.hour) | (((m >> 2) & 1) << 7)),
static_cast<std::uint8_t>(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 set_alarm_interrupt(a1ie, interrupt);
}
[[nodiscard]] static status set_alarm2(const date_time &at, alarm2_rate rate,
alarm_interrupt interrupt = alarm_interrupt::on)
{
auto m = static_cast<std::uint8_t>(rate);
std::array raw{static_cast<std::uint8_t>(detail::to_bcd(at.minute) | ((m & 1) << 7)),
static_cast<std::uint8_t>(detail::to_bcd(at.hour) | (((m >> 1) & 1) << 7)),
static_cast<std::uint8_t>(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 set_alarm_interrupt(a2ie, interrupt);
}
[[nodiscard]] static result<date_time> read_alarm1()
{
std::array<std::uint8_t, 4> 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<date_time> read_alarm2()
{
std::array<std::uint8_t, 3> 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<bool> alarm1_fired()
{
return flag_set(a1f);
}
[[nodiscard]] static result<bool> 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<bool> 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<std::int16_t> temperature_quarters()
{
std::array<std::uint8_t, 2> raw;
if (auto s = dev::read_regs(reg_temp, raw); !s) {
return std::unexpected(s.error());
}
return static_cast<std::int16_t>((static_cast<std::int16_t>(static_cast<std::int8_t>(raw[0])) << 2) |
(raw[1] >> 6));
}
// The alarms' INT/SQW wiring on its own, for a caller that arms once and
// mutes later. Alarm 1 and alarm 2 share INTCN, so muting one leaves the
// other's route intact.
[[nodiscard]] static status listen_alarm1()
{
return set_alarm_interrupt(a1ie, alarm_interrupt::on);
}
[[nodiscard]] static status mute_alarm1()
{
return set_alarm_interrupt(a1ie, alarm_interrupt::off);
}
[[nodiscard]] static status listen_alarm2()
{
return set_alarm_interrupt(a2ie, alarm_interrupt::on);
}
[[nodiscard]] static status mute_alarm2()
{
return set_alarm_interrupt(a2ie, alarm_interrupt::off);
}
private:
// Seconds, minutes and hours decode the same way wherever they are read
// from - the high bit of the first two is reserved, and the hours register
// carries its own 12/24-hour flag.
static constexpr time_of_day decode_time(std::span<const std::uint8_t> clock)
{
return {detail::hours_from_reg(clock[2]), detail::from_bcd(clock[1] & 0x7f), detail::from_bcd(clock[0] & 0x7f)};
}
static constexpr std::uint8_t day_date(std::uint8_t day, bool weekday_mode)
{
if (weekday_mode) {
return static_cast<std::uint8_t>(0x40 | (day & 0x0f));
}
return detail::to_bcd(day) & std::uint8_t{0x3f};
}
// INTCN routes the alarms to INT/SQW and BBSQW is the square wave that
// would otherwise drive it, so arming an alarm's interrupt takes the pin
// and muting it gives up only that alarm's claim on it.
static status set_alarm_interrupt(std::uint8_t enable_bit, alarm_interrupt interrupt)
{
auto control = dev::read_reg(reg_control);
if (!control) {
return std::unexpected(control.error());
}
const auto wired = static_cast<std::uint8_t>((*control & ~bbsqw) | intcn | enable_bit);
const auto muted = static_cast<std::uint8_t>(*control & ~enable_bit);
return dev::write_reg(reg_control, interrupt == alarm_interrupt::on ? wired : muted);
}
static result<bool> 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<std::uint8_t>(*flags & ~bit));
}
};
} // namespace ds3231

1
libavr Submodule

Submodule libavr added at 93d8b0e491

View File

@@ -1,429 +0,0 @@
#pragma once
#include <stdint.h>
#include "flags.hpp"
namespace rtc {
namespace detail {
//////////////////////////////////////////////////////////////////////////
template <uint8_t Mask = 0xFF>
static inline uint8_t toBcd(const uint8_t &data)
{
return ((data / 10 * 16) + (data % 10)) & Mask;
}
template <uint8_t Mask = 0xFF>
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<HOUR_12_MASK>(hoursReg);
if (hour12 == 12 && !pmFlag)
return 0;
return hour12 + pmFlag ? 12 : 0;
} else // 24 hour format
{
constexpr auto HOUR_MASK = 0b00111111;
return fromBcd<HOUR_MASK>(hoursReg);
}
}
template <uint8_t Mask = 0b01111111>
static inline uint8_t getMaskedBcd(const uint8_t &reg)
{
return fromBcd<Mask>(reg);
}
template <uint8_t Mask = 0b01111111>
static inline void setMaskedBcd(uint8_t &reg, const uint8_t &value)
{
reg &= ~Mask;
reg |= toBcd<Mask>(value);
}
static inline bool getEncodedDay(uint8_t &value, const uint8_t &reg)
{
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 &reg)
{
constexpr auto DAY_FLAG = 6;
if (!((reg >> DAY_FLAG) & 1)) {
value = getMaskedBcd<0b00111111>(reg);
return true;
}
return false;
}
static inline void setEncodedDay(uint8_t &reg, 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 &reg, 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;
enum class AlarmRate {
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,
};
//////////////////////////////////////////////////////////////////////////
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 AlarmRate 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<AlarmRate>(dayFormat);
}
return static_cast<AlarmRate>(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 AlarmRate &alarmRate)
{
const auto alarmRateFlags = static_cast<uint8_t>(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;
enum class AlarmRate {
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,
};
//////////////////////////////////////////////////////////////////////////
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 AlarmRate 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<AlarmRate>(dayFormat);
}
return static_cast<AlarmRate>(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 AlarmRate &alarmRate)
{
const auto alarmRateFlags = static_cast<uint8_t>(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<uint8_t>(flag);
}
struct [[gnu::packed]] ControlReg : FlagsImpl<ControlRegFlags>{};
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<uint8_t>(flag);
}
struct [[gnu::packed]] ControlStatusReg : FlagsImpl<ControlStatusRegFlags>{};
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

27
test/CMakeLists.txt Normal file
View File

@@ -0,0 +1,27 @@
libavr_format_test()
# The example's size is stated in the README, per chip, and a library advance
# that moves it silently is what this catches.
if(LIBAVR_MCU STREQUAL "atmega328p")
libavr_size_claim_test(clock 1480)
elseif(LIBAVR_MCU STREQUAL "attiny85")
libavr_size_claim_test(clock 1270)
endif()
# The README says the two modes emit the same image, and only a tree with
# both built can say whether they do.
libavr_mode_identity_test(clock)
# 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)
# A check this host cannot run reads as Skipped rather than Failed: the marker
# a stubbed launcher prints, over every test registered here. Nothing in this
# suite is host-gated today, and this is what keeps that a property of the
# suite rather than of whoever adds the next check.
libavr_skip_unverified()

82
test/consteval.cpp Normal file
View 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

11
tools/check.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# The task gate: every configure preset built, and only then every one tested.
#
# The loop itself is libavr's (tools/check-presets.sh) because the order in it
# is subtle and was got wrong by hand more than once - the mode-identity checks
# read the sibling mode's tree, so a build-then-test-per-preset run compares a
# fresh image against a stale sibling. Anything this repository needs beyond
# the presets is written after the call, where every tree is built and tested.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
"${LIBAVR_ROOT:-libavr}/tools/check-presets.sh" "$@"