FPGA实战:手写可配置SPI主机控制器,搞定多外设时序
2026/9/7 13:34:37 网站建设 项目流程

直接上一段近期做过的板子:主控是FPGA,板上同时挂了NOR Flash、温湿度传感器和一个LCD屏幕,三个外设全部走SPI,只是速率和时序要求完全不同。一开始想偷懒用逻辑分析仪打出波形、再用状态机拼一个收发流程,结果发现纯靠“试”根本应付不了不同外设的时序差异。后来老老实实把SPI协议的细节捋了一遍,在FPGA里写了一个可配置的SPI主机控制器,才把整个通信链路理顺。

这篇不是教科书式的SPI协议科普,我尽量用做项目的思路来写:先讲我在FPGA里设计SPI模块时的整体考虑,再拆解寄存器级的关键细节,然后给出一套可以直接参考的Verilog实现框架,最后聊聊常见的坑和调试手段。适合刚接触FPGA、想自己写一个SPI主机或者从机模块的读者,也适合已经在用IP核、但想知道内部到底发生了什么的人。

1. 为什么在FPGA里自己写SPI,而不是直接调IP核

FPGA开发里,SPI算是最常见的低速接口之一。Xilinx和Intel(原Altera)的FPGA都自带SPI IP核,配置一下就能用,按理说没必要自己写,但这个结论只对了一半——IP核确实可靠,但它往往是一个“通用解”,不一定适配你的具体场景。

我这次遇到的痛点有三个:第一,IP核的中断和FIFO机制对简单外设来说太笨重,很多时候只想“发一个字节、收一个字节”,但IP核会引入一堆控制逻辑;第二,三个外设的SPI参数不一样,Flash要求Mode 0或Mode 3,传感器又要求极低的SCLK频率,IP核的配置是静态的,运行中动态调整比较麻烦;第三,我需要把SPI时序精确到可以预测的程度,比如在某个具体时刻拉高片选、在另一个时刻捕获数据,这样才好跟其他逻辑做时序协同。

自己写SPI模块,本质上是在FPGA里用Verilog搭一个“可编程的时序发生器”。你控制了SCLK的产生、数据移位的时机、片选的拉高拉低,就能精确匹配各种外设的时序要求。而且SPI协议本身逻辑简单,不像PCIe或者DDR那样需要处理大量细节,完全适合手写。你不需要依赖厂商IP,代码放哪儿都能用,移植性特别好。

当然,这不是说IP核一无是处。生产级项目追求稳定性和开发效率,IP核是不二选择。但如果你是做FPGA学习、做原型验证,或者像我这样需要高度定制SPI时序,手写一个模块反而是更灵活、更能提升内功的做法。而且有了自己写的SPI主机控制器,再去看任何带SPI接口的外设手册,你会觉得那些时序图突然变得“可读”了。

2. SPI协议核心细节与模式选择,读懂时序图才能写对代码

2.1 四种传输模式的区别,不只是“上升沿采样”这么简单

SPI协议需要搞清楚的第一件事,就是CPOL和CPHA这两个参数。CPOL决定SCLK空闲时的电平,CPHA决定数据在哪个边沿被采样。四个模式的组合如下:

模式CPOLCPHA空闲SCLK电平数据采样边沿数据变化边沿
Mode 000上升沿下降沿
Mode 101下降沿上升沿
Mode 210下降沿上升沿
Mode 311上升沿下降沿

理解这个表的时候,很多刚接触的人会被“采样边沿”和“变化边沿”绕晕。我习惯用一个生活化的类比:发送端(主机)是在“说话的间隙换气”,接收端是在“别人说完一句话的时候记下来”。采样边沿是接收方记笔记的时刻,变化边沿是发送方改变电平的时刻。这两个边沿必须是相反的,数据才能稳定地被捕获——如果同一边沿既变化又采样,那采到的就是不确定状态。

实际项目里,Mode 0用得最多,大多数SPI Flash、SD卡、传感器默认都支持Mode 0。Mode 3也常见,比如W25Q系列Flash允许Mode 0或Mode 3。所以我的SPI控制器里把CPOL和CPHA做成了可配置参数,这样不管是接哪种外设,只需要改两个比特就行,不用动核心逻辑。

2.2 片选信号与字节序,最容易出错的两个隐藏点

片选(CS/SS)是SPI协议里容易被低估的部分。片选必须在外设“准备好接收”之前拉低,并且在整个传输过程中保持低电平。片选信号的建立时间(CS下降沿到第一个SCLK边沿)和保持时间(最后一个SCLK边沿到CS上升沿)通常都会写在外设手册里,最常见的值是几十纳秒。如果FPGA的SCLK速率本来就低,这个问题不明显,但一旦SCLK跑到几十兆赫兹,就必须用逻辑去约束这两个时间,否则会出现外设偶尔无响应的情况。

字节序(MSB first还是LSB first)同样容易踩坑。大多数SPI外设都是MSB first,但个别传感器、LCD控制器可能用LSB first。这个参数如果不检查,就会出现“读写都成功、但数据全反了”的现象。我的模块里同样把这个做成参数,默认MSB,必要时可以切换。

另外一个基础但常被忽略的点:SPI是同步通信,SCLK由主机产生,从机不产生时钟,所以主从双方不需要约定波特率——SCLK多快,通信就有多快。这一点跟UART的“异步”机制有本质区别,也决定了SPI在FPGA里实现起来要简单得多:你不需要做波特率同步和过采样,只需要保证数据在SCLK边沿稳定。

2.3 三线SPI和四线SPI,双向数据线怎么处理

标准SPI是四线:SCLK、MOSI、MISO、CS。但有些场景会用三线制,把MOSI和MISO合并成一根双向数据线(SDA),比如某些温湿度传感器,或者为了节省引脚而设计的LCD屏。三线制在FPGA里实现时,难点在于方向切换:主机发送时要让数据线输出,接收时要把数据线置为高阻输入,并且这个切换必须和SCLK边沿对齐。

我的做法是通过三态门来控制数据线方向。当一个字节传输的发送阶段,SDA作为输出;最后一个位传输结束后,立刻切换成输入,准备接收从机的响应。切换的时机要严格对齐SCLK的边沿,否则会出现总线冲突或采样数据不稳定的问题。如果项目没有引脚数量的硬约束,我建议优先用四线制——少一个方向切换的逻辑,波形也更干净,排查问题容易得多。

3. Verilog实现SPI主机控制器,状态机是核心

3.1 顶层设计思路,可配置参数如何规划

在写代码之前,先把模块的对外接口和配置参数想清楚。我会把SPI主机控制器拆成两层:顶层负责对外接口和字节级控制,底层负责SCLK生成和移位寄存器操作。

顶层参数至少需要这些:

  • CLK_FREQ:FPGA系统时钟频率,用于计算分频系数;
  • SCLK_FREQ:目标SPI时钟频率,可以据此自动算出分频系数;
  • CPOL/CPHA:模式选择;
  • DATA_WIDTH:单次传输位宽,常见8位或16位;
  • MSB_FIRST:字节序。

对外接口包括:启动信号(start)、发送数据(tx_data)、接收数据(rx_data)、忙信号(busy),以及物理引脚SCLK、MOSI、MISO、CS。这样一来,上层的状态机不需要关心SCLK怎么产生、数据什么时候移位,只需要发起传输,然后等待完成即可。

3.2 分频器与SCLK边沿检测,时钟域处理的关键

FPGA系统时钟通常是50MHz或100MHz,而SPI需要更慢的时钟,最直接的做法是分频。但有一个坑:绝不能用一个“由计数器分频出来的时钟信号”作为时序逻辑的时钟,而应该用系统时钟采样一个“SCLK电平跳变指示信号”。原因很简单,FPGA里的时钟网络是专用的,分频信号如果进入普通逻辑电路,会产生时钟偏斜和毛刺,导致数据传输不稳定。

具体做法是:用计数器产生一个周期性的脉冲(marker),然后在系统时钟的上升沿根据这个脉冲去翻转SCLK寄存器。当翻转发生时,产生一个sclk_risesclk_fall脉冲信号,SPI状态机就靠这两个脉冲来推进。这样所有逻辑都运行在系统时钟域里,时序分析和约束都很好做。

这里的参数计算逻辑是:div_cnt = CLK_FREQ / (2 * SCLK_FREQ) - 1。比如系统时钟50MHz、目标SCLK 1MHz,那么分频计数器的周期是25个时钟周期,半周期就是25拍翻转一次SCLK。为什么是2倍?因为一个完整的SCLK周期包含一个高电平和一个低电平,各占一半,所以要用2倍频来算。

3.3 状态机与数据移位,写一个可复用的SPI主机模块

下面这段代码是我实际项目中SPI主机控制器的简化版本,核心逻辑已经保留了,可以直接参考。

module spi_master #( parameter CLK_FREQ = 50_000_000, parameter SCLK_FREQ = 1_000_000, parameter CPOL = 1'b0, parameter CPHA = 1'b0, parameter DATA_WIDTH = 8, parameter MSB_FIRST = 1'b1 )( 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 sclk, output reg mosi, input wire miso, output reg cs_n ); localparam IDLE = 3'd0; localparam PRE_DELAY = 3'd1; localparam SHIFT = 3'd2; localparam POST_DELAY= 3'd3; localparam DONE = 3'd4; reg [7:0] cnt; reg [7:0] bit_cnt; reg [DATA_WIDTH-1:0] tx_shift; reg [DATA_WIDTH-1:0] rx_shift; reg [2:0] state; wire sclk_rise; wire sclk_fall; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 8'd0; sclk <= CPOL; end else if (state == IDLE) begin sclk <= CPOL; cnt <= 8'd0; end else if (cnt == ((CLK_FREQ / SCLK_FREQ / 2) - 1)) begin cnt <= 8'd0; sclk <= ~sclk; end else begin cnt <= cnt + 1'b1; end end assign sclk_rise = sclk && (cnt == ((CLK_FREQ / SCLK_FREQ / 2) - 1)) && state != IDLE; assign sclk_fall = ~sclk && (cnt == ((CLK_FREQ / SCLK_FREQ / 2) - 1)) && state != IDLE; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cs_n <= 1'b1; busy <= 1'b0; mosi <= 1'b0; bit_cnt <= 8'd0; rx_data <= {DATA_WIDTH{1'b0}}; end else begin case (state) IDLE: begin if (start) begin cs_n <= 1'b0; tx_shift <= tx_data; rx_shift <= {DATA_WIDTH{1'b0}}; bit_cnt <= 8'd0; busy <= 1'b1; state <= PRE_DELAY; end end PRE_DELAY: begin if (CPHA == 1'b0) begin mosi <= MSB_FIRST ? tx_shift[DATA_WIDTH-1] : tx_shift[0]; end if (sclk_fall) begin state <= SHIFT; end end SHIFT: begin if ((CPHA == 1'b0) && sclk_rise) begin rx_shift <= {rx_shift[DATA_WIDTH-2:0], miso}; if (MSB_FIRST) tx_shift <= {tx_shift[DATA_WIDTH-2:0], 1'b0}; else tx_shift <= {1'b0, tx_shift[DATA_WIDTH-1:1]}; bit_cnt <= bit_cnt + 1'b1; if (bit_cnt == DATA_WIDTH - 1) state <= POST_DELAY; end else if ((CPHA == 1'b1) && sclk_fall) begin rx_shift <= {rx_shift[DATA_WIDTH-2:0], miso}; if (MSB_FIRST) mosi <= tx_shift[DATA_WIDTH-2]; else mosi <= tx_shift[DATA_WIDTH-1]; bit_cnt <= bit_cnt + 1'b1; if (bit_cnt == DATA_WIDTH - 1) state <= POST_DELAY; end end POST_DELAY: begin if (sclk_fall) begin cs_n <= 1'b1; rx_data <= rx_shift; busy <= 1'b0; state <= DONE; end end DONE: begin state <= IDLE; end endcase end end endmodule

这段代码的核心逻辑是:IDLE状态检测到start信号后拉低片选,PRE_DELAY阶段等第一个SCLK边沿到来,SHIFT阶段按位收发数据,POST_DELAY阶段在最后一个SCLK边沿结束后拉高片选,DONE状态回到空闲。sclk_risesclk_fall脉冲是整个状态机的“心跳”,所有动作都对齐到这两个脉冲。

我特别想提醒的是,PRE_DELAY这个状态很容易被忽略。片选拉低之后、第一个SCLK边沿到来之前,外设需要一小段时间来完成内部准备。如果SCLK频率比较低,这个时间自然满足;但如果SCLK频率高,甚至直接跳过了PRE_DELAY,就会出现外设同步失败的问题。留出这一拍,等于给时序留下了安全裕量。

3.4 从机模式的实现思路,与主机模式互补

有些场景下,FPGA需要作为SPI从机,比如从一个MCU接收配置命令。从机模式的核心是:SCLK和片选都由外部主机控制,FPGA只能在SCLK的边沿被动地读写移位寄存器。这意味着你不能自由生成时钟,必须让所有信号都跟外部SCLK同步。

从机模块的设计难点有两个:一是外部SCLK不是FPGA系统时钟域的信号,需要做边沿检测和同步处理,避免亚稳态;二是片选的拉高拉低可能发生在任意时刻,占空比也无法预知,所以不能依赖计数器产生时序,只能纯粹靠边沿触发。

我常用的方案是:先把SCLK和CS打两拍做同步,然后用状态机检测SCLK边沿。当CS拉低后,在第一个有效的采样边沿把数据从移位寄存器送出去,同时把MISO输出使能;当CS拉高时,锁存移位寄存器里的数据作为接收结果。从机和主机相比,逻辑相对简单,但对同步的要求更高,建议先写好主机、打通链路后,再尝试从机。

4. 用自研SPI主机驱动典型外设,Flash、ADC和LCD的实战差别

4.1 W25Q系列Flash,低频慢调一次把时序参数吃透

W25Q系列是我用得最多的SPI Flash。它的标准命令包括读ID(0x9F)、读数据(0x03)、页编程(0x02)、扇区擦除(0x20)等。以读ID为例,主机先发命令字节0x9F和三个地址字节(通常写0x00 00 00),然后继续发时钟,Flash会在MISO上返回3-4个字节的厂商和设备ID。

用FPGA操作W25Q时,最关键的一点是“命令可以连续,字节之间不能随意停顿”。如果片选一旦拉低,整个命令序列必须连续发完,Flash的内部状态机才认得。这意味着每一次读ID、读数据、写数据,都必须做成一个完整的事务,而不是拆成多次字节级传输。所以我的SPI主机控制器除了单字节传输模式,还支持一个“连续传输”接口:在片选维持低电平期间,连续发送一串字节,并在最后一个字节的接收数据返回后,再拉高片选。

页编程操作更考验时序:W25Q页编程前必须先擦除扇区,且每页编程最多写入256字节。如果FPGA逻辑不小心让片选在页编程过程中抖动了一下,整页数据就可能写坏。我的建议是:在应用层做保护,一个页编程事务中,起始地址和字节数都要对齐页边界,跨页的写入要拆成多次事务。

4.2 ADC与传感器的SPI配置,低速场景下的注意事项

传感器和ADC这类外设,SPI速率通常不高,几百kHz到几MHz就够用了。但它们往往有一个共同点:配置寄存器和读取数据是两个不同的操作,而且读数据时可能需要先发送一个“读命令”字节。

我调过一个温度传感器,数据手册要求发送完读命令后,至少等待一个SCLK周期,MISO上才会出现有效数据。这个“等待”在代码里体现为:发送完命令字节后,不立刻拉高片选,而是继续发几个空闲时钟(MOSI保持高阻或0电平),再从MISO上采样。这种细节只能靠仔细读手册加示波器验证,也是我为什么强调“看懂时序图比会写代码更重要”的原因。

另外,很多模拟前端(比如AFE4490这类血氧模拟前端芯片)的SPI寄存器是8位地址加8位数据的格式,而且支持连续的寄存器读写。用自研主机做这类型号时,建议把读写函数抽象成两个底层接口:spi_write_reg(addr, data)spi_read_reg(addr),后续调传感器驱动时就不用再关心SPI层细节了。

4.3 SPI屏幕驱动,数据量大的时候要不要上DMA

LCD屏幕(比如做项目常遇到的ST7789)是SPI外设里比较特殊的一类:初始化配置慢,但刷屏时数据量特别大。如果只靠CPU或者状态机一个字节一个字节地发,刷新一帧画面的时间会非常长。以320x240的屏幕为例,RGB565格式一帧数据大约是320×240×2 = 153600字节。在16MHz的SCLK下,理论最小传输时间是153600×8/16MHz约等于76.8毫秒,实际加上命令开销只会更慢。

在FPGA里优化SPI刷屏,通常有两个方向:一是提高SCLK频率,二是用FIFO缓冲数据、利用连续传输模式。前者受限于屏幕控制器,后者比较实用。我的做法是在SPI主机前面加一个小的异步FIFO,FPGA逻辑把图像数据按行写入FIFO,SPI主机自动从FIFO取数据并发给屏幕,CPU侧只需要负责填充FIFO,不用等每一字节发送完成。实测下来,这样刷屏效率能提高不少,而且逻辑复杂度可控。

如果项目对帧率要求更高,那就要考虑RGB并口屏或者MIPI DSI了——但那就是另一个话题了。SPI屏幕适合的是低成本、低功耗、对帧率不敏感的场景,比如表盘、仪表显示、静态图片轮播等。

5. 硬件片选与软件片选,多设备共享SPI总线的设计策略

5.1 片选管理的两种实现方式,优缺点对比

SPI总线上挂多个外设时,片选的管理方式直接影响系统的可靠性。软件片选是用逻辑代码控制输出,FPGA把CS当作一个普通的GPIO来拉高拉低;硬件片选则通常指把片选信号交给专用的外设控制器或硬件逻辑来管理,在传输开始时自动拉低、结束时自动拉高。

对比项软件片选硬件片选
灵活性高,可任意控制拉低拉高时机低,跟随传输生命周期
风险容易因为代码时序错乱导致误操作由硬件保证,可靠性高
操作成本每个设备都要单独控制CS配置寄存器即可
适用场景协议不确定、需要特殊时序标准读写操作、防误操作

在FPGA自研SPI模块里,我倾向于用软件片选,因为它灵活,可以在需要的时候随时拉低CS,比如我前面说的“发送完读命令后继续发空闲时钟”的场景,这种特殊时序用硬件片选很难做到。但软件片选有个致命陷阱:如果在代码里两个模块同时想控制同一根CS引脚,就可能出现“第1个模块拉低了CS、还没拉高,第2个模块又把它拉高”的竞争状态。这种bug极难排查,因为波形上看CS翻转异常,数据却可能是对的,只有在特定时序才会出错。解决办法是给CS做一个独立的“仲裁模块”,所有操作统一申请、统一释放,相当于引入一个简单的互斥机制。

5.2 多设备总线冲突,如何避免SCLK和MOSI上的数据串扰

当多个SPI设备共享SCLK、MOSI、MISO总线时,最怕的是两个设备的片选同时有效,因为它们的MISO都会驱动总线,造成读数据错误。正常情况下,片选信号保证了同一时刻只有一个设备在总线上响应,但如果某个设备的片选时序有毛刺、或者驱动能力过强,就可能出现串扰。

一个我在SD卡和LCD屏幕共享SPI总线的项目里踩过的实际案例:两块设备挂在同一组SCLK/MOSI/MISO上,单独操作任何一块都正常,但先操作SD卡再切到屏幕时,屏幕偶发白屏。排查后发现是SD卡在被“拉高片选”后,MISO引脚并没有立刻释放成高阻,导致SD卡的数据线和屏幕的数据线在切换瞬间发生了电平竞争。解决方法是:在切换设备之后、发起新事务之前,插入一段至少两个SCLK周期的“总线空闲”时间,让上一设备完全释放总线。这个时间在代码里就相当于在上一次的POST_DELAY和下一次的PRE_DELAY之间加一个延时状态。

另外,如果设备共享同一条MOSI但其中一个设备是漏极开路结构,还需要注意上拉电阻的配置。SPI设备挂在长走线上时,建议在MOSI、MISO、SCLK上各加一个10kΩ或4.7kΩ上拉电阻,能让空闲电平更确定,减少误触发。

5.3 参考一个实际场景:ESP32屏幕与SD卡共享SPI总线

最近有朋友问我,用ESP32时屏幕和SD卡共享SPI总线,到底值不值得折腾。这个问题放在FPGA项目里也一样会遇到:节省引脚和简化布线的诱惑很大,但共享总线会带来调度复杂度和调试难度,尤其在两个设备都需要持续传输数据的场景(比如一边显示界面一边打日志到SD卡)。

我的建议分情况:如果只是初始化时读一次SD卡,之后就专心刷屏幕,共享总线完全可行,切换前加一个总线释放延时就行;但如果要一边刷屏一边读卡,读卡操作可能会打断刷屏,导致画面撕裂,那就不如牺牲两个引脚,把两条SPI总线分开。这个取舍的逻辑其实非常“FPGA”——用资源和布线换时序余量,永远是值得的。

6. 调试与定位问题,工具、方法与实测经验

6.1 示波器和逻辑分析仪的用法,先看时钟再看数据

SPI调试的优先级非常清晰:先查SCLK,再查CS,最后查数据线。如果SCLK没有波形,说明分频逻辑或者状态机可能没跑起来,后面的一切都不用看。如果SCLK有波形但CS没有拉低,可能是启动信号没有到位,或者CS的控制逻辑写错了。

我常用的工具,低速场景用逻辑分析仪就够了,十几块钱的8通道逻辑分析仪配上软件,抓SPI波形非常方便。但当SCLK跑到几十兆赫兹时,逻辑分析仪的采样率可能不够,普通的示波器往往也难看清建立时间和保持时间,这时候更容易靠FPGA内部的在线逻辑分析仪(比如Xilinx ILA)来抓内部信号。ILA的好处是直接观察FPGA内部的寄存器状态,不用关心物理探针带来的负载效应。

一个很实用的技巧:把ILA的采样时钟设成系统时钟,触发条件设为CS下降沿,然后观察CS拉低那个时刻附近,状态机在哪个状态、SCLK的边沿脉冲是否出现、移位寄存器里的数据是否在正确变化。这样基本能定位八成以上SPI通信问题。

6.2 回环测试,验证主机读写通路的最快方法

如果是自己新写的SPI主机,不要一上来就接真实外设,先用一个最简单的回环测试验证通路。把MOSI和MISO在板子上短接(或者通过跳线帽短接),然后让主机发出一个字节,同时把接收到的数据打印出来。如果发出0xA5能收到0xA5,说明移位寄存器的收发链路是对的;如果收不到或者数据错位,说明采样边沿或者数据对齐有问题。

这个测试虽然简单,但能帮你快速区分“SPI主机本身的问题”和“外设时序的问题”。很多时候我调试SPI,发现数据对不上,折腾半天,最后发现是回环测试都没通过——这就白白浪费时间了。其实先跑一回环,能省下大量排查时间。实测中,回环测试通过后,再接真实外设,问题一般就集中在外设的时序匹配上了。

6.3 常见问题速查表,按症状快速定位

下面这个表是我实际项目里总结的高频问题与定位思路,新手可以直接对照排查:

现象可能原因定位方法
SCLK无输出分频计数器没启动、状态机停在IDLE检查start信号,观察状态机状态
CS一直为高CS控制逻辑写反、启动信号未触发检查启动时序,看CS默认赋值
数据接收全0MOSI和MISO接反、从机未驱动总线回环测试,查引脚约束
数据接收错位CPHA配置错误、采样边沿不对调整CPHA,对比手册时序图
某设备偶发无响应CS建立时间不足、总线竞争增加PRE_DELAY,检查CS信号毛刺
读写结果在特定字节出错字节序配置错误确认MSB/LSB,检查移位方向

排查问题有个基本原则:不要同时怀疑多个环节。比如既要调CPHA又怀疑引脚接错,那出了问题根本不知道怪谁。最好是固定一个变量,其他保持不变,一次只改一处,验证通过了再改下一处。这在FPGA调试里尤其重要,因为编译一次时间不短,越是有条理,越能节省时间。

6.4 时序仿真的价值,先跑仿真再上板的习惯必须养成

上板调试之前,花十分钟写一个简单的Testbench做时序仿真,能避免大量低级错误。Testbench里给一个虚拟的从机(简单用一个寄存器回显接收到的字节),然后看SPI主机发出的SCLK、CS、MOSI波形是否符合预期。当时序仿真的波形和外设手册的时序图对上了,再上板测试,基本一遍过。

仿真还能验证一个关键问题:数据是在哪个边沿被采样的。你可以通过仿真波形清楚看到,MOSI上的数据在SCLK上升沿之前是否已经稳定,CS在第一个采样边沿之前是否已经拉低。这些时序关系如果不在仿真阶段确认,上板之后只能靠示波器一点一点量,效率差很多。我个人做SPI控制器的时候,仿真环节占了至少三分之一的时间,但它确实帮我在上板之前消灭了绝大多数逻辑错误。

7. 最后再补充一点个人经验

环节完整收个尾。做到现在,我最大的体会是:SPI协议看着简单,但真正落地到FPGA里,需要考虑的细节比想象中多得多。很多人在学习FPGA时,急于上手图像处理、神经网络加速这些看起来“高级”的方向,却忽略了通信接口这种最基础的能力。但恰恰是SPI、UART、IIC这类接口,决定了你能否顺利地把FPGA和其他器件对接起来。

如果是刚入门FPGA通信开发,我建议从SPI主机控制器开始练手,因为它的协议足够简单,又包含了分频、状态机、边沿检测、跨时钟域处理这些FPGA开发的通用知识点。把这个模块吃透了,再去看UART、IIC,很多东西都是相通的。再往后,你可以试着在这个SPI主机上扩展功能,比如支持连续传输、加FIFO缓冲、改成从机模式,每一步改动都会加深你对着硬件原理的理解。

调试SPI时如果遇到问题,不要急着改代码,先问自己一个问题:外设手册上的时序图、你脑子里的逻辑设计和执行波形上的实际表现,三者是否一致?百分之八十的问题,都出在这三者之间的不一致上。这样定位问题,往往比瞎猜快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询