[开发工具] STM32CubeMX-18 | 使用硬件QSPI读写SPI Flash(W25Q64)

[复制链接]
4317|48
phosphate 发表于 2020-4-12 19:32 | 显示全部楼层
封装发送数据的函数

  1. /**
  2. * @brief    QSPI发送指定长度的数据
  3. * @param    buf  —— 发送数据缓冲区首地址
  4. * @param    size —— 要发送数据的字节数
  5. * @retval    成功返回HAL_OK
  6. */
  7. HAL_StatusTypeDef QSPI_Transmit(uint8_t* send_buf, uint32_t size)
  8. {
  9.     hqspi.Instance->DLR = size - 1;                         //配置数据长度
  10.     return HAL_QSPI_Transmit(&hqspi, send_buf, 5000);        //接收数据
  11. }
phosphate 发表于 2020-4-12 19:33 | 显示全部楼层
封装接收数据的函数

  1. /**
  2. * @brief      QSPI接收指定长度的数据
  3. * @param   buf  —— 接收数据缓冲区首地址
  4. * @param   size —— 要接收数据的字节数
  5. * @retval    成功返回HAL_OK
  6. */
  7. HAL_StatusTypeDef QSPI_Receive(uint8_t* recv_buf, uint32_t size)
  8. {
  9.     hqspi.Instance->DLR = size - 1;                       //配置数据长度
  10.     return HAL_QSPI_Receive(&hqspi, recv_buf, 5000);            //接收数据
  11. }
phosphate 发表于 2020-4-12 19:33 | 显示全部楼层
5. 编写W25Q64的驱动程序

接下来开始利用上一节封装的宏定义和底层函数,编写W25Q64的驱动程序:
phosphate 发表于 2020-4-12 19:34 | 显示全部楼层
读取Manufacture ID和Device ID
读取 Flash 内部这两个ID有两个作用:
     检测SPI Flash是否存在
     可以根据ID判断Flash具体型号

数据手册上给出的操作时序如图:
364575e92fcc019b63.png
phosphate 发表于 2020-4-12 19:35 | 显示全部楼层
根据该时序,编写代码如下:

  1. /**
  2. * @brief   读取Flash内部的ID
  3. * @param   none
  4. * @retval    成功返回device_id
  5. */
  6. uint16_t W25QXX_ReadID(void)
  7. {
  8.     uint8_t recv_buf[2] = {0};    //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID
  9.     uint16_t device_id = 0;
  10.     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))
  11.     {
  12.         //读取ID
  13.         if(HAL_OK == QSPI_Receive(recv_buf, 2))
  14.         {
  15.             device_id = (recv_buf[0] << 8) | recv_buf[1];
  16.             return device_id;
  17.         }
  18.         else
  19.         {
  20.             return 0;
  21.         }
  22.     }
  23.     else
  24.     {
  25.         return 0;
  26.     }
  27. }
phosphate 发表于 2020-4-12 19:36 | 显示全部楼层
读取数据
SPI Flash读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制,数据手册给出的时序如下:

424825e92fd00921b1.png
phosphate 发表于 2020-4-12 19:37 | 显示全部楼层
根据该时序图编写代码如下:

  1. /**
  2. * @brief    读取SPI FLASH数据
  3. * @param   dat_buffer —— 数据存储区
  4. * @param   start_read_addr —— 开始读取的地址(最大32bit)
  5. * @param   byte_to_read —— 要读取的字节数(最大65535)
  6. * @retval  none
  7. */
  8. void W25QXX_Read(uint8_t* dat_buffer, uint32_t start_read_addr, uint16_t byte_to_read)
  9. {
  10.     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);
  11.     QSPI_Receive(dat_buffer, byte_to_read);
  12. }
phosphate 发表于 2020-4-12 19:38 | 显示全部楼层
读取状态寄存器数据并判断Flash是否忙碌
上文中提到,SPI Flash的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有2-3个状态寄存器,指示出 Flash 当前的状态,有趣的一点是:

当 Flash 内部在执行命令时,不能再执行 MCU 发来的命令,但是 MCU 可以一直读取状态寄存器,这下就很好办了,MCU可以一直读取,然后判断Flash是否忙完:
595475e92fd8a78917.png
phosphate 发表于 2020-4-12 19:40 | 显示全部楼层
首先读取状态寄存器的代码如下:

  1. /**
  2. * @brief    读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器
  3. * @param     reg  —— 状态寄存器编号(1~2)
  4. * @retval    状态寄存器的值
  5. */
  6. uint8_t W25QXX_ReadSR(uint8_t reg)
  7. {
  8.     uint8_t cmd = 0, result = 0;   
  9.     switch(reg)
  10.     {
  11.         case 1:
  12.             /* 读取状态寄存器1的值 */
  13.             cmd = READ_STATU_REGISTER_1;
  14.         case 2:
  15.             cmd = READ_STATU_REGISTER_2;
  16.         case 0:
  17.         default:
  18.             cmd = READ_STATU_REGISTER_1;
  19.     }
  20.     QSPI_Send_Command(cmd, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
  21.     QSPI_Receive(&result, 1);

  22.     return result;
  23. }
phosphate 发表于 2020-4-12 19:40 | 显示全部楼层
然后编写阻塞判断Flash是否忙碌的函数:

  1. /**
  2. * @brief    阻塞等待Flash处于空闲状态
  3. * @param   none
  4. * @retval  none
  5. */
  6. void W25QXX_Wait_Busy(void)
  7. {
  8.     while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
  9. }
phosphate 发表于 2020-4-12 19:42 | 显示全部楼层
本帖最后由 phosphate 于 2020-4-12 19:43 编辑

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

687475e92fea190504.png 338365e92fef585d7c.png
phosphate 发表于 2020-4-12 19:44 | 显示全部楼层
编写函数如下:

  1. /**
  2. * @brief    W25QXX写使能,将S1寄存器的WEL置位
  3. * @param    none
  4. * @retval
  5. */
  6. void W25QXX_Write_Enable(void)
  7. {
  8.     QSPI_Send_Command(WRITE_ENABLE_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_8_BITS, QSPI_DATA_NONE);
  9.         W25QXX_Wait_Busy();
  10. }

  11. /**
  12. * @brief    W25QXX写禁止,将WEL清零
  13. * @param    none
  14. * @retval    none
  15. */
  16. void W25QXX_Write_Disable(void)
  17. {
  18.     QSPI_Send_Command(WRITE_DISABLE_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_8_BITS, QSPI_DATA_NONE);
  19.         W25QXX_Wait_Busy();
  20. }
phosphate 发表于 2020-4-12 19:45 | 显示全部楼层
擦除扇区
SPI Flash有个特性:

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

所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF(即全为1),数据手册中给出的时序如下:
541965e92ff3f703ea.png
phosphate 发表于 2020-4-12 19:48 | 显示全部楼层
根据此时序编写函数如下:

  1. /**
  2. * @brief    W25QXX擦除一个扇区
  3. * @param   sector_addr    —— 扇区地址 根据实际容量设置
  4. * @retval  none
  5. * [url=home.php?mod=space&uid=536309]@NOTE[/url]    阻塞操作
  6. */
  7. void W25QXX_Erase_Sector(uint32_t sector_addr)
  8. {
  9.     sector_addr *= 4096;    //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址
  10.     W25QXX_Write_Enable();  //擦除操作即写入0xFF,需要开启写使能
  11.     W25QXX_Wait_Busy();        //等待写使能完成
  12.     QSPI_Send_Command(SECTOR_ERASE_CMD, sector_addr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_NONE);
  13.     W25QXX_Wait_Busy();       //等待扇区擦除完成
  14. }
phosphate 发表于 2020-4-12 19:49 | 显示全部楼层
页写入操作
向 Flash 芯片写数据的时候,因为 Flash 内部的构造,可以按页写入:

79515e93002245521.png
phosphate 发表于 2020-4-12 19:50 | 显示全部楼层
页写入的时序如图:

805625e930048b8151.png
phosphate 发表于 2020-4-12 19:51 | 显示全部楼层
编写代码如下:

  1. /**
  2. * @brief    页写入操作
  3. * @param    dat —— 要写入的数据缓冲区首地址
  4. * @param    WriteAddr —— 要写入的地址
  5. * @param   byte_to_write —— 要写入的字节数(0-256)
  6. * @retval    none
  7. */
  8. void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t byte_to_write)
  9. {
  10.     W25QXX_Write_Enable();
  11.     QSPI_Send_Command(PAGE_PROGRAM_CMD, WriteAddr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
  12.     QSPI_Transmit(dat, byte_to_write);
  13.     W25QXX_Wait_Busy();
  14. }
phosphate 发表于 2020-4-12 19:53 | 显示全部楼层
6. 测试驱动
在 main.c 函数中编写代码,测试驱动:

首先定义两个缓存:

  1. /* Private user code ---------------------------------------------------------*/
  2. /* USER CODE BEGIN 0 */
  3. uint8_t dat[11] = "mculover666";
  4. uint8_t read_buf[11] = {0};
  5. /* USER CODE END 0 */
phosphate 发表于 2020-4-12 19:54 | 显示全部楼层
然后在 main 函数中编写代码:

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

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

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

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

  15. /* 再次读数据 */
  16. printf("-------- read data after write -----------\r\n");
  17. W25QXX_Read(read_buf, 5, 11);
  18. printf("read date is %s\r\n", (char*)read_buf);
  19. /* USER CODE END 2 */
phosphate 发表于 2020-4-12 19:56 | 显示全部楼层
测试结果如下:

488105e9301f104b69.png
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部