2026/8/2 3:06:45

1.83英寸LCD驱动实战:从SPI接口到图形库集成全解析

1.83英寸LCD驱动实战:从SPI接口到图形库集成全解析 1. 项目缘起为什么是1.83英寸LCD最近在捣鼓一个需要显示交互信息的小玩意儿比如智能家居的温湿度面板、便携设备的参数监视器或者一个桌面小摆件。选屏幕的时候我绕开了那些常见的0.96寸、1.3寸直接盯上了1.83英寸的LCD模块。你可能要问市面上屏幕那么多为啥偏偏是它这背后其实是一系列非常实际的工程考量。首先尺寸是个黄金分割点。0.96寸太小显示内容有限看久了费眼2寸以上的屏虽然显示效果好但功耗和体积也上去了对于很多追求小巧、低功耗的嵌入式项目来说有点“杀鸡用牛刀”。1.83英寸差不多是46.5毫米的对角线这个尺寸刚好能容纳几行清晰的文字、简单的图标甚至是一些基础图形界面信息量足够又不会让整个设备变得笨重。它完美地平衡了“看得清”和“放得下”的矛盾。其次这类模块通常指的是基于SPI或I2C接口的TFT LCD驱动芯片常见的有ST7735S、ST7789等。它们色彩表现不错通常是65K色或262K色分辨率常见的是160x128或172x320竖屏刷新率也足以应付大多数非视频应用。最关键的是它们对主控MCU的要求非常友好一个STM32F103也就是我们常说的“蓝桥杯”芯片或者一块ESP32甚至是一块Arduino Uno都能轻松驱动极大地降低了开发门槛和成本。所以当你需要一个“不大不小、够用就好、容易上手”的显示方案时1.83英寸LCD模块往往会成为一个非常务实且高性价比的选择。它不像那些高端OLED屏一样追求极致的对比度和响应速度但它稳定、可靠、省电而且资料和社区支持非常丰富对于从学生到资深工程师的广大开发者来说都是一个极佳的练手和实战平台。2. 核心硬件解析模块接口与驱动原理拿到一块1.83英寸LCD模块别急着写代码先把它翻过来看看背面。理解它的硬件构成是后续一切顺利的基础。这类模块的硬件核心可以拆解为三个部分液晶面板本身、驱动芯片以及对外接口。2.1 液晶面板与驱动芯片的“共生关系”我们看到的彩色图像是由液晶面板上成千上万个微小的像素点组成的。但液晶分子自己不会发光也不会主动变色它需要一个“指挥官”来告诉每个像素点此时此刻应该显示什么颜色。这个指挥官就是驱动芯片比如ST7735S。你可以把驱动芯片想象成一个极其复杂的矩阵开关控制器。它内部有显存GRAM你通过单片机发送过来的图像数据实际上就是写入了这片显存。然后驱动芯片会根据自身的时序一行一行、一列一列地扫描这片显存并将对应的电压施加到液晶面板的相应像素电极上从而控制液晶分子的偏转让背光通常是白色LED透过滤光片形成我们看到的颜色。对于1.83寸模块ST7735S是绝对的主力。它支持最高262K色18位RGB即R/G/B各6位内置了显存正好匹配160x128这类分辨率。它的指令集非常丰富可以设置扫描方向、颜色模式、睡眠唤醒等。而ST7789则是它的“升级版”通常用于分辨率稍高如240x240或性能要求稍高的屏但用在1.83寸上也完全兼容且游刃有余。选择哪种主要看模块的具体型号但驱动逻辑大同小异。2.2 接口定义四线SPI是主流绝大多数1.83英寸LCD模块为了节省MCU的IO口都采用SPI串行外设接口通信。这里尤其要分清“三线SPI”和“四线SPI”。很多新手会在这里栽跟头。四线SPI标准SPI这是最常用、最稳定的方式。它需要4根信号线SCKSerial Clock时钟线由主控MCU产生用于同步数据。MOSIMaster Out Slave In主设备输出从设备输入。也就是MCU发送数据/命令给屏幕的线。DCData/Command数据/命令选择线。这是关键当DC线为低电平时MCU通过MOSI发送的是命令如设置地址、开关显示当DC线为高电平时发送的是像素数据。没有这根线屏幕就分不清你发来的是指令还是颜色。CSChip Select片选线。低电平有效用于在多个SPI设备中选择当前要通信的屏幕。如果系统里只有这一个SPI设备理论上可以将其永久接地但在实际调试时保留软件控制会更灵活。此外还有**RSTReset复位线和BLBack Light**背光控制线。RST用于硬件复位屏幕解决“屏幕花屏或无反应”的终极手段BL则控制背光LED的开关用于省电或实现呼吸灯效果。三线SPI节省IO模式有些教程或库为了在IO极其紧张的情况下比如某些Arduino项目使用会采用三线模式。它省去了DC线而是通过在一个字节的数据包中用最高位MSB来区分当前发送的是命令还是数据。例如规定MSB为0是命令为1是数据。这种方式需要驱动库在底层做特殊处理兼容性和稳定性通常不如四线SPI且对时序要求更严格新手不建议首选。2.3 电源与背光模块通常需要3.3V供电。绝对不要接5V虽然有些模块可能标注了宽电压范围但驱动芯片和液晶面板的核心电压往往是3.3V接5V极易烧毁。背光LED一般也是3.3V驱动电流在几十毫安左右。你可以直接用MCU的3.3V引脚供电但如果屏幕点亮后MCU出现复位或不稳定可能是背光启动瞬间电流较大导致的此时建议为背光单独供电或在其正极串联一个几欧姆的小电阻限流。3. 软件驱动实战从零点亮你的屏幕硬件连接妥当后真正的乐趣——编程就开始了。我们的目标是写一个最精简、最直白的驱动不依赖臃肿的库让你彻底明白每一个字节是如何让屏幕亮起来的。3.1 底层通信函数封装首先我们需要根据硬件连接用代码模拟出SPI的时序或者使用MCU自带的硬件SPI外设。这里以软件模拟SPI兼容性最好为例假设我们使用STM32的HAL库风格但逻辑通用。// 引脚定义 (根据你的实际连接修改) #define LCD_SCK_PIN GPIO_PIN_5 #define LCD_SCK_PORT GPIOA #define LCD_MOSI_PIN GPIO_PIN_7 #define LCD_MOSI_PORT GPIOA #define LCD_DC_PIN GPIO_PIN_6 #define LCD_DC_PORT GPIOA #define LCD_CS_PIN GPIO_PIN_4 #define LCD_CS_PORT GPIOA #define LCD_RST_PIN GPIO_PIN_3 #define LCD_RST_PORT GPIOA // 基本的GPIO操作宏 #define LCD_SCK_LOW() HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_RESET) #define LCD_SCK_HIGH() HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_SET) // ... 类似定义 MOSI, DC, CS, RST 的 HIGH/LOW 宏 // 软件SPI发送一个字节模式0CPOL0, CPHA0 void LCD_SPI_SendByte(uint8_t dat) { for(uint8_t i0; i8; i) { LCD_SCK_LOW(); // 时钟拉低 if(dat 0x80) { // 先发送最高位(MSB) LCD_MOSI_HIGH(); } else { LCD_MOSI_LOW(); } LCD_SCK_HIGH(); // 时钟上升沿数据被采样 dat 1; // 左移准备发送下一位 } } // 写命令 void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_LOW(); // DC线拉低表示接下来是命令 LCD_CS_LOW(); // 选中屏幕 LCD_SPI_SendByte(cmd); LCD_CS_HIGH(); // 取消选中 } // 写数据 void LCD_Write_Data(uint8_t dat) { LCD_DC_HIGH(); // DC线拉高表示接下来是数据 LCD_CS_LOW(); LCD_SPI_SendByte(dat); LCD_CS_HIGH(); }3.2 初始化序列唤醒屏幕的“咒语”驱动芯片上电后处于一个未知状态我们需要发送一系列特定的命令和参数来配置它。这个过程就是初始化。不同驱动芯片、甚至同芯片不同厂商的模块初始化序列都可能略有差异最佳参考来源是卖家提供的例程或芯片数据手册。下面是一个针对ST7735S驱动的简化初始化流程示例它包含了最关键的几个步骤void LCD_Init(void) { // 1. 硬件复位 LCD_RST_LOW(); HAL_Delay(100); // 保持低电平至少10ms LCD_RST_HIGH(); HAL_Delay(120); // 等待复位完成 // 2. 退出睡眠模式 LCD_Write_Cmd(0x11); // Sleep Out HAL_Delay(120); // 等待唤醒这个延迟很重要 // 3. 设置颜色格式 (RGB565: 16位2字节一个像素) LCD_Write_Cmd(0x3A); // COLMOD: Interface Pixel Format LCD_Write_Data(0x05); // 0x05 代表16位/pixel (RGB565) // 4. 设置显示方向 (0x00, 0x60, 0xC0, 0xA0 对应不同旋转) LCD_Write_Cmd(0x36); // MADCTL: Memory Data Access Control LCD_Write_Data(0x00); // 0x00 为默认方向横屏从左到右从上到下 // 5. 设置列地址和行地址范围全屏 // 对于160x128的屏X从0到159Y从0到127 LCD_Write_Cmd(0x2A); // CASET: Column Address Set LCD_Write_Data(0x00); LCD_Write_Data(0x00); // 起始列高8位低8位 LCD_Write_Data(0x00); LCD_Write_Data(0x9F); // 结束列 (0x9F 159) LCD_Write_Cmd(0x2B); // RASET: Row Address Set LCD_Write_Data(0x00); LCD_Write_Data(0x00); // 起始行 LCD_Write_Data(0x00); LCD_Write_Data(0x7F); // 结束行 (0x7F 127) // 6. 打开显示 LCD_Write_Cmd(0x29); // DISPON: Display On // 7. 打开背光如果背光由MCU控制 // LCD_BL_HIGH(); HAL_Delay(100); }3.3 绘制像素与填充区域初始化完成后屏幕是白色的或黑色取决于驱动芯片默认状态。要显示内容我们需要向显存写入数据。核心命令是0x2CRAMWR Memory Write。在发送这个命令后紧接着发送的所有数据都会被连续地写入显存直到CS片选被拉高。// 设置绘制窗口指定一块矩形区域 void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); // CASET LCD_Write_Data(x1 8); LCD_Write_Data(x1 0xFF); LCD_Write_Data(x2 8); LCD_Write_Data(x2 0xFF); LCD_Write_Cmd(0x2B); // RASET LCD_Write_Data(y1 8); LCD_Write_Data(y1 0xFF); LCD_Write_Data(y2 8); LCD_Write_Data(y2 0xFF); LCD_Write_Cmd(0x2C); // RAMWR 准备写入数据 } // 填充指定颜色到整个窗口 void LCD_Fill(uint16_t color) { uint32_t total_pixels (160 * 128); // 假设屏幕160x128 LCD_SetWindow(0, 0, 159, 127); LCD_DC_HIGH(); LCD_CS_LOW(); for(uint32_t i0; itotal_pixels; i) { // 发送颜色高8位和低8位 (RGB565) LCD_SPI_SendByte(color 8); LCD_SPI_SendByte(color 0xFF); } LCD_CS_HIGH(); } // 在指定坐标画一个点 void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { // 检查坐标是否在屏幕范围内 if(x 160 || y 128) return; LCD_SetWindow(x, y, x, y); LCD_Write_Data(color 8); LCD_Write_Data(color 0xFF); }现在调用LCD_Fill(0xF800)你的屏幕应该会变成红色RGB565中0xF800是纯红。恭喜你已经成功点亮了它4. 进阶应用与图形库集成能画点和填充只是万里长征第一步。要显示文字、图片和图形界面我们需要更高级的工具。4.1 显示位图BMP图片在嵌入式设备上显示图片通常需要先将图片转换成C语言数组。这个过程称为“取模”。你可以使用工具如Img2Lcd或LCD Image Converter。这些工具允许你设置输出格式如RGB565、扫描方向然后生成一个巨大的const uint16_t数组。显示图片的步骤是使用LCD_SetWindow设置图片要显示的区域。发送0x2C命令。循环将图片数组中的数据通过LCD_Write_Data或直接SPI发送出去。这里有个关键技巧直接使用内存到外设的数据传输DMA可以极大提高刷新速度避免在传输大数组时阻塞主程序。对于STM32你可以配置SPI的DMA通道将图片数组的地址作为源地址SPI的数据寄存器作为目标地址然后启动传输。在此期间CPU可以处理其他任务。4.2 集成轻量级图形库如u8g2, LVGL手动管理所有绘图太繁琐了。集成一个成熟的图形库是必由之路。u8g2这是一个非常强大且轻量的单色图形库虽然主打单色但其u8g2分支完美支持彩色LCD包括ST7735。它的字体和图形资源丰富API简洁。集成步骤通常是在你的工程中放入u8g2的源码。实现一个“回调函数”告诉u8g2如何驱动你的屏幕即实现u8x8_d_st7735_xxx驱动中的GPIO和SPI函数。初始化u8g2对象然后就可以调用u8g2_DrawBox、u8g2_DrawStr等高级函数了。LVGL这是一个功能完备的嵌入式GUI库支持动画、主题、各种控件按钮、滑块、图表等。它对硬件要求稍高需要更多的RAM和Flash但对于创建复杂的用户界面是终极选择。集成LVGL需要移植显示驱动实现lv_disp_drv_t注册的flush_cb回调在这个回调里将LVGL的图形缓冲区内容更新到屏幕。移植输入设备驱动如果需要触摸。配置心跳定时器用于驱动LVGL的内部任务。在主循环中不断调用lv_timer_handler()。4.3 显示中文字符显示英文很简单使用图形库内置的字体或取模即可。显示中文则需要引入中文字库。常见做法是全字库将整个GB2312或Unicode字库取模后存入外部Flash如W25Q64。优点是任意汉字都能显示缺点是占用空间巨大几MB到十几MB。部分字库只取项目用到的汉字。使用取模工具生成一个包含特定汉字点阵的C数组。这是最节省空间的方法。使用矢量字体如LVGL的Freetype支持这在资源丰富的平台如Linux MCU上是更优雅的方案但在资源紧张的单片机上不现实。一个实用的技巧是对于固定界面可以直接将包含中文的整张图片取模显示省去复杂的文字渲染逻辑。5. 性能优化与功耗控制当你的界面复杂起来或者需要电池供电时优化就显得至关重要。5.1 刷新率优化局部刷新与双缓冲全屏刷新LCD_Fill是最耗时的操作。优化策略是局部刷新只更新屏幕上发生变化的部分。例如一个变化的数字你只需要清除这个数字原来的区域再绘制新的数字而不是重绘整个背景。这需要你的程序逻辑能跟踪哪些区域是“脏的”。双缓冲在内存中开辟一块和屏幕显存一样大的缓冲区Frame Buffer。所有的绘图操作都先在这个缓冲区中进行。当一帧画面完全准备好后再一次性将整个缓冲区数据通过DMA快速传输到屏幕。这可以避免屏幕在绘制过程中出现撕裂感即看到一半旧内容一半新内容。对于160x128 RGB565的屏幕双缓冲需要160*128*2 40KB的RAM这对于许多单片机来说是奢侈的但ESP32或STM32H7这类芯片可以胜任。5.2 SPI时钟速度与数据传输提高SPI的时钟频率可以显著减少传输时间。但要注意屏幕驱动芯片有最高时钟限制ST7735S通常在15-30MHz。不要超过数据手册的规定。线长较长时过高的频率可能导致信号失真。如果屏幕出现雪花点或错位尝试降低SPI速度。使用硬件SPIDMA是速度的保证。软件模拟SPI的极限通常在10MHz以下而硬件SPI可以轻松达到几十MHz。5.3 功耗控制技巧对于便携设备屏幕是耗电大户。背光控制背光LED的电流通常在20-50mA。通过PWM控制背光亮度可以线性调节功耗。在不需要高亮度的场合如夜间大幅降低背光亮度能省很多电。睡眠模式当设备长时间不操作时可以通过发送命令如0x10进入睡眠模式让屏幕驱动芯片进入低功耗状态。此时屏幕不显示但显存内容会保留。唤醒时发送0x11命令屏幕会快速恢复显示比完全断电再上电要快得多。关闭显示发送0x28命令可以关闭显示但驱动芯片和背光可能还在工作功耗比睡眠模式高但唤醒最快。6. 常见问题排查与实战心得玩了这么多块屏幕坑没少踩。下面这些问题是新手甚至老手最容易遇到的我把排查思路和解决方法整理出来。6.1 屏幕白屏或花屏这是最经典的问题。请按以下顺序排查电源和电压万用表量一下VCC和GND之间是不是稳定的3.3V背光电压对吗这是所有问题的前提。复位信号确保RST引脚完成了正确的复位序列低电平10ms - 高电平。有些模块对复位时序很敏感尝试在初始化代码的最开始手动拉低再拉高一次RST并适当延长复位后的延时比如150ms。初始化序列这是花屏的重灾区不同厂家、不同批次的模块初始化命令可能微调。尤其是0x36MADCTL扫描方向和0x3ACOLMOD颜色格式这两个命令的参数。如果你发现颜色错乱比如红色显示成蓝色或者图像是镜像/旋转的大概率是这两个命令的参数不对。最可靠的方法是找到卖家提供的、针对你手上这块屏幕的示例代码直接复制它的初始化函数。SPI时序与模式确认你的SPI模式CPOL/CPHA与屏幕要求一致。ST7735通常使用模式0。检查DC线切换的时机是否严格在CS有效期间、且在SCK时钟变化之前完成。焊接与连接仔细检查杜邦线或焊点特别是时钟线SCK和数据线MOSI虚焊或接触不良会导致数据错位产生随机花屏。6.2 显示内容错位或只有一部分窗口设置错误0x2A和0x2B命令设置的X、Y地址范围不对。确认你设置的起始和结束地址覆盖了你想要绘制的区域。例如你的屏幕是160x128但你设置窗口为(10,10)到(150,118)那么你绘图时坐标就要做相应的偏移。扫描方向与坐标映射0x36命令不仅控制旋转还控制行列地址的增减方向。如果你设置MADCTL为0xC0垂直翻转行列交换那么你原本的X坐标实际上对应到了屏幕的Y轴。理解MX,MY,MV这几个位的含义至关重要。最简单的调试方法是写一个循环从(0,0)开始画一条斜线到(159,127)观察线条在屏幕上的走向反向调整MADCTL参数。6.3 刷新速度慢有肉眼可见的刷新过程未使用硬件SPI软件模拟SPI是速度瓶颈。切换到硬件SPI并将时钟频率提升到芯片允许的最高值。频繁全屏刷新优化你的程序逻辑避免不必要的全屏重绘。使用局部刷新。没有使用DMA对于填充大块颜色或显示图片使用DMA传输可以解放CPU让刷新在后台进行主程序流畅度会大幅提升。图形库开销像LVGL这样的库其渲染本身需要计算时间。对于复杂界面确保你的MCU主频足够高比如100MHz并合理使用LVGL的局部刷新机制。6.4 个人实战心得细节决定成败上电顺序有些屏幕对电源和信号的上电顺序有要求。一个稳妥的做法是先确保MCU的IO口处于高阻或确定状态再给屏幕供电最后进行复位和初始化。避免屏幕在IO口状态不稳定时接收到乱码。延时的重要性数据手册里的Delay建议值尤其是退出睡眠模式0x11后的120ms延时不要随意缩短。屏幕驱动芯片内部需要时间稳定贪快省掉延时是导致初始化失败的常见原因。购买时索要资料下单买屏幕时直接向卖家索要“针对此型号屏幕的Arduino/STM32例程”。这份代码的价值往往超过屏幕本身它能帮你省去无数调试时间。善用逻辑分析仪当程序怎么调都不对时一个几十块钱的逻辑分析仪配合PulseView软件是你的救命稻草。抓取SPI线上的实际波形对照数据手册的时序图可以清晰地看到命令、数据、DC信号是否完全正确。这是定位硬件通信问题的终极手段。背光串联电阻如果屏幕点亮瞬间MCU会复位可以在背光LED的正极串联一个10-22欧姆的电阻可以有效抑制浪涌电流。或者在程序初始化完成后再打开背光。从点亮第一个像素到驱动复杂的图形界面与1.83英寸LCD模块打交道的过程是一个典型的嵌入式系统软硬件协同调试的缩影。它涉及GPIO控制、SPI通信协议、时序理解、内存管理、图形算法等多个层面。把这个小模块玩透你对嵌入式外设驱动的理解会上一个大台阶。