通过点灯科技云平台实现小车的控制

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

295.13.通过点灯科技云平台实现小车的控制

#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 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 毫秒。
}                                       // 结束舵机控制函数。
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(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 发来的指令。
    if (Blinker.connected() && !setup_camera) // Blinker 已连接且摄像头服务尚未启动时进入。
    {                                   // 开始首次启动摄像头服务的分支。
        setupCamera();                  // 调用配套文件中的摄像头及本地视频流服务初始化。
        setup_camera = true;            // 记录已经初始化,避免以后每轮重复调用。
        Blinker.printObject("video", "{\"str\":\"mjpg\",\"url\":\"http://"+ WiFi.localIP().toString() + "\"}"); // 向 video 组件发送 MJPG 格式和本机局域网 URL。
    }                                   // 结束首次启动摄像头服务的分支。
}                                       // 结束本轮循环,随后再次进入 loop。

一、通过点灯科技云平台实现小车的控制

本节承接第 293 节已经完成的 ESP32-CAM 视频、LED 和舵机程序,并把第 294 节的 L298N 方向控制原理写成五个运动函数,再用五个 Blinker 按键远程调用。上方仅收录课堂主草图业务代码;Blinker.h 和 ESP32_CAM_SERVER.h 的库内实现没有抄入。代码保持视频最终版本的函数名、状态字符串、电平组合和 delay(100),并逐行补充初学者注释。

1.1 L298N电机驱动原理

  • L298N 含有两组全桥:A 通道由 ENA、IN1、IN2 控制,B 通道由 ENB、IN3、IN4 控制。本课保留 ENA、ENB 跳帽,使两个通道一直处于使能状态;程序只改变四个 IN 引脚的数字电平,不包含 PWM 调速。
  • ESP32-CAM 的 GPIO 只给出逻辑控制信号,电机的大电流经过 L298N 的功率桥和电机电源,不能把直流电机直接接到 GPIO。主控、驱动板与电机电源还必须共地,否则 L298N 无法可靠判断 GPIO 的高、低电平。
  • GoForward()、GoBack()、Left()、Right()、Stop() 是对四次 digitalWrite() 的封装。调用者不必每次重写四路电平,也能避免只改一侧电机而遗留上一动作状态。
  • digitalWrite() 设置的是持续保持的输出状态,不是一个瞬间脉冲。函数返回后,L298N 仍按最后写入的组合驱动电机,直到后续函数再次改写这些 GPIO、芯片复位或掉电。

1.2 直流电机旋转方式

在课堂当前电机安装方向和 OUT 接线下,五个动作对应关系如下。1 表示逻辑高电平,0 表示逻辑低电平,不表示 1 V 与 0 V。

函数IN1IN2IN3IN4A 通道B 通道整车动作
GoForward()1010正向正向前进
GoBack()0101反向反向后退
Left()1001正向反向原地左转
Right()0110反向正向原地右转
Stop()0000快速停止快速停止停止

“正向”只是本课为当前接线规定的方向,不是电机的绝对属性。若某侧电机的两根 OUT 线对调,或左右通道接反,同一代码组合可能表现为后退或相反方向转弯;应断电后检查布线,而不是把错误接线当作固定真值表。

1.3 引脚初始化

课堂使用的真实映射是:

L298N 输入ESP32-CAM 引脚宏定义
IN1GPIO13#define IN1 13
IN2GPIO15#define IN2 15
IN3GPIO14#define IN3 14
IN4GPIO2#define IN4 2

原笔记中的“13、14、35”“L1~L4”和“ESP32 的三个 CAN 口”都是识别或理解错误:这里是四个 IN1~IN4 数字输入,对接 ESP32-CAM 的四个 GPIO。pinMode(INx, OUTPUT) 必须先把相应 GPIO 配成输出,随后 digitalWrite() 才是在主动输出电平。

视频最终编译、上传的 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.4 前进功能实现

  • GoForward() 依次输出 1、0、1、0,两个通道都按课堂定义的正向旋转,小车前进。
  • GoBack() 依次输出 0、1、0、1,同时翻转两个通道的输入极性,小车后退。后退不是修改宏定义实现的;宏只固定“哪个 GPIO 叫 IN1~IN4”,动作函数才负责写入不同电平。
  • 两个函数每次都改写全部四路输入,因此从转向直接切换到前进或后退时,不会继续保留上一动作的某一路方向电平。

1.5 停止功能实现

Stop() 把 IN1~IN4 全部写成 LOW。在本课 ENA、ENB 跳帽保持高电平的前提下,每个桥的两路输入同为 0,L298N 数据手册将这种状态归为快速停止,即电机两端被置于同一电位而产生电气制动。它不同于把 ENA/ENB 拉低后的高阻释放、依靠惯性滑行停止;实车仍会因速度、轮胎和地面状况产生少量停止距离。ST L298 数据手册

原笔记中的“停止分装函数”应为“停止封装函数”。这里的封装只是把四条停止语句集中到 Stop(),并不是另一个软件包或“分装”步骤。

1.6 左转功能实现

Left() 输出 1、0、0、1:A 通道按正向转,B 通道按反向转。两侧轮子反向旋转,使小车以两轮之间附近的位置为中心原地转向。课堂把该动作称为左转;实物若向右转,应先核对 Motor A/B 对应哪一侧、各侧电机极性是否与上一节一致。

在 button4_callback() 中,程序不是一直调用 Left(),而是写入一次左转组合,阻塞等待 100 ms,再调用 Stop()。持续旋转的原因是 GPIO/L298N 会保持前一次写入的状态,不是回调函数在内部“无限循环执行”。

1.7 右转功能实现

Right() 输出 0、1、1、0,正好与左转的两侧方向相反:A 通道反向、B 通道正向。原笔记的“n1n2 为底高、n2n3 为高低”应更正为:IN1/IN2 为低/高,IN3/IN4 为高/低。

右转回调也只维持约 100 ms 后主动调用 Stop()。这个时间不是固定转角:电池电压、电机差异、轮胎附着力、车重和地面都会改变一次动作的实际角度,因此这里只能称为开环的短促转向。

1.8 通过点灯科技控制小车

本节在原有 Button1("led") 后增加 Button2~Button6,避免覆盖 LED 按键对象。每个对象包含一个用于通信的组件键名,再用 attach() 绑定自己的回调:

App 按键事件
    ↓ 按组件键名匹配
BlinkerButton 对象
    ↓ attach() 注册的回调
button2_callback()~button6_callback()
    ↓ 调用动作函数
ESP32-CAM GPIO13/15/14/2
    ↓
L298N IN1~IN4 → 两侧电机
  • 官方 Button Wi-Fi 示例把轻触、按下和松开作为不同状态处理,并在 setup() 中用 attach() 注册回调。本课按视频源码直接比较字符串 "tap"、"press"、"pressup",没有擅自换成官方示例里的状态宏。Blinker 官方 Button Wi-Fi 示例
  • 前进和后退回调在 press 或 tap 时写入运动电平,在收到 pressup 时停止;停止键在这三类事件中都调用 Stop()。串口日志会打印实际收到的 state,如果操作方式与预期不同,应先看日志,不应凭中文名称猜测事件。
  • 左转、右转在 press 或 tap 分支内执行“转向 → delay(100) → 停止”,随后松开事件又停止一次。Arduino 的 delay() 参数单位为毫秒,所以这里是 100 ms,不是转录中可能误听的 100 μs。Arduino delay() 参考
  • delay(100) 是阻塞等待:期间当前回调不能返回,主循环也不能继续调用 Blinker.run()。本例等待很短且视频测试成功,但它不是实时转向控制;后续若增加更多联网任务,应考虑用 millis() 做非阻塞计时,而本稿不改写课堂源码。
  • slider0_callback()、Myservo()、摄像头启动逻辑和 LED 回调来自前面的连续课程,本节继续保留,所以小车按键不是替代摄像头、灯或舵机,而是加入同一设备页面的新控制通道。
1.8.1 代码验证
  1. 在 Arduino IDE 中选择实际使用的 ESP32-CAM 开发板和串口,使用外置 USB 转串口器时按前课流程让 GPIO0 在复位/上电时临时接 GND,进入下载模式;上传结束后断开 GPIO0—GND 跳线并再次复位,才能正常启动应用。Espressif ESP32 下载模式说明
  2. 视频完成编译和上传,随后复位开发板。上方代码保留视频公开出现的 auth、ssid、pswd 只为忠实记录;自己的设备必须替换为自己的凭据,真实密钥和密码不应提交到公开仓库。
  3. 上电后应先按“停止”,再短按前进/后退并准备断电。若动作相反,先断开电池,再核对左右电机及 OUT 接线;不要在电机运行或带电时拔插端子。
  4. 左右转各执行约 100 ms 后自动停止。需要校准转向幅度时可小步调整延时并在空旷地面测试,但这会是对课程程序的后续参数调整,不代表 100 ms 对所有小车都是某个固定角度。
  5. 视频末尾依次验证了前进、停止、后退、停止、左转和右转,LED 开关也仍然有效;这能证明本次实物上的按键键名、回调注册与四路方向输出已连通,不能据此推断不同电池、地面和接线下也会得到完全相同的速度与转角。

1.9 小车接线

本节视频实物接线应按端子丝印逐根核对:

起点终点作用
ESP32-CAM GPIO13L298N IN1A 通道方向输入 1
ESP32-CAM GPIO15L298N IN2A 通道方向输入 2
ESP32-CAM GPIO14L298N IN3B 通道方向输入 1
ESP32-CAM GPIO2L298N IN4B 通道方向输入 2
ESP32-CAM GNDL298N GND建立共同的 0 V 参考点
ESP32-CAM 5VL298N 模块 +5V 节点视频所用模块条件下的 5 V 供电连接
  • 原笔记的“13→12→……→2 依次向下接入”是错误的。四根方向线只用 GPIO 13、15、14、2,并且必须按 IN1~IN4 的对应表接,不能凭开发板针脚排列顺序推断。
  • 第 294 节视频保留 L298N 模块的板载 5 V 稳压跳帽,并在其适用输入范围内由模块 +5V 节点给 ESP32-CAM 供电。不同兼容板的稳压器和跳帽逻辑可能不同;上电前必须根据手中模块说明与 PCB 丝印确认该端此时是输出还是输入,并用万用表核对极性、电压。摄像头与 Wi-Fi 可能产生电流峰值,若反复重启或视频断流,应检查电池压降和稳压能力,必要时使用电流能力足够的独立 5 V 降压电源,但仍需共地。Ai-Thinker ESP32-CAM 原理图
  • ENA、ENB 跳帽需保持安装,才能符合本节“只用 IN1~IN4 控制方向”的前提;若取下跳帽却没有另接高电平或 PWM,使能端未开启时电机不会按表格动作。
  • GPIO13、15、14、2 与 ESP32-CAM 的 microSD/SDIO 线路复用,本课占用它们控制电机后,不能直接同时使用 microSD 四线模式。GPIO2、GPIO15 还是启动配置脚,外接 L298N 若在复位瞬间影响其电平,可能造成烧录或启动异常;遇到问题先断电、暂时拔下四根 IN 控制线排查。Espressif SDIO 引脚表、ESP32 启动模式说明
  • 改线前先断开电池,避免电机端短路、正负极接反以及 USB 转串口器与 L298N 的 5 V 电源互相反向供电。GPIO 只能接 L298N 的逻辑输入,绝不能接电机电源或电机输出端。

1.10 点灯科技云平台配置

视频在设备页面进入编辑模式,新增五个普通按键组件,并把它们摆成上、下、左、右和中央停止的方向控制区。真正参与程序匹配的是“组件键名”,不是 C++ 对象名、显示中文或图标名称:

功能程序对象App 组件键名(必须精确匹配)视频最终显示文本建议图标
前进Button2GoForward前进上箭头
后退Button3GoBack后退下箭头
左转Button4Left左转左箭头
右转Button5Right右转右箭头
停止Button6Stop停止停止/暂停符号

组件键名区分大小写,应逐字与 BlinkerButton ButtonX("...") 中的字符串一致。Button2~Button6 只是草图内变量名,不必写进 App;中文显示文本、图标和颜色则只影响界面表达。

1.10.1 文本标签修改
  1. 向页面加入五个按键,分别打开编辑页。
  2. 按上表填写 GoForward、GoBack、Left、Right、Stop 组件键名。视频配置时曾发现一处名称填错并返回修正,这也说明通信依赖键名精确匹配。
  3. 把“显示文本1”分别改为“前进、后退、左转、右转、停止”。这些中文可以改成其他易懂文字,不会改变程序匹配;若修改的是组件键名,就必须同步修改代码字符串并重新烧录。
  4. 保存组件与页面布局,等待设备上线后再测试。原有摄像头、LED 和舵机组件仍留在同一页面。
1.10.2 图标修改
  • 图标库中可分别选择上、下、左、右箭头与停止/暂停类图标;颜色和尺寸也可按界面需要调整。视频最终页面用方向图形直观表示动作。
  • 原笔记所说“图标命名必须与代码一致”不正确:图标是视觉资源,不参与 BlinkerButton 的键匹配。必须一致的是组件键名。
  • “go for walk”也是转录误识别。程序前进键名是大小写准确的 GoForward;后退、左转、右转、停止依次为 GoBack、Left、Right、Stop。
1.10.3 控制效果展示
  • 点击“前进”后,Button2 回调调用 GoForward();收到松开事件或点击“停止”后调用 Stop()。
  • “后退”走 Button3 → GoBack();“左转”和“右转”各执行约 100 ms 的反向轮转后主动停止。
  • 视频实测前进、停止、后退、左转、右转均有响应,并再次操作了 LED 按键,证明新增五个按键没有覆盖 Button1("led")。
  • App 显示已响应不等于电机供电一定正常。若串口能看到按键状态但车轮不动,应依次检查:组件键名、回调注册、GPIO 电平、IN1~IN4 顺序、ENA/ENB 跳帽、共地、电机电源及端子接触。

核对依据

  • 本地 videos/295.13.通过点灯科技云平台实现小车的控制.mp4 中 Arduino IDE 最终代码、编译/上传、接线、App 五个按键配置及实车演示;对照 transcripts/295.13.通过点灯科技云平台实现小车的控制.srt、原始 notes/md/295.13.通过点灯科技云平台实现小车的控制.md,并与第 293 节累积草图、第 294 节 L298N 接线和真值表交叉核对。
  • Blinker 官方 Button Wi-Fi 示例与Blinker Arduino 组件接口:核对按键的轻触、按下、松开状态分类、回调注册与 Blinker.run() 处理方式。
  • ST L298 数据手册:核对双全桥结构、方向输入、使能条件和快速停止;Arduino delay() 参考:核对 delay(100) 的毫秒单位。
  • Ai-Thinker ESP32-CAM 原理图、Espressif SDIO 引脚表与启动模式说明:核对电源连接、GPIO2/13/14/15 的 microSD 复用与 GPIO2/GPIO15 的启动注意事项。