STM32 学习 —— 个人学习笔记11-2(SPI 通信外设 & 硬件 SPI 读写 W25Q64)

声明

文中内容为观看 BiliBili 视频【STM32入门教程-2023版 细致讲解 中文字幕】后学习并扩展总结。

本文章为个人学习使用,版面观感若有不适请谅解,文中知识仅代表个人观点,若出现错误,欢迎各位批评指正。

一、SPI 通信外设

1.1 SPI 外设简介

SPI(Serial Peripheral Interface,串行外设接口) 是一种高速、同步、全双工的串行通信总线,广泛用于STM32 微控制器与外部外设的短距离数据传输。STM32 内部集成硬件 SPI 收发电路,可自动完成时钟生成、数据收发等操作,大幅减轻 CPU 负担。其核心特性包括:可配置 8 位 / 16 位数据帧、高位先行/低位先行两种传输顺序;时钟频率由 fPCLK 经 2、4、8 等 8 种分频系数得到,适配不同外设需求;支持多主机模型、主从模式切换,可精简为半双工/单工通信,同时兼容 I2S 协议并支持 DMA 高速无阻塞传输。其中 STM32F103C8T6 集成 SPI1、SPI2 两个独立硬件 SPI 外设,SPI1 挂载 APB2 总线(最高 36MHz),SPI2 挂载 APB1 总线(最高 18MHz),可满足不同速率的通信需求,是嵌入式开发中常用的核心外设。

1.2 SPI 框图

串行外设接口(Serial Peripheral Interface, SPI)为全双工同步串行通信模块,其内部架构如图所示,主要由数据通路模块、时钟与控制模块及寄存器组构成,支持高速主从设备间的数据交互。SPI 外设的核心功能模块及关键寄存器配置如下表所示:

模块 / 寄存器 核心组成与功能说明
数据通路模块 由发送缓冲区、移位寄存器与接收缓冲区组成。CPU 通过地址和数据总线向发送缓冲区写入数据,数据经移位寄存器在时钟驱动下通过 MOSI 引脚串行输出;同时,外部数据通过 MISO 引脚移入移位寄存器,锁存至接收缓冲区供 CPU 读取。LSBFIRST位控制数据传输顺序(高位 / 低位优先)。
时钟与控制模块 波特率发生器基于系统时钟分频产生 SCK 同步时钟,分频系数由 SPI_CR1寄存器的 BR[2:0]位配置;主控制电路负责主 / 从模式切换、时钟极性与相位控制,实现 SPI 时序逻辑的调度。
控制寄存器组 SPI_CR1用于配置外设使能(SPE)、主从模式(MSTR)、时钟模式(CPOL/CPHA)及数据格式;SPI_CR2控制中断与 DMA 使能,支持 TXEIE/RXNEIE中断及 TXDMAEN/RXDMAENDMA 传输;SPI_SR提供工作状态反馈,包含忙标志(BSY)、收发缓冲区状态(TXE/RXNE)及错误标志(OVR/CRCERR)。

SPI 通信采用主从同步机制,主机通过 NSS 信号选中从机后,在 SCK 时钟驱动下实现数据的双向移位传输:主机写入发送缓冲区的数据经移位寄存器逐位输出,同时从机返回的数据同步移入移位寄存器并锁存至接收缓冲区。传输完成后,状态寄存器置位 TXE / RXNE 标志,CPU 可通过查询标志或中断方式处理收发数据,支持全双工、半双工及带 CRC 校验的多种工作模式。

1.3 SPI 基本结构

SPI 模块由波特率发生器、数据控制器、移位寄存器组及 GPIO 接口构成。波特率发生器通过 GPIO 输出串行时钟(SCK)信号,为整个通信链路提供同步时序;数据控制器负责调度数据流向,发送数据寄存器(TDR)与接收数据寄存器(RDR)通过移位寄存器完成数据的串并转换,分别实现主出从入(MOSI)与主入从出(MISO)方向的串行数据传输,GPIO 接口则作为物理层通道,配合开关控制实现通信的启停与模式切换。

1.4 主模式全双工连续传输

在主模式全双工(BIDIMODE=0、RXONLY=0)连续传输场景下,SPI 主设备可同时通过 MOSI/MISO 线与从机进行双向数据交换,其核心时序与标志位行为如下:

(1)工作机制与同步控制

主设备输出 SCK 时钟(示例中 CPOL=1、CPHA=1,空闲时 SCK 为高,数据在第一个下降沿移出、第二个上升沿锁存),驱动数据低位先行传输。发送数据(如 0xF1、0xF2)由主机通过 MOSI 发出,同时从机数据(如 0xA1、0xA2)通过 MISO 同步回传,实现全双工同步通信。

BSY标志位在数据传输期间由硬件自动置 1,传输完成后硬件自动清零,软件无需干预,用于标识总线忙状态。

(2)发送流程与 TXE 标志

TXE(发送缓冲器空)标志位初始为 1,表示发送数据寄存器(TDR)为空。当软件向SPI_DR写入数据(如 0xF1)后,TXE自动清零;当 TDR 数据移入移位寄存器后,TXE再次由硬件置 1,通知软件可写入下一个数据(如 0xF2),以此实现连续发送。

该标志需软件轮询或触发中断以维持数据写入节奏,避免缓冲区溢出。

(3)接收流程与 RXNE 标志

RXNE(接收缓冲器非空)标志位在移位寄存器完成数据接收后由硬件置 1,表示接收数据寄存器(RDR)已收到有效数据(如 0xA1)。当软件读取SPI_DR后,RXNE自动清零,准备下一次接收。

全双工模式下,每次 SCK 时钟周期完成 1 字节的双向交换,因此主机发送与接收同步进行,需在RXNE置位时及时读取数据,防止后续数据覆盖。

(4)连续传输实现逻辑

软件需配合标志位完成无缝数据交换:在TXE=1时写入下一个发送数据,在RXNE=1时读取已接收数据,确保 TDR 和 RDR 始终保持可用状态,实现多字节无间断传输。此过程中,主机始终作为时钟源,从机被动同步数据收发。

1.5 非连续传输

在全双工主模式(BIDIMODE=0、RXONLY=0)下,SPI 非连续传输的核心特征为软件写入数据的节奏慢于硬件传输速度,导致传输间出现间隙:

(1)时序与标志行为: 主机按 CPOL/CPHA 配置输出 SCK 时钟,MOSI 上的数据传输完成后,若软件未及时向SPI_DR写入下一个数据,TXE标志虽已置 1,但BSY标志会提前清零,SCK 时钟暂停,总线进入空闲状态,直到新数据写入才重启传输。

(2)控制逻辑: 软件需等待TXE=1后写入下一个数据,但写入延迟会造成帧间停顿,传输不再无缝衔接;传输结束后需等待BSY=0,确保总线完全空闲再进行后续操作。

二、硬件 SPI 读写 W25Q64

2.1 硬件 SPI 读写 W25Q64 的实现
  • 首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将 W25Q64 的 CS、DO、CLK 和 DI 分别与 PA4、PA5、PA6 和 PA7 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

  • 直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。

    #include "stm32f10x.h" // Device header
    #include "OLED.h"
    #include "HardSPI.h"
    #include "W25Q64.h"

    uint8_t MID;
    uint16_t DID;

    uint8_t ArrayWrite[] = {0xA1, 0xB2, 0xC3, 0xD4};
    uint8_t ArrayRead[4];

    int main(void)
    {
    OLED_Init();
    OLED_ShowString(1, 5, "Hard SPI");
    OLED_ShowString(2, 1, "MID: DID:");
    OLED_ShowString(3, 1, "W:");
    OLED_ShowString(4, 1, "R:");

    复制代码
      W25Q64_Init();    
      W25Q64_ReadID(&MID, &DID);
      
      OLED_ShowHexNum(2, 5, MID, 2);
      OLED_ShowHexNum(2, 12, DID, 4); 
      
      W25Q64_SectorErase(0x000000);
      W25Q64_PageProgram(0x000000, ArrayWrite, 4);
      
      W25Q64_ReadData(0x000000, ArrayRead, 4);
      
      OLED_ShowHexNum(3, 3, ArrayWrite[0], 2);
      OLED_ShowHexNum(3, 6, ArrayWrite[1], 2);
      OLED_ShowHexNum(3, 9, ArrayWrite[2], 2);
      OLED_ShowHexNum(3, 12, ArrayWrite[3], 2);
          
      OLED_ShowHexNum(4, 3, ArrayRead[0], 2);
      OLED_ShowHexNum(4, 6, ArrayRead[1], 2);
      OLED_ShowHexNum(4, 9, ArrayRead[2], 2);
      OLED_ShowHexNum(4, 12, ArrayRead[3], 2);
      
      while (1)
      {
      	
      }

    }

  • 由于仅将软件 SPI 读写方式替换为硬件 SPI 读写方式,因此最终实现效果与上一节保持一致。

三、演示代码关联的头文件与源文件说明

  • OLED 相关头文件请从 STM32 学习 ------ 个人学习笔记4(OLED 显示屏及调试工具) 文末查看,此处不重复展示。

  • HardSPI.c

    #include "stm32f10x.h" // Device header

    void HardSPI_W_SS(uint8_t BitValue){
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
    }

    void HardSPI_Init(void){

    复制代码
      // 提前声明需要使用的结构体
      GPIO_InitTypeDef GPIO_InitStructure;
      SPI_InitTypeDef SPI_InitStructure;
      
      // 配置 GPIO
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
      	
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA, &GPIO_InitStructure);
      
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA, &GPIO_InitStructure);
      
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA, &GPIO_InitStructure);
      
      // 配置 SPI    
      SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
      SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
      SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
      SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
      SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
      SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
      SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
      SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
      SPI_InitStructure.SPI_CRCPolynomial = 7;     
      
      SPI_Init(SPI1, &SPI_InitStructure);
      
      SPI_Cmd(SPI1, ENABLE);
      
      HardSPI_W_SS(1);

    }

    void HardSPI_Start(void){
    HardSPI_W_SS(0);
    }

    void HardSPI_Stop(void){
    HardSPI_W_SS(1);
    }

    uint8_t HardSPI_SwapByte_Mode_0(uint8_t ByteSend){
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);

    复制代码
      SPI_I2S_SendData(SPI1, ByteSend);
      
      while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
      
      return SPI_I2S_ReceiveData(SPI1);

    }

  • HardSPI.h

    #ifndef __HARDSPI_H
    #define __HARDSPI_H

    void HardSPI_Init(void);
    void HardSPI_Start(void);
    void HardSPI_Stop(void);
    uint8_t HardSPI_SwapByte_Mode_0(uint8_t ByteSend);

    #endif

  • W25Q64.c

    #include "stm32f10x.h" // Device header
    #include "HardSPI.h"
    #include "W25Q64_Ins.h"

    void W25Q64_Init(void){
    HardSPI_Init();

    }

    void W25Q64_ReadID(uint8_t *MID, uint16_t *DID){
    HardSPI_Start();
    HardSPI_SwapByte_Mode_0(W25Q64_JEDEC_ID);

    复制代码
      *MID = HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE);
      
      *DID = HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE);
      *DID <<= 8;
      *DID |= HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE);
      
      HardSPI_Stop();

    }

    void W25Q64_WriteEnable(void){
    HardSPI_Start();
    HardSPI_SwapByte_Mode_0(W25Q64_WRITE_ENABLE);
    HardSPI_Stop();
    }

    void W25Q64_WaitBusy(void){
    uint32_t Timeout;
    HardSPI_Start();
    HardSPI_SwapByte_Mode_0(W25Q64_READ_STATUS_REGISTER_1);
    Timeout = 100000;
    while ((HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE) & 0x01) == 0x01){
    Timeout--;
    if (Timeout == 0){
    break;
    }
    }
    HardSPI_Stop();
    }

    void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count){
    uint16_t i;

    复制代码
      W25Q64_WriteEnable();
      
      HardSPI_Start();
      HardSPI_SwapByte_Mode_0(W25Q64_PAGE_PROGRAM); 
      HardSPI_SwapByte_Mode_0(Address >> 16); 
      HardSPI_SwapByte_Mode_0(Address >> 8); 
      HardSPI_SwapByte_Mode_0(Address);
      
      for (i=0; i<Count; i++){
          HardSPI_SwapByte_Mode_0(DataArray[i]);
      }
      
      HardSPI_Stop();
      W25Q64_WaitBusy();

    }

    void W25Q64_SectorErase(uint32_t Address){
    W25Q64_WriteEnable();

    复制代码
      HardSPI_Start();
      HardSPI_SwapByte_Mode_0(W25Q64_SECTOR_ERASE_4KB);
      HardSPI_SwapByte_Mode_0(Address >> 16); 
      HardSPI_SwapByte_Mode_0(Address >> 8); 
      HardSPI_SwapByte_Mode_0(Address);
      
      HardSPI_Stop();
      W25Q64_WaitBusy();

    }

    void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count){
    uint32_t i;

    复制代码
      HardSPI_Start();
      HardSPI_SwapByte_Mode_0(W25Q64_READ_DATA); 
      HardSPI_SwapByte_Mode_0(Address >> 16); 
      HardSPI_SwapByte_Mode_0(Address >> 8); 
      HardSPI_SwapByte_Mode_0(Address);
      
      for (i=0; i<Count; i++){
          DataArray[i] = HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE);
      }
      HardSPI_Stop();

    }

  • W25Q64.h

    #ifndef __W25Q64_H
    #define __W25Q64_H

    void W25Q64_Init(void);
    void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
    void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
    void W25Q64_SectorErase(uint32_t Address);
    void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);

    #endif

  • W25Q64_Ins.h

    #ifndef __W25Q64_INS_H
    #define __W25Q64_INS_H

    #define W25Q64_WRITE_ENABLE 0x06
    #define W25Q64_WRITE_DISABLE 0x04
    #define W25Q64_READ_STATUS_REGISTER_1 0x05
    #define W25Q64_READ_STATUS_REGISTER_2 0x35
    #define W25Q64_WRITE_STATUS_REGISTER 0x01
    #define W25Q64_PAGE_PROGRAM 0x02
    #define W25Q64_QUAD_PAGE_PROGRAM 0x32
    #define W25Q64_BLOCK_ERASE_64KB 0xD8
    #define W25Q64_BLOCK_ERASE_32KB 0x52
    #define W25Q64_SECTOR_ERASE_4KB 0x20
    #define W25Q64_CHIP_ERASE 0xC7
    #define W25Q64_ERASE_SUSPEND 0x75
    #define W25Q64_ERASE_RESUME 0x7A
    #define W25Q64_POWER_DOWN 0xB9
    #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
    #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
    #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
    #define W25Q64_MANUFACTURER_DEVICE_ID 0x90
    #define W25Q64_READ_UNIQUE_ID 0x4B
    #define W25Q64_JEDEC_ID 0x9F
    #define W25Q64_READ_DATA 0x03
    #define W25Q64_FAST_READ 0x0B
    #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
    #define W25Q64_FAST_READ_DUAL_IO 0xBB
    #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
    #define W25Q64_FAST_READ_QUAD_IO 0xEB
    #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3

    #define W25Q64_DUMMY_BYTE 0xFF

    #endif


文中部分知识参考:B 站 ------ 江协科技;百度百科

相关推荐
dqsh066 小时前
STM32和STM32CubeMX实现遥控器控制, 保姆级教程
stm32·单片机·嵌入式硬件·机器人·遥控器
中屹指纹浏览器6 小时前
浏览器指纹内核级篡改技术实现与风险防御
经验分享·笔记
kaikaile19956 小时前
基于STM32F103的BMS通信控制
stm32·单片机·嵌入式硬件
天天爱吃肉82186 小时前
笔记:同步电机调试时电角度校正方法说明
大数据·人工智能·笔记·功能测试·嵌入式硬件·汽车
念恒123066 小时前
Python(简单判断) —— 从 if 开始
python·学习
Deitymoon6 小时前
STM32——外部中断
stm32·单片机·嵌入式硬件
峥无7 小时前
Linux 文件系统底层探秘:磁盘物理结构→inode→Ext 架构全链路
linux·运维·笔记
阿Y加油吧7 小时前
二刷 LeetCode:118. 杨辉三角 & 198. 打家劫舍 复盘笔记
笔记·算法·leetcode