diff --git a/.claude/skills/dmod-boot/SKILL.md b/.claude/skills/dmod-boot/SKILL.md new file mode 100644 index 0000000..efe6bb1 --- /dev/null +++ b/.claude/skills/dmod-boot/SKILL.md @@ -0,0 +1,65 @@ +--- +name: dmod-boot +description: Explains dmod-boot, the firmware entry point that initializes hardware/FreeRTOS/heap/VFS/logging and boots into a loaded module package — how module packaging (modules.dmp) works, how to test a locally-built .dmf on real hardware without a release, and the flash/connect/monitor CMake targets for working with real boards. Load this when working in the dmod-boot repo, tracing how firmware boots into a usable system, or flashing/debugging a board. Companion to the dmod-ecosystem skill. +--- + +# `dmod-boot`: the entry point + +`dmod-boot` is the bootloader/firmware entry point for a real board. It +initializes hardware, FreeRTOS (via `dmosi-freertos`), heap (`dmheap`), VFS +(`dmvfs`, mounting `dmramfs` on `/`, `dmdevfs` on `/dev`), logging (`dmlog`), +then loads a package of modules embedded directly in ROM at link time and +starts a main module — typically the shell, `dmell`. If you're tracing "how +does firmware boot into a usable system", `dmod-boot/src/main.c` is the +starting point. + +**Module packaging.** During the build, `dmod-boot` bundles the modules it +finds in `/build/dmf/` into a single `modules.dmp` package, which +is then embedded in flash alongside the rest of the firmware (`modules/modules.dmd` +is the manifest that determines which modules get fetched/built into +`build/dmf/` in the first place). A module can be present there in two forms: +`.dmf` (plain) and `.dmfc` (the same module, fastlz-compressed). +`dmf-get` normally downloads both, and **the `.dmfc` takes precedence when the +module is loaded**. + +**Testing a locally-built module on real hardware, without a release:** drop +the `.dmf` binary straight into `/build/dmf/`, **delete that +module's `.dmfc` from the same directory**, then delete +`/build/modules.dmp` and `/build/__modules_dmp.o` so the +next build is forced to regenerate the package (an incremental build won't +notice a `.dmf` was added/changed in place otherwise). + +Skipping the `.dmfc` step is the classic trap: the build succeeds, the new +`.dmf` really is in `build/dmf/`, and the board still silently runs the old +released module. It usually surfaces as a *dependency* error rather than an +obvious "stale module" one — the released build was linked against older +versions of its dependencies, so you get +`Module version mismatch: != ` and +`API '@:/' is not connected`, followed by the module failing +to enable. Two things make this hard to see: `strings build/modules.dmp` will +*not* find the offending signature (it is compressed inside the `.dmfc`), and +comparing `.dmf`/`.dmfc` timestamps in `build/dmf/` is the quickest way to +spot it — every module should have a matching pair, so the one module whose +two files disagree is the culprit. + +**Beware of reconfiguring while testing a local module.** Re-running +`cmake -S . -B ` (as opposed to `cmake --build `) re-runs +`dmf-get`, which re-downloads *every* module into `build/dmf/` and overwrites +any `.dmf` you dropped there by hand — and restores the `.dmfc` you deleted. +After any reconfigure, redo the steps above. Note also that `dmf-get` resolves +each repo's dependencies independently, so a refresh can legitimately leave +the set internally inconsistent (e.g. a command module still built against an +older version of a library module it uses); such a module fails to bind on its +own, which is harmless to the rest of the boot, but it is worth telling apart +from the stale-`.dmfc` case above. + +**Flashing and debugging** (from `/build`): +- `cmake --build . --target install-firmware` — flash the firmware to the board. +- `cmake --build . --target connect` — attach OpenOCD to the target. +- `cmake --build . --target monitor` — attach a console to `dmell` over the + existing OpenOCD connection (run `connect` first). This gives you stdin/stdout + independent of any firmware-level driver — you get a working shell even + before a UART driver is loaded, since it doesn't go through `dmuart`/`dmtty` + at all. Log output for this relies on `dmlog`, the base/kernel-level logging + system used from very early boot (before most modules are up), which is why + `dmod-boot` depends on it directly rather than through a loaded module. diff --git a/.claude/skills/dmod-coding-conventions/SKILL.md b/.claude/skills/dmod-coding-conventions/SKILL.md new file mode 100644 index 0000000..bdda94b --- /dev/null +++ b/.claude/skills/dmod-coding-conventions/SKILL.md @@ -0,0 +1,90 @@ +--- +name: dmod-coding-conventions +description: Coding conventions for DMOD module C code — heap-allocate strings/lists instead of fixed-size buffers (Dmod_StrDup, dmlist), never statically link a dependency into a module (always dmod_link_modules), keep functions under ~50 lines, and the _create/_destroy opaque-context pattern with a magic-field guard. Load this when writing or reviewing module implementation code (.c/.h files) in a dmod-* repo. Companion to the dmod-ecosystem skill. +--- + +# DMOD Module Coding Conventions + +## Strings: prefer the heap over fixed-size buffers + +The target is embedded (STM32F4/F7, ESP32-S3) — RAM is scarce and every byte +of it matters, so default to heap-allocating rather than reserving fixed +static/stack space "just in case". This applies to strings in general, not +just paths: don't declare a fixed-size `char buf[N]` for a string whose +length isn't tightly bounded — heap-allocate it instead. Beyond the general +RAM-budget argument, fixed-size stack/static buffers silently truncate or +overflow once a value gets longer than whoever wrote `N` expected, which is a +real risk for anything composed at runtime (paths, module names, mount +points under `/`, `.dmr`/`.dmm` entries, log/error messages, etc.). A static +fixed-size array is only acceptable when you can genuinely guarantee the +string will never exceed ~20 bytes (e.g. a short fixed enum-like tag) — +paths and other composed/runtime strings do not qualify. + +When you need an owned copy of a string (not just a borrowed pointer), copy +it with `Dmod_StrDup` rather than `malloc` + `strcpy`/`memcpy`, and `free` it +when done. + +The same reasoning applies to lists: don't hand-roll a fixed-size static +array for a collection whose size grows/shrinks at runtime. Use the `dmlist` +module instead — pull it in with `dmod_link_modules(${DMOD_MODULE_NAME} +dmlist)` in `CMakeLists.txt` (see `dmtty/CMakeLists.txt` for a working +example) rather than reimplementing a dynamic list. + +## Never statically link libraries into a module + +Always link a module against another module's `_if` target dynamically +(`dmod_link_modules`, the standard Built-in API / MAL / DIF mechanisms) — +never statically link a library's object code directly into a module. DMOD +modules are loaded/unloaded independently at runtime; statically linking a +dependency into a module defeats that (bloats the `.dmf`, duplicates the +dependency's RAM/flash footprint across every module that links it, and +breaks the loader's ability to resolve/share a single instance of it). If +you find yourself reaching for `target_link_libraries` with anything other +than a module's own `_if` interface target, stop and use +`dmod_link_modules` instead. + +## Keep functions short and extract shared logic + +Functions should stay short — **max ~50 lines**. If a function is growing +past that, it's a signal to split it, not to keep going. Always look for the +common part between two similar code paths (two callers doing almost the +same thing, two branches with overlapping logic, ...) and pull it into its +own function rather than duplicating it inline. + +## Prefer the `_create`/`_destroy` context pattern + +The preferred way to manage a piece of stateful data is a context object: +a `_create` function allocates and initializes a context struct holding all +the data that logic needs, callers thread that context pointer through the +rest of the API, and a `_destroy` function frees it once it's no longer +needed. See `dm_sw_ring_create()`/`dm_sw_ring_destroy()` in +`dm_sw_ring/include/dm_sw_ring.h` for a worked example of this shape. + +**Context structs must be opaque/private.** Declare only the typedef in the +public/interface header, and define the actual struct body in the private +source file: + +```c +/* public header (e.g. include/.h) */ +typedef struct context context_t; +``` + +```c +/* private file (e.g. src/.c or a private header) */ +struct context +{ + ... +}; +``` + +This keeps callers from reaching into the struct directly or depending on +its layout — all access must go through the module's API functions. + +**Guard context structs with a magic field.** Give the struct a `magic` +field (`uint32_t`) set in `_create` to a fixed value derived from a short +form of the module's name (e.g. a 4-char tag packed into a `uint32_t`), and +check it at the top of every function that receives the context pointer, +before the context is dereferenced (or used) — the check should be present +in every entry point (`_destroy` included), not just a subset. This catches +use of a stale/freed, corrupted, or wrong-type pointer early instead of +leading to undefined behavior further down. diff --git a/.claude/skills/dmod-ecosystem/SKILL.md b/.claude/skills/dmod-ecosystem/SKILL.md new file mode 100644 index 0000000..b267107 --- /dev/null +++ b/.claude/skills/dmod-ecosystem/SKILL.md @@ -0,0 +1,118 @@ +--- +name: dmod-ecosystem +description: Entry-point map of the DMOD (Dynamic Modules) embedded ecosystem architecture — the core loader, the three inter-module communication mechanisms (Built-in API, MAL, DIF), and the shared CMake/Make build system. Load this first when working in any dmod-* repository (dmod, dmod-boot, dmuart, dmtty, dmfmc, dmell, dm_sw_ring, dmclk, dmgpio, dmini, dmheap, dmvfs, dmffs, dmdevfs, dmdrvi, dmhaman, dmosi*, or a new module created from this template) to understand how the repo fits into the larger system before making changes. Points to companion skills (dmod-module-structure, dmod-boot, dmod-hardware-ports, dmod-coding-conventions) for deeper topics. +--- + +# DMOD Ecosystem + +DMOD ("Dynamic Modules") is a framework for dynamically loading and running code +on embedded microcontrollers (STM32F4/F7, ESP32-S3 seen so far), similar in spirit +to shared libraries on a desktop OS but built for firmware. A "module" is a +self-contained unit of code (library or application) compiled to a `.dmf` +(DMOD File) — optionally compressed to `.dmfc` — that a loader can load, run, +and unload at runtime without recompiling or resetting the whole firmware. + +This ecosystem is spread across many small, independent git repositories under +one parent directory (each repo = one module, or the core framework). There is +no monorepo and no shared parent git tree — treat each repo you open as +self-contained, but expect it to `FetchContent` the `dmod` repo at configure time. + +**Naming convention: snake_case everywhere** — module names, file names, +functions, types (`dm_sw_ring`, `dm_sw_ring_create`, `dm_sw_ring_t`, +`dmfmc_configure_sdram`, ...). Don't introduce camelCase or PascalCase names +in module code (the handful of `Dmod_*`/`Pascal_Case` symbols you'll see, like +`Dmod_Printf`, are dmod's own SAL/core API, not a pattern to copy in module code). + +## The core repo: `dmod` + +`dmod` is the SDK and build system every other repo depends on. It provides: + +- **The loader/runtime** (`src/system/`, `src/module/`) — loads `.dmf`/`.dmfc`/`.dmp` + files, manages module contexts, resolves inter-module calls. +- **Two build modes**, selected via `DMOD_MODE`: + - `DMOD_SYSTEM` — builds the loader itself, embedded in firmware. + - `DMOD_MODULE` — builds a single loadable module (`.dmf`). +- **CMake functions** every module repo uses (see `dmod/docs/cmake-functions.md` + for the full reference): + - `dmod_add_library(name version sources...)` / `dmod_add_executable(...)` — + create a module. Reads `DMOD_MODULE_NAME`, `DMOD_MODULE_VERSION`, + `DMOD_AUTHOR_NAME`, `DMOD_STACK_SIZE`, optionally `DMOD_PRIORITY`, + `DMOD_MAL_IMPLS`, `DMOD_DIF_IMPLS`, `DMOD_DMR_PATH` (set before calling). + Also creates a `_if` INTERFACE target for consumers that only need headers. + - `dmod_link_modules(target [PRIVATE|PUBLIC|INTERFACE] module[@version]...)` — + downloads another module's headers via `dmf-get` at configure time and adds + them as include dirs. This is how modules depend on each other without + vendoring source. + - `dmod_setup_external_module()` — call after `project()` in a standalone repo + to wire up DMOD's CMake machinery. +- **The package manager**: `dmf-get` (installs/downloads modules), plus + `todmfc`/`todmd`/`todmm`/`todmp`/`mkdmrpkg`/`dmf-man` tools and three manifest + file formats, all documented under `dmod/docs/`: + - `.dmm` (manifest) — registry mapping module name → download URL. + - `.dmr` (resource file) — declares what files go where when a module is + installed/packaged (`docs/dmr-file-format.md`). + - `.dmd` (dependencies) — generated list of a module's dependency versions. + +## Three ways modules talk to each other + +1. **Built-in / Module API** — a module's own public functions, declared in + its header with the `dmod__api(version, ret, _suffix, (args))` + macro and defined in the `.c` file with the matching + `dmod__api_declaration(version, ret, _suffix, (args))` macro (both + auto-generated per-module into `_defs.h` by the dmod core build + from `dmod/scripts/api.h.in` — you only follow the naming convention, never + hand-write the macro itself). **This is not optional boilerplate**: a + plain C function prototype/definition in a module's public header will + compile but **fail to link** (or simply never be found), because the + loader resolves these calls dynamically rather than through normal static + linkage. Use this as the default way to expose any function another module + (or this module's own `tests/`) should be able to call — see + `dm_sw_ring/include/dm_sw_ring.h` + `dm_sw_ring/src/dm_sw_ring.c` for a + fully worked example, and the `dmod__port_api(...)` port variant + described below. + - A `dmod__global_api(...)` / `_global_api_declaration(...)` + variant also exists, declaring the function in the *global* namespace + instead of the module's own — reserved for exceptional cases (e.g. + defining a `printf`-style utility meant to be called unqualified from + anywhere). Default to the plain `_api`/`_api_declaration` pair unless you + have a specific reason to go global. +2. **MAL (Module Abstraction Layer)** — 1:1 inversion of control. One module + defines an interface; another module implements it and registers via + `DMOD_MAL_IMPLS` in its `CMakeLists.txt`/`Makefile`. Lets you swap an + implementation (e.g. which UART driver backs a generic "serial" interface) + without touching the caller. +3. **DIF (Dmod Interface)** — 1:N. Multiple modules implement the same + interface (e.g. several SPI/UART drivers) and are discovered dynamically at + runtime via `Dmod_GetNextDifModule()`/`Dmod_GetDifFunction()`, registered + through `DMOD_DIF_IMPLS`. + +Pick Built-in API for a hard compile-time dependency, MAL for "one pluggable +backend", DIF for "many interchangeable backends discovered at runtime". + +## Companion skills for deeper topics + +This skill is the map, not the territory — load these alongside it as the +task calls for them, rather than assuming everything is covered here: + +- **`dmod-module-structure`** — the standard module repo layout + (`CMakeLists.txt`, `manifest.dmm`, `docs/`, `tests/`), scaffolding a new + module with `dmod/scripts/new-module.sh`, and when to extract a reusable + library out of an application module. Load when creating or restructuring + a module repo. +- **`dmod-boot`** — the firmware entry point (`dmod-boot`): boot sequence, + module packaging (`modules.dmp`), testing a locally-built `.dmf` on real + hardware, and the flash/connect/monitor CMake targets. Load when working + in `dmod-boot` or tracing how firmware boots into a usable system. +- **`dmod-hardware-ports`** — the driver+port split used by hardware + modules (`dmuart`, `dmfmc`, ...), `DMOD_CPU_FAMILY`, and the + `dmod__port_api` macro. Load when writing or modifying a module + that talks to real hardware/peripherals. +- **`dmod-coding-conventions`** — module C code style: heap over + fixed-size buffers, never statically link a dependency into a module, + keep functions short, and the `_create`/`_destroy` opaque-context pattern + with a magic-field guard. Load when writing or reviewing module + implementation code. + +For a specific module's exact API, config keys, or behavior, read that +repo's `docs/api-reference.md` and `include/.h` directly rather than +guessing from any of these skills — module APIs evolve independently of them. diff --git a/.claude/skills/dmod-hardware-ports/SKILL.md b/.claude/skills/dmod-hardware-ports/SKILL.md new file mode 100644 index 0000000..9d9c9ef --- /dev/null +++ b/.claude/skills/dmod-hardware-ports/SKILL.md @@ -0,0 +1,45 @@ +--- +name: dmod-hardware-ports +description: Explains the driver+port pattern used by DMOD hardware modules (dmuart, dmfmc, dmgpio, dmclk, and any new peripheral driver) — splitting a module into a hardware-independent core and an architecture-specific "_port" module selected via DMOD_CPU_FAMILY, the dmod__port_api macro, and sharing logic across MCU families via src/port/_common/. Load this when writing or modifying a module that talks to real hardware/peripherals. Companion to the dmod-ecosystem skill. +--- + +# Driver + port pattern (hardware modules) + +Modules that touch real hardware (`dmuart`, `dmfmc`, and any new peripheral +driver) split into **two independent DMOD modules in one repo**: + +- `` — the core module: public API, config parsing (via `dmini`), + hardware-independent logic. Links against `_port_if`. +- `_port` — a second module, built from `src/port/CMakeLists.txt`, + containing the architecture-specific implementation. One subdirectory per + MCU family under `src/port//` (e.g. `src/port/stm32f7/`), each with a + `config.cmake` that sets `DMOD_TOOLS_NAME` (e.g. + `"arch/armv7/cortex-m7"`) and a `port.c` implementing the lifecycle + (`dmod_init`/`dmod_deinit`) plus any `DMOD_IRQ_HANDLER(...)`. + +The active architecture is selected via **`DMOD_CPU_FAMILY`** (a cache +variable already defined in `dmod/dmod-cfg.cmake`, consumed by `dmf-get +--cpu-family` for package resolution) — set it directly in the module's +`CMakeLists.txt` before `FetchContent_MakeAvailable(dmod)`: + +```cmake +set(DMOD_CPU_FAMILY "stm32f7" CACHE STRING "Target CPU family") +include(${CMAKE_CURRENT_SOURCE_DIR}/src/port/${DMOD_CPU_FAMILY}/config.cmake) +``` + +Do **not** invent a module-specific variable like `_MCU_SERIES` for +this — `DMOD_CPU_FAMILY` already exists and is understood by the rest of the +toolchain (`dmf-get`, package naming). `dmfmc`'s `DMFMC_MCU_SERIES` predates +this and is legacy, not a pattern to copy in new modules. + +The port's public API is declared with the `dmod__port_api(version, +return_type, _suffix, (args))` macro — this macro is generated automatically +by the dmod core build from `dmod/scripts/api.h.in` into +`_port_defs.h`; you only need to follow the naming convention, not +hand-write the macro. See `dmfmc/include/dmfmc_port.h` for a full example, or +`dmuart/include/dmuart_port.h` for a simpler one. + +If a peripheral IP block is identical across MCU families, keep the shared +logic in `src/port/_common/` and make each `src/port//port.c` a +thin wrapper (lifecycle + IRQ only) — see `dmfmc/docs/port-implementation.md` +for the exact recipe used there. diff --git a/.claude/skills/dmod-module-structure/SKILL.md b/.claude/skills/dmod-module-structure/SKILL.md new file mode 100644 index 0000000..77446b2 --- /dev/null +++ b/.claude/skills/dmod-module-structure/SKILL.md @@ -0,0 +1,49 @@ +--- +name: dmod-module-structure +description: Explains the standard DMOD module repo layout (CMakeLists.txt/Makefile skeleton, manifest.dmm, docs/, tests/), how to scaffold a new module with dmod/scripts/new-module.sh, and when to extract a reusable library out of an application module. Load this when creating a new dmod-* module repo, restructuring an existing one, or deciding whether functionality should become its own library module. Companion to the dmod-ecosystem skill. +--- + +# DMOD Module Structure + +Every real module repo (`dmuart`, `dmtty`, `dmfmc`, `dm_sw_ring`, `dmell`, ...) +follows the same shape: + +``` +/ +├── CMakeLists.txt # FetchContent's dmod, then dmod_add_library/_executable +├── Makefile # Make-based equivalent build +├── manifest.dmm # $include .../versions.dmm + this module's download URL +├── .dmr # install/package resource mapping +├── include/.h # public API +├── src/.c # implementation + dmod_init()/dmod_deinit() +├── docs/ +│ ├── README.md # doc index +│ ├── api-reference.md # full API reference +│ └── (examples.md, configuration.md, port-implementation.md as needed) +├── tests/ +│ ├── CMakeLists.txt # dmod_add_test(...) +│ └── _test.c # #include "dmod_test.h", DMOD_TEST_STEP(...) macros +└── README.md # includes a "Project Structure" tree section +``` + +Every `CMakeLists.txt` in a standalone module repo follows the same skeleton — +`FetchContent_Declare(dmod GIT_REPOSITORY .../dmod.git GIT_TAG develop)` + +`FetchContent_MakeAvailable(dmod)` + `include(${DMOD_DIR}/paths.cmake)` + +`dmod_setup_external_module()` — rather than assuming a local checkout path. +Look at `dm_sw_ring/CMakeLists.txt` or `dmtty/CMakeLists.txt` for a clean example. + +New modules should be scaffolded with `dmod/scripts/new-module.sh` (see +`--help` for `--type library|application`, `--port`, `--dif`, `--mal`), which +generates this exact layout. + +## Extracting a library out of an application module + +Sometimes an `application`-type module accumulates functionality that other +modules need to call — not just as an implementation detail, but as an API +other modules should link against. In that case, split the reusable part +out into its own `library`-type DMOD module rather than exposing the +application module's internals directly, and give that library module a +`lib` prefix. E.g. if the application module is `systemd`, the extracted +library becomes `libsystemd`: `systemd` itself becomes a consumer of +`libsystemd` (via `dmod_link_modules`) alongside every other module that +needs that API, rather than a special case other modules reach into. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1591d31 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: CI + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + +jobs: + build-and-test-core: + name: Build and Test dmrtc (core) + runs-on: ubuntu-latest + permissions: + contents: read + container: + image: chocotechnologies/dmod:1.0.4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build dmrtc core module + run: | + mkdir -p build + cd build + # Build the deterministic test-only port; hardware releases exclude it. + cmake .. -DDMOD_MODE=DMOD_MODULE -DDMOD_CPU_FAMILY=x86_64 + cmake --build . --parallel + + - name: Verify module files + run: | + echo "Checking for module files..." + ls -lh build/dmf/ + test -f build/dmf/dmrtc.dmf + test -f build/dmf/dmrtc_version.txt + test -f build/dmf/test_dmrtc.dmf + echo "Module files present" + + - name: Run tests with dmod_loader + run: | + export DMOD_DMF_DIR=$(pwd)/build/dmf + ctest --test-dir build --output-on-failure + + discover-cpu-families: + name: Discover CPU Families + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + cpu_families: ${{ steps.list-families.outputs.cpu_families }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: List available CPU families + id: list-families + run: | + # Find all CPU families by looking for directories containing port.c + CPU_FAMILIES=$(find src/port -mindepth 2 -maxdepth 2 -name "port.c" -type f -printf '%h\n' | \ + xargs -n1 basename | \ + grep -v '^x86_64$' | sort | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "Found CPU families: $CPU_FAMILIES" + echo "cpu_families=$CPU_FAMILIES" >> $GITHUB_OUTPUT + + build-dmrtc: + name: Build dmrtc for ${{ matrix.cpu_family }} + needs: discover-cpu-families + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + matrix: + cpu_family: ${{ fromJson(needs.discover-cpu-families.outputs.cpu_families) }} + + container: + image: chocotechnologies/dmod:1.0.4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build dmrtc for ${{ matrix.cpu_family }} + run: | + set -e + CPU_FAMILY="${{ matrix.cpu_family }}" + mkdir -p build_$CPU_FAMILY + cd build_$CPU_FAMILY + + cmake .. -DDMOD_CPU_FAMILY="${CPU_FAMILY}" + cmake --build . + + echo "Build completed for CPU family: ${CPU_FAMILY}" + ls -lh dmf/ + + - name: Verify dmrtc_port module files + run: | + CPU_FAMILY="${{ matrix.cpu_family }}" + echo "Checking for dmrtc_port module files for ${CPU_FAMILY}..." + ls -lh build_$CPU_FAMILY/dmf/ + test -f build_$CPU_FAMILY/dmf/dmrtc_port.dmf + test -f build_$CPU_FAMILY/dmf/dmrtc_port_version.txt + echo "Module files present for ${CPU_FAMILY}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c270a7b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,260 @@ +name: Release + +on: + release: + types: [created] + +jobs: + discover-cpu-families: + name: Discover CPU Families + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + cpu_families: ${{ steps.list-families.outputs.cpu_families }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: List available CPU families + id: list-families + run: | + CPU_FAMILIES=$(find src/port -mindepth 2 -maxdepth 2 -name "port.c" -type f -printf '%h\n' | \ + xargs -n1 basename | \ + grep -v '^x86_64$' | sort | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "Found CPU families: $CPU_FAMILIES" + echo "cpu_families=$CPU_FAMILIES" >> $GITHUB_OUTPUT + + build-dmrtc: + name: Build dmrtc for ${{ matrix.cpu_family }} + needs: discover-cpu-families + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + cpu_family: ${{ fromJson(needs.discover-cpu-families.outputs.cpu_families) }} + + container: + image: chocotechnologies/dmod:1.0.4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version from tag + id: get_version + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Build dmrtc for ${{ matrix.cpu_family }} + run: | + set -e + CPU_FAMILY="${{ matrix.cpu_family }}" + BUILD_NAME="port-${CPU_FAMILY}" + echo "BUILD_NAME=$BUILD_NAME" >> $GITHUB_ENV + echo "ARTIFACT_NAME=release-$BUILD_NAME" >> $GITHUB_ENV + mkdir -p build_$BUILD_NAME + cd build_$BUILD_NAME + + # DMOD_DMR_PATH is already set for both dmrtc and + # dmrtc_port in their respective CMakeLists.txt, so this + # build also assembles packages// for every module defined + # in this repo (normally dmrtc and dmrtc_port, but + # this discovers whatever is actually there). + cmake .. \ + -DDMOD_MODULE_VERSION="${{ steps.get_version.outputs.version }}" \ + -DDMOD_CPU_FAMILY="${CPU_FAMILY}" + + cmake --build . + + echo "Build completed for CPU family: ${CPU_FAMILY}" + ls -la packages/ || echo "No packages directory" + + - name: Add release notes and create release archives + run: | + set -e + BUILD_DIR="build_$BUILD_NAME" + TAG="${{ github.event.release.tag_name }}" + CPU="${{ matrix.cpu_family }}" + PACKAGES_DIR="$BUILD_DIR/packages" + + if [ ! -d "$PACKAGES_DIR" ] || [ -z "$(ls -A "$PACKAGES_DIR")" ]; then + echo "Error: no packages found in $PACKAGES_DIR" + exit 1 + fi + + : > package-names.txt + for pkg_dir in "$PACKAGES_DIR"/*/; do + pkg=$(basename "$pkg_dir") + echo "$pkg" >> package-names.txt + + echo "${{ github.event.release.body }}" > "${pkg_dir}RELEASE_NOTES.txt" + (cd "$pkg_dir" && zip -r "${OLDPWD}/${pkg}-${TAG}-${CPU}.zip" .) + done + + echo "Created archives:" + ls -lh *-"${TAG}"-"${CPU}".zip + echo "Packages found:" + cat package-names.txt + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACT_NAME }} + path: | + *.zip + package-names.txt + retention-days: 1 + + generate-versions-manifest: + name: Generate versions.dmm + needs: [discover-cpu-families, build-dmrtc] + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download release artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Generate versions.dmm + run: | + set -e + echo "# List of available versions for dmrtc modules" > versions.dmm + echo "# Generated automatically by CI" >> versions.dmm + echo "" >> versions.dmm + + VERSIONS=$(git tag -l 'v*' | sed 's/^v//' | sort -V | tr '\n' ' ' | sed 's/ $//') + VERSIONS=$(echo $VERSIONS | sed 's/\blatest\b//g' | xargs) + + if [ -z "$VERSIONS" ]; then + echo "Warning: No version tags found" + VERSIONS="${{ github.event.release.tag_name }}" + VERSIONS="${VERSIONS#v}" + fi + + echo "Found versions: $VERSIONS" + + PACKAGES=$(cat artifacts/release-*/package-names.txt | sort -u) + echo "Found packages: $PACKAGES" + + for pkg in $PACKAGES; do + echo "\$version-available $pkg $VERSIONS" >> versions.dmm + done + + echo "Generated versions.dmm:" + cat versions.dmm + + - name: Upload versions.dmm as artifact + uses: actions/upload-artifact@v4 + with: + name: versions-manifest + path: versions.dmm + retention-days: 1 + + upload-release-assets: + name: Upload Release Assets + needs: [build-dmrtc, generate-versions-manifest] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display artifact structure + run: | + echo "Downloaded artifacts:" + ls -lR artifacts/ + + - name: Upload release assets to versioned tag + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + shopt -s nullglob + zip_files=(artifacts/release-*/*.zip) + + if [ ${#zip_files[@]} -eq 0 ]; then + echo "Error: No artifacts found to upload" + exit 1 + fi + + for zip_file in "${zip_files[@]}"; do + echo "Uploading $zip_file to ${{ github.event.release.tag_name }}..." + gh release upload ${{ github.event.release.tag_name }} \ + "$zip_file" \ + --repo ${{ github.repository }} \ + --clobber + done + + if [ -f artifacts/versions-manifest/versions.dmm ]; then + echo "Uploading versions.dmm to ${{ github.event.release.tag_name }}..." + gh release upload ${{ github.event.release.tag_name }} \ + artifacts/versions-manifest/versions.dmm \ + --repo ${{ github.repository }} \ + --clobber + fi + + echo "Successfully uploaded ${#zip_files[@]} artifact(s) to ${{ github.event.release.tag_name }}" + + - name: Create or update latest release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + if gh release view vlatest --repo ${{ github.repository }} >/dev/null 2>&1; then + echo "Release vlatest exists, deleting it..." + gh release delete vlatest --repo ${{ github.repository }} --yes + fi + + echo "Creating vlatest release..." + gh release create vlatest \ + --repo ${{ github.repository }} \ + --title "Latest Release (based on ${{ github.event.release.tag_name }})" \ + --notes "This release always points to the latest stable version. Currently based on ${{ github.event.release.tag_name }}." + + - name: Upload release assets to latest tag + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + shopt -s nullglob + zip_files=(artifacts/release-*/*.zip) + + for zip_file in "${zip_files[@]}"; do + echo "Uploading $zip_file to vlatest..." + gh release upload vlatest \ + "$zip_file" \ + --repo ${{ github.repository }} \ + --clobber + done + + if [ -f artifacts/versions-manifest/versions.dmm ]; then + echo "Uploading versions.dmm to vlatest..." + gh release upload vlatest \ + artifacts/versions-manifest/versions.dmm \ + --repo ${{ github.repository }} \ + --clobber + fi + + echo "Successfully uploaded ${#zip_files[@]} artifact(s) to vlatest" diff --git a/.gitignore b/.gitignore index 1f99f9d..c2af1f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,15 @@ -CMakeLists.txt.user -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -_deps -CMakeUserPresets.json +# Build directories +build/ +*.dmf +*.dmfc -# CLion -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#cmake-build-* +# IDE +# .vscode/tasks.json and .vscode/settings.json are committed (see every real +# module repo) so the build tasks are shared with the team - don't ignore +# .vscode/ wholesale. +.idea/ + +# Temporary files +*.tmp +*.swp +*~ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..237bab1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.associations": { + "dmrtc.h": "c", + "dmrtc_port.h": "c" + }, + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", + "C_Cpp.intelliSenseEngine": "default", + "C_Cpp.errorSquiggles": "enabled" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2a66b75 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,55 @@ +{ + // See https://code.visualstudio.com/docs/editor/tasks for more information + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: Configure", + "type": "shell", + "command": "cmake", + "args": [ + "-S", + "${workspaceFolder}", + "-B", + "${workspaceFolder}/build", + "-DDMOD_DIR=${input:dmodDir}" + ], + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "CMake: Build", + "type": "shell", + "command": "cmake", + "args": [ + "--build", + "${workspaceFolder}/build" + ], + "group": "build", + "problemMatcher": [] + }, + { + "label": "CMake: Clean", + "type": "shell", + "command": "cmake", + "args": [ + "--build", + "${workspaceFolder}/build", + "--target", + "clean" + ], + "group": "build", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "dmodDir", + "description": "Path to a local dmod checkout (leave blank to fetch dmod's develop branch from GitHub instead)", + "default": "${workspaceFolder}/../dmod", + "type": "promptString" + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d62090b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,86 @@ +cmake_minimum_required(VERSION 3.18) + +set(DMOD_CPU_FAMILY "stm32f7" CACHE STRING "Target CPU family") +include(${CMAKE_CURRENT_SOURCE_DIR}/src/port/${DMOD_CPU_FAMILY}/config.cmake) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(NOT DEFINED DMOD_MODULE_VERSION) + set(DMOD_MODULE_VERSION "0.1" CACHE STRING "DMOD module version") +endif() + +# If a local dmod checkout is provided, use it instead of fetching from git +if(DEFINED DMOD_DIR AND NOT "${DMOD_DIR}" STREQUAL "") + set(FETCHCONTENT_SOURCE_DIR_DMOD "${DMOD_DIR}" CACHE PATH "" FORCE) +endif() + +include(FetchContent) +FetchContent_Declare( + dmod + GIT_REPOSITORY https://github.com/choco-technologies/dmod.git + GIT_TAG develop +) + +set(DMOD_MODE "DMOD_MODULE" CACHE STRING "DMOD build mode") +set(DMOD_BUILD_TESTS OFF CACHE BOOL "Build tests") +set(DMOD_BUILD_EXAMPLES OFF CACHE BOOL "Build examples") +set(DMOD_BUILD_TOOLS OFF CACHE BOOL "Build tools") +set(DMOD_BUILD_TEMPLATES OFF CACHE BOOL "Build templates") + +FetchContent_MakeAvailable(dmod) + +project(dmrtc VERSION ${DMOD_MODULE_VERSION} + DESCRIPTION "dmrtc DMOD library module" + LANGUAGES C CXX) + +set(DMOD_DIR ${dmod_SOURCE_DIR} CACHE PATH "DMOD source directory") +set(DMOD_SCRIPTS_DIR ${DMOD_DIR}/scripts CACHE PATH "DMOD scripts directory") +include(${DMOD_DIR}/paths.cmake) +dmod_setup_external_module() + +# Name of the module +set(DMOD_MODULE_NAME dmrtc) + +# Author (should be string) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") + +# Stack size for the module (should be integer) +set(DMOD_STACK_SIZE 1024) + +# Path to the .dmr file - enables release package creation in build/packages/ +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmrtc.dmr) + +# List of MAL interfaces implemented by this module (leave empty if none) +set(DMOD_MAL_IMPLS ) + +# List of DIF interfaces implemented by this module (leave empty if none) +set(DMOD_DIF_IMPLS ) + +# +# dmod_add_library - create a library module +# it has the same signature as add_library +# and can be used in the same way after the creation +# (for example, to link libraries) +# +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + # List of source files - can include C and C++ files + src/dmrtc.c + src/dmrtc_ioctl.c +) + +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +dmod_link_modules(${DMOD_MODULE_NAME} dmdrvi dmini dmosi) + +add_subdirectory(src/port) +target_link_libraries(${DMOD_MODULE_NAME} ${DMOD_MODULE_NAME}_port_if) +enable_testing() +add_subdirectory(tests) + +add_subdirectory(apps/rtc) diff --git a/README.md b/README.md index 17ca8bc..9b603ec 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,136 @@ # dmrtc -RTC driver + +Calendar RTC driver for DMOD, with separate `dmrtc` and `dmrtc_port` modules. +STM32F4 and STM32F7 share their implementation in `src/port/stm32_common/`. +The `x86_64` port is a deterministic test double and is excluded from releases. + +## Capabilities + +- Civil calendar for 2000–2099, 24-hour time, weekday computed on write. +- LSE/LSI startup signaled by RCC IRQ; waiting task blocks on a semaphore. +- Persistent explicit-calendar-set marker in two reserved backup registers. +- Serialized operations and one owner per hardware RTC. +- `/dev/rtc` ioctl interface, and the `rtc` console application. +- Preserve running calendar on reset, context destruction and module unload. + +The driver has no timezone, network synchronization or UTC trust policy. +`calendar_set` means a valid calendar was explicitly written; it does not mean +that its source is authentic or that its value remains accurate. Those policies +belong in `dmutc`. Monotonic timeouts use the platform clock, not this calendar. + +## Board configuration + +```ini +[rtc] +driver_name=dmrtc +driver_order=0 +instance=0 +clock=lse +clock_hz=32768 +timeout_ms=5000 +backup_register=18 +``` + +Choose one configuration from `configs/board/` or `configs/mcu/`. MCU presets +assume a fitted external LSE crystal. `backup_register=18` reserves BKP18R and +BKP19R exclusively; other drivers and the bootloader must not use them. +LSI must be selected explicitly, with its nominal `clock_hz` supplied in the INI. +There is no automatic fallback. The configured frequency must be divisible by +128; asynchronous prescaler is 127, synchronous prescaler is `clock_hz/128-1`. +LSI is less accurate and is not an independently battery-backed time source. + +`timeout_ms` limits oscillator startup only (default 5000 ms). RCC's LSE/LSI +ready interrupt posts an ISR-safe semaphore; only the calling thread blocks. +DBP and INITF have no corresponding hardware interrupts: these short register +handshakes have a separate 20 ms limit and sleep between status samples. +All blocking calls require a running scheduler and task context. There is no +busy-wait fallback. The port no longer depends on `dmclk_port` delays. + +The RCC IRQ is shared: this port owns only its selected LSE/LSI ready source, +preserves other interrupt enables, and disables its source before freeing the +wait semaphore. An already-owned source or an active IRQ with a priority unsafe +for DMOSI calls is rejected. No unrelated RCC flag is acknowledged. + +A previously selected different RTC clock, different prescalers or 12-hour +format causes acquisition to fail. The driver never resets the backup domain. +Changing those settings requires a deliberate board provisioning operation. +RTC alarms, wakeup timer and calibration are outside this API. +The port exclusively owns RTC configuration and backup-domain access during +operations; do not access these registers concurrently from other code. +The boot must initialize device drivers after starting the RTOS scheduler. + +STM32F746G-DISCO has an LSE crystal, but its VBAT pin is connected to VDD. +The board preserves RTC across a system reset while powered; it does not retain +calendar through complete power removal without hardware modification. + +## Driver integration and device interface + +`dmrtc_create(&config)` acquires hardware; NULL selects `DMRTC_CONFIG_DEFAULT`. +A board-created context can be borrowed with `dmrtc_default()`. Its owner alone +may destroy it. Creation/destruction must be externally serialized with users; +operations on a live context are protected by a mutex. Destroy returns `-EBUSY` +while device files remain open. Release leaves oscillator and calendar running. + +`dmrtc_get`, `dmrtc_set`, `dmrtc_status` and `dmrtc_invalidate` take that context. +`get` returns `-ENODATA` until explicitly set, with cleared output on failure. +Dates are validated before any hardware write. A write clears the persistent +marker before modifying the calendar, and commits it after initialization exits. +Year 2000 works even though STM32's INITS flag is zero for that year. +The hardware's two-digit year wraps after 2099; applications must provision a +new epoch or replace this port before that boundary. Leap seconds are rejected. + +```text +rtc status +rtc info +rtc get +rtc set 2026-10-15T12:34:56 +rtc invalidate +rtc -d /dev/backup-rtc get +``` + +The `rtc` application uses VFS and `dmdrvi_ioctl.h` only; it has no dependency +on the dmrtc context API or module. `-d DEVICE` selects any compatible RTC node; +`/dev/rtc` is the default. It can read clocks outside STM32's range and display +fractional seconds. `set` accepts up to nine fractional digits; the physical +STM32 port rejects nonzero fractions with -ERANGE rather than truncating them. +No command implicitly assigns a timezone or authenticates UTC. + +Device files implement `DMDRVI_IOCTL_RTC_GET_TIME`, `SET_TIME`, `GET_STATUS`, +`GET_INFO`, and optional `INVALIDATE`. The shared calendar, capabilities and +status structures belong to dmdrvi. Read commands require a readable handle; +SET_TIME/INVALIDATE require write access. Private ioctl numbers are removed. +There is no byte-stream encoding. See the [shared contract](https://github.com/choco-technologies/dmdrvi/blob/main/docs/rtc.md). + +GET_INFO reports 2000–2099, one-second resolution, SET_TIME and INVALIDATE. +This driver does not advertise hardware continuity-loss detection; the console +therefore reports `time_lost=unknown`. A clear loss flag on hardware without +that capability is not evidence of clock continuity. Oscillator selection and +backup-register configuration remain driver-specific INI/port settings. + +## Build and verify + +Initialized with DMOD's `new-module.sh --type library --port --port-arch stm32f7 +--github`. CMake builds every module and its release package; no wrapper Makefile. + +```sh +cmake -S . -B build/host -DDMOD_CPU_FAMILY=x86_64 +cmake --build build/host --parallel +ctest --test-dir build/host --output-on-failure +cmake -S . -B build/stm32f7 -DDMOD_CPU_FAMILY=stm32f7 +cmake --build build/stm32f7 --parallel +``` + +Packages are in `build//packages/`. STM32F4 uses `-DDMOD_CPU_FAMILY=stm32f4`. +Install `dmrtc driver=board/stm32f746g-disco/rtc.ini`, `rtc` and `test_dmrtc` +in the board image. Run `test_dmrtc` from its console. Hardware tests briefly +change the calendar and restore its previous value or previous unset state. +Details and hardware evidence: [testing](docs/testing.md). + +## Hardware references + +- [STM32F74/F75 reference manual RM0385](https://www.st.com/resource/en/reference_manual/dm00124865.pdf) +- [STM32F4 reference manual RM0090](https://www.st.com/resource/en/reference_manual/dm00031020.pdf) +- [STM32 RTC application note AN4759](https://www.st.com/resource/en/application_note/DM00226326-.pdf) +- [STM32F746G-DISCO user manual UM1907](https://www.st.com/resource/en/user_manual/dm00190424.pdf) + +MIT license. Author: Patryk Kubiak. diff --git a/apps/rtc/CMakeLists.txt b/apps/rtc/CMakeLists.txt new file mode 100644 index 0000000..4d0eb7b --- /dev/null +++ b/apps/rtc/CMakeLists.txt @@ -0,0 +1,7 @@ +set(DMOD_MODULE_NAME rtc) +set(DMOD_MODULE_VERSION ${PROJECT_VERSION}) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 2048) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/rtc.dmr) +dmod_add_executable(rtc ${DMOD_MODULE_VERSION} rtc.c) +dmod_link_modules(rtc dmdrvi) diff --git a/apps/rtc/rtc.c b/apps/rtc/rtc.c new file mode 100644 index 0000000..0192687 --- /dev/null +++ b/apps/rtc/rtc.c @@ -0,0 +1,108 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmdrvi_ioctl.h" +#include +#include + +static void print_time(const dmdrvi_rtc_time_t* c) +{ + Dmod_Printf("%04d-%02u-%02uT%02u:%02u:%02u", (int)c->year, + (unsigned)c->month, (unsigned)c->day, (unsigned)c->hour, + (unsigned)c->minute, (unsigned)c->second); + if (c->nanosecond != 0) Dmod_Printf(".%09u", (unsigned)c->nanosecond); + Dmod_Printf(" weekday=%u\n", (unsigned)c->weekday); +} +static int number(const char* s, unsigned count) +{ + int value = 0; + while (count-- != 0) { + if (*s < '0' || *s > '9') return -1; + value = value * 10 + *s++ - '0'; + } + return value; +} +static int parse(const char* s, dmdrvi_rtc_time_t* c) +{ + size_t len = strlen(s); + if (len < 19 || len > 29 || s[4] != '-' || s[7] != '-' || s[10] != 'T' || + s[13] != ':' || s[16] != ':') return -EINVAL; + int v[] = {number(s,4),number(s+5,2),number(s+8,2),number(s+11,2), + number(s+14,2),number(s+17,2)}; + if (v[0] < 1 || v[1] < 1 || v[1] > 12 || v[2] < 1 || v[2] > 31 || + v[3] < 0 || v[3] > 23 || v[4] < 0 || v[4] > 59 || v[5] < 0 || v[5] > 59) + return -EINVAL; + memset(c, 0, sizeof(*c)); + c->year=v[0]; c->month=(uint8_t)v[1]; c->day=(uint8_t)v[2]; + c->hour=(uint8_t)v[3]; c->minute=(uint8_t)v[4]; c->second=(uint8_t)v[5]; + if (len > 19) { + if (s[19] != '.' || len == 20) return -EINVAL; + int fraction = number(s + 20, (unsigned)(len - 20)); + if (fraction < 0) return -EINVAL; + c->nanosecond = (uint32_t)fraction; + for (size_t n = len - 20; n < 9; ++n) c->nanosecond *= 10; + } + return 0; /* Complete Gregorian/range/precision validation belongs to the device. */ +} +static int execute(void* file, int argc, char** argv) +{ + const char* action = argc == 0 ? "get" : argv[0]; + if ((argc == 0 || argc == 1) && strcmp(action,"get") == 0) { + dmdrvi_rtc_time_t time; + int rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_TIME, &time); + if (rc == 0) print_time(&time); + return rc; + } + if (argc == 1 && strcmp(action,"status") == 0) { + dmdrvi_rtc_status_t status; + int rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_STATUS, &status); + dmdrvi_rtc_info_t info; + if (rc == 0) rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_INFO, &info); + if (rc == 0) { + Dmod_Printf("running=%u calendar_set=%u ", + !!(status.flags & DMDRVI_RTC_STATUS_RUNNING), + !!(status.flags & DMDRVI_RTC_STATUS_TIME_SET)); + if (info.capabilities & DMDRVI_RTC_CAP_LOSS_DETECTION) + Dmod_Printf("time_lost=%u\n", !!(status.flags & DMDRVI_RTC_STATUS_TIME_LOST)); + else Dmod_Printf("time_lost=unknown\n"); + } + return rc; + } + if (argc == 1 && strcmp(action,"info") == 0) { + dmdrvi_rtc_info_t info; + int rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_INFO, &info); + if (rc == 0) Dmod_Printf("years=%d..%d resolution_ns=%llu writable=%u invalidatable=%u loss_detection=%u\n", + (int)info.min_year, (int)info.max_year, (unsigned long long)info.resolution_ns, + !!(info.capabilities & DMDRVI_RTC_CAP_SET_TIME), + !!(info.capabilities & DMDRVI_RTC_CAP_INVALIDATE), + !!(info.capabilities & DMDRVI_RTC_CAP_LOSS_DETECTION)); + return rc; + } + if (argc == 1 && strcmp(action,"invalidate") == 0) + return Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_INVALIDATE, NULL); + if (argc == 2 && strcmp(action,"set") == 0) { + dmdrvi_rtc_time_t time; + int rc = parse(argv[1], &time); + return rc != 0 ? rc : Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_SET_TIME, &time); + } + return -EINVAL; +} +int main(int argc, char* argv[]) +{ + const char* path = "/dev/rtc"; + int first = 1; + if (argc > 1 && strcmp(argv[1],"-d") == 0) { + if (argc < 3) return 1; + path = argv[2]; first = 3; + } + const char* action = first < argc ? argv[first] : "get"; + bool write = strcmp(action,"set") == 0 || strcmp(action,"invalidate") == 0; + void* file = Dmod_FileOpen(path, write ? "r+b" : "rb"); + if (file == NULL) { Dmod_Printf("rtc: cannot open %s\n", path); return 1; } + int rc = execute(file, argc-first, argv+first); + Dmod_FileClose(file); + if (rc != 0) { + Dmod_Printf("rtc: error %d\n", rc); + if (rc == -EINVAL) Dmod_Printf("rtc [-d DEVICE] [get|status|info|invalidate|set YYYY-MM-DDTHH:MM:SS[.fraction]]\n"); + } + return rc == 0 ? 0 : 1; +} diff --git a/apps/rtc/rtc.dmr b/apps/rtc/rtc.dmr new file mode 100644 index 0000000..e79c829 --- /dev/null +++ b/apps/rtc/rtc.dmr @@ -0,0 +1,6 @@ +# Runtime diagnostic package (no generated public header). +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] diff --git a/configs/board/stm32f746g-disco/rtc.ini b/configs/board/stm32f746g-disco/rtc.ini new file mode 100644 index 0000000..8dab983 --- /dev/null +++ b/configs/board/stm32f746g-disco/rtc.ini @@ -0,0 +1,11 @@ +; STM32F746G-DISCO: X3 32.768 kHz crystal; VBAT tied to VDD. +; Select exactly one RTC instance configuration. +; BKP18R and BKP19R are reserved for the explicit-calendar-set marker. +[rtc] +driver_name=dmrtc +driver_order=0 +instance=0 +clock=lse +clock_hz=32768 +timeout_ms=5000 +backup_register=18 diff --git a/configs/mcu/stm32f407vg.ini b/configs/mcu/stm32f407vg.ini new file mode 100644 index 0000000..5f52118 --- /dev/null +++ b/configs/mcu/stm32f407vg.ini @@ -0,0 +1,11 @@ +; STM32F407VG: requires an external 32.768 kHz LSE crystal. +; Select exactly one RTC instance configuration. +; BKP18R and BKP19R are reserved for the explicit-calendar-set marker. +[rtc] +driver_name=dmrtc +driver_order=0 +instance=0 +clock=lse +clock_hz=32768 +timeout_ms=5000 +backup_register=18 diff --git a/configs/mcu/stm32f746ng.ini b/configs/mcu/stm32f746ng.ini new file mode 100644 index 0000000..a156486 --- /dev/null +++ b/configs/mcu/stm32f746ng.ini @@ -0,0 +1,11 @@ +; STM32F746NG: requires an external 32.768 kHz LSE crystal. +; Select exactly one RTC instance configuration. +; BKP18R and BKP19R are reserved for the explicit-calendar-set marker. +[rtc] +driver_name=dmrtc +driver_order=0 +instance=0 +clock=lse +clock_hz=32768 +timeout_ms=5000 +backup_register=18 diff --git a/dmrtc.dmr b/dmrtc.dmr new file mode 100644 index 0000000..f5719cd --- /dev/null +++ b/dmrtc.dmr @@ -0,0 +1,35 @@ +# DMOD Resource File for dmrtc +# This file specifies where resources should be installed + +# === Core Module === +# Main module file - always installed +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] + +# Compressed module +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] + +# Dependencies file (if exists) +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] + +# Version information +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Documentation === +# Module documentation in markdown format for dmf-man tool +docs=./docs => ${destination}/${module}/docs [origin=${repo_dir}/docs] + +# README file +readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/README.md] + +# === Header Files === +# Include directory with all headers (public header + generated _defs.h) +inc=./include => ${destination}/${module}/include [origin=${repo_dir}/include] [origin=${build_dir}/${module}_defs.h] + +# === License === +# License file +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +# Adding itself +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/${module}.dmr] + +configs=./configs => ${destination}/${module}/configs [origin=${repo_dir}/configs] diff --git a/dmrtc_port.dmr b/dmrtc_port.dmr new file mode 100644 index 0000000..724cb14 --- /dev/null +++ b/dmrtc_port.dmr @@ -0,0 +1,24 @@ +# DMOD Resource File for dmrtc_port +# This file specifies where resources should be installed + +# === Core Module === +# Main module file - always installed +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] + +# Compressed module +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] + +# Dependencies file (if exists) +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] + +# Version information +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Header Files === +# Full include directory with all headers and generated platform definitions header +inc=./include => ${destination}/${module}/include [origin=${repo_dir}/include] [origin=${build_dir}/src/port/${module}_defs.h] + +# === License === +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/${module}.dmr] diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..4959d2c --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,98 @@ +# Verification + +Native tests exercise calendar validation, unset state, set/read, rejected invalid +writes, year 2000, singleton ownership, configuration constraints, failed writes, +calendar persistence on context recreation, and conflicting source selection. + +Hardware tests additionally exercise `/dev/rtc` read/write ioctl permissions, +open-handle protection against destruction, unknown ioctls, leap-day and year +rollover. Reset retention is checked separately using OpenOCD and the console. +The x86_64 mock is not evidence of STM32 register behavior. + +## Initial implementation — 2026-10-15 + +- Native `ctest`: 8/8 steps passed, including concurrent calendar access. +- STM32F4 and STM32F7 builds passed (F4 compiled, not tested on a board). +- STM32F746G-DISCO: final suite 8/8 passed three consecutive executions. +- Firmware programmed by ST-Link/OpenOCD with `Verified OK`. +- LSE selected: `running=1 calendar_set=1 clock=lse nominal_hz=32768`. +- Year 2000 reset test: `2000-02-29T23:59:45` before system reset, + `2000-03-01T00:00:30` after reboot; state retained and calendar advanced. +- Final firmware: `2026-10-15T20:49:43` → `20:49:46` while powered, + then `20:50:23` after system reset, with `calendar_set=1` preserved. +- Calendar was set to PC UTC after the tests, without claiming authenticated UTC. +- Independent battery/power-loss retention and long-term oscillator accuracy were + not measured. VBAT is tied to VDD on this board. + +[Captured console evidence](validation/stm32f746g-disco.txt). + +Initial STM32F7 module sizes (uncompressed DMF): core 5808 bytes, port 4229 bytes. +The exact tested core/port compressed packages were compared against those +embedded in the boot image: + + +- `dmrtc.dmfc` SHA-256: `9d0cccc0609c9ebf08cafaf4c3dc293012e61f2f9825eae828d03a345340ec34` + +- `dmrtc_port.dmfc` SHA-256: `c5be69899578df59c588cc1b3c0806d543bb741fc3352c020188b22877f5b8d7` + +## IRQ waits and scheduled boot — 2026-10-16 + +The STM32 port no longer calls dmclk_port_delay_us. LSE/LSI startup waits on +an ISR-safe semaphore posted by the shared RCC IRQ handler. DBP/INITF waits +sleep between samples with a separate 20 ms deadline. Pre-scheduler and ISR +calls are rejected rather than falling back to busy waits. + +The original boot mounted /dev before starting RTOS. The accompanying boot PR +moves filesystem/device initialization and application startup into a kernel +boot task after the scheduler starts; the worker stack is reclaimed on return. + +- Native calendar suite: 8/8 steps passed. +- Actual STM32 wait source tested with MMIO/OS doubles: ready fast path, + LSE/LSI IRQ wake, unrelated IRQ rejection, event before blocking, timeout, + late IRQ after timeout, preservation of other RCC enables/NVIC priority, + conflicting owner/unsafe priority, allocation failure, sleeping register + timeout, and rejecting pre-scheduler/ISR contexts. CPU time is measured to + ensure blocked waits do not consume their wall-time budget on the CPU. +- STM32F4/F7 builds passed. +- Board RTC tests: 8/8; RNG regression: 6/6; /dev devices and DHCP address present. +- Actual LSE ready IRQ confirmed with LSE initially disabled (RCC BDCR=0x8100), + by capturing the real thread's semaphore-wake message in the ST-Link log ring. + A boot with an already-running oscillator does not exercise that path. + +[IRQ validation evidence](validation/irq-waits-2026-10-16.txt). + +Current STM32F7 package hashes: + +- `dmrtc.dmfc`: `9d0cccc0609c9ebf08cafaf4c3dc293012e61f2f9825eae828d03a345340ec34` + +- `dmrtc_port.dmfc`: `6f8f416592d6a7a8eccfe794d909536824fc5ff02bad47dd434a9ae84a26390c` + +## Standard RTC device contract — 2026-10-16 + +Public device commands and fixed-width calendar/capability/status structures now +come from `dmdrvi_ioctl.h`. The CLI includes no dmrtc API and opens a selected +VFS path. Private numeric ioctl commands are rejected. + +- Native ctest: 3/3 targets (12/12 driver steps, STM32 wait tests, generic CLI + tested with a separate read-only/subsecond RTC outside STM32's year range). +- STM32F4/F7 core, port, client and test builds passed. +- STM32F746G-DISCO: RTC 11/11, dmdrvi 6/6, RNG regression 6/6. +- CLI info/status/get and `-d /dev/rtc` checked through VFS; set restored PC UTC. +- STM32 reports 2000..2099 and one-second granularity. Unsupported fractional + writes fail with ERANGE and preserve the prior calendar. Loss detection is + not advertised; the CLI prints `time_lost=unknown`. +- Hardware validation includes the dmdevfs fix translating dmfsi RDWR=3 into + dmdrvi RDWR=4. Production integration requires that fix and a dmdrvi release + carrying the new header. Local build manifests supplied the unreleased + headers during validation; no dependency versions are pinned in source. +- dmdrvi ARM diagnostics were configured with DMOD's architecture tools-cfg.cmake + as CMAKE_TOOLCHAIN_FILE before project language initialization. + +[Console evidence](validation/rtc-device-contract-2026-10-16.txt). + +Validated STM32F7 compressed packages: + +- `dmrtc.dmfc`: `c3b4920850a903f9398d1f539d6435318e906fc327974bfe479ca36344421738` +- `dmrtc_port.dmfc`: `6f8f416592d6a7a8eccfe794d909536824fc5ff02bad47dd434a9ae84a26390c` +- `rtc.dmfc`: `d5c120a682f5fab69631444fc6edc45d2df42b8eeea38302acc21afb59962810` +- `test_dmrtc.dmfc`: `2106b08b510d912fa778fd67eb44a9903808cac87eb42214d0295dcae43ab10a` diff --git a/docs/validation/irq-waits-2026-10-16.txt b/docs/validation/irq-waits-2026-10-16.txt new file mode 100644 index 0000000..7949b22 --- /dev/null +++ b/docs/validation/irq-waits-2026-10-16.txt @@ -0,0 +1,75 @@ +LSE disabled before start: RCC BDCR = 0x00008100. +Real semaphore wake observed: dmrtc: RTC clock ready (RCC IRQ) +Captured in dmrtc-irq-long-58.bin + + + +2: Test command: /data/codex/dmrtc/build/host/tests/test_stm32_wait +2: Working Directory: /data/codex/dmrtc/build/host/tests +2: Test timeout computed to be: 10 +2: PASS already-ready fast path +2: PASS LSI IRQ wake +2: PASS LSE IRQ wake and unrelated IRQ ignored +2: PASS event before blocked wait +2: PASS timeout sleeps: wall=80 ms CPU=12 us; late IRQ safe +2: PASS shared RCC source and NVIC state preserved +2: PASS existing owner and unsafe IRQ priority rejected +2: PASS allocation failure leaves oscillator untouched +2: PASS register timeout sleeps: wall=20 ms CPU=148 us +2: PASS blocking calls reject pre-scheduler and ISR contexts +2/2 Test #2: stm32_rtc_wait ................... Passed 0.12 sec + +100% tests passed, 0 tests failed out of 2 + +Total Test time (real) = 0.27 sec + +Final hardware suite with RCC IRQ and scheduled boot: +echo RTC_IRQ_FINAL +RTC_IRQ_FINAL +stm32f746xg@/> rtc status +running=1 calendar_set=1 clock=lse nominal_hz=32768 +stm32f746xg@/> test_dmrtc +=== DMOD Test Runner === +[ RUN ] year_rollover +[ OK ] year_rollover +[ RUN ] leap_day_rollover +[ OK ] leap_day_rollover +[ RUN ] vfs_ioctl_and_access_permissions +[ OK ] vfs_ioctl_and_access_permissions +[ RUN ] single_owner_and_bad_configuration +[ OK ] single_owner_and_bad_configuration +[ RUN ] year_2000_is_not_an_uninitialized_calendar +[ OK ] year_2000_is_not_an_uninitialized_calendar +[ RUN ] set_read_and_reject_invalid +[ OK ] set_read_and_reject_invalid +[ RUN ] unset_calendar +[ OK ] unset_calendar +[ RUN ] calendar_validation +[ OK ] calendar_validation + +=== Results: 8/8 passed === +stm32f746xg@/> test_dmrng +=== DMOD Test Runner === +[ RUN ] vfs_device +dmdevfs: [ERROR] Driver failed to open device: /rng +[ERROR] Failed to open file '/dev/rng' +[ OK ] vfs_device +[ RUN ] sustained_generation +RNG_SAMPLE bytes=65536 checksum=565a0836 +[ OK ] sustained_generation +[ RUN ] single_owner_and_configuration_validation +[ OK ] single_owner_and_configuration_validation +[ RUN ] independent_blocks +[ OK ] independent_blocks +[ RUN ] unaligned_and_guarded_buffer +[ OK ] unaligned_and_guarded_buffer +[ RUN ] zero_and_invalid_reads +[ OK ] zero_and_invalid_reads + +=== Results: 6/6 passed === +stm32f746xg@/> rtc set 2026-10-16T05:15:37 +stm32f746xg@/> rtc get +2026-10-16T05:15:37 weekday=5 +stm32f746xg@/> ip addr +eth0: 192.168.50.184/24 +stm32f746xg@/> diff --git a/docs/validation/rtc-device-contract-2026-10-16.txt b/docs/validation/rtc-device-contract-2026-10-16.txt new file mode 100644 index 0000000..9e4e98c --- /dev/null +++ b/docs/validation/rtc-device-contract-2026-10-16.txt @@ -0,0 +1,99 @@ +rtc info +years=2000..2099 resolution_ns=1000000000 writable=1 invalidatable=1 loss_detection=0 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc status +running=1 calendar_set=1 time_lost=unknown +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc get +2026-10-16T06:13:03 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc -d /dev/rtc get +2026-10-16T06:13:03 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmrtc +=== DMOD Test Runner === +[ RUN ] year_rollover +[ OK ] year_rollover +[ RUN ] leap_day_rollover +[ OK ] leap_day_rollover +[ RUN ] vfs_ioctl_and_access_permissions +[ OK ] vfs_ioctl_and_access_permissions +[ RUN ] single_owner_and_bad_configuration +[ OK ] single_owner_and_bad_configuration +[ RUN ] year_2000_is_not_an_uninitialized_calendar +[ OK ] year_2000_is_not_an_uninitialized_calendar +[ RUN ] set_read_and_reject_invalid +[ OK ] set_read_and_reject_invalid +[ RUN ] unset_calendar +[ OK ] unset_calendar +[ RUN ] calendar_validation +[ OK ] calendar_validation +[ RUN ] private_rtc_command_numbers_are_not_supported +[ OK ] private_rtc_command_numbers_are_not_supported +[ RUN ] standard_rtc_range_and_precision_are_enforced +[ OK ] standard_rtc_range_and_precision_are_enforced +[ RUN ] standard_rtc_capabilities_and_unset_state +[ OK ] standard_rtc_capabilities_and_unset_state + +=== Results: 11/11 passed === +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmdrvi +=== DMOD Test Runner === +[ RUN ] dmdrvi_input_states_compare_every_field +[ OK ] dmdrvi_input_states_compare_every_field +[ RUN ] dmdrvi_monitor_policy_holds_longest_handler_name +[ OK ] dmdrvi_monitor_policy_holds_longest_handler_name +[ RUN ] dmdrvi_monitor_commands_are_distinct +[ OK ] dmdrvi_monitor_commands_are_distinct +[ RUN ] dmdrvi_errors_are_distinct_from_eof +[ OK ] dmdrvi_errors_are_distinct_from_eof +[ RUN ] dmdrvi_sizes_round_trip_above_four_gib +[ OK ] dmdrvi_sizes_round_trip_above_four_gib +[ RUN ] dmdrvi_v2_signatures_are_registered +[ OK ] dmdrvi_v2_signatures_are_registered + +=== Results: 6/6 passed === +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmrng +=== DMOD Test Runner === +[ RUN ] vfs_device +dmdevfs: [ERROR] Driver failed to open device: /rng +[ERROR] Failed to open file '/dev/rng' +[ OK ] vfs_device +[ RUN ] sustained_generation +RNG_SAMPLE bytes=65536 checksum=2231c26b +[ OK ] sustained_generation +[ RUN ] single_owner_and_configuration_validation +[ OK ] single_owner_and_configuration_validation +[ RUN ] independent_blocks +[ OK ] independent_blocks +[ RUN ] unaligned_and_guarded_buffer +[ OK ] unaligned_and_guarded_buffer +[ RUN ] zero_and_invalid_reads +[ OK ] zero_and_invalid_reads + +=== Results: 6/6 passed === +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc set 2026-10-16T06:13:10 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc get +2026-10-16T06:13:10 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc status +running=1 calendar_set=1 time_lost=unknown +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> ip addr +eth0: 192.168.50.184/24 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> \ No newline at end of file diff --git a/docs/validation/stm32f746g-disco.txt b/docs/validation/stm32f746g-disco.txt new file mode 100644 index 0000000..6c52687 --- /dev/null +++ b/docs/validation/stm32f746g-disco.txt @@ -0,0 +1,104 @@ +STM32F746G-DISCO RTC validation, 2026-10-15 UTC + +--- dmrtc-board-final.log --- +stm32f746xg@/> echo RTC_FINAL_READY +RTC_FINAL_READY +stm32f746xg@/> echo RTC_FINAL_READY +RTC_FINAL_READY +stm32f746xg@/> rtc status +running=1 calendar_set=1 clock=lse nominal_hz=32768 +stm32f746xg@/> rtc get +2000-03-01T00:02:14 weekday=3 +stm32f746xg@/> test_dmrtc +=== DMOD Test Runner === +[ RUN ] year_rollover +[ OK ] year_rollover +[ RUN ] leap_day_rollover +[ OK ] leap_day_rollover +[ RUN ] vfs_ioctl_and_access_permissions +[ OK ] vfs_ioctl_and_access_permissions +[ RUN ] single_owner_and_bad_configuration +[ OK ] single_owner_and_bad_configuration +[ RUN ] year_2000_is_not_an_uninitialized_calendar +[ OK ] year_2000_is_not_an_uninitialized_calendar +[ RUN ] set_read_and_reject_invalid +[ OK ] set_read_and_reject_invalid +[ RUN ] unset_calendar +[ OK ] unset_calendar +[ RUN ] calendar_validation +[ OK ] calendar_validation + +=== Results: 8/8 passed === +stm32f746xg@/> test_dmrtc +=== DMOD Test Runner === +[ RUN ] year_rollover +[ OK ] year_rollover +[ RUN ] leap_day_rollover +[ OK ] leap_day_rollover +[ RUN ] vfs_ioctl_and_access_permissions +[ OK ] vfs_ioctl_and_access_permissions +[ RUN ] single_owner_and_bad_configuration +[ OK ] single_owner_and_bad_configuration +[ RUN ] year_2000_is_not_an_uninitialized_calendar +[ OK ] year_2000_is_not_an_uninitialized_calendar +[ RUN ] set_read_and_reject_invalid +[ OK ] set_read_and_reject_invalid +[ RUN ] unset_calendar +[ OK ] unset_calendar +[ RUN ] calendar_validation +[ OK ] calendar_validation + +=== Results: 8/8 passed === +stm32f746xg@/> test_dmrtc +=== DMOD Test Runner === +[ RUN ] year_rollover +[ OK ] year_rollover +[ RUN ] leap_day_rollover +[ OK ] leap_day_rollover +[ RUN ] vfs_ioctl_and_access_permissions +[ OK ] vfs_ioctl_and_access_permissions +[ RUN ] single_owner_and_bad_configuration +[ OK ] single_owner_and_bad_configuration +[ RUN ] year_2000_is_not_an_uninitialized_calendar +[ OK ] year_2000_is_not_an_uninitialized_calendar +[ RUN ] set_read_and_reject_invalid +[ OK ] set_read_and_reject_invalid +[ RUN ] unset_calendar +[ OK ] unset_calendar +[ RUN ] calendar_validation +[ OK ] calendar_validation + +=== Results: 8/8 passed === +stm32f746xg@/> rtc set 2026-10-15T20:49:01 +stm32f746xg@/> rtc get +2026-10-15T20:49:01 weekday=4 +stm32f746xg@/> rtc status +running=1 calendar_set=1 clock=lse nominal_hz=32768 +stm32f746xg@/> +--- dmrtc-board-reset-before.log --- +rtc set 2000-02-29T23:59:45 +stm32f746xg@/> rtc get +2000-02-29T23:59:45 weekday=2 +stm32f746xg@/> rtc status +running=1 calendar_set=1 clock=lse nominal_hz=32768 +stm32f746xg@/> +--- dmrtc-board-reset-after.log --- +echo RTC_RESET_READY +RTC_RESET_READY +stm32f746xg@/> rtc status +running=1 calendar_set=1 clock=lse nominal_hz=32768 +stm32f746xg@/> rtc get +2000-03-01T00:00:30 weekday=3 +stm32f746xg@/> +--- final firmware: ticking and reset retention --- +rtc get +2026-10-15T20:49:43 weekday=4 +stm32f746xg@/> rtc get +2026-10-15T20:49:46 weekday=4 +stm32f746xg@/> echo RTC_RETAINED_READY +RTC_RETAINED_READY +stm32f746xg@/> rtc status +running=1 calendar_set=1 clock=lse nominal_hz=32768 +stm32f746xg@/> rtc get +2026-10-15T20:50:23 weekday=4 +stm32f746xg@/> diff --git a/include/dmrtc.h b/include/dmrtc.h new file mode 100644 index 0000000..9b375e5 --- /dev/null +++ b/include/dmrtc.h @@ -0,0 +1,28 @@ +#ifndef DMRTC_H +#define DMRTC_H +#include +#include +#include "dmod_types.h" +#include "dmrtc_defs.h" + +#include "dmrtc_types.h" + +/* One hardware owner. NULL configuration selects DMRTC_CONFIG_DEFAULT. + * Lifecycle (create/destroy/default) must be externally serialized with users. + * Operations on a live context are serialized internally. */ +dmod_dmrtc_api(1.0, dmrtc_t, _create, (const dmrtc_config_t* config)); +dmod_dmrtc_api(1.0, int, _destroy, (dmrtc_t ctx)); +dmod_dmrtc_api(1.0, bool, _is_valid, (dmrtc_t ctx)); +/* Borrowed handle, owned by the board driver; do not destroy it. */ +dmod_dmrtc_api(1.0, dmrtc_t, _default, (void)); +/* Returns -ENODATA until explicitly set; output is cleared on failure. */ +dmod_dmrtc_api(1.0, int, _get, (dmrtc_t ctx, dmrtc_calendar_t* calendar)); +dmod_dmrtc_api(1.0, int, _set, (dmrtc_t ctx, const dmrtc_calendar_t* calendar)); +dmod_dmrtc_api(1.0, int, _status, (dmrtc_t ctx, dmrtc_status_t* status)); +dmod_dmrtc_api(1.0, int, _invalidate, (dmrtc_t ctx)); +/* Pure validation/weekday normalization. Input is unchanged on error. */ +dmod_dmrtc_api(1.0, int, _normalize, (dmrtc_calendar_t* calendar)); + +/* Device files implement the standard DMDRVI_IOCTL_RTC_* contract from + * dmdrvi_ioctl.h. This direct context API is for driver/port integration. */ +#endif diff --git a/include/dmrtc_mock.h b/include/dmrtc_mock.h new file mode 100644 index 0000000..747b232 --- /dev/null +++ b/include/dmrtc_mock.h @@ -0,0 +1,6 @@ +#ifndef DMRTC_MOCK_H +#define DMRTC_MOCK_H +#include "dmrtc_port.h" +/* Test-only port: preserve state across release/acquire, inject next set failure. */ +dmod_dmrtc_port_api(1.0, void, _mock_fail_set, (int error)); +#endif diff --git a/include/dmrtc_port.h b/include/dmrtc_port.h new file mode 100644 index 0000000..3d932ee --- /dev/null +++ b/include/dmrtc_port.h @@ -0,0 +1,15 @@ +#ifndef DMRTC_PORT_H +#define DMRTC_PORT_H +#include "dmod_types.h" +#include "dmrtc_types.h" +#include "dmrtc_port_defs.h" +/* Core holds its mutex across every port call. Release leaves the clock and + * calendar running. Set clears the persistent marker before modifying calendar + * registers, and commits it only after successful initialization exit. */ +dmod_dmrtc_port_api(1.0, int, _acquire, (const dmrtc_config_t* config)); +dmod_dmrtc_port_api(1.0, int, _release, (void)); +dmod_dmrtc_port_api(1.0, int, _get, (dmrtc_calendar_t* calendar)); +dmod_dmrtc_port_api(1.0, int, _set, (const dmrtc_calendar_t* calendar)); +dmod_dmrtc_port_api(1.0, int, _status, (dmrtc_status_t* status)); +dmod_dmrtc_port_api(1.0, int, _invalidate, (void)); +#endif diff --git a/include/dmrtc_types.h b/include/dmrtc_types.h new file mode 100644 index 0000000..245ee0d --- /dev/null +++ b/include/dmrtc_types.h @@ -0,0 +1,28 @@ +#ifndef DMRTC_TYPES_H +#define DMRTC_TYPES_H +#include +#include +typedef struct dmrtc* dmrtc_t; +typedef enum { DMRTC_CLOCK_LSE = 1, DMRTC_CLOCK_LSI = 2 } dmrtc_clock_t; +/* Civil calendar, 2000..2099. Weekday: Monday=1 .. Sunday=7; computed on set. + * The driver does not assign a timezone or certify the source of this time. */ +typedef struct { + uint16_t year; + uint8_t month, day, hour, minute, second, weekday; +} dmrtc_calendar_t; +typedef struct { + uint32_t instance; + dmrtc_clock_t clock; + uint32_t clock_hz; + uint32_t timeout_ms; /* Oscillator startup only; waiting task sleeps on an IRQ event. */ + uint32_t backup_register; /* Reserves this register AND the next one. */ +} dmrtc_config_t; +#define DMRTC_CONFIG_DEFAULT {0, DMRTC_CLOCK_LSE, 32768, 5000, 18} +typedef struct { + bool running; + bool calendar_set; /* Explicitly set and structurally valid, NOT trusted UTC. */ + dmrtc_clock_t clock; + uint32_t clock_hz; /* Configured nominal frequency, not a measurement. */ +} dmrtc_status_t; + +#endif diff --git a/manifest.dmm b/manifest.dmm new file mode 100644 index 0000000..612b2d1 --- /dev/null +++ b/manifest.dmm @@ -0,0 +1,12 @@ +# Include dynamically generated versions list from latest release +$include https://github.com/choco-technologies/dmrtc/releases/download/vlatest/versions.dmm + +# Module entries with version placeholder - expanded by $version-available. +# Both the core module and the port use here, not : +# the release workflow packages this repo per DMOD_CPU_FAMILY, not per full +# architecture (see dmfmc/manifest.dmm or dmuart/manifest.dmm for the +# real-world reference). +dmrtc https://github.com/choco-technologies/dmrtc/releases/download/v/dmrtc-v-.zip +dmrtc_port https://github.com/choco-technologies/dmrtc/releases/download/v/dmrtc_port-v-.zip +rtc https://github.com/choco-technologies/dmrtc/releases/download/v/rtc-v-.zip +test_dmrtc https://github.com/choco-technologies/dmrtc/releases/download/v/test_dmrtc-v-.zip diff --git a/scripts/sync-claude.sh b/scripts/sync-claude.sh new file mode 100755 index 0000000..8ce1006 --- /dev/null +++ b/scripts/sync-claude.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# ############################################################################## +# +# Syncs Claude Code skills (.claude/skills/) from the dmod repository into the +# current module repository. Run this explicitly whenever you want the latest +# ecosystem knowledge available to Claude Code - it is not run automatically +# by CMake or by any build step, on purpose: the result is meant to be +# reviewed and committed like any other file, not regenerated silently. +# +# Only .claude/skills/ is touched. Local .claude/settings.json, hooks, or +# other skills you added under a different name are left alone. +# +# Usage: +# ./scripts/sync-claude.sh [--dmod-dir PATH] [--ref REF] +# +# Options: +# --dmod-dir PATH Use a local dmod checkout instead of cloning from GitHub +# --ref REF Git branch/tag to clone when no --dmod-dir is given +# (default: develop) +# --help Show this help message +# +# ############################################################################## + +set -e + +DMOD_DIR="" +GIT_REF="develop" +DMOD_REPO_URL="https://github.com/choco-technologies/dmod.git" + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +print_error() { + echo -e "${RED}Error: $1${NC}" >&2 +} + +print_success() { + echo -e "${GREEN}$1${NC}" +} + +print_help() { + cat << EOF +Sync Claude Code skills from dmod + +Usage: + ./scripts/sync-claude.sh [--dmod-dir PATH] [--ref REF] + +Options: + --dmod-dir PATH Use a local dmod checkout instead of cloning from GitHub + --ref REF Git branch/tag to clone when no --dmod-dir is given (default: develop) + --help Show this help message +EOF +} + +while [[ $# -gt 0 ]]; do + case $1 in + --dmod-dir) + DMOD_DIR="$2" + shift 2 + ;; + --ref) + GIT_REF="$2" + shift 2 + ;; + --help) + print_help + exit 0 + ;; + *) + print_error "Unknown parameter: $1" + print_help + exit 1 + ;; + esac +done + +TMP_CLONE_DIR="" + +cleanup() { + if [[ -n "${TMP_CLONE_DIR}" && -d "${TMP_CLONE_DIR}" ]]; then + rm -rf "${TMP_CLONE_DIR}" + fi +} +trap cleanup EXIT + +if [[ -n "${DMOD_DIR}" ]]; then + if [[ ! -d "${DMOD_DIR}" ]]; then + print_error "--dmod-dir does not exist: ${DMOD_DIR}" + exit 1 + fi + SOURCE_SKILLS_DIR="${DMOD_DIR}/.claude/skills" + echo "Using local dmod checkout: ${DMOD_DIR}" +else + TMP_CLONE_DIR="$(mktemp -d)" + echo "Cloning dmod (branch: ${GIT_REF}) ..." + git clone --depth 1 --branch "${GIT_REF}" "${DMOD_REPO_URL}" "${TMP_CLONE_DIR}" > /dev/null 2>&1 + SOURCE_SKILLS_DIR="${TMP_CLONE_DIR}/.claude/skills" +fi + +if [[ ! -d "${SOURCE_SKILLS_DIR}" ]]; then + print_error "No .claude/skills directory found at ${SOURCE_SKILLS_DIR}" + exit 1 +fi + +mkdir -p .claude/skills +cp -r "${SOURCE_SKILLS_DIR}/." .claude/skills/ + +echo "" +print_success "Synced Claude Code skills:" +for skill_dir in .claude/skills/*/; do + [[ -d "${skill_dir}" ]] || continue + echo " - $(basename "${skill_dir}")" +done +echo "" +echo "Review the changes with 'git status'/'git diff' and commit them like any other file." diff --git a/src/dmrtc.c b/src/dmrtc.c new file mode 100644 index 0000000..d59f506 --- /dev/null +++ b/src/dmrtc.c @@ -0,0 +1,241 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmrtc_internal.h" +#include "dmrtc_port.h" +#include "dmdrvi.h" +#include "dmini.h" +#include +#include + +static dmrtc_t active; +static dmosi_mutex_t registry_lock; + +dmod_dmrtc_api_declaration(1.0, int, _normalize, (dmrtc_calendar_t* c)) +{ + static const uint8_t days[] = {31,28,31,30,31,30,31,31,30,31,30,31}; + static const uint16_t before[] = {0,31,59,90,120,151,181,212,243,273,304,334}; + if (c == NULL || c->year < 2000 || c->year > 2099 || c->month < 1 || + c->month > 12 || c->hour > 23 || c->minute > 59 || c->second > 59) return -EINVAL; + bool leap = c->year % 4 == 0; /* Calendar range excludes non-leap centuries. */ + unsigned maximum = days[c->month - 1] + (c->month == 2 && leap); + if (c->day == 0 || c->day > maximum) return -EINVAL; + unsigned years = c->year - 2000; + unsigned elapsed = years * 365 + (years + 3) / 4 + before[c->month - 1] + + (leap && c->month > 2) + c->day - 1; + c->weekday = (uint8_t)((elapsed + 5) % 7 + 1); /* 2000-01-01 = Saturday. */ + return 0; +} + +dmod_dmrtc_api_declaration(1.0, bool, _is_valid, (dmrtc_t ctx)) +{ + return ctx != NULL && ctx == active && ctx->magic == DMRTC_MAGIC; +} +dmod_dmrtc_api_declaration(1.0, dmrtc_t, _default, (void)) { return active; } + +static bool valid_config(const dmrtc_config_t* c) +{ + return c->instance == 0 && (c->clock == DMRTC_CLOCK_LSE || c->clock == DMRTC_CLOCK_LSI) && + c->clock_hz >= 128 && c->clock_hz <= 65536 && c->clock_hz % 128 == 0 && + c->timeout_ms > 0 && c->timeout_ms <= 10000 && c->backup_register < 19; +} +dmod_dmrtc_api_declaration(1.0, dmrtc_t, _create, (const dmrtc_config_t* config)) +{ + dmrtc_config_t cfg = DMRTC_CONFIG_DEFAULT; + if (config != NULL) cfg = *config; + if (!valid_config(&cfg) || registry_lock == NULL) return NULL; + if (dmosi_mutex_lock(registry_lock) != 0) return NULL; + dmrtc_t ctx = active == NULL ? Dmod_Malloc(sizeof(*ctx)) : NULL; + if (ctx != NULL) { + memset(ctx, 0, sizeof(*ctx)); + ctx->config = cfg; + ctx->lock = dmosi_mutex_create(false); + int rc = ctx->lock == NULL ? -ENOMEM : dmrtc_port_acquire(&cfg); + if (rc != 0) { + DMOD_LOG_ERROR("dmrtc: acquire failed (%d)\n", rc); + if (ctx->lock != NULL) dmosi_mutex_destroy(ctx->lock); + Dmod_Free(ctx); ctx = NULL; + } else { ctx->magic = DMRTC_MAGIC; active = ctx; } + } + dmosi_mutex_unlock(registry_lock); + return ctx; +} +dmod_dmrtc_api_declaration(1.0, int, _destroy, (dmrtc_t ctx)) +{ + if (ctx == NULL) return 0; + if (!dmrtc_is_valid(ctx)) return -EINVAL; + int rc = dmosi_mutex_lock(registry_lock); + if (rc != 0) return rc; + rc = dmosi_mutex_lock(ctx->lock); + if (rc == 0) { + rc = ctx->opens ? -EBUSY : dmrtc_port_release(); + dmosi_mutex_unlock(ctx->lock); + if (rc == 0) { + active = NULL; ctx->magic = 0; + dmosi_mutex_destroy(ctx->lock); Dmod_Free(ctx); + } + } + dmosi_mutex_unlock(registry_lock); + return rc; +} +dmod_dmrtc_api_declaration(1.0, int, _get, (dmrtc_t ctx, dmrtc_calendar_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (!dmrtc_is_valid(ctx)) return -EINVAL; + int rc = dmosi_mutex_lock(ctx->lock); + if (rc != 0) return rc; + rc = dmrtc_port_get(out); + if (rc == 0 && dmrtc_normalize(out) != 0) rc = -EIO; + if (rc != 0) memset(out, 0, sizeof(*out)); + dmosi_mutex_unlock(ctx->lock); + return rc; +} +dmod_dmrtc_api_declaration(1.0, int, _set, (dmrtc_t ctx, const dmrtc_calendar_t* value)) +{ + if (!dmrtc_is_valid(ctx) || value == NULL) return -EINVAL; + dmrtc_calendar_t c = *value; + if (dmrtc_normalize(&c) != 0) return -EINVAL; + int rc = dmosi_mutex_lock(ctx->lock); + if (rc != 0) return rc; + rc = dmrtc_port_set(&c); + dmosi_mutex_unlock(ctx->lock); + return rc; +} +dmod_dmrtc_api_declaration(1.0, int, _status, (dmrtc_t ctx, dmrtc_status_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (!dmrtc_is_valid(ctx)) return -EINVAL; + int rc = dmosi_mutex_lock(ctx->lock); + if (rc != 0) return rc; + rc = dmrtc_port_status(out); + if (rc == 0 && out->calendar_set) { + dmrtc_calendar_t c; + rc = dmrtc_port_get(&c); + if (rc == 0 && dmrtc_normalize(&c) != 0) rc = -EIO; + } + if (rc != 0) memset(out, 0, sizeof(*out)); + dmosi_mutex_unlock(ctx->lock); + return rc; +} +dmod_dmrtc_api_declaration(1.0, int, _invalidate, (dmrtc_t ctx)) +{ + if (!dmrtc_is_valid(ctx)) return -EINVAL; + int rc = dmosi_mutex_lock(ctx->lock); + if (rc != 0) return rc; + rc = dmrtc_port_invalidate(); + dmosi_mutex_unlock(ctx->lock); + return rc; +} +int dmod_init(const Dmod_Config_t* config) +{ + (void)config; + registry_lock = dmosi_mutex_create(false); + return registry_lock == NULL ? -ENOMEM : 0; +} +int dmod_deinit(void) +{ + if (active != NULL) return -EBUSY; + dmosi_mutex_destroy(registry_lock); registry_lock = NULL; + return 0; +} + +static const char* config_section(dmini_context_t ini) +{ + if (dmini_has_key(ini, "rtc", "instance")) return "rtc"; + if (dmini_has_key(ini, "dmrtc", "instance")) return "dmrtc"; + return dmini_section_count(ini) ? dmini_section_name(ini, 0) : "rtc"; +} +static bool parse_config(dmini_context_t ini, dmrtc_config_t* cfg) +{ + if (ini == NULL) return true; + const char* s = config_section(ini); + const char* clock = dmini_get_string(ini, s, "clock", "lse"); + if (strcmp(clock, "lse") == 0) cfg->clock = DMRTC_CLOCK_LSE; + else if (strcmp(clock, "lsi") == 0) { + if (!dmini_has_key(ini, s, "clock_hz")) return false; + cfg->clock = DMRTC_CLOCK_LSI; + } + else return false; + cfg->instance = (uint32_t)dmini_get_int(ini, s, "instance", 0); + cfg->clock_hz = (uint32_t)dmini_get_int(ini, s, "clock_hz", 32768); + cfg->timeout_ms = (uint32_t)dmini_get_int(ini, s, "timeout_ms", 5000); + cfg->backup_register = (uint32_t)dmini_get_int(ini, s, "backup_register", 18); + return valid_config(cfg); +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_context_t, _create, + (dmini_context_t ini, dmdrvi_dev_num_t* number)) +{ + dmrtc_config_t cfg = DMRTC_CONFIG_DEFAULT; + if (number == NULL || !parse_config(ini, &cfg)) return NULL; + dmrtc_t ctx = dmrtc_create(&cfg); + if (ctx == NULL) return NULL; + memset(number, 0, sizeof(*number)); + number->flags = DMDRVI_NUM_MAJOR | DMDRVI_NUM_ALT_NAME; + memcpy(number->alt_name, "rtc", sizeof("rtc")); + return (dmdrvi_context_t)ctx; +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void, _free, (dmdrvi_context_t ctx)) +{ + int rc = dmrtc_destroy((dmrtc_t)ctx); + if (rc != 0) DMOD_LOG_ERROR("dmrtc: release failed (%d)\n", rc); +} +struct rtc_file { dmrtc_t owner; int flags; }; +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void*, _open, + (dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t* number)) +{ + dmrtc_t ctx = (dmrtc_t)context; + int access = flags & (DMDRVI_O_RDONLY | DMDRVI_O_WRONLY | DMDRVI_O_RDWR); + if (!dmrtc_is_valid(ctx) || number == NULL || number->major != 0 || + (access != DMDRVI_O_RDONLY && access != DMDRVI_O_WRONLY && access != DMDRVI_O_RDWR)) + return NULL; + if (dmosi_mutex_lock(ctx->lock) != 0) return NULL; + struct rtc_file* file = ctx->opens == UINT32_MAX ? NULL : Dmod_Malloc(sizeof(*file)); + if (file != NULL) { file->owner = ctx; file->flags = access; ++ctx->opens; } + dmosi_mutex_unlock(ctx->lock); + return file; +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void, _close, + (dmdrvi_context_t context, void* handle)) +{ + dmrtc_t ctx = (dmrtc_t)context; struct rtc_file* file = handle; + if (!dmrtc_is_valid(ctx) || file == NULL || file->owner != ctx) return; + if (dmosi_mutex_lock(ctx->lock) != 0) return; + if (ctx->opens != 0) --ctx->opens; + file->owner = NULL; Dmod_Free(file); + dmosi_mutex_unlock(ctx->lock); +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_ssize_t, _read, + (dmdrvi_context_t ctx, void* handle, void* buffer, size_t size, dmdrvi_offset_t offset)) +{ + (void)ctx; (void)handle; (void)buffer; (void)size; (void)offset; + return -ENOTSUP; +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_ssize_t, _write, + (dmdrvi_context_t ctx, void* handle, const void* buffer, size_t size, dmdrvi_offset_t offset)) +{ + (void)ctx; (void)handle; (void)buffer; (void)size; (void)offset; + return -ENOTSUP; +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, int, _ioctl, + (dmdrvi_context_t context, void* handle, int command, void* arg)) +{ + dmrtc_t ctx = (dmrtc_t)context; struct rtc_file* file = handle; + if (!dmrtc_is_valid(ctx) || file == NULL || file->owner != ctx) return -EBADF; + return dmrtc_device_ioctl(ctx, file->flags, command, arg); +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, int, _flush, + (dmdrvi_context_t context, void* handle)) +{ + struct rtc_file* file = handle; + return dmrtc_is_valid((dmrtc_t)context) && file != NULL && + file->owner == (dmrtc_t)context ? 0 : -EBADF; +} +dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, int, _stat, + (dmdrvi_context_t ctx, const char* path, dmdrvi_stat_t* stat)) +{ + (void)path; + if (!dmrtc_is_valid((dmrtc_t)ctx) || stat == NULL) return -EINVAL; + stat->size = 0; stat->mode = 0020000 | 0666; + return 0; +} diff --git a/src/dmrtc_internal.h b/src/dmrtc_internal.h new file mode 100644 index 0000000..93dff94 --- /dev/null +++ b/src/dmrtc_internal.h @@ -0,0 +1,13 @@ +#ifndef DMRTC_INTERNAL_H +#define DMRTC_INTERNAL_H +#include "dmrtc.h" +#include "dmosi.h" +#define DMRTC_MAGIC 0x52544321u +struct dmrtc { + uint32_t magic; + dmrtc_config_t config; + dmosi_mutex_t lock; + uint32_t opens; +}; +int dmrtc_device_ioctl(dmrtc_t ctx, int flags, int command, void* arg); +#endif diff --git a/src/dmrtc_ioctl.c b/src/dmrtc_ioctl.c new file mode 100644 index 0000000..7781211 --- /dev/null +++ b/src/dmrtc_ioctl.c @@ -0,0 +1,68 @@ +#include "dmod.h" +#include "dmrtc_internal.h" +#include "dmdrvi.h" +#include +#include + +static int get_time(dmrtc_t ctx, dmdrvi_rtc_time_t* out) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + dmrtc_calendar_t c; + int rc = dmrtc_get(ctx, &c); + if (rc == 0) { + out->year = c.year; out->month = c.month; out->day = c.day; + out->hour = c.hour; out->minute = c.minute; out->second = c.second; + out->weekday = c.weekday; + } + return rc; +} +static int set_time(dmrtc_t ctx, const dmdrvi_rtc_time_t* in) +{ + if (in == NULL || in->reserved != 0 || in->year < 1 || in->year > 9999 || + in->nanosecond >= 1000000000u) return -EINVAL; + if (in->year < 2000 || in->year > 2099) return -ERANGE; + dmrtc_calendar_t c = {(uint16_t)in->year, in->month, in->day, + in->hour, in->minute, in->second, 0}; + if (dmrtc_normalize(&c) != 0) return -EINVAL; + if (in->nanosecond != 0) return -ERANGE; + return dmrtc_set(ctx, &c); +} +static int get_info(dmdrvi_rtc_info_t* out) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + out->resolution_ns = 1000000000u; + out->capabilities = DMDRVI_RTC_CAP_SET_TIME | DMDRVI_RTC_CAP_INVALIDATE; + out->min_year = 2000; out->max_year = 2099; + /* STM32F4/F7 cannot attest calendar continuity across all supply failures. */ + return 0; +} +static int get_status(dmrtc_t ctx, dmdrvi_rtc_status_t* out) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + dmrtc_status_t s; + int rc = dmrtc_status(ctx, &s); + if (rc == 0) { + if (s.running) out->flags |= DMDRVI_RTC_STATUS_RUNNING; + if (s.calendar_set) out->flags |= DMDRVI_RTC_STATUS_TIME_SET; + } + return rc; +} +int dmrtc_device_ioctl(dmrtc_t ctx, int flags, int command, void* arg) +{ + bool write = command == DMDRVI_IOCTL_RTC_SET_TIME || command == DMDRVI_IOCTL_RTC_INVALIDATE; + bool read = command == DMDRVI_IOCTL_RTC_GET_TIME || command == DMDRVI_IOCTL_RTC_GET_STATUS || + command == DMDRVI_IOCTL_RTC_GET_INFO; + if (!write && !read) return -ENOTTY; + if (write && !(flags & (DMDRVI_O_WRONLY | DMDRVI_O_RDWR))) return -EACCES; + if (read && !(flags & (DMDRVI_O_RDONLY | DMDRVI_O_RDWR))) return -EACCES; + switch (command) { + case DMDRVI_IOCTL_RTC_GET_TIME: return get_time(ctx, arg); + case DMDRVI_IOCTL_RTC_SET_TIME: return set_time(ctx, arg); + case DMDRVI_IOCTL_RTC_GET_STATUS: return get_status(ctx, arg); + case DMDRVI_IOCTL_RTC_GET_INFO: return get_info(arg); + default: return arg == NULL ? dmrtc_invalidate(ctx) : -EINVAL; + } +} diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt new file mode 100644 index 0000000..d6c4039 --- /dev/null +++ b/src/port/CMakeLists.txt @@ -0,0 +1,49 @@ +# ====================================================================== +# dmrtc_port Module Configuration +# ====================================================================== + +# Name of the module +set(DMOD_MODULE_NAME dmrtc_port) + +# Version is already set above and used in project() + +# Author (should be string) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") + +# Stack size for the module (should be integer) +set(DMOD_STACK_SIZE 1024) + +# Path to the DMR file for this module +set(DMOD_DMR_PATH ${CMAKE_SOURCE_DIR}/dmrtc_port.dmr) + +# Keep register-level implementation shared by STM32 families. Other +# architectures can supply their own implementation without compiling this code. +set(COMMON_SOURCES) +if(DMOD_CPU_FAMILY MATCHES "^stm32f[47]$") + list(APPEND COMMON_SOURCES stm32_common/stm32_common.c stm32_common/clock_wait.c) +endif() + +# +# dmod_add_library - create a library module +# it has the same signature as add_library +# and can be used in the same way after the creation +# (for example, to link libraries) +# +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + # List of source files - can include C and C++ files + ${DMOD_CPU_FAMILY}/port.c + ${COMMON_SOURCES} +) + +target_include_directories(${DMOD_MODULE_NAME} BEFORE PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE + ${CMAKE_SOURCE_DIR}/include +) + +if(DMOD_CPU_FAMILY MATCHES "^stm32f[47]$") + dmod_link_modules(${DMOD_MODULE_NAME} dmosi) +endif() diff --git a/src/port/stm32_common/clock_wait.c b/src/port/stm32_common/clock_wait.c new file mode 100644 index 0000000..f74c434 --- /dev/null +++ b/src/port/stm32_common/clock_wait.c @@ -0,0 +1,90 @@ +#include "dmod.h" +#include "dmosi.h" +#include "registers.h" +#include "clock_wait.h" +#include +#include + +/* Only our selected RCC ready source is owned, never the entire RCC IRQ. + * DMOD dispatches a shared IRQ to each registered module handler. */ +static dmosi_semaphore_t volatile clock_event; +static volatile uint32_t owned_flag; +struct irq_guard { bool enabled; uint8_t priority; uint32_t flag; }; + +int stm32_rtc_wait_register(volatile uint32_t* reg, uint32_t mask, uint32_t value) +{ + if (!dmosi_is_started() || Dmod_IsInsideInterrupt()) return -ENOTSUP; + uint64_t start = Dmod_GetUptime(); + for (;;) { + if ((*reg & mask) == value) return 0; + if (Dmod_GetUptime() - start >= REGISTER_TIMEOUT_MS) return -ETIMEDOUT; + dmosi_thread_sleep(1); /* Block this task so lower-priority work can run. */ + } +} + +static int arm_irq(struct irq_guard* guard, dmosi_semaphore_t event) +{ + uint32_t minimum = dmosi_get_min_interrupt_priority(); + Dmod_EnterCritical(); + guard->enabled = (NVIC32(0) & RCC_IRQ_BIT) != 0; + guard->priority = NVIC_PRIORITY; + if (clock_event != NULL || (RCC(RCC_CIR) & (guard->flag << 8)) || + (guard->enabled && guard->priority < minimum)) { + Dmod_ExitCritical(); return -EBUSY; + } + clock_event = event; + owned_flag = guard->flag; + if (!guard->enabled) NVIC_PRIORITY = (uint8_t)minimum; + /* Preserve other enables, clear only our stale flag, then arm our source. */ + RCC(RCC_CIR) = (RCC(RCC_CIR) & RCC_IRQ_ENABLES) | + (guard->flag << 8) | (guard->flag << 16); + NVIC32(0) = RCC_IRQ_BIT; + Dmod_ExitCritical(); + return 0; +} +static void disarm_irq(const struct irq_guard* guard) +{ + Dmod_EnterCritical(); + uint32_t enables = RCC(RCC_CIR) & RCC_IRQ_ENABLES & ~(guard->flag << 8); + RCC(RCC_CIR) = enables | (guard->flag << 16); + owned_flag = 0; + clock_event = NULL; + if (!guard->enabled && enables == 0) { + NVIC32(0x80) = RCC_IRQ_BIT; + NVIC_PRIORITY = guard->priority; + } + Dmod_ExitCritical(); +} +int stm32_rtc_start_clock(dmrtc_clock_t clock, uint32_t timeout_ms) +{ + if ((clock != DMRTC_CLOCK_LSE && clock != DMRTC_CLOCK_LSI) || + timeout_ms == 0 || timeout_ms > 10000) return -EINVAL; + if (!dmosi_is_started() || Dmod_IsInsideInterrupt()) return -ENOTSUP; + volatile uint32_t* reg = clock == DMRTC_CLOCK_LSE ? &RCC(BDCR) : &RCC(CSR); + struct irq_guard guard = {false, 0, clock == DMRTC_CLOCK_LSE ? 2u : 1u}; + if ((*reg & 3u) == 3u) return 0; /* Oscillator already enabled and ready. */ + dmosi_semaphore_t event = dmosi_semaphore_create(0, 1); + if (event == NULL) return -ENOMEM; + int rc = arm_irq(&guard, event); + if (rc == 0) { + Dmod_EnterCritical(); *reg |= 1u; Dmod_ExitCritical(); + /* Covers a ready event before the wait; semaphore retains an early ISR post. */ + if (!(*reg & 2u)) { + rc = dmosi_semaphore_wait(event, 1, (int32_t)timeout_ms); + if (rc == 0) Dmod_Printf("dmrtc: RTC clock ready (RCC IRQ)\n"); + } + if (rc == 0 && !(*reg & 2u)) rc = -EIO; + disarm_irq(&guard); /* Disable source before destroying the ISR's semaphore. */ + } + dmosi_semaphore_destroy(event); + return rc; +} +void stm32_rtc_clock_irq(void) +{ + uint32_t cir = RCC(RCC_CIR); + uint32_t pending = cir & (cir >> 8) & owned_flag; + if (pending == 0) return; + RCC(RCC_CIR) = (cir & RCC_IRQ_ENABLES & ~(pending << 8)) | (pending << 16); + dmosi_semaphore_t event = clock_event; + if (event != NULL) dmosi_semaphore_post(event, 1); +} diff --git a/src/port/stm32_common/clock_wait.h b/src/port/stm32_common/clock_wait.h new file mode 100644 index 0000000..ed9ab9d --- /dev/null +++ b/src/port/stm32_common/clock_wait.h @@ -0,0 +1,8 @@ +#ifndef STM32_RTC_CLOCK_WAIT_H +#define STM32_RTC_CLOCK_WAIT_H +#include "dmrtc_types.h" +int stm32_rtc_start_clock(dmrtc_clock_t clock, uint32_t timeout_ms); +void stm32_rtc_clock_irq(void); +/* DBP/INITF have no hardware interrupt; sleeps between samples. */ +int stm32_rtc_wait_register(volatile uint32_t* reg, uint32_t mask, uint32_t value); +#endif diff --git a/src/port/stm32_common/registers.h b/src/port/stm32_common/registers.h new file mode 100644 index 0000000..d83a0aa --- /dev/null +++ b/src/port/stm32_common/registers.h @@ -0,0 +1,46 @@ +#ifndef STM32_RTC_REGISTERS_H +#define STM32_RTC_REGISTERS_H +#include +/* STM32F4 RM0090 and STM32F7 RM0385, RCC/PWR/RTC register maps. */ +#ifndef REG +#define REG(base, off) (*(volatile uint32_t*)((uintptr_t)(base) + (off))) +#endif +#define RCC(off) REG(0x40023800u, off) +#define PWR REG(0x40007000u, 0) +#define RTC(off) REG(0x40002800u, off) +#ifndef NVIC32 +#define NVIC32(off) REG(0xe000e100u, off) +#endif +#ifndef NVIC_PRIORITY +#define NVIC_PRIORITY (*(volatile uint8_t*)(uintptr_t)0xe000e405u) +#endif +#define RCC_IRQ_BIT (1u << 5) +#define RCC_CIR 0x0c +#define RCC_IRQ_ENABLES 0x7f00u +#define REGISTER_TIMEOUT_MS 20u +#define APB1ENR 0x40 +#define BDCR 0x70 +#define CSR 0x74 +#define PWREN (1u << 28) +#define DBP (1u << 8) +#define LSEON 1u +#define LSERDY 2u +#define LSEBYP 4u +#define LSION 1u +#define LSIRDY 2u +#define RTCSEL (3u << 8) +#define RTCEN (1u << 15) +#define TR 0x00 +#define DR 0x04 +#define CR 0x08 +#define ISR 0x0c +#define PRER 0x10 +#define WPR 0x24 +#define BKP(index) RTC(0x50 + 4u * (index)) +#define FMT (1u << 6) +#define BYPSHAD (1u << 5) +#define INIT (1u << 7) +#define INITF (1u << 6) +#define INITS (1u << 4) +#define MARKER 0x52544331u +#endif diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c new file mode 100644 index 0000000..c7616ef --- /dev/null +++ b/src/port/stm32_common/stm32_common.c @@ -0,0 +1,185 @@ +#include "dmod.h" +#include "dmrtc_port.h" +#include "clock_wait.h" +#include "registers.h" +#include "stm32_common.h" +#include +#include + +static bool acquired; +static dmrtc_config_t cfg; +struct backup_guard { bool power, writable; }; + +static void backup_close(struct backup_guard* g) +{ + RTC(WPR) = 0xff; + Dmod_EnterCritical(); + if (!g->writable) PWR &= ~DBP; + if (!g->power) RCC(APB1ENR) &= ~PWREN; + Dmod_ExitCritical(); +} +static int backup_open(struct backup_guard* g) +{ + Dmod_EnterCritical(); + g->power = (RCC(APB1ENR) & PWREN) != 0; + RCC(APB1ENR) |= PWREN; + (void)RCC(APB1ENR); /* Peripheral clock enable read-back delay. */ + g->writable = (PWR & DBP) != 0; + PWR |= DBP; + Dmod_ExitCritical(); + int rc = stm32_rtc_wait_register(&PWR, DBP, DBP); + if (rc != 0) { backup_close(g); return rc; } + RTC(WPR) = 0xca; RTC(WPR) = 0x53; + return 0; +} +static void clear_marker(void) +{ + BKP(cfg.backup_register) = 0; + BKP(cfg.backup_register + 1) = 0; +} +static bool marked(void) +{ + return BKP(cfg.backup_register) == MARKER && + BKP(cfg.backup_register + 1) == (uint32_t)~MARKER; +} +static bool running(void) +{ + uint32_t bdcr = RCC(BDCR); + if (!(bdcr & RTCEN) || ((bdcr & RTCSEL) >> 8) != (uint32_t)cfg.clock || + (RTC(ISR) & INIT)) return false; + return cfg.clock == DMRTC_CLOCK_LSE ? (bdcr & LSERDY) != 0 : (RCC(CSR) & LSIRDY) != 0; +} +static int init_enter(void) +{ + RTC(ISR) |= INIT; + int rc = stm32_rtc_wait_register(&RTC(ISR), INITF, INITF); + if (rc != 0) RTC(ISR) &= ~INIT; + return rc; +} +static int setup_clock(void) +{ + uint32_t select = (uint32_t)cfg.clock << 8; + uint32_t old = RCC(BDCR) & RTCSEL; + /* Changing a selected source requires destructive BDRST: refuse it. */ + if (old != 0 && old != select) return -EBUSY; + if (cfg.clock == DMRTC_CLOCK_LSE && (RCC(BDCR) & LSEBYP)) return -EBUSY; + int rc = stm32_rtc_start_clock(cfg.clock, cfg.timeout_ms); + if (rc != 0) return rc; + RCC(BDCR) = (RCC(BDCR) & ~RTCSEL) | select | RTCEN; + uint32_t prescaler = (127u << 16) | (cfg.clock_hz / 128 - 1); + /* INITS is zero for year 2000: our persistent marker is authoritative. */ + if ((RTC(ISR) & INITS) || marked()) { + if (RTC(PRER) != prescaler || (RTC(CR) & FMT)) return -EBUSY; + } else { + rc = init_enter(); + if (rc != 0) return rc; + clear_marker(); + RTC(CR) &= ~FMT; + RTC(PRER) = prescaler; + RTC(ISR) &= ~INIT; + rc = stm32_rtc_wait_register(&RTC(ISR), INITF, 0); + if (rc != 0) return rc; + } + /* Direct calendar reads avoid shadow-register locking errata. */ + RTC(CR) |= BYPSHAD; + return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _acquire, (const dmrtc_config_t* config)) +{ + if (config == NULL || config->instance != 0) return -EINVAL; + if (acquired) return -EBUSY; + if (config->backup_register >= 19 || config->timeout_ms == 0 || config->timeout_ms > 10000 || + config->clock_hz < 128 || config->clock_hz > 65536 || config->clock_hz % 128 != 0 || + (config->clock != DMRTC_CLOCK_LSE && config->clock != DMRTC_CLOCK_LSI)) return -EINVAL; + cfg = *config; + struct backup_guard guard; + int rc = backup_open(&guard); + if (rc != 0) return rc; + rc = setup_clock(); + backup_close(&guard); + acquired = rc == 0; + return rc; +} +dmod_dmrtc_port_api_declaration(1.0, int, _release, (void)) +{ + if (!acquired) return -ENODEV; + acquired = false; /* Keep oscillator, RTCEN, calendar and backup registers. */ + return 0; +} +static uint32_t bcd(unsigned n) { return (n / 10) * 16 + n % 10; } +static int unbcd(uint32_t n) +{ + if ((n & 15) > 9 || (n >> 4) > 9) return -1; + return (int)((n >> 4) * 10 + (n & 15)); +} +static int decode(uint32_t tr, uint32_t dr, dmrtc_calendar_t* c) +{ + int year = unbcd((dr >> 16) & 0xff); + int month = unbcd((dr >> 8) & 0x1f); + int day = unbcd(dr & 0x3f); + int hour = unbcd((tr >> 16) & 0x3f); + int minute = unbcd((tr >> 8) & 0x7f); + int second = unbcd(tr & 0x7f); + if (year < 0 || month < 1 || month > 12 || day < 1 || day > 31 || + hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 || + (tr & (1u << 22))) return -EIO; + *c = (dmrtc_calendar_t){(uint16_t)(2000 + year), (uint8_t)month, (uint8_t)day, + (uint8_t)hour, (uint8_t)minute, (uint8_t)second, (uint8_t)((dr >> 13) & 7)}; + return c->weekday >= 1 && c->weekday <= 7 ? 0 : -EIO; +} +dmod_dmrtc_port_api_declaration(1.0, int, _get, (dmrtc_calendar_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (!acquired) return -ENODEV; + if (!running()) return -EIO; + if (!marked()) return -ENODATA; + for (unsigned n = 0; n < 8; ++n) { + uint32_t tr = RTC(TR), dr = RTC(DR); + if (tr == RTC(TR) && dr == RTC(DR)) return decode(tr, dr, out); + } + return -EAGAIN; +} +dmod_dmrtc_port_api_declaration(1.0, int, _set, (const dmrtc_calendar_t* c)) +{ + if (c == NULL) return -EINVAL; + if (!acquired) return -ENODEV; + struct backup_guard guard; + int rc = backup_open(&guard); + if (rc != 0) return rc; + clear_marker(); /* A failed or interrupted write cannot leave a valid marker. */ + if (!running()) rc = -EIO; + else rc = init_enter(); + if (rc == 0) { + RTC(TR) = bcd(c->hour) << 16 | bcd(c->minute) << 8 | bcd(c->second); + RTC(DR) = bcd(c->year - 2000) << 16 | (uint32_t)c->weekday << 13 | + bcd(c->month) << 8 | bcd(c->day); + RTC(ISR) &= ~INIT; + rc = stm32_rtc_wait_register(&RTC(ISR), INITF, 0); + if (rc == 0) { + BKP(cfg.backup_register) = MARKER; + BKP(cfg.backup_register + 1) = (uint32_t)~MARKER; + } + } + backup_close(&guard); + return rc; +} +dmod_dmrtc_port_api_declaration(1.0, int, _status, (dmrtc_status_t* out)) +{ + if (out == NULL) return -EINVAL; + if (!acquired) return -ENODEV; + *out = (dmrtc_status_t){running(), false, cfg.clock, cfg.clock_hz}; + out->calendar_set = out->running && marked(); + return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _invalidate, (void)) +{ + if (!acquired) return -ENODEV; + struct backup_guard guard; + int rc = backup_open(&guard); + if (rc != 0) return rc; + clear_marker(); + backup_close(&guard); + return 0; +} +int stm32_rtc_port_deinit(void) { return acquired ? -EBUSY : 0; } diff --git a/src/port/stm32_common/stm32_common.h b/src/port/stm32_common/stm32_common.h new file mode 100644 index 0000000..93d4895 --- /dev/null +++ b/src/port/stm32_common/stm32_common.h @@ -0,0 +1,4 @@ +#ifndef STM32_RTC_COMMON_H +#define STM32_RTC_COMMON_H +int stm32_rtc_port_deinit(void); +#endif diff --git a/src/port/stm32f4/config.cmake b/src/port/stm32f4/config.cmake new file mode 100644 index 0000000..7461975 --- /dev/null +++ b/src/port/stm32f4/config.cmake @@ -0,0 +1 @@ +set(DMOD_TOOLS_NAME "arch/armv7/cortex-m4" CACHE STRING "Name of the tools configuration") diff --git a/src/port/stm32f4/port.c b/src/port/stm32f4/port.c new file mode 100644 index 0000000..5a7f0b7 --- /dev/null +++ b/src/port/stm32f4/port.c @@ -0,0 +1,9 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmrtc_port.h" +#include "../stm32_common/stm32_common.h" +#include "../stm32_common/clock_wait.h" +int dmod_init(const Dmod_Config_t* config) { (void)config; return 0; } +int dmod_deinit(void) { return stm32_rtc_port_deinit(); } + +DMOD_IRQ_HANDLER(5) { stm32_rtc_clock_irq(); } diff --git a/src/port/stm32f7/config.cmake b/src/port/stm32f7/config.cmake new file mode 100644 index 0000000..ed2ab0f --- /dev/null +++ b/src/port/stm32f7/config.cmake @@ -0,0 +1 @@ +set(DMOD_TOOLS_NAME "arch/armv7/cortex-m7" CACHE STRING "Name of the tools configuration") diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c new file mode 100644 index 0000000..5a7f0b7 --- /dev/null +++ b/src/port/stm32f7/port.c @@ -0,0 +1,9 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmrtc_port.h" +#include "../stm32_common/stm32_common.h" +#include "../stm32_common/clock_wait.h" +int dmod_init(const Dmod_Config_t* config) { (void)config; return 0; } +int dmod_deinit(void) { return stm32_rtc_port_deinit(); } + +DMOD_IRQ_HANDLER(5) { stm32_rtc_clock_irq(); } diff --git a/src/port/x86_64/config.cmake b/src/port/x86_64/config.cmake new file mode 100644 index 0000000..5461f9f --- /dev/null +++ b/src/port/x86_64/config.cmake @@ -0,0 +1 @@ +set(DMOD_TOOLS_NAME "arch/x86_64" CACHE STRING "Name of the tools configuration") diff --git a/src/port/x86_64/port.c b/src/port/x86_64/port.c new file mode 100644 index 0000000..035effd --- /dev/null +++ b/src/port/x86_64/port.c @@ -0,0 +1,52 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmrtc_port.h" +#include "dmrtc_mock.h" +#include +#include +static bool acquired, valid; +static dmrtc_config_t cfg; +static dmrtc_calendar_t value; +static int failure; +dmod_dmrtc_port_api_declaration(1.0, int, _acquire, (const dmrtc_config_t* config)) +{ + if (acquired) return -EBUSY; + if (config == NULL || config->instance != 0) return -EINVAL; + if (cfg.clock != 0 && (cfg.clock != config->clock || cfg.clock_hz != config->clock_hz)) + return -EBUSY; + cfg = *config; acquired = true; failure = 0; + return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _release, (void)) +{ + if (!acquired) return -ENODEV; + acquired = false; return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _get, (dmrtc_calendar_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (!acquired) return -ENODEV; + if (!valid) return -ENODATA; + *out = value; return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _set, (const dmrtc_calendar_t* c)) +{ + if (!acquired) return -ENODEV; + valid = false; + if (failure != 0) { int rc = failure; failure = 0; return rc; } + value = *c; valid = true; return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _status, (dmrtc_status_t* out)) +{ + if (!acquired) return -ENODEV; + *out = (dmrtc_status_t){true, valid, cfg.clock, cfg.clock_hz}; return 0; +} +dmod_dmrtc_port_api_declaration(1.0, int, _invalidate, (void)) +{ + if (!acquired) return -ENODEV; + valid = false; return 0; +} +dmod_dmrtc_port_api_declaration(1.0, void, _mock_fail_set, (int error)) { failure = error; } +int dmod_init(const Dmod_Config_t* config) { (void)config; return 0; } +int dmod_deinit(void) { return acquired ? -EBUSY : 0; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5750dc4 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,71 @@ +set(DMOD_MODULE_NAME test_dmrtc) +set(DMOD_MODULE_VERSION ${PROJECT_VERSION}) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 2048) +set(DMOD_PRIORITY 0) + +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/test_dmrtc.dmr) + +dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + dmrtc_test.c +) + +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) + +target_link_libraries(${DMOD_MODULE_NAME} dmrtc_if) +if(DMOD_CPU_FAMILY STREQUAL "x86_64") + target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE DMRTC_MOCK) + target_link_libraries(${DMOD_MODULE_NAME} dmrtc_port_if) +endif() + +dmod_link_modules(${DMOD_MODULE_NAME} dmosi dmdrvi dmini) + +# Register the test module with ctest - this replays the same two steps CI +# runs (see .github/templates/ci.yml.template): install the test module's +# dependencies with dmf-get, then execute it through dmod_loader. The test's +# exit code equals its number of failed steps (see dmod_add_test). +find_program(DMF_GET_EXECUTABLE dmf-get HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) +find_program(DMOD_LOADER_EXECUTABLE dmod_loader HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) + +if(DMF_GET_EXECUTABLE AND DMOD_LOADER_EXECUTABLE) + add_test( + NAME ${DMOD_MODULE_NAME} + COMMAND ${CMAKE_COMMAND} -E env DMOD_DMF_DIR=${DMOD_DMF_DIR} + bash -c "${DMF_GET_EXECUTABLE} install -d ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}-local.dmd -y && ${DMOD_LOADER_EXECUTABLE} ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}.dmf" + ) +else() + message(WARNING "dmf-get/dmod_loader not found - 'ctest' will not run ${DMOD_MODULE_NAME}. Build the tools first with -DDMOD_BUILD_TOOLS=ON") +endif() + +# Exercise the actual STM32 wait implementation with MMIO and OS test doubles. +# Invoke the host compiler directly to avoid the DMOD module linker script. +if(DMOD_CPU_FAMILY STREQUAL "x86_64") + find_program(HOST_CC NAMES cc gcc REQUIRED) + set(WAIT_TEST ${CMAKE_CURRENT_SOURCE_DIR}/stm32_wait) + set(WAIT_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/../src/port/stm32_common) + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_stm32_wait + COMMAND ${HOST_CC} -D_POSIX_C_SOURCE=200809L -std=c11 -Wall -Wextra -Werror -pthread + -include ${WAIT_TEST}/mmio.h -I${WAIT_TEST}/stubs + -I${CMAKE_CURRENT_SOURCE_DIR}/../include -I${WAIT_SOURCE} + ${WAIT_TEST}/wait_test.c ${WAIT_SOURCE}/clock_wait.c + -o ${CMAKE_CURRENT_BINARY_DIR}/test_stm32_wait + DEPENDS ${WAIT_TEST}/wait_test.c ${WAIT_SOURCE}/clock_wait.c + ${WAIT_SOURCE}/clock_wait.h ${WAIT_SOURCE}/registers.h + ${WAIT_TEST}/mmio.h ${WAIT_TEST}/stubs/dmod.h ${WAIT_TEST}/stubs/dmosi.h) + add_custom_target(stm32_wait_test_build ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_stm32_wait) + add_test(NAME stm32_rtc_wait COMMAND ${CMAKE_CURRENT_BINARY_DIR}/test_stm32_wait) + set_tests_properties(stm32_rtc_wait PROPERTIES TIMEOUT 10) + set(RTC_CLIENT_TEST ${CMAKE_CURRENT_SOURCE_DIR}/rtc_client) + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_client + COMMAND ${HOST_CC} -std=c11 -Wall -Wextra -Werror + -I${RTC_CLIENT_TEST}/stubs -I${DMOD_DMF_DIR}/inc/dmdrvi/include + ${RTC_CLIENT_TEST}/client_test.c -o ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_client + DEPENDS ${RTC_CLIENT_TEST}/client_test.c ${RTC_CLIENT_TEST}/stubs/dmod.h + ${CMAKE_CURRENT_SOURCE_DIR}/../apps/rtc/rtc.c + ${DMOD_DMF_DIR}/inc/dmdrvi/include/dmdrvi_ioctl.h) + add_custom_target(rtc_client_test_build ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_client) + add_test(NAME rtc_generic_client COMMAND ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_client) + set_tests_properties(rtc_generic_client PROPERTIES TIMEOUT 10) +endif() diff --git a/tests/dmrtc_test.c b/tests/dmrtc_test.c new file mode 100644 index 0000000..4446283 --- /dev/null +++ b/tests/dmrtc_test.c @@ -0,0 +1,292 @@ +#define DMOD_ENABLE_REGISTRATION ON +#define ENABLE_DIF_REGISTRATIONS +#include "dmod_test.h" +#include "dmrtc.h" +#include "dmdrvi.h" +#include +#include +#ifdef DMRTC_MOCK +#include "dmrtc_mock.h" +#include "dmosi.h" +#endif +static dmrtc_t ctx, owned; +static dmrtc_calendar_t saved; +static bool was_set; +static uint64_t started; +void dmod_test_setup(void) +{ + owned = dmrtc_create(NULL); + ctx = owned != NULL ? owned : dmrtc_default(); + DMOD_TEST_EXPECT_NOT_NULL(ctx); + was_set = dmrtc_get(ctx, &saved) == 0; + started = Dmod_GetUptime(); +} +/* Restore the previous civil time, accounting for elapsed whole seconds. + * Bounded: hardware tests take seconds, not years. */ +static void restore(void) +{ + if (!was_set) { DMOD_TEST_EXPECT_EQ(dmrtc_invalidate(ctx), 0); return; } + uint64_t elapsed = (Dmod_GetUptime() - started) / 1000; + while (elapsed-- != 0) { + if (++saved.second < 60) continue; + saved.second = 0; + if (++saved.minute < 60) continue; + saved.minute = 0; + if (++saved.hour < 24) continue; + saved.hour = 0; ++saved.day; + if (dmrtc_normalize(&saved) == 0) continue; + saved.day = 1; + if (++saved.month > 12) { saved.month = 1; ++saved.year; } + } + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &saved), 0); +} +void dmod_test_teardown(void) +{ + restore(); + if (owned != NULL) DMOD_TEST_EXPECT_EQ(dmrtc_destroy(owned), 0); + owned = NULL; ctx = NULL; +} +#ifdef DMRTC_MOCK +static dmod_dmdrvi_ioctl_t device_ioctl; +static dmod_dmdrvi_close_t device_close; +#endif +static void* open_device(const char* mode) +{ +#ifdef DMRTC_MOCK + Dmod_Context_t* module = Dmod_LoadModuleByName("dmrtc"); + if (module == NULL) return NULL; + dmod_dmdrvi_open_t open = (dmod_dmdrvi_open_t)Dmod_GetDifFunction(module, dmod_dmdrvi_open_sig); + device_ioctl = (dmod_dmdrvi_ioctl_t)Dmod_GetDifFunction(module, dmod_dmdrvi_ioctl_sig); + device_close = (dmod_dmdrvi_close_t)Dmod_GetDifFunction(module, dmod_dmdrvi_close_sig); + if (open == NULL || device_ioctl == NULL || device_close == NULL) return NULL; + dmdrvi_dev_num_t num = {0}; + int flags = strcmp(mode,"rb") == 0 ? DMDRVI_O_RDONLY : + strcmp(mode,"wb") == 0 ? DMDRVI_O_WRONLY : DMDRVI_O_RDWR; + return open((dmdrvi_context_t)ctx, flags, &num); +#else + return Dmod_FileOpen("/dev/rtc", mode); +#endif +} +static int ioctl_device(void* file, int command, void* arg) +{ +#ifdef DMRTC_MOCK + return device_ioctl((dmdrvi_context_t)ctx, file, command, arg); +#else + return Dmod_Ioctl(file, command, arg); +#endif +} +static void close_device(void* file) +{ +#ifdef DMRTC_MOCK + device_close((dmdrvi_context_t)ctx, file); +#else + Dmod_FileClose(file); +#endif +} +DMOD_TEST_STEP(standard_rtc_capabilities_and_unset_state) +{ + void* file = open_device("r+b"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file == NULL) return; + dmdrvi_rtc_info_t info; dmdrvi_rtc_status_t status; dmdrvi_rtc_time_t time; + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_INFO, &info), 0); + DMOD_TEST_EXPECT_EQ(info.min_year, 2000); DMOD_TEST_EXPECT_EQ(info.max_year, 2099); + DMOD_TEST_EXPECT_EQ(info.resolution_ns, 1000000000u); + DMOD_TEST_EXPECT_EQ(info.capabilities, DMDRVI_RTC_CAP_SET_TIME | DMDRVI_RTC_CAP_INVALIDATE); + DMOD_TEST_EXPECT_EQ(info.reserved, 0); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_INVALIDATE, NULL), 0); + memset(&time, 0xa5, sizeof(time)); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_TIME, &time), -ENODATA); + const dmdrvi_rtc_time_t zero = {0}; + DMOD_TEST_EXPECT_EQ(memcmp(&time, &zero, sizeof(time)), 0); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_STATUS, &status), 0); + DMOD_TEST_EXPECT_EQ(status.flags, DMDRVI_RTC_STATUS_RUNNING); + DMOD_TEST_EXPECT_EQ(status.reserved, 0); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_TIME, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_INFO, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_STATUS, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_INVALIDATE, &time), -EINVAL); + close_device(file); +} +DMOD_TEST_STEP(standard_rtc_range_and_precision_are_enforced) +{ + void* file = open_device("r+b"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file == NULL) return; + dmdrvi_rtc_time_t time = {.year=2000, .month=2, .day=29, .hour=12, .weekday=99}, out; + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), 0); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_TIME, &out), 0); + DMOD_TEST_EXPECT_EQ(out.weekday, 2); DMOD_TEST_EXPECT_EQ(out.nanosecond, 0); + DMOD_TEST_EXPECT_EQ(out.reserved, 0); + time.year = 1999; DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -ERANGE); + time.year = 2100; DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -ERANGE); + time.year = -1; DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -EINVAL); + time.year = 2000; time.day = 30; + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -EINVAL); + time.day = 29; time.nanosecond = 1; + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -ERANGE); + time.nanosecond = 1000000000; + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -EINVAL); + time.nanosecond = 0; time.reserved = 1; + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_SET_TIME, &time), -EINVAL); + DMOD_TEST_EXPECT_EQ(ioctl_device(file, DMDRVI_IOCTL_RTC_GET_TIME, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year, 2000); DMOD_TEST_EXPECT_EQ(out.day, 29); + close_device(file); +} +DMOD_TEST_STEP(private_rtc_command_numbers_are_not_supported) +{ + void* file = open_device("r+b"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file == NULL) return; + for (int command = 0x52540001; command <= 0x52540004; ++command) + DMOD_TEST_EXPECT_EQ(ioctl_device(file, command, NULL), -ENOTTY); + close_device(file); +} +DMOD_TEST_STEP(calendar_validation) +{ + dmrtc_calendar_t c = {2000, 1, 1, 0, 0, 0, 0}; + DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), 0); DMOD_TEST_EXPECT_EQ(c.weekday, 6); + c = (dmrtc_calendar_t){2024, 2, 29, 23, 59, 59, 0}; + DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), 0); DMOD_TEST_EXPECT_EQ(c.weekday, 4); + c.year = 2023; DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), -EINVAL); + c.year = 2100; DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), -EINVAL); + c = (dmrtc_calendar_t){2026, 4, 31, 0, 0, 0, 0}; + DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), -EINVAL); + c.day = 30; c.hour = 24; DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), -EINVAL); + c.hour = 0; c.second = 60; DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), -EINVAL); + c.second = 0; c.month = 0; DMOD_TEST_EXPECT_EQ(dmrtc_normalize(&c), -EINVAL); + DMOD_TEST_EXPECT_EQ(dmrtc_normalize(NULL), -EINVAL); +} +DMOD_TEST_STEP(unset_calendar) +{ + dmrtc_calendar_t c; dmrtc_status_t status; + DMOD_TEST_EXPECT_EQ(dmrtc_invalidate(ctx), 0); + memset(&c, 0xa5, sizeof(c)); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &c), -ENODATA); + DMOD_TEST_EXPECT_EQ(c.year, 0); + DMOD_TEST_EXPECT_EQ(dmrtc_status(ctx, &status), 0); + DMOD_TEST_EXPECT_TRUE(status.running); DMOD_TEST_EXPECT_FALSE(status.calendar_set); +} +DMOD_TEST_STEP(set_read_and_reject_invalid) +{ + dmrtc_calendar_t c = {2024, 2, 29, 12, 34, 56, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year, 2024); DMOD_TEST_EXPECT_EQ(out.month, 2); + DMOD_TEST_EXPECT_EQ(out.day, 29); DMOD_TEST_EXPECT_EQ(out.hour, 12); + DMOD_TEST_EXPECT_EQ(out.minute, 34); DMOD_TEST_EXPECT_EQ(out.weekday, 4); + c.day = 30; DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), -EINVAL); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); DMOD_TEST_EXPECT_EQ(out.day, 29); + DMOD_TEST_EXPECT_EQ(dmrtc_get(NULL, &out), -EINVAL); DMOD_TEST_EXPECT_EQ(out.year, 0); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, NULL), -EINVAL); +} +DMOD_TEST_STEP(year_2000_is_not_an_uninitialized_calendar) +{ + dmrtc_calendar_t c = {2000, 2, 29, 12, 0, 0, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year, 2000); DMOD_TEST_EXPECT_EQ(out.day, 29); + DMOD_TEST_EXPECT_EQ(out.weekday, 2); +} +DMOD_TEST_STEP(single_owner_and_bad_configuration) +{ + DMOD_TEST_EXPECT_NULL(dmrtc_create(NULL)); + dmrtc_config_t bad = DMRTC_CONFIG_DEFAULT; + bad.backup_register = 19; DMOD_TEST_EXPECT_NULL(dmrtc_create(&bad)); + bad.backup_register = 18; bad.instance = 1; DMOD_TEST_EXPECT_NULL(dmrtc_create(&bad)); + bad.instance = 0; bad.timeout_ms = 0; DMOD_TEST_EXPECT_NULL(dmrtc_create(&bad)); + bad.timeout_ms = 5000; bad.clock_hz = 0; DMOD_TEST_EXPECT_NULL(dmrtc_create(&bad)); + DMOD_TEST_EXPECT_EQ(dmrtc_destroy(NULL), 0); +} +DMOD_TEST_STEP(vfs_ioctl_and_access_permissions) +{ + void* reader = open_device("rb"); + void* writer = open_device("wb"); + DMOD_TEST_EXPECT_NOT_NULL(reader); DMOD_TEST_EXPECT_NOT_NULL(writer); + dmdrvi_rtc_status_t status; dmdrvi_rtc_time_t c = {.year=2026, .month=10, .day=15, .hour=1, .minute=2, .second=3}; + if (reader != NULL) { + DMOD_TEST_EXPECT_EQ(ioctl_device(reader, DMDRVI_IOCTL_RTC_GET_STATUS, &status), 0); + DMOD_TEST_EXPECT_EQ(ioctl_device(reader, DMDRVI_IOCTL_RTC_SET_TIME, &c), -EACCES); + DMOD_TEST_EXPECT_EQ(ioctl_device(reader, DMDRVI_IOCTL_RTC_INVALIDATE, NULL), -EACCES); + DMOD_TEST_EXPECT_EQ(ioctl_device(reader, 12345, NULL), -ENOTTY); + DMOD_TEST_EXPECT_EQ(dmrtc_destroy(ctx), -EBUSY); + } + if (writer != NULL) { + DMOD_TEST_EXPECT_EQ(ioctl_device(writer, DMDRVI_IOCTL_RTC_SET_TIME, &c), 0); + DMOD_TEST_EXPECT_EQ(ioctl_device(writer, DMDRVI_IOCTL_RTC_GET_TIME, &c), -EACCES); + } + if (reader != NULL) { + DMOD_TEST_EXPECT_EQ(ioctl_device(reader, DMDRVI_IOCTL_RTC_GET_TIME, &c), 0); + DMOD_TEST_EXPECT_EQ(c.year, 2026); + close_device(reader); + } + if (writer != NULL) close_device(writer); +} +#ifndef DMRTC_MOCK +DMOD_TEST_STEP(leap_day_rollover) +{ + dmrtc_calendar_t c = {2024, 2, 28, 23, 59, 59, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + Dmod_ThreadSleep(1300); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year, 2024); DMOD_TEST_EXPECT_EQ(out.month, 2); + DMOD_TEST_EXPECT_EQ(out.day, 29); DMOD_TEST_EXPECT_EQ(out.hour, 0); + DMOD_TEST_EXPECT_EQ(out.minute, 0); DMOD_TEST_EXPECT_EQ(out.weekday, 4); +} +DMOD_TEST_STEP(year_rollover) +{ + dmrtc_calendar_t c = {2023, 12, 31, 23, 59, 59, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + Dmod_ThreadSleep(1300); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year, 2024); DMOD_TEST_EXPECT_EQ(out.month, 1); + DMOD_TEST_EXPECT_EQ(out.day, 1); DMOD_TEST_EXPECT_EQ(out.weekday, 1); +} +#else +static void concurrent_writer(void* arg) +{ + int* result = arg; + dmrtc_calendar_t c = {2024, 2, 29, 12, 0, 0, 0}; + *result = 0; + for (unsigned i = 0; i < 500 && *result == 0; ++i) *result = dmrtc_set(ctx, &c); +} +DMOD_TEST_STEP(concurrent_calendar_access) +{ + dmrtc_calendar_t c = {2026, 10, 15, 0, 0, 0, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + int result = -1; + dmosi_thread_t writer = dmosi_thread_create(concurrent_writer, &result, 1, 16384, "rtc", NULL); + DMOD_TEST_EXPECT_NOT_NULL(writer); + for (unsigned i = 0; i < 500; ++i) { + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); + bool first = out.year == 2026 && out.month == 10 && out.day == 15 && out.hour == 0; + bool second = out.year == 2024 && out.month == 2 && out.day == 29 && out.hour == 12; + DMOD_TEST_EXPECT_TRUE(first || second); + } + if (writer != NULL) { + DMOD_TEST_EXPECT_EQ(dmosi_thread_join(writer), 0); + dmosi_thread_destroy(writer); + } + DMOD_TEST_EXPECT_EQ(result, 0); +} +DMOD_TEST_STEP(failed_set_invalidates_calendar) +{ + dmrtc_calendar_t c = {2026, 10, 15, 0, 0, 0, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + dmrtc_port_mock_fail_set(-ETIMEDOUT); + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), -ETIMEDOUT); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), -ENODATA); + DMOD_TEST_EXPECT_EQ(out.year, 0); +} +DMOD_TEST_STEP(calendar_survives_recreate_and_source_conflict) +{ + dmrtc_calendar_t c = {2026, 10, 15, 0, 0, 0, 0}, out; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + DMOD_TEST_EXPECT_EQ(dmrtc_destroy(owned), 0); owned = NULL; ctx = NULL; + dmrtc_config_t conflict = DMRTC_CONFIG_DEFAULT; conflict.clock = DMRTC_CLOCK_LSI; + DMOD_TEST_EXPECT_NULL(dmrtc_create(&conflict)); + owned = dmrtc_create(NULL); ctx = owned; + DMOD_TEST_EXPECT_NOT_NULL(ctx); + DMOD_TEST_EXPECT_EQ(dmrtc_get(ctx, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year, 2026); DMOD_TEST_EXPECT_EQ(out.day, 15); +} +#endif diff --git a/tests/rtc_client/client_test.c b/tests/rtc_client/client_test.c new file mode 100644 index 0000000..c1d0eeb --- /dev/null +++ b/tests/rtc_client/client_test.c @@ -0,0 +1,76 @@ +/* Compile the actual client against an independent VFS RTC implementation. + * This test intentionally has no dmrtc headers, contexts or exported API. */ +#include +#include +#include +#include +#define main rtc_client_main +#include "../../apps/rtc/rtc.c" +#undef main + +static char output[2048], opened_path[64], opened_mode[8]; +static unsigned closes, sets; +static int device; +static uint32_t capabilities; +static dmdrvi_rtc_time_t calendar; +int Dmod_Printf(const char* format, ...) +{ + va_list args; va_start(args, format); + int n = vsnprintf(output + strlen(output), sizeof(output)-strlen(output), format, args); + va_end(args); return n; +} +void* Dmod_FileOpen(const char* path, const char* mode) +{ + snprintf(opened_path, sizeof(opened_path), "%s", path); + snprintf(opened_mode, sizeof(opened_mode), "%s", mode); + return &device; +} +int Dmod_FileClose(void* file) { assert(file == &device); ++closes; return 0; } +int Dmod_Ioctl(void* file, int command, void* arg) +{ + assert(file == &device); + switch (command) { + case DMDRVI_IOCTL_RTC_GET_INFO: + *(dmdrvi_rtc_info_t*)arg = (dmdrvi_rtc_info_t){.resolution_ns=1, + .capabilities=capabilities, .min_year=1900, .max_year=2199}; return 0; + case DMDRVI_IOCTL_RTC_GET_STATUS: + *(dmdrvi_rtc_status_t*)arg = (dmdrvi_rtc_status_t){ + .flags=DMDRVI_RTC_STATUS_RUNNING | DMDRVI_RTC_STATUS_TIME_SET}; return 0; + case DMDRVI_IOCTL_RTC_GET_TIME: *(dmdrvi_rtc_time_t*)arg = calendar; return 0; + case DMDRVI_IOCTL_RTC_SET_TIME: + if (!(capabilities & DMDRVI_RTC_CAP_SET_TIME)) return -ENOTSUP; + calendar = *(dmdrvi_rtc_time_t*)arg; ++sets; return 0; + default: return -ENOTTY; + } +} +static int run(unsigned count, char** args) +{ + output[0]=opened_path[0]=opened_mode[0]=0; closes=0; + int result=rtc_client_main((int)count,args); + assert(closes==1); return result; +} +int main(void) +{ + calendar=(dmdrvi_rtc_time_t){.year=1999,.month=12,.day=31,.hour=23, + .minute=59,.second=59,.weekday=5,.nanosecond=123456789}; + char* get[]={"rtc","-d","/dev/other-clock","get"}; + assert(run(4,get)==0 && strcmp(opened_path,"/dev/other-clock")==0); + assert(strcmp(opened_mode,"rb")==0 && strstr(output,"1999-12-31T23:59:59.123456789")); + puts("PASS generic read-only RTC outside STM32 year/precision range"); + char* info[]={"rtc","info"}; assert(run(2,info)==0); + assert(strcmp(opened_path,"/dev/rtc")==0 && strstr(output,"1900..2199") && strstr(output,"writable=0")); + char* status[]={"rtc","status"}; assert(run(2,status)==0 && strstr(output,"time_lost=unknown")); + puts("PASS device info and unknown loss-detection capability"); + char* set[]={"rtc","-d","/dev/other-clock","set","1901-01-01T12:34:56.25"}; + assert(run(5,set)==1 && sets==0); + capabilities=DMDRVI_RTC_CAP_SET_TIME | DMDRVI_RTC_CAP_LOSS_DETECTION; + assert(run(5,set)==0 && sets==1 && strcmp(opened_mode,"r+b")==0); + assert(calendar.year==1901 && calendar.nanosecond==250000000 && calendar.reserved==0); + assert(run(2,status)==0 && strstr(output,"time_lost=0")); + puts("PASS optional SET_TIME and fractional-second input"); + set[4]="1901-01-01T12:34:56.1234567890"; + assert(run(5,set)==1 && sets==1); + set[4]="1901-01-01T12:34:56.x"; assert(run(5,set)==1 && sets==1); + puts("PASS invalid fractions rejected before issuing SET_TIME"); + return 0; +} diff --git a/tests/rtc_client/stubs/dmod.h b/tests/rtc_client/stubs/dmod.h new file mode 100644 index 0000000..286f56f --- /dev/null +++ b/tests/rtc_client/stubs/dmod.h @@ -0,0 +1,9 @@ +#ifndef RTC_CLIENT_DMOD_H +#define RTC_CLIENT_DMOD_H +#include +#include +int Dmod_Printf(const char* format, ...); +void* Dmod_FileOpen(const char* path, const char* mode); +int Dmod_FileClose(void* file); +int Dmod_Ioctl(void* file, int command, void* arg); +#endif diff --git a/tests/stm32_wait/mmio.h b/tests/stm32_wait/mmio.h new file mode 100644 index 0000000..c764202 --- /dev/null +++ b/tests/stm32_wait/mmio.h @@ -0,0 +1,10 @@ +#ifndef TEST_MMIO_H +#define TEST_MMIO_H +#include +extern volatile uint32_t test_rcc[64], test_nvic[64], test_pwr, test_rtc[64]; +extern volatile uint8_t test_priority; +volatile uint32_t* test_register(uintptr_t base, unsigned offset); +#define REG(base, off) (*test_register(base, off)) +#define NVIC32(off) test_nvic[(off)/4] +#define NVIC_PRIORITY test_priority +#endif diff --git a/tests/stm32_wait/stubs/dmod.h b/tests/stm32_wait/stubs/dmod.h new file mode 100644 index 0000000..b086af9 --- /dev/null +++ b/tests/stm32_wait/stubs/dmod.h @@ -0,0 +1,10 @@ +#ifndef TEST_DMOD_H +#define TEST_DMOD_H +#include +#include +void Dmod_EnterCritical(void); +void Dmod_ExitCritical(void); +uint64_t Dmod_GetUptime(void); +bool Dmod_IsInsideInterrupt(void); +#define Dmod_Printf(...) ((void)0) +#endif diff --git a/tests/stm32_wait/stubs/dmosi.h b/tests/stm32_wait/stubs/dmosi.h new file mode 100644 index 0000000..a48fca1 --- /dev/null +++ b/tests/stm32_wait/stubs/dmosi.h @@ -0,0 +1,13 @@ +#ifndef TEST_DMOSI_H +#define TEST_DMOSI_H +#include +#include +typedef struct test_semaphore* dmosi_semaphore_t; +dmosi_semaphore_t dmosi_semaphore_create(uint32_t, uint32_t); +void dmosi_semaphore_destroy(dmosi_semaphore_t); +int dmosi_semaphore_wait(dmosi_semaphore_t, uint32_t, int32_t); +int dmosi_semaphore_post(dmosi_semaphore_t, uint32_t); +uint32_t dmosi_get_min_interrupt_priority(void); +void dmosi_thread_sleep(uint32_t); +bool dmosi_is_started(void); +#endif diff --git a/tests/stm32_wait/wait_test.c b/tests/stm32_wait/wait_test.c new file mode 100644 index 0000000..4bc1a2a --- /dev/null +++ b/tests/stm32_wait/wait_test.c @@ -0,0 +1,147 @@ +#include "dmod.h" +#include "dmosi.h" +#include "clock_wait.h" +#include "registers.h" +#include +#include +#include +#include +#include +#include +#include +#include + +volatile uint32_t test_rcc[64], test_nvic[64], test_pwr, test_rtc[64]; +volatile uint8_t test_priority; +struct test_semaphore { sem_t sem; }; +static pthread_mutex_t critical = PTHREAD_MUTEX_INITIALIZER; +static unsigned allocations, posts, sleeps; +static bool started = true, in_interrupt, fail_allocation, early; +volatile uint32_t* test_register(uintptr_t base, unsigned off) +{ + if (base == 0x40023800u) return &test_rcc[off/4]; + if (base == 0x40007000u) return &test_pwr; + assert(base == 0x40002800u); return &test_rtc[off/4]; +} +static uint64_t nanoseconds(clockid_t id) +{ + struct timespec t; assert(clock_gettime(id, &t) == 0); + return (uint64_t)t.tv_sec * 1000000000u + t.tv_nsec; +} +uint64_t Dmod_GetUptime(void) { return nanoseconds(CLOCK_MONOTONIC) / 1000000; } +bool Dmod_IsInsideInterrupt(void) { return in_interrupt; } +bool dmosi_is_started(void) { return started; } +void Dmod_EnterCritical(void) { assert(pthread_mutex_lock(&critical) == 0); } +void Dmod_ExitCritical(void) { assert(pthread_mutex_unlock(&critical) == 0); } +uint32_t dmosi_get_min_interrupt_priority(void) { return 0x50; } +void dmosi_thread_sleep(uint32_t ms) +{ + ++sleeps; + struct timespec t = {ms/1000, (ms%1000)*1000000}; nanosleep(&t, NULL); +} +dmosi_semaphore_t dmosi_semaphore_create(uint32_t initial, uint32_t maximum) +{ + assert(initial == 0 && maximum == 1); + if (fail_allocation) return NULL; + struct test_semaphore* s = malloc(sizeof(*s)); assert(s != NULL); + assert(sem_init(&s->sem, 0, initial) == 0); ++allocations; return s; +} +void dmosi_semaphore_destroy(dmosi_semaphore_t s) +{ + assert(sem_destroy(&s->sem) == 0); free(s); +} +static void deliver_irq(unsigned flag) +{ + Dmod_EnterCritical(); + if (flag == 2) RCC(BDCR) |= LSERDY; + else RCC(CSR) |= LSIRDY; + RCC(RCC_CIR) = (RCC(RCC_CIR) & RCC_IRQ_ENABLES) | flag; + stm32_rtc_clock_irq(); + Dmod_ExitCritical(); +} +int dmosi_semaphore_post(dmosi_semaphore_t s, uint32_t count) +{ + assert(count == 1); ++posts; return sem_post(&s->sem); +} +int dmosi_semaphore_wait(dmosi_semaphore_t s, uint32_t count, int32_t ms) +{ + assert(count == 1); + if (early) { early = false; deliver_irq(2); } /* Event before blocking. */ + uint64_t deadline = nanoseconds(CLOCK_REALTIME) + (uint64_t)ms * 1000000; + struct timespec t = {(time_t)(deadline/1000000000), (long)(deadline%1000000000)}; + int rc; + do { rc = sem_timedwait(&s->sem, &t); } while (rc != 0 && errno == EINTR); + return rc == 0 ? 0 : -errno; +} +static void* ready_worker(void* arg) +{ + struct timespec t = {0, 10000000}; nanosleep(&t, NULL); + /* An unrelated ready source must not wake our waiter. */ + Dmod_EnterCritical(); + RCC(RCC_CIR) = (RCC(RCC_CIR) & RCC_IRQ_ENABLES) | 8; + stm32_rtc_clock_irq(); + assert(posts == 0); + Dmod_ExitCritical(); + deliver_irq((unsigned)(uintptr_t)arg); return NULL; +} +static void reset(void) +{ + memset((void*)test_rcc, 0, sizeof(test_rcc)); + memset((void*)test_nvic, 0, sizeof(test_nvic)); + test_priority = 0x20; posts = sleeps = allocations = 0; + started = true; in_interrupt = fail_allocation = early = false; +} +int main(void) +{ + reset(); RCC(BDCR) = 3; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == 0 && allocations == 0); + puts("PASS already-ready fast path"); + for (unsigned flag = 1; flag <= 2; ++flag) { + reset(); pthread_t worker; + assert(pthread_create(&worker, NULL, ready_worker, (void*)(uintptr_t)flag) == 0); + assert(stm32_rtc_start_clock(flag == 2 ? DMRTC_CLOCK_LSE : DMRTC_CLOCK_LSI, 200) == 0); + assert(pthread_join(worker, NULL) == 0); + assert(posts == 1 && !(RCC(RCC_CIR) & (flag << 8))); + puts(flag == 2 ? "PASS LSE IRQ wake and unrelated IRQ ignored" : "PASS LSI IRQ wake"); + } + reset(); early = true; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == 0 && posts == 1); + puts("PASS event before blocked wait"); + reset(); uint64_t wall = Dmod_GetUptime(), cpu = nanoseconds(CLOCK_THREAD_CPUTIME_ID); + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == -ETIMEDOUT); + cpu = nanoseconds(CLOCK_THREAD_CPUTIME_ID) - cpu; wall = Dmod_GetUptime() - wall; + assert(wall >= 70 && cpu < wall * 1000000 / 4); + assert(!(RCC(RCC_CIR) & (2u << 8)) && test_nvic[0x80/4] == RCC_IRQ_BIT); + deliver_irq(2); assert(posts == 0); /* No post to a destroyed semaphore. */ + printf("PASS timeout sleeps: wall=%llu ms CPU=%llu us; late IRQ safe\n", + (unsigned long long)wall, (unsigned long long)(cpu/1000)); + reset(); RCC(RCC_CIR) = 8u << 8; test_nvic[0] = RCC_IRQ_BIT; test_priority = 0xa0; + early = true; assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == 0); + assert((RCC(RCC_CIR) & RCC_IRQ_ENABLES) == (8u << 8) && test_priority == 0xa0); + assert(test_nvic[0x80/4] == 0 && !(RCC(RCC_CIR) & (8u << 16))); + puts("PASS shared RCC source and NVIC state preserved"); + reset(); RCC(RCC_CIR) = 2u << 8; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == -EBUSY); + assert(RCC(RCC_CIR) == (2u << 8)); + reset(); test_nvic[0] = RCC_IRQ_BIT; test_priority = 0x10; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == -EBUSY && test_priority == 0x10); + puts("PASS existing owner and unsafe IRQ priority rejected"); + reset(); fail_allocation = true; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == -ENOMEM && RCC(BDCR) == 0); + puts("PASS allocation failure leaves oscillator untouched"); + reset(); volatile uint32_t reg = 0; + cpu = nanoseconds(CLOCK_THREAD_CPUTIME_ID); wall = Dmod_GetUptime(); + assert(stm32_rtc_wait_register(®, 1, 1) == -ETIMEDOUT); + wall = Dmod_GetUptime() - wall; cpu = nanoseconds(CLOCK_THREAD_CPUTIME_ID) - cpu; + assert(sleeps > 0 && wall >= REGISTER_TIMEOUT_MS && cpu < wall * 1000000 / 4); + printf("PASS register timeout sleeps: wall=%llu ms CPU=%llu us\n", + (unsigned long long)wall, (unsigned long long)(cpu/1000)); + reset(); started = false; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == -ENOTSUP); + assert(stm32_rtc_wait_register(®, 1, 1) == -ENOTSUP); + reset(); in_interrupt = true; + assert(stm32_rtc_start_clock(DMRTC_CLOCK_LSE, 80) == -ENOTSUP); + assert(stm32_rtc_wait_register(®, 1, 1) == -ENOTSUP); + puts("PASS blocking calls reject pre-scheduler and ISR contexts"); + return 0; +} diff --git a/tests/test_dmrtc.dmr b/tests/test_dmrtc.dmr new file mode 100644 index 0000000..e79c829 --- /dev/null +++ b/tests/test_dmrtc.dmr @@ -0,0 +1,6 @@ +# Runtime diagnostic package (no generated public header). +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE]