站内搜索
发作品签到
专业版

机械狗1

工程标签

333
0
0
0

简介

该机械狗由四个舵机、一个电池盒及电池、oled屏幕等组成,并可简单的对真实小狗进行仿真例如前进后退,搭配了led显示屏可显示各种表情,打开开关可自动复原腿部位置,可通过软件遥控

简介:该机械狗由四个舵机、一个电池盒及电池、oled屏幕等组成,并可简单的对真实小狗进行仿真例如前进后退,搭配了led显示屏可显示各种表情,打开开关可自动复原腿部位置,可通过软件遥控
智能机器狗实训营

开源协议

Public Domain

创建时间:2025-03-30 17:54:50更新时间:2025-05-06 19:41:41

描述

项目功能

  • ✅手机遥控
  • ✅表情显示
  • ✅每日天气
  • ✅时钟显示

项目参数

  • 本设计采用ESP8266主控,内置WIFI功能,通过AP模式遥控
  • 本设计采用0.96寸OLED显示屏,支持SSD1315/SSD1306驱动,可显示表情、时钟、天气等相关信息
  • 选用AMS1117 LDO线性稳压器,负责将8.4V和5V电压分别转换成5V和3.3V,为舵机及主控提供电源
  • 项目支持SG-90/MG-90 180度及360度版本,推荐使用180度版本,自带限位器,无需校准电机。

原理解析

篇幅有限,这里仅讲解部分关键电路和程序,详细说明请查看开发文档

本项目提供了完整的说明及源码,并不局限于现有功能,您可以基于现有框架进行二
次开发,丰富小狗的功能。

硬件设计

本项目电路由以下部分组成,电源部分、ESP8266主控、外部接口

电源电路:

image.png
供电:供电是直接采用14500双节电池组,通过LDO降压稳压器供电。

image.png
稳压器:采用AMS1117-5V和AMS1117-3.3V线性稳压器,将8.4V电压分别转换成5V和3.3V,使其为舵机及ESP8266主控提供电力支持

主控电路:

image.png

主控:参考ESP8266数据手册,对IO0、IO2、EN使能、RST重置引脚上拉,对CS片选信号下拉,以确保ESP8266及SPI通信正常

外部接口电路:

image.png

image.png

image.png
屏幕:为了方便焊接,简化电路,这里使用的是SSD1315驱动的OLED屏幕模块,该模块自带有屏幕驱动电路,仅需接口接入即可。在此根据该屏幕模块的接口线序配置好了对应接口的线序,直接插入即可使用。
串口:串口部分为方便下载,单独引出了IO0及GND接口作为跳帽插入接口,当插入跳帽时,IO0被拉低,进入下载模式。反之被主控部分电路拉高,进入工作模式。
电池:电池部分,引出了外部充电拓展接口,VIN与VBAT是开关接口,VIN与GND接口是外部充电模块接口。充电模块选择满电电压大概在8.4V的2串锂电池充电模块。
按键:按键部分使用的是IO2和IO15引脚,IO2按键按下时拉低,空闲时被拉高。但由于IO15必须接下拉电阻,所以这里开关逻辑与IO2相反,按键按下时拉高,空闲时被拉低。

ADC电量检测电路

image.png
修改分压器适配 8.4V 到 1V
现在需要适配新的输入电压范围(最大 8.4V)到 ESP8266 的 1.0V ADC 输入。分压比计算如下:

分压比=1V8.4V=18.4≈0.119分压比=8.4V1V=8.410.119

根据分压公式:

R2R1+R2=0.119R1+R2R2=0.119

假设保持100k ,计算 :
100kR1+100k=0.119R1+100k100k=0.119

R1+100k=100k0.119≈840kR1+100k=0.119100k840k

R1≈740kΩR1740kΩ

对于 ,输出电压:

Vout=8.4×100k740k+100k=8.4×100840≈1.0VVout=8.4×740k+100k100k=8.4×8401001.0V

对于电压较低时(如 4.2V),输出电压为:

Vout=4.2×100k740k+100k=4.2×100840≈0.5VVout=4.2×740k+100k100k=4.2×8401000.5V

分压电路成功将 8.4V 的输入电压压缩到 0-1V 范围内。

软件代码

完整的项目文件已经放在了附件,可直接烧录的程序二进制及文件系统二进制文件也已经放在了附件,可自行下载。
时间有限,程序中可能存在部分代码冗余和低代码逻辑,可自行进行修改优化。
这里我们只介绍部分比较重要的关键代码。

运动控制定义

int engine1 = 14;                // 舵机引脚
int engine1offsetleftpwm = -93;   // 舵机左转补偿
int engine1offsetrightpwm = -87; // 舵机左转补偿-40
int engine2 = 16;                // 舵机引脚
int engine2offsetleftpwm = -120;  // 舵机左转补偿
int engine2offsetrightpwm = -122; // 舵机左转补偿-60
int engine3 = 12;                // 舵机引脚
int engine3offsetleftpwm = -3;   // 舵机左转补偿
int engine3offsetrightpwm = -57; // 舵机左转补偿
int engine4 = 13;                // 舵机引脚
int engine4offsetleftpwm = -78;  // 舵机左转补偿
int engine4offsetrightpwm = -109; // 舵机左转补偿-71
int speed = 300;                 // 舵机转速
int runtime = 100;                 // 运动延时**预留变量,用于控制动作连贯性,如果你不知道这是什么不建议修改**

这里的定义是用于电机校准使用的,参数仅供参考,不同批次电机参数各不相同,可以自行通过电机校准页校准。

控制页面CSS样式表

        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            text-align: center;
        }

        h1 {
            text-align: center;
        }

        button {
            display: inline-block;
            height: auto;
            width: auto;
            margin-top: 20px;

            padding: 10px 20px;
            background-color: deepskyblue;
            color: #fff;
            border: none;
            border-radius: 20px; /* 添加圆角 */
            text-decoration: none;
            line-height: 2; /* 通过调整line-height的值来调整文字的垂直位置 */
            text-align: center; /* 文字居中 */
            box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* 添加立体感 */
            transition: all 0.3s ease; /* 添加过渡效果 */
        }

        button:hover {
            background-color: skyblue; /* 鼠标悬停时的背景颜色 */
            transform: translateY(2px); /* 点击效果 */
            box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); /* 添加更多立体感 */
        }
        .button-grid3 {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 10px;
            justify-content: center;
            align-content: center;
            text-align: center;
            margin: 20px;
        }
        .button-grid2 {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 10px;
            justify-content: center;
            align-content: center;
            text-align: center;
            margin: 20px;
        } .button-grid1 {
              display: grid;
              border-radius: 20px; /* 添加圆角 */
              grid-template-columns: repeat(1, 1fr);
              justify-content: center;
              align-content: center;
              text-align: center;
              margin: 10px;

          }

控制页面JavaScript代码

        // 简化 AJAX 请求函数
        function sendCommand(action) {
            fetch(`/${action}`)
                .then(response => response.text())
                .catch(() => alert('发送失败,请检查设备连接'));
        }    
        function refreshState(url, displayElementId) {
            fetch(url)
                .then(response => response.text())
                .then(data => {                    document.getElementById(displayElementId).innerText = data;
                });
        }
        function setRefreshInterval(url, displayElementId) {
            setInterval(() => refreshState(url, displayElementId), 1000);
        }

        const states = [
         { url: '/batteryVoltage', displayId: 'batteryVoltageDisplay' },
            { url: '/batteryPercentage', displayId: 'batteryPercentageDisplay' },
            { url: '/engine1offsetleftpwm', displayId: 'engine1offsetleftpwmDisplay' },
            { url: '/engine1offsetrightpwm', displayId: 'engine1offsetrightpwmDisplay' },
            { url: '/engine2offsetleftpwm', displayId: 'engine2offsetleftpwmDisplay' },
            { url: '/engine2offsetrightpwm', displayId: 'engine2offsetrightpwmDisplay' },
            { url: '/engine3offsetleftpwm', displayId: 'engine3offsetleftpwmDisplay' },
            { url: '/engine3offsetrightpwm', displayId: 'engine3offsetrightpwmDisplay' },
            { url: '/engine4offsetleftpwm', displayId: 'engine4offsetleftpwmDisplay' },
            { url: '/engine4offsetrightpwm', displayId: 'engine4offsetrightpwmDisplay' }
        ];

        states.forEach(state => setRefreshInterval(state.url, state.displayId));

    

控制页面HTML代码

EDA-Robot遥控台

本项目基于ESP8266主控开发

电压:0

电量:0

运动控制

← →
抬左手 抬右手 坐下 趴下 自由模式开 自由模式关

表情控制

开心 生气 难受 好奇 喜欢 错误 晕

联网功能

时间 天气

控制页面的代码是存放在FS文件系统中的,这里主要看AJAX请求函数,这部分的请求与等下的页面路由监听代码相对应,我们通过点击页面按钮触发请求。这里进行了一些简化操作,避免html过长过大导致html加载和响应缓慢,这可能导致esp8266无法正确显示页面。

页面路由监听

void handleWiFiConfig()
{
    // 启动服务器
    server.on("/left90", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 10;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/right90", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 11;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/front", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 1;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/back", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 4;   // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/left", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 2;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/right", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       actionstate = 3;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/toplefthand", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 5;   // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/toprighthand", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 6;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/sitdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        actionstate = 8;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/lie", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      actionstate = 7; 
        request->send(200, "text/plain", "Front function started"); });
    // server.on("/dance", HTTP_GET, [](AsyncWebServerRequest *request)
    //           {
    //     actionstate = 7;  // 设置标志,执行舵机动作
    //     request->send(200, "text/plain", "Front function started"); });
    server.on("/free", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      freestate=true;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/offfree", HTTP_GET, [](AsyncWebServerRequest *request)
              {
      freestate=false;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/histate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        emojiState = 0;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/angrystate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        emojiState = 1;   // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });

    server.on("/errorstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 2;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine1offsetleftpwm)); });
    server.on("/engine2offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine2offsetleftpwm)); });
    server.on("/engine3offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine3offsetleftpwm)); });
    server.on("/engine4offsetleftpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine4offsetleftpwm)); });
    server.on("/engine1offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine1offsetrightpwm)); });
    server.on("/engine2offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine2offsetrightpwm)); });
    server.on("/engine3offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine3offsetrightpwm)); });
    server.on("/engine4offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine4offsetrightpwm)); });
    server.on("/engine4offsetrightpwm", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(engine4offsetrightpwm)); });
                  server.on("/batteryVoltage", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(batteryVoltage)); });     
    server.on("/batteryPercentage", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(batteryPercentage)); });  
    server.on("/speed", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", String(speed)); });
    server.on("/speedup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       speed++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/speeddown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
    speed--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });

    server.on("/engine1offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine1offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine1offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });

    server.on("/engine2offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine2offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine2offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });

    server.on("/engine3offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine3offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine3offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });

    server.on("/engine4offsetrightpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetrightpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetrightpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetrightpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetleftpwmup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetleftpwm++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/engine4offsetleftpwmdown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       engine4offsetleftpwm--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/speedup", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       speed++;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/speeddown", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       speed--;
        request->send(200, "text/plain", "Front function started"); });
    server.on("/dowhatstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 3;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/lovestate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 4;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/sickstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 5;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });
    server.on("/yunstate", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 6; 
        request->send(200, "text/plain", "Front function started"); });
    server.on("/time", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 8; 
        request->send(200, "text/plain", "Front function started"); });
    server.on("/weather", HTTP_GET, [](AsyncWebServerRequest *request)
              {
       emojiState = 7;  // 设置标志,执行舵机动作
        request->send(200, "text/plain", "Front function started"); });

    server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request)
              {
        // 获取POST参数:ssid、pass、uid、city、api
        String ssid = request->getParam("ssid", true)->value();
        String pass = request->getParam("pass", true)->value();
        String uid = request->getParam("uid", true)->value();
        String city = request->getParam("city", true)->value();
        String api = request->getParam("api", true)->value();

        // 打印接收到的参数
        Serial.println(ssid);
        Serial.println(pass);

        // 保存WiFi信息到JSON文件
        DynamicJsonDocument doc(1024);
        doc["ssid"] = ssid;
        doc["pass"] = pass;
        doc["uid"] = uid;
        doc["city"] = city;
        doc["api"] = api;
        fs::File file = SPIFFS.open(ssidFile, "w");  // 打开文件进行写入
        if (file) {
            serializeJson(doc, file);  // 将JSON内容写入文件
            file.close();  // 关闭文件
        }

        // 更新全局变量
        useruid = uid;
        cityname = city;
        weatherapi = api;

        // 开始连接WiFi
        WiFi.begin(ssid.c_str(), pass.c_str());
        // 发送HTML响应,告知用户正在连接
        request->send(200, "text/html", "

Connecting...

"); }); server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { // 检查SPIFFS文件系统中是否存在index.html文件 if (SPIFFS.exists("/index.html")) { fs::File file = SPIFFS.open("/index.html", "r"); // 打开index.html文件 if (file) { size_t fileSize = file.size(); // 获取文件大小 String fileContent; // 逐字节读取文件内容 while (file.available()) { fileContent += (char)file.read(); } file.close(); // 关闭文件 // 返回HTML内容 request->send(200, "text/html", fileContent); return; } } // 如果文件不存在,返回404错误 request->send(404, "text/plain", "File Not Found"); }); server.on("/control.html", HTTP_GET, [](AsyncWebServerRequest *request) { // 检查SPIFFS文件系统中是否存在index.html文件 if (SPIFFS.exists("/control.html")) { fs::File file = SPIFFS.open("/control.html", "r"); // 打开index.html文件 if (file) { size_t fileSize = file.size(); // 获取文件大小 String fileContent; // 逐字节读取文件内容 while (file.available()) { fileContent += (char)file.read(); } file.close(); // 关闭文件 // 返回HTML内容 request->send(200, "text/html", fileContent); return; } } // 如果文件不存在,返回404错误 request->send(404, "text/plain", "File Not Found"); }); server.on("/engine.html", HTTP_GET, [](AsyncWebServerRequest *request) { // 检查SPIFFS文件系统中是否存在index.html文件 if (SPIFFS.exists("/engine.html")) { fs::File file = SPIFFS.open("/engine.html", "r"); // 打开index.html文件 if (file) { size_t fileSize = file.size(); // 获取文件大小 String fileContent; // 逐字节读取文件内容 while (file.available()) { fileContent += (char)file.read(); } file.close(); // 关闭文件 // 返回HTML内容 request->send(200, "text/html", fileContent); return; } } // 如果文件不存在,返回404错误 request->send(404, "text/plain", "File Not Found"); }); server.on("/setting.html", HTTP_GET, [](AsyncWebServerRequest *request) { // 检查SPIFFS文件系统中是否存在index.html文件 if (SPIFFS.exists("/setting.html")) { fs::File file = SPIFFS.open("/setting.html", "r"); // 打开index.html文件 if (file) { size_t fileSize = file.size(); // 获取文件大小 String fileContent; // 逐字节读取文件内容 while (file.available()) { fileContent += (char)file.read(); } file.close(); // 关闭文件 // 返回HTML内容 request->send(200, "text/html", fileContent); return; } } // 如果文件不存在,返回404错误 request->send(404, "text/plain", "File Not Found"); }); // 启动服务器 server.begin(); };

这部分的代码较长,是所有WebServer的页面路由监听,与页面中按钮的点击操作触发的url对应,这里的url务必检查仔细,如果不能对应就无法监听到页面请求是否触发,硬件也无法做出对应的响应。另外,在/connect下还添加了写入信息到FS文件系统中的功能,只要每次开机执行读取就不需要重复配置网络信息了。

读取FS系统保存的json文件

void loadWiFiConfig()
{
    if (SPIFFS.begin())
    {
        fs::File file = SPIFFS.open(ssidFile, "r");
        if (file)
        {
            DynamicJsonDocument doc(1024);
            DeserializationError error = deserializeJson(doc, file);
            if (!error)
            {
                String ssid = doc["ssid"];
                String pass = doc["pass"];
                String uid = doc["uid"];
                String city = doc["city"];
                String api = doc["api"];
                useruid = uid;
                cityname = city;
                weatherapi = api;
                WiFi.begin(ssid.c_str(), pass.c_str());
                // 尝试连接WiFi,最多等待10秒
                unsigned long startAttemptTime = millis();
                while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 5000)
                {
                    delay(500);
                }
                // 如果连接失败,打印状态
                if (WiFi.status() != WL_CONNECTED)
                {
                    Serial.println("WiFi connection failed, starting captive portal...");
                    handleWiFiConfig(); // 启动强制门户
                }
                else
                {
                    Serial.println("WiFi connected");
                    timeClient.begin();
                }
            }
            file.close();
        }
    }
}

前面我们讲了在/connect路由监听下添加了将信息保存的FS文件系统,那么这里的loadWiFiConfig()方法就是读取FS文件系统的Json文件并将数据同步到全局变量之中,这样就不需要每次开机进入配置页面配网了,程序会自动加载上次配网保存的信息,极为方便。

运动状态

switch (actionstate)
    {
    case 0 /* constant-expression */:
        /* code */
        break;
    case 1:
        front(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 2:
        left(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 3:
        right(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 4:
        back(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 5:
        toplefthand(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 6:
        toprighthand(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 10:
        left90(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 11:
        right90(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 7:
        lie(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 8:
        sitdown(); // 执行一次舵机动作
        actionstate = 0;
        break;
    case 9:
        emojiState = random(0, 7); // 执行一次舵机动作
        actionstate = 0;
        break;
    default:
        break;
    }

运动状态代码与前面的路由监听对应,之所以没有把动作函数直接写入路由监听的代码,这是因为会导致页面响应过久,导致页面无法加载或者触发程序死机然后重启。为了避免这个情况发生,我们通过actionstate变量定义运动状态,然后再loop函数中判断。这里选择的是switch,而并没有使用if-else,理论上对应顺序较长的数据switch性能略好,看个人喜欢,其实都可以用。

前进运动

void front()
{
    //+30C 2/3
    servo2.writeMicroseconds(1500 + speed + engine2offsetleftpwm);
    servo3.writeMicroseconds(1500 - speed - engine3offsetleftpwm);
    delay(500-runtime);
    servo2.writeMicroseconds(1500);
    servo3.writeMicroseconds(1500);
    //-30C 1/4
    servo1.writeMicroseconds(1500 - speed - engine1offsetrightpwm);
    servo4.writeMicroseconds(1500 + speed + engine4offsetrightpwm);
    delay(500-runtime);
    servo1.writeMicroseconds(1500);
    servo4.writeMicroseconds(1500);
    // 0C 2/3
    servo2.writeMicroseconds(1500 - speed - engine2offsetrightpwm);
    servo3.writeMicroseconds(1500 + speed + engine3offsetrightpwm);
    delay(500-runtime);
    servo2.writeMicroseconds(1500);
    servo3.writeMicroseconds(1500);
    // 0C 1/4
    servo1.writeMicroseconds(1500 + speed + engine1offsetleftpwm);
    servo4.writeMicroseconds(1500 - speed - engine4offsetleftpwm);
    delay(500-runtime);
    servo1.writeMicroseconds(1500);
    servo4.writeMicroseconds(1500);
    //+30C 1/4
    servo1.writeMicroseconds(1500 + speed + engine1offsetleftpwm);
    servo4.writeMicroseconds(1500 - speed - engine4offsetleftpwm);
    delay(500-runtime);
    servo1.writeMicroseconds(1500);
    servo4.writeMicroseconds(1500);
    //-30C 2/3
    servo2.writeMicroseconds(1500 - speed - engine2offsetrightpwm);
    servo3.writeMicroseconds(1500 + speed + engine3offsetrightpwm);
    delay(500-runtime);
    servo2.writeMicroseconds(1500);
    servo3.writeMicroseconds(1500);
    // 0C 1/4
    servo1.writeMicroseconds(1500 - speed - engine1offsetrightpwm);
    servo4.writeMicroseconds(1500 + speed + engine4offsetrightpwm);
    delay(500-runtime);
    servo1.writeMicroseconds(1500);
    servo4.writeMicroseconds(1500);
    // 0C 2/3
    servo2.writeMicroseconds(1500 + speed + engine2offsetleftpwm);
    servo3.writeMicroseconds(1500 - speed - engine3offsetleftpwm);
    delay(500-runtime);
    servo2.writeMicroseconds(1500);
    servo3.writeMicroseconds(1500);
}

ADC电量检测

// 对 ADC 数据多次采样并计算平均值
float getAverageAdcVoltage() {
  long totalAdcValue = 0;

  // 多次采样
  for (int i = 0; i < numSamples; i++) {
    totalAdcValue += analogRead(A0); // 读取 ADC 数据
    delay(10); // 每次采样间隔 10ms
  }

  // 计算平均 ADC 值
  float averageAdcValue = totalAdcValue / (float)numSamples;

  // 将 ADC 值转换为电压
  return (averageAdcValue / 1023.0) * 1.0; // ESP8266 的参考电压为 1.0V
}

// 计算电池电量百分比的函数
int mapBatteryPercentage(float voltage) {
  if (voltage <= minVoltage) return 0;   // 小于等于最小电压时,电量为 0%
  if (voltage >= maxVoltage) return 100; // 大于等于最大电压时,电量为 100%

  // 根据线性比例计算电量百分比
  return (int)((voltage - minVoltage) / (maxVoltage - minVoltage) * 100);
}

与小车不同,机器狗不能像小车那样简单控制电机正反转就能实现前进后退,这里需要观察四足动物,进行一些仿生模拟,用舵机模拟四足动物前进时的四足变化情况。

舵机校准

  • 现已支持180度舵机,180度舵机自带限位器,无需校准

因为我们使用的是360度舵机,这种舵机拥有较高的拓展性,但不像180度舵机那样通过可以直接控制旋转角度,所有这里我们要进行舵机校准,确保舵机转速,角度均合适。刷入程序的舵机校准数据并不是通用的,这要根据自己的舵机情况进行调整。
img_52.png

校准步骤

电机底部向左或向右旋转按钮,通过减少和增加电机左右转补偿按钮校准电机
img_51.png

粗略校准

1.将所有脚固定到相同角度。
2.滑到校准页的底部,点击一次‘电机左转90度’。
img_53.png
3.找到转动大于90度或小于90度的脚,进行舵机补偿。

精确校准
在粗略校准完成后请按一下步骤进行精调.
1.将所有脚固定到相同角度。
2.滑到校准页的底部,点击4次‘电机左转90度’。
3.找到转动大于360度或小于360度的脚,进行舵机补偿。

修改程序重新烧录

记录下认为合理的各个电机补偿值,修改程序的补偿定义,重新刷入程序,当然,不重新输入也可以,这个值是立即生效的。但是为了能快速响应,避免重复刷写降低寿命,所以不会保存到FS文件系统,下次重启也不会被保留。

img_50.png

3D外壳结构

3D外壳由嘉立创云CAD平台构建
3D外壳工程链接-嘉立创云CAD

主体

image.png

  • 红框部分是8.4V的2串锂电池充电模块卡槽,连接到电路上的充电接口,可以用胶水固定

  • 蓝框部分是船型10x15mm船型开关卡槽,连接到电路上的开关接口,用于通断供电电路

底壳

image.png

  • 底壳部分为舵机设计了4个限位槽,盖上主体壳后PCB会将舵机压住,所以没有设计螺丝孔,可以正常使用。

image.png

  • 红框部分是螺丝卡槽,可以直接使用舵机附带的两颗大头螺丝任意一颗

组装

组装较为简单,分为上中下三层

PCB

5601d188-aabb-4490-881b-8c36ce104815.jpg
PCB除主控和LDO外均使用插件,非常容易焊接,焊接完成后记得使用剪钳将底部突出引脚剪平,方便后续组装。

上层

583b9b56-74e0-4af2-b2bd-9e38dd8d006d.jpg
最上层是上壳内部的充电模块和总开关,对应PCB上的开关和充电接口。

下层

cd66e5d7-dddb-4a97-a0d5-425c426d6543.jpg
下层为舵机空间,摆放四个舵机,通过限位槽限位。

中间层

bf45e03a-a9d3-4ba7-b441-21cc5a1f7c1a.jpg

中间层是电路板空间,通过上壳内的限位槽将PCB固定在中间,并压住底部电机。

功能拓展

小狗不仅仅局限于现有功能,可以根据现有代码逻辑框架添加更多好玩有趣的功能,当然你也可以完全重构,使用更好性能的主控。在软件上可以添加语音交互,大模型对话等等,在硬件上也可以添加避障,测温,搭载炮台等等,以实现更多好玩有趣的功能而不仅仅局限于此。

注意事项

  • 推荐使用180度舵机,自带限位器,无需校准
  • V1.3外壳适配光固化打印,固定PCB螺丝M2x2,固定外壳螺丝M1.4x3
  • OLED显示屏为0.96寸SSD1306或SSD1315驱动
  • 电池为2节14500锂电池,单节电压3.7-4.2v, 电池盒为2节串联14500电池盒
  • 预留开关和外部充电接口,外部充电模块应当为2串锂电池充电模块,满电电压8.1-8.4V,均衡接口需飞线连接到两节电池中间
  • 切勿强行掰动舵机,避免电机损坏,也可购买金属齿轮的MG90舵机
  • 14500单节电池容量建议达到1000mAh-1200mAh左右,确保放电能力达到1C,最高输出电流最好达到1200mA,以达到所需电流值。

更新说明

PCB
V1.2:新增ADC
V1.3:新增集成开关,新增5V接口,支持外接语音模块
程序&文档
v1.1:添加电池电量检测代码及讲解
v1.2:程序新增支持180度舵机版本,可无需校准
v1.3:适配V1.3版型
25.2.19:在线文档添加烧录教程
25.3.17:添加推荐器件清单
3D
v1.2:适配V1.2版PCB外壳
v1.3:适配V1.3版PCB外壳(支持光固化)
BOM
v1.3:适配V1.3版PCB BOM

FAQ

表情包从哪里来
是否需要网络
如何遥控
无法进入遥控页面
烧录后没反应

 

推荐器件清单

序号 器件名称 参数 立创编号 数量 单价 起购数 总价
1 主控芯片 ESP-12F(ESP8266MOD) C82891 1 14.7 1 14.7
2 电源芯片 AMS1117-5.0 C2718490 1 0.16 5 0.85
3 电源芯片 AMS1117-3.3 C347222 1 0.2597 5 1.3
4 电解电容 10uF C43351 4 0.0819 50 4.1
5 瓷片电容 100nF C84772 1 0.1881 10 1.88
6 直插电阻 10kΩ C2894649 8 0.0183 100 1.83
8 直插电阻 750kΩ C714344 1 0.0188 50 0.94
9 直插电阻 100kΩ C2894647 1 0.0668 100 1.79
10 轻触按键 6x6-TH C42416243 2 0.03145 5 1.57
11 拨动开关 SK12D07VG6 C431548 1 0.135375 20 2.71
12 排针 1*10Pin C41413838 1 0.217 10 2.18
13 OLED屏幕 HS96L03W2C03 C5248080 1 13.14 1 13.14
          15.974475   46.99
1 电池座 14500*2 淘宝 1 0.5 1 0.5
2 SG90舵机 (180度版) 淘宝 4 3.5   14
          30.474475   61.49

备注

这里的清单推荐单件用户采用,前面我们说的成本30是指不含MOQ起购量限制的单件成本价格,主控及屏幕可选淘宝购买价格更低,但兼容性不好说。
1.大部分屏幕模组I2C自带有上拉电阻,所以电路中4.7K电阻可以留空不焊。
2.LDO后的22uF电容由10uF代替,影响不大。
3.10uF瓷片电容可以留空不焊接,影响不大。
4.排针购买1*10P,按需裁切就行。
5.建议使用180度舵机,360度舵机程序不再维护

设计图

Board1
原理图
 
 
PCB
 
 
 
BOM
BOM下载在立创商城下单
ID Name Designator Footprint Quantity Manufacturer Part Manufacturer Supplier Supplier Part
1 10kΩ R1,R2,R3,R4,R7,R11,R12,R13,R14,R15 RES-TH_BD2.7-L6.2-P10.20-D0.4 10 CR1/4W-10K±5%-ST52 VO(翔胜) LCSC C2894649
2 10uF C2,C3,C5,C6 CAP-TH_BD4.0-P1.50-D0.8-FD 4 KS106M025C07RR0VH2FP0 CX(承兴) LCSC C43351
3 10uF C4 CAP-TH_L5.5-W3.8-P5.08-D0.5 1 CD1H106KC9IER2F100 Dersonic(德尔创) LCSC C2761740
4 2.4GHz U1 WIFIM-SMD_ESP-12F-ESP8266MOD 1 ESP-12F(ESP8266MOD) Ai-Thinker(安信可) LCSC C82891
5 TS665CJ SW1,SW2 SW-TH_4P-L6.0-W6.0-P4.50-LS6.5 2 TS665CJ SHOU HAN(首韩) LCSC C393938

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

3D模型

序号文件名称下载次数
暂无数据

附件

序号文件名称下载次数
1
WeChat_20250505201151.mp4
4
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

知识产权声明&复刻说明

本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。

请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。

评论

全部评论(1
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航