本文由 AI 辅助生成并经自动审查;尚未完成真实硬件验证。涉及具体芯片、时序和电气行为时,请以文末第一方资料和实际测试为准。
第 33 课:寄存器视图——有时序、可证伪的观测证据 #
寄存器视图看起来像一张外设“体检表”,但它不是硬件状态的连续录像。窗口中的每个值,都是调试器在某个时刻通过调试访问读到的样本;字段可能正在变化,读取本身可能改变状态,而暂停 CPU 也可能改变外设之间原有的时间关系。
本课只推进一个新核心概念:寄存器视图的取样语义与证据边界。先用 C11 Host Fake(主机替身)把“何时读、读什么、读取是否有副作用、能推出什么”变成可测试契约,再落到 STM32F407xx 的 RCC(Reset and Clock Control,复位与时钟控制)、TIM2 和可选 ADC1-DMA2 观察链。Host Fake 只验证观察规则,不能证明真实地址、真实时钟、调试器行为或外部信号。
参考实现以 RM0090 Rev 22、DS8626 Rev 12 和 UM1725 Rev 8 为边界。[S1][S2][S3] 当前没有完整订货料号、开发板、调试器、板级时钟、目标工程、构建输出、烧录记录或测量结果。STM32 代码均为参考实现,待目标工程编译和上板验证;本课 hardware_verified=false。
1. 本节目标 #
核心目标与本次 plan.json 一致:建立寄存器视图作为有时序、可证伪的观测证据这一心智模型,并据此为 STM32F407xx 的时钟、定时器、DMA 与外设状态设计可重复的调试观察流程。
新核心概念与计划一致:寄存器视图的取样语义与证据边界。
完成本课后,你应能:
- 把“看一下寄存器”改写成带对象、预期、时机、副作用和交叉证据的观察规格;
- 区分静态配置、运行时状态和外部效果,不让一次快照越级证明连续行为;
- 解释观察规格、寄存器快照、语义注释和交叉证据由谁定义、持有和注入;
- 用 Host Fake 复现易变字段、读取即清除字段、暂停与恢复以及链路分段失败;
- 按时钟/复位、TIM2、可选 ADC/DMA、IRQ 回调的顺序观察 STM32F407xx;
- 说明 HAL(Hardware Abstraction Layer,硬件抽象层)意图如何对应 LL(Low-Layer,低层驱动)操作、寄存器字段和硬件事件;
- 迁移到 F7、H5 或 H7 时保留观察契约,同时重新核对地址、字段、时钟域、缓存、安全属性和调试冻结行为。
2. 它解决什么问题 #
2.1 一栏数值不是一段历史 #
假设调试器显示 TIM2_CNT=357。它最多直接支持这一句:
在本次调试访问完成时,工具把 TIM2 的计数器字段解释为 357。
它不能单独证明:
- TIM2 一直以预期频率计数;
- 357 是暂停前还是暂停后得到的值;
- 更新事件按固定周期发生;
- ADC 收到了每个触发;
- DMA 没有丢数据;
- 外部引脚上出现了预期波形。
这些结论跨越了时间或跨越了模块,需要相邻样本、事件计数、缓冲区内容或外部测量等独立证据。
2.2 观察会进入被观察系统 #
调试器通常通过 SWD/JTAG 和片上调试访问端口读取地址空间。处理器暂停不等于所有外设都停止;在 F407 上,TIM2 在内核暂停时是否冻结受调试冻结配置控制。[S1] 如果 TIM2 被冻住而 DMA、ADC 或另一时钟域的行为不同,原来的相对时序已经改变。
另一类风险是读副作用(read side effect,读取副作用):某些状态或数据寄存器在读取后会清标志、弹出数据或推进硬件状态。不能把 IDE 的自动刷新当作无害操作。对本课明确核对的 F407 字段:
TIM2_CNT是运行中易变值;两次读取可以不同;TIM2_SR.UIF由更新事件置位,软件写 0 清除;只读SR不应被写成“已确认并清除”;[S1]DMA2_Stream0->NDTR在传输中递减,循环模式重装后数值可跳回初值;单次NDTR=64不能证明此前每项都正确;[S1]- DMA 状态从
LISR/HISR读取,清除通过LIFCR/HIFCR的对应清除位完成;观察窗口不得为了刷新而写清除寄存器;[S1] - ADC 数据寄存器、通信外设数据寄存器和清除序列必须逐项查目标参考手册;本课不对未纳入观察表的字段猜测。
2.3 本课的证据边界 #
把证据分成三层:
| 层次 | 典型证据 | 可以支持 | 不能单独支持 |
|---|---|---|---|
| 静态配置 | RCC 门控、PSC、ARR、DMA 地址与模式 | 初始化意图已写入相应字段 | 外设正在运行、频率正确、数据有效 |
| 运行状态 | CEN、相邻 CNT、UIF、NDTR、IRQ/回调计数 | 指定取样条件下发生了状态变化 | 外部信号质量、所有事件都未丢失 |
| 外部效果 | 示波器/逻辑分析仪、接收端数据、已校准测量 | 引脚或链路出现了可观测效果 | 内部每个配置字段都正确 |
寄存器快照很适合证伪。例如 RCC_APB1ENR.TIM2EN=0 足以否定“TIM2 已具备运行前提”;但 TIM2EN=1 只说明门控已打开,不足以证明计数器正在前进。
3. 必要前置知识 #
以下内容已在 learning-state.toml.completed 中记录,可直接使用:
clock-tree:振荡源、PLL、AHB/APB、门控、复位和最终外设时钟;hal-ll-register-crosswalk:HAL 调用表达软件意图,寄存器读回表达硬件可见状态;timer-trigger-routing:TIM2 update/TRGO 到 ADC 的触发链;adc-timer-dma-pipeline:ADC 请求、DMA stream、NDTR、HT/TC/error 和缓冲区边界;peripheral-integration-lab:回调只发布事件,资源拥有者在延后路径停止和恢复。
断点/观察点、故障分析、逻辑分析仪、缓存一致性和 trace(跟踪)尚未作为课程完成,不是本课隐含前置。本课只说明它们为何能成为后续交叉证据,不展开其完整机制。
4. 核心原理 #
4.1 一个样本的完整含义 #
一个可审计样本不是裸 uint32_t value,而是六元组:
sample = {
object, // 哪个器件、外设、实例和字段
value, // 本次读回值
moment, // 启动前、运行中、暂停后、IRQ 入口等
observer, // IDE 自动刷新、脚本、固件只读快照等
semantics, // 静态、易变、读取清除、写清除、数据端口……
conditions // CPU/外设是否暂停,时钟、错误和工具版本
}如果缺少 moment,相邻两次值无法排序;如果缺少 semantics,不知道读取是否破坏现场;如果缺少 conditions,暂停后的值可能被误当作自由运行证据。
4.2 四个对象、持有者和调用流 #
| 对象 | 解决的问题 | 在哪里定义、谁持有 | 谁注入 | 调用/证据如何流动 |
|---|---|---|---|---|
| 观察点规格 | 把模糊查看变成可证伪问题 | 实验文件中的静态常量表;实验作者持有 | 作者填入对象、预期、时机、语义、来源、交叉证据 | 观察者逐项执行并给出通过/失败/不充分 |
| 寄存器快照 | 固定一次读取结果和次序 | 调试工具导出或固件静态结构;实验记录持有 | 调试访问或固件读取函数注入样本 | 硬件状态 -> 总线读取 -> 快照 -> 比较预期 |
| 语义注释 | 限制字段的合法读法和结论 | 观察规格旁的来源核对表;课程/项目维护者持有 | RM、数据手册和驱动头文件提供定义 | 字段语义 -> 决定能否重复读、如何清除、允许何种结论 |
| 交叉证据 | 防止一次样本推出过强结论 | 实验记录中的第二观察路径 | 回调计数、相邻快照、缓冲内容或外部测量注入 | 第一证据提出假设 -> 第二证据支持或否定 |
控制流是“进入规定时机 -> 采样 -> 记录 -> 恢复运行”;数据流是“字段值进入快照”;事件流是“更新、DMA 完成或错误使字段和软件计数发生变化”。三条流必须分别记录。
4.3 观察规格的最小判定规则 #
一个观察点只有同时具备以下内容,才允许标记为“满足预期”:
- 对象唯一:至少包含器件系列、外设实例和字段;
- 预期可证伪:如“启动前
CEN=0”,而不是“定时器正常”; - 取样时机明确:如
HAL_TIM_Base_Start_IT返回后、第一次回调前; - 语义已核对:静态、易变、读清除、写清除或未知;
- 暂停影响已记录:CPU、TIM2 和数据链是否会继续推进;
- 来源可定位:至少指出手册和章节/表;
- 结论不越界:配置证据只回答配置,运行证据只回答本次运行;
- 需要时有交叉证据:凡是关于时间、频率、完整性或外部效果的结论都需要第二路径。
“未知”不是失败,而是合法结果。它要求停止推断并补来源或换观察方法。
4.4 SVD 的角色 #
SVD(System View Description,系统视图描述)把外设地址、寄存器和位域名称提供给工具,使原始地址能显示成 TIM2.CR1.CEN。SVD 提高可读性,但不是硬件行为规范:
- 工具载入了错误器件的 SVD,名称和地址可能错位;
- SVD 未必完整表达读取副作用、清除序列或保留位约束;
- 同名字段在不同系列上不保证地址、宽度和行为相同;
- 最终语义以目标器件参考手册、数据手册和对应 CMSIS 头文件为核对入口。
因此第一步总是记录完整器件和工具所用设备描述,不能用“窗口里有这个名字”替代目标确认。
5. 关键术语与直观模型 #
| 术语 | 准确含义 | 本课直观模型 |
|---|---|---|
| SFR | Special Function Register,特殊功能寄存器;映射到处理器地址空间的硬件控制/状态接口 | 一扇有规则的窗口,不是普通内存变量 |
| 快照 | 一次或一组有序读取保存下来的状态样本 | 一张带时间和拍摄条件的照片 |
| 易变字段 | 硬件、DMA 或其他执行主体可异步改变的字段 | 读完就可能过期的数值 |
| 读副作用 | 一次读取同时改变标志、FIFO 或后续状态 | 拿走票据时票据从队列消失 |
| 调试暂停 | 调试器请求内核停止执行指令 | 按下暂停键,但并不保证所有外设同时定格 |
| DWT | Data Watchpoint and Trace,数据观察点与跟踪单元 | 后续用于观察访问或计时的内核调试资源,本课不展开配置 |
| CoreSight | Arm 的片上调试与跟踪架构 | 调试访问、断点和跟踪资源的总框架 |
| 交叉证据 | 与第一观察路径相对独立的证据 | 用第二个角度检查同一假设 |
6. 从输入到结果的完整流程 #
6.1 先写假设,再打开窗口 #
以“TIM2 是否按预期启动”为例:
假设 H0:TIM2 没有运行
预期链:
RCC 门控已开
-> TIM2 PSC/ARR 与设计一致
-> 启动前 CEN=0
-> 启动后 CEN=1
-> 自由运行条件下两个有序 CNT 样本不同
-> update 事件使 UIF 或回调计数发生变化
-> 由已知 TIM2CLK、PSC、ARR 推导的周期与独立测量相符其中任一前置失败,都能定位“首次不满足点”。最后一项涉及连续时间,必须由定时记录、DWT/trace 或外部测量交叉验证;单次 CNT/UIF 不够。
6.2 推荐观察顺序 #
- 记录完整 MCU、手册版本、Cube 固件包、IDE/调试服务器和 SVD 选择;
- 在源尚未启动时观察时钟、复位和静态配置;
- 明确冻结策略,再启动源;
- 以有限次数采样易变字段,不让自动刷新无界读取;
- 对每个运行结论加入回调计数或第二快照;
- 扩展到 ADC/DMA 时按“源 -> 触发 -> 请求 -> 搬运 -> 回调”逐段观察;
- 错误发生时先保存只读快照和 HAL error code,再由资源拥有者停源、清理;
- 对频率、信号和数据内容使用适当的外部或端到端证据。
6.3 何时停止读取 #
出现下列任一情况,应停止自动刷新并先查资料:
- 字段被标为 data、FIFO、pop、clear-on-read 或需要组合清除序列;
- 读取顺序本身属于中断处理协议;
- 多个窗口、脚本和固件都可能读取同一清除型状态;
- 不知道暂停时该外设是否冻结;
- SVD 名称与参考手册或 CMSIS 头文件不一致。
7. 嵌入式系统中的对应位置 #
7.1 外设能力地图 #
| 层级 | 能力 | 工作原理与证据链 | 本课边界/后续落点 |
|---|---|---|---|
| 基础 | 查看 RCC、TIM、DMA 的配置和状态 | 调试访问读取映射寄存器,SVD 将地址显示为字段名 | 本课建立观察规格与结论边界 |
| 进阶 | 用分段证据定位链路首次失败点 | 时钟 -> 源 -> 触发 -> DMA -> IRQ/回调 逐段读取并交叉比较 |
本课用 TIM2 和可选 ADC-DMA 演示 |
| 高级 | 非侵入 trace、访问观察点、冻结矩阵和副作用审计 | CoreSight 事件/数据路径采集执行或总线活动,减少反复暂停与轮询;仍需时间戳和溢出边界 | breakpoint-watchpoint、fault-analysis、性能与 trace 专题 |
| 跨外设联动 | 观察 RCC -> TIM2 TRGO -> ADC1 -> DMA2 -> IRQ |
TIM2 update 产生内部触发,ADC 完成产生 DMA 请求,stream 更新内存和标志,IRQ 进入 HAL 回调 | 本课只训练观察顺序,不重讲采集所有权 |
| 系列差异 | 保留契约,重查实现 | 观察规格可迁移;地址、DMA 架构、缓存/域、安全授权和冻结位不可照抄 | 选定 F7/H5/H7 完整料号后查其第一方 RM/DS/Cube 包 |
高级能力不是“更大的寄存器窗口”。它解决的是暂停和轮询过度扰动实时系统的问题:硬件事件先进入 CoreSight 或目标跟踪资源,再由探针输出带顺序的信息。触发、数据、缓冲与丢失条件仍必须定义;否则 trace 也只是另一堆无上下文数值。后续 breakpoint-watchpoint 负责访问触发,fault-analysis 负责异常现场,logic-analyzer 负责外部时序,缓存一致性课程负责 CPU 与 DMA 可见性。
F4/F7/H5/H7 迁移审计表 #
| 目标 | 可保留 | 必须用目标资料重新核对 |
|---|---|---|
| F4/F407xx | 对象、时机、语义、交叉证据和分段顺序 | 完整料号、SVD、时钟源、DBGMCU 冻结位、DMA stream/channel 和具体字段 |
| F7 | 同一观察契约 | 缓存参与后的可见性、总线访问路径、DMA 映射、冻结行为和对应 CubeF7 API;本课不写具体值 |
| H5 | 同一观察契约 | 安全/非安全调试授权、目标 DMA 与请求路由、时钟选择、寄存器安全属性和 CubeH5 API;本课不写具体值 |
| H7 | 同一观察契约 | 多时钟/电源域、缓存、目标内存可达性、DMA 架构、冻结矩阵和 CubeH7 API;本课不写具体值 |
表中后 3 行是迁移检查任务,不是器件配置结论。选定完整目标并加入对应第一方来源前,不复制 F407 地址、位值、stream/channel 或工具设置。
7.2 参考目标与时钟/复位链 #
参考边界 #
- 器件系列:STM32F407xx;完整订货料号和封装待填;
- 参考手册:RM0090 Rev 22;数据手册:DS8626 Rev 12;HAL/LL 文档:UM1725 Rev 8;[S1][S2][S3]
- 板级未知:不声明 HSE、LSE、引脚、外部电路、调试连接或电源条件;
- 为得到完整可计算示例,时钟源选择片内 HSI;下述 168 MHz/
FLASH_LATENCY_5配置以**已经确认VDD=2.7..3.6 V**为执行前提。未确认供电时只能做静态审计,不得运行该配置;其他电压范围必须按 DS8626 表 15 重新选择最高频率和 Flash wait states(等待周期)。
一条完整、可计算的参考链 #
HSI = 16 MHz
-> PLLM = 16,PLL 输入 = 1 MHz
-> PLLN = 336,VCO = 336 MHz
-> PLLP = 2,SYSCLK = 168 MHz
-> AHB HPRE = /1,HCLK = 168 MHz
-> APB1 PPRE1 = /4,PCLK1 = 42 MHz
-> APB1 预分频不为 1,F407 TIM2CLK = 2 * PCLK1 = 84 MHz
-> RCC_APB1ENR.TIM2EN = 1
-> TIM2 PSC = 8399,计数 tick = 84 MHz / 8400 = 10 kHz
-> TIM2 ARR = 9,update 频率 = 10 kHz / 10 = 1 kHz上述 16 MHz HSI、PLL 约束、总线最大频率和 APB 定时器倍频规则只用于 RM0090/DS8626 覆盖的本参考目标。[S1][S2] FLASH_LATENCY_5 只适用于本例声明的 VDD=2.7..3.6 V:2.4..2.7 V 需要 6 wait states,2.1..2.4 V 需要 7 wait states,1.8..2.1 V 时 Flash 访问最高 160 MHz,不能继续使用本例 168 MHz 配置。[S2] 真实板若改用 HSE,必须先填写晶振频率,再从 PLL 输入重新推导,不能只保留 PLLN=336。
PSC 和 ARR 都按“寄存器值加一”参与本例公式:
f_update = f_TIM2CLK / (PSC + 1) / (ARR + 1)TIM2CLK、PSC、ARR 是配置证据;要证明实际 1 kHz,还需计时或外部测量。
门控、复位与调试冻结 #
RCC_CR.HSION -> 等待 HSIRDY
-> 配置 RCC_PLLCFGR
-> RCC_CR.PLLON -> 等待 PLLRDY
-> 配置 FLASH latency、HPRE、PPRE1、PPRE2
-> RCC_CFGR.SW 选择 PLL -> 检查 SWS
-> RCC_APB1ENR.TIM2EN = 1
-> 如需恢复已知初态:TIM2RST 置 1 后释放为 0
-> 写 TIM2 PSC/ARR/EGR/DIER/CR1
-> CR1.CEN = 1 后才开始计数读取 TIM2 地址不等于 TIM2 已获得内核时钟;应先核对门控。复位位一直保持为 1 时外设不能工作。调试暂停是否冻结 TIM2,由 DBGMCU_APB1_FZ.DBG_TIM2_STOP 决定;观察记录必须写明该位。本课的两种实验要分别测试“冻结 TIM2”和“不冻结 TIM2”,但不把其结果推广到 DMA、ADC 或其他系列。[S1]
7.3 CubeMX、HAL、LL 与寄存器对照 #
CubeMX/CubeMX2 配置清单 #
下列是待在实际工程执行的配置,不是本次已经生成的 .ioc:
- 选择完整 STM32F407 订货料号,记录 CubeMX/CubeMX2 与 STM32CubeF4 版本,并从原理图/电源资料确认
VDD;未确认VDD=2.7..3.6 V时不得套用下述 168 MHz 配置; RCC:使用 HSI,不启用尚未确认存在的外部晶振;Clock Configuration:PLL source=HSI,PLLM=16、PLLN=336、PLLP=2、PLLQ=7,AHB/1、APB1/4、APB2/2;TIM2:Internal Clock,Prescaler8399,Counter Period9,Up counter,Auto-reload preload 关闭;TIM2/NVIC:若使用回调交叉证据,开启 TIM2 global interrupt;优先级必须按实际系统统一设计;SYS/Debug:选择与实际探针连接相符的 Serial Wire 或 JTAG;不能由课程猜引脚是否可用;- 调试冻结:由固件显式调用 freeze/unfreeze 宏,避免依赖某个 IDE 的隐藏默认设置;
- 可选 ADC-DMA 链:沿用已核对的 F407
TIM2 TRGO -> ADC1 -> DMA2 Stream0/Channel0,接收端先武装,最后启动 TIM2;[S1][S3] - 生成后审计
.ioc、SystemClock_Config、MX_TIM2_Init、MSP、IRQ handler 和目标 CMSIS 头文件,不以 GUI 截图替代读回。
实际供电、Flash latency、ADC 通道/引脚、模拟输入、调试器和测量点均为板级待填参数。
HAL 参考实现:时钟、TIM2 和观察快照 #
以下代码属于 STM32 BSP(Board Support Package,板级支持包)实现层。htim2、回调计数和快照对象均为静态生命周期,由 BSP 唯一持有;应用只调用初始化、启动和取样接口。它是参考实现,待目标工程编译和上板验证。
#include "stm32f4xx_hal.h"
#include <stdint.h>
typedef enum {
PROBE_OK = 0,
PROBE_ERR_CLOCK = 1,
PROBE_ERR_TIM_INIT = 2,
PROBE_ERR_TIM_START = 3,
PROBE_ERR_TIM_STOP = 4
} probe_status_t;
typedef struct {
uint32_t sequence;
uint32_t tick_ms;
uint32_t rcc_apb1enr;
uint32_t dbgmcu_apb1_fz;
uint32_t tim_cr1;
uint32_t tim_dier;
uint32_t tim_sr;
uint32_t tim_psc;
uint32_t tim_arr;
uint32_t tim_cnt;
uint32_t update_callback_count;
} probe_tim2_snapshot_t;
static TIM_HandleTypeDef g_htim2;
static volatile uint32_t g_tim2_update_count;
static uint32_t g_snapshot_sequence;
static probe_status_t SystemClock_Config_Reference(void)
{
RCC_OscInitTypeDef osc = {0};
RCC_ClkInitTypeDef clk = {0};
/* 执行前提:已从板级资料确认 VDD=2.7..3.6 V。 */
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
osc.OscillatorType = RCC_OSCILLATORTYPE_HSI;
osc.HSIState = RCC_HSI_ON;
osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSI;
osc.PLL.PLLM = 16U;
osc.PLL.PLLN = 336U;
osc.PLL.PLLP = RCC_PLLP_DIV2;
osc.PLL.PLLQ = 7U;
if (HAL_RCC_OscConfig(&osc) != HAL_OK) {
return PROBE_ERR_CLOCK;
}
clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV4;
clk.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_5) != HAL_OK) {
return PROBE_ERR_CLOCK;
}
return PROBE_OK;
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) {
__HAL_RCC_TIM2_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM2_IRQn, 6U, 0U);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
}
static probe_status_t Probe_TIM2_Init(void)
{
TIM_MasterConfigTypeDef master = {0};
g_htim2.Instance = TIM2;
g_htim2.Init.Prescaler = 8399U;
g_htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
g_htim2.Init.Period = 9U;
g_htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
g_htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&g_htim2) != HAL_OK) {
return PROBE_ERR_TIM_INIT;
}
master.MasterOutputTrigger = TIM_TRGO_UPDATE;
master.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&g_htim2, &master) != HAL_OK) {
return PROBE_ERR_TIM_INIT;
}
return PROBE_OK;
}
static probe_status_t Probe_TIM2_Start(uint8_t freeze_while_halted)
{
g_tim2_update_count = 0U;
if (freeze_while_halted != 0U) {
__HAL_DBGMCU_FREEZE_TIM2();
} else {
__HAL_DBGMCU_UNFREEZE_TIM2();
}
if (HAL_TIM_Base_Start_IT(&g_htim2) != HAL_OK) {
return PROBE_ERR_TIM_START;
}
return PROBE_OK;
}
static probe_status_t Probe_TIM2_Stop(void)
{
if (HAL_TIM_Base_Stop_IT(&g_htim2) != HAL_OK) {
return PROBE_ERR_TIM_STOP;
}
return PROBE_OK;
}
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_htim2);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) {
g_tim2_update_count++;
}
}
static void Probe_TIM2_Sample(probe_tim2_snapshot_t *out)
{
uint32_t primask;
if (out == NULL) {
return;
}
primask = __get_PRIMASK();
__disable_irq();
__DMB();
out->sequence = ++g_snapshot_sequence;
out->tick_ms = HAL_GetTick();
out->rcc_apb1enr = RCC->APB1ENR;
out->dbgmcu_apb1_fz = DBGMCU->APB1FZ;
out->tim_cr1 = TIM2->CR1;
out->tim_dier = TIM2->DIER;
out->tim_sr = TIM2->SR;
out->tim_psc = TIM2->PSC;
out->tim_arr = TIM2->ARR;
out->tim_cnt = TIM2->CNT;
out->update_callback_count = g_tim2_update_count;
__DMB();
if (primask == 0U) {
__enable_irq();
}
}字段与所有权说明:
| 对象/字段 | 含义和生命周期 |
|---|---|
g_htim2 |
BSP 静态持有的 HAL 句柄;从初始化到停止始终有效;不得由应用复制后分别控制同一 TIM2 |
Prescaler/Period |
分别写入 PSC/ARR 的 HAL 配置值;本例得到 10 kHz 计数 tick 和 1 kHz update |
CounterMode |
向上计数;决定 CR1.DIR 意图 |
ClockDivision |
定时器内部滤波/死区相关分频字段,不是 APB 时钟分频;本例不把它用于 update 公式 |
AutoReloadPreload |
ARR 立即或经 update 生效的策略;本例关闭,避免把预装载作为新主题 |
MasterOutputTrigger |
选择 update 事件作为 TRGO;仅为可选 ADC 链提供源,不证明接收端已连接 |
g_tim2_update_count |
IRQ 回调写、观察函数读;只是软件看到的回调次数,不等于无丢失的真实 update 总数 |
Probe_TIM2_Sample |
固件内有序只读快照;短临界区防止回调计数夹在字段读取中间,但不能让全部硬件字段原子冻结 |
__DMB() 约束处理器显式内存访问的顺序,不会把多个外设寄存器变成同一瞬间的原子快照。快照内仍应记录读取顺序,分析时允许前后字段跨过一次硬件事件。
最小调用顺序:
int main(void)
{
probe_tim2_snapshot_t before;
probe_tim2_snapshot_t after;
HAL_Init();
if (SystemClock_Config_Reference() != PROBE_OK) {
Error_Handler();
}
if (Probe_TIM2_Init() != PROBE_OK) {
Error_Handler();
}
Probe_TIM2_Sample(&before); /* 启动前静态配置证据 */
if (Probe_TIM2_Start(1U) != PROBE_OK) {
Error_Handler();
}
/* 实际实验应由明确定义的时机触发,不在忙等待中无限采样。 */
HAL_Delay(10U);
Probe_TIM2_Sample(&after); /* 运行状态候选证据 */
for (;;) {
/* 将 before/after 交给非 ISR 诊断路径;不要在回调中阻塞打印。 */
}
}HAL_Delay(10U) 只用于规划最小观察时机。它依赖 SysTick 和中断可运行,并不能充当精确 10 ms 测量证据。若在断点处暂停,延时与 TIM2 冻结策略也会相互影响。
可选 ADC-DMA 链的 IRQ、回调和错误快照 #
若复用已完成课程中的 F407 TIM2 TRGO -> ADC1 -> DMA2 Stream0/Channel0 初始化,启动顺序必须是:DMA/NVIC -> ADC/DMA 武装 -> 最后启动 TIM2。以下代码只展示本课新增的观察钩子;hadc1、hdma_adc1 由既有生成工程/BSP 定义并持有,本观察模块通过 extern 借用其地址,不得重新初始化或改变所有权;静态缓冲区仍由原 BSP 唯一持有。[S1][S3]
typedef struct {
uint32_t half_count;
uint32_t full_count;
uint32_t error_count;
uint32_t adc_error;
uint32_t dma_error;
uint32_t dma_lisr;
uint32_t dma_s0cr;
uint32_t dma_s0ndtr;
uint32_t dma_s0par;
uint32_t dma_s0m0ar;
} probe_adc_dma_snapshot_t;
#define PROBE_DMA_ERROR_UNAVAILABLE UINT32_MAX
extern ADC_HandleTypeDef hadc1;
extern DMA_HandleTypeDef hdma_adc1;
static volatile uint32_t g_adc_half_count;
static volatile uint32_t g_adc_full_count;
static volatile uint32_t g_adc_error_count;
static volatile uint32_t g_adc_error_code;
static volatile uint32_t g_dma_error_code;
void DMA2_Stream0_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc1);
}
void ADC_IRQHandler(void)
{
HAL_ADC_IRQHandler(&hadc1);
}
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
g_adc_half_count++;
}
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
g_adc_full_count++;
}
}
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
g_adc_error_code = HAL_ADC_GetError(hadc);
g_dma_error_code = (hadc->DMA_Handle != NULL)
? HAL_DMA_GetError(hadc->DMA_Handle)
: PROBE_DMA_ERROR_UNAVAILABLE;
g_adc_error_count++;
}
}
static void Probe_ADC_DMA_Sample(probe_adc_dma_snapshot_t *out)
{
uint32_t primask;
if (out == NULL) {
return;
}
primask = __get_PRIMASK();
__disable_irq();
__DMB();
out->half_count = g_adc_half_count;
out->full_count = g_adc_full_count;
out->error_count = g_adc_error_count;
out->adc_error = g_adc_error_code;
out->dma_error = g_dma_error_code;
out->dma_lisr = DMA2->LISR;
out->dma_s0cr = DMA2_Stream0->CR;
out->dma_s0ndtr = DMA2_Stream0->NDTR;
out->dma_s0par = DMA2_Stream0->PAR;
out->dma_s0m0ar = DMA2_Stream0->M0AR;
__DMB();
if (primask == 0U) {
__enable_irq();
}
}回调只累加计数并锁存错误,不打印、不复位共享 RCC/DMA,也不在 ISR 中重启链路。PROBE_DMA_ERROR_UNAVAILABLE 是课程自定义的“没有可借用 DMA 句柄”哨兵,不是 HAL 错误码;UM1725 中 HAL_DMA_ERROR_NO_XFER 的含义是请求 abort 时没有正在进行的传输,不能挪作此用途。[S3] 错误处理流程是:应用看到 error_count 增加 -> 保存快照 -> 停 TIM2 源 -> HAL_ADC_Stop_DMA -> 确认 stream 停止 -> 分析 ADC/DMA error code 和标志 -> 显式恢复。读取 LISR 不等于清除标志;清除动作属于拥有 DMA 生命周期的恢复路径,而不是观察函数。[S1][S3]
HAL 到 LL/关键寄存器交叉表 #
| 软件意图 | HAL 入口 | LL 入口示例 | F407 关键寄存器/字段 | 观察结论上限 |
|---|---|---|---|---|
| 开 TIM2 门控 | MSP 中 __HAL_RCC_TIM2_CLK_ENABLE |
LL_APB1_GRP1_EnableClock |
RCC_APB1ENR.TIM2EN |
门控已开,不证明计数 |
| 复位 TIM2 | FORCE/RELEASE RESET 宏 | APB1 reset LL 操作 | RCC_APB1RSTR.TIM2RST |
复位请求状态 |
| 配置基准计数 | HAL_TIM_Base_Init |
LL_TIM_SetPrescaler/SetAutoReload |
PSC、ARR、CR1.DIR/ARPE、EGR.UG |
静态配置与装载事件 |
| 选择 update TRGO | HAL_TIMEx_MasterConfigSynchronization |
LL_TIM_SetTriggerOutput |
CR2.MMS |
源选择已配置,不证明 ADC 接收 |
| 启动中断计数 | HAL_TIM_Base_Start_IT |
开 UIE、使能 counter | DIER.UIE、CR1.CEN |
中断和计数已使能,不证明周期准确 |
| 处理 update IRQ | HAL_TIM_IRQHandler -> callback |
检查/清 update flag | SR.UIF、DIER.UIE |
本次事件进入软件路径 |
| 冻结 TIM2 | freeze/unfreeze 宏 | 对应 DBGMCU LL/直接配置 | DBGMCU_APB1_FZ.DBG_TIM2_STOP |
内核暂停时 TIM2 的冻结策略 |
| 武装 ADC DMA | HAL_ADC_Start_DMA |
配 ADC DMA、地址、长度并开 stream | ADC DMA 控制;DMA CR/PAR/M0AR/NDTR |
请求与搬运配置,不证明数据有效 |
| DMA 事件 | HAL_DMA_IRQHandler -> ADC callbacks |
读标志、按规则清除 | LISR 与 LIFCR,NDTR |
本次 HT/TC/error 进入软件路径 |
同一外设同一生命周期只应有一个配置所有者。可以用 LL 宏或寄存器读做解释和只读观察,但不能在 HAL 正在管理句柄时由另一路径随意改配置,否则 HAL 状态、回调和硬件会失配。
8. 主机实验与硬件迁移边界 #
8.1 Host Fake 验证轨道 #
下面是一个可单文件编译的 C11 模型。它使用固定宽度整数、静态对象和固定数组,不使用动态内存。模型故意加入一个“读取即清除”的状态,验证观察者不能无条件重复读取;这只是教学模型,不声称 F407 的任意同名寄存器具有相同副作用。
#include <assert.h>
#include <stdint.h>
#include <string.h>
#define OBS_HAS_SOURCE (UINT32_C(1) << 0)
#define OBS_HAS_MOMENT (UINT32_C(1) << 1)
#define OBS_HAS_SEMANTICS (UINT32_C(1) << 2)
#define OBS_HAS_EXPECTATION (UINT32_C(1) << 3)
#define OBS_HAS_CROSS_CHECK (UINT32_C(1) << 4)
#define OBS_REQUIRED_MASK (OBS_HAS_SOURCE | OBS_HAS_MOMENT | \
OBS_HAS_SEMANTICS | OBS_HAS_EXPECTATION | \
OBS_HAS_CROSS_CHECK)
typedef enum {
FAKE_OK = 0,
FAKE_ERR_CLOCK_OFF = 1,
FAKE_ERR_NOT_CONFIGURED = 2,
FAKE_ERR_BAD_SPEC = 3,
FAKE_ERR_SOURCE_STOPPED = 4,
FAKE_ERR_DMA_NOT_CONFIGURED = 5
} fake_status_t;
typedef enum {
EVIDENCE_STATIC_CONFIG = 0,
EVIDENCE_RUNTIME_STATE = 1,
EVIDENCE_EXTERNAL_EFFECT = 2
} evidence_class_t;
typedef struct {
uint32_t present_fields;
evidence_class_t claim;
} observation_spec_t;
typedef struct {
uint8_t clock_enabled;
uint8_t configured;
uint8_t running;
uint8_t paused;
uint8_t dma_configured;
uint8_t dma_running;
uint32_t tick;
uint32_t psc;
uint32_t arr;
uint32_t cnt;
uint32_t update_flag;
uint32_t read_clear_event;
} fake_peripheral_t;
typedef struct {
uint32_t sequence;
uint32_t tick;
uint8_t clock_enabled;
uint8_t configured;
uint8_t running;
uint8_t paused;
uint8_t dma_configured;
uint8_t dma_running;
uint32_t psc;
uint32_t arr;
uint32_t cnt;
uint32_t update_flag;
uint32_t consumed_event;
} fake_snapshot_t;
typedef struct {
uint32_t next_sequence;
} fake_observer_t;
static void fake_init(fake_peripheral_t *peripheral,
fake_observer_t *observer)
{
memset(peripheral, 0, sizeof(*peripheral));
memset(observer, 0, sizeof(*observer));
}
static fake_status_t fake_configure(fake_peripheral_t *peripheral,
uint32_t psc,
uint32_t arr)
{
if (peripheral->clock_enabled == 0U) {
return FAKE_ERR_CLOCK_OFF;
}
peripheral->psc = psc;
peripheral->arr = arr;
peripheral->cnt = 0U;
peripheral->configured = 1U;
return FAKE_OK;
}
static fake_status_t fake_start(fake_peripheral_t *peripheral)
{
if (peripheral->clock_enabled == 0U) {
return FAKE_ERR_CLOCK_OFF;
}
if (peripheral->configured == 0U) {
return FAKE_ERR_NOT_CONFIGURED;
}
peripheral->running = 1U;
return FAKE_OK;
}
static fake_status_t fake_dma_configure(fake_peripheral_t *peripheral)
{
if (peripheral->running == 0U) {
return FAKE_ERR_SOURCE_STOPPED;
}
peripheral->dma_configured = 1U;
return FAKE_OK;
}
static fake_status_t fake_dma_start(fake_peripheral_t *peripheral)
{
if (peripheral->running == 0U) {
return FAKE_ERR_SOURCE_STOPPED;
}
if (peripheral->dma_configured == 0U) {
return FAKE_ERR_DMA_NOT_CONFIGURED;
}
peripheral->dma_running = 1U;
return FAKE_OK;
}
static void fake_advance(fake_peripheral_t *peripheral, uint32_t ticks)
{
uint32_t index;
for (index = 0U; index < ticks; ++index) {
peripheral->tick++;
if ((peripheral->clock_enabled != 0U) &&
(peripheral->running != 0U) &&
(peripheral->paused == 0U)) {
if (peripheral->cnt >= peripheral->arr) {
peripheral->cnt = 0U;
peripheral->update_flag = 1U;
peripheral->read_clear_event = 1U;
} else {
peripheral->cnt++;
}
}
}
}
static fake_snapshot_t fake_sample(fake_peripheral_t *peripheral,
fake_observer_t *observer)
{
fake_snapshot_t sample;
observer->next_sequence++;
sample.sequence = observer->next_sequence;
sample.tick = peripheral->tick;
sample.clock_enabled = peripheral->clock_enabled;
sample.configured = peripheral->configured;
sample.running = peripheral->running;
sample.paused = peripheral->paused;
sample.dma_configured = peripheral->dma_configured;
sample.dma_running = peripheral->dma_running;
sample.psc = peripheral->psc;
sample.arr = peripheral->arr;
sample.cnt = peripheral->cnt;
sample.update_flag = peripheral->update_flag; /* 普通只读模型字段 */
sample.consumed_event = peripheral->read_clear_event;
peripheral->read_clear_event = 0U; /* 明确建模读副作用 */
return sample;
}
static fake_status_t observation_validate(const observation_spec_t *spec)
{
if ((spec->present_fields & OBS_REQUIRED_MASK) != OBS_REQUIRED_MASK) {
return FAKE_ERR_BAD_SPEC;
}
return FAKE_OK;
}
static uint8_t claim_is_supported(const observation_spec_t *spec,
const fake_snapshot_t *first,
const fake_snapshot_t *second)
{
if (observation_validate(spec) != FAKE_OK) {
return 0U;
}
if (spec->claim == EVIDENCE_STATIC_CONFIG) {
return (uint8_t)((first->clock_enabled != 0U) &&
(first->configured != 0U));
}
if (spec->claim == EVIDENCE_RUNTIME_STATE) {
return (uint8_t)((second->sequence > first->sequence) &&
(second->tick > first->tick) &&
((second->cnt != first->cnt) ||
(second->update_flag != first->update_flag)));
}
/* Host Fake 永远不能证明外部电平或真实硬件效果。 */
return 0U;
}
static void test_clock_gate_and_static_boundary(void)
{
fake_peripheral_t peripheral;
fake_observer_t observer;
fake_snapshot_t first;
fake_snapshot_t second;
observation_spec_t static_spec = {
OBS_REQUIRED_MASK, EVIDENCE_STATIC_CONFIG
};
observation_spec_t runtime_spec = {
OBS_REQUIRED_MASK, EVIDENCE_RUNTIME_STATE
};
fake_init(&peripheral, &observer);
assert(fake_configure(&peripheral, 3U, 9U) == FAKE_ERR_CLOCK_OFF);
peripheral.clock_enabled = 1U;
assert(fake_configure(&peripheral, 3U, 9U) == FAKE_OK);
first = fake_sample(&peripheral, &observer);
second = first;
assert(claim_is_supported(&static_spec, &first, &second) == 1U);
assert(claim_is_supported(&runtime_spec, &first, &second) == 0U);
}
static void test_ordered_runtime_samples(void)
{
fake_peripheral_t peripheral;
fake_observer_t observer;
fake_snapshot_t first;
fake_snapshot_t second;
observation_spec_t spec = {
OBS_REQUIRED_MASK, EVIDENCE_RUNTIME_STATE
};
fake_init(&peripheral, &observer);
peripheral.clock_enabled = 1U;
assert(fake_configure(&peripheral, 0U, 9U) == FAKE_OK);
assert(fake_start(&peripheral) == FAKE_OK);
first = fake_sample(&peripheral, &observer);
fake_advance(&peripheral, 3U);
second = fake_sample(&peripheral, &observer);
assert(second.cnt == 3U);
assert(claim_is_supported(&spec, &first, &second) == 1U);
}
static void test_read_side_effect(void)
{
fake_peripheral_t peripheral;
fake_observer_t observer;
fake_snapshot_t first;
fake_snapshot_t second;
fake_init(&peripheral, &observer);
peripheral.clock_enabled = 1U;
assert(fake_configure(&peripheral, 0U, 1U) == FAKE_OK);
assert(fake_start(&peripheral) == FAKE_OK);
fake_advance(&peripheral, 2U);
first = fake_sample(&peripheral, &observer);
second = fake_sample(&peripheral, &observer);
assert(first.consumed_event == 1U);
assert(second.consumed_event == 0U);
assert(first.update_flag == 1U);
assert(second.update_flag == 1U);
}
static void test_pause_and_resume_policy(void)
{
fake_peripheral_t peripheral;
fake_observer_t observer;
uint32_t paused_cnt;
fake_init(&peripheral, &observer);
peripheral.clock_enabled = 1U;
assert(fake_configure(&peripheral, 0U, 9U) == FAKE_OK);
assert(fake_start(&peripheral) == FAKE_OK);
fake_advance(&peripheral, 2U);
peripheral.paused = 1U;
paused_cnt = peripheral.cnt;
fake_advance(&peripheral, 5U);
assert(peripheral.cnt == paused_cnt);
peripheral.paused = 0U;
fake_advance(&peripheral, 1U);
assert(peripheral.cnt != paused_cnt);
}
static void test_segmented_failure_and_bad_spec(void)
{
fake_peripheral_t peripheral;
fake_observer_t observer;
fake_snapshot_t first;
fake_snapshot_t second;
observation_spec_t incomplete = {
OBS_HAS_SOURCE | OBS_HAS_MOMENT, EVIDENCE_RUNTIME_STATE
};
observation_spec_t external = {
OBS_REQUIRED_MASK, EVIDENCE_EXTERNAL_EFFECT
};
fake_init(&peripheral, &observer);
assert(fake_start(&peripheral) == FAKE_ERR_CLOCK_OFF);
assert(fake_dma_configure(&peripheral) == FAKE_ERR_SOURCE_STOPPED);
assert(fake_dma_start(&peripheral) == FAKE_ERR_SOURCE_STOPPED);
peripheral.clock_enabled = 1U;
assert(fake_start(&peripheral) == FAKE_ERR_NOT_CONFIGURED);
assert(fake_configure(&peripheral, 0U, 9U) == FAKE_OK);
assert(fake_dma_start(&peripheral) == FAKE_ERR_SOURCE_STOPPED);
assert(fake_start(&peripheral) == FAKE_OK);
assert(fake_dma_start(&peripheral) == FAKE_ERR_DMA_NOT_CONFIGURED);
assert(fake_dma_configure(&peripheral) == FAKE_OK);
assert(fake_dma_start(&peripheral) == FAKE_OK);
first = fake_sample(&peripheral, &observer);
fake_advance(&peripheral, 5U);
second = fake_sample(&peripheral, &observer);
assert(second.dma_configured == 1U);
assert(second.dma_running == 1U);
assert(observation_validate(&incomplete) == FAKE_ERR_BAD_SPEC);
assert(claim_is_supported(&external, &first, &second) == 0U);
}
int main(void)
{
test_clock_gate_and_static_boundary();
test_ordered_runtime_samples();
test_read_side_effect();
test_pause_and_resume_policy();
test_segmented_failure_and_bad_spec();
return 0;
}测试覆盖:时钟未开、静态配置不能证明运行、相邻快照可变、读取副作用、暂停/恢复策略、TIM 源未运行时拒绝 DMA 配置/启动、DMA 未配置时拒绝启动、不完整规格以及 Host Fake 不能证明外部效果。DMA 阶段只建模观察链的前置条件,不搬运数据;fake_advance 的暂停规则是模型策略,不是 F407 冻结矩阵的替代品。
8.2 STM32 参考实现轨道 #
目标实验必须补齐这些输入:
| 输入 | 当前状态 | 实施时要求 |
|---|---|---|
| 完整 MCU/封装 | 待用户提供 | 对照 DS8626,确认器件和调试端口 |
| 开发板/原理图 | 待用户提供 | 确认供电、NRST、SWD/JTAG、晶振和测量点 |
| ST-LINK/等效探针 | 待用户提供 | 记录型号、固件、速度和调试服务器版本 |
| CubeMX/CubeMX2、CubeF4、工具链 | 待用户提供 | 记录版本并保存 .ioc 和构建日志 |
| 外部测量 | 待用户提供 | 频率或波形结论使用合适探针交叉验证 |
参考步骤:
- 按第 7.3 节生成目标工程并审计差异;
- 构建失败时先修正 API/包版本,不把课程代码视为已编译;
- 连接前确认目标电压、调试接口和完整料号;
- 启动前保存 RCC/TIM2 静态快照;
- 分别用 freeze 和 unfreeze 策略运行有限采样;
- 记录两个有序
CNT样本、UIF、回调计数及暂停条件; - 可选链再记录 ADC/DMA 配置、
NDTR、标志和回调计数; - 最后才用外部测量回答实际频率或信号问题。
本课没有执行这些目标步骤,不能声称目标构建、连接、读回、烧录或测量成功。
9. 最小代码示例 #
最小使用原则不是“不断刷新整个寄存器树”,而是只读解决当前假设所需的字段:
probe_tim2_snapshot_t before;
probe_tim2_snapshot_t after;
Probe_TIM2_Sample(&before); /* CEN 应为 0;核对门控、PSC、ARR */
if (Probe_TIM2_Start(1U) != PROBE_OK) {
Error_Handler();
}
HAL_Delay(10U);
Probe_TIM2_Sample(&after); /* CEN、CNT/UIF、回调计数形成候选运行证据 */判定时逐句写:
门控已开:after.rcc_apb1enr 的 TIM2EN 位为 1。
静态配置一致:PSC=8399、ARR=9。
软件请求运行:CR1.CEN 为 1。
发生变化:after 的 CNT/UIF/回调计数至少一项相对 before 改变。
仍未证明:实际 update 频率为 1 kHz;需要独立计时或外部测量。10. 常见错误 #
- 先看数值,再编故事:没有预期和失败分支,任何值都能被事后解释。
- 把一次 CNT 当频率:频率是单位时间内的变化,需要时间基准和至少两个有序样本。
- 把门控打开当作外设运行:还缺复位释放、配置、使能和事件链。
- 把 CEN=1 当作 update 已发生:CEN 只表达计数器使能状态;还需 CNT/UIF/回调等运行证据。
- 无限自动刷新:可能读走清除型事件、改变 FIFO 或增加总线扰动。
- 忽略暂停策略:冻结 TIM2 后得到的 CNT 不代表自由运行;不冻结时单步期间它又可能跨过多个周期。
- 把 SVD 当参考手册:SVD 提供展示元数据,行为语义仍查 RM/DS/CMSIS 头文件。
- 清标志后才保存现场:恢复动作覆盖了根因证据;应先做无副作用快照。
- 在回调中打印整个寄存器树:阻塞输出改变中断时序,也可能导致新错误。
- 多个读取者争同一清除型状态:IDE、脚本和 ISR 中只有第一个读取者可能看到事件。
- 认为短临界区产生原子硬件快照:它只阻止 CPU 中断插入,不会冻结异步外设。
- 用 Host Fake 数字冒充 F407 读回:Fake 只证明契约和测试逻辑。
- 复制 F407 地址到 F7/H5/H7:迁移时必须选定器件并重新核对。
- 错误回调中复位整个 DMA/RCC:共享资源应由唯一拥有者在延后路径清理。
- 把调试窗口里的十六进制截图当完整证据:缺少工具版本、时机和交叉证据时不可复查。
11. 调试观察点 #
下面的表可以直接作为目标实验记录骨架:
| 顺序 | 对象 | 预期 | 时机 | 语义/风险 | 失败后下一步 | 交叉证据 |
|---|---|---|---|---|---|---|
| 1 | RCC_CFGR.SWS/PPRE1 |
PLL 为系统源、APB1 /4 |
时钟配置返回后 | 静态配置 | 回到时钟初始化返回值与 ready 状态 | HAL_RCC_GetClockConfig、时钟推导表 |
| 2 | RCC_APB1ENR.TIM2EN |
1 | TIM2 init 后 | 静态门控 | 检查 MSP 是否执行、句柄实例是否 TIM2 | HAL_TIM_Base_Init 返回值 |
| 3 | TIM2_PSC/ARR |
8399/9 | 启动前 | 静态配置;注意装载时机 | 检查生成代码、EGR/ARPE 策略 | Cube 配置与公式 |
| 4 | DBGMCU_APB1_FZ.DBG_TIM2_STOP |
与实验策略一致 | 每次暂停前 | 改变暂停行为 | 显式调用 freeze/unfreeze | 暂停前后的有序 CNT 样本 |
| 5 | TIM2_CR1.CEN |
启动前 0、启动后 1 | 两个已定义时机 | 运行控制位 | 检查 start 返回值和错误路径 | CNT/UIF/回调计数 |
| 6 | TIM2_CNT |
有序样本按策略变化或冻结 | 有限两次采样 | 易变,不代表频率 | 检查门控、CEN、冻结位 | tick/trace/外部测量 |
| 7 | TIM2_SR.UIF |
update 后可置位 | IRQ 前或禁用 UIE 的受控实验 | 软件写 0 清除;IRQ 可改变现场 | 核对读取者与清除者 | 回调计数 |
| 8 | DMA CR/PAR/M0AR/NDTR |
与已核对链一致 | 接收端武装后、源启动前 | NDTR 运行中易变 | 先查 stream 使能和请求映射 | HT/TC/error 计数、缓冲区 |
| 9 | DMA LISR |
事件对应标志 | IRQ 入口前的受控条件 | 由 LIFCR 清除,不在观察函数写 | 检查 IRQ handler 和清除顺序 | HAL/DMA error code |
| 10 | 外部效果 | 与设计周期/内容一致 | 稳态窗口 | 不由寄存器视图提供 | 选择合适测量工具 | 内部配置与事件证据 |
使用 IDE 窗口时关闭不需要的外设树自动展开;对副作用未知的字段改用手册核对后的有限脚本或固件快照。具体 IDE 的刷新顺序和是否缓存显示值取决于工具版本,本课未指定工具,必须在实验记录中补充。
12. 实战实验 #
实验 A:Host Fake 观察契约 #
- 将第 8.1 节保存为临时
register_view_fake.c; - 用本机 C11 编译器开启严格警告构建并运行;
- 删除
OBS_HAS_SEMANTICS,确认规格被拒绝; - 把外部效果声明改为运行状态,解释为何不能借此让 Fake 证明真实引脚;
- 保存命令、编译器版本和退出码,作为你自己的主机实验记录。
本次 Reviewer 已从正文机械提取该代码块,使用 GCC 16.2.0 和 -std=c11 -Wall -Wextra -Werror -pedantic 编译并运行;编译与执行退出码均为 0,记录见本次运行目录 review.md。这只证明当前 Host Fake 代码及断言,不证明任何 STM32 地址、时钟、调试访问或外部效果。
实验 B:读副作用与读取者所有权 #
- 在
fake_sample前后读取read_clear_event; - 增加第二个模拟观察者,依次采样;
- 证明第一个观察者消费后,第二个看不到同一事件;
- 写出真实项目规则:清除型状态只能由哪个层读取,如何把副本发布给其他消费者。
实验 C:F407 CubeMX 静态审计 #
- 选择实际完整料号和工具版本;
- 按第 7.3 节配置 HSI/PLL/TIM2;
- 不连接硬件,先审计
.ioc和生成代码; - 对每个 HAL 字段找到 LL/寄存器入口;
- 把板级未知项留空并列出,不虚构引脚或晶振。
实验 D:目标构建与启动前快照 #
- 先从原理图和电源资料确认
VDD;只有VDD=2.7..3.6 V才使用本例 168 MHz/5 wait states,否则先按 DS8626 表 15 降频或修改 latency; - 在实际工程编译参考代码,保存完整构建日志;
- 修正与实际 CubeF4 版本不一致的 API,并记录差异;
- 连接目标后只执行到 TIM2 启动前;
- 保存 RCC、DBGMCU、TIM2 静态快照;
- 若任何配置与预期不符,停止,不进入运行实验。
实验 E:冻结与不冻结的对照 #
freeze_while_halted=1,运行后暂停两次,记录 CNT、UIF、回调计数和每次时机;freeze_while_halted=0,重复同样步骤;- 解释暂停期间哪些证据仍有意义;
- 不把 TIM2 结论推广到 ADC、DMA 或其他系列。
实验 F:ADC-DMA 分段定位 #
- 沿用已核对链,源启动前确认 DMA stream、地址和 NDTR;
- 最后启动 TIM2;
- 按源、ADC、DMA、IRQ/回调顺序找首次不满足点;
- 故障时先快照,后停源,再由拥有者清理;
- 用缓冲区内容或外部输入作为交叉证据,不用 NDTR 单独证明数据正确。
实验 G:迁移审计 #
为一个明确的 F7、H5 或 H7 完整料号建立表格,只填写从对应 RM/DS/Cube 包核对到的内容:设备描述、调试授权、冻结位、时钟域、DMA 架构、缓存/内存可见性和工具设置。未找到第一方出处的单元格写“待核对”。
13. 自检题 #
TIM2_CNT=357最多直接证明什么?为什么不能证明 1 kHz?- 为什么
TIM2EN=1能证伪一部分问题,却不能证明 TIM2 正在计数? - 观察规格缺少取样时机会造成什么歧义?
- SVD 与参考手册分别承担什么角色?
- 为什么禁用中断只能改善软件字段的一致性,不能获得原子硬件快照?
- IDE 自动刷新读了清除型寄存器,IRQ handler 会受到什么影响?
NDTR在两个快照间变小,能够支持什么结论?仍不能支持什么?- 暂停时 TIM2 冻结和不冻结各会怎样影响 CNT 解释?
- Host Fake 的断言全部满足后,为什么
hardware_verified仍然必须为false? - 从 F407 迁移到 H7,观察契约中哪些部分能保留,哪些必须重查?
建议答案要点:每一题都应包含“对象、取样时机、字段语义、允许结论、不能推出的结论、需要的交叉证据”。
14. 面试题 #
14.1 如何证明一个定时器真的以 1 kHz 运行? #
先核对时钟源、PLL、APB 分频、TIM2 门控、PSC/ARR 和 CEN;再取得带可靠时间基准的多个事件或计数样本;最后用输出比较翻转、TRGO 下游事件、trace 或外部仪器交叉测量。单次寄存器截图只能支持局部配置和当次状态。
14.2 为什么调试器会改变 bug 的表现? #
暂停改变 CPU 与外设的相对进度,冻结位可能只停止部分模块,自动刷新可能读取有副作用的状态,单步与打印会改变中断延迟。应记录观察条件并优先使用扰动更小的证据路径。
14.3 如何处理读取即清除的寄存器? #
先查清完整读取/清除协议,指定唯一读取者,由它一次读取并把软件副本分发给其他层;调试窗口关闭该字段的自动刷新。故障现场先按规定顺序保存,再清除和恢复。
14.4 HAL 返回 HAL_OK 为什么不等于外设工作正常?
#
它只说明该函数按当前软件状态完成了配置或启动请求。时钟、硬件状态、事件传播、数据内容和外部效果仍需寄存器读回、回调/错误路径和独立测量证明。
14.5 如何让一次寄存器观察可复现? #
记录目标器件、文档/固件/工具版本、SVD、固件提交、观察规格、执行顺序、暂停/冻结条件、原始值、读取者、错误分支和交叉证据;脚本化有限读取优于手工反复展开窗口。
15. 延伸思考 #
- 如果一次快照的字段按固定顺序读取,如何表示“事件可能发生在字段 4 和字段 5 之间”?
- 如何给观察规格加入允许扰动预算,例如最大暂停时间或最大读取次数?
- 当 trace 缓冲区会溢出时,“没有看到事件”为什么仍不能证明事件没发生?
- 如果 DMA 与 CPU 看到的缓冲区内容不同,寄存器配置正确能否证明数据一致?这将如何连接到缓存一致性课程?
- 如何把本课观察表转成自动化调试脚本,同时避免脚本读取清除型字段?
16. 本节总结 #
寄存器视图不是持续真相,而是带取样条件的状态样本。可靠调试遵守一条固定纪律:
先写可证伪假设
-> 定义对象、时机、读取者和字段语义
-> 从时钟/复位开始分段采样
-> 限制结论在当前证据层内
-> 用第二路径交叉验证时间、完整性和外部效果
-> 先保存现场,再清理和恢复Host Fake 验证这套纪律在时钟未开、易变值、读副作用、暂停和规格不完整时仍能给出确定结果;STM32F407xx 轨道则把它落实到 RCC、TIM2、可选 ADC-DMA、HAL/LL/寄存器和 IRQ/error 流。两条轨道互补,但不能互相冒充证据。
17. 下一步 #
下一课 breakpoint-watchpoint 将研究断点和观察点如何选择触发条件、如何改变执行时序,以及何时应改用 DWT/CoreSight 等更低扰动的观察路径。本课的观察规格将作为其输入,不提前展开断点资源分配或 watchpoint 配置。
18. 参考资料 #
- [S1] STMicroelectronics,STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 advanced Arm-based 32-bit MCUs - Reference manual,RM0090 Rev 22,May 2026。重点:RCC、DMA、TIM2-TIM5 和 debug support;用于核对门控/复位、TIM2 计数与更新标志、DMA NDTR/标志和 TIM2 调试冻结语义。https://www.st.com/resource/en/reference_manual/dm00031020.pdf,访问日期:2026-10-17。
- [S2] STMicroelectronics,STM32F405xx STM32F407xx datasheet,DS8626 Rev 12,March 2026。用于核对器件适用范围、时钟/总线边界和调试接口;不用于虚构未指定板卡的引脚或外部时钟。https://www.st.com/resource/en/datasheet/stm32f407vg.pdf,访问日期:2026-10-17。
- [S3] STMicroelectronics,Description of STM32F4 HAL and low-layer drivers,UM1725 Rev 8,March 2023。重点:RCC、TIM、ADC 和 DMA 的 HAL/LL 初始化、启动、IRQ、callback、状态与错误 API。https://www.st.com/resource/en/user_manual/dm00105879.pdf,访问日期:2026-10-17。
来源的结构化元数据见同目录 lesson.json。以上资料只约束其明确覆盖的 F4 器件和 STM32F4 HAL/LL 接口;F7、H5、H7 必须在选定完整目标后加入各自第一方资料再下具体结论。