一直有个错觉,很多人觉得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的供电是5V,TXD/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哪个引脚,然后写进XDC:
set_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、相位缓冲段1(PHASE_SEG1)、相位缓冲段2(PHASE_SEG2)。每个段占多少个TQ(Time Quantum,时间量子),决定了最终采样点位置和波特率容错范围。
我以一个实际参数为例。系统时钟40MHz,目标是500kbps。CAN总线一个位时间是2μs,我希望一个位划分成16个TQ,这样采样点可以设在75%左右。每个TQ是125ns,换算成预分频系数就是40MHz×125ns=5,也就是BRP=4,因为预分频器分频系数通常是BRP+1。
有了16TQ之后,段分配是经典的1-4-7-4:
- SYNC_SEG = 1 TQ
- PROP_SEG = 4 TQ
- PHASE_SEG1 = 7 TQ
- PHASE_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%)时采样RXD:
wire 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 <= 1'b1; 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 <= 1'b1; 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和DLC),100帧就是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. 从调试到量产:怎么把程序固化进Flash
6.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协议里采样点、重同步、填充位这些细节非常依赖真实硬件行为,仿真环境里很难模拟出总线上的短线干扰和时钟漂移。上板把总线连上,用两个节点对打,才是检验控制器的唯一标准。