简介:本资源是一份面向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-api:Gradle 依赖、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 Central,
implementation 'com.github.0x00f:android-serialport-api:1.0.7'是经验证的稳定坐标。若同步失败,请检查settings.gradle是否已启用mavenCentral()和jcenter()(后者已停服,建议仅保留mavenCentral())。
2.2 声明 USB 权限与设备过滤规则
仅添加<uses-permission android:name="android.permission.USB_PERMISSION" />不足以触发授权弹窗。必须配合intent-filter与meta-data显式声明支持的 USB 设备类型:
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.USB_PERMISSION" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application ...> <activity ...> <!-- 主 Activity --> </activity> <!-- USB 设备连接广播接收器 --> <receiver android:name=".UsbReceiver" android:exported="true"> <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </receiver> </application>其中res/xml/device_filter.xml必须精确匹配目标芯片 ID,否则UsbManager.findDevice()返回 null:
<!-- res/xml/device_filter.xml --> <?xml version="1.0" encoding="utf-8"?> <resources> <!-- CH340 常见 VID/PID --> <usb-device vendor-id="6790" product-id="29987" /> <usb-device vendor-id="6790" product-id="29988" /> <!-- CP2102 --> <usb-device vendor-id="4292" product-id="60000" /> <!-- FTDI --> <usb-device vendor-id="1027" product-id="24577" /> </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 默认 9600,C51 升级常为 115200 0, // flags —— 保持 0,该库内部已处理 O_RDWR | O_NOCTTY | O_NDELAY 8, // dataBits —— 数据位,常见 8,C51 升级协议可能为 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" dev="tmpfs" ino=12345 scontext=u:r:untrusted_app:s0:c123,c256,c512,c768 tcontext=u:object_r:device:s0 tclass=chr_file permissive=0 # 则证明 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.length:
Log.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-id | product-id | 适用场景 |
|---|---|---|---|
| CH340G | 6790 | 29987 | 旧版开发板 |
| CH340T | 6790 | 29988 | 新版 USB 转 TTL 模块 |
| CH341A | 6790 | 29990 | 支持 I2C/SPI 的多协议芯片 |
| CH340B | 6790 | 29991 | 低功耗版本 |
注意:若
adb shell getprop ro.build.version.sdk返回 31(Android 12),需额外在AndroidManifest.xml中声明android:exported="true"于UsbReceiver,否则广播无法接收。
5.2 RS485 半双工模式下的 DE/RE 引脚控制
android-serialport-api本身不控制硬件 RTS/CTS,但 RS485 通信必须协调方向。常见做法是复用RTS信号线作为DE(Driver 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汉化等界面干扰,直击通信链路本质。
本文还有配套的精品资源,点击获取