STM32中的SPI通信协议

IIC和SPI的对比

  1. IIC是半双工的通信,无法同时收发信息;SPI是全双工通讯,可以同时收发信息;
  2. IIC的通讯协议较复杂,而SPI通讯协议较简单;
  3. IIC需要通过地址选择从机,而SPI只主要一个引脚即可选中从机;
  4. IIC的通讯速率一般是100kH左右,而SPI可达50MHz;
  5. IIC的通讯线较少,而SPI需要较多。

SPI的介绍

什么是SPI ?

SPI是串行外设接口(Serial Peripheral Interface) 的缩写,是一种高速 全双工 同步 的通信总线,并且在芯片上只有四个引脚,同时PCB布线上节省空间,比较方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如 AT91RM9200 。

SPI的物理架构

SPI总线包含4个通讯线,分别是 SS , MISO , MOSI , SCL 。他们的 作用 如下:

  • MISO -- Master Input Slave Output,主设备数据输入,从设备数据输出
  • MOSI -- Master Output Slave Input,主设备数据输出,从设备数据输入
  • SCK -- Serial Clock,时钟信号,由主设备产生
  • CS -- Chip Select,片选信号,由主设备控制

STM32F1 系列芯片有 3 个SPI 接口。

SPI工作原理

  • 上述中是高位先行

  • 习惯上利用0XFF将从机上的数据交换出来。

  • SPI通信只有主模式和从模式,没有明确的读和写操作之分。实际上,外设的写操作和读操作是同步完成的。在SPI通信中,发送一个数据必然会收到一个数据;如果要接收一个数据,就必须先发送一个数据。

  • 如果只进行写操作,主机可以忽略从设备传输过来的字节,因为主机不需要接收数据。

  • 如果主机要读取从设备的一个字节,那么主机必须发送一个空字节来引发从设备的传输。

说明:(狸猫换太子)

上述中主机和从机 高位先行,主机移位寄存器中,高位移出,其余整体向左移一位;同时,从机中的高位移到主机的低位中的空位,从机其余位向左移一位;主机的高位放到从机移位寄存器中空出来的低位。通过波特率发生器:一个时钟信号主从机中数据交换一位,8个时钟信号后,数据全部交换完成。

具体流程:

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

  1. 首先,拉低相应的 SS 信号线,表示与特定的从机进行通信。
  2. 主机通过发送 SCLK 时钟信号告诉从机进行数据的读写操作。
  3. 注意,SCLK 时钟信号可以是低电平有效或高电平有效,因为SPI有不同的模式(下文将介绍)。
  4. 主机将要发送的数据写入发送数据缓冲区,然后通过移位寄存器逐位地将数据传输给从机的串行移位寄存器,使用 MOSI 信号线进行传输。同时,从机的 MOSI 接口接收到的数据也经过移位寄存器一位一位地移到接收缓冲区。
  5. 从机也通过 MISO 信号线将自己串行移位寄存器中的内容返回给主机。同时,从机通过MOSI 信号线接收主机发送的数据。这样,两个移位寄存器中的内容就被交换。
  • 框图(参考手册):
  • 简图:

SPI工作模式

时钟极性(CPOL)

没有数据传输时时钟线SCL的空闲状态电平

0 :SCK在空闲状态保持低电平

1 :SCK在空闲状态保持高电平

时钟相位(CPHA)

时钟线(SCK)在第几个时钟边沿采样数据

0 :SCK在第一个(奇数)边沿进行数据位采样,数据在第一个时钟边沿被锁存;

1 :SCK的第二个(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁村。

|------------------|----------|----------|------------------|----------|----------|
| SPI 工作模式 | CPOL | CPHA | SCL 空闲状态 | 采样边沿 | 采样时刻 |
| 0 | 0 | 0 | 低电平 | 上升沿 | 奇数边沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 | 偶数边沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 | 奇数边沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 | 偶数边沿 |
[SPI的工作模式(开始和采样)]

注:红色字体是常用的:模式0和模式3

时序图

  • 模式0时序图:
  • 模式3时序图 :

STM32F103板上SPI的引脚

  • SPI1引脚:
  • SPI2引脚:
  • SP3引脚

  • STM32F1系列根据Flash和RAM容量分为不同型号:

    • 低/中容量(Low/Medium-density) :如STM32F103C8T6(64KB Flash),仅支持SPI1SPI2

    • 高容量(High-density) :如STM32F103ZE(512KB Flash),额外支持SPI3

  • SPI3是高容量型号的专属外设,STM32F103C8T6作为中容量型号,硬件上未集成SPI3模块。

SPI寄存器(控制、状态、数据)

SPI控制寄存器1(SPI_CR1)(I2S模式下不使用)

SPI控制寄存器2(SPI_CR2)

SPI状态寄存器(SPI_SR)

SPI数据寄存器(SPI_DR)

SPI的库函数(常用的有三个)

初始化SPI的函数

数据发送的函数 :

数据接收的函数:

常用的是:既发送又接受的函数

模块:W25Q128存储器介绍

电脑保存数据的有RAM、ROM、FLASH (类似于硬盘,断电保存的数据不丢失)

W25Q128是华邦公司推出的一款容量为128M-bit(相当于 16M-byte) 的 SPI 接口的NOR Flash 芯片。

  • NOR Flash :一种非易失性存储器,它可以在断电或掉电后仍然保持存储的数据,因此被广泛应用于长期数据存储。它具有容量大,可重复擦写、按"扇区/块"擦除的特性。Flash 是有一个物理特性只能写 0 ,不能写 1,写 1 靠擦除。(读、改、写)

它还有很多不同容量的好兄弟:

型号 容量
W25Q256 256M bits = 32M bytes
W25Q128 128M bits = 16M bytes
W25Q64 64M bits = 8M bytes
W25Q32 32M bits = 4M bytes
W25Q16 16M bits = 2M bytes
W25Q80 8M bits = 1M bytes

W25Q128模块参数及引脚介绍

参数:

  • 产品容量:128M-bit(16M-byte)
  • 时钟频率:<=104MHz
  • 工作电压:2.7V ~ 3.6V
  • 工作温度:-40℃ ~ +85℃
  • 支持 SPI 接口

参考接线:

W25Q128 STM32 备注
VCC 3.3(5.5v会烧掉) 电源正极
CS A4 / B12 片选信号
DO A6 / B14 输出
GND G 电源负极
CLK A5 / B13 时钟信号
DI A7 / B15 输入

W25Q128存储架构

W25Q128 将 16M 的容量分为 256 个块(block),每块 64K 字节;每块分为 16 个扇区(sector),一扇区 4K 字节;每扇区分为 16 个页(page),一页 256 字节。

这里不好理解的话,和书进行类比:

|-------------|-----------------------------|--------------------|-----------------------|---------------|
| W25Q128 | 256 个(block):每块 64K 字节 | 每块16 个扇区:4K 字节 | 每扇区分为 16 个页(page) | 一页 256 字节 |
| | 256 : 每章64K字 | 每章 16小节:4K 字 | 每一小节有16 页 | 一页 256 字 |

地址范围:

128M-bit = 16M-byte = 16 x 2^10K-byte = 16 x 2^10 x 2^10 byte = 2^24 byte = 16,777,216 个字节。

上述中,一个字节代表一个地址,总共是24位的地址,将2^24的字节数量转化成16进制就是0xFFF FFF。所以,地址范围(0x0~0xFFF FFF)。

W25Q128 的最小擦除单位一个扇区 ,也就是每次必须擦除 4K 个字节 。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区

W25Q128常用指令

指令(HEX) 名称 作用
0x06 写使能 写入数据/擦除之前,必须先发送该指令
0x05 读 SR1 判定 FLASH 是否处于空闲状态,擦除用
0x03 读数据 读取数据
0x02 页写 写入数据,最多写256字节
0x20 扇区擦除 扇区擦除指令,最小擦除单位

具体工作时序如下:

  • 写使能 (06H)

执行页写,扇区擦除,块擦除,片擦除,写状态寄存器等指令前,需要写使能。

拉低 CS 片选 → 发送 06H → 拉高 CS 片选

  • 读SR1(05H)

拉低 CS 片选 → 发送 05H → 返回SR1的值 → 拉高 CS 片选

  • 读数据(03H)

拉低 CS 片选 → 发送 03H → 发送24位地址 (封装函数)→ 读取数据(1~n)→ 拉高 CS 片选

  • 页写 (02H)

页写命令最多可以向FLASH传输256个字节的数据。

写使能->拉低 CS 片选 → 发送 02H → 发送24位地址 → 发送数据(1~n)→ 拉高 CS 片选

  • 扇区擦除(20H)

写入数据前,检查内存空间是否全部都是0xFF ,不满足需擦除

写使能- 等待空闲-拉低 CS 片选 → 发送擦除 20H→ 发送24位地址 → 拉高 CS 片选-等待空闲

W25Q128状态寄存器

W25Q128 一共有 3 个状态寄存器,它们的作用是跟踪芯片的状态

这里我们只介绍常用的状态寄存器 1

我们需要记住的是在状态寄存器 1 中:

BUSY: 指示当前的状态,0 表示空闲1 表示忙碌

WEL: 写使能锁定,为 1 时,可以操作页/扇区/块;为 0 时,写禁止。

小实验1:W25Q128的实验(封装SPI接口)

实验目的:

读写W25Q128

硬件清单:

W25Q128、开发板、ST-Link、USB转TTL

硬件接线:

|-----------|-------------|
| STM32 | W25Q128 |
| PA4 | CS |
| PA5 | CLK |
| PA6 | DO |
| PA7 | DI |
| 3V3 | VCC |
| GND | GND |

w25q128.c文件代码:

流程:

  1. 初始化SPI的函数;
  2. 初始化SPI各个引脚的函数;注意各引脚要求的输入输出模式。
  3. 封装一个发送和接受一个字节的函数:利用HAL_SPI_TransmitRecive()函数实现。
cs 复制代码
#include "w25q128.h"

SPI_HandleTypeDef spi_handle = {0};
void w25q128_spi_init(void){
    
    spi_handle.Instance = SPI1;
    spi_handle.Init.Mode = SPI_MODE_MASTER;    //配置成主模式还是从模式
    spi_handle.Init.Direction = SPI_DIRECTION_2LINES;                 //配置全双工还是半双工
    spi_handle.Init.DataSize = SPI_DATASIZE_8BIT;      //数据的长度:8bit
    spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW;      //CPOL = 0
    spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE;          //CPHA = 奇数边沿检测
    spi_handle.Init.NSS = SPI_NSS_SOFT;               //软件控制SS引脚配置
    spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;    //波特率分频 :256
    spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;             //高位先行还是低位先行:高位先行
  
    //下面这三个先不需要考虑
    spi_handle.Init.TIMode = SPI_TIMODE_DISABLE;
    spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    spi_handle.Init.CRCPolynomial = 7;
    
    HAL_SPI_Init(&spi_handle);
}


void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi){
    if(hspi->Instance == SPI1){
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_SPI1_CLK_ENABLE();
        
        GPIO_InitTypeDef gpio_initstruct;
        
        //NSS引脚
        gpio_initstruct.Pin = GPIO_PIN_4;
        gpio_initstruct.Mode = GPIO_MODE_OUTPUT_PP;
        gpio_initstruct.Pull = GPIO_PULLUP;
        gpio_initstruct.Speed  = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOA,&gpio_initstruct);
        
        //SCL引脚和输出引脚
        gpio_initstruct.Pin = GPIO_PIN_5 |GPIO_PIN_7;
        gpio_initstruct.Mode = GPIO_MODE_AF_PP;
        HAL_GPIO_Init(GPIOA,&gpio_initstruct);
        
        //输入引脚
        gpio_initstruct.Pin = GPIO_PIN_6;
        gpio_initstruct.Mode = GPIO_MODE_INPUT;
        HAL_GPIO_Init(GPIOA,&gpio_initstruct);
        
    } 
}

uint8_t w25q128_spi_swap_byte(uint8_t data){
    
    uint8_t recv_data = 0;
    HAL_SPI_TransmitReceive(&spi_handle,&data, &recv_data,1,1000);  ///size:尺寸代表是多少个字节
    return recv_data;
}

小实验2:封装读取芯片ID接口

文件代码:(在实验1的基础上添加)

  • 发送的指令:
  • 返回的数据
  • 上述中,发送的字节:FLASH_ManufactureDivceID是 0x90。
  • 上述,由于返回的是两个八位的数,将其保存在一个16位的变量中,因此使用位运算符。

小实验3:读写W25Q128(封装命令接口)

文件代码:

  • w25q128.c文件代码:

代码配置流程:

  1. 初始化spi函数;
  2. 初始化GPIO的函数MspInit();
  3. 封装数据交换的函数;HAL_SPI_TransmitRecive ( )。
  4. 接下来封装指令的函数:使能、读状态寄存器1(判断busy位,封装等待空闲的函数while())、读数据、页写、擦除等指令。
  5. 在主函数中:擦除、写数据、读数据。
cs 复制代码
#include "w25q128.h"

SPI_HandleTypeDef spi_handle = {0};
void w25q128_spi_init(void){
    
    spi_handle.Instance = SPI1;
    spi_handle.Init.Mode = SPI_MODE_MASTER;    //配置成主模式还是从模式
    spi_handle.Init.Direction = SPI_DIRECTION_2LINES;                 //配置全双工还是半双工
    spi_handle.Init.DataSize = SPI_DATASIZE_8BIT;      //数据的长度:8bit
    spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW;      //CPOL = 0
    spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE;          //CPHA = 奇数边沿检测
    spi_handle.Init.NSS = SPI_NSS_SOFT;               //软件控制SS引脚配置
    spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;    //波特率分频 :256
    spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;             //高位先行还是低位先行:高位先行
  
    //下面这三个先不需要考虑
    spi_handle.Init.TIMode = SPI_TIMODE_DISABLE;
    spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    spi_handle.Init.CRCPolynomial = 7;
    
    HAL_SPI_Init(&spi_handle);
}


void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi){
    if(hspi->Instance == SPI1){
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_SPI1_CLK_ENABLE();
        
        GPIO_InitTypeDef gpio_initstruct;
        
        //NSS引脚
        gpio_initstruct.Pin = GPIO_PIN_4;
        gpio_initstruct.Mode = GPIO_MODE_OUTPUT_PP;
        gpio_initstruct.Pull = GPIO_PULLUP;
        gpio_initstruct.Speed  = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOA,&gpio_initstruct);
        
        //SCL引脚和输出引脚
        gpio_initstruct.Pin = GPIO_PIN_5 |GPIO_PIN_7;
        gpio_initstruct.Mode = GPIO_MODE_AF_PP;
        HAL_GPIO_Init(GPIOA,&gpio_initstruct);
        
        //输入引脚
        gpio_initstruct.Pin = GPIO_PIN_6;
        gpio_initstruct.Mode = GPIO_MODE_INPUT;
        HAL_GPIO_Init(GPIOA,&gpio_initstruct);
        
    } 
}

uint8_t w25q128_spi_swap_byte(uint8_t data){
    
    uint8_t recv_data = 0;
    HAL_SPI_TransmitReceive(&spi_handle,&data, &recv_data,1,1000);  ///size:尺寸代表是多少个字节
    return recv_data;
}

//初始化w25q128模块
void w25q128_init(void){
    w25q128_spi_init();
}

//测试:读ID
uint16_t w25q128_read_id(void){
    uint16_t device_id = 0;
    
    W25Q128_CS(0);
    
    w25q128_spi_swap_byte(FLASH_ManufactDeviceID);
    w25q128_spi_swap_byte(0x00);
    w25q128_spi_swap_byte(0x00);
    w25q128_spi_swap_byte(0x00);
    
    device_id = w25q128_spi_swap_byte(FLASH_DummyBtye) << 8;  /* 将数据放在高8位 */
    device_id |= w25q128_spi_swap_byte(FLASH_DummyBtye);      /* 利用 |= 将数据放在低8位,并保留高8位的数据 */
    
    W25Q128_CS(1);
    
    return device_id; 
}

//写使能

void w25q128_write_enable(void)
{
    W25Q128_CS(0);
    w25q128_spi_swap_byte(FLASH_WriteEable);
    W25Q128_CS(1);
}

//读SR1寄存器
uint8_t w25q128_read_sr1(void){
    
    uint8_t recv_data = 0;
    W25Q128_CS(0);
    w25q128_spi_swap_byte(FLASH_ReadStatusReg1);
    recv_data = w25q128_spi_swap_byte(FLASH_DummyBtye);
    W25Q128_CS(1);
    return recv_data;
}

//发送地址的函数
void w25q128_send_address(uint32_t address){   //地址是3个字节,先发送高位,在发送中位,最后发送低位
    
    w25q128_spi_swap_byte(address >> 16);//高位
    w25q128_spi_swap_byte(address >> 8);  //中位:由于函数是一个8位的,因此,移动后数据后,高位自动去掉。
    w25q128_spi_swap_byte(address);
}

//忙等待的函数
void w25q128_wait_busy(void){
    
    while ((w25q128_read_sr1() & 0x01) == 0x01);  //判断最后一位是不是1
}

//读数据
void w25q128_read_data(uint32_t address,uint8_t *data,uint32_t size){
    
    uint32_t i = 0;
    
    W25Q128_CS(0);
    w25q128_spi_swap_byte(FLASH_ReadDate);
    w25q128_send_address(address);
    
    for(i = 0;i< size; i++)
       data[i] =  w25q128_spi_swap_byte(FLASH_DummyBtye);
    W25Q128_CS(1);
    
}
//页写:写的是256个字节,
void w25q128_write_page(uint32_t address,uint8_t *data,uint16_t size){   //代表的是字节数量
    
    uint16_t i = 0;
    
    w25q128_write_enable();
    W25Q128_CS(0);
    w25q128_spi_swap_byte(FLASH_PageProgram);
    w25q128_send_address(address);
    
    for(i = 0;i < size; i++)
       w25q128_spi_swap_byte(data[i]);
    W25Q128_CS(1);
    
    //忙等待,写入数据是需要花费时间的;看状态寄存器的最后一位是0还是1
    w25q128_wait_busy();
}
//扇区擦除
void w25q128_erase_sector(uint32_t address){
    w25q128_write_enable();
    w25q128_wait_busy();
    W25Q128_CS(0);
    w25q128_spi_swap_byte(FLASH_SectorErase);
    w25q128_send_address(address);
    W25Q128_CS(1);
    w25q128_wait_busy();
}
//如何指定变量的数据类型是多少
  • w25128.h文件代码
  • 定义一个宏函数:读取CS引脚的电平,是高电平还是低电平;
  • 将指令表进行宏定义,便于读写方便。
cs 复制代码
#ifndef __W25Q128_H__
#define __W25Q128_H__
#include "stm32f1xx.h"

#define W25Q128_CS(x)  do{x ? \
                            HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET): \
                            HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);\
                       }while(0)

/* 指令表 */
#define FLASH_ManufactDeviceID          0x90
#define FLASH_WriteEable                0x06
#define FLASH_ReadStatusReg1            0x05
#define FLASH_ReadDate                  0x03
#define FLASH_PageProgram               0x02
#define FLASH_SectorErase               0x20
#define FLASH_DummyBtye                 0xFF                       

void w25q128_init(void);
uint16_t w25q128_read_id(void);
void w25q128_read_data(uint32_t address,uint8_t *data,uint32_t size);
void w25q128_write_page(uint32_t address,uint8_t *data,uint16_t size);
void w25q128_erase_sector(uint32_t address);                       
#endif
  • main.c文件代码
cs 复制代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
#include "w25q128.h"
uint8_t data_write[4] = {0xAA,0xBB,0xEE,0xDD};
uint8_t data_read[4] = {0};

int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟, 72Mhz */
    led_init();                         /* LED初始化 */
    uart1_init(115200);
    printf("hello,world\r\n");
    w25q128_init();
    uint16_t device_id = w25q128_read_id();
    printf("device_id = %X \r\n",device_id);  /* %X是16进制,返回EF17 */
    
    
    w25q128_erase_sector(0x000000);
    w25q128_write_page(0x000000,data_write,4);
    w25q128_read_data(0x000000,data_read,4);
    
    printf("data_read: %X,%X,%X,%X\r\n",data_read[0],data_read[1],data_read[2],data_read[3]);
    while(1)
    {
        
    }
}

总结:

  • 相关的位运算符的使用:判断第一位是0 还是 1: & = 0x01**、** <<、将数据放在高8位 << 8**、将数据放在低八位利用 "** | =" ;
  • 熟悉模块的相关指令;读、写、擦除、读寄存器状态、使能。
相关推荐
most diligent2 小时前
蓝桥杯电子赛_零基础利用按键实现不同数字的显现
单片机·嵌入式硬件·蓝桥杯
物联网菜鸟3 小时前
STM32的HAL编码流程总结(上部)
stm32·单片机·嵌入式硬件
weixin_5088216512 小时前
STM32H7系列USART驱动区别解析 stm32h7xx_hal_usart.c与stm32h7xx_ll_usart.c的区别?
c语言·stm32·嵌入式硬件
给生活加糖!14 小时前
STM32与ESP32的区别
stm32·单片机·嵌入式硬件
上海易硅智能科技局有限公司14 小时前
AG32VH 系列应用指南
单片机·嵌入式硬件·fpga开发·agm芯片
长流小哥15 小时前
STM32:Modbus通信协议核心解析:关键通信技术
服务器·网络·stm32·单片机·嵌入式硬件·信息与通信·modbus
H21220216515 小时前
14.测速小车(测速模块)
单片机·嵌入式硬件·51单片机
SlientICE16 小时前
当物联网“芯”闯入纳米世界:ESP32-S3驱动的原子力显微镜能走多远?
单片机·物联网
what&&why16 小时前
电路中常见器件及作用(电阻 电容 电感)
单片机·嵌入式硬件
2401_8888597117 小时前
STM32 RTC实时时钟\BKP备份寄存器\时间戳
stm32·嵌入式硬件·实时音视频