1. 项目整体拆解与设计思路
做电子DIY这些年,Arduino驱动LCD屏加超声波测距算得上是最经典的入门组合之一。上周我帮朋友搭了一套这样的测距显示系统,用的正是市面上最常见的带I2C驱动板的LCD1602屏加HC-SR04超声波模块,全程从接线、写代码到调通大概花了一个多小时。这个项目虽然基础,但把嵌入式开发里最关键的几件事——传感器读取、数据处理、外设控制、I2C通信——全串起来了,特别适合想从"点灯"进阶到"做点有用东西"的人。
先说说这套系统能干什么。它的核心功能很直接:超声波模块负责测前方物体的距离,LCD屏实时显示厘米数。你把它放在桌面朝墙放着,屏幕上就能看到距离数字跟着你手的位置变化。再往前一步,加上蜂鸣器或者舵机,就能做成倒车雷达、智能垃圾桶盖、避障小车的原型。本质上这就是一个完整的"感知层→控制层→显示层"链路,学到的思路可以复用到几乎所有传感器项目里。
1.1 三个核心部件分别扮演什么角色
这套系统里三个核心部件各司其职,理解清楚它们之间的关系,后面接线和写代码都会顺很多。
Arduino开发板是整个系统的大脑。它负责给超声波模块发送触发信号,读取回波时间,计算距离,再把处理好的数值通过I2C总线发送给LCD屏显示。我用的是最常见的Arduino Uno R3,如果你手头是Nano也没关系,引脚定义完全一样,只是板子尺寸不同。
HC-SR04超声波模块是"眼睛"。它的工作原理其实不难懂:模块上有两个圆柱形的换能器,一个负责发射,一个负责接收。你给Trig引脚一个10微秒以上的高电平,模块就会自动发出8个40kHz的超声波脉冲,同时Echo引脚会输出一个高电平,这个高电平持续的时间恰好等于超声波从发射到碰到障碍物再返回的时间。知道时间,再乘上声速,就能算出距离。
带驱动板的LCD1602屏是"嘴巴"。这里要特别强调"带驱动板"这个条件。如果你买的是裸屏,需要用6到7个数字引脚去控制,接线复杂,代码也麻烦。而带驱动板的版本,核心板上多了一片PCF8574 IO扩展芯片,把并口转成了I2C串口。这样只需要两根信号线(SDA和SCL)就能驱动屏幕,省下的IO口可以接更多传感器,代码里也只要调用现成的库函数,不用关心底层时序。
1.2 为什么选超声波而不是红外或激光测距
刚接触测距方案的人经常问:市面上测距的传感器有好几种,为什么要用超声波?
这里我把常见方案对比一下,方便你理解这个项目在方案选型上的合理性:
| 方案 | 典型模块 | 测量范围 | 精度 | 优缺点 |
|---|---|---|---|---|
| 超声波 | HC-SR04 | 2cm - 400cm | ±1cm左右 | 便宜、不受环境光干扰,但对软质吸音物体和斜面不友好 |
| 红外测距 | GP2Y0A21 | 10cm - 80cm | 受环境光影响 | 响应快、体积小,但距离近且太阳光直射时误差大 |
| 激光测距 | VL53L0X | 几十厘米到几米 | 高 | 精度高、速度快,但价格贵、视野窄,软件配置复杂 |
从"入门学习+实用原型"的角度看,HC-SR04是性价比最高的选择。它不依赖光线,在黑暗环境中也能正常工作,这一点比红外强很多。你拿去做倒车雷达放在车库里,晚上关灯照样测得很准。缺点是对柔软物体(比如海绵)的反射效果差,还有就是测量面的角度如果不垂直于超声波波束,回波会散掉导致读数异常。知道了这个特性,后面调试的时候就心里有数了。
2. 硬件准备与电路连接
2.1 物料清单与购买要点
这个项目的物料很少,大多数玩家手头应该都有现成的。我列出完整的清单,并标注几个容易踩坑的地方:
- Arduino Uno或Nano开发板× 1。Uno的USB接口走板载串口芯片,连接稳定;Nano体积小,适合后期集成到小车或小设备里。
- HC-SR04超声波模块× 1。市面上有很多阉割版,选购时找正规商家,重点看模块背面有没有标注型号,还有换能器表面有没有破损。
- 带I2C驱动板的LCD1602× 1。这个就是标题里说的"有驱动板"的屏,驱动板一般焊在屏幕背面的针脚上,上面能明显看到一颗黑色的PCF8574芯片和一颗蓝色可调电位器。
- 杜邦线公对母若干根,公对公若干根。建议买质量好一点的,后面会讲劣质杜邦线会造成哪些奇特故障。
- 面包板(可选)。如果只做这个项目,直接杜邦线对接就行;如果还想接蜂鸣器、LED等其他外设,面包板方便很多。
提示:购买LCD带驱动板的版本时,可以顺便问商家要驱动板的具体I2C地址。不过商家未必清楚,后面我会教你怎么自己扫出地址。
2.2 驱动板上的PCF8574芯片到底做了什么
很多人拿到带驱动板的LCD,第一次拆开看都会好奇:驱动板背面那颗黑色芯片是什么?答案是PCF8574,一颗非常常用的I2C转8位数字IO扩展芯片。
LCD1602裸屏本质上需要11根控制线(8根数据线加上RS、RW、E等控制线),哪怕用4位模式也需要6根。PCF8574的作用就是把这6根或11根线压缩成2根I2C线。芯片内部有8个IO引脚,其中一个还用来控制LCD的背光,剩下的正好驱动LCD的4位数据口和各种控制脚。
这里就有一个新手必踩的坑:I2C设备都有地址。PCF8574的地址由芯片上的A0、A1、A2三个引脚的电平决定,常见成品板一般默认是0x27,但也有不少是0x3F。如果你的代码里地址写错了,屏幕就永远不显示任何东西。这也是后面调试章节我要重点讲的内容。
另外,驱动板上那颗蓝色的多圈电位器是用来调节LCD屏幕对比度的。注意,这个电位器不是调背光亮度,是调液晶屏上字符的清晰度。拧的位置不对,屏幕要么全黑要么白茫茫一片什么都看不到。很多刚上手的人以为屏幕坏了,其实只是对比度没调好。
2.3 接线对照表与引脚说明
这项目的接线非常简洁,一共5根线。我用的Arduino Uno,引脚对应关系如下:
| 设备 | 引脚 | 连接到Arduino |
|---|---|---|
| 超声波模块 | VCC | 5V |
| 超声波模块 | GND | GND |
| 超声波模块 | Trig | D11 |
| 超声波模块 | Echo | D12 |
| LCD驱动板 | VCC | 5V |
| LCD驱动板 | GND | GND |
| LCD驱动板 | SDA | A4 |
| LCD驱动板 | SCL | A5 |
先说LCD这边。Uno的I2C接口固定在A4(SDA)和A5(SCL)两个引脚上,这是硬件I2C接口,不是普通数字IO随便选的。如果你用的是Arduino Nano,同样是A4和A5。如果是Mega2560,I2C接口在20和21号引脚,接线前最好查一眼对应板卡的引脚图。
超声波那边,Trig和Echo我分别选在D11和D12而非D0/D1,有个很实际的考虑:D0和D1是串口通信引脚,接传感器虽然也能用,但你在调试时如果同时开着串口监视器,可能会造成干扰。用靠中间的引脚会稳妥一点。
注意:HC-SR04的Echo引脚返回的是5V高电平信号。Arduino Uno的数字引脚正好是5V逻辑,直接接没问题。但如果你用的是3.3V逻辑的开发板(比如某些低功耗板或ESP32),就必须给Echo做分压处理,否则可能烧坏引脚。
2.4 供电方案的经验之谈
硬件接线里最容易被忽视的就是供电。整个系统的功耗其实非常低:LCD背光耗电大约20到50毫安,超声波模块工作时峰值电流15毫安左右,Arduino开发板自身再算一部分,整体不超过100毫安,用USB线接电脑供电完全够用。
但如果你的项目不只是在桌面上跑,还要接到小车或者野外设备上,供电就要认真考虑了。我的习惯做法是分三种情况:
- 纯USB供电:适合开发调试阶段。注意不要同时给电机、舵机之类的大功率设备供电,否则压降会导致Arduino复位,屏幕闪烁。
- 独立5V电源+共地:系统接外部电源时,一定要把电源负极和Arduino的GND连在一起。这叫"共地",如果不共地,传感器和板子之间的信号参考电压不同,会导致读数乱跳甚至完全无响应。
- 降压模块供电:如果用的是7.4V锂电池组或12V电源,需要先经过降压模块稳定输出5V之后再接入系统。别直接把高压怼到Arduino的5V引脚上,开发板上虽然有个稳压芯片,长期超压工作会发热严重。
3. 代码实现与核心逻辑
3.1 库的选择与安装:LiquidCrystal_I2C
代码实现的第一个关卡是库。驱动LCD1602裸屏用的是系统自带的LiquidCrystal库,而带I2C驱动板的屏需要专门的LiquidCrystal_I2C库。这个库不属于Arduino IDE自带的库,需要手动安装。
我建议直接在Arduino IDE的库管理器里搜索"LiquidCrystal I2C",找到作者名字是fdebrabander的那个版本进行安装,这个版本的库兼容性最好,支持大多数PCF8574驱动的LCD显示屏。安装完成后,代码开头需要这样引用:
#include <Wire.h> #include <LiquidCrystal_I2C.h>Wire库是Arduino内置的I2C通信库,LiquidCrystal_I2C库依赖它工作,所以两个都要包含。有些版本的教程会让你单独去某个网站下载库文件并手动扔到libraries文件夹里,那种方式容易出依赖问题,我后来都用库管理器解决了。
3.2 HC-SR04测距的时序原理与代码拆解
超声波测距的代码是整段程序的核心,理解它的底层时序比背诵代码更重要。HC-SR04的工作时序是这样的:
- 给Trig引脚一个至少10微秒的高电平脉冲。
- 模块内部自动发出8个完整的40kHz超声波脉冲。
- 超声波遇到障碍物后反弹,模块的接收端捕获回波。
- 模块把从发射到接收的时间差,通过Echo引脚输出一个高电平信号。这个高电平持续的时间就是声波往返的总时长。
所以,测距代码要做的就是"触发→测量高电平持续时间→换算距离"这三件事。换算的公式是:
$$距离(cm) = 持续时间(us) \times \frac{343(m/s)}{2}$$
声速在空气中大约343米每秒,换算成厘米每微秒就是0.0343,所以简化写法是:
$$距离 = 持续时间 \times 0.034 \div 2$$
这里有个新手特别容易忽略的点:Echo引脚返回的是"往返"时间,超声波出去了又回来,同一条路走了两趟,所以要除以2。很多初学者的代码公式都对了,就是忘了除以2,导致测出来的距离永远是实际距离的两倍。这个细节之后我在调试部分还会提到。
对应到代码,核心段落长这样:
float readDistance() { digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); long duration = pulseIn(echoPin, HIGH); float distance = duration * 0.034 / 2; return distance; }之前的低电平停2微秒,是为了让Trig脚确保处于干净的低电平状态,避免残留电压导致误触发。pulseIn是Arduino的内置函数,功能就是测量指定引脚上某个电平状态持续的时间,这里测的是高电平持续时间,精度是微秒级。
3.3 LCD显示逻辑:清屏残留与刷新策略
LCD显示这块有两个常见坑,我都在实际项目中遇到过。
第一个坑是字符残留。LCD1602不像终端那样会自动滚动或清除旧内容,你往某一行写了"999 cm",下一次写入"5 cm"时,屏幕上会变成"5 cm9",末尾的"9"就是上一次留下的残影。解决办法有两种:一种是在每次写入前先把整行用空格清掉,另一种是每次写入的字符串末尾都带几个空格做覆盖。我习惯用后者,因为清空整行再写入会造成明显的闪烁。
第二个坑是刷新频率。如果loop循环里不加延时,主频跑起来每秒能刷新几千次,LCD来不及更新,人会感觉屏幕在狂闪。加delay(200)让每200毫秒刷新一次,人眼看起来数字就是稳定平滑变化的。200毫秒这个值是我反复试出来的:小于100毫秒时视觉上有跳动感,大于500毫秒时又显得反应迟钝。超声波测距本身单次测量也就几十毫秒级,200毫秒完全够用。
完整参考代码如下,我加上了越界处理的逻辑:
#include <Wire.h> #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); const int trigPin = 11; const int echoPin = 12; void setup() { Serial.begin(9600); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); lcd.init(); lcd.backlight(); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Distance:"); } void loop() { digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); long duration = pulseIn(echoPin, HIGH); if (duration == 0) { lcd.setCursor(0, 1); lcd.print("No echo "); } else { float distance = duration * 0.034 / 2; lcd.setCursor(0, 1); lcd.print(distance); lcd.print(" cm "); Serial.print("Distance: "); Serial.print(distance); Serial.println(" cm"); } delay(200); }代码里lcd.setCursor(0, 1)的意思是定位到第1行(从0开始计数,所以是第二行)的第0列。用setCursor精确控制显示位置,是LCD编程的基本功。注意我在字符串末尾都加了几个空格,就是刚才说的防止残留。
串口输出那一行不是为了调试期方便,也是定位问题的利器。如果LCD显示正常但串口数据异常,问题出在LCD逻辑;反过来如果串口正常但LCD乱显示,问题一定出在LCD通信上。两条腿走路,排查效率翻倍。
3.4 I2C地址初始化的细节
LiquidCrystal_I2C lcd(0x27, 16, 2);这行代码里,0x27是I2C地址,16是屏幕列数,2是行数。如果你的LCD是4行屏,这里就要改成lcd(0x27, 20, 4)。这些参数必须和你手里的硬件严格对应,尤其是第一参数。
我一直建议:在烧录主代码之前,先单独烧录一个I2C扫描程序去确认地址。扫描程序的核心代码非常简单,原理是遍历I2C总线上的所有可能地址,向每个地址发送探测信号,能收到应答的地址就是已存在设备:
#include <Wire.h> void setup() { Serial.begin(9600); Wire.begin(); for (byte addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.print("Found I2C device at: 0x"); Serial.println(addr, HEX); } } } void loop() {}烧录后打开串口监视器,设置波特率9600,如果硬件连接正常,几秒钟内就会打印出设备的实际地址。显示0x27就用0x27,显示0x3F就把主程序里的地址改掉。这一步花不了三分钟,但能省掉你接下来半小时的困惑。
4. 从零上手:烧录与调试实录
4.1 板卡选择与串口识别
硬件接好、代码写好后,第一件事是让电脑认识这块Arduino。把USB线插上开发板,打开设备管理器,看"端口(COM和LPT)"下是否多出一个COM口。Uno R3的板载芯片一般显示为"Arduino Uno"之类的名字,当你看到COM3或COM5这种端口号,就说明驱动正常。
如果插上USB线后完全没有反应,十有八九是USB线的问题。这一点务必注意:很多商家送的USB线是纯充电线,里面只有电源线没有数据线,插上能供电但电脑识别不了。换个带数据传输功能的手机数据线就能解决。如果换线还不行,再考虑是不是CH340驱动没装好,这类USB转串口芯片在某些兼容板上经常需要手动装驱动。
在Arduino IDE里,工具→开发板选择对应的型号,工具→端口选中刚才看到的COM口,然后点上传按钮。正常情况下几秒钟后就会显示"上传成功"。如果提示找不到开发板,先检查端口有没有选对,再检查开发板型号是不是选错了。
4.2 LCD黑屏或无显示的三大根源
LCD屏幕不亮是这套系统里出现频率最高的问题,但原因其实就三种,按概率排序:
第一,I2C地址不对。前面已经说过,代码里的地址就得通过扫描程序确认。这是最容易被忽略的,因为硬件看起来明明接好了。
第二,对比度电位器没调好。驱动板上那颗蓝色的小旋钮,需要用小螺丝刀顺时针或逆时针慢慢拧。拧到某个位置时屏幕突然显示出淡淡的字符方块,那就对了。很多人以为屏幕是坏的,纯粹因为对比度电位器拧到了死角。我记得自己第一次拿到这块屏幕时,拧了半天全是黑块,最后发现是电位器方向搞反了。
第三,背光跳线没连接。部分驱动板背面有个标注BLK的跳线或焊接点,是控制背光的。有些版本默认状态下背光被禁用,需要把跳线帽接上或用焊锡短接才能点亮背光。如果你的屏幕其他都正常但黑乎乎一片,检查一下这个位置。
还有一个不太常见但容易混淆的原因:LCD模块的排针焊接质量参差不齐。有些廉价模块的排针虚焊,插到面包板上时接触不良。遇到时,可以拿万用表量一下LCD模块的5V和GND引脚之间有没有电压,排除供电断路的可能。
4.3 超声波读数异常排查实录
我实际调试中遇到过几种比较典型的异常情况,整理在下面,方便你对照排查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 距离永远显示0或不变 | Echo引脚线松/Trig和Echo接反 | 重新插拔杜邦线,用万用表确认导通 |
| 距离显示值固定在某个异常大数 | 前方没有障碍物,声波反射不回来 | 在传感器前20cm处放一块平整硬板 |
| 数值剧烈跳动、忽大忽小 | 测量面是斜面/软质物,或周围有环境干扰 | 换成平整墙面测试,或增加软件滤波 |
| 读数永远停留在0.00 | pulseIn超时返回0,通常是Echo没接对引脚 | 检查Echo是否接在D12,代码引脚是否一致 |
| 数值正常但刷新特别慢 | delay设得太大 | 把delay从500ms降到200ms试一下 |
有个现场案例很能说明排查思路。上次做测试时,屏幕上的距离数值一直固定在45cm左右怎么都不变,我推手过去也没反应。检查半天,发现是我把超声波的Trig和Echo杜邦线在面包板上插反了。Trig接到了D12,Echo接到了D11,代码里却用的是D11触发D12接收,信号链路完全错位。所以,接线完成后第一步就是逐条对照表格确认,省得后面猜谜。
脉冲超时问题也很有代表性。HC-SR04测距范围上限标称4米,如果前方空无一物,声波发射出去没有回波,Echo引脚会一直维持低电平,pulseIn等到超时后返回0。代码里如果不对这个情况做判断,屏幕上就会显示一个不正常的0。所以我在主循环里加了duration == 0的判断,输出提示字符而不是傻乎乎地显示0。
4.4 显示乱码和花屏的怪问题
系统能测距了,但LCD时不时闪出毫无规律的乱码块,这个问题困扰了我最久。后来总结出三个方向:
第一个方向是干扰。HC-SR04发射瞬间的电流变化、杜邦线过长引起的信号反射,都可能在I2C数据线上产生毛刺。解决方法:把I2C线的长度控制在20cm以内,避免和超声波的Trig/Echo线绑在一起,必要时给I2C线包一层屏蔽。
第二个方向是供电不稳。当整个系统耗电突然增大(超声波发射时的峰值电流+LCD背光),而电源能力不足,电压跌落会让I2C通信失败,反映到屏幕上就是乱码。换一个电流余量更大的USB口,或者改用独立5V电源供电,问题马上消失。
第三个方向是接触不良。廉价杜邦线的母头弹簧片松垮,插上去当时是好的,稍微碰一下机壳就断连。换用质量好的杜邦线,或者干脆用面包板接线,稳定度会好很多。我的习惯是:临时验证用杜邦线,定型的项目一律焊接到洞洞板上。
5. 优化方向:滤波、扩展与项目进阶
5.1 为什么需要滤波,以及最简单的实现方式
超声波模块单次测量存在随机误差,特别是被测物体表面不太理想时,相邻两次的读数可能差好几厘米。如果你做倒车雷达,屏幕上距离数字如抽风一样跳来跳去,体验会很糟糕。
最简单的处理是滑动平均。维护一个长度为5的数组,每次测量将最新值加入,剔除最大值和最小值,剩下的3个取平均值。这种"砍头去尾取中间"的方法,专业点叫中值加均值混合滤波,能有效抑制尖峰噪声,同时保持响应速度。别一上来就搞卡尔曼滤波,对小项目来说杀鸡用牛刀,效果还不一定比这个好。
float filterDistance(float newValue) { float buf[5]; for (int i = 4; i > 0; i--) buf[i] = buf[i - 1]; buf[0] = newValue; float temp[5]; for (int i = 0; i < 5; i++) temp[i] = buf[i]; for (int i = 0; i < 5; i++) { for (int j = i + 1; j < 5; j++) { if (temp[j] < temp[i]) { float t = temp[i]; temp[i] = temp[j]; temp[j] = t; } } } return (temp[1] + temp[2] + temp[3]) / 3.0; }这段代码逻辑很清楚:先把新数据放到数组头部,把旧数据往后挪,然后排序,去掉最小和最大,中间三个取平均。实测在平整墙面上,滤波前波动±2cm左右,滤波后稳定在±0.5cm以内。
5.2 基于这套系统还能扩展出什么玩法
完成基础功能后,这个项目直接就是其他项目的底盘。我整理几个我自己做过或见别人做过的扩展方向:
- 倒车雷达:在现有代码基础上加一个蜂鸣器模块,距离小于50cm时蜂鸣器开始响,越近频率越高。蜂鸣器接在D13引脚,用tone函数输出不同频率即可。
- 温度补偿声速:超声波声速受温度影响,0℃时声速约331m/s,30℃时约350m/s,误差最多能到3%。加一个DS18B20温度传感器,用公式
speed = 331.4 + 0.6 * temperature即时计算声速,替换代码里固定的0.034系数,精度能提高不少。 - 避障小车:把超声波模块装到舵机的云台上,左右扫描测距,小车根据前方距离判断前进、转弯还是后退。
- 水位监测:把模块竖直安装在容器上方,测量水面距离,用容器总深度减去测得距离就是当前水位。目标物为液面时注意:超声波对水平液面的反射效果不错,但水面的波纹会造成读数波动,滤波就更重要了。
- 云端上报:把串口输出的距离数据发给ESP8266或蓝牙模块,就能在手机上实时查看。我后来做过一个版本,直接把Arduino换成ESP32,代码改动极小,但多出了WiFi和蓝牙能力。
5.3 从原型到产品:Arduino方案的边界在哪
Arduino做原型开发和验证概念非常快,但到了部署阶段要把整套系统做稳定,还是要花些心思。我的经验是分两步走:
第一步,原封不动先跑起来,验证功能逻辑。这时候Arduino就是万能的,库丰富、调试方便、坏了直接换一块。
第二步,当功能确定后,评估一下运行环境。如果只是室内静态运行,Arduino整套方案完全够用,成本低、维护也简单。但如果有震动、高温、强烈电磁干扰,或者要大规模部署,MCU选型、电源方案、PCB设计就要重新考虑了。我在车床上试过装上这套系统测刀具位置,就是典型的不适合场景,车床主轴的电磁干扰让超声波读数完全乱掉,后来还是换成了带屏蔽的专用位移传感器。
不过对你现在来说,用Arduino把这套系统做透,学到的那套"传感器采集→数据处理→输出控制"的思维方法,换到任何平台都是通用的。等真到了需要产品化的时候,再考虑这些问题也不迟。
最后分享一个调试顺序的习惯
我调试这套系统时养成一个固定顺序:先串口后LCD,先传感器后显示。意思是先把超声波数据通过串口打出来,确认传感器工作正常;再去折腾LCD显示,因为LCD涉及的坑(地址、对比度、背光)比传感器多得多。如果一开始就想让所有东西一起工作,出了故障你都不知道该从哪里排查。
还有一个很小的技巧:LCD1602一共有两行,我习惯第一行显示固定的标题或单位,第二行才放动态数据。这样刷新时只重写第二行,第一行的内容完全不动,大大减少闪烁感。人眼对静态区域和动态区域的差异感知很明显,这个小改动带来的体验提升非常值得。
至于后续想折腾的朋友,我真心建议你把这个小项目吃透之后,立刻开始扩展它。哪怕只是加一个LED指示灯表示“距离是否小于阈值”,都能让你对完整的输入-处理-输出闭环理解更深。等到你能独立把这类项目玩转了,再看更复杂的嵌入式项目,就不会再有无从下手的感觉了。