ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Vivado的Xilinx FPGA CAN IP核通信实现与调试指南

2026/9/2 10:12:14 拓冰建站 浏览量
基于Vivado的Xilinx FPGA CAN IP核通信实现与调试指南 简介面向使用Xilinx 7系列FPGA的开发者这份资料提供基于CAN IP核实现CAN总线通信的Verilog源码适用于汽车电子、工业自动化等需要高可靠串行通信的场合。压缩包共2个文件txt文件为核心代码html文件为排版后的源码阅览页整体仅2KB轻量易用。代码覆盖CAN控制器收发状态机、位填充与错误检测、时钟同步、物理层总线接口以及用户层调用接口注释清晰便于直接移植到Vivado工程或作为学习CAN协议硬件实现的参考。资源在Vivado环境下完成综合与布线验证适合有一定Verilog基础、希望快速搭建CAN通信链路的FPGA工程师。目前已有580人浏览学习虽然体积小但信息密度高是动手实践CAN IP集成的便捷起点。 这阵子一直在做工业控制板卡的项目核心需求就是让 xilinx FPGA 通过 can IP 实现 CAN 总线通信。整个工程基于 Vivado 开发芯片是 7 系列最后落地的是一套注释清晰的 verilog 源码收发都调通了拿来就能用。今天把这套方案的架构思路、关键参数、Vivado 操作流程和调试心得整理出来给正在跟 CAN 总线较劲、尤其是想用 IP 核省事又不想被官方手册绕晕的朋友做个参考。先说结论在 7 系列 FPGA 上跑 CAN 总线用 Vivado 自带的 CAN 2.0B Controller IP 是最稳妥的路径。协议层的东西比如帧格式、CRC、仲裁、错误帧、填充位IP 核全部处理掉了。我们要做的只是通过 AXI4-Lite 总线去配置寄存器、读写收发数据。听起来简单但实际配置和调试中有不少细节稍不注意就会让你在示波器前怀疑人生。1. 项目背景与整体设计思路1.1 为什么要用 CAN IP 而不是手写 Verilog在 FPGA 开发里很多工程师的习惯是能自己写的就不想用 IPSPI、UART、I2C 这些基础协议自己写确实没问题代码量小、逻辑也直观。但 CAN 这个总线是真的不一样协议层复杂程度远高于 UART 和 SPI。仲裁机制、错误状态机、位流填充、CRC 校验、ACK 确认再加上波特率同步和重同步逻辑自己从零写一套边界条件多到让你怀疑是不是在写协议栈。CAN IP 核把这些协议层问题全部封装好了。我们实际上面对的是一个带 AXI4-Lite 接口的控制器发送时把帧 ID、数据、长度写到寄存器里触发发送请求总线上的事情 IP 自己处理接收时查看状态寄存器有数据就读回来。从工程时间角度看这次从建工程到回环跑通一共只用两天。相同复杂度的自研 CAN 控制器我早先在别的项目里断断续续改了一个月还是存在偶发丢帧的问题。用 IP 的另一个好处是稳定性和时序可控。Xilinx 的 IP 经过了大量验证时序约束也内建好了Vivado 综合实现时不太会出莫名其妙的时序违例。七系列 FPGA 的选择上Artix-7 和 Kintex-7 都比较主流CAN 这种中低速总线占资源极少逻辑资源富余得很IP 加用户逻辑也就用了不到 2% 的 slice。1.2 整体架构与代码组织整个信号链路是这样的CAN 总线差分信号经过 TJA1050 这类收发器芯片转换成单端 TX/RX 电平然后接到 FPGA 的普通 IO 引脚。FPGA 内部CAN IP 核负责协议解析用户逻辑通过 AXI4-Lite 总线去读写寄存器把要发的数据填进去把收到的数据读出来。代码组织上我分成四个文件来管理can_top.v顶层模块例化 CAN IP暴露时钟、复位、CAN 收发引脚和用户收发接口can_config.vIP 初始化序列负责软复位、位时序配置、模式切换can_tx_logic.v发送状态机管理从用户接口到 IP 寄存器的数据搬运can_rx_logic.v接收逻辑轮询 IP 状态并读取接收数据。顶层设计里我把用户接口和数据通路拆开了用户这边只看到tx_start、tx_id[10:0]、tx_data[63:0]和rx_valid、rx_id[10:0]、rx_data[63:0]这些信号不需要关心 AXI 时序。这样做的好处是后面如果要换别的控制器逻辑只需要改底层驱动模块接口不用动。2. 核心细节解析位时序、AXI 控制与收发逻辑2.1 位时序计算与采样点设置CAN 总线的一个 bit 在时间上被分成四段同步段 SYNC_SEG、传播段 PROP_SEG、相位缓冲段 1 PHASE_SEG1、相位缓冲段 2 PHASE_SEG2。在 Xilinx CAN IP 里实际配置时只需要关心 TSEG1、TSEG2、SJW 和预分频 BRP。波特率计算公式是波特率 时钟频率 / (BRP × (1 TSEG1 TSEG2))以 50MHz 系统时钟、500kbps 波特率为例50MHz 除以 500k 等于 100也就是说一个 bit 在内部要用 100 个时间量子 tq 来表示。BRP 取 5那么一个 bit 就是 20 个 tq。我把 TSEG1 设为 13TSEG2 设为 6SJW 设为 2。这样采样点位置在 SYNC_SEG TSEG1 (1 13) / 20 70%。ISO 11898 标准建议采样点落在 70% 到 87.5% 之间。低速短距离总线可以设高一点比如 TSEG1 14、TSEG2 5采样点 75%或者 TSEG1 15、TSEG2 4采样点 80%。我之前自己写 CAN 控制器的时候对采样点没有概念随便填了个参数结果是总线一长或者干扰稍微明显一点就时不时报错。后面改成按公式严格计算问题就消失了。这里重点提醒一下波特率只是最基本的指标采样点直接影响总线抗干扰能力配置时一定要算不要用默认值直接上板。2.2 基于 AXI4-Lite 的初始化与收发流程CAN IP 的控制核心就是寄存器读写。初始化流程大致是这样先写 SRR 复位寄存器做一次软复位然后写 BRPR 和 BTR 配置预分频和位时序最后写 MSR 模式寄存器退出配置模式进入正常工作模式。发送一帧数据的流程是把帧 ID 写入 TX_ID 寄存器把数据长度写入 TX_DLC把 8 字节数据写入 TX_DATA 寄存器组然后往 TX_CMD 寄存器里触发发送请求最后等待发送完成标志置位。接收则是轮询状态寄存器发现接收 FIFO 非空后依次把 RX_ID、RX_DLC、RX_DATA 读回来。下面这段是 AXI 写任务的简化版本发送和接收模块里反复用到task axi_write; input [31:0] reg_off; input [31:0] reg_data; begin axi_awaddr BASE_ADDR reg_off; axi_awvalid 1b1; axi_wdata reg_data; axi_wvalid 1b1; (posedge clk); while (~axi_awready) (posedge clk); while (~axi_wready) (posedge clk); axi_awvalid 1b0; axi_wvalid 1b0; (posedge clk); end endtaskAXI 接口对新手来说看起来很吓人但其实这个场景里只用到写地址通道和写数据通道。握手信号就是 valid 和 ready 互相等待用 task 封装好后上层逻辑就非常清爽。接收侧我用的是轮询方式每间隔一段时间读一次接收状态这种方式虽然不如中断高效但胜在代码直观、排查问题方便。中断方式在 7 系列上也能配出来但需要处理中断服务程序还要考虑中断标志的清除顺序比轮询多不少工作量。2.3 收发逻辑中容易踩的坑第一个坑是数据字节序。CAN 总线上的标准数据场是高位先发但寄存器每个字节内部是常规的二进制位序很多第一次用的人会把数据和总线上的字节顺序搞反。判断方法很简单回环模式下发一个 0xAA如果接收端读回来是 0xAA说明字节序没反如果读出 0x55 或者数据完全对不上就要去检查字节写入寄存器的顺序。第二个坑是 DLC 的长度掩码。DLC 最大是 8但寄存器里的高位可能有保留信息写入前要按位与上 0x0F。我在调试时碰到过一次写入 0x18 之后 IP 直接进入 bus off 状态查了很久才发现是 DLC 高位带了垃圾数据。第三个坑是发送完成标志的清除时序。发送完成标志必须在收到之后立刻清除否则下一次发送请求会被误判成上一次已经完成导致数据根本没发出去上层却以为发送成功了。解决办法是在发送状态机里保证一个严格的顺序发请求、等标志、清标志、回 IDLE。3. Vivado 工程搭建与 Verilog 源码实现3.1 Vivado 建工程与 IP 配置步骤Vivado 2018.3 及以上版本都可以我这里以 2018.3 为例。新建 RTL 工程器件选对应的 7 系列芯片比如 xc7a35tcsg324-2。打开 IP Catalog搜索框里输入 CAN能看到 CAN 2.0B Controller双击进入配置界面。配置界面里主要关注两个东西一是 AXI 接口的时钟频率要和输入时钟一致二是位时序相关参数。我的习惯是初始化阶段用寄存器动态写入位时序不在 IP 配置界面里写死。这样后面调整波特率时只需要改代码里的参数不用重新生成 IP能省不少时间。生成完 IP 后在 Sources 面板里能看到自动生成的例化模板。CAN IP 对外接口主要分成三组AXI4-Lite 从接口、CAN 物理层引脚can_phy_tx和can_phy_rx、中断和状态输出。把模板复制到自己的顶层文件里就可以开始写用户逻辑了。3.2 顶层源码与发送/接收模块说明顶层代码的关键是 CAN IP 的例化和寄存器偏移宏定义。我给出一段核心代码// can_top.v // Xilinx 7系列 FPGA CAN 总线顶层模块 // 基于 Vivado CAN 2.0B Controller IP module can_top ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 低有效复位 output wire can_tx, // 连接 TJA1050 TXD input wire can_rx, // 连接 TJA1050 RXD input wire tx_start, // 发送请求脉冲 input wire [10:0] tx_id, // 标准帧 ID input wire [3:0] tx_len, // 数据长度 0~8 input wire [63:0] tx_data, // 8字节发送数据 output reg tx_done, // 发送完成脉冲 output reg rx_valid, // 接收完成脉冲 output reg [10:0] rx_id, // 接收到的 ID output reg [3:0] rx_len, // 接收长度 output reg [63:0] rx_data // 接收数据 ); // 寄存器偏移定义不同版本 IP 可能略有差异 localparam REG_SRR 8h00; // 软件复位 localparam REG_MSR 8h04; // 模式选择 localparam REG_BRPR 8h08; // 波特率预分频 localparam REG_BTR 8h0C; // 位时序 localparam REG_ESR 8h14; // 错误状态 localparam REG_TCR 8h20; // 发送命令 localparam REG_TX_ID 8h24; // 发送帧 ID localparam REG_TX_DLC 8h28; // 发送长度 // AXI4-Lite 接口信号 wire [31:0] axi_awaddr; wire axi_awvalid; wire axi_awready; // ... 其他 AXI 信号 // 例化 CAN IP can_0 u_can ( .s_axi_aclk(clk), .s_axi_aresetn(rst_n), .s_axi_awaddr(axi_awaddr), .s_axi_awvalid(axi_awvalid), .s_axi_awready(axi_awready), // ... 其余 AXI 通道信号 .can_phy_tx(can_tx), .can_phy_rx(can_rx) ); // 发送状态机 localparam IDLE 3d0, LOAD 3d1, SEND 3d2, WAIT 3d3; reg [2:0] tx_state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state IDLE; end else begin case (tx_state) IDLE: if (tx_start) tx_state LOAD; LOAD: begin axi_write(REG_TX_ID, {21b0, tx_id}); axi_write(REG_TX_DLC, {28b0, tx_len}); tx_state SEND; end SEND: begin axi_write(REG_TCR, 32h01); // 触发发送 tx_state WAIT; end WAIT: begin if (tx_done_flag) begin tx_done 1b1; tx_state IDLE; end end endcase end end endmodule代码里的寄存器偏移量以我自己项目的 IP 版本为准你在自己工程里使用时一定要用 IP 生成文件里的寄存器定义对一遍。不同的 Vivado 版本、不同的 IP 版本寄存器偏移可能差几个字节这是最容易踩的坑。发送模块是经典三段式状态机IDLE、LOAD、SEND、WAIT。每次发送时依次写 ID、DLC、数据最后触发发送命令。接收模块我用轮询方式持续读取 ESR 状态寄存器发现接收标志就读取 RX_ID、RX_DLC、RX_DATA再把rx_valid拉高一个周期。3.3 引脚约束与上板回环验证XDC 约束文件里时钟约束、复位约束、CAN 引脚约束是必须的。时钟约束很简单50MHz 输入时钟写create_clock -period 20.0 [get_ports clk]。CAN 引脚约束要特别注意电平标准我用的是 TJA1050它的 TXD/RXD 是 3.3V 逻辑所以 FPGA 引脚的电平标准要设成 LVCMOS33。set_property PACKAGE_PIN R4 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property PACKAGE_PIN T12 [get_ports can_tx] set_property IOSTANDARD LVCMOS33 [get_ports can_tx] set_property PACKAGE_PIN U12 [get_ports can_rx] set_property IOSTANDARD LVCMOS33 [get_ports can_rx]上板测试我先做了回环验证。把 CAN IP 配置成内部回环模式确认 IP 内部发送接收路径正常然后再把回环去掉FPGA 的 can_tx/can_rx 接到 TJA1050TJA1050 的 CANH/CANL 接上 120 欧姆终端电阻用示波器看波形。第一次调的时候我一直收不到数据最后发现是 TJA1050 的 TXD/RXD 接反了。TJA1050 的 TXD 对应 FPGA 的 can_txRXD 对应 can_rx我一开始想当然接反了波形完全对不上。4. 常见问题与排查技巧实录4.1 通信失败、丢包这类老大难CAN 通信调试问题很多都是共性的。我把这几个月遇到的实际问题和解决办法整理成了表方便你对照排查。问题现象可能原因解决办法完全没有波形收发器接反、终端电阻缺失检查 TXD/RXD 方向CANH/CANL 之间加 120 欧姆帧间歇性丢失采样点位置不对用公式重新计算 TSEG1/TSEG2采样点调到 75% 左右一直报 bus off总线没有 ACK或者波特率不匹配确认总线上至少有两个节点CAN 协议要求正确接收必须回 ACKID 正确但数据错乱字节序处理错误用 0xAA、0x55 这类固定模式做回环测试定位字节序发送完成但对方收不到发送完成标志误判检查发送完成标志清除顺序确保请求前清干净4.2 Vivado 工程与 IP 相关报错Vivado 工程层面的问题常见的有这几种。第一个是 IP 没有 Generate Output Products 就直接综合报错说找不到 IP 的网表文件解决办法是在 IP Sources 上右键选择 Generate Output Products等它跑完再综合。第二个是 AXI 地址没有对齐CAN IP 挂在 AXI 总线上时地址必须按边界对齐否则综合不过。第三个是综合时经常遇到的时序违例如果是在 CAN IP 的 AXI 接口上报时序优先检查时钟约束是否正确以及s_axi_aclk是否和用户逻辑用的时钟完全一致。另外很多人在 Vivado 里用 IP 时会遇到添加文件后不自动刷新的问题改了 Verilog 文件没生效。这个不是工程错误是 sources 面板没刷新右键 Refresh Hierarchy 就行。4.3 排查工具与方法调试 CAN 总线示波器和逻辑分析仪是必配。示波器主要看总线电平和波形边沿质量逻辑分析仪解帧能直观看到 ID、数据、CRC 和 ACK 位。手里有 USBCAN 卡最方便没有也没关系用两套 FPGA 板子互发互收一样能验证链路。我个人调试顺序是先用示波器确认物理层有波形再检查波特率是否匹配然后再看数据内容对不对最后才排查协议层的问题。先物理层、再数据链路层、最后应用层这个顺序能让你少做很多无用功。还有一个排查小技巧把 FPGA 内部的can_tx信号和外部引脚临时分成两个测试点直接在 FPGA 内部抓can_tx波形。这样能快速区分是 IP 配置问题还是外部收发器问题。当时我调试时发现外部引脚没有输出但内部已经有波形了一下就定位到收发器供电问题。整体用下来我的最大体会是 CAN 总线能不能稳定通信八成取决于位时序参数和终端匹配跟协议代码本身关系不大。IP 核把最复杂的协议层全部包掉了我们只需要伺候好 AXI 总线和引脚约束。如果你第一次调这个方案建议先做内部回环用示波器确认发送接收路径正常再对接真实节点。代码里的寄存器偏移量我在注释里写了但不同版本的 Vivado 生成 IP 可能会有差异拿到代码第一件事是对一下 PG096 文档和 IP 的例化模板这样后面会省很多事。本文还有配套的精品资源点击获取