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(正常模式)

// 使能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模式前,执行:

// 使能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模式并等待唤醒

// 使能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. 中断服务函数

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个脉冲唤醒一次,并累计计数。

#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,将平均功耗降至微安级。本文提供了从原理到代码的完整指南,帮助开发者快速实现低功耗脉冲计数方案。在实际项目中,还需根据具体脉冲特性(频率、电平)调整配置,并充分测试边界情况。