news 2026/9/8 12:49:40

FPGA实战:从SPI协议原理到Verilog状态机可复用代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA实战:从SPI协议原理到Verilog状态机可复用代码

刚接触FPGA的同事总爱问我,SPI这么简单的4根线,有必要专门当个项目来讲吗?等他们自己在板子上调ADC或者读Flash的时候,对着示波器瞪一小时波形就明白了,SPI协议看着简单,真正在FPGA上用状态机把时序做扎实,还是有不少门道的。我做了几年FPGA开发,SPI几乎是最常用的低速接口,没有之一。ADC读电压、DAC出信号、Flash存参数、温湿度传感器拿数据,甚至不少老式触摸屏模组的驱动,底层全是SPI。这篇就把我基于FPGA实现SPI通信的完整思路、Verilog核心代码、仿真验证和板级调试的避坑经验一次性写清楚,适合刚学FPGA拿SPI练手的朋友,也适合已经在项目里被时序问题折磨、想系统查漏补缺的工程师。

1. SPI协议到底在传什么:4根线的分工与模式选择

1.1 4根线各管什么事,为什么它能全双工

SPI全称Serial Peripheral Interface,串行外设接口,由Motorola发明,后来几乎成了各类传感器、存储芯片和模组的通用标配。它一共4根线,SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。很多人第一次看觉得别扭,为什么收发要分开两根线?恰恰是这4根线的结构,让SPI天然支持全双工,同一时刻既能往从机发数据,也能从从机收数据。对比一下UART,虽然也有收和发两根线,但靠波特率对齐,没有时钟线;I2C更是只有一根SDA数据线,半双工。SPI把时钟单独拉出来,数据方向又分开,效率自然高,协议开销也最小。

我习惯打一个比方:SCLK像一个节拍器,从机和主机跟着同一个拍子读写数据;CS是话筒开关,谁把CS拉低,谁就是当前正在对话的设备;MOSI和MISO是两条方向相反的电话线,主机对着MOSI讲话,从机对着MISO回话。只要节拍器不停,两边就能同时说、同时听。这个比喻对新手特别有用,因为很多人在后期写状态机时,会纠结“数据到底哪个沿采样”“哪个沿发送”,本质就是没理解节拍器和数据线的关系。

FPGA做SPI通信时,最常见的场景是当主机,主动发起读写。比如采集外部ADC,FPGA定时给ADC发转换命令,再读回转换结果;再比如把FPGA里算好的数据写成W25Q系列的串行Flash;还有干脆用SPI接口驱动一块TFT屏幕,刷新一帧图像。另一个常见场景是当从机,比如外部MCU拿FPGA当协处理器,通过SPI往FPGA里写配置寄存器或跑模型参数,这类项目里SPI从机逻辑虽然只有几行,但坑比主机还多,后面会详细说。

1.2 CPOL和CPHA:四种模式怎么选,不会查手册怎么办

SPI最劝退新手的地方,就是它有4种模式,由CPOL和CPHA两个参数决定。CPOL是时钟极性,意思是在没有数据传输的时候,SCLK保持在高电平还是低电平。CPOL=0表示空闲低,CPOL=1表示空闲高。CPHA是时钟相位,决定数据在SCLK的哪个边沿被采样,是第一个边沿采样还是第二个边沿采样。这两个一组合,就得到模式0到模式3。

我不建议死记硬背四种模式的对照表,因为真到了现场,你还是得翻从机的手册,看它的时序图。教你一个实用技巧:打开从机数据手册里的SPI时序图,找到标着Data或MOSI的那条线,看数据是被SCLK的哪个边沿“框住”的。如果图上用箭头标注的是在上升沿采样,并且SCLK空闲时是低电平,那大概率就是模式0;如果SCLK空闲时是高电平,但仍然是第一个边沿采样,那就是模式3。多数情况下,常见Flash、SD卡SPI模式、各种SPI接口的ADC/DAC,都默认支持模式0或模式3,这两种只是极性不同,采样逻辑本质一致。

我自己的经验是,在没有特殊要求时,一律先按模式0来设计,就是CPOL=0、CPHA=0,空闲时SCLK为低,第一个边沿采样。原因很简单,市面上大多数从机都兼容模式0,而且模式0下SCLK空闲低电平,配合片选拉低,逻辑分析仪上波形最好认。如果从机手册明确要求其他模式,改起来也就是把SCLK空闲电平和采样沿换一下,核心状态机不用动。

1.3 SPI对比I2C和UART:什么时候不该用SPI

很多人都纠结过一个问题,同样是板级通信,SPI、I2C、UART到底选谁。我的看法是,选型不是看谁先进,而是看场景。SPI最大的优势是速率高、全双工、协议简单,没有应答位和地址帧这些额外开销,非常适合数据量大、实时性要求高、主机自己掌握节奏的场景,比如图像数据缓存、高速采样数据搬移、Flash写入等。但SPI也有明显短板,引脚占用多,一个从机至少4根线,多从机时每多一个设备就要多一根CS线,而且协议本身没有应答机制,从机出了问题,主机往往不知道。

I2C的优势是只要SDA和SCL两根线就能挂一堆设备,每个设备有自己的7位地址,协议还带应答位,总线层面就能确认从机是否收到。缺点是速率相对SPI低不少,且半双工,数据线方向切换会占用总线周期。如果项目里对引脚数量敏感、需要挂多个从机、数据量又不是特别大,I2C会更合适。UART则完全是另一个维度,它连时钟线都省了,靠两边约定好的波特率收发,适合远距离、点对点、低速的场景,比如调试日志输出、GPS模块数据接收等。

这里顺便回答很多人问的“能不能用IO口模拟三线SPI”。可以,而且我在不少平台上都这么干过。所谓三线SPI,通常是把MOSI和MISO合并成一根双向数据线,省一根线,但代价是不能再全双工,时序也要额外处理总线方向切换,别指望能像四线SPI那样高速跑。FPGA内部如果只是接个温湿度传感器,SCK跑到几百千赫兹,完全可以用普通IO加状态机模拟;如果是要高速读写Flash,最好还是专门写一个SPI控制器,把时序做扎实。

2. FPGA侧方案选型:主机还是从机,片选怎么给

2.1 先想清楚:你的FPGA是Master还是Slave

很多FPGA初学者拿到需求就直接开写代码,其实有一个关键问题必须最优先确认:你这颗FPGA在这条SPI链路上是主机还是从机。这个决定直接影响到整个模块的设计,包括时钟从哪里来、要不要做跨时钟域处理、出错怎么判。别笑,我见过不止一个项目,需求里写着“FPGA读传感器”,结果传感器是SPI主机,FPGA需要被动应答,害得整个方案推倒重来。

FPGA做主机时,SCLK的极性频率和片选全部由自己产生的,所有时序都在同一个时钟域里,设计自由度很高,这也是一般项目里FPGA最常扮演的角色。比如FPGA + ADC采集,FPGA定好采样点,按固定周期拉低CS、发读命令、收数据;FPGA + Flash,则是主机主动发写命令、读命令,整个过程完全由FPGA掌握。

FPGA做从机时,外部MCU或者DSP会把SCLK送进来,这个SCLK是外部异步信号,直接采可能会有亚稳态风险,所以代码里必须先做两级同步或者用专门的引脚约束处理。还要注意从机模式下,数据方向切换必须服从主机节奏,主机没发时钟,从机就不能擅自改变MISO。这个场景在“MCU + FPGA协处理器”架构里很常见,外部MCU用SPI往FPGA里灌参数,或者读回FPGA算好的结果,相当于SPI是二者之间的寄存器通道。

如果你手上要接的是高速总线,比如ST系列的FMC、PCIE这类,那就不是SPI的范畴了,别混着用。SPI规规矩矩做低速控制面和配置面数据通道就好,非要拿它去搬大数据流,只能把自己坑死。

2.2 硬件片选与软件片选,FPGA里到底用哪种

SPI片选信号的实现方式,业内分成两派,一派叫硬件片选,一派叫软件片选。在单片机平台上,硬件片选通常是SPI外设自动控制的,主机拉低CS,传输完再自动拉高;软件片选则是用一个普通GPIO,在软件里手动拉低拉高。

但到了FPGA里,情况有点不一样。FPGA大多没有固定的SPI硬件外设,即使Xilinx、Intel提供了SPI IP核,最终片选信号落到引脚上,本质上还是内部逻辑在控制,只不过这个“逻辑”是IP核帮你写好的。所以FPGA里讨论硬件片选和软件片选,我理解成两种思路:一种是把片选交给SPI控制器内部自动管理,另一种是在使用SPI控制器的上层逻辑里手动拉低、拉高CS。我个人的建议是,自己写RTL实现SPI主机时,一定要用软件片选思路,把CS信号纳入状态机统一控制。

为什么?因为自动片选太死板了。真实世界里,从机的行为千奇百怪,有的从机要求CS拉低后先等几个时钟,SCLK才开始翻转;有的要求CS拉低的同时切换双向往返模式;有的灌完命令之后不允许立刻拉高CS,要多吃几个时钟。如果片选完全由控制器自动管理,遇到这些特殊情况你得绞尽脑汁给IP核配参数,还不如直接拉一根IO出来手动控制来得爽快。更关键的是,手动控制CS可以保证“先拉低CS、等SCLK稳定后开始传输、传输结束先撤SCLK再拉高CS”这个顺序一点也不乱,从根本上避开片选毛刺和时序竞争。

2.3 核心架构:数据通路加状态机加波特率分频

FPGA实现SPI主机的核心架构,我一般分成三个模块。第一个是分频模块,把系统时钟分频出目标SCLK频率;第二个是移位数据通路,把要发的数据从并行转串行,把收到的串行数据从串行转并行;第三个是主状态机,负责组织CS、SCLK、MOSI、MISO的时序关系。对简单项目,这三个模块可以揉在一个文件里,但逻辑上要分开想。

分频参数怎么定,这里给一个公式:SCLK频率等于系统时钟频率除以二倍分频系数,也就是F_sclk = F_clk / (2 * N)。这个二倍是因为SCLK一个完整周期需要翻转两次。举个例子,系统时钟50MHz,想得到10MHz的SCLK,N = 50 / (2 * 10) = 2.5,但N只能取整数,取2的话SCLK是12.5MHz,取3的话SCLK约8.33MHz。所以实际选择分频系数时,不能只盯着想要的理想频率,还得看从机支持的最高SCLK是多少,然后向下取能取到的最大值。保守一点的做法是把SCLK降一半,很多偶发通信错误都是SCLK跑太快导致的,后面详细说。

数据通路的设计也讲究。发送路径上,用一个移位寄存器,发送时每次都把最高位移到MOSI上,低位补零;接收路径上,每个时钟采样MISO,把采到的bit不断从最低位移入接收寄存器。这样一套下来,一个状态机同时干了两件事,在同一个时钟沿既移出数据又移入数据,全双工就实现了。这也是为什么SPI在FPGA里实现起来比UART还轻松,UART要处理起始位停止位波特率误差,SPI只要跟着SCLK走就行。

3. Verilog实现:可复用的SPI Master核心代码

3.1 模块接口定义和参数设计

下面这份代码是我在实际项目里经常用的SPI Master核心逻辑,删掉了业务相关的东西,保留最基本的功能。参数方面,CLK_DIV用于设置分频系数,SCLK频率等于clk除以二倍的CLK_DIV;DATA_WIDTH默认8位,需要传16位、32位时直接改参数就行,bit_cnt的宽度我按最大支持到16位设计,如果位宽更大,记得先把bit_cnt扩宽。

module spi_master #( parameter CLK_DIV = 8, // SCK = clk / (2*CLK_DIV) 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 cs_n, output reg mosi, input wire miso ); localparam IDLE = 2'd0; localparam START = 2'd1; localparam SHIFT = 2'd2; localparam STOP = 2'd3; reg [1:0] state; reg [7:0] cnt; reg [3:0] bit_cnt; reg [DATA_WIDTH-1:0] sh_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; done <= 1'b0; cs_n <= 1'b1; sclk <= 1'b0; mosi <= 1'b0; rx_data <= {DATA_WIDTH{1'b0}}; cnt <= 8'd0; bit_cnt <= 4'd0; end else begin done <= 1'b0; case (state) IDLE: begin if (start) begin state <= START; sh_reg <= tx_data; cs_n <= 1'b0; end end START: begin cnt <= 8'd0; bit_cnt <= 4'd0; sclk <= 1'b0; state <= SHIFT; end SHIFT: begin // 低电平期间先把数据准备好 if (cnt == 8'd0) begin mosi <= sh_reg[DATA_WIDTH-1]; end // 拉高SCK,从机在上升沿采样MOSI if (cnt == CLK_DIV - 1) begin sclk <= 1'b1; end // SCK高电平中段,采样MISO if (cnt == CLK_DIV) begin rx_data <= {rx_data[DATA_WIDTH-2:0], miso}; end // 拉低SCK,移位并切换bit计数 if (cnt == 2*CLK_DIV - 1) begin sclk <= 1'b0; sh_reg <= {sh_reg[DATA_WIDTH-2:0], 1'b0}; if (bit_cnt == DATA_WIDTH - 1) begin state <= STOP; end else begin bit_cnt <= bit_cnt + 1; end cnt <= 8'd0; end else begin cnt <= cnt + 1; end end STOP: begin cs_n <= 1'b1; done <= 1'b1; state <= IDLE; end endcase end end endmodule

这份代码主要在FPGA做主机时使用,模式为CPOL=0、CPHA=0。如果你要接的从机要求模式3,也就是空闲时SCLK为高,逻辑并不复杂,把IDLE和START状态下sclk的默认值改成1,再把拉高拉低的时机对掉,基本就能适配。

3.2 状态机设计思路和关键条件

整个状态机只有四个状态,IDLE、START、SHIFT、STOP,但每个状态都有它存在的理由,不是随随便便定的。

IDLE状态下,CS保持高电平,SCLK稳定在低电平。当外部输入start信号拉高一拍时,模块把tx_data锁存到移位寄存器,同时拉低CS。这个先锁存数据、再拉低CS的顺序非常重要,保证从机在被选中之前,MOSI上已经准备好数据了,不会出现CS拉低了、数据线上还是高阻或者旧数据的现象。

START状态只有一拍,这一拍的主要目的是整理内部计数器,把cnt和bit_cnt清零,同时强制把SCLK拉低。别小看这一拍,如果没有它,上一次传输结束时SCLK可能正好停在高电平,下一次一进SHIFT就开始拉高,两个周期会被吃掉一个沿,整个时序就乱了。在SHIFT状态里,cnt从0计到2CLK_DIV-1,正好覆盖一个完整的SCLK周期。cnt为0时更新MOSI,cnt等于CLK_DIV-1时拉高SCK,cnt等于CLK_DIV时采样MISO,cnt等于2CLK_DIV-1时拉低SCK并移出下一位。这样一个bit一个bit地循环,直到bit_cnt等于DATA_WIDTH减去1,说明最后一个bit已经处理完,状态转到STOP。

STOP状态拉高CS并输出一个done脉冲,通知上层模块本次传输结束。这里要提醒一下,done信号我设计成只拉高一个时钟周期,如果上层逻辑需要持续状态,自己再打一拍寄存。

3.3 为什么要在低电平期间准备数据,高电平期间采样

代码里有几个细节,新手经常看不懂,我单独抽出来讲。第一,为什么在cnt等于0的时候就把MOSI更新掉,而不是等SCL拉高前再更新?因为数据在电平变化之前需要一段建立时间。如果我把MOSI更新和SCLK拉高放在同一个时钟沿,虽然代码里看着没问题,但实际电路里触发器的输出跳变需要时间,SCLK和MOSI同时翻转,从机极有可能采到一个毛刺。把MOSI更新提前到cnt为0,相当于给了差不多一个完整低电平周期的时间让数据稳定,从机在上升沿采样时看到的数据早就稳如老狗了。

第二,为什么采样MISO要放在cnt等于CLK_DIV的位置,而不是SCLK上升沿?在模式0下,数据实际上是在SCLK上升沿被采样,但MISO从从机的输出引脚传到FPGA的输入引脚,是有走线延迟和从机内部延时的。外部主机想在上升沿那一瞬间采到稳定的MISO,要求从机内部电路必须又快又准。真实项目中这并不总能保证。我把采样点往后推到SCLK高电平正中间,也就是cnt等于CLK_DIV的位置,相当于留出了半个时钟周期的裕量,扫描传感器和普通存储芯片完全够用。如果SCL跑得特别高,比如超过50MHz,这个采样点还可以再往后调,或者用IODDR这类专门的接口原语,不过那就是另一个话题了。

第三,移位操作为什么放在SCLK拉低之后。SCLK下降沿之后,总线进入新的bit周期,这时候移出下一位,为下一个上升沿做准备,逻辑上正好形成一个闭环。你先移后拉高会出问题,先拉高再移位又会把当前位的最后一个bit搞乱,这个顺序别换。

3.4 用IP核还是自己写RTL,边界在哪里

很多用Xilinx或者Intel平台的人会问,明明有SPI IP核,为什么还要自己写RTL。我的回答是,能用IP核的场景肯定用IP核,省时省力,官方维护,文档齐全。但有几类情况,我强烈建议自己写。

第一类是块资源紧缺的时候。SPI控制器自带FIFO、寄存器组、中断逻辑,在MCU里很省心,但在FPGA里一套完整SPI IP核消耗的逻辑资源往往比手写一个状态机大得多,如果只是简单接个传感器,完全没必要。第二类是协议非标准的时候。有些从机对时序要求很奇葩,比如要求CS在数据发送完成后再拉高两个时钟,或者命令和数据的SCLK相位不一样,IP核配半天配不出来,手写反而改两行就完事。第三类是学习阶段。自己写一遍SPI Master,你对时序、时序约束、跨时钟域的认知会有一个质的提升,这个收益不是省几天开发时间能比的。

当然,如果项目里要接的从机数量很多,SCLK频率要求很高,或者希望有一个统一的总线接口方便后续复用,那用IP核也不亏。现在不管是Xilinx还是Intel的SPI IP,都支持配置成主机或从机模式,也有完整的AXI接口,适合挂在微处理器软核或者PicoBlaze这类片上系统里。只是用过IP核的人都知道,调IP核参数有时候比写代码还麻烦,尤其是片选行为、传输位宽这些细节,每个版本都可能有点差异。

4. 仿真验证与板级调试:我的完整流程

4.1 Testbench怎么搭:给SPI Master喂数据看波形

写代码不仿真等于裸奔,尤其是SPI这种时序敏感的逻辑,直接上板出问题根本不知道是代码问题还是接线问题。所以我习惯先写一个简单testbench,把主机模块实例化,模拟一个从机行为,检查波形对不对。

testbench的核心思路是:给start信号拉高一拍,送入一个tx_data,然后在从机侧模拟MISO数据变化。模拟从机有个技巧,不要试图在每个SCLK上升沿改变MISO,那会和真实从机的行为不符。大多数SPI从机其实是接着主机SCLK的第一个沿把数据打出来,在第二个沿被主机采走,我们只要模拟得像个真从机就行。我一般会在sclk的下降沿更新MISO,这样主机在高电平中段采数时,采到的值正好是更新完稳定后的值,波形和真实情况基本一致。

module tb_spi_master(); reg clk = 0; reg rst_n = 0; reg start = 0; reg [7:0] tx_data = 8'hA5; reg miso = 1'b0; wire [7:0] rx_data; wire done, sclk, cs_n, mosi; spi_master #( .CLK_DIV(4), .DATA_WIDTH(8) ) u_dut ( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .rx_data(rx_data), .done(done), .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso) ); always #5 clk = ~clk; initial begin repeat (10) @(posedge clk); rst_n = 1'b1; @(posedge clk); start = 1'b1; @(posedge clk); start = 1'b0; // 等传输结束 wait (done == 1'b1); @(posedge clk); $display("rx_data = %h", rx_data); $finish; end endmodule

仿真波形出来之后,主要看四件事。一看CS拉低之后过了多久SCLK才开始翻转,确保从机来得及准备。二看MOSI上第一位是不是tx_data的最高位,并且是在SCLK上升沿之前就稳定了。三看MISO采样时刻是不是落在SCLK高电平中间,如果啱就说明时钟太密需要重新调分频。四看传输结束时CS是不是等到最后一个bit发完才拉高的。这四点全过,基本就能烧板了。

4.2 板级调试三板斧:ILA、逻辑分析仪、串口打印

仿真过了不代表上板就正常。板级调试我一般按三步走,第一步用芯片厂商自带的逻辑分析仪核,Xilinx的ILA或者Intel的SignalTap,把sclk、cs_n、mosi、miso、done这些内部信号引出来看。这几乎是FPGA调试的标配操作,不用额外接线,占一点逻辑资源,但能看到FPGA内部的真实时序,比外面拿示波器戳引脚强太多。

第二步,接一个真实从机,先做一个最简单的读操作。比如接W25Q系列Flash,发0x9F命令读JEDEC ID,正常应该能回三个固定字节,例如0xEF、0x40、0x18。如果读回来的值和预期不符,直接把ILA抓到的波形对照数据手册看,是命令没发对、时钟极性反了,还是MISO时序不对,一目了然。读ID是一个极其有效的自检操作,因为它不需要复杂的初始化,Flash上电就能读,数据固定,一旦成功就说明SPI通路基本通了。

第三步,如果FPGA把数据读回来了但不确定对不对,可以再接一路UART,把SPI收下来的数据通过串口打印到电脑上看。这个方法特别适合调试传感器,比如读温度寄存器,把原始值打成十六进制,自己心里换算一下就知道对不对了。串口这里只要用最普通的UART发送模块,不要在这时候纠结波特率精度,9600或者115200都行,反正只是看数据。

4.3 时序约束和跨时钟域的基本意识

纯逻辑实现SPI主机时,SCK是内部产生的,时序约束压力不大,你只要保证系统时钟的约束正确,SCK波形天然跟着系统时钟走。真正要注意的是SPI从机模式。外部主机把异步的SCLK送进FPGA,这个信号在FPGA内部采样时,必须做跨时钟域处理,最简单的办法是加两级同步器,先用系统时钟打两拍,再进状态机。直接拿异步信号做触发器的时钟或者复位,大概率会出错,这就是常说的亚稳态问题。

顺便解释一下建立时间和保持时间。建立时间是指数据在时钟沿到来之前必须稳定的时间,保持时间是指数据在时钟沿之后必须继续保持的时间。SPI从机模式下,外部主机给的SCLK和MISO之间是有相对时序关系的,FPGA内部采样MISO时,如果SCLK采样沿距离MISO变化沿太近,就可能违反建立时间或保持时间,采到不确定的值。遇到这种情况,我会优先把SPI时钟降下来,因为降低SCLK等于拉长了建立和保持窗口,多数亚稳态问题都能缓解。其次是在采样路径上加同步器,再不行就调整采样沿,把采样点放到数据最稳定的地方。

5. 常见问题与排查技巧实录

5.1 症状对照表:从现象定位原因

SPI调试中踩过的坑实在太多了,我整理了一张症状对照表,遇到问题直接按图索骥,省得从头一点点翻代码。

现象常见原因排查思路
读回数据全是0MISO没接对、从机没供电、从机CS没拉低示波器量MISO电平,查从机电源和CS配置
读回数据全是1MISO上拉导致空闲高,从机没驱动总线查从机是否真正被选中,是否进入SPI模式
数据整体右移或错位模式选错,采样沿和从机数据变化沿撞在一起对照数据手册确认CPOL/CPHA
首字节偶尔丢失CS拉低后SCLK第一次翻转太快增加START等待周期,或手动延时
偶发一个bit跳变SCK跑太快、干扰严重、同步不足降低SCLK,检查MISO同步器
整个传输超时或卡死状态机没有正常退出,done没返回看ILA状态,检查bit_cnt计数和STOP条件
从机没响应任何命令电平不匹配或从机需要初始化序列查IO口电压域,查从机上电时序

这张表我最想强调的是第一行“读回数据全是0”。很多新手遇到后第一反应是代码写错了,查状态机查半天,结果最后发现是MISO线没焊,或者从机电源没给。所以遇到问题不要急着改代码,先用示波器或者万用表量物理信号,把“信号不存在”和“信号存在但时序不对”区分开。

5.2 电源、电平、上拉这些“低级问题”最坑人

SPI调试里最坑人的往往不是协议本身,而是一堆看着很low的外围问题。第一个就是电平匹配。SPI接口在3.3V器件和1.8V器件之间通信时,电平标准不一致,逻辑1和逻辑0的判断阈值完全不在一个频道上,直接互连轻则信号劣化,重则烧坏IO。FPGA不同bank的IO电平是独立配置的,接到外部从机之前,一定确认对应bank的VCCO和从机供电一致,或者加电平转换芯片。

第二个是上拉电阻问题。CS、MISO这类信号,在从机那边不少是开漏输出或者空闲高阻,如果总线上没有上拉电阻,空闲时MISO电压悬空,容易误判。反过来,如果从机那边带了过强的上拉电阻,而主机用推挽输出驱动同一个引脚,就可能产生冲突,表现为波形上有明显毛刺。

第三个是复位和片选时序。有些从机上电之后需要一段时间才能接受SPI命令,如果FPGA复位一释放立刻就去读,从机可能还没准备好,总线自然不响应。我一般会在上电后加一个至少100毫秒的等待,再开始第一次SPI操作。复位端本身也要干净,不要在FPGA逻辑里用组合逻辑给片选复位,容易在掉电瞬间产生毛刺,把从机内部的寄存器打乱。

5.3 实际项目里我踩过的坑:写Flash中断、SCLK太快、片选毛刺

讲两个我亲身踩过的坑,都是那种排查大半天最后发现原因特别简单的案例。

第一个坑,某项目里FPGA要往W25Q Flash里写一段配置数据,速度一开始跑得很激进,把SCLK定在了从机支持上限附近。结果表现为写完读回来校验总是偶发失败,有时连续写10次错1次,有时写100次对一次都难。一开始怀疑Flash质量,换了芯片还一样;又怀疑写命令时序不对,仔细核对手册发现没问题。最后用ILA抓了现场波形,才发现SCLK高电平期间MISO上的回读数据在边沿附近有抖动,原因是SCLK定位在从机支持上限,留给建立和保持的裕量几乎为零。把分频系数调大一倍,SCLK降到从机上限的一半,问题再没出现过。从那以后我就立了一个规矩,SPI时钟最好不要顶到从机的绝对上限,留出至少30%到50%的裕量,稳定压倒一切。

第二个坑,另一个项目里做多从机片选,图省事用组合逻辑输出去控制CS,结果在FPGA全局复位释放的时候,CS线上出现了一个很窄的毛刺,恰好把从机的配置寄存器写乱了。虽然概率很低,但一旦出现就很诡异,表现为设备时好时坏。后来把所有CS输出改成寄存器打拍输出,复位期间强制拉高,毛刺问题彻底解决。这件事给我留下的教训是:跟片选相关的输出一定要走寄存器,不要用组合逻辑直接拉出去,更不要相信复位状态下组合逻辑能稳定地保持高电平。

结尾没什么好总结的,就是最后说一句。如果你也在做FPGA和SPI相关的通信调试,建议把“先读ID,再正常通信”这个习惯刻在脑子里。不管接的是Flash、传感器还是ADC,第一步永远是读它的ID寄存器,ID读出来了,通路就通了一大半,剩下的只是业务逻辑问题。另外,等SPI通信完全稳定之后,别急着丢代码,把它封装成带参数可复用的模块,下次换一颗FPGA、换一个从机,改两个参数就能直接用,这笔账怎么算都不亏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 12:48:10

Suno AI音乐生成实战:从哼唱到完整歌曲的技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:46:51

C#上位机与ZigBee组网实战:串口通信与传感器监控

简介&#xff1a;面向C#与Zigbee物联网开发者的上位机工程资源&#xff0c;覆盖串口通信、数据解析、MySQL存储与自校验等完整链路&#xff0c;适用于小区燃气监控等场景的入门与二次开发。资源包含87个文件&#xff0c;压缩包8.48MB&#xff0c;以C#源码(.cs)、VS工程配置文件…

作者头像 李华
网站建设 2026/9/8 12:45:42

i.MX6ULL平台驱动开发:从设备树到probe匹配机制详解

做嵌入式Linux驱动开发这几年&#xff0c;i.MX6ULL算是我用得最多的一块主控&#xff0c;Cortex-A7内核、资源适中、资料也多&#xff0c;从入门学习到小批量产品都很合适。不管点灯、读按键还是驱动外设&#xff0c;Linux下都绕不开Platform设备和驱动匹配机制这个问题。很多朋…

作者头像 李华
网站建设 2026/9/8 12:43:44

接入层交换机VLAN配置:AP端口Trunk与PVID实战详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:43:18

从零手写SVD:嵌入式C语言实现奇异值分解的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:42:29

ContextCapture下载安装与许可配置全指南:从环境评估到空三稳定运行

简介&#xff1a;ContextCapture是一款广泛应用于倾斜摄影与实景三维建模的专业软件&#xff0c;这份下载地址资源面向BIM工程师、测绘人员、三维可视化从业者以及相关专业学生&#xff0c;提供实测可用且支持64位系统的获取途径&#xff0c;能够解决官方渠道入口隐蔽、网上分享…

作者头像 李华