Оптимизируем digitalWrite на Arduino

тест функции digitalWrite

Сегодня я протестирую фактическую скорость работы функции digitalWrite на своей Arduino Mega2560 и расскажу как ускорить работу программы в 50 раз! В основе отладочной платы Arduino Mega2560 лежит микроконтроллер AT2560, работающий с тактовой частотой 16 Мгц. Если перевести эти 16 миллионов колебаний во временной интервал, то получим достаточно небольшой период, равный 62.5 нс. Это быстро, но действительно ли Arduino выполняет операции с такой же скоростью? Давайте посмотрим.

Команды, которые мы пишем на языке Wiring, в процессе компиляции преобразуются в более простые команды, так называемый, машинный код, которые микроконтроллер уже непосредственно выполняет. Некоторые команды микроконтроллера выполняются за один такт работы микроконтроллера, на некоторые требуются больше тактов, соответственно и времени выполнения. Поэтому одна, на наш взгляд, простая команда или функция будет выполняться микроконтроллером за несколько, может даже несколько десятков или сотен тактов. К тому же, помимо вычислений, нам требуются операции чтения/записи в память, поэтому, средняя частота обработки команд будет значительно отличаться от тактовой частоты микроконтроллера. Вопрос только на сколько.

Попробуем выяснить сколько же выполняется простая команда изменения значения на цифровом выходе Arduino - digitalWrite. Проведем несколько экспериментов. В ходе первого эксперимента выполним следующий код.

Этот код мигает светодиодом, расположенном на плате и подключенным к 13 выводу. Мы просто поочередно меняем состояние этого выхода. Если запустить этот код, то мы увидим, что светодиод будет светится непрерывно, но, на самом деле это не так. Светодиод включается и выключается, просто наш глаз не может воспринимать колебания частотой свыше 25 Гц и такие колебания мы видим как постоянно горящий светодиод с яркостью, определяемой скважностью подаваемого сигнала.

Для того, чтобы посмотреть, что же на самом деле происходит на 13 выводе, я воспользуюсь осциллографом. Осциллограмма сигнала на 13 пине выглядит так:

Осциллограмма выполнения  команды digitalWrite
Похоже, что команда digitalWrite (13, HIGH) выполняется за 6.6 мкс, а digitalWrite (13, LOW) за 7.2 мкс. Итого 13.8 мкс. Это намного дольше, чем 62.5 нс, в действительности, в 220 раз дольше. Также, можно заметить, что нахождение с состоянии LOW (7.2 мкс) занимает больше времени, чем нахождение в состоянии HIGH (6.6 мкс).

Проведем следующий эксперимент.

Теперь мы переводим дважды 13 пин в состояние HIGH, а затем один раз в LOW и цикл повторятся заново. Я ожидал увидеть значение времени для состояния HIGH равное 6.6 × 2 = 13.2 мкс и для LOW равное по прежнему 7.2 мкс. Посмотрим на фактическую осциллограмму сигнала, полученного в результате выполнения второго скетча.

Осциллограмма digitalWrite со скважностью 2

По факту, две инструкции, переводящие вывод в состояние HIGH дважды занимают 19.4 мкс, или, в среднем, по 9.7 мкс на одну команду, на нахождение в состоянии LOW, по прежнему, уходит 7.2 мкс.

Попробуем реализовать теперь еще одну последовательность состояний: HIGHLOWLOW.

В результате выполнения этого кода, осциллограмма сигнала на 13 пине выглядит так:

Осциллограмма digitalWrite со скажностью 0.5

Единственная инструкция HIGH занимает 6.8 мкс — примерно так же как и ожидалось (6.6 мкс). Две подряд команды, переводящие вывод в состояние LOW занимают 13.8 мкс — это чуть меньше, чем ожидаемые 14.4 мкс (7.2 × 2 ).

Что получатеся? В цикле loop (), используя функцию digitalWrite, мы можем менять состояние пина с частотой максимум 72 кГц, а в отдельных случаях, эта частота может быть и ниже, например, как во втором случае — около 37 кГц.  Такая частота значительно меньше тактовых 16 Мгц, но если использовать прерывания по таймеру, то мы можем значительно увеличить этот показатель.

Реализация функции digitalWrite в языке Wiring является, мягко говоря, не оптимальной. Если посмотреть на ее реализацию на Ассемблере, то можно обнаружить несколько проверок, в частности, проверятся не нужно ли выключить таймер ШИМ после предыдущей функции analogWrite (). Наиболее быстрая реализация, но, в то же время и наиболее затратная по времени ее написания, могла бы быть на языке Ассемблер. Но написание кода на Ассемлере — то еще насилие над собой. Я уже не говорю про отладку ассемблерного кода. Одним из компромиссных решений может быть использование оптимизированных библиотек, реализующих различные функции ввода/вывода. В дальнейших своих публикациях я рассмотрю некоторые из таких библиотек.

Еще одной причиной задержки является сама функция loop (). Когда все команды внутри loop () выполнены, то неявно для нас выполняется еще код, который производит возврат к повторному выполнению команд внутри этого цикла. Именно поэтому время последовательности из двух одинаковых состояний в конце loop () не равно сумме длительностей одиночного переключения состояния — здесь микроконтроллером еще выполняются инструкции, производящие возврат к началу цикла.

Для того, чтобы сократить время изменения состояния какого-либо вывода можно использовать команды прямой записи в регистр порта. Для изменения значений пинов с 8 по 13 используется регистр PORTB в который необходимо записать слово данных (два младших бита этого слова данных, то есть 1 и 2 биты не используются, а оставшиеся шесть — как раз и устанавливают состояния цифровых портов с 8 по 13). Для изменения состояний цифровых пинов с 0 по 7 используется PORTD.

Приведу пример:

И вновь смотрим на осциллограму получившигося сигнала на 13 выводе:

Использование регистров Arduino для изменения значений портов

Мы получили заветные 62 нс! Наша программа выполняется за 4 рабочих такта микроконтроллера. Фантастика! Режим работы 13 пина в качестве цифрового выхода мы устанавливаем всего один раз и сколько уходит на это времени — нас мало интересует. Один рабочий такт микроконтроллера уходит на перевод состояния 13 вывода в HIGH, еще один — для установки состояния этого пина в LOW и за два такта, с помощью бесконечного цикла while (1){...} мы циклически возвращаемся вновь к установке состояния в HIGH. Зачем же здесь while (1), когда loop () делает фактически то же самое — циклически выполняет код, можете спросить вы? Изменим скетч, просто убрав из loop вложенный бесконечный цикл:

И вновь обратимся к помощи осциллографа:

Осциллограмма оптимизированного скетча

Как можно видеть, частота снизилась с 4 МГц до 1 Мгц. Время нахождения в состоянии HIGH не изменилось и по-прежнему равно 62.5 нс (на осцилограмме просто показано, Wid (1)<100.0ns). Следовательно, остальное время уходит на возврат к началу, и на это уходит в восемь раз больше времени, то есть 8 тактов. Вывод: функция loop не столь уж и эффективна.

В этом примере мы получили четырехкратное увеличение скорости, но это предельный случай. На самом деле, мы экономим несколько тактов работы и при увеличении времени выполнения программного кода внутри loop эффект сокращения времени работы программы значительно снизится.

Итак, подведу итог:

  • Использование регистров портов вместо функции digitalWrite позволяет значительно увеличить скорость выполнения программы.
  • Использованием вложенного в loop () бесконечного цикла while (1){...} мы можем сэкономить несколько тактов работы процессора на каждом витке работы цикла.
 
Как вы оцениваете эту публикацию? 1 звезда2 звезды3 звезды4 звезды5 звезд (49 голосов, средняя оценка: 4.82 из 5)
Loading ... Loading ...

Вы можете пропустить чтение записи и оставить комментарий. Размещение ссылок запрещено.

26 комментариев к записи “Оптимизируем digitalWrite на Arduino”

  1. Zamuhrishka пишет:

    Безусловно это клёво. Только при этом теряется вся суть работы с arduino — удобство и дистанцирование от архитектуры микроконтроллера. Если нужно писать быстрый и компактный код, то уж точно не следует писать его на Wiring. )))))

  2. valeriy пишет:

    спасибо мне очень помогло. интересно а как будет вести ардуино due в этом случае? с 84мгц тактовой частотой.

  3. denn пишет:

    while () действительно работает!

    наткнулся на статью, решил проверить. так как есть осциллограф, подумал почему бы нет, а дай ка посмотрю...

    записать в порты не получилось (nano v3.1). пока не знаю как работают мк, не смог найти нужный порт.

    а вот с while () действительно разница получилась. на простом примере с hi low после low, если без while, дополнительно 290 нс образуются. может это не много, но все же. наверное и есть результат работы loop ().

    тогда вопрос зачем она нужна вообще?

    не знал что так просто можно обращаться к мк (DDRB = B10000000).

    спасибо за идею)

    • То чем мы с вами занимаемся — это порча языка Wiring, сила которого в простоте. Когда мы пытаемся по максимуму задействовать возможности контроллера, например, напрямую работая с его регистрами, мы отходим от идеологии Wiring. Если нужна производительность, то лучше писать на чистом С/С++ и максимально использовать особенности архитектуры микроконтроллера.

      Без loop в скетче Wiring отказывается компилировать и выдает ошибку.

      • denn пишет:

        ну ардуино первый шаг в освоении мк.

        в дальнейшем конечно переду на С++ и с другими компиляторами.

        про порты разобрался. спасибо)

  4. Сергей пишет:

    Не надо мозги новичкам пудрить. Какое еще ускорение в 4 раза от while. Ускорение происходит ровно на 4 такта за весь цикл, и если на выполнение цикла нужно, к примеру, 100 тактов(что не так уж и много), то ускорение составляет всего 4%, и чем длиннее программа, тем это ускорение менее заметно.

    • Согласен с вами, Сергей. Время выполнения всего цикла loop не учел. Мы действительно экономим несколько тактов на каждом витке цикла loop. Спасибо, что исправили.

  5. Giyora пишет:

    Очень грамотно оформлена статья. Содержание тоже — понятно и локанично. Мне помоглכ закрутить трехфазный двигатель с проверкой фолдов и регулирования скорости и скважности в цикле. Спасибо.

  6. Alex пишет:

    Попробовал сделать и получилось. Долго искал Pin 13 со светодиодом. Оказалось это порт В5. Частота с использованием Loop 1 мГц. Использование оператора Goto на метку внутри Loop получил 4 мГц. Все как и положено.

  7. George пишет:

    А кто-нибудь с осциллографом попробуйте следующую комманду:

    или

    Насколько изменится время мигания?

    • Код:

      Результат:

      Осцилограмма XOR

    • Вот так, наверное, понагляднее будет:

      Время выполнения XOR: 184нс, то есть 3 рабочих такта процессора:

      Время выполнения операции XOR

      • George пишет:

        Да. Ваша реализация быстрее XORа получается, причем значительно...

        Спасибо!

      • Роман пишет:

        Откуда выбросы на фронтах? Там около 1,5 вольт...

        На других осциллограммах их не видно.

        Как-то по другому подключались?

        • Фильтр входной на осцилографе выключил — выбросы и полезли (полоса пропускания не ограничивается)

          • Nikolay пишет:

            Андрей может еще кабель звенеть

            его входная емкость. спс за статьи

          • Согласен, может «звенеть» и кабель осциллографического пробника. Это происходит из-за того, что любой провод («земля» пробника) обладает некоторой распределенной индуктивностью, которая при взаимодействии с емкостью щупа и порождает «звон» на некоторой частоте, которая определяется значениями L и C. Этот «звон» присутствует всегда и представляет собой наложенную на импульсы затухающую синусоиду. Как правило, производители осциллографов конструируют заземление пробника таким образом, чтобы частота «звона» лежала за пределами полосы пропускания системы пробник/осциллограф. В электрических цепях часто присутствуют паразитные емкости и индуктивности. Поэтому, эффект «звона» наблюдается в той или иной степени повсеместно. Конечно, все это нужно учитывать при проектировании радиоэлектронной аппаратуры.

  8. Виктор пишет:

    Спасибо большое за Ваши статьи.

  9. Саша пишет:

    Еще есть библиотека, которая делает те же и другие вещи по ускорению ардуины.

    Библиотека «CyberLib» даёт существенный прирост скорости (запись/чтение цифровых портов в 20 раз).

    Обеспечивает стабильную работу микроконтроллера.

    Уменьшает размер используемой памяти.

    Попробуйте поискать ее (ссылку нельзя)

  10. Саша пишет:

    Интересно, а как себя ведет функция pulseIn, если ее померить на осциллографе...

    Я подключаю Ультразвуковой датчик измерения расстояния HC-SR04 к 1 (!) пину, и мне надо менять состояния этого пина с выхода на вход. С помощью регистров я смогу делать это быстро. Но не будет ли тормозить сам pulseIn?

    • Саша, здравствуйте! Никак руки не дойдут вам ответить))) За наводку на библиотеку CyberLib — большое спасибо! Осциллограмму pulseIn выложу на днях.

  11. Николай пишет:

    А как записать два пина сразу?

  12. Сергей пишет:

    Объясните, пож.

    1. Загружаю скетч в Ардуино micro pro (Леонардо), где информация по USB каждую секунду направляется на выход. В консоли вижу ежесекундное обновление.

    2. Подключаю вместо консоли TeraTerm\ttermpro.exe". В окне вижу ежесекундное обновление.

    3. Все закрываю, отключаю ардуино от USB.

    4. Подключаю ардуино к USB. Подключаю TeraTerm\ttermpro.exe". В окне вижу обновление через 8 секунд.

    Почему?

  13. Саша пишет:

    Андрей Антонов

    Ваще код не работает на атмеге

    А вот ваше показала регультат 4 мГц

Оставить комментарий