文章目录

  • SPI介绍
  • SPI相关的寄存器
  • SPI 控制寄存器 1(SPI_CR1)
  • 9位SSM,8位SSI
  • 456位
  • 2位 MSTR
  • SPI数据寄存器2(SPI_CR2)
  • 2位SSOE
  • 1 位TXDMAEN
  • 0位 RXDMAEN
  • SPI 数据寄存器(SPI_DR)
  • SPI工作模式
  • SPI中断
  • SPI使用步骤
  • 使能 SPI2 的时钟
  • 配置相关引脚的复用功能
  • 初始化 SPI2, 设置 SPI2 工作模式
  • 使能SPI2
  • SPI传输数据
  • 发送数据函数
  • 接收数据函数
  • 查看 SPI 传输状态函数
  • 接收
  • 发送
  • 设置SPI2速度函数
  • 读写一个字节
  • W25Q128
  • 容量
  • 擦除
  • W25QXX驱动解读
  • W25QXX.h
  • 初始化SPI
  • 读取状态寄存器
  • 写状态寄存器
  • 擦除一个扇区
  • 读取 SPI FLASH
  • 无检查写函数
  • W25QXX_Write函数

SPI介绍

参考 中文参考手册 721
SPI 接口提供两个主要功能,支持 SPI 协议或 I 2 S 音频协议。 默认情况下,选择的是 SPI 功能。可通过软件将接口从 SPI 切换到 I 2 S。

SPI相关的寄存器

SPI 控制寄存器 1(SPI_CR1)

stm32cubemx关于spi设置_数据

9位SSM,8位SSI

  • SSM:软件从设备管理 (Software slave management)SSM置位时,NSS输入引脚的电平将被SSI的值代替。
  • SSI:内部从设备选择 (Internal slave select)
  • SSOE:SS输出使能 (SS output enable)
  • MSTR:主设备选择 (Master selection)

stm32cubemx关于spi设置_arm_02

  • 主设备和从设备在进行SPI通信的时候,从设备都有个CS片选信号,低电平有效,我们通常都要用这个NSS连到从设备的CS上。
  • 控制内部NSS引脚与SSI(一个寄存器)相连(软件模式),还是与外部NSS引脚(硬件引脚)相连(硬件模式)。

所谓输入,就是NSS的电平信号给自己,所谓输出,就是将NSS的电平信号发送出去,给从机。

stm32cubemx关于spi设置_arm_03

456位

设置SPI2的速度

stm32cubemx关于spi设置_单片机_04

2位 MSTR

  • 当SPI工作在从模式(MSTR=0)
  • 当SPI工作在主模式配置(MSTR=1)
  • 当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它的设备它是主设备;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。

SPI数据寄存器2(SPI_CR2)

stm32cubemx关于spi设置_stm32cubemx关于spi设置_05

2位SSOE

SS 输出使能 (SS output enable)
0:在主模式下禁止 SS 输出,可在多主模式配置下工作
1:在主模式下使能 SS 输出,不能在多主模式环境下工作
注意: 不适用于 I 2 S 模式和 SPI TI 模式

1 位TXDMAEN

发送缓冲区 DMA 使能 (Tx buffer DMA enable)
当此位置 1 时,每当 TXE 标志置 1 时,即产生 DMA 请求。
0:关闭发送缓冲区 DMA
1:使能发送缓冲区 DMA

0位 RXDMAEN

接收缓冲区 DMA 使能 (Rx buffer DMA enable)
当此位置 1 时,每当 RXNE 标志置 1 时,即产生 DMA 请求。
0:关闭接收缓冲区 DMA
1:使能接收缓冲区 DMA

SPI 数据寄存器(SPI_DR)

stm32cubemx关于spi设置_单片机_06

SPI工作模式

SPI总线有四种工作方式。主要输出串行同步时钟极性和相位可以进行配置。
重点:每个flash手册中会注明使用的工作模式,若没直接注明,需要根据flash命令的时序自行判断。

  • 时钟极性
    如果CPOL=0,串行同步时钟的空闲状态为低电平;
    如果 CPOL=1,串行同步时钟的空闲状态为高电平。
  • 时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。
    如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;
    如果 CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。
  • SPI 主模块和与之通信的外设备时钟相位和极性应该一致。

不同时钟相位下的总线数据传输时序如图

stm32cubemx关于spi设置_stm32cubemx关于spi设置_07

STM32F4 的 SPI 功能很强大,SPI 时钟最高可以到 37.5Mhz,支持 DMA,可以配置为 SPI协议或者 I2S 协议(支持全双工 I2S)。

SPI中断

stm32cubemx关于spi设置_arm_08

SPI使用步骤

我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128),实现类似IIC的功能。

使能 SPI2 的时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PORTB 时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE );//SPI2 时钟使能

配置相关引脚的复用功能

这里使用 PB13、14、15 这 3 个(SCK.、MISO、MOSI,CS 使用软件管理方式),所以设置这三个为复用 IO。

GPIO_InitTypeDef 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

SPI2的引脚在PB上,可以参考W25Q128硬件连接图。

stm32cubemx关于spi设置_数据_09

初始化 SPI2, 设置 SPI2 工作模式

这在库函数中是通过 SPI_Init 函数来实现的。

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

SPI_InitTypeDef 的定义:

typedef struct
{
uint16_t SPI_Direction;
uint16_t SPI_Mode;
uint16_t SPI_DataSize;
uint16_t SPI_CPOL;
uint16_t SPI_CPHA;
uint16_t SPI_NSS;
uint16_t SPI_BaudRatePrescaler;
uint16_t SPI_FirstBit;
uint16_t SPI_CRCPolynomial;
}SPI_InitTypeDef;

这里挑取几个重要的成员变量讲解一下:
• SPI_Direction 用来设置 SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式

SPI_Direction_2Lines_FullDuplex //全双工

• SPI_Mode 用来设置 SPI 的主从模式

SPI_Mode_Master  主机模式
SPI_Mode_Slave //从机模式

• SPI_DataSize为 8 位还是 16 位帧格式选择项

SPI_DataSize_8b

• SPI_CPOL 用来设置时钟极性

SPI_CPOL_High 串行同步时钟的空闲状态为高电平

• SPI_CPHA 用来设置时钟相位,就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集

SPI_CPHA_2Edge,或者1Edge

• SPI_NSS 设置NSS 信号由硬件(NSS 管脚)还是软件控制

SPI_NSS_Soft //软件

• SPI_BaudRatePrescaler设置 SPI 波特率预分频值决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值

SPI_BaudRatePrescaler_256 //256 分频值

传输速度为 36M/256=140.625KHz。2-156,凡是2的几次方都可以。
• SPI_FirstBit ,设置数据传输顺序是 MSB 位在前还是 LSB 位在前

SPI_FirstBit_MSB 高位在前

• SPI_CRCPolynomial 来设置 CRC 校验多项式,提高通信可靠性,大于 1 即可。

初始化代码:

SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收 8 位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//第二个跳变沿数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由软件控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频 256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器

使能SPI2

使能 SPI2 的方法是:

SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设
SPI2_ReadWriteByte(0xff); //④启动传输,主机发一个字节,进行一次传输,可以启动传输

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);
发送
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE)

设置SPI2速度函数

单独的设置分频系数的函数

//SPI 速度设置函数
//SpeedSet://SPI_BaudRatePrescaler_256 256 分频 (SPI 281.25K@sys 72M)
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
	assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
	SPI2->CR1&=0XFFC7;
	SPI2->CR1|=SPI_BaudRatePrescaler; //设置 SPI2 速度
	SPI_Cmd(SPI2,ENABLE);
}
  • 参考SPI 控制寄存器 1(SPI_CR1)

读写一个字节

u8 SPI2_ReadWriteByte(u8 TxData)
{
	u8 retry=0;
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //等待发送区空
	{
		retry++;//重试
		if(retry>200)return 0;
	} //读取两百次还没有值,说明无效,返回
	SPI_I2S_SendData(SPI2, TxData); //通过外设 SPIx 发送一个数据
	retry=0;
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //等待接收完一个 byte
	{
		retry++;
		if(retry>200)return 0;
	}
	return SPI_I2S_ReceiveData(SPI2); //返回通过 SPIx 最近接收的数据
}

W25Q128

• W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64等。

容量

stm32cubemx关于spi设置_数据_10


24位地址

  • 16M ,2的24次方。
  • 256 个块(Block)。2的8次方
  • 每个块大小为 64K 字节。
  • 每个块又分为16 个扇区(Sector)2的4次方
  • 每个扇区 4K 个字节。2的12次方
  • 每页256字节
    共有2的24次方
    块地址8位+扇区地址4位+偏移地址12位。

擦除

W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。
这样要求芯片必须有 4K 以上 SRAM 才能很好的操作。

  • W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,
  • W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。

stm32cubemx关于spi设置_单片机_11

写之前,必须擦除对应扇区内容,也就是确保其中的值是0xFFF。但是擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。

W25QXX驱动解读

w25qxx.c,里面编写的是与 W25Q128 操作相关的代码

W25QXX.h

stm32cubemx关于spi设置_arm_12

W25QXX_CS片选,值0选定,1取消

初始化SPI

stm32cubemx关于spi设置_数据_13


先不选中

速度设置最高,因为操作SPI flash读写数据速度越快越好

读取ID的函数

读取状态寄存器

stm32cubemx关于spi设置_数据_14

片选和取消片选
第一句写得是个指令
第二句才是读取,写了一个空

写状态寄存器

stm32cubemx关于spi设置_arm_15


写了两次,同样一个指令和一个字节

擦除一个扇区

stm32cubemx关于spi设置_stm32cubemx关于spi设置_16


先写使能

等待不繁忙

片选

写指令

写地址开始

写24位,取消片选,flash擦除100多毫秒的一个扇区。

读取 SPI FLASH

stm32cubemx关于spi设置_stm32_17

//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大 65535)
void W25QXX_Read (u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)
{
u16 i;
SPI_FLASH_CS=0; //使能器件
SPI2_ReadWriteByte(W25X_ReadData); //发送读取命令
SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //发送 24bit 地址
SPI2_ReadWriteByte((u8)((ReadAddr)>>8));
SPI2_ReadWriteByte((u8)ReadAddr);
for(i=0;i<NumByteToRead;i++)
{
pBuffer[i]=SPI2_ReadWriteByte(0XFF); //循环读数
}
SPI_FLASH_CS=1;
}

由于 W25Q128 支持以任意地址开始读取数据,在发送 24 位地址之后,程序就可以开始循环读数据了,其地址会自动增加的,不过要注意,不能读的数据超过了 W25Q128 的地址范围哦!

无检查写函数

//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{ 			 		 
	u16 pageremain;	   
	pageremain=256-WriteAddr%256; //单页剩余的字节数		 	    
	if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节,这也是结束标识
	while(1)
	{	   
		W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
		if(NumByteToWrite==pageremain)break;//写入结束了
	 	else //NumByteToWrite>pageremain
		{
			pBuffer+=pageremain;
			WriteAddr+=pageremain;	

			NumByteToWrite-=pageremain;			  //减去已经写入了的字节数
			if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
			else pageremain=NumByteToWrite; 	  //不够256个字节了
		}
		//按照页剩余写一次,然后256个字节的写,然后写最后一页多出来的。
	};	    
}

NoCheck是说可以跨扇区的写

下方表示写了一个扇区

stm32cubemx关于spi设置_arm_18

stm32cubemx关于spi设置_数据_19

W25QXX_Write函数

stm32cubemx关于spi设置_数据_20

作用与 W25QXX_Flash_Read 的作用类似,不过是用来写数据到 W25Q128 里面的,其代码如下:

u8 W25QXX_BUFFER[4096];
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
	u32 secpos;
	u16 secoff;
	u16 secremain;
	u16 i;
	u8 * W25QXX_BUF;
	W25QXX_BUF=W25QXX_BUFFER;
	secpos=WriteAddr/4096;//扇区地址,每个扇区是4096,所以除以4096得到的整数就是扇区的地址标号
	secoff=WriteAddr%4096;//在扇区内的偏移
	secremain=4096-secoff;//扇区剩余空间大小
	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
	if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于 4096 个字节
	while(1)
	{
		W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
		//secpos*4096是该扇区的起始地址
		for(i=0;i<secremain;i++)//校验数据
		{
			if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除,偏移地址内有数据
//擦除后的默认值是0xFFF
		}
		if(i<secremain)//需要擦除
		{
			W25QXX_Erase_Sector(secpos); //擦除这个扇区
			for(i=0;i<secremain;i++) //复制
			{
				W25QXX_BUF[i+secoff]=pBuffer[i];
			}
			W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
		}
		else 
			W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
		//如果扇区剩余空间足够,直接写入扇区剩余区间.
		//是否需要写入下一个扇区
		if(NumByteToWrite==secremain)break;//写入结束了
		else//写入未结束
		{
			secpos++;//扇区地址增 1
			secoff=0;//偏移位置为 0
			pBuffer+=secremain; //指针偏移
			WriteAddr+=secremain; //写地址偏移
			NumByteToWrite-=secremain; //字节数递减
			if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完
			else secremain=NumByteToWrite; //下一个扇区可以写完了
		}
	};
}
//跟无检查页写入的逻辑一致。
  • 该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的数据。
  • 先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。
  • 擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。
  • 当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。