2026/10/30 3:28:28

STM32驱动TFTLCD:FSMC并口时序、初始化与DMA刷屏优化

STM32驱动TFTLCD:FSMC并口时序、初始化与DMA刷屏优化 简介面向STM32初学者的TFTLCD显示实验程序依托ALIENTEK MINISTM32实验10整理围绕GPIO与SPI/I2C时序配置、驱动芯片初始化、显示缓冲区管理、绘图函数和刷屏流程展开帮助读者理解嵌入式图形显示从底层接口到屏幕更新的完整链路。压缩包共40个文件以18个.h头文件和11个.c源文件为主体辅以工程配置、Hex文件、启动汇编、说明文本和批处理脚本整体约87KB目录按USER、HARDWARE、SYSTEM等模块组织LCD、OLED、KEY、EXTI、TIMER、LED、WDG及delay、sys、usart等代码便于按功能查阅。已有5190人学习亲测可用的工程可作为课程实验、开发板调试和显示驱动入门的参考通过对照源码能快速梳理外设初始化、命令数据传输与屏幕刷新之间的关系减少搭建验证环境时的重复摸索。1. 白屏那十分钟里STM32 到底在等什么把一块 240×320 的并口 TFTLCD 接到 STM32 上通电那一刻大概率是白屏背光亮着、屏上什么都没有——这时候很多人第一反应是屏坏了其实九成是时序和初始化序列没对上。这份 TFTLCD 显示程序要解决的正是这一段从 FSMC 或 GPIO 模拟的 8080 并口时序、上电复位延时、控制器初始化表到画点、填块、字符显示和刷屏优化一整条链路能跑通并且可移植。它适合刚拿到屏和板子、想先把图形界面底盘打稳的人也适合已经把屏点亮但刷得慢、颜色不对、局部刷新有撕裂的熟手。下面按接线、初始化、绘图、排查、加速的顺序拆。2. 接口与时序怎么选FSMC 并口、GPIO 模拟差在哪屏能不能点亮一半取决于接口方案选得对不对。240×320 分辨率、每像素 2 字节一屏就是 153600 字节如果用 GPIO 逐位翻转搭 8080 并口一条写数据指令可能就要占用十几个 CPU 周期刷一整屏的耗时能到几百毫秒动画根本谈不上。FSMC部分系列叫 FMC的价值就在这儿它把并口写操作变成一次内存写指令CPU 只管往固定地址丢数据读写时序由硬件产生CPU 不用空转等。2.1 8080 并口真正需要关心的只有四组信号别看屏的排线有十几二十根按功能拆开其实就四组片选 CS、数据/命令选择 DC有的丝印写 RS、写使能 WR、读使能 RD加上 8 位或 16 位数据总线。控制器的内部逻辑非常简单——WR 上升沿采样总线此刻 DC 为低就当成命令DC 为高就当成数据。理解了这一条后面所有时序参数都是在保证“采样那一刻总线上数据是稳的”。时序上真正决定成败的是三个参数地址建立时间、数据建立时间、地址保持时间。数据建立时间不够控制器采到的就是上一位的残留值表现出来就是花屏、颜色串位地址建立时间不够DC 和 CS 还没到位数据就来了表现为偶发写飞、初始化表执行到一半就卡死。这种问题最玄学的地方在于示波器上看着波形都在但换一块同型号的屏又不一定复现所以参数宁可一开始放宽一点跑通之后再往回压。我一般的做法是先按“慢速可靠”的参数把屏点亮确认初始化序列没问题再逐档压缩数据建立时间每压一档就全屏填充一次看有没有噪点。这个方法比对着示波器一点点量要快得多也更贴近实际使用条件。2.2 把 DC 接到地址线上FSMC 地址映射的真实含义FSMC 最反直觉的设计是它没有专门的“命令/数据”引脚而是把 DC 挂在某根地址线上靠访问不同地址来区分。原理是 FSMC 访问 16 位位宽存储器时内部地址 HADDR[25:1] 映射到 FSMC_A[24:0]也就是说 FSMC_A[k] 对应字节偏移 2^(k1)。所以如果 DC 接在 FSMC_A18 上DC 接的地址线字节偏移宏定义写法A160x20000LCD_BASE 0x20000A170x40000LCD_BASE 0x40000A180x80000LCD_BASE 0x80000这份账算错的后果很典型往命令地址写数据时屏上出来了乱码或者写入完全没反应。因为偏移不对DC 线电平和你以为的正好相反。/* 假设 LCD 挂在 FSMC Bank1 / NE1 上DC 接 FSMC_A1816 位数据宽度 */ #define LCD_BASE ((uint32_t)0x60000000UL) #define LCD_REG (*(volatile uint16_t *)(LCD_BASE)) /* DC 0写命令 */ #define LCD_DATA (*(volatile uint16_t *)(LCD_BASE 0x80000UL)) /* DC 1写数据 */这段宏是整套驱动的基础。volatile不能省否则编译器优化时会把连续写合并或删除uint16_t指针保证一次访问正好是一个 16 位总线周期写成uint32_t会一次发出两个周期命令和数据就错位了。如果你的板子用的是 8 位并口偏移要改成 2^k同时指针类型换成uint8_t。FSMC 初始化部分不同系列在 HAL 库里前缀不一样F1/F2 叫FSMC_F4/H7 叫FMC_宏名基本一一对应SRAM_HandleTypeDef hsram; FSMC_NORSRAM_TimingTypeDef timing; hsram.Instance FSMC_NORSRAM_DEVICE; hsram.Extended FSMC_NORSRAM_EXTENDED_DEVICE; hsram.Init.NSBank FSMC_NORSRAM_BANK1; hsram.Init.DataAddressMux FSMC_DATA_ADDRESS_MUX_DISABLE; hsram.Init.MemoryType FSMC_MEMORY_TYPE_SRAM; hsram.Init.MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_16; hsram.Init.BurstAccessMode FSMC_BURST_ACCESS_MODE_DISABLE; hsram.Init.WaitSignalPolarity FSMC_WAIT_SIGNAL_POLARITY_LOW; hsram.Init.WrapMode FSMC_WRAP_MODE_DISABLE; hsram.Init.WaitSignalActive FSMC_WAIT_TIMING_BEFORE_WS; hsram.Init.WriteOperation FSMC_WRITE_OPERATION_ENABLE; hsram.Init.WaitSignal FSMC_WAIT_SIGNAL_DISABLE; hsram.Init.ExtendedMode FSMC_EXTENDED_MODE_ENABLE; hsram.Init.AsynchronousWait FSMC_ASYNCHRONOUS_WAIT_DISABLE; hsram.Init.WriteBurst FSMC_WRITE_BURST_DISABLE; hsram.Init.ContinuousClock FSMC_CONTINUOUS_CLOCK_SYNC_ONLY; timing.AddressSetupTime 2; /* 地址建立FMC 时钟周期数 */ timing.AddressHoldTime 1; timing.DataSetupTime 6; /* 数据建立先给足跑通后再压 */ timing.BusTurnAroundDuration 0; timing.CLKDivision 0; timing.DataLatency 0; timing.AccessMode FSMC_ACCESS_MODE_A; HAL_SRAM_Init(hsram, timing, timing);参数说明里最值得动手的就是DataSetupTime。它是按 FMC 时钟周期计的HCLK 168MHz 时一个周期约 6ns写 6 大约是 36ns对多数控制器的写周期通常要求不小于 50ns 左右的 WR 脉宽是安全的。跑通之后可以试着压到 3 甚至 2得到更高的刷屏带宽如果压完出现雪花点说明这块屏的时序余量到头了老老实实回退一档。AddressSetupTime影响的是 DC 和 CS 的建立裕量通常和 CD 的极性配合使用给 1 到 2 就够。2.3 GPIO 模拟版的落地写法与适用边界不是所有板子都空闲出 FSMC 引脚比如引脚本来就紧张的 48 脚封装。这时候就得用 GPIO 模拟版。它的写法思路是把 8 位或 16 位数据口整组配置成推挽输出用位带或者寄存器直接赋值代替逐位操作。/* 以 8 位并口为例数据口用一组连续的 GPIO 引脚 */ static inline void LCD_WR_CMD(uint8_t cmd) { LCD_DC_LOW(); /* DC 0表示命令 */ GPIOB-ODR (GPIOB-ODR 0xFF00) | cmd; /* 低 8 位灌数据其余位保持 */ LCD_CS_LOW(); LCD_WR_LOW(); /* WR 拉低 */ __NOP(); __NOP(); /* 给控制器一点采样窗口 */ LCD_WR_HIGH(); /* 上升沿锁存 */ LCD_CS_HIGH(); } static inline void LCD_WR_DATA(uint8_t data) { LCD_DC_HIGH(); /* DC 1表示数据 */ GPIOB-ODR (GPIOB-ODR 0xFF00) | data; LCD_CS_LOW(); LCD_WR_LOW(); __NOP(); __NOP(); LCD_WR_HIGH(); LCD_CS_HIGH(); }这段代码的核心是ODR的读-改-写 0xFF00保留高 8 位| cmd写入低 8 位一次操作完成整字节输出。__NOP()的数量决定了 WR 低电平持续多久具体要看主频和屏的时序手册主频高的时候需要加主频低的时候可以省。GPIO 模拟的实际测速参考72MHz 主频、8 位并口、每次写约 8 到 10 个周期折合单像素写入约 0.12μs全屏填充 76800 像素约 9ms 到 15ms。数字看着还行但实际画图时中间还要夹窗口设置、坐标计算实际会到 20ms 以上。画小图标、刷新几个字符完全够用做动画或滚动列表就明显不够了。我的判断标准是如果项目里需要整屏刷新频率高于每秒 30 次直接上 FSMC别在 GPIO 上想办法优化。3. 初始化序列与读 ID让屏先承认自己在线屏点亮不了的第二大原因是初始化序列。网上一搜能搜到七八套看起来差不多的表但直接抄往往不生效因为不同批次屏的控制器、偏压参数、伽马曲线可能完全不一样。这一章讲清楚初始化表里哪些是真必需的、哪些可以放、以及怎么在写代码之前先用读 ID 的方式确认自己在跟谁打交道。3.1 上电、复位与延时省掉的每一个毫秒都会反咬一口控制器上电之后并不是立刻就能接收命令的。内部的电荷泵需要时间建立稳定的偏压VCC 和 VDDI 的爬升速度又受外部电容影响。手册里普遍要求上电后至少延时 5ms 到 120ms 才能发第一条指令这个区间看着很宽但实际项目里往往卡在短的那一端出事冷启动能用掉电重启就白屏。复位时序更简单也更要命RES 拉低至少 10μs拉高之后还要等 120ms 左右控制器才会完成内部寄存器复位。很多人写HAL_Delay(1)就往下走结果只有在室温下能跑。void LCD_HardwareReset(void) { LCD_RST_LOW(); HAL_Delay(20); /* 远大于手册要求的 10us留足余量 */ LCD_RST_HIGH(); HAL_Delay(120); /* 复位后等内部逻辑稳定别改小 */ }注意上电延时和复位后的等待用示波器看波形是看不出问题的它影响的是内部电荷泵和寄存器状态。这类问题最典型的症状就是“放一晚上第二天开机不亮按一下复位又好了”。3.2 写命令 / 写数据的分层封装一旦顺手后面全都省事驱动层的骨架就是把“写命令”和“写数据”两件事封装干净其余全部基于这两个函数搭建。分层清楚的好处是换接口FSMC 换 GPIO 模拟只改这两个函数上层绘图代码一行不动。static inline void LCD_WriteCmd(uint16_t cmd) { LCD_REG cmd; /* FSMC 方案直接写命令地址 */ } static inline void LCD_WriteData(uint16_t dat) { LCD_DATA dat; /* FSMC 方案直接写数据地址 */ } /* 批量写同样数据填充矩形/清屏用得上 */ void LCD_WriteDataRepeat(uint16_t dat, uint32_t count) { while (count--) { LCD_DATA dat; /* 无循环内判断以外开销编译器会展开优化 */ } }LCD_WriteDataRepeat这个函数看着废话但它的实际作用不小把“写一个像素”的循环写在驱动层编译器有机会把LCD_DATA dat优化成寄存器写加地址自增比在应用层写for循环快 10% 到 20%。如果编译器的优化级别是-O2以上效果更明显。初始化表本身长这样参数值需要按你手上的屏手册来改typedef struct { uint8_t cmd; uint8_t len; /* 后跟的参数个数 */ uint8_t para[16]; /* 参数缓存 */ uint16_t delay_ms; /* 本条执行后需要等待的时间 */ } LCD_InitCmd_t; const LCD_InitCmd_t LCD_InitTable[] { {0x01, 0, {0}, 120}, /* 软复位之后必须等 */ {0x11, 0, {0}, 120}, /* 退出睡眠等电荷泵建立 */ {0x36, 1, {0x08}, 0}, /* 扫描方向 BGR 顺序后面细说 */ {0x3A, 1, {0x55}, 0}, /* 像素格式16 位 RGB565 */ {0x2A, 4, {0x00,0x00,0x00,0xEF}, 0}, /* 列地址范围 0~239 */ {0x2B, 4, {0x00,0x00,0x01,0x3F}, 0}, /* 行地址范围 0~319 */ {0x29, 0, {0}, 20}, /* 打开显示最后才发 */ };表里三条是必需的软复位0x01、退出睡眠0x11、打开显示0x29最后一条必须排在所有配置之后否则屏幕会在配置过程中短暂显示乱码。像素格式0x3A设成 0x55 表示 16 位 RGB565这个和后面绘图函数的颜色编码必须一致设成 0x66 就是 18 位代码里所有颜色值都得跟着改。delay_ms字段不能省尤其是 0x11 之后的那 120ms这是电荷泵建立时间省了就会出现亮度不均或者局部发暗。3.3 用读 ID 做上电自检先确认对面是谁写初始化之前先读一次控制器 ID是省时间的好习惯。读到了预期值说明接线、时序、DC 地址映射这三件事都对了读不到或者读到全 0、全 F问题一定在前三步这时候再去调初始化表纯属浪费时间。uint16_t LCD_ReadID(void) { uint16_t id; LCD_WriteCmd(0x04); /* 读显示 ID 指令部分控制器用 0xD3 */ LCD_ReadData(); /* 空读一次丢弃首个无效字节 */ id (LCD_ReadData() 8) 0xFF00; id | LCD_ReadData() 0x00FF; return id; } void LCD_SelfCheck(void) { uint16_t id LCD_ReadID(); if (id 0x0000 || id 0xFFFF) { /* 全 0 / 全 F 基本等于总线没通先查 CS、WR、DC 和地址偏移 */ LCD_ErrorHandler(); } }三个细节值得说清楚。第一读之前必须先把 FSMC 配成支持读的模式扩展模式下读写时序是分开配置的只配写时序读回来永远是 0。第二第一个字节要丢弃因为控制器需要一拍时钟把数据推到总线上这一拍读到的是上一次总线残留。第三不是所有控制器都支持 0x04另一批用 0xD3返回 4 个字节实际有效的是后两个。常见做法是两套都试一次哪个返回的不是 0x0000/0xFFFF 就用哪个。4. 绘图底层从画点、填块到字符的三层结构屏点亮之后真正拉开体验差距的是绘图层的组织方式。同样是画一个 100×100 的矩形有人写 10000 次画点函数有人设一次窗口连续灌 10000 个像素耗时能差三倍以上。这一章按“窗口 → 画点 → 填块 → 字符”的顺序把层搭起来每一层只依赖下一层的接口。4.1 窗口设置刷屏性能的分水岭控制器内部有一块 GRAM显示的时候是扫描电路在自动读 GRAM 送屏。CPU 写 GRAM 时写入位置由列地址和行地址寄存器决定每写一个像素自动递增碰到列尾回到行首。所以设置一次窗口0x2A 设列范围、0x2B 设行范围后面连续写 0x2C 就相当于往一个矩形区域里顺序灌数据地址自增由硬件做。void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); /* 列地址设置 */ LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); /* 行地址设置 */ LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); /* 之后的数据都写进这个窗口 */ }参数范围一定要做边界钳位x1超过 239 或者y1超过 319控制器会直接把地址绕回到 0表现出来就是画到屏幕右边的图形从左边缘冒出来找 bug 的时候极易误判成线圈错位。注意窗口设置和后续数据写入之间不要再插其他命令包括读状态。中间插一条指令控制器的写入指针就会重置前面设的窗口作废。4.2 画点到填块把重复的坐标计算砍掉最底层的画点函数必须遵守“先设窗口再写一个像素”的顺序void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); /* 窗口退化为 1x1 */ LCD_WriteData(color); }单个画点其实很慢——一次画点要发三条命令加五个数据总线上二十多个周期就没了。所以画线、画矩形、填充这些操作绝不能循环调用它。正确的做法是填块void LCD_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint32_t total (uint32_t)w * h; if (x LCD_W || y LCD_H) return; if (x w LCD_W) w LCD_W - x; /* 裁剪到屏幕内 */ if (y h LCD_H) h LCD_H - y; total (uint32_t)w * h; LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_WriteDataRepeat(color, total); }这段代码里裁剪的两个if看着不起眼但它决定了绘图函数能不能安全处理越界坐标。实际项目里 UI 元素带圆角、带阴影坐标算出来轻微越界太正常了没有裁剪的话轻则画出屏幕重则触发地址绕回把另一块区域刷掉。total用uint32_t是必须的。w * h最大到 240×320 76800已经超过uint16_t上限虽然这里是 16 位乘法不会溢出但一旦屏换成 320×480uint16_t就炸了静默刷一半就停很难查。有了填块画线就是一组短填块或者 Bresenham 画点画空心矩形是四条填块画实心圆是按行算弦长再填块。实测在 FSMC 方案下全屏 240×320 填充耗时约 4ms 到 6ms用画点循环拼则要 30ms 以上差距就是这么来的。4.3 字符与汉字字库、取模与半透明叠加字符显示分两派ASCII 用点阵字库汉字用取模软件生成的数组。ASCII 常用 8×16 或 12×24字库存成const uint8_t ascii_8x16[95][16]每个字符 16 字节按列或按行扫描。汉字用 16×16 点阵一个汉字 32 字节。/* 按行扫描的 16x16 汉字点阵每行 2 字节 */ void LCD_ShowChinese16(uint16_t x, uint16_t y, const uint8_t *font, uint16_t color, uint16_t bg) { for (uint8_t row 0; row 16; row) { uint16_t bits (font[row * 2] 8) | font[row * 2 1]; for (uint8_t col 0; col 16; col) { if (bits (0x8000 col)) { LCD_DrawPoint(x col, y row, color); } else if (bg ! COLOR_TRANSPARENT) { LCD_DrawPoint(x col, y row, bg); } } } }这段是最直观的实现但性能很差——16×16 一个汉字要调用 256 次画点。实用版本要做两件事一是把 16 行合并成一次窗口设置同一行的 16 位一次性写成 16 个像素二是把颜色提取出来预先生成一个 16 像素的缓冲区一次性灌进去。改完之后单个汉字的耗时能压到 200μs 以内。bg传COLOR_TRANSPARENT时只画前景点这就是所谓叠字效果做弹出菜单、提示框这类需要压在半透明背景上的场景会用到。判断透明色不要用比较浮点或者魔法值定义一个明确常量并在编译期固定下来。字库占用的 Flash 也要算清楚常用 3500 个汉字 × 32 字节 ≈ 112KB加上 ASCII 部分对 Flash 只有 64KB 的型号来说根本放不下。常见做法是外挂 SPI Flash 存字库运行时按区位码查表读取或者只放项目里实际用到的那几十上百个字。5. 避坑与排查花屏、偏色、闪屏的五条血泪记录屏调不出来的时候症状往往长得很像但根因分布在完全不同的层。这一章按“现象 → 原因 → 解决”的格式记录五个踩过并排查过的问题遇到新问题时对照症状比盲改代码快得多。5.1 背光亮着屏上什么都没有读 ID 全是 0xFF现象上电后背光点亮屏上保持纯白或纯黑串口打印读回来的 ID 恒等于 0xFFFF换初始化表也没用。原因数据总线没有真正接到位或者 CS 一直处于无效状态。FSMC 的片选是硬件自动拉低的但 GPIO 模拟方案里如果 CS 被误配置成浮空输入控制器永远收不到有效片选。解决万用表量一遍 CS、WR、DC 三根线静置状态下 CS 应为高、WR 应为高、DC 状态取决于当前操作。然后确认 FSMC 的 NE 编号和代码里用的 Bank 一致——NE1 对应FMC_NORSRAM_BANK1接 NE2 却写 BANK1 是高频错误。最后确认读时序也配了只配写时序读回来必然是 0xFF。5.2 屏幕有内容但满屏雪花点静止图像也有噪点现象能看出图形轮廓但整屏覆盖细密噪点像信号干扰。把数据建立时间调大之后噪点减少但不消失。原因典型的时序余量不足。数据建立时间不够或者主频刚调高没重新算 FSMC 参数。另外电源纹波大也会造成类似现象尤其是屏和板子共用一路 3.3V、背光升压又在附近时。解决把DataSetupTime从当前值翻倍如果噪点明显减少就说明方向对逐档压回去找到临界点再留一档余量。如果翻倍后毫无改善转去查电源给屏的 VCC 并一个 10μF 加 100nF背光走独立供电噪声常常就消失了。5.3 红色画出来是蓝色颜色整体反了现象所有颜色看起来都“对调”了红蓝互换或者整屏是负片效果。原因两个地方会出这个问题。一是初始化表里 0x36内存访问控制的 BGR 位没设对RGB 和 BGR 顺序颠倒二是像素格式和代码里的颜色编码不匹配控制器按 RGB565 接收代码里按 BGR565 拼。解决先改 0x36 的 bit3。这一位是 BGR 顺序开关取反试一次如果颜色正常了就是它。如果改完颜色还是偏检查颜色宏本身#define RED 0xF800在 RGB565 下是红色如果屏是 BGR 顺序同样的值会显示成蓝色。统一在一处定义颜色宏不要在各个绘制函数里手写十六进制。5.4 图形沿对角线错位或者上下左右镜像现象画一个矩形显示出来是斜的平行四边形或者坐标 (0,0) 显示在右下角。原因0x36 寄存器的扫描方向位MY、MX、MV设置和实际使用方式不匹配。这三个位控制行列扫描方向和交换不同屏厂默认值不一样。解决先把 0x36 设成 0x00画一个原点在左上角的矩形看它出现在哪个角。出现在左上角说明不用改出现在右上角把 MX 位置 1出现在左下角把 MY 位置 1。MV 位是行列交换一般只在横竖屏切换时用。横屏模式除了改 MV还得同步交换列地址和行地址的范围0x2A 从 0~239 改成 0~3190x2B 反过来。5.5 局部刷新时屏幕闪烁或者刷新区域边缘有拖影现象整屏刷新看不出来一旦只更新一小块区域比如进度条、数字那块区域会闪一下或者边缘残留上一帧的像素。原因窗口没设对导致写入越界到相邻区域或者刷新时先清背景再画前景两次写 GRAM 之间被扫描电路显示出来了就形成闪烁。解决第一检查窗口设置里的坐标是不是包含右边界和下边界习惯上x1和y1要传x w - 1而不是x w多传一个像素就写到了相邻行首。第二局部刷新不要把“先填充背景色再画内容”拆成两次窗口操作合并成一次先算好这块区域的完整像素内容放进缓冲区一次窗口写完。这样扫描电路看到的永远是完整帧不会有中间态。6. 进阶用 DMA 搬像素并且真的量出快了多少前面所有优化都是在“CPU 亲自搬数据”这个前提下做的再压也压不出质变。真正能把刷屏速度提一个档的做法是把像素搬运交给 DMA源地址是内存里的帧缓冲目的地址是LCD_DATA这个固定地址DMA 配成存储器到外设、地址不自增、传输宽度按半字。void LCD_BlitDMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *buf) { LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_WriteCmd(0x2C); /* 先切到写 GRAM 状态 */ /* 外设地址固定为数据口内存地址自增半字宽度 */ HAL_DMA_Start(hdma_mem2mem, (uint32_t)buf, (uint32_t)LCD_DATA, (uint32_t)w * h); /* 等本次传输完成避免下一帧覆盖缓冲区 */ while (__HAL_DMA_GET_FLAG(hdma_mem2mem, DMA_FLAG_TC) RESET) { } __HAL_DMA_CLEAR_FLAG(hdma_mem2mem, DMA_FLAG_TC); }三个参数点必须对上否则数据会半截错位。第一DMA 传输宽度要和 FSMC 的存储器位宽一致16 位并口必须用半字模式用字节模式会出现高低字节颠倒。第二外设地址不自增因为所有像素都写同一个LCD_DATA内存地址自增因为缓冲区里是连续的。第三源地址和目的地址的数据宽度要匹配帧缓冲用uint16_t数组DMA 配置填DMA_PDATAALIGN_HALFWORD | DMA_MDATAALIGN_HALFWORD。缓冲区大小要算清楚240×320×2 字节 150KB。这个量级只有带外部 SRAM 或者 256KB 以上内部 RAM 的型号才放得下。内存不够就退到“双小块缓冲”方案开两块 240×16 的缓冲区一块给 DMA 搬一块给 CPU 画乒乓切换。这样既能把 CPU 和总线重叠起来占用也只有 15KB。加速效果到底多少别信感觉量出来。用 DWT 周期计数器最省事static inline uint32_t DWT_GetCycles(void) { return DWT-CYCCNT; } void LCD_Benchmark(void) { uint32_t t0, t1; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; /* 使能周期计数器别忘开 */ t0 DWT_GetCycles(); LCD_FillRect(0, 0, 240, 320, COLOR_BLACK); t1 DWT_GetCycles(); printf(fill 240x320: %lu cycles\r\n, (unsigned long)(t1 - t0)); }在 168MHz 主频下我手上这套 FSMC 配置跑出来的结果是LCD_WriteDataRepeat整屏填充大约 4.8msDMA 方案大约 3.1ms提升约 35%。提升幅度没有想象中大因为瓶颈已经从 CPU 转移到了总线时序本身——数据建立时间 6 个 FMC 时钟周期每个像素的物理下限就在那里摆着。想再快只能压时序参数或者提高 FMC 时钟而这两条都会撞上屏的物理极限。另外一个容易被忽略的收益是 CPU 占用。DMA 搬运期间 CPU 是空闲的可以把这段时间用来算下一帧的内容或者处理按键。做简单动画时这一点比那 1.7ms 的绝对时间更重要。测速这件事我后来形成了固定习惯任何一块新屏接入先把填充、画线、单字显示这三个基准测一遍把数字记在项目文档里。换屏、改主频、改时序参数之后重测一次对比数值突然变差就说明某处配置被改坏了。这个习惯救过我两次——一次是同事把 FMC 时钟配置改回了默认值导致刷屏慢了一倍另一次是 DMA 配置被误改成了字节模式画面高低字节颠倒靠基准数字十分钟就定位到了原因。希望帮到你。本文还有配套的精品资源点击获取