stm32(十五)SPI

1、SPI介绍

SPI是一种通讯协议,目的是实现芯片与芯片之间、芯片和传感器之间的数据传输,数据传输可以是8位的也可以是16位的,具体情况具体分析。IIC可以理解成“汉语”,汉语的作用是会汉语的人之间交流的一种语言;SPI可以理解“英语”,英语的作用是会英语的人之间交流的一种语言。

学习SPI时,就是学习SPI的“语法规则”。

SPI通讯是全双工的,主机通过MOSI在发送1位数据时,主机的MISO也会接收1位数据。发送一个字节数据,相应的会接收到一个字节的数据。

2、SPI的总线接线图

  • SPI通讯需要四个管脚:SCK(serial clock串行时钟,由主控产生脉冲方波时钟);MOSI(master output salve input,主机将数据一位一位的传输给从机的数据通道)/MISO(master input salve output,从机将数据一位一位的传输给主机的数据通道)/CS(chip select,低电平表示选择,高电平表示释放)。
  • SPI总线可以挂载多个设备,如何区分多个设备呢?就是通过CS管脚来进行区分。每一个SPI从设备都对应有一个CS片选脚。主控和SPI从设备进行通信时,首先会将相应的从设备的片选管脚拉低。

3、SPI的时序

根据CPOL(时钟极性)和CPHA(CPHA)的状态的不同,CPOL的值和CPHA的值是通过寄存器来进行设置,每一款带硬件SPI的芯片可以设置CPOL和CPHA,SPI的时序分为四种:

如果 CPOL=0,串行同步时钟的空闲电平状态为低电平;如果CPOL=1,串行同步时钟的空闲电平状态高电平。

如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。

CPOL和CPHA组成成四种时序:

4、STM32F407ZGT6内部的硬件SPI控制器

4.1、SPI控制器的框图

数据发送过程

在发送缓冲区中写入字节时,即向SPIx->DR寄存器写入数据时,发送序列开始。在第一个位传输期间,数据字节并行加载到移位寄存器中,然后以串行方式移出到 MOSI 引脚。发送完成,则SPIx->SR寄存器的 TXE置 1,标志在数据从发送缓冲区传输到移位寄存器,标志着传送完成。

数据接收过程

在出现最后一个采样时钟边沿时, RXNE 位置 1, 移位寄存器中接收的数据字节被拷贝到接收缓冲区中。当读取 SPI_DR 寄存器时, SPI 外设将返回此缓冲值。

移位寄存器中的数据将传输到接收缓冲区,并且 RXNE 标志置 1,标志着接收完成。

4.2、SPI器件管理

STM32的SPI通信既可以作为主机,也可以作为从从机,对于主模式和从模式都可通过硬件或软件来进行管理:实现动态切换主/从操作

硬件管理:通过NSS管脚的电平来决定是主机还是从机

软件管理:通过寄存器的SSI位的值来决定是主机还是从机。

4.3、硬件从器件管理

通过管脚的电平来决定SPI控制器是作为主机还是作为从机。

SSM清0,禁止软件从器件管理,NSS引脚的电平用于决定是主机模式还是 从机模式:NSS接高电平则是主机模式,NSS接低电平则是从机模式。如下图所示

4.4、软件从器件管理

SSM置1,使能软件从器件管理,SSI的值用于决定是主机模式还是从机模式:SSI置1则是主机模式,SSI清零则是从机模式。

本章节,我们是软件管理从器件,并且将SPI控制器配置为主机模式。

5、相关寄存器

5.1、SPI控制器1(SPI_CR1)

位 15 BIDIMODE: 双向通信数据模式使能 

0:选择双线单向通信数据模式(全双工)
1:选择单线双向通信数据模式(半双工)

双线:MOSI/MISO

单向:MOSI只能用于数据输出,MISO只能用于数据输入

本章节,主要是学习SPI的读写是选择为“双线单向模式”,所以,该位需要清零。对应的代码:SPI1->CR1&=~(1<<15);

位 14 BIDIOE: 双向通信模式下的输出使能

此位结合 BIDIMODE 位,用于选择双向通信模式下的传输方向
0:禁止MOSI输出
1:使能MOSI输出

注意: 在主模式下,使用 MOSI 引脚

位 11 DFF: 数据帧格式

0:为发送/接收选择 8 位数据帧格式
1:为发送/接收选择 16 位数据帧格式

位 10 RXONLY: 只接收

0:全双工(发送和接收)
1:关闭输出(只接收模式)

需要清零,SPI1->CR1&=~(1<<10);

位 9 SSM: 软件从器件管理

当 SSM 位置 1 时, NSS 引脚输入替换为 SSI 位的值。
0:禁止软件从器件管理,使用硬件从器件管理。
1:使能软件从器件管理

该位需要置1,SPI1->CR1 |= (1<<9);

位 8 SSI: 内部从器件选择

当选择为软件从器件管理时

0:表示SPI控制器为从机

1:表示SPI控制器为主机

该位需要置1,SPI1->CR1 |= (1<<8);

位 7 LSBFIRST: 帧格式

0:先发送数据的最高位
1:先发送数据的最低位

本章节,主要是学习W25Q64的读写,W25Q64要求先传输最高位,该位需要清零SPI1->CR1&=~(1<<7);

位 6 SPE: SPI 使能

0:关闭外设
1:使能外设

该位需要置1

位 5:3 BR[2:0]: 波特率控制

000: fPCLK/2   100: fPCLK/32

001: fPCLK/4   101: fPCLK/64

010: fPCLK/8   110: fPCLK/128

011: fPCLK/16  111: fPCLK/256

fPCLK :是SPI的主时钟,主时钟经过分频后,则是SPI的串行时钟SCK的频率。

SPI1的主时钟是84MHZ

SPI2/SPI3的主时钟是42MHZ

提问:SPI1的SCK时钟配置2.625MHZ,那么这三位需要如何配置?

答:SPI1->CR1 |= (4<<3);

这里的配置需要考虑SPI从设备的波特率。

位 2 MSTR: 主模式选择

0:从配置

1:主配置

本章节,主要是学习W25Q64的从机,STM32作为主机

所以,该位需要置1,SPI1->CR1 |=(1<< 2);

位1 CPOL: 时钟极性

0:空闲状态时, SCK保持低电平

1:空闲状态时, SCK保持高电平

位 0 CPHA: 时钟相位

0:从第一个时钟边沿开始采样数据

1:从第二个时钟边沿开始采样数据

W25Q64支持时钟模式0:CPOL 为0,CPHA为0

W25Q64支持时钟模式3:CPOL 为1,CPHA为1

5.2、SPI状态寄存器

作用记录数据传输过程中的一些状态,我们主要关注的状态:①数据是否发送完成;②数据是否接收完成

位 1 TXE: 发送缓冲区为空 (Transmit buffer empty)

0:发送缓冲区非空

1:发送缓冲区为空,标志着发送已经完成

位 0 RXNE: 接收缓冲区非空 (Receive buffer not empty)

0:接收缓冲区为空

1:接收缓冲区非空,标志着接收到数据。

5.3、SPI数据寄存器

数据寄存器的有效位有16位,给数据寄存器赋值,则数据会通过移位寄存器一位一位发送出去。读取数据寄存器的值,可以接收数据。

5.4、SPI收发数据一体化函数

//data : 表示即将待发送的数据
//返回值:表示接收的数据
u8  SPI1_WriteRead(u8  data)
{
	//位 1 TXE的值为1,则发送缓冲区为空,标志着发送已经完成
	while(!(SPI1->SR &(1<<1)));
	SPI1->DR = data;
	//位 0 RXNE的值为1,则接收缓冲区非空,标志着接收到数据
	while(!(SPI1->SR &(1<<0)));
	return SPI1->DR;
}

6、W25Q64

W25Q64一个存储芯片。通讯接口是SPI接口。

6.1、原理图

6.2、W25Q64的芯片ID

W25Q64的ID号是0XEF16,0xEF表示华邦电子制造,0x16是设备ID。

读取W25Q64芯片ID的时序

对应的代码

u16 W25Q64_ReadDeviceID(void)
{
	u16 deviceID;
	FLASH_CS_L;
	SPI1_WriteRead(0x90);
	SPI1_WriteRead(0x00);
	SPI1_WriteRead(0x00);
	SPI1_WriteRead(0x00);
	deviceID = SPI1_WriteRead(0xFF);  //读取0xEF
	deviceID = deviceID<<8;
	deviceID |= SPI1_WriteRead(0xFF);//读取0x16,合并成0xEF16
	FLASH_CS_H;
	return deviceID;
}

6.3、W25Q64的内部存储结构

W25Q64的储存空间是:64Mbit,即8M Byte。

W25Q64的“页”:一页为256 字节。

W25Q64的“扇区”:4K个字节,即4096个字节为一个扇区。共有2048个扇区。

W25Q64的“块”:16个扇区为 1 块,64K个字节为一块,65535个字节,总共有128块。

W25Q64在写入之前必须先查出,使得每个字节空间的数据都为0xFF。

  • W25Q64寻址

W25Q64每一个字节的空间都有对应的一个地址(编号)。类似于:超市存物柜。

  • 块&扇区&页的结构图

  • 整个W25Q64的存储结构图

6.4、W25Q64的状态寄存器

BUSY位是个只读位,位于状态寄存器中的S0。在器件中执行“页编程”“扇区擦除”“块区擦除”“芯片擦除”“写状态寄存器”指令时,该位自动置1.这时,除了“读状态寄存器”指令,其他指令都忽略,当编程、擦除和写状态寄存器指令执行完毕之后,该位自动变为0,表示芯片可以接受其它指令

WEL位是只读位,位于状态寄存器中的S1,执行完“写使能”指令后,该位置1,写使能指令将会使状态寄存器WEL位置位,在执行每个页编程,扇区擦除,块区擦除,芯片擦除和写状态寄存器命令之前,都要先置位WEL

  • 读取状态寄存器的时序

  • 对应代码
u8 W25Q64_ReadStatusRegister(void)
{
  u8 status;
	FLASH_CS_L;
	SPI1_WriteRead(0x05);
  status = SPI1_WriteRead(0xFF);
	FLASH_CS_H;
  return status;
}

6.5、写使能

写使能 指令将会使 状态寄存器 WEL位置位,在执行每个“页编程”“扇区擦除”“块区擦除”“芯片擦除”和“写状态寄存器”命令之前,都要先置位WEL

  • 写使能时序

  • 对应代码
void W25Q64_WriteEnable(void)
{
	FLASH_CS_L;
	SPI1_WriteRead(0x06);
  FLASH_CS_H;
}

6.6、读数据

  • 读数据时序

当片选CS/拉低之后,紧随其后是一个24位的地址(A23-A0)(24为的地址分3次发送,每次发送1个字节,先发高位)。W25Q64收到地址后,将要读的数据按字节传送给主机,读数据时,W25Q64的地址会自动增加,允许连续的读取多个字节的数据。数据读取完成之后,片选信号/CS拉高。

  • 对应的代码
举例:从0x000000地址开始,读取50个字节
对应的代码:u8  dataBuf[50] ;  //用于保存读取的数据
W25Q64_ReadDatas(0x000000,dataBuf,50);
void W25Q64_ReadDatas(u32 addr,u8 *pData,u32 count)
{
	FLASH_CS_L;
	SPI1_WriteRead(0x03);
	SPI1_WriteRead((addr>>16)&0xFF);  //发送地址的高8位
	SPI1_WriteRead((addr>>8)&0xFF);//发送地址的中间8位
	SPI1_WriteRead(addr&0xFF);//发送地址的低8位
	while(count)  //循环读取count个字节
	{
		*pData=SPI1_WriteRead(0xFF);  //将读取到到一个字节数据保存起来
		pData++;  //指针偏移,用于保存下一个字节的数据
		count--; 
	}
	FLASH_CS_H;
}

6.7、扇区擦除

扇区擦除的作用是使得一个扇区的数据全部变为0xFF

  • 对应时序

扇区擦除指令可以擦除指定一个扇区(4 k字节)内所有数据,使得被擦除的扇区的数据都变为0xFF。写入扇区擦除指令之前必须执行设备写使能(发送设备写使能指令0x06),并判断状态寄存器(状态寄存器位最低位必须等于0才能操作)。发送的扇区擦除指令前,先拉低/CS,接着发送扇区擦除指令码0X20 ,和24位地址(A23-A0),地址发送完毕后,拉高片选线CS/,并判断状态位,等待擦除结束。擦除一个扇区的最少需要150ms时间。

  • 对应代码
void W25Q64_EraseSector(u32 addr)  
{
	u32 sectorPosition = addr/4096;  
	u32 sectorAddr = sectorPosition*4096;  
	FLASH_CS_L;
	W25Q64_WriteEnable();  //写使能
	while(0x01&W25Q64_ReadStatusRegister());  //提取状态寄存器的BUSY位
	SPI1_WriteRead(0x20);  //写命令
	SPI1_WriteRead((sectorAddr>>16)&0xFF);  //写地址
	SPI1_WriteRead((sectorAddr>>8)&0xFF);
	SPI1_WriteRead(sectorAddr&0xFF);
	FLASH_CS_H;
  while(0x01&W25Q64_ReadStatusRegister());  //提取状态寄存器的BUSY位	
}

6.8、页编程(写数据)

  • 时序

页编程指令允许写多个字节,但不能超过256个字节,页编程之前必须保证内存空间是0XFF,即先擦除再写入。在页编程之前,必须先发送写使能指令。写使能开启后,设备才能接收页编程指令。开启页编程,先拉底/CS,然后发送指令代码“02 h”,接着发送一个24位地址(A23-A0)(发送3次,每次8位)和至少一个字节的数据(数据字节不能超过256字节)。数据字节发送完毕,需要拉高片选线CS/,并判断状态位,等待写入结束。

  • 对应代码
举例:将字符串"Hello world"写入页0
u8 string[ ] = "Hello world";
W25Q64_WritePage(0x000000,string,sizeof(string));
void W25Q64_WritePage(u8 addr,u8 *pData,u8 count)
{
	
	W25Q64_WriteEnable();  //写使能
	while(0x01&W25Q64_ReadStatus());  //提取状态寄存器的BUSY位
	FLASH_CS_L;
	Spi1_WriteRead(0x02);
	Spi1_WriteRead((addr>>16)&0xFF);
	Spi1_WriteRead((addr>>8)&0xFF);
	Spi1_WriteRead(addr&0xFF);
	while(count)
	{
		Spi1_WriteRead(*pData);
		pData++;
		count --;
	}
	FLASH_CS_H;
	while(0x01&W25Q64_ReadStatus());  //提取状态寄存器的BUSY位
}

7、写FLASH函数封装

W25Q64一次最多是写一页,需要封装一个函数,实现写n个字节到Flash中(n>256)。

需要判断待写入的数据需要消耗多少“页”,在写的过程,需要动态调节写入的地址,以及写入的数据

必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
具有自动换页功能 
在指定地址开始写入指定长度的数据,但是要确保地址不越界!
********************************************************************/
void W25QXX_WriteNoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{ 			 		 
  u16 pageremain;	   
  pageremain=256-WriteAddr%256; //单页剩余的字节数		 	    
  if(NumByteToWrite<=pageremain)
pageremain=NumByteToWrite;//不大于256个字节
  while(1)
  {	   
    W25q64_WritePage(WriteAddr,pBuffer,pageremain);
    if(NumByteToWrite==pageremain)break;//写入结束了
    else //NumByteToWrite>pageremain
    {
      pBuffer+=pageremain;
      WriteAddr+=pageremain;	
      
      NumByteToWrite-=pageremain;			  //减去已经写入了的字节数
      if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
      else pageremain=NumByteToWrite; 	  //不够256个字节了
    }
  }	    
}

8、软件设计

模拟SPI读写W25Q64

原理图

寄存器版

#include "stm32f4xx.h"
#include "stdio.h"
#define FLSH_CS_H  (GPIOB->ODR |= 1<<14)
#define FLSH_CS_L  (GPIOB->ODR &=~(1<<14))
void Delay_us(u16 us)
{
	SysTick->CTRL &=~(1<<2);    //选择时钟源为21MHZ
	SysTick->CTRL &=~(1<<1);    //禁止滴答中断
	SysTick->LOAD = 21*us;      //设置重装载寄存器的值
	SysTick->CTRL |= (1<<0);    //使能滴答定时器
	while(!(SysTick->CTRL&(1<<16)));//阻塞判断定时时间是否到达,判断SysTick->CTRL的位
}
/******************** 串口打印函数 ***************************************/
/* fputc是printf最底层的调用函数 */
int fputc(int data,FILE *file)
{
	
	while( !(USART1->SR & (1 << 6)) );//等待发送完成
	USART1->DR = data;   //发送数据
	return data;
}

void usart1_Init()
{
	u16 integer ;
	u16 fraction;
	float USARTDIV;
	u32 baudRate = 115200;
	RCC->AHB1ENR |= 1<<0;//使能GPIOA的时钟  RCC_AHB1ENR
	GPIOA->MODER &= ~(3<<18);//清零
	GPIOA->MODER |= 2<<18;//设置PA9为复用功能模式MODER
	GPIOA->MODER &= ~(3<<20);//清零
	GPIOA->MODER |= 2<<20;//设置PA10为复用功能模式MODER
	GPIOA->AFR[1]&= ~(0XF<<4);//清零
	GPIOA->AFR[1]|= (7<<4);//设置PA9的复用功能为第7复用功能 RXD, AFRH
	GPIOA->AFR[1]&= ~(0XF<<8);//清零
	GPIOA->AFR[1]|= (7<<8);//设置PA10的复用功能为第7复用功能 TXD, AFRH
	
	RCC->APB2ENR |= 1<<4;//使能串口1模块时钟 RCC_APB2ENR
	USART1->CR1  |= 1<<15;//设置串口OVER8为1
	USART1->CR1  &= ~(1<<12);//设置串口数据位长度 :1 起始位, 8 数据位
	USART1->CR2 &=~(3<<12);//设置串口停止位长度 :1 个停止位
	USART1->CR1 &= ~(1<<10);//无校验
	USART1->CR1 |= 1<<3;//发送使能
	USART1->CR1 |= 1<<2;//接收使能
	USARTDIV = 84000000/8/baudRate;//串口波特率设置:USARTDIV = fCK/(8*(2-OVER8) /波特率
	integer = (u16)USARTDIV;
	fraction = ((u16)(USARTDIV-integer))<<4;
	USART1->BRR |= integer<<4 | fraction;
	USART1->CR1 |= 1<<13;//使能串口
}
//FLASH_CS  →PB14
//SPI1_SCK  →PB3
//SPI1_MISO →PB4
//SPI1_MOSI →PB5
void SPI1_Init(void)
{
	RCC->AHB1ENR |= 1<< 1;//使能GPIOB时钟
	GPIOB->MODER &=~(0x3F<<6); //清零
	GPIOB->MODER |= 0x2A<<6;   //PB3/4/5选择为复用功能模式
	GPIOB->AFR[0] &=~(0xFFF<<12);//清零
	GPIOB->AFR[0] |= 0x555<<12;//PB3/4/5选择复用功能5→AF5
	//PB14配置为推挽输出
	GPIOB->MODER &=~(3<<28); //清零
	GPIOB->MODER |= 1<<28;   //输出模式
	GPIOB->OTYPER &=~(1<<14);//推挽
	
	RCC->APB2ENR |= 1<<12;//使能SPI时钟
	SPI1->CR1 &=~(1<<15);//选择双线单向通信数据模式 :全双工,MOSI发送数据,MISO接收数据
	SPI1->CR1 &=~(1<<11);//配置数据帧格式是8位
	SPI1->CR1 &=~(1<<10);//SPI1控制器全双工通信(发送和接收)
	SPI1->CR1 |= 1<<9;//使能软件从器件管理,SSI的值决定是主机还是从机
	SPI1->CR1 |= 1<<8;//SPI1控制器作为主机
	SPI1->CR1 &=~(1<<7);//设置先送最高位
	SPI1->CR1 |= (4<<3);//配置波特率2.625MHZ
	SPI1->CR1 |= (1<<2);//主配置 : 配置为主机
	//选择时钟模式0,CPOL和CPHA都要清零
	SPI1->CR1 &=~(1<<1);
	SPI1->CR1 &=~(1<<0);
	SPI1->CR1 |= 1<<6;//使能SPI1
	FLSH_CS_H; //片选拉高,表示“取消选择”
}
//data:待发送的数据
//返回值:是接收到的数据
u8 SPI1_SendReveiveData(u8 data)
{
	u8 recvData = 0;
	while(!(SPI1->SR&(1<<1)));   //发送完成,跳出该循环
	SPI1->DR = data; //SPI1发送一个字节数据,SPI1_DR寄存器执行写操作将 TXE 位清零。
	while(!(SPI1->SR&(1<<0)));   //接收完成,跳出该循环
	recvData =SPI1->DR  ; //读取接收到的一个字节数据,通过读取 SPI1_DR 寄存器将 RXNE 位清零。
	return recvData;
}
//返回值:厂商号+ID号 0xEF16
u16 W25Q64_ReadID(void)
{
	u8 mID = 0; //厂商号
	u8 dID = 0; //设备号
	FLSH_CS_L;//拉低片选:表示选择
	SPI1_SendReveiveData(0x90);//发送指令代码“0x90”
	SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最高的字节
	SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送中间的字节
	SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最低的字节
	mID = SPI1_SendReveiveData(0xFF);//读厂商号,0xEF
	dID = SPI1_SendReveiveData(0xFF);//读取ID号,0x16
	FLSH_CS_H;//拉高片选:表示取消选择
	return mID<<8|dID;//返回厂商号+ID号 0xEF16
}

u8 W25Q64_ReadStatus(void)
{
	u8 status = 0;
	FLSH_CS_L;//片选拉低
	SPI1_SendReveiveData(0x05);//STM32发送“读状态寄存器指令”→0x05给W25Q64
	status = SPI1_SendReveiveData(0xFF);//STM32读取状态寄存器
	FLSH_CS_H;//片选管脚拉高
	return status;
}

void W25Q64_WriteEnable(void)
{
	FLSH_CS_L;//片选拉低
	SPI1_SendReveiveData(0x06);//STM32发送“写使能指令”→0x06给W25Q64
	FLSH_CS_H;//片选管脚拉高
}
//addr:地址
//pBuf:读数据的存储区
//count :要读多少给字节
void W25Q64_ReadData(u32 addr,u8*pBuf,u32 count)  
{
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0x03);//发送“读数据指令”→0x03
	//MOSI发送要读取的首地址,注意:高位先发送。
	SPI1_SendReveiveData((addr>>16)&0xFF);
	SPI1_SendReveiveData((addr>>8)&0xFF);
	SPI1_SendReveiveData(addr&0xFF);
	//MISO可以读取到数据,可以连续读,W25Q64内部地址会自动递增。
	while(count--)
	{
		*pBuf = SPI1_SendReveiveData(0xFF);
		pBuf++;
	}
	FLSH_CS_H;//拉高片选
}

void W25Q64_EraseSector(u32 addr)
{
	W25Q64_WriteEnable();//执行写使能指令,
	while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0x20);//发送“扇区擦除指令”→0x20
	//MOSI发送要扇区的首地址,注意:高位先发送。
	SPI1_SendReveiveData((addr>>16)&0xFF);
	SPI1_SendReveiveData((addr>>8)&0xFF);
	SPI1_SendReveiveData(addr&0xFF);
	FLSH_CS_H;//拉高片选
	//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
	while(((1<<0)&W25Q64_ReadStatus()));
}

void W25Q64_EraseChip(void)
{
	W25Q64_WriteEnable();//执行写使能指令,
	while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0xC7);//3)发送“芯片擦除指令”→0xC7
	FLSH_CS_H;//拉高片选
	//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
	while(((1<<0)&W25Q64_ReadStatus()));
}


u8 W25Q64_WritePage(u32 addr,u8 *pData,u16 count)
{
	if(count>256)
	{
		return 1;
	}
	W25Q64_WriteEnable();//执行写使能指令,
	while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0x02);//发送“页编程指令”→0x02
	//MOSI发送要首地址,注意:高位先发送。
	SPI1_SendReveiveData((addr>>16)&0xFF);
	SPI1_SendReveiveData((addr>>8)&0xFF);
	SPI1_SendReveiveData(addr&0xFF);
	//MOSI发送数据。W25Q64内部地址自动递增。发送多个字节的数据需要通过循环来完成。
	while(count--)
	{
		SPI1_SendReveiveData(*pData);
		pData++;
	}
	FLSH_CS_H;//拉高片选
	//读取状态寄存器,判断“页编程动作”是否忙了。只有忙完“页编程”之后,才可以去执行其他操作。
	while(((1<<0)&W25Q64_ReadStatus()));
	return 0;
}

int main(void)
{u8 writeBuf[ ] = {"欢度中秋"};
	u8 readBuf[50] ={0};
	
	u16 ID = 0x0000;
	SPI1_Init();
  usart1_Init();//初始化串口
	ID = W25Q64_ReadID();
	printf("W25Q64的ID号是0x%x\r\n",ID);   
	W25Q64_EraseSector(0x000000);
	W25Q64_WritePage(0x000000,writeBuf,sizeof(writeBuf));
	printf("写入到W25Q64的数据是   →%s\r\n",writeBuf);  
	W25Q64_ReadData(0x000000,readBuf,sizeof(writeBuf)) ;
	printf("从W25Q64的读取到数据是 →%s\r\n",readBuf);
	while(1)
	{

	}
}

库函数版

#include "stm32f4xx.h"
#include "stdio.h"
#define FLSH_CS_H  (GPIOB->ODR |= 1<<14)
#define FLSH_CS_L  (GPIOB->ODR &=~(1<<14))
void Delay_us(u16 us)
{
	SysTick->CTRL &=~(1<<2);    //选择时钟源为21MHZ
	SysTick->CTRL &=~(1<<1);    //禁止滴答中断
	SysTick->LOAD = 21*us;      //设置重装载寄存器的值
	SysTick->CTRL |= (1<<0);    //使能滴答定时器
	while(!(SysTick->CTRL&(1<<16)));//阻塞判断定时时间是否到达,判断SysTick->CTRL的位
}
/******************** 串口打印函数 ***************************************/
/* fputc是printf最底层的调用函数 */
int fputc(int data,FILE *file)
{
	
	while( !(USART1->SR & (1 << 6)) );//等待发送完成
	USART1->DR = data;   //发送数据
	return data;
}

void usart1_Init()
{
	u32 baudRate = 115200;
	GPIO_InitTypeDef GPIO_InitStructe;
	USART_InitTypeDef USART_InitStructe;
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能 A 端口
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //使能串口端口
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIO 端口映射到 USART
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIO 端口映射到 USART
	// USART_DeInit(USART1); //复位
	GPIO_InitStructe.GPIO_Mode =GPIO_Mode_AF; //配置为输出模式
	GPIO_InitStructe.GPIO_Pin =GPIO_Pin_9|GPIO_Pin_10; //初始化 GPIOA9/10;
	GPIO_Init(GPIOA,&GPIO_InitStructe);
	USART_InitStructe.USART_BaudRate =baudRate; //波特率
	USART_InitStructe.USART_HardwareFlowControl =USART_HardwareFlowControl_None;//无硬件流控制
	USART_InitStructe.USART_Mode =USART_Mode_Tx|USART_Mode_Rx; //接收模式,发送模式使能
	USART_InitStructe.USART_Parity =USART_Parity_No; //无奇偶校验
	USART_InitStructe.USART_StopBits =USART_StopBits_1; //一位停止位
	USART_InitStructe.USART_WordLength =USART_WordLength_8b;//8 位数据位
	USART_Init(USART1,&USART_InitStructe);
	USART_Cmd(USART1,ENABLE); //使能串口
}
//FLASH_CS  →PB14
//SPI1_SCK  →PB3
//SPI1_MISO →PB4
//SPI1_MOSI →PB5
void SPI1_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	SPI_InitTypeDef SPI_InitStructure;
	GPIO_InitTypeDef SPI_GPIO_InitStruct;
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);//使能 GPIOB 时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);//使能 SPI1 时钟
	//GPIOFB3,4,5 初始化设置
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;//PB3~5 复用功能输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1); //PB3 复用为 SPI1
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); //PB4 复用为 SPI1
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); //PB5 复用为 SPI1
	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_32; //定义波特率预分频的值:波特率预分频值为 32
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从 MSB 位还是 LSB 位开始:数据传输从 MSB 位开始
	SPI_Init(SPI1, &SPI_InitStructure); //根据 SPI_InitStruct 中指定的参数初始化外设 SPIx 寄存器
	SPI_Cmd(SPI1, ENABLE); //使能 SPI 外设
	
	SPI_GPIO_InitStruct.GPIO_Mode=GPIO_Mode_OUT;
	SPI_GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
	SPI_GPIO_InitStruct.GPIO_Pin=GPIO_Pin_14;
	SPI_GPIO_InitStruct.GPIO_Speed=GPIO_Fast_Speed;
	GPIO_Init(GPIOB,&SPI_GPIO_InitStruct); 
	GPIO_SetBits(GPIOB,GPIO_Pin_14);
}
//data:待发送的数据
//返回值:是接收到的数据
u8 SPI1_SendReveiveData(u8 data)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
	SPI_I2S_SendData(SPI1, data); 
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); 
	return SPI_I2S_ReceiveData(SPI1); 
}
//返回值:厂商号+ID号 0xEF16
u16 W25Q64_ReadID(void)
{
	u8 mID = 0; //厂商号
	u8 dID = 0; //设备号
	FLSH_CS_L;//拉低片选:表示选择
	SPI1_SendReveiveData(0x90);//发送指令代码“0x90”
	SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最高的字节
	SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送中间的字节
	SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最低的字节
	mID = SPI1_SendReveiveData(0xFF);//读厂商号,0xEF
	dID = SPI1_SendReveiveData(0xFF);//读取ID号,0x16
	FLSH_CS_H;//拉高片选:表示取消选择
	return mID<<8|dID;//返回厂商号+ID号 0xEF16
}

u8 W25Q64_ReadStatus(void)
{
	u8 status = 0;
	FLSH_CS_L;//片选拉低
	SPI1_SendReveiveData(0x05);//STM32发送“读状态寄存器指令”→0x05给W25Q64
	status = SPI1_SendReveiveData(0xFF);//STM32读取状态寄存器
	FLSH_CS_H;//片选管脚拉高
	return status;
}

void W25Q64_WriteEnable(void)
{
	FLSH_CS_L;//片选拉低
	SPI1_SendReveiveData(0x06);//STM32发送“写使能指令”→0x06给W25Q64
	FLSH_CS_H;//片选管脚拉高
}
//addr:地址
//pBuf:读数据的存储区
//count :要读多少给字节
void W25Q64_ReadData(u32 addr,u8*pBuf,u32 count)  
{
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0x03);//发送“读数据指令”→0x03
	//MOSI发送要读取的首地址,注意:高位先发送。
	SPI1_SendReveiveData((addr>>16)&0xFF);
	SPI1_SendReveiveData((addr>>8)&0xFF);
	SPI1_SendReveiveData(addr&0xFF);
	//MISO可以读取到数据,可以连续读,W25Q64内部地址会自动递增。
	while(count--)
	{
		*pBuf = SPI1_SendReveiveData(0xFF);
		pBuf++;
	}
	FLSH_CS_H;//拉高片选
}

void W25Q64_EraseSector(u32 addr)
{
	W25Q64_WriteEnable();//执行写使能指令,
	while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0x20);//发送“扇区擦除指令”→0x20
	//MOSI发送要扇区的首地址,注意:高位先发送。
	SPI1_SendReveiveData((addr>>16)&0xFF);
	SPI1_SendReveiveData((addr>>8)&0xFF);
	SPI1_SendReveiveData(addr&0xFF);
	FLSH_CS_H;//拉高片选
	//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
	while(((1<<0)&W25Q64_ReadStatus()));
}

void W25Q64_EraseChip(void)
{
	W25Q64_WriteEnable();//执行写使能指令,
	while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0xC7);//3)发送“芯片擦除指令”→0xC7
	FLSH_CS_H;//拉高片选
	//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
	while(((1<<0)&W25Q64_ReadStatus()));
}


u8 W25Q64_WritePage(u32 addr,u8 *pData,u16 count)
{
	if(count>256)
	{
		return 1;
	}
	W25Q64_WriteEnable();//执行写使能指令,
	while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
	FLSH_CS_L;//拉低片选
	SPI1_SendReveiveData(0x02);//发送“页编程指令”→0x02
	//MOSI发送要首地址,注意:高位先发送。
	SPI1_SendReveiveData((addr>>16)&0xFF);
	SPI1_SendReveiveData((addr>>8)&0xFF);
	SPI1_SendReveiveData(addr&0xFF);
	//MOSI发送数据。W25Q64内部地址自动递增。发送多个字节的数据需要通过循环来完成。
	while(count--)
	{
		SPI1_SendReveiveData(*pData);
		pData++;
	}
	FLSH_CS_H;//拉高片选
	//读取状态寄存器,判断“页编程动作”是否忙了。只有忙完“页编程”之后,才可以去执行其他操作。
	while(((1<<0)&W25Q64_ReadStatus()));
	return 0;
}

int main(void)
{u8 writeBuf[ ] = {"欢度中秋"};
	u8 readBuf[50] ={0};
	
	u16 ID = 0x0000;
	SPI1_Init();
  usart1_Init();//初始化串口
	ID = W25Q64_ReadID();
	printf("W25Q64的ID号是0x%x\r\n",ID);   
	W25Q64_EraseSector(0x000000);
	W25Q64_WritePage(0x000000,writeBuf,sizeof(writeBuf));
	printf("写入到W25Q64的数据是   →%s\r\n",writeBuf);  
	W25Q64_ReadData(0x000000,readBuf,sizeof(writeBuf)) ;
	printf("从W25Q64的读取到数据是 →%s\r\n",readBuf);
	while(1)
	{

	}
}

 

  • 3
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SPI(Serial Peripheral Interface)是一种串行外设接口协议,用于在主机(通常是微控制器或处理器)和外设之间进行通信。SPI支持一种主机模式,其中主机控制通信并发送数据给外设。下面是SPI主机模式发送数据的基本步骤: 1. 确定SPI通信参数:包括数据位宽、时钟极性、时钟相位和时钟速率等。这些参数需要与外设进行匹配才能正确通信。 2. 配置主机硬件:根据具体的硬件平台和开发环境,配置相关的寄存器或引脚,以使其支持SPI主机模式。 3. 启动SPI传输:启动SPI传输前,确保外设已经处于可用状态。发送一个特殊的启动命令或设置相关寄存器来启动SPI传输。 4. 准备数据:准备要发送的数据,并将其存储在适当的寄存器或变量中。 5. 发送数据:将数据按照指定的数据位宽和传输顺序逐位发送给外设。通常是从最高位开始,通过SPI接口的数据引脚发送,直到所有数据位都发送完成。 6. 等待传输完成:在数据发送完毕后,等待外设处理数据并完成回应。具体的等待时间取决于外设的响应时间。 7. 结束传输:传输完成后,根据需要可以发送一些特殊的结束命令或配置相关寄存器来结束SPI传输。 以上是SPI主机模式发送数据的基本步骤,具体的实现方式和代码可能会因硬件平台和开发环境而有所不同。在具体的开发中,可以参考相关的开发文档或示例代码来实现SPI主机模式的数据发送。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值