使用SPI接口在STM32上读取Flash存储器
本次实验为使用SPI的轮询方式读写Flash。采用HAL库进行书写。
我使用的主控芯片是stm32f103zet6,上面搭载的Flash芯片是W25Q64芯片,这个芯片的容量是8MB。
SPI的硬件接口和通信协议
SPI的硬件接口
SPI有四线串行总线,其信号线分别有:
SCLK:串行时钟(主机输出)
MOSI:主输出从机输入或主机输出从机输入(主机输出的数据)
MISO:主输入从输出或主输入从输出(从输出的数据输出)
SS:从机选择(通常为低电平有效,一般写作NSS,主机输出),当一个SPI网络中有多个SPI从设备时,主设备通过控制这些设备的NSS信号来选择通信的从机设备,未被选择的设备NSS信号为高阻态,SPI主设备可以使用普通的GPIO输出引脚连接从设备,我们一般也用普通的GPIO设备模拟信号线。
SPI的时序模式
SPI一共有四种时序模式,这四种模式是由SPI控制寄存器SPI_CR1中的CPOL(第1位)和CPHA(第0位)控制的:
CPOL:时钟极性 (Clock polarity) :为0时表示在空闲状态时,SCK保持低电平; 为1时表示在空闲状态时,SCK保持高电平。
CPHA:时钟相位 (Clock phase) 为0时表示数据采样从第一个时钟边沿开始; 为1时表示数据采样从第二个时钟边沿开始。
这两位在通信正在进行的时候,不能修改。
这样两位一共就组成了4种不同的时序模式,以下两图是分别四种时序模式的采样位置,虚线位置为读取数据的位置。
STM32F103ZET6的SPI接口
● 一共有3个SPI接口
● 8或16位传输帧格式选择 ● 主或从操作 ● 支持多主模式
● 8个主模式波特率预分频系数(最大为fPCLK/2)
● 主模式和从模式的快速通信
● 主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变
● 可编程的时钟极性和相位
● 可编程的数据顺序,MSB在前或LSB在前(高位先行或低位先行),一般选择MSB先行
● 可触发中断的专用发送和接收标志
● SPI总线忙状态标志
● 支持可靠通信的硬件CRC ─ 在发送模式下,CRC值可以被作为最后一个字节发送在全双工模式中对接收到的最后一个字节自动进行CRC校验
● 可触发中断的主模式故障、过载以及CRC错误标志
● 支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求
Flash芯片W25Q64
硬件接口
这个芯片的最大存储容量为64Mbit,即8MB。支持标准的SPI通信协议,这里的引脚和SPI的引脚对应:CS对应片选信号NSS;CLK对应时钟信号SCK;SI对应MOSI;SO对应MISO;这里的片选信号使用GPIO高低电平模拟片选信号。
这里的引脚是PB13、PB14 、PB15,所以选择SPI2(如果不清楚自己的引脚对应的是哪个SPI,可以一个一个试)。
存储空间的划分
名称 |
对应的区间 |
块Sector |
128个块 |
扇区Sector |
一个块中有16个扇区 |
页Page |
一个扇区中有16个页 |
数据读写的原则
从W25Q64中读数据:用户可以从任意地址开始读取任意长度数据
向W25Q64中写数据:用户可以从任意位置写入数据,但是写入的数据不能超过一页,也就是如果从页的开始写入数据,一次写入最多可以写入一页(256字节)的数据。而且向存储区写入数据时必须要先擦除芯片,也就是将数据恢复成0xff。芯片厂商设置的指令可以让用户以块(64KB)、块(32KB)、扇区为单位擦除,但是不能对页进行擦除,擦除的最小单位就是扇区。
轮询方式读写W25Q64的编程实现
实现功能:主要实现对芯片的写入,读取,擦除等操作
CubeMX项目设置
在RCC中设置高速时钟为外部晶振,然后在时钟树设置HCLK为72MHz,CubeMX会自动帮我们设置好时钟的具体设置。RCC设置如图
图表 1RCC设置
使用PB12引脚作为W25Q64芯片的CS信号,配置PB12为GPIO_Output,初始输出为高电平,以便芯片刚开始为不被选中的状态。
接下来设置SPI:SPI选择SPI2
图表 2SPI的模式和参数设置
这里主要要注意模式的选择,这里选择的是模式3,即CPOL选择High,CPHA选择2Edge。NSS选择软件产生NSS信号,因为我们使用GPIO输出高低电平来模拟选择信号。启用SPI2之后CubeMX会自动分配PB13、PB14 、PB15引脚,无需手动配置。之后基本配置就完成了,点击生成代码。
功能实现
这里主要实现对Flash各种基本操作。
首先利用SPI库函数实现基本收发操作:
// SPI轮询操作时的最大等待时间,单位:节拍数
#define MAX_TIMEOUT 200
// SPI接口发送一个字节
// byteData 是要传输的数据
HAL_StatusTypeDef HAL_SPI_TransmitOneByte(uint8_t byteData)
{
return HAL_SPI_Transmit(&SPIx, &byteData, 1, MAX_TIMEOUT);
}
// SPI接口发送多个字节
// pBuffer 是要传输数据的指针,byteCount 是要传输的数据量,最大为256字节
HAL_StatusTypeDef HAL_SPI_TransmitBytes(uint8_t* pBuffer,uint8_t byteCount)
{
return HAL_SPI_Transmit(&SPIx, pBuffer, byteCount, MAX_TIMEOUT);
}
// SPI接口接收一个字节
uint8_t HAL_SPI_ReceiveOneByte(void)
{
uint8_t byteData;
HAL_SPI_Receive(&SPIx, &byteData, 1, MAX_TIMEOUT);
return byteData;
}
// SPI接口接收多个字节
HAL_StatusTypeDef HAL_SPI_ReceiveBytes(uint8_t* pBuffer,uint8_t byteCount)
{
return HAL_SPI_Receive(&SPIx, pBuffer, byteCount, MAX_TIMEOUT);
}
以上函数中的SPIx为定义的宏函数,使用的是SPI2,这样写的好处是方便移植。
基本操作指令:
因为之后的读取或写入函数需要时间,有的需要较长时间,所以这里定义几个函数用来检测操作是否完成,原理是检测状态寄存器CR1的BUSY位。
// 读取状态寄存器SR1,Command = 05h
uint8_t Read_SR1(void)
{
uint8_t byte = 0;
CS_ON;
HAL_SPI_TransmitOneByte(0x05);
byte = HAL_SPI_ReceiveOneByte();
CS_OFF;
return byte;
}
// 读取状态寄存器SR2,Command = 35h,本次实验未用到CR2
uint8_t Read_SR2(void)
{
uint8_t byte = 0;
CS_ON;
HAL_SPI_TransmitOneByte(0x35);
byte = HAL_SPI_ReceiveOneByte();
CS_OFF;
return byte;
}
// 等待操作完成
uint8_t Flash_Wait_Busy(void)
{
uint8_t SR1 = 0;
uint32_t delay = 0;
SR1 = Read_SR1();
while((SR1&0x01) == 1)
{
HAL_Delay(1);
delay++;
SR1 = Read_SR1();
}
return delay; // 可以反映读取时间
}
为了检测基本收发函数的书写,这里使用基本收发函数写了一个读取芯片ID的函数,芯片的数据手册会提供读取芯片ID的操作数
// 读取器件ID号
uint16_t Flash_ID_Read(void)
{
uint16_t a = 0;
CS_ON;
HAL_SPI_TransmitOneByte(0x9f);
HAL_SPI_TransmitOneByte(0xef);
a = HAL_SPI_ReceiveOneByte()<<8;
a |= HAL_SPI_ReceiveOneByte();
CS_OFF;
Flash_Wait_Busy();
return a;
}
这个函数中的CS_ON和 CS_OFF是两个宏函数,目的是实现片选信号的开关
// CS = 0,打开片选信号
#define CS_ON HAL_GPIO_WritePin(SPI2_CS_GPIO_Port, SPI2_CS_Pin, GPIO_PIN_RESET);
// CS = 1 ,关闭片选信号
#define CS_OFF HAL_GPIO_WritePin(SPI2_CS_GPIO_Port, SPI2_CS_Pin, GPIO_PIN_SET);
#define SPIx hspi2
如果函数书写正常会读取到芯片的ID号,这个ID号是由厂商设定的,在芯片的数据手册可以查到。
计算地址的辅助功能函数:
因为W25Q64芯片的一些操作是需要使用24位的绝对地址,直接记住或者计算都是比较麻烦的,所以这里将这些封装为函数
// 根据Block的编号获取绝对地址,共128个块
// BlockNo是块地址的编号(0至127——一个Block是64KB) , 块内地址范围是0x0000——0xFFFF
uint32_t Flash_Addr_byBlock(uint8_t BlockNo)
{
uint8_t addr = 0;
addr = BlockNo << 16;
return addr;
}
// 根据Sector的编号获取绝对地址,共4096个扇区
// Sector是扇区地址的编号(0至4095——一个Sector是4KB),扇区内地址范围是0x000——0xFFF
uint32_t Flash_Addr_bySector(uint16_t SectorNo)
{
if(SectorNo > 4095) // 地址不能大于4095
SectorNo = 0;
uint16_t addr = 0;
addr = SectorNo << 12;
return addr;
}
// 根据Page的编号获取绝对地址,共65536个页
// Page是页地址的编号(0至65535——一个Page是256字节),扇区内地址范围是0x00——0xFF
uint32_t Flash_Addr_byPage(uint32_t PageNo)
{
if(PageNo > 65535) // 地址不能大于65535
PageNo = 0;
uint32_t addr = 0;
addr = PageNo << 8;
return addr;
}
// 根据Block和Sector的编号计算内部地址,一个Block内部有16个Sector
// BlockNo的取值范围为0——255,内部的SubSectorNo取值范围为0——15
uint32_t Flash_Addr_byBlockSector(uint8_t BlockNo,uint8_t SubSectorNo)
{
if(SubSectorNo > 15) // 地址不能大于15
SubSectorNo = 0;
uint32_t addr = 0;
addr = BlockNo << 16;
addr |= SubSectorNo << 12;
return addr;
}
// 根据Block和Sector和Page的编号计算内部地址,一个Block内部有16个Sector,一个Sector内部有16个Page
// BlockNo的取值范围为0——255,内部的SubSectorNo取值范围为0——15,内部的SubPageNo取值范围为0——15
uint32_t Flash_Addr_byBlockSectorPage(uint8_t BlockNo,uint8_t SubSectorNo, uint8_t SubPageNo)
{
if(SubSectorNo > 15) // 地址不能大于15
SubSectorNo = 0;
uint32_t addr = 0;
addr = BlockNo << 16;
addr |= SubSectorNo << 12;
addr |= SubPageNo << 8;
return addr;
}
// 将24位地址信息分成高中低三个字节
void Flash_SpliteAddr(uint32_t globalAddr,uint8_t* addrHigh,uint8_t* addrMid,uint8_t* addrLow)
{
*addrHigh = (globalAddr >> 16);
globalAddr = globalAddr & 0x0000FFFF;
*addrMid = (globalAddr >> 8);
*addrLow = globalAddr & 0x000000FF;
}
器件的擦除函数
在函数的擦除操作前需要先进行写使能,不然会出现擦除不成功操作,所以这里先实现写使能函数
// 写使能,Command = 06h
HAL_StatusTypeDef Flash_Write_Enable(void)
{
CS_ON;
HAL_StatusTypeDef resule = HAL_SPI_TransmitOneByte(0x06);
CS_OFF;
Flash_Wait_Busy();
return resule;
}
接下来实现块和扇区的擦除函数
// 擦除操作(擦除前必须先写使能,然后检测Busy位是否空闲,写完后也得等待Busy位空闲才能擦除完成)
// 扇区擦除(4KB),Command = 20h,擦除的最小单位是扇区擦除
void Flsah_EraseSector(uint32_t globalAddr)
{
uint8_t addrHigh;
uint8_t addrMid;
uint8_t addrLow;
Flash_Wait_Busy();
Flash_Write_Enable();
CS_ON;
Flash_SpliteAddr(globalAddr,&addrHigh,&addrMid,&addrLow);
HAL_SPI_TransmitOneByte(0x20);
HAL_SPI_TransmitOneByte(addrHigh);
HAL_SPI_TransmitOneByte(addrMid);
HAL_SPI_TransmitOneByte(addrLow);
CS_OFF;
Flash_Wait_Busy();
}
// 块擦除,Command = D8h,擦除的最小单位是扇区擦除
void Flsah_BlockSector(uint32_t globalAddr)
{
uint8_t addrHigh;
uint8_t addrMid;
uint8_t addrLow;
Flash_Wait_Busy();
Flash_Write_Enable();
CS_ON;
Flash_SpliteAddr(globalAddr,&addrHigh,&addrMid,&addrLow);
HAL_SPI_TransmitOneByte(0xD8);
HAL_SPI_TransmitOneByte(addrHigh);
HAL_SPI_TransmitOneByte(addrMid);
HAL_SPI_TransmitOneByte(addrLow);
CS_OFF;
Flash_Wait_Busy();
}
存储区读写函数
在擦除完成后就可以完成对芯片的读写操作了
// 存储区读写数据
// 读单个数据,Command = 03h
uint8_t Flsah_ReadOneByte(uint32_t globalAddr)
{
uint8_t addrHigh;
uint8_t addrMid;
uint8_t addrLow;
uint8_t byte;
Flash_Wait_Busy();
CS_ON;
Flash_SpliteAddr(globalAddr,&addrHigh,&addrMid,&addrLow);
HAL_SPI_TransmitOneByte(0x03);
HAL_SPI_TransmitOneByte(addrHigh);
HAL_SPI_TransmitOneByte(addrMid);
HAL_SPI_TransmitOneByte(addrLow);
byte = HAL_SPI_ReceiveOneByte();
CS_OFF;
Flash_Wait_Busy();
return byte;
}
// 读任意个数据,Command = 03h,byteCount是要读取的数据个数,pBuffer是数据存储区的指针
void Flsah_ReadBytes(uint32_t globalAddr,uint8_t* pBuffer ,uint8_t byteCount)
{
uint8_t addrHigh;
uint8_t addrMid;
uint8_t addrLow;
uint8_t byte;
Flash_Wait_Busy();
CS_ON;
Flash_SpliteAddr(globalAddr,&addrHigh,&addrMid,&addrLow);
HAL_SPI_TransmitOneByte(0x03);
HAL_SPI_TransmitOneByte(addrHigh);
HAL_SPI_TransmitOneByte(addrMid);
HAL_SPI_TransmitOneByte(addrLow);
HAL_SPI_ReceiveBytes(pBuffer, byteCount);;
CS_OFF;
Flash_Wait_Busy();
}
// 页写入,Command = 02h,最大写入256字节,写入前要先擦除,不然无法写入
void Flsah_WriteInPage(uint32_t globalAddr,uint8_t* pBuffer ,uint8_t byteCount)
{
uint8_t addrHigh;
uint8_t addrMid;
uint8_t addrLow;
uint8_t byte;
Flash_Wait_Busy();
Flash_Write_Enable(); // 先写使能才能写入数据
CS_ON;
Flash_SpliteAddr(globalAddr,&addrHigh,&addrMid,&addrLow);
HAL_SPI_TransmitOneByte(0x02);
HAL_SPI_TransmitOneByte(addrHigh);
HAL_SPI_TransmitOneByte(addrMid);
HAL_SPI_TransmitOneByte(addrLow);
HAL_SPI_TransmitBytes(pBuffer,byteCount);
CS_OFF;
Flash_Wait_Busy();
}
这样就完成了基本功能的实现,最后我们可以在主函数中对这些函数进行调用,进而实现对Flash的读写操作。这里对主函数部分不做讨论。