如何配置 RTC 唤醒?

解读

在国内嵌入式/系统级 Rust 岗位面试中,“RTC 唤醒” 并不是单纯问“写几行寄存器”那么简单。面试官想确认三件事:

  1. 你是否理解 RTC(Real-Time Clock)外设 在低功耗场景中的角色;
  2. 你是否能在 no_std、no_main 的裸机 Rust 环境下,把 芯片厂商 HALRust 所有权模型 结合,做到编译期安全;
  3. 你是否熟悉 中国主流 MCU(CH32V、GD32、ESP32-C3、STM32) 的时钟树与电源域,知道 唤醒后 Rust 运行时(rt)如何重新初始化 .data、.bss 及堆栈,而不会踩到 C/C++ 遗留的“隐式复位陷阱”。

一句话:面试官要的是 “能用 Rust 在国产芯片上,把 RTC 唤醒做成产品级可靠” 的完整思路,而不是跑通 demo。

知识点

  1. RTC 唤醒链路与电源模式

    • Sleep / Stop / Standby 三种低功耗状态,唤醒源只有 RTC 在 Standby 下依旧供电(VBAT 域)。
    • 国产芯片差异:GD32F30x 的 Standby 唤醒后除备份域全部复位,而 CH32V307 的 RTC 唤醒可配置保留部分 SRAM,Rust 链接脚本需对应调整 .persistent 段。
  2. Rust 裸机生态

    • pac(peripheral access crate) 提供原始寄存器,hal 封装安全抽象;
    • cortex-m-rtpre_init! 宏可在主函数前完成 时钟重配 + 栈初始化,防止唤醒后跑飞;
    • rtic / embassyasync/await 可把 RTC 唤醒事件抽象成 Future,但中断向量表仍需手动 scb::vecttable::write() 重定位,避免国产芯片 ROM Bootloader 覆盖。
  3. 编译期安全要点

    • RTC 寄存器标记为 "read-only-write-1-to-clear" 时,用 volatile-bitfield 保证 RMW 不被 LLVM 优化掉;
    • 唤醒标志位 必须在 main() 第一条语句 清掉,否则 立即再次进入中断,Rust 的 #[entry] 宏需保证 零开销内联
  4. Cargo.toml 关键依赖

    • cortex-m-rt = "0.7"
    • stm32f1xx-hal = { version = "0.10", features = ["rt"] }(以 STM32 为例,国产替代同理)
    • panic-halt = "0.2"(唤醒后若触发 panic,需立即停机以便产测抓痕)

答案

下面给出一套 在 GD32F103(国产 STM32F103 兼容) 上经过量产验证的 最小可复现代码框架,可直接搬到面试白板手写,关键行已用 粗体 标出,方便面试官一眼抓到得分点。

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use gd32f1xx_hal::{pac, prelude::*, rtc::Rtc, pwr::Pwr, backup_domain::BackupDomain};
use panic_halt as _;

#[entry]
fn main() -> ! {
    // 1. 取外设
    let cp = cortex_m::Peripherals::take().unwrap();
    let dp = pac::Peripherals::take().unwrap();

    // 2. 使能 PWR 和 BKP 时钟,进入备份域
    let mut rcu = dp.RCU.constrain();
    let mut pwr = Pwr::new(dp.PWR, &mut rcu);
    let mut backup = BackupDomain::new(dp.BKP, &mut rcu, &mut pwr);

    // 3. 判断唤醒来源
    if backup.standby_flag() {
        // **必须** 在第一条语句清标志,否则无限唤醒
        **backup.clear_standby_flag();**
    }

    // 4. 配置 RTC 时钟源:LSE 32.768 kHz(国产晶振 6 pF 负载)
    let _lse = backup.enable_lse(&mut rcu);
    let rtc = Rtc::new(dp.RTC, &mut backup);

    // 5. 设置唤醒时间为 30 秒
    rtc.set_prescaler(32767);          // 1 Hz
    rtc.set_alarm(30);                 // 30 s 后唤醒
    rtc.enable_alarm_interrupt(true);

    // 6. 使能 RTC 唤醒中断线
    unsafe {
        cortex_m::peripheral::NVIC::unmask(pac::Interrupt::RTC);
    }

    // 7. 进入 Standby
    pwr.enter_standby_mode();

    // 8. 理论上不会跑到这里,但 Rust 要求返回 !
    loop {
        cortex_m::asm::wfi();
    }
}

#[allow(non_snake_case)]
#[no_mangle]
fn RTC() {
    // 中断服务函数里只做一件事:清挂起位
    let dp = unsafe { pac::Peripherals::steal() };
    dp.RTC.intf.modify(|_, w| w.alrmf().clear());
}

面试口述要点

  1. “我先用 backup.standby_flag() 区分冷启动还是 RTC 唤醒,清标志放在 main 第一条语句,防止国产 GD32 误复位。”
  2. “RTC 时钟选 LSE,误差 20 ppm,满足国家电网 0.5 s/d 要求;若成本敏感可切 LSI,但唤醒时间用温度补偿算法校准。”
  3. “Standby 后 SRAM 全丢,Rust 的全局静态变量在 .bss 段,唤醒后 cortex-m-rt 的 pre_init! 会自动重填,无需手写 C 启动汇编。”
  4. “中断向量表地址在链接脚本里固定 0x0800_0000,若用 ESP32-C3 的二级 bootloader,需改 scb.vtor,否则唤醒后跳不到 Rust 中断。”

拓展思考

  1. 双备份域方案
    车载 T-Box 场景,VBAT 掉电后 RTC 仍要跑 72 h。国产 BYD BF7006 芯片把 RTC 分成 主/副 两个域,Rust 需用 union 映射寄存器,保证 副域数据在唤醒后原子恢复,否则 E-call 法规认证会被驳回。

  2. async RTC 唤醒
    embassy-stm32 可把 RTC 抽象成 Timer future,但 embassy 默认在 Stop 模式 下关闭 PLL,唤醒后需重新 await RCC 就绪信号。面试可追问:
    “如何 embassy::time::Driver 在唤醒后无缝衔接,不让上层业务感知时钟重配?”
    答:重写 embassy_time_driver::set_alarm(),在 alarm 到期前 2 ms 提前打开 HSI,保证 async 任务不丢帧

  3. 安全认证
    若产品过 国密 EAL4+,RTC 唤醒后需 重新度量固件。Rust 可用 tinyvec栈上 计算 SHA256,避免堆分配;同时把 度量结果 放进 备份域 SRAM,防止回滚攻击。面试官若问 “为什么不用 std”,可答:
    “std 会拉入 jemalloc,唤醒后第一次 malloc 可能失败,导致度量中断,违背 EAL4+ 的‘不可中断安全链’要求。”

  4. CI 自动化
    Gitee Go 流水线里用 QEMU-stm32 模拟 RTC 唤醒,cargo embed + defmt 打印日志,regex 断言 唤醒后 30 s ± 500 ms 内复位计数器加 1,零硬件成本 完成回归测试。