公司动态
RK35XX平台Linux内核RS-485驱动适配实战:从UART到工业通信
1. 项目缘起当RK35XX遇上工业485最近在折腾一块基于瑞芯微RK35XX系列芯片的工控板核心需求是要驱动一个标准的RS-485接口用来连接现场的PLC或者各种仪表。这听起来是个很常规的嵌入式开发任务对吧但真动起手来才发现从应用层调用write/read到数据稳定无误地在485总线上收发中间隔着一整个内核驱动的世界。市面上很多教程和SDK要么只讲应用层串口编程要么给的驱动例子就是个“半成品”直接拿来用在要求严格的工业现场大概率会出各种灵异问题——比如数据包被截断、自发自收、或者直接收不到数据。所以我决定把这次在RK35XX平台内核层完整适配485驱动的过程记录下来。这不是一个简单的“点亮”过程而是深入到Linux内核的串口子系统、设备树Device Tree、以及485收发控制逻辑的完整实践。你会看到如何从零开始让一个通用的UART驱动变身成可靠的485通信节点其中涉及的引脚复用、收发时序控制、设备树配置都是实实在在的坑。无论你是在做类似的工控项目还是单纯想深入理解Linux串口驱动和RS-485硬件这篇长文都能给你提供一条清晰的路径和一堆踩过的坑。2. RK35XX UART子系统与485的本质区别在动手改驱动之前必须彻底想明白普通的UART通用异步收发传输器驱动和RS-485驱动到底差在哪如果这个没搞清楚后面的所有修改都是空中楼阁。2.1 UART点到点的全双工通信我们平时在RK35XX上用的/dev/ttySx设备背后是标准的UART控制器。它通常有TX发送、RX接收两根线有时还有RTS/CTS等硬件流控线。通信是全双工的即发送和接收可以同时进行互不干扰。在驱动层面内核的serial_core框架以及瑞芯微提供的8250系列驱动drivers/tty/serial/8250/已经处理好了绝大部分工作配置波特率、数据位、停止位、奇偶校验管理发送和接收FIFO通过中断或DMA搬运数据。应用层打开设备文件读写即可。2.2 RS-485半双工与方向控制RS-485则是一种半双工、差分信号的通信标准。它通常只用一对线A和B进行数据传输所有设备都挂在这对总线上。这就带来了一个核心问题在任意时刻总线上只能有一个设备在发送数据其他设备都处于接收状态。因此每个485设备都需要一个额外的控制信号来切换自身的收发状态。这个信号通常被称为“方向控制引脚”Direction Control Pin或“使能引脚”Enable Pin我们姑且叫它DEDriver Enable或/REReceiver Enable低有效很多时候这两个信号会合并成一个引脚控制。关键区别就在这里硬件上比UART多了一个GPIO引脚来控制收发方向。时序上必须在开始发送数据前将总线切换到发送模式拉高DE并在数据发送完成后延迟一小段时间确保最后一个字节完全从移位寄存器发出再将总线切换回接收模式拉低DE。这个切换时机至关重要早了会切掉数据尾晚了会阻塞总线影响其他设备响应。标准的Linux UART驱动没有内置这个“方向控制”的逻辑。它只管把数据扔给TX FIFO硬件自己发出去至于外面接的是RS-232电平还是RS-485差分信号它不关心。因此适配485驱动的核心就是在内核驱动层面为UART增加方向控制的能力并精确控制其切换时序。2.3 RK35XX的硬件基础RK35XX系列芯片通常集成多个UART控制器如UART0-UART9。这些控制器在物理上仍然是标准的UART IP核。我们要做的是指定其中一个UART的某个引脚可以是普通的GPIO也可以是UART控制器本身支持的RTS/CTS等流控引脚复用为方向控制功能作为485的DE引脚并通过驱动代码去操作它。首先通过芯片的TRM技术参考手册确认两件事目标UART控制器我使用的是UART2。可用的方向控制引脚查看UART2相关的引脚复用表。理想情况下可以使用其RTSnRequest to Send引脚作为485方向控制。因为RTS本身就是一个输出信号在硬件流控中用于“请求发送”其行为逻辑发送前有效发送后无效与485方向控制有相似之处很多串口芯片如MAX3485的DE引脚就是直接连接RTS。如果硬件设计没用RTS那就需要找一个普通的GPIO并在设备树中将其配置为输出模式。3. 内核驱动适配修改串口驱动以支持485Linux内核其实已经为RS-485支持打下了基础。在include/linux/serial.h中定义了一个重要的结构体serial_rs485以及配套的ioctl命令TIOCSRS485和TIOCGRS485。这为我们扩展驱动提供了标准接口。我们的目标是修改RK35XX所使用的串口驱动通常是基于8250系列的驱动如drivers/tty/serial/8250/8250_port.c等使其能够响应应用层通过ioctl设置的485参数并在数据收发时自动控制方向引脚。3.1 驱动修改的核心步骤假设我们使用UART2并使用其RTSn引脚对应某个GPIO Bank的Pin X作为方向控制。1. 在驱动代码中使能RS485支持首先需要确保串口端口struct uart_port支持RS485。在驱动初始化端口的地方例如在serial8250_register_8250_port或平台驱动的probe函数中需要设置端口的相关标志和初始化rs485结构。/* 在你的平台驱动或端口初始化代码中 */ struct uart_8250_port *up ...; // 获取你的端口结构 struct uart_port *port up-port; /* 初始化rs485结构默认禁用 */ memset(port-rs485, 0, sizeof(port-rs485)); port-rs485.flags SER_RS485_ENABLED; // 关键启用RS485模式 port-rs485.delay_rts_before_send 1; // 发送前RTS激活的延迟单位毫秒 port-rs485.delay_rts_after_send 1; // 发送后RTS保持的延迟单位毫秒 /* 非常重要告诉内核该端口支持RS485 */ port-flags | UPF_HARD_FLOW; // 有些驱动通过这个标志判断 port-rs485_supported your_rs485_support; // 指向一个描述支持特性的结构2. 实现RTS引脚的控制回调内核的8250核心代码在发送数据前后会检查端口是否启用了RS485模式port-rs485.flags SER_RS485_ENABLED。如果启用它会尝试调用一个名为rs485_start_tx和rs485_stop_tx的回调具体函数名可能因内核版本略有差异。我们需要在驱动中实现这些回调或者更常见的是复用已有的RTS控制函数。对于许多使用8250核心的驱动RTS控制已经通过serial8250_em485_handle_start_tx和serial8250_em485_handle_stop_tx这样的函数处理了。我们需要确保驱动编译时包含了CONFIG_SERIAL_8250_RS485配置。驱动正确关联了控制RTS引脚的具体GPIO操作。3. 关联GPIO操作如果硬件设计使用普通的GPIO而非UART原生RTS我们需要在驱动中获取这个GPIO并实现控制函数。#include linux/gpio/consumer.h struct your_port_private_data { struct gpio_desc *rs485_rts_gpio; // 方向控制GPIO描述符 // ... 其他数据 }; static void your_rs485_rts_control(struct uart_port *port, int on) { struct your_port_private_data *priv port-private_data; if (!priv || !priv-rs485_rts_gpio) return; /* on为1表示进入发送模式驱动使能拉高GPIO */ gpiod_set_value(priv-rs485_rts_gpio, on); } /* 在probe函数中获取GPIO */ priv-rs485_rts_gpio devm_gpiod_get_optional(pdev-dev, rs485-rts, GPIOD_OUT_LOW); if (IS_ERR(priv-rs485_rts_gpio)) { // 错误处理 } if (priv-rs485_rts_gpio) { // 将这个控制函数注册到port的某个操作集里 // 具体注册方式取决于驱动框架可能需要挂接到port-ops-set_mctrl或自定义回调 }4. 处理延时Delay参数serial_rs485结构中的delay_rts_before_send和delay_rts_after_send是毫秒级延时。内核核心代码会在切换RTS引脚前后插入相应的忙等待udelay或mdelay。这个延时是必须的尤其是delay_rts_after_send。它确保了最后一个字节的停止位完全发出后才释放总线控制权防止数据被“切断”。具体延时值需要根据波特率计算通常至少保证1-2个字符的传输时间。例如在9600波特率下发送1个字节包括起始位、数据位、停止位大约需要1ms那么delay_rts_after_send设为1-2ms是合理的。3.2 设备树DTS配置驱动准备好后需要在设备树中声明如何启用UART2的485功能。这是将硬件连接信息告诉内核的关键。// 在rk35xx.dtsi或板级.dts文件中 uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer uart2m0_rts; // 假设使用m0引脚组并包含RTS引脚 // 或者如果使用普通GPIO // pinctrl-0 uart2m0_xfer; // rs485-rts-gpios gpio3 RK_PC1 GPIO_ACTIVE_HIGH; // 例如GPIO3_C1 // RS485参数配置 rs485-enabled; rs485-rts-active-high; // 如果方向控制引脚高电平有效 rs485-rts-delay 1 1; // 分别对应发送前和发送后的延时毫秒 linux,rs485-enabled-at-boot-time; // 可选启动时就启用485模式 };设备树配置解析rs485-enabled 这是一个布尔属性告诉驱动该UART用于RS485。rs485-rts-active-high 定义方向控制引脚的有效电平。高电平有效意味着发送时拉高接收时拉低。如果不指定默认可能是低电平有效。rs485-rts-delay 两个32位数字分别对应delay_rts_before_send和delay_rts_after_send。单位是毫秒。rs485-rts-gpios 如果方向控制使用非UART原生的普通GPIO则用此属性指定GPIO。驱动会通过gpiod_get_optional来获取。linux,rs485-enabled-at-boot-time 有些驱动需要这个属性来确保在驱动探测probe阶段就初始化RS485模式而不是等到应用层设置。这对于需要通过该串口进行早期调试或启动日志输出的场景很重要。3.3 编译与测试修改完驱动代码和设备树后重新编译内核和dtb。编译内核确保CONFIG_SERIAL_8250_RS485y被选中。make ARCHarm64 menuconfig # Device Drivers - Character devices - Serial drivers - 8250/16550 and compatible serial support - Support for RS485 make ARCHarm64 -j$(nproc)编译设备树make ARCHarm64 dtbs烧录与启动将新的内核镜像如Image和设备树二进制文件如rk3568-evb.dtb烧录到开发板并启动。系统内验证检查串口设备是否存在ls /dev/ttyS2。查看内核启动日志dmesg | grep ttyS2应该能看到串口初始化成功的信息可能包含rs485相关的提示。使用ioctl测试可以写一个简单的C程序使用TIOCGRS485命令读取当前485配置使用TIOCSRS485命令设置配置。这是验证驱动层是否响应标准接口的最直接方法。4. 应用层使用与避坑指南驱动和设备树都搞定后在应用层使用这个485串口就和普通串口几乎一样了但有几个致命细节必须注意。4.1 标准用法ioctl设置模式一个健壮的应用应该在打开串口设备后首先用ioctl将其明确设置为RS485模式。#include sys/ioctl.h #include linux/serial.h int set_serial_rs485(int fd, int enable) { struct serial_rs485 rs485conf; // 读取当前配置 if (ioctl(fd, TIOCGRS485, rs485conf) 0) { perror(TIOCGRS485); return -1; } if (enable) { rs485conf.flags | SER_RS485_ENABLED; // 设置延时参数单位毫秒 rs485conf.delay_rts_before_send 1; rs485conf.delay_rts_after_send 1; // 如果你的硬件是RTS低电平有效可能需要设置 SER_RS485_RTS_ON_SEND 等标志 // rs485conf.flags | SER_RS485_RTS_ON_SEND; } else { rs485conf.flags ~SER_RS485_ENABLED; } // 写回配置 if (ioctl(fd, TIOCSRS485, rs485conf) 0) { perror(TIOCSRS485); return -1; } return 0; }注意SER_RS485_RTS_ON_SEND和SER_RS485_RTS_AFTER_SEND等标志用于控制RTS引脚在发送期间还是发送之后有效。这需要和硬件设计以及驱动实现严格匹配。大部分情况下我们期望的是“发送期间有效”即SER_RS485_RTS_ON_SEND。务必查阅驱动代码或通过实验确认。4.2 避坑实战那些容易翻车的地方坑1自发自收Loopback现象发送的数据自己立刻就能收到。原因这是485调试中最常见的问题。硬件上485芯片的接收器在发送时如果未正确隔离会听到自己发出的信号。软件上如果方向控制切换太慢delay_rts_before_send太小可能在TX引脚开始变化时DE引脚还未有效导致差分总线处于未驱动状态而接收端可能误触发或者切换太快delay_rts_after_send太小发送未结束就切回接收收到了自己数据的尾巴。解决硬件检查确保485芯片的/RE接收使能和DE发送使能引脚连接正确。通常它们可以接在一起由同一个GPIO控制。检查A、B线是否接反终端电阻120Ω是否在总线两端正确连接。软件调整重点调整delay_rts_after_send。将其从1ms逐步调大例如调到2ms、5ms观察自发自收是否消失。用示波器同时测量TX信号和DE控制信号是最直接的调试方法。坑2数据包尾部丢失现象接收方收到的数据最后一个或几个字节经常丢失。原因delay_rts_after_send设置过小方向控制过早切回接收模式导致最后一个字节的停止位尚未完全发出就被“掐断”。解决增加delay_rts_after_send的值。计算公式可以粗略估算延时(ms) (1 / 波特率) * (数据帧位数) * 1000 * 安全系数。例如115200波特率1个字节10位帧约87μs设1ms已有较大余量。但考虑到驱动调度、中断延迟从经验出发9600波特率下1-2ms115200下0.5-1ms是常用起点。坑3驱动未生效配置ioctl失败现象应用层调用TIOCSRS485返回-1错误码ENOTTY不是tty设备或ENOSYS功能未实现。原因内核驱动未正确实现RS485支持。可能CONFIG_SERIAL_8250_RS485未开启或者驱动代码中的port-rs485_supported未正确设置port-ops中没有支持RS485的操作。解决确认内核配置。检查驱动初始化代码确保port-rs485结构被初始化且port-rs485.flags包含了SER_RS485_ENABLED。在驱动中打印调试信息看ioctl调用是否进入了你的驱动处理函数。坑4多线程/多进程访问冲突现象多个线程或进程同时读写同一个485串口数据混乱。原因RS485是半双工软件层必须严格同步收发状态。如果线程A正在发送总线处于发送模式线程B此时尝试发送会导致数据碰撞。如果线程B尝试接收可能收到A发送的数据但逻辑上这不是它期望的。解决必须在应用层做严格的互斥锁。将整个“切换发送模式 - 写数据 - 等待发送完成 - 切换回接收模式”的过程封装成一个原子操作并用互斥锁pthread_mutex_t保护。对于简单的单进程多线程一个全局锁即可。对于多进程可能需要使用文件锁fcntl的F_SETLK。4.3 高级话题与常用的串口库集成很多项目使用libserial、Qt的QSerialPort或Python的pyserial。这些库底层通常也使用ioctl。pyserial在Python中可以在打开端口后设置rs485_mode。import serial ser serial.Serial(/dev/ttyS2, baudrate9600, timeout1) # 关键设置RS485模式 ser.rs485_mode serial.rs485.RS485Settings() # 或者更详细的设置 ser.rs485_mode serial.rs485.RS485Settings( rts_level_for_txTrue, rts_level_for_rxFalse, delay_before_tx0.001, delay_before_rx0.001 )注意pyserial的RS485支持需要底层驱动即我们刚才修改的内核驱动支持标准TIOCSRS485ioctl。C/QtQSerialPort类有setRequestToSend()和setDataTerminalReady()方法但这是用于硬件流控的。对于RS485Qt本身没有直接属性。你需要用ioctl自己实现或者使用第三方库如QSerialDevice。5. 调试技巧与工具推荐内核驱动调试离不开日志。在驱动代码的关键路径添加printk或dev_dbg/dev_info是必须的。// 在驱动控制RTS的函数中添加 dev_dbg(port-dev, RS485: Setting RTS pin to %s for sending\n, on ? HIGH : LOW);查看驱动打印的信息# 动态查看内核日志 dmesg -w # 或者查看特定驱动的日志级别 echo -n module 8250_early p /sys/kernel/debug/dynamic_debug/control # 假设驱动模块名是8250_early硬件调试神器逻辑分析仪或示波器软件调得再顺硬件波形不对也是白搭。一个几十块钱的逻辑分析仪比如Saleae Logic的克隆版就能极大提升效率。看什么TX线查看从UART控制器发出的原始TTL电平信号。DE/RTS线查看方向控制引脚的波形。A-B差分线如果有条件直接看485总线上的差分信号。验证什么时序对齐DE信号必须在第一个字节的起始位开始之前就变为有效拉高并在最后一个字节的停止位结束之后再变为无效拉低。用逻辑分析仪的放大功能仔细对齐起始位和DE的边沿。延时参数测量DE有效到第一个起始位的时间对应delay_rts_before_send以及最后一个停止位结束到DE无效的时间对应delay_rts_after_send。看是否与软件设置相符。总线状态在非发送时段DE应为无效此时A-B线之间应有稳定的空闲电平通常接收器会有一个失效保护偏置使总线处于确定状态。6. 从驱动到稳定通信的完整链路最后我们来串联一下从应用层调用到物理信号发出的完整数据流这能帮你建立全局观应用层调用write(fd, data, len)。内核VFS/TTY层数据经过文件系统、tty子系统到达串口核心层serial_core。8250驱动层检查port-rs485.flags SER_RS485_ENABLED。如果启用在启动发送引擎前调用rs485_start_tx回调或相关函数将方向控制GPIO拉高发送模式。并插入delay_rts_before_send毫秒的延时。将数据写入UART的发送FIFO。UART控制器自动将数据按帧起始位、数据位、停止位移位输出到TX引脚。发送完成后可能是通过发送完成中断判断调用rs485_stop_tx回调插入delay_rts_after_send毫秒延时然后将方向控制GPIO拉低接收模式。硬件层方向控制GPIO的电平控制着485芯片的DE引脚。当DE为高时485芯片的驱动器被使能将TX引脚上的TTL电平转换为A、B线上的差分信号广播到总线。当DE为低时驱动器关闭接收器使能芯片开始监听总线上的差分信号并将其转换为RX引脚上的TTL电平回传给UART控制器。整个过程驱动层的责任就是精准地控制那个GPIO的翻转时机确保驱动器只在“安全”的时间窗口内激活。这个时间窗口就是由波特率、数据帧长度以及那两个关键的delay_rts_*参数共同决定的。