文章目录
面向嵌入式开发入门的 Flash 驱动指南,侧重零基础上手的流程梳理。
技术名词解释
- 内置 Flash & 外挂 Flash
- 非易失性存储(Non-Volatile Memory,简称 NVM)& 易失性存储(Volatile Memory)
什么是FLASH?
- Flash 全称 Flash Memory。
- Flash(闪存)得名是因为对比早期旧存储器的擦除(如单字节擦除,紫外线照射十几分钟),它可以一瞬间整块擦除数据 。
- Flash 本质是一整块存储空间,硬件将其划分为页、扇区、块多级单元。扇区、块的作用,是把整片存储器分割成多个独立区域,便于编程与擦除管理。理论上支持字节级访问,但字节粒度的擦除效率极低;所以 Flash 硬件设计为按页编程、按扇区 / 块擦除,提升操作效率。

如图所示,以W25Q系列做例子,整块Flash有256个Block,一个Block有16个Sector,这一点可以从地址上看出对应关系
如:Block(255): FF0000h - FFFFFFh, Sector(0 - 16):xx0000h - xxFFFFh
内置Flash和外挂flash(简单介绍)
| 对比项 | 内置 Flash(片内 Flash ) | 外挂 Flash (片外 Flash) |
|---|---|---|
| 位置 | 芯片内部 | 独立芯片,PCB 上另外焊一颗(如 W25Q 系列 SPI Flash) |
| 接口 | CPU 直接访问,内部总线,无需外设引脚 | SPI / QSPI 等,占用 IO 引脚 |
| 用途 | 存放固件程序、启动代码、中断向量表,也可存少量参数 | 存放大资源:字库、图片、日志、配置文件;也可跑 XIP 程序 |
非易失性存储(Non-Volatile Memory,简称 NVM)& 易失性存储(Volatile Memory)
- 非易失性存储:断电之后,存储的数据不会丢失的存储器。如 FLASH,EEPROM
- 易失性存储:断电数据直接清空,如 RAM。
Flash的使用流程
以SPI通信为例,W25Q系列,stm32标准库操作
准备工作如下:准备一份SPI主机全双工收发函数即可
void SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// ========== 1. 开启外设时钟 ==========
// 开启GPIOA时钟(SPI1默认引脚在PA口)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 开启SPI1外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
// ========== 2. 配置SPI对应GPIO引脚 ==========
// PA5 -> SCK 、PA7 -> MOSI :复用推挽输出(硬件SPI自动控制电平)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 引脚速度建议匹配SPI波特率,50MHz通用
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA6 -> MISO :上拉输入(避免浮空干扰,比浮空输入更稳定)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA8 -> NSS :片选
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT_PP; // 通用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStruct);
// ========== 3. 配置SPI工作参数 ==========
SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 8位数据宽度
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 时钟空闲电平为低(模式0)
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 第1个时钟沿采样(模式0)
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // 波特率分频(72M主频下约1.125MHz)
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // NSS由软件控制
SPI_Init(SPI1, &SPI_InitStruct);
// ========== 4. 主机模式必需:内部NSS拉高 ==========
// 软件NSS模式下,内部NSS置高才能让SPI正常工作在主机模式
SPI_NSSInternalSoftwareConfig(SPI1, SPI_NSSInternalSoft_Set);
// ========== 5. 使能SPI1外设 ==========
// 配置完成后必须开启,否则SPI不工作
SPI_Cmd(SPI1, ENABLE);
}
/**
* @brief SPI主机全双工收发函数
* @param SPIx : SPI外设实例(SPI1/SPI2等)
* @param pDataTx : 发送数据缓冲区指针 → 数据通过 PA7(MOSI) 引脚逐位输出
* @param pDataRx : 接收数据缓冲区指针 → 数据通过 PA6(MISO) 引脚逐位输入
* @param Size : 收发字节数(SPI全双工特性:发多少字节就收多少字节)
* @note 调用前必须手动拉低 PA8(NSS) 选中从机,调用完成后拉高 PA8 释放从机
* @note 对应硬件时序:CPOL=低(空闲电平)、CPHA=第1边沿采样、MSB高位先行
*/
void SPI_Master_Transmit_Receive(SPI_TypeDef *SPIx, const uint8_t *pDataTx, uint8_t *pDataRx, uint16_t Size)
{
// ========== 1. 写入首字节,启动第一帧传输 ==========
// 硬件动作:数据写入发送缓冲区→自动移入移位寄存器
SPI_I2S_SendData(SPIx, pDataTx[0]);
// ========== 2. 流水线收发循环 ==========
// SPI全双工原理:SCK每跳变1次,MOSI输出1位、MISO同时采样1位;发完1字节的同时收完1字节
// 流水线逻辑:发送第i+1字节的同时,读取第i字节的接收结果
for(uint16_t i = 0; i < Size - 1; i++)
{
// 等待发送缓冲区为空 → 上一字节已全部从PA7(MOSI)输出完成,可加载下一字节
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
// 写入下一个发送字节 → 自动加载到移位寄存器,继续从PA7(MOSI)输出后续位
SPI_I2S_SendData(SPIx, pDataTx[i + 1]);
// 等待接收缓冲区非空 → 上一字节已全部从PA6(MISO)采样完成,存入接收缓冲区
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
// 读取第i个接收字节 → 对应第i个发送字节发出时,从PA6(MISO)同步收到的数据
pDataRx[i] = SPI_I2S_ReceiveData(SPIx);
}
// ========== 3. 读取最后一个接收字节 ==========
// 此时最后一个发送字节已从PA7(MOSI)输出完毕,对应最后一个字节已从PA6(MISO)采样完成
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
pDataRx[Size - 1] = SPI_I2S_ReceiveData(SPIx);
}
使用流程
- 截取部分SPI 通信的操作码、时序格式,可对照该表进行通信,具体细节请参考手册

- 以 读 JEDEC ID为例
/**
* @brief 读取W25Q系列Flash的JEDEC ID
* @return 32位ID值,格式:0x00 + 厂商ID + 内存类型 + 容量
* 典型值:W25Q64 -> 0x00EF4017,W25Q32 -> 0x00EF4016
* @note 依赖SPI1初始化完成且持续使能,PA8为软件片选引脚
*/
uint32_t W25Q_Read_JEDEC_ID(void)
{
// 发送缓冲区:第1字节是0x9F读ID指令,后3字节是dummy数据(仅用于提供SCK时钟)
uint8_t tx_buf[4] = {0x9F, 0xFF, 0xFF, 0xFF};
// 接收缓冲区:全双工同步接收4字节,第1字节无效,后3字节为有效ID
uint8_t rx_buf[4];
// ========== 1. 拉低PA8片选,选中W25Q芯片 ==========
GPIO_WriteBit(GPIOA, GPIO_Pin_8, Bit_RESET);
// ========== 2. 全双工收发:发指令+读ID ==========
// 硬件对应:PA7(MOSI)输出指令和dummy,PA6(MISO)同步接收3字节ID数据
App_SPI_MasterTransmitReceive(SPI1, tx_buf, rx_buf, 4);
// ========== 3. 拉高PA8片选,结束通信 ==========
GPIO_WriteBit(GPIOA, GPIO_Pin_8, Bit_SET);
// ========== 4. 拼接返回3字节ID ==========
// rx_buf[0]:发送指令时同步收到的无效数据
// rx_buf[1]:厂商ID(Winbond固定为0xEF)
// rx_buf[2]:内存类型(固定为0x40)
// rx_buf[3]:容量标识(0x15=W25Q16, 0x16=W25Q32, 0x17=W25Q64, 0x18=W25Q128)
return (uint32_t)rx_buf[1] << 16 | (uint32_t)rx_buf[2] << 8 | rx_buf[3];
}
受个人水平所限,欢迎在评论区批评指正。

8455

被折叠的 条评论
为什么被折叠?



