From 99fc40368bca80eeec7cd1d9cc741c41f61e356e Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 15:42:20 -0700 Subject: [PATCH 01/12] defmt test? --- .gitmodules | 3 + cdefmt | 1 + cdefmt_port/CMakeLists.txt | 82 +++++++ cdefmt_port/README.md | 183 +++++++++++++++ cdefmt_port/cdefmt_sections.ld | 51 +++++ cdefmt_port/include/config/cdefmt_config.h | 42 ++++ ner_environment/build_system/build_system.py | 18 ++ ner_environment/build_system/cdefmt.py | 225 +++++++++++++++++++ platforms/stm32f405/include/stm32xx_hal.h | 2 +- 9 files changed, 606 insertions(+), 1 deletion(-) create mode 160000 cdefmt create mode 100644 cdefmt_port/CMakeLists.txt create mode 100644 cdefmt_port/README.md create mode 100644 cdefmt_port/cdefmt_sections.ld create mode 100644 cdefmt_port/include/config/cdefmt_config.h create mode 100644 ner_environment/build_system/cdefmt.py diff --git a/.gitmodules b/.gitmodules index 393784b6..abe15e9e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ [submodule "dev/OpenOCD"] path = dev/OpenOCD url = https://github.com/STMicroelectronics/OpenOCD.git +[submodule "cdefmt"] + path = cdefmt + url = git@github.com:RisinT96/cdefmt.git diff --git a/cdefmt b/cdefmt new file mode 160000 index 00000000..0e24735c --- /dev/null +++ b/cdefmt @@ -0,0 +1 @@ +Subproject commit 0e24735c654f197cfd131e535b56fd68dd291e73 diff --git a/cdefmt_port/CMakeLists.txt b/cdefmt_port/CMakeLists.txt new file mode 100644 index 00000000..69578f00 --- /dev/null +++ b/cdefmt_port/CMakeLists.txt @@ -0,0 +1,82 @@ +# ============================================================================= +# Embedded-Base integration layer for the cdefmt logging library +# (https://github.com/RisinT96/cdefmt). +# +# Goal: a parent project should be able to enable cdefmt with no edits to its +# CubeMX-managed linker script or toolchain file. Everything that cdefmt +# requires (compile flag, link flag, extra sections in the ELF, default +# config header) is propagated through a single INTERFACE target. +# +# Usage (from the parent project's CMakeLists.txt): +# +# add_subdirectory(Drivers/Embedded-Base/cdefmt_port) +# target_link_libraries(${CMAKE_PROJECT_NAME} embedded_base_cdefmt) +# +# Then the parent project must: +# 1. Provide an implementation of `void cdefmt_log(const void*, size_t, +# enum cdefmt_level)` in one of its source files. This is the transport +# sink (e.g. UART, ITM/SWO, RTT, ...). +# 2. Place `CDEFMT_GENERATE_INIT();` at file scope exactly once (typically +# in the same source file that implements `cdefmt_log`). +# 3. Call `cdefmt_init()` once after the transport is up, before any logs +# are emitted. +# +# Overriding the default config: +# This package adds `/include/` to the include path, which gives +# `` a sensible default. To override, the parent +# project should arrange for an earlier include directory to provide its +# own `config/cdefmt_config.h`. +# ============================================================================= + +cmake_minimum_required(VERSION 3.22) + +set(_CDEFMT_PORT_DIR ${CMAKE_CURRENT_LIST_DIR}) +set(_CDEFMT_UPSTREAM_DIR ${_CDEFMT_PORT_DIR}/../cdefmt) + +if(NOT EXISTS ${_CDEFMT_UPSTREAM_DIR}/CMakeLists.txt) + message(FATAL_ERROR + "cdefmt_port: upstream cdefmt submodule not found at " + "${_CDEFMT_UPSTREAM_DIR}. Did you `git submodule update --init " + "--recursive`?") +endif() + +# Pull in upstream cdefmt. Its CMakeLists only builds the example/decoder +# targets when it is the top-level project, so being added as a subdirectory +# is a no-op aside from defining the `cdefmt` INTERFACE library and fetching +# the two header-only Boost dependencies. +if(NOT TARGET cdefmt) + add_subdirectory( + ${_CDEFMT_UPSTREAM_DIR} + ${CMAKE_BINARY_DIR}/cdefmt + EXCLUDE_FROM_ALL + ) +endif() + +# Linker script fragment that defines the `.cdefmt` metadata section and the +# `.note.gnu.build-id` section. It is passed as an additional `-T` argument; +# GNU ld merges multiple `-T` scripts, so this layers cleanly on top of the +# main linker script without replacing it. +set(_CDEFMT_LD_FRAGMENT ${_CDEFMT_PORT_DIR}/cdefmt_sections.ld) + +add_library(embedded_base_cdefmt INTERFACE) + +target_link_libraries(embedded_base_cdefmt INTERFACE cdefmt) + +target_include_directories(embedded_base_cdefmt INTERFACE + ${_CDEFMT_PORT_DIR}/include +) + +# cdefmt's decoder needs DWARF info to parse argument types. +target_compile_options(embedded_base_cdefmt INTERFACE -g) + +# Link-time requirements: +# --build-id : tag the ELF with a GNU build-id. cdefmt uses it at +# runtime to validate that the binary matches the ELF the +# decoder was given. +# -T : add the `.cdefmt` / `.note.gnu.build-id` sections. This +# does NOT replace the main linker script -- GNU ld merges +# multiple `-T` scripts. +target_link_options(embedded_base_cdefmt INTERFACE + "LINKER:--build-id" + "-T${_CDEFMT_LD_FRAGMENT}" +) diff --git a/cdefmt_port/README.md b/cdefmt_port/README.md new file mode 100644 index 00000000..2eee5d96 --- /dev/null +++ b/cdefmt_port/README.md @@ -0,0 +1,183 @@ +# cdefmt_port + +Embedded-Base integration layer for [cdefmt](https://github.com/RisinT96/cdefmt), +a deferred-formatting logger inspired by Rust's [`defmt`](https://github.com/knurling-rs/defmt). + +The goal of this directory is to make `cdefmt` drop-in for any Embedded-Base +consumer **without** touching the parent project's CubeMX-managed linker +script or toolchain file. + +## What this provides + +A single CMake `INTERFACE` target, `embedded_base_cdefmt`, that propagates: + +| Requirement | How it's satisfied | +| --------------------------------- | ----------------------------------------------------------- | +| Header-only `cdefmt` library | `add_subdirectory(../cdefmt)` (pulls in Boost preprocessor + VMD via `FetchContent`) | +| `-g` compile flag | `target_compile_options(... -g)` | +| `-Wl,--build-id` link flag | `target_link_options(... LINKER:--build-id)` | +| `.cdefmt` + `.note.gnu.build-id` linker sections | Extra `-T cdefmt_sections.ld` passed to ld (ld merges multiple `-T` scripts, so the main linker script is **not** replaced) | +| `` | `include/config/cdefmt_config.h` added to include path | + +## Usage + +In the parent project's top-level `CMakeLists.txt`: + +```cmake +add_subdirectory(Drivers/Embedded-Base/cdefmt_port) + +target_link_libraries(${CMAKE_PROJECT_NAME} + embedded_base_cdefmt + # ... other libs ... +) +``` + +Then in one of the parent project's source files (e.g. `Core/Src/cdefmt_backend.c`): + +```c +#include +#include "main.h" + +extern UART_HandleTypeDef huart2; + +/* Emits the static `cdefmt_init()` function. Must appear exactly once in the + * program. */ +CDEFMT_GENERATE_INIT(); + +/* Transport sink. Called by every CDEFMT_* macro. */ +void cdefmt_log(const void *log, size_t size, enum cdefmt_level level) +{ + (void)level; + uint64_t hdr = (uint64_t)size; + HAL_UART_Transmit(&huart2, (uint8_t *)&hdr, sizeof hdr, HAL_MAX_DELAY); + HAL_UART_Transmit(&huart2, (uint8_t *)log, (uint16_t)size, HAL_MAX_DELAY); +} + +/* Plain-C wrapper around the static-inline cdefmt_init() so the rest of the + * program can call it without pulling in . */ +void cdefmt_init_app(void) { (void)cdefmt_init(); } +``` + +And call `cdefmt_init_app()` once, after the transport is initialized. + +## Why this design + +The `.cdefmt` and `.note.gnu.build-id` sections normally have to be added to +the project's main linker script. STM32CubeMX (and similar IDE-driven flows) +*regenerate* that linker script on every export, which would wipe manual +edits. By shipping a separate `cdefmt_sections.ld` fragment and passing it as +an additional `-T` argument, those sections are merged in by `ld` itself at +link time — so the CubeMX-managed `.ld` and toolchain file remain untouched. + +The same idea applies to `-g` and `-Wl,--build-id`: rather than editing the +CubeMX toolchain file, they ride along on the `INTERFACE` target. + +## Decoding logs on the host + +The recommended way is to use the dedicated `ner` command, which lives in +[`ner_environment/build_system/cdefmt.py`](../ner_environment/build_system/cdefmt.py): + +```bash +ner cdefmt # auto-detects port + ELF, builds decoder on first run +ner cdefmt --device /dev/ttyACM1 # specific port +ner cdefmt --elf path/to/fw.elf # specific ELF +ner cdefmt --rebuild # force rebuild of the host-side decoder +ner cdefmt --list # list candidate ports / ELFs and exit +``` + +Under the hood the command: + +1. Builds the Rust decoder at `Drivers/Embedded-Base/cdefmt/target/release/stdin` + on first invocation (or when `--rebuild` is passed). Requires `cargo` on + PATH — install Rust via if needed. +2. Picks the newest `build/*.elf` unless `--elf` overrides. +3. Picks the first `/dev/ttyACM*` or `/dev/ttyUSB*` unless `--device` + overrides (Linux only; macOS/Windows users must pass `--device`). +4. Puts the TTY in `raw -echo -ixon -icrnl` mode so binary cdefmt frames + pass through untouched (the kernel's default canonical line discipline + would buffer until a `\n` arrives, which a binary protocol may never + emit). +5. Exec's the decoder with the serial port redirected to its stdin. + +If you'd rather invoke the decoder by hand (e.g. when running on a host that +doesn't have the `ner` venv installed), the equivalent shell incantation is: + +```bash +cd Drivers/Embedded-Base/cdefmt +cargo build --release +stty -F /dev/ttyACM0 raw -echo -ixon -icrnl 115200 +target/release/stdin --elf ../../../build/F4-Nucleo.elf < /dev/ttyACM0 +``` + +The frame format that the bundled decoder expects is a 64-bit little-endian +length followed by that many bytes of payload — which is exactly what the +backend snippet above emits. + +## Thread-safety note + +`cdefmt_log` is called from whatever context invokes a `CDEFMT_*` macro. If +that includes multiple RTOS threads (or ISRs), wrap the body of `cdefmt_log` +in a mutex, or hand the frame off to a dedicated logging thread via a queue. +The integration here intentionally stays transport-agnostic and leaves this +to the consumer. + +## Mixing transports + +Do **not** interleave plain `printf` and `CDEFMT_*` output on the same UART. +cdefmt sends length-prefixed binary frames; any stray ASCII byte will be +interpreted by the host decoder as a (huge) length and crash it with +`memory allocation of bytes failed`. Either: + +- Send cdefmt and printf over different transports (e.g. cdefmt over ITM/SWO, + printf over UART), or +- Drop printf entirely and use only cdefmt on the shared UART. + +## Known cosmetic warning from STM32CubeProgrammer + +When flashing the ELF you may see: + +``` +Warning: File corrupted. Two or more segments define the same memory zone + or wrong order of segments +``` + +This is harmless. It is caused by the CubeMX-generated linker script +declaring `.bss` and `._user_heap_stack` as `(NOLOAD) >RAM` with no `AT` +clause, which makes ld auto-assign them a load address equal to the FLASH +location counter -- which happens to coincide with the address of the +auto-orphan-placed `.note.gnu.build-id` section. The conflicting segments +have `FileSiz = 0`, so no actual bytes are written or duplicated in flash; +CubeProgrammer just sees two PT_LOAD entries sharing a `PhysAddr` and +prints the warning regardless. Flashing completes successfully (`File +download complete`). + +The only way to silence this without touching the CubeMX-managed linker +script is a post-link `objcopy --change-section-lma` step. The +`embedded_base_cdefmt` target deliberately does not do this by default -- +it's cosmetic and doesn't affect correctness. + +## Validated on F4-Nucleo (June 2026) + +Working end-to-end with the integration above: + +- Init log (`cdefmt_init()` emits the build-id) decodes correctly. +- All three log levels exercised (`CDEFMT_INFO`, `CDEFMT_DEBUG`, + `CDEFMT_WARNING`) — formatting, level names, source location all show up + correctly host-side. +- Multi-arg logs work, including alt-form hex (`{:#x}`). +- Frame stream is stable for 100+ iterations back-to-back without drift. + +## Known follow-up: timing primitives inside ThreadX threads + +Unrelated to cdefmt but discovered during integration: in this project, both +`HAL_Delay()` and `tx_thread_sleep()` hang forever when called from a +ThreadX thread, because the IRQs that drive their tick sources (TIM6 for HAL, +SysTick handler's effect on the suspend list for ThreadX) are not being +delivered to thread context. PRIMASK and BASEPRI are both 0, the relevant +ISRs are linked, and `tx_time_get()` *does* increment — yet sleeping threads +are never resumed and `uwTick` stays at 0. + +This is a CubeMX-for-ThreadX integration quirk, not a cdefmt issue. As a +workaround, the demo in `Core/Src/u_threads.c` uses a CPU busy-wait between +log emissions. See the file header comment there for the full diagnosis. + diff --git a/cdefmt_port/cdefmt_sections.ld b/cdefmt_port/cdefmt_sections.ld new file mode 100644 index 00000000..963160f0 --- /dev/null +++ b/cdefmt_port/cdefmt_sections.ld @@ -0,0 +1,51 @@ +/* + * cdefmt linker script fragment. + * + * This file is consumed by passing `-T ` to the linker, IN + * ADDITION to the project's main linker script. It must NOT need to edit + * the main script (which on STM32 projects is typically regenerated by + * CubeMX on every export). + * + * Why we don't define our own `.note.gnu.build-id` section: + * + * `-Wl,--build-id` (also propagated by the embedded_base_cdefmt target) + * tells ld to synthesise a `.note.gnu.build-id` section automatically. + * When it isn't claimed by any SECTIONS command, ld places it as an + * "orphan" alongside other read-only allocated sections (typically right + * after `.rodata`/`.ARM`). That orphan placement is well-behaved -- it + * doesn't collide with the auto-assigned LMA of the CubeMX-generated + * `.bss` / `._user_heap_stack` sections, which is what would otherwise + * make STM32CubeProgrammer print: + * "File corrupted. Two or more segments define the same memory zone" + * + * An earlier version of this fragment defined `.note.gnu.build-id` + * explicitly and tried to position it with `INSERT BEFORE .data`. That + * does not work with multiple `-T` scripts (ld's INSERT only sees the + * sections defined in the same `-T` file), and the resulting LMA layout + * triggered the warning above. + * + * What this fragment DOES define: + * + * __cdefmt_build_id -- symbol that cdefmt's runtime resolves to find + * the build-id note. PROVIDE'd against the + * orphan-placed `.note.gnu.build-id` section's + * address. + * + * .cdefmt 0 (INFO) -- log metadata (format strings, argument names, + * source locations). Marked (INFO) so it is not + * allocated to a memory region -- it lives in + * the ELF for the host decoder but does NOT + * consume any flash on the device. The base + * address of 0 is what lets the linker assign + * each log a unique ID via its address. + */ + +SECTIONS { + .cdefmt 0 (INFO) : { + KEEP(*(.cdefmt.init .cdefmt.init.*)) + . = . + 8; + KEEP(*(.cdefmt .cdefmt.*)) + } +} + +PROVIDE(__cdefmt_build_id = ADDR(.note.gnu.build-id)); diff --git a/cdefmt_port/include/config/cdefmt_config.h b/cdefmt_port/include/config/cdefmt_config.h new file mode 100644 index 00000000..b3e3d148 --- /dev/null +++ b/cdefmt_port/include/config/cdefmt_config.h @@ -0,0 +1,42 @@ +/* + * Default cdefmt configuration for projects that consume Embedded-Base's + * `embedded_base_cdefmt` CMake target. + * + * cdefmt's header (`cdefmt/include/cdefmt.h`) does: + * #include + * so this file must live at `/config/cdefmt_config.h`. + * The `embedded_base_cdefmt` target adds the parent directory of `config/` + * to the include path, so this file is picked up automatically. + * + * To override these defaults from a parent project, ensure that an earlier + * include directory provides a different `config/cdefmt_config.h`. + */ + +#ifndef CDEFMT_CONFIG_H +#define CDEFMT_CONFIG_H + +/* --------------------------------------------------------------------------- + * Stack-allocated log buffer (default). + * + * Each CDEFMT_* call materializes a packed struct on the caller's stack of + * size: sizeof(log_id) + sum(sizeof(args)) + DYNAMIC_SIZE_MAX bytes. Threads + * that log must have enough stack headroom to cover this. For a typical + * Cortex-M project with multi-kB thread stacks, the default 128-byte + * dynamic reservation is comfortable. + * ------------------------------------------------------------------------- */ +#define CDEFMT_USE_STACK_LOG_BUFFER 1 +#define CDEFMT_STACK_LOG_BUFFER_DYNAMIC_SIZE_MAX 128 + +/* --------------------------------------------------------------------------- + * Alternative buffer modes (disabled by default). + * + * If switching to STATIC, also provide CDEFMT_STATIC_LOG_BUFFER, + * CDEFMT_STATIC_LOG_BUFFER_SIZE, and a lock/unlock pair if multi-threaded. + * + * If switching to DYNAMIC, also provide CDEFMT_DYNAMIC_LOG_BUFFER_ALLOC and + * CDEFMT_DYNAMIC_LOG_BUFFER_FREE. + * ------------------------------------------------------------------------- */ +#define CDEFMT_USE_STATIC_LOG_BUFFER 0 +#define CDEFMT_USE_DYNAMIC_LOG_BUFFER 0 + +#endif /* CDEFMT_CONFIG_H */ diff --git a/ner_environment/build_system/build_system.py b/ner_environment/build_system/build_system.py index dca2774e..3d5a3567 100644 --- a/ner_environment/build_system/build_system.py +++ b/ner_environment/build_system/build_system.py @@ -31,6 +31,7 @@ # custom modules for functinality that is too large to be included in this script directly from .miniterm import main as miniterm from .serial2 import main as serial2_start +from .cdefmt import main as cdefmt_main # ============================================================================== # Typer application setup @@ -262,6 +263,23 @@ def serial2( serial2_start(ls=ls, device=device, monitor=monitor, graph=graph, filter=filter) +# ============================================================================== +# CDEFMT command +# ============================================================================== + +@app.command(help="Live-decode cdefmt binary log frames coming over UART. Requires the cdefmt submodule (Drivers/Embedded-Base/cdefmt) and `cargo` on PATH for the first-run decoder build.") +def cdefmt(device: str = typer.Option("", "--device", "-d", + help="Serial device to read from (e.g. /dev/ttyACM0). Auto-detected if omitted."), + elf: str = typer.Option(None, "--elf", + help="ELF file to decode against. Defaults to the most recently built build/*.elf."), + rebuild: bool = typer.Option(False, "--rebuild", + help="Force a rebuild of the host-side cdefmt decoder before running."), + ls: bool = typer.Option(False, "--list", + help="List candidate serial ports and ELF files, then exit."), + baud: int = typer.Option(115200, "--baud", "-b", + help="Serial baud rate (USB CDC ACM ignores it, but stty wants a value).")): + cdefmt_main(device=device, elf=elf, rebuild=rebuild, ls=ls, baud=baud) + # ============================================================================== # Test command # ============================================================================== diff --git a/ner_environment/build_system/cdefmt.py b/ner_environment/build_system/cdefmt.py new file mode 100644 index 00000000..45ebcf06 --- /dev/null +++ b/ner_environment/build_system/cdefmt.py @@ -0,0 +1,225 @@ +# ============================================================================== +# `ner cdefmt` -- live decoder integration for cdefmt-formatted UART logs. +# +# cdefmt is a deferred-formatting logger: the MCU emits length-prefixed binary +# frames (8-byte LE length + log-id + raw arg bytes), and a host-side decoder +# turns those bytes into human-readable log lines by looking the metadata up +# in the firmware's ELF (DWARF) on disk. +# +# This module wires both halves together so a user just runs `ner cdefmt`: +# - locate (or first-time build) the decoder binary, +# - pick the newest ELF under build/ unless --elf overrides, +# - pick a USB serial device unless --device overrides, +# - put the TTY in raw mode so binary frames pass through untouched, +# - exec the decoder with the port redirected to stdin. +# +# Intentionally lives next to miniterm.py / serial2.py and follows the same +# pattern: a standalone main() called by a thin @app.command() wrapper in +# build_system.py. +# ============================================================================== + +import glob +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path + +from rich import print + + +# Paths are resolved relative to the project working directory (i.e. the dir +# from which the user ran `ner cdefmt`). This matches how every other ner +# command works. +CDEFMT_REPO_REL = Path("Drivers/Embedded-Base/cdefmt") +DECODER_BIN_REL = CDEFMT_REPO_REL / "target" / "release" / "stdin" + + +# ------------------------------------------------------------------------------ +# Decoder discovery / build +# ------------------------------------------------------------------------------ + +def _find_decoder(rebuild: bool = False) -> Path: + """Return the path to the host-side decoder binary. + + Builds it on first use (or when --rebuild is passed) by running + `cargo build --release` in the cdefmt submodule. Exits with a helpful + error if the submodule isn't checked out or if cargo isn't installed. + """ + cdefmt_repo = Path.cwd() / CDEFMT_REPO_REL + decoder = Path.cwd() / DECODER_BIN_REL + + if not cdefmt_repo.exists(): + print(f"[bold red]Error:[/bold red] cdefmt submodule not found at " + f"[blue]{CDEFMT_REPO_REL}[/blue]. Did you run " + f"`git submodule update --init --recursive`?", file=sys.stderr) + sys.exit(1) + + if decoder.exists() and not rebuild: + return decoder + + if shutil.which("cargo") is None: + print("[bold red]Error:[/bold red] `cargo` is not on PATH. Install " + "Rust (https://rustup.rs) to build the cdefmt host-side " + "decoder.", file=sys.stderr) + sys.exit(1) + + action = "Rebuilding" if rebuild else "Building" + print(f"[#cccccc](ner cdefmt):[/#cccccc] [blue]{action} host-side " + f"decoder...[/blue]") + result = subprocess.run(["cargo", "build", "--release"], cwd=cdefmt_repo) + if result.returncode != 0: + print("[bold red]Error:[/bold red] `cargo build` failed; see output " + "above.", file=sys.stderr) + sys.exit(result.returncode) + + if not decoder.exists(): + # Cargo succeeded but the expected binary isn't where we look for it. + # Most likely the upstream cdefmt repo restructured its `examples/`. + print(f"[bold red]Error:[/bold red] cargo build succeeded but the " + f"decoder binary was not produced at [blue]{decoder}[/blue]. " + f"Has the cdefmt example layout changed upstream?", + file=sys.stderr) + sys.exit(1) + + return decoder + + +# ------------------------------------------------------------------------------ +# ELF discovery +# ------------------------------------------------------------------------------ + +def _find_elf(explicit: str | None) -> Path: + """Return the ELF to decode against -- explicit path if given, else the + most-recently-modified .elf under build/.""" + if explicit: + p = Path(explicit) + if not p.is_file(): + print(f"[bold red]Error:[/bold red] ELF not found: " + f"[blue]{p}[/blue]", file=sys.stderr) + sys.exit(1) + return p + + candidates = glob.glob("build/*.elf") + if not candidates: + print("[bold red]Error:[/bold red] no ELF found under build/. Run " + "`ner build` first, or pass --elf.", file=sys.stderr) + sys.exit(1) + + # Newest by mtime, so multi-target projects pick whatever was just rebuilt. + return Path(max(candidates, key=os.path.getmtime)) + + +# ------------------------------------------------------------------------------ +# Serial port discovery +# ------------------------------------------------------------------------------ + +def _list_ports() -> list[str]: + """Return USB serial devices currently visible to the OS. + + Linux only for now -- mirrors miniterm.py's coverage. macOS/Windows users + should pass --device explicitly until this grows cross-platform support. + """ + if platform.system() != "Linux": + return [] + return sorted(glob.glob("/dev/ttyACM*") + glob.glob("/dev/ttyUSB*")) + + +def _find_port(explicit: str) -> str: + """Return the serial port to use -- explicit if given, else first detected. + + The first detected port is typically /dev/ttyACM0 (a Nucleo's ST-LINK VCP), + which is also what `nflash` targets by default. + """ + if explicit: + if not os.path.exists(explicit): + print(f"[bold red]Error:[/bold red] device not found: " + f"[blue]{explicit}[/blue]", file=sys.stderr) + sys.exit(1) + return explicit + + if platform.system() != "Linux": + print("[bold red]Error:[/bold red] auto-detect port not implemented " + "for this OS; please pass --device.", file=sys.stderr) + sys.exit(1) + + ports = _list_ports() + if not ports: + print("[bold red]Error:[/bold red] no USB serial devices found. Is " + "the board plugged in?", file=sys.stderr) + sys.exit(1) + + return ports[0] + + +# ------------------------------------------------------------------------------ +# TTY setup +# ------------------------------------------------------------------------------ + +def _set_raw_tty(port: str, baud: int) -> None: + """Put the TTY in raw mode so binary cdefmt frames pass through untouched. + + The kernel's default line discipline is `icanon`, which only releases + bytes to read() when a newline arrives. That's catastrophic for a + length-prefixed binary protocol -- the decoder would block waiting for an + 0x0a that may legitimately never appear. `raw` (= !icanon !echo !isig + !iexten ...) plus explicit -icrnl/-ixon disables every transformation + that would mangle binary payload bytes. + """ + if platform.system() != "Linux": + # stty's syntax differs on macOS/BSDs; not implemented yet. + return + + cmd = ["stty", "-F", port, "raw", "-echo", "-ixon", "-icrnl", str(baud)] + result = subprocess.run(cmd) + if result.returncode != 0: + print(f"[bold red]Error:[/bold red] `stty` failed on [blue]{port}" + f"[/blue].", file=sys.stderr) + sys.exit(result.returncode) + + +# ------------------------------------------------------------------------------ +# Entry point +# ------------------------------------------------------------------------------ + +def main(device: str = "", elf: str | None = None, rebuild: bool = False, + ls: bool = False, baud: int = 115200) -> None: + """Invoked by the `ner cdefmt` command. See cdefmt() in build_system.py + for the typer-level option definitions.""" + + if ls: + ports = _list_ports() + elfs = sorted(glob.glob("build/*.elf")) + print("[bold]Serial ports:[/bold]") + for p in (ports or [" (none)"]): + print(f" {p}") + print("[bold]ELF files under build/:[/bold]") + for e in (elfs or [" (none)"]): + print(f" {e}") + return + + decoder = _find_decoder(rebuild=rebuild) + elf_path = _find_elf(elf) + port = _find_port(device) + + _set_raw_tty(port, baud) + + print(f"[#cccccc](ner cdefmt):[/#cccccc] decoding [blue]{port}[/blue] " + f"against [blue]{elf_path}[/blue] -- Ctrl-C to quit") + + # Hand the open port to the decoder as its stdin. Python stays in the + # call stack just long enough to set this up; once subprocess.run returns + # we propagate its exit code so a Ctrl-C-killed decoder is indistinguish- + # able from running the binary directly. + try: + with open(port, "rb") as port_fd: + result = subprocess.run( + [str(decoder), "--elf", str(elf_path)], + stdin=port_fd, + ) + except KeyboardInterrupt: + # The decoder will have already gotten the SIGINT; exit quietly. + sys.exit(130) + + sys.exit(result.returncode) diff --git a/platforms/stm32f405/include/stm32xx_hal.h b/platforms/stm32f405/include/stm32xx_hal.h index ddc8843d..15b0dc1f 100644 --- a/platforms/stm32f405/include/stm32xx_hal.h +++ b/platforms/stm32f405/include/stm32xx_hal.h @@ -1,7 +1,7 @@ #ifndef STM32XX_HAL_H #define STM32XX_HAL_H -#ifdef STM32F405xx +#ifdef STM32F446xx #include "stm32f4xx_hal.h" #endif From 80a93dd49648d762056ca055a8f64f699318105b Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 15:58:03 -0700 Subject: [PATCH 02/12] fork of cdefmt --- .gitmodules | 2 +- ner_environment/build_system/cdefmt.py | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/.gitmodules b/.gitmodules index abe15e9e..e43e2259 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,4 +4,4 @@ url = https://github.com/STMicroelectronics/OpenOCD.git [submodule "cdefmt"] path = cdefmt - url = git@github.com:RisinT96/cdefmt.git + url = git@github.com:bjackson312006/cdefmt.git diff --git a/ner_environment/build_system/cdefmt.py b/ner_environment/build_system/cdefmt.py index 45ebcf06..0eee874d 100644 --- a/ner_environment/build_system/cdefmt.py +++ b/ner_environment/build_system/cdefmt.py @@ -1,23 +1,3 @@ -# ============================================================================== -# `ner cdefmt` -- live decoder integration for cdefmt-formatted UART logs. -# -# cdefmt is a deferred-formatting logger: the MCU emits length-prefixed binary -# frames (8-byte LE length + log-id + raw arg bytes), and a host-side decoder -# turns those bytes into human-readable log lines by looking the metadata up -# in the firmware's ELF (DWARF) on disk. -# -# This module wires both halves together so a user just runs `ner cdefmt`: -# - locate (or first-time build) the decoder binary, -# - pick the newest ELF under build/ unless --elf overrides, -# - pick a USB serial device unless --device overrides, -# - put the TTY in raw mode so binary frames pass through untouched, -# - exec the decoder with the port redirected to stdin. -# -# Intentionally lives next to miniterm.py / serial2.py and follows the same -# pattern: a standalone main() called by a thin @app.command() wrapper in -# build_system.py. -# ============================================================================== - import glob import os import platform From d0dc6eabd847e53801750c35167026a03a8093c3 Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 16:13:25 -0700 Subject: [PATCH 03/12] moved cdefmt submodule --- .gitmodules | 2 +- cdefmt => cdefmt_port/cdefmt | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename cdefmt => cdefmt_port/cdefmt (100%) diff --git a/.gitmodules b/.gitmodules index e43e2259..33d2fe06 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,5 +3,5 @@ path = dev/OpenOCD url = https://github.com/STMicroelectronics/OpenOCD.git [submodule "cdefmt"] - path = cdefmt + path = cdefmt_port/cdefmt url = git@github.com:bjackson312006/cdefmt.git diff --git a/cdefmt b/cdefmt_port/cdefmt similarity index 100% rename from cdefmt rename to cdefmt_port/cdefmt From 1feb5ee5253f7865e4642d63f486629ff4ff327b Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 17:21:09 -0700 Subject: [PATCH 04/12] new path --- ner_environment/build_system/cdefmt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ner_environment/build_system/cdefmt.py b/ner_environment/build_system/cdefmt.py index 0eee874d..4f0db825 100644 --- a/ner_environment/build_system/cdefmt.py +++ b/ner_environment/build_system/cdefmt.py @@ -12,7 +12,7 @@ # Paths are resolved relative to the project working directory (i.e. the dir # from which the user ran `ner cdefmt`). This matches how every other ner # command works. -CDEFMT_REPO_REL = Path("Drivers/Embedded-Base/cdefmt") +CDEFMT_REPO_REL = Path("Drivers/Embedded-Base/cdefmt_port/cdefmt") DECODER_BIN_REL = CDEFMT_REPO_REL / "target" / "release" / "stdin" From e6e01387f4618ee72f6e76105d0de78d6967ee69 Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 17:53:36 -0700 Subject: [PATCH 05/12] new cdefmt commits --- cdefmt_port/cdefmt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdefmt_port/cdefmt b/cdefmt_port/cdefmt index 0e24735c..e413a412 160000 --- a/cdefmt_port/cdefmt +++ b/cdefmt_port/cdefmt @@ -1 +1 @@ -Subproject commit 0e24735c654f197cfd131e535b56fd68dd291e73 +Subproject commit e413a412ee5ca620c93e0c2e798c41e095303d5c From 041e378069aa6ec8e1c3c6483622abc65998fc1a Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 18:04:43 -0700 Subject: [PATCH 06/12] change cmake path --- cdefmt_port/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdefmt_port/CMakeLists.txt b/cdefmt_port/CMakeLists.txt index 69578f00..32e89b3f 100644 --- a/cdefmt_port/CMakeLists.txt +++ b/cdefmt_port/CMakeLists.txt @@ -31,7 +31,7 @@ cmake_minimum_required(VERSION 3.22) set(_CDEFMT_PORT_DIR ${CMAKE_CURRENT_LIST_DIR}) -set(_CDEFMT_UPSTREAM_DIR ${_CDEFMT_PORT_DIR}/../cdefmt) +set(_CDEFMT_UPSTREAM_DIR ${_CDEFMT_PORT_DIR}/cdefmt) if(NOT EXISTS ${_CDEFMT_UPSTREAM_DIR}/CMakeLists.txt) message(FATAL_ERROR From 37d3753e122dee3c6c04fc583f5202707c8fdf44 Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 19:45:17 -0700 Subject: [PATCH 07/12] cdefmt_port CMakeLists.txt changes to stop library-specific warnings --- cdefmt_port/CMakeLists.txt | 30 ++++++ cdefmt_port/README.md | 181 ++++++++----------------------------- 2 files changed, 67 insertions(+), 144 deletions(-) diff --git a/cdefmt_port/CMakeLists.txt b/cdefmt_port/CMakeLists.txt index 32e89b3f..9ebe8cd3 100644 --- a/cdefmt_port/CMakeLists.txt +++ b/cdefmt_port/CMakeLists.txt @@ -66,6 +66,36 @@ target_include_directories(embedded_base_cdefmt INTERFACE ${_CDEFMT_PORT_DIR}/include ) +# Treat cdefmt's header and its Boost.Preprocessor/VMD dependencies as system +# headers for anyone who links `embedded_base_cdefmt`. cdefmt's logging macros +# expand into code that legitimately trips -Wunused-variable, +# -Wdiscarded-qualifiers and -Wunused-function; GCC attributes those warnings to +# the macro body (in cdefmt.h), so promoting these include dirs to `-isystem` +# silences them at the source without touching the parent project's build files +# or suppressing warnings in the parent's own code. +# +# We can't simply mark them SYSTEM here because the include dirs are propagated +# by the upstream `cdefmt`/`Boost::*` targets, which we don't own. Instead we +# copy each target's INTERFACE_INCLUDE_DIRECTORIES into its +# INTERFACE_SYSTEM_INCLUDE_DIRECTORIES, the portable pre-CMake-3.25 way to do +# this (the `SYSTEM` keyword on add_subdirectory/targets needs >= 3.25). +foreach(_cdefmt_dep cdefmt Boost::preprocessor Boost::vmd) + if(TARGET ${_cdefmt_dep}) + # Boost::* are ALIAS targets; set_property() rejects aliases, so resolve + # to the underlying real target before touching its properties. + get_target_property(_cdefmt_real ${_cdefmt_dep} ALIASED_TARGET) + if(NOT _cdefmt_real) + set(_cdefmt_real ${_cdefmt_dep}) + endif() + get_target_property(_cdefmt_dep_includes + ${_cdefmt_real} INTERFACE_INCLUDE_DIRECTORIES) + if(_cdefmt_dep_includes) + set_property(TARGET ${_cdefmt_real} APPEND PROPERTY + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES ${_cdefmt_dep_includes}) + endif() + endif() +endforeach() + # cdefmt's decoder needs DWARF info to parse argument types. target_compile_options(embedded_base_cdefmt INTERFACE -g) diff --git a/cdefmt_port/README.md b/cdefmt_port/README.md index 2eee5d96..bd534e3a 100644 --- a/cdefmt_port/README.md +++ b/cdefmt_port/README.md @@ -1,27 +1,14 @@ # cdefmt_port -Embedded-Base integration layer for [cdefmt](https://github.com/RisinT96/cdefmt), -a deferred-formatting logger inspired by Rust's [`defmt`](https://github.com/knurling-rs/defmt). +This is a wrapper layer to integrate the `cdefmt` into Embedded-Base. `cdefmt` is a library someone made that provides similar functionality to the `defmt` Rust crate. Link: https://github.com/RisinT96/cdefmt -The goal of this directory is to make `cdefmt` drop-in for any Embedded-Base -consumer **without** touching the parent project's CubeMX-managed linker -script or toolchain file. +## Setting Up cdefmt -## What this provides +To set up CDEFMT logging in an existing project, there are a few steps you need to follow. -A single CMake `INTERFACE` target, `embedded_base_cdefmt`, that propagates: +First, make sure the project has Embedded-Base as a submodule, on a commit that has cdefmt_port included. -| Requirement | How it's satisfied | -| --------------------------------- | ----------------------------------------------------------- | -| Header-only `cdefmt` library | `add_subdirectory(../cdefmt)` (pulls in Boost preprocessor + VMD via `FetchContent`) | -| `-g` compile flag | `target_compile_options(... -g)` | -| `-Wl,--build-id` link flag | `target_link_options(... LINKER:--build-id)` | -| `.cdefmt` + `.note.gnu.build-id` linker sections | Extra `-T cdefmt_sections.ld` passed to ld (ld merges multiple `-T` scripts, so the main linker script is **not** replaced) | -| `` | `include/config/cdefmt_config.h` added to include path | - -## Usage - -In the parent project's top-level `CMakeLists.txt`: +Then, add this to the parent project's top-level `CMakeLists.txt`: ```cmake add_subdirectory(Drivers/Embedded-Base/cdefmt_port) @@ -32,152 +19,58 @@ target_link_libraries(${CMAKE_PROJECT_NAME} ) ``` -Then in one of the parent project's source files (e.g. `Core/Src/cdefmt_backend.c`): +After that, add the cdefmt routing code to your project's `main.c`: ```c #include -#include "main.h" - -extern UART_HandleTypeDef huart2; -/* Emits the static `cdefmt_init()` function. Must appear exactly once in the - * program. */ +/* Emits the static-inline `cdefmt_init()` function used during start-up to + * send the build-id init log. Must appear at file scope exactly once in the + * whole program. */ CDEFMT_GENERATE_INIT(); -/* Transport sink. Called by every CDEFMT_* macro. */ +/** + * @brief Transport sink invoked by every CDEFMT_* macro. + */ void cdefmt_log(const void *log, size_t size, enum cdefmt_level level) { - (void)level; - uint64_t hdr = (uint64_t)size; + (void)level; /* Runtime level filtering can be added here if desired. */ + + const uint64_t hdr = (uint64_t)size; + HAL_UART_Transmit(&huart2, (uint8_t *)&hdr, sizeof hdr, HAL_MAX_DELAY); HAL_UART_Transmit(&huart2, (uint8_t *)log, (uint16_t)size, HAL_MAX_DELAY); } - -/* Plain-C wrapper around the static-inline cdefmt_init() so the rest of the - * program can call it without pulling in . */ -void cdefmt_init_app(void) { (void)cdefmt_init(); } ``` +(Obviously, you should use whatever UART your project has available, rather than specifically `huart2`. Also, make sure nothing else is using this UART aside from cdefmt; if you also have printf() routed to this same UART or something, you'll get errors when trying to run `ner cdefmt`.) -And call `cdefmt_init_app()` once, after the transport is initialized. - -## Why this design - -The `.cdefmt` and `.note.gnu.build-id` sections normally have to be added to -the project's main linker script. STM32CubeMX (and similar IDE-driven flows) -*regenerate* that linker script on every export, which would wipe manual -edits. By shipping a separate `cdefmt_sections.ld` fragment and passing it as -an additional `-T` argument, those sections are merged in by `ld` itself at -link time — so the CubeMX-managed `.ld` and toolchain file remain untouched. - -The same idea applies to `-g` and `-Wl,--build-id`: rather than editing the -CubeMX toolchain file, they ride along on the `INTERFACE` target. - -## Decoding logs on the host +Also, add `cdefmt_init()` to the `main()` function in your `main.c`, after your UART initialization code has ran. -The recommended way is to use the dedicated `ner` command, which lives in -[`ner_environment/build_system/cdefmt.py`](../ner_environment/build_system/cdefmt.py): +# Usage -```bash -ner cdefmt # auto-detects port + ELF, builds decoder on first run -ner cdefmt --device /dev/ttyACM1 # specific port -ner cdefmt --elf path/to/fw.elf # specific ELF -ner cdefmt --rebuild # force rebuild of the host-side decoder -ner cdefmt --list # list candidate ports / ELFs and exit +Now, you should be ready to use cdefmt. In any file where you want to log stuff, add `#include ` to the top. Then, you can use the following cdefmt macros: +```c +CDEFMT_ERROR("This is an error log."); +CDEFMT_WARNING("This is a warning log."); +CDEFMT_INFO("This is an info log."); +CDEFMT_DEBUG("This is a debug log."); +CDEFMT_VERBOSE("This is a verbose log."); ``` -Under the hood the command: - -1. Builds the Rust decoder at `Drivers/Embedded-Base/cdefmt/target/release/stdin` - on first invocation (or when `--rebuild` is passed). Requires `cargo` on - PATH — install Rust via if needed. -2. Picks the newest `build/*.elf` unless `--elf` overrides. -3. Picks the first `/dev/ttyACM*` or `/dev/ttyUSB*` unless `--device` - overrides (Linux only; macOS/Windows users must pass `--device`). -4. Puts the TTY in `raw -echo -ixon -icrnl` mode so binary cdefmt frames - pass through untouched (the kernel's default canonical line discipline - would buffer until a `\n` arrives, which a binary protocol may never - emit). -5. Exec's the decoder with the serial port redirected to its stdin. - -If you'd rather invoke the decoder by hand (e.g. when running on a host that -doesn't have the `ner` venv installed), the equivalent shell incantation is: - -```bash -cd Drivers/Embedded-Base/cdefmt -cargo build --release -stty -F /dev/ttyACM0 raw -echo -ixon -icrnl 115200 -target/release/stdin --elf ../../../build/F4-Nucleo.elf < /dev/ttyACM0 +Here's an example of how formatting data works in these macros: +```c +CDEFMT_DEBUG("loop state: tick={} led_pin={:#x}, cool_float={}", tick, led_pin, cool_float); +// `tick` is a uint32_t, `led_pin` is a uint16_t that we want to display in hex, and `cool_float` is a float ``` -The frame format that the bundled decoder expects is a 64-bit little-endian -length followed by that many bytes of payload — which is exactly what the -backend snippet above emits. - -## Thread-safety note - -`cdefmt_log` is called from whatever context invokes a `CDEFMT_*` macro. If -that includes multiple RTOS threads (or ISRs), wrap the body of `cdefmt_log` -in a mutex, or hand the frame off to a dedicated logging thread via a queue. -The integration here intentionally stays transport-agnostic and leaves this -to the consumer. - -## Mixing transports - -Do **not** interleave plain `printf` and `CDEFMT_*` output on the same UART. -cdefmt sends length-prefixed binary frames; any stray ASCII byte will be -interpreted by the host decoder as a (huge) length and crash it with -`memory allocation of bytes failed`. Either: - -- Send cdefmt and printf over different transports (e.g. cdefmt over ITM/SWO, - printf over UART), or -- Drop printf entirely and use only cdefmt on the shared UART. - -## Known cosmetic warning from STM32CubeProgrammer - -When flashing the ELF you may see: - +That macro call produced this log when I ran the code and viewed logging output (via `ner cdefmt`): ``` -Warning: File corrupted. Two or more segments define the same memory zone - or wrong order of segments +[u_threads.c/vDefault():78] Debug > loop state: tick=17 led_pin=0x20, cool_float=64.08844 ``` +As shown above, the logs are automatically prepended with the filename, function name, line number, and logging level (e.g., Debug, Warning, Error, etc) corresponding to the macro call. -This is harmless. It is caused by the CubeMX-generated linker script -declaring `.bss` and `._user_heap_stack` as `(NOLOAD) >RAM` with no `AT` -clause, which makes ld auto-assign them a load address equal to the FLASH -location counter -- which happens to coincide with the address of the -auto-orphan-placed `.note.gnu.build-id` section. The conflicting segments -have `FileSiz = 0`, so no actual bytes are written or duplicated in flash; -CubeProgrammer just sees two PT_LOAD entries sharing a `PhysAddr` and -prints the warning regardless. Flashing completes successfully (`File -download complete`). - -The only way to silence this without touching the CubeMX-managed linker -script is a post-link `objcopy --change-section-lma` step. The -`embedded_base_cdefmt` target deliberately does not do this by default -- -it's cosmetic and doesn't affect correctness. - -## Validated on F4-Nucleo (June 2026) - -Working end-to-end with the integration above: - -- Init log (`cdefmt_init()` emits the build-id) decodes correctly. -- All three log levels exercised (`CDEFMT_INFO`, `CDEFMT_DEBUG`, - `CDEFMT_WARNING`) — formatting, level names, source location all show up - correctly host-side. -- Multi-arg logs work, including alt-form hex (`{:#x}`). -- Frame stream is stable for 100+ iterations back-to-back without drift. - -## Known follow-up: timing primitives inside ThreadX threads - -Unrelated to cdefmt but discovered during integration: in this project, both -`HAL_Delay()` and `tx_thread_sleep()` hang forever when called from a -ThreadX thread, because the IRQs that drive their tick sources (TIM6 for HAL, -SysTick handler's effect on the suspend list for ThreadX) are not being -delivered to thread context. PRIMASK and BASEPRI are both 0, the relevant -ISRs are linked, and `tx_time_get()` *does* increment — yet sleeping threads -are never resumed and `uwTick` stays at 0. - -This is a CubeMX-for-ThreadX integration quirk, not a cdefmt issue. As a -workaround, the demo in `Core/Src/u_threads.c` uses a CPU busy-wait between -log emissions. See the file header comment there for the full diagnosis. +# Other Important Stuff +- Using cdefmt requires cargo, since the decoder will need to get built on your device. +- It may be helpful to run `ner cdefmt --rebuild` if you run into issues, since this will rebuild the decoder on your device. +- Again, you NEED to give cdefmt its own dedicated UART (or just disable printf()-to-UART routing) for `ner cdefmt` to work. If `ner cdefmt` encounters non-defmt prints when trying to decode, it will fail. \ No newline at end of file From 981b8b98918936c6dd07d95414150355c935582e Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 19:56:45 -0700 Subject: [PATCH 08/12] extra note on the readme --- cdefmt_port/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdefmt_port/README.md b/cdefmt_port/README.md index bd534e3a..fc9a8ace 100644 --- a/cdefmt_port/README.md +++ b/cdefmt_port/README.md @@ -6,7 +6,7 @@ This is a wrapper layer to integrate the `cdefmt` into Embedded-Base. `cdefmt` i To set up CDEFMT logging in an existing project, there are a few steps you need to follow. -First, make sure the project has Embedded-Base as a submodule, on a commit that has cdefmt_port included. +First, make sure the project has Embedded-Base as a submodule, on a commit that has cdefmt_port included. You'll need to run `git submodule update --init --recursive` to make sure the cdefmt library is pulled. Then, add this to the parent project's top-level `CMakeLists.txt`: From 4fb5e82ad7ff9e9770a2f6f9a89279365a071301 Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 21:05:21 -0700 Subject: [PATCH 09/12] rework for cdefmt.py --- cdefmt_port/CMakeLists.txt | 55 +------ cdefmt_port/cdefmt_sections.ld | 37 ----- cdefmt_port/include/config/cdefmt_config.h | 20 +-- ner_environment/build_system/cdefmt.py | 178 ++++++++++++--------- 4 files changed, 110 insertions(+), 180 deletions(-) diff --git a/cdefmt_port/CMakeLists.txt b/cdefmt_port/CMakeLists.txt index 9ebe8cd3..4c5f6802 100644 --- a/cdefmt_port/CMakeLists.txt +++ b/cdefmt_port/CMakeLists.txt @@ -1,33 +1,3 @@ -# ============================================================================= -# Embedded-Base integration layer for the cdefmt logging library -# (https://github.com/RisinT96/cdefmt). -# -# Goal: a parent project should be able to enable cdefmt with no edits to its -# CubeMX-managed linker script or toolchain file. Everything that cdefmt -# requires (compile flag, link flag, extra sections in the ELF, default -# config header) is propagated through a single INTERFACE target. -# -# Usage (from the parent project's CMakeLists.txt): -# -# add_subdirectory(Drivers/Embedded-Base/cdefmt_port) -# target_link_libraries(${CMAKE_PROJECT_NAME} embedded_base_cdefmt) -# -# Then the parent project must: -# 1. Provide an implementation of `void cdefmt_log(const void*, size_t, -# enum cdefmt_level)` in one of its source files. This is the transport -# sink (e.g. UART, ITM/SWO, RTT, ...). -# 2. Place `CDEFMT_GENERATE_INIT();` at file scope exactly once (typically -# in the same source file that implements `cdefmt_log`). -# 3. Call `cdefmt_init()` once after the transport is up, before any logs -# are emitted. -# -# Overriding the default config: -# This package adds `/include/` to the include path, which gives -# `` a sensible default. To override, the parent -# project should arrange for an earlier include directory to provide its -# own `config/cdefmt_config.h`. -# ============================================================================= - cmake_minimum_required(VERSION 3.22) set(_CDEFMT_PORT_DIR ${CMAKE_CURRENT_LIST_DIR}) @@ -40,10 +10,6 @@ if(NOT EXISTS ${_CDEFMT_UPSTREAM_DIR}/CMakeLists.txt) "--recursive`?") endif() -# Pull in upstream cdefmt. Its CMakeLists only builds the example/decoder -# targets when it is the top-level project, so being added as a subdirectory -# is a no-op aside from defining the `cdefmt` INTERFACE library and fetching -# the two header-only Boost dependencies. if(NOT TARGET cdefmt) add_subdirectory( ${_CDEFMT_UPSTREAM_DIR} @@ -67,18 +33,7 @@ target_include_directories(embedded_base_cdefmt INTERFACE ) # Treat cdefmt's header and its Boost.Preprocessor/VMD dependencies as system -# headers for anyone who links `embedded_base_cdefmt`. cdefmt's logging macros -# expand into code that legitimately trips -Wunused-variable, -# -Wdiscarded-qualifiers and -Wunused-function; GCC attributes those warnings to -# the macro body (in cdefmt.h), so promoting these include dirs to `-isystem` -# silences them at the source without touching the parent project's build files -# or suppressing warnings in the parent's own code. -# -# We can't simply mark them SYSTEM here because the include dirs are propagated -# by the upstream `cdefmt`/`Boost::*` targets, which we don't own. Instead we -# copy each target's INTERFACE_INCLUDE_DIRECTORIES into its -# INTERFACE_SYSTEM_INCLUDE_DIRECTORIES, the portable pre-CMake-3.25 way to do -# this (the `SYSTEM` keyword on add_subdirectory/targets needs >= 3.25). +# headers for anyone who links `embedded_base_cdefmt`. foreach(_cdefmt_dep cdefmt Boost::preprocessor Boost::vmd) if(TARGET ${_cdefmt_dep}) # Boost::* are ALIAS targets; set_property() rejects aliases, so resolve @@ -99,13 +54,7 @@ endforeach() # cdefmt's decoder needs DWARF info to parse argument types. target_compile_options(embedded_base_cdefmt INTERFACE -g) -# Link-time requirements: -# --build-id : tag the ELF with a GNU build-id. cdefmt uses it at -# runtime to validate that the binary matches the ELF the -# decoder was given. -# -T : add the `.cdefmt` / `.note.gnu.build-id` sections. This -# does NOT replace the main linker script -- GNU ld merges -# multiple `-T` scripts. +# Link-time requirements target_link_options(embedded_base_cdefmt INTERFACE "LINKER:--build-id" "-T${_CDEFMT_LD_FRAGMENT}" diff --git a/cdefmt_port/cdefmt_sections.ld b/cdefmt_port/cdefmt_sections.ld index 963160f0..99bcf873 100644 --- a/cdefmt_port/cdefmt_sections.ld +++ b/cdefmt_port/cdefmt_sections.ld @@ -1,43 +1,6 @@ /* * cdefmt linker script fragment. * - * This file is consumed by passing `-T ` to the linker, IN - * ADDITION to the project's main linker script. It must NOT need to edit - * the main script (which on STM32 projects is typically regenerated by - * CubeMX on every export). - * - * Why we don't define our own `.note.gnu.build-id` section: - * - * `-Wl,--build-id` (also propagated by the embedded_base_cdefmt target) - * tells ld to synthesise a `.note.gnu.build-id` section automatically. - * When it isn't claimed by any SECTIONS command, ld places it as an - * "orphan" alongside other read-only allocated sections (typically right - * after `.rodata`/`.ARM`). That orphan placement is well-behaved -- it - * doesn't collide with the auto-assigned LMA of the CubeMX-generated - * `.bss` / `._user_heap_stack` sections, which is what would otherwise - * make STM32CubeProgrammer print: - * "File corrupted. Two or more segments define the same memory zone" - * - * An earlier version of this fragment defined `.note.gnu.build-id` - * explicitly and tried to position it with `INSERT BEFORE .data`. That - * does not work with multiple `-T` scripts (ld's INSERT only sees the - * sections defined in the same `-T` file), and the resulting LMA layout - * triggered the warning above. - * - * What this fragment DOES define: - * - * __cdefmt_build_id -- symbol that cdefmt's runtime resolves to find - * the build-id note. PROVIDE'd against the - * orphan-placed `.note.gnu.build-id` section's - * address. - * - * .cdefmt 0 (INFO) -- log metadata (format strings, argument names, - * source locations). Marked (INFO) so it is not - * allocated to a memory region -- it lives in - * the ELF for the host decoder but does NOT - * consume any flash on the device. The base - * address of 0 is what lets the linker assign - * each log a unique ID via its address. */ SECTIONS { diff --git a/cdefmt_port/include/config/cdefmt_config.h b/cdefmt_port/include/config/cdefmt_config.h index b3e3d148..61454a8b 100644 --- a/cdefmt_port/include/config/cdefmt_config.h +++ b/cdefmt_port/include/config/cdefmt_config.h @@ -1,17 +1,3 @@ -/* - * Default cdefmt configuration for projects that consume Embedded-Base's - * `embedded_base_cdefmt` CMake target. - * - * cdefmt's header (`cdefmt/include/cdefmt.h`) does: - * #include - * so this file must live at `/config/cdefmt_config.h`. - * The `embedded_base_cdefmt` target adds the parent directory of `config/` - * to the include path, so this file is picked up automatically. - * - * To override these defaults from a parent project, ensure that an earlier - * include directory provides a different `config/cdefmt_config.h`. - */ - #ifndef CDEFMT_CONFIG_H #define CDEFMT_CONFIG_H @@ -20,12 +6,10 @@ * * Each CDEFMT_* call materializes a packed struct on the caller's stack of * size: sizeof(log_id) + sum(sizeof(args)) + DYNAMIC_SIZE_MAX bytes. Threads - * that log must have enough stack headroom to cover this. For a typical - * Cortex-M project with multi-kB thread stacks, the default 128-byte - * dynamic reservation is comfortable. + * that log must have enough stack headroom to cover this. * ------------------------------------------------------------------------- */ #define CDEFMT_USE_STACK_LOG_BUFFER 1 -#define CDEFMT_STACK_LOG_BUFFER_DYNAMIC_SIZE_MAX 128 +#define CDEFMT_STACK_LOG_BUFFER_DYNAMIC_SIZE_MAX 64 /* --------------------------------------------------------------------------- * Alternative buffer modes (disabled by default). diff --git a/ner_environment/build_system/cdefmt.py b/ner_environment/build_system/cdefmt.py index 4f0db825..9064d335 100644 --- a/ner_environment/build_system/cdefmt.py +++ b/ner_environment/build_system/cdefmt.py @@ -5,15 +5,47 @@ import subprocess import sys from pathlib import Path - +import serial +import serial.tools.list_ports from rich import print -# Paths are resolved relative to the project working directory (i.e. the dir -# from which the user ran `ner cdefmt`). This matches how every other ner -# command works. CDEFMT_REPO_REL = Path("Drivers/Embedded-Base/cdefmt_port/cdefmt") -DECODER_BIN_REL = CDEFMT_REPO_REL / "target" / "release" / "stdin" +_DECODER_NAME = "stdin.exe" if platform.system() == "Windows" else "stdin" # cargo emits `stdin.exe` on Windows, `stdin` everywhere else. +DECODER_BIN_REL = CDEFMT_REPO_REL / "target" / "release" / _DECODER_NAME + +# USB vendor IDs used to rank auto-detected adapters. +_VID_FTDI = 0x0403 # FTDI +_VID_STLINK = 0x0483 # ST-LINK + + +# ------------------------------------------------------------------------------ +# Environment helpers +# ------------------------------------------------------------------------------ + +def _is_wsl() -> bool: + """True when running under WSL, where USB serial devices must be attached + with usbipd before they're visible. Mirrors serial2.py / build_system.py.""" + return (platform.system() == "Linux" + and "microsoft" in platform.uname().release.lower()) + + +def _print_wsl_usbip_hint() -> None: + """Print the same usbipd guidance serial2.py shows when no device is found + under WSL.""" + if not _is_wsl(): + return + print("\n[blue]If you're using WSL, you may need to attach the USB device " + "from a Windows terminal:") + print("1. Open a Windows terminal [bold blue]with admin privileges[/bold blue] " + "and run 'usbipd list'.") + print("2. Find your device (often named 'CMSIS-DAP v2 Interface' or 'USB " + "Serial Device').") + print("3. Note its BUSID and run [bold green]'usbipd bind " + "--busid='[/bold green]. (Skip if already bound.)") + print("4. Then run [bold green]'usbipd attach --wsl=ubuntu --busid " + "'[/bold green].") + print("5. Re-run the command.\n") # ------------------------------------------------------------------------------ @@ -95,68 +127,41 @@ def _find_elf(explicit: str | None) -> Path: # Serial port discovery # ------------------------------------------------------------------------------ -def _list_ports() -> list[str]: - """Return USB serial devices currently visible to the OS. +def _device_priority(port: "serial.tools.list_ports_common.ListPortInfo") -> int: + desc = " ".join(filter(None, [ + port.description, port.product, port.manufacturer, port.interface, + ])).lower() - Linux only for now -- mirrors miniterm.py's coverage. macOS/Windows users - should pass --device explicitly until this grows cross-platform support. - """ - if platform.system() != "Linux": - return [] - return sorted(glob.glob("/dev/ttyACM*") + glob.glob("/dev/ttyUSB*")) + # Prioritize FTDI + if port.vid == _VID_FTDI or "ftdi" in desc: + return 0 + if "cmsis-dap" in desc or "daplink" in desc or "mbed" in desc: + return 1 + if port.vid == _VID_STLINK or "st-link" in desc or "stlink" in desc: + return 3 + return 2 -def _find_port(explicit: str) -> str: - """Return the serial port to use -- explicit if given, else first detected. +def _list_ports() -> list["serial.tools.list_ports_common.ListPortInfo"]: + """Return USB serial devices visible to the OS, preferred adapter first.""" + ports = [p for p in serial.tools.list_ports.comports() if p.vid is not None] + ports.sort(key=lambda p: (_device_priority(p), p.device)) + return ports - The first detected port is typically /dev/ttyACM0 (a Nucleo's ST-LINK VCP), - which is also what `nflash` targets by default. - """ + +def _find_port(explicit: str) -> str: + """Return the serial port to use (if nothing is passed in for `explicit`, auto-detect)""" if explicit: - if not os.path.exists(explicit): - print(f"[bold red]Error:[/bold red] device not found: " - f"[blue]{explicit}[/blue]", file=sys.stderr) - sys.exit(1) return explicit - if platform.system() != "Linux": - print("[bold red]Error:[/bold red] auto-detect port not implemented " - "for this OS; please pass --device.", file=sys.stderr) - sys.exit(1) - ports = _list_ports() if not ports: print("[bold red]Error:[/bold red] no USB serial devices found. Is " "the board plugged in?", file=sys.stderr) + _print_wsl_usbip_hint() sys.exit(1) - return ports[0] - - -# ------------------------------------------------------------------------------ -# TTY setup -# ------------------------------------------------------------------------------ - -def _set_raw_tty(port: str, baud: int) -> None: - """Put the TTY in raw mode so binary cdefmt frames pass through untouched. - - The kernel's default line discipline is `icanon`, which only releases - bytes to read() when a newline arrives. That's catastrophic for a - length-prefixed binary protocol -- the decoder would block waiting for an - 0x0a that may legitimately never appear. `raw` (= !icanon !echo !isig - !iexten ...) plus explicit -icrnl/-ixon disables every transformation - that would mangle binary payload bytes. - """ - if platform.system() != "Linux": - # stty's syntax differs on macOS/BSDs; not implemented yet. - return - - cmd = ["stty", "-F", port, "raw", "-echo", "-ixon", "-icrnl", str(baud)] - result = subprocess.run(cmd) - if result.returncode != 0: - print(f"[bold red]Error:[/bold red] `stty` failed on [blue]{port}" - f"[/blue].", file=sys.stderr) - sys.exit(result.returncode) + return ports[0].device # ------------------------------------------------------------------------------ @@ -165,15 +170,21 @@ def _set_raw_tty(port: str, baud: int) -> None: def main(device: str = "", elf: str | None = None, rebuild: bool = False, ls: bool = False, baud: int = 115200) -> None: - """Invoked by the `ner cdefmt` command. See cdefmt() in build_system.py - for the typer-level option definitions.""" + """Invoked by the `ner cdefmt` command. See cdefmt() in build_system.py.""" if ls: ports = _list_ports() elfs = sorted(glob.glob("build/*.elf")) print("[bold]Serial ports:[/bold]") - for p in (ports or [" (none)"]): - print(f" {p}") + if ports: + for p in ports: + desc = p.description if p.description and p.description != "n/a" else "" + print(f" {p.device}" + (f" [#888888]({desc})[/#888888]" if desc else "")) + print("[#888888] The first port is selected if --device is not " + "given.[/#888888]") + else: + print(" (none)") + _print_wsl_usbip_hint() print("[bold]ELF files under build/:[/bold]") for e in (elfs or [" (none)"]): print(f" {e}") @@ -183,23 +194,46 @@ def main(device: str = "", elf: str | None = None, rebuild: bool = False, elf_path = _find_elf(elf) port = _find_port(device) - _set_raw_tty(port, baud) - print(f"[#cccccc](ner cdefmt):[/#cccccc] decoding [blue]{port}[/blue] " f"against [blue]{elf_path}[/blue] -- Ctrl-C to quit") - # Hand the open port to the decoder as its stdin. Python stays in the - # call stack just long enough to set this up; once subprocess.run returns - # we propagate its exit code so a Ctrl-C-killed decoder is indistinguish- - # able from running the binary directly. + # Open the port with pyserial and pump its bytes into the decoder's stdin. + # The decoder reads a length-prefixed binary stream, so we forward raw chunks as they arrive. + proc = subprocess.Popen([str(decoder), "--elf", str(elf_path)], + stdin=subprocess.PIPE) + interrupted = False try: - with open(port, "rb") as port_fd: - result = subprocess.run( - [str(decoder), "--elf", str(elf_path)], - stdin=port_fd, - ) + with serial.Serial(port, baud, timeout=0.1) as ser: + while proc.poll() is None: + chunk = ser.read(ser.in_waiting or 1) + if not chunk: + continue + try: + proc.stdin.write(chunk) + proc.stdin.flush() + except (BrokenPipeError, OSError): + break # decoder exited, stop forwarding + except serial.SerialException as e: + print(f"[bold red]Error:[/bold red] could not open serial port " + f"[blue]{port}[/blue]: {e}", file=sys.stderr) + _print_wsl_usbip_hint() + proc.terminate() + proc.wait() + sys.exit(1) except KeyboardInterrupt: - # The decoder will have already gotten the SIGINT; exit quietly. - sys.exit(130) + interrupted = True + finally: + if proc.stdin and not proc.stdin.closed: + try: + proc.stdin.close() + except OSError: + pass + + try: + rc = proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.terminate() + rc = proc.wait() - sys.exit(result.returncode) + # Make a Ctrl-C-killed decoder indistinguishable from running it directly. + sys.exit(130 if interrupted else rc) From 477dab8a63be12c4ed1bff273a354c46b47d67aa Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 21:10:33 -0700 Subject: [PATCH 10/12] small cdefmt.py changes --- ner_environment/build_system/cdefmt.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ner_environment/build_system/cdefmt.py b/ner_environment/build_system/cdefmt.py index 9064d335..48dbb410 100644 --- a/ner_environment/build_system/cdefmt.py +++ b/ner_environment/build_system/cdefmt.py @@ -25,14 +25,13 @@ def _is_wsl() -> bool: """True when running under WSL, where USB serial devices must be attached - with usbipd before they're visible. Mirrors serial2.py / build_system.py.""" + with usbipd before they're visible.""" return (platform.system() == "Linux" and "microsoft" in platform.uname().release.lower()) def _print_wsl_usbip_hint() -> None: - """Print the same usbipd guidance serial2.py shows when no device is found - under WSL.""" + """Print hint when no device is found under WSL.""" if not _is_wsl(): return print("\n[blue]If you're using WSL, you may need to attach the USB device " @@ -103,7 +102,7 @@ def _find_decoder(rebuild: bool = False) -> Path: # ------------------------------------------------------------------------------ def _find_elf(explicit: str | None) -> Path: - """Return the ELF to decode against -- explicit path if given, else the + """Return the ELF to decode against explicit path if given, else the most-recently-modified .elf under build/.""" if explicit: p = Path(explicit) From 7117a80652288d473d2923fa9f0332bb35b8bcad Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 21:37:26 -0700 Subject: [PATCH 11/12] revert the F446 change --- platforms/stm32f405/include/stm32xx_hal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/stm32f405/include/stm32xx_hal.h b/platforms/stm32f405/include/stm32xx_hal.h index 15b0dc1f..ddc8843d 100644 --- a/platforms/stm32f405/include/stm32xx_hal.h +++ b/platforms/stm32f405/include/stm32xx_hal.h @@ -1,7 +1,7 @@ #ifndef STM32XX_HAL_H #define STM32XX_HAL_H -#ifdef STM32F446xx +#ifdef STM32F405xx #include "stm32f4xx_hal.h" #endif From 60f43aaedf878166730de82c130ffed4d777391c Mon Sep 17 00:00:00 2001 From: bjackson312006 Date: Sat, 6 Jun 2026 21:39:21 -0700 Subject: [PATCH 12/12] actually just add a F446 separate to F405 --- platforms/stm32f405/include/stm32xx_hal.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platforms/stm32f405/include/stm32xx_hal.h b/platforms/stm32f405/include/stm32xx_hal.h index ddc8843d..b2a23d9a 100644 --- a/platforms/stm32f405/include/stm32xx_hal.h +++ b/platforms/stm32f405/include/stm32xx_hal.h @@ -5,6 +5,10 @@ #include "stm32f4xx_hal.h" #endif +#ifdef STM32F446xx +#include "stm32f4xx_hal.h" +#endif + #ifdef STM32H745xx #include "stm32h7xx_hal.h" #endif