全部拼接完成WIFI监控避障小车

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

296.14.全部拼接完成WIFI监控避障小车

#define BLINKER_WIFI                    // 选择 Blinker 的 Wi-Fi 接入方式。
#include <Blinker.h>                    // 引入 Blinker 库的声明。
#include "ESP32_CAM_SERVER.h"          // 引入配套摄像头服务文件;这里不展开其内部实现。
#define LED 4                           // 把 ESP32-CAM 板载白色闪光灯所接的 GPIO4 命名为 LED。
#define Servo 12                        // 把输出舵机控制脉冲的 GPIO12 命名为 Servo;大小写照课堂代码保留。
#define Echo 16                         // 把接收超声波回波脉冲的 GPIO16 命名为 Echo。
#define Trig 1                          // 把触发超声波测距的 GPIO1 命名为 Trig。
#define IN1 13                          // 把连接 L298N IN1 的 GPIO13 命名为 IN1。
#define IN2 15                          // 把连接 L298N IN2 的 GPIO15 命名为 IN2。
#define IN3 14                          // 把连接 L298N IN3 的 GPIO14 命名为 IN3。
#define IN4 2                           // 把连接 L298N IN4 的 GPIO2 命名为 IN4。
char auth[] = "c56be051d11f";          // 视频公开展示的设备 Secret Key;复现时必须换成自己的密钥。
char ssid[] = "302";                   // 视频中的 Wi-Fi 热点名称;复现时换成自己的网络名称。
char pswd[] = "12345678";              // 视频中的 Wi-Fi 热点密码;不是 Blinker 账号密码。
bool setup_camera = false;              // 标记摄像头服务尚未由下方代码启动。
BlinkerButton Button1("led");           // 建立 LED 按键组件;"led" 是它在 App 中的组件键名。
BlinkerButton Button2("GoForward");     // 建立前进按键组件;字符串必须与 App 组件键名一致。
BlinkerButton Button3("GoBack");        // 建立后退按键组件;字符串必须与 App 组件键名一致。
BlinkerButton Button4("Left");          // 建立左转按键组件;字符串必须与 App 组件键名一致。
BlinkerButton Button5("Right");         // 建立右转按键组件;字符串必须与 App 组件键名一致。
BlinkerButton Button6("Stop");          // 建立停止按键组件;字符串必须与 App 组件键名一致。
BlinkerSlider Slider0("duoji");         // 建立舵机滑块组件;"duoji" 是它在 App 中的组件键名。
void button1_callback(const String & state) // 定义 LED 按键回调;state 保存收到的按键状态。
{                                       // 开始 LED 按键回调。
    BLINKER_LOG("get button state: ", state); // 把收到的按键状态写到 Blinker 串口调试日志。
    digitalWrite(LED, !digitalRead(LED)); // 读取 GPIO4 当前电平、取反,再写回 GPIO4。
}                                       // 结束 LED 按键回调。
void button2_callback(const String & state) // 定义前进按键 Button2 的回调函数。
{                                       // 开始处理前进按键事件。
    BLINKER_LOG("get button state: ", state); // 输出本次前进按键的状态字符串。
    if (state == "press" || state == "tap") // 收到按住或轻触事件时进入运动分支。
    {                                   // 开始前进分支。
        GoForward();                    // 向 L298N 输出 1、0、1、0,使小车前进。
    }                                   // 结束前进分支。
    else if (state == "pressup")        // 收到按键松开事件时进入停止分支。
    {                                   // 开始松开停止分支。
        Stop();                         // 把四个方向输入都拉低,让两个电机通道停止。
    }                                   // 结束松开停止分支。
}                                       // 结束前进按键回调。
void button3_callback(const String & state) // 定义后退按键 Button3 的回调函数。
{                                       // 开始处理后退按键事件。
    BLINKER_LOG("get button state: ", state); // 输出本次后退按键的状态字符串。
    if (state == "press" || state == "tap") // 收到按住或轻触事件时进入运动分支。
    {                                   // 开始后退分支。
        GoBack();                       // 向 L298N 输出 0、1、0、1,使小车后退。
    }                                   // 结束后退分支。
    else if (state == "pressup")        // 收到按键松开事件时进入停止分支。
    {                                   // 开始松开停止分支。
        Stop();                         // 停止两个电机通道。
    }                                   // 结束松开停止分支。
}                                       // 结束后退按键回调。
void button4_callback(const String & state) // 定义左转按键 Button4 的回调函数。
{                                       // 开始处理左转按键事件。
    BLINKER_LOG("get button state: ", state); // 输出本次左转按键的状态字符串。
    if (state == "press" || state == "tap") // 收到按住或轻触事件时进入转向分支。
    {                                   // 开始左转分支。
        Left();                         // 让两侧电机朝相反方向旋转,按课堂接线原地左转。
        delay(100);                     // 阻塞等待 100 毫秒;delay() 的参数单位不是微秒。
        Stop();                         // 100 毫秒后主动停止,形成一次短促转向。
    }                                   // 结束左转分支。
    else if (state == "pressup")        // 收到按键松开事件时再次进入停止分支。
    {                                   // 开始松开停止分支。
        Stop();                         // 再次保证两个电机通道停止。
    }                                   // 结束松开停止分支。
}                                       // 结束左转按键回调。
void button5_callback(const String & state) // 定义右转按键 Button5 的回调函数。
{                                       // 开始处理右转按键事件。
    BLINKER_LOG("get button state: ", state); // 输出本次右转按键的状态字符串。
    if (state == "press" || state == "tap") // 收到按住或轻触事件时进入转向分支。
    {                                   // 开始右转分支。
        Right();                        // 让两侧电机朝相反方向旋转,按课堂接线原地右转。
        delay(100);                     // 阻塞等待 100 毫秒。
        Stop();                         // 100 毫秒后主动停止,形成一次短促转向。
    }                                   // 结束右转分支。
    else if (state == "pressup")        // 收到按键松开事件时再次进入停止分支。
    {                                   // 开始松开停止分支。
        Stop();                         // 再次保证两个电机通道停止。
    }                                   // 结束松开停止分支。
}                                       // 结束右转按键回调。
void button6_callback(const String & state) // 定义停止按键 Button6 的回调函数。
{                                       // 开始处理停止按键事件。
    BLINKER_LOG("get button state: ", state); // 输出本次停止按键的状态字符串。
    if (state == "press" || state == "tap") // 收到按住或轻触事件时进入停止分支。
    {                                   // 开始主动停止分支。
        Stop();                         // 立即把 IN1~IN4 全部拉低。
    }                                   // 结束主动停止分支。
    else if (state == "pressup")        // 收到按键松开事件时进入同一停止逻辑。
    {                                   // 开始松开停止分支。
        Stop();                         // 松开停止键后仍保持停止状态。
    }                                   // 结束松开停止分支。
}                                       // 结束停止按键回调。
void slider0_callback(int32_t value)    // 定义舵机滑块回调;value 是 App 发来的 32 位有符号整数。
{                                       // 开始处理本次滑块数据。
    int servo=value;                    // 用小写 servo 临时保存滑块值;它与大写 Servo 宏不同。
    Myservo(servo);                     // 把滑块值传给舵机控制函数,生成对应脉宽。
}                                       // 结束滑块回调。
void Myservo(int angle)                 // 定义前面课程保留的舵机控制函数。
{                                       // 开始执行舵机控制函数。
    for (int i = 0; i < 5; i++) {       // 连续发送 5 个代表同一目标值的控制脉冲。
        int pulsewidth = (angle * 11) + 500; // 按课堂公式计算高电平宽度,单位为微秒。
        digitalWrite(Servo, HIGH);       // 将 GPIO12 拉高,开始本次控制脉冲。
        delayMicroseconds(pulsewidth);   // 保持高电平 pulsewidth 微秒。
        digitalWrite(Servo, LOW);        // 将 GPIO12 拉低,结束高电平脉冲。
        delayMicroseconds(20000 - pulsewidth); // 保持低电平,使本轮名义周期合计为 20 000 微秒。
    }                                   // 结束 5 次脉冲发送。
    delay(10);                          // 发送完 5 个脉冲后再等待 10 毫秒。
}                                       // 结束舵机控制函数。
long getTime()                          // 定义一次超声波测距的回波计时函数。
{                                       // 开始产生触发脉冲并等待回波。
    digitalWrite(Trig, HIGH);           // 把 Trig 拉高,开始 10 微秒触发脉冲。
    delayMicroseconds(10);              // 高电平保持 10 微秒。
    digitalWrite(Trig, LOW);            // 把 Trig 拉低,结束触发脉冲。
    return pulseIn(Echo, HIGH);         // 返回 Echo 高电平持续时间,单位为微秒。
}                                       // 结束一次回波计时。
void dataRead(const String & data)      // 定义未由已绑定组件处理的数据所使用的通用回调。
{                                       // 开始处理收到的数据。
    BLINKER_LOG("Blinker readString: ", data); // 将收到的内容写到串口调试日志。
    Blinker.vibrate();                  // 向 App 发送手机振动请求。
    uint32_t BlinkerTime = millis();    // 读取本次开机以来经过的毫秒数。
    Blinker.print("millis", BlinkerTime); // 以 millis 为键把该数值发送给 App。
}                                       // 结束通用数据回调。
void GoForward()                        // 封装小车前进所需的四路方向电平。
{                                       // 开始设置前进电平。
    digitalWrite(IN1, HIGH);            // A 通道输入组合的第一位设为 1。
    digitalWrite(IN2, LOW);             // A 通道输入组合的第二位设为 0。
    digitalWrite(IN3, HIGH);            // B 通道输入组合的第一位设为 1。
    digitalWrite(IN4, LOW);             // B 通道输入组合的第二位设为 0。
}                                       // 前进组合最终为 1、0、1、0。
void GoBack()                           // 封装小车后退所需的四路方向电平。
{                                       // 开始设置后退电平。
    digitalWrite(IN1, LOW);             // A 通道输入组合的第一位设为 0。
    digitalWrite(IN2, HIGH);            // A 通道输入组合的第二位设为 1。
    digitalWrite(IN3, LOW);             // B 通道输入组合的第一位设为 0。
    digitalWrite(IN4, HIGH);            // B 通道输入组合的第二位设为 1。
}                                       // 后退组合最终为 0、1、0、1。
void Left()                             // 封装课堂接线下的原地左转电平。
{                                       // 开始设置左转电平。
    digitalWrite(IN1, HIGH);            // 让 A 通道按课堂定义的正向工作。
    digitalWrite(IN2, LOW);             // 与上一行共同构成 A 通道的 1、0。
    digitalWrite(IN3, LOW);             // 让 B 通道按课堂定义的反向工作。
    digitalWrite(IN4, HIGH);            // 与上一行共同构成 B 通道的 0、1。
}                                       // 左转组合最终为 1、0、0、1。
void Right()                            // 封装课堂接线下的原地右转电平。
{                                       // 开始设置右转电平。
    digitalWrite(IN1, LOW);             // 让 A 通道按课堂定义的反向工作。
    digitalWrite(IN2, HIGH);            // 与上一行共同构成 A 通道的 0、1。
    digitalWrite(IN3, HIGH);            // 让 B 通道按课堂定义的正向工作。
    digitalWrite(IN4, LOW);             // 与上一行共同构成 B 通道的 1、0。
}                                       // 右转组合最终为 0、1、1、0。
void Stop()                             // 封装两个电机通道的停止电平。
{                                       // 开始设置停止电平。
    digitalWrite(IN1, LOW);             // 把 A 通道输入 1 拉低。
    digitalWrite(IN2, LOW);             // 把 A 通道输入 2 拉低。
    digitalWrite(IN3, LOW);             // 把 B 通道输入 1 拉低。
    digitalWrite(IN4, LOW);             // 把 B 通道输入 2 拉低。
}                                       // 停止组合最终为 0、0、0、0。
void setup()                            // 定义上电或复位后只执行一次的初始化函数。
{                                       // 开始初始化。
    Serial.begin(115200);               // 以 115200 波特率启动串口。
    BLINKER_DEBUG.stream(Serial);       // 把 Blinker 的调试输出指向串口。
    pinMode(LED, OUTPUT);               // 将 GPIO4 配置成数字输出。
    digitalWrite(LED, LOW);             // 启动时让板载闪光灯输出低电平。
    pinMode(Servo, OUTPUT);             // 将 GPIO12 配置成舵机控制信号输出。
    digitalWrite(Servo, LOW);           // 让舵机信号脚初始保持低电平。
    pinMode(Echo, INPUT);               // 将连接超声波 Echo 的 GPIO16 配置成输入。
    pinMode(Trig, OUTPUT);              // 将连接超声波 Trig 的 GPIO1 配置成输出。
    pinMode(IN1, OUTPUT);               // 将连接 L298N IN1 的 GPIO13 配置成输出。
    digitalWrite(IN1, LOW);             // 把 IN1 的初始输出显式设为低电平。
    pinMode(IN2, OUTPUT);               // 将连接 L298N IN2 的 GPIO15 配置成输出。
    digitalWrite(IN1, LOW);             // 课堂最终代码仍写 IN1;此处按视频原样保留该复制粘贴错误。
    pinMode(IN3, OUTPUT);               // 将连接 L298N IN3 的 GPIO14 配置成输出。
    digitalWrite(IN1, LOW);             // 课堂最终代码仍写 IN1;原意应是初始化刚配置的 IN3。
    pinMode(IN4, OUTPUT);               // 将连接 L298N IN4 的 GPIO2 配置成输出。
    digitalWrite(IN1, LOW);             // 课堂最终代码仍写 IN1;原意应是初始化刚配置的 IN4。
    Blinker.begin(auth,ssid,pswd);      // 使用设备密钥、热点名和密码接入 Blinker。
    Blinker.attachData(dataRead);       // 注册通用数据回调。
    Button1.attach(button1_callback);   // 把 LED 按键对象与 LED 回调关联。
    Button2.attach(button2_callback);   // 把前进按键对象与前进回调关联。
    Button3.attach(button3_callback);   // 把后退按键对象与后退回调关联。
    Button4.attach(button4_callback);   // 把左转按键对象与左转回调关联。
    Button5.attach(button5_callback);   // 把右转按键对象与右转回调关联。
    Button6.attach(button6_callback);   // 把停止按键对象与停止回调关联。
    Slider0.attach(slider0_callback);   // 把舵机滑块对象与滑块回调关联。
}                                       // 结束初始化。
void loop()                             // 定义 Arduino 反复执行的主循环。
{                                       // 开始本轮循环。
    Blinker.run();                      // 维持 Blinker 通信并处理 App 发来的指令。
    long dis=getTime()/58;              // 把回波往返时间近似换算成整数厘米并保存为 dis。
    if(dis >1 &&dis<20)                 // 仅当整数距离大于 1 且小于 20 厘米时进入避障分支。
    {                                   // 开始处理近距离障碍物。
        digitalWrite(LED,HIGH);         // 点亮 GPIO4 上的板载白色闪光灯。
        GoBack();                       // 写入后退电平,让小车开始向后移动。
        delay(100);                     // 保持后退 100 毫秒;不是 100 微秒。
        Stop();                         // 结束本次短促后退并停止电机。
        digitalWrite(LED,LOW);          // 关闭板载闪光灯,为下一次检测恢复状态。
    }                                   // 结束本次避障分支。
    if (Blinker.connected() && !setup_camera) // Blinker 已连接且摄像头服务尚未启动时进入。
    {                                   // 开始首次启动摄像头服务的分支。
        setupCamera();                  // 调用配套文件中的摄像头及本地视频流服务初始化。
        setup_camera = true;            // 记录已经初始化,避免以后每轮重复调用。
        Blinker.printObject("video", "{\"str\":\"mjpg\",\"url\":\"http://"+ WiFi.localIP().toString() + "\"}"); // 向 video 组件发送 MJPG 格式和本机局域网 URL。
    }                                   // 结束首次启动摄像头服务的分支。
}                                       // 结束本轮循环,随后再次进入 loop。

一、WIFI监控避障小车

本节把前面已经完成的 ESP32-CAM 视频监控、Blinker 联网控制、LED、舵机、L298N 小车运动和 HC-SR04 超声波测距拼到同一份草图中。上方只收录视频前半段实际编译、上传的主草图业务代码,不展开 Blinker.h、ESP32_CAM_SERVER.h 等库文件,也不把后半段资源包中短暂出现、但没有完整讲解的另一份辅助草图混进来。

程序仍允许 App 手动控制前进、后退、转向、停止、灯光和舵机;新增的“避障”只是每轮测得近距离物体时强制短暂后退,并不是一套会自主前进、规划路径的完整自动驾驶逻辑。

1.1 代码分析

本节在第 295 节累计代码上真正增加了四部分:Echo、Trig 两个引脚宏,getTime() 测距函数,setup() 中的超声波引脚初始化,以及 loop() 中的距离判断与后退分支。

  • 独立测距课曾使用 GPIO13、GPIO15;本节这两个引脚已经分配给 L298N 的 IN1、IN2,所以综合程序改为 Echo=16、Trig=1。这不是机械复制旧宏,而是一次重新分配;相应实物接线也必须同时更改。
  • getTime() 先让 Trig 保持高电平 10 μs,再用 pulseIn(Echo, HIGH) 测量 Echo 高电平的持续时间。HC-SR04 测得的是声波“发出—反射—返回”的往返时间;getTime()/58 是常用的厘米近似换算,并且这里执行整数除法,会丢弃小数,不能称为“精确距离”。HC-SR04 数据手册、Arduino pulseIn() 参考
  • 判断式 dis > 1 && dis < 20 两端都不包含:按这里的整数厘米结果,实际进入分支的是 2~19 cm。pulseIn() 在没有检测到脉冲时会等到超时后返回 0,因此 dis > 1 也顺带排除了无回波的 0,但不能区分“太近”和“没有回波”。
  • 条件成立后的顺序是“开灯 → GoBack() → 阻塞等待 100 ms → Stop() → 关灯”。delay(100) 的单位是毫秒,不是原笔记所写的微秒。Arduino delay() 参考
  • digitalWrite() 设置的电平会一直保持,故 GoBack() 返回后小车仍会后退,直到 100 ms 后 Stop() 改写四个方向引脚。若障碍物下一轮仍处在判断范围内,这套动作会再次执行,表现为间歇性连续后撤。
  • Blinker.run() 每轮只在测距之前调用一次。pulseIn() 默认可能长时间阻塞,避障动作又额外阻塞 100 ms;这期间 App 指令处理和画面相关任务的及时性都可能下降。HC-SR04 手册还建议两次测量的周期大于 60 ms,而课堂循环没有显式设置测量间隔,所以这是一份能展示基本功能、但时序仍可改进的入门程序。

1.2 代码上传

视频第一次编译失败,并不是第 221 行的“宏定义写错”,而是避障分支把两处 LED 写成了小写 led。C++ 标识符区分大小写,程序只定义了 #define LED 4,所以编译器提示 'led' was not declared in this scope。视频随后把开灯和关灯两处都改回大写 LED,再次编译并成功上传;上方收录的是修正后的最终版本。

上传时应选择实际 ESP32-CAM 板型和串口。使用常见 USB 转串口方式时,让 GPIO0 在复位或上电时临时接 GND 进入下载模式;上传完成后断开 GPIO0—GND,再复位运行。Espressif 下载模式说明

还需注意两项源码记录:

  1. auth、ssid、pswd 是视频公开演示值,自己的设备必须换成自己的密钥、网络名和密码,真实凭据不应提交到公开仓库。
  2. 课堂最终 setup() 在配置 IN2、IN3、IN4 后仍重复执行 digitalWrite(IN1, LOW)。上方为忠实记录没有暗改;可靠复现时,应分别写成下列四行:
digitalWrite(IN1, LOW); // 明确初始化 L298N 的 IN1。
digitalWrite(IN2, LOW); // 明确初始化 L298N 的 IN2。
digitalWrite(IN3, LOW); // 明确初始化 L298N 的 IN3。
digitalWrite(IN4, LOW); // 明确初始化 L298N 的 IN4。

1.3 修改宏定义

本节最终有效的引脚映射如下;改动宏后必须按新映射改线并重新编译、上传,单独修改宏不会调整 App 布局。

功能宏GPIO
板载闪光灯LED4
舵机信号Servo12
超声波回波Echo16
超声波触发Trig1
L298N IN1IN113
L298N IN2IN215
L298N IN3IN314
L298N IN4IN42

原笔记把修复 led/LED 大小写问题称为“修改宏定义”并不准确:视频没有改 #define LED 4,而是把使用位置的标识符改回与宏一致的 LED。同理,Blinker 组件是否能响应取决于代码字符串与 App 的“组件键名”完全一致,也不是 GPIO 宏能解决的问题。

1.3.1 界面配置文件选择

如果不想逐个摆放组件,可以在设备页面进入“设备管理 → 界面配置”,全选原 JSON,粘贴资源包中的界面配置并更新;看到“数据导入成功”后,资源布局即写入设备页面。导入配置会替换当前页面配置,操作前宜备份自己的 JSON。

视频资源布局中的五个电机键名 GoForward、GoBack、Left、Right、Stop 与上方主草图一致,但资源布局把灯和舵机键分别写成 Led、Servo,而上方主草图要求 led、duoji。因此直接导入后还要二选一:

  • 在 App 中把灯键改为 led、舵机滑块键改为 duoji;或
  • 把代码中的两个组件键改成资源布局使用的值,再重新上传。

视频后半段 Arduino IDE 背景还出现了另一份资源草图,它的对象名与上方累计代码不同。资源配置很可能与那份辅助草图配套,不能因为界面外观相同就把两套键名混用;真正参与匹配的是大小写敏感的组件键名。

1.3.1.1 摄像头IP地址修改

资源布局在摄像头组件中写入示例地址 http://192.168.28.134/,格式选择 MJPG。实际地址应是 ESP32-CAM 接入当前网络后获得的局域网 IP;DHCP 重新分配地址后,旧地址可能失效,手机还必须能够访问设备所在的局域网。

上方主草图在 Blinker 首次连通时调用 WiFi.localIP(),向键名为 video 的组件发送当前 URL,所以配套组件与通信正常时可以动态更新地址。若导入配置后仍无画面,应依次核对组件键、MJPG 类型、设备实际 IP、手机与设备的网络路由,而不是把视频中的示例 IP 原样照抄。

1.3.1.2 舵机角度设置

视频把滑块范围由默认的 0~100 改为 0~180,并开启“实时数据”。使用本节主草图时,滑块组件键还应改为 duoji,与 BlinkerSlider Slider0("duoji") 一致;“舵机”只是显示文字,可以自由修改。

0~180 只是程序输入范围,不保证每只舵机都能安全走满机械行程。课堂公式在 0°、180°时给出约 500 μs、2480 μs 的高电平,而且每次只发送 5 个周期;不同舵机的允许脉宽、端点和保持需求可能不同,应先拆开连杆小范围校准,出现抖动、堵转或异常发热就立即停止。常见 TowerPro SG90 标称工作电压为 4.8 V,不能把“信号是 3.3 V 逻辑”误解成“舵机电源应接 3.3 V”。TowerPro SG90 规格

1.4 小车接线

按视频接线图,信号对应关系如下;接线前先断开电机电源,并逐根核对模块丝印。

模块端子ESP32-CAM/其他端点作用与注意事项
L298N IN1GPIO13A 通道方向输入 1
L298N IN2GPIO15A 通道方向输入 2
L298N IN3GPIO14B 通道方向输入 1
L298N IN4GPIO2B 通道方向输入 2
L298N OUT1/OUT2一侧两只电机并联驱动同一侧车轮
L298N OUT3/OUT4另一侧两只电机并联驱动另一侧车轮
舵机信号线GPIO12输出课堂软件脉冲
HC-SR04 EchoGPIO16回波输入,对应 Echo 宏
HC-SR04 TrigGPIO1触发输出,对应 Trig 宏
各模块 GND电池负极和公共 GND所有控制信号必须共地
  • 视频由 L298N 模块的 5 V 节点给 ESP32-CAM 供电,并保留 ENA、ENB 使能跳帽。兼容板的稳压器、跳帽及 5 V 端子方向不一定相同,上电前必须查自己模块说明并用万用表确认;电机电源不能直接接 ESP32-CAM GPIO,摄像头与 Wi-Fi 的电流峰值也要求电源有足够余量。ST L298 数据手册
  • 视频图中舵机红线接 ESP32-CAM 的 3.3 V,但常见 SG90 标称 4.8 V,这种供电不可靠。应采用符合所用舵机规格、容量足够的独立电源或稳压电源,电源地仍与 ESP32-CAM 共地;不要让舵机启动电流直接压垮主控电源。
  • 视频图中 HC-SR04 的 VCC 接 3.3 V、GND 接公共地。常见 HC-SR04 数据手册标称工作电压为 5 V,3.3 V 供电是否可靠取决于具体模块版本;若按常见规格用 5 V 供电,Echo 可能输出接近 5 V,接入只允许 3.3 V 的 ESP32 GPIO 前应使用分压或电平转换,不能只改宏定义。HC-SR04 数据手册、ESP32 数据手册
  • **GPIO1 风险:**GPIO1 是 ESP32 默认的 U0TXD。本草图同时执行 Serial.begin(115200) 并把 Blinker 调试输出送到串口,却又用 GPIO1 触发超声波;串口输出可能被模块看成错误的触发脉冲,外接模块也可能妨碍下载或日志。量产或稳定方案应重新规划引脚,调试/上传时至少先断开相关外设。Espressif UART 文档
  • **GPIO16 风险:**标准 Ai-Thinker ESP32-CAM 原理图把 GPIO16 接到板载 PSRAM 的片选信号;摄像头在较高分辨率、非 JPEG 模式等场景通常依赖 PSRAM。因而视频虽把 GPIO16 标为 Echo,这不能作为“任何 ESP32-CAM 上都可自由使用 GPIO16”的通用接法,可能造成回波读取或摄像头/PSRAM 工作异常。必须核对自己的板型和实际摄像头配置;若要稳定集成,通常需要重新做整板引脚规划。Ai-Thinker ESP32-CAM 原理图、Espressif 摄像头驱动说明
  • GPIO13、15、14、2 与板载 microSD 信号复用,本节把它们用于电机后,不能直接同时使用对应 SD 线路;GPIO2、GPIO12、GPIO15 还涉及 ESP32 启动配置,外设在复位瞬间强拉电平可能导致不能启动或下载。出现问题时先断电,再临时移除相关控制线排查。Espressif SDIO 引脚说明、ESP32 启动模式说明

1.5 小车最终演示

视频最终把物体放到超声波探头前方;进入小于 20 cm 且大于 1 cm 的判断范围后,板载白灯亮起,小车短暂后退,随后停止并熄灯。这与最终代码的执行顺序一致。演示证明该套实物在当时的供电、板型、接线和网络条件下产生了预期动作,但不能据此忽略 GPIO1 串口复用、GPIO16 PSRAM 连接和传感器电平等兼容性风险。

若实车没有得到相同结果,可按以下顺序排查:

  1. 先架空车轮并准备断电,确认公共地、供电极性、L298N 使能跳帽和四根 IN 线。
  2. 核对 App 组件键名,而不是只看按钮显示文字;资源布局中的 Led、Servo 与主草图的 led、duoji 不一致。
  3. 在不接电机的情况下检查超声波 Trig/Echo 电压与回波时间,确认没有把 Echo、Trig 接反,并处理 5 V Echo 的电平转换。
  4. 若串口日志异常、测距乱跳或上传失败,断开 GPIO1 外设;若摄像头或测距异常,结合所用板型检查 GPIO16 与 PSRAM 的冲突。
  5. 超声波能测距但小车不后退时,再检查 dis 是否实际落在 2~19 cm、L298N 电机电源和 GoBack() 的四路输出。

核对依据