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

  很久之前买过一套机械臂,因为工作太忙,所以一直吃灰,到了现在才有空折腾一下。由于当时买的时候只是单机控制的,商家当时配套使用的是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,如下图:

1.3 机械臂组装

  这里放出装配图,仅供参考,大家不一定百分百的按照我这里组装。途中的支架底盘,你可以按照自己思路选型装配,并不需要跟我这个图装的一模一样。










2. 制作PCB

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

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

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 使用操作

  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则为对应代码中的Pos1-Pos3函数
  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°的话,那就会出现角度跑过头的情况了;


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