tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — SPI/FSMC章节 + 配套代码36~42" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:SPI 是全双工同步通信,就像两个人用两根电话线同时说话和听——一根你说(MOSI),一根你听(MISO),SCK 是节拍器控制节奏,NSS 是点名器(叫到谁谁回答)。FSMC 就像给芯片外接了一个"内存扩展槽"——外部 SRAM/Flash/LCD 映射到 CPU 的地址空间,访问它们就像访问内部变量一样直接。
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 模式由 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 数据手册
| 寄存器 | 地址偏移 | 功能 |
|---|---|---|
| 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 数据寄存器 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 占位)。
项目路径: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 片内外设,更高效(硬件自动移位):
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; // 读取接收数据
}
| 参数 | 值 |
|---|---|
| 容量 | 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 写入流程:
// 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 = 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_A[25:0] | 地址总线 |
| FSMC_D[15:0] | 数据总线(16 位模式) |
| FSMC_NE[4:1] | 片选(低电平有效) |
| FSMC_NOE | 读使能(低电平有效) |
| FSMC_NWE | 写使能(低电平有效) |
| 寄存器 | 功能 |
|---|---|
| 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 | 总线周转时间 |
将 LCD 连接到 FSMC,利用地址线 A0 区分命令和数据:
写 (uint16_t *)0x60000000 = cmd → RS=0 → 写命令
写 (uint16_t *)0x60020000 = data → RS=1(A0=1) → 写数据
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 |