FLASH模仿EEPROM
STM32本身没有自带EEPROM,但是自带了FLASH存储器。
STM32F103ZET6自带 1M字节的FLASH空间,和 128K+64K的SRAM空间。
STM32F4 的 SPI 功能很强大,SPI 时钟最高可以到 37.5Mhz,支持 DMA,可以配置为 SPI协议或者 I2S 协议(支持全双工 I2S)。
SPI简介
SPI,串行外围设备接口。主要应用在 EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线。
SPI内部结构简明图
SPI 接口一般使用 4 条线通信:
MISO 主入从出。
MOSI 主出从入。
SCLK 时钟信号,由主设备产生。
CS 从设备片选信号,由主设备控制。
主机和从机都有一个移位寄存器,主机通过向自己的SPI串行寄存器写入一个字节来发起一次传输。寄存器通过MOSI信号线将字节传送给从机,从机将自己移位寄存器的内容通过MISO信号线返回给主机。这样,两个移位寄存器的内容被交换。
外设的写操作和读操作是同步完成的,如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。
SPI总线有四种工作方式,通过配置时钟极性(CPOL)和相位(CPHA)实现。CPOL决定时钟空闲状态电平,CPHA选择数据传输的采样时刻。主从设备需保持时钟配置一致。
SPI1主模式配置步骤
1)配置相关引脚的复用功能,使能 SPI1 时钟。
启用SPI1需两步:
首先使能SPI1时钟,通过APB2ENR的第12位设置;
然后将PB3、4、5配置为复用输出AF5,分别对应SCK、MISO、MOSI,CS由软件管理。
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);//使能 SPI1 时钟
复用 PB3,PB4,PB5 为 SPI1 引脚的方法为:
cpp
GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1); //PB3 复用为 SPI1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); //PB4 复用为 SPI1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); //PB5 复用为 SPI1
同时我们设置相应的引脚模式为复用功能模式:
cpp
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
2)初始化 SPI1,设置 SPI1 工作模式等。
通过SPI1_CR1寄存器配置SPI1:
设为主机模式,
数据格式8位,
设置SCK时钟极性及采样方式(CPOL/CPHA),
并调整时钟频率至最大37.5MHz,
同时确定数据格式(MSB或LSB在前)。
cpp
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
typedef struct
{
uint16_t SPI_Direction;//通信方式,半双工、全双工
uint16_t SPI_Mode; //设置主从模式。例如主机模式SPI_Mode_Master
uint16_t SPI_DataSize; //8/16位帧格式选择。8位传输则SPI_DataSize_8b。
uint16_t SPI_CPOL; //时钟极性。决定同步时钟空闲状态的电平。
uint16_t SPI_CPHA; //采样方式。在串行同步时钟的第几个跳变沿采集信号。SPI_CPHA_2Edge位为第2个跳变沿。
uint16_t SPI_NSS; //设置NSS信号由硬件控制还是软件控制。SPI_NSS_Soft软件控制
uint16_t SPI_BaudRatePrescaler; //波特率预分频值。有2到256分频共八个值可选。
//初始化的时候我们选择 256 分频值
//SPI_BaudRatePrescaler_256,传输速度为 84M/256=328.125KHz。
//数据传输顺序是MSB在前还是LSB在前。MSB为最高有效位,LSB最低有效位。
uint16_t SPI_FirstBit;
uint16_t SPI_CRCPolynomial; //CRC校验多项式,大于1即可。
}SPI_InitTypeDef;
使能SPI1
cpp
SPI_Cmd(SPI1, ENABLE); //使能 SPI1 外设
SPI 传输数据
cpp
//往SPIx数据寄存器写入数据,实现发送
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
cpp
//从SPI的数据寄存器读数据
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
查看 SPI 传输状态
SPI传输过程中经常要判断数据是否传输完成,发送区是否为空等等状态。
cpp
//检查SPI1的接收缓冲区非空
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE);
W25Q128简介
W25Q128将 16M的容量分为 256个块,每个块64K字节。
一个块又分为 16个扇区,每个扇区 4K字节。
最小擦除单位为一个扇区,也就是 4K字节。
W25Q128支持不超过存储范围的任意地址开始读取数据,在发送24位地址之后程序就可以开始循环读数据,其地址会自动增加。
cpp
//读取 SPI FLASH
//在指定地址开始读取指定长度的数据
//NumByteToRead:要读取的字节数(最大 65535)
void W25QXX_Read(u8* pBuffer, //数据存储区
u32 ReadAddr, //开始读取的地址(24bit)
u16 NumByteToRead)
{
u16 i;
W25QXX_CS=0; //使能器件
SPI1_ReadWriteByte(W25X_ReadData); //发送读取命令
SPI1_ReadWriteByte((u8)((ReadAddr)>>16)); //发送 24bit 地址
SPI1_ReadWriteByte((u8)((ReadAddr)>>8));
SPI1_ReadWriteByte((u8)ReadAddr);
for(i=0;i<NumByteToRead;i++)
{
pBuffer[i]=SPI1_ReadWriteByte(0XFF); //循环读数,主机发送空字节
}
W25QXX_CS=1;
}
正点原子提供的FLASH写入函数支持在 W25Q128的任意地址写入不超过容量的任意长度。
/4096获取扇区地址。
%4096获取在扇区内的偏移。
4096-扇区偏移 = 扇区剩余空间大小。
在SPI Flash写入时,需判断扇区是否需擦除,即检查目标区域是否全为0xFF。
因Flash写入前需擦除(置位为1),擦除后扇区全为0xFF。若区域非全0xFF,则需擦除;否则可直接写入,因未擦除位写0无效。
cpp
//写 SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区 WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大 65535)
u8 W25QXX_BUFFER[4096];
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u32 secpos;
u16 secoff; u16 secremain; u16 i;
u8 * W25QXX_BUF;
W25QXX_BUF=W25QXX_BUFFER;
secpos=WriteAddr/4096;//扇区地址
secoff=WriteAddr%4096;//在扇区内的偏移
secremain=4096-secoff;//扇区剩余空间大小
//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
if(NumByteToWrite<=secremain) //写入的字节数小于扇区剩余字节
secremain=NumByteToWrite; //不大于 4096 个字节
while(1)
{
W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据,遍历整个扇区,判断是否都是0xFF
{
if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除
}
if(i<secremain)//需要擦除
{
W25QXX_Erase_Sector(secpos);//擦除这个扇区
for(i=0;i<secremain;i++) //复制
{
W25QXX_BUF[i+secoff]=pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
}
else
{
W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//已擦除的,直接写
}
if(NumByteToWrite==secremain)
break;//写入结束了
else//写入未结束
{
secpos++; //扇区地址增 1
secoff=0; //偏移位置为 0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumByteToWrite-=secremain; //字节数递减
//如果要写入的字节数
if(NumByteToWrite>4096)
secremain=4096;//下一个扇区还是写不完
else
secremain=NumByteToWrite; //下一个扇区可以写完了
}
};
}