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"
#include "delay.h"
// 宏定义:SPI总线的操作
// CS - PC13
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)
// SCK - PA5
#define SCK_HIGH (GPIOA->ODR |= GPIO_ODR_ODR5)
#define SCK_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR5)
// MOSI - PA7
#define MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)
#define MOSI_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR7)
// MISO - PA6,读取输入
#define MISO_READ ( GPIOA->IDR & GPIO_IDR_IDR6 )
// 产生标准的延迟时间
#define SPI_DELAY Delay_us(5)
// 初始化
void SPI_Init(void);
// SPI通信的开启和关闭
void SPI_Start(void);
void SPI_Stop(void);
// 一个时钟周期内,主机交换一个字节数据
uint8_t SPI_SwapByte(uint8_t byte);
#endif
文件:stm32_base/36_spi_software_register/Hardware/SPI/spi.c
#include "spi.h"
// 初始化
void SPI_Init(void)
{
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. GPIO配置模式
// 2.1 CS - PC13,通用推挽输出,CNF = 00,MODE = 11
GPIOC->CRH |= GPIO_CRH_MODE13;
GPIOC->CRH &= ~GPIO_CRH_CNF13;
// 2.2 SCK - PA5,通用推挽输出,CNF = 00,MODE = 11
GPIOA->CRL |= GPIO_CRL_MODE5;
GPIOA->CRL &= ~GPIO_CRL_CNF5;
// 2.3 MOSI - PA7,通用推挽输出,CNF = 00,MODE = 11
GPIOA->CRL |= GPIO_CRL_MODE7;
GPIOA->CRL &= ~GPIO_CRL_CNF7;
// 2.4 MISO - PA6,浮空输入,CNF = 01,MODE = 00
GPIOA->CRL &= ~GPIO_CRL_MODE6;
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
// 3. SCK 保持空闲状态(模式0 - 低电平空闲)
SCK_LOW;
// 4. 片选初始为未选中
CS_HIGH;
// 5. 延时
SPI_DELAY;
}
// SPI通信的开启和关闭(通过片选信号控制)
void SPI_Start(void)
{
CS_LOW;
}
void SPI_Stop(void)
{
CS_HIGH;
}
// 一个时钟周期内,主机交换一个字节数据
uint8_t SPI_SwapByte(uint8_t byte)
{
// 交换字节并返回接收数据
uint8_t rByte = 0x00;
// 通过循环每次读写一位
for (uint8_t i = 0; i < 8; i++)
{
// 1. 判断当前最高位,向MOSI输出相应电平
if ( byte & 0x80 )
{
MOSI_HIGH;
}
else
{
MOSI_LOW;
}
// 2. 移位
byte <<= 1;
// 3. 输出时钟,在第一个时钟沿产生上升沿
SCK_HIGH;
SPI_DELAY;
// 4. 移位,腾出最低位用来接收数据
rByte <<= 1;
// 5. 根据 MISO 电平来接收
if ( MISO_READ )
{
rByte |= 0x01; // 高电平则最低位置1
}
// 6. 下降沿,为下次传输准备
SCK_LOW;
SPI_DELAY;
}
return rByte;
}
文件:stm32_base/36_spi_software_register/User/main.c
#include "usart.h"
#include "w25q32.h"
int main(void)
{
// 1. 初始化
USART_Init();
W25Q32_Init();
printf("中国芯SPI通信模块实验...\n");
// 2. 获取JEDEC ID
uint8_t mID = 0;
uint16_t dID = 0;
W25Q32_ReadID(&mID, &dID);
printf("mid = %#x, did = %#x\n", mID, dID);
// 3. 写数据前需要先擦除一个扇区
W25Q32_EraseSector(0, 0);
// 4. 页写入一页数据
W25Q32_WritePage(0, 0, 0, "12345678", 8);
// 5. 读取
uint8_t buff[10] = {0};
W25Q32_Read(0x0, buff, 8);
printf("buff: %s\n", buff);
while (1)
{
}
}
注意:W25Q32 是 4MB(32Mbit)Flash,Page=256B,Sector=4KB,Block=64KB。与 W25Q64(8MB)指令集完全兼容,仅容量减半。
硬件 SPI 使用 STM32 片内外设,更高效(硬件自动移位)。实际项目中 CS 片选仍用 GPIO(PC13)独立控制,SCK/MOSI 配置为复用推挽输出:
文件:stm32_base/37_spi_hardware_register/Hardware/SPI/spi.h
#ifndef __SPI_H
#define __SPI_H
#include "stm32f10x.h"
// 宏定义:SPI总线的操作
// CS - PC13
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)
// 初始化
void SPI_Init(void);
// SPI通信的开启和关闭
void SPI_Start(void);
void SPI_Stop(void);
// 一个时钟周期内,主机交换一个字节数据
uint8_t SPI_SwapByte(uint8_t byte);
#endif
文件:stm32_base/37_spi_hardware_register/Hardware/SPI/spi.c
#include "spi.h"
// 初始化
void SPI_Init(void)
{
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 2. GPIO配置模式
// 2.1 CS - PC13,通用推挽输出,CNF = 00,MODE = 11
GPIOC->CRH |= GPIO_CRH_MODE13;
GPIOC->CRH &= ~GPIO_CRH_CNF13;
// 2.2 SCK - PA5,复用推挽输出,CNF = 10,MODE = 11
GPIOA->CRL |= GPIO_CRL_MODE5;
GPIOA->CRL |= GPIO_CRL_CNF5_1;
GPIOA->CRL &= ~GPIO_CRL_CNF5_0;
// 2.3 MOSI - PA7,复用推挽输出,CNF = 10,MODE = 11
GPIOA->CRL |= GPIO_CRL_MODE7;
GPIOA->CRL |= GPIO_CRL_CNF7_1;
GPIOA->CRL &= ~GPIO_CRL_CNF7_0;
// 2.4 MISO - PA6,浮空输入,CNF = 01,MODE = 00
GPIOA->CRL &= ~GPIO_CRL_MODE6;
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
// 3. SPI模块参数配置
// 3.1 通信模式配置
// 3.1.1 设为主机模式
SPI1->CR1 |= SPI_CR1_MSTR;
// 3.1.2 选择软件控制片选,NSS电平为高电平
SPI1->CR1 |= SPI_CR1_SSM;
SPI1->CR1 |= SPI_CR1_SSI;
// 3.1.2 SPI通信模式设为模式0:CPOL = 0,CPHA = 0
SPI1->CR1 &= ~(SPI_CR1_CPOL | SPI_CR1_CPHA);
// 3.2 设置波特率,时钟分频系数:001 - 4分频,18MHz
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= SPI_CR1_BR_0;
// 3.3 帧格式配置
// 3.3.1 帧长度:0 - 8位
SPI1->CR1 &= ~SPI_CR1_DFF;
// 3.3.2 数据传输顺序:0 - MSB在前
SPI1->CR1 &= ~SPI_CR1_LSBFIRST;
// 3.4 使能SPI
SPI1->CR1 |= SPI_CR1_SPE;
}
// SPI通信的开启和关闭(通过片选信号控制)
void SPI_Start(void)
{
CS_LOW;
}
void SPI_Stop(void)
{
CS_HIGH;
}
// 一个时钟周期内,主机交换一个字节数据
uint8_t SPI_SwapByte(uint8_t byte)
{
// 1. 将要发送的数据写入发送缓冲区
// 1.1 等待发送缓冲区为空(TXE = 1)
while ((SPI1->SR & SPI_SR_TXE) == 0 )
{}
// 1.2 将数据写入DR
SPI1->DR = byte;
// 2. 获取接收到的数据并返回
// 2.1 等待接收缓冲区非空(RXNE = 1)
while ( (SPI1->SR & SPI_SR_RXNE) == 0 )
{}
// 2.2 将DR中的数据返回
return (uint8_t)(SPI1->DR & 0xff);
}
| 参数 | 值 |
|---|---|
| 容量 | 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字节返回 |
注意:实际项目中使用 W25Q32(4MB/32Mbit),与 W25Q64 指令集完全兼容,容量减半。以下代码使用
SPI_SwapByte函数(软件/硬件 SPI 驱动均为此函数名)。
// 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_SwapByte(RDSR); // 发指令 0x05
sr = SPI_SwapByte(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_SwapByte(WREN); // 写使能
CS_HIGH;
CS_LOW;
SPI_SwapByte(PAGE_PROG); // 页编程指令
SPI_SwapByte(addr >> 16); // 地址高8位
SPI_SwapByte(addr >> 8); // 地址中8位
SPI_SwapByte(addr); // 地址低8位
for (uint16_t i = 0; i < len; i++) {
SPI_SwapByte(data[i]); // 发送数据
}
CS_HIGH;
W25Q64_WaitBusy(); // 等写入完成
}
注意:W25Q64/W25Q32 不支持"写覆盖"——必须先擦除再写。最小擦除单位是扇区(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 | 总线周转时间 |
项目路径:stm32_base/39_fsmc_sram_register
使用 FSMC 的 Bank1 子区3(NE3,地址范围 0x68000000~0x6BFFFFFF),16 位数据总线连接外部 SRAM。
文件:stm32_base/39_fsmc_sram_register/Hardware/FSMC/fsmc.h
#ifndef __FSMC_H
#define __FSMC_H
#include "stm32f10x.h"
// 初始化
void FSMC_Init(void);
#endif
文件:stm32_base/39_fsmc_sram_register/Hardware/FSMC/fsmc.c
#include "fsmc.h"
void FSMC_GPIO_Init(void);
// 初始化
void FSMC_Init(void)
{
// 1. 开启时钟
RCC->AHBENR |= RCC_AHBENR_FSMCEN;
RCC->APB2ENR |= (RCC_APB2ENR_IOPDEN | RCC_APB2ENR_IOPEEN |
RCC_APB2ENR_IOPFEN | RCC_APB2ENR_IOPGEN);
// 2. GPIO模式配置
FSMC_GPIO_Init();
// 3. FSMC寄存器配置
// 3.1 BCR3 - BTCR[4]
// 3.1.1 存储区使能
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MBKEN;
// 3.1.2 设置存储器类型:MTYP = 00,SRAM/ROM
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MTYP;
// 3.1.3 禁止Flash访问
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_FACCEN;
// 3.1.4 地址数据复用功能,不使能
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MUXEN;
// 3.1.5 设置总线宽度:MWID = 01,16位
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MWID_1;
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MWID_0;
// 3.1.6 使能写操作
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_WREN;
// 3.2 BTR - BTCR[5]
// 3.2.1 地址建立时间 ADDSET
FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_ADDSET;
// 3.2.2 数据建立时间 DATAST
FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_DATAST;
FSMC_Bank1->BTCR[5] |= (71 << 8);
}
// 配置GPIO引脚,均为复用推挽输出(CNF = 10,MODE = 11)
void FSMC_GPIO_Init(void)
{
// 1. 地址线 A0 ~ A18
// MODE = 11
GPIOF->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1 | GPIO_CRL_MODE2 |
GPIO_CRL_MODE3 | GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
GPIOF->CRH |= (GPIO_CRH_MODE12 | GPIO_CRH_MODE13 |
GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
GPIOG->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1 | GPIO_CRL_MODE2 |
GPIO_CRL_MODE3 | GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
GPIOD->CRH |= (GPIO_CRH_MODE11 | GPIO_CRH_MODE12 | GPIO_CRH_MODE13);
// CNF = 10(复用推挽输出)
GPIOF->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1 | GPIO_CRL_CNF2_1 |
GPIO_CRL_CNF3_1 | GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
GPIOF->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0 | GPIO_CRL_CNF2_0 |
GPIO_CRL_CNF3_0 | GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0);
// ...(其余地址线引脚配置类似)
// 2. 数据线 D0 ~ D15(MODE = 11,CNF = 10)
GPIOD->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
GPIOD->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
GPIOE->CRL |= GPIO_CRL_MODE7;
GPIOE->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
GPIO_CRH_MODE11 | GPIO_CRH_MODE12 | GPIO_CRH_MODE13 |
GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
// CNF = 10
GPIOD->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
// ...
// 3. 控制线
// PD4 - NOE(读使能),PD5 - NWE(写使能)
GPIOD->CRL |= (GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
GPIOD->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0);
// PG10 - NE3(片选)
GPIOG->CRH |= GPIO_CRH_MODE10;
GPIOG->CRH |= GPIO_CRH_CNF10_1;
GPIOG->CRH &= ~GPIO_CRH_CNF10_0;
// PE0, PE1 - NBL(字节掩码,16位SRAM的高低位使能)
GPIOE->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
GPIOE->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
}
// 配置完成后,FSMC 区域3的内存映射地址为 0x68000000
// 写: *(uint16_t *)0x68000000 = data;
// 读: data = *(uint16_t *)0x68000000;
文件:stm32_base/39_fsmc_sram_register/User/main.c
#include "usart.h"
#include "fsmc.h"
// 方法1:使用关键字attribute指定全局变量的地址
uint8_t v1 __attribute__((at(0x68000000)));
uint8_t v2 __attribute__((at(0x68000004)));
uint16_t v3 = 30;
int main(void)
{
// 1. 初始化
USART_Init();
FSMC_Init();
printf("中国芯FSMC扩展SRAM实验...\n");
v1 = 10;
v2 = 20;
// 测试局部变量指定地址
uint8_t v4 __attribute__((at(0x68000008)));
v4 = 40;
uint8_t v5 = 50;
// 打印地址验证
printf("v1 = %d, @%p\n", v1, &v1);
printf("v2 = %d, @%p\n", v2, &v2);
printf("v3 = %d, @%p\n", v3, &v3);
printf("v4 = %d, @%p\n", v4, &v4);
printf("v5 = %d, @%p\n", v5, &v5);
// 方法2:指针直接访问
uint8_t *p = (uint8_t *)0x68000FFF;
*p = 100;
printf("*p = %d, @%p\n", *p, p);
while (1)
{
}
}
将 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 |