全部拼接完成WIFI监控避障小车
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 数据手册、ArduinopulseIn()参考- 判断式
dis > 1 && dis < 20两端都不包含:按这里的整数厘米结果,实际进入分支的是 2~19 cm。pulseIn()在没有检测到脉冲时会等到超时后返回 0,因此dis > 1也顺带排除了无回波的 0,但不能区分“太近”和“没有回波”。 - 条件成立后的顺序是“开灯 →
GoBack()→ 阻塞等待 100 ms →Stop()→ 关灯”。delay(100)的单位是毫秒,不是原笔记所写的微秒。Arduinodelay()参考 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 下载模式说明
还需注意两项源码记录:
auth、ssid、pswd是视频公开演示值,自己的设备必须换成自己的密钥、网络名和密码,真实凭据不应提交到公开仓库。- 课堂最终
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 |
|---|---|---|
| 板载闪光灯 | LED | 4 |
| 舵机信号 | Servo | 12 |
| 超声波回波 | Echo | 16 |
| 超声波触发 | Trig | 1 |
L298N IN1 | IN1 | 13 |
L298N IN2 | IN2 | 15 |
L298N IN3 | IN3 | 14 |
L298N IN4 | IN4 | 2 |
原笔记把修复 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 IN1 | GPIO13 | A 通道方向输入 1 |
L298N IN2 | GPIO15 | A 通道方向输入 2 |
L298N IN3 | GPIO14 | B 通道方向输入 1 |
L298N IN4 | GPIO2 | B 通道方向输入 2 |
L298N OUT1/OUT2 | 一侧两只电机并联 | 驱动同一侧车轮 |
L298N OUT3/OUT4 | 另一侧两只电机并联 | 驱动另一侧车轮 |
| 舵机信号线 | GPIO12 | 输出课堂软件脉冲 |
HC-SR04 Echo | GPIO16 | 回波输入,对应 Echo 宏 |
HC-SR04 Trig | GPIO1 | 触发输出,对应 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 连接和传感器电平等兼容性风险。
若实车没有得到相同结果,可按以下顺序排查:
- 先架空车轮并准备断电,确认公共地、供电极性、L298N 使能跳帽和四根
IN线。 - 核对 App 组件键名,而不是只看按钮显示文字;资源布局中的
Led、Servo与主草图的led、duoji不一致。 - 在不接电机的情况下检查超声波 Trig/Echo 电压与回波时间,确认没有把 Echo、Trig 接反,并处理 5 V Echo 的电平转换。
- 若串口日志异常、测距乱跳或上传失败,断开 GPIO1 外设;若摄像头或测距异常,结合所用板型检查 GPIO16 与 PSRAM 的冲突。
- 超声波能测距但小车不后退时,再检查
dis是否实际落在 2~19 cm、L298N 电机电源和GoBack()的四路输出。
核对依据
- 本地
videos/296.14.全部拼接完成WIFI监控避障小车.mp4中的最终代码、编译报错与修正、上传过程、界面配置、接线图及实车演示;并对照transcripts/296.14.全部拼接完成WIFI监控避障小车.srt、原始notes/md/296.14.全部拼接完成WIFI监控避障小车.md以及第 288、289、295 节累计代码。 - HC-SR04 数据手册与 Arduino
pulseIn()参考:核对触发时序、厘米近似换算、建议测量周期和无回波超时行为。 - Ai-Thinker ESP32-CAM 原理图、Espressif ESP32 数据手册、UART 文档与摄像头驱动说明:核对 GPIO1/U0TXD、GPIO16/PSRAM、GPIO 电压和摄像头内存条件。
- ST L298 数据手册与 TowerPro SG90 规格:核对电机驱动、使能条件和常见 SG90 的工作电压。