零知派——ESP32 智能小车控制系统(AP模式配置+网页控制)

在物联网与嵌入式技术快速发展的背景下,智能小车作为 robotics 入门与实践的经典项目,涵盖了传感器应用、电机控制、自动控制算法及网络交互等多个技术领域

目录

一、硬件准备与电路连接

[1.1 核心硬件清单](#1.1 核心硬件清单)

[1.2 接线方案表](#1.2 接线方案表)

[1.3 具体接线图](#1.3 具体接线图)

二、安装与使用部分

三、系统代码讲解

[3.1 系统主架构](#3.1 系统主架构)

[3.2 电机驱动与速度测量](#3.2 电机驱动与速度测量)

[3.3 循迹控制](#3.3 循迹控制)

[3.4 避障控制](#3.4 避障控制)

[3.5 Web服务器与控制界面](#3.5 Web服务器与控制界面)

四、项目结果演示

[4.1 操作流程](#4.1 操作流程)

[4.2 视频演示](#4.2 视频演示)

[五、DRV8833 电机驱动原理](#五、DRV8833 电机驱动原理)

[5.1 工作原理](#5.1 工作原理)

[5.2 速度控制](#5.2 速度控制)

六、常见问题解答(FAQ)


项目概述

本文详细介绍一款基于 ESP32 的智能小车系统实现,该系统具备三大核心功能:

  • 自动循迹:通过 5 路红外传感器识别路径,实现黑线 / 白线追踪
  • 智能避障:利用超声波传感器检测障碍物,结合舵机扫描实现自主避障
  • Web 远程控制:通过 WiFi 构建 Web 服务器,提供可视化控制界面与数据监控

项目难点及解决方案

问题描述:多任务并发与实时性

**解决方案:**主循环非阻塞设计、使用外部定时器中断测量速度、进行优先级划分

一、硬件准备与电路连接

1.1 核心硬件清单

组件 型号 / 规格 数量
主控芯片 ESP32 开发板 1
电机驱动 DRV8833 2
直流电机 5~12V 减速电机 4
循迹模块 5 路红外对管传感器 1
避障模块 HC-SR04 超声波传感器 1
舵机 SG90 180° 舵机 1
测速模块 对射式红外计数 + 20格码盘 1
电源 7.4V 锂电池 1
小车底盘 带安装孔的四轮底盘 1

1.2 接线方案表

以下所有引脚定义均直接来源于项目中的 config.h 文件

模块 接口/功能 ESP32 引脚 (GPIO) 对应代码宏定义 备注
电机驱动 (左前轮) A相 (PWM) 25 MOTO_LF_A 连接至DRV8833的INA1
电机驱动 (左前轮) B相 (PWM) 26 MOTO_LF_B 连接至DRV8833的INB1
电机驱动 (右前轮) A相 (PWM) 13 MOTO_RF_A 连接至DRV8833的INA2
电机驱动 (右前轮) B相 (PWM) 12 MOTO_RF_B 连接至DRV8833的INB2
电机驱动 (左后轮) A相 (PWM) 32 MOTO_LR_A 连接至另一DRV8833的INA1
电机驱动 (左后轮) B相 (PWM) 33 MOTO_LR_B 连接至另一DRV8833的INB1
电机驱动 (右后轮) A相 (PWM) 14 MOTO_RR_A 连接至另一DRV8833的INA2
电机驱动 (右后轮) B相 (PWM) 27 MOTO_RR_B 连接至另一DRV8833的INB2
测速模块 信号输出 34 PIN_SPEED_SENSOR 连接至右后轮测速传感器
循迹模块 传感器1 (最左) 17 PIN_TRACE_1 数字输入
循迹模块 传感器2 16 PIN_TRACE_2 数字输入
循迹模块 传感器3 (中间) 4 PIN_TRACE_3 数字输入
循迹模块 传感器4 2 PIN_TRACE_4 数字输入
循迹模块 传感器5 (最右) 15 PIN_TRACE_5 数字输入
超声波模块 Trig (触发) 23 SR04_TRIG 输出
超声波模块 Echo (回响) 22 SR04_ECHO 输入
舵机 信号线 21 SERVO_PIN PWM控制
风扇 (可选) 控制引脚 5 FAN_PIN 高低电平控制

1.3 具体接线图

确保电池负极、ESP32的GND、两个DRV8833模块的GND、以及所有传感器模块的GND连接在一起,保证所有信号共地

在扩展板上直插零知 ESP32主控板 (图中的⑥,注意USB接口朝外)及电机驱动模块(图中的⑦,注意方向)。

**接线所示:**①电池接口、②循迹模块接口、③超声波模块接口、④测速模块接口、⑤电机接口

**舵机模块:**接线为小车扩展板上丝印标有舵机接口的P4引脚上(黄色接线对准PWM方向)

**模块所示:**⑧循迹模块、⑨超声波-舵机模块

请注意:电机接线顺序如图所示⑤,A电机引脚对应A接口,其他电机引脚一一对应;锂电池接线到小车扩展板时,正负极不可反接,扩展板上标+的丝印接红线,-接黑线,反接会烧毁扩展板和 ESP32

二、安装与使用部分

2.1 开源平台-输入"智能小车控制系统"并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、系统代码讲解

深入剖析电机驱动(SpeedDrive)、避障控制(AvoidControl)、循迹控制(TraceControl)、Web服务器(WebHandler)核心代码文件,理解它们是如何协同工作

3.1 系统主架构

整个系统的入口和调度中心,负责所有模块的初始化、AP 模式配置、任务优先级调度、多模式互斥执行,是连接各个子模块的桥梁

cpp 复制代码
// 关键代码片段:setup() 和 loop()
void setup() {
    // ... 初始化串口
    CarDrive.begin();   // 初始化电机与测速
    Tracer.begin();     // 初始化循迹传感器
    Avoider.begin();    // 初始化超声波与舵机
    WebSys.begin(AP_SSID, AP_PASSWORD); // 启动AP模式和Web服务器
}

void loop() {
    WebSys.loop();      // 高优先级:处理Web请求,保证界面响应
    
    CarDrive.loop();    // 后台任务:每100ms计算一次速度
    
    // 定时执行模式控制逻辑
    if (millis() - lastModeCheck >= MODE_CHECK_INTERVAL) {
        SysMode currentMode = WebSys.getMode();
        // 互斥地执行当前模式
        switch (currentMode) {
            case MODE_TRACE:
                Avoider.stop();  // 确保避障模式关闭
                Tracer.run();    // 运行循迹
                break;
            case MODE_AVOID:
                Tracer.stop();   // 确保循迹模式关闭
                Avoider.run();   // 运行避障
                break;
            case MODE_MANUAL:
                Tracer.stop();   // 停止所有自动模式
                Avoider.stop();
                break;
        }
        lastModeCheck = now;
    }
    delay(1); // 微小延时,喂狗
}

通过 millis() 和状态机思想,实现了多任务的准并行处理;switch 语句中,进入一种自动模式前,会先停止另一种,这是解决模式冲突的关键,确保了系统的行为确定性

3.2 电机驱动与速度测量

基于测速码盘 + 外部中断实现脉冲计数,通过滤波算法计算出小车的实时移动速度

cpp 复制代码
// 关键代码片段:中断服务程序 和 速度计算
void IRAM_ATTR SpeedDrive::isrHandler(void* arg) {
    SpeedDrive* obj = (SpeedDrive*)arg;
    unsigned long now = micros();
    if (now - obj->lastIsrTime > 1000) { // 1ms 硬件去抖
        obj->pulseCount++;
        obj->lastIsrTime = now;
    }
}

void SpeedDrive::loop() {
    if (now - lastCalcTime >= SPEED_CALC_INTERVAL) { // 100ms周期
        // ... 读取脉冲计数并清零 ...
        if (raw >= MIN_PULSE_FOR_SPEED) {
            // EMA滤波: 新滤波值 = α * 新脉冲 + (1-α) * 旧滤波值
            filteredPulse = (EMA_ALPHA * raw) + ((1.0 - EMA_ALPHA) * filteredPulse);
            // 计算速度: 滤波脉冲 * (每秒周期数) * 每脉冲距离
            float newSpeed = (filteredPulse * (1000.0 / SPEED_CALC_INTERVAL)) * CM_PER_PULSE;
            // 二次平滑用于显示
            smoothedSpeed = (0.7 * newSpeed) + (0.3 * smoothedSpeed);
        } else { /* 脉冲过少,速度衰减 */}
    }
}

isrHandler 是中断服务程序,IRAM_ATTR 将其放入RAM中加速执行,1ms的去抖动逻辑有效防止了机械抖动

3.3 循迹控制

采用传感器编码:将5个传感器的0/1状态组合,看作一个"二进制数",每种组合代表小车相对于黑线的一个特定姿态

cpp 复制代码
// 关键代码片段:循迹动作决策
void TraceControl::executeAction(const SensorData& sensors) {
    int activeCount = sensors.s1 + sensors.s2 + sensors.s3 + sensors.s4 + sensors.s5;

    if (activeCount >= 4) { // 十字路口或粗线
        CarDrive.run(TRACE_SPEED_SLOW, TRACE_SPEED_SLOW); // 减速直行
    } else if (activeCount == 0) { // 完全丢线
        CarDrive.stop(); // 停车等待
    } else if (!sensors.s1 && !sensors.s2 && sensors.s3 && !sensors.s4 && !sensors.s5) {
        CarDrive.run(TRACE_SPEED_FAST, TRACE_SPEED_FAST); // 完美居中,快速直行
    } else if (!sensors.s1 && sensors.s2 && sensors.s3 && !sensors.s4 && !sensors.s5) {
        CarDrive.run(TRACE_SPEED_MEDIUM, TRACE_SPEED_FAST); // 轻微左偏,左慢右快
    } else if (sensors.s1) { // 严重左偏
        CarDrive.run(-TRACE_SPEED_REVERSE, TRACE_SPEED_TURN); // 左轮反转,右轮正转,原地修正
    }
    // ... 其他更多状态判断
}

循迹的核心是差速转向,采用查表式逻辑,轻微偏航减慢偏离侧轮子速度加速另一侧;严重偏航执行更激进的修正,一正一反原地转向

3.4 避障控制

采用了连续扫描 + 状态机 + 惯性决策的架构,使避障动作更加流畅、自然

(1)连续扫描机制:永不阻塞的环境感知

cpp 复制代码
// 舵机扫描角度序列:右(30°)→中(90°)→左(150°)→中(90°)→循环
const int AvoidControl::SWEEP_ANGLES[4] = {30, 90, 150, 90};

void AvoidControl::updateSweep() {
    unsigned long now = millis();
    if (now - lastSweepTime < AVOID_SWEEP_STEP_MS) return; // 时间未到,不扫描

    servoWrite(SWEEP_ANGLES[sweepStep]);   // 转到目标角度(内含动态延时)
    int d = getDistance();                  // 测距(舵机已到位)

    // 将距离存入对应的方向变量
    int angle = SWEEP_ANGLES[sweepStep];
    if (angle <= 45)      distRight  = d;
    else if (angle >= 135) distLeft   = d;
    else                  distCenter = d;

    Serial.printf("[扫描] 角度=%3d°  左=%3dcm 中=%3dcm 右=%3dcm\n",
                  angle, distLeft, distCenter, distRight);

    sweepStep = (sweepStep + 1) % 4;       // 步进到下一个角度
    lastSweepTime = now;
}

AVOID_SWEEP_STEP_MS控制扫描节奏,updateSweep() 在每次 run() 中被调用,但只有间隔足够时才真正执行舵机转动和测距,其余时间立即返回,确保主循环流畅

(2)决策算法:惯性权重防止摇摆

cpp 复制代码
AvoidControl::AvoidState AvoidControl::decideTurnState() {
    // ... 先判断三方皆堵,返回 STATE_BACKING ...

    // 惯性加成:上次方向加10cm权重,防止来回摇摆
    int scoreLeft  = distLeft  + (lastTurnDir == -1 ? 10 : 0);
    int scoreRight = distRight + (lastTurnDir == +1 ? 10 : 0);

    if (scoreLeft >= scoreRight) {
        lastTurnLeft = true;
        lastTurnDir  = -1;   // 记录本次转向方向
    } else {
        lastTurnLeft = false;
        lastTurnDir  = +1;
    }
    return STATE_TURNING;
}

当左右两侧距离相近时,小车容易频繁摇摆。通过给上一次选择的方向额外增加10cm的虚拟"偏好分",使小车倾向于继续沿原方向转弯,从而走出平滑弧线

(3)前进状态的分级响应

cpp 复制代码
void AvoidControl::handleForward() {
    int dist = distCenter;   // 直接使用连续扫描的最新中央距离

    if (dist < AVOID_DISTANCE_EMERGENCY) {      // 20cm以内:紧急停车并决策
        CarDrive.stop();
        currentState = decideTurnState();
        stateStartTime = millis();
    }
    else if (dist < AVOID_DISTANCE_SLOW) {      // 20~35cm:减速区,停车决策
        CarDrive.stop();
        currentState = decideTurnState();
        stateStartTime = millis();
    }
    else if (dist < AVOID_DISTANCE_SAFE) {      // 35~55cm:预警区,慢速行驶并微调方向
        if (distLeft > distRight + 20)          // 左侧空间大,轻微左偏
            CarDrive.run(AVOID_SPEED_SLOW - 20, AVOID_SPEED_SLOW);
        else if (distRight > distLeft + 20)     // 右侧空间大,轻微右偏
            CarDrive.run(AVOID_SPEED_SLOW, AVOID_SPEED_SLOW - 20);
        else
            CarDrive.run(AVOID_SPEED_SLOW, AVOID_SPEED_SLOW);
    }
    else {                                       // >55cm:安全距离,全速前进
        CarDrive.run(AVOID_SPEED_NORMAL, AVOID_SPEED_NORMAL);
    }
}

侧方距离明显不均衡时,通过差速实现柔和的方向修正,避免到跟前才急转弯;保证小车在进入障碍密集区时先停下来思考,而不是盲目冲入

各状态之间的转换关系如下图所示:

3.5 Web服务器与控制界面

前后端分离:WebHandler 扮演了后端服务器的角色、前端网页通过 fetch API定期向ESP32发起AJAX请求,获取最新的速度、风扇状态等JSON数据

cpp 复制代码
// 关键代码片段:处理前端AJAX请求
void WebHandler::handleData() {
    float speed = CarDrive.getSmoothedSpeed(); // 获取平滑后的速度
    updateSpeedHistory(speed); // 更新历史数据用于图表

    // 构建JSON响应
    String json = "{";
    json += "\"speed\":" + String(speed, 2);
    json += ",\"fan\":" + String(fanState ? "true" : "false");
    json += ",\"validSpeeds\":["; // 添加最近5次有效速度
    for (int i = 0; i < VALID_SPEED_COUNT; i++) {
        if (i > 0) json += ",";
        json += String(validSpeeds[i], 2);
    }
    json += "]";
    if (logBuffer.length() > 0) { // 添加日志
        json += ",\"log\":\"" + logBuffer + "\"";
        logBuffer = "";
    }
    json += "}";
    server.send(200, "application/json", json);
}

前端点击"导出CSV"时,会将累积在隐藏textarea中的时间-速度数据保存为 .csv 文件,实现了简易的数据记录功能

系统流程图

展示了整个系统的软件运行流程和模块间的关系

四、项目结果演示

4.1 操作流程

连接电池后,小车扩展板电源灯亮起,ESP32开始自建热点 ESP32_SmartCar。手机/电脑连接该热点,输入密码 12345678

AP模式下请关闭移动数据网络,部分设备会自动切换回4G导致无法访问控制台

(1)访问控制台: 打开浏览器,在地址栏输入 192.168.4.1,进入智能小车控制台界面

**(2)功能测试:**点击"智能避障"按钮,观察小车行为

①连续扫描:舵机以约2.5秒一个循环(4步×400ms)持续左右摆动,但不会停顿,小车依然在前进。

②平稳转向:接近障碍时,小车根据左右空间惯性选择方向,流畅转弯,无"犹豫不决"现象。

③死胡同脱困:将小车困在角落,它会先后退约0.9秒,然后原地掉头,朝空旷方向驶出

打开浏览器控制台(F12 -> 网络),可以查看 /data 接口每500ms返回的JSON数据,其中包含实时距离和日志

**(3)数据导出:**避障过程中,点击"导出CSV"可获得包含时间戳、速度、风扇状态的完整日志,便于分析算法表现

4.2 视频演示

零知ESP32 AP模式智能小车实战演示:网页控制、实时测速、自动循迹与避障

本视频完整展示了基于零知ESP32的智能小车在AP模式下的操作流程。演示如何连接小车自建热点、通过浏览器访问控制台。三种核心模式:手动驾驶的即时响应、自动循迹的精准过弯,以及智能避障模式下的自主环境探测与路径决策。实时刷新的速度曲线和数据导出功能也清晰可见

五、DRV8833 电机驱动原理

DRV8833内部集成了双路H桥驱动器 。一个H桥由四个开关(MOSFET)组成,通过控制它们的导通组合,可以改变电机两端的电压方向和大小,从而控制电机的正转、反转、刹车和滑行

5.1 工作原理

如原理图所示,当INA为高电平(PWM )、INB为低电平时,电流从左向右流过电机(正转)。反之,INA低、INB高,电流反向,电机反转

正转:AIN1=PWM,AIN2=LOW、反转:AIN1=LOW,AIN2=PWM

5.2 速度控制

在一个周期内快速切换开关的导通时间(即PWM波),可以改变电机两端的平均电压,从而调节转速

PWM的占空比越高,平均电压越大,电机转得越快

H桥电路逻辑

六、常见问题解答(FAQ)

Q1: 小车在循迹模式下总是冲出赛道,应该如何调整?

*A:这通常是转向速度过快或响应不够及时。*可以尝试:降低 config.h 中 TRACE_SPEED_FAST 和 TRACE_SPEED_MEDIUM 的值。检查循迹模块的高度和灵敏度,确保它能在黑白线上正确输出高低电平

Q2: 避障模式下,小车总是撞到障碍物才停下,怎么办?

*A:首先检查超声波传感器的安装是否牢固、朝向是否正前方。*可以在代码中临时添加串口打印,输出 getDistance() 的值,判断测距是否准确。如果测距准确,说明避障的阈值过小,可以适当增大 AVOID_DISTANCE_EMERGENCY 和 AVOID_DISTANCE_SLOW 的值

Q3: 测速显示始终为0,或者数值严重不准确?

A:这通常是硬件或接线问题。请检查:测速传感器的VCC和GND是否正确连接、测速传感器的信号线是否连接到正确的GPIO、码盘是否能顺畅地通过传感器的U型槽,且遮挡效果明显、在代码中临时添加中断计数打印,看是否有脉冲产生。如果没有,可能是传感器损坏或接线错误

项目资源整合:

DRV8833 数据手册: DRV8833 Dual H-Bridge Motor Driver datasheet (Rev. E)