6自由度舵机PCB制作与Blynk接入

2,338次阅读

时效性以github版本为准,这个版本已经做出模块化PCB,点击这里查看

6自由度舵机PCB制作与Blynk接入

  很久之前买过一套机械臂,因为工作太忙,所以一直吃灰,到了现在才有空折腾一下。由于当时买的时候只是单机控制的,商家当时配套使用的是Arduino UNO R3的主控板,除了可以用微信控制和PS手柄控制以外没啥亮点(主要我当时还只是个小白,就觉得很神奇 :huaji: )。原主板已经集成好舵机插针和电源接口,之后我试过使用ESP8266 AT固件来接入Blynk,结果发现用AT命令接入是没法配合私有服务器使用的,只能用官服,然后就一直没折腾过了。最近我自己重新设计了一下主控板,主要是以模块化为主,把UNO R3换成ESP8266,实现以WiFi方式接入Blynk平台来控制舵机。
  基于道德问题,我这里就不把商家的配套教程资源发出来了,只提供一个机械臂的装配图。毕竟这是人家做的配套产品,人家卖点就是资料配套机械臂的(其实也没什么好发的,要是你熟悉原理以后,就觉得很简单)。本文只给大家说说怎么样组装、基本原理、舵机接线、放出我自己制作的PCB的原理图和GERBER文件(大家可以拿去定做电路板)。
   本文应用场景并不单单只应用于6轴的机械臂,同时也可以用于4轴或者2轴云台之类的应用场景。文中的机械臂只是作为一个应用场景来作为项目演示,本文虽然主要以6自由度(6轴即6个舵机),来阐述原理、装配、原理、接入Blynk云平台。

1. 舵机

   这里是给小白扫盲的,已经熟悉舵机的可以直接跳过。有关更多舵机的原理和讲解,这里就不再过多论述了,百度一下满大街都是。我建议初次接触舵机的,先去买一个小舵机,找一些示例入门程序测试好并理解好舵机的使用,然后再继续以下内容。

1.1 认识舵机

  舵机(又叫伺服电机)是由普通的直流电机,在加上检测电机旋转角度的电路,以及一组减速齿轮组成。当舵机转动时将带动齿轮和电位计,控制电路将从电位计的电压变化得知当前的角度,简要原理图如下:

![](http://www.eeskill.com/Uploads/2014_11/article/6095fcdc5b0eafd41f753c5985d54014.png)

  舵机有各种大小(最小只有数克重)、速度(如从0.6~0.5秒内完成60度角移,一般问0.2秒)、扭力(有的可以高达115KG.CM)。不论那种型号的舵机,他们的控制方法都是一样的。
  舵机的应用范围很多,例如:航模的摆臂、云台、某些机器人关节、机械臂等等。

1.2 舵机接线

  一般的舵机有三根线分别是电源(红色)、负极(黑色或者棕色)、信号(白色或者橙色)。电源大部分为4.8-6之间,有些特殊规格的会有12V或者24V的。市面入门学习比较经典的舵机一般都是SG90,如下图:

6自由度舵机PCB制作与Blynk接入

1.3 机械臂组装

  这里放出装配图,仅供参考,大家不一定百分百的按照我这里组装。途中的支架底盘,你可以按照自己思路选型装配,并不需要跟我这个图装的一模一样。
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入

2. 制作PCB

  因为我想做一个比较整洁项目,如果用面包板什么的话那就来凌乱了,所以做出了一个PCB,把ESP8266的引脚引出,并适配做好适合舵机的插口。这里放出我自己做的扩展板原理图和GERBE文件包,需要的可以下载下来拿去定制PCB成品,往下我会说明一下注意事项,文件我会另外开一个帖子专门放上连接。
6自由度舵机PCB制作与Blynk接入
6自由度舵机PCB制作与Blynk接入

2.1 功能和特色

  • 全程采用模块化,易修、易装、易拆,适合作为学习板或者创客项目制作使用;
  • 适配ESP8266 D1 Mini 开发板,使用2.54mm母座(8+8PIN),;
  • 电源使用市面常见常用的LM2596模块,尺寸兼容大部分淘宝商家款式;
  • 最大化利用了D1 Mini 的引脚,避免引脚功能浪费:
    • 6个通用舵机接口,即插即用;
    • 预留一个IIC液晶屏位置,用于用液晶屏显示项目信息之类内容,另外再并联出一个另一个IIC接口,这个就你自己自由发挥了 :huaji: ;
    • 预留数字信号D5、模拟量A0引脚,兼容大部分市面上的电子积木产品(S V G 3P插头),用于其他内容整合,这个也是你自己自由发挥了 :huaji: ;

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 成品图片预览

6自由度舵机PCB制作与Blynk接入

6自由度舵机PCB制作与Blynk接入

2.5 舵机在成品PCB中的对应插口顺序图

  接下来的代码中定义也是按照这个顺序排序。
6自由度舵机PCB制作与Blynk接入

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();
}
C++

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);
}
C++

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();
}
C++

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);
}
C++

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();
}
}
C++

5. 程序运行

  以下按顺序讲一下各部分的运行流程。

5.1 使用操作

  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)
    • 重启后开机,舵机会对位置复位一次。
  2. Blynk APP端配置

    • 新建留个滑动条小组件,用于舵机的微调动作,引脚分别为:V1-V0,分别对应代码中的1-6号舵机
    • 新建4个按钮小组件,分别是V0 V10 V11 V12。其中V0对应代码中的Pos0函数,为复位动作组,V10-V12则为对应代码中的Pos1Pos3函数
  3. Blynk使用

    • 滑动条:用于舵机的手动控制和微调。
    • 动作组按钮:用于快速控制舵机做出一组动作,我这里只做了4组动作用于演示,大家可以在Servo_Setup.h文件中修改每个组中每个舵机的角度,也可以新建动作组。如果新建动作组,必须要在Blynk_POS_Group.h文件中增加相应的动作组按钮函数。

5.2 硬件使用

  1. OLED部分

    • 进度条:当你对更新或者上传代码后,OLED会显示一个进度条,完成后会提示重启Restset
    • 开机画面:提示WiFi AP名称和密码
    • 待机画面:分别显示SSID HOSTNAME RSSI IP地址 MAC地址
  2. WiFi设置复位

    • 关机
    • 将D5与GND短路
    • 开机
    • 液晶屏会提示已经激活充重置模式
    • 等待重置成功,OLED会出现提示5秒倒数重启的字符

5.3 演示视频



后续补充

  • 有时间我会制作一下其他方式控制的版本,例如串口、Web界面等;
  • PCB中的舵机插口我忘记做标识了每个插口上的3个针从左到右分别为:信号 5V GND
  • 注意舵机的初始安装角度,否则会导致你的舵机转角不正确,严重的会导致舵机卡死堵死烧掉;
  • 在Blynk中,注意调整好滑动条最大活动值,例如初始默认值是滑动条0-1023 ,而你的舵机是270°,这时候你只想要180°的话,那就会出现角度跑过头的情况了;

[toc]

正文完
post-qrcode
 0