1. 项目背景与测试目标
1.1 这块板子为什么要测RS422
先说下我为什么折腾这件事。手头这块Zynq-7000板卡是客户定制的,板子上有4路隔离RS422接口,用来和工业现场的伺服驱动器、PLC控制器做长距离数据传输。Zynq-7000这颗芯片很有意思——双核ARM Cortex-A9配上可编程逻辑(PL),等于一颗芯片同时干了两件事:ARM核跑Linux系统负责逻辑控制,FPGA逻辑负责高速数据采集和协议解析。RS422这种工业现场常用的差分串口,正好需要这种异构架构来对接。
之前团队在这块板子上已经跑通了千兆网口和SD卡读写,但RS422一直是“硬件焊上去、驱动没联调”的状态。客户调试现场反馈说数据偶发乱码、丢字节,得先在Linux下走一遍完整的通信测试流程,确认到底是硬件问题、驱动问题,还是上层的串口配置参数没写对。
这次测试的最终目的有三个:一是验证4路RS422在Linux下能否被正确识别并稳定收发;二是测出每路口的实际波特率误差和误码率,给客户一个可接受的数据;三是固化一套测试步骤,后续产线烧完系统可以直接执行脚本验收,不用每次手动敲命令。
1.2 测试环境与硬件资源清单
设备清单这块得先说清楚,因为后面所有操作都依赖这套环境:
- 主控板:Zynq-7000系列,具体型号XC7Z020,双核ARM Cortex-A9跑在667MHz
- 操作系统:PetaLinux 2018.3构建的嵌入式Linux,内核版本4.14
- RS422接口芯片:板载4路,使用ADI的ADM2682隔离收发器,支持全双工
- 辅助调试板:另一块USB转RS422模块,用于和被测板对发数据
- 串口调试助手:PC端用secureCRT,截取收发数据做比对
这里有个关键点,Zynq-7000的PS端(ARM系统端)自带的UART控制器只有两个,而且是通过MIO引脚直接引出的,其中UART0默认绑定到调试串口,UART1是空闲的。如果要扩展多路RS422,通常有两个选择:一是用PS端的UART1配合外部收发器芯片,二是把UART控制器做成IP核放在PL端,再通过AXI总线挂到PS端。我这边用的是第二种方案,因为客户要求4路RS422,PS端本来就只有两个UART,不够用。
硬件连线方面,RS422是4线制的差分信号,A(同相端)、B(反相端)两对线,一对发送一对接收。接法上务必注意:板卡上的发送A要接对端设备的接收A,发送B接对端接收B,不能把同一个设备端的T和R短接,那是RS232的半双工玩法。如果手头只有DB9头子,一般2脚是发送A、7脚是接收B,但不同厂家的DB9引脚定义偶尔有差异,最好对着原理图确认一遍再动手。
2. 底层链路核验与驱动配置
2.1 为什么先查设备树而不是直接写应用层代码
很多人拿到板子的第一个动作就是写个open("/dev/ttyPS1")的测试程序去收发,然后发现收不到数据就开始怀疑这怀疑那。以我做嵌入式Linux这几年踩坑的经验,必须先确认底层链路通不通,再往上走应用层。因为RS422在Linux下的串口驱动框架里,和普通RS232复用同一套UART驱动机制,你没有直接在应用层配置差分信号的能力,能不能正常工作完全取决于设备树里有没有正确描述这路UART。
Zynq-7000的PL端挂UART,设备树里通常是这样的结构:
&axi_uart422_0 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x42C00000 0x10000>; interrupts = <0 29 4>; clock-names = "s_axi_aclk"; clocks = <&clkc 15>; current-speed = <115200>; device_type = "serial"; port-number = <2>; status = "okay"; };注意compatible字段用的是“xlnx,xps-uartlite”,这是Xilinx官方提供的UART Lite IP核,驱动在Linux内核里是现成的,文件名叫uartlite.c。如果你用的是UART 16550 IP核,compatible就得改成“xlnx,xps-uart16550-2.00.a”,对应驱动是8250系列。这两个驱动在内核配置里对应不同的选项,别搞混了——我遇到过有人把UART Lite的设备树配成16550的,结果驱动怎么Load都失败。
设备树还有一种写法就是用aliases直接指定端口号:
aliases { serial0 = &uart0; serial1 = &axi_uart422_0; };这种写法可以把PL端的UART固定映射到/dev/ttyS1,否则系统会按探测顺序自动分配,重启几次之后设备节点可能会漂移,测试脚本里写死的设备路径就失效了。工业设备上电重启之后节点变了是非常头疼的事情,所以建议直接在家目录写个udev规则,根据/sys/class/tty/ttyS1/device/of_node/compatible来固定设备名。
2.2 内核配置确认UART驱动已编入
设备树写好了,驱动还得在内核里开着才行。PetaLinux工程目录下执行:
petalinux-config -c kernel进入菜单找Device Drivers → Character devices → Serial drivers,确保下面这几个选项是开启的:
Xilinx UART Lite serial port supportXilinx UART 16550 serial port supportSupport for console on AMBA serial port
如果你是直接从Xilinx官方仓库拉的内核,这几个选项默认是打开的,但架不住有人手欠关掉过。确认完之后重新编译内核并打包:
petalinux-build petalinux-package --boot --fsbl zynq_fsbl.elf --fpga system_wrapper.bit --u-boot这里有个容易忽略的点:如果PL端的UART IP核是这次新加的,比特流(bit文件)里面必须包含这个IP的实例化,否则设备树里描述得再完美,硬件上根本没有对应的逻辑电路,驱动加载时也是“设备不存在”的错误。所以每次改完PL工程,记得重新生成比特流并重新打包BOOT.bin。
2.3 查看设备节点是否生成
烧录系统后,启动到Linux命令行,第一步就是验证内核有没有识别到串口设备。我习惯用这组命令:
dmesg | grep -i tty ls -l /dev/tty* cat /proc/tty/drivers如果一切正常,dmesg里能看到类似:
[ 1.456789] xuartps 42C00000.serial: ttyS2 at MMIO 0x42C00000 (irq = 29) is a XUARTPS注意看结尾的XUARTPS还是uartlite,这个标识符能确认驱动绑定的类型。我这边4路PL端UART在/dev/ttyS1~/dev/ttyS4,其中ttyS1是UART Lite IP核0号实例,其余类推。
如果/dev下面没有对应节点,多半是设备树没配对或者驱动没编进去。排查手段是从/proc/device-tree下手,直接把设备树子节点导出来看:
ls /proc/device-tree/axi@0/serial@42C00000/ cat /proc/device-tree/axi@0/serial@42C00000/compatible直接直视内核视角下的设备树内容,比自己翻dts源码更快定位问题。
3. RS422通信测试工具与链接操作
3.1 测试环境里有哪几种可用工具
Linux下的串口测试工具,常用的就这几个:microcom、minicom、picocom、putty,再加一个没有交互界面的stty。嵌入式板子上空间有限,我一般用microcom,因为它只依赖一个动态库,体积够小,而且支持直接指定波特率、数据位、校验位。
如果没有现成的工具,busybox里通常会带一个缩水版的microcom,命令格式是:
microcom -s 115200 -t 5000 /dev/ttyS2-s指定波特率,-t指定超时毫秒数,不加-t的话就一直挂在那里等数据。但注意,busybox的microcom功能和完整版有差距,有些参数不支持,比如数据位和校验位只能在stty里先设置好,microcom不会帮你改。
完整工具链部署到嵌入式板子上的思路是:先在PC上交叉编译好静态链接的二进制,再扔到板子的/usr/bin或/opt目录。下面是我的交叉编译流程:
wget https://github.com/ravynsoft/microcom/archive/refs/tags/v1.0.tar.gz tar xf v1.0.tar.gz && cd microcom-1.0 export CROSS_COMPILE=arm-linux-gnueabihf- make CC=${CROSS_COMPILE}gcc因为嵌入式Linux动态库版本可能和PC不完全兼容,我倾向于用-static参数编译:
make CC=${CROSS_COMPILE}gcc LDFLAGS="-static"这样生成的microcom在板子上就能直接用,不会出现“libncurses.so.5找不到”这种尴尬事。
3.2 回环测试验证本板收发通路
我把RS422的回环测试放在所有联调测试的第一步,因为它能最快验证板卡自身的UART控制器和收发器芯片是否工作正常。回环测试有两种做法:一种是直接把发送端的A接到本板接收端的A,发送端B接到本板接收端的B,用杜邦线或者短接帽在接线端子上面短接;另一种是利用RS422收发器芯片的“自动回环”功能,把DE/RE引脚拉高让收发器处在自发自收模式,不过这种方式没法验证外部接线问题,我建议还是老老实实短接端子。
短接好之后,用stty配置串口参数:
stty -F /dev/ttyS2 115200 cs8 -cstopb -parenb raw参数含义逐个解释:115200是波特率,cs8表示8位数据位,-cstopb表示1位停止位(减号表示否定,即关掉2位停止位选项),-parenb表示无校验。raw这个参数很重要,它让驱动程序不做任何行处理(比如把\n自动变成\r\n,或者把收到的\r自动忽略),这样才能保证收发内容字节完全一致。
配好之后再验证一下:
stty -F /dev/ttyS2 -a然后往里写测试数据:
echo "hello rs422" > /dev/ttyS2因为发送端和接收端在板卡内部被短接在一起,理论上一毫秒后数据就会原封不动地回来,用cat命令应该能读到:
timeout 1 cat /dev/ttyS2如果回显了你刚写的内容,说明UART控制器数据通路OK。如果没回显,先别急着怀疑驱动,确认一下短接的针脚是不是真的短接到了正确的接收差分对上——RS422的A/B极性接反,是完全静默的,什么数据都不会有,不像RS232那样至少还有个电压信号。
3.3 与PC端USB转RS422模块对发
回环测试通过后,可以开始和PC端模块对发测试了。这里的接法是:
| 板卡RS422端子 | USB转RS422模块端子 | 说明 |
|---|---|---|
| TX_A | RX_A | 板卡发送接到对端接收 |
| TX_B | RX_B | 板卡发送-接到对端接收- |
| RX_A | TX_A | 板卡接收A接到对端发送A |
| RX_B | TX_B | 板卡接收B接到对端发送B |
| GND | GND | 公共地必须连 |
很多人连接RS422时不接地线,觉得差分信号不需要公共地,这个认知是错的。RS422虽然抑制共模干扰能力强,但它并非完全隔离——收发器芯片内部的ESD保护二极管总得有泄放路径,悬空的参考地会让共模电压漂移,长时间运行之后偶尔出现一个乱码字节就是这个原因。尤其是在实验室里台式PC和设备不共地的情况特别常见,接上GND之后稳定性会明显改善。
PC端的secureCRT新建一个串口会话,选择USB转RS422对应的COM口,波特率115200、8位数据位、1位停止位、无校验、无硬件流控。然后开始双向测试。
板卡发、PC收的命令:
for i in $(seq 1 100); do echo "test_$i"; sleep 0.1; done > /dev/ttyS2在secureCRT里应该能连续看到100条test记录,每一条都不会丢。如果中途有乱码或者漏数据,先看是不是线缆太长——RS422理论上可以到1200米,但这里有个前提是波特率越低距离越远;在115200波特率下超过100米就开始有衰减风险了。实验室环境一般不超过5米,线缆长度基本可以排除。
PC发、板卡收的命令:
timeout 5 cat /dev/ttyS2然后用secureCRT的“发送文件”功能,发一个包含“0123456789abcdef”循环的文本文件,大小建议控制在10KB以内,太大会让嵌入式板子上的缓冲区溢出。板卡终端打印出来的内容应该和发送的完全一致。
3.4 老化测试与误码统计
单条数据对发成功之后,还不能立刻给客户交差,得做一轮持续性的老化测试来验证稳定性。我之前遇到过一种“薛定谔的乱码”——单帧数据收发正常,跑个半小时就随机蹦出一个错字节,这种偶发故障最让人头疼,必须靠长时规避性测试暴露它。
老化测试我习惯用脚本来做统计,核心思路是构造一个带序号的数据包发送,对端收到后检查序号是否连续。板卡端脚本:
#!/bin/sh count=0 while true do count=$((count+1)) echo "PACKET_${count}_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" > /dev/ttyS2 sleep 0.01 donePC端用Python写个监听脚本统计接收结果:
import serial import time ser = serial.Serial('COM3', 115200, timeout=1) last_seq = 0 error_count = 0 total_count = 0 start_time = time.time() while time.time() - start_time < 3600: line = ser.readline().decode('utf-8', errors='ignore').strip() if not line: continue total_count += 1 try: seq = int(line.split('_')[1]) if seq != last_seq + 1: print(f"seq jump: {last_seq} -> {seq}") error_count += 1 last_seq = seq except (IndexError, ValueError): print(f"parse error: {line}") error_count += 1 print(f"total: {total_count}, errors: {error_count}, error_rate: {error_count/total_count*100:.4f}%")这个脚本够简单也够实用。跑1小时,如果有跳序号或者解析错误,直接看总数和误码率,比人肉盯屏幕靠谱得多。间隙我会顺便测测不同波特率下的表现,常用挡位是9600、19200、38400、57600、115200这5档。
4. 排障实录与常见问题速查
4.1 端口节点不开或者权限不够
我手里这块板子,跑完PetaLinux后偶发过一次/dev/ttyS2节点消失的情况,多方排查后定位是设备树里中断号冲突了。PL端UART的中断号是在Vivado里给IP核分配的呢,如果两个IP核用了同一个中断号,Linux中断子系统会拒掉后注册的那个驱动,对应的tty设备也就创建不出来。
排查方法:先看dmesg | grep -i irq,确认有没有IRQ request failed字样;然后在Vivado里打开Address Editor,逐个核对中断号。
比中断冲突更常见的是权限问题。非root用户访问/dev/ttyS2会提示Permission denied,多数人第一反应是chmod 666 /dev/ttyS2——能用,但一重启就没了。正确姿势是加udev规则:
echo 'KERNEL=="ttyS[0-9]*", MODE="0666"' > /etc/udev/rules.d/99-rs422.rules重新插拔或者重启之后,普通用户就能直接访问了。
4.2 收不到数据时的三级排查路径
如果在回环模式下都收不到数据,按这个顺序排查,能省很多冤枉时间:
第一级:查物理层。用万用表量RS422收发器的A-B引脚之间电压,正常空闲状态应该在2V到6V之间。如果量出来是0V,大概率发送端没使能,检查发送使能引脚有没有被硬件拉高。
第二级:查驱动层。执行cat /proc/tty/driver/serial看串口状态:
0: uart:XTUARTPS port:00000000 irq:0 tx:0 rx:0 1: uart:XTUARTPS port:00000000 irq:0 tx:0 rx:0 2: uart:XTUARTPS port:42C00000 irq:29 tx:0 rx:0如果tx和rx计数始终是0,说明驱动层面就没有数据流经过,问题往内核外设方向查;如果tx计数在涨但rx是0,说明发送通路OK、接收通路有问题,重点查接线和收发器的RE引脚。
第三级:查设备树。确认设备树里UART节点的interrupts属性和Vivado里配置的保持一致,这个排查在4.1里已经说过了,不再赘述。
4.3 乱码和奇偶校验问题
RS422乱码的情况比收不到数据更磨人。有一次客户报障说“通信不稳定,大概一分钟出一个错码”,我用示波器抓A-B差分波形才定位——发送端信号幅值只剩1.8V了,衰减严重。查了一圈是板卡到设备的电缆中间过了一个转接端子,那个转接端子的焊点虚焊了,接触电阻变大导致信号幅值跌落。
如果你用示波器量波形没问题,那大概率是波特率不准或者奇偶校验配置不对。RS422的标准帧格式是“起始位 + 8数据位 + 校验位(可选) + 停止位”,如果一端开了校验另一端没开,接收端会把校验位当成数据位来读,出来的内容必然错位乱码。排查方法是两端都改成无校验、8位数据位、1位停止位,最通用的配置先跑通再精细化调整。
另外一个隐藏很深的坑:某些工控设备在上电瞬间会往总线上吐一段初始化数据,如果这段数据和业务数据混在一起,接收端的逻辑控制不好就会整体错位。通俗讲就是一个包含5个字节的数据包回来了,但你只收到3个字节,剩下2个字节被当成下一包来解析了。这种情况软件上要做的是启停位和超时判断,硬件上无解。
4.4 速率上不去与缓冲区溢出
我测试时还遇到过一种情况:波特率115200、每包256字节、每秒发100包,结果PC端频繁丢弃数据。这不是波特率不够,而是嵌入式Linux的tty缓冲区在驱动层有上限。Linux内核里tty_flip_buffer默认的缓冲区大小大约是64KB,如果应用层读数据的速度跟不上数据到达的速度,缓冲区满了之后新数据直接被丢弃。
优化思路有三个方向:
- 应用层改用
poll+read的非阻塞方式,加大每次读取的字节数,及时把数据从内核态搬到用户态; - 内核配置中调整
/proc/sys/kernel/printk,减少内核打印抢占CPU的时间; - 如果还不行,就要考虑在PL端加FIFO缓存,把UART收到的数据先暂存到FPGA内部的Block RAM里,然后由DMA批量搬运到内存,这已经是高性能方案了。
4.5 常见问题速查表
| 故障现象 | 可能原因 | 处理方法 |
|---|---|---|
| 设备节点找不到 | 设备树未描述该UART / 驱动未编入 | dmesg看报错,核对设备树compatible字段 |
| 设备节点在但打不开 | 权限问题 | 加udev规则,改文件权限 |
| 能写入但收不到 | 接线错误 / 收发器未使能 | 万用表量AB差分电压,检查DE/RE引脚 |
| 偶发乱码 | 线缆过长 / 焊接不良 / 共地问题 | 检查线缆和接插件,确保GND相连 |
| 波特率漂移 | 晶振精度不足 | 换更高精度晶振或软件校准 |
| 数据丢包 | tty缓冲区溢出 | 应用层及时读取 / PL端加FIFO |
5. 实操总结与几点个人经验
来回折腾了两天,总算把这块板子上的4路RS422接口全部调通了。回顾整个测试过程,最深的感受就是串口通信调试不存在“一步到位”,它极其依赖你把底层链路一步步踩实:设备树描述对不对、驱动有没有绑上、硬件接线有没有接反、参数配置是否一致,每一步都会让你前面的假设全部推倒重来。
我个人在实际操作中还有一个习惯:所有测试脚本和命令都沉淀成一份shell脚本放到板子的/home/root目录下,包含自检、双向对发、老化统计三个子命令。产线同事拿到板子之后不需要理解任何RS422原理,跑一条./rs422_test.sh loopback就知道硬件通不通,跑一条./rs422_test.sh aging 3600就能拿误码率报告,这才是嵌入式测试应该有的交付形态 —— 把复杂链路固化成一条可重复执行的命令,比写十页测试文档有用得多。
最后再分享一个小技巧:调试RS422的时候,在示波器上同时抓A线和B线,不要只看单端波形——差分信号的真谛在于A-B的差值,只有一对线放在一起观察,才能准确判断共模干扰和幅值衰减是否正常。很多人一开始只挂A线到示波器上,看到正弦波还觉得没问题,结果其实共模噪声早就超标了,这一条经验至少帮我省了三个小时的排障时间,今天一起整理出来,希望对后来人能少踩一些坑。