简介:STM32L0x1系列是意法半导体推出的超低功耗微控制器,基于ARM Cortex-M0+内核,主要面向电池供电的物联网应用。这个ADC程序包围绕模拟数字转换器,提供了从初始化、通道选择、分辨率与采样时间配置,到单次、连续、扫描等多种转换模式,以及中断处理与数据读取的完整示例。程序还演示了在待机或停止模式下合理关闭外设时钟、降低转换功耗的方法,有助于读者掌握低功耗数据采集的设计要点。压缩包共178个文件,大小4.6MB。其中核心源码为C语言文件与头文件,并包含STM32L0系列硬件抽象层或底层库驱动、Keil工程文件、编译生成的中间文件等,目录组织清晰,适合直接导入开发环境学习和二次开发。已有294人学习下载。通过研究此工程,可以快速搭建自己的ADC采集应用,比如温湿度、光照或电池电压监测,同时参考其中的电源管理策略,提升单片机项目的续航能力。
1. 低功耗STM32L0x1-ADC程序:电池设备的数据采集,难点根本不在ADC,而在功耗
拿到这份名为低功耗STM32L0x1-ADC程序的工程压缩包时,我第一反应是:ADC采集而已,做嵌入式的人谁没读过几次数据寄存器。可等真正把程序塞进电池供电设备里跑起来才发现,把一次12位电压采样安排进低功耗节奏里,讲究程度远超预期。问题通常不在转换本身,而在三件事:MCU为了采一次数要醒多长时间、醒过来之后电流多高、采集完能不能干净地睡回去。
这份程序解决的核心问题很明确,就是在STM32L0x1上实现一条完整的「唤醒-转换-传输-睡眠」闭环,让ADC采集不再拖累整机功耗。适合两类人看:一类是刚接触低功耗采集的开发者,需要照着搭骨架;另一类是已经做过一版采集程序,但对示波器上的电流波形始终不满意、想对照检查自己配置哪里藏着多余消耗的从业者。
2. STM32L0x1低功耗模式与ADC时钟:先想清楚「什么时候醒、醒多久、花多少电流」
2.1 电源模式选型:ADC必须在能跑时钟的域里执行
STM32L0x1的电源模式从高到低大致是Run、Sleep、Low-power run、Low-power sleep、Stop、Standby。ADC外设本身依赖时钟,Stop模式下内核和大部分外设时钟都停摆,ADC的寄存器内容也维持不了,所以「躲在Stop模式里偷偷采样」是不存在的。任何一次转换,都得先把系统拉回Run或Low-power run域,采完再睡回去。这是低功耗ADC程序在设计上绕不开的第一条约束。
| 模式 | 内核与时钟 | ADC可用性 | 典型唤醒源 | 适合场景 |
|---|---|---|---|---|
| Run / Low-power run | 运行 | ADC可正常转换 | 无需唤醒 | 转换必须在这里执行 |
| Sleep / Low-power sleep | 内核停,总线可运行 | ADC可配合DMA | 任意中断 | 短暂等待外设 |
| Stop | 全部时钟停止 | 外设掉电,ADC内容丢失 | LPTIM、RTC、外部中断 | 待机休眠 |
| Standby | 掉电,RAM丢失 | 不可用 | 复位、唤醒引脚、RTC | 深度休眠 |
我一般会让主流程走「Stop + Low-power run」组合:平时挂在Stop模式,由LPTIM或RTC定时唤醒,唤醒后切入Low-power run,系统时钟降到2MHz,做完ADC转换立刻按回Stop。为什么不用全速Run?因为Run模式下即使主频刻意调低,各总线、Flash接口、振荡器相关电路仍处于全速供电状态,唤醒期间的平均电流是Low-power run的数倍。对一节电池撑一年的设备来说,这个差距就是能不能撑到年底的分水岭。
2.2 ADC时钟源选择与转换时间公式
L0x1的ADC时钟可以在PCLK、HSI16、HSI16四分频之间选。低功耗场景下我直接让ADC吃PCLK,不再为ADC单独开一个振荡器,省下的电流虽然只有几十微安,但积少成多。假设系统时钟2MHz、APB不分频,ADC时钟就是2MHz,这个值完全落在器件限定的ADC工作范围内。转换时间公式很固定:Tconv = (T_smp + 12.5) / f_ADC,其中12.5是12位SAR转换本身的固定开销。
低功耗设备接的传感器信号源阻抗往往不低,又不想为了驱动ADC多放一级运放,所以采样时间我会尽量选大档。在2MHz的ADC时钟下,几档常见采样时间对应的总转换时间可以粗略换算:
| 采样周期档位 | 总转换时间(f_ADC = 2MHz) | 适用场景 |
|---|---|---|
| 19.5周期 | 约16µs | 信号源阻抗很低、追求转换速度 |
| 64.5周期 | 约38.5µs | 普通分压电阻采集 |
| 106.5周期 | 约59.5µs | 大多数电池电压监测 |
| 239.5周期 | 约126µs | 高阻抗传感器、需要省外部驱动电路 |
顺手把功耗账算明白:假设从唤醒到睡回去总共5ms、期间的电流3mA,采集周期是5分钟一次,那么这段窗口摊到整个周期里的平均电流只有3mA×5ms÷300s≈0.05µA。哪怕窗口拉到50ms,平均也只增加0.5µA。真正的敌人不是单次转换,而是让CPU一直开着等转换完成、或者醒来后拖着DMA和中断空转几个毫秒。毫秒级的唤醒窗口优化,直接决定整机待机能耗的量级。
2.3 用 LPTIM 搭出「采集-睡眠」节奏
LPTIM在Stop模式下仍然可以计数,时钟源选LSI或LSE。LSI约37kHz,省电但精度一般;LSE是32.768kHz,定时精准但需要外部晶振或占用专用引脚。用LPTIM做唤醒的好处是整个过程不依赖SysTick,也不用内核参与,MCU就安安静静睡到该醒的时刻。我习惯把它配置成单次计数模式,而不是连续PWM输出,这样每次计数溢出就产生一次唤醒事件,事件处理完自动停住,不会反复触发。
// LPTIM 配置:LSI 37kHz,预分频 128,约每 5 秒唤醒一次 // 唤醒周期 = (PSC + 1) * (ARR + 1) / f_LPTIM // 5s 对应 ARR = (5 * 37000) / 128 - 1 ≈ 1444 LPTIM_Enable(LPTIM1); LPTIM_SetPrescaler(LPTIM1, LPTIM_PRESCALER_DIV128); LPTIM_SetAutoreload(LPTIM1, 1444); LPTIM_StartSingle(LPTIM1); // 单次计数模式,溢出后自动停止分频和ARR的关系写成公式就是上面注释里的样子。需要注意:LSI本身有百分之十几的温漂,如果你用LPTIM推动的是计费级或者强周期性的业务,建议换LSE。LSI由于省去了外部晶振的起振电流,用在「每五分钟采一次电池电压」这类宽松周期业务里完全够。唤醒事件到来后,在中断里置一个全局标志位,主循环看到标志才去执行ADC采集,这样把「定时唤醒」和「采集执行」彻底解耦,后续调整时间节奏只改ARR,不碰状态机。
3. 程序结构:把「采集-睡眠」做成一条不会断的状态链
3.1 三层结构:硬件层、功耗调度层、应用层
拿到这类低功耗ADC工程的压缩包,第一件事不是看代码细节,而是看它的文件划分。我一般推荐拆成三层。最底层是硬件寄存器操作,只负责把一个外设配置成低功耗场景下可用的状态;中间层是功耗调度层,负责Stop与Low-power run之间的切换,并记录当前处于哪个功耗状态;最上层才是业务逻辑,比如根据分压电阻算电池电压、查表换算温度。
project/ ├─ app_main.c // 主循环:状态判断、功耗调度入口 ├─ app_power.c // Stop/LPRUN 切换,唤醒原因登记 ├─ adc_task.c // ADC 状态机与单次采集逻辑 └─ sensor_driver.c // 传感器参数的工程换算把功耗调度单独拆出来的原因很实际:低功耗程序一旦出现「睡不下去」的问题,你需要有一个地方能一眼看到当前MCU在哪个模式下、被什么事件唤醒。如果这些状态散落在各个驱动的中断里,排查时就得满工程翻,非常容易漏掉某个还在跑的DMA通道。adc_task.c里不出现任何Stop模式的代码,它只负责一件事:被调用时按状态机完成一次采集;app_power.c也不碰ADC寄存器,只管功耗状态的进出。这种约束能避免大量因交叉访问引起的隐藏依赖。
3.2 核心状态机代码:单次采集的四个状态
ADC采集在低功耗程序里不适合写成「上电-配置-死等结果-睡眠」的顺序代码,因为主循环被LPTIM唤醒后,还要处理DMA事件、数据换算、睡眠入口等事情。用状态机管理采集过程,能保证每个时刻都清楚当前处于什么阶段。四个状态就够了:IDLE、START、WAIT_COMPLETE、DONE。
typedef enum { ADC_IDLE = 0, ADC_START, ADC_WAIT_COMPLETE, ADC_DONE } adc_state_t; static adc_state_t adc_state; void adc_task_loop(void) { switch (adc_state) { case ADC_IDLE: break; case ADC_START: // 触发一次转换,配置为软件触发 LL_ADC_REG_SetTriggerSource(ADC1, LL_ADC_REG_TRIG_SOFTWARE); LL_ADC_REG_StartConversion(ADC1); adc_state = ADC_WAIT_COMPLETE; break; case ADC_WAIT_COMPLETE: if (LL_ADC_IsActiveFlag_EOC(ADC1)) { adc_result = LL_ADC_REG_ReadConversionData12(ADC1); adc_state = ADC_DONE; } break; case ADC_DONE: sensor_update(adc_result); adc_state = ADC_IDLE; break; } }注意这段状态机只在唤醒后的短暂窗口内被执行,平时系统处于Stop模式,状态机停在IDLE不消耗任何CPU周期。WAIT_COMPLETE阶段没有用轮询死等,而是每次循环查一次EOC标志,这个设计在低功耗场景里很关键:万一转换异常,主循环还能通过超时计数退出去,不会卡死在ADC上导致永远进不了睡眠。如果你手头已经有一套HAL库工程,可以把HAL_ADC_Start和HAL_ADC_PollForConversion替换成上面的LL写法,行为一致,但对时钟和外设的掌控力度完全不同。
3.3 用LL库而不是HAL库:省电的隐藏细节
并不是说HAL库不能用,但HAL在初始化时为了通用性做了大量兜底操作:打开时钟、配置延时、等待标志位、处理各种状态。对一颗目标是微安级待机的芯片来说,这些兜底随时会变成睡眠电流的黑匣子。LL库是直接在寄存器语义上封一层,每个字段配置都能对应到参考手册,排查功耗问题时省很多时间。一个典型例子:HAL_ADC_Start内部会做校准状态判断和超时等待,这些代码在唤醒窗口里可能多耗掉几十个周期,放在每秒唤醒一次的场景里不起眼,放在电池设备里就是实打实的续航损耗。
我一般建议低功耗ADC程序用LL或直接写寄存器,至少功耗路径上的时钟开关、采样完成标志、外设使能这几处要自己掌握。后面第4章给的代码就是寄存器级写法,改起来心里有底。
4. 可抄的参数配置:时钟树、ADC采样时间、LPTIM唤醒周期怎么设
4.1 先在配置工具里把时钟树和功耗节奏搭出来
不管最后用LL还是寄存器,我建议先把工程骨架用图形化配置工具搭出来,再由LL代码接管关键外设。按照下面的顺序勾选,不容易漏东西,也方便后面对照参考手册改寄存器。
- 选择具体型号,时钟源选HSI16,系统时钟分频到2MHz。
- ADC配置为单次转换、软件触发、采样时间选最大档。
- LPTIM选择LSI作为时钟源,单次计数模式。
- NVIC里只使能必要的唤醒中断,其他外设关闭。
- 把所有没用到外设的GPIO配置为模拟输入,避免数字输入缓冲器产生漏电。
第5步经常被忽略。一个复用成GPIO的引脚如果悬空且使能了数字输入,输入缓冲器会一直产生开关电流,几十nA到几百nA不等。对整机目标5µA的设备来说,这是完全不能忽视的量级。L0x1的每个引脚都能当模拟通道,哪怕你不用它的ADC功能,把引脚切到模拟模式也能同时关掉数字输入缓冲。
4.2 关键参数表:一组可以直接抄的低功耗校准值
下面这组参数覆盖了「时钟-ADC-LPTIM-功耗」的核心链路,是我在低功耗采集板上反复调出来的组合,可以直接作为初始值使用。
| 参数 | 推荐配置 | 说明 |
|---|---|---|
| 系统时钟 | 2MHz(HSI16分频) | 满足ADC和LPTIM业务即可,不要追求高频 |
| ADC时钟 | PCLK = 2MHz | 不需要额外开振荡器给ADC |
| 采样时间 | 最大档位 | 补偿信号源阻抗,减少外部驱动电路 |
| 触发源 | 软件触发 | 与LPTIM唤醒节奏解耦,避免意外连续转换 |
| 转换模式 | 单次转换 | 禁止连续模式,连续模式是睡眠电流大头 |
| LPTIM时钟 | LSI(约37kHz) | 定时精度要求高时换LSE |
| 唤醒周期 | 业务周期,如60s或1h | 周期越长待机电流越低,但数据实时性下降 |
| 对齐与分辨率 | 右对齐,12位 | 分辨率越低转换耗时越短,非必要不用8位 |
表格里需要特别强调两个地方。采样时间最大档不是玄学,它是低功耗场景里省去外部阻抗匹配电路的常见手段。触发源选软件触发,是因为你已经用LPTIM管理唤醒节奏了,再用硬件触发会把ADC的转换时机和LPTIM的比较事件绑在一起,调参时会互相牵连,排查起来非常痛苦。
4.3 寄存器级实现:初始化与单次采集的裸机写法
没有配置工具工程作参考时,直接操作寄存器也能把这套逻辑写出来。下面这段是针对L0x1的ADC初始化和单次采集的骨架代码,寄存器字段名在不同型号间略有差异,以参考手册为准。
static void adc_lowpower_init(void) { // 1. 使能 ADC 与 GPIO 时钟,对应 RCC 的 APB2ENR 位 // 2. 采样通道对应的引脚配置为模拟模式,避免数字缓冲漏电 // 3. 单次转换、软件触发、右对齐 ADC1->CFGR &= ~ADC_CFGR_CONT; // 清除连续转换位,确保单次模式 ADC1->CFGR &= ~ADC_CFGR_EXTSEL; // 触发源字段清零,对应软件触发 // 4. 采样时间设为最大档位 ADC1->SMPR |= ADC_SMPR_SMP; // SMP 字段置最大编码 // 5. 启动 ADC 校准,等待 ADCAL 位由硬件清除 ADC1->CR |= ADC_CR_ADCAL; while (ADC1->CR & ADC_CR_ADCAL) { // 等待校准结束,校准期间不要操作其他 ADC 寄存器 } } static uint16_t adc_start_once(void) { // 软件触发:置 ADSTART,转换完成后硬件自动清除 ADC1->CR |= ADC_CR_ADSTART; while (!(ADC1->ISR & ADC_ISR_EOC)) { // 等待 EOC,低功耗场景下建议配合事件中断而不是死等 } return (uint16_t)(ADC1->DR & ADC_DR_DATA); }代码里的逻辑说明:第3步把触发源字段清零,在多数型号上对应软件触发;第4步的SMP位段直接置最大编码,获得最长采样时间;第5步校准是必须的,L0x1的ADC内部电容和参考都存在出厂偏差,不校准直接用,结果可能整体偏移几十个LSB。adc_start_once里虽然用了循环等待EOC,但在实际低功耗状态机中,我会把这段换成ADC中断置标志位,主循环只在标志位置位后读取结果。上面这段保留while循环是为了方便在裸机上先跑通功能,验证完再改中断方式。
启动转换前还要确认一件事:CAL完成标志已经置位后再操作ADSTART,否则转换不会正常开始。转换完成后读取数据寄存器会自动清除EOC标志,如果既读了DR又手动清了EOC,不影响数值,但会影响后续状态判断,建议只依赖DR读取后的硬件清标志行为。
5. 低功耗ADC采集避坑指南:5条实测踩坑记录
5.1 唤醒后第一次采样值明显偏大
现象:设备从Stop唤醒后,第一个ADC结果比后续值高几十毫伏,这个偏大的值直接让电池电量计算跳了2%。原因:ADC模块刚重新上电,采样保持电容和内部参考尚未完全稳定,而应用层在LPTIM唤醒事件里立刻触发了转换。解决:把唤醒后的第一次转换视为前导转换,数据直接丢弃;或者在正式采集前先完成一次ADC校准流程。前导转换多花60µs,对整机功耗的影响可以忽略,但能让后面每一次真实采样都落在稳定工作点上。
5.2 Stop唤醒后库函数延时全面失灵
现象:进入Stop前调用延时函数一切正常,唤醒后同样的延时函数像卡死一样,程序流程完全错乱。原因:常见的库函数延时基于SysTick实现,Stop模式下SysTick时钟停摆,唤醒后SysTick计数没有恢复预期的节拍,导致延时远超预设值甚至进入无效等待。解决:功耗路径上不要用SysTick做业务延时,改用LPTIM计时,或者用DWT的CYCCNT计数。等ADC前级传感器稳定时,用LPTIM数几个周期再启动转换,比任何库函数延时都可靠。
5.3 连续转换模式没关干净,睡眠电流悄悄上涨
现象:电流波形上每隔固定时间就多一个小毛刺,整机平均功耗比预期高一截。原因:初始化时把ADC配置成了连续转换模式,DMA又配成循环模式,主循环进Stop前没有把这两者关闭。解决:无条件使用单次转换,DMA用normal模式并在采集完成事件里显式关闭,睡眠入口检查一次ADC的ADSTART位确实为0。这个坑在从其他系列芯片移植代码时最容易踩,因为不同系列复位默认值不一样,有些芯片默认连续模式,有些默认单次,只改时钟不改配置,睡眠电流立刻就翻车。
5.4 VREFINT校准值在不同温度下的漂移
现象:同一块板子,20℃和0℃下采集同一个外部电压,换算结果相差百分之几。原因:内部参考电压存在温度漂移,如果直接拿VREFINT反推Vdda,会把漂移带进最终结果。解决:把VREFINT当作一个普通通道参与采集,用校准关系做补偿;如果精度要求高,每次采集前先采一次VREFINT再做归一计算。额外这个通道只增加约60µs转换时间,对低功耗设备来说成本极其有限,但电压换算精度能提升一个档次。
5.5 LPTIM配置成连续PWM模式,ADC被反复拉起
现象:LPTIM本来只做定时唤醒,结果示波器上看到ADC每几十毫秒就转换一次,功耗曲线密集到没法看。原因:LPTIM的输出配置成了连续PWM波形,而ADC的外部触发源恰好选择了LPTIM的比较事件,一个PWM周期内多次触发转换。解决:唤醒用LPTIM的单次计数模式,ADC触发源保持软件触发,两件事彻底解耦。这条在器件间复用代码时最容易中招,换芯片型号时务必重新检查外部触发映射关系。
6. 进阶验证:电流波形、数据标准差与一个测量技巧
6.1 给「采集-睡眠」节奏做电流波形体检
把电流探头夹在供电回路上,示波器打到余晖模式,你会看到一条基本平直的低电流基线,上面每隔一个业务周期冒出一个窄脉冲。这个脉冲宽度就是唤醒和转换的实际窗口,它应该等于2MHz时钟下状态机跑完的时间,而不是你猜测的「很快」。如果脉冲宽度远大于理论值,说明里面有阻塞延时或轮询等待,需要回到状态机里排查。
6.2 用连续100次数据的标准差判断配置是否合理
// 连续性测试:采集100次,计算均值与标准差 // 标准差明显偏大,说明采样时间不足或参考电源纹波大 float sum = 0.0f, mean, var = 0.0f; uint16_t samples[100]; for (int i = 0; i < 100; i++) { samples[i] = adc_start_once(); sum += samples[i]; } mean = sum / 100.0f; for (int i = 0; i < 100; i++) { var += (samples[i] - mean) * (samples[i] - mean); } // 标准差 = sqrt(var / 99),与 LSB 换算后对比精度要求标准差如果超过两三个LSB,先别怀疑ADC本身,回头检查Vdda上有没有高频纹波、采样时间是不是为了省时间调小了、信号源阻抗是不是偏高。低功耗场景里采样时间几乎都富余,真遇到标准差超标,加采样时间永远比加滤波器划算。
6.3 低功耗场景的多通道采集技巧
多个通道要采集时,别把信号源阻抗高的传感器放在序列第一位。前一个通道转换完,采样电容上还残留上一通道电荷,第一通道的第一次结果往往不准。常见做法是先采一遍VREFINT,或者把关键通道放在第二位,再看连续两次采集结果的差值判断稳定性。另一个习惯是:需要做多次平均时,在软件里平均两三次就收手,不要开硬件过采样连采16次,微安级场景里16倍转换时间的代价完全不划算。
我每次改完这类低功耗采集程序,都会先花十分钟看电流波形,再跑一遍100次数据的标准差,两道检查都过了才敢合壳。前者确认功耗节奏没崩,后者确认ADC配置真的稳。这个习惯救过我很多次,希望帮到你。
本文还有配套的精品资源,点击获取