上周手里的一个项目突然卡壳了:主控用的 STM32H723,ADC1 配了 8 个通道的序列扫描,软件触发,结果莫名其妙就是启动不了转换。具体现象是调用启动函数后,ADC 的状态寄存器里ADSTART位死活不置 1,转换完成标志也一直不出现,数据寄存器里自然是空的。当时第一反应是自己配置写错了,结果翻来覆去检查代码看了大半天,愣是没看出个所以然来。
这种“序列 ADC 无法启动转换”的问题,在 H7 系列上其实不算罕见。H723 的 ADC 外设是个增强型设计,带校准、偏移校正、过采样、双 ADC 模式等一大堆功能,寄存器数量和配置复杂度比 F1/F4 系列高了不少。正因为它复杂,很多看起来“没问题”的配置组合,在硬件层面就是跑不通。这篇笔记就把这次排查的完整过程、背后的原理、以及最后定位到的问题点都记录下来,给同样卡在 H723 序列 ADC 上的朋友一个参考。
1. 问题定位:先搞清楚“没启动”到底卡在哪一步
遇到 ADC 不工作,千万别急着改配置。第一步永远是确认故障边界:到底是 ADC 模块本身没使能,还是使能了但转换没触发,还是转换跑了但结果没存进预期的地方。这三类问题的表现完全不同,排查路径也完全不同。
1.1 现象描述与影响范围
先说这次遇到的具体表现。项目里用的是 ADC1,开启 8 个通道序列扫描(SQR序列长度 8),软件触发模式,通过 DMA 把转换结果搬运到内存数组。启动流程是标准的 HAL 库调用:
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 8);按理说这个调用一执行,硬件就会自动完成一整个序列的转换,然后触发 DMA 传输。但实测时发现:HAL_ADC_Start_DMA()返回正常,HAL 库内部也没报错,可adc_buf数组内容始终是初始值 0。
用调试器挂上去看了几个关键寄存器:
ADC1->CR:ADEN位为 1,说明 ADC 已经使能;ADC1->ISR:ADRDY位为 1,说明 ADC 硬件状态机处于就绪状态;ADC1->CR:ADSTART位为 0,说明根本没有触发启动;ADC1->ISR:EOC系列标志均为 0,说明一次转换都没完成。
这个现象很关键:ADC 本身是好的,状态机也空闲,但就是启动信号没打进去。问题缩小到了“触发”这一环。而触发启动这件事,在 STM32 的 ADC 里既可以是软件直接控制,也可以由外部事件(定时器、GPIO、LPTIM 等)驱动。H723 还多了一个ADC 硬件触发选择器,需要排查的东西又多了一层。
1.2 先排除硬件与时钟层面的问题
软件排查到山穷水尽的时候,建议回头看一眼硬件。ADC 要正常工作有几个硬性前提:
- ADC 供电引脚(
VDDA、VREF+)必须有有效电压,且范围满足芯片规格要求。H723 的 VDDA 范围是 1.62V 到 3.6V,低于 1.62V 时 ADC 直接不工作; - ADC 时钟(
ADC kernel clock)必须存在。H723 的 ADC 内核时钟既可以选择 PLL2P、PLL3R 等 PLL 输出,也可以选择SYSCLK、HSE、CSI等;一旦选了 PLL 输出,而对应的 PLL 没有使能或输出频率配错,ADC 的ADEN位也能置上,但转换就是不动; - 对于 BGA/UFBGA 封装的芯片,还要检查部分引脚是否被其他功能复用占用,导致采样通道无法正确连接到内部 ADC 输入。
我用示波器量了VDDA,3.3V 稳定;用调试器读RCC->CFGR2确认 ADC12 的时钟源选择,也正常。硬件层面的常规嫌疑排除,接下来就是纯寄存器级的问题了。
2. 序列 ADC 启动机制拆解:为什么配置正常却不启动
既然硬件和时钟都没问题,那就得从 ADC 的启动机制入手,把“启动转换”这个动作掰开揉碎了看。H723 的 ADC 启动逻辑和 F1/F4 有明显差别,这也是很多人踩坑的根源。
2.1 从单次转换到序列转换的差异
先把基本概念对齐。STM32 的 ADC 支持两类转换序列:
- 规则序列(Regular sequence):由
SQR1/SQR2/SQR3/SQR4寄存器定义,最多 16 个转换,通常就是我们说的“序列 ADC”; - 注入序列(Injected sequence):由
JSQR寄存器定义,最多 4 个转换,优先级更高,适合打断规则转换插入关键测量。
单次转换指的是序列长度为 1,每次触发只采集一个通道;序列转换则是把一连串通道依次采完。H723 的规则序列最大支持 16 个通道,逐次比较器序列引脚映射也做了加强。但不管是哪种,硬件上都有一个统一的启动动作:置位ADSTART(规则)或JADSTART(注入)。
启动动作的本质是什么?是让 ADC 状态机从空闲(IDLE)状态进入等待触发(WAIT_TRIG)或采样(SAMPLING)状态。如果是软件触发,置位ADSTART的瞬间就会直接进入采样;如果是外部触发,置位ADSTART后只能在发起触发事件时才能在TRGO(触发请求输出)信号的同步下真正进入采样。
2.2 ADC 状态机与 ADSTART 位的关系
H723 参考手册里有一张 ADC 状态机状态图,虽然画得密密麻麻,但核心关系就这几条:
- 写
ADEN=1之后,硬件要经历一个启动时间(tSTAB),然后置位ADRDY; - 只有
ADRDY=1(状态机空闲)时,写ADSTART=1才会被硬件接受; - 如果
ADRDY=0,写ADSTART是无效的——这是最容易踩的第一个坑; - 一旦转换启动,
ADSTART会保持为 1,直到转换队列全部完成(或选中的模式允许中途停止)后才自动清 0。
这里有个细节:ADSTART在参考手册中的属性是“软件可以随时读写”,听起来好像任何时刻写 1 都能触发启动。但实际上,写之前你需要确保状态机在正确的位置。HAL 库里的ADC_Start_Conversion()内部会等待ADRDY,所以如果你用 HAL 库还出现 ADSTART 置不上,那问题往往出在 HAL 看不到的地方。
2.3 为什么“能配置”但“不启动”?
这是本次排查的核心问题。代码里每个寄存器都配了,HAL 函数也都调了,为什么硬件不响应?
我后来把问题归类为两大类:
第一类是启动条件不满足,最常见的是:
ADRDY没有置位就去触发启动;- 在
ADEN置位前就写了ADSTART; - 芯片处于
DEEPSLEEP或STOP模式,ADC 内核时钟被关闭。
第二类是启动动作被“降级”了,这是最容易忽略的:
- 配置了序列长度
L=0(即序列为空),此时写ADSTART虽然置位,但因为没有通道要转换,会立即结束; - 把
EXTEN触发使能配置成了00(禁止外部触发),但实际又依赖外部事件; - 软件触发和外部触发混用时,没注意到触发边沿配置(
EXTSEL+EXTEN)对ADSTART写操作的影响。
回到这次的问题:我读ADRDY是 1,ADEN也是 1,序列长度配置为 8,通道也都有效,理论上写ADSTART应该能启动。可它就是没置位。既然 HAL 库没报错,说明 HAL 认为启动条件满足,那问题只能出在 HAL 库没检查的寄存器上。
3. 实操过程:一步步复现并修复
这一节是整篇笔记的干货所在。从配置代码到查看寄存器,再到最终定位和修复,我把整个过程按实际操作的顺序记录一遍。
3.1 我的原始配置(错误示范)
先贴出有问题的初始化配置。这个配置看起来完全正常,但就是跑不起来:
static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 8; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; hadc1.Init.LowPowerAutoWait = DISABLE; hadc1.Init.LowPowerAutoPowerOff = DISABLE; hadc1.Init.BoostMode = ADC_BOOST_MODE_100KHZ_1MHZ; // 重点关注 hadc1.Init.OverSamplingMode = DISABLE; hadc1.Init.OverSamplingRatio = 0; hadc1.Init.OverSamplingShift = ADC_RIGHTBITSHIFT_NONE; hadc1.Init.SamplingTimeCommon = ADC_SAMPLINGTIME_1CYCLE_5; // 重点关注 hadc1.Init.OversamplingBypass = 0; hadc1.Init.TriggerFrequencyMode = 0; hadc1.Init.SamplingTimeCommon1 = 0; hadc1.Init.SamplingTimeCommon2 = 0; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } }通道配置部分也是常规操作,把 8 个通道依次加入序列:
static void MX_ADC1_Channel_Config(void) { ADC_ChannelConfTypeDef sConfig = {0}; for (uint8_t ch = 0; ch < 8; ch++) { sConfig.Channel = ADC_CHANNEL_0 + ch; // 使用 0~7 通道 sConfig.Rank = ADC_REGULAR_RANK_1 + ch; sConfig.SamplingTime = ADC_SAMPLINGTIME_1CYCLE_5; sConfig.SingleDiff = ADC_SINGLE_ENDED; sConfig.OffsetNumber = ADC_OFFSET_NONE; sConfig.Offset = 0; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } } }这一切配置完,再去调用HAL_ADC_Start_DMA(),结果就是“看起来成功、实际没启动”。
3.2 逐步排查的现场记录
排查过程我按“由外向里”的顺序走了一遍:
第一步,确认 HAL 库内部状态。在HAL_ADC_Start_DMA()里打断点,跟踪它的执行路径,确认它正确走到了ADC_Start_Conversion()。没问题。
第二步,确认启动写操作已执行到寄存器。在HAL_ADC_Start_DMA()返回后,用调试器直接读ADC1->CR。结果ADSTART位仍然是 0。这就奇怪了:HAL 里明明执行了ADC1->CR |= ADC_CR_ADSTART,为什么没有效果?
第三步,翻参考手册对照寄存器属性。这一翻发现了关键疑点。H723 的 ADC_CR 寄存器里有一个ADCAL(校准)位和ADVREGEN(电压调节器使能)位。在 ADC 初始化过程中,如果ADVREGEN没有正确配置,ADC 内部参考电压缓冲器没有工作,那么 ADC 即使 ADEN=1、ADRDY=1,也不能真正开始转换。这就像汽车发动机点火了、转速表也动了,但变速箱没挂上挡,轮子自然不走。
我立刻去查ADC1->CR的ADVREGEN位。果然,它是 0。
3.3 修复:使能 ADC 内部电压调节器
这个问题在 STM32H7 系列上非常典型。H7 的 ADC 内部有独立的电压调节器(ADC Voltage Regulator),需要软件显式使能后才能工作。F1/F4 系列没有这个要求,所以从旧平台迁移过来的代码很容易漏掉这一步。
修复方式有两种:一是用 HAL 库在初始化前手动开启,二是直接在寄存器里操作。我推荐用 HAL 库标准方式,这样维护方便:
// 在 MX_ADC1_Init() 开头、HAL_ADC_Init() 之前加这一段 static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // ... 其他配置保持不变 ... __HAL_RCC_ADC12_CLK_ENABLE(); // 使能 ADC 内部电压调节器 ADC1->CR &= ~ADC_CR_ADVREGEN; ADC1->CR |= ADC_CR_ADVREGEN; // 等待电压调节器稳定 for (volatile uint32_t i = 0; i < 1000; i++); if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } }其实更省心的办法是直接用 HAL 库提供的宏:__HAL_ADC_ENABLE和__HAL_ADC_DISABLE。但关键在于,必须在HAL_ADC_Init()之前把ADVREGEN配置好,否则 HAL 初始化时虽然会设置 ADON,但调节器还没稳定,状态机会处于异常。
加了这段之后重新编译烧录,ADSTART正常置位,EOC标志正常置位,DMA 也正常把数据搬进了缓冲区。问题解决。
3.4 验证转换结果的完整过程
修复后没有直接收货,我又做了一遍完整验证:
- 用调试器观察
ADC1->DR是否在每次转换后变化; - 在 DMA 传输完成中断里翻转一个 GPIO,用示波器看转换周期是否稳定;
- 用外部电压源给 ADC1_IN0 通道加 1.0V,采集 100 次,统计平均值和方差,确认转换精度正常。
实测 100 次采样的结果平均值在 0.998V 左右,方差在正常范围内,确认整个链路恢复健康。
4. 常见问题与排查技巧实录
踩完这个坑后,我把 H723 序列 ADC 启动相关的常见问题和排查方法整理了一下。这些问题有的是我自己遇到过的,有的是同事和网友踩过、最后确认了根因的,汇总在这里,方便以后遇到类似问题直接对照。
4.1 问题排查速查表
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
ADEN无法置 1 | ADC 内核时钟未使能或频率超限 | 检查RCC中 ADC12 时钟源与分频;确认 ADC 时钟频率在数据手册规格内 |
ADEN=1但ADRDY=0 | 内部电压调节器未使能 | 检查ADC_CR.ADVREGEN,启动前必须使能并等待稳定 |
ADRDY=1但写ADSTART无效 | 序列长度 L=0 或通道配置无效 | 检查SQR1的 L 字段和SQ1~SQ16通道选择 |
ADSTART置位后立即清 0 | 外部触发源/触发边沿配置错误 | 检查EXTEN与EXTSEL,确认触发事件是否真正到达 |
| 转换完成但 DMA 不搬运 | DMA 请求映射或DMACFG位错误 | 检查DMA的请求选择是否对应 ADC1,确认DMACFG=1 |
| 序列中途卡住,只完成前几个通道 | 采样时间与通道阻抗匹配不当 | 根据信号源内阻调整采样时间,必要时改为更长采样周期 |
| 校准后转换值整体偏差 | 校准因子未正确加载 | 等待ADCAL完成后,读取CALFACT并确认其不为 0 |
这个表虽然不能覆盖所有异常,但覆盖了 90% 以上的“序列 ADC 不启动”情况。
4.2 三个容易被忽视的细节
细节一:H723 的 ADC 外部触发选择器和触发源不是一回事。H723 的EXTEN/EXTSEL只是“触发信号入口选择”,真正到达 ADC 的触发信号还要经过一个触发选择器(ADCx_TRGSELR寄存器)。如果你使用的外部触发事件没有在TRGSEL里选上,即使EXTEN配置了,ADSTART也不会被触发。这一点在 H7 全系列都适用,非常容易踩。
细节二:低功耗自动关断(LOWPOWER_AUTOPOWEROFF)会影响外部触发。如果开启了自动关断功能,ADC 会在每次转换完成后自动关闭调节器,然后在下一个触发事件来临时重新启动。这个模式下的启动时间明显变长,如果触发事件来得太快,ADSTART会处于“待启动”状态,看起来像没启动。解决方法是检查触发事件频率是否超过了 ADC 启动时间,或者关闭自动关断。
细节三:校准完成标志不能只查ADCAL位。有个容易误用的地方是:写ADCAL=1后,大家习惯等它自动清 0 就认为校准完成。但 H723 在等待ADCAL清 0 后,还需要读取CALFACT寄存器确认校准因子是否有效。如果CALFACT全 0,说明校准流程没走完,此时虽然能启动转换,精度和稳定性都不可靠。
4.3 调试工具与手段建议
H723 这种高性能芯片,调试手段用好了事半功倍。推荐几个实战中非常好用的手段:
调试器 Live Expression 窗口观察寄存器位。把
ADC1->CR、ADC1->ISR、ADC1->SQR1加到 Live Expression 里,实时刷新看关键位变化。这次排查ADSTART是否置位就是靠这一招直接定位的。DMA 搬运数据抓拍。在 DMA 传输完成中断里设一个断点,然后在内存窗口看
adc_buf数组的前几个值。如果第一个非 0,说明采集链路通了;如果全 0,问题基本在 ADC 启动或 DMA 配置上。用逻辑分析仪观察外部触发信号。如果用的是外部触发模式,强烈建议用逻辑分析仪抓一下触发源引脚的波形。很多“ADC 不启动”的问题,其实是触发信号根本没到,或者是极性/边沿配反了。
临时切换软件触发做交叉验证。如果外部触发模式下死活不启动,可以临时改成软件触发,跑一遍
HAL_ADC_Start()。如果软件触发能转,说明问题在触发链路;如果软件触发也不转,说明 ADC 初始化或者状态机有问题。这个方法能快速缩小排查范围。
5. 关于 STM32H723 ADC 的几个延伸建议
问题解决之后,我又花了点时间把 H723 上 ADC 的常用特性做了个梳理。这里挑几个和“序列转换”强相关的点,作为补充建议。
5.1 合理配置采样时间,别为了“快”牺牲稳定性
H723 的 ADC 采样时间可以从 1.5 个 ADC 时钟周期一直选到 810.5 个周期。很多新手为了追求采样率,直接选了 1.5 周期。这个值在信号源内阻很低(比如运放直接驱动)的时候没问题,但如果信号源内阻比较大(比如通过 10kΩ 电阻分压),采样电容还没充满电就开始转换了,序列后半段的通道结果会明显偏小或者跳动。用这个原则:信号源阻抗每增加 1kΩ,建议至少增加 1.5 个周期的采样时间。实际产品里我一般选 8.5~32.5 周期,能兼顾速度和稳定性。
5.2 双 ADC 模式下的启动注意点
H723 有 ADC1 和 ADC2 两个独立的 ADC,支持双 ADC 同步采样、交叉采样等模式。在双 ADC 模式下,两个 ADC 的启动是同步的,但要注意:
- 双 ADC 模式下
ADSTART位是独立控制的,但转换结果是共享ADC_DR寄存器的; - 如果只配置了 ADC1 的序列,却开启了双 ADC 模式,ADC2 可能因为序列为空导致整个同步启动失败;
- DMA 请求在双 ADC 模式下也要配套修改,需要特别注意 DMA 的数据长度和传输方向配置。
遇到双 ADC 启动异常时,先把双 ADC 模式关掉,分别单独验证两个 ADC 都能正常转换,再开同步模式。
5.3 校准与偏移校正对序列转换的意义
H723 的 ADC 支持出厂校准,也能自动校准。自动校准流程是:
// 使能校准 ADC1->CR |= ADC_CR_ADCAL; // 等待 ADCAL 自动清 0 while ((ADC1->CR & ADC_CR_ADCAL) != 0); // 校准因子会加载到 CALFACT 寄存器校准完成后,CALFACT会保存一个非零的校准值。如果发现CALFACT为 0,说明校准没成功。另外,H723 还支持在每次上电后重新校准,建议在温漂比较大的环境中,每次 ADC 初始化时都校准一次,能明显改善序列通道之间的一致性。
5.4 过采样功能:序列转换的精度利器
H723 的 ADC 内置过采样单元,可以硬件完成多次采样并求平均。比如设置 16 倍过采样,相当于把采样率降到原来的 1/16,但信噪比提升约 12dB(2 bit)。如果你做的应用是采集缓慢变化信号(比如温度、电池电压),同时又不追求速度,强推开启过采样。
过采样配置要注意:
hadc1.Init.OverSamplingMode = ADC_OVERSAMPLING_ENABLE; hadc1.Init.OverSamplingRatio = 16; // 过采样率 hadc1.Init.OverSamplingShift = ADC_RIGHTBITSHIFT_4; // 右移 4 位,相当于除以 16这里的OverSamplingShift很关键:过采样率是 16 时,累加结果会变成 16 倍原始值,如果不右移 4 位,最终读数会偏大 16 倍。当然,如果你就是想要更高的分辨率(用 16 位结果保存),可以少移几位甚至不移,本质上是权衡分辨率和满量程的关系。
6. 写在最后:排查序列 ADC 问题的一般方法论
这次问题从出现到解决,花了大概大半天时间。复盘下来,最有价值的不是“找到了ADVREGEN没使能”这一个点,而是整个排查过程沉淀下来的方法论:
第一,先确认边界再动手改代码。任何 ADC 启动问题,都先区分是硬件问题、时钟问题、还是配置问题。用调试器读几个关键寄存器,比盲目改参数高效得多。
第二,理解启动流程的每个环节。STM32 的 ADC 启动,从内核时钟到触发选择再到状态机,是一个完整的链路。中间任何一环断了,现象都是“不启动”。把这串链路背下来,排查的时候心里就有地图了。
第三,善于利用 HAL 库源码辅助定位。HAL 库里HAL_ADC_Start()的源码只有几十行,找出来读一遍,看它到底操作了哪些寄存器、依次序是什么。很多时候问题就出在 HAL 调用和你手动配置之间的时序错位上。
第四,别忽略勘误手册和数据手册的更新。芯片厂商偶尔会更新参考手册,补充一些之前没写清楚的行为说明。H723 这样较新的芯片尤其如此。遇到疑难问题时,去官网下载最新的勘误表翻一翻,有时候直接就有答案。
最后说一个我自己的操作习惯:每次新项目初始化 ADC 时,我会把ADVREGEN使能、校准、等待ADRDY这三件事按顺序写成一个固定流程,无论用什么芯片、什么库,都保证这个流程执行完整。这就像开车前系安全带一样,看着多花了几行代码,但能避免一整个下午的排查时间。如果你也经常和 H7 系列的 ADC 打交道,建议把这个习惯也加进你的初始化流程里。