
Android 车载串口开发笔记UART、RS232、RS485、串口配置与数据通信做车载终端开发这几年串口这块算是绕不开的老伙计了。不管是接OBD盒子、调试外设、对接工业设备还是和STM32那套板子通信底层基本都是走UART再根据现场需求转成RS232或者RS485。很多刚入门的朋友一看到这几个词就懵觉得是一堆协议其实它们俩是物理层标准真正的数据收发逻辑还是串口那一套。这篇文章就围绕Android车载场景下的串口开发把UART、RS232、RS485的区别、Android端串口配置、数据收发的完整流程以及我在实际项目中踩过的那些坑一次说清楚。内容比较长适合正在做车载中控、车机互联、工业平板或者物联网网关开发的朋友也适合刚接触串口通信、想把Android设备和外部硬件打通的人。文章不会扯太深的内核源码重心放在能落地的实操方案和长期调试积累下来的经验上。1. 先分清三个概念UART、RS232、RS485到底有什么不同很多新人最常问的问题就是这三个东西是不是同一种接口。最开始我也有这个困惑直到自己画板子、看波形、焊线才彻底搞清楚。简单说UART是一种通信方式RS232和RS485是在UART基础上定义的电气标准规定了电压范围、接线方式和传输距离。1.1 UART是串口通信的底层机制UARTUniversal Asynchronous Receiver/Transmitter是通用异步收发器它本身是一套硬件通信协议。发送端和接收端各有一根数据线加上一根公共地线就能完成全双工通信。这里说的全双工意思是发送和接收可以同时进行因为TX发送和RX接收是独立的物理线路。UART的数据格式是由起始位、数据位、校验位和停止位组成的。实际传输的时候空闲状态下TX线是高电平发送一个字节前先拉低电平代表起始位然后按低位到高位的顺序发送8个数据位最后发送停止位把电平拉高。这类异步通信最关键的点是通信双方必须提前约定波特率否则接收方采样的时间点对不上收到的就是乱码。在Android设备里UART不是直接暴露给应用的它通常被集成在SoC内部通过引脚引出到主板上的排针或者连接器。车机上常见的串口设备节点是/dev/ttyS0、/dev/ttyS1这样的如果是外接USB转串口芯片则可能是/dev/ttyUSB0。后面讲到Android层配置时会进一步解释。1.2 RS232点对点通信的老将RS232是串口通信最经典的标准之一它规定了逻辑电平的范围。RS232标准里逻辑1对应的电压范围是-3V到-15V逻辑0对应的是3V到15V这种用正负电压表达逻辑的方式抗干扰能力比单纯的0到3.3V强不少所以RS232的通信距离一般能到15米左右。RS232的接口形态常见的有DB9和DB25两种车载设备上DB9见得最多。DB9的引脚定义中2脚是RXD接收3脚是TXD发送5脚是GND。如果Android主板的串口是TTL电平接RS232设备时就要加一个电平转换芯片比如MAX3232。我见过不少人在这一步偷懒直接把TTL接到DB9上结果不是烧了芯片就是完全不通。RS232只能支持一对一的点对点通信一个发送端对应一个接收端不能搞多机挂接。所以如果需要多个设备挂在同一总线上通信就得用RS485。1.3 RS485半双工、长距离、多节点组网RS485使用差分信号传输用两根线A和B的电压差值来表达逻辑状态。这种差分方式抗共模干扰能力极强配合双绞线传输距离可以达到1200米以上。RS485是半双工通信意思是同一时刻只能有一个设备在发送其他设备处于接收状态所以必须有一套方向控制机制不能同时收发。RS485支持多节点组网一条总线上可以挂接最多32个标准负载具体数量取决于收发器型号和负载阻抗这就是“一主多从”模式的主要物理基础。在车载和工业场景里RS485总线上经常跑Modbus RTU协议一个主机轮询多个从机设备。假如你要接6路RS485接口做设备管理那实际上每个接口都是独立的串口每个串口可以单独挂一条485总线总线上再挂多个子设备。RS485的另一个关键点是终端电阻匹配。如果总线长度较长或者通信速率较高需要在总线两端各接一个120欧姆终端电阻用来吸收反射信号否则波形会出现振铃严重时直接导致通信失败。后续调试章节会专门讲这个问题。2. Android车载串口开发的整体设计思路搞清楚了电平标准和接口协议接下来就是Android系统层面怎么把这些串口用起来。车载Android和普通手机最大的不同在于车机主板通常会引出一路或多路物理串口用于和车内其他电子控制单元通信而这些串口在Android系统里被抽象为设备文件。2.1 串口在Android系统里的存在形式Android基于Linux内核所以串口设备天然以文件节点的形式存在于/dev目录下。比如全志、瑞芯微、高通等平台的车机方案通常会在内核设备树里配置uart节点对应生成的设备文件可能是/dev/ttyS0、/dev/ttyS1、/dev/ttyS2等。如果是通过USB扩展出来的串口则是/dev/ttyUSB0或/dev/ttyACM0。在Android应用层直接操作这些设备文件需要Native层支持。最经典的做法是使用Google开源的SerialPort API——也就是android-serialport-api这个项目它通过JNI封装了底层的open、read、write和close操作。打开串口本质上是调用了Linux底层的open系统调用然后配置termios结构体设置波特率、数据位、停止位、校验位和流控等参数。这里要强调一个关键点应用层有没有权限访问/dev/ttyS*和设备节点的权限位密切相关。车机系统如果已经root过那就简单直接chmod 777就能读写如果是量产系统往往需要修改SELinux策略或者让应用以system权限运行。我做过一个项目在用户debug版本里串口收发一切正常一到user版本就打开失败最后排查发现就是SELinux的neverallow规则挡住了。2.2 Java层和Native层的代码划分Android串口开发代码层面一般分为两部分Java层负责业务逻辑Native层负责实际的串口操作。Java层的工作是定义串口参数设备路径、波特率、数据位、停止位、校验位通过JNI接口打开串口获取输入输出流然后基于输入流启动读取线程处理收到的数据。或者用封装好的开源库比如licheedev的Android-SerialPort库Github上星数很高内部做了不少优化比如参数配置的校验、串口关闭时对读线程的中断处理等。Native层的工作是通过JNI接收Java层传入的参数调用Linux底层的open函数打开设备文件配置termios参数然后返回文件描述符给Java层。Java层的FileInputStream和FileOutputStream可以基于这个文件描述符创建后续的read和write操作就转化为对设备文件的读写。在实际开发中我比较推荐用开源库而不是自己写JNI。即使有C/C经验自己维护JNI层也要花不少时间处理各种异常情况。开源库的使用逻辑基本相同传入串口设备路径配置参数打开后拿到InputStream和OutputStream。可能有人担心开源库不够稳定其实这类库没有太复杂的逻辑稳定性的关键还是在你配置的串口参数与设备参数是否一致。2.3 串口参数配置的原则和选择逻辑串口通信要正常进行通信双方必须在一个频率上。波特率不一致数据就全是乱码数据位不一致帧结构就错了。最常见的串口配置是115200波特率、8数据位、1停止位、无校验简称8N1。而Modbus RTU的默认配置通常是9600波特率、8数据位、1停止位、无校验。如何确定具体参数三个方面来定查看对方设备手册。工业设备通常会在说明书里明确标注通信参数比如“默认波特率9600可调整为19200”。观察数据内容。如果接收端能收到数据但是全是乱码大概率波特率不一致。结合实际线缆长度。线缆较长时优先用9600波特率因为速率越低抗干扰能力越强传输越可靠。代码里配置串口参数时如果数据位填错比如把8写成7那通信帧结构直接错位整个数据包解析都会出问题。流控参数建议默认关掉除非对方设备明确要求硬件流控否则开启之后经常会遇到莫名其妙的收发停滞问题排查半天发现是RTS/CTS引脚没有接。3. 驱动选择和硬件层面的准备说完了Android软件侧的整体思路回到硬件层。车机主板上的串口是TTL电平外部设备可能是RS232接口也可能是RS485接口甚至可能是USB转串口芯片做的调试口。不同情况需要不同的连接方案和驱动支持。3.1 常见USB转串口芯片与驱动在Android设备上调试串口最常见的方式是使用USB转串口模块。这类模块的核心是USB转UART桥接芯片最常见的包括FTDI的FT232R、FT231X以及沁恒的CH340、CH341还有Silicon Labs的CP2102等。FTDI芯片在Linux内核里已经有原生驱动支持插上之后系统会自动识别并生成/dev/ttyUSB0设备节点。不过Android设备并不像PC那样“万能”很多车机主板虽然支持USB Host但内核可能没有编译进去对应的驱动或者SELinux限制了访问这就需要单独处理。FT231X和FT232R我都用过。FT232R是老款驱动非常成熟缺点是体积较大FT231X是新一代封装更小功耗更低驱动和FT232R通用。如果是在嵌入式Linux板卡上使用需要确认内核配置中是否包含了USB_SERIAL_FTDI_SIO选项。在Android工程里对应的就是内核defconfig里有没有CONFIG_USB_SERIAL_FTDI_SIOy。如果插上USB转串口模块后串口调试助手或应用打开/dev/ttyUSB0报错“No such file or directory”先检查内核是否识别到了设备。可以用lsusb看看USB设备列表里有没有对应芯片的VID/PID。FTDI的设备ID通常是0403:6001CH340是1a86:7523。没有识别到的话就得补驱动或者换芯片了。3.2 RS485方向控制的两种实现方式RS485是半双工通信所以发送和接收必须分时进行。方向控制目前有两种主流方案第一种是软件控制方向也就是MCU或Android主板通过一个GPIO引脚来控制收发器的DE/RE引脚。发送数据前先拉高DE驱动使能发送完成后拉低让收发器回到接收状态。这种方式控制灵活但需要CPU参与时序控制如果切换不及时总线上可能会丢掉第一个字节的数据。第二种是自动收发电路也叫硬件自动换向电路。这种电路通过检测TX信号的变化自动切换收发模式不需要软件干预。原理是在TX引脚和收发器DI引脚之间加一个反相器和一个延时电路再加上比较器或者三极管实现方向切换。自动收发电路最大的优点是应用层和驱动层完全不用关心方向问题直接把485当一个普通串口用。缺点是受限于切换速度波特率很高时比如115200以上可能会出问题。实际项目中如果波特率不超过38400自动收发电路是很省心的方案。我在一个车载项目中就是用带自动收发电路的RS485模块对接多个传感器应用层代码和普通串口一模一样完全不用管半双工切换大大降低了开发复杂度。3.3 旁路保护与浪涌防护的工程经验车载环境不是干净的实验室环境电源波动、电机启动、线束感应都会在通信线上引入干扰。做车载串口开发硬件防护绝对不能省。RS232接口通常需要做静电防护和浪涌防护常用方案是在接口端加上TVS管阵列。RS485由于差分传输对共模干扰有一定抑制能力但长线缆布线时还是建议用带隔离的收发器比如ADI的ADM2483这种带磁隔离的芯片或者在接口处加TVS管。说到电源防雷和接地我看过很多车载设备的规格书动辄就是“标配网络防雷接口≥6路、接地通路接口≥2路、RS485接口≥6路”。这些要求看着多本质上都是围绕可靠性来做。RS485接口数量多意味着每路485都要有独立的收发器和保护电路。接地通路多是为了给不同模块提供独立的接地回路避免地电位差造成通信异常。4. Android侧串口配置与数据通信实现核心内容来了。这节直接进入Android代码层面的实操从引入依赖、初始化串口、配置参数、收发数据到数据分包和粘包处理。代码用的是Kotlin但原理和Java完全一致读者可以自行转换。4.1 引入串口依赖库并初始化我用的是licheedev的Android-SerialPort库在build.gradle里添加依赖dependencies { implementation com.github.licheedev:Android-SerialPort:1.0.1 }然后创建一个串口管理类负责打开和关闭串口并提供输入输出流给业务层使用class SerialPortManager { private var mSerialPort: SerialPort? null private var mInputStream: InputStream? null private var mOutputStream: OutputStream? null fun open(path: String, baudRate: Int, dataBits: Int, stopBits: Int, parity: Int) { mSerialPort SerialPort( File(path), baudRate, dataBits, stopBits, parity ) mInputStream mSerialPort!!.inputStream mOutputStream mSerialPort!!.outputStream } fun send(data: ByteArray) { mOutputStream?.write(data) mOutputStream?.flush() } fun close() { mSerialPort?.close() mSerialPort null mInputStream null mOutputStream null } }这里有个细节SerialPort构造函数的最后一个参数是校验位我用的是SerialPort.PARITY_NONE。如果对方设备使用偶校验或奇校验必须对应修改否则数据帧里多了一个校验位和对方设备对不上接收到的数据就会错位。4.2 打开串口时的权限与设备节点处理在Android应用层打开串口有两个常见问题设备不存在和权限不足。设备不存在先确认设备节点路径。瑞芯微平台的车机串口通常配置在/dev/ttyS0~S7全志平台可能是/dev/ttyS0~S3如果是USB转串口则是/dev/ttyUSB0。如何确定哪个节点对应哪个物理串口最笨也最有效的方法是依次打开每个节点用杜邦线把该串口的TX和RX短接然后发送一组数据看能否收到自己的回环数据。能收到说明这个节点就是当前物理串口。权限不足的提示通常是“Permission denied”。如果是开发板最简单粗暴的是在adb shell里执行adb shell chmod 777 /dev/ttyS0但这样重启之后权限就重置了。推荐的做法是在系统init.rc里对设备节点设置权限加上类似这样的配置chmod 666 /dev/ttyS0 chown system system /dev/ttyS0同时补充SELinux策略让应用域可以访问串口设备节点。关于SELinux我提一个重点如果应用不是system权限即使chmod 777也可能无法打开设备因为SELinux会拦截。排障时可以先执行setenforce 0临时关闭SELinux如果关闭后能正常通信那问题就出在策略上。4.3 数据接收线程与分包粘包处理串口数据接收和TCP类似也存在分包和粘包问题。还不太理解的话可以这样想对方设备可能一次性发来10个字节但底层read只读到3个字节也可能连续发送了两帧数据接收端一次性全部读出导致一帧数据里混了两条指令。处理思路启动一个独立的接收线程持续读取InputStream。根据协议帧格式按帧头、长度字段、帧尾来切分数据。使用一个缓冲区把每次读取到的数据追加进去然后循环判断缓冲区中是否已经包含一个完整的帧。举个例子一个Modbus RTU数据帧的格式是设备地址(1字节) 功能码(1字节) 数据(N字节) CRC校验(2字节)。那么读取线程可以这样处理private fun startReadThread() { thread { val buffer ByteArray(1024) val tempBuffer ByteArrayOutputStream() while (isReading) { val len mInputStream?.read(buffer) ?: -1 if (len 0) { tempBuffer.write(buffer, 0, len) handleReceivedData(tempBuffer) } } } } private fun handleReceivedData(tempBuffer: ByteArrayOutputStream) { val data tempBuffer.toByteArray() // 判断是否包含一个完整帧至少8字节且CRC校验通过 if (data.size 8) { // 取出一个完整帧剩余数据保留在缓冲区中继续累积 val frame data.copyOfRange(0, 8) processFrame(frame) tempBuffer.reset() if (data.size 8) { tempBuffer.write(data, 8, data.size - 8) } } }上面的代码是简化的处理逻辑实际项目中建议用专门的处理队列。重点在于缓冲区数据切分时必须以完整的帧为单位千万不能一收到数据就直接解析否则分包情况会很常见。4.4 发送数据的编码与写入串口发送数据直接写入字节数组即可。如果是字符串注意编码要和对方设备保持一致。比如设备要求ASCII格式那就用“Hello”.toByteArray(Charsets.US_ASCII)如果设备要求的是GBK编码就得用对应的字节。常见的坑是用默认的UTF-8编码发中文对方设备显示乱码本质原因就是编码不一致。如果是Modbus指令发送时需要对指令做字节拼装。举个读寄存器的例子读从机地址1、起始寄存器地址0、数量10个寄存器的Modbus RTU指令是01 03 00 00 00 0A C5 CD其中前6个字节是帧内容最后两个字节是CRC16校验值。这个校验值的计算有很多现成算法后面单独写一节。5. 实战Modbus RTU通信从STM32到Android的对接过程做车载串口开发绕不开Modbus协议尤其是Modbus RTU。我接触过的不少设备都是基于STM32开发的很多做嵌入式的人用标准外设库加FreeModbus协议栈实现Modbus RTU从站。Android这边作为主站需要按Modbus RTU格式自发自收完成寄存器读写操作。5.1 Modbus RTU帧结构与CRC校验计算Modbus RTU帧结构很简单字段长度说明设备地址1字节0x01~0xF7从机地址功能码1字节03读寄存器06写单个寄存器10写多个寄存器数据段N字节寄存器地址、数据值等CRC校验2字节CRC16低字节在前CRC16计算网上有很多标准算法。MODBUS的CRC校验算法是多项式0x8005初值0xFFFF结果需要高低字节交换。贴一段Kotlin实现fun calculateCrc16(data: ByteArray): ByteArray { var crc 0xFFFF for (byte in data) { crc crc xor (byte.toInt() and 0xFF) for (i in 0 until 8) { if ((crc and 1) ! 0) { crc (crc shr 1) xor 0xA001 } else { crc crc shr 1 } } } val crcLow (crc and 0xFF).toByte() val crcHigh ((crc shr 8) and 0xFF).toByte() return byteArrayOf(crcLow, crcHigh) }CRC计算必须收发一致。发送时把计算出的两个字节附加在数据帧后面接收时先对整帧做CRC校验校验通过才认为数据有效。这个逻辑看着简单但实际排查通信问题时CRC错误占了很大比例后面会细说。5.2 基于STM32 FreeModbus的从机端配置思路虽然这篇主要讲Android串口开发但对接从机时了解从机端怎么工作的对你排查问题非常有帮助。STM32跑FreeModbus的典型步骤是在FreeModbus的port层完成串口初始化和定时器初始化把串口中断里收到的数据交给MODBUS协议栈处理协议栈解析完指令后在事件回调中读写寄存器。在FreeModbus移植中需要特别注意波特率必须和主机端一致。STM32的串口波特率一般由系统时钟和USART_BRR寄存器计算而来如果系统时钟配置和库里的默认配置不一致实际波特率会偏差。这种偏差在115200波特率下可能导致超过2%的误差通信就不稳定。如果你在Android端发送Modbus读寄存器指令后完全没回应可以先从简单的地方查起从机的设备地址是否正确功能码是否被从机支持数据长度是否在从机寄存器映射范围内。我遇到过从机只实现了03功能码而Android端发的是04功能码从机自然没反应。5.3 一主多从的轮询策略和超时处理RS485总线上的Modbus RTU通常是一主多从模式。Android设备作为主机需要按从机地址依次轮询。每个从机的通信参数波特率、数据位等必须一致但设备地址不能冲突。轮询策略这一块常见的做法是按地址顺序逐一请求。关键点在于超时时间设置。RS485是半双工主机发送完指令后必须切换到接收模式等待从机回复。从机处理指令需要一定时间不同的MCU处理速度不同超时时间一般设置200ms到500ms比较稳妥。超时太短从机来不及响应误判为通信故障超时太长轮询周期变大实时性下降。如果总线上挂了多个从机某个从机掉线了不用整个轮询周期都卡在它身上。正确做法是失败后连续重试两次重试仍然失败就把该从机标记为离线跳过它继续轮询正常从机。这样单点故障不会拖垮整条总线。5.4 RS485总线终端电阻与接地问题实践中最隐蔽的问题往往不是协议层面的而是物理层的。RS485总线通信不稳定、时好时坏六个排查方向里有五个跟接线有关。第一个是终端电阻。总线两端各需要120欧姆终端电阻用来匹配传输线阻抗。如果总线上只有两个设备且距离很近比如同一块板子上对接可以不接终端电阻一旦线长超过几十米或者速率较高就必须接。有些RS485模块上有一个跳帽或拨码开关可以方便地切换是否接入120欧姆电阻。第二个是接地。RS485虽然使用差分信号但两端设备的逻辑地GND仍然需要连接。很多人在短距离调试时省略了GND线如果两端设备分别由不同电源供电地电位差会导致共模电压超出收发器允许范围通信就不稳定。实测下来加了GND线之后原本偶发乱码的问题直接消失。第三个是线材选择。RS485建议用双绞线或者屏蔽双绞线屏蔽层单端接地。平行的普通导线在长距离传输时抗干扰能力差很多特别是走线靠近电机或者大功率线缆时误码率会明显上升。6. 新手常见问题与调试技巧实录串口开发有一个特点软件上看似没问题但硬件上一个焊接点虚接就能让你排查一整天。这里整理一些我实际遇到的典型问题以及经过排查总结出来的调试方法。6.1 串口数据乱码的几种原因和对应处理乱码是串口调试里出现频率最高的问题。乱码的实质就是收发双方的数据帧格式不对齐。第一可能是波特率不一致。比如设备端实际是9600Android端配置成了115200那收到的数据看起来就是毫无规律的特殊字符。处理方法很简单把Android端的波特率改成和对方一致。如果不知道对方波特率可以用常见波特率逐个试一遍观察是否出现完整、连续、可解析的数据帧。第二可能是数据位/停止位/校验位不匹配。常见配置是8N1但有些老设备用7E17位数据位、偶校验、1位停止位如果Android端还是8N1那么每个字节都会相差一位解析出来的数据就会偏移。第三可能是线序接反。TX/RX反接双方都收不到数据而不是收到乱码。判断方法是用串口调试工具发送一组数据同时用示波器或逻辑分析仪观察波形。如果完全没波形大概率线没接对或者设备没上电。第四可能是接地不良。我在车载环境里就遇到过拔掉充电器之后通信正常插上充电器就乱码后来发现是充电器的开关电源带来的地线干扰。这种问题靠软件解决不了只能从电源隔离、磁环、屏蔽层接地这几个方向入手。6.2 打开串口设备失败的排查流程应用层报错打开串口失败最让人挠头的是在用户手里复现不了只在特定设备上出现。根据经验排查顺序应该是设备节点是否存在、应用是否有权限、SELinux是否拦截、设备是否被其他进程占用。设备节点是否存在adb shell进到设备里执行ls /dev/ttyS*看对应的ttyS节点在不在。节点不存在是内核问题节点存在再看下一步。应用是否有权限执行ls -l /dev/ttyS0查看权限位。如果显示crw-------且属主是root普通应用打开肯定失败。此时可以用adb shell chmod 666 /dev/ttyS0临时改权限然后重新打开。SELinux拦截这个最容易忽略。查看logcat里有没有avc: denied相关日志有的话基本就是SELinux。先setenforce 0关闭SELinux再试一次能打开就是策略问题。设备被占用车机上可能存在多个服务在抢占同一个串口。比如GPS的NMEA数据走的也是/dev/ttyS3你的应用也去打开这个节点有可能冲突。打开串口前最好先确认这个串口是否被系统其他服务使用。6.3 用回环测试验证Android串口是否工作正常不管你面对的是自己开发的车机主板还是设备厂商提供的Android盒子拿到设备后第一件事是验证串口硬件工作是否正常。回环测试是最基本的验证方法。先用杜邦线将串口的TX和RX短接这样发送的数据会直接返回给自身。然后在App或者串口调试工具里发送一组数据例如“Android Serial Test”如果收到的数据和你发送的内容完全一致说明这个串口的发送和接收通路都是通的。这个方法对RS232同样适用就是把DB9的2脚和3脚短接。不过动手前最好确认电压不要随便短接防止设备类型不同导致问题。对于RS485回环测试略有不同。RS485是差分信号如果模块支持全双工测试有些模块有单独的A/B测试口可以直接短接A和B。如果模块不支持需要用一个RS485转USB工具去对接验证。6.4 常用调试工具推荐和使用心得底层驱动、硬件电路都排查完之后剩下的就是纯应用层逻辑调试。聊几个我用着顺手的工具。adb shell不用多说查看设备节点、修改权限、抓日志都靠它。串口调试助手Android端可以使用类似“串口调试助手”的App直接输入设备路径和波特率就能打开串口用于快速验证硬件通路。PC端可以用友善串口助手或者MobaXterm里的Serial session。逻辑分析仪强烈建议搞一个便宜的8通道逻辑分析仪配合PulseView软件可以直接抓取UART的TX/RX波形非常直观。有时候软件上死活找不到问题用逻辑分析仪看一眼波形马上就明白是空闲电平不对还是波特率漂移了。CRC校验工具Modbus RTU通信时可以先用PC上的Modbus调试工具比如Modbus Poll验证一下从机是否正常响应然后比对Android端发的帧是否和Modbus Poll一致。如果帧一致而从机不响应说明问题在物理层或者从机端如果帧不一致说明Android端组帧代码有误。7. 踩坑案例两个实际项目的复盘聊了这么多理论和方法分享两个我实际经历过的案例都是被折磨了好几天的。7.1 第一个坑user版本SELinux拦截串口访问当时做一款商用车中控开发板阶段一切正常串口收发都很流畅。到了给客户出user版本固件时应用突然打不开串口了。第一反应是权限问题于是我们给应用加了系统权限仍然不行。后来抓logcat发现这样的日志avc: denied { read write } for pid1234 commxxx namettyS1 devtmpfs scontextu:r:untrusted_app:s0 tcontextu:r:device:s0 tclass:chr_file permissive0这就是典型的SELinux拦截。开发板带root权限selinux处于permissive模式所以不会拦截user版本是enforcing模式直接挡住。最后通过修改SELinux策略在untrusted_app的te文件里增加允许访问串口设备节点的规则问题才真正解决。这次教训后凡是量产项目我都会在发布前把所有应用的串口访问逻辑在enforcing模式下完整回归一遍。7.2 第二个坑RS485总线多从机偶发掉线另一个项目是用Android作为Modbus主机通过RS485总线轮询12个温湿度传感器。设备刚装好的头几天很稳定后面陆续出现个别从机偶发无响应的情况。一开始怀疑是从机设备的问题返厂检测又说没问题。后来我用逻辑分析仪同时抓了主机的TX波形和总线上的A/B差分波形发现在主机切换到接收模式后总线上偶尔会出现一个很短的低电平干扰脉冲。追查到最后发现是某几个从机设备的电源设计没有做隔离在上电瞬间会把总线拉低导致主机误判为收到数据帧轮询逻辑进入异常分支。底层的处理办法是对从机的RS485收发器增加隔离供电或者换成带隔离的RS485模块软件上的临时方案是主机收到数据后先做帧校验CRC不过就丢弃并重新发起轮询。这个案例给我的启发是在恶劣电磁环境下单纯靠软件协议解决不了所有问题物理层设计才是通信可靠性的基础。8. 如何把串口开发能力用到更广的场景串口开发本身不只是Android上写几个接口那么简单。把基础打牢了很多衍生技术都能很快上手比如工业平板对接PLC、车载终端对接外设、IoT网关采集传感器数据等。8.1 从车载延伸到工业平板和IoT网关车载和工业现场的通信方式有很多相通之处。Android主板的串口在你掌握了基础后拓展起来其实非常顺畅。对接PLC很多PLC支持Modbus RTU或者自定义协议Android侧直接用类似方法收发即可。对接扫码枪和打印机串口扫码枪一般配置好波特率后即插即用数据通过串口直接以ASCII码形式上报串口打印机的指令集则相对复杂需要按照厂商手册拼接控制指令。对接传感器常见的有温湿度、PM2.5、气体浓度等传感器大多走Modbus RTU或者自定义协议数据量小对Android侧的处理压力很小。IoT网关常见架构是Android系统作为边缘网关一侧通过RS485采集设备数据另一侧通过Wi-Fi或4G把数据上报到云端。串口开发只是整个网关的一部分但核心的数据采集链路完全依赖这一层。8.2 串口通信和网络通信的关系做熟练之后会发现在数据处理的层面上串口通信和网络通信是很像的。串口有分包和粘包问题TCP也有串口需要校验位网络通信需要校验和或MAC校验串口有波特率要对齐网络通信有MTU要对齐。很多网络编程的思维模式可以直接迁移到串口开发上反之亦然都是典型的字节流处理。所以在空闲时间里把串口数据缓冲区、帧解析、超时重传这些模块设计得通用一些后续做TCP、UDP通信项目时可以复用大部分代码框架。我的经验是把接收线程、环形缓冲区、帧解析器、超时管理器这些组件独立成类不管底层是串口还是Socket上层业务代码都不需要改太多。8.3 串口调试要养成的三个习惯最后分享三个我已经坚持了好几年的调试习惯第一任何一次通信调试前先用回环测试确认物理通路的健康状态。哪怕前一天刚做过第二天也要重新确认因为可能被人动过线和接口。第二抓日志时尽量保留十六进制的原始数据。只保留字符串日志很多不可见字符和帧格式问题会被隐藏掉。所以串口调试工具一定要带hex显示模式保存日志也是hex加ASCII双份。第三改动参数时一次只改一个。比如出乱码了不要同时把波特率从9600改成115200又把校验位从无改成偶校验否则出了问题根本定位不了是哪个参数引起的。一次只动一个变量确认效果后再改下一个效率最高。记得有一次我的Android应用怎么都对不上一个工业设备的协议折腾了一个下午最后发现是设备说明书上的寄存器地址是十进制的而我用成了十六进制。这个低级错误也提醒我拿到设备手册后先把协议字段的进制统一换算清楚再开始写组帧和解析代码能省下大量无用功。Android车载串口开发走到这一步核心的知识点其实就那么几个物理接口标准的区别、Android系统层的权限和配置、应用层的收发处理和协议解析、以及从硬件到软件的整体排障思路。如果这篇文章能帮你少走几步弯路那我这些年的折腾就没有白费。