FPGA SPI接口设计:手写状态机与VHDL实现全解析
简介基于FPGA的SPI通信接口设计资料包面向FPGA开发学习者和相关专业学生解决从SPI协议原理到FPGA工程实现之间的跨度问题。压缩包共428个文件、6.42MB核心包括.vhd源码、.bdf原理图、.vwf仿真波形、.pdf/docx说明文档以及Quartus工程配置覆盖时钟生成、状态机、移位寄存器、片选逻辑和四种SPI工作模式等关键设计点。已有299人浏览学习目录按代码、工程、文档、仿真划分便于对照阅读和二次开发。借助仿真波形可直观观察MISO/MOSI收发与状态跳转过程说明文档详述配置选项和常见问题整体可作为FPGA接入传感器、存储器、显示模块等外设时的参考模板也可作为理解SPI时序和接口复用的实践起点。1. 为什么FPGA上的SPI接口要自己写状态机调试一块不带SPI硬核的FPGA时最常见的需求就是让逻辑去访问板载Flash、ADC或OLED屏。虽然厂商IP核能用但通信速率在几MHz以内的外设自己写一个SPI主控状态机并不复杂反而能精确控制片选时序和时钟极性。这套资源正好是一份完整的SPI通信接口设计实例包含VHDL源码、原理图、说明文档和仿真文件适合课程设计、竞赛以及实际项目里快速搭建通信链路。手写SPI控制器最大的收益在于调整空间低速场景不用加FIFO先把收发逻辑验证对后续要提速再改分频系数。下面从协议参数讲起逐层拆解状态机、仿真和工程集成。2. SPI协议与FPGA时序映射模式、分频、片选SPI协议本身不复杂真正的陷阱全在时序参数上。时钟极性CPOL决定SCK空闲时的电平高低时钟相位CPHA决定数据在哪个边沿被采样。这两个参数组合出四种工作模式FPGA实现时只需要把模式映射成“空闲电平”和“采样沿”两个逻辑信号后续状态机就可以统一处理。2.1 CPOL/CPHA与四种模式从机手册里通常会写“数据在SCK上升沿移出”“在下降沿采样”翻译成FPGA逻辑时要先分清变化沿和采样沿。下面是四种模式的完整对照工程上绝大多数Flash和ADC默认模式0先按模式0调通再根据具体芯片数据手册切换模式。模式CPOLCPHASCK空闲电平数据采样沿数据变化沿000低上升沿下降沿101低下降沿上升沿210高下降沿上升沿311高上升沿下降沿提示数据手册描述“SCK上升沿数据移出”指的是变化沿不是采样沿。比如模式0下数据在下降沿变化上升沿采样两边刚好错开半个周期这样接收端采样时数据已经稳定。逻辑实现时我不建议为四种模式写四套代码。把CPOL和CPHA作为顶层参数传入空闲电平直接决定不传输时SCK的状态采样沿则决定移位寄存器用上升沿还是下降沿触发。这样换一颗从机芯片时只改顶层参数而不用动状态机主逻辑。2.2 时钟分频与位计数SCK由系统时钟分频得到常见做法是计数器到半周期翻转。假设系统时钟50MHz、目标SCK 1MHz分频系数DIV_RATIO fsys / (2 * fSCK) 25也就是计数器从0数到24每25个系统时钟翻转一次SCK。下面是简化版分频逻辑process(clk, reset_n) begin if reset_n 0 then div_cnt 0; sclk_i 0; elsif rising_edge(clk) then if state IDLE then sclk_i CPOL; -- 空闲电平按CPOL设定 elsif div_cnt DIV_RATIO - 1 then sclk_i not sclk_i; div_cnt 0; else div_cnt div_cnt 1; end if; end if; end process;这里DIV_RATIO是generic参数改SCK频率只需要改这个值。要注意的是SCK占空比并不是严格的50%因为状态机里SCK的启动和停止会受SETUP状态的控制但只要从机的时序裕量足够这个误差完全可以接受。低速SPI场景没必要动用PLL或者MMCM纯逻辑分频反而更容易约束和调试只有当目标SCK超过25MHz时才需要考虑用PLL输出干净时钟并增加set_output_delay约束。位计数器和分频器配合每完整传输一个SCK周期后bit_cnt加1计数到WORD_LEN-1时说明一个字节传输完成。这样整个SPI传输过程可以看作“一个状态机控制CS一个计数器控制SCK一个计数器控制字节位”三条线各自独立调试时定位问题会快很多。2.3 片选、全双工与电平匹配SPI是全双工接口MOSI和MISO在同一个时钟周期内同时收发数据。FPGA侧发送的数据在变化沿更新接收数据在采样沿打入移位寄存器。CS信号低有效FPGA实现时的经验是CS拉低后至少等待半个SCK周期再产生第一个时钟边沿最后一个数据位采样完成后再拉高CS避免从机在CS刚拉低时还没有准备好而导致首字节错位。电平匹配是最容易被忽略的环节。FPGA引脚如果是3.3V LVCMOS而外设工作在1.8V或者5V直接连接轻则通信不稳定重则损坏引脚。常见做法是与5V器件串接电阻分压或使用电平转换芯片与1.8V器件通信时在FPGA侧把MISO方向做双向电平匹配。原理图设计部分还会具体展开。3. 用VHDL实现SPI主控端口、状态机、移位寄存器这一节给出一个可直接抄作业的8位SPI主控制器采用VHDL编写结构分成顶层实体、状态机、移位寄存器三个部分。代码风格对应Xilinx和Intel原Altera两家的综合工具都能识别。3.1 顶层实体与参数定义模块以generic方式暴露关键参数这样复制到其他工程时不需要修改内部逻辑只需要改端口连接和参数值。entity spi_master is generic ( WORD_LEN : integer : 8; -- 数据位宽常用8/16/24 DIV_RATIO : integer : 25; -- 50MHz时钟分频到1MHz SCK CPOL : std_logic : 0; -- 时钟极性 CPHA : std_logic : 0; -- 时钟相位 CS_SETUP : integer : 12 -- CS拉低到首个时钟沿的等待周期数 ); port ( clk : in std_logic; -- 系统时钟 reset_n : in std_logic; -- 低有效复位 start : in std_logic; -- 启动一次传输 busy : out std_logic; -- 忙标志 din : in std_logic_vector(WORD_LEN-1 downto 0); -- 待发送数据 dout : out std_logic_vector(WORD_LEN-1 downto 0); -- 接收数据 sclk : out std_logic; -- SPI时钟 mosi : out std_logic; -- 主出从入 miso : in std_logic; -- 主入从出 cs_n : out std_logic -- 片选低有效 ); end entity;复位信号用低有效异步复位这是大部分FPGA开发板的默认配置。DIV_RATIO和CS_SETUP分开设置原因是有些从机对CS到SCK的建立时间要求比较长单独给一个参数可以灵活调整而不影响SCK频率。busy信号在传输期间保持高电平外部控制器比如一个微处理器软核或另一段状态机可以通过轮询或者中断读取传输状态。3.2 三段式状态机核心状态机分为IDLE、SETUP、TRANSFER、STOP四个状态。IDLE等待start信号SETUP负责CS拉低并等待建立时间TRANSFER完成位数据的收发STOP负责收尾并将CS拉高。type state_t is (IDLE, SETUP, TRANSFER, STOP); signal state : state_t; signal div_cnt : integer range 0 to DIV_RATIO - 1; signal bit_cnt : integer range 0 to WORD_LEN - 1; signal sclk_i : std_logic; signal cs_n_i : std_logic; signal tx_reg : std_logic_vector(WORD_LEN-1 downto 0); signal rx_reg : std_logic_vector(WORD_LEN-1 downto 0); begin sclk sclk_i when state / IDLE else CPOL; cs_n cs_n_i; process(clk, reset_n) begin if reset_n 0 then state IDLE; cs_n_i 1; busy 0; sclk_i CPOL; div_cnt 0; bit_cnt 0; elsif rising_edge(clk) then case state is when IDLE if start 1 then tx_reg din; busy 1; state SETUP; div_cnt 0; end if; when SETUP cs_n_i 0; if div_cnt CS_SETUP - 1 then div_cnt 0; sclk_i CPOL; state TRANSFER; else div_cnt div_cnt 1; end if; when TRANSFER if div_cnt DIV_RATIO - 1 then div_cnt 0; if bit_cnt WORD_LEN - 1 then state STOP; else bit_cnt bit_cnt 1; end if; else div_cnt div_cnt 1; end if; when STOP cs_n_i 1; busy 0; dout rx_reg; state IDLE; end case; end if; end process; end architecture;这段代码里SETUP状态把CS拉低后等待CS_SETUP个时钟周期保证了从机片选建立时间。TRANSFER状态里SCK翻转逻辑由外部进程处理状态机本身只负责周期计数和位计数这种解耦方式让调试比较直观。STOP状态把接收到的数据锁存到dout外部逻辑在下一次start拉起之前读取即可。3.3 数据移位与采样时刻SCK生成和移位寄存器需要独立进程原因很简单SCK翻转发生在系统时钟边沿而采样要求数据在SCK边沿附近稳定如果和数据移位放同一个进程容易产生竞争。模式0下数据变化沿是SCK下降沿采样沿是SCK上升沿对应逻辑如下process(sclk_i) begin if falling_edge(sclk_i) then mosi tx_reg(WORD_LEN-1); tx_reg tx_reg(WORD_LEN-2 downto 0) 0; end if; end process; process(sclk_i) begin if rising_edge(sclk_i) then rx_reg rx_reg(WORD_LEN-2 downto 0) miso; end if; end process;第一个进程在SCK下降沿把发送寄存器最高位移到MOSI上第二个进程在上升沿把MISO采样进接收寄存器。两个进程一个触发沿不同从时序上天然错开半个周期。注意如果切换模式2或模式3只需要把两个进程的上升沿和下降沿互换再把SCK空闲电平改成CPOL值即可状态机的流转完全不用动。3.4 参数调整与常见误用DIV_RATIO的计算方式在2.2节讲过这里补充一个容易忽略的点CS_SETUP和DIV_RATIO都是基于系统时钟周期的计数值如果系统时钟从50MHz改成100MHz同样SCK频率下的DIV_RATIO要翻倍CS_SETUP也需要等比放大否则CS建立时间会减半。常见误用是把DIV_RATIO直接照抄到新工程导致SCK变成2MHz而Flash的最大SCK上限恰好被超过。另一个常见误用是把发送和接收理解成两步操作。SPI是全双工协议一次传输过程中MOSI发出一个字节的同时MISO已经接收到一个字节不存在“先发后收”的模式。很多初学者会在发送完成后额外等待一段时间再去读dout这没有任何问题但在状态机设计上不要把接收逻辑放到STOP状态之后否则会丢最后一位数据。STOP状态直接把rx_reg锁存到dout就是最稳妥的写法。4. 仿真验证testbench、工具命令与波形判读设计代码写完之后第一件事不是上板而是跑行为级仿真。仿真能验证状态机跳转是否正常、数据位是否对齐、CS时序是否满足要求。下面用一个自环回testbench验证发送接收逻辑也就是把MOSI和MISO在测试平台内部短接。4.1 自环回testbench设计自环回是最快的验证方式发送0xA5如果接收数据也是0xA5说明移位寄存器和采样逻辑基本正确。测试平台中把DUT的mosi信号直接连接到miso端口再分别比对每次传输的结果。architecture sim of spi_master_tb is signal clk : std_logic : 0; signal reset_n : std_logic : 0; signal start : std_logic : 0; signal din : std_logic_vector(7 downto 0) : (others 0); signal dout : std_logic_vector(7 downto 0); signal sclk : std_logic; signal mosi : std_logic; signal miso : std_logic; signal cs_n : std_logic; signal busy : std_logic; begin clk not clk after 10 ns; -- 50MHz时钟 uut : entity work.spi_master generic map (WORD_LEN 8, DIV_RATIO 4, CS_SETUP 8) port map (clk, reset_n, start, busy, din, dout, sclk, mosi, miso, cs_n); miso mosi; -- 自环回 process begin reset_n 0; wait for 100 ns; reset_n 1; wait for 50 ns; din xA5; start 1; wait until busy 1; start 0; wait until busy 0; assert dout xA5 report loopback mismatch! severity error; wait; end process; end architecture;DIV_RATIO设成4是为了缩短仿真时间实际使用中该值不影响逻辑正确性。testbench里用wait until busy1来捕捉start信号的响应再用wait until busy0等待传输完成。assert语句会在dout不等于预期值时报告错误这是仿真验证里最直接的判断手段。4.2 ModelSim与Vivado的仿真命令不管是ModelSim还是Vivado自带的XSim底层流程都是编译、例化、运行、查看波形。ModelSim的命令行方式如下vlib work vcom -2008 spi_master.vhd spi_master_tb.vhd vsim work.spi_master_tb add wave -position end sim:/spi_master_tb/* run 20 usvlib创建仿真库vcom把设计文件和测试平台编译进work库vsim启动仿真add wave把spi_master_tb下的所有信号加到波形窗口run指定运行时间。Vivado环境下操作路径是Flow Navigator - SIMULATION - Run Behavioral Simulation本质上也是编译testbench后运行只是全部封装在GUI里。4.3 波形判读与仿真发散排查波形窗口打开后先确认三件事CS拉低是否早于第一个SCK上升沿MISO采样时刻数据是否稳定接收数据是否在STOP阶段被锁存到dout。如果仿真中数据出现了类似“发散”的现象即信号在复位释放后仍然是不定态或始终为0优先检查以下几个位置现象可能原因排查方法所有信号为U复位未释放或时钟没跑起来检查reset_n置高时序发送正常但dout全0采样沿和变化沿接反检查CPHA对应的sclk触发边沿SCK没有翻转状态机没进入TRANSFER检查start脉冲宽度是否太短数据错位最后一位没采到调整bit_cnt的结束条件仿真发散时不要急着改代码先在波形里找到第一个异常点看是状态机跳转错乱还是数据路径断裂。90%的SPI仿真问题出在“采样沿选择”和“位计数时序”两个位置这两个地方修好后上板的成功率会很高。5. 原理图设计、管脚约束与Quartus工程文件的取舍仿真通过后进入板级集成。原理图不只是连线还要考虑电平标准、外部上拉和信号完整性。这一步如果省掉上板出现的通信问题往往比仿真里更多。5.1 板级接口原理与推荐连接FPGA与外设之间的SPI连接通常四根信号线如下表所示。SCK和MOSI由FPGA输出MISO由从机输出CS由FPGA控制。部分从机还提供中断引脚和复位引脚需要额外占用两个GPIO。信号方向说明典型建议SCKFPGA - 从机SPI时钟串接22~33欧姆电阻抑制过冲MOSIFPGA - 从机主出从入数据直接连接或串接电阻MISO从机 - FPGA主入从出数据FPGA侧加10k上拉CSFPGA - 从机片选低有效每颗从机单独一个GPIO或硬件CSMISO加上拉电阻是因为不少从机在CS释放后会把MISO置为高阻态如果没有上拉FPGA输入悬空会导致接收数据随机跳变。上拉电阻选10k欧姆即可对通信速率几乎没有影响。如果从机是5V逻辑SCK、MOSI、CS这三个FPGA输出引脚串接分压电阻比如220欧姆加1k欧姆分压或者用单向电平转换芯片MISO方向的5V输入不能在FPGA输出侧直接连因为FPGA引脚不是5V耐压需要电阻分压后再进入FPGA内部。5.2 管脚约束与时钟约束原理图确定后在Vivado里使用XDC约束文件分配引脚和设置时序。以Xilinx开发板为例约束内容通常包含引脚位置、电平标准、系统时钟周期三个部分set_property PACKAGE_PIN L16 [get_ports {clk}] set_property IOSTANDARD LVCMOS33 [get_ports {clk}] set_property PACKAGE_PIN R14 [get_ports {mosi}] set_property PACKAGE_PIN T14 [get_ports {miso}] set_property PACKAGE_PIN U14 [get_ports {sclk}] set_property PACKAGE_PIN V14 [get_ports {cs_n}] set_property IOSTANDARD LVCMOS33 [get_ports {mosi}] set_property IOSTANDARD LVCMOS33 [get_ports {miso}] set_property IOSTANDARD LVCMOS33 [get_ports {sclk}] set_property IOSTANDARD LVCMOS33 [get_ports {cs_n}] create_clock -period 20.000 -name sys_clk [get_ports clk]create_clock定义系统时钟为50MHz。SPI信号本身的时序约束处理方式比较特殊低速SPI不需要专门为SCK和MOSI之间的延迟添加约束因为内部逻辑延迟远小于一个SCK周期只有当SCK频率超过25MHz时才需要给数据输出设置set_output_delay约束值从从机数据手册的建立/保持时间换算得到。5.3 解压后各类工程文件如何处理资料包里出现spi_out.vhd.bak、spi.root_partition.map.atm、spi.sldhu_*.map.atm这类文件时不要被它们干扰。.bak是VHDL源文件的备份说明文档里可能有修改记录.map.atm和.cmp.atm属于Quartus编译流程的中间产物与源码逻辑无关。root_partition相关文件是Quartus在增量编译时生成的根分区映射sldhu文件是SignalTap相关的硬件配置文件这些在重新编译后都会重新生成。提示跨版本打开别人的Quartus工程时中间文件很可能导致工程加载异常。最稳妥的方式是只复制.vhd源码和说明文档新建一个工程然后把源码加入工程重新分配管脚。这样能彻底避开版本兼容问题。6. 扩展用法与排错技巧QSPI、片选方式与环回测试标准SPI四线结构基本覆盖了大部分场景但工程里往往还会遇到QSPI Flash、DSPI这类变种。最后一节把边界情况和使用技巧讲透。6.1 DSPI和QSPI在FPGA上的差异DSPI和QSPI不是同一种东西。DSPI强调可配置延时和更灵活的字长控制本质还是四线SPI逻辑QSPI则把MOSI和MISO扩展成四根双向IO每个时钟周期传输4比特。FPGA实现QSPI的关键在于inout端口和三态缓冲。标准SPI的MOSI是单向输出QSPI的IO0到IO3在命令阶段由主机驱动、数据阶段由Flash驱动状态机里必须增加方向切换逻辑否则数据会相互冲突。学习这套资源时建议先跑通标准SPI再扩展QSPI。QSPI控制器在FPGA上的常见实现方式是使用IO三态门加上方向寄存器指令阶段方向为输出读取数据阶段切到输入。这个切换时刻的控制是QSPI最容易出错的地方调试时用小段波形仔细比对Flash手册的状态转换图。6.2 硬件片选与软件片选FPGA开发板上的SPI Flash通常连接在专用硬件片选引脚上但更多外设使用普通GPIO模拟片选。两者的区别在于时序可控性硬件片选由内部逻辑自动拉低拉高时序稳定但灵活性差软件片选用GPIO手动控制可以从容调整CS相对于SCK的建立时间。如果外设芯片比较多我一般会把所有片选做成软件片选这样每个外设的CS建立时间都可以独立微调。软件片选上电瞬间要注意初始电平CS引脚必须配置为默认高电平否则FPGA配置期间外设可能被误选。对上电时序敏感的外设在CS引脚上加一个10k欧姆上拉电阻可以进一步保险。6.3 用环回测试快速定位故障上板遇到通信失败时第一步不要怀疑代码先做物理层排查把MOSI和MISO用跳线短接FPGA发送0x55再读取MISO回环值。这个0x55的交叉位模式能同时验证相邻位是否有粘连比0xA5更能暴露移位寄存器故障。回环测试失败时按顺序检查SCK是否有输出、CS是否拉低、MISO是否回读用示波器量这三个点的电平一眼就能定位到问题出在FPGA内部还是外部连线。如果回环成功但外设通信失败再考虑从机配置问题。此时先读从机ID寄存器这是SPI调试最重要的技巧——大多数外设都有ID/ChipID寄存器读到了就说明时序极性基本正确读不到则逐项核对CPOL、CPHA、SCK频率上限和CS建立时间。实在不行把CS_SETUP加大到DIV_RATIO级别的长度牺牲一点传输速度换稳定先把链路打通再说。本文还有配套的精品资源点击获取