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();
}
注意:进入停止模式前,一定要把不用的GPIO设为模拟输入模式。我曾经有个项目,就因为一个GPIO没处理,停止模式功耗多了20微安。排查了两天才找到原因。

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;
}
经验之谈:RTC的唤醒时间不要设得太短。我见过有人设成10ms唤醒一次,结果功耗比正常工作还高。因为每次唤醒都要重新初始化时钟,这个开销不小。建议至少100ms以上。

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唤醒中断的优先级要设得够高,否则可能被其他中断卡住。
  • 调试器干扰:连接调试器时,停止模式可能无法正常工作。断开调试器再测功耗。

嗯,停止模式就这些。说白了,它就是低功耗和快速响应之间的一个平衡点。用好了,你的设备既能省电,又能及时响应外部事件。

停止模式工作流程 正常运行 配置RTC唤醒源 执行WFI进入停止 等待RTC唤醒中断 唤醒→恢复时钟→执行任务 关键点 • SRAM全程保持 • 唤醒时间约几微秒 • 需重新配置系统时钟 功耗对比 • 停止模式:几μA~几十μA • 深度睡眠:亚μA级