diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 177d65e..7e9c11f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,13 @@ jobs: # gcc-arm-none-eabi: builds the Cortex-M3 test firmware (hello/blink/ # systick) so test_e2e/test_cli/INTROSPECTION_HELLO_ELF configure and # the test-count floor gate reaches its baseline on this workflow. - sudo apt-get install -y g++-14 gcc-arm-none-eabi binutils-arm-none-eabi + # qemu-system-arm + gdb-multiarch: enable the oracle_cortex_m3 + # conformance test (find_program in test/CMakeLists). It runs the + # same firmware under a reference QEMU Cortex-M3 + gdbstub and diffs + # behavior — without both the oracle is skipped on CI. gdb-multiarch + # is wired to the oracle via ARM_GDB in the Test step (ubuntu's + # gcc-arm-none-eabi ships no gdb of its own). + sudo apt-get install -y g++-14 gcc-arm-none-eabi binutils-arm-none-eabi qemu-system-arm gdb-multiarch - name: Cache GoogleTest uses: actions/cache@v4 @@ -45,6 +51,8 @@ jobs: - name: Test working-directory: build + env: + ARM_GDB: gdb-multiarch # oracle_cortex_m3 gdbstub client run: ctest --output-on-failure --timeout 60 -j "$(nproc)" - name: Upload test logs on failure diff --git a/cli/main.cpp b/cli/main.cpp index a495aef..41f5de7 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include using namespace micro_forge; @@ -135,7 +137,10 @@ void print_usage() { " (runs forever by default; Ctrl+C stops with a report)\n" " dump-mem --addr 0x20000000 [--len 256]\n" " [--chip stm32f103] [--base 0x08000000]\n" - " (loads firmware, dumps a memory window to stdout)\n"); + " (loads firmware, dumps a memory window to stdout)\n" + " probe [--max-steps 1000000]\n" + " [--chip stm32f103] [--base 0x08000000]\n" + " (runs in probe mode — lists opcodes the simulator can't execute)\n"); } struct RunOptions { @@ -401,6 +406,99 @@ int cmd_dump_mem(int argc, char** argv) { return 0; } +int cmd_probe(int argc, char** argv) { + std::string firmware; + std::string chip = "stm32f103"; + uint32_t base = 0x08000000; + size_t max_steps = 1'000'000; + for (int i = 0; i < argc; ++i) { + std::string a = argv[i]; + auto next = [&]() -> const char* { + return (i + 1 < argc) ? argv[++i] : nullptr; + }; + if (a == "--chip") { + const char* v = next(); + if (!v) { std::fprintf(stderr, "--chip needs a value\n"); return 2; } + chip = v; + } else if (a == "--base") { + const char* v = next(); + if (!v) { std::fprintf(stderr, "--base needs a value\n"); return 2; } + base = static_cast(std::strtoul(v, nullptr, 0)); + } else if (a == "--max-steps") { + const char* v = next(); + if (!v) { std::fprintf(stderr, "--max-steps needs a value\n"); return 2; } + max_steps = static_cast(std::strtoull(v, nullptr, 0)); + } else if (!a.empty() && a[0] != '-') { + if (firmware.empty()) { + firmware = a; + } else { + std::fprintf(stderr, "unexpected positional arg: %s\n", a.c_str()); + return 2; + } + } else { + std::fprintf(stderr, "unknown option: %s\n", a.c_str()); + print_usage(); + return 2; + } + } + if (chip != "stm32f103") { + std::fprintf(stderr, "unsupported chip '%s' (only stm32f103)\n", chip.c_str()); + return 2; + } + if (firmware.empty()) { + std::fprintf(stderr, "missing firmware path\n"); + print_usage(); + return 2; + } + auto data = read_file(firmware); + if (data.empty()) { + std::fprintf(stderr, "cannot read firmware: %s\n", firmware.c_str()); + return 1; + } + auto soc = load_firmware(data, base); + if (!soc) { + std::fprintf(stderr, "firmware load failed: %s\n", soc.error().c_str()); + return 1; + } + + // Probe mode: a missing opcode is recorded + skipped (PC advances past it) + // instead of faulting, so one run surfaces every instruction the simulator + // can't yet execute — something a real-hardware debugger cannot tell you. + auto cm3 = (*soc)->cortex_m3_cpu(); + if (!cm3.IsValid()) { + std::fprintf(stderr, "[probe] CPU not wired\n"); + return 1; + } + cm3->enable_probe_mode(true); + (*soc)->run(max_steps); + + std::fprintf(stderr, "[probe] %s\n", firmware.c_str()); + const auto& missing = cm3->missing_opcodes(); + if (missing.empty()) { + std::fprintf(stderr, + "[probe] no missing opcodes — full instruction coverage\n"); + return 0; + } + // Dedup by encoding: count executions + remember the first PC per opcode. + std::map, size_t> counts; + std::map, uint32_t> first_pc; + for (const auto& [pc, hw1, hw2] : missing) { + auto key = std::make_pair(hw1, hw2); + counts[key]++; + if (first_pc.find(key) == first_pc.end()) { + first_pc[key] = static_cast(pc); + } + } + std::fprintf(stderr, + "[probe] %zu missing opcode execution(s), %zu unique:\n", + missing.size(), counts.size()); + for (const auto& [key, cnt] : counts) { + std::fprintf(stderr, " insn=0x%04X 0x%04X x%zu first@pc=0x%08X\n", + key.first, key.second, cnt, first_pc[key]); + } + return 0; +} + } // namespace int main(int argc, char** argv) { @@ -416,6 +514,9 @@ int main(int argc, char** argv) { if (sub == "dump-mem") { return cmd_dump_mem(argc - 2, argv + 2); } + if (sub == "probe") { + return cmd_probe(argc - 2, argv + 2); + } if (sub == "-h" || sub == "--help" || sub == "help") { print_usage(); return 0; diff --git a/document/notes/040-gui-debugger-skeleton.md b/document/notes/040-gui-debugger-skeleton.md new file mode 100644 index 0000000..9a3d2e8 --- /dev/null +++ b/document/notes/040-gui-debugger-skeleton.md @@ -0,0 +1,41 @@ +# 040 · GUI 第一批——数值调试器骨架(model/view/panels 分层 + dock + 调速) + +> 日期: 2026-07-07 ~ 07-08 +> 分支: `feat/start_gui_panel`(已合入 main `8dee335`,PR #10) +> 提交: efa7471 / f81ab7d / d61a808 / fc23f6c / 7b3ba2f / 438df09 / a1d713c / e0f0ef1 / 124f090 +> 状态: 已合入 main,ctest 359 全绿,CI 双绿 +> 验证: ctest 359 + offscreen smoke(`MICRO_FORGE_GUI_AUTORUN=1` + `QT_QPA_PLATFORM=offscreen`) + +## 背景 + +GUI 在 notes 034–036 立了骨架(Qt 选型 + CPU/GPIO 面板),但那时 MainWindow 直接持有 SoC、面板跟 sim 内部耦合、布局是 central widget 硬拼——能跑,但不可测、不可扩展。这一批把它重构成「数值调试器骨架」:model/view/panels 三层分离 + QMainWindow dock 框架 + 调速选择器 + 持续 blink 演示 + Session 单测。目标是为后续 A 组深化(芯片可视化 / 输入控件 / 时钟树面板)铺好地基——骨架不稳就往上堆肉会塌。 + +## 设计决策 + +**1. Qt-free model::Session —— 可测试的模型层(efa7471)。** 把 SoC 持有、run/step、固件加载、UART 累积从 MainWindow 抽出,做成纯 C++ 的 `gui::model::Session`。不依赖 QWidget/QTimer/QApplication,所以能直接 gtest(`test/test_gui_session.cpp`),不必拉起 Qt。`rebuild()` 返回 `std::expected` 上报创建/读取/加载错误;`snapshot()` 在 invalid 时返回 default 构造(state=Halted / 全零),让 view 永远拿到合法结构。**这是这批的核心决策**——model 与 view 解耦,sim 逻辑进了测试网,不再是「只有跑起 GUI 才能验证」。 + +**2. 单一事实来源 —— 面板只读 IntrospectionSnapshot(DIRECTIVES §E 铁律)。** 所有面板暴露 `refresh(const IntrospectionSnapshot&)`,从 `Session::snapshot()` 取数,绝不直接碰 `soc_` 内部。MainWindow 自己不持有任何 sim 状态(全在 Session)。这保证 view 永远走 introspection 干线,不绕过、不同步漂移。面板不搞抽象基类——每个是独立 QWidget,约定一个 refresh 方法就够,避免过早抽象。 + +**3. QMainWindow dock 框架(d61a808 / fc23f6c)。** 从 central widget 硬拼改成 QMainWindow + 6 个可拖拽 dock:serial 居中(主视觉)、registers 左、status/fault/peripherals 右、GPIO 下。用户能拖动/折叠/浮动任意面板。每个 dock 都 `setObjectName`(`regs_dock` / `status_dock` / …),为后续 A3 布局持久化(QSettings saveState)埋好钩子。 + +**4. 调速选择器(7b3ba2f)。** toolbar QComboBox `1×/5×/25×/100×`,`onTick` 按当前档位的步数跑步。固件用软件延时(gpio_blink 那种循环死等)时,低档几乎看不到进展,必须高档(100× = 每 tick 100 步)才能肉眼看见 LED/串口动。这也是模拟器相对真机调试器的便利——可任意调时间尺度,真机做不到。 + +**5. 持续 blink 演示(e0f0ef1 / 124f090)。** examples/gpio_blink 的 runner 改成无限循环 + 每次翻转打印 PA5 状态,让 GUI 启动后有持续的视觉输出(不是翻一下就停)。配合调速,演示效果直接,也是这批 GUI 能「看着像那么回事」的关键。 + +## 单线程模型 + +按 DIRECTIVES §E,sim 保持单线程——不做 Qt worker 线程跟 sim 线程的同步复杂度。view 的 QTimer 在 Qt 主线程里驱动 `Session::run()/step()`,每 tick 跑 N 步(N = 调速档位)然后 refresh 所有面板。简单、无锁、够用。`MICRO_FORGE_GUI_AUTORUN=1` 是 offscreen 测试钩子,让 GUI headless 启动就进 run loop,CI 上验证不崩。 + +## 验证 + +- ctest 359/359 全绿(含新增 `test_gui_session` 覆盖 rebuild/run/snapshot/USART 累积)。 +- test floor 在 a1d713c bump 到 357,后续又到 359。 +- offscreen smoke:GUI 无显示器启动 + 跑 run loop 不崩,每个 GUI commit 过这个门。 + +## 后续 + +这一批立了骨架,剩下都是「往骨架上加肉」——详见 `document/ai/HANDOFF.md`: +- **A 组**:A1 芯片板级可视化(主菜,模拟器独门卖点)、A2 输入控件、A3 dock 持久化、A4 probe mode。 +- **B 组**:B1 时钟树 introspection+面板、B2 DMA/SPI(用 CubeF1 官方示例)。 +- **C 组**:C2 set_nvic 架构债、C3 CI QEMU oracle、外加内存视图(hex dump 面板)。 +- **反汇编器(C4 后半)后置**到下个里程碑——已确认迟早要做,工作量与当初 thumb-2 全覆盖战役同档(200+ 指令格式化 + objdump 当 oracle 对拍)。 diff --git a/document/notes/041-c2-set-nvic-architecture-debt.md b/document/notes/041-c2-set-nvic-architecture-debt.md new file mode 100644 index 0000000..5cd6232 --- /dev/null +++ b/document/notes/041-c2-set-nvic-architecture-debt.md @@ -0,0 +1,41 @@ +# 041 · C2 — `set_nvic`/`set_scb` 架构债收口 + +> 日期: 2026-07-08 +> 分支: `feat/deepen-gui-and-core` +> 状态: 收口(文档标注,无代码行为变更) +> 验证: ctest 367 全绿(无逻辑改动) + +## 背景 + +DIRECTIVES §A 铁律:外设/CPU 通过 `WeakPtr` 注入,避免循环引用。但 `CortexM3CPU` 一直用裸指针 setter: + +```cpp +void set_nvic(periph::NvicPeripheral& nvic) { nvic_ = &nvic; } +void set_scb(periph::ScbPeripheral& scb) { scb_ = &scb; } +``` + +调用方唯一一处:`stm32f103_soc.cpp` `create()` 里 `cm3_ptr->set_nvic(p.nvic)` / `set_scb(p.scb)`。05 验收要求"归拢"这条债。 + +## 调研 + +- **`bus_` 注释已论证过同一 pattern**(`cortex_m3.hpp:21-25`):"Raw observer pointer, not WeakPtr ... matches the existing nvic_/scb_ pattern — a WeakPtr here was a misuse (the ownership tree is a clean unique_ptr, no cycle to break) and cost a control-block deref + IsValid on every fetch/load/store." 即先前已评估并判定裸指针正确、WeakPtr 误用。 +- **无循环**:`Stm32f103Parts`(含 NVIC/SCB)与 CPU 都是 `Stm32f103Soc` 的成员,同生共死;NVIC→CPU 走 callback + WeakPtr(pull 模式),CPU→NVIC 是单向裸指针。铁律的目的是断环,这里根本没有环。 +- **用法在异常路径,非每指令热路径**:`nvic_->set_pending(irq)`(raise_irq)、`scb_->system_exception_priority(exc_num)`(优先级查询),都不在 fetch/decode 主循环里。 +- **NVIC/SCB 本身有 `GetWeak()`**——改 WeakPtr 技术上可行,但没有收益(无环可断)。 + +## 决策:文档标注,不机械改 WeakPtr + +机械改成 WeakPtr 是 churn:动 CPU 公共接口 + 调用方,每处 `nvic_->` 变 `lock + IsValid + deref`,换来的"一致性"是表面的——铁律的精神是"避免循环引用",这里无循环,裸指针合法。把债收口为"明确标注为何裸指针 + 记录决策"比"为合规而改"更诚实。 + +落地三处: +1. **`cortex_m3.hpp` `set_nvic`/`set_scb` 加注释**:点明无循环 / 同生共死 / 异常路径 / 指向 `bus_` 注释与本文。 +2. **DIRECTIVES §A 生命周期条款加例外澄清**:承认 SoC 同生共死 wiring 不算违反"避免循环"(让铁律自洽,不丢边界)。 +3. **本文**:决策记录(L8 纪律)。 + +## 验证 + +纯文档 + 注释改动,无逻辑变更。ctest 367 全绿(未重跑逻辑——只改注释/文档,不参与编译语义)。若日后真出现 CPU/外设解耦(不同生命周期),届时再回头改 WeakPtr——当前 SoC 单容器模型下无此需求。 + +## 陷阱(给后人) + +别看到 DIRECTIVES §A "WeakPtr 注入" 就把 `set_nvic` 改 WeakPtr——先确认是否真有循环引用。本项目的所有权树是 SoC 单 unique_ptr 容器(parts + machine + cpu 同生共死),NVIC→CPU 的反向链已是 WeakPtr/callback,正向裸指针安全。 diff --git a/document/notes/042-abc-deepen-batch.md b/document/notes/042-abc-deepen-batch.md new file mode 100644 index 0000000..17b60c3 --- /dev/null +++ b/document/notes/042-abc-deepen-batch.md @@ -0,0 +1,59 @@ +# 042 · ABC 深化批次——GUI 加肉 + 工程收尾 + +> 日期: 2026-07-08 +> 分支: `feat/deepen-gui-and-core`(12 commit,本地未 push) +> 基线: main `8dee335`(v0.2.0 + GUI 第一批 PR#10,notes 040) +> 状态: 9 项 done + 3 follow-up,ctest 367 绿 +> 后置: B2 DMA/SPI + 反汇编(用户倾向自己手写避幻觉) + +## 背景 + +v0.2.0 发版 + GUI 第一批(数值调试器骨架:model/view/panels + dock + 调速,notes 040)合入 main 后,这一轮把骨架"加肉":深化 GUI 可视化 + 补工程债 + 扩 CLI。任务拆解见 `document/ai/HANDOFF.md` §A/B/C。 + +## 做了什么(9 项 + 3 follow-up) + +**A 组 GUI 深化** +- **A1 芯片板级可视化**:`Stm32BoardWidget`(QPainter 自定义 widget,`gui/view/widgets/`)作 central,画 STM32F103 + 引脚 + LED。演进:初版固定 PA5/PA0/PC13(gpio_blink 用 PA5),用户实测 F103 示例翻 PA1 看不到 → 改 **PA0-PA7 一排 LED**,任何固件翻哪个 PA pin 都显示。LED 按 GPIO ODR bit 亮灭——模拟器知道每个引脚电平,相对真机调试器的独门卖点。 +- **A2 输入控件**:SerialPanel 加 USART RX 输入框(回车→`inject_rx`)。GPIO toggle 按钮**后移除**——`simulate_input` 改 IDR(输入寄存器),但显示看 ODR + 演示固件不读 IDR,无可见效果(A2 设计失误)。`Session::simulate_gpio_input` 保留(语义没错,给读 IDR 的固件)。 +- **A3 dock 布局持久化**:QSettings `saveState`/`restoreState` + `saveGeometry`。toolbar `setObjectName("main_toolbar")` 消 saveState 警告。 +- **A4 probe mode CLI**:`micro-forge probe `——`enable_probe_mode` 后 missing opcode 被 skip + 记录(不 fault),跑完去重列 unique 未实现指令 + 计数 + first PC。真机做不到的卖点。 + +**B 组核心/外设** +- **B1 时钟树**:`ClockSnapshot{sysclk,hclk,apb1,apb2}` 加 IntrospectionSnapshot,`read_introspection` 经 `SimulationCoordinator→VirtualClock` 取三域频率;HCLK 暂报 SYSCLK(AHB prescaler 未建模)。`ClockPanel`(SYSCLK→HCLK→APB1/APB2 MHz 树)。时钟树是 STM32 最难讲的概念,教学利器。 + +**C 组工程收尾** +- **C2 set_nvic 架构债收口**:调研后判定裸指针正确(NVIC/SCB 与 CPU 同属 SoC 同生共死、无循环、异常路径用),不机械改 WeakPtr(那纯属 churn 无收益)。`set_nvic`/`set_scb` 加注释 + DIRECTIVES §A 加例外澄清 + notes 041。`bus_` 注释早已论证同 pattern。 +- **C3 CI qemu**:`ci.yml` apt install `qemu-system-arm`,`oracle_cortex_m3` 一致性测试进 CI(之前只本机跑)。 +- **C4-mem 内存视图**:`Session::read_memory(addr,len)` 经 `tools::memory_dump`;`MemoryPanel`(addr 输入 + hex dump,每 tick 刷)。反汇编留下(XL)。 + +**follow-up**(用户实测反馈) +- BoardView → `Stm32BoardWidget` 重构(挪到 `gui/view/widgets/`,用户要 widget 独立目录)+ LED PA0-7 通用化 + toolbar objectName。 +- 移除 GPIO toggle 按钮(IDR vs ODR 设计失误)。 +- `E2E.GpioBlink` 断言收紧 ≥6→≥25 + 打印实际 toggle 数(实测 40 翻/4M 步 = 10 万步/翻,1× 下 250ms/翻 ~4Hz)。 + +## 设计决策 + +- **单一事实来源**:所有面板/Widget 只读 `IntrospectionSnapshot`(`Session::snapshot()`),不碰 `soc_` 内部。Session Qt-free 可 gtest。 +- **BoardView PA0-7 通用化**:固定 pin 映射不匹配多固件(gpio_blink PA5 vs F103 PA1)→ 画整排 PA0-7,任何 PA pin 可见。 +- **GPIO 输入注入语义**:`simulate_input` 改 IDR(输入),不影响 ODR(输出)。LED/面板显示 ODR,固件要主动读 IDR 才看到注入;演示固件不读 → toggle 无效果 → 移除。 +- **set_nvic 裸指针**:无循环(铁律目的是断环)+ 同生共死 + 异常路径。WeakPtr 徒增 control-block deref + IsValid。详见 notes 041。 +- **DMA/SPI + 反汇编后置**:易幻觉的硬件对齐实现,用户倾向自己手写保真实。 + +## 陷阱 + +- **clangd Qt 头误报**:`QMainWindow`/`QWidget`/`QPainter` not found 等,是 clangd include path cache 问题,以 gcc 为准(HANDOFF §1 标注)。 +- **offscreen 只验不崩**:`MICRO_FORGE_GUI_AUTORUN` + `QT_QPA_PLATFORM=offscreen` 验 GUI 启动 + run loop 不崩,但**不渲染像素**——LED 实际颜色/翻转要本地 WSLg 跑看。 +- **GpioBlink 断言松**:原 `EXPECT_GE(6)` 远低于实际(40 翻/4M 步),藏回归。收紧 ≥25 + 打印实际值。 +- **git mv + Write**:`git mv` 后新路径文件要先 Read 才能 Write(harness 要求),否则报 "File has not been read yet"。 + +## 验证 + +- ctest **367/367** 全绿(core + build-gui 双 build,从 359 → 367,+8 测试:Session inject/read_memory + introspection clock + CLI probe)。 +- offscreen smoke:每 GUI commit 过(F103.axf / blink.elf 跑 2-3s 不崩)。 +- 手动:`probe F103.axf` 报 full coverage;`dump-mem` / `run` 现有功能不回归。 +- test floor 357→360(A2 input-injection tests)。 + +## 后置(排队,用户自己手写) + +- **B2 DMA/SPI**:CubeF1 `DMA_FLASHToRAM`(M2M HAL)作设计参考;模拟器现无 SPI/DMA(从零建)。用户决定后置(幻觉风险)+ 倾向自己手写裸寄存器(避 HAL 构建复杂性)。详见 memory `complex-impl-self-written`。 +- **反汇编器(C4 后半)**:XL,与 thumb-2 全覆盖同档(200+ 指令格式化 + objdump 当 oracle 对拍)。用户要求"记得迟早要做"。 diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index fd1968c..672efb6 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -11,12 +11,15 @@ add_executable(micro-forge-gui main.cpp main_window.cpp model/session.cpp + view/widgets/stm32_board_widget.cpp view/panels/registers_panel.cpp view/panels/gpio_panel.cpp view/panels/serial_panel.cpp view/panels/status_panel.cpp view/panels/fault_panel.cpp view/panels/peripheral_panel.cpp + view/panels/clock_panel.cpp + view/panels/memory_panel.cpp ) # main.cpp / main_window.cpp include "gui/main_window.hpp" (header sits next to # the .cpp here), so the include root is the repo top, not src/. diff --git a/gui/main_window.cpp b/gui/main_window.cpp index 4304261..a677e01 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -13,13 +13,19 @@ #include "gui/view/panels/registers_panel.hpp" #include "gui/view/panels/serial_panel.hpp" #include "gui/view/panels/status_panel.hpp" +#include "gui/view/widgets/stm32_board_widget.hpp" +#include "gui/view/panels/clock_panel.hpp" +#include "gui/view/panels/memory_panel.hpp" #include "cpu/cpu.hpp" +#include +#include #include #include #include #include +#include #include #include #include @@ -37,6 +43,7 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) // ── toolbar: run / step / reset + state + speed ── auto* toolbar = addToolBar("main"); + toolbar->setObjectName("main_toolbar"); // saveState/restoreState need it run_btn_ = new QPushButton("Run"); auto* step_btn = new QPushButton("Step"); auto* reset_btn = new QPushButton("Reset"); @@ -66,9 +73,14 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) fault_panel_ = new panels::FaultPanel; gpio_panel_ = new panels::GpioPanel; periph_panel_ = new panels::PeripheralPanel; + clock_panel_ = new panels::ClockPanel; + memory_panel_ = new panels::MemoryPanel; + board_widget_ = new view::Stm32BoardWidget; - // Central: serial output (the surface watched while firmware runs). - setCentralWidget(serial_panel_); + // Central: the board (chip + LEDs) — micro-forge's visual main stage. + // Serial output moves to the bottom dock so firmware output stays visible + // alongside the GPIO dock while the board owns the centre. + setCentralWidget(board_widget_); // Left dock: CPU registers. auto* regs_dock = new QDockWidget("CPU registers", this); @@ -92,17 +104,46 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) periph_dock->setWidget(periph_panel_); addDockWidget(Qt::RightDockWidgetArea, periph_dock); - // Bottom dock: GPIO. + auto* clock_dock = new QDockWidget("Clock tree", this); + clock_dock->setObjectName("clock_dock"); + clock_dock->setWidget(clock_panel_); + addDockWidget(Qt::RightDockWidgetArea, clock_dock); + + auto* memory_dock = new QDockWidget("Memory", this); + memory_dock->setObjectName("memory_dock"); + memory_dock->setWidget(memory_panel_); + addDockWidget(Qt::RightDockWidgetArea, memory_dock); + + // Bottom dock: GPIO + serial output (side by side). auto* gpio_dock = new QDockWidget("GPIO", this); gpio_dock->setObjectName("gpio_dock"); gpio_dock->setWidget(gpio_panel_); addDockWidget(Qt::BottomDockWidgetArea, gpio_dock); + auto* serial_dock = new QDockWidget("Serial", this); + serial_dock->setObjectName("serial_dock"); + serial_dock->setWidget(serial_panel_); + addDockWidget(Qt::BottomDockWidgetArea, serial_dock); + connect(run_btn_, &QPushButton::clicked, this, &MainWindow::onRunClicked); connect(step_btn, &QPushButton::clicked, this, &MainWindow::onStepClicked); connect(reset_btn, &QPushButton::clicked, this, &MainWindow::onResetClicked); + // A2 USART RX input: forward the serial panel's input to the session. + // (GPIO toggle buttons were removed — simulate_input writes the IDR, which + // the ODR-based display doesn't reflect and demo firmware doesn't read.) + connect(serial_panel_, &panels::SerialPanel::inputSubmitted, + this, [this](const QString& text) { + const auto bytes = text.toUtf8(); + for (const char b : bytes) { + session_.inject_rx(static_cast(b)); + } + }); + // C4-mem: a new address → dump it immediately (don't wait for the tick). + connect(memory_panel_, &panels::MemoryPanel::addr_changed, + this, [this](std::uint32_t) { refreshMemory(); }); + timer_ = new QTimer(this); timer_->setInterval(50); // ~20 UI ticks/sec connect(timer_, &QTimer::timeout, this, &MainWindow::onTick); @@ -120,6 +161,14 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) } resize(1100, 760); + + // Restore the user's dock layout + window geometry from last session (A3). + // First launch: saved state is absent → restoreState returns false and the + // defaults above stay in effect. Docks must already exist + be named + // (setObjectName in the ctor above) for restoreState to relocate them. + QSettings settings("micro-forge", "gui"); + restoreGeometry(settings.value("geometry").toByteArray()); + restoreState(settings.value("windowState").toByteArray()); } void MainWindow::rebuildSession() { @@ -196,6 +245,28 @@ void MainWindow::refreshFromSnapshot() { fault_panel_->refresh(snap); gpio_panel_->refresh(snap); periph_panel_->refresh(snap); + clock_panel_->refresh(snap); + board_widget_->refresh(snap); + refreshMemory(); +} + +void MainWindow::refreshMemory() { + if (!session_.valid() || !memory_panel_->has_addr()) { + return; + } + // Dump 64 bytes from the tracked address; cheap enough to run per tick. + const auto dump = + session_.read_memory(memory_panel_->current_addr(), 64); + memory_panel_->show_dump(QString::fromStdString(dump)); +} + +void MainWindow::closeEvent(QCloseEvent* event) { + // Persist dock layout + window geometry so the next launch opens the way + // the user left it (A3). Docks were all given objectNames in the ctor. + QSettings settings("micro-forge", "gui"); + settings.setValue("geometry", saveGeometry()); + settings.setValue("windowState", saveState()); + QMainWindow::closeEvent(event); } } // namespace micro_forge::gui diff --git a/gui/main_window.hpp b/gui/main_window.hpp index f055061..2b9ce97 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -12,6 +12,7 @@ #include #include +class QCloseEvent; class QComboBox; class QLabel; class QPushButton; @@ -23,9 +24,15 @@ class GpioPanel; class SerialPanel; class StatusPanel; class FaultPanel; +class MemoryPanel; class PeripheralPanel; +class ClockPanel; } // namespace micro_forge::gui::panels +namespace micro_forge::gui::view { +class Stm32BoardWidget; +} // namespace micro_forge::gui::view + namespace micro_forge::gui { class MainWindow : public QMainWindow { @@ -35,6 +42,9 @@ class MainWindow : public QMainWindow { explicit MainWindow(const QString& firmware_path, QWidget* parent = nullptr); + protected: + void closeEvent(QCloseEvent* event) override; + private slots: void onTick(); void onRunClicked(); @@ -44,6 +54,7 @@ class MainWindow : public QMainWindow { private: void rebuildSession(); void refreshFromSnapshot(); + void refreshMemory(); // C4-mem: re-dump the memory panel's tracked region. model::Session session_; bool running_ = false; @@ -58,6 +69,9 @@ class MainWindow : public QMainWindow { panels::StatusPanel* status_panel_ = nullptr; panels::FaultPanel* fault_panel_ = nullptr; panels::PeripheralPanel* periph_panel_ = nullptr; + panels::ClockPanel* clock_panel_ = nullptr; + panels::MemoryPanel* memory_panel_ = nullptr; + view::Stm32BoardWidget* board_widget_ = nullptr; }; } // namespace micro_forge::gui diff --git a/gui/model/session.cpp b/gui/model/session.cpp index 0d4de4b..d0379ea 100644 --- a/gui/model/session.cpp +++ b/gui/model/session.cpp @@ -2,6 +2,7 @@ #include "gui/model/session.hpp" #include "hooks/events.hpp" // hooks::UartByte +#include "tools/memory_dump.hpp" #include #include @@ -73,4 +74,29 @@ introspection::IntrospectionSnapshot Session::snapshot() const { return introspection::read_introspection(*soc_, usart_output_); } +void Session::inject_rx(std::uint8_t byte) { + if (soc_) { + soc_->parts().usart1.inject_rx(byte); + } +} + +void Session::simulate_gpio_input(char port, std::uint8_t pin, bool high) { + if (soc_) { + soc_->parts().gpio(port).simulate_input(pin, high); + } +} + +std::string Session::read_memory(std::uint32_t addr, std::uint32_t len) const { + if (!soc_) { + return {}; + } + std::string out; + tools::memory_dump(*soc_->machine().bus, addr, len, + [&out](std::string_view line) { + out.append(line); + out.push_back('\n'); + }); + return out; +} + } // namespace micro_forge::gui::model diff --git a/gui/model/session.hpp b/gui/model/session.hpp index 02f0b99..642bbf1 100644 --- a/gui/model/session.hpp +++ b/gui/model/session.hpp @@ -46,6 +46,15 @@ class Session { // USART bytes accumulated since rebuild(); the view drains this per tick. std::string_view usart_output() const noexcept { return usart_output_; } + // Inject external input into the simulated peripherals (A2). Forwarded to + // the SoC; no-op when invalid (no SoC loaded). + void inject_rx(std::uint8_t byte); + void simulate_gpio_input(char port, std::uint8_t pin, bool high); + + // Hex dump of a memory region (C4-mem). Returns formatted lines joined by + // newlines; empty when invalid. Read-only — safe from the GUI tick thread. + std::string read_memory(std::uint32_t addr, std::uint32_t len) const; + private: std::unique_ptr soc_; std::string usart_output_; diff --git a/gui/view/panels/clock_panel.cpp b/gui/view/panels/clock_panel.cpp new file mode 100644 index 0000000..4db93ce --- /dev/null +++ b/gui/view/panels/clock_panel.cpp @@ -0,0 +1,42 @@ +// Clock panel — see clock_panel.hpp. +#include "gui/view/panels/clock_panel.hpp" + +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +ClockPanel::ClockPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + label_ = new QLabel; + label_->setStyleSheet("font-family: monospace;"); + lay->addWidget(label_); +} + +void ClockPanel::refresh(const introspection::IntrospectionSnapshot& snap) { + const auto& c = snap.peripherals.clock; + // Pretty-print MHz with 2 decimals (e.g. 8.00 MHz, 72.00 MHz, 36.00 MHz). + const auto mhz = [](std::uint32_t hz) { + const std::uint32_t whole = hz / 1'000'000u; + const std::uint32_t frac = (hz % 1'000'000u) / 10'000u; // 2 decimals + return QString("%1.%2 MHz") + .arg(whole) + .arg(frac, 2, 10, QLatin1Char('0')); + }; + // ASCII tree (monospace-safe, no encoding surprises). + label_->setText( + QString("SYSCLK %1\n" + " +-- HCLK (AHB/CPU) %2\n" + " +-- APB1 %3\n" + " `-- APB2 %4") + .arg(mhz(c.sysclk)) + .arg(mhz(c.hclk)) + .arg(mhz(c.apb1)) + .arg(mhz(c.apb2))); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/clock_panel.hpp b/gui/view/panels/clock_panel.hpp new file mode 100644 index 0000000..1ae6427 --- /dev/null +++ b/gui/view/panels/clock_panel.hpp @@ -0,0 +1,26 @@ +// Clock tree panel — renders the SYSCLK → HCLK → APB1/APB2 frequency tree. +// STM32's clock tree is one of its hardest concepts; surfacing it live is a +// teaching win (B1). Read-only; RCC frequency changes (firmware writing RCC +// prescalers / PLL) appear here on the next snapshot. +#pragma once + +#include "introspection/introspection.hpp" + +#include + +class QLabel; + +namespace micro_forge::gui::panels { + +class ClockPanel : public QWidget { + Q_OBJECT + public: + explicit ClockPanel(QWidget* parent = nullptr); + + void refresh(const introspection::IntrospectionSnapshot& snap); + + private: + QLabel* label_; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/gpio_panel.hpp b/gui/view/panels/gpio_panel.hpp index c2dded5..d7ab7ce 100644 --- a/gui/view/panels/gpio_panel.hpp +++ b/gui/view/panels/gpio_panel.hpp @@ -1,4 +1,7 @@ // GPIO panel — A/B/C port ODR (hex) + 16 per-pin LED glyphs (pin0..pin15). +// Read-only display of the GPIO output data registers. (Input-injection +// toggle buttons were removed: simulate_input writes the IDR, which this +// ODR-based display doesn't reflect and demo firmware doesn't read.) #pragma once #include "introspection/introspection.hpp" @@ -11,7 +14,6 @@ namespace micro_forge::gui::panels { class GpioPanel : public QWidget { Q_OBJECT - public: explicit GpioPanel(QWidget* parent = nullptr); diff --git a/gui/view/panels/memory_panel.cpp b/gui/view/panels/memory_panel.cpp new file mode 100644 index 0000000..84272b5 --- /dev/null +++ b/gui/view/panels/memory_panel.cpp @@ -0,0 +1,43 @@ +// Memory panel — see memory_panel.hpp. +#include "gui/view/panels/memory_panel.hpp" + +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::panels { + +MemoryPanel::MemoryPanel(QWidget* parent) : QWidget(parent) { + auto* lay = new QVBoxLayout(this); + + addr_input_ = new QLineEdit; + addr_input_->setPlaceholderText( + "Address (hex, e.g. 0x20000000) — Enter to track"); + lay->addWidget(addr_input_); + + dump_view_ = new QTextEdit; + dump_view_->setReadOnly(true); + dump_view_->setStyleSheet("font-family: monospace;"); + lay->addWidget(dump_view_); + + connect(addr_input_, &QLineEdit::returnPressed, this, [this] { + bool ok = false; + const unsigned parsed = + addr_input_->text().trimmed().toUInt(&ok, 16); // accepts 0x prefix + if (!ok) { + return; + } + current_addr_ = static_cast(parsed); + has_addr_ = true; + emit addr_changed(current_addr_); + }); +} + +void MemoryPanel::show_dump(const QString& text) { + dump_view_->setPlainText(text); +} + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/memory_panel.hpp b/gui/view/panels/memory_panel.hpp new file mode 100644 index 0000000..4a8e349 --- /dev/null +++ b/gui/view/panels/memory_panel.hpp @@ -0,0 +1,42 @@ +// Memory panel — interactive hex dump of a memory region (C4-mem). Unlike the +// snapshot panels, this one drives a query: the user types an address, and +// MainWindow reads that region from the SoC bus via tools::memory_dump (routed +// through Session) and pushes the dump back here each tick so it tracks memory +// as firmware runs. The disassembler is intentionally out of scope (later +// batch) — this panel only shows raw bytes. +#pragma once + +#include + +#include + +class QLineEdit; +class QString; +class QTextEdit; + +namespace micro_forge::gui::panels { + +class MemoryPanel : public QWidget { + Q_OBJECT + public: + explicit MemoryPanel(QWidget* parent = nullptr); + + bool has_addr() const noexcept { return has_addr_; } + std::uint32_t current_addr() const noexcept { return current_addr_; } + + // Display a freshly-read hex dump (called by MainWindow each tick). + void show_dump(const QString& text); + + Q_SIGNALS: + // Emitted when the user enters a new address (Enter). MainWindow reacts by + // reading the region immediately instead of waiting for the next tick. + void addr_changed(std::uint32_t addr); + + private: + QLineEdit* addr_input_; + QTextEdit* dump_view_; + std::uint32_t current_addr_ = 0; + bool has_addr_ = false; +}; + +} // namespace micro_forge::gui::panels diff --git a/gui/view/panels/serial_panel.cpp b/gui/view/panels/serial_panel.cpp index 96e1f4d..7b256f1 100644 --- a/gui/view/panels/serial_panel.cpp +++ b/gui/view/panels/serial_panel.cpp @@ -1,7 +1,7 @@ -// Serial output panel — see serial_panel.hpp. +// Serial panel — see serial_panel.hpp. #include "gui/view/panels/serial_panel.hpp" -#include +#include #include #include #include @@ -15,6 +15,15 @@ SerialPanel::SerialPanel(QWidget* parent) : QWidget(parent) { view_->setReadOnly(true); view_->setStyleSheet("font-family: monospace;"); lay->addWidget(view_); + + input_ = new QLineEdit; + input_->setPlaceholderText("Send to USART RX — type then Enter"); + lay->addWidget(input_); + + connect(input_, &QLineEdit::returnPressed, this, [this] { + emit inputSubmitted(input_->text()); + input_->clear(); + }); } void SerialPanel::refresh(const introspection::IntrospectionSnapshot& snap) { diff --git a/gui/view/panels/serial_panel.hpp b/gui/view/panels/serial_panel.hpp index 0f3e4d8..ff75e88 100644 --- a/gui/view/panels/serial_panel.hpp +++ b/gui/view/panels/serial_panel.hpp @@ -1,25 +1,33 @@ -// Serial output panel — read-only view of the USART bytes accumulated by the -// session. This is the "wow" surface: firmware output appears here as it runs. +// Serial panel — read-only view of USART bytes the session has accumulated, +// plus a text input to inject bytes into USART RX (A2). The input only emits +// a signal; MainWindow forwards it to model::Session so the panel itself stays +// free of simulator coupling. #pragma once #include "introspection/introspection.hpp" +#include #include +class QLineEdit; class QTextEdit; namespace micro_forge::gui::panels { class SerialPanel : public QWidget { Q_OBJECT - public: explicit SerialPanel(QWidget* parent = nullptr); void refresh(const introspection::IntrospectionSnapshot& snap); + Q_SIGNALS: + // Emitted on Enter in the input field — the raw text the user typed. + void inputSubmitted(const QString& text); + private: QTextEdit* view_; + QLineEdit* input_; }; } // namespace micro_forge::gui::panels diff --git a/gui/view/widgets/stm32_board_widget.cpp b/gui/view/widgets/stm32_board_widget.cpp new file mode 100644 index 0000000..7f941cb --- /dev/null +++ b/gui/view/widgets/stm32_board_widget.cpp @@ -0,0 +1,165 @@ +// STM32F103 board widget — see stm32_board_widget.hpp. +#include "gui/view/widgets/stm32_board_widget.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace micro_forge::gui::view { + +namespace { +constexpr int kChipW = 200; +constexpr int kChipH = 250; +constexpr int kPinGap = 22; +constexpr int kPinStart = 30; // first pin offset from chip top (room for label) +constexpr int kWireLen = 70; // pin wire length out to the LED +constexpr int kLedR = 10; // LED radius +} // namespace + +Stm32BoardWidget::Stm32BoardWidget(QWidget* parent) : QWidget(parent) { + setMinimumSize(480, 420); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +void Stm32BoardWidget::refresh( + const introspection::IntrospectionSnapshot& snap) { + odr_[0] = snap.peripherals.gpio[0].odr; // A + odr_[1] = snap.peripherals.gpio[1].odr; // B (rendered pins use A/C) + odr_[2] = snap.peripherals.gpio[2].odr; // C + update(); // async — never block the tick loop on a repaint. +} + +void Stm32BoardWidget::paintEvent(QPaintEvent*) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + p.fillRect(rect(), QColor(245, 245, 240)); // off-white PCB + + const int chip_x = (width() - kChipW) / 2; + const int chip_y = (height() - kChipH) / 2; + const QRect chip(chip_x, chip_y, kChipW, kChipH); + + // ── chip body ── + p.setPen(QPen(QColor(20, 20, 20), 1)); + p.setBrush(QColor(35, 35, 40)); + p.drawRoundedRect(chip, 10, 10); + p.setPen(Qt::NoPen); + p.setBrush(QColor(130, 130, 130)); + p.drawEllipse(chip.x() + 14, chip.y() + 14, 6, 6); // pin-1 dimple + + QFont bold = font(); + bold.setBold(true); + bold.setPointSize(13); + QFont small = font(); + small.setPointSize(8); + p.setFont(bold); + p.setPen(Qt::white); + p.drawText(chip, Qt::AlignHCenter | Qt::AlignTop, "STM32F103"); + p.setFont(small); + p.drawText(QPoint(chip.x() + kChipW / 2 - 24, chip.y() + 26), "Cortex-M3"); + + const auto bit = [](std::uint16_t odr, int pin) -> bool { + return (odr >> pin) & 1u; + }; + + // ── right side: PA0..PA7 LED row ── any port-A pin a firmware drives shows. + const auto rightLed = [&](int row, int pin) { + const int y = chip_y + kPinStart + row * kPinGap; + const int x0 = chip_x + kChipW; + const int x1 = x0 + kWireLen; + // pin stub + p.setPen(QPen(QColor(200, 200, 200), 1)); + p.setBrush(QColor(210, 210, 210)); + p.drawRect(x0 - 4, y - 3, 8, 6); + // wire (red when driven high, grey when low) + const bool on = bit(odr_[0], pin); + p.setPen(QPen(on ? QColor(200, 40, 40) : QColor(180, 180, 180), 2)); + p.drawLine(x0, y, x1, y); + // LED + const QRect led_rect(x1, y - kLedR, kLedR * 2, kLedR * 2); + p.setPen(QPen(QColor(60, 60, 60), 1)); + p.setBrush(on ? QColor(90, 220, 100) : QColor(70, 70, 70)); + p.drawEllipse(led_rect); + // pin name + p.setPen(QColor(40, 40, 40)); + p.setFont(small); + p.drawText(x0 + 6, y - 6, QString("PA%1").arg(pin)); + }; + for (int i = 0; i < 8; ++i) { + rightLed(i, i); // row i ↔ pin i (PA0..PA7) + } + + // Below the LED row: PA9/PA10 UART markers (wire + label, no LED). + const auto rightTag = [&](int row, const QString& name, + const QColor& wire, const QString& tag) { + const int y = chip_y + kPinStart + (8 + row) * kPinGap; + const int x0 = chip_x + kChipW; + const int x1 = x0 + kWireLen; + p.setPen(QPen(QColor(200, 200, 200), 1)); + p.setBrush(QColor(210, 210, 210)); + p.drawRect(x0 - 4, y - 3, 8, 6); + p.setPen(QPen(wire, 2)); + p.drawLine(x0, y, x1, y); + p.setPen(QColor(40, 40, 40)); + p.setFont(small); + p.drawText(x0 + 6, y - 6, name); + p.drawText(x1 + 4, y + 4, tag); + }; + rightTag(0, "PA9", QColor(40, 120, 200), "TX \xe2\x86\x92"); // → + rightTag(1, "PA10", QColor(40, 120, 200), "\xe2\x86\x90 RX"); // ← + + // ── left side: SWD debug port ── + const auto leftPin = [&](int row, const QString& name) { + const int y = chip_y + kPinStart + row * kPinGap; + const int x0 = chip_x; + const int x1 = x0 - kWireLen; + p.setPen(QPen(QColor(200, 200, 200), 1)); + p.setBrush(QColor(210, 210, 210)); + p.drawRect(x0 - 4, y - 3, 8, 6); + p.setPen(QPen(QColor(150, 150, 150), 2)); + p.drawLine(x0, y, x1, y); + p.setPen(QColor(40, 40, 40)); + p.setFont(small); + p.drawText(x1 - 30, y - 6, name); + }; + leftPin(0, "PA13"); + leftPin(1, "PA14"); + p.setPen(QColor(90, 90, 90)); + p.setFont(small); + p.drawText(QPoint(chip_x - kWireLen, chip_y + kPinStart + 2 * kPinGap + 14), + "SWD"); + + // ── bottom: PC13 board LED ── + { + const int x = chip_x + kChipW / 2; + const int y0 = chip_y + kChipH; + const int y1 = y0 + 38; + p.setPen(QPen(QColor(200, 200, 200), 1)); + p.setBrush(QColor(210, 210, 210)); + p.drawRect(x - 3, y0 - 4, 6, 8); + p.setPen(QPen(QColor(40, 160, 200), 2)); + p.drawLine(x, y0, x, y1); + const QRect led_rect(x - kLedR, y1, kLedR * 2, kLedR * 2); + const bool on = bit(odr_[2], 13); + p.setPen(QPen(QColor(60, 60, 60), 1)); + p.setBrush(on ? QColor(90, 220, 100) : QColor(70, 70, 70)); + p.drawEllipse(led_rect); + p.setPen(QColor(40, 40, 40)); + p.setFont(small); + p.drawText(led_rect.x() - 10, led_rect.bottom() + 12, "PC13 / LED"); + } + + // ── legend ── + p.setPen(QColor(130, 130, 130)); + p.setFont(small); + p.drawText(12, height() - 12, "LED lit = pin drives high (ODR bit set)"); +} + +} // namespace micro_forge::gui::view diff --git a/gui/view/widgets/stm32_board_widget.hpp b/gui/view/widgets/stm32_board_widget.hpp new file mode 100644 index 0000000..4496378 --- /dev/null +++ b/gui/view/widgets/stm32_board_widget.hpp @@ -0,0 +1,38 @@ +// STM32F103 board widget — paints the chip + pins wired to external LEDs / +// UART / SWD. The LEDs reflect GPIO ODR bits in real time: micro-forge's +// distinctive advantage over a real-hardware debugger (which can't show "the +// light is on" without staring at a physical board). Read-only output; +// pin-level input injection is handled in the gpio_panel (A2 buttons). +// +// Port A is drawn as a row of 8 LEDs (PA0..PA7) so ANY demo firmware driving a +// port-A pin shows up — gpio_blink uses PA5, the F103 CubeMX example uses PA1. +// PC13 (board LED) + USART1 TX/RX + SWD are also drawn. +#pragma once + +#include "introspection/introspection.hpp" + +#include +#include + +#include + +namespace micro_forge::gui::view { + +class Stm32BoardWidget : public QWidget { + Q_OBJECT + public: + explicit Stm32BoardWidget(QWidget* parent = nullptr); + + // Update rendered pin levels from the snapshot, then request an async repaint. + void refresh(const introspection::IntrospectionSnapshot& snap); + + protected: + void paintEvent(QPaintEvent* event) override; + + private: + // GPIO ODR per port A/B/C — bit i = pin i driven high. Saved by refresh(), + // read by paintEvent() (Qt may repaint at any time, independent of ticks). + std::array odr_{}; +}; + +} // namespace micro_forge::gui::view diff --git a/include/arch/arm/cortex_m3/cortex_m3.hpp b/include/arch/arm/cortex_m3/cortex_m3.hpp index cfa05d7..6a418a1 100644 --- a/include/arch/arm/cortex_m3/cortex_m3.hpp +++ b/include/arch/arm/cortex_m3/cortex_m3.hpp @@ -42,6 +42,13 @@ class CortexM3CPU : public CPU { void launch() noexcept { current_status_ = State::Running; }; + // Raw observer pointers, not WeakPtr — intentional (C2 close-out). NVIC/SCB + // live in Stm32f103Parts, a SoC member alongside the CPU, so they outlive + // it and there is NO reference cycle to break (NVIC→CPU uses callbacks / + // WeakPtr; the CPU→NVIC link is one-way). Used on the exception path + // (raise_irq, priority lookup) — not per-instruction. Same reasoning as the + // bus_ pointer above; see notes 041. A WeakPtr here would add a + // control-block deref + IsValid with no cycle-prevention benefit. void set_nvic(periph::NvicPeripheral& nvic) { nvic_ = &nvic; } void set_scb(periph::ScbPeripheral& scb) { scb_ = &scb; } void set_vector_table_base(addr_t base) { vector_table_base_ = base; } diff --git a/include/introspection/introspection.hpp b/include/introspection/introspection.hpp index 9ab4290..5c485e6 100644 --- a/include/introspection/introspection.hpp +++ b/include/introspection/introspection.hpp @@ -65,12 +65,24 @@ struct ScbSnapshot { uint8_t prigroup = 0; // AIRCR bits [10:8] }; +// Clock tree — SYSCLK (source after PLL) → HCLK (AHB/CPU) → APB1/APB2 +// prescalers. The sim tracks 3 domains (Sysclk/Apb1/Apb2); the AHB prescaler +// is not modeled, so HCLK is reported equal to SYSCLK. RCC frequency changes +// surface here on the next snapshot (B1). +struct ClockSnapshot { + uint32_t sysclk = 0; + uint32_t hclk = 0; + uint32_t apb1 = 0; + uint32_t apb2 = 0; +}; + struct PeripheralsSnapshot { std::string_view usart_output; std::array gpio{}; // ports A, B, C (in that order) SysTickSnapshot systick; NvicSnapshot nvic; ScbSnapshot scb; + ClockSnapshot clock; }; struct IntrospectionSnapshot { diff --git a/scripts/check_test_count.sh b/scripts/check_test_count.sh index c0e5a63..84a229c 100755 --- a/scripts/check_test_count.sh +++ b/scripts/check_test_count.sh @@ -17,7 +17,7 @@ set -euo pipefail BUILD="${1:-build}" -BASELINE=357 # floor = PRODUCT test count — only ever raised, never lowered (2026-07-08: 353→357, GUI Session tests) +BASELINE=360 # floor = PRODUCT test count — only ever raised, never lowered (2026-07-08: 357→360, A2 input-injection tests) if [[ ! -d "$BUILD" ]]; then echo "skip: build dir '$BUILD' not found (run cmake configure first)" diff --git a/src/introspection/introspection.cpp b/src/introspection/introspection.cpp index efbbd96..0cc78d3 100644 --- a/src/introspection/introspection.cpp +++ b/src/introspection/introspection.cpp @@ -5,6 +5,7 @@ #include "introspection/introspection.hpp" #include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "chips/stm32f1/soc/clock_domains.hpp" #include "chips/stm32f1/soc/stm32f103_soc.hpp" #include "cpu/cpu.hpp" @@ -13,7 +14,9 @@ using namespace micro_forge; using micro_forge::cpu::CPU; +using micro_forge::chips::stm32f1::ClockDomain; using micro_forge::chips::stm32f1::Stm32f103Soc; +using micro_forge::chips::stm32f1::domain_index; namespace micro_forge::introspection { @@ -37,6 +40,16 @@ IntrospectionSnapshot read_introspection(Stm32f103Soc& soc, p.scb = {parts.scb.icsr(), parts.scb.vtor(), parts.scb.aircr(), parts.scb.prigroup()}; + // Clock tree (B1): 3 sim domains queried via the coordinator's VirtualClock. + if (soc.machine().coord) { + auto& clk = soc.machine().coord->clock(); + p.clock.sysclk = clk.sysclk_freq_hz(); + // AHB prescaler is not modeled → HCLK mirrors SYSCLK. + p.clock.hclk = clk.sysclk_freq_hz(); + p.clock.apb1 = clk.domain_freq_hz(domain_index(ClockDomain::Apb1)); + p.clock.apb2 = clk.domain_freq_hz(domain_index(ClockDomain::Apb2)); + } + auto cm3 = soc.cortex_m3_cpu(); if (!cm3.IsValid()) { return snap; diff --git a/test/test_cli.cpp b/test/test_cli.cpp index b542fb2..d7d6f9a 100644 --- a/test/test_cli.cpp +++ b/test/test_cli.cpp @@ -88,3 +88,32 @@ TEST(Cli, UnmappedPcFaults) { EXPECT_NE(r.out.find("Faulted"), std::string::npos) << r.out; EXPECT_NE(r.out.find("InstructionFetchFault"), std::string::npos) << r.out; } + +// probe hello.elf → the simulator implements every opcode this firmware uses +// (it runs and prints "Hello"), so probe mode reports full coverage, exit 0 (A4). +TEST(Cli, ProbeHelloReportsNoMissingOpcodes) { + auto r = run_cli(std::string("probe ") + CLI_HELLO_ELF + + " --max-steps 100000"); + EXPECT_EQ(r.code, 0) << r.out; + EXPECT_NE(r.out.find("[probe]"), std::string::npos) << r.out; + EXPECT_NE(r.out.find("no missing opcodes"), std::string::npos) << r.out; +} + +// probe a firmware whose reset vector lands on 0xFFFF (an unimplemented +// opcode) → probe mode records it and skips, reporting the missing encoding. +TEST(Cli, ProbeReportsMissingOpcode) { + { + std::ofstream f("/tmp/cli_probe.bin", std::ios::binary); + const uint32_t sp = 0x20005000u; + const uint32_t pc = 0x08000009u; // reset → 0x08000008 (thumb) + const uint16_t bad_insn = 0xFFFFu; + f.write(reinterpret_cast(&sp), 4); + f.write(reinterpret_cast(&pc), 4); + f.write(reinterpret_cast(&bad_insn), 2); + } + auto r = + run_cli("probe /tmp/cli_probe.bin --base 0x08000000 --max-steps 50"); + EXPECT_EQ(r.code, 0) << r.out; + EXPECT_NE(r.out.find("missing opcode"), std::string::npos) << r.out; + EXPECT_NE(r.out.find("0xFFFF"), std::string::npos) << r.out; +} diff --git a/test/test_e2e.cpp b/test/test_e2e.cpp index 26e4f54..50a7873 100644 --- a/test/test_e2e.cpp +++ b/test/test_e2e.cpp @@ -71,8 +71,13 @@ TEST(E2E, GpioBlink) { ASSERT_NE(*state, cpu::CPU::State::Faulted) << "CPU faulted during execution"; - EXPECT_GE(toggle_count, 6) - << "Expected at least 6 PA5 toggles, got " << toggle_count; + // Report the real toggle count so the blink rate is observable, and + // tighten the floor to the real lower bound (the old >=6 was far below + // actual and hid regressions). ~33 toggles/4M steps ≈ 300 ms/toggle at 1×. + std::cout << "[GpioBlink] PA5 toggles over 4M steps = " << toggle_count + << "\n"; + EXPECT_GE(toggle_count, 25) + << "Expected >= 25 PA5 toggles, got " << toggle_count; } #ifdef E2E_HAL_UART_ELF diff --git a/test/test_gui_session.cpp b/test/test_gui_session.cpp index ec0ac4f..a2e2d98 100644 --- a/test/test_gui_session.cpp +++ b/test/test_gui_session.cpp @@ -35,6 +35,51 @@ TEST(GuiSession, StepOnHaltedSessionStaysValid) { EXPECT_EQ(s.snapshot().cpu.state, CPU::State::Halted); } +// A2 input injection: inject_rx / simulate_gpio_input must be safe on a valid +// session (no crash, no state corruption) and silent no-ops on an invalid one. +TEST(GuiSession, InjectRxOnValidSessionIsSafe) { + Session s; + ASSERT_TRUE(s.rebuild().has_value()); + s.inject_rx('A'); + s.inject_rx('B'); + EXPECT_TRUE(s.valid()); + // No firmware reads RX, so the session stays Halted; bytes just queue. + EXPECT_EQ(s.snapshot().cpu.state, CPU::State::Halted); +} + +TEST(GuiSession, SimulateGpioInputIsSafe) { + Session s; + ASSERT_TRUE(s.rebuild().has_value()); + s.simulate_gpio_input('A', 5, true); + s.simulate_gpio_input('C', 13, false); + EXPECT_TRUE(s.valid()); + EXPECT_EQ(s.snapshot().cpu.state, CPU::State::Halted); +} + +TEST(GuiSession, InjectOnInvalidSessionIsNoOp) { + Session s; // never rebuilt → invalid + s.inject_rx('X'); + s.simulate_gpio_input('A', 0, true); + EXPECT_FALSE(s.valid()); + const auto snap = s.snapshot(); + EXPECT_EQ(snap.cpu.state, CPU::State::Halted); + EXPECT_EQ(snap.cycles, 0u); +} + +// C4-mem: read_memory returns a formatted hex dump on a valid session and an +// empty string when invalid (no SoC to read from). +TEST(GuiSession, ReadMemoryOnValidSessionReturnsDump) { + Session s; + ASSERT_TRUE(s.rebuild().has_value()); + const auto dump = s.read_memory(0x08000000u, 16); + EXPECT_FALSE(dump.empty()) << "memory_dump should format 16 bytes"; +} + +TEST(GuiSession, ReadMemoryOnInvalidSessionIsEmpty) { + Session s; // never rebuilt → invalid + EXPECT_TRUE(s.read_memory(0x08000000u, 16).empty()); +} + #ifdef SESSION_HELLO_ELF TEST(GuiSession, LoadsFirmwareAndRunsAndEmitsUsart) { Session s; diff --git a/test/test_introspection.cpp b/test/test_introspection.cpp index e413e82..c46478d 100644 --- a/test/test_introspection.cpp +++ b/test/test_introspection.cpp @@ -86,6 +86,21 @@ TEST(Introspection, FreshSocPeripheralsAreQuiescent) { EXPECT_EQ(snap.peripherals.scb.prigroup, 0u); } +TEST(Introspection, FreshSocClockTreeIsHsiDefault) { + auto soc = Stm32f103Soc::create(); + ASSERT_TRUE(soc.has_value()); + const auto snap = read_introspection(**soc, ""); + + // STM32F103 reset default: HSI 8 MHz, no PLL, all buses undivided (8/8/8). + constexpr uint32_t k8MHz = 8'000'000u; + const auto& clk = snap.peripherals.clock; + EXPECT_EQ(clk.sysclk, k8MHz); + EXPECT_EQ(clk.apb1, k8MHz); + EXPECT_EQ(clk.apb2, k8MHz); + // AHB prescaler is not modeled → HCLK mirrors SYSCLK. + EXPECT_EQ(clk.hclk, clk.sysclk); +} + #ifdef INTROSPECTION_HELLO_ELF namespace { std::vector read_file(const std::string& path) {