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根信号线:
- SCK(Serial Clock):时钟线,由主控(MCU)产生,用于同步数据。
- MOSI(Master Out Slave In):主设备输出,从设备输入。也就是MCU发送数据/命令给屏幕的线。
- DC(Data/Command):数据/命令选择线。这是关键!当DC线为低电平时,MCU通过MOSI发送的是命令(如设置地址、开关显示);当DC线为高电平时,发送的是像素数据。没有这根线,屏幕就分不清你发来的是指令还是颜色。
- CS(Chip Select):片选线。低电平有效,用于在多个SPI设备中选择当前要通信的屏幕。如果系统里只有这一个SPI设备,理论上可以将其永久接地,但在实际调试时,保留软件控制会更灵活。
- 此外,还有**RST(Reset)复位线和BL(Back 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发送一个字节(模式0,CPOL=0, CPHA=0) void LCD_SPI_SendByte(uint8_t dat) { for(uint8_t i=0; i<8; 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到159,Y从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 绘制像素与填充区域
初始化完成后,屏幕是白色的(或黑色,取决于驱动芯片默认状态)。要显示内容,我们需要向显存写入数据。核心命令是0x2C(RAMWR, 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 i=0; i<total_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速度。
- 使用硬件SPI+DMA是速度的保证。软件模拟SPI的极限通常在10MHz以下,而硬件SPI可以轻松达到几十MHz。
5.3 功耗控制技巧
对于便携设备,屏幕是耗电大户。
- 背光控制:背光LED的电流通常在20-50mA。通过PWM控制背光亮度,可以线性调节功耗。在不需要高亮度的场合(如夜间),大幅降低背光亮度能省很多电。
- 睡眠模式:当设备长时间不操作时,可以通过发送命令(如
0x10进入睡眠模式)让屏幕驱动芯片进入低功耗状态。此时屏幕不显示,但显存内容会保留。唤醒时发送0x11命令,屏幕会快速恢复显示,比完全断电再上电要快得多。 - 关闭显示:发送
0x28命令可以关闭显示(但驱动芯片和背光可能还在工作),功耗比睡眠模式高,但唤醒最快。
6. 常见问题排查与实战心得
玩了这么多块屏幕,坑没少踩。下面这些问题是新手(甚至老手)最容易遇到的,我把排查思路和解决方法整理出来。
6.1 屏幕白屏或花屏
这是最经典的问题。请按以下顺序排查:
- 电源和电压:万用表量一下VCC和GND之间是不是稳定的3.3V?背光电压对吗?这是所有问题的前提。
- 复位信号:确保RST引脚完成了正确的复位序列(低电平>10ms -> 高电平)。有些模块对复位时序很敏感,尝试在初始化代码的最开始,手动拉低再拉高一次RST,并适当延长复位后的延时(比如150ms)。
- 初始化序列:这是花屏的重灾区!不同厂家、不同批次的模块,初始化命令可能微调。尤其是
0x36(MADCTL,扫描方向)和0x3A(COLMOD,颜色格式)这两个命令的参数。如果你发现颜色错乱(比如红色显示成蓝色)或者图像是镜像/旋转的,大概率是这两个命令的参数不对。最可靠的方法是找到卖家提供的、针对你手上这块屏幕的示例代码,直接复制它的初始化函数。 - 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通信协议、时序理解、内存管理、图形算法等多个层面。把这个小模块玩透,你对嵌入式外设驱动的理解会上一个大台阶。