12-SPI通信与FSMC总线.md 22 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"
#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_base/37_spi_hardware_register)

硬件 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);
}

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/W25Q32 擦写(读写前必须擦除)

注意:实际项目中使用 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 总线

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 扩展 SRAM(寄存器版)

项目路径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)
	{
	}
}

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,否则硬件自动管理可能产生冲突