从零到一:在Vitis平台上构建ZYNQ PS-SPI Flash驱动
1. 环境准备与硬件连接在开始构建ZYNQ PS-SPI Flash驱动之前我们需要准备好开发环境和硬件平台。我推荐使用Xilinx官方提供的Vitis 2022.1版本这个版本对ZYNQ系列的支持比较稳定。硬件方面你需要一块带有SPI Flash的ZYNQ开发板比如常见的ZYNQ-7000系列开发板Flash芯片我选用的是华邦W25Q80这是一款8Mbit容量的SPI Flash工作电压3.3V最大支持25MHz时钟频率。安装Vitis时有个小技巧建议选择Full Product Installation模式这样可以避免后续缺少某些组件的麻烦。我遇到过因为没装全组件导致Standalone工程模板缺失的情况重装浪费了不少时间。安装完成后记得把Vivado和Vitis的license文件放到指定目录否则无法正常使用IP核功能。硬件连接上ZYNQ的PS端SPI接口通常通过EMIO引出。你需要确认开发板原理图中SPI Flash的四个信号线CS、CLK、MOSI、MISO是否正确连接到ZYNQ的对应引脚。以我的经验最容易出错的是CS片选信号的连接一定要确认是连接到PS_SPI0_SS引脚而不是PL端的普通IO。曾经有个项目因为CS接错引脚调试了整整两天才发现问题。2. 创建Vitis Standalone工程打开Vitis后首先点击Create Platform Project创建一个硬件平台工程。这里需要导入你的XSA文件由Vivado导出。我建议在Vivado中生成XSA时勾选Include bitstream选项这样后续调试会更方便。平台工程创建完成后右键选择Build Project编译生成平台文件。接下来创建应用工程File → New → Application Project。关键步骤是选择刚才创建的硬件平台处理器选择ps7_cortexa9_0模板选择Empty Application(C)语言标准选C99避免后续兼容性问题工程创建后需要配置BSP设置。右键工程选择Board Support Package Settings找到standalone目录下的extra_compiler_flags添加-O2优化选项。这个优化级别既能保证代码效率又不会过度优化导致调试困难。我实测过开启-O2后SPI传输速度能提升约15%。3. SPI控制器初始化配置SPI初始化的核心是配置XSpiPs驱动。首先在工程中新建一个spi_flash.c文件包含以下头文件#include xspips.h #include xil_printf.h #include xil_io.h #include xparameters.h #include sleep.h然后定义全局变量static XSpiPs g_spi0_handle; #define SPI_DEVICE_ID XPAR_XSPIPS_0_DEVICE_ID #define SPI_FIFO_DEPTH 128初始化函数需要特别注意时钟配置int spi_init(void) { XSpiPs_Config *spi_config; int status; spi_config XSpiPs_LookupConfig(SPI_DEVICE_ID); if (!spi_config) { xil_printf(SPI config lookup failed\r\n); return XST_FAILURE; } status XSpiPs_CfgInitialize(g_spi0_handle, spi_config, spi_config-BaseAddress); if (status ! XST_SUCCESS) { xil_printf(SPI init failed\r\n); return XST_FAILURE; } // 设置为Master模式手动控制片选 status XSpiPs_SetOptions(g_spi0_handle, XSPIPS_MASTER_OPTION | XSPIPS_FORCE_SSELECT_OPTION); if (status ! XST_SUCCESS) { xil_printf(Set SPI options failed\r\n); return XST_FAILURE; } // 设置时钟分频200MHz/825MHz status XSpiPs_SetClkPrescaler(g_spi0_handle, XSPIPS_CLK_PRESCALE_8); if (status ! XST_SUCCESS) { xil_printf(Set SPI clock failed\r\n); return XST_FAILURE; } XSpiPs_Enable(g_spi0_handle); return XST_SUCCESS; }这里有个实际项目中的经验如果发现SPI时钟不稳定可以尝试在Vivado中约束SPI时钟引脚的电平标准和驱动强度。我一般设置为LVCMOS33驱动强度8mA这样信号质量会更好。4. Flash指令集封装W25Q80的指令集需要按照芯片手册正确封装。我们先实现几个基础指令4.1 读ID指令#define CMD_READ_ID 0x9F uint32_t flash_read_id(void) { uint8_t tx_buf[4] {CMD_READ_ID, 0, 0, 0}; uint8_t rx_buf[4] {0}; // 手动控制CS XSpiPs_WriteReg(g_spi0_handle.Config.BaseAddress, XSPIPS_CR_OFFSET, XSpiPs_ReadReg(g_spi0_handle.Config.BaseAddress, XSPIPS_CR_OFFSET) | XSPIPS_CR_SSCTRL_MASK); XSpiPs_WriteReg(g_spi0_handle.Config.BaseAddress, XSPIPS_SSR_OFFSET, 0x01); XSpiPs_PolledTransfer(g_spi0_handle, tx_buf, rx_buf, 4); XSpiPs_WriteReg(g_spi0_handle.Config.BaseAddress, XSPIPS_SSR_OFFSET, 0x00); return (rx_buf[1] 16) | (rx_buf[2] 8) | rx_buf[3]; }4.2 写使能与状态读取#define CMD_WRITE_ENABLE 0x06 #define CMD_READ_STATUS 0x05 void flash_write_enable(void) { uint8_t cmd CMD_WRITE_ENABLE; XSpiPs_PolledTransfer(g_spi0_handle, cmd, NULL, 1); } uint8_t flash_read_status(void) { uint8_t tx_buf[2] {CMD_READ_STATUS, 0}; uint8_t rx_buf[2] {0}; XSpiPs_PolledTransfer(g_spi0_handle, tx_buf, rx_buf, 2); return rx_buf[1]; }4.3 扇区擦除#define CMD_SECTOR_ERASE 0x20 int flash_sector_erase(uint32_t addr) { uint8_t tx_buf[4] { CMD_SECTOR_ERASE, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF }; flash_write_enable(); XSpiPs_PolledTransfer(g_spi0_handle, tx_buf, NULL, 4); // 等待擦除完成 while (flash_read_status() 0x01); return 0; }在实际项目中我发现W25Q80的擦除时间会有波动。建议在擦除后增加状态检查确保擦除真正完成。我曾经遇到过因为没等擦除完成就进行写操作导致数据写入失败的情况。5. 数据读写实现5.1 页编程实现Flash的写入必须以页为单位通常256字节且写入前必须先擦除#define CMD_PAGE_PROGRAM 0x02 #define PAGE_SIZE 256 int flash_page_program(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t tx_buf[4 PAGE_SIZE]; if (len PAGE_SIZE) { xil_printf(Error: exceed page size\r\n); return -1; } tx_buf[0] CMD_PAGE_PROGRAM; tx_buf[1] (addr 16) 0xFF; tx_buf[2] (addr 8) 0xFF; tx_buf[3] addr 0xFF; memcpy(tx_buf[4], data, len); flash_write_enable(); XSpiPs_PolledTransfer(g_spi0_handle, tx_buf, NULL, 4 len); // 等待写入完成 while (flash_read_status() 0x01); return 0; }5.2 数据读取实现读取操作相对简单可以跨页读取#define CMD_READ_DATA 0x03 int flash_read_data(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t tx_buf[4] { CMD_READ_DATA, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF }; // 先发送地址再读取数据 XSpiPs_PolledTransfer(g_spi0_handle, tx_buf, NULL, 4); XSpiPs_PolledTransfer(g_spi0_handle, NULL, buf, len); return 0; }这里有个性能优化点对于大块数据读取可以使用DMA模式。实测在25MHz时钟下DMA方式读取1MB数据比轮询方式快约30%。但需要注意DMA缓冲区需要4字节对齐否则会导致传输失败。6. 驱动测试与优化6.1 基础功能测试建议按照以下顺序测试读ID测试验证SPI通路是否正常单页读写测试验证基本读写功能跨页读写测试验证地址边界处理擦除后读写测试验证擦除功能测试代码示例void flash_test(void) { uint8_t write_buf[PAGE_SIZE]; uint8_t read_buf[PAGE_SIZE]; uint32_t id; // 测试读ID id flash_read_id(); xil_printf(Flash ID: 0x%08X\r\n, id); // 测试单页读写 memset(write_buf, 0x55, PAGE_SIZE); flash_sector_erase(0); flash_page_program(0, write_buf, PAGE_SIZE); flash_read_data(0, read_buf, PAGE_SIZE); if (memcmp(write_buf, read_buf, PAGE_SIZE) ! 0) { xil_printf(Page RW test failed\r\n); } else { xil_printf(Page RW test passed\r\n); } }6.2 性能优化技巧双缓冲技术在写入数据时可以准备下一个页的数据提高吞吐量批量操作对于连续地址操作可以保持CS为低电平减少CS切换时间指令预取对于固定指令序列可以预先存储在数组中一次性发送优化后的页编程示例int flash_write_multi_pages(uint32_t addr, uint8_t *data, uint32_t total_len) { uint32_t remaining total_len; uint32_t current_addr addr; uint8_t *current_data data; while (remaining 0) { uint32_t chunk (remaining PAGE_SIZE) ? PAGE_SIZE : remaining; flash_sector_erase(current_addr); flash_page_program(current_addr, current_data, chunk); current_addr chunk; current_data chunk; remaining - chunk; } return 0; }7. 常见问题排查在开发过程中我遇到过以下几个典型问题SPI无响应检查硬件连接特别是CS信号确认SPI时钟配置正确示波器测量SCLK检查Vivado中是否使能了PS-SPI控制器数据写入失败确认执行了写使能指令检查状态寄存器的写保护位确保在写入前已擦除对应扇区读写数据错误检查SPI模式W25Q80需要Mode 0或Mode 3确认MOSI和MISO没有接反降低时钟频率测试排除信号完整性问题性能不稳定检查电源稳定性Flash对电源噪声敏感添加适当的延时特别是连续操作时优化PCB布局缩短SPI走线长度对于复杂的调试我建议使用逻辑分析仪抓取SPI波形。通过分析CS、CLK、MOSI、MISO的时序关系可以快速定位问题。比如我曾经通过波形发现因为没等前一个操作完成就发起新操作导致指令执行异常。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2505369.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!