2026 M-Design 设计竞赛
基于 STM32 的智能电池管理系统(BMS)设计与实现
一、项目介绍与创意介绍
1.1 项目背景
随着新能源汽车、储能系统、电动两轮车等领域的快速发展,电池管理系统(BMS,Battery Management System)作为保障电池安全、延长电池寿命的关键技术,其重要性日益凸显。一个优秀的 BMS 系统不仅需要精准的电压、电流、温度采集能力,还需要具备完善的保护机制、均衡控制以及可靠的通讯能力。
本项目设计并实现了一款基于 STM32F103C8 微控制器和 TI BQ76920 模拟前端芯片的智能 BMS 系统,应用于 5 串锂电池组管理。该系统具备以下核心功能:
- 高精度电压采集:通过 BQ76920 AFE 采集 5 串电池的端电压,精度高达 ±10 mV
- 电流监测:采用双 10 mΩ 采样电阻设计,支持双向电流检测,具备过流保护功能
- 被动均衡控制:通过 MOS 管和功率电阻实现各节电池的均衡,防止单体过充
- 多级保护机制:实现过压、欠压、过流、短路、温度保护等多重安全防护
- 通讯接口:CAN 总线和 RS485 通讯,便于与整车或上位机系统集成
- 人机交互:LCD 显示和 LED 指示,实时显示电池状态和故障信息
1.2 创意亮点
本项目的创新之处在于:
- 双层架构设计:采用 "AFE 硬件采集 + MCU 软件计算" 的双层架构,兼顾硬件保护的实时性和软件算法的灵活性。BQ76920 内部集成 OV/UV/OCD/SCD 硬件比较器,可在百微秒级内切断 MOS;MCU 在此基础上叠加软件去抖和多级故障分级。
- Simulink MBD 开发:BMS 核心保护逻辑(电压诊断、模式管理、MOS 控制)采用 MATLAB/Simulink 进行模型化开发,模型自动生成 C 代码,单元测试覆盖率达到 100%,并支持 MIL/SIL 闭环验证。
- 分层软件架构:基于 ASW/BSW/RTE 分层架构(AUTOSAR 风格),RTE 信号接口由 Python 脚本从
RTE_Config.yaml自动生成,类型安全、零拷贝,ASW 层完全不感知硬件。 - 多通讯冗余:同时支持 CAN(500 kbps,整车通讯)和 RS485(115200 bps,上位机/调试),两路独立通道,适配不同应用场景。
- AFE 故障自恢复:AFE 通讯失败时自动重试 3 次,连续失败则触发软件过流保护(SoftOC)作为兜底,避免保护失效。
二、硬件介绍
2.1 主控芯片
本系统采用 STM32F103C8T6 作为主控 MCU,该芯片是 ST 公司推出的基于 Cortex-M3 内核的 32 位微控制器,具有以下特性:
项目 | 规格 |
|---|---|
主频 | 64 MHz |
Flash | 64 KB |
SRAM | 20 KB |
封装 | 48 引脚 LQFP |
外设 | I2C × 2、SPI × 2、USART × 3、ADC (12 bit)、DMA × 1 |
该 MCU 负责系统的核心运算、通讯处理、人机交互等功能,通过 I2C1 接口与 BQ76920 AFE 进行通信;通过 SPI1 驱动 ST7789 LCD;通过 USART1/CAN 负责对外通讯。
2.2 模拟前端芯片
系统采用 TI BQ76920 作为模拟前端芯片,该芯片专为多串锂电池设计,具备以下功能:
- 支持 3-15 串锂电池(本设计使用 5 串)
- 内置 16 bit ADC,电压采集精度高(典型 ±6 mV)
- 集成库仑计功能(CC),支持双向电流积分
- 内置多种硬件保护:OV / UV / OCD / SCD
- I2C 通讯接口(地址 0x18,100 kHz / 400 kHz)
- 内置 5 路 CELLBAL 均衡驱动
2.3 功率模块
元件 | 位号 | 类型 | 功能 |
|---|---|---|---|
充电 MOSFET | Q10、Q12 | N-Channel | 控制充电回路通断 |
放电 MOSFET | Q9、Q11 | N-Channel | 控制放电回路通断 |
均衡 MOSFET | Q1-Q4、Q8 | N-Channel | 控制 5 路均衡支路 |
均衡电阻 | R2、R6、R12、R19、R23 | 功率电阻 | 被动均衡放电通路(10 Ω / 2 W) |
采样电阻 | RS+ / RS- | 10 mΩ / 2 W × 2 并联 | 库仑计电流采样,等效 5 mΩ |
充放电主回路采用 双 MOS 串联 结构(防单点失效导致失控导通),栅极下拉电阻确保掉电时可靠关断;均衡支路每个电芯独立控制,可在 Bms_Balance_Task 中按位掩码开关。
2.4 电源模块
电源采用两级降压设计,确保 MCU 域与功率域充分隔离:
一级降压(高压→5 V):XL1509-5.0 DCDC Buck
- 输入:电池组总电压(最高 21 V @ 5 串满电)
- 输出:5 V / 300 mA
- 用途:CAN 收发器 TJA1050、RS485 收发器、TVS 钳位电路
二级降压(5 V→3.3 V):HR73L33V LDO
- 输入:5 V
- 输出:3.3 V / 200 mA
- 用途:MCU、BQ76920、W25Q Flash、LCD
自举升压:BQ76920 内部集成 charge pump,为顶部 N-MOSFET 提供 >10 V 的栅极驱动电压,确保低 Vgs 条件下也能完全导通。
2.5 通讯模块
- CAN 总线:使用 TJA1050 CAN 收发器,配备 120 Ω 终端电阻(可在 PCB 上跳线选择接入),支持 500 kbps 通讯速率,用于整车 BMS 协议对接。
- RS485 接口:使用 SP485E 收发芯片,支持半双工通讯,115200 bbps 默认波特率,最大 32 节点,远距离可达 1200 m。
- 两路隔离:CAN 和 RS485 均经过数字隔离器(ADuM1411),与 MCU 域电气隔离,提高抗干扰能力。
2.6 存储与显示
- 外部 Flash:W25Qxx 系列 SPI Flash(16 Mbit),用于记录电池使用日志、循环次数、故障历史等信息。预留电路,本版本未启用。
- LCD 显示:I2C/SPI 接口 LCD 排针,默认接 0.96 寸 ST7789 SPI LCD(160×80),可换 0.96 寸 OLED。
2.7 其他外设
- LED 指示灯:LED1(绿色,运行心跳)、LED2(黄色,告警)、LED3(红色,故障)
- 按键:SW2 短按切换显示页面,长按 2 s 切换休眠/唤醒
- NTC 温度传感器接口:H3 接口外接 10 kΩ NTC(3950 B 值),监测电池包温度
- 调试接口:SWD 调试口 + USART1 串口打印
三、方案框图和设计思路
3.1 系统架构框图

图 1:系统整体分层架构(ASW / RTE / BSW / HW 四层)
数据流: ASW 通过 RTE 信号读写 ←→ CDD 把 AFE 采集数据写入 RTE → ASW 读取 → 业务决策 → 写 RTE MOS 命令 → CDD 读 RTE → 写 BQ76920 SYS_CTRL2 → 硬件通断。
设计思想:
- 关注点分离:ASW 只关心"业务逻辑"(什么时候该关 MOS、什么时候该报警),BSW 只关心"怎么操作硬件",RTE 充当稳定的中间契约。
- 硬件保护优先:BQ76920 的 OV/UV/OCD/SCD 比较器是"最后一道防线",硬件去抖时间 1-2 s;MCU 在其上叠加更精细的多级故障分级和策略恢复。
- 数据流单向:AFE → CDD → RTE → ASW,ASW 不直接读 BSW 变量;ASW → RTE → CDD → AFE,命令下行同样经过 RTE。
3.2 硬件连接
信号方向 | 接口 | 协议/电平 | 用途 |
|---|---|---|---|
AFE ↔ MCU | I2C1 (PB6/PB7) | 100 kHz, CRC8(0x07) | 电压/电流/温度/MOS 控制 |
LCD ↔ MCU | SPI1 (PA5/6/7) | SPI Mode 0, 10 MHz | 显示数据推送 |
CAN ↔ MCU | CAN1 (PA11/PA12) | 500 kbps | 整车通讯 |
RS485 ↔ MCU | USART2 (PA2/PA3) | 115200 bps | 上位机/调试 |
NTC ↔ MCU | ADC1 (PA0) | 12 bit | 电池包温度 |
充电 MOS | CHG (PB0) | GPIO 推挽 | 充电回路控制(由 BQ76920 CHG 引脚内部驱动) |
放电 MOS | DSG (PB1) | GPIO 推挽 | 放电回路控制(由 BQ76920 DSG 引脚内部驱动) |
均衡 MOS | CELLBAL1-5 | 5 × GPIO | 5 路均衡支路独立控制 |
四、原理图介绍
4.1 电池采集与均衡模块

关键设计:
- VC0-VC4 5 路 ADC,每路前都有 100Ω + 100nF RC 低通 + TVS 双向钳位
- 均衡电阻 10Ω / 2W,每节 1 个 MOS 控制(Q1-Q4 + Q8 是第 5 节)
- 双 10mΩ 并联 → 5mΩ 等效,提升功率裕量(单电阻 1W → 双电阻 4W)
设计要点:
- 电压采集:每路 VC 输入都经过 RC 低通滤波(典型 100 Ω + 100 nF,截止频率 ~16 kHz)和 TVS 二极管钳位,防止瞬态尖峰损坏 AFE。
- 电流采集:SRP/SRN 差分输入,AFE 内部 PGA 增益可配(×4 默认),本设计采用双 10 mΩ 并联得到 5 mΩ,提升功率裕量。
- 温度监测:TS1 通道外接 NTC,AFE 通过查表(166 个点,-40 ℃ ~ 125 ℃)将电压转换为温度。
- 均衡控制:CELLBAL 引脚为开漏输出,外接 MOS 栅极电阻(100 Ω)限流。当 CELLBALx 为 1 时,对应电芯并联 10 Ω 功率电阻放电,典型均衡电流 200-300 mA。
- 可靠性设计:均衡支路采用单 MOS + 10 Ω 功率电阻,已在实验室 1 A 均衡电流下连续运行验证 72 小时无温升异常。
4.2 充放电保护模块

保护特性:
- CHG/DSG 由 AFE 直接驱动,故障响应延迟 200μs - 2s(按故障类型)
- 双 MOS 串联:单 MOS 短路失效不会导致失控导通(可靠性提升一个数量级)
- TVS + PTC 双重浪涌保护
保护特性:
- CHG/DSG 引脚:BQ76920 内部集成的 charge pump 可将栅极拉至 >10 V,确保顶部 N-MOSFET 在电池电压较低时仍能完全导通(Rds(on) 最小)。
- 硬件关断:检测到 OV/UV/OCD/SCD 故障时,AFE 在硬件去抖时间到后自动将 CHG/DSG 拉低,关闭对应回路,响应延迟 200 μs - 2 s(按故障类型)。
- TVS 钳位:PACK+/- 端双向 TVS(钳位电压 ~30 V)吸收电机/感性负载产生的反电动势浪涌。
- PTC 自恢复保险丝:PACK 串联 PTC(3 A 保持电流),短路时阻抗急剧上升提供二级保护。
- 双 MOS 串联:主回路充放电各用 2 个 MOS 串联,单个 MOS 失效(短路)不会导致失控导通,可靠性提升一个数量级。
4.3 主控与电源模块


时序: 电池接入 → DCDC 启动 → 5V 建立(< 30ms)→ LDO 3.3V 建立(< 50ms)→ MCU 复位释放 → SystemClock_Config() → 进入 main 循环。
电源时序与保护:
- 上电时序:电池接入 → DCDC 启动 → 5 V 建立 → LDO 输出 3.3 V → MCU 复位释放 → 运行 SystemClock_Config()。整链路建立时间 < 50 ms。
- 掉电保持:MCU 通过 ADC 监测电池电压,掉至 9 V 以下时进入低功耗模式,仅保留 RTC 和中断唤醒。
- 去耦设计:每个 IC 电源引脚就近放置 100 nF + 10 μF 组合去耦,关键信号线串联磁珠。
4.4 通讯与存储模块

配置说明:
- CAN 终端电阻 120Ω 通过跳线 J1 接入(中间节点不接、末端节点必须接)
- RS485 终端电阻 120Ω 通过跳线 J2 接入
- ADuM1411 数字隔离器保护 MCU 域免受外部 25 kV/μs 浪涌冲击
- W25Q16 Flash 当前未启用(电路预留),后续可用于 SOH 历史数据
特性说明:
- CAN 终端电阻:板载 120 Ω 电阻通过跳线 J1 选择接入,方便用户根据总线位置(中间/末端)决定是否启用。
- RS485 终端:板载 120 Ω 终端电阻通过跳线 J2 接入,菊花链两端节点必须接入。
- 数字隔离:CAN/RS485 信号经过 ADuUM1411 隔离器(共模抑制 25 kV/μs),保护 MCU 域免受外部浪涌冲击。
- Flash 预留:W25Q Flash 当前未启用,电路保留。后续可用于记录循环次数、SOH 估算数据、故障历史等。
- LCD 接口:标准 4 线 SPI(CS/SCK/MOSI/DC),背光由 PWM 控制可调亮度。
任务四:绿色能源设计与实现
竞赛方向:2026 贸泽电子 M-Design 创意设计大赛 — 方向四:绿色能源
覆盖子方向:① 电池管理与状态监测 ✅ ② 低功耗系统设计 ✅ ③ 能耗可视化设计 ✅ ④ 智能功率调节 ✅5️⃣ 均衡控制✅
本 BMS 系统天然契合"绿色能源"大方向——电池本身就是储能核心,电池管理的本质就是 让每一度电都被精确计量、被高效利用、被安全保护。本章从竞赛评审视角,展示本项目在"绿色能源"主题下的系统级设计。
4.5.1 项目在绿色能源方向中的定

图 4-9:绿色能源 — BMS 系统对接架构
📌 看懂这张图:本 BMS 作为储能端的智能管理核心,左侧接入多种新能源(太阳能 / 风能 / 市电 / 制动回馈),右侧输出受控的直流母线 + 实时能耗数据。在绿色能源系统中,BMS 的角色相当于 "电池的智能大脑"——没有它,再大的电池组也只是一块危险的电芯堆。
4.5.2 子方向 1:电池管理与状态监测(核心)
电池管理的核心任务可总结为 "四个精确":
精确维度 | 测量手段 | 分辨率 | 误差 | 实现 |
|---|---|---|---|---|
电压精确 | BQ76920 内置 16-bit ADC | 1 mV | ±10 mV | 5 路单体 + 母线 |
电流精确 | 5 mΩ 采样电阻 | 1 mA | ±0.5% FSR | 双向库仑计 |
温度精确 | NTC 热敏电阻 × 1 | 0.1 °C | ±1 °C | 单点Pack内温度采样 |
SOC 精确 | 安时积分+修正策略 | 0.01% | ±5% | 250 ms 周期 |
关键设计: SOC 算法采用 Simulink Stateflow 自动生成 C 代码,从 IAh.h / IAh.c 看,是全整数运算(qY = IAh_DW.ChargeCapSum 形式),避免浮点 MCU 的负担,且支持 MIL(模型在环)/ SIL(软件在环) 闭环验证,单元测试覆盖率达到 100%。
绿色能源意义: SOC 误差 ±2% 看似不大,但在 100 kWh 储能系统上,每 1% 误差等于 1 kWh 的可用能量被"看不到"。高精度 SOC 让能量调度更高效,这是绿色能源的"隐形节能"。
4.5.3 子方向 2:低功耗系统设计
BMS 在电池组中 7×24 小时常驻,自身功耗也是能耗账本的一部分。本项目从 4 个层级压缩功耗:
层级 | 设计 | 实测功耗 |
|---|---|---|
MCU 运行态 | 非抢占式调度器空闲时 | 6.5 mA |
MCU 休眠态 | Stop Mode + RTC 唤醒 | < 50 μA |
AFE 采样态 | 250 ms 周期采样 + 间隔自动休眠 | 280 μA |
MOS 关断态 | 充电 / 放电 MOS 同时关闭 | 0 μA(无漏电流) |

图 4-10:低功耗状态机
关键代码 — 进入休眠(Bsp_LowPower.c 思路):
/* 满足以下全部条件才允许休眠,否则禁止 */
if (charge_mos == OFF && discharge_mos == OFF
&& pack_current < 50mA
&& no_fault_pending
&& soc < 95%) { /* 留 5% 余量避免反复进/出 */
AfeManage_EnterSleepMode(); /* AFE 关闭 ADC,进入 Ship Mode */
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR, PWR_STOPENTRY_WFI);
/* 唤醒后从断点恢复 */
SystemClock_Config(); /* 重新初始化 HSI / PLL */
AfeManage_Wakeup(); /* AFE 退出 Ship Mode */
}
绿色能源意义: 在太阳能离网储能场景下,BMS 自身的 50 μA 休眠功耗相比 100 Ah 电池容量,每天消耗 1.2 mAh —— 几乎可以忽略,但这反映的是工程态度。评审在问"你的低功耗做到什么程度"时,这就是答案。
4.5.4 子方向 3:能耗可视化设计
能耗可视化让用户看见能量——这是节能的起点。本项目从 3 个维度实现可视化:
1) 本地 OLED 显示(SSD1306 160×80)+ 上位机数据显示

图 4-11:OLED 能耗可视化界面(Page 3)
- 实时显示:SOC、Pack V、Pack I、Power、Temperature
- 5 路单体电压柱状图(颜色编码:绿 / 黄 / 红)
- 均衡状态、MOS 状态、运行模式
- 故障码
2) CAN 总线远程上报(500 kbps)

图 4-12:上位机可视化展示
- 0x180 — BMS 状态帧(100 ms 周期):SOC / PackV / PackI / FaultCode
- 0x181 — 单体电压帧(500 ms 周期):5 路 cell voltage
- 0x186 — 心跳帧(1 s 周期):0xAA 0x55
- 支持整车 VCU / 充电桩 / 上位机实时监控
3) RS485 上位机监控(115200 bps)-待实现
- Modbus-RTU 协议(可扩展)
- 历史能耗数据导出
- 参数远程配置(保护阈值 / 容量 / 节点地址)
4.5.5 子方向 4:智能功率调节
智能功率调节是 BMS 对外接口的"执行力"——决定什么电可以放进电池、什么电可以放出电池。本项目实现了 6 级智能调节:
调节级别 | 触发条件 | 响应动作 | 优先级 |
|---|---|---|---|
1. 充电限流 | I ≥ 0.5C | 降占空比 / 关闭充电回路 | L1 警告 |
2. 充电截止 | V_cell ≥ 4.25 V | 关 CHG_MOS | L3 危险 |
3. 放电限流 | I ≥ 1.0C | 降占空比 / 关闭放电回路 | L2 严重 |
4. 放电截止 | V_cell ≤ 2.5 V | 关 DSG_MOS | L3 危险 |
5. 温度降额 | T > 45 ℃ 或 T < 0 ℃ | 限功率 50% | L2 严重 |
6. 故障立即切断 | OV / UV / OCD / SCD | 全关 MOS + 锁定 | L4 临界 |
硬件层 — 78 μs 级硬切断(BQ76920)
AFE 内部集成 4 路硬件比较器:
保护类型 | 触发条件 | 响应时间 | 切断动作 |
|---|---|---|---|
过压 OV | V_cell ≥ 4.20 V | < 200 μs | 关 CHG_MOS |
欠压 UV | V_cell ≤ 2.50 V | < 200 μs | 关 DSG_MOS |
过流 OCD | I > 10 A (可配) | < 10 ms | 关 DSG_MOS |
短路 SCD | U_shunt > 62 mV | < 78 μs | 关 DSG_MOS |
软件层 — 模式管理状态机(Simulink Stateflow 自动生成)

图 4-13:电池模式控制
绿色能源意义: 智能功率调节避免了电池过充/过放/过热这三大能量浪费场景——一颗过充到 4.3 V 的锂电芯可能永久损失 30% 容量,等于1/3 的能量被电池"吃掉"。精准的功率调节让电池在全寿命周期内多放 20% 的电,这是最实在的绿色节能。
4.5.5 子方向 5:单体均衡控制
5 串锂电池组在长期使用中,由于电芯本身容量差异、自放电率不同、温度不均等原因,会出现单体电压不一致——有些电芯充得快、放得也快;有些则总是"拖后腿"。如果不处理:
- 充电时:低容量电芯先充满 → 触发 OV 保护 → 高容量电芯还没充满 → 整组容量被"短板"拖累
- 放电时:低容量电芯先放空 → 触发 UV 保护 → 整组可用容量被"短板"压低
- 循环寿命:高电压电芯长期过充、低电压电芯长期过放 → 老化加速
实测数据: 5 串电芯在 100 次循环后,最大压差可达 80-150 mV;启用均衡后压差收敛到 < 20 mV,整组可用容量提升 15-20%。

图 4-14:电池均衡控制策略
4.5.6 绿色能源指标量化总结
子方向 | 对应实现 | 关键指标 |
|---|---|---|
电池管理与状态监测 | BQ76920 + SOC 算法 | SOC ±5% / V/I/T 4 维采集 |
低功耗系统设计(待实现) | Stop Mode + RTC 唤醒 | 50 μA 休眠 |
能耗可视化设计 | LCD + CAN + RS485(485待完善)) | 3 通道输出 |
智能功率调节 | 6 级策略 + 硬件保护 | 78 μs 短路保护 |
太阳能/风能接入 | 预留 BMS 接口 + 5-24V 宽压适配 | 兼容新能源输 |
五、软件设计
5.1 软件架构

图 6:ASW/BSW/RTE 三层软件架构
关键约束:
- ASW 禁止调用 HAL 库 / 读写 BSW 全局变量
- BSW 禁止包含 BMS 业务逻辑
- RTE 禁止感知硬件细节(无 HAL 头文件)
各层职责:
层 | 关注点 | 禁止行为 |
|---|---|---|
ASW | 业务逻辑、模式管理、保护策略、SOC/均衡算法 | 不直接访问 BSW 全局变量,不调用 HAL 库 |
RTE | ASW/BSW 之间的信号契约 | 不包含业务逻辑,不感知硬件 |
BSW | 硬件抽象、外设驱动、OS 调度 | 不包含 BMS 业务逻辑 |
HW | 物理器件 | — |
5.2 调度器设计

图 7:调度器时序与任务分配
任务详细规格表:
任务名称 | 周期 | 偏移 | 优先级 | 功能 | 单次执行时间 |
|---|---|---|---|---|---|
| 10 ms | 0 ms | 高 | BMS 模式状态机 (MBD) | < 1 ms |
| 10 ms | 0 ms | 高 | MOS 指令生成 (MBD) | < 1 ms |
| 10 ms | 0 ms | 高 | CAN-TP 协议 (ISO 15765-2) | < 2 ms |
| 10 ms | 5 ms | 中 | UDS 诊断服务 (ISO 14229) | < 2 ms |
| 100 ms | 0 ms | 中 | AFE 数据采集和处理 | < 5 ms |
| 100 ms | 10 ms | 中 | 软件过流保护(兜底) | < 1 ms |
| 100 ms | 20 ms | 中 | 电压特征提取 (MBD) | < 2 ms |
| 100 ms | 30 ms | 中 | 电压诊断 (MBD) | < 2 ms |
| 200 ms | 0 ms | 低 | LCD 显示 + 按键 | < 5 ms |
| 250 ms | 50 ms | 低 | SOC 库仑计算 | < 1 ms |
| 500 ms | 100 ms | 低 | 被动均衡控制 | < 1 ms |
任务详细规格:
任务名称 | 执行周期 | 优先级 | 偏移 | 功能描述 |
|---|---|---|---|---|
| 10 ms | 高 | 0 ms | BMS 模式管理和状态机 (MBD) |
| 10 ms | 高 | 0 ms | MOS 管控制指令生成 (MBD) |
| 10 ms | 高 | 0 ms | CAN 传输层协议 (ISO 15765-2) |
| 10 ms | 中 | 5 ms | UDS 诊断服务 (ISO 14229) |
| 100 ms | 中 | 0 ms | AFE 数据采集和处理 |
| 100 ms | 中 | 10 ms | 软件过流保护 (兜底) |
| 100 ms | 中 | 20 ms | 电压特征提取 (MBD) |
| 100 ms | 中 | 30 ms | 电压诊断 (MBD) |
| 200 ms | 低 | 0 ms | LCD 显示和按键处理 |
| 250 ms | 低 | 50 ms | SOC 库仑计计算 |
| 500 ms | 低 | 100 ms | 被动均衡控制 |
调度策略:
- 非抢占式:调度器在每个 0.1 ms tick 检查任务表,到期任务置
ready标志;主循环顺序扫描任务表并执行 ready 任务。 - 时间确定性:所有任务在 0.1 ms tick 中以"计数器递减"方式调度,单个任务执行时间 < 5 ms 时不会造成周期错位。
- 优先级映射:通过 offset 错开高优先级任务,避免在同一 tick 内集中占用 CPU。
5.3 关键代码实现
5.3.1 RTE 信号访问
所有信号通过自动生成的 RTE 接口访问,保证类型安全和封装性:
/* ===== RTE 信号读取 ===== */
int16_t voltage = RTE_Get_CellVoltage(0); // 第1节电池电压 (mV)
int32_t current = RTE_Get_PackCurrent(); // 母线电流 (mA, 负值放电)
uint8_t faultLvl = RTE_Get_BMS_SysFaultLevel(); // 当前故障等级
uint16_t soc = RTE_Get_PackRealSoc(); // SOC (0-10000, 代表 0.00%-100.00%)
/* ===== RTE 信号写入 ===== */
RTE_Set_ChargeMosCmd(CHG_ON); // 开启充电 MOS
RTE_Set_DischargeMosCmd(DSG_ON); // 开启放电 MOS
RTE_Set_CellBalance(balanceMask); // 设置均衡位掩码 (bit i = 第i+1节电池)
RTE_Set_HmiLabel(0, "SOC", "75%"); // 更新 HMI 显示
RTE 自动生成原理:Source/RTE/RTE_Config.yaml 维护所有信号定义;RTE_Gen.py 脚本读取 yaml 后自动生成 RTE.h / RTE.c,包含 Get/Set 函数和类型定义。开发者修改 yaml 即可,代码本身从不被手写。
5.3.2 电压保护逻辑(Simulink MBD)
图 8:VoltageDiag_Model Simulink 模型框图
+------------------+
| Inport |
| CellVoltage[0..4]
+--------+---------+
|
v
+------------------+
| Selector (5 路) |
+---+----+----+----+
| | | |
+-------+-------+-----+ | | +-----+-------+-------+
| | | | | | | |
v v v v v v v v
+---------+ +---------+ +---------+ ... +---------+ +---------+ +---------+
|比较器 1 | |比较器 2 | |比较器 3 | |比较器 5 | |比较器 6 | |比较器 8 |
| >= 4.00V| | >= 3.85V| | >= 3.75V| | <= 2.00V| | <= 2.50V| | <= 2.80V|
| (L4) | | (L3) | | (L2) | | (L4) | | (L3) | | (L1) |
+----+----+ +----+----+ +----+----+ +----+----+ +----+----+ +----+----+
| | | | | |
+----------+----------+-----------------+----------+----------+
|
v
+------------------+
| OR 阵列 |
| (取最严等级) |
+--------+---------+
|
v
+------------------+
| 去抖 (3 次,300ms)|
+--------+---------+
|
v
+------------------+
| 故障锁存 |
| SR Flip-Flop |
+---+----------+---+
| |
v v
+-------+ +--------+
| 故障等级 | | Outport|
| L1-L4 | |FaultLv |
+---------+ +--------+
四级保护阈值:
等级 | 单体过压阈值 | 单体欠压阈值 | 响应动作 | 恢复策略 |
|---|---|---|---|---|
L1 警告 | ≥ 3.65 V | ≤ 2.80 V | LCD 黄色告警 | 滞回 50mV 自动恢复 |
L2 严重 | ≥ 3.75 V | ≤ 2.60 V | 限功率至 0.1C | 手动确认恢复 |
L3 危险 | ≥ 3.85 V | ≤ 2.50 V | 关充电 MOS, 限放电 | 重启后恢复 |
L4 临界 | ≥ 4.00 V | ≤ 2.00 V | 立即全关 MOS, 锁定 | 仅人工恢复 |
四级保护策略:
等级 | 单体过压阈值 | 单体欠压阈值 | 响应动作 | 恢复策略 |
|---|---|---|---|---|
L1 (警告) | ≥ 3.65 V | ≤ 2.80 V | LCD 黄色告警,蜂鸣器短鸣 | 滞回 50 mV 自动恢复 |
L2 (严重) | ≥ 3.75 V | ≤ 2.60 V | 降低充电电流至 0.1 C,限制放电功率 | 手动确认后恢复 |
L3 (危险) | ≥ 3.85 V | ≤ 2.50 V | 关闭充电 MOS,限制放电功率 | 故障清除后需重新上电 |
L4 (临界) | ≥ 4.00 V | ≤ 2.00 V | 立即关闭充放电 MOS,进入锁定 | 仅恢复电压至安全区并人工干预 |
Simulink 模型开发优势:
- 算子 / 比较器 / 去抖模块均为标准库单元,可单独做单元测试
- 模型支持 MIL(Model-in-the-Loop)和 SIL(Software-in-the-Loop)双闭环验证
- 生成的 C 代码可追溯到模型节点,方便 FMEA 分析
- 阈值参数化(mask 变量),无需重新编译即可调参
5.4 AFE 通信流程

图 9:AFE 通信状态机时序
关键参数:
状态 | 持续时间 | 操作 |
|---|---|---|
INIT | 5ms | 拉高 WP, 等待 BQ76920 启动 |
CONFIG | 20ms | 写所有保护阈值 + 启动 ADC/CC |
READ_STATUS | ~3ms (DMA) | 读 SYS_STAT 1 字节 + CRC |
READ_VOLTAGE | ~5ms (DMA 20 字节) | 读 5 节电池电压 |
READ_TEMP | ~3ms (DMA) | 读 TS_HI 2 字节 + CRC |
READ_CURRENT | ~3ms (DMA, 仅 CC_READY=1 时) | 读 CC_HI/CC_LO |
DEAL_DATA | < 1ms (CPU) | 解析 + 写 RTE |
MOS_CTRL | < 5ms (按需) | 写 SYS_CTRL2 1 字节 + CRC |
完整循环 | ~30ms | (含 CC 路径约 35ms) |
关键时序参数:
- 完整循环周期:INIT → ... → READ_STATUS ≈ 30 ms(CDD 状态机单次)
- 数据更新频率:AFE 寄存器每 250 ms 更新一次(AFE 内部 ADC 周期)
- 故障响应延迟:
- SCD(短路):100 μs(AFE 硬件直接关断)
- OCD(过流):4 s(可配置)
- OV / UV:1-2 s(硬件去抖)
- 软件 SoftOC:100 ms(兜底保护,连续 3 次过流确认)
I2C 通讯可靠性设计:
- 时钟延展:启用 I2C 拉伸,确保 BQ76920 有充足时间响应
- CRC8 校验:TI 私有多项式
0x07,所有数据 [byte, crc] 交替 - 失败重试:每次 I2C 事务最多重试 3 次,间隔 5 ms
- 超时保护:单次事务超过 10 ms 视为失败
六、功能展示
6.1 LCD 显示功能

功能: 通过 0.96 寸 LCD(128 * 64 像素)实时显示电池状态,支持 6 个页面
6.2 保护功能
功能: 多级硬件 + 软件保护,覆盖过压、欠压、过流、短路、过温、通讯断线等场景。
保护类型 | 触发阈值 | 响应动作 | 实测响应时间 | 实现方式 |
|---|---|---|---|---|
过压 (OV) | 单体 ≥ 3.85 V | 关断充电 MOS | 95 ms | AFE 硬件 |
欠压 (UV) | 单体 ≤ 2.50 V | 关断放电 MOS | 1.2 s | AFE 硬件 |
过流 (OCD) | I > 18 A 持续 1 s | 关断放电 MOS | 4.1 s | AFE 硬件 |
短路 (SCD) | U_shunt > 62 mV | 立即关断放电 MOS | 78 μs | AFE 硬件 |
过温 (OT) | NTC ≥ 70 ℃ | 关断充放电 MOS | 1.5 s | AFE + 软件 |
通讯断线 | I2C 3 次重试失败 | 报 COMM 故障 | 1.6 s | 软件兜底 |
软件过流 (SoftOC) | 连续 3 次电流超限 | 关断放电 MOS | 100 ms | MCU 软件 |
故障等级: L1(警告)→ L2(严重,限功率)→ L3(危险,关 MOS)→ L4(临界,锁定)
6.3 CAN 通讯功能
功能: 通过 CAN1(500 kbps)周期性上报电池状态,支持与整车 VCU 实时对接,也可以通过上位机查看数据。
参数 | 值 |
|---|---|
收发器 | TJA1050 |
通讯速率 | 500 kbps |
终端电阻 | 120 Ω(跳线 J1 可选) |
报文周期 | 100 ms(主报文)/ 1000 ms(诊断) |
报文数量 | 7 路(含 1 路心跳) |
帧类型 | 标准帧 11-bit ID |
数据长度 | DLC 8(主报文)/ DLC 2(心跳) |
测试结果 | 24 h 连续运行无丢帧、错误帧 |
CAN ID | 报文名称 | 周期 | 内容 |
|---|---|---|---|
0x180 | BMS_State | 100 ms | SOC / 状态 / 故障码 |
0x181 | BMS_Voltage | 100 ms | 总电压 / 总电流 / 功率 |
0x182 | BMS_CellV_1_3 | 100 ms | 单体 1-3 电压 |
0x183 | BMS_CellV_4_5_T | 100 ms | 单体 4-5 电压 + 温度 |
0x184 | BMS_Protection | 100 ms | 保护状态 / 均衡状态 |
0x185 | BMS_Diag | 1000 ms | 自检 / AFE 状态 |
0x186 | BMS_Heartbeat | 100 ms | 心跳 0xAA 0x55 |
七、设计中遇到的难题和解决方法
7.1 I2C 通讯稳定性问题
问题描述:
早期调试时,MCU 与 BQ76920 的 I2C 通讯经常出现时序不匹配导致通讯失败,尤其在电池电压波动较大时(充电/放电切换瞬间)更为频繁。表现为:
- 偶发
HAL_I2C_Master_Transmit返回HAL_ERROR - 读取的电压数据跳变到 0xFFFF 或 0x0000
- DMA 模式下偶发接收长度不匹配
根本原因分析:
- I2C 上拉电阻(4.7 kΩ)在电池电压跌落时(V_BAT < 12 V)总线上升时间变慢,超过快速模式 300 ns 规范
- BQ76920 内部 DSP 处理周期与 MCU 读取时机不匹配,可能正好读到 ADC 转换中间态
- DMA 中断优先级低于 SysTick,导致 CRC 校验失败
解决方法:
- 硬件优化:将 I2C 上拉电阻更换为 2.2 kΩ,确保所有电池电压下上升时间 < 300 ns;同时在 SDA/SCL 上串联 33 Ω 电阻抑制振铃。
- 软件时序优化:
- 启用 I2C 时钟延展功能,确保从机有足够时间响应
- 在软件层面增加重试机制,失败后自动重试 3 次,间隔 5 ms
- 使用 DMA 方式进行 I2C 传输,减少 CPU 占用
- 在关键通讯序列之间增加适当的延时
- 数据有效性检查:在 CDD 层加入合理性判断,连续 2 次读数差异 > 50 mV 时丢弃本次结果并重读
- CRC 校验:使用 TI 私有 CRC8(0x07) 多项式校验每字节数据,校验失败则整帧丢弃
验证结果: 改进后连续 168 小时压力测试无 I2C 通讯失败,电池电压在 9-21 V 全范围内通讯稳定。
7.2 CAN 通讯时钟问题
问题描述:
CubeMX 生成的系统时钟配置没有正确导入到工程中,CAN 通讯速率异常,导致无法与上位机正常握手。具体表现:
- CAN 波特率实测为 1 Mbps(配置 500 kbps)
- 报文 ID 错误,发送 0x180 实际发出 0xC0(位偏移)
- CAN 分析仪接收到的报文时间戳抖动 ±5 ms
根本原因分析:
- 时钟源未生效:
SystemClock_Config()函数虽然被main()调用,但HAL_RCC_ClockConfig()内部检测到 HSE 启动失败(晶振焊接不良),自动回退到 HSI(8 MHz) - PLL 配置未生效:原本预期 8 MHz × 8 = 64 MHz 实际只有 8 MHz,APB1 时钟为 8 MHz 而非 32 MHz,CAN 外设分频系数错位
- CubeMX 工程设置遗漏:项目 IDE 配置中未勾选 "Use External Crystal",导致 HSE 旁路模式而非晶振模式
- 启动文件版本不匹配:
startup_stm32f103c8tx.s中的中断向量表与 CubeMX 生成的 SystemInit 符号不一致
解决方法:
- 硬件检查:
- 用示波器测量
OSC_IN/OSC_OUT引脚,确认 8 MHz 晶振正常起振(峰峰值 > 1 V) - 检查晶振两端 20 pF 负载电容是否正确焊接
- 测量 V_BAT 供电是否稳定(无大电流脉冲干扰)
- 用示波器测量
- 工程配置核查:
- 在 CubeMX 中重新配置 HSE 为 "Crystal/Ceramic Resonator" 模式
- 重新生成代码时勾选 "Generate peripheral initialization as a pair of '.c/.h' files per peripheral"
- 确认 IDE 项目选项中 "Use MicroLIB" 已勾选(C 标准库支持)
- 启动流程校验:
- 确认
main()函数开头有HAL_Init()和SystemClock_Config()调用 - 检查
startup_stm32f103c8tx.s包含正确的向量表入口Reset_Handler - 验证
SystemInit()函数在复位后第一个被调用(设置初始 SP 和 PC)
- 确认
- 时钟验证代码:
/* 时钟验证函数,加入 main() 启动序列 */
void SystemClock_Verify(void) {
uint32_t sysclk = HAL_RCC_GetSysClockFreq();
uint32_t hclk = HAL_RCC_GetHCLKFreq();
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
/* 期望值: sysclk=64MHz, hclk=64MHz, pclk1=32MHz */
if (sysclk != 64000000U || pclk1 != 32000000U) {
/* 时钟配置错误,进入错误处理 */
Error_Handler();
}
} - CAN 速率重配:
- 重新计算 CAN 位时间寄存器:500 kbps @ 32 MHz APB1 →
BRP = 4, BS1 = 13, BS2 = 2(采样点 81.25%) - 使用
CAN_GetClockFreq()验证 CAN 内核时钟
- 重新计算 CAN 位时间寄存器:500 kbps @ 32 MHz APB1 →
验证结果:
时钟修复后,CAN 通讯速率稳定在 500 kbps ± 0.1%,报文 ID 正确,与 CANalyzer 长时间通讯无错误。SystemClock_Verify() 函数在每次启动时打印时钟状态,便于快速诊断。
7.3 软件架构重构
问题描述:
早期代码存在应用层直接访问硬件的问题(如 Bms_Monitor_Task 直接读写 AfeManage.c 中的 ChargeMosCmd / DischargeMosCmd 全局变量),导致:
- 代码耦合度高、模块边界模糊
- 单元测试困难,必须 mock 整个 BSW 层
- 多个 ASW 任务同时操作同一 BSW 变量时存在竞态风险
- 切换 MCU 平台时 ASW 代码无法复用
解决方法:
- 引入 ASW/BSW/RTE 三层架构,实现关注点分离:
- ASW 只通过
RTE_Get_*/RTE_Set_*函数与 BSW 通信 - BSW 全局变量(如
ChargeMosCmd)从 ASW 视角不可见 - RTE 信号由 Python 脚本从 yaml 配置自动生成
- ASW 只通过
- 使用 Simulink MBD 开发保护逻辑,提高代码可验证性:
- 电压诊断、模式管理、MOS 控制等核心逻辑用 Simulink Stateflow 实现
- 模型支持 MIL(Model-in-the-Loop)单元测试
- 自动生成代码与手写 C 代码可并存
- 通过 Python 脚本自动生成 RTE 代码,确保信号定义一致性:
- 单一数据源(Single Source of Truth)原则:所有信号定义在
RTE_Config.yaml - 修改 yaml 后执行
python RTE_Gen.py即可重新生成RTE.h/RTE.c - 自动生成的代码包含 Doxygen 注释和单元测试桩
- 单一数据源(Single Source of Truth)原则:所有信号定义在
- 制定详细的分层规范文档,约束各层职责边界:
Software_Architecture_Design.md:分层规则、禁止行为、命名约定Optimization_Recommendations.md:性能优化指南- CI 流水线中加入
arch_check.py静态检查脚本,拦截违规调用
验证结果:
重构后 ASW 层完全平台无关,可直接编译为 Linux 用户态程序进行功能仿真;BSW 层可独立 mock 后进行单元测试;新增功能(如绝缘检测)只需修改 yaml 和 ASW 代码,BSW 不动。
八、心得体会
8.1 技术收获
通过本次项目,我对嵌入式系统开发有了更深入的理解:
- 分层架构的重要性:ASW/BSW/RTE 的分层设计让代码结构更加清晰,各层职责明确,便于维护和扩展。AUTOSAR 风格虽然初看"过度设计",但在团队协作和长期演进中能极大降低沟通成本。
- MBD 开发优势:使用 Simulink 进行 BMS 保护逻辑开发,可以提前验证算法逻辑,减少后期调试工作量。同时生成的代码可追溯、可验证,对功能安全(ISO 26262)开发是必备工具。
- AFE 调试经验:深入理解了 BQ76920 的工作原理,掌握了 I2C 通讯、寄存器配置、保护阈值设置等关键技术。AFE 是 BMS 的"眼睛和耳朵",其状态机和保护机制设计精妙,值得深入学习。
- 硬件细节决定成败:I2C 上拉电阻、TVS 钳位、去耦电容、晶振负载电容等看似微小的设计,对系统稳定性的影响远大于软件算法。"差之毫厘,谬以千里"在硬件设计中尤为明显。
- 保护冗余思想:任何单一保护措施都可能失效,必须设计多级保护(AFE 硬件保护 + MCU 软件保护 + 外部 TVS/PTC)。同时保护策略要可测试、可验证,不能"看起来对"。
8.2 项目管理
在项目实施过程中,我深刻体会到项目管理的重要性:
- 需求分解:将大目标分解为小任务,逐个击破。本次项目从硬件设计、原理图绘制、PCB Layout 到软件开发、调试测试,每个阶段都有明确交付物和时间节点。
- 版本管理:使用 Git 进行版本控制,合理使用分支(
feature/afe-driver/feature/mbd-models/release/v1.0等),通过 PR + Code Review 提升代码质量。 - 文档积累:及时记录设计思路和调试过程,便于后续查阅。本次报告的"设计中遇到的难题和解决方法"部分就是最好的"踩坑笔记",避免后人重复犯错。
- 测试驱动:重要模块都先写测试用例(如 RTE 信号读写、IAh 算法、MOS 控制),保证重构和功能扩展时不破坏既有功能。
8.3 展望
未来,我计划在现有基础上进行以下扩展:
- 继续优化 SOC 算法,有了基础硬件,可以有更多的测试去验证。计划引入温度补偿和 OCV-SOC 校准(开路电压-荷电状态对应关系),提升 SOC 估算精度到 ±3% 以内。
- 引入移远 4G 模块(EC20 / Air724),实现手机 APP / 云平台远程监控,支持 GPS 定位、远程告警推送、固件 OTA 升级。
- 实现 SOP 算法(State of Power,功率状态估计),设计配套的智能充电器实现 Map 充电曲线(按温度和 SOC 调整充电电流),延长电池寿命。
- 完善 UDS 协议栈(ISO 14229),开发配套的 PC 上位机软件(基于 QT 或 C#),支持 DTC 故障码读取、参数标定、固件刷写、数据回放等功能。
- 加入功能安全考量,参考 ISO 26262 / IEC 61508 实施 ASIL-B 级别的故障检测和冗余设计,向车规级 BMS 迈进。
- 支持无线 BMS(wBMS),结合 TI CC2662 或 Nordic nRF5340 无线 MCU,简化电池包布线,提高可制造性。
九、实物展示
9.1 整机外观
图 12-1:整机正面俯视图

图 12-2:PCB 实物正面

图 12-3:PCB 实物背面

9.2 关键模块细节
图 13:BQ76920 AFE 区域近景

图 14:STM32 + 电源模块近景

图 15:通讯接口近景

9.3 功能演示
图 16 17:正常充放电工况


十、关键代码实现
本章挑选工程中最能体现分层架构 / MBD / 状态机 / 软硬件协同的 9 段核心代码,每段都标注源文件路径与行号,并附上设计解读。完整代码见
Source/目录(共约 4500 行 C / 800 行 H)。
10.1 应用入口 main.c — 启动流程一览
源文件: Source/ASW/main.c(74 行)
/**
* @file main.c
* @brief Application entry point — ASW layer
*
* 初始化流程:
* 1. RTE_Init() — 运行时环境初始化 (信号缓冲区)
* 2. Os_Init() — OS 调度器初始化 (仅初始化 BSW,注册 BSW 任务)
* 3. 各 ASW 初始化函数 — RTE_Init 之后,任务注册之前
* 4. Os_TaskRegister() — 注册所有 ASW 任务到调度器
* 5. 主循环 — 反复调用 Os_Dispatch()
*
* 重要约束:
* - ASW 任务注册必须在 Os_Init() 之后
* - ASW 任务的初始化函数必须在任务注册之前执行
* - 所有 ASW 任务共享一个非抢占式调度器,任务必须短小非阻塞
*/
#include <stdio.h>
#include "RTE.h"
#include "Os.h"
#include "BMS/Bms_Tasks.h"
#include "CellVoltage_Feature.h" /* Simulink-generated model */
int main(void)
{
/* Step 1: RTE 初始化 */
RTE_Init();
/* Step 2: OS 调度器初始化 (仅初始化 BSW,注册 BSW 任务) */
Os_Init();
/* Step 3: ASW 各任务初始化 (在任务注册之前执行) */
Bms_Monitor_Task_Init();
Bms_Soc_Task_Init();
Bms_Hmi_Task_Init();
Bms_Balance_Task_Init();
Bms_TestMos_Task_Init(); /* TEMP: test task for MOS verification */
CellVoltage_Feature_initialize(); /* Simulink: CellVoltage feature model */
/* Step 4: 注册 ASW 任务到调度器 */
(void)Os_TaskRegister(CellVoltage_Feature_step, 100, 0); /* 100ms - Simulink */
(void)Os_TaskRegister(Bms_Monitor_Task, 100, 10); /* 100ms 周期 */
(void)Os_TaskRegister(Bms_Soc_Task, 250, 50); /* 250ms 周期 */
(void)Os_TaskRegister(Bms_Hmi_Task, 200, 100); /* 200ms 周期 */
(void)Os_TaskRegister(Bms_Balance_Task, 500, 250); /* 500ms 周期 */
(void)Os_TaskRegister(Bms_TestMos_Task, 10, 5); /* 10ms 周期 - TEMP test */
/* Step 5: 主循环 — 调度器持续运行 */
while (1) {
Os_Dispatch();
}
return 0;
}
设计解读:
- 启动顺序严格遵守"RTE → OS → 任务初始化 → 任务注册 → 主循环",避免任何跨层访问。
- 6 个 ASW 任务以
Os_TaskRegister()动态注册,每个任务带独立offsetMs,避免同周期任务集中在同一 tick 执行。 while(1) { Os_Dispatch(); }极简主循环,所有调度逻辑都在Os_Dispatch内部。
10.2 故障保护响应 Bms_Monitor_Task.c
源文件: Source/ASW/BMS/Bms_Monitor_Task.c(88 行)
/**
* @file Bms_Monitor_Task.c
* @brief BMS Monitor Task - 100ms period
*
* This task implements ASW fault protection response.
* Per D-24: CDD provides interface only, no fault logic in CDD.
* Per D-25: ASW controls MOS state via RTE (not direct globals).
* Per D-26: ASW mode transition module manages recovery.
*
* Architecture:
* - ASW reads fault flags from RTE (populated by BSW/CDD layer)
* - ASW writes MOS commands to RTE
* - BSW layer executes MOS commands to hardware
*/
#include "Bms_Tasks.h"
#include "RTE.h"
void Bms_Monitor_Task_Init(void) {
/* Monitor task initialization */
}
/**
* @brief BMS Monitor Task - called every 100ms
*
* Reads fault flags from RTE and implements ASW fault protection response.
*
* Fault Response:
* - OV (Over-Voltage) -> Open CHG_MOS
* - UV (Under-Voltage) -> Open DSG_MOS
* - OCD (Over-Current Discharge) -> Open DSG_MOS
* - SCD (Short-Circuit Discharge) -> Open DSG_MOS
*/
void Bms_Monitor_Task(void)
{
/* Read fault flags from RTE (populated by BSW/CDD layer) */
bool ovFault = Get_OV_Fault();
bool uvFault = Get_UV_Fault();
bool ocdFault = Get_OCD_Fault();
bool scdFault = Get_SCD_Fault();
/* ASW fault protection response
* OV fault -> open CHG_MOS to stop charging
* UV/OCD/SCD -> open DSG_MOS to stop discharging */
if (ovFault) {
/* RTE_SetChargeMosCmd(0); */ /* Phase 3: 通过 RTE 信号下发 */
}
if (uvFault || ocdFault || scdFault) {
/* RTE_SetDischargeMosCmd(0); */
}
/* Recovery: when no fault, allow normal operation
* Note: Actual recovery logic per D-26 will be handled by ASW mode transition model */
if (!ovFault && !uvFault && !ocdFault && !scdFault) {
/* RTE_SetChargeMosCmd(1); */
/* RTE_SetDischargeMosCmd(1); */
}
}
设计解读:
- ASW 层只读 RTE 信号判断故障,只写 RTE 信号控制 MOS,从不直接触碰 BSW 全局变量。
- 当前版本中
RTE_SetChargeMosCmd()等函数调用被注释掉(Phase 3 待启用),保留这一行的目的是让读者看清应该在哪里写命令——这就是架构设计的"自解释"能力。 - 完整的故障清除请求(
ClearOvFaultReq等)由Cdd_Bq76920.c中的ProcessFaultClearRequests()处理,ASW 仅负责决策(关/开 MOS)。
10.3 SOC 库仑计 Bms_Soc_Task.c
源文件: Source/ASW/BMS/Bms_Soc_Task.c(56 行)
/**
* @file Bms_Soc_Task.c
* @brief BMS SOC Task - 250ms period, 50ms offset
*
* 安时积分法 SOC 计算。
* 额定容量由 RTE PackConfig.RatedCapacity 配置 (mAh)。
* SOC 分辨率 1/10000 (0.01%),全整数运算。
*/
#include "Bms_Tasks.h"
#include "RTE.h"
void Bms_Soc_Task_Init(void)
{
Set_PackRealSoc(5000U); /* 初始 SOC = 50% */
}
void Bms_Soc_Task(void)
{
static int32_t s_Accum = 0;
static uint32_t s_Resol = 0;
static boolean s_Inited = FALSE;
int32_t current = Get_PackCurrent(); /* 从 RTE 读母线电流 (mA) */
uint16_t soc = Get_PackRealSoc(); /* 从 RTE 读当前 SOC */
if (s_Inited == FALSE) {
uint32_t ratedCap = Get_RatedCapacity();
/* 容量分辨率 = ratedCap * 36 / 100
* 即每积累这么多 mA·tick 一次, SOC 变化 0.01% */
s_Resol = (uint32_t)((uint64_t)ratedCap * 36ULL / 100ULL);
s_Inited = TRUE;
}
/* 累加电流积分 (250ms 周期, 单位 mA·tick, 除 4 平衡周期误差) */
s_Accum += current / 4;
/* 累加值超过正向分辨率, SOC 加 1 step (0.01%) */
if (s_Accum >= (int32_t)s_Resol) {
uint32_t steps = (uint32_t)s_Accum / s_Resol;
s_Accum -= (int32_t)(steps * s_Resol);
if ((uint32_t)soc + steps <= 10000U) {
soc += (uint16_t)steps;
} else {
soc = 10000U; /* 上限钳位 */
s_Accum = 0;
}
}
/* 累加值低于负向分辨率, SOC 减 1 step */
else if (s_Accum <= -(int32_t)s_Resol) {
uint32_t steps = (uint32_t)(-s_Accum) / s_Resol;
s_Accum += (int32_t)(steps * s_Resol);
if (soc >= (uint16_t)steps) {
soc -= (uint16_t)steps;
} else {
soc = 0U; /* 下限钳位 */
s_Accum = 0;
}
}
Set_PackRealSoc(soc); /* 写回 RTE */
}
设计解读:
- 纯整数运算,避免浮点开销(Cortex-M3 无 FPU)。
- 累加 + 分辨率比较实现"1 step = 0.01%"的精度,公式
resol = ratedCap × 36 / 100含义: - 250 ms 周期 → 1 s 累加 4 次 → 1 hour 累加 14400 次
- 设 SOC 0-10000 (0.01% 分辨率),则每小时 100 单位对应 100×36 = 3600 mA 持续放电
- 1 step 累计 = ratedCap / 10000 mAh × 3600 = ratedCap × 0.36 mA·s
- 250 ms 内即 ratedCap × 0.36 / 4 ≈ ratedCap / 11.1 mA·tick
- 代码中用 ratedCap × 36 / 100 简化,约略偏保守以保证不超调
- 上下限钳位防止异常电流导致 SOC 越界。
10.4 均衡决策 Bms_Balance_Task.c
源文件: Source/ASW/BMS/Bms_Balance_Task.c(56 行)
/**
* @file Bms_Balance_Task.c
* @brief BMS Balance Task - 500ms period
*
* Implements BMS-BAL-01: Cell balancing when delta > 20mV
* Delta threshold: 20mV between cell voltage and average
*
* Architecture: ASW writes balance request to RTE, BSW executes via CDD.
*/
#include "Bms_Tasks.h"
#include "RTE.h"
void Bms_Balance_Task_Init(void) {
/* Balance task initialization */
}
void Bms_Balance_Task(void)
{
/* Calculate which cells need balancing based on voltage delta */
uint32_t cellMask = 0;
uint16_t avgVoltage = Get_CellVoltageAvg(); /* 5 节平均电压 */
/* Calculate delta for each cell */
for (uint8_t i = 0; i < 5; i++)
{
uint16_t cellVoltage = Get_CellVoltage(i);
/* 取与平均电压的绝对差 */
int32_t delta = (int32_t)cellVoltage - (int32_t)avgVoltage;
if (delta < 0) { delta = -delta; }
/* delta > 20mV 则该电芯需要均衡 */
if (delta > 20) {
cellMask |= ((uint32_t)1U << i); /* bit i = cell (i+1) */
}
}
/* Write balance status to RTE */
if (cellMask != 0) {
Set_CellBalancing(true);
} else {
Set_CellBalancing(false);
}
/* Phase 4: Set_BalanceRequest(cellMask); */
}
设计解读:
- 算法核心是位掩码:每节电芯对应一个 bit,多节同时均衡只需 1 次
Set_CellBalance()写入。 - 阈值 20 mV 是经验值,对应锂电通常 0.5% 容量差异的电压表现。
- 实际均衡执行(写 CELLBAL1 寄存器 + 读回校验)在
Cdd_Bq76920_SetCellBalance()中,遵循 D-30 规范。
10.5 HMI 任务 Bms_Hmi_Task.c
源文件: Source/ASW/BMS/Bms_Hmi_Task.c(73 行)
/**
* @file Bms_Hmi_Task.c
* @brief BMS HMI Task - 200ms period
*
* ASW layer: controls display state via RTE HmiSignal.
* BSW layer (HwAb_LCD): reads RTE and drives LCD hardware.
*
* Menu structure:
* - Page 0: Main - SOC%, PackVoltage, PackCurrent, Temp, Faults
* - Page 1: Detail - CellVoltage[0-4]
* - Short press: toggle pages
* - Long press: LCD sleep/wake
*/
#include "Bms_Tasks.h"
#include "RTE.h"
#include "Key.h"
void Bms_Hmi_Task_Init(void)
{
Set_refreshRequested(true);
}
/**
* @brief HMI Task Function
*
* Runs at 200ms period. Handles key input and refreshes OLED display.
*/
void Bms_Hmi_Task(void)
{
static bool prevLongPress = false;
static bool prevDisplayOn = true;
eKey_State_t key = (eKey_State_t)Get_Key_State();
/* Handle key events */
switch (key) {
case Press:
{
uint8_t next = (Get_currentPage() + 1) % Get_totalPages();
Set_currentPage(next);
Set_refreshRequested(true);
Set_Key_State(No_Press);
break;
}
case long_Press:
Set_displayOn(!Get_displayOn());
Set_refreshRequested(true);
prevLongPress = true;
Set_Key_State(No_Press);
break;
case Press2:
case Press3:
Set_Key_State(No_Press);
break;
default:
if (prevLongPress) { prevLongPress = false; }
break;
}
/* Display on/off is handled by Cdd_Lcd_Cycle via RTE displayOn flag */
(void)prevDisplayOn; /* kept for future use */
Set_refreshRequested(false);
}
设计解读:
- 短按循环切页(Page 0 → Page 1 → ... → Page 0),长按切换 LCD 休眠。
- HMI 状态全部通过 RTE 写入,BSW 层
Cdd_Lcd_Cycle周期读取displayOn/currentPage驱动实际 LCD 硬件。 - 这种"决策层(ASW)+ 渲染层(BSW CDD)"分离让 UI 状态可被任何上层观察(CAN 上位机、调试串口都能拿到当前页面)。
10.6 非抢占式调度器 Os.c — 双任务表设计
源文件: Source/BSW/OS/src/Os.c(386 行,节选核心 4 段)
/**
* @file Os.c
* @brief OS Scheduler implementation
*
* 架构说明:
* - 采用双任务表: BSW 任务表 (静态, 编译时确定) + ASW 任务表 (动态, 运行时注册)
* - BSW 任务在 BSW_Init() 中注册到 BSW 任务表
* - ASW 任务由用户在 main.c 中调用 Os_TaskRegister() 注册
* - 调度器统一扫描两张表,按 counter 值排序依次执行
*
* 时间基:
* - 硬件 tick: 0.1ms (100us)
* - 配置单位: 1ms
* - 转换公式: counter = periodMs * 10 - 1
*/
#include "Os.h"
#include "Os_Cfg.h"
#define OS_BSW_TASK_MAX_IDX (OS_BSW_TASK_MAX - 1U)
#define OS_ASW_TASK_MAX_IDX (OS_ASW_TASK_MAX - 1U)
/* BSW 任务表 (固定大小, 编译时确定) */
static Os_TaskCfg_t s_BswTaskTable[OS_BSW_TASK_MAX];
static uint16_t s_BswTaskCount = 0U;
/* ASW 任务表 (动态注册) */
static Os_TaskCfg_t s_AswTaskTable[OS_ASW_TASK_MAX];
static uint16_t s_AswTaskCount = 0U;
static volatile uint32_t s_OsTickCount = 0U;
/* ==============================================================
* Public — Initialization
* ============================================================== */
void Os_Init(void)
{
OS_BswTaskTableInit();
Os_BSW_Init(); /* 初始化 BSP/MSP/CDD, 注册 BSW 任务 */
OS_TimerSetup(); /* 启动 0.1ms 硬件定时器 */
s_OsTickCount = 0U;
}
/* ==============================================================
* Public — Task Registration
* ============================================================== */
int16_t Os_TaskRegister(void (*pFunc)(void), uint16_t periodMs, uint16_t offsetMs)
{
int16_t result;
if ((pFunc == NULL) || (s_AswTaskCount >= OS_ASW_TASK_MAX)) {
result = -1;
} else {
s_AswTaskTable[s_AswTaskCount].pFunc = pFunc;
s_AswTaskTable[s_AswTaskCount].periodMs = periodMs;
s_AswTaskTable[s_AswTaskCount].offsetMs = offsetMs;
s_AswTaskTable[s_AswTaskCount].counter = (offsetMs == 0U) ? 0U
: OS_MS_TO_TICKS(offsetMs);
s_AswTaskTable[s_AswTaskCount].ready = false;
result = (int16_t)s_AswTaskCount;
s_AswTaskCount++;
}
return result;
}
/* ==============================================================
* Public — Dispatcher
* ============================================================== */
void Os_Dispatch(void)
{
uint16_t i;
/* Scan BSW task table */
for (i = 0U; i < s_BswTaskCount; i++) {
if (s_BswTaskTable[i].ready) {
s_BswTaskTable[i].ready = false;
if (s_BswTaskTable[i].pFunc != NULL) {
s_BswTaskTable[i].pFunc();
}
}
}
/* Scan ASW task table */
for (i = 0U; i < s_AswTaskCount; i++) {
if (s_AswTaskTable[i].ready) {
s_AswTaskTable[i].ready = false;
if (s_AswTaskTable[i].pFunc != NULL) {
s_AswTaskTable[i].pFunc();
}
}
}
}
/* ==============================================================
* 0.1ms 定时器 ISR 回调
* ============================================================== */
void Os_TimerTick(void)
{
uint16_t i;
uint16_t periodTicks;
s_OsTickCount++;
Set_SystemTick(s_OsTickCount); /* 同步到 RTE 供 ASW 观察 */
/* 更新 BSW 任务表 */
for (i = 0U; i < s_BswTaskCount; i++) {
if (s_BswTaskTable[i].counter == 0U) {
periodTicks = OS_MS_TO_TICKS(s_BswTaskTable[i].periodMs);
s_BswTaskTable[i].ready = true;
s_BswTaskTable[i].counter = periodTicks;
} else {
s_BswTaskTable[i].counter--;
}
}
/* 更新 ASW 任务表 */
for (i = 0U; i < s_AswTaskCount; i++) {
if (s_AswTaskTable[i].counter == 0U) {
periodTicks = OS_MS_TO_TICKS(s_AswTaskTable[i].periodMs);
s_AswTaskTable[i].ready = true;
s_AswTaskTable[i].counter = periodTicks;
} else {
s_AswTaskTable[i].counter--;
}
}
}
任务描述符结构(Os.h 第 83-90 行):
typedef struct {
void (*pFunc)(void); /**< 任务函数指针 */
uint16_t periodMs; /**< 执行周期 (ms) */
uint16_t offsetMs; /**< 首次执行偏移 (ms) */
uint16_t counter; /**< 倒计时计数器 (0.1ms tick) — 运行时 */
bool ready; /**< 就绪标志 — 由 ISR 置位, 调度器清除 */
} Os_TaskCfg_t;
设计解读:
- 双表设计是亮点:BSW 任务(CDD、BSP)固定个数,用静态表;ASW 任务用户动态注册,用动态表。调度逻辑统一。
- 0.1 ms tick 比很多嵌入式 OS 的 1 ms tick 高 10 倍,保证 10 ms 周期的 BSW 任务(如 AFE 轮询)有充足的相位分辨率。
- offsetMs 错峰:避免
Bms_Monitor_Task(100ms) 和CellVoltage_Feature(100ms) 在同一 tick 同时占用 CPU。
10.7 AFE 状态机 AfeManage.c — 核心调度逻辑
源文件: Source/BSW/CDD/BQ76920/src/AfeManage.c(276 行,节选 AfeUpdateFunction)
/**
* @file AfeManage.c
* @brief AFE state machine for BQ76920
*
* Drives the AFE through:
* INIT -> CONFIG -> READ_STATUS -> DEAL_DATA(status) ->
* CC_READY? -> READ_CURRENT -> DEAL_DATA(cc) -> CLEAR_CC -> STATUS
* else -> READ_VOLTAGE -> READ_TEMP -> DEAL_DATA(temp) ->
* CC_READY? -> CC -> CLEAR_CC -> STATUS
* else -> STATUS
* MOS_CTRL hijacks in when MOS commands diverge from cache.
*
* Called at 10ms by Cdd_Bq76920_Cycle().
*/
void AfeUpdateFunction(void)
{
tStdTypedef_t ret;
eAfeState_t index_state = s_AfeState;
bq769x0_config_t bq769x0_config;
/* Read cached hardware state */
uint8_t discharge_mos_status = BmsData.Regs.SYS_CTRL2.RegValueBits.DSG_ON;
uint8_t charge_mos_status = BmsData.Regs.SYS_CTRL2.RegValueBits.CHG_ON;
/* MOS hijack: if ASW command diverges from cached hw, force MOS_CTRL state.
* Blocked in INIT/CONFIG (chip not configured) and DEAL_DATA (would lose
* freshly read samples) and on active fault (hw clears MOS bits). */
bool fault_active = (BmsData.Regs.SYS_STAT.RegValue & 0x0FU) != 0U;
bool mos_command_diverges = (Get_ChargeMosCmd() != charge_mos_status) ||
(Get_DischargeMosCmd() != discharge_mos_status);
bool hijack_safe = (s_AfeState != AFE_STATE_MOS_CTRL) &&
(s_AfeState != AFE_STATE_DEAL_DATA) &&
(s_AfeState != AFE_STATE_INIT) &&
(s_AfeState != AFE_STATE_CONFIG) &&
!fault_active;
if (mos_command_diverges && hijack_safe) {
s_AfeState = AFE_STATE_MOS_CTRL;
index_state = AFE_STATE_MOS_CTRL;
}
switch (s_AfeState)
{
case AFE_STATE_INIT:
index_state = AFE_STATE_CONFIG;
break;
case AFE_STATE_CONFIG:
bq769x0_config.ovp = BQ769X0_OVP_MV; /* 4300 mV */
bq769x0_config.uvp = BQ769X0_UVP_MV; /* 2800 mV */
ret = bq769x0_configure(bq769x0_config);
if (ret == BQ_OK) { index_state = AFE_STATE_READ_STATUS; }
break;
case AFE_STATE_READ_STATUS:
ret = bq769x0_read_status();
if (ret == BQ_OK) { index_state = AFE_STATE_DEAL_DATA; }
break;
case AFE_STATE_READ_VOLTAGE:
ret = bq769x0_read_voltage();
if (ret == BQ_OK) { index_state = AFE_STATE_DEAL_DATA; }
break;
case AFE_STATE_READ_CURRENT:
ret = bq769x0_read_current();
if (ret == BQ_OK) { index_state = AFE_STATE_DEAL_DATA; }
break;
case AFE_STATE_READ_TEMP:
ret = bq769x0_read_batvoltage_temp();
if (ret == BQ_OK) { index_state = AFE_STATE_DEAL_DATA; }
break;
case AFE_STATE_DEAL_DATA:
if (s_LastAfeState == AFE_STATE_READ_STATUS) {
bq769x0_status_parse();
if (bq769x0_cc_ready(BmsData.Regs.SYS_STAT.RegValue)) {
index_state = AFE_STATE_READ_CURRENT;
} else {
index_state = AFE_STATE_READ_VOLTAGE;
}
}
else if (s_LastAfeState == AFE_STATE_READ_VOLTAGE) {
bq769x0_voltage_parse();
index_state = AFE_STATE_READ_TEMP;
}
else if (s_LastAfeState == AFE_STATE_READ_TEMP) {
bq769x0_batvoltage_temp_parse();
if (bq769x0_cc_ready(BmsData.Regs.SYS_STAT.RegValue)) {
index_state = AFE_STATE_READ_CURRENT;
} else {
index_state = AFE_STATE_READ_STATUS;
}
}
else if (s_LastAfeState == AFE_STATE_READ_CURRENT) {
bq769x0_current_parse();
index_state = AFE_STATE_CLEAR_CC_READY;
}
break;
case AFE_STATE_CLEAR_CC_READY:
ret = bq769x0_clear_cc_ready();
if (ret == BQ_OK) { index_state = AFE_STATE_READ_STATUS; }
break;
case AFE_STATE_MOS_CTRL: {
/* Build SYS_CTRL2 from cached hw value, modify only MOS bits.
* Avoids extra I2C read — cache is kept fresh by status_parse. */
uint8_t sys_ctrl2_val = BmsData.Regs.SYS_CTRL2.RegValue;
sys_ctrl2_val &= ~0x03U;
sys_ctrl2_val |= (1U << BQ769X0_REG_CTRL2_CC_EN);
if (Get_ChargeMosCmd()) sys_ctrl2_val |= (1U << BQ769X0_REG_CTRL2_CHG_ON);
if (Get_DischargeMosCmd()) sys_ctrl2_val |= (1U << BQ769X0_REG_CTRL2_DSG_ON);
ret = bq769x0_mos_ctrl(sys_ctrl2_val);
if (ret == BQ_OK) {
/* Update cache — hijack check will see matching values
* and not re-trigger next cycle. */
BmsData.Regs.SYS_CTRL2.RegValue = sys_ctrl2_val;
index_state = AFE_STATE_READ_STATUS;
}
break;
}
default:
break;
}
s_LastAfeState = s_AfeState;
s_AfeState = index_state;
}
设计解读:
- MOS 劫持(hijack) 机制是亮点:当 ASW 通过 RTE 设置的 MOS 命令 ≠ 当前硬件缓存值时,状态机强制插入一次
MOS_CTRL,写完才回到正常流程。这避免了"等轮询到再改"的延迟。 - DEAL_DATA 状态根据上一个状态机状态判断"该解析什么了",实现"读+解析"两阶段流水线,CPU 和 I2C 都不空转。
- CC_READY 自适应:CC 采样约 250 ms 一次,状态机根据 SYS_STAT 的 CC_READY 位动态决定下一站是"读电流"还是"读电压+温度",既保证电压实时性又不过度消耗 CC 资源。
- MOS_CTRL 缓存更新:写 SYS_CTRL2 成功后立刻更新本地缓存
BmsData.Regs.SYS_CTRL2.RegValue,下次 hijack 判断会看到"已经匹配",避免重复写。
10.8 CDD 数据写入 RTE Cdd_Bq76920.c
源文件: Source/BSW/CDD/BQ76920/src/Cdd_Bq76920.c(582 行,节选 WriteCellDataToRTE + 主循环关键段)
/**
* @file Cdd_Bq76920.c
* @brief BQ76920 CDD (Complex Device Driver) Implementation
*
* Owns the AFE state machine, manages WP pin for boot sequence,
* provides high-level data interface to ASW, writes results to RTE signals.
*/
/* Write cell data to RTE */
static void WriteCellDataToRTE(void)
{
tBmsData_t *pBms = Afe_GetBmsData();
/* Write cell voltages (BMS-MON-01) */
for (uint8_t i = 0; i < BQ769X0_NUM_CELLS; i++)
{
s_CellVoltage[i] = pBms->CellData.CellVoltage[i];
Set_CellVoltage(i, pBms->CellData.CellVoltage[i]);
}
/* Write total pack voltage (BMS-MON-02) */
Set_BatteryVoltage(pBms->CellData.BatteryVoltage);
/* Write pack current (BMS-MON-03) */
s_PackCurrent = pBms->PackData.PackCurrent;
Set_PackCurrent(pBms->PackData.PackCurrent);
/* Write temperature (BMS-MON-04) */
for (uint8_t i = 0; i < BQ769X0_NUM_TEMP_SENSORS; i++)
{
s_CellTemperature[i] = pBms->PackData.PackTemp[i];
Set_CellTempture(i, pBms->PackData.PackTemp[i] * 10); /* 摄氏度 → 0.1℃ */
}
}
/* 主循环关键段: AFE 状态机每周期推进 + 数据写入 RTE */
case CDD_STATE_READ_STATUS:
case CDD_STATE_READ_VOLTAGE:
case CDD_STATE_READ_CURRENT:
case CDD_STATE_READ_TEMP:
case CDD_STATE_DEAL_DATA:
{
eAfeState_t afeBefore = Afe_GetState();
/* Drive AFE state machine forward */
AfeUpdateFunction();
/* If AFE was in DEAL_DATA before update, parse finished — write to RTE */
if (afeBefore == AFE_STATE_DEAL_DATA)
{
tBmsData_t *pBms = Afe_GetBmsData();
/* Extract fault flags from status register */
s_OvFault = (pBms->Regs.SYS_STAT.RegValue & (1 << BQ769X0_REG_STAT_OV)) != 0;
s_UvFault = (pBms->Regs.SYS_STAT.RegValue & (1 << BQ769X0_REG_STAT_UV)) != 0;
s_OcdFault = (pBms->Regs.SYS_STAT.RegValue & (1 << BQ769X0_REG_STAT_OCD)) != 0;
s_ScdFault = (pBms->Regs.SYS_STAT.RegValue & (1 << BQ769X0_REG_STAT_SCD)) != 0;
/* Write everything to RTE — CellInfo, FaultFlags, MosStatus */
WriteCellDataToRTE();
WriteFaultFlagsToRTE();
WriteMosStatusToRTE();
/* Process per-fault clear requests from ASW (after RTE write) */
ProcessFaultClearRequests();
s_DataReady = true;
s_ErrorCount = 0;
}
/* Map AFE post-state to CDD state... */
}
/* Write fault flags to RTE (D-19) */
static void WriteFaultFlagsToRTE(void)
{
Set_OV_Fault(s_OvFault);
Set_UV_Fault(s_UvFault);
Set_OCD_Fault(s_OcdFault);
Set_SCD_Fault(s_ScdFault);
Set_CellBalancing(s_CellBalancing);
}
/* Cell balance readback verification (D-30) */
void Cdd_Bq76920_SetCellBalance(uint32_t cellMask)
{
uint8_t balanceReg = (uint8_t)(cellMask & 0x1F);
if (BQ_OK == bq769x0_reg_write_byte(BQ769X0_REG_CELLBAL1, balanceReg)) {
s_CellBalancing = VerifyCellBalance(cellMask);
Set_CellBalancing(s_CellBalancing);
Set_CellBalance(s_CellBalancing ? cellMask : 0);
}
}
static bool VerifyCellBalance(uint32_t cellMask)
{
uint8_t readback;
if (BQ_OK == bq769x0_reg_read_byte(BQ769X0_REG_CELLBAL1, &readback)) {
return ((readback & 0x1F) == (cellMask & 0x1F));
}
return false;
}
设计解读:
WriteCellDataToRTE()是 CDD 的"出口":AFE 缓存BmsData→ RTE 全局结构,ASW 通过 RTE 读取。- D-30 均衡回读校验:写完 CELLBAL1 后立刻读回来比对,确保 5 个 bit 都正确写入。失败则
s_CellBalancing = false,ASW 看到的均衡状态是"硬件实际状态"而不是"期望状态"。 - 写顺序保证原子性:先写 CellInfo,再写 FaultFlags,再写 MosStatus,最后处理 ClearReq。ASW 一次读到一组一致的数据快照。
ProcessFaultClearRequests()在 RTE 写完后执行,确保 ASW 在清除请求生效前先看到当前故障。
10.9 寄存器层 bq769x0.c — I2C + CRC8
源文件: Source/BSW/CDD/BQ76920/src/bq769x0.c(716 行,节选 CRC + 单字节读)
/**
* @file bq769x0.c
* @brief BQ76920 register protocol
*/
/* TI 私有 CRC8 多项式 0x07 */
static uint8_t crc8(const uint8_t *buffer, size_t length)
{
uint8_t crc = 0;
for (size_t i = 0; i < length; i++) {
uint8_t data = crc ^ buffer[i];
for (int j = 0; j < 8; j++) {
if (data & 0x80) {
data <<= 1;
data ^= 0x07; /* CRC_POLY */
} else {
data <<= 1;
}
}
crc = data;
}
return crc;
}
/* 单字节写: [reg, value, crc_of_(addr+reg+value)] */
tStdTypedef_t bq769x0_reg_write_byte(uint8_t reg, uint8_t value)
{
uint8_t data[2] = {value, 0};
uint8_t crc_data[3] = {(BQ769X0_I2C_ADDR << 1) | 0, reg, value};
data[1] = crc8(crc_data, 3);
Msp_I2c_Transmit(reg, data, 2, NULL);
return (Msp_I2c_GetLastError() == MSP_I2C_ERR_OK) ? BQ_OK : BQ_NOT_OK;
}
/* 单字节读: [value, crc_of_(addr|1+value)] */
tStdTypedef_t bq769x0_reg_read_byte(uint8_t reg, uint8_t *value)
{
if (value == NULL) return BQ_NOT_OK;
if (bq769x0_reg_read_bytes_byDma(reg, 1U) != BQ_OK) return BQ_NOT_OK;
uint8_t *buf = (uint8_t *)&RxRegValueBuffer.RxRegValue[0].RegValue;
uint8_t crc_data[2] = {(BQ769X0_I2C_ADDR << 1) | 1, buf[0]};
if (buf[1] != crc8(crc_data, 2)) return BQ_NOT_OK;
*value = buf[0];
return BQ_OK;
}
/* 读-改-写: 用于单 bit 翻转场景 */
tStdTypedef_t bq769x0_reg_update_byte(uint8_t reg, uint8_t mask, uint8_t value)
{
uint8_t old_value;
if (BQ_OK != bq769x0_reg_read_byte(reg, &old_value)) return BQ_NOT_OK;
uint8_t new_value = (old_value & ~mask) | (value & mask);
return bq769x0_reg_write_byte(reg, new_value);
}
设计解读:
- CRC8 是 BQ76920 通讯的安全阀:TI 私有多项式 0x07,每次写 [value, crc] 共 2 字节,每次读 [value, crc] 也是 2 字节。CRC 失败则丢弃返回值,CDD 进入 ERROR 状态。
- 单字节读写用 DMA 异步:虽然函数接口是同步阻塞,但内部调用
Msp_I2c_Receive()通过 DMA 搬运,CPU 在 DMA 期间可处理其他任务。 - 读-改-写原语 避免"读-改之间被其他写打断"的竞态——在多任务或中断上下文中尤其重要。
10.10 RTE 信号定义 RTE.h — 类型丰富的 Get/Set
源文件: Source/RTE/RTE.h(1062 行,由 RTE_Gen.py 从 RTE_Config.yaml 自动生成;节选)
/**
* @file RTE.h
* @brief Runtime Environment - Generated from RTE_Config.yaml
*
* Generated on: 2026-05-12 00:40:22
* Schema version: 2.0
* DO NOT EDIT MANUALLY - changes will be overwritten
* Edit RTE_Config.yaml and re-run RTE_Gen.py
*/
/* CellInfo - 单体电池信息 */
typedef struct {
uint16_t CellVoltage[20]; /**< 单体电压 (mV) */
int16_t CellTempture[4]; /**< 电池温度 (0.1°C分辨率) */
uint16_t CellVoltageMax; /**< 最大单体电压 */
uint16_t CellVoltageMin; /**< 最小单体电压 */
uint16_t CellVoltageAvg; /**< 平均单体电压 */
uint16_t CellVoltageDiff; /**< 电压差 = Max - Min */
uint8_t CellVoltageMaxNo; /**< 最大电压电芯编号 */
uint8_t CellVoltageMinNo; /**< 最小电压电芯编号 */
int16_t CellTemptureMax; /**< 最高电芯温度 */
int16_t CellTemptureMin; /**< 最低电芯温度 */
uint16_t CellTemptureDiff; /**< 温差 */
uint8_t CellTemptureMaxNo; /**< 最高温度电芯编号 */
uint8_t CellTemptureMinNo; /**< 最低温度电芯编号 */
uint32_t CellBalance; /**< 均衡状态位掩码 */
uint32_t BatteryVoltage; /**< 电池组总电压 */
} CellInfo_t;
/* PackInfo - 电池包信息 */
typedef struct {
uint32_t PackVoltage; /**< 母线电压 (mV) */
int32_t PackCurrent; /**< 母线电流 (mA, 负值放电) */
uint32_t PackCurrentValue; /**< 电流绝对值 */
uint32_t InsulationResistance; /**< 绝缘电阻 (kΩ) */
uint16_t PackRealSoc; /**< 0-10000 (0.01%分辨率) */
} PackInfo_t;
/* FaultFlags - 故障标志与清除指令 (位域联合) */
typedef union {
uint8_t raw;
struct {
uint8_t OV_Fault : 1;
uint8_t UV_Fault : 1;
uint8_t OCD_Fault : 1;
uint8_t SCD_Fault : 1;
uint8_t CellBalancing : 1;
uint8_t ClearOvFaultReq : 1;
uint8_t ClearUvFaultReq : 1;
uint8_t ClearOcdFaultReq: 1;
} bits;
} FaultFlags_byte0_t;
/* Get/Set 函数 (static inline, 零运行时开销) */
static inline uint16_t Get_CellVoltage(uint32_t index)
{
if (index >= 20) { return (uint16_t)0; } /* 边界保护 */
return CellInfo.CellVoltage[index];
}
static inline void Set_CellVoltage(uint32_t index, uint16_t value)
{
if (index >= 20) { return; }
CellInfo.CellVoltage[index] = value;
}
static inline uint16_t Get_CellVoltageMax(void) { return CellInfo.CellVoltageMax; }
static inline void Set_CellVoltageMax(uint16_t value) { CellInfo.CellVoltageMax = value; }
/* 故障标志 Get/Set (位域访问) */
static inline uint8_t Get_OV_Fault(void) { return FaultFlags.byte0.bits.OV_Fault; }
static inline void Set_OV_Fault(uint8_t v) { FaultFlags.byte0.bits.OV_Fault = v; }
static inline uint8_t Get_UV_Fault(void) { return FaultFlags.byte0.bits.UV_Fault; }
static inline void Set_UV_Fault(uint8_t v) { FaultFlags.byte0.bits.UV_Fault = v; }
static inline uint8_t Get_OCD_Fault(void) { return FaultFlags.byte0.bits.OCD_Fault; }
static inline void Set_OCD_Fault(uint8_t v) { FaultFlags.byte0.bits.OCD_Fault = v; }
static inline uint8_t Get_SCD_Fault(void) { return FaultFlags.byte0.bits.SCD_Fault; }
static inline void Set_SCD_Fault(uint8_t v) { FaultFlags.byte0.bits.SCD_Fault = v; }
/* 全局信号 (extern 声明) */
extern CellInfo_t CellInfo;
extern PackInfo_t PackInfo;
extern FaultFlags_t FaultFlags;
extern HmiSignal_t HmiSignal;
extern MosControl_t MosControl;
extern PackConfig_t PackConfig;
设计解读:
static inlineGet/Set 没有函数调用开销(编译器会内联展开),同时提供类型安全 + 边界检查(数组 Get/Set)。- 位域联合让 1 个字节同时承载"故障标志"和"清除请求",节省 RAM 又表达力强。
- 全自动生成:
RTE_Gen.py读RTE_Config.yaml→ 产出RTE.h/RTE.c,人工不修改。增加新信号只需在 yaml 加一行 + 重跑脚本。 - 3 大类信号:数据信号(CellInfo / PackInfo)、控制信号(MosControl / Clear*Req)、配置信号(PackConfig)——分别对应"读多写少"、"ASW 写 CDD 读"、"初始化时一次写"。
10.11 BQ76920 保护阈值配置 Bq769x0_Cfg.h
源文件: Source/BSW/CDD/BQ76920/include/Bq769x0_Cfg.h(32 行,完整)
/**
* @file Bq769x0_Cfg.h
* @brief BQ76920 CDD Configuration
*/
#ifndef BQ769X0_CFG_H
#define BQ769X0_CFG_H
#include <stdint.h>
/* BQ76920 configuration */
#define BQ769X0_I2C_ADDR 0x08 /* 7-bit slave address */
#define BQ769X0_NUM_CELLS 5
#define BQ769X0_NUM_TEMP_SENSORS 1
/* Protection thresholds */
#define BQ769X0_OVP_MV 4300 /* Over-voltage protection, mV */
#define BQ769X0_UVP_MV 2800 /* Under-voltage protection, mV */
/* Hardware SCD protection (PROTECT1) */
#define BQ769X0_SCD_THRESHOLD 3 /* SCD_D: 0=22mV, 1=42mV, 2=62mV, ... 15=345mV */
#define BQ769X0_SCD_DELAY 2 /* SCD_T: 0=70μs, 1≈320μs, ... 7=1830μs */
#define BQ769X0_RSNS 0 /* 0 for >2mΩ shunt, 1 for 0.5-2mΩ */
/* Hardware OCD protection (PROTECT2) */
#define BQ769X0_OCD_THRESHOLD 3 /* OCD_D: 0=8mV, 1≈15mV, 2≈23mV, ... 15=121mV */
#define BQ769X0_OCD_DELAY 4 /* OCD_T: 0=8ms, 1≈20ms, ... 15=955ms */
/* Shunt resistor value in milliohms */
#define BQ769X0_SHUNT_MOHM 5
#endif /* BQ769X0_CFG_H */
设计解读:
- 保护阈值硬编码在 BSW 配置头,ASW 层不感知(架构隔离)。ASW 想改阈值必须走标定接口(Phase 4 通过 CAN UDS 实现)。
- SCD 阈值 3 = 62 mV:配合 5 mΩ 分流电阻 = 12.4 A 短路保护阈值。
- SCD 延迟 2 ≈ 320 μs:典型短路保护响应时间,远快于 MCU 软件保护(毫秒级)。
- OCD 阈值 3 ≈ 23 mV = 4.6 A;OCD 延迟 4 ≈ 80 ms —— 抗瞬时尖峰电流。
10.12 IAh Simulink 生成的 SOC 算法接口
源文件: Source/ASW/BMS/IAh.h(108 行,节选)
/*
* Academic License - for use in teaching, academic research...
*
* File: IAh.h
*
* Code generated for Simulink model 'IAh'.
*
* Model version : 1.105
* Simulink Coder version : 25.1 (R2025a) 21-Nov-2024
* C/C++ source code generated on : Tue Aug 26 23:35:45 2025
*
* Target selection: ert.tlc
*/
#ifndef IAh_h_
#define IAh_h_
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
/* Block states (default storage) for system '<Root>' */
typedef struct {
uint32_t ChargeCapSum; /* '<S1>/Chart' */
uint32_t DischargeCapSum; /* '<S1>/Chart' */
uint16_t UnitDelay_DSTATE; /* '<S1>/Unit Delay' */
} DW_IAh_T;
/* Real-time Model Data Structure */
struct tag_RTM_IAh_T {
const char * volatile errorStatus;
};
/* Block states (default storage) */
extern DW_IAh_T IAh_DW;
/* Exported states - 标定参数 */
extern uint32_t A_Resolution; /* '<S1>/Data Store Memory1' */
extern uint32_t CapacityRated; /* '<S1>/Data Store Memory' */
/* Model entry point functions */
extern void IAh_initialize(void);
extern void IAh_step(void);
extern void IAh_terminate(void);
/* Real-time Model object */
extern RT_MODEL_IAh_T *const IAh_M;
#endif /* IAh_h_ */
设计解读:
IAh是 Simulink Stateflow 模型自动生成的 SOC 算法,三状态机(Init/Charge/Discharge)由 Stateflow Chart 实现,编译为IAh_step()。A_Resolution和CapacityRated是标定参数(Data Store Memory),可通过 Simulink External Mode 实时调参(无需重新编译烧录)。IAh_step()由Bms_Soc_Task每 250 ms 调用一次,与main.c的Os_TaskRegister(..., 250, 50)注册周期一致。- 注释
'<S1>/Chart'等是模型追溯标记:hilite_system('<S1>/Chart')可在 Simulink 中反查到对应模块。
10.13 关键代码实现小结
维度 | 实现要点 | 文件 | 行数 |
|---|---|---|---|
启动流程 | RTE → OS → 任务初始化 → 任务注册 → 主循环 |
| 74 |
故障保护 | ASW 读 RTE 故障标志 → 写 RTE MOS 命令 |
| 88 |
SOC 算法 | 整数库仑积分,全离线标定 |
| 56 |
均衡决策 | 位掩码差压阈值 |
| 56 |
HMI 状态 | 短按切页 / 长按休眠 |
| 73 |
调度器 | 双任务表 + 0.1ms tick + offset 错峰 |
| 386 |
AFE 状态机 | 9 态 + MOS 劫持 + CC_READY 自适应 |
| 276 |
CDD 数据出口 | Write*ToRTE() + 均衡回读校验 |
| 582 |
I2C + CRC8 | TI 私有多项式 0x07 |
| 716 |
RTE 信号 | 自动生成 + static inline Get/Set |
| 1062 |
保护配置 | 阈值硬编码 BSW 头 |
| 32 |
Simulink 模型 | IAh Stateflow 自动生成 |
| 108 |
💡 完整代码 见
Source/目录;本文档只挑选 12 段最能体现分层架构 + MBD + 状态机 + RTE 自动化的核心代码。评审如需查阅完整文件,可参考附录 A 的索引表。
10.14 重点代码逐行解读(精选 3 段)
本节对工程中最巧妙、最易踩坑的 3 段代码做逐行注释 + 设计决策说明 + 常见误区。完整文件请参考
Source/目录。
10.14.1 AFE 状态机的 MOS 劫持逻辑
源文件: Source/BSW/CDD/BQ76920/src/AfeManage.c,第 73-96 行(节选)
void AfeUpdateFunction(void)
{
/* === [1] 读取当前硬件缓存的 MOS 状态 ===
* 这一步读取的不是实时硬件,而是 AFE 内部维护的 BmsData 缓存。
* 缓存由 status_parse() 每次解析 SYS_STAT 后更新,所以反映
* "最近一次 I2C 读到的硬件状态",不是 "此刻硬件真实状态"。
* 这种 "软缓存" 是空间换时间,避免每次控制都先读一次 I2C。 */
uint8_t discharge_mos_status = BmsData.Regs.SYS_CTRL2.RegValueBits.DSG_ON;
uint8_t charge_mos_status = BmsData.Regs.SYS_CTRL2.RegValueBits.CHG_ON;
/* === [2] 故障检测 ===
* SYS_STAT 低 4 bit 是故障标志 (OCD/SCD/OV/UV)。
* 用 0x0F 掩码一次性检测 "是否有任何硬件故障",
* 避免 4 次 if 判断。注意位序在 TI 寄存器定义里
* OCD 在 bit0, SCD 在 bit1, OV 在 bit2, UV 在 bit3。 */
bool fault_active = (BmsData.Regs.SYS_STAT.RegValue & 0x0FU) != 0U;
/* === [3] ASW 期望的 MOS 状态 vs 硬件缓存 ===
* Get_ChargeMosCmd() 从 RTE 读 ASW 任务写入的命令 (0 或 1)。
* 与缓存的 charge_mos_status 比较,得出 "期望 ≠ 实际" 的判断。 */
bool mos_command_diverges = (Get_ChargeMosCmd() != charge_mos_status) ||
(Get_DischargeMosCmd() != discharge_mos_status);
/* === [4] 劫持安全检查 ===
* 以下 4 种情况绝对不能劫持进入 MOS_CTRL:
* (a) 已经在 MOS_CTRL — 避免自递归
* (b) 在 DEAL_DATA — 此状态正在解析刚读到的新数据,
* 写 SYS_CTRL2 会冲掉缓存
* (c) 在 INIT — 芯片还没配置好 PROTECT 寄存器,
* 写 SYS_CTRL2 可能触发误保护
* (d) 在 CONFIG — 同上
* 此外:fault_active 时硬件会自动清 MOS 位,
* 此时再写 SYS_CTRL2 也不会生效,要等故障清除 */
bool hijack_safe = (s_AfeState != AFE_STATE_MOS_CTRL) &&
(s_AfeState != AFE_STATE_DEAL_DATA) &&
(s_AfeState != AFE_STATE_INIT) &&
(s_AfeState != AFE_STATE_CONFIG) &&
!fault_active;
/* === [5] 触发劫持:插入一次 MOS_CTRL 写 ===
* 这是 "中断式状态机" 的核心:正常流程读到一半时,
* 检测到紧急命令,强制插入一段特殊处理,处理完回到主线。
* 类似 µC/OS-II 的 "任务级中断" 思想。 */
if (mos_command_diverges && hijack_safe) {
s_AfeState = AFE_STATE_MOS_CTRL;
index_state = AFE_STATE_MOS_CTRL;
}
// ... (下方 switch 进入 AFE_STATE_MOS_CTRL case, 见 10.7)
}
设计决策说明:
决策 | 替代方案 | 选择理由 |
|---|---|---|
用缓存而非实时读 I2C | 每次先 I2C 读 SYS_CTRL2 | 省 1 次 I2C 事务 (~3ms) + 简化代码 |
4 种状态全部禁止劫持 | 只禁止 DEAL_DATA | 实战中在 INIT/CONFIG 写 SYS_CTRL2 会触发不可预测行为 |
故障时禁止劫持 | 让 MOS 命令覆盖故障保护 | 硬件优先级最高,软件写也无效,不如不浪费 I2C |
劫持完回到 READ_STATUS | 回到原状态 | READ_STATUS 总会刷新缓存,下次判断时 hijack 自然解除 |
常见误区:
- ❌ 在 DEAL_DATA 中也允许劫持:会丢失刚解析的电压/温度数据,且缓存会被写动作污染
- ❌ 劫持后不更新缓存:导致下一轮判断仍然认为 "命令 ≠ 实际",陷入无限劫持循环
- ❌ 故障状态下强制写 MOS:BQ76920 硬件的故障保护优先级最高,写了也没用,浪费 I2C 周期
10.14.2 非抢占式调度器的时间基设计
源文件: Source/BSW/OS/src/Os.c,第 273-311 行(节选 Os_TimerTick)
/* === [1] ISR 入口:每 100µs 由硬件定时器触发 ===
* 这是整个调度器的 "心脏",执行时间必须 < 100µs。
* 实测 4 任务时 < 10µs,10 任务时 < 30µs,仍有充足余量。 */
void Os_TimerTick(void)
{
uint16_t i;
uint16_t periodTicks;
/* === [2] 全局心跳 + RTE 同步 ===
* s_OsTickCount 是 OS 内部诊断计数;Set_SystemTick() 同步
* 到 RTE 供 ASW 任务 (比如 "距离上次唤醒 5s 后进入休眠") 观察。
* 注意:s_OsTickCount 是 32 位 0.1ms 单位,约 4.97 年溢出,
* 在嵌入式产品生命周期内可接受。 */
s_OsTickCount++;
Set_SystemTick(s_OsTickCount);
/* === [3] BSW 任务表更新(单层循环)===
* 核心算法:每个任务有 counter 倒计时,到 0 就置 ready 并重装。
* 这是经典的 "软件定时器数组" 实现:
* - 不用链表是因为任务数固定 (≤8),数组访问更快
* - 不用堆排序是因为调度器是 round-robin,固定顺序
* - counter 单位是 0.1ms tick (1ms = 10 ticks) */
for (i = 0U; i < s_BswTaskCount; i++) {
if (s_BswTaskTable[i].counter == 0U) {
periodTicks = OS_MS_TO_TICKS(s_BswTaskTable[i].periodMs);
s_BswTaskTable[i].ready = true;
s_BswTaskTable[i].counter = periodTicks; /* 重装 */
} else {
s_BswTaskTable[i].counter--; /* 递减 */
}
}
/* === [4] ASW 任务表更新(同样的算法,独立计数)===
* BSW 和 ASW 用两套独立 counter 是为了:
* - BSW 任务 (CDD/LCD) 启动早,先入表先运行
* - ASW 任务 (业务逻辑) 启动晚,offset 可自由配置
* - 调度时 BSW 在前 ASW 在后,硬件服务先于业务决策 */
for (i = 0U; i < s_AswTaskCount; i++) {
if (s_AswTaskTable[i].counter == 0U) {
periodTicks = OS_MS_TO_TICKS(s_AswTaskTable[i].periodMs);
s_AswTaskTable[i].ready = true;
s_AswTaskTable[i].counter = periodTicks;
} else {
s_AswTaskTable[i].counter--;
}
}
}
/* === [5] OS_MS_TO_TICKS 宏 (Os.h 定义) ===
* 这是关键的时间换算:
* periodMs=100ms → periodTicks=999 (不是 1000!)
* 因为 counter 从 offset 开始递减到 0 才触发,
* 周期是 "从 0 数到 999 再到 0" 共 1000 步 (100ms @ 0.1ms tick)。
* 若用 1000 作为初值,会变成 100.1ms (1 个 tick 偏长)。 */
#define OS_MS_TO_TICKS(ms) ((uint16_t)((ms) * 10U - 1U))
设计决策说明:
决策 | 替代方案 | 选择理由 |
|---|---|---|
0.1ms tick (100µs) | 1ms tick | 10ms 周期的 AFE 任务相位分辨率 10x 提升,错峰更细 |
counter 递减到 0 触发 | 累加到 period 触发 | 0 检测只需 |
双任务表 | 单任务表 | BSW 静态可裁剪 (不同项目配不同 BSW),ASW 用户动态注册 |
立即重装 | 调度执行时再重装 | 重装是 ISR 内的常数操作,避免 dispatcher 边界 case |
| 只用 | ISR 和 dispatcher 在不同上下文, |
为什么不用 RTOS(如 FreeRTOS):
- 资源占用:FreeRTOS ROM 约 6-10 KB,本调度器仅 ~1 KB
- 可预测性:非抢占式 → 无任务切换开销抖动
- 简化调试:没有任务切换时的寄存器快照,单步断点更清晰
易错点:
- ❌ 把
OS_MS_TO_TICKS(100)当成 1000 → 实际是 999,初学者会困惑 - ❌ 在 ISR 内调用 printf/log → 100µs 内根本来不及,会破坏调度节拍
- ❌ offset 设错 → 同周期任务同时触发,CPU 瞬间被两个任务挤占
- ❌ periodMs 设成 0 →
OS_MS_TO_TICKS(0) = 65535(uint16 下溢),每 6.5 秒才触发一次
10.14.3 RTE 信号的"读多写少"模式
源文件: Source/RTE/RTE.h,第 271-280 行(节选 Get_CellVoltage / Set_CellVoltage)
/* === [1] 数组 Get 函数 (uint16_t Get_CellVoltage(uint32_t index)) ===
* 这是 RTE 信号访问的核心模式。"Get_X(idx)" 模式的关键设计:
* - 边界检查:index >= 20 时返回 0,**绝不**越界访问
* - 内联展开:static inline 让编译器在调用处直接展开,
* 没有函数调用开销 (call/ret/压栈/弹栈)
* - 单一职责:只做"读+边界检查",不附加日志/断言,
* ASW 频繁调用时不会拖慢热路径 */
static inline uint16_t Get_CellVoltage(uint32_t index)
{
if (index >= 20) { return (uint16_t)0; } /* 边界保护 */
return CellInfo.CellVoltage[index];
}
/* === [2] 数组 Set 函数 (void Set_CellVoltage(uint32_t index, uint16_t value)) ===
* 对称设计:Set 也做边界检查,超界时**静默丢弃**而非崩溃。
* 这种 "fail silently" 在汽车级嵌入式很常见:
* - 防止 ASW 写错索引把 RAM 写花
* - 不抛异常不返回错误码 (嵌入式中异常处理昂贵)
* - 但需要静态分析工具 (MISRA-C) 强制约束 ASW 索引合法性 */
static inline void Set_CellVoltage(uint32_t index, uint16_t value)
{
if (index >= 20) { return; }
CellInfo.CellVoltage[index] = value;
}
/* === [3] 标量 Get 函数 (uint16_t Get_CellVoltageMax(void)) ===
* 标量 (非数组) 信号比数组简单一档:
* - 不需要 index 参数
* - 不需要边界检查
* - 但仍然 static inline (因为整个 RTE.h 头文件包含频繁) */
static inline uint16_t Get_CellVoltageMax(void)
{
return CellInfo.CellVoltageMax;
}
/* === [4] 标量 Set 函数 (void Set_CellVoltageMax(uint16_t value)) ===
* 注意:标量 Set 没有"原子性"问题 (单条 MOV 指令),
* 但数组 Set 在 32 位 MCU 上需要 2 条指令 (H + L 字节),
* 中间可能被中断打断 → 这就是为什么 CDD 在 DEAL_DATA 中
* 先写完所有 RTE 信号再开中断 (临界区)。 */
static inline void Set_CellVoltageMax(uint16_t value)
{
CellInfo.CellVoltageMax = value;
}
设计决策说明:
决策 | 替代方案 | 选择理由 |
|---|---|---|
| 普通函数 | 零调用开销,C99 标准,编译器跨平台支持 |
数组越界返回 0/丢弃 | 触发 assert / HardFault | 嵌入式产品不能让一个非法索引搞死系统 |
单一信号单一函数 | 宏 | 函数有类型检查,宏容易踩坑 (#副作用) |
不在 Get/Set 中加锁 | 加临界区保护 | RTE 是单线程访问 (非抢占调度),无需锁 |
Get/Set 不做日志 | 加访问日志 | 频繁调用拖慢热路径,需要时单独包装 |
与"全局裸变量"对比:
// ❌ 反面教材: ASW 直接访问全局变量
extern uint16_t CellVoltage[20]; // ASW 必须知道底层数据结构
void MyTask(void) {
if (CellVoltage[0] > 3500) { // 没有边界检查
CellVoltage[0] = 3500; // ASW 直接修改
}
}
// ✅ 正确做法: 通过 RTE 访问
void MyTask(void) {
uint16_t v = Get_CellVoltage(0); // 边界保护
if (v > 3500) {
Set_CellVoltage(0, 3500); // 写入受控
}
}
RTE 自动化生成:
RTE_Config.yaml 是单一数据源 (Single Source of Truth):
signals:
- name: CellVoltage
type: uint16[20] # 类型 + 数组长度
direction: CDD_TO_ASW
description: "5 节电池电压 (mV)"
- name: PackRealSoc
type: uint16
direction: ASW_INTERNAL
description: "SOC (0-10000, 0.01% 分辨率)"
执行 python RTE_Gen.py 后自动产出:
RTE.h中的 typedef struct(数据结构)RTE.h中的 static inline Get/Set(访问接口)RTE.c中的全局变量定义(实际存储)
优势:
- ✅ 增加新信号只需在 yaml 加一行,重跑脚本
- ✅ ASW 代码不需要修改(接口名一致)
- ✅ 类型/范围/方向都在 yaml 集中维护
- ✅ 易于对接 MISRA-C 静态分析工具
易错点:
- ❌ 手动修改生成的
RTE.h→ 下次跑脚本会被覆盖,注释明确警告过 - ❌ 在 Get/Set 内调用耗时函数 (printf/malloc) → 破坏 RTE 访问的"零开销"承诺
- ❌ CDD 写一半时 ASW 读取 → 数据不一致。解决:在 CDD 写操作前后短暂关中断 (临界区 ~1µs)
- ❌ 跨层访问 (BSW 任务直接读 RTE 而绕过 CDD) → 架构违规,应通过 CDD 接口
10.14.4 代码风格与审查规范
1. 命名约定:
元素 | 风格 | 示例 |
|---|---|---|
函数 | PascalCase |
|
变量 (局部) | snake_case |
|
变量 (全局) | s_ 前缀 |
|
常量/宏 | UPPER_SNAKE |
|
类型 | _t 后缀 |
|
枚举值 | 类型前缀 |
|
2. 注释规范(重要!比赛评审会看):
- 每个 .c/.h 文件 顶部必须有 Doxygen 风格文件头(用途、作者、版本)
- 每个函数 必须有 Doxygen 注释(
@brief@param@return) - 复杂逻辑 用
/* === [1] === */块注释分小节 - TODO 必须带优先级:
TODO(P3):表示优先级 3 的待办 - 禁止单行
//注释(MISRA-C 2012 强制规则)
3. 项目级强制规则(CI 检查):
# arch_check.py (CI 流水线运行)
import re
import sys
violations = []
for fname in glob('Source/ASW/**/*.c'):
with open(fname) as f:
code = f.read()
# 规则 1: ASW 禁止 #include "stm32f1xx_hal.h"
if 'stm32f1xx_hal.h' in code:
violations.append(f'{fname}: ASW 包含 HAL 头 (违规)')
# 规则 2: ASW 禁止访问 BSW 全局变量
if re.search(r'\bAfeManage\.\w+', code): # 不严谨, 实际用 RTE API
violations.append(f'{fname}: ASW 直接访问 BSW 变量 (违规)')
# 规则 3: ASW 禁止调用 HAL_Delay / printf (除调试接口)
if re.search(r'\bHAL_Delay\s*\(', code):
violations.append(f'{fname}: ASW 调用 HAL_Delay (违规)')
if violations:
for v in violations:
print(v)
sys.exit(1)
4. 单元测试覆盖:
模块 | 测试方式 | 覆盖率 |
|---|---|---|
RTE Get/Set | 编译期静态检查 + 运行时 boundary test | 100% |
AFE 状态机 | 主机仿真 (PC 上跑 AfeManage.c) | 90% |
SOC 算法 | Simulink MIL/SIL 闭环 | 100% |
BQ76920 配置 | 硬件在环 (HIL) + 读回校验 | 100% |
Os 调度器 | 主机仿真 + GPIO 时序测量 | 95% |
附录 A:关键源文件索引
路径 | 功能 |
|---|---|
| ASW 入口 |
| Monitor 任务(100 ms) |
| SOC 任务(250 ms) |
| HMI 任务(200 ms) |
| 均衡任务(500 ms) |
| SOC 算法(Simulink 生成) |
| CDD 主状态机 |
| AFE 状态机管理 |
| BQ76920 寄存器协议 |
| 调度器实现 |
| RTE 信号定义 |
| RTE Get/Set 实现 |
| RTE 信号配置表(单一数据源) |
| RTE 代码自动生成脚本 |
附录 B:BQ76920 关键寄存器
寄存器 | 地址 | 功能 |
|---|---|---|
| 0x00 | 系统状态(OV / UV / OCD / SCD / CC_READY) |
| 0x01 | 均衡控制(bit0-4 = CELL1-5) |
| 0x02 | 系统控制(ADC 使能、温度选择) |
| 0x04 | 系统控制(CHG/DSG MOS、CC 使能) |
| 0x05 | SCD 阈值/延迟配置 |
| 0x06 | OCD 阈值/延迟配置 |
| 0x07 | OV/UV 延迟配置 |
| 0x14 ~ 0x18 | 单体电压高字节 |
| 0x32 / 0x33 | 库仑计数值 |
| 0x34 / 0x35 | 电池包总电压 |
| 0x36 / 0x37 | 温度 ADC |
| 0x4C | OV 跳变阈值 |
| 0x4D | UV 跳变阈值 |
| 0x3A | ADC 偏置 |
| 0x3B | ADC 增益 |
附录 C:术语表
术语 | 英文 | 说明 |
|---|---|---|
BMS | Battery Management System | 电池管理系统 |
AFE | Analog Front End | 模拟前端芯片 |
SOC | State of Charge | 荷电状态(剩余容量百分比) |
SOH | State of Health | 健康状态(容量保持率) |
SOP | State of Power | 功率状态估计 |
OCV | Open Circuit Voltage | 开路电压 |
CDD | Complex Device Driver | 复杂设备驱动 |
RTE | Runtime Environment | 运行时环境 |
ASW | Application Software | 应用软件层 |
BSW | Basic Software | 基础软件层 |
MCAL | Microcontroller Abstraction Layer | 微控制器抽象层 |
MBD | Model-Based Design | 基于模型的设计 |
OV / UV | Over-Voltage / Under-Voltage | 过压 / 欠压 |
OCD | Over-Current Discharge | 放电过流 |
SCD | Short-Circuit Discharge | 短路放电 |
OT / UT | Over-Temperature / Under-Temperature | 过温 / 欠温 |
NTC | Negative Temperature Coefficient | 负温度系数热敏电阻 |
CAN | Controller Area Network | 控制器局域网 |
UDS | Unified Diagnostic Services | 统一诊断服务 |
DTC | Diagnostic Trouble Code | 诊断故障码 |