В этой гостевой статье от Валерия Ярцева вы узнаете, что «индийским кодом» грешат даже матёрые китайские разработчики, но, к счастью, с этим можно бороться. О своём опыте и методах борьбы Валерий и поведает ниже:
В рамках одного проекта (будет отдельная публикация) с использованием Arduino Uno потребовалось измерять силу тока в цепи в пределах нескольких ампер с большими допустимыми погрешностями. Проект был временный: собирается модель, проводятся испытания, делаются выводы, модель разбирается. Поэтому части модели подбирались по принципу «что ближе лежит на полке».
Ближе всех лежал сенсор тока ACS758 производства DFROBOT. Амперка называет это изделие «Сенсором», а сам чувствительный элемент «Датчиком», но я бы называл их наоборот. Сам автор в тексте вышеуказанной страницы на сайте Амперки в паре мест путается и меняет систему названий. Я и сам датчик ACS758, и его же с обвязкой, далее буду называть просто датчиком. Датчик для измерения использует эффект Холла.
Для подключения питания и провода данных предусмотрен как стандартный разъём, так и отверстия на плате под пайку, обозначенные как VCC, GND и два VOUT. Я решил подключить датчик именно через них. Один из двух VOUTов я выбрал случайным образом.
Всё спаял, собрал, как рекомендует DFROBOT на примере. Не работает. Судя по тому, что считывает Arduino с VOUT, последний просто висит в воздухе. Странно…
Взял мультиметр и стал «звонить» все ножки и контакты. И действительно «в воздухе»: один из двух VOUT не был ни к чему припаян. Сигнал надо было снимать с другого VOUT. Большое «фи» DF-роботу за «точную» маркировку контактной площадки.
Убедившись, что соответствующий аналоговый порт Adrduino реагирует на изменения протекающего тока через одноимённый датчик, я стал искать на сайте производителя инструкции по преобразованию цифровых значений в реальные аналоговые. Они там быстро нашлись в виде скетча для Arduino.
В прошлой своей публикации я писал про корявый китайский код. DFROBOT тоже является китайской компанией. И её код оказался не менее корявым. Но оба вышеупомянутых кода являются рабочими. Что неудивительно, т.к. это напрямую влияет на объёмы продаж. А вот корявость вряд ли влияет. В общем, кнута и Кнута на них не хватает. Но желания коммерческих компаний сэкономить на программистах вполне понятны.
Непонятно другое: в качестве шаблона для своего кода DFROBOT взял пример с официального сайта arduino.cc. Но это ведь не мелкий китайский производитель датчиков. Это же, в т.ч., обучающий проект. Он должен сеять вечное и доброе.
А что же он сеет?
Итак, скетч со страницы «Сглаживание» сайта arduino.cc, которую следовало озаглавить «Как не надо программировать».
На странице неизвестный автор приводит пример алгоритма сглаживания данных, получаемых с аналоговых датчиков. В качестве текущего показания принимается среднее арифметическое от последних 10 считанных значений, которые хранятся в обычном линейном массиве как в кольцевом буфере.
Автор в начале страницы прямо декларирует обучающие цели приводимого кода. Он пишет: «Этот пример … также демонстрирует использование массивов для хранения данных».
Итак, чему же он учит.
В оригинале:
... const int numReadings = 10; ...
Это очень плохой стиль. Да, этот пример приводится практически во всех статьях интернет-журналистов, рассказывающих про использовании const в программах на C++. О том, что он чисто иллюстративный, авторы не говорят или потому, что сами этого не понимают, или просто допускают методологическую ошибку в процессе обучения своей потенциальной аудитории.
Правильно:
... #define _numReadings 10 ...
Это и читабельнее, и код получается после компиляции меньше и быстрее. Для Ардуино, кстати, это вполне актуально. А подчёркивание (или какая-то иная система обозначений) требуется для того, чтобы в тексте визуально было просто отличить константы от переменных.
В оригинале:
... int readings[numReadings]; int index = 0; int total = 0; int average = 0; ...
Правильно:
... int readings[_numReadings], index = 0, total = 0, average = 0; ...
Это в разы читабельнее.
В оригинале:
... int inputPin = A0; ...
Сама идея вынести этот кусок в настроечную область скетча — правильная. Только надо было ещё выше вынести, до объявления рабочих переменных. А реализация — плохая.
Правильно:
... #define _inputPin A0 ...
В оригинале:
... Serial.begin(9600); ...
Числа в коде — это неправильно. Надо объявлять в заголовке в помощью #define мнемонические идентификаторы и потом использовать их в тексте программы.
В оригинале:
... for (int thisReading = 0; thisReading < numReadings; thisReading++) readings[thisReading] = 0; ...
Громоздко, малочитабельно.
Правильно:
... memset( readings, 0, _numReadings * sizeof( int ) ); ...
В оригинале:
... total= total - readings[index]; ... total= total + readings[index]; index = index + 1; ...
Малочитабельно.
Правильно:
... total -= readings[index]; ... total += readings[index]; index++; ...
Так лучше со всех точек зрения. В т.ч. помогает компилятору сделать более оптимальный код, т.к. отражает суть процессорной системы команд.
В оригинале:
... average = total / numReadings; ...
А это уже грубая ошибка. Предположим, что датчик выдал одно значение, равное 0, и девять значений, равных 1. Нет сомнений, что итоговое, «сглаженное» значение, должно быть равно 1. Однако по алгоритму автора мы получим 0 как результат целочисленного деления 9 на 10 с отбрасыванием дробной части.
Правильно:
... average = round( (float)total / _numReadings ); ...
И ещё ошибка: пока не будет считано 10 значений, все результаты фактически являются неправильными, т.к. усредняются с инициализирующими буфер нулями. В большинстве практических применений это, наверное, не имеет большого значения. А где имеет — легко поправить код. На раз публикация носит академический характер, то я считаю это недопустимой ошибкой.
Из вышесказанного следует, что не стоит слепо принимать на веру любой код программы, даже если он опубликован на сайте популярного open-source проекта.
Пройдёмся теперь по скетчу DF-робота.
В целом, у них такой же стиль, как и у скетча-донора. Поэтому остановимся только на различиях.
В оригинале:
... const int numReadings = 30; ...
Буфер увеличен до 30. Почему именно до этой величины — непонятно.
В оригинале:
... float readings[numReadings]; // the readings from the analog input float total = 0; // the running total float average = 0; // the average ...
Все величины они сделали float. Это устраняет вышеупомянутую ошибку с неправильным округлением. Но это в корне неверный подход. analogRead() даёт целую величину, в буфере хранятся эти же самые целые величины, операции по корректировке суммы всех величин в буфере тоже являются целочисленными. Следовательно, все переменные для хранения этих величин должны иметь тип int. Целому — целое! А вот когда при делении появляется float, то именно в тот момент и надо сделать коррекцию как я написал выше. Т.е. непосредственно перед делением использовать явное приведение типа int к float.
В оригинале:
... float currentValue = 0; ... average = total/numReadings; currentValue= average; Serial.println(currentValue); ...
Очень сомнительная идея, захламляющая код: ввести в программу мнемоническую переменную (currentValue) только для того, чтобы показать, что вычисленное среднее значение один к одному становится текущим значением целевого параметра (тока, в данном случае).
В оригинале:
... Serial.println(currentValue); ...
По умолчанию, Serial.println выведет 2 знака после запятой. Что, учитывая контекст, эквивалентно утверждению производителя «Датчик + Arduino c 10-битным аналоговым портом меряют ток с точностью 0.01″ (10 бит потому, что в коде скетча считанное значение после знакового сдвига делится на 1024). А так ли это?
DFROBOT пишет про свой датчик: «Sensitivity: 40 mV/A».
10-битный аналоговый порт с опорным напряжением в 5 вольт (это тоже следует из кода скетча) измеряет напряжение с точностью 5/1024 = ~0.005.
Таким образом, максимальная теоретическая точность измерений: (5/1024) / 0.04 = ~0.12 ампер.
Т.е. ни о каких сотых долях ампера речи и быть не может.
Поэтому правильно:
... Serial.println( currentValue, 1 ); ...
В оригинале:
... readings[index] = analogRead(0); ...
Тут 2 ошибки стиля. Во-первых, лучше использовать мнемоническую константу А0. Во-вторых, лучше сделать как в оригинале: завести константный идентификатор, инициализацию которого вынести в верхнюю часть кода, и использовать потом его.
В оригинале:
... Serial.begin(57600); ...
Это вообще что-то очень странное. Почему именно такая скорость? Скорее всего, у большинства в Arduino IDE в настройках стоит 9600 по умолчанию, чего хватает для отладочных нужд. Чтобы заработал этот скетч, надо лезть в меню и менять скорость. А при отладке другого скетча, наверняка, возвращать в умалчиваемое значение. Или исправлять скорость на 9600 в этом скетче. В любом случае, какие-то совсем ненужные хлопоты. А у неопытного пользователя может возникнуть впечатление, что указанная скорость является рекомендуемой производителем и как-то связана с аппаратными особенностями датчика.
В оригинале:
... delay(30); ...
Может быть 57600 взято потому, что при задержке в 30 мс между измерениями скорости 9600 не хватит для передачи данных? Проверим это.
Каждые 30 мс скетч будет отправлять максимум 6 байт (например, 17.67 + ‘\n’). Для этого потребуется канал: 6 байт * 8 бит/байт / 0.03 с = 1600 бод. Т.е. 9600 хватает с большим запасом.
Ну и с точки зрения визуального контроля отладочной информации, delay(30) — это запредельно быстро и не имеет никакого практического смысла. Не думаю, что даже китайцы в состоянии контролировать показатели со скоростью 30 измерений в секунду.
К скорости работы датчика и порта «delay(30)» тоже не имеет никакого отношения.
Наверняка, DFROBOT и Массимо Банци читают блоги Амперки и поправят свои программные косяки.
Валерий Ярцев