LVGL移植1.8寸TFT全流程:ST7735S初始化、SPI配置与避坑指南
简介面向STM32F10x平台的LVGL图形库移植资料适配中景园1.8寸128×160分辨率显示屏。资源适合嵌入式入门及物联网设备开发者用于解决小内存MCU上构建现代图形界面的难题涵盖从底层驱动到上层控件的完整链路。压缩包共412个文件体积约3.86MB以C源文件和头文件为主204个.c、183个.h同时包含Keil工程脚本、字体资源如simsun、montserrat系列、链接文件及说明文档可直接导入工程编译调试。项目结构按CORE、HARDWARE、Middlewares、USER和STM32F10x_FWLib分层清晰展示驱动适配、LVGL中间件与用户代码的衔接关系。描述中详述了接口判断、SPI时序编写、显示参数配置和驱动注册等关键步骤配套工程可辅助读者从零搭建移植环境理解lv_font字体集成、颜色深度设置及控件渲染流程。已有811人学习下载适合需要快速上手嵌入式GUI开发的工程师参考。1. 先搞清楚移植1.8寸128x160的LVGL代码搬的究竟是哪几层把1.8寸128x160的LVGL代码搬到一个新主控工程里很多人第一反应是“改一下引脚定义就能跑”。实际拆过这套代码你会发现引脚定义是整个移植里最不费脑子的一步真正花时间的是LVGL版本匹配、SPI时钟配置、flush回调的时序配合以及屏幕驱动IC的颜色字节顺序。这份资源不是一个跑马灯demo而是一套能直接套用的最小工程结构适合正在做小尺寸人机交互界面、手里有同型号驱动IC屏幕、想快速出界面的开发者。跟着文章把初始化序列、显示缓冲和DMA刷新梳理一遍基本能避开九成翻车点。2. 1.8寸屏的驱动逻辑ST7735S初始化序列与LVGL显示缓冲桥接2.1 先认清驱动ICST7735S和它的初始化序列1.8寸128x160屏的驱动IC大概率是ST7735S。它内部集成了GRAMSPI接口只做写入主控不需要维护整幅像素数据。初始化序列的目标是把IC内部寄存器振到正常显示状态内容包括软件复位、退出睡眠、设置像素格式、设置扫描方向和关闭/开启反色。顺序错了后面全白搭。#define LCD_W 128 #define LCD_H 160 static void lcd_init(void) { // 背光先拉高避免初始化期间是黑屏状态 lcd_bl(1); // 复位RES 拉低 20ms再拉高等 120ms lcd_res(0); delay_ms(20); lcd_res(1); delay_ms(120); // 先软件复位再退出睡眠 lcd_write_cmd(0x01); // SWRESET delay_ms(120); lcd_write_cmd(0x11); // SLPOUT delay_ms(120); // 设置像素格式为 RGB565 lcd_write_cmd(0x3A); lcd_write_data(0x05); // MADCTL控制扫描方向和 RGB/BGR 顺序 lcd_write_cmd(0x36); lcd_write_data(0x48); // 开显示 lcd_write_cmd(0x29); // DISPON delay_ms(10); }这段初始化看起来短但每一条命令都有讲究。0x01和0x11之间的延时必须给足有些屏厂家的初始化序列会把延时缩短到10ms省掉后偶尔能显示偶尔白屏非常不稳定。0x36寄存器的bit5控制RGB还是BGR置0是RGB顺序置1是BGR顺序。0x48这个值是“默认扫描方向RGB”如果你的屏显示颜色不对先改这里不要急着在LVGL里调色。屏幕型号相同但不同批次颜色顺序也可能不一样把它做成宏或者外部变量会省很多事。写命令和写数据的区别在于DC引脚电平DC0表示当前SPI字节是命令DC1表示是数据。这个电平切换要在字节发送前完成常见实现是static void lcd_write_cmd(uint8_t cmd) { lcd_dc(0); // DC 拉低进入命令模式 spi_write(cmd, 1); } static void lcd_write_data(uint8_t data) { lcd_dc(1); // DC 拉高进入数据模式 spi_write(data, 1); }有些代码把DC引脚挂在硬件SPI的MOSI复用上省一个GPIO但调试时会很麻烦。我一般把DC、CS、RESET全挂在普通GPIO因为不同屏厂的引脚顺序经常不一样灵活配置比省引脚更实用。2.2 LVGL为什么需要flush_cb显示缓冲区的桥接逻辑LVGL内部渲染完成后不直接操作屏而是准备好一份颜色缓冲调用注册的flush回调。显存缓冲区越大LVGL能往屏上一次性刷的区块越大。1.8寸屏只有128x160像素RGB565整幅需要40KB多数MCU不敢开这么大常用8KB到16KB做局部缓冲。这个取舍直接决定刷新速度。static lv_disp_draw_buf_t disp_buf; // 显示缓冲对象 static lv_color_t buf[128 * 16]; // 16 行缓冲约 8KB static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint16_t w lv_area_get_width(area); uint16_t h lv_area_get_height(area); // 把需要刷写的区域映射成屏上 GRAM 地址窗口 lcd_set_addr_window(area-x1, area-y1, area-x2, area-y2); // 一次性写 w*h 个像素每像素 2 字节 lcd_write_pixels((uint8_t *)color_p, w * h * 2); // 通知LVGL缓冲已释放可以继续渲染 lv_disp_flush_ready(drv); }area参数告诉回调这次要刷哪一块矩形区域。LVGL每次只渲染变化区域不会傻到整屏重绘。lv_disp_flush_ready是整个桥接的关键它告诉LVGL这块缓冲已经用完可以继续往里面画下一帧。如果漏了这行LVGL会一直等缓冲区释放界面卡在第一帧不动而且不报任何错误非常隐蔽。缓冲行数选16还是32取决于可用RAM。12816的缓冲约4KB12832约8KB。缓冲区越大一次能处理的渲染区域越大刷屏的SPI事务次数越少但内存占用翻倍。我一般建议先开16行跑通后再试着往上加。RAM紧张时用LV_DISP_BUF_MODE_PARTIAL配合16行缓冲配合DMA也能做到流畅的动画。3. LVGL版本差异与移植选型先对版本再动手改代码3.1 版本决定配置写法v8和v9差别不小很多人移植失败是因为代码包和当前LVGL版本对不上。从v8到v9display驱动的初始化和回调签名都有变化。如果拿着老代码硬套新版本编译报错会集中在lv_disp_drv_t附近新手很容易在这步放弃。// v8 的写法 lv_disp_drv_init(disp_drv); disp_drv.flush_cb disp_flush_cb; disp_drv.hor_res 128; disp_drv.ver_res 160; lv_disp_drv_register(disp_drv); // v9 的写法 lv_display_t *disp lv_display_create(128, 160); lv_display_set_flush_cb(disp, disp_flush_cb); lv_display_set_buffers(disp, buf, NULL, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL);v9最大的变化是把“显示设备”抽象成lv_display_t对象函数名前缀从lv_disp变成lv_displayflush回调的签名里也没了drv参数。如果代码包里是v8版本而你的工程拉的是v9源码直接改这两个地方能解决大部分编译错误。下面是几个最容易踩的差异点对比项LVGL v8LVGL v9注册显示设备lv_disp_drv_registerlv_display_create渲染模式枚举LV_DISP_BUF_MODE_PARTIALLV_DISPLAY_RENDER_MODE_PARTIALflush回调参数带disp_drv指针带lv_display_t *颜色格式LV_COLOR_DEPTHLV_COLOR_DEPTHlv_color_format_t如果你用的主控RAM不大建议先用v8跑通因为v9内部封装更重对资源需求略高。如果是从零开始的新工程直接上v9避免后期再迁移。3.2 时钟源怎么选SysTick、定时器中断还是RTOS tickLVGL的心跳源必须稳定误差大了会导致动画时序错乱、进度条拖影。常见做法是SysTick。很多主控的SysTick默认被HAL库占用改起来要小心。void SysTick_Handler(void) { lv_tick_inc(1); // 每个 tick 累加 1ms }如果SysTick已经用于其他实时任务改用硬件定时器void TIM_IRQHandler(void) { lv_tick_inc(1); // 1ms 中断里喂一次心跳 }心跳中断优先级建议调高否则被其他中断打断太久LVGL会以为时间停了动画会突然跳帧。主循环里周期性调用lv_timer_handler()处理任务频率5ms或10ms都行不要太快也不要太慢。调5ms时动画顺滑一些但MCU负载会上升调10ms对静态界面足够。4. 具体移植步骤把这套LVGL代码接进工程4.1 硬件接线与引脚定义先确认屏引脚定义。这套代码默认使用SPI接口接线顺序如下屏引脚说明主控GPIOSCL/SCKSPI时钟主机输出SCKSDA/SDISPI MOSI只写不读MOSICS片选低电平有效CSDC/RS命令/数据选择DCRESET复位RSTBL背光BL注意这款屏不需要接MISO因为ST7735S基本不支持像素回读。如果主控的SPI外设是双向的把MISO空着就好。把DC、CS、RST挂在普通GPIO上方便后续换屏时改脚位。4.2 lv_conf.h配置要点LVGL工程的配置集中在lv_conf.h下面几个宏直接影响这块屏能不能正常显示#define LV_COLOR_DEPTH 16 #define LV_HOR_RES_MAX 128 #define LV_VER_RES_MAX 160 #define LV_COLOR_16_SWAP 1 // 高低字节换序LV_COLOR_DEPTH必须和屏幕驱动IC的RGB565格式一致。LV_HOR_RES_MAX和LV_VER_RES_MAX决定LVGL渲染时考虑的屏幕尺寸设大了会浪费RAM设小了显示区域会被裁切。LV_COLOR_16_SWAP是最常改动的宏之一打开后LVGL输出的RGB565字节序会交换作用是解决部分屏厂MADCTL配置不变时颜色发红发蓝的问题。SPI时钟不要一上来拉满先用8MHz验证跑稳了再往16MHz、20MHz升。4.3 main.c初始化顺序与主循环初始化顺序错了会出现“屏幕能亮但界面不出来”“界面花屏”之类问题。参照下面这个顺序int main(void) { hal_spi_init(8 * 1000 * 1000); // SPI 8MHz 起步 lcd_gpio_init(); lcd_init(); // 屏幕初始化序列 lv_init(); // LVGL 核心初始化 lv_disp_draw_buf_init(disp_buf, buf, NULL, sizeof(buf)); lv_disp_drv_init(disp_drv); disp_drv.hor_res LCD_W; disp_drv.ver_res LCD_H; disp_drv.flush_cb disp_flush_cb; lv_disp_drv_register(disp_drv); demo_create_ui(); // 放一个 Label 进度条 while (1) { lv_timer_handler(); // 驱动动画与输入处理 delay_ms(5); } }顺序不能乱硬件初始化 → 屏幕初始化 → LVGL初始化 → 注册显示设备 → 创建UI → 循环调用lv_timer_handler。如果先创建UI再注册显示驱动第一次渲染时LVGL还没有可用的显示设备界面会卡死。屏的地址窗口函数lcd_set_addr_window是flush回调的核心实现如下static void lcd_set_addr_window(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { lcd_write_cmd(0x2A); // CASET列地址 lcd_write_data(x1 8); lcd_write_data(x1 0xFF); lcd_write_data(x2 8); lcd_write_data(x2 0xFF); lcd_write_cmd(0x2B); // RASET行地址 lcd_write_data(y1 8); lcd_write_data(y1 0xFF); lcd_write_data(y2 8); lcd_write_data(y2 0xFF); lcd_write_cmd(0x2C); // RAMWR准备写像素 }这个函数把LVGL的坐标系映射到ST7735S的GRAM行列地址。地址窗口设置错一个像素画面会整体偏移或出现条纹。x1、x2、y1、y2必须严格用area值不要自作主张加减偏移除非屏本身有坐标偏移问题。5. 排查与避坑这五个坑占了八成翻车现场5.1 白屏复位时序和片选优先级现象上电后屏幕一直白屏背光亮但不显示任何内容。原因RES复位时序太短或CS没被拉低初始化命令根本没进IC。解决RES低电平保持至少10ms拉高后延时至少120ms。在每次spi_write前把CS拉低写完再拉高。有些SPI驱动会在片选拉低前发几个空字节也会导致首条命令丢失。5.2 颜色错乱MADCTL的BGR位和字节交换现象红色显示成蓝色白色正常整体界面颜色偏得很奇怪。原因驱动IC默认RGB顺序与LVGL输出的RGB565不一致或SPI字节序被主控翻转了。解决先改0x36寄存器的bit5再打开LV_COLOR_16_SWAP。两个开关组合起来试一遍最多四种组合基本能定位。不要同时怀疑多个地方那会把问题复杂化。5.3 花屏条纹地址窗口越界现象屏幕顶部或底部出现错位条纹滚动界面后越来越严重。原因flush回调里设置的坐标窗口超出屏幕范围或缓冲行数不是2的幂导致LVGL一次渲染的行数超过缓冲容量。解决在lcd_set_addr_window里打印area值对比确认x2和y2不超过159。缓冲行数改成16确保buf大小是128*16个颜色单元。明显条纹时先检查边界是不是多写了1像素。5.4 刷新残影和重影屏没有等待上一帧刷完现象动画快速切换时出现横向撕裂像画面被切成两半。原因单缓冲模式下渲染和发送叠在同一块缓冲屏幕还没传完上一帧下一帧就开始覆盖。解决要么改双缓冲要么在flush回调里加入“DMA传输完成等待”确保像素全部写出后再调用lv_disp_flush_ready。如果用的是DMA传输检查DMA中断是否在传输完成后才置标志位。5.5 LVGL版本带来的编译错误回调签名不一致现象代码包在旧环境编译通过换到新版本后到处报错报错集中在lv_disp_drv_t。原因v9改变了display API老的disp_drv.flush_cb不再兼容。解决先看头文件里lv_disp_drv_flush_cb_t的原型。如果已经改成带lv_display_t *参数的签名就按前面3.1节的lv_display_create写法重写初始化段。别硬改库源码那样下次升级还会踩。6. 验证与进阶把帧率量化驱动优化方向界面能显示只是第一步“能显示”和“能流畅显示”之间差着一个可量化的帧率。我移植完屏驱后第一件事不是欣赏动画效果而是加一个简单的FPS统计volatile uint32_t frame_cnt 0; static uint32_t last_ms 0; static uint32_t last_frame 0; while (1) { lv_timer_handler(); frame_cnt; if (hal_get_tick_ms() - last_ms 1000) { printf(fps%lu\n, frame_cnt - last_frame); last_frame frame_cnt; last_ms hal_get_tick_ms(); } delay_ms(5); }这个统计直接放在lv_timer_handler附近统计调用次数反映的是LVGL渲染频率不是屏幕实际刷新率。如果FPS低于20优先检查SPI时钟再拔高然后是缓冲大小。单缓冲模式下LVGL在渲染当前帧时不能发送上一帧性能天然受限换成双缓冲后渲染和SPI发送可以重叠通常能明显提升FPS。进阶方向有三个第一打开DMA传输flush回调里只做启动发送和等待完成标志CPU不用在SPI发送上死等第二把缓冲行数从16翻到32减少SPI事务次数第三如果界面刷新区域很小LVGL的局部刷新机制会把大部分时间省下来让动画更顺滑。别盲目上双缓冲先确认单品缓冲帧率瓶颈在发送还是渲染。最后提一句之前移植这块屏时我全程用肉眼判断动画顺不顺结果换个主控平台后问题全暴露了。从那以后我每次做完移植都会先跑一遍FPS统计确认基线再谈优化。希望帮到你。本文还有配套的精品资源点击获取