CAN控制器的Verilog实现
本文详细介绍如何使用Verilog在FPGA上实现CAN协议控制器,涵盖位定时、仲裁、CRC计算、位填充等核心机制,并结合Vivado工程实践完成硬件部署,适用于汽车电子与工业控制领域的高可靠性通信需求。
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创建新项目时,建议按以下流程操作:
- 选择目标器件(如XC7A35T-CSG324-1);
- 添加Verilog源文件及测试激励;
- 编写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被正确识别,到第一帧数据成功回传,那种“我让它动起来了”的成就感,正是硬件工程师最纯粹的乐趣所在。
更多推荐
所有评论(0)