一块16×2的LCD1602,接Arduino最省事的方案不是拖一把杜邦线硬啃并口,而是在屏背后加一块指甲盖大小的I2C转接板。并口4-bit模式至少要占RS、E加D4到D7这6个数字脚,换成I2C之后只剩VCC、GND、SDA、SCL四根线,而且SDA和SCL挂在A4、A5上,还能和其他I2C传感器共享同一条总线。这篇把接线、地址确认、LiquidCrystal_I2C库的用法和自定义字符一次说清,代码全部逐段给出,照着敲就能亮屏。

先认清两块东西:屏是屏,转接板是转接板
LCD1602本体是HD44780控制器的并口字符屏:16列×2行,最多显示32个ASCII字符,每个字符由5×8像素的点阵拼出来,点亮不同像素组合就得到不同字形。转接板上的核心是PCF8574——按NXP官方数据手册的口径,它是一颗8位准双向I/O扩展芯片,把I2C两线信号展开成P0到P7八个口,正好喂给LCD的并口。I2C总线是开漏输出,需要上拉,转接板上那两颗4.7kΩ电阻R4、R5就是干这个的,所以模块买回来不用自己加上拉。
转接板上还有三样东西必须认识:10kΩ电位器RV1,调字符对比度,屏上看不见字先拧它而不是先怀疑代码;背光跳线JP1,拔掉可以改外接控制背光亮度;A0、A1、A2三个地址跳线焊盘,决定模块在I2C总线上的地址,后面单独说。背光则由P3脚经一颗S9013三极管开关,lcd.backlight()控制的就是它,不需要额外接线。

四根线接完:接线表与两个注意点
| I2C转接板 | Arduino UNO | 说明 |
|---|---|---|
| VCC | 5V | 必须5V,3.3V供电背光和逻辑都不可靠 |
| GND | GND | 共地是I2C通信的前提 |
| SDA | A4 | 数据线,与模拟输入A4复用 |
| SCL | A5 | 时钟线,与模拟输入A5复用 |

两个注意点。第一,用了I2C之后A4、A5就别再当模拟输入脚读传感器,它们已被总线占用。第二,代码里用0x27这个地址的前提是A0、A1、A2跳线全部断开;三个焊盘全短接时地址会落到0x20,接错地址屏自然不动。
先扫地址,再写代码
不少教程上来就写0x27,但这是PCF8574变体出厂全跳线断开的默认值,不是承诺。PCF8574的7位地址由固定基址0x20加A2、A1、A0三位拼成,短接某个焊盘就把对应位拉低,地址在0x20到0x27之间变;如果模块用的是PCF8574A变体,基址不同,常见默认地址是0x3F。与其猜,不如先上传一段地址扫描程序,把结果打在串口监视器和LCD屏上,一次定案。
按站点排版要求,代码里include的尖括号统一改成了引号形式,两种写法在Arduino工具链下编译等价,其余字符原样搬运。
/*
Code to display LCD's own address on I2C LCD
by www.playwithcircuit.com
*/
#include "Wire.h"
#include "LiquidCrystal_I2C.h"
LiquidCrystal_I2C lcd(0x27, 16, 2); // 0x27 is the Default address, will reinitialize later with correct address
void setup() {
Wire.begin();
Serial.begin(9600);
byte lcdAddress = 0;
Serial.println("Scanning for I2C devices...");
// Scan for I2C devices
for (byte address = 1; address < 127; address++) {
Wire.beginTransmission(address);
if (Wire.endTransmission() == 0) {
lcdAddress = address;
Serial.print("I2C device found at 0x");
Serial.println(address, HEX);
break; // Take the first device found (LCD expected)
}
}
if (lcdAddress == 0) {
Serial.println("No I2C device found.");
while (1); // Halt if nothing found
}
// Initialize LCD with found address
LiquidCrystal_I2C lcd(lcdAddress, 16, 2);
lcd.init();
lcd.backlight();
// Display the address on the LCD
lcd.setCursor(0, 0);
lcd.print("My Address is:");
lcd.setCursor(0, 1);
lcd.print("0x");
if (lcdAddress < 16) lcd.print("0"); // Add leading zero if needed
lcd.print(lcdAddress, HEX);
}
void loop() {
// Nothing here
}
扫描逻辑不复杂:从0x01到0x7E逐个发起传输,Wire.endTransmission()返回0说明该地址有设备应答,记下它就是LCD的地址;一个都没应答就打印提示并停机。把扫到的地址记下来,后面初始化都要用它。
装库与点屏:Hello World
库管理器里搜LiquidCrystal I2C,装Frank de Brabander维护的那一版,这是社区用得最广的LiquidCrystal_I2C实现。装完先来一段最小示例,第一行打印Hello,第二行打印PlayWithCircuit。
/*
Code to display Strings on I2C LCD
by www.playwithcircuit.com
*/
#include "LiquidCrystal_I2C.h"
LiquidCrystal_I2C lcd(0x27, 16, 2); // Format -> (Address,Width,Height )
void setup()
{
// initialize the lcd
lcd.init();
// Turn on the Backlight
lcd.backlight();
}
void loop()
{
// Clear the display buffer
lcd.clear();
// Set cursor (Column, Row)
lcd.setCursor(0, 0);
// print "Hello" at (0, 0)
lcd.print("Hello");
// Set cursor (Column, Row)
lcd.setCursor(0,1);
// print "PlayWithCircuit" at (0, 1)
lcd.print("PlayWithCircuit");
while(1);
}几个常用函数列一张表,用的时候对号入座:
| 函数 | 作用 |
|---|---|
| lcd.init() | 初始化LCD |
| lcd.backlight() | 打开背光 |
| lcd.clear() | 清屏并把光标归位 |
| lcd.setCursor(col, row) | 设置光标,左上角是(0, 0),第二行行首是(0, 1) |
| lcd.print() / lcd.write() | 打印字符串 / 写单个字节或自定义字符 |
| lcd.display() / lcd.noDisplay() | 开关显示,关掉后显存数据还在 |
| lcd.cursor() / lcd.blink() | 下划线光标 / 闪烁块光标 |
| lcd.scrollDisplayLeft() / Right() | 整屏左移 / 右移一格 |
顺手纠一个网上教程里常见的笔误:有的文章把backlight()写成初始化、把init()写成开背光,正好写反了。init()负责初始化,backlight()负责开背光,setup里两个都要调,缺一个屏要么不亮要么不理你。另外示例loop里那句while(1)不是偷懒,没有它每轮clear再重写,屏会闪。
自定义字符:CGRAM里只有64字节
HD44780控制器有两块字模存储。CGROM是出厂固化的只读存储,装着A到Z、0到9这些标准字模,断电不丢;CGRAM是易失的字符生成RAM,按Hitachi HD44780U数据手册的规格总容量64字节,断电即清空,自定义字符就存在这里。5×8字模每个占8字节,每字节对应一行、低5位有效,64除以8正好是8个槽位,编号0到7,这是上限,塞不下第9个。
做法分三步:先用8字节数组画出字模,1代表像素点亮;再用lcd.createChar(槽号, 数组)把数组写进CGRAM;最后用lcd.write(byte(槽号))显示。下面的示例定义了笑脸、举手、无表情三个字符,先逐字打出一行提示文字,再在第二行依次亮出三个图标。
/*
Code to display custom characters on I2C LCD
by www.playwithcircuit.com
*/
#include "LiquidCrystal_I2C.h"
// Initialize LCD at default I2C address 0x27, 16x2 display
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Custom Characters
byte smiley[8] = {
0b00000,
0b00000,
0b01010,
0b00000,
0b00000,
0b10001,
0b01110,
0b00000
};
byte armsUp[8] = {
0b00100,
0b01010,
0b00100,
0b10101,
0b01110,
0b00100,
0b00100,
0b01010
};
byte face[8] = {
0b11111,
0b10001,
0b00000,
0b01010,
0b00000,
0b00100,
0b00000,
0b01110
};
char u8Buffer[17]="Smile ArmUp Face";
void setup() {
lcd.init(); // Initialize the LCD
lcd.backlight(); // Turn on the backlight
// Create custom characters
lcd.createChar(0, smiley);
lcd.createChar(1, armsUp);
lcd.createChar(2, face);
// Display custom characters on LCD
lcd.setCursor(0, 0);
for(int i=0; i < 17; i++){
lcd.print(u8Buffer[i]);
delay(100);
}
lcd.setCursor(0, 1);
lcd.write(byte(0)); // smiley
lcd.print(" ");
delay(300);
lcd.write(byte(1)); // armsUp
lcd.print(" ");
delay(300);
lcd.write(byte(2)); // face
}
void loop() {
// Nothing in loop
}
画字模不用手算二进制,网上有现成的5×8点阵生成器,点格子自动出数组,选hex格式直接抄进代码就行。
排错清单:五种常见翻车
- 屏完全黑、背光也不亮:先查供电是不是5V,再查SDA、SCL有没有接反,最后确认共地。
- 背光亮但看不到字:九成是对比度,拧转接板上的蓝色电位器,慢慢转到字符清晰。
- 满屏乱码:多半是杜邦线松动或面包板接触不良,也可能地址写错,重跑一遍地址扫描。
- 自定义字符不显示或显示错位:数组必须恰好8字节,createChar()要在显示之前、init()之后调用。
- 字符很淡:还是对比度和供电的问题,电位器加稳定5V双管齐下。
小结
从数据角度看这笔账很划算:花4根线省下6个数字IO,64字节CGRAM换8个自定义图标,背光和对比度都在I2C这侧解决。对做传感器面板、小型仪表的人来说,LCD1602加PCF8574转接板仍是性价比最高的文本显示方案——想要图形和动画再上OLED,纯文本场景它就是最优解。











4根线换6个IO这笔账确实划算,I2C还能挂别的传感器
自定义字符那段讲得细,createChar要在init之后这个坑我也踩过
进度条动画这个玩法不错,用 partial block 能做出五级填充
这个思路我去年做温湿度面板时验证过一遍:并口版LCD1602占了我6个IO,加了个PCF8574背板之后A4/A5两根线搞定,剩下的脚全留给传感器。补充一个数据点,PCF8574的地址基址0x20、PCF8574A是0x38起,所以市面上两种背板默认地址分别是0x27和0x3F,扫描程序跑一遍最稳。CGRAM那64字节也别浪费,温度计、电池、信号强度几个图标做出来面板观感立刻不一样,就是记住每个字模严格8字节,我少写一个0x00导致后面字符错位过。
上手第一课就是拧那个蓝色电位器,屏没字先别怀疑代码