12-SPI通信与FSMC总线.md 15 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — SPI/FSMC章节 + 配套代码36~42" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

SPI通信与FSMC总线

用生活理解:SPI 是全双工同步通信,就像两个人用两根电话线同时说话和听——一根你说(MOSI),一根你听(MISO),SCK 是节拍器控制节奏,NSS 是点名器(叫到谁谁回答)。FSMC 就像给芯片外接了一个"内存扩展槽"——外部 SRAM/Flash/LCD 映射到 CPU 的地址空间,访问它们就像访问内部变量一样直接。


SPI 通信协议

SPI = Serial Peripheral Interface(串行外设接口),同步、全双工

四线制信号

信号 全称 功能
SCK Serial Clock 时钟线,由主机产生
MOSI Master Out Slave In 主机输出/从机输入
MISO Master In Slave Out 主机输入/从机输出
NSS/CS Chip Select 从机选择线,低电平有效

多设备拓扑

主机(SCK) ──── SCK ────┬── 从机1 CS1
                        ├── 从机2 CS2
                        └── 从机3 CS3
    (MOSI) ── MOSI ────┼── 所有从机共享
    (MISO) ── MISO ────┼── 所有从机共享

各从机独立片选(CS),SCK/MOSI/MISO 共享

SPI 四种工作模式

SPI 模式由 CPOL(时钟极性)和 CPHA(时钟相位)决定:

模式 CPOL CPHA 空闲 SCK 数据采集边沿 NSS 有效后数据变化边沿
0 0 0 低电平 上升沿(第1个) 下降沿
1 0 1 低电平 下降沿(第2个) 上升沿
2 1 0 高电平 下降沿(第1个) 上升沿
3 1 1 高电平 上升沿(第2个) 下降沿

W25Q64 Flash 支持模式 0 和模式 3。配置时注意主从必须一致。 参考:参考手册 §23(SPI 寄存器描述)、W25Q64 数据手册

SPI 关键寄存器(SPIx)

寄存器 地址偏移 功能
CR1 0x00 控制1(CPOL、CPHA、BR[2:0]波特率分频、MSTR主从、SPE使能、LSBFIRST位序、SSI、SSM)
CR2 0x04 控制2(SSOE、TXEIE/RXNEIE中断使能、DMA 使能)
SR 0x08 状态(BSY忙、TXE发送空、RXNE接收非空、MODF模式错误、OVR溢出)
DR 0x0C 数据寄存器(读写共用,写 = 发送缓冲区,读 = 接收缓冲区)
CRCPR 0x10 CRC 多项式寄存器

CR1.BR[2:0] 波特率分频: | BR[2:0] | 分频系数 | SPI1(72MHz) | SPI2/3(36MHz) | |---------|---------|-------------|--------------| | 000 | /2 | 36MHz | 18MHz | | 001 | /4 | 18MHz | 9MHz | | 010 | /8 | 9MHz | 4.5MHz | | 011 | /16 | 4.5MHz | 2.25MHz | | 100 | /32 | 2.25MHz | 1.125MHz | | 101 | /64 | 1.125MHz | 562.5KHz | | 110 | /128 | 562.5KHz | 281.25KHz | | 111 | /256 | 281.25KHz | 140.625KHz |

SPI 收发原理

SPI 数据寄存器 DR 是双缓冲结构:

写 DR → TX 缓冲区 → 移位寄存器(8位) → MOSI 逐位输出
                             ↓
MISO 逐位输入 ← 移位寄存器(8位) ← RX 缓冲区 → 读 DR

发送 1 字节的时序

1. 写 DR (CPU) → 数据进入 TX 缓冲区
2. TX 缓冲区 → 移位寄存器 (TXE=1, 可写入下一字节)
3. 移位寄存器逐位移出 (SCK 控制)
4. 同时逐位移入 MISO 数据
5. 8 位完成后数据进入 RX 缓冲区 (RXNE=1)

读写对称性:SPI 是环形移位——发 1 字节的同时必定收到 1 字节。 要读取从机数据,主机必须同时发送 1 字节(通常发 0x00 或 0xFF 占位)。


软件 SPI 实现

软件设计(寄存器版 — 软件模拟 SPI)

项目路径stm32_base/36_spi_software_register

文件:stm32_base/36_spi_software_register/Hardware/SPI/spi.h

#ifndef __SPI_H
#define __SPI_H

#include "stm32f10x.h"

// GPIO 引脚宏定义
// PA5=SCK, PA6=MISO, PA7=MOSI, PA4=CS(片选)
#define SPI_SCK_HIGH  (GPIOA->ODR |= GPIO_ODR_ODR5)   // |= SCK置1
#define SPI_SCK_LOW   (GPIOA->ODR &= ~GPIO_ODR_ODR5)  // &=~ SCK清0
#define SPI_MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)   // |= MOSI置1
#define SPI_MOSI_LOW  (GPIOA->ODR &= ~GPIO_ODR_ODR7)  // &=~ MOSI清0
#define SPI_MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6)     // & 读取MISO电平
#define SPI_CS_HIGH   (GPIOA->ODR |= GPIO_ODR_ODR4)   // |= CS置1
#define SPI_CS_LOW    (GPIOA->ODR &= ~GPIO_ODR_ODR4)  // &=~ CS清0

void SPI_Init(void);
uint8_t SPI_SendByte(uint8_t byte);

#endif

文件:stm32_base/36_spi_software_register/Hardware/SPI/spi.c

#include "spi.h"

// SPI 初始化(模式0: CPOL=0, CPHA=0)
void SPI_Init(void)
{
    // 1. 开启 GPIOA 时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. 配置 PA4(CS), PA5(SCK), PA7(MOSI) 为推挽输出 50MHz
    // PA4: CRL 控制,Pin4 的 MODE 在 CRL 的第 18~19 位
    GPIOA->CRL |= GPIO_CRL_MODE4;      // MODE4=11: 50MHz输出
    GPIOA->CRL &= ~GPIO_CRL_CNF4;      // CNF4=00: 通用推挽

    // PA5(SCK)
    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL &= ~GPIO_CRL_CNF5;

    // PA7(MOSI)
    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL &= ~GPIO_CRL_CNF7;

    // 3. 配置 PA6(MISO) 为浮空输入
    GPIOA->CRL &= ~GPIO_CRL_MODE6;     // MODE6=00: 输入
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;    // CNF6[1]=0
    GPIOA->CRL |= GPIO_CRL_CNF6_0;     // CNF6[0]=1 → 浮空输入

    // 4. 初始状态: SCK=低(CPOL=0), CS=高(不选中)
    SPI_SCK_LOW;
    SPI_CS_HIGH;
}

// SPI 模式0: CPOL=0(空闲SCK低), CPHA=0(上升沿采集)
// 发送 1 字节的同时接收 1 字节
uint8_t SPI_SendByte(uint8_t byte)
{
    uint8_t i;

    // 循环 8 次,每次处理 1 位
    for (i = 0; i < 8; i++)
    {
        // 第 1 步: 在 SCK 低电平时改变 MOSI 数据
        // MSB first (先发最高位)
        if (byte & 0x80)              // &测试: 最高位=1?
            SPI_MOSI_HIGH;            // 输出高
        else
            SPI_MOSI_LOW;             // 输出低

        byte <<= 1;                   // 左移1位,准备下一位

        // 第 2 步: SCK 上升沿 → 从机采集 MOSI
        SPI_SCK_HIGH;                 // SCK上升沿

        // 同时,主机在上升沿采集 MISO(从机的数据)
        if (SPI_MISO_READ)            // 读取MISO电平
            byte |= 1;                // 如果MISO=高,将当前位(已左移后的bit0)置1

        // 第 3 步: SCK 下降沿 → 从机改变 MISO 数据
        SPI_SCK_LOW;                  // SCK下降沿
    }

    return byte;  // 返回接收到的数据
}

硬件 SPI(stm32_base/37_spi_hardware_register)

硬件 SPI 使用 STM32 片内外设,更高效(硬件自动移位):

void SPI1_Init(void)
{
    // 1. 开启 SPI1 + GPIOA 时钟
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;      // SPI1在APB2
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. PA5(SCK) 复用推挽, PA6(MISO) 浮空输入, PA7(MOSI) 复用推挽

    // 3. 配置 SPI1
    // CR1: CPOL=0, CPHA=0, BR=011(72/16=4.5MHz), MSTR=1(主机), SPE=1(使能)
    SPI1->CR1 = 0;                              // 先清0
    SPI1->CR1 |= SPI_CR1_CPOL & 0;             // CPOL=0
    SPI1->CR1 |= SPI_CR1_CPHA & 0;             // CPHA=0
    SPI1->CR1 |= SPI_CR1_BR_1 | SPI_CR1_BR_0; // BR=011: fPCLK/16
    SPI1->CR1 |= SPI_CR1_MSTR;                  // MSTR=1: 主机模式
    SPI1->CR1 |= SPI_CR1_SSI;                   // SSI=1: 软件NSS高电平
    SPI1->CR1 |= SPI_CR1_SSM;                   // SSM=1: 软件NSS管理
    SPI1->CR1 |= SPI_CR1_SPE;                   // SPE=1: SPI使能
}

// 硬件 SPI 收发
uint8_t SPI1_SendByte(uint8_t byte)
{
    while (!(SPI1->SR & SPI_SR_TXE)) {}   // 等待 TX 缓冲区空
    SPI1->DR = byte;                       // 写入数据 → 开始发送
    while (!(SPI1->SR & SPI_SR_RXNE)) {}  // 等待 RX 缓冲区非空
    return SPI1->DR;                       // 读取接收数据
}

W25Q64 Flash 驱动

参数
容量 8MB (64Mbit)
页(Page) 256 字节
扇区(Sector) 4KB (16 页)
块(Block) 64KB (16 扇区)
擦除时间(扇区) 典型 45ms
写寿命 100,000 次

常用指令集

指令 代码 功能 后接参数
WREN 0x06 写使能(每次写前必须发)
WRDI 0x04 写禁止
RDSR 0x05 读状态寄存器 1字节返回
WRSR 0x01 写状态寄存器 1字节
READ 0x03 读数据 3字节地址 + N字节数据
PAGE_PROG 0x02 页编程(≤256字节) 3字节地址 + 数据
SECTOR_ERASE 0xD8 扇区擦除(4KB) 3字节地址
BLOCK_ERASE_32 0x52 32KB 块擦除 3字节地址
BLOCK_ERASE_64 0xD8 64KB 块擦除 3字节地址
CHIP_ERASE 0xC7 全片擦除
RDID 0x9F 读芯片 ID 3字节返回

W25Q64 擦写(读写前必须擦除)

// W25Q64 写入流程:
// 1. 读状态寄存器,检查 BUSY 位(正在擦除/写入?)
// 2. 发送 WREN (0x06) 使能写
// 3. 发送 SECTOR_ERASE (0xD8) + 3字节地址 → 等待 BUSY=0
// 4. 再次 WREN → PAGE_PROG (0x02) + 3字节地址 + 数据(≤256字节)
// 5. 等待 BUSY=0

uint8_t W25Q64_ReadSR(void)
{
    uint8_t sr;
    CS_LOW;
    SPI_SendByte(RDSR);           // 发指令 0x05
    sr = SPI_SendByte(0xFF);      // 发占住位,收状态
    CS_HIGH;
    return sr;
}

void W25Q64_WaitBusy(void)
{
    while (W25Q64_ReadSR() & 0x01);  // BUSY=1 表示忙
}

void W25Q64_WritePage(uint32_t addr, uint8_t *data, uint16_t len)
{
    W25Q64_WaitBusy();          // 等上次完成
    CS_LOW;
    SPI_SendByte(WREN);         // 写使能
    CS_HIGH;

    CS_LOW;
    SPI_SendByte(PAGE_PROG);    // 页编程指令
    SPI_SendByte(addr >> 16);   // 地址高8位
    SPI_SendByte(addr >> 8);    // 地址中8位
    SPI_SendByte(addr);         // 地址低8位
    for (uint16_t i = 0; i < len; i++) {
        SPI_SendByte(data[i]);  // 发送数据
    }
    CS_HIGH;
    W25Q64_WaitBusy();          // 等写入完成
}

注意:W25Q64 不支持"写覆盖"——必须先擦除再写。最小擦除单位是扇区(4KB)。 页编程不能跨页(256 字节边界)。如果数据跨页,需要分多次写入。


FSMC 总线

FSMC = Flexible Static Memory Controller(灵活的静态存储器控制器)。

地址映射

Bank 地址范围 容量 目标设备
Bank1 0x60000000~0x6FFFFFFF 256MB NOR Flash / PSRAM / SRAM / LCD
Bank2 0x70000000~0x7FFFFFFF 128MB NAND Flash
Bank3 0x80000000~0x8FFFFFFF 128MB NAND Flash
Bank4 0x90000000~0x9FFFFFFF 128MB PC Card

Bank1 分为 4 个子区(片选 NE1~NE4),各 64MB:

子区 片选引脚 地址范围
NE1 FSMC_NE1 0x60000000~0x63FFFFFF(最常用)
NE2 FSMC_NE2 0x64000000~0x67FFFFFF
NE3 FSMC_NE3 0x68000000~0x6BFFFFFF
NE4 FSMC_NE4 0x6C000000~0x6FFFFFFF

FSMC 信号线

信号 功能
FSMC_A[25:0] 地址总线
FSMC_D[15:0] 数据总线(16 位模式)
FSMC_NE[4:1] 片选(低电平有效)
FSMC_NOE 读使能(低电平有效)
FSMC_NWE 写使能(低电平有效)

FSMC 关键寄存器

寄存器 功能
BCRx SRAM/NOR 控制寄存器(MTYP 设备类型、MWID 数据宽度)
BTRx SRAM/NOR 时序寄存器(ADDSET 地址建立时间、DATAST 数据保持时间)
BWTRx 写时序寄存器(写操作时序,与读独立)

控制寄存器 (BCR1~BCR4): | 位 | 名称 | 说明 | |----|------|------| | 0 | MBKEN | 存储区使能 | | 1:2 | MTYP | 设备类型:00=SRAM, 01=PSRAM, 10=NOR | | 3:4 | MWID | 数据宽度:00=8位, 01=16位 | | 12 | WEN | 写使能 | | 14 | FACCEN | Flash 访问使能(NOR Flash 时用) |

时序寄存器 (BTR1~BTR4): | 位 | 名称 | 说明 | |----|------|------| | 0:3 | ADDSET | 地址建立时间(0~15个HCLK周期) | | 8:15 | DATAST | 数据保持时间(1~255个HCLK周期) | | 16:19 | BUSTURN | 总线周转时间 |

FSMC + LCD 应用

将 LCD 连接到 FSMC,利用地址线 A0 区分命令和数据:

写 (uint16_t *)0x60000000 = cmd   → RS=0 → 写命令
写 (uint16_t *)0x60020000 = data  → RS=1(A0=1) → 写数据


HAL 库版 SPI

HAL 库使用 SPI_HandleTypeDef 管理 SPI,提供 Transmit/Receive 函数。

// CubeMX 生成: MX_SPI1_Init()
SPI_HandleTypeDef hspi1;

// HAL 库发送接收
uint8_t tx_data = 0x06;           // WREN 指令
uint8_t rx_data;

HAL_SPI_Transmit(&hspi1, &tx_data, 1, 100);      // 阻塞发送 1 字节
HAL_SPI_Receive(&hspi1, &rx_data, 1, 100);        // 阻塞接收 1 字节
HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 100);  // 同时收发

// HAL 库中断方式
HAL_SPI_Transmit_IT(&hspi1, buffer, size);         // 中断发送
HAL_SPI_Receive_DMA(&hspi1, buffer, size);         // DMA 接收

// HAL 库写 W25Q64 示例
uint8_t cmd[] = {0x06};              // WREN
HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, cmd, 1, 100);
HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);

核心速查表

SPI 操作 软件模拟 硬件寄存器 HAL 库
初始化 GPIO 开漏/推挽 `CR1 = MSTR + SPE` + GPIO 复用
发 1 字节 8 次 GPIO 位操作 DR = byte; while(!RXNE); val = DR HAL_SPI_Transmit()
收 1 字节 同发送(发 0xFF 占位) 同发送(发占位字节) HAL_SPI_Receive()
同时收发 循环 8 次读写 DR = tx; val = DR HAL_SPI_TransmitReceive()
中断方式 CR2.TXEIE/RXNEIE _IT() 后缀
DMA 方式 CR2.TXDMAEN/RXDMAEN _DMA() 后缀
片选 GPIO 位操作 GPIO 位操作 HAL_GPIO_WritePin()
FSMC 操作 代码
写 SRAM *(uint16_t *)0x60000000 = data
读 SRAM data = *(uint16_t *)0x60000000
写 LCD 命令 *(uint16_t *)0x60000000 = cmd
写 LCD 数据 *(uint16_t *)0x60020000 = data

常见问题与避坑

  1. SPI 接收数据为 0xFF → MISO 连接断开、从机未选中(CS 拉低后才有输出)、从机忙于内部操作
  2. W25Q64 写入失败 → 每次写操作前必须发 WREN(0x06);扇区必须事先擦除(Flash 不能写覆盖)
  3. FSMC 读写时序不对 → 查 SRAM/LCD 数据手册的时序参数(ADDSET 和 DATAST),STM32 的 HCLK 对应关系
  4. 软件 SPI 速度太慢 → 软件 SPI 受限于 GPIO 翻转速度(约 2~4MHz),大批量数据建议用硬件 SPI
  5. NSS 软件管理模式 → 多从机时必须用 SSM=1 软件管理 NSS,否则硬件自动管理可能产生冲突