Files
5dof/jicheng1.py
2025-09-15 10:45:15 +08:00

457 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ==================== 五连杆机械臂轨迹控制集成版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/TS型典型
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】程序结束")