Android串口通信实战:android-serialport-api集成与故障排查 📅 发布时间:2026/9/17 15:45:02 👁 浏览次数: 简介本资源是一份面向Android嵌入式开发者的串口通信进阶实践指南聚焦于对Google官方库android-serialport-api的深度改造与功能扩展。针对该库版本陈旧、不兼容Android Studio工程、且缺失奇偶校验、数据位与停止位等关键串口参数配置能力的痛点文档系统讲解了如何通过修改SerialPort.h和SerialPort.c两个核心C层文件实现对波特率、校验方式无/奇/偶、数据位5–8位及停止位1/2位的完整支持并给出完整的JNI方法签名、参数校验逻辑与termios结构体配置代码。资源为单个PDF文件共271KB内容涵盖源码分析、修改步骤、关键函数说明及错误处理要点结构紧凑、实操性强。已有935人学习下载适合具备JNI基础和Linux串口编程经验的中高级Android开发者用于快速复用并定制化适配工业传感器、POS设备等需精细串口控制的硬件通信场景。1. 为什么在 Android 上用android-serialport-api而不是自己写 JNI——串口通信落地前必须厘清的底层逻辑很多工程师拿到 CH340/CP2102/FTDI USB 转串口模块后第一反应是“写个 JNI 调用 libusb 或直接 open/dev/ttyUSB0”结果卡在权限、SELinux 策略、设备热插拔识别、线程阻塞、字符粘包上。而 Google 官方维护的android-serialport-api注意它并非 Android SDK 内置组件而是 Google 工程师早期为 Android Things 项目开源的轻量级串口封装库现托管于 GitHub android-serialport-api 组织恰恰绕开了这些陷阱它不依赖 root不硬编码设备路径通过UsbManager动态获取设备描述符用FileInputStream/FileOutputStream封装读写流并内置了SerialPort对象生命周期管理与HandlerThread异步回调机制。它适合需要稳定接入工业传感器、POS 外设、单片机升级接口如 C51 串口烧写、RS485 电表采集等场景的中大型 App 开发者——尤其当你发现串口烧写失败常伴随EACCES (Permission denied)或IOException: Broken pipe时这个库提供的open()失败重试策略和close()资源释放保障比手写 JNI 更接近生产环境要求。2. 从零集成android-serialport-apiGradle 依赖、USB 权限声明与设备枚举全流程2.1 正确引入库并规避常见版本冲突android-serialport-api当前主流使用的是基于 Android 5.0 的v1.0.7版本非master分支的未发布快照其核心是纯 Java 接口 预编译.so文件。严禁直接 clone 源码 module 并修改build.gradle中的minSdkVersion——这会导致 NDK 构建失败或 ABI 不匹配。正确做法是// app/build.gradle android { compileSdk 34 defaultConfig { applicationId com.example.serial minSdk 21 // 必须 ≥21因依赖 UsbManager API Level 21 targetSdk 34 // 注意此库不支持 arm64-v8a 以外的 64 位 ABI若需全平台支持需自行补全 .so } } dependencies { implementation com.github.0x00f:android-serialport-api:1.0.7 }提示该库未发布至 Maven Centralimplementation com.github.0x00f:android-serialport-api:1.0.7是经验证的稳定坐标。若同步失败请检查settings.gradle是否已启用mavenCentral()和jcenter()后者已停服建议仅保留mavenCentral()。2.2 声明 USB 权限与设备过滤规则仅添加uses-permission android:nameandroid.permission.USB_PERMISSION /不足以触发授权弹窗。必须配合intent-filter与meta-data显式声明支持的 USB 设备类型!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.USB_PERMISSION / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / application ... activity ... !-- 主 Activity -- /activity !-- USB 设备连接广播接收器 -- receiver android:name.UsbReceiver android:exportedtrue intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /receiver /application其中res/xml/device_filter.xml必须精确匹配目标芯片 ID否则UsbManager.findDevice()返回 null!-- res/xml/device_filter.xml -- ?xml version1.0 encodingutf-8? resources !-- CH340 常见 VID/PID -- usb-device vendor-id6790 product-id29987 / usb-device vendor-id6790 product-id29988 / !-- CP2102 -- usb-device vendor-id4292 product-id60000 / !-- FTDI -- usb-device vendor-id1027 product-id24577 / /resources注意vendor-id和product-id必须为十进制整数非十六进制。可通过adb shell cat /sys/bus/usb/devices/*/idVendor和/sys/bus/usb/devices/*/idProduct查看真实值若不确定可先用串口调试助手或友善串口助手连接成功后反查。2.3 枚举设备并请求用户授权UsbManager不会自动授予权限必须显式调用requestPermission()并处理onReceive()回调// UsbReceiver.java public class UsbReceiver extends BroadcastReceiver { private static final String ACTION_USB_PERMISSION com.example.serial.USB_PERMISSION; Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device ! null isSupportedDevice(device)) { UsbManager manager (UsbManager) context.getSystemService(Context.USB_SERVICE); PendingIntent permissionIntent PendingIntent.getBroadcast( context, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE); manager.requestPermission(device, permissionIntent); // 触发系统弹窗 } } else if (ACTION_USB_PERMISSION.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { // ✅ 用户点击“允许”此时可安全调用 SerialPort.open() openSerialPort(device, 9600, 8, 1, N); } else { Toast.makeText(context, USB 权限被拒绝, Toast.LENGTH_SHORT).show(); } } } private boolean isSupportedDevice(UsbDevice device) { return device.getVendorId() 6790 (device.getProductId() 29987 || device.getProductId() 29988); } }3. 使用SerialPort对象完成可靠读写超时控制、线程模型与粘包处理实战3.1 构建SerialPort实例的 4 个关键参数解析SerialPort构造函数签名如下每个参数都直接影响通信稳定性SerialPort serialPort new SerialPort( new File(/dev/ttyUSB0), // 设备节点路径 —— 由 UsbManager 动态获取不可硬编码 9600, // 波特率 —— 必须与外设严格一致CH340 默认 9600C51 升级常为 115200 0, // flags —— 保持 0该库内部已处理 O_RDWR | O_NOCTTY | O_NDELAY 8, // dataBits —— 数据位常见 8C51 升级协议可能为 7 1, // stopBits —— 停止位1 或 2多数设备用 1 N // parity —— 校验位N(none), O(odd), E(even)工业传感器常用 E );提示/dev/ttyUSB0路径由UsbManager.getDeviceList()返回的UsbDevice对象通过getDeviceName()获取而非固定字符串。若getDeviceName()返回空说明设备未被内核识别需检查ubuntu ch340串口驱动是否加载Linux 下lsmod | grep ch340或 Windows 下驱动是否为WinUSB模式。3.2 启动异步读取线程并处理原始字节流android-serialport-api不提供自动帧解析需自行实现缓冲区管理。以下是最小可用读取循环private SerialPort mSerialPort; private InputStream mInputStream; private OutputStream mOutputStream; private Thread mReadThread; private void startReadThread() { mReadThread new Thread(() - { byte[] buffer new byte[1024]; while (!Thread.currentThread().isInterrupted()) { try { int len mInputStream.read(buffer); if (len 0) { // 关键此处收到的是 raw bytes无换行符自动截断 // 若协议为“帧头长度数据CRC”需在此处做粘包/半包处理 onDataReceived(buffer, len); } } catch (IOException e) { // EAGAIN 或 EINTR 可忽略其他异常需记录并重启串口 Log.e(Serial, read error, e); break; } } }); mReadThread.start(); } private void onDataReceived(byte[] data, int length) { // 示例假设协议为 \n 分隔的 ASCII 行如 AT 指令响应 String line new String(data, 0, length, StandardCharsets.US_ASCII).trim(); if (!line.isEmpty()) { // 发送到主线程更新 UI如 android 进度条、串口监听工具显示 runOnUiThread(() - tvLog.append(line \n)); } }3.3 写入数据时的阻塞与超时控制OutputStream.write()默认阻塞若外设未响应线程将永久挂起。必须设置setSoTimeout()但SerialPort未暴露该方法实际方案是// 方案一使用带超时的 write推荐 private boolean writeWithTimeout(byte[] data, int timeoutMs) { try { // 先清空输出缓冲区避免旧数据干扰 mOutputStream.flush(); // 写入新数据 mOutputStream.write(data); mOutputStream.flush(); // 等待外设响应如 ACK超时则返回 false long start System.currentTimeMillis(); while (System.currentTimeMillis() - start timeoutMs) { if (mInputStream.available() 0) { return true; // 收到响应 } Thread.sleep(10); } return false; // 超时 } catch (Exception e) { Log.e(Serial, write timeout, e); return false; } } // 方案二对 C51 单片机串口升级架构常需发送 0x00 同步字节 public void sendSyncByte() { try { mOutputStream.write(new byte[]{0x00}); mOutputStream.flush(); // 等待单片机返回 0x55 确认 byte[] ack new byte[1]; if (mInputStream.read(ack, 0, 1) 1 ack[0] (byte) 0x55) { Log.d(Serial, C51 sync OK); } } catch (IOException e) { Log.e(Serial, sync failed, e); } }4. 排查串口烧写失败与linux从串口接收数据丢失的 3 类根源及对应日志证据4.1 SELinux 策略拦截avc: denied { open }是最隐蔽的失败原因Android 8.0 默认启用 SELinux enforcing 模式即使有 USB 权限open(/dev/ttyUSB0)仍可能被拒绝。必须检查logcat -b events | grep avc$ adb logcat -b events | grep avc # 若出现 # avc: denied { open } for path/dev/ttyUSB0 devtmpfs ino12345 scontextu:r:untrusted_app:s0:c123,c256,c512,c768 tcontextu:object_r:device:s0 tclasschr_file permissive0 # 则证明 SELinux 拦截 —— 此时 串口烧写失败 与 串口关闭 日志均无报错但 mInputStream.available() 始终为 0解决方法在device/qcom/sepolicy/common/usb_device.te中添加规则需定制 ROM或临时切换为 permissive 模式验证仅开发阶段adb shell su -c setenforce 0 # 临时关闭 adb shell getenforce # 确认返回 Permissive4.2 USB 设备节点权限不足EACCES (Permission denied)的真实含义UsbManager.requestPermission()成功 ≠ 设备节点可访问。需验证/dev/ttyUSB0的属主与权限$ adb shell su -c ls -l /dev/ttyUSB* # 正常应为 # crw-rw---- 1 system system 188, 0 2024-01-01 10:00 /dev/ttyUSB0 # 若为 root:root 或权限为 0600则需手动 chown/chmod不推荐或确认 UsbManager 是否正确绑定更可靠的方式是在openSerialPort()前用UsbManager.openDevice()获取UsbDeviceConnection再通过claimInterface()确保独占访问UsbDeviceConnection connection manager.openDevice(device); if (connection ! null connection.claimInterface(device.getInterface(0), true)) { // ✅ 此时设备已被当前 App 独占/dev/ttyUSB0 可安全打开 serialPort new SerialPort(new File(device.getDeviceName()), baudRate, 0); }4.3 缓冲区溢出与uart串口通信速率失配导致的数据丢失当linux从串口接收数据丢失时90% 情况是InputStream缓冲区太小或读取频率过低。android-serialport-api默认使用FileInputStream其内部缓冲区为 8KB但若外设以 115200 波特率连续发送 10KB 数据而 App 每 100ms 才read()一次则必然丢包。验证方法在onDataReceived()中打印length与buffer.lengthLog.d(Serial, String.format(read %d/%d bytes, len, buffer.length)); // 若频繁出现 len buffer.length即 1024说明数据洪峰到来缓冲区已满优化方案增大读取缓冲区 提高轮询频率// 将 buffer 从 1024 改为 4096 byte[] buffer new byte[4096]; // 在 read 循环中缩短 sleep 时间需权衡 CPU 占用 Thread.sleep(1); // 替代 10ms5. 针对ch340串口驱动与rs485串口通讯的专项配置技巧5.1 CH340 设备在 Android 12 上的 VID/PID 适配表CH340 芯片存在多个硬件版本其product-id随固件升级变化。以下为实测有效的device_filter.xml补充项芯片型号vendor-idproduct-id适用场景CH340G679029987旧版开发板CH340T679029988新版 USB 转 TTL 模块CH341A679029990支持 I2C/SPI 的多协议芯片CH340B679029991低功耗版本注意若adb shell getprop ro.build.version.sdk返回 31Android 12需额外在AndroidManifest.xml中声明android:exportedtrue于UsbReceiver否则广播无法接收。5.2 RS485 半双工模式下的 DE/RE 引脚控制android-serialport-api本身不控制硬件 RTS/CTS但 RS485 通信必须协调方向。常见做法是复用RTS信号线作为DEDriver Enable// 控制 RTS 引脚需外设支持 private void setRtsState(boolean enable) { try { // 通过 ioctl 控制 RTS需 root 权限或内核支持 TIOCMSET ParcelFileDescriptor pfd ParcelFileDescriptor.open( new File(/dev/ttyUSB0), ParcelFileDescriptor.MODE_READ_WRITE); // 此处需调用 native 方法参考 android-serialport-api 的 SerialPort.java 中 setRTS() // 实际项目中更推荐使用支持自动流控的 USB 转 RS485 模块如 MAX3485 自带 RTS 控制逻辑 } catch (Exception e) { Log.w(RS485, RTS control not supported, e); } } // 更稳妥的方案使用硬件自动收发的模块App 层只需按普通串口发送 // 发送前调用 setRtsState(true)发送后 setRtsState(false) // 但 android-serialport-api 未暴露 setRTS()需 fork 修改 SerialPort.java 添加 // public void setRTS(boolean state) { mFd.setRTS(state); }5.3 在android studio中快速验证串口连通性的最小测试代码无需完整 UI一个Application子类即可验证public class SerialTestApp extends Application { Override public void onCreate() { super.onCreate(); // 自动尝试打开第一个 CH340 设备 UsbManager manager (UsbManager) getSystemService(Context.USB_SERVICE); for (UsbDevice device : manager.getDeviceList().values()) { if (device.getVendorId() 6790) { PendingIntent permissionIntent PendingIntent.getBroadcast( this, 0, new Intent(TEST_USB), PendingIntent.FLAG_IMMUTABLE); manager.requestPermission(device, permissionIntent); break; } } } }然后在logcat中过滤SerialPort关键字观察是否输出SerialPort opened at /dev/ttyUSB0及后续读写日志。此法可绕过android studio怎么设置中文?或android studio汉化等界面干扰直击通信链路本质。本文还有配套的精品资源点击获取