我第一次拿到那块RK3562J工控板时,最吸引我的不是四核A53,也不是那一堆工业接口,而是原理图上孤零零挂在SPI总线上的一颗MCP2518FD。做过CAN总线项目的人一看就懂:这颗芯片一出现,意味着这套系统要当CAN-FD网络里的高性能节点。当时我以为mainline内核已经有mcp251xfd驱动,适配不过是设备树加几行配置的事,真动手了才发现,从内核选项、设备树细节、中断触发方式到双机通信测试里的各种坑,远比想象中多。这篇文章就把我从拿到样板到CAN-FD数据段跑通的完整过程记录下来,包括所有配置、命令和踩过的坑,给后面用RK3562J外加MCP2518FD做项目的朋友做个参考。
1. 为什么选择RK3562J外挂MCP2518FD而不直接用原生CAN
1.1 从硬件设计图说起:一颗SPI转CAN-FD芯片的价值
先说结论:MCP2518FD本质上是把CAN-FD协议引擎搬到了SPI外设上。主控不需要原生CAN控制器,只需要一路SPI、一个GPIO中断、一个晶振,就能获得完整的CAN-FD节点能力。
RK3562J这颗处理器在工控、储能、机器人项目里很常见,但很多板卡设计上并没有把所有CAN-FD资源都引出来,或者原生控制器只支持CAN 2.0而项目又确实需要CAN-FD。这个矛盾在选型阶段就会暴露:换主控成本太高,多加一颗独立的CAN-FD控制器芯片反而是最稳妥的方案。
MCP2518FD的几个特点正好对得上这类需求:
- 数据段最高支持8Mbps,仲裁段兼容标准CAN和CAN-FD
- 内部集成完整的CAN-FD协议引擎,主控只负责通过SPI读写寄存器
- 自带发送队列和接收FIFO,高负载时不会把主控CPU拖死
- 支持TDC(收发延迟补偿),在高速率长线缆场景下比普通CAN控制器稳定得多
这颗芯片常见于储能BMS、工业网关、机器人控制器等场景。这类设备通常需要多个CAN节点,要求总线隔离、速率灵活,SPI外扩的方式在硬件上最灵活,出问题也容易单独替换。
1.2 RK3562J平台上的Linux CAN子系统与驱动现状
很多人的第一反应是:mcp251xfd驱动是不是要自己写?其实不用。Linux mainline里早就合入了MCP2517FD/MCP2518FD的驱动,代码路径在drivers/net/can/spi/mcp251xfd.c。RK3562J跑的是标准ARM64内核,只要内核配置里打开了对应选项,驱动就能直接编译。
所以“驱动适配”这个说法,准确来讲不是写驱动,而是做四件事:内核配置、设备树描述、硬件接线确认、CAN-FD链路验证。这里面难度最高、最容易翻车的其实是最不起眼的设备树和中断配置。
需要留个心眼的是CAN子系统的配置。MCP2518FD驱动本身只是控制器驱动,真正收发报文还要靠CAN协议栈里的RAW、BCM等协议模块。内核里这几项必须一起打开:
CONFIG_CAN:CAN总线子系统CONFIG_CAN_RAW:原始套接字协议,can-utils工具都依赖它CONFIG_CAN_BCM:广播管理报文协议,部分应用会用到CONFIG_CAN_DEV:CAN设备驱动框架CONFIG_CAN_MCP251XFD:MCP2517FD/MCP2518FD的SPI驱动
如果你的根文件系统不是自己编的,而是下载的Ubuntu镜像或者buildroot rootfs,还得确认系统里有can-utils工具集,里面包含candump、cansend、cangen这些测试命令。Ubuntu base系统直接apt install can-utils就行,buildroot的话在menuconfig里选上can-utils包再重新编译rootfs。
2. 适配前的三项准备:内核选项、设备树素材与硬件检查
2.1 内核编译:把mcp251xfd驱动选进去
拿到SDK源码后,第一步先把内核配置改好。不同厂商的SDK菜单路径可能有细微差异,但一般都在这里:
Device Drivers -> Network device support -> CAN bus subsystem support -> CAN Device Drivers -> SPI CAN drivers -> <M> Microchip MCP251XFD CAN interface我建议先编成模块(M),调试阶段方便反复modprobe/rmmod,不用每次改设备树都重新烧内核。等确认没问题了,再改成内建(Y)烧进boot镜像,避免文件系统里模块没更新导致驱动加载失败。
菜单路径在不同内核版本里可能叫法略有不同,但搜索MCP251XFD一般都能直接定位到。如果SDK提供的是defconfig方式,直接在配置文件里追加一行:
CONFIG_CAN_MCP251XFD=m CONFIG_CAN_RAW=y CONFIG_CAN_BCM=y然后编译内核和模块。ARM64平台交叉编译命令大致这样:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- xxx_defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)编译完后模块文件在drivers/net/can/spi/mcp251xfd.ko。拷贝到板子的/lib/modules/$(uname -r)/kernel/drivers/net/can/spi/下,执行depmod -a。
2.2 设备树素材:找对参考文档和绑定说明
设备树这块最重要的是找到内核里的绑定文档,路径在:
Documentation/devicetree/bindings/net/can/microchip,mcp251xfd.yaml这个YAML文件比网上任何博客都权威,里面列出了所有支持的compatible字符串、必填属性和可选属性。我遇到过有人拿老MCP2515的设备树改过来用,compatible还写成microchip,mcp2515,驱动当然probe不上。
还有一个小技巧:SDK的dts目录里搜一下mcp2518fd或者mcp251xfd,很多参考板已经写好了节点,直接把那段复制过来改引脚就行。不过复制的时候一定要查清楚:参考板用的SPI控制器编号和你的板子一样吗?中断GPIO在哪组?晶振是多少MHz?这些硬编码拷贝过来必翻车。
2.3 硬件连接的物理检查清单
写设备树之前,先对着原理图把物理连接过一遍。MCP2518FD不是只要接四根SPI线就能跑的芯片,外围还牵涉不少引脚:
- SPI的四根线:MISO、MOSI、SCLK、CS,确认接到主控的哪一路SPI,片选对应的CS编号
- INT中断线:MCP2518FD的INT引脚接主控的哪个GPIO,这个GPIO必须支持中断
- 晶振或外部时钟:常用的是40MHz晶振,也有用20MHz的,必须确认原理图
- 收发器使能脚:很多板子把收发器的STBY引脚接到主控GPIO上,这个脚如果默认是高电平,收发器会处于待机状态,CAN总线根本发不出数据
- 复位脚:RST低有效,一般的RC复位电路就行,如果是GPIO控制,需要在bootloader或驱动里处理
我曾经遇到过一次很诡异的现象:设备树、驱动、内核配置全对,但CAN口就是发不出数据。折腾了一下午,最后发现是收发器的STBY引脚悬空了,默认电平恰好把收发器禁用了。从那以后我拿到板子的第一件事就是查收发器的使能逻辑。
3. 设备树节点编写:寄存器、中断、时钟与GPIO的配置细节
3.1 一个能跑通的mcp2518fd设备树节点范例
以下是一个基于RK3562J的参考节点,SPI1控制器下挂MCP2518FD,片选CS0,中断接到GPIO3_A3:
/ { mcp2518fd_osc: mcp2518fd-osc { compatible = "fixed-clock"; #clock-cells = <0>; clock-frequency = <40000000>; }; }; &spi1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi1m0_cs0 &spi1m0_pins>; assigned-clock-rates = <100000000>; mcp2518fd: can@0 { compatible = "microchip,mcp2518fd"; reg = <0>; clocks = <&mcp2518fd_osc>; interrupt-parent = <&gpio3>; interrupts = <RK_PA3 IRQ_TYPE_LEVEL_LOW>; spi-max-frequency = <10000000>; status = "okay"; }; };几个关键点解释一下:
reg = <0>表示挂在该SPI控制器的CS0上,如果你的原理图接的是CS1,这里要改成1clocks引用的是固定时钟节点,频率必须和板上实际晶振一致,这个直接决定波特率是否准确interrupts = <RK_PA3 IRQ_TYPE_LEVEL_LOW>,中断触发方式必须用低电平,原因后面单独说spi-max-frequency先别急着跑满,10MHz是一个很稳的起点
3.2 中断引脚为什么必须配成低电平触发
MCP2518FD的INT引脚是低有效,正常工作状态下是高电平,一旦内部产生中断(比如收到报文、发送完成、错误告警),INT拉低。
如果设备树里配成下降沿触发(IRQ_TYPE_EDGE_FALLING),理论上每次拉低也能产生一次中断。但问题在于:当驱动进入中断处理函数,通过SPI读取状态寄存器时,如果此时又有新报文到达,INT引脚会一直保持低电平。下一次中断到来时不会有新的下降沿,内核就感知不到了,这帧报文对应的中断直接被吞掉。
用低电平触发(IRQ_TYPE_LEVEL_LOW)则不同。只要INT是低电平,中断就会一直被触发,直到驱动读完状态、清掉中断标志、INT恢复高电平为止。电平触发天然不会丢中断,代价是中断处理函数可能被多次调用,但这个开销对MCP2518FD这种通过SPI读状态的芯片来说完全可以接受。
另外要注意GPIO的上下拉配置。如果MCP2518FD的INT脚是开漏输出,板级必须给这个脚配上拉电阻,不然释放不了高电平。如果板子上没有外部上拉,可以在设备树的pinctrl里配上拉。具体还是要看原理图设计。
3.3 晶振时钟与SPI速率的匹配原则
晶振频率这个坑,坑过不知道多少人。MCP2518FD需要外部提供时钟,常用40MHz,也有用20MHz甚至16MHz的。驱动通过设备树的clocks属性拿到频率值,再用这个值去计算各种时间参数。
如果板子实际是40MHz晶振,设备树里却写成了20MHz,波特率配置会整整偏一倍。表现就是:节点A发送,节点B收不到;或者两边都设置了相同波特率,但示波器看波形周期明显不对。
设备树加载后可以用dmesg确认驱动识别到的时钟频率:
dmesg | grep -i mcp251xfd如果打印的时钟频率和原理图不一致,先检查设备树节点,别急着调波特率。
SPI速率方面,MCP2518FD的SPI接口最高支持20MHz,但不建议一开始就跑满。10MHz在绝大多数板级设计下都能稳定工作,20MHz对信号完整性要求高,如果板子布线一般、走线长、容性负载大,高负载下容易出现SPI传输超时。稳妥的做法是先用10MHz跑通功能,后面用示波器量过眼图再决定要不要提频。
4. 驱动加载、节点链路与环回自测:让can0先响起来
4.1 模块加载与log观察:确认驱动真正接管了设备
设备树改好、内核编译好、模块拷贝到板子之后,先加载模块:
modprobe mcp251xfd然后立即看内核日志:
dmesg | tail -30正常情况能看到类似这样的信息:
mcp251xfd spi1.0 can0: MCP2518FD successfully initialized.看到这行,说明驱动已经成功probe,CAN控制器注册成了系统中的can0网络设备。接着用ip link确认:
ip link如果can0没有出现,别急着怀疑驱动。先排查这三个点:
- 设备树里
status = "okay"有没有漏写,SPI控制器节点和子节点都要检查 - 内核配置里
CONFIG_CAN_MCP251XFD到底是编进去还是模块,模块有没有真的加载成功 - 中断GPIO有没有被其他设备占用,
cat /proc/interrupts看看对应中断号是否存在
还有一个很隐蔽的问题:can0这个编号是CAN子系统按注册顺序分配的。如果板子上其他CAN控制器先注册了,MCP2518FD可能变成can1甚至can2。开发脚本里如果写死了can0,后面接别的CAN设备时应用可能莫名其妙连不上,建议用ip link确认实际名称。
4.2 配置CAN-FD波特率与采样点:ip命令的正确用法
CAN-FD区别于经典CAN的关键在于:仲裁段和数据段可以配置不同的波特率。仲裁段一般跑500kbps,用于和总线上其他标准CAN节点兼容;数据段可以跑到2M、5M甚至8M。
最基础的配置命令:
ip link set can0 down ip link set can0 up type can bitrate 500000 dbitrate 2000000 fd on如果对采样点有要求,可以显式指定:
ip link set can0 down ip link set can0 up type can \ bitrate 500000 sample-point 0.8 \ dbitrate 5000000 dsample-point 0.75 \ fd on配置完成后用下面的命令核对实际生效的参数:
ip -details link show can0输出里会有bitrate 500000、dbitrate 2000000、fd on之类的信息,确认无误再继续。
这里有个经验:采样点建议按照总线长度和节点数来调。短距离、点对点,采样点可以放80%;长线缆、多节点,适当往75%靠。CAN-FD数据段频率高,采样点不对会出现偶发错误帧,而且这种问题很难排查,不像丢中断那么快能定位。
4.3 内部环回测试:不接总线就能验证收发通路
在连接真实总线之前,强烈建议先做内部环回测试。这个模式绕过了CAN收发器,报文从协议引擎发出去后直接收回来,用来验证SPI通路、中断链路、驱动和协议栈是否完整。
开启环回模式:
ip link set can0 down ip link set can0 up type can bitrate 500000 dbitrate 2000000 fd on loopback on ip link set can0 up然后另开一个终端,启动candump抓包:
candump can0 -n 5再发送一帧测试报文:
cansend can0 123#DEADBEEF正常情况candump会抓到这一帧:
can0 123 [4] DE AD BE EF如果抓不到,不要急着改硬件。先确认candump和cansend两个终端跑在同一个网络命名空间、同一块板子上。嵌入式系统经常被折腾出多套网络配置,这个问题比想象中常见。
环回测试通过后,用ip -statistics link show can0看一下收发计数,RX和TX统计应该都有增长。如果TX增长但RX没有,说明环回路径有问题,大概率是中断配置不对,回到设备树重新检查。
5. 两个节点实测CAN-FD收发:从candump到8Mbps压测
5.1 搭建最小双节点链路
环回测试只是第一步,它验证的是单板内部通路。真实项目里MCP2518FD要和总线上其他节点通信,必须做双机实测。
最简单的双节点链路:两块RK3562J板子,每块都挂MCP2518FD,CAN_H接CAN_H,CAN_L接CAN_L,两端各接一个120欧终端电阻。如果没有第二块RK3562J板,也可以拿另一块带CAN-FD控制器的设备,比如USB-CAN-FD调试工具。
接线看起来简单,但收发器供电和共地经常被忽略。CAN收发器要正常工作,必须有电源;两个节点的GND必须连通,不然总线电平没有参考点。我见过不少人在这一步栽跟头:明明两端都配置了相同波特率,就是通信不上,最后发现是一端的收发器供电没接。
配置双节点的波特率,仲裁段和数据段必须保持一致:
节点A:
ip link set can0 down ip link set can0 up type can bitrate 500000 dbitrate 2000000 fd on节点B执行相同命令。然后节点A开candump,节点B发报文:
节点A:
candump can0节点B:
cansend can0 123#DEADBEEF节点A能正常收到,说明最小链路通了。如果收不到,优先检查两个方面:一是接线有没有接反,二是两端采样点是否匹配。数据段速率越高,采样点差异对通信的影响越明显。
5.2 收发测试与错误计数观察
双机通了之后,别急着上高压测试,先学会看错误计数。CAN控制器内部有两个关键计数器:发送错误计数和接收错误计数。当错误超过阈值时,节点会进入bus-off状态,完全退出总线通信。
查看错误状态的方法:
ip -details -statistics link show can0正常通信中,RX和TX的bytes、packets会持续增长,can0的状态应该是ERROR-ACTIVE。如果看到ERROR-PASSIVE或者BUS-OFF,说明总线上有通信问题,典型原因包括:
- 两端波特率不一致,或者采样点不匹配
- 总线没有终端电阻,信号反射严重
- 数据段波特率超出了线缆允许范围
- 收发器供电异常或者总线电平被拉死
candump还可以用-x参数显示错误帧,方便定位问题:
candump can0 -x错误帧里会带错误类型信息,比如bit错误、填充错误、CRC错误等。如果频繁出现填充错误或者位错误,基本可以锁定是采样点或者信号完整性问题。
5.3 长时间压测中的结果解读
功能验证通过后,做一轮相对长时间的压力测试,确认在高负载下驱动和硬件都能扛住。常用组合是:一端candump持续抓包,另一端cangen批量发送。
发送端:
cangen can0 -v -g 0 -n 100000 -x解释一下这行命令:-g 0表示帧间隔为0,尽可能快发;-n 100000表示发10万帧;-x表示生成随机长度和payload的FD帧。如果总线速率足够,这个命令可以很快跑完。
接收端统计:
candump can0 -n 100000 > /dev/null跑完后在接收端用ip -statistics link show can0看是否有丢帧记录。正常情况在点对点、负载设计合理的总线上,100k帧不应该有丢失。如果出现大量丢帧,优先排查中断配置和SPI速率,其次是CAN套接字缓冲区大小。
内核的CAN套接字缓冲区在某些场景下可能不够用,可以适当调大,这一步对高频收发场景很重要:
sysctl -w net.core.rmem_max=1048576 sysctl -w net.core.wmem_max=1048576压测通过后,再试试把数据段波特率从2M提到5M,甚至8M,观察错误计数是否异常。如果高数据段速率下出现随机bus-off,大概率是采样点或者线缆质量问题,先把采样点往中间调,比如75%。
6. 驱动适配与测试中的典型坑:我从RK3562J上踩到的教训
6.1 现象:环回测试通过但实际通信偶发丢帧
这是我最开始遇到的坑。环回测试一切正常,双节点通信也通了,但高速突发数据时偶尔会丢帧,而且不是规则性丢失,完全随机。
排查链路是这样的:先看中断。/proc/interrupts里对应的中断次数远小于实际收发的报文数,这是最直接的信号。原因是设备树里一开始用了下降沿触发,当INT低电平期间又有新报文到达时,没有新的下降沿,中断就丢了。改成低电平触发后,丢帧问题立刻消失。
另一个可能性是SPI速率过高导致偶尔传输失败,这种情况下dmesg里通常能看到类似SPI transfer failed或者CRC mismatch的日志。把spi-max-frequency从20MHz降到10MHz再观察,如果问题消失,基本就是信号完整性问题。
6.2 现象:拨码开关设置了相同波特率但死活不通
这个问题是最玄学的:两个节点配置完全一样,线也接对了,终端电阻也加了,总线就是不通。最后用示波器看波形,发现MCP2518FD输出的位时间比预期的大了一倍。
根因是晶振频率写错了。板子上实际是40MHz晶振,设备树固定时钟节点写成了20MHz。驱动按照20MHz的时钟去计算波特率分频,导致实际波特率只有目标值的一半。CAN协议对位时间精度要求很高,这个偏移已经超出了容错范围,节点完全无法同步。
改回40MHz之后,没有调整任何其他配置,通信立即恢复正常。所以拿到新板子,第一件事就是核对晶振频率,别默认40MHz,也别默认20MHz,以原理图为准。
6.3 现象:设备probe成功但CPU占用率居高不下
有一次测试时发现板子特别卡,top看CPU占用到了40%以上,irq线程占了绝大部分。查/proc/interrupts,对应的GPIO中断每秒触发几万次,显然不正常。
简单的想法是中断风暴,但实际不是驱动死循环,而是中断处理中每次都要通过SPI读取MCP2518FD的状态寄存器,SPI本身又有传输延迟,高中断频率下CPU一直忙在SPI读事务上。
解决方法是降低中断频率:MCP2518FD支持中断聚合,多个事件会合并成一次INT拉低,驱动读完状态寄存器和FIFO后统一处理。关键是确保驱动没有漏读FIFO里的报文。如果频繁丢中断,驱动会多次触发中断读取,反而增加开销。把设备树里的中断改为低电平触发后,中断次数正常了,CPU占用也降到了个位数。
还有一个容易忽略的点:确认中断GPIO没有同时配置为唤醒源。有些SDK默认给GPIO加了wakeup能力,在嵌入式系统里如果没做电源管理,这个唤醒功能会额外增加GPIO中断的处理路径,带来不必要的开销。
6.4 现象:把spi-max-frequency调到20MHz后系统偶发挂死
有段时间我追求极限性能,把spi-max-frequency改成20MHz,结果系统在长时间负载下偶发挂死,表现为CAN口没有响应,但系统没有完全死机。查日志发现是SPI传输超时,驱动反复重试失败后进入了异常状态。
这个问题的原因有两层。第一层是硬件层面:板子上的SPI走线比较长,20MHz的信号边沿劣化严重,偶尔采到错误数据。第二层是软件层面:RK3562J的SPI控制器在某些data rate下,分频出来的实际SPI时钟并不是整数倍关系,导致和MCP2518FD的时序偏差过大。
最终的方案是回到10MHz稳定运行。CAN-FD的瓶颈通常在总线上,而不是SPI链路上。MCP2518FD内部有FIFO,10MHz SPI时钟下8M数据段速率也完全跟得上,20MHz的性能提升在大多数应用场景里感知不到。如果确实需要高吞吐,优先考虑的是优化应用层打包逻辑,而不是盲目提高SPI频率。
在调试CAN-FD时遇到问题,有个比较实用的排查顺序:先确认驱动probe成功、时钟频率正确,再做单板环回,再做双机收发,最后做压力测试。每步通过后做一个标记,万一后面出问题,能快速缩小范围。MCP2518FD这个组合在RK3562J上完全能稳定跑,真正耗时间的往往不是驱动本身,而是这些容易被忽略的外围细节。