4. 停止模式(Stop Mode)实战:停止模式进入与退出、RTC唤醒配置、SRAM数据保持策略
停止模式,说白了就是让芯片“浅睡”一下。跟深度睡眠不一样,它保留了SRAM的内容,唤醒也快。我刚开始用CH32的时候,总觉得低功耗就是往最深的睡眠里塞,后来发现很多场景其实用停止模式就够了——比如几秒唤醒一次做数据采集。
这一节,咱们就把它彻底搞明白。
4.1 停止模式的核心机制
停止模式下,内核时钟停了,大部分外设时钟也停了。但SRAM不掉电,寄存器状态保持。唤醒后,系统从中断向量表重新启动,速度比深度睡眠快得多。
| 特性 | 停止模式 | 深度睡眠 |
|---|---|---|
| 内核时钟 | 关闭 | 关闭 |
| SRAM保持 | 全部保持 | 可配置保持区域 |
| 唤醒源 | RTC、外部中断、PVD等 | RTC、外部中断 |
| 唤醒时间 | 约几个微秒 | 几十到上百微秒 |
| 典型功耗 | 几微安到几十微安 | 亚微安级 |
你想想看,如果你的设备需要每100ms响应一次外部事件,深度睡眠的唤醒延迟可能就占了10%的时间。停止模式才是正解。
4.2 进入停止模式
进入停止模式其实就两步:配置唤醒源,然后执行WFI或WFE指令。我习惯用WFI,因为它更直观——等待中断唤醒。
下面是一个典型的进入流程:
void Enter_StopMode(void)
{
// 1. 关闭不用的外设时钟
RCC->AHBPCENR |= RCC_AHBPeriph_GPIOA | RCC_AHBPeriph_GPIOB;
GPIOA->CFGLR = 0x44444444; // 模拟输入,最低功耗
GPIOB->CFGLR = 0x44444444;
// 2. 配置唤醒源(这里用RTC)
RTC_ConfigWakeUp();
// 3. 进入停止模式
RCC->APB1PCENR |= RCC_APB1Periph_PWR;
PWR->CTLR |= PWR_CTLR_LPDS; // 低功耗深度休眠
PWR->CTLR |= PWR_CTLR_CSBF; // 清除停止标志
PWR->CTLR |= PWR_CTLR_CWUF; // 清除唤醒标志
// 4. 执行WFI
__WFI();
// 5. 唤醒后从这里继续执行
// 恢复时钟配置
SystemInit();
}
4.3 RTC唤醒配置
RTC是停止模式最常用的唤醒源。它本身有独立的时钟域,功耗极低。配置RTC唤醒,说白了就是设置一个闹钟,到了时间就触发中断。
CH32的RTC支持两种唤醒方式:
- 周期性唤醒:每隔固定时间唤醒一次,适合定时采集
- 闹钟唤醒:在指定时刻唤醒,适合定时任务
我一般用周期性唤醒,因为它配置简单,而且时间精度够用。下面是一个配置示例:
void RTC_ConfigWakeUp(void)
{
// 使能RTC时钟
RCC->APB1PCENR |= RCC_APB1Periph_RTC;
RCC->APB1PCENR |= RCC_APB1Periph_BKP;
// 配置RTC时钟源为LSE(32.768kHz)
RCC->BDCTLR |= RCC_BDCTLR_LSEON;
while(!(RCC->BDCTLR & RCC_BDCTLR_LSERDY));
// 使能RTC写访问
RTC->PSCH = 0x00;
RTC->PSCL = 0x7FFF; // 分频到1Hz
// 配置唤醒定时器
RTC->WUTOH = 0x00;
RTC->WUTL = 5; // 5秒唤醒一次
// 使能唤醒中断
RTC->CTRLH |= RTC_CTRLH_WUTIE;
RTC->CTRLH |= RTC_CTRLH_WUTE;
// 等待同步
while(!(RTC->CTRLL & RTC_CTRLL_WUTF));
RTC->CTRLL &= ~RTC_CTRLL_WUTF;
}
4.4 SRAM数据保持策略
停止模式下,SRAM是全部保持的。但这不代表你可以随便用。有几个坑,我踩过,你注意一下。
第一个坑:唤醒后,系统从复位向量重新启动。这意味着全局变量的初始化代码会再执行一遍。如果你在初始化里把某个变量清零了,那之前保存的数据就丢了。
解决办法很简单:用__attribute__((section(".noinit")))把需要保持的变量放到不初始化的段里。
// 定义不初始化的变量
__attribute__((section(".noinit"))) uint32_t g_wakeup_count;
__attribute__((section(".noinit"))) uint8_t g_data_buffer[256];
// 使用前检查是否需要初始化
if (g_wakeup_count == 0xFFFFFFFF) {
// 首次上电,初始化
g_wakeup_count = 0;
memset(g_data_buffer, 0, sizeof(g_data_buffer));
}
第二个坑:外设寄存器在停止模式下也会保持,但唤醒后时钟重新使能时,有些寄存器的值可能被复位。我建议在唤醒后重新配置关键外设,别偷懒。
| 数据类别 | 停止模式行为 | 建议策略 |
|---|---|---|
| 全局变量(已初始化) | 保持,但唤醒后重新初始化 | 使用.noinit段 |
| 全局变量(未初始化) | 保持,唤醒后不变 | 直接使用,但注意首次上电 |
| 局部变量 | 不保持 | 唤醒后重新计算 |
| 外设寄存器 | 大部分保持 | 唤醒后重新配置关键外设 |
| 堆栈内容 | 保持 | 唤醒后堆栈指针恢复 |
4.5 完整实战:RTC周期性唤醒数据采集
咱们来一个完整的例子。假设你要做一个环境监测节点,每10秒采集一次温度,然后继续睡眠。
// 不初始化段,保持采集计数
__attribute__((section(".noinit"))) uint32_t sample_count;
__attribute__((section(".noinit"))) int16_t temp_buffer[100];
void main(void)
{
// 检查是否是首次上电
if (sample_count == 0xFFFFFFFF) {
sample_count = 0;
}
// 初始化系统
SystemInit();
GPIO_Config();
ADC_Config();
RTC_ConfigWakeUp(10); // 10秒唤醒
while(1)
{
// 采集温度
uint16_t adc_val = ADC_GetValue();
temp_buffer[sample_count % 100] = adc_val;
sample_count++;
// 如果采集够100次,处理数据
if (sample_count >= 100) {
ProcessData(temp_buffer, 100);
sample_count = 0;
}
// 进入停止模式
Enter_StopMode();
}
}
void RTC_IRQHandler(void) __attribute__((interrupt));
void RTC_IRQHandler(void)
{
if(RTC->CTRLL & RTC_CTRLL_WUTF) {
RTC->CTRLL &= ~RTC_CTRLL_WUTF; // 清除唤醒标志
}
}
关键点:
- sample_count用.noinit段,唤醒后不会清零
- 每次唤醒只采集一次,然后立即睡眠
- 采集满100次才处理数据,减少唤醒次数
- 中断服务函数只清除标志,不做复杂操作
4.6 避坑指南
最后,我把自己踩过的坑总结一下:
- GPIO漏配:进入停止模式前,所有GPIO都要设为模拟输入或固定电平。浮空输入会漏电。
- 唤醒后时钟混乱:唤醒后一定要重新配置系统时钟。我见过有人忘了,结果外设跑在错误的频率上。
- 中断优先级:RTC唤醒中断的优先级要设得够高,否则可能被其他中断卡住。
- 调试器干扰:连接调试器时,停止模式可能无法正常工作。断开调试器再测功耗。
嗯,停止模式就这些。说白了,它就是低功耗和快速响应之间的一个平衡点。用好了,你的设备既能省电,又能及时响应外部事件。