STM32读写SPI FLASH

一、简介

SPI是串行外设接口(Serial Peripheral lnterface)的缩写。SPI是一种高速的、全双工、同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,如NRF24L01、VS1053、SD卡等。

(1)速度:串口的通信一般也就是115200bps,但是SPI的通信速度可以达到10Mbps,接近快了一百倍,所以在配置的时候需要注意,一般不超过10Mbps。

(2)同步:采用同步方式(Synchronous)传输数据,主设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样,来保证数据在两个设备之间是同步传输。—简单地说,发送数据的时候,必须同时接收到数据,由时钟控制。

(3)全双工:发送数据的时候能够接收数据,接收数据的时候能够发送数据,即可以同时双向通信。

二、 spi四种模式

SPI的相位(CPHA)和极性(CPOL)都可以为0或1,对应的4种组合构成了SPI的4种模式
Mode 0: CPOL=0, CPHA=0
Mode 1 :CPOL=0, CPHA=1
Mode 2 :CPOL=1, CPHA=0
Mode 3 :CPOL=1, CPHA=1

时钟极性CPOL: 即SPI空闲时,时钟信号SCLK的电平(1:空闲时高电平; 0:空闲时低电平)
时钟相位CPHA:即SPI在SCLK第几个边沿开始采样(0:第一个边沿开始; 1:第二个边沿开始)
极性:polarity
相位:phase
常用的是mode 0 和mode 3,这两种模式的相同的地方是都在时钟上升沿采样传输数据,区别这两种方式的简单方法就是看空闲时,时钟的电平状态,低电平为mode 0 ,高电平为mode 3。

三、spi四线

SPI FLASH 一般用于存储LCD显示屏所要显示的图片,视频数据。
四根线:SDI(数据输入)、SDO(数据输出)、SCLK(时钟)、CS(片选)。
或者说是MOSI、MISO、SCLK、CS四根,CS和SS是一样的,只是表示方法不同。

SCLK:串行时钟线,用于数据的同步。
MOSI:主机输出数据,从机接收数据。
MISO:主机接收数据,从机输出数据。
SS/CS:控制从机是否工作,往往是低电平有效,低电平选中从设备。

在这里插入图片描述
就比如STM32和LCD屏,使用SPI进行通信,单片机就是主机,LCD就是从机,单片机主机发送数据给从机LCD。
在这里插入图片描述
其中的SCLK、MOSI、MISO三根线是可以复用连接的,唯独片选信号线CS是要连接到器件N,这也代表了多个从设备接在一起 的时候,被片选的设备才可以进行工作,只能多选一,当其中一个设备被选中CS引脚为低电平的时候,其他引脚一定要设置为高电平,避免传输数据的时候发生紊乱。

四、编程

SPI读取W25Q128

(1)读取设备ID

1.根据设备数据手册读时序图,根据固件库参考手册编写函数
(1)GPIO初始化、SPI初始化
(2)SPI write初始化(主机发送数据的时候,从机会返回数据给单片机)
(3)读取ID,读取id的目的是验证自己的写数据函数是否正确。

如果出现可上可下的梯形时序图,那么表示可以是任意数据,下图简单示例一下,学会看图。
根据你所采用的SPI模式进行对比,例如SPI模式0,传输数据开始:
(1)片选信号从高变低,选中从设备。
(2)空闲时时钟的电平状态为低电平,时钟第一个边沿触发,从下图读出的数据就是0000 0100,即0x04,一字节数据发送完毕。
(3)片选信号从高变低,不选中从设备。

在这里插入图片描述

读从设备的ID,Read Manufacturer / Device lD (90h),读取厂商的ID,90h是命令,具体得看从器件的数据手册,一般你Ctrl+F搜索ID就可以找得到,通信开始拉低片选信号,结束时候拉高。
在这里插入图片描述
大概意思:

读取设备lD指令,该指令是通过驱动CS引脚拉低,并移动指令90h后跟一个0x00的24位地址来启动的。最高有效位(MSB)优先。

初始化、发送字节数据、读取ID的部分代码

static GPIO_InitTypeDef  	GPIO_InitStructure;
static SPI_InitTypeDef  	SPI_InitStructure;
#define W25QXX_SS			PBout(14)

void w25qxx_init(void)
{
    
    
	/*!< Enable the SPI clock,使能SPI1硬件时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

	/*!< Enable GPIO clocks,使能GPIOB硬件时钟 */
	RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOB, ENABLE);
	
	//SPI1端口配置 PB3 PB4 PB5
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; 	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;	//复用功能									
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz									
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽复用输出	 									
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉									
	GPIO_Init(GPIOB,&GPIO_InitStructure); 	
	
	/*!< Connect SPI1 pins to AF3 AF4 AF5 */  
	GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1);

	//初始化片选引脚 PB14
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; 				
	//输出功能						
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
	//速度50MHz								
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		
	//推挽复用输出						
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 		
	//上拉							
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; 									
	GPIO_Init(GPIOB,&GPIO_InitStructure); 	
	
	//由于M4芯片还没有真正配置好,先不让外部SPI设备工作
	W25QXX_SS = 1;
	
	/*!< SPI configuration ,SPI的配置*/
	//设置SPI为双线双向全双工通信
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	
	//配置M4工作在主机模式			
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		
	//SPI的发送和接收都是8位数据位							
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;			
	//串口时钟线(SCLK)空闲的时候为高电平,这里电平的设置要根据通信的外围设备有关系的						
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		
	//串行时钟的第二跳变沿进行数据采样,即采用了模式3								
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;	
	//很多时候基于多设备通信,片选引脚都设置为软件控制								
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;										
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;				//SPI通信时钟 = 84MHz/16=5.25MHz
	//最高有效位优先,根据通信的外围设备有关系的
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;								
	SPI_Init(SPI1, &SPI_InitStructure);

	/*!< Enable the sFLASH_SPI  ,使能SPI1硬件*/
	SPI_Cmd(SPI1, ENABLE);
	
}


//发送一个字节数据
uint8_t SPI1_SendByte(uint8_t byte)
{
    
    
	/*!< Loop while DR register in not emplty */
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);

	/*!< Send byte through the SPI1 peripheral */
	SPI_I2S_SendData(SPI1, byte);

	/*!< Wait to receive a byte */
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);

	/*!< Return the byte read from the SPI bus */
	return SPI_I2S_ReceiveData(SPI1);
}

//读取厂家ID数据
uint16_t w25qxx_read_id(void)
{
    
    
	uint16_t id=0;
	
	//片选引脚为低电平
	W25QXX_SS = 0;
	
	//发送0x90指令
	SPI1_SendByte(0x90);
	
	//发送24bit地址,全都是0
	SPI1_SendByte(0x00);	
	SPI1_SendByte(0x00);
	SPI1_SendByte(0x00);

	//读取厂商ID,填写任意参数,十六位数据,先将获取的放在高八位
	id = SPI1_SendByte(0xFF)<<8;
	
	//读取设备ID,填写任意参数,读取第八位数据
	id |= SPI1_SendByte(0xFF);

	//片选引脚为高电平
	W25QXX_SS = 1;
	//打印出ID
	return id;
}

(2)读取指定地址的数据

在这里插入图片描述
英文大概意思:

(1)读取数据指令允许从内存中顺序地读取一个或多个数据字节。
(2)该指令是通过驱动/CS引脚低,然后移动指令代码03h ,以及后面的24位地址到Dl引脚发的。
(3)代码和地址位被锁在时钟引脚的上升边缘上。地址被接收后,寻址存储器位置的数据字节将被移出在以最有效位(MSB)首先的CLK的下降边缘的DO引脚上。在每个字节的数据被移出后,这个地址会自动增加到下一个更高的地址,从而允许一个连续的数据流。这意味着,只要时钟继续,就可以用一条指令访问整个内存。本指令由driving /CS high完成。
(4)如果在erase、Program或Write cycle (BuSY=1)进程中发出读数据指令,该指令将被忽略,不会对当前周期产生任何影响。读取数据指令允许时钟速率从直流到最大fR(参阅交流电气特性)。

在这里插入图片描述

参数:addr是24位地址,*pbuf需要用户在外部定义数组来存储返回的数据,len是数组的长度

void w25qxx_read_data(uint32_t addr,uint8_t *pbuf,uint32_t len)
{
    
    
	W25QXX_SS=0;//拉低片选信号
	SPI1_SendByte(0x03);//发送读数据指令
	SPI1_SendByte((addr>>16)&0xFF);//发送读取数据的地址,24位,一次一个字节
	SPI1_SendByte((addr>>8)&0xFF);//总共三次
	SPI1_SendByte((addr>>0)&0xFF);
	while(len--)
	{
    
    
		*pbuf++ = SPI1_SendByte(0XFF);//返回读取的数据
	}
	W25QXX_SS=1;//拉高片选信号
}

调用:

int main(void)
{
    
     
	uint16_t id=0;
	uint8_t buf[64];
	uint32_t i=0;

	//系统定时器初始化,时钟源来自HCLK,且进行8分频,
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); 
		
	//串口1,波特率115200bps,开启接收中断
	USART1_Init(115200);
	
	//w25qxx初始化
	w25qxx_init();

	//读取ID
	id = w25qxx_read_id();
	
	printf("w25qxx id=%X \r\n",id);

	//读取数据,从0地址开始读取,连续读取64字节
	w25qxx_read_data(0,buf,64);
	
	printf("w25qxx read addr from 0 data is:\r\n");
	
	
	for(i=0; i<64; i++)
	{
    
    
		printf("%02X ",buf[i]);
	}
	
	printf("\r\n");//打印完回车换行

	while(1)
}

(3)扇区擦除(这部分较为繁琐)

在这里插入图片描述
英文的大概意思:

(1)扇区擦除都是以4KB为单位,擦除后,所有数据都变为0xFF。
(2)进行扇区擦除之前,先执行写使能指令,解除写保护。
(3)去检查是否已经擦除完成,必须得执行读取状态寄存器指令,如果读取到BUSY位为1,代表说还没有完成;如果BUSY为0,表示已经擦除完成。
(4)执行完擦除扇区指令后,开启写保护

写使能:
在这里插入图片描述

//解除写保护
void w25qxx_write_enable(void)
{
    
    
	//片选引脚为低电平
	W25QXX_SS = 0;
	
	//发送0x06指令
	SPI1_SendByte(0x06);	
	
	//片选引脚为高电平
	W25QXX_SS = 1;

}

写失能
在这里插入图片描述

//开启写保护
void w25qxx_write_disable(void)
{
    
    
	//片选引脚为低电平
	W25QXX_SS = 0;
	
	//发送0x04指令
	SPI1_SendByte(0x04);	
	
	//片选引脚为高电平
	W25QXX_SS = 1;

}

寄存器
在这里插入图片描述

//读状态寄存器1
uint8_t w25qxx_read_status1(void)
{
    
    
	uint8_t status;
	
	//片选引脚为低电平
	W25QXX_SS = 0;
	
	//发送0x05指令
	SPI1_SendByte(0x05);

	//读取状态寄存器1的值
	status = SPI1_SendByte(0xFF);
	
	//片选引脚为高电平
	W25QXX_SS = 1;
	
	return status;

}

将上述结合起来,进行扇区擦除

void w25qxx_erase_sector(uint32_t addr)
{
    
    
	uint8_t status;
	
	//解除写保护
	w25qxx_write_enable();
	
	//延时1ms,让W25Q128能够识别到CS引脚电平的变化
	delay_ms(1);
	
	//片选引脚为低电平
	W25QXX_SS = 0;

	//发送0x20指令
	SPI1_SendByte(0x20);
	
	//发送24bit地址
	SPI1_SendByte((addr>>16)&0xFF);	
	SPI1_SendByte((addr>>8)&0xFF);
	SPI1_SendByte( addr&0xFF);	
	
	//片选引脚为高电平
	W25QXX_SS = 1;	
	
	
	while(1)
	{
    
    
		//读取状态寄存器1
		status= w25qxx_read_status1();
		
		//若BUSY位为0,则跳出循环
		if((status & 0x01) == 0)
			break;
	
	}
	
	//开启写保护
	w25qxx_write_disable();
}

调用

int main(void)
{
    
     
	uint16_t id=0;
	uint8_t buf[64];
	uint32_t i=0;

	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); 

	//串口1,波特率115200bps,开启接收中断
	USART1_Init(115200);
	
	//w25qxx初始化
	w25qxx_init();

	//读取ID
	id = w25qxx_read_id();
	
	printf("w25qxx id=%X \r\n",id);
	
	//进行扇区擦除
	printf("w25qxx erase from 0 \r\n");
	w25qxx_erase_sector(0);
	
	//读取数据,从0地址开始读取,连续读取64字节
	w25qxx_read_data(0,buf,64);
	
	printf("w25qxx read addr from 0 data is:\r\n");
	
	for(i=0; i<64; i++)
	{
    
    
		printf("%02X ",buf[i]);
	
	}
	
	printf("\r\n");
	while(1)
}

(4)页写
在这里插入图片描述

和扇区擦除过程有点像
void w25qxx_write_data(uint32_t addr,uint8_t *pbuf,uint32_t len)
{
    
    

	uint8_t status;
	
	//解除写保护
	w25qxx_write_enable();
	
	//延时1ms,让W25Q128能够识别到CS引脚电平的变化
	delay_ms(1);
	
	//片选引脚为低电平
	W25QXX_SS = 0;

	//发送0x02指令
	SPI1_SendByte(0x02);
	
	//发送24bit地址
	SPI1_SendByte((addr>>16)&0xFF);	
	SPI1_SendByte((addr>>8)&0xFF);
	SPI1_SendByte( addr&0xFF);	
	
	//写入数据
	while(len--)
		SPI1_SendByte(*pbuf++);
	
	
	//片选引脚为高电平
	W25QXX_SS = 1;	
	
	
	while(1)
	{
    
    
		//读取状态寄存器1
		status= w25qxx_read_status1();
		
		//若BUSY位为0,则跳出循环
		if((status & 0x01) == 0)
			break;
	
	}
	
	//开启写保护
	w25qxx_write_disable();

}

调用的时候需要添加头文件,string.h,在主函数中进行调用。

	//写入数据
	memset(buf,0x12,64);
	w25qxx_write_data(0,buf,64);
	printf("w25qxx write addr from 0 data is all 0x12\r\n");

(5)下一篇文章写使用普通的IO口来模拟SPI时序,替代发送一个字节数据函数:

uint8_t SPI1_SendByte(uint8_t byte)

猜你喜欢

转载自blog.csdn.net/ABCisCOOL/article/details/115191395