当前位置: 首页 > news >正文

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通讯

多从机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 */
}

结尾

这里只是给大家提供一个验证,还有一些函数没有封装。

http://www.sczhlp.com/news/8204/

相关文章:

  • 【自学嵌入式:stm32单片机】蜂鸣器
  • 带 PVC 的 Pod 提交后时序事件
  • C#自学笔记:匿名函数与Lambda表达式
  • 如何高效率使用 Cursor ?
  • 注意力头
  • 8月8日
  • 一键上云不是梦!Apache Dubbo 发布微服务集群部署与全新控制台
  • 安装libretranslate
  • 解决 Vue 路由在 IIS 中的 404 问题
  • 位置嵌入
  • threejs之灯光不跟随OrbitControls控制器旋转
  • MAC在Docker上部署Ollama详细教程
  • 8月8日随笔
  • 场景题——高并发
  • 权重衰减系数
  • Burp Suite 拦截和修改 HTTP 接口请求
  • Jvm记录GC日志输出
  • 学习率衰减的 epoch 数
  • AD方案(OpenLDAP或微软AD)适配信创存在的不足以及可能优化方案 - 指南
  • 在Java中拆平(扁平化)List主要有以下几种常见方法:
  • 漫花国货居家
  • 【pwn做题记录】ciscn_2019_ne_5 1
  • JL一拖八燒錄器帶電池升級
  • HTML基础二
  • linux安装docker教程
  • Jaeger分布式跟踪工具初探
  • HTML基础四
  • HTML 基础三
  • 电子丨LED发光二极管的电阻取值计算方法
  • Ansible部署Node_exporter