最近做一个项目时,板端需要三路CAN总线,Zynq-7020的PS自带CAN只有两路,于是把第三路放到了PL端,用AXI-CAN软核实现,再让Linux通过SocketCAN统一管理。整个流程走下来,Vivado配置、设备树、中断映射、波特率这几个环节基本每个都踩过坑。这篇主要讲如何在Linux下把基于Zynq的AXI-CAN完整跑起来,从硬件搭建到应用层收发,再到排障思路,适合正在用PetaLinux或手工内核做Zynq Linux开发,又被PL端CAN折腾得不轻的工程师参考。
1. 为什么Zynq上跑Linux还要挂AXI-CAN:先从选型逻辑说起
1.1 PS端CAN控制器与PL端AXI-CAN的差别
Zynq-7000的PS端自带两个CAN控制器,型号是Bosch C_CAN,挂在APB总线上,使用MIO引脚输出。它的好处是几乎不占PL逻辑资源,CPU直接通过寄存器访问,延迟低,软件上就是标准的Linux CAN设备驱动。但两个控制器的数量是固定的,引脚只能从MIO里选,而且MIO引脚本身还有很多复用限制,比如被以太网、SDIO、UART占用后,可选的CAN引脚就非常有限。如果板子已经做好,MIO又被占满,改PS端的CAN引脚分配往往要动硬件,非常痛苦。
AXI-CAN是Xilinx提供的一个CAN控制器软核,挂在AXI4-Lite总线上。它把CAN控制器逻辑放到了PL里,引脚可以接到任意PL Bank,灵活性高很多。只要PL资源足够,想挂多少路CAN都行,甚至可以接到FPGA内部信号上,做一些基于逻辑的报文过滤或触发。像我这次就是PS端两路CAN已经用满,还差一路,只能从PL补。
但AXI-CAN也有明显短板。第一,它只支持CAN 2.0B,不支持CAN FD,如果要上CAN FD,得换Xilinx的CANFD IP核,驱动匹配字符串也变了。第二,它占用LUT、FF和BRAM,虽然单路占得不多,但也不能完全无视。第三,它依赖外部输入一个can_ip_clock时钟,这个时钟的频率和稳定度直接影响波特率误差,后面细说。第四,所有寄存器访问都要经过AXI总线,相比PS端直接访问,中断和收发路径会多一层延迟。这个延迟在低速CAN下可以忽略,但如果做大量高频报文收发,时序上要留余量。
1.2 什么场景下值得用AXI-CAN
从我接触的项目看,用AXI-CAN主要在下面几种情况:
- CAN路数不够:PS只有两路,需要三路以上,这是最常见的原因。
- 引脚布局受限:CAN收发器放在了PL侧Bank附近,走线到MIO太绕,或者MIO已经全部被占用。
- 需要对CAN报文做PL级预处理:比如收到特定ID帧后直接触发某路IO信号,跳过了CPU中断和软件处理,延时会低很多。
- 用的是低端Zynq型号,PS端CAN控制器引脚又恰好被高速外设复用,只能从PL补。
反过来,如果只是简单的一两路CAN通信,PS端自带控制器完全够用,就不值得为了用AXI-CAN去增加PL功耗和资源开销。另外一个务实的建议:如果板子还没定型,优先评估PS端CAN能否满足需求,毕竟PS端方案少一个时钟配置、少一个中断映射,软硬件都省事很多。AXI-CAN是解决问题的手段,不是炫技的理由。
1.3 整条数据链路:从CAN引脚到Linux应用
想明白整条链路,后面排查问题会清晰很多。一帧CAN数据从外部收发器进入Zynq,大致要经过这些环节:
- CAN收发器将差分信号转换为单端TXD/RXD电平,送到PL引脚。
- AXI-CAN控制器解析帧,存入内部FIFO,产生中断或状态变化。
- CPU响应PL到PS的中断线IRQ_F2P,进入GIC中断控制器。
- Linux内核中的xilinx_can驱动读取寄存器,把帧送给SocketCAN网络层。
- SocketCAN把控制器注册成can0网络接口,应用通过socket读取数据。
这个链路决定了调试时关注的点:物理层看收发器,PL侧看IP核配置,PS侧看中断号和设备树,驱动侧看内核config和compatible字符串,应用侧看can0状态。任何一个环节断了,表现都可能是不通、不稳定或者完全没反应。我当时排障时就是按这条链路倒着查,效率比瞎猜高很多。
2. Vivado里AXI-CAN IP核的搭建:硬件侧先别急着写代码
2.1 在Block Design中挂载AXI-CAN并分配地址
在Vivado里新建Block Design后,IP Catalog里搜索CAN,可以找到“AXI CAN”这个IP核。双击添加后,把它的S_AXI接口连到PS7的S_AXI_GP0或GP1上,这一步如果不熟悉总线互联,直接用Block Automation自动连线也行。
地址分配建议手工确认一次。AXI-CAN寄存器空间一般给0x10000足矣,常见基地址是0x40000000。地址分配后,Git Bash里可以用“Address Editor”查看实际映射范围,记得和后面设备树里的reg值保持完全一致。我见过有人Vivado里配的是0x40000000,设备树里却写了0x40010000,结果驱动probe时报“failed to request region”,接口根本没起来。
如果项目中还有别的AXI设备,比如DMA、VDMA、GPIO,地址冲突也要小心。Vivado自动分配大多不冲突,但开局前扫一眼地址空间,可以省掉后续不少定位时间。
2.2 时钟、复位和中断引脚怎么连
AXI-CAN IP核有两个时钟输入:s_axi_aclk和can_ip_clock。s_axi_aclk是AXI总线时钟,一般接PS7的FCLK_CLK0,我常用100MHz或125MHz,这个时钟只要和AXI总线上其他外设保持一致就行。can_ip_clock是CAN位时间基准时钟,由它分频产生各种波特率。这个时钟的频率选择很有讲究,最好选一个能整除多个常见CAN波特率的整数值,比如50MHz或100MHz。不要用33.333MHz这类带小数的时钟,否则分频后波特率误差很难压到1%以内。
复位信号方面,s_axi_aresetn接processor_system_reset输出的peripheral_aresetn即可。AXI-CAN还有一个can_reset引脚,用来复位CAN控制器内部状态,同样可以由外设复位输出统一驱动。注意复位信号必须保证上电后能正确释放,否则会出现寄存器能读、但CAN控制器一直处于复位态的现象。
中断输出是can_interrupt,接一个Interrupt Concat IP,再连到PS7的IRQ_F2P[0]引脚。IRQ_F2P一共有8条中断线,一条IRQ_F2P线不能同时给多个PL中断源共用,每个PL外设都要独占一条。中断源多的话,需要用Concat把多路中断合并成一条,合并前先想好哪个设备占哪条IRQ_F2P,因为后面设备树里的中断号是跟着这条线走的。
2.3 FIFO深度、工作模式和位定时参数怎么选
AXI-CAN IP核配置界面里有一个比较关键的就是收发FIFO深度。TX/RX FIFO深度可以独立配置,我一般默认配32,如果项目里报文比较密,就配到64,资源紧张的板子配16也够用。FIFO太浅,高频收发时容易出现丢帧;太深又占BRAM,Vivado综合后的资源报告自己心里要有数。需要说明的是,这里配置的是IP核内部的硬件FIFO深度,Linux驱动里的发送队列和接收队列是另一层概念,两者不冲突。
Loopback模式分Internal和External两种。调试阶段可以在IP配置里打开Internal Loopback,让发送帧在IP核内部直接回到接收路径,不需要外部接设备就能自测。但正式通信前一定要把这个选项关掉,否则总线上的真实报文进不来。如果不想每次改动IP配置,也可以留着External Loopback,在外部把CAN_H和CAN_L短接,效果类似。我自己更习惯用外部短接的方式,因为还能顺便验证物理层收发器是否正常工作。
采样点和位时间参数,在IP核里可以设默认值,但真正生效的是Linux驱动通过ip link设置的bitrate和采样点,IP核里的默认值只是上电复位时的初始状态。所以Vivado配置界面里的位定时参数,只要给一个合理的默认值就行,不用花太多精力去精调,后面在Linux侧统一控制。
3. 设备树节点、驱动匹配与中断号换算:Linux下能不能认到全看这里
3.1 先确认xilinx_can驱动已经编进内核
Linux内核里负责AXI-CAN的驱动是drivers/net/can/xilinx_can.c,它不仅支持PS端CAN,也支持PL端AXI-CAN,compatible字符串匹配的是“xlnx,axi-can-1.00.a”。如果用的是PetaLinux生成的内核,一般默认已经包含该驱动,但如果是手工裁剪的内核,很容易漏掉。
先确认一下当前内核配置。如果开启了CONFIG_IKCONFIG_PROC,可以直接查:
zcat /proc/config.gz | grep XILINX_CAN没有CONFIG_IKCONFIG的话,就去内核源码目录查:
grep XILINX_CAN .config如果没有编译进去,menuconfig路径在:
“Network device support → CAN bus subsystem support → CAN device drivers → Xilinx CAN”
这个驱动建议直接编译进内核y而非模块m,尤其在开发调试阶段。用模块方式,一旦网络启动和模块加载的时序没处理好,can0接口出现的时间会飘忽不定,排查起来多一个干扰项。后面系统稳定了再改成模块也不迟。
3.2 设备树节点参考写法
我这次用的AXI-CAN节点参考写法如下:
/ { can_clock: can-clock { compatible = "fixed-clock"; #clock-cells = <0>; clock-frequency = <50000000>; }; }; &axi_can_0 { compatible = "xlnx,axi-can-1.00.a"; reg = <0x40000000 0x10000>; interrupt-parent = <&intc>; interrupts = <0 29 4>; clocks = <&can_clock>; };每个属性都有讲究。reg里的0x40000000必须和Vivado Address Editor里的基地址一致,0x10000是地址范围,多给一点没坏处。interrupt-parent指向PS端的GIC控制器intc。interrupts三个字段分别是中断类型、中断号、触发方式:0表示SPI中断,29是IRQ_F2P[0]对应到设备树里的中断号,4表示高电平触发,AXI-CAN的中断输出刚好是高电平有效。
关于中断号是怎么来的,这里值得多说几句。Zynq PS端的GIC把外部中断分成SGI、PPI、SPI三类,IRQ_F2P[0]对应的SPI硬件中断号是61,但设备树里GIC中断描述符的第二个字段要把硬件SPI号减去32,所以61减32等于29。如果中断线接的是IRQ_F2P[1],设备树就写30,IRQ_F2P[2]写31,以此类推到IRQ_F2P[7]写36。这个换算关系是很多人的坑,我见过设备树里直接写61的,也见过写28的,就差一个数,中断就是不来。
如果用PetaLinux的device tree generator自动生成设备树,这个中断号通常会自动算好,但时钟属性经常生成得不对。自动生成的节点有些情况下不会带上clocks属性,或者引用的clock和实际can_ip_clock频率不一致,要么导致驱动probe失败,要么导致波特率计算错得离谱。所以无论自动生成还是手写,都要把clocks所在的fixed-clock节点频率和Vivado里can_ip_clock实际频率对一遍。
3.3 启动后如何确认驱动正确probe
设备树改完,重新编译dtb或image.ub启动到Linux后,先做三个检查。
第一,看驱动有没有注册成功:
dmesg | grep -i xilinx_can正常的日志会显示xilinx_can控制器已被识别,像“xilinx_can 40000000.axi-can can0: device registered”之类的信息。如果这行都没有,要么驱动没编进去,要么设备树compatible没匹配上,要么驱动probe过程在获取时钟时出错了。
第二,看net设备有没有生成:
ip link正常情况下能看到can0,此时can0还是DOWN状态,这没问题。如果连can0都没有,大概率设备树有问题,优先检查reg地址、clocks节点是否存在、interrupts字段是否合法。
第三,查看中断有没有被注册:
cat /proc/interrupts | grep 40000000能看到一条中断记录,说明驱动已经把中断注册到了GIC。如果前面都正常,但中断记录没有,多半是设备树interrupt-parent或interrupts字段出错。这一步排查完,硬件和驱动的连接才算真正打通。
4. SocketCAN应用层调试:从can0 up到candump抓到第一帧
4.1 配置CAN接口:link up之前先做两件事
SocketCAN是Linux内核原生的CAN协议栈,控制器一旦注册成netdev,使用方式和网卡很像。配置和启动CAN接口的命令是:
ip link set can0 type can bitrate 500000 ip link set can0 up第一行设置波特率,第二行启动接口。注意不能先up再设置bitrate,必须在down状态下改参数。如果总线上有多个节点,波特率必须一致,否则链路层就会不断报错。
设置完成后,查看接口状态:
ip -details link show can0输出里会看到state是“DOWN/ACTIVE”?不对,这里准确说BUS状态应该是“ERROR-ACTIVE”或者一开始是“STOPPED”?实际中,刚up后正常显示为“state DOWN”。如果链路层检测到总线错误过高,会进入“bus-off”状态,需要restart或重新up。这个状态信息对排查物理层问题非常有用。
启动后可以用ifconfig can0看收发计数,或者用ip -details link show can0查看bitrate、sample-point、tseg1、tseg2等参数。这些参数是驱动根据can_ip_clock频率和设定的波特率计算出来的,看到它们能确认驱动读到的时钟频率是否正确。
4.2 用can-utils做收发验证
can-utils是Linux下最常用的CAN调试工具集,包含了cansend和candump两个核心命令。
先开一个终端监听:
candump can0再开另一个终端发送一帧标准帧:
cansend can0 123#DEADBEEF这里123是11位标准帧ID,后面跟的#号,DEADBEEF是8字节数据。如果一切正常,candump会显示收到的帧内容。如果总线上没有其他节点,可以在硬件上把CAN_H和CAN_L短接,让收发器自发自收,这个测试能同时验证物理层的发送和接收路径。
如果接收侧一直没反应,先怀疑是不是接口没起来:ip link show can0看状态。再看ifconfig can0里RX/TX计数有没有增长。如果RX明显增长但RX错误也增长,基本是波特率不一致或终端电阻有问题。
candump还有一种模式值得常用:
candump -x can0-x参数会把帧内容以十六进制完整打印,包括错误帧信息。如果总线上有错误帧,candump会显示出来,这对定位总线竞争和波特率失配非常有帮助。
4.3 在应用代码里如何读写CAN帧
调试通了之后,最终项目还是要落到自己的C/C++代码里。应用层读写CAN设备,标准做法是用裸协议(raw socket),代码如下:
#include <linux/can.h> #include <linux/can/raw.h> #include <net/if.h> #include <sys/socket.h> #include <sys/ioctl.h> int can_fd; struct sockaddr_can addr; struct ifreq ifr; can_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW); strcpy(ifr.ifr_name, "can0"); ioctl(can_fd, SIOCGIFINDEX, &ifr); addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; bind(can_fd, (struct sockaddr *)&addr, sizeof(addr)); struct can_frame frame; frame.can_id = 0x123; frame.can_dlc = 8; memcpy(frame.data, "hello!!", 8); write(can_fd, &frame, sizeof(frame));接收端同样用read读取,每次读一个struct can_frame。这里有个细节容易踩坑:标准帧ID和扩展帧ID在can_frame.can_id字段里的存放方式。标准帧ID占低11位,扩展帧ID占低29位,同时用标志位区分。判断扩展帧要看can_id & CAN_EFF_FLAG,如果这个标志位被置位,低29位才是真正的扩展帧ID。很多人第一次写解析代码时只取了低11位,扩展帧全被截断了。
如果想让波特率也由应用代码来控制,可以调用system函数去执行ip命令,但更规范的做法是使用libsocketcan库,它封装了链路层的ioctl接口,可以直接在代码里设置bitrate和restart参数。项目对稳定性要求高的话,推荐用libsocketcan,毕竟应用层直接调system执行命令,在嵌入式环境里总感觉不够干净。
5. 实测排障记录:中断不回、波特率漂移、多节点乱码的完整排查链路
5.1 中断一直为0:IRQ_F2P和GIC编号的坑
有次调试遇到一个典型问题:can0接口正常起来了,cansend发送也能成功,但candump那边死活收不到任何数据,连自己发的都收不到。先查了物理层,短接后依然收不到,于是怀疑中断没触发。
看/proc/interrupts,发现can中断的计数一直是0。Vivado里中断线明明接的是IRQ_F2P[0],设备树也写的是interrupts = <0 29 4>,理论上是没错的。但打开/sys/firmware/devicetree/base/axi-can@40000000/interrupts看实际解析出来的值,惊讶地发现是0,30,4。
问题出在PetaLinux自动生成设备树时,默认把IRQ_F2P[0]对应的中断号配成了30,这对应的是IRQ_F2P[1],而不是IRQ_F2P[0]。这种错误在自动生成脚本里出现过不止一次,尤其是当Block Design里存在多个PL中断源时,脚本偶尔会错位。改回<0 29 4>后重新编dtb,重启后中断计数就开始正常跳动了。
排查中断还有个技巧:先用cat /proc/interrupts确认驱动有没有注册中断,如果数组里根本没有这一行,说明驱动probe阶段已经把中断注册失败吞掉了。再看dmesg | grep -i interrupt有没有报错。有时候中断配置看起来没问题,但GIC层面就没把这条中断分配给CPU,这种问题只能逐个中断号去试,没有捷径。
5.2 波特率算得准,但就是不稳:都是can_ip_clock频率惹的祸
另一个折腾很久的问题:自发自收完全正常,但连上外部CAN分析仪后,正常通信偶尔会蹦出错误帧,多跑几分钟甚至直接bus-off。一开始怀疑是终端电阻,量了总线电阻也正常,最后用示波器量CAN_H和CAN_L的位宽,发现显性位的时长和500kbps标准位宽有偏差。
问题出在can_ip_clock上面。Block Design里can_ip_clock用的是100MHz,但设备树里fixed-clock写的却是50MHz。Linux驱动完全信任设备树里报的这个时钟频率,用它去计算分频比。结果就是硬件实际跑的分频关系,是按100MHz算出来的位时间,跟驱动以为自己配置的位时间差了整整一倍。CAN总线上两个不同速率的节点通信,看起来就是这种“偶尔能通一两帧,然后就乱”的现象。
这事给了一个很重要的教训:can_ip_clock的频率选择,宁可选一个整数MHz,也不要从PLL分频出33.333MHz这种数,否则不管驱动怎么算,波特率误差都可能超标。改设备树时钟频率后再看ip -details link show can0里的tseg1、tseg2和sample-point,确认分频后的组合符合预期。标准CAN建议采样点放在75%到80%之间,500kbps时典型配置是tseg1=13、tseg2=2,采样点=(1+13)/(1+13+2)=82.3%,实际工程中我觉得这个值偏后了,一般推荐75%左右,具体根据总线长度调整。
如果示波器频率不够高,看不准单bit位宽,也可以用bus-off频率粗略估算。把一个节点单独接上,另一个节点接到有波特率自动检测的分析仪上,分析仪能识别出错误速率,这也能快速判断时钟有没有配错。
5.3 多节点通信异常:终端电阻和共地问题比想象中还常见
多节点实测时碰到过一种情况:两个节点A和B,A发B能收到,B发A收不到。第一次排查,下意识认为是B的发送路径坏了,但用can0自发自收又一切正常。最后量总线电阻才发现,CAN_H和CAN_CAN_L之间电阻只有接近0欧。原因是A板上收发器输出端和B板之间CAN_H线被板卡内部短接到GND了。
另一种更隐蔽的问题是共地。CAN差分信号虽然抗共模干扰,但两个节点的GND还是必须连在一起。如果A和B各自用独立电源供电,没有共地,总线上的共模电压会超出收发器允许范围,轻则波形畸变,重则烧收发器。调试时最好先用万用表量一下两个节点之间的GND电位差,超过几百毫伏就要考虑加隔离收发器了。
终端电阻的位置也很讲究,必须是总线两端各一个120欧,而不是每个节点都加。有人图省事,在一个节点上并两个120欧,实际效果是等效60欧,对收发器驱动能力要求更高,还会导致信号反射。多节点测试前,先把CAN_H和CAN_L之间的电阻量一遍,供电状态下通常是60欧左右,如果没有电,阻值也应该是60欧左右;测到0或者120欧以上,先停一下,把接线理顺了再继续。
单节点调试时短接CAN_H和CAN_L来模拟回环,这种做法只能验证本节点,验证不了总线竞争和仲裁。真正多节点联调时,终端电阻、共地、引脚顺序这三件事,每件都要单独确认,缺一个都会让排障时间成倍增加。
实际项目中后来我直接把收发器换成了带隔离的型号,板子内部做了隔离电源,多节点通信稳定了很多。如果项目现场有电机、变频器这类强干扰设备,这一步基本是必须的。