BMS电池管理系统
2026/9/16 9:45:21 网站建设 项目流程

本项目是一套多节锂电池管理系统(BMS)固件,主控用的是常规stm32,主要实现电池电压、电流、温度采集,以及过压、欠压、过流、短路保护、充放电控制和电芯均衡等功能。

系统使用 BQ76920 作为电池模拟前端(AFE),负责采集最多 5 节串联电芯的电压、1 路 NTC 温度和电池电流。STM32 通过软件 I2C 与 BQ76920 通信,并使用 CRC8 对寄存器读写进行校验,保证通信数据可靠。

项目采用分层模块设计:

  • bq76920.c/.h:BQ76920 底层驱动、寄存器访问、采样换算和 ALERT 处理。
  • bms_hal_monitor.c:将芯片原始采样数据转换为 BMS 层数据。
  • bms_monitor.c:周期性采集电池数据,并管理系统运行模式。√
  • bms_protect.c:实现过压、欠压、过流、温度等保护逻辑。√
  • bms_energy.c:负责充放电状态控制和电芯均衡。√
  • bms_analysis.c:负责 SOC、电量和温度相关分析。√
  • bms_info.c:负责运行状态和调试信息输出。√
  • bms_config.c:提供保护参数、充放电和均衡控制接口。

项目技术栈

层级技术
主控芯片STM32F103C8T6
内核ARM Cortex-M3
主频72 MHz
开发工具Keil MDK-ARM / uVision
编译器ARM Compiler 5 / ARMCC
工程配置STM32CubeMX.ioc
底层库STM32 HAL、CMSIS
实时系统FreeRTOS
RTOS 接口CMSIS-RTOS v1
电池监控芯片TI BQ76920
主控与 AFE 通信软件 I2C
通信校验CRC8
调试输出USART1
告警处理GPIO 外部中断 EXTI
系统时基TIM1 定时器
调试下载SWD

软件运行架构

系统启动后,首先初始化 GPIO、USART、定时器、FreeRTOS 和软件 I2C 总线,然后完成 BQ76920 唤醒、ADC 校准参数读取、保护寄存器配置和状态清除。

FreeRTOS 启动后,系统通过多个任务协同运行:

硬件初始化 | v BQ76920 初始化 | v 创建 BMS 监控、信息、分析和能量管理任务 | v 周期采集电压、电流、温度 | v 保护判断、充放电控制、均衡控制和状态输出

其中,BMS 监控任务以周期方式运行:

  • 单节电压:约 250 ms 更新一次
  • 电池总电压:约 250 ms 更新一次
  • 电池电流:由 BQ76920 的库仑计采样完成事件触发
  • NTC 温度:约 2 s 更新一次
  • ALERT 告警:通过 EXTI 中断记录,再由任务上下文完成 I2C 读取和故障处理

整个项目软件分为底层,驱动层,应用层三大部分,先从底层开始,采用软件IIC进行封装,首先回顾一下IIC的时序逻辑,以写操作时序举例

1,起始信号:在SCL是高电平时,SDA由高到低跳变

2,发送7位从机地址+读写位,每位读取信号是在SCL上升沿时采集SDA电平状态

3,接收1位从机应答信号

4,发送8位从机内部地址

5,接收1位从机应答信号

6,发送8位数据

7,接收1位从机应答信号

8,停止信号:在SCL是高电平时,SDA由低到高跳变

了解完IIC基本时序之后,就可以先编写底层代码了

/* IIC总线接口 */ struct I2C_BusTypeDef { GPIO_TypeDef *gpiox; uint32_t gpio_rcc; uint16_t sda_gpio_pin; uint16_t scl_gpio_pin; uint32_t retries; // 发地址无响应重试次数(最大不能超过255) void (*udelay)(uint32_t us); void (*lockInit)(void); void (*lock)(void); void (*unlock)(void); };

先封装如图IIC总线接口,包含GPIO编号,引脚,重试次数,延时函数,锁,以便于初始化和调用;

//IIC总线初始化 static void I2C_BusHardwareInitialize(struct I2C_BusTypeDef *bus){ GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = bus->scl_gpio_pin | bus->sda_gpio_pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(bus->gpiox, &GPIO_InitStruct); //SCL和SDA初始为高电平 HAL_GPIO_WritePin(bus->gpiox, bus->scl_gpio_pin, GPIO_PIN_SET); HAL_GPIO_WritePin(bus->gpiox, bus->sda_gpio_pin, GPIO_PIN_SET); }

这块使用的是GPIO模拟IIC,所以初始化时钟线(SCL)和数据线(SDA)为推挽上拉高速模式,并默认为高电平

//将SDA调整为输出模式,使用内联函数CPU不做跳转栈操作,减少开销 static inline void SDA_SetOutMode(struct I2C_BusTypeDef *bus) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = bus->sda_gpio_pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(bus->gpiox, &GPIO_InitStruct); } //将SDA调整为输入模式 static inline void SDA_SetInMode(struct I2C_BusTypeDef *bus) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = bus->sda_gpio_pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(bus->gpiox, &GPIO_InitStruct); }

由于应答信号需要读取/发送SDA的电平状态,所以封装两个配置SDA引脚为输入输出的内联函数,方便后续调用

/* 控制SCL,SDA电平状态和读取SDA电平 */ static inline uint8_t GET_SDA(struct I2C_BusTypeDef *bus) { return HAL_GPIO_ReadPin(bus->gpiox, bus->sda_gpio_pin); } static inline void SDA_L(struct I2C_BusTypeDef *bus) { HAL_GPIO_WritePin(bus->gpiox, bus->sda_gpio_pin, GPIO_PIN_RESET); } static inline void SDA_H(struct I2C_BusTypeDef *bus) { HAL_GPIO_WritePin(bus->gpiox, bus->sda_gpio_pin, GPIO_PIN_SET); } static inline void SCL_L(struct I2C_BusTypeDef *bus) { HAL_GPIO_WritePin(bus->gpiox, bus->scl_gpio_pin, GPIO_PIN_RESET); } static inline void SCL_H(struct I2C_BusTypeDef *bus) { HAL_GPIO_WritePin(bus->gpiox, bus->scl_gpio_pin, GPIO_PIN_SET); }

由于IIC通信后期会频繁的拉高拉低SCL和SDA线,所以将配置总线电平封装成一个个函数(或者宏定义也可以)

//IIC开始信号 static inline void I2C_Start(struct I2C_BusTypeDef *bus) { SDA_L(bus); bus->udelay(1); SCL_L(bus); } //IIC重启信号 static inline void I2C_Restart(struct I2C_BusTypeDef *bus) { SDA_H(bus); SCL_H(bus); bus->udelay(1); SDA_L(bus); bus->udelay(1); SCL_L(bus); } static inline void I2C_Stop(struct I2C_BusTypeDef *bus) { SDA_L(bus); bus->udelay(1); SCL_H(bus); bus->udelay(1); SDA_H(bus); bus->udelay(1); } //IIC接收ACK信号 static inline uint8_t I2C_WaitACK(struct I2C_BusTypeDef *bus) { uint8_t ack; SDA_H(bus); bus->udelay(1); SCL_H(bus); SDA_SetInMode(bus); ack = !GET_SDA(bus); /* ACK : SDA pin is pulled low */\ SDA_SetOutMode(bus); printf("%s", ack ? "ACK" : "NACK"); SCL_L(bus); return ack; } //IIC发送ACK信号 static inline void I2C_SendAckOrNack(struct I2C_BusTypeDef *bus, int ack) { if (ack) SDA_L(bus); bus->udelay(1); //主机接收完从机信号SCL拉低,在这块再拉高,SCL产生上升沿信号 SCL_H(bus); SCL_L(bus); } //IIC发送1字节数据外加从机应答信号 static uint8_t I2C_WriteByte(struct I2C_BusTypeDef *bus, uint8_t data) { uint8_t mask; for (mask = 0x80; mask != 0; mask >>= 1) { SCL_L(bus); data & mask ? SDA_H(bus) : SDA_L(bus); bus->udelay(1); SCL_H(bus); } SCL_L(bus); bus->udelay(1); return I2C_WaitACK(bus); } //IIC读取1个字节 static uint8_t I2C_ReadByte(struct I2C_BusTypeDef *bus) { uint8_t mask; uint8_t data = 0; SDA_H(bus); bus->udelay(1); SDA_SetInMode(bus); for (mask = 0x80; mask != 0; mask >>= 1) { //由于此时读取ACK电平是从机控制,所以在SCL上升沿后采样 SCL_H(bus); if (GET_SDA(bus)) data |= mask; SCL_L(bus); bus->udelay(1); } SDA_SetOutMode(bus); return data; }

接下来就是模拟时序部分了,把开始信号,ACK应答,发送/接收8字节以及停止信号分别按照时序规则封装出来,需要注意的是发送ACK信号是主机人为主动拉低SDA线,所以放在SCL拉高之前配置SDA电平,而接收ACK信号是从机被动拉低SDA线,所以应在SCL高电平处读取SDA电平

//IIC发送多个字节 static uint16_t I2C_SendBytes (struct I2C_BusTypeDef *bus, struct I2C_MessageTypeDef *msg){ //判断是否有应答 uint8_t ret; //备份需要传输的数据 const uint8_t *ptr = msg->buf; //实际接收到的字节数和备份需要传输数据的长度 uint16_t bytes = 0, count = msg->tLen; while (count > 0) { //若为发送相同字节且无应答 if(msg->flags & I2C_CONTROL_BYTE && I2C_WriteByte(bus, msg->cByte) == 0){ printf("send bytes: NACK."); break; } //发送相同数据标志位置1,一直发送相同字节,否则发送数据 ret = msg->flags & I2C_SAME_BYTE ? I2C_WriteByte(bus, msg->sByte) : I2C_WriteByte(bus, *ptr) , ptr++; //有应答或者无应答标志位置1且无应答 if ((ret > 0) || (msg->flags & I2C_IGNORE_NACK && (ret == 0))) { count --; bytes ++; }else if (ret == 0)//否则【warning】 { printf("send bytes: NACK."); break; } } //返回实际传输的字节数 return bytes; } //IIC接收多字节 static uint16_t I2C_RecvBytes(struct I2C_BusTypeDef *bus, struct I2C_MessageTypeDef *msg) { uint8_t val; uint8_t *ptr = msg->buf; uint16_t bytes = 0, count = msg->tLen; while (count > 0) { val = I2C_ReadByte(bus); *ptr = val; bytes ++; ptr++; count--; printf("recieve bytes: 0x%02x, %s",val, (msg->flags & I2C_NO_READ_ACK) ? "(No ACK/NACK)" : (count ? "ACK" : "NACK")); //若读的无应答标志位不成立 if (!(msg->flags & I2C_NO_READ_ACK)) { I2C_SendAckOrNack(bus, count); } } return bytes; } //发送单个 1 字节 I2C 地址 static uint8_t I2C_SendAddress(struct I2C_BusTypeDef *bus, uint8_t addr, uint32_t retries) { uint8_t i, ret = 0; for (i = 0; i <= retries; i++) { ret = I2C_WriteByte(bus, addr); if (ret == 1) { printf("response ok."); break; } else if (i == retries) { printf("no response, please check slave device."); break; } printf("no response, attempt to resend the address. number:%d.", i); I2C_Stop(bus); bus->udelay(1); I2C_Start(bus); } return ret; } //IIC发送10位或7位地址全流程 static uint8_t I2C_BitSendAddress(struct I2C_BusTypeDef *bus, struct I2C_MessageTypeDef *msg) { uint8_t ret, retries, addr1, addr2; uint8_t flags = msg->flags; uint8_t ignore_nack = msg->flags & I2C_IGNORE_NACK; //无应答retries置0,否则为句柄传输值 retries = ignore_nack ? 0 : bus->retries; if (flags & I2C_ADDR_10BIT) {/*10位地址 发送第一字节 `11110 A9 A8 0` 发送第二字节 `A7~A0` */ /* 读10位地址 1. Start → 发写方向 10bit 两个地址字节(告诉设备我要访问哪个 10bit 从机) 2. Repeated Start(重复起始,不发 Stop) 3. 再发读方向的第一字节:`11110 A9 A8 1`(R/W 置 1) 4. 然后接收数据 写10位地址 1. Start → 发写方向 10bit 两个地址字节(告诉设备我要访问哪个 10bit 从机) */ addr1 = 0xf0 | ((msg->addr >> 7) & 0x06); addr2 = msg->addr & 0xff; printf("addr1: %d, addr2: %d", addr1, addr2); ret = I2C_SendAddress(bus, addr1, retries); if ((ret != 1) && !ignore_nack) { printf("NACK: sending first addr"); return 0; } ret = I2C_WriteByte(bus, addr2); if ((ret != 1) && !ignore_nack) { printf("NACK: sending second addr"); return 0; } if (flags & I2C_RD) { printf("send repeated start condition"); I2C_Restart(bus); addr1 |= 0x01; ret = I2C_SendAddress(bus, addr1, retries); if ((ret != 1) && !ignore_nack) { printf("NACK: sending repeated addr"); return 0; } } }/*7位地址*/ else{ //7位地址+1位读写位 addr1 = msg->addr << 1; if (flags & I2C_RD) addr1 |= 1; ret = I2C_SendAddress(bus, addr1, retries); if ((ret != 1) && !ignore_nack) return 0; } return 1; } //多消息 I2C 传输接口,支持一次性提交一组 I2C 消息数组(读、写混合) uint32_t I2C_TransferMessages(struct I2C_BusTypeDef *bus, struct I2C_MessageTypeDef msgs[], uint32_t num) { struct I2C_MessageTypeDef *msg; uint32_t i, ret = 0; uint8_t ignore_nack; //传参错误退出 if (NULL == bus || NULL == msgs || num == 0)return ret; for (i = 0; i < num; i++) { msg = &msgs[i]; ignore_nack = msg->flags & I2C_IGNORE_NACK; //不启动标志位不成立 if (!(msg->flags & I2C_NO_START)) { if (i) { I2C_Restart(bus); } else { printf("send start condition"); I2C_Start(bus); } ret = I2C_BitSendAddress(bus, msg); if ((ret != 1) && !ignore_nack) { printf("receive NACK from device addr 0x%02x msg %d", msgs[i].addr, i); goto out; } } if (msg->flags & I2C_RD) { ret = I2C_RecvBytes(bus, msg); msg->rLen = ret; printf("read %d byte%s", ret, ret == 1 ? "" : "s"); } else { ret = I2C_SendBytes(bus, msg); msg->rLen = ret; printf("write %d byte%s", ret, ret == 1 ? "" : "s"); if (msg->rLen != msg->tLen) { ret = 0; goto out; } } } ret = i; out: if (!(msg->flags & I2C_NO_STOP)) { printf("send stop condition"); I2C_Stop(bus); } return ret; }

最终封装成多消息 I2C 传输接口函数,也是整个文件的最终目的,主要调用关系为起始信号->IIC发送地址->应答->判断读或写,发送或接收多字节->结束信号

把底层IIC传输封装完,接下来就是bq芯片的驱动,bq76920 是 BMS 系统中面向 BQ76920 AFE 芯片的底层驱动模块。主要负责访问 BQ76920 寄存器,实现单节电芯电压、电池总电压、电流和 NTC 温度采集, ADC 校准参数, BQ76920 的初始化、唤醒和低功耗控制,配置过压、欠压、放电过流和短路保护参数,控制充电和放电 MOS,设置电芯均衡状态,并处理芯片通过 ALERT 引脚产生的保护事件。目的是使上层 BMS 框架可以通过简单的函数接口获取采样数据和控制电池状态

#define SYS_STAT 0x00 #define CELLBAL1 0x01 #define CELLBAL2 0x02 #define CELLBAL3 0x03 #define SYS_CTRL1 0x04 #define SYS_CTRL2 0x05 #define PROTECT1 0x06 #define PROTECT2 0x07 #define PROTECT3 0x08 #define OV_TRIP 0x09 #define UV_TRIP 0x0A #define CC_CFG 0x0B #define VC1_HI_BYTE 0x0C #define CC_HI_BYTE 0x32 #define CC_LO_BYTE 0x33 #define BAT_HI_BYTE

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询