FPGA上实现CAN 2.0控制器IP核:从协议到Verilog实战
2026/8/31 22:19:55 网站建设 项目流程

简介:本资源是一套面向嵌入式系统与FPGA开发者的CAN 2.0协议实现方案,专为Xilinx平台定制,适用于汽车电子、工业控制等需高可靠性实时通信的场景,助力初学者掌握CAN软核集成与硬件协同设计。压缩包共17个文件(63KB),含13个Verilog源文件(如can_top.v、can_fifo.v、can_btl.v等,覆盖CAN控制器核心逻辑、寄存器映射、CRC校验、位定时、仲裁与异步同步接口)、1个关键说明文档readme_verysource.com.txt、1个C头文件opencores_can_regs.h、1个PTF配置模板及1个Perl脚本cb_generator.pl,结构清晰、模块职责明确,便于理解CAN IP在FPGA中的分层实现与可配置性。已有546人学习下载,读者可直接复用开源OpenCores CAN核,在Vivado中完成综合、布局布线与硬件验证,快速构建符合CAN 2.0A/B规范的自定义节点,同时深入掌握帧格式解析、错误处理机制与总线时序约束等底层要点。 接手这个工程的时候,文件夹名字就叫CAN2.0_CANIP_CAN_。刚开始我以为名字拼错了,后来拆开才发现,这名字把整个项目的三要素都写进去了:CAN 2.0 是用的协议规范,CANIP 是一个可综合的 CAN 控制器 IP 核,最后的 CAN_ 则是要把这个 IP 挂到真正的 CAN 总线上跑通。整个项目说白了,就是在 FPGA 里用 Verilog 写一个 CAN 2.0 控制器,再配合外部收发器,让它在总线上收发报文。这样做虽然比直接用 MCU 内置 CAN 控制器费不少功夫,但通道数、滤波器、缓冲深度都可以自己定制,在汽车电子、工业控制、测试设备这些场景里非常实用。如果你正在做 FPGA 通信接口,或者想从寄存器层面理解 CAN 总线,这篇文章应该能给你一个完整的视角。

1. 项目拆解:CAN2.0、CAN IP 和我的工程结构

1.1 为什么叫 "CAN2.0_CANIP_CAN_"

这个命名其实透露了项目的完整链路:协议是 CAN 2.0,载体是 CAN IP 核,最终目标是 CAN 总线。CAN 2.0 是博世在 1991 年发布的规范,定义了数据链路层,包括标准帧和扩展帧两种格式;CANIP 则是我们常说的 CAN Controller IP,也就是用逻辑门实现 CAN 协议状态机;最后的 CAN_ 代表物理总线,实际跑起来需要 CAN 收发器把控制器的 TTL 电平转换成差分信号。

在 FPGA 里实现 CAN IP 核,最大的价值不是“又写了一遍协议”,而是给了你一个完全可控的控制器。MCU 自带的 CAN 控制器通常固定支持 2~3 个通道,缓冲区和滤波器也写死了,但 FPGA 里你可以例化 N 个 CAN IP,每个 IP 的 FIFO 深度、过滤掩码、中断策略都能按需改。去年我们做多通道 CAN 网关原型验证,就是靠这个方案在三天内从单通道扩展到八通道,这个灵活性是 MCU 方案给不了的。

1.2 这个 IP 核能做什么:核心功能与使用场景

我实现的这个 CAN IP 核,功能上对标入门级独立 CAN 控制器,主要特性包括:

  • 完整支持 CAN 2.0A 标准帧和 CAN 2.0B 扩展帧,RTR 帧也可以收发
  • 可配置波特率,覆盖 125kbps 到 1Mbps 的常用区间
  • 内置验收滤波器,一组验收码寄存器加一组掩码寄存器,能减少无效中断
  • 发送路径带 3 层缓冲,接收路径带 64 层 FIFO,避免高负载下丢帧
  • 错误管理逻辑,包含发送错误计数器和接收错误计数器,支持 Bus-off 恢复
  • 提供 APB 风格寄存器接口,也可以改成 AXI-Lite 接软核处理器

使用场景我实际碰到的有三类:一类是多通道 CAN 总线模拟器,用 FPGA 同时模拟多个 ECU 节点;一类是自定义协议扩展,有些私有 CAN 协议需要对仲裁场做特殊处理,市面上控制器不一定支持;还有一类是纯教学用途,把 IP 内部状态机扒开看,能直观理解总线仲裁和位填充过程。

1.3 整体方案选型:FPGA 还是 MCU

如果只是做一两个 CAN 节点,优先选 MCU,这是没有悬念的。但我在这个项目开始时就已经确定要跑在 FPGA 上,原因是应用场景需要 8 路独立 CAN,再加一路 UART 日志,MCU 选型会非常痛苦,而一片 Artix-7 FPGA 就能全包。

具体选型我用的是 Xilinx Artix-7 FPGA,外部时钟 50MHz,CAN 收发器用的是 TI SN65HVD230。HVD230 是 3.3V 供电的 CAN 收发器,可以直接接 FPGA 引脚,省去电平转换芯片,对于原型验证很友好。总线两端各接一个 120 欧终端电阻,这个后面在实测部分还会提。

2. CAN 2.0 协议细节:从帧结构到位时序

2.1 标准帧和扩展帧到底差在哪

写 CAN IP 之前,协议细节必须过一遍。CAN 2.0A 标准帧和 CAN 2.0B 扩展帧的帧结构差异集中在仲裁场和控制场,我画了一张简化对比表:

字段标准帧 2.0A扩展帧 2.0B
SOF1 位显性1 位显性
仲裁场 ID11 位29 位(基础 ID 11 位 + 扩展 ID 18 位)
RTR/SRRRTR 1 位SRR 1 位 + RTR 1 位
IDE显性隐性
DLC4 位4 位
CRC 域15 位 + 1 位定界符15 位 + 1 位定界符

仲裁的规则很简单:总线空闲时,多个节点同时发送,显性位(逻辑 0)优先。也就是说,ID 数值越小,优先级越高。扩展帧的 IDE 位是隐性,标准帧的 IDE 位是显性,所以当标准帧和扩展帧的基础 ID 完全一样时,标准帧会赢得仲裁。

我在状态机里处理仲裁时,一开始只比较了 ID 字段,忽略了 RTR 和 IDE 位,结果总分不清谁是赢家。后来把仲裁场整体当成“显性位优先逐位比较”的序列,就对了。你要写 IP 核,这个点必须想清楚。

2.2 位时序与采样点:差一点就会误码

CAN 总线是串行异步通信,节点之间有独立的时钟源,没有专门时钟线,所以每个节点都得靠位同步来保证采样点一致。位时序由四个段组成:

  • 同步段 Sync_Seg,固定 1 个 Time Quantum,用于检测边沿
  • 传播段 Prop_Seg,补偿总线传播延迟和物理延迟
  • 相位缓冲段 1 Phase_Seg1
  • 相位缓冲段 2 Phase_Seg2

采样点就是 Phase_Seg1 和 Phase_Seg2 的交界位置,公式是:

采样点 = (1 + TSEG1) / (1 + TSEG1 + TSEG2)

其中 TSEG1 是传播段加相位缓冲段 1 的总和,TSEG2 是相位缓冲段 2。波特率计算公式:

波特率 = 主时钟频率 / (BRP × (1 + TSEG1 + TSEG2))

举个例子,50MHz 主时钟,目标波特率 500kbps。我选择 BRP = 5,那么tq = 5 / 50MHz = 100ns,一个位周期需要2000ns / 100ns = 20个 tq。如果设 Sync_Seg=1,TSEG1=13,TSEG2=6,采样点就是(1+13)/(1+13+6)=14/20=70%。如果希望采样点更靠后,比如 80%,可以把 TSEG1 拉到 15、TSEG2 设为 4,这样就接近理想采样位置。

底层逻辑需要做两件事:硬同步加重同步。硬同步在总线空闲后第一次检测到下降沿时触发;重同步在后续边沿到来时,根据相位误差调整 SJW(同步跳转宽度)。我在 IP 里实现了 SJW 配置,但实际项目中大部分时间使用默认值,够用了。

2.3 位填充与 CRC 校验:协议自带的抗干扰手段

CAN 协议规定,发送端在连续发送 5 个相同电平位之后,必须插入一个反相位作为填充位。接收端要识别并删除这个填充位,恢复原始数据。位填充的作用是保证总线不会长时间处于固定电平,让节点始终能根据跳变沿保持同步。

CRC 字段覆盖从 SOF 到数据场的所有位,计算出的 15 位 CRC 放在 CRC 场。CAN 用的 CRC 生成多项式是:

x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1

在 Verilog 里实现 CRC,一般用线性反馈移位寄存器,15 位初值为 0,逐位处理原始数据。代码可以写成可综合的模块,但要注意位序是高位先出,这和很多简易 CRC 教程中低位先出的逻辑相反,我踩过一次坑。

3. FPGA 上 CAN IP 核的系统设计:架构与模块划分

3.1 整体架构与数据流

CAN IP 不能只写一个状态机就完事,否则后续加滤波、加缓存、接寄存器接口都会乱。我按功能拆成了几个模块:

  • 寄存器接口模块,负责读写配置寄存器和状态寄存器
  • 发送缓冲区和接收 FIFO,负责缓存报文
  • 位流处理器,是核心状态机,负责帧编码、解码、位填充、CRC 校验
  • 位时序逻辑,负责采样点生成和同步
  • 错误管理逻辑,维护错误计数器,控制错误帧和 Bus-off
  • 验收滤波器,根据验收码和掩码决定是否接收报文

数据流分两条。发送方向:CPU 写寄存器到发送缓冲区,位流处理器从发送缓冲区取数据,逐位编码,经过 TX 引脚送到外部收发器。接收方向:RX 引脚信号进入位时序逻辑,位流处理器按采样时钟采样并解码,经过验收滤波器后写入接收 FIFO,再触发中断让 CPU 读取。

3.2 关键模块设计:位流处理器、位时序逻辑、验收滤波器

位流处理器是这个 IP 的核心,本质上是一个大状态机。帧的每个字段对应一个状态,比如 IDLE、SOF、ARB、CTRL、DATA、CRC、ACK、EOF。发送和接收可以共用一套状态机,用方向信号区分当前是发送模式还是接收模式,但代码复杂度会上升。我实际写的时候把发送状态机和接收状态机分开,虽然有一些重复代码,但调试起来非常直观。

位时序逻辑是另一个容易写错的点。它要做的是以 tq 为单位计数,产生采样脉冲,并处理重同步。实现上,我用了bit_time_countersampling_point两个信号。每次同步段结束,计数器清零,计数到采样点位置时产生一个高电平脉冲,位流处理器在这个脉冲沿采样总线。

验收滤波器可以做得简单也可以做得复杂。简单版就是一组 32 位验收码加 32 位掩码,接收帧的 ID 和验收码做位比较,掩码为 0 的位不关心。复杂版可以做成多组滤波器,但寄存器开销大。我这个项目先用了简单版,实测够用。

3.3 与 MCU 的接口:APB 还是 AXI-Lite

CAN IP 最终要被人使用,寄存器接口必须好用。如果只是挂自己写的状态机,用普通片选读写也可以;如果想接 Xilinx 的 MicroBlaze 软核,推荐 APB 或 AXI-Lite。APB 的优势是协议简单,适合外设;AXI-Lite 的好处是 MicroBlaze、Zynq PS 端都能直接访问。

寄存器映射我大概设计了这些:

偏移名称读写描述
0x00MODR/W模式寄存器,含复位、监听模式、回环模式
0x04CMRR/W命令寄存器,发送请求、释放接收缓冲
0x08SRR状态寄存器,含发送缓冲状态、接收 FIFO 状态
0x0CIRR/W中断寄存器
0x10IERR/W中断使能
0x14BTRR/W波特率寄存器,BRP、TSEG1、TSEG2、SJW
0x18ACRR/W验收码寄存器
0x1CAMRR/W验收掩码寄存器
0x20TXBUFR/W发送缓冲区,前 4 字节是 ID/控制,后 8 字节是数据
0x40RXBUFR接收缓冲区

4. 核心代码实现:CAN 控制器 IP 的 Verilog 落地

4.1 顶层模块与端口定义

顶层模块我尽量保持简洁,方便集成。下面是一个简化版本,端口包含时钟、复位、寄存器读写接口和 CAN 信号。

module can_top #( parameter SYSCLK_FREQ = 50_000_000 )( input wire clk, input wire rst_n, // APB-like interface input wire psel, input wire penable, input wire [4:0] paddr, input wire pwrite, input wire [31:0] pwdata, output wire [31:0] prdata, // CAN bus signals input wire can_rx, output wire can_tx, output wire tx_oen, output wire irq );

这里的tx_oen是给外部收发器用的输出使能信号,低有效。如果你的收发器不需要控制方向,可以悬空。注意can_rx信号进来之后要先做两级同步寄存器,防止亚稳态。

4.2 发送路径:报文组装与仲裁

发送状态机从 IDLE 开始,检测到发送请求后,先发出 SOF 显性位,然后逐位输出 ID 和控制字段。发送时关键要处理位填充和仲裁。

仲裁的处理方法是:在发送每个位之前,把要发送的电平放到tx_bit,同时采样rx_bit。如果发送的是显性位但又采样到了隐性位,说明有更高优先级的节点在发送,自己立刻放弃仲裁,转为接收模式,并更新错误状态。这个逻辑用一段代码说明:

wire arb_lost = (tx_bit == 1'b0) && (rx_bit == 1'b1); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else if (arb_lost && (state == ARB || state == CTRL)) begin state <= RX_ACTIVE; // lost arbitration, switch to receiver end else begin // normal state transition end end

位填充发送逻辑可以用一个计数器,连续发送 5 个相同位后强制插入反相位。状态机里单独加一个STUFF_INSERT状态,比在每个字段里都加判断要干净。

4.3 接收路径:位流采样与错误处理

接收路径在总线空闲时持续监控rx_bit。当检测到从隐性到显性的下降沿,并且同步成功,就认为收到 SOF,进入接收状态机。

采样点由位时序逻辑产生,接收状态机在采样脉冲到来时读入rx_bit。协议中每个字段要做对应处理:ID 部分需要一边接收一边做验收滤波判断,数据字段要同时做 CRC 串行计算。CRC 计算模块我写成独立的函数式模块:

module can_crc15 ( input wire clk, input wire rst_n, input wire crc_en, input wire bit_in, output wire [14:0] crc_out ); reg [14:0] crc_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) crc_reg <= 15'd0; else if (crc_en) begin wire feedback = bit_in ^ crc_reg[14]; crc_reg[14] <= crc_reg[13]; crc_reg[13] <= crc_reg[12]; crc_reg[12] <= crc_reg[11]; crc_reg[11] <= crc_reg[10]; crc_reg[10] <= crc_reg[9] ^ feedback; crc_reg[9] <= crc_reg[8]; crc_reg[8] <= crc_reg[7] ^ feedback; crc_reg[7] <= crc_reg[6] ^ feedback; crc_reg[6] <= crc_reg[5]; crc_reg[5] <= crc_reg[4]; crc_reg[4] <= crc_reg[3] ^ feedback; crc_reg[3] <= crc_reg[2] ^ feedback; crc_reg[2] <= crc_reg[1]; crc_reg[1] <= crc_reg[0]; crc_reg[0] <= feedback; end end endmodule

这里反馈位是根据生成多项式推导出来的,如果你用工具自动生成 LFSR 矩阵也可以,但手工写要特别小心。我建议最终用仿真比对软件计算的 CRC 值,多个随机向量过一遍再上板。

4.4 寄存器和中断控制

寄存器接口是系统集成最容易出幺蛾子的地方。我实现 APB 接口时,用pselpenable生成写使能:

reg [31:0] mode_reg; reg [31:0] cmd_reg; reg [31:0] btr_reg; reg [31:0] ir_stat; reg [31:0] ir_en; wire apb_write = psel && penable && pwrite; wire apb_read = psel && !pwrite; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin mode_reg <= 32'h0; btr_reg <= 32'b00000010_00000111_00000011_00000101; // example reset value end else if (apb_write) begin case (paddr) 5'h00: mode_reg <= pwdata; 5'h14: btr_reg <= pwdata; // other registers endcase end end

中断控制这边我一开始只做了简单电平中断,结果在高波特率下 CPU 频繁进中断,效率很低。后来改成 FIFO 水位中断,接收 FIFO 里的报文数量超过阈值才触发一次中断,CPU 一次性读完,整体负载降了很多。这个经验在很多通信 IP 里都适用。

5. 仿真验证与上板实测:从模型到真实总线

5.1 搭建 testbench:激励与监测

写 CAN IP 核,没有测试平台就直接上板是灾难。我的做法分三层验证。

第一层是寄存器读写回环测试,配置模式寄存器,读写状态寄存器,确认 APB 接口时序正确。第二层是回环模式测试,把can_tx直接反馈到can_rx,发送一个标准帧,看接收端能不能完整解出 ID 和数据。第三层才是连接到总线模型,模拟两个节点互发。

testbench 里最简单的回环连接是:

wire loop_tx; can_top dut( .clk(clk), .rst_n(rst_n), .psel(psel), .penable(penable), .paddr(paddr), .pwrite(pwrite), .pwdata(pwdata), .prdata(prdata), .can_rx(loop_tx), .can_tx(loop_tx), .irq(irq) );

注意这里can_rxcan_tx短接后不能直接驱动收发器,但在仿真里没问题。回环测试通过后,还要用断言检查发送帧底层的显性/隐性序列。我习惯在关键节点加$display打印状态,比如“发送仲裁场中,第 N 位丢失仲裁”,这样能快速定位状态机跑偏。

5.2 连接真实 CAN 收发器:上板必须注意的电平与时序

仿真和真实总线的差距主要在物理层。我把 FPGA 板的 TX、RX 引脚接到 SN65HVD230 后,第一次上板就踩了不少坑。

首先是收发器供电。HVD230 支持 3.3V 供电,此时它的 CANH/CANL 差分输出可以满足标准总线要求。如果你的板子用 5V 供电的收发器,就必须确认 FPGA 引脚是否兼容 5V,否则要加电平转换。

其次是终端电阻。CAN 总线两端必须各接一个 120 欧电阻,如果在板卡内部只接了单端上拉或者忘了终端电阻,总线信号反射会让波形乱成一团,表现就是频繁报错帧。我用到的是外接 CAN 收发器小板,专门确认板上有 120 欧,再把它并到总线上。

上电顺序也有讲究。先给 FPGA 和收发器供电,再使能 CAN 控制器,最后发送报文。如果控制器先于收发器进入工作状态,可能导致总线上出现一段显性电平被其他节点误判为错误帧。我在 IP 里加了软复位控制,确保寄存器配置完成后再释放控制器。

5.3 抓到的坑:波特率误差、采样点偏斜、总线竞争

上板跑数据,我抓到的第一个坑是波特率误差。用示波器看 TX 脚波形,发现引脚输出的一位时间是 1980ns,而理论上应该是 2000ns。问题出在BRP = 5是整数分频,但我们时钟是 50MHz,5 分频后 tq 是 100ns,20 个 tq 应该是 2000ns,测出来却有偏差。后来发现是测试点量到了收发器本身的延迟,并不是控制器时钟问题。这个经验是:量波特率要看 FPGA 引脚,不要量收发器输出,因为收发器上升下降时间会干扰判断。

第二个坑是采样点偏斜。总线上两个板子的时钟源不一样,如果波特率误差累计超过采样点容限,就会随机丢帧。按 CAN 规范,一段总线内不同节点之间的误差应控制在 1% 以内。我建议直接把采样点设在 80% 左右,不要设在 70% 或者更早,这样容忍度更高。

第三个坑是仲裁。我第一次上总线时,两个板子同时定时发送,总是跑一会儿就出现一堆错误帧。排查后发现,发送状态机在丢失仲裁后没有正确回到接收模式,而是继续尝试发送,结果和另一个节点撞车。解决方法是把仲裁丢失分支单独处理,先等当前位结束,再转接收状态,并且不能立即重新发送。

6. 实测总结与经验清单

6.1 常见问题速查表

这张表是这轮调试下来最值钱的记录:

现象可能原因排查方法
回环正常,接总线就报错收发器接线反了,CANH/CANL 对调用万用表量总线差分电压,发送时 CANH 应高于 CANL
波特率不对,接收方频繁错帧配置的 BRP 或 TSEG 参数错误用逻辑分析仪抓 TX 脚,测量一位时间
总线上有显性电平,但收不到报文缺少终端电阻,信号反射严重检查总线两端是否各有一个 120 欧电阻
发送帧一直仲裁失败验收滤波器或 ID 配置错误用 CAN 分析仪抓总线原始波形,对比发送帧 ID
中断太频繁,CPU 被拖死每个报文都触发中断改 FIFO 水位中断,批量读取
CRC 校验偶尔失败位填充插入/删除逻辑不完整重点看连续 5 个相同位之后的第 6 位处理

6.2 几条独家经验

做完这个CAN2.0_CANIP_CAN_项目,我最大的体会是:写协议 IP 核,80% 的时间都花在边界情况上,而不是“把功能跑通”。所谓边界情况,包括丢失仲裁的一瞬间、发送期间收到总线错误、接收 FIFO 满的时候来新报文、Bus-off 后的恢复时序等。这些如果不提前处理好,最后都会在总线上变成难查的随机问题。

另外提一句实用建议:如果可能,给你的 CAN IP 加一个总线分析模式,就是纯监听不发送。很多调试场景都需要一块网卡悄悄挂到总线上看报文,不需要它主动发送。我一开始没做这个功能,后来为了抓通信问题又回头加了监听模式,白白多了两天工期。

最后一个小技巧:在 FPGA 内部把txrx信号同时引到调试探针上。这样即使没有外部总线分析仪,也能用集成逻辑分析仪直接看内部波形。遇到协议层面问题,这个办法比外部示波器好用得多。

如果你打算在这个基础上扩展,可以考虑支持 CAN FD,但这不只是改帧格式那么简单,位速率切换、更长 CRC 和收发器快速收发能力都要改。先把 CAN 2.0 玩透了,再做 CAN FD 会顺手很多。

本文还有配套的精品资源,点击获取

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

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

立即咨询