很久之前买过一套机械臂,因为工作太忙,所以一直吃灰,到了现在才有空折腾一下。由于当时买的时候只是单机控制的,商家当时配套使用的是Arduino UNO R3的主控板,除了可以用微信控制和PS手柄控制以外没啥亮点(主要我当时还只是个小白,就觉得很神奇 )。原主板已经集成好舵机插针和电源接口,之后我试过使用ESP8266 AT固件来接入Blynk,结果发现用AT命令接入是没法配合私有服务器使用的,只能用官服,然后就一直没折腾过了。最近我自己重新设计了一下主控板,主要是以模块化为主,把UNO R3换成ESP8266,实现以WiFi方式接入Blynk平台来控制舵机。
基于道德问题,我这里就不把商家的配套教程资源发出来了,只提供一个机械臂的装配图。毕竟这是人家做的配套产品,人家卖点就是资料配套机械臂的(其实也没什么好发的,要是你熟悉原理以后,就觉得很简单)。本文只给大家说说怎么样组装、基本原理、舵机接线、放出我自己制作的PCB的原理图和GERBER文件(大家可以拿去定做电路板)。
本文应用场景并不单单只应用于6轴的机械臂,同时也可以用于4轴或者2轴云台之类的应用场景。文中的机械臂只是作为一个应用场景来作为项目演示,本文虽然主要以6自由度(6轴即6个舵机),来阐述原理、装配、原理、接入Blynk云平台。
1. 舵机
这里是给小白扫盲的,已经熟悉舵机的可以直接跳过。有关更多舵机的原理和讲解,这里就不再过多论述了,百度一下满大街都是。我建议初次接触舵机的,先去买一个小舵机,找一些示例入门程序测试好并理解好舵机的使用,然后再继续以下内容。
1.1 认识舵机
舵机(又叫伺服电机)是由普通的直流电机,在加上检测电机旋转角度的电路,以及一组减速齿轮组成。当舵机转动时将带动齿轮和电位计,控制电路将从电位计的电压变化得知当前的角度,简要原理图如下:
舵机有各种大小(最小只有数克重)、速度(如从0.6~0.5秒内完成60度角移,一般问0.2秒)、扭力(有的可以高达115KG.CM)。不论那种型号的舵机,他们的控制方法都是一样的。
舵机的应用范围很多,例如:航模的摆臂、云台、某些机器人关节、机械臂等等。
1.2 舵机接线
一般的舵机有三根线分别是电源(红色)、负极(黑色或者棕色)、信号(白色或者橙色)。电源大部分为4.8-6之间,有些特殊规格的会有12V或者24V的。市面入门学习比较经典的舵机一般都是SG90,如下图:
1.3 机械臂组装
这里放出装配图,仅供参考,大家不一定百分百的按照我这里组装。途中的支架底盘,你可以按照自己思路选型装配,并不需要跟我这个图装的一模一样。
2. 制作PCB
因为我想做一个比较整洁项目,如果用面包板什么的话那就来凌乱了,所以做出了一个PCB,把ESP8266的引脚引出,并适配做好适合舵机的插口。这里放出我自己做的扩展板原理图和GERBE文件包,需要的可以下载下来拿去定制PCB成品,往下我会说明一下注意事项,文件我会另外开一个帖子专门放上连接。
2.1 功能和特色
- 全程采用模块化,易修、易装、易拆,适合作为学习板或者创客项目制作使用;
- 适配ESP8266 D1 Mini 开发板,使用2.54mm母座(8+8PIN),;
- 电源使用市面常见常用的LM2596模块,尺寸兼容大部分淘宝商家款式;
- 最大化利用了D1 Mini 的引脚,避免引脚功能浪费:
- 6个通用舵机接口,即插即用;
- 预留一个IIC液晶屏位置,用于用液晶屏显示项目信息之类内容,另外再并联出一个另一个IIC接口,这个就你自己自由发挥了 ;
- 预留数字信号D5、模拟量A0引脚,兼容大部分市面上的电子积木产品(S V G 3P插头),用于其他内容整合,这个也是你自己自由发挥了 ;
2.2 PCB焊接所需要的零件清单
项目名称 | 数量 | 单位 | 备注 |
---|---|---|---|
ESP8266 D1 Mini | 1 | 块 | 建议使用普通版 |
0.96英寸OLED | 1 | 个 | IIC接口,SSD1306主控,市面上很多版本,注意引脚必须和我PCB匹配 |
LM2596降压模块一个 | 1 | 个 | 尺寸:43 X 21 X 14 |
舵机 | 1-6 | 个 | 0-180°的舵机即可,我这里的是270°。并不要求一定要做成机械臂,也不要求一定要有6个,你可以按自己喜好做成其他项目,例如摄像头小云台之类 |
KF301-2P接线端子 | 1 | 个 | |
四脚轻触式开关 | 1 | 个 | 尺寸:6mm X 6mm |
2.3 PCB引脚定义
引脚表示 | 定义 | 用途 | 备注 |
---|---|---|---|
D0 | 舵机1 | / | 建议使用普通版 |
01 | SCL | 预留IIC接口 | 液晶屏或其他IIC传感器 |
D2 | SDA | 预留IIC接口 | 液晶屏或其他IIC传感器 |
D3 | 舵机2 | / | |
D4 | 舵机3 | / | |
D5 | D5 | 预留数字引脚 | 本文中用于复位WiFi设置用 |
D6 | 舵机4 | / | |
D7 | 舵机5 | / | |
D8 | 舵机6 | / | |
A0 | A0 | 预留模拟量接口 | 可以用来接入电压传感器什么的 |
- | GND | 电源GND输入PCB背面铺铜共用地 | |
+ | VCC/IN | 电源正极 | LM2596为5.5-24V输入 |
OUT+ | OUT+ | 降压模块输出的正极,电压为5V | 与舵机插口的正极和PCB的正面铺铜共用5V |
2.4 成品图片预览
2.5 舵机在成品PCB中的对应插口顺序图
接下来的代码中定义也是按照这个顺序排序。
3. 事前准备
3.1 软件
这里使用的是Arduino的架构,按下列清单准备好相关环境。
3.1.1 开发环境
- PlatformIO或者Arduino IDE
3.1.2 开发环境程序库
3.2 硬件
这里硬件准备,跟上文PCB零件差不多,只是某些零件不是必要的,我这里只是重新整理一下,假如列出没有PCB情况下需要的东西。对于新手来说,最好用我这里发布的PCB,因为接线很简单,我在PCB内已经尽量简化了。本文项目不一定必须全部备齐才能进行,表格里我会说明,凡是标记可选
的都可以不准备。
项目名称 | 数量 | 单位 | 备注 |
---|---|---|---|
ESP8266 D1 Mini或者NodeMCU | 1 | 块 | |
0.96英寸OLED | 1 | 个 | IIC接口,SSD1306主控,可选,用于显示OTA进度条和待机网络信息 |
LM2596降压模块一个 | 1 | 个 | 你可以拿一个5V的电压代替,最好5V-2A左右 |
面包板 | 1 | 块 | |
杜邦线 | 若干 | 条 | |
开关 | 1 | 个 | 可选,用于WiFi设置复位,电子积木的开关模块也行 |
4. 代码
由于避免代码写得过于臃肿,我这里把主程序和其他功能函数分开写了,所以篇幅比较长,手机不方便查看的可以去我的GitHub仓库看,地址:https://github1s.com/chrisxs/Blynk_Projects/blob/main/D1_Mini_Blynk_6_Servo/src/main.cpp 。代码的准确性和时效性按我的GitHub为准,这里是初始版本的代码,连接GitHub速度慢的朋友请往下继续看。
4.1 主程序
#define BLYNK_PRINT Serial
#include <FS.h> //this needs to be first, or it all crashes and burns...
#include <Arduino.h>
/////WiFiManager/////
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager
#include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson
/////OTA/////
#include <ArduinoOTA.h>
#include <WiFiUdp.h>
////Blynk/////
#include <BlynkSimpleEsp8266.h>
/////OLED设置/////
#include "OLED_Setup.h"
/////舵机设置/////
#include "Servo_Setup.h"
/////Blynk舵机滑动条/////
#include "Blynk_Slider.h"
#include "Blynk_POS_Group.h"
#include <string>
#include <stdlib.h>
//用于WiFiManager界面中的变量服务器域名、端口、口令
std::string blynk_server;
std::string blynk_port;
std::string blynk_token;
//标记是否储存
bool shouldSaveConfig = false;
const int ResetButton = D5;
int ResetButtonState = digitalRead(ResetButton);
//回调通知我们需要保存配置
void saveConfigCallback()
{
Serial.println("Should save config");
shouldSaveConfig = true;
}
//当Blynk连接时,同步APP端的引脚状态
BLYNK_CONNECTED()
{
Blynk.syncAll();
//Blynk.syncVirtual(V1, V2, V3, V4, V5, V6);
}
void setup()
{
Serial.begin(115200);
Serial.println();
pinMode(ResetButton, INPUT_PULLUP);
AtatchServo();
ServoDefaultPOS();
/////OLED/////
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_10);
/////WiFiManager/////
//从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);
#ifdef ARDUINOJSON_VERSION_MAJOR >= 6
DynamicJsonDocument json(1024);
auto deserializeError = deserializeJson(json, buf.get());
serializeJson(json, Serial);
if (!deserializeError)
{
#else
DynamicJsonBuffer jsonBuffer;
JsonObject &json = jsonBuffer.parseObject(buf.get());
json.printTo(Serial);
if (json.success())
{
#endif
Serial.println("\nparsed json");
blynk_server = json["blynk_server"].as<const char *>();
blynk_port = json["blynk_port"].as<const char *>();
blynk_token = json["blynk_token"].as<const char *>();
}
else
{
Serial.println("failed to load json config");
}
configFile.close();
}
}
}
else
{
Serial.println("failed to mount FS");
}
//读取部分结束
WiFiManagerParameter custom_blynk_server("server", "blynk server", blynk_server.c_str(), 40);
WiFiManagerParameter custom_blynk_port("port", "blynk port", blynk_port.c_str(), 6);
WiFiManagerParameter custom_blynk_token("blynk", "blynk token", blynk_token.c_str(), 32);
WiFiManagerParameter custom_text("<p>点击SSID名称选择连接WiFi,并输入密码/服务器地址/设备口令</p>");
//WiFiManager 初始化对象
WiFiManager wifiManager;
wifiManager.setSaveConfigCallback(saveConfigCallback);
//这里可以加入你要的范围项目
wifiManager.addParameter(&custom_blynk_server);
wifiManager.addParameter(&custom_blynk_port);
wifiManager.addParameter(&custom_blynk_token);
wifiManager.addParameter(&custom_text);
//当D5(按钮、低电平)被按下,ESP8266就进入重置模式,OLED屏幕会有提示
if (ResetButtonState == LOW)
{
Serial.println("Getting Reset ESP Wifi-Setting.......");
ResetMode();
wifiManager.resetSettings();
delay(5000);
Serial.println("Formatting FS......");
SPIFFS.format();
RebootCountdown();
ESP.restart();
}
ShowAP_SSID();
if (!wifiManager.autoConnect("RobotArm", ""))
{
Serial.println("failed to connect and hit timeout");
delay(3000);
//失败后重启ESP8266
ESP.reset();
delay(5000);
}
//提示WiFi连接成功
Serial.println("connected.)");
//读取已被更新的项目
blynk_server = custom_blynk_server.getValue();
blynk_port = custom_blynk_port.getValue();
blynk_token = custom_blynk_token.getValue();
Serial.println("The values in the file are: ");
Serial.println("\tblynk_server: " + String(custom_blynk_server.getValue()));
Serial.println("\tblynk_port : " + String(custom_blynk_port.getValue()));
Serial.println("\tblynk_token : " + String(custom_blynk_token.getValue()));
//把以被编辑的项目存储到FS
if (shouldSaveConfig)
{
Serial.println("saving config");
#ifdef ARDUINOJSON_VERSION_MAJOR >= 6
DynamicJsonDocument json(1024);
#else
DynamicJsonBuffer jsonBuffer;
JsonObject &json = jsonBuffer.createObject();
#endif
json["blynk_server"] = blynk_server.c_str();
json["blynk_port"] = blynk_port.c_str();
json["blynk_token"] = blynk_token.c_str();
File configFile = SPIFFS.open("/config.json", "w");
if (!configFile)
{
Serial.println("failed to open config file for writing");
}
#ifdef ARDUINOJSON_VERSION_MAJOR >= 6
serializeJson(json, Serial);
serializeJson(json, configFile);
#else
json.printTo(Serial);
json.printTo(configFile);
#endif
configFile.close();
//结束存储
}
Serial.println("local ip");
Serial.println(WiFi.localIP());
delay(500);
/////OTA/////
//OTA主机名为RobotArm
ArduinoOTA.setHostname("RobotArm");
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
{
type = "sketch";
}
else
{ // U_SPIFFS
type = "filesystem";
}
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
display.clear();
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
display.drawString(display.getWidth() / 2, display.getHeight() / 2, "Restart");
display.display();
});
//OLED显示OTA传输进度条
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
display.clear();
display.drawProgressBar(4, 32, 120, 8, progress / (total / 100));
display.display();
});
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();
Blynk.config(blynk_token.c_str(), blynk_server.c_str(), std::atoi(blynk_port.c_str()));
}
void loop()
{
ArduinoOTA.handle();
Blynk.run();
drawinfo();
}
4.2 编写设置舵机和动作组的头文件
Servo_Setup.h
#include <Servo.h>
Servo servo1, servo2, servo3, servo4, servo5, servo6;
void AtatchServo()
{
servo1.attach(D0);
servo2.attach(D3);
servo3.attach(D4);
servo4.attach(D6);
servo5.attach(D7);
servo6.attach(D8);
}
void ServoDefaultPOS()
{
int pos = 90;
servo1.write(pos);
servo2.write(pos);
servo3.write(pos);
servo4.write(pos);
servo5.write(pos);
servo6.write(pos);
}
void Pos_0()
{
int pos = 90;
servo1.write(pos);
delay(1000);
servo2.write(pos);
delay(1000);
servo3.write(pos);
delay(1000);
servo4.write(pos);
delay(1000);
servo5.write(pos);
delay(1000);
servo6.write(pos);
delay(1000);
Blynk.virtualWrite(V1, pos);
Blynk.virtualWrite(V2, pos);
Blynk.virtualWrite(V3, pos);
Blynk.virtualWrite(V4, pos);
Blynk.virtualWrite(V5, pos);
Blynk.virtualWrite(V6, pos);
}
void Pos_1()
{
int pos1 = 145;
int pos2 = 110;
int pos3 = 125;
int pos4 = 149;
int pos5 = 90;
int pos6 = 10;
servo1.write(pos1);
delay(500);
servo2.write(pos2);
delay(500);
servo3.write(pos3);
delay(500);
servo4.write(pos4);
delay(500);
servo5.write(pos5);
delay(500);
servo6.write(pos6);
delay(500);
Blynk.virtualWrite(V1, pos1);
Blynk.virtualWrite(V2, pos2);
Blynk.virtualWrite(V3, pos3);
Blynk.virtualWrite(V4, pos4);
Blynk.virtualWrite(V5, pos5);
Blynk.virtualWrite(V6, pos6);
}
void Pos_2()
{
int pos1 = 145;
int pos2 = 70;
int pos3 = 125;
int pos4 = 150;
int pos5 = 160;
int pos6 = 100;
servo1.write(pos1);
delay(500);
servo2.write(pos2);
delay(500);
servo3.write(pos3);
delay(500);
servo4.write(pos4);
delay(500);
servo5.write(pos5);
delay(500);
servo6.write(pos6);
delay(500);
Blynk.virtualWrite(V1, pos1);
Blynk.virtualWrite(V2, pos2);
Blynk.virtualWrite(V3, pos3);
Blynk.virtualWrite(V4, pos4);
Blynk.virtualWrite(V5, pos5);
Blynk.virtualWrite(V6, pos6);
}
void Pos_3()
{
int pos1 = 90;
int pos2 = 70;
int pos3 = 90;
int pos4 = 160;
int pos5 = 90;
int pos6 = 10;
servo1.write(pos1);
delay(500);
servo2.write(pos2);
delay(500);
servo3.write(pos3);
delay(500);
servo4.write(pos4);
delay(500);
servo5.write(pos5);
delay(500);
servo6.write(pos6);
delay(500);
Blynk.virtualWrite(V1, pos1);
Blynk.virtualWrite(V2, pos2);
Blynk.virtualWrite(V3, pos3);
Blynk.virtualWrite(V4, pos4);
Blynk.virtualWrite(V5, pos5);
Blynk.virtualWrite(V6, pos6);
}
4.3 用于设置OLED的头文件
OLED_Setup.h
#include <Wire.h> // Only needed for Arduino 1.6.5 and earlier
#include "SSD1306Wire.h"
SSD1306Wire display(0x3c, D2, D1); // 设置OLED屏幕的名称/引脚/地址
void ShowAP_SSID()
{
display.clear();
display.drawString(0, 10, "AP-SSID:RobotArm");
display.drawString(0, 20, "Password:none");
display.display();
}
void RebootCountdown()
{
display.clear();
display.drawString(5, 25, "Reboot in 5 Sec !");
display.display();
delay(1000);
display.clear();
display.drawString(5, 25, "Reboot in 4 Sec !");
display.display();
delay(1000);
display.clear();
display.drawString(5, 25, "Reboot in 3 Sec !");
display.display();
delay(1000);
display.clear();
display.drawString(5, 25, "Reboot in 2 Sec !");
display.display();
delay(1000);
display.clear();
display.drawString(5, 25, "Reboot in 1 Sec !");
display.display();
delay(1000);
}
void ResetMode()
{
display.setFont(ArialMT_Plain_10);
display.clear();
display.drawString(0, 40, "RESET mode activated .");
display.drawString(0, 50, "Please wait for reboot !");
display.display();
}
void drawinfo()
{
display.setFont(ArialMT_Plain_10);
display.clear();
display.drawString(0, 0, "Hostname: " + String(WiFi.hostname()));
display.drawString(0, 10, "RSSI: " + String(WiFi.RSSI()) + " dB");
display.drawString(0, 20, "MAC: " + String(WiFi.macAddress()));
display.drawString(0, 30, "IP: " + String(WiFi.localIP().toString()));
display.drawString(0, 40, "SSID: " + String(WiFi.SSID()));
display.display();
}
4.4 用于设置Blynk中的滑动条的头文件
Blynk_Slider.h
/////设置舵机在Blynk中的滑动条虚拟引脚/////
BLYNK_WRITE(V1)
{
int state = param.asInt();
servo1.write(param.asInt());
}
BLYNK_WRITE(V2)
{
int state = param.asInt();
servo2.write(param.asInt());
}
BLYNK_WRITE(V3)
{
int state = param.asInt();
servo3.write(param.asInt());
}
BLYNK_WRITE(V4)
{
int state = param.asInt();
servo4.write(param.asInt());
}
BLYNK_WRITE(V5)
{
int state = param.asInt();
servo5.write(state);
}
BLYNK_WRITE(V6)
{
int state = param.asInt();
servo6.write(state);
}
4.5 用于设置Blynk中的动作组按钮的头文件
Blynk_POS_Group.h
/////动作组按钮-0/////
BLYNK_WRITE(V0)
{
int state = param.asInt();
if (state == 1)
{
Pos_0();
}
}
BLYNK_WRITE(V10)
{
int state = param.asInt();
if (state == 1)
{
Pos_1();
}
}
BLYNK_WRITE(V11)
{
int state = param.asInt();
if (state == 1)
{
Pos_2();
}
}
BLYNK_WRITE(V12)
{
int state = param.asInt();
if (state == 1)
{
Pos_3();
}
}
5. 程序运行
以下按顺序讲一下各部分的运行流程。
5.1 使用操作
-
接入WiFi和Blynk
- 在代码写入后,ESP8266会进入第一次开机(按理来说你的8266不会有存储过任何WiFi配置)。开机后会出现一个名为
RobotArm
的WiFi热点,默认无密码,直接点击进入即可,如下图:
![](https://i.imgur.com/ESuTaS0.jpeg) - 连接到该热点后,手机一般会自动弹出一个认证网页,如果没有则在浏览器输入
192.168.4.1
进入WiFi配网界面,见下图
![](https://i.imgur.com/7vPWrYq.jpeg) - 进入后选择
configure WiFi
,会进入和下图类似的配网界面,直接点击SSID
即可选择该SSID,当然你可以手动填入,分别将Blynk Server
Blynk Port
Blynk Token
填好后点击save
即可保存,之后设备会自动重启并以你填入的信息连接WiFi和Blynk服务器。
![](https://i.imgur.com/30hhwil.jpeg) - 重启后开机,舵机会对位置复位一次。
- 在代码写入后,ESP8266会进入第一次开机(按理来说你的8266不会有存储过任何WiFi配置)。开机后会出现一个名为
-
Blynk APP端配置
- 新建留个滑动条小组件,用于舵机的微调动作,引脚分别为:
V1-V0
,分别对应代码中的1-6号舵机 - 新建4个按钮小组件,分别是
V0
V10
V11
V12
。其中V0对应代码中的Pos0
函数,为复位动作组,V10-V12则为对应代码中的Pos1
-Pos3
函数
- 新建留个滑动条小组件,用于舵机的微调动作,引脚分别为:
-
Blynk使用
- 滑动条:用于舵机的手动控制和微调。
- 动作组按钮:用于快速控制舵机做出一组动作,我这里只做了4组动作用于演示,大家可以在
Servo_Setup.h
文件中修改每个组中每个舵机的角度,也可以新建动作组。如果新建动作组,必须要在Blynk_POS_Group.h
文件中增加相应的动作组按钮函数。
5.2 硬件使用
-
OLED部分
- 进度条:当你对更新或者上传代码后,OLED会显示一个进度条,完成后会提示重启
Restset
- 开机画面:提示WiFi AP名称和密码
- 待机画面:分别显示SSID HOSTNAME RSSI IP地址 MAC地址
- 进度条:当你对更新或者上传代码后,OLED会显示一个进度条,完成后会提示重启
-
WiFi设置复位
- 关机
- 将D5与GND短路
- 开机
- 液晶屏会提示已经激活充重置模式
- 等待重置成功,OLED会出现提示5秒倒数重启的字符
5.3 演示视频
后续补充
- 有时间我会制作一下其他方式控制的版本,例如串口、Web界面等;
- PCB中的舵机插口我忘记做标识了每个插口上的3个针从左到右分别为:
信号
5V
GND
; - 注意舵机的初始安装角度,否则会导致你的舵机转角不正确,严重的会导致舵机卡死堵死烧掉;
- 在Blynk中,注意调整好滑动条最大活动值,例如初始默认值是滑动条
0-1023
,而你的舵机是270°,这时候你只想要180°的话,那就会出现角度跑过头的情况了;