news 2026/9/3 3:43:56

树莓派上使用 Node.js 驱动 I2C LCD 显示屏的实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派上使用 Node.js 驱动 I2C LCD 显示屏的实践指南

用 Node.js 在树莓派上驱动 I2C LCD,这事听起来像“用 JavaScript 干单片机活”。但说实话,Node.js 做硬件控制的异步事件模型比 Python 轮询更顺手,尤其在需要同时处理 Web 请求、定时刷新和传感器上报的场景下。这次我们直接看一套可落地的方案:树莓派 4B/5 + PCF8574 I2C 转接板 + LCD1602/LCD2004,从接线、开 I2C、装 Node.js,到写驱动、跑通显示,最后通过 HTTP 接口远程往屏幕上推文字。

阅读完你会得到一份可以在树莓派上直接运行的 Node.js 驱动模板,以及一套排查 I2C 设备检测失败、地址不对、乱码、背光不亮等问题的标准思路。硬件编程最花时间的不是写代码,而是总线不通和时序不对。这次我们把容易踩的坑一并说清楚。

1. 核心能力速览

能力项说明
硬件平台树莓派 4B / 5 或其他带 I2C 总线的 Linux 开发板
通信协议I2C,常见设备地址 0x27 或 0x3F
显示模块LCD1602 / LCD2004 + PCF8574 I2C 转接板
编程语言Node.js(推荐 LTS 版本,如 18/20)
依赖模块i2c-bus,基于文件系统 /dev/i2c-1 访问总线
异步能力基于 async/await 的异步驱动,非阻塞读写 I2C
扩展接口可用 Node.js http 模块提供 Web API,远程写入显示内容
适用场景系统状态展示、内网信息屏、传感器数值显示、IoT 教学
是否支持批量任务可在主进程中排队处理多条显示任务,防止 I2C 写冲突

从实践角度看,这个方案的门槛比较友好:不需要额外购买 USB 转 I2C 调试器,树莓派 GPIO 自带 I2C 控制器;软件层面只需要 Node.js 和 i2c-bus 一个模块;如果只是点亮屏幕显示固定文字,总代码量可以控制在 200 行以内。

2. 适用场景与使用边界

哪些人适合用 Node.js 控制 LCD?首先是前端或 Node.js 后端工程师,想进入硬件领域又不想重新学 C/Python;其次是已经在树莓派上跑 Node.js 服务的人,希望把服务运行状态直接显示在屏幕;再就是做教学演示,需要把 I2C、Linux 设备文件和异步事件模型串起来讲。

这套方案适合做这几类事情:

  • 把树莓派的 IP、CPU 温度、内存占用定时显示在 LCD 上。
  • 内网环境下通过 HTTP 接口把消息推送到 LCD。
  • 读取温湿度传感器后,把数值同步显示到屏幕。
  • 在 Node.js 的 Electron/Web 项目中增加一块物理状态面板。

但它不适合做以下场景:

  • 需要显示图片、复杂波形或大型中文点阵界面的场景,建议换用 SPI 接口的彩色屏或 HDMI 显示屏。
  • 对毫秒级时序响应要求极高的场景,Node.js 的事件循环调度不如 C 语言直接,但 LCD1602 本身对微秒级指令延时不算苛刻。
  • 需要脱离 Linux 运行的裸机 MCU 环境,Node.js 这里依赖树莓派的 Linux 系统和 /dev/i2c 设备文件,不能直接运行在 STM32/51 上。

合规方面需要留意一点:如果你采集的是真实用户信息并显示在公共屏幕上,要确认已获得相关人员授权;用于公司监控屏或产品展示时,确保信息内容合规,不泄露内部敏感数据。硬件接线也要避免在树莓派通电状态下插拔杜邦线,防止损坏 GPIO 或 I2C 转接板。

3. 树莓派 Node.js I2C LCD 的环境准备

先列硬件清单。

硬件规格说明
树莓派4B 起步,树莓派 5 同样适用,需联网安装系统
LCD 模块LCD1602(2 行 16 列)或 LCD2004(4 行 20 列)
I2C 转接板PCF8574,模块背面可见 8 位扩展芯片
杜邦线母对母 4 根:VCC、GND、SDA、SCL
microSD 卡16GB 以上,系统推荐 Raspberry Pi OS Lite

PCF8574 模块常被直接焊接在 LCD 背面,只留 4 个引脚出来,这样做的好处是省掉大量 GPIO 连线,LCD 的 D0-D7、RS、RW、E 都不需要单独处理。

接线方式固定:

LCD I2C 模块引脚树莓派 GPIO
VCC物理引脚 2(5V)或物理引脚 4(5V)
GND物理引脚 6(GND)
SDA物理引脚 3(GPIO2)
SCL物理引脚 5(GPIO3)

如果使用树莓派 5,40Pin 引脚定义与 4B 物理兼容,I2C1 仍然是物理引脚 3 和 5。注意部分 LCD 模块背光需要 5V,如果接 3.3V,屏幕可能有显示但背光偏暗。

系统选择上,推荐使用 Raspberry Pi OS Lite(无桌面版)或 Ubuntu Server。装好系统后先做两件事:第一更新软件源,第二启用 I2C 内核驱动。

如果觉得官方源比较慢,编辑/etc/apt/sources.list/etc/apt/sources.list.d/raspi.list,替换为国内镜像源。以清华源为例:如果你是树莓派 5 跑 Ubuntu 22.04,镜像源里面的$RELEASE变量会被正确替换为jammy,所以只需先备份原文件,再写入以下内容。需要注意的是,Raspberry Pi OS 源自带/etc/apt/sources.list.d/raspi.list,两者要同时处理。

# 备份原文件 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak sudo cp /etc/apt/sources.list.d/raspi.list /etc/apt/sources.list.d/raspi.list.bak

随后编辑源文件,把http://raspbian.raspberrypi.org/raspbian/之类地址替换为:

# /etc/apt/sources.list 示例,基于 Raspberry Pi OS deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ bullseye main contrib non-free rpi

替换完先运行sudo apt update,确认没有报错再继续。镜像站点的路径以你实际打开为准,不同系统版本代号不同,不要照抄 bullseye 就完事。

启用 I2C 有两种方式。有桌面环境时,在raspi-config的 Interface Options 中打开 I2C;无桌面环境时这样做:

sudo raspi-config

进入 Interface Options,选择 I2C,Enable。然后检查内核模块。

sudo apt install -y i2c-tools ls /dev/i2c-*

正常会看到/dev/i2c-1。如果看不到,编辑/boot/config.txt(树莓派 5 可能是/boot/firmware/config.txt),确认存在dtparam=i2c_arm=on

接下来安装 Node.js。Raspberry Pi OS 官方源内的 Node.js 版本可能偏旧,可以直接使用 NodeSource 安装 LTS 版本,或者从 nodejs.org 下载适用于 ARM64 的 Linux 二进制包。这里以 NodeSource 方式为例:

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs node -v npm -v

如果在内网环境无法访问 NodeSource,可以手动下载node-v20.x.x-linux-arm64.tar.xz后解压到/usr/local。树莓派 4B/5 都是 64 位系统,不要下错成 armv7l 版本。

安装编译依赖。i2c-bus 模块在安装时会通过 node-gyp 编译原生扩展,需要系统有 Python3、make 和 g++:

sudo apt install -y python3 make g++

到这里环境准备已完成。建议先在终端执行node -e "console.log('ok')"验证 Node.js 可运行,再继续后面的驱动开发。

4. I2C 设备地址检测与总线确认

LCD 接好线、系统启用 I2C 后,第一件事不是写代码,而是扫描总线,确认树莓派是否真的识别到这个设备。

i2cdetect -y 1

参数-y表示跳过交互确认,1代表 I2C 总线编号 1。执行后终端会打印一个地址表,其中出现273f就说明找到了 PCF8574 转接板。多数常见模块默认地址是 0x27,部分 0x3F,也有一些转接板通过板载 A0/A1/A2 跳线改变地址。

如果扫描不到任何设备,大概率是硬件连线问题,而不是 Node.js 代码问题。排查顺序如下:

1. VCC 是否接 5V,GND 是否共地。 2. SDA 和 SCL 是否接反。 3. 杜邦线是否松动。 4. 树莓派 I2C 是否真的启用,运行 ls /dev/i2c-*。 5. 转接板是否损坏,可换一个模块测试。

地址检测成功后,可以用i2cdump -y 1 0x27查看总线数据,但要注意读取 LCD 模块寄存器意义不大,PCF8574 只是把 I2C 数据转成 8 位并行电平,真正的“内存映射”在 LCD 控制器 HD44780 内部,不是普通 EEPROM 那样直接可读。

有些 I2C 模块要求外接上拉电阻,树莓派 GPIO 内部已经为 I2C1 启用了 1.8kΩ 上拉,所以无需额外上拉;如果是自己用杜邦线外接一个独立 PCF8574 芯片,则需要检查模块是否带 4.7kΩ 到 10kΩ 上拉电阻。4*4 键盘场景中经常讨论上拉电阻问题,本质相同:I2C 总线是开漏结构,必须有上拉才能保证高电平有效。原装 I2C LCD 转接板基本都带电阻,不需要再处理。

5. Node.js 项目初始化与依赖安装

用 mkdir 创建独立项目目录,防止依赖污染系统目录。

mkdir ~/lcd-node cd ~/lcd-node npm init -y npm install i2c-bus

安装成功后,node_modules/i2c-bus内会包含编译好的.node原生扩展,可以简单验证模块能被 require。

node -e "const i2c = require('i2c-bus'); console.log(typeof i2c.openPromisified)"

输出function即可。下面使用openPromisifiedAPI,把 I2C 读写封装成 Promise,这样就不用陷入回调嵌套,可以安心写 async/await 逻辑。

建议把驱动拆成独立文件lcd.js,主程序只负责调用。项目结构如下:

~/lcd-node/ ├── lcd.js # I2C LCD 驱动类 ├── index.js # 主程序,定时显示系统信息 ├── server.js # HTTP API 服务,可选 └── package.json

6. 编写 I2C LCD 驱动模块

PCF8574 转接板的核心原理并不复杂:它通过 8 位 I2C 数据引脚模拟 HD44780 的 4 位并行接口。怎么理解?

PCF8574 的 P0-P7 输出到 LCD 引脚,常见映射是:

P7 -> D7 P6 -> D6 P5 -> D5 P4 -> D4 P3 -> 背光控制 P2 -> E(使能) P1 -> RW(读写选择,接地为写) P0 -> RS(寄存器选择,0 为命令,1 为数据)

所以我们向 I2C 总线写入一个字节时,实际同时控制了 8 个引脚。写入一个 8 位数据到 LCD,需要拆成两个 nibble(高 4 位和低 4 位),每个 nibble 都要拉高 E 引脚再拉低,LCD 在 E 下降沿锁存数据。

先看完整驱动代码:

// lcd.js const i2c = require('i2c-bus'); const RS = 0x01; const RW = 0x02; const EN = 0x04; const BL = 0x08; class Lcd { constructor({ busNumber = 1, address = 0x27, cols = 16, rows = 2 } = {}) { this.busNumber = busNumber; this.address = address; this.cols = cols; this.rows = rows; this.bus = null; this.backlight = BL; } async init() { this.bus = await i2c.openPromisified(this.busNumber); this.bus.writeByte(this.address, 0x33); await this.delay(5); this.bus.writeByte(this.address, 0x32); await this.delay(5); await this.command(0x28); // 4 位模式,2 行,5x8 点阵 await this.command(0x0C); // 显示开,光标关 await this.command(0x06); // 写入后地址自动加 1 await this.command(0x01); // 清屏 await this.delay(2); } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async writeByte(value, mode) { const data = (value & 0xF0) | mode | this.backlight; await this.bus.writeByte(this.address, data); await this.bus.writeByte(this.address, data | EN); await this.delay(1); await this.bus.writeByte(this.address, data & ~EN); const dataLow = ((value << 4) & 0xF0) | mode | this.backlight; await this.bus.writeByte(this.address, dataLow); await this.bus.writeByte(this.address, dataLow | EN); await this.delay(1); await this.bus.writeByte(this.address, dataLow & ~EN); } async command(value) { await this.writeByte(value, 0x00); } async print(text) { const chars = text.split(''); for (const ch of chars) { await this.writeByte(ch.charCodeAt(0), RS); } } async clear() { await this.command(0x01); await this.delay(2); } async home() { await this.command(0x02); await this.delay(2); } async setCursor(col, row) { const rowOffsets = [0x00, 0x40, 0x14, 0x54]; const offset = rowOffsets[row] || 0x00; await this.command(0x80 | (offset + col)); } async displayMessage(line1, line2 = '') { await this.clear(); await this.print(line1); if (this.rows > 1 && line2) { await this.setCursor(0, 1); await this.print(line2); } } } module.exports = Lcd;

写驱动时最需要注意的是初始化序列。PCF8574 上电后可能处于 8 位模式,我们需要先发0x33再发0x32完成“降级”到 4 位模式的过程,然后才是常规的0x28、0x0C、0x06、0x01。如果少了 0x33 这一步,屏幕可能出现随机乱码或只有第一行正常。

7. 功能测试与效果验证

写一个最简单的测试文件:

// test.js const Lcd = require('./lcd'); async function main() { const lcd = new Lcd({ address: 0x27, cols: 16, rows: 2 }); try { await lcd.init(); await lcd.displayMessage('Hello CSDN', 'Node.js I2C'); console.log('LCD display success'); } catch (err) { console.error('LCD error:', err); process.exit(1); } finally { setTimeout(async () => { await lcd.clear(); process.exit(0); }, 3000); } } main();

运行命令:

node test.js

判断成功的标准有三个:

  • 终端输出LCD display success
  • LCD 第一行显示Hello CSDN
  • LCD 第二行显示Node.js I2C,且背光正常。

如果看到乱码或显示位置不对,优先检查address是否与i2cdetect探测结果一致。比如检测结果是 0x3F,代码却固定写 0x27,那么驱动能读到设备但输出完全不可用。

如果想测试滚动显示,可以写一段定时器逻辑:每 3 秒切换一次行内容,模拟动态信息面板。这里要注意,每次打印前如果不清屏也不设置光标,文字会从上次位置继续往后写,超出 16 列的部分不会自动换行到第二行,而是被截断。所以在更新整屏信息时,最稳妥的做法是先clear(),然后setCursor(0, 0),再打印第一行,最后跳转到第二行。

8. 显示中文问题:LCD1602 字库限制与替代方案

很多人在树莓派上跑通 LCD 后,第一反应就是“我想显示中文”。这里要说清楚:普通 LCD1602/LCD2004 内置的 HD44780 字符 ROM 只包含 ASCII 字符、日文假名和少量符号,没有中文字库,直接把中文字符串按 UTF-8 编码写进去,屏幕上会出现乱码。

如果必须显示中文,有几种常见路径:

方案说明难度
购买带中文字库的 LCD 模块模块内置 GB2312 或 Unicode 字库,写入对应编码即可
使用 LCD12864 图形屏将中文字模转成点阵图片,逐 bit 写入
树莓派上用 SPI OLED通过占位图方式显示中文,每次写整个帧缓冲中高

如果你的项目不是把中文字模显示当作核心诉求,建议还是沿用 ASCII 显示方案,只显示 IP、时间、温度、英文状态。比如 CPU 温度可以显示为CPU 52.3C,避免了中文字库的适配成本。

如果一定要在 Node.js 里做中文点阵,思路是准备一个包含常用汉字的字模数组,比如 16x16 点阵每个字 32 字节,然后将汉字拆分到 5x8 的自定义字符模式,这超出了普通 LCD1602 模块的能力范围。最实用的路线是换用中文字库版 LCD2004,数据写入方式和 ASCII 版几乎一致,只是字符编码上要发送 GBK 编码而非 UTF-8。

在 Node.js 中转换编码可以使用iconv-lite

npm install iconv-lite
const iconv = require('iconv-lite'); const buf = iconv.encode('温度 25C', 'gbk'); for (const byte of buf) { await lcd.writeByte(byte, RS); }

不过具体模块的字库编码方式需要以硬件说明书为准,不要盲目照搬代码。标准 PCF8574 转接板驱动 HD44780 只能处理 ASCII 字符,这点可以先接受。

9. 系统状态监视与批量显示任务

LCD 单独显示一句话意义有限,把它变成系统监视屏才有实用价值。树莓派上通过/proc/sys/class/thermal可以拿到系统信息,Node.js 不需要额外安装系统监控库。

// monitor.js const fs = require('fs'); const os = require('os'); const Lcd = require('./lcd'); function readCpuTemperature() { try { const raw = fs.readFileSync('/sys/class/thermal/thermal_zone0/temp', 'utf8'); return (parseInt(raw, 10) / 1000).toFixed(1); } catch (err) { return 'N/A'; } } function getIPAddress() { const ifaces = os.networkInterfaces(); for (const name of Object.keys(ifaces)) { for (const iface of ifaces[name]) { if (iface.family === 'IPv4' && !iface.internal) { return iface.address; } } } return '127.0.0.1'; } async function main() { const lcd = new Lcd({ address: 0x27, cols: 16, rows: 2 }); await lcd.init(); setInterval(async () => { const ip = getIPAddress(); const temp = readCpuTemperature(); const mem = os.freemem() / 1024 / 1024; const totalMem = os.totalmem() / 1024 / 1024; const memPercent = ((1 - mem / totalMem) * 100).toFixed(0); await lcd.displayMessage(`IP ${ip}`, `Mem ${memPercent}% T${temp}C`); }, 5000); } main().catch(err => { console.error(err); process.exit(1); });

运行node monitor.js后,LCD 会每 5 秒刷新一次。这里会遇到硬件编程常见的“写冲突”问题:如果下一次 setInterval 触发时上一次异步写入还没结束,两条消息交错发给 LCD,屏幕内容会错乱。虽然在上述代码中 5 秒间隔足够长,但在真实项目中可能同时存在 Web 接口写入、传感器事件写入、定时刷新三条路径,所以需要一个显示任务队列。

批量任务可以用最简单的方式实现:主程序只负责入队,驱动内部串行执行显示任务。

class LcdQueue { constructor(lcd) { this.lcd = lcd; this.queue = []; this.running = false; } push(line1, line2 = '') { this.queue.push({ line1, line2 }); this.process(); } async process() { if (this.running || this.queue.length === 0) return; this.running = true; while (this.queue.length > 0) { const task = this.queue.shift(); try { await this.lcd.displayMessage(task.line1, task.line2); } catch (err) { console.error('display error:', err); } await this.lcd.delay(300); } this.running = false; } } module.exports = LcdQueue;

这套队列的思想很简单:任何上游任务都先把内容推到队列,显示进程循环取出并执行,避免多个异步写入同时操作 I2C 总线。它不需要 redis 或消息队列,小项目中一个数组足够。类似“闪屏”“重叠文字”大部分情况下都是因为没有全局串行化显示写入。

10. 通过 HTTP API 远程控制 LCD

树莓派作为常开的低功耗服务器,最适合做的就是把 LCD 变成一个可远程控制的物理信息屏。这里用 Node.js 内置http模块提供接口,不需要引入 Express 依赖。

// server.js const http = require('http'); const Lcd = require('./lcd'); const LcdQueue = require('./lcdqueue'); const lcd = new Lcd({ address: 0x27, cols: 16, rows: 2 }); const queue = new LcdQueue(lcd); async function main() { await lcd.init(); console.log('LCD initialized'); const server = http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/display') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { const parsed = JSON.parse(body); const line1 = (parsed.line1 || '').slice(0, 16); const line2 = (parsed.line2 || '').slice(0, 16); queue.push(line1, line2); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 0, message: 'ok' })); } catch (err) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 400, message: 'invalid json' })); } }); return; } res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 404, message: 'not found' })); }); server.listen(3000, '0.0.0.0', () => { console.log('Server listening on port 3000'); }); } main().catch(err => { console.error(err); process.exit(1); });

注意构造函数里我们把 LcdQueue 单独存储,在index.js里还要调用一次lcd.init()。上面的代码为了展示写法省略了模块关系,正确做法是传入已经完成 init 的 lcd 实例。队列模块本身不管初始化流程。

通过curl调用:

curl -X POST http://127.0.0.1:3000/display \ -H "Content-Type: application/json" \ -d '{"line1":"Server OK","line2":"Port 3000"}'

返回{"code":0,"message":"ok"}说明接口调用成功,LCD 显示内容会更新。为了避免客户端传入超长内容把屏幕写坏,代码里用slice(0, 16)做了截断,具体长度要以 LCD 列数为准。第二行截断长度取 16,但 LCD2004 是 20 列,所以这个参数应该根据 lcd.cols 动态算出。

接口调用成功后,你可以直接从内网任意机器把消息推到树莓派 LCD 上,比如结合 shell 脚本、监控系统 webhook 或一个简单的移动端工具页。

11. 资源占用与性能观察

树莓派上跑 Node.js 控制 LCD,很多人担心性能问题。实际上 Node.js 进程对树莓派 4B 而言非常轻量。通过htopps观察,一个纯 LCD 服务进程的内存占用通常在 50 MB 以下,CPU 占用率基本接近 0。

ps aux | grep node

观察要点如下:

  • Node.js 进程只会在写入 I2C 时短暂占用 CPU,I2C 总线速率默认 100 kHz,写入几个字节的时间是微秒级,对 CPU 影响很小。
  • 树莓派使用 Node.js 收发 HTTP 请求时,进程会进入事件循环等待,不会持续占用 CPU。
  • 定时刷新不要设置过短,LCD1602 人眼可观察的变化在 200ms 以上,没必要 100ms 刷一次。LCD 内部控制器 HD44780 清屏指令需要 1.5ms 左右,如果频繁清屏会导致显示闪烁。
  • I2C 总线本身是半双工,同一时间只能有一个主设备访问,树莓派上不要同时运行多个直接访问 LCD 的进程,避免总线 busy 报错。
  • 如果代码中连续写入不等待,I2C 可能返回EIOEBUSY错误,此时应该在错误处理里重试或增加延时。

降低资源占用的有效手段包括:批量刷新时只在内容变化时才刷新;不要每轮都调用清屏指令,改为 setCursor 后覆盖写;全屏文本使用 Buffer 拼接再一次性写入,减少 JS 字符串到 I2C 字节的转换开销。LCD 刷新本身是低速外设,性能瓶颈从来不在树莓派 CPU,而在 I2C 总线时钟和 LCD 控制器时序。

12. 常见问题与排查方法

把实际开发中最常遇到的现象整理成表:

问题现象可能原因排查方式解决方案
i2cdetect 扫描不到设备接线错误、I2C 未启用、模块损坏检查 VCC/GND/SDA/SCL,重启后再扫描重新插拔杜邦线,执行 sudo raspi-config 启用 I2C
屏幕有背光但不显示文字对比度电位器调太低用小螺丝刀旋转 LCD 背面蓝色电位器调到能隐约看到方块,再运行显示程序
显示乱码初始化时序不对或地址错误检查代码中的 address 与 i2cdetect 结果是否一致确认驱动中的 0x33 初始化序列完整
只有第一行正常驱动未正确设置两行模式检查命令 0x28 是否发送成功确认 LCD 是 1602,且 rows 参数传 2
显示内容残影未清屏直接覆盖写打印前调用 clear 或 home更新内容前先清屏再 setCursor
Node.js 报 EIOI2C 总线忙或设备无响应查看 dmesg 是否有 i2c 错误重启树莓派或检查接线
npm install i2c-bus 编译失败缺少 python3/make/g++查看安装日志尾部错误执行 sudo apt install -y python3 make g++
屏幕出现闪烁方块背光电压不足或对比度不对检查 VCC 是否接 5V改用 5V 供电,不要接 3.3V
接口返回 ok 但屏幕没变化队列里还有多条未执行任务检查服务端日志清空队列后重试,或重启 node 进程
重启后显示服务没起来未配置开机自启查看 pm2 list 或 systemd 状态使用 pm2 或 systemd 配置常驻服务

这里特别强调几点排查经验。

I2C 设备名字扫描不到,先把目光从代码转移到硬件。杜邦线接触不良是树莓派外设场景第一杀手。换线、换引脚、重新插拔,比调试代码更有效。

屏幕有背光但无字符时,优先拧对比度电位器。PCF8574 转接板通常自带一个蓝色电位器,出厂位置可能接近最小值,导致字符完全隐形,此时你以为驱动有问题,实际上只是对比度不对。

初始化完成后第一次写文字乱码,很大原因是 PCF8574 与 LCD 模块中间的排线松动,或者转接板本身存在虚焊。可以尝试用手按压排线连接处观察现象。

13. 主机上使用 pm2 或 systemd 常驻服务

树莓派作为 IoT 网关,要让 LCD 服务在重启后自动运行,不能每次都手工执行node server.js。推荐使用 pm2 管理 Node.js 进程。

sudo npm install -g pm2 cd ~/lcd-node pm2 start server.js --name lcd-server pm2 save pm2 startup

pm2 startup会输出一条 root 权限命令,复制执行即可设置开机自启。pm2 的核心优点在于:进程崩溃后自动重启、日志统一管理、重启后自动拉起来。LCD 驱动在 I2C 设备暂时不可用时可能会抛异常,pm2 会自动拉起进程,但要注意进程拉起前需要等待 I2C 设备就绪,所以代码里最好加上重试初始化逻辑。

使用 systemd 也是常见做法,创建/etc/systemd/system/lcd.service

[Unit] Description=LCD Display Service After=network.target [Service] ExecStart=/usr/bin/node /home/pi/lcd-node/server.js Restart=always RestartSec=5 User=pi [Install] WantedBy=multi-user.target
sudo systemctl daemon-reload sudo systemctl enable lcd.service sudo systemctl start lcd.service systemctl status lcd.service

pm2 和 systemd 二选一即可,不必同时使用。

14. 最佳实践与使用建议

从可维护性角度给几条建议。

第一,把 I2C 设备地址做成环境变量或启动参数,不要硬编码。换一块 LCD 模块地址从 0x27 变成 0x3F 时,改代码要重新部署,而用环境变量只需修改启动命令。

LCD_ADDR=0x3f node server.js

第二,每次开机后 I2C 设备并不一定完全就绪,驱动 init 前要做错误重试。初始化失败不要直接 process.exit,可以延迟 1 秒后重试 5 次,确保树莓派启动完成后再初始化 LCD。

async function initWithRetry(lcd, retries = 5) { for (let i = 0; i < retries; i++) { try { await lcd.init(); return; } catch (err) { console.error(`init failed, retry ${i + 1}/${retries}`); await new Promise(resolve => setTimeout(resolve, 1000)); } } throw new Error('LCD init failed after retries'); }

第三,项目里划分 model 和 view:传感器读取是 model,LCD 显示是 view,中间通过队列解耦。不建议在传感器回调里直接调 lcd 方法,这样后续增加接口推送或数据库记录时需要改动底层逻辑。

第四,真实项目要处理 LCD 字符长度。LCD1602 第一行 16 字符,LCD2004 第一行 20 字符,字符串要按列数做 padEnd 或 slice。显示不足长度时要用空格清空残余字符,否则上一轮内容会残留。

第五,背光控制要独立抽方法。PCF8574 的 P3 引脚控制背光,把背光数据字节与显示数据分离,可以在夜间自动关闭背光。驱动里把this.backlight变为可动态修改的字段,需要时清除对应位即可。

第六,国内网络环境下载 Node.js 和 npm 包可能较慢,建议设置 npm 镜像:

npm config set registry https://registry.npmmirror.com

这能显著缩短 npm install 时间,对树莓派这种硬件环境尤其重要。安装完模块后可以把 registry 改回默认或保留都行,看个人偏好。

第七,所有对外提供的 HTTP API 都应限制访问来源。比如只在 listen 时绑定内网 IP,或用简单 token 校验,避免局域网内其他设备随意写屏。LCD 虽是低危外设,但 API 接口最好保持最基本的安全性。

15. 总结与下一步

这次带大家从硬件接线一路走到 Node.js 驱动和 HTTP 接口调用,核心是四步:接通 I2C 总线、用 i2cdetect 找到设备、用 i2c-bus 通过 PCF8574 模拟 HD44780 时序、把 LCD 操作封装成服务。整个链路跑通以后,LCD 不再是只能亮一下的“点灯工程”,而是一个可以被 Web 请求、传感器事件和定时任务共同驱动的信息输出终端。

最值得先验证的功能是驱动初始化,建议第一次上手先跑最简单的 Hello World,不要直接把队列、Web server、pm2 全部堆上去。变量越少,排查越容易。最容易踩的坑是 i2cdetect 扫不到设备,通常不是代码问题而是接线问题;其次是屏幕有背光但没字,通常要调对比度电位器。这两个坑踩完之后,再往上叠加复杂功能会更加顺利。

下一步可以扩展的方向包括:给 LCD 增加按键输入,用树莓派 GPIO 做一个菜单选择器;把 DHT11 或 BME280 传感器的数据定时显示;通过 MQTT 协议订阅消息,让其他设备远程推送内容到屏幕;或者把显示服务打包成 Docker 镜像,利用 docker-compose 统一管理树莓派上的多个服务。

如果只是做简单信息展示,保留 LCD1602 + PCF8574 + Node.js 这套组合已经足够稳定便宜;如果想做内容更丰富的仪表盘,建议考虑 3.5 寸 SPI 屏或 HDMI 触摸屏,但相应地功耗和接线复杂度也会上升。按需选择即可。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 3:43:44

A股行情Python接口:基于交易所官网的稳定实时数据方案

简介&#xff1a;这是一份面向Python初学者与量化入门者的A股实时行情数据获取工具包&#xff0c;解决个人投资者和开发者低成本接入沪深股市实时行情的痛点。资源共7个文件&#xff0c;包含4个核心Python脚本&#xff08;如Ashare.py主接口、Demo1/2.py示例程序、MyTT.py技术指…

作者头像 李华
网站建设 2026/9/3 3:43:21

EXE打包全攻略:PyInstaller、Flask-SocketIO与Qt实战排错

在实际开发与交付场景中&#xff0c;源码能编译通过、脚本能在自己电脑上运行&#xff0c;只完成了前半程。把代码交付给没有安装解释器、编译工具链或运行环境的用户时&#xff0c;最简单的做法就是把程序打包成 EXE 可执行文件。这里说的 EXE&#xff0c;不只是 Windows 平台…

作者头像 李华
网站建设 2026/9/3 3:42:40

DeepSeek Harness:API工程化调用的完整实战

这次不聊论文&#xff0c;不聊架构图&#xff0c;直接聊一个能改善 DeepSeek 使用体验的实用工具链组合——DeepSeek Harness。先说结论&#xff1a;如果你平时用 DeepSeek 主要是通过官方网页版或 App&#xff0c;且经常遇到上下文不够用、无法深度控制输出格式、想批量处理任…

作者头像 李华
网站建设 2026/9/3 3:41:01

索尼A7000传闻解析:APS-C旗舰定位与参数验证方法

如果你是被“重磅参数泄露”这几个字带进来的&#xff0c;先把期待值降一档。截至本文写作时间&#xff0c;索尼官方并没有发布任何关于 A7000 的公告&#xff0c;网上流传的具体参数多数来自第三方推测与二手转述&#xff0c;真实性需要逐条验证。A7000 这个命名在索尼 APS-C …

作者头像 李华
网站建设 2026/9/3 3:40:46

城市体检安全监管平台官网是什么?功能与APP使用全解析

城镇化进程进入存量提质增效阶段后&#xff0c;"先体检、后更新"逐步成为城市治理的共识性路径。无论是查找房屋安全隐患&#xff0c;还是判断基础设施运行状态&#xff0c;都依赖一套能持续采集、动态分析、闭环处置的数字化工具。传统人工排查模式在覆盖面和时效性…

作者头像 李华
网站建设 2026/9/3 3:40:18

磁性元件磁芯损耗建模:从Steinmetz公式到Python工程实践

简介&#xff1a;本资源是面向研究生数学建模参赛者与毕业设计学生的2024年华为杯C题专项解决方案&#xff0c;聚焦‘数据驱动下磁性元件的磁芯损耗建模’这一工程物理交叉问题&#xff0c;提供从问题理解、模型构建、算法实现到结果分析的全链路支持。压缩包共16个文件&#x…

作者头像 李华