ARTICLE DETAIL

资讯详情

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

Android车载USB开发实战:从Host枚举到串口/CAN/HID接入全攻略

Android车载USB开发实战:从Host枚举到串口/CAN/HID接入全攻略 做车载 USB 开发这行很多人一开始都以为难点在 Android 本身结果真机一插授权框不弹、设备枚举不到、数据读出来是乱码各种问题能把人逼疯。我陆陆续续在几个车机项目里折腾过 USB Host、USB 串口、USB-CAN 和 HID 设备踩的坑比写的代码都多。这篇笔记就是把我在 Android 车载场景下用 USB 的完整思路、踩坑记录和能直接抄的代码方案整理出来给同样在做车机、工控或者 OTG 外设接入的朋友做个参考。标题里这几个东西看着多其实核心就一条线搞清楚 USB Host 模式下Android 怎么枚举设备、怎么拿权限、怎么收发数据剩下的串口、CAN、HID 都是这条线上的具体应用而已。1. 写在前面为什么车载场景绕不开 USB1.1 车载 USB 开发到底在解决什么问题车机和手机有个本质区别手机上的 USB 口主要拿来充电和传文件但车机上的 USB 口是要接一堆外部设备的。调试口要接电脑、诊断仪要接 OBD 转接器、中控屏可能要接 USB 摄像头、方向盘按键模拟器、甚至你要在车机上跑自动化测试用 USB HID 设备去模拟触摸和按键。这些需求全部依赖 Android 的 USB Host 能力。USB Host 是什么简单说Android 设备当“主设备”外接的 U 盘、键盘、串口线、CAN 盒都是“从设备”主设备负责供电、枚举设备、发起通信。车机只要硬件支持 OTG 或者本身就是 Host 模式系统里就能拿到 UsbManager 这个服务然后通过它去操作所有外设。我从实际项目里感受到车载 USB 开发和手机 USB 开发最大的区别在于车机的外设需求更“硬核”不是传个文件那种量级的而是要实时读车辆总线数据、控制外部执行器、长时间稳定通信。这意味着你不能只调通一个 demo而是要处理掉线重连、数据粘包、权限持久化、时序抖动这些工程问题。1.2 这篇笔记能帮你解决什么问题这份笔记不光是代码堆砌我把每个环节的“为什么”也说明白。比如为什么有些设备枚举不到、为什么授权框有时不弹、为什么串口读出来的数据断断续续。这些问题的答案大部分不在 Android 上层代码里而在 USB 协议、内核驱动和硬件时序里。适合看这篇笔记的人有三类一是刚接手车机项目、对 USB Host 还比较陌生的 Android 开发二是做工业或车载外设接入需要快速把 USB 串口或 CAN 调通的嵌入式工程师三是想了解 Android 系统 USB 框架准备深入 framework 层的同学。如果你只是想在手机上插个 U 盘读文件这篇笔记可能偏重了但如果你是做设备接入的那这篇文章里的内容应该能帮你省掉至少两周的摸索时间。2. USB HostAndroid USB 能力的基座2.1 USB Host 和 OTG 的关系以及车机的特殊性很多人分不清 USB Host 和 USB OTG。一句话说明OTG 是硬件接口规范它允许一个设备既当 Host 又当 Device而 USB Host 是系统能力指的是 Android 系统能识别并管理外部 USB 设备。手机上的 OTG 口插 U 盘用的就是 Host 模式。车机上基本也是这个思路只是车机的主控方案五花八门有的用高通、有的用瑞萨、有的用全志或 RK这些 SoC 的 USB 控制器实现和内核驱动差异很大。我遇到过一台车机硬件上明明有 USB 口但系统设置里没有“USB 连接”之类的选项插上设备也没任何反应。查了半天问题出在 ROM 裁剪上厂商把android.hardware.usb.host这个 feature 声明删了或者内核里根本没编 USB Host 相关驱动。所以做车载 USB 开发的第一步不是写代码而是先确认两个前提第一设备支持 USB Host可以通过PackageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)检测第二内核里编入了对应外设的驱动比如 usb-storage、cdc_acm、ftdi_sio 等。2.2 声明权限和特征过滤器让系统把外设“交给你”应用要使用 USB Host必须在 AndroidManifest.xml 里声明两样东西USB Host 权限和特征过滤器。uses-feature android:nameandroid.hardware.usb.host android:requiredtrue / uses-permission android:nameandroid.permission.USB_PERMISSION /USB_PERMISSION这个权限是系统内部权限但你在 manifest 里声明了之后当应用要访问 USB 设备时系统会弹授权框让用户确认。如果没声明授权流程会不正常。特征过滤器的作用更关键当我们插上设备时Android 会根据device_filter.xml中声明的 vendorId 和 productId 来判断是否弹出“是否打开这个应用”的提示。这个文件放在res/xml/device_filter.xml下面格式是?xml version1.0 encodingutf-8? resources usb-device vendor-id1027 product-id24577 / /resourcesvendor-id 和 product-id 是 16 进制转 10 进制的数。比如 CH340 的 vendorId 是 0x1A86十进制就是 6790productId 是 0x7523十进制是 29987。你可以在设备插上后用adb shell lsusb查或者直接在代码里打印device.getVendorId()和device.getProductId()。2.3 运行时获取 UsbManager 和设备列表应用拿到 USB 设备有两种方式一种是主动扫描一种是监听插拔广播。扫描用这段代码UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice deviceList usbManager.getDeviceList(); for (UsbDevice device : deviceList.values()) { Log.d(TAG, Device: device.getDeviceName() , VID: device.getVendorId() , PID: device.getProductId() , Class: device.getDeviceClass()); }监听插拔则是注册ACTION_USB_DEVICE_ATTACHED和ACTION_USB_DEVICE_DETACHED两个系统广播。这里有个车载场景很常见的需求车机启动后自动连接上次使用的设备。如果你的应用是被授权框“被动唤起”的那么在onCreate里直接调用usbManager.getDeviceList()就能拿到设备如果应用是开机自启的就要先等设备插上再通过广播去获取设备对象。拿到设备对象后真正的数据收发要打开设备连接。UsbDeviceConnection 就是应用和内核 USB 驱动之间的桥梁后面所有串口、CAN、HID 的操作都建立在这个连接之上。所以我说 USB Host 是基座没有这个基座后面全是空中楼阁。3. USB 串口把 Android 变成超级终端3.1 常见的 USB 转串口芯片和驱动识别USB 转串口大概是车载项目里用得最多的外设了。调试车载娱乐主机、读 GPS 模块、接 arduino 或 STM32 的开发板、连 OBD 诊断线全是 USB 转串口。市面上常见的转接芯片就这么几个CH340/CH341国产便宜量大、CP2102/CP210xSilicon Labs、FT232FTDI贵但稳定、PL2303Prolific老古董兼容芯片很多。这些芯片在 Linux 内核里分别对应不同的驱动CH340 对应 ch341 驱动CP210x 对应 cp210xFT232 对应 ftdi_sioPL2303 对应 pl2303。内核有没有编入对应驱动直接决定设备能不能枚举成功。经常有人问我为什么我插上 CH340 的设备在 Android 里getDeviceList()扫不到大概率就是车机内核没编 ch341 驱动这种问题在应用层无解只能找厂商改内核或者换一个内核支持的芯片。下面这个表格是车载项目里最常用芯片的参数和注意事项芯片型号常见 VID:PID内核驱动常见问题CH3401A86:7523ch341内核未编译驱动时识别不到CP210210C4:EA60cp210x驱动一般内置好用FT2320403:6001ftdi_sio稳定但价格高PL2303067B:2303pl2303兼容芯片容易被驱动拒收3.2 用 UsbSerial 库封装底层细节Android 官方并没有提供现成的 USB 串口 API所以社区里很流行用开源的 usb-serial-for-android 库mik3y 的那个。这个库的原理并不神秘它把 UsbDeviceConnection 的 bulkTransfer 封装成了 SerialPort 接口同时把不同芯片的协议差异封装在各个驱动类里。用起来很简单implementation com.github.mik3y:usb-serial-for-android:3.4.6核心代码就这几步。先找到设备实例通过 UsbSerialProber 获取串口驱动UsbSerialProber prober UsbSerialProber.getDefaultProber(); UsbSerialPort port prober.findDevice(device).getPorts().get(0); if (!usbManager.hasPermission(device)) { usbManager.requestPermission(device, pendingIntent); } UsbDeviceConnection connection usbManager.openDevice(device); port.open(connection); port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);之前看到热词里有 PL2303 的驱动问题usb\vid_067bpid_2303这个 VID 就是 Prolific 的老批次的 PL2303 芯片在内核 3.8 之后的 Linux 里会被标记为“不支持的芯片”原因跟芯片厂家的认证机制有关。遇到这种情况如果系统里跑的是新版内核且驱动较新你可能会发现设备枚举很费劲或者打开串口后报错。最好的办法就是换芯片不要死磕。3.3 串口读写的关键代码和稳定性处理串口通信看起来就是读写两个操作但真正做车载数据采集时问题都出在细节上。写入用port.write(data, timeout)这个简单注意要处理好流控和时序。读取复杂一些因为串口本身是流式协议没有帧边界车载设备的数据往往是自定义协议的比如一帧固定 16 字节、以帧头 0xAA 0x55 开头、以校验和结尾。这时候需要自己处理粘包和半包。我习惯用一个独立的读线程 阻塞队列。读线程不断读数据写入一个ByteArrayOutputStream解析线程按协议切帧。这样即使某一次读到的数据跨越了两帧也不会造成错位。下面是我常用的读线程模式new Thread(() - { byte[] buffer new byte[4096]; while (!Thread.currentThread().isInterrupted()) { int len port.read(buffer, 1000); if (len 0) { onDataReceived(buffer, len); } } }).start();这里的onDataReceived回调里要把字节流拷贝一份因为 buffer 会被复用。我见过很多新手在这里栽跟头数据读出来一截是新的、一截是旧的排查半天才发现是 buffer 复用惹的祸。稳定性方面车载环境有个很典型的问题车辆启动时电压波动大USB 口可能会瞬间断电再上电导致连接断开。应用要想办法监听ACTION_USB_DEVICE_DETACHED在设备拔掉时清理资源在重新插上时自动重连。另外串口参数不匹配也是最常见的乱码原因尤其是波特率你设备端设 9600应用端设 115200读出来的当然全是乱码。项目里可以把波特率做成可配置项方便现场调试。4. USB-CAN直接读取车辆总线数据的正确姿势4.1 USB-CAN 适配器的选型和硬件连接如果说串口是嵌入式开发的基本功那 USB-CAN 就是车载开发的看家本领。车辆上几乎所有 ECU发动机、变速箱、ABS、车身控制模块都挂在 CAN 总线上想实时了解车辆状态就得从 CAN 总线上抓数据。USB-CAN 适配器的作用就是把车上的 CAN 总线信号转成 USB让 Android 车机能直接读取。市面上常见的 USB-CAN 适配器有几种周立功 USBCAN-I/II、CANable开源方案固件可以自己烧、PCAN 系列的如 PCAN-USB还有淘宝上大量基于 STM32 MCP2515/SJA1000 方案的自制盒子。选型时第一看协议第二看驱动。有些适配器内置了 USB 转串口比如 slcan 协议这种在 Android 里完全可以当串口用只是传输的是 CAN 帧的文本或二进制封装有些适配器需要厂商专有驱动这种要确认车机内核是否支持。CAN 总线物理上是差分信号接的时候要注意 CAN_H 和 CAN_L 不要接反OBD 接口的 6 脚是 CAN_H14 脚是 CAN_L。我第一次调试的时候贪图方便用杜邦线直接怼 OBD 座结果线序搞反一个下午都在查为什么收不到数据最后换了线序秒通。千万记得总线两端要接 120Ω 终端电阻尤其当你的适配器只是临时接入而不是挂在总线的两端时很多适配器内部已经带了终端电阻选项需要确认一下。4.2 CAN 帧结构解析和 Android 端的收发样例CAN 2.0 协议的帧格式有几个关键字段仲裁 ID标准帧 11 位、扩展帧 29 位、DLC数据长度0-8 字节、数据场、CRC 校验。如果是 CAN FD数据长度可以到 64 字节。拿到一帧数据后最重要的是区分标准帧和扩展帧、数据帧和远程帧这决定了你怎么解析 ID 和数据。以 CANable 这种开源适配器为例烧录 candleLight 固件后它通过 USB CDC 枚举成一个串口设备应用层可以用 slcan 协议和它通信。发指令很简单比如要打开 CAN 通道就发S0\r要发送一帧标准数据帧就发t1238 bytes hex\r收到的每一帧文本解析一下就能用。但是 slcan 协议是文本协议解析效率一般带宽高的时候容易丢帧。所以在车载数据量大的场景下我更推荐用带批量传输协议的适配器像 PCAN 的驱动就提供更高效的数据通道但这又要确认内核支持。如果适配器是 USB HID 类的或者自定义 bulk 传输类就得回到 UsbDeviceConnection 上用 bulkTransfer 收发。以常见的自定义协议为例CAN 帧在 USB 包里通常有一个帧头、通道号、帧信息包含 ID 类型、帧类型、DLC然后是 4 字节 ID 和 8 字节数据。收到 USB 包后按这个结构解析即可private CanFrame parseCanFrame(byte[] data) { CanFrame frame new CanFrame(); frame.channel data[1] 0xFF; frame.isExtended (data[2] 0x80) ! 0; frame.isRemote (data[2] 0x40) ! 0; frame.dlc data[2] 0x0F; if (frame.isExtended) { frame.id ((data[3] 0xFF) 24) | ((data[4] 0xFF) 16) | ((data[5] 0xFF) 8) | (data[6] 0xFF); } else { frame.id ((data[3] 0xFF) 8) | (data[4] 0xFF); } System.arraycopy(data, 7, frame.data, 0, frame.dlc); return frame; }4.3 高负载下的丢帧问题和整车联调的注意事项整车 CAN 总线负载率正常在 30% 以下但一些新车型的网关会有多路 CAN 总线数据量加起来并不小。500kbps 的 CAN 总线满负载每秒能传大约 4000 帧标准帧。Android 的串口库读数据默认用的是 Java 线程 内核缓冲区如果处理不及时用户态和内核态之间的缓冲区一旦满了新来的帧就会丢。想减少丢帧有两个方向可以走一个是关键路径用 native 层处理比如通过 JNI 直接操作 UsbDeviceConnection 的 native 句柄在一个紧密循环里做 bulkTransfer把数据塞进环形缓冲区解析也在 native 做另一个方向是适当调大 read 缓冲区和减少每次读之间的延迟。实测下来纯 Java 方案在 1000 帧/秒以内是稳定的超过这个量级最好上 native。整车联调的时候还有几个坑。首先是 CAN 总线要接终端电阻前面说了。其次是有些车的 CAN 总线是休眠的需要先给总线一点活动比如通过诊断仪发个唤醒帧否则你插上适配器收不到任何数据。第三是注意共地问题适配器的地和车身的 GND 必须连接好否则数据容易受干扰甚至损坏设备。5. HID 设备模拟键盘按键和音量控制的实战5.1 为什么车机上需要 HIDHost 和 Device 两种模式HIDHuman Interface Device协议你可能听得最多的就是键盘和鼠标但在车载场景里HID 的身影无处不在。最常见的需求就是用 USB 键盘或者自定义 HID 设备去控制车机比如方向盘上接一个自定义按键盒通过 HID 发送按键指令让车机切换歌曲、调节音量、接打电话。这里有个关键点要搞清楚车机是 USB Host外接的按键盒是 USB Device数据流从 Device 流向 Host。反过来还有一种场景是 Android 设备模拟成 HID 键盘插到电脑或者其他主机上模拟按键这就是 Android 作为 USB Device 的情况。在车机项目里两种模式都会遇到。前者主要用于车机读取外部输入后者则常见于自动化测试场景比如用 Android 板子模拟键盘去控制被测设备。这篇文章我重点讲作为 Host 读取 HID 设备的场景因为这是车机接入外部按键、触摸屏、扫码枪等设备的基础。5.2 用 UsbDeviceConnection 收发 HID 报告的完整流程HID 设备的通信单元是“报告”Report有输入报告设备发给主机、输出报告主机发给设备和特性报告双向用于配置。在 Android 上操作 HID 设备不需要额外的库直接用UsbDeviceConnection.controlTransfer就能完成大部分工作。HID class 的请求定义在 USB HID 规范里常见的请求有 GET_REPORT0x01、SET_REPORT0x09、GET_IDLE0x02、SET_IDLE0x0A等。读输入报告最简单的方式是 interrupt IN 端点也就是开一个线程在bulkTransfer或UsbRequest上阻塞读取。下面是一段通过 interrupt IN 端点读取 HID 报告的代码框架UsbInterface hidInterface findHidInterface(device); UsbEndpoint inEndpoint findEndpoint(hidInterface, UsbConstants.USB_DIR_IN); UsbDeviceConnection connection usbManager.openDevice(device); connection.claimInterface(hidInterface, true); new Thread(() - { byte[] buffer new byte[inEndpoint.getMaxPacketSize()]; while (running) { int len connection.bulkTransfer(inEndpoint, buffer, buffer.length, 1000); if (len 0) { parseHidReport(buffer, len); } } }).start();如果是输出报告比如你想给 HID 设备发送 LED 状态信息一般用 interrupt OUT 端点或者控制传输。很多自定义 HID 设备把配置写在特性报告里这时就要用 controlTransfer 发送 SET_REPORT 请求。协议细节我在后面的 HID Report Descriptor 部分展开。5.3 HID 键盘模拟音量修改和普通按键怎么发热词里有“hid 键盘发送音量修改和普通按键”这个需求其实很典型用 USB HID 键盘设备给车机发指令实现音量加减和普通按键。HID 协议里有两种报告数据格式。普通按键走的是“键盘报告”固定 8 字节第 0 字节是修饰键Ctrl、Shift、Alt、GUI第 1 字节保留第 2-7 字节是普通按键的键码。键码是 HID Usage ID比如键盘上的 A 键是 0x04B 键是 0x05数字 1 是 0x1E回车是 0x28。要模拟按一次 A 键就发送两帧报告先发[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]表示按下再发全零帧表示释放。音量键是 Consumer Page 的用法这类需求要用 Consumer Control 报告Usage Page 0x0C。音量增的 Usage ID 是 0xE9音量减是 0xEA静音是 0xE2。这个报告不在键盘的 8 字节报告里一般走单独的 HID Report ID。实际模拟的时候用控制传输或者 interrupt OUT 发送// 0x02 是 Report ID0xE9 是 Volume Increment byte[] volumeUp new byte[] {0x02, (byte)0xE9, 0x00}; sendHidReport(connection, outEndpoint, volumeUp); // 释放 byte[] volumeUpRelease new byte[] {0x02, 0x00, 0x00}; sendHidReport(connection, outEndpoint, volumeUpRelease);上面的sendHidReport可以用 interrupt OUT 端点发送也可以用 controlTransfer 带USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE的请求类型去发。很多自制的 USB 按键盒本质上就是一个带 HID 功能的 MCUAndroid 端只需要按照它定义好的报告格式发送命令就行。5.4 HID Report Descriptor读懂外设能力的“说明书”HID 设备的能力全部描述在 HID Report Descriptor 里这是一段二进制描述数据定义了设备有几个报告、每个报告的字节含义、每个字段的用途。你在 Android 里可以用controlTransfer发送 GET_DESCRIPTOR 请求来读取它但更快的办法是在 Linux 下用adb shell cat /sys/kernel/debug/usb/devices或者用 Wireshark 抓枚举过程。读一段典型的键盘 Report Descriptor开头会是 0x05 0x01Usage Page Generic Desktop然后是 0x09 0x06Usage Keyboard接着 0xA1 0x01Collection Application。对于 Consumer 按键Descriptor 里会出现 0x05 0x0CUsage Page Consumer0x09 0xE9Usage Volume Increment之类的定义。理解 Report Descriptor 的意义在于当你接上一个不认识的 HID 设备时先读它的 Descriptor才能知道报告里的每一位代表什么。不然你发了半天数据设备没反应结果发现设备根本没定义这个按钮或者按钮对应的 Usage ID 和你以为的不一样。车载项目里外设五花八门这种“先读描述符再干活”的习惯能帮你少走很多弯路。6. 系统 APIUsbManager 之外的那些细节6.1 USB 权限的两种获取方式和 Android 版本差异USB 权限是 Android 设备访问外设的第一道门槛也是最容易出问题的地方。系统提供两种授权方式一种是动态请求通过usbManager.requestPermission(device, pendingIntent)弹窗让用户确认另一种是应用声明了对应设备的过滤规则设备插入时系统自动把设备“交给”应用此时不一定弹窗但应用要处理Intent.ACTION_USB_DEVICE_ATTACHED带过来的设备对象。动态请求的代码模式很固定PendingIntent permissionIntent PendingIntent.getBroadcast( this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE); usbManager.requestPermission(device, permissionIntent);然后在广播接收器里处理授权结果。这里有个要注意的坑PendingIntent.FLAG_IMMUTABLE是 Android 12API 31之后强制要求的如果 targetSdk 升到 31 或更高不写这个 flag 会直接崩溃。另一个坑是授权本身不持久化车机重启后之前授权过的设备还会重新弹窗。如果你想做到开机自启后无感连接需要自己在应用里保存已授权设备的标识VID/PID/序列号重启后直接调openDevice但要注意这并不代表系统一定允许你访问——在没有弹窗授权的情况下应用只能打开自己曾经申请过授权的设备这是系统权限模型决定的。6.2 USB 插拔监听和设备变化处理车载场景中设备热插拔是家常便饭。Android 提供了两个相关的系统广播ACTION_USB_DEVICE_ATTACHED设备插入和ACTION_USB_DEVICE_DETACHED设备拔出。这两个广播既可以静态注册在 manifest 里也可以动态注册。静态注册的好处是应用被杀死后设备插入时还能唤醒应用坏处是静态注册时必须把你要处理的设备写进 device_filter.xml否则不会收到广播。设备拔掉之后有个非常典型的问题连接的句柄会变得无效如果你没及时清理再调bulkTransfer可能返回负值或者抛异常。所以ACTION_USB_DEVICE_DETACHED里一定要做完整的资源释放关闭UsbDeviceConnection、释放UsbInterface的 claim、关掉读线程。不然接下来再插入同一个设备有可能出现“设备在但打不开”的诡异状态甚至要重启车机才能恢复。6.3 系统层 API 背后是怎么工作的很多人用 UsbManager 用得挺溜但不知道它底层是怎么转的。Android 的 USB Host 栈大致分三层Java 层是UsbManager、UsbDevice、UsbDeviceConnection这些 APIJNI 层通过libusbhost和 Linux 内核的 usbfs 打交道内核层则是各 USB 设备驱动在跑。具体点说UsbDeviceConnection.open()的核心是打开/dev/bus/usb/xxx/yyy这个设备节点后续的controlTransfer和bulkTransfer本质上是 ioctl 调用USBDEVFS_CONTROL和USBDEVFS_BULK。这个底层模型解释了前面遇到的很多问题。比如设备的设备节点权限不对应用就没有权限打开它再比如内核没有编入对应驱动getDeviceList就根本看不到设备。做车载 USB 开发遇到问题不能只盯着上层代码要能顺着这个链路往下排查设备插上后adb shell lsusb能不能看到设备节点存不存在设备节点属主和权限对不对搞懂这层排查问题的思路就清晰多了。7. 实测中的常见问题与排查实录7.1 授权框不弹设备也扫不到这个问题十有八九出在内核驱动或设备权限上。先别急着改代码按下面的顺序排查第一步用adb shell lsusb看设备是否被内核识别第二步看adb shell getprop sys.usb.config之类属性有没有异常第三步检查/dev/bus/usb/下有没有对应设备节点以及节点权限有没有被限制。如果lsusb都看不到设备那就是驱动或硬件问题应用层怎么改都没用。如果lsusb能看到设备但应用扫不到多半是应用没有声明FEATURE_USB_HOST或者device_filter.xml的 VID/PID 不对。注意usbManager.getDeviceList()返回的是全部 USB 设备跟 filter 无关但要把设备“自动交给”应用处理filter 就必须匹配。所以我建议调试阶段先用getDeviceList()打日志确认设备在不在列表里再去处理 filter 的问题。7.2 串口数据乱码和黏包乱码先查波特率。我之前做过一个项目设备端默认的波特率被之前的调试人员改成了 38400但代码里写死 115200读出来的数据毫无规律。后来把波特率做成可配置项才彻底解决了这种因为设备参数变动导致的问题。另外检查数据位、停止位和校验位标准配置是 8 数据位、1 停止位、无校验8N1但有些设备可能用 7E1、8O1 这种冷门配置务必看设备手册确认。黏包问题是流式协议的经典难题常见于连续采集场景。我的经验是设备端尽量固定帧长并且带帧头和校验应用端在解析时不要频繁做数组拷贝用一个“累积缓冲 帧解析”的状态机。同时还有一个容易忽略的点一次read可能只读到一帧的一半也可能一次读到三帧解析代码一定要能正确处理“半包”、“整包”和“多包”的情况。7.3 USB-CAN 收不到数据CAN 收不到数据先不要怀疑代码。第一步查物理层CAN_H 和 CAN_L 是否接反、终端电阻是否接上、总线是否被休眠。第二步查适配器模式有些适配器默认是静默模式只听不发送 ACK或者通道配置错误。第三步查协议如果用的是 slcan 协议确认波特率设置指令有没有发对。我之前碰到过一次诡异的问题明明波特率设置成了 500kbps但就是收不到数据后来发现适配器出厂默认波特率是 250kbps需要先发S8\r切到 500k 才能通。最后查应用层确认打开的是不是正确的读端点读缓冲区是否够大。CAN 总线的数据速率相对较高如果应用层处理不过来丢帧是必然的。这里给个判断依据如果lsusb能看到设备、Adaptor 指示灯正常闪烁、但应用收不到数据八成是协议或配置问题如果连指示灯都不闪那就是物理层问题。7.4 设备拔插后应用“失联”这个问题在车载场景非常常见。原因通常是应用在ACTION_USB_DEVICE_DETACHED时没有释放资源或者系统没有及时回调广播。系统回调DETACHED广播有延迟设备重新插入时应用可能还在用旧的连接导致 open 新连接失败。我的处理方式是在应用里维护一个全局的“设备连接代理”屏蔽掉底层的插拔细节。插入时自动打开连接拔出时自动断开并在重连成功前阻塞所有上层调用。代理内部监听ATTACHED/DETACHED广播同时做去重和防抖。这样做的好处是上层业务代码完全不用关心设备什么时候被拔掉了反正连接代理会尽力维持一个“看起来一直在线”的连接状态。7.5 权限持久化问题系统授权不持久化这是 Android 的限制但有变通办法。如果你的车机有 root 权限可以用pm grant把 USB 权限授予给应用或者在系统设置里把应用加入“设备控制”白名单不同厂商差异比较大。如果没有 root只能接受弹窗。不过有一种场景可以规避如果你的应用声明了ACTION_USB_DEVICE_ATTACHED过滤器并且系统已经把设备分配给了你的应用那么只要应用进程活着就可以直接访问设备不需要再次弹窗。所以“开机自启 过滤器声明 保持后台进程存活”是车载项目里最常见的组合拳。8. 给新人的建议和一些工程经验8.1 从最小 Demo 开始把链路拆碎验证新手做 USB 开发最容易上来就写一大堆代码结果问题一片不知道从哪里查。我的建议是每一个环节都先用最笨的办法验证。枚举不到设备就先写个最简 Activity只打印getDeviceList()串口乱码就先在电脑上用串口助手确认设备端上报的数据格式CAN 收不到数据就先拿一个 CAN 分析仪确认总线上到底有没有数据。链路要从前到后一层层打通而不是在最后一层瞎猜。先看一个最简单的 USB 读设备 Demo 需要哪几步获取 UsbManager获取设备列表请求权限openDeviceclaimInterface找端点bulkTransfer/controlTransfer关闭连接。把这 8 步拆开每步打日志日志打清楚了问题基本也定位了。8.2 把调试的主动权握在自己手里工具方面adb shell lsusb、adb shell dmesg | grep usb和adb shell cat /sys/kernel/debug/usb/devices这三个命令每一个做车载 USB 开发的都该刻在脑子里。dmesg能看到内核收到 USB 事件之后的应答sysfs里能看到设备描述符的完整解析结果。信息量比任何 Android 上层日志都大。再配合一个 USB 协议分析仪或者临时用 Wireshark 抓 USB 包任何协议问题都能在十分钟内定位。另外现场调试时串口工具一定要备好。我常用的是一个临时 Android 应用把 USB 串口收到的原始字节流直接打印成 hex 输出再配合时间戳。很多“莫名其妙”的问题在原始数据面前都会现出原形尤其是协议字段对不上的时候。8.3 车载环境特殊做好异常降级车机和手机最大的不同是它没有一个“用户”在旁边随时插拔重试。车机启动之后可能几个月不关机外设也可能随时因为震动松脱、因为电压波动重启。所以代码里一定要做自动重连、做异常降级、做数据缓存。比如 GPS 模块暂时掉线应用要能继续用惯性数据CAN 总线暂时断开要能缓存最近一段时间的车辆状态等总线恢复后补发或补算。这些工程化的东西比单纯把 USB 通信调通更重要。最后再说一个小技巧车载 USB 开发一定要把日志维度做细。USB 层的日志、协议层的日志、业务层的日志分开定义好 tag。经常现场调了一天最后发现是应用一开始就把日志级别打错了关键信息全被过滤掉。日志是车载现场唯一的“眼睛”不要省。做这行久了最大的体会就是Android 车载 USB 开发的知识点并不是难在某个协议上而是难在它横跨了 Android 系统框架、Linux 内核驱动、USB 协议和嵌入式硬件多个层面。任何一个环节的短板都会变成实际项目的坑。我写这份笔记其实就是想把自己踩过这些坑的位置标出来能让你少走一段弯路。
返回列表