STM32 SPI协议 读写串行FLASH

通过SPI外设对板子上的串行FLASH芯片(掉电不丢失)进行读写。SPI外设产生的SPI通信协议是一种串行通信协议。STM32的内部也有FLASH存储器,用来存储烧录到板子上的程序。STM32F3系列的芯片只能把程序存储在内部的FLASH里面,而不能放在外部FLASH里面读取,但是F7系列的可以(XIP功能)

SPI通信协议是一种高速(相对于IIC)全双工的通信总线。它被广泛地使用在ADC、LCD(液晶屏)等设备与MCU间,要求通讯速率较高的场合。

目录

一、SPI物理层

二、SPI协议层

三、SPI特性与架构

1.引脚

2.时钟控制逻辑

3.数据控制逻辑

4.通讯过程

四、SPI的初始化结构体

五、W25Q64芯片

1.引脚图:

1-CS引脚

2-串行数据输入、输出和 IOs(DI、DO)

3-写保护引脚(/WP)

4-保持端(/HOLD)

5-串行时钟(CLK)

2.读状态寄存器1:

六、程序设计

1.宏定义、初始化配置

2.其他函数配置

3.配置读取操作和写入操作函数。

4.配置主函数

常见问题:

一、SPI物理层

1-SS:从设备选择信号线,片选信号线,也称为NSS、CS。

每个从设备都有独立的这一条SS信号线, 本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C协议中通过设备地址选中总线上的某个设备并与其进行通讯;而SPI协议中当主机要选择从设备时,把该从设备的SS信号线设置为低电平,该从设备即被选中,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号。

2-SCK:时钟信号线,用于通讯数据同步。与IIC中的SCL类似。

3-MOSI:这条线上数据的方向为主机到从机。

4-MISO:这条线上数据的方向为从机到主机。

MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。

二、SPI协议层

标号1处,NSS信号线由高变低,是SPI通讯的起始信号。标号6处,NSS信号由低变高,是SPI通讯的停止信号.

在IIC协议中,SCL时钟线高电平时数据有效;在SPI协议中,在边沿的时候数据有效。由上图可知,在SCK时钟线的下降沿的时候进行数据的采样(FLASH读取两根数据线的电平信息)。数据无效区在上升沿。但是这不是一成不变的,而是与CPOL/CPHA通讯有关。

CPOL/CPHA通讯模式:

时钟极性CPOL是指SPI通讯设备处于空闲状态时(NSS为高电平时)SCK信号线的电平信号。

时钟相位CPHA是指数据的采样的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样,但具体是上升沿被采样还是下降沿被采样与CPOL的值有关。当CPHA=1时,数据线在SCK的“偶数边沿”采样,同上。

由CPOL及CPHA的不同状态,SPI分成了四种模式,主机与从机需要工作 在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。【都是在上升沿采样】我们的芯片也是如此。

也就是说,CPOLCPHA都为0或者都为1,后面采用都为0

三、SPI特性与架构

SPI外设:在STM32芯片内部的一些电路逻辑,用来产生SPI协议。

支持最高的SCK时钟频率为fpclk/2 (STM32F10x型号的芯片默认fpclk1为36MHz,fpclk2为72MHz),完全支持SPI协议的4种模式,数据帧长度(每一次通讯数据的长度)可设置为8位或16位。数据MSB先行或LSB先行,MSBOUT:高位先传输

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

1.引脚

其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。

2.时钟控制逻辑

SCK线的时钟信号,由 “控制寄存器CR1”中的BR[0:2]位控制。

fpclk频率是指SPI所在的APB总线频率。APB1为fpclk1,APB2为fpckl2。

3.数据控制逻辑

数据寄存器对应两个缓冲区:一个用于写(发送缓冲);另外一个用于读(接收缓冲)。写操作将数据写到发送缓冲区;读操作将返回接收缓冲区里的数据对于8位的数据,缓冲器发送和接收时只会用到SPI_DR[7:0]。在接收时,SPI_DR[15:8]被强制为0。对于16位的数据,缓冲器是16位的,发送和接收时会用到整个数据寄存器

数据帧长度可以通过“控制寄存器CR1”的“DFF位”配置成8位及16位模式;配置“LSBFIRST位”可选择MSB先行还是LSB先行。

状态寄存器(SR)了解SPI的工作状态。TXE位置1:发送缓冲区为空;RXNE置1:接收缓存区非空

实际应用中,一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

4.通讯过程

通讯图如下:

控制NSS信号线,产生起始信号(图中没有画出)。

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

通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地发送到移位寄存器,这时发送缓冲区就变空了,TXE寄存器立刻置1。

这时,可以向发送寄存器发送第二个数据,但是发送寄存器不会立刻把数据发送给发送缓冲区,因为发送缓冲区正在通过时钟线一位一位的把数据发送给接收缓冲区;发送完之后才能从发送缓冲区发送给移位寄存器。MISO则把数据一位一位地存储进接收缓冲区中,两者是同时进行的,也就是说,我们要想读一个数据,必须先写一个数据,在读数据的时候,写入的数据是会被忽略的,所以写什么都无所谓。

四、SPI的初始化结构体

初始化结构体及函数定义在库文件“stm32f10x_spi.h”及“stm32f10x_spi.c”中。

SPI_Direction:设置SPI的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex)

SPI_Mode:设置SPI工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),这两个模式的最大区别为SPI的SCK信号线的时序,SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。

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

五、W25Q64芯片

W25Q64的内存空间结构:  一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。

擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。擦除是以扇区为最小单位。

1.引脚图:

1-CS引脚

用于选中芯片;当CS为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后,在接收新的指令前,/CS必须由高变为低电平。上电后,/CS必须上升到 VCC,在/CS接上拉电阻可以完成这个操作。 

2-串行数据输入、输出和 IOs(DI、DO)

标准的 SPI 传输用单向的DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。

标准的SPI 用单向的DO(输出)在CLK的下降沿从芯片内读出数据或状态。 

3-写保护引脚(/WP)

用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP引脚低电平有效。当状态寄存器2的QE位被置位了,/WP引脚(硬件写保护)的功能不可用。

4-保持端(/HOLD)

当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS为低电平时,当/HOLD变为低电平,DO引脚将变为高阻态,在DI和CLK引脚上的信号将无效。当/HOLD变为高电平,芯片恢复工作。/HOLD功能用在当有多个设备共享同一SPI 总线时。/HOLD引脚低电平有效。当状态寄存器2的QE位被置位了,/ HOLD 引脚的功能不可用。

5-串行时钟(CLK)

串行时钟输入引脚为串行输入和输出操作提供时序。

2.读状态寄存器1:

BUSY位是一个只读位,在状态寄存器中的S0位。当W25Q64在执行页编程扇区擦除块区擦除芯片擦除以及写状态寄存器指令时,该位被硬件自动置1。这时候,除了读状态寄存器指令和擦除暂停指令外的所有操作指令将会被芯片忽略。当芯片执行完这些指令后,硬件会自动将该位清零,表示芯片器件可以接收其他的指令。

读状态寄存器指令可以使用在任何时候,即使程序在擦除的过程中或者写状态寄存器周期正在进行中。这可以检测忙碌状态来确定周期是否完成,以确定设备是否可以接受另一个指令。

内部结构如下图:

STM32可以通过SPI外设发送指令控制W25Q64芯片。指令的初始信号是通过CS下降沿开始。数据在时钟的上升沿采样,MSB先行。

指令代码由单元格地址,写入的数据组成,CS为上升沿的时候结束。

指令代码:

第一列是指令的名字,第二列是指令的代码,输入这个代码就代表前面那个指令。括号代表里面的指令是由FLASH传给主机,没括号反之。

例如下图中第三行读寄存器1,STM32发送0X05给FLASH,FLASH在第二个字节返回S0-S7。Page Program:页写入。A0-A23:你要写入数据的存储单元格地址。Sector Erase:扇区擦除发送地址之后,擦除对应的扇区数据

关于W25Q64芯片还有一篇更加详细的介绍文章http://t.csdn.cn/qG0zUicon-default.png?t=M85Bhttp://t.csdn.cn/qG0zU

六、程序设计

1.宏定义、初始化配置

首先,对SPI外设、SPI外设用到的GPIO口进行宏定义。在对外设用到的GPIO口进行宏定义的时候,要先定义这个外设用到的GPIO口的时钟总线的宏定义:时钟线、打开时钟的函数,用来打开GPIO口时钟。再配置用到的具体GPIO口的端口与引脚

// SPI时钟线
#define  SPI_FLASHx                   SPI1//使能外设的时候使用
#define  SPI_FLASH_CLK                RCC_APB2Periph_SPI1//打开外设的时钟时使用
#define  SPI_FLASH_APBxClkCmd         RCC_APB2PeriphClockCmd//启用外设时钟的函数 

// SPI GPIO 引脚宏定义
//1-SPI外设用到的时钟总线的宏定义,时钟线、打开时钟的函数
#define  SPI_FLASH_GPIO_CLK           RCC_APB2Periph_GPIOA
#define  SPI_FLASH_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd

//2-四条线
#define  SPI_FLASH_SCK_GPIO_PORT       GPIOA//时钟信号线   
#define  SPI_FLASH_SCK_GPIO_PIN        GPIO_Pin_5

#define  SPI_FLASH_MOSI_GPIO_PORT       GPIOA//主机传输到从机
#define  SPI_FLASH_MOSI_GPIO_PIN        GPIO_Pin_7

#define  SPI_FLASH_MISO_GPIO_PORT       GPIOA//从机传输到主机
#define  SPI_FLASH_MISO_GPIO_PIN        GPIO_Pin_6

#define  SPI_FLASH_CS_GPIO_PORT         GPIOA //片选(选择设备)引脚与端口
#define  SPI_FLASH_CS_GPIO_PIN          GPIO_Pin_4

#define  SPI_FLASH_CS_LOW()            GPIO_ResetBits(SPI_FLASH_CS_GPIO_PORT,SPI_FLASH_CS_GPIO_PIN);
#define  SPI_FLASH_CS_HIGH()           GPIO_SetBits(SPI_FLASH_CS_GPIO_PORT,SPI_FLASH_CS_GPIO_PIN);

 然后配置SPI外设,打开时钟,配置GPIO口的具体输出模式,速度。

/*第一步:初始化GPIO*/
	//打开 SPI GPIO 的时钟
	SPI_FLASH_GPIO_APBxClkCmd(SPI_FLASH_GPIO_CLK, ENABLE);
	//打开 SPI 外设的时钟
	SPI_FLASH_APBxClkCmd(SPI_FLASH_CLK, ENABLE);
	
	//1-时钟线 的GPIO口配置
	GPIO_InitStructure.GPIO_Pin = SPI_FLASH_SCK_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_FLASH_SCK_GPIO_PORT, &GPIO_InitStructure);
	
  //2-MOSI线 的GPIO口配置
	GPIO_InitStructure.GPIO_Pin = SPI_FLASH_MOSI_GPIO_PIN;
	GPIO_Init(SPI_FLASH_MOSI_GPIO_PORT, &GPIO_InitStructure);	
	
	//3-MISO线 的GPIO口配置 必须配置为 上拉输入 模式
	GPIO_InitStructure.GPIO_Pin = SPI_FLASH_MISO_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(SPI_FLASH_MISO_GPIO_PORT, &GPIO_InitStructure);
	
	//4-CS线 软件配置
	GPIO_InitStructure.GPIO_Pin = SPI_FLASH_CS_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_FLASH_CS_GPIO_PORT, &GPIO_InitStructure);

 之后通过SPI结构体配置SPI的工作模式,并且初始化、使能SPI外设。注意的是,这些GPIO口的配置的顺序不要颠倒,尤其是时钟线的配置,否则会导致后面擦除、读取数据失败。

/*第二步:配置SPI 工作模式*/
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
	SPI_InitStructure.SPI_CRCPolynomial = 0;
	//虽然这个结构体成员没有用到,但是需要配置,因为默认的初始化的值为0
	//有可能对我们的程序造成不太好的后果,所以我们要配置一个不影响结果的值
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//标准的双线全双工
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//MSB先行
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//STM32做主机
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
	
	SPI_Init(SPI_FLASHx,&SPI_InitStructure);
	
	SPI_Cmd(SPI_FLASHx,ENABLE);

2.其他函数配置

根据上面的通讯过程图写写入数据的函数,由于SPI协议是收发同时进行的,我们在发数据的同时也会接收到数据,所以我们也要读出返回的数据。只有这样我们才能正常的接收数据。

配置擦除函数。注意的是:所有的擦除和写入操作之前都要调用Write Enable函数让WEL位置1,也就是写使能

/*写使能
	所有的擦除和写入操作之前都要调用Write Enable函数让 WEL位 置1
*/
void SPI_FLASH_Write_Enable(void)
{
	//软件控制CS
	SPI_FLASH_CS_LOW();//开始通讯
	
	SPI_FLASH_Send_data(W25X_WriteEnable);//发送指令写使能

	SPI_FLASH_CS_HIGH();//停止通讯
}

 擦除函数原理图:

由图可以看出,发送完擦除指令之后,要发送24位的地址位,从23位开始到第0位。

在所有的擦除操作和写操作之后都会产生内部时序,所以我们要通过读取寄存器BUSY位来获取其目前的状态。

/*读取状态寄存器*/
void SPI_FLASH_WaitForWriteEnd(void)
{
	uint8_t status;
	
	//软件控制CS
	SPI_FLASH_CS_LOW();//开始通讯
	
	SPI_FLASH_Send_data(W25X_ReadStatusReg);//发送指令:读取状态寄存器
	
	do
	{
		status = SPI_FLASH_Receive_data();//接收一个字节
	} while(status & 0X01);//检测BUSY标志 
	//与运算:同时为1,结果才为1.BUSY位为1(SPI忙于通信),status & 0X01 = 0X01 
	//就一直卡在while循环内,直到SPI不忙
	
	SPI_FLASH_CS_HIGH();//停止通讯
}

/*擦除操作 擦除扇区*/
void SPI_FLASH_Erase_Sector(uint32_t addr)
{
	SPI_FLASH_WaitForWriteEnd();
	
	SPI_FLASH_Write_Enable();//擦除、写入前必须写使能
	
	SPI_FLASH_CS_LOW();//开始通讯
	
	SPI_FLASH_Send_data(W25X_SectorErase);//发送扇区擦除指令
	
	SPI_FLASH_Send_data((addr & 0XFF0000)>>16);//提取前两位 并且后移16位
	
	SPI_FLASH_Send_data((addr & 0X00FF00)>>8);//提取中间两位 并且后移8位
	
	SPI_FLASH_Send_data(addr & 0XFF);

	SPI_FLASH_CS_HIGH();//停止通讯
	
	SPI_FLASH_WaitForWriteEnd();  
}

3.配置读取操作和写入操作函数。

/*读取操作*/
/*从哪个地址开始,读取出来的数据放在哪个地址里(数组),读取多大的数据*/
void SPI_FLASH_Read_Buffer(uint32_t addr,uint8_t *data,uint32_t size)
{
	uint32_t i;
	
	SPI_FLASH_CS_LOW();//开始通讯
	
	SPI_FLASH_Send_data(W25X_ReadData);//发送读取N个字节指令
	
	SPI_FLASH_Send_data((addr & 0XFF0000)>>16);//提取前两位 并且后移16位
	
	SPI_FLASH_Send_data((addr & 0X00FF00)>>8);//提取中间两位 并且后移8位
	
	SPI_FLASH_Send_data(addr & 0XFF);
	
	for(i=0;i<size;i++) 
	{
		*data = SPI_FLASH_Receive_data(); //把读到的数据存储到 *data 里面
		data++;//读取完之后指针地址指向下一个字节
	}

	SPI_FLASH_CS_HIGH();//停止通讯
}

/*写入操作*/
/*从哪个地址开始,读取出来的数据放在哪个地址里(数组),读取多大的数据
	size的值不能大于256
*/
void SPI_FLASH_Page_Write(uint32_t addr,uint8_t *data,uint32_t size)
{
	uint32_t i;
	
	if(size>256) printf("page_write函数不能写入超过256个字节\n");
			
	SPI_FLASH_WaitForWriteEnd();
	
	SPI_FLASH_Write_Enable();//擦除、写入前必须写使能
	
	SPI_FLASH_CS_LOW();//开始通讯
	
	SPI_FLASH_Send_data(W25X_PageProgram);//发送读取N个字节指令
	
	SPI_FLASH_Send_data((addr & 0XFF0000)>>16);//提取前两位 并且后移16位
	
	SPI_FLASH_Send_data((addr & 0X00FF00)>>8);//提取中间两位 并且后移8位
	
	SPI_FLASH_Send_data(addr & 0XFF);
	
	for(i=0;i<size;i++)
	{
		SPI_FLASH_Send_data(*data);
		data++;
	}

	SPI_FLASH_CS_HIGH();//停止通讯
	
	SPI_FLASH_WaitForWriteEnd();
}

/*连续写入操作*/
/*从哪个地址开始,读取出来的数据放在哪个地址里(数组),读取多大的数据
  不断写入操作,size的值可以大于256
*/
void SPI_FLASH_Write_Buff(uint32_t addr,uint8_t *data,uint32_t size)
{
	uint32_t i;
	
	for(i=0;i<size;i++) 
	{
		if(i==0 || (addr % 256 == 0))//到了一页的开头或者结尾 需要重新产生一次起始信号
		{
			SPI_FLASH_CS_HIGH();//停止通讯
			
			SPI_FLASH_WaitForWriteEnd();
			
			SPI_FLASH_Write_Enable();//擦除、写入前必须写使能
			
			SPI_FLASH_CS_LOW();//开始通讯
			
			SPI_FLASH_Send_data(W25X_PageProgram);//发送读取N个字节指令
			
			SPI_FLASH_Send_data((addr & 0XFF0000)>>16);//提取前两位 并且后移16位
			
			SPI_FLASH_Send_data((addr & 0X00FF00)>>8);//提取中间两位 并且后移8位
			
			SPI_FLASH_Send_data(addr & 0XFF);
		}
		
			SPI_FLASH_Send_data(*data); //发送要写入的数据
			data++;//读取完之后指针地址指向下一个字节
			addr++;
	}

	SPI_FLASH_CS_HIGH();//停止通讯
	
	SPI_FLASH_WaitForWriteEnd();
}

4.配置主函数

配置完这些函数在主函数内部应用的时候,要注意:写入之前必须要保证空间被擦除,是干净的。

uint8_t read_temp[4096] = {0};
uint8_t write_temp[4096] = {0};


int main(void)
{
	uint32_t i = 0;
	uint32_t read_id = 0;
	/*在程序来到main函数这里的时候,系统时钟已经配置成72M*/
	DEBUG_UART_Config();
	SPI_GPIO_Config();
	
	printf("你好啊\n");
	
	read_id = SPI_FLASH_Read_JEDEC_ID();
	printf("read_id = %x\n",read_id);

/*擦除扇区*/
	SPI_FLASH_Erase_Sector(0);
	printf("擦除完毕\n");
	
/*写入4096个数据*/
	for(i=0;i<4096;i++) write_temp[i] = i;
	SPI_FLASH_Write_Buff(0,write_temp,4096);
	printf("写入完毕\n");

/*读取并打印4096个数据*/	
	SPI_FLASH_Read_Buffer(0,read_temp,4096);
	printf("读取到的数据:\n");
	for(i=0;i<4096;i++) printf("%5x",read_temp[i]);
	
/*校验读写数据*/	
	for(i=0;i<4096;i++)
	{
		if(write_temp[i] != read_temp[i])
		{
			printf("读写不一致\n");break;
		}
	}
	printf("校验结束\n");
	
	while(1);
}

 串口调试窗口:

常见问题:

为什么读取地址总是返回FFFFFF?

芯片停止工作,进入低功耗模式,我们需要唤醒芯片,一是可以通过重新上电,二是可以通过W25X_JedecDeviceID指令来唤醒。

  • 2
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值