第三章 唤醒源配置:EXTI、RTC、定时器与USART

低功耗模式选好了,芯片也睡下了。但怎么把它叫醒?这才是真正见功夫的地方。

我见过不少工程师,功耗测出来漂亮得很,结果产品一上电就睡死过去,再也醒不过来。说白了,唤醒源的配置没做对。

这一章,咱们就把四种最常用的唤醒方式掰开揉碎讲清楚。EXTI、RTC、定时器、USART,每个我都踩过坑,咱们一个一个来。

3.1 EXTI外部中断唤醒

EXTI是最直接的唤醒方式。一个引脚电平变化,就能把芯片从睡梦中拉回来。

核心要点:在进入低功耗模式之前,必须确保EXTI线已经配置好,并且中断使能已经打开。否则,你按破按钮芯片都不会理你。

3.1.1 配置步骤

  1. 使能SYSCFG时钟(用于EXTI与GPIO的连线)
  2. 配置GPIO为输入模式,选择上下拉
  3. 将GPIO连接到对应的EXTI线
  4. 配置EXTI触发边沿(上升沿/下降沿/双边沿)
  5. 使能EXTI中断(非事件)
  6. 配置NVIC优先级,使能中断通道
  7. 编写中断服务函数,清除挂起位
// 以PA0为例,配置EXTI0唤醒
void EXTI_WakeUp_Init(void)
{
    // 1. 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    
    // 2. 配置PA0为输入,上拉
    GPIOA->CRL &= ~(0x0F << 0);  // 清零
    GPIOA->CRL |= (0x08 << 0);   // 输入,上拉/下拉
    GPIOA->ODR |= (1 << 0);      // 上拉
    
    // 3. 连接EXTI0到PA0
    SYSCFG->EXTICR[0] &= ~(0x0F << 0);
    SYSCFG->EXTICR[0] |= (SYSCFG_EXTICR1_EXTI0_PA);
    
    // 4. 下降沿触发(按键按下)
    EXTI->FTSR |= (1 << 0);
    
    // 5. 使能中断
    EXTI->IMR |= (1 << 0);
    
    // 6. NVIC配置
    NVIC_SetPriority(EXTI0_IRQn, 0x02);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

// 中断服务函数
void EXTI0_IRQHandler(void)
{
    if(EXTI->PR & (1 << 0))
    {
        EXTI->PR = (1 << 0);  // 清除标志
        // 处理唤醒事件
    }
}

我曾经踩过的坑:有一次产品在强电磁干扰环境下频繁误唤醒。查了两天才发现,EXTI配置的是上升沿触发,但PCB走线过长,信号线上耦合了噪声。后来改成双边沿触发,并在软件里做了20ms的去抖滤波,问题才解决。

3.2 RTC唤醒

RTC唤醒适合做定时任务。比如每隔几秒采集一次传感器数据,或者每天固定时间上报状态。

RTC有两个独立的唤醒源:RTC闹钟(Alarm)和RTC唤醒定时器(WakeUp Timer)。我个人习惯用WakeUp Timer,因为它配置更灵活,功耗也更低。

3.2.1 RTC WakeUp Timer配置

void RTC_WakeUp_Config(uint32_t seconds)
{
    // 1. 使能PWR时钟,允许访问RTC
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    PWR->CR |= PWR_CR_DBP;  // 取消后备域写保护
    
    // 2. 使能LSE或LSI作为RTC时钟源
    RCC->BDCR |= RCC_BDCR_LSEON;
    while(!(RCC->BDCR & RCC_BDCR_LSERDY));  // 等待LSE就绪
    
    // 3. 配置RTC时钟分频
    RTC->PRER = (255 << 16) | (127);  // 异步128分频,同步256分频
    // 最终时钟 = LSE(32768) / 128 / 256 = 1Hz
    
    // 4. 配置WakeUp定时器
    RTC->WUTR = seconds - 1;  // 计数值
    RTC->CR |= RTC_CR_WUTE;   // 使能WakeUp定时器
    RTC->CR |= RTC_CR_WUTIE;  // 使能WakeUp中断
    
    // 5. 等待同步
    while(!(RTC->ISR & RTC_ISR_WUTF));
    RTC->ISR &= ~RTC_ISR_WUTF;
}

小技巧:RTC的寄存器写入需要等待同步。如果你发现配置不生效,八成是没等同步完成。我习惯在每次RTC操作后加一个超时等待,防止死锁。

3.3 定时器唤醒

定时器唤醒和RTC类似,但精度更高,适合微秒级的定时任务。常用的有TIM6、TIM7这些基本定时器。

为什么用基本定时器?因为它们没有外部引脚,结构简单,功耗也低。高级定时器功能多,但唤醒时的功耗也大。

3.3.1 配置要点

void TIM_WakeUp_Init(uint16_t period_ms)
{
    // 1. 使能定时器时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
    
    // 2. 配置预分频和自动重装
    // 假设APB1时钟为36MHz,定时器时钟为72MHz(APB1预分频不为1时加倍)
    TIM6->PSC = 7200 - 1;   // 72MHz / 7200 = 10kHz
    TIM6->ARR = 10 * period_ms - 1;  // 10kHz下,10ms一个周期
    
    // 3. 使能更新中断
    TIM6->DIER |= TIM_DIER_UIE;
    
    // 4. NVIC配置
    NVIC_SetPriority(TIM6_IRQn, 0x03);
    NVIC_EnableIRQ(TIM6_IRQn);
    
    // 5. 使能定时器
    TIM6->CR1 |= TIM_CR1_CEN;
}

void TIM6_IRQHandler(void)
{
    if(TIM6->SR & TIM_SR_UIF)
    {
        TIM6->SR = ~TIM_SR_UIF;  // 清除标志
        // 唤醒后的处理
    }
}

注意:进入Stop模式前,定时器必须保持运行。如果定时器时钟在低功耗模式下被关闭,它就无法产生唤醒事件。我建议用LSI或LSE作为定时器时钟源,这样在Stop模式下也能工作。

3.4 USART唤醒

USART唤醒是通信类产品的标配。比如一个串口设备,平时休眠,收到主机发来的数据帧就立即唤醒处理。

STM32的USART支持两种唤醒方式:地址匹配唤醒和起始位唤醒。地址匹配适合多机通信,起始位唤醒适合点对点通信。

3.4.1 起始位唤醒配置

void USART_WakeUp_Init(void)
{
    // 1. 使能USART和GPIO时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    
    // 2. 配置TX(PA9)为推挽输出,RX(PA10)为浮空输入
    GPIOA->CRH &= ~(0xFF << 4);
    GPIOA->CRH |= (0x0B << 4);   // PA9: 复用推挽输出
    GPIOA->CRH |= (0x04 << 8);   // PA10: 浮空输入
    
    // 3. 配置USART
    USART1->BRR = 72000000 / 115200;  // 波特率115200
    USART1->CR1 |= USART_CR1_UE;      // 使能USART
    USART1->CR1 |= USART_CR1_RE | USART_CR1_TE;  // 使能收发
    
    // 4. 使能唤醒功能(起始位唤醒)
    USART1->CR1 |= USART_CR1_WUFIE;   // 使能唤醒中断
    USART1->CR3 |= USART_CR3_WUS;     // 选择起始位唤醒
    
    // 5. NVIC配置
    NVIC_SetPriority(USART1_IRQn, 0x01);
    NVIC_EnableIRQ(USART1_IRQn);
}

void USART1_IRQHandler(void)
{
    if(USART1->CR1 & USART_CR1_WUFIE)
    {
        USART1->CR1 &= ~USART_CR1_WUFIE;  // 清除唤醒标志
        // 读取数据,处理接收
        uint8_t data = USART1->DR;
    }
}

经验之谈:USART唤醒后,第一个字节可能会丢失。因为芯片从接收到唤醒信号到完全恢复需要时间。我通常的做法是:在数据帧前面加一个前导字节(比如0x55),唤醒后丢弃它,从第二个字节开始才是有效数据。

3.5 四种唤醒源对比

唤醒源 适用场景 唤醒延迟 功耗影响 我的评价
EXTI 按键、传感器中断、外部事件 几微秒 极低 最可靠,但需要外部信号
RTC 定时上报、周期性任务 几十微秒 极低 适合长时间休眠
定时器 高精度定时、快速轮询 几微秒 较低 精度高,但功耗略高于RTC
USART 串口通信、主机唤醒从机 几百微秒 中等 第一个字节可能丢失

3.6 唤醒源选择决策流程

下面这张图是我自己总结的唤醒源选择流程。每次做新项目,我都会拿出来对照一下。

唤醒源选择决策流程 确定唤醒需求 是否需要外部事件触发? 是 使用 EXTI 否 是否需要高精度定时? 是 使用定时器 否 是否需要串口通信唤醒? 是 使用 USART 否 使用 RTC 定时唤醒 提示:多个唤醒源可以同时使用 例如:RTC定时唤醒 + EXTI紧急唤醒

3.7 多唤醒源共存策略

实际项目中,很少只用一种唤醒方式。我做过一个环境监测终端,就是RTC定时唤醒采集数据,同时EXTI接了一个紧急按钮,USART还留着给调试用。

多唤醒源共存时,要注意优先级。NVIC优先级设置要合理,避免高优先级中断把低优先级的饿死。另外,中断服务函数要尽量短,只做标志位设置,具体处理放到主循环里。

核心原则:中断里只做最必要的事——清除标志、设置状态、唤醒系统。别在中断里做数据处理、协议解析这些耗时操作。我曾经见过有人在USART中断里做AT指令解析,结果数据一多就丢包。