SPI详细讲解+W25Q128验证
本文详细介绍了SPI(串行外设接口)通讯协议及其应用。SPI是一种高速同步串行通讯协议,采用四线制(SCK、MOSI、MISO、CS)实现全双工通信,理论速度可达100MHz+。文章对比了SPI与I2C、UART协议的差异,阐述了SPI的四种工作模式(由CPOL和CPHA决定),并详细讲解了STM32的SPI外设配置要点,包括引脚定义、主从模式设置、数据传输方向和帧格式等。最后介绍了W25Q128
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 | 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 如果只想发送或者只想接收
-
只发送:不对接收的数据做处理。
-
只接收:发送任意数据(通常是
0x00或0xFF)以置换需要接收的数据。
3.3 SPI时序图

-
开始信号(序号1):在开始发送前,
CS由高电平变为低电平,表示通讯开始。 -
停止信号(序号4):
CS由低电平变为高电平,表示本次通讯结束。 -
在开始和结束之间,橙色虚线代表上升沿,蓝色代表下降沿。前面我们说过:
-
上升沿:数据移入。主机获取
MISO的数据填充到数据寄存器中,从机同样获取MOSI的数据填充到从机的数据寄存器中。 -
下降沿:数据移出。主机通过
MOSI移出一位数据,从机通过MISO移出一位数据。
-
3.4 SPI模式
SPI 提供了四种数据移入和移出模式,主要是为了兼容更多的芯片,这取决于 SPI 控制寄存器中的 CPOL 和 CPHA 位。
-
CPOL (Clock Polarity):时钟极性,决定
SCK在空闲状态下的电平。-
0:SCK在空闲状态下保持低电平。 -
1:SCK在空闲状态下保持高电平。
-
-
CPHA (Clock Phase):时钟相位,决定数据何时进行采样(移入)。
-
0:在时钟的第 1 个跳变沿进行采样。 -
1:在时钟的第 2 个跳变沿进行采样。
-

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



| 模式 | 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存储器,它具有容量大,可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性

-
容量:128Mbit(16M字节)
-
适合存储字库,固件等
-
使用SPI通讯,时钟频率可达104MHz
-
擦写周期可达10w次,保存时间可达20年

这里我们使用正点原子精英板作为例子,通过原理图分析:
-
其中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内部结构

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 */
}
结尾
这里只是给大家提供一个验证,还有一些函数没有封装,更多详情请查看我的个人博客。
更多推荐



所有评论(0)