DIY四足机器人
发表于更新于
字数总计:4.2k阅读时长:16分钟 四川
四足机器人软硬件架构
- 🤦♀️飞书画的感觉不咋地

四足机器人硬件(所有连接仅供参考,任何问题与本人无关)
电池
- 🐱👓请一定用万用表检测一下正负极是否和pcb接口对应以及电压是否正常,理论上来说2s~4s锂电池都可以接入
【淘宝】http://e.tb.cn/h.gKCOp83eZ0tq1GN?tk=drWN3hhtbwJ HU9196 「电动玩具枪锂电池7.4V充电器四驱越野遥控车充电电池挖掘机软弹枪」
点击链接直接打开 或者 淘宝搜索直接打开
需要购买一套电池
降压模块
【淘宝】限时满20减2 http://e.tb.cn/h.gKaxrAQtpJIKUid?tk=qcLp3hhDj74 MF7997 「MP1584EN 3A 5A可调降压电源模块板稳压航模24V-12V 9V转5VDC-DC」
点击链接直接打开 或者 淘宝搜索直接打开
需要购买1块降压模块
NodeMcu 8266开发板
需要购买1块开发板,esp8266 的硬件资源介绍及使用教程请自行学习
SG90舵机
需要购买8个舵机
0.96 OLED
需要购买1块屏
结构件
- 💕3d建模当年也是我的最爱额。
Solidworks 建模装配体图 组装后和效果和模型差不多,不过组装后我只使用了一块pcb。

3D打印使用的是导出stl格式文件,只需要打印四个关节和四个手部,底盘可以直接使用pcb,然后螺丝可以使用sg90舵机自带的。可以购买m2x10的螺柱螺母或者热熔胶来固定pcb和舵机,推荐使用热熔胶,因为热熔胶还可以用来加强oled以及舵机焊接口的连线
PCB
- 🦄这个简单,花一个下午画的,能不能用不重要,干就完了
嘉立创eda简单设计了个底板主要是提供结构支撑,和连接降压,主控,电池,开关这几个模块。

原理图就是吧购买的模块连接起来

嘉立创工程文件和制造输出文件以及原理图,原理图全是模块设计,焊接简单。电池采用自带usb充电线充电。
四足机器人软件
- 🧬东拼一点西凑一点,也是不负众望,成功堆出屎山代码,毕竟大家都说代码和人有一个能跑就行!
上来先放源代码 在123网盘里可以直连下载YYDS,还有github的链接欢迎大家浏览我的github呀💖
所有资料连接
开发平台
- Vscode + PlatformIO 基于arduino
vscode安装自行学习,在插件市场搜索PlatformIO 安装插件。可能需要开魔法才可以在建立工程时快速下载框架支持包。否则有可能需要等待数小时。


打开提供的源代码。arduino 的使用方法请自行学习,资源很多。

1 2 3 4 5 6 7 8
| [env:esp12e] platform = espressif8266 board = nodemcuv2 framework = arduino board_build.filesystem = littlefs lib_deps = olikraus/U8g2@^2.35.19 ottowinter/ESPAsyncWebServer-esphome@^3.2.2
|
可以直接修改配置文件来加载库更改开发平台以及添加文件系统等,可以在platformio官网查看详细说明
wifi网络服务器代码
使用的ap模式,通俗来说就是8266发射wifi信号手机来连接,手机浏览器访问8266的ip后,8266发送相应的资源到手机浏览器展示,当然8266也可以接收浏览器发送的信息。
wifi连接的代码如下,wifi名字和密码可自行更改。
1 2 3 4 5 6 7
| const char *wifiname = "Melody"; const char *wifipassword = "12345678"; #elif ApMod WiFi.mode(WIFI_AP); WiFi.softAP(wifiname, wifipassword); IP = WiFi.softAPIP().toString(); #endif
|
异步web服务器,可以在后台异步响应请求,从而不阻塞其它程序运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static AsyncWebServer esp8266_server(80); static void webserveInit(void) { if (LittleFS.begin()) { DebugPrt("LittleFS Started\n"); } else { DebugPrt("LittleFS Failed to Start.\n"); }
esp8266_server.on("/choosNum", HTTP_GET, handlSetNum); esp8266_server.onNotFound(handleUserRequest); esp8266_server.begin(); DebugPrt("HTTP server started"); }
|
其中LittleFS文件系统用于存储前端页面的htlm,css,javascript,还有图片等静态资源,存储在flash尾部空间中,这样可以大大减小ram的使用空间。 esp8266_server.on用于绑定接收数据处理函数,/choosNum 时请求地址,当客户端使用get方式请求这个地址时就会触发回调调用handlSetNum() 函数来处理。同理esp8266_server.onNotFound就时其余未知的地址都在这里回调执行通过handleUserRequest函数来处理。
首先来看看handlSetNum函数的内容
1 2 3 4 5 6
| static void handlSetNum(AsyncWebServerRequest *request) { String res = request->getParam("num")->value(); num = res.toInt(); resflag = 1; }
|
很简单,就是将接收到的数据中num的值获取到,然后将标志位置1表示接收到了数据。
然后handleUserRequest函数的内容就相对长一点了。
通过客户端请求的数据地址来找到数据,并发送给客户端,如果文件不存在就发送给客户端404 Not Found 文本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static void handleUserRequest(AsyncWebServerRequest *request) { String reqResource = request->url();
bool fileReadOK = handleFileRead(reqResource, request);
if (!fileReadOK) { request->send(404, "text/plain", "404 Not Found"); } }
|
获取文件类型函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| static String getContentType(String filename) { if (filename.endsWith(".htm")) return "text/html"; else if (filename.endsWith(".html")) return "text/html"; else if (filename.endsWith(".css")) return "text/css"; else if (filename.endsWith(".js")) return "application/javascript"; else if (filename.endsWith(".png")) return "image/png"; else if (filename.endsWith(".gif")) return "image/gif"; else if (filename.endsWith(".jpg")) return "image/jpeg"; else if (filename.endsWith(".ico")) return "image/x-icon"; else if (filename.endsWith(".xml")) return "text/xml"; else if (filename.endsWith(".pdf")) return "application/x-pdf"; else if (filename.endsWith(".zip")) return "application/x-zip"; else if (filename.endsWith(".gz")) return "application/x-gzip"; return "text/plain"; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static bool handleFileRead(String resource, AsyncWebServerRequest *request) {
if (resource.endsWith("/")) { resource = "/index.html"; }
String contentType = getContentType(resource);
if (LittleFS.exists(resource)) { LittleFS.open(resource, "r"); request->send(LittleFS, resource, contentType); return true; } return false; }
|
通过getContentType函数获取文件发送类型,就是通过请求文件后缀来得出返回数据的类型。LittleFS.exists(resource) 如果文件系统中存在这个文件,就将文件读出来发送给客户端。contentType就是解析出来的返回数据类型。
静态网页代码
在data文件夹下存放的就是需要编译后存入flash通过LittleFS来管理的文件,htlm,css,javascript这些文件。
.png)
文件需要编译后下载,步骤如下。
.png)
一个简单的界面,一个图片和12个按钮组成,关于前端的知识可以在菜鸟教程去学习。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>手机端控制页面</title> <link rel="stylesheet" href="styles.css"> </head> <body> <header> <img src="header-image.png" alt="头部图片"> </header> <main class="content-container"> <div class="grid-container"> <button class="grid-item" id="button1" onclick="sendData(1)">站立</button> <button class="grid-item" id="button2" onclick="sendData(2)">前进</button> <button class="grid-item" id="button3" onclick="sendData(3)">来呀</button> <button class="grid-item" id="button4" onclick="sendData(4)">左转</button> <button class="grid-item" id="button5" onclick="sendData(5)">招手</button> <button class="grid-item" id="button6" onclick="sendData(6)">右转</button> <button class="grid-item" id="button7" onclick="sendData(7)">摇摆</button> <button class="grid-item" id="button8" onclick="sendData(8)">炒菜</button> <button class="grid-item" id="button9" onclick="sendData(9)">游泳</button> <button class="grid-item" id="button10" onclick="sendData(10)">俯撑</button> <button class="grid-item" id="button11" onclick="sendData(11)">动一</button> <button class="grid-item" id="button12" onclick="sendData(12)">动二</button> </div> </main> <script src="scripts.js"></script> </body> </html>
|
发送脚本 :在按钮按下时调用,将按钮的传入值发送给服务器,值绑定到了num,使用Get方法发送。同样菜鸟教程也可以学习
1 2 3 4 5 6 7 8 9 10
| function sendData(num) { var xmlhttp; if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); } else { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.open("GET", "choosNum?num="+num, true); xmlhttp.send(); }
|
最终在网页上展示的控制界面如下图

可调速舵机代码
SG90舵机时通过周期20ms占空比0.5~2.5ms的pwm波来控制的,0度对应0.5ms ,180度对应2.5ms。
调速的主要思想就是,将一段距离需要在设定的时间内完成的位移,拆分成若干个周期来运行,计算出,总共需要多少个周期,然后将角度均分,每个周期写入一个增量的占空比,逐渐累加占空比即可达到调速的目的。
简单的可以控制单个舵机调速的程序,同时控制多个舵机的设计思想也一样,就是需要在写法上做出多舵机操作接口。向上取整增量是为了防止增量为0时在循环中卡死。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
void SetangelStep(RootPin PIN, int16_t AG, uint16_t ms) {
int16_t count = ms / 20;
int16_t lastag = Getangelus(PIN); int16_t lastagbk = lastag;
AG = revvaule(AG, 2500, 500, 180, 0);
int16_t dertag = 0;
if (AG > lastag) { dertag = AG - lastag; } else { dertag = lastag - AG; }
int16_t inc = (dertag - 1) / count + 1;
while (1) {
if (AG > lastagbk) { lastag += inc; if (lastag >= AG) return; } else { lastag -= inc; if (lastag <= AG) return; }
Setangelus(PIN, lastag); delay(20); } }
|
完整的舵机调速程序如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| typedef struct {
uint16_t us; uint16_t dertms; uint8_t en; RootPin BandPin;
uint16_t usbak; uint16_t nowus;
} myservo_t;
typedef void (*setfun)(RootPin, uint16_t);
void SetangelStepSe(myservo_t *sr, uint8_t nums, setfun fun) { for (uint16_t i = 0; i < nums; i++) {
sr[i].us = revvaule(sr[i].us, 2500, 500, 180, 0); sr[i].nowus = sr[i].usbak = Getangelus(sr[i].BandPin);
if (sr[i].us > sr[i].usbak) { sr[i].dertms = (sr[i].us - sr[i].usbak - 1) / (sr[i].dertms / 20 ) + 1; } else { sr[i].dertms = (sr[i].usbak - sr[i].us - 1) / (sr[i].dertms / 20) + 1; } }
while (1) { for (uint8_t i = 0; i < nums; i++) { if (sr[i].us > sr[i].usbak) { sr[i].nowus += sr[i].dertms; if (sr[i].nowus >= sr[i].us) { sr[i].en = 0; } } else {
sr[i].nowus -= sr[i].dertms; if (sr[i].nowus <= sr[i].us) { sr[i].en = 0; } } }
uint8_t isret = 1; for (uint8_t i = 0; i < nums; i++) { if (sr[i].en == 1) { fun(sr[i].BandPin, sr[i].nowus); isret = 0; } } if (isret == 1) { return; }
delay(20); } }
|
- 其实如果自己需要自定义动作的话 只需要知道怎么使用这个可以调速的关节设定函数就可以了,我在注释中已经说明了然后,动则这些就需要你自己发挥想象空间来创造咯,相信你们的想象力一定更好了🙆♀️
1 2 3 4 5 6 7 8 9 10 11 12 13
|
void SetAllSePinStep(uint16_t ag, uint16_t ms, uint8_t en1, uint8_t en2, uint8_t en3, uint8_t en4); void SetAllSiPinStep(uint16_t ag, uint16_t ms); void SetAllSiPinStep(uint16_t ag, uint16_t ms, uint8_t en1, uint8_t en2, uint8_t en3, uint8_t en4);
|
其他模块的代码都比较简单了,请自行学习查看。
值得一提的是,为了资源的合理利用,我在主循环中加入了最原始分时调度的代码,这也就是一些实时操作系统,最最基本的思想了吧。代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void loop() {
if (millis() - LedpreviousMillis >= ledtime) { LedpreviousMillis = millis();
{ ledRun(); } } else if (millis() - WebpreviousMillis >= webtime) { WebpreviousMillis = millis();
{ ALoop(); } }
}
|
两个任务在设定的间隔周期触发一次。不会一直轮询等待,可以减轻mcu的负担。其中millis()函数是表示当前系统运行的ms数。
实物演示
让我等下再做了🐱🚀🐱👓🐱🏍🐱👤





总结
价格
物品 |
电池套件 |
降压模块 |
8266开发板 |
sg90舵机x8 |
0.96 oled |
3D打印 |
总价 |
单价 |
15.2 |
4.55 |
9.65 |
4x8 = 32 |
6.1 |
18 |
85.5 |
学习到的东西
- c语言更加熟练的使用–其中包含了结构体,函数重载,typedef,函数指针,数组指针,static,等c语言知识。
- arduino平台–熟悉arduino平台架构,加强对库文件的使用和理解。
- pcb制版–(自行学习)通过这个小底板的绘制可以加强对pcb设计的流程掌握,还可以学习在一些小技巧等。
- 前端–最基础的前端知识学习。