CAN协议控制器的Verilog实现:从协议解析到Vivado工程实践

在现代汽车电子和工业控制系统中,实时、可靠的通信是系统稳定运行的生命线。尽管软件协议栈已能完成CAN通信的基本功能,但在高负载或强干扰环境下,CPU频繁中断处理往往成为性能瓶颈。有没有一种方式能让CAN通信“自己跑起来”,不依赖主控干预?答案就是——将CAN控制器硬件化,用FPGA直接实现完整的协议逻辑。

这不仅是对协议理解的深度考验,更是一次从理论到硅片的完整跨越。本文将带你走完这条路径:从CAN帧如何被一位位发送,到状态机如何精准控制时序,再到最终在Vivado中综合出可部署的比特流。我们不只写代码,更要让代码真正“活”在硬件里。


协议的本质:CAN是如何说话的?

CAN不是简单的串口升级版,它的设计哲学体现在每一个细节中。比如总线上没有“主从”之分,任何节点都可以随时发言——但这并不意味着混乱。相反,它通过 非破坏性仲裁 机制实现了优雅的竞争控制。

想象两个节点同时发消息:一个报胎压异常(ID=0x100),另一个报告空调状态(ID=0x200)。它们都从第一位开始逐位比对。由于 0 < 1 ,当第一个bit为显性(0)而另一个为隐性(1)时,后者立刻检测到总线电平与自己发送不符,便自动退出发送,让高优先级消息无损通过。整个过程无需重传,延迟完全确定。

这种机制的背后,是对 广播+过滤 模型的极致运用。所有节点都在听,但只关心自己需要的消息。而为了确保听到的是“真话”,CAN内置了五重错误检测机制:位错误、填充错误、CRC校验、应答错误、格式错误。任何一个环节出问题,都会触发错误帧,提醒全网注意。

更重要的是,CAN的每一位都不是固定长度的“时间片”,而是由 时间量子(tq) 构建的动态结构。典型的位时间划分为16个tq,分布在四个段:

  • Sync_Seg (1 tq):同步所有节点;
  • Prop_Seg (如3 tq):补偿物理延迟;
  • Phase_Seg1 (如7 tq):采样前缓冲;
  • Phase_Seg2 (如5 tq):采样后恢复。

采样点通常设在第8~13 tq之间(即75%~81.25%位置),以避开边沿抖动。这种划分不仅支持精确采样,还允许通过 重同步 机制动态调整相位,适应晶振偏差和温度漂移。

举个例子:如果你的FPGA主频是50 MHz,要生成500 kbps波特率,那每个位需占10 μs,也就是16 tq → 每个tq = 625 ns。这意味着你需要一个8 MHz的位定时时钟(125 ns周期),可以通过MMCM分频得到。这个数字不能凑合,否则累积误差会导致采样偏移,最终丢帧。


硬件实现的核心挑战:如何让Verilog“读懂”CAN?

位定时引擎:时间的节拍器

FPGA没有内置CAN控制器,我们必须手动构建“心跳”。最基础的就是位定时计数器:

localparam TQ_NUM = 16;
reg [3:0] bit_timer;

always @(posedge clk_8m or negedge rst_n) begin
    if (!rst_n)
        bit_timer <= 0;
    else if (sync_edge || bus_idle)
        bit_timer <= 0;
    else
        bit_timer <= bit_timer + 1;
end

这里的关键在于 硬同步 重同步 的触发条件。当检测到帧起始(SOF)的下降沿,或总线空闲后首次活动,都要立即复位计数器,确保所有节点重新对齐。这就是CAN网络自同步能力的硬件体现。

进一步地,我们可以定义采样点和发送点:

wire sample_point = (bit_timer == 8);  // 第9个tq采样
wire tx_point     = (bit_timer == 15); // 最后一个tq更新输出

这样,在每个位周期内,我们就能在精确时刻读取总线状态,并决定下一个bit的值。


发送机:不只是“发数据”

发送模块远不止把寄存器里的数据推到总线上那么简单。它必须参与仲裁、执行位填充、计算CRC,并监听自己的输出是否被正确响应。

位填充机制 为例:CAN协议规定,连续5个相同电平后必须插入一个反向位,防止长时间无跳变导致时钟失步。这意味着你不能直接发送原始比特流,而要边发边监测:

reg [4:0] same_count; // 连续相同位计数
reg       last_bit;
wire      stuffed_bit = (same_count == 5) ? ~current_data_bit : current_data_bit;

always @(posedge clk_8m) begin
    if (tx_start) begin
        same_count <= 0;
        last_bit   <= 1'b1; // 初始为隐性
    end else if (tx_enable && bit_timer == 0) begin
        if (current_data_bit == last_bit)
            same_count <= same_count + 1;
        else
            same_count <= 1;
        last_bit <= current_data_bit;
    end
end

这段逻辑会实时跟踪当前位是否与前一位相同,并在第五次重复时强制插入翻转位。接收端则要做相反操作——去填充。如果发现连续6个相同位,则判定为 填充错误 ,触发错误帧。

另一个关键点是 CRC计算 。CAN使用CRC-15多项式:
$$
G(x) = x^{15} + x^{14} + x^{10} + x^8 + x^7 + x^4 + x^3 + 1
$$

其串行实现如下:

reg [14:0] crc_reg;

always @(posedge clk_8m) begin
    if (reset_crc)
        crc_reg <= 15'hFFFF;
    else if (crc_en) begin
        crc_reg[14] <= crc_reg[13];
        crc_reg[13] <= crc_reg[12];
        crc_reg[12] <= crc_reg[11];
        crc_reg[11] <= crc_reg[10];
        crc_reg[10] <= crc_reg[9] ^ new_bit;
        crc_reg[9]  <= crc_reg[8];
        crc_reg[8]  <= crc_reg[7] ^ new_bit;
        crc_reg[7]  <= crc_reg[6] ^ new_bit;
        crc_reg[6]  <= crc_reg[5];
        crc_reg[5]  <= crc_reg[4] ^ new_bit;
        crc_reg[4]  <= crc_reg[3] ^ new_bit;
        crc_reg[3]  <= crc_reg[2] ^ new_bit;
        crc_reg[2]  <= crc_reg[1];
        crc_reg[1]  <= crc_reg[0];
        crc_reg[0]  <= new_bit;
    end
end

注意:CRC在SOF之后就开始计算,包含仲裁段、控制段、数据段,但不包括填充位。也就是说,填充是在CRC之后进行的,这一点极易出错。


接收机:如何从噪声中重建信号?

接收比发送更复杂,因为它必须在不确定的时机启动,并准确捕捉每一个bit。

第一步是 SOF检测 。由于CAN总线空闲时为隐性(1),帧起始是一个显性(0)下降沿:

wire rx_falling = can_rx_d & ~can_rx; // 两级寄存防亚稳态
always @(posedge clk_8m) can_rx_d <= can_rx;

always @(posedge clk_8m or negedge rst_n) begin
    if (!rst_n)
        sof_detected <= 0;
    else
        sof_detected <= rx_falling && bus_idle_state;
end

一旦检测到SOF,立即启动位定时器,并在后续每16个tq推进一位。推荐采用 三倍过采样 策略:每个bit在第6、7、8个tq分别采样三次,取多数结果作为该位值,有效抑制毛刺。

接下来是 去填充处理 。每当接收到5个连续相同位后,若下一位仍相同,则认为是填充位,应跳过;若不同,则正常接收。但如果出现连续6个相同位(如六个0),则构成 位填充错误 ,需上报。

此外,接收端还需实现 ID滤波机制 。例如配置一个11位ID掩码寄存器,只有 (received_id & mask) == (accept_id & mask) 的帧才进入接收FIFO。这对于多节点系统尤为重要,避免MCU被无关报文淹没。


Vivado实战:从代码到硬件

工程搭建要点

使用Xilinx Vivado 2023.1创建新项目时,建议按以下流程操作:

  1. 选择目标器件(如XC7A35T-CSG324-1);
  2. 添加Verilog源文件及测试激励;
  3. 编写XDC约束文件,明确时钟和I/O属性。

关键约束示例:

create_clock -name sys_clk -period 20.000 [get_ports clk_50m]
set_false_path -from [get_ports can_rx] -to [all_registers]
set_property IOSTANDARD LVCMOS33 [get_ports {can_tx can_rx}]

其中 set_false_path 是必要的,因为 can_rx 来自外部异步信号,不应参与时序分析,否则可能报违例。

顶层接口设计

一个实用的CAN控制器IP核应提供简洁的CPU接口:

module can_controller_top (
    input         clk_50m,
    input         rst_n,
    input         can_rx,
    output        can_tx,
    input  [10:0] tx_id,
    input  [7:0]  tx_data [0:7],
    input  [2:0]  tx_dlc,
    input         tx_req,
    output        tx_done,
    output [10:0] rx_id,
    output [7:0]  rx_data [0:7],
    output [2:0]  rx_dlc,
    output        rx_valid,
    output        error_alert
);

CPU只需写入ID、数据长度(DLC)、数据内容,然后拉高 tx_req 即可发起发送。完成后 tx_done 置位并产生中断。接收数据则通过 rx_valid 通知,由CPU读取。

调试技巧

光仿真还不够,真实世界总有意外。推荐使用ILA(Integrated Logic Analyzer)抓取内部信号:

  • 观察 bit_timer 是否按时归零;
  • 检查 state 机是否按预期跳转;
  • 查看 crc_reg 在帧结束时是否匹配接收方校验值。

也可以加入环回模式(loopback mode)用于自检:将 can_tx 内部连接至 can_rx 输入路径,无需外接收发器即可验证发送流程完整性。


实际应用中的权衡与优化

当你把控制器集成进真实系统时,会面临一系列现实问题。

面积 vs 功能

在小型FPGA(如Artix-7 35T)上,资源有限。此时可考虑裁剪功能:
- 关闭扩展帧支持(仅处理11位ID);
- 固定波特率为500 kbps,省去动态配置逻辑;
- 使用单缓冲区而非双FIFO,减少BRAM占用。

实测表明,精简版CAN控制器可在<1000 LUTs内实现,适合资源敏感场景。

错误管理策略

CAN节点有三种错误状态:
- 错误主动 :正常通信,可发送错误帧;
- 错误被动 :受限通信,禁止主动错误通知;
- 总线关闭 :完全离线,需软件重启。

建议在控制器中维护两个错误计数器(TEC/RPC),依据规范自动升降级状态。更进一步,可添加超时恢复机制:若连续100次发送失败,则强制进入总线关闭,并通过中断通知CPU介入。

向CAN FD演进的可能性

虽然本文聚焦经典CAN,但设计时可预留升级空间:
- 数据段支持最大64字节(未来兼容CAN FD的64B payload);
- CRC字段扩展至17/21位选项;
- 波特率切换机制(仲裁段低速,数据段高速)。

这些改动不会显著增加当前逻辑复杂度,却为未来升级铺平道路。


写在最后:为什么我们要亲手实现CAN控制器?

市面上已有成熟的CAN控制器芯片(如MCP2515)和软核方案,为何还要费力用Verilog重造轮子?

答案在于 控制权 。当你自己实现了CRC、位定时、仲裁逻辑,你就不再只是“调API”的使用者,而是真正理解了协议的呼吸节奏。你能知道为什么采样点不能太靠前,也明白填充错误为何比CRC错误更早被发现。

更重要的是,在高度集成的SoC系统中,将CAN控制器作为FPGA内部IP核,不仅能节省PCB面积和BOM成本,还能与其他模块(如PWM生成、传感器采集)共享时钟域和内存资源,构建真正的片上系统。

这条路不容易,但每一步都踏实。从第一个SOF被正确识别,到第一帧数据成功回传,那种“我让它动起来了”的成就感,正是硬件工程师最纯粹的乐趣所在。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐