
机械狗1
简介
该机械狗由四个舵机、一个电池盒及电池、oled屏幕等组成,并可简单的对真实小狗进行仿真例如前进后退,搭配了led显示屏可显示各种表情,打开开关可自动复原腿部位置,可通过软件遥控
简介:该机械狗由四个舵机、一个电池盒及电池、oled屏幕等组成,并可简单的对真实小狗进行仿真例如前进后退,搭配了led显示屏可显示各种表情,打开开关可自动复原腿部位置,可通过软件遥控开源协议
:Public Domain
描述
项目功能
- ✅手机遥控
- ✅表情显示
- ✅每日天气
- ✅时钟显示
项目参数
- 本设计采用ESP8266主控,内置WIFI功能,通过AP模式遥控
- 本设计采用0.96寸OLED显示屏,支持SSD1315/SSD1306驱动,可显示表情、时钟、天气等相关信息
- 选用AMS1117 LDO线性稳压器,负责将8.4V和5V电压分别转换成5V和3.3V,为舵机及主控提供电源
- 项目支持SG-90/MG-90 180度及360度版本,推荐使用180度版本,自带限位器,无需校准电机。
原理解析
篇幅有限,这里仅讲解部分关键电路和程序,详细说明请查看开发文档
本项目提供了完整的说明及源码,并不局限于现有功能,您可以基于现有框架进行二
次开发,丰富小狗的功能。
硬件设计
本项目电路由以下部分组成,电源部分、ESP8266主控、外部接口
电源电路:

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

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

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



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

修改分压器适配 8.4V 到 1V
现在需要适配新的输入电压范围(最大 8.4V)到 ESP8266 的 1.0V ADC 输入。分压比计算如下:
分压比=1V8.4V=18.4≈0.119分压比=8.4V1V=8.41≈0.119
根据分压公式:
R2R1+R2=0.119R1+R2R2=0.119
假设保持100k ,计算 :
100kR1+100k=0.119R1+100k100k=0.119
R1+100k=100k0.119≈840kR1+100k=0.119100k≈840k
R1≈740kΩR1≈740kΩ
对于 ,输出电压:
Vout=8.4×100k740k+100k=8.4×100840≈1.0VVout=8.4×740k+100k100k=8.4×840100≈1.0V
对于电压较低时(如 4.2V),输出电压为:
Vout=4.2×100k740k+100k=4.2×100840≈0.5VVout=4.2×740k+100k100k=4.2×840100≈0.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度舵机那样通过可以直接控制旋转角度,所有这里我们要进行舵机校准,确保舵机转速,角度均合适。刷入程序的舵机校准数据并不是通用的,这要根据自己的舵机情况进行调整。
校准步骤
电机底部向左或向右旋转按钮,通过减少和增加电机左右转补偿按钮校准电机
粗略校准
1.将所有脚固定到相同角度。
2.滑到校准页的底部,点击一次‘电机左转90度’。
3.找到转动大于90度或小于90度的脚,进行舵机补偿。
精确校准
在粗略校准完成后请按一下步骤进行精调.
1.将所有脚固定到相同角度。
2.滑到校准页的底部,点击4次‘电机左转90度’。
3.找到转动大于360度或小于360度的脚,进行舵机补偿。
修改程序重新烧录
记录下认为合理的各个电机补偿值,修改程序的补偿定义,重新刷入程序,当然,不重新输入也可以,这个值是立即生效的。但是为了能快速响应,避免重复刷写降低寿命,所以不会保存到FS文件系统,下次重启也不会被保留。

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

-
红框部分是8.4V的2串锂电池充电模块卡槽,连接到电路上的充电接口,可以用胶水固定
-
蓝框部分是船型10x15mm船型开关卡槽,连接到电路上的开关接口,用于通断供电电路
底壳

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

- 红框部分是螺丝卡槽,可以直接使用舵机附带的两颗大头螺丝任意一颗
组装
组装较为简单,分为上中下三层
PCB

PCB除主控和LDO外均使用插件,非常容易焊接,焊接完成后记得使用剪钳将底部突出引脚剪平,方便后续组装。
上层

最上层是上壳内部的充电模块和总开关,对应PCB上的开关和充电接口。
下层

下层为舵机空间,摆放四个舵机,通过限位槽限位。
中间层

中间层是电路板空间,通过上壳内的限位槽将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度舵机程序不再维护
设计图
| 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
克隆工程工程成员
知识产权声明&复刻说明
本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。
请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。


评论