--- 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` ```c #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` ```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` ```c #include "usart.h" #include "w25q32.h" #include 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` ```c #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` ```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` ```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` ```c #define CS_Pin GPIO_PIN_13 #define CS_GPIO_Port GPIOC ``` **文件**:`Core/Src/gpio.c` ```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 段) ```c 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` ```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字节返回 | ### 核心驱动逻辑 ```c // 读取状态寄存器 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` ```c #ifndef __FSMC_H #define __FSMC_H #include "stm32f10x.h" void FSMC_Init(void); #endif ``` ### fsmc.c **文件**:`Hardware/FSMC/fsmc.c` ```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` ```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` ```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 生成) ```c 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` ```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) ```c #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` ```c #ifndef __LCD_H #define __LCD_H #include "fsmc.h" #include "delay.h" #include // 命令/数据地址(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` ```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` ```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` ```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) ```c 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` ```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()`)