☰
Zynq Linux AXI-CAN软核CAN通信全流程实战指南
2026/10/3 1:38:55 网站建设 项目流程

Zynq平台上的CAN通信,从裸机切换到Linux之后,调试方式完全变了。以前裸机时拿寄存器手册一个位一个位地抠,现在用AXI-CAN软核挂在PL侧,想在Linux下把CAN跑起来,中间隔着FPGA工程、内核驱动、设备树、SocketCAN好几层东西,新人上手很容易懵。这篇文章是我从零到一跑通"Zynq + Linux + AXI-CAN"全流程的实战记录:Vivado里怎么建工程、AXI-CAN IP那几个参数怎么选、Linux内核驱动怎么配、设备树节点怎么写,再到应用层SocketCAN怎么收发测试,最后把高频问题汇总成一张速查表。内容按实操顺序来,每一步都给出结论和可选的配置,照着做基本能通。

1. Zynq Linux CAN方案选型:为什么选AXI-CAN软核

1.1 三条路线的横评:PS CAN、AXI-CAN软核、SPI转CAN

在Zynq平台做CAN通信,业界主流方案其实有三条路线,很多人一上来就把PS内置的CAN控制器当默认选项,结果做到一半发现管脚和通道数受限,又回头折腾PL侧软核。我建议在项目立项阶段就把这几条路线摆在一起评估,别急着选型。

先看PS内置CAN。Zynq-7000的PS自带两个CAN控制器(ps7_can_0和ps7_can_1),但管脚是通过MIO复用的,位置基本固定,一般被分配到MIO 86~91这类位置,而且MIO本身还要分给UART、SD、以太网、USB这些外设。如果你选个带EMMC、带USB的板子,MIO资源一下就紧张了。PS CAN的好处是Linux原生驱动支持到位,设备树里两行配置就能用,不需要碰FPGA逻辑。坏处也明显:通道数量固定,只有两路;采样点等位时序参数调整空间有限;管脚位置没法自由规划。

再看SPI/UART转CAN芯片方案,比如用MCP2515接在SPI总线上。这方案在单片机上很流行,但放到Linux下就有点鸡肋了。MCP2515对应内核里有mcp251x驱动,确实也能用SocketCAN接口操作,但吞吐量和实时性差一大截。CAN总线跑1Mbps满负载的时候,MCP2515的中断和缓存处理很容易成为瓶颈。除非你的场景只是低速、低负载的奇偶监控,否则我不推荐在Zynq这种高性能平台上用SPI转CAN糊弄事。

最后就是AXI-CAN软核。它是Xilinx提供的CAN控制器IP,挂在AXI总线上,由PS通过AXI-Lite接口读写寄存器来控制。这方案的灵活性是前两种没法比的:你想做几路就例化几个IP,PL侧引脚可以分配到任意合适的位置,位时序和采样点可以在IP里预先配置,新版本还支持CAN FD。代价是要占PL资源,而且整个调试链路长了一圈——FPGA工程要管、bitstream要加载、设备树要写、驱动要对上。但论工程化上限,AXI-CAN才是做多路CAN或车载网关类项目的正路。

方案通道数PL资源占用Linux驱动实时性灵活性
PS内置CAN固定2路0xilinx_can原生支持高低,管脚固定MIO
AXI-CAN软核按需扩展有xilinx_can支持高高,可配置采样点/引脚
SPI/UART转CAN按芯片数少量或0各芯片独立驱动中低中,受限于SPI带宽

1.2 AXI-CAN的角色定位:一帧CAN数据从应用到总线的完整旅程

搞清楚了为什么选AXI-CAN,下面要理解这条链路里每一层都在干什么。很多人在调试时只盯着一层看,设备树配错了去改内核,驱动报错了又去重新生成FPGA位流,这样效率很低。先建立全局数据通路的概念,后面排错就能一步定位。

应用层发一帧CAN数据,路径大概是这样的:你的C程序调用write()往一个AF_CAN套接字写数据,先进入Linux内核的网络子系统,CAN协议栈(net/can/目录下的框架)会把应用层传下来的数据封装成struct can_frame,然后通过net_device接口往下发给实际驱动。这个实际驱动就是xilinx_can,它负责操作AXI-CAN IP的寄存器,把帧改成CAN总线时序,通过AXI总线写入到PL侧的控制器内部。控制器把数据按CAN协议编码成位流,从CAN_TX引脚输出到板载的CAN收发器芯片(比如TJA1050),收发器再把TTL电平转换成CAN_H和CAN_L上的差分信号,这才真正上了总线。

接收方向相反,但有一个点容易被忽略:CAN控制器收到的第一位往往是总线仲裁和错误检测阶段,驱动层实际上要做很多状态判断,比如监测bus-off、错误被动、还有ACK确认。xilinx_can驱动里这些状态都是通过中断处理的,所以硬件设计阶段把中断线接对,比后期在软件里折腾要省心得多。

这个链路里每个环节都是独立的排查单元:应用层能发、SocketCAN能通、驱动寄存器有反应、CAN_TX引脚有波形、收发器差分输出正常,一层层验证下去,问题必然能定位。后面几个章节就按这个链路逐层展开。

2. Vivado硬件工程搭建:AXI-CAN IP配置与中断规划

2.1 Block Design最小系统搭建步骤

Vivado里创建Block Design,第1步先把Zynq PS加进来,配置好DDR、串口、SD这些启动必需的外设。这一步注意,DDR型号要选对,如果和你板子实际用的颗粒不一致,后面跑Linux大概率起不来,或者跑起来以后内存不稳定。UART至少配一路,否则Linux起来后你连日志都看不到。

PS配置里有一个很关键的选项:FCLK_CLK0。这个时钟会给PL侧的AXI总线提供时钟,AXI-CAN的AXI-Lite接口和内部逻辑都要挂在这条时钟域上。建议设成100MHz,这是Zynq上最常用的PL时钟频率,后面设备树里计算波特率分频也方便。FCLK_CLK1可以留作扩展,暂时不用。

接下来从IP Catalog里搜索CAN,添加"Controller Area Network (CAN)"这个IP。双击打开配置界面,先看几个关键选项:

  1. 模式:CAN 2.0B/CAN FD,根据需求选,一般工程默认2.0B,如果要用CAN FD就选CAN FD模式。
  2. 使能中断(Enable Interrupt):这个务必勾上,Linux驱动强烈依赖中断完成收发状态处理。
  3. 采样点(Sample Point):常见选75%左右,默认50%也能工作,但如果现场总线较长、波特率又高,采样点太靠前容易采到边沿噪声。
  4. SJW(同步跳转宽度):一般设4即可,它决定了对总线时钟偏差的容忍度。

添加完之后,运行Run Block Automation,把AXI接口自动接到PS的M_AXI_GP0上,然后手动检查中断连接。这里有个容易出错的地方:AXI-CAN的中断输出是一个信号,但PS侧的中断输入IRQ_F2P是一个多bit的向量,需要一个Concat IP做位宽转换。如果你只有一个中断源,也可以直接用wire连接,但要确认位宽匹配。我习惯的做法是加一个xlconcat,把多个PL外设的中断都聚合起来,接到IRQ_F2P的某一个位上。

地址分配直接交给Vivado自动做,生成完后看一眼地址表,记录AXI-CAN被分配到哪个基地址,比如0x43C00000,这个值后面设备树里要用。整个Block Design的结构大概就四块:PS、AXI-CAN、Concat、还有DDR和UART这些基础外设。

2.2 AXI-CAN IP参数配置:采样点、时钟与中断编号

AXI-CAN IP的时钟连接要特别注意。这个IP有两个时钟输入:S_AXI_ACLK是AXI总线时钟,必须和PS的FCLK_CLK0同频,一般就是100MHz;CAN_CLK是CAN控制器的参考时钟,波特率分频就是基于这个时钟算的。对你的工程来说,最快的办法就是把CAN_CLK也接在FCLK_CLK0上,让两个时钟同源。这样做的好处是波特率计算简单,后面设备树里xlnx,can-clk-freq这个属性填100000000就行,不用额外用Clocking Wizard再生成一路时钟。

采样点这个东西,很多人不重视。CAN总线的采样点指的是在一个bit周期内,控制器在什么位置去读电平。理想情况下,采样点应该落在bit周期的中后段(70%~80%),这样离边沿远、抗干扰能力强。在Vivado的AXI-CAN IP配置页里,你可以直接填采样点百分比,IP会自动算出相位段分频。注意一点:如果Linux驱动起来后又重新设置了位时序,驱动会覆盖IP的默认值,所以IP里的采样点更像是"上电默认值"。

中断编号的映射关系是新手最容易懵的地方。PS侧的IRQ_F2P有0~7共8个中断输入,对应到GIC中断号是61~68。在设备树里,ARM GIC的中断号表示规则是:SPI中断号要减32。所以IRQ_F2P[0]对应设备树中断号29,IRQ_F2P[1]对应30,以此类推。假设你把AXI-CAN的中断接到了IRQ_F2P[0],那么设备树里写interrupts = <0 29 4>,其中最后一个4表示高电平触发。这个映射关系查不到会浪费很多时间,建议直接把这张表记下来:

IRQ_F2P位GIC中断号设备树interrupts第二个数
IRQ_F2P[0]6129
IRQ_F2P[1]6230
IRQ_F2P[2]6331
IRQ_F2P[3]6432
IRQ_F2P[4]6533
IRQ_F2P[5]6634
IRQ_F2P[6]6735
IRQ_F2P[7]6836

2.3 导出硬件描述文件并生成bitstream

硬件设计做完,保存Block Design,右键生成HDL Wrapper,然后Generate Bitstream。这一步耗时取决于工程大小,一般在几分钟到十几分钟。生成完成后,菜单栏File → Export Hardware,弹窗里务必勾选Include bitstream,导出的.xsa文件里才包含FPGA配置文件。这个xsa后面PetaLinux导入或设备树生成都要用。

有一个实操经验:bitstream生成完之后,先用硬件管理器单独烧写一次FPGA,再跑一下简单的PS访问测试。比如在Vivado的Hardware Manager里Program Device,然后用SDK(旧版本)或Vitis做一个最简应用,往AXI-CAN地址写读寄存器,确认PL侧IP确实活着。这一步虽然多花十分钟,但能把Vivado工程问题提前暴露掉,否则等Linux起来发现can0不工作,你还要在"硬件没烧好"和"设备树配错"之间反复猜。

3. Linux内核与设备树:让Linux认出AXI-CAN

3.1 启用xilinx_can驱动:内核配置项与模块加载

AXI-CAN软核在Linux下用的驱动和PS内置CAN其实是同一个:drivers/net/can/xilinx_can.c。这个驱动通过设备树的compatible字符串来区分硬件类型:PS CAN是"xlnx,zynq-can-1.0",AXI-CAN软核是"xlnx,axi-can-1.0",CAN FD版本的IP则是"xlnx,canfd-1.0"。也就是说,同一个驱动文件支持好几种不同的硬件变体,这对我们来说是好事——驱动不用额外移植,只要内核里把编译选项打开就行。

内核配置在make menuconfig里的路径是:

-> Networking support -> CAN bus subsystem support -> CAN Device Drivers -> Platform CAN drivers with Netlink support -> Xilinx CAN

如果用的是PetaLinux,执行petalinux-config -c kernel,在这个路径下把Xilinx CAN选为*编译进内核(选M也行,但要记得在启动脚本里modprobe)。同时建议把CAN raw、CAN gateway这些协议支持也打开,因为SocketCAN工具链需要它们。改完配置重新编译内核,生成的内核镜像替换进启动文件就行。

驱动编译加载成功以后,dmesg | grep xilinx_can或dmesg | grep can能看到驱动的probe信息。但这里有个很常见的坑:驱动probe只在设备树里有对应节点时才会触发,你光编译了驱动但设备树里没写AXI-CAN节点,内核根本不知道有这个设备存在,自然也看不到can0。所以设备树才是让Linux"认出"AXI-CAN的关键。

3.2 设备树节点编写:地址、中断、时钟逐项拆解

设备树节点是AXI-CAN能否工作的核心。假设Vivado里AXI-CAN分配的基地址是0x43C00000,中断接到了IRQ_F2P[0],CAN_CLK接的是FCLK_CLK0(100MHz),那么设备树节点长这样:

axi_can_0: can@43c00000 { compatible = "xlnx,axi-can-1.0"; reg = <0x43c00000 0x1000>; interrupts = <0 29 1>; interrupt-parent = <&intc>; clock-names = "can_clk", "pclk"; clocks = <&clkc 15>, <&clkc 15>; xlnx,can-clk-freq = <100000000>; };

逐个属性过一遍。compatible是驱动匹配的关键,必须和驱动里of_match_table的字符串一致。reg填Vivado分配的地址和长度,长度一般0x1000就够。interrupts这里写<0 29 1>,0代表SPI中断类型,29是按上一节表格算出来的中断号,1或4取决于你Vivado里中断边沿配置,一般用高电平或上升沿,保险起见用4。interrupt-parent指向GIC,在Zynq-7000平台是&intc。

时钟部分容易配错。clocks = <&clkc 15>, <&clkc 15>,第一个时钟是can_clk,第二个是pclk。在Zynq-7000的设备树时钟定义里,clkc 15对应FCLK_CLK0,clkc 16对应FCLK_CLK1。如果你硬件上把CAN_CLK接在FCLK_CLK1上,这里就要写<&clkc 16>。xlnx,can-clk-freq是给驱动指定的CAN时钟频率,部分内核版本必须有这个属性,否则probe直接报错。填100000000,换算成100MHz。

提示:clock-names里的字符串顺序不能反。xilinx_can驱动用devm_clk_get(&pdev->dev, "can_clk")和devm_clk_get(&pdev->dev, "pclk")分别获取两个时钟。名字对不上,驱动会probe失败。

如果你用的是PetaLinux,设备树的修改位置在project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi,把上面这个节点追加进根节点下面即可。如果用传统Linux内核源码编译,改arch/arm/boot/dts/zynq-7000.dtsi或者你自己板级的dts文件,然后重新编译dtb。

3.3 固化PL逻辑:BOOT.BIN与SD卡启动要点

很多人在这一步翻车:明明Vivado工程没问题、设备树也没问题,但板上重启后Linux就是找不到can0。原因多半是PL逻辑从来没被加载过。Zynq的PL侧是可编程逻辑,断电就没了,Linux起来之前必须先烧写bitstream到PL,否则你设备树里写0x43C00000这个地址,实际访问到的是一片空白。

SD卡启动的情况下,bitstream是打在BOOT.BIN里的。BOOT.BIN由FSBL、bitstream、U-Boot三部分组成,PetaLinux里用一条命令生成:

petalinux-package --boot --fsbl zynq_fsbl.elf --fpga design_1_wrapper.bit --u-boot

把生成的BOOT.BIN、image.ub、boot.scr拷贝到SD卡的FAT32分区,rootfs放ext4分区(或者用PetaLinux打包好的整个镜像直接烧录)。注意:如果你的BOOT.BIN里没有包含bitstream,或者FPGA没使能,就算内核和设备树都配好了,AXI-CAN也必然是失灵的。排查时先用cat /sys/class/fpga_manager或Vivado Hardware Manager确认PL是否被正确配置。

4. SocketCAN实战:从命令行到C程序收发CAN帧

4.1 CAN设备初始化:从ip link到can0 up

Linux内核把CAN设备抽象成了网络接口,所以操作CAN和操作网卡用的是同一套命令体系。系统起来以后,先验证驱动和设备树是否工作正常:

dmesg | grep -i can ip link

能看到can0设备,说明probe成功。接着就是配置波特率和启动设备,命令很简洁:

ip link set can0 down ip link set can0 type can bitrate 500000 ip link set can0 up ip -details link show can0

第三条命令执行后,CAN控制器就进入工作状态了。如果这时候设置bitrate时报RTNETLINK answers: Invalid argument,大概率是设备树里的时钟配得不对,驱动算不出合法的波特率分频组合。先在设备树里核对xlnx,can-clk-freq和clocks的频率,确认和Vivado里CAN_CLK实际接的时钟一致。

ip -details link show can0这个命令很多人忽略了,它能打印出CAN控制器当前实际的波特率参数,包括bitrate、sample-point、tq、prop_seg这些,方便你反查驱动到底按什么参数在跑。

4.2 单板回环与双板联调的完整流程

没有第二块板子怎么测?用loopback模式。SocketCAN的loopback分两层:一层是软件层的回环,发出去的帧内核会自动拷贝一份给本机的监听socket;另一层是CAN控制器的硬件环回,信号在控制器内部就绕回来了,不经过外部收发器和总线。xilinx_can驱动对loopback模式的支持在多数内核版本里是OK的。实操命令:

ip link set can0 down ip link set can0 type can bitrate 500000 loopback on ip link set can0 up cansend can0 123#DEADBEEF

再开一个终端跑candump can0,如果能看到自己发出去的那帧,说明控制器能够正常发也能正常收,问题范围缩小到外部硬件。注意,loopback模式下虽然能测通,但这不是真实的总线通信,单节点自发自收是无法产生ACK应答的,软件层会模拟出一个ACK,所以别把loopback模式当成双板通信的替代。

真正要联调,需要两块板子或者一块板一个USB-CAN分析仪。接线是CANH对CANH,CANL对CANL,然后在总线两端各接一个120欧终端电阻。如果偷懒不接电阻,短距离低速通信可能也能通,但几百米长线或者高波特率下就会随机丢帧甚至bus-off。接好后板A执行candump,板B执行cansend发几帧,收发都正常就说明硬件链路通了。

4.3 C语言收发程序:基于SocketCAN raw socket

命令行验证完,最终工程还是要落到自己的程序里。SocketCAN的raw socket和UDP非常像,API无非就是socket、bind、read、write这一套。下面这段代码可以在双板联调时直接用,板A运行起来会发一帧然后等一帧,板B跑同一个程序就能收到。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <linux/can.h> #include <linux/can/raw.h> int main(void) { int s; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame; s = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (s < 0) { perror("socket"); return 1; } strcpy(ifr.ifr_name, "can0"); if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl"); return 1; } addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; } /* 发送一帧标准帧 */ memset(&frame, 0, sizeof(frame)); frame.can_id = 0x123; frame.can_dlc = 4; frame.data[0] = 0xDE; frame.data[1] = 0xAD; frame.data[2] = 0xBE; frame.data[3] = 0xEF; if (write(s, &frame, sizeof(frame)) != sizeof(frame)) { perror("write"); return 1; } printf("send: id=0x%03X dlc=%d data=DE AD BE EF\n", frame.can_id, frame.can_dlc); /* 阻塞接收一帧 */ if (read(s, &frame, sizeof(frame)) < 0) { perror("read"); return 1; } printf("recv: id=0x%03X dlc=%d data=", frame.can_id, frame.can_dlc); for (int i = 0; i < frame.can_dlc; i++) printf("%02X ", frame.data[i]); printf("\n"); close(s); return 0; }

交叉编译命令:

arm-linux-gnueabihf-gcc -o can_test can_test.c

程序的核心就三步:socket创建CAN_RAW套接字、bind绑定can0接口、write/read收发。注意struct can_frame的结构:can_id里如果要发扩展帧,需要或上CAN_EFF_FLAG这个标志位,否则驱动默认按标准帧解析。另外,CAN帧每一次收发都是全帧传输,不像流式套接字会有粘包问题,这点对CAN协议来说很直观。

5. 高频问题排查与实战经验清单

5.1 CAN通信高频问题速查表

实战项目里最容易遇到的坑,我按"现象 → 原因 → 处理"整理成了下面的速查表。每一条都是我在实际调试中踩过或者帮别人排查过的,参考价值比单纯的原理说明高不少。

现象可能原因处理建议
ip link看不到can0设备树节点缺失或compatible不匹配检查dts里是否有can@节点,核对compatible字符串
设置bitrate报Invalid argument设备树can_clk时钟频率与硬件不符核对xlnx,can-clk-freq与Vivado中CAN_CLK实际频率
can0 up后发送报no ACK error总线上只有单节点且未开环回接对端节点或增加120欧终端电阻;单板测试用loopback
can0 up后收发一帧就bus-off终端电阻异常或波特率不匹配万用表量CANH/CANL间电阻,应在60欧左右;确认两端波特率一致
引脚有波形但总线量不到差分信号板载CAN收发器未供电或型号不支持3.3V检查收发器VCC,确认TXD/RXD电平兼容
CAN_H和CAN_L接反了接线错误对调CANH/CANL再测
系统启动后dmesg无xilinx_can信息内核未启用CONFIG_CAN_XILINX_CAN重新配置内核并编译
loopback下能收到自己帧但双板不通外部收发器/接线问题用示波器量CAN_TX与收发器输出端

这里重点说下no ACK error。CAN协议里,发送方在发送完一帧后要等总线上至少一个其他节点回应ACK显性位,没有ACK就说明总线上只有你一个"活人"。很多人第一次调CAN,收不到数据第一反应是驱动有问题,其实只要看发送端报错日志,如果出现no ack,基本板上钉钉是总线上没有对端节点。解决办法要么再接一块板子,要么开loopback模式做单板验证。

5.2 实战中容易被忽略的细节

终端电阻的问题,值得多说几句。CAN总线规范要求在物理总线两端各接一个120欧电阻,作用是匹配阻抗、防止信号反射。很多开发板为了方便用户,板载已经焊了终端电阻(有的还有跳线帽控制)。如果你用了USB-CAN分析仪,分析仪一端往往也自带终端电阻。这时候就得留意,如果板端、分析仪端、外加电阻三处都是120欧,并联之后等效电阻不到60欧,反而会加重收发器负载。实测下来最稳的做法是:先确认板上终端电阻有没有焊、能不能断开,再决定外部要不要额外接。

电平匹配也是个大坑。AXI-CAN的CAN_TX和CAN_RX引脚是1.8V或3.3V逻辑,不同板卡不一样,而常用收发器TJA1050供电是5V,SN65HVD230供电是3.3V。如果板子上的收发器和FPGA IO电平不匹配,轻则通信误码率高,重则收发器不工作。选型时看原理图:PL侧Bank电压是多少,收发器VCC是多少,中间有没有电平转换芯片。没有现成收发器的话,可以买带收发器的CAN扩展板,从PL侧的PMOD或自定义排针接出来,注意供电和地线别接反。

还有一个小技巧是看总线静态电平。正常工作的CAN总线,隐性状态下CAN_H和CAN_L对GND的电压都在2.5V左右,两者间电压差接近0。用万用表量CAN_H和CAN_L之间的电压,如果接近0V而是两点对地都接近0V,那总线可能根本没上电;如果CAN_H和CAN_L之间有超过3V的压差,可能总线一直被某个节点以显性位占用(总线锁死)。这个判断在排查现场故障时非常实用,比盲改代码快得多。

关于信号的波形观察,有示波器的话直接看CAN_H和CAN_L。发数据时,显性位CAN_H抬到约3.5V、CAN_L降到约1.5V,两者差分约2V。如果这个波形看不到,先往回量收发器的TXD/RXD,有波形说明控制器侧正常,问题在收发器;没有波形,问题就在FPGA逻辑或驱动侧。这样一步一步往前推,排错思路就清晰了。

我个人在实际项目里还有个习惯:每次改完设备树或内核,先把dmesg完整存一份,把can相关日志单独过滤出来归档。特别是驱动probe时打印的时钟频率和中断分配信息,后面排查问题能省不少时间。另外,联调时尽量先用固定报文加固定周期测试,比如500ms发一帧data全为0xAA的帧,比随机数据更容易肉眼判断异常。等基本通了再上复杂的CANopen或J1939协议栈,这样基础链路有保障,上层协议出问题了也知道从哪查起。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询