1. 硬件选型与搭建基础搞过智能小车的朋友都知道硬件选型是项目成功的第一步。我当初做ESP32-CAM小车时在淘宝上逛了整整三天才凑齐所有配件。核心部件ESP32-CAM模块现在价格已经降到30元左右这个巴掌大的板子集成了摄像头和WiFi功能性价比超高。不过要注意区分正版和山寨版——正版的摄像头是OV2640传感器画质明显更好。电机驱动模块我推荐L298N双H桥驱动虽然老但稳定最大能带动2A电流的电机。如果预算充足可以考虑TB6612FNG体积更小发热更低。底盘建议选择金属材质的四轮驱动套件带编码器的电机更好后期可以做速度闭环控制。电源部分我用的是两节18650锂电池配合XL6009升压模块实测连续工作1小时没问题。最容易被忽略的是电源分配问题。ESP32-CAM工作时电流峰值能达到500mA如果和电机共用电源会产生电压波动。我的解决方案是单独用一块降压模块给ESP32-CAM供电电机驱动板则直接接电池。这样视频传输稳定性提升了60%以上。2. ESP32-CAM开发环境搭建第一次玩ESP32-CAM的朋友肯定会遇到烧录问题。这里分享几个血泪教训首先一定要用CP2102 USB转TTL模块CH340经常识别不到。烧录时必须短接IO0和GND成功后要断开才能正常启动。如果遇到摄像头初始化失败可以尝试降低时钟频率config.ledc_channel LEDC_CHANNEL_0; config.ledc_timer LEDC_TIMER_0; config.pin_d0 5; config.pin_d1 18; // 添加这行解决花屏问题 config.xclk_freq_hz 10000000; // 默认20MHz改为10MHz视频传输优化是关键。在CameraWebServer示例中修改以下参数帧率15fps#define FRAMESIZE_QQVGA分辨率320x240QQVGA足够远程控制质量8static const int quality 8;实测在WiFi信号强度-70dBm时这个配置可以实现200ms以内的延迟。如果想进一步优化可以启用低延迟模式// 在app_httpd.cpp中找到 httpd_config_t config HTTPD_DEFAULT_CONFIG(); config.max_open_sockets 3; // 默认7改为3 config.lru_purge_enable true; // 启用LRU清理3. 远程控制方案设计局域网内控制太简单我们要解决的是跨网访问问题。测试过三种方案内网穿透花生壳/FRP云服务器中转P2P直连最稳定的还是云服务器方案。我用腾讯云轻量服务器月费24元搭建MQTT brokerESP32-CAM和手机APP都连接这个服务器。核心代码片段#include WiFi.h #include PubSubClient.h WiFiClient espClient; PubSubClient client(espClient); void reconnect() { while (!client.connected()) { if (client.connect(ESP32CAM, username, password)) { client.subscribe(car/control); // 订阅控制指令 } else { delay(5000); } } } void callback(char* topic, byte* payload, unsigned int length) { if(strcmp(topic, car/control)0){ char cmd (char)payload[0]; // 处理前进(F)、后退(B)、左转(L)、右转(R)指令 } }电机控制采用差速转向算法。通过PWM占空比差异实现转弯比舵机转向更灵活。L298N的使能端接ESP32的PWM引脚推荐GPIO12,13注意要设置正确的PWM频率const int freq 5000; const int resolution 8; // 8位分辨率(0-255) ledcSetup(0, freq, resolution); // 通道0 ledcAttachPin(12, 0); // ENA接GPIO124. 手机控制端开发Android端开发要注意三个关键点视频流显示优化控制指令防抖状态反馈机制使用ExoPlayer替代WebView加载视频流延迟能降低40%val player ExoPlayer.Builder(this).build() videoView.player player val dataSourceFactory DefaultHttpDataSource.Factory() val mediaItem MediaItem.fromUri(http://your_stream_url) player.setMediaItem(mediaItem) player.prepare()控制按钮建议采用触摸持续触发机制手指按住时连续发送指令。这是优化后的触摸监听代码private val controlListener View.OnTouchListener { v, event - when(event.action) { MotionEvent.ACTION_DOWN - { when(v.id) { R.id.btn_forward - sendCommand(F) // 其他按钮处理 } // 启动重复发送 handler.postDelayed(repeatAction, 200) } MotionEvent.ACTION_UP - { handler.removeCallbacks(repeatAction) sendCommand(S) // 停止指令 } } true } private val repeatAction object : Runnable { override fun run() { // 获取当前按钮状态继续发送 handler.postDelayed(this, 100) } }5. 常见问题解决方案调试过程中我踩过不少坑这里总结几个典型问题摄像头初始化失败检查5V供电是否稳定尝试降低XCLK频率确认摄像头排线接触良好WiFi频繁断开修改电源管理设置#include esp_wifi.h esp_wifi_set_ps(WIFI_PS_NONE); // 禁用省电模式添加看门狗定时器优化天线位置外接IPEX天线效果更好视频卡顿严重降低分辨率到QQVGA(160x120)减少同时连接的客户端数量关闭不必要的服务httpd_config_t config HTTPD_DEFAULT_CONFIG(); config.uri_match_fn httpd_uri_match_wildcard; config.server_port 81; config.ctrl_port 32768; // 修改默认控制端口电机控制异常时先检查L298N的散热情况。我后来加了散热片和风扇连续工作温度从78℃降到了45℃。另外建议在代码中加入过流保护逻辑void motorControl(int speedA, int speedB) { // 限制最大输出 speedA constrain(speedA, -200, 200); speedB constrain(speedB, -200, 200); // 死区处理 if(abs(speedA)30) speedA0; if(abs(speedB)30) speedB0; // 设置PWM输出 ledcWrite(0, abs(speedA)); ledcWrite(1, abs(speedB)); // 设置方向引脚 digitalWrite(IN1, speedA0?HIGH:LOW); digitalWrite(IN2, speedA0?LOW:HIGH); // 另一组电机同理 }6. 进阶功能扩展基础功能实现后可以尝试这些增强功能自动巡航模式通过OpenCV处理视频流实现车道保持# 在服务器端运行的识别代码 import cv2 def detect_lane(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5,5), 0) edges cv2.Canny(blur, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi/180, 50, minLineLength50, maxLineGap100) # 计算转向角度并发送给小车语音控制集成使用百度语音识别API#include HTTPClient.h void speechControl() { HTTPClient http; http.begin(http://vop.baidu.com/server_api); http.addHeader(Content-Type, application/json); String payload {\format\:\pcm\,\rate\:16000,\token\:\your_token\}; int code http.POST(payload); if(code 200) { String response http.getString(); // 解析识别结果 } }电量监控系统通过ADC检测电池电压float readBatteryVoltage() { int adcValue analogRead(34); float voltage adcValue * (3.3/4095.0) * 2.0; // 分压电阻比例 return voltage; } void checkBattery() { float v readBatteryVoltage(); if(v 3.3) { mqttClient.publish(car/status, Low battery!); // 进入省电模式 } }7. 项目优化建议经过三个版本的迭代我的小车现在可以稳定运行4小时以上。分享几个关键优化点电源管理优化使用TPS63070升降压模块效率95%添加超级电容缓冲电流突变实现动态功耗调节void setPowerMode(int mode) { switch(mode) { case 0: // 高性能模式 setCpuFrequencyMhz(240); WiFi.setTxPower(WIFI_POWER_19_5dBm); break; case 1: // 平衡模式 setCpuFrequencyMhz(160); WiFi.setTxPower(WIFI_POWER_15dBm); break; case 2: // 省电模式 setCpuFrequencyMhz(80); WiFi.setTxPower(WIFI_POWER_11dBm); camera_fb_return(fb); // 释放摄像头缓冲区 break; } }机械结构改进3D打印摄像头云台支持俯仰调节增加悬挂系统减少震动使用磁编码器测量轮速通信协议优化改用UDP协议传输控制指令延迟从120ms降到40ms。关键实现#include WiFiUdp.h WiFiUDP Udp; void setup() { Udp.begin(1234); } void loop() { int packetSize Udp.parsePacket(); if(packetSize) { char cmd[packetSize]; Udp.read(cmd, packetSize); processCommand(cmd[0]); } }最后提醒大家调试时一定要先用USB供电测试所有功能再接电池。我有次短路烧了两块ESP32-CAM都是血泪教训。现在这个小车我已经持续迭代了半年最近正在尝试加入ROS导航功能。