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 占位)。
使用 GPIO 位操作模拟 SPI 时序,纯软件实现,不依赖硬件 SPI 外设。
项目路径:上部-基础篇\03_代码\stm32\36_spi_software_register
文件: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
文件: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;
}
文件: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 引脚被占用的场景。
使用 STM32 片内 SPI 外设,硬件自动移位,效率更高。CS 片选仍用 GPIO(PC13)独立控制,SCK/MOSI 配置为复用推挽输出。
项目路径:上部-基础篇\03_代码\stm32\37_spi_hardware_register
文件: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
文件: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 控制)。
与项目 36 的 main.c 完全相同(W25Q32_* 函数适配 SPI_SwapByte 接口即可)。
使用 STM32CubeMX 生成初始化代码,HAL 库封装 SPI 外设。CS 片选仍由 GPIO 独立控制,CubeMX 中配置 PC13 为普通输出。
项目路径:上部-基础篇\03_代码\stm32\38_spi_hardware_hal
文件: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);
}
}
文件: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);
}
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;
}
文件: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 |
|---|---|---|
| 容量 | 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 = 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_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 | 总线周转时间 |
使用 FSMC Bank1 子区 3(NE3,片选 PG10),地址范围 0x68000000~0x6BFFFFFF,16 位数据总线连接外部 SRAM。
项目路径:上部-基础篇\03_代码\stm32\39_fsmc_sram_register
文件:Hardware/FSMC/fsmc.h
#ifndef __FSMC_H
#define __FSMC_H
#include "stm32f10x.h"
void FSMC_Init(void);
#endif
文件: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);
}
文件: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;
使用 CubeMX 生成 FSMC 初始化代码,HAL 库管理 SRAM 配置。同样使用 Bank3(NE3),16 位模式。
项目路径:上部-基础篇\03_代码\stm32\40_fsmc_sram_hal
文件: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();
}
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);
}
文件: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) {}
}
将 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/目录结构)。
文件: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;
}
文件: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
文件: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);
}
}
文件: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 → 数据模式。
CubeMX 生成 FSMC 初始化代码,使用 Bank4(NE4),LCD 驱动逻辑与寄存器版相同。
项目路径:上部-基础篇\03_代码\stm32\42_lcd_hal
文件: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();
}
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);
}
与项目 41 的 LCD 驱动代码几乎相同,关键差异:
LCD_Init() 调用 MX_FSMC_Init() 而非 FSMC_Init()LCD_Reset() 使用 HAL_Delay() 而非 Delay_ms()LCD_RegConfig() 使用 HAL_Delay(120) 而非 Delay_ms(120)文件: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 |
DATAST = (T_access / T_HCLK) - ADDSET。LCD 通常比 SRAM 慢,可适当增大 DATAST__HAL_AFIO_FSMCNADV_DISCONNECTED())