第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 实现
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
测试台
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
📌 扩展阅读
本课的核心概念在实际工程中有广泛应用:
- 在芯片设计中,模块化设计方法至关重要——每个 IP 核都有清晰的接口定义
- 仿真验证是确保设计正确性的关键步骤,Verilator 是工业界广泛使用的开源工具
- 时序约束和综合优化直接影响芯片的性能和功耗表现
- 形式验证和断言检查可以显著提高设计可靠性,减少流片后的 bug
🔧 调试技巧
在开发本课模块时,常见问题和解决方法:
- 仿真不收敛:检查组合逻辑是否形成了环路(combinational loop)
- 时序违例:添加流水线寄存器切断关键路径,或降低工作频率
- 资源超标:使用资源共享、时分复用等技术减少硬件开销
- 功能不正确:先写测试台(testbench),用断言检查关键信号
📊 性能指标
衡量本课模块性能的关键指标:
| 指标 | 含义 | 目标 |
| 延迟(Latency) | 从输入到输出的周期数 | 尽可能小 |
| 吞吐量(Throughput) | 每周期处理的数据量 | 尽可能大 |
| 面积(Area) | 占用的 LUT/FF 资源 | 在性能满足下最小化 |
| 功耗(Power) | 动态 + 静态功耗 | 在性能满足下最小化 |
🔗 与其他课程的联系
本课内容在整个 RISC-V 数字电路课程中的位置:
- 第01-05课的组合逻辑是所有硬件模块的基础
- 第06-10课的时序逻辑提供了寄存器和状态机的设计方法
- 第11-20课的CPU设计是 SoC 的核心处理单元
- 第21-23课的存储层次为数据访问提供速度和容量平衡
- 第24-28课的异常与中断使系统能响应外部事件
- 第29-33课的外设接口连接 CPU 与外部世界
- 第34-37课的系统集成将所有模块整合为完整 SoC
- 第38-40课的验证与毕业确保设计正确可靠
📐 设计方法论
优秀的数字设计遵循以下方法论:
- 规格定义:在写代码前,明确定义模块的接口、功能和行为
- 测试驱动开发:先写测试台,定义预期行为,再实现功能
- 渐进式开发:从简单版本开始,逐步添加功能
- 代码审查:让他人审查代码,发现逻辑错误和风格问题
- 回归测试:每次修改后运行全部测试,确保不引入新 bug
💡 工程实践经验
在实际芯片项目中积累的宝贵经验:
- 仿真通过 ≠ 硅片正确——需要考虑 PVT(工艺/电压/温度)变化
- 综合后的门级仿真可以发现 RTL 仿真遗漏的问题
- FPGA 原型验证是流片前最接近真实环境的测试手段
- 文档和注释与代码同等重要——未来的你会感谢现在的你
- 版本控制(Git)是团队协作的基础——每次提交都要有意义
三、SPI 四种模式详解
SPI 有四种工作模式,由 CPOL(时钟极性)和 CPHA(时钟相位)组合决定:
| 模式 | CPOL | CPHA | 空闲SCLK | 采样边沿 | 切换边沿 | 典型应用 |
| Mode 0 | 0 | 0 | 低 | 上升沿 | 下降沿 | Flash、ADC、传感器 |
| Mode 1 | 0 | 1 | 低 | 下降沿 | 上升沿 | 部分DAC |
| Mode 2 | 1 | 0 | 高 | 下降沿 | 上升沿 | 部分LCD |
| Mode 3 | 1 | 1 | 高 | 上升沿 | 下降沿 | SD卡、部分传感器 |
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;
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从设备,接收主设备数据并回传:
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];
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};
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. 修改参数后,系统的行为会有什么变化?