当前位置: 首页 > news >正文

STM32F103驱动1.14寸ST7789彩屏的Keil工程源码(含SPI底层+LVGL显示支持)

本文还有配套的精品资源,点击获取

简介:这个资源包提供一套开箱即用的STM32F103单片机驱动1.14英寸ST7789彩色LCD屏幕的完整Keil MDK工程。硬件接口采用标准SPI通信,配套完整的底层驱动文件:spi.c负责高速数据传输,lcd.c封装屏幕初始化、命令写入与GRAM写入逻辑,delay.c基于SysTick实现精准延时,key.c和led.c集成常用外设控制,stm32f10x_usart.c支持串口调试输出。工程已整合LittleVGL图形库适配层(lv_port_disp.c)和GUI基础功能(GUI.c),可直接显示文本、几何图形、纯色填充及简单界面元素。所有代码基于STM32F1标准外设库编写,包含CORE核心启动文件、HARDWARE外设驱动目录和littleVGL子模块,TOUCH.uvproj工程结构清晰,无需修改引脚定义或时钟配置即可编译下载,在常见1.14寸ST7789模组(如带SPI接口的圆形/方形小屏)上稳定点亮并刷新画面。

1. 项目概述:为什么这个ST7789工程值得你花十分钟细读

如果你正在用STM32F103做一块带彩色小屏的嵌入式设备——比如一个温湿度监控面板、一个简易仪表盘、一个DIY电子时钟,或者只是想给你的开发板加点“视觉反馈”,那你大概率已经踩过这几个坑:SPI时序调不对,屏幕闪一下就黑;LVGL移植后显示错位或花屏;delay函数不准导致初始化失败;甚至引脚接对了,但lcd_init()一跑就卡死在while循环里。我试过不下七种ST7789模组,从淘宝9.9包邮的裸板到嘉立创打样的定制屏,发现真正能“烧进去就亮、改几行就动”的工程,不到三成。而这个资源包,就是那三成里的头一份。

它不是网上随手搜到的“ST7789例程合集”,也不是只贴了main.c就让你自己填坑的半成品。它是一套经过真实硬件验证、Keil MDK v5.38环境下全链路编译通过、在至少五种不同批次1.14寸ST7789模组(含带电容触摸的、不带触摸的、SPI CS高有效的、CS低有效的)上反复点亮并稳定刷新超过72小时的完整工程。关键词里写的“STM32F103, ST7789驱动, SPI彩屏, LVGL移植”四个词,每一个都对应着工程中一个被反复打磨过的技术断点:SPI底层不是简单调用SPI_SendData,而是做了时钟极性/相位动态适配、DMA空闲检测与手动清空缓冲区;ST7789驱动不是照抄数据手册寄存器表,而是把Reset、Sleep Out、Display On这些关键时序拆解成带毫秒级精度的延时组合;LVGL移植不是只改disp_flush回调,而是重写了lv_port_disp.c里的缓冲区管理策略,让160×128分辨率下帧率稳定在28fps以上;就连那个看似最简单的delay_ms(),也避开了SysTick_Config()在中断嵌套场景下的常见陷阱,改用独立计数器+状态机实现非阻塞延时备用接口。

适合谁?如果你是刚学完《STM32库开发实战指南》第6章的在校学生,这个工程能让你跳过“为什么SPI发送完没反应”的调试黑洞,直接看到屏幕上画出第一个矩形;如果你是做了三年工控HMI的工程师,你会注意到lv_port_disp.c里那个__attribute__((section(“.ram_code”)))修饰的flush函数——那是为了解决F103 Flash执行速度瓶颈,在RAM中运行关键图形刷屏代码的实操技巧;如果你是创客或产品原型开发者,你会发现HARDWARE目录下的key.c和led.c已经预留了长按识别、双击消抖、LED呼吸灯PWM占空比映射等接口,连串口调试日志都按模块分级(LCD_INIT / LVGL_MEM / TOUCH_CAL),不用再自己搭日志系统。它不炫技,但每行代码背后都有一次硬件复位、一次逻辑分析仪抓波形、一次OLED对比验证的痕迹。接下来,我们就一层层剥开这个工程的内核——不是讲“怎么用”,而是告诉你“为什么必须这么写”。

2. 整体架构设计与关键决策解析

2.1 工程分层逻辑:为什么坚持“外设驱动→屏幕封装→GUI抽象”三级结构

很多初学者拿到ST7789例程,第一反应是打开main.c看for循环里怎么发数据。但这个工程的骨架,是从CORE目录开始立住的。它没有用HAL库,也没有用LL,而是基于STM32F1标准外设库(V3.5.0)构建,原因很实在:F103资源紧张,HAL库默认启用的大量回调函数和句柄结构体,会吃掉近1.8KB RAM,而1.14寸ST7789的GRAM缓冲区(160×128×2字节)就要40KB,留给LVGL的堆空间本就不宽裕。标准外设库虽然写法略显“原始”,但每个GPIO_SetBits()、SPI_I2S_SendData()调用都是可预测的指令周期,内存占用精确到字节级——这对后续LVGL内存池分配策略至关重要。

整个工程采用清晰的三层隔离:

  • 底层硬件抽象层(HARDWARE):包含spi.c、lcd.c、delay.c、key.c、led.c、stm32f10x_usart.c。这一层只做一件事:把硬件操作翻译成无状态函数。比如spi.c里的SPI1_WriteByte(uint8_t data)不关心“这是发命令还是发GRAM”,只保证MOSI线上按时序输出8位;lcd.c里的LCD_WR_CMD(uint8_t cmd)也不管cmd是什么含义,只负责拉低CS、发cmd、拉高CS。这种设计让调试变得极其简单——当屏幕不亮时,你只需用逻辑分析仪抓SPI_CS和SPI_SCK,就能100%确认问题出在硬件连接还是软件时序。

  • 中间设备封装层(lcd.c为核心):这是最容易被忽视、却最影响稳定性的部分。ST7789的数据手册里写着“Reset脉冲宽度≥10μs”,但实际模组厂商的复位电路RC常数差异很大。工程里lcd.c的LCD_Reset()函数不是简单地GPIO_ResetBits()+Delay(10)+GPIO_SetBits(),而是做了三次阶梯式复位:先拉低50ms(确保彻底复位),再拉高5ms(等待内部LDO稳定),再拉低100μs(满足最小脉宽),最后等待150ms(留给OSC稳定)。这个序列是我用示波器在六块不同品牌模组上实测收敛出来的。同理,LCD_WR_DATA()函数内部做了自动判断:如果当前要写的是GRAM区域(0x2C命令之后的数据),则关闭SPI的NSS软控制,改用硬件NSS(PB0)并启用DMA;如果是普通寄存器写入,则走纯轮询模式。这种混合传输策略,让GRAM写入速度提升3.2倍,同时避免DMA传输未完成就触发下一次写入导致的花屏。

  • 上层GUI抽象层(littleVGL子模块):LVGL官方推荐的移植方式是修改lv_conf.h并实现lv_port_disp.c中的disp_flush和disp_fill等回调。但F103的RAM只有20KB,LVGL默认的disp_buf大小(160×128像素×2字节=40KB)根本放不下。工程里采取了“双缓冲+部分刷新”策略:在lv_port_disp.c中定义了两个160×32像素的disp_buf(各10KB),每次lvgl_tick_inc()触发刷新时,只计算脏矩形区域(dirty area),将该区域内像素拷贝到当前活跃buffer,然后调用SPI_DMA_Transmit()一次性刷屏。这样既规避了大buffer内存不足的问题,又通过DMA释放了CPU资源——实测在2MHz SPI速率下,单次160×32刷屏耗时仅18.7ms,CPU占用率从92%降至14%。

提示:不要试图把disp_buf扩大到160×128。F103的SRAM起始地址是0x20000000,而标准外设库的堆空间(_heap_start)默认从0x20001000开始。一旦disp_buf超过12KB,就会与malloc()分配区重叠,导致LVGL创建对象时随机崩溃。这个限制不是理论值,是我在调试LVGL控件树深度超过5层时,用J-Link Memory Browser亲眼看到的指针越界现场。

2.2 SPI通信方案选型:为什么放弃硬件NSS,改用软件模拟+DMA混合模式

ST7789支持三种SPI模式:Mode 0(CPOL=0, CPHA=0)、Mode 3(CPOL=1, CPHA=1),以及一种特殊的“四线SPI”(额外需要D/C引脚)。市面上90%的1.14寸模组默认使用Mode 3,但有12%的批次存在CPHA采样沿偏移问题——即数据在SCK上升沿采样,但模组内部锁存却发生在下降沿后20ns。如果直接用SPI硬件NSS(由SPI_CR1::SSOE控制),一旦DMA传输完成,NSS会自动拉高,此时若SCK还有残余脉冲,就可能被误判为新命令起始,导致屏幕进入未知状态。

工程最终选择“软件NSS + DMA + 手动清空”方案,具体实现如下:

  1. NSS完全由GPIO控制:在spi.c中定义#define LCD_CS_SET() GPIO_SetBits(GPIOB, GPIO_Pin_0)和#define LCD_CS_RESET() GPIO_ResetBits(GPIOB, GPIO_Pin_0),所有SPI操作前必先LCD_CS_RESET(),操作后LCD_CS_SET()。这牺牲了约0.8μs的总线切换时间,但换来100%可控的片选时机。

  2. DMA仅用于GRAM数据传输:SPI初始化时,SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE)只在LCD_WR_DATA()写入GRAM时启用。而命令写入(LCD_WR_CMD)和参数写入(LCD_WR_REG)全部走轮询模式——因为命令帧极短(通常1~3字节),DMA启动开销(约12μs)反而比轮询慢。

  3. 关键保护:SPI_SR寄存器手动清空:ST7789要求连续写入GRAM时,不能在SPI_SR::TXE标志置位后立刻写下一个字节,必须等待SPI_SR::BSY标志清零。但标准外设库的SPI_I2S_SendData()函数不检查BSY,只查TXE。工程在spi.c中增加了SPI1_WaitBusy()函数:

void SPI1_WaitBusy(void) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET) { // 空循环,但加了__NOP()防止编译器优化掉 __NOP(); } }

并在每次LCD_WR_DATA()调用后插入此函数。这个细节让我少花了两天排查“偶发性横条纹”问题——那是BSY未清零就发新数据,导致ST7789内部GRAM地址指针错位所致。

注意:不要在SPI1_WaitBusy()里加Delay_ms(1),那会把刷新率拖垮。F103主频72MHz,BSY标志通常在3~5个指令周期内清零,空循环足够。实测在72MHz下,该函数平均耗时仅0.12μs。

2.3 LVGL移植策略:如何在20KB RAM里跑通LVGL 7.11

LVGL 7.x版本对内存要求陡增,官方文档建议MCU至少有64KB RAM。但这个工程在F103(20KB SRAM)上稳定运行LVGL 7.11,核心在于三个非常规操作:

  • 内存池定制化分配:lv_conf.h中将LV_MEM_SIZE从默认的16KB改为12KB,并禁用LV_MEM_CUSTOM(即不使用malloc),改用静态数组:
static uint8_t lv_mem_buf[12*1024] __attribute__((section(".bss.lvgl"))); void * lv_mem_alloc(uint32_t size) { static uint32_t offset = 0; if(offset + size > sizeof(lv_mem_buf)) return NULL; void * ptr = &lv_mem_buf[offset]; offset += size; return ptr; }

这个方案把LVGL所有对象(lv_obj_t、lv_style_t等)都分配在指定内存段,避免与栈空间冲突。.bss.lvgl段在链接脚本中被明确放置在SRAM末尾(0x20004000起),远离main()函数栈(从0x20000000向上增长)。

  • 字体资源精简:默认LVGL加载的dejavu_16.bin字体文件达28KB,远超可用空间。工程改用自研的“num_12”字体(仅包含0-9、:、.、°等16个字符),二进制大小压缩至1.2KB。生成方法是用LVGL Font Converter工具,设置Width=12, Height=16, BPP=2(4级灰度),导出C数组后,手工删除所有非数字字符的glyph数据。

  • 禁用高开销特性:在lv_conf.h中强制关闭:

  • #define LV_USE_ANIMATION 0(动画消耗大量定时器和内存)
  • #define LV_USE_FILESYSTEM 0(F103无外部存储)
  • #define LV_USE_GPU 0(无GPU)
  • #define LV_COLOR_DEPTH 16(必须,ST7789是16位RGB565)

这三个改动,让LVGL初始化内存占用从18.3KB降至9.7KB,为GUI逻辑留出充足余量。

3. 核心模块详解与实操要点

3.1 lcd.c:不只是初始化,更是时序安全网

lcd.c是整个工程的“心脏起搏器”。它的价值远不止于点亮屏幕,而在于构建了一套可预测、可复现、可调试的硬件交互契约。我们逐个拆解关键函数:

LCD_Init() —— 初始化不是顺序执行,而是状态机驱动

很多例程把初始化写成一长串寄存器配置,但ST7789的初始化流程存在严格依赖:必须先完成Reset,再发Sleep Out,再设Gamma,最后Display On。任意一步失败,后续命令都会被忽略。工程采用状态机方式:

typedef enum { LCD_STATE_RESET, LCD_STATE_SLEEP_OUT, LCD_STATE_GAMMA, LCD_STATE_DISPLAY_ON, LCD_STATE_READY } lcd_state_t; static lcd_state_t lcd_state = LCD_STATE_RESET; void LCD_Init(void) { switch(lcd_state) { case LCD_STATE_RESET: LCD_Reset(); lcd_state = LCD_STATE_SLEEP_OUT; break; case LCD_STATE_SLEEP_OUT: LCD_WR_CMD(0x11); // Sleep Out delay_ms(120); // 数据手册要求≥120ms lcd_state = LCD_STATE_GAMMA; break; case LCD_STATE_GAMMA: LCD_WR_CMD(0x26); LCD_WR_DATA(0x01); // Gamma set // ... 其他Gamma寄存器配置 lcd_state = LCD_STATE_DISPLAY_ON; break; case LCD_STATE_DISPLAY_ON: LCD_WR_CMD(0x29); // Display On delay_ms(10); lcd_state = LCD_STATE_READY; break; default: break; } }

这种写法的好处是:你可以随时在main()里加while(lcd_state != LCD_STATE_READY) { LED_Toggle(); delay_ms(500); },用LED闪烁直观看到初始化卡在哪一步。我在调试某款山寨模组时,就靠这个发现它在Sleep Out后需要200ms等待(而非手册写的120ms),否则Gamma配置无效。

LCD_SetCursor() —— 光标设置暗藏分辨率陷阱

ST7789的GRAM寻址范围是0~159(X轴)和0~127(Y轴),但1.14寸模组的实际可视区域常有裁剪。工程里LCD_SetCursor(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)函数做了边界校验:

if(x1 > 159) x1 = 159; if(y1 > 127) y1 = 127; if(x2 > 159) x2 = 159; if(y2 > 127) y2 = 127; if(x1 > x2) x1 = x2; if(y1 > y2) y1 = y2;

更重要的是,它把坐标转换为ST7789原生命令:

LCD_WR_CMD(0x2A); // Column Address Set LCD_WR_DATA(x1 >> 8); LCD_WR_DATA(x1 & 0xFF); LCD_WR_DATA(x2 >> 8); LCD_WR_DATA(x2 & 0xFF); LCD_WR_CMD(0x2B); // Page Address Set LCD_WR_DATA(y1 >> 8); LCD_WR_DATA(y1 & 0xFF); LCD_WR_DATA(y2 >> 8); LCD_WR_DATA(y2 & 0xFF);

注意:这里y1/y2对应Page Address,x1/x2对应Column Address,顺序不能颠倒。曾有个同事把0x2A和0x2B写反,结果屏幕显示上下颠倒,折腾半天才发现是命令顺序错了。

LCD_Fill() —— 填充不是暴力循环,而是DMA加速

最常用的LCD_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)函数,内部逻辑是:
1. 调用LCD_SetCursor()设置区域;
2. 发送0x2C命令(Memory Write);
3. 启动DMA传输:将color值重复填充到DMA内存缓冲区(大小=(x2-x1+1)×(y2-y1+1)),然后调用SPI_DMA_Transmit()。

关键点在于DMA缓冲区的构建。工程没有用malloc动态分配,而是定义了一个静态数组:

#define MAX_FILL_SIZE 160*128 static __IO uint16_t dma_fill_buf[MAX_FILL_SIZE];

填充时用memset16()(自定义的16位memset):

void memset16(uint16_t *dst, uint16_t val, uint32_t len) { for(uint32_t i=0; i<len; i++) dst[i] = val; }

这样避免了动态内存碎片,且DMA传输时CPU完全解放。实测填充全屏(160×128)耗时仅210ms(SPI 2MHz),比纯轮询快4.7倍。

3.2 lv_port_disp.c:LVGL显示端口的“呼吸感”设计

LVGL的disp_flush回调函数,本质是告诉LVGL:“我已经把你要显示的像素画到buffer里了,现在请刷到屏幕上”。但F103的瓶颈不在画图,而在“刷”的过程。工程的lv_port_disp.c实现了三个关键优化:

双缓冲切换机制

static lv_disp_buf_t disp_buf_1; static lv_disp_buf_t disp_buf_2; static lv_color_t buf_1[LV_HOR_RES_MAX * 32]; // 160×32 buffer static lv_color_t buf_2[LV_HOR_RES_MAX * 32]; void lv_port_disp_init(void) { lv_disp_buf_init(&disp_buf_1, buf_1, NULL, LV_HOR_RES_MAX * 32); lv_disp_buf_init(&disp_buf_2, buf_2, NULL, LV_HOR_RES_MAX * 32); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = 160; disp_drv.ver_res = 128; disp_drv.flush_cb = disp_flush; disp_drv.buffer = &disp_buf_1; // 初始使用buf_1 lv_disp_drv_register(&disp_drv); } static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // 计算脏区域高度,决定刷多少行 uint16_t h = area->y2 - area->y1 + 1; uint16_t y_start = area->y1; // 只刷32行,超出部分分批刷 if(h > 32) h = 32; // 将color_p中h行数据拷贝到当前disp_buf uint32_t copy_size = LV_HOR_RES_MAX * h; memcpy(disp_drv->buffer->buf_act, color_p, copy_size * sizeof(lv_color_t)); // 设置GRAM区域 LCD_SetCursor(0, y_start, LV_HOR_RES_MAX-1, y_start+h-1); LCD_WR_CMD(0x2C); // 启动DMA刷屏 SPI_DMA_Transmit((uint8_t*)disp_drv->buffer->buf_act, copy_size * 2); // 切换到另一个buffer if(disp_drv->buffer->buf_act == buf_1) { disp_drv->buffer->buf_act = buf_2; } else { disp_drv->buffer->buffer = buf_1; } lv_disp_flush_ready(disp_drv); // 通知LVGL刷屏完成 }

这个设计让LVGL可以持续渲染下一帧,而不用等DMA完成——因为DMA是异步的。lv_disp_flush_ready()的调用时机非常关键:必须在DMA传输完成中断里调用,否则LVGL会认为刷屏失败而重绘,造成闪烁。工程在spi.c的DMA传输完成中断服务函数中调用了它。

抗撕裂同步机制
ST7789没有VSYNC引脚,但工程通过“垂直空白期”模拟同步:在每次disp_flush()开始前,插入delay_us(150),确保上一帧DMA传输已结束。这个150μs是实测得出的最小安全间隔——低于此值,偶发DMA缓冲区覆盖导致花屏。

3.3 touch.c:触摸不是必须,但加了就稳如磐石

虽然摘要里提到“触摸辅助模块”,但这个touch.c的设计哲学是:“不依赖触摸,但准备好触摸”。它不实现完整触摸校准,而是提供三个基础能力:

  • 触摸中断检测:通过EXTI_Line4(假设TP_INT接PA4)检测触摸按下,避免轮询浪费CPU;
  • 原始坐标读取:调用ADS7843或XPT2046的SPI读取函数,返回12位X/Y原始值;
  • 坐标映射模板:提供touch_map_to_screen(int16_t x_raw, int16_t y_raw, int16_t *x_out, int16_t *y_out)函数,内置四点校准系数(默认值针对1.14寸模组),用户只需在首次运行时用四个点(左上、右上、左下、右下)测出实际系数,填入宏定义即可。

关键代码片段:

#define TOUCH_X_MIN 120 #define TOUCH_X_MAX 3850 #define TOUCH_Y_MIN 180 #define TOUCH_Y_MAX 3720 void touch_map_to_screen(int16_t x_raw, int16_t y_raw, int16_t *x_out, int16_t *y_out) { // 线性映射:screen_x = (raw_x - X_MIN) * 160 / (X_MAX - X_MIN) *x_out = (x_raw - TOUCH_X_MIN) * 160L / (TOUCH_X_MAX - TOUCH_X_MIN); *y_out = (y_raw - TOUCH_Y_MIN) * 128L / (TOUCH_Y_MAX - TOUCH_Y_MIN); // 边界钳位 if(*x_out < 0) *x_out = 0; if(*x_out > 159) *x_out = 159; if(*y_out < 0) *y_out = 0; if(*y_out > 127) *y_out = 127; }

这个映射公式看似简单,但系数必须实测。我用游标卡尺测量模组有效触控区域为13.2mm×10.1mm,对应屏幕像素160×128,计算出理论系数后,在实际模组上用四点校准法微调,最终确定上述X_MIN/X_MAX值。没有这一步,触摸点会整体偏移20~30像素。

4. 实操全流程与关键参数详解

4.1 Keil工程导入与编译:从零到点亮的七步法

即使你从未用过Keil,按以下步骤操作,5分钟内必见屏幕亮起:

  1. 解压资源包:得到stm32f1_1.14_OK文件夹,内含TOUCH.uvproj工程文件;
  2. 安装必要组件:Keil MDK v5.25及以上(推荐v5.38),并安装ARM Compiler 5(不是ARMCLANG);
  3. 检查芯片型号:双击TOUCH.uvproj→ Project → Options for Target → Device,确认选择的是STM32F103C8T6(或你实际使用的型号,如F103CBT6、F103RBT6);
  4. 验证Flash配置:Options for Target → Utilities → Settings → Flash Download,确认勾选了STM32F10x High density算法(F103C8是Medium density,但Keil旧版算法兼容);
  5. 检查头文件路径:Options for Target → C/C++ → Include Paths,应包含:
    ..\CORE ..\HARDWARE ..\littleVGL\src ..\littleVGL\src\lv_core ..\littleVGL\src\lv_draw ..\littleVGL\src\lv_font ..\littleVGL\src\lv_hal ..\littleVGL\src\lv_misc ..\littleVGL\src\lv_objx ..\littleVGL\src\lv_themes ..\littleVGL\src\lv_widgets
  6. 编译前清理:Project → Clean Target,然后Project → Rebuild all target files;
  7. 下载与运行:点击Load按钮(或Ctrl+L),等待J-Link提示“Programming Done”,复位开发板,屏幕应在2秒内点亮。

注意:如果编译报错undefined symbol SystemInit,说明startup_stm32f10x_md.s文件未加入工程。右键Project → Manage → Project Items → Files,勾选CORE\startup_stm32f10x_md.s(MD表示Medium density,对应F103C8/CB系列)。

4.2 硬件连接对照表:引脚定义与物理接线

工程默认引脚定义(可在HARDWARE\lcd.h中修改):

STM32引脚ST7789信号说明
PB0CS片选,低电平有效
PA5SCKSPI时钟,模式3(CPOL=1, CPHA=1)
PA7MOSI主机输出,数据线
PA4DC数据/命令选择,高=数据,低=命令
PB1RST复位,低电平有效
3.3VVCC模组电源,必须接3.3V!
GNDGND共地

关键接线提醒
-DC引脚绝不可省略:有些模组标称“三线SPI”,实则是把DC接到固定电平(如VCC),但这样只能发数据,无法发命令,屏幕永远黑屏。必须用GPIO控制DC。
-RST引脚必须接:虽然ST7789支持上电自动复位,但F103上电时序不稳定,自动复位成功率<60%。实测外接RST并软件控制,点亮成功率100%。
-VCC务必用3.3V:ST7789核心电压是2.8V,IO耐压3.3V,但接5V会永久损坏。曾有同事用开发板5V给屏供电,当场冒烟。

4.3 SPI速率与稳定性实测数据

SPI速率不是越高越好。工程默认配置为2MHz(APB2=72MHz,分频系数=36),这是经过逻辑分析仪实测的平衡点:

SPI速率波形质量屏幕表现CPU占用率备注
1MHz完美方波,边沿陡峭正常,刷新稍慢8%最稳妥,推荐新手用
2MHz上升沿略有回沟(<10ns)完全正常,流畅14%工程默认值,兼顾速度与稳定
4MHz上升沿过冲明显(>15ns)偶发横条纹(约1/200帧)22%需加强PCB走线匹配
8MHz严重振铃,边沿模糊频繁花屏,无法使用38%F103 IO驱动能力已达极限

实测方法:用Saleae Logic 8抓SPI_SCK和SPI_MOSI,观察信号完整性。结论是:2MHz是F103+杜邦线+面包板环境下的黄金速率。若你用PCB板且走线良好,可尝试2.5MHz,但需重新测试所有模组批次。

4.4 LVGL界面开发入门:三行代码画出你的第一个UI

工程自带GUI.c,里面有一个GUI_CreateDemo()函数,展示了LVGL基础用法。你只需修改三处,就能做出自己的界面:

  1. 创建屏幕对象
lv_obj_t * scr = lv_scr_act(); // 获取当前屏幕 lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), 0); // 黑色背景
  1. 添加标签
lv_obj_t * label = lv_label_create(scr); lv_label_set_text(label, "Hello STM32!"); lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0); // 白色文字 lv_obj_align(label, LV_ALIGN_CENTER, 0, -20); // 居中,上移20px
  1. 添加按钮并绑定事件
lv_obj_t * btn = lv_btn_create(scr); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 20); lv_obj_t * btn_label = lv_label_create(btn); lv_label_set_text(btn_label, "Click Me"); lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); void btn_event_cb(lv_event_t * e) { lv_obj_t * label = lv_obj_get_child(lv_scr_act(), 0); // 获取第一个子对象(即label) lv_label_set_text(label, "Clicked!"); }

编译下载后,点击按钮,文字会实时变化。这就是LVGL事件驱动的魅力——你不需要轮询按键状态,LVGL帮你完成了所有底层交互。

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

5.1 屏幕不亮/全白/全黑:时序与供电的终极排查清单

这是最高频问题,按以下顺序逐项排除,95%的情况能在10分钟内定位:

现象可能原因排查方法解决方案
完全不亮(黑屏)1. VCC未接或接触不良
2. RST一直被拉低
3. CS未拉低
用万用表测VCC对GND电压;测RST引脚电压(应为3.3V);测CS引脚电压(初始化时应有3.3V→0V跳变)检查杜邦线;确认RST上拉电阻(10kΩ)已焊接;确认CS引脚定义正确
全白屏1. DC引脚接错(始终高电平)
2. Gamma配置错误
用逻辑分析仪抓DC信号,看是否随命令/数据切换;抓SPI波形,确认0x26命令后是否有0x01数据检查DC引脚连接;在LCD_Init()中临时注释Gamma配置段,看是否变黑
全黑屏(背光亮)1. Display On命令未发送
2. GRAM写入地址错误
抓SPI波形,确认有0x29命令;抓0x2A/0x2B命令后的数据,确认坐标在0~159/0~127范围内在LCD_Init()末尾加LCD_Fill(0,0,159,127,0xFFFF)强制全白;检查LCD_SetCursor()参数
闪屏(亮→黑→亮循环)1. SysTick中断被意外关闭
2. delay_ms()函数被编译器优化掉
在SysTick_Handler()里加LED翻转;在delay_ms(100)前后加LED开关,看LED是否真灭100ms在Keil C/C++选项中关闭”Optimize for Time”;确认SysTick_Config(72000-1)已调用

实操心得:我用示波器抓过上百次SPI波形,发现83%的“不亮”问题根源是DC引脚。因为DC信号频率低(每条命令1次),逻辑分析仪容易漏抓,所以最可靠的方法是:用万用表直流档,红表笔接DC,黑表笔接GND,手按复位键,观察电压是否在3.3V和0V间跳变。没有跳变?立刻查DC引脚定义和硬件连接。

5.2 LVGL显示错位/花屏:内存与缓冲区的隐形杀手

LVGL花屏往往比硬件问题更难定位,因为现象随机。以下是三个最隐蔽的致命原因:

原因1:disp_buf地址未对齐
LVGL要求disp_buf地址必须是4字节对齐(因涉及32位访问)。如果buf定义为uint16_t buf[160*32],起始地址可能是奇数。工程中强制对齐:

static lv_color_t buf_1[LV_HOR_RES_MAX * 32] __attribute__((aligned(4)));

排查方法:在Keil Debug模式下,打开Memory窗口,输入&buf_1,看地址末两位是否为00/04/08/0C。

原因2:LVGL tick未按时调用
LVGL需要每5ms调用一次lv_tick_inc(5),否则动画、事件超时全部紊乱。工程在SysTick_Handler()中实现:

void SysTick_Handler(void) { static uint32_t tick_cnt = 0; tick_cnt++; if(tick_cnt >= 5) { // 每5ms lv_tick_inc(5); tick_cnt = 0; } }

如果SysTick_Config()参数错误(如传入72000000而非72000),tick就不会触发。排查:在lv_tick_inc()里加断点,看是否命中。

原因3:SPI DMA传输未完成就刷下一帧
这是最狡猾的问题:DMA传输需要时间,但LVGL不知道。如果disp_flush()中调用SPI_DMA_Transmit()后立即返回,而DMA还在跑,LVGL就会把新数据写入同一块buffer,导致覆盖。工程解决方案是在DMA传输完成中断中调用lv_disp_flush_ready(),确保LVGL收到“刷屏完成”信号才进行下一步。

5.3 性能瓶颈突破:从28fps到35fps的实战优化

工程默认帧率28fps,但通过三项微调,可提升至35fps(提升25%):

  1. SPI速率从2MHz→2.5MHz:修改HARDWARE\spi.cSPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_28(28=72MHz/28≈2.57MHz)。需确认硬件无振铃。

  2. 减少LVGL脏区域计算开销:在lv_conf.h中将LV_REFR_PERIOD从30ms改为25ms,并增加#define LV_VDB_SIZE (160*128)(启用虚拟显示缓冲区,减少重绘面积)。

  3. 关闭LVGL抗锯齿#define LV_ANTIALIAS 0。ST7789是RGB565,抗锯齿效果有限,却消耗大量CPU。

实测数据:优化后,用逻辑分析仪抓LVGL刷新中断间隔,从35.7ms缩短至28.6ms,帧率提升至35fps。代价是圆角边缘略显锯齿,但对1.14寸小屏几乎不可见。

6. 进阶扩展与个性化定制指南

6.1 添加自定义字体:从TTF到嵌入式可用的三步转化

工程自带的num_12字体只支持数字,若需显示中文,必须添加新字体。以“思源黑体”为例:

  1. 字体裁剪:用FontForge打开思源黑体,删除所有非中文字符(保留常用3500字),导出为TTF;
  2. 转换为LVGL格式:用LVGL Font Converter(在线工具),设置:
    - Font file: 你的TTF
    - Size: 16
    - BPP: 2(4级灰度,平衡大小与效果)
    - Range: U+4F60-U+597D(“你好”Unicode范围)
    - Output format: C array
  3. 嵌入工程:将生成的C文件(如font_siyuan_16.c)加入工程,修改lv_conf.h
#define LV_FONT_DEFAULT &lv_font_siyuan_16 extern const lv_font_t lv_font_siyuan_16;

编译后,lv_label_set_text(label, "你好")即可显示。注意:一个16号中文字体文件约180KB,F103放不下,必须严格裁剪Unicode范围。

6.2 串口调试增强:用printf重定向实现模块化日志

工程已集成stm32f10x_usart.c,但默认只支持USART_Printf("msg")。要实现类似Linux的printk(KERN_INFO "LCD: init ok\n")效果,需重定向printf:

  1. main.c中添加:
int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {} return ch; }
  1. GUI.c中定义日志宏:
#define LCD_LOG(fmt, ...) printf("[LCD] " fmt "\r\n", ##__VA_ARGS__) #define LVGL_LOG(fmt, ...) printf("[LVGL] " fmt "\r\n", ##__VA_ARGS__) // 使用 LCD_LOG("Init done, SPI speed: %dMHz", 2);

这样,所有printf输出都会通过USART1发送,用串口助手即可看到带模块前缀的日志,极大提升调试效率。

6.3 低功耗改造:待机模式下屏幕休眠与唤醒

F103支持Stop Mode(电流<10μA),但ST7789需手动进入Sleep模式。改造步骤:

  1. lcd.c中添加:
void LCD_EnterSleep(void) { LCD_WR_CMD(0x10); // Sleep In delay_ms(5); } void LCD_ExitSleep(void) { LCD_WR_CMD(0x11); // Sleep Out delay_ms(120); }
  1. 在主循环中:
while(1) { if(key_pressed(KEY_UP)) { LCD_ExitSleep(); lv_timer_handler(); // LVGL刷新 } else { lv_timer_handler(); // 维持LVGL心跳 if(no_key_press_for_30s()) { LCD_EnterSleep(); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 进入Stop模式 } } }

唤醒后,LVGL会自动恢复,屏幕重新点亮。实测待机电流从8.2mA降至9.3μA,续航提升42倍。

我个人在实际项目中发现,ST7789模组的背光驱动芯片(通常是SY7200)比屏幕本体更耗电。如果对功耗极致敏感,建议在LCD_EnterSleep()后,用GPIO关断背光使能引脚(如有),可再降3mA电流。这个细节虽小,但在电池供电的物联网设备里,就是多撑三天的关键。

本文还有配套的精品资源,点击获取

简介:这个资源包提供一套开箱即用的STM32F103单片机驱动1.14英寸ST7789彩色LCD屏幕的完整Keil MDK工程。硬件接口采用标准SPI通信,配套完整的底层驱动文件:spi.c负责高速数据传输,lcd.c封装屏幕初始化、命令写入与GRAM写入逻辑,delay.c基于SysTick实现精准延时,key.c和led.c集成常用外设控制,stm32f10x_usart.c支持串口调试输出。工程已整合LittleVGL图形库适配层(lv_port_disp.c)和GUI基础功能(GUI.c),可直接显示文本、几何图形、纯色填充及简单界面元素。所有代码基于STM32F1标准外设库编写,包含CORE核心启动文件、HARDWARE外设驱动目录和littleVGL子模块,TOUCH.uvproj工程结构清晰,无需修改引脚定义或时钟配置即可编译下载,在常见1.14寸ST7789模组(如带SPI接口的圆形/方形小屏)上稳定点亮并刷新画面。


本文还有配套的精品资源,点击获取

http://www.jsqmd.com/news/953049/

相关文章:

  • LangGraph实现可审计的人机协同工作流
  • 模板即规则:文档自动化中的低代码视觉协议设计
  • 避坑指南:MicroBlaze软核开发中DDR3和Local Memory配置的那些“坑”与优化策略
  • OpenCV凸包缺陷检测报错‘索引非单调’?自相交轮廓预处理修复方案
  • C#手写数据类和protoc自动生成类的转换
  • 2026年比较好的硫氧镁耐水改性剂/硫氧镁改性剂/硫氧镁门芯改性剂/无机硫氧镁改性剂高口碑品牌推荐 - 行业平台推荐
  • 迷你主机 EMC/ESD 测试对代工选型的影响与验厂技巧
  • 基于STC89C52的WIFI遥控四足蜘蛛机器人开发套件(含APP、ESP8266固件、Altium图纸与12路舵机控制代码)
  • Bobst 0704-1417-00电源控制板
  • Amphenol ICC 17-101324线束组件解析:工业设备网络连接方案参考
  • AI Agent如何重构DeFi流动性管理范式
  • 别再搞混了!手把手教你用D435i跑通VINS-Fusion(单目/双目模式详解)
  • STM32F103裸机移植CanFestival-3保姆级避坑指南(附对象字典生成工具使用)
  • BLE蓝牙老是断连?别慌,这份0x00到0x3E错误码排查指南帮你搞定
  • 2026年评价高的凹凸造型吸塑定制/化妆品吸塑定制/精密卡位吸塑定制横向对比厂家推荐 - 品牌宣传支持者
  • 如何深度掌控开源笔记工具:Xournal++ 实战进阶指南
  • 【信息科学与工程学】【运营科学】第二篇 C4信息与通信网络运营 (C4) ——数据中心网络运营06
  • 机器学习生产化:从模型上线到可信赖系统落地指南
  • 【AI考核革命指南】:2024年企业落地智能绩效系统的5大避坑法则与3套即插即用实施框架
  • 手把手教你为团队定制PMD规则:从发现代码坏味道到编写XPath规则文件
  • 用Docker和Nginx-RTMP模块,5分钟搞定你的私人直播服务器(保姆级教程)
  • Qt数据库开发避坑指南:QSqlTableModel的EditStrategy策略详解与实战选择
  • 三菱PLC数据采集实战:用C#和MX Component五分钟搞定D寄存器读写(附完整源码)
  • 工作中数据库知识
  • Dorisoy.AMS--一款采用C# WinForm框架+SQLite数据库的企业/机构资产管理解决方案
  • 3分钟掌握AI会议截止日期管理:科研工作者的智能时间管理终极指南
  • AI数学推理系统:形式化验证+可控生成的三明治架构
  • 用Proteus仿真555+4017流水灯:从原理图到动态效果,手把手调出你想要的频率
  • prima.cpp未来路线图:下一代家庭AI集群的发展方向
  • 2023年软考-新能源采购系统—软件设计师—东方仙盟