1. 项目概述:在桌面上复现雷达扫描的乐趣
几年前,我在一个创客展上第一次看到有人用Arduino和超声波传感器做了一个简单的距离显示器,当时就觉得这玩意儿挺酷,但总觉得少了点“味道”——它只是把数字显示在屏幕上,不够直观,也缺乏那种科技设备特有的交互感。后来,我偶然看到一些关于雷达原理的科普,尤其是那种经典的PPI(平面位置指示器)扫描画面,一个旋转的亮线扫过圆形屏幕,遇到障碍物就出现一个光点,距离和方位一目了然。我就在想,能不能用手里最便宜的Arduino开发板和一块常见的TFT彩屏,把这种经典的雷达扫描效果给复现出来呢?
这个“DIY Arduino超声波声呐——TFT显示屏上的‘雷达’”项目,就是为了实现这个想法。它本质上是一个基于超声波测距原理的、具有可视化雷达式扫描界面的近距离障碍物探测系统。核心是利用Arduino读取超声波传感器(如HC-SR04)测量的距离数据,然后通过编程,在TFT屏幕上动态绘制出一个模拟雷达扫描的画面。当传感器旋转扫描时,屏幕上的“扫描线”同步旋转,并将探测到的物体位置(角度和距离)以光点或弧线的形式实时标记在屏幕上。
它能做什么?首先,它是个非常棒的教学演示项目,可以直观地展示超声波测距、极坐标转换、实时图形绘制等嵌入式开发和基础电子学的概念。其次,它具备一定的实用价值,比如可以作为一个桌面级的简易安防监控前端,监测一定扇形区域内的闯入物体;或者作为小型机器人、智能小车的“前视声呐”,帮助感知前方环境。最重要的是,它把枯燥的数据变成了酷炫的图形,这个过程本身充满了创造的乐趣。
无论你是刚接触Arduino和电子制作的新手,想找一个既有成就感又能学到东西的进阶项目,还是有一定经验的爱好者,希望探索单片机图形界面和传感器数据融合的玩法,这个项目都非常适合。你不需要很深的数学或编程功底,我会把每一步的原理和代码都掰开揉碎了讲清楚。接下来,我们就从零开始,一步步把这个桌面“雷达”造出来。
2. 核心硬件选型与电路设计解析
工欲善其事,必先利其器。这个项目的硬件部分可以拆解为三个核心模块:主控单元、感知单元和显示单元。选型的原则是在满足功能、保证稳定性的前提下,尽可能追求高性价比和易用性。
2.1 主控芯片:为何首选Arduino Uno?
对于这个项目,Arduino Uno R3几乎是毋庸置疑的首选。原因有三点:生态、性能和引脚。
首先,生态成熟度无敌。Uno基于ATmega328P单片机,几乎所有为Arduino设计的传感器库、显示库都能完美兼容。你在项目中遇到的99%的问题,都能在网上找到现成的解决方案或讨论。这对于降低学习门槛、快速排错至关重要。
其次,性能恰到好处。ATmega328P运行在16MHz,拥有2KB SRAM和32KB Flash。驱动一个分辨率在240x320左右的TFT屏并进行实时图形绘制,虽然有些压力,但通过精心优化的代码(比如避免全局刷新、使用低层绘图函数)是完全可行的。它就像一台老式但可靠的“小卡车”,拉我们这个项目的“货”绰绰有余。
最后,引脚布局合理。Uuno提供了20个可用的数字和模拟I/O口,足够我们连接传感器、屏幕,甚至未来扩展蜂鸣器或LED指示灯。特别是其标准的ICSP接口,对于烧录引导程序或与某些模块通信也很方便。
注意:虽然像ESP32这样的板子性能更强、还带Wi-Fi,但对于初次尝试图形化项目的朋友,其复杂的双核架构和有时不太稳定的库支持,反而可能带来不必要的麻烦。Uno的“简单可靠”是这个项目初期成功的关键。
2.2 感知单元:HC-SR04超声波传感器工作原理与局限
我们项目的“眼睛”是HC-SR04超声波测距模块。它的原理很简单:触发引脚(Trig)收到一个至少10微秒的高电平脉冲后,模块会自动发射8个40kHz的超声波脉冲,然后监听回波。当接收到回波时,回声引脚(Echo)会输出一个高电平脉冲,其持续时间与超声波往返的时间成正比。
计算距离的公式是:距离(厘米) = (高电平时间 * 声速) / 2。在空气中,声速受温度影响,常温(20°C)下约为343米/秒,折算过来就是高电平时间(微秒) / 58.0厘米,或者高电平时间(微秒) / 148.0英寸。在代码中,我们通常用这个简化公式。
然而,HC-SR04有它的局限性:
- 测量角度:大约15度,不是一个点,而是一个锥形区域。这会导致雷达扫描图上显示的光点比实际物体看起来“胖”。
- 最小盲区:约2-3厘米,太近的物体测不准。
- 最大量程:官方标称4米,但在实际使用中,对于较小的物体或非理想反射面,有效距离通常在1.5-2米左右。
- 干扰问题:多个超声波模块同时工作,或环境中有类似频率的噪声,可能导致误触发。
在雷达扫描场景下,我们还需要让它“转动”起来。这里有两种方案:
- 方案A:舵机扫描:将HC-SR04固定在一个SG90或MG996R这类舵机的舵盘上。通过Arduino控制舵机角度,实现0-180度(或更小范围)的周期性扫描。这是最直观、效果最好的方案,能真实模拟雷达天线的机械旋转。
- 方案B:多传感器阵列:如果不想用活动部件,可以使用3-5个HC-SR04,以一定角度(如30度或45度)间隔固定排列,同时或分时读取数据,合成一个扇形视野。这属于电子扫描,成本略高,编程逻辑稍复杂。
为了最佳的演示效果,本教程将采用方案A:舵机扫描。
2.3 显示单元:选择适合Arduino的TFT屏幕
TFT(薄膜晶体管)彩屏是我们的“雷达显示屏”。市场上常见的有基于ILI9341、ST7735、ILI9488等驱动芯片的屏幕。对于Arduino Uno,我强烈推荐使用1.44寸或1.8寸,分辨率128x128或128x160,驱动芯片为ST7735的TFT屏。
为什么?
- 分辨率与性能平衡:128x160的分辨率对于雷达扫描界面来说足够清晰,同时像素点数量(约2万个)远低于240x320(约7.7万个)。这意味着刷新一屏数据所需的计算量和时间更少,在Uno上能获得更流畅的动画效果,避免严重的闪烁。
- 库支持完善:Adafruit的
Adafruit_ST7735库和Adafruit_GFX核心图形库对这类屏幕的支持非常成熟且高效。 - 接口简单:通常采用SPI接口,只需要连接Arduino的5-6根线(VCC, GND, SCLK, MOSI, CS, DC,有时还有RST和背光),比并口屏节省大量I/O口。
- 功耗较低:小尺寸屏幕功耗相对较低,可由Arduino板载的5V或3.3V直接驱动。
如果你手头只有更大的ILI9341屏(比如2.4寸,240x320),也能用,但需要在代码中更注意优化,比如只刷新扫描线附近区域,否则帧率会很低。
2.4 电路连接与供电考量
整个系统的接线图如下表所示:
| 元件 | 引脚 | 连接至 Arduino Uno 引脚 | 说明 |
|---|---|---|---|
| HC-SR04 | VCC | 5V | |
| Trig | 数字引脚 9 | 触发信号输出 | |
| Echo | 数字引脚 10 | 回波信号输入 | |
| GND | GND | ||
| SG90 舵机 | 红色线 (VCC) | 5V | 注意:最好接外部5V电源 |
| 棕色线 (GND) | GND | ||
| 橙色线 (信号) | 数字引脚 6 | PWM控制信号 | |
| ST7735 TFT | VCC | 5V | |
| GND | GND | ||
| SCLK | 数字引脚 13 (SCK) | SPI时钟 | |
| MOSI | 数字引脚 11 (MOSI) | SPI数据输入 | |
| CS | 数字引脚 7 | 片选,可自定义 | |
| DC (或 A0) | 数字引脚 8 | 数据/命令选择,可自定义 | |
| RST | 数字引脚 9 或 接VCC | 复位,可接固定高电平或控制 | |
| LED | 3.3V 或 通过电阻接5V | 背光控制,常亮可接3.3V |
实操心得:供电是坑!舵机在转动,尤其是堵转时,瞬间电流可能高达500mA-1A。Arduino Uno的板载5V稳压芯片可能无法提供如此大的电流,导致整个系统电压被拉低,Arduino复位或屏幕闪烁。最稳妥的方案是使用一个外部的5V/2A直流电源适配器,同时给Arduino的电源插座和舵机供电。或者,至少将舵机的VCC和GND接到一个独立的5V电源上,并与Arduino共地。
3. 软件架构与核心算法剖析
硬件搭好了,接下来就是让它们“活”起来的软件部分。整个程序的逻辑可以概括为:在一个循环中,控制舵机转到某个角度,然后触发超声波测量该方向的距离,接着将这个极坐标(角度,距离)下的障碍物信息,转换到屏幕的直角坐标系(x, y)上并绘制出来,同时擦除上一帧的旧扫描线,绘制新扫描线。
3.1 雷达扫描的核心:极坐标到直角坐标的转换
这是本项目最核心的数学点。超声波传感器告诉我们:距离 = r,舵机告诉我们:当前角度 = θ(以度为单位,并换算为弧度制)。而TFT屏幕是标准的笛卡尔直角坐标系,原点在左上角,x轴向右,y轴向下。
我们要模拟的雷达PPI显示,通常将屏幕中心设为雷达所在点(即我们自己)。因此,需要进行坐标变换:
- 将屏幕物理中心设为原点
(center_x, center_y)。 - 雷达扫描的有效范围(例如0-150厘米)需要映射到屏幕的半径上(例如0-60个像素)。
- 根据公式进行转换:
x = center_x + (r_pixels * cos(θ_radians))y = center_y - (r_pixels * sin(θ_radians))// 注意是“减”号,因为屏幕y轴向下。
这里,r_pixels = map(r_cm, 0, MAX_RANGE_CM, 0, RADIUS_PIXELS), 将实际距离映射为像素距离。θ_radians = radians(θ_degrees), 将角度转换为弧度。
为什么用map函数而不是简单比例?因为超声波测量有最小和最大有效范围。我们需要将这段有效距离(如2cm-150cm)线性映射到屏幕上的有效半径区域(如10-60像素),而map()函数完美实现了这个线性映射,并可以处理超范围的值。
3.2 显示优化:局部刷新与双缓冲思想
在Uno这种性能有限的平台上,全屏刷新(每次画线都重绘整个背景和所有目标)会导致严重的闪烁和极低的帧率,体验极差。我们必须采用局部刷新策略。
策略一:只更新变化的区域——扫描线和目标点。
- 绘制扫描线:在角度θ处,从屏幕中心画一条线到屏幕边缘。但下一帧角度变为θ+Δθ时,需要把上一帧的扫描线擦掉。怎么擦?我们不能简单地用背景色重画一遍,因为那条线上可能画有目标点,会被误擦除。
- 聪明的做法:在绘制新的扫描线之前,沿着上一帧的扫描线,从远到近“采样”。在每个采样点位置,读取屏幕上该点的颜色。
- 如果颜色是背景色,说明这里没有目标,直接用背景色画一个点覆盖即可(擦除线段)。
- 如果颜色是目标颜色(比如绿色),说明这里有一个之前画的目标点。我们不能擦掉它!所以跳过这个点,继续检查下一个采样点。这样,目标点就被保留了下来。
- 最后,再用扫描线颜色(比如亮白色)绘制当前角度的新扫描线。
策略二:目标点的绘制与持久化。当超声波测到有效距离时,我们就在对应的(x, y)位置画一个点(比如2x2像素的小方块)。这个点一旦画出,除非被明确覆盖,否则会一直留在屏幕上。这模拟了雷达屏幕上目标回波的“余晖”效果。为了不让旧目标永远残留,我们可以引入一个“衰减”机制:例如,每个循环周期,将所有非背景色的像素点的亮度稍微降低一点,几个周期后它就自动消失了。但这需要维护一个目标点列表,对Uno来说负担较重。一个更简单的方法是:定期(比如每完成10次全扫描)用背景色轻微刷新一下屏幕外围区域,只保留中心附近的最新目标。
这种“只擦除非目标像素”的局部刷新方法,结合目标点的持久化绘制,就是我们在有限资源下实现流畅雷达效果的关键算法。
3.3 主程序逻辑流程图与代码框架
整个Arduino Sketch的程序结构如下,它清晰地反映了我们上面讨论的逻辑:
#include <Adafruit_ST7735.h> #include <Adafruit_GFX.h> #include <Servo.h> // 引脚定义、常量定义(屏幕尺寸、最大距离、映射半径等) #define TRIG_PIN 9 #define ECHO_PIN 10 #define SERVO_PIN 6 #define TFT_CS 7 #define TFT_DC 8 // ... 其他定义 // 对象声明 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC); Servo radarServo; // 全局变量 int currentAngle = 0; // 当前舵机角度 int scanDirection = 1; // 扫描方向:1为增,-1为减 int lastAngle = -1; // 上一帧角度 const int ANGLE_MIN = 30; // 扫描起始角 const int ANGLE_MAX = 150; // 扫描终止角 void setup() { Serial.begin(9600); // 初始化超声波引脚 pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); // 初始化舵机 radarServo.attach(SERVO_PIN); radarServo.write(ANGLE_MIN); // 初始化TFT屏幕 tft.initR(INITR_BLACKTAB); // 初始化屏幕 tft.fillScreen(ST7735_BLACK); // 清屏为黑色 tft.setTextColor(ST7735_WHITE); // 绘制静态元素:雷达刻度圆、角度线、文本等 drawStaticRadarBackground(); } void loop() { // 1. 更新舵机角度 currentAngle += scanDirection; if (currentAngle >= ANGLE_MAX || currentAngle <= ANGLE_MIN) { scanDirection *= -1; // 到达边界,反转扫描方向 } radarServo.write(currentAngle); delay(15); // 给舵机一点时间转动到位,这个值很关键! // 2. 清除上一帧的扫描线(使用上述“智能擦除”方法) if (lastAngle != -1) { eraseScanLine(lastAngle); } // 3. 在当前角度进行超声波测距 long distance = measureDistance(); // 4. 如果距离有效,在屏幕上绘制目标点 if (distance > 0 && distance < MAX_RANGE_CM) { plotTarget(currentAngle, distance); } // 5. 绘制当前角度的新扫描线 drawScanLine(currentAngle); // 6. 更新“上一帧角度” lastAngle = currentAngle; // 7. (可选)简单“余晖”清理逻辑 static int scanCount = 0; scanCount++; if (scanCount > 100) { // 每扫描100次清理一次 fadeOldTargets(); scanCount = 0; } } // 以下是需要自己实现的函数: void drawStaticRadarBackground() { /* 画圆圈、十字线、文字 */ } long measureDistance() { /* HC-SR04测距代码 */ } void eraseScanLine(int angle) { /* 智能擦除旧扫描线 */ } void drawScanLine(int angle) { /* 画新扫描线 */ } void plotTarget(int angle, long dist) { /* 坐标转换并画点 */ } void fadeOldTargets() { /* 淡出旧目标 */ }这个框架给出了清晰的骨架。delay(15)是平衡舵机响应速度和扫描帧率的关键。太短,舵机没到位就测量,数据不准;太长,雷达扫描动画会卡顿。需要根据你的舵机性能微调。
4. 分步实现与代码详解
现在,我们来填充上面框架中的每一个函数,把雷达从概念变成现实。
4.1 初始化与静态背景绘制
setup()函数中的drawStaticRadarBackground()负责绘制所有不会动的屏幕元素,这能极大减轻主循环的负担。
void drawStaticRadarBackground() { tft.fillScreen(ST7735_BLACK); // 黑色背景模拟雷达屏幕 int centerX = tft.width() / 2; int centerY = tft.height() / 2; int maxRadius = min(centerX, centerY) - 2; // 计算最大绘制半径 // 1. 绘制同心圆距离刻度 tft.drawCircle(centerX, centerY, maxRadius, ST7735_DARKGREEN); tft.drawCircle(centerX, centerY, maxRadius * 2 / 3, ST7735_DARKGREEN); tft.drawCircle(centerX, centerY, maxRadius / 3, ST7735_DARKGREEN); // 2. 绘制角度刻度线 (每30度一条) for (int ang = 0; ang < 180; ang += 30) { float rad = radians(ang); int x1 = centerX + (maxRadius + 5) * cos(rad); // 线稍长于最外圈 int y1 = centerY - (maxRadius + 5) * sin(rad); // 注意y轴方向 tft.drawLine(centerX, centerY, x1, y1, ST7735_DARKGREEN); // 在刻度线末端添加角度数字(需要处理文本方向,略复杂,可先省略) } // 3. 在屏幕顶部显示标题和信息 tft.setCursor(10, 5); tft.setTextSize(1); tft.println("Arduino Sonar Radar"); // 可以添加距离范围等信息 }绘制静态背景一次完成,后续不再修改。深绿色(ST7735_DARKGREEN)的刻度线在黑色背景上清晰又不刺眼,符合雷达屏幕的经典美学。
4.2 超声波测距函数的稳健实现
measureDistance()函数必须稳定可靠。这里提供一个增加了超时处理和错误过滤的版本。
long measureDistance() { digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2); digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10); // 触发脉冲至少10us digitalWrite(TRIG_PIN, LOW); // 读取高电平脉冲持续时间,并设置超时(例如30ms,对应约5米) long duration = pulseIn(ECHO_PIN, HIGH, 30000); // 计算距离 long distanceCm = 0; if (duration > 0) { distanceCm = duration * 0.034 / 2; // 使用更精确的声速计算 // 简单的滤波:如果距离突变过大,可能是误回波,返回上一个有效值或0 static long lastValidDistance = 0; if (abs(distanceCm - lastValidDistance) < 50) { // 假设两帧间距离变化不会超过50cm lastValidDistance = distanceCm; } else { distanceCm = 0; // 视为无效测量 } } return distanceCm; // 返回0表示无效或超时 }pulseIn函数的第三个参数30000是超时时间(微秒)。如果30毫秒内没收到回波,函数会返回0,我们可以据此判断为“无目标”或“超出量程”。简单的跳跃滤波可以有效抑制偶尔出现的错误读数。
4.3 动态扫描线与目标绘制的核心代码
这是整个项目的精华所在,实现了之前提到的“智能擦除”和绘制。
void eraseScanLine(int angle) { int centerX = tft.width() / 2; int centerY = tft.height() / 2; float rad = radians(angle); // 从最远处向中心采样,步长可以设为2-3像素以提高效率 for (int r = RADIUS_MAX; r > 0; r -= 2) { int x = centerX + r * cos(rad); int y = centerY - r * sin(rad); // 检查该像素点当前的颜色 uint16_t currentColor = tft.readPixel(x, y); // 注意:readPixel函数非常慢!会严重拖慢帧率。因此,这是演示逻辑,实际需要优化。 // 优化方案1:如果确定目标点颜色独特,且背景是纯黑,可以改为判断颜色是否接近黑色。 // 优化方案2(推荐):维护一个目标点坐标数组。擦除时,只重绘背景色线段,然后遍历目标数组,如果目标点在这条线上,就重新绘制该目标点。 if (currentColor == ST7735_BLACK) { tft.drawPixel(x, y, ST7735_BLACK); // 是背景,直接擦除 } // 否则,是目标点颜色,跳过不擦 } } void drawScanLine(int angle) { int centerX = tft.width() / 2; int centerY = tft.height() / 2; float rad = radians(angle); // 使用drawLine函数从中心画到边缘,效率比逐点画高 int xEnd = centerX + RADIUS_MAX * cos(rad); int yEnd = centerY - RADIUS_MAX * sin(rad); tft.drawLine(centerX, centerY, xEnd, yEnd, ST7735_WHITE); } void plotTarget(int angle, long distCm) { int centerX = tft.width() / 2; int centerY = tft.height() / 2; float rad = radians(angle); // 将实际距离映射到屏幕像素半径 // 假设有效距离范围是 2cm - 150cm,映射到 5px - RADIUS_MAX px int pixelRadius = map(distCm, 2, 150, 5, RADIUS_MAX); pixelRadius = constrain(pixelRadius, 5, RADIUS_MAX); // 限制在合理范围 int x = centerX + pixelRadius * cos(rad); int y = centerY - pixelRadius * sin(rad); // 画一个2x2的小方块作为目标点,颜色可以用绿色或黄色 tft.fillRect(x-1, y-1, 3, 3, ST7735_GREEN); // 注意确保坐标不越界 }关键优化点:
readPixel函数在大多数Arduino TFT库中效率极低,会使得动画卡顿。生产代码中不应使用。这里列出是为了清晰说明原理。实际的优化方案是维护一个目标点列表(数组)。在eraseScanLine函数中,我们直接用背景色重画整条旧的扫描线(这可能会覆盖线上的旧目标点)。紧接着,在plotTarget函数中,不仅要在新位置画点,还要把这个点的坐标和颜色存入数组。然后,在主循环中,在绘制新扫描线之后,遍历整个目标点数组,把所有目标点重新绘制一遍。这样,被误擦的目标点就恢复了。虽然每次循环都要重绘所有目标点,但只要目标数量不多(比如少于20个),其开销远低于反复调用readPixel。
4.4 性能调优与“余晖”效果模拟
为了让雷达看起来更真实,我们需要模拟目标回波随时间淡出的“余晖”效果,同时也要管理好有限的单片机资源。
方案A:简易定时清屏法这是最简单粗暴但有效的方法。在loop()中设置一个计数器,每完成N次完整扫描(比如从ANGLE_MIN到ANGLE_MAX再扫回来算一次),就执行一次fadeOldTargets()。
void fadeOldTargets() { // 方法1:轻微刷新外围区域。画一个稍小的黑色实心圆,覆盖掉旧的目标点,但保留中心区域的最新目标。 int centerX = tft.width() / 2; int centerY = tft.height() / 2; tft.fillCircle(centerX, centerY, RADIUS_MAX - 10, ST7735_BLACK); // 然后立刻重画被覆盖的静态刻度圈 tft.drawCircle(centerX, centerY, RADIUS_MAX, ST7735_DARKGREEN); // ... 重画其他静态元素 // 注意:这会短暂闪烁,但发生在扫描间隙,不易察觉。 }方案B:目标点生命周期管理(进阶)这是更优雅的方案,但需要更多内存和计算。我们定义一个结构体数组来管理目标点。
struct Target { int x; int y; int life; // 生命周期,每次主循环减1,为0时消失 }; Target targets[MAX_TARGETS]; // 假设最大10个目标 void plotTarget(int angle, long distCm) { // ... 计算x,y坐标... // 查找一个空闲的target槽位,或者覆盖生命周期最短的 for (int i=0; i<MAX_TARGETS; i++) { if (targets[i].life <= 0) { targets[i].x = x; targets[i].y = y; targets[i].life = LIFE_INIT; // 例如初始化为50帧 tft.fillRect(x-1, y-1, 3, 3, ST7735_GREEN); return; } } } void updateAndDrawTargets() { for (int i=0; i<MAX_TARGETS; i++) { if (targets[i].life > 0) { targets[i].life--; if (targets[i].life > 0) { // 根据生命值调整颜色(例如从绿色渐变为黑色) uint16_t color = tft.Color565(0, targets[i].life*5, 0); // 绿色逐渐变暗 tft.fillRect(targets[i].x-1, targets[i].y-1, 3, 3, color); } else { // 生命结束,用背景色擦除 tft.fillRect(targets[i].x-1, targets[i].y-1, 3, 3, ST7735_BLACK); } } } } // 然后在loop()中,在绘制扫描线后调用 updateAndDrawTargets();方案B实现了漂亮的渐变淡出效果,但更消耗资源。对于初次实现,建议从方案A开始,它更容易理解和调试。
5. 组装、调试与效果优化实战
硬件焊接(或使用面包板)、上传代码之后,真正的挑战才刚刚开始。下面是我在多次制作中积累的调试经验和优化技巧。
5.1 硬件组装注意事项与校准
- 传感器固定:将HC-SR04牢固地粘在舵机舵盘上,确保超声波发射面朝前,且与舵机旋转轴尽可能垂直。任何歪斜都会导致角度测量误差。可以使用热熔胶或螺丝固定。
- 线缆管理:舵机转动时,连接HC-SR04的杜邦线可能会缠绕。确保线有足够的松弛度,或者使用滑环(高级玩法),但最简单有效的方法是限制舵机扫描范围(如30度到150度),避免360度旋转。
- 供电测试:先不接屏幕,单独测试舵机转动和超声波测距是否正常。用串口监视器查看距离数据。确保在舵机运动时,超声波读数依然稳定。如果出现复位,立刻检查供电。
- 距离校准:找一个已知距离的物体(如50厘米处的墙壁),测量超声波读数是否准确。如果不准,调整
measureDistance()函数中的声速系数(0.034)。温度变化大的环境,可以考虑加入温湿度传感器DHT11进行动态补偿,但这属于进阶内容。 - 角度校准:让舵机转到你认为是0度(正前方)的位置,在代码中记录下此时
servo.write()的值。然后转到180度,再记录一个值。你的扫描起始角ANGLE_MIN和终止角ANGLE_MAX应该基于这两个实测值来设置,而不是理论上的0和180。
5.2 软件调试与常见问题排查
上传代码后,屏幕可能一片黑,或者扫描线不动。别慌,按以下步骤排查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 屏幕无任何显示 | 1. 电源未接通或接反。 2. 背光未开启。 3. SPI引脚接错。 4. 屏幕初始化代码不对。 | 1. 检查VCC/GND。 2. 将屏幕LED引脚接3.3V或5V。 3. 核对CS、DC引脚定义与代码是否一致。 4. 尝试 tft.initR(INITR_GREENTAB)或INITR_REDTAB等不同初始化参数。 |
| 扫描线不转动 | 1. 舵机信号线接错或接触不良。 2. 舵机供电不足。 3. 代码中舵机控制部分被阻塞。 | 1. 检查舵机信号线是否接在PWM引脚(如6号)。 2. 用外接电源给舵机供电。 3. 检查 loop()中是否有delay()过长,或是在measureDistance()中卡住。 |
| 有扫描线但无目标点 | 1. 超声波模块故障或接线错误。 2. 测量距离超出范围或始终为0。 3. 坐标映射函数 plotTarget出错。 | 1. 用串口打印distanceCm值,看是否正常。2. 检查 TRIG_PIN和ECHO_PIN定义。3. 在 plotTarget函数中,将计算出的x, y通过串口打印出来,看是否在屏幕范围内。 |
| 目标点位置严重偏移 | 1. 角度到弧度的转换错误。 2. 屏幕坐标原点理解错误。 3. 舵机0度位置与屏幕0度(通常为右侧)未对齐。 | 1. 确认使用了radians()函数。2. 记住屏幕y轴向下为正,所以公式中是 y = centerY - r * sin(θ)。3. 进行角度校准:让舵机对准正前方,在代码中调整角度偏移量。 |
| 动画严重闪烁卡顿 | 1. 使用了readPixel等慢速函数。2. 屏幕刷新模式太慢。 3. 舵机 delay()时间过长。 | 1.务必使用“重绘目标点数组”方案替代readPixel。2. 确保使用SPI全速模式(通常默认就是)。 3. 减少舵机 delay(15)到delay(10)或更小,测试舵机能否跟上。 |
| 目标点不消失,屏幕越来越乱 | “余晖”清理逻辑未生效或生效太慢。 | 调快fadeOldTargets()的调用频率(减少scanCount阈值),或者改用生命周期管理方案。 |
调试心法:分模块测试。永远不要一次性写完所有代码。先写舵机扫描,测试角度变化。再加上超声波,测试串口距离输出。然后单独写一个在固定位置画点的程序测试屏幕。最后把所有模块像搭积木一样组合起来。利用好Arduino的串口监视器,它是你最好的朋友。
5.3 效果优化与功能扩展思路
当基础功能稳定后,你可以尝试以下优化和扩展,让你的雷达更出色:
颜色与视觉优化:
- 距离编码:用颜色表示距离远近。近处目标用红色(危险),远处用绿色(安全)。在
plotTarget中根据distCm值选择颜色。 - 扫描线渐变:让扫描线从中心到边缘颜色渐淡(如白色到灰色),增加立体感。这需要逐点绘制并计算颜色,会降低性能,慎用。
- 添加网格和数字:在静态背景上添加更精细的距离环(每50px一个)和角度数字(0°, 45°, 90°等)。
- 距离编码:用颜色表示距离远近。近处目标用红色(危险),远处用绿色(安全)。在
交互与功能扩展:
- 串口控制:通过串口发送命令,动态改变扫描范围、速度或模式。
- 报警功能:当有物体进入预设的“警戒区域”(如距离小于30厘米的扇形区)时,让一个LED闪烁或蜂鸣器鸣叫。
- 数据记录:将扫描到的目标角度和距离通过串口发送到电脑,用Processing或Python编写一个上位机软件,实现更复杂的轨迹显示和记录。
性能终极优化:
- 使用
AVR寄存器直接操作SPI:放弃Adafruit_ST7735库中通用的SPI.transfer(),直接读写ATmega328P的SPI寄存器,可以大幅提升屏幕写入速度。但这需要深入的数据手册知识和汇编思维,属于高手向的优化。 - 升级硬件:如果对帧率和效果有极致追求,将主控换成ESP32。其更高的主频和双核能力,可以轻松驱动更高分辨率的屏幕,并实现更复杂的图形算法。代码逻辑几乎可以复用,只需替换底层库。
- 使用
这个项目最迷人的地方在于,它从一个简单的想法出发,融合了硬件连接、传感器应用、实时图形和算法思维。当你第一次看到那条白色的扫描线在屏幕上划过,并在障碍物位置留下一个光点时,那种将物理世界映射到数字屏幕上的成就感,是无与伦比的。它不仅仅是一个玩具,更是一个理解信号处理、坐标系统和嵌入式图形开发的绝佳起点。希望你能在制作过程中享受到这种创造的乐趣,并以此为基础,探索出更多有趣的玩法。