资讯动态

FPGA上实现CAN 2.0控制器IP核:从协议到Verilog实战

发布时间:2026/8/31 22:19:59 来源:尧图企业网站定制
简介本资源是一套面向嵌入式系统与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外部时钟 50MHzCAN 收发器用的是 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.0BSOF1 位显性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_Seg1TSEG113TSEG26采样点就是(113)/(1136)14/2070%。如果希望采样点更靠后比如 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_counter和sampling_point两个信号。每次同步段结束计数器清零计数到采样点位置时产生一个高电平脉冲位流处理器在这个脉冲沿采样总线。验收滤波器可以做得简单也可以做得复杂。简单版就是一组 32 位验收码加 32 位掩码接收帧的 ID 和验收码做位比较掩码为 0 的位不关心。复杂版可以做成多组滤波器但寄存器开销大。我这个项目先用了简单版实测够用。3.3 与 MCU 的接口APB 还是 AXI-LiteCAN 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、SJW0x18ACRR/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 1b0) (rx_bit 1b1); 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 15d0; 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 接口时用psel和penable生成写使能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 32h0; btr_reg 32b00000010_00000111_00000011_00000101; // example reset value end else if (apb_write) begin case (paddr) 5h00: mode_reg pwdata; 5h14: 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_rx和can_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是整数分频但我们时钟是 50MHz5 分频后 tq 是 100ns20 个 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 内部把tx和rx信号同时引到调试探针上。这样即使没有外部总线分析仪也能用集成逻辑分析仪直接看内部波形。遇到协议层面问题这个办法比外部示波器好用得多。如果你打算在这个基础上扩展可以考虑支持 CAN FD但这不只是改帧格式那么简单位速率切换、更长 CRC 和收发器快速收发能力都要改。先把 CAN 2.0 玩透了再做 CAN FD 会顺手很多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价