第三章 唤醒源配置:EXTI、RTC、定时器与USART
低功耗模式选好了,芯片也睡下了。但怎么把它叫醒?这才是真正见功夫的地方。
我见过不少工程师,功耗测出来漂亮得很,结果产品一上电就睡死过去,再也醒不过来。说白了,唤醒源的配置没做对。
这一章,咱们就把四种最常用的唤醒方式掰开揉碎讲清楚。EXTI、RTC、定时器、USART,每个我都踩过坑,咱们一个一个来。
3.1 EXTI外部中断唤醒
EXTI是最直接的唤醒方式。一个引脚电平变化,就能把芯片从睡梦中拉回来。
核心要点:在进入低功耗模式之前,必须确保EXTI线已经配置好,并且中断使能已经打开。否则,你按破按钮芯片都不会理你。
3.1.1 配置步骤
- 使能SYSCFG时钟(用于EXTI与GPIO的连线)
- 配置GPIO为输入模式,选择上下拉
- 将GPIO连接到对应的EXTI线
- 配置EXTI触发边沿(上升沿/下降沿/双边沿)
- 使能EXTI中断(非事件)
- 配置NVIC优先级,使能中断通道
- 编写中断服务函数,清除挂起位
// 以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 唤醒源选择决策流程
下面这张图是我自己总结的唤醒源选择流程。每次做新项目,我都会拿出来对照一下。
3.7 多唤醒源共存策略
实际项目中,很少只用一种唤醒方式。我做过一个环境监测终端,就是RTC定时唤醒采集数据,同时EXTI接了一个紧急按钮,USART还留着给调试用。
多唤醒源共存时,要注意优先级。NVIC优先级设置要合理,避免高优先级中断把低优先级的饿死。另外,中断服务函数要尽量短,只做标志位设置,具体处理放到主循环里。
核心原则:中断里只做最必要的事——清除标志、设置状态、唤醒系统。别在中断里做数据处理、协议解析这些耗时操作。我曾经见过有人在USART中断里做AT指令解析,结果数据一多就丢包。