STM32+BMS 嵌入式开发:ADC 电压温度采集与总线通信落地教程

1. 引言:BMS 与嵌入式开发的结合

电池管理系统(BMS)是电动汽车、储能系统、无人机等设备的核心安全与性能保障单元。其核心任务之一是实时、精确地采集电池组中各个电芯的电压与温度,并通过可靠的总线将数据上报给主控制器。STM32 系列微控制器凭借其丰富的外设、出色的性能与完善的生态,成为实现 BMS 从机(或称"子板"、"采集板")的理想选择。

本教程将手把手带您完成一个基于 STM32 的 BMS 数据采集与通信模块的软硬件开发全流程,重点涵盖:

  1. 硬件设计要点:ADC 采样电路、温度传感器电路、隔离与保护。
  2. 软件驱动开发:STM32CubeMX 配置、ADC 多通道扫描与 DMA 传输、温度传感器读取。
  3. 通信协议实现:基于 CAN 总线或 UART 的定制通信协议设计。
  4. 系统集成与测试:数据打包、校验、发送与上位机解析。

通过本篇教程,您将获得一个可直接用于项目原型的完整解决方案。

2. 硬件系统架构与关键电路设计

2.1 整体系统框图

一个典型的 BMS 从机模块包含以下核心部分:

复制代码
[STM32 MCU] <-(SPI/I2C)-> [AFE 或 多路ADC]
                          |
                          V
                    [电压采样网络] ----> [电池电芯]
                    [温度传感器(NTC/PTC)] ----> [电芯或铜排]
  • MCU:STM32F103/F407 等系列,需具备足够数量的 ADC 通道和 CAN 外设。
  • 模拟前端 (AFE):对于高串数电池包,常采用专用 AFE 芯片(如 TI BQ76PL455A)进行电压采集,通过 SPI 与 MCU 通信。本教程为简化,直接使用 STM32 内部 ADC。
  • 采样电路:由精密电阻分压网络、滤波电容及保护器件(TVS、保险丝)组成。
  • 温度传感器:通常采用 NTC 热敏电阻,贴于电芯表面或 Busbar 上。
  • 通信接口:CAN 收发器(如 TJA1050)用于组网通信,或 UART 转隔离模块用于点对点通信。

2.2 ADC 电压采样电路设计

对于单节电芯(标称 3.6V),电压采样电路相对简单。关键设计在于 精度安全抗干扰

电路示例与计算:

假设电芯电压最高为 V_cell_max = 4.2V,STM32 ADC 参考电压 V_ref = 3.3V

为将 4.2V 分压至 3.3V 以内,并留有余量,设定分压比 K = R2 / (R1 + R2) = 3.0 / 4.2 ≈ 0.714

选取 R1 = 10kΩ,则 R2 = R1 * K / (1 - K) ≈ 10k * 0.714 / 0.286 ≈ 24.9kΩ,可取标称值 24.9kΩ25kΩ

实际电压计算公式:

V_cell = ADC_Value * (V_ref / 4095) * ( (R1 + R2) / R2 )

= ADC_Value * (3.3 / 4095) * (34.9k / 24.9k)

≈ ADC_Value * 0.000805 * 1.402 ≈ ADC_Value * 0.001128

PCB 布局要点:

  1. 分压电阻尽量靠近 ADC 输入引脚。
  2. 在 ADC 输入引脚处添加一个 0.1uF 的滤波电容到地,以滤除高频噪声。
  3. 串联一个 100Ω 的小电阻,与滤波电容构成 RC 低通滤波器,并限制注入电流。
  4. 在采样线入口处放置 TVS 管(如 SMAJ5.0A)进行过压保护。

2.3 温度采样电路设计(NTC 热敏电阻)

NTC 的阻值随温度升高而降低。我们将其与一个精密参考电阻串联,构成分压电路,MCU 测量分压点电压。

电路示例:

VCC(3.3V) --- [R_ref (10kΩ)] --- [ADC_IN] --- [NTC] --- GND

温度计算流程:

  1. 读取电压V_ntc = ADC_Value * (V_ref / 4095)
  2. 计算 NTC 电阻R_ntc = R_ref * (V_ntc) / (VCC - V_ntc)
  3. 查表或公式计算温度 :通常使用 Steinhart-Hart 方程 或其简化版(B 值公式)将电阻值转换为温度值。

3. 软件驱动开发:STM32CubeMX 与 HAL 库

3.1 工程创建与外设配置

  1. 打开 STM32CubeMX,选择您的目标 MCU。
  2. 配置 ADC
    • 启用 ADC1(或 ADC2, ADC3)。
    • 工作模式选择 "Scan Conversion Mode""Continuous Conversion Mode"
    • Parameter Settings 中,设置合适的采样时间(如 239.5 Cycles 以提高精度)。
    • DMA Settings 中,添加 DMA 请求,模式设为 "Circular" ,数据宽度都为 Word
    • Pinout View 中,将需要用到的 ADC 通道对应的 GPIO 引脚配置为 Analog 模式。
  3. 配置 CAN (如果使用):
    • 启用 CAN1
    • 模式选择 Normal
    • 配置波特率(如 500kbps)。Time QuantaPrescaler 需要根据时钟计算。
    • 自动重传 Enable
  4. 配置 UART (如果使用):启用相应 USART,配置波特率(如 115200),数据位 8,停止位 1,无校验。
  5. 生成代码:指定工程路径和 IDE(Keil/IAR/STM32CubeIDE),生成初始化代码。

3.2 ADC 多通道 DMA 采集代码实现

在生成的工程中,我们需要启动 ADC 的 DMA 采集,并处理数据。

c 复制代码
// 在 main.c 的 /* USER CODE BEGIN PV */ 区域定义变量
#define ADC_CHANNEL_NUM 4 // 假设采集4个电芯电压
uint32_t adc_buffer[ADC_CHANNEL_NUM]; // DMA 搬运的目标数组

// 在 /* USER CODE BEGIN 2 */ 中启动 ADC
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_CHANNEL_NUM);

// 在 /* USER CODE BEGIN 4 */ 中编写 DMA 传输完成中断回调函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if (hadc->Instance == ADC1) {
        // adc_buffer 中已按配置顺序更新了所有通道的数据
        // 可以在此处设置一个标志位,通知主循环处理数据
        adc_data_ready_flag = 1;
    }
}

// 主循环中处理数据
if (adc_data_ready_flag) {
    adc_data_ready_flag = 0;
    for (int i = 0; i < ADC_CHANNEL_NUM; i++) {
        // 将原始 ADC 值转换为电压值
        cell_voltage[i] = adc_buffer[i] * 3.3f / 4095.0f * VOLTAGE_DIVIDER_RATIO;
        // 可以加入滤波算法,如滑动平均滤波
    }
}

3.3 温度传感器读取与计算

c 复制代码
// 假设 NTC 接在 ADC Channel 5
float read_temperature(void) {
    uint32_t adc_value;
    float voltage, resistance, temperature;
    // 1. 读取 ADC 值 (可以使用单次转换或从 DMA 缓冲区获取)
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
    adc_value = HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);

    // 2. 计算电压和电阻
    voltage = adc_value * 3.3f / 4095.0f;
    resistance = REF_RESISTOR * voltage / (3.3f - voltage); // 10k 上拉

    // 3. 使用 B 值公式计算温度 (简化版,需根据具体 NTC 参数调整)
    // T = 1 / (1/T0 + 1/B * ln(R/R0))
    // T0=25°C=298.15K, R0 为 25°C 时的阻值,B 为 B 值常数
    float T0 = 298.15;
    float R0 = 10000.0; // 10k NTC
    float B = 3950.0;
    temperature = 1.0 / (1.0/T0 + log(resistance/R0)/B);
    temperature = temperature - 273.15; // 开尔文转摄氏度

    return temperature;
}

4. 通信协议设计与实现

4.1 基于 CAN 总线的通信协议

CAN 总线具有高可靠性、多主、抗干扰强等特点,非常适合 BMS 网络。

定义报文 ID 与数据结构:

假设我们使用 标准帧(11位 ID)

  • ID 分配 :高 4 位表示帧类型,低 7 位表示从机地址。

    • 0x0X0: 心跳/状态帧 (主机 -> 从机)
    • 0x0X1: 数据请求帧 (主机 -> 从机)
    • 0x1X0: 电压温度数据帧 (从机 -> 主机)
    • 0x1X1: 报警帧 (从机 -> 主机)
  • 数据帧示例(0x1X0)

    复制代码
    Byte0-1: 电芯1电压 (uint16_t, 单位 0.1mV)
    Byte2-3: 电芯2电压
    Byte4:   温度1 (int8_t, 单位 °C)
    Byte5:   温度2
    Byte6:   CRC8 校验 (从 Byte0 到 Byte5)
    Byte7:   帧尾 (0xAA)

代码实现(发送数据):

c 复制代码
void send_cell_data_via_can(uint8_t slave_addr) {
    CAN_TxHeaderTypeDef tx_header;
    uint8_t tx_data[8];
    uint32_t tx_mailbox;

    tx_header.StdId = 0x100 | slave_addr; // 组合成完整 ID
    tx_header.ExtId = 0;
    tx_header.RTR = CAN_RTR_DATA;
    tx_header.IDE = CAN_ID_STD;
    tx_header.DLC = 8; // 数据长度 8 字节
    tx_header.TransmitGlobalTime = DISABLE;

    // 填充数据
    uint16_t vol1 = (uint16_t)(cell_voltage[0] * 10000); // 转换为 0.1mV
    tx_data[0] = (vol1 >> 8) & 0xFF;
    tx_data[1] = vol1 & 0xFF;
    // ... 填充其他电压温度数据
    tx_data[6] = calculate_crc8(tx_data, 6); // 计算 CRC
    tx_data[7] = 0xAA;

    if (HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox) != HAL_OK) {
        // 发送失败处理
    }
}

4.2 基于 UART 的通信协议

对于点对点或简单系统,UART 也是常用选择。需要定义简单的帧结构来保证数据的完整性。

自定义帧格式:

[帧头 0xAA] [长度] [命令字] [数据...] [CRC] [帧尾 0x55]

代码实现(发送数据):

c 复制代码
void send_cell_data_via_uart(void) {
    uint8_t tx_buffer[32];
    uint8_t idx = 0;
    tx_buffer[idx++] = 0xAA; // 帧头
    uint8_t data_len = 1 + 2*4 + 1*2; // 命令字 + 4个电压(uint16) + 2个温度(int8)
    tx_buffer[idx++] = data_len;
    tx_buffer[idx++] = 0x01; // 命令字:上报数据

    for(int i=0; i<4; i++) {
        uint16_t vol = (uint16_t)(cell_voltage[i] * 1000); // 转换为 mV
        tx_buffer[idx++] = (vol >> 8) & 0xFF;
        tx_buffer[idx++] = vol & 0xFF;
    }
    for(int i=0; i<2; i++) {
        tx_buffer[idx++] = (uint8_t)(temperature[i]); // 温度,假设在 -40~125°C 范围内
    }

    uint8_t crc = calculate_crc8(&tx_buffer[1], idx-1); // 从长度开始计算
    tx_buffer[idx++] = crc;
    tx_buffer[idx++] = 0x55; // 帧尾

    HAL_UART_Transmit(&huart1, tx_buffer, idx, 100);
}

5. 系统集成、测试与优化

5.1 数据打包与定时发送

在主循环或定时器中断中,周期性地执行采集、处理、发送流程。

c 复制代码
// 在 main.c 的 while(1) 循环中
while (1) {
    // 1. 检查 ADC 数据就绪标志
    if (adc_data_ready_flag) {
        process_adc_data(); // 处理电压数据
        read_temperatures(); // 读取温度
        adc_data_ready_flag = 0;
    }

    // 2. 定时发送 (例如每 100ms)
    if (HAL_GetTick() - last_send_tick > 100) {
        send_cell_data_via_can(SLAVE_ADDR); // 或 via UART
        last_send_tick = HAL_GetTick();
    }

    // 3. 处理接收到的命令(如休眠、校准指令)
    check_and_handle_rx_command();

    HAL_Delay(1);
}

5.2 上位机测试(以 CAN 为例)

可以使用 USB-CAN 适配器 配合 PCAN-ViewZLG USBCAN 配套软件或 Python-can 库进行测试。

  1. 连接硬件,上电。
  2. 在上位机软件中设置相同的 CAN 波特率。
  3. 监听 ID 为 0x1X0 的报文,检查数据解析是否正确。
  4. 可以发送请求帧 (0x0X1),测试从机的响应功能。

5.3 抗干扰与可靠性优化

  1. 软件滤波 :对 ADC 采样值进行 滑动平均滤波中值滤波,消除尖峰噪声。
  2. 硬件看门狗:启用 STM32 的独立看门狗(IWDG),防止程序跑飞。
  3. 通信重发机制:在 CAN/UART 发送失败后,加入重试逻辑。
  4. 数据校验 :务必使用 CRC 校验,确保数据传输的完整性。
  5. 隔离设计 :在采样电路与 MCU 之间、通信接口处使用 光耦数字隔离器,提高系统抗共模干扰能力和安全性。

6. 总结

本教程详细介绍了从硬件选型、电路设计到软件驱动、通信协议实现的完整流程。STM32 HAL 库大大降低了开发门槛,而清晰的协议设计是系统稳定运行的关键。希望这篇教程能成为您进入 STM32 BMS 开发领域的坚实基石。在实际项目中,请务必根据具体的电池参数、安全法规和系统架构进行深入设计与充分测试。

相关推荐
szxinmai主板定制专家2 小时前
Zynq MPSoC半导体高精度运动控制方案|FPGA硬件插补实现亚微米级伺服实时控制
人工智能·嵌入式硬件·fpga开发·zynq·运动控制·ethercat
Lumos6fly11 小时前
《嵌入式通讯协议栈实战》 5 AT24C02与OLED
stm32·单片机
天吾cc13 小时前
RTT-IO设备模型
单片机·嵌入式硬件·学习
wengqidaifeng17 小时前
2026 年电赛(TI 杯)H 题:MaxiCamPro、双主控通信、VOFA+ 遥测与可复现实验方法
c语言·单片机·嵌入式硬件
寻道模式18 小时前
【运维心得】电视KK.RV22.E32B“捉鬼”实录:从炸机到 eMMC 的千里追击(1)
单片机·嵌入式硬件·mos管·开关电源
科芯创展19 小时前
XZ6206,6.5V,300mA稳压LDO芯片
单片机
科芯创展20 小时前
XC6206,8V,250mA稳压LDO芯片
嵌入式硬件
平凡的小蚂蚁20 小时前
Exynos4412-POP-7inLCD裸机开发完整教程(STM32 库风格)
驱动开发·stm32·单片机·嵌入式硬件·嵌入式·硬件工程
破晓单片机20 小时前
096、STM32项目分享开源:蓝牙智能手环
stm32·单片机·嵌入式硬件·开源