串口字符型LCD实战指南:从零实现字符串显示,告别繁琐引脚连接
你有没有遇到过这样的场景?项目快收尾了,突然要加个本地显示屏。翻出一块1602 LCD,结果发现MCU剩下的GPIO不够用了——光是并行驱动就得占6~11个引脚,瞬间头大。
别急,这正是串口字符型LCD的用武之地。
它能把原本复杂的LCD驱动变成“一句话打印”级别的操作。你只需要像往串口发数据一样,printf("Hello World");屏幕上就出来了。没有时序控制、不用管使能信号、无需处理忙标志——这一切都由模块内部自动完成。
今天我们就来手把手拆解这个“嵌入式界的快捷显示方案”,带你真正搞懂它是怎么工作的,又是如何在资源紧张的系统中救场的。
为什么传统LCD这么麻烦?
在讲解决方案之前,先看看问题本身。
标准的HD44780兼容液晶屏(比如常见的1602、2004)本质上是一个“慢速并行设备”。它的通信方式很原始:
- 要么走8位并行模式:D0~D7 全部接上,再加 RS、RW、E 三个控制线;
- 要么走4位半字节模式:只用 D4~D7,但需要分两次发送高低4位,依然得配合 RS、E 等控制信号。
这意味着你在代码里得反复模拟时序:
set_data_pins(high_nibble); set_rs(1); set_e(1); delay_us(1); set_e(0); // 脉冲触发 delay(1); // 等待处理 set_data_pins(low_nibble); set_e(1); delay_us(1); set_e(0);不仅代码冗长,还容易因延时不准确导致初始化失败。更别说当你的MCU只有十几个可用IO时,光一个屏幕就把资源耗掉一半,根本不现实。
于是,聪明的工程师想到了一个办法:把这部分复杂逻辑外包出去。
串口型LCD的本质:把驱动“黑盒化”
所谓“串口字符型LCD”,并不是说液晶屏本身支持串行协议——而是在普通LCD前面加了个“翻译官”。
这个“翻译官”可能是:
- 一颗单片机(如STC系列),负责监听UART;
- 或者一个I²C转GPIO芯片(如PCF8574T),把I²C数据映射成并行控制信号;
无论哪种方案,对外表现都是:你只要给它串行数据,它就能帮你把字符显示出来。
我们来看两种最主流的实现路径:UART直驱型和I²C扩展型。
方案一:UART串口型LCD —— “会说话”的屏幕
它是怎么工作的?
这类模块内部集成了一个微控制器或专用协议解析芯片。你通过TX→RX连一根线过去,就可以直接发送字符串和命令。
举个例子:
Serial.print("Hello!");屏幕上立刻出现Hello!
如果想清屏?
Serial.write(0xFE); // 命令前缀 Serial.write(0x01); // 清屏指令就这么简单。背后的机制其实也不复杂:
- 模块上电后启动内部MCU,初始化背后的HD44780屏;
- 开启UART接收中断,等待主机发来的字节;
- 收到第一个字节判断是否为
0xFE:
- 是 → 进入命令模式,下一个字节作为指令执行;
- 否 → 当作ASCII字符写入当前光标位置; - 自动管理光标移动、换行、DDRAM地址更新等细节。
小知识:很多模块使用的是私有协议,但基本都兼容JHD1602-UART系列定义的标准。例如
0xFE开头表示命令,0x7C可用于设置背光亮度。
实战示例:Arduino驱动UART-LCD
假设你有一块WaveShare出品的UART 1602模块,连接如下:
| MCU (Arduino) | LCD Module |
|---|---|
| 5V | VCC |
| GND | GND |
| TX | RX |
注意:这里MCU的TX接LCD的RX,因为模块是“接收方”。
代码可以直接这样写:
#define LCD Serial1 // 使用硬件Serial1(Uno需换软串口) void setup() { Serial.begin(9600); // 调试输出 LCD.begin(9600); // 必须与模块波特率一致! delay(100); lcd_command(0x01); // 清屏 delay(2); // HD44780要求延迟至少1.5ms LCD.print("UART LCD Ready"); } void loop() { static int cnt = 0; char buf[17]; snprintf(buf, sizeof(buf), "Count: %06d", cnt++); lcd_goto_line(2); // 移动到第二行 LCD.print(buf); delay(1000); } // 发送命令 void lcd_command(uint8_t cmd) { LCD.write(0xFE); // 命令引导符 LCD.write(cmd); } // 光标跳转至第2行起始位置 void lcd_goto_line(int line) { if (line == 1) lcd_command(0x80); else if (line == 2) lcd_command(0xC0); }你会发现,整个过程几乎不需要关心底层时序。模块已经替你完成了所有繁琐的操作。
关键优势总结
| 优点 | 说明 |
|---|---|
| 仅需1根数据线 | RX即可工作,有些带TX反馈状态 |
| 即插即用 | 不需要编写复杂的初始化序列 |
| 透明传输 | 打印即显示,开发体验接近调试串口 |
| 抗干扰强 | 可选差分通信版本(RS485接口)用于工业环境 |
特别适合用于远程面板、外壳封闭设备、布线空间受限的应用。
方案二:I²C字符型LCD —— 最受欢迎的“省脚神器”
如果说UART型是“极简主义者”的选择,那I²C型就是“生态最好、资料最多”的代表。
你可能已经在无数教程里见过这块蓝色小板子:背面焊着PCF8574T芯片,标签写着0x27或0x3F地址的I²C LCD1602模块。
它是怎么做到只用两根线就能控制整个屏幕的?
核心原理:PCF8574T充当“电平搬运工”
PCF8574T是个8位I/O扩展器,通过I²C总线接收一字节数据,并将每一位输出到对应的GPIO引脚。
而这些引脚正好连接到LCD的控制线上:
| PCF8574 Pin | Connected To | 功能 |
|---|---|---|
| P0 | D4 | 数据位0 |
| P1 | D5 | 数据位1 |
| P2 | D6 | 数据位2 |
| P3 | D7 | 数据位3 |
| P4 | E | 使能信号 |
| P5 | RS | 寄存器选择 |
| P6 | RW | 读写控制(通常接地,只写) |
| P7 | BL | 背光控制 |
每次你向I²C地址写入一个字节,相当于一次性更新了全部控制状态。
但由于HD44780工作在4位模式下,每个完整指令/数据需要分两次传输:先高4位,再低4位。
所以当你调用lcd.print('A')时,背后实际发生了四次I²C写入:
1. 写入高4位 + RS=1, E=1 → E=0
2. 写入低4位 + RS=1, E=1 → E=0
3. 更新内部光标位置
4. 准备下次操作
这一切都被封装在库函数中,开发者完全无感。
ESP32实战:使用LiquidCrystal_I2C库快速上屏
平台:ESP32 DevKit
模块:I²C LCD1602(地址0x27)
连接方式:
| ESP32 | LCD模块 |
|---|---|
| 3.3V | VCC |
| GND | GND |
| GPIO21 | SDA |
| GPIO22 | SCL |
代码如下:
#include <Wire.h> #include <LiquidCrystal_I2C.h> // 创建对象:I²C地址0x27,16列2行 LiquidCrystal_I2C lcd(0x27, 16, 2); void setup() { Wire.begin(); // 初始化I²C总线 lcd.init(); // 初始化LCD+背光关闭 lcd.backlight(); // 开启背光 lcd.clear(); lcd.setCursor(0, 0); lcd.print("I2C LCD Active"); lcd.setCursor(0, 1); lcd.print("Addr: 0x27"); delay(1000); } void loop() { static int tick = 0; lcd.setCursor(0, 1); lcd.print("Tick: "); lcd.print(tick++); if (tick > 9999) tick = 0; delay(500); }是不是超级简洁?
而且这套方案兼容性极强,Arduino、STM32、Raspberry Pi Pico(MicroPython)、甚至Linux下的Python都能找到对应实现。
I²C vs UART:该怎么选?
| 维度 | I²C方案 | UART方案 |
|---|---|---|
| 引脚数 | 2(SCL+SDA) | 1(RX) |
| 总线共享 | ✅ 可与其他传感器共用 | ❌ 专用通道 |
| 波特率同步 | 无需配置 | 必须严格匹配 |
| 多设备支持 | 地址可调,支持多LCD | 通常一对一 |
| 软件依赖 | 需要库支持 | 基本靠原生Serial |
| 距离能力 | 一般<1m(标准模式) | 更远(可达数米) |
| 差分扩展 | 较难 | 易转RS485 |
建议选择:
- 如果你已经有I²C总线,且希望统一管理多个外设 → 选I²C
- 如果追求极致简化、远距离传输、或做替换升级 → 选UART
工程实践中的那些“坑”与应对策略
再好的技术也有陷阱。以下是我在多个项目中踩过的坑,以及对应的解决方法。
🔧 坑点1:上电乱码或无法初始化
现象:每次上电屏幕闪一下,然后什么都不显示,或者出现奇怪符号。
原因分析:
- 模块供电不稳定,复位不彻底;
- MCU启动比LCD快,提前发送数据导致协议错位;
- UART波特率不准(尤其是RC振荡器);
解决方案:
void setup() { delay(500); // 让LCD充分上电 LCD.begin(9600); delay(100); lcd_command(0x01); // 主动清屏 delay(3); // 留足时间 }同时,在电源端增加10μF电解电容 + 0.1μF陶瓷电容并靠近模块焊接,效果显著。
🔧 坑点2:背光亮了,但没文字
常见于I²C模块。
检查以下几点:
1.I²C地址是否正确?
常见地址有0x27和0x3F,可通过万用表测量A0/A1焊盘确定。
2.上拉电阻是否存在?
若未内置,需外接4.7kΩ上拉至VCC。
3.接线是否反了?
SDA和SCL不能接反,GND必须共地。
可以用i2c_scanner程序扫描设备是否存在:
#include <Wire.h> void setup() { Serial.begin(9600); Wire.begin(); Serial.println("Scanning I2C..."); byte count = 0; for (byte i = 1; i < 120; i++) { Wire.beginTransmission(i); if (Wire.endTransmission() == 0) { Serial.printf("Found device at 0x%02X\n", i); count++; } } if (!count) Serial.println("No I2C device found."); } void loop() {}🔧 坑点3:中文无法显示
结论先行:原生字符型LCD不支持中文。
它们使用的CGROM通常是:
- 英文ASCII字符
- 少量希腊字母、数学符号
- 或可自定义的8个字符(CG RAM)
如果你硬要显示“温度”,只能想办法:
- 用拼音Wendu: 25C
- 或者自己画图标(如用自定义字符拼出℃)
- 或者直接上图形LCD / OLED
所以别挣扎了,真需要中文界面,请换平台。
设计建议:让串口LCD稳定可靠运行
最后分享几条来自实战的设计经验:
✅务必保证电平匹配
虽然很多模块声称兼容3.3V/5V,但长期工作建议保持一致。若MCU是3.3V,而LCD标称5V,建议加电平转换器(如TXS0108E)或选用宽压模块。
✅波特率尽量用标准值
UART模式下推荐使用9600、19200、115200,避免自定义速率。MCU若用内部RC振荡器,误差可能超过±5%,导致通信失败。
✅关键指令后加延时
清屏、归位等操作需要较长时间(约1.5~2ms)。即使手册写了“典型值1.5ms”,保险起见delay(2)以上。
✅合理控制背光寿命
长时间点亮背光不仅费电,还会加速LED老化。建议加入空闲检测,30秒无操作后自动调暗或关闭。
static unsigned long last_update = 0; void update_display() { // ...刷新内容... last_update = millis(); if (!backlight_on) { lcd.backlight(); backlight_on = true; } } // 在loop中检测超时 if (backlight_on && (millis() - last_update) > 30000) { lcd.noBacklight(); backlight_on = false; }结语:小屏幕,大作用
在这个动辄谈“TFT彩屏”、“LVGL动画”的时代,或许你会觉得1602这种字符屏太过原始。
但它依然活跃在工厂仪表、实验室设备、智能家居控制盒、教学套件之中。
因为它够简单、够便宜、够皮实。
而串口字符型LCD,正是让这份“简单”变得更简单的关键一步。
无论是UART的一句print,还是I²C的一个lcd.print(),背后都是对开发效率的巨大提升。
下次当你面对一个只剩两三个IO的MCU时,不妨想想这块小小的蓝色屏幕——也许它就是解决问题的关键钥匙。
如果你也曾被LCD引脚折磨过,欢迎在评论区分享你的故事。