拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA实现SPI通信:从协议原理到Verilog实战详解

1. 项目概述与SPI协议再认识搞FPGA的人迟早会和SPI打交道。这协议太常见了ADC采集、DAC输出、Flash读写、传感器配置、显示屏初始化几乎每个项目里都能碰到几路SPI。我最早接触SPI是在一个数据采集项目里当时要同时挂三片ADC和一片Flash全部走SPI接口主控用的是国产FPGA。一开始觉得SPI简单不就是四根线嘛MOSI、MISO、SCLK、CS谁不会啊。结果真正写Verilog实现的时候各种细节问题全冒出来了时钟极性搞反、采样点没对准、片选时序不对、高速下数据不稳定。调试那几天真是欲哭无泪。这个项目标题是SPI通信FPGA实现本质上是讲怎么在FPGA里用Verilog或者VHDL去实现SPI的主机或者从机功能。它本身是一个技术实现类的项目适合刚入门FPGA但已经具备基础数字电路知识的人也适合做嵌入式开发想了解一下FPGA侧SPI实现思路的人。这篇文章我不会只贴一堆代码让你抄而是把SPI协议里那些文档不会告诉你的细节、踩坑经历、选型思路一并讲清楚。先说清楚一个概念SPI全称是Serial Peripheral Interface串行外设接口由Motorola在20世纪80年代提出。它属于同步串行通信接口传数据的时候必须有时钟线配合。和UART这种异步通信不一样SPI不需要双方约定波特率主机给从机提供时钟数据按位传输速率可以做得很高。常规SPI接口速率从几百kHz到上百MHz都能跑具体上限取决于从机支持能力和PCB走线质量。FPGA实现SPI的好处在于你可以完全掌控时序实现一些非标准的、扩展的SPI协议比如多字节连续读写、任意长度数据的传输、多个从机灵活片选等这些都是通用MCU的硬件SPI外设做不到的。我建议你在动手写代码之前先花点时间看看SPI协议本身把帧格式、四种工作模式、全双工半双工这些基本概念搞透。有些初学者上来就找代码抄抄完了发现和自己的器件对不上也不知道怎么改根因就是协议没吃透。哪怕你的目标是尽快跑通功能也请先把下面这几节看完磨刀不误砍柴工。1.1 核心需求解析SPI通信的FPGA实现拆开来看有几个层次的需求。第一个层次是满足基本通信需求能够在FPGA与外部设备之间建立稳定、可靠的SPI链路。更具体地说设计者需要决定FPGA作为主机还是从机。绝大多数场合FPGA作为主机主动发起读写操作外接ADC、DAC、Flash、传感器等从机设备。第二个层次是时序与性能的匹配。SPI没有像PCIe那样的复杂链路训练机制它的时序是严格由主机SCLK决定的。所以FPGA内部所产生的SCLK频率、占空比、建立保持时间是否满足从机要求是通信成败的关键。比如很多ADC芯片要求SCLK高电平期间数据稳定采样边沿在SCLK上升沿或者下降沿。CPOL和CPHA两个位就是干这个用的后面会细讲。第三个层次是系统集成能力。在真实项目里SPI往往不是独立存在的它要挂到系统总线上接收CPU或状态机的指令再把数据送出去。所以一个合格的FPGA SPI模块除了接口逻辑本身还应当有寄存器接口方便软件配置、FIFO缓冲应对突发传输、中断或标志位通知上层处理完成。我早期的实现里就只做了裸的SPI收发逻辑上系统后发现CPU要等SPI传完才能干别的事效率极低后来加了FIFO和状态标志才解决问题。所以这篇文章的内容组织方式如下从SPI协议和时序模型讲起然后给出完整的FPGA SPImaster实现方案包括顶层架构关键模块Verilog代码和测试思路再分析硬件片选与软件片选的选择最后是时序约束和问题排查实录。全部内容围绕如何用FPGA实现一个稳健、可控、好集成的SPI通信模块展开。1.2 SPI、IIC与UART怎么选和SPI相关的常见问题里总绕不开SPI和IIC有什么区别选哪个好。做了这么多年FPGA我个人的经验判断标准其实很明确SPI最大的特点是快和简单。全双工收发同时进行没有应答机制硬件开销小。如果外设是高分辨率ADC、高速DAC或者大容量FlashSPI基本上是不二之选。举个例子我用过的一款ADC采样率2MSPS输出位数16bit用SPI接口最高时钟能跑到80MHz一帧数据32个时钟周期也就是4us多一点能采一个点。如果用IIC就算把时钟拉满400kHz一次读16bit数据加上各种协议开销至少60个时钟周期150us都下不来性能差了几十倍。IIC的优势是接线省两线制支持多主机总线仲裁每个设备有地址可以通过地址区分多个设备。适合低速、对引脚数量敏感的场景比如说给几个温度传感器、EEPROM这种低速率器件做配置和状态读取。IIC的ACK机制和仲裁机制让它比SPI更可靠、更适合板级多设备互联但复杂度也更高。UART在FPGA里更多的是做调试口和低速通信口用比如把采集结果直接通过串口打印到上位机。UART虽然只需要两根线TX、RX但双方各自要有独立的时钟源因此通信速率由双方约定一般为9600、115200、460800等固定波特率。如果让我给一个快速选型建议数据量大、速率要求高、链路简单点对点就选SPI多设备低速配置读写就选IIC异构系统间通信、调试日志输出就选UART。这些都是从实用角度出发的选择逻辑也是我项目里最常见的三种串行通信方式。2. SPI协议核心机制与时序模型既然要用FPGA实现SPI就得先把SPI的协议机制和时序模型彻底搞清楚。别嫌这些基础东西烦很多后期调试的痛苦都是因为前期对协议的理解不够扎实。下面我按线序、工作模式、数据传输三个阶段来拆解。2.1 四根线各自的作用与连接结构标准的SPI有四个信号SCLK串行时钟由主机输出从机接收。SCLK是个方波信号每来一个边沿双方移入/移出一位数据。SCLK的频率就是SPI通信速率。MOSIMaster Output Slave Input主机输出、从机输入。主机在这个线上把数据发给从机。MISOMaster Input Slave Output从机输出、主机输入。从机在这个线上回传数据。CS/SS片选信号低电平有效。主机把CS拉低表示选中某个从机设备只有被选中的设备才会参与通信。从拓扑结构上看SPI是典型的一主多从结构。多个从机的MOSI和MISO可以并联到同一条总线上但每个从机需要有独立的CS信号。所以FPGA作为主机时如果挂了N个从机就需要N个CS输出端口。MISO线要求比较特殊因为所有从机的MISO都接在一起从机没有被选中时要输出高阻态否则多个设备的输出会打架。市面上绝大多数SPI从机芯片内部已经实现了这个逻辑FPGA这边无需额外处理。有一种所谓的QSPI或者Dual SPI/Quad SPI接口是SPI的变种把MISO/MOSI复用来做双向数据线达到更高速率。这个后续有机会单独写本文聚焦标准SPI。从物理连接上我需要提醒一点SPI属于同步接口信号边沿陡峭在高速率几十MHz以上时反射和串扰都会比较明显。PCB设计上要注意SCLK尽量不要太靠近其他高速翻转信号走线保持短而直有条件的话串接33Ω左右的匹配电阻。板级经验不足的同学容易忽略这些结果在实验室调试时发现通信偶发错误拿示波器量信号又看不出明显问题其实就是信号完整性引起的。2.2 CPOL和CPHA四种模式彻底讲透SPI有四种工作模式由CPOL时钟极性和CPHA时钟相位两位共同决定。这也是新手最容易混淆的地方。我尽量用大白话讲清楚。CPOL决定SCLK在空闲状态时是高电平还是低电平。CPOL0表示空闲时SCLK为低电平有效状态是高电平CPOL1则相反空闲时SCLK为高电平有效状态是低电平。CPHA决定数据采样发生在SCLK的第几个边沿。CPHA0表示数据在第一个边沿采样CPHA1表示在第二个边沿采样。这里第一个边沿和第二个边沿是相对于一个bit周期而言的指的是SCLK的跳变沿可能是上升沿也可能是下降沿具体由CPOL决定。把两位组合起来工作模式CPOLCPHA采样边沿数据变化边沿典型应用Mode 000上升沿下降沿绝大多数器件Mode 101下降沿上升沿部分ADC/DACMode 210下降沿上升沿某些FlashMode 311上升沿下降沿部分传感器我用Mode 0举例子这是最常用的一种模式。CS拉低后SCLK从低电平开始第一个边沿是上升沿CPHA0所以在上升沿采样数据。主机在下降沿更新MOSI上的数据从机在上升沿采这个数据。对于从机发往主机的数据也一样从机在下降沿更新MISO上的数据主机在上升沿采样。这样每个时钟周期正好传输1 bit数据在采样边沿到来之前已经稳定了至少半个时钟周期。Mode 1呢CPOL0但CPHA1。SCLK空闲为低但第一个边沿上升沿不算第二个边沿下降沿才采样。这种情况下主机和从机都会在上升沿更新数据下降沿采样数据。换句话说主机和从机的动作是同步的都在同一个边沿更新数据下一个边沿去采样。说实话对FPGA实现来说你不需要在代码里显式地区分这四种模式你只需要根据从机数据手册里的时序图选择一种模式然后让SCLK的相位、采样时机和更新时机严格对齐就行。更聪明的做法是写一个参数化模块用两个parameter控制CPOL和CPHA然后在采样和更新条件里加上这两个参数判断。后面给代码的时候细说。我之前遇到过一次很奇怪的现象一块ADC在模式0下工作正常同样的寄存器配置换成模式3也能初始化成功但读取的数据偶尔错位。查了半天发现该芯片采样手册里推荐的其实是Mode 1之前能用纯属巧合因为寄存器配置阶段SCLK比较慢建立时间充裕掩盖了模式错误的时序问题。所以一定要严格按照数据手册选模式别心存侥幸。2.3 全双工传输过程逐位拆解SPI是全双工通信主机发数据的同时也在收数据。理解这一点非常关键因为很多初学者以为SPI是发送完再接收实际上并非如此。每个时钟周期都会同时完成发送1 bit和接收1 bit的操作。以Mode 0、8bit传输为例完整过程是这样的CS拉低从机被选中从机在自己的SS端口检测到低电平后开始准备数据。在第一个SCLK上升沿到来之前主机已经把第7位数据最高位放到MOSI线上从机也把第7位数据放到MISO线上。第一个上升沿到来时双方同时采样对方的线上数据。下降沿到来时双方各自更新下一位数据到线上。如此循环经过8个SCLK周期双方各收到了8个bit组合起来就是完整的一字节。注意一个细节在传输过程中MOSI和MISO上的数据都是随时钟边沿不断更新的不存在传输开始和传输结束的额外握手。因此主机必须精确控制传输的字节数和位序。大多数SPI器件是MSB-first即最高位先发。如果你的器件要求LSB-first就需要在实现时做位序反转。这个操作在移位寄存器里很自然MSB-first就是左移LSB-first就是右移。在FPGA实现中主机需要维护一个发送移位寄存器和接收移位寄存器。每个SCLK周期发送移位寄存器把最高位送出去然后左移一位接收移位寄存器从MISO上采一位数据并移入寄存器低位。8个周期后发送寄存器的值全部发完接收寄存器的值正好是一字节完整数据。我强烈建议你把SPI的字节传输过程画成一个时序图把CS、SCLK、MOSI、MISO四条波形的变化画出来标出数据有效区间和采样点。我在做每一个新器件驱动时都会干这件事一份清晰的手绘时序图往往比对着数据手册看半天更有效。等你有经验了还可以直接在FPGA工程里写一个简单的testbench把主机的SCLK和数据线拉出来做仿真波形验证协议逻辑是否正确。3. FPGA实现SPI主机的架构设计协议讲完了现在进入正题怎么写Verilog代码实现一个SPI主机。这里我给出的设计思路偏向于通用性和可复用性结构上会把模块拆分为顶层控制器、发送FIFO、接收FIFO、波特率发生器、SPI协议引擎几个部分。这样做的好处是项目里换一颗从机芯片只需要修改极少部分代码甚至只需要调整参数就行。3.1 模块划分与整体架构顶层模块设计成AXI-Lite接口和寄存器接口这是很多FPGA工程师实际项目里的做法。你有两种选择一是做一个纯粹的SPI接口IP对外暴露简单的握手信号start、busy、tx_data、rx_data二是带寄存器接口方便软核或者外部MCU通过总线配置。如果只是做纯逻辑链路选第一种就够用。如果是要嵌入到SoC系统里建议用第二种。从底层实现来看SPI主机模块分为五块clk_divSPI时钟生成模块将系统时钟分频产生SCLK。spi_master_fsmSPI主状态机负责整个传输过程的时序控制。shift_reg移位寄存器模块处理数据的并转串和串转并。tx_fifo / rx_fifo发送和接收缓冲FIFO深度按需求调整。reg_if寄存器接口用于配置分频系数、控制启停、读取状态。这种拆分方式比较常规但能保证每个模块职责单一代码可读性和可维护性都会好很多。对于刚入门的初学者我建议先把spi_master_fsm和shift_reg合起来写一个极简版先跑通逻辑再逐步加上FIFO和寄存器接口。系统的整体工作流程是上层逻辑或CPU把要发送的数据写入tx_fifo然后写一个启动寄存器。SPI主状态机检测到启动信号后拉低CS产生SCLK同时从tx_fifo取数逐位移出并把采样的数据写入rx_fifo。全部字节发送完毕后拉高CS置位完成标志。上层可以查询标志位或者接收中断来读取数据。3.2 时钟分频与SCLK生成策略FPGA的系统时钟一般是50MHz、100MHz或者更高。SCLK频率必须由系统时钟分频而来。分频系数等于系统时钟频率除以目标SCLK频率再除以2因为SCLK一个完整周期包含高低两个电平。举个例子系统时钟100MHz目标SCLK 10MHz分频系数100/10/25。也就是说计数器从0计到4前5个周期输出低电平后5个周期输出高电平合起来就是10MHz。更严谨的说法是SCLK半周期为5个系统时钟周期一个全周期10个系统时钟周期。分频系数建议写成parameter工程里可以针对不同的从机器件配置不同的分频系数。很多从机芯片的最高SCLK频率都不一样同一块板子上如果挂了Flash和ADCFlash可以跑高点ADC要保守一点这时候分频系数分开配置就很重要。一个小技巧SCLK的频率不要直接拉满到从机允许的最大值。我曾经为了让Flash读写快点把SCLK配到了芯片标称的最高频率80MHz。实际跑起来偶发数据错误排查下来是PCB走线长了点信号边沿质量不够好。降到50MHz后一切正常。工程上留20%-30%余量是常态除非你非常确定PCB信号完整性很好。3.3 主状态机设计与状态跳转spi_master_fsm是整个SPI主机的核心。我通常用三段式状态机来实现状态定义如下IDLE空闲状态。等待启动信号。启动后拉低CS跳转到发送状态。TRANSFER传输状态。计数器从0计数到(bit_width-1)每个SCLK时钟周期移位一次。计数满后判断是否还有更多字节要发送有则继续无则跳转到STOP。STOP停止状态。拉高CS产生完成脉冲返回到IDLE。这里有个细节要注意状态机是在系统时钟域工作的而SCLK是分频产生的逻辑时钟。如果直接在状态机里等SCLK边沿容易出现跨时钟域的问题。我建议的做法是在系统时钟域里控制一个SPI时钟使能信号每当分频计数器到特定值时产生一个单脉冲作为SCLK的驱动使能。SPI状态机在这个脉冲下进行计数和数据移位SCLK本身可以作为一个寄存器输出。这种设计被称为时钟使能clock enable方式避免生成门控时钟。在FPGA里门控时钟容易引发保持时间和时序收敛问题是资深工程师尽量避免的写法。虽然直接assign sclk clk en也能工作但综合工具在时钟布线和时序分析上会对这种写法做出比较保守的处理容易吃时序违例。4. 关键Verilog代码实现下面给出一个完整的SPI master Verilog实现。为了便于理解我做了一个偏教学化的版本去掉FIFO和寄存器接口只保留核心收发逻辑和clk_div方便你直接仿真和学习。实际项目需要扩展的话在这个基础上加就行。4.1 参数化SPI Master完整代码module spi_master #( parameter CPOL 1b0, // 时钟极性 parameter CPHA 1b0, // 时钟相位 parameter DATA_WIDTH 8, // 数据位宽 parameter SCLK_DIV 10 // 分频系数SCLK clk / (SCLK_DIV*2) )( input wire clk, input wire rst_n, input wire start, // 启动一次传输 input wire [DATA_WIDTH-1:0] tx_data, // 待发送数据 output wire [DATA_WIDTH-1:0] rx_data, // 接收到的数据 output reg busy, // 忙标志 output reg done, // 传输完成脉冲 output reg sclk, // 串行时钟 output reg cs_n, // 片选低有效 output reg mosi, // 主机输出 input wire miso // 主机输入 ); localparam CNT_MAX SCLK_DIV - 1; reg [$clog2(SCLK_DIV)-1:0] clk_cnt; // 分频计数器 reg sclk_en; // 时钟使能脉冲 reg [DATA_WIDTH-1:0] tx_shift; reg [DATA_WIDTH-1:0] rx_shift; reg [$clog2(DATA_WIDTH1)-1:0] bit_cnt; localparam IDLE 2b00; localparam TRANSFER 2b01; localparam STOP 2b10; reg [1:0] state; //-------------------------------------------------- // 时钟分频产生sclk和sclk_en // 当CNT_DONE为高时翻转sclk并产生一个单脉冲sclk_en //-------------------------------------------------- reg clk_toggle; wire cnt_done (clk_cnt CNT_MAX); always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 0; clk_toggle 1b0; end else if (state IDLE) begin clk_cnt 0; clk_toggle 1b0; end else if (cnt_done) begin clk_cnt 0; clk_toggle ~clk_toggle; end else begin clk_cnt clk_cnt 1b1; end end // sclk_en在sclk翻转前后更新数据/采样数据的使能 // 简单起见在clk_toggle翻转的那一拍产生使能 reg sclk_en_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_en_r 1b0; end else begin sclk_en_r cnt_done; end end assign sclk_en cnt_done; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk CPOL; end else if (state IDLE) begin sclk CPOL; end else if (cnt_done) begin sclk clk_toggle; end end //-------------------------------------------------- // 状态机主体 //-------------------------------------------------- always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n 1b1; tx_shift 0; rx_shift 0; bit_cnt 0; busy 1b0; done 1b0; mosi 1b0; sclk_en_r 1b0; end else begin done 1b0; case (state) IDLE: begin cs_n 1b1; busy 1b0; if (start) begin state TRANSFER; cs_n 1b0; // 拉低片选 tx_shift tx_data; // 载入待发送数据 bit_cnt 0; busy 1b1; end end TRANSFER: begin // 在sclk_en时根据CPOL/CPHA决定采样和移位 if (sclk_en) begin // 更新MOSI在正确边沿把最高位移出去 if ((CPOL 1b0 CPHA 1b0 clk_toggle 1b0) || // Mode 0: 下降沿更新 (CPOL 1b1 CPHA 1b1 clk_toggle 1b1) || // Mode 3: 下降沿更新 (CPOL 1b0 CPHA 1b1 clk_toggle 1b1) || // Mode 1: 上升沿更新 (CPOL 1b1 CPHA 1b0 clk_toggle 1b0)) begin // Mode 2: 上升沿更新 mosi tx_shift[DATA_WIDTH-1]; end // 采样MISO在采样边沿采入数据 if ((CPOL 1b0 CPHA 1b0 clk_toggle 1b1) || // Mode 0: 上升沿采样 (CPOL 1b1 CPHA 1b1 clk_toggle 1b0) || // Mode 3: 下降沿采样 (CPOL 1b0 CPHA 1b1 clk_toggle 1b0) || // Mode 1: 下降沿采样 (CPOL 1b1 CPHA 1b0 clk_toggle 1b1)) begin // Mode 2: 上升沿采样 rx_shift {rx_shift[DATA_WIDTH-2:0], miso}; end // 移位控制在采样边沿执行移位 if ((CPOL 1b0 CPHA 1b0 clk_toggle 1b1) || (CPOL 1b1 CPHA 1b1 clk_toggle 1b0) || (CPOL 1b0 CPHA 1b1 clk_toggle 1b0) || (CPOL 1b1 CPHA 1b0 clk_toggle 1b1)) begin tx_shift {tx_shift[DATA_WIDTH-2:0], 1b0}; bit_cnt bit_cnt 1b1; end end // 传输完DATA_WIDTH位后进入STOP状态 if (bit_cnt DATA_WIDTH - 1 sclk_en) begin state STOP; end end STOP: begin cs_n 1b1; // 拉高片选 busy 1b0; done 1b1; // 完成脉冲 state IDLE; end endcase end end assign rx_data rx_shift; endmodule这段代码里有几个地方需要仔细看关于MOSI更新和采样边沿的匹配我用了四组条件分别对应四种工作模式。你可以根据自己的需求裁剪掉不需要的模式保留一种常用模式会让代码更简洁。但如果你打算把这个模块用于不同项目保留全模式支持是值得的。关于片选时序代码里CS拉低和拉高的时刻是在状态机的IDLE和STOP里处理的。实际应用时有些器件要求CS拉低后等待一小段时间再开始SCLK有些要求SCLK停止后保持CS低电平一段时间再拉高。这些要求在状态机里预留几个空转周期即可。比如把STOP状态拆成STOP_WAIT状态多等几个周期再拉高CS。关于rx_data的生成我直接用了寄存器赋值实际使用时如果对时序要求更严格可以考虑在STOP状态把rx_shift锁存到输出寄存器避免在传输过程中rx_data值中途变化。这个看你自己的需求。4.2 极简应用示例与Testbench说明用这个master模块应用层调用方式很简单// 调用示例向一个SPI DAC写入0x3F spi_master #( .CPOL(1b0), .CPHA(1b0), .DATA_WIDTH(8), .SCLK_DIV(10) // 系统时钟100MHzSCLK5MHz ) u_spi_master ( .clk (clk), .rst_n (rst_n), .start (spi_start), .tx_data (8h3F), .rx_data (spi_rx_data), .busy (spi_busy), .done (spi_done), .sclk (sclk), .cs_n (cs_n), .mosi (mosi), .miso (miso) );写testbench时重点验证以下几件事第一CS拉低后第一个有效数据位是否在SCLK第一个有效边沿前建立第二采样的数据位是否和外部从机模拟的数据一致第三连续多字节传输时片选信号是否正确维持低电平第四完成信号done是否只在最后一个字节传输结束时出现。一个比较实用的仿真验证方式是在testbench里例化一个SPI slave的模拟模型和master对着跑。从机模型不需要太复杂用一个移位寄存器接收数据并输出一个固定值到MISO就行。这样可以看到master发出的tx_data被从机正确接收同时master也能采到从机回传的固定值。两边一致链路就是通的。5. 硬件片选与软件片选的实际取舍做FPGA SPI项目时片选信号的产生方式是个容易忽略但影响很大的工程细节。工程界常说的硬件片选和软件片选其实指的是片选信号的两种连接和控制方式。5.1 两种片选方式的原理对比硬件片选指的是FPGA通过专用的GPIO引脚直接连接从机CS引脚。片选信号的拉低和拉高完全由FPGA内部的逻辑控制和SPI数据线协同工作。这种方式的优点是响应快、时序可控FPGA可以在精确的时刻拉低CS缺点是占用的FPGA引脚多每多挂一个从机多占一个引脚。软件片选的叫法有点误导不是真的用软件去控制一般是指通过寄存器的方式外接一个译码器或者锁存器把CS的控制信号通过总线地址写操作产生。例如用GPIO扩展芯片或者74HC138译码器通过写不同的地址值来选择不同的从机。这种方式节省了FPGA引脚但是片选跳变的时刻依赖于总线写操作的时序灵活性变差。在我经历的项目中如果从机数量不超过四个我基本都用硬件片选直接在FPGA里做多个CS输出。如果从机数量多且支持SPI菊花链拓扑可以考虑软件片选配合外部译码电路。还有一种折中方案FPGA侧只引出一根CS信号外部加一个简单的移位寄存器或者反相器阵列来扩展多路片选。5.2 片选时序对通信稳定性的影响不管哪种片选方式片选时序都必须满足从机芯片的要求。很多从机手册里会给出CS相对于SCLK的建立时间t_setup和保持时间t_hold。比如有些Flash芯片要求CS拉低后至少50ns才能出现第一个SCLK上升沿有些ADC则要求最后一个SCLK下降沿之后还要保持CS低电平至少10ns。FPGA实现时可以在状态机里增加一个片选延时计数器在IDLE到TRANSFER之间加入若干个空闲SCLK周期。这个空闲周期的数量可以通过parameter配置方便适配不同从机。另外有一个非常隐蔽的坑在SPI多字节连续读写时某些从机要求CS在整个传输期间持续保持低电平而另一些则允许每字节之间CS有一个短暂的释放甚至要求释放。如果实现时不区分这一点很容易出现字节间隔的通信错误。好的做法是在状态机中为字节间隔是否拉高CS做一个参数控制位比如BYTE_GAP_CS 1表示每字节间拉高CS0表示保持低电平。适配不同芯片时改参数就行。6. 时序约束、验证与信号完整性实践写完代码只是第一步。SPI接口要在实际板上稳定工作时序约束和验证手段是绕不开的。很多初学者习惯直接下板调试发现问题后再拿示波器到处量这样效率很低。正确做法是先用仿真验证功能正确性再做时序约束确保电路满足建立保持时间最后下板用ILA实测。6.1 跨时钟域处理与同步设计思路SPI master工作在系统时钟域中但SCLK、MOSI、MISO这些信号是相对慢速的外部接口信号。如果SPI时钟不需要高达几十MHz最简单的处理就是全部寄存器输出不做内部多时钟域切换。MISO作为输入信号进入FPGA后理论上属于跨时钟域信号。如果MISO速率不高几MHz直接在系统时钟域中打两拍同步再使用基本也能满足要求。但如果SPI速率较高建议使用IDELAY或者IOBUF原语来调整输入采样窗口。Xilinx 7系列架构中ODDR、IDDR、ISERDESE2等原语可以精确控制SDR/DDR数据采样但这样做代码复杂度会显著上升。我的建议是在SPI速率低于50MHz时直接用同步打拍解决高于50MHz时认真考虑使用专用I/O原语和区域时钟资源别硬用普通逻辑去拼。此外ASIC风格地把外部采样信号在FPGA内部打两拍再使用能有效防止亚稳态传播这是FPGA设计的基本功。6.2 ILA在线调试与常见波形分析下板调试时ILAIntegrated Logic Analyzer是即时抓取内部信号波形的利器。Xilinx Vivado里的ILA可以配置多个探针把cs_n、sclk、mosi、miso、bit_cnt、state等信号引出来观测实际波形。抓到的波形一旦和预期不一致按下面的思路排查先看CS和SCLK的时序关系对不对CS拉低后第一个SCLK沿有没有正确出现再看MOSI上数据的位序对不对是不是从MSB开始的然后看MISO采样点的数据是否稳定可以用一个只在采样沿有效的信号去标记采样点最后看完整传输周期结束后CS是否正常释放。常见波形问题归纳如下表现象可能原因排查手段无SCLK输出分频器未启动或状态机卡在IDLE检查start信号、state状态SCLK频率不对分频系数配置错误重新计算SCLK_DIV参数MISO采样数据错位CPOL/CPHA配置错误对照数据手册时序图确认模式数据偶发错误信号完整性问题或片选时序问题降低SCLK频率检查PCB走线完成信号一直不产生位计数器溢出或状态机死锁检查bit_cnt和状态跳转条件6.3 从0到1完整FPGA项目流程建议如果你是从零开始做带SPI接口的FPGA项目我建议的流程是这样先明确通信需求包括从机型号、最高SPI速率、数据位宽、单次传输字节数、是否连续读写。再根据需求确定FPGA型号和引脚分配重点确认目标器件的I/O bank电压与从机接口电压是否匹配。然后编写SPI master模块并同步编写testbench在仿真环境里把功能验证通过。接着分配引脚、编写约束文件XDC或SDC包含时钟约束和I/O约束。综合实现后查看时序报告确保SPI相关路径满足约束。然后下板先用ILA抓取几个关键信号确认波形正确再接上真实从机设备联调。联调通过后再做系统集成加FIFO、寄存器接口、中断控制等。整个过程不要跳步。我亲眼见过太多人省掉了仿真步骤直接下板结果调了三天发现是时序模式搞错了如果先做仿真十分钟就能发现。仿真的价值不仅仅在于验证功能更在于能让你把调试的注意力集中在协议逻辑上而不是信号引脚和示波器探头接触不良这种物理问题。7. 项目实操心得与常见问题复盘最后写一点不太容易从文档里学到的经验教训算是给这篇文章收个尾。7.1 建立时间与保持时间到底怎么理解很多初学者搞不懂为什么FPGA内部逻辑满足时序约束这么重要。我举个例子SCLK在FPGA内部是寄存器输出它到达从机引脚需要T_co寄存器输出延迟加上T_pcbPCB走线延迟。与此同时MOSI数据线上的数据也要经过类似路径到达从机。如果两个信号到达时刻相差悬殊从机采样时就可能采到不确定的电平。这套东西在数字电路里叫建立时间和保持时间。FPGA综合工具会帮你分析这些问题。你只要在约束文件里写清楚时钟频率和I/O延时工具会自动计算每条路径的工具时延、器件时延并报告是否收敛。SPI这种速度不太高的接口只要约束正确时序收敛通常不是问题。真正需要重点关注的是那些没有约束的异步输入信号它们才是隐患所在。7.2 上拉电阻和双机通信的特殊处理SPI的MISO线在多个从机复用场景下必须保证没有被选中的从机输出高阻态不然两条推挽输出直接打对线轻则数据错误重则烧毁芯片。硬件设计上可以在MISO线上加上拉电阻保证总线悬空时是确定的高电平。FPGA内部的弱上拉在某些平台上也可以打开但引脚上外接一个10kΩ上拉电阻更可靠。双机通信时如果两个FPGA都实现了SPI master那两者之间只能一个做主机一个做从机。你要是两边都实现了master且都有定时发送的逻辑那就得自己设计好握手信号避免双方同时占据总线。我见过有的同学直接把两个master的MOSI接在一起一通电数据就不对道理就在这。7.3 向非标准SPI器件的扩展思路有些器件的接口严格来说不是标准SPI但工作原理接近。比如某些LCD驱动芯片要求9位数据帧1位DCX 8位数据又比如某些带命令/数据切换的传感器要求CS低电平时随SCLK发送的bit流里前后含义不同。这时候完全可以在SPI master模块之上包一层协议转换逻辑把标准SPI的数据帧映射到器件需要的帧格式。实现时注意不要破坏底层SPI主机的时序参数保持CS/SCLK的基本关系不变。如果你要支持DMA传输或者突发读写建议在UP层控制逻辑中维护一个传输描述符表SPI主机只负责按描述符执行传输这样架构清晰了很多。8. 写在最后的一点经验分享一个我自己项目里很深刻的教训。早年间做一块板子FPGA通过SPI控制两片ADC同步采样SCLK配到了25MHz。实验室单板调试一切正常结果整机联调时数据偶发错误而且只在机器运行几分钟后出现。拿示波器盯了半天MISO信号发现随着温度升高信号边沿变得圆润采样点刚好落在边沿附近。后来把SCLK降到12.5MHz问题彻底消失。从那以后我就养成了一个习惯只要SPI速率超过从机标称值的一半信号完整性就要当回事绝不能只看功能仿真没问题就完事。另外一个习惯是每做一个SPI项目我都会在工程里放一个循环回环测试。就是让主机把一串已知数据发出去从机模型或者实际用一个Flash芯片把收到的数据存起来再读回来比对。这个自动比对逻辑虽然写起来多花一点时间但后面排查问题省下的时间远不止这些。如果你做的是ADC采集也可以定期往DAC写一个斜坡信号再采回来画波形立刻能看出链路是否正常。SPI通信的FPGA实现看起来是个基础功能但要做到稳定可靠、可复用、可集成还是有不少细节要打磨。希望这篇文章能把那些文档不写、但又实际决定成败的东西讲清楚。你在自己的项目里如果有更特殊的SPI需求比如超高速、多从机、非标准帧格式也可以沿着这个思路继续扩展。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门