简介:本资源是面向嵌入式开发工程师与FPGA协同开发者的一套ZYNQ 7020平台SPI外设驱动实践方案,聚焦于ARM端SDK环境下标准SPI读写功能的完整实现,解决软硬件协同调试中驱动初始化、时序配置、中断响应及数据收发等核心问题。压缩包共含多个C源文件、头文件、BSP配置脚本及Xilinx SDK工程文件,涵盖SPI控制器初始化、主模式读写函数、极性/相位参数配置、超时错误处理及基础寄存器访问逻辑,总大小为7.98MB。已有405人学习下载,适用于ZYNQ初学者进阶实践或工业通信模块快速原型开发。读者可直接导入SDK工程编译运行,结合硬件平台验证SPI通信时序与数据一致性,并参考代码结构理解Xilinx官方驱动框架设计逻辑,为后续扩展DMA传输或从设备驱动开发奠定坚实基础。 在ZYNQ 7020上做SPI读写,这个问题我在实际项目里被问过太多次了。很多人一提到FPGA做SPI,第一反应就是在PL侧用Verilog/VHDL撸一个SPI时序,或者直接调用Xilinx的SPI IP核,完全不记得PS侧本身就带着两个SPI控制器。其实ZYNQ-7020的PS端集成了两个SPI控制器(SPI0和SPI1),支持主从模式、FIFO缓冲、三档片选,用SDK驱动操作起来比在PL里写时序快得多,尤其在数据量不大、时序要求不极端的场景下,PS端SPI完全够用。
这篇文章就是围绕"ZYNQ 7020用SDK驱动实现SPI读写"这条主线来写的,内容包括Vivado里的硬件搭建、SDK里的驱动调用逻辑、片选与时钟这两个最容易翻车的点,以及最终用回环测试和真实SPI设备验证。适合两类人看:一类是刚接触ZYNQ、在PS和PL之间纠结选哪条路的初学者;另一类是已经在用PS端SPI但被片选、FIFO、时钟分频问题卡了几天的工程师。
1. 为什么在7020上做SPI通信,很多人第一个想到的是PL而不是PS
这个现象很有意思。我在不少技术交流和培训场合问过同样的问题:ZYNQ做SPI通信,你会用PS还是PL?大多数人的第一反应是PL,理由无外乎"FPGA不就是干这个的嘛,时序可控、速率高"。但如果你的SPI设备速率需求在几十MHz以内、通信逻辑不复杂,PS端自带的SPI控制器反而是更务实的选择。
1.1 PS端SPI控制器到底能干什么
ZYNQ-7020的PS端集成两个SPI控制器,每个控制器具备以下能力:
- 支持SPI主机模式(Master)和从机模式(Slave),多数应用用到的是主机模式。
- 支持标准SPI、双通道(Dual)、四通道(Quad)模式,在连接SPI Flash这类器件时比较有用。
- 内置128字节的收发FIFO,发送和接收各64字节。
- 提供3个片选信号(SS0、SS1、SS2),可以挂多个从设备。
- 支持时钟极性和相位可编程配置,对应SPI Mode 0到Mode 3。
- 时钟分频由内部波特率发生器完成,可以按需配置SCLK频率。
这些特性意味着,PS端SPI不是"阉割版"的SPI,而是一个功能完整的控制器。尤其是三档片选和Dual/Quad模式,比你自己在PL里写Verilog要省事得多。
1.2 什么时候必须用PL做SPI
PS端SPI控制器当然不是万能的,下面几种情况我会建议走PL路线:
- 时钟频率要求超过PS控制器上限。PS端SPI的SCLK最高频率取决于参考时钟和分频比,通常到几十MHz没问题,但如果你追求100MHz以上,就要用PL侧的SPI IP或者自己写RTL。
- 需要自定义时序。比如某些传感器要求特殊的CS保持时间、特定字节间隙,或者需要多字节连续传输过程中切换MOSI方向,这些定制化要求用PS固定控制器很难满足。
- 需要高密度并行多路SPI。如果你的板子上有8路、16路SPI从设备需要同时采集,PS端两个控制器就不够用了,PL侧例化多个SPI IP更现实。
- 需要把SPI数据直接接入DMA或自定义数据通路。虽然PS端SPI也可以配合DMA,但灵活性不如PL侧自己掌控。
所以在项目启动阶段,先做一个判断:如果速率不极端、协议不诡异、路数不多,直接用PS端SPI,能省下不少开发和调试时间。
1.3 一个容易忽略的前提:MIO还是EMIO
在Vivado里配置PS端SPI时,引脚可以选择落在MIO上,也可以引到EMIO再通过引脚约束连到PL侧的普通IO。
MIO路线:SPI引脚直接连接到PS的MIO引脚,不需要在约束文件里写引脚位置,上电即用,不占PL逻辑资源。缺点是MIO引脚位置是固定的,如果PCB上没有把SPI信号引到你想要的引脚,就没得选。
EMIO路线:SPI控制器信号先从PS出来,通过EMIO接口进入PL,再通过普通IO引脚外接。优点是可以自由选择引脚位置,灵活性更高;缺点是要手动添加引脚约束,并且占用了PL侧的IO资源。
在7020上,MIO引脚有限,如果MIO已经被UART、SDIO、USB等占满,或者PCB布局导致MIO无法走线,那就只能走EMIO。这个选择在Block Design阶段就要确定,后面改起来比较麻烦。
2. Block Design里SPI外设的搭建与引脚分配陷阱
确定了走PS端SPI之后,Vivado里的操作其实很直接,但有几个细节不留意会卡住后面SDK的开发。
2.1 使能SPI控制器的完整步骤
在Vivado里新建或打开Block Design,添加ZYNQ7 Processing System IP核后,需要双击该IP核进入配置界面,在Peripheral IO选项里把SPI 0或者SPI 1勾上。我一般习惯勾选SPI 1,因为SPI 0的MIO引脚经常和SD卡、NAND等接口复用,容易冲突。
勾选SPI之后,还需要注意以下几点:
- 在MIO Configuration里确认SPI引脚的MIO编号,避免与其他外设冲突。
- 如果想走EMIO,则不要勾选MIO上的SPI引脚,而是在SPI配置里选择EMIO,然后引出SPI0/1的接口信号。
- 如果使用EMIO,需要在Constraints文件中添加引脚约束,生成比特流(bitstream)。
- 在时钟配置里,确保给SPI控制器所在的处理器时钟域提供合理的参考时钟,ZYNQ的SPI控制器时钟来自CPU的IO PLL或者外设时钟域,通常默认的100MHz就行。
导出硬件时,注意勾选"包括比特流"选项,因为EMIO方式下的PL引脚约束信息需要通过比特流加载。
2.2 硬件平台导出与SDK工程创建
这一步很多人是因为操作顺序不对而卡住。无论用Vivado 2019.1还是Vitis 2024,核心逻辑都是一样的:
- 在Vivado中完成Block Design和约束后,生成比特流。
- File -> Export Hardware,选择包括比特流,生成XSA文件。
- 打开Vitis(老版本叫SDK),创建一个新的平台工程(Platform Project),指向这个XSA文件。
- 基于该平台工程,创建应用工程(Application Project)。
- 在应用工程中,可以用Xilinx提供的模板,也可以手动添加C文件,编写SPI读写代码。
在Vitis 2024版本中,流程基本一致,只是界面和术语有些变化,但核心仍然是"XSA文件承载硬件配置信息"。遇到过不少人在这一步忘记"包括比特流",导致下载运行时PL侧没有任何逻辑,EMIO引脚自然也不工作。
2.3 引脚分配中让我踩过一次的坑
有一次我把SPI信号通过EMIO引出到PL引脚,在约束文件里写了引脚位置,生成了比特流,然后下载进板子发现SPI时钟完全没输出。排查了半天,最后发现是复位顺序的问题:PS端SPI控制器在软件初始化之前,EMIO引脚的默认状态是不确定的,而且PL侧的IO需要等待配置完成和释放复位。如果是通过SDK下载程序自动加载比特流,IO初始状态受硬件约束管理,但如果手动单独下载比特流再运行程序,就可能出现IO未被正确初始化的情况。
这类问题其实有章可循:排查时先确认引脚约束语法,再确认EMIO接口信号方向,最后确认PS-PL复位顺序。多数情况下,问题出在缺少或写错了引脚约束,或者导出XSA时未包含比特流。
3. SDK驱动中SPI初始化的关键参数与执行流程
到了SDK阶段,主要工作就是熟悉Xilinx提供的SPI驱动库。PS端SPI的驱动文件是xspips.h和xspips.c,底层实现已经封装好了,但有几个参数和调用顺序,初学者特别容易理解错。
3.1 SPI驱动初始化的标准代码
用PS端SPI做主机,初始化流程一般是:查找设备配置、初始化驱动、配置工作模式、配置片选、配置时钟分频。以下是我在7020上用SPI1的初始化代码示例:
#include "xspips.h" #include "xparameters.h" #include "xstatus.h" #define SPI_DEVICE_ID XPAR_XSPIPS_1_DEVICE_ID #define SPI_CLOCK_HZ 1000000 // 期望的SCLK频率 XSpiPs SpiInstance; XSpiPs_Config *SpiConfig; int SpiInit(void) { int Status; // 1. 查找SPI控制器配置 SpiConfig = XSpiPs_LookupConfig(SPI_DEVICE_ID); if (SpiConfig == NULL) { return XST_FAILURE; } // 2. 初始化SPI控制器 Status = XSpiPs_CfgInitialize(&SpiInstance, SpiConfig, SpiConfig->BaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } // 3. 设置为主机模式,默认禁止从机模式 Status = XSpiPs_SetOptions(&SpiInstance, XSPIPS_MASTER_OPTION | XSPIPS_MANUAL_CS_OPTION); if (Status != XST_SUCCESS) { return XST_FAILURE; } // 4. 设置时钟分频,使SCLK接近目标频率 Status = XSpiPs_SetClkPrescaler(&SpiInstance, SpiConfig->MaxFrequency); if (Status != XST_SUCCESS) { return XST_FAILURE; } // 5. 默认选中SS0 XSpiPs_SetSlaveSelect(&SpiInstance, 0x01); return XST_SUCCESS; }代码里有一个地方特别容易误用:XSpiPs_SetClkPrescaler的第二个参数,很多资料写的是"分频值",但实际上它的含义是"期望的SCLK频率",驱动会根据这个值计算出实际分频比,再配置到寄存器里。如果你直接传一个分频比数字,得到的时钟频率和你预期的会差很多。
3.2 初始化背后发生了什么
XSpiPs_CfgInitialize做的事比较多:
- 读取硬件配置,确认SPI控制器基地址。
- 复位SPI控制器,把所有寄存器的值清零到默认状态。
- 禁止SPI控制器,等待后续配置完毕后再使能。
- 配置中断相关寄存器。
XSpiPs_SetOptions设置控制器的工作模式。常用的选项有:
XSPIPS_MASTER_OPTION:主机模式。XSPIPS_SLAVE_OPTION:从机模式。XSPIPS_MANUAL_CS_OPTION:手动控制片选,由软件通过寄存器直接选择从设备。XSPIPS_FORCE_CS_OPTION:强制片选,在整个传输过程中保持有效。
有一点要特别注意:XSpiPs_SetOptions里如果不加XSPIPS_MANUAL_CS_OPTION,控制器会自动管理片选信号,在每次事务结束后片选会恢复为无效。但如果使用XSPIPS_MANUAL_CS_OPTION,就需要自己调用XSpiPs_SetSlaveSelect来选择要操作的从设备。这个手动和自动的选择,会直接影响你后面读多字节数据的稳定性和CS时序。
3.3 中断方式还是轮询方式
SDK里的SPI驱动支持轮询和中断两种模式。轮询模式简单,直接调用XSpiPs_PolledTransfer或XSpiPs_Transfer即可;中断模式需要初始化中断控制器、注册中断回调函数,代码量更大,但更稳定,特别是在和操作系统或复杂任务并发时。
我个人的经验是:裸机环境下,如果SPI通信数据量不大,且对实时性要求不高,直接用轮询模式就够了,代码简单,出问题也好排查。但如果是64字节以上的数据块传输,或者需要在后台持续采集传感器数据,建议上中断,因为SPI控制器的FIFO只有64字节,轮询大数据块时如果没有及时读取接收FIFO,很容易溢出丢数据。
4. 硬件片选与软件片选的取舍:多设备场景下的实战选择
片选问题是ZYNQ SPI开发里被问得最多的点之一。在SDK驱动里,片选有"硬件自动管理"和"软件手动控制"两种方式,网上关于这两者的讨论也比较多,这里结合7020的实际行为展开说说。
4.1 硬件片选与软件片选的行为差异
所谓硬件片选,就是SPI控制器在启动一个传输事务时,会自动把对应的片选信号拉低,传输结束后自动拉高。软件片选则是由软件通过寄存器设置片选电平,之后不依赖控制器自动翻转。
在SDK驱动中,使用XSPIPS_FORCE_CS_OPTION选项可以实现强制片选,即片选信号在整个传输期间保持有效,适用于需要多个SPI事务连续操作同一个从设备(比如读SPI Flash的JEDEC ID、读状态寄存器)的场景。
两者有一个关键区别:
| 片选方式 | 选项 | 适用场景 | 注意点 |
|---|---|---|---|
| 硬件自动片选 | 默认 | 每个事务独立传输 | 事务之间CS会拉高,连续读操作可能被从设备当作一次传输结束 |
| 软件手动片选 | XSPIPS_MANUAL_CS_OPTION | 多个事务连续操作同一个设备 | 需要手动设置SlaveSelect,结束后手动释放 |
| 强制有效片选 | XSPIPS_FORCE_CS_OPTION | 读操作需要连续时钟的场景 | 只影响当前事务,事务结束自动释放 |
例如读AD7606这种ADC芯片,读取转换结果时需要CS在整个读取过程中保持低电平,如果使用默认的自动片选,每个字节之间CS会拉高,ADC可能认为本次读取结束,返回的数据就不对了。这时候就要结合XSPIPS_FORCE_CS_OPTION或者手动控制GPIO实现CS。
4.2 多设备挂载时的地址选择
7020的每个SPI控制器有3个片选信号(SS0、SS1、SS2),理论上可以挂3个SPI从设备。通过XSpiPs_SetSlaveSelect(&SpiInstance, slave_mask)选择具体设备,slave_mask是位掩码,例如0x01对应SS0,0x02对应SS1,0x04对应SS2。
实际的业务场景里我踩过一个坑:SPIFlash挂在SS1上,传感器挂在SS0上,程序运行时,初始化代码里执行了XSpiPs_SetSlaveSelect(&SpiInstance, 0x01)选择Flash,读完Flash之后切换到传感器,执行XSpiPs_SetSlaveSelect(&SpiInstance, 0x02),但紧接着读取传感器数据时发现读出来的全是0xFF。排查后发现是片选切换的时序问题:从一个从设备切换到另一个从设备时,需要保证前一个事务彻底结束,片选信号完全释放,再切换下一个片选。我在代码里加了一点延时,问题就解决了。
4.3 片选信号不够用怎么办
如果需要挂载超过3个SPI从设备,可以用GPIO控制额外的CS引脚,实现"软件扩展片选"。做法是:
- 在Block Design里把额外的CS引脚配置为GPIO(MIO或者EMIO都可以)。
- SDK初始化阶段,把这些GPIO配置为输出并拉高(初始化无效状态)。
- 操作某个设备时,先把对应GPIO拉低,选通设备,操作完后再拉高。
这种方式其实就是手动扩展片选,在嵌入式系统里很常见。不过需要注意,如果使用PS端GPIO(MIO)控制片选,GPIO的翻转速度比SPI控制器内部的硬件片选更快,如果GPIO拉低后立刻发起SPI事务,某些从设备可能因为CS建立时间太短而无法响应。建议在GPIO拉低后加一小段延时,或者至少确保CS低电平的建立时间满足从设备手册要求。
5. 回环测试和真实设备读写:验证驱动是否真正可用的标准动作
初始化代码写完、编译下载之后,不要急着接外部设备,先在板子上做一套完整的验证流程。SPI通信最常见的故障就是"代码看起来没问题,但就是读不到数据",这时候回环测试是最有效的定位手段。
5.1 自环测试:把MOSI和MISO短接
一种最直接的验证方法,就是把SPI主机的MOSI和MISO在物理上短接。这样主机发送的数据会被自己的接收通道收到,可以验证整个SPI控制器的收发链路是否正常。
在SDK中实现自环测试很简洁:
int SpiLoopbackTest(void) { u8 SendBuffer[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; u8 RecvBuffer[8] = {0}; int Status = XSpiPs_PolledTransfer(&SpiInstance, SendBuffer, RecvBuffer, 8); if (Status != XST_SUCCESS) { return XST_FAILURE; } for (int i = 0; i < 8; i++) { if (SendBuffer[i] != RecvBuffer[i]) { return XST_FAILURE; } } return XST_SUCCESS; }这里要注意的是字节数和FIFO的关系。SPI控制器的接收FIFO为64字节,如果你一次传输超过64字节,轮询模式下必须及时读取FIFO,否则会溢出。我建议自环测试时先传小于64字节的数据,确认链路正常后再扩展。
还有一个细节:自环测试通过后,只能说明PS端SPI控制器自身收发没问题,不能证明外部SPI设备能正常工作。外部设备还需要单独验证。
5.2 真实设备读写:以SPI Flash为例
自环测试通过后,就可以接真实设备了。我以W25Q16 SPI Flash为例,说明一个完整的读写流程:读JEDEC ID是最简单的验证,因为不涉及写操作,只需要发送9个时钟周期的命令就能拿到3字节的设备ID。
int SpiReadJedecId(u8 *pId) { u8 WriteBuf[4] = {0x9F, 0x00, 0x00, 0x00}; // JEDEC ID命令 u8 ReadBuf[4] = {0}; int Status = XSpiPs_PolledTransfer(&SpiInstance, WriteBuf, ReadBuf, 4); if (Status != XST_SUCCESS) { return XST_FAILURE; } pId[0] = ReadBuf[1]; pId[1] = ReadBuf[2]; pId[2] = ReadBuf[3]; return XST_SUCCESS; }JEDEC ID读出来是3个字节,比如W25Q16的ID是0xEF 0x40 0x15。如果你读到的全是0xFF,往往说明片选没有选中设备、SPI模式不匹配、或者时钟频率太高导致从设备无法响应。
5.3 读状态寄存器与写使能
SPI Flash的写操作比读操作麻烦一些,必须遵循"写使能 -> 擦除/写入 -> 等待完成"的流程。在等待Flash内部操作完成时,通常通过读状态寄存器(0x05命令)判断BUSY位。
int SpiWaitFlashReady(void) { u8 WriteBuf[2] = {0x05, 0x00}; // 读状态寄存器命令 u8 ReadBuf[2] = {0}; int Timeout = 100000; while (Timeout--) { XSpiPs_PolledTransfer(&SpiInstance, WriteBuf, ReadBuf, 2); if ((ReadBuf[1] & 0x01) == 0) { // BUSY位为0 return XST_SUCCESS; } } return XST_FAILURE; }等待循环里加一个超时计数很重要。有一次我调试Flash读ID正常,但写数据总是不成功,程序卡在忙等待里出不来,添加超时后定位到是Flash的写保护寄存器被设置了,导致写操作始终没有被接受。如果不用超时机制,这种问题很难自动发现。
5.4 遇到读不到数据时的排查顺序
SPI调试是一个典型的"信号链路逐级排查"过程。我总结了一套排查顺序,供参考:
- 先确认时钟是否输出。用示波器或逻辑分析仪测SCLK引脚,如果没有时钟,问题在控制器配置或引脚约束。
- 确认MOSI是否输出。发送数据时观察MOSI引脚,如果没有数据,检查发送缓冲区和命令是否正确。
- 确认CS是否正确拉低。如果CS信号始终为高,从设备根本不会响应。
- 确认MISO信号是否回来。如果MISO始终为高或低,说明从设备没有驱动MISO,可能是SPI模式不匹配或设备未选通。
- 检查电平标准。ZYNQ的MIO和PL的IO大多支持1.8V/2.5V/3.3V,如果外部设备的电平域和ZYNQ不一致,需要在两者之间加电平转换芯片。
排查过程中,示波器和逻辑分析仪几乎是必需品,特别是查SPI时序问题,不能只靠软件打印数据。回环测试是排除软件问题的利器,把发送和接收短接后如果数据一致,说明软件层没问题,问题大概率在硬件连接或外部设备配置上。
6. 时钟频率、SPI工作模式与常见Error的对应关系
这一节单独拿出来讲,是因为很多稀奇古怪的问题,归根结底都出在时钟或SPI Mode不匹配上,而且SDK里返回的错误码往往不会直接告诉你"时钟频率太高"或者"模式不对",需要自己对照手册分析。
6.1 SPI Mode的选择与被遗忘的CPOL/CPHA
SPI通信有4种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。很多从设备手册上会写"SPI Mode 0"或"SPI Mode 3",但没有解释这意味着什么。简单回忆一下:
- Mode 0(CPOL=0, CPHA=0):空闲时SCLK为低电平,数据在第一个边沿(上升沿)采样。
- Mode 1(CPOL=0, CPHA=1):空闲时SCLK为低电平,数据在第二个边沿(下降沿)采样。
- Mode 2(CPOL=1, CPHA=0):空闲时SCLK为高电平,数据在第一个边沿(下降沿)采样。
- Mode 3(CPOL=1, CPHA=1):空闲时SCLK为高电平,数据在第二个边沿(上升沿)采样。
在SDK驱动中,通过XSpiPs_SetOptions的XSPIPS_CLK_PHASE_OPTION和XSPIPS_CLK_POLARITY_OPTION来配置:
// 配置为SPI Mode 0 Status = XSpiPs_SetOptions(&SpiInstance, XSPIPS_MASTER_OPTION | XSPIPS_MANUAL_CS_OPTION); // 配置为SPI Mode 3 Status = XSpiPs_SetOptions(&SpiInstance, XSPIPS_MASTER_OPTION | XSPIPS_MANUAL_CS_OPTION | XSPIPS_CLK_POLARITY_OPTION | XSPIPS_CLK_PHASE_OPTION);我遇到过一次很典型的故障:用SPI读取一颗ADC芯片的数据,配置的是Mode 0,读出来的数据偶尔会跳变。后来用逻辑分析仪抓时序,MISO上的数据在采样边沿附近发生变化,导致采样点不稳定。改成Mode 3之后就稳定了。原因很简单:这颗ADC手册里要求的是Mode 3,我默认按Mode 0配置了。
所以在连接新设备之前,第一件事就是查清楚该设备支持的SPI Mode,不要想当然。很多设备手册里表格式的时序图会标明默认的Mode,直接照抄即可。
6.2 时钟分频计算与最大频率限制
SPI控制器的SCLK频率由参考时钟和波特率分频器共同决定。SDK驱动里,XSpiPs_SetClkPrescaler的第二个参数是期望频率,驱动会计算实际分频比。但需要注意,实际得到的SCLK频率可能并不是期望值,而是接近期望值的某个分频结果。
ZYNQ PS端SPI时钟分频寄存器是 8-bit 的,支持的分频系数一般是 2、4、6、8 ... 256 这些偶数(实际上还会受到参考时钟的限制)。因此如果期望频率设得很高,超出了最大允许频率,驱动会返回错误,或者自动配置出接近的值。
我在实际项目中,曾试图把SCLK配到40MHz,结果读取外部ADC信号时发现转换结果经常出现丢位的情况。排查后发现,不是SPI控制器的速度上限不够,而是PCB走线过长,信号完整性变差,40MHz的边沿太陡,反射和串扰导致采样点不可靠。后来把SCLK降到10MHz就稳定了。
经验法则:SPI走线长度超过5cm时,尽量不要把SCLK配到20MHz以上;超过10cm时建议降到10MHz以下。这个不是绝对标准,但能避开大部分信号完整性问题。
6.3 SDK返回Error的常见含义与处理
开发过程中,经常会在终端里看到SPI驱动返回错误码。我自己整理了一个对照表:
| 错误现象 | 可能原因 | 处理方式 |
|---|---|---|
| XST_SUCCESS返回错误 | 参数非法或初始化未完成 | 检查设备ID、基地址和初始化顺序 |
| 传输数据不对,全0xFF | 从设备未选中或MISO未连接 | 检查CS、MISO接线和电平标准 |
| 传输数据不对,全0x00 | MOSI未连接或从设备未驱动MISO | 检查MOSI连接 |
| SCLK无输出 | 控制器未使能或时钟分频配置错误 | 检查控制器使能状态和分频寄存器 |
| 偶发数据错误 | 时钟频率过高或信号完整性差 | 降低SCLK,检查PCB走线 |
| 读操作CS期间被拉高 | 使用了自动片选,每个字节间CS释放 | 改用FORCE_CS或手动控制GPIO |
尤其注意一点:SDK驱动在XSpiPs_PolledTransfer返回失败时,并不总是说明总线错误,有时候是参数校验没过,比如传输的字节数为0、缓冲区为空等。这类"软件层面"的校验错误在调试时很容易被误认为是硬件问题,所以先确保传入参数没问题,再朝硬件方向排查。
6.4 与DMA配合时的同步问题
PS端SPI控制器支持DMA传输,适合大数据量的SPI读写。在SDK里,使用DMA模式需要额外初始化DMA控制器,配置传输方向、长度和中断。相比轮询模式,DMA模式能减轻CPU负担,但调试复杂度也会上升。
如果是为了提高吞吐量而用DMA,需要注意两点:
- SPI控制器和DMA之间的握手信号需要正确配置,尤其是
XSpiPs_SetDmaOptions和中断回调函数里的状态判断。 - DMA传输的字节粒度可能有对齐要求(比如4字节对齐),不满足时会出现总线错误。
- 实际项目里,如果只是偶尔读写几十个字节,DMA带来的收益并不明显,轮询模式反而是最简单可靠的方案。
DMA用在一个特定的高速数据采集项目上,当时要求SPI连续读取ADC数据,每秒产生几兆字节的数据,轮询模式确实扛不住,DMA模式才能稳定跑。不过DMA的初始化配置明显更复杂,我的建议是先用轮询模式把业务逻辑跑通,再根据瓶颈决定是否迁移到DMA。
7. 从SDK到Vitis:工具链迁移中的几个适配点
很多人的开发环境已经从老的Xilinx SDK迁移到Vitis,或者正在迁移的路上。Vitis 2024里如何烧录7020、如何加载PL比特流、如何调试PS端应用,这些问题在SPI开发过程中同样会遇到。
7.1 Vitis下SPI工程仍然使用XSpiPs驱动
Vitis替代了SDK之后,SPI驱动API并没有变化,仍然是XSpiPs_* 这一套接口,Xilinx从传统驱动模型过渡到embeddedsw仓库,但驱动源码、函数名、行为基本保持一致。所以网上的SDK路径经验教程(包括本文的所有代码)在Vitis里依然适用。
有一个变化值得注意:Vitis里创建应用工程时,默认会生成一个"helloworld"框架,SPI初始化代码需要自己加到main函数里。同时Vitis对编译选项和库的依赖更严格,比如如果用到中断,需要确保使能了对应的驱动库。
7.2 烧录与启动方式:从JTAG下载到QSPI固化
SPI开发过程中,通常先用JTAG调试模式下载程序。当功能验证完成后,如果需要上电自启动,就需要把程序固化到启动设备中。ZYNQ 7020的启动模式支持QSPI Flash、SD卡、JTAG等。这里提一下QSPI固化的大致流程:
- 在Vivado里生成BOOT.BIN所需的三个文件:FSBL(First Stage Boot Loader)、bitstream、应用程序elf。
- 使用Xilinx提供的创建启动镜像功能,生成BOOT.BIN。
- 通过JTAG使用Vivado的Program Flash工具,把BOOT.BIN写入QSPI Flash。
- 拨动启动模式跳线到QSPI,重新上电,观察启动是否正常。
如果SPI控制器挂在PS的MIO上,固化后启动时不需要PL的参与,SPI控制器在FSBL阶段就可以工作。如果SPI引脚通过EMIO接到了PL侧,那么启动后必须等FPGA加载完成,SPI引脚才会生效。这个区别在做上电自测时容易被忽略,表现为:JTAG调试时SPI通信正常,固化后上电却读不到设备,其实就是PL还没来得及加载。
7.3 不带DDR的7020:OCM加载方式
还有一个不少人在网上问的问题:如果板子上没有DDR,ZYNQ 7020还能跑SPI读写程序吗?答案是能,但要使用OCM(On-Chip Memory)。ZYNQ-7020的OCM总容量是256KB,程序和数据都放在OCM里,足够跑一个轻量级的SPI读写应用。
在Vitis里设置链接脚本,把代码段、数据段、堆栈都放到OCM地址段,编译时去掉DDR相关地址,即可运行。这种方式适合内存需求小的场景,比如做一个简单的SPI Flash读取器、传感器采集器。但要注意OCM容量有限,不能跑复杂的嵌入式系统,也不能分配太大的缓冲区。
这一块在实际项目里我用过一次:帮一个硬件工程师调试一块不带DDR的最小系统板,PS端跑一个SPI驱动,从一颗温湿度传感器读取数据,然后通过串口打印。因为代码量非常小,OCM完全够用,硬是把一个看似"缺少内存无法跑系统"的板子做成了可用的传感器测试工具。
8. 项目做完之后,我最后想说的几个点
写到这里,ZYNQ 7020上实现SPI读写的主要技术环节都已经过了一遍。最后分享几个实际项目中的体会,可能对你有帮助。
如果你正在纠结"PS端SPI还是PL端SPI",我的建议是先评估外部设备的速率和协议复杂度,不要一上来就写Verilog。PS端SPI在7020上是一套成熟、稳定、代码量小的方案,配合SDK驱动,从搭建到调通,熟练的话半天就能搞定。而PL端SPI虽然灵活,但代码量、仿真调试成本都高不少。除非你明确知道PS端SPI满足不了需求,否则优先用PS端。
如果你已经被片选、时钟分频这类问题卡住了,建议不要反复猜测,直接上逻辑分析仪或者示波器抓信号。SPI就是一个实打实的串行协议,时序对不对,信号一看就知道。我调试SPI踩过的坑,绝大多数都是"没看时序就开始改代码",浪费时间不说,还容易把简单问题复杂化。
如果这个项目后续要扩展到Linux系统,那么SPI控制器的设备树配置、SPI驱动框架等又是一套新知识,需要另外单独梳理。裸机SDK的开发路径是理解ZYNQ底层寄存器行为的最佳方式,先把裸机跑通,再去做Linux下的SPI移植,思路会清晰很多。
本文还有配套的精品资源,点击获取