STM32F4_SPI协议详解

目录

1. 什么是SPI

2. SPI物理层

3. SPI协议层

3.1 SPI基本通讯过程

3.2 数据有效性

3.3 CPOL/CPHA及通讯模式

4. SPI框图及通讯过程

4.1 SPI框图

4.2 通讯过程

5. SPI初始化结构体

6. Flash芯片(W25Q128)简介

7. 库函数配置SPI1的主模式

8. 实验程序

8.1 实验程序讲解

8.1.1 main.c

8.1.2 SPI.c

8.1.3 SPI.h

8.1.4 W25Q128.c

8.1.5 W25Q128.h


1. 什么是SPI

        SPI 是由摩托罗拉公司提出的通讯协议,即串行外设接口(Serial Peripheral Interface),SPI是一种高速、全双工(可以同时接收和发送)、同步通信的通信总线,被广泛的应用于ADC、MCU的通信过程中,其相对于IIC最显著的特点就是通讯的速度快,要求通讯速率较高的场合。所以对于通讯速率要求不高的场合,通常使用IIC;通讯速率要求较高的场合,通常使用SPI。 

同样的,和IIC协议一样,学习SPI协议,首先也是从其物理层和协议层开始。

2. SPI物理层

这里通过和IIC协议进行比较,来进一步学习SPI协议

STM32F4_SPI协议详解_第1张图片

IIC协议是通过寻址的方式来找到通讯的从机,那么SPI是通过什么方式呢?

SPI不同于IIC,不是通过寻址的方式来和从机进行通信的,每个从机都挂载着一条独立的SS信号线,该信号线也称为从设备选择信号线,常常称作片选信号线,也称为NSS,CS(其中STM32F4的芯片引脚就是CS)。每个从机的SS信号线独占主机的一个引脚,也可以说是,有多少个从设备,主机上就有多少条片选信号线。将该片选信号线置低电平0,则意味着选择该从机进行SPI通信;将该片选信号线置高电平1,则意味着停止通讯

        事实上,每个从设备都有独立的这一条SS信号线,该信号线独占主机的一个引脚。IIC协议中通过设备地址来寻址,选中总线上某个设备并与其进行通讯;而SPI协议中没有设备地址,它是用SS信号线来寻址,当主机选中从设备时,就将所选的从设备的信号线置为低电平0,即意味着该设备被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号

IIC通讯只需要2根串行总线即可,SPI通讯的主机需要哪些特殊的引脚呢?

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

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

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

注意:也正是因为这两条MOSI和MISO信号的存在,SPI通讯才能实现全双工,也就是发送和接收可以同步进行,大大增强了通讯的效率。因为IIC通讯只有一条串行数据总线SDA,一根线是不可能单方向的发送,同时也能反方向的接收数据的。而SPI通讯的两根数据线,一根可以用来发送数据,另外一根可以用来接受数据。

3. SPI协议层

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

3.1 SPI基本通讯过程

STM32F4_SPI协议详解_第2张图片

NSS信号,也就是上面提及的SS信号、CS信号,这三个信号表示的意义是相同的,都表示从机片选信号。NSS信号线由高到低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机进行通讯。

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

②③④⑤:分别表示触发、采样、触发、采样。SCL高电平期间,不管是MOSI还是MISO输入或者输出,此时信号正在传动,处于不稳定状态,不进行采样。当SCL低电平期间,主从传输的数据进入稳定状态,这个时间段进行采样

3.2 数据有效性

SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。

MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,如果需要传输8次,则需要循环传输,且数据输入输出是同时进行的。

3.3 CPOL/CPHA及通讯模式

STM32F4_SPI协议详解_第3张图片

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

STM32F4_SPI协议详解_第4张图片

4. SPI框图及通讯过程

4.1 SPI框图

STM32F4_SPI协议详解_第5张图片 

通讯引脚:STM32F4板载了SPI1,SPI2,SPI3。通常情况下使用SPI1,传输速度较快。SPI2和SPI3可以作为I2S音频协议使用。

时钟控制逻辑:通过控制SPI的BR寄存器来控制SPI协议的波特率。产生SPI时钟。BR[0:2]位控制f_{pclk}时钟的分频因子,对f_{pclk}的分频结果就是SCK引脚的输出时钟频率。

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

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

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

数据帧长度可以通过控制寄存器CR1DFF位配置成8位及16位。

模式配置LSBFIRST可选择MSB先行还是LSB先行

4.2 通讯过程

STM32F4_SPI协议详解_第6张图片

5. SPI初始化结构体

跟其他外设一样,STM32标准库提供了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;   //设置时钟极性,可选高低电平
    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通讯方向,可设置为双线全双工SPI_Direction_2Lines_FullDuplex,双线只接收SPI_Direction_2Lines_RxOnly,单线只接收SPI_Direction_1Line_Rx,单线只发送SPI_Direction_1Line_Tx。

SPI_Mode:设置SPI工作在主机模式SPI_Mode_Master,还是从机模式SPI_Mode_Slave。注意:这两个模式的最大区别就是如果设置为主机模式,那么时钟SCK的时序是由通讯的主机产生的,如果设置为从机模式,那么SCK时钟的时序是由外来的SCK信号提供的。

SPI_DataSize:设置SPI通讯时数据帧的大小是8位SPI_DataSize_8b还是16位SPI_DataSize_16b。

SPI_CPOL:设置时钟极性,高电平SPI_CPOL_High,低电平SPI_CPOL_Low。

SPI_CPHA:设置时钟相位,SCK奇数边沿采集SPI_CPHA_1Edge,SCK偶数边沿采集SPI_CPHA_2Edge。

SPI_NSS:配置NSS引脚的使用模式,可以选择为硬件模式SPI_NSS_Hard,软件模式SPI_NSS_Soft。硬件模式下SPI片选信号由SPI硬件自动产生,软件模式则需要亲自把相应的GPIO端口拉高或置低产生非片选和片选信号。

SPI_BaudRatePrescaler:设置波特率分频因子,分频后时钟即为SPI的SCK信号线的时钟频率。

SPI_FirstBit:配置串行的通讯协议是高位在前还是低位在前。

SPI_CRCPolynomial:SPI计算CRC校验的多项式,若我们使用CRC校验,就使用这个成员的参数,来计算CRC的值。

6. Flash芯片(W25Q128)简介

        W25Q128 是华邦公司推出的大容量SPI FLASH产品,W25Q128的容量为128Mb,该系列还有W25Q80/16/32/64等。ALIENTEK所选择的W25Q128容量为128Mb,也就是16M字节。

        W25Q128 将16M的容量分为256个块(Block),每个块的大小为64K字节,每个块又分为16个扇区Sector,每个扇区4K个字节。W25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。因此,我们必须给W25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。

        W25Q128 的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,W25Q128支持标准的SPI,最大的SPI时钟可以到80MHz。

STM32F4_SPI协议详解_第7张图片

Flash的存储特性

1. 在写入数据前必须先擦除。

2. 擦除时会把数据位全重置为1。

3. 写入数据时只能把为1的数据改为0。

4. 擦除时必须按照最小单元来擦除。(一般是整个扇区)

STM32F4_SPI协议详解_第8张图片

7. 库函数配置SPI1的主模式

STM32F4的SPI功能很强大,SPI 时钟最高可以到 37.5MHz ,支持 DMA,可以配置为 SPI 协议或者 I2S 协议

1. 配置相关引脚的复用功能,使能SPI1时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);       //使能 SPI1 时钟

SPI1_MISO、SPI1_MOSI以及SPI1_SCK连接在PB3、PB4、PB5上,所以采用库函数将对应的引脚全部复用为SPI1

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

初始化GPIO的结构体中,将对应的Mode设置为复用

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;      //复用功能

2. 初始化SPI1结构体,设置SPI1工作模式等

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);      //初始化SPI函数

//该结构的各个参数详情参照该文上述的讲解

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结构体的各个成员的配置如下

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 寄存器

3. 使能SPI1

SPI_Cmd(SPI1, ENABLE);      //使能 SPI1 外设

4. SPI传输数据

SPI通讯接口需要有发送数据接收数据的函数。

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);     //往SPIx数据寄存器写入数据Data,实现发送。

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;      //从SPIx数据寄存器读出接收到的数据。

5. 查看SPI传输状态

SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE);     //获取状态位,判断数据是否传输完成,发送区是否为空

8. 实验程序

实验现象开机的时候先检测W25Q128是否存在,main函数检测按键,KEY1执行写入W25Q128操作,KEY0执行读出W25Q128操作,在LCD上显示相关信息。

注:W25Q128中头文件的指令表来自于下述表格

STM32F4_SPI协议详解_第9张图片

8.1 实验程序讲解

本实验程序基于STM32F4开发板。

实验现象:开机的时候先检测W25Q128是否存在,然后在主循环里面检测两个按键,其中按键KEY1用来执行写入W25Q128的操作,另外一个按键KEY0用来执行读出操作,在LCD上显示相关信息。

注意:

//注意

u16 W25QXX_ReadID(void)
{
	u16 Temp=0; //定义一个16位的返回值
	W25Q128_CS=0;  //使能片选
	SPI1_ReadWriteByte(0x90);  //发送读取ID命令 0x90
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	//连续三次SPI1_ReadWriteByte(0x00);是读取器件ID的要求,伪字节定义为0x00
	Temp|=SPI1_ReadWriteByte(0xFF)<<8;  //因为定义的是16位的读ID函数,SPI1_ReadWriteByte函数类型是8位,所以读0xff放到高8位上
	Temp|=SPI1_ReadWriteByte(0xFF);
	W25Q128_CS=1;
	return Temp;
	
}

该函数读三次0x00 是读器件ID的要求

STM32F4_SPI协议详解_第10张图片

8.1.1 main.c

#include "stm32f4xx.h"                 
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "W25Q128.h"
#include "SPI.h"

//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
	LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
	led_set(sta);
}
//要写到W25Q128的字符串数组
const u8 TEXT_Buffer[]={"Explorer STM32F4 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)

int main(void)
{
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];  //因为要将 Explorer STM32F4 SPI TEST 写入到W25Q128中,所以要定义一个数组来接收,数组的大小为这串字符的大小
	u32 FLASH_SIZE;
	u16 ID=0;
	
	delay_init(168);
	uart_init(115200);  //切记:初始化延迟和串口要放在初始化LCD之前,否则初始化的LCD是没有任何显示结果的
	LED_Init();
	LCD_Init();
	Key_Init();
	W25Q128_Init();
	
	POINT_COLOR=RED;
	LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
	LCD_ShowString(30,70,200,16,16,"SPI TEST");
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2023/20/23");
	LCD_ShowString(30,130,200,16,16,"KEY1:Write  KEY0:Read");  //显示提示信息
	
	while(1)
	{
		ID=W25QXX_ReadID();
		if(ID==W25Q128||ID==NM25Q128)			//  0XEF17   0X5217
		{
			break;
		}
		LCD_ShowString(30,150,200,16,16,"W25Q128 Check Failed!");
		delay_ms(500);
		LCD_ShowString(30,150,200,16,16,"Please Check!        ");
		delay_ms(500);
		LED0=!LED0;    //LED0闪烁
	}
	LCD_ShowString(30,150,200,16,16,"W25Q128 Ready!");
	FLASH_SIZE=16*1024*1024;   //FLASH大小为16个字节
	POINT_COLOR=BLUE;   
	while(1)
	{
		key=KEY_Scan(0);
		if(key==2)  //表示KEY1按下
		{
			LCD_Fill(0,170,239,319,WHITE);   //清除半屏
			LCD_ShowString(30,170,200,16,16,"Start Write W25Q128……");
			W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);//从倒数第100个地址处开始写入SIZE字节长的数据
			//W25QXX_Write第一个参数:数据存储区,全局变量设置的数组TEXT_Buffer用来存储
			//W25QXX_Write第二个参数:开始写入的地址,本次从倒数第100个地址处开始写入SIZE字节长的数据
			//FLASH_SIZE:该变量用来存储FLASH的字节大小
			//W25QXX_Write第三个参数:要写入的字节大小  #define SIZE sizeof(TEXT_Buffer)
			LCD_ShowString(30,170,200,16,16,"W25Q128 Write Finished!");
		}
		if(key==1)  //表示KEY0按下
		{
			LCD_ShowString(30,170,200,16,16,"Start Read W25Q128…… ");
			W25Q128_Read(datatemp,FLASH_SIZE-100,SIZE);//从倒数第100个地址处开始读,读出SIZE个字节
			//第一个参数:pBuffer:数据存储区  , 把读到的数据存储在事先定义好的变量datatemp中,然后再用LCD直接显示出datatemp
			//第二个参数:ReadAddress:开始读取的地址
			//第三个参数:Num:要读取的字节数,也就是要读取的长度
			LCD_ShowString(30,170,200,16,16,"The Data Readed Is:    ");	  //提示传送完成
			LCD_ShowString(30,190,200,16,16,datatemp);      //显示读到的字符串
		}
		i++;
		delay_ms(10);
		if(i==20)
		{
			LED0=!LED0;   //提示程序正在运行
			i=0;
		}
	}
}


	

8.1.2 SPI.c

#include "stm32f4xx.h"                 
#include "SPI.h"

//初始化SPI1
void SPI1_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); //使能SPI1时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);  //使能GPIOB时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;  //模式需要设置为复用
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;  //设置为推挽输出
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
	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
	
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE);  //复位SPI1
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE);  //停止复位SPI1
	
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex; //SPI双向全双工
	SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_256;  //波特率预分频值256
	SPI_InitStructure.SPI_CPHA=SPI_CPHA_2Edge;  //串行时钟进行偶次采样
	SPI_InitStructure.SPI_CPOL=SPI_CPOL_High;  //串行时钟空闲状态为高电平
	SPI_InitStructure.SPI_CRCPolynomial=7;  //CRC值计算多项式
	SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;  //SPI数据帧8位
	SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;  //数据传输高位在前
	SPI_InitStructure.SPI_Mode=SPI_Mode_Master;  //SPI主模式,即时钟时序是由主机SCK提供的
	SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;  //NSS信号由软件管理
	SPI_Init(SPI1,&SPI_InitStructure);
	
	SPI_Cmd(SPI1,ENABLE);  //使能SPI时钟
	
	SPI1_ReadWriteByte(0xff);//启动传输
}
//SPI1速度设置函数
//SPI速度=fAPB2/分频系数
//SPI_BaudRate_Prescaler: 范围  SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256 
//fAPB2时钟一般为84MHz

//SPI_BaudRatePrescaler_4
//SPI_BaudRatePrescaler_8
//SPI_BaudRatePrescaler_16
//SPI_BaudRatePrescaler_32
//SPI_BaudRatePrescaler_64
//SPI_BaudRatePrescaler_128
//SPI_BaudRatePrescaler_256
void SPI1_SetSpeed(u8 SPI_BaudRate_Prescaler)
{
	//#define assert_param(expr) ((void)0)
	
	assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler)); //判断有效性 assert_param是STM32库函数判断有效性的函数,如果波特率预分频值不在上述的范围内,就会给程序员报错
	//位带操作主要分两步,第一步将所要操作的寄存器的某一位清零  第二步将寄存器清零的相关位写入所需的值
	SPI1->CR1&=0xFFC7; //0xFFC7对应于 1111 1111 1100 0111,相与也就表示将CR1寄存器的3-5位清零
	SPI1->CR1|=SPI_BaudRate_Prescaler;  //设置SPI速度,上一步清零,这一步将所需位写入相关的值
	
	SPI_Cmd(SPI1,ENABLE);  //使能SPI1
}
//SPI1读写一个字节
//WriteData:要写入的字节
//返回值:读到的字节
u8 SPI1_ReadWriteByte(u8 WriteData)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);  //通过之前的学习,TXE为状态位用来判断缓存寄存器是否为空
	//TXE若为0,则表示缓存寄存器非空;TXE若为1,则表示缓存寄存器空,只要跳出while循环,意味着TXE=1,缓存寄存器为空,可以写入下一个数值了
	SPI_I2S_SendData(SPI1,WriteData);  //通过SPI1发送一个字节的数据
	
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);
	//RXNE是用来判断接收缓存区是否为空的状态位,其状态位标志和TXE相同
	return SPI_I2S_ReceiveData(SPI1); //返回通过SPI接收的数据
}



8.1.3 SPI.h

#ifndef _SPI__H_
#define _SPI__H_

void SPI1_Init(void);
void SPI1_SetSpeed(u8 SPI_BaudRate_Prescaler);
u8 SPI1_ReadWriteByte(u8 WriteData);

#endif

8.1.4 W25Q128.c

#include "stm32f4xx.h"
#include "W25Q128.h"
#include "delay.h"
#include "usart.h"
#include "SPI.h"

//W25Q80 
//W25Q16 
//W25Q32 
//W25Q64 
//W25Q128
u16 W25Q128_TYPE=W25Q128;   //默认是W25Q128

//4Kbytes为一个Sector
//16个扇区为1个Block
//W25Q128
//容量为16M字节,共有128个Block,4096个Sector

//初始化SPI FLASH的IO口
void W25Q128_Init(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);  //使能GPIOB时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,ENABLE);  //使能GPIOG时钟
	
	//初始化GPIOB14,也就是F_CS片选信号线
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;  //输出
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14;
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//GPIOG7 NRF_CS W25Q128 和 NRF24L01 共用 SPI1,所以这两个器件在使用的时候,必须分时复用(通过片选控制)才行。
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7;
	GPIO_Init(GPIOG,&GPIO_InitStructure);
	
	GPIO_SetBits(GPIOG,GPIO_Pin_7);  //PG7是NRF24L01外设的片选信号线
	//意思就是STM32F4的SPI1接外设W25Q128 和 NRF24L01,目前SPI通信是一主多从,我们只需要W25Q128
	//所以需要将外设NRF24L01的片选信号线置1,表示关闭该外设,防止干扰SPI FLASH通信
	W25Q128_CS=1;//初始化默认关闭所有外设,也就是所有外设的片选信号线都置高电平
	
	SPI1_Init();
	SPI1_SetSpeed(SPI_BaudRatePrescaler_4); //总线时钟APB/2=fAPB1时钟 168/2=84M  84/4=21M时钟
	
	W25Q128_TYPE=W25QXX_ReadID(); //读取FLASH ID
}
//读W25Q128状态寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
u8 W25Q128_ReadSR(void)
{
	u8 byte=0;
	W25Q128_CS=0;  //使能片选信号
	SPI1_ReadWriteByte(W25X_ReadStatusReg);  //发送读取状态寄存器命令
	byte=SPI1_ReadWriteByte(0xff);   //读取一个字节
	W25Q128_CS=1;  //取消片选信号
	return byte;
}
//写W25Q128状态寄存器
//BIT7  6   5   4   3   2   1   0
//SPR   RV  TB BP2 BP1 BP0 WEL BUSY
//只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写
void W25Q128_Write_SR(u8 Byte)
{
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_WriteStatusReg);  //发送写取状态寄存器命令
	SPI1_ReadWriteByte(Byte);  
	W25Q128_CS=1;
	//写和读取的思想是一样的,读就是设置一个中间变量,通过调用读写函数,把读到值赋给这个变量,最后返回,注意,读一定首先给状态寄存器一个读取的命令
	//写也是首先给状态寄存器一个写的命令,然后通过调用读写函数将要写的值写入寄存器即可
}
//W25Q128写使能
//将WEL置位
void W25Q128_Write_Enable(void)
{
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_WriteEnable);  //发送写使能指令
	W25Q128_CS=1;
}
//W25Q128写禁止
//将WEL清零
void W25Q128_Write_Disable(void)
{
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_WriteDisable);  //发送写禁止指令
	W25Q128_CS=1;
}
//读取芯片ID
//返回值如下:				   
//0XEF13,表示芯片型号为W25Q80  
//0XEF14,表示芯片型号为W25Q16    
//0XEF15,表示芯片型号为W25Q32  
//0XEF16,表示芯片型号为W25Q64 
//0XEF17,表示芯片型号为W25Q128 
u16 W25QXX_ReadID(void)
{
	u16 Temp=0; //定义一个16位的返回值
	W25Q128_CS=0;  //使能片选
	SPI1_ReadWriteByte(0x90);  //发送读取ID命令 0x90
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	SPI1_ReadWriteByte(0x00);
	//连续三次SPI1_ReadWriteByte(0x00);是读取器件ID的要求,伪字节定义为0x00
	Temp|=SPI1_ReadWriteByte(0xFF)<<8;  //因为定义的是16位的读ID函数,SPI1_ReadWriteByte函数类型是8位,所以读0xff放到高8位上
	Temp|=SPI1_ReadWriteByte(0xFF);
	W25Q128_CS=1;
	return Temp;
	
}
//读取SPI FLASH
//在指定地址上开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddress:开始读取的地址
//Num:要读取的字节数,也就是要读取的长度
void W25Q128_Read(u8 *pBuffer,u32 ReadAddress,u16 Num)
{
	u16 i;
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_ReadData);   //发送读取命令
	SPI1_ReadWriteByte((u8)((ReadAddress)>>16)); //发送24bit地址,本次发送16-23位
	SPI1_ReadWriteByte((u8)((ReadAddress)>>8));//本次发送8-15位
	SPI1_ReadWriteByte((u8)ReadAddress);  //本次发送0-7位
	for(i=0;i>16));  //发送24位地址,同上一个函数,分三次发送,每次发送8位
	SPI1_ReadWriteByte((u8)((WriteAddress)>>8));
	SPI1_ReadWriteByte((u8)WriteAddress);
	for(i=0;ipageremain,否则就意味着要写入的字节数大于这一页剩余的字节数了
		{
			//首先明确要写入的字节数大于这一页剩余的字节数了,所以接下来要做的就是把剩余空间写满,不够的擦除,也可以说是另起一页接着写
			pBuffer=pBuffer+pageremain;   //初始化定义的数据存储区指针是指向扇形区的起始位置的,现在要从这一页的中间部分开始写 通过这句程序可以使指针pBuffer指向这一页的偏移地址(简单一点说就是:我们小时候写作业,上一次作业写到了一页的第5行第5个位置,那么下一次写作业本能的就从第5行第5个位置开始写,计算机是需要指针去寻找这个位置的,上述程序就是将指针定位到这个位置)
			//所以数据存储区指针应该保证接下来存储数据在剩余字节数的基础上写字节
			WriteAddress=WriteAddress+pageremain;//同理开始写入的地址也要保证随着单页剩余的字节数进行变化
			
			Num=Num-pageremain; //该程序意思就是减去已经写入的字节数,此时的Num表示下一页应该写入的字节数
			//else虽然这一页的剩余字节已经不够写了,但是还是要先计算出剩余字节写了多少,不够的字节写到下一页
			if(Num>256)  //因为一页最多只能写256个字节,如果下一页要写入的字节数大于256,只能先写满这一页,通过下一循环写剩余的
				pageremain=256;
			else  //如果要写入的字节数小于256,就意味着这一页是可以写完要写入的字节的
				pageremain=Num; //把要写入的字节给到该页剩余的字节数,随着W25Q128_Write_Page(pBuffer,WriteAddress,pageremain);函数进行写一页的操作
		}
	}
}
//写SPI FLASH
//在指定地址开始写入指定长度的数据

//该函数带擦除操作!!!

//pBuffer:数据存储区
//WriteAddress:开始写入的地址(24bit)						
//Num:要写入的字节数(最大65535)  
u8 W25Q128_BUFFER[4096];  //初始化全局变量W25Q128_BUFFER表示扇区能写入的最大字节数为4096
void W25QXX_Write(u8* pBuffer,u32 WriteAddress,u16 Num)
{
	u32 SecAddress;  //存储扇区地址
	u16 SecMove;  //存储扇区偏移量
	u16 SecRemain;  //存储扇区内剩余空间的大小
	u16 i;
	u8* W25Q128_BUF;
	W25Q128_BUF=W25Q128_BUFFER;  //定义8位的指针数组,用来存储所要写入的字节
	SecAddress=WriteAddress/4096;  //扇区地址(说白了就是一个扇区可以存放4096个字节,除以4096就表示到底放在了那个扇区,就是总字数除以每一页可以写入的字数就得到到底写在了哪一页)
	SecMove=WriteAddress%4096;  //在扇区内的偏移(也就是一页中存放在什么位置,这个位置是相对于起始位置而言的)
	SecRemain=4096-SecMove;    //扇区剩余空间大小(每个扇区能写入的最大字节数为4096,4096减去该扇区的偏移量就是该扇区内剩余空间的大小)
	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用  这也是为什么要引头文件usart.h的原因
	//%X打印十六进制数,\r\n:换行符
	if(Num<=SecRemain)//如果要写入的字节数小于这一页剩余空间的大小,意味着这一页可以写下
		SecRemain=Num;//把要写入的字节数赋值给扇区剩余空间,调用W25Q128_Write_Page函数写即可
	while(1) //进入擦除操作
	{
		//pBuffer:数据存储区
		//ReadAddress:开始读取的地址
		//Num:要读取的字节数,也就是要读取的长度
		//void W25Q128_Read(u8 *pBuffer,u32 ReadAddress,u16 Num)
		W25Q128_Read(W25Q128_BUF,SecAddress*4096,4096);//读出整个扇区的内容
		for(i=0;iSecRemain,跳出for循环;二是最主要的,break结束if判断语句,意味着从偏移量开始往后遍历不全是1,需要擦除;break直接跳出for循环,此时i的值处于一个分界线的状态,SecMove+i之前的地址上都是1,往后不全是1;
        //此时iSecRemain 否则表示要写入的字节大于该页剩余的字节了
		{
			SecAddress++;  //该页已经不够了,转下一页
			SecMove=0;   //偏移量为0,表示从下一页的起始位置开始写
			
			pBuffer=pBuffer+SecRemain;  //数据存储区指针跟着一并偏移
			WriteAddress=WriteAddress+SecRemain;//开始读取的地址也跟着一起偏移
			
			Num=Num-SecRemain;  //该程序建立在这一页不够写的基础上,虽然这一页不够写,但是我还是要计算这一页写了多少,有多少字节需要写入到下一页
			if(Num>4096)   //因为定义整个扇区可以写入的最大字节数是4096,如果要写入的字节Num大于4096,意味着下一个字节还是不够写
				SecRemain=4096;   //因为一个扇区只能写4096个字节,所以下一个扇区不够写的基础上,只能下下次循环写剩余的字节了
			else//else表示下一个字节完全够写了
				SecRemain=Num;  //把要写入的字节Num给到扇区剩余空间,调用写函数写即可
		}
	}
}
//擦除整个芯片		  
//等待时间超长...
void W25Q128_Erase_Chip(void)
{
	W25Q128_Write_Enable();  //写使能
	W25Q128_Wait_Busy();   //等待空闲
	W25Q128_CS=0;
	SPI1_ReadWriteByte(W25X_ChipErase);  //发送片擦除命令
	W25Q128_CS=1;
	W25Q128_Wait_Busy();   //等待芯片擦除结束
}
//擦除一个扇区
//SecAddress:扇区地址 根据实际容量设置
//擦除一个山区的最少时间:150ms
void W25Q128_Erase_Sector(u32 SecAddress)
{
	//监视Flash擦除情况,测试使用
	printf("fe:%x\r\n",SecAddress);
	SecAddress=SecAddress*4096;  //擦除整个扇区
	W25Q128_Write_Enable();  //写使能
	W25Q128_Wait_Busy();   //等待空闲
	W25Q128_CS=0; 
	SPI1_ReadWriteByte(W25X_SectorErase);  //发送扇区擦除命令
	SPI1_ReadWriteByte((u8)((SecAddress)>>16)); //发送24位地址,分三次发送,每次发送8位
	SPI1_ReadWriteByte((u8)((SecAddress)>>8));
	SPI1_ReadWriteByte((u8)SecAddress);
	W25Q128_CS=1;
	W25Q128_Wait_Busy();  //等待擦除完成
}
//等待空闲
void W25Q128_Wait_Busy(void)
{
	while((W25Q128_ReadSR()&0x01)==0x01);   //等待W25Q128状态寄存器的BUSY位清空
	//与&表示拿出SR最低位,也就是BUSY位,只要该位是1,就始终位于while循环中,只要跳出循环,该位就清0
}
//进入掉电模式
void W25Q128_PowerDown(void)
{
	W25Q128_CS=0;  
	SPI1_ReadWriteByte(W25X_PowerDown);  //发送掉电命令
	W25Q128_CS=1;
	delay_us(3);  
}
//唤醒
void W25Q128_WAKEUP(void)
{
	W25Q128_CS=0;  
	SPI1_ReadWriteByte(W25X_ReleasePowerDown);  //发送唤醒命令
	W25Q128_CS=1;
	delay_us(3); 
}


8.1.5 W25Q128.h

#ifndef _W25Q128__H_
#define _W25Q128__H_

//W25X系列/Q系列芯片列表	   
//W25Q80  ID  0XEF13
//W25Q16  ID  0XEF14
//W25Q32  ID  0XEF15
//W25Q64  ID  0XEF16	
//W25Q128 ID  0XEF17	
#define W25Q80 	0XEF13 	
#define W25Q16 	0XEF14
#define W25Q32 	0XEF15
#define W25Q64 	0XEF16
#define W25Q128	0XEF17

#define NM25Q80 	0X5213
#define NM25Q16 	0X5214
#define NM25Q32 	0X5215
#define NM25Q64 	0X5216
#define NM25Q128	0X5217
#define NM25Q256 	0X5218

extern u16 W25Q128_TYPE;				//定义W25Q128芯片型号		   

#define	W25Q128_CS 		PBout(14)  		//W25Q128的片选信号 ,采用位段命名PBout(14)


//指令表
#define W25X_WriteEnable		0x06 
#define W25X_WriteDisable		0x04 
#define W25X_ReadStatusReg		0x05 
#define W25X_WriteStatusReg		0x01 
#define W25X_ReadData			0x03 
#define W25X_FastReadData		0x0B 
#define W25X_FastReadDual		0x3B 
#define W25X_PageProgram		0x02 
#define W25X_BlockErase			0xD8 
#define W25X_SectorErase		0x20 
#define W25X_ChipErase			0xC7 
#define W25X_PowerDown			0xB9 
#define W25X_ReleasePowerDown	0xAB 
#define W25X_DeviceID			0xAB 
#define W25X_ManufactDeviceID	0x90 
#define W25X_JedecDeviceID		0x9F 


void W25Q128_Init(void);
u8 W25Q128_ReadSR(void);
void W25Q128_Write_SR(u8 Byte);
void W25Q128_Write_Enable(void);
void W25Q128_Write_Disable(void);
u16 W25QXX_ReadID(void);
void W25Q128_Read(u8 *pBuffer,u32 ReadAddress,u16 Num);
void W25Q128_Write_Page(u8* pBuffer,u32 WriteAddress,u16 Num);
void W25Q128_Write_NoCheck(u8* pBuffer,u32 WriteAddress,u16 Num);
void W25QXX_Write(u8* pBuffer,u32 WriteAddress,u16 Num);
void W25Q128_Erase_Chip(void);
void W25Q128_Erase_Sector(u32 SecAddress);
void W25Q128_Wait_Busy(void);
void W25Q128_PowerDown(void);
void W25Q128_WAKEUP(void);

#endif


对该文内容有疑问, 或者内容上有写的不对的地方,欢迎批评更改!

你可能感兴趣的:(STM32,stm32,单片机,嵌入式硬件)