12-SPI通信与FSMC总线.md 49 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 占位)。


项目 36:软件 SPI(寄存器版)

使用 GPIO 位操作模拟 SPI 时序,纯软件实现,不依赖硬件 SPI 外设。

项目路径上部-基础篇\03_代码\stm32\36_spi_software_register

spi.h

文件Hardware/SPI/spi.h

#ifndef __SPI_H
#define __SPI_H

#include "stm32f10x.h"
#include "delay.h"

// 宏定义:控制各信号线高低电平
// 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);
void SPI_Start(void);
void SPI_Stop(void);
uint8_t SPI_SwapByte(uint8_t byte);

#endif

spi.c

文件Hardware/SPI/spi.c

#include "spi.h"

void SPI_Init(void)
{
    // 1. 开启时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. GPIO配置模式
    // CS - PC13,通用推挽输出,CNF = 00,MODE = 11
    GPIOC->CRH |= GPIO_CRH_MODE13;
    GPIOC->CRH &= ~GPIO_CRH_CNF13;

    // SCK - PA5,通用推挽输出,CNF = 00,MODE = 11
    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL &= ~GPIO_CRL_CNF5;

    // MOSI - PA7,通用推挽输出,CNF = 00,MODE = 11
    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL &= ~GPIO_CRL_CNF7;

    // 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;
}

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;

        byte <<= 1;

        // 2. 输出时钟上升沿
        SCK_HIGH;
        SPI_DELAY;

        // 3. 移位,腾出最低位用来接收
        rByte <<= 1;
        if (MISO_READ) rByte |= 0x01;

        // 4. 下降沿,为下次传输准备
        SCK_LOW;
        SPI_DELAY;
    }

    return rByte;
}

main.c

文件User/main.c

#include "usart.h"
#include "w25q32.h"
#include <string.h>

int main(void)
{
    USART_Init();
    W25Q32_Init();

    printf("中国芯SPI通信模块实验开始...\n");

    // 读取ID进行检验
    uint8_t mid = 0;
    uint16_t did = 0;
    W25Q32_ReadID(&mid, &did);
    printf("mid = %#x, did = %#x\n", mid, did);

    // 扇区擦除
    W25Q32_EraseSector(0, 0);

    // 页写入
    W25Q32_PageWrite(0, 0, 0, "12345678", 8);

    // 读取
    uint8_t buffer[10] = {0};
    W25Q32_Read(0, 0, 0, 2, buffer, 6);

    printf("buffer = %s\n", buffer);

    while (1) {}
}

软件 SPI vs 硬件 SPI:软件 SPI 不依赖片内外设,任意 GPIO 均可模拟,但速度受限(约 2~4MHz)。适合低速设备或硬件 SPI 引脚被占用的场景。


项目 37:硬件 SPI(寄存器版)

使用 STM32 片内 SPI 外设,硬件自动移位,效率更高。CS 片选仍用 GPIO(PC13)独立控制,SCK/MOSI 配置为复用推挽输出。

项目路径上部-基础篇\03_代码\stm32\37_spi_hardware_register

spi.h

文件Hardware/SPI/spi.h

#ifndef __SPI_H
#define __SPI_H

#include "stm32f10x.h"

// CS - PC13(片选仍用GPIO独立控制)
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW  (GPIOC->ODR &= ~GPIO_ODR_ODR13)

void SPI_Init(void);
void SPI_Start(void);
void SPI_Stop(void);
uint8_t SPI_SwapByte(uint8_t byte);

#endif

spi.c

文件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配置模式
    // PC13:通用推挽输出(CS)
    GPIOC->CRH |= GPIO_CRH_MODE13;
    GPIOC->CRH &= ~GPIO_CRH_CNF13;

    // PA5(SCK)、PA7(MOSI):复用推挽输出,CNF = 10
    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL |= GPIO_CRL_CNF5_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF5_0;

    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL |= GPIO_CRL_CNF7_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF7_0;

    // PA6(MISO):浮空输入
    GPIOA->CRL &= ~GPIO_CRL_MODE6;
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOA->CRL |= GPIO_CRL_CNF6_0;

    // 3. SPI模块参数配置
    // 3.1 设为主机模式
    SPI1->CR1 |= SPI_CR1_MSTR;

    // 3.2 软件控制片选,NSS电平为高
    SPI1->CR1 |= SPI_CR1_SSM;
    SPI1->CR1 |= SPI_CR1_SSI;

    // 3.3 模式0:CPOL = 0,CPHA = 0
    SPI1->CR1 &= ~SPI_CR1_CPOL;
    SPI1->CR1 &= ~SPI_CR1_CPHA;

    // 3.4 波特率:4分频(18MHz,当APB2=72MHz时)
    SPI1->CR1 &= ~SPI_CR1_BR;
    SPI1->CR1 |= SPI_CR1_BR_0;

    // 3.5 帧格式:8位,MSB在前
    SPI1->CR1 &= ~SPI_CR1_DFF;
    SPI1->CR1 &= ~SPI_CR1_LSBFIRST;

    // 3.6 使能SPI
    SPI1->CR1 |= SPI_CR1_SPE;
}

void SPI_Start(void) { CS_LOW; }
void SPI_Stop(void)  { CS_HIGH; }

uint8_t SPI_SwapByte(uint8_t byte)
{
    // 1. 等待发送缓冲区为空(TXE = 1)
    while ((SPI1->SR & SPI_SR_TXE) == 0) {}

    // 2. 将数据写入DR
    SPI1->DR = byte;

    // 3. 等待接收缓冲区非空(RXNE = 1)
    while ((SPI1->SR & SPI_SR_RXNE) == 0) {}

    // 4. 从DR读取接收到的数据
    return (uint8_t)(SPI1->DR & 0xff);
}

关键差异:SCK/MOSI 配置为 复用推挽输出(CNF=10),由 SPI 外设自动控制电平,CPU 只需读写 DR 寄存器。CS 仍为通用推挽输出(GPIO 控制)。

main.c

与项目 36 的 main.c 完全相同(W25Q32_* 函数适配 SPI_SwapByte 接口即可)。


项目 38:硬件 SPI(HAL 库版)

使用 STM32CubeMX 生成初始化代码,HAL 库封装 SPI 外设。CS 片选仍由 GPIO 独立控制,CubeMX 中配置 PC13 为普通输出。

项目路径上部-基础篇\03_代码\stm32\38_spi_hardware_hal

CubeMX 生成:spi.c

文件Core/Src/spi.c

SPI_HandleTypeDef hspi1;

void MX_SPI1_Init(void)
{
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;
    if (HAL_SPI_Init(&hspi1) != HAL_OK)
    {
        Error_Handler();
    }
}

void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (spiHandle->Instance == SPI1)
    {
        __HAL_RCC_SPI1_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();

        // PA5(SCK), PA7(MOSI) — 复用推挽输出
        GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        // PA6(MISO) — 浮空输入
        GPIO_InitStruct.Pin = GPIO_PIN_6;
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

CS 引脚定义(CubeMX 生成)

文件Core/Inc/main.h

#define CS_Pin       GPIO_PIN_13
#define CS_GPIO_Port GPIOC

文件Core/Src/gpio.c

void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);

    GPIO_InitStruct.Pin = CS_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(CS_GPIO_Port, &GPIO_InitStruct);
}

用户添加代码(spi.c USER CODE 段)

void SPI_Start(void)
{
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
}

void SPI_Stop(void)
{
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
}

uint8_t SPI_SwapByte(uint8_t byte)
{
    uint8_t rByte;
    HAL_SPI_TransmitReceive(&hspi1, &byte, &rByte, 1, 1000);
    return rByte;
}

main.c

文件Core/Src/main.c

#include "main.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"
#include "w25q32.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_SPI1_Init();
    MX_USART1_UART_Init();

    printf("中国芯SPI通信模块实验开始...\n");

    uint8_t mid = 0;
    uint16_t did = 0;
    W25Q32_ReadID(&mid, &did);
    printf("mid = %#x, did = %#x\n", mid, did);

    W25Q32_EraseSector(0, 0);
    W25Q32_PageWrite(0, 0, 0, "12345678", 8);

    uint8_t buffer[10] = {0};
    W25Q32_Read(0, 0, 0, 2, buffer, 6);
    printf("buffer = %s\n", buffer);

    while (1) {}
}

HAL 库优势:初始化代码由 CubeMX 生成,跨芯片移植方便;HAL_SPI_TransmitReceive 封装了 TXE/RXNE 轮询逻辑。但仍有轮询等待开销,大批量数据建议使用 DMA 方式。


W25Q64 / W25Q32 Flash 驱动

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

两者指令集完全兼容,容量减半。以下驱动使用 SPI_SwapByte 接口,三个项目(36/37/38)均可直接复用。

常用指令集

指令 代码 功能 后接参数
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字节返回

核心驱动逻辑

// 读取状态寄存器
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();
}

注意:Flash 不能写覆盖——必须先擦除再写。最小擦除单位是扇区(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(SRAM 实验用)
NE4 FSMC_NE4 0x6C000000~0x6FFFFFFF(LCD 实验用)

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 | 总线周转时间 |


项目 39:FSMC 扩展 SRAM(寄存器版)

使用 FSMC Bank1 子区 3(NE3,片选 PG10),地址范围 0x68000000~0x6BFFFFFF,16 位数据总线连接外部 SRAM。

项目路径上部-基础篇\03_代码\stm32\39_fsmc_sram_register

fsmc.h

文件Hardware/FSMC/fsmc.h

#ifndef __FSMC_H
#define __FSMC_H

#include "stm32f10x.h"

void FSMC_Init(void);

#endif

fsmc.c

文件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寄存器配置 — BCR3(BTCR[4])
    // 存储区使能
    FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MBKEN;
    // 存储器类型:SRAM(MTYP = 00)
    FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MTYP;
    // 禁止Flash访问
    FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_FACCEN;
    // 禁止地址数据复用
    FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MUXEN;
    // 总线宽度:16位(MWID = 01)
    FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MWID_1;
    FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MWID_0;
    // 使能写操作
    FSMC_Bank1->BTCR[4] |= FSMC_BCR3_WREN;

    // 4. 时序配置 — BTR3(BTCR[5])
    FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_ADDSET;
    FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_DATAST;
    FSMC_Bank1->BTCR[5] |= (71 << 8);  // DATAST = 71
}

void FSMC_GPIO_Init(void)
{
    // 地址线 A0~A18:PF0~PF5, PF12~PF15, PG0~PG5, PD11~PD13
    // MODE = 11(50MHz),CNF = 10(复用推挽输出)
    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);

    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);
    // ...(其余地址线引脚配置类似,CNF=10,MODE=11)

    // 数据线 D0~D15:PD0~PD1, PD8~PD10, PD14~PD15, PE7~PE15
    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);
    // ...

    // 控制线
    // 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 - NBL0, NBL1(字节掩码)
    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);
}

main.c

文件User/main.c

#include "usart.h"
#include "fsmc.h"

// 方法1:使用 __attribute__((at())) 指定全局变量的地址
uint8_t v1 __attribute__((at(0x68000000)));
uint8_t v2 __attribute__((at(0x68000004)));
uint16_t v3 = 30;

int main(void)
{
    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 *)0x68000001;
    *p = 100;
    printf("*p = %d, @%p\n", *p, p);

    while (1) {}
}

配置完成后,FSMC 区域3 的地址为 0x68000000。写:*(uint16_t *)0x68000000 = data; 读:data = *(uint16_t *)0x68000000;


项目 40:FSMC 扩展 SRAM(HAL 库版)

使用 CubeMX 生成 FSMC 初始化代码,HAL 库管理 SRAM 配置。同样使用 Bank3(NE3),16 位模式。

项目路径上部-基础篇\03_代码\stm32\40_fsmc_sram_hal

CubeMX 生成:fsmc.c

文件Core/Src/fsmc.c

SRAM_HandleTypeDef hsram1;

void MX_FSMC_Init(void)
{
    FSMC_NORSRAM_TimingTypeDef Timing = {0};

    hsram1.Instance = FSMC_NORSRAM_DEVICE;
    hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
    hsram1.Init.NSBank = FSMC_NORSRAM_BANK3;
    hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
    hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM;
    hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
    hsram1.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
    hsram1.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
    hsram1.Init.WrapMode = FSMC_WRAP_MODE_DISABLE;
    hsram1.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
    hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
    hsram1.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
    hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
    hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
    hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE;

    Timing.AddressSetupTime = 15;
    Timing.AddressHoldTime = 15;
    Timing.DataSetupTime = 71;
    Timing.BusTurnAroundDuration = 15;
    Timing.CLKDivision = 16;
    Timing.DataLatency = 17;
    Timing.AccessMode = FSMC_ACCESS_MODE_A;

    if (HAL_SRAM_Init(&hsram1, &Timing, NULL) != HAL_OK)
    {
        Error_Handler();
    }

    __HAL_AFIO_FSMCNADV_DISCONNECTED();
}

HAL_FSMC_MspInit(GPIO 配置,由 CubeMX 生成)

static void HAL_FSMC_MspInit(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (FSMC_Initialized) return;
    FSMC_Initialized = 1;

    __HAL_RCC_FSMC_CLK_ENABLE();

    // PF0~PF5, PF12~PF15 → FSMC_A0~A9
    GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
                          |GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_12|GPIO_PIN_13
                          |GPIO_PIN_14|GPIO_PIN_15;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);

    // PG0~PG5, PG10 → FSMC_A10~A15, NE3
    GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
                          |GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

    // PE0~PE1(NBL), PE7~PE15(D4~D12)
    GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10
                          |GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14
                          |GPIO_PIN_15|GPIO_PIN_0|GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

    // PD0~PD1(D2~D3), PD4(NOE), PD5(NWE), PD8~PD10(D13~D15),
    // PD11~PD13(A16~A18), PD14~PD15(D0~D1)
    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
                          |GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15
                          |GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_4|GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}

main.c

文件Core/Src/main.c

#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "fsmc.h"

// 方法1:__attribute__((at())) 指定全局变量地址
uint8_t v1 __attribute__((at(0x68000000)));
uint8_t v2 __attribute__((at(0x68000004)));
uint16_t v3 = 30;

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_FSMC_Init();
    MX_USART1_UART_Init();

    printf("中国芯FSMC实验...\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);

    uint8_t *p = (uint8_t *)0x68000001;
    *p = 100;
    printf("*p = %d, @%p\n", *p, p);

    while (1) {}
}

项目 41:FSMC + LCD(寄存器版)

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

  • *LCD_ADDR_CMD = cmd → A10=0 → RS=0 → 写命令
  • *LCD_ADDR_DATA = data → A10=1 → RS=1 → 写数据

使用 Bank1 子区 4(NE4,片选 PG12),地址基址 0x6C000000。

项目路径上部-基础篇\03_代码\stm32\41_lcd_register

该项目的 FSMC 驱动在 Hardware/FSMC/,LCD 驱动在 Interface/LCD/(不同于其他项目的 Hardware/ 目录结构)。

fsmc.c(LCD 专用 FSMC 配置)

文件Hardware/FSMC/fsmc.c(区别于 SRAM 版本,使用 BCR4/BTCR[6] 和 NE4)

#include "fsmc.h"

void FSMC_GPIO_Init(void);

void FSMC_Init(void)
{
    // 1. 开启时钟(增加 GPIOB 用于背光)
    RCC->AHBENR |= RCC_AHBENR_FSMCEN;
    RCC->APB2ENR |= (RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPDEN |
                     RCC_APB2ENR_IOPEEN | RCC_APB2ENR_IOPFEN |
                     RCC_APB2ENR_IOPGEN);

    FSMC_GPIO_Init();

    // 3. FSMC BCR4 — BTCR[6]
    FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MBKEN;
    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MTYP;      // SRAM类型
    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_FACCEN;     // 禁止Flash访问
    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MWID_1;
    FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MWID_0;      // 16位
    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MUXEN;      // 非复用
    FSMC_Bank1->BTCR[6] |= FSMC_BCR4_WREN;        // 写使能

    // 4. FSMC BTR4 — BTCR[7]
    FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_ADDSET;
    FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_DATAST;
    FSMC_Bank1->BTCR[7] |= (71 << 8);             // DATAST = 71
}

void FSMC_GPIO_Init(void)
{
    // 地址线:只用了 A10(PG0)
    GPIOG->CRL |= GPIO_CRL_MODE0;
    GPIOG->CRL |= GPIO_CRL_CNF0_1;
    GPIOG->CRL &= ~GPIO_CRL_CNF0_0;

    // 数据线 D0~D15:PD0~PD1, PD8~PD10, PD14~PD15, PE7~PE15
    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);
    // ...

    // 控制线: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);

    // PG12 — NE4(片选)
    GPIOG->CRH |= GPIO_CRH_MODE12;
    GPIOG->CRH |= GPIO_CRH_CNF12_1;
    GPIOG->CRH &= ~GPIO_CRH_CNF12_0;

    // PG15 — LCD复位(通用推挽输出)
    GPIOG->CRH |= GPIO_CRH_MODE15;
    GPIOG->CRH &= ~GPIO_CRH_CNF15;

    // PB0 — 背光控制(通用推挽输出)
    GPIOB->CRL |= GPIO_CRL_MODE0;
    GPIOB->CRL &= ~GPIO_CRL_CNF0;
}

lcd.h

文件Interface/LCD/lcd.h

#ifndef __LCD_H
#define __LCD_H

#include "fsmc.h"
#include "delay.h"
#include <math.h>

// 命令/数据地址(A10 区分)
#define SRAM_BANK1_4   0x6C000000
#define LCD_ADDR_CMD   (uint16_t *)SRAM_BANK1_4
#define LCD_ADDR_DATA  (uint16_t *)(SRAM_BANK1_4 + (1 << 11))
// A10=0 → 命令,A10=1 → 数据(地址偏移 0x800)

#define LCD_W  320
#define LCD_H  480

/* 常用颜色 */
#define WHITE   0xFFFF
#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define YELLOW  0xFFE0
#define GRAY    0x8430
// ... 其他颜色

// 基础操作
void LCD_Init(void);
void LCD_Reset(void);
void LCD_BGOn(void);
void LCD_BGOff(void);
void LCD_RegConfig(void);
void LCD_WriteCmd(uint16_t cmd);
void LCD_WriteData(uint16_t data);
uint16_t LCD_ReadData(void);

// 绘图功能
uint32_t LCD_ReadID(void);
void LCD_ClearAll(uint16_t color);
void LCD_SetArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void LCD_WriteAsciiChar(uint16_t x, uint16_t y, uint16_t height,
                        uint8_t c, uint16_t fColor, uint16_t bColor);
void LCD_WriteAsciiString(uint16_t x, uint16_t y, uint16_t height,
                          uint8_t *str, uint16_t fColor, uint16_t bColor);
void LCD_WriteChineseChar(uint16_t x, uint16_t y, uint16_t height,
                          uint8_t index, uint16_t fColor, uint16_t bColor);
void LCD_DisplayAtguiguLogo(uint16_t x, uint16_t y);
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t w, uint16_t color);
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
                  uint16_t w, uint16_t color);
void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
                       uint16_t w, uint16_t color);
void LCD_DrawCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                    uint16_t w, uint16_t color);
void LCD_DrawCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                        uint16_t w, uint16_t color);
void LCD_DrawFilledCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                          uint16_t w, uint16_t bColor, uint16_t fColor);
void LCD_DrawFilledCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                              uint16_t w, uint16_t bColor, uint16_t fColor);

#endif

lcd.c(核心函数)

文件Interface/LCD/lcd.c

#include "lcd.h"
#include "lcd_font.h"

// 初始化调用 FSMC_Init,然后复位、亮背光、写寄存器
void LCD_Init(void)
{
    FSMC_Init();
    LCD_Reset();
    LCD_BGOn();
    LCD_RegConfig();
}

// 复位:PG15 拉低100ms再拉高
void LCD_Reset(void)
{
    GPIOG->ODR &= ~GPIO_ODR_ODR15;
    Delay_ms(100);
    GPIOG->ODR |= GPIO_ODR_ODR15;
    Delay_ms(100);
}

// 背光:PB0
void LCD_BGOn(void)  { GPIOB->ODR |= GPIO_ODR_ODR0; }
void LCD_BGOff(void) { GPIOB->ODR &= ~GPIO_ODR_ODR0; }

// 寄存器初始化序列(ILI9341 兼容)
void LCD_RegConfig(void)
{
    /* 1. 正极伽马校正 */
    LCD_WriteCmd(0xE0);
    LCD_WriteData(0x00); LCD_WriteData(0x07); LCD_WriteData(0x10);
    LCD_WriteData(0x09); LCD_WriteData(0x17); LCD_WriteData(0x0B);
    LCD_WriteData(0x41); LCD_WriteData(0x89); LCD_WriteData(0x4B);
    LCD_WriteData(0x0A); LCD_WriteData(0x0C); LCD_WriteData(0x0E);
    LCD_WriteData(0x18); LCD_WriteData(0x1B); LCD_WriteData(0x0F);

    /* 2. 负极伽马校正 */
    LCD_WriteCmd(0XE1);
    LCD_WriteData(0x00); LCD_WriteData(0x17); LCD_WriteData(0x1A);
    LCD_WriteData(0x04); LCD_WriteData(0x0E); LCD_WriteData(0x06);
    LCD_WriteData(0x2F); LCD_WriteData(0x45); LCD_WriteData(0x43);
    LCD_WriteData(0x02); LCD_WriteData(0x0A); LCD_WriteData(0x09);
    LCD_WriteData(0x32); LCD_WriteData(0x36); LCD_WriteData(0x0F);

    /* 4. 电源控制1 */
    LCD_WriteCmd(0xC0);
    LCD_WriteData(0x11); LCD_WriteData(0x09);

    /* 5. 电源控制2 */
    LCD_WriteCmd(0xC1);
    LCD_WriteData(0x02); LCD_WriteData(0x03);

    /* 6. VCOM控制 */
    LCD_WriteCmd(0XC5);
    LCD_WriteData(0x00); LCD_WriteData(0x0A); LCD_WriteData(0x80);

    /* 7. 帧率控制 */
    LCD_WriteCmd(0xB1);
    LCD_WriteData(0xB0); LCD_WriteData(0x11);

    /* 12. 像素格式:16位 */
    LCD_WriteCmd(0x3A);
    LCD_WriteData(0x55);

    /* 13. 退出睡眠 */
    LCD_WriteCmd(0x11);
    Delay_ms(120);

    /* 14. 显示开启 */
    LCD_WriteCmd(0x29);
}

// 命令/数据访问
void LCD_WriteCmd(uint16_t cmd)  { *LCD_ADDR_CMD = cmd; }
void LCD_WriteData(uint16_t data) { *LCD_ADDR_DATA = data; }
uint16_t LCD_ReadData(void)      { return *LCD_ADDR_DATA; }

// 读 LCD 控制器 ID
uint32_t LCD_ReadID(void)
{
    LCD_WriteCmd(0x04);
    LCD_ReadData();  // 丢弃第一个无效字节
    uint32_t id = 0;
    id |= (LCD_ReadData() & 0xff) << 16;
    id |= (LCD_ReadData() & 0xff) << 8;
    id |= (LCD_ReadData() & 0xff);
    return id;
}

// 设置读写窗口
void LCD_SetArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
    LCD_WriteCmd(0x2a);
    LCD_WriteData(x >> 8 & 0xff);  LCD_WriteData(x & 0xff);
    LCD_WriteData((x + w - 1) >> 8 & 0xff);
    LCD_WriteData((x + w - 1) & 0xff);

    LCD_WriteCmd(0x2b);
    LCD_WriteData(y >> 8 & 0xff);  LCD_WriteData(y & 0xff);
    LCD_WriteData((y + h - 1) >> 8 & 0xff);
    LCD_WriteData((y + h - 1) & 0xff);
}

// 全屏清空为指定颜色
void LCD_ClearAll(uint16_t color)
{
    LCD_SetArea(0, 0, LCD_W, LCD_H);
    LCD_WriteCmd(0x2c);
    for (uint32_t i = 0; i < LCD_W * LCD_H; i++) {
        LCD_WriteData(color);
    }
}

// ASCII 字符显示(支持 12/16/24/32 高度)
void LCD_WriteAsciiChar(uint16_t x, uint16_t y, uint16_t height,
                        uint8_t c, uint16_t fColor, uint16_t bColor)
{
    LCD_SetArea(x, y, height / 2, height);
    LCD_WriteCmd(0x2C);

    uint8_t index = c - ' ';

    if (height == 16 || height == 12) {
        for (uint8_t i = 0; i < height; i++) {
            uint8_t tempByte = (height == 16)
                ? ascii_1608[index][i] : ascii_1206[index][i];
            for (uint8_t j = 0; j < height / 2; j++) {
                LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
                tempByte >>= 1;
            }
        }
    } else if (height == 24) {
        for (uint8_t i = 0; i < height * 2; i++) {
            uint8_t tempByte = ascii_2412[index][i];
            uint8_t jCount = (i % 2) ? 4 : 8;
            for (uint8_t j = 0; j < jCount; j++) {
                LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
                tempByte >>= 1;
            }
        }
    } else if (height == 32) {
        for (uint8_t i = 0; i < height * 2; i++) {
            uint8_t tempByte = ascii_3216[index][i];
            for (uint8_t j = 0; j < 8; j++) {
                LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
                tempByte >>= 1;
            }
        }
    }
}

// 字符串(支持 \n 换行和自动换行)
void LCD_WriteAsciiString(uint16_t x, uint16_t y, uint16_t height,
                          uint8_t *str, uint16_t fColor, uint16_t bColor)
{
    uint8_t i = 0;
    while (str[i] != '\0') {
        if (str[i] != '\n') {
            if (x + height / 2 > LCD_W) { x = 0; y += height; }
            LCD_WriteAsciiChar(x, y, height, str[i], fColor, bColor);
            x += height / 2;
        } else {
            x = 0; y += height;
        }
        i++;
    }
}

// 显示汉字(使用 chinese[][128] 字库)
void LCD_WriteChineseChar(uint16_t x, uint16_t y, uint16_t height,
                          uint8_t index, uint16_t fColor, uint16_t bColor)
{
    LCD_SetArea(x, y, height, height);
    LCD_WriteCmd(0x2C);
    for (uint8_t i = 0; i < 128; i++) {
        uint8_t tempByte = chinese[index][i];
        for (uint8_t j = 0; j < 8; j++) {
            LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
            tempByte >>= 1;
        }
    }
}

// 显示 Logo(使用 gImage_logo 数组)
void LCD_DisplayAtguiguLogo(uint16_t x, uint16_t y)
{
    LCD_SetArea(x, y, 227, 68);
    LCD_WriteCmd(0x2C);
    uint16_t len = sizeof(gImage_logo);
    for (uint16_t i = 0; i < len; i += 2) {
        uint16_t p = gImage_logo[i] + (gImage_logo[i + 1] << 8);
        LCD_WriteData(p);
    }
}

// 画点
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t w, uint16_t color)
{
    LCD_SetArea(x, y, w, w);
    LCD_WriteCmd(0x2C);
    for (uint16_t i = 0; i < w * w; i++) LCD_WriteData(color);
}

// 画线(Bresenham 近似:y = kx + b)
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
                  uint16_t w, uint16_t color)
{
    if (x1 == x2) {
        for (uint16_t y = y1; y <= y2; y++) LCD_DrawPoint(x1, y, w, color);
        return;
    }
    double k = 1.0 * (y1 - y2) / (x1 - x2);
    double b = y1 - k * x1;
    for (uint16_t x = x1; x <= x2; x++) {
        uint16_t y = (uint16_t)(k * x + b);
        LCD_DrawPoint(x, y, w, color);
    }
}

// 矩形
void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
                       uint16_t w, uint16_t color)
{
    LCD_DrawLine(x1, y1, x2, y1, w, color);
    LCD_DrawLine(x2, y1, x2, y2, w, color);
    LCD_DrawLine(x1, y1, x1, y2, w, color);
    LCD_DrawLine(x1, y2, x2, y2, w, color);
}

// 画圆(参数方程,逐点)
void LCD_DrawCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                    uint16_t w, uint16_t color)
{
    for (uint16_t theta = 0; theta < 360; theta++) {
        uint16_t x = xCenter + r * cos(3.14 * theta / 180);
        uint16_t y = yCenter + r * sin(3.14 * theta / 180);
        LCD_DrawPoint(x, y, w, color);
    }
}

// 画圆优化版(同时画 4 象限)
void LCD_DrawCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                        uint16_t w, uint16_t color)
{
    for (uint16_t theta = 0; theta <= 90; theta++) {
        uint16_t dx = r * cos(3.14 * theta / 180);
        uint16_t dy = r * sin(3.14 * theta / 180);
        LCD_DrawPoint(xCenter + dx, yCenter + dy, w, color);
        LCD_DrawPoint(xCenter - dx, yCenter + dy, w, color);
        LCD_DrawPoint(xCenter - dx, yCenter - dy, w, color);
        LCD_DrawPoint(xCenter + dx, yCenter - dy, w, color);
    }
}

// 实心圆
void LCD_DrawFilledCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                          uint16_t w, uint16_t bColor, uint16_t fColor)
{
    for (uint16_t i = 0; i <= r; i++) {
        for (uint16_t theta = 0; theta < 360; theta++) {
            uint16_t x = xCenter + i * cos(3.14 * theta / 180);
            uint16_t y = yCenter + i * sin(3.14 * theta / 180);
            LCD_DrawPoint(x, y, w, (i == r) ? fColor : bColor);
        }
    }
}

// 实心圆优化版(画填充直线代替逐点扫描)
void LCD_DrawFilledCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
                              uint16_t w, uint16_t bColor, uint16_t fColor)
{
    for (uint16_t theta = 0; theta <= 90; theta++) {
        uint16_t dx = r * cos(3.14 * theta / 180);
        uint16_t dy = r * sin(3.14 * theta / 180);

        uint16_t x1 = xCenter + dx, y1 = yCenter + dy;
        uint16_t x2 = xCenter - dx, y2 = yCenter + dy;
        LCD_DrawPoint(x1, y1, w, fColor);
        LCD_DrawPoint(x2, y2, w, fColor);
        LCD_DrawLine(x2 + w, y2, x1 - w, y1, w, bColor);

        x2 = xCenter - dx; y2 = yCenter - dy;
        x1 = xCenter + dx; y1 = yCenter - dy;
        LCD_DrawPoint(x2, y2, w, fColor);
        LCD_DrawPoint(x1, y1, w, fColor);
        LCD_DrawLine(x2 + w, y2, x1 - w, y1, w, bColor);
    }
}

main.c

文件User/main.c

#include "usart.h"
#include "lcd.h"

int main(void)
{
    USART_Init();
    LCD_Init();

    printf("中国芯LCD实验开始...\n");

    uint32_t id = LCD_ReadID();
    printf("id = %#x\n", id);

    // 全屏白色
    LCD_ClearAll(WHITE);

    // 显示 ASCII 字符
    LCD_WriteAsciiChar(10, 10, 16, 'A', WHITE, RED);
    LCD_WriteAsciiChar(10, 30, 24, 'A', WHITE, RED);
    LCD_WriteAsciiChar(20, 60, 32, 'A', BLUE, WHITE);
    LCD_WriteAsciiChar(20, 100, 12, 'B', BLUE, YELLOW);

    // 显示字符串
    LCD_WriteAsciiString(200, 200, 24,
        "Hello\natguigu!\nHello, world!at\nguigu", BLACK, WHITE);

    // 显示汉字
    LCD_WriteChineseChar(20, 330, 32, 0, RED, BLUE);
    LCD_WriteChineseChar(20, 362, 32, 1, BLUE, RED);
    LCD_WriteChineseChar(20, 394, 32, 2, GRAY, RED);

    // 显示 Logo
    LCD_DisplayAtguiguLogo(57, 100);

    // 几何图形
    LCD_DrawPoint(300, 300, 5, RED);
    LCD_DrawLine(10, 10, 10, 300, 5, RED);
    LCD_DrawLine(10, 10, 300, 180, 3, BLUE);
    LCD_DrawRectangle(20, 20, 300, 300, 5, RED);
    LCD_DrawCircle_Pro(160, 240, 100, 5, BLUE);
    LCD_DrawFilledCircle_Pro(240, 400, 50, 3, BLUE, RED);

    while (1) {}
}

LCD 地址映射原理:NE4 基址 = 0x6C000000,A10 连接 LCD 的 RS 引脚。 当 CPU 访问 0x6C000000 时,A10=0 → RS=0 → 命令模式。 当 CPU 访问 0x6C000000 + 0x800(即 A10=1)时 → RS=1 → 数据模式。


项目 42:FSMC + LCD(HAL 库版)

CubeMX 生成 FSMC 初始化代码,使用 Bank4(NE4),LCD 驱动逻辑与寄存器版相同。

项目路径上部-基础篇\03_代码\stm32\42_lcd_hal

CubeMX 生成:fsmc.c

文件Core/Src/fsmc.c

SRAM_HandleTypeDef hsram1;

void MX_FSMC_Init(void)
{
    FSMC_NORSRAM_TimingTypeDef Timing = {0};

    hsram1.Instance = FSMC_NORSRAM_DEVICE;
    hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
    hsram1.Init.NSBank = FSMC_NORSRAM_BANK4;       // NE4
    hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
    hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM;
    hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
    hsram1.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
    hsram1.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
    hsram1.Init.WrapMode = FSMC_WRAP_MODE_DISABLE;
    hsram1.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
    hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
    hsram1.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
    hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
    hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
    hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE;

    Timing.AddressSetupTime = 15;
    Timing.AddressHoldTime = 15;
    Timing.DataSetupTime = 71;
    Timing.BusTurnAroundDuration = 15;
    Timing.CLKDivision = 16;
    Timing.DataLatency = 17;
    Timing.AccessMode = FSMC_ACCESS_MODE_A;

    if (HAL_SRAM_Init(&hsram1, &Timing, NULL) != HAL_OK)
        Error_Handler();

    __HAL_AFIO_FSMCNADV_DISCONNECTED();
}

HAL_FSMC_MspInit(LCD 版,NE4)

static void HAL_FSMC_MspInit(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (FSMC_Initialized) return;
    FSMC_Initialized = 1;

    __HAL_RCC_FSMC_CLK_ENABLE();

    // PG0(A10), PG12(NE4)
    GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_12;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

    // PE7~PE15(D4~D12)
    GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10
                          |GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14
                          |GPIO_PIN_15;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

    // PD0(D2), PD1(D3), PD4(NOE), PD5(NWE), PD8~PD10(D13~D15),
    // PD14(D0), PD15(D1)
    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_14
                          |GPIO_PIN_15|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_4
                          |GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}

lcd.h 与 lcd.c

与项目 41 的 LCD 驱动代码几乎相同,关键差异:

  • LCD_Init() 调用 MX_FSMC_Init() 而非 FSMC_Init()
  • LCD_Reset() 使用 HAL_Delay() 而非 Delay_ms()
  • LCD_RegConfig() 使用 HAL_Delay(120) 而非 Delay_ms(120)

main.c

文件Core/Src/main.c

#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "fsmc.h"
#include "lcd.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_FSMC_Init();
    MX_USART1_UART_Init();

    LCD_Init();

    printf("中国芯LCD实验开始...\n");

    uint32_t id = LCD_ReadID();
    printf("id = %#x\n", id);

    LCD_ClearAll(WHITE);

    LCD_WriteAsciiChar(10, 10, 16, 'A', WHITE, RED);
    LCD_WriteAsciiChar(10, 30, 24, 'B', RED, WHITE);
    LCD_WriteAsciiChar(10, 60, 32, 'R', BLUE, YELLOW);

    LCD_WriteAsciiString(200, 200, 24,
        "Hello, Atguigu! Hello, hello! At\nguigu! Hello, wolrd!",
        BLACK, WHITE);

    LCD_WriteChineseChar(20, 330, 32, 0, RED, BLUE);
    LCD_WriteChineseChar(20, 362, 32, 1, BLUE, RED);
    LCD_WriteChineseChar(20, 394, 32, 2, GRAY, RED);

    LCD_DisplayAtguiguLogo(50, 100);

    LCD_DrawPoint(300, 300, 5, RED);
    LCD_DrawLine(10, 10, 10, 300, 5, RED);
    LCD_DrawLine(10, 20, 300, 100, 3, BLUE);
    LCD_DrawRectangle(15, 15, 280, 55, 3, RED);
    LCD_DrawCircle(150, 400, 50, 3, BLUE);
    LCD_DrawFilledCircle_Pro(260, 400, 50, 3, BLUE, RED);

    while (1) {}
}

核心速查表

SPI 操作 软件模拟(36) 硬件寄存器(37) HAL 库(38)
SCK/MOSI 配置 通用推挽输出 复用推挽输出 GPIO_MODE_AF_PP
MISO 配置 浮空输入 浮空输入 GPIO_MODE_INPUT
初始化 GPIO 位操作 + 延时 CR1 \|= MSTR+SSM+SSI+SPE MX_SPI1_Init()
发 1 字节 8 次 GPIO 位操作 DR=byte; while(!RXNE); val=DR HAL_SPI_Transmit()
同时收发 循环 8 次读写 DR=byte; val=DR HAL_SPI_TransmitReceive()
片选 CS GPIO 位操作 GPIO 位操作 HAL_GPIO_WritePin()
适用场景 任意引脚/低速 固定引脚/高速 跨平台/快速开发
FSMC 操作 寄存器版(39/41) HAL 库版(40/42)
SRAM 片选 Bank3 (NE3, PG10) FSMC_NORSRAM_BANK3
LCD 片选 Bank4 (NE4, PG12) FSMC_NORSRAM_BANK4
命令/数据区分 A10 地址线:偏移 0x800 同左
写 SRAM *(uint16_t *)0x68000000 = data 同左
读 SRAM data = *(uint16_t *)0x68000000 同左
写 LCD 命令 *LCD_ADDR_CMD = cmd 同左
写 LCD 数据 *LCD_ADDR_DATA = data 同左
初始化 手动配置寄存器 MX_FSMC_Init() + HAL_SRAM_Init()

各项目引脚分配

信号 36/37/38 (SPI) 39/40 (SRAM) 41/42 (LCD)
SPI_SCK PA5
SPI_MOSI PA7
SPI_MISO PA6
SPI_CS PC13
FSMC_NE PG10 (NE3) PG12 (NE4)
FSMC_A10 PG0 (RS)
LCD_RST PG15
LCD_BL PB0
FSMC_NOE PD4 PD4
FSMC_NWE PD5 PD5

常见问题与避坑

  1. SPI 接收数据为 0xFF → MISO 连接断开、从机未选中(CS 拉低后才有输出)、从机忙于内部操作
  2. W25Q64 写入失败 → 每次写操作前必须发 WREN(0x06);扇区必须事先擦除(Flash 不能写覆盖)
  3. FSMC 读写时序不对 → 查 SRAM/LCD 数据手册的时序参数(ADDSET 和 DATAST),STM32 的 HCLK 对应关系。DATAST 的计算:DATAST = (T_access / T_HCLK) - ADDSET。LCD 通常比 SRAM 慢,可适当增大 DATAST
  4. 软件 SPI 速度太慢 → 软件 SPI 受限于 GPIO 翻转速度(约 2~4MHz),大批量数据建议用硬件 SPI
  5. NSS 软件管理模式 → 多从机时必须用 SSM=1 软件管理 NSS,否则硬件自动管理可能产生冲突
  6. LCD 显示异常(花屏/白屏) → 检查 FSMC 时序(LCD 控制器通常需要更长的 DATAST);检查复位引脚 PG15 和背光引脚 PB0 的 GPIO 配置;确认 LCD_ADDR_DATA 的地址偏移量(A10 连接 RS,偏移量 = 1<<10 = 0x400,但实际使用 1<<11 = 0x800,需核对硬件连接)
  7. HAL 库 FSMC 初始化失败 → 确认 CubeMX 中 FSMC 的 Bank 选择与硬件一致(SRAM 选 NE3,LCD 选 NE4);NADV 必须断开(__HAL_AFIO_FSMCNADV_DISCONNECTED()