基于STM32战舰开发板的SPI通信实验(全双工)
SPI通信模块简介
SPI是英语Serial Peripheral interface 的缩写,顾名思义就是串行外围设备接口。是 Motorola首先在其MC68HCXX系列处理器上定义的。SPI 接口主要应用在 EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB 的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,STM32也有SPI接口。
SPI 接口一般使用 的4 条线通信 |
MISO 主设备数据输入,从设备数据输出。 |
MOSI 主设备数据输出,从设备数据输入。 |
SCLK 时钟信号,由主设备产生。 |
CS 从设备片选信号,由主设备控制。 |
SPI中专有名词简介
MISO master input slave output |
主机接收从机发送 |
MOSI master output slave input |
主机发送从机接收 |
CS chip select |
片选 |
SCLK serial clock |
串行时钟 |
SPI工作原理
此图与上图一样,只不过比上图更加详细一些。这里多了一个SSPBUF缓存区,无论是读取还是发送内容,均是对缓存区进行操作。由于我们采用的是两线MISO,MOSI进行数据的双向传输,因此我们只能在MISO或者MOSI上一次传输一个bit的内容。
假如我们要求传输8个bits,那么移位寄存器与缓存区必须配合工作。又由于我们将master与slave的时钟接了相同的时钟,因此master与slave会形成一个闭环工作区,即寄存器通过 MOSI 信号线将字节传送给从机,从机也将自己的移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换,外设的写操作和读操作是同步完成的。
SPI工作状态
如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。
交换前
交换中
交换后
其实,说实话没有读和写的说法,因为实质上每次SPI是主从设备在交换数据。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。读写操作的不同只是我们关注点不同,读操作我们只要slave传给master的那一部分的(也就是传输后master的Buffer中的内容),写操作我们只要master传给slave的那一部分的(也就是传输后slave的Buffer中的内容)。
SPI模块中不同的工作区
总体分区简介
1.通讯引脚
2.时钟控制逻辑
3.数据控制逻辑
4.整体控制逻辑
整体控制逻辑部分
寄存器配置说明
SPI_CR1.RXONLY:只接收(Receive Only)
SPI_CR1.BIDIOE:双向模式下的输出使能 (Output enable in bidirectional mode)
SPI_CR1.BIDIMODE:双向数据模式使能 (Bidirectional data mode enable)
SPI_CR1.SPE:SPI使能 (SPI enable)
SPI_CR1.SSM:软件从设备管理 (Software slave management)
SPI_CR1.SSI:内部从设备选择 (Internal slave select)
总结
1. 整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等。
2. 在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,只要读取状态寄存器相关的寄存器位,就可以了解SPI的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。
3. 实际应用中,一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
通讯引脚部分
STM32芯片有多个SPI外设,它们的SPI通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚,以《STM32F10x中文参考》为准。其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。除了通讯速率,在其它功能上没有差异。
SPI_CR1.BR[2:0]:波特率控制 (Baud rate control)
时钟控制逻辑部分
SCLK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对PCLK时钟的分频因子,对PCLK的分频结果就是SCLK引脚的输出时钟频率。其中的PCLK频率是指SPI所在的APB总线频率,APB1为PCLK1,APB2为PCLK2。
SPI自定义传输模式
结构体成员变量比较多,这里我们挑取几个重要的成员变量讲解一下:
SPI_Direction |
用来设置 SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式,这里我们选择全双工模式 SPI_Direction_2Lines_FullDuplex |
SPI_Mode |
用来设置 SPI 的主从模式,这里我们设置为主机模式 SPI_Mode_Master,当然有需要你也可以选择为从机模式 SPI_Mode_Slave |
SPI_DataSiz |
为 8 位还是 16 位帧格式选择项,这里我们是8位传输,选择SPI_DataSize_8b |
SPI_CPOL |
用来设置时钟极性,我们设置串行同步时钟的空闲状态为高电平所以我们选择 SPI_CPOL_High |
SPI_CPHA |
用来设置时钟相位,也就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集,这里我们选择第二个跳变沿,所以选择 SPI_CPHA_2Edge |
SPI_NSS |
设置NSS信号由硬件(NSS 管脚)还是软件控制,这里我们通过软件控制 NSS 关键,而不是硬件自动控制,所以选择 SPI_NSS_Soft |
SPI_BaudRatePrescaler |
设置 SPI 波特率预分频值也就是决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值,初始化的时候我们选择 256 分频值SPI_BaudRatePrescaler_256, 传输速度为36M 256=140.625KHz |
SPI_FirstBit |
设置数据传输顺序是MSB位在前还是LSB位在前,这里我们选择SPI_FirstBit_MSB 高位在前 |
SPI_CRCPolynomial |
是用来设置 CRC 校验多项式,提高通信可靠性,大于 1 即可 |
SPI中SCLK的传输相位与极性
SCLK的传输相位 |
一个周期内是第一个脉冲沿触发移位寄存器移位数据还是第二个脉冲沿触发移位寄存器移位数据 |
SCLK的极性 |
空闲时SCLK上的电平是低电平还是高电平 |
注:第一个脉冲沿不一定是上升沿,这取决于初始空闲电平的状态。
SPI通道的选择(参考“STM32中文参考手册”)
MSB(最高有效位)与LSB(最低有效位)
我们在这个图中看到:NSS寄存器也就是片选引脚被拉低,这说明主从设备之间可以进行正常的通信,且这里我们设置“SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始”,那么MISO与MOSI之间的数据传输方向是”MISO的最高位->MOSI的最低位;MOSI的最高位->MISO的最低位”。
从选择(NSS)引脚管理(相当于片选端)
① 有2种NSS模式:
软件NSS模式:可以通过设置SPI_CR1寄存器的SSM位来使能这种模式(见图211)。在这种
模式下NSS引脚可以用作它用,而内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动。
② 硬件NSS模式,分两种情况:
A. NSS输出被使能:当STM32F10xxx工作为主SPI,并且NSS输出已经通过SPI_CR2寄存
器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并
配置为硬件NSS的SPI设备,将自动变成从SPI设备。
当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它的设备它是主
设备;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个
硬件失败错误(Hard Fault)。
B. NSS输出被关闭:允许操作于多主环境。
注:软件NSS与硬件NSS的配置详见“STM32中文参考手册->从选择(NSS)脚管理”。
NSS不同模式寄存器配置简介
SSM:软件从设备管理 (Software slave management)
当SSM被置位时,NSS引脚上的电平由SSI位的值决定。
0:禁止软件从设备管理;
1:启用软件从设备管理。
注:I2S模式下不使用。
SSI:内部从设备选择 (Internal slave select)
该位只在SSM位为’1’时有意义。它决定了NSS上的电平,在NSS引脚上的I/O操作无效。 注:I2S模式下不使用。
NSS两种不同模式的不同之处
1. 硬件NSS,是指SPI自动控制SPI的片选信号,发送数据的时候,输出低电平,不发送的时候,是高电平,这个模式一般不用.因为这种方式只能1个SPI接1个从机,很是蛋疼.
2. 软件模式就是完全软件控制SPI片选,就是一个普通IO控制,你要SPI通信之前,必须先用软件的方式,控制SPI从机的片选为低电平,然后在发送数据.发完后,拉高. 一般用这个模式,因为可以一个SPI控制N多个从机...
基于SPI固件库的正点原子封装库简介
(将STM32开发板配置成主机模式,可以访问SD Card/W25Q64/NRF24L01)
以下几个函数是正点原子根据固件库所配置的常用函数:
SPI初始化函数
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能
RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );//SPI2时钟使能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB
GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15); //PB13/14/15上拉
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小:SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
SPI_Cmd(SPI2, ENABLE); //使能SPI外设
SPI2_ReadWriteByte(0xff);//启动传输
}
我们这里有些疑问的是最后一句语句,这个语句其实并不难理解,就是发送给从机一个0xFF数据吗!但是为什么发送呢?我们联想一下,SPI是一个闭环通信,只有主机发送给从机规定位数的数据从机才可以将相同数量的位发送给主机,这样就实现了主机与从机的数据传输。这里,我们给外设发送一个0xFF无效字节,然后接收外围设备发送的字节。其实,我们预先读取一个字节是为了读取dummy无效字节,这样在后续的数据读取中我们读到的字节均为有效字节。
我们看上述代码,正点原子封装的SPI2_Init()相当于固件库的SPI_Init()+SPI_I2S_ReceiveData。
读操作与写操作
//SPIx 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI2_ReadWriteByte(u8 TxData)
{
u8 retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
{
retry++;
if(retry>200)return 0;
}
SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个数据
retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位
{
retry++;
if(retry>200)return 0;
}
return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据
}
我们可以这样理解这段程序:
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
{
retry++;
if(retry>200)return 0;
}
这段程序用于等待SPI2两端的主从设备是否已经建立联系,如果没有成功建立联系,最多轮询200次,如果还没成功建立联系就不必要在建立联系了,直接向SPI2的slave设备发送信息。
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位
{
retry++;
if(retry>200)return 0;
}
return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据
再次等待200次,看看发送的信息是否已经被slave接收到,如果还没有,返回最近一次master设备接收的信息。
其实,这里的while循环也有一个延时的作用,我们发送的字节由于要通过串行通信进行传输,因此我们必须等待一小会时间才可以。
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)
{
/* Check the parameters */
assert_param(IS_SPI_ALL_PERIPH(SPIx));
/* Write in the DR register the data to be sent */
SPIx->DR = Data;
}
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)
{
/* Check the parameters */
assert_param(IS_SPI_ALL_PERIPH(SPIx));
/* Return the data in the DR register */
return SPIx->DR;
}
我们可以看到无论是SPI_I2S_ReceiveData还是SPI_I2S_SendData函数都是操作的master(STM32上的SPI设备)的DR寄存器,再有我们之前提到过的SPI闭环通信,我们可以得知:当我们传输数据的同时也在接收数据,最终相当于将slave(外围设备)寄存器中的内容更新至master(STM32的SPI通信设备)寄存器中。
速度设置函数
//SPI 速度设置函数
//SpeedSet:
//SPI_BaudRatePrescaler_2 2分频
//SPI_BaudRatePrescaler_8 8分频
//SPI_BaudRatePrescaler_16 16分频
//SPI_BaudRatePrescaler_256 256分频
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
SPI2->CR1&=0XFFC7; // 清除SPI2的CR1寄存器的BR[2:0]
SPI2->CR1|=SPI_BaudRatePrescaler; //设置SPI2的CR1寄存器的BR[2:0]分频系数
SPI_Cmd(SPI2,ENABLE); // 使能SPIx
}
固件库的库函数中没有可以设置SPIx数据发送速度的函数,我们可以仿造上述函数的编写逻辑自己编写一个控制SPIx数据发送速度的函数。
注:当SPI内部设备为master,外设为slave时,我们可以设置master向slave发送数据的频率;但是当SPI内部设备为slave,外设为master时,master接收数据的速度只能取决于slave发送的速度,此时我们在设置master的发送数据的频率是没有用的。
常用SPI固件库库函数解析
初始化SPI函数 |
SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct) |
SPI使能函数 |
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState) |
SPI中断配置函数 |
void SPI_ITConfig(SPI_TypeDef* SPIx, u16 SPI_IT, FunctionalState NewState) |
SPI发送数据函数 |
void SPI_SendData(SPI_TypeDef* SPIx, u16 Data) |
SPI接收数据函数 |
u16 SPI_ReceiveData(SPI_TypeDef* SPIx) |
NSS模式选择函数 void |
SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, u16 SPI_NSSInternalSoft) |
SPI数据大小设置函数 |
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, u16 SPI_DatSize) |
获得SPI状态标志位函数 |
FlagStatus SPI_GetFlagStatus(SPI_TypeDef* SPIx, u16 SPI_FLAG) |
清除指定的状态标志位函数 |
void SPI_ClearFlag(SPI_TypeDef* SPIx, u16 SPI_FLAG) |
获得指定的中断标志位状态函数 |
ITStatus SPI_GetITStatus(SPI_TypeDef* SPIx, u8 SPI_IT) |
清除SPI中断标志位函数 |
void SPI_ClearITPendingBit(SPI_TypeDef* SPIx, u8 SPI_IT) |
SPI固件库库函数配置的注意事项
初始化SPI之前GPIO口模式的选择
SPIx对应的GPIO端口的引脚
SPI2对应的GPIO模式配置示例
GPIO_InitTyprDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB
常用SPI固件库函数参数配置(顺序如上)
初始化SPI函数参数
初始化SPI函数参数配置示例
/* Initialize the SPI1 according to the SPI_InitStructure members */
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 外设为从设备,内设为主设备
SPI_InitStructure.SPI_DatSize = SPI_DatSize_16b; // 传输容量为16bits
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 空闲电平为低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 第二个脉冲沿触发移位寄存器
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // NSS为软件控制
SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_128; // 128分频:36MHz/128=281250Hz,这是STM32中的SPI设备向外设发送数据的频率
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 从高位开始移位
SPI_InitStructure.SPI_CRCPolynomial = 7; // 设置CRC多项式的值
SPI_Init(SPI1, &SPI_InitStructure);
SPI中断配置函数参数
SPI可触发中断类型如上。
SPI的NSS引脚模式设置函数参数
第一个参数 |
NSS引脚由普通GPIO引脚代替,也就是说我们不用硬件自带的片选CS引脚,我们自己用普通GPIO口配置片选端; |
第二个参数 |
使用硬件上自带的NSS片选引脚。 |
注:软件与硬件的区别,详见“从选择(NSS)引脚管理(相当于片选段)”
获得SPI状态标志位函数参数
获得指定的中断标志位状态函数参数
SPI固件库库函数配置流程
步骤 |
库函数 |
配置相关引脚的复用功能,使能SPIx时钟 |
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); |
初始化SPIx,设置SPIx工作模式 |
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct); |
使能SPIx |
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState); |
SPI传输数据 |
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data); uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ; |
查看SPI传输状态 |
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE); |
W25Q128 配置原理简介
W25Q128将16M(8M)的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。W25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。
配置的基本流程
W25Q128内存地址映射
我们看如上地址映射关系可以得出:
共有256个块(64KB),每个块共有16个扇区(4KB),因此flash最小的的内存擦除单元为一个扇区(4K)。
基于正点原子的W25Q128库函数解析
W25Q128 Flash初始化函数 |
void W25QXX_Init(void) |
读取W25Qxx的ID的函数 |
u16 W25QXX_ReadID(void) |
读取W25Q128的状态的函数 |
u8 W25QXX_ReadSR(void) |
向W25Q128写入指令的函数 |
void W25QXX_Write_SR(u8 sr) |
写数据/指令操作使能函数 |
void W25QXX_Write_Enable(void) |
写数据/指令操作失能函数 |
void W25QXX_Write_Disable(void) |
写数据函数 |
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) |
写数据函数 |
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) |
在一页中写入数据的函数 |
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) |
读数据函数 |
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) |
擦除整个芯片数据的函数 (时间太长) |
void W25QXX_Erase_Chip(void) |
擦除一个扇区数据的函数 (时间约为150ms左右) |
void W25QXX_Erase_Sector(u32 Dst_Addr) |
等待芯片空闲下来/所有命令执行完毕的函数 |
void W25QXX_Wait_Busy(void) |
Flash芯片进入掉电模式(休眠模式)的函数 |
void W25QXX_PowerDown(void) |
将flash芯片从掉电模式唤醒的函数 |
void W25QXX_WAKEUP(void) |
读数据函数参数参数
pBuffer |
数据存储区 |
ReadAddr |
开始读取的地址(24bit) |
NumByteToRead |
要读取的字节数(最大65535) |
写数据①函数参数
pBuffer |
数据存储区 |
WriteAddr |
开始写入的地址(24bit) |
NumByteToWrite |
要写入的字节数(最大65535) |
写数据②函数参数
pBuffer |
数据存储区 |
WriteAddr |
开始写入的地址(24bit) |
NumByteToWrite |
要写入的字节数(最大65535) |
在一页中写入数据的函数参数
pBuffer |
数据存储区 |
WriteAddr |
开始写入的地址(24bit) |
NumByteToWrite |
要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!! |
擦除一个扇区内数据的函数参数
Dst_Addr |
扇区地址 根据实际容量设置 |
两个写操作函数的区别
W25QXX_Write |
把原有的数据读出来在修改原来扇区内的数据 |
W25QXX_Write_NoCheck |
是直接写入覆盖原有的数据(没有扇区的概念,直接写入全部数据) |
ID读取函数的返回值
#define W25Q80 0XEF13
#define W25Q16 0XEF14
#define W25Q32 0XEF15
#define W25Q64 0XEF16
#define W25Q128 0XEF17
// 用于判断是什么类型的flash芯片
W25128的指令表
//指令表
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
如何使用SPI的NSS片选引脚实现SPI与W25Q128的连接?
我们可以仔细的分析W25Q128初始化函数:
//初始化SPI FLASH的IO口
void W25QXX_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; // PB12 推挽
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_12); // 将PB12设置为NSS软件模式下的片选引脚
W25QXX_CS=1; //高电平代表FLASH不被选中(相当于PBout(12)=1)
SPI2_Init(); //初始化SPI
SPI2_SetSpeed(SPI_BaudRatePrescaler_2); //设置为36MHz/2=18MHz时钟,高速模式
W25QXX_TYPE=W25QXX_ReadID();//读取FLASH ID
}
预定义命令:
#define W25QXX_CS PBout(12) //W25QXX的片选信号
我们可能有疑问:为什么片选端都关闭了(W25QXX_CS=1)还能成功读取到Flash的ID呢?
我们可以看ID读取函数的内容:
//读取芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q32
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
u16 W25QXX_ReadID(void)
{
u16 Temp = 0;
W25QXX_CS=0; // 使能片选位
SPI2_ReadWriteByte(0x90);//发送读取ID命令
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
Temp|=SPI2_ReadWriteByte(0xFF)<<8; // 使用移位寄存器将ID的高8位移入Temp变量的高8位中
Temp|=SPI2_ReadWriteByte(0xFF); // 使用移位寄存器将ID的低8位移入Temp变量的低8位中
W25QXX_CS=1; // 失能片选位
return Temp; // 返回16位的ID
}
// 注:由于SPI是闭环通信,因此当我们向W25Q128写入8位数据,W25Q128就会向STM32的内部SPI发送8位数据,这样就达到了SPI内部设备读取W25Q128内容的目的。
一般我们对于W25Q128 flash芯片操作有如下流程:
基于STM32的SPI内部设备与W25Q128 Flash的通信实验
实验的主要原理
其实,以上是我们更新扇区的操作流程,但是我们要知道SPI通信是个闭环通信,因此我们读出扇区数据的时候,实际上已经将4*1024个0xFF写入扇区当中相当于清空了一个扇区。
SPI通信库函数操作流程
SPI通信实验代码示例
#include "w25qxx.h"
#include "lcd.h"
#include "spi.h"
#include "delay.h"
#include "usart.h"
#include "stm32f10x.h"
u8 String[] = "Hello World";
const u8 Size = sizeof(String);
int main()
{
u8 Buffer[Size];
memset(Buffer,0,Size);
delay_init(); // 延迟函数初始化
uart_init(115200); // 串口1初始化(TFTLCD初始化一定要伴随着串口的初始化)
LCD_Init(); // TFTLCD初始化
LCD_Clear(LIGHTBLUE); // TFTLCD页面为浅蓝色
SPI2_Init(); // SPI2初始化
W25QXX_Init(); // W25Q128初始化
while(1)
{
POINT_COLOR = RED; // 画笔为红色
while(W25QXX_ReadID() != W25Q128)
{
LCD_ShowString(0,0,200,16,16,(u8*)"W25Q128 Check Failed");
delay_ms(500);
}
LCD_ShowString(0,16,200,16,16,(u8*)"W25Q128 Ready");
POINT_COLOR = BLUE; // 画笔为蓝色
LCD_ShowString(0,32,200,16,16,(u8*)"Start to Write...");
W25QXX_Write(String,0,Size); // 写数据
LCD_ShowString(0,48,200,16,16,(u8*)"Finish Writing");
POINT_COLOR = BROWN; // 画笔为棕色
LCD_ShowString(0,64,200,16,16,(u8*)"Start to Read...");
W25QXX_Read(Buffer,0,Size);
LCD_ShowString(0,80,200,16,16,(u8*)"Finish Reading");
LCD_ShowString(0,96,200,16,16,(u8*)"Start to Show Information...");
LCD_ShowString(0,112,200,16,16,Buffer);
LCD_ShowString(0,128,200,16,16,(u8*)"Finish Showing Information");
}
}
// TFTLCD初始化函数中有printf串口打印函数因此我们必须初始化串口(所有串口均可),我们主要是为了使用printf串口信息发送函数
注:这里先引用正点原子的例程,等稍后在进行代码例程的更新。
基于STM32的SPI该如何使用?
我们知道STM32的MCU在进行实验的过程中会产生海量的数据,如何存储这些数据成为了STM32的一个大问题,用SRAM中的内存去存储吗(即我们创建一个数组用数组去存储然后将结果通过对Keil MDK的命令行窗口进行操作将数据输出到txt文本文件中)?这极大地浪费了内存而且给数组预留的内存是固定的失去了灵活性。
我们可以将数据存入flash中,SPI通信可以帮助我们,海量的数据不再极大的浪费我们有限的SRAM存储空间(SRAM是用来运行程序时使用的因此SRAM本身的内存并不算太大而且如果我们占用了过多的内存会影响程序运行的效率)。
STM32的SPI通信理论上完成的是如下过程的操作:
SPI通信程序操作原理