Оптимізуємо digitalWrite на Arduino
Сьогодні я протестує фактичну швидкість роботи функції digitalWrite на своїй Arduino Mega2560 і розповім як прискорити роботу програми в 50 разів! В основі отладочной плати Arduino Mega2560 лежить мікроконтролер AT2560. працює з тактовою частотою 16 Мгц. Якщо перевести ці 16 мільйонів коливань в часовий інтервал, то отримаємо досить невеликий період, рівний 62.5 нс. Це швидко, але чи дійсно Arduino виконує операції з такою ж швидкістю? Давайте подивимося.
Команди, які ми пишемо на мові Wiring. в процесі компіляції перетворюються в більш прості команди, так званий, машинний код, які мікроконтролер вже безпосередньо виконує. Деякі команди мікроконтролера виконуються за один такт роботи мікроконтролера, на деякі потрібні більше тактів, відповідно і часу виконання. Тому одна, на наш погляд, проста команда або функція буде виконуватися мікро контролером за кілька, може навіть кілька десятків або сотень тактів. До того ж, крім обчислень, нам потрібні операції читання / запису в пам'ять, тому, середня частота обробки команд буде значно відрізнятися від тактової частоти мікроконтролера. Питання тільки на скільки.
Спробуємо з'ясувати скільки ж виконується проста команда зміни значення на цифровому виході Arduino - digitalWrite. Проведемо кілька експериментів. В ході першого експерименту виконаємо наступний код.
Цей код блимає світлодіод, розташованому на платі і підключеним до 13 висновку. Ми просто по черзі міняємо стан цього виходу. Якщо запустити цей код, то ми побачимо, що світлодіод буде світиться безперервно, але, насправді це не так. Світлодіод вмикається і вимикається, просто наше око не може сприймати коливання частотою понад 25 Гц і такі коливання ми бачимо як постійно горить світлодіод з яскравістю, яка визначається скважностью подається сигналу.
Для того, щоб подивитися, що ж насправді відбувається на 13 виводі, я скористаюся осциллографом. Осцилограма сигналу на 13 піне виглядає так:
Схоже, що команда digitalWrite (13, HIGH) виконується за 6.6 мкс, а digitalWrite (13, LOW) за 7.2 мкс. Разом 13.8 мкс. Це набагато довше, ніж 62.5 нс, в дійсності, в 220 разів довше. Також, можна помітити, що знаходження у стані LOW (7.2 мкс) займає більше часу, ніж знаходження в стані HIGH (6.6 мкс).
Проведемо наступний експеримент.
В результаті виконання цього коду, осцилограма сигналу на 13 піне виглядає так:
Єдина інструкція 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.
І знову звернемося до допомоги осцилографа:
Як можна бачити, частота знизилася з 4 МГц до 1 Мгц. Час перебування в стані HIGH не змінилося і як і раніше дорівнює 62.5 нс (на осцілограмме просто показано, Wid (1)<100.0ns ). Следовательно, остальное время уходит на возврат к началу, и на это уходит в восемь раз больше времени, то есть 8 тактов. Вывод: функция loop не столь уж и эффективна.
У цьому прикладі ми отримали чотириразове збільшення швидкості, але це граничний випадок. Насправді, ми економимо кілька тактів роботи і при збільшенні часу виконання програмного коду всередині loop ефект скорочення часу роботи програми значно знизиться.
Отже, підведу підсумок:
- Використання регістрів портів замість функції digitalWrite дозволяє значно збільшити швидкість виконання програми.
- Використанням вкладеного в loop () нескінченного циклу while (1) ми можемо заощадити кілька тактів роботи процесора на кожному витку роботи циклу.
Як ви оцінюєте цю публікацію? (64 голосів, середня оцінка: 4.86 з 5)
Ще по цій темі
Безумовно це кльово. Тільки при цьому втрачається вся суть роботи з arduino - зручність і дистанціювання від архітектури мікроконтролера. Якщо потрібно писати швидкий і компактний код, то вже точно не варто писати його на Wiring. )))))
спасибі мені дуже допомогло. цікаво а як буде вести Ардуіно due в цьому випадку? з 84мгц тактовою частотою.
Теоретично, швидкість роботи програми має зрости пропорційно збільшенню тактової частоти в 84Мгц / 16Мгц = 5.25 рази.
while () дійсно працює!
натрапив на статтю, вирішив перевірити. так як є осцилограф, подумав чому б ні, а дай ка подивлюся.
записати в порти не вийшло (nano v3.1). поки не знаю як працюють мк, не зміг знайти потрібний порт.
а ось з while () дійсно різниця вийшла. на простому прикладі з hi low після low, якщо без while, додатково 290 нс утворюються. може це не багато, але все ж. напевно і є результат роботи loop ().
тоді питання навіщо вона потрібна взагалі?
не знав що так просто можна звертатися до мк (DDRB = B10000000).
спасибі за ідею)
То чим ми з вами займаємося - це псування мови Wiring, сила якого в простоті. Коли ми намагаємося по максимуму задіяти можливості контролера, наприклад, безпосередньо працюючи з його регістрами, ми відходимо від ідеології Wiring. Якщо потрібна продуктивність, то краще писати на чистому С / С ++ і максимально використовувати особливості архітектури мікроконтролерів.
Без loop в скетчі Wiring відмовляється компілювати і видає помилку.
ну Ардуіно перший крок в освоєнні мк.
На майбутнє «карта» висновків Ардуіно описані у файлі pins_arduino.h, але таких файлів кілька, є базовий, який лежить в папці
Для деяких Ардуіно (Леонардо, мега, мікро і ін.) Є винятки для них є свої файли які лежать
Ім'я порту (PORTA, PORTB.) Зазначено в константностей масиві digital_pin_to_port_PGM
Маска біта в порте в масиві digital_pin_to_bit_mask_PGM (по суті він же номер біта).
Індекс ясна річ йде з нуля.
Отже беремо номер виводу: 13, дивимося що у нас
digital_pin_to_bit_mask_PGM [13] = _BV (5)
_BV - це такий макрос, оголошений в надрах пакету AVR
#define _BV (bit) (1 < і нам потрібно тільки число вказане в дужках виходить PortB, біт 5 На щастя, підсумки вийшли майже вірними, але тільки з одним доповненням: «Використанням вкладеного в loop () нескінченного циклу while (1) ми можемо заощадити кілька тактів роботи процесора на кожному витку роботи циклу», але при цьому втратимо те, що дає нам функція serialEvent. А саме, обробка вхідних даних по послідовним портам буде зупинена. Ще є варіант фьюз дозволити передавати тактовий сигнал на вихід, 16 МГц відразу отримаємо апаратно. З цікавого можна генерувати тактовий сигнал RC ланцюжком і міняти частоту програмно, регістром корекції частоти. Якщо зовнішня RC ланцюжок, змінюючи параметри RC або напруги харчування отримаємо взагалі генератор частоти плавно змінюється і по точності Самокалібрующійся хоч від кварцу щомиті, хоч від GPS супутників. Статейку треба актуалізувати. час йде. вносяться зміни. Адже після виходу статті пройшло 3 роки. код digitalWrite (13, HIGH); digitalWrite (13, LOW); виконується швидше. загальний час - в районі 7нсек, частота виходить в районі 150кГц. (IDE 1.8.1, UNO R3 328 16MHz). і так. як вже згадали вище - не працює код команди прямого запису в регістр порту.