东方蓄能项目解析
面向从 Qt 上位机转入嵌入式开发的学习笔记。
本文根据源码静态分析整理。当前没有连接实物设备,也没有用串口工具、CAN 分析仪抓取真实报文;“已确认”表示能从代码直接确认,“待验证”表示需要结合原理图、协议文档或实机验证。
1. 项目先看懂什么
这是一个运行在 NXP S32K144 上的热管理控制器(TMS)软件,同时完成:
采集传感器和开关量
↓
把输入交给 Simulink 控制算法
↓
计算压缩机、水泵、风机、加热器目标
↓
通过PWM、GPIO、CAN、Modbus输出
↓
读取执行器反馈和外部控制命令
↺
最重要的总链路:
硬件输入 → BSW驱动 → APP_IO接口 → Simulink算法
算法输出 → APP_IO接口 → 执行器/通信 → 外部设备
- BSW(Basic Software)负责调度、驱动和通信;
- APP 目录主要是 Simulink 自动生成的控制代码;
APP_IO.c是硬件/通信变量和算法变量之间的适配层;- Modbus、CAN 是两种并行存在的通信方式。
2. 工程目录和源码副本
当前用于分析的源码副本:
D:\002Jack.cheng\project\index_08_dongfangxuneng\DFXN01\TMS_SW_Master_20260306\script\S32K144_TMS\FreeRTOS_TAD_logs\DFXN01
正式修改前必须确认 S32DS 实际编译的是哪一份 Sources。工程中有日志、备份和其他平行工程,改错目录会出现“编译成功但程序没变化”。
TMS_SW_Master_20260306/
├─ models/ Simulink模型,例如 TMS_SLM.slx
├─ libraries/ Simulink库
├─ script/ MATLAB生成、编译、发布脚本
├─ work/ 代码生成中间文件
├─ script/S32K144_TMS/
│ └─ DFXN01/
│ ├─ Sources/APP/ Simulink生成C/H
│ ├─ Sources/BSW/ 调度、IO、Modbus、CAN
│ ├─ Sources/CCP/ CCP标定通信
│ ├─ Sources/main.c 程序入口
│ ├─ Generated_Code/ 外设配置
│ ├─ driver/ 项目底层驱动、诊断支持
│ └─ SDK/ NXP SDK驱动
├─ release/ bin/elf/map/a2l发布物
└─ backup/ 带时间戳的归档
3. 启动、主循环和任务
main()的初始化顺序大致是:
TMS_SLM_initialize()
→ 时钟、缓存、电源、内存、GPIO
→ 看门狗、CAN、定时器、队列
→ FlexCAN邮箱
→ BSW_Init(ADC/PWM/LPIT/UART/CAN)
→ APP_Initial()
→ eMB_Init()
→ 进入无限循环
主循环核心:
for (;;)
{
BSW_Schedule();
MB_Master_Poll();
if (machineStat == MACHINE_STAT_CONTROLLED_BY_BMS)
eMBPoll();
else
{
MBResetTimerTimeOut();
Modbus_clear();
}
}
含义:BSW_Schedule()执行周期任务;MB_Master_Poll()处理LPUART2方向的Modbus主机;只有控制权属于BMS时,才处理LPUART1方向的Modbus从机。
LPIT通道0周期为1000 us(1 ms)。LPIT_IRQHandler()只调用BSW_TaskTimer()设置任务标志,复杂工作在主循环执行。
| 周期 | 函数 | 主要内容 |
|---|---|---|
| 每轮 | BSW_Task0ms | CCP后台、CAN接收入口 |
| 10ms | BSW_Task10ms | 读输入、ADC、Inputs()、TMS_SLM_step()、Outputs() |
| 100ms | BSW_Task100ms | CAN_Rx_Tx()、MB_Master_main()、保存标定、CCP采集 |
| 1ms | BSW_Task1ms | 计时和通信超时维护 |
最核心的控制周期:
每10ms:采集 → 算法 → 输出
每100ms:外部设备通信轮询
每1ms:计时和任务标志
4. 输入、Simulink、输出
4.1 Inputs
APP_IO.c的Inputs()完成硬件到算法的适配:
Sens_ADOutWaterTemp = ADC1_SE1_PTA3;
Sens_ADInWateTemp = ADC1_SE0_PTA2;
Sens_ADHiPrs = ADC1_SE5_PTC7;
Sens_ADLoPrs = ADC1_SE12_PTA15;
链路是:
ADC通道 → Read_ADValue()
→ ADC1_SE1_PTA3 → Inputs()
→ Sens_ADOutWaterTemp → Simulink SigIn/滤波
Inputs()还读取液位、强制启动等数字量,并包含液位延时和通信故障自动控制。
4.2 Simulink
模型:
models\TMS_SLM.slx
生成代码:
Sources\APP\TMS_SLM.c/.h
Sources\APP\SigIn.c
Sources\APP\TMSCtl.c
Sources\APP\OBD.c
Sources\APP\Global.c/.h
入口:
TMS_SLM_initialize();
TMS_SLM_step();
TMS_SLM_step()内部可理解为:
SigIn() 输入换算和滤波
OBD() 故障诊断和去抖
TMSCtl() 控制决策、转速和占空比
模型配置为固定步长离散求解,FixedStep=0.01,即10ms。不要长期直接修改APP生成文件,应修改.slx后重新生成。
4.3 Outputs
Outputs()顺序:
_deb_val()
→ APP_SetInfoToBMS()
→ APP_SetInfoToHMI()
→ HMI控制权处理
→ 压缩机适配
→ 风机适配
→ GPIO/继电器输出
压缩机目标转速链:
TMS_nCompSpd
→ APP_SetInfoToCompressor_main()
→ mc_fr_set = TMS_nCompSpd / TMS_nCompMax_C * 10000
→ Modbus主机或对应执行器通信
5. 出水温度完整链路
ADC原始值
→ Sens_ADOutWaterTemp
→ SigIn换算和一阶滤波
→ ADC_tOutWaterTempFlt
→ OBD传感器故障去抖
→ _deb_val()
→ _deb_temp_water_out
出水传感器故障且回水正常时,_deb_val()按工作模式用回水温度±2℃替代;高压传感器故障固定3999,压缩机温度故障固定299(代码单位)。
之后分成三条对外链路:
_deb_temp_water_out
├─ Reg6 → BMS Modbus读取
├─ hmiRegs.chushui_wendu → HMI Modbus主机写入
└─ TX_BTMS_Data[1] = 温度 + 40 → BMS CAN上报
通信值不一定等于算法内部值,必须查协议的比例和偏移。
6. 通信总览
| 对端 | 接口 | 本机角色 | 代码 | 备注 |
|---|---|---|---|---|
| BMS | LPUART1 + RS-485 | Modbus从机 | cus_modbus.c | BMS问,TMS答 |
| 压缩机/风机/HMI | LPUART2 + RS-485 | Modbus主机 | cus_modbus_CAN.c | TMS主动轮询 |
| BMS | CANCOM2 | CAN节点 | FlexCAN.c | 周期8字节报文 |
| DCDC/主从TMS/HMI | CANCOM1 | CAN节点 | FlexCAN.c | 多套自定义ID |
| 标定工具 | CANCOM1 | CCP从设备 | CCP/ | DAQ和标定 |
| 诊断/刷写工具 | CAN | 诊断节点 | CanTp、UDS | ISO-TP/UDS |
7. Modbus RTU从机:BMS方向
7.1 理论
UART:MCU逐字节收发
RS-485:电气层和半双工总线
Modbus RTU:地址、功能码、寄存器和CRC格式
LPUART1配置为9600、8N1。TMS从机地址为1,支持0x03、0x04、0x06、0x10。
WATERCOOL配置下:
协议地址0x0000,长度90,mode=0:只读区
协议地址200,长度30,mode=1:可读写区
因此地址0~89对应Reg1~Reg90,地址200~229对应Reg91~Reg120;Reg91不是协议地址90。
7.2 一帧读取Reg6
Modbus 常见功能码:
01 → 读线圈 Coil
02 → 读离散输入
03 → 读保持寄存器 Holding Register
04 → 读输入寄存器 Input Register
05 → 写单个线圈
06 → 写单个保持寄存器
10 → 写多个保持寄存器
按当前地址权限代码,Reg6索引为5,属于只读区,使用0x04读取:
请求:01 04 00 05 00 01 21 CB
01 从机地址1
04 读取输入寄存器
00 05 起始地址5,对应Reg6
00 01 读取1个寄存器
21 CB CRC,低字节在前
接收过程:
LPUART1单字节接收
→ LPUART1_RX_ISR()
→ xMBRTUReceiveFSM()
→ 字节进入Rx_Buf[0..]
→ 每字节重启t3.5
→ 约4.01ms静默
→ LPIT_CH1_IRQHandler()
→ EV_FRAME_RECEIVED
→ 主循环eMBPoll()
Rx_Buf是256字节接收缓冲区。接收状态机从STATE_RX_IDLE进入STATE_RX_RCV,每次回调只安排下一个字节的存储位置。
7.3 CRC、地址、功能码
eMBPoll()收到完整帧后:
- 对完整帧计算
MB_CRC16(),正确帧重新计算结果为0; - 检查地址是否为广播0或本机地址1;
- 校验通过后投递
EV_EXECUTE; MB_Execute()调用MB_Analyze_Execute()。
MB_Parse_Data()解析:
Code = Rx_Buf[1];
Addr = (Rx_Buf[2] << 8) | Rx_Buf[3];
Num = (Rx_Buf[4] << 8) | Rx_Buf[5];
本例为Code=0x04、Addr=5、Num=1。MID_Adr_judge()检查范围、权限并返回idx=5。
7.4 生成回复
APP_LUART_ReadReg(5,1)先调用APP_SetInfoToBMS()刷新:
Reg6 = _deb_temp_water_out;
假设Reg6=25(0x0019),回复是:
01 04 02 00 19 78 FA
生成:
Tx[0] = 地址
Tx[1] = 功能码
Tx[2] = 数据字节数
Tx[3] = 寄存器高字节
Tx[4] = 寄存器低字节
Tx[5..6] = CRC低、高字节
发送时PTE9=1切RS-485到发送方向,调用LPUART_DRV_SendData(INST_LPUART1,...)。发送完成中断再切回接收。
7.5 BMS写入
0x06:写一个寄存器并回显请求
0x10:解析多个寄存器并回显地址、功能码、起始地址和数量
典型控制链:
BMS写Reg91/Reg92/Reg96
→ APP_LUART_WrtieReg()
→ APP_GetInfoFromBMS()
→ BMS_stModeReq/BMS_stHiVoltgState/BMS_tSetTemp
→ 下一次10ms控制算法读取
非法功能码返回原功能码|0x80 + 异常码01;非法地址/权限返回异常码02;CRC错误或地址不是本机时通常静默丢弃。
8. Modbus RTU主机:LPUART2方向
地址1:设备1/主压缩机
地址2:设备2/副压缩机
地址4:风机
地址64:HMI
MB_Master_main()在STC_IDLE时轮询字段表:
mode=0 → 0x03读取
mode=2且size=1 → 0x06写单寄存器
mode=2且size>1 → 0x10写多寄存器
它只选择命令并调用MB_Master_CMD();真正加CRC、切方向、用LPUART2发送的是MB_Master_Poll()。
STC_IDLE
→ MB_Master_main选择命令
→ STC_FRAME_SENT
→ MB_Master_Poll加CRC并发送
→ STC_WAIT
→ 收到回复后STC_EXECUTE
→ MB_Master_Execute解析
→ STC_IDLE
虽然缓存名叫CAN_Tx_Buf、文件名叫cus_modbus_CAN.c,实际调用的是LPUART_DRV_SendData(INST_LPUART2,...),所以这里是RS-485 Modbus。
HMI Modbus表:
0x1040,30个,WO:TMS写HMI状态
0x105E,1个,RO:读取HMI控制请求
0x105F,4个,WO:下发当前BMS控制参数
0x1063,4个,RO:读取HMI用户参数
0x1067,1个,WO:控制权应答
9. CAN通信
9.1 基础
Modbus:报文中有从机地址,通常一问一答,软件CRC
CAN:报文由ID标识,硬件完成CRC/ACK/仲裁,经典CAN最多8字节
主要ID:
| 用途 | 发送ID | 接收ID | 帧类型 |
|---|---|---|---|
| BMS | 0x18FFC13A | 0x18FF45F4 | 扩展 |
| DCDC | 0x10200010 | 0x10000010 | 扩展 |
| 压缩机(当前逻辑禁用) | 0x236 | 0x235 | 标准 |
| 主从TMS | 0x111111 | 0x222222/0x333333 | 扩展 |
| HMI | 0x18FFF545 | 0x18FF45F5 | 扩展 |
| CCP | CCP_DTO_ID | CCP_CRO_ID | 扩展 |
dataInfo_EXT/STD都配置为8字节、非CAN-FD。邮箱MB是硬件缓冲槽,不是Modbus寄存器。
9.2 BMS CAN
BMS接收ID为0x18FF45F4,解析:
Byte0 bit0~1:模式请求
Byte0 bit2~3:高压请求
Byte0 bit6~7:高压继电器状态
Byte1~2:高压,小端16位
Byte3:温度,raw×0.5
Byte4:设定温度,raw-40
Byte5:湿度,raw×0.5
TMS发送0x18FFC13A:
Byte0:工作模式和高压状态
Byte1:出水温度+40
Byte2:回水温度+40
Byte3:高压/20
Byte4:低压×50
Byte7:故障等级高2位+故障码低6位
CAN_Rx_Tx()在100ms任务中被调用,但Can_BMS_cnt>=9才处理一次,实际约每1秒一次;源码注释中的周期需要实机确认。
9.3 主从TMS
TMS向从机发送0x111111,包括模式、高压、电压、设定温度、压力和环境值;从机用0x333333返回温度、转速和压力。解析后的slv_info_comp_spd等变量会进入BMS上报寄存器。
9.4 HMI自定义CAN
HMI → TMS:0x18FF45F5
TMS → HMI:0x18FFF545
应用帧:idx + 6字节data + lrc。
idx bit7:写入控制参数
idx bit6:读取时选择控制数据或状态数据
idx bit0~3:6字节分片编号
lrc:idx和6个data字节相加的低8位
写入链:
HMI帧 → 校验 → tms_ctrl → 限幅 → 回写BMS控制变量
读取链:
tms_info/tms_ctrl → 按idx取6字节 → 加LRC → 回复HMI
9.5 当前启用和禁用分支
- BMS CAN、主从TMS CAN、HMI CAN代码处于启用路径;
- 压缩机CAN收发整段位于
#if 0,当前主要走Modbus; - 水泵CAN位于
#if 0; - DCDC代码执行,但故障标志随后被强制清零,需核对;
- CCP使用CANCOM1邮箱8/9;
- UDS/ISO-TP使用CAN完成诊断和刷写,不要和业务CAN混为一谈。
10. CCP、UDS和后台通信
每轮主循环的BSW_Task0ms()调用:
CAN_receive_msg();
ccpBackground();
ccpBootReceiveCro();
ccpCommand();
ccpSendCallBack();
这是标定、DAQ和CCP命令链。主循环的1ms节拍还调用CanTpMainFun()推进ISO-TP,10ms节拍调用看门狗和UDS处理。它们属于诊断/刷写链路,不是温度和转速业务报文。
11. Simulink到固件的生成流程
script\CodeGenerate.m主要做:
rtwbuild("TMS_SLM")
→ work/code/TMS_SLM_ert_rtw生成C/H/A2L
→ 清空目标工程Sources/APP
→ 复制生成C/H
→ 调用build.bat
→ 复制bin/elf/map/a2l到release和backup
正确修改流程:
修改models/TMS_SLM.slx
→ MATLAB重新生成
→ 检查Sources/APP差异
→ 编译
→ 下载和验证
不要把生成后的APP/TMS_SLM.c当作长期手写源码。
12. 后续修改重点
| 需求 | 首先看 |
|---|---|
| 改制冷/制热/转速算法 | models/TMS_SLM.slx |
| 改ADC、GPIO到算法变量 | APP_IO.c、BSW_interface.c |
| 新增BMS Modbus上报量 | APP_SetInfoToBMS()、cus_modbus.c/.h |
| 新增BMS控制量 | APP_GetInfoFromBMS()、可写寄存器区 |
| 改压缩机/风机Modbus | cus_modbus_CAN.c、设备字段表 |
| 改HMI | hmi.c/.h、HMI字段表 |
| 改CAN信号 | FlexCAN.c的ID、Data字节和位域 |
| 改执行周期 | BSW_Task.c、LPIT配置 |
| 改波特率/引脚 | Generated_Code和Processor Expert |
| 改标定/诊断 | CCP/、CAN-TP、UDS/Boot相关目录 |
每次改协议前先写一行协议表:
对端 | 接口 | 方向 | 角色 | 地址/CAN ID | 功能码
字节/位 | 类型 | Factor | Offset | 周期 | 超时行为
13. 没有设备时的验证方法
Modbus BMS方向
用USB转RS-485上位机模拟主机,读取:
地址5/功能码04:Reg6出水温度
地址6/功能码04:Reg7回水温度
地址200以后:控制参数读写
检查CRC、大小端、异常码、控制权条件和超时行为。
Modbus主机方向
让上位机模拟地址1、2、4、64的从机,观察TMS发出的地址、功能码、起始地址、数量、写入值和轮询顺序。
CAN方向
USB-CAN可模拟:
0x18FF45F4:BMS控制帧
0x18FF45F5:HMI请求帧
0x333333:从机反馈帧
同时监听:
0x18FFC13A:TMS给BMS
0x18FFF545:TMS给HMI
0x111111:TMS给从机
14. 已确认、待验证和风险
已从代码确认
- 10ms任务执行“采集→算法→输出”;
- BMS有LPUART1 Modbus和CAN两条链路;
- LPUART2承担压缩机、风机、HMI方向的Modbus主机;
Reg6由_deb_temp_water_out提供;- BMS CAN出水温度使用“温度+40”;
- HMI同时存在Modbus和自定义CAN;
- APP代码由
TMS_SLM.slx生成。
待文档、原理图或实机确认
- 两路CAN实际接到哪些物理设备;
- CAN实际波特率;
- RS-485的A/B、共地、PTE方向脚和终端电阻;
- 正式寄存器地址、单位和符号类型;
- 注释周期与实际周期;
FLEXCAN_DRV_Receive()异步接收的正确使用方式;- 当前IDE真正编译的源码副本。
修改前注意
main.c调用了一次FlexCAN_Initial(),BSW_Init()内部又调用一次,存在重复初始化风险。- BMS CAN由
Can_BMS_cnt分频,实际约1秒一次,注释写有100ms。 - DCDC和从机CAN故障标志在判断后又被直接清零,可能掩盖故障。
FLEXCAN_DRV_Receive()是非阻塞的,调用后立即读取缓冲区可能读到旧帧。hmi.c中#if 1/#else有重复实现,只有前者生效;旧状态机在#if 0中。lpuartComm.c也有被#if 0屏蔽的旧回调,不要误读。- 变量和缓存有
CAN/Modbus历史命名混用,判断通信类型要看实际驱动调用。
15. 从Qt上位机转嵌入式的排查方法
Qt按钮/定时器 ≈ MCU任务调度
QSerialPort ≈ LPUART驱动和中断
QTimer ≈ LPIT定时器和任务标志
信号槽 ≈ 中断回调、事件和状态机
协议解析类 ≈ cus_modbus.c / FlexCAN.c
界面状态 ≈ 控制、故障和通信状态变量
遇到“设备不动作”,按层排查:
任务有没有执行?
→ 输入变量有没有更新?
→ Simulink输出是否合理?
→ APP_IO是否正确映射?
→ 报文是否组对?
→ 驱动是否真正发送?
→ 总线电气是否正常?
→ 对端是否按同一协议解析?
→ 执行器是否有反馈?
16. 术语速查
| 术语 | 含义 |
|---|---|
| BSW | 基础软件层,调度、驱动、通信 |
| APP_IO | 应用与硬件/通信之间的适配层 |
| Rx/Tx | 接收/发送 |
| Mailbox/MB | CAN硬件消息缓冲槽 |
| CAN ID | CAN报文标识符和仲裁依据 |
| Factor/Offset | 比例/偏移 |
| t3.5 | Modbus RTU判断帧结束的静默时间 |
| CRC | 报文错误校验 |
| LRC | HMI应用层累加校验 |
#if 0 | 编译时排除,不是当前运行逻辑 |
17. 一句话总结
10ms任务负责控制算法,100ms任务负责外部设备轮询,
APP_IO负责变量翻译,Modbus负责寄存器通信,CAN负责ID和字节通信,
Simulink模型是控制逻辑源头,Generated Code是编译产物,
实机联调前必须用协议表、抓包和硬件测量把静态分析结论逐项验证。