From 50e2efba92dc7746472530234ff48b08e8e71a75 Mon Sep 17 00:00:00 2001 From: Erik Rainey Date: Wed, 12 Aug 2026 22:31:32 -0500 Subject: [PATCH] stm32: program the ST SWO/SWTF trace blocks for H7 SWO (issue-41) Program the ST Serial Wire Output path on the H7 so the Cortex-M7 ITM trace reaches the SWO pin (PB3/TRACESWO). The ARM CoreSight TPIU does not drive the pin on the H7; the ST SWO (0x5C003000) and SWTF funnel (0x5C004000) do. AI (opencode, model big-pickle, 128k context) implemented: - Hand-written peripheralyzer-style Swo.hpp/SwoTraceFunnel.hpp register headers (offsets static_assert'd) + UNITTEST RAM globals + linker PROVIDEs for both debug blocks. - initialize::enable_serial_wire_output() in the H7 debug.cpp: enables the DBGMCU trace port clocks (TRACECLKEN/D1DBGCKEN/D3DBGCKEN), unlocks the SWO/SWTF blocks (0xC5ACCE55), selects Async-NRZ, programs SWO_CODR from TRACECK (= PLL1R, 100 MHz) and enables the SWTF. F4 gets a documented no-op (its standard TPIU drives SWO). - clocks.cpp now computes ClockTree.trace (PLL1R on H7, sysclk on F4) and calls enable_serial_wire_output() from the vendor clocks(): module archives are scanned once before jarnax, so vendor symbols referenced only from configure.cpp are never extracted (same issue as early_power, FIXME at clocks.cpp:38). - BoardContext: PB3 as TRACESWO (AF0, very-high speed, push-pull). - Demo emits a periodic SWO-OK marker on the SWO System port. - 10 new gtest cases over the UNITTEST globals (DBGMCU bits, unlock values, CODR, protocol, ENSO); host llvm + clang pass. Human: board pin bring-up decisions and review. Verified: M4/M7 cross (+ci variants) and host llvm/clang presets build clean; hardware shows DBGMCU_CR=0x00700007, SWO_CODR=0x2b (100 MHz / 2.24 MBaud), SWO_SPPR=2 (AsyncNRZ), SWTF_CTRL=0x301 at cortex::system::main() entry. --- GOTCHAS.md | 39 +++++ PLAN.md | 164 +++++++++++++----- applications/nucleo-demo/source/Demo.cpp | 3 + boards/nucleo_h753zi/include/BoardContext.hpp | 1 + boards/nucleo_h753zi/source/BoardContext.cpp | 5 + modules/cortex/source/initialize.cpp | 2 +- modules/jarnax/source/configure.cpp | 4 + modules/stm32/CMakeLists.txt | 2 + modules/stm32/include/stm32/Initialize.hpp | 16 ++ modules/stm32/include/stm32/h7xx/Swo.hpp | 73 ++++++++ .../include/stm32/h7xx/SwoTraceFunnel.hpp | 64 +++++++ modules/stm32/include/stm32/stm32f4xx.hpp | 1 + modules/stm32/include/stm32/stm32h7xx.hpp | 11 ++ .../linkerscripts/stm32h753zi-sections.ld | 9 +- modules/stm32/source/stm32f4xx/clocks.cpp | 9 + modules/stm32/source/stm32f4xx/debug.cpp | 21 +++ modules/stm32/source/stm32h7xx/clocks.cpp | 14 +- modules/stm32/source/stm32h7xx/debug.cpp | 46 +++++ .../stm32/source/stm32h7xx/peripherals.cpp | 2 + modules/stm32/tests/CMakeLists.txt | 16 ++ modules/stm32/tests/gtest-stm32-debug.cpp | 117 +++++++++++++ 21 files changed, 570 insertions(+), 49 deletions(-) create mode 100644 modules/stm32/include/stm32/h7xx/Swo.hpp create mode 100644 modules/stm32/include/stm32/h7xx/SwoTraceFunnel.hpp create mode 100644 modules/stm32/source/stm32f4xx/debug.cpp create mode 100644 modules/stm32/source/stm32h7xx/debug.cpp create mode 100644 modules/stm32/tests/gtest-stm32-debug.cpp diff --git a/GOTCHAS.md b/GOTCHAS.md index 7a1b2b6..ca9bcf3 100644 --- a/GOTCHAS.md +++ b/GOTCHAS.md @@ -1,5 +1,44 @@ # Gotchas +## 2026-08-12 — Vendor symbols referenced only from `configure.cpp` do not link (archive ordering) + +- **Symptom:** `undefined reference to stm32::initialize::enable_serial_wire_output(core::units::Hertz, unsigned long)` + at link time for **all** F4 apps AND all M7 apps that use jarnax `configure.cpp`, even though + `nm` shows the symbol IS defined (`T`) in `libmodule-stm32-basic-.a` and the mangled name + matches (`Em` = unsigned long = both `std::uint32_t` and `std::size_t` on ARM32). +- **Root cause:** module static-library archives are placed **once**, *before* the jarnax archive, + on the link line (`libmodule-stm32-...a libarch-cortex-...a libmodule-jarnax-...a libarch-cortex-...a libmodule-jarnax-...a`). + When ld scans the stm32 archive nothing has referenced the symbol yet, so the member is never + extracted; by the time jarnax's reference appears the archive has passed. `-Wl,--no-undefined` + then fails the link. (cortex+jarnax are repeated twice on the link line for exactly this reason.) +- **Pre-existing example:** the `@FIXME` at `modules/stm32/source/stm32h7xx/clocks.cpp:38` documents + the same problem for `early_power()`: it has to be called from inside the vendor's `clocks()` + rather than from `configure.cpp`. +- **Fix / rule:** any vendor symbol referenced only from `configure.cpp` must be *pulled in* by + an already-extracted member — call it from the vendor `clocks()` (guarded by + `if constexpr (cortex::swo::enable)`), not from `configure.cpp`. Do not rely on the board + archive happening to reference it. +- **Gotcha:** a misleading "M7 passes" check — building only a module or a configure stage and + grepping for `FAILED` can miss that the app ELFs never linked. Verify app `.elf` artifacts + actually exist / contain the symbol. + +## 2026-08-12 — J-Link rewrites DBGMCU_CR on connect, unclocking the D3 debug domain + +- **Symptom:** after the firmware programs the ST SWO/SWTF blocks (0x5C003000/0x5C004000), a + fresh J-Link session reads `DBGMCU_CR` = `0x00000007` (bits 0-2 = J-Link's own + DBG_SLEEP/DBG_STOP/DBG_STANDBY) and the SWO/SWTF registers are unreadable + (`Could not start CPU core. (ErrorCode: -1)`), making it look like the firmware never + programmed them. +- **Root cause:** each pylink-square-mcp tool call is a new J-Link connection; on connect/reset + the J-Link writes `DBGMCU_CR` with its low-power debug bits (0x07), clobbering + TRACECLKEN/D1DBGCKEN/D3DBGCKEN (bits 20/21/22). With those clear, the D3 debug domain is + unclocked, so SWO/SWTF reads hang. +- **Fix / verification pattern:** verify in a **single session** — set a hardware breakpoint at a + post-configure symbol (e.g. `cortex::system::main()`) via `pylink-square-mcp test_breakpoint` + and dump the registers when it hits, before any reconnect. Verified values for issue-41: + `DBGMCU_CR`=0x00700007, `SWO_CODR`=0x2b (43 @100MHz/2.24MBaud), `SWO_SPPR`=2 (AsyncNRZ), + `SWTF_CTRL`=0x301 (ENSO). + ## 2026-08-11 — J-Link Remote Server has no `-if/-device/-speed` options - **Symptom:** Launching the backend with `JLinkRemoteServer -if SWD -device STM32H753ZI -speed auto` diff --git a/PLAN.md b/PLAN.md index 3fcb3f8..cd8fbe5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,59 +1,131 @@ -# PLAN: Route pylink-square-mcp through a persistent J-Link Remote Server +# PLAN: Enable Serial Wire Output (SWO) for debug tracing -Issue: #52 — branch `issue-52` (tracks `github/develop`). +Issue: #41 — branch `issue-41` (tracks `github/develop`). ## Summary -The `tools/pylink-square-mcp` J-Link MCP server crashes periodically. Every -tool call created a fresh `pylink.JLink()`, opened the USB link, connected, -and discarded it — repeated J-Link DLL load/unload + USB connect/disconnect -churn inside a long-lived process. Fix: connect through a persistent local -**J-Link Remote Server** (default `127.0.0.1:19020`), and have -`mcp_server.py` launch that daemon on startup if it isn't already listening. -The daemon intentionally outlives the MCP process so USB stays stable across -MCP crashes/restarts. +On the H7 the Cortex-M7 TPIU (`0xE0040000`) does NOT drive the SWO pin. The trace +path is ITM → SWTF (`0x5C004000`) → SWO (`0x5C003000`) → PB3 (`TRACESWO`, AF0), +and the ST SWO block is driven from TRACECK. This change programs those ST blocks +and un-gates the debug clocks: + +1. `DBGMCU_CR`: `TRACECLKEN` (bit 20), `D1DBGCKEN` (bit 21), `D3DBGCKEN` (bit 22). +2. `SWO` (`0x5C003000`): unlock via LAR, Async-NRZ mode (`SWO_SPPR`=2), baud + prescaler (`SWO_CODR`, zero-based: `prescaler = trace_clock / baud - 1`). +3. `SWTF` (`0x5C004000`): unlock via LAR, `ENSO=1` to forward the ITM trace bus. +4. TRACECK follows `RCC_CFGR[SW]`; with SYSCLK = PLL1P, TRACECK = PLL1R. Board + config gives VCO 800 MHz, `pll_r=8-1` → TRACECK = **100 MHz** → CODR = 43 for + the 2.24 MBaud `basic` config. + +The F4 has no ST SWO block (standard TPIU drives the pin), so its +`enable_serial_wire_output` is a documented no-op and `ClockTree.trace = sysclk`. + +## Verified facts + +- SWO/SWTF register layout per RM0433 Rev 8 §35.4; LAR unlock key `0xC5ACCE55` + (`SWO_LAR`@0x5C003FB0, `SWTF_LAR`@0x5C004FB0). Reset values: SWTF_CTRL 0x300 + (bit 0 `ENSO` off), SWO_SWTF_PRIORITY 0x8. +- `DBGMCU_CR` field names confirmed from SVD: `trace_clock_enable` (bit 20), + `domain1_debug_clock_enable` (bit 21), `domain3_debug_clock_enable` (bit 22). + The D3 domain holds the SWO/SWTF blocks, so D3DBGCKEN must be set. +- PB3 = `JTDO/TRACESWO`, AF0 (datasheet stm32h753zi.txt line 6873), unused by any + board pin. +- The SVD has no RCC trace-clock mux field on the H7 — TRACECK is derived from + SYSCLK selection; the STM32H753.svd has no SWO/SWTF description, so the headers + were hand-written in the peripheralyzer style (per-register struct with + `Fields bits` + `uint32_t whole`, `static_assert` on size/offsets, + `sizeof(Swo)==sizeof(SwoTraceFunnel)==0xFB4`). +- **Link ordering**: module archives are scanned once, before the jarnax archive. + Vendor symbols referenced only from `configure.cpp` do not link (same problem + documented at `clocks.cpp:38` for `early_power`). The SWO call therefore lives + in the vendor `clocks()`; `configure.cpp` keeps only `cortex::initialize::swo()` + (TPIU/ITM, which is in the repeated cortex archive). ## Changes -1. **`jlink_connection.py` (new)** — shared helper: - - `add_connection_args(parser)`: adds `--remote-host` (default - `127.0.0.1`), `--remote-port` (default `19020`), `--direct`. - - `connect(device, speed, remote_host, remote_port, interface=SWD)`: opens - via `jlink.open(ip_addr="host:port")`; falls back to a direct USB - connection (with a stderr warning) if the Remote Server is unreachable. - - Device/speed stay client-side in `jlink.connect()`; the Remote Server - only needs `-Port` (and optionally `-USB `) per - https://kb.segger.com/J-Link_Remote_Server. -2. **All 13 J-Link tool scripts** — import the shared helper, register the - connection args, and replace the inline open/connect block with - `connect(...)` (backtrace, clock_tree, debug_target, dump_ethernet, - dump_memory, flash_target, flash_verify, live_dump, rtt_read, run_for, - run_to_main, step_target, test_breakpoint). `svd_query` is SVD-only, no - J-Link. -3. **`mcp_server.py`** — on startup: - - parse `--device/--speed/--remote-port/--usb-serial` (env fallbacks - `JLINK_MCP_*`); - - `ensure_remote_server()`: if nothing is listening on the port, locate - `JLinkRemoteServer` and spawn `-Port [-USB ]` with output - to `jlink-remote-server.log`; poll for the listener; never kill on exit; - - every J-Link tool handler appends `--remote-host/--remote-port` so all - calls route through the daemon. -4. **`.gitignore`** — ignore the Remote Server log artifacts. -5. **README.md** — document the backend, options/env vars, config example. +1. **`modules/stm32/include/stm32/h7xx/Swo.hpp`** (new) — `Swo` peripheral at + 0x5C003000: `current_output_divisor` (`CODR`@0x010, 13-bit `prescaler`), + `selected_pin_protocol` (`SWO_SPPR`@0x0F0), `lock_access` (`SWO_LAR`@0xFB0). +2. **`modules/stm32/include/stm32/h7xx/SwoTraceFunnel.hpp`** (new) — `SwoTraceFunnel` + at 0x5C004000: `control` (`SWTF_CTRL`@0x000, bit 0 `enable_swo`), + `priority` (`SWTF_PRIORITY`@0x004), `lock_access` (`SWTF_LAR`@0xFB0). +3. **`stm32h7xx.hpp`** — include both new headers; add externs + `serial_wire_output` / `swo_trace_funnel` beside `debug`. +4. **`source/stm32h7xx/peripherals.cpp`** — UNITTEST RAM globals for the two new + peripherals (same pattern as `debug`). +5. **`modules/stm32/linkerscripts/stm32h753zi-sections.ld`** — PROVIDE + `_stm32_swo = 0x5C003000`, `_stm32_swo_trace_funnel = 0x5C004000` (mangled + extern names also PROVIDEd). +6. **`modules/stm32/include/stm32/Initialize.hpp`** — declare + `enable_serial_wire_output(core::units::Hertz trace_clock, std::uint32_t baud)`. +7. **`source/stm32h7xx/debug.cpp`** (new) — `enable_trace_port_clock()` (sets bits + 20/21/22, read-modify-write, idempotent) and `enable_serial_wire_output()`: + unlock LARs, `transmit_mode = AsyncNRZ`, masked CODR write (whole register to + dodge GCC `-Wconversion` on the 13-bit field), `SWTF_CTRL.ENSO = 1`. +8. **`source/stm32f4xx/debug.cpp`** (new) — no-op implementations of both + functions (added to the F4 CMakeLists sources). +9. **`source/stm32h7xx/clocks.cpp`** — `clock_tree.trace = pll_vco / (pll_r + 1)`; + at the end, `if constexpr (cortex::swo::enable)` + `enable_serial_wire_output(trace, baud)` (cast to `std::uint32_t`). Called here, + not from configure.cpp, for the link-ordering reason above. +10. **`source/stm32f4xx/clocks.cpp`** + **`stm32f4xx.hpp`** — `ClockTree.trace = + sysclk`; same guarded no-op call. +11. **`modules/jarnax/source/configure.cpp`** — SWO vendor call removed; comment + explains why it lives in the vendor `clocks()`. `cortex::initialize::swo()` kept. +12. **`modules/cortex/source/initialize.cpp`** — fix stale black-magic SWO URL + comment. +13. **`boards/nucleo_h753zi/**`** — PB3 `swo_pin_` configured AF0 in `Initialize()` + (matches existing AF pin pattern). +14. **`applications/nucleo-demo/source/Demo.cpp`** — TEMP trace-capture + instrumentation removed; SWO marker emit + `cortex/swo.hpp` include retained. + +## Tests + +- **`modules/stm32/tests/gtest-stm32-debug.cpp`** (new, registered in + `modules/stm32/tests/CMakeLists.txt` as `host_unit_test(NAME stm32-debug ... + BOARDS nucleo_h753zi CONFIGURATIONS basic)`): 10 tests over the UNITTEST + globals — Empty setup/teardown check; D3DBGCKEN set (bit 22); idempotence; + unrelated CR bits preserved by RMW; LAR unlock values; AsyncNRZ protocol; + CODR = 24 for 100 MHz/4 MBaud and 43 for 100 MHz/2.24 MBaud; ENSO enabled. + All pass on host (llvm + clang). +- The register writes are hardware init; host coverage is register-level via the + UNITTEST globals. Future emulator-based testing will exercise the end-to-end + trace path (ITM → SWTF → SWO → pin). ## Verification -- `python3 -m py_compile` on all touched scripts. -- Live hardware test through the daemon: `debug_target`, `run_for --seconds 2`, - `rtt_read --continuous 4` all connected/ran/read correctly via - `127.0.0.1:19020`. -- MCP startup twice: first run launched the daemon; second run detected the - existing listener ("already listening"). -- No C++ source touched; firmware build presets unaffected (CI still validates). +- Host: `cmake --workflow --preset on-host-native-llvm` and `-clang` — all pass. +- Cross: `cmake --workflow --preset on-target-cortex-m4-gcc-arm-none-eabi` and + `-cortex-m7-gcc-arm-none-eabi` — all link and build clean. +- Live hardware (DONE): flashed `firmware-nucleo-demo-basic-nucleo_h753zi.elf`, + reset+ran to `cortex::system::main()`. Single-session breakpoint dump at main + entry (avoiding the J-Link's DBGMCU_CR clobber on connect) showed: + - `DBGMCU_CR` = 0x00700007 (TRACECLKEN/D1DBGCKEN/D3DBGCKEN set; low bits are + the J-Link's own DBG_SLEEP/STOP/STANDBY). + - `SWO_CODR` = 0x2b (43) → 100 MHz / 2.24 MBaud. + - `SWO_SPPR` = 2 → AsyncNRZ (UART). + - `SWTF_CTRL` = 0x301 → reset 0x300 + ENSO. + - A J-Link reconnect between sessions rewrites DBGMCU_CR to 0x07 (unclocks the + D3 debug domain, making SWO/SWTF reads fail) — the firmware writes are only + visible in a single session, which is why the in-session breakpoint dump was + needed. +- PB3 waveform capture deferred: the tooling to read SWO bytes from the J-Link + probe does not exist yet (issue #54). ## Gotchas -- `JLinkRemoteServer` on macOS is a symlink to `JLinkRemoteServerCLExe`; its - accepted options are `-Port`, `-USB`, `-IP` — there is no `-if/-device/-speed` - (those are client-side). -- Do NOT put the probe's real serial number in commits/examples/code. +- Do NOT enable the H7 DBGMCU/SWO in a shared TU: F4 must not link H7 + `debug.cpp` (references `stm32::h7xx::debug`). The `add_module` CHIPS split + handles this; F4 has its own no-op `debug.cpp`. +- **Archive ordering**: vendor symbols referenced only from `configure.cpp` are + never extracted (single scan before jarnax). Keep vendor-init calls inside the + vendor module (see the `early_power` FIXME at `clocks.cpp:38`). +- `std::size_t` → `std::uint32_t` for the baud needs an explicit cast on host + builds (`-Wconversion -Werror`); identical types on ARM32. +- CODR is a 13-bit field; write the whole register with `& 0x1FFFU` to avoid + `-Wconversion` on the truncated bitfield. +- `clocks(ClockConfiguration const&)` early-returns when SWS already = PLL (warm + boot), so the SWO programming only runs on cold boots; DBGMCU/SWO/SWTF + registers survive system reset, so this is acceptable. +- Deferred to issue #55: cold-POR boot hard-fault (CFSR IMPREISERR, HFSR + 0xC0000000) observed in `on_startup()` before `configure()`. diff --git a/applications/nucleo-demo/source/Demo.cpp b/applications/nucleo-demo/source/Demo.cpp index 8d6db17..65b5284 100644 --- a/applications/nucleo-demo/source/Demo.cpp +++ b/applications/nucleo-demo/source/Demo.cpp @@ -2,6 +2,7 @@ #include "BoardContext.hpp" #include "board.hpp" #include "core/Conversions.hpp" +#include "cortex/swo.hpp" #include "jarnax/Assertion.hpp" #include "memory.h" @@ -119,6 +120,8 @@ void Demo::OnCycle(DemoState state) { } if (countdown_.IsExpired()) { + // TEMP: issue-41 SWO verification - emit a marker every 2 seconds + cortex::swo::emit(cortex::swo::Port::System, "SWO-OK\r\n"); jarnax::Ticks ticks = ticker_.GetTicksSinceBoot(); jarnax::Time time = ticker_.GetTimeSinceBoot(); uint32_t random = rng_.GetNextRandom(); diff --git a/boards/nucleo_h753zi/include/BoardContext.hpp b/boards/nucleo_h753zi/include/BoardContext.hpp index dc6a473..a6f7db9 100644 --- a/boards/nucleo_h753zi/include/BoardContext.hpp +++ b/boards/nucleo_h753zi/include/BoardContext.hpp @@ -108,6 +108,7 @@ class BoardContext { stm32::gpio::Pin status_pin_; ///< The Status Pin stm32::gpio::Pin performance_pin_; ///< The Performance Pin stm32::gpio::Pin timing_pin_; ///< The Timing Pin (for debugging) + stm32::gpio::Pin swo_pin_; ///< The Serial Wire Output (SWO) Pin (PB3) /// The Error Indicator stm32::Indicator error_indicator_; /// The Status Indicator diff --git a/boards/nucleo_h753zi/source/BoardContext.cpp b/boards/nucleo_h753zi/source/BoardContext.cpp index 34152bd..beda495 100644 --- a/boards/nucleo_h753zi/source/BoardContext.cpp +++ b/boards/nucleo_h753zi/source/BoardContext.cpp @@ -122,6 +122,7 @@ BoardContext::BoardContext() , status_pin_{stm32::gpio::Port::B, 0} // LD1 Green LED , performance_pin_{stm32::gpio::Port::E, 1} // LED2 Yellow LED , timing_pin_{stm32::gpio::Port::A, 3} // GPIO + , swo_pin_{stm32::gpio::Port::B, 3} // SWO (JTDO/TRACESWO) , error_indicator_{error_pin_, stm32::Level::High} , status_indicator_{status_pin_, stm32::Level::High} , performance_indicator_{performance_pin_, stm32::Level::High} @@ -184,6 +185,10 @@ core::Status BoardContext::Initialize(void) { .SetOutputSpeed(stm32::gpio::Speed::Medium) .SetOutputType(stm32::gpio::OutputType::PushPull) .SetResistor(stm32::gpio::Resistor::PullDown); + swo_pin_.SetMode(stm32::gpio::Mode::AlternateFunction) + .SetAlternative(0) // Alt 0 is TRACESWO (JTDO) + .SetOutputSpeed(stm32::gpio::Speed::VeryHigh) + .SetOutputType(stm32::gpio::OutputType::PushPull); error_indicator_.Inactive(); status_indicator_.Inactive(); performance_indicator_.Inactive(); diff --git a/modules/cortex/source/initialize.cpp b/modules/cortex/source/initialize.cpp index 35a0217..dbf960d 100644 --- a/modules/cortex/source/initialize.cpp +++ b/modules/cortex/source/initialize.cpp @@ -263,7 +263,7 @@ void swo(std::uint32_t desired_baud, Hertz clock_frequency) { if constexpr (not cortex::swo::enable) { return; } - // @see https://black-magic.org/usage/swo.html + // @see https://black-magic.org/docs_v1/usage/swo/ // compute the clock divider as a zero based value std::uint32_t clock_divider = (clock_frequency.value() / desired_baud) - 1U; diff --git a/modules/jarnax/source/configure.cpp b/modules/jarnax/source/configure.cpp index 0cc9410..3b1e81e 100644 --- a/modules/jarnax/source/configure.cpp +++ b/modules/jarnax/source/configure.cpp @@ -48,6 +48,10 @@ void ATTRIBUTE((used)) configure(void) { "Initializing Tick @ %" PRIu32 "/sec from %" PRIu32 "\r\n", cortex::GetTickRate().value(), cortex::GetSystemClockFrequency().value() ); cortex::initialize::tick(cortex::GetTickRate(), cortex::GetSystemClockFrequency()); + // The H7 SWO block is driven from TRACECK (PLL1R), not the core clock, so the vendor + // programs the ST SWO/SWTF blocks from its own clocks() (like early_power, because the + // module archive is only scanned once before jarnax, so vendor symbols referenced only + // here would not link). cortex::initialize::swo(cortex::swo::baudrate, cortex::GetClockFrequency()); // configure the system control block configuration register cortex::initialize::configuration(); diff --git a/modules/stm32/CMakeLists.txt b/modules/stm32/CMakeLists.txt index b5cd1bc..dd008aa 100644 --- a/modules/stm32/CMakeLists.txt +++ b/modules/stm32/CMakeLists.txt @@ -3,6 +3,7 @@ add_module(NAME stm32 ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32f4xx/vectors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32f4xx/power.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32f4xx/clocks.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32f4xx/debug.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/gpio/Pin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/Conversions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/Indicator.cpp @@ -33,6 +34,7 @@ add_module(NAME stm32 ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32h7xx/vectors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32h7xx/power.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32h7xx/clocks.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/source/stm32h7xx/debug.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/gpio/Pin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/Conversions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/Indicator.cpp diff --git a/modules/stm32/include/stm32/Initialize.hpp b/modules/stm32/include/stm32/Initialize.hpp index 6bf9365..1c6afc5 100644 --- a/modules/stm32/include/stm32/Initialize.hpp +++ b/modules/stm32/include/stm32/Initialize.hpp @@ -4,6 +4,10 @@ /// @file /// STM32 Chip Initialization Functions +#include + +#include "core/Units.hpp" + /// The ST Micro 32 Namespace namespace stm32 { @@ -16,6 +20,18 @@ void early_power(void); /// @brief The required Clock initialization for all STM32 chips void clocks(void); +/// @brief Enables the DBGMCU trace port clocks (TRACECLKEN/D1DBGCKEN/D3DBGCKEN) so the trace +/// blocks can be clocked. +/// @note On the H7 this also un-gates the D3 domain which contains the SWO/SWTF blocks. +void enable_trace_port_clock(void); + +/// @brief Programs the ST Serial Wire Output path (SWO + SWTF) for UART trace on the SWO pin. +/// @param trace_clock The frequency of the trace clock (TRACECK) driving the SWO block. +/// @param baud The desired SWO UART baud rate. +/// @note H7 only: the Cortex-M7 TPIU (0xE0040000) does not drive the SWO pin, the ST SWO block +/// (0x5C003000) does. The F4 implementation is a no-op as it uses the standard TPIU instead. +void enable_serial_wire_output(core::units::Hertz trace_clock, std::uint32_t baud); + /// @brief The required GPIO initialization /// @note Implemented in the Board extern void gpio(void); diff --git a/modules/stm32/include/stm32/h7xx/Swo.hpp b/modules/stm32/include/stm32/h7xx/Swo.hpp new file mode 100644 index 0000000..bace037 --- /dev/null +++ b/modules/stm32/include/stm32/h7xx/Swo.hpp @@ -0,0 +1,73 @@ + +#ifndef STM32_H7XX_SWO__HPP_ +#define STM32_H7XX_SWO__HPP_ + +/// @file +/// Hand-written structure definitions for the Serial Wire Output (SWO) trace block. +/// @note RM0433 Rev 8, Section 35.4 (SWO trace block) + Table 605. The block is memory mapped +/// at 0x5C003000 on the APB-D debug bus of the STM32H753. It is NOT the ARM CoreSight TPIU +/// (0xE0040000 alias / 0x5C015000 on the H7) which the Cortex-M7 reference peripheral maps to. + +#include +#include +#include + +namespace stm32 { +namespace h7xx { + +/// The Serial Wire Output (SWO) trace block. +/// @details Transmits the trace stream (fed in by the SWO Trace Funnel) on the SWO pin in +/// Manchester or UART (Async NRZ) mode. +struct Swo final { + /// The trace protocol on the SWO pin (SWO_SPPR) + enum class Protocol : std::uint32_t { + Reserved = 0U, ///< Reserved + AsyncManchester = 1U, ///< Asynchronous Manchester encoded + AsyncNRZ = 2U, ///< Asynchronous NRZ encoded (UART) + }; + + /// SWO Current Output Divisor register (SWO_CODR) at offset 0x010. + /// @details The divider is zero based: the baud rate is the trace clock frequency divided by + /// (prescaler + 1). + struct CurrentOutputDivisor final { + struct Fields { + std::uint32_t prescaler : 13U; ///< Divider value for the trace clock (zero based) + std::uint32_t : 19U; ///< Reserved field + }; + union { + Fields bits; + std::uint32_t whole; + }; + }; + + /// SWO Selected Pin Protocol register (SWO_SPPR) at offset 0x0F0. + struct SelectedPinProtocol final { + struct Fields { + Protocol transmit_mode : 2U; ///< Trace protocol selection + std::uint32_t : 30U; ///< Reserved field + }; + union { + Fields bits; + std::uint32_t whole; + }; + }; + + std::uint32_t _reserved0[4]; ///< Offset 0x000 to 0x00C + CurrentOutputDivisor current_output_divisor; ///< Offset 0x010 (SWO_CODR) + std::uint32_t _reserved1[55]; ///< Offset 0x014 to 0x0EC + SelectedPinProtocol selected_pin_protocol; ///< Offset 0x0F0 (SWO_SPPR) + std::uint32_t _reserved2[943]; ///< Offset 0x0F4 to 0xFAC + std::uint32_t lock_access; ///< Offset 0xFB0 (SWO_LAR) +}; +static_assert(std::is_standard_layout::value, "Must be standard layout"); +static_assert(offsetof(Swo, current_output_divisor) == 0x010, "Must be at this offset"); +static_assert(offsetof(Swo, selected_pin_protocol) == 0x0F0, "Must be at this offset"); +static_assert(offsetof(Swo, lock_access) == 0xFB0, "Must be at this offset"); +static_assert(sizeof(Swo) == 0xFB4, "Must be this exact size"); + +/// The value to write to the SWO_LAR register to unlock the SWO block (write 0xC5ACCE55). +constexpr static std::uint32_t SwoLockValue = 0xC5ACCE55U; + +} // namespace h7xx +} // namespace stm32 +#endif // STM32_H7XX_SWO__HPP_ diff --git a/modules/stm32/include/stm32/h7xx/SwoTraceFunnel.hpp b/modules/stm32/include/stm32/h7xx/SwoTraceFunnel.hpp new file mode 100644 index 0000000..8fd3cc2 --- /dev/null +++ b/modules/stm32/include/stm32/h7xx/SwoTraceFunnel.hpp @@ -0,0 +1,64 @@ + +#ifndef STM32_H7XX_SWO_TRACE_FUNNEL__HPP_ +#define STM32_H7XX_SWO_TRACE_FUNNEL__HPP_ + +/// @file +/// Hand-written structure definitions for the SWO Trace Funnel (SWTF) trace block. +/// @note RM0433 Rev 8, Section 35.4 (SWO trace funnel SWTF). The block is memory mapped +/// at 0x5C004000 on the APB-D debug bus of the STM32H753. It forwards the ATB trace bus +/// from the Cortex-M7 ITM to the SWO block, and must be programmed before trace is enabled. + +#include +#include +#include + +namespace stm32 { +namespace h7xx { + +/// The SWO Trace Funnel (SWTF) trace block. +struct SwoTraceFunnel final { + /// SWTF Control register (SWTF_CTRL) at offset 0x000. + /// @details Reset value 0x0000 0300 (MIN_HOLD_TIME = 3). + struct Control final { + struct Fields { + std::uint32_t enable_swo : 1U; ///< Enable the trace bus from the Cortex-M7 ITM to the SWO block (ENSO) + std::uint32_t : 7U; ///< Reserved field + std::uint32_t minimum_hold_time : 4U; ///< Minimum hold time of the ATB bus (MIN_HOLD_TIME) + std::uint32_t : 20U; ///< Reserved field + }; + union { + Fields bits; + std::uint32_t whole; + }; + }; + + /// SWTF Priority register (SWTF_PRIORITY) at offset 0x004. + /// @details Reset value 0x0000 0008. + struct Priority final { + struct Fields { + std::uint32_t port_priority : 3U; ///< ATB bus priority (PRIPORTO) + std::uint32_t : 29U; ///< Reserved field + }; + union { + Fields bits; + std::uint32_t whole; + }; + }; + + Control control; ///< Offset 0x000 (SWTF_CTRL) + Priority priority; ///< Offset 0x004 (SWTF_PRIORITY) + std::uint32_t _reserved0[1002]; ///< Offset 0x008 to 0xFAC + std::uint32_t lock_access; ///< Offset 0xFB0 (SWTF_LAR) +}; +static_assert(std::is_standard_layout::value, "Must be standard layout"); +static_assert(offsetof(SwoTraceFunnel, control) == 0x000, "Must be at this offset"); +static_assert(offsetof(SwoTraceFunnel, priority) == 0x004, "Must be at this offset"); +static_assert(offsetof(SwoTraceFunnel, lock_access) == 0xFB0, "Must be at this offset"); +static_assert(sizeof(SwoTraceFunnel) == 0xFB4, "Must be this exact size"); + +/// The value to write to the SWTF_LAR register to unlock the SWTF block (write 0xC5ACCE55). +constexpr static std::uint32_t SwoTraceFunnelLockValue = 0xC5ACCE55U; + +} // namespace h7xx +} // namespace stm32 +#endif // STM32_H7XX_SWO_TRACE_FUNNEL__HPP_ diff --git a/modules/stm32/include/stm32/stm32f4xx.hpp b/modules/stm32/include/stm32/stm32f4xx.hpp index d7f30bc..fdfa58a 100644 --- a/modules/stm32/include/stm32/stm32f4xx.hpp +++ b/modules/stm32/include/stm32/stm32f4xx.hpp @@ -414,6 +414,7 @@ struct ClockTree { Hertz usbotg; ///< The frequency of the USB OTG clock (from PLL48CK) Hertz apb1_timer_clk; ///< The frequency of the TIMx clocks (TIMxCLK) Hertz apb2_timer_clk; ///< The frequency of the TIMx clocks (TIMxCLK) + Hertz trace; ///< The frequency of the trace clock (the core clock on F4) }; /// @brief The STM32F40xx clock configuration. diff --git a/modules/stm32/include/stm32/stm32h7xx.hpp b/modules/stm32/include/stm32/stm32h7xx.hpp index 92c839f..bbd85a5 100644 --- a/modules/stm32/include/stm32/stm32h7xx.hpp +++ b/modules/stm32/include/stm32/stm32h7xx.hpp @@ -18,6 +18,8 @@ #include "stm32/h7xx/RandomNumberGenerator.hpp" #include "stm32/h7xx/ResetAndClockControl.hpp" #include "stm32/h7xx/SerialPeripheralInterface.hpp" +#include "stm32/h7xx/Swo.hpp" +#include "stm32/h7xx/SwoTraceFunnel.hpp" #include "stm32/h7xx/SystemConfiguration.hpp" #include "stm32/h7xx/Timer2.hpp" #include "stm32/h7xx/UniversalAsynchronousReceiverTransmitter.hpp" @@ -101,6 +103,14 @@ extern SerialPeripheralInterface volatile spi5; /// @note The address of the peripheral is set either by the linkerscript or by a unit test. extern SerialPeripheralInterface volatile spi6; +/// The external volatile serial_wire_output which is a memory mapped register or peripheral. +/// @note The address of the peripheral is set either by the linkerscript or by a unit test. +extern Swo volatile serial_wire_output; + +/// The external volatile swo_trace_funnel which is a memory mapped register or peripheral. +/// @note The address of the peripheral is set either by the linkerscript or by a unit test. +extern SwoTraceFunnel volatile swo_trace_funnel; + /// The external volatile timer2 which is a memory mapped register or peripheral. /// @note The address of the peripheral is set either by the linkerscript or by a unit test. extern Timer2 volatile timer2; @@ -489,6 +499,7 @@ struct ClockTree { Hertz rtc; Hertz rng; Hertz usbotg; + Hertz trace; ///< The frequency of the Trace clock }; /// @brief The STM32H7xxx clock configuration. diff --git a/modules/stm32/linkerscripts/stm32h753zi-sections.ld b/modules/stm32/linkerscripts/stm32h753zi-sections.ld index 48749a5..f36f755 100644 --- a/modules/stm32/linkerscripts/stm32h753zi-sections.ld +++ b/modules/stm32/linkerscripts/stm32h753zi-sections.ld @@ -210,6 +210,11 @@ is not mapped on STM32H7. */ _stm32_debug = 0x5C001000; + /* SWO and SWTF are ST debug blocks on the APB-D bus. They are NOT the ARM + CoreSight TPIU, so the core must program these addresses directly. */ + _stm32_swo = 0x5C003000; + _stm32_swo_trace_funnel = 0x5C004000; + /* Provide the C++ mangled names */ PROVIDE(_ZN6cortex11peripherals27instruction_trace_macrocellE = _cortex_instruction_trace_macrocell); PROVIDE(_ZN6cortex11peripherals20data_watch_and_traceE = _cortex_debug_watch_and_trace); @@ -224,4 +229,6 @@ PROVIDE(_ZN6cortex11peripherals12itcm_controlE = _cortex_itcm_control); PROVIDE(_ZN6cortex11peripherals12dtcm_controlE = _cortex_dtcm_control); PROVIDE(_ZN6cortex11peripherals25trace_port_interface_unitE = _cortex_trace_port_inferface_unit); - PROVIDE(_ZN5stm324h7xx5debugE = _stm32_debug); \ No newline at end of file + PROVIDE(_ZN5stm324h7xx5debugE = _stm32_debug); + PROVIDE(_ZN5stm324h7xx18serial_wire_outputE = _stm32_swo); + PROVIDE(_ZN5stm324h7xx16swo_trace_funnelE = _stm32_swo_trace_funnel); \ No newline at end of file diff --git a/modules/stm32/source/stm32f4xx/clocks.cpp b/modules/stm32/source/stm32f4xx/clocks.cpp index fe6efd1..2af7211 100644 --- a/modules/stm32/source/stm32f4xx/clocks.cpp +++ b/modules/stm32/source/stm32f4xx/clocks.cpp @@ -1,5 +1,6 @@ #include "core/Status.hpp" #include "core/core.hpp" +#include "configure.hpp" #include "cortex/tick.hpp" #include "cortex/vectors.hpp" #include "stm32/Initialize.hpp" @@ -225,6 +226,14 @@ void clocks(ClockConfiguration const& clkcfg) { clock_tree.apb1_peripheral * (GetAPB1Divider(clkcfg.apb1_low_speed_divider) == 1 ? 1U : 2U); // APB1 is doubled if the divider is not 1 clock_tree.apb2_timer_clk = clock_tree.apb2_peripheral * (GetAPB2Divider(clkcfg.apb2_high_speed_divider) == 1 ? 1U : 2U); // APB2 is doubled if the divider is not 1 + // On F4 the SWO clock is the core clock (the TPIU is driven by TRACECKIN = core clock). + clock_tree.trace = clock_tree.sysclk; + // The F4 SWO goes through the ARM TPIU (cortex::initialize::swo), so this vendor call is + // a no-op, but it is still invoked here (like early_power) so the module archive member is + // pulled in for symbols referenced only from configure.cpp. + if constexpr (cortex::swo::enable) { + enable_serial_wire_output(clock_tree.trace, static_cast(cortex::swo::baudrate)); + } // clock_tree.rng = clock_tree.sysclk; } diff --git a/modules/stm32/source/stm32f4xx/debug.cpp b/modules/stm32/source/stm32f4xx/debug.cpp new file mode 100644 index 0000000..e68586b --- /dev/null +++ b/modules/stm32/source/stm32f4xx/debug.cpp @@ -0,0 +1,21 @@ + +#include "stm32/Initialize.hpp" + +namespace stm32 { +namespace initialize { + +/// @brief Enables the DBGMCU trace port clocks. +/// @note F4 uses the standard Cortex-M TPIU (driven by the core clock) for SWO, which is +/// configured by cortex::initialize::swo(). No vendor specific trace blocks need clocking. +void enable_trace_port_clock(void) { +} + +/// @brief Programs the Serial Wire Output path for UART trace on the SWO pin. +/// @note F4 uses the standard Cortex-M TPIU instead of the ST SWO/SWTF blocks, so this is a no-op. +void enable_serial_wire_output(core::units::Hertz trace_clock, std::uint32_t baud) { + (void)trace_clock; + (void)baud; +} + +} // namespace initialize +} // namespace stm32 diff --git a/modules/stm32/source/stm32h7xx/clocks.cpp b/modules/stm32/source/stm32h7xx/clocks.cpp index cce1e99..672371b 100644 --- a/modules/stm32/source/stm32h7xx/clocks.cpp +++ b/modules/stm32/source/stm32h7xx/clocks.cpp @@ -3,6 +3,7 @@ #include "cortex/halt.hpp" #include "cortex/thumb.hpp" #include "cortex/tick.hpp" +#include "configure.hpp" #include "cortex/vectors.hpp" #include "stm32/Initialize.hpp" #include "stm32/f4xx/ResetAndClockControl.hpp" @@ -118,7 +119,7 @@ void clocks(ClockConfiguration const& clkcfg) { return; } - // TODO Enable Power Supply Correctly? + // TODO Enable Power Supply Correctly? // PWR supply config + ACTVOSRDY? // Set flash wait states for high-speed operation before increasing clocks. @@ -289,6 +290,17 @@ void clocks(ClockConfiguration const& clkcfg) { clock_tree.apb1_peripheral * (GetAPBDivider(clkcfg.apb1_low_speed_divider) == 1 ? 1U : 2U); // APB1 is doubled if the divider is not 1 clock_tree.apb2_timer_clk = clock_tree.apb2_peripheral * (GetAPBDivider(clkcfg.apb2_high_speed_divider) == 1 ? 1U : 2U); // APB2 is doubled if the divider is not 1 + + // TRACECK follows RCC_CFGR[SW]: when SYSCLK is PLL1P, TRACECK is PLL1R. + clock_tree.trace = clock_tree.pll_vco / (clkcfg.pll_r + 1U); + + // The H7 SWO block is driven from TRACECK (PLL1R), not the core clock, so the ST + // SWO/SWTF blocks are programmed here (like early_power, because the module archive is + // only scanned once before jarnax, so vendor symbols referenced only from configure.cpp + // would not link). + if constexpr (cortex::swo::enable) { + enable_serial_wire_output(clock_tree.trace, static_cast(cortex::swo::baudrate)); + } } } // namespace initialize diff --git a/modules/stm32/source/stm32h7xx/debug.cpp b/modules/stm32/source/stm32h7xx/debug.cpp new file mode 100644 index 0000000..f0799d3 --- /dev/null +++ b/modules/stm32/source/stm32h7xx/debug.cpp @@ -0,0 +1,46 @@ + +#include "stm32/Initialize.hpp" +#include "stm32/peripherals.hpp" + +namespace stm32 { +namespace initialize { + +/// @brief Enables the DBGMCU trace port clocks so the ST trace blocks can drive the SWO pin. +/// @note RM0433 Rev 8 p.3167: TRACECK must be enabled via TRACECLKEN (bit 20) before trace is +/// sent to the trace blocks. D1DBGCKEN (bit 21) keeps the D1 debug components clocked while the +/// D1 core clock is active, and D3DBGCKEN (bit 22) does the same for the D3 domain which holds +/// the SWO (0x5C003000) and SWTF (0x5C004000) blocks. +void enable_trace_port_clock(void) { + h7xx::Debug::Control control; + control = h7xx::debug.control; // read + control.bits.trace_clock_enable = 1U; // bit 20 TRACECLKEN + control.bits.domain1_debug_clock_enable = 1U; // bit 21 D1DBGCKEN + control.bits.domain3_debug_clock_enable = 1U; // bit 22 D3DBGCKEN + h7xx::debug.control = control; // write +} + +/// @brief Programs the ST Serial Wire Output path (SWO + SWTF) for UART trace on the SWO pin. +/// @param trace_clock The frequency of the trace clock (TRACECK) driving the SWO block. +/// @param baud The desired SWO UART baud rate. +/// @note The Cortex-M7 TPIU (0xE0040000 alias) does not drive the SWO pin on the H7; the ST SWO +/// block (0x5C003000) does, and the SWTF funnel (0x5C004000) must forward the ITM trace bus to it. +void enable_serial_wire_output(core::units::Hertz trace_clock, std::uint32_t baud) { + // the SWO/SWTF blocks sit in the D3 debug domain, so un-gate all the debug clocks first + enable_trace_port_clock(); + + // unlock the SWO and SWTF blocks + h7xx::serial_wire_output.lock_access = h7xx::SwoLockValue; + h7xx::swo_trace_funnel.lock_access = h7xx::SwoTraceFunnelLockValue; + + // configure the SWO block for UART (Async NRZ) mode + h7xx::serial_wire_output.selected_pin_protocol.bits.transmit_mode = h7xx::Swo::Protocol::AsyncNRZ; + // set the baud rate divider (zero based): baud = trace_clock / (prescaler + 1). + // The CODR register only has a 13-bit PRESCALER field; write the whole register with the + // masked value to avoid GCC -Wconversion on the truncated bitfield. + h7xx::serial_wire_output.current_output_divisor.whole = ((trace_clock.value() / baud) - 1U) & 0x1FFFU; + // enable the trace bus from the Cortex-M7 ITM to the SWO block + h7xx::swo_trace_funnel.control.bits.enable_swo = 1U; +} + +} // namespace initialize +} // namespace stm32 diff --git a/modules/stm32/source/stm32h7xx/peripherals.cpp b/modules/stm32/source/stm32h7xx/peripherals.cpp index c322214..75b00e1 100644 --- a/modules/stm32/source/stm32h7xx/peripherals.cpp +++ b/modules/stm32/source/stm32h7xx/peripherals.cpp @@ -17,6 +17,8 @@ RandomNumberGenerator volatile random_number_generator; FlashControl volatile flash_control; Timer2 volatile timer2; Debug volatile debug; +Swo volatile serial_wire_output; +SwoTraceFunnel volatile swo_trace_funnel; SerialPeripheralInterface volatile spi1; SerialPeripheralInterface volatile spi2; SerialPeripheralInterface volatile spi3; diff --git a/modules/stm32/tests/CMakeLists.txt b/modules/stm32/tests/CMakeLists.txt index 0d50bab..3941a9a 100644 --- a/modules/stm32/tests/CMakeLists.txt +++ b/modules/stm32/tests/CMakeLists.txt @@ -1,5 +1,21 @@ ## Unit Tests +host_unit_test(NAME stm32-debug + SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/gtest-stm32-debug.cpp + LIBRARIES + strict + GENERIC_MODULES + core + MODULES + jarnax stm32 + GOOGLETEST + CONFIGURATIONS + basic + BOARDS + nucleo_h753zi +) + host_unit_test(NAME stm32-i2c SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/gtest-stm32-i2c.cpp diff --git a/modules/stm32/tests/gtest-stm32-debug.cpp b/modules/stm32/tests/gtest-stm32-debug.cpp new file mode 100644 index 0000000..52ff873 --- /dev/null +++ b/modules/stm32/tests/gtest-stm32-debug.cpp @@ -0,0 +1,117 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +/// @brief Tests for the H7 trace/SWO enable path (issue-41). +/// @note These tests run against the UNITTEST RAM-backed `stm32::h7xx::debug`, +/// `stm32::h7xx::serial_wire_output` and `stm32::h7xx::swo_trace_funnel` globals so no +/// emulator or target is required. +class DebugTest : public ::testing::Test { +protected: + void SetUp() override { + // Reset the registers to their reset values (all zero). + stm32::h7xx::Debug::Control control; + control.whole = 0U; + stm32::h7xx::debug.control = control; + stm32::h7xx::serial_wire_output.lock_access = 0U; + stm32::h7xx::serial_wire_output.selected_pin_protocol.whole = 0U; + stm32::h7xx::serial_wire_output.current_output_divisor.whole = 0U; + stm32::h7xx::swo_trace_funnel.control.whole = 0U; + stm32::h7xx::swo_trace_funnel.priority.whole = 0U; + stm32::h7xx::swo_trace_funnel.lock_access = 0U; + } +}; + +/// @brief Empty test to exercise SetUp/Teardown and confirm the registers reset cleanly. +TEST_F(DebugTest, ResetValue) { + stm32::h7xx::Debug::Control control = stm32::h7xx::debug.control; + EXPECT_EQ(control.whole, 0U); + EXPECT_EQ(stm32::h7xx::serial_wire_output.current_output_divisor.whole, 0U); + EXPECT_EQ(stm32::h7xx::swo_trace_funnel.control.whole, 0U); +} + +/// @brief Enabling the trace port clock sets TRACECLKEN (20), D1DBGCKEN (21) and D3DBGCKEN (22). +/// @note D3DBGCKEN un-gates the D3 debug domain which holds the SWO/SWTF blocks on the H7. +TEST_F(DebugTest, EnableTracePortClockSetsExpectedBits) { + stm32::initialize::enable_trace_port_clock(); + stm32::h7xx::Debug::Control control = stm32::h7xx::debug.control; + EXPECT_EQ(control.bits.trace_clock_enable, 1U); // bit 20 TRACECLKEN + EXPECT_EQ(control.bits.domain1_debug_clock_enable, 1U); // bit 21 D1DBGCKEN + EXPECT_EQ(control.bits.domain3_debug_clock_enable, 1U); // bit 22 D3DBGCKEN +} + +/// @brief The enable is a read-modify-write and must preserve unrelated CR bits. +TEST_F(DebugTest, EnableTracePortClockPreservesOtherBits) { + stm32::h7xx::Debug::Control control; + control = stm32::h7xx::debug.control; + control.bits.external_trigger_output_enable = 1U; // bit 28 TRGOEN, unrelated + stm32::h7xx::debug.control = control; + + stm32::initialize::enable_trace_port_clock(); + + control = stm32::h7xx::debug.control; + EXPECT_EQ(control.bits.external_trigger_output_enable, 1U); + EXPECT_EQ(control.bits.trace_clock_enable, 1U); + EXPECT_EQ(control.bits.domain1_debug_clock_enable, 1U); + EXPECT_EQ(control.bits.domain3_debug_clock_enable, 1U); +} + +/// @brief Calling the enable twice is idempotent. +TEST_F(DebugTest, EnableTracePortClockIsIdempotent) { + stm32::initialize::enable_trace_port_clock(); + stm32::initialize::enable_trace_port_clock(); + stm32::h7xx::Debug::Control control = stm32::h7xx::debug.control; + EXPECT_EQ(control.whole, (1U << 20U) | (1U << 21U) | (1U << 22U)); +} + +/// @brief Enabling the serial wire output also enables all of the trace port clocks. +TEST_F(DebugTest, EnableSerialWireOutputEnablesTracePortClocks) { + stm32::initialize::enable_serial_wire_output(core::units::Hertz{100'000'000U}, 2'240'000U); + stm32::h7xx::Debug::Control control = stm32::h7xx::debug.control; + EXPECT_EQ(control.bits.trace_clock_enable, 1U); + EXPECT_EQ(control.bits.domain1_debug_clock_enable, 1U); + EXPECT_EQ(control.bits.domain3_debug_clock_enable, 1U); +} + +/// @brief The SWO and SWTF blocks are unlocked with the CoreSight key. +TEST_F(DebugTest, EnableSerialWireOutputUnlocksBlocks) { + stm32::initialize::enable_serial_wire_output(core::units::Hertz{100'000'000U}, 2'240'000U); + EXPECT_EQ(stm32::h7xx::serial_wire_output.lock_access, 0xC5ACCE55U); + EXPECT_EQ(stm32::h7xx::swo_trace_funnel.lock_access, 0xC5ACCE55U); +} + +/// @brief The SWO block is configured for UART (Async NRZ) mode. +TEST_F(DebugTest, EnableSerialWireOutputSetsUartProtocol) { + stm32::initialize::enable_serial_wire_output(core::units::Hertz{100'000'000U}, 2'240'000U); + std::uint32_t whole = stm32::h7xx::serial_wire_output.selected_pin_protocol.whole; + EXPECT_EQ(whole & 0x3U, static_cast(stm32::h7xx::Swo::Protocol::AsyncNRZ)); +} + +/// @brief The CODR prescaler is zero based: prescaler = (trace_clock / baud) - 1. +TEST_F(DebugTest, EnableSerialWireOutputSetsBaudDivider) { + stm32::initialize::enable_serial_wire_output(core::units::Hertz{100'000'000U}, 4'000'000U); + std::uint32_t whole = stm32::h7xx::serial_wire_output.current_output_divisor.whole; + EXPECT_EQ(whole & 0x1FFFU, 100'000'000U / 4'000'000U - 1U); // 24 +} + +/// @brief The CODR prescaler for the default 2.24 MBaud config is 43. +TEST_F(DebugTest, EnableSerialWireOutputSetsDefaultBaudDivider) { + stm32::initialize::enable_serial_wire_output(core::units::Hertz{100'000'000U}, 2'240'000U); + std::uint32_t whole = stm32::h7xx::serial_wire_output.current_output_divisor.whole; + EXPECT_EQ(whole & 0x1FFFU, 100'000'000U / 2'240'000U - 1U); // 43 +} + +/// @brief The SWTF funnel is enabled so the ITM trace bus reaches the SWO block. +TEST_F(DebugTest, EnableSerialWireOutputEnablesFunnel) { + stm32::initialize::enable_serial_wire_output(core::units::Hertz{100'000'000U}, 2'240'000U); + std::uint32_t whole = stm32::h7xx::swo_trace_funnel.control.whole; + EXPECT_EQ(whole & 0x1U, 1U); // ENSO +}