news 2026/9/5 19:44:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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这个“房间”里的空调,你把门锁了(时钟关了),但空调还在运转。

只有当ADEN=0(ADC禁用)且没有转换任务挂起时,你才可以去清这个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的配置寄存器恢复成复位默认值,包括清除ADENADVREGENCFGR等,三是调用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,或者需要更细粒度的控制,寄存器级的禁用序列要严格按下面这个顺序来:

  1. 断开外部触发源,把ADC3->CFGREXTENJEXTEN清零,否则外部触发信号可能在禁用过程中再次启动转换,造成竞态。
  2. 如果有DMA在跑,先停DMA,并清掉DMA传输完成中断标志,再关ADC的DMA请求位DMAEN
  3. 强制停止当前转换:置位ADC3->CRADSTP,等待硬件自动清零ADSTP。需要注意的是,ADSTP是一个软件写1后由硬件清零的位,所以“等待清零”这一步必须做,不能直接往下走。
  4. 软件禁用ADC:清零ADEN,然后等待ADEN硬件清零。在H7上,如果当前还有转换在跑,你写0也不一定立即生效,所以第3步先停转换很重要。
  5. 关闭内部电压调节器:清零ADVREGEN。这一步必须放在ADEN=0之后,因为调节器还在给转换电路供电时,直接关会有风险,参考手册里也明确写了这个顺序。
  6. 最后才关时钟:调用__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. 实战:一次测量后彻底关闭ADC3

3.1 关闭前必须记录哪些状态

实战场景里,你不能简单地把ADC3关了就算完。如果后续还需要重新启用,你得在关闭前把现场状态记下来,否则重新初始化时很容易漏配置:

  • ADC3的时钟源选择D3CCIPR寄存器的ADCSEL位域,重新启用时要恢复,不然ADC时钟来源不对,采样值会乱跳。
  • 引脚复用关系:ADC3用的哪些GPIO,是纯模拟输入还是复用了其他外设,关闭时要记下引脚号,方便重新初始化时恢复。
  • 触发源配置:如果ADC3是被定时器TRGO触发,这个触发源不会因为ADC3关闭而停止。重启用时,你得重新配置EXTSELEXTEN,同时确认定时器那边没有残留的更新事件,否则复位后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()内部会清ADENADVREGEN,而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禁用前把EXTENJEXTEN清零。这是我在上一节函数第一步做的事。之后重新启用时,再按需恢复触发配置。这个坑给我的教训是: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->CRADEN位应该为0,ADVREGEN位应该为0,ADC3->ISRADRDY位应该为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->CRADENADVREGEN是0,确认DMA通道已经失能把总线主设备让出来。

这三项检查写成一个函数,放在进入STOP模式的公共入口里,每次都能提前暴露问题。实测下来,这比在功耗测试阶段发现异常再回头排查要省时间得多。

我个人在实际操作中的体会是,H7时代的“关外设”不能再想当然,尤其是ADC这种带独立时钟和内部LDO的外设,一定要回到参考手册看清楚每个电源域和使能位的关联。ADC3这套“先停转换、再清使能、再关调节器、最后断时钟”的顺序,看似繁琐,但只要你按这个节奏走,后续重新启用就很少会遇到那些说不清道不明的问题。如果你只是在两三次转换之间想省点电,也可以考虑开启H7自带的LowPowerAutoPowerOff,让ADC在每次转换结束后自动掉电,但那只适合频繁采样的场景,真要彻底摘掉ADC3,还是得老老实实走完上面这几步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 19:37:57

水声通信MIMO-OFDM系统设计:原理、挑战与工程实践

简介:本资源是一套基于MATLAB实现的水声通信MIMO-OFDM系统仿真方案,面向计算机、电子信息工程及数学等专业的本科生,适用于课程设计、期末大作业与毕业设计等实践环节,帮助学习者深入理解水下信道建模、空时信号处理与抗多径调制等…

作者头像 李华
网站建设 2026/8/31 16:19:02

2026 年企业数字人直播平台5 大盘点:高性价比主流方案对比

摘要2026年企业数字人直播平台已进入“合规化规模化”竞争阶段。本文从技术拟真度、直播稳定性、全平台适配、AI交互能力、合规风控、成本效率六个维度,对五家主流平台进行横向对比。核心结论:晟诺科讯达在成本控制与全链路功能覆盖上表现突出&#xff0…

作者头像 李华
网站建设 2026/9/2 8:51:49

Java后端双线作战:华为校招与阿里社招全流程复盘

1. 为什么9月同时投华为校招和阿里社招:我的求职背景与策略 9月这个时间节点挺特殊的。华为校招的机考和性格测试集中在8月底到9月中旬铺开,而阿里那边很多部门的社招HC(Headcount,招聘名额)也在9月做最后的盘点&#…

作者头像 李华
网站建设 2026/8/31 22:31:06

认知具身智能体架构(CEAA):从感知到行动的智能体闭环设计

过去两年,大模型把“对话智能”做到了几乎人人可用的程度,但真正把 AI 接入业务系统的人会发现一个尴尬事实:模型知道你问过什么,却不知道当前订单状态;能生成漂亮文案,却没法安全地替你点一个按钮&#xf…

作者头像 李华
网站建设 2026/8/31 16:28:08

ESRGAN水印去除实战:从zip包解压失败到ComfyUI集成

简介:图像超分辨率是AI视觉基础技术之一,其核心原理在于通过深度生成模型重建高频细节而非简单插值放大;ESRGAN作为代表性对抗式超分架构,凭借感知损失、RRDB模块与相对判别器,在纹理真实感上显著优于传统方法&#xf…

作者头像 李华
网站建设 2026/9/2 5:58:49

用C语言打破Python可视化部署性能瓶颈

标题里出现“Python可视化部署天花板”和“纯c语言”的时候,很多人第一反应是标题党。但我把“Python可视化”“部署”“C语言”这三个词拆开再组合之后发现,这个方向其实非常实际:Python负责画图和分析,C语言负责底层计算和性能热…

作者头像 李华