低功耗与响应性能设计(一):I2C中断、低功耗与RISC-V底层实现
电量计芯片的固件面临三重约束:低功耗(电池供电,待机电流μA级)、实时响应(I2C从机必须在协议时序内应答)、资源受限(RISC-V小核,KB级RAM)。这三者相互矛盾——低功耗要求CPU尽量睡眠,实时响应要求CPU随时可用,资源受限要求代码路径尽可能短。
本文以purdy_g2电量计固件为例,展示如何在RISC-V平台上做这些工程权衡。该芯片基于HBird E200 RISC-V内核,32MHz主频,配备PLIC中断控制器、硬件I2C从机、VADC/CADC双ADC子系统。
I2C从机中断驱动架构
电量计通过I2C/SMBus与主机通信,遵循SBS(Smart Battery Specification)协议。主机随时可能发起读写请求,固件必须在I2C时钟的SCL周期内完成应答,否则总线超时。
状态机设计
I2C从机采用三态状态机,完全由中断驱动:
stateDiagram-v2
[*] --> IDLE
IDLE --> SLVRCV : "地址匹配 (写方向)"
SLVRCV --> SLVSND : "重复START (读方向)"
SLVRCV --> IDLE : "STOP / NACK"
SLVSND --> IDLE : "STOP / NACK"
典型的SMBus读事务流程:
- 主机发START + 从机地址(W) → 进入SLVRCV
- 主机发命令字节 → 从机查表确定数据长度
- 主机发重复START + 从机地址(R) → 进入SLVSND
- 从机逐字节发送数据 + PEC校验字节
- 主机发NACK + STOP → 回到IDLE
中断处理核心逻辑
1 | void I2C_IRQHandler(void) |
i2cif_slave_handle() 是状态机的核心,约300行代码处理所有I2C事件:
地址匹配 → 进入接收状态:
1 | // START检测,写方向 |
第一个数据字节是命令码,查表确定协议参数:
1 | if (i2cif_p->rx_idx == 0) { |
接收完成后通过回调通知业务层:
1 | if (i2cif_p->rx_idx == i2cif_p->rx_size && !i2cif_p->i2c_pec_enable) { |
命令定义表
每个SBS命令用一个32位整数编码所有协议信息:
1 | // sbsd_cmd_def[] 每个条目的位域编码: |
这种紧凑编码使得整个命令表只占几百字节Flash,查表时一次内存访问即可获取所有协议参数。
PEC校验
SMBus的PEC(Packet Error Checking)是CRC-8校验,覆盖从地址字节到最后一个数据字节的所有内容。固件在每个字节收发时递增计算PEC:
1 | // 发送阶段:数据发完后追加PEC字节 |
I2C活动对睡眠的影响
I2C通信会清除睡眠准备标志,推迟CPU进入低功耗模式:
1 | // sbs_callback_i2c_slave() 中 |
这确保了一次I2C事务的多个阶段(写命令→读数据)不会被睡眠打断。
低功耗设计
睡眠模式策略
固件支持两级睡眠:Sleep(浅睡眠)和Deep Sleep(深度睡眠)。两者的区别在于哪些硬件模块保持运行:
| 模式 | CPU | MTIMER | VADC | CADC | I2C唤醒 |
|---|---|---|---|---|---|
| Active | 运行 | 运行 | 运行 | 运行 | - |
| Sleep | 停止 | 运行 | 自动扫描 | 运行 | 支持 |
| Deep Sleep | 停止 | 停止 | 停止 | 可选 | 支持 |
睡眠进入由状态机控制,不是简单的空闲检测:
1 | // stm_translate_pwrmd() — 睡眠延迟倒计时 |
stm_drive_sleep() 是实际进入硬件睡眠的函数:
1 | uint32_t stm_drive_sleep(uint32_t slptime, STM_SLPMD_T slpmd_ctrl) |
主循环与WFI
主循环采用经典的超级循环(super loop)模式,每次迭代末尾执行__WFI():
1 | void main_loop() |
__WFI()是RISC-V的Wait For Interrupt指令,CPU时钟门控但不关断电源域。mtime每125ms产生一次中断唤醒CPU,systime_passed_sec()在8次125ms tick后返回true,触发1秒周期的业务逻辑。
定时器与唤醒
MTIMER是RISC-V标准的机器级定时器,由32.768kHz低频振荡器驱动,在Sleep模式下保持运行:
1 |
每次mtime中断触发后,重新设置下一次比较值:
1 | __STATIC_INLINE void MTimer_Config() |
进入Deep Sleep时,通过自定义CSR mcounterstop(地址0xBFF)停止所有计数器:
1 | // 停止mtime + mcycle + minstret,节省时钟树功耗 |
CSR 0xBFF是HBird E200内核的厂商扩展:bit0控制mcycle,bit1控制mtime,bit2控制minstret。写入0x07一次性停止三个计数器。
睡眠期间ADC自动采集
电量计即使在睡眠状态也需要持续监测电池状态(电压、电流、温度),否则无法检测过压/过流等安全事件。硬件ADC子系统支持自主运行:
VADC自动扫描:
1 | // 根据电芯数量配置扫描通道 |
AUTOSCAN模式下,硬件按配置的周期(1~31秒可选)自动完成采样序列,结果写入寄存器。CPU唤醒后直接读取,零等待延迟。
CADC(库仑计ADC)硬件滤波链:
1 | Chip_ADC_SetupCADCCtrl( |
CADC的信号链:原始ADC → 16次平均 → LPF低通 → 均值滤波 → 输出。OSR 2048意味着每个输出样本由2048个原始采样累加,有效分辨率远超ADC本身位数。整个滤波链在硬件中完成,CPU完全不参与。
RISC-V中断与异常处理
Trap入口
RISC-V的所有中断和异常统一通过trap入口处理。entry.S放置在DTCM(紧耦合数据存储器)中以获得最低访问延迟:
1 | .section .dtcm |
TRAP_ENTRY宏只保存x1~x15(caller-saved寄存器):
1 | .macro TRAP_ENTRY |
为什么不保存x16x31?RISC-V ABI规定x16x31是callee-saved寄存器,如果handle_trap()内部调用的C函数使用了这些寄存器,编译器会自动在函数序言/尾声中保存和恢复。中断入口只需保存caller-saved部分,将栈帧从32个寄存器缩减到15个,中断响应延迟减少约一半。
Trap分发
handle_trap()根据mcause的最高位区分中断和异常:
1 | uintptr_t handle_trap(uintptr_t mcause, uintptr_t epc, uintptr_t adaddr) |
PLIC中断控制器
PLIC(Platform-Level Interrupt Controller)是RISC-V标准的外部中断管理器。所有外设中断(I2C、ADC、Timer等)都通过PLIC路由到CPU:
flowchart LR
A["I2C中断"] --> D["PLIC"]
B["CADC中断"] --> D
C["Timer中断"] --> D
D --> E["CPU trap_entry"]
E --> F["handle_m_ext_interrupt"]
F --> G["PLIC_claim"]
G --> H["查handler表分发"]
H --> I["PLIC_complete"]
PLIC的Claim/Complete协议:
1 | void handle_m_ext_interrupt() |
嵌套中断实现
RISC-V默认不支持中断嵌套——进入trap后mstatus.MIE自动清零,屏蔽所有中断。但电量计的安全中断(过压/过流/过温)必须能打断正在处理的低优先级中断。
实现方式:在特定ISR内手动提升PLIC threshold并重新开启MIE:
1 |
|
当CADC中断正在处理时(优先级6),如果发生优先级>6的安全中断,CPU会再次进入trap_entry,形成一级嵌套。栈空间需要预留两层中断帧(2 × 16 × 4 = 128字节)。
mtime中断处理
mtime是系统心跳,每125ms触发一次:
1 | void handle_m_time_interrupt() |
主循环架构与状态机
超级循环模式
固件不使用RTOS,采用经典的超级循环(super loop)+ 中断驱动模式。所有业务逻辑在主循环中按时间片执行:
flowchart TD
A["main_loop()"] --> B["systime_update_EX()"]
B --> C{"systime_passed_sec()?"}
C -->|"否"| H["__WFI()"]
C -->|"是"| D["dacq_update - ADC数据采集"]
D --> E["sbsif_update - SBS数据更新"]
E --> F["fg_update - 电量算法"]
F --> G["stm_update - 状态机/睡眠决策"]
G --> H
H --> A
时间层次:
- 125ms:mtime中断唤醒CPU,更新system_tick
- 1秒:8次tick后触发业务逻辑(ADC采集、电量计算、状态机)
- 分钟/小时:SOH计算、温度趋势分析等慢任务
功耗状态转换
flowchart LR
A["Active"] -->|"空闲超时"| B["Sleep Prepare"]
B -->|"延迟到期"| C["Sleep"]
C -->|"mtime/I2C唤醒"| A
A -->|"主机命令"| D["Deep Sleep Prepare"]
D -->|"延迟到期"| E["Deep Sleep"]
E -->|"I2C唤醒"| A
B -->|"I2C活动"| A
关键设计:Sleep Prepare状态下如果检测到I2C活动,立即取消睡眠准备,回到Active。这避免了”正在进入睡眠时主机发来请求”的竞态条件。
性能与功耗的权衡总结
| 设计决策 | 性能收益 | 功耗代价 | 权衡点 |
|---|---|---|---|
| 中断驱动I2C | 零轮询开销,响应延迟<10μs | 中断唤醒功耗 | I2C活动稀疏时净省电 |
| ADC自动扫描 | CPU完全释放 | 模拟前端持续运行 | 扫描率越低越省电(1s vs 31s) |
| CADC硬件滤波 | 无需软件DSP | 模拟电路常开 | OSR 2048换取高精度 |
| 嵌套中断 | 安全事件响应<1μs | 额外128字节栈空间 | 仅CADC ISR允许被嵌套 |
| 只保存caller-saved | 中断帧60字节→减半 | 无 | RISC-V ABI保证正确性 |
| mcounterstop | 无 | 省去计数器时钟树功耗 | 仅Deep Sleep时停止 |
| 125ms tick周期 | 1秒内最多8次唤醒 | 每次唤醒消耗~10μs | 比1ms tick省电8倍 |
核心思想:让硬件做硬件擅长的事。ADC采样、电流积分、数据滤波全部交给专用硬件,CPU只在需要做决策(电量计算、状态判断)时才醒来。醒来后尽快完成计算,然后立即回到WFI等待下一个事件。
系列文章
- 本文:MCU固件架构 - I2C中断、低功耗与RISC-V底层实现
- 相关:ARM平台固件架构 - I2C中断、低功耗与Cortex-M0实现
- 相关:阻抗追踪的嵌入式实现 - 定点运算与牛顿迭代
- 相关:SOC电量算法架构 - 库仑积分、开路电压与电量追赶机制