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

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

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


一、项目背景

1.1 当前系统状态

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

串口 设备 角色 协议 用途
串口 1 /dev/ttyS3 从机 MODBUS_RTU EMS 通信
串口 2 /dev/ttyS4 从机 MODBUS_RTU HMI 通信
串口 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 控制方向:

串口 UART DE GPIO chip/line 全局编号
/dev/ttyS5 UART5 GPIO3_C1 chip3, line17 113
/dev/ttyS9 UART9 GPIO3_B5 chip3, line13 109

二、协议分析

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 开关量输入 DI U16 位
0x0002 继电器输出 DO U16 位
0x0003 供液温度 S16 ×0.1℃
0x0004 回液温度 S16 ×0.1℃
0x000C 水泵转速 U16 %
0x000D 压缩机转速 U16 RPM
0x0012 供水压力 S16 ×0.1bar
0x0019~0x001D 故障状态字 1~5 U16 位
0x0020 变频器故障代码 U16
0x0022 系统总故障等级 U16
用户参数 (0x1000~0x1011)
0x1001 工作模式 U16 0 待机/1 制冷/2 制热/3 自循环/4 自动
0x1003 制冷预设温度 U16 0~55℃
0x1010 RS485 地址 U16 可修改
控制 (0x0300)
0x0300 开关机命令 U16 Bit0=开机, 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)

c 复制代码
typedef struct {
    // ... 原有字段 ...

    // RS485 DE GPIO
    int de_gpio_num;
    int de_gpio_fd;

    // 主从配置
    int master_slave;        // 0 = 从机, 1 = 主机
    int poll_interval_ms;    // 主机轮询间隔 (ms)

    // Modbus 相关
    modbus_t* mb_ctx;
    modbus_mapping_t* mb_mapping;
} SerialCtx;

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

load_serial_config() 中增加两列读取:

c 复制代码
else if ( strcmp ( col_name, "主从机配置" ) == 0 )
{
    const char* v = ( const char* ) sqlite3_column_text ( stmt, i );
    serial->master_slave = ( v && strcmp ( v, "主机" ) == 0 ) ? 1 : 0;
}
else if ( strcmp ( col_name, "协议轮询时间(ms)" ) == 0 )
{
    serial->poll_interval_ms = sqlite3_column_int ( stmt, i );
}

4.3 线程分发 (rtu.c)

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 控制:

c 复制代码
static int master_read_hr(SerialCtx *ctx, int uart_fd,
                          uint16_t addr, uint16_t nb, uint16_t *dest)
{
    uint8_t req[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_t crc = master_crc16(req, 6);         // 计算 CRC
    req[6] = crc & 0xFF;
    req[7] = (crc >> 8) & 0xFF;

    rs485_set_tx(ctx);                           // DE = 1, 发送模式
    int sent = modbus_send_raw_request(ctx->mb_ctx, req, 8);
    rs485_set_rx(ctx, uart_fd);                  // tcdrain + DE = 0, 接收模式

    if (sent < 0) return -1;

    uint8_t rsp[MODBUS_RTU_MAX_ADU_LENGTH];
    int rc = modbus_receive_confirmation(ctx->mb_ctx, rsp);
    if (rc <= 0) return rc;

    /* 解析响应: rsp[0]=地址, rsp[1]=0x03, rsp[2]=字节数, rsp[3..]=数据 */
    for (int i = 0; i < nb; i++)
        dest[i] = (rsp[3 + i*2] << 8) | rsp[3 + i*2 + 1];
    return 0;
}

4.5 消防传感器轮询

c 复制代码
static int poll_fire_sensor(SerialCtx *ctx, CTX *global_ctx)
{
    static int fire_ok_cnt = 0;
    uint16_t buf[128];
    int fail = 0;

    // burst 1: 5 个探测器 × 11 寄存器 = 55 个 (地址 0~54)
    if (master_read_hr(ctx, fd, 0, 55, buf) == 0) {
        for (int d = 0; d < 5; d++) {
            int base = d * 11;
            for (int j = 0; j < 11; j++) {
                int reg = base + j;
                for (int k = 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 液冷机组轮询

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

4.7 GPIO 控制函数 (rtu.c)

跨文件可调用:

c 复制代码
// 初始化:export GPIO → 输出 → 初始 LOW
int rs485_de_gpio_init(SerialCtx *ctx);

// 清理:拉低 → close → unexport
void rs485_de_gpio_cleanup(SerialCtx *ctx);

// 发送前:DE = HIGH
void rs485_set_tx(SerialCtx *ctx) {
    if (ctx->de_gpio_fd >= 0) write(ctx->de_gpio_fd, "1", 1);
}

// 发送后:等 FIFO 空 → DE = LOW
void rs485_set_rx(SerialCtx *ctx, int uart_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 开启 串口1 19200 从机 150 16 EMS
2 开启 串口2 9600 从机 150 16 HMI
3 开启 串口3 9600 主机 10000 1 液冷
4 开启 串口4 9600 主机 200 1 消防

5.3 新增点表

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

六、测试验证

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

bash 复制代码
sudo systemctl restart com_run
sudo journalctl -u com_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 接设备后验证

bash 复制代码
# 消防成功日志(每 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

相关推荐
vsropy1 小时前
安装虚拟机VMware
linux·windows
Jason_chen1 小时前
Linux 3.0 串口机制深度解析:传统8250驱动与基础RS-232/485支持
linux·前端
Jason_chen1 小时前
Linux 5.10 串口机制深度解析:serial_core重构与RS-485自动方向控制革命
linux
无敌的牛2 小时前
自省。。。。
linux
lqjun08272 小时前
Linux 下 Hermes Agent 代理配置不生效问题的解决
linux·服务器
Gary Studio2 小时前
复杂 SoC(RK3568)PCB 布局的五步
android·linux·硬件
一拳一个娘娘腔2 小时前
CVE-2026-43284 — Dirty Frag 深度拆解:当零拷贝遇上原地解密,页缓存成了攻击者的画板
linux·缓存
c_lb72882 小时前
期货量化策略从 Windows 迁到 Linux 服务器:环境注意点
linux·服务器·windows·python
熙芯XiChip2 小时前
Linux SPI从机驱动开发要点
linux