ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA实战:手写SPI主机模块的协议解析与调试经验

FPGA实战:手写SPI主机模块的协议解析与调试经验 写SPI的FPGA实现这事儿我太熟了。这几年调试各种ADC、Flash、传感器SPI是我在FPGA上写过最多次的接口协议没有之一。很多朋友一上来就找现成IP核或者去GitHub上扒代码我倒觉得SPI这玩意儿把时序理清楚之后自己动手写一个真的不难而且自写的主机/从机模块用起来比啥都顺手。这篇就把我在实际项目中摸索出来的经验和思路做个总结从协议细节到Verilog实现再到板级调试一步步拆开聊希望能给正在搞FPGA通信接口的朋友一些参考。1. 动手之前先把SPI协议的底裤扒干净SPI的全称是Serial Peripheral Interface串行外设接口是Motorola公司搞出来的一种同步串行通信协议。FPGA开发里它出镜率极高几乎所有板上外设都留了SPI接口。它跟UART这类异步协议不一样SPI靠独立的时钟线来同步收发数据所以时序天然就可预测只要时钟沿干净基本不需要担心UART那种波特率误差的问题。SPI总线上一般有四种信号线SCLK串行时钟、MOSI主设备输出/从设备输入、MISO主设备输入/从设备输出、CS/SS片选信号。主机产生时钟同时决定片选拉低哪个从设备所有通信由主机掌控。这里经常有新手问MOSI和MISO能不能合并成一根线当然可以那就是半双工模式三线制SPI。不过大多数普通器件都是标准的四线全双工结构除非是某些特殊传感器或节省IO的应用否则不建议自己搞三线排查问题会麻烦不少。SPI协议最核心也最容易绕晕的一点就是它的四种工作模式。这个模式由时钟极性CPOL和时钟相位CPHA两个参数决定。CPOL决定空闲时SCLK是高还是低CPHA决定数据在第一个还是第二个时钟沿被采样。不少工程师在这里栽过跟头因为数据手册上写着“Mode 3”或者“CPOL1, CPHA1”翻译到代码里就是一堆纠结。这两个参数我建议这样理解CPOL0表示空闲时时钟是低电平CPOL1表示空闲时时钟是高电平CPHA0表示数据在第一个时钟边沿采样CPHA1表示在第二个时钟边沿采样。把所有组合都列出来就是下面这张表模式CPOLCPHA采样边沿常见应用场景Mode 000上升沿大多数Flash、ADC、SD卡Mode 101下降沿较少见部分传感器Mode 210下降沿部分工业级DACMode 311上升沿SPI FlashW25Q系列常用这张表不是让你背的是让你查的。我开始做FPGA那会儿每次遇到新器件都要翻出这张表对照手册确认一次。用Verilog写主机的时候我倾向于把CPOL和CPHA做成模块参数而不是直接写死在代码里。这样遇到换器件、换模式的时候改个顶层参数就行不用动核心时序逻辑。实测下来这个习惯帮我省了非常多的时间强烈推荐。还有个关键概念MSB First还是LSB First。绝大多数SPI器件是MSB先行但个别器件反着来比如某些LCD驱动芯片就是LSB First。Verilog设计时需要把移位方向做成可配置否则换器件又得改代码。我自己习惯用参数配置加上通用移位逻辑后面会放代码详细介绍。2. 主机还是从机先定方案再写代码SPI设备分主从但FPGA这边做主机的场景占绝大多数。比如给ADC采集数据、给DAC送波形、读写Flash、配置传感器全是FPGA当主机去主动发起通信。少数场景下FPGA要当从机比如跟MCU通信、被外部处理器配置但这种一般用现成IP核更省事自己写从机的收益不大。我在项目里遇到最多的组合是FPGA主机 多个SPI从设备。控制思路很简单每个从设备分配一个独立的CS信号MOSI、MISO、SCLK可以共享。但实际上这里有个坑如果多个从设备的MISO都挂在同一根线上那么必须保证同一时刻只有一个从设备的CS被拉低否则MISO上会出现总线冲突。更稳妥的做法是给每个从设备加上输出使能控制从设备没被选中时它的MISO输出必须处于高阻态。这一点在选型和设计阶段就要确认清楚有些器件的手册会注明“MISO is tri-stated when CS is high”但有些器件不会遇到后者只能软件上额外处理。关于硬件片选和软件片选我也简单说说。硬件片选就是CS直接由FPGA的GPIO控制拉低即选中拉高即释放简单直接。软件片选则是在有些场景下希望通过发送特定的命令字来切换从设备或者片选信号由某个移位寄存器的输出去控制这样做的好处是省IO坏处是时序复杂度上来了而且一旦软件跑飞总线可能一直挂着。我个人的建议是在FPGA资源允许的情况下老老实实用硬件片选一个设备一个CS引脚代码清晰时序可控排查问题也方便。只有在GPIO严重不足的极端场景下才考虑软件片选方案。主从方案定了之后接下来就是传输速率和SCLK频率的规划。SPI的速率上限取决于从设备能跑多快以及FPGA内部逻辑能不能跟上。比如W25Q64这款Flash新的版本支持104MHz甚至更高但老版本或者走长飞线的时候就只能跑20MHz以内。FPGA侧生成SCLK有两种思路一种是直接用系统时钟分频得到整数分频的SCLK另一种是使用PLL/MMCM产生精确的时钟。前者适合低速通信后者的抖动特性更好适合告诉传输。这里有个容易被新手忽略的问题分频产生的SCLK相位抖动和占空比不对称在高速模式下会限制通信距离。如果只是跑一个板级Flash30MHz以下随便分频都能用如果是要拉长线跟外部设备通信或者SCLK超过50MHz就必须用PLL去生成干净时钟了。我在一个需要跟外部工业设备通信的项目里吃过这个亏代码看着没问题就是偶发传错数据最后用示波器一测发现是SCLK在边沿附近有毛刺改成PLL出时钟后问题立刻消失。3. RTL实战写一个参数化的SPI主机模块我平时写SPI主机核心就是一个带状态机的移位寄存器。别看网上讲得玄乎其实就三步拉低CS按位收发数据拉高CS。复杂点的场景会加FIFO做缓冲加DMA做数据搬运但通信内核没变。先上一个最精简的四线主机代码框架这也是我项目里反复在用、逐步完善的版本module spi_master #( parameter CPOL 1, // 时钟极性 parameter CPHA 1, // 时钟相位 parameter DATA_WIDTH 8, // 数据位宽 parameter SCLK_DIV 4, // SCLK clk / (SCLK_DIV*2) parameter MSB_FIRST 1 // 1: MSB先行, 0: LSB先行 )( 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 cs_n, output reg mosi, input wire miso ); localparam IDLE 2d0; localparam TX 2d1; localparam DONE 2d2; reg [1:0] state; reg [7:0] clk_cnt; reg [3:0] bit_cnt; reg sclk_r; wire sclk_rise (sclk_r 1b0) (sclk 1b1); wire sclk_fall (sclk_r 1b1) (sclk 1b0); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; busy 0; cs_n 1; sclk CPOL; mosi 0; bit_cnt 0; clk_cnt 0; end else begin case (state) IDLE: begin cs_n 1; busy 0; if (start) begin cs_n 0; busy 1; bit_cnt 0; clk_cnt 0; state TX; end end TX: begin // SCLK 分频生成 if (clk_cnt SCLK_DIV - 1) begin clk_cnt 0; sclk ~sclk; end else begin clk_cnt clk_cnt 1; end // 发送和接收数据 if (sclk_fall) begin if (MSB_FIRST) mosi tx_data[DATA_WIDTH - 1 - bit_cnt]; else mosi tx_data[bit_cnt]; end if (sclk_rise) begin if (MSB_FIRST) rx_data[DATA_WIDTH - 1 - bit_cnt] miso; else rx_data[bit_cnt] miso; if (bit_cnt DATA_WIDTH - 1) state DONE; else bit_cnt bit_cnt 1; end end DONE: begin cs_n 1; busy 0; state IDLE; end endcase end end always (posedge clk or negedge rst_n) begin if (!rst_n) sclk_r CPOL; else sclk_r sclk; end endmodule这段代码默认支持CPOL/CPHA参数化采样和更新沿也是通过sclk_rise和sclk_fall这两个脉冲信号来统一控制的。有个细节值得注意我在发送数据的时候用sclk_fall更新MOSI在sclk_rise采样MISO这样能保证数据变化沿和采样沿错开半个周期从设备端更不容易采到跳变中的数据。当然如果你的CPHA配置不同这两个沿的对应的采样关系会变但只要抓住“数据在采样沿稳定即可”这个原则代码微调一下就能适配所有模式。这个模块在功能上是能用的但实际项目里我不会直接这么裸用。因为真实场景下需要处理连续传输、FIFO对接、多设备切换等复杂逻辑所以我会在顶层再包一层状态机负责装配命令、启动传输、校验返回值。比如在Flash芯片的读操作里主机先发送读命令和24位地址然后再产生额外的时钟来读数据。你会发现一个单次的8位SPI主机是不够用的需要把单次传输的位宽扩展或者连续多次调用模块接口。我后来把模块扩展成可配置位宽最长支持64位单次传输这样不管读Flash还是读ADC状态寄存器都够用了。再说从机实现。FPGA做从机代码上不复杂但验证起来麻烦。核心思路是借助外部时钟直接采样数据。但有一种场景我会特别注意就是从机是FPGA主机是MCU或DSP时钟线上可能有毛刺或者电平不稳这时候需要加输入同步器和边沿检测。我的做法是先把SCLK打两拍同步到系统时钟域然后再取上升沿和下降沿。注意这里的同步打拍会引入两个时钟周期的延迟对从机来说问题不大因为SPI本身就是主机主导的协议主机发送的时序不会等从机反馈。但如果你要在这个场景下做高精度的回环测试就要把这几个周期的延迟算进去了。4. 上板调试别急着接外设先用仿真和ILA把时序跑稳我在工程调试中养成了一个习惯任何SPI模块先做仿真再用ILA集成逻辑分析仪实测最后才接真实外设。这三步顺序不能乱否则出了问题很难定位是逻辑错了、时序约束不够还是外部器件本身响应异常。仿真阶段的重点是检查波形边沿关系。我给SPI模块搭的testbench会生成一个模拟从机响应这样能验证MISO数据的接收路径。很多人只在发送侧看MOSI波形忽略了回读校验实际上SPI是双向的仿真阶段把回读路径打通后面接真实器件会顺畅很多。用Vivado的同学仿真完之后记得拉一个ILA核来抓实际波形。我这里说几个ILA调试的小技巧触发条件可以用start信号的上升沿也可以用cs_n下降沿看你要观察哪个阶段采样深度不用开太大SPI波形比较规律512或1024足够如果需要观察SCLK边沿附近的毛刺ILA的采样时钟必须比SCLK高4倍以上才有意义否则采出来的波形是混叠的别把ILA的probe加太多我见过有人把每个内部寄存器都拉出来最后调试界面根本看不过来。实测阶段有一个经验每次接新板子我都会先做一个最简单的回环测试把MOSI直接短接到MISO然后让FPGA发送一组递增数据并回读比对是否一致。这个测试做通了说明主机的发送、接收、时序都没问题之后再去接外部器件。回环测试通过之后再去连真实从设比如Flash芯片先读JEDEC ID0x9F命令如果ID能读出来通常说明菜鸟阶段最担心的“时序不对”已经过去了后面就是根据具体协议操作寄存器的事了。我遇到很多新手上来就怼着W25Q64的读数据操作调半天结果发现寄存器配置都还没理清。所以这里特别强调接口调通和芯片功能调通是两回事。接口调通靠的是SPI逻辑正确芯片功能调通需要吃透芯片手册。建议先从最简单的操作入手一步一步来。5. 便宜好用的“接口测试利器”逻辑分析仪和必要的上拉电阻调试SPI最痛苦的事情之一是遇到“看起来波形都对但数据不对”的情况。这里我强烈建议备一个便宜的逻辑分析仪不需要什么高级货市面上几十块钱的USB逻辑分析仪就行采样率24MHz的够用了。用逻辑分析仪去抓SCLK、MOSI、MISO、CS四根线跟用ILA相比有一个好处不占用FPGA内部资源也不需要重新综合布线而且能看到真实的引脚电平而不是内部信号。用逻辑分析仪时要注意探头的输入电容会引入信号边沿变缓。低速率比如几MHz下影响不大但速率一高波形边沿可能变得不那么陡峭容易误判。所以排查高速SPI问题最好还是用示波器逻辑分析仪适合看协议层示波器适合看物理层两者各有分工。另外特别想说一下上拉电阻的问题。SPI的MOSI、SCLK、CS一般由主机强驱动不需要上拉。但MISO这条线如果从设备支持三态输出建议加一个10kΩ左右的上拉电阻避免在所有从设备都未被选中时MISO浮空。浮空的输入引脚在逻辑器件看来是高电平还是低电平是不确定的容易导致主机在非通信期间采到随机数据。如果板上已经预留了上拉位置就焊上没有预留就在FPGA内部把MISO引脚的弱上拉打开。当然如果所有从设备的MISO都是推挽输出且不会同时使能可以不处理但这个习惯养成了以后做项目会少很多奇怪的问题。说到这顺便提一个容易踩的坑FPGA和外部设备的电平标准不一致。现在很多传感器和Flash是1.8V或2.5V供电而FPGA的Bank电压可能是3.3V。直接相连轻则通信异常重则烧引脚。做SPI对接之前一定先查清楚两边IO的电平标准和Bank电压设置。Xilinx 7系列FPGA的HR Bank支持1.2V到3.3VHP Bank最高支持1.8V选Bank和分配引脚时就得考虑电平匹配不能随便找一个空闲引脚就分配出去。6. 常见问题排查与波形实例我把自己和身边朋友做SPI时踩过的一些典型坑整理成了下面这张表遇到问题直接按图索骥问题现象可能原因排查方法通信完全没反应MISO一直高电平CS信号没拉低、从设备供电异常、MISO上拉导致空闲被拉高先查从设备电源再用逻辑分析仪看CS有没有正确拉低数据偶尔错一位或错几位SCLK频率过高、走线过长导致信号质量差、时钟采样沿选错降低SCLK频率试一下再用示波器看SCLK边沿是否恶化读回来的数据全FF或全00SPI模式不匹配CPOL/CPHA配错、从设备没被正确唤醒对着手册确认Mode用回环测试验证主机侧多个从设备挂在总线上时互相干扰某个从设备MISO没有三态导致总线冲突逐个拉CS测试判断是哪个设备在捣乱高速通信时波形有回沟信号反射、阻抗不匹配、走线过孔过多降低速率先验证功能再优化PCB走线或串联33Ω电阻这里重点讲一下SPI模式不匹配的问题。你写代码的时候CPOL和CPHA各配一个参数看着很简单但一旦碰到那种“必须完全匹配器件手册才能工作”的从设备就很容易抓狂。我记得有次调试一颗ADC数据手册明明写着Mode 3结果按Mode 3配怎么读都是乱码后来用逻辑分析仪对比了厂家驱动板卡的波形才发现它实际是Mode 0。这种情况虽不常见但一旦遇到记住示波器和逻辑分析仪上看到的实际数据才是王道别迷信手册。另一个高频问题就是跨时钟域。如果你的SPI主机模块工作在100MHz系统时钟下而从设备的响应时间是微妙级别那么在状态机设计时必须有超时处理机制否则从设备无响应时整个模块会卡在某个状态出不来。我的做法是在状态机里加一个超时计数器超过阈值自动回到IDLE并置位错误标志。这个在ADC或Flash等响应时间不确定的器件上特别实用。7. 进阶思路FIFO缓冲、DMA和时钟域隔离到这里你已经能用自写的SPI主机跟Flash、ADC、传感器打交道了。但遇到高吞吐量的应用比如高速数据采集或图像数据读取纯靠CPU / 状态机搬运数据就不够用了。这时候就需要在SPI主机外面再加两层东西FIFO和DMA控制器。FIFO的作用是缓冲数据避免主机和SPI之间频繁握手。主机模块负责往FIFO里写读到的数据DMA或者上层读逻辑再从FIFO里批量取走。这个结构能让SPI一直跑在满速率状态不会因为等待取走数据而停顿。DMA控制器跟SPI对接的时候主要注意两点一是FIFO的空满标志要能正确触发DMA请求二是DMA传输结束之后要能产生中断或标志位通知上层软件处理。在Xilinx平台上通常的做法是用AXI4-Stream接口把SPI数据流接到AXI DMA IP核上这样能跟PS侧内存直接打交道。不过这套做法的学习曲线比较陡建议基础功能跑通之后再一步步加。时钟域隔离是另一个容易被忽视的点。假设你的SPI跑在25MHz而系统总线跑在150MHz如果直接让跨时钟域信号在系统逻辑里使用可能需要加同步器或者异步FIFO来处理。我在使用专用IP核时一般都能满足这个需求但如果自己写主机还接了很多外设就需要注意时钟域的约束问题。Vivado里可以建不同的时钟域约束文件把SCLK分频时钟和主时钟定义清楚不然综合结果可能不理想。芯片设计里常说“时序收敛”FPGA开发也一样只是层次不同。8. 拓展应用SPI与I2C怎么选以及后续扩展方向很多朋友在项目选型时会问SPI和I2C到底用哪个其实没有绝对更好的答案只有更合适的应用场景。SPI优点是速率高、全双工、时序简单缺点是引脚多4根起步且每个从设备都要单独拉一根CS。I2C优点就是省引脚两根线上能挂一票设备还有设备地址机制缺点是速率低、半双工、时序协议相对复杂还需要处理ACK仲裁之类的问题。如果只是在FPGA内部跟板级器件打交道而且速率要求不低SPI压倒性胜出。但如果是要跟一堆低速传感器交互比如温度传感器、EEPROMI2C可能更合适。我之前有一块板子板级温度监控用的是I2C数据采集链路用的是SPI各司其职互不冲突。所以别纠结谁取代谁作为FPGA开发者我建议SPI和I2C的接口代码都能随手写出基本功扎实了后续扩展做什么都顺。至于SPI的后续扩展方向有两条我觉得值得关注一个是把SPI主机封装成AXI4-Lite或AXI4-Stream接口方便做SoC集成这也是从逻辑设计走向系统设计必须跨过的一步另一个是在高速SPI场景下去研究硬核SPI控制器比如Zynq UltraScale里的SPI IP功耗和时序表现都比纯逻辑好很多。FPGA开发就是这样先把基础接口吃透后面无论做什么项目这些底子都会成为你最稳定的“技术底盘”。我个人在实际项目中还有一个习惯就是每做好一个SPI模块就把它的Testbench、约束文件和调试笔记归档到一个模板工程里。下次遇到类似的从设备直接在这个模板上改参数半天就能把接口调通。这比每次从零开始写要快得多而且随着沉淀的模块越来越多后续项目的开发速度会有明显提升。分享出来希望能帮到你。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表