Руководство · Средний · 35 мин
Управление скоростью колёс с обратной связью по энкодерам
ШИМ (PWM) задаёт напряжение, а не скорость: постройте контур скорости для каждого колеса с упреждением и PI и выберите частоту дискретизации.
ШИМ (PWM) — это напряжение, а не скорость
analogWrite(motorPin, 180) не запрашивает скорость. Он запрашивает около 70% напряжения аккумулятора, а получаемая скорость зависит от нагрузки на колесо, трения в редукторе, состояния заряда и температуры щёток.
Поэтому робот с двумя одинаковыми двигателями и одинаковым ШИМ едет по дуге. Они никогда не были одинаковыми: один редуктор туже, другой комплект щёток новее, и при одинаковом напряжении их скорости отличаются на 3–8%. На трёх метрах это заметная кривая.
На роботе с дифференциальным приводом это можно замаскировать константой подстройки. На базе mecanum — нет, потому что обратная кинематика задаёт четырём колёсам совершенно разные скорости: в приведённом там примере одному колесу достаётся 0.020 m/s, а другому 0.580 m/s из одной команды, и именно отношения определяют направление робота. Колесо, которое медленнее на 5%, не делает робота немного медленнее: оно изменяет направление движения робота.
Замыкание контура по измеренной скорости каждого колеса исправляет это и является тем же решением для любого робота с более чем двумя колёсами или с необходимостью прибыть в заданную точку.
Измеряйте скорость, которая действительно есть
Начните с отсчётов квадратурного энкодера. Преобразование задаётся фиксированной геометрией:
counts per wheel revolution = CPR_motor × gear ratio × 4 (×4 for quadrature edges)
metres per count = 2πr / counts per revolution
Для энкодера двигателя 12 CPR за редуктором 30:1 и колёсами 97 mm: 12 × 30 × 4 = 1440 отсчётов/оборот, а 2π × 0.0485 / 1440 = 212 µm на отсчёт.
Теперь часть, которую все делают неправильно:
// DON'T: counts since last loop, divided by loop time.
float speed = (count - lastCount) * METRES_PER_COUNT / dt;
При 100 Hz и 0.05 m/s на одну выборку приходится один отсчёт. Сигнал скорости равен 0, 212, 0, 212 µm — прямоугольный сигнал с пульсациями ±100%, а D усилит их в гудящий двигатель. Есть три выхода, и обычно нужен первый:
- Выбирайте медленнее, чем управляете. Вычисляйте скорость за 20 ms (50 Hz) и запускайте контур с частотой 50 Hz. В окне больше отсчётов, квантование меньше, запаздывание фильтра отсутствует.
- Измеряйте время между фронтами, а не число фронтов за время. Это отлично работает на низкой скорости, неудобно на высокой и требует временных меток прерываний.
- Пропустите скорость через фильтр нижних частот. Это дешевле всего, но расходует запас устойчивости — того самого, которого у вас меньше всего.
// A 50 Hz velocity estimate, with the counter read atomically because an
// encoder ISR is writing it.
noInterrupts();
long count = encoderCount;
interrupts();
float dt = (now - lastTime) * 1e-6f; // micros(), so no ms rounding
float v = (count - lastCount) * METRES_PER_COUNT / dt;
lastCount = count; lastTime = now;
volatile long encoderCount и блок noInterrupts() не опциональны: чтение 32 бит на AVR занимает несколько инструкций, и если обработчик прерывания попадёт посередине, получится число, которого никогда не было.
Сначала упреждение, затем PID
Первое, что хочется сделать, — взять ПИД-регулятор (PID). Сначала выполните простое 90%.
Установившаяся скорость двигателя постоянного тока (DC) близка к линейной зависимости от приложенного напряжения, поэтому, зная нужную скорость, можно вычислить большую часть ШИМ:
pwm ≈ kS·sign(v) + kV·v
- kV — наклон. Сколько ШИМ требуется на один m/s.
- kS — свободный член. ШИМ, необходимый только для преодоления статического трения, до того как колесо вообще начнёт двигаться.
Измерьте оба значения за десять минут:
- Поставьте робота на опоры так, чтобы колёса свободно вращались.
- Медленно увеличивайте ШИМ, пока колесо только-только не начнёт вращаться. Это kS, обычно 25–45 из 255.
- Задайте ШИМ 100, 150, 200, 250 и запишите установившуюся скорость для каждого значения.
- Аппроксимируйте прямой. Её наклон — kV, и при продолжении назад он должен приблизительно дать kS.
Теперь контур исправляет только небольшую ошибку между этой оценкой и реальностью, поэтому коэффициенты могут быть малыми, а вся система — устойчивой и тихой:
float feedforward = kS * sign(target) + kV * target;
float output = feedforward + kP * error + kI * integral;
Без упреждения интегратору при каждом изменении цели приходится строить всю команду с нуля. Это медленно, приводит к перерегулированию и является причиной того, что многие настроенные вручную контуры двигателей получают огромный kI и робот с рывками.
Регулятор
P и I. Не D.
struct WheelLoop {
float kS, kV, kP, kI;
float integral = 0, target = 0;
int update(float measured, float dt) {
float error = target - measured;
float ff = kS * (target > 0 ? 1 : target < 0 ? -1 : 0) + kV * target;
float unclamped = ff + kP * error + kI * (integral + error * dt);
int output = (int)constrain(unclamped, -255, 255);
// Conditional integration: only accumulate if the output is NOT saturated,
// or if the error would wind the integrator back toward the middle.
// Otherwise a stalled wheel builds a command it cannot use and then keeps
// driving for a second after you let go of the stick.
if (fabsf(unclamped) < 255.0f || (error * unclamped) < 0) integral += error * dt;
return output;
}
};
Почему нет производной: составляющая D дифференцирует сигнал скорости, который сам является разностью отсчётов энкодера. Вы дифференцируете дважды, то есть дважды усиливаете шум квантования, а на колесе почти нечего гасить. Контур скорости с PI и хорошим упреждением — стандартное решение, а добавление D — один из распространённых способов испортить работающий контур. (Контур положения отличается — см. каскадное управление.)
Защита от накопления интеграла — часть, которую нельзя пропустить. Удерживайте робота у стены, подавая команду скорости, и наивный интегратор за несколько секунд достигнет полной шкалы. Отпустите робота — и он рванёт. Условное интегрирование, один if, решает задачу.
Настройка в правильном порядке
- Только упреждение. kP = kI = 0. Задайте 0.3 m/s и измерьте результат. Отклонение в пределах примерно 10% означает, что kV выбран верно. Исправьте его до любых других изменений.
- Увеличивайте kP до появления гудения, затем отступите на треть. Вы слушаете вой, который появляется вместе с коэффициентом и исчезает без него.
- Увеличивайте kI, пока не исчезнет установившаяся ошибка. Колесу с правильным упреждением нужно совсем немного. Начните с kP/10 в секунду и увеличивайте постепенно.
- Проверяйте под нагрузкой, а не на опорах. Ведь важно проверить колесо, которому задана наибольшая скорость.
- Проверяйте все четыре колеса вместе, двигаясь по диагонали. В этой команде самый большой разброс целей колёс, и именно там проявится ненастроенное колесо.
Симулятор ПИД-регулятора быстрее робота развивает интуицию для шагов 2 и 3, а руководство по настройке описывает общую процедуру.
Частота дискретизации
Частота контура должна быть значительно выше частоты управляемого процесса. У небольшого редукторного двигателя постоянного тока скорость устанавливается с постоянной времени примерно 50–150 ms; обычное правило — десять выборок за это время, то есть 50–200 Hz.
| Частота | Вердикт |
|---|---|
| 10 Hz | Слишком медленно. Двигатель заканчивает реагировать до того, как контур это заметит. |
| 50 Hz | Практический минимум для радиолюбительского привода, даёт окно энкодера 20 ms. |
| 100 Hz | Достаточно быстрый. Четыре контура и кинематика легко помещаются в 10 ms на Uno. |
| 1 kHz | Квантование энкодера начинает преобладать. Теперь вы управляете шумом. |
Запускайте по фиксированному расписанию, а не «как быстро работает loop()», потому что dt входит и в оценку скорости, и в интегратор:
const unsigned long PERIOD_US = 10000; // 100 Hz
static unsigned long next = 0;
void loop() {
unsigned long now = micros();
if ((long)(now - next) < 0) return; // signed compare survives rollover
next += PERIOD_US;
// ... read encoders, run four loops, write four PWMs
}
Знаковое сравнение — та же ловушка переполнения millis() в форме micros(), где она возникает через 71 минуту, а не через 49 дней.
Что это даёт в измерениях
Один и тот же робот, одна и та же команда, один и тот же пол:
| Разомкнутый контур (PWM) | Замкнутый контур (скорость) | |
|---|---|---|
| Дрейф на прямой на 3 m | 80–200 mm, зависит от аккумулятора | Менее 20 mm, повторяемо |
| Скорость при падении аккумулятора 8.4 → 7.0 V | Падает примерно на 17% | Не меняется, пока двигатели не насыщаются |
| Скорость на подъёме 5° | Резко падает | Удерживается, пока хватает момента |
| Точность диагонального движения mecanum | Плохая — отношения колёс неверны | Правильная, потому что удерживаются отношения |
| Режим отказа при нехватке момента | Тихо становится медленнее | Насыщается, и это можно обнаружить |
Последняя строка недооценена. Замкнутый контур знает, когда потерпел неудачу: колесо, у которого выход зафиксирован на 255, но цель ещё не достигнута, сообщает полезную информацию. Её можно вывести в телеметрию, отклонить команду или замедлить весь робот, чтобы сохранить правильные отношения.
Диагностика
| Симптом | Причина | Исправление |
|---|---|---|
| Колесо гудит или воет на месте | Слишком велик kP или D действует на шумную скорость | Снизьте kP; удалите D |
| Робот рвётся после удержания | Накопление интеграла | Условное интегрирование |
| Скорость колеблется вокруг цели | Слишком велик kI для частоты дискретизации | Уменьшите kI вдвое; проверьте фактическую частоту контура |
| Скорость читается как 0, затем удваивается поочерёдно | Слишком короткое окно энкодера для этой скорости | Измеряйте более длинное окно или считайте фронты между метками времени |
| Одно колесо всегда насыщено при диагональном движении | У этого колеса запрашивают больше всего; робот перегружен командой | Масштабируйте всю команду, не обрезайте — см. кинематику |
| Отсчёты скачут или идут назад | Пропущены фронты или volatile long прочитан неатомарно |
Прерывайте по обоим каналам; обрамите чтение noInterrupts() |
| На опорах работает, на полу отказывает | Настроено без нагрузки | Настраивайте при движении, а не при свободном вращении |
Далее
Колёса теперь выдают запрошенные скорости. Направьте всю конструкцию туда, куда хочет оператор, с помощью управления с ориентацией по полю, и подавайте плавные цели вместо скачков через профили движения. Затем посмотрите в симуляторе привода mecanum, от чего контур скорости может и чего не может спасти: идеально удержанная скорость колеса всё равно ничего не значит, если пол начинает скользить.
Исследуйте граф
Входит в эти сборки
Проекты и учебные пути, в которые входит это руководство.
Дополнительные материалы