简介:这款 Arduino 液晶库由 Stanley 编写并以 GPL 协议开源,面向 Arduino 开发者、电子爱好者及创客。它提供统一、易用的 API 驱动不同型号的液晶显示模块,可显示字符、位图与简单图形,能显著降低多屏适配与切换成本,适合需要同时维护多种屏幕或快速验证显示效果的项目。包体共 44 个文件、76KB,以 h 头文件和 cpp 驱动实现为主,另有 ino 示例和 cbp 工程文件,整体目录结构清晰,便于在 Arduino IDE 中直接加载并对照学习。资源涵盖 SSD1306、PCD8544、ILI9341、ILI9325D、SSD1289 等常见屏驱源码,并附 MicroLCD、UTFT、RGBMatrix 等硬件层实现,读者可获得驱动移植、初始化配置、字体与图形绘制,以及不同驱动芯片的时序适配与内存配置细节等完整参考。目前已有 2036 人学习下载,对需要快速集成多种液晶模块或深入了解 Arduino 显示底层逻辑的开发者,是一份实用的开源参考资料。 做 Arduino 项目的时候,最让我头疼的其实不是传感器,而是屏幕。屏幕型号多、接口杂、不同厂商的库调用方式还不一样,今天用 LiquidCrystal 写 1602,明天换一块 TFT 就得重新学另外一套 API,改到后面整个人都不想碰显示部分。后来我接触了 MultiLCD 这个 Arduino 液晶库,它把 1602 字符屏、5110 点阵屏、ST7789/ILI9341 的 TFT、SSD1306 的 OLED 这些常见的液晶屏和模块,收敛成一套调用接口。你只需要把屏幕对应的类名和构造参数换掉,初始化、清屏、光标、打印这些逻辑几乎不用动。这篇文章就把我实际用这个库踩过的坑和完整的用法记录下来,给同样被屏幕搞疯的人一个能直接抄作业的参考。
1. 项目背景与库的整体设计思路
1.1 为什么需要“一个库管多种屏幕”
过去我做小型仪表盘,一开始用 1602,后来嫌显示信息太少,换成 5110 点阵屏。本来以为只是“多接几根线”的事情,结果发现两个库的架构差别极大:1602 的lcd.print("hello")和 5110 的display.print("hello")虽然长得像,但背后的坐标系、字库、取模方式完全不一样。再往后换 ST7789 的 1.8 寸 TFT,又要引入 Adafruit_GFX 一整套依赖。项目里同时出现三套屏幕驱动,光是维护#include和条件编译就够烦了。
MultiLCD 这个库的设计思路正好撞在我的需求上:它把所有屏幕差别封装到底层驱动,对外暴露的是一套“普适接口”。你上层代码只需要知道自己要“打印一行字”“画一条线”“清个屏”,至于底层是字符屏还是图形屏,是 SPI 还是 I2C,都无所谓。这样带来的最大直接好处是:想换屏的时候,可以少改甚至不改业务逻辑,改动集中在屏幕对象的构造和接线定义。
1.2 库的架构是怎么组织的
MultiLCD 的典型结构是:一个公共基类定义操作接口,各种具体屏幕驱动程序继承并实现这套接口。比如公共基类会提供begin()、clear()、setCursor(x, y)、print()、home()这些基础方法,字符屏和图形屏再各自实现。
用我实际调用过的几个类举例:
LCD_1602/LCD_2004:对应 HD44780 字符屏,支持 4 位并行或通过 I2C 转接板。LCD_5110:对应 PCD8544,Nokia 5110 屏幕那种点阵屏。LCD_TFT:对应 ST7735、ST7789、ILI9341 这类 SPI TFT 屏。LCD_OLED:对应 SSD1306、SH1106 这类常见 OLED 屏。
这种结构让“换屏幕”变成一件特别直观的事。我在同一个项目里测试过 1602 和 TFT 两个版本,上层打印逻辑并没有重写,只是把LCD_1602 lcd;换成了LCD_TFT lcd;,然后把begin()的引脚参数和setCursor()的坐标含义对齐,就完成了替换。
1.3 主要支持的屏型与实际使用场景
我用的时间不算长,但几个常用型号都试过一遍,整理了一个简单对照表,方便你判断手上的屏幕能不能用这个库。
| 屏幕模块/芯片 | 类型 | 通信接口 | 典型分辨率 | 库中对应类 |
|---|---|---|---|---|
| HD44780(1602/2004) | 字符 | 并行4/8位,或I2C转接 | 16列2行 / 20列4行 | LCD_1602 / LCD_2004 |
| PCD8544(Nokia 5110) | 点阵图形 | SPI兼容 | 84×48 | LCD_5110 |
| ST7735 | TFT | SPI | 128×160,常见1.8寸 | LCD_TFT |
| ST7789 | TFT | SPI | 240×240 / 240×320,常见1.3/1.54寸 | LCD_TFT |
| ILI9341 | TFT | SPI/8080并行 | 240×320,常见2.8/3.5寸 | LCD_TFT |
| SSD1306 / SH1106 | OLED | I2C/SPI | 128×64 / 128×32,常见0.96寸 | LCD_OLED |
从应用场景看,1602 适合做最简单的状态显示,OLED 适合低功耗嵌入式设备,5110 适合性价比点阵应用,TFT 则适合要做复杂界面、小图标的项目。一台 Arduino 板子配一块小 TFT,再配合 MultiLCD 库,基本能覆盖大部分桌面级项目的显示需求。
2. 安装、接线与初始化
2.1 把库装进 Arduino IDE
安装步没什么特别的,有两种方式,任选其一。
第一种,直接下载 ZIP 压缩包,然后在 Arduino IDE 里选“项目 -> 加载库 -> 添加 .ZIP 库”,选中下载好的MultiLCD...zip文件,IDE 会自动解压并注册。第二种,在“库管理器”里搜索 MultiLCD,如果作者上传过官方版本,可以直接搜索安装,后续更新也方便。
装完之后,建议先跑一个库自带的示例,不要一上来就写自己的脚本。我之前跳过这一步,直接在业务代码里引用,结果连最基本的方法名对不对都没验证,后面排查了半天才发现是调用方式和库版本不一致。
2.2 接线先分清接口类型,别想当然
MultiLCD 库里不同屏幕的接线逻辑差异很大,最忌讳拿 1602 的引脚定义去套 TFT。先看模块的接口类型,再对照引脚接线。
对于并行 1602,常见的接法是:
- RS、E 接到 Arduino 数字口。
- D4~D7 接到四个数字口。
- 如果用了 PCF8574 I2C 转接板,就只剩 VCC、GND、SDA、SCL 四根线。
- 第三个引脚 V0(对比度)一般要接一个 10K 电位器,否则直接接地可能显示不出内容。
对于 SPI 接口的 TFT 和 5110,引脚通常是 SCLK、MOSI、CS/CE、DC、RST,可能还要加一个背光 BL 引脚。ST7735/ST7789 这类屏在 Arduino Uno 上可以直接复用硬件 SPI 的 SCK 和 MOSI 引脚,CS、DC、RST 这三个控制脚随意指定数字口就行。
2.3 初始化参数:不同屏的“打开方式”
每个屏的初始化方法都会有一些自己的参数,但 MultiLCD 把它们尽量统一了。比如字符屏的初始化是:
LCD_1602 lcd; // 声明对象 lcd.begin(12, 11, 5, 4, 3, 2); // RS, E, D4, D5, D6, D7TFT 屏的初始化可能变成:
LCD_TFT lcd(10, 9, 8); // CS, DC, RST lcd.begin();begin()里做的事情包括端口初始化、内部延时等待液晶控制器复位、发送初始化命令序列。所以如果你发现屏幕不显示,先检查begin()是否被调用,这是最简单也是最容易忽视的问题。
有一个经验:尽量把屏幕对象定义放在全局区,不要在setup()里用局部对象。因为局部对象在setup()结束后可能被释放或者内存布局发生变化,某些平台上会出现“开机正常几秒后花屏”的诡异现象。
3. 统一 API 实战:一套代码切换多种液晶屏
3.1 字符型 LCD 的经典写法
字符屏的 API 最直观,先看一个完整的 1602 示例。
#include <MultiLCD.h> LCD_1602 lcd; void setup() { lcd.begin(12, 11, 5, 4, 3, 2); // RS, E, D4, D5, D6, D7 lcd.setCursor(0, 0); lcd.print("MultiLCD Demo"); lcd.setCursor(0, 1); lcd.print("Hello, Maker!"); } void loop() { }setCursor()在字符屏里的两个参数是“列”和“行”,第一行第一列是(0, 0),最多到(15, 1)。这个坐标语义和图形屏是不一样的,后面切 TFT 时要特别注意。如果你想显示动态内容,比如温湿度传感器的值,可以用sprintf先把数值拼成字符串,再交给print()输出,这样能避免print各种重载方法在字符屏上产生的显示混乱。
3.2 LCD_TFT 的绘制调用
TFT 屏的 API 会更丰富,因为要支持画点、画线、填充矩形这些图形操作。我用的 ST7789 屏是 240x240 分辨率,示例代码大概长这样:
#include <MultiLCD.h> LCD_TFT lcd(10, 9, 8); // CS, DC, RST void setup() { lcd.begin(); lcd.fillScreen(COLOR_RED); lcd.setColor(COLOR_WHITE); lcd.drawLine(0, 0, 239, 239); lcd.drawRect(20, 20, 100, 60); lcd.setCursor(0, 0); lcd.print("TFT OK 240x240"); } void loop() { }fillScreen是整个屏幕填充一种颜色,setColor设置后续绘制颜色,drawLine和drawRect的坐标是像素坐标。这里需要特别说明的是,不同屏幕的色值定义可能不同,库一般会内置COLOR_WHITE、COLOR_RED这些常量,比你自己拼 RGB565 要省心得多。
OLED 的用法也类似,我手头 0.96 寸 I2C SSD1306 的初始化需要扫描地址,一般是0x3C或0x3D,然后:
LCD_OLED lcd(0x3C); lcd.begin();OLED 优点是对比度高、视角大,缺点是需要预留 I2C 总线,如果总线上还有别的设备,记得地址别冲突。
3.3 从 1602 换成 TFT,到底要改哪些地方
切换屏幕时,最关心的就是“我到底要改多少代码”。以我的经验,这三个地方是逃不掉要动的:
- 对象声明:
LCD_1602 lcd;改成LCD_TFT lcd(10, 9, 8);。 - 初始化:
begin()的参数从并行引脚变成 SPI 控制引脚。 - 坐标语义:字符屏
setCursor(0, 1)是第二行第一个字符,TFT 的setCursor(0, 1)如果对应像素坐标,那就很可能只差了 1 个像素,需要改成setCursor(0, 16)这样的值才能让文字出现在左上角。
至于print()、clear()这些基本方法,MultiLCD 尽量做成了通用接口,不用改。不过打印文字时字符屏自带字体,TFT 需要内置字库才能输出,如果你自定义了字体,那么切屏时也要把字库一起换掉,这点很容易被新手忽略。
4. 常见问题与排查技巧实录
4.1 白屏、黑屏、花屏:先按优先级排查
实际调试屏幕,最常见的就是这三个症状,我按遇到频率从高到低排个序。
第一类是“完全不亮”。先量背光供电,再看对比度。1602 的 V0 如果不接电位器,可能整个屏幕就是一片白底黑块,或者干脆没有任何内容。OLED 如果完全黑,大概率是 I2C 地址不对或者 SDA/SCL 接反。
第二类是“白屏”。TFT 白屏特别常见。可能原因包括:初始化命令序列没发成功、SPI 引脚没接对、CS/DC/RST 三根控制线搞混、屏幕驱动型号选错。比如 ST7789 的初始化命令和 ILI9341 不一样,库如果自动识别不了,就会白屏。要排查,先确认你实际模组用的是哪个主控芯片,再在库配置里指定对应型号,不要想当然。
第三类是“花屏”。TFT 花屏往往和时序有关。接线太长、杜邦线质量差、SPI 速率拉太高,都可能导致数据传输出错。我有一块 1.54 寸 TFT,刚开始怎么调都是下半屏花,后来发现是排线旁边横着一根电源线,干扰太严重了,把线分开之后问题就没了。
4.2 电压和电平:那个“AVDD 多少伏”的坑
屏幕的供电和逻辑电平问题很多人会忽略,等屏烧了才后悔。
多数 Arduino 模块是 5V 逻辑,但很多小屏,尤其是 ST7789 和 SSD1306,模块上面的 VCC 或者 VDD 都是 3.3V。如果你的 Arduino 板子是 5V,最好确认模块是否自带电平转换。网上很多几十块的小屏模组虽然标注“兼容 5V”,但其实是靠板载稳压芯片和电平转换实现的,如果是裸屏没有转换电路,5V 直接怼上去轻则白屏花屏,重则直接冒烟。
至于“AVDD 电压多少伏”这个问题,经常出现在 TFT 数据手册里。AVDD 是液晶面板内部的模拟电源电压,一般不是给用户直接使用的,常见值在 5V 到 7V 左右,但不同面板差异很大,必须查具体驱动芯片的手册。模组化的小屏通常已经用 DC-DC 升压把模块输入电压转换成内部需要的 AVDD,你不需要自己去调。库能做的事情,只是保证初始化时序正确,电源域这种硬件风险,只能靠你按模块说明书来。
4.3 I2C 地址扫描与引脚冲突
I2C 类屏幕最常见的问题是找不到设备。别猜,直接写个扫描程序,几秒钟就出结果。
#include <Wire.h> void setup() { Wire.begin(); Serial.begin(9600); Serial.println("I2C Scanner"); } void loop() { for (byte addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.print("Found I2C device: 0x"); Serial.println(addr, HEX); } } delay(1000); }如果扫描不到,先检查 SDA/SCL 是否接反,再接法参考 Uno 上的 A4/A5 引脚(不同板子不一样)。如果有多个 I2C 设备,还要看地址是否冲突。PCF8574 转接板上一般有 A0/A1/A2 三个跳线,可以改地址,0x3F、0x27 都是常见默认值。
还有一个容易忽略的问题:Uno 的 SDA/SCL 和某些 SPI 引脚共用,或者和中断引脚冲突。如果同一个引脚既要接屏幕又要接另一个外设,初始化顺序不对就会互相干扰。碰到这种情况,干脆给屏幕单独分引脚,能少很多莫名其妙的 bug。
4.4 SPI 接线太长导致花屏的规避
如果你的 ST7735/ST7789 接线特别长,比如超过 15cm,高速 SPI 下数据信号容易出现反射和串扰。最直接的解决办法是把 SPI 时钟降下来。在 MultiLCD 这类库中,一般有setSpeed()或者初始化参数里可以指定 SPI 频率,改成 4MHz 甚至 2MHz,很多花屏问题立刻消失。
另外,CS 引脚一定要接对并且保持有效电平,有些库在初始化期间会把 CS 拉低,如果 CS 悬空,控制器可能收不到命令。RST 引脚也不能省,有些模块上面虽然有复位电路,但初始化序列里仍然需要一次硬件复位,否则首次开机就是白屏。
5. 扩展:给 MultiLCD 增加新屏和跨平台移植
5.1 底层驱动是怎么组织的,如何加一块新屏
如果你手头有一块不在支持列表里的屏幕,也不用慌,MultiLCD 这类库的设计本身就方便扩展。你只需要理解它的顶层抽象:一个新屏幕驱动,本质上就是实现“清屏、定位、打印、画点”这几件事。
画点是最底层的基础操作,有了drawPixel(x, y, color),就能组合出画线、画矩形、画圆。新增屏幕时,先查阅主控芯片的数据手册,把初始化命令序列写对,再填上基础接口函数。我写过一个简化的类骨架,大概长这样:
class LCD_MyScreen : public LCD_Base { public: void begin() { // 初始化控制器,发送reset和命令序列 } void clear() { // 填充背景色或清空显示RAM } void setCursor(uint8_t x, uint8_t y) { // 设置显示RAM地址 } void print(const char *s) { // 把字符串映射到显示缓存 } };关键不是代码量,而是初始化时序。不同液晶控制器的命令字、延时要求、MADCTL 之类寄存器配置都不同,直接照抄别的型号大概率翻车。建议先用原厂或分厂商的资料验证单色/单行显示,再逐步加入图形接口。
5.2 在 ESP32、STM32 上使用时的注意点
如果你准备把 MultiLCD 用到非 AVR 平台,也就是 ESP32、STM32 这类开发板上,有几个点需要提前留意。
首先是引脚映射。ESP32 的硬件 SPI 分为 VSPI 和 HSPI,默认引脚和 Arduino Uno 完全不同。比如默认 SPI 时钟可能在 GPIO18,MOSI 在 GPIO23,不同的开发板丝印可能又重新映射过。接线之前先去确认自己板子的引脚图,别把 MISO 当 MOSI 用。
其次是逻辑电平。ESP32 和 STM32 基本都是 3.3V 逻辑,和大多数 3.3V 液晶模块匹配良好。但如果你还在板上插了 5V 外设,务必把电源域分离,否则逻辑电平不匹配会让屏幕显示颜色不准或闪烁。
再就是性能和内存。STM32 的底层 GPIO 操作和 AVR 不一样,有些库会为平台做条件编译。如果库本身没有适配,你可能要用 HAL 库自己改底层驱动,或者在 ESP32 上直接用 Arduino 核心层提供的 SPI API。好在 MultiLCD 封装出来的顶层接口在移植时不用动,真正需要改的是每个屏幕驱动里和引脚读写相关的底层代码。
还有一个实用建议:如果你没有实物,又想在动手前快速验证逻辑,可以先用 Wokwi 仿真平台模拟一块屏幕。仿真虽然不能完全复现硬件时序细节,但能检查引脚接法、代码逻辑这类“人容易犯的错”,等逻辑跑通了再上真硬件,能省不少调线时间。
最后再分享一个我自己的习惯:把各种屏幕的接线和初始化参数统一写到一个Display_Config.h文件里,每次换屏只改宏定义,不用反复翻代码。比如把 1602 的引脚定义、TFT 的 SPI 引脚和型号、OLED 的 I2C 地址都集中管理。这样物理换屏时,我只需要把对应宏切换掉,然后重新编译验证一遍。MultiLCD 的价值不在于让你少写几行代码,而在于它把“不同屏幕之间的差异”隔离在了一个可控位置,让你在硬件折腾中能少掉一层无谓的烦恼。
本文还有配套的精品资源,点击获取