做小车的以往版本选型历史记录

  • 原型机-1:树莓派2b + UNO R3 + L298N
    最初入坑的版本,2017年那时候啥都不会,直接搞了逗逼版本
  • 原型机-2:NodeMCU v2(CP2102)+L293D扩展板
    第一次做的正规点的版本,带有舵机的云台,近光灯和远光灯

  • MK.1.1:NodeMCU v2(CP2102)+L293D扩展板
    基本开始按这个版本定型了
  • MK.2.1:NodeMCU v3(CH340)+L293D驱动模块
    这个版本是内网用的,APP不是Blynk,是别人开发的,可以使用语音控制和通过手机陀螺仪控制,不过自由度没有Blynk高,最近也没怎么折腾了
  • MK.2.2:NodeMCU v2(CP2102)+L293D扩展板

    MK1为摇杆版,MK2系列为按钮版.原型机是使用舵机控制摇摆摄像头的,所以MK1保留了舵机控制功能,MK2时候因为直接用了现成的云台摄像头,大幅度节省空间.


一路以来的坑

最初入门是使用树莓派,通过树莓派插网卡后,串口输出指令连接到Blynk,体型巨笨重.来发现ArduinoIDE写的ESP8266系列可以省那么多功夫,树莓派就直接弃坑了,直接使用NodeMCU + L293D电机扩展板的,后来做的体积小了很多.最初功能很简陋,只能固定SSID,不能重置,也没有OLED显示等等...直到现在保留了两个版本,基于这两个版本上改进修复.博客是最近才开的,所以以前的版本都还没公开.

  • 参考-1,参考-2,参考-3,

  • MK1

    • 使用舵机控制云台,安装了外置摄像头等,探照灯属于车身重的支架,同时驱动四个电机,不适合大电流电机急速转动.所以使用摇杆控制PWM信号驱动电机,车身转弯有点类似真车,是通过左右轮子速度差来实现前进/退后式转弯驱动方向拐弯(专业术语不知道怎么称呼这种方式),需要较大的转弯空间.即左2个轮正转1023,右2个正转轮512,使用12.6V锂电池驱动,电机是串联(标称3-9V/个的TT马达).
  • MK2
    • 属于小型车辆,使用按钮控制,由于车身小,适合驱动2个电机的小车.转弯是两边车轮反转,可以实现原地360°转弯,车速较快.最好不要驱动4个电机(特别是并联),会出现"虚脱",亲测试过会导致L293D过热直接冒火,请使用大功率点的电机驱动(如:L298N或者L2934路版本),这样要重写一下电机驱动的代码加上没时间,也就没搞了.

需要准备的环境和材料

环境

ArduinoIDE编译通过的库版本:

  • IDE版本:1.8.10
  • ESP8266库:2.4.2
  • Blynk版本:0.6.1
  • ArduinoJson:5.13.3
  • WiFiManager:0.14.4

PlatformIO编译通过的库版本:

  • Blynk:0.6.1
  • ArduinoJson:5.13.4
  • WifiManager:0.14
  • ESP8266:2.0.4 / 2.1.1 / 2.2.0 / 2.2.3

2020-01-13:在PlatformIO 2.3.2板库下编译会出错,2.2.3编译成功,尽量不要用高版本的板库.平常工作忙,具体我也没查清楚是啥原因.

材料

  • ESP8266 X 1
  • L293D电机扩展板 X 1
  • 电压传感器 X 1(逻辑电平3.3 V)
  • 自锁开关 X 1
  • 按钮开关 X 1(或者直接用按钮模块取代)
  • 10K电阻 X 1
  • TT马达 X 1
  • 0.96'OLED X 1(SSD1306主控)
  • 7.4 v两串电池组 X 1(超过该电压的记得移除连接在VIN VM一起的跳线帽)

接线图


(点击图片放大)

使用PlatformIO(Linux环境)的刷入方法

  1. 下载python工具:
    sudo pip install -U pip setuptools

    sudo pip install -U platformio

  2. 下载仓库:
    不行的可以直接去我的github下载:https://github.com/chrisxs/Blynk_WiFi_RC_Car_MK2.5
    git clone https://github.com/chrisxs/Blynk_WiFi_RC_Car_MK2.5.git


(点击图片放大)

  1. 进入目录:
    cd Blynk_WiFi_RC_Car_MK2.5

  2. 运行编译:
    platformio run


(点击图片放大)

platformio run -e generic -t upload - 编译并上传

PlatformIO会自动帮你下载好所需要的衣依赖库,推荐用此方法,需要修改代码的话可以进入图形化IDE里open project一下就好了(见下图).

(点击图片放大)

当然,如果你熟悉EPStool的话你也可以使用该工具直接上传二进制文件,编译的结果可以在/bin目录里找到
MAC和Windows环境操作差不多,这里就不分开写了,具体参考PlatformIO官方文档.觉得实在看不懂的可以看下一节使用ArduinoIDE的方法 :huaji:

使用ArduinoIDE

这里我就不上图了,同时不推荐使用这个方法,因为很麻烦要一个个的下载库,不小心的话你可能会下错

IDE版本:1.8.10

  1. 先下载好依赖库,在IDE里面都可以搜到
  1. 接下来是老办法,直接在IDE写代码:
    (手机浏览器显示不正常的话点击左上角的三个点点)
#define BLYNK_PRINT Serial
#include <FS.h> //this needs to be first, or it all crashes and burns...

#include <Arduino.h>
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
#include <ESP8266mDNS.h> //https://github.com/esp8266/Arduino
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>

//needed for library
#include <DNSServer.h>
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager
#include <WiFiUdp.h>
#include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson
#include <ArduinoOTA.h>

#include <Wire.h>        // Only needed for Arduino 1.6.5 and earlier
#include "SSD1306Wire.h" // legacy include: `#include "SSD1306.h"

SSD1306Wire display(0x3c, D7, D6);
BlynkTimer timer; //clean 为电压传感器引用计时器,名称为timer

const int voltagePin = A0; //电压传感器引脚
float R1 = 30000;          //声明R1电阻值,即为:30KΩ
float R2 = 7500;           //声明R2电阻值,即为:7.5KΩ

const int LEDPin = LED_BUILTIN;
static const uint8_t pwm_A = 5;
static const uint8_t pwm_B = 4;
static const uint8_t dir_A = 0;
static const uint8_t dir_B = 2;

int motor_speed = 0;
int motor_speed_V;

const int ResetButton = D5; //设置用于自定义重置WIFI存储信息的按键引脚

char blynk_token[34] = "请输入你的TokenKEY";

bool shouldSaveConfig = false;

void Blink();

void STOP()
{ //刹车
  analogWrite(pwm_A, 0);
  analogWrite(pwm_B, 0);
}

void backward()
{ //后退

  analogWrite(pwm_A, motor_speed);
  analogWrite(pwm_B, motor_speed);
  digitalWrite(dir_A, HIGH);
  digitalWrite(dir_B, HIGH);
}

void forward()
{ //前进
  analogWrite(pwm_A, motor_speed);
  analogWrite(pwm_B, motor_speed);
  digitalWrite(dir_A, LOW);
  digitalWrite(dir_B, LOW);
}

void turnleft()
{ //左转
  analogWrite(pwm_A, motor_speed);
  analogWrite(pwm_B, motor_speed);
  digitalWrite(dir_A, LOW);
  digitalWrite(dir_B, HIGH);
}

void turnright()
{ //右转
  analogWrite(pwm_A, motor_speed);
  analogWrite(pwm_B, motor_speed);
  digitalWrite(dir_A, HIGH);
  digitalWrite(dir_B, LOW);
}

void saveConfigCallback()
{
  Serial.println("Should save config");
  shouldSaveConfig = true;
}

void voltageRead()
{                                                                        //电压传感器自定义函数
  int VoltageState = analogRead(voltagePin);                             //暂存A0的数值,类型为整数(0-1023)
  float Votage = (3.313 * VoltageState * (R1 + R2)) / (1024 * R2) - 0.4; //暂存A0的数值,类型为浮点
  Blynk.virtualWrite(V0, Votage);                                        //写入blynk的V0
}

BLYNK_WRITE(V8) //APP的调速杆插件,最低启动值600-1023
{
  int vel = param.asInt();
  //int motor_speed_V;
  motor_speed_V = map(vel, 0, 1023, 600, 1023);
  motor_speed = motor_speed_V;
}

BLYNK_WRITE(V1)
{
  int FowardButton = param.asInt();
  if (FowardButton == 1)
  {
    forward();
  }
  else
  {
    STOP();
  }
}

BLYNK_WRITE(V2)
{
  int BackwardButton = param.asInt();
  if (BackwardButton == 1)
  {
    backward();
  }
  else
  {
    STOP();
  }
}

BLYNK_WRITE(V3)
{
  int TurnleftButton = param.asInt();
  if (TurnleftButton == 1)
  {
    turnleft();
  }
  else
  {
    STOP();
  }
}

BLYNK_WRITE(V4)
{
  int TurnrightButton = param.asInt();
  if (TurnrightButton == 1)
  {
    turnright();
  }
  else
  {
    STOP();
  }
}

BLYNK_CONNECTED()
{
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_16);
  display.drawString(0, 0, "Blynk server");
  display.drawString(0, 15, "is conneted...");
  display.display();
  STOP();
  Blynk.syncAll();
  Blink();
  digitalWrite(LEDPin, LOW);
}
void Blink()
{
  digitalWrite(LEDPin, HIGH);
  delay(500);
  digitalWrite(LEDPin, LOW);
  delay(500);
  digitalWrite(LEDPin, HIGH);
  delay(500);
  digitalWrite(LEDPin, LOW);
  delay(500);
  digitalWrite(LEDPin, HIGH);
  delay(500);
  digitalWrite(LEDPin, LOW);
  delay(500);
  digitalWrite(LEDPin, HIGH);
}

void setup()
{
  Serial.begin(115200);

  // initial settings for motors off and direction forward
  display.init();
  display.flipScreenVertically();
  display.setFont(ArialMT_Plain_10);
  display.clear();

  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_16);
  display.drawString(0, 0, "Device is booting");
  display.display();

  pinMode(pwm_A, OUTPUT); // PMW A
  pinMode(pwm_B, OUTPUT); // PMW B
  pinMode(dir_A, OUTPUT); // DIR A
  pinMode(dir_B, OUTPUT); // DIR B
  pinMode(LEDPin, OUTPUT);
  int ResetButtonState = digitalRead(ResetButton);
  pinMode(ResetButton, INPUT);
  digitalWrite(LEDPin, HIGH);

  //从FS json中读取配置
  Serial.println("mounting FS...");

  if (SPIFFS.begin())
  {
    Serial.println("mounted file system");
    if (SPIFFS.exists("/config.json"))
    {
      //未见存在提取并读取
      Serial.println("reading config file");
      File configFile = SPIFFS.open("/config.json", "r");
      if (configFile)
      {
        Serial.println("opened config file");
        size_t size = configFile.size();
        // 分配一个缓冲区来存储文件的内容。
        std::unique_ptr<char[]> buf(new char[size]);
        configFile.readBytes(buf.get(), size);
        DynamicJsonBuffer jsonBuffer;
        JsonObject &json = jsonBuffer.parseObject(buf.get());
        json.printTo(Serial);
        if (json.success())
        {
          Serial.println("\nparsed json");
          strcpy(blynk_token, json["blynk_token"]);
        }
        else
        {
          Serial.println("failed to load json config");
        }
        configFile.close();
      }
    }
  }
  else
  {
    Serial.println("failed to mount FS");
  }
  //end read

  // The extra parameters to be configured (can be either global or just in the setup)
  // After connecting, parameter.getValue() will get you the configured value
  // id/name placeholder/prompt default length
  WiFiManagerParameter custom_blynk_token("blynk", "序列号", blynk_token, 34);

  //WiFiManager
  //Local intialization. Once its business is done, there is no need to keep it around
  WiFiManager wifiManager;

  //set config save notify callback
  wifiManager.setSaveConfigCallback(saveConfigCallback);

  //在这里增加参数
  wifiManager.addParameter(&custom_blynk_token);

  if (ResetButtonState == HIGH)
  {
    Serial.println("Getting Reset ESP Wifi-Setting.......");
    display.clear();
    display.setFont(ArialMT_Plain_16);
    display.drawString(0, 0, "Getting Reset for ");
    display.drawString(0, 15, "ESP Wifi-Setting.......");
    display.display();
    wifiManager.resetSettings();
    delay(5000);
    Blink();
    Serial.println("Formatting FS......");
    display.clear();
    display.setFont(ArialMT_Plain_16);
    display.drawString(0, 0, "Formatting FS......");
    display.display();
    SPIFFS.format();
    delay(5000);
    Blink();
    Serial.println("Done,Reboot.");
    display.clear();
    display.setFont(ArialMT_Plain_16);
    display.drawString(0, 0, "Done,Reboot.");
    display.display();
    Blink();
    ESP.reset();
  }

  //fetches ssid and pass and tries to connect
  //if it does not connect it starts an access point with the specified name
  //here  "AutoConnectAP"
  //and goes into a blocking loop awaiting configuration
  if (!wifiManager.autoConnect("Blynk_WIFI_Car_MK2.5"))
  {
    Serial.println("failed to connect and hit timeout");
    delay(3000);
    //reset and try again, or maybe put it to deep sleep
    ESP.reset();
    delay(5000);
  }
  //if you get here you have connected to the WiFi
  Serial.println("WIFI is connected");

  //read updated parameters
  strcpy(blynk_token, custom_blynk_token.getValue());


  //save the custom parameters to FS
  if (shouldSaveConfig)
  {
    Serial.println("saving config");
    DynamicJsonBuffer jsonBuffer;
    JsonObject &json = jsonBuffer.createObject();
    json["blynk_token"] = blynk_token;

    File configFile = SPIFFS.open("/config.json", "w");
    if (!configFile)
    {
      Serial.println("failed to open config file for writing");
    }
    json.printTo(Serial);
    json.printTo(configFile);
    configFile.close();
    //end save
  }
  Serial.println("local ip");
  Serial.println(WiFi.localIP());
  ArduinoOTA.setHostname("Blynk_WIFI_Car_MK2.5");
  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH)
    {
      type = "sketch";
    }
    else
    { // U_SPIFFS
      type = "filesystem";
    }

    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR)
    {
      Serial.println("Auth Failed");
    }
    else if (error == OTA_BEGIN_ERROR)
    {
      Serial.println("Begin Failed");
    }
    else if (error == OTA_CONNECT_ERROR)
    {
      Serial.println("Connect Failed");
    }
    else if (error == OTA_RECEIVE_ERROR)
    {
      Serial.println("Receive Failed");
    }
    else if (error == OTA_END_ERROR)
    {
      Serial.println("End Failed");
    }
  });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  Blynk.config(blynk_token, "你的域名", 8080);
  timer.setInterval(1000L, voltageRead);
}

void loop()
{
  int VoltageState = analogRead(voltagePin);                             //暂存A0的数值,类型为整数(0-1023)
  float Votage = (3.3 * VoltageState * (R1 + R2)) / (1024 * R2) - 0.4; //暂存A0的数值,类型为浮点
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 0, "IP: " + String(WiFi.localIP().toString()));
  display.drawString(0, 10, "MAC: " + String(WiFi.macAddress()));
  display.drawString(0, 20, "SSID: " + String(WiFi.SSID()));
  display.drawString(0, 30, "RSSI: " + String(WiFi.RSSI()) + " dB");
  display.drawString(0, 40, "Votage: " + String(Votage, 2) + " V");
  display.drawString(0, 50, " Connection Activated .");
  display.display();
  Blynk.run();
  timer.run();
  ArduinoOTA.handle();
  Blynk.virtualWrite(V5, "IP地址:", WiFi.localIP().toString());
  Blynk.virtualWrite(V6, "MAC地址:", WiFi.macAddress());
  Blynk.virtualWrite(V7, "SSID:", WiFi.SSID(), "   RSSI:", WiFi.RSSI(), " dB");
}
  1. 先编译一下,然后上传即可,如果有错,请检查一下库的版本有没有对上

关于代码里的电压计算公式

有偏差的可以自行修正一下,毕竟不是很精确的传感器,计算方法和电路图如下:


(点击图片放大)

公式很简单:Vin = Vout * (R2/(R1+R2))
R1 = 30000R2 = 7500Vout可以通过使用Vout =(analogvalue * 3.3/1024)模拟输入中可以计算得出结果。

注意:esp8266的模拟量接口只支持3.3V!


APP端的设置与WIFI配网操作流程

配网

上传后,你会看见一个热点名为"Blynk_WIFI_Car_MK2.5",代码中我没设密码,请自行修改.具体流程如下图,图中的操作不是小车的,只是引用一下,但是操作一样,不具体说了,就是连接你路由或者手机热点WIFI-SSID就行:

一般手机连接后会自动弹出验证界面,如果连接后没有弹出,那就就手动打开你的浏览器,输入192.168.4.1进入验证界面

APP端设置

如下图,按途中虚拟引脚的编号设置即可(详细可以参考代码)


点击图片放大

设备操作

开稽! :huaji:

更新Note:

2020-02-21

MK2.5

  1. 优化代码
  2. 修改platformio.ini文件,让IDE自动下载好相关依赖库,先用命令行运行:
  • 编译命令
    platformio run
  • 其他命令
    • platformio run - 编译
    • platformio run -e generic -t upload - 编译并上传
    • platformio run -t clean- 移除已编译文件

IDE会自动安装,如果是第一次运行,会自动下载相关依赖库(包含对应的版本)

2020-01-26:

MK2.5

引入新功能

1.加入OLED驱动,显示启动/重置/网络/电压等相关信息;

2.加入调速功能,使用滑动条设置PWM速度;


2020-01-17:

MK.I:

引脚变动

  1. 项目-取消舵机、远近光灯的功能和所占引脚、资源,控制摄像头拍摄直接使用带云台和夜视功能的成品,照明功能用红外夜视代替;
  2. 加入复位功能按钮(D5),按下按钮不放,点“Reset”,进入WIFI配网;

    小变动

  3. 精简车身,轻量化,链接线路简化,代码简化,电源使用改用8.4V锂电驱动;

  4. 保留摇杆控制(电机接线与MK2版本相反);
  5. 保留电量电压采集(代码保留);

其他更新(MK.1/MK2共有)

  1. 加入OTA更新功能;
  2. 加入实时显示SSID、IP、MAC、RSSI信息功能;

2020-01-17:

MK.II:

变动

  1. 修改电机驱动代码,使内网版APP控制界面与BLYNK物联网平台保持一致;
  2. 统一反馈标准,如重置引脚、配网LED提示、平台接入提示


一沙一世界,一花一天堂。君掌盛无边,刹那成永恒。