
一直有个错觉很多人觉得FPGA上做CAN总线通信核心难度在“把CAN协议跑起来”。真做过的都知道协议本身反而简单难的是一开始就选错路子用单片机外设的思维方式去套FPGA然后在Vivado里翻了半天IP目录找不到一个能直接用的CAN控制器整个人都懵了。这篇我直接把用Vivado从零搭CAN总线通信系统的完整过程写出来包括工程怎么建、控制器怎么选、位时序参数怎么算、上板调试踩过哪些坑、最后怎么固化到Flash你想知道的都在里面。适合那种准备在FPGA上扩展一路或多路CAN接口、做车载网关或工业控制的工程师参考也适合刚接触Vivado但想直接上手一个真实通信项目的同学。1. 先说结论用FPGA做CAN通信核心不是收发而是时序和状态机1.1 MCU外设思维移植到FPGA会踩什么坑我在一个车载网关项目里第一次在FPGA上跑CAN当时同事拿着STM32的思维跟我说“这不就是配几个寄存器吗”。这句话害人不浅。MCU的CAN外设把协议栈固化在硅片里你只要配置波特率、滤波器、中断剩下的事情硬件都替你办了。但FPGA是一张白纸Xilinx的IP目录里并没有一个开箱即用的“CAN控制器”你要么自己做状态机要么去OpenCores上找兼容SJA1000的第三方IP要么用Zynq的时候把PS端的CAN外设拉出来用。这个“没有现成IP”的事实是很多Vivado新手第一次做CAN项目时进度卡住的最大原因。你打开IP Catalog搜“CAN”搜出来的东西跟CAN没半点关系那一刻确实会怀疑人生。所以开篇我先把结论摆出来如果只是想在FPGA板上挂一个CAN收发器跑通信正经路线是自己写一个精简的CAN 2.0B控制器状态机或者移植开源核然后把收发器芯片挂在FPGA的某个IO上。1.2 时钟方案与收发器电路TJA1050那类芯片怎么接CAN总线物理层是差分信号FPGA的IO是单端电平所以中间必须有一层CAN收发器常用的是TJA1050、TJA1042、MCP2551这些。它们的接法很简单TXD接到FPGA的发送管脚RXD接到FPGA的接收管脚CANH和CANL接总线终端电阻120Ω根据节点位置决定要不要加。这个环节就有个特别容易被忽略的问题——电平域。很多FPGA开发板是3.3V IO而TJA1050的供电是5VTXD/RXD的IO电平跟着VCC走直接接3.3V的FPGA管脚长期运行有风险。我现在的做法是直接用支持3.3V供电的TJA1042T或兼容电平的收发器或者中间加电平转换。这块要在画板子之前想清楚等板子焊好了再发现电平不匹配调试周期直接多两周。时钟方案上CAN控制器需要一个比CAN波特率高得多的时钟通常用FPGA的全局时钟引脚进来比如50MHz或40MHz然后在内部做分频。分频逻辑和位时序直接相关这是后面第三节的重点这里先记住一条系统时钟频率必须是波特率整数倍关系较容易处理但更关键的是要能通过预分频器组合出足够精度。2. Vivado工程搭建版本选择、License、新建RTL工程与管脚约束2.1 版本与License的坑Vivado版本选择是个老生常谈但永远有人栽跟头的问题。新版本功能多但对老破解工具和第三方IP兼容性差旧版本稳定但对新器件支持不够。做CAN这种偏通信控制类的项目我建议选2020.1到2022.2之间的版本。2022.2算是近一年社区反馈比较稳的用Artix-7或Zynq-7000系列器件都没毛病。再往上的版本不是不好而是如果你要配合老一点的第三方CAN控制器IP后仿真和综合脚本偶尔会冒些莫名其妙的警告。License方面如果是做普通的7系列芯片完全可以用Vivado的免费WebPACK版本不需要企业License。很多人装完Vivado发现License Manager打不开或者识别不了license文件这种问题绝大多数是环境变量或杀毒软件拦截导致的。我试过最干脆的解决方式以管理员身份运行Vivado License Manager把license文件路径重新指定一次然后重启Vivado基本能解决。2.2 顶层模块划分与管脚约束工程结构上我习惯把顶层拆成三个模块CAN控制器逻辑、时钟分频、上层应用接口比如寄存器或FIFO。这样在Vivado里做约束和仿真都清晰。顶层代码大致长这样module can_top( input wire clk_50m, input wire rst_n, input wire can_rx, output wire can_tx, // 上层接口用于和MCU或逻辑交互 input wire [7:0] tx_data, input wire tx_wr, output wire [7:0] rx_data, output wire rx_valid ); wire clk_can; clk_div u_clk_div( .clk_in (clk_50m), .rst_n (rst_n), .clk_out(clk_can) ); can_controller u_can_ctrl( .clk (clk_can), .rst_n (rst_n), .rx (can_rx), .tx (can_tx), .tx_data(tx_data), .tx_wr (tx_wr), .rx_data(rx_data), .rx_valid(rx_valid) ); endmodule管脚约束XDC里最基础的三样时钟引脚、复位引脚、CAN收发器引脚。别小看这个好多人在“连接硬件的情况下生成固化文件”时卡住根源就是管脚约束里没把CAN的TXD/RXD分配到实际板卡的对外连接器上。确认管脚映射的方法很简单去板卡原理图里找到CAN收发器芯片连接到FPGA的哪个Bank哪个引脚然后写进XDCset_property PACKAGE_PIN P14 [get_ports {can_tx}] set_property IOSTANDARD LVCMOS33 [get_ports {can_tx}] set_property PACKAGE_PIN P15 [get_ports {can_rx}] set_property IOSTANDARD LVCMOS33 [get_ports {can_rx}]有个经验把TXD和RXD放在同一Bank并且确认该Bank的VCCO供电和收发器电平一致不然后期跑高速率时偶尔出现错误帧排查半天可能只是电平不干净。2.3 FPGA板卡上没有CAN收发器怎么办很多入门级FPGA开发板不带CAN收发器这时你有两个选择买一个外置的CAN收发器模块淘宝上十几块钱那种用杜邦线从FPGA引脚引出去或者直接在自定义底板上加一颗TJA1050。外置模块接法简单但要特别注意共地。CAN总线是隔离差分场景吗不一定实验环境里两端共地是前提杜邦线一定要把GND连上不然总线上的共模电压飘着收发器工作状态非常不稳定。我之前有一次调了两天最后发现是模块电源没和FPGA共地属于典型的低级错误但低级错误恰恰最容易在项目赶工时发生。3. CAN控制器逻辑实现位定时、帧格式、错误帧与采样点3.1 位时序参数换算是绕不过去的坎CAN协议里一个位时间分成四段同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段1PHASE_SEG1、相位缓冲段2PHASE_SEG2。每个段占多少个TQTime Quantum时间量子决定了最终采样点位置和波特率容错范围。我以一个实际参数为例。系统时钟40MHz目标是500kbps。CAN总线一个位时间是2μs我希望一个位划分成16个TQ这样采样点可以设在75%左右。每个TQ是125ns换算成预分频系数就是40MHz×125ns5也就是BRP4因为预分频器分频系数通常是BRP1。有了16TQ之后段分配是经典的1-4-7-4SYNC_SEG 1 TQPROP_SEG 4 TQPHASE_SEG1 7 TQPHASE_SEG2 4 TQ采样点 (1 4 7) / 16 75%这个采样点是业内比较常用的配置兼顾总线长度和晶振误差容忍度。如果总线比较短、节点距离近可以把采样点往后调到80%以上抗干扰能力更强。代码实现的时候本质上就是用计数器把这些TQ数出来在对应时刻采样RXD引脚。发送侧反过来在对应时刻把TXD引脚拉高低。下面是我在控制器里用的一段位时序状态机骨架关键是把采样点配出来reg [4:0] tq_cnt; reg [2:0] seg_state; localparam SYNC_END 1; localparam PROP_END SYNC_END 4; localparam PHASE1_END PROP_END 7; localparam PHASE2_END PHASE1_END 4; // 16 always (posedge clk_can or negedge rst_n) begin if (!rst_n) begin tq_cnt 0; seg_state 0; end else begin if (tq_cnt PHASE2_END - 1) begin tq_cnt 0; seg_state 0; // 下一个位 end else begin tq_cnt tq_cnt 1; end end end这种写法直观把每个TQ的计数器跑起来然后在tq_cnt 8也就是第9个TQ采样点75%时采样RXDwire sample_point (tq_cnt PHASE1_END - 1); reg can_rx_sync; always (posedge clk_can or negedge rst_n) begin if (!rst_n) can_rx_sync 1b1; else if (sample_point) can_rx_sync can_rx; end这个细节就是整个CAN接收链路的第一步在采样点把差分总线电平读进来后面做显性/隐性电平判断、位流解码、填充位移除、CRC校验才有意义。如果采样点位置不对后面的逻辑再正确也白搭。3.2 接收状态机与过滤匹配标准帧和扩展帧都绕不开CAN控制器的接收状态机无非就是在等帧起始、收仲裁场、收控制场、收数据场、收CRC、收ACK、收EOF这几步之间跳转。看起来简单实际编码时最容易错的是填充位处理。CAN协议规定连续5个相同电平之后必须插入一个反相位填充位接收时要把这个填充位移除不然数据场和CRC会对不上。我当时写接收状态机时的血泪教训填充位计数器必须在SOF之后就开始不能在数据场才开始。如果只在数据场处理填充遇到ID段里有连续5个相同位帧就废了。这个坑在仿真里反而不容易暴露因为仿真数据经常是理想位流真实总线上什么情况都有。过滤匹配这块如果不需要复杂滤波可以在接收仲裁场后直接判断ID是否命中某个预设值命中就把数据收下来不命中就继续把当前帧接收完但不写入FIFO。CAN总线上节点很多的话这个过滤能大幅降低上层处理负担。我做过一个8路CAN网关每路ID过滤表是32条规则用CAM结构实现在Vivado里综合后资源占用可以接受。如果规则少直接case语句最省事。3.3 错误帧的产生机制与调试手段热搜词里有“can总线中的错误帧”说明很多人调试CAN通信时被错误帧折磨过。错误帧是节点在检测到总线错误时主动发出一条错误标志叠加在总线上让所有节点都意识到这帧坏了。反应到你的逻辑里就是接收状态机在某个时刻收到了一堆显性电平然后总线电平一直异常。我用ILA抓错误帧占空比的方法把错误计数器拉出来配合BusOff状态位一旦计数器超过阈值就强制节点进入BusOff并在软件里打印状态。这个方法比直接看CAN分析仪的数据更细腻能定位到具体是哪一类错误位错误、填充错误、CRC错误导致的帧失败。4. 数据通路设计FIFO缓冲、中断通知还是DMA搬运4.1 中断接收和DMA接收FPGA里怎么选热搜词里“can总线一般中断接收还是dma接收”是个高频问题。这个问题的答案取决于你用什么处理器配合。纯FPGA方案里根本没有“中断”“DMA”这两个概念只有“用FIFO缓冲”和“用AXI DMA把数据搬进DDR”的差别。如果是Zynq方案PS端集成CAN控制器的话DMA和中断都由Linux/裸机驱动管理但那用的是PS自带外设不是PL逻辑和“Vivado构建CAN总线通信系统”的关系就不大了。在PL里自己实现的控制器我习惯是接收侧用FIFO 一个rx_valid脉冲通知上层应用。上层可以是PS端的AXI-Lite寄存器也可以是另一个逻辑模块。每个收到完整帧就把数据压入FIFO同时拉高rx_valid上层检测到rx_valid后从FIFO读取。这种方案的好处是实现简单、移植性好数据量不大时完全够用。// FIFO写入 if (rx_frame_done) begin fifo_wr_en 1b1; fifo_din {rx_id, rx_dlc, rx_data}; end如果数据吞吐要求很高比如多路CAN汇聚后要转发到以太网再考虑用AXI DMA直接把FIFO数据搬到DDR让PS端应用直接处理。但这样做的前提是你的CAN控制器必须带一个标准AXI4-Stream接口否则还得做协议转换工作量一下子上来。我一开始图省事把FIFO做成普通双口RAM后面接DMA时多写了一大段AXI逻辑不如第一次就把接口规范化。4.2 负载率怎么算、缓存深度留多少这是一个经常被拿出来聊的工程问题CAN总线的负载率到底按多少算教科书公式是“单位时间总线上传输的比特数 / 总线波特率”但要注意CAN一帧的实际比特数不是固定值。标准帧8字节数据不算填充位是111位左右算上最坏情况的填充位可能到126位以上。扩展帧8字节数据则可能到150位。实际计算时我按下面这个简化公式估算负载率负载率 (帧数/秒) × (平均帧位数) / 波特率举例波特率500kbps每秒发送1000帧标准帧8字节数据每帧按修正后的约115位含平均填充位计算负载率 1000 × 115 / 500000 23%。为了工程冗余我一般把设计余量留到70%以下超过70%的CAN总线在错误率上会肉眼可见地变差。缓存深度按最坏突发情况估算假设应用突发转发100帧标准帧8字节一帧最多126位折合约16字节含ID和DLC100帧就是1600字节所以FIFO至少做到2KB。很多第一次做的人只留了256B遇到突发就直接丢帧。做CAN网关的人对这个数字一定要有概念。5. 上板调试全记录ILA采样、Implement变红、比特流失败5.1 ILA采样频率限制与探针信号选择“vivado中ila的采样频率是不是有范围限制”这个问题得拆开回答。ILA集成逻辑分析仪本身没有固定的采样频率设置项它的采样频率由你连接ILA的那个时钟决定你给ILA接50MHz时钟采样率就是50MHz。真正的限制在于采样深度和探针宽度ILA内部用的是BRAM做存储探针太多、深度设置过大BRAM资源不够的时候Vivado要么布局布线失败要么报告超资源。实践中我一般把ILA的采样深度设在1024到4096之间探针数量控制在32位以下。关键在于不是所有信号都能抓到。ILA采样的信号必须是你的逻辑里真实存在的寄存器或wire而且它会改变布局布线结果所以加了ILA后时序结果可能和裸工程不一样。遇到过一个问题加了ILA后本来能跑通的收发逻辑反而出错去掉ILA就正常。这种情况大多是采样时钟和逻辑时钟约束不一致导致的检查一下时钟约束让ILA和被测逻辑使用同一个时钟域。5.2 Implement design变红典型的排查链路“vivado implement design变红”是绕不开的坑。我刚开始以为是指标红了后来发现是Vivado的Implementation流程跑不过去步骤前面显示红叉。这种情况分三类第一类是综合阶段就fail逻辑错误、端口对接错误去看Synthesis的Messages窗口一般会有具体报错行号。 第二类是实现阶段fail但综合通过常见的是布局布线冲突或DRC错误 第三类最磨人——实现阶段没报错但时序不收敛Vivado在Generate Bitstream时把Implementation标记为无法继续因为时序不满足。排查链路我固定是这么走的看Messages窗口的红色Error → 打开Schematic检查网表连接 → 如果没找到Open Implemented Design → Report Timing Summary → 看WNS(最差负裕量)有没有变负 → 定位violation path对应的寄存器/组合逻辑 → 优化代码或加约束很多人的“implement design变红”其实发生在Generate Bitstream阶段是因为Timing未收敛Vivado默认策略是Fail。有一个临时办法在Settings里把-intransit或-nocleanup相关选项改一改或者在bitstream生成设置里允许时序不满足也继续。生产环境不建议这么干但调试阶段用它能跑一把抓到信号很香。5.3 generate bitstream失败不都是代码问题热搜词“vivado生成比特流失败”下面能搜出来一堆帖子但十有八九不是代码逻辑问题而是工具链问题。我自己碰到过几次起因各异工程路径里有中文或空格Vivado的bitgen有时候抽风。复位引脚或时钟引脚没加约束导致Vivado无法确定时钟关系。配置模式设置成非易失模式但Flash型号没选对。资源占用超过100%比如BRAM用超了。其中第三种最隐蔽因为工程能综合能实现就是生成比特流时挂掉。我遇到过一个案例是用ILA探测信号时配置了太大深度BRAM直接爆掉Vivado报的却是“max fanout”或“routing resource”错误让人摸不着头脑。排查思路可以这么看先看资源利用率报告确认BRAM、LUT、FF没有超过90%。超过80%就已经很危险了布局布线会非常紧张时序很难收敛。生成比特流失败时先删掉多余的ILA探针往往能解决一大批问题。6. 从调试到量产怎么把程序固化进Flash6.1 生成固化文件bin还是mcs调试完成后程序要固化到板载Flash里这样上电才能自动加载。Vivado里生成固化文件有两种常见途径一种是在Hardware Manager界面里连接板子后直接“Add Configuration Memory Device”然后下载另一种是先离线生成镜像文件再统一烧写。开发阶段推荐前者产线阶段推荐后者。在线生成固化文件的操作路径是打开Hardware Manager → 右键FPGA设备 → Add Configuration Memory Device → 选择板载Flash型号如n25q128→ Program Configuration Memory Device → 选择bitstream文件 → 烧写如果想把多个bitstream合并成一个镜像或者要从MCS启动就要在Tools里用“Generate Memory Configuration File”生成bin或mcs。这里有个细节7系列FPGA固化一般用bin文件配合偏移地址0但要用SPI x4模式的话需要在设置里把-interface SPIx4加上。很多板子上明明焊了八脚的SPI Flash下载后却没反应最后发现是生成镜像时接口模式没选对。6.2 连接硬件的情况下生成固化文件有什么讲究热搜词“vivado如何在连接硬件的情况下生成固化文件”其实就是上一节讲的操作。在连接硬件的情况下Vivado会直接枚举板上的Flash型号你只需要把生成好的bin文件下载进去即可。这里的关键点有三个第一确认当前连线的是JTAG下载器且FPGA已经被识别。如果出现“vivado安装驱动无法识别板子”大概率是下载器驱动问题需要单独装对应下载器的驱动或者用Vivado Hardware Manager重新扫描。第二下载完成后必须掉电重启验证。很多人下载完mcs后在Hardware Manager里看到有数据以为固化成功了一掉电再上电发现FPGA还是空白那是因为Flash烧写没成功或者配置引脚被拉到了JTAG模式。7系列FPGA的配置模式由M[2:0]引脚电平决定板上必须设置成SPI/Quad-SPI启动模式。第三固化之后程序跑起来的时间点比JTAG下载早。如果你在Vivado的ILA里能看到前面调试时的波形固化之后就看不到了——除非你在逻辑里保留ILA核并通过JTAG重新连接。很多现场问题在“固化后CAN通信异常”时只能抓指示灯和CAN分析仪所以量产前一定要把关键状态寄存器暴露出来方便现场诊断。我自己在固化环节踩得最深的坑是把bin文件烧进Flash后CAN收发器一直发错误帧查了半天发现是Flash配置模式没改FPGA根本没从Flash启动而是在JTAG模式下随机加载了个空配置导致引脚电平不确定CAN收发器的TXD脚被一个未初始化的IO拽着低电平总线一直被显性占用。低级错误但真的很耽误事。所以固化后第一件事不是测通信而是看FPGA的DONE引脚有没有拉高配置有没有真的完成。6.3 一个关于稳定性验证的补充CAN系统固化完成后稳定性验证比功能验证更花时间。我会在500kbps波特率下跑24小时压力测试用CAN分析仪持续发送随机ID和数据帧观察错误帧计数和BusOff次数。如果环境温度升高后开始出错误帧优先怀疑收发器芯片的电源纹波其次是终端电阻匹配。很多FPGA板载的DC-DC纹波偏大直接给CAN收发器供电会导致误码率升高这时候在收发器电源脚加一个10μF电解电容加100nF陶瓷电容往往立竿见影。另外要提醒一句千万不能只在Vivado仿真里觉得“收发对了”就完事。CAN协议里采样点、重同步、填充位这些细节非常依赖真实硬件行为仿真环境里很难模拟出总线上的短线干扰和时钟漂移。上板把总线连上用两个节点对打才是检验控制器的唯一标准。