拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32H743 低功耗实战:ADC3 安全禁用与重新校准指南

先交代一下背景我最近在一个用 STM32H743 的电池供电设备上做功耗优化ADC3 只在开机的时候采一次内部温度之后就再也不需要它了。第一次想把 ADC3 disable 掉的时候我还以为就是调用一下HAL_ADC_Stop然后关个时钟的事结果实际调下来才发现“禁用 ADC3”这个操作牵扯到的寄存器状态、HAL 库状态机、电源域关系比我想象的要细得多。如果你的项目也遇到过类似情况或者正准备给 H743 的 ADC3 加一个“用完即关”的逻辑那这篇内容应该能帮你少踩几个坑。这篇东西我尽量按“先讲原理、再给操作、最后列问题”的方式来写代码基于 STM32CubeMX HAL 库寄存器细节也会点到因为只抄代码不搞懂状态机换个场景照样卡住。1. 为什么需要禁用ADC3能不能不关1.1 ADC3 在 H743 里是什么地位STM32H743 一共有 ADC1、ADC2、ADC3 三个 ADC都是 12 位最高采样速率可以跑到 5Msps 左右。其中 ADC1 和 ADC2 可以组成双 ADC 模式ADC3 更像是独立工作的第三路采集单元它有自己的通道映射比如部分封装上可以接到内部温度传感器、VREFINT、VBAT 通道也可以配合 DFSDM、定时器触发做工业采样。很多人在 CubeMX 里初始化 ADC3是因为它的通道编号和 ADC1/2 不同能避开 GPIO 复用冲突。但这也带来一个问题ADC3 一旦被初始化并且跑起来在 H7 的整个电源体系里它就属于“活的”外设只要时钟没有关、使能位没有清零它就会持续对 VDDA 产生消耗哪怕你不再调用它。1.2 禁用 ADC3 要解决的三个真实问题第一个问题是功耗。电池供电设备在待机时每一百微安都要抠。ADC3 如果一直霸占着模拟电源和时钟哪怕没有在转换芯片内部为 ADC 供电的部分仍然在工作整机待机电流可能差出几十微安甚至更多。对便携仪表、传感器节点这类应用来说这就是不可接受的浪费。第二个问题是信号干扰。ADC3 继续运行时会周期性对采样电容进行充放电在 PCB 布局比较紧凑的板子上它可能会通过 VDDA 或者基准电压引入噪声影响其他模拟电路和微弱信号调理电路。我实际遇到过一次ADC3 采集完没关后面做高精度称重传感器采集时数据老是带周期性跳动把 ADC3 彻底停掉之后纹波立刻消失了。第三个问题是资源归属。ADC3 和定时器、DMA、DFSDM 之间往往存在触发关系如果你在它触发 DMA 之后把它停了但 DMA 请求还没清干净下一次启动设备或者重新初始化 ADC3 时可能直接卡在等待 DMA 完成的状态里。这种情况在串口和 DMA 上大家比较熟悉其实 ADC 也一样。所以“用完禁用 ADC3”不是没事找事是工程上很实际的需求。2. 先弄懂这些才不会把 disable 变成打补丁2.1 时钟门控不等于断电VDDA 才是大头H743 的 ADC3 外设时钟是挂在 AHB 总线上的CubeMX 生成代码后会通过__HAL_RCC_ADC3_CLK_ENABLE()把对应使能位置位。你以为调用__HAL_RCC_ADC3_CLK_DISABLE()关闭时钟后ADC 就彻底不耗电了不完全是数字逻辑部分的动态功耗确实停了但 ADC 内部的模拟部分仍然由 VDDA 引脚供电这部分电流是关闭外设时钟之后依然存在的。如果你的目标只是让 ADC3 不再受软件控制、不再产生中断和 DMA 请求那么关掉外设时钟就够了。但如果你的目标是让整个系统功耗降到最低那还要考虑 VDDA 域是不是由外部 LDO 供电、能不能在睡眠模式下切断或者是否进入待机模式让内部模拟电路整体掉电。数据手册里会给出不同模式下 VDDA 的功耗参考这一点比单纯关时钟的影响大得多。另外要注意ADC3 的参考电压来源可能是 VREF 引脚也可能是内部 VREFBUF。如果用了内部 VREFBUF 且一直开着即使 ADC3 关了VREFBUF 本身的静态电流还在。很多低功耗项目就在这里吃亏明明关了 ADC3功耗还是降不下来。2.2 H7 的 ADC 状态机和 F4 不一样用 F4 用习惯的人对 ADC 停止操作的理解可能是“把 ADON 清零就行”。但 H7 系列加上 HAL 库之后ADC 的状态机比 F4 复杂。H7 的 ADC 启停逻辑里ADEN 是总使能位ADSTART 表示常规转换是否在进行JADSTART 表示注入转换是否在进行ADSTP 是停止请求位。要想干净地禁用 ADC3必须先确保没有正在进行的转换再通过写停止位让控制器完成最后的转换序列最后才能清 ADEN。如果直接往 ADEN 写 0在转换正在进行的时候状态机可能处于一种“不可预测”的中间状态等你下次再启动 ADC3 时转换结果可能错位甚至卡死在等待状态。HAL 库的HAL_ADC_Stop函数内部做了这些状态机处理所以绝大多数情况下我们都应该优先使用它。但前提是你没有在中断服务函数里和 ADC 操作打架也没有在多个线程里同时调用 ADC 启停接口。3. 实操从应用到寄存器把 ADC3 安全关闭3.1 最稳妥的操作顺序禁用 ADC3 不是只调一个函数的事而是一套顺序。我的建议是如果有外部触发源定时器、EXTI、DFSDM 触发先关闭触发源如果有 DMA 传输先调用HAL_ADC_Stop_DMA让 ADC 和 DMA 都停下来如果开启了中断调用HAL_ADC_Stop_IT如果什么高级模式都没用直接HAL_ADC_Stop(hadc3)确认停止成功后再调用__HAL_RCC_ADC3_CLK_DISABLE()关闭外设时钟。这个顺序的核心逻辑是先让 ADC 状态机回到可控状态再切断触发源和传输通道最后才关时钟。顺序反了可能会在关时钟的一瞬间留下悬空的请求信号导致后续再次使能时出现异常。有些朋友可能会觉得我只是想省电ADC3 就让它随便停不想管 DMA 之类的东西。但如果你初始化 ADC3 的时候选了 DMA 模式CubeMX 会自动把 DMA 句柄绑定到hadc3上DMA 通道一直处于使能状态。这时你只调HAL_ADC_StopDMA 并不知道 ADC 已经停了当下一次 ADC 触发到来时DMA 可能又搬运一次旧数据。长此以往要么 DMA 卡住要么溢出中断反复触发。所以在禁用之前先确认自己在用哪一种模式然后调用对应的 Stop 接口。3.2 别忘了 DMA、中断和外部触发这里单独展开一下 DMA。H743 的 DMA 是独立的双向通道资源如果 ADC3 一直在工作DMA 会按配置把结果搬到内存数组里。当你调用HAL_ADC_Stop_DMA之后HAL 库会停止 DMA 中断并且把 DMA 状态清掉。但底层 DMA 的使能位还在严格来说应该再调用HAL_DMA_Abort把 DMA 句柄复位。虽然很多场景下不调用HAL_DMA_Abort也能正常跑但在可重入的功能模块里比如你后面还要用同一个 DMA 通道去服务串口接收忘记 Abort 就极有可能在 DMA 句柄状态上互相干扰。外部触发源同样容易忽略。比如你用 Timer 的 TRGO 事件触发 ADC3 转换禁用 ADC3 之前如果不关闭定时器即便 ADC3 已经停止定时器触发信号还在不断产生。这种信号不会有实际转换效果但在中断计数器或 DMA 请求统计上会产生“幽灵事件”。我个人习惯是在禁用前先把触发定时器关掉或者至少在 FIFO 处理逻辑里加一个标志位忽略停止期间的触发。3.3 CubeMX 工程里最容易翻车的点CubeMX 会生成MX_ADC3_Init()和HAL_ADC_MspInit()这两段代码在 CubeMX 重新生成时会覆盖你的手动修改。很多新手朋友直接在MX_ADC3_Init()里加了一句“禁用 ADC3”的代码结果每次重新生成工程这段代码就消失了而且不知道去哪找。正确做法是在应用层单独写一个ADC3_DeInit()或者ADC3_PowerDown()函数所有禁用逻辑都放在你自己的模块里。CubeMX 生成的初始化函数只负责“上电配置”你的函数负责“按业务逻辑关闭”两边职责分开后面维护时不会互相踩脚。另外HAL_ADC_MspDeInit()是由 CubeMX 生成的它负责反初始化 GPIO 和时钟。如果你自己手动关时钟又调用了HAL_ADC_MspDeInit()会导致 GPIO 配置也被改变。如果后续还需要复用 ADC3 的引脚做别的功能请注意重新配置 GPIO否则引脚状态可能不是你期望的。4. 参考代码禁用与重新启用4.1 禁用函数下面给出一段我在项目里实际使用的代码场景是 ADC3 单次采集后关闭void ADC3_DisableAfterUse(void) { // 如果 ADC3 使用了外部触发先停掉触发源 // 以定时器为例这里先关闭定时器触发输出 // HAL_TIM_Base_Stop(htim_trigger); // 根据初始化模式选择停止接口 // 如果 CubeMX 里配置了 DMA用这一行 // if (HAL_ADC_Stop_DMA(hadc3) ! HAL_OK) // { // Error_Handler(); // } // 如果配置了中断用这一行 // if (HAL_ADC_Stop_IT(hadc3) ! HAL_OK) // { // Error_Handler(); // } // 最普通的停止方式 if (HAL_ADC_Stop(hadc3) ! HAL_OK) { Error_Handler(); } // 如果 DMA 句柄需要释放可以调用 Abort // if (HAL_DMA_Abort(hdma_adc3) ! HAL_OK) // { // Error_Handler(); // } // 关闭 ADC3 外设时钟 __HAL_RCC_ADC3_CLK_DISABLE(); // 可选将 ADC3 相关引脚从模拟状态切回数字状态 // 以避免悬空输入引脚带来的漏电 // GPIOA-MODER | GPIO_MODER_MODE0_0; // 以 PA0 为例 }这里有几个关键点。第一HAL_ADC_Stop调用后HAL 库内部会把hadc3.State设置成HAL_ADC_STATE_READY所以下次你可以直接调用HAL_ADC_Start不需要重新初始化整个 ADC。第二__HAL_RCC_ADC3_CLK_DISABLE()放在停止转换之后因为关闭时钟前要确保 ADC 不存在任何挂起的转换请求。第三如果你完全确定不需要再启用 ADC3可以把 GPIO 也改成数字模式降低悬空引脚漏电。4.2 重新启用函数和校准禁用不是一次性的如果产品需要在特定事件后再次使用 ADC3那就要写一个重新启用函数void ADC3_ReEnable(void) { // 重新打开 ADC3 外设时钟 __HAL_RCC_ADC3_CLK_ENABLE(); // 等待模拟部分稳定一般在 1ms 左右 HAL_Delay(1); // H7 上重新使能后建议重新校准 // 原因是 ADC 内核的电容阵列状态可能随电源波动发生略微变化 HAL_ADCEx_Calibration_Start(hadc3, ADC_SINGLE_ENDED); // 启动 ADC3 if (HAL_ADC_Start(hadc3) ! HAL_OK) { Error_Handler(); } }注意HAL_ADCEx_Calibration_Start在 H7 上是校准函数参数里ADC_SINGLE_ENDED表示单端输入如果你的引脚配置成差分输入要改成ADC_DIFFERENTIAL_ENDED。校准函数必须在 ADC 启动之前调用如果顺序反了校准不会生效可能采样偏大或偏小。4.3 功耗数据参考我在实际测试中使用 H743 主频 480MHz外设时钟开启状态ADC3 使能但不转换时系统电流比单纯时钟关闭状态大约高出几十微安到一百微安不等具体数值取决于 VREF、VDDA 供电方式和测量位置。如果你在做低功耗建议用高精度万用表测量 VDDA 输入端电流对比“ADC3 时钟开启”和“ADC3 时钟关闭”两种状态下的差异。这里也顺带说一句如果你只在开机时采一次温度然后马上关闭 ADC3并且进入了 STOP 模式那么 CPU 的功耗才是待机电流的主要因素ADC3 的贡献很小。真正影响大的是你在正常工作模式下一直把 ADC3 开着同时在实时任务里频繁切换功能模块这种情况下 ADC3 的时钟开销会一直存在。5. 常见问题与避坑记录5.1 禁用后重新采集为什么数值飘了这是我在论坛上看到最多的问题也是我自己踩过的一个坑。禁用 ADC3 后重新启用并开始转换采出来的数值和第一次上电时差了几个 LSB甚至差几十个 LSB。原因通常有两个一个是 ADC 校准寄存器在断电或时钟关闭后没有保持另一个是 VDDA 电压波动导致校准基准漂移。解决办法就是上面代码里写的重新启用后一定要重新做校准。H7 的校准过程会自动调整 ADC 内部电容阵列的充放电补偿保证转换结果的线性度。不要觉得“我之前已经校准过了”就能一直用尤其当电源从低功耗状态恢复、VDDA 发生了轻微跌落之后校准的必要性会增加。5.2 关闭 ADC3 时钟之后 ADC1/2 会不会受影响不会。H743 的 ADC3 和 ADC1/2 的时钟使能位是独立控制的关闭 ADC3 的时钟不会影响 ADC1/2 的正常工作。但在同一时刻如果你用到了双 ADC 模式一定要确认使用的是 ADC1/2而不是 ADC3因为 ADC3 不支持独立的双ADC同步模式。把 ADC3 当作第三个独立通道时关闭它不影响其他两个 ADC 的转换。另外如果你开启了系统级校准或者使用了 ADC 公共参考电压ADC3 关闭后内部参考电压可能处于空闲状态这是正常的不会影响 ADC1/2。5.3 进入 STOP 模式前要不要先禁用 ADC3建议先禁用。虽然 H7 在进入 STOP 模式时硬件会暂停大部分时钟但如果 ADC3 正在进行转换且触发源和 DMA 还在等待停止模式下的行为可能会变得不可预期。我遇到过一种现象直接进入 STOP 模式后唤醒瞬间 ADC3 的 DMA 中断被挂起紧接着触发了一次系统错误排查了很久才发现是 ADC 停止前没有做完整关闭流程。所以我在进 STOP 模式前都会调用一次ADC3_DisableAfterUse()确保转换和 DMA 都处于停止状态然后再调用HAL_PWR_EnterSTOPMode()。这样唤醒后的恢复逻辑也简单重新打开时钟校准启动。5.4 常见问题速查表问题现象可能原因解决办法禁用后电流仍然偏高VDDA 或 VREFBUF 仍是供电状态关闭 VREFBUF或在待机模式切断模拟电源重新启动后采样值漂移未重新校准调用HAL_ADCEx_Calibration_StartDMA 卡死无法再次启动只调了 Stop 没调 DMA Abort调用HAL_DMA_Abort并复位 DMA 句柄外部触发导致反复中断定时器 TRGO 未关闭先停触发源再停 ADCADC3 关闭后 GPIO 漏电引脚仍处于模拟模式将引脚配置为数字输出或确定状态HAL_ADC_Stop 返回 HAL_BUSY有转换或校准正在执行等状态机空闲或检查是否在中断中调用了操作函数6. 一些实操上的额外心得最后再分享几个我在调试过程中总结的小经验。第一不要在中断服务函数里直接调用HAL_ADC_Stop或者HAL_ADC_Stop_DMA尤其是高优先级中断嵌套时。HAL 库的 ADC 停止过程需要等待状态机迁移如果中断抢占了状态机的等待过程可能出现 HAL_BUSY 或者死等。正确做法是在中断里只置标志位在主循环或低优先级任务中处理停止逻辑。第二如果项目里同时需要功耗优化和快速唤醒建议不要频繁开关 ADC3而是用一次性采集配合低功耗模式的方案。比如设备每 5 秒唤醒一次唤醒后打开 ADC3、采集、关闭、再进 STOP这样既能保证采样实时性也能把 ADC3 的使用时间压缩到最小。第三HAL 库的 ADC 状态机在 DEBUG 时比较难观察我一般直接读hadc3.State和hadc3.ErrorCode来定位问题。如果hadc3.ErrorCode里有HAL_ADC_ERROR_DMA说明 DMA 链路没有清理干净。不要只盯着 ADC 相关的寄存器DMA 和 GPIO 的状态也要一起查。第四在你对自己的状态机逻辑不是特别有把握的时候寄存器操作和 HAL 库操作不要混着写。HAL 库已经处理了大部分状态机迁移你再去直接操作 ADEN/ADSTART 反而容易破坏它内部的状态同步。如果你是跑到寄存器层面做底层驱动那就整个模块都用寄存器操作保持一致。ADC3 的禁用操作本质上是把“外设使用”和“外设生命周期管理”分开看待。很多人在嵌入式开发里只关心怎么把外设用起来却忽略了一个外设用完之后的收尾工作。希望这篇内容能帮你把 STM32H743 的 ADC3 关得干净、开得顺畅少在调试器前多耗几个晚上。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门