Руководство по управлению квадрокоптером
Введение
Это руководство от математических основ шаг за шагом строит полную систему управления квадрокоптером. Вот основные задачи глав и их взаимосвязь:
| Глава | Основная задача | Связь с другими главами |
|---|---|---|
| 1. Системы координат и основы вращения | Как описать ориентацию дрона в трёхмерном пространстве? | Математическая база, нужна всем следующим главам |
| 2. Динамическое моделирование | Как силы и моменты приводят дрон в движение? Каковы уравнения движения? | Опирается на главу 1 (матрицу вращения); даёт модель объекта управления для всех регуляторов |
| 3. Дифференциальная плоскостность | Можно ли спланировать только траекторию и вывести по ней все состояния и входы? | Опирается на главу 2 (уравнения Ньютона); мост между «планированием траектории» и «управлением» |
| 4. Основы PID-управления | Как параметры регулятора влияют на отклик системы? | Введение в теорию управления; отправная точка всех последующих методов |
| 5. Управление на основе модели | Как использовать информацию о модели для улучшения управления? (2D) | Опирается на главу 2 (линеаризованную модель) и главу 4 (PID); добавляет структуру «прямая связь + обратная связь» |
| 6. Отслеживание трёхмерной траектории | Как обобщить 2D-управление на полноценный 3D-полёт? | Трёхмерное расширение главы 5; добавлена обработка связи через угол рыскания |
| 7. Управление на SE(3) | Как при больших углах манёвра избежать карданного замка эйлеровых углов? | Опирается на главу 3 (построение матрицы ориентации); нелинейная альтернатива главе 6 |
| 8. MPC-управление | Как одновременно выполнять локальное планирование и управление при наличии ограничений? | Объединяет планирование и управление; опирается на главу 2 (прогнозную модель) |
Рекомендации по чтению:
- Быстрое знакомство (понять базовое управление): 1 → 2 → 4 → 5
- Полное изучение: по порядку 1 → 8
- Тематическое чтение: глава 3 (дифференциальная плоскостность) — необходимая предпосылка для руководства по планированию траекторий
Руководство по управлению (8 блоков кода):
- Системы координат и вращение — визуализация трёхмерного вращения: «крест» дрона вокруг осей Z/Y/X
- Динамика — по заданным частотам вращения четырёх моторов вычисляются тяга и момент, визуализируется направление тяги
- Дифференциальная плоскостность — спиральная траектория → вывод углов ориентации и тяги по плоскому выходу
- PID-управление — сравнение влияния , , на ступенчатый отклик системы второго порядка
- Управление на основе модели — моделирование в плоскости Y-Z двумерного дрона: прямая связь + PD-управление
- Трёхмерное отслеживание траектории — отслеживание траектории в форме восьмёрки, сравнение ожидаемого и фактического
- Управление на SE(3) — большой переворот: сравнение ошибок управления по эйлеровым углам и на SE(3)
- MPC-управление — MPC по высоте (1D), показаны прогнозный горизонт и обработка ограничений
1. Системы координат и основы вращения
Прежде чем изучать квадрокоптер, нужно разобраться в преобразованиях систем координат и матрицах вращения — это математическая основа описания ориентации дрона.
1.1 Геометрический вывод поворота двумерных координат
Рассмотрим точку на плоскости . Когда система координат поворачивается на угол , координаты точки в новой системе равны .
Способ 1: геометрическая проекция
Пусть исходная система координат — , новая — (поворот против часовой стрелки на ). Из геометрических соотношений проекций:
В матричной форме:
Способ 2: полярные координаты
Пусть расстояние от точки до начала координат равно , а угол с осью равен . После поворота:
В матричной форме:
Пояснение: Способ 2 — это поворот точки вокруг начала координат против часовой стрелки на угол , поэтому матрица поворота здесь транспонирована по отношению к способу 1 (поворот системы координат).
1.2 Два режима поворота: отличия
Режим 1: поворот осей координат (точка неподвижна, система координат поворачивается)
Когда исходная система координат неподвижна, а её оси поворачиваются, находим координаты точки в новой системе:
Режим 2: поворот точки (система координат неподвижна, точка поворачивается)
Пояснение: Координаты точки в новой системе равны тому, что оси координат повернуты по часовой стрелке на . Представьте, что вы стоите на оси координат и вращаетесь вместе с ней: точки вокруг меняют положение относительно вас.
Пояснение: Матрицы двух режимов транспонированы друг другу. Режим 1 можно понимать как «активный поворот системы координат», режим 2 — как «активный поворот точки». В задачах дрона обычно используется режим 1: известно, насколько повернулся корпус, и нужно выразить ту же точку в новой системе.
1.3 Трёхмерные матрицы вращения
Обобщим двумерный случай на три измерения: вращение вокруг каждой из трёх осей.
Вращение вокруг оси (рыскание, угол ):
Вращение вокруг оси (тангаж, угол ):
Вращение вокруг оси (крен, угол ):
1.4 Преобразования систем координат дрона
Пояснение: Для дрона применим «поворот системы координат»: система координат корпуса повёрнута относительно мировой, и нужно выразить вектор из системы корпуса в мировой системе.
Таким образом, поворот собственной системы координат задаётся как .
Если известна относительно , то можно найти обратным расчётом из . Но в этом случае поворот относительно мировой системы.
Пояснение: В приложениях к дронам матрица вращения обычно описывает преобразование из системы корпуса (Body Frame) в мировую (World Frame): . Обратное преобразование (мир → корпус) равно , потому что матрица вращения ортогональна: .
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# ========== Визуализация вращения «крестообразного» дрона вокруг осей Z / Y / X ==========
# --- Строим «крест» дрона (4 конца луча + центр) ---
L = 1.0 # длина луча
drone = np.array([
[ L, 0, 0], # правый конец
[-L, 0, 0], # левый конец
[ 0, L, 0], # передний конец
[ 0,-L, 0], # задний конец
[ 0, 0, 0], # центр
]).T # матрица 3×5, каждый столбец — одна точка
# --- Трёхмерные матрицы вращения ---
def Rz(psi):
"""Вращение вокруг оси Z (угол рыскания ψ)"""
c, s = np.cos(psi), np.sin(psi)
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
def Ry(theta):
"""Вращение вокруг оси Y (угол тангажа θ)"""
c, s = np.cos(theta), np.sin(theta)
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
def Rx(phi):
"""Вращение вокруг оси X (угол крена φ)"""
c, s = np.cos(phi), np.sin(phi)
return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
angle = np.radians(45) # поворот на 45°
# --- Рисуем 4 подграфика: исходное положение + 3 поворота ---
titles = ['Исходная ориентация', f'Поворот вокруг Z на {int(np.degrees(angle))}° (рыскание)',
f'Поворот вокруг Y на {int(np.degrees(angle))}° (тангаж)',
f'Поворот вокруг X на {int(np.degrees(angle))}° (крен)']
rotations = [np.eye(3), Rz(angle), Ry(angle), Rx(angle)]
fig = plt.figure(figsize=(14, 10))
for i, (R, title) in enumerate(zip(rotations, titles)):
ax = fig.add_subplot(2, 2, i + 1, projection='3d')
pts = R @ drone # координаты точек после поворота
# Рисуем два луча (крест)
ax.plot([pts[0,0], pts[0,1]], [pts[1,0], pts[1,1]],
[pts[2,0], pts[2,1]], 'b-o', lw=3, label='Луч 1')
ax.plot([pts[0,2], pts[0,3]], [pts[1,2], pts[1,3]],
[pts[2,2], pts[2,3]], 'r-o', lw=3, label='Луч 2')
# Отмечаем центр
ax.scatter(*pts[:, 4], color='k', s=80, zorder=5)
# Рисуем повёрнутые оси системы корпуса
axis_len = 0.6
for j, (color, lbl) in enumerate(zip(['r','g','b'], ['Xb','Yb','Zb'])):
axis_vec = R @ (axis_len * np.eye(3)[:, j])
ax.quiver(0, 0, 0, *axis_vec, color=color, arrow_length_ratio=0.15)
ax.set_xlim(-1.5, 1.5); ax.set_ylim(-1.5, 1.5); ax.set_zlim(-1.5, 1.5)
ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z')
ax.set_title(title)
plt.suptitle('Трёхмерный поворот «крестообразного» дрона', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control1.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
2. Динамическое моделирование квадрокоптера
2.1 Модель мотора
Тяга квадрокоптера создаётся четырьмя моторами. Их модель такая:
Модель частоты вращения мотора (инерционное звено первого порядка):
Пояснение: Модель частоты вращения мотора предоставляется производителем: внутри уже есть замкнутый контур управления, поэтому при моделировании его внутреннее устройство можно не учитывать и использовать модель напрямую. Коэффициент можно определить по измерениям.
Тяга одного мотора:
Реактивный момент одного мотора:
Здесь — постоянная отклика мотора, — коэффициент тяги, — коэффициент момента.
Пояснение: Тяга и момент пропорциональны квадрату частоты вращения — это базовый результат аэродинамики. В реальных системах и определяются стендовыми испытаниями. Модель мотора — инерционное звено первого порядка: чем больше , тем быстрее отклик мотора; при проектировании регулятора обычно предполагают, что отклик достаточно быстрый, и эту динамику можно не учитывать.
2.2 Эйлеровы углы в порядке Z-X-Y и матрица вращения
Вращение описывается эйлеровыми углами в порядке Z-X-Y (сначала рыскание , затем крен , затем тангаж ):
Раскрывая (обозначим , ):
2.3 Связь угловой скорости и производных эйлеровых углов
Связь угловой скорости корпуса и производных эйлеровых углов :
Пояснение: Эта матрица преобразования вырождается при (карданный замок) — это внутренний недостаток представления эйлеровыми углами. Для малых углов (например, около зависания) матрица близка к единичной, то есть угловая скорость примерно равна производным эйлеровых углов.
2.4 Уравнение Ньютона (динамика поступательного движения)
Пояснение: Уравнения Ньютона — Эйлера записываются в инерциальной системе координат и описывают движение дрона под действием суммарной силы и момента.
Уравнение Ньютона описывает поступательное движение центра масс:
Пояснение: Левая часть — масса, умноженная на ускорение центра масс. Первое слагаемое справа — сила тяжести (вертикально вниз), второе — суммарная тяга, которую матрица переводит из системы корпуса в мировую (тяга всегда направлена вдоль оси корпуса). Сопротивление воздуха не учитывается.
Обозначим суммарную тягу . Раскрывая по компонентам:
2.5 Уравнения Эйлера (динамика вращения)
Правые части задают входы управляющего момента:
Пояснение: Уравнения Эйлера записываются в системе корпуса, потому что тензор инерции в системе корпуса — постоянная диагональная матрица. Так избегается сложность, связанная с изменением ориентации.
Пояснение: Член — момент Кориолиса / гироскопический эффект. Вблизи зависания (малые угловые скорости) им можно пренебречь, что и служит основой для последующей линеаризации. и создают моменты крена и тангажа через разность тяги, а создаёт момент рыскания через разность реактивных моментов.
2.6 Сводка модели динамики
| Номер | Название | Описание |
|---|---|---|
| ① | Модель мотора | |
| ② | Уравнения Эйлера | Динамика вращения |
| ③ | Преобразование угловых скоростей | Связь угловой скорости и производных эйлеровых углов |
| ④ | Матрица вращения | Представление эйлеровыми углами Z-X-Y |
| ⑤ | Уравнения Ньютона | Динамика поступательного движения |
2.7 Пример уравнения состояния для плоскости
Рассмотрим движение в плоскости (Y-Z). Вектор состояния , вход :
Пояснение: Это нелинейное уравнение состояния для двумерного случая. Видно, что ускорение по зависит от (сначала нужно наклониться, чтобы двигаться горизонтально), ускорение по — от (вертикальная составляющая тяги), а напрямую управляется моментом . Это проявление «недоактуированности» квадрокоптера: четыре входа, а управлять приходится шестью степенями свободы.
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# ========== Модель тяги и момента квадрокоптера: расчёт и визуализация ==========
# --- Параметры моторов ---
kF = 6.11e-8 # коэффициент тяги (Н/(рад/с)^2)
kM = 1.5e-9 # коэффициент момента (Н·м/(рад/с)^2)
L = 0.175 # длина луча (м)
# --- Частоты вращения четырёх моторов (рад/с), имитация асимметричного полёта ---
omega = np.array([4000, 4200, 3800, 4100]) # ω1, ω2, ω3, ω4
# --- Тяга и реактивный момент каждого мотора ---
F = kF * omega**2 # тяга каждого мотора
M = kM * omega**2 # реактивный момент каждого мотора
# --- Суммарная тяга и управляющие моменты ---
u1 = np.sum(F) # суммарная тяга
u2 = L * (F[1] - F[3]) # момент крена
u3 = L * (F[2] - F[0]) # момент тангажа
u4 = M[0] - M[1] + M[2] - M[3] # момент рыскания
print(f"Тяга моторов: F = {F} Н")
print(f"Суммарная тяга u1 = {u1:.4f} Н")
print(f"Момент крена u2 = {u2:.6f} Н·м")
print(f"Момент тангажа u3 = {u3:.6f} Н·м")
print(f"Момент рыскания u4 = {u4:.8f} Н·м")
# --- Визуализация: вид сверху (положения моторов и тяга) и 3D-векторы тяги ---
fig = plt.figure(figsize=(14, 5))
# Подграф 1: вид сверху, положения моторов и величина тяги
ax1 = fig.add_subplot(1, 2, 1)
motor_pos = np.array([[0, L], [-L, 0], [0, -L], [L, 0]]) # мотор 1 спереди, 2 слева, 3 сзади, 4 справа
colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12']
for i in range(4):
circle = plt.Circle(motor_pos[i], 0.04, color=colors[i], zorder=5)
ax1.add_patch(circle)
ax1.annotate(f'M{i+1}\nω={omega[i]}\nF={F[i]:.3f}Н',
motor_pos[i], textcoords="offset points",
xytext=(15, 10), fontsize=8, color=colors[i])
# Рисуем корпус-крест
ax1.plot([motor_pos[1,0], motor_pos[3,0]],
[motor_pos[1,1], motor_pos[3,1]], 'k-', lw=2)
ax1.plot([motor_pos[0,0], motor_pos[2,0]],
[motor_pos[0,1], motor_pos[2,1]], 'k-', lw=2)
ax1.set_xlim(-0.35, 0.35); ax1.set_ylim(-0.35, 0.35)
ax1.set_aspect('equal'); ax1.grid(True, alpha=0.3)
ax1.set_title('Вид сверху: расположение моторов и тяга'); ax1.set_xlabel('Y (м)'); ax1.set_ylabel('X (м)')
# Подграф 2: 3D-вид, стрелки тяги
ax2 = fig.add_subplot(1, 2, 2, projection='3d')
for i in range(4):
x, y = motor_pos[i]
# Стрелки тяги каждого мотора (вдоль оси Z вверх)
scale = F[i] / np.max(F) * 0.3 # нормировка для отображения
ax2.quiver(x, y, 0, 0, 0, scale, color=colors[i],
arrow_length_ratio=0.2, lw=2, label=f'M{i+1}: {F[i]:.3f}Н')
ax2.scatter(x, y, 0, color=colors[i], s=60)
# Стрелка суммарной тяги (из центра)
ax2.quiver(0, 0, 0, 0, 0, 0.4, color='k', arrow_length_ratio=0.15,
lw=3, label=f'Суммарная тяга u1={u1:.3f}Н')
ax2.set_xlabel('Y'); ax2.set_ylabel('X'); ax2.set_zlabel('Z (направление тяги)')
ax2.set_title('3D-вид: векторы тяги'); ax2.legend(fontsize=7, loc='upper left')
plt.suptitle('Визуализация модели мотора квадрокоптера', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control2.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
3. Дифференциальная плоскостность
3.1 Определение и смысл дифференциальной плоскостности
Пояснение: Дифференциальная плоскостность — мост между планированием траектории и управлением. Если система плоская, планировщику достаточно задать траекторию плоского выхода, а все состояния и входы выводятся автоматически. В сочетании с управлением на SE(3) это позволяет очень точно отслеживать положение и ориентацию.
Дифференциальная плоскостность (Differential Flatness) означает: если все состояния и входы системы выражаются через набор плоских выходов (Flat Output) и их конечное число производных, то система дифференциально плоская.
Пояснение: У квадрокоптера 12 переменных состояния и 4 входа, но плоский выход имеет только 4 компонента (). Смысл плоскостности в том, что если все состояния и входы выражаются через плоский выход и его конечные производные, то задачу управления высокой размерности можно свести к проектированию траектории для четырёх плоских выходов.
3.2 Выбор плоского выхода
Для квадрокоптера плоский выход выбирается так:
Он задаёт достаточно широкое множество траекторий:
Пояснение: Плоский выход задаёт траекторию на , то есть полное движение твёрдого тела, включающее и положение (), и ориентацию (). Преобразование из мировой системы координат в систему корпуса полностью определяется и его производными.
Полный вектор состояния:
Пояснение: Если задать и , можно аналитически получить углы крена, тангажа, рыскания и угловые скорости.
3.3 Вывод всех состояний по плоскому выходу
Главная ценность дифференциальной плоскостности: зная и его производные, можно вывести все 12 состояний и 4 входа. Путь вывода:
| Известная величина | Выводимая величина | Требуемый порядок производной |
|---|---|---|
| Положение | 0-го порядка | |
| Скорость | 1-го порядка | |
| Суммарная тяга ; углы ориентации | 2-го порядка | |
| Угловые скорости | 3-го порядка | |
| Угловые ускорения ; моменты | 4-го порядка (snap) |
Пояснение: Поэтому при планировании траектории минимизируют snap (четвёртую производную): он напрямую связан с величиной управляющего момента. Если snap велик, требуются большие моменты, и моторы могут выйти на насыщение.
3.4 Вывод углов ориентации
Исходим из уравнения Ньютона:
Вывод суммарной тяги
Пояснение: Суммарная тяга равна модулю равнодействующей: . Физический смысл: тяга должна одновременно компенсировать силу тяжести и создавать требуемое ускорение.
Отсюда направление оси корпуса:
где — единичный вектор направления тяги.
Построение желаемой матрицы вращения
Через промежуточный вектор (направление которого задаётся углом рыскания ):
Пояснение: Идея построения матрицы вращения: сначала из направления тяги определяем , затем из угла рыскания — вспомогательное направление . получаем векторным произведением : он перпендикулярен плоскости, образованной и . Наконец, дополняет правую систему координат.
Извлечение эйлеровых углов
Из матрицы вращения извлекаем эйлеровы углы:
Пояснение: Поэтому и полностью определяются второй производной плоского выхода (то есть ускорением); более высокие производные не нужны.
3.5 Вывод угловых скоростей
Пояснение: Далее выводим угловые скорости . Для этого нужно ещё раз взять производную уравнения Ньютона по времени (от ускорения к рывку, jerk).
Производная уравнения Ньютона по времени (рывок):
Пояснение: — постоянный единичный вектор мировой системы, не меняющийся во времени, поэтому .
Так как — постоянный вектор, его производная равна нулю, и:
Пояснение: При дифференцировании используем правило произведения: — скорость изменения величины тяги, — скорость изменения направления тяги. По кинематике твёрдого тела производная направляющего вектора выражается через векторное произведение с угловой скоростью: .
С учётом кинематического соотношения :
Исключаем составляющую вдоль (умножаем скалярно на и вычитаем):
Введём вспомогательный вектор:
Пояснение: — вспомогательный вектор: он вычисляется по известным рывку и тяге и содержит информацию о составляющих в плоскости, перпендикулярной , из которой можно извлечь и .
Извлечение компонент
Угловая скорость в системе корпуса:
Пояснение: — разложение вектора по базису. Благодаря ортогональности компоненты выделяются скалярными произведениями.
Поскольку (ортогональные базисные векторы), скалярное произведение ортогональных векторов равно нулю.
Используя и скалярно умножая на единичные векторы осей:
Для компоненты — угловая скорость вдоль :
Пояснение: По ортогональности и , поэтому составляющая вдоль не может быть получена из (он перпендикулярен ); её находят через скорость рыскания .
Пояснение: Компонента напрямую связана со скоростью рыскания — это отражает, что угол рыскания — самостоятельный плоский выход. Компоненты требуют рывка (третьей производной), а достаточно — в этом проявляется развязка канала рыскания.
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
# ========== Проверка дифференциальной плоскостности: вывод углов ориентации и тяги по круговой траектории ==========
# --- Физические параметры ---
m = 0.5 # масса дрона (кг)
g = 9.81 # ускорение свободного падения (м/с²)
# --- Круговая траектория (плоский выход σ = [x, y, z, ψ]) ---
T_total = 10.0 # полное время (с)
dt = 0.01
t = np.arange(0, T_total, dt)
omega_c = 2 * np.pi / T_total # угловая частота круговой траектории
# Траектория положения: горизонтальный круг + медленный подъём + рыскание по направлению скорости
radius = 2.0
x = radius * np.cos(omega_c * t)
y = radius * np.sin(omega_c * t)
z = 0.5 * t # спиральный подъём
psi = np.arctan2(np.gradient(y, dt), np.gradient(x, dt)) # угол рыскания следует направлению скорости
# --- Вычисление производных ---
ddx = np.gradient(np.gradient(x, dt), dt) # вторая производная x (ускорение)
ddy = np.gradient(np.gradient(y, dt), dt) # вторая производная y
ddz = np.gradient(np.gradient(z, dt), dt) # вторая производная z
# --- Суммарная тяга u1 из дифференциальной плоскостности ---
u1 = m * np.sqrt(ddx**2 + ddy**2 + (ddz + g)**2)
# --- Углы крена φ и тангажа θ по ускорениям ---
# Направление оси z корпуса (единичный вектор тяги)
tx = ddx / (ddz + g + 1e-10)
ty = ddy / (ddz + g + 1e-10)
# Эйлеровы углы через связь с матрицей вращения
phi = np.arcsin(np.clip(tx * np.sin(psi) - ty * np.cos(psi), -1, 1)) # угол крена
theta = np.arctan2(tx * np.cos(psi) + ty * np.sin(psi),
1 + ddz / g) # угол тангажа
# --- Построение результатов ---
fig = plt.figure(figsize=(15, 10))
# Подграф 1: трёхмерная траектория
ax1 = fig.add_subplot(2, 2, 1, projection='3d')
ax1.plot(x, y, z, 'b-', lw=1.5)
# Через равные интервалы отмечаем положение дрона и направление
step = len(t) // 8
for i in range(0, len(t), step):
ax1.scatter(x[i], y[i], z[i], c='r', s=40, zorder=5)
dx_arrow = 0.3 * np.cos(psi[i])
dy_arrow = 0.3 * np.sin(psi[i])
ax1.quiver(x[i], y[i], z[i], dx_arrow, dy_arrow, 0,
color='r', arrow_length_ratio=0.3)
ax1.set_xlabel('X (м)'); ax1.set_ylabel('Y (м)'); ax1.set_zlabel('Z (м)')
ax1.set_title('Спиральная траектория σ(t)')
# Подграф 2: суммарная тяга u1
ax2 = fig.add_subplot(2, 2, 2)
ax2.plot(t, u1, 'g-', lw=1.5)
ax2.axhline(y=m*g, color='k', ls='--', alpha=0.5, label=f'Тяга зависания mg={m*g:.2f}Н')
ax2.set_xlabel('Время (с)'); ax2.set_ylabel('u₁ (Н)')
ax2.set_title('Суммарная тяга u₁ (выведена из вторых производных)'); ax2.legend(); ax2.grid(True, alpha=0.3)
# Подграф 3: угол крена φ
ax3 = fig.add_subplot(2, 2, 3)
ax3.plot(t, np.degrees(phi), 'r-', lw=1.5, label='φ (крен)')
ax3.set_xlabel('Время (с)'); ax3.set_ylabel('Угол (°)')
ax3.set_title('Угол крена φ (выведен из σ̈)'); ax3.legend(); ax3.grid(True, alpha=0.3)
# Подграф 4: угол тангажа θ
ax4 = fig.add_subplot(2, 2, 4)
ax4.plot(t, np.degrees(theta), 'b-', lw=1.5, label='θ (тангаж)')
ax4.set_xlabel('Время (с)'); ax4.set_ylabel('Угол (°)')
ax4.set_title('Угол тангажа θ (выведен из σ̈)'); ax4.legend(); ax4.grid(True, alpha=0.3)
plt.suptitle('Проверка дифференциальной плоскостности: ориентация и тяга из траектории [x,y,z,ψ]', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control3.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
4. Основы PID-управления
4.1 Влияние параметров PID на отклик системы
Три параметра PID-регулятора , , по-разному влияют на характеристики отклика:
| Параметр | Время нарастания (Rise Time) | Перерегулирование (Overshoot) | Время установления (Settling Time) | Установившаяся ошибка (Steady-state Error) |
|---|---|---|---|---|
| уменьшается | увеличивается | уменьшается | уменьшается | |
| — | уменьшается | уменьшается | — | |
| увеличивается | увеличивается | увеличивается | — |
4.2 Характеристики ступенчатого отклика
Ключевые показатели ступенчатого отклика:
- Время нарастания (Rise Time): время, за которое отклик поднимается от начального значения до установившегося
- Перерегулирование (Overshoot): максимальное превышение отклика над установившимся значением
- Время установления (Settling Time): время, за которое отклик входит и остаётся в пределах (или ) от установившегося значения
- Установившаяся ошибка (Steady-state Error): разность между установившимся значением отклика и ожидаемым
Пояснение: Для времени установления часто используют два стандарта: вход в зону или . В инженерной практике чаще применяют .
Пояснение: уменьшает время нарастания, но увеличивает перерегулирование; добавляет демпфирование и снижает перерегулирование; устраняет установившуюся ошибку, но может вызвать колебания. При управлении квадрокоптером разумная настройка этих трёх параметров критически важна: обычно сначала подбирают для скорости отклика, затем для подавления перерегулирования, и наконец добавляют умеренный для устранения установившейся ошибки.
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
# ========== Влияние настроек PID на ступенчатый отклик системы второго порядка ==========
# Объект управления: упрощённая модель высоты дрона m * z̈ = u - mg
# Эквивалентная система второго порядка: ë + (Kd/m)*ė + (Kp/m)*e = -(Ki/m)*∫e dt
def simulate_pid(Kp, Kd, Ki, dt=0.001, T=5.0):
"""Моделирование ступенчатого отклика системы с PID-управлением"""
steps = int(T / dt)
z = 0.0 # текущая позиция (высота)
dz = 0.0 # текущая скорость
z_des = 1.0 # заданная высота (ступенчатый вход)
m = 0.5 # масса дрона
g = 9.81 # ускорение свободного падения
integral = 0.0 # интегральная составляющая
z_hist = np.zeros(steps)
for i in range(steps):
e = z_des - z # ошибка положения
de = -dz # ошибка скорости (ожидаемая скорость 0)
integral += e * dt # интеграл ошибки
# Закон PID-управления: u = mg + Kp*e + Kd*ė + Ki*∫e
u = m * g + Kp * e + Kd * de + Ki * integral
# Динамика: m * z̈ = u - mg
ddz = (u - m * g) / m
dz += ddz * dt
z += dz * dt
z_hist[i] = z
return z_hist
dt = 0.001; T = 5.0
t = np.arange(0, T, dt)
# --- Подграф 1: меняем Kp (Kd=2, Ki=0 фиксированы) ---
fig, axes = plt.subplots(1, 3, figsize=(16, 4.5))
ax = axes[0]
for Kp in [1, 5, 15, 30]:
z_hist = simulate_pid(Kp=Kp, Kd=2.0, Ki=0.0, dt=dt, T=T)
ax.plot(t, z_hist, label=f'Kp={Kp}')
ax.axhline(y=1.0, color='k', ls='--', alpha=0.4, label='Цель')
ax.set_title('Изменение Kp (Kd=2, Ki=0)')
ax.set_xlabel('Время (с)'); ax.set_ylabel('Высота z (м)')
ax.legend(fontsize=8); ax.grid(True, alpha=0.3)
ax.set_ylim(-0.1, 1.8)
# --- Подграф 2: меняем Kd (Kp=15, Ki=0 фиксированы) ---
ax = axes[1]
for Kd in [0, 1, 3, 8]:
z_hist = simulate_pid(Kp=15.0, Kd=Kd, Ki=0.0, dt=dt, T=T)
ax.plot(t, z_hist, label=f'Kd={Kd}')
ax.axhline(y=1.0, color='k', ls='--', alpha=0.4, label='Цель')
ax.set_title('Изменение Kd (Kp=15, Ki=0)')
ax.set_xlabel('Время (с)'); ax.set_ylabel('Высота z (м)')
ax.legend(fontsize=8); ax.grid(True, alpha=0.3)
ax.set_ylim(-0.1, 1.8)
# --- Подграф 3: меняем Ki (Kp=10, Kd=3 фиксированы) ---
ax = axes[2]
for Ki in [0, 2, 8, 20]:
z_hist = simulate_pid(Kp=10.0, Kd=3.0, Ki=Ki, dt=dt, T=T)
ax.plot(t, z_hist, label=f'Ki={Ki}')
ax.axhline(y=1.0, color='k', ls='--', alpha=0.4, label='Цель')
ax.set_title('Изменение Ki (Kp=10, Kd=3)')
ax.set_xlabel('Время (с)'); ax.set_ylabel('Высота z (м)')
ax.legend(fontsize=8); ax.grid(True, alpha=0.3)
ax.set_ylim(-0.1, 1.8)
plt.suptitle('Влияние параметров PID на ступенчатый отклик высоты дрона', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control4.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
5. Управление на основе модели
5.1 Модель второго порядка
Типичная модель второго порядка:
где — масса, — коэффициент демпфирования, — жёсткость пружины, — управляющий вход.
5.2 PD-управление
Закон PD-управления:
где — ошибка отслеживания.
Пояснение: Чистое PD-управление не использует информацию о модели: оно не может полностью компенсировать динамику системы, а при наличии шума датчиков и ошибок модели ошибка отслеживания неизбежна. Именно поэтому вводят управление на основе модели с прямой связью.
5.3 Закон управления на основе модели (прямая связь + PD-обратная связь)
Пояснение: Структура закона: прямая связь ( — модель компенсирует известную динамику) + PD-обратная связь ( — исправляет ошибки модели и возмущения).
Пояснение: По сравнению с чистым PD-управлением закон на основе модели содержит дополнительные члены прямой связи и . Прямая связь заранее компенсирует известную динамику, а PD-обратная связь исправляет ошибки модели. — оценки параметров модели: чем точнее оценка, тем меньше нагрузка на обратную связь.
5.4 Этапы управления на основе модели
- Получить динамику объекта управления
- Выделить подмодели
Сервоуправление:
- использовать PID или PD-регулятор с дифференцированием, чтобы свести ошибку к нулю
- независимые подсистемы
Пояснение: Для PID и LQR необходима линеаризованная модель. Нелинейную систему линеаризуют в рабочей точке (например, в точке зависания), а затем проектируют регуляторы для каждой линеаризованной подсистемы отдельно.
5.5 Блок-схема управления (2D)
ydes, zdes ┌──────────┐ ┌──────┐ ┌──────────┐ u₂ ┌─────┐
ẏdes, żdes ──>│ Управление│────>│ φ_c │────>│ Управление│──────>│ БПЛА│
│ положением│ └──────┘ │ ориентацией│ └─────┘
└──────────┘ u₁ └──────────┘ │
↑ │
└──── y, z, ẏ, ż ──────── φ, φ̇ ──────────────┘
5.6 Условие равновесия и линеаризация
Условие равновесия (зависание):
Линеаризованная динамика модели (вблизи точки зависания):
Пояснение: После линеаризации и связаны напрямую (), управляется независимо через , а — независимо через . Исходная нелинейная система из 6 переменных распадается на две независимые линейные подсистемы: (1) управление высотой по оси Z и (2) управление положением по Y с углом крена .
5.7 Управление положением по оси Z
Закон PD-управления:
Подставляя модель :
5.8 Управление положением по оси Y
Внешний контур — PD-закон управления положением:
Из модели получаем желаемый угол крена (выход внешнего контура):
Внутренний контур — PD-управление ориентацией:
Пояснение: Управление положением по Y — типичная каскадная структура: внешний регулятор положения выдаёт желаемый угол крена , а внутренний регулятор ориентации отслеживает этот угол. Частота внутреннего контура должна быть выше внешнего (обычно в 5–10 раз), чтобы обеспечить разделение временных масштабов.
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
# ========== Моделирование 2D-дрона (плоскость Y-Z): прямая связь + PD-управление ==========
# --- Физические параметры ---
m = 0.5 # масса (кг)
g = 9.81 # ускорение свободного падения (м/с²)
Ixx = 0.002 # момент инерции вокруг оси x (кг·м²)
dt = 0.002 # шаг моделирования (с)
T_sim = 6.0 # полное время моделирования
N = int(T_sim / dt)
# --- Коэффициенты PD ---
kp_z, kd_z = 15.0, 8.0 # PD по оси Z
kp_y, kd_y = 8.0, 6.0 # PD по оси Y
kp_phi, kd_phi = 150.0, 20.0 # PD по углу ориентации
# --- Желаемая траектория: зависание 2 с, затем переход в целевую точку ---
y_des = np.where(np.arange(N) * dt < 2.0, 0.0, 2.0)
z_des = np.where(np.arange(N) * dt < 2.0, 1.0, 2.5)
# --- Инициализация состояния [y, z, phi, dy, dz, dphi] ---
state = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
hist_y, hist_z, hist_u1, hist_u2, hist_phi = [], [], [], [], []
for i in range(N):
y, z, phi, dy, dz, dphi = state
# Внешний контур: PD по Z + прямая связь по силе тяжести
ddz_c = kp_z * (z_des[i] - z) + kd_z * (0 - dz)
u1 = m * (g + ddz_c) / (np.cos(phi) + 1e-6) # прямая связь: mg/cos(φ)
u1 = np.clip(u1, 0, 3 * m * g) # ограничение тяги
# Внешний контур: PD по Y → желаемый угол крена
ddy_c = kp_y * (y_des[i] - y) + kd_y * (0 - dy)
phi_c = -ddy_c / g # из линеаризации ÿ = -g·φ
phi_c = np.clip(phi_c, -0.5, 0.5) # ограничение желаемого угла
# Внутренний контур: PD ориентации
u2 = Ixx * (kp_phi * (phi_c - phi) + kd_phi * (0 - dphi))
# Обновление нелинейной динамики
ddy = -(u1 / m) * np.sin(phi)
ddz = (u1 / m) * np.cos(phi) - g
ddphi = u2 / Ixx
state += np.array([dy, dz, dphi, ddy, ddz, ddphi]) * dt
hist_y.append(y); hist_z.append(z)
hist_u1.append(u1); hist_u2.append(u2); hist_phi.append(phi)
t_arr = np.arange(N) * dt
# --- Построение: 2 строки × 2 столбца ---
fig, axes = plt.subplots(2, 2, figsize=(13, 9))
# Траектория в плоскости Y-Z
axes[0, 0].plot(hist_y, hist_z, 'b-', lw=1.5, label='Фактическая траектория')
axes[0, 0].plot(y_des[0], z_des[0], 'go', ms=10, label='Начальная цель')
axes[0, 0].plot(y_des[-1], z_des[-1], 'r*', ms=14, label='Конечная цель')
axes[0, 0].set_xlabel('Y (м)'); axes[0, 0].set_ylabel('Z (м)')
axes[0, 0].set_title('Траектория полёта в плоскости Y-Z'); axes[0, 0].legend(); axes[0, 0].grid(True, alpha=0.3)
# Отклик по высоте и горизонтальному положению
axes[0, 1].plot(t_arr, hist_z, 'b-', label='z фактическое')
axes[0, 1].plot(t_arr, z_des, 'b--', alpha=0.5, label='z желаемое')
axes[0, 1].plot(t_arr, hist_y, 'r-', label='y фактическое')
axes[0, 1].plot(t_arr, y_des, 'r--', alpha=0.5, label='y желаемое')
axes[0, 1].set_xlabel('Время (с)'); axes[0, 1].set_ylabel('Положение (м)')
axes[0, 1].set_title('Отклик отслеживания положения'); axes[0, 1].legend(); axes[0, 1].grid(True, alpha=0.3)
# Управляющие входы
axes[1, 0].plot(t_arr, hist_u1, 'g-', label='u₁ (суммарная тяга)')
axes[1, 0].axhline(y=m * g, color='k', ls='--', alpha=0.4, label=f'Тяга зависания mg={m*g:.2f}')
axes[1, 0].set_xlabel('Время (с)'); axes[1, 0].set_ylabel('Тяга (Н)')
axes[1, 0].set_title('Суммарная тяга u₁'); axes[1, 0].legend(); axes[1, 0].grid(True, alpha=0.3)
# Угол крена
axes[1, 1].plot(t_arr, np.degrees(hist_phi), 'm-', label='φ (угол крена)')
axes[1, 1].set_xlabel('Время (с)'); axes[1, 1].set_ylabel('Угол (°)')
axes[1, 1].set_title('Отклик угла крена φ'); axes[1, 1].legend(); axes[1, 1].grid(True, alpha=0.3)
plt.suptitle('Глава 5: моделирование 2D-дрона с прямой связью и PD-управлением (зависание → перемещение)', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control5.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
6. Управление отслеживанием трёхмерной траектории
6.1 Общая схема отслеживания траектории
Задана желаемая траектория , , ; требуется отслеживать её на интервале .
Определим ошибки:
Закон управления:
6.2 Блок-схема трёхмерного управления
p_des ┌────────────┐ ┌──────────┐ ┌──────────┐ u₂,u₃,u₄ ┌─────┐
ṗ_des ─>│Отслеживание│──>│ Управление│──u₁──>│ Управление│──────────>│ БПЛА│
│ траекторией │ │ положением│ φc,θc │ ориентацией│ └─────┘
└────────────┘ └──────────┘ └──────────┘ │
↑ │
└─── p, ṗ ──────────────── φ,θ,ψ, ωx,ωy,ωz ───────────────┘
6.3 Уравнения динамики в системе корпуса
Уравнение Ньютона:
Уравнения Эйлера:
Восстановление угловой скорости:
6.4 Раскрытие эйлеровых углов в порядке ZXY
где — угловая скорость крена, — угловая скорость тангажа, — угловая скорость рыскания.
Пояснение: Эта матрица преобразования переводит производные эйлеровых углов (заданные в промежуточных системах вращения) в компоненты угловой скорости в системе корпуса.
6.5 Дифференциальные уравнения эйлеровых углов
Обратное соотношение (найти ):
6.6 Линеаризация в точке зависания
Линеаризация вблизи точки зависания (, , ):
Пояснение: Заметьте, что при ненулевом угле рыскания () ускорения по и одновременно зависят от и : это и есть связь через угол рыскания. При получаем , , что совпадает с двумерным случаем главы 5.
6.7 Регулятор положения (PID)
Суммарная тяга:
Желаемые углы ориентации (с учётом угла рыскания ):
Пояснение: Регулятор положения выдаёт только желаемые и тягу ; угол рыскания отслеживается отдельным регулятором рыскания по желаемому и во внешний контур положения не входит.
6.8 Регулятор ориентации (PID)
Управляющие моменты:
Пояснение: Второе слагаемое закона управления моментом, , — компенсация прямой связью, которая нейтрализует гироскопический эффект. Вблизи зависания им можно пренебречь, но при быстрых манёврах его необходимо учитывать.
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# ========== Моделирование отслеживания трёхмерной траектории «восьмёрка» ==========
# --- Физические параметры ---
m = 0.5; g = 9.81; dt = 0.005; T_sim = 12.0
N = int(T_sim / dt)
t = np.arange(N) * dt
Kp = np.diag([6.0, 6.0, 10.0]) # коэффициенты PD по положению
Kd = np.diag([4.0, 4.0, 6.0])
kp_att, kd_att = 80.0, 15.0 # коэффициенты PD по ориентации
Ixx = Iyy = 0.002; Izz = 0.004
# --- Желаемая траектория «восьмёрка» ---
omega_t = 2 * np.pi / T_sim
x_des = 2.0 * np.sin(omega_t * t)
y_des = 1.5 * np.sin(2 * omega_t * t)
z_des = 1.0 + 0.3 * np.sin(omega_t * t)
# Желаемые скорость и ускорение (аналитическое дифференцирование)
dx_des = 2.0 * omega_t * np.cos(omega_t * t)
dy_des = 1.5 * 2 * omega_t * np.cos(2 * omega_t * t)
dz_des = 0.3 * omega_t * np.cos(omega_t * t)
ddx_des = -2.0 * omega_t**2 * np.sin(omega_t * t)
ddy_des = -1.5 * (2*omega_t)**2 * np.sin(2 * omega_t * t)
ddz_des = -0.3 * omega_t**2 * np.sin(omega_t * t)
# --- Инициализация состояния ---
pos = np.array([0.0, 0.0, 0.0]) # положение
vel = np.array([0.0, 0.0, 0.0]) # скорость
phi, theta, psi = 0.0, 0.0, 0.0 # эйлеровы углы
dphi, dtheta, dpsi = 0.0, 0.0, 0.0
hist_pos = np.zeros((N, 3))
for i in range(N):
p_des = np.array([x_des[i], y_des[i], z_des[i]])
v_des = np.array([dx_des[i], dy_des[i], dz_des[i]])
a_des = np.array([ddx_des[i], ddy_des[i], ddz_des[i]])
# PD-управление положением → желаемое ускорение
ep = p_des - pos; ev = v_des - vel
acc_cmd = a_des + Kp @ ep + Kd @ ev
# Суммарная тяга (прямая связь + PD)
u1 = m * np.sqrt(acc_cmd[0]**2 + acc_cmd[1]**2 + (acc_cmd[2] + g)**2)
u1 = np.clip(u1, 0.1, 4 * m * g)
# Желаемые углы ориентации (упрощение ψ≈0)
phi_c = np.clip((acc_cmd[0]*np.sin(psi) - acc_cmd[1]*np.cos(psi)) / g, -0.5, 0.5)
theta_c = np.clip((acc_cmd[0]*np.cos(psi) + acc_cmd[1]*np.sin(psi)) / g, -0.5, 0.5)
# PD-управление ориентацией
u2 = Ixx * (kp_att * (phi_c - phi) + kd_att * (0 - dphi))
u3 = Iyy * (kp_att * (theta_c - theta) + kd_att * (0 - dtheta))
# Упрощённое обновление трёхмерной динамики
cphi, sphi = np.cos(phi), np.sin(phi)
cth, sth = np.cos(theta), np.sin(theta)
cpsi, spsi = np.cos(psi), np.sin(psi)
ax = (u1/m) * (cpsi*sth*cphi + spsi*sphi)
ay = (u1/m) * (spsi*sth*cphi - cpsi*sphi)
az = (u1/m) * cphi*cth - g
vel += np.array([ax, ay, az]) * dt
pos += vel * dt
dphi += (u2 / Ixx) * dt; phi += dphi * dt
dtheta += (u3 / Iyy) * dt; theta += dtheta * dt
hist_pos[i] = pos
# --- Построение: сравнение 3D-траекторий ---
fig = plt.figure(figsize=(12, 8))
ax3d = fig.add_subplot(1, 1, 1, projection='3d')
ax3d.plot(x_des, y_des, z_des, 'b--', lw=1.5, alpha=0.6, label='Желаемая траектория («восьмёрка»)')
ax3d.plot(hist_pos[:, 0], hist_pos[:, 1], hist_pos[:, 2],
'r-', lw=1.2, label='Фактическая траектория')
# Отмечаем начало и конец
ax3d.scatter(*hist_pos[0], c='g', s=100, marker='o', zorder=5, label='Начало')
ax3d.scatter(*hist_pos[-1], c='k', s=100, marker='*', zorder=5, label='Конец')
# Через равные интервалы рисуем соединители ошибки
step = N // 10
for j in range(0, N, step):
ax3d.plot([x_des[j], hist_pos[j, 0]], [y_des[j], hist_pos[j, 1]],
[z_des[j], hist_pos[j, 2]], 'k-', alpha=0.2, lw=0.8)
ax3d.set_xlabel('X (м)'); ax3d.set_ylabel('Y (м)'); ax3d.set_zlabel('Z (м)')
ax3d.set_title('Глава 6: отслеживание трёхмерной траектории «восьмёрка» (PD-управление)', fontsize=14)
ax3d.legend(fontsize=10)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control6.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
7. Управление на SE(3)
Пояснение: Когда ошибка линеаризации эйлеровых углов недопустима (например, при больших углах манёвра), используют управление на SE(3): оно задаёт ошибку непосредственно на матрице вращения и не делает линеаризации, поэтому не теряет точности.
7.1 Управление отслеживанием на группе SE(3)
Управление на SE(3) задаёт ошибку непосредственно на специальной евклидовой группе , что позволяет избежать проблемы карданного замка (Gimbal Lock) эйлеровых углов.
Представление состояния:
где — матрица вращения, — положение.
7.2 Определение ошибок
Ошибка положения:
Ошибка скорости:
7.3 Расчёт желаемой силы
Пояснение: Проецируя желаемую силу на ось корпуса, получаем скалярную суммарную тягу ; остаток — направление силы, из которого строится желаемая ориентация. Так контуры положения и ориентации остаются развязанными: контур положения выдаёт желаемую силу, контур ориентации отслеживает желаемую ориентацию.
Суммарная тяга (проекция на ось корпуса):
7.4 Построение желаемой матрицы ориентации
Из направления желаемой силы строится желаемая матрица вращения:
Пояснение: Из направления желаемой силы строим полную матрицу вращения . Обратите внимание: при резких манёврах с большим snap направление силы может стать почти горизонтальным, и тогда почти параллелен ; векторное произведение вырождается, поэтому нужна защита от сингулярности.
Из желаемого угла рыскания строим полную матрицу вращения:
7.5 Ошибка ориентации и ошибка угловой скорости
Ошибка ориентации (используется отображение Vee из в ):
Ошибка угловой скорости:
7.6 Управляющие моменты
Пояснение: Преимущества управления на SE(3): (1) нет карданного замка, подходит для больших углов манёвра; (2) ошибка ориентации определена на , что даёт глобальную сходимость. Цена — более сложная реализация: нужно работать непосредственно с матрицами вращения. Метод построения желаемой матрицы ориентации полностью совпадает с выводом из главы 3 (дифференциальная плоскостность).
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
# ========== Сравнение управления по эйлеровым углам и на SE(3) при большом перевороте ==========
def vee(M):
"""Кососимметричная матрица → вектор"""
return np.array([M[2,1], M[0,2], M[1,0]])
def Ry(a):
"""Матрица вращения вокруг оси Y"""
c, s = np.cos(a), np.sin(a)
return np.array([[c,0,s],[0,1,0],[-s,0,c]])
# --- Параметры моделирования (единичный момент инерции — акцент на различиях управления) ---
dt = 0.005; T_sim = 6.0; N = int(T_sim / dt)
t = np.arange(N) * dt
I_mat = np.diag([1.0, 1.0, 1.0]) # единичный момент инерции
# Цель: переворот на 170° вокруг оси Y (близко к области карданного замка по тангажу ±90°)
flip_angle = np.deg2rad(170)
R_des = Ry(flip_angle)
# --- Метод 1: PD-управление по эйлеровым углам ---
euler = np.zeros(3) # [phi, theta, psi]
deuler = np.zeros(3)
kp_e, kd_e = 4.0, 3.0
# Извлекаем целевые эйлеровы углы из R_des (порядок ZYX)
theta_des = np.arctan2(-R_des[2,0], np.sqrt(R_des[0,0]**2 + R_des[1,0]**2))
phi_des = np.arctan2(R_des[2,1], R_des[2,2])
psi_des = np.arctan2(R_des[1,0], R_des[0,0])
euler_des = np.array([phi_des, theta_des, psi_des])
err_euler = np.zeros(N)
for i in range(N):
e_ang = euler_des - euler
err_euler[i] = np.linalg.norm(e_ang)
tau = kp_e * e_ang + kd_e * (-deuler)
deuler += tau * dt
euler += deuler * dt
# --- Метод 2: управление на SE(3) ---
R_cur = np.eye(3)
omega = np.zeros(3)
kR, kw = 4.0, 3.0
err_se3 = np.zeros(N)
for i in range(N):
eR_mat = R_des.T @ R_cur - R_cur.T @ R_des
eR = 0.5 * vee(eR_mat)
err_se3[i] = np.linalg.norm(eR)
tau_se3 = -kR * eR - kw * omega
omega += tau_se3 * dt
# Обновление матрицы вращения по формуле Родрига
wx = omega * dt
ang = np.linalg.norm(wx)
if ang > 1e-12:
K = np.array([[0,-wx[2],wx[1]],[wx[2],0,-wx[0]],[-wx[1],wx[0],0]]) / ang
dR = np.eye(3) + np.sin(ang)*K + (1-np.cos(ang))*(K@K)
else:
dR = np.eye(3)
R_cur = R_cur @ dR
# --- Сравнительные графики ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(t, err_euler, 'r-', lw=1.5, label='Ошибка PD по эйлеровым углам')
axes[0].plot(t, err_se3, 'b-', lw=1.5, label='Ошибка ориентации SE(3)')
axes[0].set_xlabel('Время (с)'); axes[0].set_ylabel('Норма ошибки ориентации')
axes[0].set_title('Сравнение ошибки ориентации (цель: переворот вокруг Y на 170°)')
axes[0].legend(); axes[0].grid(True, alpha=0.3)
axes[1].semilogy(t, np.clip(err_euler, 1e-10, None), 'r-', lw=1.5, label='PD по эйлеровым углам')
axes[1].semilogy(t, np.clip(err_se3, 1e-10, None), 'b-', lw=1.5, label='SE(3)')
axes[1].set_xlabel('Время (с)'); axes[1].set_ylabel('Ошибка ориентации (логарифм)')
axes[1].set_title('Сравнение в логарифмическом масштабе'); axes[1].legend(); axes[1].grid(True, alpha=0.3)
plt.suptitle('Глава 7: эйлеровы углы и SE(3) — большой переворот', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control7.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
8. Управление MPC
8.1 Связь планирования и управления
Пояснение: Планирование — это внешний контур управления, а управление — внутренний контур планирования.
Пояснение: Планирование опирается на модель, предсказывающую реакцию системы на входы, и заранее строит последовательность действий; управление компенсирует расхождение модели и реальности (ошибки моделирования, возмущения). Эти две задачи не противоречат друг другу: MPC объединяет планирование и управление, выполняя в скользящем окне времени и планирование, и управление одновременно.
Пояснение: MPC включает и планирование траектории, и управление: ему нужна полная траектория на горизонте прогноза. Если требуется лишь отслеживать готовую опорную траекторию (положение, скорость, ускорение), её можно передать напрямую регулятору SE(3), и MPC не нужен. Ценность MPC в том, что он умеет учитывать нелинейные ограничения и перепланировать в реальном времени, но за это приходится платить высокой вычислительной сложностью.
8.2 Модель пространства состояний CMPC
Вектор состояния:
Вектор входа:
8.3 Задача оптимизации
Ограничения:
Пояснение: Смысл ограничений: ограничения состояния задают пределы скорости (например, ), ограничения входа задают диапазон рывка, а требует, чтобы на каждом шаге позиция находилась внутри безопасной области (ограничение на избегание столкновений).
Пояснение: В CMPC линеаризованная модель выражает динамику между состояниями через дискретную рекуррентную зависимость (), включающую интегрирование между положением, скоростью и ускорением, а также динамику ориентации. Цели отслеживания траектории и ограничения безопасности обрабатываются в едином оптимизационном каркасе CMPC.
Пояснение: Основная идея MPC — на каждом шаге решать задачу оптимизации на конечном горизонте, применять только первый управляющий вход и затем повторять процесс (скользящий горизонт, Receding Horizon). Преимущество — возможность явно учитывать ограничения (пределы скорости, ограничения положения, ограничения на столкновения), но вычисления тяжелее и задачу приходится решать в реальном времени. MPC стирает границу между управлением и планированием: в каждом цикле управления он одновременно выполняет локальное планирование траектории и управление.
Проверка в Python
import numpy as np
import matplotlib.pyplot as plt
# ========== Упрощённое 1D MPC-управление высотой (скользящая оптимизация + ограничения тяги) ==========
plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # Исправляет отображение знака минус
# --- Параметры системы ---
m = 0.5; g = 9.81; dt_ctrl = 0.05 # период управления 50 мс
N_horizon = 20 # длина прогноза (шагов)
T_sim = 6.0; N_sim = int(T_sim / dt_ctrl)
z_target = 2.0 # целевая высота
# Ограничения тяги (физические пределы)
a_min = -g # минимальное ускорение (свободное падение, тяга = 0)
a_max = 2.0 * g # максимальное ускорение (предел тяги)
# Веса стоимости MPC
Q_z, Q_v, R_a = 10.0, 5.0, 0.1 # стоимость положения, скорости и управления
def solve_mpc(z0, v0, z_ref, N_h):
"""
Решаем 1D MPC: вручную строим квадратичную задачу и решаем её через numpy.
Состояние: [z, v], вход: a (ускорение), дискретная модель: z+=v*dt, v+=a*dt
"""
# Строим матрицы предсказания: X = Sx*x0 + Su*U
Sx = np.zeros((2 * N_h, 2)) # матрица распространения состояния
Su = np.zeros((2 * N_h, N_h)) # матрица влияния входа
A = np.array([[1, dt_ctrl], [0, 1]])
B = np.array([[0.5 * dt_ctrl**2], [dt_ctrl]])
# Пошагово строим матрицы предсказания
A_pow = np.eye(2)
for k in range(N_h):
A_pow = A_pow @ A
Sx[2*k:2*k+2, :] = A_pow
for j in range(k + 1):
A_mid = np.linalg.matrix_power(A, k - j)
Su[2*k:2*k+2, j] = (A_mid @ B).flatten()
# Матрицы стоимости Q_bar, R_bar
Q_bar = np.zeros((2 * N_h, 2 * N_h))
for k in range(N_h):
Q_bar[2*k, 2*k] = Q_z # вес положения
Q_bar[2*k+1, 2*k+1] = Q_v # вес скорости
R_bar = R_a * np.eye(N_h)
# Вектор опорной траектории
X_ref = np.zeros(2 * N_h)
for k in range(N_h):
X_ref[2*k] = z_ref
X_ref[2*k+1] = 0.0 # ожидаемая скорость 0
x0 = np.array([z0, v0])
# Оптимальное решение без ограничений: U* = (Su'QSu + R)^{-1} Su'Q(Xref - Sx*x0)
H = Su.T @ Q_bar @ Su + R_bar
f = Su.T @ Q_bar @ (X_ref - Sx @ x0)
U_star = np.linalg.solve(H, f)
# Применяем ограничения (простое отсечение)
U_star = np.clip(U_star, a_min, a_max)
return U_star
# --- Основной цикл симуляции ---
z, v = 0.0, 0.0 # начальное состояние: на земле, покой
hist_z, hist_v, hist_a = [], [], []
hist_pred_z = [] # предсказанные траектории на каждом шаге (для визуализации)
for i in range(N_sim):
U_opt = solve_mpc(z, v, z_target, N_horizon)
a_cmd = U_opt[0] # выполняем только первый шаг (скользящая оптимизация)
# Записываем предсказанную траекторию
if i % 20 == 0:
pred_z = [z]
pz, pv = z, v
for k in range(N_horizon):
ak = np.clip(U_opt[k], a_min, a_max)
pv += ak * dt_ctrl
pz += pv * dt_ctrl
pred_z.append(pz)
hist_pred_z.append((i * dt_ctrl, pred_z))
# Обновление состояния
v += a_cmd * dt_ctrl
z += v * dt_ctrl
hist_z.append(z); hist_v.append(v); hist_a.append(a_cmd)
t_arr = np.arange(N_sim) * dt_ctrl
# --- Построение ---
fig, axes = plt.subplots(2, 2, figsize=(14, 9))
# Отклик по высоте + предсказанные траектории
ax = axes[0, 0]
ax.plot(t_arr, hist_z, 'b-', lw=2, label='Фактическая высота')
ax.axhline(y=z_target, color='r', ls='--', lw=1.5, label=f'Цель z={z_target} м')
for (t0, pred) in hist_pred_z:
t_pred = t0 + np.arange(len(pred)) * dt_ctrl
ax.plot(t_pred, pred, 'g-', alpha=0.3, lw=0.8)
ax.plot([], [], 'g-', alpha=0.5, label='Предсказанная траектория (скользящее окно)')
ax.set_xlabel('Время (с)'); ax.set_ylabel('Высота (м)')
ax.set_title('Отклик MPC по высоте'); ax.legend(); ax.grid(True, alpha=0.3)
# Скорость
ax = axes[0, 1]
ax.plot(t_arr, hist_v, 'c-', lw=1.5)
ax.set_xlabel('Время (с)'); ax.set_ylabel('Скорость (м/с)')
ax.set_title('Вертикальная скорость'); ax.grid(True, alpha=0.3)
# Управляющий вход (ускорение/тяга)
ax = axes[1, 0]
ax.plot(t_arr, hist_a, 'm-', lw=1.5, label='Выход MPC a')
ax.axhline(y=a_max, color='r', ls=':', label=f'Макс. ускорение {a_max:.1f}')
ax.axhline(y=a_min, color='b', ls=':', label=f'Мин. ускорение {a_min:.1f}')
ax.axhline(y=0, color='k', ls='-', alpha=0.2)
ax.set_xlabel('Время (с)'); ax.set_ylabel('Ускорение (м/с²)')
ax.set_title('Управляющий вход (с учётом отсечения ограничений)'); ax.legend(fontsize=8); ax.grid(True, alpha=0.3)
# Соответствующая тяга
ax = axes[1, 1]
thrust = [m * (a + g) for a in hist_a]
ax.plot(t_arr, thrust, 'g-', lw=1.5, label='Тяга u₁ = m(a+g)')
ax.axhline(y=m * g, color='k', ls='--', alpha=0.4, label=f'Тяга зависания {m*g:.2f} Н')
ax.axhline(y=m * (a_max + g), color='r', ls=':', alpha=0.6, label='Верхний предел тяги')
ax.axhline(y=0, color='b', ls=':', alpha=0.6, label='Нижний предел тяги (0 Н)')
ax.set_xlabel('Время (с)'); ax.set_ylabel('Тяга (Н)')
ax.set_title('Визуализация ограничений тяги'); ax.legend(fontsize=8); ax.grid(True, alpha=0.3)
plt.suptitle('Глава 8: MPC-управление высотой в 1D — скользящая оптимизация и ограничения тяги', fontsize=14)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/control8.png', dpi=150, bbox_inches='tight')
plt.show()
Результат выполнения:
Приложение: таблица обозначений для раздела управления
| Обозначение | Смысл |
|---|---|
| Масса дрона | |
| Ускорение свободного падения | |
| Тензор момента инерции | |
| Момент инерции вокруг оси | |
| Матрица вращения (корпус → мир) | |
| Углы крена, тангажа и рыскания | |
| Компоненты угловой скорости корпуса | |
| Угловые скорости крена, тангажа и рыскания | |
| Тяга -го мотора | |
| Реактивный момент -го мотора | |
| Суммарная тяга | |
| Управляющие моменты | |
| Расстояние от мотора до центра масс | |
| Коэффициенты тяги и момента | |
| Коэффициенты PID | |
| Единичные векторы оси корпуса и мировой системы | |
| Ошибки отслеживания положения и скорости | |
| Ошибки ориентации и угловой скорости (SE(3)) | |
| Специальная евклидова группа | |
| Специальная ортогональная группа | |
| Плоский выход |