作者:B站 UP 李不胖谁胖 · QQ 交流群:757384775

概述

简介:

相信童年看过《机器人瓦力》电影的小伙伴一定都想拥有一台属于自己的瓦力,本教程就DIY瓦力所需的物料和控制程序开始,带着大家一起DIY一个瓦力机器人。我想没有男孩子能抵抗的住这集成声、光、电、动的小机器人玩具吧,寒假带着娃做一个,把隔壁小孩馋哭 哈哈。

成品实拍

整体架构

动手做之前先理解这一节。80% 的故障可以靠这张图在 10 秒内定位:哪一段通信断了,就检查哪一段。

三段通信架构

瓦力的控制信号从手机/网页发出后,要经过三段通信才能到达执行模块:

1
2
3
4
5
6
┌──────────────────┐ 一段通信  ┌──────────────────┐ 二段通信  ┌──────────────────┐ 三段通信  ┌────────────────────┐
│ 控制端 │ MQTT / │ ESP32 C3 │ Serial1 │ Arduino UNO │ GPIO/I²C │ 执行模块 │
│ H5 / Blinker / │ Blinker / │ 网络通信桥 │ (UART) │ 主控解析指令 │ /SPI │ 电机/舵机/MP3 / │
│ 手柄 / 语音 │ 蓝牙 │ │ │ │ │ NFC / 激光 │
│ │ ─────────>│ │ ─────────>│ │ ─────────>│ │
└──────────────────┘ └──────────────────┘ └──────────────────┘ └────────────────────┘

排查问题时按 一段 → 二段 → 三段 顺序逐级定位(详见文末”问题 Q&A”章节)。

控制链路原理图

整体连接图

电源拓扑

瓦力使用 12V → 5V 两级电源,三组电源域要严格区分共地、不共电

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[3 × 18650 串联 ≈ 11.1V ~ 12.6V]

├─→ 船型开关 ─→ L298N 12V 输入 ─→ 履带电机 ×2

└─→ 船型开关 ─→ DC-DC 降压模块 ─→ 5V ─┬─→ Arduino UNO (VIN/5V)

├─→ ESP32 C3 (5V)

├─→ PCA9685 VCC(逻辑电源)

└─→ PCA9685 V+(舵机电源,独立引线)
└─→ 7 个 SG90 舵机

[所有模块的 GND 必须全部共地]
电源域 电压 供给对象 备注
主电池 ~12V L298N、DC-DC 降压模块 船型开关串接在 + 极
主 5V 5V UNO、ESP32、PCA9685 VCC DC-DC 降压输出
舵机 5V 5V PCA9685 V+ 独立引线,电流峰值可达 3A
信号 GND 0V 所有模块 必须全部共地

⚠️ 电源接错是最常见的炸板原因

  • 不要把 12V 直接接到 UNO 5V 或 ESP32 5V 引脚(会烧主控
  • PCA9685 V+ 不要从 UNO 5V 借电(舵机带不动、UNO 反复重启
  • 必须共地,否则串口通信会乱码或完全不通

完整接线总览

模块详细接线见各章节,下表是一站式查询:

模块 引脚 接到 引脚 用途
ESP32 C3UNO IO00 (RX) UNO D1 (TX) UART 透传
IO01 (TX) UNO D0 (RX) UART 透传
GND UNO GND 共地
UNOL298N D6 L298N IN1 左电机
D7 L298N IN2 左电机
D8 L298N IN3 右电机
D9 L298N IN4 右电机
GND L298N GND 共地
UNOPCA9685 A4 PCA9685 SDA I²C 数据
A5 PCA9685 SCL I²C 时钟
5V PCA9685 VCC 逻辑供电
GND PCA9685 GND 共地
UNORC522 NFC(选配) D10 RC522 SDA SPI 片选
D11 RC522 MOSI SPI
D12 RC522 MISO SPI
D13 RC522 SCK SPI
3.3V RC522 3.3V RC522 是 3.3V 模块
UNODFPlayer MP3(选配) D2 DFPlayer RX 软串口
D3 DFPlayer TX 软串口
5V DFPlayer VCC 电源
ESP32激光头(选配) IO10 激光头 + ESP32 直接控制
GND 激光头 - 共地
ESP32ASR PRO 语音(选配) IO06 ASR PRO PB5 软串口
IO07 ASR PRO PB6 软串口
5V ASR PRO 5V 电源
ESP32DHT11 温湿度(选配) IO04 DHT11 OUT 数据
3.3V DHT11 + 电源

⚠️ ESP32 C3 不同板型 IO 编号不同,详见 一、1.2.1 节”ESP32 C3 板型引脚对照表”

控制指令字符表

控制端发出的指令通过 ESP32 透传到 UNO 后,UNO 按以下字符表执行动作:

字符 动作 字符 动作
w 前进 s 后退
a 左转 d 右转
q 停止 k / i 抬 / 低脖子
j / l 左 / 右转头 r / f 抬 / 低头
t / g 左手上 / 下 y / h 右手上 / 下
1 播放音乐 2 动作编组 1
3 复位

物料清单(BOM)

序号 模块 功能 数量 参考价格(单价) 参考价格(总价) 备注
1 Arduino UNO R3开发板 主控芯片 1 16.4 16.4
2 ESP32 C3开发板 MQTT协议连接服务器 1 9.9 9.9
3 L298N 双H桥电机驱动板 驱动履带电机 1 6.4 6.4
4 PCA9685 16路舵机驱动板 舵机驱动 1 12.5 12.5
5 随身WIFI 提供WIFI 1 15 15
6 DC-DC 12V 转5V 直流降压模块 12V 转 5V供电 1 5.9 5.9
7 12V 370偏轴减速电机 107转/分钟 驱动履带 2 15 30 注意要买偏轴的,不是中置的轴
8 18650电池盒(3节) 电池仓 1 1.2 1.2
9 18650电池 串联提供12V电源 3 8 24
10 18650充电器 1 10 10
11 SG90 9g舵机 驱动运动关节 7 3.79 26.53
12 DC电源插头5.5mm(公头) 电池仓电源输出接头 2 0.3 0.6
13 PETG-ECO 黑色 打印履带、电机座 1 35 35 实际总用量大概1.2kg左右吧,毛估没有具体算过
14 PETG-ECO 灰色 打印瓦力头部 1 35 35
15 PETG-ECO 卡特黄 打印瓦力身体 1 35 35
16 ASR PRO语音模块(带喇叭) 语音识别 1 28.9 28.9 语音控制(可选)
17 1.3寸TFT彩屏 天气时钟屏幕 1 12.9 12.9 天气时钟使用(可选)
18 ESP8266开发板 天气时钟控制芯片 1 13.3 13.3 天气时钟使用(可选)
19 5V 激光模块 眼睛 1 0.65 0.65 可选
20 WIFI摄像头 网络传输视频 1 20 20 可选
21 Mini Mp3 Player 播放音乐 1 3.8 3.8 可选
22 MFRC-522 RFID模块 NFC无线识别 1 3.9 3.9 可选
23 DHT11温度传感器 测量温度 1 2.5 2.5 可选
24 船型开关 电源开关 1 0.5 0.5 可选
25 3W 8R 喇叭 播放音乐 1 4.2 4.2 可选
26 1u2g云服务器 MQTT服务器、部署H5控制页面 1 69 69 可选
27 丙烯颜料(12种盒装) 瓦力上色 1 1.8 1.8 可选
合计 424.88

如果只想完成基础功能,实现履带底盘的行进和关节运动的话硬件部分成本大概在160元,扩展的模块可根据自己需要选择性购买即可。

以下是DIY必备的一些工具和常见的小物品就不算在内了

序号 工具&物品名称
1 热熔胶枪
2 万用表
3 电烙铁
4 老虎钳
5 TF卡、读卡器
6 回形针
7 M3螺柱 螺母
8 502胶水
9 杜邦线(公对母、母对母)

使用或涉及的技术栈

  1. Arduino IDE 开发环境

  2. Element Plus + Vue3 + Vite + TypeScript 自建H5控制页面

  3. Linux + Nginx + Docker + EMQX 后台服务器

烧录前配置私密凭据

联网固件不再在源码中保存家庭 WiFi 或 Blinker 设备密钥。进入所选固件目录后执行:

1
cp secrets.example.h secrets.h

然后只在本机编辑 secrets.h。该文件已被 .gitignore 忽略,不要上传到网盘、Git 仓库或聊天群。公网 MQTT 的服务器配置继续保留在固件中,但 topic 必须改成自己的唯一值;Topic 中不要包含手机号、密码、地址等隐私信息。

如果旧版本中的 WiFi、Blinker 或 SSH 凭据曾公开过,仅从文件中删除并不能使旧凭据失效,还需要在路由器、Blinker 控制台或服务器上主动更换/撤销。

一、履带电机控制

1.1 履带电机使用的是12V偏轴107转/分钟的减速电机,电机驱动使用的是双H桥L298N驱动板

L298N资料:

IN1 IN2 ENA(A) 电机A状态
0或1 0或1 0 停止
1 0 1 顺时针
0 1 1 逆时针
0 0 1 制动
1 1 1 制动
IN3 IN4 ENA(B) 电机B状态
0或1 0或1 0 停止
1 0 1 顺时针
0 1 1 逆时针
0 0 1 制动
1 1 1 制动

这里我们不使用PWM调速,ENA(A) ENA(B)默认短接就是最大转速。

控制方式:两个履带电机正转为前进,都反转为后退,一正转一反转为转向。

IN1 IN2 IN3 IN4 行进状态
1 1 1 1 停止
0 0 0 0 停止
1 0 1 0 前进
0 1 0 1 后退
1 0 0 1 右转
0 1 1 0 左转

1.2 电路连接:

DC12V降压模块的12V 连接 L298N 的12V输入来给L298N供电。

1
2
3
4
5
6
7
8
9
10
L298N模块                           Arduino UNO开发板

IN1 -------------------------- 6

IN2 -------------------------- 7

IN3 -------------------------- 8

IN4 -------------------------- 9

1
2
3
4
5
6
7
8
9
10
ESP32 C3开发板                      Arduino UNO开发板

GND -------------------------- GND

5V -------------------------- 5V

IO00 -------------------------- TX

IO01 -------------------------- RX

1.2.1 ESP32 C3 板型引脚对照表

⚠️ 市面上”ESP32 C3”有多种板型,UART 引脚定义不同,烧代码前请按自己的板子修改,否则 ESP32 ↔ UNO 二段通信会完全不通!

板型 Serial1 RX / TX 板载 LED Boot 键 USB-串口芯片 备注
合宙 ESP32 C3 经典版 IO00 / IO01 IO12, IO13 IO09 CH343 本教程默认
合宙 ESP32 C3 简约版 IO20 / IO21 IO12, IO13 IO09 CH343 -
ESP32 C3 SuperMini IO20 / IO21 IO08 BOOT 按键 CH340/CH9102 体积最小
立创 ESP32 C3 / 第三方板 IO20 / IO21 IO13 BOOT 按键 CP2102 -

代码中的 Serial1 初始化要按板型修改:

1
2
3
4
5
// 合宙经典版(本教程默认)
Serial1.begin(115200, SERIAL_8N1, 0, 1); // RX=IO00, TX=IO01

// 合宙简约版 / SuperMini / 立创等
Serial1.begin(115200, SERIAL_8N1, 20, 21); // RX=IO20, TX=IO21

不知道自己板子是什么型号?

  • 看板子背面丝印 / 包装上的型号
  • 看插上电脑后设备管理器识别出的串口芯片(CH343 → 合宙;CH340/CP2102 → 第三方)
  • 实在不确定,先用万用表通断测一下 UART TX 焊盘是哪个 IO

软串口方案(不推荐,会丢字符且性能差,仅作备选):

1
2
#include <SoftwareSerial.h>
SoftwareSerial uart1(6, 7); // RX=IO06, TX=IO07

1.3 Arduino UNO代码

百度网盘链接:https://pan.baidu.com/s/1HNKWE4Z2C2nG0kT3WxBQWA?pwd=zm6l

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* @author B站 李不胖谁胖
* @version 1.0.0
* @date 2024-01-14
*/
// 前进
void handleForward(){
digitalWrite(6, HIGH);
digitalWrite(7, LOW);
digitalWrite(8, HIGH);
digitalWrite(9, LOW);
}

// 后退
void handleFallback(){
digitalWrite(6, LOW);
digitalWrite(7, HIGH);
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
}

// 左转
void handleTurnLeft(){
digitalWrite(6, HIGH);
digitalWrite(7, LOW);
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
}

// 右转
void handleTurnRight(){
digitalWrite(6, LOW);
digitalWrite(7, HIGH);
digitalWrite(8, HIGH);
digitalWrite(9, LOW);
}

// 停止
void handleStop(){
digitalWrite(6, HIGH);
digitalWrite(7, HIGH);
digitalWrite(8, HIGH);
digitalWrite(9, HIGH);
}

// 处理动作
void handleMotorAction(char commend){
if(commend == '\n' || commend == '\r'){
return;
}
Serial.print("commend:");
Serial.print(commend);
Serial.println();

switch (commend){
case 'w':
// 如果指令为前进
handleForward();
break;
case 's':
// 如果指令为后退
handleFallback();
break;
case 'a':
// 如果指令为左转
handleTurnLeft();
break;
case 'd':
// 如果指令为右转
handleTurnRight();
break;
case 'q':
// 如果指令为停止
handleStop();
break;
default:
// 如果不匹配任何指令 不进行操作
break;
}
}

void setup() {
// put your setup code here, to run once:
// 初始化串口
Serial.begin(115200);
// 定义6、7、8、9引脚模式
pinMode(6, OUTPUT);
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
// 初始化时履带为停止状态 所以给6、7、8、9引脚输出高电平
digitalWrite(6, HIGH);
digitalWrite(7, HIGH);
digitalWrite(8, HIGH);
digitalWrite(9, HIGH);
}

void loop() {
// put your main code here, to run repeatedly:
// 如果串口接收到了控制指令则读取指令并做出相应的动作
if (Serial.available() > 0) {
char commend = Serial.read();
handleMotorAction(commend);
}
}

二、舵机控制

舵机控制使用的是PCA9685 16路舵机驱动板

下面是电路连接图:

1
2
3
4
5
6
7
8
9
10
11
PCA9685舵机驱动板                   Arduino UNO开发板

GND -------------------------- GND

SCL -------------------------- A5

SDA -------------------------- A4

VCC -------------------------- 5V


使用前请先确保Arduino IDE已安装了Adafruit PWM Servo Driver Library库

Arduino UNO舵机控制代码

百度网盘地址:链接:https://pan.baidu.com/s/1U4rW1MrFWgN5JhVQwd_Qtg?pwd=rghc

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* @author B站 李不胖谁胖
* @version 1.0.0
* @date 2024-01-20
*/
#include <Adafruit_PWMServoDriver.h>
// Servo shield controller class - assumes default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
//控制头俯仰
double servo_0_min_angle = 0;
double servo_0_max_angle = 120;
double servo_0_current_angle = 0;

//控制头旋转
double servo_1_min_angle = 45;
double servo_1_max_angle = 165;
double servo_1_current_angle = 105;

//控制脖子俯仰
double servo_2_min_angle = 0;
double servo_2_max_angle = 50;
double servo_2_current_angle = 0;

//控制左手
double servo_5_min_angle = 20;
double servo_5_max_angle = 100;
double servo_5_current_angle = 100;

//控制右手
double servo_7_min_angle = 100;
double servo_7_max_angle = 180;
double servo_7_current_angle = 100;

// 前进
void handleForward(){
digitalWrite(6, HIGH);
digitalWrite(7, LOW);
digitalWrite(8, HIGH);
digitalWrite(9, LOW);
}

// 后退
void handleFallback(){
digitalWrite(6, LOW);
digitalWrite(7, HIGH);
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
}

// 左转
void handleTurnLeft(){
digitalWrite(6, HIGH);
digitalWrite(7, LOW);
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
}

// 右转
void handleTurnRight(){
digitalWrite(6, LOW);
digitalWrite(7, HIGH);
digitalWrite(8, HIGH);
digitalWrite(9, LOW);
}

// 停止
void handleStop(){
digitalWrite(6, HIGH);
digitalWrite(7, HIGH);
digitalWrite(8, HIGH);
digitalWrite(9, HIGH);
}

// 控制舵机转动
void slowSetServoPulse(uint8_t n, double step, double current, double min, double max) {
double min_pulse = getServoPulse(min);
double max_pulse = getServoPulse(max);
double current_pulse = getServoPulse(current);
double target_pulse = getServoPulse(current + step);
Serial.print("min_pulse:");
Serial.println(min_pulse);
Serial.print("max_pulse:");
Serial.println(max_pulse);
Serial.print("target_pulse:");
Serial.println(target_pulse);
Serial.print("current_pulse:");
Serial.println(current_pulse);
if(current_pulse == target_pulse){
Serial.println("current_pulse == target_pulse:");
return;
}

if(target_pulse > max_pulse || target_pulse < min_pulse){
Serial.println("outof_pulse:");
return;
}

if(current_pulse > target_pulse){
for(int pulseLength = (int)(current_pulse - 1); pulseLength >= target_pulse ; pulseLength--){
pwm.setPWM(n, 0, pulseLength);
//servo_1_current_angle = pulseLength;
Serial.print("--current_pulse:");
Serial.println(pulseLength);
delay(6);
}
}
if(current_pulse < target_pulse){
for(int pulseLength = (int)(current_pulse + 1); pulseLength <= target_pulse ; pulseLength++){
pwm.setPWM(n, 0, pulseLength);
//servo_1_current_angle = pulseLength;
Serial.print("++current_pulse:");
Serial.println(pulseLength);
delay(6);
}
}
Serial.print("servo:");
Serial.println(n);
Serial.print("current_angle:");
if(n==0){
servo_0_current_angle += step;
Serial.println(servo_0_current_angle);
}
if(n==1){
servo_1_current_angle += step;
Serial.println(servo_1_current_angle);
}
if(n==2){
servo_2_current_angle += step;
Serial.println(servo_2_current_angle);
}
if(n==5){
servo_5_current_angle += step;
Serial.println(servo_5_current_angle);
}
if(n==7){
servo_7_current_angle += step;
Serial.println(servo_7_current_angle);
}
delay(100);
}

double getServoPulse(double angle) {
double pulse = angle;
pulse = pulse/90 + 0.5;
double pulselength;
pulselength = 1000000; // 1,000,000 us per second
pulselength /= 50; // 50 Hz
pulselength /= 4096; // 12 bits of resolution
pulse *= 1000;
pulse /= pulselength;
Serial.print("getServoPulse pulse:");
Serial.println(pulse);
return pulse;
}

//设置9g舵机角度
void servo_9g_write(uint8_t n,int Angle)
{
double pulse = Angle;
pulse = pulse/90 + 0.5;
setServoPulse(n,pulse);//0到180度映射为0.5到2.5ms
}

void setServoPulse(uint8_t n, double pulse) {
double pulselength;
pulselength = 1000000; // 1,000,000 us per second
pulselength /= 50; // 50 Hz
pulselength /= 4096; // 12 bits of resolution
pulse *= 1000;
pulse /= pulselength;
Serial.print("pulse:");
Serial.println(pulse);
pwm.setPWM(n, 0, pulse);
}

void demoAction(){
slowSetServoPulse(0, 90-servo_0_current_angle, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
slowSetServoPulse(1, 75-servo_1_current_angle, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
slowSetServoPulse(5, 50-servo_5_current_angle, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
slowSetServoPulse(7, 100-servo_7_current_angle, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
delay(1000);
slowSetServoPulse(0, 0-servo_0_current_angle, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
slowSetServoPulse(1, 135-servo_1_current_angle, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
slowSetServoPulse(5, 100-servo_5_current_angle, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
slowSetServoPulse(7, 150-servo_7_current_angle, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
delay(2000);
slowSetServoPulse(0, 90-servo_0_current_angle, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
slowSetServoPulse(1, 75-servo_1_current_angle, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
slowSetServoPulse(5, 50-servo_5_current_angle, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
slowSetServoPulse(7, 100-servo_7_current_angle, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
delay(1000);
slowSetServoPulse(0, 0-servo_0_current_angle, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
slowSetServoPulse(1, 135-servo_1_current_angle, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
slowSetServoPulse(5, 100-servo_5_current_angle, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
slowSetServoPulse(7, 150-servo_7_current_angle, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
delay(2000);
}

// 处理动作
void handleMotorAction(char commend){
if(commend == '\n' || commend == '\r'){
return;
}
Serial.print("commend:");
Serial.print(commend);
Serial.println();

switch (commend){
case 'w':
// 如果指令为前进
handleForward();
break;
case 's':
// 如果指令为后退
handleFallback();
break;
case 'a':
// 如果指令为左转
handleTurnLeft();
break;
case 'd':
// 如果指令为右转
handleTurnRight();
break;
case 'q':
// 如果指令为停止
handleStop();
break;
case 'k':
// 抬脖子
slowSetServoPulse(2, -25, servo_2_current_angle,servo_2_min_angle,servo_2_max_angle);
break;
case 'i':
// 低脖子
slowSetServoPulse(2, 25, servo_2_current_angle,servo_2_min_angle,servo_2_max_angle);
break;
case 'j':
// 左转头
slowSetServoPulse(1, -30, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
break;
case 'l':
// 右转头
slowSetServoPulse(1, 30, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
break;
case 'r':
// 抬头
slowSetServoPulse(0, 40, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
break;
case 'f':
// 低头
slowSetServoPulse(0, -40, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
break;
case 't':
// 左手上
slowSetServoPulse(5, -20, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
break;
case 'g':
// 左手下
slowSetServoPulse(5, 20, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
break;
case 'y':
// 右手上
slowSetServoPulse(7, 20, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
break;
case 'h':
// 右手下
slowSetServoPulse(7, -20, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
break;
case 'z':
// 播放音乐4
//myDFPlayer.playMp3Folder(4);
break;
case '1':
// 播放音乐6
//myDFPlayer.playMp3Folder(6);
break;
case '2':
// 动作编组1
demoAction();
break;
case '3':
// 复位
resetServo();
break;
default:
// 如果不匹配任何指令 不进行操作
break;
}
}

void initMotor(){
// 定义6、7、8、9引脚模式
pinMode(6, OUTPUT);
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
// 初始化时履带为停止状态 所以给6、7、8、9引脚输出高电平
digitalWrite(6, HIGH);
digitalWrite(7, HIGH);
digitalWrite(8, HIGH);
digitalWrite(9, HIGH);
}

// 关节复位
void resetServo(){
slowSetServoPulse(0, 0-servo_0_current_angle, servo_0_current_angle,servo_0_min_angle,servo_0_max_angle);
slowSetServoPulse(1, 105-servo_1_current_angle, servo_1_current_angle,servo_1_min_angle,servo_1_max_angle);
slowSetServoPulse(2, 0-servo_2_current_angle, servo_2_current_angle,servo_2_min_angle,servo_2_max_angle);
slowSetServoPulse(5, 100-servo_5_current_angle, servo_5_current_angle,servo_5_min_angle,servo_5_max_angle);
slowSetServoPulse(7, 100-servo_7_current_angle, servo_7_current_angle,servo_7_min_angle,servo_7_max_angle);
}
void servoSetUp(){
servo_9g_write(0,0);//控制头俯仰
servo_9g_write(1,105);//控制头旋转
servo_9g_write(2,0);//控制脖子俯仰
// servo_9g_write(3,0);//控制右眼
// servo_9g_write(4,0);//控制左眼
servo_9g_write(5,100);//控制左手
servo_9g_write(7,100);//控制右手
}
void initServo(){
// Communicate with servo shield (Analog servos run at ~60Hz)
pwm.begin();
pwm.setPWMFreq(50);
servoSetUp();
}

void setup() {
// put your setup code here, to run once:
// 初始化串口
Serial.begin(115200);
initMotor();
initServo();
servoSetUp();
}

void loop() {
// put your main code here, to run repeatedly:
// 如果串口接收到了控制指令则读取指令并做出相应的动作
if (Serial.available() > 0) {
char commend = Serial.read();
handleMotorAction(commend);
}
}



2.1 舵机角度标定(必读,避免炸机)

直接烧别人的代码不标定,大概率会把舵机或外壳卡死。 每只瓦力组装后的舵机机械零位都不同,UP 主代码里的 servo_X_min/max_angle 是他自己机器测出来的值,你必须按自己的机器重新标定

为什么必须标定

代码中每个舵机都定义了机械限位常量:

1
2
3
double servo_0_min_angle = 0;     // 头俯仰下限
double servo_0_max_angle = 120; // 头俯仰上限
double servo_0_current_angle = 0; // 初始角度

如果你的舵机臂安装角度不同、或外壳干涉位置不同,直接用 UP 主的 0~120 可能会:

  • 越过机械限位 → 舵机 “嗞嗞” 响、烫手、齿轮打齿 → 舵机报废
  • 碰到外壳 → 外壳变形、舵机堵转 → 舵机或外壳报废
  • 关节抖动不停 → 舵机内部反馈错乱 → 过热烧毁

标定步骤

1. 不要装外壳! 只把 PCA9685、舵机、UNO、电源接好通电。

2. 烧录最简测试代码(每次只测一个舵机):

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
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

void servo_9g_write(uint8_t n, int angle) {
double pulse = angle / 90.0 + 0.5;
pulse *= 1000;
pulse /= (1000000.0 / 50 / 4096);
pwm.setPWM(n, 0, pulse);
}

void setup() {
Serial.begin(115200);
pwm.begin();
pwm.setPWMFreq(50);

// 每次改这两个值后重新烧录
uint8_t channel = 0; // 测试通道(0~7)
int test_angle = 90; // 测试角度

servo_9g_write(channel, test_angle);
Serial.print("CH"); Serial.print(channel);
Serial.print(" -> "); Serial.println(test_angle);
}

void loop() {}

3. 从 90° 开始(SG90 物理中位,最安全)。

4. 逐步增减 5° 重新烧录,听声音、看动作

现象 含义 下一步
舵机平稳无杂音 角度安全 继续推 5°
舵机轻微抖动 接近限位 退回 5° 记下
舵机 “嗞嗞” 卡顿 / 烫手 越过限位 立刻断电,刚才的角度就是机械极限

不要越过限位再试 —— 越一次伤一次

5. 记录每个舵机的安全 min / max,更新到完整代码:

1
2
3
double servo_0_min_angle = <你测出来的最小值>;
double servo_0_max_angle = <你测出来的最大值>;
double servo_0_current_angle = <装好后的初始角度>;

6. 装好外壳后再标一次 —— 外壳会进一步压缩活动范围,没装壳标定的值不能直接用

各关节参考值(UP 主机器,仅作起点)

通道 关节 UP 主 min UP 主 max UP 主初始 你的 min 你的 max 你的初始
CH0 头俯仰 120°
CH1 头水平旋转 45° 165° 105°
CH2 脖子俯仰 50°
CH3 左眼 20°
CH4 右眼 20° 20°
CH5 左手 20° 100° 100°
CH6 (空,未使用) - - - - - -
CH7 右手 100° 180° 100°

CH6 在原作中跳过未用,你可以分配给”嘴部开合”等扩展关节。

常见标定问题

现象 原因 解决
舵机抖动不停 PCA9685 V+ 电流不够 V+ 独立接 5V,不要从 UNO 5V 借电
上电瞬间舵机猛抽一下 上电瞬间 PWM 不稳定 setup() 里立刻调用 servoSetUp() 复位
改了代码常量但不生效 servo_X_current_angle 没同步改 min / max / current 三个常量一起改
关节互相干扰、误动作 多舵机同时上电误触发 逐个标定,未标定的舵机断电
复位后位置漂移 / 不准 SG90 是开环舵机,电压不稳 检查 18650 电量、降压模块输出是否稳 5V
每次上电姿态不一样 上电瞬间舵机随机抽搐 setup() 末尾必须调用 servoSetUp() 强制归位

三、Web控制端

3.1 Arduino IDE开发环境搭建

Arduino官网 https://www.arduino.cc/

Arduino中文社区 https://arduino.me/

  1. 安装Arduino软件

  2. 添加开发板管理器 文件 -> 首选项 -> 其他开发板管理器地址 将如下链接填入

    以下是Arduino中文社区提供的index 会比github上下载的快些,github可能会遇到下载失败的情况

1
2
http://arduino.me/packages/esp8266.json
https://arduino.me/packages/esp32.json

3.安装Blinker库 在线安装失败 请尝试离线安装包 https://www.diandeng.tech/doc/getting-start-esp32-wifi

Blinker 首页 https://www.diandeng.tech/home

Blinker 的github仓库地址可以自行下载最新支持包 https://github.com/blinker-iot

Blinker 官网提供的安装包 https://www.diandeng.tech/sdk/blinker-library-0.3.10230510.zip

4.安装 ESP32、ESP8266 开发板库,直接点击分享的对应库的exe文件安装既可

5.安装USB串口驱动,这个要看你所使用的开发板上下载芯片的型号,合宙的ESP32 C3要安装CH343驱动,常见的是CH34X、CP21XX芯片等,文件里分享了这两种,如果没有的话请搜索你开发板的串口芯片型号,下载对应驱动后安装。

3.2 使用Blinker App 作为控制端

  1. 新建Blinker独立设备、复制设备秘钥替换代码中的Blinker设备秘钥。

    1
    2
    3
    // Blinker设备秘钥
    #include "secrets.h"
    char auth[] = BLINKER_AUTH;
  2. 更新Blinker App 瓦力设备的界面配置,使用如下代码进行替换。

瓦力界面配置代码:

1
2
{¨version¨¨2.0.0¨¨config¨{¨headerColor¨¨transparent¨¨headerStyle¨¨dark¨¨background¨{¨img¨¨assets/img/headerbg.jpg¨¨isFull¨«}}¨dashboard¨|{¨type¨¨tex¨¨t0¨¨blinker入门示例¨¨t1¨¨文本2¨¨bg¨Ë¨ico¨´´¨cols¨Í¨rows¨Ê¨key¨¨tex-272¨´x´É´y´É¨speech¨|÷¨lstyle¨Ê¨clr¨¨#FFF¨}{ßC¨btn¨ßJ¨fas fa-arrow-alt-down¨¨mode¨ÉßE¨后退¨ßGßHßIÉßKËßLËßM¨btn-back¨´x´Ì´y´¤FßPÉßQ¨#076EEF¨}{ßCßSßJ¨fas fa-arrow-alt-up¨ßUÉßE¨前进¨ßGßHßIÉßKËßLËßM¨btn-go¨´x´Ì´y´¤BßQßXßPÉ}{ßCßSßJ¨fal fa-power-off¨ßUÉßE¨急停¨ßGßHßIÉßKËßLËßM¨btn-stop¨´x´Ì´y´¤DßQ¨#EA0909¨ßPÉ}{ßCßSßJ¨fas fa-arrow-alt-right¨ßUÉßE¨向右¨ßGßHßIÉßKËßLËßM¨btn-right¨´x´Î´y´¤DßPÉßQßX}{ßCßSßJ¨fas fa-arrow-alt-left¨ßUÉßE¨向左¨ßGßHßIÉßKËßLËßM¨btn-left¨´x´Ê´y´¤DßQßXßPÉ}{ßCßSßJ¨fad fa-arrow-alt-circle-up¨ßUÉßE¨抬脖子¨ßGßHßIÉßKËßLËßM¨btn-nick-up¨´x´Ì´y´ÏßQ¨#FBA613¨}{ßCßSßJ¨fad fa-arrow-alt-circle-down¨ßUÉßE¨低脖子¨ßGßHßIÉßKËßLËßM¨btn-nick-down¨´x´Ì´y´ÑßQßoßPÉ}{ßCßSßJ¨fad fa-arrow-alt-up¨ßUÉßE¨左手上¨ßGßHßIÉßKËßLËßM¨btn-left-hand-up¨´x´É´y´ÒßQ¨#00A90C¨ßPÉ}{ßCßSßJßsßUÉßE¨右手上¨ßGßHßIÉßKËßLËßM¨btn-right-hand-up¨´x´Ï´y´ÒßQßv}{ßCßSßJ¨fad fa-arrow-alt-down¨ßUÉßE¨左手下¨ßGßHßIÉßKËßLËßM¨btn-left-hand-down¨´x´É´y´¤BßPÉßQßv}{ßCßSßJßyßUÉßE¨右手下¨ßGßHßIÉßKËßLËßM¨btn-right-hand-down¨´x´Ï´y´¤BßQßv}{ßCßSßJ¨fad fa-arrow-alt-circle-left¨ßUÉßE¨左转头¨ßGßHßIÉßKËßLËßM¨btn-head-left¨´x´Ê´y´ÐßQßo}{ßCßSßJ¨fad fa-arrow-alt-circle-right¨ßUÉßE¨右转头¨ßGßHßIÉßKËßLËßM¨btn-head-right¨´x´Î´y´ÐßPÉßQßo}{ßCßSßJßyßUÉßE´低头´ßGßHßIÉßKËßLËßM¨btn-head-down¨´x´Ì´y´ÍßQßoßPÉ}{ßCßSßJßsßUÉßE´抬头´ßGßHßIÉßKËßLËßM¨btn-head-up¨´x´Ì´y´ËßQßo}{ßCßSßJ¨fad fa-user-robot¨ßUÉßE¨动作1¨ßGßHßIÉßKËßLËßM¨btn-act1¨´x´É´y´ÎßQßXßPÉ}{ßCßSßJ¨fad fa-redo-alt¨ßUÉßE¨动作复位¨ßGßHßIÉßKËßLËßM¨btn-reset¨´x´Ï´y´ÎßPÉßQßX}{ßCßSßJ¨fad fa-lightbulb-on¨ßUÉßE´激光´ßGßHßIÉßKËßLËßM¨btn-led¨´x´É´y´ÌßPÉßQße}{ßCßSßJ¨fad fa-thermometer-three-quarters¨ßUÉßE´温度´ßGßHßIÉßKËßLËßM¨btn-tem¨´x´Ï´y´ÌßPÉßQßv}{ßC¨deb¨ßUÉßIÉßKÑßLÌßM¨debug¨´x´É´y´¤H}÷¨actions¨|¦¨cmd¨¦¨switch¨‡¨text¨‡´on´¨打开?name¨¨off¨¨关闭?name¨—÷¨triggers¨|{¨source¨ß1P¨source_zh¨¨开关状态¨¨state¨|´on´ß1S÷¨state_zh¨|´打开´´关闭´÷}÷´rt´|÷}

3.烧写控制代码到ESP32 C3开发板

合宙ESP32 C3 开发板官方文档 https://wiki.luatos.com/chips/esp32c3/index.html

如果使用的开发板不是ESP32 C3 请自行替换引脚定义,否则编译可能会出现报错。

代码中需要替换以下内容

1
2
3
4
5
6
7
// Blinker设备秘钥
#include "secrets.h"
char auth[] = BLINKER_AUTH;
// WIFI名称
char ssid[] = WIFI_SSID;
// WIFI密码
char pswd[] = WIFI_PASSWORD;

更改设备秘钥和wifi名称密码后,将ESP32_Blinker.ino 烧录到ESP32 C3开发板上

烧录之前请确保Arduino IDE已安装如下库:

1
2
3
4
Blinker
DHT sensor library
Adafruit Unified Sensor
EspSoftwareSerial

使用合宙ESP32 C3 烧录程序有一下几点需要注意,否则可能会导致烧录失败:

1.Flash Mode选择 “DIO” 模式。

2.USB CDC On Boot 选择 “Enabled” 方便串口调试。

3.按住板载boot键上电进入下载模式,此时两个板载led微亮。

4.如果烧录完成,板载的灯没有亮起,还是微亮状态,请按下reset键重启,或者给开发板重新上电。

3.3 Blinker控制方式的ESP32 C3代码(不推荐,连接可能不稳定)

百度网盘链接:https://pan.baidu.com/s/1OK_kX1OvmjliplcLMSUIGQ?pwd=e1in

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
/**
* @author B站 李不胖谁胖
* @version 1.0.0
* @date 2024-01-14
*
* 如何使用:
* 1. 安装Blinker库
* a. 在Arduino IDE中 点击项目->导入库->管理库 搜索Blinker
* 2. 上传代码至 合宙ESP32 C3
*/
#define BLINKER_WIFI
#include <Blinker.h>
#include <SoftwareSerial.h>
// 温度模块
#include "DHT.h"
#define DHTTYPE DHT11 // DHT 11
#define DHTPIN 4 // 温度模块引脚
DHT dht(DHTPIN, DHTTYPE);
// RX, TX 软串口用于连接语音模块串口
SoftwareSerial asrProSerial(4, 5);
// Blinker设备秘钥
#include "secrets.h"
char auth[] = BLINKER_AUTH;
// WIFI名称
char ssid[] = WIFI_SSID;
// WIFI密码
char pswd[] = WIFI_PASSWORD;

// 合宙ESP32 C3板载Led引脚
const int board_led_left = 12;
const int board_led_right = 13;

// 新建控制组件对象
BlinkerButton Button1("btn-abc");
// 前进
BlinkerButton Button_GO("btn-go");
// 后退
BlinkerButton Button_BACK("btn-back");
// 左转
BlinkerButton Button_LEFT("btn-left");
// 右转
BlinkerButton Button_RIGHT("btn-right");
// 停止
BlinkerButton Button_STOP("btn-stop");
// 左手上抬
BlinkerButton Button_LEFT_HAND_UP("btn-left-hand-up");
//左手下
BlinkerButton Button_LEFT_HAND_DOWN("btn-left-hand-down");
// 右手上抬
BlinkerButton Button_RIGHT_HAND_UP("btn-right-hand-up");
//右手下
BlinkerButton Button_RIGHT_HAND_DOWN("btn-right-hand-down");
// 抬脖子
BlinkerButton Button_NICK_UP("btn-nick-up");
// 低脖子
BlinkerButton Button_NICK_DOWN("btn-nick-down");
// 抬头
BlinkerButton Button_HEAD_UP("btn-head-up");
// 低头
BlinkerButton Button_HEAD_DOWN("btn-head-down");
// 左转头
BlinkerButton Button_HEAD_LEFT("btn-head-left");
// 右转头
BlinkerButton Button_HEAD_RIGHT("btn-head-right");
// 温度
BlinkerButton Button_TEM("btn-tem");
// 激光
BlinkerButton Button_LED("btn-led");
// 动作1
BlinkerButton Button_ACT1("btn-act1");
// 动作重置
BlinkerButton Button_RESET("btn-reset");

// 按下前进按键即会执行该函数
void button_go_callback(const String & state)
{
if (state=="tap") {
Button_GO.print("前进");
Serial1.println('w');
delay(100);
}
if(state=="pressup"){
Button_GO.print("前进结束");
Serial1.println('q');
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下后退按键即会执行该函数
void button_back_callback(const String & state)
{
if (state=="tap") {
Serial1.println("s");
delay(100);
Button_BACK.print("后退");
}
if(state=="pressup"){
Serial1.println("q");
Button_BACK.print("后退结束");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下左转按键即会执行该函数
void button_left_callback(const String & state)
{
if (state=="tap") {
Serial1.println("a");
Serial.println("a");
Button_LEFT.print("左转一下");
}
if(state=="pressup"){
Serial1.println("q");
Serial.println("q");
Button_LEFT.print("左转结束");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下右转按键即会执行该函数
void button_right_callback(const String & state)
{
if (state=="tap") {
Serial1.println("d");
Serial.println("d");
Button_RIGHT.print("右转一下");
}
if(state=="pressup"){
Serial1.println("q");
Serial.println("q");
Button_RIGHT.print("右转结束");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}


// 按下停止按键即会执行该函数
void button_stop_callback(const String & state)
{
if (state=="tap") {
Serial1.println("q");
Serial.println("q");
Button_STOP.print("停止");
}
if(state=="pressup"){
Serial1.println("q");
Serial.println("q");
Button_STOP.print("停止");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}


// 按下抬脖子执行该函数
void button_nick_up_callback(const String & state)
{
if (state=="tap") {
Serial1.println("k");
Serial.println("k");
Button_NICK_UP.print("抬脖子");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下低脖子执行该函数
void button_nick_down_callback(const String & state)
{
if (state=="tap") {
Serial1.println("i");
Serial.println("i");
Button_NICK_DOWN.print("低脖子");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下左转头执行该函数
void button_head_left_callback(const String & state)
{
if (state=="tap") {
Serial1.println("j");
Serial.println("j");
Button_HEAD_LEFT.print("左转头");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下右转头执行该函数
void button_head_right_callback(const String & state)
{
if (state=="tap") {
Serial1.println("l");
Serial.println("l");
Button_HEAD_RIGHT.print("右转头");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下右手上执行该函数
void button_right_hand_up_callback(const String & state)
{
if (state=="tap") {
Serial1.println("y");
Serial.println("y");
Button_RIGHT_HAND_UP.print("右手上");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下右手下执行该函数
void button_right_hand_down_callback(const String & state)
{
if (state=="tap") {
Serial1.println("h");
Serial.println("h");
Button_RIGHT_HAND_DOWN.print("右手下");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下左手上执行该函数
void button_left_hand_up_callback(const String & state)
{
if (state=="tap") {
Serial1.println("t");
Serial.println("t");
Button_LEFT_HAND_UP.print("左手上");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下左手下执行该函数
void button_left_hand_down_callback(const String & state)
{
if (state=="tap") {
Serial1.println("g");
Serial.println("g");
Button_LEFT_HAND_DOWN.print("左手下");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下抬头执行该函数
void button_head_up_callback(const String & state)
{
if (state=="tap") {
Serial1.println("r");
Serial.println("r");
Button_HEAD_UP.print("抬头");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下低头执行该函数
void button_head_down_callback(const String & state)
{
if (state=="tap") {
Serial1.println("f");
Serial.println("f");
Button_LEFT_HAND_DOWN.print("低头");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下动作1执行该函数
void button_act1_callback(const String & state)
{
if (state=="tap") {
Serial1.println("2");
Serial.println("2");
Button_LEFT_HAND_DOWN.print("动作1");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下重置执行该函数
void button_reset_callback(const String & state)
{
if (state=="tap") {
Serial1.println("3");
Serial.println("3");
Button_LEFT_HAND_DOWN.print("重置");
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下温度执行该函数
void button_tem_callback(const String & state)
{
if (state=="tap") {
// 打印温度
float h = dht.readHumidity();
// Read temperature as Celsius (the default)
float t = dht.readTemperature();
String str1 = String(t, 1);
String wendu = "温度";
wendu.concat(str1);
String str2 = String(h, 1);
String shidu = "湿度";
shidu.concat(str2);
wendu.concat("\n");
wendu.concat(shidu);
Button_TEM.print(wendu);
}
// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 按下激光执行该函数
void button_led_callback(const String & state)
{
int currentLedLevel = digitalRead(10);
if (state=="tap") {
if(currentLedLevel == HIGH){
digitalWrite(10, LOW);
Serial.println("关闭激光");
Button_LED.print("关闭激光");
}
if(currentLedLevel == LOW){
digitalWrite(10, HIGH);
Serial.println("开启激光");
Button_LED.print("开启激光");
}
}

// 板载Led交替闪亮
setLed();
//digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 接收到指令时 板载Led交替闪亮 方便查看
void setLed() {
int currentLevelLeft = digitalRead(board_led_left);
int currentLevelRight = digitalRead(board_led_right);
if (currentLevelLeft == LOW) {
digitalWrite(board_led_left, HIGH);
}
if (currentLevelLeft == HIGH) {
digitalWrite(board_led_left, LOW);
}
if (currentLevelRight == LOW) {
digitalWrite(board_led_right, HIGH);
}
if (currentLevelRight == HIGH) {
digitalWrite(board_led_right, LOW);
}
delay(1000);
}

// 初始化板载led
void initLed(){
pinMode(board_led_left, OUTPUT);
pinMode(board_led_right, OUTPUT);
digitalWrite(board_led_left, HIGH);
digitalWrite(board_led_right, LOW);
}

void setup()
{
Serial.begin(115200);
// 初始化串口1 用于连接Arduino UNO 主控板
Serial1.begin(115200, SERIAL_8N1, 0, 1);
// 初始化软串口 用于连接语音模块接收语音识别控制信号
asrProSerial.begin(115200);
// 初始化温度传感器
dht.begin();
// 初始化有LED的IO
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
// 初始化激光引脚
pinMode(10, OUTPUT);
digitalWrite(10, LOW);
// 初始化blinker
Blinker.begin(auth, ssid, pswd);
// 注册控制按钮组件
// 前、后、左、右
Button_GO.attach(button_go_callback);
Button_BACK.attach(button_back_callback);
Button_LEFT.attach(button_left_callback);
Button_RIGHT.attach(button_right_callback);
// 停止
Button_STOP.attach(button_stop_callback);
// 抬脖子
Button_NICK_UP.attach(button_nick_up_callback);
// 抬脖子
Button_NICK_DOWN.attach(button_nick_down_callback);
// 左转头
Button_HEAD_LEFT.attach(button_head_left_callback);
// 右转头
Button_HEAD_RIGHT.attach(button_head_right_callback);
// 抬头
Button_HEAD_UP.attach(button_head_up_callback);
// 低头
Button_HEAD_DOWN.attach(button_head_down_callback);
// 温度
Button_TEM.attach(button_tem_callback);
// 激光
Button_LED.attach(button_led_callback);
// 动作1
Button_ACT1.attach(button_act1_callback);
// 动作重置
Button_RESET.attach(button_reset_callback);
// 初始化板载led灯 12 13引脚
initLed();
BLINKER_DEBUG.stream(Serial);
}

void loop() {
Blinker.run();
// 语音识别模块是否发来了控制指令
if(asrProSerial.available() > 0) {
// 读取控制指令,并下发到Arduino UNO主控板
char commend = asrProSerial.read();
Serial1.println(commend);
}
}

3.4 使用自建H5页面作为控制端

1.安装Docker

1
2
3
4
5
6
7
8
9
10
#安装相关工具
yum install -y yum-utils device-mapper-persistent-data lvm2
#配置清华yum源
yum-config-manager --add-repo https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo
#安装Docker
yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin
#启动Docker
systemctl start docker
#查看Docker版本号
docker -v

2.Docker安装 EMQX

1
2
3
4
5
#拉取镜像
docker pull emqx/emqx:5.4.1
#启动EMQX
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx:5.4.1
#开启服务器的1883、8083、8084、8883端口

3.安装Nginx

1
2
3
yum install -y nginx
#启动 nginx
systemctl start nginx

4.部署H5控制页面

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
#安装unzip
yum install -y unzip

#新建文件夹用于存放H5页面
cd /home
mkdir emqx

#上传dist.zip到 /home/emqx文件夹中

#解压dist.zip
unzip /home/emqx/dist.zip

#Nginx增加如下配置
#域名 xxxx.xxx
server {
listen 80;
server_name xxxx.xxx;
location / {
client_max_body_size 200m;
root /home/emqx/dist;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}

#最后重启Nginx
nginx -s reload

5.烧录ESP32_H5.ino程序到ESP32中

  • 打开 ESP32_H5.ino 文件,替换mqtt链接相关的参数
1
2
3
4
5
6
7
// MQTT Broker
const char *mqtt_broker = "服务器ip或域名";
const char *topic = "订阅的topic";
const char *mqtt_username = "";
const char *mqtt_password = "";
const int mqtt_port = 1883;

  • 参数替换完成后,烧录程序到ESP32中,打开串口监视器查看日志。
  • 打开H5控制页面,测试发送指令,并在串口监视器中查看。

不想自己搭建?直接使用作者分享的网站和 MQTT 服务器

1
2
3
4
5
6
7
8
9
10
11
H5控制页面地址:https://walle.werfamily.fun
mqtt服务器:t.werfamily.fun
Topic 替换为自己的topic防止与其他小伙伴冲突 格式为 bili/B站UID

// MQTT Broker
const char *mqtt_broker = "t.werfamily.fun";
const char *topic = "bili/你的B站UID";
const char *mqtt_username = "";
const char *mqtt_password = "";
const int mqtt_port = 1883;

自己搭建:需要替换如下,协议、服务器地址、端口

使用我搭建好的:topic请输入自己的B站UID

为了防止小伙伴订阅的主题重复冲突,所以使用时务必要将UID替换为自己的B站UID

B站UID查看的方法为 进入”我的主页” -> 点击空间 -> 点击详情

3.5 H5页面控制方式的ESP32 C3代码(推荐,连接稳定)

百度网盘链接:https://pan.baidu.com/s/1HEGymdvOeyf_LJFmb-WZXg?pwd=qh1i

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
* @author B站 李不胖谁胖
* @version 1.0.0
* @date 2024-01-14
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <SoftwareSerial.h>
#include <ArduinoJson.h> // JSON解析库
#include <string.h>
SoftwareSerial uart1(6,7);//RX=6,TX=7
// WiFi
#include "secrets.h"
const char *ssid = WIFI_SSID;
const char *password = WIFI_PASSWORD;

// MQTT Broker
const char *mqtt_broker = "t.werfamily.fun";
const char *topic = "bili/423212190";
const char *mqtt_username = "";
const char *mqtt_password = "";
const int mqtt_port = 1883;
// 合宙ESP32 C3板载Led引脚
const int board_led_left = 12;
const int board_led_right = 13;
WiFiClient espClient;
PubSubClient client(espClient);

void setup() {
// 初始化串口
Serial.begin(115200);
uart1.begin(115200);
Serial1.begin(115200, SERIAL_8N1, 0, 1);
pinMode(board_led_left, OUTPUT);
pinMode(board_led_right, OUTPUT);
digitalWrite(board_led_left, HIGH);
digitalWrite(board_led_right, LOW);
pinMode(10, OUTPUT);
digitalWrite(10, LOW);
// 连接wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.println("Connecting to WiFi..");
}
Serial.println("Connected to the WiFi network");
// 连接mqtt broker
client.setServer(mqtt_broker, mqtt_port);
client.setCallback(callback);
while (!client.connected()) {
String client_id = "esp32-client-";
client_id += String(WiFi.macAddress());
Serial.printf("The client %s connects to the public mqtt broker\n", client_id.c_str());
if (client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
Serial.println("Mqtt broker已连接");
} else {
Serial.print("Mqtt broker连接失败 state:");
Serial.print(client.state());
delay(2000);
}
}
// 发送消息
client.publish(topic, "Hi EMQX I'm ESP32");
// 订阅消息topic
client.subscribe(topic);
}

// led交替闪烁
void setLed() {
int currentLevelLeft = digitalRead(board_led_left);
int currentLevelRight = digitalRead(board_led_right);
if (currentLevelLeft == LOW) {
digitalWrite(board_led_left, HIGH);
}
if (currentLevelLeft == HIGH) {
digitalWrite(board_led_left, LOW);
}
if (currentLevelRight == LOW) {
digitalWrite(board_led_right, HIGH);
}
if (currentLevelRight == HIGH) {
digitalWrite(board_led_right, LOW);
}
}

// 处理瓦力眼睛红色激光灯
void handlerRedLed(String msg) {
String ledLow = "0";
String ledHigh = "1";
if (ledHigh == msg) {
digitalWrite(10, HIGH);
}
if (ledLow == msg) {
digitalWrite(10, LOW);
}
}

// mqtt回调
void callback(char *topic, byte *payload, unsigned int length) {
Serial.print("Message arrived in topic: ");
Serial.println(topic);
Serial.print("Message:");
String msg = "";
for (int i = 0; i < length; i++) {
msg.concat((char)payload[i]);
}
Serial.println(msg);
// 解析JSON消息
char *msgBody; // 创建指向char类型的指针变量
int len = msg.length(); // 获取字符串长度
msgBody = new char[len + 1]; // 分配足够大小的内存空间(加上结尾的'\0')
strcpy(msgBody, msg.c_str()); // 将字符串复制到char*变量中
const size_t capacity = JSON_OBJECT_SIZE(1) + 60; // 根据JSON结构调整容量大小
StaticJsonDocument<256> doc; // 创建一个静态大小为 256 字节的 JsonDocument 对象
DeserializationError error = deserializeJson(doc, msgBody); // 将 JSON 字符串反序列化到 JsonDocument
if (error) {
Serial.print("deserializeJson() failed with code ");
Serial.println(error.c_str());
return;
}
String commend = doc["commend"]; // 获取名称属性值
String value = doc["value"]; // 获取年龄属性值
Serial.print("指令: ");
Serial.println(commend);
Serial.print("参数值: ");
Serial.println(value);
// 处理瓦力眼睛红色激光灯
handlerRedLed(commend);
// 向arduino uno透传数据
Serial1.println(commend);
// 板载Led交替闪亮
setLed();
}

void loop() {
client.loop();
// 接收到语音模块的之后后向Arduino UNO透传指令
if(uart1.available()>0){
char commend = uart1.read();
Serial1.println(commend);
}
}

3.6 如何获取UID

电脑浏览器查看自己的UID 在个人资料中

手机端查看自己的UID

第一步

第二步

第三步

四、扩展模块(选配)

没有使用的模块请自行删除对应代码

完整版(包含所有模块)ESP32 C3代码:链接:https://pan.baidu.com/s/1AcRYgk3Fzj8J-DgBbBVwbA?pwd=453w

完整版(包含所有模块)Arduino UNO代码:链接:https://pan.baidu.com/s/1WTP7OwrzfyabWFWJCDkhqA?pwd=w5f2

使用语音控制模块前请先安装天问Block软件

天问官网 :http://www.twen51.com/new/twen51/index.php

ASR PRO语音模块代码:https://pan.baidu.com/s/1F9mbbMGzFKIO1RYE3TRoSA?pwd=ucwb

4.1 ASR PRO语音控制模块

1
2
3
4
5
6
7
8
9
10
11
12

ASR PRO语音模块 ESP32 C3

GND -------------------------- GND

5V -------------------------- 5V

PB5 -------------------------- IO06

PB6 -------------------------- IO07


4.2 DHT11温湿度模块

需要安装 DHT sensor library 库

1
2
3
4
5
6
7
8
DHT11温湿度模块                          ESP32 C3

+ -------------------------- 3.3V

OUT -------------------------- IO04

GND -------------------------- GND

4.3 RC522 RFID识别模块

Arduino_NFC代码:链接:https://pan.baidu.com/s/1FvI96uUmDy-ZOgpB4U8FaA?pwd=wzwa

需要安装 MFRC522 库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RC522 RFID模块                        Arduino UNO 开发板

SDA -------------------------- 10

SCK -------------------------- 13

MOSI -------------------------- 11

MISO -------------------------- 12

GND -------------------------- GND

3.3V -------------------------- 3.3V


4.4 Mini Mp3Player Mp3播放模块

需要安装 DFRobotDFPlayerMini 库

官方文档:https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299

把音乐文件夹导入SD卡中

音乐链接:https://pan.baidu.com/s/15qRZ5JTXNoIE5nzJVDV0Vw?pwd=dmkf

Arduino_MP3代码:链接:https://pan.baidu.com/s/1Si77ZYmtONfURM49FJ6aQw?pwd=dpfk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                   Mini Mp3Player模块                    Arduino UNO 开发板

VCC -------------------------- 5V

RX -------------------------- 2

TX -------------------------- 3

DAC_R -------------------------- 不接线

DAC_L -------------------------- 不接线

喇叭1 ---------------- SPK_1

GND -------------------------- GND

喇叭2 ---------------- SPK_2


4.5 红色激光

1
2
3
4
5
6
红色激光头                               ESP32 C3

+ -------------------------- IO10

GND -------------------------- GND

问题Q&A

排查问题前,请先回到文档开头 “整体架构” 章节,对照其中的三段通信图整体连接图,按图中定义来约定一二三段通信。

控制不了?没反应?

(1)首先确保H5控制端和ESP32都连接上了mqtt服务器。

(2)一段通信:H5控制端连接mqtt服务器后,点击发送指令,如果页面自己能收到指令并打印日志,则证明一段通信没问题。

(3)二段通信:H5发送指令后,ESP32串口有相应日志输出,并且板载Led交替闪烁。

(4)三段通信:H5发送指令后,UNO串口有相应日志输出。

ESP32连接不上MQTT服务器?

1.mqtt的topic是否已替换

2.WIFI和密码是否已替换

如何提问?

先确保以上链路的通信没问题之后,在检查模块接线。

1.开发板代码版本、问题描述

2.ESP32串口日志截图

3.UNO串口日志截图

第一次写教程录视频,难免有些卡壳和不足,如有错误之处还请指正,请见谅。

如果感觉对你有帮助的话,请帮忙点个一键三连,谢谢。


附录:资源下载汇总

各章节内部的下载链接保留不动,本附录用于一站式查询,方便对照下载。链接均为作者原始百度网盘地址,提取码已附带。

A.1 代码资源

# 资源 适用 链接 提取码 出现章节
1 Arduino UNO 履带电机代码 UNO 单独控制履带 https://pan.baidu.com/s/1HNKWE4Z2C2nG0kT3WxBQWA zm6l 一、1.3
2 Arduino UNO 舵机控制代码 UNO 单独控制舵机(PCA9685) https://pan.baidu.com/s/1U4rW1MrFWgN5JhVQwd_Qtg rghc 二、舵机控制
3 Blinker 控制方式 ESP32 C3 代码 ESP32 端,配合 Blinker App https://pan.baidu.com/s/1OK_kX1OvmjliplcLMSUIGQ e1in 三、3.3
4 H5 控制方式 ESP32 C3 代码 ⭐推荐 ESP32 端,配合 H5 网页 https://pan.baidu.com/s/1HEGymdvOeyf_LJFmb-WZXg qh1i 三、3.5
5 完整版 ESP32 C3 代码 含所有模块(MQTT + MP3 + 舵机 + OTA) https://pan.baidu.com/s/1AcRYgk3Fzj8J-DgBbBVwbA 453w 四、扩展模块
6 完整版 Arduino UNO 代码 UNO 主控集成所有模块(履带 + 舵机 + MP3 + NFC) https://pan.baidu.com/s/1WTP7OwrzfyabWFWJCDkhqA w5f2 四、扩展模块
7 ASR PRO 语音模块代码 语音识别(天问 Block 工程) https://pan.baidu.com/s/1F9mbbMGzFKIO1RYE3TRoSA ucwb 四、4.1
8 Arduino_NFC 代码 UNO + RC522 单模块测试 https://pan.baidu.com/s/1FvI96uUmDy-ZOgpB4U8FaA wzwa 四、4.3
9 Arduino_MP3 代码 UNO + DFPlayer 单模块测试 https://pan.baidu.com/s/1Si77ZYmtONfURM49FJ6aQw dpfk 四、4.4
10 MP3 音乐文件包 配套音乐资源(拷贝到 TF 卡) https://pan.baidu.com/s/15qRZ5JTXNoIE5nzJVDV0Vw dmkf 四、4.4

A.2 在线服务(作者搭建,可直接使用)

服务 地址 端口/协议 说明
H5 控制页面 https://walle.werfamily.fun HTTPS 浏览器打开即可,记得替换 Topic 为自己的 B 站 UID
公共 MQTT Broker t.werfamily.fun 1883 (TCP) Topic 格式 bili/<你的B站UID>,防止冲突

A.3 官方文档与开发工具

类别 名称 链接
开发 IDE Arduino 官网 https://www.arduino.cc/
Arduino 中文社区 https://arduino.me/
开发板 合宙 ESP32 C3 文档 https://wiki.luatos.com/chips/esp32c3/index.html
控制端 Blinker 官网 https://www.diandeng.tech/home
Blinker GitHub https://github.com/blinker-iot
MQTT EMQX 官方文档 https://www.emqx.io/docs/zh/latest/deploy/install-docker.html
EMQX Vue3 示例 https://github.com/emqx/MQTT-Client-Examples/tree/master/mqtt-client-Vue3.js
模块 DFPlayer Mini 官方 Wiki https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299
板包索引 ESP8266 板包 http://arduino.me/packages/esp8266.json
ESP32 板包 https://arduino.me/packages/esp32.json
Blinker 离线安装 Blinker 库 0.3.10230510 https://www.diandeng.tech/sdk/blinker-library-0.3.10230510.zip

A.4 配套子项目

项目 位置 说明
太空人天气时钟简明教程 太空人天气时钟+简明教程/ 子文件夹 NodeMCU + 1.3” IPS 屏天气时钟(物料表第 17/18 项可选模块)
配套源代码(含 ESP32 全套 .ino) 代码/ 子文件夹 与本文档配套的完整 Arduino 工程目录

A.5 烧录注意事项速查

板型 Flash Mode USB CDC On Boot 进入下载模式
合宙 ESP32 C3 DIO Enabled 按住 Boot 键上电,两个板载 LED 微亮
烧录失败 按 Reset 重启或重新上电

烧录前请确保已安装如下 Arduino 库:BlinkerDHT sensor libraryAdafruit Unified SensorEspSoftwareSerialAdafruit_PWMServoDriverPubSubClientArduinoJsonMFRC522DFRobotDFPlayerMiniTFT_eSPI(按需)。


未完待续。视频教程还在陆续制作中,文档会同步更新,敬请期待!