ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA实现SPI从机:VHDL设计与ModelSim仿真全流程解析

FPGA实现SPI从机:VHDL设计与ModelSim仿真全流程解析 简介面向FPGA开发初学者的SPI通信接口完整设计资料包基于Quartus II工具链实现内容涵盖原理说明、工程源码、仿真验证与设计文档适合学习同步串行接口时序与FPGA开发流程。资源共424个文件压缩包约4.73MB主要包含Quartus II工程文件qpf/qsf/bdf/vhd/tdf等、仿真波形、FPGA配置文件sof/pof以及docx/pdf说明文档和png/jpg截图目录结构清晰便于检索。已有753人学习下载。资料对SPI的SCK时序、MISO/MOSI数据收发、从设备选择逻辑及四种工作模式均有分析并给出仿真结果截图方便对照时序理解。项目划分SPI生成器与主通信逻辑等子模块附带文档说明和可能的MATLAB交互记录能帮助读者系统掌握FPGA实现SPI通信的完整流程为后续驱动传感器、存储器等外设打下基础。1. 自己写SPI从机是FPGA入门性价比最高的一课很多FPGA初学者拿到SPI模块需求第一反应是从Quartus II的IP Catalog里拖一个SPI controller出来。拖IP没有错但把接口时序完全交给黑盒之后遇到SCK丢沿、数据移位错位、从机不回数据这类问题往往只能对着ModelSim发呆连仿真波形是红线还是高阻都分不清。SPI是同步串行协议没有握手、没有仲裁把它的时钟极性/相位CPOL/CPHA、采样沿、片选时序吃透就等于解锁了FPGA设计的核心基本功跨时钟域边沿检测、移位寄存器设计、状态机的精打细算。这份资源包是一个基于Quartus II的完整FPGA工程里面包含VHDL源文件例如spi_out.vhd以及工程编译生成的一系列映射文件、仿真结果截图和说明文档。它适合正在做FPGA课程设计的人也适合准备FPGA笔试面试、想自己动手写SPI通信接口的人。下面我按“协议分析 → RTL实现 → ModelSim仿真 → 板级调试”的顺序把这个SPI接口从时序图到波形验证完整拆开讲一遍。2. SPI协议时钟极性与相位先把四种模式选对2.1 四个信号和一份时序图SPI物理层只有四根线SCK串行时钟、MOSI主出从入、MISO主入从出、SS片选低有效。通信方向由SS控制SS拉低表示选中从机之后主设备在SCK上给出时钟双方按约定好的边沿收发数据。从机侧数据能不能被正确采样完全取决于SCK空闲电平和采样边沿。这两个参数组合出四种协议模式。所有从机芯片的数据手册都会明确写支持哪几种模式例如W25Q系列Flash通常支持Mode 0和Mode 3部分ADC固定工作在Mode 1。设计的第一步不是写代码而是先确认主设备或从设备手册里给出的是哪种模式否则后面的时序对齐全是白费。以下表格是最直接的对照本文后面的RTL实现也完全按照这个关系展开。模式CPOLCPHA从机采样边沿从机输出边沿常见器件Mode 000SCK 上升沿SCK 下降沿W25Q系列Flash、多数SPI接口外设Mode 101SCK 下降沿SCK 上升沿部分ADC、传感器Mode 210SCK 下降沿SCK 上升沿特定工业接口芯片Mode 311SCK 上升沿SCK 下降沿W25Q系列另一种常用配置2.2 从设备视角采样沿决定接收逻辑在FPGA内部系统时钟比如50MHz通常比外部SCK快得多。SCK由主设备产生对FPGA来说是一个异步信号。如果直接用SCK去采样MOSI容易引入亚稳态。常见做法是在FPGA内部将SCK、SS、MOSI都打两拍同步再检测SCK的上升沿和下降沿。打拍后的边沿会滞后两个系统时钟周期但SPI的SCK通常只有几MHz到几十MHz这个延迟完全能接受。用两拍同步写法伪代码如下sck_pipe sck_pipe(0) sck; -- 每时钟周期左移一次 sck_rise sck_pipe(1) and not sck_pipe(0); -- 上一次低、本次高 sck_fall not sck_pipe(1) and sck_pipe(0); -- 上一次高、本次低这里sck_pipe(0)是最新采样值sck_pipe(1)是上一拍的值。用这种写法提取边沿比直接判断电平更安全不容易出现毛刺。接收逻辑只在采样沿工作。以Mode 0为例主设备在SCK上升沿之前就把MOSI数据准备好从机在上升沿采一次就能得到稳定值。实际项目中最容易犯的错误是把接收和发送放在同一个边沿导致输出数据还没切换输入数据又发生了移位最终波形和数据全部错位。2.3 主设备模式SCK分频不能随便给FPGA端作为SPI主设备时需要自己产生SCK。常见做法是将系统时钟分频。分频系数和系统时钟频率、目标SCK频率直接相关。下面是一个通用分频器它产生的是“时钟使能脉冲”而不是直接分频出时钟。这样做的目的是让整个设计仍然由单一系统时钟驱动避免在FPGA内部产生门控时钟综合时更容易收敛。-- spi_clk_en.vhd library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity spi_clk_en is generic ( CLK_FREQ : integer : 50_000_000; -- 系统时钟频率 (Hz) SPI_FREQ : integer : 1_000_000 -- 目标SCK频率 (Hz) ); port ( clk : in std_logic; rst_n : in std_logic; sck_en : out std_logic -- 输出使能脉冲 ); end entity; architecture rtl of spi_clk_en is constant DIV_N : integer : CLK_FREQ / (2 * SPI_FREQ) - 1; signal cnt : integer range 0 to DIV_N : 0; begin process (clk, rst_n) begin if rst_n 0 then cnt 0; sck_en 0; elsif rising_edge(clk) then if cnt DIV_N then cnt 0; sck_en 1; else cnt cnt 1; sck_en 0; end if; end if; end process; end architecture;逻辑说明当sck_en拉高一个系统时钟周期外部逻辑就可以用它去翻转SCK引脚从而得到目标频率的方波。DIV_N的计算公式是CLK_FREQ / (2 * SPI_FREQ) - 1。假设系统时钟50MHz目标SCK为1MHz那么DIV_N24也就是每25个系统时钟翻转一次SCK一个SCK周期正好50个时钟周期。使用这个模块时要注意SPI_FREQ必须能被CLK_FREQ整除否则实际频率会偏差。另外DIV_N必须为正整数所以CLK_FREQ不能低于2 * SPI_FREQ。3. Quartus II工程搭建与VHDL从机实现3.1 工程目录和顶层架构拿到这个资料包时会看到一堆工程文件spi.sldhu、spi.root_partition.map.atm、spi_out.vhd.bak、spi.autos.map.atm等。其中.sldhu是Quartus II的层次化视图文件.map.atm是编译过程中自动生成的映射文件.vhd.bak是VHDL源码备份。真正需要关注的是VHDL源文件和说明文档。在Quartus II 13.0sp1以上版本打开spi.qpf就能还原工程如果没有.qpf新建工程后把.vhd文件用“Add All”添加进去也能编译。顶层设计一般是顶层模块比如spi_top.vhd例化时钟分频器、SPI从机逻辑和寄存器接口。spi_out.vhd很可能就是从机实体里面定义了CPOL、CPHA、数据位宽等generic参数。把所有参数保留在generic里好处是仿真和综合共用一套RTL不会出现仿真模型和实际电路不一致的情况。模块划分大致如下spi_top.vhd顶层例化分频器和SPI从机spi_out.vhdSPI收发核心逻辑spi_tb.vhdtestbench用于ModelSim仿真。3.2 核心逻辑一个支持四种模式的SPI从机下面是一个常用的SPI从机RTL实现没有使用状态机只用移位寄存器加边沿检测。代码量少综合后资源占用也很低并且通过generic参数就可以切换四种SPI模式。-- spi_slave.vhd library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity spi_slave is generic ( CPOL : std_logic : 0; -- 时钟极性 CPHA : std_logic : 0; -- 时钟相位 DATA_WIDTH : integer : 8 -- 数据位宽 ); port ( clk : in std_logic; -- 系统时钟 rst_n : in std_logic; -- 异步复位低有效 sck : in std_logic; -- SPI 时钟 ss_n : in std_logic; -- 片选低有效 mosi : in std_logic; -- 主出从入 miso : out std_logic; -- 主入从出 data_tx : in std_logic_vector(DATA_WIDTH-1 downto 0); -- 待发送数据 data_rx : out std_logic_vector(DATA_WIDTH-1 downto 0); -- 接收数据 byte_done : out std_logic -- 一个字节接收完成指示 ); end entity; architecture rtl of spi_slave is signal sck_pipe : std_logic_vector(1 downto 0); signal ss_pipe : std_logic_vector(1 downto 0); signal sck_rise : std_logic; signal sck_fall : std_logic; signal ss_fall : std_logic; signal shift_reg : std_logic_vector(DATA_WIDTH-1 downto 0); signal rx_buf : std_logic_vector(DATA_WIDTH-1 downto 0); signal tx_buf : std_logic_vector(DATA_WIDTH-1 downto 0); signal bit_cnt : integer range 0 to DATA_WIDTH - 1; signal done_r : std_logic; signal sample_rd : std_logic; signal sample_wr : std_logic; begin -- 两级同步 边沿检测 process (clk, rst_n) begin if rst_n 0 then sck_pipe 00; ss_pipe 11; elsif rising_edge(clk) then sck_pipe sck_pipe(0) sck; ss_pipe ss_pipe(0) ss_n; end if; end process; sck_rise sck_pipe(1) and not sck_pipe(0); sck_fall not sck_pipe(1) and sck_pipe(0); ss_fall ss_pipe(1) and not ss_pipe(0); -- 采样沿与输出沿的映射 sample_rd sck_rise when (CPOL xor CPHA) 0 else sck_fall; sample_wr sck_fall when (CPOL xor CPHA) 0 else sck_rise; -- 接收逻辑SS下降沿计数清零采样沿移入 MOSI process (clk, rst_n) begin if rst_n 0 then shift_reg (others 0); rx_buf (others 0); bit_cnt 0; done_r 0; elsif rising_edge(clk) then done_r 0; if ss_fall 1 then bit_cnt 0; elsif sample_rd 1 then shift_reg shift_reg(DATA_WIDTH-2 downto 0) mosi; if bit_cnt DATA_WIDTH - 1 then rx_buf shift_reg(DATA_WIDTH-2 downto 0) mosi; done_r 1; bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; end if; end process; -- 发送逻辑SS下降沿装载发送数据输出沿左移 process (clk, rst_n) begin if rst_n 0 then tx_buf (others 0); elsif rising_edge(clk) then if ss_fall 1 then tx_buf data_tx; elsif sample_wr 1 then tx_buf tx_buf(DATA_WIDTH-2 downto 0) 0; end if; end if; end process; miso Z when ss_n 1 else tx_buf(DATA_WIDTH-1); data_rx rx_buf; byte_done done_r; end architecture;逻辑说明所有异步信号都先经过两级D触发器同步sck_pipe(1)是上一拍的SCKsck_pipe(0)是当前拍所以sample_rd和sample_wr能够根据CPOL/CPHA正确选择采样沿和输出沿。例如Mode 0CPOL0CPHA0时CPOL xor CPHA为0因此sample_rd上升沿sample_wr下降沿与SPI协议规定一致。接收流程中SS下降沿把bit_cnt清零之后每个采样沿把MOSI移入shift_reg最低位。当bit_cnt达到7时说明已经移入8位把当前完整字节锁存到rx_buf并产生一个byte_done脉冲。发送流程中SS下降沿包含data_tx到tx_bufMISO在SS为低时输出最高位后续每个输出沿左移一位。需要注意的是从机必须在主设备采样沿到来之前完成MISO切换否则主设备读到的仍然是旧数据。参数说明CPOL决定SCK空闲电平CPHA决定采样沿相对首位的相位。实际使用时根据外部主设备手册把这两个generic固定下来即可不要在顶层做成运行时动态切换的寄存器否则切换瞬间可能产生毛刺。3.3 引脚分配与仿真约束在Quartus II中打开Pin Planner把sck、ss_n、mosi、miso分配到开发板的具体引脚。需要注意ss_n是低有效输入如果PCB上没接上拉电阻需要在Pin Planner里为它开启弱上拉避免悬空误触发。下面是一组典型的引脚属性参考顶层信号方向I/O属性说明clkInput3.3VLVTTL板载晶振连接到全局时钟引脚rst_nInput3.3V带上拉按键复位低有效sckInput3.3V外部主设备时钟输入ss_nInput3.3V开弱上拉片选信号低有效mosiInput3.3V主出从入misoOutput3.3V主入从出异步输入信号不需要做精确时序约束因为逻辑已经用两级同步器处理了跨时钟域问题。如果是SPI主设备模式SCK作为输出则要检查输出引脚驱动能力和负载电容是否匹配。这一步虽然不影响仿真但会直接决定上板后波形质量。4. ModelSim仿真验证从波形红线到数据正确4.1 写一个参数化testbench在Quartus II中可以通过Tools → Run Simulation Tool → RTL Simulation启动ModelSim它会自动把工程源码和库加载进去。也可以手动使用ModelSim命令行编译。先写一个testbench例化上一章的spi_slave然后产生一帧SPI主设备时序。下面是一个可用的测试骨架重点在于能够看到byte_done和数据接收是否正确。-- spi_slave_tb.vhd library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity spi_slave_tb is end entity; architecture sim of spi_slave_tb is constant CPOL : std_logic : 0; -- 使用 Mode 0 constant CPHA : std_logic : 0; constant DATA_W : integer : 8; constant SCK_HALF : time : 500 ns; -- 1MHz SCK signal clk : std_logic : 0; signal rst_n : std_logic : 0; signal sck : std_logic : 0; signal ss_n : std_logic : 1; signal mosi : std_logic : 0; signal miso : std_logic; signal data_tx, data_rx : std_logic_vector(DATA_W-1 downto 0); signal byte_done : std_logic; begin uut : entity work.spi_slave generic map ( CPOL CPOL, CPHA CPHA, DATA_WIDTH DATA_W ) port map ( clk clk, rst_n rst_n, sck sck, ss_n ss_n, mosi mosi, miso miso, data_tx data_tx, data_rx data_rx, byte_done byte_done ); clk not clk after 10 ns; -- 50MHz 系统时钟 stim : process variable tx : std_logic_vector(DATA_W-1 downto 0) : x5A; begin rst_n 0; data_tx xA5; wait for 100 ns; rst_n 1; wait until rising_edge(clk); ss_n 0; for i in DATA_W-1 downto 0 loop mosi tx(i); wait for SCK_HALF; sck not sck; -- 产生一个 SCK 边沿 wait for SCK_HALF; end loop; ss_n 1; wait; end process; check : process begin wait until byte_done 1; assert data_rx x5A report RX data mismatch severity error; wait; end process; end architecture;逻辑说明系统时钟clk是50MHzSCK半周期SCK_HALF为500ns对应1MHz SCK。激励进程stim把数据从高位到低位依次放到MOSI上每设置一次数据就等半周期SCK然后翻转一次SCK模拟主设备产生时钟。check进程会等待byte_done然后断言接收数据等于0x5A。注意sck的初始值要与CPOL一致Mode 0初始为0Mode 2初始为1改模式时这里也要同步修改。4.2 用ModelSim命令行跑回归如果喜欢命令行方式在ModelSim的Transcript窗口中输入以下命令即可完成编译和仿真vlib work vmap work work vcom spi_slave.vhd spi_slave_tb.vhd vsim work.spi_slave_tb add wave -hex /spi_slave_tb/* run 30 us参数说明vcom用于编译VHDL文件vsim指定仿真顶层模块add wave -hex把顶层下所有信号以十六进制格式加入波形窗口run 30 us仿真时长要覆盖一帧传输。1MHz SCK下传输8bit约需要8微秒30微秒完全足够。如果编译时报vcom不可用先确认ModelSim安装的是Altera Edition且包含VHDL licenseQuartus II自带的ModelSim Starter版通常没问题。仿真结果截图是这类资源包里的重要组成部分。我习惯在Wave窗口把数据信号、byte_done和data_rx一起加进视图设置为白色背景、十六进制显示再把关键时序点加注释。这样放进课程设计报告里答辩时一眼就能看出数据何时完成接收。4.3 modelsim仿真波形是红线常见的三个原因很多人在第一次跑SPI仿真时会发现波形不是正常的蓝线而是红线或棕线。红线通常代表信号没有被正确驱动可能的原因有三个。第一个是高阻态。miso这种三态信号最典型。如果testbench里SS在仿真一开始没有拉高或者代码里miso没有赋初值仿真器会显示红色。在SPI从机中SS为高时miso输出高阻是正常行为看起来是红线并不代表错误要看它是否在SS拉低后正常变成有效数据。第二个是复位未释放或释放顺序不对。如果rst_n一直为0所有寄存器输出都会被锁定在复位值波形会是一条平的线没有边沿活动。排查时先看rst_n是否如期拉高再看同步器内部的sck_pipe是否跟随外部SCK变化。如果sck_pipe不变说明外部信号没进来或者同步器写错了。第三个是信号连接错误。testbench例化时少连了一个端口或者模块内部信号名拼写不一致都会导致信号悬空。排查时在ModelSim的Object面板点击信号查看它的逻辑值如果是Z或X就沿着驱动链往上找。这里有个比较实用的定位技巧在Wave窗口左侧选中所有信号右键选择“Format → Data Value → Hex”把二进制改成十六进制能更快发现数据位是否错位。5. 进阶用SignalTap II抓板级时序和仿真截图互证5.1 在Quartus II中插入SignalTap II仿真通过不代表上板就能跑外部引脚电平、时钟稳定度、片选信号毛刺都会影响实际SPI通信。Quartus II里集成SignalTap II逻辑分析仪可以在FPGA运行时实时抓取内部信号这是验证SPI从机最直接的手段。在Tools菜单下打开SignalTap II Logic Analyzer新建一个实例把clk作为采样时钟把sck、ss_n、mosi、miso、byte_done、data_rx加入采集列表。SignalTap II的配置参数如下表参数设置值说明Sample clockclk(50MHz)采样时钟必须比SCK高至少4倍Trigger inputss_n用片选下降沿做触发Trigger conditionFalling edgeSS拉低即开始捕获Sample buffer depth512足够存一帧8bit数据Buffer acquisition modeContinuous连续采集循环覆盖旧数据设置好之后重新编译工程把sof文件下载到FPGA。SignalTap II的嵌入式逻辑分析仪会占用少量片上RAM和逻辑单元对SPI从机这种小设计来说资源影响可以忽略。实际抓取时先手动触发一次通信例如通过按键或上位机让主设备发一帧数据然后观察SignalTap II捕获到的波形。5.2 触发条件和波形对照如果ss_n没有引出到外部引脚或者开发板上的片选信号不是全局引脚也可以把触发条件改成byte_done上升沿。这样捕获的是SPI传输完成前后的数据同样能验证接收结果。触发条件设置成“下降沿”时波形窗口里能看到从SS拉低到字节接收完成的完整过程。上板后对照仿真截图重点看三个时间点SS拉低到SCK第一个采样沿的间隔MOSI数据在采样沿时是否已经稳定MISO输出在输出沿之前是否完成切换。这三个点中任何一个对不上通信都不会正常。常见原因是主设备SCK频率太高导致从机同步器延迟后错过了采样窗口。比如主设备用10MHz SCK系统时钟却只有25MHz两级同步器已经占掉两个时钟周期采样沿滞后严重这时候要把SCK降到4MHz以下或从机改用更高频率的采样时钟。5.3 把仿真截图变成可对比的交付材料课程设计答辩时最好把ModelSim仿真波形和SignalTap II抓到的波形截到同一张图里并标注出byte_done的位置。方法是在SignalTap II里使用“Export Waveform”功能导出VCD文件然后在ModelSim里用vcd2wlf转换后加载这样两个波形的数据可以在同一个时间轴上对比。如果没有VCD导出条件就在两张截图上手动标出关键沿再把它们并排粘贴说明。这样能直接证明设计不是只在仿真里跑通而是真正在FPGA板上工作对答辩和文档评审都是很有说服力的证据。本文还有配套的精品资源点击获取
返回列表