From 84c73e643743646873edb51abc475538a791e217 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 23 Jun 2026 17:42:45 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(cortex-m3):=20wire=20Timer=20UIF=20?= =?UTF-8?q?=E2=86=92=20NVIC=20via=20raise=5Firq=20(C2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit raise_irq was a no-op — the entire peripheral→NVIC IRQ channel was never connected (SysTick bypassed it via sys_tick_irq). Implement it as nvic_->set_pending(irq); this is the shared entry for all MMIO IRQs (TIM/USART/EXTI). Stm32f1Timer gains set_irq_callback (SysTick pattern); tick() raises on UIF 0→1 edge when DIER.UIE is set. SoC wires TIM2 (kTim2Irqn=28). Add unit tests (edge/UIE semantics) and TimerUifRoundtrip E2E (coordinator Apb1 tick → handler entry → BX LR return). ctest 289/289. --- .../06-progress-assessment-and-replan.md | 9 ++++ document/notes/014-timer-uif-irq-e2e.md | 37 ++++++++++++++ include/chips/stm32f1/interrupt_config.hpp | 5 ++ include/chips/stm32f1/stm32f1_timer.hpp | 6 +++ src/arch/arm/cortex_m3/cortex_m3.cpp | 8 +++- src/chips/stm32f1/stm32f103_soc.cpp | 7 +++ src/chips/stm32f1/stm32f1_timer.cpp | 10 +++- test/test_interrupt_roundtrip.cpp | 48 +++++++++++++++++++ test/test_stm32f1_periph.cpp | 36 ++++++++++++++ 9 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 document/notes/014-timer-uif-irq-e2e.md diff --git a/document/milestones/06-progress-assessment-and-replan.md b/document/milestones/06-progress-assessment-and-replan.md index 56d638d..65af09a 100644 --- a/document/milestones/06-progress-assessment-and-replan.md +++ b/document/milestones/06-progress-assessment-and-replan.md @@ -172,6 +172,15 @@ **02 仍剩**:仅 tail-chaining(同步模拟器天然退化,见上)。**03 外设中断路径**(EXTI / Timer UIF→IRQ E2E / USART RX-RXNE-TXE IRQ)进入第二波 C1–C3。 +## 实施记录(2026-06-23 · 第二波 C2a/b) + +继 Thumb-2 全覆盖里程碑收口(T0-T4 + T5c,notes 012/013)后,焦点转第二波外设中断端到端。C2 Timer 落地两件事: + +- **打通 `raise_irq` 公共通道**:原 `CortexM3CPU::raise_irq` 是 no-op(`return {};`)—— **整个外设→NVIC 注入通道从未接通**(SysTick 靠独立 `sys_tick_irq()` 绕过)。实现为 `nvic_->set_pending(irq)` 一行;这是所有 MMIO IRQ(TIM/USART/EXTI)的公共入口。 +- **Timer UIF → NVIC → handler 端到端**:Timer 仿 SysTick `set_irq_callback` 模式,`tick()` 在 UIF **0→1 edge + `DIER.UIE`** 时回调一次;SoC 接 `raise_irq(kTim2Irqn=28)`;`kTim2Irqn` 常量落 interrupt_config.hpp(vector index 44)。 +- 验证:单元(edge/UIE 语义)+ `TimerUifRoundtrip` E2E(coordinator Apb1 驱动 tick → handler 往返)。**289/289 绿**,固件 E2E/CLI 无回归。细节见 notes 014。 +- **C2 进度**:Timer UIF→IRQ E2E 由 ~50%(UIF 产生但无端到端)提升到通道打通 + 往返验证。剩 C2c 抢占/嵌套场景(检验第一波 A 的 `active_priorities_`),再 C1 EXTI / C3 USART RX-IRQ。 + ## 结论 / 下一步 diff --git a/document/notes/014-timer-uif-irq-e2e.md b/document/notes/014-timer-uif-irq-e2e.md new file mode 100644 index 0000000..d1ed28c --- /dev/null +++ b/document/notes/014-timer-uif-irq-e2e.md @@ -0,0 +1,37 @@ +# 014 — Timer UIF → NVIC → handler 端到端(C2a/b) + +> 06 第二波 C2(外设中断端到端)。打通外设→NVIC 的 IRQ 注入通道(原 `raise_irq` 空壳),Timer UIF 经 edge 回调 + UIE 使能触发,coordinator 驱动 tick → handler 往返 E2E 验证。ctest **289/289 绿**(286 + 3 新)。下一步 C2c 抢占/嵌套验证。 + +## 背景 + +06 第二波:外设中断路径紧跟第一波 A(抢占)做端到端验证 ——「中断在真实外设上跑通是抢占正确性的最佳验证手段」。Timer 已半成(UIF 产生、PSC/ARR/CNT 与 VirtualClock 联动),缺 UIF→NVIC→handler 端到端。 + +## 核心发现:raise_irq 是空壳 + +`CortexM3CPU::raise_irq` 原为 no-op(`return {};`)—— **整个外设→NVIC 通道从未接通**。SysTick 靠独立的 `sys_tick_irq()`(直设 `pending_sys_tick_`,系统异常 15)绕过它。所以 C2 不只是「Timer 没调 raise」,而是**第一个打通这条公共通道**的里程碑(USART/EXTI 以后都走它)。 + +## 实现 + +1. **`raise_irq`**([cortex_m3.cpp](src/arch/arm/cortex_m3/cortex_m3.cpp)):`if (nvic_) nvic_->set_pending(irq);`。一行接通 NVIC ISPR。 +2. **Timer UIE edge 回调**([stm32f1_timer.hpp](include/chips/stm32f1/stm32f1_timer.hpp) / [.cpp](src/chips/stm32f1/stm32f1_timer.cpp)):仿 SysTick `set_irq_callback(std::function)`。`tick()` 在 UIF **0→1 edge 且 `DIER.UIE`(bit0)使能**时调回调一次。 +3. **`kTim2Irqn=28`**([interrupt_config.hpp](include/chips/stm32f1/interrupt_config.hpp)):`inline constexpr intr::intr_n_t kTim2Irqn = 28`(vector index 16+28=44)。 +4. **SoC 接线**([stm32f103_soc.cpp](src/chips/stm32f1/stm32f103_soc.cpp)):`tim2.set_irq_callback([cm3_weak]{ (void)cm3_weak->raise_irq(kTim2Irqn); })`,仿 SysTick 接线(WeakPtr + IsValid 守卫)。 + +## 验证(3 新单测) + +- **单元**([test_stm32f1_periph.cpp](test/test_stm32f1_periph.cpp)): + - `UifEdgeWithUieTriggersIrqCallback`:edge+UIE → 回调一次;UIF 未清时再溢出不重 fire(edge);清 UIF 后重新 fire。 + - `UifWithoutUieDoesNotFireCallback`:UIF 置位但 UIE 未使能 → 不回调(只状态,无 IRQ)。 +- **E2E**([test_interrupt_roundtrip.cpp](test/test_interrupt_roundtrip.cpp) `TimerUifRoundtrip`):fixture 加 TIM2(0x40000000)+ NVIC enable bit28 + prio 0xE0 + vector[44];配 PSC=0/ARR=5/UIE/CEN;coordinator(Apb1)驱动 tick → UIF → raise_irq(28) → handler 进入 → BX LR 返回。断言 entered + returned。 +- `ctest` 全量 **289/289 绿**,固件 E2E/CLI/中断抢占无回归。 + +## 陷阱 + +- **`raise_irq` nodiscard**:返回 `expected`,丢弃触发 `-Wunused-value`(-Werror)。调用点(SoC 回调、test)须 `(void)` 包裹。SysTick 的 `sys_tick_irq` 返回 void 无此问题 —— 外设 IRQ 通道首次遇到。 +- **edge vs level**:选 edge(UIF 0→1 raise 一次)。NVIC `set_pending` 幂等,但 edge 语义干净(避免每 tick 重复调回调);固件不清 UIF 导致持续 pending 是固件行为非 bug。 +- **Apb1 时钟域**:Timer 是 tickable on Apb1,coordinator 每 step 按 Apb1 频率给 cycle。ARR=5/PSC=0 在数 step 内触发(SysTick Sysclk 类比)。 +- **IPR 偏移**:TIM2=IRQ28,优先级寄存器 `0xE000E400+28`(=0xE000E41C,word 对齐,byte0=IRQ28 prio);ISER0 bit28 enable;vector index 16+28=44。 + +## 成果 / 下一步 + +`raise_irq` 公共通道打通 + Timer UIF 端到端往返验证。**C2c**(下批):抢占/嵌套场景(高优先级 Timer IRQ 抢占 Thread / 嵌套),检验第一波 A 的 `active_priorities_` 栈 —— 这是 C2 的核心价值。之后 C1 EXTI / C3 USART RX-IRQ 复用同通道。 diff --git a/include/chips/stm32f1/interrupt_config.hpp b/include/chips/stm32f1/interrupt_config.hpp index e048ce6..faf98ce 100644 --- a/include/chips/stm32f1/interrupt_config.hpp +++ b/include/chips/stm32f1/interrupt_config.hpp @@ -1,5 +1,6 @@ #pragma once +#include "cpu/intr.hpp" #include "memory/bus.hpp" #include "periph/nvic.hpp" #include "periph/scb.hpp" @@ -7,6 +8,10 @@ namespace micro_forge::chips::stm32f1 { +// STM32F103 external IRQ numbers (Cortex-M exception base is 16, so TIM2 IRQ 28 +// lives at vector-table index 16+28 = 44). Source: STM32F1 vector table. +inline constexpr intr::intr_n_t kTim2Irqn = 28; + Expected configure_interrupt_devices(memory::Bus& bus, periph::NvicPeripheral& nvic, periph::SysTickPeripheral& systick, diff --git a/include/chips/stm32f1/stm32f1_timer.hpp b/include/chips/stm32f1/stm32f1_timer.hpp index 1926723..81ec7e1 100644 --- a/include/chips/stm32f1/stm32f1_timer.hpp +++ b/include/chips/stm32f1/stm32f1_timer.hpp @@ -5,6 +5,7 @@ #include "util/weak_ptr/weak_ptr_factory.h" #include +#include namespace micro_forge::chips::stm32f1 { @@ -26,6 +27,9 @@ class Stm32f1Timer : public periph::Device, public periph::Timer { bool update_flag() const override; void clear_update_flag() override; + // Peripheral → CPU IRQ channel: invoked on UIF 0→1 edge when DIER.UIE is set. + void set_irq_callback(std::function cb) { irq_cb_ = std::move(cb); } + WeakPtr GetWeak() { return weak_factory_.GetWeakPtr(); } private: @@ -37,6 +41,8 @@ class Stm32f1Timer : public periph::Device, public periph::Timer { uint32_t cnt_ = 0; uint64_t prescaler_residual_ = 0; + std::function irq_cb_; + WeakPtrFactory weak_factory_{this}; }; diff --git a/src/arch/arm/cortex_m3/cortex_m3.cpp b/src/arch/arm/cortex_m3/cortex_m3.cpp index 5b2768b..14f9e87 100644 --- a/src/arch/arm/cortex_m3/cortex_m3.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3.cpp @@ -101,7 +101,13 @@ CPU::CPUExpected CortexM3CPU::set_pc(addr_t new_pc) { return new_pc; } -CPU::CPUExpected CortexM3CPU::raise_irq(intr::intr_n_t) { +CPU::CPUExpected CortexM3CPU::raise_irq(intr::intr_n_t irq) { + // External peripheral → NVIC pending injection. This is the single channel + // every MMIO IRQ (TIM/USART/EXTI…) funnels through; SysTick bypasses it via + // sys_tick_irq() (system exception 15, not an NVIC line). + if (nvic_) { + nvic_->set_pending(static_cast(irq)); + } return {}; } diff --git a/src/chips/stm32f1/stm32f103_soc.cpp b/src/chips/stm32f1/stm32f103_soc.cpp index 39ae947..1df12e4 100644 --- a/src/chips/stm32f1/stm32f103_soc.cpp +++ b/src/chips/stm32f1/stm32f103_soc.cpp @@ -64,6 +64,13 @@ Stm32f103Soc::create() { } }); + // Wire Timer UIF (edge, DIER.UIE) → NVIC TIM2 line (IRQ 28). + p.tim2.set_irq_callback([cm3_weak]() { + if (cm3_weak.IsValid()) { + (void)cm3_weak->raise_irq(kTim2Irqn); + } + }); + // Wire SCB VTOR write → CPU vector_table_base_ update p.scb.set_vtor_callback([cm3_weak](uint32_t vtor) { if (cm3_weak.IsValid()) { diff --git a/src/chips/stm32f1/stm32f1_timer.cpp b/src/chips/stm32f1/stm32f1_timer.cpp index 4803a44..3106f3b 100644 --- a/src/chips/stm32f1/stm32f1_timer.cpp +++ b/src/chips/stm32f1/stm32f1_timer.cpp @@ -66,8 +66,16 @@ void Stm32f1Timer::tick(uint64_t cycles) { cnt_ += static_cast(prescaled); if (arr_ > 0 && cnt_ >= arr_) { - sr_ |= 0x0001; // UIF cnt_ = cnt_ % arr_; + // Update event: set UIF on the 0→1 edge only. With DIER.UIE (bit0) + // enabled, raise the TIM IRQ once per edge (NVIC set_pending is + // idempotent, but edge semantics avoid re-raising while UIF stays set). + if (!(sr_ & 0x0001u)) { + sr_ |= 0x0001u; // UIF + if ((dier_ & 0x0001u) && irq_cb_) { + irq_cb_(); + } + } } } diff --git a/test/test_interrupt_roundtrip.cpp b/test/test_interrupt_roundtrip.cpp index 9e48a5e..0aa81b4 100644 --- a/test/test_interrupt_roundtrip.cpp +++ b/test/test_interrupt_roundtrip.cpp @@ -4,6 +4,7 @@ #include "chips/stm32f1/clock_domains.hpp" #include "chips/stm32f1/interrupt_config.hpp" #include "chips/stm32f1/memory_bus.hpp" +#include "chips/stm32f1/stm32f1_timer.hpp" #include "memory/bus.hpp" #include "memory/flat_memory.hpp" #include "periph/nvic.hpp" @@ -48,6 +49,7 @@ class InterruptTest : public ::testing::Test { NvicPeripheral nvic_; ScbPeripheral scb_; std::unique_ptr systick_; + chips::stm32f1::Stm32f1Timer tim2_; std::unique_ptr cpu_; void SetUp() override { @@ -61,6 +63,10 @@ class InterruptTest : public ::testing::Test { cpu_ = std::make_unique(bus_.GetWeak()); cpu_->set_nvic(nvic_); cpu_->set_scb(scb_); + // TIM2 at 0x40000000; UIF edge → NVIC TIM2 line (IRQ 28). + ASSERT_TRUE( + bus_.map(memory::region(0x40000000, 0x400, tim2_.GetWeak())).has_value()); + tim2_.set_irq_callback([this]() { (void)cpu_->raise_irq(kTim2Irqn); }); scb_.set_vtor_callback( [this](uint32_t vtor) { cpu_->set_vector_table_base(vtor); }); scb_.set_prigroup_callback( @@ -487,3 +493,45 @@ TEST_F(InterruptTest, PriorityGroupingAffectsPreemption) { EXPECT_TRUE(cpu_->in_handler_mode()); EXPECT_EQ(cpu_->pc().value(), kHandlerA + 4); } + +// ── Timer UIF → NVIC → handler roundtrip (C2) ── +// Coordinator drives TIM2 tick → UIF edge → raise_irq(28) → NVIC pending → +// handler entry → BX LR return. First end-to-end use of the raise_irq channel. +TEST_F(InterruptTest, TimerUifRoundtrip) { + constexpr addr_t kHandler = kFlashBase + 0x110; + store_vector_table_entry(0, kInitSp); + store_vector_table_entry(1, kMainCode); + store_vector_table_entry(16 + kTim2Irqn, kHandler | 1u); // TIM2 → vector 44 + store_instructions(kMainCode, {0xE7FE}); // B . + store_instructions(kHandler, {0x4770}); // BX LR + ASSERT_TRUE(cpu_->set_pc(kMainCode).has_value()); + + // NVIC: enable TIM2 (bit 28), priority 0xE0 (below thread 0xF0). + ASSERT_TRUE(bus_.write(0xE000E100, 1u << kTim2Irqn, Width::Word).has_value()); + ASSERT_TRUE(bus_.write(0xE000E400 + kTim2Irqn, 0xE0u, Width::Word).has_value()); + + // TIM2: PSC=0, ARR=5, DIER.UIE, CR1.CEN + ASSERT_TRUE(bus_.write(0x40000028, 0u, Width::Word).has_value()); // PSC + ASSERT_TRUE(bus_.write(0x4000002C, 5u, Width::Word).has_value()); // ARR + ASSERT_TRUE(bus_.write(0x4000000C, 1u, Width::Word).has_value()); // DIER.UIE + ASSERT_TRUE(bus_.write(0x40000000, 1u, Width::Word).has_value()); // CR1.CEN + + VirtualClock clk(stm32f103_default_clocks); + SimulationCoordinator coord(std::move(clk)); + coord.set_cpu(cpu_->GetWeak()); + coord.add_tickable(tim2_.GetWeak(), domain_index(ClockDomain::Apb1)); + + bool entered = false, returned = false; + for (size_t i = 0; i < 80; ++i) { + ASSERT_TRUE(coord.step().has_value()); + if (cpu_->in_handler_mode() && !entered) { + entered = true; + } + if (entered && !cpu_->in_handler_mode()) { + returned = true; + break; + } + } + EXPECT_TRUE(entered) << "TIM2 UIF did not raise an IRQ that entered the handler"; + EXPECT_TRUE(returned) << "TIM2 handler did not return"; +} diff --git a/test/test_stm32f1_periph.cpp b/test/test_stm32f1_periph.cpp index 1f5f592..7b39160 100644 --- a/test/test_stm32f1_periph.cpp +++ b/test/test_stm32f1_periph.cpp @@ -358,6 +358,42 @@ TEST(TimerTest, MmioThroughBus) { EXPECT_EQ(*cnt, 10u); } +TEST(TimerTest, UifEdgeWithUieTriggersIrqCallback) { + // UIF 0→1 edge + DIER.UIE → irq_callback fires exactly once; re-overflow + // while UIF stays set does not re-fire (edge); clearing UIF re-arms. + Stm32f1Timer tim; + int count = 0; + tim.set_irq_callback([&] { ++count; }); + tim.set_prescaler(0); // divisor 1 + tim.set_auto_reload(5); + ASSERT_TRUE(tim.write(0x0C, 1u, Width::Word).has_value()); // DIER.UIE + tim.enable(true); // CR1.CEN + + tim.tick(5); // cnt 0→5 → overflow, UIF edge + EXPECT_TRUE(tim.update_flag()); + EXPECT_EQ(count, 1); + + tim.tick(10); // UIF still set → no re-fire (edge) + EXPECT_EQ(count, 1); + + tim.clear_update_flag(); // re-arm + tim.tick(5); + EXPECT_EQ(count, 2); +} + +TEST(TimerTest, UifWithoutUieDoesNotFireCallback) { + Stm32f1Timer tim; + bool fired = false; + tim.set_irq_callback([&] { fired = true; }); + tim.set_prescaler(0); + tim.set_auto_reload(5); + tim.enable(true); + // DIER.UIE not set + tim.tick(5); + EXPECT_TRUE(tim.update_flag()); // UIF still set + EXPECT_FALSE(fired); // but no IRQ raised +} + // ── FLASH Tests ── TEST(FlashTest, AcrDefault) { From 857fb0e0cc53ee195b636a584fb742d22e554680 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 23 Jun 2026 18:03:33 +0800 Subject: [PATCH 2/3] feat(cortex-m3): EXTI external interrupt + AFIO EXTICR routing (C1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Stm32f1Exti (IMR/EMR/RTSR/FTSR/SWIER/PR @ 0x40010400). GPIO edges → AFIO EXTICR routing (new exti_line_port getter) → IMR+RTSR/FTSR match → pending + raise NVIC (line→IRQ: 0-4=6-10, 5-9=23, 10-15=40). Reuses the raise_irq channel from C2 (EXTI is its second consumer). GPIO simulate_input now emits edge_signal so external input edges feed EXTI. SoC wires gpioa/b/c edge_signal → EXTI → raise_irq. Tests: 6 unit (registers/PR/routing/mask/edge-select) + ExtiGpioEdgeRoundtrip E2E. ctest 296/296. --- CMakeLists.txt | 1 + .../06-progress-assessment-and-replan.md | 12 +++ document/notes/015-exti-external-interrupt.md | 33 ++++++++ include/chips/stm32f1/stm32f103_soc.hpp | 2 + include/chips/stm32f1/stm32f1_afio.hpp | 4 + include/chips/stm32f1/stm32f1_exti.hpp | 72 +++++++++++++++++ src/chips/stm32f1/peripheral_config.cpp | 6 ++ src/chips/stm32f1/stm32f103_soc.cpp | 17 ++++ src/chips/stm32f1/stm32f1_afio.cpp | 7 ++ src/chips/stm32f1/stm32f1_exti.cpp | 80 +++++++++++++++++++ src/chips/stm32f1/stm32f1_gpio.cpp | 8 ++ test/test_interrupt_roundtrip.cpp | 48 +++++++++++ test/test_stm32f1_periph.cpp | 80 +++++++++++++++++++ 13 files changed, 370 insertions(+) create mode 100644 document/notes/015-exti-external-interrupt.md create mode 100644 include/chips/stm32f1/stm32f1_exti.hpp create mode 100644 src/chips/stm32f1/stm32f1_exti.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dfcb81c..7c17534 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set(MICRO_FORGE_SOURCES src/chips/stm32f1/peripheral_config.cpp src/chips/stm32f1/stm32f103_soc.cpp src/chips/stm32f1/stm32f1_afio.cpp + src/chips/stm32f1/stm32f1_exti.cpp src/chips/stm32f1/stm32f1_flash.cpp src/chips/stm32f1/stm32f1_gpio.cpp src/chips/stm32f1/stm32f1_rcc.cpp diff --git a/document/milestones/06-progress-assessment-and-replan.md b/document/milestones/06-progress-assessment-and-replan.md index 65af09a..2ed83e5 100644 --- a/document/milestones/06-progress-assessment-and-replan.md +++ b/document/milestones/06-progress-assessment-and-replan.md @@ -181,6 +181,18 @@ - 验证:单元(edge/UIE 语义)+ `TimerUifRoundtrip` E2E(coordinator Apb1 驱动 tick → handler 往返)。**289/289 绿**,固件 E2E/CLI 无回归。细节见 notes 014。 - **C2 进度**:Timer UIF→IRQ E2E 由 ~50%(UIF 产生但无端到端)提升到通道打通 + 往返验证。剩 C2c 抢占/嵌套场景(检验第一波 A 的 `active_priorities_`),再 C1 EXTI / C3 USART RX-IRQ。 +## 实施记录(2026-06-23 · 第二波 C1 EXTI) + +继 C2 打通 raise_irq 公共通道后,EXTI 作为第二个消费者落地(证通道通用): + +- **EXTI 控制器**(`Stm32f1Exti` @0x40010400):IMR/EMR/RTSR/FTSR/SWIER/PR(PR rc_w1)。GPIO 边沿 → AFIO EXTICR 路由校验 → IMR+RTSR/FTSR 沿匹配 → set PR + raise(线→IRQ:0-4=6-10,5-9=23,10-15=40)。 +- **AFIO EXTICR 终于有消费者**:加 `exti_line_port(line)` getter,EXTI 据此路由。 +- **GPIO simulate_input 补 edge emit**:原只 ODR 变化 emit;EXTI 监听外部输入边沿,故输入边沿同路径喂 EXTI。 +- **SoC 接线**:`gpioa/b/c.edge_signal()` → EXTI;EXTI → `raise_irq`(复用 C2 通道)。 +- 验证:6 单元(寄存器/PR/路由/屏蔽/沿选择)+ `ExtiGpioEdgeRoundtrip` E2E(simulate_input PA2 → handler 往返)。**296/296 绿**,无回归。细节 notes 015。 +- **坑**:`MICRO_FORGE_SOURCES` 是显式 `set()` 列表(非 GLOB_RECURSE,DIRECTIVES A 描述不准),新 `src/*.cpp` 须手动加 CMakeLists。C2c 抢占验证边际价值低(test_interrupt 已覆盖),跳过。 +- 06 进度:EXTI 0% → 端到端通。剩 C3 USART RX-IRQ(raise_irq 第三消费者)。 + ## 结论 / 下一步 diff --git a/document/notes/015-exti-external-interrupt.md b/document/notes/015-exti-external-interrupt.md new file mode 100644 index 0000000..a571263 --- /dev/null +++ b/document/notes/015-exti-external-interrupt.md @@ -0,0 +1,33 @@ +# 015 — EXTI 外部中断 + AFIO EXTICR(C1) + +> 06 第二波 C1。新增 EXTI 控制器(IMR/EMR/RTSR/FTSR/SWIER/PR),GPIO 边沿经 AFIO EXTICR 路由 → 触发 → raise NVIC(复用 C2 的 raise_irq 通道,EXTI 是其第二个消费者)。GPIO `simulate_input` 改为也 emit edge(外部输入边沿喂 EXTI)。ctest **296/296 绿**(289 + 7 新)。 + +## 背景 + +06 第二波 C1:GPIO 外部中断模式。AFIO EXTICR 寄存器已存但无消费者,缺 EXTI 控制器(06 实测 EXTI 0%)。复用 C2 打通的 raise_irq 通道 —— EXTI 作为第二个消费者,证明该通道通用(TIM/EXTI/USART 都走它)。 + +## 实现 + +1. **`Stm32f1Exti : Device`**(@0x40010400):6 寄存器 IMR/EMR/RTSR/FTSR/SWIER/PR(PR rc_w1,写 1 清)。`on_gpio_edge(GpioEdge)`:线=pin → EXTICR 路由校验 port → IMR 使能 + RTSR/FTSR 沿匹配 → set PR + raise(经 `exti_irq_for_line`)。 +2. **AFIO `exti_line_port(line)`**([stm32f1_afio.hpp](include/chips/stm32f1/stm32f1_afio.hpp)):暴露 EXTICR 路由(线 N → 端口 0=PA,1=PB,…)。原 `exticr_[4]` 私有无 getter。 +3. **GPIO `simulate_input` emit edge**([stm32f1_gpio.cpp](src/chips/stm32f1/stm32f1_gpio.cpp)):原只改 `idr_` 不 emit;EXTI 监听外部输入边沿,故现也 emit edge_signal(与 ODR 边沿同路径)。 +4. **线→IRQ**(`exti_irq_for_line`,EXTI hpp):0-4=6-10,5-9=23(EXTI9_5),10-15=40(EXTI15_10)。 +5. **SoC 接线**([stm32f103_soc.cpp](src/chips/stm32f1/stm32f103_soc.cpp)):`exti.set_afio(afio)` + `gpioa/b/c.edge_signal().connect(exti slot)` + `exti.set_irq_callback(raise)`。 + +## 验证(7 新单测) + +- **单元**([test_stm32f1_periph.cpp](test/test_stm32f1_periph.cpp)):寄存器 R/W、PR w1c、上升沿触发 + IRQ 号、错端口不触发、IMR 屏蔽、FTSR-only 不响应上升沿。 +- **E2E**([test_interrupt_roundtrip.cpp](test/test_interrupt_roundtrip.cpp) `ExtiGpioEdgeRoundtrip`):`simulate_input` PA2 上升沿 → EXTI → raise IRQ8 → handler 进入 → BX LR 返回。 +- `ctest` 全量 **296/296 绿**,固件 E2E/CLI/中断抢占无回归。 + +## 陷阱 + +- **`MICRO_FORGE_SOURCES` 是显式 `set()` 列表**(CMakeLists.txt 行 25),**非 DIRECTIVES A 说的 `GLOB_RECURSE`**:新 `src/*.cpp` 须手动加该列表,否则 `vtable undefined` link error(reconfigure 也不会自动扫)。 +- **IPR word 对齐**:EXTI 线优先级寄存器 `0xE000E400+N`(byte N);word 写须 4 对齐(IRQ8 在 0xE000E408 byte0;IRQ9 在 byte1 需 `<<8`)。测试选 IRQ8(word 对齐)避 unaligned。 +- **simulate_input vs ODR edge**:原 `edge_signal` 只 ODR 变化 emit;EXTI 要外部输入,故补 `simulate_input` edge emit(输入边沿同路径喂 EXTI)。 +- **EXTICR 路由**:EXTI 线 N 路由到 `EXTICR[N/4]` bits[(N%4)*4 : +4] 选的端口;错端口的 edge 不触发。 +- **C2c 跳过**:抢占/嵌套验证边际价值低(Thread 被抢占已由 `TimerUifRoundtrip` 验证 + `active_priorities_` 已被 test_interrupt 5 测试覆盖),本里程碑不补。 + +## 成果 + +EXTI 外部中断端到端(GPIO 边沿 → EXTI → NVIC → handler),AFIO EXTICR 终于有消费者,raise_irq 通道第二个用户(证通用)。剩 C3 USART RX-IRQ(第三消费者,需先设计串口输入注入)。 diff --git a/include/chips/stm32f1/stm32f103_soc.hpp b/include/chips/stm32f1/stm32f103_soc.hpp index 58191c5..d749668 100644 --- a/include/chips/stm32f1/stm32f103_soc.hpp +++ b/include/chips/stm32f1/stm32f103_soc.hpp @@ -3,6 +3,7 @@ #include "chips/machine.hpp" #include "chips/stm32f1/clock_domains.hpp" #include "chips/stm32f1/stm32f1_afio.hpp" +#include "chips/stm32f1/stm32f1_exti.hpp" #include "chips/stm32f1/stm32f1_flash.hpp" #include "chips/stm32f1/stm32f1_gpio.hpp" #include "chips/stm32f1/stm32f1_rcc.hpp" @@ -45,6 +46,7 @@ struct Stm32f103Parts { Stm32f1Gpio gpioc{'C'}; Stm32f1Usart usart1; Stm32f1Timer tim2; + Stm32f1Exti exti; Stm32f103Parts() = default; diff --git a/include/chips/stm32f1/stm32f1_afio.hpp b/include/chips/stm32f1/stm32f1_afio.hpp index 09dae1d..adb6908 100644 --- a/include/chips/stm32f1/stm32f1_afio.hpp +++ b/include/chips/stm32f1/stm32f1_afio.hpp @@ -14,6 +14,10 @@ class Stm32f1Afio : public periph::Device { Expected write(addr_t offset, data_t data, Width w) override; std::string_view name() const noexcept override { return "AFIO"; } + // EXTI line N → owning GPIO port index (0=PA,1=PB,2=PC,3=PD,4=PE), + // decoded from EXTICR. Consumed by Stm32f1Exti to route GPIO edges. + uint8_t exti_line_port(uint8_t line) const; + WeakPtr GetWeak() { return weak_factory_.GetWeakPtr(); } private: diff --git a/include/chips/stm32f1/stm32f1_exti.hpp b/include/chips/stm32f1/stm32f1_exti.hpp new file mode 100644 index 0000000..1186a5e --- /dev/null +++ b/include/chips/stm32f1/stm32f1_exti.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "cpu/intr.hpp" +#include "hooks/events.hpp" +#include "periph/device.hpp" +#include "util/weak_ptr/weak_ptr_factory.h" + +#include +#include + +namespace micro_forge::chips::stm32f1 { + +class Stm32f1Afio; + +// STM32F1 External Interrupt/Event controller (EXTI), mapped at 0x40010400. +// Each EXTI line 0..15 is routed to one GPIO port via AFIO EXTICR; a configured +// edge on that port's pin sets the pending bit and raises the line's NVIC IRQ. +class Stm32f1Exti : public periph::Device { + public: + Stm32f1Exti() = default; + + // EXTI needs AFIO EXTICR to map a line number to the owning GPIO port. + void set_afio(Stm32f1Afio& afio) { afio_ = &afio; } + + // Peripheral → CPU IRQ channel: invoked once per line that goes pending. + void set_irq_callback(std::function cb) { + irq_cb_ = std::move(cb); + } + + // GPIO edge slot — subscribe this to each GPIO's edge_signal(). + void on_gpio_edge(const hooks::GpioEdge& edge); + + // Device + Expected read(addr_t offset, Width w) override; + Expected write(addr_t offset, data_t data, Width w) override; + std::string_view name() const noexcept override { return "EXTI"; } + + bool pending(uint8_t line) const { return (pr_ >> line) & 1u; } + + WeakPtr GetWeak() { return weak_factory_.GetWeakPtr(); } + + private: + uint32_t imr_ = 0; // Interrupt mask (line enable) + uint32_t emr_ = 0; // Event mask (stored; v1 has no event consumer) + uint32_t rtsr_ = 0; // Rising-edge trigger select + uint32_t ftsr_ = 0; // Falling-edge trigger select + uint32_t swier_ = 0; // Software interrupt trigger + uint32_t pr_ = 0; // Pending (rc_w1: write 1 to clear) + + Stm32f1Afio* afio_ = nullptr; + std::function irq_cb_; + + WeakPtrFactory weak_factory_{this}; + + void trigger_line(uint8_t line); +}; + +// EXTI line N → NVIC IRQ number (STM32F1 vector table). Lines 0-4 have +// individual IRQs (6-10); 5-9 share EXTI9_5 (23); 10-15 share EXTI15_10 (40). +inline intr::intr_n_t exti_irq_for_line(uint8_t line) { + switch (line) { + case 0: return 6; + case 1: return 7; + case 2: return 8; + case 3: return 9; + case 4: return 10; + case 5: case 6: case 7: case 8: case 9: return 23; + default: return 40; // 10-15 + } +} + +} // namespace micro_forge::chips::stm32f1 diff --git a/src/chips/stm32f1/peripheral_config.cpp b/src/chips/stm32f1/peripheral_config.cpp index 4c98832..7e5a2b5 100644 --- a/src/chips/stm32f1/peripheral_config.cpp +++ b/src/chips/stm32f1/peripheral_config.cpp @@ -59,6 +59,12 @@ Expected configure_peripherals(memory::Bus& bus, Stm32f103Parts& parts) { return result; } + result = map_checked(0x4001'0400_addr, 0x400_addr, + parts.exti.GetWeak()); + if (!result) { + return result; + } + return {}; } diff --git a/src/chips/stm32f1/stm32f103_soc.cpp b/src/chips/stm32f1/stm32f103_soc.cpp index 1df12e4..db1ec90 100644 --- a/src/chips/stm32f1/stm32f103_soc.cpp +++ b/src/chips/stm32f1/stm32f103_soc.cpp @@ -1,6 +1,7 @@ #include "chips/stm32f1/stm32f103_soc.hpp" #include "arch/arm/cortex_m3/cortex_m3.hpp" #include "arch/arm/cortex_m3/cortex_m3_reset.hpp" +#include "hooks/events.hpp" #include "chips/stm32f1/interrupt_config.hpp" #include "chips/stm32f1/memory_bus.hpp" #include "chips/stm32f1/peripheral_config.hpp" @@ -71,6 +72,22 @@ Stm32f103Soc::create() { } }); + // Wire EXTI: GPIO edges → EXTI (EXTICR routing) → NVIC. + p.exti.set_afio(p.afio); + p.exti.set_irq_callback([cm3_weak](intr::intr_n_t irq) { + if (cm3_weak.IsValid()) { + (void)cm3_weak->raise_irq(irq); + } + }); + auto exti_edge = [exti_weak = p.exti.GetWeak()](const hooks::GpioEdge& e) { + if (exti_weak.IsValid()) { + exti_weak->on_gpio_edge(e); + } + }; + p.gpioa.edge_signal().connect(exti_edge); + p.gpiob.edge_signal().connect(exti_edge); + p.gpioc.edge_signal().connect(exti_edge); + // Wire SCB VTOR write → CPU vector_table_base_ update p.scb.set_vtor_callback([cm3_weak](uint32_t vtor) { if (cm3_weak.IsValid()) { diff --git a/src/chips/stm32f1/stm32f1_afio.cpp b/src/chips/stm32f1/stm32f1_afio.cpp index 5efdf0b..b189c77 100644 --- a/src/chips/stm32f1/stm32f1_afio.cpp +++ b/src/chips/stm32f1/stm32f1_afio.cpp @@ -40,4 +40,11 @@ Expected Stm32f1Afio::write(addr_t offset, data_t data, Width w) { } } +uint8_t Stm32f1Afio::exti_line_port(uint8_t line) const { + if (line > 15) { + return 0; + } + return (exticr_[line / 4] >> ((line % 4) * 4)) & 0xFu; +} + } // namespace micro_forge::chips::stm32f1 diff --git a/src/chips/stm32f1/stm32f1_exti.cpp b/src/chips/stm32f1/stm32f1_exti.cpp new file mode 100644 index 0000000..b9bcc76 --- /dev/null +++ b/src/chips/stm32f1/stm32f1_exti.cpp @@ -0,0 +1,80 @@ +#include "chips/stm32f1/stm32f1_exti.hpp" +#include "chips/stm32f1/stm32f1_afio.hpp" + +namespace micro_forge::chips::stm32f1 { + +Expected Stm32f1Exti::read(addr_t offset, Width w) { + if (w != Width::Word) { + return std::unexpected(BusError::Unaligned); + } + switch (offset) { + case 0x00: return imr_; + case 0x04: return emr_; + case 0x08: return rtsr_; + case 0x0C: return ftsr_; + case 0x10: return swier_; + case 0x14: return pr_; + default: return std::unexpected(BusError::PeripheralFault); + } +} + +Expected Stm32f1Exti::write(addr_t offset, data_t data, Width w) { + if (w != Width::Word) { + return std::unexpected(BusError::Unaligned); + } + switch (offset) { + case 0x00: imr_ = data; return {}; + case 0x04: emr_ = data; return {}; + case 0x08: rtsr_ = data; return {}; + case 0x0C: ftsr_ = data; return {}; + case 0x10: + // SWIER: writing 1 software-triggers a line if IMR is enabled and + // it is not already pending. Bits without IMR are ignored. + swier_ = data; + for (uint8_t line = 0; line < 16; ++line) { + if ((data & (1u << line)) && (imr_ & (1u << line)) && + !(pr_ & (1u << line))) { + trigger_line(line); + } + } + return {}; + case 0x14: + // PR is rc_w1: writing 1 clears the pending bit. + pr_ &= ~data; + return {}; + default: return std::unexpected(BusError::PeripheralFault); + } +} + +void Stm32f1Exti::on_gpio_edge(const hooks::GpioEdge& edge) { + uint8_t line = edge.pin; + if (line > 15) { + return; + } + if (!(imr_ & (1u << line))) { + return; // line not unmasked + } + // EXTICR routes this line to exactly one GPIO port; only an edge on that + // port fires. + if (afio_) { + uint8_t port_sel = afio_->exti_line_port(line); // 0=PA,1=PB,... + char expected = static_cast('A' + port_sel); + if (edge.port != expected) { + return; + } + } + bool match = (edge.rising && (rtsr_ & (1u << line))) || + (!edge.rising && (ftsr_ & (1u << line))); + if (match) { + trigger_line(line); + } +} + +void Stm32f1Exti::trigger_line(uint8_t line) { + pr_ |= (1u << line); + if (irq_cb_) { + irq_cb_(exti_irq_for_line(line)); + } +} + +} // namespace micro_forge::chips::stm32f1 diff --git a/src/chips/stm32f1/stm32f1_gpio.cpp b/src/chips/stm32f1/stm32f1_gpio.cpp index 1ec5b1e..dd4c4c5 100644 --- a/src/chips/stm32f1/stm32f1_gpio.cpp +++ b/src/chips/stm32f1/stm32f1_gpio.cpp @@ -132,11 +132,19 @@ void Stm32f1Gpio::simulate_input(uint8_t pin, bool high) { if (pin > 15) { return; } + bool was_high = (idr_ >> pin) & 1u; if (high) { idr_ |= (1u << pin); } else { idr_ &= ~(1u << pin); } + // An external input edge feeds EXTI the same way an output edge does. + bool now_high = (idr_ >> pin) & 1u; + if (was_high != now_high && !edge_signal_.empty()) { + edge_signal_.emit(hooks::GpioEdge{ + {cycle_source_ ? cycle_source_() : 0}, + static_cast(port_id_), pin, now_high}); + } } void Stm32f1Gpio::set_pin_change_callback(PinChangeCallback cb) { diff --git a/test/test_interrupt_roundtrip.cpp b/test/test_interrupt_roundtrip.cpp index 0aa81b4..7630aeb 100644 --- a/test/test_interrupt_roundtrip.cpp +++ b/test/test_interrupt_roundtrip.cpp @@ -4,7 +4,11 @@ #include "chips/stm32f1/clock_domains.hpp" #include "chips/stm32f1/interrupt_config.hpp" #include "chips/stm32f1/memory_bus.hpp" +#include "chips/stm32f1/stm32f1_afio.hpp" +#include "chips/stm32f1/stm32f1_exti.hpp" +#include "chips/stm32f1/stm32f1_gpio.hpp" #include "chips/stm32f1/stm32f1_timer.hpp" +#include "hooks/events.hpp" #include "memory/bus.hpp" #include "memory/flat_memory.hpp" #include "periph/nvic.hpp" @@ -50,6 +54,9 @@ class InterruptTest : public ::testing::Test { ScbPeripheral scb_; std::unique_ptr systick_; chips::stm32f1::Stm32f1Timer tim2_; + chips::stm32f1::Stm32f1Afio afio_; + chips::stm32f1::Stm32f1Exti exti_; + chips::stm32f1::Stm32f1Gpio gpioa_{'A'}; std::unique_ptr cpu_; void SetUp() override { @@ -67,6 +74,17 @@ class InterruptTest : public ::testing::Test { ASSERT_TRUE( bus_.map(memory::region(0x40000000, 0x400, tim2_.GetWeak())).has_value()); tim2_.set_irq_callback([this]() { (void)cpu_->raise_irq(kTim2Irqn); }); + // EXTI: AFIO (0x40010000) + EXTI (0x40010400). GPIOA driven directly + // via simulate_input (no bus map needed); its edges feed EXTI. + ASSERT_TRUE( + bus_.map(memory::region(0x40010000, 0x400, afio_.GetWeak())).has_value()); + ASSERT_TRUE( + bus_.map(memory::region(0x40010400, 0x400, exti_.GetWeak())).has_value()); + exti_.set_afio(afio_); + exti_.set_irq_callback( + [this](intr::intr_n_t irq) { (void)cpu_->raise_irq(irq); }); + gpioa_.edge_signal().connect( + [this](const hooks::GpioEdge& e) { exti_.on_gpio_edge(e); }); scb_.set_vtor_callback( [this](uint32_t vtor) { cpu_->set_vector_table_base(vtor); }); scb_.set_prigroup_callback( @@ -535,3 +553,33 @@ TEST_F(InterruptTest, TimerUifRoundtrip) { EXPECT_TRUE(entered) << "TIM2 UIF did not raise an IRQ that entered the handler"; EXPECT_TRUE(returned) << "TIM2 handler did not return"; } + +// ── EXTI GPIO edge → NVIC → handler roundtrip (C1) ── +// simulate_input rising edge on PA2 → GPIO edge_signal → EXTI (EXTICR routes +// line 2 to PA, IMR+RTSR match) → raise EXTI2 IRQ (8) → handler entry/return. +TEST_F(InterruptTest, ExtiGpioEdgeRoundtrip) { + constexpr addr_t kHandler = kFlashBase + 0x110; + store_vector_table_entry(0, kInitSp); + store_vector_table_entry(1, kMainCode); + store_vector_table_entry(16 + 8, kHandler | 1u); // EXTI2 → IRQ8 → vector 24 + store_instructions(kMainCode, {0xE7FE}); // B . + store_instructions(kHandler, {0x4770}); // BX LR + ASSERT_TRUE(cpu_->set_pc(kMainCode).has_value()); + + // NVIC: enable EXTI2 (IRQ8), priority 0xE0 (IPR2 byte0, word-aligned). + ASSERT_TRUE(bus_.write(0xE000E100, 1u << 8, Width::Word).has_value()); + ASSERT_TRUE(bus_.write(0xE000E408, 0xE0u, Width::Word).has_value()); + + // AFIO EXTICR1: line 2 → port A (0). EXTI: IMR line2 + RTSR line2. + ASSERT_TRUE(bus_.write(0x40010008, 0u, Width::Word).has_value()); // EXTICR1 + ASSERT_TRUE(bus_.write(0x40010400, 1u << 2, Width::Word).has_value()); // IMR + ASSERT_TRUE(bus_.write(0x40010408, 1u << 2, Width::Word).has_value()); // RTSR + + gpioa_.simulate_input(2, true); // rising edge PA2 → EXTI → raise IRQ8 + + ASSERT_TRUE(cpu_->step().has_value()); // pending → enter handler + EXPECT_TRUE(cpu_->in_handler_mode()); + EXPECT_EQ(cpu_->pc().value(), kHandler); + ASSERT_TRUE(cpu_->step().has_value()); // BX LR → return + EXPECT_FALSE(cpu_->in_handler_mode()); +} diff --git a/test/test_stm32f1_periph.cpp b/test/test_stm32f1_periph.cpp index 7b39160..2b8eef3 100644 --- a/test/test_stm32f1_periph.cpp +++ b/test/test_stm32f1_periph.cpp @@ -1,6 +1,7 @@ #include #include "chips/stm32f1/stm32f1_afio.hpp" +#include "chips/stm32f1/stm32f1_exti.hpp" #include "chips/stm32f1/stm32f1_flash.hpp" #include "chips/stm32f1/stm32f1_gpio.hpp" #include "chips/stm32f1/stm32f1_rcc.hpp" @@ -394,6 +395,85 @@ TEST(TimerTest, UifWithoutUieDoesNotFireCallback) { EXPECT_FALSE(fired); // but no IRQ raised } +// ── EXTI Tests ── + +TEST(ExtiTest, RegisterReadWrite) { + Stm32f1Exti exti; + ASSERT_TRUE(exti.write(0x00, 0x100u, Width::Word).has_value()); // IMR + ASSERT_TRUE(exti.write(0x08, 0x200u, Width::Word).has_value()); // RTSR + auto imr = exti.read(0x00, Width::Word); + auto rtsr = exti.read(0x08, Width::Word); + ASSERT_TRUE(imr.has_value() && rtsr.has_value()); + EXPECT_EQ(*imr, 0x100u); + EXPECT_EQ(*rtsr, 0x200u); +} + +TEST(ExtiTest, PendingClearsOnWrite1) { + Stm32f1Exti exti; + ASSERT_TRUE(exti.write(0x00, 1u, Width::Word).has_value()); // IMR line0 + ASSERT_TRUE(exti.write(0x10, 1u, Width::Word).has_value()); // SWIER line0 + EXPECT_TRUE(exti.pending(0)); + ASSERT_TRUE(exti.write(0x14, 1u, Width::Word).has_value()); // PR w1c + EXPECT_FALSE(exti.pending(0)); +} + +TEST(ExtiTest, RisingEdgeOnRoutedPortTriggers) { + Stm32f1Afio afio; + Stm32f1Exti exti; + exti.set_afio(afio); + intr::intr_n_t raised = 0xFF; + exti.set_irq_callback([&](intr::intr_n_t irq) { raised = irq; }); + ASSERT_TRUE(afio.write(0x08, 0u, Width::Word).has_value()); // EXTICR1 → PA + ASSERT_TRUE(exti.write(0x00, 1u << 3, Width::Word).has_value()); // IMR line3 + ASSERT_TRUE(exti.write(0x08, 1u << 3, Width::Word).has_value()); // RTSR line3 + exti.on_gpio_edge({{}, 'A', 3, true}); + EXPECT_TRUE(exti.pending(3)); + EXPECT_EQ(raised, 9u); // EXTI3 → IRQ9 +} + +TEST(ExtiTest, EdgeOnWrongPortDoesNotTrigger) { + Stm32f1Afio afio; + Stm32f1Exti exti; + exti.set_afio(afio); + bool fired = false; + exti.set_irq_callback([&](intr::intr_n_t) { fired = true; }); + ASSERT_TRUE(afio.write(0x08, 0u, Width::Word).has_value()); // line3 → PA + ASSERT_TRUE(exti.write(0x00, 1u << 3, Width::Word).has_value()); // IMR + ASSERT_TRUE(exti.write(0x08, 1u << 3, Width::Word).has_value()); // RTSR + exti.on_gpio_edge({{}, 'B', 3, true}); // PB3 but routed to PA → no trigger + EXPECT_FALSE(fired); + EXPECT_FALSE(exti.pending(3)); +} + +TEST(ExtiTest, ImrMasksLine) { + Stm32f1Afio afio; + Stm32f1Exti exti; + exti.set_afio(afio); + bool fired = false; + exti.set_irq_callback([&](intr::intr_n_t) { fired = true; }); + ASSERT_TRUE(afio.write(0x08, 0u, Width::Word).has_value()); + ASSERT_TRUE(exti.write(0x08, 1u << 3, Width::Word).has_value()); // RTSR line3 + // IMR line3 NOT set + exti.on_gpio_edge({{}, 'A', 3, true}); + EXPECT_FALSE(fired); + EXPECT_FALSE(exti.pending(3)); +} + +TEST(ExtiTest, FallingOnlyDoesNotTriggerOnRising) { + Stm32f1Afio afio; + Stm32f1Exti exti; + exti.set_afio(afio); + int count = 0; + exti.set_irq_callback([&](intr::intr_n_t) { ++count; }); + ASSERT_TRUE(afio.write(0x08, 0u, Width::Word).has_value()); + ASSERT_TRUE(exti.write(0x00, 1u << 3, Width::Word).has_value()); // IMR + ASSERT_TRUE(exti.write(0x0C, 1u << 3, Width::Word).has_value()); // FTSR only + exti.on_gpio_edge({{}, 'A', 3, true}); // rising, only falling armed + EXPECT_EQ(count, 0); + exti.on_gpio_edge({{}, 'A', 3, false}); // falling → triggers + EXPECT_EQ(count, 1); +} + // ── FLASH Tests ── TEST(FlashTest, AcrDefault) { From 0364f163e369b4a864f7f3bd1aedc5eb7bb2ec04 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 23 Jun 2026 18:15:16 +0800 Subject: [PATCH 3/3] feat(cortex-m3): USART RX injection + RXNE interrupt (C3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit USART gains inject_rx(byte): single-slot RX buffer + SR.RXNE, DR read returns the byte and clears RXNE (read=RX/write=TX shared). RXNEIE (CR1 bit5) + RXNE → raise USART1 (IRQ37) via the raise_irq channel — its third consumer (TIM/EXTI/USART). TXEIE skipped (instant TX keeps TXE high → would loop-raise; firmware polling TXE still works). Tests: 4 unit (RXNE/DR/RXNEIE enabled+disabled) + UsartRxRoundtrip E2E (inject_rx → handler reads DR via r4, which survives exception return unlike auto-stacked r0-r3). ctest 301/301. --- .../06-progress-assessment-and-replan.md | 9 ++++ document/notes/016-usart-rx-injection.md | 27 ++++++++++++ include/chips/stm32f1/interrupt_config.hpp | 1 + include/chips/stm32f1/stm32f1_usart.hpp | 10 +++++ src/chips/stm32f1/stm32f103_soc.cpp | 7 ++++ src/chips/stm32f1/stm32f1_usart.cpp | 16 +++++++- test/test_interrupt_roundtrip.cpp | 41 +++++++++++++++++++ test/test_stm32f1_periph.cpp | 36 ++++++++++++++++ 8 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 document/notes/016-usart-rx-injection.md diff --git a/document/milestones/06-progress-assessment-and-replan.md b/document/milestones/06-progress-assessment-and-replan.md index 2ed83e5..6df70cc 100644 --- a/document/milestones/06-progress-assessment-and-replan.md +++ b/document/milestones/06-progress-assessment-and-replan.md @@ -193,6 +193,15 @@ - **坑**:`MICRO_FORGE_SOURCES` 是显式 `set()` 列表(非 GLOB_RECURSE,DIRECTIVES A 描述不准),新 `src/*.cpp` 须手动加 CMakeLists。C2c 抢占验证边际价值低(test_interrupt 已覆盖),跳过。 - 06 进度:EXTI 0% → 端到端通。剩 C3 USART RX-IRQ(raise_irq 第三消费者)。 +## 实施记录(2026-06-23 · 第二波 C3 USART RX,第二波收尾) + +- **USART RX 注入 + RXNEIE**:USART 加 `inject_rx(byte)`(单字节缓冲 + SR.RXNE bit5),DR read 返回 rx 字节 + 清 RXNE(读=RX/写=TX 共享地址)。RXNEIE(CR1 bit5)+ RXNE → raise USART1(IRQ37)。 +- **raise_irq 第三消费者**:TIM(C2)/EXTI(C1)/USART(C3)三外设全通同一通道 —— 证明 C2 打通的通道通用。 +- **TXEIE 跳过**:模拟器 TX 即时,TXE 常高 → TXEIE 会循环 raise(无 TX 延迟可消耗);固件轮询 TXE 位仍工作。 +- 验证:4 单元(RXNE/DR/RXNEIE enabled/disabled)+ `UsartRxRoundtrip` E2E。**301/301 绿**,无回归。细节 notes 016。 +- **坑**:ARM 异常自动压栈 r0-r3,handler 改它们被返回 POP 覆盖;测试读 handler 结果须用 r4+(不自动压栈)。IRQ≥32 在 ISER1,IPR IRQ37 在 0xE000E424 byte1 需 shift。 +- **第二波全部完成**(C1/C2/C3 + C4 bit-band 早前):06 第二波「外设中断端到端」收口,raise_irq 通道三消费者(TIM/EXTI/USART)全通。下一里程碑候选:第三波 DMA/SPI/FLASH、04 GUI、02 收尾。 + ## 结论 / 下一步 diff --git a/document/notes/016-usart-rx-injection.md b/document/notes/016-usart-rx-injection.md new file mode 100644 index 0000000..7c9b319 --- /dev/null +++ b/document/notes/016-usart-rx-injection.md @@ -0,0 +1,27 @@ +# 016 — USART RX 注入 + RXNE 中断(C3) + +> 06 第二波 C3(收尾)。USART 加 RX 注入(`inject_rx` 单字节 + RXNE)+ RXNEIE 中断 → raise USART1(IRQ37)。raise_irq **第三消费者**(TIM/EXTI/USART 全通)。TXEIE 跳过(模拟器 TX 即时,TXE 常高会循环 raise)。ctest **301/301 绿**(296 + 5 新)。 + +## 实现 + +1. **USART `inject_rx(byte)`**:设 `rx_dr_` + SR.RXNE(bit5);RXNEIE(CR1 bit5)使能 → `irq_callback`。 +2. **DR read 返回 rx_dr_ + 清 RXNE**(硬件 DR 读=RX/写=TX 共享地址)。 +3. **CR1 write 使能 RXNEIE 时若 RXNE 已置** → 立即 raise(固件先收字节后开中断的场景)。 +4. **`kUsart1Irqn=37`**(interrupt_config)。SoC `usart1.set_irq_callback(raise 37)`。 + +## 验证(5 新) + +- **单元**([test_stm32f1_periph.cpp](test/test_stm32f1_periph.cpp)):`inject_rx`→RXNE、DR read 清+返回字节、RXNEIE enabled→callback、disabled→不 callback。 +- **E2E**([test_interrupt_roundtrip.cpp](test/test_interrupt_roundtrip.cpp) `UsartRxRoundtrip`):`inject_rx('A')` → USART1 IRQ37 → handler `ldr r4,[r1]` 读 DR → r4='A'。 +- `ctest` 全量 **301/301 绿**,无回归。 + +## 陷阱 + +- **ARM 异常模型**:exception entry 自动压栈 r0-r3/r12/lr/pc/xpsr,exception return POP 恢复。handler 改 r0-r3 会被返回覆盖。测试读 handler 结果须用 **r4-r11**(不自动压栈,返回后保留;handler 本应 push/pop 保护 —— 测试简化可省)。 +- **IRQ≥32 在 ISER1/ISPR1**:NVIC ISER `idx=offset/4`,ISER1(0x004)enable IRQ32+(37→bit5)。IPR IRQ37 在 `0xE000E424`(IPR9)byte1,word 写需 `<<8`。 +- **TXEIE 跳过**:模拟器 TX 即时完成,TXE 常高(`0xC0`);TXEIE 会循环 raise(无 TX 移位延迟可消耗)。固件用 TXEIE 时轮询 TXE 位仍工作。MVP 不做 TXEIE。 +- **USART DR 共享**:读 DR=返回 RX+清 RXNE;写 DR=TX(原逻辑)。`dr_` 成员 write 存但 read 不返回(无害)。 + +## 成果 + +第二波外设中断端到端**全部完成**:raise_irq 公共通道(C2)+ TIM(C2)+ EXTI(C1)+ USART RX(C3)三消费者全通同一通道。06 第二波 C1/C2/C3 + C4(bit-band 早前)✅。下一里程碑候选:第三波 DMA/SPI/FLASH、04 GUI dashboard、02 收尾。 diff --git a/include/chips/stm32f1/interrupt_config.hpp b/include/chips/stm32f1/interrupt_config.hpp index faf98ce..0a9fd14 100644 --- a/include/chips/stm32f1/interrupt_config.hpp +++ b/include/chips/stm32f1/interrupt_config.hpp @@ -11,6 +11,7 @@ namespace micro_forge::chips::stm32f1 { // STM32F103 external IRQ numbers (Cortex-M exception base is 16, so TIM2 IRQ 28 // lives at vector-table index 16+28 = 44). Source: STM32F1 vector table. inline constexpr intr::intr_n_t kTim2Irqn = 28; +inline constexpr intr::intr_n_t kUsart1Irqn = 37; Expected configure_interrupt_devices(memory::Bus& bus, periph::NvicPeripheral& nvic, diff --git a/include/chips/stm32f1/stm32f1_usart.hpp b/include/chips/stm32f1/stm32f1_usart.hpp index 8b4f004..17b70eb 100644 --- a/include/chips/stm32f1/stm32f1_usart.hpp +++ b/include/chips/stm32f1/stm32f1_usart.hpp @@ -23,6 +23,13 @@ class Stm32f1Usart : public periph::Device, public periph::SerialPort { bool can_send() const override; void set_output(OutputCallback cb) override; + // RX injection (host → firmware): buffers a received byte, sets RXNE, and + // raises the USART IRQ if RXNEIE (CR1 bit5) is enabled. + void inject_rx(uint8_t byte); + + // Peripheral → CPU IRQ channel (SoC wires this to raise the USART IRQ). + void set_irq_callback(std::function cb) { irq_cb_ = std::move(cb); } + WeakPtr GetWeak() { return weak_factory_.GetWeakPtr(); } private: @@ -33,7 +40,10 @@ class Stm32f1Usart : public periph::Device, public periph::SerialPort { uint32_t cr2_ = 0; uint32_t cr3_ = 0; + uint8_t rx_dr_ = 0; // received byte (single-slot buffer) + OutputCallback output_; + std::function irq_cb_; WeakPtrFactory weak_factory_{this}; }; diff --git a/src/chips/stm32f1/stm32f103_soc.cpp b/src/chips/stm32f1/stm32f103_soc.cpp index db1ec90..bfc3e6e 100644 --- a/src/chips/stm32f1/stm32f103_soc.cpp +++ b/src/chips/stm32f1/stm32f103_soc.cpp @@ -88,6 +88,13 @@ Stm32f103Soc::create() { p.gpiob.edge_signal().connect(exti_edge); p.gpioc.edge_signal().connect(exti_edge); + // Wire USART RXNE (RXNEIE) → NVIC USART1 line (IRQ 37). + p.usart1.set_irq_callback([cm3_weak]() { + if (cm3_weak.IsValid()) { + (void)cm3_weak->raise_irq(kUsart1Irqn); + } + }); + // Wire SCB VTOR write → CPU vector_table_base_ update p.scb.set_vtor_callback([cm3_weak](uint32_t vtor) { if (cm3_weak.IsValid()) { diff --git a/src/chips/stm32f1/stm32f1_usart.cpp b/src/chips/stm32f1/stm32f1_usart.cpp index bd5a34a..e0b5844 100644 --- a/src/chips/stm32f1/stm32f1_usart.cpp +++ b/src/chips/stm32f1/stm32f1_usart.cpp @@ -13,7 +13,9 @@ Expected Stm32f1Usart::read(addr_t offset, Width w) { case 0x00: return sr_; case 0x04: - return dr_; + // DR read returns the received byte and clears RXNE. + sr_ &= ~(1u << 5); + return rx_dr_; case 0x08: return brr_; case 0x0C: @@ -51,6 +53,10 @@ Expected Stm32f1Usart::write(addr_t offset, data_t data, Width w) { return {}; case 0x0C: cr1_ = data; + // Enabling RXNEIE while RXNE is already pending must raise now. + if ((cr1_ & (1u << 5)) && (sr_ & (1u << 5)) && irq_cb_) { + irq_cb_(); + } return {}; case 0x10: cr2_ = data; @@ -80,4 +86,12 @@ void Stm32f1Usart::set_output(OutputCallback cb) { output_ = std::move(cb); } +void Stm32f1Usart::inject_rx(uint8_t byte) { + rx_dr_ = byte; + sr_ |= (1u << 5); // RXNE + if ((cr1_ & (1u << 5)) && irq_cb_) { // RXNEIE enabled + irq_cb_(); + } +} + } // namespace micro_forge::chips::stm32f1 diff --git a/test/test_interrupt_roundtrip.cpp b/test/test_interrupt_roundtrip.cpp index 7630aeb..e5e870e 100644 --- a/test/test_interrupt_roundtrip.cpp +++ b/test/test_interrupt_roundtrip.cpp @@ -8,6 +8,7 @@ #include "chips/stm32f1/stm32f1_exti.hpp" #include "chips/stm32f1/stm32f1_gpio.hpp" #include "chips/stm32f1/stm32f1_timer.hpp" +#include "chips/stm32f1/stm32f1_usart.hpp" #include "hooks/events.hpp" #include "memory/bus.hpp" #include "memory/flat_memory.hpp" @@ -57,6 +58,7 @@ class InterruptTest : public ::testing::Test { chips::stm32f1::Stm32f1Afio afio_; chips::stm32f1::Stm32f1Exti exti_; chips::stm32f1::Stm32f1Gpio gpioa_{'A'}; + chips::stm32f1::Stm32f1Usart usart1_; std::unique_ptr cpu_; void SetUp() override { @@ -85,6 +87,10 @@ class InterruptTest : public ::testing::Test { [this](intr::intr_n_t irq) { (void)cpu_->raise_irq(irq); }); gpioa_.edge_signal().connect( [this](const hooks::GpioEdge& e) { exti_.on_gpio_edge(e); }); + // USART1 at 0x40013800; RXNE (RXNEIE) → NVIC USART1 line (IRQ 37). + ASSERT_TRUE( + bus_.map(memory::region(0x40013800, 0x400, usart1_.GetWeak())).has_value()); + usart1_.set_irq_callback([this]() { (void)cpu_->raise_irq(kUsart1Irqn); }); scb_.set_vtor_callback( [this](uint32_t vtor) { cpu_->set_vector_table_base(vtor); }); scb_.set_prigroup_callback( @@ -583,3 +589,38 @@ TEST_F(InterruptTest, ExtiGpioEdgeRoundtrip) { ASSERT_TRUE(cpu_->step().has_value()); // BX LR → return EXPECT_FALSE(cpu_->in_handler_mode()); } + +// ── USART RXNE → NVIC → handler roundtrip (C3) ── +// inject_rx → RXNE + RXNEIE → raise USART1 IRQ (37) → handler reads DR. +TEST_F(InterruptTest, UsartRxRoundtrip) { + constexpr addr_t kHandler = kFlashBase + 0x110; + store_vector_table_entry(0, kInitSp); + store_vector_table_entry(1, kMainCode); + store_vector_table_entry(16 + 37, kHandler | 1u); // USART1 → IRQ37 → vector 53 + store_instructions(kMainCode, {0xE7FE}); // B . + // handler: ldr r4, [r1] (r1=USART1 DR) ; bx lr. r4 is not auto-stacked, so + // it survives exception return (r0-r3 are restored by the POP). + store_instructions(kHandler, {0x680C, 0x4770}); // ldr r4,[r1,#0] ; bx lr + ASSERT_TRUE(cpu_->set_pc(kMainCode).has_value()); + ASSERT_TRUE(cpu_->set_register_value(1, 0x40013804u).has_value()); // r1 = DR + + // NVIC: enable USART1 (IRQ37 → ISER1 bit5), priority 0xE0 (IPR9 byte1). + ASSERT_TRUE(bus_.write(0xE000E104, 1u << 5, Width::Word).has_value()); + ASSERT_TRUE(bus_.write(0xE000E424, 0xE0u << 8, Width::Word).has_value()); + + // USART1 CR1: UE(13) + RXNEIE(5) + RE(2). + ASSERT_TRUE( + bus_.write(0x4001380C, (1u << 13) | (1u << 5) | (1u << 2), Width::Word) + .has_value()); + + usart1_.inject_rx('A'); // → RXNE + raise IRQ37 + + ASSERT_TRUE(cpu_->step().has_value()); // pending → enter handler + EXPECT_TRUE(cpu_->in_handler_mode()); + ASSERT_TRUE(cpu_->step().has_value()); // ldr r0,[r1] → r0='A', RXNE cleared + ASSERT_TRUE(cpu_->step().has_value()); // bx lr → return + EXPECT_FALSE(cpu_->in_handler_mode()); + auto r4 = cpu_->register_value(4); + ASSERT_TRUE(r4.has_value()); + EXPECT_EQ(*r4, static_cast('A')); +} diff --git a/test/test_stm32f1_periph.cpp b/test/test_stm32f1_periph.cpp index 2b8eef3..6936c6a 100644 --- a/test/test_stm32f1_periph.cpp +++ b/test/test_stm32f1_periph.cpp @@ -279,6 +279,42 @@ TEST(UsartTest, MmioThroughBus) { EXPECT_EQ(captured, 'A'); } +TEST(UsartTest, InjectRxSetsRxne) { + Stm32f1Usart usart; + usart.inject_rx('X'); + auto sr = usart.read(0x00, Width::Word); + ASSERT_TRUE(sr.has_value()); + EXPECT_TRUE(*sr & (1u << 5)); // RXNE +} + +TEST(UsartTest, DrReadReturnsByteAndClearsRxne) { + Stm32f1Usart usart; + usart.inject_rx('Q'); + auto dr = usart.read(0x04, Width::Word); + ASSERT_TRUE(dr.has_value()); + EXPECT_EQ(*dr, static_cast('Q')); + auto sr = usart.read(0x00, Width::Word); + ASSERT_TRUE(sr.has_value()); + EXPECT_FALSE(*sr & (1u << 5)); // RXNE cleared +} + +TEST(UsartTest, RxneIrqRaisedWhenEnabled) { + Stm32f1Usart usart; + ASSERT_TRUE(usart.write(0x0C, 1u << 5, Width::Word).has_value()); // CR1 RXNEIE + bool fired = false; + usart.set_irq_callback([&] { fired = true; }); + usart.inject_rx('A'); + EXPECT_TRUE(fired); +} + +TEST(UsartTest, RxneIrqNotRaisedWhenDisabled) { + Stm32f1Usart usart; + bool fired = false; + usart.set_irq_callback([&] { fired = true; }); + usart.inject_rx('A'); // RXNEIE not set + EXPECT_FALSE(fired); +} + // ── Timer Tests ── TEST(TimerTest, TickIncrements) {