SPI协议学习Cubmx——读写Flash W25Q64

这是最好的时代,这是最坏的时代;
这是智慧的时代,这是愚蠢的时代;
这是信仰的时期,这是怀疑的时期;
这是光明的季节,这是黑暗的季节;
这是希望之春,这是失望之冬;
人们面前有着各样事物,人们面前一无所有;
人们正在直登天堂,人们正在直下地狱。 ——狄更斯《双城记》

一、SPI简介

SPI(Serial Peripheral Interface),从英文上理解,顾名思义就是串行外围设备接口。该协议是由摩托罗拉公司提出的通讯协议,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用于 EEPROMFLASH实时时钟AD转换器,还有数字信号处理器数字信号解码器之间。

芯片的管脚上只占用四根线:

MISO:主器件(master)数据输出,从器件(slave)数据输入;
MOSI:主器件(master)数据输入,从器件(slave)数据输出;
SCL串行时钟信号,由主设备(master)控制发出;
CS(NSS)从设备片选信号,由主设备控制,它的功能是用来作为片选引脚,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。

  • SPI:一对一

SPI:一对一

  • SPI:一对多

SPI:一对多


  • SPI设备选择

SPI是[单主设备( single-master )]通信协议,这意味着总线中的只有一支中心设备能发起通信。当SPI主设备想读/写[从设备]时,它首先拉低[从设备]对应的SS线(SS是低电平有效),接着开始发送工作脉冲到时钟线上,在相应的脉冲时间上,[主设备]把信号发到MOSI实现“写”,同时可对MISO采样而实现“读”,如下图:

  • SPI数据发送接收

SPI主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。

  1. 首先拉低对应的CS(也就是SS)信号线,表示将要与该设备进行通信,即“双方已经准备好进行通信”;
  2. 主机通过发送SCLK时钟信号,来告诉从机写数据或者读数据,注意:SCLK时钟信号可能是低电平有效,也可能是高电平有效,因为SPI有四种模式,且在下面的讲解中听我娓娓道来。
  3. 主机将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(0~7),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区;
  4. 从机(Slave)也将自己的串行移位寄存器(0~7)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。

    注:SPI只有主模式和从模式之分,毕竟SPI是一种通讯协议,它会有不同的模式,但并不会有读和写这样的操作,读和写是在SPI准备好之后可以进行的工作,类似的把SPI看作“仪器设施”,“读和写”是在仪器设施上要进行的操作,外设的写操作和读操作是同步完成的。
    如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。
    也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。

二、SPI的四种工作模式

SPI的四种工作模式:简单地说,就是进行SCLK时钟信号线的配置

  • 以下是Cubemx中对SPI进行的模式选择:

  • SPI通信为什么有四种模式不同的工作模式:

不同的工作模式也就是意味着“可以改变”,但是我们所知道主设备和从设备这些硬件设备是我们无法改变的,但是如果他们要通信,他们的工作模式必须是要匹配的,也就是我们所说的工作在同一种模式下,“通信”:自然要在相同的时钟频率下,**时钟信号(SCLK)**无疑控制他们工作在同一模式下的重要因素。我们通过CPOL(时钟极性)和CPHA(时钟相位)来对我们的主设备的SPI模式进行配置,控制我们的通信模式。

具体如下:
  1. 时钟极性(CPOL: Clock Polarity)定义了时钟空闲状态电平:
  • CPOL = 0,表示当SCLK=0时,处于空闲状态,所以有效状态就是SCLK处于高电平;
  • CPOL = 1,表示当SCLK=1时,处于空闲状态,所以有效状态就是SCLK处于低电平;
  1. 时钟相位(CPHA: Clock Phase)定义数据的采集时间:
  • CPHA = 0,表示:在时钟的第一个跳变沿(上升沿或者下降沿)进行数据采集,在第二个跳变沿发送数据;
  • CPHA = 1,表示:在时钟的第二个跳变沿(上升沿或者下降沿)进行数据采集,在第一个跳变沿发送数据;
四种模式:
  • Mode0:CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),数据发送是在下降沿(发送数据)
  • Mode1:CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
  • Mode2:CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
  • Mode3:CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

注:它们的区别是:定义了在时钟脉冲的哪条边沿转换(toggles)输出信号,哪条边沿采样输入信号,还有时钟脉冲的稳定电平值(就是时钟信号无效时是高还是低)。每种模式由一对参数刻画,它们称为时钟极(clock polarity)CPOL与时钟期(clock phase)CPHA。


三、SPI的通讯协议:

SPI简单的收发时序:上升沿发送、下降沿接收、高位先发送。

  • 主从设备必须使用相同的工作模式——SCLK、CPOL(时钟极性)和CPHA(时钟相位),才能正常工作。
  • 如果有多个从设备,并且它们使用了不同的工作模式,那么主设备必须在读写不同从设备时需要重新修改对应从设备的模式
  • 上述就是通讯协议的主要内容,SPI是一个很“随意”的通讯协议,他只需要我们能够正确连接它的四根线,并且配置SPI工作的模式相同,将CS/SS信号线拉低,就可以进行通信,一次一个字节传输。(没有规定最大传输速率,没有地址方案,也没规定通信应答机制,没有规定流控制规则。通信控制都得通过SPI设备自行实现,SPI并不关心物理接口的电气特性,例如信号的标准电压。)
  • 注:SPI接口的一个弊端:没有指定的流控制,没有应答机制确定是否接收到数据。

四、SPI 的三种工作模式

这里不要搞混了,刚刚上面讲得是SPI的四种通信模式,是为了保证SPI正常工作的,现在讲的是,它在正常工作时的三种模式。

SPI有三种工作模式:运行、等待和停止。

运行模式(Run Mode)

这是基本的工作模式。

等待模式(Wait Mode)

SPI工作在等待模式是一种可配置的低功耗模式,可以通过SPICR2寄存器的SPISWAI位进行控制。在等待模式下:

  • 如果SPISWAI位清0,SPI操作类似于运行模式。
  • 如果SPISWAI位置1,SPI进入低功耗状态,并且SPI时钟将关闭。
  • 如果SPI配置为主机,所有的传输将停止,但是会在CPU进入运行模式后重新开始。
  • 如果SPI配置为从机,会继续接收和传输一个字节,这样就保证从机与主机同步。

停止模式(Stop Mode)

为了降低功耗,SPI在停止模式是不活跃的。如果SPI配置为主机,正在进行的传输会停止,但是在CPU进入运行模式后会重新开始。如果SPI配置为从机,会继续接受和发送一个字节,这样就保证了从机与主机同步。


四、SPI原理图连接及引脚分布

  • 基于芯片:STM32F103RBT6(正点原子NANO)
  • 各个SPI在芯片原理图上的分布:


  • 引脚分布:

STM32芯片有多个SPI外设,不同信号的芯片,有不同的个数。它们的 SPI 通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚。其中 SPI1 是 APB2 上的设备,最高通信速率达 36Mbtis/s,SPI2、SPI3 是 APB1 上的设备,最高通信速率为 18Mbits/s。除了通讯速率,在其它功能上没有差异。其中 SPI3 用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是 IO 口,如果想使用 SPI3 接口,则程序上必须先禁用掉这几个 IO 口的下载功能。一般在资源不是十分紧张的情况下,这几个 IO 口是专门用于下载和调试程序,不会复用为 SPI3。

  • FLASH芯片:

开发板中的 FLASH 芯片型号:W25Q64W25Q 系列为台湾华邦公司推出的是一种使用 SPI 通讯协议的 NOR FLASH 存储器。芯片型号后两位表示芯片容量,例如 W25Q6464 就是指 64Mbit 也就是 8M 的容量。它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SPI 引脚 NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚虽然是其片上 SPI 外设的硬件引脚,但实际上后面的程序只是把它当成一个普通的 GPIO,使用软件的方式控制 NSS 信号,所以在 SPI 的硬件设计中,NSS 可以随便选择普通的 GPIO,不必纠结于选择硬件 NSS 信号。

FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。

  • 通过控制STM32利用SPI总线向FLASH芯片发送指令,FLASH芯片收到后就会进行相应的操作。


  • 主机首先通过 MOSI 线向 FLASH 芯片发送第一个字节数据为“9F H”,当 FLASH 芯片收到该数据后,它会解读成主机向它发送了“JEDEC 指令”,然后它就作出该命令的响应:通过 MISO 线把它的厂商 ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备 ID 来测试硬件是否连接正常,或用于识别设备。
  • 对于FLASH芯片的其他指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多数据。
  • In fact,在编写设备驱动时都是有一定的规律可循的。
  • 首先:我们需要知道设备使用的是什么通讯协议。(I2C、SPI……) EEPROM使用的是I2C,FLASH使用的是SPI……再根据它的通讯协议,选择好STM32硬件模块,并进行相应的I2C或者是SPI模块的初始化;

  • 接着:我们要了解目标设备的相关指令。 因为不同的的设备,都会有相应的不同的指令。如 EEPROM 中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而 FLASH 则定义了更多的指令,有写指令,读指令,读 ID 指令等等。

  • 最后:我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。


五、STM32中SPI初始化配置

  1. 初始化GPIO口,配置相关引脚的复用功能,使能SPIx时钟。调用函数:void GPIO_Init();
  2. 使能SPI时钟总线。调用函数:RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  3. 配置SPI初始化的参数,设置SPI工作模式。调用函数:SPI_Init(SPI1, &SPI_Initstructure);
  4. 使能SPI外设。调用函数:SPI_Cmd(SPI1,ENABLE);

SPI配置设置:

typedef struct
{
uint16_t SPI_Direction;  /*!< 传输方向,两向全双工,单向接收等*/
uint16_t SPI_Mode;       /*!< 模式选择,确定主机还是从机 */
uint16_t SPI_DataSize;   /*!< 数据大小,8位还是16位 */
uint16_t SPI_CPOL;       /*!< 时钟极性选择 */
uint16_t SPI_CPHA;       /*!< 时钟相位选择 */
uint16_t SPI_NSS;        /*!< 片选是硬件还是软件*/
uint16_t SPI_BaudRatePrescaler;     /*!< 分频系数 */
uint16_t SPI_FirstBit;              /*!< 指定数据传输是从MSB还是LSB位开始的。M
SB就是二进制第一位,LSB就是最后一位 */
uint16_t SPI_CRCPolynomial;         /*!< CRC校验 ,设置 CRC 校验多项式,提高通
信可靠性,大于 1 即可*/
}SPI_InitTypeDef;


SPI初始化:

void SPI2_Init(void)
{
 	GPIO_InitTypeDef GPIO_InitStructure;
  SPI_InitTypeDef  SPI_InitStructure;
 
	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能 
	RCC_APB1PeriphClockCmd(	RCC_APB1Periph_SPI2,  ENABLE );//SPI2时钟使能 	
 
	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
 
 	GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);  //PB13/14/15上拉
 
	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_256;		//定义波特率预分频的值:波特率预分频值为256
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
	SPI_Init(SPI2, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
 
	SPI_Cmd(SPI2, ENABLE); //使能SPI外设
	
	SPI2_ReadWriteByte(0xff);//启动传输		 

} 

SPI发送函数:

  • 标准库:
u8 SPIx_ReadWriteByte(u8 TxData)
{
    u8 retry=0;
    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET)
    {
    }//等待发送区空
    SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个byte 数据
    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET)
    {
    } //等待接收完一个byte
    return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据
}
  • HAL库:
uint8_t SPI_SendByte(uint8_t byte)
{
    uint8_t d_read,d_send=byte;
    if(HAL_SPI_TransmitReceive(&hspi1,&d_send,&d_read,1,0xFFFFFF)!=HAL_OK)
        d_read=0XFF;
    return d_read;
}

六、新建工程

1.打开STM32CubeMX软件,点击“新建工程”

2. 选择 MCU 和封装

3.配置时钟



具体学习可以参考:本人博客网站-RCC学习

4.配置调试模式

5.SPI配置
  • 参数配置:
    Connectivity 中选择 SPI1 设置,并选择 Full-Duplex Master 全双工主模式,不开启 NSS 即,不使用硬件片选信号。

    • 通信模式:主机全双工

    因为我们需要和W25Q128V芯片中的闪存芯片进行通信,所以设置为 主机全双工。 前面讲了SPI有四种通信模式。

    • 不使能硬件NSS

    原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PB12 配置为普通 GPIO,手动控制片选信号。

原理图中虽然将 CS 片选接到了硬件 SPI2NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PB12 配置为普通 GPIO,手动控制片选信号。

在引脚图中找到 SPI NSS 对应的引脚,选择GPIO_Output注:正点原子NANO开发板 SP2 NSS选择PB12。野火STM32F103指南者开发板SPI1 NSS须配置为PC0。

修改初始引脚电平为 High ,标签为 SPI2_CS 或者其他。

SPI 为默认设置不作修改。只需注意一下,Prescaler 分频系数最低为 2,波特率 (Baud Rate) 为 18.0 MBits/s。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s

  • Clock Polarity(CPOL): SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
  • Clock Phase(CPHA): 指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。

根据 FLASH 芯片的说明,它支持 SPI 模式0模式 3,支持双线全双工,使用 MSB 先行模式,数据帧长度为 8 位。

所以这里配置 CPOLLowCPHA1 EdgeSPI 模式0

  • 生成代码:
    输入项目名称和路径。(注:路径中不允许出现中文。)

    选择应用的IDE,开发环境MDK-ARM V5

    每个外设生成独立的 ’.c/.h’ 文件
  • 不勾: 所有初始化代码都生成在 main.c
  • 勾选: 初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。

    点击 GENERATE CODE 生成代码

七、SPI函数讲解

stm32f1xx_hal_spi.h 头文件中可以看到 SPI 的操作函数。分别对应轮询,中断和DMA三种控制方式。

  • 轮询: 最基本的发送接收函数,就是正常的发送数据和接收数据;

轮询(Polling)是一种CPU决策如何提供周边设备服务的方式,又称“程控输入输出”(Programmed I/O)。轮询法的概念是:由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。

  • 中断: 在SPI发送或者接收完成的时候,会进入SPI回调函数,用户可以编写回调函数,实现设定功能(类似于其他通信协议(USART、I2C)的中断);

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

  • DMA: DMA 传输 SPI 数据;

DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

SPI发送数据函数:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据

参数:

  • *hspi: 选择SPI1/2(句柄),比如&hspi1,&hspi2
  • *pData:需要发送的数据,可以为数组(指针指向数组的首地址);
  • Size:发送数据的字节数,为 1 就表示发送一个字节的数据;
  • Timeout: 超时时间,就是执行发送函数最长的时间,超过该时间自动退出发送函数;
SPI接收数据函数:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据

参数:

  • *hspi: 选择SPI1/2(句柄),比如&hspi1,&hspi2
  • *pData: 接收发送过来的数据的数组;
  • Size:接收数据的字节数,为 1 就表示发送一个字节的数据;
  • Timeout: 超时时间,就是执行发送函数最长的时间,超过该时间自动退出发送函数;
SPI接收回调函数:
 HAL_SPI_TransmitReceive_IT(&hspi1, TXbuf,RXbuf,CommSize);

注: 当SPI上接收出现了 CommSize个字节的数据后,中断函数会调用SPI回调函数:

 HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)

用户可以重新定义回调函数,编写预定功能即可,在接收完成之后便会进入回调函数。(其实通讯协议(USART、I2C)的回调函数都挺类似的)

片选引脚:

因为我们在CubeMX中配置的是软件使能片选,定义片选引脚, CS片选低电平为有效使能, CS片选高电平不使能。
需要使用两个宏定义来代替:
main.h 中有宏定义命名,SPI2_CS_Pin 就是PB12

//以W25Q128为例
#define SPI_CS_Enable() 			HAL_GPIO_WritePin(GPIOB, SPI2_CS_Pin, GPIO_PIN_RESET)
#define SPI_CS_Disable() 		HAL_GPIO_WritePin(GPIOB, SPI2_CS_Pin, GPIO_PIN_SET)

八、编写代码

1.封装SPI Flash(W25Q64)的命令和底层函数
  • SPI Flash 发送数据的函数
/**
 * @brief    SPI发送指定长度的数据
 * @param    buf  —— 发送数据缓冲区首地址
 * @param    size —— 要发送数据的字节数
 * @retval   成功返回HAL_OK
 */
static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)
{
    return HAL_SPI_Transmit(&hspi2, send_buf, size, 100);
}
  • SPI Flash 接收数据的函数
/**
 * @brief   SPI接收指定长度的数据
 * @param   buf  —— 接收数据缓冲区首地址
 * @param   size —— 要接收数据的字节数
 * @retval  成功返回HAL_OK
 */
static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)
{
   return HAL_SPI_Receive(&hspi2, recv_buf, size, 100);
}
  • 发送数据的同时读取数据的函数
/**
 * @brief   SPI在发送数据的同时接收指定长度的数据
 * @param   send_buf  —— 接收数据缓冲区首地址
 * @param   recv_buf  —— 接收数据缓冲区首地址
 * @param   size —— 要发送/接收数据的字节数
 * @retval  成功返回HAL_OK
 */
static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)
{
   return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);
}
2.编写W25Q64的驱动程序
2.1 读取 Manufacture ID 和 Device ID

作用:

  • 检测 SPI Flash 是否存在
  • 可以根据 ID 判断 Flash 的具体型号

/**
 * @brief   读取Flash内部的ID
 * @param   none
 * @retval  成功返回device_id
 */
uint16_t W25QXX_ReadID(void)
{
    uint8_t recv_buf[2] = {0};    //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID
    uint16_t device_id = 0;
    uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00};   //待发送数据,命令+地址
    
    /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    /* 发送并读取数据 */
    if (HAL_OK == SPI_Transmit(send_data, 4)) 
    {
        if (HAL_OK == SPI_Receive(recv_buf, 2)) 
        {
            device_id = (recv_buf[0] << 8) | recv_buf[1];
        }
    }
    
    /* 取消片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    return device_id;
}
2.2 读取状态寄存器数据并判断Flash是否忙碌

SPI Flash 的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有 2-3 个状态寄存器,指示出 Flash 当前的状态,还有一点就是:

Flash 内部在执行命令时,此时不再执行 MCU 发来的命令,但是 MCU 却可以去一直读取 Flash 的状态寄存器,从而可以判断 Flash 是否忙碌。


/**
 * @brief     读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器
 * @param     reg  —— 状态寄存器编号(1~2)
 * @retval    状态寄存器的值
 */
static uint8_t W25QXX_ReadSR(uint8_t reg)
{
    uint8_t result = 0; 
    uint8_t send_buf[4] = {0x00,0x00,0x00,0x00};
    switch(reg)
    {
        case 1:
            send_buf[0] = READ_STATU_REGISTER_1;
        case 2:
            send_buf[0] = READ_STATU_REGISTER_2;
        case 0:
        default:
            send_buf[0] = READ_STATU_REGISTER_1;
    }
    
     /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    if (HAL_OK == SPI_Transmit(send_buf, 4)) 
    {
        if (HAL_OK == SPI_Receive(&result, 1)) 
        {
            HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
            
            return result;
        }
    }
    
    /* 取消片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);

    return 0;
}

然后编写 阻塞判断 Flash是否忙碌的函数:

/**
 * @brief   阻塞等待Flash处于空闲状态
 * @param   none
 * @retval  none
 */
static void W25QXX_Wait_Busy(void)
{
    while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}
2.3 读取数据

SPI Flash 读取数据可以任意地址 (地址长度32bit) 读任意长度数据 (最大 65535 Byte),没有任何限制。

/**
 * @brief   读取SPI FLASH数据
 * @param   buffer      —— 数据存储区
 * @param   start_addr  —— 开始读取的地址(最大32bit)
 * @param   nbytes      —— 要读取的字节数(最大65535)
 * @retval  成功返回0,失败返回-1
 */
int W25QXX_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes)
{
    uint8_t cmd = READ_DATA_CMD;
    
    start_addr = start_addr << 8;
    
    W25QXX_Wait_Busy();
    
     /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    if (HAL_OK == SPI_Transmit((uint8_t*)&start_addr, 3)) 
    {
        if (HAL_OK == SPI_Receive(buffer, nbytes)) 
        {
            HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
            return 0;
        }
    }
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    return -1;
}
2.4 写使能/禁止

Flash 芯片默认禁止写数据,所以 在向 Flash 写数据之前,必须发送命令开启写使能。

/**
 * @brief    W25QXX写使能,将S1寄存器的WEL置位
 * @param    none
 * @retval
 */
void W25QXX_Write_Enable(void)
{
    uint8_t cmd= WRITE_ENABLE_CMD;
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();

}

/**
 * @brief    W25QXX写禁止,将WEL清零
 * @param    none
 * @retval    none
 */
void W25QXX_Write_Disable(void)
{
    uint8_t cmd = WRITE_DISABLE_CMD;

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();
}
2.5 擦除扇区

SPI Flash 有个特性:

数据位可以由1变为0,但是不能由0变为1。

所以在向 Flash 写数据之前,必须要先进行擦除操作 ,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF (即全为1)。

/**
 * @brief    W25QXX擦除一个扇区
 * @param   sector_addr    —— 扇区地址 根据实际容量设置
 * @retval  none
 * @note    阻塞操作
 */
void W25QXX_Erase_Sector(uint32_t sector_addr)
{
    uint8_t cmd = SECTOR_ERASE_CMD;
    
    sector_addr *= 4096;    //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址
    sector_addr <<= 8;
    
    W25QXX_Write_Enable();  //擦除操作即写入0xFF,需要开启写使能
    W25QXX_Wait_Busy();        //等待写使能完成
   
     /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);
    
    SPI_Transmit((uint8_t*)&sector_addr, 3);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();       //等待扇区擦除完成
}
2.6 页写入操作

因为 Flash 内部的构造,向 Flash 芯片写数据的时候,可以按页写入。

/**
 * @brief    页写入操作
 * @param    dat —— 要写入的数据缓冲区首地址
 * @param    WriteAddr —— 要写入的地址
 * @param   byte_to_write —— 要写入的字节数(0-256)
 * @retval    none
 */
void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes)
{
    uint8_t cmd = PAGE_PROGRAM_CMD;
    
    WriteAddr <<= 8;
    
    W25QXX_Write_Enable();
    
    /* 使能片选 */
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
    
    SPI_Transmit(&cmd, 1);

    SPI_Transmit((uint8_t*)&WriteAddr, 3);
    
    SPI_Transmit(dat, nbytes);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();
}
3.在 main.c 文件中添加宏定义和全局变量
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define ManufactDeviceID_CMD    0x90
#define READ_STATU_REGISTER_1   0x05
#define READ_STATU_REGISTER_2   0x35
#define READ_DATA_CMD           0x03
#define WRITE_ENABLE_CMD        0x06
#define WRITE_DISABLE_CMD       0x04
#define SECTOR_ERASE_CMD        0x20
#define CHIP_ERASE_CMD          0xc7
#define PAGE_PROGRAM_CMD        0x02
/* USER CODE END PD */

/* Private user code ---------------------------------------------------------*/
SPI_HandleTypeDef hspi2;

UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */
uint16_t device_id;
uint8_t read_buf[10] = {0};     //读缓冲区
uint8_t write_buf[10] = {0};    //写缓冲区
int i;
/* USER CODE END PV */
  • 在主函数中,添加测试函数:
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SPI2_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

    device_id = W25QXX_ReadID();
    printf("W25Q64芯片的ID号为:0x%04x\r\n",device_id);
    
      /* 为了验证,首先读取要写入地址处的数据 */
    printf("************************ 初始数据 ************************\r\n");
    W25QXX_Read(read_buf, 0, 10);
        
    for(i = 0; i < 10; i++) 
    {
        printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
    }
        
    /* 擦除该扇区 */
    printf("********************** 擦除该扇区的数据 **********************\r\n");
    W25QXX_Erase_Sector(0);
    
    /* 再次读数据 */
    printf("****************** 在擦除扇区后,该地址处的数据 *******************\r\n");
    W25QXX_Read(read_buf, 0, 10);
    for(i = 0; i < 10; i++) 
    {
        printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
    }
        
    /* 写数据 */
    printf("************************ 写入数据 ************************\r\n");
    for(i = 0; i < 10; i++) 
    {
        write_buf[i] = i;
    }
    W25QXX_Page_Program(write_buf, 0, 10);
        
    /* 再次读数据 */
    printf("********************** 写入数据后,该地址处的数据 ***********************\r\n");
    W25QXX_Read(read_buf, 0, 10);
    for(i = 0; i < 10; i++) 
    {
        printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
    }
        
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
  • 重写fgetfput函数:
/**
  * 函数功能: 重定向c库函数printf到DEBUG_USARTx
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}
 
/**
  * 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
int fgetc(FILE *f)
{
  uint8_t ch = 0;
  HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
  return ch;
}


九、打印结果

串口打印功能,查看结果:

谢谢阅读!!!

  • 17
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
SPI(Serial Peripheral Interface)是一种串行通信协议,可以用于与外设进行数据交互。W25Q64是一款64Mb的串行Flash存储器,支持SPI接口。 以下是使用C语言进行SPI读写W25Q64的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/spi/spidev.h> #define SPI_DEVICE "/dev/spidev0.0" // SPI设备节点路径 #define BUFFER_SIZE 256 // 缓冲区大小 int main() { int fd, ret; uint8_t tx_buffer[BUFFER_SIZE], rx_buffer[BUFFER_SIZE]; struct spi_ioc_transfer tr; // 打开SPI设备 fd = open(SPI_DEVICE, O_RDWR); if (fd < 0) { perror("Failed to open SPI device"); exit(1); } // 初始化SPI传输结构体 memset(&tr, 0, sizeof(tr)); tr.tx_buf = (unsigned long)tx_buffer; tr.rx_buf = (unsigned long)rx_buffer; tr.len = BUFFER_SIZE; tr.speed_hz = 10000000; tr.bits_per_word = 8; // 发送写命令 tx_buffer[0] = 0x06; // 写使能 ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr); if (ret < 0) { perror("Failed to send write enable command"); exit(1); } tr.len = 4; tx_buffer[0] = 0x02; // 写指令 tx_buffer[1] = 0x00; // 地址高位 tx_buffer[2] = 0x00; // 地址中位 tx_buffer[3] = 0x00; // 地址低位 memcpy(tx_buffer + 4, "Hello", 5); // 写入数据 // 发送写数据命令 ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr); if (ret < 0) { perror("Failed to send write command"); exit(1); } // 发送读命令 tr.len = 4; tx_buffer[0] = 0x03; // 读指令 tx_buffer[1] = 0x00; // 地址高位 tx_buffer[2] = 0x00; // 地址中位 tx_buffer[3] = 0x00; // 地址低位 ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr); if (ret < 0) { perror("Failed to send read command"); exit(1); } printf("Data read from flash memory: %s\n", rx_buffer + 4); // 关闭SPI设备 close(fd); return 0; } ``` 以上代码演示了如何使用SPI接口读写W25Q64芯片。其中,首先通过ioctl函数设置SPI传输参数和指令,然后通过ioctl函数发送指令,并通过缓冲区读取或写入数据。 需要注意的是,通过SPI接口读写W25Q64芯片需要按照其通信协议进行操作,具体指令和数据格式可参考W25Q64的数据手册。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

W_oilpicture

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值