1. 为什么要在FPGA里自己写一个CAN控制器
很多做嵌入式或板卡开发的朋友,应该都搜过类似“FPGA的CAN控制器源码资料”这样的关键词。大家的目标其实都很一致:在手头的FPGA工程里,尽快稳定地跑起CAN通信,最好是纯Verilog实现,能适配Altera和Xilinx等各种常见FPGA平台,而不是绑死在某个厂商的专用IP上。这个需求看着简单,真落地时会发现中间藏着不少协议细节和工程细节,尤其是从“看懂了CAN协议”到“写出能通过总线上电实测的RTL代码”这一步,跨度远比想象中大。
我最初接触这类需求,是在一块带多路CAN口的工业数据采集板上。当时方案讨论了一圈:用带CAN外设的MCU做协议转发,确实省事;但板子上主控已经是FPGA,再为CAN单独挂MCU,成本、体积、软件栈都得增加。另一条路是买现成的CAN控制器芯片,从FPGA拉个SPI或并行总线去调度,这也没问题,就是灵活性稍差,而且芯片供货和批次也让人头疼。最后决定还是在自己熟的那套FPGA流程里,用纯Verilog把CAN控制器写出来。这个决定虽然在前期多花了一些验证时间,但后续带来的自由度非常值:通道数可以按需扩展、仲裁和滤波逻辑可以随意裁剪、波特率配置完全受控,还能把收发的报文直接和FPGA内部的高速信号处理逻辑打通。
所以这篇东西不是去讲一堆泛泛的“CAN原理百科”,而是围绕“用纯Verilog在FPGA上实现可移植CAN控制器”这条主线,把我实际写RTL、做仿真、在两个主流FPGA平台上跑通、再挂到真实总线设备上调试的过程和踩过的坑,完整地梳理出来。适合谁看?正在做FPGA通信接口但又不想碰厂商收费IP的工程师,刚学CAN协议、想动手写状态机练手的同学,以及那些需要给自己板卡增加一路或几路CAN,却不清楚该从哪里下手的硬件设计朋友。
1.1 先把实现方案讲透:外置芯片、硬核IP和纯Verilog该怎么选
很多人看见“FPGA上做CAN”第一反应是:是不是直接调厂商自带的CAN IP核就行?这个思路没错,但有几个现实情况必须摆出来说明白。
先说独立CAN控制器芯片方案。早年常见的做法是在FPGA旁边挂一颗功能简单的外部CAN控制器芯片,FPGA通过并行总线或者SPI管脚去读写它的寄存器。这种方案的优点是协议栈非常成熟、代码量小,缺点是要多花一颗芯片的钱和几十个引脚的布线资源,而且芯片的最大CAN通道数有限,碰到一次要扩展多路CAN的时候,PCB面积和BOM成本会立刻膨胀。另外,外部CAN控制器的中断和缓存机制都是固定死的,你想在收发路径上插一段自定义的报文优先级调度逻辑,几乎做不到。
再看FPGA厂商提供的CAN IP硬核方案。部分高端FPGA芯片内部直接集成了CAN收发控制器硬核,软件上通过配置向导生成封装,用起来比较“爽快”。但硬核IP有它的问题:第一是和具体芯片型号强绑定,换一个平台就不得不重新生成、重新适配;第二,很多硬核IP并不是你想用就能用,得看芯片选型,商用授权和配套文档也未必免费开放;第三,IP核会给你一个封装好的“黑盒”,出了问题、想深挖内部逻辑时经常无从下手。如果项目目标是“资料完备、可自由修改、便于跨平台移植”,它天然不合适。
纯Verilog自研方案解决的正是上面两条路的核心痛点。它本质上就是把CAN协议栈用可综合的硬件描述语言写成自己可以完全掌控的RTL模块,不依赖任何厂商私有逻辑,只要你的FPGA资源有几百个触发器和几百个逻辑单元,就能在几乎任何平台上下下来直接用。修改滤波规则、增加多个独立CAN通道、调整采样点位置、对接自定义总线,全部变成普通RTL改动。代价就是你得吃透协议,把CAN那套仲裁、位填充、CRC、错误状态机在代码里一五一十地“实化”出来。只要这层基本功过关,这种方案的收益长期看是最稳的。
1.2 这些应用场景才是纯Verilog CAN控制器的真正主场
哪些项目值得花这个精力去自研?我在实际接触中总结过三类典型场景。
第一类是多通道CAN汇聚与转发。比如一块数据采集板要同时记录8路CAN总线上不同设备的数据,如果用外部控制器芯片,得挂多颗芯片,通信总线和引脚瞬间紧张起来;但在FPGA里实例化多个CAN控制器模块,每个模块独立一条通道,内部再经一个统一的报文分发结构把数据汇到上位机接口,资源复用和代码组织都很顺。这个场景是纯Verilog方案最舒服的主场。
第二类是协议需要定制或扩展的场景。标准CAN协议对某些工业现场并不完全够用,比如你希望过滤逻辑不仅仅按ID范围,还想按数据内容里的某个字段做二级过滤;或者你希望某几个通道之间可以不经CPU直接做报文路由转发,又或者你要在一个FPGA里同时跑CAN2.0和CAN FD的早期原型验证。这些需求如果拿着商用IP核去改,多半会撞到黑盒墙,但你在自研RTL代码里改起来,其实只是一次普通的功能迭代。
第三类是教学和竞赛场景。很多同学学CAN协议,文档看了很多遍,但内存里对“总线空闲检测”“逐位仲裁”“错误主动”这些概念始终缺一个直观印象。如果能把一个可综合的简化版CAN控制器跑在FPGA上,再用逻辑分析仪抓出仲裁失败时的退让波形,那理解深度完全不一样。这个领域不需要完整的商业级实现,更看重源码结构清晰和注释友好,所以一个分模块、可裁剪的Verilog版本就很受欢迎。
2. 清楚这几个协议机制,写RTL才有方向
写CAN控制器之前,如果直接打开编辑器开始敲状态机,八成会走弯路。CAN协议的机制虽然不像以太网那样审美疲劳,但里面有好几处“看着简单、实现时容易错”的地方。我建议先把以下四个层面的机制彻底搞明白,再动代码。
2.1 显性隐性的电平模型决定了一切
CAN总线物理层用的是差分信号,总线状态只分为两种:显性(Dominant)和隐性(Recessive)。显性电平在逻辑上对应0,隐性对应1,总线上同时出现显性和隐性时,显性电平会“压过”隐性电平,这就是后续所有仲裁机制的地基。
在FPGA内部,CAN控制器其实不直接处理差分总线电压,它和外部收发芯片(总线收发器)之间交互的是单端TTL电平:总线收发器把FPGA发送的显性/隐性逻辑转换成差分信号,又把总线上的差分信号还原成单端逻辑给FPGA收。因此RTL代码核心关注的采样序列是“0/1”序列,但务必要记住:谁先看到0,谁就拥有仲裁优先级。写接收状态机的每一位,处理“总线变0”的时刻都要比“总线保持1”更敏感,任何一位出现0,都可能引发当前节点让出仲裁或终止发送。
很多人写RTL时容易忘记的一个细节是:CAN的“隐形”状态(逻辑1)在空闲总线时是持续存在的。所以接收状态机里判断总线空闲,不应当只靠“没有下降沿”,更稳妥的方式是让空闲检测模块统计“连续11位都为隐性电平”,才判定总线进入空闲态。如果拿外部简单逻辑直接FOLLOW电平,往往会在总线抖动时产生虚假空闲,导致启动一次本来不该有的发送。
2.2 帧格式和位填充:接收解析的重点和难点
CAN2.0B的帧结构大致可以分成帧起始、仲裁场、控制场、数据场、CRC场、应答场和帧结束。标准帧的仲裁场由11位标识符和RTR位组成,扩展帧则由29位标识符构成。标识符本身还承担着仲裁优先级的作用:标识符数值越小,优先级越高,这一点在写发送状态机时要和位流顺序严格对应。
数据场长度由DLC(数据长度代码)决定,DLC在标准帧里占4位,但协议规定超过8字节的数据长度编码不被完整支持(CAN FD里的长度编码才更灵活)。接着是15位CRC,它覆盖的范围是从帧起始直到数据场结束的全部内容。
这里最“折磨人”的是位填充机制。CAN协议规定,在帧起始到CRC场之前,如果发送方连续输出5个相同极性的位,后面必须插入一个反向的填充位。接收方在读总线时一旦发现连续5个相同位,也要按规则“吃掉”紧接着的第6个反转位,同时把它作为帧校验的一部分。
位填充机制让RTL的接收解析逻辑不能简单照着理想帧结构一位一位地数完就完事,你必须在逐位采样的过程中维护一个“连续相同位计数器”。当计数器到5时,下一个采样值无论是什么,都不能当作真正的数据,只能作为填充位跳过。这一步一旦写漏,收到的数据在报文长度稍长时立刻会错位。同理,发送模块也需要一个完全对称的填充逻辑,很多初版代码发短报文没问题,发长报文就跑飞了,绝大多数是填充状态机的问题。
2.3 位时序和采样点:决定能否在真实总线上稳定通信
CAN不是像UART那样简单把波特率时钟分成一位一位,而是把一个位的时长分成若干个时间量子(TQ),再组合成同步段、传播段、相位缓冲段1和相位缓冲段2。采样点通常落在相位缓冲段1和相位缓冲段2的边界附近。
为什么非要这么搞?因为CAN总线是共享总线,多个节点必须对“一个位的开始”“在一个位之内总线状态可能发生变化”有统一的认知。同步段用来检测总线上由隐性跳变到显性的边沿,传播段用来补偿收发器和总线上的信号延迟,两个相位缓冲段则允许通过重同步能力灵活调整采样时刻,以对抗各个节点时钟的微小偏差。
用公式表示:一个位的总TQ数 = 同步段TQ + 传播段TQ + 相位缓冲段1 TQ + 相位缓冲段2 TQ,通常配置成8到25个TQ。采样点位置的计算公式大致是:(同步段 + 传播段 + 相位缓冲段1) / 总TQ。在实际项目中,我一般把采样点配置在80%左右,比如500k波特率、50MHz系统时钟下,总TQ设为25,预分频1,采样点取第20个TQ附近,这样对总线传播时间的长短比较宽容。
同步机制也要在RTL里体现:硬同步发生在帧起始的第一个隐性到显性跳变,重同步则发生在帧内部,同步跳转宽度(SJW)决定了每次最多能调整多少个TQ。很多“偶尔丢一帧但没规律”的底层问题,最后都是采样点偏了或者SJW配置过小导致的。
3. 整体架构怎么拆:模块划分、寄存器接口和时钟设计
把协议机制搞清楚后,动手写RTL之前,先画一遍模块划分和接口草稿。这个阶段多花半小时,后面调试能省好几天。
3.1 顶层模块划分与信号流
我最终采用的顶层结构大致分为6个子模块:位时序管理、发送状态机、接收状态机、CRC单元、填充单元、寄存器配置接口。另外还有报错管理和验收滤波模块,可以根据需要裁剪。
| 模块名 | 主要职责 | 关键对外接口 |
|---|---|---|
| 位时序模块 | 产生位时钟、采样点脉冲、同步跳转逻辑 | 输入系统时钟和波特率分频寄存器,输出采样脉冲 |
| 发送状态机 | 组织发送帧,完成逐位输出、仲裁退让、错误帧生成 | 读发送缓冲区,输出总线TX电平 |
| 接收状态机 | 逐位采样、解析帧结构、填充位删除、错误判断 | 采样总线RX电平,写接收缓冲区 |
| CRC模块 | 计算并校验CRC15,支持标准帧和扩展帧 | 输入串行数据和帧类型,输出CRC结果 |
| 填充模块 | 发送时插入填充位,接收时删除填充位 | 位流输入输出和填充计数使能 |
| 寄存器接口 | 提供读写控制寄存器、状态寄存器、中断控制 | APB或用户自定义总线接口 |
| 错误管理模块 | 维护错误状态寄存器,控制错误主动/被动状态 | 输入错误标志,输出总线状态和中断 |
模块间的位流信号要尽量保持串行一位一位流动,不要在图省事时把整帧做成并行大块放到内部FIFO里,那样会极大地浪费寄存器资源,而且时序收敛会很痛苦。核心数据通路上的信号,比如TX位流、RX位流、采样点脉冲,建议单独命名为can_tx_bit、can_rx_bit、sample_point这样的风格,调试时一眼能定位。
顶层还有一个很实用的设计原则:让CAN控制器对外呈现为一个带简单寄存器的从设备接口,上位机通过读写寄存器来配置模式和取数据。接口总线的选择不用纠结,无论是AVALON还是AXI-Lite还是用户自定义的简便总线,最终在顶层做一个内部寄存器组适配即可。因为CAN控制器核心逻辑不依赖这些具体总线,适配层写到100行以内就能完成,这也是“可移植”的重要保障。
3.2 发送与接收状态机的设计思路
发送状态机我建议按协议逐段划分为:空闲、本帧起始、仲裁场发送、控制场与数据场发送、CRC发送、ACK槽接收、帧结束、间隔等待。仲裁场发送阶段不仅要输出标识符位流,还要在每一位周期监测RX电平,如果发现自己输出的是隐性1而总线上采样到显性0,立刻主动退出仲裁,转入“隐性监听状态”,直到当前帧结束再回空闲。
这里有个必须严格处理的地方:仲裁只在仲裁场中有效,到了控制场及以后,一旦进入发送就不可退让,协议规定如果在这种阶段发现总线被其他节点占据,应当产生错误帧并进入错误处理流程。很多初版代码把“仲裁退让逻辑”扩散到了整个帧周期,导致的后果是正常多节点通信时节点随意放弃发送,总线上大量错误帧。
接收状态机的关键则是维持“当前正在解析的帧字段”和“填充状态”两组状态。在进入数据场之前,接收到的每一位都可能同时影响字段切换和填充计数;填充分隔符、CRC界定符、ACK槽、EOF这几个字段是不参与反独占填充的。初学者常在这里把字段判断写成“按理想位序计数字符”,一旦总线上混入错误帧或填充位异常,整个接收状态机就卡死了。正确的写法是每个字段分别定义状态,位计数值只在所属字段内有效,字段切换时清零。
3.3 寄存器映射建议:兼容经典寄存器会增加复用性
CAN控制器的寄存器设计,我建议参考目前行业里用得比较广的经典控制器寄存器布局,比如模式寄存器、命令寄存器、状态寄存器、中断寄存器、验收滤波寄存器、波特率分频寄存器。不用逐字逐句抄,但保持“管理层、状态层、数据层”清晰分离。
我实际使用的配置寄存器大概包括这样几个字段:模式控制字段(复位、监听模式、自测回环模式、总线关闭恢复方式)、波特率分频和位时序参数段(预分频、TQ数、采样点位置、SJW)、验收滤波模式字段(单滤波、双滤波、屏蔽模式)、中断使能字段。数据通路通常由一个发送缓冲区和两三个接收缓冲区组成,每个缓冲区内分配标识符字段、DLC字段和8字节数据段。寄存器数量控制在20个左右,代码结构会很清楚。
3.4 时钟域与跨时钟域设计
大多数CAN控制器设计里,位时序模块、收发状态机和总线收发器中间都工作在同一个时钟域,也就是FPGA的系统主时钟经过PLL产生的工作时钟。因此内部不涉及复杂的跨时钟域问题,最关键在于把寄存器的读写入操作同步到工作时钟域。
如果上位机总线速度很低或者来自另一个异步时钟域,就需要在寄存器接口处插入两级同步器或异步FIFO。比如把CAN控制器挂到某个低速并行总线上时,寄存器读写的地址和数据信号都应当经过同步,确保在CAN工作时钟上升沿稳定采样。这块如果省略,现象就是写配置寄存器偶尔不生效、读取发送状态寄存器的时候抓到中间态。
还有一个容易被忽略的时钟问题是位计时模块输入时钟的频率选择。不要让CAN控制器的位时序模块直接使用几十上百MHz的高频差分时钟,而是先用PLL生成一个干净的低抖动工作时钟,再统一分发到所有模块。这样在计算波特率分频、生成采样点脉冲的时候,时序关系简单可控,Quartus或Vivado在时序分析时也更容易给出干净的收敛结果。
4. 关键Verilog代码的实战写法:位时序、填充、CRC与错误管理
下面给出一段我在项目中实际使用过的核心代码片段。这里不追求贴出几千行完整源码,而是把最容易卡住大家的几个关键逻辑点拆解出来,每个知识点都能直接迁移到自己的实现里。
4.1 位时序模块的计数逻辑
CAN控制器的位时序模块本质是一个可配置的计数器。我通常维护一个tq_count,从0计数到(总TQ数-1),在每个TQ计数结束点产生一个位时钟脉冲bit_clock_pulse,同时在特定TQ位置产生sample_pulse。
module bit_timing ( input wire clk, input wire rst_n, input wire [15:0] tq_total, // 一个位的TQ总数 input wire [15:0] sample_tq, // 采样点所在TQ位置 output wire sample_pulse, // 采样脉冲 output wire bit_done // 当前位结束脉冲 ); reg [15:0] tq_cnt; reg cur_bit; assign sample_pulse = (tq_cnt == sample_tq) ? 1'b1 : 1'b0; assign bit_done = (tq_cnt == (tq_total - 1)) ? 1'b1 : 1'b0; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tq_cnt <= 16'd0; end else if (bit_done) begin tq_cnt <= 16'd0; end else begin tq_cnt <= tq_cnt + 16'd1; end end endmodule这段代码最需要注意的地方是采样点脉冲的生成。采样点不能设在TQ0,也不能太靠前,否则在总线边沿附近采样容易采到跳变过程中的不稳定电平。实际项目中,我会根据波特率计算好sample_tq后写成一个寄存器,调试时可以直接调整。如果现场遇到偶发错误帧,优先在合法范围内把采样点往后移动几个TQ。
4.2 发送方向的位填充实现
位填充逻辑分开看其实不复杂:发送时维护一个当前连续相同位计数器,每发出一位数据位,如果该位和前一位极性相同,计数器加1;一旦计数器到5,下一位必须强制输出与前一位相反的填充位,同时计数器清零。关键点在于“只有从帧起始到CRC场之前的内容参与填充”,填充位本身不参与再次计数。
一个简化的Verilog描述如下(仅示范填充计数思路):
reg last_tx_bit; reg [2:0] same_cnt; wire insert_stuff; assign insert_stuff = (same_cnt == 3'd5); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin last_tx_bit <= 1'b0; same_cnt <= 3'd0; end else if (insert_stuff) begin last_tx_bit <= ~last_tx_bit; same_cnt <= 3'd0; end else begin if (next_tx_bit == last_tx_bit) same_cnt <= same_cnt + 3'd1; else same_cnt <= 3'd1; last_tx_bit <= next_tx_bit; end end写这段逻辑时有三个常见的坑。一是计数器初值和边界:第一位置为“连续相同位=1”而不是“0”,否则连续的两位相同位可能到不了5就被漏插。二是insert_stuff判断必须优先于所有数据位判断,否则填充位会被当成数据位写入总线。三是注意发送仲裁场的RTR位、IDE位期间,填充逻辑要正常工作,不能因为字段切换就把计数器清零。
4.3 接收方向的反填充与CRC校验配合
接收方向的反填充是整个接收状态机里最容易写崩的一半。逻辑上要在每一位采样结束时检查连续相同位计数,如果连续5位相同,则第6位采样值不是数据,而是填充位,需要跳过,并且不能进入任何字段的数据。为了防止状态机在填充位期间执行字段计数器等逻辑,通常的做法是单独产生一个data_valid信号:填充位期间data_valid为低,真实数据位期间为高。
CRC模块则相对独立:发送时,把填充前或填充后的数据位流送入CRC计算器(注意CAN的CRC输入是未经填充的原始位流),计算完CRC后把结果也填入。接收时校验的思路也一样,接收方把接收到且经过去填充处理的位流重新计算CRC,最后与收到的CRC字段对照。如果在ACK槽之前出现CRC错误,要立即置位CRC错误中断,同时按协议产生错误帧。
我建议CRC计算单元不要写成一长串组合逻辑展开,而是用一个带反馈的移位寄存器实现,每一位进一个时钟算一步,结构清晰且时序友好。同样,CRC模块应该支持标准帧和扩展帧的差异,它们的填充起始位置和CRC覆盖范围有些许不同,代码里要预留一个帧类型信号。
4.4 错误状态机的关键状态位
CAN的错误管理模块需要跟踪两种计数值:发送错误计数TEC和接收错误计数REC。错误主动、错误被动和总线关闭三种状态,就是根据TEC和REC的阈值来切换的。实际协议里还要区分“主控上报错误”和“接收检测错误”计数的增减规则:成功收发时计数要递减,错误累计则递增,递增和递减的比例并不对称。
很多简化版本的实现只做“犯错就记一账”而完全不做递减,短期调试可以,长期接真实设备必然出问题。真正要稳定通信,错误状态机必须按照CAN协议表格里的增减规则做,尤其要注意在发送错误计数超过127后节点必须进入错误被动状态,此时再出错就要发错误被动标志,差错处理节奏完全不同。我见过一个项目在协议一致性和总线上多节点并存时,因为错误计数规则被简化,导致节点从错误主动变成总线关闭状态却无法自动恢复,整条总线被一个故障节点拖住。
5. 跨平台移植的实操细节:Altera和Xilinx都能跑起来
标题里强调“可在Altera和Xilinx等FPGA上运行”,这一点也是实际交付时用户最关心的。核心代码只要是标准可综合Verilog,跨平台基本没有逻辑上的障碍,真正的差异集中在时钟资源、复位策略和IP配置方式上。
5.1 时钟和复位策略的适配
Altera器件和Xilinx器件的时钟资源虽然名字不一样,但本质都是PLL/MMCM。CAN控制器入口一般只接收一个工作时钟clk和低有效异步复位rst_n。为了避免在移植时反复改代码,我建议在IP的顶层定义一个标准时钟接口,再由不同平台的顶层适配文件把各自的PLL输出转换过来。
复位策略上,Altera器件习惯使用全局异步复位、同步释放;Xilinx平台也支持同样的模式。CAN控制器的复位信号必须保证在最开始几个时钟周期内稳定拉低,待PLL锁定后再释放。一个比较稳妥的做法是在顶层用两个触发器级联产生内部复位:外部复位拉低时内部复位立即拉低,外部复位释放后内部复位延迟几个时钟周期再释放,这套结构不依赖具体器件,跨平台表现一致。
5.2 不同平台的引脚和约束文件
跨平台移植时,综合、布局布线的工程配置文件各写各的,但RTL代码本身不应改动。实际操作中,我一般按平台分别维护一个约束文件:在Altera平台的Quartus工程中写SDC文件约束时钟周期和引脚location,在Xilinx平台的Vivado工程中写XDC文件。只要顶层引脚名固定,底层模块的例化和信号连接可以完全复用。
一个容易忽略的问题是跨平台时PLL的生成方式。Altera上常用ALTCLKCTRL或嵌入式PLL IP,Xilinx上则需要用Clocking Wizard修改频率。如果不想在每次换平台时都重新生成IP,可以考虑不去调用厂商的PLL宏单元,直接把外部时钟引入,在PCB上做时钟分配,或者只在每个平台各写一个几行的PLL例化文件。这样可以保证剩下的CAN控制器源码彻底摆脱平台绑定。
5.3 资源占用与时序收敛
主流FPGA平台跑一个单路CAN控制器,资源占用其实非常小。以一般中低端FPGA器件为例,实现一个完整CAN2.0B控制器通常只需要几百个LUT和几百个触发器,占用的BRAM也主要用在校验FIFO上。即使要做8路CAN通道,总资源损耗也在一个中等规模FPGA的承受范围内。
真正需要注意的时序收敛点在于位时序模块的计数器链路和CRC移位寄存器链。如果会把高位宽比较放到关键路径上,建议做成流水线或者减少组合逻辑级数。实际布局时,系统时钟只要控制在50MHz到100MHz以内,多数工程一次编译就能收敛,不需要做太多额外约束。有个实用技巧:把采样点生成逻辑放到靠近接收引脚的地方,并添加适当的引脚伪路径或时序例外,让工具在布线时优先保证采样路径的时序。
6. 验证与调试:从testbench到挂真实CAN设备
写完RTL只是完成了三分之一,真正的考验在验证。这一章讲一讲我要跑通一套CAN控制器时,会从哪几个层面去做验证。
6.1 仿真阶段的回环测试
仿真不需要一开始就搭复杂的多节点总线模型。最简单的做法是配置内部自测回环模式:发送状态机发出的位流直接馈送给接收状态机,不通过外部引脚。这个模式能快速验证寄存器读写、配置、发送缓冲区、接收缓冲区、填充、CRC、状态机切换等几乎全部基础功能。
testbench的基本结构是:例化顶层DUT,生成100MHz或50MHz系统时钟,模拟外部寄存器的读写,先让DUT进入配置模式,写入波特率参数、验收滤波寄存器,然后启动一次发送。写入几组已知ID和数据的报文,仿真结束后再检查接收缓冲区的内容。这里我建议在testbench里集成一个简单的数据比对任务,每次发送报文后自动把接收端读到的那组数据与发送初值比较,不一致时打印出错计数,避免肉眼盯波形盯到怀疑人生。
如果做更贴近真实场景的仿真,可以用两个CAN控制器例化在同一个testbench里,通过外部wire互联它们的TX和RX引脚,各自独立配置,模拟A节点发给B节点的过程。这样仲裁退让、错误帧检测、多节点收发的行为都能覆盖。
6.2 在FPGA上在线调试:抓精准信号
仿真通过后,下一步是综合下载到FPGA开发板或者自己的工程板上。在线调试我习惯的做法是把系统时钟和CAN收发引脚之间留几个调试信号,比如关键状态机state状态、采样点脉冲、总线忙信号、发送使能信号,全部引到逻辑分析仪调试IP里。
Altera平台通常用SignalTap逻辑分析仪,Xilinx平台通常用ILA调试核。它们的用法大同小异:在RTL网表里选中你想观察的信号,设置触发条件为“发送状态变化或总线错误中断”,然后跑一段真实总线通信,抓取故障瞬间的位流状态。很多偶发问题,比如“为什么发送一帧时在CRC场就退出了仲裁”,用在线逻辑分析仪抓状态机位流一眼就能定位到是在哪一位发生了异常。
6.3 接真实总线的三层调试法
在线调试过了,接下来必然要把CAN控制器的引脚接到外部CAN收发器芯片上,和真实的总线设备互联。我第一次做这个环节时踩过不少坑,总结出三层调试顺序。
第一层是环回模式接硬件。把控制器配置成回环模式,但收发引脚经过真实的CAN收发器芯片后再内部环回,此时可以确认外部收发器芯片的供电、引脚映射和电平转换是否正常。第二层是纯FPGA自发自收,用一根导线短接到外部已配置好的总线分析仪,或者接到一个谐振匹配的简单接收器上,检查波形是否满足隐性显性切换、毛刺率是否可接受。第三层才建议直接并联到有两个以上真实节点的CAN总线上,配置好终端电阻,用上位机工具或另一块带CAN的设备做交互通信。
三层逐步推进的好处在于,当通信失败时,你很清楚问题大致出在哪个环节。直接接到多节点总线上,设备一多,症状五花八门,排查成本会成倍上升。
7. 常见问题排查速查表:这些坑我基本都踩过
最后把这套CAN控制器在FPGA上从仿真到实测过程中遇到频率较高的几个问题整理成表格,方便大家对照排查。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 仿真正常,上板后完全收不到报文 | 引脚分配错误或收发器芯片未配置 | 检查引脚约束、收发器芯片的使能引脚和供电 |
| 能发送一帧,长时间通信后偶发业务中断 | 采样点位置偏早或SJW配置过小 | 调整采样点,确保采样点后留有足够相位缓冲2余量 |
| 两个节点通信正常,三个节点以上频繁出错 | 错误状态机未按协议递减计数 | 检查TEC/REC增减规则,确认错误被动转换逻辑 |
| 同一套代码在Altera平台正常,Xilinx平台不正常 | PLL时钟未锁定或复位释放时序异常 | 检查PLL配置和复位同步释放电路 |
| CRC错误帧频繁且规律性出现 | 接收方向反填充逻辑在连续位时错位 | 单独测试位填充模块,连续发同一极性的长数据段 |
| 发送长报文时状态机跑飞 | 发送状态机字段计数器未在字段边界清零 | 检查字段切换逻辑,确认CRC段启动条件 |
| 接收中断偶尔多触发一次 | 空闲检测判定条件过松 | 改为连续11个隐性位才判定为空闲 |
| 差分支路振荡但软件看正常 | PCB走线或终端电阻问题 | 检查总线终端电阻和收发器芯片外围电路 |
排查技巧上,我最常用的招数是“在发送端强制插入固定错误位”,比如通过寄存器配置让发送模块在控制的某个位故意发送相反极性,再观察错误管理模块是否按预期置位状态中断。这比在真实总线上制造干扰信号要安全得多,也能快速验证错误处理链路是否完整。
还有一个值得单独提醒的经验:CAN控制器的验收滤波模块不要一开始就做得太花哨。很多项目在初期为了灵活接入了复杂的双滤波和掩码逻辑,结果寄存器配置参数组合暴多,两两之间还有优先级问题,调试时反而看不清基础功能是否正常。初期先做最简单的“全接收”模式,跑通通信后再逐步增加掩码过滤,每一步都验证一次数据正确性。
最后再分享一个小心得:当你在FPGA上写CAN控制器时,别把所有模块都追求一步到位。先用最简状态机把“能发一帧、能收一帧”跑通,再逐步加入错误处理、仲裁退让、自动重发、错误计数递减。这个“由简到繁”的开发顺序,能让你把每个环节的因果关系看得清清楚楚。很多刚开始接触的朋友喜欢一上来就照着完整协议表格把几十个状态全部铺开,结果第一次出现异常时,面对一堆状态位根本无从下手。我的习惯是,每个新加的功能都单独做一次仿真和一次上板测试,确认稳定后再加下一个。这样虽然前期节奏慢一些,但后期联调时非常省心,几乎没有出现“改动一行却不知道动坏了什么”的窘境。如果你的项目也在走这条纯Verilog实现CAN控制器的路,希望这篇笔记能帮你少绕几个弯。