欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

STM32学习心得三十:SPI接口原理、配置及实验

程序员文章站 2022-06-08 18:43:25
...

记录一下,方便以后翻阅~
主要内容:
1) SPI接口原理;
2) 相关寄存器及库函数解读;
3) W25Qxx配置介绍;
4) 相关实验代码解读。
实验功能:系统启动后,按键KEY1控制W25Q128的写入,按键KEY0控制W25Q128的读取。并在串口调试助手上面显示相关信息,LED0闪烁提示程序正在运行。
官方资料:《STM32中文参考手册V10》第23章——串行外设接口SPI和W25Q128芯片资料
1. SPI(SerialPeripheral Interface)接口原理
1.1 SPI简介
SPI 是串行外围设备接口,是Motorola首先在其MC68HCXX系列处理器上定义的。
SPI,是一种高速,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
1.2 SPI结构图
SPI接口一般使用4条线通信:
MISO 主设备数据输入,从设备数据输出(该引脚做主机时为输入,做从机时为输出);
MOSI 主设备数据输出,从设备数据输入(该引脚做主机时为输出,做从机时为输入);
SCLK时钟信号,由主设备产生;
CS从设备片选信号,由主设备控制。
STM32学习心得三十:SPI接口原理、配置及实验
若主机传1个字节给从机,那么从机也必须传1个字节给主机。
1.3 SPI接口框图
STM32学习心得三十:SPI接口原理、配置及实验
1.4 SPI工作原理总结
1.4.1 硬件上为4根线;
1.4.2 主机和从机有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输;
1.4.3 串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换;
1.4.4 外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。
2. SPI特点
2.1 特点一览
2.1.1 3线全双工同步传输;
2.1.2 8或16位传输帧格式选择;
2.1.3 主或从操作;
2.1.4 支持多主模式;
2.1.5 8个主模式波特率预分频系数(最大为fPCLK/2);
2.1.6 从模式频率(最大为fPCLK/2);
2.1.7 主模式和从模式的快速通信;
2.1.8 主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变;
2.1.9 可编程的时钟极性和相位;
2.1.10 可编程的数据顺序,MSB在前或LSB在前;
2.1.11 可触发中断的专用发送和接收标志;
2.1.12 SPI总线忙状态标志;
2.1.13 支持可靠通信的硬件CRC:1)在发送模式下,CRC值可以被作为最后一个字节发送;2)在全双工模式中对接收到的最后一个字节自动进行CRC校验;
2.1.14 可触发中断的主模式故障、过载以及CRC错误标志;
2.1.15 支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求;
2.1.16 STM32 SPI接口可配置为支持SPI协议或者支持I2S音频协议,默认是SPI模式。可以通过软件切换到I2S方式。
2.2 具体特点详解
2.2.1 从选择(NSS)脚管理
STM32学习心得三十:SPI接口原理、配置及实验
2.2.2 时钟信号的相位和极性
STM32学习心得三十:SPI接口原理、配置及实验
STM32学习心得三十:SPI接口原理、配置及实验
STM32学习心得三十:SPI接口原理、配置及实验
CPHA和CPOL的目的是为了配合从机外设(其极性一般有严格要求)。
2.2.3 数据帧格式
STM32学习心得三十:SPI接口原理、配置及实验
2.2.4 状态标志
STM32学习心得三十:SPI接口原理、配置及实验
2.2.5 SPI中断
STM32学习心得三十:SPI接口原理、配置及实验
2.3 SPI引脚配置和模式
2.3.1 SPI1引脚配置
STM32学习心得三十:SPI接口原理、配置及实验
2.3.2 SPI2引脚配置
STM32学习心得三十:SPI接口原理、配置及实验
2.3.3 SPI3引脚配置
STM32学习心得三十:SPI接口原理、配置及实验
2.3.4 引脚模式
STM32学习心得三十:SPI接口原理、配置及实验
3. 常用寄存器及库函数
3.1 相关寄存器
3.1.1 SPI控制寄存器1(SPI_CR1);
3.1.2 SPI控制寄存器2(SPI_CR2);
3.1.3 SPI状态寄存器(SPI_SR);
3.1.4 SPI数据寄存器(SPI_DR);
3.1.5 SPI_I2S配置寄存器(SPI_I2S_CFGR);
3.1.6 SPI_I2S预分频寄存器(SPI_I2SPR)。
3.2 相关库函数
3.2.1 void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
3.2.2 void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
详解 void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)的第二个入口参数

typedef struct
{
uint16_t SPI_Direction;          /*Specifies the SPI unidirectional or bidirectional data mode.*/
uint16_t SPI_Mode;               /* Specifies the SPI operating mode.*/
uint16_t SPI_DataSize;           /* Specifies the SPI data size.*/
uint16_t SPI_CPOL;               /* Specifies the serial clock steady state.*/
uint16_t SPI_CPHA;               /*Specifies the clock active edge for the bit capture.*/
uint16_t SPI_NSS;   
/* Specifies whether the NSS signal is managed by hardware (NSS pin) or by software using the SSI bit.*/
uint16_t SPI_BaudRatePrescaler;  
 /* Specifies the Baud Rate prescaler value which will be used to configure the transmit and receive SCK clock.*/
uint16_t SPI_FirstBit;           /* Specifies whether data transfers start from MSB or LSB bit.*/
uint16_t SPI_CRCPolynomial;      /* Specifies the polynomial used for the CRC calculation. */
}SPI_InitTypeDef;

3.2.3 void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
3.2.4 void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
3.2.5 void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
3.2.6 void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
3.2.7 uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
3.2.8 void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
3.2.9 FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
3.2.10 void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
3.2.11 ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
3.2.12 void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
4. 程序配置过程
4.1 配置相关引脚的复用功能,使能SPIx时钟:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

4.2 初始化SPIx,设置SPIx工作模式:

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

4.3 使能SPIx:

void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

4.4 SPI传输数据:

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;

4.5 查看SPI传输状态:

SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE);

5. 硬件连接
STM32学习心得三十:SPI接口原理、配置及实验
6. W25Q128JV芯片
6.1 概念
W25Q128JV(128M-bit)串行闪存为有限空间、引脚和电源的系统提供了存储解决方案。25Q系列提供的灵活性和性能远远超过普通的串行闪存设备。
W25Q128将16M的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。W25Qxx的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Qxx开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。
JV表示速度为133MHz:
STM32学习心得三十:SPI接口原理、配置及实验
6.2 W25Q128部分指令集及驱动代码编写(还可以参考)
6.2.1 Write Enable (06h) 写入使能指令
举例:

void W25QXX_Write_Enable(void)   
{
W25QXX_CS=0;                            //W25QXX_CS对应PBout(12),即PB12置0//    
SPI2_ReadWriteByte(W25X_WriteEnable);   //写入使能,W25X_WriteEnable为0x06//  
W25QXX_CS=1;                            //取消片选,PB12值1//      
}  

6.2.2 W25X_WriteDisable (0x04) 写入禁止指令
举例:

void W25QXX_Write_Disable(void)   
{  
 W25QXX_CS=0;                              //使能器件//   
 SPI2_ReadWriteByte(W25X_WriteDisable);    //发送写入禁止指令,W25X_WriteDisable为0x04//     
 W25QXX_CS=1;                              //取消片选//            
}  

6.2.3 W25X_ReadStatusReg (05h) 读取状态寄存器指令
6.2.4 W25X_WriteStatusReg (01h)写入状态寄存器
6.2.5 W25X_ReadData (03h)读取数据指令
6.2.6 W25X_FastReadData (0Bh) 快速读取数据指令
6.2.7 W25X_FastReadDual (3Bh) 快速双端口输出方式读取存储器数据指令
6.2.8 W25X_PageProgram (02h) 页编程指令
6.2.9 W25X_BlockErase (D8h) 块擦除指令
6.2.10 W25X_SectorErase (20h) 扇擦除指令
6.2.11 W25X_ChipErase (C7h) 芯片擦除指令
6.2.12 W25X_PowerDown (B9h) 掉电指令
6.2.13 W25X_ReleasePowerDown (ABh) 释放掉电指令
6.2.14 W25X_DeviceID (ABh) 设备ID号指令
6.2.15 W25X_ManufactDeviceID (90h) 制造商设备ID号指令
6.2.16 W25X_JedecDeviceID (9Fh) JEDEC(Joint Electron Device Engineering Council)读取电子元件工业联合会设备号指令
6.3 W25Qxx_Write函数思路
STM32学习心得三十:SPI接口原理、配置及实验
6.3.1 每个sector是4K,也就是4096个地址,在写任何一个地址之前,如果该地址的值不是0xFF,必须先擦除对应的sector,然后再写。
6.3.2 主要步骤:
1) 据要写的起始地址,确定要写的起始区域的Sector号以及在起始Sector中的偏移量;
2) 根据要写的起始地址和字节数,确定要写的数据是否跨sector;
3) 定好要操作的sector以及sector的地址范围;
4) 对每一个sector,先遍历要写的地址区域保存的数据是不是0xff,如果都是,就不用擦除。如果有不是0xff的区域,先读出里面的数据,保存在缓存W25QXX_BUFFER,然后擦除里面的内容。然后把这个sector要操作的数据,写到缓存。最后一次性吧缓存W25QXX_BUFFER的数据写到这个对应的sector。
7. 部分代码解读
7.1 W25Q128.h头文件代码解读

#ifndef __FLASH_H
#define __FLASH_H       
#include "sys.h" 
#define W25Q128 0XEF17               //本开发版采用的芯片是W25Q128,对应制造商设备ID号0xEF17//
extern u16 W25QXX_TYPE;              //定义W25QXX芯片型号//     
#define W25QXX_CS   PBout(12)        //W25QXX的片选信号//
//W25Q128 指令表//
#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    //设备ID号//
#define W25X_ManufactDeviceID 0x90   //制造商设备ID号//
#define W25X_JedecDeviceID   0x9F    //JEDEC(Joint Electron Device Engineering Council)电子元件工业联合会//
//申明14个函数//
void W25QXX_Init(void);             //W25Qxx初始化函数*//
u16  W25QXX_ReadID(void);           //读取FLASH ID函数*//
u8  W25QXX_ReadSR(void);            //读取状态寄存器函数*// 
void W25QXX_Write_SR(u8 sr);        //写入状态寄存器函数*//
void W25QXX_Write_Enable(void);     //写入使能函数*// 
void W25QXX_Write_Disable(void);    //写入失能函数*//
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//无检查写入flash函数*//
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead);           //读取flash函数*//
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);        //写入flash函数//
void W25QXX_Erase_Chip(void);                                           //整片芯片擦除函数*//
void W25QXX_Erase_Sector(u32 Dst_Addr);                                 //扇区擦除函数*//
void W25QXX_Wait_Busy(void);                                            //等待空闲函数*//
void W25QXX_PowerDown(void);                                            //进入掉电模式函数*//
void W25QXX_WAKEUP(void);                                               //唤醒函数*//
#endif

7.2 W25Q128.c文件代码解读

#include "w25qxx.h" 
#include "spi.h"
#include "delay.h"
#include "usart.h"
u16 W25QXX_TYPE=W25Q128; //型号W25Q128,4K字节为一个扇区,16个扇区为1个块,容量为16M字节,共128个块,4096个扇区//               
//初始化SPI FLASH的IO口//
void W25QXX_Init(void)
{ 
 GPIO_InitTypeDef GPIO_InitStructure;
 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;             // PB12,对应该闪存的CS引脚// 
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;       //推挽输出//
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(GPIOB, &GPIO_InitStructure);
 GPIO_SetBits(GPIOB,GPIO_Pin_12);                       //PB12置高电平//
 W25QXX_CS=1;                                           //SPI FLASH不选中,其实跟上一行代码实现功能一样//
 SPI2_Init();                                           //初始化SPI//
 SPI2_SetSpeed(SPI_BaudRatePrescaler_2);                //重新设置波特率为18M时钟,高速模式//
 W25QXX_TYPE=W25QXX_ReadID();                           //读取FLASH ID//  
}  
//编写读取W25QXX的状态寄存器函数,这里只读取S0-S7位的值//
u8 W25QXX_ReadSR(void)   
{  
 u8 byte=0;   
 W25QXX_CS=0;                            //使能器件//   
 SPI2_ReadWriteByte(W25X_ReadStatusReg); //发送读取状态寄存器指令,W25X_ReadStatusReg=0x05,可忽略接收值//     
 byte=SPI2_ReadWriteByte(0Xff);          //读取一个字节,发送0xff,读取回来的值传至byte//  
 W25QXX_CS=1;                            //取消片选     
 return byte;   
} 
//编写写入W25QXX状态寄存器函数,这里只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写???//
void W25QXX_Write_SR(u8 sr)   
{   
 W25QXX_CS=0;                            //使能器件//   
 SPI2_ReadWriteByte(W25X_WriteStatusReg);//发送写入状态寄存器指令,W25X_WriteStatusReg=0x01,忽略接收值//     
 SPI2_ReadWriteByte(sr);                 //写入一个字节,忽略接收值//  
 W25QXX_CS=1;                            //取消片选//            
}   
//W25QXX写入使能,将WEL置1//   
void W25QXX_Write_Enable(void)   
{
 W25QXX_CS=0;                             //W25QXX_CS对应PBout(12),即PB12置0//    
 SPI2_ReadWriteByte(W25X_WriteEnable);    //写入使能,W25X_WriteEnable的指令为0x06//  
 W25QXX_CS=1;                             //取消片选,即PB12置1//            
} 
//W25QXX写入禁止,将WEL清零,即置0//  
void W25QXX_Write_Disable(void)   
{  
 W25QXX_CS=0;                              //使能器件//   
 SPI2_ReadWriteByte(W25X_WriteDisable);    //发送写入禁止指令,W25X_WriteDisable为0x04//     
 W25QXX_CS=1;                              //取消片选//            
}   
//读取芯片ID,本开发版返回值0XEF17,表示芯片型号为W25Q128//    
u16 W25QXX_ReadID(void)
{
 u16 Temp = 0;   
 W25QXX_CS=0;        
 SPI2_ReadWriteByte(0x90);           //发送读取ID命令,即0x90//     
 SPI2_ReadWriteByte(0x00);           //后面跟一个24位0x000000地址//
 SPI2_ReadWriteByte(0x00);      
 SPI2_ReadWriteByte(0x00);         
 Temp|=SPI2_ReadWriteByte(0xFF)<<8;  //读取Manufacturer ID号,即0xEF//
 Temp|=SPI2_ReadWriteByte(0xFF);     //读取Device ID号,即0x17//
 W25QXX_CS=1;        
 return Temp;
}         
//pBuffer:数据存储区,ReadAddr:开始读取的地址(24bit),NumByteToRead:要读取的字节数(最大65535)//
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)   
{ 
  u16 i;                 
  W25QXX_CS=0;                                //使能器件//   
  SPI2_ReadWriteByte(W25X_ReadData);          //发送读取指令,W25X_ReadData为0x03//   
  SPI2_ReadWriteByte((u8)((ReadAddr)>>16));   //发送24bit地址,最低的8位//    
  SPI2_ReadWriteByte((u8)((ReadAddr)>>8));    //发送24bit地址,次低的8位//  
  SPI2_ReadWriteByte((u8)ReadAddr);           //发送24bit地址,高8位// 
  for(i=0;i<NumByteToRead;i++)
 { 
    pBuffer[i]=SPI2_ReadWriteByte(0XFF);      //循环读数,每次读1个字节//  
   }
 W25QXX_CS=1;                 
}  
//SPI在一页(0~65535个字节)内写入少于256个字节的数据//
//pBuffer:数据存储区,WriteAddr:开始写入的地址(24bit),NumByteToWrite:要写入的字节数(最大256),不应超过该页的剩余字节数//  
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
  u16 i;  
  W25QXX_Write_Enable();                      //SET WEL// 
  W25QXX_CS=0;                                //使能器件//   
  SPI2_ReadWriteByte(W25X_PageProgram);       //发送写页命令,W25X_PageProgram为0x02//    
  SPI2_ReadWriteByte((u8)((WriteAddr)>>16));  //发送24bit地址,最低的8位//    
  SPI2_ReadWriteByte((u8)((WriteAddr)>>8));   //发送24bit地址,次低的8位//
  SPI2_ReadWriteByte((u8)WriteAddr);          //发送24bit地址,高8位//
  for(i=0;i<NumByteToWrite;i++)
   SPI2_ReadWriteByte(pBuffer[i]);            //循环写数,每次写1个字节//  
  W25QXX_CS=1;                                //取消片选// 
  W25QXX_Wait_Busy();                         //等待写入结束//
} 
//无检验写入SPI FLASH,确保所写地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败//
//具有自动换页功能,在指定地址开始写入指定长度的数据,但是要确保地址不越界//
//pBuffer:数据存储区,WriteAddr:开始写入的地址(24bit),NumByteToWrite:要写入的字节数(最大65535)//
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{        
 u16 pageremain;    
 pageremain=256-WriteAddr%256;                      //单页剩余的字节数//        
 if(NumByteToWrite<=pageremain)                     //判断要写入的字节数是否小于单页剩余的字节数//
  pageremain=NumByteToWrite;                        //如果小于,就将NumByteToWrite值赋给pageremain//             
 while(1)
 {    
  W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);  //一次写入少于256个字节的数据//
  if(NumByteToWrite==pageremain)break;              //如果写入字节数小于剩余字节数,则写完//
   else 
  {
   pBuffer+=pageremain;                           
   WriteAddr+=pageremain; 
   NumByteToWrite-=pageremain;                      //减去已经写入了的字节数//
   if(NumByteToWrite>256)pageremain=256;            //一次可以写入256个字节//
   else pageremain=NumByteToWrite;                  //不够256个字节//
  }
 };     
} 
//写SPI FLASH,在指定地址开始写入指定长度的数据,该函数带擦除操作//
//pBuffer:数据存储区,WriteAddr:开始写入的地址(24bit),NumByteToWrite:要写入的字节数(最大65535)//   
u8 W25QXX_BUFFER[4096];                            //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;                                //扇区起始地址//  
 secoff=WriteAddr%4096;                                //在该扇区内的偏移量//
 secremain=4096-secoff;                                //扇区剩余空间大小//   
 if(NumByteToWrite<=secremain)
  secremain=NumByteToWrite;                            //不大于4096个字节//
 while(1) 
 { 
  W25QXX_Read(W25QXX_BUF,secpos*4096,4096);            //读出整个扇区的内容,保存在W25QXX_BUF里//
  for(i=0;i<secremain;i++)                             //校验数据
  {
   if(W25QXX_BUF[secoff+i]!=0XFF)break;                //判断是否需要擦除//     
  }
  if(i<secremain)                                     
  {
   W25QXX_Erase_Sector(secpos);                        //擦除这个扇区//
   for(i=0;i<secremain;i++)                      
   {
    W25QXX_BUF[i+secoff]=pBuffer[i];                   //把要写的数据写到W25QXX_BUF缓存中//
    }
   W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);  //一次性吧缓存W25QXX_BUFFER的数据写到对应的sector// 
  }
  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;                          //下一个扇区可以写完//
  }  
 };  
}
//擦除整个芯片,等待时间超长//
void W25QXX_Erase_Chip(void)   
{                                   
  W25QXX_Write_Enable();                      //SET WEL// 
  W25QXX_Wait_Busy();   
  W25QXX_CS=0;                                //使能器件//   
  SPI2_ReadWriteByte(W25X_ChipErase);         //发送片擦除命令,W25X_ChipErase为0xC7//  
  W25QXX_CS=1;                                //取消片选// 
  W25QXX_Wait_Busy();                         //等待芯片擦除结束//
}   
//擦除一个扇区,Dst_Addr:扇区地址 根据实际容量设置,擦除一个扇区的最少时间:150ms//
void W25QXX_Erase_Sector(u32 Dst_Addr)   
{    
  Dst_Addr*=4096;
  W25QXX_Write_Enable();                      //SET WEL//   
  W25QXX_Wait_Busy();   
  W25QXX_CS=0;                                //使能器件//   
  SPI2_ReadWriteByte(W25X_SectorErase);       //发送扇区擦除指令,W25X_SectorErase为0x20// 
  SPI2_ReadWriteByte((u8)((Dst_Addr)>>16));   //发送24bit地址//    
  SPI2_ReadWriteByte((u8)((Dst_Addr)>>8));   
  SPI2_ReadWriteByte((u8)Dst_Addr);  
  W25QXX_CS=1;                                //取消片选//            
  W25QXX_Wait_Busy();                         //等待擦除完成//
}  
//等待空闲函数,只有当状态寄存器的S0位(BUSY)置0时,循环结束//
void W25QXX_Wait_Busy(void)   
{   
 while((W25QXX_ReadSR()&0x01)==0x01);    
 }  
//进入掉电模式//
void W25QXX_PowerDown(void)   
{ 
  W25QXX_CS=0;                                //使能器件//   
  SPI2_ReadWriteByte(W25X_PowerDown);         //发送掉电命令,W25X_PowerDown为0xB9//  
  W25QXX_CS=1;                                //取消片选//            
  delay_us(3);                                //等待tDP//  
}   
//唤醒,释放掉电模式//
void W25QXX_WAKEUP(void)   
{  
  W25QXX_CS=0;                               //使能器件//   
  SPI2_ReadWriteByte(W25X_ReleasePowerDown); //发生释放掉电指令,ReleasePowerDown为0xAB//    
  W25QXX_CS=1;                               //取消片选//            
  delay_us(3);                               //等待TRES1//
}  

7.3 spi.h头文件代码解读

#ifndef __SPI_H
#define __SPI_H
#include "sys.h"
//申明三个函数//                           
void SPI2_Init(void);           //初始化SPI2接口//
void SPI2_SetSpeed(u8 SpeedSet);  //设置SPI2速度//   
u8 SPI2_ReadWriteByte(u8 TxData); //SPI总线读写一个字节//  
#endif

7.4 spi.c文件代码解读

#include "spi.h"
//SPI2初始化函数:配置成主机模式,访问W25Q128//        
void SPI2_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;  //GPIO初始化结构体,选择SPI2,对应PB12,PB13,PB14,PB15//
  SPI_InitTypeDef  SPI_InitStructure;   //SPI初始化结构体//
  RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );     //GPIOB时钟使能// 
  RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2,  ENABLE );     //SPI2时钟使能//  
  //初始化GPIOB,PB13/14/15都设置复用推挽输出,PB14对应MISO,最好设为带上拉输入// 
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
  GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);   //PB13/14/15置高电平//
  //初始化SPI函数//
  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工//
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  
  //针对SPI_CR1寄存器的SSI位和MSTR为,均设置1,即SPI工作模式为主SPI//
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;  
  //针对SPI_CR1寄存器的DFF位,设置数据帧大小为8位//
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
  //针对SPI_CR1寄存器的CPOL位,串行同步时钟的空闲状态为高电平// 
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;  
  //针对SPI_CR1寄存器的CPHA位,串行同步时钟的第二个跳变沿(上升沿)数据被采样//
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; 
  //针对SPI_CR1寄存器的SSM位,NSS信号由软件(使用SSI位)管理//
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;  
  //针对SPI_CR1寄存器的BR位,波特率预分频值为256//
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;  
  //针对SPI_CR1寄存器的LSBFIRST位,数据传输从MSB位开始//
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; 
  //针对SPI_CRCPR寄存器的CRCPOLY位,设为0x0007,为复位值//
  SPI_InitStructure.SPI_CRCPolynomial = 7; 
  SPI_Init(SPI2, &SPI_InitStructure);  
  SPI_Cmd(SPI2, ENABLE);                                     //使能SPI外设//
  //启动传输,目的是让MOSI维持高。因为一般空闲状态电平都是高,这样不容易出问题// 
  SPI2_ReadWriteByte(0xff);                                  
}   
//SPI2速度设置函数// 
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
 assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));   //有效性判断//
 SPI2->CR1&=0XFFC7;                                                //先将SPI_CR1寄存器的BR位置000//
 SPI2->CR1|=SPI_BaudRatePrescaler;                                 //再设置SPI2速度// 
 SPI_Cmd(SPI2,ENABLE); 
} 
//u8 SPI2_ReadWriteByte(u8 TxData)读写一个字节函数,TxData:要写入的字节,返回值:读取到的字节//
u8 SPI2_ReadWriteByte(u8 TxData)
{  
 u8 retry=0; 
 //检查SPI_SR寄存器的TXE位(发送缓冲为空),其值0时为非空,1时为空// 
 while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) 
  {
   retry++;                           //发送缓冲为空时,retry++//
   if(retry>200)return 0;
  }     
 SPI_I2S_SendData(SPI2, TxData);       //通过外设SPI2发送一个数据//
 retry=0;
 //检查SPI_SR寄存器的RXNE位(接收缓冲为空),其值0时为空,1时为非空// 
 while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) 
  {
   retry++;                           //当接收缓冲为非空时,retry++//
   if(retry>200)return 0;
  }             
 return SPI_I2S_ReceiveData(SPI2);    //返回通过SPIx最近接收的数据//         
}

7.4 main.c文件代码解读

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"  
#include "w25qxx.h"        
//写入到W25Q128的字符串数组//
const u8 TEXT_Buffer[]={"这是我的SPI实验"};
#define SIZE sizeof(TEXT_Buffer)
int main(void)
 {  
 u8 key;
 u16 i=0;
 u8 datatemp[SIZE];
 u32 FLASH_SIZE;  
 delay_init();                   //延时函数初始化//   
 uart_init(115200);              //串口初始化为115200//
 LED_Init();                     //初始化与LED连接的硬件接口//
 KEY_Init();                     //按键初始化//      
 W25QXX_Init();                  //W25QXX初始化//
 while(W25QXX_ReadID()!=W25Q128) //while循环直到检测到W25Q128闪存才退出//
 {
  printf("检测不到W25Q128闪存\n");
  delay_ms(500);
  LED1=!LED1;
 }
 LED1=0;  
 FLASH_SIZE=256*16*4*1024*8;     //FLASH大小为16M字节,分256块,每个块分16个扇区,每个扇区4k字节//   
 while(1)
 {
  key=KEY_Scan(0);
  if(key==KEY1_PRES)             //KEY1按下,写入W25Q128//
  {
   printf("\n开始写入W25Q128闪存数据\r\n");
   W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-1000,SIZE);   //从倒数第1000个地址处开始,写入SIZE长度的数据//
   printf("\n写入W25Q128闪存数据完成\r\n");
  }
  if(key==KEY0_PRES)             //KEY0按下,读取字符串并显示//
  {
   printf("\n开始读取W25Q128闪存数据\r\n");
   W25QXX_Read(datatemp,FLASH_SIZE-1000,SIZE);            //从倒数第1000个地址处开始,读出SIZE个字节//
   printf("\n读取W25Q128闪存数据完成\r\n");
   printf("\n从W25Q128闪存读取数据的内容为:%s\r\n",datatemp);
  }
  i++;
  delay_ms(10);
  if(i==20)
  {
   LED0=!LED0;                                            //LED0每隔200ms闪烁,提示系统正在运行// 
   i=0;
  }     
 }
}

8. 实验结果
STM32学习心得三十:SPI接口原理、配置及实验
旧知识点
1)复习如何新建工程模板,可参考STM32学习心得二:新建工程模板
2)复习基于库函数的初始化函数的一般格式,可参考STM32学习心得三:GPIO实验-基于库函数
3)复习寄存器地址,可参考STM32学习心得四:GPIO实验-基于寄存器
4)复习位操作,可参考STM32学习心得五:GPIO实验-基于位操作
5)复习寄存器地址名称映射,可参考STM32学习心得六:相关C语言学习及寄存器地址名称映射解读
6)复习时钟系统框图,可参考STM32学习心得七:STM32时钟系统框图解读及相关函数
7)复习延迟函数,可参考STM32学习心得九:Systick滴答定时器和延时函数解读
8)复习ST-LINK仿真器的参数配置,可参考STM32学习心得十:在Keil MDK软件中配置ST-LINK仿真器
9)复习ST-LINK调试方法,可参考STM32学习心得十一:ST-LINK调试原理+软硬件仿真调试方法
10)复习串口通信相关知识,可参考STM32学习心得十四:串口通信相关知识及配置方法