前言:本文为手把手教学ESP32-CAM实战项目——ESP32-CAM图传勘探小车,本项目仅采用ESP32-CAM作为核心MCU,实现小车项目的图传与控制一体化。图传小车的底盘驱动轮采用麦克纳姆轮,搭配4个N20马达与TC118S驱动电路。上位机则采用APP inventor(Wxbit图形化)进行编程实现,UDP网络协议下发上位机指令控制小车运动。本文将以ESP32-CAM为作为核心控制板实现麦克纳姆轮的解运动学与局域网图片传输实现,项目可二次开发。(文末代码与资料开源!)

        实验硬件:ESP32-CAM;驱动PCB(TC118S);GA12-N20马达×4;麦克纳姆轮×4;小车3D打印机

    硬件实物图:

        效果图:

一、ESP32-CAM简介与使用

        ESP32-CAM是安信可最新发布小尺寸的摄像头模组。该模块可以作为最小系统独立工作,尺寸仅为27*40.5*4.5mm,深度睡眠电流最低达到6mA。相较于K210,OpenMV等一系列MCU,ESP32-CAM可以说是最具性价比的图传模组了。

        借于ESP32-CAM的优秀表现与价格优势,ESP32-CAM被广泛应用于各种物联网场合,适用于家庭智能设备、工业无线控制、无线监控、QR无线识别,无线定位系统信号以及其它物联网应用,是物联网应用的理想解决方案(国外各大论坛与Github上都有各路大神炫酷的DIY设计,感兴趣的朋友去了解一下)。

特别说明:

(1)读者朋友在正常或者初次使用ESP32CAM浏览摄像头网站IP的时候,如果出现视频FPS较低卡顿时,建议检查电源供电是否合理,ESP32CAM至少需要5V供电。

(2)安可信原厂的ESP32CAM使用时发热现象较少,后期淘宝售卖的大都发热严重(普遍现象),一般情况下不影响使用。

技术文档:ESP32-CAM摄像头开发板 | 安信可科技 (ai-thinker.com)

二、麦克纳姆轮

2.1 什么是麦克纳姆轮

        本项目的图传小车使用麦克纳姆轮作为驱动轮,相比传统的驱动轮,其可以实现更高自由度的运动状态。在竞赛机器人和特殊工种机器人中,全向移动经常是一个必需的功能。「全向移动」意味着可以在平面内做出任意方向平移同时自转的动作。为了实现全向移动,一般机器人会使用「全向轮」(Omni Wheel)或「麦克纳姆轮」(Mecanum Wheel)这两种特殊轮子。其中,麦克纳姆轮如图所示:

        麦克纳姆轮由两大部分组成:轮毂和辊子(roller),轮毂是整个轮子的主体支架,辊子则是安装在轮毂上的鼓状物。麦克纳姆轮的轮毂轴与辊子转轴呈 45° 角。在理论上,这个夹角可以是任意值,根据不同的夹角可以制作出不同的轮子,但最常用的还是45°。

        关于麦克纳姆轮的运动学解析,为了方便大家理解。这里抛开繁琐复杂的推导公式,直接给大家以运动学分解来阐明其巧妙地原理。我们这里首先以单个麦轮为例,已知单个麦轮可以分解成轴向向右和垂直轴向向前的速度分量,具体如下图所示:

以此类推,我们将其代入到4轮小车上,并以下图所示。当四个轮子都向前转的时候,AB轮可以相互抵消轴向速度,只剩下向前的速度,这样底盘就是向前直行、不会跑偏(当然,提前是各个轮子的转速差不是很大,否则有稍微偏移也是正常的)。

根据麦轮的运动学特殊性A1、B1、B2、A2(前面的为1,后面的为2)合理驱动可以实现多自由度控制具体如下(麦轮与下方安装方式一致,O-长方形(O-rectangle)):

运动方向

A1

B1

B2

A2

向前FF

向前

向前

向前

向前

向后BB

向后

向后

向后

向后

左横LL

向前

向后

向后

向前

右横RR

向后

向前

向前

向后

左上LF

向前

停止

停止

向前

右上RF

停止

向前

向前

停止

左下LB

停止

向后

向后

停止

右下RB

向后

停止

停止

向后

左旋ll

向前

向后

向前

向后

右旋rr

向后

向前

向后

向前

2.2 麦克纳姆轮的安装

        麦克纳姆轮因结构独特性,其安装方式也有一定讲究。麦轮一般是四个一组使用,两个左旋轮,两个右旋轮,左旋轮和右旋轮呈手性对称。常见的安装方式有:X-正方形(X-square)、X-长方形(X-rectangle)、O-正方形(O-square)、O-长方形(O-rectangle)。其中 X 和 O 表示的是与四个轮子地面接触的辊子所形成的图形;正方形与长方形指的是四个轮子与地面接触点所围成的形状。

        本项目图传小车的麦轮使用的是O-长方形(O-rectangle)安装方式,具体安装情况如下图所示,复现本项目的读者朋友可以参考一下。采用此安装方式的朋友,可以直接套用作者提供的麦轮运动控制学代码,进行稍微调试即可应用于自己的项目小车上。

详细解析参考:麦克纳姆轮(万向轮)运动原理 - 小鹏STEM (xpstem.com)

额外补充:为什么麦克纳姆轮如此神奇灵活,却得不到大范围普及呢?

(1)、根据麦克纳姆轮需要在平坦的陆地上才能准确有效的发挥其特性,出现轮子悬空的情况则容易控制失灵;

(2)、麦克纳姆轮的使用将降低车子的效率,造成资源浪费;

(3)、因为结构特性原因,不利于爬坡、越障,比较适合于平地使用,大大限制了应用场景;

三、代码与解析

三、代码与解析

3.1 代码基础框架

项目的基本框架可以从Arduino的示例教程中获取,后续的项目代码在此基础上进行修改删减即可。读者朋友在正确导入ESP32工程库后,按照下图去创建项目的基础框架:

3.2 核心部分代码与解析

3.2.1 库文件引入与变量定义

#include "esp_camera.h"
#include <WiFi.h>
#include <AsyncUDP.h>
#include<Arduino.h>
#include "camera_pins.h"    //摄像头型号引脚配置
 
#define CAMERA_MODEL_AI_THINKER    //选择安可信的ESP32CAM
 
AsyncUDP udp;               //UDP网络协议导入
AsyncUDP Rudp;
const char* ssid = "**********";        //WIFI名
const char* password = "**********";    //WIFI密码
char rBuff[18];                    //UDP接收缓存
void startCameraServer();          //摄像头视频流服务函数
String inputString;                //字符串变量

 3.2.2 麦克纳姆轮驱动代码

项目小车的4个麦克纳姆轮采用4个N20马达连接,为了保证可以每个马达的减速尽可能保持一致,可以用pwm调节马达转速,保证麦轮小车的稳定。读者朋友可以根据自己的实际情况去设置一下pwm值,驱动端口的引脚同理。

void setpin_pwm(uint8_t Pinport,uint8_t pwmchannel,uint8_t pwmcnt)
{
  ledcAttachPin(Pinport, pwmchannel);   
  ledcSetup(pwmchannel, 1000, 8);
  ledcWrite(pwmchannel, pwmcnt);
}
 
#define motopwm 110
#define right_back  {ledcDetachPin(15);setpin_pwm(14,1, motopwm);digitalWrite(15,0);}     //digitalWrite用于某引脚号写高低电平
#define right_forward {ledcDetachPin(14);setpin_pwm(15,1,motopwm);digitalWrite(14,0);}
#define right_stop {ledcDetachPin(14);ledcDetachPin(15);digitalWrite(15,0);digitalWrite(14,0);}
 
 
#define right2_back  {ledcDetachPin(13);setpin_pwm(12,2,motopwm);digitalWrite(13,0);}
#define right2_forward {ledcDetachPin(12);setpin_pwm(13,2,motopwm);digitalWrite(12,0);}
#define right2_stop {ledcDetachPin(12);ledcDetachPin(13);digitalWrite(13,0);digitalWrite(12,0);}
 
#define left_back {ledcDetachPin(4);setpin_pwm(2,3,motopwm); digitalWrite(4,0);}
#define left_forward   {ledcDetachPin(2);setpin_pwm(4,3,motopwm); digitalWrite(2,0);}
#define left_stop {ledcDetachPin(4);ledcDetachPin(2);digitalWrite(2,0); digitalWrite(4,0);}
 
#define left2_forward {ledcDetachPin(33);setpin_pwm(32,4,motopwm); digitalWrite(33,0);}
#define left2_back  {ledcDetachPin(32);setpin_pwm(33,4,motopwm); digitalWrite(32,0);}
#define left2_stop {ledcDetachPin(32);ledcDetachPin(33);digitalWrite(32,0); digitalWrite(33,0);}

3.2.3 启动函数 

arduino编程编程习惯void setup()函数作为启动函数,类似于初试init()函数。该段函数中开启打印串口(波特率为115200,方便和上位机调试相关信息以及输出IP地址),设置摄像头参数,设置麦克纳姆轮引脚为输出引脚。

  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();
 
//摄像头参数设置
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG; //为了串流
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;
 
  WiFi.begin(ssid, password);   //连接热点服务
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  startCameraServer();        //开启摄像头服务
 
  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");
 
//麦克纳姆轮的驱动引脚设置为输出
  pinMode(4, OUTPUT);
  pinMode(2, OUTPUT);
  pinMode(14, OUTPUT);
  pinMode(15, OUTPUT);
  pinMode(13, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(32, OUTPUT);
  pinMode(33, OUTPUT);
 

3.2.4 UDP网络协议控制麦轮代码

ESP32CAM的远程端口为10011,小车通过UDP协议监听10011是否有数据传输,如果存在数据,则利用封包Rpacket来保存上位机发送过来的实时数据。将得到的数据放入接受数组rBuff中,通过if语句进行判断,当前需要的麦轮运动方式(运动方式结合上述表格)。

while (!Rudp.listen(10011)) //等待本机udp监听端口设置成功,用于接收上位端发送过来的数据
{
    Serial.println("waiting");    
}
 
Rudp.onPacket([](AsyncUDPPacket Rpacket) 
{//注册一个端口10011的数据包接收事件,可异步接收数据,用于接收上位机发送过来的数据
 
   Serial.println("Camera data ");
   if(LedFlash)    //通过LED闪烁确定程序正常运行
   {
    LedFlash = 0;    
   }
   else
   {
    LedFlash=1;
   }
  
    for (int i = 0; i < Rpacket.length(); i++)
    {
      rBuff[i] = (char) * (Rpacket.data() + i);
    }
    inputString = String(rBuff);
    if (inputString.indexOf("are you here") != -1)
    {
         
         if (udp.connect(Rpacket.remoteIP(), 10000)) //检查网络连接是否存在,这取决于上位机是否连接,这里条件是如果有连接则处理,否则进入下一次loop.
         {
           udp.print("ok,it is me"); 
           //udp.write(fb->buf, max_packet_byte); //将图片分包发送
         }
        
    }
//利用UDP发送的指令得到的缓存数组数值去选择麦轮的运动方式
    if ((rBuff[0]=='F')&&(rBuff[1]=='F'))//前
    {
       Serial.print("FF/r/n");
       left_forward;
       left2_forward
       right_forward;
       right2_forward;
    }
    if  ((rBuff[0]=='R')&&(rBuff[1]=='F'))//右上
    {
       Serial.print("RF/r/n");
       right2_forward;
       left2_stop;
       left_stop;
       right_forward
    }
    if  ((rBuff[0]=='L')&&(rBuff[1]=='F'))//左上
    {
       Serial.print("LF/r/n");
       right2_stop;
       left_forward;
       left2_forward;
       right_stop;
    }
    if  ((rBuff[0]=='R')&&(rBuff[1]=='B'))//右下
    {
       Serial.print("RB/r/n");
       right_stop;
       left_back;
       left2_back;
       right2_stop;
    }
     if ((rBuff[0]=='L')&&(rBuff[1]=='B'))//左下
    {
       Serial.print("LB/r/n");
       right_back;
       left2_stop;
       left_stop;
       right2_back
    }
     if ((rBuff[0]=='r')&&(rBuff[1]=='r'))//右转
    {
       Serial.print("rr/r/n");
       left_back;
       left2_forward;
       right_forward;
       right2_back       
    }
     if  ((rBuff[0]=='l')&&(rBuff[1]=='l'))//左转
    {
       Serial.print("ll/r/n");
       left_forward;
       left2_back;
       right_back;
       right2_forward;       
    }
     if  ((rBuff[0]=='R')&&(rBuff[1]=='R'))//右横
    {
       Serial.print("RR/r/n");
       left_back;
       left2_back;
       right_forward;
       right2_forward;
 
    }
    
    if  ((rBuff[0]=='L')&&(rBuff[1]=='L'))//左横
    {
       Serial.print("LL/r/n");
       left_forward;
       left2_forward;
       right_back;
       right2_back;       
    }
    if  ((rBuff[0]=='B')&&(rBuff[1]=='B'))//后退
    {
       Serial.print("BB/r/n");
       left_back;
       right_back;
       left2_back;
       right2_back;
    }
      if  ((rBuff[0]=='S')&&(rBuff[1]=='S'))//停止
    {
       Serial.print("SS/r/n");
       left_stop;
       right_stop;
       left2_stop;
       right2_stop;
    }

3.2.5 循环主代码 

//防止网络连接中断超时后再次连接网络
void loop() {
  unsigned long currentMillis = millis();   //获取当前机器运行时间
// 定时检查WIFI是否连接,如果无连接则重连
  if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval)) {
    WiFi.disconnect();
    WiFi.reconnect();
    previousMillis = currentMillis;
  }
 
}

四、上位机制作

4.1 APP Inventor简介

        App Inventor是一款谷歌公司开发的手机编程软件。 谷歌推出一款名叫Google App Inventor的工具软件, Google App Inventor用户能够通过该工具软件使用谷歌的Android系列软件自行研发适合手机使用的任意应用程序。

        App Inventor是基于图形化编程的快速入门手机APP编程软件,操作入手起来相当简单。搭配自带的在线调试助手,可以轻松帮助开发人员实现简单APP的制作。App Invntor版本目前有很多,其大致的操作界面如下:

         用户可以根据自己的需要从左边的组件面板中选择自己需要的组件元素,然后放置在工作面板的手机中的合适位置上(APP Inventor的版本很多,收费版本提供的组件元素更丰富)。当然,APP Inventor也提供了拓展包接口,用户可以通过导入拓展包实现自定义组件功能实现。

推荐的APP Inventor地址:

(1)WxBit(APP Inventor汉化版,收费):WxBit 图形化编程

(2)APP Inventor2测试版(免费):MIT App Inventor

4.2 上位机源码与分析

4.2.1 上位机组件选择

(1)球形精灵组件+摇杆组件:设计的摇杆按键,实现小车的麦克纳姆轮的控制;

(2)网页浏览框组件:显示得到的ESP32CAM局域网内的摄像头视频流(可直接浏览读取);

(3)刷新视频按键组件:防止视频流因网络连接不稳定断开,使用该按钮再次刷新网络浏览框内的视频流;

(4)左旋按键+右旋按键组件:长按后实现小车麦克纳姆轮的左右旋转;

(5)计时器组件:用来时刻定时读取摇杆装置是否复归原位,帮助实现动态连续控制小车;

(6)UDP监听和发送组件:需要使用扩展包,采用UDP网络协议下发上位机的小车控制指令(UDP监听视频流可用可不用效果差不多);

4.2.2 上位机逻辑设计

App Inventor的好处就是可以直接通过逻辑设计去联动编程,实现APP的制作,而且随着其版本的不断更新,其功能和上限也越发强大(和常规的比还是有一定局限的)。

(1)设定全局变量与屏幕初始化设置:设置计时器20ms触发一次;摇杆和球形精灵的初始位置;使用UDP网络协议向小车端口发送消息(测试);网页浏览器打开ESP32CAM的IP地址网页并且读取视频流;

(2)设置3个按钮组件的逻辑:视频刷新按钮再次打开视频流;左旋和右旋按钮使用UDP协议向下位机发送指定命令(RemoteHost为ESP32CAM的IP地址,RemotePort端口号:10011);按下时需要将转向标志位置1,松开置0,避免和摇杆装置控制起冲突

(3)摇杆装置的逻辑设置:包含按下和松开后的位移以及旋转角度等;

(4)摇杆控制麦克纳姆轮的逻辑设置:主要取决于摇杆角度,停止时需要看是否转向标志置0;

4.3 上位机效果图

五、项目效果

ESP32CAM图传小车

六、代码开源

代码地址: 基于ESP32CAM的图传勘探小车代码(含APPInventor制作的APP上位机)-嵌入式文档类资源-CSDN文库