mian.py测试完成的多轨迹运动控制代码

This commit is contained in:
琉璃月光
2025-09-17 16:39:51 +08:00
parent 95c75bd409
commit 02268ca185
14 changed files with 561 additions and 603 deletions

View File

@ -2,14 +2,14 @@
import numpy as np
def circle_trajectory(center=(80, 0), radius=40, num_points=60):
def circle_trajectory(center=(80, 0), radius=40, num_points=200):
""" 圆形轨迹 """
angles = np.linspace(0, 2 * np.pi, num_points)
x_list = center[0] + radius * np.cos(angles)
y_list = center[1] + radius * np.sin(angles)
return x_list, y_list
def line_trajectory(start=(40, 0), end=(120, 0), num_points=20):
def line_trajectory(start=(40, 0), end=(120, 0), num_points=100):
""" 直线轨迹 """
t = np.linspace(0, 1, num_points)
x_list = start[0] + t * (end[0] - start[0])
@ -53,7 +53,7 @@ def line_trajectory_fix(start=(40, 0), end=(120, 100), vx=0.1, vy=0.1, num_point
return x_list, y_list
def ellipse_trajectory(center=(80, 0), rx=50, ry=25, num_points=60):
def ellipse_trajectory(center=(80, 0), rx=50, ry=25, num_points=200):
""" 椭圆轨迹 """
angles = np.linspace(0, 2 * np.pi, num_points)
x_list = center[0] + rx * np.cos(angles)

View File

@ -1,457 +0,0 @@
# ==================== 五连杆机械臂轨迹控制集成版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】程序结束")

81
main.py
View File

@ -13,7 +13,7 @@ plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong']
plt.rcParams['axes.unicode_minus'] = False
# ------------------------ 调试开关 ------------------------
DEBUG_MODE = False # <<< 设为 False 控制真实电机
DEBUG_MODE = True # <<< 设为 False 控制真实电机
# 导入运动学和轨迹函数(确保路径正确)
try:
@ -56,7 +56,7 @@ CAN_SERIAL_PORT = '/dev/ttyACM0'
BAUD_RATE = 921600
KP = 50.0
KD = 1.0
DT = 0.01 # 控制周期 10ms每点延时
DT = 0.01# 控制周期 10ms每点延时
# ------------------------ 全局变量 ------------------------
motor1 = motor2 = motor_control = None
@ -264,7 +264,8 @@ def draw_frame(i):
return line,
# ------------------------ 轨迹动画与执行 + 可视化 + FK 验证 ------------------------
def run_trajectory_with_animation(trajectory_func, **params):
# ------------------------ 轨迹动画预览 ------------------------
def preview_trajectory(trajectory_func, **params):
global x_list, y_list, line
print(f"生成轨迹: {trajectory_func.__name__}")
@ -286,13 +287,18 @@ def run_trajectory_with_animation(trajectory_func, **params):
ani = FuncAnimation(fig, draw_frame, frames=len(x_list),
interval=50, blit=True, repeat=False)
plt.show()
return x_list, y_list
# --- 执行并记录 ---
# ------------------------ 执行轨迹 ------------------------
def execute_trajectory(x_list, y_list, dt=DT):
print("开始执行轨迹(直接发送角度)...")
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = execute_direct_trajectory(
x_list, y_list, dt=DT # 每个点发送后延时 1ms这个地方控制电机延时
)
return execute_direct_trajectory(x_list, y_list, dt=dt)
# ------------------------ 可视化与误差统计 ------------------------
# ------------------------ 可视化与误差统计 ------------------------
def visualize_results(x_list, y_list, time_log, theta1_log, theta4_log, x_fk_log, y_fk_log):
# --- 绘制关节角度 ---
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)')
@ -332,7 +338,6 @@ def run_trajectory_with_animation(trajectory_func, **params):
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
@ -349,11 +354,39 @@ def run_trajectory_with_animation(trajectory_func, **params):
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}°")
# 角度转换为弧度,并组合成配对列表
theta1_rad = np.radians(theta1_log)
theta4_rad = np.radians(theta4_log)
# 构建嵌套列表:[[θ1_0, θ4_0], [θ1_1, θ41], ...]
angles_rad_list = [[t1, t4] for t1, t4 in zip(theta1_rad, theta4_rad)]
# 格式化为字符串保留6位小数用逗号分隔
nested_str = ",\n ".join([f"[{t1:.6f}, {t4:.6f}]" for t1, t4 in angles_rad_list])
nested_str = f"[\n {nested_str}\n]"
# 保存为 txt 文件
filename = 'joint_angles_rad_nested.txt'
with open(filename, 'w') as f:
f.write("angles_rad_list = ")
f.write(nested_str)
f.write("\n")
print(f"\n✅ 嵌套列表格式的弧度数据已保存至: {filename}")
# ------------------------ 主流程 ------------------------
def run_full_trajectory(trajectory_func, **params):
x_list, y_list = preview_trajectory(trajectory_func, **params)
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = execute_trajectory(x_list, y_list, dt=DT)
visualize_results(x_list, y_list, time_log, theta1_log, theta4_log, x_fk_log, y_fk_log)
# ==================== 获取电机当前位置 =====================
def get_current_motor_position():
"""
@ -448,19 +481,41 @@ if __name__ == "__main__":
# --- 获取当前位置测试 ---
print("\n=== 测试:获取电机当前位置 ===")
theta1, theta4 = get_current_motor_position()
#theta1, theta4 = get_current_motor_position()
# --- MIT 模式回零 ---
print("\n=== MIT 模式回零 ===")
move_motors_to_zero(duration=1.0) # 主函数里执行回零
#move_motors_to_zero(duration=1.0) # 主函数里执行回零
# ---------------- 选择轨迹 ----------------
'''
#直线轨迹:
trajectory_config = {
'func': line_trajectory_fix,
'params': {'start': (25, 300), 'end': (200, 300), 'num_points': 100}
}
run_trajectory_with_animation(trajectory_config['func'], **trajectory_config['params'])
#圆形轨迹
trajectory_config = {
'func': circle_trajectory,
'params': {'center': (100, 300), 'radius': 100}
}
trajectory_config = {
'func': square_trajectory,
'params': {'side':60, 'num_points': 200}
}
#椭圆轨迹
trajectory_config = {
'func': ellipse_trajectory,
'params': {'center': (100, 250), 'rx': 100, 'ry': 60, 'num_points': 200}
}
'''
trajectory_config = {
'func': circle_trajectory,
'params': {'center': (100, 300), 'radius': 100, 'num_points': 100}
}
run_full_trajectory(trajectory_config['func'], **trajectory_config['params'])
except KeyboardInterrupt:
print("\n【用户中断】")

490
main_test.py Normal file
View File

@ -0,0 +1,490 @@
# ==================== 五连杆机械臂轨迹控制(直接角度发送版)====================
# 功能:轨迹生成 + 逆解 + 直接控制 + 动画显示 + 关节角度可视化 + 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 = False # <<< 设为 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 = 0x02
CAN_SERIAL_PORT = '/dev/ttyACM0'
BAUD_RATE = 921600
KP = 50.0
KD = 1.0
DT = 0.01# 控制周期 10ms每点延时
# ------------------------ 全局变量 ------------------------
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 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, 0x12)
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
# 电机1、4安全角度范围弧度
THETA1_MIN = -np.pi/2 # -60°
THETA1_MAX = np.pi/2 # 60°
THETA4_MIN = -np.pi/2 # -60°
THETA4_MAX = np.pi/2 # 60°
# ------------------------ MIT 控制函数(带限位 + 回零逻辑) ------------------------
def control_two_motors_mit(theta1_rad, theta4_rad):
"""
发送 MIT 控制指令(角度单位:弧度)
带限位检测:如果超限,自动触发回零。
"""
global current_theta1, current_theta4
# --- 限位检测 ---
if not (THETA1_MIN <= theta1_rad <= THETA1_MAX):
print(f"θ1 超出限位 [{THETA1_MIN}, {THETA1_MAX}],执行回零...")
move_motors_to_zero()
return
if not (THETA4_MIN <= theta4_rad <= THETA4_MAX):
print(f"θ4 超出限位 [{THETA4_MIN}, {THETA4_MAX}],执行回零...")
move_motors_to_zero()
return
# --- 正常控制 ---
pos1 = theta1_rad
pos4 = theta4_rad
vel = 0.1
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={np.degrees(theta1_rad):.2f}°, θ4={np.degrees(theta4_rad):.2f}°")
current_theta1 = theta1_rad
current_theta4 = theta4_rad
# ------------------------ 直接轨迹执行函数(无插值)------------------------
from time import perf_counter
import numpy as np
# 假设其他所需函数和全局变量已定义,例如 inverseF, forwardF, adjust_angle_continuity, control_two_motors_mit, 等等.
def execute_direct_trajectory(x_list, y_list, dt=DT):
"""
直接执行轨迹:对每个 (x,y) 计算逆解并立即发送角度,不做插值或速度规划
执行前:先把电机从零点平滑移动到轨迹起点
返回: (time_log, theta1_log, theta4_log, x_fk_log, y_fk_log)
"""
global motor_control, motor1, motor2
ANGLE_OFFSET_RAD = np.pi / 2 # 90° 偏移量
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = [], [], [], [], []
l1, l2, l3, l4, l5 = L1, L2, L3, L4, L0
omega1 = omega4 = 0.0
alpha1 = alpha4 = 0.0
# --- 轨迹起点逆解 ---
x0, y0 = x_list[0], y_list[0]
try:
raw_theta1, raw_theta4 = inverseF(x0, y0, L1, L2, L3, L4, L0)
theta1_start = float(raw_theta1) - ANGLE_OFFSET_RAD
theta4_start = float(raw_theta4) - ANGLE_OFFSET_RAD
print(f"起点逆解成功 -> θ1={np.degrees(theta1_start):.2f}°, θ4={np.degrees(theta4_start):.2f}°")
except Exception as e:
print(f"❌ 起点逆解失败: {e}")
return [], [], [], [], []
# --- 电机移动到起点 ---
print("电机从零点移动到轨迹起点...")
move_motors_to_target(theta1_start, theta4_start, duration=1.5, dt=dt)
current_theta1, current_theta4 = theta1_start, theta4_start
# --- 轨迹执行 ---
start_time = perf_counter()
for i, (x, y) in enumerate(zip(x_list, y_list)):
try:
raw_theta1, raw_theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
target_theta1 = float(raw_theta1) - ANGLE_OFFSET_RAD
target_theta4 = float(raw_theta4) - ANGLE_OFFSET_RAD
target_theta1 = adjust_angle_continuity(target_theta1, current_theta1)
target_theta4 = adjust_angle_continuity(target_theta4, current_theta4)
except Exception as e:
print(f"{i} 点逆解失败 ({x:.2f}, {y:.2f}): {e}")
target_theta1, target_theta4 = current_theta1, current_theta4
else:
current_theta1, current_theta4 = target_theta1, target_theta4
# 控制电机
control_two_motors_mit(target_theta1, target_theta4)
# FK 验证
try:
xc, yc, *_ = forwardF(
u1=target_theta1 + ANGLE_OFFSET_RAD,
u4=target_theta4 + ANGLE_OFFSET_RAD,
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)
# 记录日志
elapsed = perf_counter() - start_time
time_log.append(elapsed)
theta1_log.append(np.degrees(target_theta1))
theta4_log.append(np.degrees(target_theta4))
busy_wait(dt)
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_loop(trajectory_func, loop_count=3, **params):
"""
循环执行轨迹指定次数,保持关节连续性
loop_count: 循环次数
"""
global x_list, y_list, line, current_theta1, current_theta4
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()
plt.ion()
plt.show()
for count in range(1, loop_count + 1):
print(f"\n=== 第 {count} 次执行轨迹 ===")
# --- 执行轨迹 ---
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = execute_direct_trajectory(
x_list, y_list, dt=DT
)
# --- 动画更新 ---
for i in range(len(x_list)):
draw_frame(i)
plt.pause(0.01)
# --- 绘制关节角度 ---
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(f'电机关节角度随时间变化 - 第 {count}')
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(f'FK 验证:目标 vs 实际轨迹 - 第 {count}')
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无法计算误差")
continue
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}°")
plt.ioff()
print(f"\n=== 循环执行 {loop_count} 次完成 ===")
# ==================== 获取电机当前位置 =====================
def get_current_motor_position():
"""
获取当前电机角度(电机原始单位,通常是度),并打印。
返回theta1_current, theta4_current
"""
if not DEBUG_MODE:
try:
motor_control.refresh_motor_status(motor1)
motor_control.refresh_motor_status(motor2)
time.sleep(0.05)
theta1_current = motor1.getPosition()
theta4_current = motor2.getPosition()
except Exception:
theta1_current = theta4_current = 0.0
else:
theta1_current = motor1.getPosition()
theta4_current = motor2.getPosition()
print(f"当前电机弧度 -> θ1={theta1_current:.2f}, θ4={theta4_current:.2f}")
return theta1_current, theta4_current
# ==================== MIT 模式回零函数 =====================
def move_motors_to_zero(duration=1.0, dt=DT):
"""
使用 MIT 模式将电机从当前位置平滑回到零点
duration: 总运动时间 (秒)
dt: 控制周期 (秒)
"""
global motor1, motor2, motor_control, current_theta1, current_theta4
# 获取当前位置
theta1_start, theta4_start = get_current_motor_position()
# 计算步数
steps = int(duration / dt)
for i in range(steps + 1):
t = i / steps
# 线性插值
theta1_target = theta1_start * (1 - t)
theta4_target = theta4_start * (1 - t)
# 发送 MIT 控制
control_two_motors_mit(theta1_target, theta4_target)
busy_wait(dt)
# 确保最终位置为零
control_two_motors_mit(0.0, 0.0)
current_theta1 = current_theta4 = 0.0
print("电机已回零完成。")
# ==================== MIT 模式移动到指定角度(任意两点) =====================
def move_motors_to_target(theta1_target_rad, theta4_target_rad, duration=1.0, dt=DT,
theta1_start_rad=None, theta4_start_rad=None):
"""
使用 MIT 模式将电机从起点平滑移动到指定目标角度
参数:
theta1_target_rad: 电机1目标角度弧度
theta4_target_rad: 电机4目标角度弧度
duration: 总运动时间 (秒)
dt: 控制周期 (秒)
theta1_start_rad: 电机1起始角度弧度默认 None 表示从零点
theta4_start_rad: 电机4起始角度弧度默认 None 表示从零点
"""
global motor1, motor2, motor_control, current_theta1, current_theta4
# 默认从零点出发
if theta1_start_rad is None:
theta1_start_rad = 0.0
if theta4_start_rad is None:
theta4_start_rad = 0.0
steps = int(duration / dt)
for i in range(steps + 1):
t = i / steps
# 线性插值
theta1_now = theta1_start_rad + (theta1_target_rad - theta1_start_rad) * t
theta4_now = theta4_start_rad + (theta4_target_rad - theta4_start_rad) * t
# 发送 MIT 控制
control_two_motors_mit(theta1_now, theta4_now)
busy_wait(dt)
# 确保最终位置精确到目标
control_two_motors_mit(theta1_target_rad, theta4_target_rad)
current_theta1 = theta1_target_rad
current_theta4 = theta4_target_rad
print(f"电机已移动到目标角度: θ1={np.degrees(theta1_target_rad):.2f}°, θ4={np.degrees(theta4_target_rad):.2f}°")
if __name__ == "__main__":
try:
# ---------------- 初始化电机 ----------------
init_motors()
# --- 获取当前位置测试 ---
print("\n=== 测试:获取电机当前位置 ===")
theta1, theta4 = get_current_motor_position()
# --- MIT 模式回零 ---
print("\n=== MIT 模式回零 ===")
move_motors_to_zero(duration=1.0) # 主函数里执行回零
# ---------------- 选择轨迹 ----------------
#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': 100}
}
run_trajectory_with_animation_loop(trajectory_config['func'], loop_count=2, **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】程序结束")

View File

@ -1,130 +0,0 @@
import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from time import perf_counter
# ------------------------ 调试开关 ------------------------
DEBUG_MODE = False # True: 模拟电机, False: 真机
# ------------------------ 电机与控制 ------------------------
if DEBUG_MODE:
class MockMotor:
def __init__(self):
self._pos_deg = 45.0
def getPosition(self):
return self._pos_deg
def setPosition(self, deg):
self._pos_deg = deg
print(f"[DEBUG] 电机位置 -> {deg:.2f}°")
class MockControl:
def enable(self, m): return True
def disable(self, m): return None
def controlMIT(self, m, kp, kd, pos, vel, torq):
deg = np.degrees(pos)
m.setPosition(deg)
motor1 = MockMotor()
motor2 = MockMotor()
motor_control = MockControl()
else:
import serial
from DM_CAN.DM_CAN import Motor, MotorControl, DM_Motor_Type, Control_Type
CAN_SERIAL_PORT = '/dev/ttyACM0'
BAUD_RATE = 921600
MOTOR1_ID = 0x01
MOTOR2_ID = 0x02
def init_real_motors():
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)
mc = MotorControl(can_serial)
m1 = Motor(DM_Motor_Type.DM4310, MOTOR1_ID, 0x11)
m2 = Motor(DM_Motor_Type.DM4310, MOTOR2_ID, 0x12)
mc.addMotor(m1)
mc.addMotor(m2)
mc.switchControlMode(m1, Control_Type.MIT)
mc.switchControlMode(m2, Control_Type.MIT)
time.sleep(0.1)
mc.save_motor_param(m1)
mc.save_motor_param(m2)
mc.enable(m1)
mc.enable(m2)
print("真实电机已使能。")
return m1, m2, mc
# ------------------------ 通用函数 ------------------------
def busy_wait(dt):
start = perf_counter()
while perf_counter() - start < dt:
pass
def adjust_angle_continuity(new_angle, prev_angle):
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 control_two_motors_mit(theta1_rad, theta4_rad, dt=0.01):
motor_control.controlMIT(motor1, 50, 1, theta1_rad, 0.1, 0.0)
motor_control.controlMIT(motor2, 50, 1, theta4_rad, 0.1, 0.0)
busy_wait(dt)
# ------------------------ 移动到零点 ------------------------
def move_to_zero_point(steps=60, dt=0.01):
theta1_current = np.radians(motor1.getPosition())
theta4_current = np.radians(motor2.getPosition())
theta1_list = np.linspace(theta1_current, 0.0, steps)
theta4_list = np.linspace(theta4_current, 0.0, steps)
for t1, t4 in zip(theta1_list, theta4_list):
t1_adj = adjust_angle_continuity(t1, theta1_current)
t4_adj = adjust_angle_continuity(t4, theta4_current)
control_two_motors_mit(t1_adj, t4_adj, dt)
print("已到达零点。")
print(f"电机1角度: {motor1.getPosition():.2f}°")
print(f"电机2角度: {motor2.getPosition():.2f}°")
# ------------------------ 示例轨迹动画 ------------------------
def draw_demo_trajectory():
x_list = np.linspace(0, 200, 50)
y_list = 100 + 50 * np.sin(np.linspace(0, 2*np.pi, 50))
fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(-50, 250)
ax.set_ylim(0, 250)
ax.set_aspect('equal')
ax.grid(True)
ax.plot(x_list, y_list, 'b--', label='示例轨迹')
line, = ax.plot([], [], 'ro-', lw=2, label='机械臂')
def update(i):
line.set_data([0, x_list[i]], [0, y_list[i]])
return line,
ani = FuncAnimation(fig, update, frames=len(x_list), interval=50, blit=True, repeat=False)
ax.legend()
plt.show()
# ------------------------ 主程序 ------------------------
if __name__ == "__main__":
if not DEBUG_MODE:
motor1, motor2, motor_control = init_real_motors()
else:
print("【DEBUG模式】使用模拟电机")
print(f"当前电机角度 -> 电机1: {motor1.getPosition():.2f}°, 电机2: {motor2.getPosition():.2f}°")
print("移动到零点...")
move_to_zero_point(steps=60, dt=0.01)
print("显示示例轨迹动画...")
draw_demo_trajectory()