尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

1.69英寸SPI LCD驱动全解析:从硬件连接到GUI优化实战

1.69英寸SPI LCD驱动全解析:从硬件连接到GUI优化实战
📅 发布时间:2026/8/2 10:28:57

1. 从选型到点亮:为什么是1.69英寸SPI LCD?

如果你正在为一个便携式设备、智能手表或者一个需要紧凑显示界面的嵌入式项目寻找屏幕,那么1.69英寸的LCD显示屏很可能已经进入了你的视野。这个尺寸非常微妙——它比常见的0.96英寸OLED大得多,能显示更丰富的内容,但又比2英寸以上的屏幕小巧,非常适合对空间有严格限制的应用。而SPI接口,则是让这颗“小眼睛”亮起来的关键钥匙。

在嵌入式开发中,显示屏的驱动方式直接决定了硬件设计的复杂度和软件资源的消耗。面对I2C、8080并行、RGB并行和SPI这几种常见接口,为什么SPI成为了这类小尺寸LCD的绝对主流?简单来说,SPI在引脚占用、驱动复杂度和速度之间取得了最佳平衡。一个标准的4线SPI(SCLK, MOSI, MISO, CS)加上DC(数据/命令)和RST(复位)引脚,通常6个GPIO就能搞定,这极大地节省了宝贵的MCU引脚资源。相比之下,并口驱动动辄需要16根以上的数据线,对于主控芯片的引脚数量和PCB布线都是巨大挑战。

我手头这块1.69英寸的屏幕,分辨率通常是240x280或240x240,色彩深度为16位RGB565。这意味着它需要驱动近7万个像素点,每个点包含16位颜色信息。如果使用低速I2C,刷屏速度会慢到令人难以接受;而SPI,尤其是配合MCU的硬件SPI和DMA,完全能满足这类屏幕的刷新需求,实现流畅的界面效果。从网络上的热议也能看出,无论是STM32、ESP32还是FPGA,开发者们都在围绕“SPI驱动LCD”进行着深入的探讨和优化,这恰恰证明了其广泛的应用基础和持续的技术生命力。

接下来的内容,我将以一个典型的1.69英寸SPI LCD(驱动芯片多为ST7789或ST7735S)为例,带你从硬件连接到软件驱动,从底层时序到上层GUI,完整地走通点亮、绘图、显示中文乃至优化性能的全过程。无论你是刚接触嵌入式显示的初学者,还是想优化现有驱动性能的进阶者,这里都有你需要的“干货”。

2. 硬件连接与SPI模式深度解析

拿到一块屏幕,第一步永远是看手册(Datasheet)。但很多廉价模块附带的手册可能语焉不详,这时就需要我们根据驱动芯片的通用特性和引脚定义进行推断和验证。

2.1 引脚定义与硬件连接

一块典型的1.69英寸SPI LCD模块,其引脚通常包括以下核心信号:

  1. VCC/GND:电源与地。常见工作电压为3.3V,务必确认你的主控电平与之匹配。
  2. SCLK (SCK):串行时钟线,由主控产生,用于同步数据。
  3. MOSI (SDA):主设备输出,从设备输入。即主控发送数据/命令给屏幕的线路。
  4. DC (RS, A0):数据/命令选择线。这是SPI驱动LCD区别于其他SPI设备(如Flash)的关键引脚。它告诉驱动芯片,当前在MOSI上发送的是命令(如设置显示区域)还是数据(如像素颜色)。
  5. CS (CSX):片选信号,低电平有效。用于在多个SPI设备共存时选择当前要通信的设备。
  6. RST (RESX):复位引脚,低电平复位。用于硬件复位屏幕驱动芯片,确保其从一个已知的初始状态开始工作。
  7. BLK (LED, LEDK):背光控制。可以是简单的PWM调光控制,也可能是直接接电源常亮。

连接时,将VCC/GND接好,SCLK、MOSI、DC、CS、RST分别连接到MCU的任意GPIO(对于SCLK和MOSI,建议使用硬件SPI引脚以获得最佳性能)。一个常见的连接示例如下(以STM32F103为例):

LCD_VCC -> 3.3V LCD_GND -> GND LCD_SCLK -> PA5 (SPI1_SCK) LCD_MOSI -> PA7 (SPI1_MOSI) LCD_CS -> PA4 (任意GPIO) LCD_DC -> PA3 (任意GPIO) LCD_RST -> PA2 (任意GPIO) LCD_BLK -> PA1 (PWM输出,用于调光)

注意:MISO引脚在这类屏幕上通常不需要连接,因为屏幕是纯输出设备,不会向主控发送数据。但有些屏幕的MISO可能用于读取芯片ID或状态,可根据具体需求决定是否连接。

2.2 SPI四种模式与驱动芯片的匹配

SPI有四种时钟模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。这是驱动任何SPI设备前必须确认的参数,一旦错误,通信将完全失败。

  • CPOL (Clock Polarity):时钟空闲状态。0=空闲时低电平,1=空闲时高电平。
  • CPHA (Clock Phase):数据采样时刻。0=在时钟的第一个边沿采样,1=在时钟的第二个边沿采样。

对于ST7789/ST7735S这类驱动芯片,最常用的模式是SPI Mode 0 (CPOL=0, CPHA=0) 或 SPI Mode 3 (CPOL=1, CPHA=1)。具体需要查阅芯片手册。一个快速验证的方法是:如果模块供应商提供了示例代码,直接查看其SPI初始化配置;如果没有,可以先用Mode 0尝试,如果不行再换Mode 3。

在STM32的HAL库中,初始化SPI时需在SPI_HandleTypeDef结构中设置:

hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 仅发送,可设为2LINES_TXONLY hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 通常使用8位数据帧 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL = 0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA = 0 hspi1.Init.NSS = SPI_NSS_SOFT; // 使用软件控制CS引脚 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 时钟分频,决定SPI速度 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 通常为MSB先行 hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); }

这里的关键是CLKPolarity和CLKPhase。一个常见的踩坑点:有些屏幕模块为了兼容性,内部可能已经有上拉或下拉电阻,影响了信号边沿。如果严格按照手册配置模式仍无法通信,可以尝试用逻辑分析仪或示波器抓取SCLK和MOSI的波形,对照芯片手册的时序图,检查数据采样边沿是否正确。我曾遇到过一款屏幕,手册写Mode 0,但实际必须用Mode 3才能驱动,最后就是靠抓波形发现的。

3. 驱动层构建:从复位初始化到像素写入

硬件连通后,我们进入软件驱动层。这一层的目标是封装出最基础的三个函数:写命令、写数据、以及最核心的“在指定位置画一个点”。

3.1 底层GPIO控制与通信时序

在调用HAL库的HAL_SPI_Transmit之前,我们需要手动控制DC和CS引脚。这是因为SPI外设本身不管理这些与协议相关的控制信号。

// 假设引脚已定义为宏 #define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET) #define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET) #define LCD_DC_CMD() HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET) #define LCD_DC_DATA() HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET) #define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_RESET) #define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_SET) // 写命令函数 void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_CMD(); // 设置DC为命令状态 LCD_CS_LOW(); // 选中设备 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); // 取消选中(可选,有些驱动要求CS一直保持有效直到数据写完) } // 写数据函数 void LCD_Write_Data(uint8_t data) { LCD_DC_DATA(); // 设置DC为数据状态 LCD_CS_LOW(); HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); } // 写多个数据(用于连续写入像素数据) void LCD_Write_Data_Buffer(uint8_t *data, uint16_t length) { LCD_DC_DATA(); LCD_CS_LOW(); HAL_SPI_Transmit(&hspi1, data, length, HAL_MAX_DELAY); LCD_CS_HIGH(); }

注意:关于CS引脚的操作时机,存在两种流派。一种是每次传输(命令或数据)都拉低再拉高;另一种是在一系列连续操作(如初始化序列、填充整屏)前拉低,全部完成后才拉高。后者效率稍高,但需要确保操作序列间没有其他SPI设备干扰。对于单一屏幕的应用,两种方式均可,我通常使用第一种,逻辑更清晰。

3.2 初始化序列:唤醒屏幕的“咒语”

驱动芯片上电后处于休眠或未知状态,需要发送一系列特定的命令和参数进行初始化。这个过程就是初始化序列。不同驱动芯片、甚至同芯片不同分辨率的屏幕,初始化序列都可能不同。最好的来源是供应商提供的示例代码或驱动芯片的数据手册。

一个ST7789V的简化初始化流程如下:

  1. 硬件复位:拉低RST引脚至少10ms,再拉高,等待120ms以上让芯片稳定。
  2. 发送软件复位命令:通常为0x01,并延迟等待。
  3. 退出睡眠模式:命令0x11,并延迟等待。
  4. 设置颜色格式:如0x3A命令,参数0x55代表16位RGB565。
  5. 设置显示方向(旋转):命令0x36,通过参数控制MX,MY,MV等位。
  6. 设置列地址和行地址范围:命令0x2A和0x2B,这决定了后续像素数据写入的窗口。
  7. 打开显示:命令0x29。
void LCD_Init(void) { // 1. 硬件复位 LCD_RST_LOW(); HAL_Delay(20); LCD_RST_HIGH(); HAL_Delay(120); // 等待复位完成 // 2. 发送初始化命令序列 LCD_Write_Cmd(0x01); // 软件复位 HAL_Delay(150); LCD_Write_Cmd(0x11); // 退出睡眠 HAL_Delay(255); // 设置颜色接口格式 16bit/pixel LCD_Write_Cmd(0x3A); LCD_Write_Data(0x55); HAL_Delay(10); // 设置显示方向 (0度, 可根据需要调整) LCD_Write_Cmd(0x36); LCD_Write_Data(0x00); // MY=0, MX=0, MV=0, ML=0, BGR=0, MH=0 // 设置显示区域为全屏 (240x280示例) LCD_Set_Address(0, 0, 239, 279); LCD_Write_Cmd(0x21); // 关闭反色显示(可选) LCD_Write_Cmd(0x29); // 打开显示 HAL_Delay(100); }

关键心得:初始化序列中的延迟(HAL_Delay)至关重要且非常敏感。延迟太短,芯片可能未准备好接收下一条命令,导致初始化失败,屏幕出现花屏、条纹或全白。如果屏幕无法点亮,首先检查的就是这些延迟时间,可以尝试适当加长。最好的方法是参考已知可用的代码,或者用逻辑分析仪捕获一次成功的通信,观察命令之间的时间间隔。

3.3 设置窗口与像素写入:高效刷新的基础

在屏幕上画点或填充区域前,必须告诉驱动芯片你要操作的像素范围,这就是设置“窗口”。通过0x2A和0x2B命令设置列(X)和行(Y)的起止地址。

void LCD_Set_Address(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { // 设置列地址范围 LCD_Write_Cmd(0x2A); LCD_Write_Data(x1 >> 8); // 起始列高8位 LCD_Write_Data(x1 & 0xFF); // 起始列低8位 LCD_Write_Data(x2 >> 8); // 结束列高8位 LCD_Write_Data(x2 & 0xFF); // 结束列低8位 // 设置行地址范围 LCD_Write_Cmd(0x2B); LCD_Write_Data(y1 >> 8); LCD_Write_Data(y1 & 0xFF); LCD_Write_Data(y2 >> 8); LCD_Write_Data(y2 & 0xFF); // 发送写RAM命令,准备接收像素数据 LCD_Write_Cmd(0x2C); }

设置好窗口后,发送0x2C命令,之后所有通过LCD_Write_Data发送的数据都会被依次填入这个窗口的像素中,从左到右,从上到下。

基于此,我们可以实现最基础的画点函数:

void LCD_Draw_Pixel(uint16_t x, uint16_t y, uint16_t color) { // 1. 设置窗口为单个像素点 LCD_Set_Address(x, y, x, y); // 2. 写入该像素的颜色数据(16位,RGB565) LCD_Write_Data(color >> 8); // 先发送高8位 LCD_Write_Data(color & 0xFF); // 再发送低8位 }

这个LCD_Draw_Pixel函数是构建一切图形(线、矩形、圆、图片)的基石。但请注意,这是效率最低的画点方式,因为每次画一个点都要重复设置一次窗口(发送至少6个命令字节)。在需要连续绘制(如清屏、画图)时,我们应该使用优化后的方法。

4. 性能飞跃:DMA驱动与显存管理实战

当你用LCD_Draw_Pixel函数画一个矩形时,你会发现速度慢得令人发指。这是因为大量的时间浪费在SPI传输的等待和频繁的窗口设置命令上。要获得流畅的动画或快速刷屏,必须引入两项关键技术:DMA和显存(FrameBuffer)管理。

4.1 理解SPI+DMA的工作机制

DMA(直接存储器访问)允许外设(如SPI)直接与内存交换数据,而无需CPU的干预。在填充屏幕时,CPU只需要准备好一块包含所有像素颜色数据的缓冲区(显存),然后启动SPI的DMA传输,就可以去处理其他任务,SPI和DMA会自动将数据“搬运”到屏幕上。

为什么需要显存?没有显存时,我们画一个复杂的UI,需要实时计算每个像素的颜色,然后通过SPI发送出去。计算和发送是串行的,且发送占用大量时间。有了显存,我们可以先在内存里“画”好一整帧图像,计算过程可以充分利用CPU。计算完成后,再通过一次高效的DMA传输,将整块显存数据“倾倒”到屏幕上,刷屏速度极快。

4.2 为1.69英寸屏分配与管理显存

以240x280分辨率、16位色深为例,一帧图像需要的内存为:240 * 280 * 2 bytes = 134,400 bytes ≈ 131 KB

这对于RAM资源紧张的MCU(如STM32F103的64KB RAM)来说是巨大的负担。因此,我们需要根据项目需求权衡:

  • 全帧缓冲:分配131KB的数组。优点是刷屏简单(一次DMA),可实现复杂动画和特效。缺点是占用内存大。
  • 行缓冲或块缓冲:只分配几行或一小块区域的内存。画图时更新这部分缓冲,然后只DMA传输这一部分到屏幕的对应位置。节省内存,但软件控制更复杂。
  • 无缓冲,直接DMA:不分配显存,但将画图函数(如画线算法)生成的数据流,通过DMA直接发送到SPI。这需要精心设计数据流生成与DMA传输的配合。

对于STM32H750这类拥有大容量RAM(1MB)的MCU,可以轻松使用全帧缓冲。我们可以在外部SDRAM甚至内部DTCM中定义显存:

// 在外部SDRAM中定义显存 (使用绝对地址或链接脚本指定) #define FB_SIZE (240 * 280) uint16_t lcd_framebuffer[FB_SIZE] __attribute__((section(".sdram"))); // 或者使用动态分配(如果内存管理已设置好) // uint16_t *lcd_framebuffer = (uint16_t*)SDRAM_MALLOC(FB_SIZE * sizeof(uint16_t));

4.3 实现DMA刷屏函数

首先,需要配置SPI的DMA通道。在CubeMX中,为SPI的TX流启用DMA,并设置为存储器到外设、半字或字节传输(取决于SPI数据宽度)。

然后,实现刷屏函数:

extern DMA_HandleTypeDef hdma_spi1_tx; // SPI TX DMA句柄 void LCD_Refresh_DMA(void) { // 1. 设置窗口为全屏 LCD_Set_Address(0, 0, 239, 279); // 2. 等待上一次DMA传输完成(非阻塞操作时需要) // while (HAL_DMA_GetState(&hdma_spi1_tx) != HAL_DMA_STATE_READY); // 3. 启动DMA传输,将显存数据发送到SPI // 注意:需要将DC引脚设置为数据模式,并拉低CS LCD_DC_DATA(); LCD_CS_LOW(); // 关键点:HAL_SPI_Transmit_DMA要求数据地址是uint8_t* // 我们的显存是uint16_t数组,需要强制转换,并乘以2(字节数) if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)lcd_framebuffer, FB_SIZE * 2) != HAL_OK) { // 错误处理 Error_Handler(); } // 传输完成后,DMA中断或回调函数中可以拉高CS(可选,取决于通信协议) }

一个极其重要的坑:SPI数据宽度与DMA传输宽度的匹配。如果你的SPI配置为8位数据帧(SPI_DATASIZE_8BIT),那么DMA也应该配置为字节传输(DMA_PDATAALIGN_BYTE),并且显存缓冲区需要被视为uint8_t数组。如果你配置为16位数据帧,那么DMA应配置为半字传输,显存直接就是uint16_t数组。混用会导致数据错位,屏幕显示乱码。我强烈建议使用8位模式,因为更通用,且大多数SPI外设对8位模式支持最好。此时,一个16位的像素颜色(0xABCD)需要拆成两个8位数据(0xAB, 0xCD)顺序发送,正如我们在LCD_Draw_Pixel函数里做的那样。在DMA传输中,我们直接传输FB_SIZE * 2个字节即可。

4.4 双缓冲与撕裂效应规避

当你使用DMA刷新显存时,如果CPU同时也在修改这块显存(比如在绘制下一帧的图形),就会导致屏幕上显示的内容一部分是旧帧,一部分是新帧,产生撕裂现象。

解决方案是双缓冲:

  1. 分配两块大小相同的显存:fb_front和fb_back。
  2. CPU始终在fb_back(后缓冲)上进行绘制。
  3. 当一帧绘制完成后,交换前后缓冲指针,并启动DMA将新的fb_front(即刚才的fb_back)发送到屏幕。
  4. CPU开始在新的fb_back上绘制下一帧。

这样,DMA传输的始终是一块完整的、稳定的帧数据,有效避免了撕裂。交换指针是一个极快的操作,几乎不耗时。

uint16_t *fb_front = lcd_framebuffer0; uint16_t *fb_back = lcd_framebuffer1; void LCD_Swap_Buffer(void) { uint16_t *temp = fb_front; fb_front = fb_back; fb_back = temp; // 下次刷新时,DMA应传输fb_front }

在LCD_Refresh_DMA函数中,将传输源地址改为fb_front。在图形引擎的主循环中,逻辑是:绘制完成 -> 交换缓冲区 -> 启动DMA刷新。

5. 图形与字体:在有限资源上构建UI

有了高效的刷屏机制,我们就可以在屏幕上绘制更丰富的内容了。所有图形本质上都是通过算法,在显存中设置特定位置的像素颜色。

5.1 基础图形绘制算法

画线(Bresenham算法):这是计算机图形学中最经典的算法之一,只用整数加减和位运算,效率极高。以下是水平/垂直/任意线段的简化实现思路:

void LCD_Draw_Line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { int16_t dx = abs(x2 - x1); int16_t dy = -abs(y2 - y1); // 注意负号 int16_t sx = (x1 < x2) ? 1 : -1; int16_t sy = (y1 < y2) ? 1 : -1; int16_t err = dx + dy; // 误差项 while (1) { LCD_Draw_Pixel_FB(x1, y1, color); // 使用操作显存的函数 if (x1 == x2 && y1 == y2) break; int16_t e2 = 2 * err; if (e2 >= dy) { err += dy; x1 += sx; } // 水平步进 if (e2 <= dx) { err += dx; y1 += sy; } // 垂直步进 } }

画矩形和填充矩形:矩形非常简单,四条线或者两层循环即可。画圆(中点圆算法):与Bresenham直线算法类似,利用圆的对称性,只需计算八分之一的圆弧点,然后映射到其他七个象限。

关键优化:这些绘制函数不应再调用原始的LCD_Draw_Pixel(它涉及SPI通信),而应该调用一个直接向显存写入的函数,例如LCD_FB_Draw_Pixel(uint16_t x, uint16_t y, uint16_t color),这个函数只执行lcd_framebuffer[y * LCD_WIDTH + x] = color;。所有图形绘制都在内存中完成,最后统一用DMA刷屏。

5.2 中文字库的集成与显示

显示英文和数字相对简单,使用8x16或16x24的点阵字模即可。显示中文则面临挑战,因为汉字数量庞大,全字库动辄数MB。

常用方案有以下几种:

  1. 部分字库:只提取项目中用到的汉字。使用PC端工具(如“取模软件”)生成所用汉字的点阵数据(如16x16, 24x24),将其作为常量数组存储在代码或外部Flash中。这是最节省资源的方法。

    // 示例:“中”字的16x16点阵数据(纵向取模,字节高位在下) const uint8_t font_zhong[] = { 0x00,0x40,0x00,0x80,0x01,0x00,0x06,0x00,0x1C,0x00,0xF0,0x0F,0x10,0x08,0x10,0x08, 0x10,0x08,0x10,0x08,0xF0,0x0F,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x00,0x00 }; void LCD_Show_Chinese(uint16_t x, uint16_t y, const uint8_t *font, uint16_t color) { // 解析点阵数据,每个bit对应一个像素 for(int i=0; i<16; i++) { // 行 for(int j=0; j<2; j++) { // 每行2个字节 uint8_t byte = font[i*2 + j]; for(int k=0; k<8; k++) { // 每个字节8个bit if(byte & (0x80>>k)) { // 判断bit是否为1 LCD_FB_Draw_Pixel(x + j*8 + k, y + i, color); } } } } }
  2. 外部Flash存储全字库:将完整的字库文件(如GB2312编码的HZK16)烧录到外部SPI Flash(如W25Q64)中。显示时,根据汉字的机内码计算出该字在字库文件中的偏移地址,然后从Flash中读取点阵数据。这需要文件系统或直接的地址映射知识。

  3. 矢量字体:对于高性能MCU(如STM32H7+外部SDRAM),可以考虑集成微型矢量字体引擎(如stb_truetype的简化版)。它从.ttf文件中提取字形轮廓,然后进行缩放和光栅化。这种方式字体质量高,且支持任意大小,但计算复杂,对CPU要求高。

选择建议:对于大多数1.69英寸屏的应用,显示汉字数量有限,部分字库是最务实、最稳定的选择。在项目初期就整理好所有需要显示的文本,一次性生成字模数据。

5.3 图片显示:从BMP到自定义格式

显示图片同样需要先将图片转换为MCU可识别的像素数组。常见格式有BMP、JPG(需要解码)、以及自定义的二进制数组。

显示BMP图片(未经压缩的24位或16位):

  1. 使用工具(如Photoshop、在线转换器)将图片转换为目标分辨率(240x280),颜色深度为16位RGB565。
  2. 将转换后的BMP文件以二进制数组形式嵌入代码,或者存储到外部Flash。
  3. 显示时,因为BMP文件头包含54字节的文件信息,需要跳过文件头,直接将像素数据复制到显存或通过DMA发送。注意BMP的像素存储顺序是从下到上的,可能需要翻转。

更高效的自定义格式:为了节省存储空间和加速显示,可以定义自己的图片格式。例如,使用PC端工具将图片直接转换成RGB565的纯像素数据数组(.c文件),并压缩(如简单的RLE游程编码)。在MCU端,编写对应的解压显示函数。

// 示例:直接包含转换后的像素数组 const uint16_t img_logo[240*280] = {0xFFFF, 0xF800, ...}; void LCD_Show_Image(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint16_t *img) { for(int i=0; i<height; i++) { for(int j=0; j<width; j++) { LCD_FB_Draw_Pixel(x+j, y+i, img[i*width + j]); } } } // 或者更高效的方式:使用memcpy直接复制一行或一块数据到显存

对于需要显示多张图片的应用,务必将其存储到外部Flash,并通过SPI DMA读取到SDRAM或直接发送到屏幕,避免占用宝贵的内部RAM。

6. 高级优化与调试技巧

当基础功能都实现后,我们往往会追求极致的性能和稳定性。这一部分分享一些高阶优化手段和调试中遇到的真实问题。

6.1 SPI时钟极限与信号完整性

SPI的时钟速度(BaudRate)并非越高越好。它受到以下限制:

  1. MCU SPI外设的最高时钟:查阅数据手册。
  2. LCD驱动芯片的最大SCLK频率:ST7789典型值为62.5MHz(在3.3V IO下),但实际模块可能因为PCB布线、导线长度等因素达不到。
  3. 信号完整性:当SCLK频率过高时,长导线或不良的PCB布局会导致信号边沿变缓、过冲、振铃,造成通信错误。表现为屏幕显示雪花点、局部错乱或完全无显示。

调试方法:

  • 逐步提高:从低速(如1MHz)开始测试,逐步提高(2M, 5M, 10M, 20M...),直到出现显示异常,然后退回一个稳定值。
  • 使用示波器:观察SCLK和MOSI的波形。健康的信号应该是干净、陡峭的方波。如果看到圆角、振铃或毛刺,就需要降低频率、缩短连线、或在信号线上串联一个小电阻(如22-100欧姆)来改善。
  • 我的经验值:对于杜邦线连接的1.69英寸模块,SPI时钟在20-40MHz之间通常比较稳定。如果使用精心设计的PCB,可以尝试跑到极限。

6.2 降低SPI通信中的CPU占用

即使使用了DMA,CPU仍然需要参与准备数据、管理传输。进一步降低CPU开销的方法:

  1. 使用SPI的Tx FIFO和DMA双缓冲模式:一些高端MCU的SPI支持FIFO和DMA的双缓冲(或链表模式)。可以配置DMA在传输完一个缓冲区的数据后,自动切换到下一个缓冲区,并产生中断让CPU准备下一块数据,实现“乒乓操作”,几乎可以做到数据传输的零等待。

  2. 利用硬件加速:如果MCU带有图形加速器(如STM32H7的Chrom-ART),或者像FPGA这样的硬件,可以将填充矩形、图像混合(Alpha Blending)、旋转等操作卸载到硬件,极大释放CPU。

  3. 优化绘制算法:避免在显存上进行像素级的擦除和重绘。采用“脏矩形”技术,只更新UI中发生变化的部分区域,而不是刷新整个屏幕。

6.3 常见问题排查清单

  1. 屏幕白屏或完全不亮:

    • 检查电源和背光电压。
    • 检查复位时序,确保复位脉冲宽度和复位后的延迟足够。
    • 用逻辑分析仪抓取SPI信号,确认初始化序列的命令和数据是否正确发送,特别是0x11(退出睡眠)和0x29(打开显示)命令。
    • 检查SPI模式(CPOL/CPHA)是否正确。
  2. 屏幕花屏、错位、颜色异常:

    • 颜色格式不匹配:确认初始化命令0x3A的参数与你的颜色数据格式一致(RGB565常用0x55或0x65)。
    • 显存与屏幕分辨率不匹配:检查显存数组大小是否等于width * height。检查画点函数中的坐标计算y * width + x是否正确。
    • DMA传输数据宽度错误:如前所述,确认SPI数据宽度与DMA传输宽度匹配。
    • 字节序问题:RGB565颜色值在发送时,是先发高8位还是低8位?需要与驱动芯片约定一致。通常ST7789是先发高8位。
  3. 刷新速度慢,有肉眼可见的刷新过程:

    • 确认是否使用了DMA。
    • 提高SPI时钟频率(在信号完整的前提下)。
    • 检查刷屏函数LCD_Refresh_DMA是否在每次刷新时都重复调用了LCD_Set_Address。可以在启动DMA前只设置一次窗口。
    • 如果使用memcpy将图片数据复制到显存,这个过程可能很耗时。考虑使用DMA从Flash搬运数据到显存。
  4. 显示内容撕裂:

    • 引入双缓冲机制。
    • 确保在垂直回扫期间(如果屏幕有该信号)或DMA传输完成中断中进行缓冲区交换和刷新触发。

驱动一块小小的1.69英寸SPI LCD,从简单的点灯到构建一个流畅的UI界面,是一个层层递进、不断优化的过程。它涉及硬件接口、通信协议、内存管理、图形算法等多个层面的知识。每一次问题的解决和性能的提升,都建立在对底层原理更深刻的理解之上。希望这篇长文能成为你探索嵌入式显示世界的扎实起点,当你看到自己设计的界面在这块小巧的屏幕上流畅呈现时,那种成就感便是对所有这些努力的最佳回报。

相关新闻

  • 游戏开发中高可用Buff系统架构设计:从核心原理到Unity实践
  • 嵌入式TFT LCD API设计:从硬件抽象到图形界面构建实战
  • 魔兽争霸III性能优化终极指南:如何让经典游戏在现代电脑上流畅运行

最新新闻

  • 2026年南宁净水设备安装前必看:免费上门勘测与水质实测是关键第一步 - 品牌品鉴馆
  • Chat API与XDK开发指南:快速集成智能对话能力
  • 450nm 5W蓝光激光器:从原理、选型到安全集成的完整实践指南
  • 2026海北宠物店推荐黑心宠物店套路曝光这5家口碑靠谱 - 莘州文化
  • 2026年盐城收银系统服务商深度推荐:三大本地品牌实测对比,微城市网络科技领跑智慧门店数字化 - 钦扬网络
  • AI名片信息提取:3步实现99%字段召回率,附开源工具链+私有化部署全流程

日新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

周新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

月新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号