ARTICLE DETAIL

资讯详情

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

Zynq Linux下AXI-CAN完整开发指南:从硬件搭建到SocketCAN排障

Zynq Linux下AXI-CAN完整开发指南:从硬件搭建到SocketCAN排障 最近做一个项目时板端需要三路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太浅高频收发时容易出现丢帧太深又占BRAMVivado综合后的资源报告自己心里要有数。需要说明的是这里配置的是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-CANcompatible字符串匹配的是“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]设备树就写30IRQ_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 中断一直为0IRQ_F2P和GIC编号的坑有次调试遇到一个典型问题can0接口正常起来了cansend发送也能成功但candump那边死活收不到任何数据连自己发的都收不到。先查了物理层短接后依然收不到于是怀疑中断没触发。看/proc/interrupts发现can中断的计数一直是0。Vivado里中断线明明接的是IRQ_F2P[0]设备树也写的是interrupts 0 29 4理论上是没错的。但打开/sys/firmware/devicetree/base/axi-can40000000/interrupts看实际解析出来的值惊讶地发现是0304。问题出在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时典型配置是tseg113、tseg22采样点(113)/(1132)82.3%实际工程中我觉得这个值偏后了一般推荐75%左右具体根据总线长度调整。如果示波器频率不够高看不准单bit位宽也可以用bus-off频率粗略估算。把一个节点单独接上另一个节点接到有波特率自动检测的分析仪上分析仪能识别出错误速率这也能快速判断时钟有没有配错。5.3 多节点通信异常终端电阻和共地问题比想象中还常见多节点实测时碰到过一种情况两个节点A和BA发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来模拟回环这种做法只能验证本节点验证不了总线竞争和仲裁。真正多节点联调时终端电阻、共地、引脚顺序这三件事每件都要单独确认缺一个都会让排障时间成倍增加。实际项目中后来我直接把收发器换成了带隔离的型号板子内部做了隔离电源多节点通信稳定了很多。如果项目现场有电机、变频器这类强干扰设备这一步基本是必须的。
返回列表