diff --git a/.gitmodules b/.gitmodules index 393784b6..33d2fe06 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_port/cdefmt + url = git@github.com:bjackson312006/cdefmt.git diff --git a/cdefmt_port/CMakeLists.txt b/cdefmt_port/CMakeLists.txt new file mode 100644 index 00000000..4c5f6802 --- /dev/null +++ b/cdefmt_port/CMakeLists.txt @@ -0,0 +1,61 @@ +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() + +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 +) + +# Treat cdefmt's header and its Boost.Preprocessor/VMD dependencies as system +# 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 + # 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) + +# Link-time requirements +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..fc9a8ace --- /dev/null +++ b/cdefmt_port/README.md @@ -0,0 +1,76 @@ +# cdefmt_port + +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 + +## Setting Up cdefmt + +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. 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`: + +```cmake +add_subdirectory(Drivers/Embedded-Base/cdefmt_port) + +target_link_libraries(${CMAKE_PROJECT_NAME} + embedded_base_cdefmt + # ... other libs ... +) +``` + +After that, add the cdefmt routing code to your project's `main.c`: + +```c +#include + +/* 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(); + +/** + * @brief Transport sink invoked by every CDEFMT_* macro. + */ +void cdefmt_log(const void *log, size_t size, enum cdefmt_level level) +{ + (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); +} +``` +(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`.) + +Also, add `cdefmt_init()` to the `main()` function in your `main.c`, after your UART initialization code has ran. + +# Usage + +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."); +``` + +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 +``` + +That macro call produced this log when I ran the code and viewed logging output (via `ner cdefmt`): +``` +[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. + +# 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 diff --git a/cdefmt_port/cdefmt b/cdefmt_port/cdefmt new file mode 160000 index 00000000..e413a412 --- /dev/null +++ b/cdefmt_port/cdefmt @@ -0,0 +1 @@ +Subproject commit e413a412ee5ca620c93e0c2e798c41e095303d5c diff --git a/cdefmt_port/cdefmt_sections.ld b/cdefmt_port/cdefmt_sections.ld new file mode 100644 index 00000000..99bcf873 --- /dev/null +++ b/cdefmt_port/cdefmt_sections.ld @@ -0,0 +1,14 @@ +/* + * cdefmt linker script fragment. + * + */ + +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..61454a8b --- /dev/null +++ b/cdefmt_port/include/config/cdefmt_config.h @@ -0,0 +1,26 @@ +#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. + * ------------------------------------------------------------------------- */ +#define CDEFMT_USE_STACK_LOG_BUFFER 1 +#define CDEFMT_STACK_LOG_BUFFER_DYNAMIC_SIZE_MAX 64 + +/* --------------------------------------------------------------------------- + * 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..48dbb410 --- /dev/null +++ b/ner_environment/build_system/cdefmt.py @@ -0,0 +1,238 @@ +import glob +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path +import serial +import serial.tools.list_ports +from rich import print + + +CDEFMT_REPO_REL = Path("Drivers/Embedded-Base/cdefmt_port/cdefmt") +_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.""" + return (platform.system() == "Linux" + and "microsoft" in platform.uname().release.lower()) + + +def _print_wsl_usbip_hint() -> None: + """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 " + "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") + + +# ------------------------------------------------------------------------------ +# 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 _device_priority(port: "serial.tools.list_ports_common.ListPortInfo") -> int: + desc = " ".join(filter(None, [ + port.description, port.product, port.manufacturer, port.interface, + ])).lower() + + # 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 _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 + + +def _find_port(explicit: str) -> str: + """Return the serial port to use (if nothing is passed in for `explicit`, auto-detect)""" + if explicit: + return explicit + + 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].device + + +# ------------------------------------------------------------------------------ +# 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.""" + + if ls: + ports = _list_ports() + elfs = sorted(glob.glob("build/*.elf")) + print("[bold]Serial ports:[/bold]") + 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}") + return + + decoder = _find_decoder(rebuild=rebuild) + elf_path = _find_elf(elf) + port = _find_port(device) + + print(f"[#cccccc](ner cdefmt):[/#cccccc] decoding [blue]{port}[/blue] " + f"against [blue]{elf_path}[/blue] -- Ctrl-C to quit") + + # 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 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: + 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() + + # Make a Ctrl-C-killed decoder indistinguishable from running it directly. + sys.exit(130 if interrupted else rc) 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