Arduino скорость выполнения

Ускоряем программу под ArduinoIDE (Arduino NaNO, UNO, MEGA)

Скачать материалы к данной статье

  1. Если в программе много расчетов, рекомендую заменить числа с плавающей точкой целыми числами.
  2. Убрать приостановки программы функцией delay(). Использовать временные метки.
  3. Заменить чтение и запись в GPIO работой с портами ввода вывода.
  4. Уменьшить точность чтения аналогового сигнала функций analogRead(). https://github.com/avandalen/avdweb_AnalogReadFast/blob/master/avdweb_AnalogReadFast.h
  5. Не пользоваться функциями задерживающими выполнение программы , например, pulseIn().
  6. Не использовать Software Serial порты и прочие программные эмуляторы протоколов обмена.
  7. Отключить вывод данных в Serial порт.
  8. Где возможно, использовать 8-разрядные числа.
  9. Применить быстрый контроллер.

Микроконтроллер (ATMega328) Arduino Nano/Uno является довольно медленными и за один такт выполняет только одну 8-и битовую операцию. Этого достаточно в большинстве приложений для данных контроллеров. Если вы используете Arduino NANO с парой датчиков, для анализа обстановки пару раз в секунду, то задумываться об ускорении вашей программы не стоит. А вот если таких датчиков много, допустим пара десятков, подключенных через мультиплексор, с которых считывается аналоговый сигнал, затем сигнал анализируется, обрабатывается и передается на исполнительных механизм, то вы может столкнуться с тем, что ряд показаний датчиков будет теряться, система ошибается «виснет».

Давайте проверим насколько данные рекомендации эффективны

Создадим тестовые программы, в которых масса расчетов производится в цикле, а время начала и окончания цикла запоминается, и выводится в порт для анализа.

1. Пункт — заменить числа с плавающей точкой целыми числами

Тестирование примера 1 показывает, что при использовании типа int (целое 16 разрядное знаковое для 8битных плат) в расчетах вместо float (32 разрядное с плавающей точкой) дает ускорение более чем в 3 раза. Использование типа int32_t уже не столь эффективно, а вот использование in8_t , дает ускорение аж в 6 раз.

Отметим, что использовать в расчетах переменные длиной в 1 или 2 байта (int8_t, int16_t) следует только, если значения (даже внутри формулы) в расчетах не превышают пределы (-128/127, -32768/32767). Если возможны, даже промежуточные выходы получаемых в результате расчетов чисел за указанные пределы, то результаты вычислений будут неверными.

2. Пункт — Убрать приостановки программы функцией delay(). Использовать временные метки

Допустим, в программе один раз в 10 секунд требуется выводить некоторое значение в порт.
Возьмем в качестве примера расчет из первого пункта. Мы в качестве задержки вывода используем функцию delay(количество миллисекунд задержки), но во время данной задержки никакие расчеты производиться не могут (кроме обработки прерываний), и вычислительная способность программы падает, программа простаивает.
Сделаем так, чтобы наши расчеты выполнялись без задержки, но через 10 секунд. Для этого введем дополнительную глобальную переменную (описана не в функции, а в основной программе)
uint32_t goTime = 0; ,
в которой будем хранить время запуска следующего расчета
if (goTime

Источник

Ускоряем свою Arduino

Месяца 3 назад, как и многие горе-электроники, купил себе на мой тогдашний взгляд самую навороченную микропроцессорную плату из семейства Arduino, а именно Seeeduino Mega, на базе процессора Atmega1280. Побаловавшись всласть вращающимся сервоприводом и моргающим светодиодом, встал вопрос: «зачем же я её купил?».

Я работаю одним из ведущих конструкторов на одном крупном военном Зеленоградском заводе, и в данный момент веду проект по разработке метрологического средства измерения. В данной задаче существует бесконечное множество проблем, которые требуют индивидуального решения. Одной из таких задач является управление шаговым двигателем без шумов и с шагом не 1.8 градуса, как сказано в документации шагового двигателя, а до 0.0001 градуса. Казалось бы, задача сложна и нерешабельна, но, повозившись немного со схемами управления, пришёл к выводу, что всё реально и возможно. Требуется только генерация двух сигналов специфичной формы и со сдвигом фаз и частотой изменения напряжения до 1 МГц. (Подробное исследование шагового мотора и раскрытие всех тайн управления напишу в следующей статье) Сразу же в голове стали появляться проблески надежды, что я не зря потратил 1500 рублей на свою красненькую Seeeduino, и я, набравшись энтузиазма, начал разбираться.

Первоначальный ужас:

Подключив микропроцессорную плату к осцилографу, и написав цикл digitalWrite(HIGH), и ниже digitalWrite(LOW), на осцилографе обнаружил довольно унылый меандр с частотой 50Гц. Это кошмар. Это крах, подумал я, на фоне требуемых 1Мгц.
Далее, через осцилограф, я изучил еще несколько скоростей выполнения:
AnalogRead() — скорость выполнения 110 мкс.
AnalogWrite() — 2000 мкс
SerialPrintLn() — при скорости 9600 около 250мкс, а при максимальной скорости около 3мкс.
DigitalWrite() — 1800мкс
DigitalRead() — 1900мкс

На этом я, всплакнув, чуть не выкинул свою Seeeduino. Но не тут-то было!

Глаза боятся, руки делают!

Не буду рассказывать свои душевные муки и описывать три долгих дня изучения, лучше сразу скажу всё как есть!
Подняв всю возможную документацию на Arduino и на процессор Atmega1280, исследовав опыт зарубежных коллег, хочу предложить несколько советов, как заменять чтение/запись:

Улучшаем AnalogRead()

#define FASTADC 1

// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &=

_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

void setup() <
int start ;
int i ;

#if FASTADC
// set prescale to 16
sbi(ADCSRA,ADPS2) ;
cbi(ADCSRA,ADPS1) ;
cbi(ADCSRA,ADPS0) ;
#endif

Serial.begin(9600) ;
Serial.print(«ADCTEST: «) ;
start = millis() ;
for (i = 0 ; i

Результат: скорость 18,2 мкс против бывших 110 мкс.
Кстати, максимальная скорость АЦП Атмеги как раз 16мкс. Как вариант — использовать другую микросхему, заточенную именно под АЦП, которая позволит уменьшить скорость до 0,2мкс (читать ниже, почему)

Улучшаем digitalWrite()

Каждая Arduino/Seeeduino/Feduino/Orduino/прочаяduino имеет порты. Каждый порт — 8 бит, которые сначала надо настроить на запись. Например, на моей Seeeduino PORTA — c 22 по 30 ножку. Теперь всё просто. Управляем с 22 по 30 ножки с помощью функций
PORTA=B00001010 (битовая, ножки 23 и 25 — HIGH)
или
PORTA=10 (десятичная, всё так же)
Результат = 0,2мкс против 1800мкс, которые достигаются обычным digitalWrite()

Улучшаем digitalRead()

Практически то же самое, что и в улучшении с digitalWrite(), но теперь настраиваем ножки на INPUT, и используем, например:
if (PINA==B00000010) <. >(если на ножке 23 присутствует HIGH, а на 22 и 24-30 присутствует LOW)
Результат выполнения этого if() — 0.2мкс против 1900мкс, которые достигаются обычным digitalRead()

Улучшаем ШИМ модулятор, или analogWrite()

for (int k=0;k
Вот и получили ШИМ с частотой 19кГц против 50Гц.

Источник

К вопросу о быстродействии и измерении его в Ардуино

Данная задачка возникла при исследовании быстродействия работы Ардуино при выполнении различных команд (об этом в отдельном посте). В процессе исследования возникли сомнения относительно постоянности времени работы отдельных команд при изменении значения операндов (как выяснилось позже, небезосновательные) и было принято решение попытаться оценить время исполнения отдельной команды. Для этого была написана небольшая программа (кто сказал скетч — выйти из класса), которая, на первый взгляд, подтвердила гипотезу. В выводе можно наблюдать значения 16 и 20, но иногда встречаются и 28 и даже 32мксек. Если умножить полученные данные на 16 (тактовую частоту МК), получим время исполнения в тактах МК (от 256 до 512). К сожалению, повторный прогон основного цикла программы (с теми же исходными данными), при сохранении общей картины, дает уже иное распределение времени исполнения, так что действительно имеющие место вариации времени не связаны с исходными данными. Исходная гипотеза опровергнута, но становится интересно, а с чем именно связан столь значительный разброс.

Необходимое примечание — я прекрасно понимаю, что для измерения времени исполнения команд следует использовать более сложные программы, однако для грубой оценки вполне достаточно и такой, что будет продемонстрировано далее.

Итак, время меняется, причем весьма существенно, ищем причины этого явления. Прежде всего, обращаем внимание на кратность полученных величин, смотрим описание на библиотеку работы со временем и видим, что 4мксек — это квант измерения, поэтому лучше перейти к квантам и понимаем, что мы получаем 4 либо 5 (весьма часто) и 6 либо 7 либо 8 (весьма редко) единиц измерения. С первой половиной все легко — если измеряемое значение лежит между 4 и 5 единицами, то разброс становится неизбежным. Более того, считая отсчеты независимыми, мы можем статистическими методами повысить точность измерения, что и делаем, получая приемлемые результаты.

А вот со второй половиной (6,7,8) дела хуже. Мы выяснили, что с исходными данными разброс не коррелирует, значит, это проявление других процессов, влияющих на время исполнения команд. Отметим, что выбросы достаточно редкие и на вычисляемое среднее значение существенным образом не являют. Можно было бы вообще ими пренебречь, но это не наш стиль. Я вообще за годы работы в инженерии осознал, что нельзя оставлять непонятки, сколь бы незначительными они не казались, поскольку они имеют отвратительное свойство бить в спину (ну или еще куда дотянутся) в самый неподходящий момент.

Начинаем выдвигать гипотезу 1 — самая удобная (в удобстве и универсальности она уступает только прямому вмешательству Творца) – глюки программного обеспечения, конечно же, не моего, мои программы никогда не глючат, а подключаемых библиотек (компилятора, операционной системы, браузера и т.д. – нужное подставить). Более того, поскольку я прогоняю программу в эмуляторе на www.tinkercad.com, то можно еще сослаться на баги эмулятора и закрыть тему, ведь исходники нам не доступны. Минусы данной гипотезы:

  1. От цикла к циклу расположение отклонений меняется, что намекает.
  2. Этот сайт все таки поддерживает AutoDesk, хотя аргумент слабоват.
  3. «Мы приняли постулат, что происходящее не является галлюцинацией, иначе было бы просто неинтересно».

Следующая предположение – влияние некоторых фоновых процессов на результат измерения. Вроде бы ничего не делаем, кроме как считаем, хотя … мы же выводим результаты в Serial. Возникает гипотеза 2 – время вывода иногда (странно как то… но все бывает) добавляется к времени выполнения команды. Хотя сомнительно, сколько там того вывода, но все равно – добавляем Flush и не помогло, добавляем задержку на завершение вывода и не помогло, вообще выносим вывод за пределы цикла – все равно время прыгает – это точно не Serial.

Ладно, что осталось – собственно организация цикла ( с какого перепугу ей менять свою длительность, не понятно) и все … хотя остался micros(). Я подразумевал, что время выполнения первого вызова этой функции и второго одинаково и при вычитании этих двух значений получу ноль, но если это предположение неверно?

Гипотеза 3 – иногда второй вызов отсчета времени выполняется дольше, нежели первый либо действия, связанные с отсчетом времени, иногда влияют на результат. Смотрим исходный код функции работы со временем (arduino-1.8.4\hardware\arduino\avr\cores\arduino\wiring.c – я уже неоднократно выражал свое отношение к подобным вещам, повторяться не буду) и видим, что 1 раз из 256 циклов аппаратного увеличения младшей части счетчика происходит прерывание для инкрементирования старшей части счетчика.

Наше время исполнения цикла от 4 до 5, поэтому можно ожидать 170*(4..5)/256 = от трех до четырех аномальных значений на отрезке из 170 измерений. Смотрим – очень похоже, их действительно 4 штуки. Чтобы разделить первую и вторую причину, делаем вычисления критической секцией с запрещенными прерываниями. Результат особо не меняется, выбросы все равно имеют место быть, значит, дополнительное время вносит вызов micros(). Здесь мы не можем ничего поделать, исходный код хотя и доступен, но менять мы его не можем – библиотеки включены в бинарях. Конечно, мы можем написать свои собственные функции работы со временем и смотреть их поведение, но есть путь проще.

Раз возможной причиной увеличения длительности являются «длинная» обработка прерывания, исключим возможность его возникновения в процессе измерения. Для этого дождемся его проявления и только потом этого проведем цикл измерения. Поскольку прерывание возникает намного реже, чем длится наш цикл измерения, то можно гарантировать его отсутствие. Пишем соответствующий фрагмент программы (пользуясь грязными хаками информацией, извлеченной из исходного кода) и, «это такая уличная магия», все становится нормально – мы измеряем время исполнения 4 и 5 квантов со средним значением времени исполнения операции сложения с ПТ в 166 тактов, что соответствует ранее замеренному значению. Гипотезу можно считать подтвержденной.

Остается еще один вопрос – а что так долго делается в прерываниях, что это занимает
(7,8) – (5)

2 кванта = *4 = 8мксек *16 = 128 тактов процессора? Обращаемся к исходному коду (то есть к ассемблерному коду, сформированному компилятором на сайте godbolt.com) и видим, что собственно прерывание исполняется приблизительно 70 тактов, из них 60 постоянно, а при считывании имеются дополнительные расходы в 10 тактов, итого 70 при попадании на прерывание – меньше, чем получено, но достаточно близко. Разницу отнесем на различие компиляторов либо режимов их использования.

Ну и теперь мы можем замерить собственно время исполнения команды сложения ПТ с различными аргументами и убедиться, что оно действительно сильно меняется при изменении аргументов: от 136 тактов для 0.0 до 190 для 0.63 (магическое число), причем составляет всего 162 для 10.63. С вероятностью 99.9% это связано с необходимостью выравнивания и особенностями его реализации в данной конкретной библиотеке, но это исследование явно выходит за пределы рассматриваемой задачи.

Источник

Почему Arduino такая медленная и что с этим можно сделать

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

Часть 1 «Вопросы»

Цитируя автора указанной статьи:

Получается проигрыш производительности в данном случае — 28 раз. Разумеется что это не значит, что ардуино работает в 28 раз медленнее, но я считаю, что для наглядности, это лучший пример того, за что не любят ардуино.

Так как статья только началась, не будем пока разбираться, а проигнорируем второе предложение и будем считать что скорость работы контроллера приблизительно эквивалентна частоте переключения пина. Т.е. перед нами стоит задача сделать генератор наибольшей частоты из того что имеем. Для начала посмотрим насколько всё плохо.

Напишем простую программу для ардуино (по сути просто скопируем blink).

Зашиваем в контроллер. Так как у меня нет осциллографа, а только китайский логический анализатор, его необходимо правильно настроить. Максимальная частота анализатора 24 MHz следовательно её необходимо уравнять с частотой контроллера — выставить 16MHz. Смотрим .

… долго. Пытаемся вспомнить, от чего зависит скорость работы контроллера — точно, частота. Смотрим в arduino.cc. Clock Speed — 16 MHz, а у нас тут 145.5 kHz. Что делать? Попробуем решить в лоб. На том же arduino.cc смотрим остальные платы:

  • Leonardo — не подайдёт — там тоже 16 MHz
  • Mega — тоже — 16 MHz
  • 101 — подойдёт — 32MHz
  • DUE — ещё лучше — 84 MHz

Можно предположить, что если увеличить частоту контроллера в 2 раза, то частота мигания светодиода тоже увеличится в 2 раза, а если в 5 — то в 5 раза.

Мы не получили желаемых результатов. Да и генератор все меньше и меньше напоминает меандр. Думаем дальше — теперь, наверное, язык плохой. Вроде как есть с, с++, но это сложно(в соответствии с эффектом Даннинга-Крюгера мы не можем осознать что уже пишем на с++), потому ищем альтернативы. Недолгие поиски приводят нас к BASCOM-AVR (тут неплохо про него рассказано), ставим, пишем код:

Результат намного лучше, к тому же получился идеальный меандр, но… бейсик в 2018м, серьезно? Пожалуй, оставим это в прошлом.

Часть 2 «Ответы»

Кажется, уже пора переставать валять дурака и начинать разбираться (а также вспомнить си и ассемблер). Просто скопируем «полезный» код из статьи, упоминавшейся в начале, в loop().

Здесь, полагаю, нужно пояснение: весь код будет писаться в проекте ардуино, но в среде Atmel Studio 7.0 (там удобный дизассемблер), скрины будут из неё же.

Вот оно! Почти то, что нужно. Только форма не особо на меандр похожа и частота, хоть и уже ближе, но все равно не та. Также попробуем увеличить масштаб и обнаружим разрывы в сигнале каждую миллисекунду.

Связано это со срабатыванием прерываний от таймера, отвечающего за millis(). Так что поступим просто — отключим. Ищем ISR (функция обработчик прерывания). Находим:

Много бесполезного для нас кода. Можно изменить режим работы таймера или отключить прерывание, но это излишне для наших целей, поэтому просто запретим все прерывания командой cli(). Так же посмотрим на наш код:

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

Да и переход по loop() занимает много команд, так как это лишняя функция в основном цикле.

Поэтому просто сделаем бесконечный цикл в setup(). Получаем следующее:

61 ns это максимум, соответствующий частоте работы контроллера. А можно ли быстрее? Спойлер — нет. Давайте попробуем понять почему — для этого дизасемблим наш код:

Как видно из скрина, для того чтобы записать в порт 1 или 0 тратится ровно 1 такт, вот только дальше идет переход, который не может быть выполнен меньше чем за один такт (RJMP выполняется за два такта, а, например, JMP, за три). И мы практически у цели — для того, чтобы получился меандр, необходимо увеличить время, когда подан 0, на два такта. Добавим для этого две ассемблерные команды nop, которые ничего не делают, но занимают 1 такт:

Часть 3 «Выводы»

К сожалению, все что мы делали абсолютно бесполезно с практической точки зрения, потому что мы не можем больше исполнять никакой код. Так же в 99,9% случаев частот переключения портов вполне хватает для любых целей. Да и если нам очень нужно генерировать ровный меандр, можно взять stm32 с dma или внешнюю микросхему таймера вроде NE555. Данная статья полезна для понимания устройства работы mega328p и arduino в целом.

Тем не менее запись в регистры 8ми битных значений PORTB = 0b11111111; намного быстрее чем digitalWrite(13, 1); но за это придется заплатить невозможностью переноса кода на другие платы, потому что названия регистров могут отличатся.

Остался лишь один вопрос: почему использование более быстрых камней не дало результатов? Ответ очень прост — в сложных системах частота gpio ниже чем частота ядра. А вот насколько ниже и как её выставить всегда можно посмотреть в даташите на конкретный контроллер.

Источник

Adblock
detector