1.69英寸SPI LCD驱动全解析:从硬件选型到DMA优化实战

1.69英寸SPI LCD驱动全解析:从硬件选型到DMA优化实战

1. 项目概述:为什么是1.69英寸SPI LCD?

如果你正在为一个嵌入式项目寻找一块小巧、省电、驱动简单的显示屏,那么1.69英寸的SPI接口LCD绝对是一个值得深入研究的选项。它不像那些动辄5寸、7寸的“大家伙”需要复杂的RGB接口和高速内存,也不像I2C屏那样在刷新率和分辨率上有所妥协。这块屏的定位非常精准:在有限的引脚资源和主控性能下,提供一块足够清晰、刷新流畅、且易于集成的视觉交互窗口。

我手头这块典型的1.69英寸LCD,分辨率通常是240x280或240x240,色彩深度为16位或18位(即RGB565或RGB666)。SPI(Serial Peripheral Interface)作为其通信接口,意味着你只需要3到4根数据线(MOSI, SCLK, DC, CS,有时再加一根RST),就能完全控制它。这对于引脚紧张的单片机(比如STM32F103的某些小封装型号、ESP8266/ESP32,甚至是Arduino Nano)来说,简直是福音。你不需要为并行总线预留8位或16位的数据端口,也无需担心高速信号带来的PCB布局难题。

那么,谁适合用这块屏呢?首先是各类便携式穿戴设备、智能家居中控的副屏、小型工控仪表显示单元,以及所有对体积、功耗和开发复杂度有严格限制的嵌入式创客项目。它的核心价值在于,用极简的硬件连接,换取一个功能完整的图形输出能力。接下来,我将从硬件选型、驱动原理、软件实现到实战调优,完整拆解如何玩转这块屏。

2. 核心硬件解析与选型要点

2.1 显示屏模组本身:驱动IC与接口

市面上常见的1.69英寸SPI LCD,其核心是一块液晶面板搭配一个驱动芯片。最常见的驱动芯片是ST7789ILI9341(虽然它更常用于2.8寸屏,但也有小尺寸变体)或GC9A01。选型时,驱动芯片是关键,它决定了你后续的驱动库和初始化序列。

你需要从卖家提供的资料或芯片数据手册中确认以下几点:

  1. 分辨率与色彩格式:确认是240x280还是240x240。色彩格式支持RGB565(16位色)还是RGB666(18位色)。RGB565是嵌入式领域最常用的格式,因为它刚好占用两个字节,处理效率高。
  2. SPI模式与速率:SPI有4种模式(CPOL, CPHA组合),驱动芯片会指定一种,常见的是Mode 0(CPOL=0, CPHA=0)或Mode 3。必须在主控初始化SPI时匹配。最大SPI时钟速率(如80MHz)决定了你的刷新速度上限。
  3. 引脚定义
    • SCLK (Serial Clock): SPI时钟线。
    • MOSI (Master Out Slave In): 主控输出数据到屏幕。
    • DC (Data/Command): 至关重要的引脚。用于区分当前发送的是“命令”(如设置地址窗口)还是“数据”(实际的像素颜色)。拉低为命令,拉高为数据。
    • CS (Chip Select): 片选,低电平有效。当总线上有多个SPI设备时用于选择屏幕。
    • RST (Reset): 硬件复位,低电平有效。通常上电后需要拉低一段时间再拉高,以确保驱动芯片状态可靠。
    • BL (Backlight): 背光控制。可能是高电平使能,也可能是PWM调光。这是影响功耗和观感的重要部分。

注意:有些廉价的模块为了省引脚,可能省略了DCRST,而使用特定的数据包协议来区分命令/数据,或者只依赖软件复位。这会给驱动编写带来额外复杂度,优先选择带有独立DCRST引脚的模块。

2.2 主控MCU的选择与资源评估

选择主控时,SPI LCD的驱动对性能有一定要求,尤其是你想实现流畅动画或快速刷屏时。

  • 基础需求:一个硬件SPI外设(至少支持Master模式)、足够的GPIO(用于DC, CS, RST, BL)、以及足够的RAM和Flash。
  • 性能进阶:如果你希望获得更高的帧率,DMA(直接存储器访问)功能几乎是必选项。DMA可以在不占用CPU的情况下搬运显存数据到SPI外设,让CPU腾出手来处理业务逻辑或图形计算。这也是网络热词中“stm32h750 dma 驱动 spi lcd 问题”备受关注的原因——高性能主控搭配DMA是优化体验的关键。
  • 内存考量:对于240x280 RGB565的屏幕,一帧图像需要240 * 280 * 2 = 134,400字节(约131KB)的显存。如果你的主控(如STM32F103)内部RAM不足,可以考虑:
    1. 使用局部刷新:只更新屏幕上变化的部分区域,而非全屏刷新。
    2. 使用外部RAM(如SDRAM):像STM32H750这类高性能MCU可以外扩SDRAM作为显存。
    3. 使用单缓冲+直接流式写入:不分配完整显存,绘制图形时直接通过SPI发送像素数据。这需要图形库有相应的流式输出支持,且无法实现复杂的双缓冲防撕裂。

常见主控方案对比

  • STM32F1系列(如F103C8T6):硬件SPI速率可达36MHz,但RAM较小(20KB),通常只能用于静态显示或极小区域动态更新。适合简单界面。
  • STM32F4系列(如F407):性能更强,RAM更大(192KB),可以轻松分配完整显存,配合DMA能实现流畅刷新。
  • ESP32系列:集成Wi-Fi/蓝牙,双核,SPI速率高,且通常有足够PSRAM可扩展作为显存,是物联网显示终端的绝佳选择。
  • GD32、AT32等国产替代:与STM32 Pin-to-Pin兼容,性价比高,驱动代码通常可移植。

2.3 电源与背光电路设计

显示屏模组通常需要3.3V供电(逻辑部分),背光LED可能是3.3V或5V驱动。务必查阅数据手册。

  • 电源稳定性:当屏幕全白(所有像素点亮)时,瞬时电流可能达到100mA以上。确保你的电源电路(尤其是LDO)能提供足够且稳定的电流,否则可能导致MCU复位或屏幕闪烁。
  • 背光控制:如果BL引脚支持PWM调光,强烈建议连接到MCU的一个PWM输出通道。这样你就可以通过代码无级调节屏幕亮度,适应不同环境光线,这也是提升产品质感的一个小细节。如果只是简单的开关控制,用一个GPIO口即可。

3. 驱动原理深度剖析:从SPI协议到帧缓冲

3.1 SPI通信协议在LCD驱动中的具体应用

SPI是一种全双工、同步的串行通信总线。在驱动LCD时,我们通常只使用主控发送(MOSI)功能,屏幕作为只接收的Slave设备。

关键点在于DC引脚的作用:它本质上扩展了SPI的数据协议。当DC为低电平时,通过MOSI发送的一个字节(8位)被屏幕解释为命令码(Command),例如0x2A可能代表“设置列地址”。当DC为高电平时,发送的字节则被解释为数据(Data),该数据可能是命令的参数,也可能是像素的颜色值。

一个典型的设置屏幕显示区域(Setting Column Address)的序列如下:

  1. DC拉低,通过SPI发送命令字节0x2A
  2. DC拉高,通过SPI发送4个数据字节:起始列高8位、起始列低8位、结束列高8位、结束列低8位。
  3. 接着,DC拉低,发送设置行地址的命令0x2B,再拉高发送4个行地址数据字节。
  4. 最后,DC拉低,发送开始写入显存(GRAM)的命令0x2C
  5. 此后,只要保持DC为高,通过SPI发送的所有数据字节都会被依次存入上一步设定的矩形区域,并显示在屏幕上。对于RGB565格式,每个像素点需要连续发送2个字节(先高8位,后低8位)。

SPI的四种模式:这由时钟极性(CPOL)和时钟相位(CPHA)决定。必须与屏幕驱动芯片要求的一致。通常ST7789等芯片在上升沿采样数据,这意味着在SCLK的上升沿,MOSI上的数据必须已经稳定。这对应着CPOL=0, CPHA=0(模式0)或CPOL=1, CPHA=1(模式3)。具体需查手册,但模式0最为常见。

3.2 显存(GRAM)与地址窗口机制

屏幕驱动芯片内部有一个图形显存(GRAM)。这个显存的大小正好对应屏幕的分辨率。我们通过SPI写入的数据,最终就是存放在这个GRAM的特定位置。

地址窗口(Address Window)是一个核心概念。在向屏幕发送像素数据前,我们必须先通过0x2A0x2B命令设定一个矩形的“窗口”。设定后,后续发送的像素数据流会从该窗口的左上角开始,按行优先的顺序自动填充。每填满一行,列地址自动复位到起始列,行地址加一。这极大地简化了操作:你只需要设定一次窗口,然后就可以连续发送一整块区域(甚至全屏)的像素数据,而无需为每个像素指定坐标。

3.3 DMA如何加速SPI数据传输

没有DMA时,CPU需要执行以下操作:从内存中读取一个颜色数据(2字节)-> 等待SPI发送缓冲区空 -> 将数据写入SPI数据寄存器 -> 重复数十万次(对于一帧图像)。这期间CPU被完全占用。

启用DMA后,流程变为:

  1. CPU配置好SPI和DMA:告诉DMA源地址(显存数组)、目标地址(SPI数据寄存器地址)、传输数据量。
  2. CPU启动SPI和DMA传输。
  3. DMA控制器自动从内存搬运数据到SPI外设,完全不需要CPU干预。
  4. CPU可以立即去执行其他任务,比如准备下一帧的图形数据(实现双缓冲),或者处理网络数据、传感器读数等。
  5. 当DMA传输完成一整帧数据后,会产生一个中断,CPU在中断服务程序里进行帧同步(如交换双缓冲指针)等操作。

这就是“stm32h750 dma 驱动 spi lcd 问题”的核心价值所在。H750的高性能SPI(可达150MHz以上)配合DMA,可以轻松实现60fps甚至更高的刷屏率,为流畅的UI动画打下基础。常见问题通常集中在DMA传输完成中断的处理、内存对齐(Cache一致性)以及SPI时钟配置上。

4. 软件驱动实现全流程

4.1 底层硬件抽象层(HAL)驱动编写

无论你用标准外设库、HAL库还是LL库,底层驱动函数都需要实现几个核心功能:

1. 引脚初始化:

void LCD_GPIO_Init(void) { // 初始化 SCLK, MOSI, CS, DC, RST, BL 对应的GPIO // 将SCLK和MOSI配置为复用推挽输出(连接到SPI外设) // 将DC, CS, RST, BL配置为通用推挽输出 // 初始状态:CS高(不选中),RST高,BL低(关闭背光) }

2. SPI外设初始化:

void LCD_SPI_Init(void) { SPI_HandleTypeDef hspi; hspi.Instance = SPI1; hspi.Init.Mode = SPI_MODE_MASTER; hspi.Init.Direction = SPI_DIRECTION_2LINES; // 全双工,但只发不收 hspi.Init.DataSize = SPI_DATASIZE_8BIT; // 以8位为单位发送 hspi.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL = 0 hspi.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA = 0, 模式0 hspi.Init.NSS = SPI_NSS_SOFT; // 软件控制CS引脚 hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 系统时钟/2,追求速度 hspi.Init.FirstBit = SPI_FIRSTBIT_MSB; // 先发送最高位 hspi.Init.TIMode = SPI_TIMODE_DISABLE; hspi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi); }

3. 基本读写函数:

void LCD_Write_Cmd(uint8_t cmd) { LCD_DC(0); // DC拉低,写命令 LCD_CS(0); // 选中设备 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); LCD_CS(1); } void LCD_Write_Data(uint8_t data) { LCD_DC(1); // DC拉高,写数据 LCD_CS(0); HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); LCD_CS(1); } void LCD_Write_Data_16bit(uint16_t data) { uint8_t buf[2]; buf[0] = data >> 8; // 发送高字节 buf[1] = data & 0xFF; // 发送低字节 LCD_DC(1); LCD_CS(0); HAL_SPI_Transmit(&hspi1, buf, 2, HAL_MAX_DELAY); LCD_CS(1); }

4. 初始化序列(Reset & Initialization):这是最繁琐但必须精确的一步。不同驱动芯片的初始化序列不同,通常由厂家提供或从开源驱动库中获取。序列包含一系列命令和数据,用于设置屏幕的扫描方向、颜色格式、伽马校正、电源控制等。

void LCD_Init(void) { // 1. 硬件复位 LCD_RST(0); HAL_Delay(100); // 保持低电平至少10ms LCD_RST(1); HAL_Delay(120); // 等待复位稳定 // 2. 发送初始化命令序列 LCD_Write_Cmd(0x01); // Software reset HAL_Delay(150); LCD_Write_Cmd(0x11); // Sleep out HAL_Delay(120); LCD_Write_Cmd(0x3A); // Interface Pixel Format LCD_Write_Data(0x55); // 0x55 for 16bit/pixel (RGB565) // ... 发送更多初始化命令 LCD_Write_Cmd(0x29); // Display ON HAL_Delay(100); // 3. 开启背光 LCD_BL(1); }

4.2 基于DMA的刷屏函数优化

这是提升性能的关键。我们以填充整个屏幕一种颜色为例:

1. 准备显存(Frame Buffer):

// 在内存中分配一个显存数组,大小为屏幕分辨率 * 2字节 uint16_t frame_buffer[LCD_HEIGHT][LCD_WIDTH]; // 二维数组,便于按坐标操作 // 或者一维数组 uint16_t frame_buffer[LCD_HEIGHT * LCD_WIDTH];

2. DMA刷屏函数:

void LCD_Refresh_DMA(uint16_t *buf, uint32_t size) { // 1. 设置地址窗口为全屏 LCD_Set_Address(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1); // 2. 发送写GRAM命令 (0x2C) LCD_Write_Cmd(0x2C); // 3. 启动DMA传输,将buf中的数据通过SPI发送出去 LCD_DC(1); // 后续都是数据 LCD_CS(0); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)buf, size * 2); // size是像素数,乘以2是字节数 // 注意:这里CS在DMA传输完成中断中再拉高更安全 } // DMA传输完成中断回调函数 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi->Instance == SPI1.Instance) { LCD_CS(1); // 传输完成,取消片选 // 可以在这里设置一个标志位,通知主循环一帧刷新完成 g_frame_ready = 1; } }

3. 双缓冲(Double Buffering)防撕裂:这是实现流畅动画的高级技巧。准备两个显存缓冲区:frame_buffer_frontframe_buffer_back

  • 绘制阶段:CPU在frame_buffer_back(后缓冲)中绘制下一帧图像。
  • 交换阶段:当后缓冲绘制完成,且前缓冲(frame_buffer_front)的DMA传输也已完成时,交换两个缓冲区的指针。
  • 显示阶段:启动DMA,将新的frame_buffer_front(即刚才画好的后缓冲)发送到屏幕。 这样,屏幕永远只显示完整的、绘制好的一帧,避免了绘制到一半的图像被显示出来造成的“撕裂”现象。

4.3 图形库(GUI)的集成与使用

直接操作像素点太低效。集成一个轻量级GUI库能极大提升开发效率。常用的有:

  • LVGL:当前最热门的开源嵌入式GUI库,功能强大,支持动画、主题、多种控件,对SPI LCD和DMA有很好的支持。
  • u8g2:专注于单色显示屏,但也支持彩色,API简单,资源占用极小。
  • TouchGFX:ST主推的商用级GUI,效果炫酷,但需要授权且对硬件要求高。

集成LVGL为例,关键步骤是提供其所需的“显示驱动”和“刷屏回调函数”:

  1. 在LVGL的显示驱动初始化中,注册一个flush_cb回调函数。
  2. 当LVGL需要刷新一块区域时,会调用这个回调,并传入一个包含像素数据的数组和区域信息。
  3. flush_cb函数内部,你调用前面写好的LCD_Set_AddressLCD_Refresh_DMA(或非DMA版本)函数,将这块区域的数据发送到屏幕。
  4. 发送完成后,调用lv_disp_flush_ready通知LVGL刷新完成。

这样,你就可以使用LVGL丰富的API来创建按钮、标签、图表等界面元素,而无需关心底层像素如何发送。

5. 实战调试与性能优化技巧

5.1 上电无显示?系统化排查流程

  1. 电源与背光:首先用万用表测量屏幕VCC和GND引脚是否有稳定的3.3V?背光引脚(BL)电压是否正常(高电平或PWM信号)?可以尝试强制给BL高电平看是否有背光亮起。
  2. 复位时序:确保复位序列正确。有些屏幕对复位低电平的持续时间很敏感,太短或太长都不行。严格按照数据手册的时序操作,必要时用逻辑分析仪抓取RST引脚波形。
  3. SPI信号:使用逻辑分析仪或示波器连接SCLK和MOSI线。上电初始化阶段,应该能看到密集的SPI数据波形。检查:
    • SCLK频率是否在芯片支持的范围内(初始阶段不宜过高)。
    • SPI模式(CPOL/CPHA)是否正确。波形不对通常表现为屏幕花屏、错位或完全无反应。
    • DC引脚是否在正确的时间切换电平。
  4. 初始化序列:这是最可能出问题的地方。逐条核对初始化命令和数据,特别是关键命令如睡眠退出(Sleep Out)、像素格式设置(Pixel Format)、显示开(Display ON)。一个命令错误就可能导致屏幕处于未知状态。最好能找到官方示例代码或经过验证的开源代码进行比对。
  5. 软件片选与硬件片选:如果你使用软件控制CS(SPI_NSS_SOFT),确保在每个命令/数据包前后正确控制CS引脚。如果使用硬件NSS,检查SPI配置是否正确。

5.2 提升刷新率的实战策略

刷新率(FPS) = SPI时钟频率 / (每帧像素数 * 每像素位数 * 开销系数)。

  • 最大化SPI时钟:在保证信号完整性的前提下,将SPI的预分频器(BaudRatePrescaler)调到最小。STM32H7的SPI可以轻松跑到100MHz以上。
  • 启用DMA:这是最有效的措施,解放CPU。
  • 减少传输开销
    • 合并传输:设置地址窗口后,发送像素数据时,尽量使用一次性发送大量数据的API(如HAL_SPI_Transmit_DMA),而不是逐像素调用LCD_Write_Data_16bit。后者会产生大量DCCS的控制开销。
    • 优化DC控制:在全屏刷新时,可以在发送0x2C命令后,将DC引脚置高,然后发送一整帧数据,期间不再切换DC。这需要驱动函数配合。
  • 使用缩减更新区域:UI变化时,只刷新脏矩形区域(Dirty Rectangle),而不是全屏。LVGL等GUI库自动支持此优化。
  • 选择RGB565格式:相比RGB666(18位),RGB565(16位)每个像素减少1字节传输量,对240x280屏幕,一帧可减少约26KB的数据量,提升显著。

5.3 降低系统功耗的考量

对于电池供电设备,屏幕是耗电大户。

  • 背光管理:背光LED是主要功耗源。通过PWM动态调节亮度,在环境光暗时调低亮度。在系统休眠时,彻底关闭背光。
  • 利用睡眠模式:大部分LCD驱动芯片支持睡眠(Sleep In)和深度睡眠模式。在长时间不刷新时,发送睡眠命令,可以将芯片功耗从毫安级降至微安级。唤醒时再发送睡眠退出(Sleep Out)命令。注意,睡眠后需要重新初始化部分寄存器。
  • 降低刷新率:对于静态显示内容,可以大幅降低刷新率,比如从60Hz降到1Hz。这需要GUI库支持按需刷新或定时刷新。

5.4 抗干扰与PCB布局建议

SPI速率较高时(>20MHz),布线不当会引起显示异常(雪花、条纹)。

  • 等长与紧耦合:SCLK和MOSI信号线应尽可能等长,并平行走线,且远离其他高速或噪声源(如电机驱动、开关电源)。
  • 串联匹配电阻:在靠近MCU输出的SCLK和MOSI线上串联一个22Ω-100Ω的小电阻,可以抑制信号过冲和振铃。
  • 电源去耦:在屏幕模组的电源入口处,紧贴引脚放置一个10μF的钽电容和一个0.1μF的陶瓷电容,用于滤除低频和高频噪声。
  • 接地:确保屏幕模组的地线与主控地线连接良好,最好有独立的宽走线或铺铜连接。

6. 进阶应用与项目构思

掌握了基础驱动后,这块小屏能玩出很多花样:

1. 便携式游戏机:使用STM32F4或ESP32作为主控,搭配摇杆和按键,利用DMA双缓冲实现流畅的2D游戏画面(如贪吃蛇、打飞机)。关键在于高效的精灵图(Sprite)渲染和碰撞检测算法。

2. 智能家居信息屏:ESP32连接Wi-Fi,从网络API获取天气、时间、新闻头条等信息,通过LVGL制作精美的信息卡片界面。可以增加触摸功能(电阻屏或电容屏模组),实现交互。

3. 微型示波器/频谱仪:利用MCU的ADC采集信号,进行FFT运算(如网络热词“基于stm32的lcd信号波形和fft频谱显示”),将时域波形和频域频谱实时绘制在屏幕上。这对MCU的运算能力和屏幕刷新率有较高要求。

4. 自定义仪表盘:用于小型机器人、无人机或智能车,显示速度、电量、传感器状态、摄像头预览图等。需要将各种数据源进行融合,并以直观的图形化方式呈现。

5. 电子相框/动画展示盒:从SD卡读取图片(BMP、JPEG需解码)或动画序列帧,循环播放。可以研究如何用硬件解码(如STM32的JPEG硬解)或高效软件解码来减轻CPU负担。

在实现这些项目时,你会遇到更具体的问题,例如在ESP32上如何将PSRAM配置为LVGL的显存,或者如何优化STM32的LTDC+SPI混合使用。但万变不离其宗,核心依然是理解SPI通信、显存管理、DMA应用以及图形库的整合。这块1.69英寸的SPI LCD,就像一扇小窗,背后连接的是一个充满可能性的嵌入式图形世界。从点亮它的第一个像素开始,你的项目就拥有了与用户视觉交互的灵魂。