拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Arduino与Android蓝牙串口通信实现超声波雷达扫描系统

1. 项目概述当Arduino遇见Android一个“声呐”项目的诞生几年前我在一个创客空间里看到有人用超声波传感器和舵机做了一个简单的雷达扫描装置屏幕上实时显示着障碍物的距离和角度。当时就觉得这玩意儿挺酷但总得连着电脑屏幕看不够“移动”。后来随着手头的Android设备越来越多一个想法冒了出来能不能把Arduino的感知能力和Android的图形显示、计算能力结合起来做一个可以拿在手里、实时查看周围环境“声呐图”的小玩意儿这就是“ArduSonar”这个项目最初的念头。简单来说ArduSonar是一个结合了Arduino硬件和Android手机应用的简易声呐/雷达模拟系统。它的核心逻辑是Arduino板比如经典的Uno连接一个超声波传感器如HC-SR04和一个舵机。舵机带动传感器水平旋转每旋转一个角度传感器就测量一次前方障碍物的距离。然后Arduino将这些“角度-距离”数据通过蓝牙或USB串口发送到Android手机上。手机上的App通常用Android Studio开发接收到数据后将其转换为极坐标点再实时绘制成类似雷达扫描的扇形图或点状图直观地展示出周围物体的轮廓和位置。这听起来像是两个不同世界的技术硬凑在一起但实际做下来你会发现它完美地诠释了“软硬结合”的乐趣。Arduino负责稳定、可靠地采集物理世界的数据而Android则提供了强大的数据处理、图形渲染和交互界面。对于硬件爱好者这是一个学习传感器控制、串口通信和舵机精度的好项目对于移动开发者这是一个接触硬件通信、自定义视图绘制和实时数据处理的绝佳切入点。无论你是想做一个智能小车的避障感知模块还是一个探索性质的桌面玩具ArduSonar都能提供一个清晰、完整的实现路径。2. 核心硬件选型与电路搭建从传感器到通信模块一个ArduSonar系统硬件部分是它的“感官”和“神经”。这部分的选择和搭建直接决定了最终数据的准确性和系统的稳定性。很多人一开始会纠结于用哪款Arduino、哪种传感器其实对于这个项目选择的标准很明确够用、稳定、易得。2.1 Arduino主控板Uno的经典与ESP32的进阶最直接的选择是Arduino Uno。它几乎是所有创客的入门板资源丰富社区支持强大。对于驱动一个舵机和一个超声波传感器Uno的5V/3.3V输出和数字I/O口完全够用。它的ATmega328P芯片处理简单的角度步进、距离计算和串口发送绰绰有余。使用Uno的好处是几乎所有教程和库都以其为范本排查问题非常方便。但如果你希望系统更“无线化”、更独立那么ESP32系列开发板是更优的选择。以ESP32 DevKit为例它集成了Wi-Fi和蓝牙功能这意味着你可以直接通过蓝牙与手机通信省去了额外的蓝牙模块简化了电路。同时ESP32拥有更强大的双核处理器和更多的内存为未来扩展功能比如加入更多传感器、本地数据滤波算法留足了空间。在Arduino IDE中通过安装ESP32开发板支持包你就可以像使用普通Arduino一样对它进行编程学习曲线平缓。注意如果使用ESP32需要注意其工作电压是3.3V而常见的舵机如SG90和控制信号虽然是5V但通常3.3V的PWM信号也能驱动为了稳妥起见可以在信号线之间加一个电平转换模块或者选择支持3.3V逻辑的舵机。2.2 感知与执行单元HC-SR04与SG90舵机HC-SR04超声波传感器是这个项目的“眼睛”。它价格低廉、使用简单通过发送40kHz的超声波并接收回波通过时间差计算距离。其有效测距范围在2cm到400cm之间精度对于室内环境探测完全足够。接线也简单Vcc接5VGnd接GndTrig触发和Echo回响各接一个数字引脚。SG90微型舵机则是系统的“脖子”负责带动传感器旋转。SG90体积小、扭矩适中价格便宜是DIY项目的常客。它有三根线电源红接5V、地线棕接Gnd和控制信号线橙接一个支持PWM的数字引脚如Arduino Uno的9号引脚。舵机通过接收PWM信号的脉冲宽度来控制旋转角度通常脉冲宽度在500μs到2500μs之间对应0到180度的角度。2.3 通信桥梁蓝牙模块HC-05/06或USB-OTG要让Arduino和Android手机对话你需要一个通信渠道。有两种主流方案蓝牙串口模块如HC-05或HC-06这是最无线、最灵活的方案。HC-05功能更强支持主从模式HC-06更便宜仅从模式。将它们与Arduino连接Vcc-5V Gnd-Gnd TXD-RX RXD-TX配置好波特率如9600或115200手机就能像连接一个无线串口一样与之通信。在Android端你需要使用蓝牙API来发现、配对和建立Socket连接进而读取数据流。USB-OTG数据线这是一种有线方案但极其稳定无需配对且可以同时为Arduino供电。你需要一根USB-OTG线将手机与Arduino的USB口通过Uno的USB口或FTDI芯片连接。在Android端这需要用到USB Host API并安装一个串口通信库如usb-serial-for-android。这种方案延迟极低数据传输可靠适合对实时性要求高的场景但会占用手机的充电口。电路连接示意图以Arduino Uno HC-05为例Arduino 5V - HC-05 VCC、舵机红、HC-SR04 VccArduino GND - HC-05 GND、舵机棕、HC-SR04 GndArduino Pin 9PWM - 舵机橙信号Arduino Pin 10 - HC-SR04 TrigArduino Pin 11 - HC-SR04 EchoArduino Pin 0 (RX) - HC-05 TXDArduino Pin 1 (TX) - HC-05 RXD实操心得在连接HC-05时务必注意Arduino的RX/TX引脚0和1也用于通过USB与电脑通信。因此在上传程序时最好先将HC-05的TXD/RXD线断开否则可能因串口冲突导致上传失败。程序上传成功后再接回去。3. Arduino端固件开发数据采集与串口协议设计硬件搭好了接下来是让Arduino“动起来”的大脑——固件程序。这段代码的核心任务有三个控制舵机匀速扫描、触发超声波测距、将数据打包并通过串口发送出去。代码本身不复杂但细节决定成败。3.1 舵机扫描控制平滑与精度使用Arduino IDE我们首先需要导入舵机库#include Servo.h。创建一个Servo对象并绑定到对应的引脚。扫描逻辑通常是在loop()函数中让舵机角度从0度逐步增加到180度再逐步减小回来实现来回扫描。一个常见的误区是使用delay()函数来控制扫描速度。例如for(int angle 0; angle 180; angle 1) { myServo.write(angle); delay(15); // 等待舵机转到指定位置 measureDistance(angle); // 测量距离 }这样做的问题是delay()会阻塞整个程序在等待舵机转动期间单片机什么也做不了影响实时性。更好的方法是采用非阻塞式定时。利用millis()函数记录时间戳在达到间隔时间后再执行角度递增和测距这样在等待期间MCU可以处理其他任务虽然本项目任务简单但养成好习惯很重要。unsigned long previousScanMillis 0; const long scanInterval 20; // 扫描间隔单位毫秒 int currentAngle 0; int scanDirection 1; // 1为增-1为减 void loop() { unsigned long currentMillis millis(); if (currentMillis - previousScanMillis scanInterval) { previousScanMillis currentMillis; // 更新角度 currentAngle scanDirection; if (currentAngle 180 || currentAngle 0) { scanDirection * -1; // 到达边界后反向 } myServo.write(currentAngle); // 测量该角度下的距离 int distance measureDistance(); // 发送数据 sendData(currentAngle, distance); } }3.2 超声波测距滤波与错误处理HC-SR04的测距原理很简单给Trig引脚一个至少10μs的高电平脉冲触发测距然后监听Echo引脚的高电平持续时间这个时间即超声波往返时间。距离 (高电平时间 * 声速) / 2。但在实际环境中会遇到回波丢失物体太远或吸收太强、偶然干扰其他超声波源等问题导致测距失败或出现极大/极小的异常值。因此加入简单的软件滤波至关重要。一个实用的方法是连续采样多次去掉最大最小值然后取平均。例如连续测5次排序后取中间3次的平均值。这能有效滤除偶然的跳变。long measureDistance() { long duration; long distance; long samples[5]; for(int i0; i5; i){ digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); duration pulseIn(echoPin, HIGH, 30000); // 设置超时30ms对应约5米 samples[i] duration * 0.034 / 2; // 计算距离单位厘米 delay(10); // 两次测量间短暂间隔 } // 简单的排序冒泡即可 for(int i0; i4; i){ for(int ji1; j5; j){ if(samples[j] samples[i]){ long temp samples[i]; samples[i] samples[j]; samples[j] temp; } } } // 取中值平均 distance (samples[1] samples[2] samples[3]) / 3; // 超范围处理 if(distance 400 || distance 2){ distance 0; // 或一个特殊值如-1表示无效数据 } return distance; }pulseIn函数的超时参数很关键它避免了因为未收到回波而导致的程序长时间挂起。3.3 串口数据协议定义Android与Arduino的“语言”原始数据角度和距离需要以一种双方都能理解的格式发送。最常用、最易解析的格式是字符串协议。例如我们可以定义每帧数据为A:角度,D:距离\n。void sendData(int angle, long distance) { Serial.print(A:); Serial.print(angle); Serial.print(,D:); Serial.print(distance); Serial.println(); // 发送换行符作为帧结束标志 }在Android端就可以按行读取然后根据“A:”和“,D:”这样的分隔符来解析出角度和距离值。务必在每帧数据末尾加上换行符\n这是区分数据帧最简便的方法。避坑指南串口通信的波特率必须一致Arduino端Serial.begin(9600)Android端连接时也要设置为9600。如果使用HC-05蓝牙模块其默认波特率通常是9600或38400最好通过AT命令将其设置为与代码一致的波特率以避免乱码。另外发送数据不要太快要给手机端留出解析和处理的时间间隔20-50ms发送一帧是合理的。4. Android端应用开发从数据接收到动态可视化这是项目的“面子工程”也是用户体验的核心。我们需要开发一个Android应用其核心功能包括建立与硬件的通信连接、实时接收并解析数据、将数据动态绘制成雷达图。开发环境自然是Android Studio。4.1 通信层实现蓝牙串口连接详解如果使用蓝牙方案首先需要在AndroidManifest.xml中声明蓝牙权限uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- 蓝牙扫描需要位置权限 --对于Android 6.0以上还需要在运行时动态申请位置权限。连接流程大致如下发现设备通过BluetoothAdapter获取本地适配器然后开始搜索或获取已配对设备列表。建立连接找到目标HC-05设备通常名称就是HC-05后通过其MAC地址创建BluetoothSocket。这里需要使用一个特定的UUID通用唯一标识符来创建RFCOMM通道最常用的是00001101-0000-1000-8000-00805F9B34FB这是蓝牙串口服务的标准UUID。数据读写连接成功后通过socket.getInputStream()和getOutputStream()获取输入输出流就可以像操作文件流一样读写数据了。关键代码片段示例// Kotlin 示例Java逻辑类似 val adapter: BluetoothAdapter? BluetoothAdapter.getDefaultAdapter() val device: BluetoothDevice ... // 从已配对列表或扫描结果中获取 val uuid UUID.fromString(00001101-0000-1000-8000-00805F9B34FB) val socket device.createRfcommSocketToServiceRecord(uuid) socket.connect() val inputStream socket.inputStream val outputStream socket.outputStream // 启动一个线程来持续读取数据 Thread { val buffer ByteArray(1024) var bytes: Int while (true) { bytes inputStream.read(buffer) val data String(buffer, 0, bytes, Charsets.UTF_8) // 将数据传递给主线程进行解析和UI更新 runOnUiThread { parseAndUpdateUI(data) } } }.start()重要提示网络蓝牙操作必须在子线程中进行绝对不能放在主线程UI线程否则会导致应用无响应ANR。读取数据通常在一个独立的Thread或Coroutine中进行然后将解析后的数据通过Handler或LiveData等方式发送到主线程更新UI。4.2 数据解析与处理字符串协议的拆解从串口读取到的是一串字节流我们需要将其转换为字符串并按帧解析。由于我们定义了以换行符\n为帧分隔符所以常见的做法是使用一个缓冲区StringBuilder来累积数据一旦遇到换行符就处理一整帧。private val dataBuffer StringBuilder() private fun parseIncomingData(newData: String) { dataBuffer.append(newData) val bufferString dataBuffer.toString() val lines bufferString.split(\n) // 如果最后一行不是完整的一行没有\n则放回缓冲区 if (bufferString.lastOrNull() ! \n) { dataBuffer.clear() dataBuffer.append(lines.last()) } else { dataBuffer.clear() } // 处理所有完整的行 for (line in lines.dropLast(1)) { // 去掉最后可能不完整的行 if (line.isNotBlank()) { // 解析 A:120,D:45 val angleMatch Regex(A:(\\d)).find(line) val distMatch Regex(,D:(\\d)).find(line) val angle angleMatch?.groupValues?.get(1)?.toIntOrNull() ?: continue val distance distMatch?.groupValues?.get(1)?.toIntOrNull() ?: continue // 将有效数据传递给雷达视图进行绘制 radarView.addDataPoint(angle, distance) } } }这里使用了正则表达式来匹配和提取数字简单高效。记得做好异常处理toIntOrNull因为通信中可能会有干扰导致数据格式错误。4.3 雷达视图自定义绘制让数据“看得见”这是最具成就感的部分。我们需要自定义一个View继承自View或SurfaceView在其onDraw(canvas: Canvas)方法中将接收到的极坐标数据角度、距离转换为屏幕上的直角坐标x, y并绘制出来。绘制步骤绘制背景画一个扇形或圆形区域作为扫描范围画上同心圆作为距离刻度线例如每50厘米一个圈画上角度射线如每30度一条。绘制扫描线根据当前舵机的角度画一条从圆心出发的射线模拟雷达的实时扫描。绘制目标点对于解析出的每一个有效数据点角度距离将其转换为屏幕坐标。转换公式x 圆心x distance * cos(angle_radian)y 圆心y distance * sin(angle_radian)。注意数学库中的三角函数使用弧度制需要将角度转换为弧度radian angle * Math.PI / 180。另外屏幕坐标系Y轴向下为正与数学坐标系相反可能需要调整正负。动态效果为了让扫描线动起来需要定期调用invalidate()方法请求重绘视图。可以在一个定时器或在新数据到达时触发。绘制目标点时可以加入淡出效果通过改变绘制点的颜色透明度或半径让旧的点逐渐消失形成“历史轨迹”。性能考虑如果数据更新很快比如每秒几十帧频繁重绘整个视图可能卡顿。可以考虑以下优化使用SurfaceView代替普通View它拥有独立的绘制线程。只重绘发生变化的部分脏矩形但在这个全屏动态的场景下优化有限。确保在子线程中进行数据解析和坐标计算只在主线程中调用invalidate()和进行简单的绘制操作。// 自定义RadarView的onDraw方法简化示例 override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val centerX width / 2f val centerY height / 2f val maxRadius Math.min(centerX, centerY) * 0.9f // 最大绘制半径 // 1. 绘制背景网格和刻度 drawBackground(canvas, centerX, centerY, maxRadius) // 2. 绘制扫描线 val sweepAngle currentScanAngle // 当前角度 canvas.drawLine(centerX, centerY, centerX maxRadius * Math.cos(Math.toRadians(sweepAngle.toDouble())).toFloat(), centerY maxRadius * Math.sin(Math.toRadians(sweepAngle.toDouble())).toFloat()), scanLinePaint) // 3. 绘制所有目标点 synchronized(dataPoints) { // 数据点列表需要线程安全 for (point in dataPoints) { val plotRadius maxRadius * (point.distance / MAX_DETECT_DISTANCE) // 按比例缩放 val x centerX plotRadius * Math.cos(Math.toRadians(point.angle.toDouble())).toFloat() val y centerY plotRadius * Math.sin(Math.toRadians(point.angle.toDouble())).toFloat() canvas.drawCircle(x, y, TARGET_DOT_RADIUS, targetPaint) } } }5. 系统集成、调试与性能优化实战当硬件固件和手机App都单独调通后真正的挑战才开始让它们稳定地协同工作。这个阶段会遇到各种各样意料之外的问题也是积累经验的关键。5.1 联调常见问题与排查链路问题一手机App搜索不到蓝牙设备HC-05。排查步骤确认硬件检查HC-05模块是否已通电LED应闪烁。尝试用手机系统蓝牙设置直接搜索“HC-05”并进行配对默认配对码常为1234或0000。如果系统蓝牙都搜不到可能是模块损坏或供电不足。检查权限确保App已获得定位精确定位权限。这是Android 6.0后蓝牙扫描的强制要求很多新手会在这里卡住。检查代码确认在扫描前已调用BluetoothAdapter.startDiscovery()并正确注册了BroadcastReceiver来接收BluetoothDevice.ACTION_FOUND意图。问题二连接建立后App收不到任何数据或收到乱码。排查步骤确认波特率这是最常见的原因。用Arduino IDE的串口监视器设置与代码中Serial.begin()相同的波特率如9600看是否能正常打印数据。如果能说明Arduino端正常。然后检查Android端建立BluetoothSocket时使用的串口UUID是否正确以及读写流时是否使用了相同的字符集如UTF-8。检查接线确认HC-05的TXD是否接到了Arduino的RX0号引脚RXD接到了TX1号引脚。接反了数据无法传输。添加日志在Android端数据读取线程中将收到的原始字节数组以十六进制格式打印出来看是否接收到任何内容。如果收到的是非预期内容可能是协议不匹配或干扰。流量控制尝试在Arduino端发送数据后加一个小的延时delay(20)避免发送过快导致手机端缓冲区溢出。问题三雷达图绘制卡顿、闪烁或刷新不及时。排查步骤检查UI线程确保耗时的数据解析、坐标计算操作不在主线程进行。使用AsyncTask、Thread或协程在后台处理仅将最终用于绘制的数据列表通过线程安全的方式如Handler传递给主线程。优化绘制检查onDraw方法中是否进行了不必要的对象创建如new Paint()这些操作应提前初始化。考虑使用SurfaceView获得更流畅的动画效果。控制刷新率不要每收到一帧数据就立即invalidate()。可以设置一个固定的刷新频率如每秒30帧用一个定时器来触发重绘或者累积一定数量的数据点后再统一更新一次UI。5.2 数据稳定性优化软件滤波与校准原始超声波数据是充满噪声的。除了在Arduino端做的多次采样平均在Android端还可以进行进一步的软件滤波让雷达图上的点更稳定。滑动平均滤波为每个角度方向维护一个小型的历史距离队列比如最近5次测量值每次更新时取平均值。这能有效平滑数据但会引入一定的延迟。val historyMap mutableMapOfInt, ArrayDequeInt() // 角度 - 历史距离队列 fun updateFilteredData(angle: Int, newDistance: Int) { val queue historyMap.getOrPut(angle) { ArrayDeque() } queue.addLast(newDistance) if (queue.size 5) { queue.removeFirst() } val filteredDistance queue.average().toInt() // 使用filteredDistance进行绘制 }阈值过滤设置一个最大可信距离如400cm和最小可信距离2cm超出范围的数据直接丢弃。对于连续出现的剧烈跳变如从30cm突然跳到300cm又跳回来可以判断为干扰而忽略。舵机角度校准舵机可能存在死区或非线性。可以在代码中建立一个“角度-脉冲宽度”的查找表或者通过实验记录下指令角度与实际指向角度比如对准一个固定标记的偏差在发送数据前进行补偿。5.3 功能扩展与进阶思路一个基础的ArduSonar完成后你可以从多个方向进行扩展让它变得更强大多传感器融合在舵机上安装多个朝向不同的超声波传感器或者增加一个红外测距传感器作为互补超声波对柔软物体和复杂表面测不准红外对强光和黑色物体测不准通过数据融合算法得到更可靠的距离信息。三维扫描使用两个舵机一个控制水平旋转一个控制俯仰让传感器能做二维扫描从而在手机App上生成三维点云图或深度图。这对硬件控制和手机端渲染提出了更高要求。数据记录与回放在App中增加功能将扫描到的数据角度、距离、时间戳记录到手机存储中后续可以像播放视频一样回放整个扫描过程用于分析。网络传输与云端如果使用ESP32可以将其连接到Wi-Fi将数据通过MQTT或HTTP协议发送到云端服务器。这样你就可以通过网页或其他手机远程查看雷达图实现物联网应用。障碍物识别与预警在手机端或ESP32上运行简单的算法对连续扫描的数据进行分析识别出可能是墙壁、家具或移动物体的轮廓并当物体进入预设的“危险区域”时发出声音或振动警报。从点亮第一个LED到完成一个软硬件结合的可视化项目ArduSonar带给你的远不止一个会转的雷达图。它是一条清晰的路径让你亲手实践了从信号采集、数据处理、无线通信到移动应用开发的全链路。过程中遇到的每一个坑解决的每一个问题都是实实在在的经验。当你第一次在手机上看到来自真实世界的距离数据被实时绘制出来时那种连接虚拟与现实的成就感正是创客项目的魅力所在。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门