457 lines
16 KiB
Python
457 lines
16 KiB
Python
# ==================== 五连杆机械臂轨迹控制集成版(S型速度 + 五次插值)====================
|
||
# 功能:轨迹生成 + 逆解 + S型速度规划 + 五次多项式平滑控制 + 动画显示 + 关节角度可视化 + FK 验证
|
||
# =======================================================================================
|
||
|
||
import time
|
||
import numpy as np
|
||
import matplotlib.pyplot as plt
|
||
from matplotlib.animation import FuncAnimation
|
||
from time import perf_counter # 高精度计时
|
||
|
||
# 设置中文字体和负号显示
|
||
plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong']
|
||
plt.rcParams['axes.unicode_minus'] = False
|
||
|
||
# ------------------------ 调试开关 ------------------------
|
||
DEBUG_MODE = True # <<< 设为 False 控制真实电机
|
||
|
||
# 导入运动学和轨迹函数(确保路径正确)
|
||
try:
|
||
from calculate.ik import inverseF
|
||
from calculate.fk import forwardF
|
||
from calculate.trajectory import (
|
||
circle_trajectory,
|
||
line_trajectory,
|
||
line_trajectory_fix,
|
||
ellipse_trajectory,
|
||
square_trajectory,
|
||
triangle_trajectory
|
||
)
|
||
except ImportError as e:
|
||
print(f"【警告】无法导入运动学模块: {e}, 使用 DEBUG_MODE")
|
||
DEBUG_MODE = True
|
||
|
||
# -------------------- 非 Debug 模式导入硬件库 --------------------
|
||
if not DEBUG_MODE:
|
||
try:
|
||
from DM_CAN.DM_CAN import Motor, MotorControl, DM_Motor_Type, Control_Type
|
||
import serial
|
||
except ImportError as e:
|
||
print(f"硬件库导入失败: {e}")
|
||
exit(1)
|
||
else:
|
||
print("【DEBUG MODE】: 已启用调试模式,不连接真实硬件。")
|
||
|
||
# ------------------------ 机械臂参数 ------------------------
|
||
L1 = 250 # 左臂长度 (mm)
|
||
L2 = 300
|
||
L3 = 300
|
||
L4 = 250 # 右臂长度
|
||
L0 = 250 # 基座右端偏移
|
||
|
||
# ------------------------ 电机与通信配置 ------------------------
|
||
MOTOR1_ID = 0x01
|
||
MOTOR2_ID = 0x04
|
||
CAN_SERIAL_PORT = '/dev/ttyACM0'
|
||
BAUD_RATE = 921600
|
||
KP = 50.0
|
||
KD = 1.0
|
||
DT = 0.001 # 控制周期 1ms
|
||
|
||
# ------------------------ 全局变量 ------------------------
|
||
motor1 = motor2 = motor_control = None
|
||
current_theta1 = current_theta4 = 0.0
|
||
x_list = y_list = [] # 轨迹点
|
||
line = None
|
||
|
||
# ------------------------ 精确延时 ------------------------
|
||
def busy_wait(dt):
|
||
"""高精度延时"""
|
||
start = perf_counter()
|
||
while perf_counter() - start < dt:
|
||
pass
|
||
|
||
# ------------------------ 角度连续性调整 ------------------------
|
||
def adjust_angle_continuity(new_angle, prev_angle):
|
||
"""
|
||
防止角度跳变(如 3.14 → -3.14),保持连续
|
||
"""
|
||
diff = new_angle - prev_angle
|
||
while diff > np.pi:
|
||
diff -= 2 * np.pi
|
||
while diff < -np.pi:
|
||
diff += 2 * np.pi
|
||
return prev_angle + diff
|
||
|
||
# ==================== 五次多项式插值 ====================
|
||
def quintic_interpolation(q0, qf, t, T):
|
||
"""
|
||
五次多项式平滑插值
|
||
q0: 起始位置
|
||
qf: 目标位置
|
||
t: 当前时间(0~T)
|
||
T: 总时间
|
||
返回:平滑位置
|
||
"""
|
||
s = t / T
|
||
s = np.clip(s, 0, 1) # 限制在0~1
|
||
alpha = s**3 * (10 - 15*s + 6*s**2)
|
||
return q0 + (qf - q0) * alpha
|
||
|
||
# ==================== S型速度规划(5段式)====================
|
||
def s_curve_timing(total_time, dt=0.001, max_accel_ratio=2.0):
|
||
"""
|
||
生成 S 型速度剖面的时间参数化 s(t) ∈ [0,1]
|
||
使用 5 段:加加速 → 匀加速 → 匀速 → 匀减速 → 减减速
|
||
"""
|
||
t_list = np.arange(0, total_time, dt)
|
||
if len(t_list) == 0:
|
||
t_list = np.array([0])
|
||
|
||
# 参数:最大加速度设为 4v/T(S型典型)
|
||
v_max = 0.5 * max_accel_ratio * total_time # 归一化速度
|
||
a_max = 4 * v_max / total_time # 加速度
|
||
t_jerk = v_max / a_max # 加加速时间
|
||
t_acc = 2 * t_jerk # 总加速时间
|
||
t_cruise = total_time - 2 * t_acc
|
||
|
||
if t_cruise < 0:
|
||
t_acc = total_time / 2
|
||
t_jerk = t_acc / 2
|
||
t_cruise = 0
|
||
|
||
s_list = []
|
||
for t in t_list:
|
||
if t <= t_jerk:
|
||
s = (a_max / t_jerk) * t**3 / 6
|
||
elif t <= t_acc - t_jerk:
|
||
t2 = t - t_jerk
|
||
s = (a_max / t_jerk) * t_jerk**3 / 6 + a_max * t_jerk * t2 + 0.5 * a_max * t2**2
|
||
elif t <= t_acc:
|
||
t3 = t - (t_acc - t_jerk)
|
||
s1 = (a_max / t_jerk) * t_jerk**3 / 6
|
||
s2 = a_max * t_jerk * (t_acc - 2*t_jerk) + 0.5 * a_max * (t_acc - 2*t_jerk)**2
|
||
s3 = a_max * t_jerk * t3 - 0.5 * a_max * t3**2
|
||
s = s1 + s2 + s3
|
||
elif t <= t_acc + t_cruise:
|
||
t4 = t - t_acc
|
||
s = s_list[-1] if s_list else 0
|
||
s += v_max * t4
|
||
else:
|
||
t5 = t - (t_acc + t_cruise)
|
||
if t5 <= t_jerk:
|
||
s = 1 - (v_max * (total_time - t) - 0.5 * a_max * t5**2)
|
||
elif t5 <= t_acc - t_jerk:
|
||
s = 1 - (v_max * (total_time - t - t_jerk) - 0.5 * a_max * t_jerk**2)
|
||
else:
|
||
t6 = t5 - (t_acc - t_jerk)
|
||
s = 1 - (a_max / t_jerk) * t6**3 / 6
|
||
s_list.append(s)
|
||
|
||
# 归一化到 [0,1]
|
||
s_max = max(s_list) if s_list else 1
|
||
s_normalized = np.array(s_list) / s_max if s_max > 0 else np.zeros_like(s_list)
|
||
return t_list, s_normalized
|
||
|
||
# ------------------------ 初始化电机 ------------------------
|
||
def init_motors():
|
||
global motor1, motor2, motor_control
|
||
|
||
if DEBUG_MODE:
|
||
print("【DEBUG】跳过电机初始化")
|
||
motor1 = motor2 = type('Motor', (), {'id': 0})()
|
||
motor_control = type('MotorControl', (), {
|
||
'enable': lambda x: True,
|
||
'disable': lambda x: None,
|
||
'controlMIT': lambda m, kp, kd, pos, vel, torq: None,
|
||
'refresh_motor_status': lambda m: None,
|
||
'switchControlMode': lambda m, mode: None,
|
||
'save_motor_param': lambda m: None
|
||
})()
|
||
return motor1, motor2, motor_control
|
||
|
||
try:
|
||
can_serial = serial.Serial(CAN_SERIAL_PORT, BAUD_RATE, timeout=0.5)
|
||
print(f"CAN 串口 {CAN_SERIAL_PORT} 打开成功")
|
||
except Exception as e:
|
||
print(f"无法打开串口: {e}")
|
||
exit(1)
|
||
|
||
motor_control = MotorControl(can_serial)
|
||
motor1 = Motor(DM_Motor_Type.DM4310, MOTOR1_ID, 0x11)
|
||
motor2 = Motor(DM_Motor_Type.DM4310, MOTOR2_ID, 0x15)
|
||
|
||
motor_control.addMotor(motor1)
|
||
motor_control.addMotor(motor2)
|
||
motor_control.switchControlMode(motor1, Control_Type.MIT)
|
||
motor_control.switchControlMode(motor2, Control_Type.MIT)
|
||
time.sleep(0.1)
|
||
motor_control.save_motor_param(motor1)
|
||
motor_control.save_motor_param(motor2)
|
||
motor_control.enable(motor1)
|
||
motor_control.enable(motor2)
|
||
print("电机已使能。")
|
||
return motor1, motor2, motor_control
|
||
|
||
# ------------------------ MIT 控制函数 ------------------------
|
||
def control_two_motors_mit(theta1, theta4):
|
||
"""发送 MIT 控制指令(角度单位:弧度)"""
|
||
global current_theta1, current_theta4
|
||
|
||
pos1 = np.degrees(theta1)
|
||
pos4 = np.degrees(theta4)
|
||
vel = 0.0
|
||
torq = 0.0
|
||
|
||
if not DEBUG_MODE:
|
||
motor_control.controlMIT(motor1, KP, KD, pos1, vel, torq)
|
||
motor_control.controlMIT(motor2, KP, KD, pos4, vel, torq)
|
||
else:
|
||
print(f"[DEBUG] 控制 -> θ1={pos1:.2f}°, θ4={pos4:.2f}°")
|
||
|
||
current_theta1 = theta1
|
||
current_theta4 = theta4
|
||
|
||
# ------------------------ 平滑轨迹执行函数(带 S 型速度)------------------------
|
||
def execute_smooth_trajectory(x_list, y_list,
|
||
total_time=8.0,
|
||
kp=KP, kd=KD, dt=DT,
|
||
read_interval=0.01):
|
||
"""
|
||
执行平滑轨迹:使用 S 型速度剖面 + 五次多项式插值
|
||
返回: (time_log, theta1_log, theta4_log, x_fk_log, y_fk_log)
|
||
"""
|
||
global motor_control, motor1, motor2
|
||
|
||
time_log = []
|
||
theta1_log = []
|
||
theta4_log = []
|
||
x_fk_log = []
|
||
y_fk_log = []
|
||
|
||
# FK 参数
|
||
l1, l2, l3, l4, l5 = L1, L2, L3, L4, L0
|
||
omega1 = omega4 = 0.0
|
||
alpha1 = alpha4 = 0.0
|
||
|
||
# 获取起始角度
|
||
if not DEBUG_MODE:
|
||
motor_control.refresh_motor_status(motor1)
|
||
motor_control.refresh_motor_status(motor2)
|
||
time.sleep(0.1)
|
||
motor_control.refresh_motor_status(motor1)
|
||
motor_control.refresh_motor_status(motor2)
|
||
start_theta1 = np.radians(motor1.getPosition())
|
||
start_theta4 = np.radians(motor2.getPosition())
|
||
else:
|
||
start_theta1 = start_theta4 = 0.0
|
||
|
||
# 生成 S 型时间剖面
|
||
t_profile, s_profile = s_curve_timing(total_time=total_time, dt=dt)
|
||
if len(s_profile) == 0:
|
||
s_profile = np.linspace(0, 1, len(x_list))
|
||
|
||
# 插值轨迹点(使用 S 型进度)
|
||
num_s = len(s_profile)
|
||
x_interp = np.interp(s_profile, np.linspace(0, 1, len(x_list)), x_list)
|
||
y_interp = np.interp(s_profile, np.linspace(0, 1, len(y_list)), y_list)
|
||
|
||
prev_theta1 = start_theta1
|
||
prev_theta4 = start_theta4
|
||
|
||
start_time = perf_counter()
|
||
|
||
for i in range(num_s):
|
||
x = x_interp[i]
|
||
y = y_interp[i]
|
||
|
||
try:
|
||
target_theta1_raw, target_theta4_raw = inverseF(x, y, L1, L2, L3, L4, L0)
|
||
target_theta1 = float(target_theta1_raw)
|
||
target_theta4 = float(target_theta4_raw)
|
||
except Exception as e:
|
||
print(f"逆解失败: ({x:.2f}, {y:.2f}) -> {e}")
|
||
target_theta1 = prev_theta1
|
||
target_theta4 = prev_theta4
|
||
else:
|
||
target_theta1 = adjust_angle_continuity(target_theta1, prev_theta1)
|
||
target_theta4 = adjust_angle_continuity(target_theta4, prev_theta4)
|
||
|
||
# 五次多项式插值(单点瞬时)
|
||
T_seg = dt
|
||
steps = 1 # 每个点只执行一次
|
||
for step in range(steps):
|
||
t_step = step * T_seg / steps
|
||
theta1 = quintic_interpolation(prev_theta1, target_theta1, t_step, T_seg)
|
||
theta4 = quintic_interpolation(prev_theta4, target_theta4, t_step, T_seg)
|
||
|
||
control_two_motors_mit(theta1, theta4)
|
||
|
||
# FK 验证
|
||
try:
|
||
xc, yc, _, _, _, _, _, _ = forwardF(
|
||
u1=theta1, u4=theta4, omega1=omega1, omega4=omega4,
|
||
l1=l1, l2=l2, l3=l3, l4=l4, l5=l5, alpha1=alpha1, alpha4=alpha4
|
||
)
|
||
x_fk_log.append(xc)
|
||
y_fk_log.append(yc)
|
||
except:
|
||
x_fk_log.append(np.nan)
|
||
y_fk_log.append(np.nan)
|
||
|
||
# 读取反馈
|
||
now = perf_counter()
|
||
if not DEBUG_MODE and (now - start_time) % read_interval < dt:
|
||
motor_control.refresh_motor_status(motor1)
|
||
motor_control.refresh_motor_status(motor2)
|
||
|
||
time_log.append(now - start_time)
|
||
theta1_log.append(np.degrees(theta1))
|
||
theta4_log.append(np.degrees(theta4))
|
||
|
||
next_time = start_time + t_profile[i] + dt
|
||
busy_wait(next_time - perf_counter())
|
||
|
||
prev_theta1, prev_theta4 = target_theta1, target_theta4
|
||
|
||
return time_log, theta1_log, theta4_log, x_fk_log, y_fk_log
|
||
|
||
# ------------------------ 动画绘制函数 ------------------------
|
||
def draw_frame(i):
|
||
x = x_list[i]
|
||
y = y_list[i]
|
||
try:
|
||
theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
|
||
x2 = L1 * np.cos(theta1)
|
||
y2 = L1 * np.sin(theta1)
|
||
x4 = L0 + L4 * np.cos(theta4)
|
||
y4 = L4 * np.sin(theta4)
|
||
x_coords = [0, x2, x, x4, L0]
|
||
y_coords = [0, y2, y, y4, 0]
|
||
line.set_data(x_coords, y_coords)
|
||
except:
|
||
line.set_data([], [])
|
||
return line,
|
||
|
||
# ------------------------ 轨迹动画与执行 + 可视化 + FK 验证 ------------------------
|
||
def run_trajectory_with_animation(trajectory_func, **params):
|
||
global x_list, y_list, line
|
||
|
||
print(f"生成轨迹: {trajectory_func.__name__}")
|
||
x_list, y_list = trajectory_func(**params)
|
||
print(f"轨迹点数: {len(x_list)}")
|
||
|
||
# --- 动画 ---
|
||
fig, ax = plt.subplots(figsize=(10, 8))
|
||
ax.set_xlim(-50, L0 + 100)
|
||
ax.set_ylim(0, 500)
|
||
ax.set_aspect('equal')
|
||
ax.grid(True, alpha=0.6)
|
||
ax.set_title(f"五连杆机械臂 - 轨迹: {trajectory_func.__name__}")
|
||
|
||
ax.plot(x_list, y_list, 'b--', alpha=0.5, label='目标轨迹')
|
||
line, = ax.plot([], [], 'r-o', lw=3, ms=6, label='机械臂')
|
||
ax.legend()
|
||
|
||
ani = FuncAnimation(fig, draw_frame, frames=len(x_list),
|
||
interval=50, blit=True, repeat=False)
|
||
plt.show()
|
||
|
||
# --- 执行并记录 ---
|
||
print("开始执行轨迹(S型速度 + 五次插值)...")
|
||
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = execute_smooth_trajectory(
|
||
x_list, y_list,
|
||
total_time=8.0,
|
||
kp=50.0, kd=1.0, dt=0.001
|
||
)
|
||
|
||
# --- 绘制关节角度 ---
|
||
fig2, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
|
||
ax1.plot(time_log, theta1_log, 'b-o', markersize=3, linewidth=1.5, label='θ₁ (电机1)')
|
||
ax1.set_ylabel('关节角 θ₁ (°)')
|
||
ax1.set_title('电机关节角度随时间变化')
|
||
ax1.grid(True, alpha=0.5)
|
||
ax1.legend()
|
||
|
||
ax2.plot(time_log, theta4_log, 'r-o', markersize=3, linewidth=1.5, label='θ₄ (电机2)')
|
||
ax2.set_xlabel('时间 (秒)')
|
||
ax2.set_ylabel('关节角 θ₄ (°)')
|
||
ax2.grid(True, alpha=0.5)
|
||
ax2.legend()
|
||
plt.tight_layout()
|
||
plt.show()
|
||
|
||
# --- FK 验证 ---
|
||
fig3, ax3 = plt.subplots(figsize=(10, 8))
|
||
ax3.plot(x_list, y_list, 'b--', linewidth=2, label='目标轨迹', alpha=0.8)
|
||
ax3.plot(x_fk_log, y_fk_log, 'g-', linewidth=2, marker='o', markersize=3, label='FK 重建轨迹', alpha=0.7)
|
||
ax3.scatter(x_fk_log[0], y_fk_log[0], c='green', s=100, marker='s', label='起点')
|
||
ax3.scatter(x_fk_log[-1], y_fk_log[-1], c='red', s=100, marker='x', label='终点')
|
||
ax3.set_xlabel('X (mm)')
|
||
ax3.set_ylabel('Y (mm)')
|
||
ax3.set_title('FK 验证:目标 vs 实际轨迹')
|
||
ax3.grid(True, alpha=0.5)
|
||
ax3.legend()
|
||
ax3.set_aspect('equal')
|
||
plt.tight_layout()
|
||
plt.show()
|
||
|
||
# --- 误差统计 ---
|
||
mask = ~np.isnan(x_fk_log) & ~np.isnan(y_fk_log)
|
||
x_fk_valid = np.array(x_fk_log)[mask]
|
||
y_fk_valid = np.array(y_fk_log)[mask]
|
||
if len(x_fk_valid) == 0:
|
||
print("【警告】FK 数据全为 nan,无法计算误差")
|
||
return
|
||
|
||
t_target = np.linspace(0, 1, len(x_list))
|
||
t_fk = np.linspace(0, 1, len(x_fk_valid))
|
||
from scipy.interpolate import interp1d
|
||
try:
|
||
f_x = interp1d(t_fk, x_fk_valid, kind='linear', fill_value='extrapolate')
|
||
f_y = interp1d(t_fk, y_fk_valid, kind='linear', fill_value='extrapolate')
|
||
x_interp = f_x(t_target)
|
||
y_interp = f_y(t_target)
|
||
errors = np.sqrt((x_interp - x_list)**2 + (y_interp - y_list)**2)
|
||
print("\n=== FK 验证误差统计 ===")
|
||
print(f"平均误差: {np.mean(errors):.3f} mm")
|
||
print(f"最大误差: {np.max(errors):.3f} mm")
|
||
print(f"标准差: {np.std(errors):.3f} mm")
|
||
except Exception as e:
|
||
print(f"误差计算失败: {e}")
|
||
|
||
# --- 角度范围 ---
|
||
print("\n=== 关节角度范围统计 ===")
|
||
print(f"θ₁ 范围: {min(theta1_log):.2f}° ~ {max(theta1_log):.2f}°")
|
||
print(f"θ₄ 范围: {min(theta4_log):.2f}° ~ {max(theta4_log):.2f}°")
|
||
|
||
# ------------------------ 主函数 ------------------------
|
||
if __name__ == "__main__":
|
||
try:
|
||
init_motors()
|
||
|
||
# 选择轨迹
|
||
trajectory_config = {
|
||
'func': line_trajectory_fix,
|
||
'params': {'start': (25, 300), 'end': (200, 300), 'num_points': 100}
|
||
}
|
||
|
||
# 示例:圆形轨迹
|
||
# trajectory_config = {
|
||
# 'func': circle_trajectory,
|
||
# 'params': {'center': (100, 300), 'radius': 40, 'num_points': 100}
|
||
# }
|
||
|
||
run_trajectory_with_animation(trajectory_config['func'], **trajectory_config['params'])
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n【用户中断】")
|
||
except Exception as e:
|
||
print(f"程序异常: {e}")
|
||
finally:
|
||
if not DEBUG_MODE:
|
||
motor_control.disable(motor1)
|
||
motor_control.disable(motor2)
|
||
print("电机已停机。")
|
||
else:
|
||
print("【DEBUG】程序结束") |