基于CAN IP核的FPGA通信Verilog源码设计与调试实践

发布时间:2026/10/21 10:03:54
基于CAN IP核的FPGA通信Verilog源码设计与调试实践
做了这么久FPGA开发说实话CAN总线通信这个需求在工业控制和车载电子里太常遇到了。很多朋友一上来就问想用FPGA做CAN是不是必须自己把协议栈全部撸一遍答案是不用。我这次分享的是一套基于某主流FPGA厂商CAN IP软核的Verilog源码方案整体思路就是站在厂商IP的肩膀上做二次封装把复杂的位时序、仲裁、错误处理交给IP核我们只需要管好寄存器配置和收发逻辑。整套源码都是可以直接拿到工程里用的级别关键模块注释写得很细哪怕你不是资深FPGA工程师照着把配置捋一遍也能把CAN通信跑起来。1. 整体方案设计FPGA做CAN到底图什么1.1 为什么不用现成的MCU内置CAN控制器很多设备里要用CAN第一反应是选一颗带CAN控制器的MCU。这个没毛病MCU方案确实成熟上电之后调个库函数就能收发。但真到了某些场景MCU方案就很尴尬一路CAN不够用要扩展两路、三路甚至更多MCU得带多个CAN外设料件选型一下子窄了或者你需要对CAN报文的过滤逻辑做非常灵活的定制比如按时间片切换过滤规则MCU那个硬件过滤寄存器很难随心所欲地改再比如整套系统本身就是FPGA为主导做的信号采集和协议转换为了CAN再塞一颗MCU进去还要解决两片芯片之间的通信握手成本和复杂度都上去了。所以FPGA方案的价值不在“能不能发CAN帧”而在“怎么把CAN和你的核心逻辑无缝集成”。用厂商提供的CAN控制IP核把协议栈妥妥地封装好用户侧只需要操作寄存器。你要两路CAN就例化两个核要三路就例化三个扩展性极好。而且FPGA做CAN收发延迟是确定性的没有操作系统调度抖动对要求严格时序的场合来说这种确定性本身就是硬通货。1.2 用IP核还是自己写CAN协议栈刚开始我其实也动过自己写CAN控制器链路层的念头。CAN总线的本质是CSMA/CA带优先级仲裁的串行协议里面有同步段、传播段、相位缓冲段还有位填充规则、CRC校验、错误计数这些机制。真从头写一套能过一致性测试的CAN控制器工作量相当可观而且自己写的代码未必能达到车规级可靠性要求。搞了几天之后我就想明白了厂商把CAN IP核免费丢给你就是为了让你把精力放在上层应用和系统集成上何必和协议栈死磕呢。厂商提供的CAN IP软核一般支持标准帧和扩展帧具备收发FIFO、验收滤波、错误处理中断等能力。我们要做的就是写好一个顶层封装把复位、时钟、寄存器配置、中断处理、数据通路这些部分理顺。这也是这套Verilog源码的结构安排底层直接复用IP核上层用一个can_top模块包起来对外暴露的接口简洁清晰内部把寄存器读写的繁琐细节都消化掉。2. CAN IP核配置参数比代码更关键2.1 波特率与采样点计算一步一步推给你看CAN通信能不能稳定跑起来九成取决于波特率和采样点配得对不对。IP核内部通常有一个baud_rate_prescaler波特率预分频器和time_segment寄存器用来把系统时钟切成一个个位时间bit time。一个位时间由四段组成同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段PHASE_SEG1、相位缓冲段PHASE_SEG2采样点就落在PHASE_SEG1边缘。CAN协议里有严格约束PHASE_SEG1和PHASE_SEG2不能比SYNC_SEG短通常每个时间段用多少个Time QuantumTQ来量化。我拿一个常见配置举例。假设系统时钟是50 MHz目标波特率500 kbps每个位时间规划成10个TQ。先算预分频系数TQ 系统时钟周期 × 预分频系数 波特率 系统时钟频率 / (预分频系数 × 每个位时间TQ数) 预分频系数 50 MHz / (500 kbps × 10) 50 MHz / 5 MHz 10也就是说预分频系数设10一个TQ是200 ns一个位时间2 us正好500 kbps。时间段分配我习惯配置成SYNC_SEG1、PROP_SEG3、PHASE_SEG14、PHASE_SEG22。这样采样点落在(134)/1080%的位置正好在CAN协议推荐的75%到85%区间中间容错性比较好。这组分配建议先照抄等实际联调的时候再用示波器或者CAN分析仪去验证。采样点太靠前总线上延长的传播延迟会被误判成位错误采样点太靠后又容易把下一个位开始的跳变沿卷进来。80%附近是个甜点区。2.2 验收滤波从一堆报文里挑出你要的CAN总线是一根线上的所有节点收到同一帧报文的那怎么判断这帧是不是发给我的呢靠的就是验收滤波。IP核里通常有一个或多个验收代码寄存器Acceptance Code RegisterACR和验收掩码寄存器Acceptance Mask RegisterAMR。掩码位是0表示这一位必须匹配代码寄存器掩码位是1表示这一位不关心。举个例子我想接收所有ID以0x1F0到0x1FF这个范围为主的报文可以把代码寄存器的“关注位”设成0x1F0对应的ID段掩码寄存器低4位设为1表示忽略其余位设为0表示严格匹配。这样总线上发来的一堆0x200、0x320之类的帧就会被直接滤掉不会进接收FIFO。验收滤波的逻辑虽然IP核帮你执行了但具体配置值还是得自己算算的时候把标准帧和扩展帧格式区分开别把H/L两个字节的对应关系搞反了这是很容易踩的低级坑。3. Verilog源码结构与核心模块实现3.1 模块划分与信号约定源码打开先看这些这套源码的工程里文件结构很简单核心就这几个Verilog文件can_top.v顶层封装包含IP核例化、寄存器地址译码、中断整合can_config.v上电自动配置状态机负责往IP核写入波特率等参数can_tx.v发送任务调度模块把应用层请求打包成IP核能识别的寄存器操作时序can_rx.v接收模块负责从接收FIFO里把数据搬运到用户数据通路can_apb_bridge.v总线读写桥给IP核寄存器提供一个简洁的读取写入接口工程里所有端口都有注释每个模块顶部有一段模块说明写明功能、端口含义和简单的读写时序示例。我特别建议你打开源码后先看can_top.v顶部的注释块那里画着一份收发数据流图虽然是用文字形式表达的但能把整个通路的上下游关系讲明白。寄存器读写时序我统一封装在can_apb_bridge.v里。对外只暴露reg_wr_en、reg_addr、reg_data_in、reg_data_out这几个信号后续不管你是用状态机还是嵌入式软核来配置接上去就能用。这一步封装很重要省得在业务逻辑代码里到处散落一堆地址魔法数字。3.2 初始化配置状态机上电后三秒内把IP核带到正常模式CAN IP核上电后不会马上进入正常通信状态需要按一定顺序把控制寄存器配置好。这套源码里的初始化配置状态机大概是这样一个流程等待系统复位释放和时钟稳定至少等上几十个周期让内部状态归位往模式寄存器写入复位模式强制IP核进入配置等待状态写波特率预分频寄存器、时间段寄存器、采样点配置配置验收过滤相关寄存器写中断使能寄存器把接收FIFO非空、发送完成、错误警告这些中断打开模式寄存器切换为正常模式等待IP核确认当前处于总线在线状态整个流程用一段有限状态机顺序执行每一拍写入完成之后会检查寄存器回读值确认写入生效再走下一步。这种“写—读—确认”的稳妥写法比无脑延时之后一路写下去可靠得多因为不同IP核配置寄存器生效时机略有差异回读确认是最保险的方式。关键信号是CAN_MODE状态寄存器里那几位状态标志。切换到正常模式之后我会用一个循环等待去轮询它等它报告已经同步到总线上的时候才对外拉高can_ready信号。我见过不少初学朋友代码里没有这个ready信号结果配置刚结束就立刻发数据此时IP核还没有完成总线同步第一帧就丢在空气里了。这一版源码专门加了can_ready输出你在自己的应用逻辑if(can_ready)再启动发送稳得很。3.3 发送流程从应用请求到总线电平的最后一公里发送模块要做的事情说简单也简单就是把“我要发ID0x123DLC8数据...”这套请求翻译成IP核寄存器操作时序。CAN发送寄存器操作顺序一般是先把数据填进发送缓冲区寄存器接着把帧格式和DLC信息写入仲裁寄存器最后置发送请求位。置位之后IP核会自动处理总线仲裁、位填充、CRC校验发完之后通过发送完成状态标志或者中断通知我们。源码里发送模块对外接口长这样input tx_req, // 发送请求脉冲 input [28:0] tx_id, // CAN ID标准帧用低11位扩展帧用低29位 input [3:0] tx_dlc, // 数据长度 input [63:0] tx_data, // 8字节数据 output reg tx_done, // 发送完成 output reg tx_busy // 发送忙忙时外部不应发起新请求收到tx_req之后发送状态机会先检查tx_busy空闲才拉高busy并开始事务。整个发送序列用移位寄存器按位拼出一个32比特的“寄存器地址数据”控制字逐步写入IP核。这中间有个容易踩的细节CAN报文内部数据字节序和你在总线上看到的字节序不一样发送数据时必须做字节序转换比如本地数据数组[0]对应ID/DLC之后最先发出的那个字节。源码里直接写好了一个byte_swap逻辑你只需要按业务习惯填tx_data就行。发送完成之后我习惯让模块多等一个位时间再拉低tx_busy因为IP核实际释放发送缓冲区还需要几个周期如果立刻允许新请求极大概率会撞上“发送缓冲区仍在忙”的标志导致请求丢弃。3.4 接收流程FIFO搬空和帧信息解析接收方向的逻辑核心是及时把接收FIFO里的帧读出来释放FIFO空间然后完整地把ID、DLC、数据解析到用户接口。接收状态机的流程是轮询中断状态或FIFO计数器发现非空后先读帧信息寄存器判断是标准帧还是扩展帧接着读数据字节读完后写释放接收缓冲区命令。接收数据同样要做字节序还原否则你会看到总线上发过来的01 02 03 04在你的系统里变成04 03 02 01。这个问题在仿真里不容易暴露因为仿真模型收发数据往往完全对称到了真机上跟别的CAN设备联调字节序错了数据一对比就原形毕露。接收接口我做了两级FIFO缓冲。第一级是IP核自带的硬件FIFO第二级是源码里自己写的一段简单双端口RAM暂存模块。这样做的目的是让接收侧和用户主逻辑解耦用户主逻辑哪怕偶尔忙得晚来几步读数据也不会立刻导致IP核接收FIFO溢出丢帧。对于突发流量比较大的场景这层缓冲非常管用。4. 仿真验证与上板实测从波形到真相4.1 回环模式仿真不用硬件也能跑拿到代码第一步先在仿真环境里跑回环测试。回环模式是CAN IP核自带的测试功能发送路径在IP核内部直接转回接收路径不需要外部接入CAN收发器不需要接第二块板子。在这套源码的Testbench里我把接收回环使能位置1然后用Testbench模拟应用层发送一帧标准帧ID0x123DLC8数据头两个字节填0xAA、0x55。跑完仿真波形检查接收侧能否在下一个位周期收到同样的帧内容。回环仿真能帮你验证三件事寄存器配置流程有没有写错、时钟复位时序有没有问题、发送接收状态机能不能走通。波形上重点看寄存器接口的读写时序地址相位和数据相位是否满足IP核的时序要求。我一般建议仿真跑满几百个位时间至少发个几十帧把连续收发和间隔收发都覆盖到。如果回环仿真都过不了先别急着上板检查仿真日志里有没有打印出IP核上报的错误寄存器值。CAN控制器IP核很多都带错误计数寄存器发送错误或接收错误计数一旦超过阈值IP核会进入Bus-off状态收发全挂。仿真阶段就能看到错误计数在增长说明你配置的波特率分频和时间段参数有问题趁早回头改。4.2 上板实测两块板子互发才算数回环仿真通过之后就该上板了。我习惯按这个顺序来先在FPGA板上焊接好CAN收发器电路注意CAN_TX出去要接到收发器的TXD输入CAN_RX接收对应RXD输出千万别接反接上120欧终端电阻CAN总线两端各一个短距离调试时至少也要一头有终端电阻用一块现成的CAN分析仪接到总线上和FPGA板卡共地上电后先用逻辑分析仪抓CAN_TX引脚确认FPGA内部确实产生了波形再抓收发器出来的CAN_H和CAN_L差分信号看显性位和隐性位电平是否正常实测时我最关心的就是电平转换关系。显性位收发器会拉高一对差分电平隐性位回到0差分。如果你看到CAN_H和CAN_L完全没有差分动作大概率是FPGA到收发器的电平方向接反了芯片一直在憋着没法驱动总线。如果FPGA这边已经有发送波形但总线侧没动静先测一下收发器供电对不对收发器的VIO和VCC引脚都要按数据手册正确供给。CAN收发器通常需要5V或3.3V电源我用过的好几种收发器都是两路供电一旦基准电压VIO没接对TX端逻辑电平根本不翻转总线当然纹丝不动。两块板子互发的实测场景我会把其中一块板子配成500 kbps发送标准帧另一块配成接收各自打印或记录收到的报文内容。这一步建议先用CAN分析仪看帧类型、ID、DLC、数据对不对帧对上了再下结论说协议层通了。我遇到太多情况是物理层通了但数据字节序反了看起来“能收到帧”但内容怎么都对不上。4.3 实测心得波形细节比现象更重要联调时我很推荐在逻辑分析仪里抓两条信号一条是FPGA发出的CAN_TX另一条是收发器输出的CAN_RX。这两条信号对应总线上原始数据和接收回环后的数据对比它们就能直接把波特率误差和采样点问题暴露出来。比如发送的位宽度如果明显比理论值宽或者窄在CAN分析仪上就会表现为持续的位错误。原因通常是系统时钟实际频率和设计假设的不一致或者预分频系数四舍五入带来的舍入误差。CAN协议要求任意两个节点的位时间相对误差小于1.5%如果总线上没有一个节点是标准时钟源误差会叠加这种时候只能重新检查晶振频率和PLL倍频配置。我还发现一个容易忽略的小坑CAN_H和CAN_L连接线太长且没有双绞的时候信号边沿被拉宽采样点较晚的节点比较容易误判。调试时如果物理上没法缩短线缆就把采样点往早调一两个TQ比硬扛着不校调好很多。5. 常见问题与排查技巧实录5.1 发送一直报ACK错误但逻辑分析仪明明有波形这是上了板之后最容易碰到的现象。FPGA端发送请求发了逻辑分析仪抓CAN_TX也看到一帧完整波形但IP核状态寄存器总在报告ACK错误重发3次之后直接进Bus-off。原因基本逃不出下面这几条总线上只有这一个节点在发数据没有第二个节点给出ACK应答位收发器电路没接好导致总线始终处于隐性状态终端电阻缺失严重干扰差分信号。CAN协议规定发送方必须监视总线上有没有其他节点拉低ACK时隙如果总线上光杆司令一个根本没有节点应答发送必然失败。最简单的验证方法就是模拟一个回环应答或者老老实实接一台CAN分析仪。5.2 波特率配置明明一样为什么两个节点还是对不上话这里有个认知误区波特率一样不等于位时序一样。CAN节点的位时间取决于预分频系数、时间段TQ分配、采样点设置这三者的组合不同厂商的CAN控制器默认值往往不同。哪怕两个节点都标称500 kbps一个采样点70%另一个采样点90%碰上干扰或线缆较长时就是会出错。排查时把两边寄存器配置表打印出来逐项对比重点看采样点。没有CAN协议分析仪的情况下也可以用回环模式自测看自己和自己通信是否稳定。自己和自己都调不稳那就是你配置逻辑本身有问题自己没问题但跨设备就出错再往采样点差异那边找。5.3 IP核中断一直触发但FIFO里读不出完整帧这种情况往往是接收过滤配置太严格把帧过滤掉了但错误中断或者仲裁丢失中断又开着于是中断标志满天飞。我排查时会把所有中断使能先关掉只留接收FIFO非空中断然后严格按照“读状态寄存器→读帧信息→读数据→释放缓冲区”的顺序操作。中途任何一步乱序比如读完数据忘了写释放命令FIFO就会一直呈现半满状态新帧进不来中断却一直报。还要检查一个问题接收中断状态位是写1清0还是读1清0不同实现差很多。这个都可以从IP核寄存器手册里找到源码里我在状态回读之后会立刻做一次写1清中断操作所以这一步被写死在模块里你直接例化即可不用关心细节。5.4 综合之后时序违例和跨时钟域问题CAN IP核的时钟域如果和你的数据通路时钟不一致跨时钟域处理必须做好。我在源码里对IP核寄存器读写接口做了多周期路径约束并在模块内部对异步信号做了两级同步处理。如果你的设计里有多个时钟域不要图省事直接把另一时钟域的脉冲信号接到tx_req上轻则偶发丢请求重则进入亚稳态把状态机带飞。实际上我在做某跨平台系统项目时就遇到过由于接收FIFO读侧时钟和写侧时钟频率差异导致偶发读空的情况后来加了一个异步FIFO做缓冲把两侧时钟彻底隔离问题才根治。这次这套源码上的接收二级FIFO也是当时实践下来的改进结果。5.5 常见问题速查表现象高频原因排查顺序发送报ACK错误总线只有单节点无ACK应答先接CAN分析仪或第二节点再测能发不能收验收过滤配置错误或RX引脚接错关过滤寄存器核对引脚映射帧内容和发送不一致数据字节序没有转换收发都统一按总线字节序转换偶发错误帧或Bus-off采样点配置偏差大、线缆过长查看两边采样点设置用示波器验证无法进正常模式复位未释放/配置寄存器未写成功检查回读确认信号延长复位时间中断风暴但无数据中断清标志方式错误按手册确认清中断机制关闭无关中断6. 写在最后的一点实践心得这套源码后来在我手里反复用过好几轮每次新项目要接CAN基本都是把这套顶层照着例化一份改改波特率配置和过滤规则就完事。我的经验是用IP核做CAN通信真正花时间的不是代码本身而是把配置参数和模块边界想清楚。你看这套源码里配置状态机、发送、接收各自独立后续想加CAN OPEN或者J1939这类上层协议直接在收发接口上套一层状态机就行不用回改底层。最后再透个底如果你手里的工程对资源占用极其敏感可以把IP核实例化时关掉扩展帧支持只保留标准帧逻辑单元能省下一截处理速度和时序余量都会好看一些。这算是我踩了不少坑之后总结出来的一个偏方分享给需要的朋友。