简介:一份面向STM32F4系列开发者的FSMC(灵活静态存储控制器)应用示例代码,重点演示通过FSMC接口驱动LCD显示屏,解决外扩SRAM、NOR Flash以及屏幕连接时硬件接口与时序匹配的常见问题,也适合高校毕业生课程设计与入门嵌入式开发者。压缩包共664个文件,压缩后仅3.86MB,以C源码、H头文件为主,辅以链接脚本、启动文件、Keil工程配置及Hex固件,内容覆盖HAL库驱动与底层时序设置,便于直接对照工程进行二次开发。目前已有65人浏览学习。示例按照实际开发流程组织:先说明FSMC引脚连接与总线复用配置,再指导寄存器读写周期、片选信号等时序参数的设定;随后提供LCD上电初始化、画点画线、字符显示和图片输出等可复用驱动代码,并给出性能调优建议。掌握后可快速将STM32F4的FSMC接口用于人机交互界面或仪表显示项目,是一份可从寄存器到应用层闭环学习的参考代码。
1. STM32F4 FSMC 应用示例代码到底解决什么问题
一块板子既要外扩几十 KB 的 SRAM,又要顶一路并口屏,很多工程师第一反应是用 GPIO 手动模拟读写时序。结果跑起来发现,每刷一帧图像 CPU 都要花掉一大半时间去翻转电平,稍微加个动画就卡顿。STM32F4 FSMC 应用示例代码这类方案,就是把外部 SRAM、NOR Flash、并口 LCD 挂到芯片的内部存储总线上,访问外部器件就像访问数组一样直接。FSMC 全称是 Flexible Static Memory Controller,它的价值在于硬件自动产生片选、读使能、写使能等时序,软件只需要往映射地址写数据。适合正在做并口屏、外部存储扩展,或者想摆脱 GPIO 模拟时序的开发者。
2. 地址映射与外设选型:先搞懂 FSMC 在跟谁说话
2.1 三类外设能挂同一组总线:SRAM、NOR 与并口 LCD 的连接差异
FSMC 能接的外设类型看似很多,但按“访问方式”分只有三类:
外部 SRAM 最单纯。CPU 给一个地址,FSMC 自动拉低片选和读使能,SRAM 就把数据放到数据总线上,整个过程没有任何“命令”概念。访问它等于访问一块连续内存,memset、memcpy 直接能用。
NOR Flash 和 PSRAM 则多了一层命令周期。NOR 需要先向特定地址写入命令序列,比如进入读 ID 模式、擦除、编程,之后才能读写数据。所以你不能把 NOR 当作纯内存直接写,必须构造命令地址。
并口 LCD 又不太一样。液晶驱动芯片内部有寄存器堆,靠 RS 引脚区分“当前写的是寄存器地址还是数据”。FSMC 下最常见的做法是把 RS 接在一条地址线上,比如 FSMC_A0,那么 CPU 访问命令地址和数据地址只差一个 bit。
这三种器件的共同点是读写时序都由外部信号控制,FSMC 正好把这些信号全部接管。你不需要关心 NE、NOE、NWE 怎么翻转,只要把寄存器配置好,硬件会按照配置的时序去驱动总线。
2.2 Bank 与片选区间:NE1~NE4 的地址这样算
FSMC 对外提供多个片选区域,每个片选对应一个独立的地址窗口。STM32F4 上 Bank1 是 NOR/PSRAM/LCD 的天下,NAND 和 PC 卡在后面的 Bank 区域。绝大多数用 FSMC 接 SRAM 和并口 LCD 的工程,只用 Bank1。
| 片选信号 | 基地址 | 典型用途 |
|---|---|---|
| NE1 | 0x6000 0000 | SRAM、NOR、LCD,最常用 |
| NE2 | 0x6400 0000 | 第二片外部设备 |
| NE3 | 0x6800 0000 | 第三片外部设备 |
| NE4 | 0x6C00 0000 | 第四片外部设备 |
每个片选区域内部还有子区划分,不过对应用层来说,只需记住基地址。访问某个外部器件,就在基地址上加偏移。
这里有一个最容易绕晕的点:地址线错位。当 FSMC 配置为 16 位数据宽度时,内部地址总线 HADDR[25:1] 连接到外部地址线 FSMC_A[24:0],也就是说 HADDR 的最低位根本没有参与外部寻址。外部器件的地址 0x0001,在 CPU 这边看起来是 0x0002。
我一般这样换算:外部地址 = CPU 偏移地址 >> 1,CPU 偏移地址 = 外部地址 << 1。如果 FSMC 配置为 8 位数据宽度,则一一对应,不用移位。这个错位在接 NOR Flash 写命令地址时极其关键,后面我专门展开。
2.3 什么时候用 FSMC,什么时候用 SPI/QSPI
FSMC 不是万能的,它吃掉整组 GPIO。选型时我给三个判断条件:
第一,是否要连续、随机、高频地访问大块数据。并口屏刷图、音频缓冲、摄像头帧缓冲这类场景,FSMC 几乎绕不开。SPI 屏刷新率受限于串行时钟,像素时钟一旦超过 20MHz,SPI 就顶不住了。
第二,引脚余量是否足够。FSMC 至少占用数据线、地址线、控制线几十个引脚,小封装 MCU 接了 FSMC 之后,剩下的 IO 可能不够用。这时候宁可退回去用 SPI 加一个带缓存的屏控制器。
第三,有没有 XIP 需求。代码直接从外部 NOR 执行,FSMC 总线方式比 SPI 的 XIP 快很多,但 F4 的 FSMC 本身没有缓存,随机访问性能一般,适合低速代码场景。
| 对比项 | FSMC 并口方案 | SPI NOR 方案 |
|---|---|---|
| 带宽 | 并行总线,一次 8/16bit | 串行,受限于 SPI 时钟 |
| CPU 占用 | 硬件时序,极低 | 软件搬运,较高 |
| 引脚占用 | 20 个以上 | 4~6 个 |
| 适用场景 | 大容量 SRAM、并口屏 | 配置存储、OTA、日志 |
3. 写出可跑的 FSMC 初始化示例代码:GPIO、结构与时序参数
3.1 GPIO 复用与总线时钟:先把引脚交到 FSMC 手上
很多初始化跑不通,问题不在 FSMC 配置,而是 GPIO 没配好。FSMC 引脚必须配置为复用功能,并且要开启 FSMC 外设时钟。在 STM32F4 的标准库和 HAL 库中,FSMC 引脚复用号通常是 AF12,个别引脚存在差异,以数据手册为准。
static void FSMC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; /* 打开所有可能用到的 GPIO 端口时钟 */ __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOG_CLK_ENABLE(); __HAL_RCC_GPIOH_CLK_ENABLE(); __HAL_RCC_GPIOI_CLK_ENABLE(); /* 打开 FSMC 外设时钟 */ __HAL_RCC_FSMC_CLK_ENABLE(); GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = GPIO_AF12_FSMC; /* 把你原理图上所有 FSMC 网络引脚都列进去 */ GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | 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; HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); /* 其它端口按同样方式逐个初始化 */ HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); HAL_GPIO_Init(GPIOG, &GPIO_InitStruct); HAL_GPIO_Init(GPIOH, &GPIO_InitStruct); HAL_GPIO_Init(GPIOI, &GPIO_InitStruct); }这段代码的逻辑:先把端口时钟打开,再统一配置为复用推挽输出。Pull = GPIO_PULLUP是给总线一个确定的默认电平,防止引脚悬空,这个细节后面避坑章节会再讲。GPIO_AF12_FSMC在 STM32F4 系列中对应 FSMC 复用功能。如果你的开发库来自自动生成的初始化器,它会自动填好这个值。
实际接线中,数据线、地址线、控制线分散在不同端口,比如地址线在 GPIOD,数据线在 GPIOE,控制线在 GPIOF。我在代码里统一把所有相关端口都初始化了一遍,最坏情况是某些端口上没有 FSMC 引脚,多执行一次配置并不会产生副作用,但能减少漏配的概率。
3.2 一个最小 FSMC 初始化示例代码:SRAM 模式
下面这段是我在 HAL 库下最常用的 SRAM 初始化写法。整套配置适用于大部分外部 SRAM,也适用于并口 LCD 的存储器接口模式。
#include "stm32f4xx_hal.h" static SRAM_HandleTypeDef hsram; void MX_FSMC_Init(void) { FSMC_NORSRAM_TimingTypeDef Timing = {0}; FSMC_NORSRAM_InitTypeDef FSMC_Init = {0}; FSMC_GPIO_Init(); /* 时序参数,单位是 HCLK 周期 */ Timing.AddressSetupTime = 15; /* 地址建立时间 */ Timing.AddressHoldTime = 15; /* 地址保持时间 */ Timing.DataSetupTime = 15; /* 数据建立时间 */ Timing.BusTurnAroundDuration = 0; /* 总线转换间隔 */ Timing.CLKDivision = 0; /* 异步模式下用不到 */ Timing.DataLatency = 0; /* 异步模式下用不到 */ Timing.AccessMode = FSMC_ACCESS_MODE_A; FSMC_Init.FSMC_Bank = FSMC_Bank1_NORSRAM1; /* 对应 NE1 */ FSMC_Init.FSMC_DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; FSMC_Init.FSMC_MemoryType = FSMC_MEMORY_TYPE_SRAM; FSMC_Init.FSMC_MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; FSMC_Init.FSMC_BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; FSMC_Init.FSMC_AsyncWait = FSMC_ASYNWAIT_DISABLE; FSMC_Init.FSMC_WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; FSMC_Init.FSMC_WriteOperation = FSMC_WRITE_OPERATION_ENABLE; FSMC_Init.FSMC_WaitTiming = FSMC_WAIT_TIMING_BEFORE_WS; FSMC_Init.FSMC_ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; FSMC_Init.FSMC_WriteBurst = FSMC_WRITE_BURST_DISABLE; FSMC_Init.FSMC_ReadWriteTiming = &Timing; FSMC_Init.FSMC_WriteTiming = &Timing; if (HAL_SRAM_Init(&hsram, &FSMC_Init, &Timing) != HAL_OK) { Error_Handler(); } }这段代码的关键字段说明:
FSMC_Bank1_NORSRAM1对应 NE1 片选,基地址是 0x60000000。如果你的外部器件接在 NE2,改成FSMC_Bank1_NORSRAM2,基地址变成 0x64000000。
FSMC_MEMORY_TYPE_SRAM让控制器按 SRAM 时序工作。接并口 LCD 时,很多屏驱动芯片本身支持 SRAM 接口模式,这个保持 SRAM 类型即可。
FSMC_NORSRAM_MEM_BUS_WIDTH_16是总线宽度。宽度会影响地址错位,以及 8 位访问时字节选择信号 NBL0/NBL1 的行为。
FSMC_AccessMode有 A、B、C、D 四种模式,区别在于读和写周期的时序结构。SRAM 一般选 Mode A;NOR Flash 建议看器件手册中的时序图,很多参数需要选 Mode C 或 D 才能对齐。
3.3 三个必调时序参数:ADDSET、ADDHOLD、DATAST
FSMC 初始化里真正需要反复调整的是时序结构体里的三个字段。它们都以 HCLK 周期为单位,假设 HCLK 约 168MHz,一个周期大概 6ns。
AddressSetupTime表示地址信号在片选拉低之前就绪的时间。外部器件的数据手册一般写成 tAS,比如 SRAM 手册要求地址建立 20ns,那么按 6ns 一个周期计算,寄存器至少给 4 个周期。我习惯先给 10~15,跑通后再逐次减小。
AddressHoldTime是地址在周期结束后还要保持的时间。SRAM 对这个参数不太敏感,给个 15 不会翻车。NOR Flash 的命令周期访问中,这个参数影响较小,但会给写时序留出余量。
DataSetupTime是最关键的数据建立时间。数据必须在读使能有效期间稳定出现在总线上,这个时间不够,读回来的数据就是错的。遇到读回 0xFF 或者偶发错误,优先把它调大。
| 外设类型 | AddressSetupTime | AddressHoldTime | DataSetupTime |
|---|---|---|---|
| 普通 SRAM | 5~15 | 15 | 15~30 |
| NOR Flash 命令操作 | 10~20 | 15 | 30~50 |
| 并口 LCD(8080 接口) | 5~10 | 15 | 15~30 |
这几组是我常用的起步值,不是最优值。最优时序必须对着你自己的 HCLK 频率和器件数据手册算,或者用示波器观察实际波形。照抄别人工程里数值是最容易踩坑的做法,因为主频不同,相同的配置字对应完全不同的物理时间。
4. 用 FSMC 访问外部 SRAM 和并口屏:从验证代码到完整读写
4.1 把外部 SRAM 当数组用:一段自检代码帮你探路
初始化跑通后,第一步不是直接做业务逻辑,而是先做一个小范围的读写自检。这段代码能验证硬件连接、地址映射和时序配置是否整体成立。
#define EXT_SRAM_BASE 0x60000000U /* NE1 片选基地址 */ #define TEST_COUNT 1024U uint32_t test_sram(void) { volatile uint16_t *mem = (volatile uint16_t *)EXT_SRAM_BASE; uint32_t i; /* 写一遍 */ for (i = 0; i < TEST_COUNT; i++) { mem[i] = (uint16_t)(i ^ 0xA55A); } /* 读回来比对 */ for (i = 0; i < TEST_COUNT; i++) { if (mem[i] != (uint16_t)(i ^ 0xA55A)) { return i; /* 返回第一个出错的位置 */ } } return 0; /* 全部通过 */ }这段代码的逻辑:用一个volatile指针指向 FSMC 映射地址,写入带规律的数据,再逐字读回比对。volatile不能省,否则编译器可能把两次读操作合并优化,掩盖真实总线时序问题。
测试数据i ^ 0xA55A会形成高低字节都有变化的随机序列。如果只写 0x00 或 0xFF,可能掩盖数据线短路问题,比如某根线虚焊时 0xFF 读出来还是 0xFF。用异或数据能暴露单根数据线错误。
uint16_t而非uint8_t是因为初始化配置了 16 位数据宽度。用 8 位指针访问会触发字节选择信号,在大部分 SRAM 上能工作,但会把总线时序变复杂,自检阶段不值得冒这个风险。跑通后再按业务需要封装 8 位访问接口。
4.2 并口 LCD 的命令与数据访问:两个指针差一位,屏就点亮了
并口 LCD 通过 RS 引脚区分命令和数据。常见接法把 RS 连到 FSMC_A0,那么命令地址和数据地址只差一个地址位。在 16 位总线宽度下,FSMC_A0 对应 HADDR[1],所以两个地址相差 2 个字节。
#define LCD_BASE 0x60000000U #define LCD_CMD_ADDR (LCD_BASE + 0x00000000U) /* RS = 0 时写命令 */ #define LCD_DATA_ADDR (LCD_BASE + 0x00000002U) /* RS = 1 时写数据 */ static inline void lcd_write_cmd(uint16_t cmd) { *(volatile uint16_t *)LCD_CMD_ADDR = cmd; } static inline void lcd_write_data(uint16_t dat) { *(volatile uint16_t *)LCD_DATA_ADDR = dat; }LCD_CMD_ADDR和LCD_DATA_ADDR的差异源自 RS 接的地址线。如果 RS 接的是 FSMC_A0,在 16 位总线时差 0x2;如果接的是 FSMC_A1,地址差就变成 0x4。如果初始化时把数据宽度改为 8 位,地址差又变成 0x1。这是个需要反复核对的细节,经常有人照着例程抄地址,结果自己的 RS 接线不一样,导致整屏花掉。
访问方式上,LCD 控制器初始化流程通常是一串命令加参数的写入。用上面两个宏,写一个简单的颜色填充:
void lcd_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { /* 设置窗口区域,代码根据你的屏驱动寄存器定义 */ lcd_write_cmd(0x2A); lcd_write_data(x + w - 1); lcd_write_data(x); lcd_write_cmd(0x2B); lcd_write_data(y + h - 1); lcd_write_data(y); lcd_write_cmd(0x2C); for (uint32_t i = 0; i < (uint32_t)w * h; i++) { lcd_write_data(color); } }这段代码展示的是连续写入大批像素数据的过程。FSMC 的优势在这时候体现出来:每个lcd_write_data都是一次地址写入和数据写入,CPU 不需要额外翻转引脚,硬件自动完成完整的写周期。
4.3 NOR Flash 的命令周期:为什么 memcpy 写不进去
外部 NOR Flash 不能像 SRAM 一样直接memcpy。它需要先发命令序列进入编程模式,然后才能写数据,写完后还要等待器件内部编程完成。命令序列里的地址,正是前面 2.2 提到的地址错位最典型的应用场景。
以最常见的 NOR 命令序列为例,手册要求“向地址 0x555 写 0xAA,向地址 0x2AA 写 0x55”。这里的 0x555 和 0x2AA 是外部器件的地址。16 位 FSMC 总线访问时需要将外部地址左移一位,CPU 实际地址应该是 0xAAA 和 0x554。
#define NOR_BASE 0x60000000U static void nor_write_cmd(uint16_t cmd, uint32_t ext_addr) { /* 16 位总线时,外部地址要左移一位再访问 */ volatile uint16_t *reg = (volatile uint16_t *)(NOR_BASE + (ext_addr << 1)); *reg = cmd; } void nor_read_id(void) { /* 进入读 ID 模式,命令地址是 NOR 器件外部地址 */ nor_write_cmd(0xAA, 0x555); nor_write_cmd(0x55, 0x2AA); nor_write_cmd(0x90, 0x555); /* 读取厂商 ID 和器件 ID */ uint16_t mfr = *(volatile uint16_t *)(NOR_BASE + ((0x100) << 1)); uint16_t dev = *(volatile uint16_t *)(NOR_BASE + ((0x101) << 1)); /* 退出命令序列,恢复到读状态 */ nor_write_cmd(0xF0, 0x555); printf("manufacturer=0x%04X device=0x%04X\r\n", mfr, dev); }这段代码里的ext_addr << 1就是地址错位的补偿。外部器件把 0x555 作为命令地址,FSMC 把 HADDR 右移一位接到外部地址线,CPU 侧必须给 0xAAA。如果你直接写 0x555,NOR 器件收到的是 0x2AA,命令序列完全错乱,操作永远不会有反应。
不同厂商 NOR 器件在地址和数据命令字上有各自定义,进入读 ID 的地址可能不同。但“外部地址左移一位”这个规则不随器件变化,理解这一条,无论换什么 NOR、什么并口屏,都能自己推导出正确的 CPU 访问地址。
5. FSMC 调试避坑:五个高频翻车点的现象与根因
5.1 读回来全是 0xFF:DATAST 给得太短
现象:外部 SRAM 初始化通过,但读回数据全为 0xFF,偶尔写入大块数据后整体校验失败。
原因:DataSetupTime太短。FSMC 发出读使能后,SRAM 需要一定时间才能把数据放到总线上,这段时间小于数据手册要求的 tDOE,读周期结束前数据还没稳定。
解决:把Timing.DataSetupTime从 0 或 5 调大到 15 以上,重新测试。如果问题消失,再逐步减小,直到找到临界值。用示波器观察读周期里数据线的翻转时刻,可以精确确定安全余量。
5.2 16 位总线混用 8 位访问,数据像打乱了一样错位
现象:用uint8_t *指针访问外部 SRAM 或 NOR,写入 0x55,读出来却是 0x0055 或者数据整体左移一个字节。改用 16 位访问后一切正常。
原因:16 位数据宽度下,8 位访问依赖 NBL0/NBL1 字节选择信号。如果外部器件的字节选择引脚没接好,或者 FSMC 配置的字节访问模式与器件不匹配,就会出现半个字节有效、半个字节高阻的问题。
解决:优先统一用 16 位访问。业务层需要 8 位数组时,先用读写封装函数做字节拆分,不要直接靠 8 位指针。如果器件本身是 8 位的,把FSMC_NORSRAM_MEM_BUS_WIDTH_8配上,同时重新核对地址线错位规则。
5.3 并口 LCD 花屏:命令和数据地址差算错了
现象:LCD 初始化时写寄存器正常,但刷屏出现整体花屏,颜色错乱,甚至有时屏幕像被随机写入了命令。
原因:命令地址和数据地址的偏移与 RS 接线不匹配。RS 接 FSMC_A1 却按 FSMC_A0 的差 0x2 去访问,写命令和写数据实际落在了同一条地址线上,LCD 无法区分当前写入的是命令还是数据。这类问题看起来像是时序玄学,其实就是地址差错位。
解决:回到原理图确认 RS 接到哪根 FSMC 地址线。FSMC_A0 在 16 位总线对应 CPU 地址 +0x2,FSMC_A1 对应 +0x4,以此类推。用示波器看写命令和写数据时 RS 引脚电平是否翻转,能立刻判断。
5.4 FSMC 引脚悬空没处理:不接器件时漏电还乱跳
现象:整板静态电流比预期高,FSMC 引脚对地测量有几百毫伏的乱跳电压,外部器件没接的时候 NE 信号还能在示波器上看到毛刺。
原因:FSMC 引脚配置成复用推挽后,如果外部器件没焊或者处于高阻态,没有外部驱动也没有内部上下拉,引脚浮空产生漏电和随机翻转。
解决:所有 FSMC 引脚配置内部上拉,GPIO_InitStruct.Pull = GPIO_PULLUP。硬件上,外部器件的片选和读写引脚应加默认拉高电阻,防止上电瞬间误操作。空板调试时优先焊上 SRAM,让总线有确定的负载。
5.5 照抄参考设计却一进去就跑飞
现象:把别人的 FSMC 初始化代码复制过来,直接改 bank 号,结果程序一访问外部地址就 HardFault,停在HardFault_Handler。
原因:不同型号 STM32F4 的 FSMC Bank 能力不一样,有些小封装型号没有完整引出所有片选,访问不存在的地址区域直接触发总线错误。另一个原因是被参考者的 HCLK 频率和时序值不同,外部器件响应太慢导致总线 hang 住。
解决:先查数据手册确认自己的型号支持哪个 Bank。用最小的例子,只初始化 NE1 和 SRAM,按 3.2 节的起步参数跑起来,确认外部地址能顺利读写后再扩展。访问前在调试器里观察 FSMC 的 BTR/BCR 寄存器值,和配置核对一遍。
6. 把 FSMC 封装成内存映射的外设层:换器件只改宏
当 FSMC 总线稳定后,代码组织方式决定后期维护成本。我习惯把 LCD、外部 SRAM、NOR 的访问封装成一组宏或函数指针,上层代码只依赖语义接口,不依赖具体地址。
typedef struct { void (*write_cmd)(uint16_t); void (*write_data)(uint16_t); } LCD_Ops; static void cmd_via_fsmc(uint16_t cmd) { *(volatile uint16_t *)LCD_CMD_ADDR = cmd; } static void data_via_fsmc(uint16_t dat) { *(volatile uint16_t *)LCD_DATA_ADDR = dat; } const LCD_Ops lcd_fsmc = { .write_cmd = cmd_via_fsmc, .write_data = data_via_fsmc };如果哪天把并口屏换成了 SPI 屏,只需要另外写一个lcd_spi操作集,上层所有绘制代码不需要改。这个抽象层的成本极低,但能避免在几十个文件里搜地址替换。
我自己习惯在拿到新板子时先写一个时序抓波形的小工具:用一个 GPIO 翻转信号触发示波器,观察 FSMC 的 NE 低电平时间、NOE 有效窗口和 D 数据线的建立关系。这样调参数不再靠猜,每个时序值都能对应到实际波形。第一次调 FSMC 时我为了省事,直接照抄了一份参数,结果在排查上花了两天,从那以后每次都先把时序波形抓稳再写业务逻辑。希望你也能避开这个弯路,希望帮到你。
本文还有配套的精品资源,点击获取