1. 项目概述:为什么我们需要一个SJA1000的Verilog实现?
如果你正在做汽车电子、工业控制或者任何需要节点间可靠通信的嵌入式项目,那么CAN总线(Controller Area Network)大概率是你绕不开的技术。而提到CAN控制器,飞利浦(现恩智浦NXP)的SJA1000绝对是一个经典到不能再经典的型号。它定义了早期独立CAN控制器的标准接口和寄存器模型,无数产品都基于它进行开发。然而,对于FPGA开发者来说,直接使用这颗物理芯片有时会面临采购、板级空间、设计灵活性等限制。这时,一个用Verilog硬件描述语言实现的、与SJA1000软件兼容的IP核,其价值就凸显出来了。
这个项目,简单说,就是用代码“复刻”一颗SJA1000芯片。它不是一个简单的总线接口,而是一个完整的、包含协议引擎、缓冲区管理和寄存器配置的控制器。你把它集成到你的FPGA设计中,通过一组类似内存访问的并行总线(通常是类似8051的接口)与你的处理器(无论是软核如NIOS II、MicroBlaze,还是硬核ARM)通信,你的软件驱动几乎可以原封不动地从标准SJA1000移植过来。这意味着你无需重写复杂的CAN协议栈,就能在FPGA内部获得一个高度集成的、可定制的CAN节点。
这适合谁呢?首先是正在使用或计划使用FPGA进行系统集成的硬件工程师和FPGA逻辑工程师,尤其是那些系统里已经有多个标准CAN设备,需要快速增加一个FPGA内部CAN节点的场景。其次,它也适合做ASIC前端设计的学习者,通过研读和修改这样一个中型规模的数字IP核,你能深入理解状态机设计、时钟域交叉、总线协议和通信协议栈的硬件实现。最后,对于教学和原型验证,一个可综合、可仿真的Verilog CAN控制器,比盯着数据手册和物理波形要直观得多。
2. 核心架构与模块深度拆解
一个与SJA1000兼容的CAN控制器,其内部结构远不止是一个串行转并行的收发器。它需要完整实现CAN 2.0A/B协议,并精准模拟SJA1000的寄存器行为和中断机制。通常,这样一个IP核会采用分层或模块化的设计思想。
2.1 顶层接口与模块划分
最顶层的模块(通常命名为can_top或can_wrapper)主要完成两件事:对外提供与主控制器(CPU)通信的宿主接口,以及对内集成各个功能子模块。宿主接口通常是类似SJA1000的Intel或Motorola总线时序,包括地址线、数据线、读/写选通、片选和中断输出。这个接口的设计至关重要,它直接决定了你的CPU能否像访问外部存储器一样访问这个CAN控制器。
内部模块的划分,则严格遵循CAN控制器的功能逻辑:
- 协议引擎核心:这是大脑,负责实现CAN帧的组装、拆分、位填充、CRC计算与校验、应答、错误处理和重发逻辑。它通常是一个复杂的状态机。
- 比特时序逻辑:这是心脏,根据配置的波特率参数,精确控制每一位的发送和采样时刻。它需要处理同步、相位缓冲段,以应对总线时钟漂移。
- 验收滤波模块:这是守门员,根据预设的ID验收码和掩码,决定哪些接收到的报文可以进入接收缓冲区,极大地减轻了主处理器的中断负担。
- 缓冲区管理:这是仓库,包括发送缓冲区和接收缓冲区(通常是FIFO)。SJA1000有特定的缓冲区结构,Verilog实现需要模拟其双缓冲或FIFO机制。
- 寄存器文件:这是控制面板,将所有的控制命令、状态标志、错误计数、验收滤波码等映射到特定的内存地址。CPU通过读写这些寄存器来操控整个控制器。
在开源的verilog-can项目中,我们可以看到清晰的模块对应关系:can_bsp.v很可能是比特时序处理,can_crc.v是CRC专用计算模块,can_registers.v是寄存器文件,而can_top.v则是顶层集成模块。这种分而治之的设计,不仅便于理解和调试,也方便后续的功能裁剪或增强。
2.2 协议兼容性的关键:寄存器模型
为什么强调“SJA1000兼容”?核心就在于寄存器模型。SJA1000的寄存器地址、位定义、功能描述是公开的标准。我们的Verilog实现必须“复刻”这个内存映射视图。
这包括几个关键部分:
- 控制寄存器:模式寄存器、命令寄存器。例如,写入命令寄存器的特定位可以触发发送请求、释放接收缓冲区或进入睡眠模式。
- 状态寄存器:总线状态、错误状态、中断标志。CPU通过轮询或中断来获取控制器和总线的实时状态。
- 验收代码/掩码寄存器:用于配置滤波器的关键寄存器,通常有多个字节。
- 发送/接收缓冲区:在SJA1000中,它们共享一段地址空间,通过命令区分。Verilog实现需要模拟这种访问方式,即向特定地址写入数据,硬件逻辑会自动将其装入发送缓冲区;从特定地址读取,则是从接收缓冲区取出数据。
- 总线定时寄存器:配置波特率分频、采样点位置等。这里的计算需要非常小心,一个参数错误就可能导致通信失败。
注意:在实现寄存器模块时,必须特别注意异步复位和时钟域的问题。CPU接口的时钟和CAN核心模块的时钟可能是同源但不同频率,甚至是异步的。通常,寄存器模块内部会使用
can_register_syn.v和can_register_asyn.v这样的子模块来处理同步寄存器与异步寄存器,确保信号跨时钟域传递的稳定性和无亚稳态。这是此类设计中最容易出错的地方之一。
3. 核心模块实现细节与实操要点
理解了整体架构,我们深入到几个最关键的子模块,看看用Verilog实现时有哪些“坑”和技巧。
3.1 比特时序逻辑的实现
比特时序逻辑是CAN通信稳定的基石。它的任务是根据配置的BRP、TSEG1、TSEG2等参数,将一个位时间划分为同步段、传播段、相位缓冲段1和2,并生成精确的采样点时钟。
一个典型的实现思路是使用一个位计数器和一个段计数器。位计数器以系统时钟(由BRP分频得到)运行,其计数值对应位时间内的“最小时间份额”。段计数器则根据位计数器的值,判断当前处于哪个时间段。
// 简化的比特时序状态机片段 localparam SYNC_SEG = 1; // 同步段固定为1个时间份额 reg [7:0] bit_timer; // 位时间计数器 reg [1:0] segment; // 当前段:0=同步段,1=传播段,2=相位缓冲段1,3=相位缓冲段2 always @(posedge clk or posedge rst) begin if (rst) begin bit_timer <= 8'd0; segment <= 2'd0; sample_point <= 1'b0; end else if (enable) begin if (bit_timer == (SYNC_SEG + TSEG1 + TSEG2 - 1)) begin bit_timer <= 8'd0; // 一个位时间结束 segment <= 2'd0; sample_point <= 1'b0; end else begin bit_timer <= bit_timer + 1'b1; // 判断并切换段 if (bit_timer == SYNC_SEG - 1) segment <= 2'd1; else if (bit_timer == SYNC_SEG + TSEG1 - 1) segment <= 2'd2; else if (bit_timer == SYNC_SEG + TSEG1 + TSEG2 - 2) sample_point <= 1'b1; // 在相位缓冲段2结束前采样 end end end实操心得:采样点的位置(通常在相位缓冲段2结束前)对抗总线节点间时钟累积误差至关重要。在调试时,如果发现偶尔的CRC错误或应答错误,首先应该用逻辑分析仪抓取CAN_TX和CAN_RX信号,对比采样点位置和实际信号跳变沿的关系。TSEG1和TSEG2的配置需要根据总线长度和节点数进行微调,并非波特率算对就万事大吉。
3.2 CRC计算模块的硬件优化
CAN协议使用15位CRC,生成多项式为 ( x^{15} + x^{14} + x^{10} + x^{8} + x^{7} + x^{4} + x^{3} + 1 )。在硬件中实现CRC计算,通常采用线性反馈移位寄存器。
一个直接但低效的方法是每个位时钟周期,根据输入位和当前寄存器值,计算下一位并移位。但我们可以利用Verilog的位操作特性,写出更简洁高效的并行计算式(虽然CAN是串行逐位计算,但此逻辑仍适用)。
module can_crc ( input clk, input rst, input bit_in, // 串行输入的数据位 input bit_enable, // 有效位时钟 input clear, // 开始新帧时清零CRC寄存器 output reg [14:0] crc_out ); wire [14:0] crc_next; // 根据生成多项式计算的组合逻辑 assign crc_next[0] = crc_out[14] ^ bit_in; assign crc_next[1] = crc_out[0]; assign crc_next[2] = crc_out[1]; assign crc_next[3] = crc_out[2] ^ crc_out[14] ^ bit_in; assign crc_next[4] = crc_out[3] ^ crc_out[14] ^ bit_in; // ... 中间位省略,根据多项式推导 assign crc_next[14] = crc_out[13] ^ crc_out[14] ^ bit_in; always @(posedge clk or posedge rst) begin if (rst) begin crc_out <= 15'b0; end else if (clear) begin crc_out <= 15'b0; end else if (bit_enable) begin crc_out <= crc_next; end end endmodule注意事项:CRC计算的范围是从帧起始到数据场结束,不包括填充位。这意味着你的CRC模块需要接收一个“计算使能”信号,该信号在填充位插入期间需要被暂时关闭。这是一个非常容易忽略的细节,如果处理不当,会导致本节点计算的CRC与总线其他节点不一致,从而引发错误帧。
3.3 验收滤波器的高效设计
验收滤波器是减轻CPU负载的关键。SJA1000支持单滤波器和双滤波器模式,Verilog实现需要能够灵活配置。其核心是一个比较器阵列。
假设我们实现一个标准的32位验收码+32位验收掩码的滤波器。掩码位为1表示对应的验收码位必须与接收到的标识符位严格匹配;掩码位为0则表示该位“无关”,不参与比较。
// 简化的验收滤波判断逻辑 reg [31:0] acceptance_code; // 从寄存器加载 reg [31:0] acceptance_mask; // 从寄存器加载 reg [31:0] received_id_ext; // 接收到的扩展标识符(标准帧则高位补0) wire filter_pass; assign filter_pass = ((received_id_ext ^ acceptance_code) & acceptance_mask) == 32'b0;设计技巧:为了支持标准帧(11位ID)和扩展帧(29位ID),通常的做法是将接收到的ID统一扩展到32位进行比较,验收码和掩码的高位用于配置帧类型(标准/扩展)等附加过滤条件。在can_acf.v模块中,可能会看到更复杂的逻辑,包括多个滤波器并行工作,以及根据模式寄存器选择不同的滤波策略。这里要特别注意滤波器的使能时机,它应该在帧的仲裁场结束后立即做出判断,以决定是否继续接收该帧的后续部分,从而节省功耗和缓冲区资源。
4. 集成、仿真与测试平台搭建
有了各个子模块,下一步就是将它们集成到顶层,并搭建一个可靠的测试环境。这是将代码转化为可用IP的关键一步。
4.1 顶层模块集成与时钟域处理
在can_top.v中,你需要实例化所有子模块,并连接它们之间的信号线。最关键的是处理好三个主要的时钟域:
- 宿主接口时钟域:CPU访问寄存器的时钟。
- CAN核心时钟域:由外部晶振或PLL产生,用于驱动协议引擎和比特时序逻辑。
- CAN总线时钟域:实际上就是接收到的串行数据流,它是异步的。
时钟域交叉处理主要集中在:
- 寄存器配置传递:从宿主接口时钟域同步到CAN核心时钟域。使用两级或多级同步器。
- 状态与中断标志传递:从CAN核心时钟域同步到宿主接口时钟域。
- 接收数据同步:从CAN_RX引脚(异步)同步到CAN核心时钟域。
一个常见的做法是,为所有跨时钟域的寄存器读写信号设计握手协议或使用异步FIFO。对于控制寄存器,通常采用“CPU写入 -> 同步到核心域 -> 生效”的流程;对于状态寄存器,采用“核心域更新 -> 同步到CPU域 -> CPU读取”的流程。
4.2 使用Icarus Verilog与GTKWave进行仿真
对于开源项目,Icarus Verilog是一个轻量级且强大的仿真工具。假设你的项目目录结构清晰,一个简单的Makefile可以自动化编译和仿真流程。
# Makefile 示例 TARGET = can_top_tb SRC = can_top.v can_registers.v can_bsp.v can_crc.v can_acf.v can_fifo.v TB_SRC = can_top_tb.v all: compile run view compile: iverilog -o $(TARGET).vvp $(SRC) $(TB_SRC) run: vvp $(TARGET).vvp view: gtkwave $(TARGET).vcd &你的测试平台can_top_tb.v需要完成以下几件事:
- 生成时钟和复位信号。
- 实例化待测设计。
- 模拟CPU的读写行为:编写任务来模拟总线时序,初始化控制器(设置波特率、验收滤波、工作模式),然后写入一帧发送数据,触发发送。
- 模拟CAN总线环境:最好能实例化另一个CAN控制器模型或一个简单的总线行为模型,来对被测设计进行回环测试或注入特定的报文。
- 收集并检查结果:自动检查发送的帧是否被正确接收,CRC是否正确,中断是否按预期触发。
仿真要点:在测试平台中,不仅要测试正常流程,更要刻意构造错误场景:如总线错误、格式错误、ACK缺失、连续发送导致溢出等。观察控制器的错误计数器是否递增,是否进入错误被动或总线关闭状态,以及能否根据协议恢复。
4.3 关键测试用例与断言
一个严谨的测试平台应该包含自检机制。使用Verilog的$display或$error在仿真中输出检查结果,甚至可以使用SystemVerilog断言。
// 一个简单的发送-接收回环测试检查点 initial begin // ... 初始化与配置 cpu_write(ADDR_COMMAND, CMD_TRANSMIT); // 触发发送 repeat(1000) @(posedge clk); // 等待一段时间 if (interrupt != 1'b1) begin $error("发送完成中断未触发!"); end else begin cpu_read(ADDR_STATUS, status); if (status[4] != 1'b1) begin // 检查发送完成标志位 $error("状态寄存器发送完成标志未置位!"); end end // 检查接收缓冲区 cpu_write(ADDR_COMMAND, CMD_RELEASE_RX_BUFFER); // ... 读取接收缓冲区数据并与发送数据对比 if (received_data !== sent_data) begin $error("接收数据与发送数据不匹配!"); end else begin $display("测试用例PASS: 发送-接收回环成功。"); end end实操心得:仿真时,建议将CAN总线的位时间设置得比实际快很多(例如用10MHz时钟模拟125Kbps波特率),以加快仿真速度。但要注意,过快的仿真速度可能会掩盖一些时序边界问题。在功能仿真通过后,应该用接近实际的时钟频率再跑一遍关键用例。另外,务必关注仿真中是否有“X”(不定态)传播,这通常是未初始化的寄存器或冲突赋值导致的,在硬件中可能导致不可预测的行为。
5. 综合、上板调试与常见问题排查
仿真通过后,就可以进行逻辑综合,并在真实的FPGA开发板上进行调试了。这是从“理论正确”到“实际可用”的惊险一跃。
5.1 综合约束与注意事项
将Verilog代码放入Quartus、Vivado等工具进行综合前,必须编写正确的约束文件。
- 时钟约束:为宿主接口时钟和CAN核心时钟创建时钟约束,指明频率和不确定性。
- 引脚约束:将顶层模块的
CAN_TX、CAN_RX、INT(中断)等信号分配到FPGA的具体物理引脚上。特别注意:CAN_TX和CAN_RX通常是3.3V LVCMOS电平,而标准的CAN总线需要CAN收发器芯片(如TJA1050)将逻辑电平转换为差分信号。FPGA引脚应连接到收发器的TXD和RXD。 - 时序例外:跨时钟域的信号路径,通常需要设置
set_false_path或set_clock_groups约束,告诉综合工具不要对这些路径进行时序分析,因为其正确性由同步器保证。
一个常见的疏忽是忘记约束异步复位信号的输入引脚。如果复位信号来自按键或外部电路,其毛刺可能导致系统异常。建议在FPGA内部使用同步复位,或者对外部复位信号进行消抖和同步化处理。
5.2 上板调试实战步骤
- 硬件连接检查:确保FPGA与CAN收发器之间的连线正确,收发器的VCC和地稳定,终端电阻(通常为120欧姆)在总线两端已连接。
- 静态测试:先不连接外部CAN网络。下载配置后,用逻辑分析仪或示波器测量
CAN_TX引脚。通过CPU接口让控制器发送一帧数据。你应该能看到符合设定波特率的规整的NRZ位流,包含帧起始、仲裁场、控制场等。这是验证控制器基本功能的第一步。 - 回环模式测试:将控制器配置为自回环模式(如果支持)。在此模式下,发送的帧会被内部直接接收。通过CPU读取接收缓冲区,验证数据一致性。这可以排除物理层问题,专注验证逻辑功能。
- 接入真实网络:连接到一个有其他正常节点的CAN总线。首先,确保你的FPGA节点的波特率与其他节点完全一致。先不发送,只监听。查看是否能正确接收到总线上的其他报文,中断是否正常触发。这一步验证了接收路径和滤波功能。
- 主动发送测试:在监听正常后,尝试发送一帧数据。用逻辑分析仪同时抓取FPGA的
CAN_TX和收发器后的CANH、CANL差分信号。观察发送是否成功,是否收到其他节点的ACK位,总线是否出现错误帧。
5.3 常见问题与排查速查表
以下表格总结了上板调试时最常见的问题及其排查思路:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 发送时无波形输出 | 1. 控制器未正确初始化(模式/波特率) 2. 发送缓冲区未写入有效数据 3. 总线关闭状态 | 1. 检查寄存器配置流程,确认写入了正确的波特率参数和控制字。 2. 单步调试CPU代码,确认数据已写入发送缓冲区地址,并发送了传输请求命令。 3. 读取错误状态寄存器,看是否因错误计数过多进入总线关闭。 |
| 能发送,但收不到ACK或总报错 | 1. 波特率不匹配(最常见) 2. 物理连接问题(终端电阻) 3. 采样点设置不佳 | 1.重中之重:用示波器精确测量发送位宽度,计算实际波特率,与目标值对比。检查波特率寄存器计算是否正确。 2. 检查总线两端是否有120欧姆终端电阻,线缆是否连接可靠。 3. 调整总线定时寄存器的TSEG1、TSEG2,改变采样点位置。 |
| 能接收,但滤波器不生效 | 1. 验收滤波模式配置错误 2. 验收码/掩码寄存器写入顺序或值错误 3. 滤波器未使能 | 1. 确认模式寄存器中选择了正确的滤波模式(单/双)。 2. 仔细核对数据手册,验收码和掩码寄存器通常是多字节,需按顺序写入。确认写入的值是否符合预期(注意字节序)。 3. 检查命令寄存器或模式寄存器中是否有独立的滤波器使能位。 |
| 中断不触发 | 1. 中断使能寄存器未配置 2. 中断标志被清除 3. CPU未正确读取中断寄存器 | 1. 确认中断使能寄存器已打开对应中断源(发送完成、接收、错误等)。 2. 在中断服务程序中,需要先读取中断寄存器以清除标志位,否则中断会持续有效。 3. 检查CPU的中断引脚连接和中断服务程序是否被正确触发。 |
| 通信不稳定,偶发错误 | 1. 时钟质量差(抖动大) 2. 跨时钟域同步问题 3. 电源噪声 | 1. 测量供给FPGA和CAN收发器的时钟信号质量。 2. 回顾代码中的CDC设计,是否所有跨时钟域信号都经过了至少两级同步器?仿真时加入时钟抖动模型测试。 3. 检查电源纹波,在FPGA和收发器电源引脚附近增加去耦电容。 |
踩坑记录:我曾遇到一个棘手问题,控制器在长时间运行后随机出现“鬼帧”(接收到未发送的乱码)。最终定位到是接收FIFO的读指针在跨时钟域同步时发生了亚稳态,导致指针跳变,数据错乱。解决方案是将格雷码用于FIFO指针的跨时钟域传递,彻底解决了问题。这提醒我们,对于任何异步FIFO或计数器指针,格雷码转换是必须考虑的安全措施。
6. 进阶优化与扩展思路
一个基础可用的SJA1000兼容IP核只是起点。在实际项目中,我们往往需要根据具体需求进行优化和扩展。
6.1 性能优化:提升吞吐量与降低延迟
- 深接收FIFO:标准SJA1000的接收缓冲区深度有限。在Verilog实现中,可以轻易地将接收FIFO加深到16、32甚至更多帧。这在高波特率或突发报文场景下,能有效降低因CPU来不及处理而丢帧的风险。
- 发送缓冲队列:实现一个多帧的发送队列。CPU可以连续写入多帧数据,由硬件控制器自动按序发送,进一步解放CPU。
- DMA集成:对于高性能处理器(如FPGA内的ARM硬核),可以设计一个DMA控制器,将接收FIFO的数据直接搬移到系统内存,或将内存中的待发送帧列表搬移到发送缓冲区。这能极大减少CPU中断开销,提升整体系统效率。
6.2 功能扩展:超越标准SJA1000
- 时间戳:为每一条接收到的帧打上精确的时间戳(基于本地高精度时钟),这对于网络分析、故障诊断和同步应用非常有用。
- 监听/嗅探模式:实现一种模式,可以接收总线上的所有报文(包括错误帧),而不管验收滤波器设置。这是CAN网络分析仪的核心功能。
- CAN FD支持:虽然SJA1000是经典CAN,但CAN FD(灵活数据速率)已成为新趋势。可以在原有架构基础上,扩展比特时序逻辑和协议引擎,支持可变速率和数据场长度。这是一项重大的但极具价值的升级。
6.3 代码质量与可维护性
参考的verilog-can项目提到了“提高代码质量”。对于我们的实现,这意味着:
- 清晰的代码风格:使用一致的命名规范(如寄存器信号加
_reg后缀,组合逻辑输出加_nxt),添加充分的注释,特别是状态机的状态定义和复杂组合逻辑。 - 参数化设计:使用
parameter或 ``define` 来定义FIFO深度、滤波器数量、地址位宽等,使得IP核易于在不同项目中复用和配置。 - 完善的文档:除了代码注释,应编写独立的用户手册,详细说明寄存器映射、接口时序、配置步骤和示例代码。这对于团队协作和项目传承至关重要。
从一颗经典的物理芯片到一个灵活、可定制的Verilog IP核,这个过程不仅是对协议和硬件的深刻理解,更是数字系统设计能力的综合体现。当你看到自己编写的代码在FPGA中运行,并通过CAN总线与真实的汽车ECU或工业设备稳定通信时,那种成就感是无可替代的。这个项目最大的价值或许不在于复现了一个过时的控制器,而在于为你打开了一扇门,一扇通往车载网络、工业通信乃至更复杂SoC设计的大门。