
1. 项目概述与整体设计思路1.1 为什么用ESP32做智能车主控做智能车这件事很多入门玩家第一反应是51单片机或者STM32。但如果你问我现在想做一个支持无线遥控、以后还想扩展摄像头图传、甚至接入物联网平台的智能车我基本不会推荐传统单片机——ESP32才是更合适的选择。原因很简单ESP32自带Wi-Fi和蓝牙双模主频跑到240MHz价格却压到了和普通STM32F103差不多的水平。这意味着你不需要额外买一块蓝牙模块或者Wi-Fi模块直接在代码里初始化无线功能就能实现手机遥控。对于智能车这种对实时性有一定要求、但又没那么苛刻的场景ESP32的性能完全够用而且开发效率高得多。另一个核心优势是生态。ESP32可以用Arduino框架开发也能用ESP-IDF做更底层的事情。如果你是初学Arduino框架下写MPU6050、OLED、超声波、电机驱动的库一抓一大把基本属于“把库拉进来、填几行参数就能跑”的级别。我这次做的小车就是用Arduino框架完整代码几百行就能搞定不用啃寄存器手册。结合搜索热词里频繁出现的“esp32接入米家mesh”“esp32-cam手机远程监控”“ros 2 micro-ros esp32”你会发现ESP32这条技术线从入门小车做到智能家居、机器视觉、机器人操作系统延展性非常强。这个小车做完之后后面想加摄像头做视觉循迹或者接ROS做SLAM硬件上都不用换主控。1.2 这个项目能做什么适合谁来参考简单说这辆小车实现了三个核心功能手机网页遥控、PWM调速转向、辅助测速显示。手机不需要安装任何App只要和ESP32连到同一个路由器打开浏览器输入IP地址就能看到控制面板按键控制小车前进、后退、左转、右转、加速、减速。它的使用场景不仅是“做个玩具”。这套东西你可以直接用在大学生创新创业项目里可以作为物联网课程的实践作品也可以作为ROS机器人的底层运动平台。和我之前做的红外遥控小车相比ESP32方案真正的优势在于遥控距离不再是几米而是整个Wi-Fi覆盖范围后续扩展OTA升级、Web配置界面、手机App都有了现成基础。适合参考这份教程的人群有三类电子类专业但动手经验不多的大学生对Arduino有一定了解、想做第一个无线项目的爱好者以及想把小车作为算法平台、但不想在底层硬件上耗太多时间的玩家。如果你连LED点灯都没做过建议先去跑一遍Arduino基础例程再回来别一上来就啃Wi-Fi通讯。2. 硬件选型与原理剖析2.1 元器件清单与选型理由先看物料清单。我做这辆车用到的核心硬件如下部件型号/规格数量说明主控ESP32 DevKit v138pin1Wi-Fi/蓝牙双模IO充足电机驱动DRV8833模块1双通道2.7-10.8V电流1.5A/通道直流电机3-6V N20减速电机2带编码器版本可选扭矩够用车底盘亚克力小车底盘套件1含轮子、万向轮、电池盒电池18650锂电两节7.4V2或用5V移动电源升压模块稳压AMS1117-3.3或MP15841给ESP32供电测速可选槽型光电传感器2配合码盘测速其他杜邦线、面包板、开关、电阻若干尽量用短杜邦线选DRV8833而不是L298N是我踩过坑之后的决定。L298N虽然经典但压降大、发热明显一个模块得吃掉2V左右的压降用两节18650满电8.4V供电到电机端实际只有6V多速度一快电压就往下掉甚至会导致ESP32重启。DRV8833是MOSFET方案压降小得多体积只有拇指大小PWM频率可以拉到较高水平驱动N20这种小电机绰绰有余。电机选择方面N20减速电机带编码器版本大概十几块钱一个转速比普通TT电机稳力矩也够。如果测试中用TT电机替换也完全可以代码不用改只是测速部分需要用码盘去适配孔距。2.2 供电方案最容易翻车的地方供电是整个项目里最容易翻车的地方没有之一。ESP32的工作电流在Wi-Fi开启时能瞬间冲到300mA以上电机启动瞬间的电流更是可以达到正常工作电流的两三倍。如果电源设计有问题表现就是小车一动ESP32就重启或者Wi-Fi连接一切正常但电一加速就掉线。我推荐两个方案方案A我实际用的两节18650串联得到7.4V直接给DRV8833供电同时用MP1584降压模块转出5V然后再经过ESP32板载的AMS1117降出3.3V给芯片使用。这样电机和主控电源从源头分开电机的大电流波动不会直接打到ESP32的供电上。方案B更省事用两个电源电池盒给电机和驱动板供电同时用一个5V移动电源给ESP32单独供电。这个方案最简单也最稳缺点就是车身上要多带一块充电宝重量增加。这里有一个关键点必须说清楚很多人习惯把驱动板的5V输出直接接到ESP32的5V引脚这在电池电压较低时勉强能跑但一旦电机调速导致电流波动ESP32的供电就会被拉垮。我建议在VIN引脚和GND之间并联一个470uF电解电容能明显改善电压跌落问题。这不是玄学是实测有效的。2.3 引脚分配避开默认冲突引脚ESP32的引脚资源和普通单片机有些不同它的GPIO大部分都能做输入输出但有一些引脚有默认功能不小心用了就是神坑。GPIO引脚默认/复用功能是否建议使用GPIO0下载模式低电平触发不推荐GPIO1/3UART0可作为普通IO但影响串口日志GPIO6-11SPI Flash绝对不能用GPIO12上电时电平影响Flash电压容易导致无法烧录不推荐GPIO15下载模式相关不推荐GPIO16-17无特殊推荐GPIO18/19/21/22推荐推荐GPIO25/26/27/32/33推荐推荐我用的分配方案是电机A右轮接GPIO25/26电机B左轮接GPIO32/33PWM通道选择0/1/2/3。GPIO26和GPIO32同时作为Wi-Fi天线相关的引脚并没有这个问题放心用。建议你在写代码之前先把引脚分配表画出来贴在桌上。不然等你把线全接好了发现两个PWM通道冲突或者某个引脚不能输出再拆线重接就非常痛苦。3. 完整代码设计与逐段解析3.1 整体程序框架整个程序用Arduino框架开发逻辑分为三层Wi-Fi连接层、Web控制层、电机驱动层。Wi-Fi连接层负责连接路由器并启动Web ServerWeb控制层监听浏览器发来的HTTP请求解析控制指令并调用电机驱动函数电机驱动层负责把指令翻译成PWM输出和方向信号。这个小车本质上是一个Web Server GPIO控制的组合。启动流程是上电 - 连接Wi-Fi - 启动Web Server - 循环接收控制请求。延时不放在主循环里全部靠server处理请求回调所以控制响应很快实测局域网内遥控延迟在50ms以内体感上属于“指哪打哪”的水平。3.2 核心代码实现#include WiFi.h #include WebServer.h // Wi-Fi配置 const char* ssid 你的WiFi名称; const char* password 你的WiFi密码; // 定义引脚 #define MOTOR_A_IN1 25 #define MOTOR_A_IN2 26 #define MOTOR_B_IN1 32 #define MOTOR_B_IN2 33 // PWM配置 #define PWM_FREQ 5000 #define PWM_RES 8 #define PWM_CH_A1 0 #define PWM_CH_A2 1 #define PWM_CH_B1 2 #define PWM_CH_B2 3 // 速度变量0-255 int speedValue 150; const int speedStep 30; WebServer server(80); void setup() { Serial.begin(115200); // 初始化PWM引脚 ledcSetup(PWM_CH_A1, PWM_FREQ, PWM_RES); ledcSetup(PWM_CH_A2, PWM_FREQ, PWM_RES); ledcSetup(PWM_CH_B1, PWM_FREQ, PWM_RES); ledcSetup(PWM_CH_B2, PWM_FREQ, PWM_RES); ledcAttachPin(MOTOR_A_IN1, PWM_CH_A1); ledcAttachPin(MOTOR_A_IN2, PWM_CH_A2); ledcAttachPin(MOTOR_B_IN1, PWM_CH_B1); ledcAttachPin(MOTOR_B_IN2, PWM_CH_B2); // 连接Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(); Serial.print(IP地址: ); Serial.println(WiFi.localIP()); // 配置Web路由 server.on(/, handleRoot); server.on(/action, handleAction); server.on(/speed, handleSpeed); server.begin(); Serial.println(Web服务器已启动); stopCar(); } void loop() { server.handleClient(); } // 控制函数 void handleRoot() { String html htmlheadmeta nameviewport contentwidthdevice-width, initial-scale1; html titleESP32遥控小车/title/headbody styletext-align:center;font-size:24px; html h1ESP32智能车遥控/h1; html p当前速度: String(speedValue) /p; html pa href/action?dirforwardbutton stylewidth:120px;height:60px;font-size:20px前进/button/a/p; html pa href/action?dirleftbutton stylewidth:120px;height:60px;font-size:20px左转/button/a ; html a href/action?dirrightbutton stylewidth:120px;height:60px;font-size:20px右转/button/a/p; html pa href/action?dirbackwardbutton stylewidth:120px;height:60px;font-size:20px后退/button/a/p; html pa href/action?dirstopbutton stylewidth:120px;height:60px;font-size:20px;background-color:red;color:white停止/button/a/p; html pa href/speed?dupbutton stylewidth:120px;height:60px;font-size:20px加速/button/a ; html a href/speed?ddownbutton stylewidth:120px;height:60px;font-size:20px减速/button/a/p; html /body/html; server.send(200, text/html, html); } void handleAction() { String dir server.arg(dir); if (dir forward) { forward(); } else if (dir backward) { backward(); } else if (dir left) { turnLeft(); } else if (dir right) { turnRight(); } else if (dir stop) { stopCar(); } server.send(200, text/plain, OK); } void handleSpeed() { String d server.arg(d); if (d up) { speedValue min(255, speedValue speedStep); } else if (d down) { speedValue max(0, speedValue - speedStep); } server.send(200, text/plain, String(speedValue)); } // 电机控制函数 void motorA(int speed, bool forward) { if (forward) { ledcWrite(PWM_CH_A1, speed); ledcWrite(PWM_CH_A2, 0); } else { ledcWrite(PWM_CH_A1, 0); ledcWrite(PWM_CH_A2, speed); } } void motorB(int speed, bool forward) { if (forward) { ledcWrite(PWM_CH_B1, speed); ledcWrite(PWM_CH_B2, 0); } else { ledcWrite(PWM_CH_B1, 0); ledcWrite(PWM_CH_B2, speed); } } void forward() { motorA(speedValue, true); motorB(speedValue, true); } void backward() { motorA(speedValue, false); motorB(speedValue, false); } void turnLeft() { motorA(speedValue, true); motorB(speedValue, false); } void turnRight() { motorA(speedValue, false); motorB(speedValue, true); } void stopCar() { ledcWrite(PWM_CH_A1, 0); ledcWrite(PWM_CH_A2, 0); ledcWrite(PWM_CH_B1, 0); ledcWrite(PWM_CH_B2, 0); }3.3 代码背后的原理PWM调速与方向逻辑这段代码里最关键的部分是电机控制函数。DRV8833的每个通道有两路输入IN1和IN2逻辑是IN1高电平IN2低电平电机正转反过来反转两个都低电机停止滑行两个都高电机制动刹车。实际操作中我用了PWM调速而不是简单的digitalWrite是因为数字高低电平只能控制电机转或者不转没法控制速度。用ledcWrite输出不同占空比的PWM波就能调节电机的平均电压实现“半油门”“全油门”的效果。在这里PWM的频率我设置的是5000Hz分辨率8位即0-255。如果你用L298NPWM频率不能太高不然驱动芯片跟不上但DRV8833的开关频率比较高5kHz完全没问题电机运行时噪音更小、线性度也更好。如果你用的是带编码器的N20电机想后续做闭环PID调速建议把PWM频率提升到10kHz或者更高编码器采样会更干净。方向控制上我的做法是原地转向左轮正转、右轮反转这样小车能原地掉头在室内狭窄空间非常实用。如果你想做平滑弧线转弯可以让两轮速度不一致差速转向比如左转时左轮速度减半、右轮速度保持小车的行进路线就是一个大弧线。代码里只需要把turnLeft()改成motorA(speedValue / 2, true); motorB(speedValue, true);即可。3.4 网页控制面板的“为什么这么写”很多第一次看到代码的人会好奇为什么网页控制居然是用/action?dirforward这样的URL来实现的这里解释一下最底层的原理。Web Server收到浏览器发来的HTTP请求时URL路径和查询参数都会带上。比如你点击“前进”按钮浏览器实际上向ESP32发出一个GET /action?dirforward的请求。ESP32上的handleAction函数通过server.arg(dir)取到forward这个值再对应的调用底层函数最后响应一个text/plain的“OK”。这种方法的本质是把ESP32当作一个微型网页服务器用HTTP GET请求来控制GPIO。它的好处显而易见任何能打开浏览器的设备都能遥控不管是手机、平板还是电脑完全不用装App、不用配对蓝牙。坏处是控制体验依赖于Wi-Fi网络的稳定性如果路由器比较远延迟会明显增加。如果你想让操作更流畅可以做下面的升级把HTTP请求改成WebSocket连接。WebSocket是长连接服务器可以主动推送数据控制延迟能从几十毫秒降到几毫秒而且可以做双向实时的数据回传比如测速数据实时刷新到页面上。不过WebSocket的代码复杂度会高一截初学阶段先用HTTP跑通整体流程后续再优化完全不迟。4. 组装过程与调试实战4.1 底盘与电机安装底盘安装看起来简单但实际上有几处影响整车性能的细节。我用的亚克力底盘配N20电机安装时需要注意三个点第一电机安装方向要对称。左右两个电机的轴心线必须处在同一条直线上否则车轮会出现“外八”或者“内八”的现象小车跑起来会向一侧偏。装完之后用手转动两个轮子感受阻力是否一致如果一边紧一边松先松螺丝微调。第二万向轮的安装高度很关键。万向轮太高小车的两个主动轮会悬空打滑严重太低底盘会前倾转弯阻力大。正确的调整方法是把小车放在桌面上用手按下底盘尾部主动轮应该能完全贴合桌面且没有明显滑动阻力。第三如果你装了测速码盘码盘和槽型光电传感器的位置要在安装电机之前就规划好。码盘离传感器太远会漏计数太近会摩擦到外壳。一般距离控制在1mm到2mm之间比较稳。4.2 接线检查清单接线是整个项目中最容易出错、也最伤硬件的一步。我在装机调试过程中吃过不少亏总结出一个铁律接线完成之后、上电之前必须做一遍完整检查。不要嫌麻烦这能帮你省下至少两小时的排障时间。检查项检查方法错误后果电源正负极万用表通断档确认接反必烧板ESP32和驱动板共地确认GND已相连控制信号无效电机接线顺序左右轮分别测试方向反PWM引脚是否冲突对照引脚分配表无输出或烧引脚电池电压万用表实测供电不足系统频繁重启稳压模块输出电压空载实测5V/3.3V电压过高烧芯片这里需要重点强调“共地”概念。ESP32的GPIO输出是相对于它自己的GND的电压信号而驱动板判断高低电平也是相对于自己的GND。如果两块板子没有连接GND那么GPIO的3.3V和驱动板之间就没有参考基准控制信号完全无效。这个问题在视频教程里经常被忽略但实际中至少一半的“电机不转”问题都是共地没接好导致的。4.3 第一次上电调试的步骤第一次上电调试我强烈建议先烧录一个最简测试程序而不是直接烧完整程序。理由很简单如果完整程序跑不起来你没法确定问题出在Wi-Fi、服务器、引脚映射还是电机驱动排查范围太大。我的调测顺序是先用ESP32板载LED做Blink测试确认板子本身正常再烧录一个电机测试代码直接让两个电机正反各转2秒确认电机和驱动板接线正确最后再烧完整无线遥控代码通过串口监视器查看IP地址用手机访问。有几个细节值得注意如果烧录失败按住ESP32开发板上的BOOT键再尝试烧录如果串口监视器输出乱码检查波特率是否是115200如果连接不上路由器先确认5GHz频段不支持——ESP32只支持2.4GHz把手机热点改成2.4GHz频段再试。5. 常见问题与避坑指南5.1 问题速查表问题现象可能原因解决办法手机上不了网页Wi-Fi未连接成功查看串口打印的IP确认同一网段检查是否连5G Wi-Fi切换2.4G网页能打开但按键没反应引脚映射错误对照分配表检查接线确认引脚没被占用小车前进时跑偏电机安装不对称/两侧电压不一致机械调平代码里加左右轮速度补偿加速瞬间ESP32重启供电电压被拉低改用独立供电加470uF电解电容电机转了但速度特别慢PWM占空比初始值太低把speedValue初始值调到150以上有焦糊味电机堵转/驱动板短路立即断电排查接线检查电机是否卡死红外遥控小车没反应接收头供电不足确保接收头供电在5V并加0.1uF去耦电容想接入米家AppESP32直连米家受限通过ESPHome或者官方协议转换网关接入这张表基本上覆盖了我调试过程中遇到的所有问题类型。其实你会发现大部分问题归根结底就是供电、接线和引脚三件事。把这三方面做好检查90%的问题都能提前避免。5.2 供电问题的最终解决方案关于供电问题值得单独再说一段。ESP32小车做好的第一个周末我以为硬件和代码都完美了结果拿到楼下试跑每次一加速就重启。当时还怀疑是代码问题查了半天才发现是电池用了很久的劣质18650内阻早就变大了大电流输出时电压瞬间掉到4V以下直接把ESP32的电给断掉。后来我做了三件事彻底解决换了两节全新松下3400mAh电池在ESP32的VIN和GND之间并联了一个470uF电解电容把MP1584降压模块的输出调到5.1V而不是标准5V留出一点压降裕量。改完之后暴力加减速测试半小时一次重启都没有出现。如果你搞不到18650电池直接用5V充电宝给ESP32供电电机另用电池盒供电方案B也能稳定运行。但要注意两个电源之间还是要共地把ESP32的GND和驱动板的GND连起来不然控制信号依然是无效的。5.3 遥控延迟优化技巧HTTP方式控制已经有不错的响应速度但如果你追求更跟手的操控手感可以用下面两个优化方向第一在WebServer的handleClient循环中增加一个快速请求响应逻辑比如当方向按钮是按住状态时通过JS定时器每100ms发送一次控制请求松开时发stop。我试过把请求间隔压到50msESP32依然能稳定处理延迟体感上已经和有线手柄接近。第二改用异步Web服务器库ESPAsyncWebServer。这个库解决了WebServer在主循环处理请求时的阻塞问题同时还能支持SSEServer-Sent Events和WebSocket。我之前给小车加了测速功能后传统服务器每次请求都会阻塞几十毫秒换异步库之后整体流畅度明显改善。5.4 项目扩展的三条路线小车跑通之后别急着收工。这个项目平台的扩展潜力非常大我列三条值得尝试的路线路线一加传感器做自动控制。给车头装一个HC-SR04超声波传感器就能实现自动避障装两个五路红外循迹模块就能做道路巡线。这些都是全国大学生智能车竞赛和电子设计竞赛的常见题目方向。路线二加摄像头做视觉。ESP32-CAM模块大概30块钱直接通过串口和ESP32主控通信或者干脆用ESP32-CAM做主控单独控制小车。搜热词里的“esp32-cam手机远程监控”就是一个很好的参考做完你就是“手机远程遥控可视小车”了。路线三上ROS 2做机器人系统。ESP32作为底层运动执行器通过micro-ROS协议与树莓派上的ROS 2通信。这样小车就从一个玩具变成了SLAM导航机器人的底盘。搜索里大量出现的“ros 2 humble micro-ros esp32”“docker microros ros2 humble vscode platformio esp32”都在指向这条路线。6. 代码复现与调参技巧6.1 把代码部署到你的ESP32上如果你用的是Arduino IDE需要先在“开发板管理器”中安装esp32扩展包国内用户可以设置开发板管理地址为Espressif的镜像不然下载速度会让你怀疑人生。安装完成后在开发板列表里选择“ESP32 Dev Module”USB线连上电脑端口选择正确就能直接编译上传。如果你是PlatformIO用户直接在platformio.ini里添加一行board esp32dev然后写代码、编译、上传一步到位。我个人更推荐PlatformIO因为它的依赖管理更干净也方便后续加库。原文里的Wi-Fi名称和密码需要替换成你自己的。连不上路由器的时候先看看串口监视器里打印的错误日志ESP32会输出连接状态码比如WIFI_CONNECTED、WIFI_DISCONNECTED这些。如果一直连不上检查路由器是否开启了AP隔离功能——很多公共Wi-Fi默认开启AP隔离设备间互相不能访问这种情况手机是打不开控制页面的。6.2 速度参数与转向补偿的调校代码里speedValue初始值是150其范围是0-255。在室内光滑地面120到180之间比较合适在粗糙的地毯上需要调到200以上才有力。我个人建议第一次试跑先用比较低的速度比如100确认转向逻辑正确之后再逐步往上加。如果你发现小车前进时总是偏向左边大概率是左右两个电机的转速不一致。N20电机虽然是减速电机但同型号个体之间也会有5%-10%的转速差异。解决办法是在代码里做转向补偿如果小车右偏就把左侧电机的PWM值减小或者右侧电机加大。我测试时右轮明显快一点所以在motorB函数里对PWM做了constrain(speed * 0.95, 0, 255)的微调效果立竿见影。这里再分享一个经验技巧用按键控制加速减速时复位后的初始值不要太低。我之前设成50结果小车在地毯上干脆原地不动还以为是电机坏了。后来想明白了PWM占空比大约低于25%时直流电机可能连启动堵转力矩都克服不了尤其带负载时毛都动不了。建议初始值从120以上起步。6.3 网页端控制响应速度的调优网页控制面板用的是最基础的HTML按钮加超链接跳转。在局域网内使用响应速度已经能接受但如果把功能扩展得比较复杂比如加上了视频流、速度曲线、按键组合纯HTTP GET的方式就会感觉到卡顿。优化的思路是把按钮的点击事件改成AJAX异步请求并且给按钮绑定TouchStart和TouchEnd事件实现按住持续响应、松开停止——注意这里不能用click事件click在手机上有300ms左右的触发延迟手感差别非常大。script function sendCmd(cmd) { fetch(/action?dir cmd); } /script这段代码配合ontouchstartsendCmd(forward)和ontouchendsendCmd(stop)遥控延迟会有质的提升。放开后每200ms重复发送一次当前指令直到收到新的指令也能进一步减少丢包带来的操控迟钝感。7. 最后从遥控小车到智能平台的延伸思考其实做完一辆能跑的遥控车只是一个开始。这个项目的真正价值在于它帮你打通了“传感器采集 - 数据处理 - 无线通信 - 运动控制”这条完整的技术链路。以后无论你往哪个方向扩展底层这套架构都能复用。我个人的建议是先把这一步做得足够扎实别急着堆功能。把每个环节的原理想明白——为什么电机需要PWM调速、为什么ESP32要共地、为什么HTTP控制有延迟——这些基础问题搞清楚了后面加再复杂的功能都不会慌。我见过很多人小车还没跑稳就想着加摄像头、上AI、搞SLAM结果代码一复杂就彻底乱了最后还是回炉重造。另外有一点值得提醒遥控小车的续航和使用频率直接相关如果只是偶尔玩两节18650能撑一周但如果你加了摄像头、云台、各种传感器续航可能就只剩一两个小时。建议在底盘设计阶段就预留好电池仓的空间后续换大容量电池就不用重新打样了。