FSMC一般只有STM32大容量产品才具备。因此在使用中小容量产品外接存储器时,一般会通过硬件SPI模块软件模拟驱动来进行拓展。
本文将以常见的 NOR Flash(多个厂家有对标的同类产品)为例。
我使用的是普亚的P25Q32SH,这个flash除了贵和多一些功能外,在基本控制方面和华邦的W25Q32差不多,基本指令通用。但不同flash之间还是存在一些差异,要注意适配。
一、封装
8引脚的spi Flash除了封装方式有些差异,引脚排列基本是一模一样的。
代码:
总的来说还是很简单的。因为时间比较赶,只求能用,存在代码冗余和效率较低的问题,欢迎改进指正!
复制
//******************************************************************************
//* 文件名 ExtFlashSPI.h
//* 介绍: 利用STM32硬件spi实现对spi的控制
//* 基于W25Q32,在基础指令方面兼容
//* 使用其他芯片请参照手册进行指令集和参数的适配
//*
//* ※适用最大容量为16M(128Mbit)Flash
//*
//* @Author Sachefgh Xu
//*********************************模块介绍************************************
// 适用8引脚的spi flash
//
//
//
//引脚配置: /VCC 一般选择 2.7-3.6v 的元件,flash对电压有要求,推荐供电接稳压管
// /GND 接地
// /CS 片选,低电平使能;上电时应当置高电平,推荐NSS引脚使能上拉或外接上拉
// /DI(IO0) Data-in
// /DO Data-out
// /CLK 时钟线
// /WP 写保护 默认不启用;启用后高电平+写使能指令解锁----------本驱动中WP接vcc拉高
// /HOLD Hold-input; 时钟线和hold均为低电平时触发暂停;默认高电平------本驱动中HOLD接vcc拉高
//说明:
//对Flash时序的规定:MOSI-》DI, flash在时钟上升沿采样
// MISO- >DO flash在下降沿设置。当片选使能时时钟处于低电平,视为已接收一个下降沿。主机在上升沿读取采样
//配置spi模块时,时钟线空闲为低电平,上升沿采样(CPHA=0,CPOL=0); MSB模式
//
//上电时,模块写使能被禁用。
//
/***********************************ED***********************************/
#ifndef _SWSPI_FLASH_H_
#define _SWSPI_FLASH_H_
#include "stm32f1xx_ll_gpio.h"
#include "stm32f1xx_ll_spi.h"
#include "stm32f1xx_ll_dma.h"
#include "stm32f1xx_ll_utils.h"
/***********************************配置参数***********************************/
#define Flash_SPI SPI1 //连接的硬件spi模块,spi应配置全双工主机模式
#define Flash_CSPORT GPIOA //片选线;应当配置为高速输出,初始高电平
#define Flash_CSPIN LL_GPIO_PIN_4
#define BlockNumber 64 //块数量
#define Page /*Each Page has*/ 256 /*Bytes*/
#define Sector /*Each Sector has*/ 16 /*Pages*/
#define Block /*Each Block has*/ 16 /*Sectors*/
#define AddressMax (BlockNumber * Page * Sector* Block-1) //最大内存地址,每一地址对应一字节
#if (Page==256)
#define PageMsk 0xFFFF00
#if (Sector==16)
#define SectorMsk 0xFFF000
#endif // (Sector==16)
#endif
//不同容量Flash只有块数量有区别,一般扇区数量和页数量一致。
//24Bits地址 最高8位标定block,高16位标定page
//页地址 addr & 0xFFFF00
//扇区地址 addr & 0xFFF000
//块地址 addr & 0xFF0000
//额外指令配置:
//#define _81H //page erase页擦除功能.-----w25qxx系列无此功能
/******************************************************************************/
uint8_t ManufacturerID; //制造商信息
uint8_t MemoryTypeID;
uint8_t CapacityID; //容量信息
//上述信息在初始化时读取
//临时数据
/***********************/
__STATIC_INLINE void Flash_GetInformation();
__STATIC_INLINE void Flash_WaitWriteToFinish();
/**
* @brief 初始化函数,首先调用
* @note 一并读取和存储制造商信息、容量和存储类型数据
*/
__STATIC_INLINE void Flash_Init()
{
LL_mDelay(7); //等待上电初始化,可删
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);//关闭片选
//
LL_SPI_Enable(Flash_SPI); //重新开启SPI模块
LL_SPI_ReceiveData8(Flash_SPI); //置零RXNE
Flash_GetInformation();
}
/**
* @brief 读取制造商ID、存储类型ID、容量ID
* @cmd: 90h
* @note 读取后存入 ManufacturerID、MemoryTypeID、CapacityID变量中
*/
__STATIC_INLINE void Flash_GetInformation()
{ //读取Manufacturer ID& Device ID (90h)
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x9FU);
while (!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)) ; //等待接收完
LL_SPI_ReceiveData8(Flash_SPI); //置零RXNE
LL_SPI_TransmitData8(Flash_SPI, 0x00U); //生成时钟
while(!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)); //等待接收完
ManufacturerID = LL_SPI_ReceiveData8(Flash_SPI);
LL_SPI_TransmitData8(Flash_SPI, 0x00U);
while (!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)) ;//等待接收完
MemoryTypeID = LL_SPI_ReceiveData8(Flash_SPI);
LL_SPI_TransmitData8(Flash_SPI, 0x00U);
while (!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)) ;//等待接收完
CapacityID = LL_SPI_ReceiveData8(Flash_SPI);
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
}
/**
* @brief 使能擦写
* @cmd: 06h
* @note 再通过指令进行页写入、扇区擦除、块擦除、整片擦除、写状态寄存器时均需调用
*/
__STATIC_INLINE void Flash_WriteEnable()
{
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x06U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
}
/**
* @brief 禁用擦写(写入锁)
* @cmd: 04h
* @note 写入、擦除、写状态寄存器完成后调用
*/
__STATIC_INLINE void Flash_WriteDisable()
{
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x04U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
}
/**
* @brief 连续字节读取(常速)
* @cmd: 03h
* @param
* uint32_t addr //24位地址(数据最高8位忽略),每一位代表一字节数据;addr可取任意有效地址
* uint8_t *data //传入的uint_8数组地址或者 变量地址(当读取数为1时)
* uint8_t number //读取字节数
*
* @note 发送指令03h后分3字节从高到低传输地址位; Flash将在之后的时钟周期从传入地址开始
* 以地址递增顺序传出片上数据(数据位数共number位),直到CS被拉高
* 当number=1,读取指定位数据
*/
__STATIC_INLINE void Flash_ReadData(uint32_t addr, uint8_t *data, uint16_t length)
{
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x03);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)(addr >>16)&0xFF);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)((addr > > 8)&0xFF));
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)(addr & 0xFF));
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_SPI_ReceiveData8(Flash_SPI);//置零标志
//开始读取
for(uint16_t i = 0 ; i < length ; i++)
{
LL_SPI_TransmitData8(Flash_SPI, 0x00U);//generate clock
while (!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)) ;//wait till tranfer complete
data[i] = LL_SPI_ReceiveData8(Flash_SPI);
}
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
//延时
uint16_t dlay=0;
while (dlay < 960){dlay++;}
}
/**
* @brief 整片擦除(变为FF) ※此操作无法复原,使用请谨慎
* @cmd: 60h(或C7h)
* @note 将整片flash数据擦除
*/
__STATIC_INLINE void Flash_EraseChip()
{
Flash_WriteEnable();//使能写
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x60U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_mDelay(1);
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
Flash_WaitWriteToFinish();
}
#ifdef _81H
/**
* @brief 擦除整个page(将整页256bytes数据写为FF) ※此操作无法复原,使用请谨慎
* @cmd: 81h
* @param: uint32_t addr //24位页地址(数据最高8位忽略)。前16位规定页地址,最后8位无意义(dummy)。
* addr可填位于 目标页 的任一地址
*
* @note 擦除指定Page上的内容;写入前必须先进行擦除
*/
__STATIC_INLINE void Flash_ErasePage(uint32_t addr)
{
Flash_WriteEnable();//使能读写
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x81U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)(addr > > 16)&0xFF);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)((addr > > 8) & 0xFF));
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_SPI_TransmitData8(Flash_SPI, 0X00U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;//传输完毕
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
Flash_WaitWriteToFinish();
}
#endif
/**
* @brief 擦除整个sector(4096bytes = 16 pages) ※此操作无法复原,使用请谨慎
* @cmd: 20h
* @param: uint32_t addr //24位扇区地址(数据最高8位忽略)。扇区由addr A23-A12确定
* addr可填位于 目标扇区 的任一地址
* @note 擦除指定Page上的内容;写入前必须先进行擦除
*/
__STATIC_INLINE void Flash_EraseSector(uint32_t addr)
{
Flash_WriteEnable();
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x20U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)(addr > > 16)&0xFF);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_SPI_TransmitData8(Flash_SPI, (uint8_t)((addr > > 8) & 0xF0));
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_SPI_TransmitData8(Flash_SPI, 0X00U);
while (!LL_SPI_IsActiveFlag_TXE(Flash_SPI)) ;
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
Flash_WaitWriteToFinish();
}
/**
* @brief 等待退出写BUSY状态
* @retval
*/
__STATIC_INLINE void Flash_WaitWriteToFinish()
{
LL_GPIO_ResetOutputPin(Flash_CSPORT, Flash_CSPIN);
LL_SPI_TransmitData8(Flash_SPI, 0x05U);
while (!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)) ;
LL_SPI_ReceiveData8(Flash_SPI);//clear DR
do
{
LL_SPI_TransmitData8(Flash_SPI, 0x00);//dummy
while (!LL_SPI_IsActiveFlag_RXNE(Flash_SPI)) ;
} while ((LL_SPI_ReceiveData8(Flash_SPI) & 0x01));//忙时循环,不忙退出
LL_GPIO_SetOutputPin(Flash_CSPORT, Flash_CSPIN);
}
/**************************************************************************************************/
//有问题
/**
* @brief 写入数据,伴有覆盖擦除功能
* @cmd: 02h
* @param: uint32_t addr //24位地址(数据最高8位忽略),可填片上任意地址;写入
* 将从该地址开始递增, ※但写入数据长度不能溢出地址所在页(1页256Bytes).
*
* @param uint8_t length //数据长度,必须大于0
* @param uint8_t *data //写入数据所在地址指针
*
* @note 本函数工作原理如下:
* 1.通过宏定义判断是否有页擦除功能
* 2.将需要写入地址所在的扇区/页数据暂存至temp中
* 3.进行页/扇区擦除操作
* 4.将temp对应位置数据用data中待写入数据替换
* 5.将更改后的temp原位写入
*/
__STATIC_INLINE void Flash_WriteData(uint32_t addr, uint8_t *data, uint16_t length)
{
#ifdef _81H //有页擦除功能
Flash_WriteEnable();