四旋翼无人机姿态控制详解
一、通俗易懂的解释
想象你正在骑一辆没有车把的自行车,要保持平衡需要不断调整身体重心。四旋翼无人机也是如此,它需要实时感知自己的倾斜角度,并通过调整四个电机转速来保持平衡。
- 欧拉角:就像你用手机查看地图时看到的"方向朝北",告诉无人机当前是倾斜还是正直
- 陀螺仪:就像你转动手机时屏幕自动旋转的感应器,能感知无人机转动的速度
- PID控制:就像骑车时不断微调身体姿势,太歪就往回掰一点,不够就再加点力
四个电机分工合作:
- 1号和3号电机为一组(对角线)
- 2号和4号电机为另一组(对角线)
当无人机向右倾斜时:
- 增加2号和4号电机转速(让右侧升力更大)
- 减少1号和3号电机转速(让左侧升力减小)
- 这样合力就会把无人机"拉回"水平位置
二、抽象理解
数学模型
无人机姿态控制可以简化为三个控制环:
- Roll轴(X轴):左右倾斜控制
- Pitch轴(Y轴):前后倾斜控制
- Yaw轴(Z轴):旋转控制
每个轴都采用PID控制器,形成串级控制结构:
- 内环:角速度控制(快速响应)
- 外环:角度控制(稳定目标)
力学原理
四个电机产生的升力矢量合成决定无人机的姿态:
- 当Roll角为正(右倾)时,需要增加左侧电机转速,减少右侧电机转速
- Yaw控制通过调整对角电机转速差实现旋转
三、实现的原理
1. 传感器数据融合
使用互补滤波或卡尔曼滤波将陀螺仪和加速度计数据融合,得到准确的欧拉角。
2. PID控制算法
输出 = Kp*误差 + Ki*积分误差 + Kd*微分误差
- P(比例):偏差越大,纠正力度越强
- I(积分):累积偏差消除静差(如长时间偏移)
- D(微分):预测偏差趋势提前干预(抑制震荡)
3. 控制流程
传感器数据 → 姿态解算 → PID计算 → 电机控制 → 反馈调节
四、实现代码示例
#include <PID_v1.h>
// 定义PID参数
double rollKp = 2.0, rollKi = 0.0, rollKd = 1.0;
double pitchKp = 2.0, pitchKi = 0.0, pitchKd = 1.0;
double yawKp = 3.0, yawKi = 0.0, yawKd = 0.5;
// 创建PID控制器
PID rollPID(&rollInput, &rollOutput, &rollSetpoint, rollKp, rollKi, rollKd, DIRECT);
PID pitchPID(&pitchInput, &pitchOutput, &pitchSetpoint, pitchKp, pitchKi, pitchKd, DIRECT);
PID yawPID(&yawInput, &yawOutput, &yawSetpoint, yawKp, yawKi, yawKd, DIRECT);
void setup() {
// 初始化PID
rollPID.SetMode(AUTOMATIC);
pitchPID.SetMode(AUTOMATIC);
yawPID.SetMode(AUTOMATIC);
// 设置输出范围
rollPID.SetOutputLimits(-500, 500);
pitchPID.SetOutputLimits(-500, 500);
yawPID.SetOutputLimits(-500, 500);
}
void loop() {
// 1. 读取传感器数据(假设已获取)
rollInput = getRollAngle(); // 获取当前Roll角度
pitchInput = getPitchAngle(); // 获取当前Pitch角度
yawInput = getYawAngle(); // 获取当前Yaw角度
// 2. PID计算
rollPID.Compute(); // 计算Roll轴控制量
pitchPID.Compute(); // 计算Pitch轴控制量
yawPID.Compute(); // 计算Yaw轴控制量
// 3. 电机控制
int motor1 = baseThrottle + rollOutput - pitchOutput + yawOutput; // 对角线电机
int motor2 = baseThrottle - rollOutput - pitchOutput - yawOutput;
int motor3 = baseThrottle + rollOutput + pitchOutput - yawOutput;
int motor4 = baseThrottle - rollOutput + pitchOutput + yawOutput;
// 4. 限制电机转速范围
motor1 = constrain(motor1, 1000, 2000);
motor2 = constrain(motor2, 1000, 2000);
motor3 = constrain(motor3, 1000, 2000);
motor4 = constrain(motor4, 1000, 2000);
// 5. 输出PWM信号
setMotorPWM(1, motor1);
setMotorPWM(2, motor2);
setMotorPWM(3, motor3);
setMotorPWM(4, motor4);
}
五、实际应用和场景
1. 航拍无人机
- 保持相机平稳,抵消风扰
- 实现自动航点飞行
- 精准悬停拍摄
2. 物流配送
- 稳定飞行确保包裹安全
- 精准降落至指定位置
- 避障飞行
3. 农业植保
- 平稳飞行喷洒农药
- 覆盖均匀,不漏喷不重喷
- 自动规划航线
4. 搜索救援
- 稳定悬停观察灾区
- 长时间续航巡逻
- 夜间红外成像
六、如何使用
1. 硬件准备
- 四旋翼无人机机架
- STM32/Arduino飞行控制器
- MPU6050姿态传感器
- 无刷电机和电调
- 电池和电源管理系统
2. 软件设置
传感器校准:
- 水平放置无人机,校准加速度计
- 旋转无人机校准陀螺仪
PID参数整定:
- 先调Roll/Pitch轴
- 再调Yaw轴
- 使用Ziegler-Nichols法或试凑法
测试飞行:
- 手动模式测试基本控制
- 稳定模式测试悬停能力
- 自动模式测试航线规划
3. 参数调整技巧
- P值过大:飞行器抖动严重,像喝醉一样
- I值过大:长时间偏移后可能失控
- D值过大:飞行器反应过度,出现震荡
4. 高级功能实现
- 自动返航:当电量低或信号丢失时自动返回起飞点
- 路径规划:按预设航线飞行,自动避开障碍物
- 目标跟踪:锁定特定目标并跟随移动
5. 安全注意事项
- 初次飞行在空旷无人的场地
- 设置低高度限制(如2米)
- 准备紧急开关(如遥控器一键降落)
- 避免在强风或雨天飞行
好的,遵照您的要求,以下是关于“无人机电机线性化函数”的详细阐述文档。
核心知识点:无人机电机线性化函数
1. 通俗易懂的解释
想象一下你正在拧一个水龙头来控制水流大小。理想情况下,你把手柄转到一半,水就流出一半;转到头,水就最大。但有些水龙头不是这样的:可能一开始转一点点几乎没水,然后转到某个位置水突然就变得很大。
在无人机上,我们用控制器(比如遥控器上的油门杆)来控制电机的转速和产生的升力(推力),这就像控制水龙头一样。我们希望油门杆推到一半时,电机产生大约一半的最大推力,这样才能方便我们稳定控制无人机的高度和运动。
但是,无人机上的电机(通常是无刷直流电机 BLDC)通过电子调速器(ESC)接收一个信号(比如 PWM 脉冲宽度),将这个信号转化为电机转速。不幸的是,这个“信号”到“推力”的关系往往不是直线的(线性的)。比如,给 ESC 50%的信号,电机产生的推力可能只有最大推力的 30%;而从 70%信号到 80%信号,推力可能增加得非常快。
“电机线性化函数”就像是在你控制油门杆和你发送给 ESC 的实际信号之间加了一个“智能转换器”。你推油门杆(输入一个你期望的“线性”推力百分比),这个转换器就会根据电机的实际特性,计算出一个非线性的 ESC 信号值,然后把这个信号发给 ESC。这样一来,尽管电机本身的特性是非线性的,但从你控制油门杆的角度来看,推力和你的油门位置就呈现出一种线性的、容易预测的关系了。
现实例子:
你想让无人机悬停在某一高度。你的飞控系统计算出需要提供 50% 的总推力来抵消重力。如果没有线性化,飞控可能会直接给电机发送 50% 的 PWM 信号。结果电机的推力可能不足 50%(比如只有 30%),无人机就开始下降。有了线性化函数,飞控会先查一下“线性推力 50%”应该对应多少实际的 PWM 信号(比如查到是 70%),然后发送 70% 的 PWM 给 ESC。这样,电机产生的推力就更接近你想要的 50%,飞控就能更准确地保持高度。
2. 抽象理解
共性 (Abstract Understanding):
电机线性化函数是系统输入/输出映射的非线性补偿的一种具体应用。在很多控制系统中,我们希望被控对象的输出与输入信号之间是线性的或具有简单的数学关系,以便于设计和分析控制器(如 PID 控制器)。然而,物理系统往往存在各种非线性,例如饱和、死区、摩擦、平方关系等。线性化就是通过一个预处理或后处理环节,将原始的非线性关系转换为近似的线性关系,或者说,提供一个逆向的非线性映射来抵消系统的固有非线性。
对于无人机电机,核心是补偿ESC接收信号(如PWM)与电机推力或转速之间的非线性关系。常见的非线性来源包括:
- 电机的物理特性。
- 螺旋桨的空气动力学特性(推力大致与转速的平方成正比,F∝ω2)。
- ESC 的内部处理。
- 低油门时的死区效应。
潜在问题 (Potential Issues):
- 精确性问题: 线性化函数的效果取决于标定的精确度。如果用于建立线性化模型的数据不够准确或不够密集,得到的函数就无法完美匹配电机的实际特性。
- 环境和工况依赖性: 电机的推力/转速特性会受到多种因素影响,如电池电压、环境温度、空气密度、螺旋桨状况(磨损、损坏)等。静态的线性化函数可能无法适应所有这些变化,导致在不同工况下线性化效果下降。
- 个体差异: 即使是同型号的电机和螺旋桨,也可能存在细微的个体差异。通用的线性化函数可能不完全适用于每一台具体的电机,最理想情况下需要对每台电机进行单独标定(但这在实际应用中通常不现实)。
- 动态特性: 线性化函数主要处理的是稳态的输入-输出关系(即给定一个信号,最终稳定下来的推力是多少)。它通常不考虑电机响应速度、加速度限制等动态过程中的非线性。
3. 实现的原理
电机线性化函数的主要原理是建立一个从期望的线性输出值到实际需要的非线性输入值的映射。这个映射的建立通常分以下几步:
数据采集 (System Identification):
- 将电机固定在测试台上,配备推力传感器(或转速传感器)。
- 逐步增加输入信号(如 ESC 的 PWM 占空比),从 0% 到 100%,记录每个输入值对应的稳态推力(或转速)。
- 得到一系列数据点:(Input1,Output1),(Input2,Output2),…,(Inputn,Outputn)。这些点构成了输入到输出的实际非线性曲线。
建立反向映射 (Inverse Mapping):
- 我们的目标是:给定一个期望的线性输出值(比如期望的总推力占最大推力的百分比),能找到需要给电机的实际输入信号值。
- 我们将原始数据点翻转,得到 (Output1,Input1),(Output2,Input2),…,(Outputn,Inputn)。这些点构成了从实际输出到所需输入的曲线。
- 通常,我们会对输出值进行归一化,比如将其表示为最大推力的百分比(0% 到 100%)。这样数据点就变成 (Normalized_Output1,Input1),…。
生成线性化函数 (Generating the Linearization Function):
- 查表法 (Lookup Table): 这是最常见且灵活的方法。
- 直接存储一系列关键点 (Normalized_Output,Required_Input)。
- 当需要某个不在表中的期望输出值时,使用插值(如线性插值)来估算所需的输入值。例如,如果表中有 (40%, 60% PWM) 和 (60%, 75% PWM),当期望 50% 线性推力时,通过线性插值计算出所需的 PWM 值。
- 函数拟合法 (Function Fitting):
- 尝试用一个数学函数(如多项式,常见的是二次或三次多项式)来拟合反向映射的数据点。
- 例如,拟合出一个函数 PWM=flinearize(Desired_Linear_Thrust_Percentage)。
- 在运行时,直接调用这个函数计算所需的 PWM 值。
- 查表法 (Lookup Table): 这是最常见且灵活的方法。
在飞控系统中,线性化函数通常被实现为一个查找表或一个简单的多项式计算,将飞控内部计算出的“期望归一化推力”(通常是一个 0 到 1 的值,表示占最大推力的比例)转换为发送给 ESC 的 PWM 脉冲宽度值(通常也是一个 0 到 1 的比例或具体的微秒值)。
例如,如果飞控计算出需要 60% 的推力,它会将这个 0.6 的值输入到线性化函数中。线性化函数查表或计算后,可能会返回 0.72,表示需要发送 72% 的 PWM 占空比给 ESC 才能获得接近 60% 的推力。
4. 实现代码 (示例)
以下是一个使用 Python 演示的简单查表法和线性插值的线性化示例。
Python
import matplotlib.pyplot as plt
import numpy as np
# 假设通过实验测量得到的数据点 (输入 PWM %, 实际推力 % of max)
# 注意:真实数据可能更复杂,这里简化
measured_data = [
(0, 0),
(10, 1), # 死区或非常低推力
(20, 3),
(30, 7),
(40, 12),
(50, 20),
(60, 30),
(70, 45),
(80, 65),
(90, 85),
(100, 100)
]
# 提取数据点
pwm_inputs = np.array([d[0] for d in measured_data])
actual_thrust_outputs = np.array([d[1] for d in measured_data])
# 建立反向映射的查找表:(期望线性推力 %, 需要的 PWM %)
# 注意:期望线性推力是飞控“认为”的理想推力输出,这里我们用实际推力作为这个“期望”的基准,
# 但在实际应用中,线性化函数是用来让“期望的线性输入”产生对应的“实际输出”。
# 所以查找表应该是 (实际推力 %, 对应的 PWM %)
linearization_lookup_table = []
for i in range(len(measured_data)):
# 将实际推力作为我们“期望的线性推力”的一个参考点
linearization_lookup_table.append((actual_thrust_outputs[i], pwm_inputs[i]))
# 打印查找表 (Desired Linear Thrust %, Required PWM %)
print("线性化查找表 (期望线性推力 % -> 需要的 PWM %):")
for entry in linearization_lookup_table:
print(f" {entry[0]:.2f}% -> {entry[1]:.2f}%")
# 实现线性化函数(使用线性插值)
def linearize_thrust_command(desired_linear_thrust_percent, lookup_table):
"""
根据期望的线性推力百分比,使用查找表和线性插值计算所需的 PWM 百分比。
Args:
desired_linear_thrust_percent (float): 期望的线性推力百分比 (0-100)。
lookup_table (list): 线性化查找表 [(thrust_percent, pwm_percent), ...]。
Returns:
float: 需要发送给 ESC 的 PWM 百分比 (0-100)。
"""
# 确保输入在有效范围内
if desired_linear_thrust_percent <= lookup_table[0][0]:
return lookup_table[0][1] # 小于或等于最小推力,返回最小PWM
if desired_linear_thrust_percent >= lookup_table[-1][0]:
return lookup_table[-1][1] # 大于或等于最大推力,返回最大PWM
# 在查找表中找到包含 desired_linear_thrust_percent 的两个点
x0, y0 = lookup_table[0] # (thrust, pwm)
for i in range(1, len(lookup_table)):
x1, y1 = lookup_table[i]
if desired_linear_thrust_percent <= x1:
# 找到了区间 [x0, x1],进行线性插值
# 插值公式: y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
required_pwm_percent = y0 + (desired_linear_thrust_percent - x0) * (y1 - y0) / (x1 - x0)
return required_pwm_percent
x0, y0 = x1, y1 # 更新点,继续查找
# 理论上不会到达这里,除非输入超出范围且没被上面的if捕获
return lookup_table[-1][1] # 默认返回最大值
# --- 示例使用 ---
desired_thrusts = np.linspace(0, 100, 20) # 从 0% 到 100% 期望线性推力
required_pwms = [linearize_thrust_command(dt, linearization_lookup_table) for dt in desired_thrusts]
print("\n--- 线性化效果示例 ---")
for i in range(len(desired_thrusts)):
print(f"期望线性推力: {desired_thrusts[i]:.2f}% -> 需要的 PWM: {required_pwms[i]:.2f}%")
# --- 绘图对比 ---
plt.figure(figsize=(10, 6))
# 原始电机特性曲线 (PWM -> 推力)
plt.plot(pwm_inputs, actual_thrust_outputs, 'o-', label='原始电机特性 (PWM -> 推力)', color='red')
# 线性化后的输入/输出关系 (期望线性推力 -> 需要的 PWM)
desired_linear_inputs_for_plot = np.array([entry[0] for entry in linearization_lookup_table])
required_pwms_for_plot = np.array([entry[1] for entry in linearization_lookup_table])
plt.plot(desired_linear_inputs_for_plot, required_pwms_for_plot, 's-', label='线性化函数 (期望线性推力 -> 需要的 PWM)', color='blue')
# 理想的线性关系 (期望线性推力 -> 同样的百分比)
plt.plot(np.linspace(0, 100, 100), np.linspace(0, 100, 100), '--', label='理想线性关系', color='gray')
plt.xlabel("输入百分比 (%)")
plt.ylabel("输出百分比 (%)")
plt.title("电机特性与线性化函数对比")
plt.legend()
plt.grid(True)
plt.show()
# 验证线性化效果:将线性化后的 PWM 输入到原始电机模型中,看输出是否接近期望线性值
# 注意:这里我们没有原始电机的连续数学模型,只能用测量点近似。
# 更精确的做法是用原始数据点进行插值来模拟原始电机
def simulate_motor_output(pwm_percent, measured_pwm, measured_thrust):
# 使用测量点进行线性插值模拟原始电机输出
return np.interp(pwm_percent, measured_pwm, measured_thrust)
simulated_outputs_after_linearization = [simulate_motor_output(pwm, pwm_inputs, actual_thrust_outputs) for pwm in required_pwms]
print("\n--- 验证线性化效果 (期望线性推力 -> 线性化PWM -> 模拟实际推力) ---")
for i in range(len(desired_thrusts)):
print(f"期望线性推力: {desired_thrusts[i]:.2f}% -> 线性化PWM: {required_pwms[i]:.2f}% -> 模拟实际推力: {simulated_outputs_after_linearization[i]:.2f}%")
# 绘制线性化后的端到端关系 (期望线性推力 -> 模拟实际推力)
plt.figure(figsize=(8, 5))
plt.plot(desired_thrusts, simulated_outputs_after_linearization, 'x-', label='线性化后的端到端关系', color='green')
plt.plot(np.linspace(0, 100, 100), np.linspace(0, 100, 100), '--', label='理想线性关系', color='gray')
plt.xlabel("期望线性推力 (%)")
plt.ylabel("模拟实际推力 (%)")
plt.title("线性化效果验证")
plt.legend()
plt.grid(True)
plt.show()
代码解释:
measured_data
: 模拟通过实验测得的 PWM 输入与实际推力输出(占最大值的百分比)的关系。可以看到,当 PWM 从 0 到 50 时,推力增长缓慢,而 50 以后增长较快,这是典型的非线性。linearization_lookup_table
: 构建了一个反向查找表,其结构是(实际推力百分比, 对应的 PWM 百分比)
。这个表是线性化函数的核心。linearize_thrust_command
函数:实现了查找和线性插值逻辑。给定一个期望的线性推力百分比,它在linearization_lookup_table
中找到其所在的区间,并计算出所需的 PWM 值。- 绘图:
- 第一张图展示了原始的电机特性曲线(PWM 到推力)以及线性化函数本身(期望线性推力到所需 PWM)。可以看出线性化函数是原始曲线的一种“逆向”形态。
- 第二张图通过将线性化函数计算出的 PWM 值再输入到模拟的原始电机模型中,来验证最终的“期望线性推力”到“模拟实际推力”的关系是否接近理想的线性关系。可以看到,经过线性化后,最终的推力输出与期望的线性输入之间的关系变得更加接近直线。
5. 实际应用和场景
电机线性化函数在需要对多旋翼无人机进行精确控制的各种场景中都至关重要:
- 无人机飞控系统 (Flight Controllers): 这是最主要的应用场景。Pixhawk (运行 ArduPilot 或 PX4), Betaflight, Cleanflight 等主流开源和商业飞控软件内部都实现了某种形式的电机线性化或推力/转速映射功能。它使得上层的姿态控制器、位置控制器等能够基于一个更加线性的推力/转速模型进行设计和调参,简化了控制律的设计,提高了控制性能和鲁棒性。
- 精确悬停和高度保持 (Precise Hover & Altitude Hold): 线性化确保了在悬停附近小范围调整油门时,推力的变化是可预测且平滑的,这对于维持稳定的高度至关重要。
- 定点和路径跟踪 (Position Holding & Path Following): 在需要无人机精确停留在某个位置或沿着预定路径飞行时,飞控需要精确控制各个电机的推力差异来产生所需的力和力矩。线性化保证了推力控制的准确性。
- 特技飞行和响应性 (Acrobatic Flight & Responsiveness): 虽然有些特技飞行模式(如 Rate 模式)直接控制角速率,对推力线性度要求相对较低,但在使用油门进行高度或速度控制时,线性化的油门响应能提供更直观和可预测的控制手感。
- 大型和工业无人机 (Large & Industrial Drones): 对于载重或执行精密任务(如测绘、喷洒)的无人机,推力输出的准确性和一致性直接影响作业效果。线性化是实现高性能控制的基础。
- 无人机仿真 (Drone Simulation): 在开发和测试飞控算法时,需要准确模拟电机的推力特性。实现电机线性化模型能够使仿真更加逼真,从而提高仿真结果的可信度。
总而言之,电机线性化函数是连接飞控系统控制逻辑与电机实际输出之间的重要桥梁,它通过补偿非线性,使得复杂的无人机动力学控制变得更加容易、精确和稳定。