给机器人加一双能测距的"眼睛",HC-SR04 大概是绕不开的第一课:两支金属罐似的超声换能器,一块蓝色 PCB,价格只有激光 ToF 模块的零头,却能测 2 厘米到 4 米。它是靠声波而不是靠光工作的,所以透明玻璃这类激光容易直接穿过去的障碍物,反而难不倒它;代价是有盲区、读数会随温度漂。这篇把它的 Trig/Echo 时序、pulseIn 测距和换算公式一次讲清,代码与参数均取自 Makerguides 与 HowToMechatronics 两篇公开教程,可逐项核对。

先搞懂它怎么"听"出距离
HC-SR04 本质上是一套迷你声呐,工作原理和潜艇、和蝙蝠觅食是同一套逻辑。两侧的圆柱体一个是发射器(T)、一个是接收器(R):触发之后,发射器朝正前方发出一束 40kHz 的超声波——这个频率已经超出人耳上限,你听不见它"喊"了什么。声波碰到障碍物反射回来,被接收器接住。只要记下从"喊一声"到"听见回声"之间隔了多久,再乘以声速、除以二,距离就出来了。
为什么要除以二?因为声波跑的是一个来回:先飞到物体,再弹回传感器,Echo 给出的时间是两段路程的总和,除以二才是单程。把这个逻辑记牢,后面的公式全是水到渠成。
选型时用得上的关键规格,官方资料给得很齐,列表对着看:
| 项目 | 参数 |
|---|---|
| 工作电压 | 5V |
| 工作电流 | 15mA |
| 工作频率 | 40kHz |
| 测距范围 | 2cm 至 400cm |
| 精度 | 3mm |
| 测量角度 | 15° 以内 |
| 尺寸 | 约 45×20×15mm |
15mA 的功耗对 Arduino 板载稳压来说毫无压力,直接从 5V 引脚取电即可,不用单独供电。
引脚定义与接线
模块一共四个引脚,PCB 两边各留了安装孔,方便固定在小车前脸:
- VCC:接 5V 供电;
- Trig:触发输入,单片机往这里发一个 10 微秒的高电平脉冲,模块就开始发声;
- Echo:回波输出,模块用高电平的持续时长告诉你声波飞了多久;
- GND:接地。

接 Arduino Uno 只需要四根线:VCC 接 5V,GND 接 GND,Trig 接 D9,Echo 接 D10。Trig 和 Echo 用任意数字引脚都行,代码里对应改一下就好——Makerguides 的例程用的是 D2 和 D3,HowToMechatronics 用的是 D9 和 D10,本质上没有区别。
Makerguides 还提到一种省引脚的接法:把 Trig 和 Echo 并到同一个引脚上,做成三针模式。同一个脚先设为输出发触发脉冲,再切成输入等回波,一个引脚当两个用,引脚紧张的小项目里挺实用。


Trig/Echo 时序:一次测距的完整过程
每次测距按固定剧本走。第一步,把 Trig 拉低两微秒左右,把引脚状态清干净;第二步,把 Trig 拉高至少 10 微秒——这是数据手册要求的触发脉宽,短了模块不搭理你;第三步,模块收到触发后发出一串 8 个周期的 40kHz 超声波脉冲。
第四步才是重点:8 个周期发完的瞬间,Echo 引脚被拉高,开始等回波;声波往返一圈回来时,Echo 拉低。所以 Echo 高电平的持续时长,就是声波跑一个来回的总时间,和时间成正比,测得越远高电平越长。
两个容易踩坑的边界要记住。一是超时:如果前方没有反射物,Echo 会一直撑到 38 毫秒才回低,写代码时要给 pulseIn 设个超时或者判断这种情况,否则程序会干等。二是间隔:两次测距之间至少隔 29 毫秒,让上一轮的声波余响彻底散掉,不然新旧回波叠在一起,读数会抽风。后面例程里的 delay(50) 就是这个用意。

pulseIn 测距与换算公式
Arduino 自带的 pulseIn() 正好干计时这个活:它盯着指定引脚,等电平跳变并开始计时,直到电平变回去,返回脉冲宽度,单位微秒。有了微秒数,换算公式一行搞定:
距离(厘米)= 脉冲时长(微秒)× 0.034 ÷ 2
0.034 是声速的换算系数:20°C 时声速约 343 米/秒,折算过来是 0.0343 厘米每微秒;除以 2 是因为声波走了一个来回。代入验证一下:pulseIn 返回 2000 微秒,0.0343 × 2000 ÷ 2 = 34.3 厘米,量纲和数值都合理。
把 HowToMechatronics 的完整例程原样贴出来,触发、计时、换算、输出一条龙:
/*
Ultrasonic Sensor HC-SR04 and Arduino Tutorial
by Dejan Nedelkovski,
www.HowToMechatronics.com
*/
// defines pins numbers
const int trigPin = 9;
const int echoPin = 10;
// defines variables
long duration;
int distance;
void setup() {
pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
pinMode(echoPin, INPUT); // Sets the echoPin as an Input
Serial.begin(9600); // Starts the serial communication
}
void loop() {
// Clears the trigPin
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Sets the trigPin on HIGH state for 10 micro seconds
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Reads the echoPin, returns the sound wave travel time in microseconds
duration = pulseIn(echoPin, HIGH);
// Calculating the distance
distance = duration * 0.034 / 2;
// Prints the distance on the Serial Monitor
Serial.print("Distance: ");
Serial.println(distance);
}烧进去打开串口监视器(波特率 9600),手在传感器前晃一晃,Distance 后面的数字就会跟着变。想显示成英寸也有现成系数:脉冲时长乘 0.0133 再除以 2 即可。
讲究一点还可以做温度补偿。声速对温度挺敏感:20°C 时约 340 米/秒,零下 20°C 就掉到约 315 米/秒,差了快一成。常用近似式是 331.3 加 0.606 乘以摄氏温度;HowToMechatronics 用 DHT22 的版本还把湿度项也算了进去。测三四米的远距离时,这个修正是有感观的,近距离可以忽略。
加块 LCD,做成独立测距尺
不想依赖电脑串口,接一块 1602 液晶就能做成独立的电子尺。HowToMechatronics 的 LCD 例程同时输出厘米和英寸两行读数,代码同样原样保留:
/*
Ultrasonic Sensor HC-SR04 and Arduino Tutorial
by Dejan Nedelkovski,
www.HowToMechatronics.com
*/
#include "LiquidCrystal.h" // includes the LiquidCrystal Library
LiquidCrystal lcd(1, 2, 4, 5, 6, 7); // Creates an LCD object. Parameters: (rs, enable, d4, d5, d6, d7)
const int trigPin = 9;
const int echoPin = 10;
long duration;
int distanceCm, distanceInch;
void setup() {
lcd.begin(16, 2); // Initializes the interface to the LCD screen, and specifies the dimensions (width and height) of the display
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
}
void loop() {
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
duration = pulseIn(echoPin, HIGH);
distanceCm = duration * 0.034 / 2;
distanceInch = duration * 0.0133 / 2;
lcd.setCursor(0, 0); // Sets the location at which subsequent text written to the LCD will be displayed
lcd.print("Distance: "); // Prints string "Distance" on the LCD
lcd.print(distanceCm); // Prints the distance value from the sensor
lcd.print(" cm");
delay(10);
lcd.setCursor(0, 1);
lcd.print("Distance: ");
lcd.print(distanceInch);
lcd.print(" inch");
delay(10);
}这段例程里 LiquidCrystal 对象用的引脚是 1、2、4、5、6、7,与测距的 D9/D10 并不冲突,照着接就行。
盲区、误差与实用建议
第一是盲区。最小测距 2 厘米,比这更近的物体它测不了——发射和接收挨得太近,回波和发射余振在时间上分不开。装在小车最前端时,注意别让障碍物有机会贴脸。
第二是波束角和材质。15° 的测量角度意味着它看的是一张锥形"视野",锥外的物体它看不见;斜面会把声波反射到别处去,海绵、布料这类软材料会吸声,这些情况读数都会飘或者丢。需要更稳的输出,用 NewPing 库的 ping_median() 做中值滤波,默认采 5 次丢掉离谱值再取平均,效果立竿见影。
用 NewPing 的话,主循环还能再瘦一圈,一行 ping_cm() 直接拿厘米数,超出量程返回 0,判断起来省事:
#include "NewPing.h"
#define TRIGGER_PIN 9
#define ECHO_PIN 10
#define MAX_DISTANCE 400 // Maximum distance we want to measure (in centimeters).
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.
void setup() {
Serial.begin(9600);
}
void loop() {
delay(50); // Wait 50ms between pings (about 20 pings/sec). 29ms should be the shortest delay between pings.
int distance = sonar.ping_cm(); // Send ping, get distance in cm and print result (0 = outside set distance range)
Serial.print("Distance: ");
Serial.print(distance);
Serial.println("cm");
}第三是环境适配。HC-SR04 不防水,潮湿、粉尘大的场合换带防水壳的 JSN-SR04T 更省心;如果应用里温度变化极端、声速修正都嫌麻烦,Makerguides 也提示可以看 SHARP 的红外测距方案(GP2Y0A21YK0F 一类),红外方案基本不受气温影响。
总的来看,四根线、十几行代码,就能给小车或者机器人加上最基础的避障感知。把 10 微秒触发、8 周期 40kHz、Echo 计时、除以二这四件事记牢,HC-SR04 这块小板子就真正为你所用了。











潮湿车间里用这模块就是遭罪,后来换了带防水壳的 JSN-SR04T 才消停,文中提到这个替代款很实在。
之前用这块模块做避障,小车老在墙角抽风,后来才明白是两次触发间隔太短,声波余响没散干净。照这篇说的把循环里加上 delay(50) 之后稳多了。另外 38 毫秒超时这个点讲得很到位,pulseIn 不设超时的话,前方没东西时程序真的会卡一拍,远距离测距建议都处理一下。
2 厘米盲区这个是真的坑,我装车头第一次贴墙就翻车了,读数直接没了,哈哈。
NewPing 的 ping_median 中值滤波确实好用,默认采五次去离谱值,读数立马不飘了,推荐新手直接上这个库。
除以二这事我当年想了半天,原来声波走个来回,恍然大悟。