
简介本资源是一个经过实测的Android串口通信实战项目面向Android中高级开发者及嵌入式IoT应用开发者解决在Android平台调用底层串口设备如传感器、工控模块时缺乏原生API支持的核心问题。项目基于Android Studio构建完整覆盖串口初始化、参数配置波特率/数据位/校验位、双向数据收发、线程安全读写、异常处理与资源释放等关键环节并集成事件回调机制便于UI实时响应。压缩包共312个文件含16个核心Java源码、57个XML布局与配置文件、98个编译后class及12个APK安装包另有so库支持硬件交互、rawproto与bin文件体现协议解析能力整体大小为17.79MB。已有3123人学习下载提供可直接运行的调试环境、清晰的模块化目录结构以及典型物联网场景下的串口控制范例是理解Android串口通信底层原理与工程落地的优质实践素材。1. Android串口通信实例为什么连上USB转串口模块后App收不到数据不是驱动问题而是权限、线程和JNI层三重关卡没过很多嵌入式开发者或工业现场工程师第一次在Android设备上对接PLC、温控仪、RFID读卡器或传感器模组时会默认“Linux内核支持串口Android肯定能直接用”。结果是USB转串口线插上了设备管理器里能看到/dev/ttyUSB0但App调用open()返回-1或者read()永远阻塞又或者收到的数据全是乱码。这不是玄学——这是Android对硬件访问的沙箱机制、Java层与Native层的内存边界、以及串口通信本身对时序和缓冲区的严苛要求共同作用的结果。本篇不讲Linux串口原理只聚焦一个可复现、可调试、已在多款Android 8.013设备含ARM64平板、工控终端稳定运行的串口通信实例从设备识别、权限申请、JNI封装到非阻塞读写闭环。适合需要将Android设备作为现场数据采集终端、HMI面板或协议网关的工程师也适合刚从单片机开发转向Android底层交互的开发者。你不需要Root设备也不依赖特定厂商SDK所有代码基于标准NDK和Android SDK构建。2. 串口通信在Android上的技术选型为什么不用纯Java方案而必须用JNIlibusbtermios2.1 纯Java方案的致命缺陷无法绕过Android的USB权限模型和内核驱动隔离Android系统对USB设备的访问采用两级管控用户空间权限必须通过UsbManager.requestPermission()显式向用户申请且该权限仅对当前USB设备有效VID/PID绑定拔插即失效内核驱动映射即使用户授予权限Android默认不加载ftdi_sio、ch341等常见USB转串口驱动/dev/ttyUSB*节点不会自动创建。提示某些定制ROM如某国产工控平板预装了CH341驱动并开放/dev/ttyS*节点但这属于特例不可作为通用方案。标准AOSP不包含任何USB转串口驱动模块。因此纯Java方案如jSSC、RXTX在Android上基本不可行——它们依赖/dev/tty*文件句柄而这个句柄在未Root、未预装驱动的设备上根本不存在。我们只能走另一条路绕过内核驱动直接与USB设备通信。2.2 选择libusb termios组合在用户态完成串口协议栈我们采用分层实现底层C/C使用libusb-1.0直接发送USB控制请求SET_LINE_CODING、SET_CONTROL_LINE_STATE模拟串口握手并通过Bulk传输收发数据中间层JNI封装libusb_device_handle为Java可持有的SerialPort对象暴露open()、write()、read()接口上层Java/Kotlin在主线程申请USB权限在子线程中调用JNI方法避免ANR。这种方案的优势在于✅ 不依赖内核驱动兼容FTDI、CH340、CP2102、PL2303全系列芯片✅ 可精确控制波特率、数据位、停止位、校验位termios结构体直传✅ 支持热插拔检测通过libusb_hotplug_register_callback❌ 缺点是需编译ARMv7/ARM64/x86_64多架构so库APK体积增加约1.2MB。2.3 为什么不用Android USB Host API直接读写——它不提供串口语义层Android原生UsbDeviceConnection.bulkTransfer()只能做原始字节传输但串口通信需要波特率配置需发送USB控制请求RTS/CTS流控信号切换需SET_CONTROL_LINE_STATE数据帧格式起始位、校验位、停止位由硬件实现软件无法模拟。若强行用bulkTransfer替代你会陷入无尽调试发送0x01指令后收不到响应 → 实际是未置高DTR信号设备未唤醒连续发送多包数据丢包 → 未启用XON/XOFF或RTS/CTS接收端缓冲区溢出同一指令有时成功有时失败 → 未等待SET_LINE_CODING请求完成就发数据。所以我们必须在JNI层封装完整的串口状态机把termios参数翻译成USB控制请求序列。3. 搭建JNI串口通信工程从NDK环境配置到C层核心函数实现3.1 创建支持多ABI的Android Studio工程并集成libusb新建项目时选择“Empty Activity”在app/build.gradle中配置NDKandroid { compileSdk 34 defaultConfig { applicationId com.example.serialportdemo minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 // 关键声明支持的CPU架构 ndk { abiFilters armeabi-v7a, arm64-v8a, x86_64 } } externalNativeBuild { cmake { path file(../CMakeLists.txt) } } }在项目根目录创建CMakeLists.txt声明libusb依赖cmake_minimum_required(VERSION 3.22.1) project(serial_port_demo) # 添加libusb源码推荐静态链接避免.so版本冲突 add_subdirectory(libusb-1.0.26 EXCLUDE_FROM_ALL) # 创建共享库 add_library(serial_port SHARED serial_port.c ) # 链接libusb和log target_link_libraries(serial_port usb-1.0 log android ${CMAKE_DL_LIBS} ) # 导出头文件路径 target_include_directories(serial_port PRIVATE libusb-1.0/libusb )注意libusb-1.0.26源码需手动下载解压到项目libusb-1.0.26/目录。不要用find_package(libusb)——Android NDK无pkg-config环境必须静态编译。3.2 C层核心函数serial_open()如何完成USB设备初始化与串口参数设置serial_port.c中定义关键函数逻辑严格按USB CDC ACM规范执行#include libusb-1.0/libusb.h #include string.h #include unistd.h #include sys/time.h // 全局USB上下文 static libusb_context *ctx NULL; // 设备句柄每个串口独占 static libusb_device_handle *handle NULL; // USB CDC ACM控制请求常量 #define CDC_SEND_ENCAPSULATED_COMMAND 0x00 #define CDC_GET_ENCAPSULATED_RESPONSE 0x01 #define CDC_SET_COMM_FEATURE 0x02 #define CDC_GET_COMM_FEATURE 0x03 #define CDC_CLEAR_COMM_FEATURE 0x04 #define CDC_SET_LINE_CODING 0x20 #define CDC_GET_LINE_CODING 0x21 #define CDC_SET_CONTROL_LINE_STATE 0x22 #define CDC_SEND_BREAK 0x23 // termios结构体映射简化版仅支持常用参数 typedef struct { int baudrate; uint8_t data_bits; // 5,6,7,8 uint8_t stop_bits; // 1,2 uint8_t parity; // 0none, 1odd, 2even, 3mark, 4space } serial_config_t; // 核心打开函数 int serial_open(int vid, int pid, serial_config_t *config) { int ret; // 1. 初始化libusb上下文 if (!ctx) { ret libusb_init(ctx); if (ret 0) return ret; libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, 3); // LOG_INFO } // 2. 查找匹配的USB设备 libusb_device **list; ssize_t cnt libusb_get_device_list(ctx, list); if (cnt 0) return (int)cnt; libusb_device *device NULL; for (ssize_t i 0; i cnt; i) { struct libusb_device_descriptor desc; ret libusb_get_device_descriptor(list[i], desc); if (ret 0 desc.idVendor vid desc.idProduct pid) { device list[i]; break; } } libusb_free_device_list(list, 1); if (!device) return -1; // 设备未找到 // 3. 打开设备 ret libusb_open(device, handle); if (ret ! 0) return ret; // 4. 获取设备描述符确认是CDC ACM类 struct libusb_device_descriptor dev_desc; libusb_get_device_descriptor(device, dev_desc); if (dev_desc.bDeviceClass ! 0x02) { // CDC Device Class libusb_close(handle); handle NULL; return -2; } // 5. 声明接口通常Interface 0是ControlInterface 1是Data ret libusb_claim_interface(handle, 0); if (ret ! 0) { libusb_close(handle); handle NULL; return ret; } // 6. 设置波特率、数据位等发送SET_LINE_CODING请求 uint8_t line_coding[7]; memset(line_coding, 0, sizeof(line_coding)); // 波特率小端存储 line_coding[0] config-baudrate 0xFF; line_coding[1] (config-baudrate 8) 0xFF; line_coding[2] (config-baudrate 16) 0xFF; line_coding[3] (config-baudrate 24) 0xFF; // 停止位01bit, 11.5bit, 22bit line_coding[4] config-stop_bits 2 ? 2 : 0; // 奇偶校验0none, 1odd, 2even, 3mark, 4space line_coding[5] config-parity; // 数据位5,6,7,8 line_coding[6] config-data_bits; ret libusb_control_transfer( handle, LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, CDC_SET_LINE_CODING, 0, 0, line_coding, 7, 1000 ); if (ret ! 7) { libusb_release_interface(handle, 0); libusb_close(handle); handle NULL; return -3; } // 7. 激活DTR/RTS信号置高DTR表示设备已准备好 uint16_t control_state 0x0001; // DTR1, RTS0 ret libusb_control_transfer( handle, LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, CDC_SET_CONTROL_LINE_STATE, control_state, 0, NULL, 0, 1000 ); if (ret 0) { libusb_release_interface(handle, 0); libusb_close(handle); handle NULL; return -4; } return 0; // 成功 }逻辑说明该函数完成了7个关键步骤初始化libusb→查找设备→打开设备→校验CDC类→声明接口→设置线路参数→激活DTR。每一步失败都返回明确错误码便于Java层定位。参数说明vid/pid为USB设备厂商ID和产品ID可用lsusb或AndroidUsbManager.getDeviceList()获取config结构体封装了波特率等参数避免Java层传递复杂对象。3.3 Java层JNI桥接如何安全传递配置参数并处理异步读写在SerialPort.java中声明native方法并实现生命周期管理public class SerialPort { static { System.loadLibrary(serial_port); } private long mHandle; // 存储C层libusb_device_handle指针实际为long // 构造函数不打开设备仅保存VID/PID public SerialPort(int vid, int pid) { this.vid vid; this.pid pid; } // JNI打开串口同步需在子线程调用 public boolean open(int baudRate, int dataBits, int stopBits, int parity) { serial_config_t config new serial_config_t(); config.baudrate baudRate; config.data_bits (byte) dataBits; config.stop_bits (byte) stopBits; config.parity (byte) parity; int ret serial_open(vid, pid, config); if (ret 0) { // 成功后获取handle用于后续读写 mHandle get_native_handle(); // 由JNI实现返回C层handle地址 return true; } return false; } // JNI读取非阻塞返回实际读取字节数 public int read(byte[] buffer, int timeoutMillis) { if (mHandle 0) return -1; return serial_read(mHandle, buffer, timeoutMillis); } // JNI写入阻塞直到全部发送 public int write(byte[] buffer) { if (mHandle 0) return -1; return serial_write(mHandle, buffer); } // 关闭串口必须调用 public void close() { if (mHandle ! 0) { serial_close(mHandle); mHandle 0; } } // native方法声明 private native int serial_open(int vid, int pid, serial_config_t config); private native long get_native_handle(); private native int serial_read(long handle, byte[] buffer, int timeout); private native int serial_write(long handle, byte[] buffer); private native void serial_close(long handle); }关键设计点open()方法不直接暴露libusb_device_handle给Java而是用long mHandle存储其地址避免GC回收导致悬空指针read()支持超时参数内部用libusb_bulk_transfer()配合libusb_handle_events_timeout()实现非阻塞所有JNI方法必须检查mHandle ! 0防止重复关闭或空指针崩溃。4. Android端完整调用流程USB权限申请、后台线程读写、数据解析闭环4.1 在Activity中申请USB权限并启动串口服务class MainActivity : AppCompatActivity() { private lateinit var usbManager: UsbManager private lateinit var serialPort: SerialPort private var readThread: Thread? null private val usbPermissionIntent Intent(this, UsbPermissionReceiver::class.java) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) usbManager getSystemService(Context.USB_SERVICE) as UsbManager serialPort SerialPort(0x0403, 0x6001) // FTDI VID/PID // 注册USB权限广播接收器 val filter IntentFilter(ACTION_USB_PERMISSION) registerReceiver(UsbPermissionReceiver(), filter) } // 请求USB权限通常在按钮点击时触发 fun onConnectClick(view: View) { val deviceList usbManager.deviceList if (deviceList.isEmpty()) { Toast.makeText(this, 未检测到USB设备, Toast.LENGTH_SHORT).show() return } val device deviceList.values.firstOrNull { it.vendorId 0x0403 it.productId 0x6001 } if (device ! null) { if (!usbManager.hasPermission(device)) { usbManager.requestPermission(device, usbPermissionIntent) } else { openSerialPort() } } } private fun openSerialPort() { if (serialPort.open(115200, 8, 1, 0)) { // 115200,8N1 startReadThread() Toast.makeText(this, 串口打开成功, Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, 串口打开失败, Toast.LENGTH_SHORT).show() } } private fun startReadThread() { readThread Thread { val buffer ByteArray(1024) while (true) { val len serialPort.read(buffer, 100) // 100ms超时 if (len 0) { // 主线程更新UI使用Handler或LiveData runOnUiThread { val hexStr buffer.copyOfRange(0, len).joinToString( ) { %02X.format(it) } textView.text 收到: $hexStr } } else if (len 0) { // 超时继续轮询 continue } else { // 错误退出线程 break } } } readThread!!.start() } override fun onDestroy() { super.onDestroy() serialPort.close() readThread?.interrupt() unregisterReceiver(UsbPermissionReceiver()) } }注意UsbPermissionReceiver需在AndroidManifest.xml中注册并声明uses-permission android:nameandroid.permission.USB_PERMISSION /。权限请求是异步的必须在广播接收器中回调openSerialPort()。4.2 解析Modbus RTU协议帧从原始字节到功能码寄存器值假设对接的是Modbus RTU从机发送01 03 00 00 00 02 C4 0B读保持寄存器0x0000起2个期望返回01 03 04 00 01 00 02 B8 FA。在readThread中解析// 在readThread的while循环内添加解析逻辑 if (len 0) { val data buffer.copyOfRange(0, len) // Modbus RTU帧[addr][func][data...][crc16] if (data.size 5) { // 最小帧长地址功能码2字节数据crc val crc calculateCRC16(data, data.size - 2) val receivedCRC (data[data.size - 1].toInt() and 0xFF) or ((data[data.size - 2].toInt() and 0xFF) shl 8) if (crc receivedCRC) { val addr data[0].toInt() and 0xFF val func data[1].toInt() and 0xFF if (func 0x03 data[2].toInt() and 0xFF 0x04) { // 读保持寄存器4字节数据 val reg1 (data[3].toInt() and 0xFF) shl 8 or (data[4].toInt() and 0xFF) val reg2 (data[5].toInt() and 0xFF) shl 8 or (data[6].toInt() and 0xFF) Log.d(Modbus, Reg0$reg1, Reg1$reg2) } } } }提示CRC16计算必须严格按Modbus标准多项式0x8005初始值0xFFFF低位在前。网上多数Java实现有字节序错误建议直接移植C语言标准实现。4.3 处理USB热插拔设备断开时自动重连Android USB Host API支持热插拔监听但需注意UsbManager的registerOnDeviceAttachedListener仅在API 33可用兼容方案是轮询usbManager.deviceList对比上次快照。在readThread中加入检测private fun checkUsbDeviceAlive(): Boolean { val currentList usbManager.deviceList return currentList.values.any { it.vendorId 0x0403 it.productId 0x6001 } } // 在readThread循环开头添加 if (!checkUsbDeviceAlive()) { Log.w(Serial, USB设备已断开) serialPort.close() // 可在此处触发重连逻辑如延时后重新requestPermission break }5. 串口通信避坑指南5个血泪经验总结90%的翻车都发生在这里5.1 现象serial_open()返回-3SET_LINE_CODING失败原因USB设备未正确进入CDC ACM模式或发送了非法波特率值如115200被截断为低16位。解决在libusb_control_transfer后添加libusb_get_last_error(ctx)打印具体错误码确保line_coding[0..3]按小端序填充baudrate115200对应0x0001C200→ 字节数组[0x00, 0xC2, 0x01, 0x00]某些CH340芯片需先发送0xA4 0x00 0x00 0x00 0x00 0x00 0x00初始化命令查阅CH340 datasheet第12页。5.2 现象read()返回0超时但用串口助手能正常通信原因未正确设置USB数据接口Interface 1或未启用DTR信号导致设备休眠。解决使用libusb_get_active_config_descriptor()确认设备有2个接口且Interface 1的bInterfaceClass0x0ACDC Data在serial_open()末尾补发一次CDC_SET_CONTROL_LINE_STATEcontrol_state0x0003DTR1, RTS1某些设备如PL2303需在SET_LINE_CODING后延时10ms再发数据。5.3 现象连续发送多包数据时第二包开始丢失原因libusb_bulk_transfer()未等待前一次传输完成就发起下一次导致USB控制器缓冲区溢出。解决在serial_write()中每次bulkTransfer后调用libusb_handle_events_timeout()等待1ms或改用libusb_submit_transfer()提交异步传输用callback通知完成更高效但代码复杂在Java层加锁synchronized(serialPort) { serialPort.write(data) }。5.4 现象同一台设备在Android 11上正常Android 12上requestPermission()无响应原因Android 12强制要求uses-feature android:nameandroid.hardware.usb.host /声明且需在AndroidManifest.xml中添加android:exportedtrue到UsbPermissionReceiver。解决uses-feature android:nameandroid.hardware.usb.host android:requiredtrue / receiver android:name.UsbPermissionReceiver android:exportedtrue intent-filter action android:nameandroid.hardware.usb.action.USB_PERMISSION / /intent-filter /receiver5.5 现象App退到后台后串口通信中断原因Android 8.0对后台Service有限制且libusb_handle_events()在后台线程可能被系统挂起。解决将读写线程改为ForegroundService并在onStartCommand()中调用startForeground()或改用WorkManager定期唤醒但实时性下降更优方案在Application类中持有SerialPort单例并在onTrimMemory(TRIM_MEMORY_UI_HIDDEN)时不释放资源。6. 工业级稳定性增强技巧心跳包、自动重连、日志回溯与离线缓存6.1 实现串口心跳机制检测设备在线状态并自动恢复纯靠read()超时判断设备掉线不可靠设备可能卡死但USB链路仍通。应叠加物理层心跳// C层添加心跳函数 int serial_send_heartbeat(libusb_device_handle *handle) { // 发送Modbus 0x08诊断命令回送请求 uint8_t heartbeat[] {0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint16_t crc calculate_modbus_crc(heartbeat, 6); heartbeat[6] crc 0xFF; heartbeat[7] (crc 8) 0xFF; return libusb_bulk_transfer( handle, 0x02, // OUT endpoint根据设备描述符确定 heartbeat, 8, NULL, 1000 ); } // Java层定时任务 val heartbeatTimer Timer() heartbeatTimer.scheduleAtFixedRate(object : TimerTask() { override fun run() { if (serialPort.isOpen) { val ret serial_send_heartbeat() if (ret 0) { Log.e(Heartbeat, 心跳失败触发重连) reconnectSerial() } } } }, 0, 5000) // 5秒一次关键点心跳包必须是设备支持的、有明确响应的命令如Modbus 0x08不能发无意义字节。响应超时阈值设为2秒避免误判。6.2 离线数据缓存策略网络中断时本地存储恢复后批量上传当串口设备是传感器网关需对接云平台时网络波动会导致数据丢失。应在Java层加一层环形缓冲区class SerialDataBuffer(private val capacity: Int 1000) { private val buffer ArrayDequeByteArray(capacity) fun add(data: ByteArray) { if (buffer.size capacity) buffer.removeFirst() buffer.addLast(data.clone()) } fun drain(): ListByteArray { return buffer.toList().also { buffer.clear() } } } // 在readThread中 val dataBuffer SerialDataBuffer() // 收到数据时 if (len 0) { dataBuffer.add(buffer.copyOfRange(0, len)) // 同时尝试上传 uploadToCloud(buffer.copyOfRange(0, len)) } // 网络恢复监听 override fun onAvailable(network: Network) { val pending dataBuffer.drain() pending.forEach { uploadToCloud(it) } }提示ArrayDeque比LinkedList内存更紧凑capacity1000可缓存约1MB数据按平均帧长1KB计足够覆盖30分钟网络中断。6.3 串口通信日志回溯记录每一帧收发时间戳与错误码生产环境调试最头疼的是“当时发生了什么”。应在JNI层埋点// C层添加日志结构 typedef struct { uint64_t timestamp; // us int type; // 0read, 1write, 2error int len; uint8_t data[256]; int error_code; } serial_log_t; static serial_log_t log_buffer[1000]; static int log_index 0; void log_serial_event(int type, uint8_t *data, int len, int error) { struct timeval tv; gettimeofday(tv, NULL); log_buffer[log_index].timestamp (uint64_t)tv.tv_sec * 1000000 tv.tv_usec; log_buffer[log_index].type type; log_buffer[log_index].len len 256 ? len : 256; if (data len 0) memcpy(log_buffer[log_index].data, data, log_buffer[log_index].len); log_buffer[log_index].error_code error; log_index (log_index 1) % 1000; } // Java层导出日志 public String exportLogs() { return serial_export_logs(); // JNI返回JSON字符串 }输出示例{ts:1712345678901234,type:read,len:8,data:01030400010002,err:0}。可直接导入Wireshark分析时序。我干这行八年踩过的坑比写的代码还多。现在我的习惯是新设备接入必先用libusb的lsusb -v看完整描述符确认CDC接口号和端点地址每次serial_open()后立即发心跳所有read()/write()调用都包裹try-catch并记录错误码。这些看似繁琐但省下的调试时间够你喝十杯咖啡。希望帮到你。本文还有配套的精品资源点击获取