文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 本文目标](#1.2 本文目标)
- [1.3 技术栈](#1.3 技术栈)
- 二、环境准备
-
- [2.1 硬件准备](#2.1 硬件准备)
- [2.2 W25Q128存储结构](#2.2 W25Q128存储结构)
- 三、SPI通信协议
-
- [3.1 SPI基本原理](#3.1 SPI基本原理)
- [3.2 SPI时序图](#3.2 SPI时序图)
- 四、W25Q128指令集
-
- [4.1 常用指令](#4.1 常用指令)
- [4.2 状态寄存器](#4.2 状态寄存器)
- 五、驱动程序实现
-
- [5.1 SPI外设初始化](#5.1 SPI外设初始化)
- [5.2 主程序](#5.2 主程序)
- 六、测试与验证
-
- [6.1 硬件测试](#6.1 硬件测试)
- [6.2 软件测试](#6.2 软件测试)
- 七、故障排查
-
- [7.1 常见问题](#7.1 常见问题)
- [7.2 调试技巧](#7.2 调试技巧)
- 八、总结
-
- [8.1 核心知识点](#8.1 核心知识点)
- [8.2 扩展应用](#8.2 扩展应用)
一、前言
1.1 技术背景
在嵌入式系统中,数据存储是一个核心需求。STM32F103内部Flash容量有限(64KB或128KB),且擦写次数受限(约1万次),不适合频繁写入的场景。外部SPI Flash存储器成为扩展存储容量的理想选择。
W25Q系列是Winbond(华邦)公司生产的SPI接口NOR Flash,具有以下特点:
- 大容量:从512Kb到512Mb多种规格
- 高速度:标准SPI模式80MHz,Dual/Quad SPI模式可达104MHz
- 低功耗:待机电流仅1μA
- 长寿命:擦写次数10万次,数据保持20年
- 小封装:SOP8、WSON8、USON8等多种封装
1.2 本文目标
通过本教程,你将学会:
- SPI通信协议原理和时序
- STM32F103的SPI外设配置
- W25Q128 Flash的读写操作
- Flash存储器的扇区擦除和页编程
- 文件系统和数据管理
适合读者:
- 需要扩展存储容量的嵌入式开发者
- 学习SPI通信协议的初学者
- 需要实现数据记录功能的开发者
1.3 技术栈
| 组件 | 型号/版本 | 说明 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | ARM Cortex-M3 |
| Flash芯片 | W25Q128JV | 128Mbit(16MB) SPI Flash |
| 开发环境 | Keil MDK 5 | 嵌入式开发IDE |
| 通信协议 | SPI | 串行外设接口 |
二、环境准备
2.1 硬件准备
核心硬件清单:
- STM32F103C8T6最小系统板 × 1
- W25Q128JV Flash模块 × 1
- USB转TTL模块 × 1
- 杜邦线若干
硬件连接图:
STM32F103C8T6 W25Q128JV
┌─────────────┐ ┌─────────────┐
│ │ │ │
│ PA5(SCK) ──┼────────┼────> CLK │
│ PA6(MISO) <┼────────┼──── DO │
│ PA7(MOSI) ─┼────────┼────> DI │
│ PA4(CS) ──┼────────┼────> /CS │
│ 3.3V ──┼────────┼──── VCC │
│ GND ──┼────────┼──── GND │
│ │ │ │
└─────────────┘ └─────────────┘
W25Q128引脚定义:
| 引脚 | 名称 | 类型 | 功能 |
|---|---|---|---|
| 1 | /CS | 输入 | 片选,低电平有效 |
| 2 | DO | 输出 | 数据输出(MISO) |
| 3 | /WP | 输入 | 写保护,低电平有效 |
| 4 | GND | 电源 | 地 |
| 5 | DI | 输入 | 数据输入(MOSI) |
| 6 | CLK | 输入 | 串行时钟 |
| 7 | /HOLD | 输入 | 暂停,低电平有效 |
| 8 | VCC | 电源 | 2.7-3.6V |
注意: /WP和/HOLD引脚如果不使用,应接VCC(高电平)。
2.2 W25Q128存储结构
容量划分:
总容量:128Mbit = 16MB = 16,777,216字节
┌─────────────────────────────────────────────────────────────┐
│ W25Q128 存储结构 │
├─────────────────────────────────────────────────────────────┤
│ 块(Block) │ 64个块 × 64KB = 4096KB (4MB) │
│ 扇区(Sector) │ 每个块16个扇区,共1024个扇区 × 4KB │
│ 页(Page) │ 每个扇区16页,共16384页 × 256字节 │
│ 字节(Byte) │ 每页256字节,共4,194,304字节 │
└─────────────────────────────────────────────────────────────┘
地址范围:0x000000 ~ 0xFFFFFF (24位地址)
重要特性:
- 最小擦除单位:扇区(4KB)
- 最小编程单位:页(256字节)
- 页内地址连续递增可跨页写入
- 擦除后所有位变为1(0xFF)
- 编程只能将1变为0
三、SPI通信协议
3.1 SPI基本原理
SPI(Serial Peripheral Interface)是一种同步串行通信协议,特点:
- 全双工通信:同时收发数据
- 主从架构:一主多从
- 4线接口:SCK、MOSI、MISO、CS
- 时钟极性和相位可配置
SPI模式:
| 模式 | CPOL | CPHA | 时钟极性 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 空闲低 | 上升沿采样 |
| 1 | 0 | 1 | 空闲低 | 下降沿采样 |
| 2 | 1 | 0 | 空闲高 | 下降沿采样 |
| 3 | 1 | 1 | 空闲高 | 上升沿采样 |
W25Q128支持模式0和模式3,我们使用模式0。
3.2 SPI时序图
标准SPI模式0时序:
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
SCK ───┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
采样 采样 采样 采样 采样 采样 采样 采样
(上升沿)
MOSI ───┐ ┌─────┐ ┌─────────┐
└─────┘ └───────────┘ └──────────────
D7 D6 D5 D4 D3...
MISO ─────────┐ ┌─────┐ ┌─────────┐
└─────┘ └───────────┘ └────────
D7 D6 D5 D4 D3...
CS ─┐ ┌──
└──────────────────────────────────────────────────┘
选中 释放
四、W25Q128指令集
4.1 常用指令
| 指令 | 代码 | 功能 | 地址字节 | 数据字节 |
|---|---|---|---|---|
| WREN | 0x06 | 写使能 | 0 | 0 |
| WRDI | 0x04 | 写禁止 | 0 | 0 |
| RDID | 0x9F | 读ID | 0 | 3 |
| RDSR | 0x05 | 读状态寄存器 | 0 | 1-∞ |
| READ | 0x03 | 读数据 | 3 | 1-∞ |
| PP | 0x02 | 页编程 | 3 | 1-256 |
| SE | 0x20 | 扇区擦除(4KB) | 3 | 0 |
| BE32 | 0x52 | 块擦除(32KB) | 3 | 0 |
| BE64 | 0xD8 | 块擦除(64KB) | 3 | 0 |
| CE | 0xC7 | 全片擦除 | 0 | 0 |
4.2 状态寄存器
状态寄存器1 (S1):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ SRP│ SEC│ TB │ BP2│ BP1│ BP0│ WEL│ BUSY│
└────┴────┴────┴────┴────┴────┴────┴────┘
7 6 5 4 3 2 1 0
BUSY (Bit0): 忙标志,1=正在擦除或编程
WEL (Bit1): 写使能锁存,1=写使能
BP0-BP2 (Bit2-4): 块保护位
TB (Bit5): 顶部/底部保护
SEC (Bit6): 扇区保护
SRP (Bit7): 状态寄存器保护
五、驱动程序实现
5.1 SPI外设初始化
📄 创建文件:
spi_flash.c
c
/**
* @file spi_flash.c
* @brief W25Q128 SPI Flash驱动程序
*
* 功能:
* - SPI1初始化(模式0,18MHz)
* - W25Q128基本操作(读ID、读状态、写使能)
* - 数据读写(页编程、扇区擦除)
* - 高级功能(全片擦除、写保护)
*/
#include "spi_flash.h"
#include "stm32f10x.h"
#include <string.h>
/* 片选引脚定义 */
#define FLASH_CS_PORT GPIOA
#define FLASH_CS_PIN GPIO_Pin_4
#define FLASH_CS_LOW() GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN)
#define FLASH_CS_HIGH() GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN)
/* 超时时间定义 */
#define FLASH_TIMEOUT_MS 1000
#define SECTOR_ERASE_TIMEOUT_MS 400
#define CHIP_ERASE_TIMEOUT_MS 10000
/**
* @brief 初始化SPI1和GPIO
*
* 配置参数:
* - SPI模式:全双工主模式
* - 时钟极性:空闲低电平(CPOL=0)
* - 时钟相位:第一个边沿采样(CPHA=0)
* - 波特率:18MHz(72MHz/4)
* - 数据格式:8位,MSB先
*/
void SPI_Flash_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
/* 1. 使能时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
/* 2. 配置GPIO */
// PA5 - SCK (复用推挽)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA6 - MISO (浮空输入)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA7 - MOSI (复用推挽)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA4 - CS (推挽输出)
GPIO_InitStruct.GPIO_Pin = FLASH_CS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(FLASH_CS_PORT, &GPIO_InitStruct);
// 初始状态:CS高电平(未选中)
FLASH_CS_HIGH();
/* 3. 配置SPI1 */
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 时钟极性:空闲低
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位:第一个边沿
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件NSS
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 18MHz
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStruct.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStruct);
/* 4. 使能SPI */
SPI_Cmd(SPI1, ENABLE);
}
/**
* @brief SPI发送/接收一个字节
* @param byte 要发送的字节
* @return 接收到的字节
*/
static uint8_t SPI_SendByte(uint8_t byte)
{
// 等待发送缓冲区空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPI1, byte);
// 等待接收缓冲区非空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 返回接收到的数据
return SPI_I2S_ReceiveData(SPI1);
}
/**
* @brief 发送指令(无地址无数据)
* @param cmd 指令代码
*/
static void Flash_SendCmd(uint8_t cmd)
{
FLASH_CS_LOW();
SPI_SendByte(cmd);
FLASH_CS_HIGH();
}
/**
* @brief 读取Flash ID
* @return 设备ID(0xEF4018表示W25Q128)
*/
uint32_t Flash_ReadID(void)
{
uint32_t id = 0;
FLASH_CS_LOW();
// 发送读ID指令
SPI_SendByte(FLASH_CMD_RDID);
// 读取3字节ID
id |= (uint32_t)SPI_SendByte(0xFF) << 16; // Manufacturer ID
id |= (uint32_t)SPI_SendByte(0xFF) << 8; // Memory Type
id |= (uint32_t)SPI_SendByte(0xFF); // Capacity
FLASH_CS_HIGH();
return id;
}
/**
* @brief 读取状态寄存器
* @return 状态寄存器值
*/
uint8_t Flash_ReadStatus(void)
{
uint8_t status;
FLASH_CS_LOW();
SPI_SendByte(FLASH_CMD_RDSR);
status = SPI_SendByte(0xFF);
FLASH_CS_HIGH();
return status;
}
/**
* @brief 检查是否忙
* @return 1=忙,0=空闲
*/
uint8_t Flash_IsBusy(void)
{
return (Flash_ReadStatus() & FLASH_SR_BUSY) ? 1 : 0;
}
/**
* @brief 等待操作完成
* @param timeout_ms 超时时间(毫秒)
* @return 0=成功,1=超时
*/
uint8_t Flash_WaitReady(uint32_t timeout_ms)
{
uint32_t start_time = GetSysTick();
while (Flash_IsBusy()) {
if ((GetSysTick() - start_time) > timeout_ms) {
return 1; // 超时
}
}
return 0;
}
/**
* @brief 写使能
*/
void Flash_WriteEnable(void)
{
Flash_SendCmd(FLASH_CMD_WREN);
}
/**
* @brief 写禁止
*/
void Flash_WriteDisable(void)
{
Flash_SendCmd(FLASH_CMD_WRDI);
}
/**
* @brief 读取数据
* @param addr 起始地址(24位)
* @param buffer 数据缓冲区
* @param len 读取长度
*/
void Flash_ReadData(uint32_t addr, uint8_t *buffer, uint32_t len)
{
FLASH_CS_LOW();
// 发送读指令
SPI_SendByte(FLASH_CMD_READ);
// 发送24位地址
SPI_SendByte((addr >> 16) & 0xFF);
SPI_SendByte((addr >> 8) & 0xFF);
SPI_SendByte(addr & 0xFF);
// 读取数据
for (uint32_t i = 0; i < len; i++) {
buffer[i] = SPI_SendByte(0xFF);
}
FLASH_CS_HIGH();
}
/**
* @brief 页编程(256字节)
* @param addr 页起始地址
* @param data 数据指针
* @param len 数据长度(最大256)
* @return 0=成功,1=失败
*/
uint8_t Flash_PageWrite(uint32_t addr, const uint8_t *data, uint16_t len)
{
// 检查参数
if (len == 0 || len > FLASH_PAGE_SIZE) {
return 1;
}
// 检查地址是否页对齐
if ((addr & (FLASH_PAGE_SIZE - 1)) != 0) {
return 1;
}
// 等待空闲
if (Flash_WaitReady(FLASH_TIMEOUT_MS)) {
return 1;
}
// 写使能
Flash_WriteEnable();
FLASH_CS_LOW();
// 发送页编程指令
SPI_SendByte(FLASH_CMD_PP);
// 发送地址
SPI_SendByte((addr >> 16) & 0xFF);
SPI_SendByte((addr >> 8) & 0xFF);
SPI_SendByte(addr & 0xFF);
// 发送数据
for (uint16_t i = 0; i < len; i++) {
SPI_SendByte(data[i]);
}
FLASH_CS_HIGH();
// 等待编程完成
if (Flash_WaitReady(FLASH_TIMEOUT_MS)) {
return 1;
}
return 0;
}
/**
* @brief 扇区擦除(4KB)
* @param addr 扇区内的任意地址
* @return 0=成功,1=失败
*/
uint8_t Flash_SectorErase(uint32_t addr)
{
// 等待空闲
if (Flash_WaitReady(FLASH_TIMEOUT_MS)) {
return 1;
}
// 写使能
Flash_WriteEnable();
FLASH_CS_LOW();
// 发送扇区擦除指令
SPI_SendByte(FLASH_CMD_SE);
// 发送地址(扇区地址,低12位会被忽略)
SPI_SendByte((addr >> 16) & 0xFF);
SPI_SendByte((addr >> 8) & 0xFF);
SPI_SendByte(addr & 0xFF);
FLASH_CS_HIGH();
// 等待擦除完成(扇区擦除约45-400ms)
if (Flash_WaitReady(SECTOR_ERASE_TIMEOUT_MS)) {
return 1;
}
return 0;
}
/**
* @brief 块擦除(64KB)
* @param addr 块内的任意地址
* @return 0=成功,1=失败
*/
uint8_t Flash_BlockErase(uint32_t addr)
{
// 等待空闲
if (Flash_WaitReady(FLASH_TIMEOUT_MS)) {
return 1;
}
// 写使能
Flash_WriteEnable();
FLASH_CS_LOW();
// 发送块擦除指令
SPI_SendByte(FLASH_CMD_BE64);
// 发送地址(块地址,低16位会被忽略)
SPI_SendByte((addr >> 16) & 0xFF);
SPI_SendByte((addr >> 8) & 0xFF);
SPI_SendByte(addr & 0xFF);
FLASH_CS_HIGH();
// 等待擦除完成(块擦除约150-1000ms)
if (Flash_WaitReady(2000)) {
return 1;
}
return 0;
}
/**
* @brief 全片擦除
* @return 0=成功,1=失败
*/
uint8_t Flash_ChipErase(void)
{
// 等待空闲
if (Flash_WaitReady(FLASH_TIMEOUT_MS)) {
return 1;
}
// 写使能
Flash_WriteEnable();
// 发送全片擦除指令
Flash_SendCmd(FLASH_CMD_CE);
// 等待擦除完成(全片擦除约20-100秒)
if (Flash_WaitReady(CHIP_ERASE_TIMEOUT_MS)) {
return 1;
}
return 0;
}
/**
* @brief 无校验写入(自动处理页边界)
* @param addr 起始地址
* @param data 数据指针
* @param len 数据长度
* @return 0=成功,1=失败
*/
uint8_t Flash_WriteData(uint32_t addr, const uint8_t *data, uint32_t len)
{
uint32_t page_addr;
uint16_t page_offset;
uint16_t write_len;
uint32_t remaining = len;
uint32_t data_index = 0;
while (remaining > 0) {
// 计算当前页地址和偏移
page_addr = addr & ~(FLASH_PAGE_SIZE - 1);
page_offset = addr & (FLASH_PAGE_SIZE - 1);
// 计算本次可写入的长度
write_len = FLASH_PAGE_SIZE - page_offset;
if (write_len > remaining) {
write_len = remaining;
}
// 如果写入跨越页边界,只写到页尾
if (page_offset + write_len > FLASH_PAGE_SIZE) {
write_len = FLASH_PAGE_SIZE - page_offset;
}
// 页编程
if (Flash_PageWrite(page_addr, data + data_index, write_len) != 0) {
return 1;
}
addr += write_len;
data_index += write_len;
remaining -= write_len;
}
return 0;
}
/**
* @brief 带擦除的写入(自动擦除扇区)
* @param addr 起始地址
* @param data 数据指针
* @param len 数据长度
* @return 0=成功,1=失败
*/
uint8_t Flash_WriteDataWithErase(uint32_t addr, const uint8_t *data, uint32_t len)
{
uint32_t sector_addr;
uint32_t end_addr = addr + len;
uint8_t sector_buffer[FLASH_SECTOR_SIZE];
// 处理起始地址未对齐的情况
if ((addr & (FLASH_SECTOR_SIZE - 1)) != 0) {
sector_addr = addr & ~(FLASH_SECTOR_SIZE - 1);
uint32_t offset = addr - sector_addr;
uint32_t write_len = FLASH_SECTOR_SIZE - offset;
if (write_len > len) {
write_len = len;
}
// 读取原扇区数据
Flash_ReadData(sector_addr, sector_buffer, FLASH_SECTOR_SIZE);
// 修改数据
memcpy(sector_buffer + offset, data, write_len);
// 擦除并写入
if (Flash_SectorErase(sector_addr) != 0) {
return 1;
}
// 逐页写入
for (uint16_t i = 0; i < FLASH_SECTOR_SIZE; i += FLASH_PAGE_SIZE) {
if (Flash_PageWrite(sector_addr + i, sector_buffer + i, FLASH_PAGE_SIZE) != 0) {
return 1;
}
}
addr += write_len;
data += write_len;
len -= write_len;
}
// 处理完整的扇区
while (len >= FLASH_SECTOR_SIZE) {
sector_addr = addr;
// 擦除扇区
if (Flash_SectorErase(sector_addr) != 0) {
return 1;
}
// 写入数据
for (uint16_t i = 0; i < FLASH_SECTOR_SIZE; i += FLASH_PAGE_SIZE) {
if (Flash_PageWrite(sector_addr + i, data + i, FLASH_PAGE_SIZE) != 0) {
return 1;
}
}
addr += FLASH_SECTOR_SIZE;
data += FLASH_SECTOR_SIZE;
len -= FLASH_SECTOR_SIZE;
}
// 处理剩余数据
if (len > 0) {
sector_addr = addr;
// 读取原扇区数据
Flash_ReadData(sector_addr, sector_buffer, FLASH_SECTOR_SIZE);
// 修改数据
memcpy(sector_buffer, data, len);
// 擦除并写入
if (Flash_SectorErase(sector_addr) != 0) {
return 1;
}
for (uint16_t i = 0; i < FLASH_SECTOR_SIZE; i += FLASH_PAGE_SIZE) {
if (Flash_PageWrite(sector_addr + i, sector_buffer + i, FLASH_PAGE_SIZE) != 0) {
return 1;
}
}
}
return 0;
}
📄 创建文件:
spi_flash.h
c
/**
* @file spi_flash.h
* @brief W25Q128 Flash驱动头文件
*/
#ifndef __SPI_FLASH_H
#define __SPI_FLASH_H
#include <stdint.h>
/* Flash容量定义 */
#define FLASH_PAGE_SIZE 256 // 页大小
#define FLASH_SECTOR_SIZE 4096 // 扇区大小
#define FLASH_BLOCK_SIZE 65536 // 块大小
#define FLASH_CHIP_SIZE 0x1000000 // 16MB
/* 指令定义 */
#define FLASH_CMD_WREN 0x06 // 写使能
#define FLASH_CMD_WRDI 0x04 // 写禁止
#define FLASH_CMD_RDID 0x9F // 读ID
#define FLASH_CMD_RDSR 0x05 // 读状态寄存器
#define FLASH_CMD_READ 0x03 // 读数据
#define FLASH_CMD_PP 0x02 // 页编程
#define FLASH_CMD_SE 0x20 // 扇区擦除
#define FLASH_CMD_BE32 0x52 // 32KB块擦除
#define FLASH_CMD_BE64 0xD8 // 64KB块擦除
#define FLASH_CMD_CE 0xC7 // 全片擦除
/* 状态寄存器位 */
#define FLASH_SR_BUSY 0x01 // 忙标志
#define FLASH_SR_WEL 0x02 // 写使能锁存
/* 函数声明 */
void SPI_Flash_Init(void);
uint32_t Flash_ReadID(void);
uint8_t Flash_ReadStatus(void);
uint8_t Flash_IsBusy(void);
uint8_t Flash_WaitReady(uint32_t timeout_ms);
void Flash_WriteEnable(void);
void Flash_WriteDisable(void);
void Flash_ReadData(uint32_t addr, uint8_t *buffer, uint32_t len);
uint8_t Flash_PageWrite(uint32_t addr, const uint8_t *data, uint16_t len);
uint8_t Flash_SectorErase(uint32_t addr);
uint8_t Flash_BlockErase(uint32_t addr);
uint8_t Flash_ChipErase(void);
uint8_t Flash_WriteData(uint32_t addr, const uint8_t *data, uint32_t len);
uint8_t Flash_WriteDataWithErase(uint32_t addr, const uint8_t *data, uint32_t len);
#endif /* __SPI_FLASH_H */
5.2 主程序
📄 创建文件:
main.c
c
/**
* @file main.c
* @brief SPI Flash测试主程序
*/
#include "stm32f10x.h"
#include "spi_flash.h"
#include "usart1.h"
#include <stdio.h>
#include <string.h>
static volatile uint32_t sys_tick = 0;
void SysTick_Handler(void)
{
sys_tick++;
}
uint32_t GetSysTick(void)
{
return sys_tick;
}
void Delay_ms(uint32_t ms)
{
uint32_t start = GetSysTick();
while ((GetSysTick() - start) < ms);
}
/**
* @brief 测试Flash ID读取
*/
void Test_ReadID(void)
{
uint32_t id = Flash_ReadID();
printf("Flash ID: 0x%06X\r\n", id);
// 解析ID
uint8_t manufacturer = (id >> 16) & 0xFF;
uint8_t memory_type = (id >> 8) & 0xFF;
uint8_t capacity = id & 0xFF;
printf("Manufacturer: 0x%02X ", manufacturer);
if (manufacturer == 0xEF) {
printf("(Winbond)\r\n");
} else {
printf("(Unknown)\r\n");
}
printf("Memory Type: 0x%02X\r\n", memory_type);
printf("Capacity: 0x%02X ", capacity);
// 计算容量
uint32_t size_mb = 1 << (capacity - 0x14); // 0x18 = 128Mbit = 16MB
printf("(%lu MB)\r\n", size_mb);
}
/**
* @brief 测试数据读写
*/
void Test_ReadWrite(void)
{
uint32_t test_addr = 0x000000;
uint8_t write_data[256];
uint8_t read_data[256];
uint8_t error = 0;
printf("\r\n=== Flash读写测试 ===\r\n");
// 准备测试数据
for (uint16_t i = 0; i < 256; i++) {
write_data[i] = i;
}
// 擦除扇区
printf("擦除扇区...");
if (Flash_SectorErase(test_addr) == 0) {
printf("OK\r\n");
} else {
printf("FAILED\r\n");
return;
}
// 写入数据
printf("写入数据...");
if (Flash_PageWrite(test_addr, write_data, 256) == 0) {
printf("OK\r\n");
} else {
printf("FAILED\r\n");
return;
}
// 读取数据
printf("读取数据...");
Flash_ReadData(test_addr, read_data, 256);
printf("OK\r\n");
// 校验数据
printf("校验数据...");
for (uint16_t i = 0; i < 256; i++) {
if (write_data[i] != read_data[i]) {
error++;
printf("Error at %d: W=0x%02X, R=0x%02X\r\n", i, write_data[i], read_data[i]);
}
}
if (error == 0) {
printf("PASS\r\n");
} else {
printf("FAILED (%d errors)\r\n", error);
}
}
/**
* @brief 测试大数据量写入
*/
void Test_BigDataWrite(void)
{
uint32_t test_addr = 0x001000; // 从4KB处开始
uint8_t buffer[4096];
uint32_t errors = 0;
printf("\r\n=== 大数据量写入测试 ===\r\n");
// 准备数据
for (uint16_t i = 0; i < 4096; i++) {
buffer[i] = i & 0xFF;
}
// 擦除扇区
printf("擦除扇区...");
if (Flash_SectorErase(test_addr) != 0) {
printf("FAILED\r\n");
return;
}
printf("OK\r\n");
// 写入数据(使用自动分页函数)
printf("写入4KB数据...");
uint32_t start_time = GetSysTick();
if (Flash_WriteData(test_addr, buffer, 4096) != 0) {
printf("FAILED\r\n");
return;
}
uint32_t write_time = GetSysTick() - start_time;
printf("OK (%lu ms)\r\n", write_time);
// 读取并校验
printf("读取并校验...");
uint8_t read_buffer[4096];
start_time = GetSysTick();
Flash_ReadData(test_addr, read_buffer, 4096);
uint32_t read_time = GetSysTick() - start_time;
for (uint16_t i = 0; i < 4096; i++) {
if (buffer[i] != read_buffer[i]) {
errors++;
}
}
if (errors == 0) {
printf("PASS (read: %lu ms)\r\n", read_time);
printf("写入速度: %.2f KB/s\r\n", 4096.0 / write_time);
printf("读取速度: %.2f KB/s\r\n", 4096.0 / read_time);
} else {
printf("FAILED (%lu errors)\r\n", errors);
}
}
/**
* @brief 显示菜单
*/
void ShowMenu(void)
{
printf("\r\n");
printf("========== SPI Flash 测试菜单 ==========\r\n");
printf("1. 读取Flash ID\r\n");
printf("2. 读取状态寄存器\r\n");
printf("3. 基本读写测试\r\n");
printf("4. 大数据量测试\r\n");
printf("5. 全片擦除\r\n");
printf("6. 读取指定地址数据\r\n");
printf("7. 写入指定地址数据\r\n");
printf("0. 清屏\r\n");
printf("========================================\r\n");
printf("请输入选项: ");
}
int main(void)
{
char cmd;
// 初始化
SystemInit();
SysTick_Config(SystemCoreClock / 1000);
USART1_Init();
SPI_Flash_Init();
printf("\r\n");
printf("====================================\r\n");
printf(" W25Q128 SPI Flash 测试程序\r\n");
printf("====================================\r\n");
ShowMenu();
while (1) {
if (USART1_GetRxDataLength() > 0) {
if (USART1_ReadData((uint8_t *)&cmd, 1) > 0) {
switch (cmd) {
case '1':
Test_ReadID();
break;
case '2':
printf("Status: 0x%02X\r\n", Flash_ReadStatus());
break;
case '3':
Test_ReadWrite();
break;
case '4':
Test_BigDataWrite();
break;
case '5':
printf("全片擦除中,请等待...\r\n");
if (Flash_ChipErase() == 0) {
printf("全片擦除完成\r\n");
} else {
printf("全片擦除失败\r\n");
}
break;
case '6':
// 读取指定地址(简化处理)
{
uint8_t buf[16];
Flash_ReadData(0, buf, 16);
printf("Addr 0x000000: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", buf[i]);
}
printf("\r\n");
}
break;
case '7':
// 写入测试数据到地址0
{
uint8_t test[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
Flash_SectorErase(0);
Flash_PageWrite(0, test, 8);
printf("写入完成\r\n");
}
break;
case '0':
for (int i = 0; i < 50; i++) printf("\r\n");
break;
default:
printf("无效选项\r\n");
break;
}
ShowMenu();
}
}
}
}
六、测试与验证
6.1 硬件测试
连接检查:
- 测量VCC电压:3.3V
- 检查SPI信号:SCK、MOSI、MISO、CS
- 使用示波器观察SPI时序
6.2 软件测试
测试项目:
- ID读取测试
- 单字节读写测试
- 页编程测试(256字节)
- 扇区擦除测试(4KB)
- 跨页写入测试
- 大数据量读写测试
- 速度测试
预期结果:
- 写入速度:约50-100KB/s
- 读取速度:约1-2MB/s
- 扇区擦除:约45-400ms
七、故障排查
7.1 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 读ID失败 | 接线错误 | 检查MOSI/MISO是否接反 |
| 写入失败 | 未擦除 | Flash写入前必须先擦除 |
| 数据错误 | 时序问题 | 降低SPI速度 |
| 全片擦除慢 | 正常现象 | 全片擦除需要20-100秒 |
7.2 调试技巧
- 使用示波器观察SPI波形
- 先测试读ID,确认基本通信正常
- 逐步测试:单字节→页→扇区
八、总结
8.1 核心知识点
- SPI通信协议和时序
- W25Q128存储结构和指令集
- Flash擦写特性(先擦后写)
- STM32 SPI外设配置
8.2 扩展应用
- 实现FATFS文件系统
- 存储日志数据
- 固件升级(IAP)
- 参数存储