1. 项目概述:一块1.69英寸LCD模块的深度探索
最近在捣鼓一个小型嵌入式项目,需要一个既能显示丰富信息又足够小巧的屏幕。市面上选择不少,但综合分辨率、接口易用性和性价比,一块基于ST7789V2驱动芯片的1.69英寸IPS LCD模块进入了我的视线。这玩意儿别看个头小,240x280的分辨率,加上IPS硬屏的可视角度,用在便携设备或者作为主控的调试信息屏再合适不过了。很多朋友可能对这类小屏又爱又怕,爱它的精致,怕驱动起来麻烦。其实,只要你摸清了它的“脾气”——主要是SPI通信协议和ST7789V2这颗驱动芯片的寄存器配置——让它点亮并流畅显示图形文字,并没有想象中那么复杂。这篇文章,我就结合自己实际调试这块屏的经历,从硬件连接到软件驱动,再到性能优化和常见坑点,给你掰开揉碎了讲清楚。
2. 核心硬件解析与接口选择
2.1 模块核心:ST7789V2驱动芯片探秘
这块1.69寸屏的核心是ST7789V2驱动控制器。你可以把它理解成屏幕的“大脑”,我们的主控(比如STM32、ESP32)并不直接操控成千上万的像素点,而是通过向ST7789V2发送命令和数据,由它来负责具体的扫描、刷新和显示。
ST7789V2内部集成了一块显存(GRAM),大小正好对应屏幕的分辨率(240x280)。当我们想更新屏幕内容时,实际上是在通过SPI接口向这片GRAM写入数据。芯片会按照设定好的扫描方向(从左到右、从上到下,或者旋转90度等),自动从GRAM中读取数据并转换成对应的RGB信号,驱动LCD面板。
它支持多种颜色格式,常见的有RGB565(16位色,R5G6B5)和RGB666(18位色)。为了节省传输带宽和显存,我们最常用的是RGB565格式,这也是大多数嵌入式GUI库的默认格式。16位色对于很多应用来说,色彩表现已经足够丰富。
2.2 接口对比:为何选择SPI?
这类小型LCD模块常见的接口有SPI和8080并行接口。这块1.69寸模块通常采用SPI接口,原因很直接:
- 节省引脚:SPI通常只需要4根线(SCK时钟、MOSI主出从入、DC命令/数据选择、CS片选),再加上背光控制和复位,总共6-7个IO口就够了。而8080并行接口需要至少8位数据线(D0-D7)加上若干控制线,对引脚紧张的单片机(尤其是小封装的)是巨大负担。
- 布线简单:线少意味着PCB布线简单,抗干扰能力相对更强,尤其适合模块与主控板分离的场景。
- 速度权衡:对于240x280这种分辨率,全屏刷新一帧的数据量是 240 * 280 * 2 bytes = 134,400 字节。在SPI时钟频率较高的情况下(比如40MHz以上),刷新率可以达到几十Hz,满足大多数非高速动画的应用场景。如果追求极致的刷新速度(比如游戏),才会考虑并行接口。
所以,SPI是在引脚资源、开发难度和性能之间一个非常平衡的选择。模块上的SCK、SDA(即MOSI)、DC、CS、BL(背光)、RST(复位)这几个引脚,就是我们需要连接和控制的全部。
注意:模块上标注的
SDA在SPI模式下就是MOSI(主机输出),不要和I2C的SDA混淆。有些模块会同时引出SPI和I2C的焊盘,需要根据丝印或说明书确认。
2.3 电源与背光电路要点
模块通常需要3.3V供电。务必确保你的电源能提供足够的电流,尤其是在背光全亮的时候。背光(BL引脚)一般是高电平点亮,可以通过一个GPIO口直接驱动(如果电流不大,通常<20mA),或者更稳妥地用三极管或MOS管来驱动,以实现PWM调光。
RST是硬件复位引脚,低电平有效。上电后,一个低电平脉冲(持续几毫秒)来确保芯片内部状态复位是良好的习惯。有些驱动库会依赖硬件复位,有些则主要靠软件复位命令。
3. 底层驱动实现:从SPI配置到图形显示
3.1 SPI外设初始化与配置
要让数据飞起来,首先得把单片机的SPI外设配置好。这里以STM32的HAL库为例,但思路是通用的。
关键配置参数:
- 模式:ST7789V2的SPI模式是Mode 3。即时钟极性(CPOL)为高电平空闲,时钟相位(CPHA)为在第二个边沿采样数据。配置错误会导致通信完全失败。
- 数据大小:8位(DataSize)。
- 位序:MSB先行(FirstBit)。
- 时钟频率:初始化和配置寄存器时,频率可以低一些(如10MHz),确保稳定性。在后续传输大量显存数据时,可以尝试提高到芯片和布线允许的极限(如40MHz、80MHz)。ST7789V2的最高SPI时钟频率需要查手册,常见规格是62.5MHz。
- NSS(片选)管理:建议使用软件控制(
NSS = Soft)。这样我们可以通过GPIO手动控制CS引脚,在传输间隙拉高,符合SPI总线规范,也便于驱动多个SPI设备。
// STM32 HAL SPI 初始化结构体示例(部分关键设置) hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工,但只使用发送 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL = 1 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA = 1 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 假设系统时钟80MHz,则SPI时钟20MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_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(); }3.2 基础通信函数编写
驱动库的基石是两个最基础的函数:写命令(WriteCommand)和写数据(WriteData)。它们的区别仅在于DC引脚的电平。
// 假设已定义好对应的GPIO引脚:LCD_CS_Pin, LCD_DC_Pin, LCD_RST_Pin // 以及SPI句柄:hspi1 void LCD_WriteCommand(uint8_t cmd) { LCD_CS_LOW(); // 使能片选 LCD_DC_LOW(); // DC置低,表示接下来是命令 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); // 关闭片选 } void LCD_WriteData(uint8_t dat) { LCD_CS_LOW(); LCD_DC_HIGH(); // DC置高,表示接下来是数据 HAL_SPI_Transmit(&hspi1, &dat, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); } void LCD_WriteData_Burst(uint8_t *pData, uint32_t len) { // 用于连续写入大量数据,比如GRAM数据 LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit(&hspi1, pData, len, HAL_MAX_DELAY); LCD_CS_HIGH(); }实操心得:在连续写入大量数据(如一整屏的像素数据)时,使用
LCD_WriteData_Burst这样的函数,一次性传输所有数据,而不是循环调用单字节写入函数,可以极大提升效率。因为每次CS拉低和拉高,SPI硬件都需要重新同步,会产生额外开销。
3.3 初始化序列与屏幕设置
上电后,ST7789V2需要一系列初始化命令来设置工作模式。这些命令序列通常由模块厂商提供,或者可以从开源驱动库(如Arduino的Adafruit_ST7789库)中找到。初始化流程一般如下:
- 硬件复位:拉低
RST引脚至少10ms,然后拉高,等待至少120ms让芯片稳定。 - 发送初始化命令序列:包括退出睡眠模式、设置颜色格式(RGB565)、设置扫描方向、打开显示等。
- 设置显示窗口:通过
CASET(列地址设置)和RASET(行地址设置)命令,告诉驱动芯片接下来要写入数据的GRAM区域。通常初始化时设置为全屏(0,239列,0,279行)。 - 发送
RAMWR命令:该命令之后发送的数据,就会被写入到之前设置的GRAM窗口中。
一个关键的初始化命令是设置内存数据访问控制(MADCTL),它决定了扫描方向、颜色字节顺序等,直接影响你显示的方向和颜色是否正确。
// 示例:设置屏幕为RGB565格式,扫描方向为常规(行地址自增,列地址自增) LCD_WriteCommand(0x3A); // 接口像素格式 LCD_WriteData(0x55); // 16位/pixel (RGB565) LCD_WriteCommand(0x36); // 内存数据访问控制 MADCTL // 参数含义:MY MX MV RGB MH - 具体位定义需查手册 // 0x00 通常表示:行地址顺序正常,列地址顺序正常,不旋转,RGB顺序,水平刷新顺序正常 LCD_WriteData(0x00);3.4 实现像素与图形绘制函数
有了基础的读写函数,我们就可以构建更上层的图形功能了。核心是SetWindow和DrawPixel。
设置窗口函数:此函数用于设定接下来要写入的GRAM矩形区域。它先发送CASET命令和起始/结束列地址,再发送RASET命令和起始/结束行地址,最后发送RAMWR命令。
void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { // 设置列地址 LCD_WriteCommand(0x2A); // CASET LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteData(x2 >> 8); LCD_WriteData(x2 & 0xFF); // 设置行地址 LCD_WriteCommand(0x2B); // RASET LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteData(y2 >> 8); LCD_WriteData(y2 & 0xFF); // 准备写入GRAM LCD_WriteCommand(0x2C); // RAMWR }画点函数:在指定坐标画一个点。原理是先设置窗口为该点所在的单个像素区域,然后连续写入两个字节的RGB565颜色数据。
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return; // 边界检查 LCD_SetWindow(x, y, x, y); // 发送颜色数据,高位在前 LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); }填充矩形函数:这是优化显示效率的关键。与其循环画点,不如一次性设置一个矩形窗口,然后连续发送整个矩形的颜色数据。
void LCD_FillRect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { uint32_t pixels = (uint32_t)(x2 - x1 + 1) * (y2 - y1 + 1); LCD_SetWindow(x1, y1, x2, y2); // 使用DMA或循环连续写入 LCD_CS_LOW(); LCD_DC_HIGH(); for(uint32_t i=0; i<pixels; i++) { uint8_t buffer[2] = {color >> 8, color & 0xFF}; HAL_SPI_Transmit(&hspi1, buffer, 2, HAL_MAX_DELAY); } LCD_CS_HIGH(); }有了DrawPixel和FillRect,再结合一些算法,画线、画圆、显示位图等功能都可以逐步实现。
4. 性能优化与高级技巧
4.1 启用DMA传输以解放CPU
当需要刷新全屏或大块区域时,使用CPU通过SPI一个字节一个字节地发送数据(即使是用HAL_SPI_Transmit)会大量占用CPU时间。此时,DMA(直接存储器访问)是救星。DMA可以在CPU不干预的情况下,自动将内存中的数据搬运到SPI的数据寄存器中发送出去。
配置步骤:
- 在CubeMX中启用SPI对应通道的DMA(通常是TX方向)。
- 在代码中,使用
HAL_SPI_Transmit_DMA函数替代HAL_SPI_Transmit。 - 准备一个足够大的缓冲区(例如全屏缓冲区
uint16_t frameBuffer[LCD_WIDTH * LCD_HEIGHT]),将需要显示的数据预先填充到这个缓冲区。 - 调用DMA传输函数发送整个缓冲区。
// 准备全屏数据(示例:填充为红色) uint16_t red = 0xF800; // RGB565 红色 for(int i=0; i<LCD_WIDTH*LCD_HEIGHT; i++) { frameBuffer[i] = red; } // 设置窗口为全屏 LCD_SetWindow(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1); // 启动DMA传输 LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)frameBuffer, LCD_WIDTH*LCD_HEIGHT*2); // 注意:传输完成后(可在SPI传输完成回调函数中),需要拉高CS踩坑记录:使用DMA时,必须注意数据对齐和缓冲区生命周期。确保
frameBuffer在DMA传输期间一直有效(通常定义为全局静态数组)。另外,DMA传输完成中断中拉高CS引脚是关键,否则SPI总线会一直处于占用状态。
4.2 双缓冲与局部刷新策略
对于动态显示(如UI界面),频繁全屏刷新既没必要也浪费资源。可以采用两种策略:
- 局部刷新:只更新屏幕上发生变化的部分区域。这需要你的图形库或应用逻辑能够追踪“脏矩形”区域,然后只调用
FillRect或DMA传输该区域的数据。 - 双缓冲:在内存中维护两个完整的屏幕缓冲区(
frontBuffer和backBuffer)。所有绘图操作都在backBuffer中进行。当一帧绘制完成后,通过一次DMA传输,将整个backBuffer复制到屏幕的GRAM中。然后交换两个缓冲区的角色。这可以避免屏幕撕裂,但会消耗双倍内存。对于240x280的16位色屏幕,双缓冲需要约 2402802*2 ≈ 268.8KB 的内存,这对某些单片机(如STM32F103)是巨大压力,但对于STM32H750这类拥有大容量RAM的芯片则很轻松。
4.3 集成轻量级GUI库
如果项目需要显示复杂的用户界面,手动管理所有绘图会非常繁琐。可以考虑集成轻量级的嵌入式GUI库,它们已经实现了控件(按钮、标签、滑块)、字体、事件管理等高级功能。常见的适合单片机的GUI库有:
- LVGL:当前最流行的开源嵌入式图形库,功能强大,文档丰富,对STM32系列支持很好。它本身需要一定的RAM和Flash,但提供了丰富的配置选项来裁剪。
- u8g2:专注于单色显示屏,但也支持彩色。非常轻量,适合资源极其有限的场景。
- TouchGFX:ST官方主推的GUI解决方案,基于C++,工具链强大,生成代码效率高,但相对封闭,对硬件有要求。
将我们写好的底层驱动(DrawPixel,FillRect等)按照这些GUI库要求的“显示设备驱动接口”进行封装,就可以让GUI库接管上层的绘图逻辑,我们只需专注于业务应用开发。
5. 典型问题排查与调试心得
驱动一块新屏幕,很少能一次成功。下面是一些我遇到过的典型问题及解决方法。
5.1 屏幕白屏或花屏
这是最常见的问题。
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 完全白屏,背光亮 | 1. 初始化序列未正确执行或缺失关键命令。 2. SPI模式(CPOL/CPHA)设置错误。 3. 电源电压不足或不稳。 | 1. 用逻辑分析仪或示波器抓取SPI时序,确认CS,DC,SCK,MOSI信号是否正确。2. 核对 MADCTL、像素格式等初始化命令的值。3. 测量模块供电电压(3.3V)和背光电压。 |
| 花屏(错乱色块) | 1. 颜色数据格式(RGB565/RGB666)设置与发送的数据不匹配。 2. 扫描方向( MADCTL)设置错误,导致像素错位。3. 显存窗口( CASET/RASET)设置错误,数据写到了GRAM之外。 | 1. 尝试发送纯色(红、绿、蓝)填充全屏,看颜色是否正确。 2. 调整 MADCTL寄存器中的MY、MX、MV位,观察屏幕旋转和镜像变化。3. 单步调试,确认发送的坐标和颜色数据值是否符合预期。 |
| 屏幕部分区域显示正常,部分异常 | 1. 屏幕物理损坏(排线接触不良、面板坏点)。 2. 驱动芯片部分GRAM损坏(罕见)。 | 1. 轻轻按压排线连接处,观察显示是否有变化。 2. 尝试显示不同的测试图案,看异常区域是否固定。 |
5.2 SPI通信速率与稳定性问题
- 问题:提高SPI时钟频率后,显示出现毛刺、错线或通信失败。
- 排查:
- 硬件布线:检查SCK和MOSI走线是否过长,是否靠近干扰源。高速SPI需要尽可能短的走线。
- 上拉电阻:如果线长超过10cm,考虑在SCK和MOSI上添加一个4.7kΩ - 10kΩ的上拉电阻到3.3V,增强信号完整性。
- 电源去耦:确保模块的VCC和GND引脚附近有足够的滤波电容(如一个10uF电解电容并联一个0.1uF陶瓷电容)。
- 软件延时:在关键命令(如复位、退出睡眠)后,确保留有足够长的延时(参考数据手册,通常几十到几百毫秒)。
5.3 集成DMA时的陷阱
- 问题:使用DMA刷新屏幕,第一次正常,后续刷新卡住或花屏。
- 排查:
- DMA传输完成中断:必须在DMA传输完成的回调函数(
HAL_SPI_TxCpltCallback)中,执行拉高CS、切换缓冲区等操作。并确保中断优先级设置合理,不会被其他中断长时间阻塞。 - 缓冲区竞争:在双缓冲模式下,确保CPU在向
backBuffer绘图时,DMA没有正在传输这个缓冲区。通常需要简单的信号量或标志位进行同步。 - 数据对齐:如果使用
uint16_t类型的颜色缓冲区,传递给HAL_SPI_Transmit_DMA时需要转换为(uint8_t*),并注意字节序。STM32是小端模式,而RGB565数据我们通常按“高字节-低字节”顺序发送,这与存储顺序可能一致也可能不一致,需要测试。
- DMA传输完成中断:必须在DMA传输完成的回调函数(
5.4 功耗与背光控制
对于电池供电设备,屏幕是耗电大户。除了选择低功耗面板,软件上可以:
- 动态调光:根据环境光传感器(如果有)自动调整背光PWM占空比。
- 睡眠模式:在长时间不操作时,通过发送命令(
SLPIN0x10)让ST7789V2进入睡眠模式,可以大幅降低功耗。唤醒时发送SLPOUT(0x11)命令,并等待120ms以上。 - 局部刷新:如前所述,只更新变化区域,减少SPI总线活动时间,也能间接降低功耗。
驱动一块像1.69寸这样的SPI LCD模块,从硬件连接到软件点亮,再到优化和集成,整个过程就像在跟一个沉默的伙伴对话。你需要按照它的协议(SPI时序)和语言(寄存器命令)来沟通。最大的成就感莫过于第一次看到屏幕按你的意愿亮起,显示出一个清晰的图案或文字。过程中遇到的每一个问题——白屏、花屏、颜色不对、刷新慢——都是加深你对底层硬件理解的机会。我的建议是,准备好逻辑分析仪(哪怕是便宜的那种),它对于调试SPI、I2C通信是无价之宝;另外,多利用现有的开源驱动代码作为参考,但不要迷信,一定要结合数据手册和自己的硬件环境去理解每一行配置的意义。最后,当基础驱动稳定后,尝试集成像LVGL这样的库,你会发现为这个小屏幕打造一个交互式界面,原来也可以如此高效和有趣。