# STM32 低功耗模式下使用 LPTIM 进行异步脉冲计数:配置技巧与实战 在物联网传感器、智能水表等电池供电设备中,主控MCU大部分时间应处于低功耗模式。若需对外部脉冲(如流量计、风速计)计数,传统做法是让MCU定期唤醒并读取GPIO状态,这会导致频繁唤醒,功耗高达数十微安甚至毫安级。而STM32的LPTIM(Low-Power Timer)专为此场景设计,它能在停机(Stop)模式下由外部信号异步驱动,独立于CPU运行,仅在计数溢出或比较匹配时唤醒CPU。本文将详解其原理与配置。 ## 一、LPTIM异步计数原理 LPTIM是一个16位低功耗定时器,其核心特性是: - **异步时钟源**:可使用外部时钟(如LSE 32.768kHz)或外部触发信号(如GPIO)作为计数时钟,无需系统时钟(HSI/HSE)。 - **低功耗运行**:在CPU进入Stop模式后,LPTIM仍可依靠LSE或外部信号继续运行。 - **事件唤醒**:当计数值达到预设的自动重载值(ARR)或比较值(CMP)时,可产生中断并唤醒CPU。 对于脉冲计数,我们通常将外部脉冲信号连接到LPTIM的输入端(如LPTIM1_IN1),配置为外部时钟源模式。每个脉冲上升沿(或下降沿)触发一次计数,当计数值等于ARR时,产生唤醒事件。这样,CPU只需在每N个脉冲后醒来处理一次,功耗大幅降低。 ## 二、硬件连接与引脚选择 以STM32L4系列为例,LPTIM1的输入引脚通常为: - LPTIM1_IN1:PC0(或PA6,取决于封装) - LPTIM1_IN2:PC1(或PA7) 请查阅具体型号的数据手册,确认引脚复用功能。硬件上,外部脉冲信号需经过电平转换(如3.3V逻辑)后直接连接至输入引脚,无需额外上拉(内部可配置)。注意脉冲频率应低于LPTIM最大计数频率(通常为LSE/2或外部信号最大频率)。 ## 三、关键寄存器配置步骤 配置分为初始化、低功耗模式设置、中断处理三部分。 ### 1. 初始化LPTIM(正常模式) ```c // 使能LPTIM时钟(以LPTIM1为例) __HAL_RCC_LPTIM1_CLK_ENABLE(); // 配置LPTIM输入引脚(PC0)为复用功能 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; // 根据信号类型选择 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate = GPIO_AF2_LPTIM1; // 查手册确认AF号 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 配置LPTIM参数 LPTIM_HandleTypeDef hlptim1; hlptim1.Instance = LPTIM1; hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK; // 初始化时用APB时钟,便于配置 hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV1; hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE; // 软件触发启动 hlptim1.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH; hlptim1.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE; hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_EXTERNAL; // 外部脉冲计数 hlptim1.Init.Input1Source = LPTIM_INPUT1SOURCE_GPIO; // 输入1来自GPIO hlptim1.Init.Input2Source = LPTIM_INPUT2SOURCE_GPIO; HAL_LPTIM_Init(&hlptim1); // 设置自动重载值(例如每100个脉冲唤醒一次) HAL_LPTIM_SetAutoreload(&hlptim1, 100); // 使能比较中断(可选,若需在特定值中断) HAL_LPTIM_Compare_Set(&hlptim1, 50); // 比较值50 HAL_LPTIM_Compare_Start_IT(&hlptim1, 50); // 使能比较中断 ``` ### 2. 进入低功耗模式前切换时钟源 为了在Stop模式下继续计数,需将LPTIM时钟切换为LSE(或外部时钟)。在进入Stop模式前,执行: ```c // 使能LSE时钟(若未开启) __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET); // 等待就绪 // 将LPTIM时钟源切换为LSE __HAL_RCC_LPTIM_CONFIG(RCC_LPTIMCLKSOURCE_LSE); // 重新初始化LPTIM,使用LSE时钟 hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_LSE; HAL_LPTIM_Init(&hlptim1); // 重新设置ARR和比较值(因为时钟变了,计数频率不同) HAL_LPTIM_SetAutoreload(&hlptim1, 100); HAL_LPTIM_Compare_Set(&hlptim1, 50); // 启动LPTIM计数(软件触发) HAL_LPTIM_Counter_Start(&hlptim1, 100); // 启动并设置ARR ``` ### 3. 进入Stop模式并等待唤醒 ```c // 使能LPTIM中断(在NVIC中) HAL_NVIC_SetPriority(LPTIM1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(LPTIM1_IRQn); // 进入Stop模式(使用WFI) HAL_SuspendTick(); // 暂停SysTick,避免唤醒 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后恢复 HAL_ResumeTick(); SystemClock_Config(); // 重新配置系统时钟(因为Stop模式可能关闭HSE) ``` ### 4. 中断服务函数 ```c void LPTIM1_IRQHandler(void) { HAL_LPTIM_IRQHandler(&hlptim1); } void HAL_LPTIM_CompareMatchCallback(LPTIM_HandleTypeDef *hlptim) { // 处理计数达到比较值的事件 uint32_t count = HAL_LPTIM_ReadCounter(hlptim); // 读取当前计数值 // 例如:累计脉冲数,更新状态,发送数据等 // 注意:此处应快速处理,避免长时间占用 } void HAL_LPTIM_AutoReloadMatchCallback(LPTIM_HandleTypeDef *hlptim) { // 处理计数达到ARR的事件(如果使能了自动重载中断) } ``` ## 四、完整代码示例(基于STM32L4 HAL库) 以下是一个完整示例,实现每100个脉冲唤醒一次,并累计计数。 ```c #include "stm32l4xx_hal.h" LPTIM_HandleTypeDef hlptim1; volatile uint32_t pulse_count = 0; // 累计脉冲数 void LPTIM1_Init(void) { // 使能GPIOA和LPTIM时钟 __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_LPTIM1_CLK_ENABLE(); // PC0 -> LPTIM1_IN1 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate = GPIO_AF2_LPTIM1; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 初始化LPTIM(先使用APB时钟) hlptim1.Instance = LPTIM1; hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK; hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV1; hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE; hlptim1.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH; hlptim1.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE; hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_EXTERNAL; hlptim1.Init.Input1Source = LPTIM_INPUT1SOURCE_GPIO; hlptim1.Init.Input2Source = LPTIM_INPUT2SOURCE_GPIO; HAL_LPTIM_Init(&hlptim1); // 使能LSE时钟 __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET); // 切换LPTIM时钟源为LSE __HAL_RCC_LPTIM_CONFIG(RCC_LPTIMCLKSOURCE_LSE); // 重新初始化LPTIM(使用LSE) HAL_LPTIM_DeInit(&hlptim1); hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_LSE; HAL_LPTIM_Init(&hlptim1); // 设置ARR为100,比较值为50(可选) HAL_LPTIM_SetAutoreload(&hlptim1, 100); HAL_LPTIM_Compare_Set(&hlptim1, 50); // 使能比较中断 HAL_LPTIM_Compare_Start_IT(&hlptim1, 50); // 启动计数(软件触发) HAL_LPTIM_Counter_Start(&hlptim1, 100); // 使能NVIC中断 HAL_NVIC_SetPriority(LPTIM1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(LPTIM1_IRQn); } void EnterStopMode(void) { HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); HAL_ResumeTick(); SystemClock_Config(); // 重新配置系统时钟 } void LPTIM1_IRQHandler(void) { HAL_LPTIM_IRQHandler(&hlptim1); } void HAL_LPTIM_CompareMatchCallback(LPTIM_HandleTypeDef *hlptim) { pulse_count += 50; // 每50个脉冲触发一次(因为比较值50) // 注意:实际计数可能因ARR溢出而丢失,需结合ARR中断处理 // 此处简化,实际应读取计数器并计算 } int main(void) { HAL_Init(); SystemClock_Config(); LPTIM1_Init(); while (1) { // 进入Stop模式,等待脉冲计数达到比较值唤醒 EnterStopMode(); // 唤醒后处理业务,例如发送数据、更新显示等 // 注意:此时LPTIM仍在计数,但CPU已运行 } } ``` ## 五、注意事项与优化技巧 - **时钟源选择**:LSE(32.768kHz)是常用选择,但LPTIM的最大计数频率有限(通常为LSE/2),若脉冲频率过高,需使用外部时钟直接输入(如LPTIM_IN1作为时钟),但此时无法使用LSE,需权衡。 - **计数溢出处理**:LPTIM是16位,最大计数65535。若脉冲数可能超过,需在中断中读取计数器并累加,或使用ARR中断(每65535个脉冲唤醒一次)来避免溢出丢失。 - **中断唤醒延迟**:从Stop模式唤醒到CPU执行中断服务函数有延迟(通常几微秒),若脉冲频率极高,可能丢失脉冲。建议脉冲频率低于1kHz。 - **GPIO滤波**:外部脉冲可能含有毛刺,可在GPIO配置中启用数字滤波(如使用LPTIM的输入滤波器),但会增加功耗和延迟。 - **调试技巧**:在调试时,可暂时禁用低功耗模式,用逻辑分析仪观察LPTIM输出(如MFO引脚)来验证计数。 - **功耗优化**:在Stop模式下,关闭不必要的外设时钟,将未使用的GPIO设为模拟输入或低功耗状态。 ## 六、总结 通过LPTIM的异步计数功能,STM32可以在Stop模式下保持对脉冲的精准计数,仅在达到预设阈值时唤醒CPU,将平均功耗降至微安级。本文提供了从原理到代码的完整指南,帮助开发者快速实现低功耗脉冲计数方案。在实际项目中,还需根据具体脉冲特性(频率、电平)调整配置,并充分测试边界情况。