☰
Android串口开发demo实战:从设备连接到数据收发
2026/10/15 11:21:07 网站建设 项目流程

简介:这是一份面向Android开发者的串口通信实战示例,适合需要与外部硬件设备交互的智能硬件控制、数据采集等场景,帮助开发者掌握在Android平台上打开、配置与读写串口的完整流程。资源包共1040个文件,以630个flat构建产物、185个xml配置、124个json数据、45个java源码为主,另含png、bin、properties、gradle等辅助文件,压缩包约4.45MB,目录结构清晰,便于按模块检索学习。目前已有2017人学习下载。示例覆盖权限声明、第三方库Android-SerialPort-API的使用、波特率与数据位等参数配置、串口读写与事件监听、异常处理及资源释放等关键知识点,并涉及USB Host API与硬件兼容性思路,可作为开发者快速搭建串口通信应用、排查连接与通信问题的实践参考。

1. Android 串口开发demo:从插上设备到收到第一帧数据

很多人第一次做 Android 串口开发demo,卡住的地方不是代码,而是「线插上了,App 里却一个字节都读不到」。工控平板、扫码枪、电子秤、门禁读头、单片机调试板,这些设备在 Android 上通信,绕不开串口。它解决的是「App 直接和硬件对话」这件事,适合做工业手持终端、自助机、智能硬件的开发者。串口不像网络请求那样有现成的 OkHttp,它要处理设备节点权限、波特率、数据位、校验位、流控,还要面对不同芯片厂商的驱动差异。这篇笔记按「先跑通最小闭环,再补参数和排错」的顺序写,代码可以直接抄,参数会逐个解释,坑也会提前标出来。

2. 串口通信在 Android 上到底怎么落地

2.1 为什么 Android 串口不是标准 API

Android 官方 SDK 里没有串口 API,这是很多人第一个意外。原因是串口属于 Linux 设备节点,Android 内核虽然基于 Linux,但应用层被沙箱隔离,普通 App 没有权限直接打开/dev/ttyS*或/dev/ttyUSB*。所以常见做法是两条路:一条是用 JNI 调用 C/C++ 打开设备节点,另一条是用厂商提供的.so库或系统签名权限。前者通用但需要自己写 native 层,后者省事但绑定硬件。

我一般会先确认设备节点是否存在。把串口设备插上,用 adb 进 shell 看:

adb shell ls -l /dev/ttyS* ls -l /dev/ttyUSB*

如果能看到类似crw-rw---- 1 root root 4, 64 /dev/ttyS0的输出,说明内核识别到了。注意权限位,普通 App 用户是读不了的。很多工控板会在init.rc里把节点权限改成0666,或者把 App 加入system组。没有 root 权限时,这是第一个要跟硬件厂商确认的点。

2.2 最小可跑通的工程结构

一个能复现的 Android 串口 demo,通常包含四块:Java 层调用接口、JNI 桥接、native 串口操作、权限申请。下面是一个精简的 JNI 接口定义,放在SerialPort.java里:

public class SerialPort { static { System.loadLibrary("serial_port"); } // 打开串口,返回文件描述符,失败返回 -1 public static native int open(String path, int baudRate); // 关闭串口 public static native void close(int fd); // 读取数据,返回实际读到的字节数 public static native int read(int fd, byte[] buffer, int size); // 写入数据,返回实际写入的字节数 public static native int write(int fd, byte[] data, int size); }

native 层用open()系统调用打开设备,再用termios结构配置波特率、数据位、停止位、校验位。核心配置片段如下:

#include <fcntl.h> #include <termios.h> #include <unistd.h> int open_serial(const char *path, int baud) { int fd = open(path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd < 0) return -1; struct termios options; tcgetattr(fd, &options); // 设置波特率,常见 9600、115200 cfsetispeed(&options, B115200); cfsetospeed(&options, B115200); // 8 数据位,无校验,1 停止位 options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB; options.c_cflag &= ~CSIZE; options.c_cflag |= CS8; // 关闭流控 options.c_cflag &= ~CRTSCTS; options.c_cflag |= CREAD | CLOCAL; // 原始模式,不做行处理 options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); options.c_iflag &= ~(IXON | IXOFF | IXANY); options.c_oflag &= ~OPOST; tcsetattr(fd, TCSANOW, &options); return fd; }

逻辑说明:O_NOCTTY防止串口成为控制终端,O_NDELAY让read不阻塞。cfsetispeed和cfsetospeed分别设输入输出波特率,两边必须和设备一致。CS8表示 8 位数据,PARENB取反表示无校验,CSTOPB取反表示 1 位停止位。CLOCAL忽略调制解调器控制线,CREAD允许接收。ICANON等关闭后进入原始模式,否则读到换行才返回。

参数说明:波特率不是随便设的,设备手册写多少就设多少,9600 和 115200 最常见。数据位多数是 8,校验位多数是 None,停止位多数是 1。流控如果设备没接 RTS/CTS 线,一定要关掉,否则会一直等。

2.3 在 Java 层收发数据的线程模型

串口读写不能放在主线程,否则会 ANR。常见做法是开一个读线程循环读,写操作单独加锁。下面是一个简化的读取循环:

public class SerialReader extends Thread { private final int fd; private volatile boolean running = true; public SerialReader(int fd) { this.fd = fd; } @Override public void run() { byte[] buffer = new byte[1024]; while (running) { int len = SerialPort.read(fd, buffer, buffer.length); if (len > 0) { byte[] data = new byte[len]; System.arraycopy(buffer, 0, data, 0, len); // 回调给业务层,注意切回主线程更新 UI onDataReceived(data); } else { try { Thread.sleep(20); // 没有数据时短暂休眠,避免空转 } catch (InterruptedException e) { break; } } } } public void stopRead() { running = false; } private void onDataReceived(byte[] data) { // 业务处理,比如解析协议帧 } }

逻辑说明:read返回大于 0 表示读到了字节,拷贝出来交给业务层。返回 0 或 -1 时休眠 20 毫秒再试,避免 CPU 空转。running用volatile修饰,保证停止标志对读线程可见。

参数说明:缓冲区大小 1024 是经验值,协议帧长就调大。休眠时间 20 毫秒适合大多数场景,太短费电,太长丢实时性。写数据时建议加synchronized,防止多线程交叉写导致帧破碎。

3. 参数配置与协议解析:让数据真正可用

3.1 波特率、数据位、校验位怎么对齐

串口通信最玄学的问题就是「收到乱码」。九成原因是参数没对齐。下面这张表是我常用的对照:

参数常见值设置位置对不齐的后果
波特率9600 / 19200 / 115200cfsetispeed乱码或全 0
数据位8CS8字节错位
校验位None / Even / OddPARENB、PARODD偶发校验错
停止位1 / 2CSTOPB帧边界错
流控None / RTSCTSCRTSCTS卡死或丢数据

对齐方法:先看设备手册,再用串口调试工具在 PC 上验证一遍,确认参数无误后再写进 App。如果手册没写,按 9600、8、None、1 试,这是最通用的组合。

3.2 粘包和半包:协议解析绕不开的坎

串口是字节流,没有消息边界。设备一次发 20 字节,App 可能分两次读到,也可能两次数据粘在一起。常见做法是定长帧、帧头帧尾、或者长度字段。下面是一个带帧头的解析示例:

public class FrameParser { private static final byte HEADER = (byte) 0xAA; private static final byte TAIL = (byte) 0x55; private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); public void feed(byte[] data) { buffer.write(data, 0, data.length); byte[] all = buffer.toByteArray(); int start = -1; for (int i = 0; i < all.length; i++) { if (all[i] == HEADER) { start = i; } else if (all[i] == TAIL && start >= 0) { byte[] frame = new byte[i - start + 1]; System.arraycopy(all, start, frame, 0, frame.length); onFrame(frame); // 移除已解析部分 buffer.reset(); if (i + 1 < all.length) { buffer.write(all, i + 1, all.length - i - 1); } return; } } } private void onFrame(byte[] frame) { // 校验并解析业务字段 } }

逻辑说明:把所有收到的字节先塞进缓冲区,再扫描帧头和帧尾。找到完整帧后回调,并把剩余字节留在缓冲区等下一帧。这样能同时处理粘包和半包。

参数说明:帧头帧尾要选设备协议里不会出现在数据区的值,否则会误判。如果协议有校验和,解析后先校验再使用。缓冲区要设上限,防止异常数据把内存撑爆。

3.3 权限与设备节点适配

没有 root 的 Android 设备,打开/dev/ttyS*会返回Permission denied。常见解法有三种:让系统预置 App 为系统应用、用厂商提供的sepolicy放行、或者用 USB 转串口芯片走 USB 权限。USB 方案在普通手机上更可行,通过UsbManager申请权限后拿到文件描述符,再交给 native 层配置。这条路不需要 root,但需要处理 USB 插拔广播和权限对话框。

4. 串口开发避坑:那些让我熬夜的翻车现场

4.1 打开成功但读不到数据

现象:open返回了有效 fd,读线程一直返回 0。原因通常是流控没关,或者设备节点选错了。有些板子有多个ttyS,对应不同物理接口。解决:先用cat /dev/ttyS0在 adb shell 里看有没有输出,确认节点正确;再检查CRTSCTS是否关闭。

4.2 收到数据全是乱码

现象:能读到字节,但解析出来是乱码。原因九成是波特率不匹配,或者数据位、校验位设错。解决:把设备手册翻出来逐项核对,用 PC 串口工具交叉验证。如果 PC 上正常、Android 上乱码,检查 native 层是否真的调用了tcsetattr,有些芯片需要先tcflush清空缓冲区。

4.3 写数据后设备没反应

现象:write返回了正数,但设备不执行。原因可能是没发完整帧,或者缺少结束符。有些协议要求\r\n结尾,有些要求校验和。解决:用逻辑分析仪或串口监听工具抓一次 PC 端正常通信的原始字节,和 App 发出的字节逐位对比。

4.4 长时间运行后读线程卡死

现象:跑几个小时或几天后,读线程不再返回。原因是read在某些驱动上会阻塞,或者 fd 被意外关闭。解决:给read设置超时,用select或poll监听可读事件;同时加看门狗,超过一定时间没数据就重开串口。

4.5 不同设备节点名不统一

现象:这台板子是/dev/ttyS0,换一台变成/dev/ttyS3。原因是硬件厂商的引脚映射不同。解决:不要硬编码节点名,做成配置项,或者遍历/dev下所有ttyS*逐个尝试打开,用设备返回的握手帧确认。

5. 进阶技巧:把 demo 变成能上线的串口模块

5.1 用 select 做非阻塞读取

前面用O_NDELAY加休眠的方式简单,但实时性差。更稳的做法是在 native 层用select监听 fd:

#include <sys/select.h> int read_with_timeout(int fd, unsigned char *buf, int size, int timeout_ms) { fd_set readfds; struct timeval tv; FD_ZERO(&readfds); FD_SET(fd, &readfds); tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; int ret = select(fd + 1, &readfds, NULL, NULL, &tv); if (ret > 0 && FD_ISSET(fd, &readfds)) { return read(fd, buf, size); } return 0; // 超时 }

逻辑说明:select在超时时间内等待 fd 可读,可读才调用read,避免阻塞。参数说明:timeout_ms一般设 100 到 500 毫秒,太短增加系统调用次数,太长影响响应。

5.2 串口重连与状态机

上线设备会遇到串口松动、设备重启。一个健壮的模块要有状态机:关闭、打开中、已打开、错误。每次读写失败进入错误态,延时后重试打开。重试间隔建议指数退避,从 500 毫秒开始,最大 5 秒。

5.3 日志与抓包

串口问题难查,是因为没有后悔药。我习惯在 native 层加一个环形缓冲区,记录最近 4KB 的收发原始字节,出问题时导出成文件。Java 层再记录每次打开的参数和时间戳。这样现场设备出问题,拿回日志就能还原。

5.4 验证方法

写完 demo 后,用回环测试验证:把串口的 TX 和 RX 短接,App 发什么就应该收到什么。这一步能排除协议解析的干扰,单独验证驱动和配置。回环通过后,再接真实设备调协议。

我自己的习惯是,每换一款硬件,先花十分钟做三件事:确认设备节点、确认权限、做一次回环。这三步做完,后面基本不会翻车。串口开发没有捷径,参数对齐和边界处理就是全部。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询