ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32H743低功耗设计:ADC3彻底关闭的寄存器与HAL实现

STM32H743低功耗设计:ADC3彻底关闭的寄存器与HAL实现 低功耗项目里“外设用完就关”是基本功但STM32H743的ADC3是个例外。我最初处理它的时候按F4时代的习惯结束转换后顺手加一句__HAL_RCC_ADC3_CLK_DISABLE()结果用电流表量下来模拟功耗一点没降重新初始化时还经常卡在ADRDY置位不了的状态。翻了几遍参考手册才弄明白H7这一代的ADC和F4完全是两套逻辑ADC3不只有独立的内核时钟源还带一个内部的电压调节器只断AHB/APB总线时钟相当于把门锁了但没关灯模拟核心还在工作。这篇文章就把我在H743上“第一次使用ADC3采样后彻底禁用”的完整过程写清楚包括寄存器操作顺序、HAL层的配合、重启用时的校准恢复还有几个我实际踩过、修了几天才解决的坑。如果你也在用H7做低功耗设计或者因为引脚复用必须临时把ADC3摘掉这篇应该能帮你少走不少弯路。1. 为什么H743的ADC3不能像F4那样“用完就关”1.1 H7的ADC3和ADC1/2不在同一个电源时钟域F1/F4系列的外设基本都挂在APB总线上总线时钟一断外设就彻底歇菜这也是很多老工程师习惯性“关时钟”的原因。但H7系列把芯片内部划分成了多个电源域D2域和D3域各自管理一批外设ADC1/ADC2在D2域ADC3单独挂在D3域。这带来一个直接影响ADC3的时钟选择、触发源映射、低功耗模式下的行为都和ADC1/2有区别。ADC3的内核时钟不是直接来自APB而是通过RCC的D3CCIPR寄存器里ADCSEL位选择可选PLL2P、PLL3P或per_ck。你自己配置的hadc3.Init.ClockPrescaler其实是在这个ADC内核时钟基础上做的分频而不是传统的“PCLK分频”。所以你关掉AHB/APB上的ADC3寄存器接口时钟只能让CPU访问不了ADC3的寄存器而已ADC3本身如果还在工作它靠着内部逻辑继续跑完当前的转换然后停在某个中间状态等着你重新开门。你要是想做低功耗这点电流虽然不算大但在待机场景里它就是你压不下去的那几百微安。1.2 只断时钟会留下一个“半冷”的模拟核心H7的ADC内部集成了一个可关断的电压调节器参考手册里叫ADVREGEN位默认是1也就是调节器上电。这个调节器负责给ADC的模拟电路提供稳定的内部电压类比的话它就是ADC这个“房间”里的空调你把门锁了时钟关了但空调还在运转。只有当ADEN0ADC禁用且没有转换任务挂起时你才可以去清这个ADVREGEN让模拟核心真正掉电。如果只是关了总线时钟来不及清调节器ADC3的模拟部分就一直是上电状态。我在实测中H743的VCCA上这部分额外电流大概有几百微安到毫安级具体数值和ADC时钟频率、采样配置有关但对一个目标是“待机电流低于50µA”的设计来说完全不可接受。更麻烦的是重新启用。如果你在ADC3没有彻底关闭的状态下关掉了总线时钟之后再开时钟ADRDY状态位可能不会正常置位HAL的HAL_ADC_Init会卡在等待校准完成的地方或者校准出来的偏移数据明显不对。这就是典型的“半冷启动”问题。2. 第一次使用后禁用ADC3的完整操作序列2.1 HAL层HAL_ADC_DeInit到底做了什么HAL库提供了HAL_ADC_DeInit()但它不是万能的。它会做三件事一是停止正在进行的转换二是把ADC的配置寄存器恢复成复位默认值包括清除ADEN、ADVREGEN、CFGR等三是调用HAL_ADC_MspDeInit()回调。关键在于HAL_ADC_DeInit()不会自己去关AHB/APB上的ADC3时钟也不会把GPIO配置恢复。这两件事需要你在HAL_ADC_MspDeInit()里手动做。很多人在初始化时写了HAL_ADC_MspInit()却忘了成对写HAL_ADC_MspDeInit()导致DeInit之后时钟照样开着外设也并没有真正“关闭”。所以标准做法是在HAL_ADC_MspDeInit()里做三件事关闭ADC3的内核时钟__HAL_RCC_ADC3_CLK_DISABLE()、把对应的GPIO口恢复为默认状态、如果有DMA请求也要关闭DMA通道并清标志。这样HAL_ADC_DeInit(hadc3)一调用整条链路就收拾干净了。2.2 寄存器级序列为什么这个顺序不能乱如果不想依赖HAL或者需要更细粒度的控制寄存器级的禁用序列要严格按下面这个顺序来断开外部触发源把ADC3-CFGR的EXTEN和JEXTEN清零否则外部触发信号可能在禁用过程中再次启动转换造成竞态。如果有DMA在跑先停DMA并清掉DMA传输完成中断标志再关ADC的DMA请求位DMAEN。强制停止当前转换置位ADC3-CR的ADSTP等待硬件自动清零ADSTP。需要注意的是ADSTP是一个软件写1后由硬件清零的位所以“等待清零”这一步必须做不能直接往下走。软件禁用ADC清零ADEN然后等待ADEN硬件清零。在H7上如果当前还有转换在跑你写0也不一定立即生效所以第3步先停转换很重要。关闭内部电压调节器清零ADVREGEN。这一步必须放在ADEN0之后因为调节器还在给转换电路供电时直接关会有风险参考手册里也明确写了这个顺序。最后才关时钟调用__HAL_RCC_ADC3_CLK_DISABLE()。这个顺序的核心思想是先让ADC从“工作状态”退出再关内部电源最后才断寄存器接口。反过来如果你一上来就关总线时钟后面所有寄存器写操作都变成无效ADC3可能停在一个半激活状态导致重新启用时ADRDY异常。2.3 一个可以直接用的disable函数封装结合上面的顺序我封装了一个函数平时项目里直接调用就行void ADC3_DisableAfterUse(void) { /* 1. 断开外部触发避免禁用过程中被硬件信号再次拉起 */ ADC3-CFGR ~(ADC_CFGR_EXTEN | ADC_CFGR_JEXTEN); /* 2. 如果有DMA还在接收数据先停DMA再清传输标志 */ __HAL_DMA_DISABLE(hdma_adc3); __HAL_DMA_CLEAR_FLAG(hdma_adc3, DMA_LISR_FEIF0 | DMA_LISR_DMEIF0 | DMA_LISR_TEIF0 | DMA_LISR_HTIF0 | DMA_LISR_TCIF0); /* 3. 强制停止当前转换等待ADSTP硬件自动清零 */ SET_BIT(ADC3-CR, ADC_CR_ADSTP); while ((ADC3-CR ADC_CR_ADSTP) ! 0) { } /* 4. 软件禁用ADC等待ADEN硬件清零 */ CLEAR_BIT(ADC3-CR, ADC_CR_ADEN); while ((ADC3-CR ADC_CR_ADEN) ! 0) { } /* 5. 关闭ADC内部电压调节器让模拟核心真正掉电 */ CLEAR_BIT(ADC3-CR, ADC_CR_ADVREGEN); /* 6. 最后关闭总线时钟 */ __HAL_RCC_ADC3_CLK_DISABLE(); }这里有个细节如果你的项目里ADC3根本没用到DMA第二步直接跳过不影响后续逻辑。如果你用的是HAL的HAL_ADC_Stop_DMA()它内部会停DMA但中断标志的清理不一定彻底建议还是手动再清一次尤其是DMA配置成循环模式时容易残留TCIF标志导致下一次HAL_ADC_Start_DMA()时误进回调。3. 实战一次测量后彻底关闭ADC33.1 关闭前必须记录哪些状态实战场景里你不能简单地把ADC3关了就算完。如果后续还需要重新启用你得在关闭前把现场状态记下来否则重新初始化时很容易漏配置ADC3的时钟源选择D3CCIPR寄存器的ADCSEL位域重新启用时要恢复不然ADC时钟来源不对采样值会乱跳。引脚复用关系ADC3用的哪些GPIO是纯模拟输入还是复用了其他外设关闭时要记下引脚号方便重新初始化时恢复。触发源配置如果ADC3是被定时器TRGO触发这个触发源不会因为ADC3关闭而停止。重启用时你得重新配置EXTSEL和EXTEN同时确认定时器那边没有残留的更新事件否则复位后ADC3可能被一个“陈旧”的触发信号立刻启动首轮转换数据完全不可信。校准状态H743的ADC在每次上电或完全复位后工厂校准值并不会自动加载到运行状态需要靠HAL_ADCEx_Calibration_Start()做一次实际校准。如果你关闭后重新初始化必须重新执行校准否则转换结果会有明显偏移。3.2 完整示例代码采集一次就关闭下面是一段完整的流程涵盖了初始化、校准、单次采样、彻底关闭的步骤static ADC_HandleTypeDef hadc3; /* 初始化ADC3并完成校准 */ static void ADC3_InitForFirstUse(void) { ADC_ChannelConfTypeDef sConfig {0}; /* 如果之前关过ADC3这里先把时钟打开 */ __HAL_RCC_ADC3_CLK_ENABLE(); hadc3.Instance ADC3; hadc3.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc3.Init.Resolution ADC_RESOLUTION_12B; hadc3.Init.ScanConvMode ADC_SCAN_DISABLE; hadc3.Init.EOCSelection ADC_EOC_SINGLE_CONV; hadc3.Init.LowPowerAutoWait DISABLE; hadc3.Init.LowPowerAutoPowerOff DISABLE; hadc3.Init.ContinuousConvMode DISABLE; hadc3.Init.NbrOfConversion 1; hadc3.Init.DiscontinuousConvMode DISABLE; hadc3.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc3.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; hadc3.Init.DMAContinuousRequests DISABLE; hadc3.Init.Overrun ADC_OVR_DATA_OVERWRITTEN; hadc3.Init.LeftBitShift ADC_LEFTBITSHIFT_NONE; hadc3.Init.OversamplingMode DISABLE; if (HAL_ADC_Init(hadc3) ! HAL_OK) { Error_Handler(); } /* 上电校准每次重新启用都必须做 */ if (HAL_ADCEx_Calibration_Start(hadc3, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) ! HAL_OK) { Error_Handler(); } /* 配置通道H7的采样时间由公共采样时间寄存器决定这里用COMMON_1 */ sConfig.Channel ADC_CHANNEL_5; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLINGTIME_COMMON_1; sConfig.SingleDiff ADC_SINGLE_ENDED; sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; if (HAL_ADC_ConfigChannel(hadc3, sConfig) ! HAL_OK) { Error_Handler(); } } /* 采样一次然后立刻彻底关闭ADC3 */ void ADC3_MeasureOnceAndShutdown(void) { uint32_t adc_val 0; ADC3_InitForFirstUse(); if (HAL_ADC_Start(hadc3) ! HAL_OK) { Error_Handler(); } if (HAL_ADC_PollForConversion(hadc3, 100) HAL_OK) { adc_val HAL_ADC_GetValue(hadc3); /* 这里根据VREF计算实际电压 */ /* mv (adc_val * VREF) / 4095; */ } /* 停止转换并走DeInit重点在于MspDeInit里要把GPIO和时钟一起关掉 */ HAL_ADC_Stop(hadc3); HAL_ADC_DeInit(hadc3); }这段代码里HAL_ADC_DeInit()内部会清ADEN和ADVREGEN而HAL_ADC_MspDeInit()里我会关GPIO和ADC3时钟。如果你自己想完全掌控时序也可以不用HAL_ADC_DeInit()直接调用上一节那个寄存器级函数效果一样但HAL版本更省心。3.3 重新启用时的恢复清单如果后续某个时刻又要再次使用ADC3不能直接调用HAL_ADC_Init就完事我整理过一个恢复清单重新使能__HAL_RCC_ADC3_CLK_ENABLE()。重新调用HAL_ADC_Init()并确认ClockPrescaler和实际ADC内核时钟匹配特别是你改过PLL配置的时候。重新执行HAL_ADCEx_Calibration_Start()不做这一步转换值大概率偏。重新配置通道和引脚复用GPIO如果之前被复用成GPIO输出还得先切回模拟模式或复用功能模式。重新配置触发源包括定时器的TRGO输出使能、边沿选择防止遗留事件造成意外启动。这五步缺一不可。我最初只做了前两步结果重新采集到的电压值整体偏了2%到3%排查半天才意识到是校准失效。4. 禁用之后我踩过的三个坑4.1 坑一触发源没断ADC3被“残留触发”偷偷带跑项目里我用TIM8的TRGO触发ADC3做定频采样低功耗阶段把ADC3关了但TIM8还在跑。一开始我只关了ADC3的时钟没去清EXTEN结果重新初始化ADC3时每次启动采样都能读到一串正常数据但有时会多采一次数据完全对不上。排查过程比较绕我甚至怀疑DMA配置错了。后来用逻辑分析仪抓TIM8的TRGO引脚发现定时器那边持续输出触发脉冲才想到ADC3的触发配置在禁用后仍然是“使能”状态。重新初始化时只要定时器有一个更新事件ADC3就会立刻启动一次转换而此时我还没调用HAL_ADC_Start()导致状态机错乱。修复方法很简单就是在ADC3禁用前把EXTEN和JEXTEN清零。这是我在上一节函数第一步做的事。之后重新启用时再按需恢复触发配置。这个坑给我的教训是H7的触发矩阵是独立的关闭ADC本身并不等于断开了触发链路要主动去清。4.2 坑二引脚还留在模拟输入模式复用GPIO时读不到电平在一次改版中我把原来接ADC3的引脚改成了按键输入代码里直接重新配置GPIO结果读GPIO电平怎么读都是0。用万用表量引脚电压也是正常的3.3V一度怀疑芯片坏了。其实原因是HAL_ADC_MspDeInit()里没有把GPIO恢复引脚从初始化到关闭始终停在ANALOG模式。在STM32的GPIO配置里模拟模式会关闭施密特触发器输入数据寄存器读不到有效电平。解决办法是在关闭ADC3时把对应引脚配置成GPIO_MODE_INPUT或直接HAL_GPIO_DeInit()让引脚回到默认状态。这件事给我的提醒是ADC引脚和普通GPIO引脚共用一套物理引脚外设的“关”和引脚的“恢复”是两码事。做引脚复用设计时一定要把MspDeInit里的GPIO处理写完整否则很容易出现这种“硬件没坏但读不到数据”的诡异现象。4.3 坑三重开ADC3忘了重新校准转换值偏了一大截前面提到过H7的ADC重新上电后不会自动带出校准结果必须软件执行校准。我踩这个坑是因为当时很自信地认为HAL在HAL_ADC_Init()里会校准但实际H7上校准是独立的HAL_ADCEx_Calibration_Start()你必须显式调用。当时表现为温度传感器读值偏高且误差不稳定有时差1度有时差3度。我原本以为是传感器贴装问题后来无意中注释掉校准代码做对照误差反而稳定了才意识到问题在ADC校准上。重新启用ADC3后补了一次校准误差立刻恢复正常。这个坑在ADC3上比ADC1/2更容易踩因为ADC3用于一次性测量的场景更多我常见到有人只在系统上电时校准一次之后每次都复用同一个hadc3句柄一旦你用了DeInit校准结果就丢了。5. 怎么确认ADC3确实关了5.1 寄存器状态确认读CR和ISR关闭之后如果还想确认状态可以在关时钟前读一下寄存器。ADC3-CR的ADEN位应该为0ADVREGEN位应该为0ADC3-ISR的ADRDY位应该为0。这几个条件都满足才说明ADC3进入了完全掉电状态。要注意的是一旦__HAL_RCC_ADC3_CLK_DISABLE()执行了你对ADC3寄存器的读取就不再可靠所以验证动作必须放在关时钟之前或者通过检查RCC的时钟门控状态来间接确认。我在调试时通常用一个断点停在ADC3_DisableAfterUse()函数的最后一行然后在调试器里展开CR和ISR寄存器确认状态再放关时钟那一行继续跑。5.2 电源实测VCCA上的电流变化寄存器验证只能说明软件状态最终效果要看硬件。ADC3模拟核心掉电后VCCA上的电流会明显下降。我在实测中ADC3正常使能且待机空闲时VCCA大概多出0.5mA到1mA关闭ADVREGEN后这部分电流基本降到了微安级以下。测量方法没什么特别的用万用表串在VCCA供电回路上分别记录开启和关闭两种状态的电流对比差值。如果项目里VCCA是独立LDO供电那就更简单直接看LDO的静态电流就能判断ADC3是否真的关干净。这个数据也可以写进测试报告里作为低功耗优化的证据。5.3 进入STOP模式前的残留检查H743进入STOP模式之前D3域的外设如果没有完全关闭可能会阻止进入深睡眠状态或者唤醒后外设状态错乱。ADC3挂在D3域所以我习惯在准备进入低功耗前做一次“残留检查”确认RCC-D3AHBENR里ADC3的时钟门控是0确认ADC3-CR的ADEN和ADVREGEN是0确认DMA通道已经失能把总线主设备让出来。这三项检查写成一个函数放在进入STOP模式的公共入口里每次都能提前暴露问题。实测下来这比在功耗测试阶段发现异常再回头排查要省时间得多。我个人在实际操作中的体会是H7时代的“关外设”不能再想当然尤其是ADC这种带独立时钟和内部LDO的外设一定要回到参考手册看清楚每个电源域和使能位的关联。ADC3这套“先停转换、再清使能、再关调节器、最后断时钟”的顺序看似繁琐但只要你按这个节奏走后续重新启用就很少会遇到那些说不清道不明的问题。如果你只是在两三次转换之间想省点电也可以考虑开启H7自带的LowPowerAutoPowerOff让ADC在每次转换结束后自动掉电但那只适合频繁采样的场景真要彻底摘掉ADC3还是得老老实实走完上面这几步。
返回列表