ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android USB转RS-232串口通信实战:从OTG到UsbSerial

Android USB转RS-232串口通信实战:从OTG到UsbSerial 1. 项目概述为什么要在Android上做RS-232串口通信RS-232串口适配器这块东西说新不新说旧不旧。工业设备、仪器仪表、PLC、老式的医疗设备、称重模块甚至很多实验室里的测量仪器到今天依然大量使用RS-232作为标准通信接口。问题在于现在手头能拿出来干活的设备基本都是手机和平板这些设备上没有DB9接口也没有原生串口驱动所以要做一台临时上位机就必须用一个适配器把RS-232协议搬到Android设备上。这个项目的核心就是解决Android设备怎么可靠地和RS-232设备通信这个问题。常见的做法是走USB Host通道用OTG线接一个USB转串口模块再靠Android的UsbManager API和对应的串口驱动库完成数据读写。如果你要做的只是偶尔连一次设备、读几个寄存器值或者发几条AT指令那买条线直接连就行但如果你要做一款配套App需要每次插上设备自动识别、自动打开串口、自动配置波特率那就必须把整个底层流程理顺包括设备权限、串口参数、数据流控、异常断开重连这些环节。这套方案适合几类人做工业上位机开发的Android工程师、搞嵌入式又要做配套工具的全栈开发者、以及需要临时用手机调试硬件设备的测试人员。它可以帮你省掉一台笔记本电脑直接用手机或者平板完成串口调试、数据采集、固件参数配置这类工作。我自己做过不下十个类似的项目从接一个串口扫码枪到对接一整条自动化产线的PLC采集过程中踩了不少坑这篇文章把整个链路从头到尾捋一遍也包括一些文档里不会写的细节。2. 硬件准备与关键技术细节2.1 RS-232电平与TTL电平的差异很多第一次做串口接入的人都会栽在这上面。RS-232标准里的逻辑电平跟芯片上常见的UART TTL电平完全是两码事。RS-232规定逻辑1是-3V到-15V逻辑0是3V到15V也就是说它用的是正负电压来表示高低电平。而Android设备USB接口、单片机IO口这些地方出来的UART信号逻辑1是3.3V或5V逻辑0是0V。直接拿一根TTL电平的USB转串口线去接一个符合RS-232标准的设备运气好一点什么都不通运气差一点直接把设备IO口打坏。所以你需要的是带电平转换的适配器常见芯片是MAX232或者MAX3232前者需要5V供电后者3.3V就能跑很适合在USB转串口模块上使用。市面上卖的RS-232串口线里面基本都集成了USB转UART和UART转RS-232两级转换你不需要自己搭电路但要知道这其中的电平差异否则排查问题的时候方向会搞错。我自己习惯的做法是拿到任何一条线先用万用表量一下DB9母头的2脚和3脚在空闲状态下的电压。正常的RS-232空闲状态是负电压一般在-5V到-12V之间。如果量出来是0V或者正电压那说明这条线很可能就是纯TTL的USB转串口线接到真RS-232设备上必出问题。2.2 适配器芯片选型与Android内核驱动不管买哪种USB转串口模块核心是它上面的桥接芯片。市面主流的芯片有这么几类芯片型号常见厂商Android内核支持情况稳定性备注FT232R / FTDI系列FTDI支持ftdi_sio驱动很好价格偏高兼容性最好的选择CP2102 / CP210xSilicon Labs支持cp210x驱动很好工业设备里很常见CH340南京沁恒部分内核支持ch341驱动一般便宜但部分国产Android设备内核未编译该驱动PL2303Prolific支持pl2303驱动一般假货极多假芯片会导致驱动不识别核心逻辑是Android手机和平板的内核里到底编了哪些USB串口驱动决定了你这根线插上去能不能被识别。大部分主流Android设备的kernel里默认启用了ftdi_sio、cp210x和pl2303CH340的支持则看厂商通常国产设备会带部分原生机型就不一定了。一个比较实用的判断方法在App里通过UsbManager枚举设备列表看一下你的USB转串口模块有没有出现在列表里。如果出现了说明内核已经识别到了USB设备只是上层还没驱动如果完全没有说明硬件层面就没枚举成功先换线或者换OTG口再排查。这件事可以做个一分钟的快速测试避免在软件上白白折腾很久。2.3 三线连接与交叉线陷阱RS-232全功能连接是9根针都用上但在绝大多数Android项目场景下三线制就够了TXD发送、RXD接收、GND地。这里有个特别容易搞反的坑——交叉连接。两个串口设备之间通信时A设备的TXD要接B设备的RXDA设备的RXD要接B设备的TXD而不是同名直连。很多设备上DB9接口的2脚是RXD3脚是TXD但这不是绝对标准不同厂商的设备可能采用不同的定义。比较保险的办法是查对应设备的手册或者直接用万用表测在设备上电后发送数据的那一脚在空闲状态会维持在负电压而接收脚通常没有固定电平。我遇到过一次设备手册没写清楚针脚定义最后就是靠用逻辑分析仪抓了两个针脚的波形才确认的。还有一种情况是适配器本身已经是交叉线了。有一些标着串口延长线的是直通线有一些标着交叉线的是两边收发对调过的。如果插上去之后数据发出去但收不到优先检查一下是不是交叉/直通选错了。2.4 流控信号与工业设备交互注意事项很多RS-232设备会开启硬件流控也就是用到RTS和CTS这两个信号。但USB转串口模块上的RTS/CTS线到了Android层面之后控制逻辑往往被绕过了。也就是说即使底层芯片支持流控Android上通用的UsbSerial库也可能不会完整处理这些信号。讲一个实际例子。我之前接一台工业打印贴标机它默认开启了RTS/CTS流控我用普通三线直连的方式去发大包数据每次发几条数据之后就会卡死。后来看了一下这台设备的手册发现它在每收到一个数据包之后会拉高CTS信号等待上位机收到确认后再继续发送。解决办法是对设备端做了测试先尝试把设备侧流控关闭如果关不了就在软件层做成发一条、等响应、再发下一条的同步模式不要在一条串口上直接做长包突发传输。提一句如果设备支持尽量关闭硬件流控。RS-232在短距离、低波特率的情况下不需要硬件流控也能跑得很稳定而去掉流控可以少一大半连接和软件上的麻烦。3. Android端环境搭建与开发实现3.1 USB Host模式与OTG硬件配置Android设备要读USB串口走的是USB Host模式。这个模式要求设备本身就支持OTG或者USB Host插上OTG线之后Android系统会枚举USB总线上的设备。老一些的手机如果USB口本身不支持Host模式插上外部USB设备是没有任何反应的这一点在选测试机的时候要留意。OTG线本身也有讲究。市面上很多OTG线供电能力不足接一个CH340模块还行但如果你接的是那种带隔离的RS-232模块或者某些耗电较大的工业USB设备低质量的OTG线会导致电压跌落USB设备枚举不稳定。建议使用短一点的OTG线线越短损耗越小同时优先选支持快充协议、线材粗一点的那种。如果设备必须同时充电和通信就得用带辅助供电的OTG HubUSB Hub外接一个5V电源给设备供电Android端只走数据。Android应用侧16.0和更高版本系统对USB Host的权限管理没有本质变化但从Android 12开始PendingIntent必须显式指定FLAG_IMMUTABLE否则申请USB权限的时候会直接抛异常。这里展开说一下USB权限的申请流程是App注册一个广播接收器然后通过USBManager.requestPermission去弹系统级Dialog。在Android 12上如果不加FLAG_IMMUTABLE你收到广播的时候会发现自己接收不到权限结果这个问题很多人排查半天才发现是PendingIntent这个Flag的锅。3.2 UsbManager设备枚举与权限申请在Android上写串口通信第一步是拿到UsbManager实例val usbManager getSystemService(Context.USB_SERVICE) as UsbManager然后枚举所有已连接的USB设备val deviceList: HashMapString, UsbDevice usbManager.deviceList for ((_, device) in deviceList) { Log.d(UsbDevice, deviceId${device.deviceId}, vendorId${device.vendorId}, productId${device.productId}) }如果你的USB转串口模块被正常识别这里会输出它的vendorId和productId。拿这些ID去对一下芯片型号比如FT232的vendorId是0x0403CP2102的vendorId是0x10C4CH340的vendorId是0x1A86可以快速确认系统识别的对不对。要发起权限申请你需要准备一个PendingIntentprivate fun requestUsbPermission(device: UsbDevice) { val permissionIntent PendingIntent.getBroadcast( this, 0, Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE ) usbManager.requestPermission(device, permissionIntent) }注意Android 12之后必须加FLAG_IMMUTABLE。注册广播接收器的时候建议在onResume里动态注册、onPause里注销别在Manifest里静态注册因为USB插拔事件频繁触发静态注册容易造成资源泄漏。一个常见的坑是Nexus/Pixel等原生系统的USB设备接入弹窗和App内requestPermission弹窗会同时出现用户如果先在系统弹窗里点了取消App自己再requestPermission就会没有反应。处理方式是监听ACTION_USB_DEVICE_DETACHED和ACTION_USB_DEVICE_ATTACHED在设备重新插拔后再重新申请权限而不是只在进入页面时申请一次。3.3 基于UsbSerial的读写实现设备权限拿到之后接下来要建立真正的串口通信。这里推荐直接用开源的UsbSerial库GitHub上felHR85/UsbSerial是比较常用的一个它封装了底层的controlTransfer和bulkTransfer省去很多麻烦。基本流程是val connection usbManager.openDevice(device) val interface device.getInterface(0) connection.claimInterface(interface, true) val serialDevice UsbSerialDevice.createUsbSerialDevice(device, connection) serialDevice.open() serialDevice.setBaudRate(115200) serialDevice.setDataBits(UsbSerialInterface.DATA_BITS_8) serialDevice.setStopBits(UsbSerialInterface.STOP_BITS_1) serialDevice.setParity(UsbSerialInterface.PARITY_NONE)打开之后读取数据有两种方式一种是调用setReadCallback在回调里异步接收数据另一种是直接调read方法同步读取。第一种方式更适合平时做上位机因为串口数据到达时机不确定用回调模式不用专门起接收线程。serialDevice.setReadCallback { data - val received String(data) runOnUiThread { textView.append(received) } } serialDevice.read()写入数据更直接val bytes command.toByteArray(Charsets.ISO_8859_1) serialDevice.write(bytes, 2000)写入超时建议设成2000ms不要设成无限等待因为如果设备端没有正常接收底层bulkTransfer会一直阻塞线程导致UI卡死。底层实现上UsbSerial库内部调用的是usbConnection.bulkTransfer(endpoint, buffer, length, timeout)。USB的bulk传输适合大数据量交互最大包大小通常取决于端点描述符常见是512字节这和RS-232一次传几个字节到几十个字节的小包处理方式不同所以如果你一次要发几十KB的数据最好拆包每个包控制在几千字节否则容易触发双方缓冲区的溢出出现丢包。3.4 串口参数设置的选型理由串口参数这块最常见的组合是115200-8-N-1也就是波特率115200、8位数据位、无校验、1位停止位。但实际对接设备时参数必须严格按设备手册来尤其是校验位这块最容易出问题。有些老设备用偶校验或者奇校验如果你设成无校验数据就会随机出现错位或乱码。有一个容易混淆的点是即使设置了对应的校验位通信仍可能因为是数据位配置不匹配而失败常见的老设备是7位数据位加偶校验加2位停止位比如连接一些电子秤、老式仪器会碰到这种配置。波特率这里要特别注意一点USB转串口芯片上设的波特率和下位机设备上设的波特率必须完全一致。有些设备支持自动波特率检测比如扫码枪默认支持识别不同波特率但大多数工业设备是固定波特率的不匹配的话收到的全是乱码。设置波特率时UsbSerial库的setBaudRate方法底层会计算分频因子不同芯片对非标准波特率的支持不同如果你用到250000、500000这种非标准波特率建议先查一下芯片手册确认支持范围。4. 实操过程记录从接线到收发一条完整数据4.1 硬件联调准备这里我以最常见的场景为例Android手机通过OTG接一条USB转RS-232线连接一个波特率9600的工业称重仪表读取实时重量数据。硬件清单支持OTG的Android手机一台Android 11及以上系统比较省心OTG转接头一个USB转RS-232串口线一条带MAX3232电平转换的型号被测试设备一台工业仪表RS-232接口接线方式DB9接口侧只接三根线。手机侧的串口线我用的是一条成品线DB9公头直接插到仪表上如果仪表上是DB9母头就直接插。GND必须接TXD和RXD按交叉方式接好。接好之后先别急着写代码先在手机上装一个串口调试助手类App用USB模式打开串口手动设置波特率9600、数据位8、停止位1、校验位N。如果能看到仪表上电后主动上报的数据帧说明硬件链路已经通了。仪表串口一般分为主动上报和指令应答两种模式我的这台仪表是主动上报每秒发一帧每帧大概是20个字节。4.2 Android代码侧的关键环节在Android Studio里新建一个项目引入UsbSerial库的依赖implementation com.github.felHR85:UsbSerial:6.1.0先做一个简单的页面包含设备连接状态、接收数据显示区和发送按钮。核心逻辑按这个顺序来写第一步声明权限相关变量private var serialDevice: UsbSerialDevice? null private var connection: UsbDeviceConnection? null第二步在页面加载完成后开始枚举并申请权限override fun onResume() { super.onResume() registerReceiver(usbReceiver, IntentFilter().apply { addAction(ACTION_USB_PERMISSION) addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) }) findSerialDevice() } private fun findSerialDevice() { val devices usbManager.deviceList.values devices.forEach { device - if (device.vendorId 0x0403 || device.vendorId 0x10C4 || device.vendorId 0x1A86) { requestUsbPermission(device) } } }第三步权限授权成功后打开串口并设置参数override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) val device intent?.getParcelableExtra(UsbManager.EXTRA_DEVICE) if (device ! null) { connectDevice(device) } } private fun connectDevice(device: UsbDevice) { connection usbManager.openDevice(device) serialDevice UsbSerialDevice.createUsbSerialDevice(device, connection!!) serialDevice?.open() serialDevice?.setBaudRate(9600) serialDevice?.setDataBits(UsbSerialInterface.DATA_BITS_8) serialDevice?.setStopBits(UsbSerialInterface.STOP_BITS_1) serialDevice?.setParity(UsbSerialInterface.PARITY_NONE) serialDevice?.setReadCallback { data - val hex data.toHexString() runOnUiThread { Log.d(SerialData, hex) } } serialDevice?.read() }这里有一个需要注意的点UsbSerialDevice.createUsbSerialDevice在创建实例时会自动根据内核驱动类型选择对应的驱动实现。如果你的设备没有被内核标记为串口设备这一步会返回null。如果你用的是FTDI芯片但内核没编ftdi_sio驱动库会找不到对应的设备类型直接崩或返回null。一个处理方式是加个兜底逻辑如果createUsbSerialDevice拿到null提示用户当前设备内核不支持该USB转串口芯片。第四步发送数据时按设备协议组帧。我的仪表协议是查询指令以0xAA开头以0x55结尾中间是命令字和校验位fun sendWeightCommand() { val command byteArrayOf(0xAA.toByte(), 0x01, 0x03.toByte(), 0x00, 0x55.toByte()) serialDevice?.write(command, 2000) }4.3 实测场景与抓包结果我在实际测试中用一根USB转RS-232线连接仪表手机端一打开App很快就识别到了FT232芯片权限弹窗确认后不到一秒就收到了仪表主动上报的数据帧。这里分享一下我在调试过程中做的一个小技巧把接收到的原始数据同时打印成Hex格式和ASCII格式。很多时候串口二进制协议里夹杂着不可见字符只看ASCII会漏掉关键信息。下面是我调试时打印出来的部分数据55 AA 01 64 00 00 00 05 4B A2 0D 0A对照仪表协议文档55 AA是帧头01 是仪表地址64是重量数据高字节和低字节的组合最后两位是校验和和帧尾。数据解析出来之后重量值就是0x0000054B换算成十进制是1355再根据仪表量程换算成实际重量单位。这中间其实遇到一个问题最初我把波特率设成了115200仪表上报的数据全是乱码。排查时先看Hex发现数据帧头都不对然后意识到仪表固定波特率9600改过来之后数据马上就正常了。乱码问题如果在接线正确的前提下出现十有八九就是波特率不对优先查这个参数。5. 常见问题与排查技巧实录5.1 串口调试问题速查表现象可能原因排查方式设备列表里找不到USB设备OTG线不支持Host模式、线材供电不足、设备不支持OTG换OTG线、换测试机、用USB Hub辅助供电找到设备但createUsbSerialDevice返回null内核缺少对应USB串口驱动换芯片型号再试FT232兼容性最好申请USB权限没弹窗或点了没反应PendingIntent缺少FLAG_IMMUTABLE系统弹窗和App弹窗冲突检查Android版本适配重新插拔后重试数据收到但全是乱码波特率不匹配、校验位不对、数据位不对、交叉线接反逐一核对设备参数打印Hex确认帧结构数据能发不能收TXD/RXD接反、设备没上电、对方没有主动上报交叉线改一下检查设备状态发大包数据卡死、掉线流控信号未处理、USB buffer溢出关闭流控改成小包同步发送等待响应后再发下一条插拔后App不能自动重连未注册ACTION_USB_DEVICE_DETACHED/ATTACHED广播在广播回调里重新初始化串口串口打开后read回调不触发设备未被正确打开、读取超时、串口被其他App占用检查串口是否被占用重新open确认读写线程活着5.2 独家避坑经验很多问题在文档里根本不会写这里讲几个我自己踩过之后总结出来的经验。一个是关于USB权限申请时机的问题。不要在App刚启动就立刻申请USB权限因为Android系统的USB枚举可能需要几百毫秒刚插上设备的一瞬间usbManager.deviceList可能是空的。我现在的做法是在页面里加一个延时300ms后重新枚举一次如果一次没找到就再等300ms再查最多查三次。这样能解决一大部分插上设备但App没识别到的偶发问题。另一个是串口关闭时的处理流程。很多人只调serialDevice.close()却忘了把connection的claimInterface释放掉。正确顺序是先调usbSerialDevice.close()再调usbManager.releaseInterface(usbInterface)最后connection.close()。不按这个顺序下次插拔设备时系统可能拒绝再次open。我遇到过最典型的情况是App崩溃后USB口被硬件锁定必须重启手机才能恢复这就是没有正确释放USB资源导致的。还有一个比较隐蔽的问题是Android 11以上的包可见性变化。如果你是做SDK给别人的App集成或者你的App要通过PackageManager查询其他App需要在AndroidManifest.xml里声明queries元素否则在部分系统上会查询不到设备相关的服务。这个问题在串口设备接入场景中不太常遇到但如果你同时接了多个USB设备需要区分还是要注意。5.3 关于USB供电不足的实战补充USB转RS-232模块本身耗电不大一般不到100mA但有些带隔离的工业级USB转串口模块因为板载了隔离电源芯片工作电流会到200mA甚至更高。如果你的手机OTG口输出电流能力不足会出现一插上就识别一打开就掉线的现象。排除方法是用USB电流计看工作电流如果模块工作电流超过了手机OTG口的限制就得用带外部供电的USB Hub。这个Hub供电有个坑很多普通USB Hub在没有数据通信时会休眠导致串口直接断开。挑选的时候注意看一下有没有常供电模式或者选择工业级的USB Hub它们一般不会自动休眠。6. 最后再分享一个实用小技巧如果你的Android设备需要频繁切换连接不同波特率的串口设备可以在App里做一个最近使用参数的记忆功能把每次成功连接时的波特率、数据位、校验位、停止位保存到SharedPreferences里下次检测到同样的vendorId和productId时自动套用上次的参数。这个功能听着简单但在实际使用时能省很多事——尤其是当你同时维护多台不同波特率的设备时经常确认上一次连的是9600还是19200会非常烦人。另外如果条件允许建议在开发阶段准备一个USB转串口的回环测试头就是把TXD和RXD短接的那种。插上它之后你发送的任何数据都会原样返回可以快速验证Android端读写链路是否正常排除设备侧问题。这个测试头成本不到几块钱却能节省大量排查问题的时间。串口通信这件事本身不复杂但它夹在USB协议栈、Android系统权限管理、硬件驱动和外部设备协议之间任何一个环节出问题都会表现成数据不对或连不上。把这套链路拆开、逐层验证是最有效的调试方式。如果这篇文章能帮你少踩几个坑我觉得就很值了。欢迎有类似项目经验的朋友在评论区聊聊你们遇到过的奇葩问题我看到了会回复。
返回列表