STM32下的SPI(从原理到代码)

十. SPI通讯–读写串行FLASH

文章目录

  • 十. SPI通讯–读写串行FLASH
  • 10.1 SPI协议简介
  • 10.2 SPI协议层
  • 10.2.2 SPI的通讯模式
  • 10.3 STM32下的SPI架构与通讯过程
  • 10.3.2 SPI的通讯过程
  • 10.4 代码实现
  • 10.4.1 FLASH简介
  • 10.4.2 具体实现
  • SPI协议解决的场所是什么?

    SPI与I2C协议的区别是什么?

    STM32—SPI通信协议(小白入、含源码)_spi协议代码-CSDN博客

    10.1 SPI协议简介

    ​ 关于SPI通讯的话,主要还是采用标准库的学习较好,主要原因在于HAL库的话比较冗余,其启动占用比较多,而标准库则相对简洁。由于IIC通讯速率还是比较慢,对于某些实时要求比较高的场景可能不利。因此SPI诞生,即串行外围设备接口,是一种高速全双工的通信总线,主要适用于通信速率要求较高的场合如ADC LCD与MCU之间。 常见SPI(一主多从)的物理层的构造主要如下所示:

    ​ SPI的构成由三条总线(SCK,MOSI,MISO),和一系列片选线(具体片选线的位数看从机个数)构成(片选线也称为NSS、CS)。

    SCK:时钟信号线

    MOSI(Master Output, Slave Input):主设备输出,从设备输入(主机输出,从机读取)

    MISO(Master Input,,Slave Output):主设备输入,从设备输出(主机读取,从机输入)

    NSS/CS:从设备片选信号,由主设备产生。

    10.2 SPI协议层

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    SPI的通讯模式有哪些?通讯模式之间的区别是什么?

    ​ 其中NSS、SCK、MOSI三者信号均是由主机产生,MISO则是从机产生。MOSI与MISO均是在NSS为低电平时有效,每个SCK主要传送一位数据。即SCK主要进行时钟同步作用。SPI每次传输8到16个单位数据,该单位具体自己定义。

    10.2.2 SPI的通讯模式

    ​ 常见的SPI通讯模式除了最基本的上图时序通讯外,还有与CPOL(时钟极性)和CPHA(时钟相位)所组成有关的4种通讯模式,其主要的区别在于总线空闲时的SCK状态以及采样时刻

    10.3 STM32下的SPI架构与通讯过程

    SPI通讯的数据格式是怎样的?

    SPI的系统架构是怎样的?

    ​ STM32的SPI外设即可作主机又可作从机,支持的最高SCK频率为fpclk[^101] / 2, 且支持数据从高(低)位开始传输。其通讯引脚如下:

    1.通讯引脚

    ​ 其中SPI1是APB2的设备,最高为36Mbit/s,SPI2、SPI3 是 APB1 上的设备,最高通信速率为 18Mbits/s。SPI3通常为下载接口,不太常用。

    2.时钟控制逻辑

    ​ 由控制寄存器CR1中的BR[2:0]位控制,控制如下:

    3. 数据控制逻辑

    ​ 主要通过数据寄存器DR[15:0]和数据接收和发送缓冲区进行数据逻辑的控制,即可进行接受缓存与发送缓存。

    4.整体逻辑控制区域

    ​ 主要使用控制寄存器(CR)来进行一系列控制相关。

    10.3.2 SPI的通讯过程

    [SPI原理超详细讲解—值得一看-CSDN博客](https://blog.csdn.net/as480133937/article/details/105764119#:~:text=SPI数据发送接收 1 首先拉低对应SS信号线,表示与该设备进行通信 2 主机通过发送SCLK时钟信号,来告诉从机写数据或者读数据 这里要注意,SCLK时钟信号可能是低电平有效,也可能是高电平有效,因为SPI有四种模式,这个我们在下面会介绍 3 主机,(Master)将要发送的数据写到发送数据缓存区 (Menory),缓存区经过移位寄存器 (0~7),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。 4 从机 (Slave)也将自己的串行移位寄存器 (0~7)中的内容通过MISO信号线返回给主机。 同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。)

    ​ 整个过程主要参照上图主要如下:

    1. 控制NSS线,进行片选并产生起始信号(片选线上信号为高或低对应进行起始信号,,通常高电平为停止信号,低电平起始信号),然后主机通过发送SCK时钟信号告诉从机读或写
    2. 要发送的数据从核心的DR寄存器写入到发送缓冲区中
    3. 此时通讯正式开始,SCK开始工作,主机MOSI接口开始把缓冲区的数据一位位的传输出来,在发送的同时主机的MISO接口把接受到的数据一位位移入到接收缓冲区中。
    4. 同时从机也进行上述3的步骤,即将自己的MOSI接口把自己缓冲区的数据发送给主机,同时自己的MISO接口接受来自主机的数据,这样即完成了一次全双工通信。

    发送完一帧数据后TXE标志位置1,接受完一帧后RXNE置1(这里可进行和中断还有DMA的联动),另外需要注意的是SPI并不像I2C那样有明确的读写之分,SPI有的只是主从模式之分。

    10.4 代码实现

    主机(MCU)通过SPI和从机(W25Q64)进行数据的传输,并最终将传输数据通过串口进行展示

    10.4.1 FLASH简介

    ​ FLASH又称之为闪存是一种掉电后数据不丢失存储器,常用的U盘与SSD(固态),SD卡均是由其所构成,FLASH区别于EEPROM在于其擦除方式为一大片大片的擦除,而EEPROM只能单字节的擦写。其中WP为读写保护功能,HOLD引脚则用于暂停通讯相关(均是低电平有效)。

    ​ 另外关于FLASH系列的读取是由FLASH指令进行完成的,其指令的话主要分为 :

  • 读取指令
  • 写入指令
  • 擦除指令
  • 状态寄存指令(保护某些扇区不被修改)
  • 设备相关指令
  • 10.4.2 具体实现

    (1).初始化相关

    相关设置中的CPOL和CPHA的作用是什么?

    ​ SPI是一种同步通信,而SCK则是保证通信双方数据的准确性,而CPOL和CPHA设置模式的要点在于使通信双方保持同一节拍或是数据采样频率,保证数据的稳定性。FLASH相关芯片又只支持模式0和模式3。

    void SPI_FLASH_Init(void)
    {
      SPI_InitTypeDef  SPI_InitStructure;
      GPIO_InitTypeDef GPIO_InitStructure;
    	
    	/* 0.无论如何先是使能SPI时钟 */
    	FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
    	
    	/* 使能SPI引脚相关的时钟 */
     	FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|
    																	FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );
    	
      /* 1.1 配置SPI的 CS引脚(即片选线引脚),普通IO即可 */
      GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //使用软件控制,调为普通推挽输出模式即可
      GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
    	
      /* 1.2 配置SPI的 SCK引脚,均是复用推挽输出*/
      GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
      GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);
    
      /*1.2 配置SPI的 MISO引脚*/
      GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
      GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);
    
      /*1.2 配置SPI的 MOSI引脚*/
      GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
      GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);
    
      /* 停止信号 FLASH: CS引脚高电平*/
      SPI_FLASH_CS_HIGH();
    
      /* 2.SPI 模式配置 */
      // FLASH芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
        //设置SPI通讯方向单双向模式,即通常为双向全双工模式
      SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
      	//设置主机或从机模式,区别在于SCK信号产生源,若主机模式则MCU的SCK信号,从机模式则外来的SCK信号
      SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
        //SPI通信的数据帧为8位
      SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
        //时钟与相位极性
      SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
      SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//偶数边沿进行采样
      	//CS/NSS片选信号由软件还是硬件产生(软件的话手动置电平高低),通常选择软件
      SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
        //fpclk分频因子的设置
      SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
      	//先行通信是高位还是低位
      SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
      	//CRC校验值,了解即可
      SPI_InitStructure.SPI_CRCPolynomial = 7;
      SPI_Init(FLASH_SPIx , &SPI_InitStructure);
    
      /* 使能 SPI  */
      SPI_Cmd(FLASH_SPIx , ENABLE);
    	
    }
    

    (2).发送接受字节数据

    ​ 这里只是SPI发送后接受数据的方法封装,并不是直接写入到FLASH中去

     /**
      * @brief  使用SPI发送一个字节的数据
      * @param  byte:要发送的数据
      * @retval 返回接收到的数据
      */
    u8 SPI_FLASH_SendByte(u8 byte)
    {
    	 SPITimeout = SPIT_FLAG_TIMEOUT;
      /* 1.等待发送缓冲区为空,TXE事件 发送完一帧数据后TXE置1*/
      while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_TXE) == RESET)
      {
        if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
       }
    
      /* 2.写入数据寄存器,把要写入的数据写入发送缓冲区 */
      SPI_I2S_SendData(FLASH_SPIx , byte);
    
    	SPITimeout = SPIT_FLAG_TIMEOUT;
      /* 3.等待接收缓冲区非空,RXNE事件 , 接收缓冲区接收完一帧时,RXNE寄存器置1即SET*/
      while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_RXNE) == RESET)
      {
        if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
       }
    
      /* 读取数据寄存器,获取接收缓冲区数据 */
      return SPI_I2S_ReceiveData(FLASH_SPIx );
    }
    
     /**
      * @brief  使用SPI读取一个字节的数据
      * @param  无
      * @retval 返回接收到的数据
      */
    u8 SPI_FLASH_ReadByte(void)
    {
      return (SPI_FLASH_SendByte(Dummy_Byte));
    }
    

    (3).FLASH指令的一系列初始化定义

    ​ 这一系列初始化后的指令标准称之为JEDEC识别方法集,再通过要读取的FLASH ID的过程编写为一个函数即可

    /*命令定义-开头*******************************/
    #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
    
    /* WIP(busy)标志,FLASH内部正在写入 */
    #define WIP_Flag                  0x01
    #define Dummy_Byte                0xFF
    

    (4).读取一系列指令

    ​ 整个读取(FLASH)的过程分为以下几步,片选先是拉低电平,即开始读取,然后MOSI引脚发送读取相关指令,然后MISO引脚发送数据,主机端进行接受即可,而Dummy_Byte表示虚拟占位字节,并无实际含义,最后再拉高电平结束即可。且通常来讲FLASH芯片(W25Qxx系列)普遍采用的是24位(3字节)地址格式,因此用三个变量来进行接收(选用u32的原因在于防止数据的丢失)。

    例如下面是读取FLASH ID的一系列函数封装:

    /**
      * @brief  读取FLASH ID
      * @param 	无
      * @retval FLASH ID
      */
    u32 SPI_FLASH_ReadID(void)
    {
      u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
    
      /* 开始通讯:CS低电平 */
      SPI_FLASH_CS_LOW();
    
      /* 发送JEDEC指令,读取ID */
      SPI_FLASH_SendByte(W25X_JedecDeviceID);
    
      /* 读取一个字节数据 */
      Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
    
      /* 读取一个字节数据 */
      Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
    
      /* 读取一个字节数据 */
      Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
    
     /* 停止通讯:CS高电平 */
      SPI_FLASH_CS_HIGH();
    
      /*把数据组合起来,作为函数的返回值*/
    	Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
    
      return Temp;
    }
    

    写入一个字节数据

    void SPI_FLASH_WriteByte(u8 byte, u32 WriteAddr)
    {
       SPI_FLASH_WriteEnable();
    
       //通讯开始信号,拉低CS线电平
       SPI_FLASH_CS_LOW();
       /*发送写页指令*/
       SPI_FLASH_SendByte(W25X_PageProgram);
       
       SPI_FLASH_SendByte((WriteAddr >> 16) & 0xFF);
       SPI_FLASH_SendByte((WriteAddr >> 8) & 0xFF);
       SPI_FLASH_SendByte(WriteAddr & 0xFF);
    
       //写入数据
       SPI_FLASH_SendByte(byte);
    
       SPI_FLASH_CS_HIGH();
       SPI_FLASH_WaitForWriteEnd();
    
       //SPI_FLASH_CS_HIGH();
    }
    

    ​ 这里需要注意的是SPI_FLASH_WaitForWriteEnd();封装,因为虽然SPI外设传输速度很快了,但跟MCU比起来还是差了不少,因此写入后加个等待操作,其次是因为写完后主程序main中紧跟着的是读,因此可能导致数据缓存寄存器的混乱,因此加上了相关保险函数才能实现正常逻辑的执行。

    读取一个单位数据

    u32 SPI_FLASH_ReadByte(u32 address)
    {
        u8 data = 0;
    
        /* 开始通讯:CS低电平 */
        SPI_FLASH_CS_LOW();
    
        /* 发送读取指令,读取数据 */
        SPI_FLASH_SendByte(W25X_ReadData); // 发送读取数据的指令,通常为0x03
    
        /* 发送3字节地址 */
        /* 发送 读 地址高位 */
      SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16); //这样保证采样到的数据为高位
      //也可以写为SPI_FLASH_SendByte((ReadAddr >> 16) & 0xFF);
      /* 发送 读 地址中位 */
      SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
      /* 发送 读 地址低位 */
      SPI_FLASH_SendByte(ReadAddr & 0xFF);
        /*SPI_FLASH_SendByte((address >> 16) & 0xFF); // 发送高字节
        SPI_FLASH_SendByte((address >> 8) & 0xFF);  // 发送中字节
        SPI_FLASH_SendByte(address & 0xFF);         // 发送低字节*/
    
        /* 发送一个Dummy Byte以获取数据 */
        data = SPI_FLASH_SendByte(Dummy_Byte);      // 读取数据
    
        /* 停止通讯:CS高电平 */
        SPI_FLASH_CS_HIGH();
    
        return data; // 返回读取的字节数据
    }
    
    

    总结: 代码与I2C不同的是,I2C的数据发送与接收的对象是会将SDA数据线考虑进去的,而SPI则只是控制主机端无论是发送还是接受,均是要在发送的基础上进行的。另外则是关于GPIO的复用推挽与普通推挽的区分注意一下,其次关于MCU读写FLASH的整个流程为:1.片选线使能 2.SPI高中低每8位发送数据 先发送读写(Flash)指令 3.若为写再发送写地址,并接着发送具体写入数据,并加上保险函数 4.若为读则接着发送读地址,然后发送占位数据并进行读数据接收 5.拉高片选线结束使能。

    ​ 关于SPI能实现双向全双工的原因主要在于其物理结构与I2C的差异,这也就注定着SPI的成本更高。

    需要注意的是SPI关于主机对从机通讯地址的定位通常采用的是片选线的高低电平进行的,这点也是与IIC通信的一大区别。

    作者:神奇小炒肉

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32下的SPI(从原理到代码)

    发表回复