
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面板。它支持多种颜色格式常见的有RGB56516位色R5G6B5和RGB66618位色。为了节省传输带宽和显存我们最常用的是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 i0; ipixels; 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 i0; iLCD_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专注于单色显示屏但也支持彩色。非常轻量适合资源极其有限的场景。TouchGFXST官方主推的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数据我们通常按“高字节-低字节”顺序发送这与存储顺序可能一致也可能不一致需要测试。5.4 功耗与背光控制对于电池供电设备屏幕是耗电大户。除了选择低功耗面板软件上可以动态调光根据环境光传感器如果有自动调整背光PWM占空比。睡眠模式在长时间不操作时通过发送命令SLPIN0x10让ST7789V2进入睡眠模式可以大幅降低功耗。唤醒时发送SLPOUT(0x11)命令并等待120ms以上。局部刷新如前所述只更新变化区域减少SPI总线活动时间也能间接降低功耗。驱动一块像1.69寸这样的SPI LCD模块从硬件连接到软件点亮再到优化和集成整个过程就像在跟一个沉默的伙伴对话。你需要按照它的协议SPI时序和语言寄存器命令来沟通。最大的成就感莫过于第一次看到屏幕按你的意愿亮起显示出一个清晰的图案或文字。过程中遇到的每一个问题——白屏、花屏、颜色不对、刷新慢——都是加深你对底层硬件理解的机会。我的建议是准备好逻辑分析仪哪怕是便宜的那种它对于调试SPI、I2C通信是无价之宝另外多利用现有的开源驱动代码作为参考但不要迷信一定要结合数据手册和自己的硬件环境去理解每一行配置的意义。最后当基础驱动稳定后尝试集成像LVGL这样的库你会发现为这个小屏幕打造一个交互式界面原来也可以如此高效和有趣。