行业资讯
📅 2026/9/7 7:01:00
Android Java实现CAN通信Demo:USB转CAN适配器与slcan协议实战
简介面向Android平台Java开发者的CAN通信实战示例覆盖从CAN协议基础到can4android库集成的完整实现链路可广泛适用于汽车电子、工业控制、物联网设备交互等多种场景。包内共337个文件体积仅2.14MB包含Java源码、class字节码、APK安装包、Gradle配置文件、XML布局、JSON资源等可直接导入Android Studio运行和拆解方便从源码层面理解设备发现、端口打开与参数配置流程。已有1048人学习浏览。借助该示例可以学会构造标准帧与扩展帧完成设备连接、波特率配置、消息发送接收与回调处理并掌握通信异常、设备断开等错误处理思路。示例代码内含各模块注释与资源索引使用时需关注后台线程与UI线程的调度避免阻塞界面适合正在开发车机互联或工业采集应用的工程师参考复用能有效缩短CAN功能的上手周期。 做Android开发的人一听到“车规级”三个字多少都会有点发怵。调CAN总线这件事放在电脑端不算稀奇但要在手机上跑通一个CAN通信demo坑是真不少。这篇文章把我最近在Android上用Java开发CAN通信demo的完整思路和踩坑过程整理出来重点不是背书CAN协议的大部头而是讲清楚“怎么在Android设备上把一帧CAN报文发出去、再接回来”。如果你正在做车辆诊断、工业设备控制、机器人通信这类项目或者单纯想在面试时把CAN通信讲得不那么虚这篇都值得看完。先说我的结论Android侧做CAN通信最省心也最容易复现的路线就是“USB转CAN适配器 串口协议 Java层收发”不需要碰JNI不需要自己焊板子一个USB Host接口就能搞定。下面我把环境准备、协议核心、完整demo代码、调试方法挨个拆开讲。1. 先搞清楚这个demo到底解决什么问题1.1 为什么在Android上做CAN通信这么“别扭”Android系统的定位是消费级设备官方压根没给普通App开放直接操作CAN控制器的能力。你既不能像单片机那样直接往寄存器里写数据也不能像在Linux桌面端那样随便打开SocketCAN。想绕过去无非三条路外挂USB转CAN适配器通过USB Host接口读读写写这是目前兼容性最好、最不折腾的路子用蓝牙/WiFi转CAN网关绕开USB驱动问题但要额外买一个网关盒子延迟稍微高一些自己写内核驱动或者魔改系统工程量大到不值得为demo去做。我这次选的就是第一条路。适配器这边跑的是开源的slcan固件相当于把CAN控制器虚拟成了一个串口设备。Android这边只需要用现成的串口库跟它对话数据收发就完成了。整个过程纯Java可跑对初学者非常友好。1.2 技术路线选型USB-CAN适配器 slcan协议纯Java不碰JNI为什么强调“不碰JNI”很多厂商的USB-CAN适配器都只提供Windows SDKAndroid端要调用就得自己用JNI包装底层库光是交叉编译就够喝一壶。而slcan固件的思路完全不同它把CAN报文编码成了ASCII字符串底层就是一条普通串口Android只需要打开串口、发字符串、收字符串即可。我的开发顺序是这样的准备一个USB-CAN适配器刷好slcan固件在Android里引入usb-serial-for-android串口库枚举USB设备并申请权限打开串口发送slcan命令设置CAN波特率、打开通道组帧发送、开线程收帧完成。这套方案真正做到了“协议在固件里逻辑在Java里”排查问题也方便用PC端串口工具连上同一个适配器能看到同样的字符串问题出在哪一层一目了然。1.3 需要的硬件和软件环境硬件方面我用了这几样东西Android设备最好带USB Host功能手机和平板基本都支持USB-CAN适配器支持slcan固件的开源板子很多选一个带标准9针或DB9接口的即可CAN收发器板很多适配器已经集成了不用单独准备120Ω终端电阻排查总线问题的时候必备USB OTG转接线手机和适配器之间连接用。软件方面Android Studio JDK环境是基础项目里加一个依赖就行。环境变量JAVA_HOME这些该配还是得配不然AS正常跑起来都是问题。另外如果你的适配器是国产方案最好先确认固件是否支持slcan命令有些需要先用PC工具切换模式。2. 动手前必须搞懂的CAN核心概念2.1 报文帧格式标准帧、扩展帧、远程帧CAN报文最核心的规则就是“一条报文带一个ID加最多8字节数据”。标准帧的ID是11位范围0x000到0x7FF扩展帧的ID是29位范围大得多。8字节的数据是固定上限虽然CAN FD已经在推更高负载但普通CAN通信demo用8字节完全够。slcan协议里发送标准帧用字母t开头扩展帧用T开头。比如t1238DEADBEEF01020304\r的意思是ID0x123DLC8后面是8字节数据。DLC是十六进制的8如果数据只有4字节DLC就写4后面跟8个十六进制字符。字节顺序按大端来先发的数据在字符串前面这个顺序别搞反了。有一个新手容易忽略的点DLC后面的十六进制字符串长度必须是DLC的两倍如果数据不足要做到左补齐还是右补齐要跟对面设备约定好。实际项目里遇到过两边对数据解析不一致导致功能异常的情况后来统一成“数据左对齐低字节在前”才解决。2.2 波特率决定一切不是随便填个数字CAN通信对时序要求非常严格总线上所有节点的波特率必须一致否则数据收不下来。slcan固件里用S命令切档位比如S6\r对应500kbpsS5\r对应250kbps不同固件的映射表可能会调整拿到适配器先看固件文档。我之前犯过一个错把串口的波特率也当成CAN波特率来设。串口这边的波特率只是USB适配器和Android之间传ASCII字符串的速率一般是115200它跟CAN总线上的波特率是两码事。CAN波特率由适配器固件负责对上层完全透明。这两个概念分开以后调参就清晰多了。常见车载网络用的波特率是500k和250k如果终端设备不给文档可以通过示波器量总线上的位时间推出来后面我会专门说波形怎么看。2.3 过滤器ACCCode和ACCMask在说什么如果你在别的平台调过CAN会看到ACCCode和ACCMask这两个参数。ACCCode是验收码ACCMask是屏蔽码。屏蔽码为1的位必须和验收码匹配为0的位不关心。比如你想接收ID0x123就把验收码设为0x123屏蔽码设成全1想接收所有帧就把屏蔽码设成全0。在Android USB-CAN适配器的方案里过滤通常由适配器固件完成上层不用管。但如果你是在MCU端做开发这两个参数直接影响能否收到目标报文。我建议写demo的时候先不过滤全收等逻辑跑通了再按ID筛数据。2.4 demo代码结构一个Activity跑起来但别全堆在里面一开始我也图省事把所有内容写在一个Activity里后来发现权限回调、收帧线程、界面更新全搅在一起很难维护。第二版拆成了三个类CanController负责USB设备枚举、权限、串口打开关闭和收发命令CanFrameParser负责把slcan收上来的字符串解析成ID、DLC、数据MainActivity只做界面展示和按钮事件。对于demo来说这样的结构已经足够了。别一上来就上MVVM或者协程先把流程跑通后面再优化不迟。3. demo关键代码实现细节3.1 用UsbManager拿到USB设备并申请权限Android这边先要拿到USB设备对象。我直接用UsbManager枚举出所有连到手机上的设备再靠VID/PID判断哪个是CAN适配器UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice devices usbManager.getDeviceList(); UsbDevice target null; for (UsbDevice device : devices.values()) { Log.d(CAN, pid device.getProductId() vid device.getVendorId()); // 示例VID 0x1d50, PID 0x606f 是常见开源CAN适配器 if (device.getVendorId() 0x1d50 device.getProductId() 0x606f) { target device; } } if (target null) { // 提示用户插好USB-CAN适配器 return; } if (!usbManager.hasPermission(target)) { PendingIntent pi PendingIntent.getBroadcast(this, 0, new Intent(com.demo.CAN_PERMISSION), 0); usbManager.requestPermission(target, pi); return; }一个实际体会不要只靠USB设备枚举就完事有些手机在USB插上以后会弹授权框不点允许是拿不到设备句柄的。建议在onResume里重新检查权限避免插拔后权限丢失。3.2 打开串口并配置参数拿到设备之后用usb-serial-for-android库打开串口。打开前要先建立USB设备连接这个连接在整个收发过程中都不能关一旦关闭会收到一堆异常。UsbDeviceConnection connection usbManager.openDevice(target); UsbSerialDriver driver UsbSerialProber.getDefaultProber().probeDevice(target); if (driver null) { // 认不出设备确认固件是否是slcan模式 return; } UsbSerialPort port driver.getPorts().get(0); port.open(connection, 115200); port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);打开之后立刻发送slcan初始化命令port.write(S6\r.getBytes(UTF-8), 1000); // 设置CAN波特率为500k port.write(O\r.getBytes(UTF-8), 1000); // 打开CAN通道这里有一点有些固件要求\r\n结尾而不是\r如果你的适配器没反应先接PC串口工具看一眼到底是什么换行符。3.3 发送一帧CAN报文发送的核心就是把ID和字节数组拼成一个符合slcan格式的字符串然后写到串口。我封装了一个方法private void sendFrame(UsbSerialPort port, int id, byte[] data) throws IOException { int dlc data.length; if (dlc 8) dlc 8; StringBuilder sb new StringBuilder(); sb.append(t); sb.append(String.format(%03X, id 0x7FF)); // 标准帧只用11位ID sb.append(String.format(%01X, dlc)); for (int i 0; i dlc; i) { sb.append(String.format(%02X, data[i])); } sb.append(\r); port.write(sb.toString().getBytes(UTF-8), 1000); }实际调用时sendFrame(port, 0x123, new byte[]{0x01, 0x02, 0x03, 0x04})生成的字符串是t123401020304\r。这里最容易出错的是ID超过0x7FF字符串长度变成4位固件就会把第一字节当DLC解析数据全乱套。建议在方法入口加一层校验越界直接抛异常。3.4 接收数据的读取线程和帧解析串口读取是阻塞式必须放到子线程里。我开了一个while(running)循环持续往缓冲区读数据读到之后转成字符串丢给界面。slcan收到真实报文时固件会以t或T开头的字符串返回所以解析也比较简单new Thread(() - { byte[] buffer new byte[256]; while (running) { try { int len port.read(buffer, 1000); if (len 0) { String line new String(buffer, 0, len, UTF-8); runOnUiThread(() - logView.append(line \n)); } } catch (IOException e) { e.printStackTrace(); } } }).start();解析帧的时候我踩过一个坑固件返回的字符串可能不是一帧一行偶尔会把两帧报文拼在一个read里或者一帧报文被拆成两次read。如果按“读一行解析一行”来写会漏帧。稳妥的做法是先开一个StringBuilder做粘包缓存按\r切分后再逐条解析。解析的代码思路就是把t后面的前3个十六进制字符当ID第4个字符当DLC剩下的当数据。DLC小于8时数据也要按DLC来截取不要无脑取16个字符。这属于基础操作但一旦字长错了后面所有业务逻辑全白搭。3.5 界面层怎么显示收发状态demo界面我用了一个滚动的TextView来打印日志发送按钮调sendFrame接收线程把数据append到TextView。代码很简单但有个体验问题数据更新频繁时TextView不断刷新会卡顿。如果想让demo看起来更专业一点可以改用RecyclerView或者配合LiveData把这些数据流放到列表里。不过demo阶段完全没必要追求界面复杂度。能看到字符串收发你的CAN链路已经通了。4. 调试实录常见问题和排查技巧4.1 回环测试能过标准模式发不出去这个情况我在网上见到很多次自己也中过招。回环模式是MCU控制器内部把发送数据直接送回到接收邮箱根本不经过CAN收发器也不经过总线。所以回环能过只能说明控制器逻辑没问题物理层完全是另一码事。如果你在标准模式下发不出去按下面的顺序排查检查CAN收发器芯片是否正常供电和使能很多收发器的STB引脚默认接地会进入待机模式确认CANH和CANL两条线没有接反这是最基础但也最常见的错误量一下总线端的电压显性位差分电压应该在2V左右如果没有说明收发器没有把信号驱动到总线上看终端电阻总线两端都要有120Ω缺了终端电阻信号反射严重数据根本发不出来。回环测试正常但标准模式失败这种情况90%都是物理层问题别一上来就怀疑协议代码。4.2 能发不能收多半是滤波和终端电阻的问题demo能发不能收先分清楚是“对方收到没有”还是“自己收不到对方的回复”。如果是后者优先级最高的怀疑对象是接收过滤和终端电阻。终端电阻的排查方式很直接用万用表量总线两端正常应该在60Ω左右两个120Ω并联。如果量出来是120Ω说明另一端没接电阻总线上只有一个节点在工作。很多CAN适配器内部默认不焊终端电阻需要你在外部并联一个120Ω电阻。至于ACCCode/ACCMask就像前面说的在MCU端直接影响能不能收到指定ID的报文。排查的时候可以先关掉所有过滤把总线上所有帧全收下来看一眼。以前在一个项目里我们还在用“码分多址”的方式对设备分组结果验收码和屏蔽码配置错了一位导致连续几分钟收不到数据后来是拿总线分析软件对比了原始ID才定位到问题。4.3 波形怎么看判断通信好坏的最直接办法说真的调CAN通信最好用的调试工具不是代码也不是日志而是示波器。示波器探头夹到CANH和CANL上观察差分波形。正常的CAN差分信号长这样隐性位CANH和CANL都约2.5V差分电压为0显性位CANH升到3.5V左右CANL降到1.5V左右差分电压约2V。波形质量的判断标准一个是看边沿是否干净有没有回勾或台阶另一个是量单个bit的时间。500kbps速率下一个bit应该是2微秒250k是4微秒。如果你看到位宽明显偏大或偏小多半是波特率设置有问题先别管代码逻辑把时序对齐了再说。没有示波器的时候可以接上第二个CAN节点用软件监听看能不能收到正常数据帧。这方法不如波形直观但至少能确认物理层通不通。4.4 乱码、丢帧、时序错乱乱码大概率是波特率不匹配或者串口参数设置不对。slcan场景下串口波特率固定115200没变过一般不会乱如果乱码只出现在CAN数据区建议先打印原始字符串确认数据长度是否跟DLC一致。丢帧的问题集中在读取线程。如果处理读到的字符串时做了耗时操作比如主线程阻塞、日志文件IO下一帧就丢了。这时候最有效的办法是加一个生产者消费者模式读取线程只负责把收到的完整字符串放进队列解析线程再从队列里取。时序错乱主要发生在多线程读写同一个串口实例。我为这个专门加了synchronized锁住写方法防止两个地方同时write导致报文交错。这个坑不好复现一旦出现偶尔间歇性数据错误多半就是竞争问题。5. 这个demo后续还能怎么扩展当前这套demo跑通以后可以顺着这几个方向继续深入把解析和发送逻辑改成封装好的SDK通过Listener回调上抛数据帧这样业务层就不用关心CAN细节了增加多帧发送队列和定时轮发机制模拟周期报文用于测试对方设备的稳定性和时序支持扩展帧区分t和T两条路径把29位ID场景也覆盖掉把固定ID改成动态配置做一个简单的“发送配置页”方便不同项目间复用对接数据库或者云平台把采集到的CAN数据直接上报做远程监控或故障分析。我个人实际操作下来最深的体会是CAN通信demo的技术难度其实不在Android端也不在Java而在你对“物理层、控制器、协议”这三层模型是不是足够清晰。很多问题看着像软件bug最后都出在硬件和配置上。建议你先拿示波器确认波形再怀疑并发和粘包排查路径会顺畅很多。最后再分享一个小技巧调试时用PC端的串口助手接同一个适配器和手机App同时观察收发字符串能省掉大量来回猜的过程。这比日志打印高效得多遇到奇怪问题就试试这个办法。本文还有配套的精品资源点击获取