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

相关推荐
HLC++11 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室13 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy15 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕16 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫17 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生17 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1119 小时前
linux基础IO
linux·运维·服务器
BelongPanda20 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯21 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983421 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器