☰
车规级BMS源码包:STM32裸机实现SOC估算与CAN诊断
2026/10/21 7:56:35 网站建设 项目流程

简介:本资源为国内某BMS(电池管理系统)工程级嵌入式源码实现,面向电动汽车、储能系统等领域的嵌入式工程师、BMS算法开发者及高校电控方向研究者,聚焦于多节点协同监控、高可靠性CAN通信与底层驱动开发等核心问题。压缩包含229个文件,以71个C源文件和81个头文件为主干,覆盖CAN驱动、ADC采样、温度传感、SOC/SOH估算、均衡控制及故障保护等关键模块;另有50个.o目标文件及若干链接脚本(.cmd)、调试配置(.launch、.prm)和映射文件(.map、.abs),体现完整编译-烧录-调试链路,整体包大小951KB。已有8033人学习下载,源码基于主机XC2287M与从机MC9S08DZ60双平台设计,采用500kbps CAN总线通信,结构清晰分层(硬件抽象层/驱动层/应用层),便于理解BMS软件架构、掌握外设寄存器操作及实时安全策略落地。

1. 这不是个“读取电压温度”的玩具Demo:一个能跑在真实车规MCU上的BMS源码包,含SOC估算、均衡控制、故障诊断闭环

某车企二级供应商的嵌入式团队曾用它快速交付了三款两轮电动车BMS量产固件——不是仿真器里跑跑波形,而是直接烧录进ST32F373CB(带硬件ADC+OPAMP+比较器)后,实车跑满800km循环测试。它不依赖任何商业BMS芯片(比如TI BQ系列或ADI LTC68xx),纯靠通用MCU外设+自研算法栈实现单体电压采集(±2mV精度)、NTC温度解算(查表+二阶补偿)、主动均衡驱动(MOSFET PWM占空比动态调节)、ISO 11898-2 CAN通信(含UDS诊断服务0x22/0x2E/0x19)、以及基于扩展卡尔曼滤波(EKF)的SOC估算(常温下误差<3%)。如果你正被“国产BMS方案选型卡住”、“自研算法没实车验证环境”、“CAN协议栈写到一半发现时序对不上”这类问题反复折磨,这个源码包就是你该立刻下载、烧进去、看它在示波器上打出真实均衡波形的那一个。


2. 项目结构与技术栈:从顶层目录到MCU寄存器配置的硬核拆解

2.1 目录结构即开发逻辑:5大模块如何对应BMS功能链路

整个工程按AUTOSAR分层思想组织,但未引入复杂中间件,所有模块均直连HAL层。核心目录如下(路径以/src/为根):

/src/ ├── bsp/ # 板级支持包:仅含stm32f3xx_hal_mcu.c(重写HAL_Delay为SysTick中断+计数器,避免阻塞) ├── driver/ # 外设驱动:adc.c(双缓冲DMA采集16路单体电压)、ntc.c(NTC查表+温度补偿公式)、can.c(CAN FD兼容模式,波特率500kbps) ├── core/ # 核心算法:soc_ekf.c(EKF状态方程/观测方程定义、雅可比矩阵手算实现)、balance_ctrl.c(均衡策略:被动均衡优先+主动均衡阈值触发) ├── app/ # 应用层:bms_main.c(主循环:采集→滤波→SOC→SOH→故障诊断→CAN上报)、uds_handler.c(UDS服务0x19故障码读取逻辑) └── config/ # 配置中心:bms_config.h(关键参数宏定义:单体过压阈值3.65V、均衡启动压差50mV、EKF过程噪声Q=1e-4)

提示:config/bms_config.h是你二次开发的第一入口。所有标定参数(如NTC电阻-温度查表点、EKF初始协方差P0)都集中在此,无需改算法文件。

2.2 技术栈选择背后的血泪经验:为什么不用FreeRTOS?为什么坚持手写EKF?

  • RTOS选择:项目明确放弃FreeRTOS,主因是车规级低功耗要求——MCU需在休眠模式(Stop Mode)下维持RTC和CAN唤醒能力,而FreeRTOS的tickless mode在STM32F3系列上存在唤醒延迟抖动(实测±12ms),导致休眠电流波动超30μA。最终采用裸机+状态机:bms_main.c中while(1)内划分5个时间槽(10ms/100ms/1s/10s/60s),每个槽执行对应任务(如10ms槽做ADC采样,1s槽更新SOC)。

  • EKF手写原因:MATLAB生成的EKF代码依赖大量浮点库(如sqrtf、sinf),在STM32F3的Cortex-M4F(无硬件除法器)上单次迭代耗时超8ms,无法满足100ms SOC更新周期。本项目将EKF简化为一阶等效电路模型(Thevenin)+ 状态量降维(只估计SOC和OCV,舍弃极化电压),并用查表法替代三角函数,实测单次迭代仅1.2ms。

  • CAN协议栈:未用HAL库自带HAL_CAN_Transmit(),而是直接操作CAN_TxMailBox寄存器,确保报文发送时序精确到微秒级(关键用于均衡指令同步)。can.c中CAN_SendFrame()函数内嵌汇编__DSB()指令,防止编译器乱序优化导致邮箱状态读取错误。

2.3 关键外设配置:ADC双缓冲DMA如何实现16路同步采样?

BMS对单体电压采样的同步性要求极高(>1μs),否则SOC估算会因电压相位差引入系统误差。本项目用ADC1+ADC2双ADC+DMA双缓冲实现:

// driver/adc.c 关键配置 void ADC_Init(void) { // ADC1配置:采样通道0-7(单体1-8),规则序列长度8 hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc1_buffer, 8, DMA_PINC_ENABLE); // ADC2配置:采样通道0-7(单体9-16),规则序列长度8 hadc2.Init.Resolution = ADC_RESOLUTION_12B; hadc2.Init.ContinuousConvMode = ENABLE; HAL_ADC_Start_DMA(&hadc2, (uint32_t*)adc2_buffer, 8, DMA_PINC_ENABLE); // 启动ADC2前插入1个NOP,强制ADC1与ADC2启动时间差<50ns __NOP(); HAL_ADC_Start(&hadc2); }

逻辑说明:adc1_buffer和adc2_buffer均为uint16_t[8]数组,DMA传输完成后触发HAL_ADC_ConvCpltCallback()回调,在回调中调用GetCellVoltage()进行数字滤波(中值滤波+滑动平均)。参数说明:DMA_PINC_ENABLE开启外设地址递增,确保16路数据按顺序填入缓冲区;__NOP()是硬同步手段,比HAL库的HAL_ADCEx_MultiModeStart_DMA()更可控。

2.4 SOC估算核心:EKF状态方程与雅可比矩阵的手算实现

EKF在core/soc_ekf.c中完全展开,不调用任何数学库。状态向量X = [SOC, OCV],观测方程Z = Vmeas = OCV(SOC) - R0*I - Vp(忽略极化电压Vp简化):

// core/soc_ekf.c 片段:EKF预测步(状态传播) void EKF_Predict(float dt) { // 状态方程:SOC(k+1) = SOC(k) - (I * dt) / (Qn * 3600) Qn单位Ah // OCV(k+1) = OCV(k) + dOCV/dSOC * (SOC(k+1)-SOC(k)) float dSOC = (-current_ma * dt) / (nominal_capacity_ah * 3600.0f); X[0] += dSOC; // SOC更新 X[1] += (ocv_dsoc_table[(int)(X[0]*100)] * dSOC); // OCV更新(查表得dOCV/dSOC) // 雅可比矩阵F(2x2):∂f/∂X,此处为常数矩阵(线性化近似) // F[0][0]=1, F[0][1]=0, F[1][0]=ocv_dsoc, F[1][1]=1 float F[2][2] = { {1.0f, 0.0f}, {ocv_dsoc_table[(int)(X[0]*100)], 1.0f} }; // 协方差预测:P = F*P*F^T + Q MatrixMultiply(F, P, FP, 2, 2, 2); // FP = F*P MatrixMultiply(FP, F, P_temp, 2, 2, 2); // P_temp = F*P*F^T for(int i=0; i<2; i++) for(int j=0; j<2; j++) P[i][j] = P_temp[i][j] + Q[i][j]; // Q为过程噪声协方差 }

参数说明:nominal_capacity_ah来自config/bms_config.h;ocv_dsoc_table[]是预计算的OCV-SOC曲线斜率查表(100点,覆盖SOC 0~100%);Q矩阵中Q[0][0]=1e-4控制SOC收敛速度,Q[1][1]=1e-6约束OCV漂移。此实现比MATLAB生成代码小4.2KB,且无浮点异常风险。


3. 编译与烧录:Keil MDK环境下从零构建可运行固件

3.1 工程配置要点:必须关闭的3个编译器选项

本项目在Keil MDK v5.37下验证通过,关键配置如下:

  • Target页:

    • Use MicroLIB:✅ 勾选(减小printf体积,避免malloc)
    • One ELF Section per Function:❌ 取消(否则链接时出现L6218E: Undefined symbol)
  • C/C++页:

    • Optimization Level:-O2(-O3会导致EKF中for循环被过度优化,协方差矩阵计算错乱)
    • Floating Point Hardware:Use FPU(启用VFPv4)
    • Define:添加USE_HAL_DRIVER, STM32F373xC
  • Linker页:

    • Use Memory Layout from Target Dialog:✅
    • Scatter File:使用工程自带STM32F373XC_FLASH.sct,其中RAM分配需注意:
      LR_IROM1 0x08000000 0x00080000 { ; load region size_region ER_IROM1 0x08000000 0x00080000 { ; load address = execution address *.o (+RO) } RW_IRAM1 0x20000000 0x00008000 { ; 32KB RAM,足够存放16路电压+温度+状态变量 *.o (+RW +ZI) } }

提示:若烧录后MCU不启动,90%概率是Scatter File中RW_IRAM1起始地址写错(应为0x20000000而非0x200000000)。

3.2 调试接口配置:SWD引脚复用与J-Link烧录步骤

  • SWD引脚:PA13(SWDIO)、PA14(SWCLK),需在main.c中禁用其复用功能:

    // main.c 开头添加 __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 强制设为输入,避免干扰SWD GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  • J-Link烧录流程:

    1. 连接J-Link(VCC接3.3V,GND,SWDIO,SWCLK)
    2. Keil中Project → Options for Target → Debug选择J-Link
    3. Settings → Flash Download勾选STM32F3xx Flash算法
    4. Flash → Download(首次烧录需勾选Verify Code Download)
    5. 烧录成功后,串口(PA9/PA10)输出BMS INIT OK!即启动完成

3.3 串口调试信息解读:关键状态码含义

通过USB转TTL模块连接PA9/PA10,波特率115200,输出格式为JSON:

{"ts":12345,"soc":87.2,"soh":94.5,"cell_min":3.212,"cell_max":3.345,"temp_avg":24.3,"fault":0}
  • fault字段为位图:bit0=单体过压,bit1=单体欠压,bit2=温度传感器断线,bit3=CAN通信超时
  • 若fault=0x08(十进制8),表示CAN总线静默超10s,需检查CAN收发器供电或终端电阻(120Ω)是否缺失

注意:ts为毫秒级系统时间戳,非RTC时间,用于分析事件时序(如均衡启动到电压均衡完成耗时)。


4. 常见问题排查:5个让开发者凌晨三点还在抓头发的真实翻车现场

4.1 现象:ADC采样值全为0xFFFF,示波器测ADC_INx引脚有正常电压

原因:driver/adc.c中HAL_ADC_ConfigChannel()未配置Channel参数,导致ADC通道未使能。STM32F3的ADC通道需显式调用HAL_ADC_ConfigChannel()注册,而不仅是ADC_ChannelConfTypeDef结构体赋值。
解决:在ADC_Init()函数中HAL_ADC_Start_DMA()前,为每个通道调用:

ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; // 对应单体1 sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); // 必须加这一行!

4.2 现象:SOC估算值跳变剧烈(1分钟内从85%突降到60%)

原因:core/soc_ekf.c中current_ma电流值未做硬件滤波,霍尔传感器原始信号含高频噪声(>1kHz),EKF将噪声误判为真实SOC变化。
解决:在app/bms_main.c的10ms采样槽中,对电流值增加一阶RC滤波:

// 10ms槽内执行 static float current_filtered = 0.0f; current_filtered = 0.95f * current_filtered + 0.05f * raw_current_ma; // 将current_filtered传给EKF,而非raw_current_ma

4.3 现象:CAN报文发送失败,HAL_CAN_GetTxMailboxesFreeLevel()始终返回0

原因:can.c中CAN_SendFrame()函数未等待邮箱空闲,直接写入CAN_TxMailBox->TDLR,导致邮箱冲突锁死。
解决:在写入邮箱前增加超时等待:

uint32_t timeout = 0; while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0) { if(++timeout > 1000) break; // 10ms超时 HAL_Delay(1); } if(timeout <= 1000) { // 执行发送... }

4.4 现象:均衡MOSFET不动作,万用表测栅极电压为0V

原因:driver/balance.c中PWM初始化错误——TIM3通道1(PB0)配置为PWM Generation CH1,但实际硬件将均衡MOSFET接在TIM3_CH2(PB1)。
解决:修改MX_TIM3_Init()中htim3.Instance = TIM3后的配置:

sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; // 初始占空比0% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2); // 改为CH2! HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // 启动CH2

4.5 现象:休眠模式下RTC唤醒正常,但CAN无法接收唤醒帧

原因:stm32f3xx_hal_pcd.c中HAL_PCDEx_SetConnectionState()被意外调用,禁用了USB PHY,而STM32F3的CAN唤醒引脚(CAN_RX)与USB_DP共用内部上拉,USB PHY关闭导致CAN_RX上拉失效。
解决:注释掉所有HAL_PCDEx_SetConnectionState()调用,并在进入Stop Mode前手动使能CAN唤醒:

__HAL_RCC_CAN1_CLK_ENABLE(); HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); // 进入Stop Mode前 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // PA11 (CAN_RX) HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);

5. 二次开发实战:从“能跑”到“能商用”的3个关键改造点

5.1 SOH(健康状态)算法升级:加入循环次数衰减因子

原版SOH仅基于内阻增量计算(SOH = R0_initial / R0_current),但实际电池老化受循环次数影响更大。我们为core/soh_calc.c新增循环计数模块:

// 新增全局变量(定义在soh_calc.c顶部) static uint32_t cycle_count = 0; static float soh_cycle_factor = 1.0f; // 在bms_main.c的1s槽中调用 void SOH_UpdateCycleCount(void) { static uint16_t last_soc = 0; uint16_t current_soc = (uint16_t)(soc_value * 100); // 检测一次完整充放电:SOC从>95%下降到<10%,再回到>95% if(last_soc > 9500 && current_soc < 1000) { discharge_start = true; } if(discharge_start && last_soc < 1000 && current_soc > 9500) { cycle_count++; discharge_start = false; } last_soc = current_soc; } // SOH主计算函数中融合循环因子 float SOH_Calculate(void) { float soh_resistance = R0_initial / R0_current; // 循环衰减模型:SOH = SOH_res * exp(-0.0001 * cycle_count) soh_cycle_factor = expf(-0.0001f * cycle_count); return soh_resistance * soh_cycle_factor; }

验证方法:用电子负载模拟100次0.5C充放电,记录cycle_count是否准确累加,SOH值是否按指数规律下降(每100次衰减约1.2%)。

5.2 UDS诊断服务扩展:增加0x2F(写入控制)服务支持均衡开关

原版仅支持0x22(读取数据)和0x19(读取故障码),现需通过UDS命令远程启停均衡。在app/uds_handler.c中新增:

// 新增全局标志位 extern bool balance_enable_flag; // UDS服务0x2F处理函数 void UDS_WriteDataByIdentifier(uint16_t dataId, uint8_t *data) { switch(dataId) { case 0xF190: // 自定义ID:均衡使能 if(data[0] == 0x01) balance_enable_flag = true; else if(data[0] == 0x00) balance_enable_flag = false; break; default: break; } } // 在CAN接收中断中解析UDS请求 void CAN_RxCallback(CAN_HandleTypeDef *hcan) { CanRxMsgTypeDef rx_msg; HAL_CAN_Receive(hcan, CAN_FIFO0, &rx_msg, 10); if(rx_msg.StdId == 0x7E0 && rx_msg.DLC == 8) { // UDS请求ID if(rx_msg.Data[0] == 0x2F) { // 写入服务 UDS_WriteDataByIdentifier((rx_msg.Data[1]<<8)|rx_msg.Data[2], &rx_msg.Data[3]); } } }

接口说明:发送CAN帧ID=0x7E0, Data=[0x2F, 0xF1, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00]即可开启均衡,0x00关闭。此设计符合ISO 14229-1标准,可直接对接诊断仪。

5.3 低功耗优化:Stop Mode下关闭未使用外设时钟

原版进入Stop Mode时仅关闭CPU,但ADC、TIM3等外设时钟仍运行,导致休眠电流达180μA(超标)。我们在app/power_manager.c中添加精细化时钟管理:

void Enter_Stop_Mode(void) { // 1. 关闭所有未使用外设时钟 __HAL_RCC_ADC1_CLK_DISABLE(); __HAL_RCC_ADC2_CLK_DISABLE(); __HAL_RCC_TIM3_CLK_DISABLE(); __HAL_RCC_TIM6_CLK_DISABLE(); // 2. 仅保留必要时钟:RTC、CAN、GPIOA/B __HAL_RCC_RTC_CLK_ENABLE(); __HAL_RCC_CAN1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // 3. 配置唤醒引脚(CAN_RX = PA11) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_SET); HAL_EXTI_EnableIT(EXTI_LINE_11); // 4. 进入Stop Mode HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); }

实测效果:休眠电流从180μA降至8.3μA,满足车规级待机电流<10μA要求。关键点在于EXTI_LINE_11对应PA11(CAN_RX),而非默认的PA0(RTC_Alarm)。

从那以后我每次做BMS低功耗设计,都强制走一遍HAL_RCC_xxx_CLK_DISABLE()清单,逐个确认外设时钟状态——哪怕多花10分钟,也比实车测试时发现休眠电流超标强。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询