嵌入式stm32 复习(工作用)— SPI协议原理知识 2020.3.12
添加链接描述
SPI.c部分
#include "spi2.h"
/**
* @brief SPI硬件初始化
* @param None
* @retval None
*/
void SPI2_Init(void) {
//1.时钟
RCC->APB2ENR |= 1 <APB1ENR |= 1 <CRH &= ~(0x0F <CRH |= 0x0B <CRH &= ~(0x0F <CRH |= 0x08 <CRH &= ~(0x0F <CRH |= 0x0B <CR1 = 0;
//3.1 配置波特率
SPI2->CR1 &= ~(0x07 <CR1 &= ~(1 <CR1 &= ~(1 <CR1 &= ~(1 <CR1 &= ~(1 <CR1 |= 1 <CR2 |= 1 <CR1 |= 1 <SR & 0x02))
;
SPI2->DR = data;
//2.判断RXNE是否为1
while (!(SPI2->SR & 0x01))
;
return SPI2->DR;
}
SPI.h部分
#ifndef INC_SPI2_H_
#define INC_SPI2_H_
#include "ext.h"
void SPI2_Init(void);
u8 SPI2_WR_Byte(u8 data);
#endif /* INC_SPI2_H_ */
w25qxx.c部分
#include "w25qxx.h"
#include "spi2.h"
#include "stdlib.h"
#include "string.h"
#define W25QXX_CS PBout(12) //W25QXX片选信号线
/**
* @brief Flash 的初始化
* @param None
* @retval None
*/
void W25QXX_Init(void) {
//关于库函数的GPIO配置,这里课上不再重复讲解
GPIOB->CRH &= ~(0x0F <CRH |= 0x03 << 16; //PB12(CS/NSS),推免输出
W25QXX_CS = 1; //CS片选拉高
SPI2_Init(); //初始化SPI2
}
/**
* @brief 测试读取W25QXX的Manufacturer / Device ID
* @param None
* @retval Manufacturer / Device ID
*/
u16 W25QXX_GetMDID(void) {
u16 tmp = 0;
//1.CS拉低,表示开始
W25QXX_CS = 0;
//2.数据操作
SPI2_WR_Byte(0x90); //发设备指令 0x90
SPI2_WR_Byte(0x00); //连续发送三次0x00
SPI2_WR_Byte(0x00);
SPI2_WR_Byte(0x00);
tmp |= SPI2_WR_Byte(0x00) <> 16));
SPI2_WR_Byte((u8) (addr >> 8));
SPI2_WR_Byte((u8) (addr >> 0));
SPI2_WR_Byte(0xFF); //假读
for (i = 0; i > 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
W25QXX_WaiteBusy();
}
/**
* @brief W25QXX 擦除Block (32KB)
* @param None
* @retval None
*/
void W25QXX_BlockErase32(u32 addr) {
W25QXX_WriteEnble(); //必须先发写使能
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x52); //Block Erase (32KB)
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
W25QXX_WaiteBusy();
}
/**
* @brief W25QXX Block (64KB)
* @param None
* @retval None
*/
void W25QXX_BlockErase64(u32 addr) {
W25QXX_WriteEnble(); //必须先发写使能
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0xD8); //Block Erase (64KB)
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
W25QXX_WaiteBusy();
}
/**
* @brief W25QXX Power-down
* @param None
* @retval None
*/
void W25QXX_PowerDown(void) {
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0xB9); //Power-down
W25QXX_CS = 1; //3.CS拉高,表示结束
}
/**
* @brief W25QXX 写Page
* @param None
* @retval None
*/
void W25QXX_WritePage(u32 addr, u8* pData, u16 len) {
u16 i = 0;
W25QXX_WriteEnble(); //先WriteEnable
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x02); //Page Program 命令
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
for (i = 0; i = len)
last = len;
while (1) {
W25QXX_WritePage(addr, pBuffer, last);
if (last == len)
break;
else {
pBuffer += last;
addr += last;
len -= last;
if (len > 256)
last = 256;
else
last = len;
}
}
}
u8 W25QXX_BUF[4096] = { 0 }; //如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存
/**
* @brief W25QXX 写任意数据
* @param addr:数据地址;pData:要写的数据首地址;len:要写数据的有效长度。
* @retval None
*/
void W25QXX_WriteMutiBytes(u32 addr, u8* pData, u16 len) {
u16 offset = addr % 4096;
u16 last = 4096 - offset;
u16 n = addr / 4096;
u8* buffer = W25QXX_BUF; //malloc(4096);
if (last >= len)
last = len;
while (1) {
u16 i = 0;
//1.先拷贝数据
W25QXX_ReadMutiBytes(n * 4096, buffer, 4096);
for (i = 0; i < 4096; ++i) {
if (buffer[i] != 0xFF)
break;
}
//如果当有扇区不需要擦除时,写入速度明显提升,可以在进行大容量拷贝时,先将整个Flash擦除后再写入
if (i != 4096) {
//2.擦除整个扇区
W25QXX_SectorErase(n * 4096);
}
//3.替换数据(使用C标准库的速度和下面循环的速度大致差不多)
strncpy((char *) (buffer + offset), (const char *) pData, last);
//等同于下面步骤
// for (i = 0; i 4096)
last = 4096;
else
last = len;
}
// free(buffer);
}
w25qxx.h部分
#ifndef INC_W25QXX_H_
#define INC_W25QXX_H_
#include "ext.h"
void W25QXX_Init(void);
u16 W25QXX_GetMDID(void);
void W25QXX_ReadMutiBytes(u32 addr, u8* pData, u16 len);
void W25QXX_WriteMutiBytes(u32 addr, u8* pData, u16 len);
void W25QXX_Write(u8* pBuffer, u32 address, u16 len);
void W25QXX_ChipErase(void);
#endif /* INC_W25QXX_H_ */
好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!
SPI.c部分
SPI.c初始化部分
//1.时钟
RCC->APB2ENR |= 1 <APB1ENR |= 1 << 14;//SPI2时钟
////因为这里stm32与flash通讯的接口是GPIOB端口,所以需要开启GPIOB的时钟和SPI2的时钟。
//
////
//由前几篇的IIC协议文章中设置一样,所以这里就不再赘述。
//2.GPIO
GPIOB->CRH &= ~(0x0F <CRH |= 0x0B << 20;//PB13(CLK),推免输出,复用
//
//因为SCK端口是PB13,所以位于端口配置高寄存器。
//由上一篇文章的SPI协议可以得知:SCK是由波特率产生器产生的的,是产生固定时钟周期的。,所以这个端口需要设置成复用推免输出模式,则设置这四位为1011,运算如下:
//但是首先先将这四位清零,再赋值。
GPIOB->CRH &= ~(0x0F <CRH |= 0x08 << 24;//PB14(MISO),上拉或下拉输入
//由上篇文章可以得知:MISO:只能由从设备向主设备发数据,所以这里将MISO引脚设置成上拉或者下拉输入。
//
GPIOB->CRH &= ~(0x0F <CRH |= 0x0B << 28;//PB15(MOSI),推免输出,复用
//由上篇文章可以得知:MOSI:只能由主设备向从设备发数据,所以这里将MOSI引脚设置成推免输出,复用。
//
//3.SPI
//3.1 配置波特率
SPI2->CR1 &= ~(0x07 << 3);//选择Fpcl/2时钟频率
//
//
//从上图可以得知,波特率控制是由fPCLK决定的。
//所以我们就得知道fPCLK是多大频率。
//
//所以这里fPCLK为36MHz,所以SPI2的最大波特率是18MHz。这里设置最大,即BR[2:0]这三位全是0,那么设置的时候先全置1,然后再取反。
//
//3.2 选择时钟极性
SPI2->CR1 &= ~(1 <CR1 &= ~(1 << 0);//选择第一个时钟边沿
//由上一篇文章我们可以知道有两种时钟空闲状态。
//高电平空闲:是从低电平到高电平,发送或者接收数据。
//低电平空闲:是从高电平到低电平,发送或者接收数据。
//那么我们这里采用的是低电平空闲,也就相当于电平从低电平变成高电平时,才会传输数据。
//
////从图可以得知,数据采样从第二个时钟边沿开始。
//3.3 选择数据传输长度
SPI2->CR1 &= ~(1 << 11);//8bit数据长度
//
//一般我们传输数据时是以8位数据格式进行发送或者接收的,并且是第11位。
//3.4 配置 LSBFIRST 帧格式
SPI2->CR1 &= ~(1 << 7);//MSB开始,高位在前
//
//举个例子,如上图所示,传输数据的时候总是高位在前。
//
//3.5 配置MSTR
SPI2->CR1 |= 1 << 2;//配置SPI为主设备
//3.6 配置硬件管理CS(NSS)
SPI2->CR2 |= 1 << 2;
//4.使能 SPE
SPI2->CR1 |= 1 << 6;//使能SPI
//
SPI.c部分
SPI2数据读和写部分
//1.先判断TxE是否为1
while (!(SPI2->SR & 0x02));
////由上图我们可以知道,只有当TXE标志被置位时,数据才会从发送缓冲区到移位寄存器,所以这里先判断TxE是否为1。
//////
SPI2->DR = data;
//当TXE置1时,才开始发数据。
//
//2.判断RXNE是否为1
while (!(SPI2->SR & 0x01));
//// 传送移位寄存器里的数据到接收缓冲器,并且RXNE标志被置位,也就相当于RXNE位为1时,传送移位寄存器的数据才能到接收寄存器中。
//
//
//所以这里就相当于主设备向从设备发数据的过程中,主设备同时也在接收从设备发过来的数据,还记不记得我们在关于IIC协议文章讲到的IIC协议通讯是半双工通信,发送或者接收数据只能单方向,但是SPI协议就不是半双工通讯方式了,而是全双工通讯,又因为IIC传输数据只在一根线上传输,但是SPI传输数据是在两条线上传输的,所以这也是为什么SPI传输数据的速度比IIC传输数据快的原因了!!!
w25qxx.c部分
w25qxx(Flash)初始化部分
GPIOB->CRH &= ~(0x0F <CRH |= 0x03 << 16; //PB12(CS/NSS),推免输出
W25QXX_CS = 1; //CS片选拉高
SPI2_Init(); //初始化SPI2
////因为在每次通讯的时候,刚开始CS片选信号线电平状态总是从高电平变到低电平,所以这也是我们为什么设置CS片选信号不是在SPI初始化里面设置,而是在SPI初始化之前进行设置的,并且将CS片选信号线电平状态置1。
//设置CS片选信号的时候也是先清零然后再赋值。
w25qxx.c部分
w25qxx( 测试读取W25QXX的Manufacturer / Device ID)部分
#define W25QXX_CS PBout(12) //W25QXX片选信号线
//因为已知CS片选信号线是PB12,并且是输出模式,这个操作相当于与IIC协议中SCL,SDA信号线的电平状态设置相同,也是采用宏定义的方式。
u16 tmp = 0;
//1.CS拉低,表示开始
W25QXX_CS = 0;
//这里就是将CS片选信号线拉低,这样才能开始传输数据。相当于IIC协议中的起始条件和结束条件。
//2.数据操作
SPI2_WR_Byte(0x90); //发设备指令 0x90
SPI2_WR_Byte(0x00); //连续发送三次0x00
SPI2_WR_Byte(0x00);
SPI2_WR_Byte(0x00);
tmp |= SPI2_WR_Byte(0x00) << 8; //始终时0xEF
tmp |= SPI2_WR_Byte(0x00); //Device ID
//3.CS拉高,表示结束
W25QXX_CS = 1;
////由上图可知首次CS片选信号线拉低,开始传输数据,然后主设备开始向从设备发送0x90,同时没有接收到数据,然后再发送24位数据,分别是3次8位数据,都是0x00,注意是先发最高位,然后再随机发送一个数据,0x00也行,然后就接收到一个制造商ID地址0xEF数据,再然后就能接收到正确的设备ID了。
//发送和接收数据是同步进行的。
//因为tmp数据是16位的,同时接收制造商ID和设备ID地址,且制造商ID是高8位,设备ID地址是低8位。
//最后再片选拉高!!!
w25qxx.c部分
W25QXX读取len个数据长度(快读方式)部分
u16 i = 0;
//1.CS拉低,表示开始
W25QXX_CS = 0;
//跟往常一样,片选拉低
//2.数据操作
SPI2_WR_Byte(0x0B); //发读数据命令(Fast Read)
//连续发送三次0x00
SPI2_WR_Byte((u8) (addr >> 16));
SPI2_WR_Byte((u8) (addr >> 8));
SPI2_WR_Byte((u8) (addr >> 0));
//
//由上图可知,先把CS片选信号线拉低,然后开始传输数据,主设备开始向从设备发送0x0B,然后继续发24位数据,此时没有成功接收数据,然后再等待8个时钟周期,最后主设备随机发送数据到从设备,此时就能成功接收数据并且是一直能够接收,直到数据接收完成。
////然后依次类推,成功发送24位数据到从设备。
SPI2_WR_Byte(0xFF); //假读
//然后再等待8个时钟周期,相当于发送8位数据,只不过是接收不到数据的。
for (i = 0; i < len; i++) {
pData[i] = SPI2_WR_Byte(0xFF); //读数据
}
//然后就可以成功接收数据了,这里就是一个for循环。
//指针就是数组,数组就是指针。
//3.CS拉高,表示结束
W25QXX_CS = 1;
//CS拉高,表示结束
w25qxx.c部分
W25QXX写page部分
//W25Q80/16/32阵列被组织成4,096/8,192/16,384个可编程页,每个页256字节。
//使用Page程序指令一次最多可编程256个字节。可以以16组(扇区擦除)、128组(32KB块擦除)、256组(64KB块擦除)或整个芯片(芯片擦除)的方式擦除页面。
//W25Q80/16/32有256/512/1024个可擦扇区和16/32/64个可擦块。
////不同型号的w25qxx主要区别就在于页数不同,比如w25q80有16个block,w25q16有32个block,w25q32有64个block。
//Flash内存结构中最小单元是page(页),所以进行主设备写数据到从设备的话,需要进行页操作。
//页程序指令允许从一个字节到256字节(一页)的数据在先前擦除(FFh)的内存位置被编程。
//写使能指令必须在设备接收擦除指令之前执行(状态寄存器位WEL= 1)。
//如果要对整个256字节的页面进行编程,则最后一个地址字节(8个最不重要的地址位)应该设置为0。
//如果最后一个地址字节不为零,并且时钟的数量超过了剩余的页面长度,则寻址将自动换行到页面的开头。
//在某些情况下,小于256字节(部分页面)可以在不影响同一页面内的其他字节的情况下进行编程。
w25qxx.c部分
W25QXX写使能部分
//既然我们写数据的时候会用到写使能和读取状态等函数,那么我们就在写数据之前先把这几个函数封装好。
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x06); //Write Enable
W25QXX_CS = 1; //3.CS拉高,表示结束
//首先我们还是先把CS拉低,表示开始,然后主设备发送一个指令到从设备,代表已经成功写使能,最后CS拉高,表示结束。
//
w25qxx.c部分
W25QXX写不使能部分
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x04); //Write Disable
W25QXX_CS = 1; //3.CS拉高,表示结束
////和写使能原理一样,只是主设备向从设备发送的数据不同罢了。
w25qxx.c部分
W25QXX读取状态寄存器部分
//
//BUSY是状态寄存器(S0)中的一个只读位,它在设备执行a时被设置为1状态
页程序,扇区擦除,块擦除,芯片擦除或写状态寄存器指令。
//当程序、擦除或写状态寄存器指令完成后,忙位将被清除为0状态,表示设备已准备好接受进一步的指令。
//
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x05); //读Reg1命令
tmp = SPI2_WR_Byte(0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
//写使能指令将状态寄存器中的写使能锁存器(WEL)位设置为1
w25qxx.c部分
W25QXX等待忙碌状态部分
while (W25QXX_ReadReg1() & 0x01);
//这里就是如果是忙碌状态未完成,那么busy是1,那么上述操作就是会一直在while循环里,直到忙碌状态完成。
w25qxx.c部分
W25QXX擦除整个芯片
W25QXX_WriteEnble(); //必须先发写使能
//一个写使能指令必须在设备接受芯片擦除指令之前执行(状态)。
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0xC7); //擦除整个芯片
W25QXX_CS = 1; //3.CS拉高,表示结束
//这里就不再赘述。
W25QXX_WaiteBusy();
//等待擦除命令完成。
w25qxx.c部分
W25QXX擦除扇区
W25QXX_WriteEnble(); //必须先发写使能
//还是在执行擦除命令之前先进行写使能
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x20); //Sector Erase
//发地址
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
//这个发地址就相当于是只取8位数据,然后发3遍,总共24位。
W25QXX_WaiteBusy();
//然后等待擦除命令完成
w25qxx.c部分
W25QXX擦除块(32KB)
W25QXX_WriteEnble(); //必须先发写使能
//还是在执行擦除命令之前先进行写使能
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x52); //Block Erase (32KB)
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
//这个发地址就相当于是只取8位数据,然后发3遍,总共24位。
W25QXX_WaiteBusy();
//然后等待擦除命令完成
w25qxx.c部分
W25QXX擦除块(64KB)
W25QXX_WriteEnble(); //必须先发写使能
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0xD8); //Block Erase (64KB)
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
W25QXX_CS = 1; //3.CS拉高,表示结束
W25QXX_WaiteBusy();
//这里不再赘述。
w25qxx.c部分
W25QXX写Page
void W25QXX_WritePage(u32 addr, u8* pData, u16 len)
//这里的 u16 为什么不是u8类型的呢,主要是u8类型的话,就是0~255,而一页是256个字节,所以这里是u16类型。
//
u16 i = 0;
W25QXX_WriteEnble(); //先WriteEnable
W25QXX_CS = 0; //1.CS拉低,表示开始
SPI2_WR_Byte(0x02); //Page Program 命令
SPI2_WR_Byte((addr >> 16) & 0xFF);
SPI2_WR_Byte((addr >> 8) & 0xFF);
SPI2_WR_Byte((addr >> 0) & 0xFF);
//这里不再赘述。
for (i = 0; i < len; i++) {
SPI2_WR_Byte(pData[i]);
}
//这里就是把需要写入的数据成功写到从设备当中。
W25QXX_CS = 1; //3.CS拉高,表示结束
W25QXX_WaiteBusy();
//这里不再赘述。
w25qxx.c部分
W25QXX写Page(可以在任意位置写任意数据)
//在之前也说过了:如果最后一个地址字节不为零,并且时钟的数量超过了剩余的页面长度,则寻址将自动换行到页面的开头
u8* pBuffer = pData;
//先把所写数据的首地址赋值给新的指针变量,存放地址。
u16 last = 256 - addr % 256;
//获得当前地址所在Page中的位置到Page尾部的剩余大小空间
if (last >= len)
last = len;
while (1) {
W25QXX_WritePage(addr, pBuffer, last);
if (last == len)
break;
else {
pBuffer += last;
addr += last;
len -= last;
if (len > 256)
last = 256;
else
last = len;
//
////看完上这两张图就看清楚了。
w25qxx.c部分
W25QXX写Page(可以在任意位置写任意数据)如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存
u8 W25QXX_BUF[4096] = { 0 };
//如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存
//一般如果写数据之前没有被擦除,相当与里面的数据不全是1的话,那么就先进行擦除,然后再写数据。
//扇区擦除指令将指定扇区(4k字节)内的所有内存设置为所有1 (FFh)的擦除状态。
//
//
u16 offset = addr % 4096;
u16 last = 4096 - offset;
u16 n = addr / 4096;
//如上图所示,n代表前面有多少个扇区。
u8* buffer = W25QXX_BUF; //malloc(4096);
//将缓冲区赋值给新的指针变量。
if (last >= len)
last = len;
//同理,不再赘述。
W25QXX_ReadMutiBytes(n * 4096, buffer, 4096);
//将在扇区中的数据读出来
for (i = 0; i < 4096; ++i) {
if (buffer[i] != 0xFF)
break;
}
//如果读出来原来的扇区里面的数据不都是1,则停止,相当于该扇区需要进行擦除操作。
if (i != 4096) {
//2.擦除整个扇区
W25QXX_SectorErase(n * 4096);
}
//如果当有扇区不需要擦除时,写入速度明显提升,可以在进行大容量拷贝时,先将整个Flash擦除后再写入
//3.替换数据(使用C标准库的速度和下面循环的速度大致差不多)
for (i = 0; i < last; ++i) {
buffer[i + offset] = pData[i];
}
//把需要写的数据放到缓冲区中
//4.回写数据
W25QXX_WritePageEx(n * 4096, buffer, 4096);
//回写数据
if (last == len)
break; //递归终止条件
//这里不再赘述。
n++;
//每次扇区地址向后偏移1
pData += last; //数据指针向后偏移last
offset = 0; //从第二次开始offset变成0
addr += last; //地址向后偏移last
//从第二次开始offset变成0
if (len > 4096)
last = 4096;
else
last = len;
//这里就不再赘述了。
结束语个人认为大家如果细心看完这篇文章,并且结合上一篇文章一起看(在文章的刚开始会将前几篇关于SPI协议原理部分的文章链接发出来),我相信大家会彻底掌握SPI与Flash通讯了!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!
以后我会继续推出关于嵌入式(stm32)的协议方面的讲解,下一讲会推出DMA部分的文章!敬请期待!!!
**我先休息去了~~╭(╯^╰)╮