ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

QSPI-Flash-deepseek 读写测试

QSPI-Flash-deepseek 读写测试

/*! \file qspi_hdl.h \brief firmware functions to manage qspi \version 2024-04-15, V1.0.0 \author tbj */ #ifndef QSPI_HDL_H #define QSPI_HDL_H #include "xqspips.h" //QSPI器件ID #define QSPI_DEVICE_ID XPAR_XQSPIPS_0_DEVICE_ID //flash操作指令 #define READ_CMD 0x03 //读指令 #define WRITE_CMD 0x02 //写指令 #define READ_STATUS_CMD 0x05 //读状态指令 #define WRITE_STATUS_CMD 0x01 //写状态指令 #define WRITE_ENABLE_CMD 0x06 //写使能指令 #define WRITE_DISABLE_CMD 0x04 //禁止写使能指令 #define FAST_READ_CMD 0x0B //单通道读取 #define DUAL_READ_CMD 0x3B //双通道读取-半双工 #define QUAD_READ_CMD 0x6B //四通道读取-半双工 #define BULK_ERASE_CMD 0xC7 //擦除整片flash-全部写1 #define SEC_ERASE_CMD 0xD8 //擦除一个扇区-全部写1 #define READ_ID 0x9F //读取flash ID指令 //定义flash操作指令、地址、数据等在读写buffer中的位置 #define COMMAND_OFFSET 0 //flash操作指令在写buffer中的位置(第0个字节) #define ADDRESS_1_OFFSET 1 //操作flash数据起始地址的高字节在写buffer中的位置(第1个字节) #define ADDRESS_2_OFFSET 2 //操作flash数据起始地址的中字节在写buffer中的位置(第2个字节) #define ADDRESS_3_OFFSET 3 //操作flash数据起始地址的低字节在写buffer中的位置(第3个字节) #define DATA_OFFSET 4 //操作flash的数据,读取或写入的数据,在写buffer中的位置(第4个字节开始是纯数据区) #define DUMMY_OFFSET 4 //空闲字节的位置,当使用快速、双线、四线模式读取数据时,空闲字节占读buffer的第4个字节,纯数据区从第5个字节开始 //定义各种操作所需字节长度 #define DUMMY_SIZE 1 //空闲字节大小占1个字节(当使用快速、双线、四线模式读取数据时存在dummy byte) #define RD_ID_SIZE 4 //读取flash ID占字节数,其中第0个字节为读取ID指令号,后3个字节为读取到的flash ID号 #define BULK_ERASE_SIZE 1 //清空整片flash指令占字节数,只需要一个清空整片flash的指令号 #define SEC_ERASE_SIZE 4 //按扇区清空flash指令占字节数,其中第0字节为按扇区清空flash的指令号,后3个字节是起始清空的flash地址 #define OVERHEAD_SIZE 4 //定义读写buffer头部数据长度,包括指令号1字节和操作地址3字节 //定义flash的参数数据 #define SECTOR_SIZE 0x10000 //定义单个扇区大小-64KB(根据自己flash芯片手册确定) #define NUM_SECTORS 0x200 //定义扇区数量-256个(根据自己flash芯片手册确定) #define NUM_PAGES 0x20000 //定义页数量-65536个(根据自己flash芯片手册确定) #define PAGE_SIZE 256 //定义每页字节数-256个字节(根据自己flash芯片手册确定) //定义实际读写操作的范围和数据大小 #define PAGE_COUNT 16 //定义需要操作读写的页数 #define TEST_ADDRESS 0x01FF0000//0x00055000 //定义读写操作的起始地址 #define UNIQUE_VALUE 0x05 //定义读写操作的起始值 #define MAX_DATA (PAGE_COUNT * PAGE_SIZE) //定义读写操作的最大数据量 //定义QSPI操作结构体对象 extern XQspiPs QspiInstance; #ifdef __cplusplus extern "C" { #endif //初始化QSPI控制器 int Qspi_Init(XQspiPs *QspiInstancePtr); //通过QSPI将数据写入flash中 void FlashWrite(XQspiPs *QspiPtr, u32 Address, u8 *WriteBuf, u32 ByteCount, u8 Command); //通过QSPI读取flash中的数据 void FlashRead(XQspiPs *QspiPtr, u32 Address, u8 *ReadBuf, u32 ByteCount, u8 Command); //擦除flash void FlashErase(XQspiPs *QspiPtr, u32 Address, u32 ByteCount); //读取flash ID int FlashReadID(void); //使能四线模式 void FlashQuadEnable(XQspiPs *QspiPtr); #ifdef __cplusplus } #endif #endif /* QSPI_HDL_H */
/*! \file qspi_hdl.c \brief firmware functions to manage qspi \version 2024-04-15, V1.0.0 \author tbj */ #include "qspi_hdl.h" XQspiPs QspiInstance; //QSPI读写flash使用的buffer,内部使用 static u8 FlashReadBuffer[MAX_DATA + DATA_OFFSET + DUMMY_SIZE]; static u8 FlashWriteBuffer[PAGE_SIZE + DATA_OFFSET]; /* 功能:初始化QSPI控制器 * 入参1:QSPI控制器实例化对象指针 */ int Qspi_Init(XQspiPs *QspiInstancePtr){ int Status; XQspiPs_Config *QspiConfig; //初始化QSPI控制器 QspiConfig = XQspiPs_LookupConfig(QSPI_DEVICE_ID); if (QspiConfig == NULL) { return XST_FAILURE; } Status = XQspiPs_CfgInitialize(QspiInstancePtr, QspiConfig, QspiConfig->BaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } //QSPI控制器自检,保证初始化成功 Status = XQspiPs_SelfTest(QspiInstancePtr); if (Status != XST_SUCCESS) { return XST_FAILURE; } //清空读写操作的buffer memset(FlashWriteBuffer, 0x00, sizeof(FlashWriteBuffer)); memset(FlashReadBuffer, 0x00, sizeof(FlashReadBuffer)); //将flash配置为手动启动、手动片选模式,将hold(reset)引脚配置为高电平,hold低电平,暂停收发数据,高电平恢复收发数据 Status |= XQspiPs_SetOptions(QspiInstancePtr, XQSPIPS_MANUAL_START_OPTION | XQSPIPS_FORCE_SSELECT_OPTION | XQSPIPS_HOLD_B_DRIVE_OPTION); //设置QSPI预分频系数 Status |= XQspiPs_SetClkPrescaler(QspiInstancePtr, XQSPIPS_CLK_PRESCALE_8); //将片选信号置为有效 Status |= XQspiPs_SetSlaveSelect(QspiInstancePtr); //读取flash ID Status |= FlashReadID(); //使能QSPI Quad模式 FlashQuadEnable(QspiInstancePtr); return Status; } /** * @brief 通过QSPI将数据写入flash中 * @param QSPI结构体指针 * @param 要写入数据的起始地址 * @param 要写入数据的数量-按字节 * @param 写数据指令 * @return 无 * @note 无 * */ void FlashWrite(XQspiPs *QspiPtr, u32 Address, u8 *WriteBuf, u32 ByteCount, u8 Command) { u8 WriteEnableCmd = { WRITE_ENABLE_CMD }; u8 ReadStatusCmd[] = { READ_STATUS_CMD, 0 }; /* must send 2 bytes */ u8 FlashStatus[2]; //发送写使能指令 XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL, sizeof(WriteEnableCmd)); //将写操作指令以及数据地址写入对应待发送buffer的前4个字节,第五个字节开始才是写入的数据 FlashWriteBuffer[COMMAND_OFFSET] = Command; FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16); FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8); FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF); //将要写入的纯数据,写入到flash写操作buffer中(flash写操作buffer包括写指令、地址、数据等内容) memcpy(FlashWriteBuffer + 4, WriteBuf, ByteCount); //将写指令、写起始地址信息、写入数据内容,写到flash中 XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL, ByteCount + OVERHEAD_SIZE); //等待数据写入完毕 while (1) { //通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕 XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus, sizeof(ReadStatusCmd)); //如果读取的状态值是0xff,则证明数据还未写完 FlashStatus[1] |= FlashStatus[0]; if ((FlashStatus[1] & 0x01) == 0) { break; } } } /** * @brief 通过QSPI读取flash中的数据 * @param QSPI结构体指针 * @param 读取数据的起始地址 * @param 读取数据的数量-按字节 * @param 读取数据指令-普通读取、快速、双线、四线读取等 * @return 无 * @note 无 * */ void FlashRead(XQspiPs *QspiPtr, u32 Address, u8 *ReadBuf, u32 ByteCount, u8 Command) { //将读指令和读数据首地址写入到要发送的buffer中,它们占4个字节 FlashWriteBuffer[COMMAND_OFFSET] = Command; FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16); FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8); FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF); //如果是快速读取、双线读取和四线读取,则需要增加一个空闲字节的长度DUMMY_SIZE if ((Command == FAST_READ_CMD) || (Command == DUAL_READ_CMD) || (Command == QUAD_READ_CMD)) { ByteCount += DUMMY_SIZE; } //将读指令和读地址发送到通过QSPI发送到flash,等待数据读取至ReadBuffer中 XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, FlashReadBuffer, ByteCount + OVERHEAD_SIZE); //如果是快速读取、双线读取和四线读取,则需要增加一个虚拟字节的长度DUMMY_SIZE if ((Command == FAST_READ_CMD) || (Command == DUAL_READ_CMD) || (Command == QUAD_READ_CMD)) { //非普通模式读取,有空闲字节,从第5个字节开始是纯数据 memcpy(ReadBuf, FlashReadBuffer + 5, ByteCount - 1); }else{ //普通模式读取,无空闲字节,从第4个字节开始是纯数据 memcpy(ReadBuf, FlashReadBuffer + 4, ByteCount); } } /** * @brief 擦除flash * @param QSPI结构体指针 * @param 擦除的起始地址 * @param 擦除数据的数量-按字节 * @return 无 * @note 无 * */ void FlashErase(XQspiPs *QspiPtr, u32 Address, u32 ByteCount) { u8 WriteEnableCmd = { WRITE_ENABLE_CMD }; u8 ReadStatusCmd[] = { READ_STATUS_CMD, 0 }; /* must send 2 bytes */ u8 FlashStatus[2]; int Sector; //如果是擦除整片flash,则使用整片擦除指令chip erase if (ByteCount == (NUM_SECTORS * SECTOR_SIZE)) { //发送写使能指令 XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL, sizeof(WriteEnableCmd)); //将整片擦除指令写入到发送buffer的首个字节的位置 FlashWriteBuffer[COMMAND_OFFSET] = BULK_ERASE_CMD; //将整片擦除指令发送到flash XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL, BULK_ERASE_SIZE); //等待擦除完成 while (1) { //通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕 XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus, sizeof(ReadStatusCmd)); //如果读取的状态值是0xff,则证明数据还未写完 FlashStatus[1] |= FlashStatus[0]; if ((FlashStatus[1] & 0x01) == 0) { break; } } return; } //如果是部分擦除,则使用扇区sector擦除的指令进行擦除操作 for (Sector = 0; Sector < ((ByteCount / SECTOR_SIZE) + 1); Sector++) { //发送写使能指令 XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL, sizeof(WriteEnableCmd)); //将扇区擦除指令,以及开始擦除首地址写入到发送buffer的前四个字节 FlashWriteBuffer[COMMAND_OFFSET] = SEC_ERASE_CMD; FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)(Address >> 16); FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)(Address >> 8); FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF); //将扇区擦除指令,以及开始擦除首地址发送到flash XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL, SEC_ERASE_SIZE); //等待擦除完成 while (1) { //通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕 XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus, sizeof(ReadStatusCmd)); //如果读取的状态值是0xff,则证明数据还未写完 FlashStatus[1] |= FlashStatus[0]; if ((FlashStatus[1] & 0x01) == 0) { break; } } Address += SECTOR_SIZE; } } /** * @brief 读取flash ID * @param 无 * @return 无 * @note 无 * */ int FlashReadID(void) { int Status; //读取ID指令,后三个字节是空闲字节,填不填都行,填什么也无所谓 FlashWriteBuffer[COMMAND_OFFSET] = READ_ID; FlashWriteBuffer[ADDRESS_1_OFFSET] = 0x23; FlashWriteBuffer[ADDRESS_2_OFFSET] = 0x08; FlashWriteBuffer[ADDRESS_3_OFFSET] = 0x09; //将读ID指令发送到flash Status = XQspiPs_PolledTransfer(&QspiInstance, FlashWriteBuffer, FlashReadBuffer, RD_ID_SIZE); if (Status != XST_SUCCESS) { return XST_FAILURE; } xil_printf("FlashID=0x%x 0x%x 0x%x\n\r", FlashReadBuffer[1], FlashReadBuffer[2], FlashReadBuffer[3]); return XST_SUCCESS; } /** * @brief 使能四线模式 * @param QSPI结构体指针 * @return 无 * @note 无 * */ void FlashQuadEnable(XQspiPs *QspiPtr) { u8 WriteEnableCmd = {WRITE_ENABLE_CMD}; u8 ReadStatusCmd[] = {READ_STATUS_CMD, 0}; u8 QuadEnableCmd[] = {WRITE_STATUS_CMD, 0}; u8 FlashStatus[2]; //判断读取的flash ID是否正确,不加这个判断也行 if (FlashReadBuffer[1] == 0xEF) { //获取读状态 XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus, sizeof(ReadStatusCmd)); QuadEnableCmd[1] = FlashStatus[1] | 1 << 6; //发送写使能指令 XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL, sizeof(WriteEnableCmd)); //发送Quad配置指令 XQspiPs_PolledTransfer(QspiPtr, QuadEnableCmd, NULL, sizeof(QuadEnableCmd)); while (1) { //获取读状态,等待指令写入完毕 XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus, sizeof(ReadStatusCmd)); /* * 第6it置1,第0bit置0,则Quad模式设置成功、且设备状态准备就绪 */ if ((FlashStatus[0] == 0x40) && (FlashStatus[1] == 0x40)) { break; } } } }
/****************************************************************************** * * Copyright (C) 2009 - 2014 Xilinx, Inc. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * Use of the Software is limited solely to applications: * (a) running on a Xilinx device, or * (b) that interact with a Xilinx device through a bus or interconnect. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * XILINX BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * Except as contained in this notice, the name of the Xilinx shall not be used * in advertising or otherwise to promote the sale, use or other dealings in * this Software without prior written authorization from Xilinx. * ******************************************************************************/ /* * helloworld.c: simple test application * * This application configures UART 16550 to baud rate 9600. * PS7 UART (Zynq) is not initialized by this application, since * bootrom/bsp configures it to baud rate 115200 * * ------------------------------------------------ * | UART TYPE BAUD RATE | * ------------------------------------------------ * uartns550 9600 * uartlite Configurable only in HW design * ps7_uart 115200 (configured by bootrom/bsp) */ #include <stdio.h> #include "platform.h" #include "xil_printf.h" #include "xstatus.h" #include "qspi_hdl.h" //读写buffer数据长度 #define test_buf_len 255 //QSPI读写flash测试 void QSPI_Flash_Opt(){ u8 nRet = XST_SUCCESS; u8 write_buf[test_buf_len] = {0}; u8 read_buf[test_buf_len] = {0}; //初始化QSPI控制器 Qspi_Init(&QspiInstance); //write buffer填写数据 for(int i = 0; i < test_buf_len; i++){ write_buf[i] = i + 1; } //清除要写入的flash区域 FlashErase(&QspiInstance, TEST_ADDRESS, test_buf_len); //将write buffer数据写入flash中 FlashWrite(&QspiInstance, TEST_ADDRESS, write_buf, test_buf_len, WRITE_CMD); //将写入flash中的数据再进行读取 // FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, READ_CMD); // FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, FAST_READ_CMD); // FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, DUAL_READ_CMD); FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, QUAD_READ_CMD); //打印写buffer区数据 for(int i = 0; i < test_buf_len; i++){ printf("%d ", write_buf[i]); if(i == test_buf_len - 1) printf("\n"); } //打印读buffer区数据 for(int i = 0; i < test_buf_len; i++){ printf("%d ", read_buf[i]); if(i == test_buf_len - 1) printf("\n"); } //对比写入和读出的数据是否一致 for(int i = 0; i < test_buf_len; i++){ if(read_buf[i] != write_buf[i]){ nRet = XST_FAILURE; } } if(nRet == XST_SUCCESS){ printf("QSPI Operate flash successful!\n"); }else{ printf("QSPI Operate flash failed!\n"); } } int main() { init_platform(); print("Hello World\n\r"); print("Successfully ran Hello World application"); cleanup_platform(); QSPI_Flash_Opt(); return 0; }

把代码放里面了,这里有报错;

https://blog.csdn.net/qq_38584212/article/details/137965232,本人参考链接这文章,老师可以根据这文章详细讲讲操作flash的流程吗,代码讲讲注释哈,对应底层如何运行的,谢谢啦

老师,别着急,这个multiple definition of 'QspiInstance'的报错是C语言里非常经典的问题,在Vitis里也很常见。

🎯 先解决报错:多个定义 (Multiple Definition)

问题根源:在C语言中,变量的“定义”(分配内存)应该放在.c文件里,而.h文件里只应该做“声明”(告诉编译器有这么一个变量,但不分配内存)。你的qspi_hdl.h第58行直接写了XQspiPs QspiInstance;,这既是声明也是定义。当main.c和qspi_hdl.c都包含这个头文件时,编译器就看到了两次定义,于是报错。

解决方法:

  1. 修改头文件qspi_hdl.h:把第58行改成extern XQspiPs QspiInstance;,只做声明。

  2. 修改源文件qspi_hdl.c:在文件开头(包含头文件之后)加上一行XQspiPs QspiInstance;,这里才是真正的定义。

这样修改后,整个工程里只有qspi_hdl.c真正定义了QspiInstance,其他文件通过extern引用它,就不会再冲突了。


🗺️ QSPI Flash 操作全流程(以代码为例)

整个过程就是软件通过Zynq的QSPI控制器,严格按照Flash芯片的“命令手册”与它对话。

第一步:初始化 (Qspi_Init)
  • XQspiPs_LookupConfig&XQspiPs_CfgInitialize:查找并初始化QSPI控制器的硬件配置(基地址、设备ID等)。

  • XQspiPs_SetOptions:设置手动模式,这是为了精确控制片选和传输时序。

  • FlashReadID:读取Flash的ID(0x9F指令)。这一步很重要,它确认了芯片通信正常,也为后面判断芯片厂商(如Winbond的ID是0xEF)提供了依据。

  • FlashQuadEnable:使能四线(Quad)模式。默认是单线,四线可以同时用4根数据线传输,大幅提升读写速度。它通过读取状态寄存器,把第6位(QE位)置1来实现。

第二步:擦除 (FlashErase)

Flash存储器的特性是写数据前必须先擦除(把对应区域全部置为1,即0xFF)。

  • 擦除指令:代码支持两种模式。

    • 全片擦除:发送0xC7指令。

    • 扇区擦除:发送0xD8指令。代码会根据地址计算出需要擦除多少个扇区,然后循环发送指令和地址。

  • 等待完成:发送擦除指令后,循环发送0x05(读状态寄存器)指令,直到状态寄存器的第0位变为0(表示不再“忙”),擦除才真正结束。

第三步:写入 (FlashWrite)
  • 写使能 (0x06):在每次写/擦除操作前,必须先发送0x06指令,告诉Flash“我要开始写了”,这是一个安全机制。

  • 页编程 (0x02):这是写入数据的核心指令。代码先构造一个缓冲区FlashWriteBuffer,依次放入:0x02指令 -> 3字节地址 -> 你要写入的数据。然后通过XQspiPs_PolledTransfer一次性发送出去。

  • 等待完成:同样,发送完数据后也要轮询状态寄存器,等待Flash内部编程完成。

第四步:读取 (FlashRead)
  • 读指令:代码支持多种读指令。

    • 0x03(普通读)。

    • 0x0B(快速读)。

    • 0x3B(双线读)。

    • 0x6B(四线读)。

  • DUMMY字节:使用快速/双线/四线读指令时,在发送完指令和地址后,需要额外插入1个DUMMY字节(空闲字节),给Flash内部电路一个准备数据的时间。

  • 数据对齐:因为发送缓冲区前4个字节被指令和地址占用,所以读取回来的数据要从FlashReadBuffer的第5个字节(FlashReadBuffer + 5)开始,才是真正的用户数据。


🔬 底层原理:Zynq QSPI 控制器如何工作?

你代码里调用的XQspiPs_PolledTransfer函数,底层其实是在操作Zynq芯片内部QSPI控制器的一系列寄存器:

  1. 配置寄存器:设置时钟分频(XQspiPs_SetClkPrescaler)、传输模式等。

  2. 命令寄存器:把你的“指令字节”和“地址字节”写入特定寄存器,控制器会自动将它们打包成符合SPI协议的波形,从引脚发送出去。

  3. 数据寄存器:把要发送的“纯数据”写入FIFO,或者从接收FIFO中读出收到的数据。

  4. 状态寄存器:控制器有“忙”标志位,PolledTransfer函数就是通过不断查询这个标志,来知道一次传输是否完成。

整个过程就是CPU通过配置和控制QSPI控制器,间接地操控着外部SPI Flash芯片的引脚电平。

先按上面的方法把报错解决了,代码能跑起来后,再对着流程看代码,会清晰很多。如果修改后还有别的报错,随时发我~

修改后,可以跑通,谢谢啦(上面代码已按照deepseek两处修改好,可以使用)


参考链接:https://blog.csdn.net/AlibabaApple/article/details/169951375?spm=1001.2014.3001.5501 老师,这篇文章是关于flash操作的,作为c语言小白阅读代码,初始化QSPI控制器?QSPI控制器实例化对象指针?什么是实例?指针?这些基本c语言的基本概念可以给小白扫盲一下嘛,谢谢啦;弄懂了这个基础,然后把这份代码,重新讲解一遍,每一个函数是用来干嘛的,对应到底层是如何运行的,谢谢啦


看到这一大串代码和“实例化”、“指针”这些词,觉得懵是很正常的。我们先把这些C语言概念弄明白,再回头看代码,就会清晰很多。

🧩 第一步:C语言概念扫盲(“实例”与“指针”)

在嵌入式开发中,这两个概念的核心逻辑是:通过“指针”找到并操作“实例”。

  • 实例 (Instance):可以理解为根据设计图纸(结构体定义)建造出来的一个实体对象。在代码中,XQspiPs是一个“结构体”类型,它就像一个图纸,规定了QSPI控制器需要哪些数据(比如基地址、配置信息等)。XQspiPs QspiInstance;这行代码就是根据图纸,在内存中真的建造了一个对象,并给它取名叫QspiInstance。这个对象里就保存了当前QSPI控制器运行时的所有状态信息。

  • 指针 (Pointer):可以理解为一张写着实体地址的“名片”。&QspiInstance就是获取这个实体的地址,而XQspiPs *QspiInstancePtr就是一个专门用来存放这张“名片”的变量。函数通过这张“名片”就能找到并操作那个实体。

所以,Qspi_Init(&QspiInstance)的意思就是:把QspiInstance这个实体的“名片”(地址)交给Qspi_Init函数,函数拿到名片后,就能找到这个对象并完成初始化了。

🗺️ 第二步:QSPI Flash 操作全流程(代码精讲)

现在来看整个Flash操作的全流程,代码是如何一步步实现的。

第一步:初始化 (Qspi_Init)

  • XQspiPs_LookupConfig:根据设备ID,找到QSPI控制器的硬件配置信息。

  • XQspiPs_CfgInitialize:用找到的配置信息,初始化我们前面创建的QspiInstance实例。

  • XQspiPs_SetOptions:设置控制器为手动模式。这是为了精确控制片选和传输时序。

  • FlashReadID:发送0x9F指令读取Flash的ID。这一步能确认通信正常,并识别芯片厂商(如Winbond的ID是0xEF)。

  • FlashQuadEnable:使能四线(Quad)模式。默认是单线,四线可以同时用4根数据线传输,大幅提升读写速度。它通过读取状态寄存器,把第6位(QE位)置1来实现。

第二步:擦除 (FlashErase)
Flash存储器的特性是写数据前必须先擦除(把对应区域全部置为1,即0xFF)。

  • 全片擦除:发送0xC7指令,将整个芯片擦除。

  • 扇区擦除:发送0xD8指令。代码会根据地址计算出需要擦除多少个扇区,然后循环发送指令和地址。

  • 等待完成:发送擦除指令后,循环发送0x05(读状态寄存器)指令,直到状态寄存器的第0位变为0(表示不再“忙”),擦除才真正结束。

第三步:写入 (FlashWrite)

  • 写使能 (0x06):在每次写/擦除操作前,必须先发送0x06指令,告诉Flash“我要开始写了”,这是一个安全机制。

  • 页编程 (0x02):这是写入数据的核心指令。代码先构造一个缓冲区FlashWriteBuffer,依次放入:0x02指令 -> 3字节地址 -> 你要写入的数据。然后通过XQspiPs_PolledTransfer一次性发送出去。

  • 等待完成:同样,发送完数据后也要轮询状态寄存器,等待Flash内部编程完成。

第四步:读取 (FlashRead)

  • 读指令:代码支持多种读指令,包括普通读(0x03)、快速读(0x0B)、双线读(0x3B)和四线读(0x6B)。

  • DUMMY字节:使用快速/双线/四线读指令时,在发送完指令和地址后,需要额外插入1个DUMMY字节(空闲字节),给Flash内部电路一个准备数据的时间。

  • 数据对齐:因为发送缓冲区前4个字节被指令和地址占用,所以读取回来的数据要从FlashReadBuffer的第5个字节(FlashReadBuffer + 5)开始,才是真正的用户数据。

🔬 第三步:底层运行原理(Zynq QSPI控制器)

你代码里调用的XQspiPs_PolledTransfer函数,底层其实是在操作Zynq芯片内部QSPI控制器的一系列寄存器:

  1. 配置寄存器:设置时钟分频(XQspiPs_SetClkPrescaler)、传输模式等。

  2. 命令寄存器:把你的“指令字节”和“地址字节”写入特定寄存器,控制器会自动将它们打包成符合SPI协议的波形,从引脚发送出去。

  3. 数据寄存器(FIFO):把要发送的“纯数据”写入FIFO,或者从接收FIFO中读出收到的数据。

  4. 状态寄存器:控制器有“忙”标志位,PolledTransfer函数就是通过不断查询这个标志,来知道一次传输是否完成。

整个过程就是CPU通过配置和控制QSPI控制器,间接地操控着外部SPI Flash芯片的引脚电平。

💎 总结

  • C语言概念:实例是内存中的实体对象,指针是访问它的地址凭证。

  • 代码流程:初始化控制器 -> 擦除目标扇区 -> 写入数据 -> 读取数据验证。

  • 底层原理:CPU通过读写QSPI控制器的寄存器,间接控制SPI总线上Flash芯片的引脚电平,从而完成通信。


//将写操作指令以及数据地址写入对应待发送buffer的前4个字节,第五个字节开始才是写入的数据 FlashWriteBuffer[COMMAND_OFFSET] = Command; FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16); FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8); FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF); 老师,这几句可以给小白从语法,含义,各方面解释一下嘛,谢谢啦


这几行代码可以说是嵌入式C语言里处理“协议封包”最经典、最教科书级别的操作了。

我们用寄快递来打个比方:芯片(CPU)要发送一封信给Flash芯片,信的内容(Data)比较长,但必须先填写“快递单”(前4个字节)。这4行代码,就是在手写这张快递单。

我们先明确几个前提:

  1. Address是一个32位(4字节)的变量,比如0x01FF0000。

  2. FlashWriteBuffer是一个字节数组(u8类型)。因为 SPI 通信是一根线,只能一个字节一个字节地发。所以哪怕地址是 32 位的,我们也必须把它“拆”成 3 个 8 位的字节,分开放。

  3. 宏定义COMMAND_OFFSET是 0,ADDRESS_1_OFFSET是 1,以此类推。


第一句:填收件人(指令)

c

FlashWriteBuffer[COMMAND_OFFSET] = Command;
  • 语法:把Command(比如写指令0x02)放到数组的第0个位置。

  • 含义:告诉 Flash 芯片,“我要开始给你下命令了,接下来我要执行Command这个动作”。这是快递单上的“业务类型”。


第二句:填地址高位(比如 0x01)

c

FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16);
  • 含义:提取 32 位地址的最高位字节(第 16 到 23 位),放到数组的第 1 个位置。

  • 语法拆解(洋葱剥皮法):

    1. Address & 0xFF0000:位运算&(按位与)。0xFF0000就像个“面具”,只保留高位的 2 个字节(红色的部分),把低 16 位全变成 0。

      • 比如:0x01FF0000 & 0xFF0000=0x01000000。

    2. >> 16:位移运算。把结果向右移动 16 位,把高位的0x01挪到最低 8 位。

      • 比如:0x01000000 >> 16=0x00000001。

    3. (u8):强制类型转换。把一个大整数,硬生生截断成 8 位(1个字节),多余的高位丢掉。

      • 比如:最终变成0x01。


第三句:填地址中位(比如 0xFF)

c

FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8);
  • 含义:提取 32 位地址的中间字节(第 8 到 15 位),放到数组的第 2 个位置。

  • 语法拆解:

    1. Address & 0xFF00:保留中间两个字节。

      • 比如:0x01FF0000 & 0xFF00=0x0000FF00。

    2. >> 8:向右移动 8 位。

      • 比如:0x0000FF00 >> 8=0x000000FF。

    3. (u8):截断,变成0xFF。


第四句:填地址低位(比如 0x00)

c

FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);
  • 含义:提取 32 位地址的最低位字节(第 0 到 7 位),放到数组的第 3 个位置。

  • 语法拆解:

    1. Address & 0xFF:0xFF就是0000 0000 0000 0000 0000 0000 1111 1111,直接把最低 8 位保留,高位全清零。

    2. 不需要位移了,因为已经在最低位了。

    3. (u8):截断。

      • 比如:0x01FF0000 & 0xFF=0x00000000,最终变成0x00。


💡 底层逻辑:为什么非要拆成 3 个字节?

在 SPI 通信协议中(比如操作 Flash),硬件底层规定:发完 1 个字节的指令后,紧接着必须连续发 3 个字节的地址。这 3 个字节必须是高位在前(大端模式)。

如果你直接发一个 32 位的Address(比如0x01FF0000),在内存里因为小端模式,它存的是00 00 FF 01,如果直接发出去就全乱套了。

所以,这段代码用“掩码(&)+ 移位(>>)”的方法,强行把一个大地址,按顺序拆成了01,FF,00三个字节,依次塞进数组。最后通过函数XQspiPs_PolledTransfer把数组发出去时,物理引脚上就会精准地输出01->FF->00的波形。

这就像把一个大包裹,拆分成三个小快递,按顺序发给对方。对方收到后再重新拼起来。这也是嵌入式底层开发必须掌握的基本功!

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进