☰
FSMC驱动LCD显示实战:从原理到代码,解决花屏错位问题
2026/10/24 8:33:17 网站建设 项目流程

这篇是系列实验的第四篇,前面几期我们用GPIO模拟时序、SPI接口点过屏,也踩了不少坑,这次换FSMC来搞LCD显示。很多朋友会觉得FSMC是个一听就头大的外设,寄存器又多又抽象,实际跑完一遍你会发现,它就是个“按内存地址写值”的工具罢了。这篇文章从FSMC驱动LCD的原理讲起,把硬件接线、时序配置、代码封装、常见花屏错位问题一并拆开聊,适合刚好做到这一课的嵌入式学习者,也适合想把底层机制吃透、不再靠复制粘贴驱动代码的开发者。

1. 为什么LCD偏偏要挂在FSMC上

1.1 先认清LCD控制器的“性格”

现在手头常用的TFT彩屏,内部基本都嵌了一颗液晶控制芯片,对外提供标准的8080并行接口,也可以兼容6800时序。8080接口有几个信号非常固定:片选CS、寄存器选通RS(也叫DC)、读写控制WR/RD,以及8位或16位并行数据线D[15:0]。写入一条命令还是写入一个数据,区别就在于RS这个引脚的电平状态。如果我用普通GPIO一根一根去拉,一次往数据口写一个16位的值就得折腾二十根引脚,刷一个全屏要算好几十万次循环,CPU忙到焦头烂额,帧率还上不去。

我早期用GPIO模拟总线点一块240x320的屏,刷一帧纯色都要几十毫秒,显示图片直接肉眼可见地从上往下扫。这个阶段的练习是有价值的,能帮你记住时序波形长什么样,但到了实际项目里,要么上FSMC,要么上LTDC之类专用显示外设,否则性能就是过不去。

1.2 FSMC到底解决了什么问题

FSMC的全称是Flexible Static Memory Controller,灵活静态存储控制器。它原本的设计目标是让MCU可以外扩SRAM、NOR Flash这类存储芯片,通过一套固定的读写时序,把外部存储器映射到芯片内部的地址空间里。你往某个内存地址写一个数,FSMC外设会自动帮你把CS、WR、数据总线等等信号按照配置好的时序拉一遍,完全不用CPU去干预引脚电平。

而8080接口的LCD控制芯片,本质上就是一个“长得像SRAM”的设备。它也有CS、RS、WR、RD,也有并行数据总线,读的时候返回数据,写的时候接收命令或数据。既然时序长得像,那FSMC完全可以把它当成一片外部SRAM来驱动。更妙的是,RS这个引脚可以直接接到FSMC的地址线上,借地址译码的机制来区分命令和数据。

这个思路一旦想通,后面所有代码就顺了。写LCD的寄存器,就是往SRAM地址区间的某个偏移地址写值;写像素数据,就是往另一个偏移地址写值。CPU看来只是简简单单的赋值操作,真正的时序细节全部交给FSMC硬件去完成。

1.3 FSMC方案的实际收益

用FSMC驱动LCD,最直观的变化就是快。同样是刷一屏240x320的图片,GPIO模拟可能要上百毫秒,FSMC配合16位数据线能做到个位数毫秒级别,如果再把DMA拉进来搬运数据,刷屏期间CPU几乎可以腾出来跑其他逻辑。

其次是代码变得干净。你自己封装几个基础函数之后,上层不管是画点、画线、显示字符还是显示图片,统统不用再关心时序怎么拉。这个抽象层次对于后续的项目迭代太重要了,哪怕后面换芯片平台,只要保留接口不变,上层UI代码基本可以原封不动搬走。

2. 硬件连接与地址映射认知

2.1 引脚分配和板级接线的底层逻辑

先把接线这层讲清楚。我用的开发板MCU是M4内核,FSMC外设挂在AHB总线上,不同型号引脚略有区分,但大思路一致。PCB上把LCD模组的DB0~DB15接到FSMC的D0~D15,WR接NWE,RD接NOE,CS接NE1片选,RS接在某根地址线上,比如FSMC_A6,RESET单独接普通GPIO。

这里有一个细节经常让新手翻车:NE1对应的是Bank1的片选区域,基地址是0x60000000,而RS接FSMC_A6意味着当你访问0x60000000到0x6000007F范围内的地址时A6为低,访问0x60000080以上且对应位为高的地址时A6为高。所以在软件里定义命令地址和寄存器地址时,不是随便编一个偏移值,而是由硬件接在哪根地址线上决定的,必须一一对应。

2.2 为什么数据宽度、地址线偏移要配套检查

FSMC支持8位、16位数据宽度。LCD屏一般用16位模式,因为RGB565颜色格式一个像素正好16位,一次总线访问写一个像素,效率最高。在16位模式下,FSMC内部地址线是自动左移一位对齐的,也就是说你代码里写地址0x60000080,实际上对应的FSMC_A6输出高电平。这一点软件工程师容易忽略,都以为地址就是物理引脚电平的直接映射,其实不是。

所以再看代码的时候,你会发现大家用HADDR这种概念。简单记就是:16位模式下访问地址左移1位,地址最低位被忽略。我调试时遇到过自己写的地址偏移差一倍,导致写数据命令被当成了写寄存器命令,整个屏幕颜色全乱,排查了半天才发现是地址换算出了问题。

2.3 硬件设计上预留的调试点

如果你是自己画板子,PCB布线时尽量让FSMC数据线等长,避免信号偏移过大。我当时自己打样时没太在意,后来高频率刷新时屏幕偶尔出现随机噪点,用示波器抓信号发现D7和D10的走线长度差了一大截。这不是FSMC配置能解决的,只能改板。如果是买现成开发板,这些基本不用操心,但你要清楚板厂已经把数据线接好了,不能随意改引脚映射。

注意:复位引脚不要和FSMC的任何控制信号共用,LCD控制器需要一个干净的上电复位时序,否则初始化命令发得再对也白搭。

3. FSMC初始化与LCD驱动代码实现

3.1 时钟、引脚复用和FSMC外设配置

先把FSMC外设时钟打开,再把对应引脚配置为复用推挽输出。这里比较适合用标准库或HAL库,手动去翻寄存器也行,但不利于专心搞清逻辑。我的习惯是先用库函数跑通,再回头对照寄存器手册理解每个位段的意义。

FSMC初始化的核心是配置NOR/SRAM Bank1的时序参数。结构体里有一堆字段,最关键的是:

参数作用说明
AddressSetupTime地址建立时间从片选有效到写信号有效的等待周期,单位是HCLK时钟个数
AddressHoldTime地址保持时间地址信号在访问周期内的保持时间,对于SRAM模式可设为0
DataSetupTime数据建立时间数据线上数据稳定到采样点的时间宽度,直接影响读时序正确性
AccessMode访问模式一般选模式A,和SRAM接口最匹配
MemoryType存储器类型选择SRAM,LCD就是当作SRAM访问
MemoryDataWidth数据总线宽度必须和LCD数据引脚配置保持一致,选16位

这些时序参数的取值不是拍脑袋出来的,要看LCD控制芯片数据手册里给出的最小读写周期时间,再结合系统时钟频率折算成HCLK周期个数。比如手册要求写信号最小宽度是几十纳秒,而系统时钟是168MHz,一个HCLK周期约6纳秒,那你DataSetupTime至少要配到十几以上。宁大勿小,配大了只是稍慢,配小了屏幕会花屏或者完全没反应。

3.2 地址宏定义与底层读写函数

初始化完成后,就可以定义命令地址和数据地址。假设NE1片选基地址是0x60000000,RS接FSMC_A6,那么命令地址和寄存器地址相差的偏移是1 << (6 + 1),这里加1是因为16位模式地址左移一位。所以代码看起来像:

#define LCD_BASE ((uint32_t)(0x60000000 | 0x00000000)) #define LCD_REG_ADDR (LCD_BASE) /* A6 = 0 */ #define LCD_RAM_ADDR (LCD_BASE | (1 << (6 + 1))) /* A6 = 1 */

底层读写函数就这么点东西:

static inline void LCD_WriteReg(uint16_t reg) { *(volatile uint16_t *)LCD_REG_ADDR = reg; } static inline void LCD_WriteData(uint16_t data) { *(volatile uint16_t *)LCD_RAM_ADDR = data; } static inline uint16_t LCD_ReadData(void) { return *(volatile uint16_t *)LCD_RAM_ADDR; }

你别看它短,这就是FSMC驱动LCD的全部核心。后面的画点、画线、图片显示,最终都是调用这三个函数。因为FSMC硬件已经处理了时序,所以你甚至可以用调试器在内存窗口里直接改动LCD_REG_ADDR的值,屏幕上立刻就有反应,调试体验非常好。

3.3 初始化序列和延时细节

LCD控制器上电之后要执行一串初始化命令,具体命令序列和参数值,芯片厂商的规格书或者驱动例程里都有。我习惯把初始化序列放在一个数组里,包含命令号、参数个数、参数内容,用统一的解析函数逐条发送,这样代码清晰、方便增删调试命令。

上电延时是容易翻车的地方。LCD控制器的内部电源和振荡器需要时间稳定,芯片手册通常会规定一个上电延时时间,常见的是几十到一百多毫秒。有些同学初始化命令发太快,屏上什么都没有,还以为是接线错了,实际就是延时不够。我第二步做完之后习惯手动在代码里加一个100ms的延时,别省这点时间。

3.4 读写时序的选型心得

FSMC时序里还有一个容易忽视的参数:总线周转时间。在极端情况下,读写切换太快会导致总线上残留上一次读的无效数据,所以有些配置里会把BusTurnAroundDuration设为1或2。LCD这种设备本身不太挑剔,但如果你发现读取像素数据偶尔出现杂讯,可以尝试把这个参数调大一点再测试。

实际项目里,我一般先把地址建立时间和数据建立时间配得偏大,确保能点亮屏幕,跑通全部功能之后,再一点一点往小调,每次调完刷一幅特定测试图看效果。这种做法比一上来就追求极限性能稳妥得多。

4. 点线面函数、刷屏和图片显示进阶

4.1 画点函数必须设置可视窗口

往LCD写一个像素之前,通常需要先设置显示窗口区域,就是告诉控制器“我接下来要往哪个坐标范围里写数据”。不同的LCD控制器设置窗口的命令不一样,但思路一致:先写命令,再写坐标参数,最后连续写像素。

我这里画点函数比较粗暴,每次画点时重新设置窗口区域,只包括一个像素点。对于几十个点没问题,但如果全屏逐点画,窗口设置的开销会让性能很难看。所以进阶做法是:上层在进入一帧渲染前设置一次区域,然后连续写所有像素点,不频繁切换窗口。

代码逻辑大概是:

void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { LCD_SetWindow(x0, y0, x1, y1); for (uint32_t i = 0; i < (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); i++) LCD_WriteData(color); }

这个矩形填充函数是后续显示图片、显示汉字、滑块进度条的基础,性能瓶颈都在那个连续写数据的循环里。

4.2 利用DMA搬运整帧数据

说到刷屏,我强烈建议把FSMC和DMA结合起来。DMA能够把内存里的一块连续数据不断写入LCD_RAM_ADDR,这个过程不用CPU干预,刷新一帧图片时CPU基本还能跑其他任务。

配置DMA时要注意两点。数据宽度必须和FSMC的数据宽度匹配,也就是16位;地址递增方式要设置对,源地址递增,目标地址固定,因为LCD数据端口地址是不变的。方向是内存到内存或内存到外设,取决于你的DMA视图。如果你把LCD_RAM_ADDR当作一个外设地址来用,那就选择内存到外设。

整个DMA发送过程完成后,记得检查传输完成标志。尤其是连续多帧动画刷新时,上一帧还没传完下一帧就启动,数据会全部乱掉,表现就是画面撕裂、颜色错乱。这种问题很难一眼看出来,我排查时花了不少时间才定位到是没有等待上一次传输完成。

4.3 图片取模与显示时的颜色格式坑

嵌入式里大多数TFT屏默认用的颜色格式是RGB565。图片取模工具输出的也是RGB565,但如果LCD控制器的像素格式设置成了RGB666或者其他格式,屏幕颜色会明显发紫发绿。所以设置像素格式命令前要仔细检查RGB比特位顺序。

这里面还有一个容易踩的坑:RGB565与GBR565顺序不同。同样是0xF800,在某些控制器上是纯红色,在另一些控制器上可能是黄色。如果全屏颜色都对就红色和蓝色对调,十有八九是取模工具的颜色字节序和LCD控制器不同,调一下取模设置里高低字节交换选项就解决了。

4.4 文字和GUI的显示思路

显示文字本质上也是显示图像,点阵字库取模后存成数组,每个字节的每一位映射到一个像素点。用FSMC刷文字,比SPI接口快很多,滑动翻页和动态刷新都流畅得多。如果再结合帧缓冲,先在一大块内存里画好所有内容,然后一次性刷到屏幕,性能会再上一个台阶。

帧缓冲方案有一个取舍:MCU内部RAM可能不够大。240x320分辨率RGB565一帧需要约150KB,很多芯片RAM没这么大,需要外扩SRAM或者分块刷新。这个就需要结合自己项目实际情况来定了。

5. 常见问题与排查技巧实录

5.1 上电白屏

白屏通常代表LCD控制器没有正常初始化,或者背光能亮但显示内容一片空白。先从最简单的排查起:复位引脚是否正确拉高、复位延时是否够长。接着检查NE片选有没有生效,可以用示波器测CS脚有没有在发送命令时出现低电平脉冲。如果这个脉冲都没有,说明FSMC的外设时钟、引脚复用或者地址映射根本没配对。

我遇到过一次特别隐蔽的情况:把LCD_REG_ADDR和LCD_RAM_ADDR定义反了,写命令变成了写数据,初始化序列全乱,屏幕亮但始终是白屏。后来用调试器读寄存器值,再对照地址宏定义,才发现低级错误。

5.2 颜色错乱、出现横条纹

如果屏幕能显示内容但颜色不正常,优先怀疑数据总线接线和时序参数。颜色低分量和高分量颠倒是字节序问题;整幅画面发蓝发绿可能是RGB格式配置错误;刷新时出现横条纹则可能是LCD像素时钟设置和FSMC写入速度不匹配。

横条纹还有一种常见原因:控制器在扫描一帧的过程中,MCU写入的数据来不及被消化,即“撕裂”。解决办法是开启LCD控制器的同步信号或采用TE引脚同步机制,或者让写入频率略低于面板刷新节奏。当然,对于静态显示,这种条纹几乎不会出现,基本是动画、滚屏时比较明显。

5.3 读取ID失败

读取LCD控制器ID是判断硬件接线是否正确最有效的自检手段。但读ID很容易翻车,因为很多控制器要求在读取之前先设置一个无操作或者空读周期。我的做法是先发送一条读取ID命令,然后连续读三次数据,第一次丢掉,后三次取有效数据。

如果你用的LCD控制芯片ID读出来始终是0xFFFF,先别怀疑控制器坏了,用万用表确认一下RS引脚的电平变化是否和地址访问对应,尤其是地址线偏移有没有像前面说的差一位。

5.4 调试总结表格

现象最可能原因排查方向
白屏、无任何反映复位时序不对、片选未使能测RESET脉冲、测CS电平、检查地址宏定义
屏幕有条纹但不完整FSMC写入太快、窗口设置错误调大时序参数、检查窗口坐标命令
颜色明显不对RGB格式不匹配、字节序颠倒检查像素格式寄存器、取模高低字节序
写入偶发乱码时序余量不足、总线竞争增大DataSetupTime、检查DMA完成等待
刷新撕裂数据写入和LCD扫描不协同使用TE同步或用DMA降低占用时间

6. 一点经验之谈

这套实验跑到FSMC这一篇,算是把MCU并行外设和显示设备的交互彻底打通了。回过头来看,我觉得最有价值的不是屏幕点亮那一刻的成就感,而是你第一次意识到,原来外设控制器可以把复杂的时序协议抽象成简单的内存读写。这个思路放到其他外设上同样通用,比如外部NOR Flash启动、FPGA并行通信,都属于同一类心智模型。

最后再分享一个我实际项目中用的技巧:如果你经常要调LCD驱动,建议预留一个调试小函数,可以随时在屏幕上显示几个十六进制数字,比如当前系统运行状态、最近一次错误码、DMA传输剩余计数等等。这比接一根串口线输出调试信息直观很多,尤其在现场没有上位机的时候,帮了大忙。FSMC驱动LCD本身不复杂,但稳定可靠的显示系统需要在硬件、时序、软件架构三方面都考虑周全,希望这篇内容能帮你少走一段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询