1. SJA1000 CAN总线控制器概述
SJA1000是NXP公司推出的一款独立CAN控制器,广泛应用于汽车电子和工业控制领域。作为CAN总线通信的核心器件,它实现了CAN2.0A/B协议规范,支持最高1Mbps的通信速率。在汽车电子系统中,SJA1000常被用于ECU(电子控制单元)之间的数据交换,如发动机控制、车身电子等场景。
我曾在多个汽车电子项目中采用Verilog实现SJA1000控制器,发现其设计关键在于状态机的合理划分和时序的精确控制。与直接使用现成IP核相比,自主实现可以更灵活地适配不同FPGA平台,也便于进行功能定制和性能优化。
2. CAN总线协议基础
2.1 CAN协议核心特性
CAN总线采用差分信号传输(CAN_H和CAN_L),具有以下突出特点:
- 多主架构:任何节点都可以在总线空闲时发起通信
- 非破坏性仲裁:通过标识符优先级解决总线冲突
- 强大的错误检测:包含CRC校验、帧检查等5种错误检测机制
- 高可靠性:典型应用环境下误码率低于10^-11
2.2 数据帧结构解析
标准CAN帧(2.0A)由以下字段组成:
code复制[SOF][11位ID][RTR][IDE][r0][4位DLC][数据域(0-8字节)][CRC][ACK][EOF]
其中关键字段说明:
- ID:报文标识符,决定报文优先级
- DLC:数据长度码(0-8)
- 数据域:实际传输的有效载荷
注意:实际Verilog实现时需要严格遵循位时序规范,特别是同步段、传播段等时间段的设置,这直接影响通信稳定性。
3. Verilog实现方案设计
3.1 整体架构设计
完整的SJA1000控制器包含以下功能模块:
- 协议引擎:处理CAN帧的组帧和解析
- 位时序处理:管理位定时和同步
- 验收滤波:实现报文过滤
- 错误管理:错误检测和统计
- 寄存器接口:提供配置访问通道
我们采用自顶向下的设计方法,先定义顶层接口:
verilog复制module sja1000_top (
input clk_16mhz, // 16MHz系统时钟
input rst_n, // 低电平复位
// CAN总线物理接口
output can_tx,
input can_rx,
// 主机接口
input [7:0] addr,
input [7:0] data_in,
output [7:0] data_out,
input wr_n,
input rd_n,
input cs_n,
// 中断输出
output int_n
);
3.2 三段式状态机实现
3.2.1 发送状态机设计
发送过程采用经典的三段式状态机:
verilog复制// 状态定义
typedef enum logic [2:0] {
IDLE,
TX_SOF, // 发送起始帧
TX_ID, // 发送标识符
TX_DATA, // 发送数据域
TX_CRC, // 发送CRC
TX_ACK, // 等待应答
TX_EOF // 发送结束帧
} tx_state_t;
// 状态转移逻辑
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_state <= IDLE;
end else begin
case (tx_state)
IDLE: if (tx_req) tx_state <= TX_SOF;
TX_SOF: tx_state <= TX_ID;
// ...其他状态转移
TX_EOF: tx_state <= IDLE;
endcase
end
end
// 输出逻辑
always_comb begin
case (tx_state)
TX_SOF: can_tx = 0; // 显性位
TX_ID: can_tx = id_bits[id_index];
// ...其他输出
endcase
end
3.2.2 接收状态机设计
接收状态机需要处理位填充规则和错误检测:
verilog复制typedef enum logic [3:0] {
RX_IDLE,
RX_SOF,
RX_ID,
RX_CTRL,
RX_DATA,
RX_CRC,
RX_ACK,
RX_EOF,
RX_ERROR
} rx_state_t;
// 位填充检测逻辑
always_ff @(posedge clk) begin
if (bit_stuff_cnt == 5) begin
// 检测到6个相同位,应出现填充位
if (can_rx == last_bit) begin
rx_state <= RX_ERROR;
end
bit_stuff_cnt <= 0;
end else if (can_rx == last_bit) begin
bit_stuff_cnt <= bit_stuff_cnt + 1;
end else begin
bit_stuff_cnt <= 0;
end
last_bit <= can_rx;
end
4. 关键实现细节
4.1 位时序处理
CAN总线要求严格的位定时设置,典型配置如下(16MHz时钟):
code复制Tq = 1/16MHz = 62.5ns
同步段(Sync_Seg) = 1Tq
传播段(Prop_Seg) = 2Tq
相位缓冲段1(Phase_Seg1) = 3Tq
相位缓冲段2(Phase_Seg2) = 2Tq
采样点位置 = (Sync_Seg + Prop_Seg + Phase_Seg1) / 总Tq = 6/8 = 75%
Verilog实现代码:
verilog复制// 位时序生成器
always @(posedge clk_16mhz) begin
if (bit_timer == (TSEG1 + TSEG2)) begin
bit_timer <= 0;
sample_point <= 1;
end else begin
bit_timer <= bit_timer + 1;
sample_point <= (bit_timer == (TSEG1 - 1));
end
end
4.2 CRC校验实现
CAN使用15位CRC多项式:x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1
verilog复制// CRC计算模块
always @(posedge clk) begin
if (crc_en) begin
crc[14:0] <= next_crc;
end else if (crc_init) begin
crc <= 15'h0000;
end
end
assign next_crc[0] = crc[14] ^ bit_in;
assign next_crc[1] = crc[0];
assign next_crc[2] = crc[1];
// ...中间位省略
assign next_crc[14] = crc[13] ^ (crc[14] ^ bit_in);
5. 验证与调试
5.1 测试平台搭建
建议采用以下验证方法:
- 单元测试:针对每个状态机单独验证
- 总线仿真:使用CANoe或类似工具模拟总线环境
- 硬件回环测试:短接TX和RX引脚
典型测试用例:
verilog复制initial begin
// 复位测试
rst_n = 0;
#100 rst_n = 1;
// 发送标准帧测试
send_frame(11'h123, 8'hAA);
// 错误帧注入测试
force can_rx = 0; // 违反位填充规则
#1000 release can_rx;
end
5.2 常见问题排查
-
通信失败
- 检查位时序配置是否匹配总线速率
- 验证采样点位置(建议75%-80%)
- 检查物理层终端电阻(120Ω)
-
CRC校验错误
- 确认CRC多项式实现正确
- 检查数据域传输顺序(MSB/LSB)
- 验证填充位处理逻辑
-
状态机卡死
- 添加超时保护机制
- 检查所有状态转移条件是否完备
- 添加状态机监控逻辑
6. 性能优化技巧
- 流水线设计
verilog复制// CRC计算与位处理并行
always @(posedge clk) begin
tx_bit <= next_bit; // 位处理
crc <= next_crc; // CRC计算
bit_cnt <= next_bit_cnt; // 位计数器
end
- 时钟域交叉处理
verilog复制// 异步信号同步化
always @(posedge clk) begin
can_rx_sync <= {can_rx_sync[0], can_rx};
end
assign can_rx_stable = (can_rx_sync[1] == can_rx_sync[0]);
- 资源优化
- 使用共享的CRC计算模块
- 时分复用发送和接收缓冲区
- 采用格雷码编码状态寄存器
我在实际项目中发现,合理设置状态机的超时阈值能显著提高系统鲁棒性。例如在发送状态机中添加:
verilog复制// 发送超时保护
always @(posedge clk) begin
if (tx_state != IDLE) begin
if (timeout_cnt == TIMEOUT_VAL) begin
tx_state <= IDLE;
error_flag <= 1;
end else begin
timeout_cnt <= timeout_cnt + 1;
end
end else begin
timeout_cnt <= 0;
end
end
对于需要兼容不同CAN协议版本的项目,建议采用参数化设计:
verilog复制module can_core #(
parameter SUPPORT_CANFD = 0
) (
// 接口定义
);
generate
if (SUPPORT_CANFD) begin
// CAN-FD专用逻辑
end else begin
// 标准CAN逻辑
end
endgenerate
endmodule