本文由 AI 辅助生成并经自动审查;尚未完成真实硬件验证。涉及具体芯片、时序和电气行为时,请以文末第一方资料和实际测试为准。
第 28 课:HAL、LL 与寄存器的交叉映射 #
调用 HAL_ADC_Start_DMA() 返回 HAL_OK,只说明 HAL(hardware abstraction layer,硬件抽象层)接受了这次启动请求;它不等于 ADC 已完成转换,也不等于 DMA 已写入缓冲区,更不等于应用已经消费数据。要判断链路真正停在哪里,必须把同一个操作在三个观察层对齐:HAL 的句柄和状态机、LL(low-layer,低层驱动)的近位域操作,以及芯片寄存器和状态位。
本课只推进一个核心概念:HAL、LL 与寄存器交叉映射。它是把同一硬件状态转换在抽象 API、近寄存器 API、寄存器/标志位三个层次逐项对应,并核对前置条件与可观察结果的方法。它不是三套独立硬件,也不是鼓励在一个运行中的驱动里随意混写 HAL、LL 和寄存器。
课程采用两条轨道:Host Fake(主机替身)先验证调用顺序、前置条件、状态转换和错误路径;STM32 参考轨道再把方法落实到 STM32F407xx 的 ADC1 -> DMA2 Stream0/Channel0。当前没有具体开发板、完整订货料号、板级 ADC 引脚、模拟输入、CubeMX 工程、目标构建日志、烧录记录或寄存器快照。所有 STM32 代码均为“参考实现,待目标工程编译”,没有烧录或测量证据,hardware_verified=false。
1. 本节目标 #
完成本课后,你应能:
- 从一个操作意图建立“前置条件 -> HAL 对象/API -> LL 操作 -> 寄存器/标志 -> 观察证据”的交叉表;
- 区分 API 返回成功、硬件开始运行、硬件置位标志、IRQ 被服务和应用消费完成五个时刻;
- 说明配置对象、HAL 句柄、DMA 缓冲区和诊断快照分别在哪里定义、由谁持有、由谁注入;
- 沿
HAL_ADC_Start_DMA -> ADC 请求 -> DMA 搬运 -> DMA IRQ -> HAL_DMA_IRQHandler -> ADC 回调追踪控制流和数据流; - 在 STM32F407xx 上将 RCC 门控、ADC 公共预分频、DMA stream 配置、ADC/DMA 使能和中断标志对应到 HAL、LL 与 RM0090 寄存器;
- 用证据定位失败属于参数、时钟/复位、ADC、DMA、NVIC/向量、HAL 分发还是应用消费;
- 说明迁移 F7、H5、H7 时哪些映射必须按新目标的第一方资料重建。
核心目标与本次计划一致:建立 HAL、LL 与寄存器交叉映射的追踪方法,能以 STM32F407xx 的 ADC1 -> DMA2 Stream0 参考路径说明一个 HAL 初始化/启动调用如何落到时钟门控、外设/DMA 配置、状态位和 IRQ 回调,并保持 hardware_verified=false 的证据边界。
2. 它解决什么问题 #
2.1 “函数成功”与“工作完成”之间还有一条链 #
一次 ADC-DMA 启动至少有五种不同证据:
HAL_ADC_Start_DMA() 返回 HAL_OK
-> ADC 与 DMA 的相关使能状态已被请求
-> ADC 转换事件产生 DMA 请求
-> DMA 搬运并置位 HT/TC 或错误标志
-> NVIC 接受 IRQ,向量处理函数调用 HAL 分发
-> HAL 调用应用回调,应用随后消费对应缓冲区只观察第一行,无法证明后四行。只看到 DMA TCIF0,也不能证明向量、HAL 分发和应用回调正常。交叉映射解决的是证据断层:每一层都写清“要发生什么、前置条件是什么、成功后能观察什么、失败时下一站查哪里”。
2.2 为什么不能靠背 API 或背寄存器 #
- 只背 HAL:遇到
HAL_OK但缓冲区不变时,不知道应观察 ADC 请求、DMA 计数还是 IRQ。 - 只背寄存器:知道
DMA_S0CR.EN,却可能绕过 HAL 句柄状态、锁和回调连接,制造软件与硬件视图不一致。 - 只看 CubeMX:生成代码展示了一种配置,但不证明目标时钟、电气输入、请求映射和实际运行结果。
- 只看调试器:暂停内核可能改变外设/总线的相对时序;单次快照必须放回事件链解释。
交叉映射的目的不是选出“最好的一层”,而是让设计、实现和调试使用同一条因果链。
2.3 本课边界 #
ADC 采样、DMA 传输契约、中断优先级和时钟树已经完成,本课不把它们重新计为新概念。这里固定一个受控案例来练习追踪法,不展开定时器 TRGO(trigger output,触发输出)、双缓冲、缓存一致性或 RTOS 任务通知。它们分别在 timer-trigger-routing、adc-timer-dma-pipeline、dma-coherency 和后续并发课程深入。
3. 必要前置知识 #
3.1 最小复习 #
- ADC 转换完成后产生数据;在 DMA 模式下,外设请求驱动 DMA 从
ADC_DR搬到内存。 - DMA 的
NDTR是剩余数据项计数;它变化说明请求到达且搬运发生,不说明应用已处理数据。 - 硬件状态标志、外设中断使能、NVIC 使能、向量入口和 HAL 回调是不同关卡。
- RCC(reset and clock control,复位与时钟控制器)门控让外设接口获得时钟;它不决定 ADC 何时产生请求。
- HAL 句柄是软件对象,寄存器是硬件接口;两者相关但不是同一个对象。
3.2 术语 #
| 术语 | 准确含义 | 本课观察点 |
|---|---|---|
| HAL | 硬件抽象层,以句柄、初始化结构体、状态机和统一回调组织外设流程 | 返回值、句柄状态、错误码、回调 |
| LL | 低层驱动,通常以接近寄存器位域的内联函数/宏表达局部硬件动作 | 单项配置、使能和 flag 查询/清除 |
| CMSIS | Cortex Microcontroller Software Interface Standard,Cortex 微控制器软件接口标准 | 设备头文件、IRQ 名称、NVIC 接口和寄存器结构入口 |
| 句柄 | HAL 用来关联外设实例、初始化参数、DMA 子句柄、状态与错误的长期软件对象 | hadc1、hdma_adc1 |
| 初始化结构体 | 调用期内描述目标配置的字段集合 | hadc1.Init、ADC_ChannelConfTypeDef、hdma_adc1.Init |
| IRQ | interrupt request,中断请求 | 外设/ DMA 标志到处理函数的入口请求 |
| NVIC | nested vectored interrupt controller,嵌套向量中断控制器 | IRQ enable、pending、优先级和向量分发 |
| 状态位 | 硬件报告已发生或正在发生事件的位 | DMA_LISR.TCIF0、ADC_SR.OVR 等 |
4. 核心原理 #
4.1 一行交叉映射必须回答七件事 #
对每个操作意图,建立如下记录:
| 字段 | 要回答的问题 |
|---|---|
| 意图 | 这一步解决什么问题? |
| 前置条件 | 哪些时钟、复位、静止状态或对象连接必须先成立? |
| HAL | 哪个字段或 API 表达意图?返回值只证明什么? |
| LL | 同一意图是否有 LL 操作?其适用版本是什么? |
| 寄存器/标志 | 芯片实际接收配置或报告结果的位置是什么? |
| 成功证据 | 调用后可以安全观察什么?哪个事件尚未被证明? |
| 错误去向 | 失败如何返回、置位、进入 IRQ 或被应用记录? |
“HAL 名称看起来相似”不是映射证据。HAL 可能一次写多个字段、安装内部回调并改变句柄状态;LL 通常只表达局部动作;直接寄存器路径还要求调用者自己负责顺序、清标志和软件状态。因此映射可以是一对多或多对一,不要求 API 数量相等。
4.2 同一转换的三个观察层 #
操作意图:启动 ADC,并把结果循环搬到内存
HAL 层
hadc1.Init + hdma_adc1.Init + __HAL_LINKDMA
HAL_ADC_Start_DMA(&hadc1, buffer, count)
|
| 展开并核对
v
LL 层
配置 ADC/DMA 的局部字段、清标志、开中断、使能 stream/ADC、启动转换
|
| 最终作用于
v
寄存器/事件层
RCC enable/reset -> ADC_CCR/CR1/CR2/SQR/SMPR
DMA_S0CR/PAR/M0AR/NDTR -> ADC request -> LISR flags
-> NVIC -> vector -> HAL dispatch -> callbackHAL、LL 与寄存器不是顺序执行的三段程序。一个 HAL 函数内部通常直接通过设备头文件宏或寄存器访问完成工作,不一定调用公开 LL API。LL 列用于表达“如果以 LL 方式完成同一意图,应操作什么”,不是对 HAL 源码调用栈的虚构。
4.3 五种成功不能合并 #
| 成功层次 | 可以证明 | 不能证明 |
|---|---|---|
| 参数/API 成功 | HAL 接受参数并完成其同步配置路径 | 外设事件已经发生 |
| 配置提交成功 | 目标寄存器读回符合预期 | 输入信号、触发或请求一定到达 |
| 硬件事件成功 | EOC、HT、TC 等状态出现 | IRQ 一定被内核服务 |
| IRQ 分发成功 | 进入正确向量并调用正确 HAL handler | 应用回调一定完成业务消费 |
| 应用消费成功 | 所有权协议允许使用对应数据块 | 下一周期不会覆盖或发生过载 |
调试时应记录“目前最深到哪一层”,不要笼统写“DMA 不工作”。
4.4 五项责任 #
| 问题 | Host Fake | STM32F407xx 参考轨道 |
|---|---|---|
| 它解决什么 | 在无芯片头文件时验证启动前置条件、状态转换和错误路径 | 把同一意图落实到 F407 的时钟、ADC、DMA 和 IRQ 接口 |
| 对象在哪里定义 | 公共契约、快照、错误码在主机接口中定义 | 设备头文件定义寄存器;BSP 定义 HAL 句柄和缓冲区 |
| 谁持有 | 测试静态/栈上持有契约和 Fake 状态 | BSP 在整个运行期静态持有 hadc1、hdma_adc1 和缓冲区 |
| 谁注入 | 测试注入前置状态、请求与故障 | CubeMX/BSP 写初始化字段并用 __HAL_LINKDMA 连接句柄;板级组装选择 ADC 通道 |
| 调用如何流动 | 启动 -> 产生请求 -> 置位完成/错误 -> Fake IRQ -> 消费事件 | RCC -> DMA/ADC 配置 -> HAL 启动 -> 硬件请求/搬运 -> DMA IRQ -> HAL -> 应用回调 |
4.5 不混写的纪律 #
可以在调试器中用寄存器视图读取 HAL 配置结果,也可以用交叉表解释 HAL;但不要在 HAL 正持有同一外设时用 LL 或调试器直接改配置来“修好”现场。这样会绕过 HAL 的 State、Lock、错误码和内部回调连接。若决定改为 LL-only 或寄存器驱动,应明确建立新的所有权边界、完整初始化/停止/清错顺序和测试,而不是在一次事务中临时混用。
5. 关键术语与直观模型 #
把交叉映射想成一份带检查点的货运单:HAL 是业务委托和软件状态,LL 是局部控制动作,寄存器/标志是设备真正接受和报告的接口。货运单上的“已受理”不是“已送达”;每个中转点都要有可观察凭据。
最小表可以这样写:
| 意图 | HAL | LL | 硬件状态 | 观察结论 |
|---|---|---|---|---|
| 允许 DMA2 工作 | __HAL_RCC_DMA2_CLK_ENABLE() |
LL_AHB1_GRP1_EnableClock(...) |
RCC_AHB1ENR.DMA2EN=1 |
只证明门控打开,不证明有 ADC 请求 |
| 启动 stream | HAL_ADC_Start_DMA() 的内部启动路径 |
LL_DMA_EnableStream(...) |
DMA_S0CR.EN=1 |
只证明 stream 已使能,不证明已搬运 |
| 证明搬运完成 | ADC 完成回调最终被调用 | 查询 TC0 flag |
DMA_LISR.TCIF0=1 |
flag 证明硬件完成一轮;回调还依赖 IRQ 链 |
6. 从输入到结果的完整流程 #
6.1 初始化控制流 #
HAL_Init
-> SystemClock_Config
-> HSI/PLL/AHB/APB/Flash 条件稳定
-> MX_DMA_Init
-> DMA2 AHB1 门控
-> DMA2_Stream0 IRQ 优先级与 NVIC enable
-> MX_ADC1_Init
-> HAL_ADC_Init
-> HAL_ADC_MspInit
-> ADC1 APB2 门控
-> HAL_DMA_Init(&hdma_adc1)
-> __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1)
-> HAL_ADC_ConfigChannel
-> HAL_ADC_Start_DMA每个返回 HAL_StatusTypeDef 的调用都必须立即检查。初始化失败后继续运行,会把首个错误覆盖成下游“无数据”。
6.2 运行时数据流与中断流 #
软件启动或外部触发
-> ADC 开始转换
-> ADC_DR 产生数据并发出 DMA 请求
-> DMA2 Stream0 从 ADC1->DR 读 half-word
-> DMA2 Stream0 写内存并递减 NDTR
-> 到半边界:HTIF0;到末尾:TCIF0;出错:TEIF0/DMEIF0/FEIF0
-> DMA2_Stream0_IRQn pending
-> DMA2_Stream0_IRQHandler()
-> HAL_DMA_IRQHandler(&hdma_adc1)
-> HAL 安装的 ADC-DMA 内部回调
-> HAL_ADC_ConvHalfCpltCallback / HAL_ADC_ConvCpltCallback / HAL_ADC_ErrorCallback
-> 应用只记录事件;主循环消费数据或执行恢复控制流沿函数和 IRQ 入口移动,数据流从 ADC_DR 到内存。两条流在 DMA 标志和 HAL handler 处会合,但不能混为一条线。
6.3 失败定位树 #
HAL_ADC_Start_DMA != HAL_OK
-> 查参数、句柄状态、DMA 句柄关联和 HAL 错误码
HAL_OK,但 NDTR 从不变化
-> 查 RCC 门控/复位、ADC 时钟、ADC enable、触发/软件启动、DMA request、stream enable
NDTR 变化且 flag 出现,但不进 IRQ
-> 查 stream interrupt enable、NVIC enable/pending、优先级屏蔽、向量表入口
进入 DMA2_Stream0_IRQHandler,但无 ADC 回调
-> 查是否调用 HAL_DMA_IRQHandler(&hdma_adc1)、句柄是否为同一实例、HAL 内部回调连接
回调发生,但应用看不到数据
-> 查事件变量、volatile/并发规则、缓冲区半区所有权和消费速度7. 嵌入式系统中的对应位置 #
7.1 外设能力地图 #
| 层级 | 能力与工作原理 | 触发/数据/中断链 | 本课与后续落点 |
|---|---|---|---|
| 基础 | 从初始化/启动动作追到门控、配置、使能、状态和错误返回 | 软件调用 -> 配置寄存器 -> 状态读回 | 本课完整示范 |
| 进阶 | 将硬件 flag、NVIC、向量、HAL handler 和应用回调分层观察 | ADC request -> DMA HT/TC/TE -> IRQ -> 回调 | 本课静态交叉表;register-view 做实时证据采集 |
| 高级 | 双缓冲在两个地址间切换以延长消费窗口;DMA burst 用一次请求序列搬多个寄存器;链表 DMA 由描述符驱动后续节点 | 请求路由 -> DMA 队列/双缓冲 -> 分段中断 -> 消费者;缓存系统还需 clean/invalidate | 双缓冲与 burst 在 DMA 深化实验;缓存一致性在 dma-coherency;H5 GPDMA 链表按具体目标课核对 |
| 跨外设联动 | 定时器更新事件经 TRGO 驱动 ADC,ADC 完成产生 DMA 请求,DMA 批量完成再通知软件 | Timer TRGO -> ADC sample/convert -> DMA -> IRQ/任务 | timer-trigger-routing 与 adc-timer-dma-pipeline |
| 系列差异 | 映射方法可迁移,具体时钟、请求器、DMA 控制器、缓存和安全属性不可照搬 | 每个目标从 RCC/trigger/request/memory/IRQ 重新建图 | F4 本课;F7/H5/H7 在对应系列与性能课程深化 |
F4/F7/H5/H7 的迁移重点:
| 系列 | 可迁移 | 必须按具体器件第一方资料重查 |
|---|---|---|
| F4 | 意图、前置条件、证据层次和错误树 | 本课给出的 stream/channel、RCC 位、ADC/DMA 字段只限 RM0090 覆盖器件 |
| F7 | 同样可从 HAL 追到 flag/IRQ/回调 | DMA 请求表、存储区可达性;具备数据缓存的目标还要重建 DMA 缓冲区一致性协议 |
| H5 | 同样区分 API 成功与硬件完成 | GPDMA/DMAMUX 的 request、channel、链表节点、时钟、TrustZone/安全属性;未选目标前不写寄存器名 |
| H7 | 同样按控制流和数据流收集证据 | DMA/BDMA/MDMA 选择、DMAMUX、域时钟、总线/内存域可达性、D-Cache;双核型号还需核对共享与中断归属 |
高级功能不能靠替换一个 API 名获得。例如缓存存在时,即使 DMA TC 已置位,CPU 读取的 cache line 仍可能是旧数据;此时交叉表必须增加“缓冲区位于何处、谁 clean/invalidate、屏障何时发生”这一层证据。F407 本案例不据此推断 F7/H7 行为。
7.2 参考目标与时钟/复位链 #
参考目标为 STM32F407xx,寄存器和请求映射依据 RM0090 Rev 22,HAL/LL API 边界依据 UM1725 Rev 8,运行条件边界由 DS8626 Rev 12 约束。[S1][S2][S3] 本课没有选定封装、引脚或开发板。
沿用上一课已建立的参考参数集,不声称未知板卡采用此配置:
HSI 16 MHz
-> PLLM /16 = 1 MHz
-> PLLN x336 = 336 MHz VCO
-> PLLP /2 = 168 MHz SYSCLK
-> AHB /1 = 168 MHz HCLK
-> DMA2 AHB1 interface:168 MHz 分支 + RCC_AHB1ENR.DMA2EN
-> APB2 /2 = 84 MHz PCLK2
-> ADC common prescaler /4 = 21 MHz ADCCLK
-> ADC1 interface gate:RCC_APB2ENR.ADC1EN最终 21 MHz 是这组参考参数下的 ADC 时钟,不是采样率。采样时间与分辨率对应的转换周期共同决定请求上限;软件触发/外部触发决定何时开始;DMA2 的 HCLK 只支撑总线访问。[S1][S3]
完整门控与复位顺序:
HSI ready -> PLL ready -> SYSCLK switch confirmed
-> AHB/APB prescalers established
-> enable DMA2 AHB1 gate
-> optional DMA2 reset pulse, then release (only before other DMA2 users run)
-> enable ADC1 APB2 gate
-> optional shared ADC reset pulse, then release (only before every ADC user is idle)
-> program DMA2 Stream0 while EN=0; clear stale flags
-> program ADC common prescaler and ADC1 fields
-> enable NVIC path
-> enable DMA stream and ADC/DMA request
-> trigger conversion门控、复位和外设使能是三个动作:门控决定寄存器接口/模块能否获得时钟;复位把模块恢复到规定状态;DMA_S0CR.EN 与 ADC_CR2.ADON 才启动对应运行状态。整块 DMA2 复位只能在确认没有其他 stream 使用者时执行;F407 的 RCC_APB2RSTR.ADCRST 是 ADC1/2/3 共用复位,只有确认所有 ADC 使用者都静止时才可脉冲,不能用它恢复单个 ADC1。[S1]
7.3 CubeMX、HAL、LL 与寄存器对照 #
CubeMX/CubeMX2 配置清单 #
- 选择实际使用的 STM32F407 完整料号;记录 STM32CubeMX/CubeMX2 和 STM32CubeF4 包版本。
- RCC / Clock Configuration:参考方案选择 HSI,设置 PLLM=16、PLLN=336、PLLP=2,AHB=/1、APB1=/4、APB2=/2;目标工程必须按完整料号、VDD、调压与 Flash 条件重新核对。
- ADC1:选择实际板卡允许的 ADC 通道和模拟引脚;本课不指定引脚。12-bit、right alignment、single conversion rank;参考连续模式开启,ADC clock=PCLK2/4,采样时间需按模拟源阻抗核对。
- DMA Settings:ADC1 request,DMA2 Stream0/Channel0,Peripheral-to-Memory,Circular,Peripheral Increment Disabled,Memory Increment Enabled,Peripheral/Memory Data Width=Half Word,Priority=High,FIFO Disabled。该映射来自 RM0090 表 44,只适用于手册覆盖目标。[S1]
- NVIC:启用 DMA2 Stream0 global interrupt;如需要由 HAL 处理 ADC overrun(过载),启用共享 ADC global interrupt。优先级按完整系统策略选择,不能从本文复制为通用值。
- Project Manager:生成后审计
SystemClock_Config、MX_DMA_Init、HAL_ADC_MspInit、MX_ADC1_Init、stm32f4xx_it.c和句柄链接,不以 GUI 绿色状态代替源码审计。
板级必须提供:完整料号、实际 ADC channel/pin、模拟输入范围/参考与源阻抗、外部时钟条件(如不用 HSI)、中断优先级政策。缺少这些信息时,参考机制仍可实现,但不能声称引脚、电气或时序已适配。
对象、所有权和生命周期 #
| 对象 | 定义/实例化 | 持有者与生命周期 | 注入方式 |
|---|---|---|---|
ADC_HandleTypeDef hadc1 |
BSP/生成代码的目标适配文件 | BSP 静态持有,覆盖初始化、运行、IRQ 与停止 | Instance、Init 字段由初始化函数写入 |
DMA_HandleTypeDef hdma_adc1 |
BSP/生成代码的目标适配文件 | BSP 静态持有;IRQ handler 与 ADC 句柄长期引用 | HAL_DMA_Init 后由 __HAL_LINKDMA 双向连接 |
ADC_ChannelConfTypeDef channel |
ADC 初始化函数内 | 调用期局部对象;提交后可结束生命周期 | 板级组装传入合法通道常量 |
| DMA 缓冲区 | BSP 数据模块 | BSP/DMA 共同持有;DMA 运行时应用不得写正在填充区 | 地址和数据项数传给 HAL_ADC_Start_DMA |
| 诊断记录 | BSP 诊断模块 | 静态持有;ISR 写最小字段,主循环读取 | 回调注入事件、HAL/寄存器快照 |
HAL 初始化字段 #
hadc1.Init 的关键字段不是装饰性元数据:
ClockPrescaler:选择 ADC 公共时钟相对 PCLK2 的分频,本例/4;最终进入 ADC common control register 的ADCPRE。Resolution、DataAlign:决定 ADC 结果格式及ADC_DR中的有效布局。ScanConvMode、NbrOfConversion:本例单 rank;多 rank 会改变序列与 DMA 数据顺序。ContinuousConvMode:转换完成后继续下一次转换;它不同于 DMA circular。ExternalTrigConvEdge、ExternalTrigConv:本例软件启动;未来 Timer TRGO 课改为硬件触发。DMAContinuousRequests:使连续 DMA 请求与 circular stream 配合;对应 F4 ADCCR2.DDS行为。EOCSelection:HAL 如何解释 conversion/sequence 完成;迁移时按包版本核对。
hdma_adc1.Init 定义方向、地址递增、peripheral/memory 数据宽度、normal/circular、优先级和 FIFO。HAL_ADC_Start_DMA 的 Length 最终对应 DMA 数据项数,本例 half-word 配置下不是字节数。[S1][S2]
HAL、LL、寄存器交叉表 #
下表的寄存器语义来自 RM0090 Rev 22;LL 名称以 UM1725 Rev 8 的 STM32F4 API 面为版本边界。实际工程仍需与安装的 STM32CubeF4 头文件逐项比对。[S1][S2]
| 意图/前置条件 | HAL 字段或动作 | 同意图 LL 入口 | F407 寄存器/状态 | 成功观察与错误边界 |
|---|---|---|---|---|
| 开 DMA2 门控;RCC 可访问 | __HAL_RCC_DMA2_CLK_ENABLE() |
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA2) |
RCC_AHB1ENR.DMA2EN |
读回为 1 只证明门控;无请求时 NDTR 仍不变 |
| 开 ADC1 门控;PCLK2 已建立 | __HAL_RCC_ADC1_CLK_ENABLE() |
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_ADC1) |
RCC_APB2ENR.ADC1EN |
读回为 1;不证明 ADCCLK/触发正确 |
| 复位并释放 DMA2;所有 stream 静止 | __HAL_RCC_DMA2_FORCE_RESET() / __HAL_RCC_DMA2_RELEASE_RESET() |
LL_AHB1_GRP1_ForceReset/ReleaseReset(LL_AHB1_GRP1_PERIPH_DMA2) |
RCC_AHB1RSTR.DMA2RST |
必须看到 reset 位释放;共享运行期禁止整块复位 |
| 复位并释放 ADC 公共接口;ADC1/2/3 均静止 | __HAL_RCC_ADC_FORCE_RESET() / __HAL_RCC_ADC_RELEASE_RESET() |
LL_APB2_GRP1_ForceReset/ReleaseReset(LL_APB2_GRP1_PERIPH_ADC) |
RCC_APB2RSTR.ADCRST |
该位共用于全部 ADC,不能当成 ADC1 私有恢复动作 |
| 设置 ADC 公共分频;ADC 未运行 | ClockPrescaler=ADC_CLOCK_SYNC_PCLK_DIV4 后 HAL_ADC_Init |
LL_ADC_SetCommonClock(..., LL_ADC_CLOCK_SYNC_PCLK_DIV4) |
ADC_CCR.ADCPRE=01 |
PCLK2 84 MHz 时推导 ADCCLK 21 MHz;这是计算值,不是物理测量 |
| 选择 DMA 请求映射;stream disabled | Instance=DMA2_Stream0、Channel=DMA_CHANNEL_0 |
LL_DMA_SetChannelSelection(DMA2, LL_DMA_STREAM_0, LL_DMA_CHANNEL_0) |
DMA_S0CR.CHSEL=000;RM0090 表 44 |
读回映射正确仍不证明 ADC request 到达 |
| 配方向/宽度/递增/循环;EN=0 | hdma_adc1.Init.* 后 HAL_DMA_Init |
LL_DMA_SetDataTransferDirection、SetPeriphSize、SetMemorySize、SetMemoryIncMode、SetMode |
DMA_S0CR.DIR/PINC/MINC/PSIZE/MSIZE/CIRC/PL、DMA_S0FCR |
HAL 返回值与寄存器读回;EN=1 时不得改大多数配置字段 |
| 连接软件对象;两个句柄已存在 | __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc1) |
无硬件 LL 等价;这是 HAL 软件连接 | hadc1.DMA_Handle 与 hdma_adc1.Parent,不写外设寄存器 |
指针双向一致;错误连接会让 IRQ 到错误 parent/回调 |
| 提交地址和计数;buffer 有效 | HAL_ADC_Start_DMA(hadc, buffer, count) 内部 DMA 启动 |
LL_DMA_SetPeriphAddress、SetMemoryAddress、SetDataLength |
DMA_S0PAR=&ADC1->DR、M0AR=buffer、NDTR=count |
地址/计数读回;仍未证明已产生一次请求 |
| 开 HT/TC/error interrupt;旧 flag 已清 | HAL ADC-DMA 启动内部配置 DMA callback/IT | LL_DMA_EnableIT_HT/TC/TE/DME;FIFO error 按实际配置核对 |
DMA_S0CR.HTIE/TCIE/TEIE/DMEIE、DMA_S0FCR.FEIE |
enable 位 + NVIC enable 均需要;有 flag 无 IRQ 时查后半链 |
| 使能 stream;配置已完成 | HAL ADC-DMA 启动内部 DMA start | LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_0) |
DMA_S0CR.EN |
EN=1 证明 stream armed;NDTR 变化才证明请求/搬运 |
| 允许 ADC DMA 请求与连续请求 | HAL_ADC_Start_DMA,结合 DMAContinuousRequests=ENABLE |
LL_ADC_REG_SetDMATransfer(...UNLIMITED),常量名按包版本复核 |
ADC_CR2.DMA[1:0]、DDS |
位读回;错误模式可能导致序列后停止请求 |
| 使能 ADC 并软件启动;ADC 配置合法 | HAL_ADC_Start_DMA |
LL_ADC_Enable、LL_ADC_REG_StartConversionSWStart |
ADC_CR2.ADON、SWSTART;转换状态在 ADC_SR |
SWSTART 是启动动作;EOC/OVR 与 DMA 请求才是后续证据 |
| 半/全搬运或 DMA 错误 | HAL DMA handler 分发到 ADC 回调 | LL_DMA_IsActiveFlag_HT0/TC0/TE0/DME0/FE0 与 clear API |
DMA_LISR.HTIF0/TCIF0/TEIF0/DMEIF0/FEIF0;DMA_LIFCR 清除 |
flag 是硬件证据;handler 必须按文档顺序处理/清除 |
| ADC overrun 等 ADC 错误 | HAL_ADC_IRQHandler / HAL_ADC_ErrorCallback / HAL_ADC_GetError |
查询并清 ADC flag 的 LL API,按包版本复核 | ADC_SR.OVR 等、ADC_CR1 对应中断使能 |
区分 ADC_IRQn 与 DMA2 Stream0 IRQ;保存 ADC 和 DMA 两侧错误码 |
关键审计结论:__HAL_LINKDMA 没有寄存器等价物,因为它解决的是软件对象关系;反过来,某些硬件 flag 也未必有一个独立 HAL API,可能只在 handler 内部被消费。交叉映射不应强迫三列一一同名。
HAL 参考实现 #
以下代码属于目标 BSP 适配层,依赖方向为 App -> BSP -> STM32CubeF4 HAL -> CMSIS/硬件。句柄、缓冲区和诊断记录由 BSP 静态持有整个运行期。代码未在本次运行中放入实际 Cube 工程编译,需以安装包头文件核对字段和宏。
/* 示例,待目标工程编译和上板验证。 */
#include "stm32f4xx_hal.h"
#include <stdint.h>
#define ADC_DMA_SAMPLE_COUNT 128U
typedef uint32_t bsp_adc_error_t;
#define BSP_ADC_OK UINT32_C(0)
#define BSP_ADC_ERR_CLOCK UINT32_C(1)
#define BSP_ADC_ERR_DMA_INIT UINT32_C(2)
#define BSP_ADC_ERR_ADC_INIT UINT32_C(3)
#define BSP_ADC_ERR_CHANNEL UINT32_C(4)
#define BSP_ADC_ERR_START UINT32_C(5)
#define BSP_ADC_ERR_RUNTIME UINT32_C(6)
typedef struct {
volatile uint32_t half_events;
volatile uint32_t full_events;
volatile uint32_t error_events;
volatile uint32_t hal_adc_error;
volatile uint32_t hal_dma_error;
volatile bsp_adc_error_t first_error;
} bsp_adc_diag_t;
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
_Alignas(uint32_t)
static uint16_t g_adc_samples[ADC_DMA_SAMPLE_COUNT];
static bsp_adc_diag_t g_adc_diag;
static HAL_StatusTypeDef g_adc_msp_status = HAL_OK;
static void bsp_record_first_error(bsp_adc_error_t error)
{
if (g_adc_diag.first_error == BSP_ADC_OK) {
g_adc_diag.first_error = error;
}
}
static void MX_DMA_Init_Reference(void)
{
__HAL_RCC_DMA2_CLK_ENABLE();
/* 仅允许在启动早期、确认 DMA2 没有其他使用者时复位。 */
__HAL_RCC_DMA2_FORCE_RESET();
__HAL_RCC_DMA2_RELEASE_RESET();
/* 示例优先级;集成时按完整系统策略重新选择。 */
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5U, 0U);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{
if ((hadc == NULL) || (hadc->Instance != ADC1)) {
return;
}
__HAL_RCC_ADC1_CLK_ENABLE();
/* ADCRST 共用于 ADC1/2/3;只允许在所有 ADC 使用者均静止的启动期执行。 */
__HAL_RCC_ADC_FORCE_RESET();
__HAL_RCC_ADC_RELEASE_RESET();
hdma_adc1.Instance = DMA2_Stream0;
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) {
g_adc_msp_status = HAL_ERROR;
bsp_record_first_error(BSP_ADC_ERR_DMA_INIT);
return;
}
__HAL_LINKDMA(hadc, DMA_Handle, hdma_adc1);
}
static bsp_adc_error_t MX_ADC1_Init_Reference(uint32_t board_adc_channel)
{
ADC_ChannelConfTypeDef channel = {0};
g_adc_msp_status = HAL_OK;
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1U;
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if ((HAL_ADC_Init(&hadc1) != HAL_OK) ||
(g_adc_msp_status != HAL_OK)) {
bsp_record_first_error(BSP_ADC_ERR_ADC_INIT);
return BSP_ADC_ERR_ADC_INIT;
}
channel.Channel = board_adc_channel;
channel.Rank = 1U;
channel.SamplingTime = ADC_SAMPLETIME_84CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &channel) != HAL_OK) {
bsp_record_first_error(BSP_ADC_ERR_CHANNEL);
return BSP_ADC_ERR_CHANNEL;
}
return BSP_ADC_OK;
}
static bsp_adc_error_t BSP_ADC_Start(void)
{
if ((hadc1.DMA_Handle != &hdma_adc1) ||
(hdma_adc1.Parent != &hadc1)) {
bsp_record_first_error(BSP_ADC_ERR_START);
return BSP_ADC_ERR_START;
}
if (HAL_ADC_Start_DMA(&hadc1,
(uint32_t *)g_adc_samples,
ADC_DMA_SAMPLE_COUNT) != HAL_OK) {
g_adc_diag.hal_adc_error = HAL_ADC_GetError(&hadc1);
g_adc_diag.hal_dma_error = HAL_DMA_GetError(&hdma_adc1);
bsp_record_first_error(BSP_ADC_ERR_START);
return BSP_ADC_ERR_START;
}
return BSP_ADC_OK;
}这里把板级 ADC channel 作为组装参数传入,避免虚构引脚。_Alignas(uint32_t) 满足 HAL 启动入口的指针对齐要求,实际 DMA 传输宽度仍由 half-word 配置决定。通道宏只能由选定完整料号、封装、板卡原理图和 CubeMX pinout 共同确定。
调用顺序属于 BSP 组装层:
/* 示例,待目标工程编译和上板验证。 */
int main(void)
{
HAL_Init();
if (SystemClock_Config_Reference() != HAL_OK) {
Error_Handler();
}
MX_GPIO_Init(); /* 实际 ADC 模拟引脚由目标工程生成。 */
MX_DMA_Init_Reference();
if (MX_ADC1_Init_Reference(BSP_ADC_CHANNEL) != BSP_ADC_OK) {
Error_Handler();
}
if (BSP_ADC_Start() != BSP_ADC_OK) {
Error_Handler();
}
for (;;) {
app_consume_adc_events();
}
}BSP_ADC_CHANNEL 必须在实际板级配置头文件中定义为合法的 ADC_CHANNEL_x;它不是本文偷偷选定的占位引脚。SystemClock_Config_Reference 复用上一课的 HSI 参考方案,实施时由实际 CubeMX 工程生成并审计。
IRQ、回调与错误路径 #
/* stm32f4xx_it.c:向量层,只把正确句柄交给 HAL。 */
void DMA2_Stream0_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc1);
}
/* 若目标配置启用 ADC IRQ,用它捕获 ADC 侧中断源。 */
void ADC_IRQHandler(void)
{
HAL_ADC_IRQHandler(&hadc1);
}
/* BSP 事件层:ISR 中只记录,不打印、不重新初始化。 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
if ((hadc != NULL) && (hadc->Instance == ADC1)) {
g_adc_diag.half_events++;
}
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if ((hadc != NULL) && (hadc->Instance == ADC1)) {
g_adc_diag.full_events++;
}
}
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc)
{
if ((hadc != NULL) && (hadc->Instance == ADC1)) {
g_adc_diag.hal_adc_error = HAL_ADC_GetError(hadc);
g_adc_diag.hal_dma_error = HAL_DMA_GetError(hadc->DMA_Handle);
g_adc_diag.error_events++;
bsp_record_first_error(BSP_ADC_ERR_RUNTIME);
}
}DMA 硬件置位 flag 后,只有对应 stream interrupt enable、NVIC enable、内核未屏蔽该优先级、向量表指向正确 handler、handler 使用正确句柄、HAL 内部 DMA callback 已由 HAL_ADC_Start_DMA 安装,才会进入 ADC 完成/错误回调。[S2]
恢复动作放在主循环:先阻止应用把新数据当成有效;调用 HAL_ADC_Stop_DMA;保存 ADC_SR/CR1/CR2、DMA_S0CR/NDTR/LISR、HAL 错误码;确认 stream disabled;按错误类型清理和重新初始化。不能在 ISR 中打印或复位整个 DMA2,也不能只清 flag 后假装根因消失。
声明、实现、实例化、组装和使用 #
即使本课不是架构课,也要把五个动作分清:
| 动作 | 示例位置 | 责任 |
|---|---|---|
| 声明 | bsp_adc.h 中声明 BSP_ADC_Start、事件读取接口和错误类型 |
定义 App 能依赖的契约,不暴露寄存器 |
| 实现 | bsp_adc_stm32f407.c 中实现 HAL 初始化、回调和恢复 |
依赖 STM32CubeF4 HAL/CMSIS |
| 实例化 | 同文件定义 hadc1、hdma_adc1、静态缓冲区 |
建立唯一长期对象和生命周期 |
| 组装 | board.c/main.c 选择实际 channel,按顺序调用 clock/DMA/ADC init |
注入板级决定并连接对象 |
| 使用 | app.c 消费半区/全区事件 |
只依赖 BSP 契约,不改 HAL/寄存器 |
依赖方向始终从 App 指向 BSP 接口,再到目标实现;中断从硬件反向进入实现,但不会让 App 持有 HAL 句柄。
8. 主机实验与硬件迁移边界 #
8.1 Host Fake 验证轨道 #
Host Fake 不模拟 F407 位布局,只表达“启动前置条件、循环请求、计数重装、半/全完成标志、IRQ 消费和显式停止”的最小闭环。它与本课 F407 参考配置的 circular DMA 生命周期一致;公开接口使用固定宽度整数,不使用动态内存。
/* host_crosswalk.c:C11,主机机制模型。 */
#include <assert.h>
#include <stdint.h>
#include <string.h>
typedef int32_t peripheral_status_t;
#define PERIPH_OK INT32_C(0)
#define PERIPH_ERR_ARGUMENT -INT32_C(1)
#define PERIPH_ERR_CLOCK -INT32_C(2)
#define PERIPH_ERR_CONFIG -INT32_C(3)
#define PERIPH_ERR_DMA -INT32_C(4)
#define PERIPH_ERR_IRQ -INT32_C(5)
#define PERIPH_ERR_RUNNING -INT32_C(6)
typedef struct {
uint8_t clock_enabled;
uint8_t config_committed;
uint8_t dma_linked;
uint8_t irq_registered;
uint32_t transfer_count;
} peripheral_contract_t;
typedef struct {
uint8_t running;
uint8_t request_enabled;
uint8_t half_flag;
uint8_t complete_flag;
uint8_t error_flag;
uint32_t remaining;
uint32_t reload_count;
uint32_t half_callbacks;
uint32_t completed_callbacks;
uint32_t error_callbacks;
} peripheral_snapshot_t;
peripheral_status_t peripheral_start(
const peripheral_contract_t *contract,
peripheral_snapshot_t *state)
{
peripheral_snapshot_t next;
if ((contract == NULL) || (state == NULL) ||
(contract->transfer_count < 2U) ||
((contract->transfer_count % 2U) != 0U)) {
return PERIPH_ERR_ARGUMENT;
}
if (state->running != 0U) {
return PERIPH_ERR_RUNNING;
}
if (contract->clock_enabled == 0U) {
return PERIPH_ERR_CLOCK;
}
if (contract->config_committed == 0U) {
return PERIPH_ERR_CONFIG;
}
if (contract->dma_linked == 0U) {
return PERIPH_ERR_DMA;
}
if (contract->irq_registered == 0U) {
return PERIPH_ERR_IRQ;
}
next = *state;
next.running = 1U;
next.request_enabled = 1U;
next.half_flag = 0U;
next.complete_flag = 0U;
next.error_flag = 0U;
next.remaining = contract->transfer_count;
next.reload_count = contract->transfer_count;
*state = next;
return PERIPH_OK;
}
peripheral_status_t peripheral_fake_request(
peripheral_snapshot_t *state,
uint8_t inject_dma_error)
{
if (state == NULL) {
return PERIPH_ERR_ARGUMENT;
}
if ((state->running == 0U) || (state->request_enabled == 0U)) {
return PERIPH_ERR_RUNNING;
}
if (inject_dma_error != 0U) {
state->error_flag = 1U;
state->request_enabled = 0U;
return PERIPH_ERR_DMA;
}
state->remaining--;
if (state->remaining == (state->reload_count / 2U)) {
state->half_flag = 1U;
}
if (state->remaining == 0U) {
state->complete_flag = 1U;
state->remaining = state->reload_count;
}
return PERIPH_OK;
}
void peripheral_fake_irq(peripheral_snapshot_t *state)
{
if (state == NULL) {
return;
}
if (state->error_flag != 0U) {
state->error_flag = 0U;
state->running = 0U;
state->request_enabled = 0U;
state->error_callbacks++;
return;
}
if (state->half_flag != 0U) {
state->half_flag = 0U;
state->half_callbacks++;
}
if (state->complete_flag != 0U) {
state->complete_flag = 0U;
state->completed_callbacks++;
}
}
peripheral_status_t peripheral_stop(peripheral_snapshot_t *state)
{
if (state == NULL) {
return PERIPH_ERR_ARGUMENT;
}
if (state->running == 0U) {
return PERIPH_ERR_RUNNING;
}
state->request_enabled = 0U;
state->running = 0U;
state->remaining = 0U;
return PERIPH_OK;
}
static void test_circular_completion_requires_irq_consumption(void)
{
const peripheral_contract_t contract = {1U, 1U, 1U, 1U, 4U};
peripheral_snapshot_t state;
uint32_t index;
(void)memset(&state, 0, sizeof(state));
assert(peripheral_start(&contract, &state) == PERIPH_OK);
assert(state.running == 1U);
for (index = 0U; index < 2U; index++) {
assert(peripheral_fake_request(&state, 0U) == PERIPH_OK);
}
assert(state.half_flag == 1U);
assert(state.half_callbacks == 0U);
peripheral_fake_irq(&state);
assert(state.half_flag == 0U);
assert(state.half_callbacks == 1U);
for (index = 0U; index < 2U; index++) {
assert(peripheral_fake_request(&state, 0U) == PERIPH_OK);
}
assert(state.complete_flag == 1U);
assert(state.remaining == contract.transfer_count);
assert(state.request_enabled == 1U);
assert(state.completed_callbacks == 0U);
peripheral_fake_irq(&state);
assert(state.complete_flag == 0U);
assert(state.completed_callbacks == 1U);
assert(state.running == 1U);
assert(peripheral_stop(&state) == PERIPH_OK);
assert(state.running == 0U);
assert(state.request_enabled == 0U);
}
static void test_each_precondition(void)
{
peripheral_contract_t contract = {1U, 1U, 1U, 1U, 4U};
peripheral_snapshot_t state;
(void)memset(&state, 0, sizeof(state));
contract.clock_enabled = 0U;
assert(peripheral_start(&contract, &state) == PERIPH_ERR_CLOCK);
contract.clock_enabled = 1U;
contract.config_committed = 0U;
assert(peripheral_start(&contract, &state) == PERIPH_ERR_CONFIG);
contract.config_committed = 1U;
contract.dma_linked = 0U;
assert(peripheral_start(&contract, &state) == PERIPH_ERR_DMA);
contract.dma_linked = 1U;
contract.irq_registered = 0U;
assert(peripheral_start(&contract, &state) == PERIPH_ERR_IRQ);
assert(state.running == 0U);
}
static void test_dma_error_path(void)
{
const peripheral_contract_t contract = {1U, 1U, 1U, 1U, 4U};
peripheral_snapshot_t state;
(void)memset(&state, 0, sizeof(state));
assert(peripheral_start(&contract, &state) == PERIPH_OK);
assert(peripheral_fake_request(&state, 1U) == PERIPH_ERR_DMA);
assert(state.error_callbacks == 0U);
peripheral_fake_irq(&state);
assert(state.error_callbacks == 1U);
assert(state.running == 0U);
}
int main(void)
{
test_circular_completion_requires_irq_consumption();
test_each_precondition();
test_dma_error_path();
return 0;
}最小构建命令示例:
cc -std=c11 -Wall -Wextra -Wpedantic -Werror host_crosswalk.c -o host_crosswalk
./host_crosswalk课程正文不把这条主机构建命令列为 STM32 构建或硬件证据。即使断言全部通过,也只证明抽象前置条件、循环重装、显式停止和“flag 与 callback 是两个时刻”;它不证明 STM32 API、位布局、ADC 电气行为或真实 IRQ。主机检查是否执行以及结果如何,应以当次运行记录为准。
8.2 STM32 参考实现轨道 #
可由文档静态核对:RM0090 表 44 的 ADC1 -> DMA2 Stream0/Channel0 映射、F407 RCC/ADC/DMA 寄存器语义,UM1725 的 HAL/LL API 与回调契约,以及 DS8626 的器件工作条件边界。[S1][S2][S3]
仍需实际目标工程证明:
- 完整 STM32F407 料号、封装、板卡原理图和合法 ADC 引脚;
- CubeMX 与 STM32CubeF4 精确版本,生成
.ioc和源码; - Arm 工具链版本、完整构建日志和 map 文件;
- 烧录记录、调试器连接条件和固件标识;
- RCC/ADC/DMA/NVIC 寄存器快照、缓冲区变化、回调计数和错误注入结果;
- 模拟输入、参考电压、源阻抗与必要仪器记录。
缺少这些证据时保持 hardware_verified=false,但参考代码、配置和观察计划仍然完整保留。
9. 最小代码示例 #
最小可运行形态是 8.1 节的 Host Fake:四个前置条件全部满足才进入 running;半数请求时置 half flag,四次请求后置完成 flag 并按 circular 语义重装计数;只有 Fake IRQ 消费 flag 后才增加相应回调计数,停止则必须显式请求。这个分离正是交叉映射的最小价值。
将它映射到 F407:
| Fake 字段 | F407 对应证据 | 明确不等价之处 |
|---|---|---|
clock_enabled |
RCC_AHB1ENR.DMA2EN、RCC_APB2ENR.ADC1EN 与时钟链 |
Fake 没有 PLL、总线和 ADC 分频位布局 |
config_committed |
ADC/DMA 配置寄存器读回 | Fake 不验证字段编码与电气限制 |
dma_linked |
HAL 句柄双向关联 + DMA request mapping | 前者是软件关系,后者是芯片路由,必须分别核对 |
irq_registered |
stream IT enable + NVIC enable + 正确向量 | Fake 把多个关卡压成一个布尔值 |
remaining / reload_count |
DMA_S0NDTR.NDT 与 circular 自动重装 |
Fake 每次函数调用等于一个请求,不模拟总线仲裁或重装时序 |
half_flag / half_callbacks |
DMA_LISR.HTIF0 与 HAL_ADC_ConvHalfCpltCallback |
Fake 只表达 flag 先于回调,不模拟 IRQ 延迟 |
complete_flag |
DMA_LISR.TCIF0 |
真硬件 flag 的清除规则由 RM0090 定义 |
completed_callbacks |
HAL_ADC_ConvCpltCallback 中事件计数 |
真回调还依赖 HAL 内部 callback 和句柄 parent |
10. 常见错误 #
- 把
HAL_OK写成“采样完成”:它最多证明同步启动路径成功。 - 强迫三层 API 一一同名:HAL 可能一次覆盖多个硬件动作;
__HAL_LINKDMA甚至没有硬件等价物。 - 认为 HAL 会调用公开 LL API:HAL 和 LL 可以共享设备头文件,但不能据此虚构调用关系。
- 只看配置寄存器,不看状态 flag:配置正确仍可能没有触发或请求。
- 只看 flag,不看 IRQ enable/NVIC/vector:flag 已置位不保证 handler 运行。
- handler 传错句柄:IRQ 进入了,但错误的 stream handle 无法沿正确 parent 分发。
- 忘记
__HAL_LINKDMA:DMA 可被配置,却没有与 ADC HAL 状态机正确关联。 - 在 EN=1 时改 DMA 配置:F407 DMA stream 大多数配置必须在 disabled 状态修改。[S1]
- 把 DMA2 HCLK 当采样率:请求节奏来自 ADC 时钟、采样/转换周期和触发。
- 把
Length当字节数:它是按 DMA 配置宽度计数的数据项数。 - ISR 内打印或重初始化:延长中断并破坏首个故障现场;ISR 只保存最小证据。
- 用调试器改寄存器修 HAL:软件句柄与硬件状态立即失配。
- 复位整个 DMA2 恢复单个 stream:会破坏其他 DMA2 使用者。
- 复制 F407 stream/channel 到 H5/H7:DMA 架构、请求器和时钟/内存边界不同。
- 把 CubeMX 生成当硬件证据:生成只证明工具产出,不证明编译、烧录、电气和事件链。
11. 调试观察点 #
按因果链由前向后记录,不要随机翻寄存器:
RCC_CFGR.SWS、AHB/APB 分频和软件计算频率:上游时钟是否成立;RCC_AHB1ENR.DMA2EN、RCC_APB2ENR.ADC1EN:门控是否打开;RCC_AHB1RSTR.DMA2RST、RCC_APB2RSTR.ADCRST:模块是否意外仍在复位;注意ADCRST同时影响 ADC1/2/3;ADC_CCR.ADCPRE:参考方案是否为 PCLK2/4;hadc1.DMA_Handle与hdma_adc1.Parent:软件关联是否双向一致;DMA_S0CR的 CHSEL/DIR/MINC/PSIZE/MSIZE/CIRC/IT/EN,及PAR/M0AR/NDTR:传输契约是否提交;ADC_CR1/CR2:分辨率、DMA/DDS、ADON、触发边沿/源和 SWSTART 是否符合预期;ADC_SR:EOC/OVR 等 ADC 侧事件;DMA_LISR:HTIF0/TCIF0/TEIF0/DMEIF0/FEIF0;记录 handler 前后和清除行为;- NVIC enable/pending/active 与
DMA2_Stream0_IRQHandler断点:硬件 flag 是否越过中断控制器; HAL_DMA_IRQHandler入参地址与hdma_adc1地址:是否分发给正确对象;- 半/全/错误回调计数和 HAL ADC/DMA 错误码:HAL 到应用的最后一段是否成立;
- 缓冲区每半区的代次/校验:回调存在不等于应用及时消费。
建议每次故障保存一个快照表,包含时间点、HAL 返回、句柄状态、RCC、ADC、DMA、NVIC 和应用事件计数。只保存最终值会丢失“哪个 flag 最先出现”的信息。
12. 实战实验 #
实验 A:Host Fake 最小闭环 #
- 将 8.1 节保存为
host_crosswalk.c; - 使用本机 C11 编译器和严格警告选项构建;
- 记录编译器版本、命令、退出码和输出;
- 单步确认第二次 request 后
half_flag=1,Fake IRQ 消费后半传输回调计数增加; - 确认第四次 request 后
complete_flag=1、计数已重装但completed_callbacks=0;调用 Fake IRQ 后确认 flag 清除、完成回调计数增加且仍处于运行态; - 调用显式停止接口,确认
running和请求使能清零。
验收只可写“主机模型通过”,不能写成 STM32 硬件结果。
实验 B:逐项错误注入 #
依次把 clock、configuration、DMA link、IRQ registration 置为 0,记录返回码、running 和 callback 计数。再注入 DMA error,确认错误 flag 出现时应用错误计数仍为 0,经过 IRQ 分发后才变为 1 并停止循环请求。要求失败时不出现伪完成。
实验 C:建立一张新外设交叉表 #
从已学 UART 或 SPI 选一个“启动一次中断传输”操作,按 4.1 的七列建表。只引用对应 F407 第一方资料;至少找出一个“HAL 软件对象动作,没有直接寄存器等价物”的行和一个“一项硬件 flag,由 HAL handler 内部消费”的行。
实验 D:生成并静态审计 F407 工程 #
- 选择实际完整料号和合法 ADC 引脚,记录 CubeMX/CubeMX2 与 STM32CubeF4 版本;
- 按 7.3 配置时钟、ADC1、DMA2 Stream0/Channel0 和 NVIC;
- 生成代码但先不烧录;
- 对照本课交叉表,标注生成代码中每个 HAL 字段最终对应的寄存器/状态;
- 核对
HAL_Init -> SystemClock_Config -> MX_GPIO_Init -> MX_DMA_Init -> MX_ADC1_Init -> HAL_ADC_Start_DMA; - 核对
__HAL_LINKDMA、向量 handler 和回调是否完整; - 保存静态审计记录。生成本身不提高硬件验证状态。
实验 E:目标构建与寄存器观察计划 #
在实际具备工具链和板卡后:
- 保存完整构建日志、工具链版本、固件 hash 和 map 文件;
- 烧录前记录板卡、调试器、供电、ADC 输入条件;
- 在启动前、HAL 返回后、第一次 HT、第一次 TC 和错误回调各保存一次快照;
- 对比
NDTR、LISR flag、NVIC pending、回调计数和缓冲区; - 分别禁用 stream IT 与 NVIC,验证“flag 有但回调无”的两个断点;
- 恢复配置后再验证,禁止用直接改寄存器代替重新初始化。
这些步骤当前未执行。
实验 F:跨系列迁移审计 #
选择一个具体 F7、H5 或 H7 完整料号,使用对应 RM、数据手册和 Cube 包重填:时钟/复位、ADC kernel clock、DMA controller、request/DMAMUX、内存可达性、cache、安全属性、IRQ 名称、HAL/LL 字段和错误 flag。任何未查到的单元写“未核对”,不得复制 F407 结论。
13. 自检题 #
HAL_ADC_Start_DMA返回HAL_OK能证明什么,不能证明什么?- 为什么 HAL、LL、寄存器不是三套独立硬件?
- 一行合格的交叉映射应包含哪七类信息?
__HAL_LINKDMA为什么没有寄存器等价物?谁持有关联后的对象?- 为什么
DMA_S0CR.EN=1不能证明搬运发生? NDTR递减、TCIF0置位、ADC 完成回调发生分别证明哪一层?TCIF0已出现但 handler 不运行,应先检查哪几个关卡?- handler 已运行却无 ADC 回调,应检查什么?
- PCLK2=84 MHz、ADC 分频=/4 时 ADCCLK 是多少?为什么它不是采样率?
- 初始化结构体可以是局部对象,而
hadc1/hdma_adc1通常静态持有,原因是什么? - Host Fake 通过后仍缺哪些目标证据?
- 迁移到 H7 时,为什么不能只替换 HAL 头文件?
参考要点:API 成功只覆盖同步软件路径;三个层是同一状态转换的不同表达/观察面;交叉表包含意图、前置、HAL、LL、硬件、证据、错误;link 宏连接软件对象;EN 只表示 armed;NDTR/flag/callback 依次证明搬运、硬件完成、IRQ/HAL 分发;时钟为 21 MHz但请求还受采样、转换和触发决定;句柄会被 IRQ 长期引用;目标还需版本、构建、烧录、寄存器与电气证据;H7 的时钟域、DMA/DMAMUX、内存、cache 和寄存器都要重建映射。
14. 面试题 #
- 请解释“HAL 返回成功不等于硬件事件完成”,并给出 ADC-DMA 的五级证据。
- 你如何从
HAL_ADC_Start_DMA追到 DMA2 Stream0 的寄存器和完成回调? - 为什么读取寄存器验证 HAL 配置是合理的,而运行中用 LL 改同一外设通常危险?
- DMA
TC已置位但应用没收到数据,你会按什么顺序排查? __HAL_LINKDMA做了什么?忘记它可能表现为何种故障?- 如何区分 ADC 没产生请求、DMA 请求映射错误、NVIC 未使能和 handler 传错句柄?
- 为什么 F407 的 ADC1 Stream0/Channel0 结论不能直接迁移到 H5?
- 在带 D-Cache 的 F7/H7 上,为什么 DMA 完成仍不一定意味着 CPU 读到新数据?交叉表需新增哪些责任?
- 如何设计一份故障快照,使其既能定位首错,又不会让 ISR 本身制造时序问题?
15. 延伸思考 #
- 若 HAL 一次调用写十个寄存器,交叉表应按“API”还是按“操作意图”拆行?什么粒度最利于故障定位?
- 如果目标项目决定 ADC 使用 HAL、DMA 使用 LL,谁负责两边软件状态、停止顺序和错误恢复?是否值得承担这个组合边界?
- 寄存器快照是瞬间状态。对于写 1 清除、读后清除或高速变化的 flag,应如何使用 trace、事件计数或断点避免误读?
- 自动生成代码更新后,怎样用静态检查或单元测试发现句柄关联、IRQ handler 或配置字段被改坏?
- 在 Timer -> ADC -> DMA -> task 链路中,怎样给每一段定义时间戳和所有权证据,使延迟与丢样可定位?
16. 本节总结 #
HAL、LL 与寄存器交叉映射是一种追踪方法:从操作意图出发,逐项列出前置条件、软件对象/API、近位域动作、硬件配置/状态、成功证据和错误去向。它避免把 HAL、LL、寄存器误当成互不相关的知识,也避免把一次函数返回误写成整条硬件链路完成。
Host Fake 用静态 C11 对象验证了最小机制:时钟、配置、DMA 关联和 IRQ 路径缺一不可;circular 计数会重装并持续请求,正常停止必须显式发生;硬件完成 flag 与应用回调是两个时刻;错误也必须经过明确分发路径。它不模拟 STM32。
STM32F407xx 参考轨道把方法落实到 ADC1 -> DMA2 Stream0/Channel0:HSI/PLL/AHB/APB/ADC 分频建立运行时钟,RCC 分别控制 DMA2/ADC1 门控与复位,HAL 初始化结构体提交配置,句柄连接软件对象,ADC 请求驱动 DMA,DMA flag 经 NVIC、向量和 HAL_DMA_IRQHandler 进入 ADC 回调。当前未进行目标构建、烧录或测量,hardware_verified=false 保持不变。
17. 下一步 #
下一课 timer-trigger-routing 将把软件启动替换为 Timer TRGO,使用本课方法追踪“定时器事件 -> 内部触发路由 -> ADC 边沿选择 -> 转换事件”。随后 advanced-control-timer 深入互补输出、死区、刹车、重复计数和预装载;adc-timer-dma-pipeline 把触发、采样、DMA 和消费连成可测量流水线。调试阶段的 register-view 会把本课静态交叉表转为实时证据采集流程。
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 门控/复位、ADC 公共与实例寄存器、DMA2 stream/channel、表 44 请求映射、flag 和配置限制;适用范围仅限手册列出的器件。https://www.st.com/resource/en/reference_manual/dm00031020.pdf,访问日期:2026-10-12。
- [S2] STMicroelectronics,Description of STM32F4 HAL and low-layer drivers,UM1725 Rev 8(March 2023)。用于 STM32F4 HAL/LL 的 ADC、DMA、RCC API、句柄关联、状态返回、IRQ handler 和回调契约;实际工程仍须与所用 STM32CubeF4 包头文件核对。https://www.st.com/resource/en/user_manual/dm00105879.pdf,访问日期:2026-10-12。
- [S3] STMicroelectronics,STM32F405xx STM32F407xx datasheet,DS8626 Rev 12(March 2026)。用于器件范围、时钟和 ADC 工作条件边界;本文没有据此假定封装、引脚、板级输入或物理测量结果。https://www.st.com/resource/en/datasheet/stm32f407vg.pdf,访问日期:2026-10-12。
三份资料均来自 config/source-policy.toml 允许的 st.com 第一方域名。F7、H5、H7 仅列出迁移时必须重查的类别;由于没有选择具体目标和对应第一方资料,本课不声称它们的具体寄存器、request ID、缓存维护 API 或安全配置。