Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
103 changes: 102 additions & 1 deletion cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
#include <cstdlib>
#include <fstream>
#include <iterator>
#include <map>
#include <span>
#include <string>
#include <utility>
#include <vector>

using namespace micro_forge;
Expand Down Expand Up @@ -135,7 +137,10 @@ void print_usage() {
" (runs forever by default; Ctrl+C stops with a report)\n"
" dump-mem <firmware.{elf,bin}> --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 <firmware.{elf,bin}> [--max-steps 1000000]\n"
" [--chip stm32f103] [--base 0x08000000]\n"
" (runs in probe mode — lists opcodes the simulator can't execute)\n");
}

struct RunOptions {
Expand Down Expand Up @@ -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<uint32_t>(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<size_t>(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<std::pair<uint16_t, uint16_t>, size_t> counts;
std::map<std::pair<uint16_t, uint16_t>, 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<uint32_t>(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) {
Expand All @@ -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;
Expand Down
41 changes: 41 additions & 0 deletions document/notes/040-gui-debugger-skeleton.md
Original file line number Diff line number Diff line change
@@ -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<void, std::string>` 上报创建/读取/加载错误;`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 对拍)。
41 changes: 41 additions & 0 deletions document/notes/041-c2-set-nvic-architecture-debt.md
Original file line number Diff line number Diff line change
@@ -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,正向裸指针安全。
59 changes: 59 additions & 0 deletions document/notes/042-abc-deepen-batch.md
Original file line number Diff line number Diff line change
@@ -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 <fw>`——`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 对拍)。用户要求"记得迟早要做"。
3 changes: 3 additions & 0 deletions gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/.
Expand Down
Loading
Loading