车载Android串口通信实战:UART/RS232/RS485选型与Modbus RTU对接
做车载Android开发这几年我接手的项目里十个有七个都绕不开串口通信。最近一个项目是把几台商用车的仪表信息车速、转速、油量通过串口接到中控Android屏上显示另外还要跟一套RS485总线上挂着的多个传感器节点通信。刚开始以为Android端调个串口库就能搞定结果UART、RS232、RS485三种协议、电平转换、设备节点权限、数据组帧、ModbusRTU轮询这些环节一个接一个踩过去。这篇文章把我在这个项目里实际验证过的方案、选型逻辑和排查经验完整记录下来给正在做Android车载串口开发的同行一个可以直接参考的路线图。我尽量用项目里真实发生过的顺序来讲先解决“串口协议怎么选”再解决“Android主机侧硬件通路怎么搭”然后落到“串口库和参数配置”最后才是数据通信层面的帧设计、ModbusRTU对接和硬件抗干扰。这样从物理层一路做到应用层跟实际开发节奏是对上的。1. 车载Android盒子的串口家族UART、RS232、RS485怎么选很多刚接触车载开发的Android工程师看到板上有个4针排针标注TX、RX、5V、GND就以为串口通信就是把两根线连上然后读写。这个认知在开发板调试阶段够用但到了车载现场你必须先搞清楚UART、RS232、RS485到底分别是什么因为你的Android主机侧的TTL电平跟外面那些车规设备用的RS232、RS485电平完全不兼容直接接十有八九会烧口或者通信没反应。1.1 UART是地基RS232/RS485只是它的物理层外壳UARTUniversal Asynchronous Receiver/Transmitter本质上是一种异步串行通信协议只规定了数据帧格式和时序空闲时信号线为高电平起始位是一个低电平接着是5到8个数据位然后是可选的校验位最后是1到2个停止位。它没有规定电平标准所以同一个UART协议可以跑在TTL电平、RS232电平、RS485电平上区别只在于外面加了一个电平转换/驱动芯片。Android主机或者任何嵌入式主板串口引脚出来的通常是TTL电平逻辑1是3.3V少数是5V逻辑0是0V。TTL电平的优点是直接和芯片引脚对接、电路简单缺点是抗干扰能力差、传输距离短一般超过1米就不太靠谱。所以TTL串口在车载上只用于板内通信比如Android主板到4G模块、GPS模块、蓝牙模块这些。RS232则把逻辑电平变换成负逻辑逻辑1对应-3V到-15V逻辑0对应3V到15V。这个电平摆幅大、抗干扰能力比TTL强不少支持点对点全双工通信最远能到15米左右。车载设备里很多老一代的仪表、计价器、诊断口用的都是RS232。RS485用的是差分信号靠A、B两根线之间的电压差超过200mV为逻辑1低于-200mV为逻辑0来传数据抗共模干扰能力强最远能到1200米而且支持一主多从一条总线上理论上可以挂32个标准负载节点。代价是半双工同一时刻只能收或只能发。车载上凡是“多个传感器/设备挂一条总线”的场景基本都是RS485。1.2 车载设备选型依据——距离、节点和速率我在项目里总结了一个简单粗暴的选型表方便之前没做过车载的同事快速上手项目TTL UARTRS232RS485电平3.3V/5V±3V~±15VA-B差分传输距离1米内15米左右1200米通信方式全双工全双工半双工节点数点对点点对点最多32/128个抗干扰弱中强典型车载场景板内模块通信仪表、诊断口多传感器总线、网关实际车载项目里中控屏和仪表之间如果距离很近、就一个屏一个仪表很多方案直接用RS232如果要在车身不同位置挂温度、压力、液位好几个传感器99%会走RS485总线而Android主机和内置的4G模块、蓝牙模块之间基本都是TTL串口。确定好协议类型之后Android端要做的第一件事不是写代码而是确认硬件上已经把TTL转换成了对应的RS232/RS485电平并且知道转换板接在了主板的哪个串口控制器上。另一个需要早点和硬件同事对齐的是波特率。车载设备最常用的是9600和115200老一点的设备还有2400、4800ModbusRTU协议默认常用9600、8N1。波特率不一致会出现“能收到数据但是全是乱码”而且是那种非常规律的乱码。这个坑我在项目里遇到过两次后面第六章详细说排查链路。2. 主机侧串口通路TTL引脚、USB转串口与设备节点对Android应用层开发者来说串口最终映射成一个设备节点文件比如/dev/ttyS1、/dev/ttyMT0、/dev/ttyUSB0。你能读到这个文件就能拿到串口数据。但是不同车机主板上串口是怎么引出的直接决定了你用什么方式去打开这个节点。2.1 车机上常见的三类串口引出方式一类是主板直接引出TTL排针或者排座这是最常见的方案。瑞芯微RK3288/RK3399方案的车机一般引出几路TTL串口系统里对应/dev/ttyS0、/dev/ttyS1、/dev/ttyS2。全志方案类似高通平台有时候是/dev/ttyHS开头MTK平台则常见/dev/ttyMT0这类节点。如果你的Android主机是root过的或厂商给了系统权限可以直接通过JNI去open这些节点。第二类是USB转串口很多不带原生串口的通用Android工控板会用USB转TTL/RS232/RS485模块来接外部设备系统里枚举出来的节点是/dev/ttyUSB0FT232R、CH340这类芯片或者/dev/ttyACM0USB CDC ACM设备。这种情况下其实不需要root用Android的USB Host API配合usb-serial-for-android库就能操作但要求设备硬件上支持USB Host模式而且每个串口都要占用一个USB口。第三类是厂商SDK封装好的系统串口服务。部分车厂Rom会把串口封装成类似SerialManager的系统服务对外提供Java接口但这不是AOSP公开API不同厂商接口差异很大而且资料通常不公开。我见过有厂商直接给一个.so库和几个隐藏接口这种就没什么通用经验可讲只能按厂商文档来。2.2 device节点与open方式为什么一定要O_NOCTTY无论哪种方式Android端读写串口本质上是open设备文件然后read/write。这里直接参考Google开源的android-serialport-api项目GitHub上cepr维护的那个底层SerialPort.c里open设备时带了几个关键flagint fd open(path_utf, O_RDWR | O_NOCTTY | O_NONBLOCK);O_NOCTTY的意思是“如果打开的文件是终端设备不要把它设置为当前进程的控制终端”对串口开发来说是必须的否则可能影响进程行为。O_NONBLOCK是非阻塞模式这里一定要配合后面讲到的termios配置来用。打开成功后还要用tcgetattr、cfmakeraw、cfsetispeed这些函数去配置串口参数。2.3 权限问题没有root要怎么处理车载Android整机现在的SELinux策略收得越来越紧即使设备有root权限应用直接写/dev/ttyS1也可能被SELinux挡住。我常用的几种处理方式按可靠性排序最稳的是在ROM的init.rc里给对应设备节点加chmod 666并在file_contexts或者sepolicy里给应用放行对应串口节点的访问权限。如果是root过的工程机可以在应用启动时执行su -c chmod 666 /dev/ttyS1但重启后失效需要重复执行。如果不是root设备可以尝试用USB转串口方案走USB Host API应用层就能拿到访问权限这是唯一不需要动系统底层的通用路径。权限问题容易在项目联调阶段突然冒出来App装上去打开串口报Permission denied真机shell里用cat /dev/ttyS1能读到数据同一台机器同一个节点应用里就是打不开。这个就是典型的SELinux拦截。我曾经在这上面耗了整整半天后来在系统日志里看到avc: denied记录才反应过来。3. Android串口库选型与JNI配置serial_port的termios真相串口库选择上Android生态里其实就两条主流路线选错了后面维护很被动。一条是基于Google android-serialport-api下来的JNI直驱动路线另一条是基于USB Host的usb-serial-for-android路线。两条我都用过适用场景完全不同。3.1 两条路线JNI直驱动 vs USB Host库JNI直驱动路线的代表是android-serialport-api它通过JNI直接open设备节点底层用Linux的termios接口配置串口。优点是速度快、延迟低、不依赖USB枚举直接操作内核设备节点非常接近嵌入式原生开发缺点是要求设备有root或者厂商开权限而且不同平台ABI要分别编译so库。车载上如果Android主板有原生TTL串口、系统权限也给了这条路线是首选。USB Host路线的代表是mik3y/usb-serial-for-android底层调用Android的UsbManager和UsbSerialPort接口不需要root。库本身封装好了FTDI、CH340、CP210x、PL2303等常见芯片的驱动。优点是免root、维护成本低缺点是每个串口占一个USB口、通过UsbManager请求权限时有弹窗车机环境有时候弹窗不好点、链路比直接open节点多一层而且少数车载盒子对USB转串口芯片的兼容性不太好。我见过一个全志方案的车机插上CH340模块后系统一直不枚举设备换FT232R就好了没有任何道理可讲芯片兼容性就是得实测。选型结论很直接有原生串口节点、有root或系统权限用JNI直驱动只有USB扩展、没有root老老实实用usb-serial-for-android。不要试图在非root设备上硬改/dev节点权限既不稳定也不安全。3.2 编译JNI库的NDK配置与串口参数设置如果你用android-serialport-api拿到源码后需要编译出libserial_port.so。用Android Studio NDK编译时最需要注意的是Application.mk里把APP_ABI配全APP_ABI : arm64-v8a armeabi-v7a x86_64 APP_PLATFORM : android-21别只编一个arm64-v8a很多车机虽然CPU是64位但系统还是32位的用户态只放64位so会直接崩。这个坑在RK328832位系统上特别常见。JNI层配置串口参数还是那套经典的termios调用struct termios cfg; tcgetattr(fd, cfg); cfmakeraw(cfg); cfsetispeed(cfg, speed); cfsetospeed(cfg, speed); cfg.c_cflag | (CLOCAL | CREAD); // 忽略modem控制线允许接收 cfg.c_cflag ~CSIZE; cfg.c_cflag | CS8; // 8数据位 cfg.c_cflag ~CSTOPB; // 1停止位 cfg.c_cflag ~PARENB; // 无校验 cfg.c_cflag ~PARODD; cfg.c_cc[VMIN] 1; cfg.c_cc[VTIME] 0; tcflush(fd, TCIOFLUSH); tcsetattr(fd, TCSANOW, cfg);这里c_cflag的配置是“先清零再置位”的套路。CSIZE、CSTOPB、PARENB这些位域如果不先清零再设置想要的值很容易因为默认值残留导致配置变成7位数据位、2位停止位之类接收端自然就乱了。项目里经常有人改了波特率但忘了改数据位/校验位结果就是和传感器设备对不上。另外VMIN1、VTIME0的意思是read在有数据时至少返回1个字节没有数据时立即返回这个配合非阻塞模式用刚好。3.3 波特率、数据位、校验位、停止位背后的termios语义串口参数本质上是在跟底层驱动约定“怎么把字节流变成电平跳变”。波特率就是每秒钟电平变化的次数9600波特率下传1个字节要10个bit1起始8数据1停止耗时约1.04ms这个时间对后面的超时设计很关键。数据位在车载设备里基本都用8校验位常用无校验8N1少数老设备用偶校验8E1。停止位常用1位。用termios配置时有个细节如果要用奇偶校验除了设置PARENB还需要设置PARODD奇校验或清零偶校验否则设备行为可能和预期不一致。ModbusRTU设备大部分要求8N1所以默认按8N1去配遇到不吐数据的再回头检查是不是校验位的问题。另外cfsetispeed和cfsetospeed要同时设虽然很多驱动里收和发是同一个时钟但严谨起见两个都设上。4. 串口数据通信实战流式字节组帧与多路并发读取串口通信应用层写起来有一个非常重要的思维转变串口是流不是包。TCP至少还能保证你不会读到半个包串口连这层保障都没有。你在read()里拿到的可能是一个字节、半个帧、三个帧拼在一起。所有帧边界都需要自己设计、自己解析。4.1 串口是“流”不是“包”——如何设计自己的帧协议车载仪表、传感器设备一般都有自己的私有协议。如果没有你就得自己设计一版最简可用的帧格式比如帧头(2字节) 长度(1字节) 命令(1字节) 数据(N字节) 校验(1字节) AA 55 LEN CMD DATA CHECK帧头选0xAA 0x55是为了方便识别这两个字节二进制是10101010 01010101波形上看比较明显而且出现随机匹配的概率低。长度字段表示后面数据部分的字节数校验字段可以用异或校验简单计算量小。解析端建议别用边读边拆的土办法而是维护一个字节缓冲区每收到一段数据就追加进去然后循环尝试从缓冲区里解析出完整帧。我用的是状态机方式代码结构大概是public class FrameParser { private static final int STATE_WAIT_HEAD1 0; private static final int STATE_WAIT_HEAD2 1; private static final int STATE_WAIT_LEN 2; private static final int STATE_WAIT_CMD 3; private static final int STATE_WAIT_DATA 4; private static final int STATE_WAIT_CHECK 5; private int state STATE_WAIT_HEAD1; private int frameLen; private byte[] frameBuffer; private int frameIndex; private Listbyte[] completeFrames new ArrayList(); public void push(byte[] data, int size) { for (int i 0; i size; i) { byte b data[i]; switch (state) { case STATE_WAIT_HEAD1: if ((b 0xFF) 0xAA) { state STATE_WAIT_HEAD2; } break; case STATE_WAIT_HEAD2: if ((b 0xFF) 0x55) { state STATE_WAIT_LEN; } else if ((b 0xFF) ! 0xAA) { state STATE_WAIT_HEAD1; } break; case STATE_WAIT_LEN: frameLen (b 0xFF); frameBuffer new byte[frameLen 5]; frameBuffer[0] (byte) 0xAA; frameBuffer[1] (byte) 0x55; frameBuffer[2] b; frameIndex 3; state STATE_WAIT_CMD; break; // ... 略 } } } }这套状态机的要点是任何一个字节不符合预期就回退到找帧头的状态绝不能把一个错位字节当成帧头继续解析。帧错位是串口通信最常见的问题一旦错位轻则丢一帧重则连续解出好几帧乱码。状态机这个写法虽然代码多几行但鲁棒性比“用indexOf扫描帧头”要高得多尤其是数据区里可能恰好出现0xAA 0x55这种伪帧头的情况下。4.2 多路串口并发读的实现要点车载中控经常要同时管多路串口比如一路RS232接仪表、一路RS485挂总线、一路TTL接4G模块。每路串口一个独立线程去阻塞读是所有方案里最不容易出问题的。线程读串口的模板我一直在用new Thread(() - { byte[] buffer new byte[256]; while (!isStop) { int size read(fd, buffer, buffer.length); if (size 0) { parser.push(buffer, size); Listbyte[] frames parser.getCompleteFrames(); for (byte[] frame : frames) { handler.onFrameReceived(frame); } } } }).start();几个实际项目里验证过的注意点读取缓冲区不要太小256字节起步否则高速率下一帧拆成多次read解析逻辑就要处理半帧。不要把业务解析逻辑放进读取线程里。读取线程只负责搬运字节和组帧帧拿到之后丢到Handler或者阻塞队列里交给工作线程解析。有一次我把数据库写入直接写在读取线程里结果串口数据一密集界面直接卡死还被误判成ANR。每路串口必须有独立的心跳或超时监控。车载设备有时候会“假死”就是不发送也不响应如果读取线程被阻塞在read上非阻塞模式下不会但要防止应用层其他问题导致线程卡死整个串口链路就断掉了。我习惯在解析线程里加一个LastReceiveTime时间戳超过设定时间没有数据就报警或者主动重连。轮询模式下还要注意RS485是半双工同一时刻只能一路在发。多路串口如果共用了同一个RS485总线必须串行调度按从站地址顺序轮流发请求不能开多个线程同时往一条总线发数据否则总线上全是冲突帧谁也别想收到正常响应。5. 对接车载协议栈Modbus RTU、CRC16与一主多从轮询车载设备通信里Modbus RTU是出镜率最高的协议尤其是工程机械、农用机械、充电桩、环境监测这些场景里的传感器和仪表很多都支持Modbus RTU。Android端做主站Master轮询从站Slave是非常典型的车载网关形态。5.1 Modbus RTU报文结构Modbus RTU报文没有起始符和结束符靠“总线空闲时间大于3.5个字符时间”来区分每一帧。每个帧的结构是固定的字段长度说明从站地址1字节1~2470为广播功能码1字节03读保持寄存器、04读输入寄存器、06写单寄存器、10H写多寄存器等数据N字节寄存器地址、数量、数据值等CRC162字节低字节在前高字节在后9600波特率下3.5个字符时间大约是3.64ms也就是帧和帧之间要停顿这么久对端才能识别出这是两个帧而不是一帧。在Android端做主站时发完一帧请求后不要立刻发下一个请求至少要等应答超时或收到完整响应后再继续。5.2 Android端CRC16计算与常用功能码封装Modbus CRC16的多项式是0xA001初始值0xFFFF。Java实现不复杂但要注意无符号右移和掩码处理public static int getCRC(byte[] data, int len) { int crc 0xFFFF; for (int i 0; i len; i) { crc ^ (data[i] 0xFF); for (int j 0; j 8; j) { if ((crc 1) ! 0) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }调用时组装请求帧比如读从站1的保持寄存器起始地址0x0000读2个寄存器byte[] request new byte[8]; request[0] 0x01; // 从站地址 request[1] 0x03; // 功能码读保持寄存器 request[2] 0x00; // 起始地址高字节 request[3] 0x00; // 起始地址低字节 request[4] 0x00; // 寄存器数量高字节 request[5] 0x02; // 寄存器数量低字节 int crc getCRC(request, 6); request[6] (byte) (crc 0xFF); // CRC低字节在前 request[7] (byte) ((crc 8) 0xFF); // CRC高字节在后功能码03H的响应帧结构是从站地址、功能码、字节数、寄存器数据每个寄存器2字节高字节在前、CRC16。解析的时候要注意字节序Modbus寄存器数据是大端高字节在前这个跟很多嵌入式设备直接发小端数据的习惯不一样我因为没注意字节序把温度和转速解析反了好几回。5.3 车载网关的轮询调度与超时处理一个Android主机挂多个Modbus从站设备时最简单可靠的做法是定时轮询。轮询队列想清楚一个点RS485是半双工同一时刻只能有一个设备占用总线所以Android端要“串行”地一帧一帧发请求、等响应、再发下一帧。绝对不能为每个从站开一个线程并发去读写总线会直接乱掉。我实现轮询的方式是维护一个请求队列用一个单一的工作线程循环执行从队列头取出一个请求。清空接收缓冲区的旧数据。通过串口发送请求。等待响应设定超时时间一般200~500ms取决于波特率和设备响应速度。收到响应则解析、分发、记录该从站在线超时则标记离线发下一个请求。一个周期扫完之后重新入队。响应帧还有可能被拆成两段到达尤其是波特率低、帧数据长的时候。所以在主站解析时同样要“攒够字节再判断”按最小帧长8个字节起算读完一个完整的响应帧再做CRC校验。如果CRC不对按照Modbus规范应该丢弃这帧而且不回复在主站侧我一般连续收到3次错误CRC就把这个从站标记为异常进入下一个从站避免卡死在某个坏设备上。超时时间怎么设9600波特率下一个字节约1.04ms一帧典型请求/响应在8~20字节之间算上设备的处理时间超时设200ms已经非常充裕。但有些老式仪表响应极慢可能要好几百毫秒最好给每个从站单独配一个超时参数而不是一刀切。6. 实测链路硬件坑RS485自动收发、防护电路与排查流程Android端的代码写得再完美硬件的坑一样能让你数据收不到。这一章是项目实测里最容易踩的几个硬件层面的问题也是纯软件出身的Android工程师最陌生的部分。6.1 RS485自动收发电路的原理与工作边界RS485是半双工所以芯片比如MAX3485、SP3485都有DE发送使能和RE接收使能引脚。常见设计是用一个GPIO控制DE/RE发送时拉高DE、拉低RE接收时反过来。但在很多现成的RS485模块上你会看到它没有引出方向控制线而是用了“自动收发电路”靠串口自身的电平变化去切换方向。自动收发电路的原理不复杂UART空闲时TXD是高电平把这个高电平经过三极管反相后接到DE/RE上就能让模块默认处于接收状态当TXD发起始位低电平时反相变成高电平模块切到发送状态。用三极管搭的电路大致是这样的思路TXD经过一个NPN三极管反相后去控制RE/DE引脚空闲时TXD高→三极管导通→DE/RE为低→接收发送起始位低→三极管截止→DE/RE被上拉为高→发送。这类电路的好处是少一根MCU控制线省事坑在于它依赖TXD在发送整帧数据期间保持紧密连续的跳变。如果发送端每发一个字节之间有个微小的高电平间隙自动收发电路就会误以为发送结束了把方向切回接收总线后半部分的数据直接发不出去。解决办法有两个一是发送端要把整帧数据用一个write调用连续发出去不要逐字节flush二是对于波特率高的场合超过115200自动收发电路可能跟不上方向切换的速度波形会变形这时必须改成GPIO控制方向。我在项目里用过带自动收发的模块9600波特率下工作正常但提到115200后偶发丢字节拆开看波形才发现是方向切换导致的。6.2 RS232/RS485的防护设计TVS、自恢复保险丝与隔离车载环境对串口的浪涌和静电非常不友好。RS232接口上我测过直接插拔线缆时会产生比较明显的电压毛刺严重的时候会把电平转换芯片打坏。RS485总线上更是如此长线缆在雷雨天气甚至大型电机启停时都会感应出高压。最基础的防护方案是在接口处加TVS管瞬态电压抑制二极管RS232常用SM712这类专用的双极性TVSRS485一般用法是A、B线对地各接一个TVS再在A、B之间跨接一个。TVS管的作用是当线上电压超过钳位电压时迅速导通泄放能量防止后级芯片被打坏。注意TVS的钳位电压要选得比芯片极限电压低但又高于正常信号电平否则正常通信就会被削波。电源和信号地也要留意。RS232/RS485通信两端设备地电位不一致时会有共模电流流过信号线导致通信异常甚至烧毁接口芯片。我排查过一个“RS485在A设备上通信正常换到B设备就丢数据”的案例最后发现是B设备的外壳地没接好A、B两端地电位差了十几伏RS485芯片的共模输入范围也就-7V到12V直接超限。规范做法是RS485总线用双绞屏蔽线屏蔽层单端接地同时在每个设备端做好隔离比较彻底的是用带DC-DC隔离电源的数字隔离芯片方案比如ADUM1201隔离电源。车上不方便做完整隔离的话至少保证收发器芯片的电源是干净的并且总线的终端电阻要按规范只在最远端设备上并一个120欧姆。6.3 丢帧、乱码、偶发卡死的排查流程最后分享几个实测排查链路都是我在项目里用了很多遍的招数按这个顺序排查绝大多数串口问题都能定位先确认物理连接和电平匹配。检查Android主机串口引出的到底是TTL还是RS485电平跟目标设备是否一致。TTL对接RS232/RS485必须经过转换板而且转换板的TXD要接目标设备的RXDRXD接目标设备的TXD交叉接线。接反的典型现象是收不到任何数据。用串口助手验证目标设备本身是否在发数据。PC接USB转RS485通过SSCOM这类工具看看能不能正常收到仪表/传感器的数据。这次能收到说明问题在Android侧收不到说明设备侧或线缆有问题。在Android端打原始字节日志。在JNI层或者解析层把read到的十六进制数据直接打出来跟PC串口助手上的数据对比。如果Android端收到的字节跟PC端不一致考虑波特率、校验位、地线问题如果一致但业务解析不对那就是帧解析逻辑问题。做回环测试。把串口的TX和RX直接短接如果是RS485把A-A、B-B接好在Android侧自发自收能收到自己发的数据说明Android串口通路基本是通的问题在外部链路。检查地线。TTL和RS232通信必须在设备之间共地否则表现为时通时不通、偶发乱码。RS485则在总线上A/B线之外尽量保证各设备参考地一致。波形测量。上述都不行就上示波器测TXD/RXD或A/B线的波形看电平幅度够不够、波形畸变是否严重。我之前遇到一个RS232通信间歇性失败的问题示波器一测发现某个转换板的负电平只有-3V刚好在临界值换一个转换板就好了。另外还有一个隐蔽的坑Android系统的串口节点有时候会被别的进程占用。比如有些车机的系统服务自己启动了串口读取你的应用再去open同一个节点就会冲突表现为设备节点打开成功但读不到数据或者打开直接报错。排查方法是在shell里用lsof /dev/ttyS1或者fuser -v /dev/ttyS1看有没有其他进程占着。车载项目里做串口开发很大一部分精力其实不在Android代码上而是在和硬件、协议打交道。一定要尽早确认电平类型、波特率、数据格式、帧协议、设备地址表这些信息让硬件同事把所有设备节点的对应关系列成一张表能省掉后面非常多联调时间。我自己吃过好几次亏都是因为“先写代码再补硬件确认”结果代码写完发现电平都没对上白白返工。建议你接到项目的头两天先花半天时间把所有串口相关的硬件链路和协议文档梳理清楚再动Android代码效率会高很多。