diff --git a/document/notes/043-tamcpp-spike-loader-cpu-bugs.md b/document/notes/043-tamcpp-spike-loader-cpu-bugs.md new file mode 100644 index 0000000..1f283e1 --- /dev/null +++ b/document/notes/043-tamcpp-spike-loader-cpu-bugs.md @@ -0,0 +1,66 @@ +# 043 · 线 A A1 点灯 spike——C++ HAL 样例跑通 + 两处 loader/CPU bug + +> 日期: 2026-07-08 +> 分支: `feat/run-tamcpp-samples`(worktree,从 `feat/deepen-gui-and-core` 97c90d3 分) +> 基线: ctest 367 绿 +> 状态: A1 spike 通(PC13 toggle=1>0),修 elf_loader + cortex_m3 两 bug,ctest 368 绿 +> 后置: A2 串口(3_uart_logger TX→EventBus) / A3 样例卫生 / A4 跨仓库文档 + +## 背景 + +线 A 任务:让 micro-forge 加载并运行 `~/Tutorial_AwesomeModernCPP` 的 C++ HAL 样例,读者不买板子也能跑教程。A1 点灯 spike 是生死验证——确认 TAMCPP `1_led_control` 的 C++ elf 能在模拟器跑(GPIO PC13 toggle>0)。 + +最大未知:C++ 程序 main 前要 `__libc_init_array` 初始化全局对象,且 `HAL_Delay` 依赖 SysTick 中断递增 `uwTick`。micro-forge 的 elf_loader 之前只加载过 C 样例,这两条链路没验证过。TAMCPP `third_party/STM32F1` 是空目录,样例编译借 micro-forge 自带的 STM32CubeF1 HAL(不改 TAMCPP,跨仓库纪律)。 + +## 卡点 1:elf_loader .data 段漏 flash LMA 副本 + +**现象**:spike 跑出 toggle=0,PC 卡在 `HAL_Delay` 的 `while(HAL_GetTick()-start0)。uwTick=0x4E3=1259(>1000,第一个 HAL_Delay(500) 返回 + led.on() + 第二个 HAL_Delay(500) + led.off())。 + +## 陷阱小结 + +- **busy-loop 掩盖 bug**:hal_blink 用 `delay_ms` 不碰 SysTick,掩盖了 .data LMA + SysTick 优先级两个 bug。spike 必须用真实 HAL_Delay 链路才暴露。 +- **COUNTFLAG 不代表次数**:Ctrl bit16 COUNTFLAG 置位不清(读 VAL/CTRL 才清),只说明"到过 0",不说明次数。诊断时一度误以为只到 0 一次,实际 312 次。 +- **TAMCPP third_party 空**:`~/Tutorial_AwesomeModernCPP/third_party/STM32F1` 是空目录,样例编译借 micro-forge 的 STM32CubeF1(A0 用 `/tmp/tamcpp-build/build.sh` 手动编出 elf)。 +- **worktree submodule**:git worktree 的 `git submodule update --init` 报 "Unable to find current revision"(worktree gitdir 链接问题),symlink 主仓 STM32CubeF1 临时跑 ctest(commit 不含 symlink,只 add 具体文件)。 + +## 后置 + +- **A2 串口**:加载 TAMCPP `3_uart_logger`,TX 经 EventBus(UartByte)→ GUI serial 面板可见;进 ctest E2E(参考 `test/CMakeLists.txt:175` hal_uart 模式)。tamcpp_blink 的 firmware build 正规化(借 STM32CubeF1 + TAMCPP 源码路径)。 +- **A3 样例卫生**:`examples/hal_blink` `examples/hal_uart` 的 `CUBE_BASE` 参数化 + README + 重编验证仍通。 +- **A4 跨仓库文档**:TAMCPP `documents/vol8-domains/embedded/` 补"用 micro-forge 无硬件跑教程"(跨仓库改动先报用户确认)。 diff --git a/document/notes/044-tamcpp-samples-spike.md b/document/notes/044-tamcpp-samples-spike.md new file mode 100644 index 0000000..b0a84cd --- /dev/null +++ b/document/notes/044-tamcpp-samples-spike.md @@ -0,0 +1,42 @@ +# 044 — TAMCPP C++ HAL 样例 spike(线 A) + +> 让 micro-forge 加载运行 TAMCPP(`~/Tutorial_AwesomeModernCPP/code/stm32f1-tutorials`)的 C++ HAL 样例(1_led/2_button/3_uart)。TAMCPP 是 C++(main.cpp + device/ + system/),micro-forge 之前只加载过 C 样例(hal_blink/hal_uart)。 + +## 已修 6 个模拟器 bug + +1. **elf_loader .data LMA**(043):按 `p_vaddr` 加载 .data 到 SRAM,漏了 flash LMA 副本;裸机 startup `CopyDataInit` 从 `_sidata`(flash LMA)复制,读到 0 覆盖 SRAM → `SystemCoreClock=0` → SysTick 不配 → `HAL_Delay` 死循环。修:PT_LOAD 若 `p_paddr≠p_vaddr`,额外写副本到 `p_paddr`(flash LMA)。hal_blink 因用 busy-loop `delay_ms`(不碰 SysTick)从未暴露。 +2. **cortex_m3 SysTick 线程优先级**(043):线程模式 `active_preempt` 用 0xFF,被 `preempt_priority`(0xF)截断,SysTick(0xF0)无法抢占线程 → `uwTick` 不涨。修:线程用 0xFF(不截断)。 +3. **nvic IPR 字节写**(本批):`HAL_NVIC_SetPriority` 用 `strb` 写 `IP[IRQn]`,原 NVIC write 只支持 Word 返回 `Unaligned` → `DataAccessFault`。修:IPR 分支提前 + 按 width 更新字节,ISER/ICER 等仍 Word。TAMCPP 3_uart `enable_interrupt` 因此通。 +4. **IT mask 解码**:`HAL_GPIO_ReadPin` 的 `ite ne` 被当成两个 THEN 槽,导致 `movne`/`moveq` 都执行、返回值恒为 0。修正 slot 对应的 mask bit 后,高低电平可正确返回 1/0。 +5. **异常未保存 ITSTATE**:模拟器把 IT 条件保存在独立 `it_conditions_` 中,SysTick 若打断 `HAL_GPIO_ReadPin` 的 `ITE` 块,handler 会消费线程的条件,异常返回也不恢复。表现为 IDR 始终为 1 但 `ReadPin` 偶发返回 0,每次假按下都重置 release 去抖。修:异常 entry 按嵌套层级保存并清空 IT 状态,return 恢复;新增精确回归测试。 +6. **t16_pop `pop {pc}` SP writeback 顺序**(本批):`POP {pc}` 弹出 EXC_RETURN 时,`write_pc` 先触发 `interrupt_return`(从 SP 弹 8 字异常帧并恢复 SP),但 t16_pop 随后用局部 `sp`(尚未 +4)writeback,**覆盖了 interrupt_return 刚恢复的 SP** → 每次 exception 净漏 0x20。3_uart_logger 的 USART1 RXNE 中断每收一字节 sp 偏 0x20 → `line_buf`(@sp+16)地址漂移 → `strb` 写错位置 → `handle_command` 的 `cmd=="LED ON"` 比较恒失败 → 回 `ERR`。修:`pop {pc}` 先 writeback `sp+4`、再 `write_pc`,让 interrupt_return 从正确位置弹帧。SysTick handler 不经 `pop {pc}`(走 `bx lr`),故此前一直平衡、未被本题暴露。 + +## spike 状态 + +- **1_led_control 通**:PC13 toggle=1(A1 生死验证,C++ elf 能在模拟器跑)。 +- **2_button_control 通**:PA0 idle=1 → press=0 → release=1;Pressed 写 PC13 BSRR reset (`0x20000000`),Released 在 20ms 后写 set (`0x2000`),runner 验证 `on=1 off=1 ... [PASS]`。 +- **3_uart_logger 通**:TX `UART Logger Ready!\r\n`(USART1 TX 链路与 hal_uart E2E 同);RX 注入 `LED ON\r\n` → RXNE 中断 → ring → main `handle_command` 回 `OK: LED ON\r\n`(全链路通)。 + +## firmware build 正规化 + +- `examples/tamcpp_blink/firmware/build_sample.sh`:参数化编 TAMCPP 1/2/3 C++ elf(借 micro-forge 的 STM32CubeF1 HAL —— TAMCPP `third_party/STM32F1` 是空目录,不改 TAMCPP)。gcc 编 .c、g++ 编 .cpp(`-std=c++23 -fno-exceptions -fno-rtti`)、g++ 链接(libstdc++ init)。 +- `examples/tamcpp_blink/firmware/CMakeLists.txt`:参数化 `foreach(led/button/uart)`,`TAMCPP_ROOT` cache 变量(默认 `$ENV{TAMCPP_ROOT}` 或 `$HOME/Tutorial_AwesomeModernCPP/...`)+ STM32CubeF1 存在性门禁,绝不进 git。 +- 链接脚本用 TAMCPP 的 `STM32F103C8TX_FLASH.ld`(带 `.init_array`,C++ 全局构造必需;不能用 micro-forge 的 `STM32F103_HAL.ld`——它无 `.init_array`)。 + +## 陷阱 + +- **TAMCPP third_party/STM32F1 空目录**:借 micro-forge 的 STM32CubeF1(不改 TAMCPP,跨仓库纪律)。 +- **-std=c++23**:`uart_driver.hpp` 用 `std::expected`(C++23),build_sample.sh 必须加(1/2 不用 std::expected,c++17 够,但统一加无害)。 +- **worktree STM32CubeF1 submodule 未 init**:hal_blink/hal_uart/E2E 被 skip(ctest 不破);tamcpp firmware 需 symlink 主仓的 STM32CubeF1。 +- **C++ 启动**:startup `__libc_init_array` 调 `.init_array`(Meyers singleton `ClockConfig::instance()`);1_led 已验证通。 + +## 验证 + +- ctest 全绿,新增 `ExceptionPreservesInterruptedItBlock` 防止 ITSTATE 跨异常回归。 +- led spike:PC13 toggle=1。 +- button spike:`on=1 off=1 PA0=1 PC13=1 state=0 [PASS]`。 +- uart spike:TX `UART Logger Ready!` + RX `OK: LED ON`(`TX banner=1 RX cmd=1`)。 + +## 状态(线 A 完成) + +`E2E.Tamcpp{Led,Button,Uart}` 三 TEST 进 ctest(ctest 372 绿)。线 A 三样例全通,6 个模拟器 bug 修完:elf_loader `.data` LMA / SysTick 线程优先级 / nvic IPR 字节写 / IT mask 解码 / ITSTATE 异常保存 / `t16_pop` pop{pc} SP writeback。分支 `feat/run-tamcpp-samples`(本批 commit 本地未 push)。 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 294fe61..2c1e71e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(systick) add_subdirectory(hal_blink) add_subdirectory(hal_uart) add_subdirectory(hook_demo) +add_subdirectory(tamcpp_blink) diff --git a/examples/tamcpp_blink/CMakeLists.txt b/examples/tamcpp_blink/CMakeLists.txt new file mode 100644 index 0000000..3969edd --- /dev/null +++ b/examples/tamcpp_blink/CMakeLists.txt @@ -0,0 +1,54 @@ +# TAMCPP C++ HAL 样例(1_led_control / 2_button_control / 3_uart_logger)。 +# elf 由 firmware/ 参数化编译(借 micro-forge 的 STM32CubeF1 HAL,不改 TAMCPP 仓库)。 +# runner 加载 elf 验证:led 看 PC13 toggle、button 注入 PA0 看 toggle、uart 看 TX 输出。 + +# TAMCPP_ROOT: stm32f1-tutorials 根,绝不硬编码进 git(cache 变量 + 环境变量 + 默认)。 +if(NOT TAMCPP_ROOT) + if(DEFINED ENV{TAMCPP_ROOT}) + set(TAMCPP_ROOT "$ENV{TAMCPP_ROOT}" CACHE PATH "TAMCPP stm32f1-tutorials root") + else() + set(TAMCPP_ROOT "$ENV{HOME}/Tutorial_AwesomeModernCPP/code/stm32f1-tutorials" + CACHE PATH "TAMCPP stm32f1-tutorials root") + endif() +endif() +if(NOT EXISTS "${TAMCPP_ROOT}/1_led_control/main.cpp") + message(STATUS "TAMCPP_ROOT not found (${TAMCPP_ROOT}), skipping tamcpp samples") + return() +endif() +message(STATUS "TAMCPP_ROOT = ${TAMCPP_ROOT}") + +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/STM32CubeF1/Drivers") + message(STATUS "STM32CubeF1 submodule not found, skipping tamcpp samples") + return() +endif() + +add_subdirectory(firmware) + +# ── runner: 每样例一个(elf 由 firmware target 编出,POST_BUILD copy 到 runner 目录)─ +add_executable(tamcpp_blink_runner runner.cpp) +target_link_libraries(tamcpp_blink_runner PRIVATE micro_forge) +add_dependencies(tamcpp_blink_runner tamcpp_led_firmware) +add_custom_command(TARGET tamcpp_blink_runner POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/firmware/led.elf + ${CMAKE_CURRENT_BINARY_DIR}/tamcpp_led.elf) + +if(TARGET tamcpp_button_firmware) + add_executable(tamcpp_button_runner runner_button.cpp) + target_link_libraries(tamcpp_button_runner PRIVATE micro_forge) + add_dependencies(tamcpp_button_runner tamcpp_button_firmware) + add_custom_command(TARGET tamcpp_button_runner POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/firmware/button.elf + ${CMAKE_CURRENT_BINARY_DIR}/tamcpp_button.elf) +endif() + +if(TARGET tamcpp_uart_firmware) + add_executable(tamcpp_uart_runner runner_uart.cpp) + target_link_libraries(tamcpp_uart_runner PRIVATE micro_forge) + add_dependencies(tamcpp_uart_runner tamcpp_uart_firmware) + add_custom_command(TARGET tamcpp_uart_runner POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/firmware/uart.elf + ${CMAKE_CURRENT_BINARY_DIR}/tamcpp_uart.elf) +endif() diff --git a/examples/tamcpp_blink/firmware/CMakeLists.txt b/examples/tamcpp_blink/firmware/CMakeLists.txt new file mode 100644 index 0000000..dae1be5 --- /dev/null +++ b/examples/tamcpp_blink/firmware/CMakeLists.txt @@ -0,0 +1,41 @@ +# 参数化编译 TAMCPP 1/2/3 样例的 C++ elf,借 micro-forge 的 STM32CubeF1 HAL。 +# 不复制 TAMCPP 源码(绝对路径引用),用 build_sample.sh 编译。 +# TAMCPP_ROOT 由父 CMakeLists 解析(cache 变量,不进 git)。 + +find_program(ARM_GCC arm-none-eabi-gcc) +find_program(ARM_GXX arm-none-eabi-g++) +find_program(ARM_OBJCOPY arm-none-eabi-objcopy) +if(NOT ARM_GCC OR NOT ARM_GXX OR NOT ARM_OBJCOPY) + message(STATUS "arm-none-eabi toolchain incomplete, skipping TAMCPP firmware") + return() +endif() + +set(CUBE_BASE "${CMAKE_CURRENT_SOURCE_DIR}/../../../third_party/STM32CubeF1") +set(OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + +# 编一个样例:short(产物名/ target 后缀)、dir(TAMCPP 子目录)、extra(额外 .cpp) +function(tamcpp_build_sample short dir) + set(extra ${ARGN}) + set(sample ${TAMCPP_ROOT}/${dir}) + set(ld ${sample}/STM32F103C8TX_FLASH.ld) + set(deps ${CMAKE_CURRENT_SOURCE_DIR}/build_sample.sh ${ld} + ${sample}/main.cpp ${sample}/system/clock.cpp + ${sample}/system/hal_mock.c ${sample}/system/syscall.c) + foreach(e ${extra}) + list(APPEND deps ${sample}/${e}) + endforeach() + add_custom_command( + OUTPUT ${OUT_DIR}/${short}.elf ${OUT_DIR}/${short}.bin + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/build_sample.sh + ${short} ${dir} ${TAMCPP_ROOT} ${CUBE_BASE} ${OUT_DIR} ${extra} + DEPENDS ${deps} + COMMENT "[tamcpp/${short}] building ${short}.elf (STM32CubeF1 + TAMCPP C++)" + VERBATIM + ) + add_custom_target(tamcpp_${short}_firmware ALL + DEPENDS ${OUT_DIR}/${short}.elf ${OUT_DIR}/${short}.bin) +endfunction() + +tamcpp_build_sample(led 1_led_control) +tamcpp_build_sample(button 2_button_control) +tamcpp_build_sample(uart 3_uart_logger system/printf_redirect.cpp system/uart_irq.cpp) diff --git a/examples/tamcpp_blink/firmware/build_sample.sh b/examples/tamcpp_blink/firmware/build_sample.sh new file mode 100644 index 0000000..0263f29 --- /dev/null +++ b/examples/tamcpp_blink/firmware/build_sample.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# 编译单个 TAMCPP C++ 样例 elf,借 micro-forge 的 STM32CubeF1 HAL(不改 TAMCPP 仓库)。 +# 用法:build_sample.sh [extra_cpp...] +# extra_cpp: 相对样例根的额外 .cpp +# 3_uart_logger 需: system/printf_redirect.cpp system/uart_irq.cpp +set -euo pipefail +short=$1; dir=$2; tamcpp_root=$3; cube=$4; out=$5; shift 5 +extra=("$@") + +sample=$tamcpp_root/$dir +ld=$sample/STM32F103C8TX_FLASH.ld +cmsis=$cube/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates +obj=$out/obj_$short +mkdir -p "$obj" + +common=(-mcpu=cortex-m3 -mthumb -O2 -g -Wall -Wextra -Wno-missing-field-initializers + -ffunction-sections -fdata-sections -DUSE_HAL_DRIVER -DSTM32F103xB -DHSE_VALUE=8000000) +inc=(-I"$sample" -I"$sample/system" -I"$cube/Drivers/CMSIS/Include" + -I"$cube/Drivers/CMSIS/Device/ST/STM32F1xx/Include" -I"$cube/Drivers/STM32F1xx_HAL_Driver/Inc") + +echo "--- [tamcpp/$short] C sources (HAL + system + syscall) ---" +for f in "$cube"/Drivers/STM32F1xx_HAL_Driver/Src/*.c "$cmsis/system_stm32f1xx.c" "$sample/system/hal_mock.c" "$sample/system/syscall.c"; do + [[ "$f" == *_template.c ]] && continue + arm-none-eabi-gcc "${common[@]}" "${inc[@]}" -c "$f" -o "$obj/$(basename "$f" .c).o" +done + +echo "--- [tamcpp/$short] C++ sources (main + clock + extra) ---" +arm-none-eabi-g++ "${common[@]}" -std=c++23 -fno-exceptions -fno-rtti "${inc[@]}" -c "$sample/main.cpp" -o "$obj/main.o" +arm-none-eabi-g++ "${common[@]}" -std=c++23 -fno-exceptions -fno-rtti "${inc[@]}" -c "$sample/system/clock.cpp" -o "$obj/clock.o" +if [[ ${#extra[@]} -gt 0 ]]; then + for e in "${extra[@]}"; do + arm-none-eabi-g++ "${common[@]}" -std=c++23 -fno-exceptions -fno-rtti "${inc[@]}" -c "$sample/$e" -o "$obj/$(basename "$e" .cpp).o" + done +fi + +echo "--- [tamcpp/$short] startup.s ---" +arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -x assembler-with-cpp -c "$cmsis/gcc/startup_stm32f103xb.s" -o "$obj/startup.o" + +echo "--- [tamcpp/$short] link (g++ for libstdc++ init) ---" +arm-none-eabi-g++ -mcpu=cortex-m3 -mthumb -T"$ld" -nostartfiles -specs=nano.specs -specs=nosys.specs \ + -Wl,--gc-sections -Wl,-Map="$out/$short.map" -o "$out/$short.elf" "$obj"/*.o +arm-none-eabi-objcopy -O binary "$out/$short.elf" "$out/$short.bin" +arm-none-eabi-size --format=berkeley "$out/$short.elf" +echo "=== [tamcpp/$short] OK: $out/$short.elf ===" diff --git a/examples/tamcpp_blink/runner.cpp b/examples/tamcpp_blink/runner.cpp new file mode 100644 index 0000000..3a6a51c --- /dev/null +++ b/examples/tamcpp_blink/runner.cpp @@ -0,0 +1,86 @@ +// A1 spike: 加载 TAMCPP 1_led_control 的 C++ elf,看 GPIO PC13 toggle count>0。 +// 验证 C++ HAL 样例(__libc_init_array + HAL_Delay/SysTick 中断链路)能在模拟器跑。 +// 模板抄自 examples/hal_blink/runner.cpp,改:监听 gpioc pin13(TAMCPP 板载 LED 在 PC13, +// ActiveLevel::Low)。elf 由 /tmp/tamcpp-build/build.sh 编出(借 STM32CubeF1 HAL)。 +#include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "chips/stm32f1/soc/stm32f103_soc.hpp" + +#include +#include +#include + +using namespace micro_forge; +using namespace micro_forge::cpu::arm::cortex_m3; +using namespace micro_forge::chips::stm32f1; + +static std::vector read_file(const char* path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return {}; + } + return {std::istreambuf_iterator(f), {}}; +} + +int main(int argc, char** argv) { + const char* elf_path = (argc > 1) ? argv[1] : "tamcpp_led.elf"; + auto data = read_file(elf_path); + if (data.empty()) { + fprintf(stderr, "Failed to read %s\n", elf_path); + return 1; + } + + auto soc = Stm32f103Soc::create(); + if (!soc) { + fprintf(stderr, "Failed to create SoC: %s\n", soc.error().c_str()); + return 1; + } + + // TAMCPP 1_led_control: LED。 + // HAL_Delay(500) 交替 on/off,on() 写 RESET(低有效亮),off() 写 SET。 + int toggle_count = 0; + (*soc)->parts().gpioc.set_pin_change_callback([&](uint8_t pin, bool) { + if (pin == 13) { + toggle_count++; + } + }); + + auto r = (*soc)->load_elf(data); + if (!r) { + fprintf(stderr, "Failed to load ELF: %s\n", r.error().c_str()); + return 1; + } + + auto cm3 = (*soc)->cortex_m3_cpu(); + if (!cm3.IsValid()) { + fprintf(stderr, "Cortex-M3 CPU not initialized\n"); + return 1; + } + + // 80M step ≈ 80M cycle → SysTick(load=63999)约 1250 次中断 → uwTick≈1250, + // 够第一个 HAL_Delay(500) 返回 + led.on() + 第二个 HAL_Delay(500) + led.off(), + // 即至少 1 次 PC13 翻转。 + (*soc)->run(80000000); + + auto pc_val = cm3->pc(); + auto state_res = cm3->state(); + fprintf(stderr, "[TAMCPP] PC=0x%08X state=%d\n", + pc_val.has_value() ? *pc_val : 0, + state_res ? static_cast(*state_res) : -1); + + const auto& missing = cm3->missing_opcodes(); + if (!missing.empty()) { + fprintf(stderr, "=== Missing instructions: %zu ===\n", + missing.size()); + for (auto& [addr, hw1, hw2] : missing) { + if (hw2) { + fprintf(stderr, " PC=0x%08X hw1=0x%04X hw2=0x%04X\n", addr, + hw1, hw2); + } else { + fprintf(stderr, " PC=0x%08X hw1=0x%04X\n", addr, hw1); + } + } + } + + printf("GPIO PC13 toggled %d times\n", toggle_count); + return toggle_count > 0 ? 0 : 2; +} diff --git a/examples/tamcpp_blink/runner_button.cpp b/examples/tamcpp_blink/runner_button.cpp new file mode 100644 index 0000000..cb33177 --- /dev/null +++ b/examples/tamcpp_blink/runner_button.cpp @@ -0,0 +1,90 @@ +// Load TAMCPP 2_button_control, drive its active-low PA0 button, and verify +// that the active-low PC13 LED receives both the Pressed and Released writes. +#include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "chips/stm32f1/soc/stm32f103_soc.hpp" +#include "tools/mmio_trace.hpp" + +#include +#include +#include + +using namespace micro_forge; +using namespace micro_forge::chips::stm32f1; + +namespace { + +std::vector read_file(const char* path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return {}; + } + return {std::istreambuf_iterator(f), {}}; +} + +} // namespace + +int main(int argc, char** argv) { + const char* elf_path = (argc > 1) ? argv[1] : "tamcpp_button.elf"; + auto data = read_file(elf_path); + if (data.empty()) { + std::fprintf(stderr, "Failed to read %s\n", elf_path); + return 1; + } + + auto soc = Stm32f103Soc::create(); + if (!soc) { + std::fprintf(stderr, "SoC create failed: %s\n", soc.error().c_str()); + return 1; + } + if (auto loaded = (*soc)->load_elf(data); !loaded) { + std::fprintf(stderr, "ELF load failed: %s\n", loaded.error().c_str()); + return 1; + } + + int led_on_writes = 0; + int led_off_writes = 0; + tools::enable_mmio_trace( + *(*soc)->machine().bus, [&](const tools::MmioAccess& access) { + if (!access.is_write || !access.ok || access.addr != 0x4001'1010u || + access.width != Width::Word) { + return; + } + if (access.value & (1u << (13 + 16))) { + ++led_on_writes; // active-low PC13: BSRR reset half + } + if (access.value & (1u << 13)) { + ++led_off_writes; // active-low PC13: BSRR set half + } + }); + + // The GPIO model has no floating-pad/pull resistor model yet, so inject + // the pull-up level before firmware reaches Button's BootSync state. + (*soc)->parts().gpioa.simulate_input(0, true); + (*soc)->run(10'000'000); + + (*soc)->parts().gpioa.simulate_input(0, false); // press + (*soc)->run(4'000'000); // >20 ms debounce + + (*soc)->parts().gpioa.simulate_input(0, true); // release + (*soc)->run(4'000'000); // >20 ms debounce + + auto pa0 = (*soc)->parts().gpioa.read(0x08, Width::Word); + auto pc13 = (*soc)->parts().gpioc.read(0x0C, Width::Word); + auto cpu = (*soc)->cortex_m3_cpu(); + auto state = cpu.IsValid() + ? cpu->state() + : cpu::CPU::CPUExpected{ + std::unexpected{cpu::CPU::CPUError::NotRunning}}; + + const bool passed = pa0 && ((*pa0 & 1u) != 0) && pc13 && + ((*pc13 & (1u << 13)) != 0) && led_on_writes > 0 && + led_off_writes > 0 && state && + *state == cpu::CPU::State::Running; + + std::printf("TAMCPP button: on=%d off=%d PA0=%u PC13=%u state=%d [%s]\n", + led_on_writes, led_off_writes, pa0 ? (*pa0 & 1u) : 0u, + pc13 ? ((*pc13 >> 13) & 1u) : 0u, + state ? static_cast(*state) : -1, + passed ? "PASS" : "FAIL"); + return passed ? 0 : 2; +} diff --git a/examples/tamcpp_blink/runner_uart.cpp b/examples/tamcpp_blink/runner_uart.cpp new file mode 100644 index 0000000..e1c946e --- /dev/null +++ b/examples/tamcpp_blink/runner_uart.cpp @@ -0,0 +1,95 @@ +// Load TAMCPP 3_uart_logger: verify USART1 TX prints the banner and an injected +// RX line ("LED ON\r\n") is parsed by the firmware into "OK: LED ON\r\n". +// +// TX chain == hal_uart E2E (send_string → HAL_UART_Transmit → DR → output). +// RX chain is the new path: inject_rx → RXNE IRQ → HAL_UART_RxCpltCallback → +// ring buffer → main pop → handle_command → send_string("OK: LED ON"). +// +// Each byte is injected then run until PC returns to main's while loop before +// the next byte — this keeps the host-driven injection synchronous with one +// complete IRQ entry/handler/return round trip per byte. +#include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "chips/stm32f1/periph/stm32f1_usart.hpp" +#include "chips/stm32f1/soc/stm32f103_soc.hpp" + +#include +#include +#include +#include + +using namespace micro_forge; +using namespace micro_forge::cpu::arm::cortex_m3; +using namespace micro_forge::chips::stm32f1; + +namespace { + +std::vector read_file(const char* path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return {}; + } + return {std::istreambuf_iterator(f), {}}; +} + +} // namespace + +int main(int argc, char** argv) { + const char* elf_path = (argc > 1) ? argv[1] : "tamcpp_uart.elf"; + auto data = read_file(elf_path); + if (data.empty()) { + std::fprintf(stderr, "Failed to read %s\n", elf_path); + return 1; + } + + auto soc = Stm32f103Soc::create(); + if (!soc) { + std::fprintf(stderr, "SoC create failed: %s\n", soc.error().c_str()); + return 1; + } + + std::string output; + (*soc)->parts().serial().set_output( + [&](uint8_t ch) { output += static_cast(ch); }); + + if (auto loaded = (*soc)->load_elf(data); !loaded) { + std::fprintf(stderr, "ELF load failed: %s\n", loaded.error().c_str()); + return 1; + } + auto cm3 = (*soc)->cortex_m3_cpu(); + if (!cm3.IsValid()) { + std::fprintf(stderr, "CPU not init\n"); + return 1; + } + + // Run to the point main has printed the banner, armed RXNE reception, and + // entered its while loop. + (*soc)->run(2'000'000); + + // Inject "LED ON\r\n" one byte at a time. After each byte, run until PC is + // back in main's while loop (0x0800'02B2–0x0800'0334) so the RXNE IRQ for + // that byte is fully entered, handled, and returned before the next byte. + auto& usart1 = static_cast((*soc)->parts().serial()); + const char cmd[] = "LED ON\r\n"; + for (char c : cmd) { + usart1.inject_rx(static_cast(c)); + for (int i = 0; i < 200'000; ++i) { + (*soc)->run(1'000); + const uint32_t pc = cm3->pc().value_or(0); + if (pc >= 0x0800'02B2u && pc <= 0x0800'0334u) { + break; + } + } + } + (*soc)->run(5'000'000); + + const bool tx_ok = output.find("UART Logger Ready!") != std::string::npos; + const bool rx_ok = output.find("OK: LED ON") != std::string::npos; + + std::fprintf(stderr, "UART output(%zu bytes): ", output.size()); + for (unsigned char c : output) { + std::fprintf(stderr, "%02X ", c); + } + std::fprintf(stderr, "\n[TAMCPP-uart] TX banner=%d RX cmd=%d\n", + tx_ok ? 1 : 0, rx_ok ? 1 : 0); + return (tx_ok && rx_ok) ? 0 : 2; +} diff --git a/gui/main_window.cpp b/gui/main_window.cpp index a677e01..c920b33 100644 --- a/gui/main_window.cpp +++ b/gui/main_window.cpp @@ -23,12 +23,20 @@ #include #include #include +#include +#include +#include +#include +#include #include +#include +#include #include #include #include #include #include +#include #include #include @@ -38,6 +46,21 @@ namespace micro_forge::gui { MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) : QMainWindow(parent) { setWindowTitle("micro-forge"); + setAcceptDrops(true); // drag-and-drop an .elf/.bin onto the window to load it + + // File menu: Open firmware via dialog — alternative to drag-and-drop / + // command-line. Reuses loadFirmware (stop, set path, rebuild, refresh). + auto* file_menu = menuBar()->addMenu(tr("&File")); + auto* open_action = + file_menu->addAction(tr("&Open firmware..."), this, [this]() { + const QString path = QFileDialog::getOpenFileName( + this, tr("Open firmware"), QString(), + tr("Firmware (*.elf *.bin);;All files (*)")); + if (!path.isEmpty()) { + loadFirmware(path); + } + }); + open_action->setShortcut(QKeySequence::Open); // Ctrl+O session_.set_firmware(firmware_path.toStdString()); @@ -130,15 +153,28 @@ MainWindow::MainWindow(const QString& firmware_path, QWidget* parent) 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.) + // USART RX: forward the serial panel's input box to the session. Inject one + // byte at a time and run enough steps for the RXNE IRQ to consume it (the + // USART model has a single-slot DR — back-to-back injects with no run in + // between overwrite DR and lose bytes). Append CRLF so the firmware's line + // parser (main.cpp: waits for '\r'/'\n') fires handle_command. 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)); + session_.run(50'000); } + session_.inject_rx('\r'); + session_.run(50'000); + session_.inject_rx('\n'); + session_.run(50'000); + }); + // GPIO input injection (PA0 button in gpio_panel): firmware that polls IDR + // — e.g. TAMCPP 2_button_control's HAL_GPIO_ReadPin — sees the level. + connect(gpio_panel_, &panels::GpioPanel::injectGpio, + this, [this](char port, std::uint8_t pin, bool high) { + session_.simulate_gpio_input(port, pin, high); }); // C4-mem: a new address → dump it immediately (don't wait for the tick). connect(memory_panel_, &panels::MemoryPanel::addr_changed, @@ -269,4 +305,32 @@ void MainWindow::closeEvent(QCloseEvent* event) { QMainWindow::closeEvent(event); } +void MainWindow::dragEnterEvent(QDragEnterEvent* event) { + if (event->mimeData()->hasUrls()) { + event->acceptProposedAction(); + } +} + +void MainWindow::dropEvent(QDropEvent* event) { + const auto urls = event->mimeData()->urls(); + if (urls.isEmpty()) { + return; + } + const QString path = urls.first().toLocalFile(); + if (path.isEmpty()) { + return; + } + loadFirmware(path); + event->acceptProposedAction(); +} + +void MainWindow::loadFirmware(const QString& path) { + running_ = false; + run_btn_->setText("Run"); + timer_->stop(); + session_.set_firmware(path.toStdString()); + rebuildSession(); + refreshFromSnapshot(); +} + } // namespace micro_forge::gui diff --git a/gui/main_window.hpp b/gui/main_window.hpp index 2b9ce97..0190357 100644 --- a/gui/main_window.hpp +++ b/gui/main_window.hpp @@ -14,6 +14,8 @@ class QCloseEvent; class QComboBox; +class QDragEnterEvent; +class QDropEvent; class QLabel; class QPushButton; class QTimer; @@ -44,6 +46,8 @@ class MainWindow : public QMainWindow { protected: void closeEvent(QCloseEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; private slots: void onTick(); @@ -53,6 +57,7 @@ class MainWindow : public QMainWindow { private: void rebuildSession(); + void loadFirmware(const QString& path); void refreshFromSnapshot(); void refreshMemory(); // C4-mem: re-dump the memory panel's tracked region. diff --git a/gui/view/panels/gpio_panel.cpp b/gui/view/panels/gpio_panel.cpp index ee0aa43..1580b53 100644 --- a/gui/view/panels/gpio_panel.cpp +++ b/gui/view/panels/gpio_panel.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,15 @@ GpioPanel::GpioPanel(QWidget* parent) : QWidget(parent) { label_ = new QLabel; label_->setStyleSheet("font-family: monospace;"); lay->addWidget(label_); + + // PA0 input button (active-low): hold = press (drive IDR low), release = + // idle (drive IDR high). Forwarded to session.simulate_gpio_input. Useful + // for firmware that polls IDR (TAMCPP 2_button_control's ReadPin). + pa0_btn_ = new QPushButton("PA0 button — hold = press"); + pa0_btn_->setCheckable(true); + lay->addWidget(pa0_btn_); + connect(pa0_btn_, &QPushButton::toggled, this, + [this](bool checked) { emit injectGpio('A', 0, !checked); }); } void GpioPanel::refresh(const introspection::IntrospectionSnapshot& snap) { diff --git a/gui/view/panels/gpio_panel.hpp b/gui/view/panels/gpio_panel.hpp index d7ab7ce..0d9de31 100644 --- a/gui/view/panels/gpio_panel.hpp +++ b/gui/view/panels/gpio_panel.hpp @@ -1,14 +1,17 @@ -// 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.) +// GPIO panel — A/B/C port ODR (hex) + 16 per-pin LED glyphs (pin0..pin15), +// plus a PA0 push-button for IDR input injection. Firmware that polls IDR +// (e.g. TAMCPP 2_button_control's HAL_GPIO_ReadPin) reads the injected level; +// the ODR display above still reflects what the firmware drives out. #pragma once #include "introspection/introspection.hpp" +#include + #include class QLabel; +class QPushButton; namespace micro_forge::gui::panels { @@ -19,8 +22,14 @@ class GpioPanel : public QWidget { void refresh(const introspection::IntrospectionSnapshot& snap); + signals: + // PA0 button toggled. Active-low wiring: checked (held) = drive IDR low, + // released = drive IDR high — matching a pull-up button to GND. + void injectGpio(char port, std::uint8_t pin, bool high); + private: QLabel* label_; + QPushButton* pa0_btn_; }; } // namespace micro_forge::gui::panels diff --git a/gui/view/widgets/stm32_board_widget.cpp b/gui/view/widgets/stm32_board_widget.cpp index 4b51a5a..9c89ac6 100644 --- a/gui/view/widgets/stm32_board_widget.cpp +++ b/gui/view/widgets/stm32_board_widget.cpp @@ -51,8 +51,12 @@ void Stm32BoardWidget::refresh( odr_[1] = snap.peripherals.gpio[1].odr; // B (rendered pins use A/C) odr_[2] = snap.peripherals.gpio[2].odr; // C if (ledPanel_ != nullptr) ledPanel_->setLevels(odr_[0]); // PA0..PA7 - if (pc13Bulb_ != nullptr) // PC13 = port C bit 13 - pc13Bulb_->setState(static_cast((odr_[2] >> 13) & 1u)); + if (pc13Bulb_ != nullptr) { // PC13 = port C bit 13 + // Blue Pill PC13 LED is active-low: lit when ODR bit13 == 0 (firmware + // led.on() writes BSRR reset → ODR=0 → LED conducts). Invert so the + // bulb reflects the physical LED, not the raw register bit. + pc13Bulb_->setState(!static_cast((odr_[2] >> 13) & 1u)); + } update(); // async — never block the tick loop on a repaint. } diff --git a/include/arch/arm/cortex_m3/cortex_m3.hpp b/include/arch/arm/cortex_m3/cortex_m3.hpp index 6a418a1..440a35b 100644 --- a/include/arch/arm/cortex_m3/cortex_m3.hpp +++ b/include/arch/arm/cortex_m3/cortex_m3.hpp @@ -273,8 +273,16 @@ class CortexM3CPU : public CPU { // Probe mode state bool probe_mode_ = false; std::vector> missing_opcodes_; + struct ItState { + std::vector conditions; + size_t condition_pos = 0; + }; std::vector it_conditions_; size_t it_condition_pos_ = 0; + // ITSTATE is part of xPSR on real Cortex-M hardware and is therefore + // preserved across exception entry/return. The emulator represents it + // separately, so keep an explicit stack for nested exceptions. + std::vector suspended_it_states_; std::optional pending_bus_error_; std::optional pending_access_addr_; std::optional pending_access_width_; diff --git a/src/arch/arm/cortex_m3/cortex_m3.cpp b/src/arch/arm/cortex_m3/cortex_m3.cpp index b146279..bd28a5f 100644 --- a/src/arch/arm/cortex_m3/cortex_m3.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3.cpp @@ -57,6 +57,7 @@ CPU::CPUExpected CortexM3CPU::reset() { active_priorities_.clear(); it_conditions_.clear(); it_condition_pos_ = 0; + suspended_it_states_.clear(); current_status_ = State::Halted; cycles_ = 0; return {}; diff --git a/src/arch/arm/cortex_m3/cortex_m3_interrupt.cpp b/src/arch/arm/cortex_m3/cortex_m3_interrupt.cpp index e63d392..54e55ac 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_interrupt.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_interrupt.cpp @@ -26,8 +26,16 @@ CPU::CPUExpected CortexM3CPU::check_and_handle_interrupt() { // running exception's priority. An exception may pre-empt only if its // preemption-group priority is strictly smaller than the active one. This // (rather than an early return in handler mode) is what enables nesting. + // + // Thread mode uses 0xFF directly (NOT preempt_priority(0xFF)): that helper + // truncates to the preempt-group width (e.g. 0xF for 4-bit STM32F1), which + // equals the lowest-priority exception's preempt (e.g. SysTick at 0xF0 → + // 0xF). The strict `<` would then block that exception from preempting + // thread mode, violating ARMv7-M (thread is preempted by ANY exception). + // 0xFF is larger than any exception preempt (0..0xF), so all pending + // exceptions preempt thread mode; handler-mode nesting is unaffected. const uint8_t active_preempt = - preempt_priority(in_handler_mode_ ? current_priority_ : 0xFFu); + in_handler_mode_ ? preempt_priority(current_priority_) : 0xFFu; // Pick the highest-priority candidate across SysTick and external IRQs. bool take_systick = false; @@ -87,6 +95,15 @@ CortexM3CPU::exception_entry_common(addr_t vector_addr, uint8_t new_priority) { // what makes nested preemption return to the right active priority. active_priorities_.push_back(current_priority_); + // ITSTATE lives in xPSR on Cortex-M and the hardware-stacked xPSR restores + // it on exception return. Our IT decoder keeps the remaining conditions + // out-of-band, so suspend them explicitly and start the handler outside + // any interrupted thread/handler IT block. + suspended_it_states_.push_back( + {std::move(it_conditions_), it_condition_pos_}); + it_conditions_.clear(); + it_condition_pos_ = 0; + // Switch active stack to MSP for stacking. Handler mode always uses MSP; // if thread mode was on PSP, preserve PSP then load MSP. The active-SP // invariant (R13 == active shadow) is maintained because push_stack routes @@ -221,6 +238,18 @@ CPU::CPUExpected CortexM3CPU::interrupt_return(data_t exc_return) { } xpsr_ = *xpsr_val; + // Restore the IT block interrupted by this exception. This must happen + // before the resumed instruction is fetched; otherwise handler + // instructions consume the thread's conditions and conditional moves can + // become unconditional after return. + if (suspended_it_states_.empty()) { + return std::unexpected{CPUError::ExceptionReturnFault}; + } + auto suspended_it = std::move(suspended_it_states_.back()); + suspended_it_states_.pop_back(); + it_conditions_ = std::move(suspended_it.conditions); + it_condition_pos_ = suspended_it.condition_pos; + // Restore PC (clear Thumb bit). write_reg(15) writes R15 directly. if (auto r = write_reg(15, *pc_val & ~1u); !r) { return r; diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb16_loadstore.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb16_loadstore.cpp index 4370463..eb85df5 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb16_loadstore.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb16_loadstore.cpp @@ -222,12 +222,23 @@ CPU::CPUExpected CortexM3CPU::t16_pop(uint16_t insn) { it_conditions_.clear(); it_condition_pos_ = 0; it_conditions_.reserve(static_cast(count)); + // ARMv7-M IT mask: bit3 is the count sentinel, bits below it encode + // THEN/ELSE per slot. countr_zero(mask) gives count, but slot i's + // THEN/ELSE bit is mask[4-i] (slot1→mask[3], slot2→mask[2], …), NOT + // mask[3-i]. Encoding proof (firstcond[0]=1): + // itt ne=0xBF1C mask=0b1100 → slot1 mask[3]=1==1 THEN → ne + // ite ne=0xBF14 mask=0b0100 → slot1 mask[3]=0≠1 ELSE → eq + // itee ne=0xBF12 mask=0b0010 → [ne,eq,eq] + // The old `1u<<(3-slot)` (mask[3-slot]) collapsed itt and ite (both + // have mask[2]=1) to the same [ne,ne], so ite's ELSE slot executed + // under the THEN condition — e.g. HAL_GPIO_ReadPin's + // `ite ne; movne r0,#1; moveq r0,#0` always returned 0. for (int slot = 0; slot < count; ++slot) { if (slot == 0) { it_conditions_.push_back(first_cond); continue; } - uint8_t bit = 1u << (3 - slot); + uint8_t bit = 1u << (4 - slot); bool then_path = (mask & bit) != 0; it_conditions_.push_back(then_path ? first_cond : (first_cond ^ 1u)); @@ -259,11 +270,17 @@ CPU::CPUExpected CortexM3CPU::t16_pop(uint16_t insn) { if (!v) { return std::unexpected{v.error()}; } - auto res = write_pc(*v); - if (!res) { - return res; + // Commit SP BEFORE write_pc. pop {pc} of an EXC_RETURN magic makes + // write_pc() run interrupt_return(), which pops the 8-word exception + // frame starting at [sp+4]. Doing the SP writeback after write_pc + // instead clobbers interrupt_return's restored SP with the stale local + // sp (pre-+4), leaking 0x20 of stack every exception (e.g. every + // STM32F1 USART RXNE IRQ → line_buf drift → command parse fail). + auto wb = write_reg(13, sp + 4); + if (!wb) { + return wb; } - sp += 4; + return write_pc(*v); } auto wr = write_reg(13, sp); if (!wr) { diff --git a/src/loader/elf_loader.cpp b/src/loader/elf_loader.cpp index 9732c8b..e7c5412 100644 --- a/src/loader/elf_loader.cpp +++ b/src/loader/elf_loader.cpp @@ -155,7 +155,18 @@ load_elf(memory::Bus& bus, std::span elf_data) { auto seg_data = elf_data.subspan(phdr->p_offset, phdr->p_filesz); - auto res = write_segment(bus, phdr->p_vaddr, seg_data, phdr->p_memsz); + // 裸机 ELF 约定:.data 段 p_vaddr=SRAM(运行时位置),p_paddr=flash LMA + // (初值存储位置)。初值应加载到 p_paddr,让 startup 的 CopyDataInit 从 + // _sidata(=p_paddr)复制到 SRAM(p_vaddr)——这与真实烧录器行为一致。 + // .text 段 p_vaddr==p_paddr 无变化;p_paddr 未设(==0)时 fallback p_vaddr。 + // 旧实现按 p_vaddr 加载,导致 .data 初值被写到 SRAM 后又被 CopyDataInit + // 从空 flash LMA 用 0 覆盖(SystemCoreClock 被清零,SysTick 不配)。 + uint32_t load_addr = phdr->p_vaddr; + if (phdr->p_paddr != 0 && phdr->p_paddr != phdr->p_vaddr) { + load_addr = phdr->p_paddr; + } + + auto res = write_segment(bus, load_addr, seg_data, phdr->p_memsz); if (!res) { return std::unexpected(res.error()); } diff --git a/src/periph/nvic.cpp b/src/periph/nvic.cpp index af990a0..b78e3ea 100644 --- a/src/periph/nvic.cpp +++ b/src/periph/nvic.cpp @@ -59,6 +59,19 @@ Expected NvicPeripheral::read(addr_t offset, Width w) { } Expected NvicPeripheral::write(addr_t offset, data_t data, Width w) { + // IPR(优先级寄存器)支持任意 width:ARMv7-M NVIC 优先级是字节寄存器, + // HAL_NVIC_SetPriority 用 strb(字节写)IP[IRQn]。先处理 IPR。 + if (offset >= 0x300 && offset < 0x500) { // IP priority + size_t byte_idx = offset - 0x300; + size_t nbytes = static_cast(w); + for (size_t i = 0; i < nbytes && (byte_idx + i) < kMaxIrq; ++i) { + priorities_[byte_idx + i] = + static_cast((data >> (i * 8)) & 0xFF); + } + invalidate_cache(); + return {}; + } + if (w != Width::Word) { return std::unexpected(BusError::Unaligned); } @@ -99,20 +112,6 @@ Expected NvicPeripheral::write(addr_t offset, data_t data, Width w) { invalidate_cache(); return {}; } - if (offset >= 0x300 && offset < 0x500) { // IP priority - size_t word_idx = (offset - 0x300) / 4; - size_t base_irq = word_idx * 4; - if (base_irq >= kMaxIrq) { - return std::unexpected(BusError::Unmapped); - } - for (size_t i = 0; i < 4 && (base_irq + i) < kMaxIrq; ++i) { - priorities_[base_irq + i] = - static_cast((data >> (i * 8)) & 0xFF); - } - invalidate_cache(); - return {}; - } - return std::unexpected(BusError::Unmapped); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cd5f5f7..f4ed845 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -187,6 +187,30 @@ if(TARGET hello_firmware AND TARGET blink_firmware AND TARGET systick_firmware) message(STATUS "STM32CubeF1 submodule not found — skipping E2E.HalUartTransmit") endif() + # TAMCPP C++ HAL 样例 E2E — 需 STM32CubeF1 submodule + TAMCPP_ROOT + if(TARGET tamcpp_led_firmware AND TARGET tamcpp_button_firmware AND TARGET tamcpp_uart_firmware) + message(STATUS "TAMCPP + STM32CubeF1 found — enabling E2E.Tamcpp{Led,Button,Uart}") + target_compile_definitions(test_e2e PRIVATE + E2E_TAMCPP_LED_ELF="${CMAKE_CURRENT_BINARY_DIR}/../examples/tamcpp_blink/firmware/led.elf" + E2E_TAMCPP_BUTTON_ELF="${CMAKE_CURRENT_BINARY_DIR}/../examples/tamcpp_blink/firmware/button.elf" + E2E_TAMCPP_UART_ELF="${CMAKE_CURRENT_BINARY_DIR}/../examples/tamcpp_blink/firmware/uart.elf" + ) + add_dependencies(test_e2e tamcpp_led_firmware tamcpp_button_firmware tamcpp_uart_firmware) + add_custom_command(TARGET test_e2e POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/../examples/tamcpp_blink/firmware/led.elf + ${CMAKE_CURRENT_BINARY_DIR}/tamcpp_led.elf + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/../examples/tamcpp_blink/firmware/button.elf + ${CMAKE_CURRENT_BINARY_DIR}/tamcpp_button.elf + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_BINARY_DIR}/../examples/tamcpp_blink/firmware/uart.elf + ${CMAKE_CURRENT_BINARY_DIR}/tamcpp_uart.elf + ) + else() + message(STATUS "TAMCPP samples not found — skipping E2E.Tamcpp{Led,Button,Uart}") + endif() + gtest_discover_tests(test_e2e) endif() diff --git a/test/test_e2e.cpp b/test/test_e2e.cpp index 50a7873..0703749 100644 --- a/test/test_e2e.cpp +++ b/test/test_e2e.cpp @@ -1,7 +1,9 @@ #include #include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "chips/stm32f1/periph/stm32f1_usart.hpp" #include "chips/stm32f1/soc/stm32f103_soc.hpp" +#include "tools/mmio_trace.hpp" #include #include @@ -107,6 +109,120 @@ TEST(E2E, HalUartTransmit) { } #endif +#ifdef E2E_TAMCPP_LED_ELF +// TAMCPP 1_led_control: C++ HAL LED. Boots and toggles PC13 at least once. +TEST(E2E, TamcppLed) { + auto data = read_file(E2E_TAMCPP_LED_ELF); + ASSERT_FALSE(data.empty()) << "Firmware not found at " E2E_TAMCPP_LED_ELF; + + auto soc = Stm32f103Soc::create(); + ASSERT_TRUE(soc.has_value()); + + int toggle = 0; + (*soc)->parts().gpioc.set_pin_change_callback([&](uint8_t pin, bool) { + if (pin == 13) ++toggle; + }); + + auto r = (*soc)->load_elf(data); + ASSERT_TRUE(r.has_value()) << r.error(); + + // main loops HAL_Delay(500) between on/off; the first on() lands past 40M + // cycles (the delay is longer than the naive 64 MHz × 500 ms estimate), so + // run 80M to observe at least one PC13 toggle. + (*soc)->run(80'000'000); + + auto state = (*soc)->machine().cpu->state(); + ASSERT_TRUE(state.has_value()); + ASSERT_NE(*state, cpu::CPU::State::Faulted) << "CPU faulted"; + EXPECT_GE(toggle, 1) << "PC13 should toggle at least once; got " << toggle; +} +#endif + +#ifdef E2E_TAMCPP_BUTTON_ELF +// TAMCPP 2_button_control: C++ HAL Button. Inject idle→press +// →release and verify both Pressed (led.on → PC13 BSRR reset) and Released +// (led.off → PC13 BSRR set) fire. Exercises the IT-block + ITSTATE paths. +TEST(E2E, TamcppButton) { + using tools::enable_mmio_trace; + using tools::MmioAccess; + auto data = read_file(E2E_TAMCPP_BUTTON_ELF); + ASSERT_FALSE(data.empty()) << "Firmware not found at " E2E_TAMCPP_BUTTON_ELF; + + auto soc = Stm32f103Soc::create(); + ASSERT_TRUE(soc.has_value()); + + // active-low PC13: BSRR reset half (bit 13+16) = led on, set half = led off. + int on_w = 0, off_w = 0; + enable_mmio_trace(*(*soc)->machine().bus, [&](const MmioAccess& a) { + if (!a.is_write || !a.ok || a.addr != 0x4001'1010u || + a.width != Width::Word) { + return; + } + if (a.value & (1u << (13 + 16))) ++on_w; + if (a.value & (1u << 13)) ++off_w; + }); + + auto r = (*soc)->load_elf(data); + ASSERT_TRUE(r.has_value()) << r.error(); + + // No floating-pad model: inject the pull-up idle level before the firmware + // reaches Button's BootSync state, then drive a clean press/release spike. + (*soc)->parts().gpioa.simulate_input(0, true); // idle (high) + (*soc)->run(10'000'000); + (*soc)->parts().gpioa.simulate_input(0, false); // press (low) + (*soc)->run(4'000'000); // > 20 ms debounce + (*soc)->parts().gpioa.simulate_input(0, true); // release (high) + (*soc)->run(4'000'000); // > 20 ms debounce + + EXPECT_GT(on_w, 0) << "Pressed should fire led.on() (BSRR reset)"; + EXPECT_GT(off_w, 0) << "Released should fire led.off() (BSRR set)"; +} +#endif + +#ifdef E2E_TAMCPP_UART_ELF +// TAMCPP 3_uart_logger: C++ HAL USART1. TX prints the banner; an injected RX +// line "LED ON\r\n" is parsed into "OK: LED ON\r\n". Exercises the RXNE IRQ → +// ring → handle_command path that depends on pop {pc} restoring SP correctly. +TEST(E2E, TamcppUart) { + auto data = read_file(E2E_TAMCPP_UART_ELF); + ASSERT_FALSE(data.empty()) << "Firmware not found at " E2E_TAMCPP_UART_ELF; + + auto soc = Stm32f103Soc::create(); + ASSERT_TRUE(soc.has_value()); + + std::string output; + (*soc)->parts().serial().set_output( + [&](uint8_t ch) { output += static_cast(ch); }); + + auto r = (*soc)->load_elf(data); + ASSERT_TRUE(r.has_value()) << r.error(); + auto cm3 = (*soc)->cortex_m3_cpu(); + ASSERT_TRUE(cm3.IsValid()); + + (*soc)->run(2'000'000); // banner + arm RXNE + enter while loop + + auto& usart1 = static_cast((*soc)->parts().serial()); + const char cmd[] = "LED ON\r\n"; + for (char c : cmd) { + usart1.inject_rx(static_cast(c)); + // Run until PC is back in main's while loop so this byte's RXNE IRQ is + // fully entered/handled/returned before the next byte. PC range is the + // uart.elf main while body — re-check if the firmware is rebuilt. + for (int i = 0; i < 200'000; ++i) { + (*soc)->run(1'000); + const uint32_t pc = cm3->pc().value_or(0); + if (pc >= 0x0800'02B2u && pc <= 0x0800'0334u) break; + } + } + (*soc)->run(5'000'000); + + EXPECT_NE(output.find("UART Logger Ready!"), std::string::npos) + << "TX banner missing; output: " << output; + EXPECT_NE(output.find("OK: LED ON"), std::string::npos) + << "RX command response missing; output: " << output; +} +#endif + TEST(E2E, SysTick) { auto data = read_file(E2E_SYSTICK_ELF); ASSERT_FALSE(data.empty()) << "Firmware not found at " E2E_SYSTICK_ELF; diff --git a/test/test_elf_loader.cpp b/test/test_elf_loader.cpp index 522a563..05c4247 100644 --- a/test/test_elf_loader.cpp +++ b/test/test_elf_loader.cpp @@ -136,6 +136,40 @@ TEST(ElfLoaderTest, BssZeroFill) { EXPECT_EQ(*r1, 0u); } +// 裸机 .data 段:p_vaddr=SRAM(运行时位置),p_paddr=flash LMA(初值存储)。 +// loader 应把初值加载到 p_paddr(flash),SRAM(p_vaddr)留给 startup 的 +// CopyDataInit 从 _sidata(=p_paddr)复制——与真实烧录器行为一致。 +// 旧实现按 p_vaddr 加载,导致 CopyDataInit 从空 flash LMA 用 0 覆盖 SRAM 初值 +// (TAMCPP 样例 SystemCoreClock 被清零 → SysTick 不配 → HAL_Delay 死循环)。 +TEST(ElfLoaderTest, DataSegmentLoadedToFlashLmaWhenVaddrDiffers) { + Bus bus; + FlatMemory flash(8 * 1024); + FlatMemory sram(4 * 1024); + ASSERT_TRUE( + bus.map(region(0x08000000, 8 * 1024, flash.GetWeak())).has_value()); + ASSERT_TRUE( + bus.map(region(0x20000000, 4 * 1024, sram.GetWeak())).has_value()); + + uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; // 模拟 .data 初值 + auto elf = build_minimal_elf(0x20000000, 0x20000001, data); + // patch p_paddr = flash LMA,与 p_vaddr(0x20000000)分离 + uint32_t lma = 0x08001000; + std::memcpy(elf.data() + 52 + 12, &lma, 4); + + auto result = load_elf(bus, elf); + ASSERT_TRUE(result.has_value()); + + // 初值在 flash LMA:startup CopyDataInit 从这里复制 + auto flash_val = bus.read(0x08001000, Width::Word); + ASSERT_TRUE(flash_val.has_value()); + EXPECT_EQ(*flash_val, 0xEFBEADDEu); + + // SRAM(VMA)未被 elf_loader 写——模拟真实上电:SRAM=0,等 startup 复制 + auto sram_val = bus.read(0x20000000, Width::Word); + ASSERT_TRUE(sram_val.has_value()); + EXPECT_EQ(*sram_val, 0u); +} + TEST(ElfLoaderTest, PayloadTailLengthsWriteExactBytes) { for (size_t len = 1; len <= 5; ++len) { Bus bus; diff --git a/test/test_interrupt_roundtrip.cpp b/test/test_interrupt_roundtrip.cpp index 71047d9..190be05 100644 --- a/test/test_interrupt_roundtrip.cpp +++ b/test/test_interrupt_roundtrip.cpp @@ -155,6 +155,42 @@ TEST_F(InterruptTest, BxLrReturnFromInterrupt) { EXPECT_EQ(cpu_->pc().value(), kMainCode); } +TEST_F(InterruptTest, ExceptionPreservesInterruptedItBlock) { + store_vector_table_entry(0, kInitSp); + store_vector_table_entry(1, kMainCode); + store_vector_table_entry(16, kHandlerCode | 1u); // IRQ 0 + + // Exact control-flow shape used by HAL_GPIO_ReadPin: + // Z=0; ite ne; movne r1,#1; moveq r1,#0 + // Take the interrupt after ITE and before either conditional instruction. + store_instructions(kMainCode, + {0x2001, // movs r0,#1 (Z=0) + 0xBF14, // ite ne + 0x2101, // movne r1,#1 + 0x2100, // moveq r1,#0 + 0xE7FE}); + // Deliberately change flags in the handler. Its instructions must not + // consume the thread's pending NE/EQ conditions. + store_instructions(kHandlerCode, + {0x2200, // movs r2,#0 (Z=1) + 0x4770}); // bx lr + + ASSERT_TRUE(cpu_->set_pc(kMainCode).has_value()); + ASSERT_TRUE(cpu_->step().has_value()); // movs r0,#1 + ASSERT_TRUE(cpu_->step().has_value()); // ite ne + + ASSERT_TRUE(nvic_.write(0x000, 1u, Width::Word).has_value()); + nvic_.set_pending(0); + ASSERT_TRUE(cpu_->step().has_value()); // exception entry + ASSERT_TRUE(cpu_->step().has_value()); // handler changes flags + ASSERT_TRUE(cpu_->step().has_value()); // bx lr / exception return + + ASSERT_TRUE(cpu_->step().has_value()); // movne r1,#1 + ASSERT_TRUE(cpu_->step().has_value()); // moveq skipped + EXPECT_EQ(cpu_->register_value(1).value(), 1u); + EXPECT_EQ(cpu_->pc().value(), kMainCode + 8u); +} + // ── Test 2: Stack frame layout verification ── TEST_F(InterruptTest, StackFrameLayout) {