恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32用CubeMX+HAL库移植正点原子LCD例程:FSMC并口屏驱动实战
首页
资讯中心
/
STM32用CubeMX+HAL库移植正点原子LCD例程:FSMC并口屏驱动实战
STM32用CubeMX+HAL库移植正点原子LCD例程:FSMC并口屏驱动实战
发布时间:2026/10/3 1:11:25
说实话我一开始接到这个活儿的时候是有点犹豫的。正点原子的LCD例程在STM32圈子里几乎是“开箱即用”的代名词标准库版、寄存器版一大堆能跑、能用、资料全我为什么要费劲用CubeMX和HAL库重新造一遍轮子但真把这个工程复现完我觉得这件事比想象中值得做。原因有三一是很多新项目从CubeMX起步HAL库工程里想直接塞正点原子的标准库例程会遇到一堆预期外的“水土不服”二是正点原子例程里有大量值得学习的底层设计——比如FSMC映射、RS地址线选通、读ID判屏——这些不看源码很难吃透三是自己做一遍之后屏幕型号、引脚、时序参数全在掌控里换板子、换屏幕都能快速适配不用每次去翻例程猜原理。这篇文章我按自己完整实操的路径来写先从例程架构拆起再讲CubeMX里怎么配置FSMC和时钟接着是代码移植和关键函数实现最后是调试实录和常见问题速查。代码基于STM32F407系列但F103的操作路径完全一致区别只在时序参数和引脚编号文中会单独标注。1. 先搞懂正点原子LCD例程是怎么拼起来的1.1 8080并口时序到底是什么东西TFTLCD模块内部其实是一块带控制器的液晶面板市面上主流的是ILI9341、ILI9488、ST7789之类。这些控制器对外提供一套类似内存总线的接口最常见的就是“8080并口”一组16根数据线D0-D15加上CS片选、RS命令/数据选择也叫DC、WR写信号、RD读信号部分模块还有RST复位和BL背光。RS这根线决定了当前传输的是“命令”还是“数据”。比如我想告诉屏幕“设置显示窗口”就先拉低RS再在数据线上送出0x2A这个命令字节随后拉高RS再把坐标值通过数据线传进去。整个读写过程就像在和一个异步SRAM打交道CPU给出地址、产生读写脉冲数据就进去了。所以STM32里有FSMC外设的型号天然适合驱动这类屏。正点原子例程的底层核心就是围绕这个时序展开的但它并没有用GPIO去模拟而是把LCD接到了FSMC片上外设上。这也就是例程里你几乎看不到读写时序代码的原因——时序由硬件生成程序员只负责“往地址写值”。1.2 例程里的四层源码结构拆开正点原子的TFTLCD例程代码看着多实际上可以分成四层第一层是硬件初始化也就是LCD_Init函数前半部分负责复位屏幕、初始化FSMC外设、把背光引脚拉高。第二层是寄存器操作基元包括LCD_WR_REG、LCD_WR_DATA、LCD_RD_DATA所有后续操作全部建立在它们之上。第三层是控制器初始化序列LCD_Init里根据读回来的屏幕ID执行对应型号ILI9341、ILI9488、ST7789等的寄存器配置。第四层才是用户接口比如LCD_ShowChar、LCD_DrawPoint、LCD_Fill、LCD_ShowString、LCD_ShowChinese等。这个分层结构非常典型也适合我们自己移植。难点在第一层到第二层的桥接正点原子用的是FSMC地址映射方式把LCD设备当作一个外部存储器的Bank来访问命令区和数据区在地址空间上是错开的这需要认真理解硬件设计。1.3 为什么非要用FSMCGPIO模拟不行吗很多入门朋友会问LCD不就十几根线吗用GPIO翻转模拟WR脉冲不是更简单确实可以我早期玩51和普通STM32时也这么干过但问题在于如果直接翻转GPIO每写一个像素要拉低WR、置数据、拉高WR全手动一个16位像素往往要几十条指令刷一屏320x240约7.6万个像素CPU就被拖死了。而且GPIO模拟的时序抖动大、频率低容易遇到初始化不稳定或者屏幕闪烁的问题。FSMC就不一样。STM32的FSMC可以把外部设备映射到内部地址空间CPU执行*(uint16_t *)0x6C000080 color这类语句时FSMC硬件会自动产生地址、片选、WR/RD脉冲、数据总线的时序所有时序参数可以精确到HCLK周期。刷屏时CPU只需连续写内存测试下来比GPIO模拟快几十倍还为DMA刷屏、跑LVGL留了足够性能。一句话总结GPIO模拟适合“点个亮”FSMC适合“真正做产品”。这也是正点原子所有高配开发板都用FSMC接LCD的原因。2. CubeMX工程配置从新建工程到点亮背光用CubeMX复现的第一步不是写代码而是把工程配置搞对。很多人在这一步就翻车了引脚没配全、时钟不对、FSMC参数套错导致上电直接白屏然后怀疑人生。2.1 时钟树和引脚这些前置条件新建一个STM32F407VET6或你自己手上的型号工程后首先配RCC把HSE设为Crystal/Ceramic Resonator然后在Clock Configuration里把系统主频调到标称值F407就是168MHz。FSMC驱动LCD的时序参数都以HCLK为单位主频不准确后边的时序全都不对。接着配置SYS里的Debug为Serial Wire。这个很多人会漏漏了会导致板子上ST-Link连不上或者程序跑飞后没法重新下载。引脚这块要按开发板的原理图来不同板子的接线差异很大。以正点原子探索者F407为例LCD用的是FSMC Bank1的NE3地址线A6接LCD的RS数据线PD0-PD15接LCD的D0-D15RD和WR分别接PD4、PD5背光BL接PB15复位RST接PG12部分批次是PG15一定要查图纸。如果你用的是其他开发板比如战舰F103片选是NE1RS接的是A10基址和偏移都要改。在CubeMX的Pinout视图中把这些引脚功能全部选出来FSMC_NE3、FSMC_A6、FSMC_NOE、FSMC_NWE、FSMC_D0到FSMC_D15再加上GPIO输出口PB15和PG12。配置完成后CubeMX会自动生成引脚初始化代码。2.2 FSMC关键参数到底怎么填FSMC配置是重头戏。CubeMX里选中FSMC后添加一个NOR/PSRAM设备关键字段如下Memory type选SRAM因为8080接口和SRAM的时序模型最接近Data bus width选16 bitWrite operation勾选EnableExtended mode可以不开用同一个读写时序就行重点在Read/Write Timing参数正点原子标准库例程里F407的FSMC时序参数是AddressSetupTime2、AddressHoldTime4、DataSetupTime10、BusTurnAroundDuration5这些值在CubeMX里就是字面填写即可。F103因为主频只有72MHz常用值是AddressSetupTime1、AddressHoldTime15、DataSetupTime2、BusTurnAroundDuration1。时序参数STM32F103HCLK72MHzSTM32F407HCLK168MHzAddressSetupTime12AddressHoldTime154DataSetupTime210BusTurnAroundDuration15我建议第一次移植时不要自己发明参数直接抄例程验证过的组合把屏幕点亮后再微调。时序太紧会导致花屏或数据错误太松也看不出问题只会白白降低刷屏速度。2.3 工程生成的代码在哪里改CubeMX生成完代码后主流程在main.c里。FSMC初始化由MX_FSMC_Init完成GPIO初始化由MX_GPIO_Init完成。个人经验是不要在CubeMX生成的初始化函数里东改西改因为你下次重新生成代码时这些改动全会被冲掉。更好的做法是新建一个lcd.c和lcd.h把LCD的全部驱动代码放在这里main.c里只保留必要的初始化顺序。初始化顺序也很关键。正确顺序应该是先初始化GPIO和FSMCSystemClock_Config之后自动调用再执行LCD_Init里的复位序列最后设置背光高电平。如果先把背光点亮再复位屏幕你会看到几十毫秒的白光闪光虽然不影响使用但观感很差而且容易误以为初始化失败。3. 驱动代码移植把正点原子的lcd.c驯服到HAL库3.1 地址映射和读写宏的改写正点原子例程里最精妙也最劝退初学者的就是这个地址映射。以探索者F407为例LCD挂在NE3上FSMC的Bank1 NE3基址是0x6C000000RS接在FSMC_A6上。关键在于当CPU访问地址的某个特定bit变化时FSMC_A6这根线的电平也会变化。这里有个16位总线下的地址偏移陷阱。FSMC内部地址线和外部地址线不是一一对应的访问一个16位数据时内部地址会“右移”一位再送到外部地址线。想让FSMC_A6翻转CPU访问地址的第7位就要变化也就是偏移量是0x80不是很多人以为的0x40。所以在HAL库里可以这样定义#define LCD_BASE 0x6C000000UL #define LCD_CMD_ADDR (LCD_BASE) /* FSMC_A6 0命令区 */ #define LCD_DATA_ADDR (LCD_BASE 0x80) /* FSMC_A6 1数据区 */ #define LCD_CMD (*(volatile uint16_t *)LCD_CMD_ADDR) #define LCD_DATA (*(volatile uint16_t *)LCD_DATA_ADDR)如果你的板子RS接的是FSMC_A10那数据区偏移就是0x800写法是LCD_BASE 0x800。判断方法非常简单确认原理图上RS接到FSMC_A几就用1 (引脚编号 1)得到偏移量。读写基元函数写成内联方便编译器优化static inline void LCD_WR_REG(uint16_t reg) { LCD_CMD reg; } static inline void LCD_WR_DATA(uint16_t data) { LCD_DATA data; } static inline uint16_t LCD_RD_DATA(void) { return LCD_DATA; }正点原子标准库里的LCD_WR_DATA是LCD-LCD_RAM data本质和我这里的宏完全一样区别只是他们用结构体封装了这两个地址我用两个宏展开。理解这一点后你再去读正点原子的lcd.h和lcd.c会发现一点都不神秘。3.2 初始化序列和读ID的逻辑有了读写基元下一步是LCD的初始化。正点原子例程用一个联合体结构来存储当前屏幕的ID通过读取ID来决定执行哪套寄存器序列。ILI9341的ID读取方式通常是发命令0xD3然后连续读4个字节前两个是厂商ID和版本后两个能拼出0x9341。我简化实现了一下uint16_t LCD_ReadID(void) { uint16_t id 0; LCD_WR_REG(0xD3); /* ILI9341 读ID命令 */ LCD_RD_DATA(); /* 丢弃第一个字节 */ LCD_RD_DATA(); /* 丢弃第二个字节 */ id LCD_RD_DATA() 8; id | LCD_RD_DATA(); return id; }有的屏用的是0x04命令有的返回0x9488、0x7789这就是为什么正点原子代码里初始化前要先读ID。自己做项目时可以只支持手头这块屏把读到的ID打日志确认一下然后写死对应初始化序列即可。初始化序列的核心是几个关键命令。以ILI9341为例0x3A设置像素格式0x55表示16bit/像素这是跟后面画点函数密切相关的0x36设置扫描方向和RGB/BGR顺序0xAF~0x1F这一堆是电源、伽马、时序相关的厂家推荐值直接抄就行0x11退出睡眠模式之后要延时120ms0x29开启显示我在HAL库下把初始化函数组织成了这样void LCD_Init(void) { LCD_Reset(); LCD_WR_REG(0xCF); LCD_WR_DATA(0x00); LCD_WR_DATA(0xC1); LCD_WR_DATA(0x30); LCD_WR_REG(0xED); LCD_WR_DATA(0x64); LCD_WR_DATA(0x03); LCD_WR_DATA(0x12); LCD_WR_DATA(0x81); /* ... 中间省略若干组寄存器配置 ... */ LCD_WR_REG(0x3A); LCD_WR_DATA(0x55); /* 16bit 颜色格式 */ LCD_WR_REG(0x11); HAL_Delay(120); LCD_WR_REG(0x29); LCD_LED_On(); }注意一个小差别正点原子例程自己实现了delay_ms并且依赖滴答定时器。HAL库里直接用HAL_Delay即可但HAL_Delay以1ms为最小粒度初始化序列里有些要求“延时50us”的地方我用一个简单的for循环空转或者直接用HAL_Delay(1)糊弄过去实测对初始化来说问题不大。3.3 画点、画矩形和显示窗口的用法屏幕初始化完成后最基础的操作是画点。画点前要告诉LCD显示窗口的范围这对应0x2A列地址设置和0x2B行地址设置然后发0x2C开始写像素数据。void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { LCD_WR_REG(0x2A); LCD_WR_DATA(x 8); LCD_WR_DATA(x 0xFF); LCD_WR_REG(0x2B); LCD_WR_DATA(y 8); LCD_WR_DATA(y 0xFF); LCD_WR_REG(0x2C); LCD_WR_DATA(color); }这个函数一次只画一个点性能很一般。实际刷屏时更常用的是“先设窗口再连续灌数据”的方式。比如清屏或者填充矩形就可以这样void LCD_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { uint32_t total (uint32_t)(x2 - x1 1) * (y2 - y1 1); LCD_WR_REG(0x2A); LCD_WR_DATA(x1 8); LCD_WR_DATA(x1 0xFF); LCD_WR_DATA(x2 8); LCD_WR_DATA(x2 0xFF); LCD_WR_REG(0x2B); LCD_WR_DATA(y1 8); LCD_WR_DATA(y1 0xFF); LCD_WR_DATA(y2 8); LCD_WR_DATA(y2 0xFF); LCD_WR_REG(0x2C); while (total--) { LCD_WR_DATA(color); } }因为FSMC写一个16位数据就是一次内存操作这个while循环在168MHz下跑得飞快320x240整屏填充实测大概几十毫秒完全可接受。如果你想更激进可以加DMA和内存数组但那是后话。中文显示本质上也是“点阵画点”。正点原子例程里提供了大而全的字库但对初学者来说自己用取模软件生成字模放在一个const数组里然后按16x16点阵逐行画出来是最容易理解的方式。字模数组是16个16位数据每个bit代表一个像素为1就画前景色为0就画背景色或者跳过。取模的方向要和屏幕扫描方向一致不然字会横过来或者反着这是中文显示里最典型的坑。4. 上电调试实录白屏、花屏、反色一个个来代码写完上电那一刻才是最刺激的。我调这个例程时前后撞到了不少问题列出来给后面的人省时间。4.1 白屏是最容易踩的坑白屏通常在两种情况下出现一是FSMC配置没生效或者地址不对程序往LCD写数据时根本没操作到屏上二是复位和初始化序列顺序不对屏幕没退出睡眠模式。排查白屏我的顺序是先确认硬件复位正常。用示波器或者万用表量LCD的RST脚上电后应该能看到一个低脉冲再拉高。如果RST脚一直低电平检查GPIO配置是不是忘设了或者初始化顺序里复位代码没执行。很多模块的RST脚是必须由MCU控制的不是接个RC电路就完事。确认复位没问题后再查地址映射。这一步可以用调试器在初始化代码里加断点执行LCD_WR_REG(0x3A)后看FSMC的地址线有没有动作。如果RS、CS、WR的电平都对但就是白屏大概率是时序参数太紧或者太松。我遇到过一次把DataSetupTime从10改到4后直接白屏的情况改回去就好了。还有一个容易被忽略的是供电。TFTLCD模块背光和逻辑电路电流不小如果用杜邦线从开发板3.3V飞线接触不良时屏幕会表现为白屏加背光亮度不稳定。最好用开发板板载的LCD接口供电或者单独接可靠的电源。4.2 花屏和颜色错乱怎么定位花屏通常指向两类问题数据线/地址线接错或者RGB颜色格式设置不对。数据线D0-D15如果错位最常见的症状是显示内容像“撕裂”一样字符残缺、颜色花成一团。这种错位在PCB上很难查但在杜邦线连接时经常发生逐根核对16根线是个体力活建议先检查D0和D15这两根最容易接反的端点。地址线RS接错则表现为命令和数据全乱屏幕连纯色都显示不对。颜色错乱就更有意思了。如果你发现显示蓝色时变成了红色显示绿色正常大概率是RGB/BGR顺序反了。ILI9341的0x36命令第3位BGR位控制三原色通道顺序修改这个bit即可。另外0x3A设置16bit还是18bit也会影响颜色如果屏幕控制器默认18bit而MCU发16bit数据会出现颜色偏淡或者渐变过渡粗糙的问题这个可以在初始化序列里统一设置为0x55。读取ID和实际屏幕不匹配也会导致初始化序列完全跑偏。我有一个习惯初始化函数里把读到的ID通过串口打印出来确认屏的型号再跑对应序列省去大量盲目尝试时间。4.3 显示方向不对和镜像屏幕方向和坐标原点由0x36命令的6个bit控制分别是MY、MX、MV这三个旋转/镜像位加上BGR位。正点原子的例程里提供了LCD_Display_Dir函数其实就是对这个寄存器做位运算。如果你自己写初始化序列建议先确定屏的安装方向再一次性设置好不然后续所有画图函数的x、y坐标都得跟着改。镜像问题比较隐蔽。有的屏幕模组厂家默认扫描方向是从右到左、从下到上直接套用公版初始化序列显示出来是镜像的图形左右颠倒。遇到这种情况不要怀疑硬件去调0x36的MY/MX/MV位就行。4.4 常见问题速查表现象可能原因排查方法上电白屏FSMC时序过紧、复位信号异常、供电不稳用示波器检查RST和WR波形时序参数改回例程默认值显示花屏数据线D0-D15错位、地址偏移算错逐根核对数据线确认RS接FSMC_A几颜色反色/错乱RGB/BGR顺序反、像素格式不是16bit修改0x36的BGR位确认0x3A设为0x55图像镜像0x36扫描方向位配置不对调整MY/MX/MV位刷新闪烁背光PWM频率过低、刷屏无同步提高PWM频率或启用撕裂同步命令0x35字符扭曲取模方向和扫描方向不一致重新取模或调整0x36坐标方向4.5 再说说显示中文这件事标题里的“tftlcd显示中文”其实是个高频需求。实现中文显示无外乎两种路径一种是直接把字库存在MCU内部Flash或者外部Flash中按GB2312/GBK编码查表取字模另一种是用带中文字库的屏或者字库芯片比如正点原子的一些高级屏模块MCU只需要传编码过去屏自己就把字渲染出来了。第一种路径更通用但要注意存储空间。一个16x16点阵字模是32字节一级汉字3755个字就是约120KB对F407的1MB Flash来说没问题对F103的512KB也够呛但能装。取模软件有很多PCtoLCD2002是常见的选择取模方向选“阴码、逐行式、顺向”然后在代码里按行画点。如果临时不想做字库也有个取巧的办法用开源的小字库方案比如嵌入式方向常用的“u8g2_font”系列字体或者自己只内置几个常用字的数组。做设备菜单显示时这个方法非常实用。5. 后续还能怎么扩展DMA刷屏、LVGL和VSCode工作流LCD驱动跑通后千万别满足于“点亮了、显示个字符”。我自己的体会是这一步打通后整个工程的地基就稳了可以往上盖不少东西。刷屏性能上FSMC写数据本身不慢但CPU一个像素一个像素去填还是浪费。可以用内部SRAM做一块显存画图先在显存里完成然后通过DMA把整块显存搬到LCD的GRAM里。DMA配合FSMC时注意数据宽度必须都是16bit而且DMA传输完成中断里要清标志否则连续刷屏会掉数据。图形界面这块LVGL是目前最主流的轻量级方案F407跑LVGL FSMC并口屏完全可行。LVGL的底层只需要提供flush回调函数本质上就是把一块颜色缓冲区丢给底层底层再用FSMCDMA搬运到屏幕。这套架构跑起来后做产品界面比裸写画点函数效率高太多。开发环境方面CubeMX可以生成Makefile工程配合VSCode开发比KEIL轻量得多。CubeMX生成Makefile后用VSCode的Embedded IDE插件或者CMake插件打开代码补全、git管理都很方便。我自己后来做F407项目就是CubeMX VSCode Makefile的组合建议有命令行洁癖的朋友试一次。还有一个容易被忽视的点如果打算用LVGL这类GUI库初始化屏幕方向时尽量把坐标系设为横屏因为多数GUI库的默认逻辑是横屏布局避免后期在坐标转换上折腾。这次实操下来我最深的感触是正点原子的LCD例程不只是一个“能用”的驱动它更是一份很好的FSMC地址映射和LCD控制器初始化教材。用CubeMX复现它的过程本质上是把人家封装好的东西重新理解一遍。如果你也在做这件事记住一个原则——先看原理图确认RS和CS的接线再配置CubeMX最后才写初始化代码顺序别反了能省下大量排查时间。