在mdk使用swd最后一步无反应_STM32Cube18 | 使用QSPI读写SPI Flash(W25Q64)

本文详细介绍了如何使用STM32CubeMX配置STM32L431RCT6的QSPI接口与W25Q64 SPI Flash通信。从硬件准备、MDK工程生成、串口调试到SPI Flash的命令封装和驱动编写,再到最终的测试,提供了一套完整的步骤和代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

61d244fea999045ecc7b10903b6a73a4.png更多精彩~点击上面蓝字关注我们呀!  17cf79e5634a057a2e3c7f6323e898ec.png

本篇详细的记录了如何使用STM32CubeMX配置STM32L431RCT6的硬件QSPI外设与 SPI Flash 通信(W25Q64)。

1. 准备工作

硬件准备

  • 开发板
    首先需要准备一个开发板,这里我准备的是STM32L4的开发板(BearPi):

d32067a6afae56c2a62b93e7b6cbe2dc.png
  • SPI Flash
    小熊派开发板板载一片SPI Flash,型号为 W25Q64,大小为 8 MB,最大支持 80 Mhz的操作频率。

软件准备

  • 需要安装好Keil - MDK及芯片对应的包,以便编译和下载生成的代码;

  • 准备一个串口调试助手,这里我使用的是Serial Port Utility

Keil MDK和串口助手Serial Port Utility 的安装包都可以在文末关注公众号获取,回复关键字获取相应的安装包:

ac756f7e7c5a9d6f6b43c871ea47d3fb.png

2.生成MDK工程

选择芯片型号

打开STM32CubeMX,打开MCU选择器:

2ea2e789d7b2fc9474461c530fb4c098.png

搜索并选中芯片STM32L431RCT6:

82be0858b85ebb0aeee32f574329c6c3.png

配置时钟源

  • 如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC;

  • 如果使用默认内部时钟(HSI),这一步可以略过;

这里我都使用外部时钟:

b523c3415083b9cd46686c11b799a8f6.png

配置串口

小熊派开发板板载ST-Link并且虚拟了一个串口,原理图如下:

80a2b5997334916e1145a85d9f841dc5.png

这里我将开关拨到AT-MCU模式,使PC的串口与USART1之间连接。

接下来开始配置USART1

90c4e4d88f3c28e22478485f8fccccda.png

配置QSPI接口

首先查看小熊派开发板上 SPI Flash 的原理图:

aa9781953569809c8c68db541484b6a6.png

其引脚连接情况如下:

SPI Flash连接引脚对应引脚
QUADSPI_BK1_NCSPB11
QUADSPI_BK1_CLKPB10
QUADSPI_BK1_IO0PB1
QUADSPI_BK1_IO1PB0

接下来配置 QSPI 接口:

425765a0d5ca0ecf74fa4b9bcfe77162.png

配置时钟树

STM32L4的最高主频到80M,所以配置PLL,最后使HCLK = 80Mhz即可:

3e277140194c1a78676cb13e13fb2566.png

生成工程设置

f49b056391ac280b750da2aa8ab127a2.png

代码生成设置

最后设置生成独立的初始化文件:

9111865aadf4cc85f3774408545d957a.png

生成代码

点击GENERATE CODE即可生成MDK-V5工程:

0216c05882d84628106c3d07c4aa468c.png

3. 在MDK中编写、编译、下载用户代码

重定向printf( )函数

参考:

  • 【STM32Cube_09】重定向printf函数到串口输出的多种方法。

4. 封装 SPI Flash(W25Q64)的命令和底层函数

MCU 通过向 SPI Flash 发送各种命令 来读写 SPI Flash内部的寄存器,所以这种裸机驱动,首先要先宏定义出需要使用的命令,然后利用 HAL 库提供的库函数,封装出三个底层函数,便于移植

  • 向 SPI Flash 发送命令的函数

  • 向 SPI Flash 发送数据的函数

  • 从 SPI Flash 接收数据的函数

接下来开始编写代码~

宏定义操作命令

#define ManufactDeviceID_CMD    0x90
#define READ_STATU_REGISTER_1   0x05
#define READ_STATU_REGISTER_2   0x35
#define READ_DATA_CMD            0x03
#define WRITE_ENABLE_CMD        0x06
#define WRITE_DISABLE_CMD        0x04
#define SECTOR_ERASE_CMD        0x20
#define CHIP_ERASE_CMD            0xc7
#define PAGE_PROGRAM_CMD        0x02

封装发送命令的函数(重点)

/**
 * @brief        向SPI Flash发送指令
 * @param        instruction —— 要发送的指令
 * @param        address     —— 要发送的地址
 * @param        dummyCycles —— 空指令周期数
 * @param        instructionMode —— 指令发送模式
 * @param        addressMode —— 地址发送模式
 * @param        addressSize —— 地址大小
 * @param        dataMode    —— 数据发送模式
 * @retval        成功返回HAL_OK
*/
HAL_StatusTypeDef QSPI_Send_Command(uint32_t instruction, uint32_t address, uint32_t dummyCycles, uint32_t instructionMode, uint32_t addressMode, uint32_t addressSize, uint32_t dataMode){
    QSPI_CommandTypeDef cmd;

    cmd.Instruction = instruction;                     //指令
    cmd.Address = address;                          //地址
    cmd.DummyCycles = dummyCycles;                  //设置空指令周期数
    cmd.InstructionMode = instructionMode;            //指令模式
    cmd.AddressMode = addressMode;                   //地址模式
    cmd.AddressSize = addressSize;                   //地址长度
    cmd.DataMode = dataMode;                         //数据模式
    cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;           //每次都发送指令
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //无交替字节
    cmd.DdrMode = QSPI_DDR_MODE_DISABLE;               //关闭DDR模式
    cmd.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;

    return HAL_QSPI_Command(&hqspi, &cmd, 5000);
}

封装发送数据的函数

/**
* @brief    QSPI发送指定长度的数据
* @param    buf  —— 发送数据缓冲区首地址
* @param    size —— 要发送数据的字节数
 * @retval    成功返回HAL_OK
 */
HAL_StatusTypeDef QSPI_Transmit(uint8_t* send_buf, uint32_t size){
    hqspi.Instance->DLR = size - 1;                         //配置数据长度
    return HAL_QSPI_Transmit(&hqspi, send_buf, 5000);        //接收数据
}

封装接收数据的函数

/**
 * @brief   QSPI接收指定长度的数据
 * @param   buf  —— 接收数据缓冲区首地址
 * @param   size —— 要接收数据的字节数
 * @retval    成功返回HAL_OK
 */
HAL_StatusTypeDef QSPI_Receive(uint8_t* recv_buf, uint32_t size){
    hqspi.Instance->DLR = size - 1;                       //配置数据长度
    return HAL_QSPI_Receive(&hqspi, recv_buf, 5000);            //接收数据
}

5. 编写W25Q64的驱动程序

接下来开始利用上一节封装的宏定义和底层函数,编写W25Q64的驱动程序:

读取Manufacture ID和Device ID

读取 Flash 内部这两个ID有两个作用:

  • 检测SPI Flash是否存在

  • 可以根据ID判断Flash具体型号

数据手册上给出的操作时序如图:

3a1361142434054bdf47dffca4abd293.png

根据该时序,编写代码如下:

/**
 * @brief   读取Flash内部的ID
 * @param   none
 * @retval    成功返回device_id
 */
uint16_t W25QXX_ReadID(void)
{
    uint8_t recv_buf[2] = {0};  //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID
    uint16_t device_id = 0;
    if(HAL_OK == QSPI_Send_Command(ManufactDeviceID_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE))
    {
        //读取ID
        if(HAL_OK == QSPI_Receive(recv_buf, 2))
        {
            device_id = (recv_buf[0] <8) | recv_buf[1];
            return device_id;
        }
        else
        {
            return 0;
        }
    }
    else
    {
        return 0;
    }
}

读取数据

SPI Flash读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制,数据手册给出的时序如下:

fdf467c982e807f6e487dc3d27dbb63e.png

根据该时序图编写代码如下:

/**
 * @brief    读取SPI FLASH数据
 * @param   dat_buffer —— 数据存储区
 * @param   start_read_addr —— 开始读取的地址(最大32bit)
 * @param   byte_to_read —— 要读取的字节数(最大65535)
 * @retval  none
 */
void W25QXX_Read(uint8_t* dat_buffer, uint32_t start_read_addr, uint16_t byte_to_read){
    QSPI_Send_Command(READ_DATA_CMD, start_read_addr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
    QSPI_Receive(dat_buffer, byte_to_read);
}

读取状态寄存器数据并判断Flash是否忙碌

上文中提到,SPI Flash的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有2-3个状态寄存器,指示出 Flash 当前的状态,有趣的一点是:

当 Flash 内部在执行命令时,不能再执行 MCU 发来的命令,但是 MCU 可以一直读取状态寄存器,这下就很好办了,MCU可以一直读取,然后判断Flash是否忙完

1d9911094aa4237c09b0334b6c897b29.png

首先读取状态寄存器的代码如下:

/**
 * @brief    读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器
 * @param     reg  —— 状态寄存器编号(1~2)
 * @retval    状态寄存器的值
 */
uint8_t W25QXX_ReadSR(uint8_t reg)
{
    uint8_t cmd = 0, result = 0;    
    switch(reg)
    {
        case 1:
            /* 读取状态寄存器1的值 */
            cmd = READ_STATU_REGISTER_1;
        case 2:
            cmd = READ_STATU_REGISTER_2;
        case 0:
        default:
            cmd = READ_STATU_REGISTER_1;
    }
    QSPI_Send_Command(cmd, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
    QSPI_Receive(&result, 1);

    return result;
}

然后编写阻塞判断Flash是否忙碌的函数:

/**
 * @brief    阻塞等待Flash处于空闲状态
 * @param   none
 * @retval  none
 */
void W25QXX_Wait_Busy(void){
    while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}

写使能/禁止

Flash 芯片默认禁止写数据,所以在向 Flash 写数据之前,必须发送命令开启写使能,数据手册中给出的时序如下:

19d47d991784c3d9a8e7b393d927f19a.png
cf19d68f8637e4f065d67681ae4a7405.png

编写函数如下:

/**
 * @brief    W25QXX写使能,将S1寄存器的WEL置位
 * @param    none
 * @retval
 */
void W25QXX_Write_Enable(void){
    QSPI_Send_Command(WRITE_ENABLE_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_8_BITS, QSPI_DATA_NONE);
    W25QXX_Wait_Busy();
}

/**
 * @brief    W25QXX写禁止,将WEL清零
 * @param    none
 * @retval    none
 */
void W25QXX_Write_Disable(void){
    QSPI_Send_Command(WRITE_DISABLE_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_8_BITS, QSPI_DATA_NONE);
    W25QXX_Wait_Busy();
}

擦除扇区

SPI Flash有个特性:

数据位可以由1变为0,但是不能由0变为1。

所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF(即全为1),数据手册中给出的时序如下:

a993bec963042d7a24efcbe2ad203532.png

根据此时序编写函数如下:

/**
 * @brief    W25QXX擦除一个扇区
 * @param   sector_addr    —— 扇区地址 根据实际容量设置
 * @retval  none
 * @note    阻塞操作
 */
void W25QXX_Erase_Sector(uint32_t sector_addr){
    sector_addr *= 4096;    //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址
    W25QXX_Write_Enable();  //擦除操作即写入0xFF,需要开启写使能
    W25QXX_Wait_Busy();        //等待写使能完成
    QSPI_Send_Command(SECTOR_ERASE_CMD, sector_addr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_NONE);
    W25QXX_Wait_Busy();       //等待扇区擦除完成
}

页写入操作

向 Flash 芯片写数据的时候,因为 Flash 内部的构造,可以按页写入:

2b29d23b3c6fadf75c4e4730ae4a9492.png

页写入的时序如图:

3145117e2a3e7f3af3606fc9cb7f492e.png

编写代码如下:

/**
 * @brief    页写入操作
 * @param    dat —— 要写入的数据缓冲区首地址
 * @param    WriteAddr —— 要写入的地址
 * @param   byte_to_write —— 要写入的字节数(0-256)
 * @retval    none
 */
void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t byte_to_write){
    W25QXX_Write_Enable();
    QSPI_Send_Command(PAGE_PROGRAM_CMD, WriteAddr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
    QSPI_Transmit(dat, byte_to_write);
    W25QXX_Wait_Busy();
}

6. 测试驱动

main.c 函数中编写代码,测试驱动:

首先定义两个缓存:

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint8_t dat[11] = "mculover666";
uint8_t read_buf[11] = {0};
/* USER CODE END 0 */

然后在 main 函数中编写代码:

/* USER CODE BEGIN 2 */
printf("Test W25QXX...\r\n");
device_id = W25QXX_ReadID();
printf("device_id = 0x%04X\r\n\r\n", device_id);

/* 为了验证,首先读取要写入地址处的数据 */
printf("-------- read data before write -----------\r\n");
W25QXX_Read(read_buf, 5, 11);
printf("read date is %s\r\n", (char*)read_buf);

/* 擦除该扇区 */
printf("-------- erase sector 0 -----------\r\n");
W25QXX_Erase_Sector(0);

/* 写数据 */
printf("-------- write data -----------\r\n");
W25QXX_Page_Program(dat, 5, 11);

/* 再次读数据 */
printf("-------- read data after write -----------\r\n");
W25QXX_Read(read_buf, 5, 11);
printf("read date is %s\r\n", (char*)read_buf);
/* USER CODE END 2 */

测试结果如下:

169424fa8fab5f1d033beaf4a661da5a.png

至此,我们已经学会如何使用硬件QSPI接口读写SPI Flash的数据,下一节将讲述如何使用硬件SDMMC接口读取SD卡数据。

更多精彩文章及资源,请关注我的微信公众号:『mculover666』。

dfc05e4dec2e9d6590a0e69063e06494.png

历史好文集合(点击标题可跳转):

四轴学习课程连接、资料分享、交流群汇总

PCB设计就别再用AD了,有更好的选择!

[飞控]从零开始建模(一)-牛顿欧拉方程

开源STM32F1小四轴完整资料发布一(源代码、原理图、3D库、PCB)

如何制作炫酷的PCB板3D效果图

基于面向对象思维的STM32开发基本思路--以GPIO口的操作为例

灵动微MM32F103C8T6使用初体验

原来飞机还可以这样玩——手拋飞机改无刷背推

宇宙最强编辑器VS Code(十)(完结)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值