BCU 平台 RS485 驱动适配:从 THVD1406 到 ISO3082

背景:BCU(Battery Control Unit)是新一代电池管理控制单元,基于 RK3568 核心板 + 自研底板。与上一代 FCU2601 使用的 RS485 收发器芯片不同,BCU 选用了 ISO3082 隔离型收发器,导致原有驱动无法直接复用。本文记录完整的适配过程。


1. 硬件现场

1.1 两代平台对比

FCU2601(上一代) BCU(新一代)
核心板 RK3568 RK3568
RS485 芯片 THVD1406(TI) ISO3082(TI)
隔离特性 无隔离 2500Vrms 隔离
方向控制 芯片自动完成 需要 CPU GPIO 控制
封装 8-Pin 16-Pin SOIC
RS485 路数 11 路 4 路
使用 UART UART3 / UART4 / UART5 / UART9

1.2 芯片关键区别

┌─────────────────────────────────────────────────────────┐
│  THVD1406(FCU2601)          ISO3082(BCU)             │
│                                                         │
│  ┌──────────┐                 ┌──────────┐              │
│  │ D  ← TX  │                 │ D  ← TX  │              │
│  │ R  → RX  │                 │ R  → RX  │              │
│  │ RE → GND │                 │ RE ──┐   │              │
│  │ SHDN→VCC│                 │ DE ──┤   │ ← GPIO       │
│  │ A  ↔ 485+│                 │ A  ↔ 485+│              │
│  │ B  ↔ 485-│                 │ B  ↔ 485-│              │
│  └──────────┘                 └──────────┘              │
│                                                         │
│  ★ 方向自动切换                ★ 需要 GPIO 主动控制       │
│    无 DE 引脚                    DE + RE 并联接 GPIO      │
└─────────────────────────────────────────────────────────┘
  • THVD1406:通过 D 引脚电平变化自动触发方向切换,t_device_autodir ≈ 8µs,应用层完全无感
  • ISO3082:DE(pin5,高有效)和 RE(pin4,低有效)必须由外部 GPIO 控制。通常将 DE 和 RE 并联,一个 GPIO 同时控制收发方向

1.3 BCU 硬件接线

RK3568 核心板                     ISO3082 芯片
┌──────────────┐                ┌──────────────┐
│ UARTn_TXD    │───────────────→│ D  (pin 6)   │
│ UARTn_RXD    │←───────────────│ R  (pin 3)   │
│ GPIO3_Cx     │──────┬────────→│ DE (pin 5)   │
│              │      └────────→│ RE (pin 4)   │  ← DE/RE 并联
│ GND          │───────────────→│ GND1         │
└──────────────┘                └──────────────┘

1.4 GPIO 引脚分配

串口 设备节点 UART 控制器 DE 控制引脚 chip/line Kernel GPIO 编号
1 /dev/ttyS3 UART3 GPIO3_C4 chip3, line20 116
2 /dev/ttyS4 UART4 GPIO3_B6 chip3, line14 110
3 /dev/ttyS5 UART5 GPIO3_C1 chip3, line17 113
4 /dev/ttyS9 UART9 GPIO3_B5 chip3, line13 109

2. 问题描述

2.1 现象

FCU2601 的 RS485 驱动代码直接搬到 BCU 后,所有 485 从设备无法通信

2.2 根因分析

原有代码 rtu.c 中:

// modbus_rtu_set_serial_mode(ctx->mb_ctx, MODBUS_RTU_RS485);  // ← 被注释掉

这行代码原本的作用是通知内核 8250 驱动通过 RTS 引脚自动控制方向。但在 FCU2601 上被注释掉了,因为 THVD1406 芯片内部自动完成方向切换,根本不需要外部控制。

到了 BCU 平台,RS485 芯片换成了 ISO3082

  • ISO3082 没有自动方向控制能力
  • BCU 硬件上 没有将 RTS 引脚连接到 ISO3082 的 DE/RE
  • 而是用了独立的 GPIO(GPIO3 组的 4 个引脚)

结果:DE 始终为低电平,ISO3082 永远处于接收模式,无法发送数据。

2.3 为什么不用内核 RS485 框架?

Linux 内核提供了标准的 RS485 支持:通过 ioctl(TIOCSRS485) 让 8250 驱动自动控制 RTS 引脚。但 BCU 硬件设计时没有把 UART 的 RTS 信号接到 ISO3082,而是用了独立的 GPIO,所以内核方案行不通,必须在应用层控制。


3. 解决方案

3.1 方案选择

方案 描述 评估
A. 内核 RS485 框架 设备树加 linux,rs485-enabled-at-boot-time,内核自动控制 RTS ❌ BCU 未使用 RTS 引脚
B. 应用层 GPIO 控制 在 modbus 收发前后通过 sysfs 操作 GPIO 选用

3.2 核心设计

modbus_slave_thread 的收发循环中,包裹 GPIO 控制:

正常状态(接收):DE = 0,ISO3082 处于接收模式
        │
        ↓ poll() 检测到 POLLIN
        │
modbus_receive()     ← 接收从机请求(DE=0)
        │
        ↓
rs485_set_tx()       ← GPIO 写 "1",DE=1,切换到发送模式
        │
modbus_reply()       ← 发送响应数据
        │
rs485_set_rx()       ← tcdrain() + GPIO 写 "0",切回接收模式
        │
handle_modbus_write()← 处理写操作
        │
        ↓ 继续 poll 等待

3.3 代码改动

3.3.1 global.h — 新增 DE GPIO 字段
typedef struct {
    // ... 原有字段 ...

    // RS485 DE GPIO 控制
    int de_gpio_num;    // GPIO 全局编号 (chip*32+line),-1=禁用
    int de_gpio_fd;     // /sys/class/gpio/gpioN/value 文件描述符

    // Modbus 相关
    modbus_t* mb_ctx;
    modbus_mapping_t* mb_mapping;
} SerialCtx;
3.3.2 rtu.c — GPIO 映射表
static const int DE_GPIO_MAP[NUM_SERIAL_PORTS] = {
    116,   // serial_port=1  /dev/ttyS3  UART3  GPIO3_C4
    110,   // serial_port=2  /dev/ttyS4  UART4  GPIO3_B6
    113,   // serial_port=3  /dev/ttyS5  UART5  GPIO3_C1
    109,   // serial_port=4  /dev/ttyS9  UART9  GPIO3_B5
    -1, -1, -1, -1, -1, -1, -1,   // 预留,-1 = 禁用
};
3.3.3 rtu.c — GPIO 控制函数
// 初始化:export GPIO → 设为输出 → 初始值 LOW(接收模式)
static int rs485_de_gpio_init(SerialCtx *ctx) { ... }

// 清理:恢复 LOW → close fd → unexport GPIO
static void rs485_de_gpio_cleanup(SerialCtx *ctx) { ... }

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

// 发送后:等待 FIFO 空 → DE = LOW
static inline void rs485_set_rx(SerialCtx *ctx, int uart_fd) {
    if (ctx->de_gpio_fd >= 0) {
        tcdrain(uart_fd);           // ★ 关键:等 UART 发完
        write(ctx->de_gpio_fd, "0", 1);
    }
}
3.3.4 rtu.c — modbus_slave_thread 改造
// 初始化阶段:连接串口后初始化 GPIO
//
//     rs485_de_gpio_init(ctx);   ← 新增

// 主循环中的收发:
//
//     if (rc > 0) {
//         rs485_set_tx(ctx);                                ← 新增
//         modbus_reply(ctx->mb_ctx, query, rc, ctx->mb_mapping);
//         rs485_set_rx(ctx, fd);                            ← 新增
//         handle_modbus_write(ctx, global_ctx, query);
//     }

// 退出时清理:
//
//     rs485_de_gpio_cleanup(ctx);  ← 新增
//     modbus_close(ctx->mb_ctx);

3.4 兼容性设计

de_gpio_num = -1 时所有 GPIO 操作自动跳过:

if (ctx->de_gpio_num < 0)
    return 0;  // 无需 DE 控制,直接返回

这意味着同一份代码可以同时兼容:

  • FCU2601DE_GPIO_MAP 全部填 -1,THVD1406 自动方向控制
  • BCUDE_GPIO_MAP 填实际 GPIO 编号,ISO3082 应用层控制

4. 测试方案

4.1 测试环境

┌──────────────────────────────────────────────────────────┐
│   BCU  ←──RS485总线──→  模拟从机设备/PC端Modbus工具        │
│                                                          │
│  4路 RS485 接口:                                         │
│  /dev/ttyS3  (UART3)  ←→  从机设备组1                     │
│  /dev/ttyS4  (UART4)  ←→  从机设备组2                     │
│  /dev/ttyS5  (UART5)  ←→  从机设备组3                     │
│  /dev/ttyS9  (UART9)  ←→  从机设备组4                     │
└──────────────────────────────────────────────────────────┘

4.2 测试步骤

步骤 操作 验证点
1 上电,启动 com_run 服务 观察启动日志
2 检查 GPIO 初始化日志 RS485 DE GPIOxxx (chipx,linexx) OK
3 用 PC Modbus 工具发送读请求 是否能收到正确响应

4.3 关键观察点

  • GPIO 时序rs485_set_tx → 数据发送 → tcdrainrs485_set_rx 的间隔是否合理
  • 总线冲突:GPIO 是否在数据完全发完后才切回接收模式(tcdrain 是关键)
  • 异常恢复:通信超时后 GPIO 是否正确恢复为接收模式

5. 测试结果

5.1 启动日志

Jun 10 13:47:14 ok3568 com_run[25008]: ✓ Modbus 从机启动: /dev/ttyS3 (从机ID=16)
Jun 10 13:47:14 ok3568 com_run[25008]: ✓ Modbus 从机启动: /dev/ttyS4 (从机ID=16)
Jun 10 13:47:14 ok3568 com_run[25008]: ✓ Modbus 从机启动: /dev/ttyS5 (从机ID=16)
Jun 10 13:47:14 ok3568 com_run[25008]: ✓ Modbus 从机启动: /dev/ttyS9 (从机ID=16)
Jun 10 13:47:14 ok3568 com_run[25008]: 串口5~11: 未使能,跳过
Jun 10 13:47:14 ok3568 com_run[25008]: === 启动完成,共 4 个 Modbus 从机运行 ===

5.2 GPIO 初始化成功

[/dev/ttyS9] RS485 DE GPIO109 (chip3,line13) OK
[/dev/ttyS5] RS485 DE GPIO113 (chip3,line17) OK
[/dev/ttyS3] RS485 DE GPIO116 (chip3,line20) OK
[/dev/ttyS4] RS485 DE GPIO110 (chip3,line14) OK

4 路 GPIO 全部 export、设置为输出、初始化为 LOW 一次性成功。

5.3 联调结果

测试项 结果
GPIO 初始化 ✅ 4/4 通过
Modbus 从机启动 ✅ 4/4 通过
从机读写响应 ✅ 正常
长时间运行稳定性 ✅ 无异常
GPIO 波形(示波器) ✅ 发送时拉高,发送后恢复低电平

5.4 GPIO sysfs 验证

板端可随时确认 GPIO 状态:

# 查看 GPIO 已正确导出
ls /sys/class/gpio/
# export  gpio109  gpio110  gpio113  gpio116  ...

# 查看 GPIO 当前值(0=接收模式,1=发送模式)
cat /sys/class/gpio/gpio109/value   # 空闲时应为 0

6. 经验总结

6.1 设计决策

  1. 为什么不放内核层?
    BCU 硬件未使用 UART 的 RTS 引脚,而是独立 GPIO 控制 DE/RE。内核 RS485 框架依赖 RTS,无法适配。应用层控制更灵活。

  2. 为什么用 sysfs 而不是 libgpiod?
    选择 sysfs 的原因:

    • 保持文件描述符打开,write(fd, "0"/"1", 1) 一次系统调用完成切换,性能足够
    • OK3568 的 Linux 4.19 内核完全支持 sysfs GPIO
    • 无需额外链接 libgpiod,依赖更少
  3. tcdrain() 为什么不能省略?
    UART 有硬件 FIFO(通常 64 字节),write() 返回只代表数据写入内核缓冲区,不代表硬件发送完成。不做 tcdrain() 会导致 GPIO 提前拉低,截断正在发送的数据帧,造成总线冲突。

6.2 踩过的坑

  • 编码问题:项目源文件是 GBK 编码,用文本工具直接编辑中文注释会损坏文件。最终用 PowerShell 字节级操作完成修改。
  • 备份先行:每次修改前 .orig 文件是救命稻草。

6.3 后续优化建议

  1. 设备树配置化:可将 DE GPIO 信息写入设备树,应用层通过 /proc/device-tree 读取,避免硬编码
  2. 数据库配置化:在 bcu_cfg.db 的"串口配置"表中增加 de_gpio 字段,实现完全可配置
  3. 性能优化:当前 write() 走 sysfs,可改为 libgpiod 的 chardev ioctl 接口,减少 sysfs 开销
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐