SPI通信协议

SPI通信协议

1. SPI总线协议

  • SPI简介:

    SPISerial Peripheral interface串行外设设备接口。SPI通信协议是Motorola公司首先在其MC68HCXX系列处理器上定义的。SPI接口是一种高速全双工同步的通信总线,已经广泛应用在众多MCU、存储芯片、AD转换器和LCD之间。

  • SPI总线与IIC总线的比较:

    功能说明SPI总线IIC总线
    通信方式同步 串行 全双工同步 串行 半双工
    总线接口MOSI、MISO、SCL、CSSDA、SCL
    拓扑结构一主多从/一主一从多主从
    从机选择片选引脚选择SDA上设备地址片选
    通信速率一般50MHz以下100kHz、400kHz、3.4MHz
    数据格式8位/16位8位
    传输顺序MSB/LSBMSB
  • 连接示意图:
    在这里插入图片描述

2. SPI结构框图

在这里插入图片描述

  • SPI引脚信息:

    MISO (Master In/Slave Out)主设备数据输入,从设备数据输出。

    MOSIMaster Out/Slave In)主设备数据输出,从设备数据输入。

    SCLKSerial Clock)时钟信号,由主设备产生。

    CSChip Select)从设备片选信号,由主设备产生。

  • SPI的工作原理:
    在这里插入图片描述

    在主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作,主机只需忽略接收到的字节。反之,若主机要读取从机的一个字节,就必须发送一个空字节引发从机传输。

  • SPI的传输方式:

    全双工通信,在任何时刻,主机与从机之间都可以同时进行数据的发送和接收。
    单工通信,在同一时刻,只有一个传输方向,发送或者是接收。
    半双工通信,在同一时刻,只能为一个方向传输数据。

  • 数据发送流程:
    在这里插入图片描述

  • 数据接收流程:
    在这里插入图片描述

  • 优缺点:

    优点缺点
    高速数据传输需要更多的引脚(尤其是多从设备时)
    全双工通信无标准化的通信协议(如I2C)
    硬件实现简单,通常无需复杂的协议栈支持无法进行多主设备通信
    数据传输稳定,适用于高速数据应用数据传输距离有限
  • 应用场景:

    SPI广泛用于连接高速外围设备,如:

    • 数据存储设备(SD卡、闪存)
    • 显示屏(LCD、OLED)
    • 传感器(加速度计、陀螺仪)
    • 数模转换器(DAC)和模数转换器(ADC)

3. SPI工作模式

在这里插入图片描述

具体分析:

  • CPOL = 0, CPHA = 0:
    在这里插入图片描述

    传输的数据会在奇数边沿上升沿被采集,MOSI和MISO数据的有效信号需要在SCK奇数边沿保持稳定且被采样。在非采样时刻,MOSI和MISO的有效信号才发生变化。

  • CPOL = 0, CPHA = 1:
    在这里插入图片描述

    传输的数据会在偶数边沿下降沿被采集,MOSI和MISO数据的有效信号需要在SCK偶数边沿保持稳定且被采样。在非采样时刻,MOSI和MISO的有效信号才发生变化。

  • CPOL = 1, CPHA = 0:
    在这里插入图片描述

    传输的数据会在奇数边沿下降沿采集,MOSI和MISO数据的有效信号需要在SCK奇数边沿保持稳定且被采样。在非采样时刻,MOSI和MISO的有效信号才发生变化。

  • CPOL = 1, CPHA = 1:
    在这里插入图片描述

    传输的数据会在偶数边沿上升沿采集,MOSI和MISO数据的有效信号需要在SCK偶数边沿保持稳定且被采样。在非采样时刻,MOSI和MISO的有效信号才发生变化。

4. SPI相关寄存器

  • SPI控制寄存器1 (SPI_CR1):
    在这里插入图片描述

  • SPI状态寄存器 (SPI_SR):
    在这里插入图片描述

  • SPI数据寄存器 (SPI_DR):
    在这里插入图片描述

5. SPI相关HAL库函数

驱动函数关联寄存器功能描述
__HAL_RCC_SPIx_CLK_ENABLE()RCC_APB2ENR使能SPIx时钟
HAL_SPI_Init()SPI_CR1初始化SPI
HAL_SPI_MspInit()初始化回调初始化SPI相关引脚
HAL_SPI_Transmit()SPI_DR/SPI_SRSPI发送
HAL_SPI_Receive()SPI_DR/SPI_SRSPI接收
HAL_SPI_TransmitReceive()SPI_DR/SPI_SRSPI接收发送
__HAL_SPI_ENABLE()SPI_CR1(SPE)使能SPI外设
__HAL_SPI_DISABLE()SPI_CR1(SPE)失能SPI外设
  • HAL_SPI_Init()

    void spi2_init(void)
    {
        g_spi2_handler.Instance = SPI2;                          // 选择SPI2外设
        g_spi2_handler.Init.Mode = SPI_MODE_MASTER;              // 设置SPI为主机模式
        g_spi2_handler.Init.Direction = SPI_DIRECTION_2LINES;    // 数据传输方向为全双工(双线)
        g_spi2_handler.Init.DataSize = SPI_DATASIZE_8BIT;        // 数据大小为8位
        g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_HIGH;     // 时钟极性设置为高电平空闲
        g_spi2_handler.Init.CLKPhase = SPI_PHASE_2EDGE;          // 数据在第二个时钟沿采样
        g_spi2_handler.Init.NSS = SPI_NSS_SOFT;                  // NSS信号由软件控制
        g_spi2_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;// 波特率预分频为256
        g_spi2_handler.Init.FirstBit = SPI_FIRSTBIT_MSB;          // 数据传输从MSB开始
        g_spi2_handler.Init.TIMode = SPI_TIMODE_DISABLED;         // 禁用TI模式
        g_spi2_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLED; // 禁用CRC校验
        g_spi2_handler.Init.CRCPolynomial = 7;                            // CRC多项式为7
        HAL_SPI_Init(&g_spi2_handler);                                    // 初始化SPI2
    }
    
    1. g_spi2_handler.Instance = SPI2:指定要配置的SPI外设,此处为SPI2。

    2. g_spi2_handler.Init.Mode = SPI_MODE_MASTER:设置SPI的工作模式为主机模式(Master)。

    3. g_spi2_handler.Init.Direction = SPI_DIRECTION_2LINES:设置数据传输方向为全双工模式(双线模式),即使用MOSI和MISO两条线进行数据传输。

    4. g_spi2_handler.Init.DataSize = SPI_DATASIZE_8BIT:设置每次传输的数据位大小为8位。

    5. g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_HIGH:设置时钟极性为高电平空闲状态。这意味着时钟线在空闲状态下保持高电平。

    6. g_spi2_handler.Init.CLKPhase = SPI_PHASE_2EDGE:设置(时钟相位)数据在时钟的第二个边沿(从空闲状态改变的第二个边沿)采样。

    7. g_spi2_handler.Init.NSS = SPI_NSS_SOFT:设置NSS(片选信号)由软件控制,而不是硬件自动管理。

    8. g_spi2_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256:设置波特率预分频值为256,这将决定SPI的时钟速度。

    9. g_spi2_handler.Init.FirstBit = SPI_FIRSTBIT_MSB:设置数据传输顺序从最高有效位(MSB)开始。

    10. g_spi2_handler.Init.TIMode = SPI_TIMODE_DISABLED: 禁用TI模式(特定于德州仪器的SPI协议)。

    11. g_spi2_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLED:禁用CRC校验计算。

    12. g_spi2_handler.Init.CRCPolynomial = 7:设置CRC多项式值为7(此项在禁用CRC校验时不会影响操作)。

6. NOR FLASH

在这里插入图片描述

  • NOR Flash与NAND Flash的区别:
    在这里插入图片描述

    NOR与NAND在数据写入前都需要有擦除操作,但实际上NOR FLASH的一个bit可以从 1变成0,而要从0变1就要擦除后再写入,NAND Flash这两种情况都需要擦除。擦除操作的最小单位为“扇区块”,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前的数据都可能会被擦除。

    NOR的地址线和数据线分开,它可以按“字节”读写数据,符合CPU的指令译码执行要求,所以假如NOR上存储了代码指令,CPU给NOR一个地址,NOR就能向CPU返回一个数据让CPU执行,中间不需要额外的处理操作(支持XIP)。因此可以用NOR FLASH直接作为嵌入式MCU的程序存储空间。

    NAND的数据线和地址线共用,只能按“”来读写数据,假如NAND上存储了代码指令, CPU给NAND地址后,它无法直接返回该地址的数据,所以不符合指令译码要求。

  • NM25Q128:

    NM25Q128是一款大容量SPI FLASH产品,其容量为16M。它将16M字节的容量分为256块(Block),每个块大小为64K字节,每个块又分为16扇区(Sector),每一个扇区16页,每页256个字节,即每个扇区4K个字节。NM25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给NM25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。
    在这里插入图片描述

  • NM25Q128引脚图:
    在这里插入图片描述

  • NM25Q128常用指令:

    指令(HEX)名称作用
    0X06写使能写入数据/擦除之前,必须先发送该指令
    0X05读SR1判定FLASH是否处于空闲状态,擦除用
    0X03读数据用于读取NOR FLASH数据
    0X02页写用于写入NOR FLASH数据,最多写256字节
    0X20扇区擦除扇区擦除指令,最小擦除单位(4096字节)
  • 0x06 写使能:
    在这里插入图片描述

    写入使能(WREN)命令序列:CS变低 -> 发送写入使能命令 -> CS变高。

  • 0x05 读SR1:
    在这里插入图片描述

  • 0x03 读时序:
    在这里插入图片描述

    读数据指令是03H,可以读出一个字节或者多个字节。发起读操作时,先把CS片选管脚拉低,然后通过MOSI引脚把03H发送芯片,之后再发送要读取的24位地址,这些数据在CLK上升沿时采样。芯片接收完24位地址之后,就会把相对应地址的数据在CLK引脚下降沿从MISO引脚发送出去。从图中可以看出只要CLK一直在工作,那么通过一条读指令就可以把整个芯片存储区的数据读出来。当主机把CS引脚拉高,数据传输停止。

  • 0x02 页写:
    在这里插入图片描述

    在发送页写指令之前,需要先发送“写使能”指令。然后主机拉低CS引脚,然后通过MOSI引脚把02H发送到芯片,接着发送24位地址,最后你就可以发送需要写的字节数据到芯片。完成数据写入之后,需要拉高 CS 引脚,停止数据传输。扇区擦除需要等待是否擦除完成

  • 0x20 扇区擦除:
    在这里插入图片描述

    扇区擦除指的是将一个扇区擦除,NM25Q128的扇区大小是4K字节。擦除扇区后,扇区的位全置1,即扇区字节为FF。同样的,在执行扇区擦除之前,需要先执行写使能指令。这里需要注意的是当前SPI总线的状态,假如总线状态是BUSY,那么这个扇区擦除是无效的,在拉低CS引脚准备发送数据前,需要先确定SPI总线的状态,这就需要执行读状态寄存器指令,读取状态寄存器BUSY位,需要等待BUSY位为0,才可以执行擦除工作。扇区擦除需要等待是否擦除完成

7. NOR FLASH基本驱动步骤

  • SPI配置步骤:

    1. SPI工作参数配置初始化

      HAL_SPI_Init()
      
    2. 使能SPI时钟和初始化相关引脚

      HAL_SPI_MspInit()
      
    3. 使能SPI

      __HAL_SPI_ENABLE()
      
    4. SPI传输数据

      //发送数据
      HAL_SPI_Transmit() 
      
      //接收数据
      HAL_SPI_Receive() 
      
      //发送与接收数据
      HAL_SPI_TransmitReceive() 
      
  • NM25Q128配置步骤:

    1. 初始化片选引脚与SPI接口

      相关GPIO初始化、SPI初始化(模式、位数、分频、MSB等)

    2. NM25Q128 读取

      0x03指令 + 24位地址 + 读取数据

    3. NM25Q128 扇区擦除

      0x06指令 + 等待空闲 + 0x20指令 + 24位地址 + 等待空闲

    4. NM25Q128 写入

      擦除扇区(可选)+ 0x06指令 + 0x02指令+ 24位地址 + 写入数据 + 等待空闲

8. 代码实现

  • SPI初始化函数:

    void spi2_init(void)
    {
        g_spi2_handler.Instance = SPI2;                          // 选择SPI2外设
        g_spi2_handler.Init.Mode = SPI_MODE_MASTER;              // 设置SPI为主机模式
        g_spi2_handler.Init.Direction = SPI_DIRECTION_2LINES;    // 数据传输方向为全双工(双线)
        g_spi2_handler.Init.DataSize = SPI_DATASIZE_8BIT;        // 数据大小为8位
        g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_HIGH;     // 时钟极性设置为高电平空闲
        g_spi2_handler.Init.CLKPhase = SPI_PHASE_2EDGE;          // 数据在第二个时钟沿采样
        g_spi2_handler.Init.NSS = SPI_NSS_SOFT;                  // NSS信号由软件控制
        g_spi2_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;// 波特率预分频为256
        g_spi2_handler.Init.FirstBit = SPI_FIRSTBIT_MSB;          // 数据传输从MSB开始
        g_spi2_handler.Init.TIMode = SPI_TIMODE_DISABLED;         // 禁用TI模式
        g_spi2_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLED; // 禁用CRC校验
        g_spi2_handler.Init.CRCPolynomial = 7;                            // CRC多项式为7
        HAL_SPI_Init(&g_spi2_handler);                                    // 初始化SPI2
    }
    
  • SPI MSP 回调函数:

    void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
    {
        SPI2_SPI_CLK_ENABLE();
        
        GPIO_InitTypeDef gpio_init_struct;
        
        if(hspi->Instance == SPI2_SPI)
        {
            SPI2_SCK_GPIO_CLK_ENABLE();
            SPI2_MISO_GPIO_CLK_ENABLE();
            SPI2_MOSI_GPIO_CLK_ENABLE();
            
            gpio_init_struct.Pin = GPIO_PIN_13;
            gpio_init_struct.Mode = GPIO_MODE_AF_PP;
            gpio_init_struct.Pull = GPIO_PULLUP;
            gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
            HAL_GPIO_Init(GPIOB, &gpio_init_struct);
            
            gpio_init_struct.Pin = GPIO_PIN_14;
            HAL_GPIO_Init(GPIOB, &gpio_init_struct);
            
            gpio_init_struct.Pin = GPIO_PIN_15;
            HAL_GPIO_Init(GPIOB, &gpio_init_struct);
        }
    }
    

    配置STM32微控制器的SPI2外设所需的GPIO引脚,包括SCK时钟线)、MISO主输入从输出)和MOSI主输出从输入)。通过使能相应的时钟信号并配置这些引脚的工作模式、上拉设置和速度,使得SPI2外设能够正常工作并进行数据传输。

  • SPI读取函数:

    uint8_t spi2_read_write_byte(uint8_t data)
    {
        uint8_t rec_data = 0;
        
        HAL_SPI_TransmitReceive(&g_spi2_handler, &data, &rec_data, 1, 1000);
        
        return rec_data;
    }
    

    HAL_SPI_TransmitReceive函数

    HAL_SPI_TransmitReceive(&g_spi2_handler, &data, &rec_data, 1, 1000);
    
    • 该函数是HAL库提供的用于SPI通信的函数,通过SPI同时发送和接收数据。
    • 参数解析:
      • &g_spi2_handler:SPI外设的句柄,这里是SPI2的句柄。
      • &data:指向要发送的数据的指针。
      • &rec_data:指向存储接收到数据的变量的指针。
      • 1:表示发送和接收的数据长度为1字节。
      • 1000:超时时间,单位为毫秒。

    这个函数调用后,会发送data变量中的数据,并将从SPI总线上接收到的数据存储到rec_data变量中

  • NOR FLASH初始化函数:

    void norflash_init(void)
    {
        GPIO_InitTypeDef gpio_init_struct;
        
        __HAL_RCC_GPIOB_CLK_ENABLE();
        
        gpio_init_struct.Pin = GPIO_PIN_12;
        gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
        gpio_init_struct.Pull = GPIO_PULLUP;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOB, &gpio_init_struct);
        
        spi2_init();
        spi2_read_write_byte(0xFF);  //清空DR的作用
        
        NORFLASH_CS(1);  
    }
    
    1. 初始化GPIOB的12号引脚为推挽输出模式,并设置上拉和高频率。
    2. 初始化SPI2接口,为后续的SPI通信做好准备。
    3. 通过发送一个字节(0xFF)清空SPI的数据寄存器。
    4. 将NOR Flash的片选引脚设置为高电平,取消选择NOR Flash。
  • 读数据函数:

    uint8_t norflash_read_data(uint32_t addr)
    {
        uint8_t rec_data = 0;
        
        NORFLASH_CS(0);
        
        //发送读命令
        spi2_read_write_byte(0x03);
        
        //发送地址
        spi2_read_write_byte(addr >> 16);
        spi2_read_write_byte(addr >> 8);
        spi2_read_write_byte(addr);
        
        //读取数据
        rec_data = spi2_read_write_byte(0xFF);
        
        NORFLASH_CS(1);
        
        return rec_data;
    }
    
    1. 使能NOR Flash的片选信号。

    2. 发送读取命令0x03

    3. 发送24位地址,确定读取位置。

      SPI总线每次只能发送8位数据,因此需要将24位地址拆成三个8位部分依次发送。

    4. 发送一个字节以触发读取操作,同时接收数据。

    5. 结束通信,禁用NOR Flash的片选信号。

    6. 返回读取到的数据。

    总结: 通过逐字节发送一个24位地址,确保NOR Flash能够正确接收并识别要操作的内存地址。通过右移操作,可以将地址分割成三个独立的8位字节,每个字节通过SPI接口发送给NOR Flash。这是遵循NOR Flash设备的通信协议的标准做法。

  • 等待擦除函数:

    uint8_t norflash_td_sr1(void)
    {
         uint8_t rec_data = 0;
        
        NORFLASH_CS(0);                    // 使能 NOR Flash,选中芯片
        spi2_read_write_byte(0x05);        // 发送读状态寄存器1的命令
        rec_data = spi2_read_write_byte(0xFF); // 读取状态寄存器1的值
        NORFLASH_CS(1);                    // 禁能 NOR Flash,取消选中芯片
        
        return rec_data;                   // 返回状态寄存器1的值
    }
    
  • 扇区擦除函数:

    void norflash_erease_sector(uint32_t addr)
    {  
        //写使能
        NORFLASH_CS(0);
        spi2_read_write_byte(0x06); //写入数据/擦除之前,必须先发送该指令
        NORFLASH_CS(1);
        
        //等待空闲
        while(norflash_td_sr1() & 0x01);
        
        //发送扇区擦除指令
        NORFLASH_CS(0);
        spi2_read_write_byte(0x20);   
    
        //发送地址
        spi2_read_write_byte(addr >> 16);
        spi2_read_write_byte(addr >> 8);
        spi2_read_write_byte(addr);
        NORFLASH_CS(1);
        
        //等待擦除
        while(norflash_td_sr1() & 0x01);
    }
    

    扇区擦除操作之前,需要进行写使能,然后判断是否为空闲状态,若为空闲,发送扇区擦除的指令,随后发送地址,然后等待擦除完成。

  • 写数据函数:

    void norflash_write_data(uint8_t data, uint32_t addr)
    {
        //擦除扇区
        norflash_erease_sector(addr);
        
        //写使能
        NORFLASH_CS(0);
        spi2_read_write_byte(0x06); //写入数据/擦除之前,必须先发送该指令
        NORFLASH_CS(1);
        
        //发送页写指令
        NORFLASH_CS(0);
        spi2_read_write_byte(0x02); 
        
        //发送地址
        spi2_read_write_byte(addr >> 16);
        spi2_read_write_byte(addr >> 8);
        spi2_read_write_byte(addr);
        
        //写入数据
        spi2_read_write_byte(data);
        NORFLASH_CS(1);
        
        while(norflash_td_sr1() & 0x01);
    }
    

    给指定的地址 addr 处向 NOR Flash 写入一个字节的数据 data。它首先执行擦除操作确保写入的地址是擦除过的,然后发送写使能指令和页写入指令,接着发送地址和数据,最后等待 NOR Flash 完成写入操作。

    在嵌入式系统中,特别是在和外部设备(比如 NOR Flash)进行通信时,经常需要进行等待操作的原因如下:

    1. 操作完成确认: 在与外部设备通信时,发送命令或数据后,设备需要一定时间来执行这些操作。等待的过程确保设备已经完成了当前的操作,然后才能进行下一步操作,以避免操作冲突或数据丢失。

    2. 设备忙碌状态: 外部设备在执行诸如擦除、写入等操作时,会设置忙碌状态标志位。等待设备状态从忙碌变为空闲,表示设备已经完成了当前的操作,可以安全地进行下一步操作。

    3. 操作完成时间不确定性: 外部设备的操作完成时间可能不确定,取决于设备的具体硬件实现、时钟频率、操作类型等因素。等待操作完成确保了在设备准备好接受下一个命令或数据之前,主控制器不会继续向设备发送新的指令,从而保证了通信的正确性和可靠性。

    等待操作完成是嵌入式系统中保证设备通信正确性和可靠性的重要步骤之一。

声明:资料来源(战舰STM32F103ZET6开发板资源包)

  1. Cortex-M3权威指南(中文).pdf
  2. STM32F10xxx参考手册_V10(中文版).pdf
  3. STM32F103 战舰开发指南V1.3.pdf
  4. STM32F103ZET6(中文版).pdf
  5. 战舰V4 硬件参考手册_V1.0.pdf
  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值