
最近调试一块基于 STM32H723 的自制板卡遇到了一个非常典型的 ADC 问题序列 ADC 配置完成后软件触发下去转换就是不启动。寄存器翻来覆去查了好几遍HAL 也换了几种写法最后沉下心把启动链路里的每一个门控条件都过了一遍才定位到问题。说实话这种“配置看着没问题但实际不动”的故障比直接报错难查得多。这篇应用笔记就把我这次的排查过程、H723 序列 ADC 的启动机制以及最终能直接复用的修复方案整理出来希望能帮你少走几趟弯路。1. 从症状到现场先把“不能启动”这件事拆清楚1.1 现场症状与初步排查环境我的硬件环境是自研板卡主控 STM32H723ZET6VDDA 供电 3.3VADC1 的 6 个通道做规则序列扫描采样对象是几路电压和电流信号。软件框架上没用 CubeMX 自动生成代码而是用寄存器直接配置这样更容易控制 ADC 的初始化顺序。现象很明确初始化函数执行完往 ADC_CR 里写 ADSTART1 之后ADC 始终不产生 EOC/EOS 标志也没有 DMA 请求产生。用调试器单步的时候发现ADSTART 位的状态很值得琢磨——有时候写完读出来还是 1但 ISR 里的 ADRDY、EOC 标志就那么干巴巴地站着什么都没有有时候写完再读 ADSTART 已经是 0但同样没有任何数据。这两种情况反映的内在原因其实不一样后面章节会专门展开。刚开始我也走了一些弯路。第一反应是检查 GPIO 配置确认所有模拟通道引脚都设成了 analog mode没有问题。然后又怀疑是不是采样时间太短导致采样电容没充满把采样周期调到最大依然无效。最后打开参考手册的 ADC 启动流程图一步步对照寄存器门控条件才找到根子上的问题。1.2 序列 ADC 的启动流程到底是什么很多朋友用 STM32F1/F4 的 ADC 用得顺手会觉得“ADC 就那几步”。但 H7 系列的 ADC 结构和启动流程有明显区别尤其是校准、电压调节器、通道预选这些环节任何一个没有就位后面的启动命令都可能被硬件状态机直接忽略。H723 的序列 ADC本质上是规则组转换的扫描模式。你可以把规则组理解成一条流水线SQR1、SQR2、SQR3、SQR4 这些寄存器里按顺序写好了要采的通道号当触发信号到来时ADC 内部状态机从 SQ1 开始逐个通道采样转换。序列长度由 SQR1 寄存器里的 L 位段决定L 的数值加 1就是实际转换的通道个数。启动一个序列转换完整流程可以概括为使能 ADC 内核时钟和总线时钟配置 ADC 时钟源。ADC 退出深度掉电模式如果需要使能内部电压调节器。等待电压调节器稳定。执行校准等待校准完成并检查校准失败标志。配置规则序列寄存器 SQRx选定扫描方向和触发源。配置通道预选寄存器 PCSEL打开对应模拟输入通道。设置 ADEN1 使能 ADC等待 ADRDY1。设置 ADSTART1 发起启动请求。等待转换结束读取数据。看起来简单但第 4 步到第 8 步之间的门控关系非常严格。尤其是 ADSTART 的置位并不是你写 1 它就一定会去转换。硬件会检查一系列前置条件不符合条件时这个启动请求要么被无视要么悬在那里等待外部事件。这也是“无法启动转换”这个症状最让人困惑的地方——软件上你确实发出了启动指令但硬件没有理会。2. 为什么 H723 的序列 ADC 容易“卡住”核心机制剖析2.1 启动序列前必须完成的校准门控H7 系列的 ADC 在使能和启动转换之前必须完成校准这一点比 F1/F4 更严格。官方的说法是校准用于确定高低电平的偏移和增益误差转换结果才能准确。但从机制上讲校准是否完成直接影响后续 ADSTART 命令能否被执行。如果你在 ADCAL1 校准进行中或者干脆还没发起校准时就去置位 ADSTART转换请求是不会被接受的。硬件状态机在这里设置了一道隐形的门校准未完成采样链路不认为是有效的所以不会启动新的序列转换。更隐蔽的是这道门并不会产生任何错误中断你只会看到“启动了但又没完全启动”的诡异状态。我这次遇到的第一个问题就在这里。程序里有一个上电自检逻辑会在初始化阶段先做一次系统自检其中有个看门狗喂狗操作打断了校准流程。校准函数本身没有问题但被中断后我的代码没有重新等待 ADCAL 清零就直接返回初始化流程继续往下走最后当 ADC 校准还挂着的时候写着 ADSTART1自然就启动不了。这里必须强调校准完成等待不能省而且要通过读寄存器确认 ADCAL 位从 1 变成 0不要只做一个延时。调试时更要注意 ISR 里的 EOCALF 标志。这个标志如果被置位说明校准过程异常结束需要软件清除后重新校准。否则即使你看到 ADCAL 已经清零校准结果也是不可信的。2.2 软件触发与外部触发的配置边界我在项目初期希望先用软件触发方式跑通序列但真正干活的时候才发现CFGR 寄存器里的 EXTEN 位段被默认值带偏了。这个位段控制外部触发的使能状态如果 EXTEN 不为 0ADC 就被配置成等待外部事件触发转换。这时你即使写入 ADSTART1ADSTART 位可能会变成 1但 ADC 不会真正开始转换只会等待你配置好的那个外部触发源到来。很多从 CubeMX 切到寄存器开发的工程师容易在这里踩坑。CubeMX 里你选 Software Trigger它确实会把 EXTEN 置 0但如果你自己操作寄存器或者复用了一段其他项目的初始化代码EXTEN 很容易残留成某个外部触发模式。当时我读取 CFGR 时EXTEN 的值是 0b01对应的是定时器触发的上升沿。而我的系统里这个定时器根本没有初始化触发事件永远不会来ADC 就这么一直干等。处理方式很简单软件触发的场景下显式把 EXTEN 清 0然后置位 ADSTART。如果你想用外部触发也要把 EXTSEL 配置正确并且确认触发源确实在产生有效边沿。用逻辑分析仪或者示波器测触发引脚是最直接的验证手段不要只靠代码逻辑推断。这里还顺带解释一个细节。ADSTART 位在序列转换期间会保持 1转换完成后自动清零。如果你观察到 ADSTART1 但没有任何转换标志很可能是它在等待外部触发如果你观察到 ADSTART 立刻回到 0说明硬件根本没接受启动请求需要回头查校准、使能、时钟这些更基本的前置条件。2.3 序列地址生成和扫描方向的坑序列 ADC 在 H7 上还有一个容易忽略的寄存器是 SCANDIR它控制序列扫描的方向。默认是正向扫描也就是从 SQ1 排到序列末尾如果把它改成反向转换就会从最后一个序列项开始逐个往前扫。这本身不会导致“无法启动”但它会直接影响你对数据的判断。比如你明明在 SQ1 里放了 ADC_INP4SQ2 里放了 ADC_INP5但结果寄存器第一次读出来的是 INP5第二次才是 INP4你会怀疑自己的配置是不是写错了甚至误判为序列没按预期工作。另外H7 的 SQR 寄存器组可以支持很长序列H723 最高支持 16 个规则转换序列。要注意序列长度 L 的编码方式是“0 表示 1 个1 表示 2 个”也就是说配 6 个通道的序列时L 要写 5。我见过有人把序列长度理解成“配置的最后一个索引”导致只转换了第一个或者前几个通道然后拿着逻辑分析仪数据抱怨 ADC 坏了。这类问题虽然不直接表现为“无法启动”但在排查过程中很容易混淆视听。我的建议是排查时先用最小序列比如只采一个通道跑通后再逐步扩展到多通道这样能快速区分是序列配置问题还是启动门控问题。3. 寄存器级排查路径一步步定位问题现场3.1 第一步ADC 内核时钟和电源状态拿到“ADC 无法启动”的问题我建议不要先看转换代码而是先把时钟和电源状态确认干净。H7 系列 ADC 挂在 APB2 总线上但它的内核时钟需要从 RCC 配置里选择。如果 ADC 内核时钟没有正常使能即使你通过 APB 总线访问寄存器没问题模拟转换链路也完全无法工作。具体到 H723重点检查 RCC_CCIPR 里的 ADC12 时钟源选择。这个位段可以选择 PLL2、PLL3、系统时钟分频等来源。很多 bootloader 或者早期初始化代码会在别处把 PLL 改掉导致 ADC 的时钟源实际停摆。此时寄存器能写、校准可能也能启动但转换就是不出结果非常难排查。电源状态上H7 的 ADC 有深度掉电模式由 ADC_CR 的 DEEPPWD 控制。如果这个位为 1ADC 内核处于深度掉电必须先把 DEEPPWD 清 0然后等一段时间再操作其他寄存器。同时还要确认 ADVREGEN 内部电压调节器已经使能。F1/F4 上没有这个东西很多迁过来的老手容易忽略。正确的上电顺序是清除 DEEPPWD置位 ADVREGEN延时等待调节器稳定然后才可以执行校准。如果你在代码里用 HAL 库的 HAL_ADC_Init 和 HAL_ADCEx_Calibration_Start这些细节 HAL 会帮你处理但寄存器方式下这一步必须自己想清楚。3.2 第二步通道预选和 GPIO 模拟模式H7 系列 ADC 与 F4 一个很重要的不同点是 PCSEL 寄存器。它用来选择哪些模拟通道被真正连接到 ADC 内部采样电路。如果你在 SQR 里写了通道号但没有在 PCSEL 里使能对应通道输入通路就是断开的。这种情况下转换是否启动取决于具体实现但即使启动了采样也可能采集不到正确外部信号或者反复采到内部默认值。我建议把 PCSEL 当成一个强约束条件来检查。每个要用的通道都要在 PCSEL 寄存器里把对应位置 1。如果使用差分输入还要把 DIFSEL 寄存器里对应差分使能位置为 1。只有单端采样时DIFSEL 对应位保持 0。这个步骤在 CubeMX 中会在初始化代码里自动生成但在手写寄存器时很容易漏掉。GPIO 侧的模拟模式同样重要。不是说设成模拟模式就能保证正常而是如果忘了设引脚的数字输入缓冲器会一直工作可能引入额外噪声甚至造成通道间串扰。以序列 ADC 多通道采样来说一个通道的 IO 状态影响另一个通道读数的情况我在实际项目中见过不止一次。检查方法是逐脚确认 GPIOx_MODER 寄存器里对应位是 0b11。顺便看一下 ASW 开关但如果用标准 GPIO 配置只要 MODER 正确就够了。3.3 第三步SQR 序列长度和通道排序序列配置的核心是 SQR1 到 SQR4 这几个寄存器。H723 最多支持 16 个规则序列项SQR1 的高四位 L 控制序列长度其余位段依次存放 SQ1 到 SQ4SQR2 存放 SQ5 到 SQ9SQR3 存放 SQ10 到 SQ14SQR4 存放 SQ15 和 SQ16。我习惯先在纸上把通道顺序列出来比如 SQ1ADC_INP4、SQ2ADC_INP6、SQ3ADC_INP8然后转成寄存器需要的位段配置。这里要注意H7 的序列是从 SQ1 开始的而 L 位是“序列项数减一”。如果 L0只有 SQ1 会被转换如果 L2SQ1、SQ2、SQ3 三个都会被转换。如果你填好了 SQ1 到 SQ3却忘了把 L 改成 2那么只会转换一个通道看起来就像序列功能没工作。SQR 寄存器里没有用到的位段最好显式清 0。有些初始化代码是从别处拷贝过来的上一版配置在多通道序列中写入了旧值如果没有全部清掉当前序列可能会带上意外通道。这样即使启动没问题数据也不是你想要的那一路。3.4 第四步从 ISR/CR 寄存器判断转换机状态当一切配置看起来都正常但转换就是不动时直接读寄存器状态比反复改配置有效得多。我常用的几个判断点位是ADC_ISR.ADRDYADC 是否已经使能并进入待命状态。如果为 0说明 ADEN 没有正确配置或等待时间不够。ADC_ISR.EOCALF校准是否失败。这个标志为 1 时后续转换不可信。ADC_CR.ADSTART是否发出过启动命令以及是否还在等待。ADC_CFGR.EXTEN触发模式是否残留外部触发。ADC_CSR如果是多 ADC或者对应 ADC 的 ISR.EOC/EOS是否已经有转换完成事件。把这些状态组合起来看基本能判断出启动链卡在哪一环。比如 ADSTART1、ADRDY1、但 EOC 一直为 0这种情况下重点关注触发源是否到位ADSTART0、ADRDY0则需要回头查 ADC 使能流程ADSTART0、ADRDY1、EOCALF1那就要先解决校准异常。我建议在调试器里建立一个 Watch 窗口把这几个寄存器位放在一起单步执行到 ADSTART 写入之后观察它们的变化。这个习惯能帮你快速定位 80% 的 ADC 启动问题。4. 三个真实案例和对应修复4.1 案例一ADCAL 没做就写 ADSTART转换被内部状态机拒绝这个案例就是我这次项目的直接问题。在初始化序列中我先写了 ADCAL1但是没有等待 ADCAL 清零代码就往下执行了 ADC 通道配置和 ADEN 使能。由于校准过程还在进行后续使能和启动命令都被状态机挂起或忽略。最终表现是 ADSTART 写入后读出来是 0什么转换事件都没有。修复方式分两步。第一步在校准代码里加严格的状态轮询置位 ADCAL 后循环等待 ADCAL 清零同时在 EOCALF 置位时做错误处理。第二步在使能 ADC 时不要在校准完成前进行 ADEN 置位。这里的先后顺序不能颠倒。我最终把代码结构调整成清 DEEPPWD置位 ADVREGEN。延时等待调节器稳定。校准并等待完成。配置 PCSEL、SQR、CFGR。ADEN 置位并等待 ADRDY。ADSTART 置位。改完后序列转换一次通过。这个案例给我的教训是不要把“延时足够”当作“硬件就绪”的依据一定要读标志位。尤其是在有 RTOS 或者中断抢占的环境下固定延时极其不可靠。4.2 案例二EXTEN 残留外部触发ADSTART 设置了但一直等触发这是我帮一个朋友排查时遇到的情况。他用的也是一块 H723 板子初始化代码来自一个电机控制工程里面配置过定时器触发 ADC。在新项目里他没有用这个定时器但 EXTEN 位段没有被重置依然保持外部触发使能。于是每次软件置位 ADSTART 后ADSTART 位保持 1但转换不进行因为没有外部脉冲来启动转换。从调试器看ADRDY 正常EOCALF 也正常PCSEL、SQR 都没问题唯一可疑的就是 ADSTART 一直停在 1。我当时直接在 CFGR 寄存器里把 EXTEN 清 0然后重新写了一次 ADSTART转换立即发生。这个案例提醒我寄存器初始化不能只写“我关心的那几位”还要把所有可能影响本次功能的位段一次性配置成确定值。最稳妥的做法是先对整个 CFGR 做掩码清零再设置需要的值而不是只在原值基础上置位。比如ADC1-CFGR ~ADC_CFGR_EXTEN_Msk; ADC1-CFGR ~ADC_CFGR_CONT; ADC1-CFGR ~ADC_CFGR_SCANDIR;这样就不会被此前遗留的位值干扰。4.3 案例三DMA 配置与序列长度不匹配EOC 不产生还有一个常见现象是序列本身能启动但转换到一半卡住给外界的表现依然像“无法启动转换”。比如配置了 6 通道序列转换同时使能 DMA 连续传输但 DMA 传输长度设置成了 2。这样 DMA 传输完两个数据后就会停止而 ADC 如果配置成依赖 DMA 的溢出处理或者连续模式后续序列转换请求就会被阻塞EOC 不再产生。此类问题的排查要点是把 DMA 和 ADC 解耦测试。先关闭 DMA只用中断模式跑序列看 ADC 是否能够连续产生 EOC/EOS。如果能就说明 ADC 本身没问题矛盾集中在 DMA 配置上。然后把 DMA 的传输长度设成与序列通道数一致并确认 DMA 循环模式配置正确。H7 的 ADC DMA 配置还涉及 DMACFG 位它决定了 DMA 请求是每次转换后产生还是序列结束后产生这个位不同对 DMA 传输长度的要求也不一样。最终我采用的配置是 DMACFG0每个规则通道转换完成都触发一次 DMA 请求DMA 传输总数为序列通道数。这样每个通道的数据都能及时转移到内存也方便后续做数据处理。如果选择 DMACFG1DMA 请求会在序列结束以后统一产生此时传输长度设成 1 就够了但如果序列数据量大内存压力会集中爆发。具体选哪个要看应用场景但无论选哪个都要保证 DMA 与 ADC 配置逻辑一致不然就会出现“启动后不转换”或者“转换到一半罢工”的怪现象。5. 可直接抄的参考配置模板H723 序列 ADC DMA5.1 最小配置代码与关键注释下面这份代码是我验证通过的最小序列 ADC 配置3 个通道软件触发DMA 模式传输。编译环境是 Keil MDK使用 CMSIS 寄存器定义。每句关键操作后面都写了为什么这么做。/* 1. 使能 ADC1 总线时钟和内核时钟 */ __HAL_RCC_ADC12_CLK_ENABLE(); /* 2. 配置 ADC12 内核时钟源这里选择 PLL2P实测稳定 */ RCC-CCIPR ~RCC_CCIPR_ADC12SEL; RCC-CCIPR | RCC_CCIPR_ADC12SEL_0; /* 根据实际时钟树调整 */ /* 3. 让 ADC 退出深度掉电并打开内部电压调节器 */ ADC1-CR ~ADC_CR_DEEPPWD; ADC1-CR | ADC_CR_ADVREGEN; /* 4. 延时等待电压调节器稳定 */ for (volatile int i 0; i 1000; i); /* 5. 清校准标志并开始校准 */ ADC1-CR ~ADC_CR_ADCALDIF; /* 单端校准 */ ADC1-CR ~ADC_CR_ADCALL; /* 校准所有通道 */ ADC1-CR | ADC_CR_ADCAL; while (ADC1-CR ADC_CR_ADCAL); /* 必须等待校准完成 */ /* 6. 校准失败处理 */ if (ADC1-ISR ADC_ISR_EOCALF) { ADC1-ISR | ADC_ISR_EOCALF; /* 这里可以重新校准或者上报错误 */ } /* 7. 配置规则序列3 个通道直接配置 SQR1 和 SQR2 */ ADC1-SQR1 (2UL ADC_SQR1_L_Pos); /* L2表示 3 个转换 */ ADC1-SQR1 | (4UL ADC_SQR1_SQ1_Pos); /* SQ1 INP4 */ ADC1-SQR2 | (6UL ADC_SQR2_SQ2_Pos); /* SQ2 INP6 */ ADC1-SQR2 | (8UL ADC_SQR2_SQ3_Pos); /* SQ3 INP8 */ /* 8. 使能通道预选 */ ADC1-PCSEL | (1UL 4) | (1UL 6) | (1UL 8); /* 9. 配置转换模式单次序列、软件触发、数据右对齐 */ ADC1-CFGR ~ADC_CFGR_CONT; /* 单次模式不连续转换 */ ADC1-CFGR ~ADC_CFGR_EXTEN_Msk; /* 软件触发 */ ADC1-CFGR ~ADC_CFGR_SCANDIR; /* 正向扫描 */ ADC1-CFGR ~ADC_CFGR_ALIGN; /* 右对齐 */ /* 10. 使能 ADC 并等待就绪 */ ADC1-CR | ADC_CR_ADEN; while (!(ADC1-ISR ADC_ISR_ADRDY)); /* 11. 触发序列转换 */ ADC1-CR | ADC_CR_ADSTART;这段代码在调试时可以在第 11 步后面打断点然后观察 ISR 是否出现 EOC 或者 EOS。如果你的项目里已经有 DMA可以把 DMA 配置对接到第 9 步之前并设置 DMAEN 位和 DMACFG 位。5.2 参数选择的思路这里的采样通道、序列顺序、时钟源都只是参考。真正的关键在于理解每个参数对启动链的影响。序列长度 L 我选择 2是因为实际需要 3 个通道。如果未来要扩展到 6 个通道就把 L 改成 5然后按顺序填入 SQR1、SQR2、SQR3 对应位段。千万不要一次性把 16 个位段都填满再改 L那样排查时很难定位到具体哪个位写错了。时钟源选择 PLL2P 是因为我这个板卡上用系统时钟分频得到 ADC 时钟时实际频率超过了 H723 在 16 位模式下的上限。如果你也是高分辨率模式一定要先查参考手册的频率表再决定 CKMODE 和时钟分频。触发方式我建议先用软件触发把链路跑通。软件触发意味着 EXTEN0ADSTART 写 1 就是启动不存在等待外部事件的问题。等软件触发和多通道中断或者 DMA 都正常了再加外部触发这样每一步的故障边界都清晰。6. 防踩坑清单与后续扩展6.1 快速检查清单每次遇到 H723 序列 ADC 无法启动转换我都会按下面这张表排查。这张表也可以直接贴到你的调试笔记里。检查项寄存器/标志正确状态常见错误ADC 内核时钟RCC_CCIPR.ADC12SEL对应时钟源已使能时钟源未使能或选错深度掉电ADC_CR.DEEPPWD0初始化时未退出掉电电压调节器ADC_CR.ADVREGEN1未使能调节器校准完成ADC_CR.ADCAL0校准过程被跳过校准失败ADC_ISR.EOCALF0校准超时未清除ADC 使能ADC_ISR.ADRDY1ADEN 后未等待就绪通道预选ADC_PCSEL每个通道对应位为 1只配了 SQR 没配 PCSEL触发模式ADC_CFGR.EXTEN软件触发时为 0残留外部触发配置序列长度ADC_SQR1.LN-1误填成通道数启动命令ADC_CR.ADSTART转换结束后自动清 0写 1 但未查状态这张表并不是替代参考手册而是把最常出问题的几个门控点提前暴露出来。碰到问题时按顺序查一圈比反复猜测有效得多。6.2 后续可扩展方向多 ADC 同步与高级触发序列 ADC 跑通以后还可以在 H723 上继续做两件事。第一件事是多 ADC 同步采样ADC1 和 ADC2 可以配置成同步规则模式在同一个触发源下并行采集两路序列适合电压电流需要严格同时采样的场景。第二件事是加入定时器触发或者外部事件触发让 ADC 转换与 PWM 周期对齐这在电机控制、开关电源里非常有用。我个人的体会是H723 的 ADC 功能很强但它的启动链路比传统 F1/F4 复杂得多。多花一点时间把启动顺序和寄存器门控条件搞清楚后面写再多通道扫描也不会心虚。也建议你在工程里把 ADC 初始化代码封装成独立模块把“校准完成”“等待就绪”这些状态都用返回值反馈出来。这样以后不管是换 MCU 还是改采样通道都能第一时间知道问题出在哪一环而不是拿着示波器从头猜到尾。