Ускорение Arduino

Введение Для многих из нас мы начали программировать на настольных компьютерах и серверах, которые, казалось, обладали бесконечной памятью и вычислительной мощностью (ну, я полагаю, в зависимости от того, когда вы начали программировать). Не было особых причин для оптимизации вашего кода, поскольку в любом случае вы вряд ли превысите ограничения системы. А потом, когда вы занялись встраиваемыми системами, было грубое пробуждение. Переход от такой мощной системы к гораздо меньшей и менее способной, такой как Arduino [/ arduino-separated /], был

Вступление

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

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

После нескольких недель споров с микроконтроллером, чтобы выжать максимум вычислительной мощности для контроллера полета дрона, я подумал, что напишу статью, чтобы помочь вам найти способы повысить скорость и эффективность ваших собственных проектов.

В этой статье я сосредоточусь на Arduino Uno, поскольку это, кажется, самая распространенная плата, хотя большая часть этой статьи также должна относиться и к другим платам.

Почему Arduinos медленные

Тактовая частота

Прежде всего, вы работаете так же быстро, как ваши часы (без учета многоядерных процессоров), которые Arduino Uno по умолчанию использует кристалл с частотой 16 МГц. Это означает, что микроконтроллер ATmega может выполнять до 16 миллионов инструкций в секунду. 16 миллионов инструкций в секунду могут показаться большим количеством (и так оно и есть), но если учесть, что все, что нужно Arduino для выполнения даже простых операций, на самом деле не так уж и много. Для многих проектов тактовые циклы распределяются между такими вещами, как вычисления, связь I2C, чтение и запись на контакты и регистры, а также многие другие операции.

Даже тогда, казалось бы, простые команды могут занять довольно много тактов, например, установка цифрового вывода на высокий уровень. Это одна из простейших операций ввода-вывода, которые вы можете выполнить на Arduino, но на самом деле она занимает очень много времени (более 50 тактов!) Из-за количества кода, используемого в digitalWrite() , который я рассмотрю в следующий раздел. Таким образом, более быстрые часы позволят вам выполнять инструкции в более быстром темпе.

Проверки безопасности и валидация

Так почему же Arduinos медленные, если не считать тактовой частоты? Что ж, в основном это связано с некоторыми стандартными вызовами методов и объектами, которые мы используем в нашем коде. Вот лишь несколько основных виновников:

  • digitalWrite()
  • digitalRead()
  • pinMode()

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

 void digitalWrite(uint8_t pin, uint8_t val) 
 { 
 uint8_t timer = digitalPinToTimer(pin); 
 uint8_t bit = digitalPinToBitMask(pin); 
 uint8_t port = digitalPinToPort(pin); 
 volatile uint8_t *out; 
 
 if (port == NOT_A_PIN) return; 
 
 // If the pin that support PWM output, we need to turn it off 
 // before doing a digital write. 
 if (timer != NOT_ON_TIMER) turnOffPWM(timer); 
 
 out = portOutputRegister(port); 
 
 uint8_t oldSREG = SREG; 
 cli(); 
 
 if (val == LOW) { 
 *out &= ~bit; 
 } else { 
 *out |= bit; 
 } 
 
 SREG = oldSREG; 
 } 

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

Первые несколько строк используют pin чтобы найти соответствующий timer , bit и port для данного вывода. На port - это просто регистр с отображением памяти, который управляет несколькими выводами. Чтобы включить или выключить только тот вывод, который нам нужен, нам нужно определить, какому биту регистра соответствует наш вывод, что и делает функция digitalPinToBitMask()

Найдя timer , bit и port , мы проверяем, действительно ли это правильный контакт. Эта строка не требуется для функции digitalWrite() для выполнения своей работы, но она действует как подстраховка для более неопытных программистов (и даже для опытных). Мы бы очень не хотели, чтобы запись в неправильный адрес памяти приводила к повреждению программы.

if (timer != NOT_ON_TIMER) ... предназначена для того, чтобы убедиться, что мы завершили любое предыдущее использование вывода ШИМ перед записью «постоянного» высокого или низкого уровня. Многие выводы на Arduinos также могут использоваться для вывода ШИМ, для чего требуется таймер для работы, отсчитывая рабочие циклы. При необходимости эта строка отключит ШИМ. И снова, чтобы убедиться, что мы не видим странного поведения, это проверка безопасности, призванная помочь пользователю.

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

Проверки безопасности немного замедляют выполнение, но также значительно упрощают отладку. Таким образом, когда что-то пойдет не так, у вас меньше шансов получить странное поведение, которое заставит вас чесать голову. Нет ничего более неприятного, если иметь, казалось бы, логичный код и не получить ожидаемого результата. Программирование микроконтроллеров сильно отличается от программирования настольных или телефонных приложений (хотя и с ними тоже есть свои трудности). Поскольку вы работаете напрямую с оборудованием и у вас нет операционной системы, которая обеспечивала бы вашу безопасность, проблемы может быть трудно найти.

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

Вы также должны знать, что вызовы методов не всегда медленные из-за количества выполняемого кода, но способствующий фактор может быть из-за физических ограничений устройства. Например, analogRead() занимает около 100 микросекунд на вызов из-за предоставляемого разрешения и тактовой частоты. Более низкое разрешение АЦП уменьшит время каждого вызова. Однако даже тогда, хотя аппаратное обеспечение в конечном итоге является здесь ограничивающим фактором, код Arduino консервативно устанавливает максимальную частоту дискретизации АЦП только на 9600 Гц (в то время как поддерживает около 77 кГц). Итак, хотя Arduinos намного медленнее, чем они должны быть, это не всегда из-за выбора дизайна и компромиссов. Существует хорошее обсуждение об этом здесь , и документация здесь .

Как ускорить Arduino

Чтобы было ясно, мы на самом деле не делаем Arduino быстрее , скорее, мы делаем код более эффективным . Я указываю на это различие, потому что использование этих уловок не даст нам более быстрых часов (хотя мы можем ускорить их, о чем я коснусь позже), они просто будут выполнять меньше кода. Это важное различие, потому что более быстрые часы дают нам другие преимущества, такие как наличие более точных таймеров, более быстрое общение и т. Д.

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

Во всяком случае, перейдем к коду.

Цифровая запись

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

 #define CLR(x,y) (x&=(~(1<<y))) 
 #define SET(x,y) (x|=(1<<y)) 

Ага, вот и все.

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

 SET(PORTB, 0); 

Это приведет к записи HIGH значения на вывод 8 на вашем Arduino Uno. Это немного грубо, но намного быстрее. Это также означает, что мы более склонны делать что-то неправильно, например, ссылаться на несуществующий порт, писать поверх активного ШИМ или делать многое другое.

Макрос дает нам значительный импульс, используя примерно 2 цикла (частота 8 МГц), тогда как digitalWrite() использует колоссальные 56 циклов (частота 285 кГц).

Серийный

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

Последовательная связь обычно используется для отправки отладочной информации или информации о состоянии в настольную среду IDE, что означает, что у вас, вероятно, есть Serial.println() во всем вашем коде. Об этих операторах легко забыть после разработки, поэтому, если вы ищете повышения скорости и больше не нуждаетесь в отладке, попробуйте удалить все println() и полностью удалить Serial из кода. Просто инициализировав его (и даже не используя Serial.println() ), вы тратите много циклов на прерывания TX и RX. В одном случае , это было измерено , что только с Serial включенным замедлились в digitalWrite() š примерно на 18%. Это много потраченных впустую циклов из-за мертвого кода.

Тактовая частота

Хотя я в основном сосредоточился на улучшениях программного обеспечения, которые вы можете внести, не забывайте, что всегда есть «простое» улучшение - ускорение часов. Я говорю это так, потому что на самом деле это не простое улучшение plug-and-play. Чтобы ускорить часы на Arduino, вам необходимо вставить новый кристалл в плату, что может оказаться сложным, а может и нет, в зависимости от ваших навыков пайки.

После того, как вы установили новый кварцевый генератор, вам все равно нужно обновить загрузчик, чтобы отразить изменения, иначе он не сможет получать код через последовательный порт. И, наконец, вам нужно изменить F_CPU на правильную тактовую частоту. Например, если вы увеличили частоту до 20 МГц (самые быстрые часы, на которые рассчитана ATmega), вам необходимо изменить несколько файлов в среде Arduino IDE:

  • В preferences.txt измените
    • из: build.f_cpu=16000000L
    • кому: build.f_cpu=20000000L
  • В makefile файле измените
    • из: F_CPU = 16000000
    • к: F_CPU = 20000000

Согласно этой публикации , ATmega328 можно разогнать до 30 МГц, но я не рекомендую это делать =)

Заключение

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

Есть ли какие-нибудь собственные оптимизации, которыми вы хотели бы поделиться? Дайте нам знать об этом в комментариях!

Не забудьте подписаться на нашу рассылку, чтобы получать наши лучшие статьи прямо в свой почтовый ящик!

comments powered by Disqus