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

SPI Flash开发全解(基于GD25Qxx)

SPI Flash开发全解(基于GD25Qxx)

  • 一,一些前置基础
    • 1,通信流程:
    • 2,通信速度
    • 3,应用场景
  • 二,开发步骤1--硬件连接
  • 三,开发步骤2--STM32CubeMX配置:启动SPI
    • 1,Mode
    • 2,Configuration
      • Basic Parameters(基本参数)
      • Clock Parameters(时钟参数)
      • Advanced Parameters(高级参数)
  • 四,开发步骤3--底层驱动代码设计
    • 底层SPI发送/接收函数
    • CS引脚控制函数
    • 辅助函数
    • 操作函数
  • 五,开发步骤4-驱动测试
  • 5,向flash写入字符串

上一节我们学习了以ssd1306为例开发iic的流程,本节我们以GD25Q为例学习spi的开发流程

一,一些前置基础

1,通信流程:

spi支持一个主机与多个从机交流,如何实现呢?
我知道iic通信是通过从机地址去区分不同的从机,而spi则是通过一根片选信号线:
spi有4根线,时钟线,数据线(MOSI:主机输出,从机输入和MISO:从机输出,主机输入)有两根(因为为了实现全双工),还有一根片选信号线CS(Chip Select),也叫 SS(Slave Select)
当主机要和某个从机通信时,将对应的 CS 拉低(激活);不通信时,拉高(失能),这就代替了iic通信中的地址机制

在spi通信中,所有线上的电平都由主机控制,CS线默认为高电平,有时需要外接上拉电阻在这里插入图片描述

2,通信速度

一般可达几十MHz(视MCU和外设支持)

3,应用场景

高速 ADC/DAC、SD 卡、WIFI 模块
大容量显示屏(TFT LCD)
需要高吞吐或实时性的场合

二,开发步骤1–硬件连接

将MCU的SPI引脚 (SCK, MOSI, MISO) 连接到从设备的对应引脚
将MCU的一个通用GPIO引脚连接到从设备的CS引脚

三,开发步骤2–STM32CubeMX配置:启动SPI

1,Mode

在这里插入图片描述
选择Full-Duplex Master,表示STM32作为主机
如果选择Full-Duplex Slave则表示STM32作为从机
其他选项我们遇到 再说

在这里插入图片描述

这里的NSS表示片选信号,前面的SS和CS也可以表示片选信号

我们可以看到这里有三个选项:
Disable
Hardware NSS Input Singnal
Hardware NSS Output Singnal
如果我们选择失能,那就要为NSS选择一个GPIO引脚,实现软件NSS,我选择的是PB12,配置为输出模式:
在这里插入图片描述
并且在驱动代码里写以下的宏:
在这里插入图片描述

如果选择Hardware NSS Input Singnal:STM32 作为 SPI 从设备时使用。NSS 引脚配置为硬件输入,当外部主机拉低此引脚时,STM32 SPI 从设备被选中。

Hardware NSS Output Singnal
STM32 作为 SPI 主机时使用。SPI 外设硬件在数据传输开始前自动拉低 NSS 引脚,传输结束后自动拉高。

2,Configuration

在这里插入图片描述

Basic Parameters(基本参数)

Frame Format(帧格式):通常选择Motorola

Data Size(数据大小):即一个数据帧大小,可以选择8位和16位

First Bit(数据顺序):
MSB First:表示数据是从最高位(Most Significant Bit)开始传输的。另一个常见选项是 LSB First,表示从最低位(Least Significant Bit)开始传输。

Clock Parameters(时钟参数)

Prescaler(分频器):系统时钟流向SPI外设之后,SPI自己可以使用分配器 进行时钟分频,这里的时钟频率和下面的波特率大小保持一致

Baud Rate(波特率):
22.5 MBits/s:表示 SPI 总线的传输速度为 22.5 Mbps,即每秒钟传输 22.5 百万比特。波特率的选择取决于主从设备的能力以及传输距离。

Clock Polarity (CPOL)(时钟极性):
CPOL = Low: SCK 时钟在空闲状态为低电平。
CPOL = High: SCK 时钟在空闲状态为高电平。

Clock Phase (CPHA)(时钟相位):
CPHA = 1 Edge: 数据在 SCK 的第一个时钟边沿被采样。
CPHA = 2 Edge: 数据在 SCK 的第二个时钟边沿被采样。
SPI 模式总结 (CPOL/CPHA)

CPOL 和 CPHA 的组合定义了四种 SPI 工作模式:

模式CPOLCPHA描述 (空闲电平, 采样边沿)
0Low (0)1 Edge (0)空闲低,SCK 第一个边沿采样
1Low (0)2 Edge (1)空闲低,SCK 第二个边沿采样
2High (1)1 Edge (0)空闲高,SCK 第一个边沿采样
3High (1)2 Edge (1)空闲高,SCK 第二个边沿采样

我使用的GD25芯片只支持模式0和3

Advanced Parameters(高级参数)

CRC Calculation(CRC 校验):CRC 校验只能检测到数据错误,不能纠正错误

NSS Signal Type(NSS 信号类型):这个其实就是照应了前面的NSS类型

四,开发步骤3–底层驱动代码设计

底层SPI发送/接收函数

这类函数是对HAL库SPI发送和接收函数HAL_SPI_TransmitReceive的进一步封装,和前面封装iic发送函数一样,目的是减少函数形参,方便使用

uint8_t spi_flash_send_byte(uint8_t byte)
{uint8_t rx_data;HAL_SPI_TransmitReceive(&hspi2, &byte, &rx_data, 1, 1000);return rx_data;
}

CS引脚控制函数

这类函数帮助STM32主机通过软件的方式拉低片选信号线,实现与特定从机的通信

#define SPI_FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET)
#define SPI_FLASH_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET)

辅助函数

这些函数主要辅助上面两类函数,构成下面的高级API(操作 函数)
1,写使能函数

void spi_flash_write_enable(void)
{SPI_FLASH_CS_LOW();spi_flash_send_byte(WREN);SPI_FLASH_CS_HIGH();
}

2,等待写完成函数

void spi_flash_wait_for_write_end(void)
{uint8_t flash_status = 0;SPI_FLASH_CS_LOW();spi_flash_send_byte(RDSR);do{flash_status = spi_flash_send_byte(DUMMY_BYTE);} while ((flash_status & WIP_FLAG) == 0x01);SPI_FLASH_CS_HIGH();
}

操作函数

这类函数通过CS引脚控制函数选择从机设备,SPI发送/接收函数向 从机发送命令(来自从机数据手册),实现以下操作:

1,初始化即片选信号线拉高:

void spi_flash_init(void)
{// Ensure CS pin is high (deselected) initially.// The GPIO for CS (PB12 in your macros) should be configured as output push-pull in MX_GPIO_Init.SPI_FLASH_CS_HIGH();// Optional: Add a small delay if needed after power-up or SPI init, before first command// HAL_Delay(1);// Optional: You could read the Flash ID here to verify communication// uint32_t id = spi_flash_read_id();// (Add code to check ID or print it for debugging)
}

2,清除指定扇区函数

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

void spi_flash_sector_erase(uint32_t sector_addr)
{spi_flash_write_enable();SPI_FLASH_CS_LOW();spi_flash_send_byte(SE);spi_flash_send_byte((sector_addr & 0xFF0000) >> 16);spi_flash_send_byte((sector_addr & 0xFF00) >> 8);spi_flash_send_byte(sector_addr & 0xFF);SPI_FLASH_CS_HIGH();spi_flash_wait_for_write_end();
}

形参:sector_addr - 要擦除扇区内的任意地址。驱动程序通常会根据此地址计算出扇区的起始地址
函数逻辑:
调用spi_flash_write_enable使能写操作
发送扇区擦除(SE 0x20)指令
后跟24位地址
调用spi_flash_wait_for_write_end等待操作完成

3,清除全片操作

void spi_flash_bulk_erase(void)
{spi_flash_write_enable();SPI_FLASH_CS_LOW();spi_flash_send_byte(BE);SPI_FLASH_CS_HIGH();spi_flash_wait_for_write_end();
}

函数逻辑:
调用 spi_flash_write_enable()。
发送全片擦除(BE/CE 0xC7)指令
调用 spi_flash_wait_for_write_end() 等待擦除完成。

4,指定页写入函数

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

这个函数的作用是将pbuffer中的数据写入到write_addr地址处
注意:pbuffer的大小不能超过num_byte_to_write个字节,并且不能超过本页范围
为什么不能超过本页范围?
因为Flash芯片的页编程机制有一个硬性限制:单次写入操作不能跨越页边界,即页回卷现象
什么页回卷现象呢?举个例子
1,页内写入:如果从地址0x0F0开始写入16字节,数据会正确写入0x0F0至0x0FF。

2,跨页写入问题:如果从地址0x0F0开始写入30字节:
前16字节(0x0F0到0x0FF)会正确写入
后14字节不会写入下一页(0x100起始位置)
而是会回卷到当前页的开始位置,写入0x000到0x00D
这会覆盖页开头的数据!
这种现象称为"页回卷"(Page Rollover)或"地址回环"(Address Wrapping)。

void spi_flash_page_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)
{spi_flash_write_enable();SPI_FLASH_CS_LOW();spi_flash_send_byte(WRITE);spi_flash_send_byte((write_addr & 0xFF0000) >> 16);spi_flash_send_byte((write_addr & 0xFF00) >> 8);spi_flash_send_byte(write_addr & 0xFF);while (num_byte_to_write--){spi_flash_send_byte(*pbuffer);pbuffer++;}SPI_FLASH_CS_HIGH();spi_flash_wait_for_write_end();
}

函数逻辑:
调用 spi_flash_write_enable()。
发送页编程(WRITE 0x02)指令,后跟24位地址。
发送 num_byte_to_write 个字节的数据。
调用 spi_flash_wait_for_write_end() 等待写入完成。

5,跨页写入

此函数是更高级的写入函数。它会判断起始地址和写入长度,如果发生跨页,则会将数据拆分成多个部分,分别调用 spi_flash_page_write() 进行写入

void spi_flash_buffer_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write)
{uint8_t num_of_page = 0, num_of_single = 0, addr = 0, count = 0, temp = 0;addr = write_addr % SPI_FLASH_PAGE_SIZE;count = SPI_FLASH_PAGE_SIZE - addr;num_of_page = num_byte_to_write / SPI_FLASH_PAGE_SIZE;num_of_single = num_byte_to_write % SPI_FLASH_PAGE_SIZE;if (0 == addr){if (0 == num_of_page){spi_flash_page_write(pbuffer, write_addr, num_byte_to_write);}else{while (num_of_page--){spi_flash_page_write(pbuffer, write_addr, SPI_FLASH_PAGE_SIZE);write_addr += SPI_FLASH_PAGE_SIZE;pbuffer += SPI_FLASH_PAGE_SIZE;}spi_flash_page_write(pbuffer, write_addr, num_of_single);}}else{if (0 == num_of_page){if (num_of_single > count){temp = num_of_single - count;spi_flash_page_write(pbuffer, write_addr, count);write_addr += count;pbuffer += count;spi_flash_page_write(pbuffer, write_addr, temp);}else{spi_flash_page_write(pbuffer, write_addr, num_byte_to_write);}}else{num_byte_to_write -= count;num_of_page = num_byte_to_write / SPI_FLASH_PAGE_SIZE;num_of_single = num_byte_to_write % SPI_FLASH_PAGE_SIZE;spi_flash_page_write(pbuffer, write_addr, count);write_addr += count;pbuffer += count;while (num_of_page--){spi_flash_page_write(pbuffer, write_addr, SPI_FLASH_PAGE_SIZE);write_addr += SPI_FLASH_PAGE_SIZE;pbuffer += SPI_FLASH_PAGE_SIZE;}if (0 != num_of_single){spi_flash_page_write(pbuffer, write_addr, num_of_single);}}}
}

函数形参:
pbuffer: 指向待写入数据缓冲区的指针。
write_addr: 起始写入地址。
num_byte_to_write: 要写入的总字节数。

6,读取数据函数
从FLASH的指定地址读取一块数据到MCU的缓冲区。

void spi_flash_buffer_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
{SPI_FLASH_CS_LOW();spi_flash_send_byte(READ);spi_flash_send_byte((read_addr & 0xFF0000) >> 16);spi_flash_send_byte((read_addr & 0xFF00) >> 8);spi_flash_send_byte(read_addr & 0xFF);while (num_byte_to_read--){*pbuffer = spi_flash_send_byte(DUMMY_BYTE);pbuffer++;}SPI_FLASH_CS_HIGH();
}

函数形参:
pbuffer: 指向用于存储读取数据的MCU缓冲区的指针。
read_addr: FLASH内部的24位起始读取地址。
num_byte_to_read: 要读取的字节数。

函数逻辑:
拉低CS片选。
发送读数据(READ 0x03)指令,后跟24位地址。
循环读取 num_byte_to_read 个字节:为每个要读取的字节发送一个DUMMY_BYTE (0xA5),并将接收到的字节存入 pbuffer。
拉高CS片选。

7,读取id函数

uint32_t spi_flash_read_id(void)
{uint32_t temp = 0, temp0 = 0, temp1 = 0, temp2 = 0;SPI_FLASH_CS_LOW();spi_flash_send_byte(RDID);temp0 = spi_flash_send_byte(DUMMY_BYTE);temp1 = spi_flash_send_byte(DUMMY_BYTE);temp2 = spi_flash_send_byte(DUMMY_BYTE);SPI_FLASH_CS_HIGH();temp = (temp0 << 16) | (temp1 << 8) | temp2;return temp;
}

五,开发步骤4-驱动测试

\r\n是两个特殊的转义字符:
\r 是回车符(Carriage Return) - 将光标移到行首
\n 是换行符(Line Feed) - 将光标移到下一行
组合使用\r\n可确保终端显示换到新行并回到行首,这是Windows风格的换行。
常见的类似转义字符有:
\t - 水平制表符(Tab键)
\b - 退格符
\ - 反斜杠本身
’ - 单引号
" - 双引号
\0 - 空字符(字符串结束)
\a - 警报声
\f - 换页符
\v - 垂直制表符
\xhh - 十六进制字符(如\x0A)
1,开始spi通信

    my_printf(&huart1, "SPI FLASH Test Start\r\n");

2,spt初始化

    spi_flash_init();my_printf(&huart1, "SPI Flash Initialized.\r\n");

3,读取spi设备id,不是地址

读取ID主要作为测试的第一步,确认SPI通信正常工作

    uint32_t flash_id;flash_id = spi_flash_read_id();my_printf(&huart1, "Flash ID: 0x%lX\r\n", flash_id);

flash_id不是地址,而是芯片的识别码,包含:
制造商ID
内存类型
容量信息

格式说明符
整数类型:
%d - 十进制整数
%u - 无符号整数
%x - 小写十六进制
%o - 八进制
修饰符:
%ld - 长整数
%hd - 短整数
%lld - 长长整数
其他类型:
%f - 浮点数
%s - 字符串
%c - 字符
%p - 指针
格式控制:
%5d - 宽度至少5字符
%.2f - 保留2位小数
%08x - 用0填充到8位宽度

修饰符可以与整数类型组合
有符号整数:
%hd - short int (2字节)
%d - int (4字节)
%ld - long int (4字节/32位系统, 8字节/64位系统)
%lld - long long int (8字节)
无符号整数:
%hu - unsigned short (2字节)
%u - unsigned int (4字节)
%lu - unsigned long (4字节/32位系统, 8字节/64位系统)
%llu - unsigned long long (8字节)
十六进制:
%hx/%hX - unsigned short (2字节)
%x/%X - unsigned int (4字节)
%lx/%lX - unsigned long (4字节/32位系统, 8字节/64位系统)
%llx/%llX - unsigned long long (8字节)
八进制:
%ho - unsigned short (2字节)
%o - unsigned int (4字节)
%lo - unsigned long (4字节/32位系统, 8字节/64位系统)
%llo - unsigned long long (8字节)

4,擦除扇区数据,再回读一页
Flash存储结构中:
1页(Page) = 256字节(0x100)
1扇区(Sector) = 4KB(0x1000) = 16页
1块(Block) = 64KB = 16扇区
写入以页为单位
擦除以扇区为最小单位
读取可以任意字节寻址

测试代码是从test_addr开始擦除整个扇区,然后再从test_addr开始读取整个页,最后通过erased_check_ok 检查页是否被删除

	#define SPI_FLASH_PAGE_SIZE 0x100uint8_t read_buffer[SPI_FLASH_PAGE_SIZE];uint32_t test_addr = 0x000000; // Test address, choose a sector startmy_printf(&huart1, "Erasing sector at address 0x%lX...\r\n", test_addr);spi_flash_sector_erase(test_addr);my_printf(&huart1, "Sector erased.\r\n");spi_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){my_printf(&huart1, "Erase check PASSED. Sector is all 0xFF.\r\n");}else{my_printf(&huart1, "Erase check FAILED.\r\n");}

这里的地址test_addr 是uint32_t ,是32位(4字节)无符号整数,可表示0x00000000到0xFFFFFFFF

5,向flash写入字符串

字符串常量定义:
const char *message - 声明一个指向常量字符数组(字符串)的指针,指向测试用的字符串内容
strlen(message) 计算字符串的实际长度(不包括结尾的’\0’)

这里将最后一位加上\0,方便后面处理字符串
write_buffer[data_len] = ‘\0’;
索引255位置留给结束符’\0’

memset(write_buffer, 0, SPI_FLASH_PAGE_SIZE);
memcpy(write_buffer, message, data_len);
则是将write_buffer清空,再将上面的字符串写入到write_buffer

最后将 spi_flash_buffer_write(write_buffer, test_addr, SPI_FLASH_PAGE_SIZE);写入到flash

    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; // Ensure not exceeding page size}memset(write_buffer, 0, SPI_FLASH_PAGE_SIZE);memcpy(write_buffer, message, data_len);write_buffer[data_len] = '\0'; // Ensure string terminationmy_printf(&huart1, "Writing data to address 0x%lX: \"%s\"\r\n", test_addr, write_buffer);// Use spi_flash_buffer_write (can handle cross-page, but here we're writing within one page)// Or use spi_flash_page_write directly if certain it's within one pagespi_flash_buffer_write(write_buffer, test_addr, SPI_FLASH_PAGE_SIZE); // Write entire page with padding// spi_flash_page_write(write_buffer, test_addr, data_len + 1); // Only write valid datamy_printf(&huart1, "Data written.\r\n");

6,读取字符串
使用 memset(read_buffer, 0, SPI_FLASH_PAGE_SIZE);将read_buffer清空,再使用spi_flash_buffer_read将字符串写入到read_buffer

    my_printf(&huart1, "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);my_printf(&huart1, "Data read: \"%s\"\r\n", read_buffer);

7,比较read_buffer和write_buffer

memcmp(write_buffer, read_buffer, SPI_FLASH_PAGE_SIZE)

相关文章:

  • 下载https协议的网络图片,并转为Base64
  • 基于vue框架的多媒体教室管理系统72d6w(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • Stream API:高效处理数据流!
  • 华为WLAN概述知识点及案例试题
  • 基于单片机的宠物屋智能系统设计与实现(论文+源码)
  • VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
  • 【Docker 03】Docker Registry - 镜像仓库
  • 基于uni-app for HarmonyOS5 的跨平台组件库开发指南,以及组件示例
  • 安宝特科技丨Pixee Medical产品获FDA认证 AR技术赋能骨科手术智能化
  • 热烈祝贺埃文科技正式加入可信数据空间发展联盟
  • 无需布线的革命:电力载波技术赋能楼宇自控系统-亚川科技
  • PydanticAI 作为 MCP 客户端示例
  • 报文口令重写功能分析(以某巢为例)
  • AI知识库调用全攻略:四种实战方法与技术实现
  • 读书笔记:83页华为数据之道提炼整理【附全文阅读】
  • 浅谈 ST 表(Sparse Table,稀疏表)
  • 规则与人性的天平——由高考迟到事件引发的思考
  • 从零手写Java版本的LSM Tree (六):WAL 写前日志
  • 从零手写Java版本的LSM Tree (七):压缩策略
  • 第二章:文本处理与表示的基础 —— 解码语言的奥秘
  • 贺州住房和城乡建设部网站/重庆网站制作系统
  • 海网站建设生产厂家哪家好/长沙百度首页排名
  • java动态网站开发/石狮seo
  • 重庆网站建设搜外/黄页网站推广app咋做广告
  • 泉州专业建站品牌/衡水seo营销
  • 山东咕果做网站怎么样/山东最新消息今天