【FPGA实战】基于Verilog的MCP2515 CAN控制器SPI驱动详解 | 附完整代码
📌 摘要:本文详细介绍一款基于Verilog HDL实现的MCP2515 CAN控制器SPI驱动模块。内容涵盖模块架构设计、接口协议说明、状态机工作流程、数据格式转换逻辑及关键代码解析,适合FPGA工程师、嵌入式开发者学习参考。文末附完整可综合代码及调试建议。
一、前言:为什么需要这个模块?
1.1 CAN总线简介
CAN(Controller Area Network)总线是一种广泛应用于汽车电子、工业控制领域的串行通信协议,具有多主仲裁、错误检测、高可靠性 等特点。在资源受限或需要隔离的场景中,常通过SPI接口的外置CAN控制器(如MCP2515)实现CAN通信功能。
1.2 MCP2515芯片特性
Microchip的MCP2515是一款经典的独立CAN控制器,主要特点:
- ✅ 支持CAN 2.0B协议(标准帧/扩展帧)
- ✅ SPI接口(最高10MHz)与MCU/FPGA通信
- ✅ 3个发送缓冲区 + 2个接收缓冲区
- ✅ 可编程波特率(5Kbps~1Mbps)
- ✅ 中断输出、休眠模式等低功耗特性
1.3 本模块设计目标
verilog
// 核心目标:
// 1. 封装MCP2515的底层SPI操作,提供简洁的RAM式接口
// 2. 自动完成初始化、波特率配置、中断轮询
// 3. 支持扩展帧格式转换,适配上层"以太网风格"数据帧
// 4. 全同步设计,可综合,适配20MHz系统时钟
二、模块整体架构
2.1 顶层接口概览
verilog
module mcp2515_controller #(
parameter CLK_FREQ_HZ = 20_000_000, // FPGA系统时钟
parameter CAN_BAUD_HZ = 250_000, // CAN波特率
parameter OSC_FREQ_HZ = 20_000_000 // MCP2515晶振频率
)(
// 系统接口
input wire clk, rst_n,
// SPI接口(连接MCP2515)
output wire can_cs_n, can_sclk, can_mosi,
input wire can_miso,
// 控制信号
input wire can_int_n, // MCP2515中断(低有效)
output wire can_rst_n, can_clk,
// TX RAM接口(外部写,内部读)
input wire tx_ram_wr_clk, tx_ram_wr_en, tx_trigger,
input wire [3:0] tx_ram_wr_addr,
input wire [7:0] tx_ram_wr_data,
output wire tx_busy,
// RX RAM接口(内部写,外部读)
input wire rx_ram_rd_clk, rx_done,
input wire [3:0] rx_ram_rd_addr,
output wire [7:0] rx_ram_rd_data,
output reg rx_valid,
// 状态输出
output reg init_done,
output reg [7:0] status_reg, error_reg
);
2.2 数据流图

2.3 关键参数配置
| 参数 | 默认值 | 说明 |
|---|---|---|
CLK_FREQ_HZ |
20MHz | FPGA系统时钟,用于时序计算 |
CAN_BAUD_HZ |
250Kbps | 目标CAN波特率 |
OSC_FREQ_HZ |
20MHz | MCP2515外接晶振频率 |
💡 波特率配置原理 :MCP2515通过
CNF1/2/3寄存器设置位时序。本模块预设250Kbps配置:
veriloglocalparam VAL_CNF1 = 8'h03; // SJW=1, BRP=3 localparam VAL_CNF2 = 8'h92; // PHSEG1=3, PRSEG=3, BTLMODE=1 localparam VAL_CNF3 = 8'h02; // PHSEG2=3 // 计算公式:Baud = Fosc / (2×BRP×(1+PRSEG+PHSEG1+PHSEG2)) // 250K = 20M / (2×4×(1+3+3+3)) ✓
三、关键接口协议详解
3.1 SPI通信时序
MCP2515采用SPI Mode 0(CPOL=0, CPHA=0):
- 📡 SCLK空闲为低,上升沿采样数据,下降沿输出数据
- 🔄 MSB先传,8位为一字节
- ⚡ 本模块SPI时钟固定10MHz(
can_sclk = clk/2)
verilog
// SPI状态机核心片段(简化)
case (spi_state)
SPI_TX: begin
if (!spi_phase) begin
can_sclk <= 0;
can_mosi <= spi_shift_reg[7]; // 下降沿输出
spi_phase <= 1;
end else begin
can_sclk <= 1;
spi_shift_reg <= {spi_shift_reg[6:0], can_miso}; // 上升沿采样
spi_phase <= 0;
// ...字节计数逻辑
end
end
endcase
3.2 TX/RX RAM数据格式(16字节/帧)
模块采用统一帧格式,屏蔽MCP2515寄存器细节:
| 地址 | 字段 | 扩展帧格式 | 标准帧格式 |
|---|---|---|---|
| 0 | 控制字 | [IDE, RTR, 2'b00, DLC] |
同左 |
| 1 | ID28:21 | EID28:21 | SID10:3 |
| 2 | ID20:13 | {EID[20:18], SRR, EXIDE, EID[17:16]} |
{SID[2:0], RTR, 1'b0, 1'b0, 2'b00} |
| 3-4 | EID15:0 | 扩展ID低16位 | 保留 |
| 5-12 | Data0:7 | 数据字节(最多8字节) | 同左 |
| 13-15 | 保留 | - | - |
🔁 格式转换逻辑 :模块内部通过
tx_cov_ram/rx_cov_ram组合逻辑自动完成格式映射,用户只需按统一格式读写RAM。
3.3 控制信号时序
verilog
// 发送流程:
1. 外部逻辑在tx_ram_wr_clk上升沿写入16字节帧数据
2. 拉高tx_trigger脉冲(≥1个clk周期)
3. 模块自动检测TXB0空闲→加载数据→发送→等待完成
4. tx_busy拉高表示发送中,拉低表示可发送新帧
// 接收流程:
1. MCP2515收到CAN帧后拉低can_int_n
2. 模块轮询CANINTF寄存器,识别RX0IF/RX1IF
3. 自动读取RXB0/RXB1缓冲区,写入rx_ram
4. 拉高rx_valid脉冲通知外部逻辑
5. 外部逻辑读取完成后拉高rx_done,模块清除中断标志
四、核心状态机深度解析
4.1 SPI事务控制器(底层驱动)
verilog
// 6状态SPI控制器,支持可变长度读写
localparam SPI_IDLE=0, SPI_CS_LO=1, SPI_TX=2, SPI_RX=3, SPI_CS_HI=4, SPI_DONE=5;
// 关键特性:
// • 支持先TX后RX的复合事务(如READ命令)
// • 字节计数自动切换收发模式
// • 严格的CS/SCLK时序控制,满足MCP2515时序要求
4.2 主控制状态机(业务流程)

4.3 中断处理机制
verilog
// 双缓冲接收设计:
// • RX0/RX1独立中断标志,避免帧丢失
// • 优先级:先处理挂起的接收,再响应发送请求
// • 自动清除中断标志,防止重复触发
if (spi_rx_buf[0] & 8'h01) rx0_pending <= 1; // RX0IF
if (spi_rx_buf[0] & 8'h02) rx1_pending <= 1; // RX1IF
// 清除中断使用BIT_MODIFY命令(原子操作,避免读-改-写竞争)
spi_tx_buf[0] <= CMD_BIT_MODIFY;
spi_tx_buf[1] <= ADDR_CANINTF;
spi_tx_buf[2] <= 8'h01; // 掩码:清除RX0IF
spi_tx_buf[3] <= 8'h00; // 值:写0
五、关键代码片段精讲
5.1 数据格式转换(TX方向)
verilog
// 扩展帧ID拆分示例(32位→MCP2515寄存器格式)
assign tx_cov_ram[0] = (tx_ide) ? tx_id[28:21] : tx_id[10:3]; // SIDH
assign tx_cov_ram[1] = (tx_ide) ?
{tx_id[20:18], 1'b1, 1'b1, 1'b0, tx_id[17:16]} : // SIDL: EXIDE=1, SRR=1
{tx_id[2:0], tx_rtr, 1'b0, 1'b0, 2'b00}; // 标准帧SIDL
5.2 初始化序列(确保可靠启动)
verilog
// 关键步骤:
// 1. 发送RESET命令(软复位所有寄存器)
// 2. 等待振荡器稳定(10us延时)
// 3. 配置波特率寄存器(必须在配置模式下)
// 4. 设置接收过滤为"接收所有"(开发阶段方便调试)
// 5. 使能接收中断
// 6. 切换到正常模式,轮询CANSTAT确认模式切换成功
// 错误处理:若10次轮询未进入正常模式,置ERR_INIT_FAIL并重启初始化
5.3 发送流程(零拷贝优化)
verilog
// 使用LOAD_TX0命令一次性写入14字节(地址0x31~0x3E)
// 避免多次SPI事务,提升效率
spi_tx_buf[0] <= CMD_LOAD_TX0_ID; // 0x40: 从SIDH开始加载
spi_tx_buf[1] <= tx_cov_ram[0]; // SIDH
spi_tx_buf[2] <= tx_cov_ram[1]; // SIDL
// ... 中间11字节 ...
spi_tx_buf[13] <= tx_cov_ram[12]; // D7
spi_tx_cnt <= 4'd14; // 命令+13字节数据
// 发送RTS命令触发硬件发送
spi_tx_buf[0] <= CMD_RTS_TX0; // 0x81: Request-to-Send for TXB0
六、使用示例与调试指南
6.1 模块实例化
verilog
mcp2515_controller #(
.CLK_FREQ_HZ(50_000_000), // 根据实际时钟修改
.CAN_BAUD_HZ(500_000), // 500Kbps CAN
.OSC_FREQ_HZ(16_000_000) // MCP2515使用16MHz晶振
) u_can_ctrl (
.clk(sys_clk),
.rst_n(sys_rst_n),
// SPI连接
.can_cs_n(can_cs_n),
.can_sclk(can_sclk),
.can_mosi(can_mosi),
.can_miso(can_miso),
// 控制信号
.can_int_n(can_int_n),
.can_rst_n(can_rst_n),
.can_clk(can_clk_out), // 连接到MCP2515 OSC1
// TX接口
.tx_ram_wr_clk(user_clk),
.tx_ram_wr_en(tx_wr_en),
.tx_ram_wr_addr(tx_addr),
.tx_ram_wr_data(tx_data),
.tx_trigger(tx_start_pulse),
.tx_busy(can_tx_busy),
// RX接口
.rx_ram_rd_clk(user_clk),
.rx_ram_rd_addr(rx_addr),
.rx_ram_rd_data(rx_data),
.rx_valid(can_rx_valid),
.rx_done(rx_read_done),
// 状态
.init_done(can_ready),
.status_reg(can_status),
.error_reg(can_error)
);
6.2 发送一帧扩展帧数据
verilog
// 假设要发送:扩展ID=0x18FF0001, DLC=8, 数据=8'hAA~8'hFF
reg [7:0] tx_frame [0:15];
initial begin
tx_frame[0] = 8'b1000_1000; // IDE=1, RTR=0, DLC=8
tx_frame[1] = 8'h18; // EID[28:21]
tx_frame[2] = 8'hFF; // {EID[20:18], SRR=1, EXIDE=1, EID[17:16]}
tx_frame[3] = 8'h00; // EID[15:8]
tx_frame[4] = 8'h01; // EID[7:0]
tx_frame[5] = 8'hAA; // Data[0]
// ... 填充Data[1:7] ...
tx_frame[12] = 8'hFF; // Data[7]
// 写入TX RAM(16个时钟周期)
for (integer i=0; i<16; i=i+1) begin
#10 tx_ram_wr_addr = i;
#10 tx_ram_wr_data = tx_frame[i];
#10 tx_ram_wr_en = 1;
#10 tx_ram_wr_en = 0;
end
// 触发发送
#20 tx_trigger = 1;
#10 tx_trigger = 0;
// 等待发送完成
wait (!can_tx_busy);
$display("Frame sent!");
end
6.3 常见调试问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
init_done 始终为0 |
SPI连接错误/晶振未起振 | 用ILA抓取SPI波形,检查MCP2515 OSC1/OSC2 |
| 发送后无CAN波形 | TXEN未使能/波特率配置错误 | 检查ADDR_TXB0CTRL的TXEN位,验证CNF寄存器值 |
| 接收不到帧 | 过滤器配置过严 | 开发阶段设VAL_RXB_CTRL=8'h60(接收所有) |
tx_busy 不释放 |
TXB0被占用/发送失败 | 检查ADDR_TEC/REC错误计数器,增加重试逻辑 |
| SPI数据错乱 | 时钟域跨域问题 | 确保tx_ram_wr_clk与clk异步时使用同步器 |
🔧 调试技巧 :在
M_READY状态添加$display或ILA探针,监控main_state、can_intf_reg、spi_state等关键信号。
七、优化建议与扩展方向
7.1 性能优化
- ✅ 多缓冲区支持:当前仅用TXB0,可扩展TXB1/TXB2实现发送队列
- ✅ 零拷贝接收:RX RAM可直接映射到FIFO,减少RAM访问延迟
- ✅ 中断驱动替代轮询 :将
can_int_n连接到FPGA中断控制器,降低CPU占用
7.2 可靠性增强
verilog
// 建议添加:
// 1. SPI事务超时检测(防止MCP2515挂死)
// 2. CAN错误计数器监控(TEC/REC > 127时报警)
// 3. 看门狗复位机制(主状态机卡死时自动重启)
// 示例:SPI超时检测
reg [15:0] spi_timeout;
always @(posedge clk) begin
if (spi_busy) begin
spi_timeout <= spi_timeout + 1;
if (spi_timeout > 16'd10000) begin // 500us超时
error_reg <= ERR_SPI_ERR;
main_state <= M_ERROR;
end
end else begin
spi_timeout <= 0;
end
end
7.3 低功耗设计
- 🌙 利用MCP2515的
Sleep Mode:空闲时发送CMD_WRITE到CANCTRL进入休眠 - ⚡ 动态调整SPI时钟:通信时10MHz,空闲时降频降低功耗
八、总结
本mcp2515_controller模块通过分层设计思想,将复杂的MCP2515 SPI操作封装为简洁的RAM接口,具有以下优势:
🔹 易用性 :用户只需按16字节帧格式读写RAM,无需关心底层寄存器
🔹 可靠性 :完整的初始化序列+错误处理机制,适应工业环境
🔹 可移植性 :参数化时钟/波特率,适配不同FPGA平台
🔹 可综合 :纯同步逻辑,无#delay,支持Xilinx/Intel主流工具链
九、完整代码
verilog
//==============================================================================
// Module Name : mcp2515_controller
// Description : SPI Controller for MCP2515 CAN Controller
//==============================================================================
module mcp2515_controller #(
parameter CLK_FREQ_HZ = 20_000_000, // FPGA system clock frequency
parameter CAN_BAUD_HZ = 250_000, // CAN bus baud rate
parameter OSC_FREQ_HZ = 20_000_000 // MCP2515 oscillator frequency
)(
//==========================================================================
// System Interface
//==========================================================================
input wire clk, // FPGA system clock (20MHz)
input wire rst_n, // Active-low asynchronous reset
//==========================================================================
// MCP2515 SPI Interface
//==========================================================================
output wire can_cs_n, // SPI Chip Select (active low)
output wire can_sclk, // SPI Clock (10MHz)
output wire can_mosi, // SPI Master Out Slave In
input wire can_miso, // SPI Master In Slave Out
//==========================================================================
// MCP2515 Control Signals
//==========================================================================
input wire can_int_n, // MCP2515 INT pin (active low)
output wire can_rst_n, // MCP2515 RESET pin (active low)
output wire can_clk, // 20MHz clock output to MCP2515 OSC1
//==========================================================================
// TX RAM Interface (External Write, Internal Read)
// Frame format (16 bytes per frame):
// [0] SIDH = EID[28:21]
// [1] SIDL = {EID[20:18], SRR, EXIDE(1'b1), EID[17:16]}
// [2] EID8 = EID[15:8]
// [3] EID0 = EID[7:0]
// [4] DLC = {1'b0, RTR, 2'b00, DLC[3:0]}
// [5] D0 = Data[7:0]
// ...
// [13-15] Reserved
//==========================================================================
input wire tx_ram_wr_clk,
input wire tx_ram_wr_en, // TX RAM write enable
input wire [3:0] tx_ram_wr_addr, // TX RAM write address (0-15)
input wire [7:0] tx_ram_wr_data, // TX RAM write data
input wire tx_trigger, // Pulse to start transmitting current frame
output wire tx_busy, // High when transmission in progress
//==========================================================================
// RX RAM Interface (Internal Write, External Read)
//==========================================================================
input wire rx_ram_rd_clk,
input wire [3:0] rx_ram_rd_addr, // RX RAM read address (0-15)
output wire [7:0] rx_ram_rd_data, // RX RAM read data
output reg rx_valid, // Pulse when new frame stored in RX RAM
input wire rx_done, // Pulse when frame read by external logic
//==========================================================================
// Status Interface
//==========================================================================
output reg init_done, // High when initialization complete
output reg [7:0] status_reg, // Current operation status
output reg [7:0] error_reg // Error flags register
);
//==========================================================================
// MCP2515 SPI Command Definitions (Table 12-1 in datasheet)
//==========================================================================
localparam CMD_RESET = 8'hC0; // Reset internal registers
localparam CMD_READ = 8'h03; // Read from register
localparam CMD_WRITE = 8'h02; // Write to register
localparam CMD_BIT_MODIFY = 8'h05; // Bit modify register
// LOAD TX BUFFER commands
localparam CMD_LOAD_TX0_ID = 8'h40; // Load TXB0 starting at SIDH (addr 0x31)
localparam CMD_LOAD_TX0_DT = 8'h41; // Load TXB0 starting at D0 (addr 0x36)
// REQUEST-TO-SEND commands
localparam CMD_RTS_TX0 = 8'h81; // RTS for TXB0
localparam CMD_RTS_TX1 = 8'h82; // RTS for TXB1
localparam CMD_RTS_TX2 = 8'h84; // RTS for TXB2
// READ RX BUFFER commands
localparam CMD_READ_RX0 = 8'h90; // Read RXB0 starting at SIDH (addr 0x61)
localparam CMD_READ_RX1 = 8'h94; // Read RXB1 starting at SIDH (addr 0x71)
// Status commands
localparam CMD_READ_STAT = 8'hA0; // Quick status read
localparam CMD_RX_STATUS = 8'hB0; // RX filter match status
//==========================================================================
// MCP2515 Register Addresses
//==========================================================================
localparam ADDR_RXF0SIDH = 8'h00;
localparam ADDR_RXF0SIDL = 8'h01;
localparam ADDR_BFPCTRL = 8'h0C;
localparam ADDR_TXRTSCTRL = 8'h0D;
localparam ADDR_CANSTAT = 8'h0E;
localparam ADDR_CANCTRL = 8'h0F;
localparam ADDR_TEC = 8'h1C;
localparam ADDR_REC = 8'h1D;
localparam ADDR_CNF3 = 8'h28;
localparam ADDR_CNF2 = 8'h29;
localparam ADDR_CNF1 = 8'h2A;
localparam ADDR_CANINTE = 8'h2B;
localparam ADDR_CANINTF = 8'h2C;
localparam ADDR_EFLG = 8'h2D;
localparam ADDR_TXB0CTRL = 8'h30;
localparam ADDR_TXB0SIDH = 8'h31;
localparam ADDR_TXB0SIDL = 8'h32;
localparam ADDR_TXB0EID8 = 8'h33;
localparam ADDR_TXB0EID0 = 8'h34;
localparam ADDR_TXB0DLC = 8'h35;
localparam ADDR_TXB0D0 = 8'h36;
localparam ADDR_RXB0CTRL = 8'h60;
localparam ADDR_RXB0SIDH = 8'h61;
localparam ADDR_RXB0D0 = 8'h66;
localparam ADDR_RXB1CTRL = 8'h70;
//==========================================================================
// Baud Rate Configuration
//==========================================================================
localparam VAL_CNF1 = 8'h03; // SJW=1, BRP=3
localparam VAL_CNF2 = 8'h92; // BTLMODE=1, SAM=0, PHSEG1=3, PRSEG=3
localparam VAL_CNF3 = 8'h02; // PHSEG2=3
localparam VAL_RXB_CTRL = 8'h60; // RXM[1:0]=11, receive all messages
localparam VAL_CANINTE = 8'h03; // RX0IE=1, RX1IE=1 (receive interrupts)
localparam VAL_CANCTRL = 8'h03; // Normal mode, CLKOUT=disabled
//==========================================================================
// Timing parameters
//==========================================================================
localparam OST_WAIT_CNT = 8'd200; // Wait 200*50ns = 10us
localparam POLL_INTERVAL = 16'd1000; // Poll interval: 1000*50ns = 50us
localparam TRANS_DELAY = 16'd10000; // Frame Trans Delay: 10000*50ns = 500us
//==========================================================================
// Internal RAM (16 bytes per frame)
//==========================================================================
reg [7:0] tx_ram [0:15];
reg [7:0] rx_ram [0:15];
wire [7:0] tx_cov_ram [0:15];
wire [7:0] rx_cov_ram [0:15];
reg [7:0] rx_ram_rdata;
// RX RAM write control signals
reg rx_ram_wr_en;
reg [3:0] rx_ram_wr_addr;
reg [7:0] rx_ram_wr_data;
// TX RAM write port (external)
always @(posedge tx_ram_wr_clk) begin
if (tx_ram_wr_en) begin
tx_ram[tx_ram_wr_addr] <= tx_ram_wr_data;
end
end
// RX RAM write port (internal)
always @(posedge clk) begin
if (rx_ram_wr_en) begin
rx_ram[rx_ram_wr_addr] <= rx_ram_wr_data;
end
end
// RX RAM read port (external)
always @(posedge rx_ram_rd_clk) begin
rx_ram_rdata <= rx_cov_ram[rx_ram_rd_addr];
end
assign rx_ram_rd_data = rx_ram_rdata;
//==========================================================================
// 1. RX Logic (MCP2515 Registers -> Ethernet Format)
//==========================================================================
wire rx_ide = rx_ram[1][3];
wire rx_rtr = rx_ram[4][6];
wire [3:0] rx_dlc = rx_ram[4][3:0];
assign rx_cov_ram[0] = {rx_ide, rx_rtr, 2'b00, rx_dlc};
assign rx_cov_ram[1] = (rx_ide) ? {3'b000, rx_ram[0][7:3]} : 8'h00;
assign rx_cov_ram[2] = (rx_ide) ? {rx_ram[0][2:0], rx_ram[1][7:5], rx_ram[1][1:0]} : 8'h00;
assign rx_cov_ram[3] = (rx_ide) ? rx_ram[2] : {5'b00000, rx_ram[0][7:5]};
assign rx_cov_ram[4] = (rx_ide) ? rx_ram[3] : {rx_ram[0][4:0], rx_ram[1][7:5]};
assign rx_cov_ram[5] = rx_ram[5];
assign rx_cov_ram[6] = rx_ram[6];
assign rx_cov_ram[7] = rx_ram[7];
assign rx_cov_ram[8] = rx_ram[8];
assign rx_cov_ram[9] = rx_ram[9];
assign rx_cov_ram[10] = rx_ram[10];
assign rx_cov_ram[11] = rx_ram[11];
assign rx_cov_ram[12] = rx_ram[12];
//==========================================================================
// 2. TX Logic (Ethernet Format -> MCP2515 Registers)
//==========================================================================
wire tx_ide = tx_ram[0][7];
wire tx_rtr = tx_ram[0][6];
wire [3:0] tx_dlc = tx_ram[0][3:0];
wire [31:0] tx_id = {tx_ram[1], tx_ram[2], tx_ram[3], tx_ram[4]};
assign tx_cov_ram[0] = (tx_ide) ? tx_id[28:21] : tx_id[10:3];
assign tx_cov_ram[1] = (tx_ide) ? {tx_id[20:18], 1'b1, 1'b1, 1'b0, tx_id[17:16]} :
{tx_id[2:0], tx_rtr, 1'b0, 1'b0, 2'b00};
assign tx_cov_ram[2] = (tx_ide) ? tx_id[15:8] : 8'h00;
assign tx_cov_ram[3] = (tx_ide) ? tx_id[7:0] : 8'h00;
assign tx_cov_ram[4] = {1'b0, tx_rtr, 2'b00, tx_dlc};
assign tx_cov_ram[5] = tx_ram[5];
assign tx_cov_ram[6] = tx_ram[6];
assign tx_cov_ram[7] = tx_ram[7];
assign tx_cov_ram[8] = tx_ram[8];
assign tx_cov_ram[9] = tx_ram[9];
assign tx_cov_ram[10] = tx_ram[10];
assign tx_cov_ram[11] = tx_ram[11];
assign tx_cov_ram[12] = tx_ram[12];
//==========================================================================
// Static Assignments
//==========================================================================
assign can_clk = clk;
assign can_rst_n = 1'b1;
//==========================================================================
// SPI Transaction Controller
//==========================================================================
localparam SPI_IDLE = 4'd0;
localparam SPI_CS_LO = 4'd1;
localparam SPI_TX = 4'd2;
localparam SPI_RX = 4'd3;
localparam SPI_CS_HI = 4'd4;
localparam SPI_DONE = 4'd5;
reg spi_start;
reg spi_busy;
reg [7:0] spi_tx_buf [0:15];
reg [7:0] spi_rx_buf [0:15];
reg [3:0] spi_tx_cnt;
reg [3:0] spi_rx_cnt;
reg [3:0] spi_state;
reg [3:0] spi_byte_idx;
reg [2:0] spi_bit_cnt;
reg spi_phase;
reg [7:0] spi_shift_reg;
reg spi_cs_n_reg;
reg spi_sclk_reg;
reg spi_mosi_reg;
reg spi_done;
assign can_cs_n = spi_cs_n_reg;
assign can_sclk = spi_sclk_reg;
assign can_mosi = spi_mosi_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_state <= SPI_IDLE;
spi_cs_n_reg <= 1'b1;
spi_sclk_reg <= 1'b0;
spi_mosi_reg <= 1'b0;
spi_busy <= 1'b0;
spi_done <= 1'b0;
spi_byte_idx <= 4'd0;
spi_bit_cnt <= 3'd7;
spi_phase <= 1'b0;
spi_shift_reg <= 8'h00;
end else begin
spi_done <= 1'b0;
case (spi_state)
SPI_IDLE: begin
spi_cs_n_reg <= 1'b1;
spi_sclk_reg <= 1'b0;
spi_busy <= 1'b0;
if (spi_start) begin
spi_state <= SPI_CS_LO;
spi_busy <= 1'b1;
spi_byte_idx <= 4'd0;
spi_bit_cnt <= 3'd7;
spi_phase <= 1'b0;
spi_shift_reg <= spi_tx_buf[0];
end
end
SPI_CS_LO: begin
spi_cs_n_reg <= 1'b0;
spi_state <= SPI_TX;
end
SPI_TX: begin
if (spi_phase == 1'b0) begin
spi_sclk_reg <= 1'b0;
spi_mosi_reg <= spi_shift_reg[7];
spi_phase <= 1'b1;
end else begin
spi_sclk_reg <= 1'b1;
spi_shift_reg <= {spi_shift_reg[6:0], can_miso};
spi_phase <= 1'b0;
if (spi_bit_cnt == 3'd0) begin
spi_bit_cnt <= 3'd7;
spi_byte_idx <= spi_byte_idx + 1'b1;
if (spi_byte_idx + 1'b1 < spi_tx_cnt) begin
spi_shift_reg <= spi_tx_buf[spi_byte_idx + 1'b1];
end else if (spi_rx_cnt > 4'd0) begin
spi_state <= SPI_RX;
spi_shift_reg <= 8'h00;
spi_byte_idx <= 4'd0;
end else begin
spi_state <= SPI_CS_HI;
end
end else begin
spi_bit_cnt <= spi_bit_cnt - 1'b1;
end
end
end
SPI_RX: begin
if (spi_phase == 1'b0) begin
spi_sclk_reg <= 1'b0;
spi_mosi_reg <= spi_shift_reg[7];
spi_phase <= 1'b1;
end else begin
spi_sclk_reg <= 1'b1;
spi_shift_reg <= {spi_shift_reg[6:0], can_miso};
spi_phase <= 1'b0;
if (spi_bit_cnt == 3'd0) begin
spi_rx_buf[spi_byte_idx] <= {spi_shift_reg[6:0], can_miso};
spi_bit_cnt <= 3'd7;
spi_byte_idx <= spi_byte_idx + 1'b1;
if (spi_byte_idx + 1'b1 < spi_rx_cnt) begin
spi_shift_reg <= 8'h00;
end else begin
spi_state <= SPI_CS_HI;
end
end else begin
spi_bit_cnt <= spi_bit_cnt - 1'b1;
end
end
end
SPI_CS_HI: begin
spi_cs_n_reg <= 1'b1;
spi_sclk_reg <= 1'b0;
spi_state <= SPI_DONE;
end
SPI_DONE: begin
spi_done <= 1'b1;
spi_busy <= 1'b0;
spi_state <= SPI_IDLE;
end
default: begin
spi_state <= SPI_IDLE;
end
endcase
end
end
//==========================================================================
// Main Control State Machine
//==========================================================================
localparam M_IDLE = 8'd0;
localparam M_RST_CMD = 8'd1;
localparam M_RST_WAIT = 8'd2;
localparam M_RST_DELAY = 8'd3;
localparam M_WR_CNF3 = 8'd4;
localparam M_WR_CNF2 = 8'd5;
localparam M_WR_CNF1 = 8'd6;
localparam M_WR_RXB0 = 8'd7;
localparam M_WR_RXB1 = 8'd8;
localparam M_WR_INTE = 8'd9;
localparam M_WR_CTRL = 8'd10;
localparam M_RD_STAT = 8'd11;
localparam M_READY = 8'd12;
localparam M_POLL_INTF = 8'd13;
localparam M_CHK_RX0 = 8'd14;
localparam M_READ_RX0 = 8'd15;
localparam M_STORE_RX0 = 8'd16;
localparam M_CLR_RX0IF = 8'd17;
localparam M_CHK_RX1 = 8'd18;
localparam M_READ_RX1 = 8'd19;
localparam M_STORE_RX1 = 8'd20;
localparam M_CLR_RX1IF = 8'd21;
localparam M_TX_RD_CTRL = 8'd22;
localparam M_TX_CHK = 8'd23;
localparam M_TX_LOAD = 8'd24;
localparam M_TX_RTS = 8'd25;
localparam M_TX_WAIT = 8'd26;
localparam M_ERROR = 8'd27;
reg [7:0] main_state;
reg [7:0] wait_cnt;
reg [15:0] poll_timer;
reg tx_busy_reg;
reg [3:0] tx_byte_idx;
reg [3:0] rx_byte_idx;
reg rx0_pending;
reg rx1_pending;
reg [7:0] can_intf_reg;
reg tx_req_active;
localparam STAT_INIT_DONE = 8'h01;
localparam STAT_TX_BUSY = 8'h02;
localparam STAT_RX_VALID = 8'h04;
localparam STAT_INT_ACTIVE = 8'h08;
localparam STAT_MODE_ERR = 8'h10;
localparam STAT_SPI_ERR = 8'h20;
localparam ERR_INIT_FAIL = 8'h01;
localparam ERR_TX_FAIL = 8'h02;
localparam ERR_RX_OVF = 8'h04;
localparam ERR_BUS_OFF = 8'h08;
localparam FRAME_BYTE_CNT = 4'd13;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
main_state <= M_IDLE;
wait_cnt <= 8'd0;
poll_timer <= 16'd0;
tx_busy_reg <= 1'b0;
tx_byte_idx <= 4'd0;
rx_byte_idx <= 4'd0;
init_done <= 1'b0;
rx_valid <= 1'b0;
rx0_pending <= 1'b0;
rx1_pending <= 1'b0;
can_intf_reg <= 8'd0;
status_reg <= 8'd0;
error_reg <= 8'd0;
tx_req_active <= 1'b0;
rx_ram_wr_en <= 1'b0;
rx_ram_wr_addr <= 4'd0;
rx_ram_wr_data <= 8'd0;
spi_start <= 1'b0;
end else begin
// Default pulse clear
rx_valid <= 1'b0;
rx_ram_wr_en <= 1'b0;
case (main_state)
M_IDLE: begin
main_state <= M_RST_CMD;
status_reg <= 8'd0;
error_reg <= 8'd0;
end
M_RST_CMD: begin
if (!spi_busy) begin
spi_tx_buf[0] <= CMD_RESET;
spi_tx_cnt <= 4'd1;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
main_state <= M_RST_WAIT;
wait_cnt <= 8'd0;
end
end
M_RST_WAIT: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_RST_DELAY;
end
end
M_RST_DELAY: begin
if (wait_cnt < OST_WAIT_CNT) begin
wait_cnt <= wait_cnt + 1'b1;
end else begin
main_state <= M_WR_CNF3;
end
end
M_WR_CNF3: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_CNF2;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_CNF3;
spi_tx_buf[2] <= VAL_CNF3;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_WR_CNF2: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_CNF2 + 1'b1;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_CNF2;
spi_tx_buf[2] <= VAL_CNF2;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_WR_CNF1: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_CNF1 + 1'b1;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_CNF1;
spi_tx_buf[2] <= VAL_CNF1;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_WR_RXB0: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_RXB0 + 1'b1;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_RXB0CTRL;
spi_tx_buf[2] <= VAL_RXB_CTRL;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_WR_RXB1: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_RXB1 + 1'b1;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_RXB1CTRL;
spi_tx_buf[2] <= VAL_RXB_CTRL;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_WR_INTE: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_INTE + 1'b1;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_CANINTE;
spi_tx_buf[2] <= VAL_CANINTE;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_WR_CTRL: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_WR_CTRL + 1'b1;
wait_cnt <= 8'd0;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_WRITE;
spi_tx_buf[1] <= ADDR_CANCTRL;
spi_tx_buf[2] <= VAL_CANCTRL;
spi_tx_cnt <= 4'd3;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_RD_STAT: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
if ((spi_rx_buf[0] & 8'hE0) == 8'h00) begin
main_state <= M_RD_STAT + 1'b1;
poll_timer <= 16'd0;
init_done <= 1'b1;
status_reg <= STAT_INIT_DONE;
end else if (wait_cnt < 8'd10) begin
wait_cnt <= wait_cnt + 1'b1;
end else begin
error_reg <= ERR_INIT_FAIL;
main_state <= M_ERROR;
end
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_READ;
spi_tx_buf[1] <= ADDR_CANSTAT;
spi_tx_cnt <= 4'd2;
spi_rx_cnt <= 4'd1;
spi_start <= 1'b1;
end
end
M_READY: begin
status_reg <= STAT_INIT_DONE;
if (tx_trigger && !tx_busy_reg) begin
tx_req_active <= 1'b1;
tx_busy_reg <= 1'b1;
main_state <= M_TX_RD_CTRL;
end else if (!can_int_n) begin
main_state <= M_POLL_INTF;
end else if (poll_timer >= POLL_INTERVAL - 1'b1) begin
main_state <= M_POLL_INTF;
end else begin
poll_timer <= poll_timer + 1'b1;
end
end
M_POLL_INTF: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
can_intf_reg <= spi_rx_buf[0];
if (spi_rx_buf[0] & 8'h01) begin
rx0_pending <= 1'b1;
end
if (spi_rx_buf[0] & 8'h02) begin
rx1_pending <= 1'b1;
end
main_state <= M_CHK_RX0;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_READ;
spi_tx_buf[1] <= ADDR_CANINTF;
spi_tx_cnt <= 4'd2;
spi_rx_cnt <= 4'd1;
spi_start <= 1'b1;
end
end
M_CHK_RX0: begin
if (rx0_pending) begin
rx0_pending <= 1'b0;
main_state <= M_READ_RX0;
rx_byte_idx <= 4'd0;
end else begin
main_state <= M_CHK_RX1;
end
end
M_READ_RX0: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_STORE_RX0;
rx_byte_idx <= 4'd0;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_READ_RX0;
spi_tx_cnt <= 4'd1;
spi_rx_cnt <= FRAME_BYTE_CNT;
spi_start <= 1'b1;
end
end
M_STORE_RX0: begin
if (rx_byte_idx < FRAME_BYTE_CNT) begin
rx_ram_wr_en <= 1'b1;
rx_ram_wr_addr <= rx_byte_idx;
rx_ram_wr_data <= spi_rx_buf[rx_byte_idx];
rx_byte_idx <= rx_byte_idx + 1'b1;
end else begin
rx_valid <= 1'b1;
status_reg <= STAT_INIT_DONE | STAT_RX_VALID;
main_state <= M_CLR_RX0IF;
end
end
M_CLR_RX0IF: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_CHK_RX1;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_BIT_MODIFY;
spi_tx_buf[1] <= ADDR_CANINTF;
spi_tx_buf[2] <= 8'h01;
spi_tx_buf[3] <= 8'h00;
spi_tx_cnt <= 4'd4;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_CHK_RX1: begin
if (rx1_pending) begin
rx1_pending <= 1'b0;
main_state <= M_READ_RX1;
rx_byte_idx <= 4'd0;
end else if (tx_req_active) begin
main_state <= M_TX_RD_CTRL;
end else begin
poll_timer <= 16'd0;
main_state <= M_READY;
end
end
M_READ_RX1: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_STORE_RX1;
rx_byte_idx <= 4'd0;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_READ_RX1;
spi_tx_cnt <= 4'd1;
spi_rx_cnt <= FRAME_BYTE_CNT;
spi_start <= 1'b1;
end
end
M_STORE_RX1: begin
if (rx_byte_idx < FRAME_BYTE_CNT) begin
rx_ram_wr_en <= 1'b1;
rx_ram_wr_addr <= rx_byte_idx;
rx_ram_wr_data <= spi_rx_buf[rx_byte_idx];
rx_byte_idx <= rx_byte_idx + 1'b1;
end else begin
rx_valid <= 1'b1;
status_reg <= STAT_INIT_DONE | STAT_RX_VALID;
main_state <= M_CLR_RX1IF;
end
end
M_CLR_RX1IF: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
if (tx_req_active) begin
main_state <= M_TX_RD_CTRL;
end else begin
poll_timer <= 16'd0;
main_state <= M_READY;
end
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_BIT_MODIFY;
spi_tx_buf[1] <= ADDR_CANINTF;
spi_tx_buf[2] <= 8'h02;
spi_tx_buf[3] <= 8'h00;
spi_tx_cnt <= 4'd4;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_TX_RD_CTRL: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_TX_CHK;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_READ;
spi_tx_buf[1] <= ADDR_TXB0CTRL;
spi_tx_cnt <= 4'd2;
spi_rx_cnt <= 4'd1;
spi_start <= 1'b1;
end
end
M_TX_CHK: begin
if ((spi_rx_buf[0] & 8'h08) == 8'h00) begin
main_state <= M_TX_LOAD;
tx_byte_idx <= 4'd0;
end else begin
poll_timer <= 16'd0;
main_state <= M_READY;
end
end
M_TX_LOAD: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_TX_RTS;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_LOAD_TX0_ID;
spi_tx_buf[1] <= tx_cov_ram[0];
spi_tx_buf[2] <= tx_cov_ram[1];
spi_tx_buf[3] <= tx_cov_ram[2];
spi_tx_buf[4] <= tx_cov_ram[3];
spi_tx_buf[5] <= tx_cov_ram[4];
spi_tx_buf[6] <= tx_cov_ram[5];
spi_tx_buf[7] <= tx_cov_ram[6];
spi_tx_buf[8] <= tx_cov_ram[7];
spi_tx_buf[9] <= tx_cov_ram[8];
spi_tx_buf[10] <= tx_cov_ram[9];
spi_tx_buf[11] <= tx_cov_ram[10];
spi_tx_buf[12] <= tx_cov_ram[11];
spi_tx_buf[13] <= tx_cov_ram[12];
spi_tx_cnt <= 4'd14;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_TX_RTS: begin
if (spi_busy) begin
spi_start <= 1'b0;
end
if (spi_done) begin
main_state <= M_TX_WAIT;
poll_timer <= 16'd0;
end else if (!spi_busy) begin
spi_tx_buf[0] <= CMD_RTS_TX0;
spi_tx_cnt <= 4'd1;
spi_rx_cnt <= 4'd0;
spi_start <= 1'b1;
end
end
M_TX_WAIT: begin
if (poll_timer >= TRANS_DELAY - 1'b1) begin
tx_req_active <= 1'b0;
tx_busy_reg <= 1'b0;
poll_timer <= 16'd0;
status_reg <= STAT_INIT_DONE;
main_state <= M_READY;
end else begin
poll_timer <= poll_timer + 1'b1;
end
end
M_ERROR: begin
status_reg <= STAT_MODE_ERR;
if (wait_cnt < 8'hFF) begin
wait_cnt <= wait_cnt + 1'b1;
end else begin
main_state <= M_RST_CMD;
end
end
default: begin
main_state <= M_IDLE;
end
endcase
if (rx_done) begin
status_reg <= status_reg & ~STAT_RX_VALID;
end
end
end
// TX busy output
assign tx_busy = tx_busy_reg;
endmodule
本文代码经平台实测通过。如有疑问欢迎留言讨论~ 🚀