ARTICLE DETAIL

资讯详情

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

Android Studio串口通信实例:权限、参数、数据收发与粘包处理

Android Studio串口通信实例:权限、参数、数据收发与粘包处理 简介这是一份面向Android开发者的串口通信实例工程基于Android Studio构建并已通过测试适合需要实现设备交互或物联网通信的开发者参考。工程覆盖串口参数配置、设备打开与关闭、数据收发以及定时自动发送等核心操作能够帮助理解Android平台下访问串口设备的完整流程与权限处理方式。项目涉及波特率、数据位、停止位、校验位等常用参数设置并给出数据读写时的字节流编码解码处理思路对实际接入外部硬件设备很有参考价值。压缩包内共62个文件以Java源码、XML布局与配置、Gradle构建脚本为主同时包含SO动态库、APK安装包及辅助脚本整体大小362KB结构简明便于直接导入学习。资源目前已有377人学习下载既可作为入门串口开发的实战范例也可在项目开发中快速复用其中的串口管理逻辑与界面交互设计。1. Android Studio 里的串口通信实例先把场景摆出来接到一块 STM32F103C8T6 开发板再把 CH340 模块插进手机的 OTG 线Android Studio 跑起来的 App 就能读到单片机定时上报的传感器数据。这个动作是很多做嵌入式配套调试、工业终端、实验室设备采集的工程师的日常手机当串口助手用。标题里这个串口通信实例项目核心能力其实就四件事串口设置、打开、发送、接收再加一个自动发送。这篇文章不依赖任何一份源码包只沿着我在 Android 上做串口调试的常用路径把方案讲清楚。新手能照着配置出第一个可收发数据的 App熟手也能在权限、参数、粘包这些环节找到自己之前没处理干净的点。2. 打开串口前的那些事驱动识别、权限与串口参数选型2.1 为什么 Android 侧串口多半走 USB 转串口Android 设备没有像 PC 那样直接引出的 COM 口常见做法是用 USB Host 模式外接一个 USB 转 TTL 模块。开发板侧大多是 UART 串口通信电平是 TTL 的 3.3V 或 5V而老式 RS232 串口通信用的是负逻辑电平两者不能直接对接。手机发出 USB 数据后由 CH340、CP2102 这类芯片转成 TTL 电平再送到单片机的 RX/TX。所以 Android 串口通信的第一件事是确认转接模块的芯片型号后续的权限过滤和驱动识别都依赖这个型号对应的 vendorId 与 productId。这里容易踩一个思路上的坑别一上来就找/dev/ttyS0。Android 不像常规 Linux 发行版那样把 USB 转串口都枚举成 ttyUSB 设备供应用直接读写除非系统做过内核适配并且有 root 权限。绝大多数产品项目走 USB Host UsbSerial 库更稳妥驱动识别、参数设置、读写超时都封装在库层代码量少一个数量级。2.2 权限声明与设备过滤先让系统把串口设备认出来在AndroidManifest.xml里声明 USB Host 功能并放一个设备过滤文件把 CH340 这类芯片的 vendorId/productId 写进去。CH340 常见的 vendorId 是0x1A86对应产品可能是0x7523CP2102 的 vendorId 是0x10C4productId 是0xEA60。然后通过UsbManager获取设备列表并发起权限请求。UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice deviceList usbManager.getDeviceList(); for (UsbDevice device : deviceList.values()) { if (device.getVendorId() 0x1A86 device.getProductId() 0x7523) { if (usbManager.hasPermission(device)) { openSerial(device); } else { PendingIntent permissionIntent PendingIntent.getBroadcast( this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE ); usbManager.requestPermission(device, permissionIntent); } } }这段代码里有两个参数值得留意vendorId 匹配芯片厂商productId 匹配具体型号。做通用工具类时可以把 vendorId 放宽用device.getVendorId()打印出枚举到的设备列表再决定要不要精确匹配 productId。FLAG_IMMUTABLE是 targetSdk 31 之后 PendingIntent 的硬性要求老项目升版本后不加会直接崩这不是串口问题但会卡在串口权限这步。2.3 串口参数选型波特率、数据位、停止位、校验位参数不对代码再漂亮也收不到有效数据。以下是串口参数选型的常规对照表。参数常见值选型说明波特率9600 / 115200短距离调试默认 115200长线或低速单片机用 9600 更稳数据位8少数老协议用 7文本类协议默认 8Modbus ASCII 等老协议可能用 7停止位1 / 2默认 1设备时钟误差偏大时用 2 增加容错校验位None / Even / Odd多数传感器协议用 None少数可靠性要求高的用 CRC 而不是校验位真实项目中遇到最多的情况是串口波特率 9600 能通信4800 反而没有数据。很多人以为是 App 参数填错反复改停止位和校验位问题依旧。常见原因是单片机用内部 RC 时钟时4800 这类低波特率下 USB 转串口芯片的分频误差被放大采样点偏移到边界。排查方向应该先看设备端是不是用了外部晶振再用逻辑分析仪抓实际波形而不是在 Android 端死磕参数。3. 在 Android Studio 中实现串口的打开、发送与接收代码3.1 用 UsbSerial 库把设备抽象成串口Android Studio 开发 App 实例里串口通信最省事的做法是引入 usb-serial-for-android 库。在build.gradle里加上依赖版本以仓库实际解析到为准。implementation com.github.mik3y:usb-serial-for-android:latest.release库的工作方式是把 USB 设备包装成UsbSerialPort对象。先通过UsbSerialProber在已知驱动表里查找匹配端口再 open 并设置参数。UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); UsbDeviceConnection connection usbManager.openDevice(device); UsbSerialPort port UsbSerialProber.getDefaultProber().findPort(usbManager, device); if (port null || !port.open(connection)) { // 打印 vendorId:productId确认驱动表是否覆盖当前芯片 return; } port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);findPort返回 null 是最常见的失败点。如果是 CH340但库版本较旧驱动表里可能没有这个芯片。此时打印device.getVendorId() : device.getProductId()拿去和库的驱动表比对或者注册自定义的UsbSerialProber。setParameters的第一个参数是波特率后面三个参数要和设备端固件完全一致。3.2 发送数据与接收线程主线程绝不能碰 read下面是一个精简的串口读写封装保留最核心的打开、接收、发送、关闭四条路径。public class SerialHelper { private UsbSerialPort port; private Thread readThread; private volatile boolean running; public void open(UsbManager usbManager, UsbDevice device) throws IOException { UsbDeviceConnection connection usbManager.openDevice(device); if (connection null) throw new IOException(openDevice failed); port UsbSerialProber.getDefaultProber().findPort(usbManager, device); if (port null || !port.open(connection)) throw new IOException(port open failed); port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); startReadThread(); } private void startReadThread() { running true; byte[] buffer new byte[1024]; readThread new Thread(() - { while (running) { try { int len port.read(buffer, 500); if (len 0) { // 回调到 UI 层不要在这里直接更新控件 } } catch (IOException e) { break; } } }); readThread.start(); } public void send(byte[] data) throws IOException { port.write(data, 1000); } public void close() throws IOException { running false; if (readThread ! null) readThread.interrupt(); if (port ! null) { port.close(); port null; } } }接收线程里的port.read(buffer, 500)第二个参数是超时毫秒数。传 500 表示每 500ms 返回一次即使没有数据也会返回 0这样running标志能被及时检查到。真正耗时的是协议解析不要直接写在 readThread 里否则下一帧数据到了没人读缓冲区会越积越乱。3.3 生命周期与设备插拔不关闭端口的下一次打开必失败串口设备是独占资源。Activity 走到 onDestroy 时如果不调用close()下一次插上同一个设备port.open()大概率返回 false因为系统认为这个设备仍被占用。更隐蔽的问题是设备物理拔出时没有释放引用App 里还拿着一个失效的UsbSerialPort再往里write会抛异常。我一般会在 onResume 注册ACTION_USB_DEVICE_DETACHED广播在广播里把当前串口引用置空并通知界面切换状态。真机调试时注意Android Studio 模拟器不会转发 USB Host 事件串口通信只能在真机上验证插上 OTG 线后还要在系统设置里确认有没有弹出 USB 设备授权框。4. 自动发送、十六进制收发与串口异常排查4.1 自动发送定时任务要能随时停得下来自动发送在很多协议调试场景下是刚需设备上电后需要周期性地发送心跳帧、查询指令或者循环发送一组配置命令验证设备响应。常见间隔是 100ms、500ms、1s。用Handler.postDelayed自回调比Timer更轻也更容易在销毁时停止。private final Handler autoSendHandler new Handler(Looper.getMainLooper()); private final Runnable autoSendTask new Runnable() { Override public void run() { if (!autoSending) return; sendData(hexStringToBytes(55 AA 01 02)); autoSendHandler.postDelayed(this, sendInterval); } }; private byte[] hexStringToBytes(String hex) { String clean hex.replaceAll(\\s, ); byte[] bytes new byte[clean.length() / 2]; for (int i 0; i clean.length(); i 2) { bytes[i / 2] (byte) ((Character.digit(clean.charAt(i), 16) 4) | Character.digit(clean.charAt(i 1), 16)); } return bytes; }hexStringToBytes的作用是把输入框里的55 AA 01 02这类带空格的十六进制字符串转成真实字节而不是把字符5的 ASCII 码发出去。postDelayed的间隔在系统省电模式下会有几十毫秒误差如果设备要求精确到毫秒级帧间隔改用ScheduledExecutorService配合单线程池。停止自动发送时记得removeCallbacks否则界面销毁后回调还在跑会触发空指针。4.2 接收侧的粘包与半包不要按 read 长度当一帧port.read读到的长度是驱动层面给到的一次数据量和协议帧没有任何对应关系。一帧 10 字节可能被拆成两次 read 到达也可能 3 帧数据在一次 read 里全部出现。如果直接把每次 read 的结果丢给界面解析显示会断断续续低频数据时偶尔正常高频数据时必然出问题。常用的规避方式是在接收线程维护一个临时缓冲把每次 read 到的数据追加进去然后按照协议里的帧头、帧尾或固定长度来切帧。简单协议可以在一帧数据较短时用ByteArrayOutputStream暂存每次写入后循环截取完整帧帧长不固定时就需要状态机逐字节处理。4.3 乱码与无数据先定位链路问题还是协议问题现象检查项内容乱码波特率不一致或数据位/停止位/校验位不匹配发送正常但收不到模块 RX/TX 是否接反GND 有没有和设备端共地一段时间后无数据流控未关闭CTS/RTS 悬空导致芯片进入等待状态插拔后第一次打开失败上一次连接没有 close端口被系统标为占用手机重启后不识别重新插拔并重新授权检查应用是否被系统杀掉PC 端 STC-ISP 这类烧录软件出现串口通信乱码时大家第一反应是波特率选错。Android 端排查逻辑完全一样先用设备端跑一个固定发送0x55 0xAA的测试固件App 按十六进制显示。如果字节内容稳定说明链路没问题乱码只出现在文本编码环节如果字节本身就是乱的多半在波特率或硬件接线不需要继续纠结解析代码。5. 环形缓冲、帧同步与低速设备兼容性优化5.1 用环形缓冲替代无上限的字节流拼接接收线程里如果只做baos.write(data, 0, len)然后反复toByteArray()数据量一大就会出现频繁扩容和内存抖动。更好的做法是固定容量的环形缓冲写入时覆盖最旧数据帧解析时只从头部消费。public synchronized int readFrame(byte[] out, int maxLen) { while (size 0 (buffer[head] 0xFF) ! FRAME_HEAD) { head (head 1) % capacity; size--; } if (size FRAME_LEN || maxLen FRAME_LEN) return 0; for (int i 0; i FRAME_LEN; i) { out[i] buffer[head]; head (head 1) % capacity; } size - FRAME_LEN; return FRAME_LEN; }这段代码解决了两个问题一是自动丢弃帧头之前的噪声字节二是半包到达时返回 0等下一批数据写进来再继续取。FRAME_HEAD和FRAME_LEN按设备协议定义如果协议里只有帧头没有固定长度就把循环条件改成“找到帧头后再按帧尾字节判断”。5.2 低速设备兼容性把参数配置做成可切换的预设51 单片机串口通信这类慢速设备上很多模块对波特率误差非常敏感9600 以下还要注意外部晶振频率是否为 11.0592MHz。我一般会把波特率、数据位、停止位、校验位、帧头、帧尾、帧长封装成一个SerialConfig对象简单场景下直接在设置页下拉切换预设而不是每次改固件都重新编译 App。这样串口设置、打开、发送、接收、自动发送这些操作入口都收敛在同一个界面里排障时只要先看当前配置项再决定动硬件还是动代码。本文还有配套的精品资源点击获取
返回列表