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 工作模式:
模式 | CPOL | CPHA | 描述 (空闲电平, 采样边沿) |
---|---|---|---|
0 | Low (0) | 1 Edge (0) | 空闲低,SCK 第一个边沿采样 |
1 | Low (0) | 2 Edge (1) | 空闲低,SCK 第二个边沿采样 |
2 | High (1) | 1 Edge (0) | 空闲高,SCK 第一个边沿采样 |
3 | High (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)