ZYNQ 7020 PS端SPI读写驱动开发与调试实战
简介本资源是面向嵌入式开发工程师与FPGA软硬件协同设计学习者的ZYNQ 7020平台SPI外设驱动实战项目聚焦于基于Xilinx SDK环境实现标准SPI协议的可靠读写功能解决ARM端软件驱动与FPGA逻辑协同通信中的初始化配置、数据收发、错误处理等核心问题。压缩包共含多个C源文件、头文件及SDK工程配置文件如system.h、xspi.h等涵盖SPI控制器初始化、全双工读写函数、时钟/极性/相位参数配置、基础中断响应逻辑等关键模块整体大小为7.98MB结构清晰便于直接导入Vivado SDK调试复用。已有405人学习下载适合具备ZYNQ基础、正开展工业传感器通信、Flash读写或自定义SPI外设对接的中高级开发者可快速掌握SDK驱动开发流程、理解Xilinx官方API调用规范并为DMA扩展与多设备挂载提供可演进的代码框架。 ZYNQ 7020上调通一路SPI读写数据是很多做嵌入式驱动开发的人迟早要碰到的任务。我最近把手头一个完整的SDK驱动工程整理成了压缩包这里面是一套从Vivado硬件搭建到SDK驱动代码的实现过程专门针对ZYNQ 7020的PS端SPI控制器实现了对SPI从设备的读写。很多朋友在ZYNQ上第一次接触SPI时都有个困惑PS端明明有SPI外设但用起来总是读不到对的数据要么全是0xFF要么时序对不上。这篇文章就把我当时怎么选型、怎么配硬件、怎么写驱动、怎么排查问题完整地讲一遍。适合刚开始玩ZYNQ、想在PS端快速跑通SPI外设读写的人参考也适合做传感器采集、SPI Flash存储、屏幕驱动这类场景的朋友抄作业。1. 项目拆解与方案选型SPI这条路到底该走PS还是PL1.1 这个工程解决的核心痛点ZYNQ 7020这颗芯片的特点是ARM处理器PS和FPGA逻辑PL集成在一起。SPI控制器在PS和PL两侧都有很多第一次上手的人会卡在“我到底应该用哪个SPI”这个问题上。这个工程选择的是PS端自带的SPI控制器配合SDK里的XSpiPs驱动库来实现而不是在PL里用Verilog写SPI时序也不是用AXI Quad SPI这个IP核。为什么这么选因为对于大部分中低速SPI外设比如SPI Flash、温湿度传感器、ADC芯片、LCD屏幕PS端SPI控制器的能力完全够用而且开发效率高很多。你不需要去关心FPGA时序收敛不需要写Verilog状态机只需要在Vivado里把SPI外设勾选出来然后在SDK里调用库函数配置寄存器。这个工程解决的核心问题就是让一条SPI总线从硬件配置到软件驱动完整跑通。压缩包里包含了Vivado工程硬件部分和SDK工程软件部分照着走一遍你手里的ZYNQ 7020板子就能通过SPI和外部设备正确交换数据。1.2 PS端SPI控制器与PL端SPI IP的取舍我先把两套方案的差异摊开来说。PS端SPI控制器是ARM核内置的外设寄存器映射到PS的地址空间里由XSpiPs驱动控制。PL端则是用AXI Quad SPI软核挂在AXI总线上需要额外的IP配置、中断控制器、有时候还要管DMA。对比项PS端SPI控制器PL端AXI Quad SPI IP开发难度低SDK库函数封装好中高需要理解IP配置和中断/轮询逻辑引脚来源使用PS专用MIO引脚不需额外逻辑使用PL引脚需要写XDC约束最高速度控制器自身的时钟分频适合中低速场景可以做到更高支持Quad/Dual模式传输方式轮询、中断都可以支持FIFO、中断、DMA灵活性高FIFO深度128字节由IP配置决定可以更大上手时间约半天能跑通如果没接触过AXI总线可能要两三天一句话总结如果是快速验证、中小数据量、外设速率要求不极端的场景PS端SPI SDK驱动是性价比最高的路线。这个工程就是这条路线。如果你的目标外设是高速大容量Flash或者需要非常灵活的时序控制再考虑PL端方案。1.3 6针SPI接口与硬件连接市面上最常见的SPI外设接口就是6针VCC、GND、SCLK、MOSI、MISO、CS有的板子标为SS或CSN。连接前有两件事最容易翻车电平匹配和主从角色。ZYNQ PS端的MIO引脚电平由所在BANK的供电电压决定常见的是3.3V或1.8V。如果外设模块是5V供电的就一定要加电平转换不然长时间跑大概率烧引脚。我当时的连接方式是ZYNQ的SPI1控制器作为主机SCLK、MOSI、MISO、CS四根线分别接到SPI Flash模块的对应引脚VCC接3.3VGND必须和设备共地。这里有个细节MISO和MOSI非常容易接反很多板子的丝印又不够清楚建议先用万用表确认好模块引脚定义再接线。另外一个硬件上实用的习惯SCLK时钟线上串一个22到33欧姆的电阻用来抑制时钟信号的过冲和振铃。SPI速率不高的时候可能感觉不到差别但一旦提高时钟频率或者连接线比较长这个电阻能省很多排查时间。调试阶段可以先不串稳定运行后再看波形决定是否需要。2. Vivado硬件搭建把PS端SPI控制器“请”出来2.1 Block Design中的关键配置硬件搭建的第一步是打开Vivado新建一个RTL工程目标芯片选XC7Z020。这里我用的流程是Vivado 2019.1以上版本都适用更早的版本界面略有差异但逻辑一样。在Block Design里添加ZYNQ7 Processing System IP后会自动弹出PS配置界面。这里要做的事情很明确第一在I/O外设配置页找到SPI选项卡勾选SPI 0或SPI 1。我选的是SPI 1因为当时板卡上SPI Flash正好挂在SPI 1对应的MIO引脚上。勾选之后界面里会列出SCLK、MOSI、MISO、CS这些引脚对应的MIO选项。不同开发板的连接不一样有的板子接在SPI 0有的接在SPI 1一定要查自己板子的原理图再选。第二记得使能UART1用于SDK里的printf打印调试信息。调试SPI的时候没有串口打印就只能瞪着眼睛猜效率极低。第三DDR配置按板卡的型号和位宽选择。这个直接影响到后续SDK能否正常启动所以板卡厂商提供的例程里怎么配你就怎么配不要自己乱改。2.2 MIO引脚分配与电平标准ZYNQ的MIO引脚是PS专用的物理引脚不需要像PL引脚那样在XDC文件里写约束。你只需要在PS配置界面里选择对应的MIO组合Vivado会自动完成引脚分配。这一步要特别注意MIO Bank的电平标准。PS的MIO分成几个Bank每个Bank的电压在硬件设计时就确定了。在Vivado里可以看到你选择的MIO引脚属于哪个Bank电平标准是LVCMOS33还是LVCMOS18必须和板卡实际供电一致。如果选错轻则通信不稳定重则引脚损坏。我当时配置SPI 1时界面里可以选择MIO 8到MIO 15范围内的组合最终选了一组和板卡原理图对应的引脚。SPI引脚的配置不需要我们手动指定物理引脚号只需要在界面上选对组合这一点对刚接触ZYNQ的人特别友好不容易出错。2.3 导出硬件与生成SDK工程的三个细节硬件配置完成后经过综合、实现、生成比特流就到了导出硬件的环节。这里有几个细节任何一个不注意后面SDK都会踩坑。第一File - Export Hardware时一定要勾选Include bitstream。如果不勾选SDK虽然在运行但硬件平台文件里缺少PL配置信息后续如果用到PL会出问题也可能导致启动流程不完整。第二导出后Launch SDK时留意一下SDK里自动生成的platform和xparameters.h文件。打开xparameters.h找到XPAR_XSPIPS_0_DEVICE_ID和XPAR_XSPIPS_0_BASEADDR这两个宏确认设备ID和基地址和你用的SPI控制器一致。如果不一致SDK驱动初始化时就会失败。第三新建Application Project时我建议直接选空工程Empty Application不要选hello_world模板。模板会引入一堆初始化代码虽然不影响功能但干扰你对SPI代码结构的理解。空工程里自己写main.c结构清爽出问题也好排查。3. SDK驱动开发从初始化到成功回读的完整实现3.1 动手前先把SPI协议四种模式捋清楚SPI协议本身并不复杂四根线全双工主机产生时钟同时发送和接收数据。但“简单归简单”很多人第一次跑通后还是会遇到数据错位、偶发乱码根源往往是对四种SPI模式的CPOL和CPHA理解不透。CPOL决定时钟空闲时的电平0代表空闲为低1代表空闲为高。CPHA决定数据是在哪个边沿被采样0代表第一个边沿采样1代表第二个边沿采样。组合起来就是表格里这四种模式。SPI模式CPOLCPHASCLK空闲电平数据采样边沿常见外设Mode 000低第一个边沿上升沿大多数SPI Flash、传感器Mode 101低第二个边沿下降沿部分音频芯片Mode 210高第一个边沿下降沿少数LCD驱动Mode 311高第二个边沿上升沿部分Flash、SD卡我调试时用的SPI Flash是W25Q16默认就是Mode 0或Mode 3。很多SPI器件都支持Mode 0和Mode 3因为这两种模式下时钟空闲电平和数据采样边沿刚好都满足器件要求。如果选错了模式典型现象是数据能发出去但读回来是乱的或者根本无响应。如果不想记这些还有个笨办法看外设手册里的时序图对照时序图把CPOL和CPHA设置好。看得多了就会发现Mode 0确实是绝大多数器件的默认选择。3.2 XSpiPs驱动初始化与参数配置SDK里PS端SPI的驱动是XSpiPs初始化流程很有套路。我用的是轮询方式适合中小数据量传输代码清晰也容易调。先上一段完整的初始化代码。#include xspips.h #include xspips_hw.h #include xparameters.h #include xil_printf.h #define SPI_DEVICE_ID XPAR_XSPIPS_0_DEVICE_ID static XSpiPs SpiInstance; static XSpiPs_Config *SpiConfig; int SpiInit(void) { int Status; SpiConfig XSpiPs_LookupConfig(SPI_DEVICE_ID); if (SpiConfig NULL) { xil_printf(SPI LookupConfig failed\r\n); return XST_FAILURE; } Status XSpiPs_CfgInitialize(SpiInstance, SpiConfig, SpiConfig-BaseAddress); if (Status ! XST_SUCCESS) { xil_printf(SPI CfgInitialize failed\r\n); return XST_FAILURE; } Status XSpiPs_SetOptions(SpiInstance, XSPIPS_MASTER_OPTION | XSPIPS_FORCE_SSELECT_OPTION | XSPIPS_MANUAL_START_OPTION); if (Status ! XST_SUCCESS) { xil_printf(SPI SetOptions failed\r\n); return XST_FAILURE; } XSpiPs_SetClkPrescaler(SpiInstance, XSPIPS_CLK_PRESCALE_16); XSpiPs_SetSlaveSelect(SpiInstance, 0x01); return XST_SUCCESS; }逐个说明这段代码里关键函数的作用。LookupConfig是根据设备ID找到SPI控制器的配置结构体里面包含了基地址等关键信息。CfgInitialize则把控制器初始化到可用状态。SetOptions是这一步的重头戏。MASTER_OPTION把控制器设置为主机模式。FORCE_SSELECT_OPTION表示片选信号由软件强制控制不会在每个字节传输完成后自动拉高这个选项对连续读多字节数据非常重要。MANUAL_START_OPTION表示写入数据后不会自动启动传输必须显式调用XSpiPs_Start才会产生时钟。这三个选项组合起来让我能完全掌控一次SPI事务的片选和启动时机。SetClkPrescaler是时钟分频配置。ZYNQ PS端SPI的参考时钟一般来自IOU时钟默认通常是100MHz左右。我设置为16分频SCLK大约6.25MHz。这个速率对于W25Q16这种Flash来说绰绰有余。如果你外设速率要求更快可以根据实际分频选项调整分频支持2、4、8、16一直到256。SetSlaveSelect用于选中从设备0x01表示选中SS0引脚对应的从设备。如果只有一个从设备这个值基本固定。3.3 读写函数封装以SPI Flash为例初始化好之后核心就是封装一个通用的收发函数。SPI是全双工协议主机发出一字节的同时会收到一字节。所以读操作不能只读不写必须同时发送dummy字节来产生时钟。我封装了一个SpiTransfer函数同时处理写和读int SpiTransfer(u8 *TxBuf, u8 *RxBuf, u32 Len) { u32 Timeout 1000000; u32 Sr; u32 BaseAddr SpiInstance.Config.BaseAddress; if (Len 128) { xil_printf(SpiTransfer len exceed 128\r\n); return XST_FAILURE; } /* 清空RX FIFO防止残留数据干扰本次读取 */ Sr XSpiPs_ReadReg(BaseAddr, XSPIPS_SR_OFFSET); while ((Sr XSPIPS_SR_RX_EMPTY_MASK) 0) { XSpiPs_ReadReg(BaseAddr, XSPIPS_RXDR_OFFSET); Sr XSpiPs_ReadReg(BaseAddr, XSPIPS_SR_OFFSET); } XSpiPs_Write(SpiInstance, TxBuf, Len); XSpiPs_Start(SpiInstance); while (XSpiPs_IsBusy(SpiInstance)) { if (--Timeout 0) { xil_printf(SpiTransfer timeout\r\n); return XST_FAILURE; } } XSpiPs_Read(SpiInstance, RxBuf, Len); return XST_SUCCESS; }这里有个非常关键的细节XSpiPs_Write和XSpiPs_Read一次最多处理128字节因为PS端SPI控制器的FIFO深度就是128字节。超过这个长度驱动内部会直接失败或者只处理一部分这就是很多人读大块数据时读到一半就错乱的原因。解决方法是把大于128字节的传输拆成多个事务。以读W25Q16芯片的JEDEC ID为例这是验证SPI通路最简单粗暴的方法。JEDEC ID是指令0x9F发送后芯片会返回3个字节的ID。主机需要发送4个字节0x9F加上三个dummy字节同时接收4个字节其中后三个是有效的ID值。u8 TxData[4] {0x9F, 0x00, 0x00, 0x00}; u8 RxData[4] {0}; int ReadJedecId(void) { int Status; Status SpiTransfer(TxData, RxData, 4); if (Status ! XST_SUCCESS) { return Status; } xil_printf(JEDEC ID: %02X %02X %02X\r\n, RxData[1], RxData[2], RxData[3]); return XST_SUCCESS; }如果SPI通路正常打印出来应该类似“JEDEC ID: EF 40 15”这是Winbond W25Q16系列的标准ID。如果打印全是0xFF说明芯片没有应答问题大概率在片选、时钟或者模式配置上。再举一个读Flash数据的例子。读数据指令是0x03后面跟24位地址。如果要连续读多个字节需要保持片选一直为低。这正好用到了前面FORCE_SSELECT选项。如果只发一个读命令然后CS就拉高了Flash会认为传输结束后面的数据根本不会出来。#define DUMMY 0xFF int ReadFlashData(u32 Addr, u8 *Data, u32 Len) { u8 CmdBuf[4]; u8 DummyBuf[256]; u8 RxBuf[4]; int Status; /* 第一次事务发送读命令和24位地址 */ CmdBuf[0] 0x03; CmdBuf[1] (Addr 16) 0xFF; CmdBuf[2] (Addr 8) 0xFF; CmdBuf[3] Addr 0xFF; Status SpiTransfer(CmdBuf, RxBuf, 4); if (Status ! XST_SUCCESS) { return Status; } /* 第二次事务连续读出Len字节数据 */ memset(DummyBuf, DUMMY, sizeof(DummyBuf)); /* 注意Len不能超过128DummyBuf长度根据实际调整 */ Status SpiTransfer(DummyBuf, Data, Len); if (Status ! XST_SUCCESS) { return Status; } return XST_SUCCESS; }看到这里你应该明白SPI读操作本质上就是主机用0xFF作为时钟发生器从机在时钟边沿把数据放到MISO线上。发送的dummy字节内容本身无关紧要重要的是时钟在走片选保持有效。3.4 硬件片选与软件片选的应用差异关于片选这个点值得单独拎出来讲因为很多人栽在这里。前面提到FORCE_SSELECT_OPTION它对应的是“软件片选”思路片选信号的有效和释放由软件通过寄存器控制。在不设置FORCE_SSELECT的情况下硬件会自动管理片选每次调用XSpiPs_Start启动传输CS自动拉低传输完成CS自动拉高。这对单字节读写是没问题的但对于“先发命令和地址再连续读数据”这种需要片选持续为低的操作自动机制就是灾难因为命令阶段结束后CS已经拉高了从机根本不会理你后面的数据。我初始化的代码里用了FORCE_SSELECT和MANUAL_START手动启动手动控制片选。流程是设置片选有效写数据启动等传输完成读数据。如果要结束一次事务就把SetSlaveSelect的值改为0让CS释放。void SpiCsLow(void) { XSpiPs_SetSlaveSelect(SpiInstance, 0x01); } void SpiCsHigh(void) { XSpiPs_SetSlaveSelect(SpiInstance, 0x00); }有些外设对片选时序极敏感比如SPI Flash写状态寄存器、写使能这类指令要求CS在指令字节结束后有严格的释放时间。用硬件自动片选时传输结束CS立即拉高这个时序其实是由硬件保证的反而更可靠。而连续读数据时软件片选能让CS保持拉低完成整个读序列后再释放。判断该用哪种方式核心就一句话看这次SPI事务是否需要片选持续有效。如果一次事务只包含单个命令或单个字节自动片选没问题如果一次事务包含命令、地址、数据三个阶段且中间CS不能拉高就必须用软件片选或FORCE_SSELECT模式。4. 调试实录那些年SPI踩过的坑4.1 典型异常现象与排查思路SPI调不通的时候现象看起来千奇百怪归纳起来其实就几类读回全0xFF、数据错位、偶发乱码、SCLK没有输出、CS时序不对。读回全0xFF是最高频的问题。所有字节都是0xFF说明从机没有任何数据返回。按这个顺序排查先量MISO引脚电平确认该引脚不是一直悬空再用示波器看SCLK有没有时钟输出然后看CS是不是全程高电平如果CS从未拉低从机完全不会工作最后检查SPI模式是否匹配比如外设要求Mode 0你配成了Mode 3时序完全对不上。数据错位的典型表现是能收到数据但字节位置不对或者第一个字节总是多出来的。这通常是因为RX FIFO里有残留数据上电后没有清空。我在SpiTransfer函数里加了清空FIFO的逻辑就是针对这个问题。另一个常见原因是SPI模式设置错了导致从机在错误的边沿送出数据主机在错误的边沿采样收到的数据自然不对。偶发乱码一般和环境干扰或时序裕量不足有关优先检查SCLK线上是否需要串电阻MISO和MOSI走线是否太长共地是否可靠。软件上可以尝试降低SPI时钟分频看问题是否消失。如果降速后乱码消失基本可以确定是硬件信号质量问题。SCLK完全没有输出这个问题多半出在硬件配置阶段。检查一下Vivado里是否真的使能了SPI控制器MIO引脚是否选对了Bank参考时钟是否正常。XSpiPs初始化如果基地址都不对控制器根本没工作自然没有时钟。4.2 回环测试与波形验证方法我在调试这路SPI时做过一次回环测试效果立竿见影。所谓回环就是把MISO和MOSI两根线在外部短接主机发什么就从MISO线上读回什么。如果SPI控制器本身工作正常发送0xA5就应该读回0xA5。回环测试的价值在于它能帮你把“控制器问题”和“外设问题”一刀切开。如果回环测试通不过说明问题出在ZYNQ这边的配置或者物理链路跟外设毫无关系。如果回环测试能通过再接上SPI Flash读不到ID那问题大概率在外设连接、电平、外设初始状态这些方面。u8 TestTx 0xA5; u8 TestRx 0x00; int LoopBackTest(void) { SpiTransfer(TestTx, TestRx, 1); if (TestRx TestTx) { xil_printf(Loopback test pass: %02X\r\n, TestRx); return XST_SUCCESS; } else { xil_printf(Loopback test fail: tx %02X rx %02X\r\n, TestTx, TestRx); return XST_FAILURE; } }回环测试通过后再挂上真正的外设设备。这样每一步的变量都很少定位问题非常快。我当时的操作顺序是先跑回环再读Flash的JEDEC ID再读Flash状态寄存器最后才做页编程和读数据。每一步都确认无误再进下一步。有条件的话用逻辑分析仪抓一下SPI时序。重点看三个东西CS低电平的时间窗口是否覆盖了整个命令数据阶段SCLK是否连续输出中间有没有异常的毛刺MOSI上的数据字节是否和你代码里发的一致。拿到波形图很多问题一眼就能看出来。4.3 问题快查表最后分享一张我调试SPI时经常对照的快查表。这些现象和原因都是我实际遇到的不一定覆盖所有情况但能帮你在抓狂时快速缩小范围。异常现象可能原因排查方向读回全0xFF外设无响应、CS未拉低、MISO悬空示波器看CLK/MOSI/CS波形万用表量MISO数据错位CPHA/CPOL配置错误、RX FIFO残留查看外设手册时序图初始化时清空FIFO偶发乱码信号质量差、时钟过冲SCLK串电阻、降低分频、缩短走线SCLK无输出SPI控制器未使能、MIO配置错误检查Vivado配置、xparameters.h地址CS时序不对自动片选与连续读冲突改FORCE_SSELECT MANUAL_STARTlen超过128报错XSpiPs FIFO深度限制拆分事务按64或128字节分批传输还有一条硬件层面的经验SPI走线尽量短SCLK、MOSI、MISO三根信号线尽量保持在同一层不要跨分割。如果是飞线调试线不要太长优先用杜邦线里最短的那几根。ZYNQ的PS端SPI虽然不像高速接口那么娇气但信号完整性该注意还是要注意。另外调试SPI Flash这类外设时建议先打印每次写命令后读回的状态寄存器。W25Q系列写操作完成后状态寄存器的bit0会从1变成0表示写操作结束。如果没有这个轮询机制你连续写数据时很容易翻车因为上次写操作还没完成你又发了下一条指令。我实际调这个工程的感受是SPI跑通并不难难的是“读到的数据是错的”这种状态因为它不像“完全没有波形”那样容易定位。每次遇到奇怪的问题先问自己三个问题我的片选时序对吗我的模式配置对吗我有没有清FIFO这三个问题排查完八成以上的坑都能填上。最后再分享一个我自己一直坚持的习惯任何一块新板子拿到手的第一件事就是先把SPI回环测试跑通再去接真实外设。这个习惯帮我省了大量的排查时间。你如果也用ZYNQ做SPI开发不妨从这个工程里的回环代码开始验证自己的板子确认通路没问题了再上真正的Flash或者传感器。本文还有配套的精品资源点击获取