ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android车载串口开发:UART、RS232、RS485选型配置与通信稳定性实战

Android车载串口开发:UART、RS232、RS485选型配置与通信稳定性实战 去年接手一个车载中控的辅控项目客户给的需求只有一句话让 Android 车机通过串口和底盘的几个控制板通信。听起来简单但真正动手才发现坑是一个接一个——Android 上的串口和 Linux 上的串口不是一回事UART、RS232、RS485 这三个词经常被混着叫配置参数错一位就是满屏乱码USB 转串口的芯片型号又各有各的脾气。这篇笔记就是把我从选型、配置、写代码到现场排障的整个过程捋一遍聊聊 Android 车载串口开发里 UART、RS232、RS485 各自的定位串口配置到底要动哪些参数数据通信怎么做才能稳。不管你是刚接触车载嵌入式的新手还是已经写过几行串口代码想补全知识点的老手应该都能从这里找到能直接抄的部分。我会尽量把为什么这么选讲清楚而不是只丢一段代码给你。1. 车载串口开发到底在做什么需求拆解与整体思路1.1 车载 Android 上串口都在干哪些活先说说为什么车载场景里 Android 还要去碰串口这种老古董。车机主控跑的是 Android负责屏幕、语音、导航、娱乐这些交互但真正管底盘、电源、灯控、空调风门的往往是几块独立的 MCU 控制板。这些板子成本敏感、实时性要求高用 CAN、LIN 或者串口跟主机通信都很常见。串口的好处是简单、便宜、驱动成熟一块几毛钱的 MCU 就能带一路 UART所以大量的辅控通信仍然走串口。具体一点常见的几类活电池管理板BMS上报电压电流温度灯光控制板接收开关指令座椅、后视镜调节板收位置命令还有各种传感器采集盒把数据打包往车机送。这些通信的共同特点是数据量不大、频率不高、但要稳掉一帧可能就是一个功能失效。你在 Android 侧要做的就是打开串口设备、按约定的参数配置、收发字节流、把字节流按协议拆成一条条报文、再交给上层业务逻辑去处理。这里有个容易踩的认知误区很多人以为 Android 就是个 Linux串口随便读写就行。实际上 Android 出于安全考虑对设备节点的访问做了 SELinux 限制和应用沙箱隔离普通 APK 直接 open(/dev/ttyS1) 大概率是 permission denied。所以车载 Android 上的串口开发从来不只是写通信逻辑还包括权限申请、系统集成、甚至要动到内核和设备树。这一步想不明白后面代码写得再漂亮也跑不起来。1.2 方案选型的三个前置判断动手之前我一般会先问三个问题这三个问题基本决定了后面整套方案怎么搭。第一个问题串口是主控芯片自带的还是外接 USB 转串口芯片如果是 SoC 原生 UART 引出的引脚那走的是 TTL 电平需要外接电平转换芯片才能接 RS232 或 RS485 设备设备节点通常是 /dev/ttyS0、/dev/ttyS1 这种。如果是通过 USB 口接的转换器那设备节点一般是 /dev/ttyUSB0 或者 /dev/ttyACM0这一路就牵扯到 USB 转串口芯片的驱动问题。第二个问题对面设备是点对点还是总线组网点对点通信UART 加电平转换就够如果要挂多个从设备比如一路串口接六块采集板那就得上 RS485 总线因为 RS232 只能一对一而且传输距离也就十几米RS485 差分信号可以拉到上千米还能一主多从。第三个问题通信是半双工还是全双工RS485 大多数应用是半双工同一时刻只能收或发需要控制收发方向RS232 是全双工收发线分开互不干扰。这个区别直接影响到你代码里的收发线程设计。把这三个问题回答清楚选型基本就定了原生 UART TTL 直连、原生 UART RS232 转换、原生 UART RS485 转换、USB 转串口 RS232/RS485一共就那么几种组合。我见过不少项目一开始没理清这层关系结果硬件做出来了才发现电平不匹配、方向控制没引出返工成本很高。2. UART、RS232、RS485 到底差在哪从协议到电气标准2.1 UART定义字节怎么排队往外送的协议UART 全称通用异步收发器它描述的是一套数据怎么组织成帧、怎么在这个时序上发送和接收的规则属于逻辑层或者叫协议层。它规定了起始位、数据位、校验位、停止位这几样东西怎么排。一个典型的 UART 帧长这样先拉低一个起始位表示注意我要开始发了然后依次发 8 个数据位先发最低位接着可选一个校验位最后拉高停止位表示这一帧结束。收发双方必须约定好同样的波特率和帧格式否则收到的就是一堆错位的比特。UART 是异步的意思是收发双方没有共享时钟线全靠各自的波特率去对时间。这带来一个实际影响双方波特率哪怕差一点点累积到后面就会采样错位。一般要求时钟误差控制在 2% 以内比较稳晶振精度差的板子建议把波特率降下来115200 不稳就退到 57600 甚至 9600。我在项目里遇到过一块便宜的温度采集板115200 下每几百帧就错一帧降到 38400 后就稳定了这就是异步通信对时钟精度的现实要求。还有一点UART 本身是 TTL 电平的它的1和0对应的是芯片引脚上的高电平和低电平通常是 0V 和 3.3V或 5V。TTL 电平不能直接拉长距离也不抗干扰所以它更多是芯片之间近距离通信用的。要引出机箱、接远距离设备就得靠 RS232 或 RS485 把电平翻译出去。2.2 RS232把 TTL 电平抬高做点对点传输RS232 是一套电气标准它管的是线上用什么电压表示逻辑 1 和 0。它用的是负逻辑逻辑 1 对应 -3V 到 -15V逻辑 0 对应 3V 到 15V。你没看错是负逻辑而且是高压摆幅摆幅越大越抗干扰这也是 RS232 比裸 TTL 强的地方。但代价是电平转换芯片和接口电路成本上去了而且它是单端信号一根线参考地共模干扰抑制能力有限所以距离一般也就 15 米左右。RS232 是全双工的标准的 DB9 接口里 TXD、RXD、GND 三根线就能通信再加 RTS、CTS、DTR、DSR 这些做硬件流控。车载里 RS232 常见于一些工业传感器、调试口、老式仪表。TTL 转 RS232 常用的芯片有 MAX232、SP3232 这些它们内部有电荷泵能把 3.3V 或 5V 电压通过电容泵到 ±10V 左右来驱动 RS232 总线。这里有个经典乱码坑TTL 和 RS232 直接对接。有些人图省事把 MCU 的 TX 直接接到 RS232 设备的 RX没加转换芯片结果要么收不到要么收到乱码。因为逻辑电平反了不说电压摆幅也不够RS232 接收器根本识别不出有效的逻辑电平。所以看到 RS232 乱码第一反应就是查有没有电平转换、TX/RX 有没有交叉接、共地有没有做好。2.3 RS485差分总线上的半双工组网RS485 同样是一套电气标准但它和 RS232 最大的区别是用了差分信号。它用两根线 A 和 B 上的电压差来表示逻辑A 比 B 高多少伏是一种逻辑反过来是另一种。差分的好处是共模干扰会同时作用在两根线上接收端做减法就把干扰抵消掉了所以 RS485 抗干扰能力比 RS232 强很多加上差分驱动能力强传输距离可以做到 1200 米低速时节点数也能挂到 32 个甚至更多取决于收发器驱动能力。RS485 通常是半双工的也就是一对差分线上同一时刻只能有一个方向的数据。所以它必须约定一个主设备主设备发指令从设备应答靠谁先发谁占线的规则来避免冲突。这就引出 RS485 开发里最关键的一个硬件细节方向控制。大多数 RS485 收发器比如经典 MAX485、SP3485有一个 DE发送使能脚和一个 RE接收使能低有效脚。发送时必须把 DE 拉高、RE 拉高关接收接收时反过来。工程上常见两种做法一种是用 MCU 的一个 GPIO 去控制方向发数据前拉高、发完拉低另一种是自动收发电路把 DE 和 RE 短接然后用三极管或者门电路从 TX 信号里自动提取方向发的时候自动切换省掉一根 GPIO。自动收发电路省事但波特率高的时候切换延迟可能导致第一个字节丢掉这个后面实操部分再展开。特性UART (TTL)RS232RS485信号方式单端0V/3.3V 或 5V单端±3V~±15V差分A/B 两线电压差逻辑正逻辑负逻辑差分极性通信方式全双工硬件上收发独立全双工一般半双工可全双工(4线)距离板上几十厘米约 15 米约 1200 米(低速)组网点对点点对点一主多从最多 32 节点以上抗干扰弱中强典型芯片无芯片原生MAX232、SP3232MAX485、SP34853. Android 侧串口配置从设备节点到参数设置3.1 先搞清楚设备节点和权限问题在 Android 里操作串口第一步不是写代码而是找到那个设备节点、并且拿到访问权限。原生 UART 的设备节点一般是 /dev/ttyS0、/dev/ttyS1、/dev/ttyS2……编号对应 SoC 的哪一路 UART这要看芯片手册和设备树配置。USB 转串口的节点一般是 /dev/ttyUSB0或者 USB CDC-ACM 设备会是 /dev/ttyACM0。你可以用 adb shell 进去 ls /dev/tty* 看看有哪些节点。找到节点之后就是权限。Android 上这些节点默认属主是 root权限可能是 660 或者更严普通应用进程根本打不开。常见的做法有这么几种一是在 init.rc 或者 ueventd.rc 里针对这个设备节点设置属主和权限比如给某个 group 读写权限然后让应用跑在这个 group 下二是把应用做成系统应用用系统权限去访问三是如果是第三方 APK那就得靠厂商在系统层做权限放开或者用 USB Host API 这种不需要直接访问设备节点的方案。我踩过一个很典型的坑设备节点权限改好了应用还是打不开。排查半天发现是 SELinux 在拦。Android 的 SELinux 默认是 enforcing 模式即使文件权限看起来对了进程的安全上下文不匹配一样会被拒绝。这时候要去看 dmesg 或者 logcat 里的 avc denied 日志它会告诉你哪个进程对哪个节点缺什么权限然后针对性地写 sepolicy 规则。这一步对没有系统源码权限的开发者来说挺麻烦所以如果项目一开始就能定下来用 USB 转串口走用户态方案会省很多事。3.2 串口参数怎么配波特率到流控一个都不能错串口配置这几个参数任何一个和对面不一致通信就会失败或者乱码。先说波特率它表示每秒传输的码元数常见的取值有 9600、19200、38400、57600、115200、230400、460800、921600。车载里 9600 和 115200 用得最多低速传感器用 9600数据量大一点的用 115200。选波特率的原则是够用就好不要盲目求高因为波特率越高对时钟精度和线材质量要求越高出错概率越大。数据位一般是 8 位这也是最通用的因为一个字节正好 8 位。有些老设备用 7 位这时候如果你按 8 位去收就会错位。停止位常见是 1 位偶尔有 2 位的停止位多一些能给对方更多处理时间但效率低。校验位有 None无校验、Even偶校验、Odd奇校验几种车载通信里很多设备干脆用 None把校验放到应用层协议里去做因为硬件校验位只能发现单比特错误能力有限不如应用层加个 CRC 靠谱。还有一个容易被忽略的是流控。流控分硬件流控RTS/CTS和软件流控XON/XOFF。硬件流控是通过额外的两根线来告诉对方我缓冲区满了先别发适合高速大数据量场景。软件流控是用特殊字符来控制的但如果你传输的数据里恰好包含这些控制字符就会误触发所以一般不用。车载里大部分通信量不大基本都把流控设成 None。这些参数在 Android 的 Java 层其实配不了得下沉到 JNI 层用 termios 来设置。android-serialport-api 这类库做的就是这件事打开设备节点的 fd通过 ioctl 或者 tcsetattr 把这些参数写进串口驱动。3.3 代码层从 android-serialport-api 说起android-serialport-api 可能是最广为人知的一个 Android 串口库虽然它很老但思路值得学。它的核心是一个 JNI 封装Java 层调 open() 传入设备路径和波特率JNI 层用 open() 打开设备得到 fd再用 tcgetattr/tcsetattr 配置 termios 结构体最后把 fd 包成 FileDescriptor 返回给 JavaJava 就能用 FileInputStream/FileOutputStream 去读写。下面是一段简化的 JNI 配置思路我用伪代码加注释说明方便理解每一步在干什么// 打开串口设备节点 int fd open(path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { return -1; // 打不开多半是权限或SELinux问题 } struct termios options; tcgetattr(fd, options); // 先取当前配置 // 设置波特率用 cfsetispeed/cfsetospeed cfsetispeed(options, B115200); cfsetospeed(options, B115200); // 8位数据位、1位停止位、无校验这是最常见组合 options.c_cflag ~CSIZE; // 先清掉数据位掩码 options.c_cflag | CS8; // 8位数据位 options.c_cflag ~PARENB; // 无校验 options.c_cflag ~CSTOPB; // 1位停止位 // 关闭硬件流控 options.c_cflag ~CRTSCTS; // 关键设置成原始模式不做任何字节处理 options.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); options.c_iflag ~(IXON | IXOFF | IXANY); // 关软件流控 options.oflag ~OPOST; // 原始输出 // 读超时控制VMIN和VTIME决定read的阻塞行为 options.c_cc[VMIN] 0; options.c_cc[VTIME] 10; // 单位是0.1秒即1秒超时 tcsetattr(fd, TCSANOW, options); // 立即生效这段代码里 VMIN 和 VTIME 值得单独说。VMIN 设为 0、VTIME 设为 10表示 read 会阻塞最多 1 秒收到数据就立即返回超时没数据也返回 0。这样设计是为了让读线程既能及时拿到数据又不会永久卡死方便在退出时能主动结束线程。如果 VMIN 设成大于 0read 就会一直等够那么多字节才返回高速数据传输下容易出现攒批延迟所以车载这种要求实时响应的场景一般用 VMIN0。Java 侧拿到 FileDescriptor 后读写其实就是普通的流操作但要注意别在主线程读写会阻塞 UI。另外写入之后可以调一下 tcdrain 确保数据真正发出去了再切方向RS485 场景这步很关键。3.4 系统集成init.rc 与 SELinux 的那点事如果你打的是整机方案手里有系统源码那权限问题可以在系统层一次性解决。通常的做法是在 ueventd.rc 或者设备相关的 init.rc 里给串口节点加上一行权限配置比如# 给 ttyS1 设置属主和权限允许某个 group 读写 /dev/ttyS1 0660 system system类似的还有 chown、chmod 的命令形式。配上之后再把你的应用声明成用 system 用户或者对应 group 运行就能拿到读写权限了。这一步之所以放在系统层做是因为普通应用没有资格改设备节点权限必须由 init 进程在开机时完成。如果节点权限改了还不行那就是 SELinux。你需要给应用进程的域domain加上对 tty 设备类型的访问规则写在一个 .te 文件里类似 allow 你的域 tty_device:chr_file { read write open ioctl } 这种然后编译进 sepolicy。这块内容对纯应用开发者来说比较偏系统但车载整机项目里几乎是绕不开的。我个人的建议是项目早期就把这个通路打通别等应用功能都做完才发现权限卡住那时候牵涉面就大了。4. USB 转串口桥接方案FT231X、FT232R 这些芯片怎么选4.1 为什么车载偏爱 USB 转串口不是所有车机都能从主控引出原生 UART 引脚尤其是消费级平板改装或者后装车机外设接口就剩 USB 口了。这时候 USB 转串口芯片就派上用场它把 USB 协议转成 UART对外提供 TX/RX 引脚你再接电平转换芯片就能连 RS232 或 RS485 设备。好处是通用性强、即插即用、不用改设备树坏处是受 USB 总线稳定性影响而且驱动依赖芯片型号。常见的 USB 转串口芯片有这么几类FTDI 家的 FT232R、FT231XSilicon Labs 的 CP2102、CP2105Prolific 的 PL2303还有国产的 CH340、CH341。车载里 FTDI 和 CH340 用得比较多前者稳定但贵后者便宜量大。芯片选型的时候要注意电平FT231X 是 3.3V 电平FT232R 有 3.3V 和 5V 版本CH340 也有不同电压版本接错电平轻则不通重则烧片子。4.2 FT231X 与 FT232R 的驱动差异FT232R 是老牌型号功能全自带 EEPROM 可以改 VID/PID、序列号甚至自定义引脚功能内部还有 3.3V LDO能对外供电。FT231X 算是它的精简版去掉了不少额外功能成本更低更适合只需要基本串口功能的场景。在 Linux/Android 内核里这两个都归 ftdi_sio 驱动管识别到对应的 VID/PID 就会创建 /dev/ttyUSBx 节点。但这里有个坑Android 内核默认可能没编 ftdi_sio 驱动。很多消费级 Android 系统为了精简只带了几个常用驱动插上 FT231X 设备后 lsusb 能看到设备但 /dev/ttyUSB0 死活不出来就是因为驱动没编进去。解决办法要么是让厂商重新编译内核加上驱动要么走用户态方案。判断驱动有没有可以 adb shell 进去看 /sys/bus/usb-serial/drivers/ 下面有没有 ftdi_sio 目录或者看 dmesg 里插拔时有没有 ftdi 相关的打印。4.3 内核驱动还是用户态库两条路怎么走有内核驱动的前提下用起来最省心插上就有设备节点读写就跟普通文件一样。但如果是第三方应用、拿不到系统源码、内核又没带驱动那就得走用户态方案靠 Android 的 USB Host API 直接和 USB 设备通信。用户态方案的代表是 usb-serial-for-android 这个库。它基于 UsbManager 拿到设备申请权限然后自己实现各芯片的 USB 协议FTDI、CP210x、CH34x 都有实现建立批量传输通道来收发数据。优点是免驱动、免 root插上就能用缺点是要处理 USB 权限弹窗、只能前台或特定条件下工作、性能比内核驱动略低。这里就会出现一个经典问题同一个 FT232R 设备用内核驱动时是 /dev/ttyUSB0用用户态库时是自己申请 USB 设备两条路的代码完全不同。项目里要提前定别做了一半才发现内核没驱动临时改用户态工作量翻倍。我自己的经验是整机项目优先内核驱动后装或者跨平台应用优先用户态库。5. 数据通信实现收发、组包、校验与稳定性5.1 读写线程模型与缓冲区设计串口通信在代码层面最忌讳的就是在主线程或者 UI 线程里读写因为串口的 read 是阻塞的一旦没数据就卡住界面直接假死。标准做法是开两个独立线程一个读线程专门死循环收数据一个写线程或者用写队列负责发送。读线程收到字节后先丢进一个接收缓冲区然后由一个解析器从缓冲区里往出抠完整报文。接收缓冲区的设计有讲究。串口是字节流你不知道一帧什么时候结束所以缓冲区要能动态增长但又不能无限增长否则遇到异常设备疯狂发数据会把内存吃爆。我的习惯是设一个上限比如 4KB超过就说明对端行为异常直接清空重来并记日志。读到数据后立刻唤醒解析逻辑而不是等满一整个缓冲区再处理这样延迟最低。写这边如果是多业务并发发送一定要加锁或者走一个发送队列避免两条指令的字节交错在一起。比如业务 A 在发一条长指令业务 B 插进来发一条短指令两条数据混在一起对端根本解析不出来。用一个单线程的发送队列谁要发就入队队列线程顺序发送能彻底避免这个问题。5.2 帧协议设计帧头、长度、校验、帧尾裸的字节流没法用因为接收端不知道从哪儿切分。所以实际项目里几乎都会在串口之上自定义一层帧协议把一条条指令封装起来。一个典型且够用的帧结构是这样的帧头2 字节固定值比如 0xAA 0x55 长度1~2 字节表示数据区长度 命令字1 字节区分指令类型 数据区N 字节 校验1~2 字节 可选的帧尾。帧头的作用是让解析器快速定位一帧的开始。选帧头值有技巧尽量选平时数据里不常出现的组合减少误判。但光靠帧头不够因为数据区里也可能偶然出现同样的字节所以还需要配合长度字段来精确切分。校验我用得最多的是 CRC16比简单的累加和更能发现错误。累加和实现简单但两字节同时翻转就可能检不出来CRC16 计算量也不大MCU 和 Android 都能轻松搞定。下面给一个解析器的核心逻辑用 Java 写思路是维护一个缓冲区不断尝试从里面抠出完整且校验通过的帧// 从接收缓冲区中解析出完整帧 private void parseBuffer() { while (buffer.size() MIN_FRAME_LEN) { // 1. 找帧头对不上就丢弃最前面一个字节继续找 if (buffer.get(0) ! (byte)0xAA || buffer.get(1) ! (byte)0x55) { buffer.removeFirst(); continue; } // 2. 读到长度字段判断缓冲区里是否够一整帧 int dataLen buffer.get(2) 0xFF; int frameLen 2 1 1 dataLen 2; // 头长度命令数据CRC16 if (buffer.size() frameLen) { break; // 数据还不够等下次再来 } // 3. 抠出一整帧做校验 byte[] frame new byte[frameLen]; for (int i 0; i frameLen; i) frame[i] buffer.get(i); if (!checkCrc16(frame)) { // 校验失败丢掉帧头重找避免死等一个坏帧 buffer.removeFirst(); continue; } // 4. 校验通过交给业务层处理 onFrameReceived(frame); // 把这一帧从缓冲区移除 for (int i 0; i frameLen; i) buffer.removeFirst(); } }这段代码里有个细节很关键校验失败时不要整帧丢掉而只丢掉一个字节重新找帧头。因为有可能帧头是误匹配的真实的帧头在后面几个字节的位置。如果一次性丢掉整帧就可能把真正的帧头也丢掉了导致后续数据再也同步不上。这个处理能显著提升异常情况下的自恢复能力我是被现场问题教育过之后才养成的习惯。5.3 粘包与断帧字节流的两大难题串口是流式传输没有消息边界所以粘包和断帧几乎是必然要面对的。粘包是两次发送的数据连在一起被一次读到断帧是一帧数据被拆成好几次读到。这两个问题本质上都是读到的字节数不等于一整帧的字节数所以处理方式也是统一的用缓冲区攒数据解析器只从完整帧层面处理不完整的就留着等下一批。粘包处理相对简单因为数据都到了只要解析器能正确按长度切分就行。真正麻烦的是断帧尤其是帧头和长度字段刚收到、数据区还没来的时候解析器不能急着报错得耐心等。我上面那段代码里判断 buffer.size() frameLen 就 break 等下次正是处理这个。断帧场景还要注意超时如果一帧的头部到了但后面数据迟迟不来比如对方发一半断电了缓冲区就会一直挂着半帧垃圾影响后面帧的解析。所以通常还要加一个帧接收超时比如 200ms 内没凑齐就清掉这半帧。这里插一句波特率低的时候断帧尤其常见。比如 9600 波特率传 1KB 数据要 1 秒多中间被系统调度打断分成几次读太正常了。所以解析器的设计必须假设任何时候都可能只收到半帧这跟网络 TCP 编程的思路是一致的。5.4 RS485 方向控制与半双工时序如果走的是 RS485那方向控制就是成败关键。用 GPIO 控制的方案时序是发数据前把方向脚拉高进入发送态写完数据后一定要等数据从移位寄存器真正发完再把方向脚拉低回到接收态。如果发完就立刻切回接收最后一个字节可能还在发送寄存器里没发出去被硬生生截断。所以写完之后要调 tcdrain 或者等一个字节时间的延时再切换。上面说的自动收发电路虽然省了 GPIO但在高波特率下有个隐患方向切换是由 TX 信号触发的第一个字节刚发出时方向可能还没切过去导致第一个字节丢失发完最后一个字节时方向又切太快尾部字节可能被削。低速9600、19200下一般没问题但 115200 以上就建议还是用 GPIO 手动控制时序自己把握更可靠。半双工还有一个绕不开的问题总线争用。RS485 总线上多个从设备主设备发问询只有被点名的从设备才能应答其他设备必须闭嘴。如果两个从设备同时发总线上信号就会打架收到的是乱码。这靠的是软件协议层的纪律代码里要做好发送后等待应答、超时重发的机制避免自己这边还没等回应就发下一帧。6. 常见问题与排查技巧实录6.1 乱码到底是谁的锅乱码是串口调试最常见的问题它的原因可能有一堆排查要有顺序。第一步查参数波特率、数据位、停止位、校验位这四项只要有一个不一致就会乱码。经验上波特率不匹配导致的乱码通常表现为整片乱码、偶尔能蹦出正确字符数据位或校验位不匹配则可能是每个字节都错。第二步查电平TTL 直接接 RS232 必然乱码因为电平不兼容。用逻辑分析仪或者示波器看一下 TX 线上的电压摆幅TTL 应该在 0~3.3VRS232 应该在 ±5V 以上如果对不上就是电平转换没做好。第三步查接线TX 要接 RXRX 要接 TX地要共。这个最基础但也最容易错尤其是自己焊的线接反了自己还看不出来。RS485 的 A/B 也要对应接反了收不到数据。第四步查时钟精度如果前三个都对还是偶尔乱码那可能是对端 MCU 晶振精度不够或者 GPS 定位授时的温补晶振配错了。这种硬件问题降波特率往往能缓解。6.2 收不到数据怎么一层层定位一点数据都收不到比乱码还让人抓狂因为没反馈。我一般从底层往上排查先用万用表或示波器量 TX 线上有没有波形没有波形说明发送端就没发或者线断了。有波形但收不到说明接收侧的问题。接收侧首先看设备节点有没有、能不能打开。adb shell 进去ls -l /dev/ttyS1 看权限cat /dev/ttyS1 试着手工读一下如果有数据会刷屏。打不开就回到权限和 SELinux 那部分去查。如果节点能打开但 read 一直返回 0 或 -1要看是不是配置错了。比如波特率设成了 115200 但对端发的是 9600那虽然能读但读到的是乱码而不是完全没数据。完全没数据的常见原因是流控没关如果硬件流控开着但 CTS 线没接好发送会被卡住正确做法是确认流控设为 None。还有可能是 RX/TX 接反这个前面说过。再往上就是应用层读线程是不是根本没起来有没有被异常吞掉日志打全一点把每次 read 返回的字节数都记下来一眼就能看出是没数据还是数据被丢了。6.3 干扰、接地与传输距离通信时好时坏、偶尔丢帧这类问题多半和硬件环境有关。RS485 虽然抗干扰强但前提是布线规范。差分线 A/B 要尽量用双绞线两根线绞在一起干扰才能共模抵消如果分开走或者和电源线捆一起抗干扰能力大打折扣。终端电阻也很关键长距离传输时总线两端各接一个 120 欧姆的终端电阻做阻抗匹配不然信号会反射表现为随机的误码。但短距离几米一般不用加加了反而增加总线负载。接地也是个大坑。RS485 用差分信号本身不需要地线做参考但如果两端设备地电位差太大比如一个在车头一个在车尾各接不同地点共模电压可能超出收发器允许范围导致通信异常甚至烧芯片。工程上的做法是加隔离收发器比如带隔离的 ADM2483或者用共模电感抑制。车载环境电磁环境复杂点火、电机启停都会产生干扰所以布线和隔离宁可多做一点。还有供电问题。有些 USB 转串口模块靠 USB 供电驱动 RS485/RS232 设备时电流不够电压被拉低通信就时断时续。这种可以给模块单独供电或者换低功耗的收发器试试。6.4 常见问题速查表现象可能原因排查方向整片乱码波特率不匹配核对两端波特率设置每字节都错数据位/校验位不一致核对 8N1 等帧格式收到固定几个乱码字符电平不兼容检查是否有 TTL/RS232 转换完全收不到数据接线反、流控、权限量波形、查节点权限、关流控偶尔丢帧干扰、无终端电阻、时钟漂移检查双绞线、加终端电阻、降波特率发送后收不到应答RS485 方向切换太快发送完调 tcdrain 再切方向数据混在一起多线程并发写改单线程发送队列半帧卡住对端发一半断开加帧接收超时清理机制节点打开被拒SELinux 拦截看 avc denied 日志加策略USB 设备无节点内核缺驱动查 dmesg 和驱动目录或改用户态方案7. 我在车载串口项目里踩过的几个真实坑先说一个让我印象最深的。有个项目用 RS485 总线接六块采集板前期一直好好的装车测试时发现只要开空调数据就丢得厉害。折腾了两天最后发现是 RS485 的走线从空调压缩机附近经过且这段线没用双绞线共模干扰直接把差分信号淹了。换成带屏蔽的双绞线、并把屏蔽层单端接地之后问题就消失了。这件事教会我一个道理串口通信的稳定性一半靠软件一半靠硬件布线代码写得再好也救不了烂线。还有一个是 RS485 方向控制的时序问题。我们最早用的是自动收发电路9600 波特率下一直很稳后来客户要求提速到 115200就出现了每帧第一个字节丢失的现象。查了一阵才定位到是自动收发电路的切换延迟高速下第一个字节发出去的时候方向还没切过来。后来改成用 GPIO 手动控制方向并且发完调 tcdrain 等数据真正发出再切回接收问题就解决了。所以我现在默认的建议是只要条件允许RS485 方向就用 GPIO 控别图省事用自动电路。第三个坑是断帧。一开始我们的解析器是收到一帧就处理写得很天真。结果低波特率下一帧被拆成好几次读解析器每次只拿到一部分对不上帧头就疯狂丢数据通信成功率很低。后来改成带缓冲区的状态机解析并且加了帧接收超时才彻底解决。这个改造让我意识到串口编程本质上和网络编程是一类问题都要处理流式数据的边界缓存区和超时机制是标配不是可选项。最后再分享一个小技巧调试串口的时候手边一定要有一个能看的工具。软件层面可以用串口助手类工具对着发数据硬件层面最好有一个 USB 转串口的抓包模块直接并联在 TX/RX 上把原始字节流抓下来存成文件慢慢分析。现场出问题时光看日志里的解析结果往往看不出真相只有看到最原始的字节流才能判断到底是硬件没收到、还是软件解析错了。这个抓包的习惯帮我定位过好几次那种看起来是软件 bug、其实是硬件接线的问题。整体上Android 车载串口开发没有特别高深的技术难的是把物理层、驱动层、系统权限、应用协议这一整条链路都理顺任何一环出问题都会表现为通信不正常。所以我的经验是每接一个新设备先别急着写业务代码用最原始的方式把链路打通——能收发单字节再往上加协议、加业务一步步来这样排查范围小出问题也好定位。
返回列表