Считать показания потенциометра для удобного аналогового управления системой, что может быть проще? Сеть завалена примерами работы с АЦП. Считали значение, вот мы и знаем положение… Но не всё так просто – эти значения всё время будут разными. Близкими, но разными. То есть, программа будет считать, что положение головки регулятора всё время дрожит.
В статье будет показано несколько методов борьбы с этими дрожаниями, начиная от самых простых (которые в подавляющем большинстве случаев достаточны), через более сложные и вплоть до алгоритма, придуманного специально для недавнего проекта. Наверняка, я уже 100500-й, кто такой алгоритм придумал, но после опроса знакомых, оказалось, что в списке тех, кто его собирается опубликовать, мой номер значительно меньше. Среди типовых он не находится. Возможно, кому-то он тоже окажется полезным.
Введение
Как обычно, намечалась рутинная работа. Меня попросили помочь коллеге устранить несколько мелких проблем в его проекте. Он делал «прошивку» для платы на базе процессора AtMega… Да, процессор староват. Но тут мы были просто подрядчиками, нам заказали разработать «прошивку», а к железу мы не имели никакого отношения. Так что Мега, значит Мега.
Устройство на базе Меги управляет неким шпинделем. В нормальном режиме работы требуемые обороты задаются в цифровом виде через мобильное приложение. Но иногда требуется переключиться на задание оборотов при помощи потенциометра. Угол поворота ручки задаёт требуемую скорость вращения.
И вот данные с этого потенциометра, считанные «прошивкой», дрожали. В целом… Шпиндель достаточно инертен. Все эти дрожания на плюс-минус несколько оборотов в минуту, ему не сделают никакой погоды. Мало того, измерение этих оборотов имеет намного большую погрешность, чем та самая ошибка считанных с потенциометра данных. Вроде, и ненужная работа намечалась, если бы не одно «но»: эти требуемые обороты также отображаются всё в том же мобильном приложении.
Любому уважающему себя пользователю категорически не нравится, когда какие-то показания дрожат. Даже я, когда у меня заканчивается работа, и я перехожу из программистов в пользователи, начинаю сильно возмущаться, видя подобные дрожания. Правда, когда снова начинаю работать – не менее рьяно доказываю, что не так это и страшно…
В общем, надо было убрать те мелкие дрожания значений на экране, которые зависят от положения ручки потенциометра. Ну, и сделать так, чтобы в оборудование уходили эти стабильные значения.
Уже по тому факту, что некоторые программисты просто читают показания АЦП и сразу пользуются ими, можно было бы написать короткую поучительную статью на тему «Почему нельзя просто читать аналоговые показания, почему их надо ещё и усреднять». Но реальность внесла свои коррективы. Простое усреднение не принесло желаемых результатов. Поэтому статья получилась не короткая. И она будет перечислять действия, которые можно предпринять, по мере их усложнения. В зависимости от обстоятельств, кто-то сможет остановиться на одном из описанных шагов, и следующие ему уже не потребуются. Но вот мне пришлось пройти все, причём последним шагом была разработка довольно забавного алгоритма фильтрации. Наверняка не я первый, кто его придумал, но мне кажется, что он должен валяться на каждом углу, чтобы разработчики «прошивок» могли пользоваться либо им, либо чем-то подобным, если вдруг это потребуется.
Итак, переходим к самому простому, но стопроцентно необходимому шагу – усреднению аналоговых значений. Без него не обойтись. О нём обязан знать каждый.
Усреднение значений
Философы говорят, что нельзя войти в одну реку дважды. Мы, программисты, вполне можем сказать, что нельзя считать одно и то же значение из АЦП дважды. Аналоговый мир полон шумов и помех. Давайте я покажу шум на линии, идущей от термодатчика, который я когда-то ловил на своём 3D-принтере. Всё-таки это мой личный прибор, хочу и публикую его осциллограммы, а много снимков с реального Изделия я публиковать не рискну. Вот этот шум:
Любые данные, считанные напрямую со входа АЦП, будут шуметь примерно так же. Они всё время плавают вокруг какого-то среднего значения. Так что это только блогеры, быстренько освоив чтение данных с АЦП, пишут, что «вот так мы будем читать потенциометр на Ардуинке», и скорей бегут строчить другие записи в блоге. На самом деле, хрестоматийным решением будет усреднять эти считанные значения. Решение настолько хрестоматийно, что многие производители контроллеров встраивают функцию усреднения значений прямо в АЦП. Вот так выглядит фрагмент первой попавшейся под руку документации на STM32 (я выделил слово «усреднение»):
Производители других современных контроллеров не отстают… Но в проекте использовалась весьма древняя по своей архитектуре AtMega. Там усреднение надо делать программно.
Я попробовал сначала усреднять 8 показаний. Вышло маловато. Потом 16. Тоже маловато. Перед тем, как начать говорить о количестве усредняемых отсчётов, существенно превышающих эти величины, сначала рассмотрим более продвинутый метод фильтрации.
Медианная фильтрация
На самом деле, простое усреднение хоть и хорошо, но не решает некоторых проблем. Давайте я покажу полную осциллограмму, считанную с термодатчика 3D-принтера:
Видите ту «иголку»? Откуда она берётся, я расскажу чуть позже. А пока просто примем за должное, что если начать усреднять в её районе, то ничего хорошего не получим. Один мой коллега продвигает идею медианной фильтрации. При ней мы сначала отбрасываем максимум и минимум из накопленного массива, а уже оставшиеся значения усредняем. Если разрешение будет таким, что «иголка» внесёт всего один вклад, всё отфильтруется в лучшем виде, она просто будет отброшена.
Ну, или я как-то делал двухпроходный фильтр. Сначала вычислял среднее, затем – отбрасывал те значения, которые отстояли от него более, чем на определённый процент, после чего усреднял оставшееся. Там отбрасывались массово влезшие «иголки».
В общем, нужна фильтрация, при которой всё, что сильно отличается от среднего, отбрасывается, а усредняется только то, что осталось. А уж как этого добиться — каждый может подойти к этой задаче творчески.
Большинству читателей этого может хватить, и они на этом смогут остановиться, но мне это не подошло. В первую очередь, вот сколько иголок было на реальной системе (я закрыл вход для постоянной составляющей и увеличил чувствительность осциллографа, чтобы помехи было лучше видно):
Это просто ёжик какой-то! Чтобы хоть что-то усреднить, надо отснять сотни показаний. В итоге, я остановился на двухстах пятидесяти. Под них надо выделить 250 слов, то есть, 500 байт. Напомню, у нас весьма скромный контроллер! Причём скромен он не только по объёмам ОЗУ и не только по быстродействию. У него ещё и восьмибитное АЛУ, так что работа по усреднению большого количества шестнадцатибитных значений превратится в те ещё вычислительные задачи. Поэтому я пошёл другим путём.
Я решил усреднять всё на лету. Запустил АЦП в режиме Free Running, а в обработчике прерывания копил данные не в массив, а в одну переменную. Как насуммировал 250 значений, так разом эту сумму на 250 и поделил:
ISR(ADC_vect)
{
static uint32_t avg_buf = 0;
static uint8_t avg_cnt = 0;
avg_buf += ADCW;
avg_cnt += 1;
if (avg_cnt == ADC_AVERAGE_CNT)
{
lastAvgValue = (uint16_t)(avg_buf / ADC_AVERAGE_CNT);
avg_buf = 0;
avg_cnt = 0;
}
}
Я измерил при помощи осциллографа: при моих настройках АЦП переменная lastAvgValue обновляется примерно 10 раз в секунду. Честно говоря, на основе расчётов, ожидал я не 10, а 50, но, когда просто улучшаешь работающий проект, лучше не лезть в недра, чтобы не испортить какие-нибудь таймауты. Так что десять, и ладно. Приемлемая цифра. Жаль только, что даже при таком усреднении, всё стало намного лучше, но осталось на неприемлемом уровне.
А она всё равно дёргается
Да, стало всё существенно лучше, но показания стояли не как вкопанные. Они всё равно периодически «дышали». Взлетали вверх, потом, постояв некоторое время там, падали вниз. Причём дело явно не в ошибках реализации усреднения. Переключим осциллограф на другую развёртку:
Сигнал достаточно медленно гуляет вверх-вниз. Понятно, что, когда усредняешь период 100 мс, в пределах периода всё будет хорошо. Но у разных периодов средние значения будут разными. Усреднять по большему промежутку? Тогда данные в мобильном приложении будут «замирать». Заказчик начинает нервничать уже не потому, что всё прыгает, а потому, что он крутит ручку, а реакции никакой.
Чтобы решить проблему, было решено делать усреднение в два этапа. Тот самый период 100 мс усреднять описанным выше способом, а полученные значения уже складывать в кольцевой буфер. И в любой произвольный момент времени выдавать среднее значение для того, что накопилось в буфере. То есть, сначала усредняем с «замиранием» процесса кусочки, длительность которых не ощущается человеком, а уже их усредняем, складывая в буфер, так что тут, с точки зрения пользователя, всё происходит «на лету». Буфер же получается небольшой, я остановился на хранении пяти последних отсчётов. Обработчик прерывания стал выглядеть так:
#define vspAvgCnt 5
uint16_t secondVspAverager [vspAvgCnt];
…
ISR(ADC_vect)
{
static uint8_t secondAvgPtr = 0;
static uint32_t avg_buf = 0;
static uint8_t avg_cnt = 0;
avg_buf += ADCW;
avg_cnt += 1;
if (avg_cnt == ADC_AVERAGE_CNT)
{
lastAvgValue = (uint16_t)(avg_buf / ADC_AVERAGE_CNT);
avg_buf = 0;
avg_cnt = 0;
secondVspAverager [secondAvgPtr++] = lastAvgValue;
if (secondAvgPtr == vspAvgCnt)
{
secondAvgPtr = 0;
}
}
}
Стало лучше, но не идеально. Но перед тем, как рассказать о придуманном третьем этапе фильтрации, я сначала остужу пыл тех, что уже кипит от возмущения: «Вечно эти программисты делают какую-то ерунду, когда можно всё зло зарубить на корню» … Действительно, зачем устранять то, чего можно избежать? Поэтому поговорим о том, что можно сделать на аппаратном уровне… И почему в нашем случае, это не было сделано…
Добавляем фильтр сигнала
Очень интересный случай был у меня, когда я гасил ту самую помеху от термодатчика в 3D-принтере. Как работает тот термодатчик? Давайте посмотрим схему древней, но зато классической платы RAMPS. Схемы всех прочих плат основывались на тех же самых принципах (я специально использую прошедшее время, но почему – расскажу чуть позже).
Что это за резисторы такие, и как они работают? Вспоминается анекдот, где идут два мужика, один ямку выкапывает, второй – закапывает… Их спрашивают: «Что вы делаете? Почему один выкапывает ямку, второй закапывает?». Второй и говорит: «Я не второй, я третий, второй должен в ямку дерево сажать, но он заболел». Вот и тут та же история. Перед нами только половина схемы. Вторая половина расположена не на плате, а на головке. Там к соответствующей линии подключён термистор, идущий на землю. Получается такая схема:
Пара резисторов R7 и Rt образуют классический делитель напряжения, коэффициент деления которого вычисляется по известной формуле
В зависимости от температуры головки, сопротивление Rt будет изменяться, а значит – напряжение Vcc будет делиться на некий коэффициент. Измерив это напряжение, по формулам мы можем вычислить температуру. Ура? В целом, да, но в частности, в первых 3D-принтерах показания температуры были весьма нестабильными, хоть значения с АЦП вполне себе усреднялись. Почему? О-о-о-о-о! Когда я это понял, то сразу всё исправил. Помните ту самую иголку?
Знаете, откуда она берётся? Давайте в схеме я отмечу один провод красным
В классическом 3D-принтере этот провод идёт от платы к головке в толстом шлейфе. Длина шлейфа у Дельты может достигать метра. Ну, или близко к тому. А что ещё идёт в этом же шлейфе? Во-первых, кабели к нагревателю. А по ним, извините, передаётся минимум двенадцативольтовый ШИМ-сигнал. Там постоянно идут то положительные, то отрицательные прямоугольные фронты высокой амплитуды. Дальше, там идёт четыре провода к шаговому двигателю. В них фронты не такие крутые, но всё же… Как минимум, они тоже высоковольтные. Но если честно, нам и ШИМа к нагревателю хватит.
И вот все эти провода идут одним длинным жгутом. А что такое жгут? Это конденсатор, в котором провода – это обкладки. Дёрганья силовых проводов и наводятся на нежный выход с термистора через емкостную связь. Вот откуда эти иголки лезут!
Поняв это, я просто добавил небольшой RC-фильтр. Для этого, около платы поставил низкоомный резистор. Такой, чтобы он гасил помеху, но не вносил существенного изменения в коэффициент деления. Ну, и небольшой конденсатор добавил, чтобы линия уж совсем не заводилась. Слева конденсатор и так уже имеется.
Теперь в длинном кабеле помеха как была, так и осталась, но на АЦП сигнал стал приходить уже отфильтрованный. Показания температуры застабилизировались сами собой, без внесения изменений в «прошивку».
Правда, это всё относится к красивым воспоминаниям. Сегодня вместо толстенного шлейфа с кучей источников помех, идут два провода цифровой шины CAN. Это решение создаёт новые проблемы, но обсуждаемую проблему устраняет идеально. Аналоговые и силовые линии стали короткими и разнесёнными, эффект конденсатора пропал, наводки стали минимальными.
Однако подумать об аппаратной фильтрации всегда полезно. В случае того железа, с которым шла работы (но фрагменты схемы которого я не могу публиковать, как бы ни были они просты), там не было длинной линии, которую имело бы смысл отрывать. Оба резистора там расположены прямо на плате, рядом друг с другом, так что описанная фильтрация там получается сама собой. Но возможно, помогло бы увеличение ёмкости конденсатора. Это в 3D-принтере стоит кондёр на 10 мкф, а в изделии – жалкие 100 нФ.
Жаль, узнать о том, помогла бы замена конденсатора или нет, мы не сможем. Те представители Заказчика, с которыми мы общались, явно не рисовали ту схему. Не факт, что они вообще понимали, чего мы от них хотим, когда вносим такие предложения. Но оставим все домыслы за рамками статьи. Просто укажем, что, когда программист может влиять на схему, он должен посмотреть, не может ли он добавить фильтрацию на аппаратном уровне. То ли, увеличив ёмкость фильтрующего конденсатора, то ли посмотрев, а нет ли в схеме неявного источника помех, который можно было бы задавить, внеся какие-либо несложные доработки. И может оказаться, что после этого править код и не придётся (если в нём уже применено усреднение, разумеется, без него никуда).
Дайте мне точку опоры, и я переверну мир
Работая с аналого-цифровым преобразователем, всегда полезно помнить, что на самом деле, это набор, состоящий из цифро-аналогового преобразователя, компаратора и управляющей логики. Вот эта троица в документации на AtMega:
На первых курсах нас заставляли изучать методы поиска пересечения монотонных функций, сравнивая их по принципу «больше-меньше». Мы делали лабораторные сначала при помощи метода половинного деления, потом – методами, которые обеспечивают как можно меньшее количество сравнений. Вот АЦП – это как раз пример того, что та задача имеет вполне себе прикладной смысл.
Входной сигнал подаётся на вход компаратора «+». В идеале, он должен там зафиксироваться на всё время преобразования (судя по комментарию на рисунке, так оно и есть). А дальше, при помощи ЦАП, формируется ряд напряжений, подаваемых на вход компаратора «-». По результату «Больше» или «Меньше» (где-то там добавляется «или равно», но это более низкий уровень, чем тот, на котором мы сейчас работаем, так что не будем даже разбираться, где именно) делаются выводы, в каком районе находится наш сигнал. Постепенно, логика выясняет то единственное напряжение на выходе ЦАП, которое соответствует напряжению на входе.
Теперь поговорим о том, что такое ЦАП. Это всего лишь управляемый многоразрядный делитель напряжения. Ему на вход подаётся опорное напряжение и коэффициент, на который оное следует поделить. В результате, на выходе получается уровень, пропорциональный опорному. Теперь нам уже не будет страшно, если я покажу чуть больший фрагмент структурной схемы АЦП.
Красная линия – это как раз путь опорного напряжения для ЦАП. Но в контроллере AtMega оно может быть получено совершенно разными путями, на выбор программиста. Можно просто закрыть транзистор, обеспечивающий жёлтый путь, и подать совершенно произвольное напряжение на ножку AREF. Тогда это напряжение войдёт по синему пути и перейдёт на красный. Про этот путь мы ещё поговорим в будущем.
Можно скоммутировать всё так, что напряжение аналогового питания AVCC (зелёный путь) пройдёт на жёлтый, а потом – разбежится и на вход ЦАП (красный путь), и на ножку AREF. Зачем так сложно? Просто в этом случае, на ножку AREF можно припаять фильтрующий конденсатор, а мы уже выяснили, что конденсаторы – это хорошо, они обеспечивают стабильность. Поэтому производитель обеспечил нам такую возможность. Если на AREF ничего не подаётся извне, она используется для подключения к внешним конденсаторам. В документации это показано так:
Ну, и наконец, путь от пурпурного на жёлтый с последующим разбегом на красный и синий – это сформированный высокоточным опорным источником уровень 1.1 или 2.56 вольта. Константа 2.56 выбрана не случайно. Она хорошо делится на 256, ну и на последующие степени двойки. Тогда нам, программистам, будет удобно пересчитывать милливольты в единицы, считанные из АЦП, и обратно.
Целевая плата сделана так, что вариантов нет. Ножка AREF припаяна чисто к фильтрующему конденсатору, так что на неё точно ничего опорного снаружи не подаётся. Ну, а входной делитель может пропускать напряжение, сильно превышающее 2.56 вольта. Остаётся только использовать в качестве опорного напряжение 5 вольт, имеющееся на входе AVСС. И вот здесь возникает целый ряд проблем. Давайте посмотрим на эквивалентную схему узла на плате:
Напряжение для входа АЦП получается путём двойного деления (сначала на потенциометре затем – на делителе R1R2) уровня 12 вольт. Откуда берётся этот уровень? Конечно же, с импульсного стабилизатора. У которого, конечно же, есть масса шумов и ошибка задания напряжения, вызванная погрешностями элементов в его обратной связи (что это такое – выходит за рамки статьи). А опорное напряжение для AVCC формируется другим импульсным стабилизатором, у которого есть свои помехи и своя ошибка выходного напряжения. Каждые помехи пляшут по-своему, а суммарная ошибка напряжений такая, что приходится максимальный уровень на АЦП для каждой платы сохранять в EEPROM.
Как я уже говорил, играть со схемой нам не довелось, но вот нутром чую, что вот такой вариант был бы поинтереснее:
Это тот самый сине-красный путь при отключённой жёлтой ветке, к которому я обещал вернуться. Здесь напряжение с потенциометра (идущее на вход АЦП) и опорное напряжение для АЦП формируются от одного источника. Иголки такая схема не отфильтрует (ЦАП же получает не зафиксированное опорное напряжение), а вот медленные колебания уровня, которые были показаны в предыдущем разделе – вполне могут оказаться для этой схемы невидимыми. Входное напряжение и опорное будут колебаться одновременно (лишь бы в пределах одного цикла измерения всё было более-менее стабильно). Ну, и все постоянные ошибки будут вызваны только погрешностями резисторов. Как минимум, эту калибровку сохранять в EEPROM не придётся.
Жаль, что в нашем случае, это всё было неприемлемо. Но те программисты, которые работают в содружестве со схемотехниками, вполне могут попытаться устранить проблему на аппаратном уровне. А что не устранится, то уже может быть добито простыми методами, описанными выше, без погружения в недра алгоритмов. Но увы. У нас схему править было нельзя, а простые программные методы хоть и существенно улучшили картину, но не решили проблему полностью, поэтому пришлось идти дальше.
Жёсткий алгоритм стабилизации
Подозреваю, что некоторые читатели уже устали разбираться в методах стабилизации показаний какого-то вшивого потенциометра… А уж я-то как устал, когда всё это проверял на практике! К этому месту я тоже был уже настроен решительно устранить помеху, не особо с нею церемонясь.
Давайте посмотрим на проблему с точки зрения пользователя. Вот у нас есть переменный резистор, который может совершать даже не целый оборот. И всё это делится чуть менее, чем на тысячу значений (в реальности, с имеющимися делителями и стабилизаторами, я видел значения на выходе АЦП от 93 до 930). Малейшее изменение угла ручки, даст существенное изменение значения. Не надо бояться и биться за каждую единицу на выходе АЦП! Правда не стоит и тупо отбрасывать младшие биты, там уже возникнут проблемы с разрешением. Надо анализировать все биты, но учитывать показания, только если значения изменились существенно.
Поэтому давайте к первым двум ступеням фильтрации, добавим третью. Она будет звучать так: «Если значение отклонилось вниз или вверх от текущего менее, чем на пять единиц, то это просто шум, оставляем предыдущее значение, а если отклонилось более, чем на эту величину – примем новое значение за новое положение ручки».
Идея оказалась вполне себе жизнеспособной, если не считать одной мелочи. Если мы угадали и встали строго по центру шумящего участка, мы выиграли. Значения колеблются, мы не выходим за коридор, удерживая это среднее положение в качестве текущего.
Но если мы совершенно случайно один раз попали рядом с границей, то начинаются скачки на плюс-минус пять. Нам нельзя прыгать сразу на большие величины, надо ещё и двигаться более плавно, чтобы постепенно нащупывать то самое центральное значение, к которому стоит стремиться.
Тогда я добавил ещё одно правило: «Если же напряжение отклонилось более чем на плюс-минус 4, но менее чем на плюс-минус 10, то считаем, что у нас на входе не это напряжение, а среднее между ним и предыдущим центральным». Благодаря такому правилу, система достаточно быстро находит центральную точку, и снова приходит в покой.
Алгоритмически, в код добавился следующий обработчик, расположенный в задаче, активирующейся пять раз в секунду… Кстати, в том проекте применён очень красивый планировщик с кооперативной многозадачностью и единым стеком. Самое то для восьмибитных процессоров, вместо ресурсоёмких вытесняющих планировщиков!
// Вторичное усреднение
uint32_t adcSum = 0;
for (uint8_t i=0;i=lastVSPvalue-4)&&(curVspValue<=lastVSPvalue+4))
{
// То оставляем значение на месте
curVspValue = lastVSPvalue;
// Если находимся внутри большого диапазона
} else if ((curVspValue>=lastVSPvalue-10)&&(curVspValue<=lastVSPvalue+10))
{
// То новое значение будет усреднено с предыдущим
curVspValue = (lastVSPvalue+curVspValue)/2;
}
// Если вылетели за диапазон – берём без обработки
lastVSPvalue = curVspValue;
Заключение
Работа с аналоговыми величинами – это нечто чуть более сложное, чем просто «считать АЦП». К счастью, чаще всего достаточно считать значение несколько раз и усреднить результат. Но иногда приходится делать какие-либо, более сложные действия. Знание сути работы АЦП, а также применение проблемно ориентированных алгоритмов, позволяет сделать всё с минимальным потреблением системных ресурсов ради вспомогательных задач (которых и так вечно не хватает для реализации основного функционала).