Linux 下配置 RS-485 半双工方向控制:TIOCSRS485 设置、回读与总线验证实操

  2026-10-25   hermes     13   0  

半双工的 RS-485 只有一对差分线,发送和接收不能在时间上重叠,收发器必须在两者之间来回切换。这个切换动作看似简单,落到 Linux 上却经常出问题:首字节丢失、末字节被截断、收到自己发出的报文、单帧长报文偶发 CRC 错误,绝大多数都来自"方向切换的时机没有和 UART 的发送时序对齐"。把这件事从应用层挪到内核驱动层,是消除这类时序抖动最直接的办法。

本文给出 Linux 下用内核 RS-485 接口配置半双工方向控制的完整操作过程:先确认驱动是否支持,再用 TIOCSRS485 写入配置,然后回读核对驱动实际接受的参数,最后在总线和协议层验证。文中命令与代码可直接复现,代码里的结构体布局与 ioctl 返回值语义均按内核 uapi 头文件编写。

一、适用场景:三种方向控制方案,只有一种用得上本文

RS-485 收发器有发送使能(DE)和接收使能(/RE)两个控制脚,谁负责切换这两个脚,决定了软件要不要介入:

  • 方案 A:收发器方向脚接到 UART 的 RTS。发送前把 RTS 置为有效、发送完再释放。这是本文要解决的情形,也是工控主板、多串口卡上最常见的做法。
  • 方案 B:板上自带自动方向电路。用发送数据本身(或专用自动方向收发器)检测后自动切换,软件按普通串口收发即可。此时不应再开启内核 RS-485 模式,否则两套机制会互相干扰。
  • 方案 C:用户态程序手动翻 RTS。通过 TIOCMBIS/TIOCMBIC 操作 TIOCM_RTS。时序取决于进程调度,延迟通常在毫秒量级,波特率一高就不可靠,只适合低速场合。

判断依据很具体:查板卡原理图或手册,确认 UART 的 RTS 引脚是否连到收发器的 DE/RE;如果连了,且所用串口驱动实现了 RS-485 配置,就可以用本文流程。反过来,如果板上已经是自动方向电路,或者 RTS 根本没接出去,本文的配置不会生效。

二、前置条件

项目要求与检查方法
硬件接线RTS 连到 DE/RE,且清楚是否经过反相器;同时确认该 RTS 没有被设计成硬件流控脚
驱动支持对目标 tty 执行 TIOCGRS485 能成功返回(第三步的探测代码)。返回 ENOTTY 说明这条 tty 上根本没有实现该 ioctl
访问权限/dev/ttyS* 通常是 root:dialout、权限 crw-rw----;非 root 账号需加入 dialout 组,否则 open() 直接返回 Permission denied
总线侧两端 120 Ω 终端电阻、A/B 接线正确、共地或屏蔽层处理到位。总线电气问题不要和方向切换问题混在一起排查
工具gcc 或 python3;强烈建议备一台示波器或逻辑分析仪,方向切换最终要看波形确认

三、第一步:确认端口与驱动支持情况

先确定设备名,并看内核是怎么枚举这个串口的:

ls -l /dev/ttyS* /dev/ttyUSB*
dmesg | grep -iE "ttyS|8250|uart" | tail -20

设备名会随插槽、枚举顺序变化,量产设备建议用 /dev/serial/by-path/ 下的固定路径,避免重启后 ttyS 编号漂移。

接着用 TIOCGRS485 做一次探测。这一步的目的不是读取配置,而是确认"这条 tty 上有没有这个 ioctl",第三步的完整代码可以直接拿来跑:能成功返回就说明驱动实现了 RS-485 配置;返回 ENOTTY(errno 25,"Inappropriate ioctl for device")说明不支持。需要提醒的是,pty(/dev/pts/N)以及部分 USB 转串口适配器通常不支持该 ioctl,这类设备需要改用方案 B/C,或换成实现了该接口的驱动/适配器。

有 root 权限时,还可以用 cat /proc/tty/driver/serial 看 UART 类型、端口地址与中断,确认目标端口确实是真实 UART 而不是虚拟端口。

四、第二步:固定帧格式,并关掉所有会改写字节流的选项

RS-485 传的是二进制帧,行规程任何"贴心的转换"都是灾难。先把端口设成 raw 模式:

stty -F /dev/ttyS0 raw -echo -echoe -echok -crtscts 115200 cs8 -cstopb -parenb
stty -F /dev/ttyS0 -a
  • raw:关闭回车换行转换、特殊字符解释、信号生成等行规程处理。不设这一项,0x0D、0x11、0x13 之类的字节会被改写或吞掉。
  • -crtscts:必须关闭硬件流控。RTS 已被用作方向控制脚,再交给流控逻辑抢占,方向切换时序就无从保证。
  • 帧格式:命令里的 115200 cs8 -cstopb -parenb 就是 8N1(8 数据位、无校验、1 停止位)。Modbus RTU 现场常用 8E1(偶校验),具体以从站的实际配置为准,改帧格式时只动 parenb/parodd 即可。

注意 stty 没有 RS-485 相关选项,标准内核 RS-485 模式的方向控制只能通过 ioctl 完成(板上自带自动方向电路的方案不在此列),这也是很多人卡住的地方。

五、第三步:用 TIOCSRS485 写入方向控制配置

C 语言写法最直接,编译成一个小工具即可:

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

int main(int argc, char **argv)
{
    int fd = open(argv[1], O_RDWR | O_NOCTTY);
    if (fd < 0) { perror("open"); return 1; }

    struct serial_rs485 conf;
    memset(&conf, 0, sizeof(conf));
    if (ioctl(fd, TIOCGRS485, &conf) < 0) {   /* 探测驱动是否支持 */
        perror("TIOCGRS485");
        return 2;
    }
    printf("before: flags=0x%08x before=%ums after=%ums\n", conf.flags,
           conf.delay_rts_before_send, conf.delay_rts_after_send);

    memset(&conf, 0, sizeof(conf));
    conf.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND;
    conf.delay_rts_before_send = 1;   /* 单位:毫秒 */
    conf.delay_rts_after_send  = 1;   /* 单位:毫秒 */

    if (ioctl(fd, TIOCSRS485, &conf) < 0) {
        perror("TIOCSRS485");
        return 3;
    }
    /* 成功时 conf 里已被填入驱动实际生效的配置 */
    printf("after : flags=0x%08x before=%ums after=%ums\n", conf.flags,
           conf.delay_rts_before_send, conf.delay_rts_after_send);
    close(fd);
    return 0;
}
gcc -O2 -o rs485cfg rs485cfg.c
./rs485cfg /dev/ttyS0

习惯用 Python 的话,用 fcntl.ioctl 加 struct 打包同样可行。结构体总长 32 字节,字段顺序为 flags、delay_rts_before_send、delay_rts_after_send 再加 20 字节的填充/地址联合体:

import fcntl, struct

TIOCGRS485 = 0x542E
TIOCSRS485 = 0x542F
FMT = '<III5I'          # flags, delay_before, delay_after, padding[5]
SIZE = struct.calcsize(FMT)                      # 32

def get_rs485(fd):
    buf = bytearray(SIZE)
    fcntl.ioctl(fd, TIOCGRS485, buf, True)
    return struct.unpack_from('<III', buf, 0)

def set_rs485(fd, flags, before_ms, after_ms):
    buf = bytearray(struct.pack(FMT, flags, before_ms, after_ms, 0, 0, 0, 0, 0))
    fcntl.ioctl(fd, TIOCSRS485, buf, True)       # True 表示允许内核回写生效值
    return struct.unpack_from('<III', buf, 0)

flags = 0x1 | 0x2        # ENABLED | RTS_ON_SEND
print(set_rs485(fd, flags, 1, 1))

各字段的具体含义

字段 / 标志位含义使用要点
SER_RS485_ENABLED(bit 0)启用 RS-485 模式不清零其他位的基座,所有配置都建立在它之上
SER_RS485_RTS_ON_SEND(bit 1)发送期间 RTS 的逻辑电平为 1收发器 DE 高有效且 RTS 直连 DE 时置位;若板上加了反相器则要清掉
SER_RS485_RTS_AFTER_SEND(bit 2)发送结束后 RTS 的逻辑电平为 1半双工通常清掉,让总线回到接收状态
SER_RS485_RX_DURING_TX(bit 4)发送期间仍然接收用于四线全双工或需要本地回显的场合;半双工两线一般不开,否则会读到自己的数据
SER_RS485_TERMINATE_BUS(bit 5)使能总线终端电阻仅在收发器/板卡带可切换终端时才有效
SER_RS485_ADDRB、ADDR_RECV、ADDR_DEST(bit 6/7/8)多点寻址模式与收发地址部分驱动支持,用于硬件地址过滤,不是所有平台都实现
SER_RS485_MODE_RS422(bit 9)切换为 RS-422 模式仅在需要四线制时使用
delay_rts_before_send拉高 RTS 到开始发送之间的延时,单位毫秒给收发器留出使能时间;设 0 时高波特率下容易丢首字节
delay_rts_after_send发送结束后到释放 RTS 的延时,单位毫秒等最后一个字节从移位寄存器移出;设 0 时末字节可能被截断

两个 delay 的单位是毫秒,这一点在内核 uapi 头文件的注释里写得很明确。这个粒度相对字符时间相当粗:115200 bit/s、8N1 时一个字符约 86.8 μs,1 ms 已经相当于约 11.5 个字符时间。所以 delay 只能"宁大勿小"地微调,不要指望用它做精细的时序补偿。另外串口核心层对两个 delay 设有上限(100 ms),超过的值会被截断;如果所用驱动完全不支持延时,写入的值会被清零并打印一条警告——这两种情况都不会让 ioctl 失败,只能靠回读和内核日志发现。

结构体总长固定为 32 字节,末尾 20 字节是保留空间,其中较新内核用于放置多点寻址的地址字段。不同内核版本的字段细节有差异,所以只用"启用 + RTS 电平 + 两个 delay"这几项时按上面的布局打包即可;确实要用多点寻址时,应以目标内核的 uapi 头文件为准逐字段填写。

六、第四步:回读确认驱动实际接受了什么

这一步最容易被跳过,也是最容易踩坑的地方:TIOCSRS485 对"能被识别但驱动不支持"的项并不报错,而是修正后回填并返回。ioctl 返回 0 只说明这次配置被接受了,不代表每一项都按你的意思生效。

下面几类情况属于"静默修正",只能靠回读发现:

  • 请求了驱动没有实现的标志位(例如可切换终端 SER_RS485_TERMINATE_BUS、发送期间接收 SER_RS485_RX_DURING_TX),对应位会被清掉;
  • 请求的 delay 超过串口核心层允许的上限,会被截断到上限;驱动完全不支持延时功能时,delay 直接被清零;
  • 两个 RTS 电平标志被设成同一个取值(都置位或都不置位,等于"发送前后电平相同",对半双工没有意义),内核会自动改成其中一种(优先 RTS_ON_SEND)并打一条警告。

而下面这些属于"直接拒绝",ioctl 返回 -1、errno 为 EINVAL:

  • 置了 SER_RS485_ADDR_RECV/ADDR_DEST 却没有置 SER_RS485_ADDRB;
  • 填了 addr_recv/addr_dest 的值,但对应的使能位没有置;
  • 置了超出当前驱动支持范围的新增标志位(多点寻址、RS-422 等)。

所以配置完之后应该做三件事:

  1. 把 TIOCSRS485 回填的 conf(或紧接着再 TIOCGRS485 一次的结果)打印出来,逐位核对 flags 与两个 delay 是否符合预期;
  2. 立刻看一眼内核日志:dmesg | grep -i rs485。上面那些静默修正都会在日志里留下痕迹(延时不被支持、延时被截断、RTS 电平被自动改写),这是最快的确诊线索;
  3. 把回读输出和日志检查一起写进设备初始化脚本,让"配置是否真的生效"可回溯。现场排障时,一行回读输出往往比反复改应用代码更快定位问题。

七、第五步:在总线与协议层验证

1. 波形:最直接的证据

示波器或逻辑分析仪接三路信号:RTS(或收发器 DE)、TXD、A/B 差分线。观察两件事:

  • DE 的上升沿应出现在起始位之前——这对应 delay_rts_before_send;
  • DE 的下降沿应出现在最后一个停止位之后、FIFO 完全排空之后——这对应 delay_rts_after_send。

如果下降沿落在停止位中间,总线上就会出现被截断的最后一个字节,对端表现为 CRC 错或帧错。

2. 双节点对测

用一块 USB 转 RS-485 适配器(或第二台设备的 485 口)与板载口对拖:A 接 A、B 接 B,两端各接 120 Ω 终端(或一端终端、另一端偏置),双方互发随机数据并逐字节比对。这一步能快速区分"方向切换问题"和"总线电气问题"。

3. 协议层:Modbus RTU 短报文往返

用从站设备或模拟从站做短报文往返,统计超时与 CRC 错误次数。要注意帧间静默的要求:RTU 规定帧与帧之间至少 3.5 个字符时间的静默,且波特率高于 19200 时该间隔固定取 1.75 ms(字符间超时 t1.5 固定取 0.75 ms)。如果 delay_rts_after_send 设得过大(比如几毫秒),主站侧的接收窗口会被拉长,轻则响应变慢,重则把一帧拆成两段处理。

4. 长稳测试

连续跑几万帧并统计错误率,比单次通断更能暴露方向切换的边界问题。产线设备上尤其要覆盖高低温与长线缆工况。

八、常见坑与处置

现象常见原因处置
发送正常,但从站一条也收不到RTS 极性反了,发送时收发器实际处于接收态翻转 SER_RS485_RTS_ON_SEND,再用波形确认 DE 是否随发送拉高
收到的数据里混着自己发的报文发送期间接收器仍使能(DE/RE 未并接,或开了 RX_DURING_TX)检查 DE/RE 接法;半双工两线清掉 RX_DURING_TX
高波特率下首字节丢失delay_rts_before_send 为 0 或太小,收发器还没来得及使能从 1 ms 起调,按波形收紧
末字节被截断、对端报帧错RTS 释放过早,最后一个字节还在移位寄存器里delay_rts_after_send 至少给 1 ms,配合 FIFO 深度确认
ioctl 返回成功,行为却没变驱动回退了不支持的标志位,或 delay 被截断/清零回读 flags 与 delay,并查 dmesg | grep -i rs485 的警告
ioctl 返回 -1、errno 为 EINVAL置了地址接收/发送位却没有 ADDRB,或给了地址值却没有对应使能位;也可能是请求了驱动不支持的新增标志位按"地址位—使能位"成对设置,或去掉多点寻址/RS-422 请求
两个 RTS 电平标志设成了同一个值对半双工无意义,内核会自动改写其中一个并打警告明确只置 RTS_ON_SEND(或只置 RTS_AFTER_SEND),用回读确认
设了 delay_rts_after_send,回读却是 0驱动不支持该延时,值被清零(内核日志有 not supported)改用支持延时的驱动,或从应用层保证帧间隔
返回 ENOTTY(errno 25)该 tty 上没有实现 RS-485 ioctl(pty、部分 USB 转串口)换用支持的内核驱动/板卡,或退化为方案 B/C
用户态翻 RTS 在高速下不稳进程调度与系统调用延迟,量级可达毫秒只在低速(社区经验 9600 bit/s 以下)使用;高速必须下移到驱动层
配置莫名"失效"RS-485 配置属于端口级别,其他进程重新打开或初始化该 tty 会覆盖统一由一处初始化并在打开后回读确认,避免多处重复配置
方向时序错乱、偶发冲突同一端口上同时打开了硬件流控(CRTSCTS)关闭硬件流控,RTS 只能专职做方向控制
open() 报 Permission denied当前账号不在 dialout 组加入 dialout 组,或用 udev 规则固定属主与权限
板上已有自动方向电路,却仍开内核 RS-485 模式两套方向控制互相干扰只保留一套:自动方向电路场合按普通串口使用

九、边界说明

本文覆盖的是"RTS 驱动收发器方向"这一类半双工配置。多点寻址(ADDRB)与 RS-422 四线模式只有在驱动明确实现时才可用,不能仅凭 flags 位存在就假定可用。内核 RS-485 接口能否使用、能用到什么程度,最终取决于所用 UART 驱动与 BSP 的实现,不同平台的行为可能差异明显——判断依据始终是"ioctl 是否成功 + 回读到的实际配置",而不是内核版本号或芯片手册上的一句"支持 RS-485"。涉及产线停机的改动,建议先在备机上完整走一遍本文的探测、配置、回读与波形验证流程,再推到现场。

客服微信:18025338165点击复制并跳转微信