STM32裸机移植LVGL到1.8寸ST7735S屏:配置、裁剪与踩坑记录

📅 发布时间:2026/9/8 3:09:45
STM32裸机移植LVGL到1.8寸ST7735S屏:配置、裁剪与踩坑记录
简介这是一份面向嵌入式开发者的LVGL移植工程源码包目标硬件为中景园1.8寸128x160 SPI接口LCD解决在STM32F10x平台上快速接入LVGL图形库并适配显示驱动的问题。工程采用Keil MDK组织含412个文件以C/H源码为主约387个另有Makefile、TTF字体、启动汇编、说明文档等包体仅3.86MB结构清晰HARDWARE存放LCD底驱Middlewares集成LVGL库USER负责初始化与显示配置STM32F10x_FWLib提供标准外设支持。内容覆盖显示驱动编写、颜色深度配置、字体资源加载及LVGL常用控件调用适合初学LVGL移植或想在1.8寸小屏上快速做出交互界面的开发者参考。由描述可见作者梳理了从接口确认、驱动适配到工程编译的完整流程可直接作为移植模板压缩包内还保留了Keil工程文件与字体文件方便按需修改。已有804人学习使用具备一定的实践参考价值。 手头这块1.8寸屏前前后后折腾了我好几个晚上。屏本身不贵中景园家的经典款128x160分辨率ST7735S驱动四线SPI接口资料包里带的是标准库工程下的裸机例程。网上LVGL移植教程一搜一大把但大部分跑在ESP32上剩下的不是半成品就是贴两句源码就没下文。真正面向裸机STM32、面向这种小分辨率屏的落地经验反而零碎得很。这篇博文我打算把“移植中景园1.8寸128x160屏的LVGL代码”这件事完整拆一遍哪些要改哪些要砍哪些是移植完才会踩到的雷。先说结论在STM32F103C8T6这种48KB RAM的芯片上LVGL v8完全跑得动UI响应流畅关键是配置要够“抠”。下面直接进正题。1. 先泼一盆冷水128x160的分辨率到底撑不撑得起LVGL1.1 小屏跑LVGL的真实收益很多人的第一反应是1.8寸这么小的屏跑LVGL是不是大材小用实际上128x160这个分辨率恰好是LVGL的舒适区。小分辨率意味着像素总量小128x160x2字节约40KB的帧缓冲总量放在那里即使不用外部SRAM也能在单片机的内存里玩出花来。收益在于三点。第一UI开发效率质变。裸机驱动做界面每个控件位置都要手算坐标改一个按钮位置可能牵扯十几处绘制代码。LVGL里一个lv_obj_set_pos就搞定而且自带事件回调、动画、样式表这些是裸机绘制很难低成本复刻的。第二视觉一致性。LVGL控件风格统一换主题只需改一个宏项目后期给客户演示时观感远好于拼凑的裸机界面。第三生态和调试工具。lv_simulator配合VSCode可以在PC上先调UI逻辑再交叉编译到板子不必每次烧录看效果。但也要泼盆冷水。小屏上跑LVGL内存就是最硬的约束。lv_conf.h里的LV_MEM_SIZE直接决定能创建多少控件和对象。给少了UI刚初始化就崩给多了剩下的业务代码内存不够用。这块屏的移植本质上是一场内存精算课不是简单地把官方demo搬过来就能跑。1.2 硬件选型F103C8T6够用但这些外设不能省中景园这块1.8寸屏是4线SPI接口片选、时钟、MOSI、复位、D/C各占一个IO背光一路。算下来一个普通SPI外设就够。我这里用的是STM32F103C8T6SPI1做主速度配到18MHz实测稳定。如果手上是F103RCT6甚至F407资源更宽裕移植思路完全一样。比较关键的一点是DMA。移植LVGL之后flush回调里一次要写几千字节到SPI如果纯靠CPU死等SPI发送完成主循环会被拖得很惨。所以选型时尽量挑有DMA的MCUF103全系都有SPI DMA只是通道映射不同配的时候翻一下参考手册的DMA请求映射表就行。另外一个容易被忽略的是定时器。LVGL需要1ms到10ms的tick心跳裸机工程里用SysTick最简单但如果你已经用SysTick做其他事就得腾一个基本定时器出来比如TIM2或TIM3。这个后面第3部分详细说。2. 中景园裸机驱动与LVGL的“翻译”从画点到整块flush2.1 原厂例程的核心函数边界中景园资料包里的裸机驱动核心抽象是LCD_Fill、LCD_DrawPoint、LCD_ShowChar这一组。它们的工作方式是一个点一个点往SPI写像素屏幕控制器内部根据列地址和行地址自增填充。这种模型在裸机界面上没问题但在LVGL里不够用。LVGL的显示驱动接口要求你提供一个flush回调它一次给出一块矩形区域的完整像素数据。换句话说LVGL不关心你会不会画点它只认“把这块区域的每一个像素颜色都给我送出去”。所以在移植时裸机驱动里真正有价值的是底层那几个操作LCD_SetWindow设置列地址和行地址窗口、LCD_WriteData_16Bit连续写像素、以及初始化序列本身。画点函数和字符函数可以全部丢进回收站它们工作了但工作方式不匹配。这个认知转换是整个移植的灵魂。很多人卡在“为什么我的flush回调写了数据屏幕就是没反应”根源就是还在用画点逻辑一次写一个像素区域窗口没设置对或者写完窗口之后没有切换数据模式。2.2 ST7735s初始化序列同一型号也有不同脾气ST7735S是一颗很成熟的控制器但不同批次、不同模块厂商给出的初始化序列差异很大。中景园资料包里的序列我这边验证可用但网上还有ST7735R、ST7735B的版本寄存器定义略有不同直接抄可能花屏。我手头这批屏用的初始化序列核心步骤是这样的软件复位SWRESET然后SLPOUT退出休眠等待120ms再配置FRMCTR1帧率、PWRCTR1/2/3电源控制、MADCTL扫描方向、COLMOD像素格式、伽马曲线最后DISPON开显示。COLMOD这步尤其重要必须写成0x05代表16位RGB565格式。如果这里错配成18位LVGL按16位颜色往显存写屏幕上会出现明显的偏色和颗粒感。序列里最后两行很关键涉及窗口设置中景园常见的是CASET从0x02开始、RASET从0x01开始。这个偏移值跟屏的玻璃切割和驱动IC出厂配置有关不是固定的。我在另一批同型号屏上就遇到过0x00偏移不出问题的版本。所以建议不要把初始化序列当成黑盒一把梭要理解哪几条是帧率、哪几条是电源、哪几条是窗口。2.3 坐标偏移排查全屏三色测试法移植过程中最磨人的一个现象是LVGL界面能显示但整体偏了几个像素或者屏幕右侧有一条竖线花边。这类问题基本定位在窗口偏移也就是CASET/RASET或者这里的坐标计算没对上。排查方法很简单初始化之后依次全屏填充纯红、纯绿、纯蓝三色观察是否有边缘异常。如果红色填充到屏幕顶部有1像素白边说明纵向偏移差了1。如果填充后右侧有横向条纹多半是列起始地址多了或少了。调的时候改初始化序列里的CASET数值或者在LCD_SetWindow里统一加一个固定的偏移量二选一推荐后者这样初始化序列保持原样所有上层调用都走同一套坐标换算后期换屏批次时只需改一个宏。这个三色测试必做不要跳。中景园这块屏虽然出货量大但同型号下不同批次的偏移差异我是真遇到过宁可多花五分钟排查也不要等整个UI画完才发现坐标全歪。3. 对接三板斧lv_conf.h裁剪、flush回调、心跳来源3.1 lv_conf.h里必须改的六处LVGL官方的lv_conf.h是全功能打开状态直接用肯定不行F103的Flash和RAM都兜不住。先把它从lvgl目录复制到工程根目录或者放到编译器能搜到的include路径里然后把LV_CONF_SKIP宏的依赖处理好保证这个配置文件被正确包含。移植到128x160这块屏有六处必须改。LV_COLOR_DEPTH改成16匹配RGB565。LV_COLOR_16_SWAP按屏幕字节序决定开不开后面4.1节细讲。LV_HOR_RES和LV_VER_RES分别设成128和160这是面板实际分辨率。LV_MEM_SIZE先给10KB到16KB跑通demo后再按实际控件量微调。LV_DPI设成75到100都可以它影响控件默认尺寸的缩放比例1.8寸物理尺寸小DPI调高一点控件不至于小到点不中。最后打开LV_USE_PERF_MONITOR这是调试阶段最值的开关后文实测调优会用到。除这六处还有一类开关值得顺手关掉LV_BUILD_EXAMPLES、LV_USE_DEMO_WIDGETS等demo宏正式移植时全部置0省Flash也省内存。小屏上LVGL的默认字体Montserrat 14保留即可其他大号字体按需开不要全开。3.2 flush回调的正确写法与DMA注意事项flush回调是LVGL和屏幕之间的唯一桥梁也是性能瓶颈所在。核心逻辑是根据传入的area区域调用LCD_SetWindow设置好ST7735s的窗口然后连续输出该区域的每一个像素颜色最后调用lv_disp_flush_ready告诉LVGL这帧数据已经送完。下面是参考实现static void flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w lv_area_get_width(area); uint32_t h lv_area_get_height(area); uint32_t size w * h; LCD_SetWindow(area-x1, area-y1, area-x2, area-y2); LCD_CS_CLR(); for (uint32_t i 0; i size; i) { LCD_WriteData16(color_p[i].full); } LCD_CS_SET(); lv_disp_flush_ready(drv); }这段代码能跑但如果屏幕刷起来有明显等待问题基本出在纯CPU循环等待SPI发送。改进方案是启用SPI DMA。具体做法是先把DMA通道配置好在flush回调里启动一次mem2periph传输然后立刻返回在DMA传输完成中断里调用lv_disp_flush_ready。注意一个细节lv_disp_flush_ready必须在所有像素数据确实写完SPI之后调用否则LVGL会认为缓冲区已经释放下一帧可能覆盖正在传输的数据造成闪屏或花屏。如果不想用DMA中断也可以用轮询标志位的半阻塞方式先把DMA配置好启动传输后while等待DMA完成标志再调用flush_ready。实测这个方式在18MHz SPI下已经能释放大部分CPU代码也简单不少。3.3 心跳裸机定时器和FreeRTOS任务两种接法LVGL内部所有动画、事件、控件刷新都依赖tick计数。移植时必须提供一个周期递增的时钟源核心函数是lv_tick_inc。裸机工程里最顺手的是在SysTick中断里每1ms调用一次lv_tick_inc(1)。如果SysTick已经被其他逻辑占用就用TIM2做1ms中断内容完全一样。FreeRTOS工程里有一个更优做法LVGL官方建议tick和task_handler放在两个不同优先级任务里tick任务每1到5ms调用lv_tick_incUI任务里循环调用lv_timer_handler并使用vTaskDelay让出CPU。我这里实际用的是TIM2中断负责lv_tick_incUI任务里每5ms调用一次lv_timer_handler这样tick周期稳定不受任务调度抖动影响。如果只靠FreeRTOS的vTaskDelay来产生tick调度器繁忙时会有明显的时间抖动动画会一卡一卡的。4. 小RAM平台的内存博弈颜色深度、缓冲块大小与LV_MEM_SIZE4.1 RGB565、RGB444与16位色深下的缓冲开销ST7735s支持RGB444和RGB565两种主要像素格式。RGB444每个像素12位但在SPI传输里通常按16位对齐实际没有省传输量颜色精度反而下降。所以这条路基本不考虑直接用RGB565这也是LVGL的LV_COLOR_DEPTH 16最顺滑的配合。有个细节非常坑RGB565在两个字节的排列顺序上屏幕控制器和LVGL的默认字节序可能不一致。如果你刷出来红色和蓝色对调或者颜色整体偏色先在lv_conf.h里打开LV_COLOR_16_SWAP试一下。这个宏让LVGL在生成像素数据时就交换高低字节避免在flush回调里每个像素手动swap。手动swap不是不行但在DMA传输场景下会影响数据流的连续构建能用宏解决的事尽量别用代码解决。色彩空间的另一个影响在内存占用。128x160分辨率下一行像素是128x2等于256字节半屏40行是10KB全屏是40KB。F103C8T6是48KB RAM全屏缓冲肯定放不下所以必须靠分块缓冲。4.2 双缓冲该设多大一行、半屏还是全屏LVGL的显示缓冲区可以是一个也可以是两个。单缓冲时LVGL在一块缓冲里渲染完成后调用flush送出送完再渲染下一块整个过程中渲染和传输是串行的。双缓冲时LVGL可以在UI任务里渲染第二块的同时让DMA异步送出第一块渲染和传输部分重叠帧率提升明显。在F103C8T6上我的配置是两块各128x40像素的缓冲区。128x40x2字节等于10KB一块两块共20KB加上LVGL内部的LV_MEM_SIZE 10到16KB以及系统栈、业务变量48KB RAM分配得比较紧但可行。缓冲块再小一点比如128x16也能跑但LVGL每次渲染区域被切得更碎绘制效率下降。缓冲块再大内存就彻底不够了。半屏双缓冲是我验证下来性能和内存最平衡的点。缓冲区初始化代码static lv_color_t buf_1[128 * 40]; static lv_color_t buf_2[128 * 40]; static lv_disp_draw_buf_t disp_buf; static lv_disp_drv_t disp_drv; void lvgl_port_init(void) { lv_init(); lv_disp_draw_buf_init(disp_buf, buf_1, buf_2, 128 * 40); lv_disp_drv_init(disp_drv); disp_drv.hor_res 128; disp_drv.ver_res 160; disp_drv.flush_cb flush_cb; lv_disp_drv_register(disp_drv); }如果你用的是ESP32这类RAM更宽裕的平台缓冲块可以加大到128x80甚至直接上全屏双缓冲效果更好。但本文面向的主要是STM32F103这类资源受限平台半屏已经够用。4.3 LV_MEM_SIZE怎么估demo能跑起来只是个开始LV_MEM_SIZE是LVGL内部动态分配的内存池大小它决定你能创建的控件总量。这个值没有公式只能按实际对象数量估算。一个基础label大概占几百字节一个button包含容器和样式表占1到2KB一个带图标的列表项更费。项目只要涉及3到5个页面、每页十来个控件12KB起步16KB比较稳。有个经验法则先把LV_MEM_SIZE调到16KB跑完整个UI流程包括创建、切换、删除页面然后把LVGL自带的分配统计打出来看峰值使用量。具体方法是打开lv_conf.h里的LV_USE_MEM_MONITOR在log输出里查看used percent。根据峰值再微调。不要一上来就追求大给业务代码留点余地。另外一个常见崩点在动态创建控件后忘记在页面销毁时释放对象。LVGL不会自动回收所有对象屏幕切换如果只创建不删除内存池迟早耗尽。对策是页面切换前统一调用lv_obj_clean或者lv_obj_del删除父容器这也是规范工程must-do。5. 在1.8寸屏上显示中文的两种务实路线5.1 用编译期字体子集按需裁字LVGL默认字体是Montserrat只覆盖ASCII。要显示中文有两条主流路线。第一条是用字体转换工具生成C数组字库编译期烧进Flash。LVGL官方工具或lv_font_conv都支持指定字符集不需要全量收录。操作方法是先准备一个中文字体文件比如思源黑体或文泉驿正黑然后运行转换命令指定需要的字符范围。典型命令是这样lv_font_conv --font SourceHanSansCN-Regular.otf --size 16 \ --format lvgl --no-compress \ --range 0x20-0x7F \ --symbols 设置温度电压电流功率开关机返回确定保存启动停止 \ -o my_font.c--range 0x20-0x7F包含ASCII可见字符--symbols指定项目里实际用到的汉字。这样生成的字体文件大小可能只有十几KB到几十KB放进内部Flash完全可行。使用时在LVGL里声明这个字体并设为控件样式LV_FONT_DECLARE(my_font); lv_obj_set_style_text_font(label, my_font, 0);要提醒一句如果项目界面是动态拼接的比如用户输入的文本、远程下发的告警内容那么编译期子集方案覆盖不到所有情况。这种场景就要走第二条路线。5.2 内部Flash字库和外部存储字库的取舍LVGL还支持从存储设备读取字体前提是你把字体数据放到外部Flash、SD卡或文件系统里。用lv_font_load加载后字体数据不会占用内部Flash但需要LVGL源码里打开LV_USE_FS_POSIX或对应的文件系统接口并实现lv_fs_drv_init相关底层。中景园这块屏模块上通常没带Flash芯片但如果你把屏接到带SD卡槽的开发板方案就成立。如果你的项目只是几个固定页面、菜单文案不变选第一种编译期方案就够了在我投的项目里这种方案占九成。只有涉及动态文本或者中英文切换轮播的场景才需要认真考虑外部字库。一个折中方案是把常用汉字子集编译器烧进Flash另一个更大字库放外部存储运行时按需加载这样两边优势都拿到但工程复杂度高一个级别不建议新手一上来就这么干。6. 调优复盘从“能显示”到“流畅显示”的三个关键改动6.1 SPI时钟与DMA传输的收益实测这批屏的ST7735s数据手册上限是15.12MHz左右但我实际把SPI时钟设到18MHz长时间运行没有出现花屏。不同批次体质有差异建议从较低频率开始测试逐步往上加。SPI时钟越高flush一帧所需时间越短界面的动画跟手度感知非常明显。DMA的收益我用一组数据说明。纯CPU轮询发送半屏128x40像素数据在18MHz SPI下一次flush大约消耗8到10ms的CPU时间期间主循环全部卡住。启用SPI DMA后同样的传输CPU占用几乎为零主循环能继续处理按键扫描、业务逻辑LVGL的timer_handler也能更及时地执行。这个提升在刷全屏背景图或者大面积滑块拖动时能清晰感知到强烈建议一开始就把DMA接入。SPI DMA配置时要注意F103的DMA通道映射SPI1_TX一般走DMA1_Channel3配好外设地址(SPI1-DR)、内存地址(buf)、方向为内存到外设数据宽度都设半字内存地址递增外设地址不递增。传输完在DMA中断里清标志并调用lv_disp_flush_ready注意不要在全速调试时频繁进出中断可以在中断里只做置标志然后由主循环统一处理。6.2 用内置性能监视器验证帧率而不是凭感觉调优要有客观数据靠肉眼判断“好像流畅了点”不靠谱。把lv_conf.h里的LV_USE_PERF_MONITOR打开LVGL会在屏幕左上角显示两个数字当前FPS和CPU占用率。这个小窗口在128x160的屏幕上会占掉一块区域但调试阶段完全能接受。我调试中发现一个很典型的现象某个页面单独切换时FPS能到50以上但列表滚动时掉到20以下。这时候性能监视器的CPU数值会明确告诉你瓶颈在哪。如果是CPU占用接近100%说明绘制耗在了软件渲染上可以考虑缩小缓冲块让LVGL更细粒度地分块刷新虽然单帧开销变大但每帧渲染时间变短动画反而更流畅。如果是CPU占用不高但FPS很低那瓶颈在flush传输优先检查SPI时钟和DMA配置。性能监视器还有一个附加价值它本身就在不断刷新一个小区域能直观验证局部刷新是否正常工作。如果FPS窗口区域出现残影或花屏说明你的flush回调区域设置有问题或者是DMA传输与lv_disp_flush_ready的时序没对齐。这个现象能帮你顺藤摸瓜解决一大批底层时序问题。6.3 撕裂、闪烁与局部刷新的观察记录小屏上撕裂问题不如大屏明显但不代表没有。表现是界面滚动或动画时屏幕中上部偶尔出现一条水平错位线这是显存写入和面板扫描不同步造成的。ST7735没有TE引脚的情况下处理撕裂最务实的思路是接受少量撕裂优先保证帧率。我实测中使用双缓冲加DMA后撕裂出现的概率已经很低肉眼基本不可见。如果项目对画面完整性要求高另一个思路是在flush回调里判断当前发送的是不是最后一帧数据通过lv_disp_flush_is_last接口在最后一段flush结束后延迟几毫秒变相等待面板扫描同步但也不能完全根治而且会降低帧率。闪烁问题则多半出在初始化时序和背光上。如果上电时屏幕先白屏再出画面检查复位脚时序和SLPOUT后的延时是否足够。如果刷新过程中有可见闪烁先确认是否开了双缓冲单缓冲在低SPI频率下闪烁会更明显。我踩过的一个坑是背光PWM频率设得太低导致界面刷新时视觉上像在闪把PWM频率提升到20kHz以上就消失了。这类问题往往被误判成LVGL配置问题实际上和图形库没有关系。移植中景园这块1.8寸屏的LVGL代码核心难点其实不在LVGL本身而在于裸机驱动和LVGL显示模型之间的思维转换以及小内存平台上的精打细算。跑通demo只是第一步把缓冲、心跳、DMA、字体这几块都理顺之后你会发现128x160的小屏能做的事情远比想象中多。最后分享两个我个人习惯拿到新屏先全屏刷三色确认没有坐标偏移再把工程里的LV_USE_PERF_MONITOR长期打开运行半小时看有没有周期性掉帧。这两步基础检查做扎实后面能省掉大量调试时间。本文还有配套的精品资源点击获取