低功耗与响应性能设计(一):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从机采用三态状态机,完全由中断驱动:
1 2 3 4 5 6 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 2 3 4 5 6 7 8 void I2C_IRQHandler (void ) { if ((i2cif_p->i2c_state == I2C_XFER_MSTSND) || (i2cif_p->i2c_state == I2C_XFER_MSTRCV)) i2cif_master_handle(); else i2cif_slave_handle(); }
i2cif_slave_handle() 是状态机的核心,约300行代码处理所有I2C事件:
地址匹配 → 进入接收状态:
1 2 3 4 5 6 i2cif_p->i2c_state = I2C_XFER_SLVRCV; i2cif_p->rx_idx = 0 ; i2cif_p->tx_idx = 0 ; i2cif_p->i2c_cmd = 0 ; i2cif_p->i2c_pec = i2cif_calc_pec((uint8_t )i2cif_p->slv_addr, 0x00 );
第一个数据字节是命令码,查表确定协议参数:
1 2 3 4 5 6 7 8 9 10 11 12 if (i2cif_p->rx_idx == 0 ) { i2cif_p->i2c_cmd = (uint8_t )i2cif_p->data; do { if (((sbsd_cmd_def[i2cif_p->sbd_idx] & SBSD_CMD_Msk) >> SBSD_CMD_Pos) == i2cif_p->i2c_cmd) break ; } while ((i2cif_p->sbd_idx++) < SBSD_CMD_MAX); i2cif_p->rx_size = ((sbsd_cmd_def[i2cif_p->sbd_idx] & SBSD_LEN_Msk) >> SBSD_LEN_Pos) + 1 ; }
接收完成后通过回调通知业务层:
1 2 3 4 5 if (i2cif_p->rx_idx == i2cif_p->rx_size && !i2cif_p->i2c_pec_enable) { i2cif_p->i2c_status = I2C_STATUS_RXDONE; if (i2cif_p->i2c_callback_f) i2cif_p->i2c_callback_f(i2cif_p->i2c_status, i2cif_p->sbd_idx); }
命令定义表 每个SBS命令用一个32位整数编码所有协议信息:
这种紧凑编码使得整个命令表只占几百字节Flash,查表时一次内存访问即可获取所有协议参数。
PEC校验 SMBus的PEC(Packet Error Checking)是CRC-8校验,覆盖从地址字节到最后一个数据字节的所有内容。固件在每个字节收发时递增计算PEC:
1 2 3 if (tx_idx == tx_size && i2cif_p->i2c_pec_enable) data = i2cif_p->i2c_pec;
I2C活动对睡眠的影响 I2C通信会清除睡眠准备标志,推迟CPU进入低功耗模式:
1 2 3 4 5 if (status == I2C_STATUS_TXDATA) { stm_p->stm_flag &= (~STMFLAG_SLEEP_PREPARE); stm_p->stm_sleep_delay = DEFAULT_TIMER_VAL; }
这确保了一次I2C事务的多个阶段(写命令→读数据)不会被睡眠打断。
低功耗设计 睡眠模式策略 固件支持两级睡眠:Sleep(浅睡眠)和Deep Sleep(深度睡眠)。两者的区别在于哪些硬件模块保持运行:
模式
CPU
MTIMER
VADC
CADC
I2C唤醒
Active
运行
运行
运行
运行
-
Sleep
停止
运行
自动扫描
运行
支持
Deep Sleep
停止
停止
停止
可选
支持
睡眠进入由状态机控制,不是简单的空闲检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (stm_p->stm_flag & STMFLAG_SLEEP_PREPARE) { if (stm_p->stm_sleep_delay == DEFAULT_TIMER_VAL) { stm_p->stm_sleep_delay = timer_set_target( param_board_cfg[PARM_BCFG_IDLETOSLEEP] - 1 ); } if (systime_get_sec() >= stm_p->stm_sleep_delay) { stm_p->pwrmd = STM_PWRMD_SLEEP_START; uSleepTime = stm_drive_sleep(uSleepTime, STM_SLPMD_SLEEP); stm_p->stm_flag &= (~STMFLAG_SLEEP_PREPARE); stm_p->stm_sleep_delay = DEFAULT_TIMER_VAL; } }
stm_drive_sleep() 是实际进入硬件睡眠的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 uint32_t stm_drive_sleep (uint32_t slptime, STM_SLPMD_T slpmd_ctrl) { if (g_internal_charge_full_flag == 1 ) return 0 ; if (calibration_flag == 1 ) return 0 ; Chip_DFE_ClearWakeupFLAG(WAKEUP_SOURCE_ALL); wake_source = PROT_WKUP_SAFETY_ENABLE | PROT_WKUP_CADC_ENABLE | PROT_WKUP_TIMER1_ENABLE | PROT_WKUP_I2C_ENABLE | PROT_WKUP_STP_ENABLE; Chip_DFE_SetWakeupOption(wake_source); timer_setup_timer1((slptime) - 1 , 15 ); Chip_Timer_reload(CH_TIMER1); if (slpmd_ctrl == STM_SLPMD_DEEPSLP) syshw_enter_deepslp(); else syshw_enter_sleep(); }
主循环与WFI 主循环采用经典的超级循环(super loop)模式,每次迭代末尾执行__WFI():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void main_loop () { systime_update_EX(); if (systime_passed_sec()) { dacq_update_cadc(); syshw_feed_wdt(); dacq_update(); sbsif_update(); stm_check_imode(); sleep_time = stm_update(); } __WFI(); }
__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 2 3 #define MTIMER_FREQ 32768 #define INTERVAL_125MS (MTIMER_FREQ / 8) #define TICKRATE_1S 8
每次mtime中断触发后,重新设置下一次比较值:
1 2 3 4 __STATIC_INLINE void MTimer_Config () { Set_mtimecmp_Value(Get_mtime_Value() + INTERVAL_125MS); }
进入Deep Sleep时,通过自定义CSR mcounterstop(地址0xBFF)停止所有计数器:
1 2 3 4 5 6 7 8 9 void SysCounter_Stop () { asm volatile ( ".equ mcounterstop, 0xBFF ;\ li a0, 0x07 ;\ csrw mcounterstop, a0" ) ;}
CSR 0xBFF是HBird E200内核的厂商扩展:bit0控制mcycle,bit1控制mtime,bit2控制minstret。写入0x07一次性停止三个计数器。
睡眠期间ADC自动采集 电量计即使在睡眠状态也需要持续监测电池状态(电压、电流、温度),否则无法检测过压/过流等安全事件。硬件ADC子系统支持自主运行:
VADC自动扫描:
1 2 3 4 5 6 7 if (param_board_cfg[PARM_BCFG_CELLNUM] == PARAM_CELL02_VAL) Chip_ADC_SetupAutoScan(AUTOSCAN_2CELLS_1THM, AUTOSCAN_RATE_01SEC, 1 , 1 ); else Chip_ADC_SetupAutoScan(AUTOSCAN_1CELL_1THM, AUTOSCAN_RATE_01SEC, 1 , 1 );
AUTOSCAN模式下,硬件按配置的周期(1~31秒可选)自动完成采样序列,结果写入寄存器。CPU唤醒后直接读取,零等待延迟。
CADC(库仑计ADC)硬件滤波链:
1 2 3 4 5 6 7 8 9 10 Chip_ADC_SetupCADCCtrl( CADC_SW_FS_144MV | CADC_CLK_SEL_32K | CADC_SW_AVG_16 | CADCSCAN_SWDITHER_ENABLE | CADC_SW_OSR_2048 | CADC_LPF_FILTER_ENABLE | CADC_MEAN_FILTER_ENABLE | CADCSCAN_SCAN_CC_ENABLE );
CADC的信号链:原始ADC → 16次平均 → LPF低通 → 均值滤波 → 输出。OSR 2048意味着每个输出样本由2048个原始采样累加,有效分辨率远超ADC本身位数。整个滤波链在硬件中完成,CPU完全不参与。
RISC-V中断与异常处理 Trap入口 RISC-V的所有中断和异常统一通过trap入口处理。entry.S放置在DTCM(紧耦合数据存储器)中以获得最低访问延迟:
1 2 3 4 5 6 7 8 9 10 11 .section .dtcm .align 2 .global trap_entry trap_entry: TRAP_ENTRY // 宏:保存寄存器到栈 csrr a0, mcause // 参数1:中断/异常原因 csrr a1, mepc // 参数2:被中断的PC csrr a2, mtval // 参数3:异常附加信息 call handle_trap // 调用C分发函数 csrw mepc, a0 // handle_trap可能修改返回地址 TRAP_EXIT // 宏:恢复寄存器,mret返回
TRAP_ENTRY宏只保存x1~x15(caller-saved寄存器):
1 2 3 4 5 6 7 8 .macro TRAP_ENTRY addi sp, sp, -16*REGBYTES STORE x1, 1*REGBYTES(sp) STORE x5, 5*REGBYTES(sp) STORE x6, 6*REGBYTES(sp) ... STORE x15, 15*REGBYTES(sp) .endm
为什么不保存x16x31?RISC-V ABI规定x16x31是callee-saved寄存器,如果handle_trap()内部调用的C函数使用了这些寄存器,编译器会自动在函数序言/尾声中保存和恢复。中断入口只需保存caller-saved部分,将栈帧从32个寄存器缩减到15个,中断响应延迟减少约一半 。
Trap分发 handle_trap()根据mcause的最高位区分中断和异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 uintptr_t handle_trap (uintptr_t mcause, uintptr_t epc, uintptr_t adaddr) { if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_EXT)) handle_m_ext_interrupt(); else if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_TIMER)) handle_m_time_interrupt(); else if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_SOFT)) handle_m_sotfware_interrupt(); else { dbg_print(debug_str, mcause, epc, adaddr); while (1 ); } return epc; }
PLIC中断控制器 PLIC(Platform-Level Interrupt Controller)是RISC-V标准的外部中断管理器。所有外设中断(I2C、ADC、Timer等)都通过PLIC路由到CPU:
1 2 3 4 5 6 7 8 9 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void handle_m_ext_interrupt () { plic_source int_num = PLIC_claim_interrupt(&g_plic); if (int_num == INT_CADC) interrupt_nest_mode_start(); if ((int_num >= 1 ) && (int_num < PLIC_NUM_INTERRUPTS)) g_ext_interrupt_handlers[int_num](); if (int_num == INT_CADC) interrupt_nest_mode_end(); PLIC_complete_interrupt(&g_plic, int_num); }
嵌套中断实现 RISC-V默认不支持中断嵌套——进入trap后mstatus.MIE自动清零,屏蔽所有中断。但电量计的安全中断(过压/过流/过温)必须能打断正在处理的低优先级中断。
实现方式:在特定ISR内手动提升PLIC threshold并重新开启MIE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define NEST_PLIC_THRE 6 #define DEF_PLIC_THRE 0 void interrupt_nest_mode_start (void ) { PLIC_set_threshold(&g_plic, NEST_PLIC_THRE); Chip_Int_INTGerenalSet(1 ); } void interrupt_nest_mode_end (void ) { Chip_Int_INTGerenalSet(0 ); PLIC_set_threshold(&g_plic, DEF_PLIC_THRE); }
当CADC中断正在处理时(优先级6),如果发生优先级>6的安全中断,CPU会再次进入trap_entry,形成一级嵌套。栈空间需要预留两层中断帧(2 × 16 × 4 = 128字节)。
mtime中断处理 mtime是系统心跳,每125ms触发一次:
1 2 3 4 5 6 7 void handle_m_time_interrupt () { clear_csr(mie, MIP_MTIP); system_tick = (system_tick + 1 ) & (TICKTIMERLEN - 1 ); MTimer_Config(); set_csr(mie, MIP_MTIP); }
主循环架构与状态机 超级循环模式 固件不使用RTOS,采用经典的超级循环(super loop)+ 中断驱动模式。所有业务逻辑在主循环中按时间片执行:
1 2 3 4 5 6 7 8 9 10 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计算、温度趋势分析等慢任务
功耗状态转换 1 2 3 4 5 6 7 8 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电量算法架构 - 库仑积分、开路电压与电量追赶机制