【花雕学编程】Arduino BLDC 之基于反向动力学的关节角度闭环控制
摘要:反向动力学(IK)控制是机器人实现智能协作的关键技术,通过模型驱动的前瞻性控制和力矩输出,实现高精度、柔顺的运动控制。其核心优势包括解耦非线性系统、简化运动规划,适用于协作机器人、精密加工等场景。实施时需关注计算资源、精确建模、硬件协同和安全机制。示例代码展示了单/双关节控制及重力补偿的实现方法,体现了IK在实时控制中的应用。

基于反向动力学(Inverse Kinematics, IK)的关节角度闭环控制,是机器人控制领域从“简单自动化”迈向“智能协作”的关键一步。它不再仅仅是让关节机械地转动到指定角度,而是让机器人具备了根据末端执行器的任务目标,自主计算并协调各关节运动的能力。
🛠️ 主要特点
模型驱动的前瞻性控制
与传统的反应式控制(如 PID 在出现误差后才纠正)不同,反向动力学控制是前瞻性的。
核心逻辑: 它基于一个精确的机器人动力学模型。当给定一个期望的末端轨迹(例如机械臂末端要画一条直线),该模型能提前计算出各个关节需要产生的精确力矩,以克服惯性、重力和关节间的耦合效应。
优势: 这种“预判”能力使得机器人的运动极其平滑、精确且高效,避免了传统控制方法在处理复杂轨迹时容易产生的滞后和振荡。
力矩作为核心控制变量
这是反向动力学控制与传统位置/速度控制的根本区别。
直接力矩输出: 系统的核心输出是转矩指令,而非简单的 PWM 占空比或目标转速。这使得机器人能够直接控制与环境交互的力。例如,在进行抛光作业时,可以精确控制末端对工件保持恒定的压力,而不是一个固定的位置。
柔顺性实现: 通过力矩控制,机器人可以模拟出类似人体肌肉的“柔顺性”,在意外碰撞时顺应外力,保护自身和环境。
系统解耦与线性化
多关节机器人是一个高度耦合和非线性的系统,一个关节的运动会直接影响其他关节。
解耦作用: 反向动力学控制器作为一个高阶的“内环”,其核心作用之一就是实时计算并补偿这些复杂的耦合项和非线性项(如科里奥利力、离心力)。
简化设计: 经过补偿后,从外层的控制环(如位置环)来看,整个复杂的多关节系统表现得就像一个简单的、解耦的线性系统,从而极大地简化了高级运动规划算法的设计难度。
🏭 应用场景
该技术主要应用于对运动精度、平滑性和人机交互安全性有高要求的场景:
协作机器人(Cobot): 在工业生产线上,协作机器人需要与人类工人近距离协同工作。基于反向动力学的控制使其能够安全地响应外部的推拉力,实现拖拽示教或柔性装配。
精密加工与打磨: 在对曲面工件进行打磨、抛光或去毛刺时,工具末端必须始终垂直于表面并保持恒定的压力。力矩控制是实现这一工艺要求的关键。
仿生机器人与假肢: 无论是双足行走的仿人机器人,还是智能假肢,都需要模拟生物体自然、节能且柔顺的运动方式,反向动力学是实现这一目标的核心算法。
科研与高级原型开发: 作为研究先进控制理论、人机交互和复杂运动规划的理想平台。
⚠️ 注意事项
实施该系统是一项复杂的系统工程,需重点关注以下挑战:
计算资源的高门槛
算力瓶颈: 反向动力学的实时计算非常消耗算力。经典的 Arduino Uno/Nano 等 8 位单片机完全无法胜任。必须采用高性能的 ARM 架构开发板,如 Arduino Portenta H7、Teensy 4.1 或结合 Raspberry Pi/Jetson Nano 等上位机协同工作。
算法优化: 必须对动力学方程进行极致的代码优化,利用查表法、近似计算等手段减少浮点运算量,确保控制周期的稳定性(通常要求 1ms 以内)。
精确的动力学建模
模型准确性: 控制效果高度依赖于动力学模型的准确性。必须精确测量或辨识每个连杆的质量、质心、转动惯量等参数。模型失配会导致补偿不足或过度,引发系统振荡。
摩擦力补偿: 实际机械结构中的摩擦力(库仑摩擦、粘滞摩擦)是非线性的,且难以精确建模,这是影响低速控制精度的主要因素之一。
底层硬件的协同
传感器精度: 高精度的关节角度反馈(如高分辨率磁编码器)是闭环控制的基础。传感器的噪声和延迟会直接影响力矩控制的精度。
驱动器性能: 底层的 BLDC 驱动器必须具备快速的电流环响应能力,以准确执行上层控制器下发的力矩指令。通信延迟必须极低且确定。
安全与保护机制
物理限位: 必须设置硬件限位开关,防止算法计算错误导致关节超程损坏机械结构。
力矩限制: 在软件中必须设置力矩输出的上下限,防止电机堵转或过载烧毁。

1、单关节位置闭环控制(使用编码器反馈)
#include <SimpleFOC.h>
// 电机与编码器配置
BLDCMotor motor(7); // 电机PWM引脚
BLDCDriver3PWM driver(3, 5, 6, 11); // 驱动器PWM引脚
Encoder encoder(2, 4); // 编码器A/B相引脚
// PID参数
float target_angle = 90.0; // 目标角度(度)
float Kp = 0.5, Ki = 0.1, Kd = 0.01;
PIDController pid({Kp, Ki, Kd});
void setup() {
Serial.begin(9600);
// 初始化编码器(4倍频)
encoder.init();
encoder.enableInterrupts(doA, doB);
// 链接电机与驱动器
motor.linkDriver(&driver);
motor.linkSensor(&encoder);
motor.controller = MotionControlType::torque; // 扭矩控制模式
// 初始化FOC
motor.init();
motor.initFOC();
}
void loop() {
// 反向动力学:将目标角度转换为扭矩指令
float current_angle = encoder.getAngle() * 180 / PI; // 编码器角度(度)
float error = target_angle - current_angle;
float torque = pid(error); // PID输出扭矩
// 设置电机扭矩
motor.move(torque);
// 调试输出
Serial.print("Target: "); Serial.print(target_angle);
Serial.print(" Current: "); Serial.print(current_angle);
Serial.print(" Torque: "); Serial.println(torque);
delay(10);
}
// 编码器中断回调
void doA() { encoder.handleA(); }
void doB() { encoder.handleB(); }
2、双关节协同控制(机械臂模拟)
#include <SimpleFOC.h>
// 关节1配置
BLDCMotor motor1(7);
BLDCDriver3PWM driver1(3, 5, 6, 11);
Encoder encoder1(2, 4);
PIDController pid1({0.5, 0.1, 0.01});
// 关节2配置
BLDCMotor motor2(8);
BLDCDriver3PWM driver2(9, 10, 12, 13);
Encoder encoder2(A0, A1);
PIDController pid2({0.4, 0.08, 0.01});
// 目标点(笛卡尔坐标转关节角度)
float target_x = 10.0, target_y = 5.0; // 单位:cm
float L1 = 15.0, L2 = 10.0; // 连杆长度
void setup() {
Serial.begin(9600);
// 初始化关节1
encoder1.init(); encoder1.enableInterrupts(doA1, doB1);
motor1.linkDriver(&driver1); motor1.linkSensor(&encoder1);
motor1.init(); motor1.initFOC();
// 初始化关节2
encoder2.init(); encoder2.enableInterrupts(doA2, doB2);
motor2.linkDriver(&driver2); motor2.linkSensor(&encoder2);
motor2.init(); motor2.initFOC();
}
void loop() {
// 反向运动学:笛卡尔坐标转关节角度
float theta2 = acos((sq(target_x) + sq(target_y) - sq(L1) - sq(L2)) / (2 * L1 * L2));
float theta1 = atan2(target_y, target_x) - atan2(L2 * sin(theta2), L1 + L2 * cos(theta2));
// 转换为度并控制关节1
float angle1 = theta1 * 180 / PI;
float error1 = angle1 - encoder1.getAngle() * 180 / PI;
motor1.move(pid1(error1));
// 控制关节2
float angle2 = theta2 * 180 / PI;
float error2 = angle2 - encoder2.getAngle() * 180 / PI;
motor2.move(pid2(error2));
delay(20);
}
// 中断回调
void doA1() { encoder1.handleA(); } void doB1() { encoder1.handleB(); }
void doA2() { encoder2.handleA(); } void doB2() { encoder2.handleB(); }
3、带重力补偿的关节控制(模拟负载)
#include <SimpleFOC.h>
// 电机与编码器配置
BLDCMotor motor(7);
BLDCDriver3PWM driver(3, 5, 6, 11);
Encoder encoder(2, 4);
// 参数
float target_angle = 45.0; // 目标角度
float Kp = 0.6, Ki = 0.05, Kd = 0.02;
float gravity_comp = 0.3; // 重力补偿系数(需根据实际负载调整)
void setup() {
Serial.begin(9600);
encoder.init(); encoder.enableInterrupts(doA, doB);
motor.linkDriver(&driver); motor.linkSensor(&encoder);
motor.init(); motor.initFOC();
}
void loop() {
// 当前角度(弧度)
float current_angle = encoder.getAngle();
// 反向动力学:PID + 重力补偿
float error = target_angle * PI / 180 - current_angle;
float pid_output = Kp * error + Ki * integral(error) + Kd * derivative(error);
float torque = pid_output + gravity_comp * sin(current_angle); // 补偿重力
motor.move(torque);
Serial.print("Angle: "); Serial.print(current_angle * 180 / PI);
Serial.print(" Torque: "); Serial.println(torque);
delay(10);
}
// 简化的积分与微分(实际建议用库函数)
float integral(float error) {
static float sum = 0;
sum += error;
return sum * 0.01; // 假设循环周期10ms
}
float derivative(float error) {
static float last_error = 0;
float d = (error - last_error) / 0.01;
last_error = error;
return d;
}
void doA() { encoder.handleA(); } void doB() { encoder.handleB(); }
要点解读
反向动力学与正向动力学的区别
正向动力学:已知关节角度/扭矩 → 计算末端位置(如机械臂仿真)。
反向动力学:已知目标位置 → 计算所需关节角度/扭矩(如实际控制)。
案例2通过几何关系将笛卡尔坐标转换为关节角度,体现反向动力学核心思想。
传感器反馈的精度要求
编码器分辨率直接影响控制精度。例如:
6.5寸轮毂电机若需1°精度,需≥360CPR编码器(案例1)。
机械臂关节可能需要更高分辨率(如案例2使用绝对编码器)。
重力补偿的必要性
水平关节无需补偿,但垂直关节(如案例3)需额外扭矩平衡负载重力。
补偿系数需通过实验标定(如悬挂砝码测试)。
多关节协同的复杂性
双关节系统(案例2)需解耦控制,避免关节间干扰。
高级方案可引入雅可比矩阵或计算力矩法(CMC)处理非线性。
实际工程中的挑战
摩擦补偿:机械关节的库仑摩擦需额外建模(如添加前馈项)。
安全限制:需软件限位(如if (angle > MAX_ANGLE) torque = 0;)。
通信延迟:远程控制时需预测补偿(如卡尔曼滤波)。

4、平面2自由度BLDC关节机械臂(物料抓取定位)
场景定位
工业生产线上的物料精准抓取与定位,机械臂通过2个BLDC驱动的关节,实现平面内末端位置的高精度控制,比如从传送带抓取零件并放置到指定工位。核心需求是根据目标坐标,快速求解关节角度,并通过闭环控制修正角度误差,确保末端定位误差≤0.1cm。
核心控制逻辑
IK求解:平面2连杆(长度l1=20cm,l2=15cm),给定末端坐标(x,y),通过余弦定理+反三角函数求解关节角度θ1(底座关节)、θ2(肘部关节);
闭环控制:Arduino向VESC驱动板发送目标角度指令(CAN协议),VESC读取编码器反馈的实际角度,执行内部PID算法,修正角度误差;
反馈补偿:Arduino读取VESC返回的关节实际角度,计算角度误差,叠加辅助PID补偿(增强精度,避免驱动板内置PID的误差),确保末端定位精度。
// 平面2自由度BLDC关节机械臂控制代码(物料抓取)
#include <CAN.h> // Arduino CAN通信库(需适配VESC的CAN协议)
#include <Arduino.h>
// ========== 机械臂结构参数(可根据实际情况修改) ==========
const float l1 = 20.0; // 关节1到关节2的连杆长度(cm)
const float l2 = 15.0; // 关节2到末端的连杆长度(cm)
const float PI = 3.1415926;
// ========== CAN通信配置(VESC默认CAN参数) ==========
#define CAN_BAUD_RATE 500000
#define JOINT1_ID 0x01 // 关节1的CAN节点ID(VESC配置的ID)
#define JOINT2_ID 0x02 // 关节2的CAN节点ID
// ========== 全局变量(存储目标与实际角度) ==========
float targetX = 0, targetY = 0; // 末端目标坐标(cm)
float targetTheta1 = 0, targetTheta2 = 0; // IK求解的目标关节角度(弧度)
float actualTheta1 = 0, actualTheta2 = 0; // 反馈的实际关节角度(弧度)
float pidError1 = 0, pidError2 = 0; // 角度误差
float lastError1 = 0, lastError2 = 0; // 上一时刻误差(用于PID微分)
// ========== PID参数(辅助补偿,提升闭环精度) ==========
const float Kp1 = 0.8, Ki1 = 0.01, Kd1 = 0.1; // 关节1 PID参数
const float Kp2 = 0.7, Ki2 = 0.008, Kd2 = 0.08;// 关节2 PID参数
// ========== 硬件引脚定义 ==========
#define ULTRA_TRIG 10 // 超声波Trig引脚
#define ULTRA_ECHO 11 // 超声波Echo引脚
#define GRIPPER_PIN 9 // 舵机控制引脚(末端抓取)
// ========== 函数声明 ==========
void setupCAN();
void setupHardware();
void readTargetCoordinate(); // 读取超声波检测的目标坐标
void calculateIK(); // 反向动力学求解关节角度
void sendJointTarget(); // 向VESC发送关节目标角度(CAN协议)
void readJointActual(); // 读取关节实际角度(CAN反馈)
void pidCompensation(); // 辅助PID补偿
void controlGripper(bool open);// 控制末端抓取器
void setup() {
Serial.begin(115200);
Serial.println("2自由度BLDC机械臂初始化...");
setupCAN(); // 初始化CAN通信(连接VESC驱动板)
setupHardware(); // 初始化硬件引脚(超声波、舵机)
// 初始化VESC CAN(根据VESC CAN协议,配置节点ID与波特率)
// 注:VESC默认支持CANopen,需提前配置电机ID和闭环参数
Serial.println("初始化完成,开始检测目标坐标...");
}
void loop() {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate < 50) return; // 20Hz更新频率,避免通信过载
lastUpdate = millis();
1. 检测目标坐标(物料位置)
readTargetCoordinate();
if (targetX == 0 && targetY == 0) {
Serial.println("未检测到目标物料");
return;
}
2. 反向动力学求解目标关节角度
calculateIK();
3. 向VESC发送关节目标角度(闭环控制指令)
sendJointTarget();
4. 读取关节实际反馈角度
readJointActual();
5. 辅助PID补偿(叠加到VESC目标角度,修正误差)
pidCompensation();
6. 末端抓取控制(到达目标后闭合)
if (sqrt(pow(targetX, 2) + pow(targetY, 2)) < 0.5) { // 末端到位(误差<0.5cm)
controlGripper(true); // 闭合抓取
Serial.println("末端到位,执行抓取");
delay(2000);
controlGripper(false);// 张开释放
}
}
// ========== 初始化CAN通信(适配VESC协议) ==========
void setupCAN() {
CAN.begin(CAN_BAUD_RATE);
Serial.println("CAN通信初始化完成,波特率500k");
}
// ========== 初始化硬件引脚 ==========
void setupHardware() {
pinMode(ULTRA_TRIG, OUTPUT);
pinMode(ULTRA_ECHO, INPUT);
pinMode(GRIPPER_PIN, OUTPUT);
// 舵机初始化
Serial.println("硬件引脚初始化完成");
}
// ========== 读取超声波检测的目标坐标(x:水平距离,y:垂直高度) ==========
void readTargetCoordinate() {
digitalWrite(ULTRA_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(ULTRA_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(ULTRA_TRIG, LOW);
long duration = pulseIn(ULTRA_ECHO, HIGH);
// 距离转换(cm),假设物料在机械臂前方,y=10cm(固定高度)
float distance = duration * 0.0343 / 2;
targetX = distance; // 水平距离作为x
targetY = 10.0; // 固定高度作为y(可扩展为多超声波检测)
// 坐标有效性校验
if (targetX < 0 || targetX > (l1 + l2) * 0.9) {
targetX = 0; // 超出工作空间,清零等待重新检测
}
}
// ========== 反向动力学求解关节角度(平面2连杆模型) ==========
void calculateIK() {
float dx = targetX;
float dy = targetY;
float r = sqrt(dx * dx + dy * dy); // 原点到末端的距离
// 校验:目标点是否在工作空间内(l1+l2为最大距离,l1-l2为最小距离)
if (r > l1 + l2 || r < fabs(l1 - l2)) {
Serial.println("目标点超出工作空间!");
targetTheta1 = 0;
targetTheta2 = 0;
return;
}
// 求解θ2(肘部关节角度,用余弦定理)
float cosTheta2 = (dx*dx + dy*dy - l1*l1 - l2*l2) / (2 * l1 * l2);
cosTheta2 = constrain(cosTheta2, -1.0, 1.0); // 防止浮点误差导致超出acos范围
targetTheta2 = acos(cosTheta2);
// 求解θ1(底座关节角度,用反正切)
float beta = atan2(dy, dx);
float alpha = acos((l1*l1 + r*r - l2*l2) / (2 * l1 * r));
targetTheta1 = beta - alpha;
// 转换弧度为度(VESC角度指令通常用度,需按需转换)
targetTheta1 = targetTheta1 * 180 / PI;
targetTheta2 = targetTheta2 * 180 / PI;
Serial.print("目标角度:θ1="); Serial.print(targetTheta1);
Serial.print("° θ2="); Serial.println(targetTheta2);
}
// ========== 向VESC发送关节目标角度(CANopen协议,简化版) ==========
void sendJointTarget() {
// CANopen协议中,目标角度通常以「目标位置」数据帧发送,格式为:
// ID:节点ID + 0x600(PDO发送ID),数据:6字节(32位浮点数,单位:度)
// 此处简化为发送3字节整数(示例,实际需按VESC CANopen手册配置)
// 关节1目标角度(角度值转换为0-360,避免溢出)
int theta1Target = (int)constrain(targetTheta1, 0, 360);
// 关节2目标角度
int theta2Target = (int)constrain(targetTheta2, 0, 360);
// 发送关节1目标(CAN数据帧:ID=JOINT1_ID,数据=theta1Target的字节)
byte data1[4] = {(byte)(theta1Target >> 8), (byte)theta1Target, 0, 0};
CAN.send(JOINT1_ID, data1, 4);
// 发送关节2目标
byte data2[4] = {(byte)(theta2Target >> 8), (byte)theta2Target, 0, 0};
CAN.send(JOINT2_ID, data2, 4);
Serial.println("已发送关节目标角度指令");
}
// ========== 读取关节实际角度(CAN反馈) ==========
void readJointActual() {
// VESC通过CAN反馈实际位置,格式为:ID=节点ID+0x580(PDO接收ID),数据:6字节浮点数
// 此处简化为接收数据并解析(示例,实际需按VESC反馈格式解析)
if (CAN.available()) {
byte id = CAN.getReceivedId();
byte len = CAN.getReceivedLength();
byte* data = CAN.getReceivedData();
if (id == JOINT1_ID) {
// 解析关节1实际角度(假设前2字节为整数角度)
actualTheta1 = (data[0] << 8) | data[1];
} else if (id == JOINT2_ID) {
actualTheta2 = (data[0] << 8) | data[1];
}
Serial.print("实际角度:θ1="); Serial.print(actualTheta1);
Serial.print("° θ2="); Serial.println(actualTheta2);
}
}
// ========== 辅助PID补偿(叠加角度修正) ==========
void pidCompensation() {
// 计算角度误差
pidError1 = targetTheta1 - actualTheta1;
pidError2 = targetTheta2 - actualTheta2;
// 防止误差过大导致超调
pidError1 = constrain(pidError1, -10, 10);
pidError2 = constrain(pidError2, -10, 10);
// PID计算(P:比例,I:积分,D:微分)
// 注:此处为简化PID,实际可优化为增量式PID,且积分项需限幅
float delta1 = Kp1 * pidError1 + Kd1 * (pidError1 - lastError1);
float delta2 = Kp2 * pidError2 + Kd2 * (pidError2 - lastError2);
// 将补偿量叠加到目标角度(发送给VESC)
// 实际应用中,可将delta转换为PWM信号或CAN指令的微调量
targetTheta1 += delta1 * 0.1; // 小幅度补偿,避免震荡
targetTheta2 += delta2 * 0.1;
lastError1 = pidError1;
lastError2 = pidError2;
}
// ========== 控制末端抓取器(舵机) ==========
void controlGripper(bool open) {
int angle = open ? 0 : 90; // 张开0°,闭合90°
analogWrite(GRIPPER_PIN, angle); // 简化PWM控制(实际用Servo库更准确)
Serial.print("抓取器状态:"); Serial.println(open ? "张开" : "闭合");
}
5、四足机器人关节腿(BLDC闭环控制-步态规划)
场景定位
小型四足机器人的关节腿闭环控制,通过反向动力学规划腿部关节角度,实现行走步态,核心需求是腿部关节根据步态时序动态调整目标角度,并通过BLDC闭环控制保证关节角度误差≤0.5°,避免行走晃动或关节失步。
核心控制逻辑
步态规划与IK求解:四足机器人采用三足步态,每个步态周期内,腿部需完成摆腿→落地→支撑→收腿四个动作,通过目标足端坐标,用平面单腿IK求解髋关节角度θ1、膝关节角度θ2;
多关节同步闭环:Arduino通过CAN总线向4个关节的VESC发送目标角度,VESC读取编码器反馈,执行PID控制,同时Arduino接收所有关节的反馈数据,实时监控步态状态;
步态时序控制:通过状态机实现步态切换,当足端压力传感器检测到落地时,切换为支撑相,调整关节角度,确保行走稳定。
// 四足机器人关节腿步态控制代码(BLDC闭环)
#include <CAN.h>
#include <Arduino.h>
// ========== 单腿结构参数(四足每条腿相同) ==========
const float legL1 = 12.0; // 髋关节到膝关节长度(cm)
const float legL2 = 10.0; // 膝关节到足端长度(cm)
const float PI = 3.1415926;
// ========== CAN通信配置 ==========
#define CAN_BAUD_RATE 500000
#define NUM_JOINTS 4 // 4个关节(2腿×2关节/腿)
// 关节ID映射:腿1髋关节=0x01,腿1膝关节=0x02,腿2髋关节=0x03,腿2膝关节=0x04
const byte jointIDs[NUM_JOINTS] = {0x01, 0x02, 0x03, 0x04};
// ========== 步态参数 ==========
const int stepFrequency = 1; // 步态频率(步/秒)
const float stepLength = 15.0; // 步长(cm)
const float stepHeight = 8.0; // 摆腿最大高度(cm)
// ========== 全局变量(关节目标与实际角度) ==========
float targetTheta[NUM_JOINTS] = {0}; // 4个关节的目标角度(度)
float actualTheta[NUM_JOINTS] = {0}; // 4个关节的实际反馈角度(度)
int gaitState = 0; // 步态状态:0=摆腿相,1=支撑相
unsigned long gaitTimer = 0;
const long stepPeriod = 1000 / stepFrequency; // 步态周期(ms)
// ========== 函数声明 ==========
void setupCAN();
void setupGait();
void gaitPlanning(); // 步态规划(生成目标足端坐标)
void singleLegIK(float x, float y, float& theta1, float& theta2); // 单腿IK求解
void sendAllJointTargets(); // 向所有关节发送目标角度
void readAllJointActual(); // 读取所有关节实际角度
void gaitStateMachine(); // 步态状态机切换
void setup() {
Serial.begin(115200);
Serial.println("四足关节腿初始化...");
setupCAN();
setupGait();
Serial.println("初始化完成,启动步态规划");
}
void loop() {
unsigned long now = millis();
1. 步态时序控制(按步态周期更新目标)
if (now - gaitTimer >= stepPeriod) {
gaitTimer = now;
gaitStateMachine(); // 切换步态状态
gaitPlanning(); // 生成新的目标足端坐标
}
2. 反向动力学求解关节角度
// 假设步态规划输出:腿1足端(x1,y1),腿2足端(x2,y2)
float x1 = 7.5, y1 = 15.0; // 示例:腿1支撑相足端坐标
float x2 = 12.5, y2 = 15.0; // 腿2摆腿相足端坐标
singleLegIK(x1, y1, targetTheta[0], targetTheta[1]); // 腿1关节1、2
singleLegIK(x2, y2, targetTheta[2], targetTheta[3]); // 腿2关节1、2
3. 发送目标角度到VESC
sendAllJointTargets();
4. 读取关节反馈
readAllJointActual();
5. 打印调试信息(步态周期内输出一次)
if (now - gaitTimer >= stepPeriod / 2) {
Serial.print("步态状态:"); Serial.println(gaitState == 0 ? "摆腿相" : "支撑相");
}
}
// ========== 初始化CAN(级联4个VESC驱动板) ==========
void setupCAN() {
CAN.begin(CAN_BAUD_RATE);
Serial.println("CAN初始化完成,支持4个关节级联");
}
// ========== 步态初始化(设置初始关节角度) ==========
void setupGait() {
// 初始姿态:所有关节角度为0(直立)
for (int i = 0; i < NUM_JOINTS; i++) {
targetTheta[i] = 0;
}
gaitState = 0;
gaitTimer = millis();
}
// ========== 步态状态机(切换摆腿相/支撑相) ==========
void gaitStateMachine() {
gaitState = (gaitState + 1) % 2; // 0和1交替切换
// 可根据足端压力传感器进一步确认状态(此处简化为时序切换)
// 若接入压力传感器,可在落地后检测到压力,触发状态切换
Serial.print("步态切换至:"); Serial.println(gaitState == 0 ? "摆腿相" : "支撑相");
}
// ========== 步态规划(生成目标足端坐标) ==========
void gaitPlanning() {
// 简化步态:摆腿相时,足端前伸+抬腿;支撑相时,足端后缩+落地
static float footPos1 = 0; // 腿1足端x坐标(周期性变化)
static float footPos2 = 0; // 腿2足端x坐标
// 按步态状态生成目标坐标
if (gaitState == 0) { // 摆腿相:前伸+抬腿
footPos1 += stepLength / (stepPeriod / 50); // 50ms更新一次,平滑过渡
footPos2 = footPos1 + stepLength;
} else { // 支撑相:后缩+落地
footPos1 -= stepLength / (stepPeriod / 50);
footPos2 = footPos1;
}
// 计算y坐标(摆腿相时抬腿,支撑相时落地)
float y1 = gaitState == 0 ? stepHeight + 10.0 : 10.0; // 落地高度
float y2 = gaitState == 0 ? 10.0 : stepHeight + 10.0;
// 此处仅作示例,实际需同步4条腿的坐标(四足步态为三足着地)
// 简化为两腿交替摆腿,实际可扩展为4腿步态规划
}
// ========== 单腿反向动力学(平面2连杆,足端(x,y)求关节角度) ==========
void singleLegIK(float x, float y, float& theta1, float& theta2) {
float r = sqrt(x*x + y*y);
// 工作空间校验
if (r > legL1 + legL2 || r < fabs(legL1 - legL2)) {
theta1 = 0;
theta2 = 0;
return;
}
// 求解θ2(膝关节角度)
float cosTheta2 = (x*x + y*y - legL1*legL1 - legL2*legL2) / (2 * legL1 * legL2);
cosTheta2 = constrain(cosTheta2, -1.0, 1.0);
theta2 = acos(cosTheta2) * 180 / PI;
// 求解θ1(髋关节角度)
float beta = atan2(y, x);
float alpha = acos((legL1*legL1 + r*r - legL2*legL2) / (2 * legL1 * r));
theta1 = (beta - alpha) * 180 / PI;
// 限制关节角度范围(髋关节:-45~45°,膝关节:0~135°)
theta1 = constrain(theta1, -45.0, 45.0);
theta2 = constrain(theta2, 0.0, 135.0);
}
// ========== 向所有关节发送目标角度(CAN级联发送) ==========
void sendAllJointTargets() {
for (int i = 0; i < NUM_JOINTS; i++) {
// 转换目标角度为整数(0-360)
int thetaTarget = (int)constrain(targetTheta[i], 0, 360);
byte data[4] = {(byte)(thetaTarget >> 8), (byte)thetaTarget, 0, 0};
CAN.send(jointIDs[i], data, 4);
}
Serial.println("已发送所有关节目标角度");
}
// ========== 读取所有关节实际角度(CAN级联接收) ==========
void readAllJointActual() {
while (CAN.available()) {
byte id = CAN.getReceivedId();
byte len = CAN.getReceivedLength();
byte* data = CAN.getReceivedData();
// 查找对应关节ID
for (int i = 0; i < NUM_JOINTS; i++) {
if (id == jointIDs[i]) {
actualTheta[i] = (data[0] << 8) | data[1];
Serial.print("关节"); Serial.print(i+1);
Serial.print("实际角度:"); Serial.println(actualTheta[i]);
break;
}
}
}
}
6、舵机+BLDC混合关节(舵机粗定位+BLDC闭环精调)
场景定位
需要大范围转动+高精度角度锁定的场景,比如云台跟踪、协作机械臂的关节,采用舵机实现大角度粗定位,BLDC驱动核心负载并实现高精度闭环控制,核心是舵机快速定位到目标范围,BLDC通过反向动力学计算精调角度,实现误差≤0.1°的高精度闭环。
核心控制逻辑
粗定位+精调分工:舵机负责大范围快速转动,使目标落入工作范围(误差≤10°),BLDC通过反向动力学计算精调角度,修正剩余误差;
目标角度计算:红外传感器检测目标方位,结合传感器安装位置,计算目标相对于关节的角度;
混合闭环控制:舵机开环控制(快速响应),BLDC位置闭环控制(高精度),Arduino实时读取编码器反馈,计算误差,输出BLDC控制指令。
// 舵机+BLDC混合关节控制代码(高精度角度闭环)
#include <Servo.h>
#include <CAN.h>
#include <Arduino.h>
// ========== 系统参数 ==========
const int SERVO_PIN = 9; // 舵机PWM引脚
#define CAN_BAUD_RATE 500000
#define BLDC_JOINT_ID 0x01 // BLDC关节的CAN ID
const float TARGET_ERROR = 0.1; // 目标跟踪误差≤0.1°
// ========== 全局变量 ==========
Servo servoMotor; // 舵机对象
float targetAngle = 0; // 目标总角度(舵机+BLDC共同指向)
float servoAngle = 0; // 舵机粗定位角度(0-180°)
float bldcTargetAngle = 0; // BLDC精调目标角度(剩余误差)
float bldcActualAngle = 0; // BLDC实际反馈角度
float angleError = 0; // 角度误差
// ========== 函数声明 ==========
void setupHardware();
void detectTarget(); // 检测目标并计算目标角度
void servoCoarsePosition(); // 舵机粗定位
void calculateBLDCTarget(); // 计算BLDC精调目标(反向动力学修正)
void bldcClosedLoop(); // BLDC闭环控制
void sendBLDCTarget(); // 发送BLDC目标角度
void readBLDCActual(); // 读取BLDC实际角度
void setup() {
Serial.begin(115200);
Serial.println("舵机+BLDC混合关节初始化...");
setupHardware();
servoMotor.attach(SERVO_PIN);
servoMotor.write(90); // 初始位置90°
Serial.println("初始化完成,开始检测目标");
}
void loop() {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate < 30) return; // 30ms更新一次,平衡响应与精度
lastUpdate = millis();
1. 检测目标,计算目标总角度
detectTarget();
if (targetAngle == 0) {
Serial.println("未检测到目标");
return;
}
2. 舵机粗定位(快速转到目标范围,误差≤10°)
servoCoarsePosition();
3. 计算BLDC精调目标(反向动力学修正剩余误差)
calculateBLDCTarget();
4. BLDC闭环控制(高精度跟踪)
bldcClosedLoop();
5. 打印调试信息
Serial.print("目标角度:"); Serial.print(targetAngle);
Serial.print("° 舵机角度:"); Serial.print(servoAngle);
Serial.print("° BLDC角度:"); Serial.print(bldcActualAngle);
Serial.print("° 误差:"); Serial.println(angleError);
// 达标判断:误差≤0.1°时停止调整
if (abs(angleError) <= TARGET_ERROR) {
Serial.println("角度闭环达标,跟踪稳定");
delay(100);
}
}
// ========== 初始化硬件(红外传感器、CAN) ==========
void setupHardware() {
pinMode(10, INPUT); // 红外传感器引脚
CAN.begin(CAN_BAUD_RATE);
Serial.println("硬件初始化完成");
}
// ========== 检测目标,计算目标角度(红外传感器示例) ==========
void detectTarget() {
int sensorState = digitalRead(10);
if (sensorState == HIGH) {
// 假设红外传感器安装位置固定,目标在正前方时角度为0°,左偏1°/像素,简化计算
// 实际可扩展为多个红外传感器,计算目标方位角
targetAngle = 45.0; // 示例:目标在左前方45°
} else {
targetAngle = 0;
}
}
// ========== 舵机粗定位(快速转到目标范围附近) ==========
void servoCoarsePosition() {
// 舵机粗定位逻辑:将目标角度映射到0-180°(舵机范围)
servoAngle = constrain(targetAngle, 0, 180);
// 舵机转动(开环控制,快速响应)
servoMotor.write((int)servoAngle);
delay(100); // 等待舵机到位(SG90转动时间约0.2s,简化处理)
}
// ========== 计算BLDC精调目标(反向动力学修正剩余误差) ==========
void calculateBLDCTarget() {
// 粗定位后,剩余误差为:目标角度 - 舵机角度
float remainingError = targetAngle - servoAngle;
// 反向动力学核心:通过剩余误差计算BLDC需要修正的角度
// 假设BLDC驱动的关节负责微调,修正范围0-10°(可根据实际结构调整)
// 简化为:剩余误差全部由BLDC修正,且修正角度不超过10°
bldcTargetAngle = constrain(remainingError, -10, 10);
// 若舵机已精准到位,BLDC目标为0
if (abs(remainingError) <= 0.5) {
bldcTargetAngle = 0;
}
Serial.print("剩余误差:"); Serial.print(remainingError);
Serial.print("° BLDC精调目标:"); Serial.println(bldcTargetAngle);
}
// ========== BLDC闭环控制(PID精调) ==========
void bldcClosedLoop() {
1. 发送BLDC目标角度
sendBLDCTarget();
2. 读取BLDC实际角度
readBLDCActual();
3. 计算角度误差
angleError = bldcTargetAngle - bldcActualAngle;
4. 增量式PID控制(高精度修正)
static float integral = 0;
static float lastError = 0;
const float Kp = 1.2, Ki = 0.05, Kd = 0.8; // PID参数(调试后确定)
float delta = Kp * angleError + Ki * integral + Kd * (angleError - lastError);
delta = constrain(delta, -5, 5); // 限制输出幅度,避免震荡
// 叠加修正到目标角度(实际通过CAN发送微调指令,此处简化为直接更新目标)
bldcTargetAngle += delta;
bldcTargetAngle = constrain(bldcTargetAngle, -10, 10);
// 积分限幅(避免积分饱和)
integral += angleError;
integral = constrain(integral, -10, 10);
lastError = angleError;
5. 发送修正后的目标角度
sendBLDCTarget();
}
// ========== 向BLDC发送目标角度(CAN协议) ==========
void sendBLDCTarget() {
int thetaTarget = (int)constrain(bldcTargetAngle, 0, 360);
byte data[4] = {(byte)(thetaTarget >> 8), (byte)thetaTarget, 0, 0};
CAN.send(BLDC_JOINT_ID, data, 4);
}
// ========== 读取BLDC实际角度(CAN反馈) ==========
void readBLDCActual() {
if (CAN.available()) {
byte id = CAN.getReceivedId();
byte len = CAN.getReceivedLength();
byte* data = CAN.getReceivedData();
if (id == BLDC_JOINT_ID && len >= 2) {
bldcActualAngle = (data[0] << 8) | data[1];
// 转换为有符号角度(-180~180)
if (bldcActualAngle > 180) bldcActualAngle -= 360;
}
}
}
要点解读
- 反向动力学求解的“场景适配性”:不同结构用不同算法,精度与效率平衡
反向动力学是关节控制的“大脑”,需根据关节结构选择算法,避免盲目套用公式,否则会导致计算误差或超时:
简单平面结构(案例4、5):2自由度连杆优先用解析法(余弦定理+反三角函数),计算速度快(微秒级),适合Arduino实时求解,精度依赖结构参数标定(如连杆长度需实际测量,而非理论值),案例中通过constrain函数避免浮点误差导致的acos参数超范围;
复杂空间结构:若为6自由度空间臂、球面关节,解析法公式复杂,需用数值迭代法(如雅可比迭代、Newton-Raphson),虽然计算耗时略长,但Arduino Mega等大内存芯片可胜任,需设置迭代次数和误差阈值,避免无限循环;
精度前置校验:求解前必须做工作空间校验,案例1中通过判断目标点是否在连杆可达范围内,避免无效解,防止关节角度输出为NaN,导致电机失控,这是工业场景的必备安全逻辑。 - BLDC关节的“闭环控制层设计”:驱动板与Arduino分工,避免资源冲突
BLDC的闭环控制需明确Arduino与驱动板的分工,避免两者重复做闭环导致震荡,同时保证控制精度:
驱动板的核心角色:VESC等专业驱动板内置电流环、速度环,支持位置环,Arduino无需实现底层PID,只需发送目标角度指令,驱动板自动完成“目标角度→电机转动→编码器反馈→PID修正”的闭环,案例1、2中通过CAN协议发送目标角度,依赖驱动板的位置闭环,简化Arduino代码;
Arduino的辅助闭环:案例4中的辅助PID补偿,是叠加在驱动板目标角度上的“上层修正”,用于补偿驱动板内置PID的稳态误差,两者是“主从关系”,而非重复闭环,避免输出指令冲突,确保最终角度误差控制在0.1°以内;
反馈精度的核心保障:编码器分辨率直接决定闭环精度,案例5中选用14位AS5048编码器,分辨率0.022°,配合VESC的高频率闭环响应,才能实现0.5°的关节误差控制,普通3线霍尔传感器分辨率低,无法满足高精度需求。 - 多关节/多任务的“同步与通信设计”:CAN总线级联+时序调度,避免数据阻塞
多关节系统的核心痛点是通信冲突和任务不同步,需从通信方式和时序两方面优化:
通信协议选择:多关节优先用CAN总线,而非串口,CAN支持多节点级联,每个关节对应独立ID,发送/接收互不干扰,案例5中4个关节通过1个CAN口级联,通信延迟低,且可通过ID快速识别数据来源,串口为点对点通信,多关节需切换引脚,效率极低;
时序调度逻辑:代码中通过millis()计算时间间隔,设置合理的更新频率,比如电机控制10-20Hz,传感器采集1-2Hz,避免高频发送CAN指令导致总线阻塞,同时防止Arduino CPU过载,多关节同步发送时,按ID顺序依次发送,而非同时发送,CAN总线仲裁机制可自动处理冲突;
数据格式标准化:所有关节的目标角度、反馈角度需采用统一的数据格式,比如32位浮点数或16位整数,避免字节解析错误,案例中简化为2字节整数,实际需按VESC CANopen协议配置标准PDO格式,确保数据兼容性。 - 闭环系统的“误差补偿机制”:从参数标定到PID优化,解决稳态误差
反向动力学与闭环控制的精度瓶颈是误差,需从参数、算法、反馈三方面建立补偿机制:
结构参数标定:连杆长度、关节零点是基础参数,标定误差会直接传递到末端,比如案例4中连杆长度理论20cm,实际测量可能19.8cm,需通过“末端触碰已知点,反向计算参数误差”的方式标定,代码中可将标定参数设为全局变量,方便调整;
关节零点校准:每次上电后,需控制关节转动到机械零点,读取编码器零点值,将后续角度统一以零点为基准,避免零点漂移,案例5中可在初始化时发送零点指令,驱动板自动记录零点,无需手动校准;
PID参数调优:闭环控制的核心是PID参数,比例增益负责响应速度,积分增益消除稳态误差,微分增益抑制超调,初始参数可设为经验值,然后通过逐步增大比例增益,观察阶跃响应,避免震荡,再调整积分增益,微分增益,案例3中采用增量式PID,叠加微量修正,避免PWM突变,确保平滑跟踪。 - 系统可靠性的“安全与容错设计”:从硬件保护到软件逻辑,防止失控
关节控制涉及机械运动,可靠性是底线,需从硬件和软件双重防护:
硬件层面防护:BLDC电机需搭配过流保护的驱动板,防止堵转过流烧毁电机,编码器线缆采用屏蔽线,避免工业环境中的电磁干扰导致反馈数据错误,电源端加滤波电容,防止电压波动导致驱动板重启;
软件层面限幅与校验:对目标角度、控制输出做限幅,比如案例4中关节角度限制在0-360°,避免角度溢出导致电机超限转动,CAN数据接收后做长度校验,防止解析错误数据,目标点超出工作空间时,输出默认安全角度,电机停止,避免机械碰撞;
故障自恢复逻辑:当编码器反馈失效或通信中断时,软件需检测到错误,自动切换为开环控制,输出低PWM让关节缓慢回零,同时通过串口报警,案例5中若CAN通信超时,可设置标志位,停止发送目标指令,驱动板内置的过流保护会触发电机停止,防止失控。
注意,以上案例只是为了拓展思路,仅供参考。它们可能有错误、不适用或者无法编译。您的硬件平台、使用场景和Arduino版本可能影响使用方法的选择。实际编程时,您要根据自己的硬件配置、使用场景和具体需求进行调整,并多次实际测试。您还要正确连接硬件,了解所用传感器和设备的规范和特性。涉及硬件操作的代码,您要在使用前确认引脚和电平等参数的正确性和安全性。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)