当前位置: 首页 > news >正文

STM32SPI实战-Flash模板

STM32SPI实战-Flash模板

  • 一,常用指令集(部分)
  • 二,组件库GD25QXX API 函数解析
    • 1,前提条件
    • 2,初始化与识别
        • 1, void spi_flash_init(void)
        • 2, uint32_t spi_flash_read_id(void)
    • 3,擦除操作
        • 1, void spi_flash_sector_erase(uint32_t sector_addr)
        • 2, void spi_flash_bulk_erase(void)
    • 3,写入操作
        • 1, void spi_flash_page_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)
        • 2, void spi_flash_buffer_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)
    • 4,读取操作
        • void spi_flash_buffer_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
    • 5,辅助及底层函数
        • 1, void spi_flash_write_enable(void)
        • 2, void spi_flash_wait_for_write_end(void)
        • 3, uint8_t spi_flash_send_byte(uint8_t byte)
        • 4, uint16_t spi_flash_send_halfword(uint16_t half_word)
        • 5, 其他底层读取函数
  • 三,SPI CubeMX配置
    • 1,选择引脚
    • 2,参数配置
    • 3,配置CS (Chip Select) GPIO引脚:
    • 4,生成代码:
  • 四,Flash模板移植
    • 1 获取并放驱动文件
    • 二,移植模板

上一节介绍了通用的SPI模块的开发流程,本节以GD25QXX系列FLASH芯片为例,移植Flash模板

我们提供的GD25QXX驱动库 (gd25qxx.c 和 gd25qxx.h) 封装了与SPI FLASH通信的底层细节,提供了一系列简单易用的API函数供上层应用调用。这些函数依赖于STM32 HAL库的SPI和GPIO功能。

一,常用指令集(部分)

GD25QXX系列芯片支持一套丰富的操作指令。以下列出了一些驱动代码中涉及或常用的指令:

WREN (0x06): 写使能。在执行任何写入或擦除操作之前,必须先发送此指令。

WRDI (0x04): 写禁止。禁止写入操作。

RDID (0x90 或 0x9F): 读ID。读取制造商ID、器件ID等信息。0x90通常用于读取制造商和设备ID,0x9F (RDID JEDEC) 读取更详细的JEDEC ID信息。本驱动使用 0x90。

RDSR (0x05): 读状态寄存器。用于查询FLASH的当前状态,如WIP(写操作进行中)位、WEL(写使能锁存)位等。

WRSR (0x01): 写状态寄存器。用于修改状态寄存器的某些位(如块保护位),需谨慎使用。

READ (0x03): 读数据。从指定地址开始读取数据。

FAST_READ (0x0B): 快速读数据。通常在指令和地址后需要跟一个dummy byte。

PP (0x02): 页编程 (Page Program)。向指定页写入数据,写入前该页对应区域通常需要被擦除。本驱动中使用 WRITE (0x02) 宏定义此指令。

SE (0x20): 扇区擦除 (Sector Erase)。擦除指定的扇区(通常为4KB)。

BE32K (0x52): 32KB块擦除 (Block Erase)。

BE64K (0xD8): 64KB块擦除。

CE (0xC7 或 0x60): 全片擦除 (Chip Erase)。0xC7 和 0x60 作用相同。本驱动中使用 BE (0xC7) 宏定义此指令。

DP (0xB9): 深度掉电。

RES (0xAB): 从深度掉电模式唤醒,并读取电子签名。

具体指令值和支持情况请务必参考您所使用的GD25QXX芯片的最新数据手册

二,组件库GD25QXX API 函数解析

我们提供的GD25QXX驱动库 (gd25qxx.c 和 gd25qxx.h) 封装了与SPI FLASH通信的底层细节,提供了一系列简单易用的API函数供上层应用调用。这些函数依赖于STM32 HAL库的SPI和GPIO功能。

1,前提条件

在使用这些API之前,请确保你已经在STM32CubeMX中正确配置了SPI外设 (例如SPI2) 以及对应的MOSI, MISO, SCK引脚和片选(CS)引脚 (本驱动默认为PB12)。SPI外设和GPIO应已通过 MX_SPIx_Init() 和 MX_GPIO_Init() 完成初始化。驱动代码通过 extern SPI_HandleTypeDef hspi2; 来引用SPI句柄,请确保你的项目中定义了此句柄或将其修改为实际使用的句柄。

2,初始化与识别

1, void spi_flash_init(void)

用途: 初始化SPI FLASH芯片。
参数: 无。
返回: 无。
说明: 此函数主要确保SPI FLASH的片选(CS)引脚在初始状态时被拉高(即芯片未被选中)。它假设SPI外设本身(如时钟、模式、数据位等)和CS引脚的GPIO模式已由STM32CubeMX生成的代码(如 MX_SPIx_Init(), MX_GPIO_Init())配置完毕。你可以在此函数中添加读取Flash ID的操作,作为初始通信检查。

// 在系统初始化代码中调用
MX_SPI2_Init(); // 由CubeMX生成
spi_flash_init();// 可选:检查ID
uint32_t flash_id = spi_flash_read_id();
if (flash_id == EXPECTED_FLASH_ID) {// 初始化成功
} else {// FLASH ID不匹配或通信错误
}
2, uint32_t spi_flash_read_id(void)

用途: 读取SPI FLASH的ID。通常包含制造商ID和设备ID。
参数: 无。
返回: uint32_t - 读取到的ID。例如,对于GD25Q40,可能返回 0xC84013 (制造商ID C8h, 设备ID 4013h)。高字节通常是制造商ID,后续字节是设备相关ID。
内部操作: 发送 RDID (0x90) 指令,然后读取三个字节的ID信息。

uint32_t id = spi_flash_read_id();
printf("Flash ID: 0x%08X\r\n", id);
// 一般 JEDEC ID (0x9F指令) 会返回 制造商ID, 存储器类型, 容量
// 此驱动使用的0x90指令返回的ID格式需查阅具体芯片手册

3,擦除操作

在对FLASH进行写入(编程)之前,目标区域的存储单元必须先被擦除。擦除操作会将指定区域的每一位(bit)都设置为1。

1, void spi_flash_sector_erase(uint32_t sector_addr)

用途: 擦除指定的扇区。一个扇区的大小通常为4KB。
参数: sector_addr - 要擦除扇区内的任意地址。驱动程序通常会根据此地址计算出扇区的起始地址。
返回: 无。
内部操作:
调用 spi_flash_write_enable() 发送写使能(WREN)指令。
发送扇区擦除(SE 0x20)指令,后跟24位地址。
调用 spi_flash_wait_for_write_end() 等待擦除操作完成。
注意: 传入的地址 sector_addr 会被用来定位扇区。例如,如果一个扇区是4KB (0x1000字节),地址0x0000至0x0FFF属于第一个扇区,地址0x1000至0x1FFF属于第二个扇区。擦除操作会擦除整个扇区。

// 擦除地址为0x2000所在的扇区 (即第二个扇区, 假设扇区大小4KB)
spi_flash_sector_erase(0x002000); // 擦除首个扇区
spi_flash_sector_erase(0x000000);
2, void spi_flash_bulk_erase(void)

用途: 擦除整个FLASH芯片。这是一个耗时操作,需谨慎使用。
参数: 无。
返回: 无。
内部操作:
调用 spi_flash_write_enable()。
发送全片擦除(BE/CE 0xC7)指令。
调用 spi_flash_wait_for_write_end() 等待擦除完成。
警告: 此操作会清除芯片上的所有数据!所需时间较长,具体时间取决于芯片容量。

// 警告:这将擦除芯片所有内容!
// printf("Performing bulk erase. This may take a while...\r\n");
// spi_flash_bulk_erase();
// printf("Bulk erase complete.\r\n");

3,写入操作

FLASH写入(编程)操作只能将存储单元的位从1变为0,不能从0变为1。因此,在写入之前,目标区域必须已经被擦除(所有位为1)。

1, void spi_flash_page_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)

用途: 向指定的页内写入数据。写入的数据长度不能超过一页的剩余空间,且不能跨页。FLASH的页大小通常为256字节 (由宏 SPI_FLASH_PAGE_SIZE 定义为 0x100)。
参数:
pbuffer: 指向包含待写入数据的缓冲区的指针。
write_addr: FLASH内部的24位起始写入地址。
num_byte_to_write: 要写入的字节数 (最大256,且不能跨页)。
返回: 无。
内部操作:
调用 spi_flash_write_enable()。
发送页编程(WRITE 0x02)指令,后跟24位地址。
发送 num_byte_to_write 个字节的数据。
调用 spi_flash_wait_for_write_end() 等待写入完成。
注意: 如果 write_addr + num_byte_to_write 超出当前页的边界,行为未定义或可能导致数据写入到下一页的起始部分,具体取决于FLASH芯片特性,但此函数设计上不处理跨页。

uint8_t data_to_write[10] = "HelloSPI";
// 确保目标扇区已擦除
spi_flash_sector_erase(0x000000); 
// 在地址0x0000处写入数据 (在第一页内)
spi_flash_page_write(data_to_write, 0x000000, strlen((char*)data_to_write));
2, void spi_flash_buffer_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)

用途: 向FLASH写入一个数据块,自动处理跨页写入。
参数:
pbuffer: 指向待写入数据缓冲区的指针。
write_addr: 起始写入地址。
num_byte_to_write: 要写入的总字节数。
返回: 无。
说明: 此函数是更高级的写入函数。它会判断起始地址和写入长度,如果发生跨页,则会将数据拆分成多个部分,分别调用 spi_flash_page_write() 进行写入。

uint8_t large_data_buffer[500];
// Fill large_data_buffer with data...
for(int i=0; i<500; i++) large_data_buffer[i] = i % 256;// 假设0x10000开始的区域已擦除
spi_flash_sector_erase(0x010000); // 擦除4KB
if (500 > 256) spi_flash_sector_erase(0x010000 + 0x1000); // 可能需要擦除更多扇区// 从地址 0x0100F0 开始写入500字节数据 (将会跨页)
spi_flash_buffer_write(large_data_buffer, 0x0100F0, 500);

4,读取操作

void spi_flash_buffer_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)

用途: 从FLASH的指定地址读取一块数据到MCU的缓冲区。
参数:
pbuffer: 指向用于存储读取数据的MCU缓冲区的指针。
read_addr: FLASH内部的24位起始读取地址。
num_byte_to_read: 要读取的字节数。
返回: 无。
内部操作:
拉低CS片选。
发送读数据(READ 0x03)指令,后跟24位地址。
循环读取 num_byte_to_read 个字节:为每个要读取的字节发送一个DUMMY_BYTE (0xA5),并将接收到的字节存入 pbuffer。
拉高CS片选。

uint8_t read_buffer[10];
// 从地址0x000000读取之前写入的 "HelloSPI" (假设长度为8)
spi_flash_buffer_read(read_buffer, 0x000000, 8);
read_buffer[8] = '\0'; // Null-terminate for string operations
printf("Read data: %s\r\n", read_buffer);// 比较写入和读出的数据
// if (memcmp(data_to_write, read_buffer, 8) == 0) {
//    printf("Data verification successful!\r\n");
// } else {
//    printf("Data verification failed!\r\n");
// }

5,辅助及底层函数

这些函数主要被上述高级API内部调用,但在某些特定情况下,用户也可能需要了解或直接使用它们。

1, void spi_flash_write_enable(void)

用途: 发送写使能 (WREN) 指令 (0x06) 到FLASH芯片。
说明: 在执行任何擦除或写入操作(如扇区擦除、页编程)之前,必须先调用此函数来设置FLASH内部的写使能锁存(WEL)位。否则,擦除/写入指令将被忽略。

2, void spi_flash_wait_for_write_end(void)

用途: 等待FLASH内部的擦除或写入操作完成。
说明: FLASH的擦除和写入操作需要一定的时间(从几毫秒到几秒不等)。此函数通过循环读取状态寄存器(RDSR 0x05指令)并检查其中的WIP(Write In Progress)位来实现等待。当WIP位为0时,表示操作已完成。
注意: 这是一个阻塞型函数,在等待期间会占用CPU。对于需要实时响应的系统,可能需要考虑非阻塞的实现方式(如使用定时器和中断)。

3, uint8_t spi_flash_send_byte(uint8_t byte)

用途: 通过SPI发送一个字节,并返回接收到的一个字节。
参数: byte - 要发送的字节。
返回: uint8_t - SPI总线上同时接收到的字节。
说明: 这是底层的SPI单字节通信函数,基于 HAL_SPI_TransmitReceive()。在SPI全双工通信中,发送一个字节的同时也会接收一个字节。

4, uint16_t spi_flash_send_halfword(uint16_t half_word)

用途: 通过SPI发送一个半字 (16位),并返回接收到的一个半字。
参数: half_word - 要发送的半字。
返回: uint16_t - SPI总线上同时接收到的半字。
说明: 底层SPI双字节通信函数,基于 HAL_SPI_TransmitReceive()。

5, 其他底层读取函数

void spi_flash_start_read_sequence(uint32_t read_addr): 发送READ指令和地址,为连续读取做准备,但不结束SPI会话(CS保持低)。
uint8_t spi_flash_read_byte(void): 在已启动的读取序列中,发送一个dummy byte并读取一个字节。
说明: 这两个函数主要被 spi_flash_buffer_read() 内部使用,用于优化连续读取。一般情况下,用户直接使用 spi_flash_buffer_read() 即可。

三,SPI CubeMX配置

1,选择引脚

根据原理图,选择SPI的四个引脚:PB12,13,14,15
其中PB12配置为输出
在这里插入图片描述
根据四个引脚是SPI2,选择SPI2,并且选择全双工模式
在这里插入图片描述

2,参数配置

根据芯片类型选择极性和相位,不同的芯片支持的模式不同
在这里插入图片描述

参数设置
Frame Format: Motorola
Data Size: 8 Bits
First Bit: MSB First
Clock Polarity (CPOL): High. GD25QXX系列SPI FLASH通常支持SPI模式0 (CPOL=Low, CPHA=1 Edge an CubeMX) 和SPI模式3 (CPOL=High, CPHA=2 Edge in CubeMX)。此处的 High 是SPI模式3的CPOL要求。
Clock Phase (CPHA): 2 Edge. 结合CPOL=High,这构成了SPI模式3。请务必查阅您所用FLASH芯片的数据手册,确保所选SPI模式(CPOL/CPHA组合)受其支持。
Prescaler (for Baud Rate): 2. 此预分频值将决定SPI通讯的波特率。根据截图,产生的波特率为 22.5 MBit/s。实际波特率计算公式为:SPI_CLK = PCLK / PrescalerValueFromTable (其中PCLK是SPI外设的时钟源频率,PrescalerValueFromTable是基于设置值’2’从CubeMX内部表格确定的实际分频系数)。例如,若PCLK为45MHz且设置’2’对应实际分频系数2, 则SPI_CLK = 45MHz / 2 = 22.5MHz。选择合适的波特率需兼顾通讯速度和稳定性,且不能超过FLASH芯片规格上限。
NSS Signal Type: Software. 此选项通常与 “Hardware NSS Signal: Disable” 自动关联。再次确认由软件控制CS。
CRC Calculation: Disabled. 本驱动不使用CRC校验。
请添加图片描述
上图展示了一个基于STM32CubeMX的SPI配置实例。关键在于确保CPOL和CPHA的组合与您的FLASH芯片兼容,并且波特率设置合理。 不同系列的STM32芯片,其CubeMX界面和可用预分频值可能略有差异。

3,配置CS (Chip Select) GPIO引脚:

SPI相关的SCK, MISO, MOSI引脚在启用SPI并分配到具体引脚后,通常会自动配置为正确的复用功能模式 (Alternate Function Push-pull)。请在CubeMX的Pinout视图中确认这些引脚分配正确。

CS引脚配置:
1,选择一个未被占用的通用GPIO引脚作为CS信号线。本驱动在 gd25qxx.h 文件中通过宏定义 (如 SPI_FLASH_CS_GPIO_PORT 和 SPI_FLASH_CS_PIN,默认可能是 GPIOB, GPIO_PIN_12) 来指定CS引脚。您需要确保CubeMX中的配置与这些宏定义一致,或修改宏以匹配您的硬件连接。
2,在CubeMX中,找到您选定的CS引脚,将其配置为 GPIO_Output。
3,GPIO Output level: High (SPI片选通常是低电平有效,因此空闲时应为高电平,确保FLASH芯片初始未被选中)。
4,GPIO mode: Output Push Pull。
5,GPIO Pull-up/Pull-down: No pull-up and no pull-down。
6,Maximum output speed: Medium 或 High 通常足够。
7,(可选) User Label: 为该引脚设置一个易于识别的标签,如 SPI_FLASH_CS。

请添加图片描述

请务必将上图替换为您项目中CS引脚的实际CubeMX GPIO配置截图,并确保其与驱动代码中的CS引脚定义匹配。

4,生成代码:

完成所有SPI和GPIO配置后,保存 .ioc 文件。
在CubeMX中点击 “Generate Code” (或快捷键 ALT+K)。这将更新您的项目,生成或修改包括 main.c (包含 MX_SPIx_Init() 和 MX_GPIO_Init() 等函数初始化代码) 在内的相关文件。

四,Flash模板移植

1 获取并放驱动文件

获取文件: 从提供的地方获取 gd25qxx.c 和 gd25qxx.h 驱动文件。
放置文件: 将这两个文件复制到你项目的一个合适位置,例如在项目组件库目录下创建GD25QXX 文件夹,并将它们放入其中。在这里插入图片描述
将组件库文件添加到keil
在这里插入图片描述
在这里插入图片描述
打开组件库文件,发现这个spi句柄是hspi2,与我们原理图一致,所以不用修改
在这里插入图片描述
跳转到头文件里,发现组件库是用软件模拟的片选信号
在这里插入图片描述

二,移植模板

#include "gd25qxx.h"
#include   // For printf, etc.
#include  // For strlen, strcmp// 假设SPI2和相关GPIO已通过CubeMX初始化
// extern SPI_HandleTypeDef hspi2; (确保在main.c或对应位置有定义)void test_spi_flash(void) {uint32_t flash_id;uint8_t write_buffer[SPI_FLASH_PAGE_SIZE];uint8_t read_buffer[SPI_FLASH_PAGE_SIZE];uint32_t test_addr = 0x000000; // 测试地址,选择一个扇区的起始printf("SPI FLASH Test Start\r\n");// 1. 初始化SPI Flash驱动 (主要是CS引脚状态)spi_flash_init();printf("SPI Flash Initialized.\r\n");// 2. 读取Flash IDflash_id = spi_flash_read_id();printf("Flash ID: 0x%lX\r\n", flash_id);// 你可以根据你的芯片手册检查ID是否正确,例如 GD25Q64的ID可能是 0xC84017// 3. 擦除一个扇区 (大小通常为4KB)// 注意:擦除操作耗时较长printf("Erasing sector at address 0x%lX...\r\n", test_addr);spi_flash_sector_erase(test_addr);printf("Sector erased.\r\n");// (可选) 校验擦除:读取一页数据,检查是否全为0xFFspi_flash_buffer_read(read_buffer, test_addr, SPI_FLASH_PAGE_SIZE);int erased_check_ok = 1;for (int i = 0; i < SPI_FLASH_PAGE_SIZE; i++) {if (read_buffer[i] != 0xFF) {erased_check_ok = 0;break;}}if (erased_check_ok) {printf("Erase check PASSED. Sector is all 0xFF.\r\n");} else {printf("Erase check FAILED.\r\n");}// 4. 准备写入数据 (写入一页)const char* message = "Hello from STM32 to SPI FLASH! Microunion Studio Test - 12345.";uint16_t data_len = strlen(message);if (data_len >= SPI_FLASH_PAGE_SIZE) {data_len = SPI_FLASH_PAGE_SIZE -1; // 确保不超过页大小}memset(write_buffer, 0, SPI_FLASH_PAGE_SIZE);memcpy(write_buffer, message, data_len);write_buffer[data_len] = '\0'; // 确保字符串结束printf("Writing data to address 0x%lX: \"%s\"\r\n", test_addr, write_buffer);// 使用 spi_flash_buffer_write (可以处理跨页,但这里只写一页内)// 或者直接用 spi_flash_page_write 如果确定在一页内spi_flash_buffer_write(write_buffer, test_addr, SPI_FLASH_PAGE_SIZE); // 写入整页数据,包括填充的0// spi_flash_page_write(write_buffer, test_addr, data_len + 1); // 只写入有效数据printf("Data written.\r\n");// 5. 读取写入的数据printf("Reading data from address 0x%lX...\r\n", test_addr);memset(read_buffer, 0, SPI_FLASH_PAGE_SIZE);spi_flash_buffer_read(read_buffer, test_addr, SPI_FLASH_PAGE_SIZE);printf("Data read: \"%s\"\r\n", read_buffer);// 6. 校验数据if (memcmp(write_buffer, read_buffer, SPI_FLASH_PAGE_SIZE) == 0) {printf("Data VERIFIED! Write and Read successful.\r\n");} else {printf("Data VERIFICATION FAILED!\r\n");}printf("SPI FLASH Test End\r\n");
}// 在你的main函数中合适的位置调用 test_spi_flash()
// int main(void) {
//   ...
//   MX_SPI2_Init(); // CubeMX生成的SPI初始化
//   MX_GPIO_Init(); // CubeMX生成的GPIO初始化 (包括CS引脚)
//   test_spi_flash();
//   while(1) {}
// }

上述代码仅为基本功能演示。实际应用中,你需要根据具体需求进行错误处理、地址管理、数据结构设计等。 SPI_FLASH_PAGE_SIZE (256字节) 是一个重要参数。

相关文章:

  • html文件cdn一键下载并替换
  • 计算机图形学中MVP变换的理论推导
  • R for Data Science(3)
  • windows环境下c语言链接sql数据库
  • Spring 框架线程安全的五大保障策略解析
  • 山东大学计算机图形学期末复习11——CG13上
  • NAT(网络地址转换)逻辑图解+实验详解
  • symfonos: 2靶场
  • C++(21):fstream的读取和写入
  • StarRocks Community Monthly Newsletter (Apr)
  • 系统性能不达标,如何提升用户体验?
  • 嵌入式学习的第二十二天-数据结构-栈+队列
  • NC016NC017美光固态芯片NC101NC102
  • LLMs:《POE报告:2025年春季人工智能模型使用趋势》解读
  • 服务器防文件上传手写waf
  • 【每日一题丨2025年5.12~5.18】排序相关题
  • LeeCode 101.对称二叉树
  • 互联网大厂Java面试场景:从缓存到容器化的技术问答
  • H2数据库源码学习+debug, 数据库 sql、数据库引擎、数据库存储从此不再神秘
  • 代码随想录算法训练营第六十六天| 图论11—卡码网97. 小明逛公园,127. 骑士的攻击
  • 优质文化资源下基层,上海各区优秀群文团队“文化走亲”
  • 证监会副主席李明:支持符合条件的外资机构申请新业务、设立新产品
  • 上海迪士尼蜘蛛侠主题园区正式动工,毗邻“疯狂动物城”
  • 央媒聚焦文明交流互鉴中的“上博现象” :跨越山海,抒写自信
  • 聘期三年已至:37岁香港青年叶家麟卸任三亚市旅游发展局局长
  • 肖钢:一季度证券业金融科技投资强度在金融各子行业中居首