十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Android车载串口开发实战:UART/RS232/RS485避坑指南

Android车载串口开发实战:UART/RS232/RS485避坑指南 先说句实在话Android 车载开发这活儿硬件层和系统层的问题往往比应用逻辑更折腾人。尤其做车机与外围设备通信串口这块绕不开。UART、RS232、RS485 这几个词看着相近实际用起来门道不少配置错了要么收不到数据要么乱码要么直接烧接口。这篇笔记就围绕 Android 车载场景下的串口开发展开从硬件电平、系统节点、配置参数到数据收发和 RS485 组网把能踩的坑和能抄的作业都整理出来给正在做车机、做工业平板、做物联网网关调试的朋友做个参考。1. 串口家族先捋清楚UART、RS232、RS485到底差在哪很多人一开始就被这三个词搞晕。其实可以这么理解UART 是“底层传输机制”RS232 和 RS485 是“基于 UART 的物理层标准”。打个比方UART 是普通话的发音规则RS232 和 RS485 是不同场合下说话的音量和接线方式——内容一样但传输距离、抗干扰能力、连接方式完全不同。1.1 电平标准不一样接错就烧UART 电平通常就是芯片本身的 IO 电平常见的是 3.3V TTL 电平逻辑 1 对应 3.3V逻辑 0 对应 0V。RS232 是负逻辑电平逻辑 1 对应 -3V 到 -15V逻辑 0 对应 3V 到 15V所以才需要 MAX3232 这类芯片做电平转换。RS485 用的则是差分信号靠 A、B 两线之间的电压差来表示 0 和 1逻辑 1 时 A 比 B 高 2V 到 6V逻辑 0 时反过来。这个差异在实际开发中意味着什么车机上很多调试串口直接引出的是 TTL 电平如果你拿一个 RS232 接口的设备直接怼上去轻则通信失败重则烧坏主控的串口引脚。接手项目第一件事就是拿万用表量一下车机串口引脚的静态电平判断它是 TTL 还是 RS232 标准。3.3V 左右的静态高电平基本是 TTL负电压或者 0V 静止状态多半是 RS232。1.2 接线方式和传输距离的取舍RS232 是点对点通信标准就是一台设备对一台设备全双工TXD 和 RXD 交叉连接。RS485 是半双工总线支持一主多从A 线接 A 线、B 线接 B 线所有设备并联在同一条总线上距离能到 1200 米左右。车载场景里这两种都有用武之地。RS232 常用于短距离连接调试电脑、OBD 诊断头、部分老式外设RS485 则大量用于车载显示屏与多个传感器、门控模块、空调面板的通信尤其跑 Modbus RTU 协议的时候几乎成了标配。提示RS485 总线上一定要接终端电阻。两个末端设备各接一个 120 欧姆电阻否则数据反射会导致通信不稳定尤其是波特率高于 9600 的时候一帧数据尾部经常出现乱码。1.3 车辆环境下的选型建议车上干扰源多点火线圈、电机、CAN 总线、大功率音响。串口这种异步通信方式本身抗干扰能力就有限所以选型优先级可以参考走线超过 1 米优先上 RS485实在必须用 RS232 就选带隔离的方案TTL 串口只适合板级调试或者 20 厘米以内的短连线。之前有次调试座椅控制器TTL 线拉了 40 厘米一踩油门就丢字节后来全部换成 RS485 接法问题立刻消失。2. Android 侧串口开发核心从节点到权限再到数据读写Android 系统底层是 Linux 内核串口设备在 /dev 目录下以 tty 开头比如 /dev/ttyS0、/dev/ttyMTK、/dev/ttyHSL0具体名称取决于 SoC 平台和内核配置。应用层要直接操作串口本质上就是打开设备文件、配置 termios、读写字节流。2.1 先找到你的串口节点很多新手上来就问“怎么打开串口”其实第一步是确认节点路径。车机上常见的串口节点有这么几类平台可能节点说明高通/dev/ttyHSL0、/dev/ttyMSM0高通平台常用HSL 是高速串口MTK/dev/ttyMT0、/dev/ttyMT1MTK 平台数字对应物理串口编号RK/dev/ttyS0、/dev/ttyS1Rockchip 平台标准 ttyS 命名全志/dev/ttyS0、/dev/ttyS1方案商定制时也可能改名为 ttyAS0怎么确认两个办法一是看内核 dmesg 日志搜索 tty 相关输出二是用串口调试工具挨个试同时触发外设发送数据看哪个节点能读到。实测中第二种办法最快写一个小工具遍历所有 /dev/ttyS* 和 /dev/ttyMT*open 成功后尝试 read谁先读到数据基本就是它。2.2 权限问题为什么 open 总是 Permission denied串口节点默认属于 root 和 dialout 组普通 App 没权限打开。车载系统一般有两种对策一种是改系统权限在 init.rc 里对目标节点执行 chmod 666比如chmod 666 /dev/ttyS0这种方法简单粗暴适合系统内置 App。另一种是做成系统签名 App通过申请权限的方式获取访问能力。调试阶段还有个偷懒的办法通过 adb shell 直接给节点提权然后让 App 以 shell 身份运行。但在正式工程里不要这么干因为每次重启权限会重置。我习惯写一个小脚本放在系统启动脚本里统一设置节点权限这样避免同事调试时反复折腾。2.3 串口配置的关键参数串口配置主要涉及波特率、数据位、停止位、校验位、流控。接头字段定义如下struct termios { tcflag_t c_iflag; // 输入模式 tcflag_t c_oflag; // 输出模式 tcflag_t c_cflag; // 控制模式 tcflag_t c_lflag; // 本地模式 cc_t c_cc[NCCS]; };这些参数在 NDK 层用 tcsetattr 函数设置。核心配置逻辑像这样int set_serial_attr(int fd, int baudrate, int data_bits, int stop_bits, char parity) { struct termios options; tcgetattr(fd, options); // 设置为原始模式避免系统对输入输出做额外处理 cfmakeraw(options); options.c_cflag | CLOCAL | CREAD; // 根据波特率查表设置 cfsetispeed(options, get_baud_const(baudrate)); cfsetospeed(options, get_baud_const(baudrate)); // 数据位 options.c_cflag ~CSIZE; switch (data_bits) { case 5: options.c_cflag | CS5; break; case 6: options.c_cflag | CS6; break; case 7: options.c_cflag | CS7; break; case 8: options.c_cflag | CS8; break; } // 校验位 switch (parity) { case N: options.c_cflag ~PARENB; break; case E: options.c_cflag | PARENB; options.c_cflag ~PARODD; break; case O: options.c_cflag | PARENB; options.c_cflag | PARODD; break; } // 停止位 model, 1, 1 if (stop_bits 2) options.c_cflag | CSTOPB; else options.c_cflag ~CSTOPB; tcsetattr(fd, TCSANOW, options); // 清空缓冲区 tcflush(fd, TCIOFLUSH); return 0; }这里特别要注意设备和外设的参数必须完全一致。车载外设默认配置五花八门有的 9600 8N1有的 115200 8E1务必先确认外设的出厂参数。之前同事调一个扫码枪默认波特率 9600代码里写死 115200调了整整三天最后发现是手册看错了。还有一个容易忽略的地方如果外设发上来的数据中带有 0x0A 和 0x0D而你没有设置原始模式系统默认输出模式会把换行符转换掉导致收到的数据莫名其妙多出或丢失字节。这就是每次配置串口都要调用 cfmakeraw 的原因。它把输入输出标志位全部清掉告诉内核“别动我的数据”。2.4 Java 层调用方式与常用工具库Android 应用层直接操作串口的方案主流是 native 层通过 JNI 调用上面那套 termios 操作Java 层负责管理打开、读取、关闭生命周期。社区里最常用的库是 android-serialport-api虽然年久失修但核心逻辑仍然能用。另一条路是直接用 google 官方在 Android Things 时期推出的 PeripheralIO不过 Android Things 已经停服新项目基本不考虑它。如果不想自己封装 JNI可以用现成库比如 cheda 或 android-serialport-lib。但车载项目我建议还是自己维护一套串口封装因为车机上的串口节点常常不止一个还要配合 GPIO 控制 RS485 方向、做数据报文的分帧与粘包处理第三方库一般只做了最基础的打开和读写这些业务逻辑还是得自己写。一个实用的 Java 层打开串口思路public class SerialPort { private FileDescriptor fd; private FileInputStream inputStream; private FileOutputStream outputStream; public SerialPort(File device, int baudrate, int flags) throws IOException { fd open(device.getAbsolutePath(), baudrate, flags); if (fd null) { throw new IOException(open serial port failed); } inputStream new FileInputStream(fd); outputStream new FileOutputStream(fd); } private native static FileDescriptor open(String path, int baudrate, int flags); public native static void close(); public native void sendData(byte[] data); public native byte[] recvData(int maxSize); static { System.loadLibrary(serial_port); } }然后再开一个读线程持续 read 字节流遇到业务帧头帧尾再解析。注意串口是持续的字节流协议没有“包”的概念必须在业务层自己定义帧格式帧头、帧尾、长度、校验位缺一不可。3. RS485 开发进阶方向控制、自动收发电路与 Modbus 组网RS485 在车载和工业场景的大量应用使得它值得单独拆出来聊。RS485 是半双工意味着同一时间只能收或者只能发所以驱动侧必须控制收发方向的切换。方向控制要是做得不好数据冲突导致总线冲突严重的时候整个总线的设备都会被带崩。3.1 方向控制的两种主流玩法最常见的硬件方案是“自动收发电路”利用三极管或 MOS 管检测发送端电平自动切换 DE/RE 引脚原理图大致是TXD 通过电阻连接到三极管基极发送低电平时三极管导通DE 拉高进入发送模式发送高电平时进入接收模式。这种电路的优势是软件无感直接像普通串口那样写数据就行缺点在于切换时机不好精确控制尤其是在连续发送、相邻字节间隙很短的时候可能造成最后一个字节发送不完整。另一种方案是 GPIO 控制方向。RS485 收发芯片比如 MAX3485、SP3485DE 和 RE 引脚相连用一个 GPIO 控制高电平发送、低电平接收。软件关键是在写数据之前把 GPIO 拉高数据写完再拉低。难点在于“写完”的时机判断。由于串口驱动有 FIFOwrite 函数返回并不代表数据已经全部从 TX 引脚发出去了过早拉低会把最后一个字节截断。不少工程师会在这里踩坑最简单的解决办法是在 write 之后延时 1 到 2 个字节的发送时间。以 9600 波特率为例一个字节大约 1.04ms延时 2ms 比较稳妥。速率更高的 115200一个字节约 87 微秒延时 200 微秒左右够用。但延时法终归不够优雅更可靠的做法是使用 serialcore 的 TIOCOUTQ 或等待 TX complete 信号但 Android 设备上不一定暴露这些接口所以我在实际开发中用的还是延时 实测调参的组合。注意如果 RS485 总线上同时挂着多个从机切换方向时建议在发送完成后留出至少 4 个字节的静默时间保证总线电平完全回落到空闲状态后再切换到接收模式避免把最后一个字节的尾巴当成新数据。3.2 RS485 常见硬件电路参考这里给一个典型的 RS485 节点电路设计思路收发芯片MAX3485 或 SP34853.3V 供电速率最高支持 10Mbps 以上足够覆盖车载常用波特率。A、B 线各串一个 10 欧姆电阻起到限流作用。A 线接上拉电阻 5.1K 到 VCCB 线下拉 5.1K 到 GND保证总线空闲时处于确定的状态。终端电阻是否焊接看节点是不是总线末端。焊接终端电阻要注意120 欧姆电阻不是随便加的。如果总线上每个设备都焊了 120 欧姆并联之后等效电阻会很低驱动芯片负载过重信号幅度衰减严重。规范做法是只在物理链路的两端各放一个 120 欧姆电阻。判断自己是不是末端最简单的方法看 RS485 线是不是在这个设备这里到头了。3.3 Modbus RTU 与 RS485 的结合应用车载设备与充电桩、逆变器、BMS 通信Modbus RTU 协议出现频率很高。Modbus RTU 的数据帧很简单地址码 功能码 数据 CRC16 校验地址码 1 字节功能码一般用 03 读寄存器、06 写单个寄存器、10 写多个寄存器CRC16 是两字节校验。实现时需要注意的细节是 CRC16是 Modbus 特有的多项式 0xA001不是通用的 CRC16-CCITT。这一点网上搜索 CRC 算法时要特别看清楚。用错了校验设备会一直返回异常码 03 或者干脆不响应。下面这段就是软件常见的方向切换伪代码流程private void sendModbusFrame(byte[] frame) { // 切换到发送模式 rs485Gpio.setValue(true); try { outputStream.write(frame); outputStream.flush(); // 等待最后一个字节发完 Thread.sleep(estimateSendTime(frame.length)); } catch (IOException | InterruptedException e) { e.printStackTrace(); } finally { // 回到接收模式 rs485Gpio.setValue(false); } }estimateSendTime 的计算公式(帧字节数 2) × 10 / 波特率 × 1000 毫秒。10 表示 1 个起始位 8 个数据位 1 个停止位。如果是 8E1校验位也算 1 位同样是 10 位如果 8N2那就是 11 位。加 2 是余量避免线程调度导致时序紧张。4. 波特率、数据位、校验位配置背后的计算与原理串口通信看起来是“两边设一样的参数就能通”但参数背后的原理以及为什么某些外设必须用某些参数往往被忽略了。4.1 波特率不是“越大越好”波特率指的是每秒传输的码元数单位是 baud换算成字节速率要除以 10或者 11取决于帧格式。9600 波特率约等于 960 字节每秒实际扣掉帧间隙大约 900 字节每秒左右115200 大约 11520 字节每秒。先算链路带宽再算业务流量这是我做车载串口开发之前一定做的事。举个例子一个外设每 100ms 上报一帧数据每帧 64 字节那么每秒需要传输 640 字节加上协议开销9600 波特率传输这种流量已经很紧张了。这时候至少提到 38400 甚至 115200避免读取不及时导致数据堆积、FIFO 溢出丢包。4.2 数据位和校验位的选择逻辑传统串口有 5、6、7、8 四种数据位现在绝大多数场景用 8。7 位数据位常见于老式 ASCII 字符通信比如某些称重仪表。校验位的作用是检测单比特错误偶校验Even在工业设备里最常见但要注意如果链路质量问题多到校验通不过提高校验位也没多大意义老老实实换 RS485 或降速率更靠谱。一个来自实际项目的教训带 Modbus 协议的小型气象站默认参数竟然是 9600 8N1但很多开发例程里写的是 9600 8E1。因为 Modbus 标准规定 RTU 模式默认是 8 数据位 无校验或偶校验但具体设备手册为准。一定要先看手册然后拿串口调试工具抓一下设备主动上报的字节流确认无措后再动代码。4.3 流控到底要不要用硬件流控RTS/CTS在某些工控设备上是必需的但 Android 车机上很多串口芯片根本没有把流控线引出来。所以想用 4 线制 RS232TXD、RXD、RTS、CTS的设备先检查硬件设计是否支持。软件上如果设置了硬件流控但实际没有接线通信表现会极其诡异发数据发不出去收数据收不到或者只能单向通。在 Android 的 termios 配置里关闭流控的方式是注释掉 CRTSCTS 标志。调试时如果遇到“写进去没反应且 TX 灯不亮”第一个查的就是流控配置。5. Android 串口调试过程中的常见问题排查串口开发的特点就是“出错难定位”因为信号是物理层的很多问题在应用层看起来一模一样。收藏下面这个排查列表能帮你省掉很多无头苍蝇式的抓瞎时间。5.1 打不开 /dev/ttyS0提示 Permission denied权限问题的原因最直接。先在 adb shell 里执行 ls -l /dev/ttyS0 看文件权限。如果是 root root 拥有且权限只开放了 620dialout 组可读写那普通 App 就没权限。解决方案adb root adb shell chmod 666 /dev/ttyS0临时试一下可以重启失效。要永久生效改 init.rcchmod 666 /dev/ttyS0 chown system system /dev/ttyS05.2 打开成功了但 read 一直超时收不到数据先排除硬件层问题用示波器或者串口转 USB 工具直接量 TX/RX 引脚确认外设确实在发数据。如果外设发送正常再确认是不是 Android 侧的 RX 引脚和外部设备的 TX 引脚接反了。RS232 需要交叉连接RS485 是 A 接 A、B 接 B很多人把这两个搞混导致看似接对了实际反了。5.3 能收到数据但是乱码乱码第一嫌疑是波特率不一致第二嫌疑是数据位/校验位/停止位不一致第三嫌疑是电气干扰。排查顺序先用示波器看一帧数据的波形宽度实测一个字节的时间是否符合当前波特率不符合就是波特率错误符合的话再看校验位和停止位配置最后才怀疑干扰。干扰导致的乱码有一个特征低速时正常高速时乱码特别是踩油门、开关空调等动作时偶发。这种情况优先检查 RS485 是否接终端电阻、屏蔽层是否单端接地、有没有和电源线共走线槽。5.4 数据总是缺末尾几个字节这个是 RS485 半双工方向切换的经典症状。检查方向和 DE/RE 控制逻辑确认发送完成后是否给予了足够的延时再切换到接收模式。或者干脆改用自动收发电路减少软件时序依赖。5.5 一帧数据被拆成多段到达或者帧之间粘在一起串口驱动把数据按字节写入流read 返回的字节数是不确定的跟内核 FIFO、调度延时都有关系。所以业务代码必须做“粘包处理”。常见做法是定义帧头帧尾用一个缓冲区累积字节遇到完整帧再向外抛出。我在车载项目里通常会维护一个环形缓冲把读取线程拿到的原始字节流按帧切分再交给业务解析线程这样即使每帧到达时间不均匀逻辑上也是完整可靠的。6. 实操经验车载串口工程的架构与开发流程建议如果项目刚起步或者你准备从零搭一套车载串口通信中间件下面这套流程是我踩了不少坑之后总结出来的可以少走弯路。6.1 先做硬件确认清单代码开工前先确认五件事车机板串口电平是 TTL 还是 RS232是否已经通过板载芯片做了转换。每个串口节点对应物理引脚位置拿万用表导通测试确认。外设端的通信协议格式包括波特率、帧格式、命令交互时序。RS485 有没有接终端电阻A/B 线是否与对端 A/B 对应。电源地是不是共地。RS232 和 TTL 通信要求共地如果两个设备各自独立供电又不共地电平参考点不同通信一定是异常的。6.2 中间件设计上预留动态配置能力车载项目的串口参数经常因为外设供应商变更而调整所以不要写死。我的做法是做一个串口配置表用 JSON 维护每个串口的参数存放在系统属性里打开时动态读取。这样换外设时只需要改配置不需要重新编译固件。比如{ serial_ports: [ { name: ttyS0, baudrate: 115200, dataBits: 8, stopBits: 1, parity: N, flowControl: NONE, rs485Gpio: 101, readBufferSize: 4096 } ] }6.3 日志要包含收发指纹串口问题很多是偶发的复现难日志就显得格外重要。我的习惯是每帧数据都记录这样一条日志[SEND] len8 [AA 55 01 02 03 04 05 B2] [RECV] len12 [AA 55 10 01 02 03 04 05 06 07 08 B2] duration12ms不要只记 HEX 数组还要记录时间戳和收发间隔。很多通信超时问题一看到 duration 异常波动就有思路了。6.4 车规级场景的额外注意事项车辆供电不稳是常态串口电路往往直接挂在 12V 或 24V 电瓶环境下所以正经的车载串口硬件都要做隔离和防护。常见的防护设计包括TVS 管防浪涌、光耦隔离、DC-DC 隔离电源。如果你的板子是工装样板没有这些设计也不要太焦虑至少保证调试时严格共地并远离大电流负载的走线。软件上针对车载环境的策略也有讲究车辆启动瞬间电压跌落可能导致主控和外设启动时序不同步。App 的串口初始化要做成“失败重试”且带退避策略的机制比如首次失败后等待 500ms、1s、2s、5s 依次重试最多重试 10 次不要无限死循环。这样既不会错过外设的晚启动又不会因为疯狂重试把 CPU 占满。7. 一套完整的 Android 串口通信实现参考结合前面的思路这里给一个简洁但完整的工程实现参考。整个框架分三层驱动层、协议层、业务层。驱动层职责打开串口、配置参数、提供读写接口、管理 RS485 方向 GPIO。协议层职责组帧、拆帧、CRC 校验、指令超时重发。业务层职责解析业务数据更新 UI 或上报给上层服务。驱动层核心代码已经在上面的示例里展示过这里补上协议层的 CRC16 计算private int calculateCRC(byte[] data, int length) { int crc 0xFFFF; for (int i 0; i length; i) { crc ^ (data[i] 0xFF); for (int j 0; j 8; j) { if ((crc 0x0001) ! 0) { crc 1; crc ^ 0xA001; } else { crc 1; } } } return crc; }注意 Modbus 的 CRC 字节序是低字节在前、高字节在后组帧时先把 crc 与 0xFF 相与放入帧尾第一个字节再把 crc 右移 8 位放入第二个字节。写反了设备同样会拒绝响应。接收端的拆帧逻辑可以用状态机来实现我的做法是读到一个字节 0xAA帧头时进入“接收体”状态。持续累积字节直到拿到帧长度字段解析出完整帧长度。帧尾验证后再做 CRC 校验通过后交给业务层。校验失败或超时超过 100ms复位状态机重新等待帧头。这样处理之后即使底层数据传输被拆成 3 次 read上层拿到的也是完整一帧。8. 多串口并发管理的一些心得车载车机往往不止一个串口。中控屏要接 4G 模块、行车记录仪、车身控制器甚至还有 RS232 的调试口。多串口并发时最容易犯的错误是每个串口开一个独立线程导致线程太多、上下文切换频繁、日志混乱。建议的做法是一个串口一个读线程但共用一个线程池每个读线程只负责 read 流并进行极轻量的帧切分业务解析统一丢到业务线程队列。缓冲区大小也很有讲究常见串口 FIFO 在 64 字节到 4KB 之间如果上层处理不及时大流量下很容易丢包。实测在 460800 波特率、极端情况下每秒 40KB 的数据量缓冲区 16KB 基本够用但不建议把缓冲区开得过大因为内存占用和 GC 压力也会影响稳定性。多串口日志我强烈建议打上标签每个串口日志的前缀统一为 [Port: ttyS0] 这样排查问题时能快速过滤不用一行一行猜到底是哪个口的数据。真到现场联调日志筛选能力决定你的调试效率。9. 写在最后一些实战中沉淀下来的习惯串口开发本身不算难真正难的是环境复杂。车机上跑串口时常一句话就能讲完的通信逻辑会因为供电波动、EMC 干扰、外设协议不透明、Android 权限机制等问题卡上几天。我个人的体会是越早把硬件层的事实摸清后面软件调起来越轻松。拿到板子先别急着写代码花半天时间用示波器把每个串口的电平特性、接法、节点名称全部确认一遍再写出来的程序基本一版就能通过。另外说一个特别实用的小技巧调试阶段一定准备一个串口转 USB 工具无论是 FT231X 还是 CP2102 都行配合 PC 端串口助手可以独立于 Android 系统监听或者模拟外设直接验证某根线上是不是真的有数据在跑。很多时候 Android 上层一脸懵但用串口助手一挂立刻就能判断问题出在硬件链路还是系统软件。最后再分享一个习惯车载串口开发不像纯互联网开发很多 Bug 是硬件相关、环境相关的所以每解决一个诡异问题我都在项目文档里记一条“现象—原因—解决方案”。时间长了这份笔记比任何代码库都有价值。这个项目最近刚又把一条“RS485 总线终端电阻接多了导致信号反射报错率升高”的案例补了进去。希望这篇博文里分享的内容也能成为你解决实际问题的线索和参考。
返回列表