低功耗与响应性能设计(二):I2C中断、低功耗与Cortex-M0实现
上一篇介绍了RISC-V平台的电量计固件架构。本文以Newton平台(ARM Cortex-M0 @ 25MHz)为例,展示同样的设计问题在ARM生态下的不同解法。两个平台的业务逻辑几乎相同(I2C从机、电量计算、睡眠管理),但底层实现因架构差异而截然不同。
Newton是O2Micro的电池管理SoC,5MHz晶振×5倍频=25MHz主频,768字节栈,零堆(无动态内存分配),11个外部中断源。
I2C从机中断驱动架构
初始化
I2C从机配置为400kHz SMBus模式,支持clock stretching:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void i2cif_init(uint32_t slv_addr, uint8_t *t_buff, i2c_callback p_callback) { i2cif_p->pI2C_Base = I2C; i2cif_p->slv_addr = slv_addr; i2cif_p->i2c_callback_f = p_callback; i2cif_p->i2c_pec_enable = true;
Chip_I2C_SetSlvAddr1(i2cif_p->pI2C_Base, slv_addr); Chip_I2C_SetSlaveMode(i2cif_p->pI2C_Base); Chip_I2C_SetSCLHigh(i2cif_p->pI2C_Base, I2C_FREQ_400K); Chip_I2C_SetSCLLow(i2cif_p->pI2C_Base, I2C_FREQ_400K); Chip_I2C_SetSlaveDelay(i2cif_p->pI2C_Base, 15); Chip_I2C_EnableInts(i2cif_p->pI2C_Base, I2C_IER_SLV_STOP | I2C_IER_SLV_NACK | I2C_IER_SLV_ADDR | I2C_IER_SLV_RXNE | I2C_IER_SLV_TXIS); Chip_I2C_Enable(i2cif_p->pI2C_Base); }
|
ISR固定地址放置
ARM平台的一个特殊设计——I2C ISR被放置在Flash的固定地址:
1 2 3 4 5 6 7 8 9
| void I2C_IRQHandler(void) __attribute__((section(".ARM.__at_0x00004800"))); 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(); }
|
为什么要固定地址?这是OTP(一次性编程)芯片的需求——向量表中的ISR地址在出厂时烧录,后续固件更新只能修改非ISR区域的代码。ISR入口地址必须在芯片生命周期内保持不变。
状态机
与RISC-V平台完全相同的三态设计:
1 2 3 4 5 6
| stateDiagram-v2 [*] --> IDLE IDLE --> SLVRCV : "地址匹配 + 写方向" SLVRCV --> SLVSND : "重复START + 读方向" SLVRCV --> IDLE : "STOP / NACK" SLVSND --> IDLE : "STOP / NACK"
|
接收阶段——命令解析与数据接收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| if (i2cif_p->isr2stat & I2C_ISR2_SLV_RXNE) { i2cif_p->data = Chip_I2C_ReadRXData(i2cif_p->pI2C_Base);
if (i2cif_p->rx_idx == 0) { i2cif_p->i2c_cmd = (uint8_t)i2cif_p->data; do { if (((sbsd_cmd_def[sbd_idx] & SBSD_CMD_Msk) >> SBSD_CMD_Pos) == i2cif_p->i2c_cmd) break; } while (sbd_idx++ < SBSD_CMD_MAX); i2cif_p->rx_size = ((sbsd_cmd_def[sbd_idx] & SBSD_LEN_Msk) >> SBSD_LEN_Pos) + 1; } else { i2cif_p->rx_buffer[rx_idx - 1] = (uint8_t)data; } i2cif_p->i2c_pec = i2cif_calc_pec(data, i2cif_p->i2c_pec); }
|
发送阶段——逐字节泵出数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if ((i2cif_p->isr2stat & I2C_ISR2_SLV_TXIS) && (i2cif_p->i2c_state == I2C_XFER_SLVSND)) { if (tx_idx < tx_size) { data = *tx_buffer++; i2cif_p->i2c_pec = i2cif_calc_pec((uint8_t)data, i2cif_p->i2c_pec); } else if (tx_idx == tx_size && i2cif_p->i2c_pec_enable) { data = i2cif_p->i2c_pec; } else { data = 0xFF; i2cif_p->i2c_status = I2C_STATUS_OVERFLOW; } Chip_I2C_WriteTXData(i2cif_p->pI2C_Base, data); i2cif_p->tx_idx++; }
|
STOP/NACK处理——回到IDLE并通知业务层:
1 2 3 4 5 6 7 8 9 10 11
| if (i2cif_p->isr2stat & (I2C_ISR2_SLV_NACK | I2C_ISR2_SLV_STOP)) { i2cif_p->i2c_state = I2C_XFER_IDLE; if (i2cif_p->isr2stat & I2C_ISR2_SLV_STOP) i2cif_p->i2c_status = I2C_STATUS_STOP; else i2cif_p->i2c_status = I2C_STATUS_NACK; if (i2cif_p->i2c_callback_f) i2cif_p->i2c_callback_f(i2cif_p->i2c_status, i2cif_p->data); Chip_I2C_ClearStatus(i2cif_p->pI2C_Base, I2C_ISR2_SLV_NACK | I2C_ISR2_SLV_STOP); }
|
回调与命令分发
I2C ISR通过回调函数 sbs_callback_i2c_slave() 与业务层交互。回调在中断上下文中执行,必须尽快返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if (status == I2C_STATUS_TXDATA) { stm_p->stm_flag &= (~STMFLAG_SLEEP_PREPARE); stm_p->stm_sleep_delay = DEFAULT_TIMER_VAL; i2cif_set_tx(size, &sbs_data_buff[idx]); }
if (status == I2C_STATUS_RXDONE) { switch (subcmd) { case SBSF9_SUBCMD_SLEEP: stm_p->stm_flag |= STMFLAG_SLEEP_PREPARE; break; case SBSF9_SUBCMD_RESET: syshw_sw_reset(); break; } }
|
低功耗设计
硬件睡眠模式
Newton平台的睡眠不是通过ARM标准的WFI+SCB.SCR实现,而是通过专用电源管理寄存器PWRMD控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void syshw_enter_sleep(void) { uint32_t reg = PWRMD->PWRMDCTRL; Chip_ADC_updateVADCScanRate(AUTOSCAN_RATE_05SEC); PWRMD->UNLOCK = PATTERN_6318; PWRMD->PWRMDCTRL = reg | PWR_CTRL_SLEEP; while (PWRMD->PWRMDCTRL & PWRMD_PWRMDCTRL_SLEEP_CTRL_Msk) __nop(); }
void syshw_enter_deepslp(void) { uint32_t reg = PWRMD->PWRMDCTRL; PWRMD->UNLOCK = PATTERN_6318; PWRMD->PWRMDCTRL = reg | PWR_CTRL_DEEPS; while (PWRMD->PWRMDCTRL & PWRMD_PWRMDCTRL_SLEEP_CTRL_Msk) __nop(); }
|
写入PWRMD->PWRMDCTRL前必须先向UNLOCK寄存器写入魔数0x6318,这是硬件写保护机制,防止软件bug意外进入睡眠。
双定时器睡眠架构
Newton的睡眠管理使用两个16位定时器协同工作:
- Timer2:计量完整睡眠时长(如60秒),到期后唤醒并返回main_loop
- Timer1:周期性短唤醒(每1秒),让唤醒中断有机会检查I2C活动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| uint32_t stm_drive_sleep(uint32_t slptime, STM_SLPMD_T slpmd_ctrl) { Chip_DFE_ClearWakeupFLAG(WAKEUP_SOURCE_ALL);
wake_source = PROT_WKUP_TIMER1_ENABLE | PROT_WKUP_I2C_ENABLE | PROT_WKUP_STP_ENABLE; Chip_DFE_SetWakeupOption(wake_source); PROT->CADCOPTION = 0x06;
timer_setup_timer2(slptime - 1, 15); Chip_Timer_reload(CH_TIMER2);
timer_setup_timer1(slptime - 1, 15); Chip_Timer_reload(CH_TIMER1);
if (slpmd_ctrl == STM_SLPMD_DEEPSLP) syshw_enter_deepslp(); else syshw_enter_sleep();
if (TIMER16_2->TIMINTR & TIMER_INT_FLAG) ustime = slptime; else ustime = Chip_Timer_get_timer_length(CH_TIMER2) - Chip_Timer_get_counter(CH_TIMER2); systime_add_system_sec(ustime); return ustime; }
|
唤醒中断的特殊设计
Newton平台最独特的设计是唤醒中断处理函数可以直接重新进入睡眠,CPU不需要返回main_loop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| void WKUP_IRQHandler(void) { uint32_t reg = PWRMD->PWRMDCTRL; uint16_t wkp_flag = DFE_INT->WKUPFLAG;
if (wkp_flag & (GSTATUS_STM_WKP_I2C | GSTATUS_STM_WKP_HDQ)) { timer_setup_timer1(1, 12); Chip_Timer_reload(CH_TIMER1); if (Chip_ADC_GetVADCScanRate() == AUTOSCAN_RATE_05SEC) Chip_ADC_updateVADCScanRate(AUTOSCAN_RATE_01SEC); timer_nwkup_delay = 0;
if ((TIMER16_2->TIMINTR & TIMER_INT_FLAG) == 0 && stm_p->imode_pre == stm_p->imode) { PWRMD->UNLOCK = PATTERN_6318; PWRMD->PWRMDCTRL = reg | PWR_CTRL_SLEEP; } } else if (wkp_flag & GSTATUS_STM_WKP_TIMER1) { timer_nwkup_delay++; DFE_INT->WKUPFLAG = GSTATUS_STM_WKP_TIMER1;
if (timer_nwkup_delay >= TIMER_STOP_WKUP_TIME_5S) { Chip_ADC_updateVADCScanRate(AUTOSCAN_RATE_05SEC); } PWRMD->UNLOCK = PATTERN_6318; PWRMD->PWRMDCTRL = reg | PWR_CTRL_SLEEP; } }
|
这个设计的精妙之处:
- I2C唤醒时:I2C中断(IRQ3,优先级1)先于WKUP中断(IRQ6,优先级3)执行,完成数据收发。然后WKUP_IRQHandler判断是否需要继续睡眠。
- CPU从不返回main_loop:整个睡眠期间,即使被I2C唤醒,处理完通信后直接在ISR内重新写入sleep位。
- ADC扫描率自适应:I2C活动时加速到1秒(保证数据新鲜),5秒无活动后降回5秒(省电)。
ADC自动扫描与睡眠
VADC在睡眠期间以自动扫描模式运行,硬件独立完成采样:
1 2 3 4 5 6 7 8 9
| void Chip_ADC_updateVADCScanRate(uint8_t scan_rate) { uint8_t delay_cnt = 200; uint16_t udata = SCANCTRL->AUTOSCAN & (~SCANCTRL_AUTOSCAN_SCAN_RATE_Msk); SCANCTRL->AUTOSCAN = udata; while (delay_cnt--) __NOP; udata |= (scan_rate & SCANCTRL_AUTOSCAN_SCAN_RATE_Msk); SCANCTRL->AUTOSCAN = udata; }
|
扫描率在两个档位间切换:
AUTOSCAN_RATE_01SEC(1秒):活跃状态或刚被I2C唤醒
AUTOSCAN_RATE_05SEC(5秒):I2C静默5秒后,降低功耗
CADC(库仑计ADC)通过 PROT->CADCOPTION = 0x06 配置为睡眠期间持续运行,确保电流积分不中断。这是电量计精度的关键——即使CPU睡眠,库仑计数也不能停。
ARM Cortex-M0底层
向量表与启动
Cortex-M0使用硬件向量表,中断分发由NVIC硬件完成(无需软件查表):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| __Vectors DCD __initial_sp ; 栈顶指针 DCD Reset_Handler ; 复位 DCD NMI_Handler ; NMI DCD HardFault_Handler ; 硬件错误 DCD 0 ; 保留 (×4) ... DCD SVC_Handler ; SVCall DCD PendSV_Handler ; PendSV DCD SysTick_Handler ; SysTick ; 外部中断 (IRQ0–IRQ10) DCD WDT_IRQHandler ; 0: 看门狗 DCD TIM1_IRQHandler ; 1: Timer1 DCD TIM2_IRQHandler ; 2: Timer2 DCD I2C_IRQHandler ; 3: I2C DCD UART_IRQHandler ; 4: UART DCD RST_IRQHandler ; 5: 复位 DCD WKUP_IRQHandler ; 6: 唤醒 DCD VADC_IRQHandler ; 7: VADC DCD CADC_IRQHandler ; 8: CADC DCD EXINT_IRQHandler ; 9: 外部中断 DCD STP_IRQHandler ; 10: STP
|
启动序列:
1 2 3 4 5 6
| Reset_Handler PROC LDR R0, =SystemInit BLX R0 ; SystemCoreClock = 25MHz LDR R0, =__main BX R0 ; C运行时初始化 → main() ENDP
|
栈大小768字节,堆大小0——整个固件无动态内存分配,所有数据结构静态分配。
NVIC中断优先级
Cortex-M0只有2位优先级(__NVIC_PRIO_BITS = 2),即4个优先级等级:
1 2 3
| NVIC_SetPriority(WDT_IRQn, 0); NVIC_SetPriority(I2C_IRQn, 1); NVIC_SetPriority(WKUP_IRQn, 3);
|
| 优先级 |
中断源 |
设计意图 |
| 0 |
WDT |
系统安全,不可被抢占 |
| 1 |
I2C |
通信时序敏感,必须快速响应 |
| 2 |
VADC, CADC |
数据采集,可被I2C抢占 |
| 3 |
WKUP |
睡眠管理,最低优先级 |
Cortex-M0不支持优先级分组(无抢占优先级/子优先级的区分),但支持尾链优化(tail-chaining):当一个ISR执行完毕时,如果有同优先级或更低优先级的中断pending,硬件直接跳转到下一个ISR,省去出栈-入栈的开销(节省12个时钟周期)。
WFI vs 硬件Sleep
Newton平台有两层功耗管理:
第一层:WFI(浅层休眠)
WFI是ARM标准指令,CPU核心时钟停止但不影响任何外设。任何中断都能唤醒。这是主循环每次迭代的默认行为——1秒周期任务执行完毕后,CPU在WFI中等待下一个125ms tick。
第二层:PWRMD Sleep(深层休眠)
通过PWRMD寄存器进入,关断更多电源域,只保留必要的唤醒逻辑和ADC。需要unlock序列保护,唤醒后需要重新初始化部分外设。
两层配合的逻辑:
- 正常运行:WFI在125ms tick之间休眠(功耗~mA级)
- 空闲状态:PWRMD Sleep持续数秒到数分钟(功耗~μA级)
- 极低功耗:PWRMD Deep Sleep,仅I2C地址匹配可唤醒
主循环架构
主循环结构与RISC-V平台几乎相同,体现了跨平台的架构一致性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void main_loop() { systime_update_EX();
if (systime_passed_sec()) { dacq_update_cadc(); syshw_feed_wdt(); dacq_update(); sbsif_update(1); stm_check_imode();
gg_step(sleep_time); fg_update(...); gg_sync_result();
sleep_time = stm_update(); if (stm_p->state == STM_STATE_DSG_RUN) sleep_time = stm_drive_sleep(10, STM_SLPMD_SLEEP2); }
if (systime_passed_min()) { }
__wfi(); }
|
任务执行顺序有严格依赖:dacq(数据采集)→ sbsif(数据格式化)→ fg(算法计算)→ stm(状态决策)。每一步的输出是下一步的输入。
RISC-V vs ARM 架构对比
两个平台实现相同的电量计功能,但底层机制差异显著:
| 维度 |
RISC-V (purdy_g2) |
ARM (Newton) |
| 中断入口 |
统一trap_entry汇编 + 软件分发 |
NVIC硬件向量表,直接跳转ISR |
| 寄存器保存 |
软件保存x1-x15(15个,60字节) |
硬件自动压栈r0-r3,r12,lr,pc,xPSR(8个,32字节) |
| 中断延迟 |
~20周期(软件保存) |
~12周期(硬件压栈) |
| 嵌套中断 |
PLIC threshold手动控制 |
NVIC优先级自动抢占 |
| 睡眠进入 |
__WFI() |
PWRMD寄存器 + unlock序列 |
| 睡眠唤醒 |
mtime中断 → 返回main_loop |
WKUP ISR内直接re-sleep |
| 定时器 |
MTIMER(64位,CSR 0xBFF控制) |
16-bit Timer1/Timer2(寄存器映射) |
| 时钟源 |
32.768kHz(MTIMER专用) |
32kHz内部振荡器(Timer共用) |
| 优先级位数 |
PLIC支持多位(本项目用7级) |
2位(4级) |
| ISR放置 |
.dtcm段(SRAM,零等待) |
固定Flash地址(OTP需求) |
关键架构差异的工程影响
1. 中断响应速度
ARM Cortex-M0的硬件压栈机制使得中断响应比RISC-V快约8个时钟周期。但RISC-V通过只保存caller-saved寄存器(而非全部32个)来缩小差距。对于400kHz I2C(2.5μs/bit),两者都能在一个SCL周期内完成中断入口。
2. 嵌套中断的复杂度
ARM的NVIC天然支持优先级抢占,无需额外代码。RISC-V需要手动管理PLIC threshold和MIE位,代码更复杂但控制更精细——可以选择性地只在特定ISR内允许嵌套。
3. 睡眠策略的根本差异
RISC-V平台:mtime唤醒 → 回到main_loop → 判断是否需要继续睡眠。每次唤醒都执行完整的主循环逻辑。
ARM平台:WKUP ISR内直接判断并重新睡眠,CPU可能永远不返回main_loop。这种设计更省电(省去了main_loop的执行开销),但增加了ISR的复杂度和调试难度。
4. 定时器精度
RISC-V的MTIMER是64位计数器,理论上可以计时到数百年不溢出。ARM的16位Timer最大计数65535,需要通过clock divider(mark参数)来扩展计时范围,但分辨率随之降低。
性能与功耗的权衡总结
| 设计决策 |
性能收益 |
功耗代价 |
权衡点 |
| ISR内re-sleep |
省去main_loop执行(~100μs) |
ISR复杂度增加 |
睡眠期间I2C频繁时收益大 |
| 双定时器架构 |
Timer2精确计时,Timer1灵活唤醒 |
两个定时器功耗 |
比单定时器多一个唤醒检查点 |
| ADC扫描率自适应 |
活跃时数据新鲜(1s) |
静默时仍有5s扫描 |
安全保护不能完全关闭 |
| CADC睡眠运行 |
库仑积分不中断 |
模拟电路持续功耗 |
电量精度的硬性要求 |
| PWRMD unlock保护 |
防止误入睡眠 |
多一次寄存器写入 |
安全性优先于便利性 |
| I2C优先级=1 |
通信不被ADC打断 |
可能延迟ADC中断 |
SMBus时序要求严格 |
| 768字节栈 |
支持ISR嵌套 |
RAM占用 |
Cortex-M0最多2级嵌套 |
系列文章
- 本文:ARM平台固件架构 - I2C中断、低功耗与Cortex-M0实现
- 相关:MCU固件架构 - I2C中断、低功耗与RISC-V底层实现
- 相关:阻抗追踪的嵌入式实现 - 定点运算与牛顿迭代
- 相关:SOC电量算法架构 - 库仑积分、开路电压与电量追赶机制