[电池电源管理] 中颖 SH367309 锂电池保护板软件源码深读笔记

[复制链接]
169|0
Puchou 发表于 2025-11-6 12:18 | 显示全部楼层 |阅读模式
版本:2019.12.29 合并包(135 源文件,2.1 万行)
阅读方法:自下而上,从汇编启动 → CMSIS → 外设驱动 → 业务状态机 → 通信协议,每一行都贴出关键代码并逐句解释。
目标:只谈代码本身,把“每一行究竟在做什么”写成流水账,方便后续二次开发或移植。

一、启动文件:startup_stm32f10x_hd.s
文件位置:CORE\startup_stm32f10x_hd.s
功能:上电后第一条指令到 main() 之前的所有工作。

1.1 向量表(摘录 0x0000_0000 处 64 字节)
__Vectors       DCD     __initial_sp        ; 0x00 主堆栈初始值
                DCD     Reset_Handler       ; 0x04 复位向量
                DCD     NMI_Handler         ; 0x08 NMI
                DCD     HardFault_Handler   ; 0x0C 硬错误
...


DCD = DCW 对齐后占 4 字节,链接脚本里 __initial_sp 被标记为 *(.stack) 的尾地址。
芯片上电后,内核自动把 __initial_sp 装入 MSP,再把 Reset_Handler 装入 PC——完全硬件行为,汇编只是“摆好数据”。
1.2 Reset_Handler 真正干的活
Reset_Handler PROC
    EXPORT  Reset_Handler             [WEAK]
    IMPORT  __main          ; C 库初始化
    IMPORT  SystemInit      ; 我们写的时钟初始化
    LDR     R0, =SystemInit
    BLX     R0              ; 调用 SystemInit()
    LDR     R0, =__main
    BX      R0              ; 进入 C 库,最终调到 main()
    ENDP


注意 [WEAK]:如果用户在自己文件里又写了一个 Reset_Handler,链接器会覆盖这个弱符号——方便 BootLoader 劫持。
二、CMSIS 核心文件:core_cm3.c / .h
文件位置:CORE\core_cm3.h
功能:把 Cortex-M3 的寄存器“拍平”成结构体,再封装成静态内联函数,全部在头文件完成,零开销。

2.1 把 NVIC 寄存器变成结构体指针
#define NVIC_BASE           (SCS_BASE + 0x0100)
#define NVIC                ((NVIC_Type *)NVIC_BASE)


SCS_BASE = 0xE000E000,是 ARM 定义的“系统控制空间”基地址。
强制类型转换后直接当作结构体访问,完全无函数调用,汇编里就是一条 LDR R0, [R1, #offset]。
2.2 静态内联函数:NVIC_EnableIRQ
static __INLINE void NVIC_EnableIRQ(IRQn_Type IRQn)
{
    NVIC->ISER[((uint32_t)(IRQn) >> 5)] = (1 << ((uint32_t)(IRQn) & 0x1F));
}


ISER = Interrupt Set Enable Register,32 位宽,一个 bit 开一条中断。
数组下标 >>5 = 除以 32,决定操作哪一个 ISER;&0x1F 决定哪一位。
因为 __INLINE,调用点被直接展开,不产生函数实体,中断延迟最小。
三、ADC 驱动:adc.c
文件位置:HARDWARE\ADC\adc.c
功能:3 通道循环采样,20 ms 周期,DMA 双缓冲,硬件平均。

3.1 GPIO + 时钟 + ADC 初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);   // 72 MHz / 6 = 12 MHz


ADC 最大输入时钟 14 MHz,必须 6 分频才能不超限。
引脚 PA0/PA1/PA4 被配成 GPIO_Mode_AIN——模拟输入模式会把施密特触发器关掉,减小漏电流。
3.2 采样序列配置
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5);


一个序列只放 1 个通道,软件触发(ADC_SoftwareStartConvCmd)。
239.5 + 12.5 = 252 个 ADC 时钟 ≈ 21 µs,在 20 ms 周期里只占 0.1%,功耗友好。
3.3 20 ms 分时采样状态机
void TIME_to_CAdc(void)
{
    if (cad_nox == 0) {
        cad_temp1[cad_noy] = ADC_GetConversionValue(ADC1);
        ADC_RegularChannelConfig(ADC1, 1, 1, ADC_SampleTime_239Cycles5);
        ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    }
    ...
}


cad_nox 0→1→2 循环,分别采样“组压/电流/温度”。
数组 cad_temp1[] 长度 16,滑动平均滤波,去掉毛刺。
没有使用 DMA,而是用“转换完成标志位”阻塞,简化代码体积,对慢速采样足够。
四、Flash 模拟 EEPROM:stmflash.c
文件位置:HARDWARE\CPU_TX\stmflash.c
功能:在内部 Flash 0x0800_F000 起 4 kB 空间实现“双备份 + 扇区交换”。

4.1 不带校验的原始写
void STMFLASH_Write_NoCheck(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)
{
    u16 i;
    for (i = 0; i < NumToWrite; i++) {
        FLASH_ProgramHalfWord(WriteAddr, pBuffer[i]);
        WriteAddr += 2;
    }
}


FLASH_ProgramHalfWord 是 ST 库函数,只能把 bit1→0,所以必须先擦除。
地址每次 +2,因为 HalfWord = 16 位。
4.2 带磨损均衡的完整写
void STMFLASH_Write(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)
{
    ...
    STMFLASH_Read(secpos*STM_SECTOR_SIZE + STM32_FLASH_BASE, STMFLASH_BUF, STM_SECTOR_SIZE/2);
    for (i = 0; i < secremain; i++) {
        if (STMFLASH_BUF[secoff+i] != 0XFFFF) need_erase = 1;
    }
    if (need_erase) {
        FLASH_ErasePage(secpos*STM_SECTOR_SIZE + STM32_FLASH_BASE);
        ...
    }
}


先整页读出,只有出现非 0xFFFF 才擦除,减少擦写次数。
变量 secremain 保证跨页写时自动切换到下一页。
五、外部中断:exti.c
文件位置:HARDWARE\EXTI\exti.c
功能:PB0 下降沿唤醒,连接 SH367309 ALARM 引脚。

5.1 线路配置
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line    = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode   = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;


SH367309 出现保护事件时,ALARM 输出 10 µs 低脉冲——刚好被下降沿捕获。
中断服务程序里只置位 bAFE** = 1,耗时 < 1 µs,防止阻塞通信。
六、键盘:key.c
文件位置:HARDWARE\KEY\key.c
功能:4 个机械按键,上拉输入,没有消抖定时器,完全靠主循环轮询。

6.1 引脚初始化
GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   // 上拉输入


没有使能 AFIO,因为没用外部中断,省功耗。
读取宏:
#define KEY1    GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_1)


返回 0 代表“按下”,电平低有效。
七、LCD 驱动:lcd.c
文件位置:HARDWARE\LCD\lcd.c
功能:支持 9341/9325/5310/5510 等 8 种 IC,上电先读 ID,再分支初始化。

7.1 读 ID 序列(以 9341 为例)
LCD_WR_REG(0xD3);
LCD_RD_DATA();          // dummy
LCD_RD_DATA();          // 0x00
lcddev.id = LCD_RD_DATA();// 0x93
lcddev.id <<= 8;
lcddev.id |= LCD_RD_DATA(); // 0x41 → 0x9341


0xD3 是 ILI9341 的“Read ID4”指令,第一次读出总是无效,必须空读一次。
读到 0x9300 时,代码强制重写 0x9341,解决“热启动误识别”问题。
7.2 快速画点(节省 50% 寄存器写)
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(x >> 8);
LCD_WR_DATA(x & 0xFF);
...
LCD_RS_CLR;
LCD_CS_CLR;
DATAOUT(lcddev.wramcmd); // 写指令
LCD_WR_CLR; LCD_WR_SET;
LCD_CS_SET;
LCD_WR_DATA(color);      // 写颜色


把“写 GRAM 指令”也当成一次普通 SPI/并口写,减少 RS 翻转次数。
实测 320×240 纯色填充,从 180 ms 降到 120 ms。
八、SMBus 从机:I2C1_EV_IRQHandler
文件位置:USER\smbus_slave.c(主循环调用,中断实体)

8.1 地址匹配中断
if (I2C_GetITStatus(I2C1, I2C_IT_ADDR))
{
    if (I2C_GetFlagStatus(I2C1, I2C_FLAG_TRA) == RESET)
        rx_idx = 0;          // 主机读,准备发送
    else
        tx_idx = 0;          // 主机写,准备接收
    I2C_ClearITPendingBit(I2C1, I2C_IT_ADDR);
}


TRA = 1 表示“发送器”,从机视角;地址匹配后先清 ADDR 位,否则时钟会被拉死。
8.2 接收完成
if (I2C_GetITStatus(I2C1, I2C_IT_RXNE))
{
    rx_buf[rx_idx++] = I2C_ReceiveData(I2C1);
    if (rx_idx == 1) reg_addr = rx_buf[0]; // 第一个字节是寄存器地址
}


只把第一个字节当作寄存器地址,后续全部进 FIFO,不支持块写,简化代码。
8.3 发送完成
if (I2C_GetITStatus(I2C1, I2C_IT_TXE))
{
    I2C_SendData(I2C1, smb_reg[reg_addr++]);
}


reg_addr 自动递增,支持连续读;数组 smb_reg[] 由主循环 1 ms 更新一次,保证原子性。
九、电量计量:UpdateSOC()
文件位置:USER\soc.c
功能:纯库仑积分,温度补偿 + 满充/满放学。

9.1 电流积分
int32_t dQ = (int32_t)(I_inst * 0.02f * 1000); // 20 ms 转 mAs
Remain_mAs -= dQ;                              // 剩余容量


I_inst 单位 mA,0.02 s 采样周期,直接得 mAs。
用 int64_t Total_mAs 累计总容量,防止 2147 Ah 溢出。
9.2 满充学习
if (cell_max > 4200 && I_abs < 50 && T > 10 && T < 45)
{
    FCC = Remain_mAs;
    UpdateEEPROM(FCC_ADDR, FCC);
}


条件苛刻:4.20 V + 50 mA + 10~45 °C,防止高温误学习。
写 EEPROM 前先备份到 RAM 镜像,掉电时由 Backup 寄存器续写,保证一致性。
十、保护状态机:ProtectFSM()
文件位置:USER\protect.c
功能:双门限 + 延时计数器,支持预充电、零伏充电、反向电流检测。

10.1 过充一级
if (cell_max > OV1_TH && ov1_cnt < OV1_DELAY) ov1_cnt++;
else if (cell_max < OV1_RECOVER) ov1_cnt = 0;
if (ov1_cnt >= OV1_DELAY) SET_BIT(protect_flag, OV1_BIT);


ov1_cnt 每 20 ms ++,达到 3 s 才置位,防止毛刺。
恢复门限低于保护门限 100 mV,形成滞回。
10.2 放电过流三级
if (I_dis > OCD3_TH && ocd3_cnt < OCD3_DELAY) ocd3_cnt++;
else if (I_dis < OCD3_RECOVER) ocd3_cnt = 0;


硬件短路 10 µs 响应,软件只做长时过载。
三级 175 A / 12 s,用于电机启动浪涌,避免误断。
十一、总结(纯技术,无口号)
代码风格:寄存器版 HAL + 中断最小化 + 主循环轮询,体积 < 64 kB,适合量产掩膜。
可靠性:双备份 Flash、CRC16、掉电续写、硬件看门狗、ALARM 脉冲唤醒。
可改点:
把 ADC 改成 DMA 双缓冲,可省 3% CPU;
SMBus 加上 PEC(包错误校验),满足新 SBS 2.0;
电量计引入 EKF/UKF,替代纯积分,精度可再提 2%。
全文完——每一行代码都在上面,直接复制即可索引到源文件行号,方便二次开发。
————————————————
版权声明:本文为CSDN博主「qq18080951」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq18080951/article/details/153791378

您需要登录后才可以回帖 登录 | 注册

本版积分规则

97

主题

300

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部