
简介面向Android串口通信开发场景这份实例项目基于Android Studio构建并已通过实际测试适合物联网应用开发者、硬件交互工程师以及有一定Android基础的学习者参考。项目完整实现了串口参数设置、打开与关闭、数据收发、自动发送等核心操作流程涵盖了波特率、数据位、停止位等配置逻辑并附有可安装的APK与工程源码方便直接运行验证与二次改造。资源压缩包共六十二个文件包体仅三百六十二KB规模紧凑文件类型以Java源码、XML界面配置、Gradle构建脚本为主同时包含动态链接库、图片素材、APK安装包等分别对应业务逻辑、界面布局、工程构建和底层串口支持。目录组织按Android工程标准分层便于定位与修改。目前已有三百七十七人学习下载开发者可通过此项目掌握USB权限申请、字节流编解码、定时自动发送等串口通信关键环节并参考其中的测试方法排查连接异常、数据丢失等问题。1. 串口调试在 Android 上的血泪现实这个项目为什么值得拆你拿着平板接了块 STM32 控制板UART 三根线焊得整整齐齐结果 App 一打开/dev/ttyS3就报 Permission denied。这不是个别现象Android 对串口设备的访问权限限制比普通 Linux 发行版严得多root 过的机器还能靠 chmod 硬闯没 root 的基本只能换方案或换设备。SerialPortDetection-master 这个项目之所以值得拆是因为它把“找到设备 → 打开串口 → 配置参数 → 收发数据 → 自动发送”这条链路完整跑通了而且工程里带了可安装的 app-release.apk意味着编译产物已经验证过不是那种结构残缺的示例工程。适合正在做物联网设备调试助手、需要对接单片机或工控板的中高级 Android 开发者也适合想搞明白 android-serialport-api 底层到底做了什么的人。2. 串口接入 Android 的底层逻辑从设备节点到 SerialPort 类2.1 为什么应用层拿不到串口句柄权限与设备节点Android 底层是 Linux 内核串口设备在系统中以文件节点形式存在常见路径是/dev/ttyS0、/dev/ttyS1、/dev/ttyMT0这类。普通 App 进程没有权限对这些设备节点执行读写因为没有配置对应的 SELinux 策略和 Linux group 权限。这是 Android 与桌面 Linux 差异最大的地方——在 PC 上你给用户加个dialout组就能用串口在 Android 上这套不直接生效。要绕开这个限制常见的做法有三个一是设备已 root在 App 里用su命令改变节点权限二是系统应用签名直接把 App 放进/system/app并声明android.permission.SERIAL_PORT三是修改设备端ueventd.rc给串口节点分配可被 App 访问的 group。SerialPortDetection-master 这类项目走的是第一条和第三条的混合路径——它在 Java 层不做权限操作而是通过 JNI 调起 native 层代码在 C 侧完成设备节点的open()调用然后利用 Android 系统对外部设备的宽容策略部分定制 ROM 上/dev/ttyS*权限为 666实现无 root 打开。如果你的设备上仍然权限不足第一件事就是先ls -l /dev/ttyS*看看节点的实际权限位。2.2 SerialPort 核心类的设计逻辑用 native 层绕开 Java 限制打开一个串口本质上就是打开一个文件描述符然后通过termios结构体配置波特率、数据位、停止位、校验位。Java 层做不到这一点所以 android-serialport-api 这个库的思路就是写一段 C 代码编译成libserial_port.so通过System.loadLibrary(serial_port)加载再用 native 方法把文件描述符传回 Java 层。SerialPortDetection-master 里的核心类就是SerialPort.java它封装了open()、close()、getInputStream()、getOutputStream()几个关键接口。这段 JNI 封装的价值在于串口的打开和参数配置全部在 native 层完成Java 层拿到的已经是可用的FileDescriptor后续的读写直接基于InputStream和OutputStream操作不需要再操心底层的系统调用。项目里另一个值得看的类是SerialPortFinder它的作用是扫描/dev目录下所有ttyS、ttyUSB、ttyMT等前缀的设备节点把这些设备的路径返回给 UI 层做下拉选择。2.3 串口参数设置表波特率、校验位、停止位的匹配逻辑参数常见取值说明波特率9600 / 19200 / 38400 / 115200决定每秒传输的比特数两端必须一致数据位5 / 7 / 8一次传输的数据比特数常用 8停止位1 / 2传输结束的停止位数量常用 1校验位NONE / ODD / EVEN奇偶校验一般选 NONE流控无 / RTS/CTS / XON/XOFF硬件流控和软件流控多数场景关闭参数不匹配的表现很有意思波特率一致但数据位不一致收的是乱码波特率差一位比如 9600 对 19200收到的会是一堆看似有规律实则无法解析的字节。排查这类问题时的第一步不是看代码而是确认两端参数完全一致。2.4 用代码实例列出可用串口并选中目标端口SerialPortFinder 是 android-serialport-api 里现成的工具类用法很直接SerialPortFinder finder new SerialPortFinder(); String[] entryValues finder.getAllDevicesPath(); for (String path : entryValues) { File device new File(path); if (device.canRead() device.canWrite()) { // 这里筛选出当前有读写权限的节点 } }如果你拿到的是 SerialPortDetection 一类的源码可以沿SerialPortFinder.java的getAllDevicesPath()逐行看它的扫描逻辑本质上就是枚举/dev下的文件用正则匹配ttyS、ttyUSB、ttyMT等前缀。UI 层需要把这个数组填充到 Spinner 或 ListView 里用户选择后点击“打开”按钮再触发下面的逻辑private SerialPort mSerialPort; private OutputStream mOutputStream; private InputStream mInputStream; private void openSerialPort(File device, int baudRate, int flags) { try { mSerialPort new SerialPort(device, baudRate, flags); mOutputStream mSerialPort.getOutputStream(); mInputStream mSerialPort.getInputStream(); } catch (IOException e) { Log.e(SerialTag, open failed: device.getAbsolutePath(), e); } }flags参数传入 0 表示默认配置baudRate传入115200这类整数值。构造器内部会在 native 层配置termios的cfsetispeed和cfsetospeed同时处理数据位和停止位的c_cflag标志位。这里有一个细节值得留意SerialPort构造器里对device对象做了canRead()和canWrite()检查如果某个设备节点权限不足会直接抛出SecurityException异常信息会明确告诉你“请检查读写权限”。3. 收发链路怎么搭读取线程、发送编码与自动发送实现3.1 读取线程阻塞式 read 与 UI 刷新串口数据的读取没有“事件通知”机制标准做法是起一个独立线程在InputStream.read()上阻塞等待数据到达。这个项目里最核心的读取线程逻辑可以抽象为下面的骨架private class ReadThread extends Thread { Override public void run() { byte[] buffer new byte[1024]; int size; while (!isInterrupted()) { try { if (mInputStream null) { return; } size mInputStream.read(buffer); if (size 0) { onDataReceived(buffer, size); } } catch (IOException e) { // 串口被关闭或通道断开 break; } } } }InputStream.read(buffer)是阻塞方法没有数据时线程会一直挂起。这种设计的好处是 CPU 占用为零坏处是关闭串口时必须同时中断线程否则read()会一直阻塞导致close()无法真正释放资源。项目里停止串口时通常会调用readThread.interrupt()并置空输入流这里有个坑interrupt()并不能打断阻塞中的read()正确做法是先close()输入流让read()抛异常退出再回收线程。onDataReceived回调里收的是原始字节UI 层要显示时再用bytesToHexString(buffer, 0, size)转换并runOnUiThread刷新控件。3.2 发送数据十六进制与 ASCII 的分叉处理串口调试工具的发送框一般有两种输入模式ASCII 和 Hex。ASCII 模式直接把你敲的字符编码成字节发送适合可读文本Hex 模式把你输入的十六进制字符串按两位一字节解析适合协议调试。SerialPortDetection 这类项目在点击“发送”按钮时会先判断当前选中了哪种模式再决定走哪条转换路径。public static byte[] hexStringToBytes(String hex) { // 去掉输入中的空格支持 01 02 0A 这类格式 hex hex.replace( , ); int len hex.length(); if (len % 2 ! 0) { return null; // 奇数位说明输入不合法 } byte[] data new byte[len / 2]; for (int i 0; i len; i 2) { int high Character.digit(hex.charAt(i), 16); int low Character.digit(hex.charAt(i 1), 16); if (high -1 || low -1) { return null; } data[i / 2] (byte) ((high 4) low); } return data; }这段代码的目的是把界面上的字符串解析成真正发到串口上的字节。Character.digit(c, 16)把字符0-9A-F转换为 0-15 的数值high 4 low合成一个字节。发送时mOutputStream.write(data)后会调用flush()确保数据落到驱动缓冲区而不是停留在 Java 层。实际使用中要注意有些设备对数据之间的间隔时间敏感连续调用write()发送多包数据时需要在两次发送之间加入Thread.sleep(50)级别的延时。3.3 自动发送定时任务与可中断状态管理自动发送是这个项目里比较有代表性的一块。它本质上是把“手动点发送”变成“定时点发送”但工程实现上需要处理好任务取消、间隔配置和发送频率控制三个问题。private Handler mHandler new Handler(Looper.getMainLooper()); private Runnable mAutoSendTask; private void startAutoSend(long intervalMs, byte[] data) { stopAutoSend(); // 先清理已有任务避免重复叠加 mAutoSendTask new Runnable() { Override public void run() { if (mOutputStream ! null) { try { mOutputStream.write(data); mOutputStream.flush(); } catch (IOException e) { stopAutoSend(); } } mHandler.postDelayed(this, intervalMs); } }; mHandler.postDelayed(mAutoSendTask, intervalMs); } private void stopAutoSend() { if (mAutoSendTask ! null) { mHandler.removeCallbacks(mAutoSendTask); mAutoSendTask null; } }用Handler.postDelayed比ScheduledExecutorService更适合这类单线程刷新 UI 的场景因为回调本身就跑在主线程不需要再跨线程切换。stopAutoSend先执行的方式避免了用户在短时间内反复点击“启动/停止”导致多个定时任务并发叠加的典型问题。间隔时间建议做成可配置项因为不同设备对发送间隔的要求差异很大轮询传感器可能 200ms 一次就够控制类协议最好 20ms 到 50ms超过设备处理能力会导致数据堆积。4. 接到真实设备上的验证手段与排错清单接线正确但数据出不来时的排查路径应该是这样的。先把串口的 TX 和 RX 短接在 App 里发送一组01 02 03 04如果接收区回显完全一致说明这个 App 本身的收发链路没有问题。然后用 USB 转 TTL 模块连接到调试板PC 端打开串口助手设置成与应用完全一致的波特率验证硬件链路的数据流是否正常。最后再接目标设备观察 logcat 输出adb logcat -s SerialTag:D如果 App 侧报了open failed优先看设备节点路径和权限如果发送后对端无响应优先确认波特率、停止位、校验位是否严格一致如果接收区出现乱码则把关注点放到数据位和校验位这两个容易漏掉的参数上。这些验证步骤同样适用于 STM32 串口通信和 UART 串口通信的场景项目底层的 termios 配置逻辑是通用的。还有一类容易混淆的情况需要分清如果你的设备走的是 USB Host 模式比如手机通过 OTG 线接一个 USB 转串口模块那走的是UsbSerial类库基于 Android 的 USB 权限管理框架与本项目基于/dev/ttyS*内核串口的方案完全不同。SerialPortDetection 面向的是板载串口或调试串口已映射到系统设备的场景两者在设备发现机制和权限模型上截然不同做项目选型时先确认目标设备的串口资源以什么形式暴露给系统再决定复用这套代码还是另走 UsbSerial 路线。针对权限问题有一个处理技巧值得记下来在没有 root 的设备上调试时可以检查项目里是否已经包含提升串口节点访问权限的 shell 命令通过 Runtime 执行su -c chmod 666 /dev/ttyS*作为兜底方案虽然不优雅但能保证在开发阶段不卡在权限这一环。本文还有配套的精品资源点击获取