欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

STM32F429入门(二十一):SPI协议及SPI读写FLASH

发布时间:2024/3/26 编程问答 86 豆豆
生活随笔 收集整理的这篇文章主要介绍了 STM32F429入门(二十一):SPI协议及SPI读写FLASH 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

IIC主要用于通讯速率一般的场合,而SPI一般用于较高速的场合。

一、SPI协议简介

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设 备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间, 要求通讯速率较高的场合。

(一)物理层

 

SPI 通讯使用 3 条总线及片选线,3 条总线分别为 SCK、MOSI、MISO,片选线为SS,它们的作用介绍如下:

  • SS:从设备选择信号线,常称为片选信号线,也称为NSS、CS。每个从设备都有独立的一条SS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。IIC协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用SS信号线来寻址,当主机选择从设备时,把该从设备的SS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号

  • SCK:时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样。(同步通讯)STM32 的 SPI 时钟频率最大为 fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备

  • MOSI(Master Output,Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机

  • MISO(Master Input,Slave Output):主设备输入/从设备输出的引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机

(二)协议层

SPI协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。

(1)SPI基本通讯过程

  • 通讯的起始和停止信号

    SS信号线由高变低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机检测到NSS线检测的起始信号后,就知道被选中了,开始准备与主机通讯。

    当信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

  • 数据有效性

    SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步,MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。(图示为下降沿采集数据)

  • CPOL/CPHA及通讯模式

    时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、NSS线为高电平时SCK的状态)。CPOL=0时,SCK在空闲状态时为低电平,CPOL=1时则相反。

    时钟相位CPHA是指数据在采用的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。当CPHA=1时,数据线在SCK的“偶数边沿”采样。(无关上升沿下降沿),下图为奇数边沿采样。

 由CPOL及CPHA的不同状态,SPI分成了四种模式,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。

 

二、STM32的SPI外设架构

 

STM32的SPI外设可用作通讯的主机及从机,支持最高的SCK时钟 频率为fpclk/2 (STM32F429型号的芯片默认fpclk1为90MHz,fpclk2为45MHz), 完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位,可设置数据 MSB先行(高位先行,从左往右)或LSB先行(低位先行,从右往左)。它还支持双线全双工(前面小节说明的都是这种模式)、 双线单向以及单线模式。

1-通讯引脚,2-时钟控制逻辑,3-数据控制逻辑,4-整体控制逻辑。

(1)通讯引脚

其中SPI1\SPI4\SPI5\SPI6是APB2上的设备,最高通讯速率达到45Mbit/s,SPI2\SPI3是APB1上的设备,最高通信速率为22.5Mbit/s。其它功能上没有差异。SPI2\SPI3引脚上上均有I2S,可用来设置音频,但是IIS与SPI不可以共用。

(2)时钟控制逻辑

SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCK引脚的输出时钟频率。

 其中fpclk频率是指SPI所在的APB总线频率,APB1为fpclk1,APB2为fpclk2。为了协调通讯速度比较慢的设备。

(3)数据控制逻辑

SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源于接收缓冲区及发送缓冲区。

  • 通过写SPI的数据寄存器DR把数据填充到发送缓冲区中。

  • 通过读数据寄存器DR,可以获取接收缓冲区的内容。

  • 其中数据帧长度可以通过控制寄存器DR的DFF位配置成8位及16位模式:配置LSBFIRST位可以选择MSB先行还是LSB先行。

  • SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及 MISO、MOSI 线。当向外发送数据的时候,数据移位寄存器以 “发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的 时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。

 

(4)整体控制逻辑

整体控制逻辑复制协调整个SPI外设。控制逻辑的工作模式根据我们配置的“控制寄 存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB 先行、主从模式、单双向模式等等。我们可以通过工作状态寄存器读取SPI的工作状态,“状态寄存器(SR)”。控制逻辑还可以根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。在实际的应用中,我们一般不使用SPI外设的标准NSS信号线,而是更简单地使用普通GPIO,软件控制它地电平输出,从而产生通讯起始和停止信号。

(5)通讯过程

 TXE标志代表的是缓冲区是否为空,当TXE为0时,发送缓冲区为非空,若为1时,发送缓冲区为空。当其为空时,也就说明可以准备发送下一个数据。RXNE为接收缓冲区是否为空的标志,其中0代表接收缓冲区为空,1代表接收缓冲区非空。

  • 控制NSS信号线,产生起始信号。

  • 把要发送的数据写入到”数据寄存器DR“中,该数据会被存储到发送缓冲区。

  • 通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去;MISO则把数据一位一位地存储进接收缓冲区中;

  • 当发送完一帧数据的时候,”状态寄存器SR“中的"TXE标志位"会被置1,表示传输完一帧,发送缓冲区已空;类似的,当接收完一帧数据的时候,”RXNE标志位“会被置1,表示传输完一帧,接收缓冲区非空;

  • 等待到”TXE标志位“为1时,若还要继续发送数据,则再次往”数据寄存器DR“写入数据即可;等待到”RXENE标志位“为1时,通过读取”数据寄存器DR“可以获取接收缓冲区中的内容。

假如使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发”数据寄存器DR“中的数据。

需要注意的是CR寄存器中的SSM位:

 当我们让这个寄存器置1时,我们可以通过软件来模拟SPI,这也是比较常用的方式。

三、SPI结构体

typedef struct {uint16_t SPI_Direction; /*设置 SPI 的单双向模式 */uint16_t SPI_Mode; /*设置 SPI 的主/从机端模式 */uint16_t SPI_DataSize; /*设置 SPI 的数据帧长度,可选 8/16 位 */uint16_t SPI_CPOL; /*设置时钟极性 CPOL,可选高/低电平*/uint16_t SPI_CPHA; /*设置时钟相位,可选奇/偶数边沿采样 */uint16_t SPI_NSS; /*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/uint16_t SPI_BaudRatePrescaler; /*设置时钟分频因子,fpclk/分频数=fSCK */uint16_t SPI_FirstBit; /*设置 MSB/LSB 先行 */uint16_t SPI_CRCPolynomial; /*设置 CRC 校验的表达式 */ } SPI_InitTypeDef;
  • SPI_Direction:有双线全双工、双线只接收、单线只接收、单线只发送模式。

  • SPI_Mode:主机模式、从机模式。这两个模式的最大区别是在于时钟信号线SCK信号线的时序,SCK的时序由通讯中的主机产生。若被设置为从机模式,则要接受外来的SCK信号。

  • SPI_DataSize:可以选择SPI通讯的数据帧大小为8位或者16位。

  • SPI_CPOL和SPI_CPHA:这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,这两个配置影响到SPI的通讯模式。时钟极性CPOL成员,可以设置为高电平或者为低电平。时钟相位CPHA成员,可以设置为在SCK奇数边沿采集数据或者是偶数边沿。

  • SPI_NSS:可以选择硬件模式或软件模式。在硬件模式中的SPI片选信号由SPI硬件自动产生,而软件模式则需要亲自把相应的GPIO端口拉高或者置低产生非片选和片选信号。

  • SPI_BaudRatePrescaler:参数可以设定为2、4、6、8、16、32、64、128、256分频。

  • SPI_FirstBit:MSB先行(高数据在前)还是LSB先行(低位数据在前)。

  • SPI_CRCPolynomial:适用于比较复杂的环境,这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数 (多项式),来计算 CRC 的值。

四、实践——SPI读写串行FLASH

 上面是我们即将改写的FLASH芯片。容量为16M。NCS引脚也为NSS引脚,DIO为MOSI引脚,DO为MISO引脚。WP为写保护引脚,低电平有效。HOLD为暂停通讯或结束通讯,用的很少,接为高电平。以下为引脚的连接图。

 在FLASH中,它一共有0-255即256个块(Block),每个块是64KB,16M=64*255/1024。每个块右分为0-15个扇区(Sector),每个扇区4KB。写入数据之前,必须要擦除数据,再重新写入数据,擦除的最小单位为扇区。设备ID为4018H,设备ID可以用来判断设备是否连接正常,以及设备是否配套正确。擦除整个芯片的命令为:C7h/60h。擦除扇区的命令为20h。此芯片为MSB先行。以上为该芯片手册中得出。

 

了解这款FLASH后,我们开始进行读写。

(1)定义引脚以及时钟

#define FLASH_SPI SPI5 #define FLASH_SPI_CLK RCC_APB2Periph_SPI5 #define RCC_APB_CLOCK_FUN RCC_APB2PeriphClockCmd#define FLASH_SPI_CS_GPIO_PORT GPIOF #define FLASH_SPI_CS_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_CS_PIN GPIO_Pin_6#define FLASH_SPI_SCK_GPIO_PORT GPIOF #define FLASH_SPI_SCK_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_SCK_PIN GPIO_Pin_7 #define FLASH_SPI_SCK_AF GPIO_AF_SPI5 #define FLASH_SPI_SCK_SOURCE GPIO_PinSource7#define FLASH_SPI_MISO_GPIO_PORT GPIOF #define FLASH_SPI_MISO_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_MISO_PIN GPIO_Pin_8 #define FLASH_SPI_MISO_AF GPIO_AF_SPI5 #define FLASH_SPI_MISO_SOURCE GPIO_PinSource8#define FLASH_SPI_MOSI_GPIO_PORT GPIOF #define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_MOSI_PIN GPIO_Pin_9 #define FLASH_SPI_MOSI_AF GPIO_AF_SPI5 #define FLASH_SPI_MOSI_SOURCE GPIO_PinSource9

(2)引脚初始化(复用GPIO)

#define CS_HIGH_DISABLE() GPIO_SetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_PIN) #define CS_LOW_ENABLE() GPIO_ResetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_PIN)void FLASH_SPI_Config(void) {GPIO_InitTypeDef GPIO_InitStructure;SPI_InitTypeDef SPI_InitStructure;//1.初始化GPIO RCC_AHB1PeriphClockCmd(FLASH_SPI_CS_GPIO_CLK|FLASH_SPI_SCK_GPIO_CLK|FLASH_SPI_MISO_ GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK,ENABLE);/* 连接 引脚源*/GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_SOURCE,FLASH_SPI_SCK_AF);/* 连接 */GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_SOURCE,FLASH_SPI_MISO_AF);GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_SOURCE,FLASH_SPI_MOSI_AF);/* 使能 SPI 时钟 */RCC_APB_CLOCK_FUN(FLASH_SPI_CLK, ENABLE);/* GPIO初始化 */GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //复用引脚配置为输出模式也可以进行输入GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;/* 配置SCK引脚为复用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN ; GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);/* 配置MISO引脚为复用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);/* 配置MOSI引脚为复用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);/*CS引脚 */GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //推挽输出,本身硬件就有一个上拉/* 配置SCK引脚为复用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN ; GPIO_Init(FLASH_SPI_CS_GPIO_PORT, &GPIO_InitStructure);//2.配置SPI工作模式SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //最快的分频SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge ; //偶数边沿SPI_InitStructure.SPI_CPOL = SPI_CPOL_High ; //空闲时SCK时钟高电平SPI_InitStructure.SPI_CRCPolynomial = 0 ; //不需要使用CRC校验SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据帧SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双向SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//高位先行SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//软件配置SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主机SPI_Init(FLASH_SPI,&SPI_InitStructure); SPI_Cmd(FLASH_SPI,ENABLE); CS_HIGH_DISABLE(); }

为了看初始化是否成功,我们可以获取设备ID来检验。获取ID的命令为:9Fh。

 

uint32_t Read_Device_ID(void) {uint8_t temp[3];//拉低片选CS_LOW_ENABLE();Read_Write_Byte(JEDEC_ID);temp[0] = Read_Write_Byte(DUMMY); //发送任意字节,产生时序,下面同理temp[1] = Read_Write_Byte(DUMMY);temp[2] = Read_Write_Byte(DUMMY);//拉高片选CS_HIGH_DISABLE();//将数据进行组合return temp[0]<<16|temp[1]<<8|temp[2]; }

 以上的命令可以使用宏定义来方便使用:

#define DUMMY 0xFF //任意值 #define JEDEC_ID 0x9F //ID #define ERACE_SECTOR 0x20 //擦除扇区 #define READ_DATA 0x03 //读取数据 #define READ_STATUS 0x05 //空闲 #define WRITE_ENABLE 0x06 //写使能 #define PAGE_PROGRAM 0x02 //写入的地址

为了防止时钟频率错误响应,我们需要检验标志位,取决于我们什么时候读,什么时候写:

//先发送再接收才会产生时序,一定要注意!!否则STM32不会产生时序,产生一下后就停止,只接受到了地址的数据 uint8_t Read_Write_Byte(uint8_t data) {time_out = SPI_FLAG_TIMEOUT;while(SPI_GetFlagStatus(FLASH_SPI,SPI_FLAG_TXE) == RESET){if((time_out--)==0) return SPI_TIMEOUT_UserCallback(0);}//发送缓冲区为空SPI_I2S_SendData(FLASH_SPI,data);time_out = SPI_FLAG_TIMEOUT;//接收缓冲区为空,死循环while(SPI_GetFlagStatus(FLASH_SPI,SPI_FLAG_RXNE) == RESET){if((time_out--)==0) return SPI_TIMEOUT_UserCallback(1);}return SPI_I2S_ReceiveData (FLASH_SPI); }

 其中的变量以及报错函数定义:

/*等待超时时间*/ #define SPI_FLAG_TIMEOUT ((uint32_t)0x1000) #define SPI_LONG_TIMEOUT ((uint32_t)(10 * SPI_FLAG_TIMEOUT))/*信息输出*/ #define FLASH_DEBUG_ON 0#define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg) #define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg) #define FLASH_DEBUG(fmt,arg...) do{\if(FLASH_DEBUG_ON)\printf("<<-FLASH-DEBUG->>[%s] [%d]"fmt"\n",__FILE__,__LINE__, ##arg);\}while(0) static uint8_t SPI_TIMEOUT_UserCallback(uint8_t errorCode) {FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode); return 0xFF; }

为了保持在运行的过程中复位不会因为掉电而乱发数据,我们需要控制Release_Power_Down,稍后会进行补充。

(3)编写FLASH读写过程

//擦除过后扇区内所有的数据都应为1 void erace_setor(uint32_t addr) {//在擦除之前必须写使能Write_Enable();Wait_for_Ready();//拉低片选CS_LOW_ENABLE();Read_Write_Byte(ERACE_SECTOR);//一次能发送24bitRead_Write_Byte((addr>>16)&0xFF);Read_Write_Byte((addr>>8)&0xFF);Read_Write_Byte(addr&0xFF); //拉高片选CS_HIGH_DISABLE();//等待内部时序(等待擦除完成) } //写使能函数 void Write_Enable(void) {//拉低片选CS_LOW_ENABLE();Read_Write_Byte(WRITE_ENABLE); //拉高片选CS_HIGH_DISABLE(); }

在读取的过程中,我们需要得知状态,看它是否空闲后再写入数据、擦除数据、读取数据,这个函数需要在拉低片选前使用:

void Wait_for_Ready(void) {uint8_t reg_status=0x01;while(reg_status &0x01){//拉低片选CS_LOW_ENABLE();//读状态寄存器Read_Write_Byte(READ_STATUS);reg_status = Read_Write_Byte(DUMMY); //拉高片选CS_HIGH_DISABLE(); }}

读取数据的函数如下(整块数据而非单个):

void Read_buffer(uint8_t* pdata,uint32_t addr,uint32_t numByteTorRead) {Wait_for_Ready();//拉低片选CS_LOW_ENABLE();Read_Write_Byte(READ_DATA);Read_Write_Byte((addr>>16)&0xFF);Read_Write_Byte((addr>>8)&0xFF);Read_Write_Byte(addr&0xFF); while(numByteTorRead--){*pdata = Read_Write_Byte(DUMMY);pdata++;}//拉高片选CS_HIGH_DISABLE();}

完成了读数据,接下来是写入数据,最多写入256个数据:

void Write_buffer(uint8_t* pdata,uint32_t addr,uint32_t numByteTorWrite) {Write_Enable();Wait_for_Ready();//拉低片选CS_LOW_ENABLE();Read_Write_Byte(PAGE_PROGRAM);Read_Write_Byte((addr>>16)&0xFF);Read_Write_Byte((addr>>8)&0xFF);Read_Write_Byte(addr&0xFF); while(numByteTorWrite--){Read_Write_Byte(*pdata);pdata++;}//拉高片选CS_HIGH_DISABLE();}

主函数:

uint8_t readBuff[4096] = {0x0}; uint8_t writeBuff[256] = {0x0}; int main(void) { uint32_t device_id = 0;uint32_t i=0;/*初始化USART 配置模式为 115200 8-N-1,中断接收*/Debug_USART_Config();FLASH_SPI_Config();/* 发送一个字符串 */Usart_SendString( DEBUG_USART,"这是一个FLASH实验\n");printf("这是一个FLASH实验\n");device_id = Read_Device_ID();printf("device_id =0x%x",device_id);erace_setor(0x00);//FLASH先擦除后写入 //读出擦除后的数据Read_buffer(readBuff,0x00,4096); printf("\r\n*************读出擦除后的数据**********\r\n");for(i=0;i<4096;i++)printf("0x%x ",readBuff[i]);for(i=0;i<256;i++)writeBuff[i] = i;Write_buffer(writeBuff,0x00,256);//读出擦除后的数据Read_buffer(readBuff,0x00,256); printf("\r\n*************读出写入后的数据**********\r\n");for(i=0;i<256;i++)printf("0x%x ",readBuff[i]);while(1){ } }

五、看库理清思路

下面的代码是已经进行初始化过后,使能NSS引脚后的操作:

(1)使用SPI发送和接收一个数据

/* * @brief 使用SPI发送一个字节的数据 * @param byte:要发送的数据 * @retval 返回接收到的数据 */ u8 SPI_FLASH_SendByte(u8 byte) {SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待发送缓冲区为空,TXE事件 */while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET){if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);}/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */SPI_I2S_SendData(FLASH_SPI, byte);SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待接收缓冲区非空,RXNE事件 */while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET){if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);}/* 读取数据寄存器,获取接收缓冲区数据 */return SPI_I2S_ReceiveData(FLASH_SPI); }//当使用SPI进行读取时,我们需要先写入,再读出/* * @brief 使用SPI读取一个字节的数据 * @param 无 * @retval 返回接收到的数据 */ u8 SPI_FLASH_ReadByte(void) {return (SPI_FLASH_SendByte(Dummy_Byte)); }
  • 函数u8 SPI_FLASH_SendByte(u8 byte)实现了SPI的通讯过程。

  • 上面两个函数都不包含SPI的起始和停止信号,只是收发的主要过程,所以以上两个函数都是拿来调用的,前后需要做好起始和停止信号的操作。

  • 通过检测TXE,获取发送缓冲区状态,若发送缓冲区为空,则说明上一个数据已发送完毕,若不为空,则等待其为空后,再调用库函数SPI_I2S_SendData把要发送的数据写入到数据寄存器DR,写入SPI的数据会存储到发送缓冲区,由SPI外设发送出去。从这一点就可以说明,当你想要读取数据时,必须先写入,后再读出

  • 写入完毕后,等待RXNE事件,即接收缓冲区非空事件。由于 SPI 双线全双工模式下 MOSI 与 MISO 数据传输是同步的,当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据。

  • 等待至接收缓冲区非空时,通过调用库函数SPI_I2S_ReceiveData读取寄存器DR中的数据,最后将其return。

  • 最后看一下读取一个字节数据,它只是简单地调用了一个任意值Dummy_Byte,然后获取返回值,其实发送值是什么无关紧要,然后获取其返回值。SPI接收过程和发送过程实质是一样的,收发同时进行,关键在于上层应用关注的是接收还是发送。

(2)写使能以及读取当前的状态

/* * @brief 向FLASH发送 写使能 命令 * @param none * @retval none */ void SPI_FLASH_WriteEnable(void) {/* 通讯开始:CS低 */SPI_FLASH_CS_LOW();/* 发送写使能命令*/SPI_FLASH_SendByte(W25X_WriteEnable);/*通讯结束:CS高 */SPI_FLASH_CS_HIGH(); }

FLASH芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以需要检验FLASH是否空闲。FLASH芯片定义了一个状态寄存器:

我们需要关注这个状态寄存器的第0位BUSY是否为1,表明FLASH芯片处于忙碌状态,也就是说这个时候它可能在进行擦除或者写入的操作。利用指令表中的“Read Status Register”指令可以获取FLASH芯片寄存器的内容。并校验第0位,判断当前是否可以写入。判断函数如下:

/* * @brief 等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕 * @param none * @retval none */ void SPI_FLASH_WaitForWriteEnd(void) {u8 FLASH_Status = 0;/* 选择 FLASH: CS 低 */SPI_FLASH_CS_LOW();/* 发送 读状态寄存器 命令 */SPI_FLASH_SendByte(W25X_ReadStatusReg);SPITimeout = SPIT_FLAG_TIMEOUT;/* 若FLASH忙碌,则等待 */do{/* 读取FLASH芯片的状态寄存器 */FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte); {if((SPITimeout--) == 0) {SPI_TIMEOUT_UserCallback(4);return;}} }while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 *//* 停止信号 FLASH: CS 高 */SPI_FLASH_CS_HIGH(); }

(3)FLASH扇区擦除

FLASH的存储特性:由于 FLASH 存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原 来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入 前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的 时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。

擦除有以下分类:扇区擦除(Sector Erase)、块擦除(Block Erase)、整片擦除(Chip Erase)

扇区擦除指令的第一个字节为指令编码,紧接着发送的 4 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。注意发送擦除地址时高位在前即可。

void SPI_FLASH_SectorErase(u32 SectorAddr) {/* 发送FLASH写使能命令 */SPI_FLASH_WriteEnable();SPI_FLASH_WaitForWriteEnd();/* 擦除扇区 *//* 选择FLASH: CS低电平 */SPI_FLASH_CS_LOW();/* 发送扇区擦除指令*/SPI_FLASH_SendByte(W25X_SectorErase);/*发送擦除扇区地址的高8位*/SPI_FLASH_SendByte((SectorAddr & 0xFF000000) >> 24);/*发送擦除扇区地址的中前8位*/SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);/* 发送擦除扇区地址的中后8位 */SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);/* 发送擦除扇区地址的低8位 */SPI_FLASH_SendByte(SectorAddr & 0xFF);/* 停止信号 FLASH: CS 高电平 */SPI_FLASH_CS_HIGH();/* 等待擦除完毕*/SPI_FLASH_WaitForWriteEnd(); }

(4)FLASH的页写入

FLASH的页写入命令最多一次可以传输256个字节数据,这个单位也是页大小。FLASH页写入的时序如图:

 

从时序图可知,第 1 个字节为“页写入指令”编码,24 字节为要写入的“地址 A”, 接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始, 按顺序写入到 FLASH 的存储矩阵。若发送的数据超出 256 个,则会覆盖前面发送的数据。

与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储 单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后, 发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写 入 200 个字节也是没有问题的(小于 256 均可)。

/* * @brief 对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区 * @param pBuffer,要写入数据的指针 * @param WriteAddr,写入地址 * @param NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize * @retval 无 */ void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u32 NumByteToWrite) {/* 发送FLASH写使能命令 */SPI_FLASH_WriteEnable();/* 选择FLASH: CS低电平 */SPI_FLASH_CS_LOW();/* 写页写指令*/SPI_FLASH_SendByte(W25X_PageProgram);/*发送写地址的高8位*/SPI_FLASH_SendByte((WriteAddr & 0xFF000000) >> 24);/*发送写地址的中前8位*/SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);/*发送写地址的中后8位*/SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);/*发送写地址的低8位*/SPI_FLASH_SendByte(WriteAddr & 0xFF);if(NumByteToWrite > SPI_FLASH_PerWritePageSize){NumByteToWrite = SPI_FLASH_PerWritePageSize;FLASH_ERROR("SPI_FLASH_PageWrite too large!");}/* 写入数据*/while (NumByteToWrite--){/* 发送当前要写入的字节数据 */SPI_FLASH_SendByte(*pBuffer);/* 指向下一字节数据 */pBuffer++;}/* 停止信号 FLASH: CS 高电平 */SPI_FLASH_CS_HIGH();/* 等待写入完毕*/SPI_FLASH_WaitForWriteEnd(); }

先发送“写使能”命令,接着才开始页写入时序,然后发送指令 编码、地址,再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH 状态寄存器,等待 FLASH 内部写入结束。

当我们有不定量数据写入时,大于256时,可以用下面的函数:

/*** @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区* @param pBuffer,要写入数据的指针* @param WriteAddr,写入地址* @param NumByteToWrite,写入数据长度* @retval 无*/ void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u32 NumByteToWrite) {u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/Addr = WriteAddr % SPI_FLASH_PageSize;/*差count个数据值,刚好可以对齐到页地址*/count = SPI_FLASH_PageSize - Addr; /*计算出要写多少整数页*/NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;/*mod运算求余,计算出剩余不满一页的字节数*/NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;/* Addr=0,则WriteAddr 刚好按页对齐 aligned */if (Addr == 0) {/* NumByteToWrite < SPI_FLASH_PageSize */if (NumOfPage == 0) {SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);}else /* NumByteToWrite > SPI_FLASH_PageSize */{/*先把整数页都写了*/while (NumOfPage--){SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);WriteAddr += SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不满一页的数据,把它写完*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);}}/* 若地址与 SPI_FLASH_PageSize 不对齐 */else {/* NumByteToWrite < SPI_FLASH_PageSize */if (NumOfPage == 0) {/*当前页剩余的count个位置比NumOfSingle小,写不完*/if (NumOfSingle > count) {temp = NumOfSingle - count;/*先写满当前页*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;/*再写剩余的数据*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);}else /*当前页剩余的count个位置能写完NumOfSingle个数据*/{ SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);}}else /* NumByteToWrite > SPI_FLASH_PageSize */{/*地址不对齐多出的count分开处理,不加入这个运算*/NumByteToWrite -= count;NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;/*把整数页都写了*/while (NumOfPage--){SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);WriteAddr += SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不满一页的数据,把它写完*/if (NumOfSingle != 0){SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);}}} }

(5)从FLASH读取数据

相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可。

发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩 阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。

/* * @brief 读取FLASH数据 * @param pBuffer,存储读出数据的指针 * @param ReadAddr,读取地址 * @param NumByteToRead,读取数据长度 * @retval 无 */ void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u32 NumByteToRead) {/* 选择FLASH: CS低电平 */SPI_FLASH_CS_LOW();/* 发送 读 指令 */SPI_FLASH_SendByte(W25X_ReadData);/* 发送 读 地址高8位 */SPI_FLASH_SendByte((ReadAddr & 0xFF000000) >> 24);/* 发送 读 地址中前8位 */SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);/* 发送 读 地址中后8位 */SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);/* 发送 读 地址低8位 */SPI_FLASH_SendByte(ReadAddr & 0xFF);/* 读取数据 */while (NumByteToRead--){/* 读取一个字节*/*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);/* 指向下一个字节缓冲区 */pBuffer++;}/* 停止信号 FLASH: CS 高电平 */SPI_FLASH_CS_HIGH(); }

 六、FLASH存储小数和整数

需要注意的是,存储各种数据类型的时候,我们需要将不同的数据类型分在不同的扇区,不可以混着,主要原因是下面这个动态存储,因为不同的字节数存储的方式不一样,比如说,存储整数,我们将整数通过十六进制传入,占两个字节,当我们需要读取时,四个字节四个字节的读,即合为一个整数,若这个时候我们用浮点数的方式来运算,它就为八个字节八个字节的读,会出错。所以,当我们存储不管是浮点数还是整数,存储方式都一样,可是你想读出来的时候,你就应该区分他们之间的区别,不可以混为一谈,怎么读数据,还是取决于上位机的处理。

/*写入小数数据到第一页*/ SPI_FLASH_BufferWrite((void*)double_buffer, SPI_FLASH_PageSize*1, sizeof(double_buffer)); /*写入整数数据到第二页*/ SPI_FLASH_BufferWrite((void*)int_bufffer, SPI_FLASH_PageSize*2, sizeof(int_bufffer));

 SPI协议初步就学习到这啦,国庆也就结束了,好像任务量也没有完成很多,接下来也要忙比赛啦,希望自己再接再厉。

 

总结

以上是生活随笔为你收集整理的STM32F429入门(二十一):SPI协议及SPI读写FLASH的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。