1. 项目背景与核心价值
在嵌入式系统开发中,存储设备的读写操作是基础但至关重要的功能。W25Q16作为一款常见的16Mbit串行Flash存储器,因其低成本、高可靠性和SPI接口的简洁性,被广泛应用于各种嵌入式场景。而Xilinx Zynq系列SoC的PL(Programmable Logic)侧通过AXI Quad SPI IP核实现主机模式访问,则提供了一种灵活高效的硬件加速方案。
这个实验的核心价值在于:
- 掌握Zynq PL侧硬件SPI控制器的配置与使用
- 理解AXI Quad SPI IP核在主机模式下的工作原理
- 实现W25Q16存储器的完整读写控制
- 构建软硬件协同的SPI通信体系
注意:SPI通信的时序要求严格,PL实现相比PS(Processing System)软件模拟能提供更稳定的时钟和精确的时序控制。
2. 硬件架构设计解析
2.1 系统整体框图
典型的实现架构包含以下关键组件:
code复制Zynq PS部分
|
| AXI4-Lite
|
AXI Quad SPI IP (PL)
|
| SPI总线
|
W25Q16 Flash
2.2 AXI Quad SPI IP核关键配置
在Vivado中配置IP核时需要注意以下参数:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| Mode | Standard Mode | 标准SPI模式 |
| Slave Device | Single | 单设备连接 |
| FIFO Depth | 16 | 平衡资源占用和性能 |
| Transaction Width | 8-bit | 匹配W25Q16的数据宽度 |
| SCK Ratio | 2:1 | 提供25MHz时钟(50MHz AXI时钟) |
2.3 W25Q16接口连接
Flash器件与IP核的物理连接建议:
| W25Q16引脚 | Zynq PL引脚 | 功能说明 |
|---|---|---|
| CS# | SPI_SS | 片选(低电平有效) |
| DO(IO1) | SPI_MISO | 主入从出 |
| DI(IO0) | SPI_MOSI | 主出从入 |
| CLK | SPI_SCK | 串行时钟 |
| WP#/HOLD# | 接高电平 | 禁用写保护/保持功能 |
3. 软件驱动开发详解
3.1 初始化流程
c复制// 初始化代码示例
int spi_init(XSpi *SpiInstancePtr, u16 DeviceId)
{
XSpi_Config *ConfigPtr;
// 查找IP核配置
ConfigPtr = XSpi_LookupConfig(DeviceId);
if (ConfigPtr == NULL) return XST_FAILURE;
// 初始化驱动实例
if (XSpi_CfgInitialize(SpiInstancePtr, ConfigPtr,
ConfigPtr->BaseAddress) != XST_SUCCESS)
return XST_FAILURE;
// 设置SPI模式
XSpi_SetOptions(SpiInstancePtr, XSP_MASTER_OPTION | XSP_MANUAL_SSELECT_OPTION);
XSpi_SetSlaveSelect(SpiInstancePtr, 0x01); // 选择SS0
// 启动SPI设备
return XSpi_Start(SpiInstancePtr);
}
3.2 W25Q16指令集实现
关键操作指令封装示例:
c复制#define W25Q16_WRITE_ENABLE 0x06
#define W25Q16_PAGE_PROGRAM 0x02
#define W25Q16_READ_DATA 0x03
#define W25Q16_SECTOR_ERASE 0x20
void send_spi_command(XSpi *SpiPtr, u8 cmd)
{
u8 SendBuffer[1] = {cmd};
u8 RecvBuffer[1];
XSpi_Transfer(SpiPtr, SendBuffer, RecvBuffer, 1);
}
u8 read_status_register(XSpi *SpiPtr)
{
u8 SendBuffer[2] = {0x05, 0x00}; // 状态寄存器读取指令
u8 RecvBuffer[2];
XSpi_Transfer(SpiPtr, SendBuffer, RecvBuffer, 2);
return RecvBuffer[1];
}
4. 关键操作流程实现
4.1 数据写入流程
- 发送WRITE_ENABLE指令
- 等待状态寄存器WEL位置1
- 发送PAGE_PROGRAM指令(0x02)
- 发送24位地址(MSB first)
- 发送数据(最多256字节)
- 等待写入完成(BUSY位清零)
c复制int flash_write_page(XSpi *SpiPtr, u32 addr, u8 *data, u16 len)
{
u8 cmd[4] = {W25Q16_PAGE_PROGRAM};
u8 status;
// 构造地址(24位)
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
// 使能写入
send_spi_command(SpiPtr, W25Q16_WRITE_ENABLE);
// 组合命令+地址+数据
u8 *tx_buf = malloc(4 + len);
memcpy(tx_buf, cmd, 4);
memcpy(tx_buf+4, data, len);
// 执行传输
XSpi_Transfer(SpiPtr, tx_buf, NULL, 4+len);
// 等待操作完成
while((read_status_register(SpiPtr) & 0x01) == 0x01);
free(tx_buf);
return XST_SUCCESS;
}
4.2 数据读取流程
- 发送READ_DATA指令(0x03)
- 发送24位地址(MSB first)
- 连续读取数据
- 可随时终止传输
提示:读取操作不需要等待,但要注意CS#信号的保持时间要求。
5. 性能优化技巧
5.1 使用DMA加速传输
对于大数据量传输,启用AXI Quad SPI的DMA功能:
c复制// 启用DMA的配置步骤
XSpi_SetOptions(SpiInstancePtr, XSP_MASTER_OPTION |
XSP_MANUAL_SSELECT_OPTION |
XSP_ENABLE_DMA_OPTION);
// DMA传输示例
XDmaPs_Config *DmaConfig;
XDmaPs DmaInstance;
DmaConfig = XDmaPs_LookupConfig(XPAR_PS7_DMA_NS_DEVICE_ID);
XDmaPs_CfgInitialize(&DmaInstance, DmaConfig);
5.2 双缓冲技术实现
c复制#define BUF_SIZE 1024
u8 tx_buffer[2][BUF_SIZE];
u8 active_buffer = 0;
// 填充非活动缓冲区
void fill_buffer(u8 *data, u16 len)
{
memcpy(tx_buffer[!active_buffer], data, len);
}
// 交换缓冲区并启动传输
void swap_and_transfer(XSpi *SpiPtr)
{
active_buffer = !active_buffer;
XSpi_Transfer(SpiPtr, tx_buffer[active_buffer], NULL, BUF_SIZE);
}
6. 常见问题排查
6.1 典型问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读取数据全为0xFF | 1. 片选信号未生效 | 检查CS#引脚连接和驱动逻辑 |
| 2. 未正确发送读取指令 | 确认指令序列符合规范 | |
| 写入失败 | 1. 未使能写操作(WEL=0) | 检查WRITE_ENABLE指令执行 |
| 2. 处于写保护状态 | 检查WP#引脚电平 | |
| SPI时钟无输出 | 1. IP核时钟未连接 | 检查时钟配置和约束 |
| 2. 设备未正确初始化 | 验证初始化流程 |
6.2 调试技巧
-
逻辑分析仪抓包:
- 配置采样率至少4倍于SPI时钟频率
- 同时捕获CS、CLK、MOSI、MISO信号
- 注意触发条件设置为CS下降沿
-
寄存器检查:
c复制// 打印SPI控制寄存器状态
void print_spi_registers(XSpi *SpiPtr)
{
printf("SRR: 0x%X\n", XSpi_ReadReg(SpiPtr->BaseAddr, XSP_SRR_OFFSET));
printf("CR: 0x%X\n", XSpi_ReadReg(SpiPtr->BaseAddr, XSP_CR_OFFSET));
printf("SR: 0x%X\n", XSpi_ReadReg(SpiPtr->BaseAddr, XSP_SR_OFFSET));
}
7. 进阶应用方向
7.1 实现内存映射访问
通过配置Zynq的AXI总线,可以将SPI Flash映射到内存地址空间:
- 在Vivado中启用AXI Quad SPI的"XIP Mode"
- 配置正确的地址映射范围
- 修改FSBL或U-Boot初始化代码
- 应用程序可直接通过指针访问:
c复制volatile u8 *flash_ptr = (u8 *)0x80000000; // 示例地址
u8 data = flash_ptr[offset]; // 直接读取
7.2 多器件共享总线
通过片选信号管理多个SPI设备:
c复制// 设备选择宏定义
#define SELECT_FLASH() XSpi_SetSlaveSelect(SpiPtr, 0x01)
#define SELECT_SENSOR() XSpi_SetSlaveSelect(SpiPtr, 0x02)
// 使用示例
SELECT_FLASH();
flash_read_data(...);
SELECT_SENSOR();
sensor_read_data(...);
在实际工程中,我发现SPI总线的走线长度和负载电容会显著影响通信稳定性。对于超过10cm的连接线,建议:
- 在SCK信号上串联33Ω电阻
- 在接收端添加10pF对地电容
- 降低SPI时钟频率至5MHz以下
另一个实用技巧是在写入操作前检查目标区域是否已经为空白状态(全0xFF),可以避免不必要的擦写操作,显著延长Flash寿命。实现示例:
c复制int is_sector_blank(XSpi *SpiPtr, u32 addr)
{
u8 buf[256];
flash_read_data(SpiPtr, addr, buf, 256);
for(int i=0; i<256; i++) {
if(buf[i] != 0xFF) return 0;
}
return 1;
}