首页 / 资讯中心 / 文章详情

安卓USB Host与STM32串口通讯实战:从权限到帧协议全解析

安卓USB Host与STM32串口通讯实战:从权限到帧协议全解析 ★ FEATURED ARTICLE
1. 为什么安卓设备要和 STM32 通讯一个真实的需求场景先说件我自己经历过的事。早几年做一款便携式设备调试工具现场工程师带着笔记本电脑到处跑每次连 STM32 板子都要开 Keil、插 J-Link、拉一根 USB 线到电脑。项目后期甲方突然提了个要求说现场人员手里只有安卓平板能不能不做界面培训、拿起平板就能读设备的运行参数和控制电机启停。这个需求听起来不复杂但真做起来才发现安卓设备和单片机之间的通讯链路没有想象中那么简单。当时的第一反应是走蓝牙或者 Wi-Fi结果一评估就放弃了现有板子根本没有无线模块重新改硬件、换 MCU、加协议栈开发周期和成本都承受不起。后来把注意力放到 USB 串口上因为大部分 STM32 板子天然带 USART而市面上成熟的 USB 转 TTL 方案一抓一大把硬件上几乎零改动只需要在安卓端做好 USB Host 模式下对串口设备的管理和读写。这篇文章就是把这个方案的完整落地过程整理出来从硬件链路选型、安卓端 USB 权限处理、串口数据读写封装到 STM32 端的配合和实际踩坑一次讲清楚。如果读者正准备做一个安卓 App 来和 STM32 通讯或者说手上正好有一块开发板想实现手机控制功能这篇文章可以作为起步阶段的参考路线。先说清楚一个概念。安卓设备通过 USB 和外部设备通讯核心机制是 USB Host 模式简单理解就是安卓设备当主机外面的 USB 设备当从机。手机和平板上的 Type-C 或者 Micro-USB 口在硬件上基本都支持 Host 模式但系统默认不一定开放而且不同厂商的兼容性差异很大。这个机制的底层是 Linux Kernel 的 USB Host 协议栈安卓在框架层封装成了 UsbManager、UsbDevice、UsbInterface 等 API 给上层调用但直接拿这些 API 操作串口设备会发现根本不够用因为你要处理的是 CDC ACM 类协议、批量传输端点、串口参数设置这些偏底层的细节。所以业界通常的做法是引入一个串口通讯库比较通用的有 usb-serial-for-android 和 maven 上的一些封装库本质是对 UsbDeviceConnection 的二次封装把 CDC 和 FTDI、CP210x、CH34x 这些常见 USB 转串口芯片的协议全部兼容掉。这样开发者就不需要关心某颗具体的 USB 芯片内部寄存器怎么配置只需要拿到一个串口对象设置波特率、数据位、停止位、校验位然后读写字节流即可。2. 从单片机到安卓你必须先理清的硬件链路2.1 USB 转串口芯片选型STM32 的 USART 引脚输出的是 TTL 电平信号安卓设备的 USB 口输出的是 USB 差分信号两者不能直接连中间必须有一颗 USB 转 TTL 的桥接芯片。市面上主流的芯片方案有 CP2102、CH340、FT232、PL2303 这几类从安卓兼容性的角度来说我个人的经验排序是 CP2102 最好CH340 次之FT232 也可以PL2303 兼容性最不可控。为什么 CP2102 在安卓上表现最好主要是因为它的 CDC ACM 类实现比较标准而安卓系统对 CDC ACM 的设备识别非常宽松很多设备不需要额外驱动就能被系统识别为标准串口类设备。CH340 在国内用的最多价格便宜、货源充足但它的厂商 VID 是 1A86PID 是 7523有一部分安卓 ROM 在枚举设备时对这颗芯片的处理有差异容易出现设备能识别但打不开的情况。FT232 是老牌厂商稳定性没问题文档也全就是价格偏高适合产品量产阶段而不是学习测试。引脚上就是四根线VCC、GND、TX、RX交叉连接——USB 转串口模块的 TX 接 STM32 的 RX模块的 RX 接 STM32 的 TX。实际项目中我不推荐用 VCC 引脚给 STM32 供电尤其是 STM32 带电机驱动、传感器阵列的时候电平不稳容易引发复位和通讯错乱最好独立供电只共地。2.2 设备枚举信息与判定逻辑安卓端拿到一个 USB 设备后第一步就是判断它是不是我们想要的串口设备。判断的依据是 VIDVendor ID和 PIDProduct ID。以 CH340 为例VID 是 0x1A86PID 是 0x7523CP2102 的 VID 是 0x10C4PID 是 0xEA60FT232 的 VID 是 0x0403PID 是 0x6001。判断逻辑不能只判断 VID/PID因为一个 Android 设备可能同时插了 U 盘、键盘、串口模块多个 USB 设备还需要进一步检查设备的接口类型和端点信息。串口设备通常有中断输入端点Interrupt IN和批量输入/输出端点Bulk IN/Bulk OUT拿到 USBInterface 之后遍历 Endpoint 类型确认是否满足一个中断 IN 一个批量 IN 一个批量 OUT的结构再把它当作串口设备处理。这里有一个很容易忽略的点同一个 USB 转串口芯片在系统里可能被枚举成两个接口一个用于数据通讯一个用于 CDC 控制。翻开 usb-serial-for-android 的源码可以看到它会枚举所有 interface然后选择协议匹配的那个。手动实现的时候要小心不要把控制接口当成数据接口来读。2.3 供电和地线问题串口通讯看似简单其实很多人在硬件连接上踩坑。最典型的问题就是模块插上后安卓设备没反应或者时好时坏。排查思路里面优先看供电。安卓设备的 USB 口确实能输出 5V 电压但是电流输出能力取决于主板设计有的平板才 500mA有的手机能到 1.5A 以上。USB 转 TTL 模块本身功耗不高但如果你顺便用这个 5V 给 STM32 的板载外设供电一旦外设电流超出 USB 口的能力总线电压就会被拉低结果就是设备反复枚举、通讯断断续续。所以我的建议是STM32 开发板用独立电源USB 口单独供电或者 DC 电源安卓设备的 USB 只负责数据通讯两边把 GND 连在一起就行。共地这个点很多人容易忽略。如果安卓设备和 STM32 板子各自用独立的电源电源之间没有物理连接那么 TX、RX 之间的电压参考点不一致通讯大概率是乱码或者白噪。把两边的 GND 用一根杜邦线连起来逻辑电平参考同一个 0V才能谈下一步的通讯。3. 安卓端 USB Host 模式权限是第一道坎3.1 清单文件与设备过滤声明安卓端开发的第一步是声明 USB Host 功能。在 AndroidManifest.xml 的 manifest 节点里加一行uses-feature android:nameandroid.hardware.usb.host /告诉系统和应用商店这个 App 需要 USB Host 能力。然后在 activity 节点下声明 USB 设备过滤规则这样可以实现插入指定 VID/PID 的设备时自动拉起 App的效果。activity android:name.UsbSerialActivity intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /activityres/xml/device_filter.xml 的内容?xml version1.0 encodingutf-8? resources usb-device vendor-id1027 product-id24592 / !-- 1027 是 0x0403 的十进制FT23224592 是 0x6001 的十进制 -- usb-device vendor-id6790 product-id29987 / !-- 6790 是 0x1A86 的十进制CH34029987 是 0x7523 的十进制 -- /resources注意usb-device标签里的 vendor-id 和 product-id 必须填十进制数不是十六进制。这个细节调试时最容易卡住顺手算一下0x1A86 116^3 1016^2 816 6 4096 2560 128 6 67900x7523 74096 5256 216 3 28672 1280 32 3 29987。3.2 动态申请权限usbManager.requestPermission设备插入后即使声明了 intent-filter 也不代表 App 自动拥有对这个 USB 设备的操作权限。Android 的安全机制要求 App 在读写设备之前必须获得用户显式授权。这个授权的 API 是UsbManager.requestPermission。UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice deviceList usbManager.getDeviceList(); UsbDevice device null; for (UsbDevice d : deviceList.values()) { if (d.getVendorId() 6790 || d.getVendorId() 1027) { device d; break; } } if (device ! null) { PendingIntent permissionIntent PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); usbManager.requestPermission(device, permissionIntent); }这里要动态注册一个 BroadcastReceiver接收ACTION_USB_PERMISSION广播在 onReceive 里判断授权结果。授权通过后才能调用usbManager.openDevice获得 UsbDeviceConnection。如果用户勾选了默认使用或者之前已经授权过requestPermission会直接回调授权成功。踩坑经验不要在 onResume 里反复调用 requestPermission否则用户还没点弹窗下一次 onResume 又触发一次导致弹窗闪烁甚至崩溃。正确做法是记录一个 Session 级别的权限状态只有确实没有权限时才发起申请申请后的结果统一在 BroadcastReceiver 处理。3.3 拔插监听与设备热插拔USB 设备是支持热插拔的安卓系统会发出两个系统广播USB_DEVICE_ATTACHED和USB_DEVICE_DETACHED。动态注册 BroadcastReceiver 监听这两个事件可以实现插入自动打开串口拔出自动关闭串口的体验。需要注意USB 串口设备拔出时如果此时还持有 UsbDeviceConnection 并且正在读写系统会抛异常或者直接断开底层连接。规范的流程是收到 DETACHED 广播后立刻关闭输入输出流、停止读写线程、关闭 UsbDeviceConnection然后更新 UI 状态。还有一点拔出后重新插入设备对象是全新的之前的 UsbDeviceConnection 实例已经完全失效必须重新枚举、重新 requestPermission、重新 openDevice。4. USBSerialLibrary 的原理与改造比想象中更需要代码级理解4.1 为什么不直接用系统 API自己用 UsbDeviceConnection 做底层读写不是不行但是有太多细节要处理。USB CDC ACM 的串口参数设置是通过控制传输control transfer往接口的 CDC 控制端点发 SET_LINE_CODING 请求数据格式是一个 7 字节的结构体4 字节波特率 1 字节停止位 1 字节校验位 1 字节数据位。不同芯片厂商对这个请求的细节处理不同CP210x 甚至不接受系统默认的直接设置需要用私有命令。与其重复造轮子不如用开源的 usb-serial-for-android。这个库我用了三年多整体设计思路清晰它把所有 USB 转串口芯片抽象成一个统一接口提供 open、close、read、write 方法底层封装了各芯片的协议差异。引入方式很简单在 build.gradle 里加一行依赖即可。4.2 必须手工处理的两个坑第一个坑是串口设备没有真正的打开成功回调。调用 open 方法后底层只是完成了 UsbDeviceConnection 的连接和串口参数配置但这时候你马上开一个线程去 read可能一上来就读到异常或者返回 0。最佳实践是先做一次空读或者直接 sleep 200ms 等待底层稳定再启动读写线程。第二个坑是 write 方法的同步机制。这个库的 write 内部是有同步锁的但如果你在 UI 线程直接调用 write一旦底层 USB 总线拥堵或者对端设备没 Readywrite 会阻塞几十毫秒甚至更久直接导致界面卡顿。所有串口读写必须放到子线程这个不是建议是强制要求。4.3 波特率异常和流控参数STM32 与安卓通讯波特率两边必须设置一致。常见的是 9600、115200实测 STM32 主频 72MHz 或 168MHz 时115200 的误差是可以接受的。但如果你要用 250000 这种非标准波特率一定要去芯片数据手册里查分频计算方式否则两边实际波特率误差超 2%通讯就会不稳定。打开串口时的参数设置UsbSerialPort port usbSerialDevice.getPorts().get(0); port.open(connection); port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);这一行代码背后做的事情很多向 USB 设备发出 CDC SET_LINE_CODING 请求把波特率、数据位、停止位、校验位封装成 7 字节数据通过控制传输发到控制端点。STM32 端如果配置的是 8 数据位、1 停止位、无校验两边就对上了。5. 串口数据在安卓线程模型中的生存法则5.1 读线程阻塞读与超时机制USB 串口的读操作是阻塞式的port.read(dst, timeout)会一直等到有数据或者超时才返回。所以必须用专用线程循环读取不能在主线程直接 read。private void startReadLoop() { readThread new Thread(() - { byte[] buf new byte[1024]; while (!isStopped) { int len port.read(buf, 200); if (len 0) { byte[] data new byte[len]; System.arraycopy(buf, 0, data, 0, len); onDataReceived(data); } } }); readThread.setDaemon(true); readThread.start(); }这里超时时间我通常设为 100-200ms不建议太长因为要考虑到关闭串口时线程能及时退出。如果你把 timeout 设为 0 表示无限阻塞那 close 操作就会一直卡住直到有数据进来这是很糟糕的体验。5.2 写数据命令帧的组包与排队向 STM32 发数据时建议按帧来组织。比如控制指令可以设计成帧头(0xAA 0x55) 功能码(1字节) 数据长度(1字节) 数据体(N字节) CRC16校验(2字节)写操作最好用一个独立发送队列把要发送的帧依次放入由发送线程逐个取出调用 port.write。好处是避免了多线程同时调用 write 导致的互斥和乱序也方便后期做重发机制。实际项目中我的发送线程结构是BlockingQueuebyte[] sendQueue new LinkedBlockingQueue(); // 调用的地方 sendQueue.offer(buildFrame(cmd, payload)); // 发送线程 while (!isStopped) { byte[] frame sendQueue.poll(200, TimeUnit.MILLISECONDS); if (frame ! null) { port.write(frame, 200); } }5.3 接收数据的分帧与粘包处理这是整个通讯方案中最容易出现逻辑错误的地方。串口是字节流不是消息流一次 read 可能读到半条帧也可能一次读到两条半帧。如果你直接按读到的字节数组就是完整一帧来处理大概率会得到错误结果。正确的处理方式是用一个 ByteArrayOutputStream 做字节累积然后按照帧格式做状态机解析搜索帧头读到帧头后继续读直到凑齐帧头 功能码 长度 数据 CRC完整长度校验 CRC通过则回调上层然后从缓冲区移除这一帧继续解析剩余字节。private void parseBuffer(byte[] data) { buffer.write(data, 0, data.length); byte[] all buffer.toByteArray(); int pos 0; while (pos 5 all.length) { // 帧头2 功能码1 长度1 CRC2 if (all[pos] (byte)0xAA all[pos 1] (byte)0x55) { int len all[pos 3] 0xFF; int totalLen 5 len; // 帧头2 功能1 长度1 数据len CRC2 if (pos totalLen all.length) { break; // 半包等待更多数据 } if (verifyCrc(all, pos, totalLen)) { onFrameReceived(Arrays.copyOfRange(all, pos, pos totalLen)); pos totalLen; } else { pos; } } else { pos; } } buffer.reset(); buffer.write(all, pos, all.length - pos); }这段代码基本就是串口分帧的标准范式读者可以直接抄进自己的工程里只要按自己定义的帧结构调整 totalLen 的计算公式就行。它解决了 90% 以 STM32 为下位机、以帧为单位通讯的场景需求。6. STM32 端的配合USART 中断接收与帧解析6.1 串口初始化与中断接收STM32 端的关键不在于能发数据而在于能完整地收到安卓发来的指令并解析。最常用的方案是 USART 中断接收。以 HAL 库为例初始化 USART1 为 115200、8N1然后启动中断接收。但这里有个细节HAL 库默认的HAL_UART_Receive_IT只接收单字节需要不断回调补发而且环形缓冲区的管理要自己写。我给出的建议是使用空闲中断IDLE配合 DMA 接收这是目前工业场景中比较稳妥的方式。原理是DMA 连续地把 USART 接收到的字节搬运到内存缓冲区当 USART 总线空闲一帧数据发送完毕之后总线电平保持高电平超过一个字节时间时触发空闲中断此时 DMA 已经收到了完整的一帧直接在中断里做数据解析。DMA IDLE 接收模板伪代码读者根据自己用的 STM32 型号调整// 启动一次 DMA 接收 HAL_UART_Receive_DMA(huart1, rxBuf, RX_BUF_SIZE); // 开启空闲中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 在 USART1_IRQHandler 中 void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t receivedLen RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); if (receivedLen 0) { parseAndExecute(rxBuf, receivedLen); // 重新启动 DMA 接收 HAL_UART_Receive_DMA(huart1, rxBuf, RX_BUF_SIZE); } } }使用空闲中断的好处是一帧数据来了你直接就能拿到完整的一帧不需要自己再在中断里做状态机去拼字节。但要注意这个方案要求安卓端发送数据时是连续一次性发完一帧不要在帧中间插入太长的停顿否则空闲中断会提前触发导致半包。6.2 命令解析与执行不要在主循环里做延时STM32 收到安卓发来的指令后解析动作和执行动作最好分离。解析在中断里完成只把解析出来的命令码和参数存入对应的全局变量然后置一个标志位。主循环检测到标志位后去执行对应的动作。这样做的目的是让中断处理尽量短避免长时间占用 CPU 导致丢帧。执行动作时千万不要在主循环里用HAL_Delay(1000)这种方式等待。想想看如果安卓端发来一条读取传感器数据的指令ST 主循环要等 1 秒才回数据安卓端设置的 read 超时是 200ms那通讯必然失败。如果执行的动作确实耗时应该改成非阻塞的状态机方式先回复一个收到指令正在执行的应答执行完后再补发结果。6.3 回传数据的协议格式统一STM32 回传给安卓的数据也按帧格式封装。我常用的简单协议帧头0xAA 0x55功能码1 字节数据长度1 字节数据N 字节校验CRC16 低字节在前、高字节在后这样安卓端和 STM32 端共用同一套组帧/解析逻辑双方联调时只要对着协议文档核对功能码和数据结构基本不会出现歧义。测试阶段建议先用手头的 USB 转 TTL 模块插电脑用串口助手分别模拟安卓端和 STM32 端先把协议打通再去接真机能够省下大量联调时间。7. 实测踩坑记录从 no permission 到乱码的全排查链路7.1 device not found明明插上了却枚举不到症状设备插上后usbManager.getDeviceList()返回列表为空或者列表里有但 VID/PID 不是预期值。排查链路先确认安卓设备的 USB 口支持 Host 模式。有个别手机比如部分只支持 Device 模式的设备硬件上根本没有 Host 能力这个无法通过软件解决只能换设备。确认 USB 转 TTL 模块本身是好的。把模块插到电脑上看电脑能否识别。换根 OTG 线。安卓这边绝大多数设备需要 OTG 线转换口普通的 Micro-USB 数据线是 Device 模式信号不能用于 Host。我遇到过好几次换个 OTG 线就好了的情况是接触不良或者线材质量差。拔掉其他占用 USB 口的设备比如充电线、U 盘。有的安卓设备同时只支持一个 Host 设备。7.2 open failed / permission denied权限机制与广播时序症状触发了requestPermission也弹窗了用户点了允许但紧接着 openDevice 就抛异常。这个坑我排查了很久。原因在于requestPermission的 PendingIntent 构造方式以及 BroadcastReceiver 的 onReceive 时序。如果你在 onReceive 里没有通过intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)判断授权结果而是直接调用 openDevice可能在授权广播还没到达的时候就开始打开操作底层设备还没绑定自然失败。规范流程是if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (device ! null) { openSerialPort(device); // 拿到授权后才打开 } } } }7.3 乱码问题第一反应不是换波特率而是查电平症状安卓端能打开串口STM32 也能收到数据但收到的全是乱码甚至完全错乱。排查链路先确认两边波特率一致115200 就是 115200不要一边 115200 一边 115200.0 这种精度差异基本没有影响。如果波特率一致仍乱码查 TX/RX 是否交叉接反。这是最常见的原因。用示波器或逻辑分析仪抓波形看电平标准是否一致。如果 STM32 是 3.3V 逻辑而 USB 转 TTL 模块是 5V 逻辑输出有些模块跳线帽可切换两者相连时虽然大多能工作但边沿不理想长线传输时就会出现偶发错位。把模块跳线切换到 3.3V 逻辑再试。检查共地。GND 没接好偶尔能通但抗干扰能力极差尤其在电机附近工作时一启动就乱码。如果以上都排除了用串口助手先把安卓端发出的原始字节抓出来确认组帧没有错位比如帧头字节在传输时被上层的编码转换吃掉了。安卓端如果用了 BufferedReader 按字符读会把二进制数据按 UTF-8 转换直接破坏帧结构。处理串口数据必须用字节数组流不能用字符流。7.4 通讯一段时间后卡死底层的僵尸连接问题症状刚开始通讯一切正常运行了十几分钟或者几十次操作后安卓端 read 返回超时write 也不成功App 看起来就像卡住了一样。这个问题的根源在于 USB 设备在通讯异常时会产生一个僵尸连接状态。主控端的 UsbDeviceConnection 还在但底层 USB 协议已经发生了错误比如对端 STM32 在某个瞬间停止了响应导致批量传输端点阻塞。普通的解决方法是重新调用 open 或者 close 再 open但也有概率失败因为这个僵尸状态发生在 USB 总线层面不是用户态能完全重置的。比较实用的做法是增加读写超时重试机制连续 3 次超时后主动关闭连接、释放设备、重新枚举并打开。如果重新打开仍然失败提示用户重新插拔 USB 线。实操中还有一个加快恢复的小技巧在关闭连接时先调用port.close()再调用usbConnection.close()最后调用usbManager.release()如果没有这个方法就调usbManager.close()或者直接置空引用让系统垃圾回收顺序不能反。反了的话底层资源没释放干净再次 open 时会出现 open 成功但 read/write 全部失败的情况。7.5 多款手机兼容性的差异名单按我这些年实测的情况整理一下兼容性梯队作参考机型群体USB Host 表现常见问题绝大多数国产旗舰机华为、小米、OPPO、vivo兼容性良好个别机型默认关闭 OTG需在设置里打开OTG 连接选项三星部分机型兼容性较好需要在通知栏确认 USB 用途选择传输文件以外的模式部分低端平板/工控机不稳定USB 供电不足容易断连老版本安卓6.0 以下的定制 ROM兼容性差权限申请频繁或无法识别 CDC 类设备做产品化项目时尽量在目标机型真机上提前做一轮兼容性测试把 OTG 开关、默认 USB 模式、锁屏休眠导致的 USB 挂起这几个因素都覆盖到。锁屏休眠导致的 USB 挂起是一个很隐蔽的问题——有些手机息屏一段时间后 USB 总线进入挂起状态串口数据不再上行这时候必须重新插拔或者唤醒屏幕才能恢复。针对这个场景可以把 App 设为前台服务或者用 WakeLock 保持 CPU 和 USB 总线唤醒状态代价是功耗会增加根据产品场景权衡。8. 从通讯打通到稳定运行更多工程化建议写到这里核心的链路已经完整了硬件接线 → 安卓端 USB 权限 → 串口读写 → 线程模型 → 帧协议 → STM32 配合 → 排错。最后说几个工程化方面的经验。首先是日志系统。整个通讯链路至少有五层数据流动App 业务层 → 串口封装层 → USB 协议层 → 硬件桥接层 → STM32 USART 层。任何一层出问题表现都是收发不正常。所以从开发第一天就要把每一层的收发日志打全尤其是App 发出的字节和STM32 收到的字节这两端的原始 hex 日志遇到问题直接对比能节省大量联调时间。其次是波特率的选择。理论允许范围内比如 115200工程上我推荐用 115200 而不是 9600。STM32 和 USB 转串口芯片都能轻松达到这个速率115200 在 1-2 米 USB 线长下抗干扰完全没问题。如果通讯内容主要是下行指令而不是高频数据流115200 的吞吐量足够守卫。高波特率如 460800 以上虽然能跑但线材和接触质量要求显著提高非必要不建议。再次是异常处理与重连机制。USB 通讯的稳定性不如网络通讯任何可靠传输的设计都尽量靠应用层来完成。比如下发关键指令后STM32 必须回一个 ACK安卓端收到 ACK 才算下发成功没有 ACK 则超时重发最多重发 3 次。这个机制虽然简单但能把绝大多数偶发性的 USB 丢帧问题消化在业务层。最后留一个进阶的切入点如果项目后期要用到更大的数据量比如把 STM32 采集的 ADC 波形实时传回安卓端绘图那么基于 USB 串口协议虽然有理论带宽瓶颈但实测在 115200 下每秒能传 11KB 左右足够支撑数千点每秒的简单波形。换句话说USB 转串口这套方案不仅适合控制指令也适合中低速率的数据采集场景架构上不用推倒重来。安卓设备与 STM32 的通讯入门难在环境搭建和概念打通但一旦把USB Host → 串口对象 → 字节流 → 帧协议这条链路在脑子里建立起来后续的开发无非是往这个框架里填充业务逻辑而已。顺手把这个链路用代码固定成公司内部的基础组件换项目、换单片机型号的时候安卓端几乎不用动这也是我反复重构这个方案最大的收获。
阅读完成 · 觉得有帮助?
咨询建站