[Из песочницы] Почему Arduino такая медленная и что с этим можно сделать

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 ниже чем частота ядра. А вот насколько ниже и как её выставить всегда можно посмотреть в даташите на конкретный контроллер.

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


© Habrahabr.ru