news 2026/6/11 1:17:51

BCU 平台 Modbus 主机功能开发:液冷机组 消防传感器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BCU 平台 Modbus 主机功能开发:液冷机组 消防传感器

BCU 平台 Modbus 主机功能开发:液冷机组 & 消防传感器

继 RS485 驱动适配(THVD1406 → ISO3082)完成后,BCU 需要在已有从机功能基础上新增两路 Modbus 主机,分别对接液冷机组和消防传感器。本文记录完整的设计与实现过程。


一、项目背景

1.1 当前系统状态

BCU 基于 RK3568 平台,已有 4 路 RS485,全部配置为 Modbus从机

串口设备角色协议用途
串口 1/dev/ttyS3从机MODBUS_RTUEMS 通信
串口 2/dev/ttyS4从机MODBUS_RTUHMI 通信
串口 3/dev/ttyS5从机待改为液冷主机
串口 4/dev/ttyS9从机待改为消防主机

1.2 新增需求

  • 液冷机组(串口 3,/dev/ttyS5):BCU 作为 Modbus 主机,周期性读取液冷运行状态、故障信息、运行参数,支持远程开关机和参数设置
  • 消防传感器(串口 4,/dev/ttyS9):BCU 作为 Modbus 主机,每 200ms 轮询 5 个复合探测器(烟雾/温度/CO/VOC/H₂),同时读取消防主机状态和报警阈值

1.3 芯片选型

两路主机同样使用 ISO3082 隔离型 RS485 收发器,DE/RE 通过独立 GPIO 控制方向:

串口UARTDE GPIOchip/line全局编号
/dev/ttyS5UART5GPIO3_C1chip3, line17113
/dev/ttyS9UART9GPIO3_B5chip3, line13109

二、协议分析

2.1 消防传感器协议

  • 物理层:RS485,9600bps,8N1,MODBUS RTU
  • 主从关系:BMS 为主机,消防控制器为从机(默认地址 1)
  • 功能码:0x03(读保持寄存器)、0x06(写单个寄存器)
  • 寄存器分布
寄存器地址 内容 说明 ────────── ────────────────────── ────────────────── 0 ~ 54 复合探测器 1~5 每个占 11 个寄存器 偏移 0 烟雾浓度值 uint16 偏移 1 温度值 uint16, ×0.1℃ 偏移 2 一氧化碳浓度 uint16, ppm 偏移 3 VOC 浓度 uint16, ppm 偏移 4 氢气浓度 uint16, %LEL 偏移 5 通讯状态 0=正常, 1=故障 偏移 6~10 烟雾/温度/CO/VOC/H₂ 报警 0=正常, 1=报警(锁定) 1000 ~ 1008 主机状态区域 报警/电压/输出/输入 1009 ~ 1015 可读写区域 复位/地址/阈值 探测器基地址 = (N - 1) × 11
  • 报警锁定:报警后保持,写 12345 到地址 1009 复位
  • 广播改地址:忘记地址时可用广播地址 0 写寄存器 1010

2.2 液冷机组协议

  • 物理层:RS485,9600bps,MODBUS RTU
  • 主从关系:BMS 为唯一主站,液冷为从站(默认地址 1)
  • 功能码:0x03(读)、0x06(写单个)、0x10(写多个)

关键寄存器:

地址参数类型说明
状态反馈 (0x0000~0x003F)
0x0000运行状态+模式U16低字节状态,高字节模式
0x0001开关量输入 DIU16 位
0x0002继电器输出 DOU16 位
0x0003供液温度S16×0.1℃
0x0004回液温度S16×0.1℃
0x000C水泵转速U16%
0x000D压缩机转速U16RPM
0x0012供水压力S16×0.1bar
0x0019~0x001D故障状态字 1~5U16 位
0x0020变频器故障代码U16
0x0022系统总故障等级U16
用户参数 (0x1000~0x1011)
0x1001工作模式U160 待机/1 制冷/2 制热/3 自循环/4 自动
0x1003制冷预设温度U160~55℃
0x1010RS485 地址U16可修改
控制 (0x0300)
0x0300开关机命令U16Bit0=开机, Bit1=关机, Bit2=报警复位
  • 故障等级:一级(停整机)、二级(停部分功能)、三级(仅报警)
  • 轮询策略:液冷 10s 一次,消防 200ms 一次

三、架构设计

3.1 整体架构

┌─────────────────────────┐ │ bcu_cfg.db │ │ 串口配置.主从机配置 │ │ ├─ 串口1: 从机 (EMS) │ │ ├─ 串口2: 从机 (HMI) │ │ ├─ 串口3: 主机 (液冷) │ │ └─ 串口4: 主机 (消防) │ └──────┬──────────────────┘ │ ┌───────────────┼───────────────┐ ↓ ↓ ↓ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ slave thread │ │ master thread│ │ master thread│ │ modbus_slave_│ │ (液冷/ttyS5) │ │ (消防/ttyS9) │ │ thread() │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ ↓ ↓ ↓ ┌──────────────────────────────────────────────┐ │ ctx->points[] │ │ [10001~] EMS 数据 (从CAN来) │ │ [30001~] 消防数据 ← 主机轮询写入 │ │ [50001~] 液冷数据 ← 主机轮询写入 │ └──────────────────┬───────────────────────────┘ ↓ ┌────────────────┐ │ shm_table │ ← EMS上报/逻辑判断/冻结帧 │ (共享内存) │ └────────────────┘

3.2 主机线程 GPIO 时序

由于 ISO3082 的 DE 和 RE 并联,必须精确控制 GPIO:

空闲状态:DE = 0(接收模式,等待从机数据) │ ↓ 构建 Modbus 请求帧 + CRC │ rs485_set_tx() → GPIO 写 "1" → DE = 1, RE = 1 → 发送模式 │ modbus_send_raw_request() → 发送请求帧 │ rs485_set_rx() → tcdrain() + GPIO 写 "0" → DE = 0, RE = 0 → 接收模式 │ modbus_receive_confirmation() → 等待从机应答 │ ↓ 解析应答,写入 points[]

关键点:不能直接使用modbus_read_registers()高级 API,因为 libmodbus 内部将发送和接收封装在一起,GPIO 无法在中间切换。必须使用modbus_send_raw_request()+modbus_receive_confirmation()分离收发。

3.3 文件改动范围

文件改动类型改动内容
COM/global.h修改SerialCtx增加master_slavepoll_interval_ms
COM/rs485/rtu.c修改load_serial_config()读取"主从机配置" ② 线程创建处分发主/从 ③ GPIO 函数去static
COM/rs485/rtu_master.c新增主机线程 + 消防/液冷轮询逻辑
Core/CMakeLists.txt修改编译列表增加rtu_master.c

四、关键代码实现

4.1 数据结构扩展 (global.h)

typedefstruct{// ... 原有字段 ...// RS485 DE GPIOintde_gpio_num;intde_gpio_fd;// 主从配置intmaster_slave;// 0 = 从机, 1 = 主机intpoll_interval_ms;// 主机轮询间隔 (ms)// Modbus 相关modbus_t*mb_ctx;modbus_mapping_t*mb_mapping;}SerialCtx;

4.2 数据库配置读取 (rtu.c)

load_serial_config()中增加两列读取:

elseif(strcmp(col_name,"主从机配置")==0){constchar*v=(constchar*)sqlite3_column_text(stmt,i);serial->master_slave=(v&&strcmp(v,"主机")==0)?1:0;}elseif(strcmp(col_name,"协议轮询时间(ms)")==0){serial->poll_interval_ms=sqlite3_column_int(stmt,i);}

4.3 线程分发 (rtu.c)

// serial_init_all() 中if(serial->master_slave==1){pthread_create(&serial->thread,NULL,modbus_master_thread,serial);printf("Master start: %s (slave=%d, interval=%dms)\n",serial->device,serial->slave_id,serial->poll_interval_ms);}else{pthread_create(&serial->thread,NULL,modbus_slave_thread,serial);printf("Slave start: %s (addr=%d)\n",serial->device,serial->slave_id);}

4.4 底层收发函数 (rtu_master.c)

手动构建 Modbus 请求帧 + CRC,分离 GPIO 控制:

staticintmaster_read_hr(SerialCtx*ctx,intuart_fd,uint16_taddr,uint16_tnb,uint16_t*dest){uint8_treq[8];req[0]=(uint8_t)ctx->slave_id;req[1]=0x03;// 读保持寄存器req[2]=(addr>>8)&0xFF;req[3]=addr&0xFF;req[4]=(nb>>8)&0xFF;req[5]=nb&0xFF;uint16_tcrc=master_crc16(req,6);// 计算 CRCreq[6]=crc&0xFF;req[7]=(crc>>8)&0xFF;rs485_set_tx(ctx);// DE = 1, 发送模式intsent=modbus_send_raw_request(ctx->mb_ctx,req,8);rs485_set_rx(ctx,uart_fd);// tcdrain + DE = 0, 接收模式if(sent<0)return-1;uint8_trsp[MODBUS_RTU_MAX_ADU_LENGTH];intrc=modbus_receive_confirmation(ctx->mb_ctx,rsp);if(rc<=0)returnrc;/* 解析响应: rsp[0]=地址, rsp[1]=0x03, rsp[2]=字节数, rsp[3..]=数据 */for(inti=0;i<nb;i++)dest[i]=(rsp[3+i*2]<<8)|rsp[3+i*2+1];return0;}

4.5 消防传感器轮询

staticintpoll_fire_sensor(SerialCtx*ctx,CTX*global_ctx){staticintfire_ok_cnt=0;uint16_tbuf[128];intfail=0;// burst 1: 5 个探测器 × 11 寄存器 = 55 个 (地址 0~54)if(master_read_hr(ctx,fd,0,55,buf)==0){for(intd=0;d<5;d++){intbase=d*11;for(intj=0;j<11;j++){intreg=base+j;for(intk=0;k<global_ctx->point_count;k++){if(points[k].data_addr==reg&&points[k].point_id>=30000&&points[k].point_id<40000){points[k].current_value=buf[reg];// 同步到共享内存shm_table->points[k]=points[k];}}}}}else{fail++;}// burst 2: 主机状态 (地址 1000, 9 个)// burst 3: 阈值 (地址 1009, 7 个)// ...}

4.6 液冷机组轮询

staticintpoll_liquid_cooling(SerialCtx*ctx,CTX*global_ctx){// burst 1: 运行状态 (地址 0, 36 个) — 含温度/压力/转速/故障// burst 2: 变频器功率 (地址 60, 1 个)// burst 3: 用户参数 (地址 4096, 18 个) — 含模式/预设温度/地址/波特率// ...}

4.7 GPIO 控制函数 (rtu.c)

跨文件可调用:

// 初始化:export GPIO → 输出 → 初始 LOWintrs485_de_gpio_init(SerialCtx*ctx);// 清理:拉低 → close → unexportvoidrs485_de_gpio_cleanup(SerialCtx*ctx);// 发送前:DE = HIGHvoidrs485_set_tx(SerialCtx*ctx){if(ctx->de_gpio_fd>=0)write(ctx->de_gpio_fd,"1",1);}// 发送后:等 FIFO 空 → DE = LOWvoidrs485_set_rx(SerialCtx*ctx,intuart_fd){if(ctx->de_gpio_fd>=0){tcdrain(uart_fd);write(ctx->de_gpio_fd,"0",1);}}

五、数据库配置

5.1 “串口配置” 表增加字段

新增列说明示例值
主从机配置“主机” 或 “从机”主机
协议轮询时间(ms)主机轮询间隔200(消防)/ 10000(液冷)

5.2 BCU 完整串口配置

串口ID开关串口号波特率主从机配置轮询时间从机ID用途
1开启串口119200从机15016EMS
2开启串口29600从机15016HMI
3开启串口39600主机100001液冷
4开启串口49600主机2001消防

5.3 新增点表

  • 消防传感器配置点表:72 行,点位 ID 30001~30071
    • 5 个探测器 × 11 寄存器 + 主机状态 9 个 + 阈值 7 个
  • 液冷机组配置点表:45 行,点位 ID 50001~50044
    • 运行状态 26 个 + 用户参数 18 个 + 开关机命令 1 个

六、测试验证

6.1 无设备测试(先验证框架)

sudosystemctl restart com_runsudojournalctl-ucom_run-f

期望日志:

--- Start Modbus threads --- Slave start: /dev/ttyS3 (addr=16) ← EMS 从机 Slave start: /dev/ttyS4 (addr=16) ← HMI 从机 Master start: /dev/ttyS5 (slave=1, interval=10000ms) ← 液冷主机 Master start: /dev/ttyS9 (slave=1, interval=200ms) ← 消防主机 serial5~11: disabled [/dev/ttyS3] RS485 DE GPIO116 (chip3,line20) OK [/dev/ttyS4] RS485 DE GPIO110 (chip3,line14) OK [/dev/ttyS5] RS485 DE GPIO113 (chip3,line17) OK [/dev/ttyS9] RS485 DE GPIO109 (chip3,line13) OK [FIRE] 3/3 read bursts failed ← 未接设备,正常超时 [LC] 3/3 read bursts failed ← 未接设备,正常超时

6.2 实测结果

检查项结果
GPIO 初始化 (4 路)
从机线程 (EMS/HMI)
主机线程 (液冷/消防)
主从分发正确
轮询间隔准确
超时机制正常
共享内存同步
硬件联调待接设备验证

6.3 接设备后验证

# 消防成功日志(每 2s 一条)[FIRE]OK#1 det1_smoke=25 temp=28.5C# 液冷成功日志(每 2min 一条)[LC]OK#1 state=0x0201 supplyT=22.0C# 验证共享内存数据cat/dev/shm/point_shm# 或通过 EMS 查询点位 30001/50001

七、经验总结

7.1 为什么不在内核层做

BCU 硬件设计时未使用 UART 的 RTS 引脚,而是独立 GPIO 控制 ISO3082 的 DE/RE。内核 RS485 框架(ioctl(TIOCSRS485))依赖 RTS 自动切换,无法适配独立 GPIO 的场景。应用层控制虽然多了一次系统调用的开销,但灵活性更高。

7.2 libmodbus 的坑

对于 RS485 半双工 + GPIO 方向控制的场景,不能使用modbus_read_registers()等高级 API,因为内部封装了发送和接收,GPIO 无法插入切换。必须:

rs485_set_tx → modbus_send_raw_request → rs485_set_rx → modbus_receive_confirmation

7.3 tcdrain() 不能省

UART 有硬件 FIFO(通常 64 字节),write()返回只代表数据进了内核缓冲区,不代表硬件发送完成。不做tcdrain()会导致 GPIO 提前拉低,截断正在发送的数据帧,造成总线冲突。

7.4 编码问题

项目源文件是 GBK 编码,含有大量中文注释。跨平台编辑时容易导致乱码。建议开发时统一用 UTF-8,或者在板端直接编辑。

7.5 编译踩坑

  • inline函数跨.c文件调用时,C99 标准要求必须提供外部定义体,否则链接报undefined reference。解决方案:去掉inline关键字,让编译器自行优化。
  • CMakeLists.txt 新增源文件后要确保路径正确。

7.6 后续优化

  1. 写命令支持:当前master_write_hr()已实现但未接入轮询流程,后续可用来下发液冷开关机、参数修改等控制指令
  2. 通信故障上报:连续 N 次失败后可标记通讯故障点位
  3. 动态地址配置:利用消防传感器广播地址 0 改地址的能力,实现从机地址自动分配

项目:BCU 电池管理控制单元
平台:OK3568(Rockchip RK3568)
日期:2026-06-10
作者:zzj

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 1:17:13

7种相关矩阵实现方法:从Pandas到稀疏计算的工程实践

1. 项目概述&#xff1a;为什么一张相关系数表值得花7种方式去实现&#xff1f;在数据分析的日常工作中&#xff0c;我几乎每天都会打开Jupyter Notebook&#xff0c;敲下df.corr()——这行代码像呼吸一样自然。但直到去年帮一家电商公司做用户行为归因分析时&#xff0c;我才真…

作者头像 李华
网站建设 2026/6/11 1:17:05

鼎讯信通DX-SZ5000 深耕电磁监测 助力能源行业平稳运营

在能源产业智能化发展的大趋势下&#xff0c;电网、油气、风电等场景高度依赖无线通信网络。复杂的工业电磁环境&#xff0c;常常引发信号干扰、瞬态异常等问题。DX-SZ5000 系列数字化射频频谱侦测接收机模块&#xff0c;凭借过硬的技术实力&#xff0c;成为保障通信稳定、优化…

作者头像 李华
网站建设 2026/6/11 1:16:59

Bankrate停用AI理财内容:金融内容可信度的七道生死线

1. 项目概述&#xff1a;一场被公开叫停的AI内容实验Bankrate——美国老牌金融信息平台&#xff0c;以房贷利率、信用卡对比、保险报价等垂直工具起家&#xff0c;过去二十年靠“人肉编辑专业撰稿数据验证”建立起行业公信力。2023年中&#xff0c;它悄悄启动了一项内部代号为“…

作者头像 李华
网站建设 2026/6/11 1:15:59

Beyond Compare 高级玩法:用脚本和规则实现自动化忽略修改时间

Beyond Compare自动化配置&#xff1a;忽略修改时间的高级实践在文件同步和版本控制的工作流中&#xff0c;修改时间&#xff08;timestamp&#xff09;经常成为比较操作的干扰因素。特别是当文件在不同系统间传输或通过不同工具处理后&#xff0c;时间戳的变化并不代表内容实质…

作者头像 李华
网站建设 2026/6/11 1:11:53

遗传算法实战:N皇后问题的Python工程化求解

1. 这不是教科书里的遗传算法&#xff0c;而是我亲手调通100皇后问题后写下的实操笔记你点开这篇文章&#xff0c;大概率不是想背诵“遗传算法是模拟生物进化过程的优化方法”这种定义。你真正想知道的是&#xff1a;当代码跑起来卡在fitness600不动了&#xff0c;到底该砍掉哪…

作者头像 李华
网站建设 2026/6/11 1:11:52

AI 电动仿真树智能功率 MOSFET 完整选型方案

2026年随着 AI 技术在动态仿生装置中的深度渗透&#xff08;如智能姿态控制、实时环境响应、自适应运动规划&#xff09;&#xff0c;电动仿真树对功率 MOSFET 提出更高要求&#xff1a;高频响应、低损耗、高可靠性。微碧半导体&#xff08;VBsemi&#xff09;基于 SJ_Multi-EP…

作者头像 李华