DIY四足机器人

四足机器人软硬件架构

  • 🤦‍♀️飞书画的感觉不咋地

四足机器人硬件(所有连接仅供参考,任何问题与本人无关)

电池

  • 🐱‍👓请一定用万用表检测一下正负极是否和pcb接口对应以及电压是否正常,理论上来说2s~4s锂电池都可以接入

【淘宝】http://e.tb.cn/h.gKCOp83eZ0tq1GN?tk=drWN3hhtbwJ HU9196 「电动玩具枪锂电池7.4V充电器四驱越野遥控车充电电池挖掘机软弹枪」
点击链接直接打开 或者 淘宝搜索直接打开

需要购买一套电池

降压模块

  • 👀保证至少可以过3A电流。

【淘宝】限时满20减2 http://e.tb.cn/h.gKaxrAQtpJIKUid?tk=qcLp3hhDj74 MF7997 「MP1584EN 3A 5A可调降压电源模块板稳压航模24V-12V 9V转5VDC-DC」
点击链接直接打开 或者 淘宝搜索直接打开

需要购买1块降压模块

NodeMcu 8266开发板

  • 🤷‍♀️这个神板因该人手一个,不过现在乐鑫已经推出esp32c3了,新神板来了
    【淘宝】限时官方立减0.73元 http://e.tb.cn/h.gKQFJ88xUs7VLfy?tk=jwn13hGPIay HU0854 「ESP8266串口WIFI模块 CP2102/CH340 NodeMCU Lua V3物联网开发板」
    点击链接直接打开 或者 淘宝搜索直接打开

需要购买1块开发板,esp8266 的硬件资源介绍及使用教程请自行学习

SG90舵机

  • 🐱‍🐉那家便宜买那家,基本都大差不差呀。
    【淘宝】限时满88减2 http://e.tb.cn/h.gq7QGmTuJ59kCKV?tk=Wd5P3hGJuMC HU9196 「伺服舵机 SG90 MG90S 9g舵机 450直升飞机小车伺服器 180度 360度」
    点击链接直接打开 或者 淘宝搜索直接打开

需要购买8个舵机

0.96 OLED

  • 🐱这个也是人均有吧。
    【淘宝】限时满88减2 http://e.tb.cn/h.gK4KNkc34hgFGe9?tk=9eZ13hGGVYG MF7997 「0.91/0.96/1.3寸白/黄/蓝色 双色 IIC通信小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); // 建立网络服务器对象,该对象用于响应HTTP请求。监听端口(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); //绑定函数 /choosNum 在网页get请求的资源地址
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(); // 参考静态网页javascrpt xmlhttp.open("GET", "choosNum?num="+num, true); get带参数请求
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)
{
// 获取用户请求资源地址(Request Resource)
String reqResource = request->url();

// 通过handleFileRead函数处处理用户请求资源
bool fileReadOK = handleFileRead(reqResource, request);

// 如果在SPIFFS无法找到用户访问的资源,则回复404 (Not Found)
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)
{ // 处理浏览器HTTP访问

if (resource.endsWith("/"))
{ // 如果访问地址以"/"为结尾
resource = "/index.html"; // 则将访问地址修改为/index.html便于SPIFFS访问
}

String contentType = getContentType(resource); // 获取文件类型

if (LittleFS.exists(resource))
{ // 如果访问的文件可以在SPIFFS中找到
LittleFS.open(resource, "r"); // 则尝试打开该文件
request->send(LittleFS, resource, contentType); // 并且将该文件返回给浏览器
return true; // 返回true
}
return false; // 如果文件未找到,则返回false
}

通过getContentType函数获取文件发送类型,就是通过请求文件后缀来得出返回数据的类型。LittleFS.exists(resource) 如果文件系统中存在这个文件,就将文件读出来发送给客户端。contentType就是解析出来的返回数据类型。

静态网页代码

在data文件夹下存放的就是需要编译后存入flash通过LittleFS来管理的文件,htlm,css,javascript这些文件。

文件需要编译后下载,步骤如下。

一个简单的界面,一个图片和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
/**
* @brief 设置单个关节 可以调速
* @param PIN 那个关节
* @param AG 要到达的角度
* @param ms 过程时间
*/
void SetangelStep(RootPin PIN, int16_t AG, uint16_t ms)
{

int16_t count = ms / 20; // 20ms(pwm周期) 一个间隔

int16_t lastag = Getangelus(PIN); // 获取上一次的占空比时间us
int16_t lastagbk = lastag; // 备份时间

AG = revvaule(AG, 2500, 500, 180, 0); // 量程转换 目标角度转换成目标占空比时间us

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; // 目标角度 -在运算时时转换后成us
uint16_t dertms; // 设定速度(ms) - 在运算时为增量
uint8_t en; // 是否使能
RootPin BandPin; // 绑定的关节
/*运算过程变量*/
uint16_t usbak; // 最初的us备份
uint16_t nowus; // 当前的us

} myservo_t;

typedef void (*setfun)(RootPin, uint16_t); // 函数指针取别名 自己的舵机角度设定函数

/**
* @brief 通过定义结构体来实现舵机的调速控制,支持自定义舵机数量,支持不同速度
* @param myservo_t *sr 舵机控制结构体,设定目标角度、到达目标的过程时间,是否使能,以及绑定引脚
* @param uint8_t nums 舵机数组的数量(长度)
* @param setfun fun 舵机的执行函数
*/
void SetangelStepSe(myservo_t *sr, uint8_t nums, setfun fun)
{
for (uint16_t i = 0; i < nums; i++)
{

// 计算目标us数
sr[i].us = revvaule(sr[i].us, 2500, 500, 180, 0);
// 获取当前us数并备份
sr[i].nowus = sr[i].usbak = Getangelus(sr[i].BandPin);

// 计算出增量 注意要向上取整,增量为0,在误差判断里会无限循环。
if (sr[i].us > sr[i].usbak) // 如果目标us大于当前us
{
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) // 还没到达目标us
{
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
/**
* @brief设置 肩部关节 转动 可以调速。
* @param ag 目标角度
* @param ms 转动的时间 也就是平均速度啦
* @param en1 是否使能第一脚
* @param en2 是否使能第一脚
* @param en3 是否使能第一脚
* @param en4 是否使能第一脚
*/
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();//翻转led
}
}
else if (millis() - WebpreviousMillis >= webtime)
{
WebpreviousMillis = millis();

// 运行内容
{
ALoop();//调度动作执行
}
}

}

两个任务在设定的间隔周期触发一次。不会一直轮询等待,可以减轻mcu的负担。其中millis()函数是表示当前系统运行的ms数。

实物演示

让我等下再做了🐱‍🚀🐱‍👓🐱‍🏍🐱‍👤

  • **ok呀ok来做了e-拍了个小视频,哈哈,直接看视频吧🐱‍👤!不对我还拍了图片,那先看高清美图吧!

  • 我给它来了个360无死角展示首先来展示它的正面

  • 然后是侧面

  • 然后看看屁股吧

  • 那小肚子也给你们看吧

  • 直接解刨给你看

  • 展示完毕上vlog

总结

价格

物品 电池套件 降压模块 8266开发板 sg90舵机x8 0.96 oled 3D打印 总价
单价 15.2 4.55 9.65 4x8 = 32 6.1 18 85.5

学习到的东西

  1. c语言更加熟练的使用–其中包含了结构体,函数重载,typedef,函数指针,数组指针,static,等c语言知识。
  2. arduino平台–熟悉arduino平台架构,加强对库文件的使用和理解。
  3. pcb制版–(自行学习)通过这个小底板的绘制可以加强对pcb设计的流程掌握,还可以学习在一些小技巧等。
  4. 前端–最基础的前端知识学习。