BQ76930():
QT上位机:
项目描述
48V 智能电池管理板核心目标是实现精准监测、多维保护、低耗节能。适配电动自行车、滑板车、便携式储能设备、工业 AGV 机器人三大核心场景,为设备提供安全可靠的电池管理解决方案。
项目实现
基于 STM32F103C8T6+TI 芯片组(BQ76940AFE/BQ34Z100-G1/BQ76200)
- freertos 实现 7 大核心任务。故障保护 / 电芯电压与电流采样 / 电池均衡控制 / SOC 计算与电量管理 / 充放电状态识别与逻辑控制 / 外部 CAN 通信 / 其它辅助功能。
- BQ76940 中断处理过流、短路、过温、过压 / 欠压等紧急故障。读取 13 串电压 (I2C)、总电流、NTC 温度(内置 ADC)。电池均衡控制 (内置均衡开关与外部均衡电阻)。
- BQ34Z100-G1 (I2C) 计算 SOC (剩余电量)、循环次数、健康状态 (剩余电量),并处理容量学习。
- BQ76200 (GPIO) 控制主回路中功率 MOS 管的通断。
- CAN 总线数据收发,支持与外部设备(仪表盘、AGV 主控)的信息交互。管理板的对外接口。
- 技术扩展 1。QT 上位机 (eth<lwip>\wifi<spi>\uart) 测试显示。IAP/OTA 升级。看门狗保护。
- 技术扩展 2。多 BMS 节点 + QT 实现数字化多电池组集中监控与管理系统。
| 任务名称 | 函数名 | 优先级(FreeRTOS) | 任务周期 | 函数作用 |
|---|---|---|---|---|
| 故障保护任务 | void FaultProtectTask(void *pvParameters) |
7(最高) | 非周期性(中断触发 + 10ms 轮询兜底) | 处理 BQ76940 中断上报的过流 / 短路 / 过温等故障,执行保护逻辑(如切断 MOS 管) |
| 电芯电压与电流采样任务 | void CellSampleTask(void *pvParameters) |
6 | 250ms(匹配 BQ76940 硬件采样周期) | 定时读取 BQ76940 的电芯电压、电流、温度数据,更新全局状态变量 |
| 充放电状态识别与逻辑控制任务 | void ChargeDischargeCtrlTask(void *pvParameters) |
5 | 50ms | 根据采样数据和故障状态,控制 BQ76200 驱动 MOS 管,切换充放电状态 |
| 外部 CAN 通信任务 | void CANCommTask(void *pvParameters) |
4 | 100ms(主动上报)+ 中断触发(接收) | 处理 CAN 总线数据收发,解析外部指令并上报电池状态 |
| SOC 计算与电量管理任务 | void SOCCalculateTask(void *pvParameters) |
3 | 1s | 基于安时积分 + 开路电压 法计算 SOC、SOH,管理电池容量数据 |
| 电池均衡控制任务 | void CellBalanceTask(void *pvParameters) |
2 | 1 分钟(或按需触发) | 检测电芯压差,控制 BQ76940 均衡开关,实现电芯主动均衡 |
| 其它辅助功能任务 | void AuxiliaryTask(void *pvParameters) |
1(最低) | 500ms~10s(依功能类型) | 处理指示灯、看门狗喂狗、日志记录等非核心功能 |
| 专属打印任务(异步队列用) | void PrintTask(void *pvParameters) |
1(最低) | 无固定周期(队列消息触发) | 从打印队列读取数据,统一输出到串口,避免多任务打印冲突 |
CAN:
1:


数据帧+标准帧
cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file can.c
* @brief This file provides code for the configuration
* of the CAN instances.
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "can.h"
/* USER CODE BEGIN 0 */
CAN_FilterTypeDef CAN_Filter;
/* USER CODE END 0 */
CAN_HandleTypeDef hcan;
/* CAN init function */
void MX_CAN_Init(void)
{
/* USER CODE BEGIN CAN_Init 0 */
// 波特率 = APB1时钟 (CAN挂载的总线)/ (Prescaler * (TimeSeg1 + TimeSeg2 + 1))
// 示例:APB1=36MHz,Prescaler=6,TimeSeg1=5,TimeSeg2=2,波特率为:
// 36MHz / (16 * (1+1+1)) = 750kbps
/* USER CODE END CAN_Init 0 */
/* USER CODE BEGIN CAN_Init 1 */
/* USER CODE END CAN_Init 1 */
hcan.Instance = CAN1;
hcan.Init.Prescaler = 9;
hcan.Init.Mode = CAN_MODE_LOOPBACK;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_12TQ;
hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = DISABLE;
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN CAN_Init 2 */
// 32位滤波位
CAN_Filter.FilterIdHigh=0x0000<<16; // ID
CAN_Filter.FilterIdLow=0x0000;
CAN_Filter.FilterMaskIdHigh=0x0000<<16; // 掩码
CAN_Filter.FilterMaskIdLow=0x0000;
CAN_Filter.FilterFIFOAssignment=CAN_FILTER_FIFO0;
CAN_Filter.FilterBank=0;
CAN_Filter.FilterMode=CAN_FILTERMODE_IDMASK; // 掩码模式
CAN_Filter.FilterScale=CAN_FILTERSCALE_32BIT;
CAN_Filter.FilterActivation=CAN_FILTER_ENABLE;
HAL_CAN_ConfigFilter(&hcan,&CAN_Filter);
HAL_CAN_Start(&hcan);
/* USER CODE END CAN_Init 2 */
}
void HAL_CAN_MspInit(CAN_HandleTypeDef* canHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(canHandle->Instance==CAN1)
{
/* USER CODE BEGIN CAN1_MspInit 0 */
/* USER CODE END CAN1_MspInit 0 */
/* CAN1 clock enable */
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**CAN GPIO Configuration
PA11 ------> CAN_RX
PA12 ------> CAN_TX
*/
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN CAN1_MspInit 1 */
/* USER CODE END CAN1_MspInit 1 */
}
}
void HAL_CAN_MspDeInit(CAN_HandleTypeDef* canHandle)
{
if(canHandle->Instance==CAN1)
{
/* USER CODE BEGIN CAN1_MspDeInit 0 */
/* USER CODE END CAN1_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_CAN1_CLK_DISABLE();
/**CAN GPIO Configuration
PA11 ------> CAN_RX
PA12 ------> CAN_TX
*/
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_11|GPIO_PIN_12);
/* USER CODE BEGIN CAN1_MspDeInit 1 */
/* USER CODE END CAN1_MspDeInit 1 */
}
}
/* USER CODE BEGIN 1 */
/**
* @brief 发送CAN消息
* @param ID: 标识符
* @param Length: 数据长度(1-8)
* @param RTR: 1-远程帧, 0-数据帧
* @param data: 数据指针
* @param IDE: 1-扩展帧, 0-标准帧
* @retval HAL状态值
*/
HAL_StatusTypeDef CAN_AddTxMessage(uint32_t ID, uint8_t Length, uint8_t RTR, uint8_t *data, uint8_t IDE)
{
CAN_TxHeaderTypeDef TxHeader;
uint32_t TxMailbox;
// 校验数据长度
if (Length == 0 || Length > 8) {
return HAL_ERROR;
}
// 配置CAN消息头
if (IDE) {
TxHeader.IDE = CAN_ID_EXT; // 扩展帧
TxHeader.ExtId = ID;
TxHeader.StdId = 0; // 扩展帧时标准ID无效
} else {
TxHeader.IDE = CAN_ID_STD; // 标准帧
TxHeader.StdId = ID & 0x7FF; // 确保ID在11位范围内
TxHeader.ExtId = 0; // 标准帧时扩展ID无效
}
// 配置远程/数据帧
TxHeader.RTR = (RTR == 1) ? CAN_RTR_REMOTE : CAN_RTR_DATA;
// 配置数据长度
TxHeader.DLC = Length;
TxHeader.TransmitGlobalTime = DISABLE;
// 等待发送邮箱空闲
while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0);
// 发送CAN消息并返回状态
return HAL_CAN_AddTxMessage(&hcan, &TxHeader, data, &TxMailbox);
}
/**
* @brief 接收CAN消息
* @param ID: 接收ID指针
* @param Length: 接收长度指针
* @param Data: 接收数据指针
* @retval HAL状态值
*/
HAL_StatusTypeDef MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data)
{
CAN_RxHeaderTypeDef RxHeader;
uint8_t rxData[8] = {0}; // 初始化接收数据数组
// 检查FIFO0是否有消息
if (HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0) == 0) {
return HAL_TIMEOUT;
}
// 接收CAN消息
if (HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &RxHeader, rxData) != HAL_OK) {
return HAL_ERROR;
}
// 提取ID
if (RxHeader.IDE == CAN_ID_STD) {
*ID = RxHeader.StdId; // 标准ID
} else {
*ID = RxHeader.ExtId; // 扩展ID
}
// 提取数据(仅数据帧有效)
if (RxHeader.RTR == CAN_RTR_DATA) {
*Length = RxHeader.DLC > 8 ? 8 : RxHeader.DLC;
for (uint8_t i = 0; i < *Length; i++) {
Data[i] = rxData[i];
}
} else {
*Length = 0; // 远程帧无数据
}
return HAL_OK;
}
/**
* @brief 通过CAN发送int类型数据(4字节)
* @param ID: CAN标识符(标准帧/扩展帧)
* @param int_data: 要发送的int数据
* @param IDE: 1-扩展帧, 0-标准帧
* @retval HAL状态值
*/
HAL_StatusTypeDef CAN_SendIntData(uint32_t ID, int int_data, uint8_t IDE)
{
uint8_t tx_data[4]; // int占4字节,存放到字节数组
// 将int拆分为4个字节(小端模式:低字节在前)
tx_data[0] = (uint8_t)(int_data & 0xFF); // 第0字节:最低8位
tx_data[1] = (uint8_t)((int_data >> 8) & 0xFF); // 第1字节:次低8位
tx_data[2] = (uint8_t)((int_data >> 16) & 0xFF); // 第2字节:次高8位
tx_data[3] = (uint8_t)((int_data >> 24) & 0xFF); // 第3字节:最高8位
// 调用已有的发送函数,数据长度为4,RTR=0(数据帧)
return CAN_AddTxMessage(ID, 4, 0, tx_data, IDE);
}
/**
* @brief 从CAN接收数据并还原为int类型
* @param ID: 接收的CAN标识符(输出)
* @param int_data: 接收的int数据(输出)
* @retval HAL状态值
*/
HAL_StatusTypeDef CAN_ReceiveIntData(uint32_t *ID, int *int_data)
{
uint8_t rx_data[4];
uint8_t length;
HAL_StatusTypeDef status;
// 调用已有的接收函数,获取4字节数据
status = MyCAN_Receive(ID, &length, rx_data);
if (status != HAL_OK || length != 4) {
return HAL_ERROR;
}
// 将4个字节合并为int(小端模式)
*int_data = (int)(
(rx_data[3] << 24) |
(rx_data[2] << 16) |
(rx_data[1] << 8) |
rx_data[0]
);
return HAL_OK;
}
/* USER CODE END 1 */
/* USER CODE BEGIN Header_CANCommTask */
/**
* @brief Function 外部 CAN 通信任务
* @param argument: 处理 CAN 总线数据收发,解析外部指令并上报电池状态
* @retval None
*/
/* USER CODE END Header_CANCommTask */
void CANCommTask(void *argument)
{
/* USER CODE BEGIN CANCommTask */
uint32_t preTime = xTaskGetTickCount();
uint32_t TxID = 0x555; // 发送ID (标准帧)
uint8_t TxLength = 8; // 发送数据长度
uint8_t TxData[] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88}; // 发送数据
uint32_t RxID; // 接收ID
uint8_t RxLength; // 接收数据长度
uint8_t RxData[8]; // 接收数据缓冲区
int data;
/* Infinite loop */
for(;;)
{
CAN_SendIntData(TxID,Battery[0],0);
CAN_SendIntData(TxID,Battery[1],0);
CAN_SendIntData(TxID,Battery[2],0);
CAN_SendIntData(TxID,Battery[3],0);
CAN_SendIntData(TxID,Battery[4],0);
CAN_SendIntData(TxID,Battery[5],0);
CAN_SendIntData(TxID,Battery[6],0);
CAN_SendIntData(TxID,Battery[7],0);
CAN_SendIntData(TxID,Battery[8],0);
CAN_SendIntData(TxID,Battery[9],0);
CAN_SendIntData(TxID,Battery[10],0);
// if ( CAN_SendIntData(TxID,Battery[0],0) != HAL_OK) {
// u1_printf("CAN发送失败!\n");
// } else {
// u1_printf("发送数据: ID=0x%03X\r\n", TxID);
// u1_printf("TXdata[]:=0x%02X %d\r\n", Battery[0],Battery[0]);
// }
// // // 接收CAN消息
// if (CAN_ReceiveIntData(&RxID, &data) == HAL_OK) {
// if (RxLength > 0) {
// u1_printf("接收到数据: ID=0x%03X, 长度=%d\r\n",data, data );
// }
// }
// 接收CAN消息
if (MyCAN_Receive(&RxID, &RxLength, RxData) == HAL_OK);
if(RxData[0]==0xAA&&RxData[1]==0xBB&&RxData[2]==0x13&&RxData[3]==0xCC)
{
xEventGroupSetBits(EventGroupHandle,V_C_T_FLAG);
}
vTaskDelayUntil(&preTime, 1000);
}
/* USER CODE END CANCommTask */
}
内存:
https://zhuanlan.zhihu.com/p/1975520017564067749
https://zhuanlan.zhihu.com/p/1975520017564067749

ROM:64K ARM:20K
stm32f103c8t6 ARM一点少容易被撑死。

1. pflash 分区(非易失性,断电数据保留)
存放程序代码、只读数据 ,特点是只读 / 少写、断电不丢失,是程序的 "静态基础":
| 分区名称 | 作用说明 | 核心特性 |
|---|---|---|
.text(函数) |
存放编译后的机器指令(程序的执行逻辑,如函数体、算法流程的二进制代码) | - 程序启动后,CPU 从这里取指令执行 - 只读,运行时不可修改 |
.rodata |
存放只读数据 (全局 const 常量、字符串字面量,如 const int a = 10;) |
- 只读,防止程序意外篡改数据 - 直接存储在 pflash,节省 RAM 空间 |
2. ram 分区(易失性,断电数据丢失)
存放动态数据、运行时临时空间 ,特点是可读写、断电清空,是程序的 "动态工作区":
| 分区名称 | 作用说明 | 核心特性 |
|---|---|---|
.bss |
存放未显式初始化的全局 / 静态变量 (默认值为 0,如 int global_var;) |
- 程序加载时,系统自动清零 - 仅占内存 "地址空间",不占可执行文件体积 |
.data |
存放显式初始化的全局 / 静态变量 (如 int global_var = 10;) |
- 程序加载时,从可执行文件中加载初始值 - 运行时可修改,属于 "动态数据" |
.Stack(栈区) |
存放函数调用的临时数据(局部变量、函数参数、返回地址等) | - 自动管理:函数调用时分配,返回时释放 - 空间小、速度快,遵循 "后进先出(LIFO)" |
.Heap(堆区) |
存放动态分配的内存 (如 C malloc、C++ new 申请的内存) |
- 手动管理:程序员控制分配(malloc)和释放(free) - 空间灵活,按需扩容(但需注意内存泄漏) |
.Reg(寄存器区域) |
处理器最核心的临时存储(存放运算中间值、指令指针、函数调用上下文) | - 速度极快(比 RAM 快数倍) - 数量少(几十个通用寄存器),硬件 / 编译器自动调度 |
3. 程序运行流程(pflash ↔ ram 协同)
-
启动阶段 :程序从
pflash的.text段开始执行(CPU 读取指令),rodata提供只读常量支持。 -
运行阶段 :
- 全局变量(
.bss/.data)、局部变量(.Stack)、动态内存(.Heap)在ram中动态分配 / 使用; - 寄存器(
.Reg)作为 "高速中转站",衔接 CPU 运算与内存数据,加速程序执行。
- 全局变量(
-
关键设计逻辑
- "静态存储(pflash) + 动态存储(ram)" 分离:既保证程序 / 常量的 "持久化"(断电不丢),又满足运行时数据的 "灵活性"(可读写、动态分配)。
- 不同分区的 "管理方式" 差异 :
.Stack自动管理(简单但空间有限)、.Heap手动管理(灵活但需警惕泄漏)、.bss/.data关联全局变量生命周期。