ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zynq Linux AXI-CAN软核CAN通信全流程实战指南

Zynq Linux AXI-CAN软核CAN通信全流程实战指南 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接口读写寄存器来控制。这方案的灵活性是前两种没法比的你想做几路就例化几个IPPL侧引脚可以分配到任意合适的位置位时序和采样点可以在IP里预先配置新版本还支持CAN FD。代价是要占PL资源而且整个调试链路长了一圈——FPGA工程要管、bitstream要加载、设备树要写、驱动要对上。但论工程化上限AXI-CAN才是做多路CAN或车载网关类项目的正路。方案通道数PL资源占用Linux驱动实时性灵活性PS内置CAN固定2路0xilinx_can原生支持高低管脚固定MIOAXI-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。双击打开配置界面先看几个关键选项模式CAN 2.0B/CAN FD根据需求选一般工程默认2.0B如果要用CAN FD就选CAN FD模式。使能中断Enable Interrupt这个务必勾上Linux驱动强烈依赖中断完成收发状态处理。采样点Sample Point常见选75%左右默认50%也能工作但如果现场总线较长、波特率又高采样点太靠前容易采到边沿噪声。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同频一般就是100MHzCAN_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]对应设备树中断号29IRQ_F2P[1]对应30以此类推。假设你把AXI-CAN的中断接到了IRQ_F2P[0]那么设备树里写interrupts 0 29 4其中最后一个4表示高电平触发。这个映射关系查不到会浪费很多时间建议直接把这张表记下来IRQ_F2P位GIC中断号设备树interrupts第二个数IRQ_F2P[0]6129IRQ_F2P[1]6230IRQ_F2P[2]6331IRQ_F2P[3]6432IRQ_F2P[4]6533IRQ_F2P[5]6634IRQ_F2P[6]6735IRQ_F2P[7]68362.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-CAN3.1 启用xilinx_can驱动内核配置项与模块加载AXI-CAN软核在Linux下用的驱动和PS内置CAN其实是同一个drivers/net/can/xilinx_can.c。这个驱动通过设备树的compatible字符串来区分硬件类型PS CAN是xlnx,zynq-can-1.0AXI-CAN软核是xlnx,axi-can-1.0CAN 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_CLK0100MHz那么设备树节点长这样axi_can_0: can43c00000 { 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 10代表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_CLK0clkc 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 upLinux内核把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对CANHCANL对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: id0x%03X dlc%d dataDE AD BE EF\n, frame.can_id, frame.can_dlc); /* 阻塞接收一帧 */ if (read(s, frame, sizeof(frame)) 0) { perror(read); return 1; } printf(recv: id0x%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欧终端电阻单板测试用loopbackcan0 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供电是5VSN65HVD230供电是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协议栈这样基础链路有保障上层协议出问题了也知道从哪查起。
返回列表