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 */
}
结尾
这里只是给大家提供一个验证,还有一些函数没有封装。