资讯动态

车载Android USB Host开发实战:USB串口、USB-CAN与HID外设接入全解析

发布时间:2026/9/12 12:17:01 来源:尧图企业网站定制
搞过车载 Android 开发的人应该都有同感车机这个场景里USB 几乎是所有外设的汇聚点。不管是你想接一个 USB-CAN 诊断工具去读整车报文还是拿 USB 转串口线连 MCU 看日志或者让方向盘物理按键通过 HID 协议跟中控交互背后都得先跟 Android 的 USB Host 体系打交道。这篇文章就是我针对车载项目中常用的 USB Host、USB 串口、USB-CAN、HID 以及相关系统 API 做的开发笔记既聊原理也放能直接用的代码和踩坑记录适合做车机系统开发、车载应用层开发以及想从手机 App 转车载方向的工程师参考。先交代一下我遇到的实际场景。我们那台 Android 车机主板上有 4 路 USB两路固定给内置 4G 模组和行车记录仪剩下两路从中央扶手箱引出给用户。而项目里要支持的设备包括一个 USB 转串口调试线、一个诊断用的 USB-CAN 盒子、一个外接方向盘按键控制器本质是 HID 键盘设备再加上 U 盘批量导出日志。这几样东西本质上都走 USB但协议栈完全不一样处理方式也完全不同。1. 项目背景与整体技术选型思路1.1 为什么车载场景绕不开 USB Host先明确一点车机上的 Android 并不是像手机那样默认作为 USB Device 存在的。手机插上电脑是 MTP 模式但在车里车机才是那个主动枚举外部设备的主机U 盘、USB-CAN 盒子、串口线、按键控制器全部挂在车机 USB Host 控制器下面。这也是为什么做车载开发时android.hardware.usb.host这个 feature 几乎都会出现在车机固件里。USB Host 模式的意义在于它让车机有能力同时管理多个不同功能的 USB 外设。但这也带来一个麻烦每种外设背后是不同的协议和访问策略。比如 U 盘走的是 Mass Storage Class系统拿到后自动挂载到某一路径USB 串口走 CDC ACM 或者厂商私有命令USB-CAN 盒子通常就是一个自定义的 bulk 传输设备HID 键鼠则由内核 input 子系统接管。如果这些设备同时插在同一个 Hub 上怎么让它们互不干扰这就是我们需要在项目里重点解决的问题。1.2 “Android 充当 USB 主机”到底意味着什么从 Android 架构看USB Host 支持分为两层。上面一层是 Java API也就是UsbManager、UsbDevice、UsbDeviceConnection这一套普通 App 通过它可以枚举设备、申请权限、做 bulk 读写和 control 传输。下面一层是 Linux 内核的 usbfs也就是/dev/bus/usb/xxx/yyy节点系统进程或者 root 应用可以直接对这个节点做ioctl绕开 Java 层更灵活但也更危险。这里需要先建立一个认知Android 的 USB Host API 只是一个“管道”它本身不解析设备和数据。真正要跟 USB 串口、USB-CAN、HID 这种具体设备通信还是得自己按照对应协议组织数据。拿 USB 串口举例你知道要在哪个 endpoint 上写数据没有用还得先通过 control transfer 把波特率、数据位这些参数配置对对方才认你的帧。所以下文所有的实操都是这个思路先用 USB Host API 打开设备再按具体协议做配置和数据交换。1.3 整体技术栈选型串口、CAN、HID 的取舍项目初始我做了个简单对比把几种常见外设的接入方式、难易程度和典型场景列出来这样后面开发方向就不会跑偏。通信方式典型设备Android 接入方式车载典型场景USB 串口PL2303、FT232、CH340、CP2102UsbBulk 读写 Line Coding 配置MCU 日志、诊断命令、外设控制USB-CANCANable、各厂商 USB-CAN 盒串口协议或厂商私有 bulk 协议整车报文解析、UDS 诊断、节点仿真HID方向盘按键盒、旋钮、手柄内核 input 子系统 / Raw HID 读写物理按键、音量旋钮、快捷操作Mass StorageU 盘、移动硬盘StorageManager / MediaStore日志导出、固件升级包选型上有一个原则我一直在用能走标准协议就不走私有协议。比如 HID如果按键盒能模拟成标准键盘Android 系统直接就把它当输入设备了上层用KeyEvent监听就行如果非要走自定义 HID Report那就得自己解析 Report Descriptor开发量和维护成本都会明显上去。USB 串口同理优先选支持 CDC ACM 标准的芯片因为 Android 系统层面对标准类有基础支持而不是所有厂商私有方案都能直接跑通。2. USB Host 模式下的设备发现与权限管理2.1 从 UsbManager 拿到设备列表USB Host 开发的第一步就是枚举设备。Android 里UsbManager.getDeviceList()返回一个HashMapString, UsbDevicekey 是设备节点路径value 就是设备对象。拿到UsbDevice之后上面有 VID、PID、设备类、接口列表这些关键信息。下面是我在项目里常用的枚举代码测试车机 USB 插了什么设备非常方便UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice deviceList usbManager.getDeviceList(); if (deviceList.isEmpty()) { Log.d(USB, no device); return; } for (UsbDevice device : deviceList.values()) { Log.d(USB, String.format(device%s vid%04x pid%04x, device.getDeviceName(), device.getVendorId(), device.getProductId())); for (int i 0; i device.getInterfaceCount(); i) { UsbInterface intf device.getInterface(i); Log.d(USB, interface # i class intf.getInterfaceClass() subclass intf.getInterfaceSubclass() protocol intf.getInterfaceProtocol()); for (int e 0; e intf.getEndpointCount(); e) { UsbEndpoint ep intf.getEndpoint(e); Log.d(USB, endpoint dir ep.getDirection() type ep.getType() address0x Integer.toHexString(ep.getAddress())); } } }这里比较关键的是看interface class。比如串口芯片的 CDC ACM 接口class 一般是 2UsbConstants.USB_CLASS_COMMHID 键盘的接口 class 是 3USB_CLASS_HIDU 盘的 Mass Storage 接口 class 是 8。如果看到某个设备的接口 class 数值很奇怪大概率是厂商私有协议这种就要按厂商文档去解析了。很多刚接触 USB 开发的同学会忽略一个细节同一个物理设备可能挂多个接口比如一个带耳机孔的 USB-CAN 盒子它可能同时枚举出 HID 接口和厂商私有接口访问不同接口要用不同的 endpoint。别只盯第一个接口一定要循环遍历。2.2 动态权限申请与 USB 设备拔插监听跟蓝牙权限类似Android 上访问 USB 设备需要动态申请。流程是构造一个PendingIntent通过usbManager.requestPermission(device, pendingIntent)弹窗用户在系统弹窗里允许之后应用才会拿到设备的访问权。实际项目中我习惯于把权限申请和拔插监听放到一起处理。我看过不少人的代码只申请权限不注册拔插广播结果设备一断开就崩溃或者连接假死。正确的姿势是注册两个广播ACTION_USB_DEVICE_ATTACHED和ACTION_USB_DEVICE_DETACHED。private static final String ACTION_USB_PERMISSION com.example.carusb.action.USB_PERMISSION; private final BroadcastReceiver usbReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { openDevice(device); } else { Log.e(USB, permission denied for device.getDeviceName()); } } } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); Log.d(USB, attached device.getDeviceName()); } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); closeDevice(device); Log.d(USB, detached device.getDeviceName()); } } };广播注册别忘了在onResume里注册、onPause里注销。另外注意从 Android 13targetSdk 33开始PendingIntent必须显式指定 mutable 或 immutable我一般用PendingIntent.FLAG_MUTABLE避免低版本机型上因为 flag 不对导致权限回调不触发。2.3 权限持久化与系统 API 的边界用户点一次“允许”之后这个授权是临时的设备拔掉再插上、或者应用重启大概率会再次弹窗。如果车机上不想每次插拔外设都弹窗有两个方向。普通应用层面只能让用户勾选“默认使用这个 USB 设备”或者捕获取向下一次授权如果是系统集成可以改UsbUserSettingsManager相关的持久化逻辑或者用系统签名应用直接调grantDevicePermission这类隐藏 API。还有一个容易被坑的地方UsbManager虽然能拿到设备但拿到的UsbDeviceConnection是独占的同一时间只能有一个连接持有。多个应用同时抢同一个 USB 串口设备后面打开的一方会失败或者读不到数据。车载项目里经常有中控 App 和诊断 App 同时想访问同一个 USB-CAN 盒子这种冲突必须靠上层互斥或系统服务去仲裁而不是两个应用各干各的。系统 API 的边界也得心里有数。Java 层能做的事情到bulkTransfer、controlTransfer、claimInterface基本就到底了。遇到 USB 的异步中断、批量大数据吞吐、轮询高实时性的场景Java API 的性能并不理想需要走 JNI 直接操作 usbfs用USBDEVFS_SUBMITURB做异步传输。我在后面 CAN 高频收发时实测过Java 层bulkTransfer在 500kbps CAN 总线上连续收报文偶尔会出现帧间隙抖动想要稳定抓包还是得下沉到 native 层。3. USB 串口通信实战从驱动到数据帧3.1 USB 转串口芯片与驱动原理车载调试中最常见的 USB 转串口芯片就那么几颗FTDI FT232、Prolific PL2303、沁恒 CH340、Silicon Labs CP210x。它们把 USB 包转成 UART 信号本质上是做协议转换。芯片型号不同means 不同但在 Android 上不用太关注“驱动”这个词因为绝大多数情况用户态是不需要装驱动的关键是识别它枚举成什么接口。CH340 和 PL2303 很多实例会枚举成厂商私有接口FT232 和 CP210x 则更接近标准 CDC ACM 虚拟串口。这意味着你在 Android 上用同一套 UsbDeviceConnection 不一定都能直接跑通。碰到私有接口的设备要么按厂商手册手动配置 baud rate要么参考开源库里对特定 VID/PID 做的适配。这里放一个常见芯片的 VID/PID 对照表开发时可以直接对照日志来判断设备类型芯片VIDPID接口类型FT232R/FT230X0x04030x6001自定义串口类PL23030x067B0x2303厂商私有CH3400x1A860x7523厂商私有CP2102/CP210x0x10C40xEA60CDC ACM 类3.2 Android 上实现串口读写在 Linux 桌面系统上插上一个 USB 转串口内核会生成/dev/ttyUSB0应用直接 open 这个节点就行。但 Android 默认情况下普通应用拿不到这个 tty 节点权限而且很多车机固件的内核默认没有把 USB serial 驱动编译进去。所以 Android 上更通用的做法是用 UsbDeviceConnection 直接跟 USB bulk 端点通信绕开内核 tty 驱动。具体来说打开设备之后要遍历接口找到那个接口的子类为 2CDC ACM或者厂商自定义数据的接口拿到 bulk IN 和 bulk OUT endpoint然后做配置。以 FT232 为例初始化流程大概是UsbDeviceConnection connection usbManager.openDevice(device); // 选接口这里假设是第 0 个 UsbInterface intf device.getInterface(0); connection.claimInterface(intf, true); int baudRate 115200; // FT232 的设置波特率命令是通过 vendor 请求 0x03 实现的不同芯片不同 int requestType 0x40; // host-to-device, vendor, device int request 0x03; // FTDI 的 SET_BAUDRATE int value 115200; // 有的芯片直接传波特率有的是分频系数 int index 0; connection.controlTransfer(requestType, request, value, index, null, 0, 5000);这里我不打算把每家芯片的初始化命令全抄一遍因为不同芯片差异很大建议直接参考开源库felHR85/UsbSerial或ksksue/UsbSerial。我最早是自己对照芯片手册手撸的后来发现开源库已经把 VID/PID 适配、波特率计算、流控这些都处理好了车机上直接引库能省不少时间。3.3 波特率、流控与数据粘包处理串口通信第一步是配置波特率和帧格式两边参数必须一致否则收到的全是乱码。常用参数是波特率 115200、数据位 8、停止位 1、无校验也就是我们在代码里说的 8N1。有些老设备还在用 9600 波特率车机上对接 MCU 时一定要跟硬件同事确认猜是猜不出来的。再就是流控。RS232 串口上有 RTS/CTS 硬件流控USB 虚拟串口也有对应的控制请求。大部分车载调试场景不需要开流控用默认 None 就好。但如果串口线很长或者对端设备比较老可以考虑在SET_LINE_CODING请求里把 flow control 位设置上。数据粘包这个是串口开发的老问题。USB 传输层是按包发的应用层读到的是连续字节流协议上要自己定帧边界。我常用的做法是帧头比如 0xAA 0x55加长度字节加数据加校验接收端用状态机或者环形缓冲区来切帧。千万不要直接bulkTransfer一次就读一个“完整包”因为对方可能把一帧数据拆成两个 USB 包发出来也可能把两帧数据拼在一个 USB 包里。4. USB-CAN 转换器的接入与 CAN 帧收发4.1 CAN 总线基础与车载 CAN 报文格式说 USB-CAN 之前得先简单补一下 CAN 总线的底子。CAN 是车载上最常用的现场总线现在还有 CAN FD 在普及但最基础的还是 CAN 2.0A 和 CAN 2.0B分别对应 11 位标准帧 ID 和 29 位扩展帧 ID。一个标准 CAN 数据帧包含这么几个部分帧起始、仲裁段ID 加 RTR、控制段IDE 加 DLC、数据段最多 8 字节、CRC、ACK、EOF。从上层的角度看我们最关心的就三样ID、DLC、数据。比如动力 CAN 上常见的转速报文ID 是 0x0C08 个数据字节里前两个字节拼出一个转速值。具体的位定义每个车型都不一样需要拿到 OEM 的 DBC 文件或者通过逆向去标定。车载上不同总线通常波特率不同我见过比较多的是动力 CAN 500kbps、车身 CAN 125kbps。如果波特率配错CAN 控制器会一直报总线错误抓回来的报文也全是垃圾。这个在 USB-CAN 调试时是最容易踩的坑。4.2 USB-CAN 适配器在 Android 上的工作方式USB-CAN 适配器本质上是一个 USB 设备它把 USB 包转成 CAN 帧。市面上产品分两类。一类是 CANable、CANtact 这种开源方案它们支持 slcan 协议简单说就是把 CAN 帧编码成 ASCII 字符串通过串口发送所以 Android 这边可以把它当作 USB 串口来访问。另一类是各厂商的私有协议 USB-CAN 盒比如创芯、周立功的有专门的上位机 SDK在 Android 上走的是私有 bulk 传输需要对照厂商协议文档来自行封装。我项目里优先用了 CANable 这类支持 slcan 的盒子因为它协议公开、解析简单调试周期最短。slcan 协议的常用命令也很直白S 波特率字符设置 CAN 波特率O打开 CAN 接口C关闭 CAN 接口t发送标准帧T发送扩展帧波特率字符和实际速率的对应关系通常是010k120k250k3100k4125k5250k6500k7800k81M。不同固件可能略有差异以固件源码里的码表为准。4.3 实操从打开设备到收发 CAN 帧我用 CANable 举例完整流程分三步先当作 USB 串口打开再发 slcan 命令配置波特率并把 CAN 接口打开最后按帧格式收发。发送标准帧时协议格式是tidlendataid 固定 3 个 HEX 字符len 是 1 个 HEX 字符data 是 2*len 个 HEX 字符。比如发送 ID 为 0x123、8 字节数据11 22 33 44 55 66 77 88的标准帧对应的 ASCII 行就是t12381122334455667788\r这里的t表示标准数据帧123是 ID8是 DLC后面 16 个字符是数据。注意 ID 必须补齐三位比如 0x7E0 就写7E0不能写成7E0以外的格式否则固件解析不了。接收端从串口读取 ASCII 行以\r为行结束符。下面是简化版的 CAN 帧解析代码private void processCanLine(String line) { if (line null || line.length() 4) return; char type line.charAt(0); if (type t || type T) { int idx 1; int idLen (type t) ? 3 : 8; long id Long.parseLong(line.substring(idx, idx idLen), 16); idx idLen; int dlc Integer.parseInt(String.valueOf(line.charAt(idx)), 16); idx; if (line.length() idx dlc * 2) { byte[] data new byte[dlc]; for (int i 0; i dlc; i) { data[i] (byte) Integer.parseInt( line.substring(idx i * 2, idx i * 2 2), 16); } long timestamp System.nanoTime(); onCanFrameReceived(id, data, timestamp); } } }实测下来slcan 方案在 500kbps 总线上跑普通数据采集是没问题的但到了需要大量连续发送或者需要精确时间戳的场景slcan 的文本解析会成为瓶颈。这时候就得改走厂商私有二进制协议用更紧凑的帧格式。另外CAN 总线两端需要接 120 欧姆终端电阻一般 CAN 盒上会带跳线或者内置开关否则会因为信号反射导致持续报错这是一个硬件层面的坑软件再调也没用。5. HID 设备交互方向盘按键、音量旋钮与专用 HID 设备5.1 HID 协议报告描述符与发送/接收路径HID 设备在 USB 里属于人机交互类接口 class 是 3。Android 系统对接标准 HID 键盘鼠标时内核 input 子系统会把 HID report 翻译成 Linux input event再往上层走 InputManagerService最终变成应用层能收到的KeyEvent和MotionEvent。这也是为什么你把一个 USB 键盘插到车机上不写一行代码就能打字。但“能打字”和“能拿到你想要的按键”是两回事。很多非标 HID 设备比如自定义按键盒、旋钮它不一定按标准键盘 report 发送。这种设备就需要应用自己去拿 HID Report Descriptor按照 Usage Page 和 Usage ID 去理解数据含义。第一步是用 control transfer 读 Report Descriptor// GET_DESCRIPTOR requestwValue 高字节 0x22 表示 HID Report Descriptor byte[] desc new byte[512]; int len connection.controlTransfer( 0x81, // device-to-host, standard, interface 0x06, // GET_DESCRIPTOR 0x2200, // HID Report Descriptor intf.getInterfaceNumber(), desc, desc.length, 1000);拿到 desc 之后可以丢给hidrd-convert这类工具去看也可以写个简单的解析器。实际开发中很多时候不需要完整解析只要确认设备上报的数据结构然后用 bulk interrupt IN 端点拿数据、把 report 里的 Usage ID 和实际物理按键对应起来就行。5.2 模拟 HID 键盘发送音量修改和普通按键再看如何发送。车载项目里除了接收 HID 按键还有一个常见需求是在测试或者自动化场景下模拟一个 HID 设备让车机以为用户按了物理按键。比如我们要模拟“音量加”键这个其实是 Consumer PageUsage Page 0x0C下的 Usage不是标准键盘页面的按键。USB HID 键盘有专门的标准报告格式通常是 8 字节第一个字节是修饰键第二个字节保留后面最多 6 个普通按键。普通按键的 Usage ID 从 0x04 开始0x04 是字母 a0x28 是 Enter。要发送普通按键需要同时发送“按下”和“松开”两个报告。比如模拟按字母 abyte[] press new byte[] { 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 }; byte[] release new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // 发送到 interrupt OUT endpoint connection.bulkTransfer(epOut, press, press.length, 2000); Thread.sleep(20); connection.bulkTransfer(epOut, release, release.length, 2000);音量加减这类多媒体键不同它走的是 Consumer Page很多键盘设备会有第二个 HID 接口专门发这个。常见 Usage ID0xE9 是 Volume Increment0xEA 是 Volume Decrement0xCD 是 Play/Pause。发送一个音量加报告内容一般是两三个字节比如byte[] volUp new byte[] { 0x00, (byte) 0xE9 }; connection.bulkTransfer(epOut, volUp, volUp.length, 2000);不过要注意这里我举的是“设备发送 HID report”的例子。正常情况下我们不会在 Android 应用里模拟 HID 上报更常见的做法是用adb shell input keyevent KEYCODE_VOLUME_UP或者Instrumentation.sendKeyDownUpSync向系统注入按键事件那是走 InputManager 的注入通道。但如果你的外设本身就是一个带 MCU 的自制按键盒那通过 HID 固件模拟键盘设备就是完全合理的路子。5.3 与 Android 事件分发机制的衔接不管 HID 键鼠事件来自哪个设备进入 Android 后都会经过 InputManagerService 和 InputDispatcher 做分发。应用层关心的是两个方法dispatchKeyEvent负责按键事件dispatchGenericMotionEvent负责旋钮这类相对位移事件。实际项目里我们那套方向盘按键盒上报的是标准 HID 键盘报告但因为车载场景需要把这些按键映射成车机业务比如左边按键是“上一曲”右边是“音量加”我们做了一个动态按键映射层。做法是在 framework 层把 HID usage 映射成自定义KeyEventkey code应用层根据 key code 执行对应操作。这块如果只做应用不打算改 framework也能通过OnKeyListener监听 KEYCODE_MEDIA_NEXT、KEYCODE_VOLUME_UP 这些标准 key code但灵活性就差一些。另外如果按键设备上报的是模拟鼠标或者旋钮滚轮那事件类型会变成ACTION_SCROLL或者ACTION_MOVE并且带有MotionEvent.AXIS_VSCROLL这类的轴值。判断之前建议先打日志看 event 的 source 类型InputDevice.SOURCE_KEYBOARD、SOURCE_MOUSE、SOURCE_ROTARY_ENCODER分别对应不同的处理路径。6. 车载环境下的系统 API 细节与权限踩坑6.1 Android 版本差异分区存储、FileProvider 与共享路径车载项目里经常要跟文件路径打交道尤其是 U 盘日志导出、升级包拷贝。Android 11 之后分区存储收紧很多以前随手访问的路径现在都过不去了。最常见的现象是U 盘挂载后应用想直接读/storage/XXXX-XXXX/下的文件发现没有权限或者接收方打开content://路径时提示文件不存在。这里有个细节要特别注意content://com.tencent.wework.fileprovider/external_path/android/data/com.xxx这种 Uri 是某个应用把自己的 FileProvider 暴露出来的但它指向的物理路径通常在/storage/emulated/0/Android/data/com.xxx/下面。Android 11 以后这个目录对别的应用就不可见了所以别人拿到 Uri即使权限没断也可能因为路径被限制而打不开。车载上我们常用一个折中方案U 盘日志先通过MediaStore或者StorageManager写入公共目录再通过 FileProvider 分享而不是直接分享应用私有目录下的文件。另外如果在调试阶段需要快速执行脚本比如adb shell sh /storage/emulated/0/android/data/com.omarea.vtools/up.sh这种场景需要注意 Android 对/storage/emulated/0/Android/data的访问限制同样存在USB 调试模式下 adb shell 权限高能读但应用进程并不能随意访问别人目录下的文件。6.2 SELinux、USB 设备节点权限与直接访问节点如果是做系统集成权限问题突出表现在 SELinux 上。默认enforcing模式下普通应用通过UsbManager拿到的 fd 是 UsbService 代开的审批流程没问题。但如果你想在 native 层直接打开/dev/bus/usb/xxx/yyy就涉及 SELinux domain 的usb_device节点访问权限。车机固件如果不加上对应 policy系统应用 open 也会得到 Permission denied。调试阶段可以用setenforce 0临时关掉 SELinux 验证功能但正式版本一定要写好 policy。另外就算关掉 SELinux普通目录的权限也不够/dev/bus/usb下面的节点默认只有 USB 服务进程能访问。车机上如果有很多功能要直接访问 USB 节点我见过不少方案是做一个系统级 USB 服务统一收口设备访问而不是让每个应用各自去开节点。6.3 多 USB 设备并发、供电与热拔插稳定性最后必须说供电和并发。车机 USB Host 口供电能力有限同时插 USB-CAN 盒、4G 模块、U 盘经常会出现电压跌落导致某个设备掉线。我踩过最深的坑是USB-CAN 盒子在整车电源波动时反复掉线重连重连后固件没有恢复到打开状态上位机一直收不到帧。解决思路分几层第一硬件上选择带外部供电的 USB Hub第二软件上监听ACTION_USB_DEVICE_DETACHED在重连后自动重配置设备比如重新设置波特率、重新打开 CAN 通道第三如果一台车要同时接多个 USB 设备尽量让应用按设备节点路径而不是 VID/PID 去区分因为同型号两个设备插上后节点号可能会变。之前我们车机上插了两块同型号 USB-CAN应用一开始只按 PID 匹配结果一块拔掉再插上之后节点路径变了应用就把两块设备的访问对象搞混了抓回来的报文全都乱了。7. 常见问题与排查技巧实录7.1 设备枚举不到getDeviceList一直为空最基础的排查点是硬件。先确认车机 USB 口本身是 Host 模式有些车机或者板卡会把 USB 口配成 Device 模式那UsbManager什么也拿不到。再看系统里有没有声明 USB Host featuremanifest 里漏了uses-feature android:nameandroid.hardware.usb.host /虽然不一定导致设备列表为空但会导致在某些市场或者系统服务里被阉割掉。还有一种情况是 Hub 供电不足导致设备枚举失败。规格书上写着 5V 1A实际满载瞬间电压降到 4.5V 以下设备就起不来。这种问题在日志里往往能看到内核打印device descriptor read/64, error -71。可以考虑换带供电的 Hub或者给外设单独供一路电。7.2 USB 转串口驱动识别失败VID_067B 与 PID_2303很多做串口调试的同行应该都遇到过USB\VID_067BPID_2303\52a81f84703这种设备实例路径这就是一颗 PL2303。Windows 上这个芯片的问题是经典中的经典新版 3.x 驱动会检测到老款 HXA 芯片或者兼容芯片然后直接驱动加载失败设备管理器里黄色感叹号错误码 10。解决办法不是一味更新驱动而是找到对应的旧版 2.x 驱动来安装或者换一颗官方新版芯片。在车机 Android 端PL2303 通常会被内核的pl2303驱动识别如果内核没编这个驱动就把它当普通 USB 设备用UsbManager打开。这一点跟 Windows 完全是两套体系别把 Windows 的驱动思维带过来。7.3 数据乱码、CAN 帧丢失怎么排查乱码优先怀疑波特率不匹配和电平不匹配。电平方面TTL 电平的串口不能直接接 RS232 设备必须加 MAX232 这类电平转换芯片否则数据大概率是乱码或者根本不通。CAN 帧丢失也不一定是代码问题先确认终端电阻、总线波特率、总线负载率再看是不是上层解析丢包。如果确认硬件没问题再看 USB 传输层。Java 层bulkTransfer是同步阻塞的接收速率跟不上总线速率时缓冲区溢出丢帧很常见。我的经验是开一个专门的读线程并且把读缓冲设成设备最大包长的整数倍比如 64 字节端点就开 4096 的缓冲减少bulkTransfer次数。7.4 常见问题速查表现象可能原因排查建议getDeviceList为空USB 口非 Host / Hub 供电不足换口、换供电 Hub、查内核日志授权弹窗不出现PendingIntent flag 错误 / intent-filter 缺失检查FLAG_MUTABLE和设备过滤配置串口打开失败接口被占用 / 权限未授予关掉其他 App、检查claimInterface返回串口数据乱码波特率不匹配 / 电平不匹配确认参数 8N1、加电平转换CAN 一直报总线错误波特率配错 / 缺终端电阻两端加 120Ω、确认总线速率HID 按下无事件设备不是标准键盘 / Report Descriptor 解析错读 Descriptor确认 Usage PageApp 退后台后 USB 失效广播未处理 / 连接未持有前台服务持有连接并注册监听最后再分享一个我觉得很有用的经验车载 USB 开发一定要在台架上把“插拔、断电重启、多个外设同时接入”这三种场景反复测因为车机上的 USB 环境比开发板恶劣得多。很多问题不是代码写错而是时序和资源竞争。调试时多用adb shell dmesg看内核日志多用adb shell cat /sys/kernel/debug/usb/devices看设备树这些信息往往比应用层日志可靠得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价