STM32 HAL库驱动W25QXX Flash

STM32 HAL库驱动W25QXX Flash

1. 概述

W25QXX系列是一种SPI接口的Flash存储器,广泛应用于嵌入式系统中作为数据存储设备。本文档详细介绍了基于STM32 HAL库的W25QXX Flash驱动实现,包括硬件连接、驱动函数实现以及使用示例。

项目源码仓库:STM32_Sensor_Drives

2. 硬件连接

在这里插入图片描述

W25QXX Flash通过SPI接口与STM32连接,主要包括以下引脚:

  • SCK - 连接到STM32的SPI1_SCK (PA5)
  • MISO - 连接到STM32的SPI1_MISO (PA6)
  • MOSI - 连接到STM32的SPI1_MOSI (PA7)
  • CS - 连接到STM32的GPIO (PA4)

3. 驱动实现

3.1 SPI配置

首先,我们需要配置SPI接口以与W25QXX通信。在spi.c文件中,SPI1的初始化配置如下:

void MX_SPI1_Init(void)
{
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
  hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 10;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
}

这里配置SPI为主模式,8位数据宽度,高电平空闲,第二个边沿采样,软件控制NSS,波特率预分频为8,MSB优先传输。

3.2 GPIO配置

W25QXX的片选信号需要通过GPIO控制,在gpio.c中配置如下:

void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

  /*Configure GPIO pin : PA4 */
  GPIO_InitStruct.Pin = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

PA4配置为推挽输出模式,用于控制W25QXX的片选信号。初始状态设置为高电平(未选中)。

3.3 W25QXX命令定义

spi.h文件中,定义了W25QXX的各种命令和片选引脚:

#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

#define W25Q64_CHIP_SELECT_GPIO_Port GPIOA
#define W25Q64_CHIP_SELECT_Pin GPIO_PIN_4

这些命令用于实现读取ID、读写数据、擦除扇区等操作。

3.4 SPI基础通信函数

spi.c文件中,实现了三个基础的SPI通信函数:

/**
 * @brief    SPI发送指定长度的数据
 * @param    buf  —— 发送数据缓冲区首地址
 * @param    size —— 要发送数据的字节数
 * @retval   成功返回HAL_OK
 */
static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)
{
    return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);
}

/**
 * @brief   SPI接收指定长度的数据
 * @param   buf  —— 接收数据缓冲区首地址
 * @param   size —— 要接收数据的字节数
 * @retval  成功返回HAL_OK
 */
static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)
{
   return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);
}

/**
 * @brief   SPI在发送数据的同时接收指定长度的数据
 * @param   send_buf  —— 接收数据缓冲区首地址
 * @param   recv_buf  —— 接收数据缓冲区首地址
 * @param   size —— 要发送/接收数据的字节数
 * @retval  成功返回HAL_OK
 */
static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)
{
   return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);
}

这三个函数分别用于发送数据、接收数据和同时发送接收数据,是W25QXX驱动的基础。

3.5 W25QXX驱动函数实现

3.5.1 读取Flash ID
/**
 * @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;
    uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00};   //待发送数据,命令+地址
    
    /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    /* 发送并读取数据 */
    if (HAL_OK == SPI_Transmit(send_data, 4)) 
    {
        if (HAL_OK == SPI_Receive(recv_buf, 2)) 
        {
            device_id = (recv_buf[0] << 8) | recv_buf[1];
        }
    }
    
    /* 取消片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    return device_id;
}

该函数用于读取W25QXX的制造商ID和设备ID,通过发送0x90命令和三个字节的地址(全0),然后读取两个字节的数据。

3.5.2 读取状态寄存器
/**
 * @brief     读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器
 * @param     reg  —— 状态寄存器编号(1~2)
 * @retval    状态寄存器的值
 */
static uint8_t W25QXX_ReadSR(uint8_t reg)
{
    uint8_t result = 0; 
    uint8_t send_buf[4] = {0x00,0x00,0x00,0x00};
    switch(reg)
    {
        case 1:
            send_buf[0] = READ_STATU_REGISTER_1;
        case 2:
            send_buf[0] = READ_STATU_REGISTER_2;
        case 0:
        default:
            send_buf[0] = READ_STATU_REGISTER_1;
    }
    
     /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    if (HAL_OK == SPI_Transmit(send_buf, 4)) 
    {
        if (HAL_OK == SPI_Receive(&result, 1)) 
        {
            HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
            
            return result;
        }
    }
    
    /* 取消片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);

    return 0;
}

该函数用于读取W25QXX的状态寄存器,W25Q64有两个状态寄存器,通过参数reg选择要读取的寄存器。

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

该函数通过循环检查状态寄存器1的最低位(BUSY位),等待其变为0,表示Flash处于空闲状态。

3.5.4 读取数据
/**
 * @brief   读取SPI FLASH数据
 * @param   buffer      —— 数据存储区
 * @param   start_addr  —— 开始读取的地址(最大32bit)
 * @param   nbytes      —— 要读取的字节数(最大65535)
 * @retval  成功返回0,失败返回-1
 */
int W25QXX_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes)
{
    uint8_t cmd = READ_DATA_CMD;
    
    start_addr = start_addr << 8;
    
	W25QXX_Wait_Busy();
    
     /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    if (HAL_OK == SPI_Transmit((uint8_t*)&start_addr, 3)) 
    {
        if (HAL_OK == SPI_Receive(buffer, nbytes)) 
        {
            HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
            return 0;
        }
    }
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    return -1;
}

该函数用于从指定地址读取指定长度的数据,首先发送读取命令(0x03),然后发送3字节地址,最后读取数据。

3.5.5 写使能和写禁止
/**
 * @brief    W25QXX写使能,将S1寄存器的WEL置位
 * @param    none
 * @retval
 */
void W25QXX_Write_Enable(void)
{
    uint8_t cmd= WRITE_ENABLE_CMD;
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();

}

/**
 * @brief    W25QXX写禁止,将WEL清零
 * @param    none
 * @retval    none
 */
void W25QXX_Write_Disable(void)
{
    uint8_t cmd = WRITE_DISABLE_CMD;

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();
}

这两个函数分别用于使能和禁止写操作。在进行写入或擦除操作前,必须先调用写使能函数。

3.5.6 扇区擦除
/**
 * @brief    W25QXX擦除一个扇区
 * @param   sector_addr    —— 扇区地址 根据实际容量设置
 * @retval  none
 * @note    阻塞操作
 */
void W25QXX_Erase_Sector(uint32_t sector_addr)
{
    uint8_t cmd = SECTOR_ERASE_CMD;
    
    sector_addr *= 4096;    //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址
    sector_addr <<= 8;
    
    W25QXX_Write_Enable();  //擦除操作即写入0xFF,需要开启写使能
    W25QXX_Wait_Busy();        //等待写使能完成
   
     /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    SPI_Transmit((uint8_t*)&sector_addr, 3);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();       //等待扇区擦除完成
}

该函数用于擦除指定的扇区,每个扇区大小为4KB。擦除前需要先使能写操作,擦除后需要等待操作完成。

3.5.7 页编程(写入数据)
/**
 * @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 nbytes)
{
    uint8_t cmd = PAGE_PROGRAM_CMD;
    
    WriteAddr <<= 8;
    
    W25QXX_Write_Enable();
    
    /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);

    SPI_Transmit((uint8_t*)&WriteAddr, 3);
    
    SPI_Transmit(dat, nbytes);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();
}

该函数用于向指定地址写入数据,写入前需要先使能写操作,写入后需要等待操作完成。W25QXX的页大小为256字节,一次写入不能超过一页。

4. 使用示例

main.c文件中,展示了如何使用W25QXX驱动进行读写操作:

int main(void)
{
  /* 省略初始化代码 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
    printf("System will start while\n");
    printf("read data before write\r\n");
    W25QXX_Read(read_buf, 0, 10);
    snprintf(str, 15, "read data: %x", read_buf[2]);
    printf("%s\r\n", str);

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

    /* 再次读数据 */
    printf("read erase data\r\n");
    W25QXX_Read(read_buf, 0, 10);
    memset(str, 0, sizeof(str)); // 清空读缓冲区
    snprintf(str, 15, "read data: %x", read_buf[2]);
    printf("%s\r\n", str);

    /* 写数据 */
    printf("write data \r\n");
    for (i = 0; i < 10; i++)
    {
        write_buf[i] = i;
    }
    W25QXX_Page_Program(write_buf, 0, 10); // 写数据

    /* 再次读数据 */
    printf("read write data \r\n");
    W25QXX_Read(read_buf, 0, 10);
    memset(str, 0, sizeof(str)); // 清空读缓冲区
    snprintf(str, 15, "read data: %x", read_buf[2]);
    printf("%s\r\n", str);
    while (1)
    {
        HAL_Delay(200);
    }
}

这个示例展示了完整的W25QXX操作流程:

  1. 首先读取Flash中的数据
  2. 擦除扇区0
  3. 再次读取数据,验证擦除效果
  4. 写入新数据
  5. 再次读取数据,验证写入效果

5. 串口打印功能

为了方便调试,在usart.c中实现了printf函数的重定向:

int fputc(int ch, FILE *f)
{
    if (f == stdout)  // 仅处理标准输出
    {
        HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 100);  // 阻塞发送
        if (ch == '\n')  // 发送\n时自动补充\r
            HAL_UART_Transmit(&huart2, (uint8_t *)"\r", 1, 100);
    }
    return ch;
}

通过重定向fputc函数,使得printf函数的输出通过UART2发送,便于观察程序运行状态。

6. 总结

本文档详细介绍了基于STM32 HAL库的W25QXX Flash驱动实现,包括:

  1. SPI和GPIO的配置
  2. W25QXX的命令定义
  3. 基础SPI通信函数
  4. W25QXX驱动函数实现(读ID、读写数据、擦除扇区等)
  5. 使用示例
  6. 串口打印功能

通过这些代码,可以方便地在STM32项目中使用W25QXX Flash进行数据存储和读取。

7. 注意事项

  1. W25QXX的写入操作需要先擦除扇区,因为Flash只能将1变为0,不能将0变为1
  2. 擦除和写入操作前必须先使能写操作
  3. 写入数据不能跨页,一页为256字节
  4. 擦除和写入操作需要等待完成,否则可能导致数据错误
  5. 频繁擦写同一区域会导致Flash寿命减少,建议实现磨损均衡算法

8. 参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值