-
主控:ESP8266(NodeMCU 一类)
-
舵机驱动:PCA9685(16 路)
-
I²C 连接:
- PCA9685 SDA → D1
- PCA9685 SCL → D2
-
舵机通道分配(你描述的映射,我按"前/后 + 左/右 + 髋/膝"来命名):
| PCA9685 通道 | 你的叫法 | 建议名字 |
|---|---|---|
| 0 | 左上 | 前左髋 LF_HIP |
| 1 | 右上 | 前右髋 RF_HIP |
| 2 | 左下 | 后左髋 LR_HIP |
| 3 | 右下 | 后右髋 RR_HIP |
| 4 | 左上前 | 前左膝 LF_KNEE |
| 5 | 右上前 | 前右膝 RF_KNEE |
| 6 | 左下前 | 后左膝 LR_KNEE |
| 7 | 右下前 | 后右膝 RR_KNEE |
说明:
- "髋 HIP" 控制左右摆动(往前迈腿 / 往后收腿)
- "膝 KNEE" 控制抬腿 / 落腿
程序功能说明
这个程序主要做三件事:
-
初始化所有 8 个舵机到"中立角度"(建议 90°)
-
串口手动调节:
- 在串口监视器里输入:
id angle(比如:0 80表示把通道 0 调到 80°) - 用于校准舵机安装角度(即把各关节调到你认为合适的"零位")
- 在串口监视器里输入:
-
简单动作演示:
f:向前走几步b:向后走几步l:原地左转r:原地右转h:回到站立姿态(全部回 90°)
你可以先用"手动模式"调好每个关节的"零位",把这些中立角度记下来,之后可写死成 offset[] 或 neutralAngle[]。
完整 Arduino 程序(ESP8266 + PCA9685)
需要的库:
Adafruit PWM Servo Driver Library(Adafruit_PWMServoDriver)Wire(ESP8266 Arduino 核心自带)
cpp
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
// PCA9685 I2C 地址,一般是 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
// 根据官方例程,给一个大致的范围(可以之后根据你自己的舵机微调)
#define SERVOMIN 150 // 0° 对应脉宽
#define SERVOMAX 600 // 180° 对应脉宽
// 便于以后修改频率
const int SERVO_FREQ = 50; // SG90 一般 50Hz
// 通道定义,方便阅读
enum {
CH_LF_HIP = 0, // 左前髋
CH_RF_HIP = 1, // 右前髋
CH_LR_HIP = 2, // 左后髋
CH_RR_HIP = 3, // 右后髋
CH_LF_KNEE = 4, // 左前膝
CH_RF_KNEE = 5, // 右前膝
CH_LR_KNEE = 6, // 左后膝
CH_RR_KNEE = 7 // 右后膝
};
// 每个舵机当前角度记录,方便手动调节和动作控制
float servoAngle[8] = {45,45,45,45,45,45,45,45};
// 是否反向(有些舵机安装方向相反,可以在这里调整)
bool servoReversed[8] = {
false, // 0 LF_HIP
false, // 1 RF_HIP
false, // 2 LR_HIP
false, // 3 RR_HIP
false, // 4 LF_KNEE
false, // 5 RF_KNEE
false, // 6 LR_KNEE
false // 7 RR_KNEE
};
// 校准偏移(单位:度),用来把物理安装误差调平
// 手动调试完成后,可以把最终的中立角写到这里
int servoOffset[8] = {0,0,0,0,0,0,0,0};
// --------- 工具函数:角度转脉宽 0-4095 ----------
uint16_t angleToPulse(float angle) {
// 限制在 0~180
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
// 从 [0,180] 映射到 [SERVOMIN,SERVOMAX]
float pulse = SERVOMIN + (SERVOMAX - SERVOMIN) * (angle / 180.0);
return (uint16_t)pulse;
}
// 设置单个舵机角度(带 offset 和 reverse)
void setServoAngle(uint8_t ch, float angle) {
if (ch > 15) return; // 安全检查
// 记录逻辑角度
servoAngle[ch] = angle;
// 先加上校准偏移
float realAngle = angle + servoOffset[ch];
// 是否反向
if (servoReversed[ch]) {
realAngle = 180.0 - realAngle;
}
uint16_t pulse = angleToPulse(realAngle);
pwm.setPWM(ch, 0, pulse);
}
// 批量设置多个舵机角度,带时间渐变(简单线性插值)
void moveServosSmooth(int duration_ms, float targetAngles[8]) {
const int steps = 20; // 越大越平滑,但越慢
int stepDelay = duration_ms / steps;
float startAngles[8];
for (int i = 0; i < 8; i++) {
startAngles[i] = servoAngle[i];
}
for (int s = 1; s <= steps; s++) {
float alpha = (float)s / steps;
for (int i = 0; i < 8; i++) {
float a = startAngles[i] + (targetAngles[i] - startAngles[i]) * alpha;
setServoAngle(i, a);
}
delay(stepDelay);
}
}
// 让全部舵机回到"站立姿态"(中立角度,暂用 90°)
void standPose() {
float neutral[8] = {90,90,90,90,90,90,90,90};
moveServosSmooth(500, neutral);
}
// 简单前进动作(四步一个周期)
void forwardStep() {
// 这里是非常简化的示范动作,你后面可以慢慢优化 gait
// 基础中立角
float base[8] = {90,90,90,90, 90,90,90,90};
// 抬起并前摆:左前 & 右后
float pose1[8] = {
70, // LF_HIP 前摆一点
90, // RF_HIP
90, // LR_HIP
110, // RR_HIP 后摆一点
60, // LF_KNEE 抬腿
90, // RF_KNEE
90, // LR_KNEE
60 // RR_KNEE 抬腿
};
moveServosSmooth(250, pose1);
// 落下脚
float pose2[8] = {
70,90,90,110,
90,90,90,90
};
moveServosSmooth(200, pose2);
// 另一对:右前 & 左后
float pose3[8] = {
90, 110, 70, 90,
90, 60, 60, 90
};
moveServosSmooth(250, pose3);
float pose4[8] = {
90,110,70,90,
90,90,90,90
};
moveServosSmooth(200, pose4);
// 回到中立(可选)
moveServosSmooth(200, base);
}
// 简单后退:反过来摆
void backwardStep() {
float base[8] = {90,90,90,90, 90,90,90,90};
float pose1[8] = {
110, // LF 后摆
90,
90,
70, // RR 前摆
60,90,90,60
};
moveServosSmooth(250, pose1);
float pose2[8] = {110,90,90,70, 90,90,90,90};
moveServosSmooth(200, pose2);
float pose3[8] = {
90,
70, // RF 后摆
110, // LR 前摆
90,
90,60,60,90
};
moveServosSmooth(250, pose3);
float pose4[8] = {90,70,110,90, 90,90,90,90};
moveServosSmooth(200, pose4);
moveServosSmooth(200, base);
}
// 原地左转(前腿向左摆,后腿向右摆)
void turnLeftStep() {
float base[8] = {90,90,90,90, 90,90,90,90};
float pose1[8] = {
70, 110, 110, 70, // 髋:前左往前,前右往后,后左往后,后右往前
70, 70, 70, 70 // 四条腿稍微抬一点
};
moveServosSmooth(300, pose1);
moveServosSmooth(300, base);
}
// 原地右转
void turnRightStep() {
float base[8] = {90,90,90,90, 90,90,90,90};
float pose1[8] = {
110, 70, 70, 110,
70, 70, 70, 70
};
moveServosSmooth(300, pose1);
moveServosSmooth(300, base);
}
// 串口帮助说明
void printHelp() {
Serial.println(F("==== 蜘蛛机器人控制帮助 ===="));
Serial.println(F("手动调整舵机角度:"));
Serial.println(F(" 在串口输入: id angle"));
Serial.println(F(" 例如: 0 90 (把通道0调到90度)"));
Serial.println(F(""));
Serial.println(F("动作指令:"));
Serial.println(F(" f : 向前走一步"));
Serial.println(F(" b : 向后退一步"));
Serial.println(F(" l : 左转一步"));
Serial.println(F(" r : 右转一步"));
Serial.println(F(" h : 回到站立姿态(全部90度)"));
Serial.println(F(" p : 打印当前 8 个通道的角度"));
Serial.println(F(" ? : 显示本帮助"));
Serial.println(F("============================"));
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println();
Serial.println(F("ESP8266 + PCA9685 蜘蛛机器人启动"));
// I2C 初始化(SDA, SCL)
Wire.begin(D1, D2); // 按你接线:SDA->D1, SCL->D2
pwm.begin();
pwm.setPWMFreq(SERVO_FREQ);
delay(10);
// 初始化:先把目前逻辑角度当作 45°,然后缓慢移动到 90°
for (int i = 0; i < 8; i++) {
setServoAngle(i, servoAngle[i]); // 先写入初始45°
}
delay(500);
standPose(); // 全部移动到 90°
printHelp();
}
void loop() {
// 处理单字符动作命令
if (Serial.available()) {
// 先看看是不是一行"id angle"
if (Serial.peek() >= '0' && Serial.peek() <= '9') {
String line = Serial.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
int spaceIndex = line.indexOf(' ');
if (spaceIndex > 0) {
int id = line.substring(0, spaceIndex).toInt();
int angle = line.substring(spaceIndex + 1).toInt();
if (id >= 0 && id < 8) {
Serial.print(F("设置通道 "));
Serial.print(id);
Serial.print(F(" 到 "));
Serial.print(angle);
Serial.println(F(" 度"));
setServoAngle(id, angle);
} else {
Serial.println(F("通道 id 必须在 0~7"));
}
}
}
} else {
char c = Serial.read();
switch (c) {
case 'f':
Serial.println(F("向前一步"));
forwardStep();
break;
case 'b':
Serial.println(F("向后一步"));
backwardStep();
break;
case 'l':
Serial.println(F("左转一步"));
turnLeftStep();
break;
case 'r':
Serial.println(F("右转一步"));
turnRightStep();
break;
case 'h':
Serial.println(F("回到站立姿态"));
standPose();
break;
case 'p':
Serial.println(F("当前舵机角度:"));
for (int i = 0; i < 8; i++) {
Serial.print(i);
Serial.print(F(": "));
Serial.print(servoAngle[i]);
Serial.println(F(" deg"));
}
break;
case '?':
printHelp();
break;
}
}
}
}
使用建议(实际调机步骤)
-
先保证接线正确
- PCA9685 供电要足够(舵机电源最好独立 5V,大电流,GND 与 ESP8266 共地)
- SDA→D1, SCL→D2;其它电源线按板子说明连接。
-
上电 / 烧录后:
- 打开串口监视器(115200 波特率,换行
\n) - 机器人会自动把 8 个舵机缓慢移动到 90°(站立姿态)
- 打开串口监视器(115200 波特率,换行
-
手动校准:
- 例如要调通道 0 到 80°:在串口输入:
0 80回车 - 慢慢调每个关节,让机器人站得"端正"
- 把每个通道的最终"中立值"记下来,填进代码里的
servoOffset[]或者改成neutral[]。
- 例如要调通道 0 到 80°:在串口输入:
-
测试动作:
- 串口输入:
f→ 前进一步 - 输入:
b→ 后退 - 输入:
l/r→ 左转 / 右转 - 随时输入
h让它回到站立姿态
- 串口输入:
四足机器人 - ESP8266 + PCA9685 + 8舵机(带 trim 持久化)
SDA -> D2 (GPIO4), SCL -> D1 (GPIO5)
PCA9685 通道分配:
0: 左上(左前腿 - 髋/肩)
1: 右上(右前腿 - 鋒/肩)
2: 左下(左后腿 - 髋/肩)
3: 右下(右后腿 - 髋/肩)
4: 左上前(左前腿 - 膝/抬腿)
5: 右上前(右前腿 - 膝/抬腿)
6: 左下前(左后腿 - 膝/抬腿)
7: 右下前(右后腿 - 膝/抬腿)
from machine import Pin, I2C
import time
=============== PCA9685 驱动类 ===================
class PCA9685:
_MODE1 = 0x00
_PRESCALE = 0xFE
_LED0_ON_L = 0x06
def __init__(self, i2c, addr=0x40):
self.i2c = i2c
self.addr = addr
self.reset()
def reset(self):
# 复位寄存器
self.i2c.writeto_mem(self.addr, self._MODE1, b'\x00')
time.sleep_ms(5)
def set_pwm_freq(self, freq):
# 设置 PWM 频率(舵机一般用 50Hz)
prescaleval = int(25000000.0 / (4096 * freq) - 1)
oldmode = self.i2c.readfrom_mem(self.addr, self._MODE1, 1)[0]
newmode = (oldmode & 0x7F) | 0x10 # sleep
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([newmode]))
self.i2c.writeto_mem(self.addr, self._PRESCALE, bytes([prescaleval]))
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([oldmode]))
time.sleep_ms(5)
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([oldmode | 0xA1])) # 重启 + 自动递增
def set_pwm(self, channel, on, off):
# 设置指定通道的 on/off 计数(0~4095)
data = bytearray(4)
data[0] = on & 0xFF
data[1] = (on >> 8) & 0xFF
data[2] = off & 0xFF
data[3] = (off >> 8) & 0xFF
self.i2c.writeto_mem(self.addr, self._LED0_ON_L + 4 * channel, data)
=============== I2C 初始化(注意:D2=SDA, D1=SCL) ===================
実测结果:SDA = GPIO4(D2), SCL = GPIO5(D1)
try:
i2c = I2C(scl=Pin(5), sda=Pin(4)) # D1 = SCL, D2 = SDA
except TypeError:
i2c = I2C(-1, scl=Pin(5), sda=Pin(4))
pca = PCA9685(i2c, addr=0x40) # PCA9685 地址 0x40
pca.set_pwm_freq(50) # 舵机 50Hz
=============== 舵机控制封装 & trim 持久化 ===================
SERVO_MIN_US = 500 # 对应 0°
SERVO_MAX_US = 2500 # 对应 180°
PWM_PERIOD_US = 20000 # 50Hz -> 20ms
NEUTRAL_ANGLE = 90 # 理论中位
当前 8 个舵机"物理角度"记录(0~180,用于显示)
servo_angles = [NEUTRAL_ANGLE] * 8
舵机方向(某些安装反向,可设为 -1)
servo_dir = [1] * 8
每个舵机的 trim(相对 90° 的偏移,-90~+90),会保存到文件
TRIM_FILE = "servo_trim.txt"
servo_trim = [0] * 8 # 启动默认 0,之后从文件加载
def load_trims():
global servo_trim
try:
with open(TRIM_FILE) as f:
s = f.read().strip()
if not s:
print("trim 文件为空,使用默认 0 偏移。")
return
parts = s.split(',')
if len(parts) != 8:
print("trim 文件格式不正确,使用默认 0 偏移。")
return
vals = []
for p in parts:
v = int§
if v < -90: v = -90
if v > 90: v = 90
vals.append(v)
servo_trim = vals
print("已加载 trim 值:", servo_trim)
except Exception as e:
print("未找到 trim 文件,使用默认 0 偏移。")
def save_trims():
try:
with open(TRIM_FILE, "w") as f:
text = ",".join(str(int(max(-90, min(90, t)))) for t in servo_trim)
f.write(text)
print("trim 值已保存:", servo_trim)
except Exception as e:
print("保存 trim 失败:", e)
load_trims()
def angle_to_ticks(angle):
角度(0~180) -> PCA9685 的 0~4095 计数值
angle = max(0, min(180, angle))
pulse_us = SERVO_MIN_US + (SERVO_MAX_US - SERVO_MIN_US) * angle / 180.0
ticks = int(pulse_us * 4096 / PWM_PERIOD_US)
if ticks < 0: ticks = 0
if ticks > 4095: ticks = 4095
return ticks
def set_servo(channel, angle):
"""设置单个舵机物理角度(0~180°)"""
if servo_dir[channel] == -1:
angle = 180 - angle
ticks = angle_to_ticks(angle)
pca.set_pwm(channel, 0, ticks)
servo_angles[channel] = angle
def channel_neutral_angle(ch):
"""返回某通道的"中立物理角度" = 90° + trim,并限制在 0~180"""
base = NEUTRAL_ANGLE + servo_trim[ch]
if base < 0: base = 0
if base > 180: base = 180
return base
def all_to_neutral():
"""所有舵机转到各自的中立角度(含 trim)"""
for ch in range(8):
set_servo(ch, channel_neutral_angle(ch))
time.sleep_ms(20)
def all_to_angle(angle):
"""所有舵机都转到同一个物理角度(通常用不到了)"""
for ch in range(8):
set_servo(ch, angle)
time.sleep_ms(20)
=============== 机器人腿的语义映射 ===================
CH_LU = 0 # 左上(左前腿,髋/肩)
CH_RU = 1 # 右上
CH_LD = 2 # 左下(左后腿)
CH_RD = 3 # 右下
CH_LU_F = 4 # 左上前(左前腿膝/抬腿)
CH_RU_F = 5 # 右上前
CH_LD_F = 6 # 左下前
CH_RD_F = 7 # 右下前
def leg_neutral_angles(leg):
"""返回某条腿的 (hip_neutral, knee_neutral) 物理角度"""
if leg == 'LF':
return channel_neutral_angle(CH_LU), channel_neutral_angle(CH_LU_F)
if leg == 'RF':
return channel_neutral_angle(CH_RU), channel_neutral_angle(CH_RU_F)
if leg == 'LB':
return channel_neutral_angle(CH_LD), channel_neutral_angle(CH_LD_F)
if leg == 'RB':
return channel_neutral_angle(CH_RD), channel_neutral_angle(CH_RD_F)
return NEUTRAL_ANGLE, NEUTRAL_ANGLE
def leg_set(leg, hip_angle, knee_angle):
"""
leg: 'LF','RF','LB','RB'(左前、右前、左后、右后)
hip_angle / knee_angle:物理角度(0~180)
"""
if leg == 'LF':
set_servo(CH_LU, hip_angle)
set_servo(CH_LU_F, knee_angle)
elif leg == 'RF':
set_servo(CH_RU, hip_angle)
set_servo(CH_RU_F, knee_angle)
elif leg == 'LB':
set_servo(CH_LD, hip_angle)
set_servo(CH_LD_F, knee_angle)
elif leg == 'RB':
set_servo(CH_RD, hip_angle)
set_servo(CH_RD_F, knee_angle)
def neutral_pose():
"""站立中立姿态:所有舵机 = 90° + 各自 trim"""
all_to_neutral()
=============== 基本步态参数 ===================
HIP_SWING = 25 # 髋关节左右摆动幅度
KNEE_LIFT = 25 # 膝关节抬腿幅度
STEP_DELAY = 0.20 # 每小动作间的延时(秒)
=============== 步态动作(基于每个关节的 trim 后中位) ===================
def step_forward_once():
"""前进一步(简化 trot 步态)"""
# 先取各腿的中立角度
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 阶段 1:抬起 左前(LF) + 右后(RB)
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 阶段 2:向前迈 LF,向后蹬 RB
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 阶段 3:放下 LF、RB
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# 阶段 4:右前(RF) + 左后(LB) 调整
leg_set('RF', RF_hip0 - HIP_SWING, RF_knee0)
leg_set('LB', LB_hip0 + HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def step_backward_once():
"""后退一步"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 抬起 左前 + 右后
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 向后迈 LF,向前蹬 RB
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 放下
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# RF + LB 调整
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def turn_left_once():
"""原地左转一下(右侧腿跨大步)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 抬起右侧两条腿(RF + RB)
leg_set('RF', RF_hip0, RF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 右侧向前迈/向后蹬
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 落地
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def turn_right_once():
"""原地右转一下(左侧腿跨大步)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 抬起左侧两条腿(LF + LB)
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
leg_set('LB', LB_hip0, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 左侧向前迈/向后蹬
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0 - KNEE_LIFT)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 落地
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
=============== 手动调节菜单(设置 trim) ===================
def print_servo_map():
print("舵机通道对应:")
print(" 0: 左上 (LF 髋)")
print(" 1: 右上 (RF 髋)")
print(" 2: 左下 (LB 髋)")
print(" 3: 右下 (RB 髋)")
print(" 4: 左上前 (LF 膝)")
print(" 5: 右上前 (RF 膝)")
print(" 6: 左下前 (LB 膝)")
print(" 7: 右下前 (RB 膝)")
print()
def manual_adjust():
"""
手动调节某个舵机的 trim:
-
输入的是相对 90° 的偏移 (-90~+90)
-
会保存到文件,下次上电自动生效
"""
global servo_trim
print_servo_map()
while True:
try:
s = input("输入舵机通道(0-7),或 q 返回主菜单: ").strip()
except EOFError:
return
if s.lower() == 'q':
return
if not s.isdigit():
print("请输入 0-7 或 q")
continue
ch = int(s)
if ch < 0 or ch > 7:
print("通道范围 0-7")
continue
curr_trim = servo_trim[ch]
curr_neutral = channel_neutral_angle(ch)
print("当前通道 {}: trim = {:+d}°,中立物理角度 ≈ {:.1f}°".format(ch, curr_trim, curr_neutral))
off_s = input("输入新的 trim (-90 ~ +90,相对于 90°): ").strip()
try:
off = int(off_s)
except:
print("trim 必须是数字")
continue
if off < -90 or off > 90:
print("trim 范围 -90 ~ +90")
continue
servo_trim[ch] = off
save_trims()
new_neutral = channel_neutral_angle(ch)
set_servo(ch, new_neutral)
print("通道 {} 新 trim = {:+d}°,中立角度 ≈ {:.1f}°".format(ch, off, new_neutral))
time.sleep(0.1)
=============== 主循环菜单 ===================
def main():
print("初始化:按 trim 让所有舵机转到中立位置...")
neutral_pose()
time.sleep(1)
print("初始化完成。")
while True:
print("\n===== 蜘蛛机器人 控制菜单 =====")
print(" m : 手动调节舵机 trim(-90~+90,永久保存)")
print(" w : 前进一步")
print(" s : 后退一步")
print(" a : 左转一下")
print(" d : 右转一下")
print(" n : 回到中立站立姿态 (90° + trim)")
print(" q : 退出(停止程序)")
try:
cmd = input("请输入命令: ").strip().lower()
except EOFError:
break
if cmd == 'm':
manual_adjust()
elif cmd == 'w':
step_forward_once()
elif cmd == 's':
step_backward_once()
elif cmd == 'a':
turn_left_once()
elif cmd == 'd':
turn_right_once()
elif cmd == 'n':
neutral_pose()
elif cmd == 'q':
print("退出主循环。")
break
else:
print("未知命令。")
直接运行 main
if name == 'main ':
main()
bash
# 四足机器人 - ESP8266 + PCA9685 + 8舵机(带 trim 持久化)
# SDA -> D2 (GPIO4), SCL -> D1 (GPIO5)
# PCA9685 通道分配:
# 0: 左上(左前腿 - 髋/肩)
# 1: 右上(右前腿 - 鋒/肩)
# 2: 左下(左后腿 - 髋/肩)
# 3: 右下(右后腿 - 髋/肩)
# 4: 左上前(左前腿 - 膝/抬腿)
# 5: 右上前(右前腿 - 膝/抬腿)
# 6: 左下前(左后腿 - 膝/抬腿)
# 7: 右下前(右后腿 - 膝/抬腿)
from machine import Pin, I2C
import time
# =============== PCA9685 驱动类 ===================
class PCA9685:
_MODE1 = 0x00
_PRESCALE = 0xFE
_LED0_ON_L = 0x06
def __init__(self, i2c, addr=0x40):
self.i2c = i2c
self.addr = addr
self.reset()
def reset(self):
# 复位寄存器
self.i2c.writeto_mem(self.addr, self._MODE1, b'\x00')
time.sleep_ms(5)
def set_pwm_freq(self, freq):
# 设置 PWM 频率(舵机一般用 50Hz)
prescaleval = int(25000000.0 / (4096 * freq) - 1)
oldmode = self.i2c.readfrom_mem(self.addr, self._MODE1, 1)[0]
newmode = (oldmode & 0x7F) | 0x10 # sleep
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([newmode]))
self.i2c.writeto_mem(self.addr, self._PRESCALE, bytes([prescaleval]))
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([oldmode]))
time.sleep_ms(5)
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([oldmode | 0xA1])) # 重启 + 自动递增
def set_pwm(self, channel, on, off):
# 设置指定通道的 on/off 计数(0~4095)
data = bytearray(4)
data[0] = on & 0xFF
data[1] = (on >> 8) & 0xFF
data[2] = off & 0xFF
data[3] = (off >> 8) & 0xFF
self.i2c.writeto_mem(self.addr, self._LED0_ON_L + 4 * channel, data)
# =============== I2C 初始化(注意:D2=SDA, D1=SCL) ===================
# 実测结果:SDA = GPIO4(D2), SCL = GPIO5(D1)
try:
i2c = I2C(scl=Pin(5), sda=Pin(4)) # D1 = SCL, D2 = SDA
except TypeError:
i2c = I2C(-1, scl=Pin(5), sda=Pin(4))
pca = PCA9685(i2c, addr=0x40) # PCA9685 地址 0x40
pca.set_pwm_freq(50) # 舵机 50Hz
# =============== 舵机控制封装 & trim 持久化 ===================
SERVO_MIN_US = 500 # 对应 0°
SERVO_MAX_US = 2500 # 对应 180°
PWM_PERIOD_US = 20000 # 50Hz -> 20ms
NEUTRAL_ANGLE = 90 # 理论中位
# 当前 8 个舵机"物理角度"记录(0~180,用于显示)
servo_angles = [NEUTRAL_ANGLE] * 8
# 舵机方向(某些安装反向,可设为 -1)
servo_dir = [1] * 8
# 每个舵机的 trim(相对 90° 的偏移,-90~+90),会保存到文件
TRIM_FILE = "servo_trim.txt"
servo_trim = [0] * 8 # 启动默认 0,之后从文件加载
def load_trims():
global servo_trim
try:
with open(TRIM_FILE) as f:
s = f.read().strip()
if not s:
print("trim 文件为空,使用默认 0 偏移。")
return
parts = s.split(',')
if len(parts) != 8:
print("trim 文件格式不正确,使用默认 0 偏移。")
return
vals = []
for p in parts:
v = int(p)
if v < -90: v = -90
if v > 90: v = 90
vals.append(v)
servo_trim = vals
print("已加载 trim 值:", servo_trim)
except Exception as e:
print("未找到 trim 文件,使用默认 0 偏移。")
def save_trims():
try:
with open(TRIM_FILE, "w") as f:
text = ",".join(str(int(max(-90, min(90, t)))) for t in servo_trim)
f.write(text)
print("trim 值已保存:", servo_trim)
except Exception as e:
print("保存 trim 失败:", e)
load_trims()
def angle_to_ticks(angle):
# 角度(0~180) -> PCA9685 的 0~4095 计数值
angle = max(0, min(180, angle))
pulse_us = SERVO_MIN_US + (SERVO_MAX_US - SERVO_MIN_US) * angle / 180.0
ticks = int(pulse_us * 4096 / PWM_PERIOD_US)
if ticks < 0: ticks = 0
if ticks > 4095: ticks = 4095
return ticks
def set_servo(channel, angle):
"""设置单个舵机物理角度(0~180°)"""
if servo_dir[channel] == -1:
angle = 180 - angle
ticks = angle_to_ticks(angle)
pca.set_pwm(channel, 0, ticks)
servo_angles[channel] = angle
def channel_neutral_angle(ch):
"""返回某通道的"中立物理角度" = 90° + trim,并限制在 0~180"""
base = NEUTRAL_ANGLE + servo_trim[ch]
if base < 0: base = 0
if base > 180: base = 180
return base
def all_to_neutral():
"""所有舵机转到各自的中立角度(含 trim)"""
for ch in range(8):
set_servo(ch, channel_neutral_angle(ch))
time.sleep_ms(20)
def all_to_angle(angle):
"""所有舵机都转到同一个物理角度(通常用不到了)"""
for ch in range(8):
set_servo(ch, angle)
time.sleep_ms(20)
# =============== 机器人腿的语义映射 ===================
CH_LU = 0 # 左上(左前腿,髋/肩)
CH_RU = 1 # 右上
CH_LD = 2 # 左下(左后腿)
CH_RD = 3 # 右下
CH_LU_F = 4 # 左上前(左前腿膝/抬腿)
CH_RU_F = 5 # 右上前
CH_LD_F = 6 # 左下前
CH_RD_F = 7 # 右下前
def leg_neutral_angles(leg):
"""返回某条腿的 (hip_neutral, knee_neutral) 物理角度"""
if leg == 'LF':
return channel_neutral_angle(CH_LU), channel_neutral_angle(CH_LU_F)
if leg == 'RF':
return channel_neutral_angle(CH_RU), channel_neutral_angle(CH_RU_F)
if leg == 'LB':
return channel_neutral_angle(CH_LD), channel_neutral_angle(CH_LD_F)
if leg == 'RB':
return channel_neutral_angle(CH_RD), channel_neutral_angle(CH_RD_F)
return NEUTRAL_ANGLE, NEUTRAL_ANGLE
def leg_set(leg, hip_angle, knee_angle):
"""
leg: 'LF','RF','LB','RB'(左前、右前、左后、右后)
hip_angle / knee_angle:物理角度(0~180)
"""
if leg == 'LF':
set_servo(CH_LU, hip_angle)
set_servo(CH_LU_F, knee_angle)
elif leg == 'RF':
set_servo(CH_RU, hip_angle)
set_servo(CH_RU_F, knee_angle)
elif leg == 'LB':
set_servo(CH_LD, hip_angle)
set_servo(CH_LD_F, knee_angle)
elif leg == 'RB':
set_servo(CH_RD, hip_angle)
set_servo(CH_RD_F, knee_angle)
def neutral_pose():
"""站立中立姿态:所有舵机 = 90° + 各自 trim"""
all_to_neutral()
# =============== 基本步态参数 ===================
HIP_SWING = 25 # 髋关节左右摆动幅度
KNEE_LIFT = 25 # 膝关节抬腿幅度
STEP_DELAY = 0.20 # 每小动作间的延时(秒)
# =============== 步态动作(基于每个关节的 trim 后中位) ===================
def step_forward_once():
"""前进一步(简化 trot 步态)"""
# 先取各腿的中立角度
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 阶段 1:抬起 左前(LF) + 右后(RB)
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 阶段 2:向前迈 LF,向后蹬 RB
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 阶段 3:放下 LF、RB
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# 阶段 4:右前(RF) + 左后(LB) 调整
leg_set('RF', RF_hip0 - HIP_SWING, RF_knee0)
leg_set('LB', LB_hip0 + HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def step_backward_once():
"""后退一步"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 抬起 左前 + 右后
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 向后迈 LF,向前蹬 RB
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 放下
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# RF + LB 调整
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def turn_left_once():
"""原地左转一下(右侧腿跨大步)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 抬起右侧两条腿(RF + RB)
leg_set('RF', RF_hip0, RF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 右侧向前迈/向后蹬
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0 - KNEE_LIFT)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 落地
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def turn_right_once():
"""原地右转一下(左侧腿跨大步)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 抬起左侧两条腿(LF + LB)
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
leg_set('LB', LB_hip0, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 左侧向前迈/向后蹬
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0 - KNEE_LIFT)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
# 落地
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
# =============== 手动调节菜单(设置 trim) ===================
def print_servo_map():
print("舵机通道对应:")
print(" 0: 左上 (LF 髋)")
print(" 1: 右上 (RF 髋)")
print(" 2: 左下 (LB 髋)")
print(" 3: 右下 (RB 髋)")
print(" 4: 左上前 (LF 膝)")
print(" 5: 右上前 (RF 膝)")
print(" 6: 左下前 (LB 膝)")
print(" 7: 右下前 (RB 膝)")
print()
def manual_adjust():
"""
手动调节某个舵机的 trim:
- 输入的是相对 90° 的偏移 (-90~+90)
- 会保存到文件,下次上电自动生效
"""
global servo_trim
print_servo_map()
while True:
try:
s = input("输入舵机通道(0-7),或 q 返回主菜单: ").strip()
except EOFError:
return
if s.lower() == 'q':
return
if not s.isdigit():
print("请输入 0-7 或 q")
continue
ch = int(s)
if ch < 0 or ch > 7:
print("通道范围 0-7")
continue
curr_trim = servo_trim[ch]
curr_neutral = channel_neutral_angle(ch)
print("当前通道 {}: trim = {:+d}°,中立物理角度 ≈ {:.1f}°".format(ch, curr_trim, curr_neutral))
off_s = input("输入新的 trim (-90 ~ +90,相对于 90°): ").strip()
try:
off = int(off_s)
except:
print("trim 必须是数字")
continue
if off < -90 or off > 90:
print("trim 范围 -90 ~ +90")
continue
servo_trim[ch] = off
save_trims()
new_neutral = channel_neutral_angle(ch)
set_servo(ch, new_neutral)
print("通道 {} 新 trim = {:+d}°,中立角度 ≈ {:.1f}°".format(ch, off, new_neutral))
time.sleep(0.1)
# =============== 主循环菜单 ===================
def main():
print("初始化:按 trim 让所有舵机转到中立位置...")
neutral_pose()
time.sleep(1)
print("初始化完成。")
while True:
print("\n===== 蜘蛛机器人 控制菜单 =====")
print(" m : 手动调节舵机 trim(-90~+90,永久保存)")
print(" w : 前进一步")
print(" s : 后退一步")
print(" a : 左转一下")
print(" d : 右转一下")
print(" n : 回到中立站立姿态 (90° + trim)")
print(" q : 退出(停止程序)")
try:
cmd = input("请输入命令: ").strip().lower()
except EOFError:
break
if cmd == 'm':
manual_adjust()
elif cmd == 'w':
step_forward_once()
elif cmd == 's':
step_backward_once()
elif cmd == 'a':
turn_left_once()
elif cmd == 'd':
turn_right_once()
elif cmd == 'n':
neutral_pose()
elif cmd == 'q':
print("退出主循环。")
break
else:
print("未知命令。")
# 直接运行 main
if __name__ == '__main__':
main()
bash
# 四足机器人 - ESP8266 + PCA9685 + 8舵机(带 trim 持久化 + 网页控制)
# WiFi: SSID = jerry, 密码 = ldk123456
# SDA -> D2 (GPIO4), SCL -> D1 (GPIO5)
#
# PCA9685 通道分配:
# 0: 左上(左前腿 - 髋/肩)
# 1: 右上(右前腿 - 髋/肩)
# 2: 左下(左后腿 - 髋/肩)
# 3: 右下(右后腿 - 髋/肩)
# 4: 左上前(左前腿 - 膝/抬腿)
# 5: 右上前(右前腿 - 膝/抬腿)
# 6: 左下前(左后腿 - 膝/抬腿)
# 7: 右下前(右后腿 - 膝/抬腿)
from machine import Pin, I2C
import time
import network
import socket
# =============== PCA9685 驱动类 ===================
class PCA9685:
_MODE1 = 0x00
_PRESCALE = 0xFE
_LED0_ON_L = 0x06
def __init__(self, i2c, addr=0x40):
self.i2c = i2c
self.addr = addr
self.reset()
def reset(self):
# 复位寄存器
self.i2c.writeto_mem(self.addr, self._MODE1, b'\x00')
time.sleep_ms(5)
def set_pwm_freq(self, freq):
# 设置 PWM 频率(舵机一般用 50Hz)
prescaleval = int(25000000.0 / (4096 * freq) - 1)
oldmode = self.i2c.readfrom_mem(self.addr, self._MODE1, 1)[0]
newmode = (oldmode & 0x7F) | 0x10 # sleep
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([newmode]))
self.i2c.writeto_mem(self.addr, self._PRESCALE, bytes([prescaleval]))
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([oldmode]))
time.sleep_ms(5)
self.i2c.writeto_mem(self.addr, self._MODE1, bytes([oldmode | 0xA1])) # 重启 + 自动递增
def set_pwm(self, channel, on, off):
# 设置指定通道的 on/off 计数(0~4095)
data = bytearray(4)
data[0] = on & 0xFF
data[1] = (on >> 8) & 0xFF
data[2] = off & 0xFF
data[3] = (off >> 8) & 0xFF
self.i2c.writeto_mem(self.addr, self._LED0_ON_L + 4 * channel, data)
# =============== I2C 初始化(D2=SDA, D1=SCL) ===================
try:
i2c = I2C(scl=Pin(5), sda=Pin(4)) # D1 = SCL, D2 = SDA
except TypeError:
i2c = I2C(-1, scl=Pin(5), sda=Pin(4))
pca = PCA9685(i2c, addr=0x40) # PCA9685 地址 0x40
pca.set_pwm_freq(50) # 舵机 50Hz
# =============== 舵机控制 & trim 持久化 ===================
SERVO_MIN_US = 500 # 对应 0°
SERVO_MAX_US = 2500 # 对应 180°
PWM_PERIOD_US = 20000 # 50Hz -> 20ms
NEUTRAL_ANGLE = 90 # 理论中位
# 当前 8 个舵机"物理角度"记录(0~180,用于显示)
servo_angles = [NEUTRAL_ANGLE] * 8
# 舵机方向(某些安装反向,可设为 -1)
servo_dir = [1] * 8
# 每个舵机的 trim(相对 90° 的偏移,-90~+90),会保存到文件
TRIM_FILE = "servo_trim.txt"
servo_trim = [0] * 8 # 默认 0,之后从文件加载
def load_trims():
global servo_trim
try:
with open(TRIM_FILE) as f:
s = f.read().strip()
if not s:
print("trim 文件为空,使用默认 0 偏移。")
return
parts = s.split(',')
if len(parts) != 8:
print("trim 文件格式不正确,使用默认 0 偏移。")
return
vals = []
for p in parts:
v = int(p)
if v < -90: v = -90
if v > 90: v = 90
vals.append(v)
servo_trim = vals
print("已加载 trim 值:", servo_trim)
except Exception as e:
print("未找到 trim 文件,使用默认 0 偏移。")
def save_trims():
try:
with open(TRIM_FILE, "w") as f:
text = ",".join(str(int(max(-90, min(90, t)))) for t in servo_trim)
f.write(text)
print("trim 值已保存:", servo_trim)
except Exception as e:
print("保存 trim 失败:", e)
load_trims()
def angle_to_ticks(angle):
# 角度(0~180) -> PCA9685 的 0~4095 计数值
angle = max(0, min(180, angle))
pulse_us = SERVO_MIN_US + (SERVO_MAX_US - SERVO_MIN_US) * angle / 180.0
ticks = int(pulse_us * 4096 / PWM_PERIOD_US)
if ticks < 0: ticks = 0
if ticks > 4095: ticks = 4095
return ticks
def set_servo(channel, angle):
"""设置单个舵机物理角度(0~180°)"""
if servo_dir[channel] == -1:
angle = 180 - angle
ticks = angle_to_ticks(angle)
pca.set_pwm(channel, 0, ticks)
servo_angles[channel] = angle
def channel_neutral_angle(ch):
"""返回某通道的"中立物理角度" = 90° + trim,并限制在 0~180"""
base = NEUTRAL_ANGLE + servo_trim[ch]
if base < 0: base = 0
if base > 180: base = 180
return base
def all_to_neutral():
"""所有舵机转到各自的中立角度(含 trim)"""
for ch in range(8):
set_servo(ch, channel_neutral_angle(ch))
time.sleep_ms(20)
def all_to_angle(angle):
"""所有舵机都转到同一个物理角度"""
for ch in range(8):
set_servo(ch, angle)
time.sleep_ms(20)
# =============== 机器人腿的语义映射 ===================
CH_LU = 0 # 左上(左前腿,髋/肩)
CH_RU = 1 # 右上
CH_LD = 2 # 左下(左后腿)
CH_RD = 3 # 右下
CH_LU_F = 4 # 左上前(左前腿膝/抬腿)
CH_RU_F = 5 # 右上前
CH_LD_F = 6 # 左下前(左后腿膝)
CH_RD_F = 7 # 右下前(右后腿膝)
def leg_neutral_angles(leg):
"""返回某条腿的 (hip_neutral, knee_neutral) 物理角度"""
if leg == 'LF':
return channel_neutral_angle(CH_LU), channel_neutral_angle(CH_LU_F)
if leg == 'RF':
return channel_neutral_angle(CH_RU), channel_neutral_angle(CH_RU_F)
if leg == 'LB':
return channel_neutral_angle(CH_LD), channel_neutral_angle(CH_LD_F)
if leg == 'RB':
return channel_neutral_angle(CH_RD), channel_neutral_angle(CH_RD_F)
return NEUTRAL_ANGLE, NEUTRAL_ANGLE
def leg_set(leg, hip_angle, knee_angle):
"""
leg: 'LF','RF','LB','RB'(左前、右前、左后、右后)
hip_angle / knee_angle:物理角度(0~180)
"""
if leg == 'LF':
set_servo(CH_LU, hip_angle)
set_servo(CH_LU_F, knee_angle)
elif leg == 'RF':
set_servo(CH_RU, hip_angle)
set_servo(CH_RU_F, knee_angle)
elif leg == 'LB':
set_servo(CH_LD, hip_angle)
set_servo(CH_LD_F, knee_angle)
elif leg == 'RB':
set_servo(CH_RD, hip_angle)
set_servo(CH_RD_F, knee_angle)
def neutral_pose():
"""站立中立姿态:所有舵机 = 90° + 各自 trim"""
all_to_neutral()
# =============== 步态参数(更适合仿真慢步) ===================
HIP_SWING = 20 # 髋关节摆动幅度,适当减小
KNEE_LIFT = 18 # 膝关节抬腿幅度
STEP_DELAY = 0.15 # 每小动作间的延时
# =============== 爬行步态(一次只动一条腿) ===================
def step_forward_once():
"""
爬行步态前进:
顺序:左前 -> 右后 -> 右前 -> 左后
始终保持 3 条腿着地,更稳定,更像慢速四足。
"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 定义"向前"的 hip 偏移策略:
# 左腿:+HIP_SWING 为向前;右腿:-HIP_SWING 为向前
# 这个方向如与实际相反,可以把对应腿的 servo_dir 改成 -1 调整
# 1. 左前 (LF) 抬腿 -> 向前迈 -> 落地
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT) # 抬腿
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0 - KNEE_LIFT) # 向前
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0) # 落地
time.sleep(STEP_DELAY)
# 2. 右后 (RB)
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# 3. 右前 (RF)
leg_set('RF', RF_hip0, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 - HIP_SWING, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 - HIP_SWING, RF_knee0)
time.sleep(STEP_DELAY)
# 4. 左后 (LB)
leg_set('LB', LB_hip0, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 + HIP_SWING, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 + HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
# 收回到中立(可注释掉,看"连续姿态"效果)
neutral_pose()
time.sleep(STEP_DELAY)
def step_backward_once():
"""爬行步态后退(前进反向)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 左前 (LF):向后 = -HIP_SWING
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0)
time.sleep(STEP_DELAY)
# 右后 (RB):向后 = +HIP_SWING
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# 右前 (RF):向后 = +HIP_SWING
leg_set('RF', RF_hip0, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0)
time.sleep(STEP_DELAY)
# 左后 (LB):向后 = -HIP_SWING
leg_set('LB', LB_hip0, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def turn_left_once():
"""原地左转(右侧腿跨大步,左侧补偿)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 思路:右侧腿向前,左侧腿向后,形成左转
# 1. 右前 (RF)
leg_set('RF', RF_hip0, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 - HIP_SWING, RF_knee0 - KNEE_LIFT) # 右前向前(假设 - 为前)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 - HIP_SWING, RF_knee0)
time.sleep(STEP_DELAY)
# 2. 左后 (LB) 向后
leg_set('LB', LB_hip0, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0 - KNEE_LIFT) # 向后
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 - HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
# 3. 右后 (RB) 向前
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 - HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# 4. 左前 (LF) 向后
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 - HIP_SWING, LF_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
def turn_right_once():
"""原地右转(左侧腿跨大步,右侧补偿)"""
LF_hip0, LF_knee0 = leg_neutral_angles('LF')
RF_hip0, RF_knee0 = leg_neutral_angles('RF')
LB_hip0, LB_knee0 = leg_neutral_angles('LB')
RB_hip0, RB_knee0 = leg_neutral_angles('RB')
# 1. 左前 (LF) 向前
leg_set('LF', LF_hip0, LF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LF', LF_hip0 + HIP_SWING, LF_knee0)
time.sleep(STEP_DELAY)
# 2. 右后 (RB) 向后
leg_set('RB', RB_hip0, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RB', RB_hip0 + HIP_SWING, RB_knee0)
time.sleep(STEP_DELAY)
# 3. 左后 (LB) 向前
leg_set('LB', LB_hip0, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 + HIP_SWING, LB_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('LB', LB_hip0 + HIP_SWING, LB_knee0)
time.sleep(STEP_DELAY)
# 4. 右前 (RF) 向后
leg_set('RF', RF_hip0, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0 - KNEE_LIFT)
time.sleep(STEP_DELAY)
leg_set('RF', RF_hip0 + HIP_SWING, RF_knee0)
time.sleep(STEP_DELAY)
neutral_pose()
time.sleep(STEP_DELAY)
# =============== 手动 trim 调节(串口用) ===================
def print_servo_map():
print("舵机通道对应:")
print(" 0: 左上 (LF 髋)")
print(" 1: 右上 (RF 髋)")
print(" 2: 左下 (LB 髋)")
print(" 3: 右下 (RB 髋)")
print(" 4: 左上前 (LF 膝)")
print(" 5: 右上前 (RF 膝)")
print(" 6: 左下前 (LB 膝)")
print(" 7: 右下前 (RB 膝)")
print()
def manual_adjust():
"""
串口下手动调节某个舵机的 trim:
- 输入的是相对 90° 的偏移 (-90~+90)
- 会保存到文件,下次上电自动生效
"""
global servo_trim
print_servo_map()
while True:
try:
s = input("输入舵机通道(0-7),或 q 返回主菜单: ").strip()
except EOFError:
return
if s.lower() == 'q':
return
if not s.isdigit():
print("请输入 0-7 或 q")
continue
ch = int(s)
if ch < 0 or ch > 7:
print("通道范围 0-7")
continue
curr_trim = servo_trim[ch]
curr_neutral = channel_neutral_angle(ch)
print("当前通道 {}: trim = {:+d}°,中立物理角度 ≈ {:.1f}°".format(ch, curr_trim, curr_neutral))
off_s = input("输入新的 trim (-90 ~ +90,相对于 90°): ").strip()
try:
off = int(off_s)
except:
print("trim 必须是数字")
continue
if off < -90 or off > 90:
print("trim 范围 -90 ~ +90")
continue
servo_trim[ch] = off
save_trims()
new_neutral = channel_neutral_angle(ch)
set_servo(ch, new_neutral)
print("通道 {} 新 trim = {:+d}°,中立角度 ≈ {:.1f}°".format(ch, off, new_neutral))
time.sleep(0.1)
# =============== WiFi + 网页控制 ===================
SSID = "jerry"
PASSWORD = "ldk123456"
def wifi_connect():
sta = network.WLAN(network.STA_IF)
sta.active(True)
if not sta.isconnected():
print("连接 WiFi 中:", SSID)
sta.connect(SSID, PASSWORD)
# 等几秒钟
for _ in range(20):
if sta.isconnected():
break
time.sleep(0.5)
if sta.isconnected():
print("WiFi 已连接:", sta.ifconfig())
else:
print("WiFi 连接失败,仍可用串口控制。")
return sta
def web_page():
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>四足机器人控制</title>
<style>
body { font-family: Arial; text-align: center; }
h1 { margin-top: 20px; }
.btn {
display: inline-block;
padding: 15px 30px;
margin: 8px;
font-size: 18px;
text-decoration: none;
border-radius: 8px;
border: 1px solid #333;
}
.btn-main { }
.btn-small { font-size: 14px; padding: 8px 16px; }
</style>
</head>
<body>
<h1>四足机器人控制面板</h1>
<p>点击按钮让机器人动作(每次一步)</p>
<div>
<a class="btn btn-small" href="/?cmd=n">回中立姿态</a>
</div>
<div>
<a class="btn" href="/?cmd=w">⬆ 前进</a>
</div>
<div>
<a class="btn" href="/?cmd=a">⬅ 左转</a>
<a class="btn" href="/?cmd=d">右转 ➡</a>
</div>
<div>
<a class="btn" href="/?cmd=s">⬇ 后退</a>
</div>
<p style="margin-top:30px;font-size:13px;color:#666;">
可以多次点击前进/后退/转向按钮来连续移动。<br/>
trim 校准仍建议通过串口菜单完成。
</p>
</body>
</html>
"""
return html
def web_server():
sta = wifi_connect()
# 获取 IP
if sta.isconnected():
ip = sta.ifconfig()[0]
print("在浏览器中打开: http://{}/".format(ip))
else:
print("WiFi 未连接,网页控制不可用。")
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(1)
print("HTTP 服务器已启动,监听端口 80")
while True:
try:
cl, addr = s.accept()
except OSError:
continue
# print('客户端连接自', addr)
req = cl.recv(1024)
req = str(req)
# 简单解析命令
cmd = None
if "/?cmd=w" in req:
cmd = "w"
elif "/?cmd=s" in req:
cmd = "s"
elif "/?cmd=a" in req:
cmd = "a"
elif "/?cmd=d" in req:
cmd = "d"
elif "/?cmd=n" in req:
cmd = "n"
if cmd == "w":
step_forward_once()
elif cmd == "s":
step_backward_once()
elif cmd == "a":
turn_left_once()
elif cmd == "d":
turn_right_once()
elif cmd == "n":
neutral_pose()
response = web_page()
cl.send('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n')
cl.send(response)
cl.close()
# =============== 主程序 ===================
def main():
print("初始化:按 trim 让所有舵机转到中立位置...")
neutral_pose()
time.sleep(1)
print("初始化完成。启动 WiFi + 网页控制。")
web_server() # 进入 HTTP 循环
# 如果需要串口菜单,可自己在 REPL 中调用 manual_adjust()
if __name__ == '__main__':
main()