Flash入门使用

面向嵌入式开发入门的 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];
}

受个人水平所限,欢迎在评论区批评指正。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值