做机器人底盘、无人配送车这些项目时,Jetson Orin NX几乎成了标配计算平台,算力、外设、生态都没得挑。但每次接到CAN总线的需求,很多人包括我自己,第一步就会卡住:Orin NX模块上压根没有原生的CAN控制器。底盘电机、BMS电池包、车辆VCU、激光雷达里那些工业设备,十个里有八个是走CAN的,绕不开。这篇文章是我在Orin NX上从零调通CAN的完整记录,把硬件焊接、模块选型、内核驱动、SocketCAN配置、systemd开机自启动、常见报错排查一条龙串起来讲,适合正在做机器人和车载项目的朋友直接照抄。
1. 为什么Orin NX没有原生CAN:三种外扩方案怎么选
1.1 Orin NX的接口现状与CAN总线的实际地位
NVIDIA的Jetson系列模块,设计目标始终是“AI计算平台”,板子上把SPI、I2C、UART、USB、PCIe、GPIO、网口都铺得很全,唯独没把CAN控制器做进SoC里。这和STM32、瑞萨这些传统车规MCU完全不同,人家是出厂就带CAN外设,改改寄存器就能用。所以拿到Orin NX,首要任务是搞清楚一点:不是“Jetson不能接CAN”,而是“Jetson没把CAN控制器集成进来”,必须在外部配上CAN控制器加收发器,通过SPI或者USB把数据桥接进去。
这个认知很重要,它决定了后面所有选型方向。CAN控制器负责协议层,比如帧格式、仲裁、错误检测;CAN收发器负责物理层,把控制器输出的逻辑电平转换成CAN_H和CAN_L之间的差分信号。任何一个缺失,总线都跑不起来。
1.2 三种主流外扩方案对比
| 方案 | 典型模块 | 接线复杂度 | 驱动方式 | 稳定性 | 推荐场景 |
|---|---|---|---|---|---|
| USB-CAN | CANable、PCAN-USB、创芯科技USB-CAN | 即插即用 | gs_usb / peak_usb | 中等 | 调试、样机验证、多平台复用 |
| SPI-CAN | MCP2515、MCP2518FD模块 | 需接SPI、中断、电源 | mcp251x / mcp251xfd | 高,取决于设备树配置 | 产品集成、底盘量产 |
| M.2转CAN | M.2 Key B / A+E载板模块 | 需确认载板信号定义 | 同SPI或USB驱动 | 高 | 定制载板、前装方案 |
USB-CAN是调试阶段最省心的方案。比如CANable这种基于gs_usb协议的小板子,Linux内核自带gs_usb驱动,插上就能识别成can0,不需要碰设备树。PCAN-USB则走peak_usb驱动,同样即插即用。缺点是USB链路经过Hub之后,延迟和数据吞吐会受一点影响,做高负载测试时可能成为瓶颈。
SPI-CAN是产品化常用的路线。MCP2515模块几块钱到十几块钱一块,网上随便买,接到Orin NX的40-pin排针上就行。但是要让内核在SPI总线上正确识别这块芯片,必须改设备树,把MCP2515注册成SPI控制器的一个子节点。这部分工作主要是一次性的,做好了以后每次开机动静态加载都很稳定,不占USB口,也适合塞进机箱内部。
M.2方案更多是配合定制载板。官方开发套件的M.2 Key M槽主要给NVMe SSD用,多数人不会拿它接CAN。如果你们公司自己画载板,可以考虑在M.2 Key B上引出一路CAN,走PCIe或USB信号,这样整机更紧凑。对于多数开发者来说,这个方案不常用,知道有这条路就行。
1.3 我的选型建议
我自己在项目里是两套都备着:调试时用USB-CAN,插上就跑;最终装机用MCP2518FD通过SPI接入,支持CAN FD,数据量大的时候有余量。如果你手头已经有一块USB-CAN模块,先别买新硬件,直接拿它把链路跑通,后面再根据产品形态决定要不要上SPI方案。注意有些山寨USB-CAN用的是CH340加SLCAN固件,Linux下不会自动出现can0,需要手动slcand处理,买之前最好确认卖家是否支持gs_usb。
2. 硬件动手:焊接、接线与终端电阻的验证方法
2.1 MCP2515模块焊接细节与引脚分配
如果你选的是MCP2515这类SPI模块,到手之后首先面对的是焊排针。焊接本身不难,但有几个细节值得注意:
- 烙铁温度设定在350℃左右,不要太高,MCP2515模块背面往往贴着TJA1050或者SN65HVD230这类CAN收发器,过热容易把贴片件烫出虚焊。
- 先把排针焊好,再焊其他需要连接的线,不要反复加热同一个焊盘。
- 焊接完成后用万用表通断档检查相邻引脚有没有连锡,尤其是SPI那几根线,短路一次可能把芯片烧掉。
接线方面,以官方载板和常见MCP2515模块为例,大致对应关系如下:
| MCP2515模块引脚 | Jetson 40-pin排针 |
|---|---|
| VCC(3.3V/5V) | 3.3V或5V,看模块稳压设计 |
| GND | GND |
| SCK | SPI0_SCK |
| MOSI(SI) | SPI0_MOSI |
| MISO(SO) | SPI0_MISO |
| CS | SPI0_CS0 |
| INT | 任意空闲GPIO |
INT脚是MCP2515的中断输出,低有效。接到Jetson这边必须用支持输入的GPIO,并且在设备树里正确声明中断号。很多新手把INT漏接或者接到输出脚上,导致内核能枚举到SPI设备,但收不到任何CAN帧,排查起来特别隐蔽。另外,如果模块是5V供电的,它的SPI逻辑电平可能也是5V,直接怼到3.3V的Jetson GPIO上有风险,建议优先选3.3V版本模块,或者加个电平转换。
2.2 CAN_H、CAN_L双绞线与公共地
CAN物理层用差分信号,CAN_H和CAN_L必须是一对双绞线,不能随便拉两根平行线凑合。双绞的目的是抗共模干扰,工业现场有电机、变频器这些强干扰源,线缆不规范很容易出随机错误帧。
接线时记住一个原则:CAN_H接CAN_H,CAN_L接CAN_L,不要交叉。听起来像废话,但实际项目里我还真遇到过有人把两个节点之间的CAN_H和CAN_L接反了,结果两边都能初始化,数据就是不通。
另外一个工程上容易被忽略的点是公共地。CAN虽然是差分传输,不依赖地线做信号参考,但收发器的共模电压范围有限,如果两个节点之间没有公共地,CAN_H和CAN_L的静态电平会漂移,轻则误码,重则直接通讯失败。所以总线里面除了CAN_H、CAN_L这两根信号线,最好再拉一根GND,把各个节点的地电位钳住。
线缆长度和分支长度也有讲究。500kbps速率下,从总线主干到节点的“拖尾线”最好控制在0.3米以内。总线上应该是“手拉手”串联结构,而不是星形结构。实在避不开星形接法,只能降速率用,比如125kbps甚至更低,实测才稳。
2.3 万用表验证120Ω终端电阻
CAN总线规范要求在物理线路的两端各接一个120Ω终端电阻,用来匹配阻抗、消除信号反射。实际操作中最常见的问题,不是没接终端电阻,就是接了三个、四个,或者干脆接错位置。
上电之前,用万用表电阻档直接量CAN_H和CAN_L之间:
- 读数约60Ω:两端各一个120Ω,标准情况,正确。
- 读数约120Ω:只有一端有120Ω,另一端没接。短距离调试时还能用,但长距离或者现场环境复杂时建议补上。
- 读数特别小,比如40Ω以下:终端电阻过多,节点阻抗太低,驱动能力被拉垮。
- 读数无穷大:两端都没有终端电阻,这是错误帧泛滥的头号原因。
量的时候务必断掉总线上所有设备的电源,不然量出来的是电路板上的等效电阻,没有参考意义。有些模块板上自带终端电阻跳线,比如CANable就有120Ω跳线帽,用之前先确认是插上还是拔掉,别和其他节点的电阻重复。
2.4 示波器看CAN波形的基础手法
如果你手头有示波器,把探头地接GND,分别量CAN_H和CAN_L。正常高速CAN总线在静态(隐性)时,两根线都接近2.5V;有数据帧时,CAN_H会拉到3.5V左右,CAN_L会拉到1.5V左右,形成2V的差分摆幅。
调通之前先量一下静态电平,如果CAN_H不是2.5V附近,而是明显偏低或偏高,基本可以断定供电或者接地有问题。波形确认之后,再用示波器量最短脉冲宽度,算一下实际波特率:500kbps时一个显性位宽度是2微秒,250kbps是4微秒。这个方法比猜配置快得多。
3. 内核驱动、设备树与SocketCAN工具链
3.1 SPI-CAN方案的内核模块与设备树配置
在Orin NX上让MCP2515工作,核心是把芯片注册到Linux的CAN子系统里。先确认内核有没有编进mcp251x驱动:
find /lib/modules/$(uname -r) -name '*mcp251x*'如果查不到模块,说明当前内核没编译这个驱动,需要从NVIDIA官网下载对应JetPack版本的BSP源码,把CONFIG_CAN_MCP251X或CONFIG_CAN_MCP251XFD打开,重新编译内核。折腾内核属于一次性的活,完成后一劳永逸。
驱动有之后,还要在设备树里把MCP2515挂到SPI控制器下面。Jetson的设备树管理和树莓派不太一样,通常是在BSP源码里修改dts/dtsi,编译生成新的dtb再刷到启动分区。设备树片段大致长这样:
&spi0 { status = "okay"; mcp2515: mcp2515@0 { compatible = "microchip,mcp2515"; reg = <0>; spi-max-frequency = <10000000>; clocks = <&clk24m>; interrupt-parent = <&gpio>; interrupts = <TEGRA_GPIO(X, Y, GPIO_INT_LOW)>; pinctrl-names = "default"; }; };这里有几个容易踩的坑:interrupts里GPIO的编号和你实际接的INT引脚必须一一对应,否则驱动收不到中断,表现就是CAN接口能起来、能发不能收;spi-max-frequency如果设太高,比如超过10MHz,SPI线稍微长一点就会出现CRC错误,调试阶段先降到1MHz排查问题更稳妥。
设备树配置完成并重新刷机后,开机看一眼设备是否注册成功:
ls /sys/bus/spi/devices/ dmesg | grep -i mcp能看到spi0.0之类的节点,而且dmesg里没报错,才算把内核这一层打通。
3.2 USB-CAN方案的即插即用
USB-CAN就轻松很多。以CANable为例,烧录candlelight固件之后,插上USB线,运行:
dmesg | tail -30 ip linkdmesg里会出现类似gs_usb的驱动加载信息,ip link里面多出can0接口。直接就能用can-utils工具收发。如果插上之后没有任何反应,先看lsusb辨认设备VID/PID,再看内核有没有gs_usb模块。
有些USB-CAN模块出厂固件是SLCAN模式,Linux不认,需要先把固件刷成candlelight。如果不想刷固件,也可以通过slcand把串口设备虚拟成CAN接口,但那套流程复杂,不如刷固件来得干净。我自己的建议是:凡是能在Linux下原生识别成SocketCAN接口的USB-CAN,优先买这种。
3.3 can-utils常用命令速查
装工具:
sudo apt install can-utils配置并开启CAN接口:
sudo ip link set can0 up type can bitrate 500000 restart-ms 1000restart-ms 1000的意思是,如果控制器进入Bus-Off状态,1秒后自动重启恢复,这个参数在无人值守的机器人上非常实用,后面讲systemd时还会用到。
看接口状态:
ip -details link show can0这条命令会显示CAN接口是ERROR-ACTIVE、ERROR-PASSIVE还是BUS-OFF,排查总线问题时第一位要看的就是这个。
发送一帧数据:
cansend can0 123#DEADBEEF接收并打印时间戳:
candump -tz can0接收并显示错误帧:
candump -e can0周期发送用于压力测试:
cangen can0 -g 10cangen会以10毫秒间隔循环发随机帧,做稳定性测试时很好用,但注意别在已运行的业务总线上乱开,会把总线打爆。
3.4 中断接收还是DMA接收:mcp251x与mcp251xfd的驱动差异
很多从STM32转到Linux的人会问,CAN接收到底用中断还是DMA。在Linux SocketCAN框架下,这个问题被驱动层封装了,但不同芯片的驱动策略确实有区别。
经典MCP2515的驱动走的是中断加SPI同步读:CAN控制器收到一帧数据,INT脚拉低,触发内核中断处理函数,驱动再通过SPI把接收缓冲区的数据读回来。这种设计简单可靠,但在总线负载很高时,每个中断都要做一次SPI事务,CPU占用率会比较明显。500kbps的经典CAN,理论上总线最多每秒跑三千多帧8字节报文,MCP2515勉强够用,但已经是它的能力上限附近了。
MCP2518FD配合的mcp251xfd驱动则先进不少,芯片内部带FIFO,驱动支持一次DMA搬移多帧数据,中断频率大幅降低。如果做CAN FD,数据段速率到了2Mbps甚至更高,MCP2515基本顶不住,直接选MCP2518FD就对了。选型时别只看模块价格,把未来数据量预估一下,避免后期换方案。
3.5 CAN总线负载率怎么算
网上搜“CAN总线负载率计算”的朋友,多半是遇到了“总线老出错误帧,怀疑是太忙了”的情况。负载率的定义很简单:单位时间内总线上实际传输的比特数除以比特率。
一帧经典CAN标准帧,8字节数据,开销大约是:帧起始1位、仲裁段11位、控制段6位、数据段64位、CRC段15位、CRC分隔符1位、ACK段2位、EOF 7位、帧间隔3位,加起来110位,再加上位填充规则平均约20到30位,一帧下来大概128到130位。
举个例子:总线上1秒传输100帧8字节报文,波特率500kbps,负载率就是:
100 × 130 ÷ 500000 × 100% ≈ 2.6%
如果每秒传2000帧,负载率约52%。实际工程经验是,负载率超过80%,总线碰撞和错误重传会明显加剧,延迟不可控。算清楚负载率,才能合理规划各节点报文的发送周期,这是CAN工程设计里很重要的一步。
4. systemd服务实现开机自启动与Bus-Off自动恢复
4.1 为什么不能直接往rc.local里塞命令
很多老玩家习惯在/etc/rc.local里写一句ip link set can0 up,在Jetson上这个做法不靠谱。rc.local的执行时机很靠前,不能保证USB设备枚举完成,如果用的是USB-CAN,执行时can0可能根本还没出现,自然报“Cannot find device can0”。就算这次碰巧成功了,下次换了个USB Hub或者多插了个设备,时序一变,启动就失败。
正确做法是交给systemd,用Unit文件管理依赖关系,在服务里加一个轮询重试逻辑。
4.2 编写一个带重试机制的can0服务
先写一个启动脚本,放在/usr/local/bin/can-up.sh:
#!/bin/bash for i in $(seq 1 10); do if /sbin/ip link show can0 > /dev/null 2>&1; then /sbin/ip link set can0 up type can bitrate 500000 restart-ms 1000 exit 0 fi sleep 1 done exit 1给脚本加上执行权限:
sudo chmod +x /usr/local/bin/can-up.sh然后创建systemd服务文件/etc/systemd/system/can0.service:
[Unit] Description=Configure CAN0 interface at boot After=network.target Wants=network.target [Service] Type=oneshot RemainAfterExit=yes ExecStartPre=/sbin/modprobe can_dev ExecStartPre=/sbin/modprobe can_raw ExecStartPre=/sbin/modprobe gs_usb ExecStart=/usr/local/bin/can-up.sh [Install] WantedBy=multi-user.target几个关键点解释一下:
Type=oneshot表示这个服务只跑一次命令就退出。RemainAfterExit=yes让systemd认为服务执行成功后仍然保持active状态,系统不会反复拉起它。ExecStartPre先把CAN相关内核模块加载上,gs_usb是针对USB-CAN的;如果你用的是SPI的MCP2515,把gs_usb换成mcp251x或者mcp251xfd。- 脚本里轮询10次,每次1秒,给USB设备枚举留出足够时间。
启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable can0.service sudo systemctl start can0.service验证状态:
systemctl status can0.service ip -details link show can0重启系统之后,can0应该自动处于UP状态,candump can0能正常监听总线。
4.3 用udev规则固定设备名防止can0和can1漂移
当系统里插了多块USB-CAN,或者同时有USB-CAN和SPI-CAN时,接口命名顺序不固定,can0和can1可能每次重启都交换,业务代码里引用can0就全乱了。这时可以用udev规则把设备名和物理USB端口绑定。
比如你的USB-CAN总是插在某个固定端口,通过查询设备所在的USB路径:
udevadm info -a -n can0 | grep -i path然后在/etc/udev/rules.d/50-can.rules里写:
ACTION=="add", SUBSYSTEM=="net", SUBSYSTEMS=="usb", KERNELS=="1-1.2", NAME="can0" ACTION=="add", SUBSYSTEM=="net", SUBSYSTEMS=="usb", KERNELS=="1-1.3", NAME="can1"KERNELS后面的1-1.2、1-1.3是你的USB端口路径。这样每次插同一个物理口,can0和can1永远不会漂移。修改完之后:
sudo udevadm control --reload-rules sudo udevadm trigger4.4 开机自启动的日志与问题确认
服务跑起来之后,用日志确认没有隐藏问题:
journalctl -u can0.service -b如果看到脚本执行成功但接口没起来,多半是ip link命令里的bitrate参数和底层驱动不兼容,或者modprobe阶段就失败了。日志里都会有线索,别只看一眼“active (exited)”就完事。
工业现场如果担心CAN接口因为总线问题反复掉线,还可以再配一个定时检查的systemd timer,每隔几秒查一次接口状态,发现DOWN就重新up。不过一般restart-ms已经能处理Bus-Off,定时器属于双保险,看项目要求加。
5. 常见问题排查链路:从“看不到设备”到“全总线错误帧”
5.1 插上USB-CAN后看不到can0
排查顺序:
lsusb确认硬件是否被识别。看不到设备,先换USB线、换USB口,排除供电和线材问题。dmesg | tail -30看内核有没有枚举信息。如果报device descriptor read/64, error -71这类错误,大概率是USB供电不稳或者模块坏了。- 设备枚举正常但没有can0,执行
modprobe gs_usb或modprobe peak_usb,确认驱动加载情况。 - 模块是SLCAN固件的,
lsusb能看到串口芯片,但系统里只有ttyUSB0没有can0,要用slcand挂载,或者直接刷candlelight固件。 - 以上都正常还是不行,换一台普通Linux电脑插上试试。如果其他电脑能识别,说明Jetson的内核裁剪掉了对应驱动;如果其他电脑也不行,模块固件大概率有问题。
5.2 SPI-CAN注册失败的内核日志分析
SPI方案出问题,先抓内核日志:
dmesg | grep -i "mcp251x\|spi"常见的报错和对应原因:
| 日志关键字 | 大概率原因 |
|---|---|
failed to load message | SPI接线错误,MISO/MOSI接反或CS接错 |
probe ... failed with error -110 | 与MCP2515通信超时,检查INT引脚、供电和复位电路 |
CRC error | SPI信号质量差,降spi-max-frequency或缩短杜邦线 |
| 完全没有probe日志 | 设备树节点没生效,重新检查dtb是否刷入 |
SPI线如果超过10厘米,尽量用杜邦线直接短接,不要飞线绕来绕去,高频信号最怕乱绕。
5.3 错误帧刷屏与Bus-Off的完整排查链路
现象是candump -e can0疯狂打印错误帧,或者ip -details link show can0里状态变成BUS-OFF。按这个顺序查:
- 断电量CAN_H和CAN_L之间的电阻,目标60Ω。这一步能排除一大半物理层问题。
- 确认总线上所有设备的波特率一致。CAN没有自动协商,500k和250k混着接,双方都会报错。
- 检查CAN_H和CAN_L有没有接反。
- 一台一台断开总线上的节点,找到那个把总线拖死的设备。
- 试着降速率。如果500kbps错误不断,改到250kbps错误消失,说明线缆质量、终端电阻或者连接器不达标,先从物理层整改。
Bus-Off恢复,最简单的方式:
sudo ip link set can0 down sudo ip link set can0 up type can bitrate 500000 restart-ms 1000带restart-ms配置后,控制器会自动从Bus-Off状态恢复,省得每次手动干预。
5.4 能发不能收、能收不能发的经典原因
单独一个节点挂在总线上,用cansend发数据,会不断触发错误帧。原因是CAN协议要求发送节点必须收到至少一个其他节点的ACK响应,没有其他节点接收,就会因为没有ACK而持续重发,直到错误计数器爆掉进Bus-Off。这不是你发送配置错了,而是总线上根本没有“听众”。
验证单节点自发自收,用回环模式:
sudo ip link set can0 up type can bitrate 500000 loopback on cansend can0 123#DEADBEEF candump can0能收到自己发的帧,说明控制器、驱动、SocketCAN这一整条链路是通的。
如果多节点场景下能发不能收,重点查对方有没有发数据,以及CAN_H/L是不是交叉了。有些模块的连接器丝印不标准,不能只看颜色,有条件就用示波器量一下谁的CAN_H电压更高。
5.5 供电、地线和电平兼容的隐藏坑
最后一类问题最隐蔽,硬件连接都对、波特率也对,但偶发错误帧。优先排查供电:TJA1050这类收发器要5V供电,供电压降太大会导致显性电平摆幅不够;SN65HVD230支持3.3V,但3.3V电源纹波大时同样出问题。
其次是电平兼容。MCP2515模块如果用5V供电,SPI引脚按5V逻辑输出,直接接到Orin NX的3.3V GPIO上,短时间看着能通信,时间长了GPIO可能损坏。产品阶段务必用3.3V模块或者加电平转换芯片,别拿IO寿命赌稳定性。
我做过的项目里,最折磨人的一次是终端电阻用了三颗,本来应该是两个120Ω并联,结果PCB板上还焊了一颗,量出来只有40Ω,500kbps下随机错误帧持续了整整两天,最后用万用表量终端电阻才发现。所以硬件这层,真的别嫌基础,按规范一项项验完再碰软件,反而最快。