目录
项目需求
本项目主要时SPI用于FLASH存储器的应用。通过SPI控制FLash进行数据的保存和删除。
一、SPI概要
在我们使用UART(通用串行异步通讯协议)时,因为UART没有时钟信号,速度不同,无法控制何时收发数据。非要解决这个问题的话,需要为UART传输的数据添加起始位和停止位,双方波特率还需同步,很麻烦,比较不好。于是由摩托摩拉公司牵头推出了一种新的通讯总线 SPI:串行外设接口。
SPI是一个高速、全双工、同步的串行通信总线。应用场景:OLED屏幕、FLASH存储器、AD转换器
通信方式:串行同步全双工(就是数据在线上按照时间顺序一位一位的传输,发送和接收时要在通信时钟的同步下进行数据传输,且可以同时发送和接收)该总线就是利用单独的数据线(MISO和MOSI)和单独的时钟信号线(SCK)完美解决了收发端的数据完美同步。
接线:
五线制SPI:全双工通信
MOSI:主设备输出、从设备输入 TX
MISO: 主设备输入、从设备输出 RX
SCK:时钟线
CS:片选信号线 一主多从,拉低选择和哪个从机通信
GND:地线
单机通信:
多机通信:
总结:除了SS片选线需要单独备一根线和从机进行一对一的链接,其他都可以一对多,多个从机接同一个主机接口。
通信时序:
1,拉低片选信号线,决定要跟谁通信
2,操作时钟线,让MOSI和MISO准备数据(采样)
3,操作时钟线,让MOSI和MISO发送数据(接收)
4,拉高CS片选信号线,停止通信
SPI通信模式:SPI通信是一个环形传输,主机发送数据的同时,从机也会回复对应的数据。数据发送完成,其实也就接收完成了。
比较关键的点:发送数据时高位先发,还是低位先发,时钟的边沿,在哪个边沿上进行数据的发送,时钟的极性,时钟默认的电平。时钟的速度。
CPHA:时钟相位
CPHA=0时,在时钟的第一个边沿进行采样,第二个边沿进行输出
CPHA=1时,在时钟的第二个边沿进行采样,第三个边沿进行输出
CPOL:时钟极性
CPOL=0时,空闲时时钟为低电平CPOL=1时,空闲时时钟为高电平
CPHA=0,CPOL=0:SPI的模式0:上升沿采样、下降沿接受
CPHA=0,CPOL=1:SPI的模式1:下降沿采样、上升沿接受
CPHA=1,CPOL=0:SPI的模式2:下降沿采样、上升沿接受
CPHA=1,CPOL=1:SPI的模式3:上升沿采样、下降沿接受
一般能够支持SPI模式0的设备也支持SPI模式3,支持模式1的设备也支持模式2。SPI通信没有具体的协议格式,格式根据通信对象的要求来定。一般的传输数据时候采用的就是8位进行,高位先传还是低位先传,传输的速度。这些参数都需要根据我们的通信对象来进行配置。
STM32中SPI接口的框图
寄存器配置步骤
主要用到的就是SPI_CR1寄存器:
二、SPI配置
1.开时钟
硬件接口:
配置对应引脚:
根据引脚的开SPI2和GPIOB的时钟
2.配置IO口
配置GPIO:MOSI,SCK:复用推挽输出;MISO:浮空输入 CS:通用推挽输出
由于是主机所以CS当做GPIO配就行(软件模式)从机的话配硬件模式
最后拉高CS是因为CS处于低电平时表示开始通信了。
3.配置和使能SPI
主要配置SPI极性,相位等配置。
三、FLASH应用
本次用到的FLash为W25Q64,是华邦存储推出的一款串行FLASH存储芯片。
空间规格:
64Mbit (兆位)== 8Mbyte(兆字节)
256字节为一页、16页为一扇区、16扇区为一块
写入最小可以1字节1字节写入,删除则最少也要擦除一个扇区。
地址表示:
0x000000->第0个块的第0个扇区的第0个页的第0个字节
地址划分:0x000000 - 0x7FFFFF总计的地址空间
0x010111 0x023510
黄色:表示第几块
绿色:第0个扇区
蓝色:第1页
紫色:表示第几个字节(0x11 ->17)
W25Q64存储器驱动接口
接线方式:
CS->CS
DO->MISO
DI->MOSI
CLK->CLK
1.SPI发送数据
发送:
判断发送缓冲器空闲标志(TXE)是否为0,为0表示正在发,此时持续等待数据发完。标志为1表示之前的发送完了,空闲下来了。此时就可以开始往DR发数据。
接收:
判断接收缓冲器非空(RXNE)是否为0,为0表示接受缓冲区为空,没数据,此时持续等待数据到来。标志为1表示数据来了,此时就可以开始读DR的数据了。
/*****************************************************************
*函 数 名 称:SPI2_SendData
*函 数 功 能:SPI2发送数据
*函 数 形 参:data:发送内容
*函 数 返 回:返回收到的数据
*******************************************************************/
uint8_t SPI2_SendData(uint8_t data)
{
uint8_t recv = 0;
while((SPI2->SR & (0x1<<1)) == 0);
SPI2->DR = data;
while((SPI2->SR & (0x1<<0)) == 0);
recv = SPI2->DR;
return recv;
}
2.FLASH写使能
FLASH存储器,只能写入0,不能写入1,对FLASH擦除操作,其实就是将存储器指定空间全部置1.
06H:写使能:写入和擦除之前都要,发送06H
/*****************************************************************
*函 数 名 称:Write_Enable
*函 数 功 能:FLASH写使能操作
*函 数 形 参:无
*函 数 返 回:无
*******************************************************************/
void Write_Enable(void)
{
//拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
//发送0x06指令
SPI2_SendData(0x06);
//拉高CS
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
3.FLASH等待操作完成
等待操作步骤:
1.先拉低CS。
2.发送05或35指令。
3.循环往FLash发送数据(无所谓什么数据,只是为了置换出FLash状态寄存器的值),判断寄存器的第0位BUSY是否为0,为0时就代表Flash为空闲状态,可以执行其他操作。
4.判断结束就拉高CS。
/*****************************************************************
*函 数 名 称:Flash_WaitForWriteEnd
*函 数 功 能:FLASH等待操作完成
*函 数 形 参:无
*函 数 返 回:无
*******************************************************************/
void Flash_WaitForWriteEnd(void)
{
uint8_t recv = 0;
//拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
SPI2_SendData(0x05);//发送读取指令
do{
recv = SPI2_SendData(0x55);//循环读取寄存器数据,0x55随便改
}while((recv & 0x01) == SET);//判断寄存器的第0位是否是1
//拉高CS
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
4.FLASH页写操作
02H(0x02):
写入数据指令,执行之前先写使能,06H
写入之前先擦除、发送02H之前先发06H指令。先拉低CS,然后发送02H,发送三个字节的地址,然后至少跟一个字节的数据,最多是256字节的数据。写入完成之后要判断一下BUSY标志位。
1.由于要进行写操作,所以要先进行写使能。
2.拉低CS,发送02指令。
3.发送三个字节,即24位的地址,每次发8位分3次发送。为了告知写入的位置。
4.发送数据,8位8位发,最多256。
5.调用FLash等待函数,等待写入完成。
6.写入完成后,拉高CS。
/*****************************************************************
*函 数 名 称:Flash_PageProgram
*函 数 功 能:FLASH页写操作
*函 数 形 参:add:写入地址 data:写入数据 len:写入数据长度
*函 数 返 回:无
*******************************************************************/
void Flash_PageProgram(uint32_t add, uint8_t *data,uint16_t len)
{
//1,先写使能
Write_Enable();
//2,拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
//3,发送命令
SPI2_SendData(0x02);
//4,发送三个字节地址 add = 0x123456
SPI2_SendData((add&0xFF0000)>> 16);
SPI2_SendData((add&0x00FF00) >> 8);
SPI2_SendData((add&0x0000FF));
while(len--)
{
SPI2_SendData(*data);
data++;
}
//5,拉高CS
GPIO_SetBits(GPIOB,GPIO_Pin_12);
//6,等待操作完成
Flash_WaitForWriteEnd();
}
5.FLASH读操作
03H:读取操作,先拉低CS、发送03H、发送要读取数据的地址,然后主机发送任意数据,置换出来FLASH中的数据
读数据步骤:
1.拉低CS,发送03指令。
2.发送3字节读取的地址。
3.随便发送1个字节的数据,返回值就是要获取的数据。
4.拉高CS。
/*****************************************************************
*函 数 名 称:Flash_ReadData
*函 数 功 能:FLASH读操作
*函 数 形 参:add:读取地址 data:保存读取数据 len:读取数据长度
*函 数 返 回:无
*******************************************************************/
void Flash_ReadData(uint32_t add, uint8_t *data,uint16_t len)
{
//1,拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
//2,发送命令
SPI2_SendData(0x03);
//3,发送三个字节地址 add = 0x123456
SPI2_SendData((add&0xFF0000)>> 16);
SPI2_SendData((add&0x00FF00) >> 8);
SPI2_SendData((add&0x0000FF));
while(len--)
{
*data = SPI2_SendData(0x55);
data++;
}
//4,拉高CS
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
6.FLASH扇区擦除
擦除一个扇区步骤:
1.擦除操作实际上也是写入操作,是往Flash中写入0xFF。所以此时也要先开写使能。
2.拉低CS,发送指令20。
3.发送三个字节。
4.拉高电平,等待操作完成
/*****************************************************************
*函 数 名 称:Flash_SectorErase
*函 数 功 能:FLASH扇区擦除
*函 数 形 参:add:读取地址
*函 数 返 回:无
*******************************************************************/
void Flash_SectorErase(uint32_t add)
{
//先写使能
Write_Enable();
//1,拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
//2,发送命令
SPI2_SendData(0x20);
//3,发送三个字节地址 add = 0x123456
SPI2_SendData(add >> 16);
SPI2_SendData((add&0x00FF00) >> 8);
SPI2_SendData((add&0x0000FF));
//4,拉高CS
GPIO_SetBits(GPIOB,GPIO_Pin_12);
//5,等待忙标志变0
Flash_WaitForWriteEnd();
}
7.读取设备ID
9FH:
读取设备ID;W25Q64固定ID为0xEF4017
/*****************************************************************
*函 数 名 称:Flash_ReadID
*函 数 功 能:FLASH读取ID
*函 数 形 参:无
*函 数 返 回:无
*作 者:CYM
*修 改 日 期:xx/xx/xx
*******************************************************************/
void Flash_ReadID(void)
{
uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
//1,拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
SPI2_SendData(0x9F);
Temp0 = SPI2_SendData(0x55);
Temp1 = SPI2_SendData(0x55);
Temp2 = SPI2_SendData(0x55);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
printf("FLASH_ID:%x\r\n",Temp);
}
8.全片擦除
0x60:每次擦除一片
/*****************************************************************
*函 数 名 称:Flash_SectorAll
*函 数 功 能:FLASH整个芯片擦除
*函 数 形 参:add:读取地址
*函 数 返 回:无
*作 者:QXJ
*修 改 日 期:xx/xx/xx
*******************************************************************/
void Flash_SectorAll(void)
{
//先写使能
Write_Enable();
//等待忙标志变0
//Flash_WaitForWriteEnd();
//拉低CS
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
//发送命令
SPI2_SendData(0x60);
//拉高CS
GPIO_SetBits(GPIOB,GPIO_Pin_12);
//等待忙标志变0
Flash_WaitForWriteEnd();
}
9.写入任意行
/*****************************************************************
*函 数 名 称:sFLASH_WriteBuffer
*函 数 功 能:FLASH写任意行
*函 数 返 回:无
*******************************************************************/
void sFLASH_WriteBuffer(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
Addr = WriteAddr % 256;
count = 256 - Addr;
NumOfPage = NumByteToWrite / 256;//写多少页
NumOfSingle = NumByteToWrite % 256;//写满多少页后剩余多少字节
if (Addr == 0) //从一页的头开始写
{
if (NumOfPage == 0)//写入的数据小于256
{
Flash_PageProgram(WriteAddr,pBuffer, NumByteToWrite);
}
else //一页写不完,需要换页
{
while (NumOfPage--)//循环写入整页数据
{
Flash_PageProgram(WriteAddr,pBuffer, 256);//写一页操作
WriteAddr += 256;//地址偏移256个
pBuffer += 256;//数据偏移256个
}
Flash_PageProgram(WriteAddr,pBuffer, NumOfSingle);
}
}
else //从一页中间开始写
{
if (NumOfPage == 0) /*写入数据小于256 */
{
if (NumOfSingle > count) /*!< (NumByteToWrite + WriteAddr) > sFLASH_PAGESIZE */
{
temp = NumOfSingle - count;//计算把第一页写入完还剩余多少
Flash_PageProgram(WriteAddr,pBuffer, count);//先写入当前页剩余空间
WriteAddr += count;
pBuffer += count;
Flash_PageProgram(WriteAddr,pBuffer, temp);
}
else//剩余空间大于要写入的数据,直接写
{
Flash_PageProgram(WriteAddr,pBuffer, NumByteToWrite);
}
}
else //要写入数据大于256
{
NumByteToWrite -= count;//计算写完第一页还有多少字节
NumOfPage = NumByteToWrite / 256;//写完第一页还要写多少页
NumOfSingle = NumByteToWrite % 256;//写满每一页还剩多少字节
Flash_PageProgram(WriteAddr,pBuffer, count);//先写当前页地址空间
WriteAddr += count;
pBuffer += count;
while (NumOfPage--)
{
Flash_PageProgram(WriteAddr,pBuffer, 256);
WriteAddr += 256;
pBuffer += 256;
}
if (NumOfSingle != 0)
{
Flash_PageProgram(WriteAddr,pBuffer, NumOfSingle);
}
}
}
}