资讯详情

Linux RS485驱动与收发切换时序:从设备树配置到工程实践

📅 2026/10/23 3:15:34 | 华诺云谱 👁 阅读
Linux RS485驱动与收发切换时序:从设备树配置到工程实践
1. 先理清楚RS485在Linux内核里到底由谁说了算这个系列写到第4篇前面我们把RS485的电气基础、硬件电路、还有T113平台上的串口设备树配置都过了一遍。不少朋友照着配完发现一个很尴尬的现象板子能发能收但一旦接到真实的RS485总线上要么从站不回复要么回复就乱码。问题大多不出在硬件上而出在你根本没搞懂Linux内核里“谁在控制方向切换”这件事。RS485和普通UART最大的区别就是半双工。总线上一根双绞线A/B差分信号同一时刻只能有一个设备往总线上发送数据。这就意味着驱动必须在“发送”和“接收”两个状态之间来回切换。普通UART根本不需要这种切换所以Linux内核里RS485的支持不是简单地把一个寄存器位打开就完事它牵扯到串口核心层、具体驱动、设备树、应用层ioctl四个层面的配合。你要理解的第一件事是Linux将RS485抽象成了一个叫struct serial_rs485的结构体定义在include/uapi/linux/serial.h里。应用层通过TIOCSRS485和TIOCGRS485这两个ioctl去设置和读取RS485参数内核里serial core收到ioctl后会调用具体串口驱动注册的set_rs485回调。如果你的驱动没有实现这个回调那你应用层再怎么折腾都是白搭。struct serial_rs485 { __u32 flags; #define SER_RS485_ENABLED (1 0) #define SER_RS485_RTS_ON_SEND (1 1) #define SER_RS485_RTS_AFTER_SEND (1 2) #define SER_RS485_RX_DURING_TX (1 4) __u32 delay_rts_before_send; __u32 delay_rts_after_send; __u32 padding[5]; };1.1 你改的ioctl和内核回调之间的调用链很多人只知道用stty或者自己写个ioctl去配RS485但不知道这背后发生了什么。我实际跟踪过一轮代码链路是这样的应用层调用ioctl(fd, TIOCSRS485, rs485)之后进入drivers/tty/serial/serial_core.c里的uart_set_rs485_config函数这个函数会做两件事先把用户传入的参数存到uart_port-rs485结构体里然后调用port-ops-set_rs485(port, rs485)去通知具体驱动。关键点在于uart_port是串口核心层的抽象它不关心你用的是全志T113还是别的SoC。真正干活的set_rs485回调是每个驱动自己实现的。有的驱动写得比较完整会处理RTS电平切换、延时、FIFO状态有的驱动只是把结构体存下来什么都不做还有的驱动根本没注册这个回调你调用ioctl只会收到-ENOTTY。所以我每次到一块新板子上调试RS485第一件事不是改设备树而是用grep在内核源码目录里搜一下这个驱动的uart_ops结构体里有没有.set_rs485这一项。没有直接换方案有再继续往下看它到底做了什么。1.2 不同驱动对“RS485支持”的理解完全不一样同样是声称“支持RS485”的驱动实际支持程度天差地别。我习惯把驱动分为四个层级层级驱动行为实际效果完整支持实现set_rs485且在start_tx/stop_tx里自动翻转RTS/GPIO处理延时配置后即可用波形时序正确部分支持实现了set_rs485但只存参数RTS需要应用层自己控制能发能收但方向切换可能丢字节伪支持设备树里能写属性但驱动只是解析后忽略表面配置成功实际没有任何变化不支持uart_ops里没有set_rs485回调ioctl返回-ENOTTYT113平台的内核驱动通常是sunxi-uart但它不同SDK版本的实现差异很大。有的BSP内核里挂的还是8250框架有的则是全志自己的独立驱动。你在网上抄的设备树配置往往只适用于某一个特定内核版本。所以在往下走之前先确认你手上这份SDK的串口驱动到底属于哪一层。2. T113设备树里写RS485配置远不止加一个属性很多朋友在设备树里加了一行linux,rs485-enabled-at-boot-time;就以为完事了。这行属性在内核标准框架里确实存在它的作用是让串口驱动在初始化时就把RS485模式打开省去应用层再去调用一次TIOCSRS485。但它的前提依然是你的驱动真的去解析了这个属性并且处理了后续的RTS翻转逻辑。我在T113上遇到过最典型的场景是属性加了启动后串口设备确实存在但用示波器抓RTS引脚发现它根本没有动作。追了半天代码才发现这个BSP内核里的sunxi-uart驱动压根就没有解析linux,rs485-enabled-at-boot-time这个属性。设备树里写不写对驱动来说完全是透明的。这就是典型的“伪支持”。2.1 设备树属性和驱动代码像不像“配对螺丝”设备树里的属性名和驱动代码里的解析逻辑必须严格匹配一个拼写错误都会导致属性被静默丢弃。T113上常见的做法是两种一是用UART本身的RTS引脚直接控制RS485收发芯片的DE/RE二是用一颗普通GPIO来模拟方向控制。先说RTS方案。RTS本来是硬件流控用的在RS485场景里被借来当方向控制脚。这种情况下设备树里通常要配置引脚的mux功能把RTS复用为UART功能然后在驱动里通过设置SER_RS485_RTS_ON_SEND和SER_RS485_RTS_AFTER_SEND这两个标志位来定义“发送时RTS是高还是低”。再说GPIO方案。很多RS485收发芯片的DE发送使能和RE接收使能是分开的两个引脚硬件设计上经常把两个引脚直接短接成一个方向控制脚。高电平进入发送模式低电平进入接收模式。这时候你在设备树里需要声明一个GPIO并且注意极性。我用过的板子上典型写法是这样的uart3 { pinctrl-names default; pinctrl-0 uart3_pins_a; rs485-gpios pio 1 18 GPIO_ACTIVE_HIGH; linux,rs485-enabled-at-boot-time; status okay; };注意这里的GPIO_ACTIVE_HIGH不是随便写的。它要和你实际的硬件电路对应。如果收发芯片的DE引脚要求高电平才发送那么GPIO_ACTIVE_HIGH是对的如果你的电路上中间还加了一级三极管反相那这里可能就要改成GPIO_ACTIVE_LOW。我见过有人在这里反复折腾了一整天最后发现是原理图上DE前面加了一个反相器。2.2 用GPIO模拟DE/RE方向控制时的设备树样例上面那段设备树里用的rs485-gpios属性名并不是Linux标准内核的属性。标准8250驱动里更常见的是rs485-rts-delay和linux,rs485-enabled-at-boot-timeGPIO控制方向在部分厂商BSP里才会这样实现。所以你在全志SDK里看到rs485-gpios说明这个驱动是厂商自己改过的一定要去源码里确认它的解析逻辑。我调试过的某块T113板子上设备树是这样配的uart3 { pinctrl-names default; pinctrl-0 uart3_pins_a; rs485-gpios pio 1 18 GPIO_ACTIVE_HIGH; rs485-rts-delay 1 1; linux,rs485-enabled-at-boot-time; status okay; };rs485-rts-delay后面的两个数字分别对应delay_rts_before_send和delay_rts_after_send单位是毫秒。前者是发送前把DE拉高到第一个数据bit出现在总线上的时间间隔后者是最后一个数据bit发完到DE拉低的时间间隔。这两个值在实际调试中经常要改我后面会专门用一节来讲怎么取值。还有一个坑是如果你的DE接到了GPIO上驱动在翻转方向时可能无法用标准的gpiod_set_value接口。原因很简单有些GPIO操作函数可能会睡眠但串口驱动的start_tx/stop_tx可能运行在中断上下文或持有自旋锁的临界区里。直接调用带_cansleep后缀的函数会导致内核睡眠在原子上下文系统直接死给你看。所以很多厂商驱动在“GPIO控制RS485方向”这个场景下会选择绕过GPIO子系统直接操作SoC的PIO寄存器来翻转电平。这也解释了为什么你在设备树里配了GPIO但用gpio命令去操作同一个引脚时发现它根本不听使唤。3. 收发切换的微妙时序那几百微秒才是RS485的灵魂如果说前面的配置是“能用”那收发切换时序就是把RS485调到“好用”的分水岭。RS485总线上出现数据错乱十有八九是方向切换的时机不对。原理很简单RS485是半双工你必须保证“发送完成”和“方向切换到接收”这两个动作之间有一个合理的时间间隔。切早了最后一个字节还没完全送上总线方向就被拉回接收态导致对端收到不完整的数据帧切晚了发送端一直占着总线对端的应答数据发不出来也可能造成总线冲突。3.1 TEMT才是真的发完了THRE不是我在调试过程中发现很多对RS485理解不深的驱动在判断“数据是否发送完成”时用错了状态位。UART发送路径上有两级缓冲发送保持寄存器THR和发送移位寄存器TSR。数据先被CPU写入THR再移到TSR最后通过移位寄存器一位一位地送到总线上。对应的状态位也有两个THRE表示THR空了CPU可以往FIFO里填下一批数据TEMT表示THR和TSR都空了也就是所有数据已经真正从引脚上送出去了。RS485方向切换必须等TEMT不能只等THRE。有些驱动为了省事在start_tx里把数据丢进FIFO之后等到发送完成中断来了就直接切方向。发送完成中断触发的时机在实际硬件上经常是FIFO里的数据已经全部进入移位寄存器但最后一个bit还在引脚上往外送。这时候你把DE拉低等于把最后一个字节的后半截直接掐断了。对端收到的帧就少了一个或几个字节校验自然过不去。我用逻辑分析仪抓过这种故障的波形数据帧的最后一个字节缺失而且缺失的bit数不固定完全取决于中断响应延迟。这种问题极其难查因为不是每次都丢而是一百次里偶尔丢几次。解决办法有两个方向一是驱动里明确判断TEMT状态再切方向二是加上足够的delay_rts_after_send用时间换可靠性。3.2 delay_rts_before_send和delay_rts_after_send怎么取这两个参数的设计初衷是补偿两段硬件的延迟RS485收发芯片从DE拉高到真正使能驱动器输出有一段建立时间从DE拉低到驱动器完全释放总线也有一段释放时间。不同芯片的延迟参数差距很大便宜的几微秒贵的可以到几十微秒。所以在不知道具体芯片参数的情况下宁可多给一点时间也别省。我在实际项目里一般先按波特率粗算一个字节的传输时间然后把延时设置在半个到一个字节时间之间。粗算公式很简单假设8N1格式波特率为B一位的时间是1/B秒一个字节是10位8位数据起始位停止位。波特率1位时间1字节时间9600104.17us1.04ms3840026.04us260.4us1152008.68us86.8us2500004us40usdelay_rts_before_send如果是接收方通常可以设小一点1ms以内足够。但如果你用的是那种很差的RS485芯片DE建立时间标称几十微秒建议至少留1位时间。delay_rts_after_send则相反它影响的是对方从站能不能立刻抢占总线回复数据。我遇到过一个从站模块要求主站发送完最后一字节后至少保持DE拉高1ms以上否则它检测不到完整帧。这个要求明显大于一个字节时间最后我按它的要求把delay_rts_after_send调到了2ms问题解决。3.3 DMA完成中断不等于发送完成DMA的问题是RS485时序里最容易翻车的隐蔽角落。很多人为了让高波特率下CPU占用率低一些开启了UART的DMA发送。DMA的完成中断代表什么代表DMA控制器已经把数据从内存搬到了UART的FIFO里并不代表FIFO里的数据已经全部从总线上送出去了。如果你在DMA完成中断里直接切RS485方向那你基本上等于告诉对端“我最后那几个字节不要了”。我在T113上实测过开启DMA发送后如果不做任何处理115200波特率下每发送几百帧就会出现一次末尾字节丢失。这种丢帧问题靠应用层重试很难根治因为错误率不稳定。可行的方案有三种第一种发送完DMA数据后等TEMT置位再切方向第二种DMA完成中断触发后加上固定的延时延时时间按最大帧长计算保守但可靠第三种干脆小数据帧不用DMA走PIO发送保证TEMT判断的准确性。我通常的做法是帧长小于64字节用PIO大于64字节才走DMADMA完成后再额外等一个字节时间的延时再切方向。4. 中断并发、回显和波特率自适应三个更难缠的边界问题方向切换时序搞定了还会遇到一类更隐蔽的问题它们不是每次都出现而是跟系统的调度、中断优先级、总线负载纠缠在一起。4.1 半双工时序下的临界区和RX_DURING_TX先看一个很常见的现象主站发送完数据、切回接收态之后收到了一帧“自己发出去的数据”。这是因为你的RS485收发器在发送模式下RX引脚其实也连着总线。如果硬件上没有做回显抑制主站自己的发送数据会通过收发器的接收通路传回UART。Linux内核里对应的标志位是SER_RS485_RX_DURING_TX。这个标志置位表示驱动允许在发送过程中同时接收数据。如果清除驱动应该保证发送期间不处理接收。但问题来了很多RS485芯片的DE/RE是短接的发送模式下RE也被拉高物理上就收不到总线上的数据所以回显这个现象往往取决于你的硬件电路设计。这里真正的难点是并发控制。发送方向切换这个动作和接收中断之间是有竞态的。假设驱动刚把DE拉低准备接收此时总线上来了一帧数据接收中断触发。但如果驱动在中断处理里又去操作RTS引脚就可能和正在进行的发送流程打架。这是我见过最多的内核崩溃原因之一。所以调试RS485时如果出现莫名其妙的内核卡死优先怀疑驱动里对RTS/GPIO的操作是不是进了临界区。一个务实的建议是在BSP驱动不够完善时不要在中断处理函数里直接调用uart_handle_cts_change之外的GPIO操作而是用tasklet或者workqueue延后处理方向切换。代价是切换延迟增加几十微秒但换来的是稳定性。4.2 为什么我劝你别在应用层硬等“自动波特率”很多做RS485总线的朋友都会遇到一个需求下位机不知道上位机的波特率需要自动适应。RS485没有时钟线波特率的唯一信息源就是数据流本身。Linux内核并没有提供通用的RS485自动波特率机制所以我见过很多项目是在应用层“猜”波特率。最常见的猜法把串口设置成接收BREAK信号。上位机先发一个连续的BREAK下位机收到后触发一个事件然后应用层遍历几个候选波特率逐个尝试看哪个能收到合法数据帧。这个方案看起来可行但实际上坑很多。第一切换波特率需要重新配置termios每次配置都有不小的延迟第二在总线上还有其他设备在通信时你盲目切换波特率会把总线搅乱第三如果协议本身没有同步帧设计光靠“猜”很难收敛。更可靠的做法是分层设计在协议层规定设备上电后先以一个固定波特率比如9600发送握手帧确认握手成功后再切换到工作波特率。这样Linux驱动这边完全不需要做任何特殊处理你只需要在应用层管理好两次波特率切换的时机。我做过几个工业项目这套方案比任何“自动波特率检测”都稳定而且出了问题也好排查。5. 从“能收发”到“能商用”完整排查链路与实测技巧最后分享一套我在T113上调试RS485的完整流程。这套流程帮我节省了大量盲调时间也让我能在现场快速定位是硬件问题、驱动问题还是应用层问题。5.1 用ioctl小工具和stty把配置抠出来先别急着写业务代码。内核里的RS485配置到底生效了没有用一个小工具就能验证。我把下面的代码交叉编译到板子上运行后直接打印当前串口的RS485配置#include stdio.h #include fcntl.h #include sys/ioctl.h #include linux/serial.h int main(int argc, char **argv) { struct serial_rs485 rs485; int fd; if (argc 2) { printf(usage: %s /dev/ttyS3\n, argv[0]); return 1; } fd open(argv[1], O_RDWR); if (fd 0) { perror(open); return 1; } if (ioctl(fd, TIOCGRS485, rs485) 0) { printf(RS485 enabled: %d\n, !!(rs485.flags SER_RS485_ENABLED)); printf(RTS on send: %d\n, !!(rs485.flags SER_RS485_RTS_ON_SEND)); printf(RTS after send: %d\n, !!(rs485.flags SER_RS485_RTS_AFTER_SEND)); printf(delay before: %u ms\n, rs485.delay_rts_before_send); printf(delay after: %u ms\n, rs485.delay_rts_after_send); } else { perror(TIOCGRS485); } close(fd); return 0; }如果这个程序返回RS485 enabled: 0说明驱动根本没有把配置保存下来设备树那套大概率没生效。如果返回TIOCGRS485: Unknown error 515这类错误说明驱动根本不支持这个ioctl那问题就大了得回到内核驱动层面去改。5.2 四类典型故障现象与对应波形我在调试中总结了一个快速对照表现场遇到问题先按这个表排除效率比瞎猜高一倍不止。故障现象判断方向最常见原因快速验证方法首字节丢失或变形发送侧发送前DE建立时间不够或RS485芯片建立慢逻辑分析仪抓RTS和TX看DE拉高到第一个bit的间隔尾字节缺失帧长度不稳定发送侧方向切换过早只等THRE/DMA完成就拉低DE在batch模式下连续发1000帧统计丢帧模式收到自己发出的回显数据硬件/驱动收发器DE/RE没短接或驱动未抑制RX_DURING_TX主站串口开回显测试确认是否收到自己的帧总线上多个设备互相干扰时序/地址DE释放过慢或从站应答过早总线冲突示波器抓总线波形看是否有两个设备同时驱动A/B线首字节丢失和设备调试中常说的“帧头错位”很容易混淆。如果是帧头错位多半是A/B线接反了在转换器端把A和B对调一下就好。如果是首字节的位宽明显变形那才是DE建立时间的问题优先加大delay_rts_before_send。5.3 一主多从测试脚本的搭建思路最后说一个我一直沿用的压力测试方法。单对单通信能过不代表总线场景能过一主多从时设备数量一多各种时序问题都会暴露出来。我建议搭建一个最简单的测试环境一个主站T113板子两个或三个从站设备每个从站有不同的设备地址。测试脚本的逻辑很简单主站循环给每个从站发送查询帧每个从站收到后延时固定时间回复数据。主站统计三类指标未收到回复的帧数、收到乱码回复的帧数、收到错误校验的帧数。脚本跑起来一晚上能跑几十万帧任何偶发问题都会暴露出来。从站的回复延时是必测项。我遇到过一批从站设备它们的回复延时差异很大有的从站收到查询帧后1ms就回复有的要3ms才回复。如果主站的delay_rts_after_send设得太小提前切回了接收态那也能收到但有些从站因为总线还没释放而错过了发送窗口问题就表现为“某个从站总是收不到回复”。我在调试中把这个现象记录下来后来发现是主站DE释放时间不够把delay_rts_after_send从0.5ms调到2ms所有从站全部恢复正常。我个人的体会是RS485在Linux下真正麻烦的不是协议本身而是“时序”和“谁拥有控制权”。把内核里start_tx、stop_tx和set_rs485这几个关键路径读透比在网上抄一百遍设备树配置都有用。这个系列后面我还会继续写应用层协议栈的设计尤其是Modbus类协议在T113上如何配合RS485时序做优化到时候再跟大家分享实测数据。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑