简介:面向Xilinx 7系列FPGA开发者与CAN总线通信学习者,这套基于Vivado的CAN IP实现Verilog源码,可直接用于工程集成与二次开发。资源聚焦CAN控制器物理层与数据链路层设计,包含帧收发状态机、位填充、错误检测、用户接口等关键模块,注释清晰,适合快速理解在FPGA上落地网络协议的思路。包内共2个文件,1个txt说明文档与1个html设计/源码文件,压缩包仅2KB,轻量便携,便于对照查阅与移植。已有580人浏览学习。通过该资源可掌握CAN IP在Vivado中的集成方式、模块划分与调试重点,免去从零搭建的重复工作,对学习ISO 11898协议、Verilog硬件实现及实际调试都很有参考价值。 我最早接触CAN总线,是在一个需要把FPGA板卡接入工业控制网络的现场项目里。当时摆在面前的无非两条路:自己用Verilog从零写一个CAN控制器,或者直接在Vivado里例化Xilinx官方CAN IP。说实话,一开始我天真地觉得CAN协议也就那么回事,结果越查资料越觉得水深。最后老老实实用了Xilinx的CAN 2.0B IP,在7系列FPGA上把工程跑通了。这篇博客就是把我这轮从配置到上板、从回环到联调的全过程梳理一遍,顺便把那些网上没人告诉你的坑讲清楚,给打算在Vivado里用CAN IP做通信的同行一个可以直接抄作业的参考。
这套源码的定位很简单:基于Vivado开发环境,面向Artix-7 / Kintex-7 / Zynq-7000等7系列器件,例化Xilinx CAN IP,通过AXI4-Lite接口读写寄存器完成CAN报文的收发,Verilog代码里带清晰注释,工程结构可以直接复用。下面我会先把“为什么用IP”这件事说透,再一步步拆解IP配置、源码结构、寄存器操作和上板验证,保证新手照着操作也能跑起来。
1. 为什么我推荐直接用CAN IP,而不是手写CAN控制器
1.1 CAN协议比你想的更麻烦
很多从UART/SPI转过来做CAN的FPGA工程师,第一反应都是“我直接写个状态机就行”。CAN的帧结构看起来确实不难:SOF、仲裁场、控制场、数据场、CRC场、ACK场、EOF,字段都能背下来。但真正落到硬件实现的时候,麻烦全在细节里:位填充(Bit Stuffing)要求连续5个相同电平后自动插入反相电平,CRC是15位带特殊生成多项式的计算,仲裁过程完全是电平竞争,错误帧和过载帧的响应时序差一个位时间就会出问题,还有位定时同步、采样点选择、错误计数器状态机……这些规则叠加在一起,自己写出来的控制器大概率只能在家里实验室跑通,接到真实总线上就会被各种边沿事件打回原形。
CAN总线上同时挂着多个节点,谁先发谁后发、谁能成功发出去,是由仲裁现场决定的。你的控制器必须保证在任何一个位时间都能正确采集总线电平、判断同步、参与仲裁,这种实时性要求对状态机的设计能力是很大的考验。我见过不少朋友花两周时间手写CAN控制器,最后连BasicCAN模式和PeliCAN模式的寄存器兼容问题都能绕晕,更别提出错重发策略了。
1.2 IP核方案与自研方案的对比
在我这次项目里,最终选择Xilinx CAN IP核,理由很直接。用一张表说清楚:
| 对比维度 | 手写Verilog控制器 | Xilinx CAN IP核 | 外挂CAN控制器芯片 |
|---|---|---|---|
| 开发周期 | 两周起步,调协议细节可能按月算 | 半天搞定配置和例化 | 需要额外的MCU或SPI接口逻辑 |
| 稳定性 | 需要大量测试积累 | 官方IP经过验证,可靠性高 | 芯片厂商已验证 |
| 资源占用 | 取决于实现质量 | 约几百个LUT+BRAM,可接受 | 极低,但占PCB面积 |
| 灵活性 | 寄存器完全自定义 | 寄存器映射固定但文档齐全 | 受限 |
| 维护成本 | 自己维护,出问题自己扛 | 升级Vivado即可获得更新 | 换芯片要重新适配 |
对绝大多数项目来说,IP核是性价比最高的选择。官方IP不仅把位填充、CRC、错误管理这些脏活累活都包了,还提供了AXI4-Lite标准寄存器接口,对上层逻辑来说就是个读写存储器而已。唯一需要你自己做的,是搞清楚寄存器映射、初始化序列和收发流程。
1.3 这套源码的定位:开箱即用的工程模板
这套Verilog源码不是炫技,而是面向一个很实际的诉求:拿到一个7系列FPGA板卡,板上带有CAN收发器,拨码或软件配置好波特率之后,代码直接下载就能收发。源码围绕Xilinx CAN 2.0B IP展开,包括顶层模块、AXI4-Lite读写控制逻辑、简单的发送/接收状态机,以及详细的注释。它的价值在于把“IP核怎么接、寄存器怎么配、报文怎么发怎么收”这条链路完整打通了,你可以直接把顶层模块搬到自己工程里,改一改时钟频率和引脚约束就能用。
2. Vivado里把CAN IP配置到能用的关键几步
2.1 IP版本、7系列适配与时钟规划
在Vivado的IP Catalog里直接搜“can”,会看到Xilinx的CAN 2.0B IP核,它支持7系列以及更新的UltraScale系列。选IP时的第一个关键决策是接口协议,一般选AXI4-Lite,这样CPU或内部逻辑都可以通过一组标准的读写寄存器来控制它。另一个决策是IP工作在“标准帧(Standard,11位ID)”还是“扩展帧(Extended,29位ID)”,如果你的产品只跑J1939或CANopen这类协议,通常标准帧就够了,但建议直接选同时支持两种帧的配置,后续不用改工程。
时钟规划上要注意,IP核需要一路“APB时钟”或叫系统时钟,AXI4-Lite寄存器的读写、位定时器的分频都基于这路时钟,一般给50MHz比较常见。还有一个要留意的细节是,CAN IP内部把时钟分频后作为位时序基础,所以系统时钟越高,波特率配置的分频系数选择范围越大,但也别盲目拉高时钟导致时序收敛紧张。7系列上50MHz、25MHz都很稳妥,我这次工程用的就是50MHz。
2.2 位时序参数:波特率怎么算出来的
CAN的每个位时间由同步段、传播段、相位缓冲段1、相位缓冲段2组成,默认同步段是1个时间量子(TQ),其余三段通过IP的位时序寄存器(BTR)配置。位时间总TQ数 = 1 + TSEG1 + TSEG2,而波特率 = 系统时钟 / 分频系数 / 总TQ数。采样点位置 = (1 + TSEG1) / (1 + TSEG1 + TSEG2)。
以50MHz系统时钟、500kbps波特率、采样点75%为例来算:位时间 = 50MHz / 500kbps = 100个时钟周期。如果设置分频系数BRP=5,那么每个TQ是5个时钟周期,总TQ数=20。令TSEG1=14、TSEG2=5,则总TQ=1+14+5=20,采样点=(1+14)/20=75%,刚好满足。CANopen和DeviceNet等主流协议栈都推荐75%附近的采样点,短距离高速传输时这个值很稳。
很多人在这一步翻车,是因为直接用IP GUI里默认的波特率设置,没仔细核算实际时钟。IP配置页会让你填位时序参数,但不代表它知道你的板卡时钟是不是50MHz。务必自己动笔算一遍,把算出来的BRP、TSEG1、TSEG2填进去,再去IP里确认仿真波形,这一步省不得。
2.3 顶层例化:管脚、约束与收发器电平
CAN IP对外的物理信号只有两根:CAN_TX和CAN_RX。这两根是单端信号,必须接到外部CAN收发器(常见的有TJA1050、SN65HVD230、MCP2562),由收发器转换成CAN_H和CAN_L差分电平,才能挂到总线上。顶层例化的时候,FPGA管脚直接连接收发器的TXD和RXD引脚,注意收发器方向通常是TXD输入、RXD输出。
电平匹配是需要特别留意的:7系列FPGA的普通HP/HR bank支持3.3V,绝大多数CAN收发器也支持3.3V供电,直接连没问题。但有些老收发器是5V供电,TXD/RXD引脚电平可能是5V,这时候就要加电平转换或选带5V容忍输入的型号,否则容易烧坏FPGA引脚。另外,总线两端必须各接一个120欧终端电阻,这个经常被忽略,导致长距离通信时波形反射严重。
管脚约束用XDC文件,类似这样:
set_property PACKAGE_PIN E4 [get_ports can_tx] set_property PACKAGE_PIN E5 [get_ports can_rx] set_property IOSTANDARD LVCMOS33 [get_ports can_tx] set_property IOSTANDARD LVCMOS33 [get_ports can_rx]如果板卡上的收发器带STB或EN使能引脚,记得一并约束并拉成有效电平,不然收发器处于待机模式,总线根本收发不了。
3. 源码结构拆解:接口与注释是怎么组织的
3.1 模块分层与命名约定
拿到源码后,第一件事是看目录结构。我这套工程按功能分了四层:最顶层是top_can.v,负责例化CAN IP核和收发状态机;第二层是axi4_lite_master.v,封装AXI4-Lite读写时序,供状态机调用;第三层是can_tx_ctrl.v和can_rx_ctrl.v,分别负责发送和接收流程;第四层是IP核本身由Vivado生成的包装文件。
命名上我尽量让注释“自解释”。模块头部会写明模块功能、输入输出信号含义、依赖的时钟和复位;关键状态机状态用有意义的参数名,比如S_IDLE、S_LOAD_TX、S_TRIGGER、S_WAIT_DONE;端口注释统一标注方向和高有效电平。这样做的真实原因是:过了三个月你自己看代码,注释写得再啰嗦都不嫌多,尤其是状态机这种逻辑密集的地方。
3.2 发送路径:从上层请求到TXD输出
发送一帧CAN报文,逻辑上分三步:组装报文、写入TX缓冲寄存器、请求发送。上层模块(比如一个简单的按键触发或定时发送模块)给出tx_start脉冲和tx_data[63:0]、tx_id[10:0]、tx_dlc[3:0]等信号,can_tx_ctrl.v状态机负责把这组数据通过AXI4-Lite接口写入IP核的TX报文缓冲区,然后置发送请求位。
发送状态机的核心代码逻辑大致长这样:
always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; end else begin case (state) S_IDLE: begin if (tx_start) state <= S_WRITE_ID; end S_WRITE_ID: begin // 通过 axi_write 写入 TX_ID 寄存器 if (axi_w_done) state <= S_WRITE_DLC; end S_WRITE_DLC: begin // 写入 TX_DLC(含数据长度和帧类型) if (axi_w_done) state <= S_WRITE_DATA; end S_WRITE_DATA: begin // 写入 TX_DATA1 / TX_DATA2 if (axi_w_done) state <= S_SET_TXRQ; end S_SET_TXRQ: begin // 置发送请求位,CAN IP开始总线仲裁和发送 if (axi_w_done) state <= S_WAIT_DONE; end S_WAIT_DONE: begin // 查询状态寄存器,发送完成或错误则回 IDLE if (tx_done || tx_error) state <= S_IDLE; end endcase end end注意发送请求位(TXRQ)写入后,不代表立刻发完。IP核要等总线空闲、完成仲裁、等ACK槽,所以必须轮询状态寄存器或等中断,确认发送完成再处理下一帧,否则连续发送时会丢帧。这也是很多初学者最容易卡住的地方。
3.3 接收路径:RXD采样、滤波与中断通知
接收方向,IP核在内部完成RXD引脚的位采样、填充消除、CRC校验、错误检测,然后把完整报文填充进接收FIFO。上层逻辑只需要做两件事:检测接收FIFO是否非空,然后读取RX报文缓冲区寄存器。为了让CPU或上层状态机感知“收到新报文”,IP核会有接收中断标志,可以开中断,也可以直接轮询状态寄存器。
我习惯用查询方式,因为FPGA内部逻辑轮询AXI寄存器开销很小。接收状态机在IDLE状态持续读状态寄存器,当发现RX FIFO非空(或收到中断标志)时,依次读取ID寄存器、DLC寄存器、数据寄存器,把整帧报文拉回来,同时给出rx_valid和rx_data输出,供上层FIFO或直接送串口显示。
这里有一个重要的细节:读完接收寄存器后要清标志,否则IP核会认为你没消费完这帧数据,下一帧可能被堵住或直接丢弃。清标志的方式通常是写“读清零”寄存器,具体要看IP核给出的寄存器定义,这一点注释里我会特别标红提醒。
4. AXI4-Lite寄存器读写:收发链路的核心
4.1 复位与初始化时序
不管上层逻辑多简单,拿到CAN IP后第一件事都是初始化寄存器。初始化顺序不对,后面全白搭。我总结的固定流程是:先确认IP处于复位态——写控制寄存器时置位CCE位进入配置模式,同时清掉CEN位让控制器停止;然后配置位时序寄存器(BTR),设置BRP、TSEG1、TSEG2;接着配置中断使能寄存器(IER),把需要的中断打开;最后清掉CCE位、置位CEN位,让控制器进入正常工作模式。
注意,配置模式切换不是写完寄存器立即生效,必须等状态寄存器里的相应标志位翻转。常见做法是写完后轮询等待,比如写CCE=1后,要读状态寄存器直到确认控制器进入配置模式,再继续写BTR。跳过等待直接写配置寄存器,大概率配置失败,因为IP内部还没切换完状态。这个坑我踩过一次,整半天才发现是初始化太快。
4.2 发送一帧报文的最小代码路径
AXI4-Lite读写时序本身不难,但代码里我封装成了两个任务,方便状态机调用:
task axi_write; input [31:0] addr; input [31:0] data; begin axi_awaddr <= addr; axi_wdata <= data; axi_awvalid <= 1'b1; axi_wvalid <= 1'b1; @(posedge clk); while (!(axi_awready && axi_wready)) @(posedge clk); axi_awvalid <= 1'b0; axi_wvalid <= 1'b0; @(posedge clk); axi_bready <= 1'b1; @(posedge clk); while (!axi_bvalid) @(posedge clk); axi_bready <= 1'b0; end endtask发送一帧11位标准帧数据的调用序列大致是:先写控制寄存器确认非复位;写TX_ID寄存器写入11位ID;写TX_DLC寄存器,其中低4位是DLC,同时可能需要标志位表示标准帧还是扩展帧;写TX_DATA1和TX_DATA2寄存器填8字节数据(数据不足8字节也没关系,DLC决定实际发送长度);最后写发送请求位。IP核收到TXRQ后会自己完成后续的仲裁和发送,完成后会产生发送完成状态或中断。
4.3 接收一帧报文的处理流程
接收流程和发送对称,但有一个额外好处:接收FIFO本身有缓冲,上层逻辑来不及处理时,报文可以先存几帧。大致的处理流程是:状态机轮询状态寄存器的接收FIFO非空标志位;一旦发现非空,读取RX_ID寄存器得到源ID,读取RX_DLC得到数据长度,读取RX_DATA1/RX_DATA2得到报文内容;全部读完后再读一次状态寄存器确认FIFO可以继续接收;如果使能了接收中断,还需要在中断处理里清标志。
在处理接收的时候,要注意DLC最大只有8,数据寄存器读出来时,超过DLC的字节是无效的,别直接拿16字节数据全往上抛。另外,在总线负载很高的场合,接收FIFO深度不够会丢帧,IP GUI里可以配置FIFO深度,建议根据你项目的峰值报文速率估算,别默认用最深,也别用最浅。
5. 上板验证与踩坑记录:7系列实测遇到的问题
5.1 第一步先跑回环,别急着接总线
拿到能编译通过的工程后,我强烈建议先在IP内部开回环模式,而不是直接往总线上发数据。回环模式下,发送的数据立刻从IP内部回到接收FIFO,不经过外部引脚,也没有收发器参与。这一步能快速验证寄存器读写、初始化序列、状态机逻辑是否正确。
我自己习惯的做法:让状态机每100ms自动发一帧固定ID的报文,然后把收到的帧ID、DLC和数据通过板载串口打印出来,或者直接放到ILA逻辑分析仪里抓。如果回环能收到自己发的数据,说明寄存器链路已经通了。回环跑通后再关掉回环模式,接上外部收发器和总线,找另一个CAN节点对发。
5.2 三个容易翻车的细节
第一个坑是波特率偏差。CAN总线上所有节点必须使用同一标称波特率,且位时间偏差在容差范围内。很多时候代码在回环没问题,一接总线就疯狂报错,原因往往是总线上的其他节点实际波特率和你算的对不上,或者是采样点设置不合理。肉眼看不出来,最靠谱的方法是拿CAN分析仪(比如周立功的USBCAN系列)挂在总线上看错误帧计数,或者用示波器抓CAN_TX引脚的位宽,实测一下1 bit是不是真的是2us。
第二个坑是复位时序。7系列FPGA里IP核的复位信号和AXI总线的复位必须同时释放,否则寄存器访问可能一开始就进入不确定状态。我遇到过的情况是主复位拉低再拉高后,马上执行初始化读写,结果第一次读写一直等不到ready信号,最后发现是复位释放后需要至少几个时钟周期的稳定时间,再加个延时再开始初始化就好了。
第三个坑在引脚与收发器配合。前面说了电平匹配和终端电阻,还有一个容易被忽略的点:收发器的TXD输入引脚如果FPGA端的CAN_TX没有正确初始化,可能是高阻态或不定态,导致收发器一直向总线发送主导电平,整个总线被“堵死”。这种问题单看FPGA代码很难发现,务必用万用表或示波器确认FPGA下载程序后CAN_TX引脚电平确定。
5.3 用CAN分析仪联调的经验
回环通过、单板自发自收也通过之后,下一步就是把FPGA板卡挂到真实总线上,和CAN分析仪互发报文。联调时我习惯先在分析仪软件里设置好同样的波特率,周期发送帧,FPGA这边接收并统计成功帧数;然后反过来,FPGA周期发送,分析仪软件看帧。如果两边都能正确收到,再把另一个真实设备节点挂进来。
联调过程中如果出现丢帧或错误帧,除波特率外,还要检查终端电阻。短距离两节点互连,只要两端各一个120欧就行,但如果总线上已经有两个终端电阻了,再并一个会拉低总线阻抗,反而让信号质量变差。另外总线线缆超过几米后,建议用带屏蔽的双绞线,且尽量在总线上只保留CAN_H、CAN_L、GND三根线,避免地环路。
最后分享一个我实测下来很有用的小技巧:在驱动代码里加一个发送计数器和接收错误计数器,通过串口或JTAG随时读出。一旦现场总线异常,不用抓波形就能先判断是发不出去、收不到还是总线上在疯狂报错,这比拿示波器蹲半天高效得多。CAN总线这套东西,看着简单,真正稳定跑起来靠的就是这些细节经验。
本文还有配套的精品资源,点击获取