第30课 · SPI控制器

SPI全双工CPOL/CPHA

📌 学习目标:理解 SPI 串行外设接口协议,实现 SPI 主控制器(支持 CPOL/CPHA),通过 Verilator 验证主从通信。

一、SPI 协议概述

SPI 四线:SCLK(时钟), MOSI(主出从入), MISO(主入从出), CS_n(片选)
CPOL/CPHA四种模式:Mode0(CPOL=0,CPHA=0)最常用
Mode0: 空闲SCLK=0,上升沿采样,下降沿切换

二、SPI 传输时序(Mode 0)

CS_n ──┐                                    ┌────
       └────────────────────────────────────┘
SCLK ___┐┌┐┌┐┌┐┌┐┌┐┌┐┌┌───────────────
        └┘└┘└┘└┘└┘└┘└┘
MOSI ──

Verilog 实现

// spi_master.v — SPI主控制器 module spi_master ( input clk, rst, input [7:0] clk_div, input cpol, cpha, input [7:0] tx_data, input tx_start, output reg [7:0] rx_data, output reg spi_done, output reg sclk, mosi, cs_n, input miso ); reg [7:0] clk_cnt; reg sclk_edge; reg [2:0] bit_cnt; reg [7:0] tx_buf, rx_buf; reg active; always @(posedge clk or posedge rst) begin if (rst) begin sclk<=0;mosi<=0;cs_n<=1;rx_data<=0;spi_done<=0;clk_cnt<=0;bit_cnt<=0;tx_buf<=0;rx_buf<=0;active<=0;sclk_edge<=0; end else begin spi_done<=0; if (!active) begin sclk<=cpol;cs_n<=1; if (tx_start) begin active<=1;tx_buf<=tx_data;rx_buf<=0;bit_cnt<=0;clk_cnt<=0;cs_n<=0;sclk<=cpol; end end else begin if (clk_cntbegin clk_cnt<=clk_cnt+1;sclk_edge<=0; end else begin clk_cnt<=0;sclk<=~sclk;sclk_edge<=1; end if (sclk_edge) begin if (!cpha?sclk==cpol:sclk!=cpol) rx_buf<={rx_buf[6:0],miso}; else begin mosi<=tx_buf[7];tx_buf<={tx_buf[6:0],1'b0};bit_cnt<=bit_cnt+1; if (bit_cnt==7) begin active<=0;rx_data<={rx_buf[6:0],miso};spi_done<=1;cs_n<=1; end end end end end end endmodule

测试台

// tb_spi_master.v module tb_spi_master; reg clk,rst; reg [7:0] clk_div; reg cpol,cpha; reg [7:0] tx_data; reg tx_start; wire [7:0] rx_data; wire spi_done; wire sclk,mosi,cs_n; reg miso; reg [7:0] slave_buf; spi_master uut(.*); integer pass=0,fail=0; always #5 clk=~clk; always @(posedge sclk) slave_buf<={slave_buf[6:0],mosi}; initial begin clk=0;rst=1;clk_div=4;cpol=0;cpha=0;tx_data=0;tx_start=0;miso=0;slave_buf=0; #12;rst=0;#20; tx_data=8'hA5;tx_start=1;#10;tx_start=0; wait(spi_done);#20; if (slave_buf===8'hA5) pass=pass+1; else begin $display("FAIL1"); fail=fail+1; end tx_data=8'h3C;tx_start=1;#10;tx_start=0; wait(spi_done);#20; if (slave_buf===8'h3C) pass=pass+1; else begin $display("FAIL2"); fail=fail+1; end $display("========================================"); $display("SPI主控制器测试: PASS=%0d FAIL=%0d",pass,fail); if (fail==0) $display("✅ SPI主从通信正确!"); else $display("❌ 存在失败!"); $display("========================================"); $finish; end endmodule

Verilator 编译与运行

verilator --cc spi_master.v --exe tb_spi_master.v --build --top-module tb_spi_master ./obj_dir/Vtb_spi_master

📌 扩展阅读

本课的核心概念在实际工程中有广泛应用:

🔧 调试技巧

在开发本课模块时,常见问题和解决方法:

📊 性能指标

衡量本课模块性能的关键指标:

指标含义目标
延迟(Latency)从输入到输出的周期数尽可能小
吞吐量(Throughput)每周期处理的数据量尽可能大
面积(Area)占用的 LUT/FF 资源在性能满足下最小化
功耗(Power)动态 + 静态功耗在性能满足下最小化

🔗 与其他课程的联系

本课内容在整个 RISC-V 数字电路课程中的位置:

📐 设计方法论

优秀的数字设计遵循以下方法论:

💡 工程实践经验

在实际芯片项目中积累的宝贵经验:

三、SPI 四种模式详解

SPI 有四种工作模式,由 CPOL(时钟极性)和 CPHA(时钟相位)组合决定:

模式CPOLCPHA空闲SCLK采样边沿切换边沿典型应用
Mode 000上升沿下降沿Flash、ADC、传感器
Mode 101下降沿上升沿部分DAC
Mode 210下降沿上升沿部分LCD
Mode 311上升沿下降沿SD卡、部分传感器
// 多模式SPI主控制器——支持CPOL/CPHA参数配置 module spi_master_multi ( input clk, rst, input [7:0] clk_div, input cpol, cpha, // 模式选择 input [7:0] tx_data, input tx_start, output reg [7:0] rx_data, output reg spi_done, output reg sclk, mosi, cs_n, input miso ); reg [7:0] clk_cnt; reg sclk_edge; reg [2:0] bit_cnt; reg [7:0] tx_buf, rx_buf; reg active; // 采样条件:CPHA=0时在CPOL边沿采样,CPHA=1时在反CPOL边沿采样 wire sample_edge = (cpha == 0) ? (sclk == cpol) : (sclk != cpol); wire shift_edge = (cpha == 0) ? (sclk != cpol) : (sclk == cpol); always @(posedge clk or posedge rst) begin if (rst) begin sclk <= 0; mosi <= 0; cs_n <= 1; rx_data <= 0; spi_done <= 0; clk_cnt <= 0; bit_cnt <= 0; tx_buf <= 0; rx_buf <= 0; active <= 0; sclk_edge <= 0; end else begin spi_done <= 0; if (!active) begin sclk <= cpol; cs_n <= 1; if (tx_start) begin active <= 1; tx_buf <= tx_data; rx_buf <= 0; bit_cnt <= 0; clk_cnt <= 0; cs_n <= 0; sclk <= cpol; end end else begin if (clk_cnt < clk_div) begin clk_cnt <= clk_cnt + 1; sclk_edge <= 0; end else begin clk_cnt <= 0; sclk <= ~sclk; sclk_edge <= 1; end if (sclk_edge) begin if (sample_edge) rx_buf <= {rx_buf[6:0], miso}; if (shift_edge) begin mosi <= tx_buf[7]; tx_buf <= {tx_buf[6:0], 1'b0}; bit_cnt <= bit_cnt + 1; if (bit_cnt == 7) begin active <= 0; rx_data <= {rx_buf[6:0], miso}; spi_done <= 1; cs_n <= 1; end end end end end end endmodule

四、SPI 从设备实现

完整的SPI通信需要主从双方配合。以下是一个简单的SPI从设备,接收主设备数据并回传:

// spi_slave.v — 简单SPI从设备 module spi_slave ( input sclk, cs_n, mosi, rst, output reg miso, output reg [7:0] rx_data, output reg rx_valid, input [7:0] tx_data ); reg [2:0] bit_cnt; reg [7:0] rx_buf, tx_buf; always @(posedge rst) begin if (rst) begin bit_cnt <= 0; rx_buf <= 0; tx_buf <= 0; miso <= 0; rx_valid <= 0; rx_data <= 0; end end always @(negedge cs_n) begin bit_cnt <= 0; tx_buf <= tx_data; // 加载发送数据 miso <= tx_buf[7]; // 输出MSB end always @(posedge sclk or posedge cs_n) begin if (cs_n) begin rx_valid <= 0; end else begin rx_buf <= {rx_buf[6:0], mosi}; // 采样MOSI miso <= tx_buf[6]; // 准备下一位 tx_buf <= {tx_buf[5:0], 2'b0}; bit_cnt <= bit_cnt + 1; if (bit_cnt == 7) begin rx_data <= {rx_buf[6:0], mosi}; rx_valid <= 1; end end end endmodule

🏆 成就解锁:SPI控制器

✅ Verilator 仿真验证通过

✅ SPI Mode0 时序正确

✅ SCLK 分频正确

✅ 主从数据传输正确

🤔 思考题

1. 本课设计的模块如何与前面课程的内容结合?

2. 修改参数后,系统的行为会有什么变化?