
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#它会返回如L156mm格式的字符串。其优点是精度高毫米级、波束角非常小指向性强、响应速度快且不易受环境声波干扰。缺点是成本较高强光直射下可能失效对透明物体如玻璃或极度吸光的黑色绒布测量可能不准。在实际搭建中让这两个传感器一起工作非常有价值。你可以通过UI按钮实时切换直观对比两者在不同材质、不同距离、不同环境光下的测量差异。这是一种非常有效的传感器特性学习方式。2.3 动力与可视化舵机与DumbDisplaySG90舵机是一个位置伺服电机。它通过接收周期为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_PINGP12实际是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引脚设置为OUTPUTEcho引脚设置为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解析出数字。因为原始数据单位是毫米如L156mm除以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的RXGP13RxD接Pico的TXGP12。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线的限制。这个项目的魅力在于它从一个简单的想法出发串联起了嵌入式开发中硬件接口、传感器原理、实时控制、数据通信和可视化等多个核心环节。完成它你收获的不仅仅是一个会转的雷达模型更是一套解决实际问题的嵌入式系统开发方法论。