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/ttyS5BCU 作为 Modbus 主机周期性读取液冷运行状态、故障信息、运行参数支持远程开关机和参数设置消防传感器串口 4/dev/ttyS9BCU 作为 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 消防传感器协议物理层RS4859600bps8N1MODBUS 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 写寄存器 10102.2 液冷机组协议物理层RS4859600bpsMODBUS 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压缩机转速U16RPM0x0012供水压力S16×0.1bar0x0019~0x001D故障状态字 1~5U16 位0x0020变频器故障代码U160x0022系统总故障等级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_slave、poll_interval_msCOM/rs485/rtu.c修改①load_serial_config()读取主从机配置 ② 线程创建处分发主/从 ③ GPIO 函数去staticCOM/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(vstrcmp(v,主机)0)?1:0;}elseif(strcmp(col_name,协议轮询时间(ms))0){serial-poll_interval_mssqlite3_column_int(stmt,i);}4.3 线程分发 (rtu.c)// serial_init_all() 中if(serial-master_slave1){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](addr8)0xFF;req[3]addr0xFF;req[4](nb8)0xFF;req[5]nb0xFF;uint16_tcrcmaster_crc16(req,6);// 计算 CRCreq[6]crc0xFF;req[7](crc8)0xFF;rs485_set_tx(ctx);// DE 1, 发送模式intsentmodbus_send_raw_request(ctx-mb_ctx,req,8);rs485_set_rx(ctx,uart_fd);// tcdrain DE 0, 接收模式if(sent0)return-1;uint8_trsp[MODBUS_RTU_MAX_ADU_LENGTH];intrcmodbus_receive_confirmation(ctx-mb_ctx,rsp);if(rc0)returnrc;/* 解析响应: rsp[0]地址, rsp[1]0x03, rsp[2]字节数, rsp[3..]数据 */for(inti0;inb;i)dest[i](rsp[3i*2]8)|rsp[3i*21];return0;}4.5 消防传感器轮询staticintpoll_fire_sensor(SerialCtx*ctx,CTX*global_ctx){staticintfire_ok_cnt0;uint16_tbuf[128];intfail0;// burst 1: 5 个探测器 × 11 寄存器 55 个 (地址 0~54)if(master_read_hr(ctx,fd,0,55,buf)0){for(intd0;d5;d){intbased*11;for(intj0;j11;j){intregbasej;for(intk0;kglobal_ctx-point_count;k){if(points[k].data_addrregpoints[k].point_id30000points[k].point_id40000){points[k].current_valuebuf[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_fd0)write(ctx-de_gpio_fd,1,1);}// 发送后等 FIFO 空 → DE LOWvoidrs485_set_rx(SerialCtx*ctx,intuart_fd){if(ctx-de_gpio_fd0){tcdrain(uart_fd);write(ctx-de_gpio_fd,0,1);}}五、数据库配置5.1 “串口配置” 表增加字段新增列说明示例值主从机配置“主机” 或 “从机”主机协议轮询时间(ms)主机轮询间隔200消防/ 10000液冷5.2 BCU 完整串口配置串口ID开关串口号波特率主从机配置轮询时间从机ID用途1开启串口119200从机15016EMS2开启串口29600从机15016HMI3开启串口39600主机100001液冷4开启串口49600主机2001消防5.3 新增点表消防传感器配置点表72 行点位 ID 30001~300715 个探测器 × 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 (addr16) ← EMS 从机 Slave start: /dev/ttyS4 (addr16) ← HMI 从机 Master start: /dev/ttyS5 (slave1, interval10000ms) ← 液冷主机 Master start: /dev/ttyS9 (slave1, interval200ms) ← 消防主机 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_smoke25 temp28.5C# 液冷成功日志每 2min 一条[LC]OK#1 state0x0201 supplyT22.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_confirmation7.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 后续优化写命令支持当前master_write_hr()已实现但未接入轮询流程后续可用来下发液冷开关机、参数修改等控制指令通信故障上报连续 N 次失败后可标记通讯故障点位动态地址配置利用消防传感器广播地址 0 改地址的能力实现从机地址自动分配项目BCU 电池管理控制单元平台OK3568Rockchip RK3568日期2026-06-10作者zzj