
做车载 Android 项目这些年我慢慢摸出一个规律只要涉及 USB 调试和外设接入问题大多不在 Android 代码本身而在你“有没有先把 USB 当成一个电气实体去理解”。车机上的 USB 口身兼数职——刷机调试、插 U 盘升级、接 USB 转串口模块读取车机协议、连 USB-CAN 工具抓总线上报文、还可能同时接 HID 方向盘按键或者音量控制设备——而这些职责正好覆盖了 Android 的 USB Host、USB 串口、USB-CAN、HID 四大类开发场景。这篇文章不是简单贴十几个 API 就完事。我会先从车载环境下 USB 的供电、权限和驱动关系讲起再分别给出 USB Host 设备监听、USB 串口通信、USB-CAN 收帧发帧、HID 键值处理的一套可落地做法最后把我真车上踩过的坑和排查思路整理成速查表。适合正在做车机系统集成、车载 HMI 联调、或者打算给 Android 设备扩展外设的开发者参考。你不一定需要很懂 USB 协议但读完至少要能自己上手定位问题而不是对着 Log 干瞪眼。1. 车载 USB 开发的环境认知1.1 为什么车载 USB 和手机上的完全是两回事Android 手机上的 USB 大多就是 Type-C 口加 OTG 功能应用层拿到标准 UsbManager设备和驱动都比较干净。到了车载上情况立刻变复杂车机常见四口 USB HUB 或双 Type-C其中有的是从车规级电源模块直接供电有的中间还经过一颗 USB 模拟开关或防静电保护芯片甚至有些口要复用给 CarPlay、Android Auto、百度 Carlife 做投屏通道。同一个物理 USB 口有时是 Host 模式给手机提供数据通路有时又要切到 Device 模式让车机变成外设这种多模式切换是车载特有的场景。如果你只是按手机开发思维去写标准 UsbHost会碰到两个典型问题: 口线开关切换不及时插上设备根本没枚举设备枚举正常但供电能力不足数据一传就掉线。有个项目我们为了一个 USB 摄像头连续调了三天最后发现车机底层把那个口配置成了 0.5A而摄像头正常工作要 1A供电一补上所有现象全部消失。因此开发车载 USB 外设之前我最建议先画一张车机 USB 拓扑图有几个 USB 控制器、是否经过 USB Switch、外设供电是否独立、D/D- 是否经过 HUB、哪些口常供电哪些口随系统休眠断电。这张图画清楚了后面很多问题其实已经解决了一大半。1.2 权限、供电、枚举是三个必须同时看的环节车载 USB 开发里权限、供电、枚举可以说是三位一体。Android 应用层的 UsbManager 只负责权限和访问但设备能不能被系统发现取决于内核驱动和电气环境。我在排查问题时一般按这个顺序走插上设备先看设备本身的供电指示、上电行为是否正常查看dmesg中是否出现 usb 相关报错比如 over-current、device descriptor read error检查 sysfs 下/sys/bus/usb/devices/是否生成了设备节点最后才让应用层通过usbManager.getDeviceList()去拿设备列表。很多同事习惯一上来就在 Android Studio 里打 Log那是把排查顺序搞反了。USB 枚举是内核干的事应用层 Log 再多也没法告诉你是 D 上拉电阻出了问题还是波特率没了。权限层面也有几个容易踩的点。普通应用用UsbManager.requestPermission()请求权限时如果用户点过一次“拒绝”部分系统就不再弹窗了需要去系统设置里清掉 USB 权限记录或者直接换一个 USB 口重新插。另外如果应用在 manifest 里声明了uses-feature android:nameandroid.hardware.usb.host/在没有 USB Host 能力的设备上安装时可能会被过滤掉做车机这种形态百变的项目建议把required设为false。2. USB Host 权限申请与热插拔监听实战2.1 manifest 声明和 device_filter 的正确写法Android 的 USB Host 开发第一步是让应用知道自己需要 USB 能力并且想知道哪些设备能唤起自己。常规写法是在 AndroidManifest.xml 里声明 uses-feature再注册一个带 intent-filter 的 receiveruses-feature android:nameandroid.hardware.usb.host android:requiredfalse / receiver android:name.UsbDeviceReceiver android:exportedtrue intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / action android:nameandroid.hardware.usb.action.USB_DEVICE_DETACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /receiverdevice_filter.xml 写在 res/xml 目录下用来声明你关心的设备。可以只写 vendor-id也可以精确到 vendor-id product-id?xml version1.0 encodingutf-8? resources usb-device vendor-id1027 / usb-device vendor-id4292 product-id60000 / /resources这里有个能救命的细节vendor-id 和 product-id 都是十进制不是你在 Windows 设备管理器里看到的那种十六进制 vid_1A86。比如 CH340 常见的 vid 是 0x1A86对应十进制 6790pid 0x7523对应十进制 29987。我第一次写 filter 时直接抄了 1A86结果怎么都匹配不上后来才意识到进制问题。2.2 用 UsbManager 实现热插拔监听和动态权限申请静态 receiver 适合设备插入时自动拉起应用的场景但实际开发中我更多是动态注册广播来做权限申请和设备状态维护因为很多时候应用已经在前台了不需要被设备“唤醒”。核心流程是获取 UsbManager → 拿到设备列表 → 检查权限 → 没权限就 requestPermission → 收到授权广播后打开设备。UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice deviceList usbManager.getDeviceList(); if (deviceList.isEmpty()) { return; } UsbDevice target null; for (UsbDevice device : deviceList.values()) { if (device.getVendorId() 6790) { target device; break; } } if (target null) { return; } if (usbManager.hasPermission(target)) { openDevice(target); } else { PendingIntent permissionIntent PendingIntent.getBroadcast( this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE); usbManager.requestPermission(target, permissionIntent); }动态监听插入和拔出的广播我习惯单独写一个 receiverprivate final BroadcastReceiver usbReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device ! null) { handleDeviceAttached(device); } } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); handleDeviceDetached(device); } } };注意广播的 flag 问题。Android 12 之后 PendingIntent 的FLAG_MUTABLE和FLAG_IMMUTABLE不能乱选USB 权限回调的结果要通过 PendingIntent 回传必须使用FLAG_MUTABLE否则系统会有安全性检查问题部分定制 ROM 直接不回调。2.3 打开设备、claimInterface 和端点方向判断拿到权限后真正打开设备的代码要处理接口Interface和端点Endpoint。一个 USB 设备可能有多个 interface比如 USB 摄像头同时有 VideoControl 和 VideoStreaming一个 interface 下又会有多个 endpointIN 端点是设备发给主机OUT 端点是主机发给设备。UsbDeviceConnection connection usbManager.openDevice(device); UsbInterface usbInterface device.getInterface(0); boolean claimed connection.claimInterface(usbInterface, true); if (!claimed) { // 通常是被内核驱动或其他应用占用 return; } for (int i 0; i usbInterface.getEndpointCount(); i) { UsbEndpoint ep usbInterface.getEndpoint(i); Log.d(UsbDemo, direction ep.getDirection() type ep.getType() maxPacketSize ep.getMaxPacketSize()); }claimInterface 的第二个参数 forcetrue 表示强制抢占。在手机开发中很少遇到抢设备的问题但车机上经常有系统级的拨号、语音模块占用同一个 USB 设备。如果你在日志里看到could not claim interface不要急着把 force 设为 true 硬抢先查一下到底是哪个进程在占用否则会造成系统服务崩溃。数据收发用bulkTransfer或usbRequestQueue。前者简单直接是阻塞的建议放到子线程后者异步效率高适合连续传感器数据。车载项目里我最常用的是bulkTransfer加一个读写线程配合阻塞队列简单不容易出错。3. USB 串口开发从驱动识别到应用层通信3.1 常见 USB 转串口芯片与 Android 端的接入思路车载设备里最常见的 USB 转串口芯片无非这几类CH340/CH343、CP2102/CP210x、FT232、PL2303。它们对应不同的 Linux 内核驱动枚举后内核可能生成/dev/ttyUSB0、/dev/ttyCH343USB0、/dev/ttyS4之类的节点也可能完全不生成节点只作为 USB device 让应用层直接访问。这里要区分两条路。一条是系统已经有了串口驱动节点比如有些车机 ROM 把 USB 转串口接成了 debug 串口会在 /dev 下生成 tty 节点。应用要读写这个节点除了要 root 或者 shell 权限还要过 SELinux 这一关普通应用一般打不开。另一条路是应用层不依赖内核 tty 节点直接用时尚的 USB Host 方案把串口芯片当成一个普通 USB 设备来操作这也是开源库 usb-serial-for-android 的核心思路。usb-serial-for-android 这个库我用了好几年它支持 CH340、CP210x、FTDI、PL2303 等常见芯片不需要 root也不依赖系统有没有生成 tty 节点。原理是它自己解析 USB CDC 协议的 class 描述符直接用 bulk endpoint 收发数据。这对做车机第三方应用的开发者来说是最省事的方案。3.2 串口连接、参数配置和流控避坑用 usb-serial-for-android 的典型流程如下ListUsbSerialDriver drivers UsbSerialProber.getDefaultProber().findAllDrivers(usbManager); if (drivers.isEmpty()) { // 找不到支持的串口芯片 return; } UsbSerialDriver driver drivers.get(0); UsbDevice device driver.getDevice(); if (!usbManager.hasPermission(device)) { usbManager.requestPermission(device, permissionIntent); return; } UsbSerialPort port driver.getPorts().get(0); UsbDeviceConnection connection usbManager.openDevice(device); port.open(connection); port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); port.setDTR(true); port.setRTS(true);这里有个关键的坑setDTR和setRTS。很多 USB 转串口模块靠 DTR/RTS 控制复位如果你的串口设备是一块 MCU 开发板打开串口时千万不要盲目把 DTR 拉高否则 MCU 会直接复位。我之前接一块串口屏每次应用启动都收到设备重启排查了大半天最后发现是这个模块的 DTR 接在了 MCU 的复位引脚上。波特率、数据位、停止位、校验位这些参数必须和对方一致常见的车机调试协议一般是 115200 8N1也就是波特率 115200、8 个数据位、无校验、1 个停止位。有些老款 OBD 或者车身控制器会用 9600、38400建议把波特率写成可配置项方便真车联调时现场改。流控建议默认全关。硬件流控需要额外接线和双方协议支持软件流控在 Android 的 USB 串口实现中经常出问题。遇到数据乱码先别怀疑流控先拿逻辑分析仪或者电脑串口工具对比一下通常问题出在波特率偏差和地线共地。3.3 串口协议解析帧头帧尾、粘包拆包串口数据是流式的不是一次一个完整包。实际开发中你发一个“查询版本”的命令设备可能回一段带帧头的二进制数据也可能因为调度原因拆成好几个包到达。为了保证解析不丢数据我习惯写一个专门的字节缓冲区和状态机。public class SerialFrameParser { private final ByteArrayOutputStream buffer new ByteArrayOutputStream(); private static final byte FRAME_HEAD (byte) 0xA5; private static final byte FRAME_TAIL (byte) 0x5A; public Listbyte[] push(byte[] data) { buffer.write(data, 0, data.length); Listbyte[] frames new ArrayList(); byte[] all buffer.toByteArray(); int start -1; for (int i 0; i all.length; i) { if (all[i] FRAME_HEAD) { start i; } else if (all[i] FRAME_TAIL start 0 i start) { byte[] frame Arrays.copyOfRange(all, start, i 1); frames.add(frame); start -1; } } buffer.reset(); if (start 0) { buffer.write(all, start, all.length - start); } return frames; } }这种按帧头帧尾切包的方式比简单地按长度截取要稳得多。如果协议里还带 CRC 校验建议在确认帧头帧尾之后再做一遍完整性校验校验失败不要直接丢弃打印出来对比方便排查是不是波特率不对导致丢字节。4. USB-CAN 模块接入让 Android 车机直接收 CAN 报文4.1 USB-CAN 硬件选型与 Linux 内核协议栈背景USB-CAN 的出现是为了给没有板载 CAN 控制器的计算机提供 CAN 总线接入能力。常见的 USB-CAN 适配器有周立功 USBCAN-II、CANable、基于 STM32 和 MCP2515 的各类自定义盒子以及现在很火的 candleLight 固件方案。这些硬件在 Linux 内核里大多有对应驱动比如gs_usb支持 candleLightpeak_usb支持 PEAK 系列canable也基本走 gs_usb 协议栈。SocketCAN 是 Linux 内核的 CAN 子系统它把 CAN 总线抽象成了类似 socket 的接口应用层可以用 read/write 收发 CAN 帧。Android 基于 Linux 内核理论上也具备这个能力但要看厂商有没有把相关内核配置打开。我在定制车机上遇到过好几种情况内核根本没有 CAN 子系统内核有 CAN 但没编 USB-CAN 驱动有驱动但 SELinux 不允许应用创建 AF_CAN socket。因此拿到一台车机后我第一件事就是检查内核配置。能上 adb 就优先用 adbuserdebug 版本上直接执行adb shell zcat /proc/config.gz | grep -i can如果系统没有 /proc/config.gz那就只能看 dmesg 或者编译一个测试小程序去创建 AF_CAN socket。内核这一关不过应用层再怎么写都没用。4.2 用 SocketCAN 配置 CAN 接口确认内核支持后下一步是加载驱动并配置 CAN 接口。USB-CAN 插上后理论上 dmesg 会显示类似gs_usb: probing的日志然后出现can0接口。如果没有自动出现可能需要手动加载模块adb root adb shell modprobe gs_usb adb shell ip link set can0 up type can bitrate 500000注意配置写法ip link set can0 up type can bitrate 500000比较直观也可以在 up 之前先设置类型ip link set can0 type can bitrate 500000 ip link set can0 up500000 就是常说的 500k 波特率。整车总线里动力 CAN 和车身 CAN 常用 500k部分娱乐系统和诊断 CAN 也可能是 250k。波特率必须和总线上其他节点保持一致否则你只能看到一堆错误帧或者干脆什么都收不到。如果这条命令提示Operation not permitted先检查 root 权限再检查 SELinux。Android 上即使有 root也经常因为 SELinux enforcing 模式拦掉了 CAP_NET_ADMIN 能力此时要么临时把 SELinux 设为 permissive要么给对应的 te 文件加 allow 规则。4.3 上层收发 CAN 帧的 JNI 封装配置好 can0 接口后应用层要访问它最干净的做法是写一层 JNI。因为 Android 的 Java 层没有原生 CAN socket API你可以用 SocketChannel 模拟但那个并不能直接用 AF_CAN。C 代码大致如下#include linux/can.h #include linux/can/raw.h #include sys/socket.h #include net/if.h #include sys/ioctl.h #include string.h #include unistd.h int can_open(const char *ifname) { int s socket(AF_CAN, SOCK_RAW, CAN_RAW); if (s 0) return -1; struct sockaddr_can addr; struct ifreq ifr; strcpy(ifr.ifr_name, ifname); if (ioctl(s, SIOCGIFINDEX, ifr) 0) { close(s); return -1; } addr.can_family AF_CAN; addr.can_ifindex ifr.ifr_ifindex; if (bind(s, (struct sockaddr *)addr, sizeof(addr)) 0) { close(s); return -1; } return s; } int can_send(int s, int can_id, uint8_t *data, uint8_t len) { struct can_frame frame; memset(frame, 0, sizeof(frame)); frame.can_id can_id; frame.can_dlc len; memcpy(frame.data, data, len); int nbytes write(s, frame, sizeof(frame)); return nbytes sizeof(frame) ? 0 : -1; }CAN 帧结构体里can_id需要注意扩展帧的标识。标准帧 ID 是 11 位扩展帧是 29 位发给底层时如果有扩展帧还需要在can_id上或上CAN_EFF_FLAG。这个话题展开又是一篇长文初期做项目先拿标准帧跑通流程。4.4 实际项目中抓取总线报文和 OBD 诊断CAN 抓包最常用的工具是 can-utils 里的candump。在 Android 上可以交叉编译后推到车机里执行adb shell candump -x can0这条命令能打印出所有帧 ID 和数据非常适合前期验证硬件通不通、波特率对不对。看到类似can0 123 [8] 01 02 03 04 05 06 07 08的输出就说明链路通了。如果要做 OBD 诊断就绕不开 ISO-TP 协议。标准 OBD-II 请求一般发到功能寻址 ID0x7DF比如发送 PID 0x0C 读取转速后续回复会出现在 0x7E8 之类的物理 ID 上。这种多帧诊断报文需要 ISO-TP 层做拆分和重组Linux 上有can-utils的isotpsend和isotprecv也可以在内核里启用can_isotp模块。我在 Android 里做诊断功能时一般把 ISO-TP 的收发逻辑放到 native 层用独立的 CAN socket 线程去处理。尽量不要把 CAN 数据收发直接放在 Java 主线程CAN 总线报文密度高的时候Java GC 会导致丢帧。5. HID 设备接入与车载按键处理5.1 HID 在车载场景里到底承担什么角色HIDHuman Interface Device在车机上的存在感其实很强。标准 USB 键盘、USB 鼠标、无线遥控器接收器都是 HID 设备很多多功能方向盘按键通过车身模块转换成 CAN 信号但如果你想做一个外接的 USB 旋钮或按键面板最通用的方案就是把它实现成一个 HID 键盘或者 HID Consumer Control 设备。搜索词里那句“HID 键盘发送音量修改和普通按键”对应的就是 HID Consumer Page 里的音量增大、音量减小和普通键盘按键。这类设备插入 Android 车机后系统 input 子系统会自动识别并派发键值不需要应用层做太多事麻烦的是“如何把某个 HID 用法映射成你想要的 Android KeyEvent”。5.2 应用层如何发现 HID 设备并区分类型在 UsbManager 的设备列表里HID 设备同样是一个 UsbDevice你可以通过接口的 class 来判断是不是 HIDUsbInterface intf device.getInterface(0); int deviceClass intf.getInterfaceClass(); if (deviceClass UsbConstants.USB_CLASS_HID) { // 这是一个 HID 设备 }但要注意Android 系统的 input 子系统可能会“抢走”这个设备应用层再来 claimInterface 时会失败这属于正常现象。普通应用想直接读 HID report 很困难除非是系统应用并且内核允许你访问 hidraw 节点。判断 HID 的用途通常看它的 report descriptor。一个 Volume Up 按键在 report descriptor 里很可能体现为 Consumer Page 的某个 Usage比如 0xE9 对应音量增加。演示文稿里不用展开字节级的 descriptor但你要知道这个方向硬件厂商会写好 descriptor系统靠它来完成“HID 按键 → Android Volume KeyEvent”的映射。5.3 音量控制与普通按键注入的系统级方案开发中常遇到的另一个需求是“让车机应用主动发出音量调整、普通按键”。如果你只是想在应用内响应音量键直接在 Activity 里重写onKeyDown就够。但如果要做自动化测试、全局控制或者让一个后台服务模拟物理按键就需要更底层的方案。最简单的方式是 shell 命令前提是有 shell 权限或者应用是系统签名# 音量增大 input keyevent 24 # 音量减小 input keyevent 25 # 回车键 input keyevent 66这种方式适合集成测试不适合作为产品功能因为input命令本质上是往 InputManager 注入事件安全策略比较严格的 ROM 会对调用来源做限制。更专业的方式是走隐藏 APIInputManager.injectInputEvent。这个方法在第三方应用里反射调用很容易被 ROM 拦截但在系统应用、system_server 进程或者有 SYSTEM_ALERT_WINDOW 权限的 app 里是比较标准的做法。需要先构造 KeyEventlong downTime SystemClock.uptimeMillis(); KeyEvent keyDown new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP, 0); InputManager.getInstance().injectInputEvent(keyDown, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); KeyEvent keyUp new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP, 0); InputManager.getInstance().injectInputEvent(keyUp, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);这种注入方式对时序有要求Action Down 和 Action Up 之间间隔太短可能被系统判断为误触间隔太长又可能出现长按效果。我实际项目的经验是 30ms 到 50ms 比较稳定。5.4 自定义 HID 按键失效的排查思路如果 USB HID 设备插上车机指示灯也亮了但按下去系统没反应按照下面顺序查先看设备是否被系统 input 识别查看dumpsys input里的 Input Devices 列表用getevent命令行看有没有上报原始事件如果是 Consumer 按键检查系统按键映射表很多 ROM 只处理标准键盘键码消费类按键没有被映射如果按键在特定 App 里没反应在其他 App 里正常那就要检查目标 App 是不是锁定了自有的按键截获逻辑。HID 开发最怕的就是把 report descriptor 写松散。建议初期先用现成的 HID 键盘模块验证通路确认系统按键映射没问题再上量产的定制 HID 设备。6. 系统 API、权限等级与产品化落地6.1 系统应用与普通应用的权限边界Android 的 USB Host 相关 API普通应用基本都能用。但到了车机这种定制系统里如果你要让应用在后台开机自启、屏蔽权限弹窗、自动 claim 某类 USB 设备就需要把应用升级成系统级应用或者申请更高等级权限。常见的做法是把 APK push 到/system/priv-app目录或者经过平台签名后预置到 ROM。只有到了 priv-app 级别你才能使用MANAGE_USB、WRITE_SETTINGS、PACKAGE_USAGE_STATS这类 signature|privileged 权限。具体在车机 ROM 里配置很简单在 vendor 分区放上 APK并给一个对应的permissions.xml声明你需要预授权的权限。6.2 SELinux 策略是车机开发绕不开的坎很多 USB 外设问题最终都指向 SELinux。Android 默认 enforcing 模式下普通应用即使有 UsbManager 权限也不能随便访问 /dev/ttyUSB0更不能访问 /dev/hidraw0 这类节点。CAN 设备更特殊创建 AF_CAN socket 需要allow system_app can_device:chr_file或者网络相关的能力。在 userdebug 版本上测试时可以直接adb root adb shell setenforce 0但产品发布时肯定不能这么干。正确的做法是给平台 sepolicy 打补丁。比如你要让系统下某个应用可以访问 /dev/ttyUSB0需要写类似下面的 te 规则allow system_app tty_device:chr_file { open read write ioctl }; allow system_app tty_device:dir { search };CAN socket 的规则要更复杂可能还需要放行 netlink 和 can 相关 class。做这些之前强烈建议先统一跑一遍 CTS/GTS 之外的权限回归因为 sepolicy 改得太宽系统服务很容易被第三方恶意应用利用。6.3 USB 系统 API 的状态查看与运维工具车机 USB 状态可以用dumpsys usb查看。它会打印当前 USB 连接模式、设备列表、PID/VID 等信息。遇到“设备插上但应用收不到广播”的情况第一件事就是执行adb shell dumpsys usb adb shell dumpsys input | grep -A 10 USBdumpsys usb里能看到 Android 系统视角的 USB 状态dumpsys input能看到 HID 设备是否被 input 子系统接收。两者配合起来就能快速定位问题出在 USB 枚举阶段还是 input 分发阶段。另外车机的电源管理对 USB 外设影响很大。很多车机休眠后USB Host 端口会掉电USB 设备直接断开这时候需要系统层通过UsbPortManager重新配置或者唤醒 Host。如果你的外设特殊建议定时轮询UsbManager.getDeviceList()来做视觉辅助恢复而不完全依赖热插拔广播。7. 常见问题与排查技巧实录7.1 快速定位的常用命令把下面这几个命令收藏起来它们能覆盖 80% 的车载 USB 问题# 查看 USB 枚举日志 adb shell dmesg | grep -i usb # 查看 sysfs 下的 USB 设备节点 adb shell ls -l /sys/bus/usb/devices/ # 查看某个设备的 vendor id adb shell cat /sys/bus/usb/devices/1-1/idVendor # 查看系统当前的 USB 配置 adb shell getprop sys.usb.config # 查看输入设备 adb shell dumpsys input | grep -E USB|HID如果你发现 dmesg 输出一大片device descriptor read/64, error -71一般就是供电不足或者信号线接触不良。如果 vendor id 能读出来但 Android 应用层拿不到设备多半是权限或者 driver 冲突。7.2 周边问题排障速查表下面这张表是我在各种项目里反复用到的整理成表格方便现场快速对照问题现象可能原因建议处理插上设备没有权限弹窗设备没有匹配 device_filter 或权限被系统记录拒绝检查 VID/PID清除 USB 权限记录权限已授予但打不开设备接口被内核驱动或其他应用占用dumpsys usb 查占用必要时强制卸载驱动USB 串口打开后立即断开供电不足DTR/RTS 触发复位检查供电调整 DTR/RTS设置 DTR false串口收到乱码波特率不匹配、地线不共地核对对方波特率检查接线CAN 接口配置失败内核没开 CAN或缺少模块检查 /proc/config.gzmodprobe 驱动CAN 能收不能发缺少终端电阻或总线错误万用表量总线确认 120 欧终端电阻HID 按键无反应映射表缺失或 report descriptor 问题用 getevent 确认原始事件检查键位映射系统休眠后 USB 设备失联Host 端口掉电设备未重新枚举系统层配置唤醒应用层增加轮询恢复7.3 现场调试的一些土办法如果手边没有高端 USB 协议分析仪也不要着急。很多问题靠几个顺手的小工具就能定位。USB 转串口模块 串口调试 App 是我在车上用得最多的组合。拿一根 CH340 模块插到车机的 USB 口打开串口 App 自测收发如果都不通说明车机的 USB Host 配置或供电有问题和你写的应用无关。如果串口能通再接你要调试的 CAN 盒子或 HID 设备就能把问题切得很干净。还有一个小技巧事先交叉编译一份静态编译的 can-utils 放到车机上比如 candump、cansend、isotprecv。这些工具在嵌入式 Linux 车机里经常没有但有了它们调试 CAN 的速度会快很多。编译的时候注意架构车机大概率是 aarch64不要拿 x86 的二进制直接用。最后再给一个建议如果你正在做一个车载 USB 相关的新项目我建议你在动手写应用之前先把下面三件事做完: 画出车机 USB 口的完整拓扑确认每个口的供电能力和是否走 USB Switch确认内核里有你需要的驱动和 SELinux 策略准备一台 userdebug 版本的机器用来现场验证底层问题。很多同事喜欢在一个 Android 普通版车机上调 USB-CAN 或 HID遇到问题就认为是应用代码写错了实际上底层根本没有提供相应能力。先确认“底层通不通”这件事比我下面分享的任何代码技巧都重要。USB 这类工程师经验性很强的问题养好习惯比背一万行 API 实在得多。