ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

FPGA实现SPI控制器:从Verilog代码到上板调试全解析

FPGA实现SPI控制器:从Verilog代码到上板调试全解析 1. 项目背景为什么在FPGA上自己写SPI控制器先说个我自己的经历。两年前做一块采集板主控用的是Zynq外设挂了一片工业级的ADC接口就是SPI。当时想省事直接调PS端的SPI控制器结果折腾了一下午发现驱动配置复杂不说时序还总是差那么几个纳秒。后来一气之下在PL端用Verilog写了一个SPI主机模块一百行不到的代码问题全解决了时序完全可控带宽还比原来高出一截。从那以后我在项目里遇到SPI接口的外设基本都会优先考虑在FPGA里自己实现控制器而不是依赖处理器自带的SPI外设。原因其实不难理解FPGA里的逻辑是并行运行的SPI的时钟SCLK可以由内部时钟分频得到收发数据可以和外部逻辑无缝衔接不需要经过CPU干预。很多场景下SPI从机设备的时序要求比较特殊比如某些ADC要求CS拉低后必须等待几个时钟周期才能发起SCLK或者需要连续读取多个通道的数据。处理器的SPI控制器可能不支持这么灵活的自定义时序但FPGA完全可以做到。如果系统里有多路SPI外设FPGA实现多个SPI主机也就是例化几次模块的事不额外占用PCB面积也不用增加物料成本。另外SPI协议本身是实现其它很多协议的基础。比如SD卡就是工作在SPI模式下初始化的很多Nor Flash、传感器、触摸屏控制器都支持SPI接口。把这套逻辑吃透了后面再做SD卡控制器、Flash控制器、甚至I3C接口上手速度都会快很多。这篇博客我打算从SPI的基本原理讲起然后给出一份可以直接用的Verilog实现包括主机和从机的代码框架再配合仿真和上板实测的流程最后聊聊我在实际项目中踩过的坑和总结下来的经验。适合正在学FPGA的初学者也适合在项目里需要快速实现SPI通信的工程师参考。2. SPI协议速览四种模式与时序核心很多初学者上来就写代码结果波形出来完全不对往往是对协议本身的理解不够透彻。所以这里先把SPI的关键知识点梳理清楚这些内容也是后面代码实现的直接依据。2.1 四根线的分工与数据交换逻辑SPISerial Peripheral Interface是Motorola在70年代末提出的一种同步串行通信协议。标准情况下需要四根信号线信号名方向作用SCLK主机→从机串行时钟由主机产生MOSI主机→从机Master Output Slave Input主机发数据MISO从机→主机Master Input Slave Output从机发数据CS/SS主机→从机片选信号低电平有效选中对应从机SPI和UART、I2C最大的不同在于它是一种全双工的同步协议。发送和接收是同时进行的主机在SCLK的某个边沿把MOSI上的数据发送出去同时在同一时刻采样MISO上的数据。换句话说每次时钟脉冲到来主机和从机实际上完成了“一次数据交换”双方各发出去一位、各接收进来一位。理解这一点非常重要因为它直接决定了代码怎么写发送模块和接收模块本质上是一个状态机不能拆成独立的两套逻辑。我见过有人把SPI的发送FIFO和接收FIFO完全分开设计结果读取逻辑经常对不上就是这个原因。2.2 CPOL和CPHA四种工作模式是怎么来的SPI的四种模式由两个参数决定CPOLClock PolaritySCLK空闲时的电平。CPOL0表示空闲为低电平CPOL1表示空闲为高电平。CPHAClock Phase数据采样和切换的相位关系。CPHA0表示在第一个边沿采样CPHA1表示在第二个边沿采样。组合起来就是常见的SPI Mode 0~3对应关系如下模式CPOLCPHA数据采样边沿数据切换边沿Mode 000上升沿下降沿Mode 101下降沿上升沿Mode 210下降沿上升沿Mode 311上升沿下降沿实际项目里最常用的是Mode 0和Mode 3这两种模式都在上升沿采样时序最容易满足。多数Flash芯片默认支持Mode 0或Mode 3。我踩过一个坑某颗传感器芯片数据手册只写了SPI时序图没有直接标注Mode几结果按Mode 0配置怎么都不出数后来对照时序图一帧一帧比对才发现它要求的是Mode 1。所以拿到新芯片第一件事就是对着数据手册的时序图把CPOL和CPHA确认清楚不要想当然。2.3 数据帧格式与MSB/LSB问题常规SPI通信是8位一字节但很多外设用的是16位帧格式例如某些音频编解码器和ADC需要一次性发送16位控制字。更复杂的帧格式包括24位、32位和可变长度帧。工程上有两种组织方式固定位宽直接按帧传输适合比较器、ADC这类数据格式固定的设备。可变位宽由计数器控制在通用的SPI控制器里常用例如MIPI I3C或SD卡的标准SPI模式。还需要注意MSB传输顺序。SPI标准协议默认MSB先行但也有芯片偏偏要用LSB先行比如部分LCD驱动IC和传感器。如果复用模块最好把移位方向做成参数可配。不过这里要提个建议不要为了通用性把模块设计得过于复杂。我见过有工程师把SPI控制器写成了类似通用DMA的架构支持任意长度、任意模式、任意位序、FIFO、中断结果代码量两千行最终调了半个月性能还未必比得上一个精简版本。FPGA的每个逻辑单元都是有成本的按需设计才是正道。3. 主机端Verilog实现状态机为主的可复用方案这一节给出一个相对完整的SPI主机实现支持参数化时钟分频、四种模式配置、8/16/32位数据宽度代码量不大适合直接移植到项目里使用。3.1 模块端口定义与参数设计module spi_master #( parameter DATA_WIDTH 8, parameter CLK_DIV 10, // 系统时钟频率 / SPI时钟频率例如50MHz / 5MHz 10 parameter CPOL 0, parameter CPHA 0 )( 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 busy, output reg done, // SPI物理接口 output reg sclk, output reg cs_n, output reg mosi, input wire miso );参数里面最关键的是CLK_DIV。这个值的计算方法是系统时钟频率除以SPI时钟频率比如系统时钟100MHz、SPI目标时钟10MHz那么CLK_DIV就是10。注意这里的含义是倍数关系需要产生的是分频后的时钟沿触发而不是直接用always块生成内部时钟网络。3.2 时序控制的两种实现思路主机的核心逻辑其实就一句话控制SCLK产生正确的脉冲序列同时在正确的时间采样MISO。具体实现有两种主流写法第一种是分频后沿触发。用计数器对系统时钟分频每当计数器归零时产生一个sclk_tick脉冲状态机在这个脉冲的驱动下推进一位。好处是逻辑清晰不容易产生毛刺。第二种是直接计数采样。不生成显式的分频时钟而是用计数器记录分频位置在不同计数位置执行不同的操作比如在计数到中间位置时采样MISO。这种方式更加鲁棒也是我目前比较推荐的做法。我这里给出的是第二种方式的简化版本核心代码如下reg [DATA_WIDTH-1:0] shift_reg; reg [7:0] tick_cnt; reg [7:0] bit_cnt; wire sclk_tick (tick_cnt CLK_DIV/2 - 1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin tick_cnt 0; bit_cnt 0; cs_n 1b1; busy 1b0; done 1b0; shift_reg 0; end else begin if (start !busy) begin busy 1b1; done 1b0; cs_n 1b0; bit_cnt 0; tick_cnt 0; shift_reg tx_data; end else if (busy) begin if (sclk_tick) begin // 生成SCLK翻转逻辑根据CPOL/CPHA调整 // 采样MISO和移位逻辑 if (bit_cnt DATA_WIDTH) begin busy 1b0; done 1b1; cs_n 1b1; rx_data shift_reg; end end end end end上面的代码只是一个骨架完整实现里还需要加入SCLK翻转、MISO采样、边沿控制等逻辑。3.3 状态机完整设计一次传输的完整流程我把一次完整的SPI传输过程用状态机拆解成五个状态状态行为进入条件IDLE等待start信号CS保持高复位或done拉高CS_LOWCS拉低等待t_setup时间收到startTRANSFER按bit_cnt逐位收发数据等待时间结束CS_HIGHCS拉高等待t_hold时间所有位传输完毕DONE拉高done信号回IDLECS高电平保持结束比较重要的一步是在CS_LOW状态下延时几个时钟周期再开始第一个SCLK。虽然很多SPI从机不要求这个建立时间但一些严格芯片的数据手册明确标注了t_cs_sclk的最小值。用一个通用的延时计数器解决匹配所有设备都更安全。采样逻辑我用了一个技巧在SCLK的上升沿前后各延时半个分频周期采样。比如CLK_DIV10时SCLK周期是10个系统时钟那么在第2.5个时钟位置采样最稳妥。工程上一般先做分频计数器把SCLK翻转推迟半步这样采到的数据就是稳定电平。3.4 信号同步与跨时钟域的工程习惯在FPGA工程中来自外部的信号都必须经过同步处理。SPI的MISO来自外接从机芯片频率可能和FPGA内部时钟毫无关系如果不处理就大概率出现亚稳态。reg miso_s1, miso_s2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin miso_s1 1b0; miso_s2 1b0; end else begin miso_s1 miso; miso_s2 miso_s1; end end wire miso_sync miso_s2;用两级触发器进行同步是处理跨时钟域最基础、也最可靠的方式。SPI的时钟频率普遍不高几MHz到几十MHz两级同步足够满足需求。如果外部信号频率非常高或者设计要求更高的可靠性可以再加一级或者使用专门的异步FIFO方案但SPI场景通常用不上那么重的手段。4. 从机端Verilog实现时序解析与数据收发主机端的代码是大多数项目的核心但有些场景必须在FPGA里实现SPI从机。比如FPGA作为协处理器挂在ARM或MCU下通过SPI接口接收配置命令和数据。我做过一个高速数据采集项目MCU就是通过SPI向FPGA发送控制字和读取采样结果的。4.1 从机模块的端口与整体结构SPI从机的设计思路和主机完全不同因为它没有主动权一切时序都由外部主机决定。从机模块的任务是被动解析SCLK边沿完成数据的移位接收和移位发送。module spi_slave #( parameter DATA_WIDTH 8, parameter CPOL 0, parameter CPHA 0 )( input wire clk, // 系统时钟用于同步和采样 input wire rst_n, input wire sclk, // 外部主机提供的SPI时钟 input wire cs_n, // 片选低有效 input wire mosi, output reg miso, // 用户接口 output reg rx_valid, output reg [DATA_WIDTH-1:0] rx_data, input wire [DATA_WIDTH-1:0] tx_data, input wire tx_valid, output reg tx_ready );和主机端最大的区别在于SCLK是从外部输入进来的无法保证与内部系统时钟同步。所以所有的边沿判断都得用系统时钟去采样而且要注意避免毛刺。4.2 SCLK边沿检测与数据采样时机在从机端检测SCLK边沿是核心操作。我用两级触发器同步SCLK然后通过组合逻辑判断上升沿和下降沿reg sclk_s1, sclk_s2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_s1 1b0; sclk_s2 1b0; end else begin sclk_s1 sclk; sclk_s2 sclk_s1; end end wire sclk_posedge sclk_s2 ~sclk_s1; // 上升沿 wire sclk_negedge ~sclk_s2 sclk_s1; // 下降沿有了这两个信号就能根据CPHA参数决定什么时候采样数据CPHA0时数据在第一个边沿上升沿或下降沿取决于CPOL被采样从机应该在SCLK的第一个边沿之后把数据放到MOSI上在第二个边沿输出下一位。CPHA1时数据在第二个边沿被采样从机应该在第一个边沿就输出数据第二个边沿保持。这部分的逻辑容易写错我的经验是严格按照数据手册的时序图来写case不要试图用一句话概括所有模式。详细的对照表如下// 采样条件 wire sample_en (CPOL 0 CPHA 0) ? sclk_posedge : (CPOL 0 CPHA 1) ? sclk_negedge : (CPOL 1 CPHA 0) ? sclk_negedge : sclk_posedge; // 数据更新条件 wire update_en (CPOL 0 CPHA 0) ? sclk_negedge : (CPOL 0 CPHA 1) ? sclk_posedge : (CPOL 1 CPHA 0) ? sclk_posedge : sclk_negedge;4.3 从机接收与发送的实现细节从机的状态机相对简单主要受cs_n控制。当cs_n为低时接收逻辑逐位采样mosi同时根据tx_data逐位输出到miso。注意从机必须在CS拉低之前就把发送数据准备好因为第一个SCLK边沿到来时就必须把第一位数据放到MISO上留给从机的反应时间非常短。这里有一个很关键的工程细节从机不能等到发现CS拉低了才开始准备TX数据而应该在上一帧传输结束后就立即准备好下一帧的数据。我在代码里是这样处理的always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_ready 1b0; end else if (cs_n) begin // CS空闲准备好发送数据 shift_tx tx_data; tx_ready 1b1; end else if (update_en !cs_n) begin // 传输中逐位移出 shift_tx {shift_tx[DATA_WIDTH-2:0], 1b0}; tx_ready 1b0; end endCS空闲时就锁存发送数据是最稳妥的做法。否则外部主机SCLK一旦开始跑从机还没准备好数据MISO上输出的就是无效电平接收方就收到了乱码。4.4 多从机场景与三态输出的处理如果总线上挂了多个SPI从机每个从机的MISO引脚需要支持三态输出在CS为高时输出高阻态否则会互相拉低冲突。assign miso cs_n ? 1bz : shift_tx[DATA_WIDTH-1];这是SPI和I2C的一大区别I2C靠开漏来避免冲突SPI必须靠片选三态。如果设计里没有处理好这一点两个从机同时驱动MISO就会烧毁器件或导致逻辑错误。如果FPGA作为从机需要确认FPGA引脚支持三态输出。大多数FPGA引脚都可以配置为三态但要注意约束文件里需要标注IOBUF或设置为inout类型否则综合工具可能会报错。5. 仿真验证从Testbench到波形检查写完RTL代码不仿真就上板基本等于赌博。我在项目里养成的习惯是每写完一个模块先做功能仿真确认逻辑正确以后再进行综合和布局布线。SPI模块的仿真相对容易因为协议本身是标准化的可以直接写一个行为模型来模拟从机。5.1 Testbench环境搭建与激励生成Testbench的核心工作有两件一是生成SPI主机需要的start信号和tx_data数据二是模拟一个SPI从机设备回传已知的应答数据。下面是一个最小化的Testbench骨架module tb_spi_master(); reg clk; reg rst_n; reg start; reg [7:0] tx_data; wire [7:0] rx_data; wire busy; wire done; spi_master #( .DATA_WIDTH(8), .CLK_DIV(10), .CPOL(0), .CPHA(0) ) u_spi_master ( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .rx_data(rx_data), .busy(busy), .done(done), .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso) ); // 模拟从机行为 reg [7:0] slave_rx; always (posedge sclk or negedge cs_n) begin if (!cs_n) slave_rx {slave_rx[6:0], mosi}; // 在SCLK上升沿采样MOSI end assign miso (cs_n 1b0) ? slave_reply[7:0] : 1bz; initial begin clk 0; forever #10 clk ~clk; // 50MHz end initial begin rst_n 0; #100 rst_n 1; #100; start 1b1; tx_data 8hA5; #20 start 1b0; wait (done 1); $display(rx_data %h, rx_data); $finish; end endmodule5.2 时序验证的重点检查项仿真跑起来后不要只看done信号有没有拉高就完事了要逐个检查关键时间节点CS拉低后到第一个SCLK上升沿的延时是否满足从机的建立时间要求。MISO数据的采样点把光标放在SCLK的上升沿上检查此时MISO的数据是否稳定如果采到了跳变沿附近的数据说明采样时机不对。最后一位数据发送完后CS的拉高时机有些从机要求数据位结束后SCLK保持低电平一段时间再拉高CS如果CS拉高太早最后一拍数据可能没有被正确捕获。连续两次传输之间的间隔需要确认从机有足够的时间内部处理数据。还有一个仿真注意事项SPI仿真时如果用了forever #10 clk ~clk这种方式生成时钟有一个隐藏风险。当Testbench在等待done信号时如果逻辑里存在组合环或者漏判条件仿真会一直卡住。建议给仿真加上超时机制比如用#10000 $error(timeout)来兜底。5.3 覆盖边界条件数据全0、全1与交替序列代码写完后的第一轮仿真通过后我通常会把测试数据换成几种特殊值再跑一轮8h00和8hFF检查连续同电平传输时是否存在时序偏移。8hAA和8h55检查最高频翻转时信号完整性。从机回传数据使用伪随机序列验证从机返回数据的接收路径是否正确。这三个测试数据分别覆盖了“直流电平稳定”、“高频翻转”和“非规律翻转”三种工况。如果这三种都过了基本可以认为逻辑没问题。真实的信号完整性问题比如反射、串扰那是PCB层面的问题仿真只能保证逻辑功能正确不能保证硬件电气性能。6. 上板实测从约束文件到逻辑分析仪调试验证仿真通过只是万里长征第一步真正到了板子上问题才会浮出水面。这里分享几个我在上板实测环节积累的经验。6.1 引脚约束与时序约束的配置细节SPI接口的引脚约束其实很简单难的是时序约束。如果只是把SPI时钟频率控制在几MHz以内而且外设和FPGA之间走线不长时序约束不做也能跑。但如果频率上到20MHz以上或者外设要求严格的建立保持时间就必须做约束。基础的引脚约束在XDC文件中这样写set_property PACKAGE_PIN AB16 [get_ports {sclk}] set_property IOSTANDARD LVCMOS33 [get_ports {sclk}] set_property PACKAGE_PIN AB17 [get_ports {cs_n}] set_property IOSTANDARD LVCMOS33 [get_ports {cs_n}] set_property PACKAGE_PIN AA16 [get_ports {mosi}] set_property IOSTANDARD LVCMOS33 [get_ports {mosi}] set_property PACKAGE_PIN AA17 [get_ports {miso}] set_property IOSTANDARD LVCMOS33 [get_ports {miso}]时序约束的关键在于create_clock和set_input_delaycreate_clock -name sys_clk -period 20.000 [get_ports {clk}] set_input_delay -clock [get_clocks spi_sclk] -max 5.0 [get_ports {miso}] set_input_delay -clock [get_clocks spi_sclk] -min 2.0 [get_ports {miso}]不过要提醒一句SPI的时序约束很容易做错因为SPI的SCLK不是FPGA内部时钟而是由内部逻辑翻转产生的。如果SCLK频率不超过30MHz而且线路长度很短很多工程师实际上是不单独约束SCLK的而是依靠系统时序分析来保证。如果这部分拿不准建议把SCLK分组设为虚拟时钟再通过set_false_path排除和系统时钟的跨时钟域路径避免Vivado报大片的时序违例把真正的问题淹没掉。6.2 用ILA抓取SPI波形的操作细节上板调试最有用的工具就是ILAIntegrated Logic Analyzer。在Vivado里把cs_n、sclk、mosi、miso以及done、busy信号加入调试窗口设置采样深度为1024触发条件设为cs_n下降沿。这里分享一个经常踩的坑ILA的采样时钟必须比被测信号频率高至少一个数量级。如果你用系统时钟100MHz作为ILA采样时钟去抓10MHz的SCLK信号采样点之间会有100ns的间隔能看清基本的电平变化但如果要分析数据在SCLK边沿附近是否建立稳定就有点力不从心了。另外ILA触发条件不要设置得太复杂。我见过有人设置cs_n下降沿 AND mosi上升沿 AND done下降沿结果怎么都触发不了因为多个条件同时发生的时刻根本不存在。最简单的做法是只触发cs_n下降沿然后在波形里通过miso和mosi的电平变化手动判断数据位置。6.3 实测中遇到的典型案例SPI数据错位问题有一次调试SPI Flash的读取操作发现读出来的数据和预期完全对不上——不是全0也不是全F而是看起来像“错位”的乱码。用ILA抓了波形以后发现一个很有意思的现象MISO上的数据在SCLK上升沿到来时处于高阻状态上升沿之后才慢慢变成有效数据。这个问题的根源是Flash芯片的输出延迟时间t_V太大超过了主机的建立时间要求。当时我的主机在上升沿采样但Flash的数据在上升沿之后才稳定所以采到了尚未稳定的电平。解决方案有两种一是降低SCLK频率给从机更多的时间来响应二是修改采样相位在下降沿采样。第二种方案效果更明显因为下降沿到来的时候数据已经稳定了半个周期。这个案例说明一个道理如果数据出现系统性错位优先看波形不要怀疑代码逻辑。代码的问题通常导致的是完全无输出或者固定错误而不是这种看起来有规律的错位。7. 工程实践中的常见坑从学到的教训中避雷SPI看起来简单但实际工程里坑非常多。把我在多个项目中遇到过的典型问题整理如下都是真实踩过、修复过、又验证过的经验。7.1 时钟极性配置错误数据全对的假象这个坑很隐蔽。在某些情况下CPOL和CPHA配置错误并不会导致通信完全失败而是会得到“部分数据正确、部分数据错误”的结果。比如用CPOL1 CPHA1去尝试读取一颗CPOL0 CPHA0的Flash如果Flash内部对时序不敏感可能会正常响应读ID命令但读取数据时出现字节顺序颠倒。我当时排查这个问题花了半天时间最后是用逻辑分析仪逐位比对发现数据的字节顺序调换了而不是位顺序调换。原因是SPI主机在错误的边沿采样每一次都晚了一个节拍导致帧边界错位。这里的教训是拿到一颗新芯片一定要先发送读ID或读状态寄存器命令验证通信的正确性然后再发送实际数据命令。读ID命令的返回数据是固定的可以快速判断时序配置是否正确。7.2 CS信号的毛刺与复位期间的电平问题FPGA上电复位期间所有信号都处于未定义状态。如果此时CS恰好处于低电平而从机是Flash或者Sensor就可能在复位期间被误触发一次传输导致内部状态混乱。解决办法是在顶层模块加一个上电复位延时reg [15:0] rst_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_cnt 0; end else if (rst_cnt ! 16hFFFF) begin rst_cnt rst_cnt 1b1; end end assign sys_rst_n rst_n (rst_cnt 16hFFFF);同时在SPI主机模块里cs_n在复位期间必须被强制拉高不能是z状态或者悬空。Verilog里复位初始化的写法要保证cs_n在所有分支下都有确定的赋值。7.3 系统时钟与SPI时钟的频率匹配问题很多初学者把CLK_DIV参数理解错了。CLK_DIV代表的是“系统时钟频率/SPI时钟频率”的比值这个值直接决定了SCLK的时间精度。但要注意如果CLK_DIV取了奇数SCLK的高低电平宽度就不相等时序看起来会有轻微的占空比失真。例如系统时钟40MHzSPI时钟10MHz那么CLK_DIV4高低电平各占两个系统时钟周期。但如果系统时钟30MHzSPI时钟10MHzCLK_DIV3那么低电平一个周期、高电平两个周期占空比就变成了33.3%/66.7%。大多数SPI从机对占空比的要求是40%~60%所以奇数分频也不是不能用但一定要确认芯片手册。如果芯片对占空比要求严格建议在系统时钟和SPI时钟之间加一个PLL做一个整分频或者用DCM产生一个正好两倍于SPI时钟的时钟以保证50%的占空比。7.4 MISO线上拉电阻与空闲状态的确定SPI总线在空闲状态下MISO的电平取决于从机在CS为高时输出什么。有些芯片设计为高阻态需要外部上拉或下拉电阻来确定电平有些芯片则始终驱动MISO线。如果MISO悬空FPGA内部采样到的电平就是不确定的可能导致状态机误判。工程建议是在MISO信号上加一个10kΩ上拉电阻这样空闲状态默认为高电平。注意选值不要太低否则会增加功耗而且多个从机共用MISO时上拉电阻太低会影响信号驱动能力。8. 从SPI到其它协议的演进路径SPI作为最基础的同步串行协议掌握之后可以很快上手其它类似协议的实现。这里聊聊几个值得关注的方向对做FPGA开发的工程师很有参考价值。8.1 双线高速模式QSPI与OSPI在NOR Flash和部分存储器的应用场景中标准SPI的8位单线传输带宽已经成了瓶颈。QSPI在标准四线基础上增加了IO2和IO3两根数据线可以实现x4模式。OSPI则进一步扩展到了x8模式。FPGA实现QSPI控制器并不难核心是在标准SPI状态机的基础上增加模式切换逻辑命令阶段使用单线、数据阶段切换到4线并行。关键点在于切换的时候要保证所有IO方向的正确性尤其在读操作时IO0~IO3在命令阶段是输入在数据阶段要切换为输出。这个方向切换如果处理不当会造成总线冲突。8.2 与DMA结合的高吞吐路径如果SPI通信数据量比较大比如每秒超过几MB单纯靠CPU中断交互的效率就太低。FPGA的方案通常是在SPI控制器后面挂一个FIFO接收到的数据先缓存到FIFO然后通过DMA搬移到内存。我在一个项目里就是这样做的SPI接收数据直接写入AXI-Stream FIFODMA把FIFO里的数据搬到DDR整个链路CPU零参与。实测下来SPI时钟50MHz、8位数据宽度时吞吐率可以达到接近6.25MB/sCPU占用率几乎可以忽略。如果只是简单的MCUFPGA结构也可以用一组寄存器缓冲硬件标志位来实现“伪DMA”效果外部主机通过SPI写入数据FPGA内产生中断标志处理器在空闲时读取。8.3 SPI与FMC/AMBA总线桥接的实践经验STM32H743处理器的FMC接口和Xilinx FPGA组合在工业产品里很常见。FMC本身支持并行NOR接口时序可以模拟成外扩的并行总线接口把FPGA内部的寄存器空间映射到MCU的地址空间里。相比SPIFMC的优势是带宽高、时延低缺点是引脚占用多。有些项目两种接口都会用上高速数据通道走FMC低速配置通道走SPI。如果做类似设计要注意FMC的时序参数和FPGA内部逻辑的匹配问题。FMC接口的读信号和写信号时序相对规范但在FPGA侧需要做地址译码和读写控制逻辑。常见的问题是MCU端配置了异步模式访问时等待周期太长导致FPGA内部的握手信号超时。建议先用逻辑分析仪抓一次FMC读写的实际波形确认地址建立时间、数据保持时间的具体数值再在FPGA代码里匹配这些时序。9. 项目复盘与工程师的三个习惯写到这里整个SPI通信FPGA实现的核心内容基本讲完了。从协议原理、RTL实现、仿真验证到上板调试再到协议演进的方向这是一条完整的认知链路也是我从多个项目中慢慢积累起来的。如果让我总结在这类基础外设控制模块开发中最重要的经验大概就三条。第一协议参数的确认永远是最优先的。不要根据“大多数芯片都是Mode 0”的经验去猜测一定以数据手册的时序图为准。花十分钟把时序图看明白后面能省一天的调试时间。第二波形验证的效率远高于逻辑走读。代码写完之后不管是仿真还是上板尽快把波形抓出来看眼见为实。我见过太多工程师对着代码一行行“人肉仿真”效率低、容易遗漏而实际上只要仿真跑一遍很多逻辑漏洞立刻现形。第三模块设计要控制复杂度不要为了“通用”而“通用”。SPI控制器的本质是一个状态机而不是一个通用的DMA。把不太用到的功能去掉把核心逻辑做精简不仅代码量小、容易维护时序收敛也更轻松。SPI协议并不复杂但每一次和真实硬件打交道的过程都可能暴露出新的问题。希望这篇博客里的实现代码和调试思路能帮你少走一些弯路。也欢迎在实际项目中遇到类似问题时回来对照一下其中的排查流程应该会有点帮助。
返回列表