Преамбула
Продолжаю подробное описание использования линейно-квадратичного регулятора на примере управления перевёрнутым маятником. К слову сказать, термин «ЛКР» очень неточно отражает суть происходящего, как мне уже подсказали в комментариях, в русской школе теории управления этот подход называется «аналитическим конструированием оптимальных регуляторов», что существенно точнее.
Как обычно, я стараюсь разжевать математику по максимуму, чтобы материал был доступен заинтересованному школьнику. Я глубоко убеждён, что использование математики по-хорошему должно бы быть платным: любая формула должна быть использована только тогда, когда она призвана облегчить понимание, а не для того, чтобы выпендриваться.
Итак, это уже четвёртая статья, для лучшего понимания происходящего неплохо бы прочитать предыдущие три:
- 1. Методы наименьших квадратов
- 2. Линейно-квадратичный регулятор, вводная
- 3. Управление двигателем постоянного тока при помощи линейно-квадратичного регулятора
Вот фотография системы (кликабельно):
![](https://habrastorage.org/getpro/habr/post_images/d3f/c9f/b0e/d3fc9fb0eb13163ee9be6a54ec7bac47.jpg)
Используемые датчики
У меня только два источника информации о происходящем в системе, это два инкрементальных энкодера, один (1000 импульсов на оборот) показывает положение каретки, второй (2000 импульсов на оборот) даёт положение самого маятника. В предыдущей статье я считал импульсы при помощи самой же ардуины, но это довольно неточно и слабо устойчиво к зашумлённым показаниям энкодеров. Кроме того, это тратит и без того слабые вычислительные ресурсы атмеги. Сейчас я поставил одну микросхему HCTL-2032 (примерно пять евро штука на алиэкспрессе), она умеет считать импульсы двух энкодеров, предоставляя для этого 32-битные счётчики. Чтобы сэкономить ноги ардуины, между счётчиком и самой ардуиной стоит 74hc165, читающая один байт в параллель и отдающая этот байт последовательно.
Измеряем параметры двигателя ещё раз
По сравнению с прошлой статьёй у меня чуточку изменились параметры системы (другой драйвер двигателя плюс поставил энкодер на каретку), поэтому измеряю ещё раз параметры системы. Итак, с небольшим шагом (примерно в один вольт, от -24В до 24В) подаю постоянное напряжение на двигатель и измеряю изменение скорости каретки. Скорость каретки считается как разность между соседними положениями каретки делить на прошедшее время (о корректности такого подхода позже). Использовавшийся код можно увидеть здесь.
По горизонтали время в секундах, а по вертикали скорость движения каретки в метрах в секунду. Зашумлённые кривые — это результаты реального измерения, гладкие — это моя математическая модель системы. На сейчас задача посчитать именно модель для гладких кривых, приближающих реальность.
![](https://habrastorage.org/getpro/habr/post_images/c3b/113/303/c3b113303821dab5f5968d80e0dae37a.png)
Выяснилось, что я слегка перестарался с мотором, на 24В каретка ускоряется примерно на 60 метров в секунду за секунду и жёсткости системы не хватает для разумного измерения чего бы то ни было. Поэтому обрезаю измерения на пятнадцати вольтах, в начальной части кривых можно видеть колебания в скорости каретки.
Итак, для каждого измерения считаю установившуюся скорость (высота плато) и рисую график, на котором по горизонтали поданное напряжение, а по вертикали установившаяся скорость.
![](https://habrastorage.org/getpro/habr/post_images/5eb/546/193/5eb546193776fe00051e2a057640a60d.png)
Если бы у меня в системе не было бы трения, то этот график был бы линейным, и моя модель строилась бы как v_{k+1} = a*v_k + b*u_k. И ведь он практически линеен на своей положительной и отрицательной частях одновременно, но эти две прямые не проходят через ноль. Это (в том числе) из-за трения. Мы видим, что если при положительном напряжении мы добавим, а при отрицательном отнимем буквально долю вольта, то график пройдёт через ноль и станет линейным.
Если мы хотим учитывать трение в системе, то модель строится следующим образом (не забудьте прочитать предыдущую статью, если неясно, откуда берётся эта формула):
![](https://habrastorage.org/getpro/habr/post_images/7eb/384/afa/7eb384afab7385fd6cd212fd17b1bac1.png)
Итак, имея данные, нам нужно найти значения a, b и c. Фиттинг можно делать как угодно, учитывая, что он делается только один раз на десктопе, можно просто сделать три вложенных цикла. Вот код фиттинга, который мне говорит, что для дискретизации в 20мс между измерениями a=.4, b = .06, c=-.01. То есть, напряжение, необходимое для компенсации трения у меня в системе, равно одной десятой вольта. Кривые, полученные таким приближением, наложены на реальное измерение на картинке выше.
Контролируем прикладываемую силу, а не напряжение
Итого, на данный момент наша система может быть записана следующим образом:
![](https://habrastorage.org/getpro/habr/post_images/ab3/166/a83/ab3166a83716a64ef1bac3144511117d.png)
К сожалению, это не является линейным дифференциальным уравнением вида x_{k+1} = A x_k + B u_k, которое нужно для линейно-квадратичного регулятора. Кроме того, даже если бы не это, то мне нужно добавить ещё и маятник на каретку, а я не знаю как вывести зависимость положения и угловой скорости маятника от напряжения. Я бы предпочёл, чтобы у меня в качестве управления было бы не напряжение, а непосредственно ускорение каретки:
![](https://habrastorage.org/getpro/habr/post_images/31e/ea3/7b3/31eea37b31b5be675a1d946e8b2020e2.png)
Строго говоря, x_{k+1} = x_k + dt v_k + (dt)^2 / 2 a_k, но для dt = .02 секунды (dt)^2/2 равняется .0002, что уже пренебрежимо мало, поэтому выкидывается из записи.
Окей, предположим, что мой регулятор использует ускорение в качестве управления. Но ведь в реальности-то я должен подать напряжение на мотор. Как его получить? Очень просто:
![](https://habrastorage.org/getpro/habr/post_images/d6f/9fa/e34/d6f9fae34808f7e22d6f3b5612124169.png)
То есть, имея текущую скорость (состояние системы), мы легко можем посчитать напряжение, которое нужно приложить, чтобы получить необходимое ускорение. Таким образом, мы убили двух зайцев разом: и избавились от нелинейности в регуляторе, но при это оставив компенсацию трения, и получили интуитивно понятный контроль. Мне куда как проще представлять себе ускорение, нежели напряжение, которое очень нелинейно влияет на скорость.
Несложная, но длинная математика или добавляем маятник
Уравнения движения маятника
Итак, задача добавить маятник в нашу систему и записать соответствующие линейные уравнения для регулятора. Давайте приведём полный список необходимых для этого физических величин:
- m: масса маятника
- M: масса каретки
- f: сила, приложенная мотором к каретке через ремень
- 2l: длина маятника (l — это расстояние от петли до центра масс маятника)
- I: момент инерции маятника (к слову, а ваши шрифты позволяют видеть разницу между I и l?)
- θ(t): угол отклонения маятника от вертикали, в положении неустойчивого равновесия нулевой, увеличивается по часовой стрелке
- x(t): положение каретки
![](https://habrastorage.org/getpro/habr/post_images/b56/0eb/031/b560eb03131aa239d3263fd056367d6a.png)
Итого, переменными моей системы будет четырёхмерный вектор, состоящий из положения каретки, её скорости, угла отклонения маятника и его угловой скорости. Задача на сейчас записать систему линейных дифференциальных уравнений вот такого вида:
![](https://habrastorage.org/getpro/habr/post_images/49d/d34/891/49dd348911352615c399afbbd6387beb.png)
Если мы сумеем её записать в непрерывном виде, то потом просто дискретизуем и мы выиграли бой. Обратите внимание, что тут я использую в качестве управления приложенную силу, а не ускорение, но второй закон Ньютона нам покажет, как это сделать. Итак, нам нужно найти неизвестные матрицы A и B.
Запишем второй закон Ньютона для каретки: ускорение каретки, помноженное на массу, равняется сумме двух горизонтальных сил: первая сила — это наше управление, сила, приложенная через ремень электромотором. Вторая сила — это сила, с которой маятник, стараясь упасть или подняться, толкает каретку вбок. Чтобы её найти, можно записать координаты центра массс маятника как двумерный вектор (x(t) + l sin(θ(t)), l cos(θ(t))). Если мы продифференцируем дважды координату, то получим ускорение, а сила — это ускорение на массу. Поскольку нас интересует только горизонтальная составляющая, то имеет смысл брать только вторую производную от x(t) + l sin(θ(t). Итого:
![](https://habrastorage.org/getpro/habr/post_images/ebe/9bb/244/ebe9bb24489ad2ff176c9c20c973e9a9.png)
Это нелинейное уравнение, но оно может быть хорошо приближено линейным вокруг точки равновесия. Если угол близок к нулевому, то первый замечательный предел (ну или ряд Тейлора) говорят, что синус угла примерно равен углу. То же самое для косинуса: для углов, близких к нулевым, косинус почти равен единице. Ну и точно так же пренебрежём степенями, начиная с квадрата (если у нас скорость равна .1 радиана в секунду, то квадрат скорости даст совсем маленький вклад (.01), поэтому пренебрегаем им смело):
![](https://habrastorage.org/getpro/habr/post_images/a54/f43/f2d/a54f43f2db01b976fac8bd9bb7975fe6.png)
Тогда предыдущее уравнение может быть приближено линейным:
![](https://habrastorage.org/getpro/habr/post_images/993/42e/254/99342e25477ec14c7d289c3860ea6cf3.png)
Теперь запишем второй закон Ньютона (версия для вращения) для маятника. Для начала, что такое второй закон Ньютона для вращения? Это сумма моментов равна угловому ускорению, помноженному на момент инерции. Момент инерции — это такой аналог массы для поступательного движения. Нас интересует проекция всех сил на прямую, перпендикулярную маятнику (ведь вращает только перпендикулярная составляющая, правда?) Ну и разумеется, эти силы надо помножить на плечо, чтобы получить момент. Сил, действующих на маятник, три: гравитация и горизонтальная и вертикальная составляющие силы реакции опоры, которые мы можем получить как и в прошлый раз, дважды взяв производную по времени от координат центра масс маятника:
![](https://habrastorage.org/getpro/habr/post_images/95a/9cf/28b/95a9cf28b94650322ee81ece212f9c33.png)
Приблизим его линейным уравнением:
![](https://habrastorage.org/getpro/habr/post_images/102/dcd/558/102dcd558d63545080a18b65fc4095a9.png)
Запишем уравнения (1) и (2) в систему:
![](https://habrastorage.org/getpro/habr/post_images/940/31a/080/94031a08089288ec2179a9ee0cbf49d0.png)
Для удобства назовём определитель матрицы буквой D:
![](https://habrastorage.org/getpro/habr/post_images/747/bc4/a74/747bc4a74928e6974ee12bc2cc4b3aee.png)
Обратим матрицу:
![](https://habrastorage.org/getpro/habr/post_images/a0c/2ce/f18/a0c2cef18e1e9fbb56e358669cc4bfef.png)
Теперь запишем наши дифференциальные уравнения, по факту, мы нашли матрицы A и B:
![](https://habrastorage.org/getpro/habr/post_images/371/3b8/ced/3713b8ced5f5553f2cb1e2ec71933a8f.png)
Дискретизируем линейную систему
Итак, мы записали неперывное дифференциальное уравнение:
![](https://habrastorage.org/getpro/habr/post_images/df1/8a1/0a0/df18a10a01ef2cf949b5d56b5cf31bdf.png)
Для постоянного шага Δt получаем следующее:
![](https://habrastorage.org/getpro/habr/post_images/733/1b9/fda/7331b9fdab755f4a80cb39fd1e8da836.png)
Если мы обозначим через E единичную матрицу 4x4, то можно записать:
![](https://habrastorage.org/getpro/habr/post_images/eda/dcc/457/edadcc4579aee466014dd864e2e431bf.png)
Момент инерции для стержня, который вращается вокруг конца, можно посмотреть здесь, поэтому упрощаем всё что можно:
![](https://habrastorage.org/getpro/habr/post_images/acd/bca/d20/acdbcad20213ddd6c89db06c51c7ea30.png)
Это и есть самое главное уравнение движения маятника.
Update:
Тут я налажал. Когда я написал, что I = 4/3 ml^2, это неправда. I = 1/3 ml^2, поскольку это момент инерции при вращении вокруг центра масс. Так что и D вычислен неверно. D = l^2 m * (m + 4M) / 3.
Спасибо DanielWatton за багрепорт!
Линейно-квадратичный регулятор
Код вычисления коэффициентов регулятора можно взять здесь. Раньше я использовал наименьшие квадраты для подсчёта, но это всё же довольно громоздко, поэтому в новом коде матрица усиления считается напрямую. Матлаб для этого необязателен, вполне хватит обычного питона.
Итак, ЛКР нам говорит, что если взять управляющую силу f_k = 24.95 x_k + 18.54 v_k + 70.44 θ_k + 14.96 ω_k, то отклонив маятник на 12° от вертикали, и уведя каретку на 20 сантиметров от положения рановесия, то мы должны получить следующее поведение системы:
![](https://habrastorage.org/getpro/habr/post_images/324/ab2/96d/324ab296dd4bcc89c3647a0056d41219.png)
x_k — положение каретки, v_k — её скорость, θ_k — угол маятника, ω_k — его угловая скорость. Напряжение на графике дано в десятках вольт, чтобы примерно сравнять масштабы трёх графиков.
Забиваем коэффициенты в ардуину
Вот таким у меня получился маятник, управляющий код можно посмотреть здесь:
В принципе, результатом я пока вполне доволен. Амплитуда отклонения каретки от желаемого положения в районе двух сантиметров, что довольно прилично. Почему она не стоит как вкопанная на нулевой позиции, ловя отклонения маятника очень маленькими движениями? Причин несколько. Самая простая — люфт в редукторе мотора, который составляет примерно пять миллиметров положения каретки, что означает, что каретке довольно дорого менять направление движения. Следующая, и не менее важная причина — это оценка угловой скорости движения маятника (ну и скорости движения каретки).
Как оценить скорость движения, если в наличии лишь инкрементальный энкодер? Самый простой способ — это взять два последних значения энкодера и поделить их разницу на прошедшее время.Это известно как конечные разности и довольно широко применяется на практике.
Давайте посмотрим на следующий график:
![](https://habrastorage.org/getpro/habr/post_images/949/653/817/9496538179a8f1678f86334cbe17d1ef.png)
Я записал угол маятника в течение нескольких секунд, он показан красным. График слегка зашумлён из-за разрешения энкодера, но в общем и целом довольно гладкий. Скорость нам приходится находить синтетически из истории положения маятника. Если мы возьмём напрямую разность двух соседних положений и поделим на 20 миллисекунд, то это нам даст синий график. Польза конечных разностей в том, что они очень просто программируются. Их недостаток в том, что они драматическим образом усиливают любой шум измерений.
Если взять такую оценку скорости, то регулятор сходит с ума:
Единственная разница с предыдущим видео — это оценка скорости, ничего больше.
По-хорошему, нужно бы приблизить красную кривую каким-нибудь многочленом, и считать его производную, это основная идея фильтра Савицкого-Голея. Чтобы не ломать голову, я просто сгладил оценку скорости, взяв не соседние сэмплы, а разницу текущего сэмпла и восемь сэмплов тому назад, поделив на (20мс*8). Такой подход сильно сглаживает оценку скорости, но его недостаток в том, что он вносит лаг в систему. Посмотрите на зелёную кривую: она явно отстаёт от реального положения вещей. Такая задержка в оценке и заставляет мой маятник слегка покачиваться.
К слову сказать, если взять фильтр Савицкого-Голея в лоб, то он тоже создаст похожую задержку. Таким образом, я нашёл разумный для меня компромисс между достижением цели и простотой артиллерии. Бороться с зашумлёнными измерениями я планирую совсем другими методами, это тема для последущих разговоров. Также в последующие разговоры будет входить автоматическая раскачка маятника, чтобы он сам поднимался из нижнего состояния в верхнее.
Stay tuned!