电量计 -- 低功耗与响应性能设计(二):I2C中断、低功耗与Cortex-M0实现

低功耗与响应性能设计(二):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; // 基地址 0x40000400
i2cif_p->slv_addr = slv_addr; // 0x16 (SBS标准地址)
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); // 最大clock stretch
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) {
// 第一个字节是SBS命令码
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;
}
// 逐字节累计PEC
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; // PEC校验字节
} else {
data = 0xFF; // 溢出保护:填充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;
// 从sbs_data_buff[]中取出预计算好的数据
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); // 降低ADC扫描率
PWRMD->UNLOCK = PATTERN_6318; // 解锁序列
PWRMD->PWRMDCTRL = reg | PWR_CTRL_SLEEP; // 置位sleep控制位
// CPU在此停止,直到唤醒事件发生
while (PWRMD->PWRMDCTRL & PWRMD_PWRMDCTRL_SLEEP_CTRL_Msk)
__nop(); // 等待硬件清除sleep位
}

void syshw_enter_deepslp(void)
{
uint32_t reg = PWRMD->PWRMDCTRL;
PWRMD->UNLOCK = PATTERN_6318;
PWRMD->PWRMDCTRL = reg | PWR_CTRL_DEEPS; // deep sleep位
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);

// 配置唤醒源:安全事件 + CADC + Timer1 + I2C + STP
wake_source = PROT_WKUP_TIMER1_ENABLE | PROT_WKUP_I2C_ENABLE
| PROT_WKUP_STP_ENABLE;
Chip_DFE_SetWakeupOption(wake_source);
PROT->CADCOPTION = 0x06; // CADC在睡眠期间保持运行

// Timer2:全程计时
timer_setup_timer2(slptime - 1, 15); // mark=15 → 1Hz
Chip_Timer_reload(CH_TIMER2);

// Timer1:周期唤醒
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; // Timer2到期:睡满了
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)) {
// I2C唤醒:加速ADC扫描,重置Timer1
timer_setup_timer1(1, 12); // 125ms短周期
Chip_Timer_reload(CH_TIMER1);
if (Chip_ADC_GetVADCScanRate() == AUTOSCAN_RATE_05SEC)
Chip_ADC_updateVADCScanRate(AUTOSCAN_RATE_01SEC);
timer_nwkup_delay = 0;

// 如果Timer2未到期且电池状态未变 → 直接重新睡眠
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; // 从ISR内重新睡眠
}
}
else if (wkp_flag & GSTATUS_STM_WKP_TIMER1) {
// Timer1周期唤醒
timer_nwkup_delay++;
DFE_INT->WKUPFLAG = GSTATUS_STM_WKP_TIMER1;

if (timer_nwkup_delay >= TIMER_STOP_WKUP_TIME_5S) {
// 5秒无I2C活动:降低ADC扫描率
Chip_ADC_updateVADCScanRate(AUTOSCAN_RATE_05SEC);
}
// 重新睡眠
PWRMD->UNLOCK = PATTERN_6318;
PWRMD->PWRMDCTRL = reg | PWR_CTRL_SLEEP;
}
}

这个设计的精妙之处:

  1. I2C唤醒时:I2C中断(IRQ3,优先级1)先于WKUP中断(IRQ6,优先级3)执行,完成数据收发。然后WKUP_IRQHandler判断是否需要继续睡眠。
  2. CPU从不返回main_loop:整个睡眠期间,即使被I2C唤醒,处理完通信后直接在ISR内重新写入sleep位。
  3. 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); // I2C:次高
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(浅层休眠)

1
2
// main_loop() 末尾
__wfi(); // CPU时钟门控,外设继续运行

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()) { // 1秒周期
dacq_update_cadc(); // CADC数据读取
syshw_feed_wdt(); // 喂狗
dacq_update(); // VADC通道更新
sbsif_update(1); // SBS数据计算
stm_check_imode(); // 充电/放电/静置判断

gg_step(sleep_time); // 同步ADC数据到电量计输入
fg_update(...); // 运行电量算法(库仑积分+阻抗追踪)
gg_sync_result(); // 结果写入SBS寄存器

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()) { // 分钟级任务
// SOH计算等
}

__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电量算法架构 - 库仑积分、开路电压与电量追赶机制