BlueNRG-LP定时器实战:TIMER与LPTIM的低功耗设计要点
发布时间:2026/8/29 18:26:58 作者:尧图编辑部 阅读量:1,286

去年做一款基于BlueNRG-LP的无线温湿度记录仪项目功能本身不复杂真正耗时间的反而是这颗芯片的定时器模块。功能验证阶段一切正常一进入低功耗调试就开始碰壁LPTIM配好后休眠电流多出几个微安PWM驱动蜂鸣器时偶尔出现一次异常短脉冲从STOP模式唤醒后系统时钟掉速。这些问题单个看都不大串在一起就是半个月的调试周期。这篇应用笔记把这些定时器相关的实战经验整理出来既包括TIMER1/TIMER2、LPTIM的配置思路也包括SysTick、RTC、看门狗在实际工程里的分工适合正在评估或已经用BlueNRG-LP、BlueNRG-LPS做低功耗蓝牙产品的朋友参考。1. BlueNRG-LP/LPS定时器家族先看清楚盘子里有什么1.1 芯片与定时器资源概览BlueNRG-LP和BlueNRG-LPS都是基于Cortex-M0内核、主频最高48MHz的低功耗蓝牙SoC两个型号的外设基本一致区别主要在RAM/Flash容量和封装尺寸LP提供256KB RAM、320KB FlashLPS提供192KB RAM、256KB Flash。射频指标相同的前提下选型更多看资源余量和PCB面积定时器模块本身两个型号完全兼容代码可以直接移植。这颗芯片上的定时器资源大致可以分为五类TIMER1/TIMER216位通用定时器面向运行态的高频任务支持PWM输出、输入捕获、输出比较通常还有中心对齐和互补输出能力。LPTIM32位低功耗定时器专门为深度睡眠场景设计可以由低速时钟驱动在STOP模式下继续计数并唤醒系统。RTC实时时钟提供日历和闹钟功能适合分钟级甚至天级的长期定时。WDT看门狗定时器防止程序跑飞低功耗流程里必须谨慎处理。SysTickCortex-M0内核自带的24位递减计数器在RTOS工程中一般被OS调度器占用。很多刚接触这颗芯片的人会犯一个错误把TIMER1、TIMER2当成普通单片机里的通用定时器拿起就配置完全不关心时钟树。结果就是PWM频率算不对或者进低功耗后整个系统反而更耗电。1.2 定时器的时钟域分配与选型原则从时钟树角度看TIMER1/TIMER2挂接在APB总线时钟下最高48MHz通常由系统PLL输出驱动。LPTIM则比较特殊它的时钟源可以在低速时钟和APB时钟之间选择低功耗模式下为了保证它在STOP状态中继续计数必须让它使用LSI或LSE这类不受系统主时钟关断影响的时钟源。选型逻辑可以归纳成一张简单的对照表应用场景首选定时器原因高频PWM输出如蜂鸣器、LED调光TIMER1/TIMER2分辨率高可配置频率范围大脉冲宽度测量、输入捕获TIMER1/TIMER2硬件级捕获不受中断延迟影响深度睡眠周期唤醒LPTIM可由低速时钟驱动STOP下继续计数日历、闹钟、长期定时RTC独立日历计数可分钟级/天级定时RTOS任务调度SysTick内核自带OS默认占用一个常见的需求是低功耗蓝牙设备每5秒醒来一次采集传感器数据。这种场景用TIMER1/TIMER2去做就意味着主控必须每5秒从STOP唤醒一次处理定时器中断而TIMER本身在STOP模式下无法保持计数需要靠RTC或LPTIM。所以我在实际项目中几乎不会把通用定时器用在低功耗唤醒路径上这个任务从一开始就交给LPTIM或RTC。1.3 协议栈占用与用户可用资源在实际BLE工程里另一个关键问题哪些定时器已经被协议栈占用了。ST的BLE协议栈和射频驱动在底层会使用部分定时器资源例如连接事件调度、扫描窗口控制、HAL tick等都可能占用TIMER或SysTick。如果用户代码盲目把某个TIMER重新初始化可能造成射频时序错乱表现为连接不稳定、广播间隔异常、甚至无法被手机扫描到。我的建议是新建工程后先打开SDK中的初始化代码和外设分配文档确认哪些定时器已经由协议栈或OS占用不要直接上手改。在ST提供的BlueNRG-LP SDK中用户应用通常是运行在RTOS任务里的SysTick已经被OS接管应用层需要额外的时间基准时自己找TIMER或LPTIM补齐。这里我踩过一次坑最初想用SysTick做毫秒延时结果发现RTOS调度全部依赖它重写SysTick_Handler导致任务切换完全错乱。后来才意识到在OS环境下应用层的延时应该走osDelay这类接口而不是自己操作SysTick。2. TIMER1/TIMER2通用定时器PWM与输入捕获实战2.1 TIMER的基本结构与计数模式TIMER是16位向上计数器带有可编程预分频器和自动重载寄存器。预分频寄存器从0开始实际分频值是Prescaler1自动重载寄存器ARR决定计数周期计数器从0计到ARR后产生更新事件并重新开始。频率计算公式为定时器频率 定时器时钟 / ((Prescaler1) × (Autoreload1))例如APB时钟为48MHz预分频设为47自动重载设为999那么计数器频率是1MHzPWM周期是1000个计数最终输出频率为1kHz。占空比则由比较寄存器CCR决定PWM输出为高电平的持续时间等于CCR个计数周期即占空比 CCR / (Autoreload 1)。这部分难度不大但有一个细节容易被忽略自动重载寄存器和比较寄存器是否开启预装载。如果关闭预装载修改ARR或CCR会立即生效可能导致当前PWM周期长度突变在电机驱动或调光场景中表现为肉眼可见的抖动。正确做法是开启预装载让新值在下一个更新事件时同步加载。/* 使能 TIMER1 时钟 */ LL_APB_EnableClock(LL_APB_PERIPH_TIMER1); /* 初始化时基48MHz / 48 1MHz自动重载999 1kHz */ LL_TIM_InitTypeDef TIM_InitStruct {0}; TIM_InitStruct.Prescaler 47; TIM_InitStruct.Autoreload 999; TIM_InitStruct.AutoreloadMode LL_TIM_ARRLOAD_PRELOAD; /* 开启自动重载预装载 */ LL_TIM_Init(TIMER1, TIM_InitStruct);2.2 PWM输出的关键配置与死区配置PWM输出的核心流程分五步初始化GPIO复用功能、使能TIMER时钟、配置时基参数、配置输出比较通道模式、使能通道输出和计数器。顺序上建议先配置GPIO再初始化定时器避免输出引脚在定时器使能瞬间出现不确定电平。/* 通道1 PWM模式初始化 */ LL_TIM_OC_InitTypeDef OC_InitStruct {0}; OC_InitStruct.OCMode LL_TIM_OCMODE_PWM1; OC_InitStruct.CompareValue 500; /* 占空比 50% */ OC_InitStruct.CompareMode LL_TIM_CCLOAD_PRELOAD; LL_TIM_OC_Init(TIMER1, LL_TIM_CHANNEL_CH1, OC_InitStruct); /* 使能通道输出与计数器 */ LL_TIM_CC_EnableChannel(TIMER1, LL_TIM_CHANNEL_CH1); LL_TIM_EnableCounter(TIMER1);在电机驱动或H桥应用中两个互补PWM之间需要插入死区时间防止上下桥臂短时间内直通。死区长度由死区寄存器换算不同芯片的换算公式不完全一致建议直接查参考手册中的对照表不要凭经验拍脑袋设置。还要注意刹车输入功能它用于故障时硬件级强制关闭PWM输出比中断里做保护更可靠。有一个实际经验修改PWM占空比时如果直接写比较寄存器占空比会立即变化。某些负载比如蜂鸣器对这种瞬时变化不敏感但驱动LED灯时可能有轻微的亮度跳变。所以批量更新占空比时最好在同一个更新事件内完成所有通道的比较值修改保持输出同步。2.3 输入捕获与编码器模式的补充输入捕获功能在调试那些输出脉冲信号的传感器时特别有用比如流量计、风速计、红外测距模块。以测脉冲宽度为例把捕获通道配置为上升沿触发记录第一次上升沿的计数器值下一次上升沿再来时两次计数值之差乘以计数周期就是信号周期。对占空比的测量还需要同时捕获高电平和低电平的跳变。编码器模式则用于正交编码器计数常见于旋钮和直流电机转速检测。两个通道的相位关系决定计数方向硬件自动完成加减计数不占用CPU。这个功能在需要闭环控制的BLE设备里会用到比如智能窗帘的电机反馈。实际使用中输入捕获最怕的是中断处理不及时。如果捕获中断里做了太多无关操作下一次捕获事件来了还没处理完就会丢事件。我的做法是中断里只做最小操作——读取捕获值、存到全局变量、清标志具体的数据换算和业务逻辑放到主循环或RTOS任务里处理。2.4 阻塞式延时为什么不该用TIMER有人会把TIMER直接用作阻塞延时硬件定时确实准但有两个问题。一是TIMER资源宝贵BLE产品往往同时需要PWM输出和低功耗唤醒几个需求一叠通用定时器就不够用了。二是TIMER做阻塞延时和低功耗设计天然矛盾——阻塞等待期间CPU无法进入STOP模式设备电流高企这对电池供电的产品是致命的。我的做法是分层处理毫秒级延时用RTOS的延时接口微秒级短延时用循环加读取TIMER计数值实现自旋时间短对功耗影响可控长时间周期唤醒交给LPTIM或RTC。这样每个定时器做它最擅长的事既保证精度又不破坏低功耗架构。3. LPTIM低功耗定时器休眠场景下真正的主角3.1 LPTIM的时钟源与低功耗能力LPTIM和TIMER最大的区别在于它可以由LSE/32.768kHz晶振或LSI/内部低速振荡器驱动在系统进入STOP模式后继续计数并在比较值到达时产生事件把MCU唤醒。这颗定时器就是为深度睡眠场景准备的。配置LPTIM时最容易踩坑的地方就是时钟源。如果你把LPTIM时钟源选成了APB总线时钟比如HSI或PLL一旦系统进入STOPAPB时钟停止LPTIM跟着停摆根本谈不上唤醒。只有确保时钟源是LSE或LSILPTIM才能在深度睡眠中继续工作。/* 选择 LSI 作为 LPTIM 时钟源 */ LL_LPTIM_SetClockSource(LPTIM, LL_LPTIM_CLK_SOURCE_LSI); /* 预分频1比较值32000在32kHz时钟下对应1秒 */ LL_LPTIM_SetPrescaler(LPTIM, LL_LPTIM_PRESCALER_DIV1); LL_LPTIM_SetCompare(LPTIM, 32000); /* 使能LPTIM并启动单次计数 */ LL_LPTIM_Enable(LPTIM); LL_LPTIM_StartCounter(LPTIM, LL_LPTIM_OPERATING_MODE_SINGLE);32位LPTIM在32kHz时钟下的最长计数时间大约是37小时绝大多数低功耗场景都够用。3.2 LPTIM在周期性唤醒中的应用典型场景温度采集节点每5秒醒来一次采集数据其余时间深度睡眠。实现步骤配置LPTIM时钟源为LSI预分频1比较值设为1600005秒。使能LPTIM中断并清空标志位。进入STOP模式前调用启动计数接口进入单次计数模式。STOP模式下LPTIM继续计数到达比较值产生唤醒事件。唤醒后先清除中断标志再采集数据和发送BLE广播。处理完成后再次启动LPTIM进入下一次STOP。这个流程看起来简单实际有个隐患唤醒后如果代码逻辑里忘了清除LPTIM比较标志直接再次进入STOP可能会立刻被唤醒形成假死循环表现为系统永远无法真正休眠功耗反而比其他方案更高。所以每次唤醒后第一件事是清标志不要等到最后才处理。3.3 使用LPTIM的注意事项综合几个项目和SDK迭代的经验LPTIM有几点需要特别留意第一初始化顺序不要乱。先配置时钟源和预分频再设置比较值最后启动。某些SDK版本中如果比较值在时钟源未稳定前写入可能出现首次计数异常。第二LPTIM启动后需要等待几个低速时钟周期才能稳定。启动后立刻检查状态寄存器或比较标志可能因为同步还没完成而读取到旧值。稳妥做法是启动后加一个小延时或者轮询使能状态位确认。第三在BLE连接保持的工程里LPTIM唤醒要和BLE协议栈的睡眠管理协同。如果应用层的LPTIM唤醒时间点正好卡在连接事件附近可能出现唤醒后射频还没准备好就要收发数据的情况表现为偶发性丢包。我的做法是在连接间隔之外设置唤醒点并允许少量时间余量。第四不断进入退出STOP模式也有代价。每次唤醒和重新进入STOP都有固定开销如果唤醒周期太短功耗反而比一直运行还高。一般唤醒周期低于10毫秒时就要评估是否值得进STOP了。4. 定时器延时方案的取舍SysTick、TIMER与RTC的分工4.1 SysTick给标准库和RTOS让路在BlueNRG-LP的SDK工程中SysTick通常被RTOS内核占用用于任务调度和osDelay等时间接口。开发者如果自己去重写SysTick_Handler会导致系统时间基准漂移任务调度混乱表现为延时不准、任务优先级反转等奇怪问题。Cortex-M0核的SysTick是24位递减计数器最大计数周期在48MHz下约为349毫秒。即便不跑RTOS只要你在用ST的BLE协议栈库协议栈的HAL层也可能依赖SysTick。所以我的建议很简单SysTick默认当它不存在应用层时间基准不要直接碰它。4.2 用TIMER实现精确微秒延时BlueNRG-LP是Cortex-M0内核没有DWT周期计数器这类高级调试功能高精度微秒延时的实现要靠TIMER配合。方法是把某个TIMER配置为1MHz计数源然后通过读取计数器值实现短延时。/* 定时器已初始化为1MHz计数预分频47自由运行模式 */ void delay_us_timer(TIMER_TypeDef* tim, uint32_t us) { uint32_t start LL_TIM_GetCounter(tim); while ((uint32_t)(LL_TIM_GetCounter(tim) - start) us) { } }这段代码利用了计数器回绕的特性两次计数值相减是无符号差值即使计数器发生回绕只要延时时间不超过计数器最大周期1MHz下16位计数器为65.535毫秒结果就是正确的。实际使用中这种自旋等待方式的微秒延时适合驱动DS18B20、DHT11这类单总线传感器。但要注意两点一是延时期间中断无法响应如果系统正在跑BLE连接长时间自旋会破坏射频时序所以只建议在初始化阶段或非连接状态下使用二是长延时不建议用这招几十毫秒以上的延时应该交给RTOS或低功耗定时器。4.3 RTC唤醒替代长定时的更好选择分钟级、小时级甚至天级的长期定时用LPTIM也能做但我更推荐RTC。RTC自带日历功能可以设置闹钟在STOP模式下由LSE驱动功耗和LPTIM相当。它的优势在于记录的是绝对时间方便对时和定时任务。举个例子设备需要每天上午9点上报一次状态用RTC闹钟是最自然的做法用LPTIM则需要计算从当前时刻到目标时刻的秒数再换算成计数值逻辑上绕了一圈。RTC还有一个LPTIM不具备的特性时间戳。某些需要记录事件发生时间的应用比如门磁报警、震动检测用RTC时间戳可以直接得到几分钟前发生的事件这样的信息。LPTIM只能给出相对时间如果系统在两次唤醒之间漏掉了事件时间线就对不上了。5. 看门狗、RTC与定时器配合别让看门狗吃掉功耗5.1 WDT在低功耗流程里的存在感看门狗在低功耗蓝牙设备里是个容易被忽视的功耗来源。如果WDT由LSI驱动并持续运行即使频率只有32kHz芯片在STOP模式下的唤醒源多了一个有时会影响进入最深睡眠状态的可能性。更麻烦的是如果WDT超时时间设置得比最长睡眠周期还短设备会在深度睡眠中被看门狗复位表现为周期性重启、状态参数丢失。实际项目中我一般把WDT超时时间设得比最长睡眠周期大一个数量级确保睡眠期间不会触发复位。还有一个细节睡眠前如果能临时关闭WDT就关闭不能关闭就确保超时时间覆盖整个睡眠周期。另外从STOP模式唤醒后WDT可能继续工作主循环里要及时喂狗否则唤醒后几秒内就复位了。5.2 RTC与LPTIM的分工把多个定时器放在一张图里看分工就很清晰定时器擅长场景典型用途TIMER1/TIMER2运行态高频任务PWM、输入捕获、输出比较LPTIM短周期低功耗唤醒秒级周期采样、事件唤醒RTC长期定时、绝对时间每日上报、日历闹钟、时间戳SysTickRTOS时间基准任务调度、系统TickWDT异常复位保护程序跑飞检测这里建议记住一条原则越是靠近长期定时的场景越要选择功耗低、独立时钟的定时器。RTC和LPTIM都满足这个条件但RTC更适合到点才动的场景LPTIM更适合每隔固定时间就动的场景。两者配合使用时还可以用RTC做天级校准用LPTIM做秒级唤醒。5.3 喂狗的最佳位置与常见死循环喂狗代码的最佳位置是主循环和关键任务完成之后不要放在定时器中断里。为什么如果你把喂狗放到定时器中断一旦主循环卡死但中断还在正常触发看门狗形同虚设。定时器中断只是证明了中断系统正常并不能证明业务逻辑正常。我的做法是在主循环末尾喂狗同时在重要任务里加一个软件计数器。比如每次BLE广播成功或传感器读取完成后把计数器加一。喂狗之前检查计数器是否在增长如果一段时间内无增长说明业务卡死这时才允许看门狗复位。这样可以更快定位是哪个任务异常。另外SDK里常见的一个死循环是等待某个标志位被置位如果中断没有发生循环永远出不来看门狗就成了唯一的救命稻草。所以调试阶段千万不要把WDT关闭太久等发现问题再打开可能已经错过了定位时机。6. 实测中的几个坑与排查方法6.1 PWM输出不正常的排查顺序如果PWM输出完全无信号或波形异常我建议按以下顺序排查而不是上来就怀疑定时器寄存器配置GPIO复用功能有没有选对引脚PA/PB复用映射不是默认值必须显式配置。定时器时钟有没有使能LL库或HAL库的时钟使能接口遗漏是最常见问题。计数器有没有启动只配置时基不调用使能计数接口不会输出波形。PWM模式和极性有没有反PWM1和PWM2的极性逻辑不一样输出反相时先检查这里。比较值是否超过自动重载值CCR大于ARR时输出恒高CCR为0时输出恒低。互补通道和死区设置是否合法死区时间太长会直接吃掉PWM有效脉冲表现是输出频率正常但占空比偏小。排错最好的工具是逻辑分析仪或示波器。如果没有硬件工具可以先把PWM频率降到1Hz用万用表观察电平变化再用GPIO翻转方式验证IO口本身是否正常。6.2 LPTIM在STOP模式下不工作的根因LPTIM在STOP模式下不工作的原因按概率排序时钟源选择错误选了APB时钟而不是LSI/LSE这是最常见的根因。中断没有使能LPTIM比较事件已经发生但NVIC里没开对应中断系统无法被唤醒。比较值超出范围32位定时器也有上限时钟越高可用时间越短。标志位未清除上次唤醒后标志没清再次进入STOP后立即唤醒看似不工作实际是一直在唤醒。电源模式不匹配某些低功耗模式下外设电源域会被关闭需要确认芯片的具体低功耗模式是否保留LPTIM电源。排查方式比较直接先在正常运行状态下查看LPTIM是否计数读计数器值变化再进STOP模式后用电流表观察唤醒行为。如果正常运行时计数正常、进STOP后不唤醒重点查时钟源和NVIC如果进STOP后电流异常偏高重点查标志位和唤醒循环。6.3 从STOP唤醒后时钟状态从STOP模式唤醒后系统时钟可能回落到HSI 16MHz而不是之前的PLL 48MHz。如果应用代码没有重新配置PLL单片机依然正常工作但所有依赖总线时钟的定时器频率都会按比例降低比如串口波特率漂移、PWM频率下降、SysTick周期变化。这个问题的典型表现是功能看起来正常但外设频率全都不对。比如同一个PWM配置从STOP唤醒后频率变成了原来的三分之一蜂鸣器声音变闷LED调光曲线异常。排查方法是唤醒后读系统时钟状态寄存器确认PLL是否锁定如果没有在唤醒流程中重新完成PLL配置再继续业务逻辑。6.4 中断优先级影响BLE连接的稳定性用户定时器中断的优先级设置直接影响BLE连接的稳定性。如果用户定时器中断优先级高于BLE协议栈的中断优先级一旦定时器中断触发频繁就会在射频收发关键处理期间打断协议栈导致连接事件处理超时表现为连接断开或丢包率上升。我遇到过一个问题把TIMER中断优先级设成最高用于每100微秒读取一次传感器结果BLE连接变得极不稳定手机靠近时偶尔断连距离稍远一点几乎连不上。后来把定时器中断优先级降低只在BLE协议栈空闲期间响应连接立刻恢复正常。建议是尽量用SDK提供的回调机制和默认优先级配置不要自己随意调整NVIC优先级分组。用户应用的中断响应时间要求再高也不能牺牲射频时序。这些坑我基本都踩过一遍而且每一个都是那种代码看着没问题但实际表现就是不正常的类型。定时器在BlueNRG-LP里不算复杂外设但因为它和功耗管理、BLE协议栈、时钟树都耦合在一起调起来才这么有意思。每次新项目开始时我都会把这篇笔记里的排查清单检查一遍省下来的调试时间够多喝好几杯咖啡了。