树莓派4B+Python控制HC-SR04超声波测距:接线、代码与避障实战
上周有个做避障小车的朋友在群里喊救命模块都接好了Python代码也跑了但树莓派4B一接上Echo引脚就重启远程登录直接断开测距数据一条都读不到。这问题我太熟悉了——两年前我第一次把HC-SR04超声波模块接到树莓派4B上时也栽在同一个坑里。那会儿网上教程清一色说“Trig接GPIO17Echo接GPIO18VCC接5VGND接地”然后扔一段一模一样的Python代码就完事。至于为什么Echo要分压、为什么GPIO调速要加延时、为什么大冬天测出来的距离比夏天偏大几厘米没人提。这篇文章就想把这套事情讲透。内容围绕树莓派4B Python控制超声波模块HC-SR04展开从选型思路、测距原理、接线分压到代码实现、实际排错、再到避障小车的联动玩法一步一步拆给你看。适合刚接触树莓派外设的初学者也适合那些已经抄过代码但每次都“跑不通”的人。1. 为什么测距实验首选树莓派4B以及HC-SR04的定位1.1 树莓派4B在硬件外设实验里的独特优势树莓派4B这块板子放到今天来看性能谈不上惊艳但做硬件控制实验它有几个别的板子暂时替代不了的位置。首先4B有40Pin的GPIO排针引脚资源足够丰富除了一路I2C、一路SPI、多路UART之外还支持普通电平检测、PWM输出、软硬件中断。一颗传感器接上去你既可以用系统提供的高级库两三行搞定也可以直接用底层库读引脚电平变化自由度非常高。其次是Python生态。树莓派官方系统自带Python 3环境RPi.GPIO和gpiozero这两个库都是预装或一条命令能装好的。对于控制超声波模块这种场景核心动作就是“拉高一个引脚、计时、等待另一个引脚变化”Python表达这类逻辑非常直观。不需要编译C代码也不会被交叉编译环境折磨这对快速验证传感器是否正常、算法是否合理价值巨大。还有一点容易被忽略树莓派4B是完整跑Linux系统的设备。这意味着你测距得到的数据可以直接写成CSV、推送到MQTT、通过HTTP接口展示在网页上甚至和摄像头画面做简单融合。单片机也能做测距但要把数据传到应用层总要费一番周折。树莓派4B相当于把传感器控制和数据处理放在同一个环境里我后面会专门讲数据往外送的扩展玩法。1.2 超声波模块有这么多为什么先认HC-SR04市面上测距模块大致分三类超声波、红外/激光TOF、微波雷达。它们之间不是简单的替代关系而是各管一摊。超声波模块价格便宜、原理透明、抗环境光干扰能力强是入门首选。我整理了一张对比表方便你根据场景快速选型模块方案测距方式标称范围典型精度大致成本适合场景HC-SR04超声波脉冲回波2cm - 400cm约3mm很低避障、液位、近距离测距JSN-SR04T防水一体化超声波探头25cm - 600cm约1cm低水箱液位、户外防尘场景VL53L0X红外激光TOF飞行时间最大约2m受环境光影响约1mm中高精度近距离测距、无人机定高RCWL-9621微波雷达探测范围5-9m测距较弱中中人体感应、存在检测HC-SR04在这个表里的定位很清晰便宜、够用、容易买到。它的模块结构简单正面两颗银色圆柱体分别是发射换能器和接收换能器背面一颗主控芯片负责把返回的脉冲整理成TTL电平信号用户只需要负责计时和计算。这种设计把复杂的超声波信号处理封装在模块内部对Python控制来说最大的好处就是接口极其简单——一根触发线、一根回波线就这两根信号线。也有人说HC-SR04精度不行确实它到了3米以上误差会明显变大45度斜角照射光滑墙面时还可能直接丢回波。但做小车避障、倒车提醒、水箱预警这类场景它的表现完全够用。先把HC-SR04玩明白后面再根据项目需要换激光或雷达方案思路是通的。2. HC-SR04的测距原理与接线前必须想清楚的电平问题2.1 量时间、除以二、乘声速说到底就这六步HC-SR04的测距过程可以用一个生活场景理解你站在山谷里喊一声等回声传回来你掐表记录从喊到听到回声的秒数距离就等于秒数乘声速再除以二。因为声音走的是来回路径实际距离要砍半。具体到模块工作时序一共六个状态主控向Trig引脚发送一个10微秒以上的高电平脉冲模块开始发射超声波模块自动发出8个40kHz的超声波脉冲串超声波遇到障碍物反射回来被接收换能器捕获模块在发出脉冲的同一时刻把Echo引脚拉高当接收换能器捕获到第一个回波的时刻模块把Echo引脚拉低于是Echo引脚高电平持续的时间就等于超声波从发射到返回的全程时间。树莓派这边做的事不是去控制超声波本身而是“测量Echo引脚高电平持续了多久”。得到时间t单位秒再套公式distance_cm t * 34300 / 234300是声速340m/s换算成cm/s后的值。举个实际数字如果Echo高电平持续了5.8毫秒也就是0.0058秒那么distance_cm 0.0058 * 34300 / 2 99.47cm接近1米。这里必须明白一个关键点树莓派无法直接影响Echo引脚何时拉高拉低只能被动读取电平变化。所以Python代码里要用“死循环等待引脚跳变”的方式记录跳变瞬间的时间戳。等待跳变这个过程是后续踩坑的重灾区后面代码部分细说。2.2 Trig、Echo、VCC、GND每根线该接哪HC-SR04总共四个引脚很好认VCC供电正极官方标称5V。模块内部有稳压处理直接从树莓派4B的5V引脚取电即可。GND供电负极必须和树莓派共地。这一步我见过很多人漏掉导致Echo信号没有参考电平读数完全乱跳。Trig触发输入树莓派用GPIO输出给这里控制模块发射超声波。Echo回波输出模块把回波时间门控成一个高电平脉冲从这根线输出。我在默认接线里用GPIO17当Trig、GPIO18当Echo两者分别是BCM编号的第11脚和第12脚。如果你习惯用其他引脚只要在代码里改对应编号就行模块本身不挑引脚。2.3 Echo输出5V树莓派GPIO只能承受3.3V这里必须处理HC-SR04的Echo引脚在模块正常运行时会输出5V高电平但树莓派4B的GPIO电平是3.3VGPIO输入引脚最高容忍电压也是3.3V。直接把5V接到3.3V输入上轻则读数不稳定重则电流倒灌进SoC导致系统重启甚至损坏引脚。解决办法是在Echo信号线和树莓派GPIO之间做电阻分压。最常见的做法是用1kΩ和2kΩ两个电阻做一个分压网络Echo (5V) ──[1kΩ]──┬── GPIO18 │ [2kΩ] │ GND输出电压近似为5V × (2kΩ / (1kΩ 2kΩ)) 3.33V略低于GPIO允许的最高3.3V而在安全范围内。如果你手头没有2kΩ也可以搭配两个1kΩ得到2.5V更保守但同样能正常识别高电平。分压电阻精度不用纠结普通5%碳膜电阻就够用。有人会问不接分压直接上电实测很多模块短期也能工作为什么非要处理我的回答是短期能工作不代表是对的。树莓派的GPIO引脚标称最大输入电压是3.3V超过这个值就是风险负载长期使用或在电磁环境复杂的场景下稳定性很难保证。做项目不是图一两次能跑而是要保证不在关键时刻掉链子。2.4 完整接线清单与通电前自检下面是一份我实际用过的接法照抄即可树莓派4B引脚功能连接目标Pin 2 (5V)5V供电HC-SR04的VCCPin 6 (GND)地HC-SR04的GNDPin 11 (GPIO17)Trig触发HC-SR04的TrigPin 12 (GPIO18)Echo回波先接1kΩ电阻再到GPIO18的节点2kΩ电阻从该节点接地建议使用至少两种颜色的杜邦线把不同功能区分开红色接VCC、黑色接GND、黄色接信号线。通电之前最好用万用表测一下GPIO18节点对地电压确保没有被错误接成5V。这一步只要做一次能帮你省掉后面一整晚的排错时间。3. Python控制代码从引脚脉冲到稳定距离值3.1 两套主流库怎么选RPi.GPIO还是gpiozero树莓派Python控制GPIO最常见的选择是RPi.GPIO和gpiozero。RPi.GPIO是老牌库提供最底层的GPIO读写接口逻辑直白适合理解原理。gpiozero是树莓派官方推荐的高层库把传感器抽象成类代码更短但封装层比较厚出问题时反而不容易看清内部发生了什么。我的建议是第一遍学习用RPi.GPIO手写完整时序搞明白每个引脚状态变化确认理解之后再上手gpiozero写生产代码它能自动处理不少边缘情况代码也更易读。两种库我都给出代码你在实际项目里根据偏好选一套用就行。3.2 用RPi.GPIO手写脉冲时序先搞懂原理再谈封装这是我最常用的RPi.GPIO版本加入了超时保护避免了网上流传的经典版本里“死循环卡死”的问题。import RPi.GPIO as GPIO import time TRIG 17 ECHO 18 GPIO.setmode(GPIO.BCM) GPIO.setup(TRIG, GPIO.OUT) GPIO.setup(ECHO, GPIO.IN) def read_distance(): # 确保Trig为低电平并稳定一小段时间 GPIO.output(TRIG, GPIO.LOW) time.sleep(0.002) # 发送至少10微秒的高电平触发脉冲 GPIO.output(TRIG, GPIO.HIGH) time.sleep(0.00001) GPIO.output(TRIG, GPIO.LOW) # 等待Echo变高并记录起始时间 timeout_start time.time() while GPIO.input(ECHO) GPIO.LOW: if time.time() - timeout_start 0.05: return None # 超时没有检测到脉冲起点 pulse_start time.time() # 等待Echo变低并记录结束时间 timeout_start time.time() while GPIO.input(ECHO) GPIO.HIGH: if time.time() - timeout_start 0.05: return None # 超时没有检测到脉冲终点 pulse_end time.time() duration pulse_end - pulse_start distance_cm duration * 34300 / 2 return round(distance_cm, 2) if __name__ __main__: try: while True: dist read_distance() if dist is not None: print(f距离{dist} cm) else: print(读数为空请检查模块连接或障碍物距离) time.sleep(1) except KeyboardInterrupt: print(程序退出) finally: GPIO.cleanup()解释几个设计决策。等待Echo变高之前为什么要先让Trig低电平睡2毫秒因为某些模块在上电后的初始状态可能还在处理残余信号先让触发线稳定在低电平能避免误触发。触发脉冲用sleep(0.00001)正好10微秒满足数据手册“触发脉冲不低于10微秒”的要求。两个超时判断都设为50毫秒。正常情况下2米以内的目标完整往返时间大约11.6毫秒50毫秒已经远超正常范围。如果模块损坏、接线断开或目标超出量程程序不会卡死而是返回None让上层处理。这里我要特别强调一个容易踩的坑Echo信号是从“发完触发脉冲之后”才开始等不是从代码一开始就等。如果物体距离特别近比如贴着模块表面回波时间极短Echo高电平可能只有几十微秒循环等待的粒度会直接影响精度。Python的time.time()精度在多数情况下够用但如果你追求极致精度可以考虑改用time.perf_counter_ns()后面进阶部分会提。3.3 用gpiozero十行代码快速验证如果你只是临时验证模块好坏或者代码可维护性优先我推荐直接用gpiozero。from gpiozero import DistanceSensor from time import sleep sensor DistanceSensor(echo18, trigger17, max_distance4) while True: dist_cm sensor.distance * 100 print(f距离{dist_cm:.2f} cm) sleep(1)gpiozero的DistanceSensor把脉冲测量、超时处理、单位换算全部封装好了。sensor.distance返回的是以米为单位的浮点数乘以100就是厘米。max_distance参数特别关键gpiozero默认值是1米不设置的话超过1米的目标会直接被截断成最大值这是新手最容易忽略的默认值陷阱。还有一个小细节gpiozero在程序被CtrlC中断时会自动清理GPIO设置不需要手动调用cleanup()。RPi.GPIO则必须在finally块里手动清理否则下次运行会报“GPIO already in use”之类的警告。3.4 数据能读出来只是第一步滤波与超时保护超声波模块有个天然缺陷单次读数会有明显的抖动。哪怕模块固定不动连续测10次结果也可能在±0.5cm之间波动遇到表面倾斜或环境干扰偶尔还会冒出个离谱值。处理办法分两级。第一级是单点滤波连续读5次去掉最大最小值取中间3次平均值。这样能把随机噪声压下去又不至于响应太慢。第二级是逻辑过滤直接丢弃超出量程范围的读数比如小于2cm或大于400cm的这些值往往是误触发的结果。代码可以这样写def stable_distance(samples5): values [] for _ in range(samples): d read_distance() if d is not None: values.append(d) time.sleep(0.03) if len(values) 3: return None values.sort() return sum(values[1:-1]) / (len(values) - 2)排序后去头去尾再取平均对脉冲型噪声特别有效。实际做避障小车时这种处理方式能明显减少误判不会因为一个尖峰就急刹车。4. 实测中的坑从“一插就重启”到“读数乱跳”的完整排查链路4.1 树莓派一接Echo引脚就重启分压电阻救了一命我朋友遇到的那个问题——树莓派一接Echo引脚就重启——原因就出在Echo输出的5V电平上。5V信号接入3.3V输入引脚时电流会通过引脚内部保护二极管倒灌进3.3V电源轨导致3.3V电压出现瞬间坍陷系统供电不稳于是重启。排查过程是这样的先把所有接线断开用万用表单独量树莓派GPIO18对地电压发现只有0.0几V说明GPIO本身正常。接着把HC-SR04单独上电用万用表量Echo引脚空闲电压0V正常。再用一个干扰物快速晃动模块前方触发测距万用表能看到Echo引脚跳到4.9V左右问题核心浮现——Echo空闲时是0V一旦工作就是5V而树莓派要求不超过3.3V。解决方案就是2.3节里说的1kΩ2kΩ分压网络。接好之后再次测量GPIO18节点高电平时稳定在3.3V附近树莓派不再重启读数正常。这里有一个教训不要因为模块“看起来”能工作就忽略电平规格。那天朋友把模块接在Arduino上也觉得没事但树莓派是3.3V逻辑不是5V逻辑平台的差异决定了同样的接法在不同板上结果是天壤之别。4.2 读数变成0cm或上下乱跳共地、探头朝向与表面材质读数乱跳的问题我在排错时归纳出三个高频原因。第一个是没共地。有些教程画图时只画信号线新手就照着只接一根GPIO线地线没接到树莓派的GND。这样Echo输出的高电平没有一个统一的参考地树莓派测到的电平可能随机漂移。解决方式很粗暴VCC和GND都从树莓派引出尤其GND必须共用同一电源的地。第二个是探头正面没对准障碍物或者障碍物表面是吸音材质。HC-SR04的发射探头有大概15度左右的波束角如果墙面倾斜角度太大回波会反射到别处接收探头收不到信号Echo高电平持续时间就会异常表现为读数跳变。这时候可以把障碍物换成一块垂直于探头的硬纸板看读数是否稳定。第三个是电源干扰。如果超声波模块和电机驱动共用电源电机启动瞬间的电压跌落会影响模块工作导致Echo脉冲时序变乱。我在一个避障小车项目里遇到的情况就是模块放桌面正常装到小车上就乱跳最后把电机驱动独立供电并给模块的VCC和GND之间并了一个100μF电解电容问题才消失。引起乱跳的原因可能不止一个但排查顺序可以固定先保证模块在桌面上读数稳定再装到设备上装上去之后若乱跳优先考虑电源共地干扰而不是怀疑代码。4.3 距离整体偏大或偏小跟温度有什么关系超声波在空气中的传播速度不是恒定340m/s而是随温度变化。近似公式是c 331.4 0.6 × T其中T是摄氏温度。25℃时声速约为346.4m/s0℃时大约331.4m/s。如果你在0℃环境下用340m/s计算一个2米的目标会差出大约1.2厘米。冬天做户外测距这个误差已经足够让避障逻辑产生误判。修正方法很简单读一个温度传感器把声速作为参数代进计算def sound_speed(temp_c): return 331.4 0.6 * temp_c def distance_from_duration(duration, temp_c20): speed_cm_s sound_speed(temp_c) * 100 return duration * speed_cm_s / 2湿度对声速也有影响但影响幅度比温度小一个数量级工程上一般忽略。如果你测量的是室内液位、桌面物体这类环境温度变化不大的场景用固定340m/s其实问题不大但如果温差超过10℃或者做户外项目温度补偿就值得加进去。4.4 一份可以直接照抄的排查顺序清单我再把整个排查思路浓缩成一张清单遇到问题按顺序执行断电状态用万用表量接线是否短路GPIO18节点对地电压必须为0V单独给HC-SR04供电用万用表或示波器量Echo引脚确认空闲0V、触发后有5V脉冲确认GND共地后再检查Trig引脚是否接收到10微秒高电平可以用示波器或逻辑分析仪观察不接树莓派先做桌面测试排除安装位置和周边障碍物干扰接上树莓派后用print输出原始duration值确认数据在合理范围而不是直接被上层滤波逻辑吞掉如果读数仍然异常逐项排查电源干扰、探头朝向、温度补偿三个方面。这套流程不需要复杂的工具一个万用表加一个示波器就能覆盖九成以上的问题。很多项目最后被证明是“模块和板子都正常唯独线序接错了”而这套流程能帮你最快定位。5. 顺着这个项目还能玩出什么从单点测距到避障小车5.1 低成本水位预警GPIO联动LED和蜂鸣器把测距逻辑稍微改一改就能变成一个水箱水位预警器。超声波模块从水箱顶部向下照射水面返回的距离就是液面到探头的距离水位高度等于水箱总高度减去这个距离。这时候代码的核心不是读距离而是判断阈值。比如总高度是50cm当距离值大于35cm意味着水位低于15cm触发GPIO输出高电平让LED亮起当距离小于10cm水位接近满溢让蜂鸣器响。import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.OUT) # Trig GPIO.setup(18, GPIO.IN) # Echo GPIO.setup(23, GPIO.OUT) # 液位过低 LED GPIO.setup(24, GPIO.OUT) # 液位过高蜂鸣器 while True: dist stable_distance() if dist 35: GPIO.output(23, GPIO.HIGH) GPIO.output(24, GPIO.LOW) elif dist 10: GPIO.output(23, GPIO.LOW) GPIO.output(24, GPIO.HIGH) else: GPIO.output(23, GPIO.LOW) GPIO.output(24, GPIO.LOW) time.sleep(1)注意防水问题普通HC-SR04不防水水箱场景建议换JSN-SR04T这种一体化防水探头探头本体可以浸入液体环境或贴合容器外壁后面的驱动板部分放在干燥处。5.2 避障小车的控制思路用一个传感器做“看到就停”避障小车是超声波测距最经典的落地场景。逻辑非常朴素测到前方距离小于某个阈值就停止或转弯否则继续前进。但把传感器接到小车底盘时有两个问题必须先处理好。第一是供电隔离电机启动的电流尖峰会污染电源导致超声波模块读数乱跳通常需要单独供电或做电源滤波第二是探头固定角度一个探头朝正前方能覆盖的只是一个扇形区域侧面容易被遗漏所以很多车用两个探头错开安装一个朝前一个朝左前。用树莓派4B接L298N这类电机驱动芯片时控制代码的核心是把测距结果和PWM输出结合起来。转速控制用RPi.GPIO的PWM功能速度调整不要太激进把距离变化做一阶平滑小车跑起来会平稳很多。from gpiozero import Robot, DistanceSensor from time import sleep sensor DistanceSensor(echo18, trigger17, max_distance2) robot Robot(left(14, 15), right(25, 26)) while True: dist sensor.distance * 100 if dist 20: robot.left() # 障碍物太近向左转 else: robot.forward(0.4) # 正常前行 sleep(0.1)Robot类里left和right的参数是电机驱动板上两个电机的正反转引脚。实际接线时电机驱动板需要独立的降压模块供电不要直接吃树莓派GPIO口的电流这个基本常识能避免很多烧板事故。5.3 多探头组网与数据上云测距结果不只是给自己看的单个超声波模块只有一个角度覆盖范围有限。在稍微完整一点的项目里我通常用一个树莓派4B接两到四个HC-SR04分别探测前、后、左、右四个方向。每个传感器都用独立的Trig引脚Echo引脚如果数量紧张可以把同一个方向的两个传感器错开时序共用一根Echo线但一般建议GPIO资源够用就直接分配独立引脚代码逻辑会更清晰。数据往外送的部分最简单的方案是写一个Flask接口树莓派把距离数据POST到一个局域网服务前端用ECharts画一个实时折线图。更进一步可以把数据送到MQTT Broker手机上通过订阅主题查看当前距离。这些扩展本质上都不难测距模块只是数据源剩下的是树莓派上的数据管道开发。我个人做这类项目时会先把“单次读取稳定”这个基础砸实再考虑多传感器和数据上云。传感器数据不稳定的时候上云只会把噪声传播到各个终端排错时脑子会炸。最后再聊点实际操作心得把树莓派4B和Python控制超声波模块这套流程完整跑一遍之后你会发现自己对“GPIO电平”“时序等待”“数据滤波”这些概念都有了很直观的理解。HC-SR04虽然是最便宜的测距模块但它逼着你思考信号电平匹配、外部干扰隔离、声速环境补偿这些做硬件项目绕不开的问题这些经验迁移到其他传感器上同样适用。我个人踩过最大的坑就是一开始太迷信现成代码拿到就跑不出问题还好一出问题完全没头绪。后来老老实实用万用表量电平、用示波器看时序、每根线都追一遍反而很快定位了问题所在。所以如果你现在也卡在某一步我建议先放下代码去检查物理连接和电平关系大概率能找到答案。接下来你可以试着给这个小项目加一个温度补偿或者换上防水探头测一次水箱液位会打开很多新玩法。