Закрыть
Оптимизируем digitalWrite на Arduino

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

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

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

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

void setup()
{
 pinMode(13, OUTPUT);
}
void loop()
{
 digitalWrite(13, HIGH);
 digitalWrite(13, LOW);
}

Этот код мигает светодиодом, расположенном на плате и подключенным к 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 мкс).

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

void setup()
{
 pinMode(13, OUTPUT);
}
void loop()
{
 digitalWrite(13, HIGH);
 digitalWrite(13, HIGH);
 digitalWrite(13, LOW);
}

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

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

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

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

void setup()
{
 pinMode(13, OUTPUT);
}
void loop()
{
 digitalWrite(13, HIGH);
 digitalWrite(13, LOW);
 digitalWrite(13, LOW);
}

В результате выполнения этого кода, осциллограмма сигнала на 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.

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

void setup()
{
 DDRB = B10000000; // устанавливаем 13 пин как OUTPUT
}
void loop()
{
 // создаем свой бесконечный цикл
 while (1)
 {
  PORTB = B10000000; // устанавливаем состояние pin13 как HIGH
  PORTB = B00000000; // устанавливаем состояние pin13 как LOW
 }
}

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

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

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

void setup()
{
 DDRB = B10000000; // устанавливаем 13 пин как OUTPUT
}
void loop()
{
 PORTB = B10000000; // устанавливаем состояние pin13 как HIGH
 PORTB = B00000000; // устанавливаем состояние pin13 как LOW
}

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

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

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

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

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

  • Использование регистров портов вместо функции digitalWrite позволяет значительно увеличить скорость выполнения программы.
  • Использованием вложенного в loop () бесконечного цикла while (1){...} мы можем сэкономить несколько тактов работы процессора на каждом витке работы цикла.

[add_ratings]

30 thoughts on “Оптимизируем digitalWrite на Arduino

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    void setup() { pinMode(13, OUTPUT); } void loop() { label: PORTB = B00100000; // устанавливаем состояние pin13 PB5 как HIGH PORTB = B00000000; // устанавливаем состояние pin13 PB5 как LOW goto label; // переходим к метке label }

    1. На будущее «карта» выводов ардуины описаны в файле pins_arduino.h, но таких файлов несколько, есть базовый, который лежит в папке

      «C:\Program Files\Arduino\hardware\arduino\avr\variants\standard\».

      Для некоторых ардуин (леонардо, мега, микро и др.) есть исключения для них есть свои файлы которые лежат

      «C:\Program Files\Arduino\hardware\arduino\avr\variants\»

      Имя порта (PORTA, PORTB, ...) указано в константном массиве digital_pin_to_port_PGM

      Маска бита в порте в массиве digital_pin_to_bit_mask_PGM (по сути он же номер бита).

      Индекс понятное дело идет с нуля...

      Итак берем номер вывода: 13, смотрим что у нас

      digital_pin_to_port_PGM[13] = PB

      digital_pin_to_bit_mask_PGM[13] = _BV (5)

      _BV — это такой макрос, объявленный в недрах пакета AVR

      #define _BV (bit) (1 << bit)

      и нам нужно только число указанное в скобках

      получается PortB, бит 5

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

    PORTB = PORTB^B0010000;

    или PORTB = PORTB^0x20;

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

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

      void setup() { DDRB = B10000000; // устанавливаем 13 пин как OUTPUT } void loop() { while (1) { PORTB = B10000000; // pin13 - HIGH PORTB = PORTB^B10000000; // инвертируем пин 13 PORTB = PORTB^B10000000; // инвертируем пин 13 } }

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

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

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

        Спасибо!

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

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

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

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

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

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

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

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

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

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

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

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

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

    #include "CyberLib.h" long previousMillis = 0; unsigned long currentMillis; void setup() { Serial.begin(9600); } void loop() { Start; D8_Out; D8_High; currentMillis = millis(); if (currentMillis - previousMillis >= 10) { previousMillis = currentMillis; D8_In; if ((pulseIn(8, HIGH) / 58) <= 30) { Serial.println("OBSTACLE"); // OBSTACLE! } else { Serial.println("FREE WAY"); // FREE WAY } } End; }

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

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

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

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

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

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

    Почему?

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

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

    void setup() { DDRB = B10000000; // устанавливаем 13 пин как OUTPUT } void loop() { while (1) { PORTB = B10000000; // pin13 - HIGH PORTB = PORTB^B10000000; // инвертируем пин 13 PORTB = PORTB^B10000000; // инвертируем пин 13 } }

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

  12. Автору спасибо — ржали всем коллективом программистов.

    Вывод о том, что «команда digitalWrite (13,HIGH) выполняется за 6.6 мкс, а digitalWrite (13,LOW) за 7.2 мкс» — это глупейшая ошибка, которой автор наполнил половину статьи.

    А «функция loop не столь уж и эффективна» — это вообще комедия. Автор не только заново «открыл Америку», но и получил оригинальные выводы о том, что в ней живут американцы и якуты.

    К счастью, итоги получились почти верными, но только с одним дополнением:

    «Использованием вложенного в loop () бесконечного цикла while (1){...} мы можем сэкономить несколько тактов работы процессора на каждом витке работы цикла», но при этом потеряем то, что даёт нам функция serialEvent. А именно, обработка входных данных по последовательным портам будет остановлена.

    1. Еще есть вариант фьюзом разрешить передавать тактовый сигнал на выход, 16 МГц сразу получим аппаратно.

      Из интересного можно генерировать тактовый сигнал RC цепочкой и менять частоту программно, регистром коррекции частоты. Если внешняя RC цепочка, меняя параметры RC или напряжения питания получим вообще генератор частоты плавно меняющийся и по точности самокалибрующийся хоть от кварца ежесекундно, хоть от GPS спутников.

  13. Статейку надо актуализировать... время идёт... вносятся изменения... Ведь после выхода статьи прошло 3 года... код

    digitalWrite (13, HIGH);

    digitalWrite (13, LOW);

    выполняется быстрее... общее время — в районе 7нсек, частота получается в районе 150кГц. (IDE 1.8.1, UNO R3 328 16MHz).

    и да... как уже упомянули выше — не работает код команды прямой записи в регистр порта.

Оставить ответ

Ваш email не будет опубликован.Обязательны поля помечены *