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

目录

一、硬件准备与电路连接

1.1  核心硬件清单

1.2 接线方案表

1.3 具体接线图

二、安装与使用部分

三、系统代码讲解

3.1 系统主架构

3.2 电机驱动与速度测量

3.3 循迹控制

3.4 避障控制

3.5 Web服务器与控制界面

四、项目结果演示

4.1 操作流程

4.2 视频演示

五、DRV8833 电机驱动原理

5.1 工作原理

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 模式配置、任务优先级调度、多模式互斥执行,是连接各个子模块的桥梁

// 关键代码片段: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 电机驱动与速度测量

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

// 关键代码片段:中断服务程序 和 速度计算
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状态组合,看作一个“二进制数”,每种组合代表小车相对于黑线的一个特定姿态

// 关键代码片段:循迹动作决策
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)连续扫描机制:永不阻塞的环境感知

// 舵机扫描角度序列:右(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)决策算法:惯性权重防止摇摆

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)前进状态的分级响应

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数据

// 关键代码片段:处理前端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)

更多推荐