Verilog手写SPI主机模块:从架构设计到板级调试的完整指南

发布时间:2026/10/14 2:03:01
Verilog手写SPI主机模块:从架构设计到板级调试的完整指南 1. SPI协议主机模块的设计思路与整体架构1.1 为什么选择Verilog手写SPI主机SPI这东西搞过嵌入式的人都不陌生。它本质上就是一个同步串行通信接口靠四根线打天下SCLK、MOSI、MISO、CS。协议本身不复杂但真到用FPGA去驱动外设的时候很多人第一反应是找个现成的IP核。我一开始也这么干过后来发现两个问题一是通用IP核参数太多配置起来比自己写还费劲二是有些IP核的时序跟特定芯片对不上调试起来反而更麻烦。所以后来我干脆自己用Verilog写了一个SPI主机模块。好处很直接代码量不大逻辑清晰想改哪里改哪里时序完全可控。尤其是对接一些时序要求比较刁钻的ADC或者Flash芯片时自己写的模块调起来心里有底。这个模块适合谁呢如果你正在学FPGA想找一个不太复杂但又能覆盖状态机、时钟分频、移位寄存器这些核心知识点的练手项目SPI主机是个很好的选择。如果你已经工作了需要快速对接某个SPI从设备手头又没有合适的IP核那这篇文章里的代码和思路你可以直接拿去改。1.2 整体架构拆解SPI主机模块的核心组成部分其实就几块时钟分频器把系统时钟分频成SPI的SCLK。SPI的时钟频率取决于从设备的规格常见的有1MHz、10MHz、25MHz等。状态机控制整个传输流程包括片选拉低、数据移位、片选拉高。移位寄存器发送和接收数据都靠它发送时并转串接收时串转并。边沿检测与采样逻辑根据CPOL和CPHA决定在哪个时钟边沿输出数据、哪个边沿采样数据。这四块拼在一起就是一个完整的SPI主机。下面我逐个拆开讲。1.3 参数化设计的考量我在模块定义的时候用了参数化设计主要暴露这几个参数module spi_master #( parameter CLK_DIV 4, // 系统时钟分频系数 parameter CPOL 0, // 时钟极性 parameter CPHA 0, // 时钟相位 parameter DATA_WIDTH 8 // 数据位宽 )( input wire clk, input wire rst_n, input wire start, input wire [DATA_WIDTH-1:0] tx_data, output reg [DATA_WIDTH-1:0] rx_data, output reg done, output reg sclk, output reg mosi, input wire miso, output reg cs_n );为什么要参数化因为不同从设备的要求不一样。比如某款Flash芯片支持SPI模式0和模式3你只需要改CPOL和CPHA两个参数就能切换不用重写代码。数据位宽也是有些ADC是12位的有些是16位的参数化之后复用性大大提升。注意CPOL和CPHA的组合决定了SPI的四种工作模式这个后面会详细讲。参数化的时候一定要确保综合工具支持有些老版本的ISE对参数化支持不太好建议用Vivado或者Quartus。2. SPI四种工作模式与时序细节深度解析2.1 CPOL和CPHA到底怎么理解SPI的四种模式由CPOL和CPHA两个参数组合而成这是很多人一开始容易绕晕的地方。我用最直白的方式解释一下。CPOL是时钟极性决定SCLK空闲时是高电平还是低电平。CPOL0表示空闲时SCLK为低电平CPOL1表示空闲时SCLK为高电平。CPHA是时钟相位决定数据在哪个边沿采样。CPHA0表示在第一个边沿采样也就是SCLK从空闲状态跳变到有效状态的那个边沿CPHA1表示在第二个边沿采样。组合起来就是四种模式模式CPOLCPHA空闲电平采样边沿输出边沿000低上升沿下降沿101低下降沿上升沿210高下降沿上升沿311高上升沿下降沿这张表建议你存下来调试的时候对着看能省不少时间。2.2 时序设计的核心难点SPI主机的时序设计有两个容易出问题的地方。第一个是采样边沿的确定。如果你在输出数据的同一个边沿去采样MISO那大概率会采到不稳定值。正确的做法是在一个边沿输出MOSI在另一个边沿采样MISO。具体哪个边沿输出、哪个边沿采样取决于CPHA的设置。第二个是SCLK的生成方式。我见过不少人直接用系统时钟分频出一个方波给SCLK然后数据在always块里跟着SCLK跳变。这种做法在低速下没问题但速度一上去就容易出亚稳态。更稳妥的方式是用系统时钟驱动一个计数器在计数器的特定值上翻转SCLK同时更新MOSI和采样MISO。这样所有操作都同步于系统时钟时序更可靠。2.3 一个实际案例对接某款12位ADC我之前用这个模块对接过一款12位SPI ADC它的时序要求是模式1CPOL0CPHA1SCLK最高10MHz。系统时钟是50MHz所以分频系数至少是5。我选了CLK_DIV5实际SCLK频率是10MHz。ADC的转换结果是在SCLK的下降沿输出主机需要在上升沿采样。对应到我的代码里就是在计数器前半周期采样MISO后半周期更新MOSI。调试的时候用逻辑分析仪抓了一下波形确认采样点正好在数据稳定的中间位置一次就通了。实操心得调试SPI的时候逻辑分析仪比示波器好用得多。示波器只能看一两路信号逻辑分析仪可以同时抓SCLK、MOSI、MISO、CS四路一眼就能看出时序对不对。3. Verilog代码实现与关键模块详解3.1 时钟分频与SCLK生成时钟分频这块我用了一个简单的计数器reg [15:0] clk_cnt; reg sclk_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 16d0; sclk_r CPOL[0]; end else if (busy) begin if (clk_cnt CLK_DIV - 1) begin clk_cnt 16d0; sclk_r ~sclk_r; end else begin clk_cnt clk_cnt 1b1; end end else begin clk_cnt 16d0; sclk_r CPOL[0]; end end这段代码的逻辑是当传输忙的时候计数器每数到CLK_DIV-1就翻转一次SCLK同时计数器归零。这样SCLK的周期就是2*CLK_DIV个系统时钟周期。不忙的时候SCLK保持空闲电平。为什么要用busy信号控制因为SPI传输不是一直进行的只有在start信号触发后才开始。不传输的时候SCLK应该保持在CPOL定义的空闲电平不能一直翻转。3.2 状态机设计状态机我用的是经典的三段式写法状态定义很简单localparam IDLE 2d0; localparam TRANS 2d1; localparam DONE 2d2; reg [1:0] state, next_state; reg [4:0] bit_cnt; reg [DATA_WIDTH-1:0] tx_shift; reg [DATA_WIDTH-1:0] rx_shift;IDLE状态等待start信号TRANS状态进行数据移位DONE状态输出done信号并拉高CS。状态转移逻辑always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end always (*) begin next_state state; case (state) IDLE: if (start) next_state TRANS; TRANS: if (bit_cnt DATA_WIDTH) next_state DONE; DONE: next_state IDLE; default: next_state IDLE; endcase end这里bit_cnt用来计数已经传输了多少位当计数达到DATA_WIDTH时说明一个完整的数据帧传输完毕跳转到DONE状态。3.3 移位寄存与数据采样移位寄存器是SPI主机的核心。发送时每个SCLK周期把tx_shift的最高位输出到MOSI然后左移一位。接收时在每个采样边沿把MISO的值移入rx_shift的最低位。always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_shift {DATA_WIDTH{1b0}}; rx_shift {DATA_WIDTH{1b0}}; mosi 1b0; bit_cnt 5d0; end else if (state IDLE start) begin tx_shift tx_data; rx_shift {DATA_WIDTH{1b0}}; bit_cnt 5d0; end else if (state TRANS) begin if (clk_cnt CLK_DIV - 1) begin // SCLK翻转点 if (sclk_r CPOL[0]) begin // 第一个边沿输出数据 mosi tx_shift[DATA_WIDTH-1]; tx_shift {tx_shift[DATA_WIDTH-2:0], 1b0}; end else begin // 第二个边沿采样数据 rx_shift {rx_shift[DATA_WIDTH-2:0], miso}; bit_cnt bit_cnt 1b1; end end end end这段代码的关键在于根据sclk_r的当前值判断是第一个边沿还是第二个边沿。当sclk_r等于CPOL时说明SCLK即将从空闲电平跳变到有效电平这是第一个边沿。当sclk_r不等于CPOL时说明SCLK即将从有效电平回到空闲电平这是第二个边沿。注意上面的代码是CPHA0的情况。如果是CPHA1输出和采样的边沿需要对调。实际写代码的时候可以用一个参数来控制避免写两套逻辑。3.4 片选与完成信号片选信号CS在传输期间拉低传输结束后拉高。done信号在DONE状态拉高一个时钟周期通知上层模块数据已经准备好。always (posedge clk or negedge rst_n) begin if (!rst_n) begin cs_n 1b1; done 1b0; end else begin case (state) IDLE: begin cs_n 1b1; done 1b0; end TRANS: begin cs_n 1b0; done 1b0; end DONE: begin cs_n 1b1; done 1b1; end endcase end endCS的拉低和拉高时机很重要。有些从设备要求CS在第一个SCLK边沿之前就要拉低并且要保持到最后一个SCLK边沿之后。我的做法是在进入TRANS状态的同一个时钟周期拉低CS在进入DONE状态时拉高CS。这样CS的有效窗口比SCLK的有效窗口略宽满足大多数从设备的要求。4. 仿真验证与板级调试实操4.1 Testbench搭建要点仿真验证是FPGA开发中不可缺少的一步。SPI主机的Testbench需要模拟一个SPI从设备的行为包括接收MOSI数据、返回MISO数据。module tb_spi_master; reg clk; reg rst_n; reg start; reg [7:0] tx_data; wire [7:0] rx_data; wire done; wire sclk; wire mosi; reg miso; wire cs_n; spi_master #( .CLK_DIV(4), .CPOL(0), .CPHA(0), .DATA_WIDTH(8) ) uut ( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .rx_data(rx_data), .done(done), .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n) ); // 50MHz系统时钟 initial clk 0; always #10 clk ~clk; // 模拟从设备返回数据 reg [7:0] slave_data 8hA5; reg [2:0] slave_bit_cnt 0; always (posedge sclk or negedge cs_n) begin if (!cs_n) begin slave_bit_cnt 0; end else begin miso slave_data[7 - slave_bit_cnt]; slave_bit_cnt slave_bit_cnt 1; end end initial begin rst_n 0; start 0; tx_data 8h5A; miso 0; #100; rst_n 1; #100; start 1; #20; start 0; wait(done); #100; $display(TX: %h, RX: %h, tx_data, rx_data); $finish; end endmodule这个Testbench模拟了一个简单的从设备在SCLK的上升沿输出数据。仿真跑起来之后你可以看到rx_data应该等于slave_data也就是8hA5。4.2 逻辑分析仪抓波形技巧仿真通过之后下一步就是上板调试。上板调试最重要的是抓波形。我用的是某款逻辑分析仪采样率100MHz足够抓10MHz的SPI信号。抓波形的时候注意几点触发条件设在CS的下降沿这样可以抓到完整的传输过程。采样深度要足够至少能覆盖一个完整的数据帧。如果SCLK频率较高逻辑分析仪的采样率至少要是SCLK频率的4倍以上。抓到波形之后重点看三个地方一是CS拉低到第一个SCLK边沿的间隔是否满足从设备要求二是MOSI数据在SCLK采样边沿是否稳定三是MISO数据是否在正确的边沿被采样。4.3 常见问题排查表现象可能原因排查方法从设备无响应CS未拉低或时序不对用逻辑分析仪看CS信号读回数据全为0MISO采样边沿错误检查CPHA设置调整采样边沿读回数据全为1MISO未连接或从设备未驱动检查硬件连接数据偶尔出错时序余量不足降低SCLK频率或增加采样延迟第一个bit错误CS拉低与第一个边沿间隔太短增加CS拉低后的等待时间实操心得我遇到过一个问题从设备在CS拉低后需要至少100ns的准备时间才能接收第一个SCLK边沿。当时我的代码是CS拉低后立刻开始SCLK翻转结果第一个bit总是错。后来在状态机里加了一个等待状态CS拉低后等几个系统时钟周期再开始传输问题就解决了。这个等待时间可以根据从设备的数据手册来定一般留足余量就行。5. 模块优化与扩展思路5.1 支持多从设备的片选扩展实际项目中经常需要用一个SPI主机对接多个从设备。这时候只需要扩展片选信号的数量每个从设备分配一个CS。MOSI、MISO、SCLK三根线可以共用也可以独立。output reg [3:0] cs_n_vec // 4个片选信号在状态机里增加一个cs_index输入根据cs_index选择拉低哪个CS。这样一套SPI主机逻辑可以驱动多个从设备节省逻辑资源。5.2 加入FIFO缓冲提升吞吐率如果SPI主机需要连续传输大量数据可以在发送和接收路径上各加一个FIFO。发送FIFO缓存待发送的数据接收FIFO缓存已接收的数据。这样CPU或者上层模块不需要等待每次传输完成可以批量写入和读取提升整体吞吐率。FIFO的深度根据实际数据量来定一般16到64深度就够了。我用的是Xilinx的FIFO IP核配置成独立时钟域读写时钟都接系统时钟简单省事。5.3 时序约束与跨时钟域处理如果SPI主机的系统时钟和上层模块的时钟不是同一个时钟域就需要做跨时钟域处理。start信号、tx_data、rx_data、done这些信号都需要同步。简单的做法是用两级触发器做同步reg start_sync1, start_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin start_sync1 1b0; start_sync2 1b0; end else begin start_sync1 start; start_sync2 start_sync1; end end对于多bit的tx_data和rx_data建议用握手信号或者异步FIFO来处理不要直接用两级触发器同步否则可能出现数据错位。时序约束方面SCLK输出需要设置输出延迟约束MISO输入需要设置输入延迟约束。具体数值根据从设备的数据手册和PCB走线来定。如果SCLK频率不高比如10MHz以下时序约束可以适当放宽但最好还是加上养成好习惯。5.4 从主机到从机的角色切换这个模块目前是主机模式。如果需要从机模式核心逻辑其实差不多主要区别在于SCLK变成输入CS变成输入数据移位的方向和控制逻辑需要调整。从机模式下SCLK由外部主机提供模块需要检测SCLK的边沿来移位数据。从机模式的一个难点是SCLK的频率可能很高而系统时钟频率有限。如果SCLK频率接近系统时钟频率采样可能会出问题。这时候需要用SCLK的边沿作为时钟来驱动移位寄存器但这样又会引入跨时钟域的问题。我的建议是尽量让系统时钟频率远高于SCLK频率至少4倍以上这样用系统时钟采样SCLK边沿才可靠。6. 写在最后的一些实操体会这个SPI主机模块我从第一次写到现在前后改了好几版。最开始那版只支持模式0后来慢慢扩展到四种模式都支持。代码从最初的100多行涨到了现在的300多行但逻辑反而更清晰了。有几个坑我踩过这里分享一下。第一个是SCLK的初始电平。如果你的CPOL1但SCLK在复位后是低电平那从设备可能会误判一个时钟边沿。所以复位后SCLK一定要设置成CPOL对应的空闲电平。第二个是bit_cnt的位宽。如果你支持的最大数据位宽是32位那bit_cnt至少需要6位别用5位否则计数会溢出。第三个是done信号的脉冲宽度。有些上层模块对done信号的宽度有要求如果太窄可能采不到。我的做法是让done保持一个完整的系统时钟周期这样基本不会出问题。另外如果你用的是Xilinx的FPGA建议把SCLK和MOSI这些输出信号放到ODDR原语里这样输出延迟更可控时序更容易收敛。具体做法可以参考Xilinx的SelectIO文档这里就不展开了。这个模块后续还可以继续扩展比如加入DMA接口、支持QSPI模式、增加CRC校验等。但核心的SPI主机逻辑就是这些把基础打牢了扩展起来就是搭积木的事。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询