基于Raspberry Pi Pico的超声波与激光测距传感器融合雷达系统实践
1. 项目概述与核心价值
最近在捣鼓一些嵌入式传感项目,手头正好有常见的HC-SR04超声波传感器和TOF10120激光测距模块,想着能不能把它们玩出点新花样。单纯测个距离显示在串口监视器上,总觉得少了点工程实践的“味道”。于是,一个想法冒了出来:能不能用最基础的硬件,模拟出一个简易的“雷达”扫描效果?不仅能实时显示距离,还能把探测到的物体位置可视化出来。
这个项目的核心,就是利用Raspberry Pi Pico这块性价比极高的微控制器,同时驱动超声波传感器和ToF激光测距传感器进行距离探测。为了实现扫描效果,我加入了一个SG90伺服电机来带动传感器旋转。整个系统的“大脑”运行着基于Arduino框架编写的固件,而所有的数据可视化工作,则交给了运行在安卓手机上的DumbDisplay应用来完成。通过一根USB线连接Pico和手机,你就得到了一个完整的、带图形化界面的微型雷达模拟器。
这个项目非常适合已经接触过Arduino基础、想进一步了解多传感器融合、实时数据可视化以及电机控制的爱好者。你将学到如何让两种原理不同的测距传感器协同工作,如何通过PWM信号精确控制舵机角度,以及如何利用串口通信将微控制器的数据“投射”到手机屏幕上,形成一个动态的雷达图。整个过程涉及硬件连接、底层驱动编写、数据处理和上层UI交互,是一个综合性很强的嵌入式系统入门实践。
2. 硬件选型与连接方案解析
2.1 核心控制器:为什么选择Raspberry Pi Pico?
在这个项目中,微控制器需要承担多项任务:产生超声波传感器的触发信号并捕获回波、通过UART与ToF传感器通信、生成舵机控制所需的PWM信号,同时还要通过USB虚拟串口与手机端的DumbDisplay应用进行高速数据交换。Raspberry Pi Pico基于RP2040双核ARM Cortex-M0+处理器,主频133MHz,性能对于此类任务绰绰有余。其最大的优势在于丰富的GPIO资源和极低的成本,并且完美支持Arduino框架,使得开发体验与传统的Arduino Uno/Mega非常接近,但性能却强得多。
我选择Pico的另一个关键原因是其USB通信的稳定性和灵活性。它能够稳定地模拟一个CDC(通信设备类)串口,与手机连接时可以被识别为一个标准串行设备,这是DumbDisplay应用能够与之通信的基础。虽然ESP32等开发板也能实现类似功能,但Pico在纯粹的数据吞吐和GPIO直接控制方面显得更加“专注”和高效。
2.2 传感器双雄:超声波与ToF的优劣与互补
HC-SR04超声波传感器的工作原理是经典的“回声测距”。它通过Trig引脚接收一个至少10微秒的高电平脉冲来触发一次声波发射,然后Echo引脚会输出一个高电平脉冲,其宽度与声波往返时间成正比。计算距离的公式为:距离 = (高电平时间 × 声速) / 2。在常温下,声速约340m/s,即0.034cm/微秒。因此,代码中常使用duration * 0.034 / 2来计算厘米距离。
它的优点是成本极低、原理简单、不受光线影响。但缺点也很明显:测量精度受环境温度、湿度影响较大;波束角较宽(约15度),容易受到侧面物体的干扰;对柔软、吸音材质的物体检测效果差;最小测量距离存在盲区(通常2-3厘米)。
TOF10120激光测距传感器采用的是“飞行时间”原理。它向目标发射一束调制过的红外激光,并测量激光反射回来的时间,直接计算出距离。我们通过UART发送指令r6#,它会返回如L=156mm格式的字符串。其优点是精度高(毫米级)、波束角非常小(指向性强)、响应速度快,且不易受环境声波干扰。缺点是成本较高,强光直射下可能失效,对透明物体(如玻璃)或极度吸光的黑色绒布测量可能不准。
在实际搭建中,让这两个传感器一起工作非常有价值。你可以通过UI按钮实时切换,直观对比两者在不同材质、不同距离、不同环境光下的测量差异。这是一种非常有效的传感器特性学习方式。
2.3 动力与可视化:舵机与DumbDisplay
SG90舵机是一个位置伺服电机。它通过接收周期为20ms的PWM信号,并根据信号中高电平的脉宽(通常在0.5ms到2.5ms之间)来对应输出0到180度的角度。在Arduino框架下,我们可以直接使用强大的Servo库来控制它,无需手动计算PWM细节。在本项目中,舵机的作用是带着传感器做周期性摆动,从而实现扫描效果。
DumbDisplay是这个项目的“点睛之笔”。它是一个运行在安卓手机上的应用,通过USB OTG线接收来自Pico的串口数据,并根据预定义的协议在手机上绘制出雷达图、波形图、数字显示和按钮控件。这省去了为项目单独配备屏幕的麻烦,并且手机屏幕的高分辨率和色彩表现力远超一般的TFT屏模块。其通信本质是串口,因此任何能输出串口数据的设备都能驱动它,通用性极强。
2.4 电路连接实战与注意事项
正确的硬件连接是项目成功的基石。下面给出经过验证的接线方案,并解释每一根线的作用。
电源规划:这是最容易出错的地方。Pico的VBUS引脚直接来自USB口的5V电源。HC-SR04和SG90舵机都需要5V工作电压,因此它们的VCC引脚都应接在Pico的VBUS上。而TOF10120的工作电压是3.3V,必须接在Pico的3V3(OUT)引脚上。切勿接错,否则可能损坏传感器。所有设备的GND都必须连接到Pico的GND,共地是电路正常工作的前提。
信号线连接:
- HC-SR04:
Trig->GP4(Pico输出触发信号)Echo->GP5(Pico输入回波信号)
- TOF10120:
TxD->GP13(传感器发送数据线,接Pico的接收引脚RX)RxD->GP12(传感器接收数据线,接Pico的发送引脚TX)- 注意:这里容易混淆。传感器的TxD应连接微控制器的RX引脚,传感器的RxD连接微控制器的TX引脚。代码中
UART Serial3(TOF_RX_PIN, TOF_TX_PIN, 0, 0),第一个参数是RX引脚号,第二个是TX引脚号,所以TOF_RX_PIN(GP12)实际是Pico的RX,用来接收来自传感器TxD的数据。
- SG90:
- 信号线(橙色)->
GP10(Pico输出PWM控制信号)
- 信号线(橙色)->
注意:当同时连接舵机和超声波传感器到VBUS时,在电机启动瞬间可能会引起电压小幅波动,可能导致超声波传感器读数异常。一个实用的技巧是在VBUS和GND之间跨接一个100μF以上的电解电容,可以起到电源滤波和缓冲的作用。
为了接线整洁,强烈建议使用一块面包板或者一个Pico扩展板。将所有电源和地线在面包板上汇流,可以避免Pico引脚上插满杜邦线的混乱局面,也使得电路更稳定可靠。
3. 软件架构与核心代码实现
3.1 开发环境搭建:PlatformIO的优势
我强烈推荐使用VSCode + PlatformIO来代替传统的Arduino IDE进行开发。PlatformIO是一个专业的嵌入式开发平台,支持库依赖管理、版本控制、更强大的代码提示和调试功能。对于这个多文件、多依赖库的项目,它能管理得井井有条。
项目配置文件platformio.ini是核心,它定义了项目的所有构建规则:
[env:pico] platform = raspberrypi board = pico framework = arduino lib_deps = https://github.com/trevorwslee/Arduino-DumbDisplay arduino-libraries/Servo@^1.1.8 upload_port = /dev/cu.usbmodem1101 ; Mac/Linux示例,Windows通常是COMx monitor_speed = 115200platform和board指定了硬件平台。framework = arduino意味着我们使用Arduino框架来编程。lib_deps部分至关重要,它声明了项目依赖的两个外部库:Arduino-DumbDisplay(用于UI通信)和官方的Servo库。PlatformIO会自动从网络仓库下载并管理这些库。upload_port需要根据你的操作系统和实际连接的端口进行修改。
将提供的.ino草图文件内容复制到src/main.cpp中,PlatformIO就能正确识别并编译。这种将.ino作为.cpp来管理的方式,避免了Arduino IDE的一些隐式规则,让项目结构更清晰。
3.2 传感器数据采集的核心代码剖析
项目的逻辑核心在于稳定、准确地读取两个传感器的数据。代码中通过宏定义来灵活配置引脚,方便你根据实际拥有的硬件进行裁剪。
超声波测距的实现: 超声波测距的代码逻辑是标准的,但时序要求严格。在setup()中,需要将Trig引脚设置为OUTPUT,Echo引脚设置为INPUT。
pinMode(US_TRIG_PIN, OUTPUT); pinMode(US_ECHO_PIN, INPUT);实际的测量函数通常被封装在一个循环中。关键步骤是:
- 先将Trig置低至少2微秒,确保一个稳定的起始状态。
- 拉高Trig引脚至少10微秒,这是HC-SR04要求的触发信号。
- 拉低Trig,然后立即使用
pulseIn(US_ECHO_PIN, HIGH)函数等待并测量Echo引脚高电平的持续时间。这个函数会阻塞直到检测到引脚上升沿,然后计时直到下降沿到来。 - 利用公式计算距离。这里有一个重要的细节:
pulseIn返回的单位是微秒,声速0.034的单位是厘米/微秒,除以2是因为声音走了往返路程。
实操心得:
pulseIn函数在未检测到回波时会超时等待(默认1秒),这可能导致程序卡住。在雷达扫描场景下,如果某个方向没有物体,这是正常情况。因此,最好将测距代码放在非阻塞的定时器中断中,或者使用pulseIn的超时参数(第三个参数)设置一个合理的最大等待时间(例如30000微秒,对应约5米距离)。
ToF传感器数据读取: ToF传感器通过UART通信,这比超声波的时序控制要简单,但需要处理数据协议。
UART Serial3(TOF_RX_PIN, TOF_TX_PIN, 0, 0); #define TOF Serial3 void setup() { TOF.begin(9600); // 初始化串口,波特率9600 TOF.print("s5-1#"); // 发送命令,设置为不自动上报测量值 }在测量时,我们发送读取指令r6#,然后等待传感器的回复。
TOF.print("r6#"); int count = 0; while (count++ < 5) { // 设置最大重试次数,避免死循环 String dist = TOF.readStringUntil('\n'); if (dist.startsWith("L=")) { tofDistance = dist.substring(2, dist.indexOf('m')).toInt() / 10; break; } }这段代码的健壮性处理值得学习:它通过while循环设置了最多5次读取尝试;通过readStringUntil('\n')读取一行数据;通过startsWith("L=")来筛选出有效的距离数据行;最后使用substring和toInt解析出数字。因为原始数据单位是毫米(如L=156mm),除以10得到厘米,以便和超声波数据单位统一。
3.3 舵机扫描控制与角度映射
舵机控制使用了ArduinoServo库,这极大地简化了操作。
#include <Servo.h> Servo radarServo; void setup() { radarServo.attach(SERVO_PIN); // 关联舵机信号线引脚 } void loop() { // 控制舵机从0度转到90度 for(int angle = 0; angle <= 90; angle++) { radarServo.write(angle); delay(15); // 给舵机一点时间转动到指定位置 // ... 在此位置进行传感器测量 ... } }在雷达扫描模式下,我们让舵机在0到90度(可配置)之间往复运动。每一个角度位置,都进行一次传感器测量。这样,距离数据就和角度信息绑定在了一起。这个(角度, 距离)的数据对,正是我们在雷达图上绘制一个点的极坐标。
这里有一个关键点:舵机的机械角度需要映射到雷达屏幕上的扫描角度。例如,舵机实际转动0-90度,你可能希望它在UI上显示为扫描-45度到+45度(以正前方为0度)。这个映射关系需要在发送给DumbDisplay的数据协议中体现出来。通常,我们会在代码里做一个线性变换:ui_angle = (servo_angle - 45)。这样,当舵机在45度时,UI上显示为正前方0度。
3.4 与DumbDisplay的通信协议设计
DumbDisplay应用通过串口接收特定格式的指令来更新UI。通信协议的设计原则是简洁、高效。协议通常是文本形式的,以换行符\n结尾。
例如,更新一个显示距离的标签可能发送:labelid:distLabeltext:123 cm\n其中,label是指令类型,id:distLabel指定了UI中哪个组件需要更新,text:123 cm是更新的内容。
对于雷达图,协议会更复杂一些。需要发送一个命令来清空画布,然后发送一系列点数据。可能像这样:canvasid:radarclear\ncanvasid:radardrawpointpolar:45,60color:green\n这条指令告诉id为radar的画布,在极坐标角度45度、距离60像素的位置,画一个绿色的点。
在代码中,我们需要根据当前舵机角度和测量到的距离,实时构造这些指令字符串,并通过Serial.println()发送出去。DumbDisplay应用会解析这些指令并实时渲染UI。这种将底层数据采集与上层UI显示分离的架构,使得代码逻辑清晰,也方便后期扩展更多的显示元素。
4. 系统集成调试与性能优化
4.1 多任务调度与时间管理
当系统需要同时处理传感器读取、舵机控制、数据发送和UI响应时,一个简单的loop()函数如果使用delay(),很容易导致系统反应迟钝。例如,超声波传感器测量和舵机转动都需要等待时间。为了构建一个响应灵敏的系统,我们需要采用非阻塞的编程模式。
核心思想是使用millis()函数来管理时间。millis()返回Arduino启动后的毫秒数,我们可以通过比较时间差来判断某个操作是否应该执行,而不是用delay()干等。
示例:非阻塞的超声波测距
unsigned long lastUsReadTime = 0; const unsigned long usReadInterval = 50; // 每50ms读取一次超声波 void loop() { unsigned long currentMillis = millis(); // 检查是否到了读取超声波的时间 if (currentMillis - lastUsReadTime >= usReadInterval) { lastUsReadTime = currentMillis; int distance = readUltrasonic(); // 封装好的测距函数 // ... 处理并发送距离数据 ... } // 其他任务,如检查ToF传感器、更新舵机角度等,也可以用同样的模式 // 它们彼此独立,不会互相阻塞 }对于舵机控制,我们不再使用for循环加delay来扫掠,而是根据时间计算当前的目标角度。
int sweepDirection = 1; // 1为增加角度,-1为减少角度 int currentAngle = 0; const int angleStep = 1; // 每次增加的角度 const unsigned long servoUpdateInterval = 20; // 每20ms更新一次舵机 unsigned long lastServoUpdateTime = 0; void loop() { unsigned long currentMillis = millis(); if (currentMillis - lastServoUpdateTime >= servoUpdateInterval) { lastServoUpdateTime = currentMillis; currentAngle += angleStep * sweepDirection; if (currentAngle >= 90 || currentAngle <= 0) { sweepDirection *= -1; // 到达边界时反向 } radarServo.write(currentAngle); // 在此角度进行传感器测量 performMeasurementAtAngle(currentAngle); } }这种模式下,舵机平滑运动,传感器在每一个新角度位置进行测量,而主循环还能腾出时间来处理串口指令和更新UI,整个系统显得非常流畅。
4.2 数据滤波与传感器融合初探
原始传感器数据往往带有噪声。超声波传感器容易因环境声波或测量表面特性产生跳变值;ToF传感器在极限距离或强光下也可能读数不稳。直接使用这些原始值会导致雷达图上的点剧烈抖动,影响观感和判断。
一个简单有效的软件滤波方法是移动平均滤波。它保存最近N次的测量值,并取平均值作为输出。这能有效平滑掉随机尖峰噪声。
const int numReadings = 5; int usReadings[numReadings]; // 超声波读数数组 int usReadIndex = 0; int usTotal = 0; int usAverage = 0; int smoothUltrasonicDistance(int newDistance) { // 减去最旧的读数,加上最新的读数 usTotal = usTotal - usReadings[usReadIndex] + newDistance; usReadings[usReadIndex] = newDistance; // 存储最新读数 usReadIndex = (usReadIndex + 1) % numReadings; // 循环索引 usAverage = usTotal / numReadings; return usAverage; }对于更高级的应用,你可以尝试让两个传感器协同工作,即传感器融合。一个基本的策略是:在近距离(例如30厘米内),优先信任ToF传感器,因为其精度高、盲区小;在远距离或ToF传感器失效(如返回错误值)时,采用超声波传感器的数据。你可以在代码中设置一个逻辑判断,根据当前条件选择最终使用的距离值。这能提升系统在不同环境下的鲁棒性。
4.3 DumbDisplay UI布局与交互优化
默认的UI已经提供了雷达图、波形图和按钮。但你可以通过发送不同的DumbDisplay指令来自定义UI,使其更符合你的需求。
例如,你可以改变雷达图的外观:
canvasid:radarbgcolor:black\n– 将背景设为黑色,更像传统雷达屏幕。canvasid:radardrawlinepolar:0,0topolar:0,100color:white\n– 在雷达图上画一条从中心指向0度方向的白色参考线。
你还可以增加UI控件。比如,增加一个滑块来实时调整超声波测量的频率,或者增加一个开关来选择是否启用数据滤波。这需要在Arduino代码中定义新的UI元素ID,并解析从手机端发送过来的控制指令(DumbDisplay也支持从手机向设备发送数据)。
交互优化的核心是减少不必要的数据传输。例如,只有当距离值发生变化超过某个阈值(如1厘米)时,才更新UI上的数字显示。对于雷达图,可以设置一个“持久化”时间,让点迹慢慢淡出,而不是立即消失,这样能更好地观察物体的运动轨迹。
4.4 功耗考量与便携化设想
目前系统通过手机USB供电,对于Pico、两个传感器和一个微型舵机来说,手机的电池足以支撑数小时的连续运行。但如果考虑更长时间的户外使用或电池供电,就需要进行功耗优化。
- 降低工作频率:在不影响体验的前提下,降低传感器采样率和舵机更新频率。例如,将扫描范围从90度减小到60度,扫描速度放慢。
- 睡眠模式:当没有检测任务时,可以让Pico进入深度睡眠模式,仅由外部中断(如一个按键)唤醒。这需要更复杂的电路和程序设计。
- 选择性供电:可以通过一个MOSFET管,用Pico的GPIO控制给传感器和舵机的5V电源通断。在不需要测量时,彻底切断它们的电源。
一个有趣的便携化设想是使用一块小容量的锂电池(如18650)配合充电管理模块为整个系统供电,并将Pico通过蓝牙模块与手机连接,摆脱USB线的束缚,成为一个真正的无线便携雷达探测器。
5. 常见问题排查与进阶玩法
5.1 硬件连接与电源问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 上电后无任何反应,手机不识别串口 | 1. USB线仅能充电,无数据传输功能。 2. Pico损坏或Bootloader模式。 3. 电源短路。 | 1. 更换为已知可传输数据的USB线。 2. 按住Pico上的BOOTSEL按钮再上电,看是否出现U盘。重新烧录固件。 3. 断开所有外设,仅连接Pico与电脑,检查是否有串口。逐步连接外设,定位短路点。 |
| 超声波传感器始终返回0或极大值 | 1. 接线错误,Trig和Echo接反。 2. 电源电压不足(低于4.5V)。 3. 物体超出测量范围或表面不反射声波。 4. 环境噪声干扰。 | 1. 确认Trig接GPIO输出,Echo接GPIO输入。 2. 用万用表测量VCC引脚电压,确保在5V左右。检查VBUS连接。 3. 在传感器正前方20cm处放置一个平整硬物测试。 4. 尝试在代码中增加 digitalWrite(US_TRIG_PIN, LOW); delay(100);后再触发,避开自身余震。 |
| ToF传感器无数据返回 | 1. UART接线Tx/Rx接反。 2. 波特率不匹配。 3. 传感器未正确初始化。 | 1. 确认传感器的TxD接Pico的RX(GP13),RxD接Pico的TX(GP12)。 2. 确保代码中 TOF.begin(9600);与传感器规格一致。3. 确认初始化命令 TOF.print("s5-1#");已成功发送。可在发送后添加delay(100)。 |
| 舵机抖动或不转动 | 1. 电源功率不足,特别是转动瞬间。 2. 信号线接触不良。 3. 舵机损坏或角度超出范围。 | 1. 在VBUS和GND间并联一个大电容(100-470μF)。尝试用独立5V电源给舵机供电,并与Pico共地。 2. 重新插拔信号线,或更换杜邦线。 3. 用 servo.write(90)测试,缓慢改变角度值,看是否在特定位置卡住。 |
| DumbDisplay连接成功但无数据显示 | 1. Arduino代码中串口波特率与DumbDisplay设置不匹配。 2. 数据协议格式错误。 3. UI图层ID不匹配。 | 1. 检查Arduino代码中Serial.begin(115200)与DumbDisplay App连接设置的波特率是否相同。2. 使用串口监视器(如PlatformIO的Serial Monitor)查看Pico实际发出的数据,与DumbDisplay协议手册对比。 3. 确认代码中发送指令指定的UI组件ID(如 id:radar)与DumbDisplay App中创建的图层ID完全一致。 |
5.2 软件与数据异常调试技巧
- 串口调试是王道:在代码关键位置(如进入测距函数、收到有效数据、发送UI指令前)添加
Serial.print语句,输出变量值或状态标志。这是定位逻辑错误最直接的方法。 - 隔离测试:先将系统拆解。单独编写一个只测试超声波传感器的程序,确认其工作正常。再单独测试ToF传感器和舵机。最后再将它们整合到一起,并加入DumbDisplay通信。分而治之可以快速定位问题模块。
- 检查变量溢出:
pulseIn返回的duration是long类型,但在计算duration * 0.034时,如果距离很远(duration很大),乘法可能导致中间结果溢出。确保使用浮点数计算或调整公式顺序。 - 注意全局变量冲突:在中断服务函数中修改的全局变量,在主循环中读取时,如果该变量大于MCU的原子操作长度(如8位MCU上的
int),可能会读到不完整的值。可以考虑暂时关闭中断进行读取,或者使用volatile关键字声明。
5.3 项目扩展与进阶思路
这个基础框架有巨大的扩展潜力:
- 多传感器阵列:使用多个超声波传感器,固定在不同角度,无需舵机即可实现更广范围的瞬时探测。Pico有足够的GPIO来驱动多个HC-SR04。
- 数据记录与回放:在Pico上挂载一个微型SD卡模块,将扫描到的(角度,距离,时间戳)数据记录到CSV文件中。之后可以导入电脑进行更深入的分析,或实现轨迹回放。
- 物体追踪与预警:在软件算法中加入简单逻辑。连续几次扫描在同一区域发现物体,则判定为静止物体;如果物体坐标连续变化,则计算其移动速度和方向,并通过DumbDisplay发出图形或声音预警。
- 三维扫描:增加一个垂直方向的舵机,将传感器安装在由两个舵机组成的云台上,实现水平和垂直两个维度的扫描,从而获取三维点云数据。这需要更复杂的坐标变换和UI显示。
- 更换核心板:尝试将主控换成ESP32。利用其Wi-Fi功能,可以将雷达数据以WebSocket形式发送到局域网内的任何设备(电脑、平板、手机)上的浏览器进行显示,彻底摆脱USB线的限制。
这个项目的魅力在于,它从一个简单的想法出发,串联起了嵌入式开发中硬件接口、传感器原理、实时控制、数据通信和可视化等多个核心环节。完成它,你收获的不仅仅是一个会转的雷达模型,更是一套解决实际问题的嵌入式系统开发方法论。
