Как стать автором
Обновить

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

Время на прочтение4 мин
Количество просмотров24K

LOGO


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


Часть 1 "Вопросы"


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


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

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


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


void setup() {
  pinMode(13, OUTPUT);
}

void loop() {
  digitalWrite(13, 1);   // turn the LED on (HIGH is the voltage level)
  digitalWrite(13, 0);    // turn the LED off by making the voltage LOW
}

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


Test_1


… долго. Пытаемся вспомнить, от чего зависит скорость работы контроллера — точно, частота. Смотрим в 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 раза.


Test_2


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


$Regfile="m328pdef.dat"
$Crystal=16000000
Config Portb.5 = Output

Do
Toggle Portb.5
Loop

Получаем:


Test_3


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


Часть 2 "Ответы"


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


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


void setup() {
  DDRB |= (1 << 5);   // PB5
}

void loop() {
  PORTB &= ~(1 << 5); //OFF
  PORTB |= (1 << 5);  //ON
}

результат:


Test_4


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


Test_5


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


ISR(TIMER0_OVF_vect)
{
  // copy these to local variables so they can be stored in registers
  // (volatile variables must be read from memory on every access)
  unsigned long m = timer0_millis;
  nsigned char f = timer0_fract;

  m += MILLIS_INC;
  f += FRACT_INC;
  if (f >= FRACT_MAX) {
    f -= FRACT_MAX;
    m += 1;
  }

  timer0_fract = f;
  timer0_millis = m;
  timer0_overflow_count++;
}

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


PORTB &= ~(1 << 5); //OFF
PORTB |= (1 << 5);  //ON

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


PORTB = 0b00000000; //OFF
PORTB = 0b11111111; //ON

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


int main(void)
{
  init();
// ...
  setup();

  for (;;) {
    loop();
  if (serialEventRun) serialEventRun();
  }

  return 0;
}

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


void setup() {
  cli();
  DDRB |= (1 << 5);    // PB5
  while (1) {
    PORTB = 0b00000000; //OFF
    PORTB = 0b11111111; //ON
  }
}

Test_6


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


Code_asm_1


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


void setup() {
  cli();
  DDRB |= (1 << 5);    // PB5
  while (1) {
    PORTB = 0b00000000; //OFF
    asm("nop");
    asm("nop");
    PORTB = 0b11111111; //ON
  }
}

Test_end


Часть 3 "Выводы"


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


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


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


В публикации ссылался на статьи:



Теги:
Хабы:
Всего голосов 50: ↑42 и ↓8+34
Комментарии84

Публикации

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань