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 中:
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 字段
c
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 映射表
c
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 控制函数
c
// 初始化: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 改造
c
// 初始化阶段:连接串口后初始化 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 操作自动跳过:
c
if (ctx->de_gpio_num < 0)
return 0; // 无需 DE 控制,直接返回
这意味着同一份代码可以同时兼容:
- FCU2601 :
DE_GPIO_MAP全部填-1,THVD1406 自动方向控制 - BCU :
DE_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→ 数据发送 →tcdrain→rs485_set_rx的间隔是否合理 - 总线冲突 :GPIO 是否在数据完全发完后才切回接收模式(
tcdrain是关键) - 异常恢复:通信超时后 GPIO 是否正确恢复为接收模式
5. 测试结果
5.1 启动日志
log
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 初始化成功
log
[/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 状态:
bash
# 查看 GPIO 已正确导出
ls /sys/class/gpio/
# export gpio109 gpio110 gpio113 gpio116 ...
# 查看 GPIO 当前值(0=接收模式,1=发送模式)
cat /sys/class/gpio/gpio109/value # 空闲时应为 0
6. 经验总结
6.1 设计决策
-
为什么不放内核层?
BCU 硬件未使用 UART 的 RTS 引脚,而是独立 GPIO 控制 DE/RE。内核 RS485 框架依赖 RTS,无法适配。应用层控制更灵活。
-
为什么用 sysfs 而不是 libgpiod?
选择 sysfs 的原因:
- 保持文件描述符打开,
write(fd, "0"/"1", 1)一次系统调用完成切换,性能足够 - OK3568 的 Linux 4.19 内核完全支持 sysfs GPIO
- 无需额外链接 libgpiod,依赖更少
- 保持文件描述符打开,
-
tcdrain()为什么不能省略?UART 有硬件 FIFO(通常 64 字节),
write()返回只代表数据写入内核缓冲区,不代表硬件发送完成。不做tcdrain()会导致 GPIO 提前拉低,截断正在发送的数据帧,造成总线冲突。
6.2 踩过的坑
- 编码问题:项目源文件是 GBK 编码,用文本工具直接编辑中文注释会损坏文件。最终用 PowerShell 字节级操作完成修改。
- 备份先行 :每次修改前
.orig文件是救命稻草。
6.3 后续优化建议
- 设备树配置化 :可将 DE GPIO 信息写入设备树,应用层通过
/proc/device-tree读取,避免硬编码 - 数据库配置化 :在
bcu_cfg.db的"串口配置"表中增加de_gpio字段,实现完全可配置 - 性能优化 :当前
write()走 sysfs,可改为 libgpiod 的 chardev ioctl 接口,减少 sysfs 开销