SPI详细讲解+W25Q128验证

一、SPI简介

SPI (Serial Peripheral Interface) 是摩托罗拉公司开发的一种高速同步串行通讯协议。

  • SPI 没有理论速度限制,通讯速度由硬件决定,例如 W25Q128 的通讯速度可达 104MHz。

  • 常用于存储器、显示器、传感器等设备。

  • 支持主从模式(一主多从/多主多从)。

  • SPI 通过四根线进行通讯,分别是:

    • SCK:时钟线

    • MOSI:主输出从输入线

    • MISO:主输入从输出线

    • CS:片选线

  • CS 片选引脚用于选择需要通讯的设备,低电平选中,高电平取消选中。

  • SCK 提供数据传输的同步时钟信号。

  • MOSI (Master Out Slave In):主设备向从设备发送数据。

  • MISO (Master In Slave Out):从设备向主设备发送数据。

  • 全双工:支持同时发送和接收数据。


具体可以查看官方文档

SPI通讯

多从机SPI通讯

二、通讯协议对比

| 协议 | SPI | I2C | UART |

| — | — | — | — |

| 通讯模式 | 同步,全双工 | 同步,半双工 | 异步,全双工 |

| 数据方式 | 主从结构,从设备由片选控制 | 主从结构,地址控制 | 点对点通讯 |

| 信号线数量 | 4根(SCLK,MOSI,MISO,CS) | 2根(SCL,SDA) | 2根(TXD,RXD) |

| 通讯速率 | 最高可达100Mhz+ | 快速模式下400kHz | 一般最高3Mbps |

| 优点 | 高速,全双工,简单 | 简单,成本低 | 成本低,异步通讯,全双工 |

| 缺点 | 需要更多信号线 | 速率低,协议复杂 | 速率低 |

三、SPI通讯协议

3.1 SPI如何传输数据

主从设备传输

  • 图片中的SS就是我们上面提到的CS

  • SCK只能由主机发送

  • SCK来到上升沿时,数据的移除,高位先行

  • SCK下降沿时,数据的移入

  • 循环八次时钟SCK信号,就可以完成八位数据的交换,从而实现数据的收发

3.2 如果只想发送或者只想接收

  • 只发送:不对接收的数据做处理。

  • 只接收:发送任意数据(通常是 0x000xFF)以置换需要接收的数据。

3.3 SPI时序图

SPI模式0,CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出

  • 开始信号(序号1):在开始发送前,CS 由高电平变为低电平,表示通讯开始。

  • 停止信号(序号4):CS 由低电平变为高电平,表示本次通讯结束。

  • 在开始和结束之间,橙色虚线代表上升沿,蓝色代表下降沿。前面我们说过:

    • 上升沿:数据移入。主机获取 MISO 的数据填充到数据寄存器中,从机同样获取 MOSI 的数据填充到从机的数据寄存器中。

    • 下降沿:数据移出。主机通过 MOSI 移出一位数据,从机通过 MISO 移出一位数据。

3.4 SPI模式

SPI 提供了四种数据移入和移出模式,主要是为了兼容更多的芯片,这取决于 SPI 控制寄存器中的 CPOLCPHA 位。

  • CPOL (Clock Polarity):时钟极性,决定 SCK 在空闲状态下的电平。

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

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

  • CPHA (Clock Phase):时钟相位,决定数据何时进行采样(移入)。

    • 0:在时钟的第 1 个跳变沿进行采样。

    • 1:在时钟的第 2 个跳变沿进行采样。

SPI模式0,CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出

需要注意模式0在片选信号拉低的时候进行数据的移出了,提前了半个位,这样就能保证第一个上升沿来到时,数据的移入

SPI模式1,CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出

SPI模式2,CPOL = 1,CPHA = 1:CLK空闲状态 = 高电平,数据在下降沿采样,并在上升沿移出

SPI模式3,CPOL = 1,CPHA = 0:CLK空闲状态 = 高电平,数据在上升沿采样,并在下降沿移出

| 模式 | CPOL | CPHA | SCK空闲状态 | 采样时刻 |

| :–: | :–: | :–: | :–: | :–: |

| 模式0 | 0 | 0 | 低电平 | 第一个边沿 |

| 模式1 | 0 | 1 | 低电平 | 第二个边沿 |

| 模式2 | 1 | 0 | 高电平 | 第一个边沿 |

| 模式3 | 1 | 1 | 高电平 | 第二个边沿 |

通常情况下,最常见的是模式0和模式3。

四、SPI外设关键知识点

4.1 引脚

stm32c8t6为例,这颗芯片有两个SPI接口SPI1,SPI2,每个SPI都有对应的信号引脚

|SPI|NSS|SCK|MOSI|MISO|

|–|–|–|–|–|

|SPI1|PA4|PA5|PA7|PA6|

|SPI2|PB12|PB13|PB15|PB14|

  • STM32每个SPI外设仅有一个固定的硬件NSS引脚

  • 片选信号可以由我们自己通过软件进行选择,后面我们会进行讲解

  • NSS:当使用硬件片选时,使用复用推挽输出,当使用软件片选时,使用推挽输出

  • SCK:是由硬件所决定的,所以使用复用推挽输出

  • MISO:是由硬件所决定的,所以使用浮空输入或者上拉输入

  • MOSI:是由硬件所决定的,所以使用复用推挽输出

4.2 SPI模式

这里的模式和我们上面提到的CPOL和CPHA不一样,我们这里的SPI只的时在通讯时担任的角色(主设备/从设备)

  • 主设备(Master): 控制通讯时序 提供时钟信号 发起数据传输

  • 从设备(Slave): 被动等待主设备发起通信 按照主设备时钟信号收发数据

4.3 传输方向

SPI数据传输方向,决定了数据传输的单向性或者双向性

  • 双线全双工模式(SPI_DIRECTION_2LINE):使用两条数据线(MISO和MOSI)来进行全双工通讯

  • 双线仅接收模式(SPI_DIRECTION_2LINE_RX): 设备仅接收数据,而不发送数据

  • 单线双向模式(SPI_DIRECTION_1LINE):数据在同一条线上进行发送和接收

4.4 数据帧格式

数据帧格式格式,指的是SPI数据传输时,数据的位数,数据传输的单位是“帧”

  • 8位:一帧数据的大小位8位,即每次传输1字节的数据

  • 16位:一帧数据的大小位16位,即每次传输2字节的数据

4.5 数据帧顺序

数据帧顺序,指的是每次SPI数据传输时,数据帧传输的首位是最高位还是最低位

  • 高位优先(MSB):数据传输时,数据的最高位先被传输

    • 如:需要传输的数据为0xAA,转换成二进制就是 1010 1010 如果是高位优先,就先将最高位(最左边)的1给移出去
  • 低位优先(LSB):数据传输时,数据的最低位先被传输

    • 同样的我们以0xAA为例,转换成二进制就是 1010 1010 如果是低位优先,就先将最低位(最右边)的0给移出

4.6 片选信号

  • 软件管理模式:

    • NSS信号由软件手动控制

    • 主设备通过软件拉高拉低NSS引脚,从而控制从设备的选择和数据传输

  • 硬件管理模式:

    • NSS信号由硬件自动控制

    • 主设备通过拉低NSS引脚来选择从设备,数据传输自动开始和结束

一般情况下使用软件管理模式,因为比较灵活

4.7SPI波特率

SPI波特率:指的是SPI传输数据的速率

  • SPI波特率计算公式:

    • SPI波特率 = Fclk / 波特率预分频系数
  • Fclk: SPI外设时钟(在stm32c8t6中,SPI1:72MHz,SPI2:36MHz)

  • 波特率预分频系数: 2,4,6,16,64,32,128,256

4.8 数据发送和数据接收

我们使用HAL_SPI_TransmitReceive()函数来进行数据的发送和接收


HAL_SPI_TransmitReceive

(

    SPI_HandleTypeDef *hspi,     /*SPI句柄*/

    uint8_t *pTxData,            /*指向待发送数据的缓冲区指针*/

    uint8_t *pRxData,            /*指向接收数据的缓冲区指针*/

    uint16_t Size,               /*要发送和接收的数据字节数*/

    uint32_t Timeout             /*超时时间,单位:ms*/

)

五、使用SPI控制W25Q128

5.1 SPI FLASH模块简介

5.1.1 W25Q128简介

W25Q128是常用的FLASH存储器,它具有容量大,可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性

W25Qxx

  • 容量:128Mbit(16M字节)

  • 适合存储字库,固件等

  • 使用SPI通讯,时钟频率可达104MHz

  • 擦写周期可达10w次,保存时间可达20年

正点原子精英板SPI FLASH原理图

这里我们使用正点原子精英板作为例子,通过原理图分析:

  • 其中SO就是SPI的MISO,也就是从机的输出,这里的W25Q128作为从机,SI同理

  • HOLD引脚用于在通讯过程中,如果需要暂停就拉低HOLD引脚,数据就保持现有的状态,拉高恢复继续传输

    | 引脚 | 功能 |

    | ---------- | ------------------------------ |

    | VCC、GND | 电源(2.7V~3.6V) |

    | CS | 片选引脚(低电平选中) |

    | CLK | 时钟引脚 |

    | SO(MISO) | 主机输入从机输出 |

    | SI(MOSI) | 主机输出从机输入 |

    | WP | 写保护(0:只读,1:可读可写) |

    | HOLD | 数据保持引脚 |

5.1.2 W25Q128内部结构

W25Qxx内部结构

5.1.2.1 内存划分
  • 16MB存储空间共分为256个64KB块

  • 一个64KB块包含16个扇区(每个扇区4KB)

  • 一个扇区包含16个页(每个页256字节)

  • 由此我们可以进行验算一下: 256 * 16 * 256 = 16MB

5.1.2.2 擦除操作
  • 可按扇区(4KB),块(64KB)或整片(16MB)进行擦除

  • 擦除后的数据会全部被清楚(变成0xFF)

  • 由于最小可擦除的单位为4KB,一般情况下如果想要改写某个字节,需要先将整个4KB的区域擦除,然后再写入就需要引入缓存区

5.1.2.3 写入操作
  • 连续写入量不可超过256字节,下一个写入时序需要等到状态寄存器的busy位清空才能写入

  • 每个数据位只能由1改为0,不能由0改为1

  • 如果需要写入的扇区全为0xFF,则可以直接写入,不需要先擦除

  • 如果需要写入的扇区有其他数据,则需要先将该扇区擦除,然后再写入

5.1.3 W25Q128常用指令

| 指令 | 名称 | 作用 |

| ---- | -------- | ------------------------------------------ |

| 0x90 | 读设备ID | 读取设备ID使用,验证通信是否成功 |

| 0x06 | 写使能 | 写入数据/擦除之前,必须先发送该指令 |

| 0x05 | 读SR1 | 判定FLASH是否处于空闲状态,擦除/写入用 |

| 0x20 | 扇区擦除 | 扇区擦除指令,最小擦除单位(4KB/4096字节) |

| 0x02 | 页写 | 用于写入FLASH数据,最多可写256字节 |

| 0x03 | 读数据 | 读取FLASH数据 |


写使能时序

从芯片手册写使能的时序我们可以看到,W25Q128支持模式0模式3

  • DI就是数据输入的意思,这里指W25Q128在位从机,所以对应的主机引脚位MOSI,也就是说这是主机发送给从机的数据0x06

  • 因为0x06只需要发送不需要接收,所以DO默认保存高电平(0xFF)就好

更多指令请参考W25Q128 datasheet

5.2 SPI FLASH代码

首先先对SPI接口的封装(25Q128_PORT.h):


#ifndef _25Q128_PORT_H_

#define _25Q128_PORT_H_

#include "./SYSTEM/sys/sys.h"



#define W25Q128_SPI         SPI2

#define W25Q128_SPI_CLK_ENABLE() __HAL_RCC_SPI2_CLK_ENABLE()



#define W25Q128_CS_PORT    GPIOB

#define W25Q128_CS_PIN     GPIO_PIN_12

#define W25Q128_CS_CLK_ENABLE()  __HAL_RCC_GPIOB_CLK_ENABLE()



#define W25Q128_SCK_PORT   GPIOB

#define W25Q128_SCK_PIN    GPIO_PIN_13

#define W25Q128_SCK_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()



#define W25Q128_MISO_PORT  GPIOB

#define W25Q128_MISO_PIN   GPIO_PIN_14

#define W25Q128_MISO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()



#define W25Q128_MOSI_PORT  GPIOB

#define W25Q128_MOSI_PIN   GPIO_PIN_15

#define W25Q128_MOSI_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()



#define W25Q128_CS(x)         do{ x ? \

                                    HAL_GPIO_WritePin(W25Q128_CS_PORT, W25Q128_CS_PIN, GPIO_PIN_SET) : \

                                    HAL_GPIO_WritePin(W25Q128_CS_PORT, W25Q128_CS_PIN, GPIO_PIN_RESET);\

                                }while(0)



uint8_t w25q128_read_write_byte(uint8_t byte);

void spi_init(void);



#endif

对SPI的初始化函数(25Q128_PORT.c):


#include "./BSP/25Q128/25Q128_PORT.h"

#include "./SYSTEM/usart/usart.h"



SPI_HandleTypeDef g_spi_handle;



static void spi_gpio_config(void)

{

    GPIO_InitTypeDef gpio_init_struct;



    W25Q128_CS_CLK_ENABLE();   /* 使能CS时钟 */

    W25Q128_SCK_CLK_ENABLE();  /* 使能SCK时钟 */

    W25Q128_MISO_CLK_ENABLE(); /* 使能MISO时钟 */

    W25Q128_MOSI_CLK_ENABLE(); /* 使能MOSI时钟 */



    /* 配置CS引脚 */

    gpio_init_struct.Pin = W25Q128_CS_PIN;             /* CS引脚 */

    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;       /* 推挽输出 */

    gpio_init_struct.Pull = GPIO_NOPULL;               /* 无上下拉 */

    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;     /* 高速 */

    HAL_GPIO_Init(W25Q128_CS_PORT, &gpio_init_struct); /* 初始化CS引脚 */



    W25Q128_CS(1); /* 取消片选 */



    /* 配置SCK引脚 */

    gpio_init_struct.Pin = W25Q128_SCK_PIN;             /* SCK引脚 */

    gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 复用推挽输出 */

    gpio_init_struct.Pull = GPIO_NOPULL;                /* 无上下拉 */

    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */

    HAL_GPIO_Init(W25Q128_SCK_PORT, &gpio_init_struct); /* 初始化SCK引脚 */



    /* 配置MISO引脚 */

    gpio_init_struct.Pin = W25Q128_MISO_PIN;             /* MISO引脚 */

    gpio_init_struct.Mode = GPIO_MODE_INPUT;             /* 输入 */

    gpio_init_struct.Pull = GPIO_PULLUP;                 /* 上拉 */

    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;       /* 高速 */

    HAL_GPIO_Init(W25Q128_MISO_PORT, &gpio_init_struct); /* 初始化MISO引脚 */



    /* 配置MOSI引脚 */

    gpio_init_struct.Pin = W25Q128_MOSI_PIN;             /* MOSI引脚 */

    gpio_init_struct.Mode = GPIO_MODE_AF_PP;             /* 复用推挽输出 */

    gpio_init_struct.Pull = GPIO_NOPULL;                 /* 无上下拉 */

    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;       /* 高速 */

    HAL_GPIO_Init(W25Q128_MOSI_PORT, &gpio_init_struct); /* 初始化MOSI引脚 */

}



static void spi_config(void)

{



    W25Q128_SPI_CLK_ENABLE(); /* 使能SPI时钟 */



    g_spi_handle.Instance = W25Q128_SPI;

    g_spi_handle.Init.Mode = SPI_MODE_MASTER;                      /* 主模式 */

    g_spi_handle.Init.Direction = SPI_DIRECTION_2LINES;            /* 全双工 */

    g_spi_handle.Init.DataSize = SPI_DATASIZE_8BIT;                /* 8位数据帧格式 */

    g_spi_handle.Init.CLKPolarity = SPI_POLARITY_HIGH;             /* 时钟悬空高 */

    g_spi_handle.Init.CLKPhase = SPI_PHASE_2EDGE;                  /* 第2个时钟边沿采样数据 */

    g_spi_handle.Init.NSS = SPI_NSS_SOFT;                          /* 软件NSS管理 */

    g_spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; /* 波特率预分频8 */

    g_spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;                 /* MSB先行 */

    g_spi_handle.Init.TIMode = SPI_TIMODE_DISABLE;                 /* 关闭TI模式 */

    g_spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /* 关闭CRC计算 */

    g_spi_handle.Init.CRCPolynomial = 7;                           /* CRC多项式7 */



    HAL_SPI_Init(&g_spi_handle); /* 初始化SPI */



    __HAL_SPI_ENABLE(&g_spi_handle); /* 使能SPI */

}



uint8_t w25q128_read_write_byte(uint8_t byte)

{

    uint8_t read_byte = 0;

    HAL_SPI_TransmitReceive(&g_spi_handle, &byte, &read_byte, 1, HAL_MAX_DELAY);

    return read_byte;

}



void spi_init(void)

{

    spi_gpio_config(); /* 配置SPI引脚 */

    spi_config(); /* 配置SPI */

}

对于w25Q128函数的封装(25Q128.c):


#include "./BSP/25Q128/25Q128_PORT.h"

#include "./BSP/25Q128/25Q128.h"

#include "./SYSTEM/usart/usart.h"



static uint8_t w25q128_check_busy(void)

{

    uint8_t status = 0;

    W25Q128_CS(0);

    w25q128_read_write_byte(0x05);          /* 发送读取状态寄存器命令 */

    status = w25q128_read_write_byte(0xFF); /* 读取状态寄存器 */

    W25Q128_CS(1);



    return status;

}




void w25q128_waite_busy(void)

{

    while (w25q128_check_busy() & 0x01) /* 检查忙状态 */

        ;

}



uint16_t w25q128_id_check(void)

{

    uint16_t id = 0;

    W25Q128_CS(0);                 /* 片选 */

    w25q128_read_write_byte(0x90); /* 发送读取ID命令 */

    w25q128_read_write_byte(0x00);

    w25q128_read_write_byte(0x00);

    w25q128_read_write_byte(0x00);



    id = w25q128_read_write_byte(0xFF) << 8; /* 写入ID高8位 */

    id |= w25q128_read_write_byte(0xFF);     /* 写入ID低8位 */



    W25Q128_CS(1);

    w25q128_waite_busy(); /* 等待空闲 */



    return id; /* 返回ID */

}



void w25q128_init(void)

{

    uint16_t id = 0;

    spi_init();                                    /* 初始化SPI */

    w25q128_waite_busy();                          /* 等待空闲 */

    id = w25q128_id_check();                       /* 发送复位命令 */



    printf("W25Q128 ID: 0x%04X\n", id);            /* 打印ID */

}

W25Q128头文件(w25q128.h):


#include "./BSP/25Q128/25Q128_PORT.h"

#include "./BSP/25Q128/25Q128.h"

#include "./SYSTEM/usart/usart.h"



static uint8_t w25q128_check_busy(void)

{

    uint8_t status = 0;

    W25Q128_CS(0);

    w25q128_read_write_byte(0x05);          /* 发送读取状态寄存器命令 */

    status = w25q128_read_write_byte(0xFF); /* 读取状态寄存器 */

    W25Q128_CS(1);



    return status;

}




void w25q128_waite_busy(void)

{

    while (w25q128_check_busy() & 0x01) /* 检查忙状态 */

        ;

}



uint16_t w25q128_id_check(void)

{

    uint16_t id = 0;

    W25Q128_CS(0);                 /* 片选 */

    w25q128_read_write_byte(0x90); /* 发送读取ID命令 */

    w25q128_read_write_byte(0x00);

    w25q128_read_write_byte(0x00);

    w25q128_read_write_byte(0x00);



    id = w25q128_read_write_byte(0xFF) << 8; /* 写入ID高8位 */

    id |= w25q128_read_write_byte(0xFF);     /* 写入ID低8位 */



    W25Q128_CS(1);

    w25q128_waite_busy(); /* 等待空闲 */



    return id; /* 返回ID */

}



void w25q128_init(void)

{

    uint16_t id = 0;

    spi_init();                                    /* 初始化SPI */

    w25q128_waite_busy();                          /* 等待空闲 */

    id = w25q128_id_check();                       /* 发送复位命令 */



    printf("W25Q128 ID: 0x%04X\n", id);            /* 打印ID */

}



结尾

这里只是给大家提供一个验证,还有一些函数没有封装,更多详情请查看我的个人博客

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐