引言

在物联网传感器节点、可穿戴设备等场景中,系统常需在“沉睡”与“苏醒”间快速切换。STM32F4 系列(如 STM32F411)的 STOP 模式可将功耗降至 10µA 以下,但默认的唤醒源(如 RTC 闹钟)分辨率仅为秒级,难以满足微秒级采样或同步需求。LPTIM 作为低功耗定时器,可在 STOP 模式下运行,且支持外部时钟源(如 LSE),理论上可实现微秒级定时。然而,STOP 模式下 SRAM 内容保留,但外设寄存器会丢失,若 RTC 配置被意外复位,将导致时间基准错乱。本文将提供一套完整方案,实现 LPTIM 微秒唤醒且 RTC 计数不丢失。

原理剖析

1. STOP 模式下的时钟与外设状态

  • STOP 模式:内核时钟停止,但 SRAM、备份域(含 RTC)保持供电。所有外设时钟(PCLK1/2)被关闭,外设寄存器内容丢失(除非配置为备份域或具有独立供电)。
  • LPTIM 特殊性:LPTIM 可由 LSE(32.768kHz)或 LSI(约 32kHz)驱动,在 STOP 模式下继续计数。其预分频器可产生低至 1/32768 秒(约 30.5µs)的时基,通过配置比较值可实现微秒级唤醒。
  • RTC 计数保持:RTC 位于备份域,由 VBAT 供电,即使主电源切断也能运行。但在 STOP 模式下,若 RTC 的 APB 接口时钟关闭,其寄存器仍可通过备份域访问,但需注意:若代码在唤醒后重新初始化 RTC,会覆盖原有计数。

2. 关键设计思路

  • LPTIM 唤醒:配置 LPTIM 为单次计数模式,比较值设为所需微秒数对应的时钟周期数。唤醒后,通过中断标志位判断唤醒源。
  • RTC 计数保护:在进入 STOP 前,将 RTC 的当前计数(如秒计数)保存到备份寄存器(RTC_BKPxR)中。唤醒后,先读取备份值,再与当前 RTC 值比对,若发现 RTC 被复位(计数归零),则用备份值恢复。
  • 时钟源选择:LPTIM 使用 LSE 时,精度高(32.768kHz),但需注意 LSE 的启动时间;LSI 精度稍差(±1%),但启动快。

配置步骤

1. 初始化 RTC 与备份寄存器

void RTC_Init(void) {
    // 使能 PWR 时钟,允许访问备份域
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();
    
    // 使能 LSE
    __HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
    while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET);
    
    // 配置 RTC 时钟源为 LSE,并使能 RTC
    __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSE);
    __HAL_RCC_RTC_ENABLE();
    
    // 初始化 RTC 为秒中断模式(示例)
    RTC_HandleTypeDef hrtc = {0};
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;   // 异步预分频
    hrtc.Init.SynchPrediv = 255;    // 同步预分频,得到 1Hz
    HAL_RTC_Init(&hrtc);
    
    // 设置初始时间(可选)
    RTC_TimeTypeDef sTime = {0};
    sTime.Hours = 0; sTime.Minutes = 0; sTime.Seconds = 0;
    HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
}

// 保存当前 RTC 秒计数到备份寄存器
void RTC_SaveCounter(void) {
    RTC_TimeTypeDef sTime;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    uint32_t seconds = sTime.Hours * 3600 + sTime.Minutes * 60 + sTime.Seconds;
    HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR0, seconds);
}

// 恢复 RTC 计数(若被复位)
uint32_t RTC_RestoreCounter(void) {
    uint32_t saved = HAL_RTCEx_BKUPRead(RTC, RTC_BKP_DR0);
    RTC_TimeTypeDef sTime;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    uint32_t current = sTime.Hours * 3600 + sTime.Minutes * 60 + sTime.Seconds;
    if (current < saved) { // 检测到复位(当前值小于保存值)
        // 重新设置 RTC 时间为保存值
        RTC_TimeTypeDef newTime;
        newTime.Hours = saved / 3600;
        newTime.Minutes = (saved % 3600) / 60;
        newTime.Seconds = saved % 60;
        HAL_RTC_SetTime(&hrtc, &newTime, RTC_FORMAT_BIN);
        return saved;
    }
    return current;
}

2. 配置 LPTIM 用于微秒唤醒

// 假设 LSE 为 32.768kHz,预分频 1,则计数周期约 30.5µs
// 若需 100µs 唤醒,则比较值 = 100 / 30.5 ≈ 3(取整)
void LPTIM_Init(uint32_t compare_value) {
    // 使能 LPTIM 时钟(在 STM32F4 中,LPTIM 位于 APB1,但需在 STOP 前配置)
    __HAL_RCC_LPTIM1_CLK_ENABLE();
    
    // 选择 LSE 作为 LPTIM 时钟源
    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_LPTIM1;
    PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;
    HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
    
    // 初始化 LPTIM
    LPTIM_HandleTypeDef hlptim = {0};
    hlptim.Instance = LPTIM1;
    hlptim.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK; // 实际由 LSE 驱动
    hlptim.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV1;
    hlptim.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE;
    hlptim.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH;
    hlptim.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE;
    hlptim.Init.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL;
    HAL_LPTIM_Init(&hlptim);
    
    // 设置比较值
    HAL_LPTIM_SetOnceMode(&hlptim, compare_value);
    
    // 使能中断
    HAL_NVIC_SetPriority(LPTIM1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(LPTIM1_IRQn);
}

// 进入 STOP 模式并等待 LPTIM 唤醒
void EnterStopWithLPTIM(uint32_t compare_value) {
    // 保存 RTC 计数
    RTC_SaveCounter();
    
    // 配置 LPTIM 比较值
    LPTIM_Init(compare_value);
    
    // 启动 LPTIM 单次计数
    HAL_LPTIM_Start_IT(&hlptim, compare_value);
    
    // 进入 STOP 模式
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    
    // 唤醒后,重新配置系统时钟(STOP 模式会丢失时钟配置)
    SystemClock_Config();
    
    // 恢复 RTC 计数(若被复位)
    RTC_RestoreCounter();
    
    // 停止 LPTIM
    HAL_LPTIM_Stop_IT(&hlptim);
}

// LPTIM 中断处理
void LPTIM1_IRQHandler(void) {
    HAL_LPTIM_IRQHandler(&hlptim);
}

void HAL_LPTIM_CompareMatchCallback(LPTIM_HandleTypeDef *hlptim) {
    // 唤醒后的处理,如读取传感器等
    // 注意:此时系统时钟尚未恢复,避免复杂操作
}

3. 主程序示例

int main(void) {
    HAL_Init();
    SystemClock_Config();
    RTC_Init();
    
    while (1) {
        // 进入 STOP 模式,100µs 后唤醒
        EnterStopWithLPTIM(3); // 3 * 30.5µs ≈ 91.5µs
        
        // 唤醒后执行任务
        uint32_t current_time = RTC_RestoreCounter();
        printf("Wake at %lu s\n", current_time);
        HAL_Delay(1000); // 模拟任务
    }
}

注意事项

  • LPTIM 时钟源选择:LSE 精度高但启动慢(约 1-2 秒),若系统频繁启停,建议使用 LSI(约 32kHz,精度 ±1%),但需校准。
  • 比较值计算:微秒换算公式:compare_value = (time_us * clock_hz) / 1000000。例如 LSE 32.768kHz,100µs 对应 3.2768,取整为 3,实际唤醒时间约 91.5µs,误差可接受。
  • RTC 备份寄存器:备份寄存器数量有限(通常 20 个),且掉电后由 VBAT 供电,需确保 VBAT 连接电池或大电容。
  • 唤醒后时钟恢复:STOP 模式会关闭 PLL 和 HSI,唤醒后必须重新配置系统时钟,否则外设工作异常。建议在中断中仅设置标志,主循环中恢复时钟。
  • 中断优先级:LPTIM 中断优先级应高于 RTC 闹钟(若同时使用),避免竞争。
  • 调试技巧:使用逻辑分析仪测量 GPIO 翻转时间,验证实际唤醒延迟。

总结

通过 LPTIM 与 RTC 的协同,STM32F4 在 STOP 模式下可实现微秒级定时唤醒,同时利用备份寄存器保护 RTC 计数,确保时间基准不丢失。此方案适用于需要高精度周期采样的低功耗场景,如环境监测、工业传感器等。开发者可根据实际需求调整 LPTIM 时钟源和比较值,平衡功耗与精度。