低功耗项目里,“外设用完就关”是基本功,但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的配置寄存器恢复成复位默认值,包括清除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。这一步必须放在ADEN=0之后,因为调节器还在给转换电路供电时,直接关会有风险,参考手册里也明确写了这个顺序。 - 最后才关时钟:调用
__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关闭而停止。重启用时,你得重新配置
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位应该为0,ADVREGEN位应该为0,ADC3->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,还是得老老实实走完上面这几步。