Контакти

Arduino mega 2560 таймери. AVR. Навчальний курс. таймери. Таймери на Arduino

Останнім часом дедалі більше початківців стикаються з проблемою освоєння Таймерів/Лічильників (далі Т/С) на етапі вивчення мікроконтролерів. У цій статті я постараюся розвіяти страхи перед даними модулями і доступно пояснити, як і з чим використовують ті самі Т/С.

За основу візьмемо дуже популярну серед розробників пристроїв на МК книгу, автором якої є А.В. Євстіфєєв. За посиланнями наприкінці статті Ви зможете знайти проект і проект в . У цій статті ми розберемо роботу 8-бітного Т/С Т2, який входить до складу Т/С МК Atmega8.

Отже, що таке Таймер/Лічильник? Т/С - це один із модулів МК AVR за допомогою якого можна відміряти певні проміжки часу, організувати ШІМ та багато інших завдань. Залежно від моделі МК кількість Т/С може становити 4 і більше. Приклад тому - МК Atmega640х, 1280х/1281х, 2560х/2561х, які містять на своєму борту 6 Т/С: два 8 бітних і чотири 16 бітних. МК Atmega8 містить три Т/С: Т0 і Т2 з розрядністю 8 біт, Т1 з розрядністю 16 біт.

Розгляньмо детальніше Т/С Т2 мікроконтролера Atmega8.

Цей таймер може працювати у кількох режимах: Normal, Phase correct PWM, CTC (скидання під час збігу), Fast PWM. Докладніше про кожен режим Ви можете прочитати у книзі.

Даний Т/С складається з регістру управління, лічильного регістру, регістру порівняння, регістру стану асинхронного режиму. Структурна схема Т2 наведено на рис.1

Розглянемо теоретично як працює цей модуль. Щоб для початку Вам було зрозуміліше, ми не розглядатимемо всі зайві примочки таймера і розглянемо звичайнісінький його режим - NORMAL. Для себе визначимо, що МК тактується від внутрішнього RC-генератора з частотою 1МГц і таймер налаштований на роботу в режимі NORMAL.

Тактові імпульси надходять на вхід clk i і потрапляють у предделитель таймера. Представник може бути налаштований, за Вашими потребами, на прямий прохід тактових імпульсів або ділити вхідні імпульси, пропускаючи лише їхню певну частину. Поділити вхідні імпульси можна /8, /64, /256, /1024. Так як у нас Т\С може працювати в асинхронному режимі, то при включенні його в цей режим кількість предітелелі істотно зростає, але ми їх розглядати поки не будемо. З предделителя тактові імпульси надходять у блок управління і з нього потрапляють у рахунковий регістр. Рахунковий регістр у свою чергу робить інкремент на кожен вхідний імпульс. Рахунковий регістр Т2 8-ми бітний, тому може вважати лише до 255. Коли настає переповнення рахункового регістру, він скидається в 0 і в цьому ж такті починає вважати заново. Також у момент переповнення рахункового регістру встановлюється прапор TOV2 (прапор переривання по переповненню) регістру TIFR.

Тепер, коли ми торкнулися таких слів, як РЕЄСТР, саме час з ними познайомиться. Для початку ми торкнемося тільки ті регістри, з якими безпосередньо працюватимемо, щоб не забивати мозок зайвою інформацією.

TCNT2 - лічильний регістр, про його роботу ми вже говорили.

TCCR2 – регістр управління таймером.

TIMSK – регістр маски переривань (в Atmega8 цей регістр є єдиним для всіх таймерів).

TIFR - регістр прапорів переривань (В Atmega8 цей регістр є єдиним для всіх таймерів).

А тепер про кожного докладно:

Регістр керування TCCR2. Вміст цього регістру можна подивитися на рис.2.


рис.2

Біти 0-2 відповідають за тактування таймера. Установка певних комбінацій у тих бітах налаштовує предделитель даного таймера. Якщо всі три біти скинуто - таймер вимкнено.

Біти 3.6 відповідають за режим роботи таймера.

Біти 4,5 потрібні для налаштування поведінки виведення ОСn (простіше кажучи, використовуються при налаштуванні ШІМ)

І останній біт цього регістру - біт 7. З його допомогою ми можемо примусово змінювати стан виведення ОСn.

Реєстр маски переривань – TIMSK. Його ми бачимо на малюнку №3

З цього регістру нас цікавлять лише два останні біти, біти 6 і 7. Цими бітами ми дозволяємо роботу переривань.

Біт 6, якщо в нього записати одиницю, дозволяє переривання за подією "Переповнення Т\С Т2"

Біт 7, якщо в нього записати їжуницу, дозволяє переривання за подією "Збіг рахункового регістру з регістром порівняння"

Реєстр прапорів переривань TIFR. Його ми бачимо на малюнку №4

рис.4

У цьому регістрі нас також цікавлять два останні біти: біти 6 і 7.

Біт 6 - прапор, встановлюється за подією "Переповнення Т\С Т2"
Біт 7 - прапор, встановлюєтьсяза подією "Збіг рахункового регістру з регістром порівняння"

Ці біти скидаються автоматично при виході з обробника переривання, але для надійності їх можна скидати самостійно, скидаючи ці біти "0".

Інші біти регістрів TIMSK і TIFR використовуються ТС Т0 і Т1. Як ви вже помітили, у бітів цих регістрів навіть назви збігаються, за винятком цифри в кінці назви, яка і вказує до якого таймера даний біт застосовується.

Залишилося розглянути дві нескладні таблички, а саме: таблиця, в якій описано керування тактовим сигналом (рис. 6), та таблиця, в якій описано, як загалом налаштувати таймер (рис.5).

Про те, що знаходиться в цих таблицях, я писав вище, але наводжу Вам їх для наочності.

Ось ми і закінчили з теорією, і настав час приступити до практичної частини. Відразу обмовлюся.

ГОДИННИК, ЯКИЙ ВИХОДИТЬСЯ У ХОДІ ВИВЧЕННЯ ДАНОЇ СТАТТІ, НЕ МАЮТЬ ВИСОКОЮ ТОЧНІСТЮ. ДАНА СТАТТЯ ОРІЄНТУВАНА НА ЗАГАЛЬНІ ПРИНЦИПИ РОБОТИ З ТАЙМЕРАМИ.

Відкриваємо Studio 6, створюємо проект та вибираємо Atmega8.

На початку вказуємо частоту тактування і підключаємо потрібні нам для роботи бібліотеки

< avr/io.h >#include< avr/interrupt.h >

У першому рядку ми вказуємо частоту. Це необхідно для того, щоб компілятор нас краще розумів, якщо ми раптом захочемо використовувати функції _delay_().

У другому рядку коду підключається бібліотека із загальним описом регістрів нашого МК. Також у ній всім регістрам присвоєно читабельні імена.

У третьому рядку підключається бібліотека для роботи з векторами переривань.

TIMSK | = (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7);

На цьому налаштування нашого таймера закінчено. Давайте докладніше розглянемо останні три рядки коду.

У першому рядку ми дозволили переривання за подією "Переповнення таймера\лічильника Т2"

І в третьому рядку ми глобально дозволили переривання. Це можна було також написати так:

Asm("sei");

Залишається додати обробник переривання і код нашого годинника реального часу.

ISR (TIMER2_OVF_vect) ( takt++; if (takt>=4)(sek++; takt=0x00;) if (sek>=60) (min++; sek=0x00;) if (min>=60) (hour++; min=0x00 ;) if (hour>=24) (hour=0х00);

У коді, який знаходиться в обробнику переривання, немає нічого складного та нового для Вас. Увага звернемо лише на змінну такт та чарівну цифру "4". Звідки взялася ця цифра? Давайте докладно розглянемо цей момент.

Ми знаємо, що наш МК працює від внутрішнього генератора з частотою 1МГц, таймер тактується з передником \1024, рахувати наш таймер може до 255. Знаючи ці параметри ми можемо порахувати скільки переповнень він здійснить за 1 секунду

1 000 000 \ 1024 \ 256 = 3,814697.....

Ну, а оскільки ми вчимося працювати з таймерами і не ставили за мету отримати суперточний хід годинника, ми округляємо наш результат і отримуємо "4". Тобто. за 1 секунду таймер переповниться ~4 рази.

Чому ми ділили на 256 якщо таймер рахує лише до 255? Тому що "0" це теж число. Думаю, тут усе зрозуміло.

Не забуваймо, що всі змінні потрібно оголосити як глобальні.

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

#define F_CPU 1000000UL #include< avr/io.h >#include< avr/interrupt.h >unsigned char takt = 0; unsigned char sek = 0; unsigned char min = 0; unsigned char hour=0; ISR (TIMER2_OVF_vect) ( takt++; if (takt>=4)(sek++; takt=0x00;) if (sek>=60) (min++; sek=0x00;) if (min>=60) (hour++; min=0x00 ;) if (hour>=24) (hour=0х00); ) int main(void) ( TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7); while(1) { } }

А як висновок інформації користувачеві? А тут кому як подобається. Можете використовувати семисегментні індикатори, графічні або знакогенеруючі дисплеї і т.д.

В архіві Ви знайдете проект з виведенням інформації на дисплей від nokia5110, проект Proteus 7 і всі потрібні файли та бібліотеки для роботи.

Звертаю увагу на те, що бібліотека LCD_5110 для роботи з дисплеєм написана учасником форуму та надана з дозволу.

З урахуванням всього сказаного напишемо програму, що перемикає світлодіод. У цьому випадку вона робитиме це за подією переповнення таймера-лічильника Timer 1(Вектор у нас позначений: TIM1_OVF). Так як лічильник 16-розрядний, то подія переповнення буде виникати при кожному 65536-му імпульсі вхідної частоти. Якщо ми задамо коефіцієнт поділу тактової частоти на вході Timer 1рівним 64, то при 4 МГц частоти генератора ми отримаємо приблизно 1 Гц: 4000000/64/65536 = 0,953674 Гц.

Це не зовсім те, що нам потрібно, і до того ж частота неточно дорівнює одному герцю. Для того щоб світлодіод перемикався точно раз на півсекунди (тобто період його дорівнював секунді), програму доведеться трохи ускладнити, завантажуючи кожен раз у рахункові регістри певне значення, яке розраховується просто: якщо період одного тактового імпульсу таймера дорівнює 16 мкс (частота 4 000 000/64), то для отримання 500 000 мікросекунд треба відрахувати таких імпульсів 31 250. Так як лічильник підсумовує, а переривання виникає при досягненні числа 65 536, потрібно попередньо завантажувати в нього необхідне число 65 536 - 31.

Це не єдиний спосіб, але найбільш універсальний, що підходить для всіх таймерів. До речі, саме таким способом реалізовано відлік часу у Arduino(Див. розділ 21). Інший спосіб – використовувати переривання для досягнення певної кількості, завантаженої в регістр порівняння Аабо У. Як це робиться, ми побачимо далі у цьому розділі. Для того щоб здійснити саме перемикання з червоного в зелений, нам доведеться вчинити як раніше, тобто по кожній події переповнення перекидати два біти в регістрі PortD .

Повністю програма тоді виглядатиме так:

Я не коментуватиму докладно кожен оператор, тому що це зайняло б занадто багато місця. Після виконання всіх команд початкової установки МК зациклюється, але нескінченний цикл буде перериватись виникненням переривання – тут все аналогічно операційній системі Windows, яка також є нескінченним циклом очікування подій. Як ви дізнаєтеся з наступних розділів, у Arduinoтакий цикл – одна з головних складових будь-якої програми, саме тому, що переривання там майже не використовуються. Всередину безкінечного циклу тут можна поставити знайому команду sleep, без додаткових налаштувань режиму енергоспоживання вона заощаджуватиме близько 30% харчування. А ось заощадити ще більше просто так не вийде, оскільки доведеться зупиняти процесорне ядро ​​і таймер перестане працювати.

Нотатки на полях

До речі, а як зупинити запущений таймер, якщо це потрібно? Дуже просто: якщо обнулити регістр TCCR1B (той, у якому задається коефіцієнт поділу тактової частоти), таймер зупиниться. Щоб запустити його знову з коефіцієнтом 1/64, потрібно знову записати в цей регістр значення 0b00000011.

Зверніть увагу, що оператор reti(Закінчення обробки переривання) при обробці переривання таймера зустрічається двічі - це цілком нормальний прийом, коли підпрограма розгалужується. Можна, звісно, ​​і помітити останній оператор retiі тоді текст процедури став би невідмінним від першого варіанту, але так буде коректніше.

Зверніть також увагу на форму запису ldi temp, (1<< TOIE1) . Оскільки біт, що позначається як TOIE1, у регістрі TIMSK має номер 7, то цей запис еквівалентний запису ldi temp, 0b10000000 - можна писати і так, і так, і ще купою різних способів. Наприклад, для запуску таймера з коефіцієнтом 1/64 потрібно, як видно з тексту програми, встановити молодші два біти регістра TCCR1B. Тут ми встановлюємо їх у tempбезпосередньо, але оскільки ці біти називаються CS11 і CS10, можна записати так:

ldi temp, (1<< CS11) I (1 << CS10)

або навіть так:

ldi temp, (3<< CS10)

Детально цей спосіб запису наведено в описі AVR-ассемблера на сайті Atmel .

Подробиці

У цій програмі є один тонкий момент, пов'язаний із завантаженням лічильників таймера. При читанні та запису 16-розрядних регістрів Timer 1 їх вміст може змінитися в проміжку між читанням або записом окремих 8-розрядних «половинок» (адже, наприклад, у даному випадку таймер продовжує вважати, поки триває обробка переривання). Тому в 16-розрядних таймерах AVR передбачено спеціальний механізм читання та запису таких регістрів. При записі першим завантажується значення старшого байта, яке автоматично міститься у якийсь (недоступний для програміста) буферний регістр. Потім, коли надходить команда на запис молодшого байта, обидва значення об'єднуються, і запис проводиться одночасно в обидві половинки 16-розрядного регістру. Навпаки, під час читання першим має бути прочитаний молодший байт, у своїй значення старшого автоматично фіксується приміщенням у той самий буферний регістр, і за наступної операції читання старшого байта його значення витягується звідти. Таким чином і при читанні значення обидва байта відповідають одному й тому моменту часу.

Коли у мене виникло бажання вести розробку під Arduino, я зіткнувся з кількома проблемами:
  • Вибір моделі зі списку доступних
  • Спроби зрозуміти, чого мені знадобиться, крім самої платформи.
  • Встановлення та налаштування середовища розробки
  • Пошук та розбір тестових прикладів
  • "Розбірки" з екраном
  • "Розбірки" з процесором

Для вирішення цих проблем я переглянув і прочитав досить багато різних джерел і в цій статті я спробую зробити огляд знайдених мною рішень та методів їхнього пошуку.

Вибір платформи

Перед початком програмування під залізяку потрібно спочатку її купити. І тут я зіткнувся з першою проблемою: виявилося, що різних дуїн досить багато. Тут є і широка лінійка Arduino і приблизно така сама широка Freeduino та інші аналоги. Як виявилось, великої різниці, що саме брати, немає. Тобто одні з цих пристроїв трохи швидше, інші трохи повільніше, одні дешевші, інші – дорожчі, але основні принципи роботи практично не відрізняються. Відмінності з'являються практично тільки при роботі з регістрами процесора і я далі поясню, як по можливості уникнути проблем.

Я вибрав платформу Arduino Leonardo як найдоступнішу за ціною і наявну на той момент в Інтернет-магазині, в якому я все і замовляв. Відрізняється вона від решти лінійки тим, що в неї на борту встановлено лише один контролер, який займається і роботою з USB-портом і виконанням тих завдань, які ми на наш пристрій повісимо. У цього є свої плюси та мінуси, але напоротися на них при початковому вивченні не вийде, тому забудемо про них поки що. Виявилося, що вона підключається до комп'ютера через micro-USB, а не USB-B, начебто більшість інших. Це мене дещо здивувало, але й зраділо, бо я, як власник сучасного пристрою на Android без цього кабелю взагалі з дому не виходжу.
Так, харчується майже будь-яка дуїно-сумісна залізяка декількома способами, у тому числі, від того ж кабелю, через який програмується. Також один світлодіод майже у всіх плат розміщений прямо на платі контролера, що дозволяє почати роботу з пристроєм відразу після покупки, навіть не маючи в руках нічого, крім сумісного кабелю.

Спектр завдань

Я думаю, що перш ніж взятися за таке написання чогось під залізяку, цікаво зрозуміти, що на ній можна реалізувати. З Ардуїно продати вийде практично що завгодно. Системи автоматизації, ідеї для «розумного будинку», контролери управління чимось корисним, «мозки» роботів… Варіантів просто безліч. І сильно допомагає в цьому напрямку досить широкий набір модулів розширення, що надзвичайно просто підключаються до плати контролера. Список їх досить довгий та перспективний, та шукаються вони в Інтернеті за словом shield. З усіх цих пристроїв я для себе вважав найкориснішим LCD екран з базовим набором кнопок, без якого на мою скромну думку займатися будь-якими тренувальними проектами зовсім нецікаво. Екран брався звідси, ще там є його опис, а також із наведеної сторінки ведуть посилання на офіційний сайт виробника.

Постановка задачі

Я якось звик при отриманні в свої руки нового інструменту відразу ставити собі якесь в міру складне і абсолютно непотрібне завдання, браво його вирішувати, після чого відкладати вихідний бік і тільки потім братися за щось по-справжньому складне і корисне. Зараз у мене під рукою був дуже схожий на складову частину міни з якогось голлівудського кінофільму екран із усіма необхідними кнопками, з яким треба було навчитися працювати, а також дуже хотілося освоїти роботу з перериваннями (інакше в чому сенс використання контролера?) тому першим ж, що спало на думку, виявилося написати годинник. А оскільки розмір екрану дозволяв, то ще й із датою.

Перші кроки

Ось мені нарешті приїхали всі куплені компоненти, і я їх зібрав. Роз'єм екрану підключився до плати контролера як рідний, плату було підключено до комп'ютера… І тут мені дуже допомогла ця стаття. Повторювати те саме я не буду.

Прихований текст

Єдине скажу, що згадавши молодість (а точніше перший «проект», зібраний під час вивчення радіоелектроніки у Палаці піонерів – мультивібратор із двома світлодіодами), я знайшов 2 світлодіоди та поправив наведений у статті приклад і почав блимати ними:).

«Другі кроки»

Наступним закономірним питанням для мене стало "як працювати з LCD екраном?". Офіційна сторінка пристрою люб'язно надала мені посилання на архів, в якому опинилися 2 бібліотеки з чудовими прикладами. Лише не сказала, що з цим усім робити. Виявилося, що вміст потрібно просто розпакувати в папку libraries середовища розробки.

Після цього можна відкривати приклад GuessTheNumber.pde і заливати в плату аналогічно прикладу з світлодіодом, що миготить. Однак особисто у мене після прошивки екран залишився і без жодної літери, що рівномірно світиться. Після недовгих пошуків проблеми виявилося, що треба було просто підкрутити викруткою єдиний потенціометр, що є на платі екрана, щоб задати нормальне значення контрастності.

Використаного в прикладі набору команд у принципі достатньо для простої роботи з екраном, але якщо захочеться чогось більшого, можна відкрити вихідний текст бібліотек LCDKeypad і LiquidCrystal і подивитися, що там є ще.

Архітектура програми

Основне завдання годинника - вважати час. І робити це вони мають точно. Природно, що без використання механізму переривань ніхто не зможе гарантувати, що час зважає на достатню точність. Тому обчислення часу потрібно залишити їм. Решту можна винести в тіло основної програми. А цього «остального» у нас досить багато – вся робота з інтерфейсом. Можна було б зробити інакше, створити стек подій, створюваний у тому числі і механізмом обробки переривань, а оброблюваний всередині основного додатка, це дозволило б наприклад займатися оновленням екрана не частіше, ніж раз на пів секунди (або натискання кнопки) але я вважав це зайвим для такого простого завдання, оскільки крім перемальовки екрану процесору все одно зайнятися нічим. Тому весь вільний час програма перечитує стан кнопок та перемальовує екран.
Проблеми, пов'язані з таким підходом
Періодичні зміни екрану
Дуже хотілося зробити миготливі двокрапки між годинами, хвилинами і секундами, щоб як у класичному годиннику вони пів секунди горіли, а підлога - ні. Але оскільки екран перемальовується весь час, треба було якось визначати, в яку половину секунди їх малювати, а в яку – ні. Найпростішим виявилося зробити 120 секунд на хвилину і малювати двокрапки кожну непарну секунду.
Миготіння
При постійному перемальовуванні екрана стають помітними миготіння. Щоб цього не виникало, є сенс не очищати екран, а малювати новий текст поверх старого. Якщо сам текст при цьому не змінюється, миготіння на екрані не буде. Тоді функція перемальовування часу виглядатиме ось так:
LCDKeypad lcd; void showTime()( lcd.home(); if (hour<10) lcd.print("0"); // Случай разной длины приходится обрабатывать руками lcd.print(hour,DEC); // английские буквы и цифры ОНО пишет само, русские буквы нужно определять программисту if (second %2) lcd.print(" "); else lcd.print(":"); // вот они где используются, мои 120 секунд в минуте if (minute<10) lcd.print("0"); lcd.print(minute,DEC); if (second %2) lcd.print(" "); else lcd.print(":"); if (second<20) lcd.print("0"); lcd.print(second / 2,DEC); lcd.print(" "); lcd.setCursor(0,1); // переходим в координаты x=0, y=1 то есть в начало второй строки lcd.print(" "); lcd.print(day,DEC); lcd.print(months); // месяцы мне было приятнее нумеровать от 1 до 12, а массив с названиями от 0 до 11 lcd.print(year,DEC); }
Робота з кнопками
Схожа ситуація із кнопками. Натиснена кнопка вважається натиснутою при кожному прогоні програми, тому за одне натискання може обробитися будь-яку кількість разів. Доводиться змушувати програму чекати на «віджимання» окремо. Почнемо основну програму так:
int lb = 0; // змінна зберігає старе значення кнопки void loop()( // main program int cb,rb; // визначимо 2 змінні, для реально натиснутої кнопки й у тій, яку вважатиме натиснутою програма cb=rb=lcd.button(); // на початку можна вважати, що це та сама кнопка if (rb!=KEYPAD_NONE) showval=1;// змінна вказує, що поки натиснута кнопка не треба блимати тому, що налаштовується if (cb!=lb) lb= cb;// якщо стан кнопки змінився, запам'ятовуємо новий, else cb=KEYPAD_NONE;// інакше говоримо програмі, що всі кнопки давно відпущені.

Робота з таймером

Власне, щоб вся робота з таймером складається із двох важливих компонентів:
  • Ініціалізації механізму переривань від таймера у зручному для нас режимі
  • Власне, обробки переривання
Ініціалізація таймера
Для того, щоб почати отримувати потрібні переривання, потрібно налаштувати процесор таким чином, щоб він почав їх генерувати. Для цього потрібно встановити потрібні нам регістри у потрібні значення. Які саме регістри і в які саме значення потрібно встановлювати, потрібно дивитися в ... Даташ на процесор: (. Чесно кажучи, сильно сподівався, що цю інформацію можна буде знайти в документації на Arduino, але ні, це було б занадто просто. Мало того , для різних процесорів серії номери бітів можуть відрізнятися.І я сам особисто натрапив на те, що спроба встановлення бітів у відповідність з даташитом на сусідній процесор привели до плачевних результатів... Але все ж не настільки сумно, як може здатися, оскільки для цих бітів є ще й імена, вони більш-менш спільні для різних процесорів, тому використовувати цифрові значення ми не будемо, лише імена.

Для початку пригадаємо, що у мікроконтролерах AVR таймерів кілька. Нульовий використовується для обчислення значень delay() тощо, тому його ми використовувати не будемо. Відповідно, використовуємо перший. Тому далі в позначенні регістрів часто буде проскакувати одиниця, для налаштування другого таймера потрібно там же поставити двійку.

Вся ініціалізація таймера має відбуватися у процедурі setup(). Складається вона з приміщення значень 4 регістра, TCCR1A, TCCR1B, TIMSK1, OCR1A. Перші 2 з них називаються «регістрів A і B управління таймера-лічильника 1». Третій - «реєстр маски переривань таймера/лічильника 1», і останній - «регістр порівняння A лічильника 1».

Команди для встановлення бітів прийнято використовувати такі (зрозуміло, що варіантів багато, але найчастіше використовуються саме ці):
BITE | = (1<< POSITION)
тобто всуваємо «1» на POSITION біт праворуч наліво і проводимо логічне «або» між цільовим та отриманим байтами. При включенні контролера значення всіх цих регістрів містять 0, тому про нулі ми просто забуваємо. Таким чином, після виконання наступного коду

A=0; A |= (1<< 3)

Значення A стане 8.

Варіантів налаштування таймера сила-силенна, але нам потрібно домогтися від таймера наступного:

  • Щоб таймер перейшов у режим роботи CTC (тобто режим рахунку зі скиданням після збігу, «Clear Timer on Compare match»), судячи з даташиту це досягається установкою бітів WGM12:0 = 2, що саме собою означає встановити біти з другого по нульовий значення «2», тобто, «010», команда TCCR1B |= (1<< WGM12) ;
  • Оскільки 16МГц (а саме така частота у кварцового резонатора на моїй платі) це багато, вибрати максимально можливий дільник, 1024 (тобто тільки кожен 1024 такт буде доходити до нашого лічильника), CS12:0=5
  • Зробити так, щоб переривання приходило при збігу з регістром A для даного лічильника TIMSK1 |= (1<< OCIE1A)
  • Вказати при досягненні якого саме значення викликати обробку переривання, це значення міститься в цей регістр A лічильника 1 (цілком його назва OCR1A), переривання по збігу з яким ми включали попереднім пунктом.

Як порахувати, до скільки нам потрібно проводити обчислення? - Легко, якщо тактова частота кварцового резонатора 16МГц, то при досягненні лічильником значення 16000 пройшла б секунда, якби коефіцієнт розподілу був 1. Так як він 1024, ми отримуємо 16000000/1024 = 15625 в секунду. І все б добре, але нам потрібно отримувати значення кожні пів секунди, а 15 625 на 2 не ділиться. Значить ми до цього помилилися і доведеться взяти менший коефіцієнт розподілу. А наступний по зменшенню у нас 256, що дає 62500 тиків на секунду або 31250 за пів секунди. Лічильник у нас 16-тибітний, тому може дорахувати до 65536. Іншими словами, нам його вистачає і на пів секунди, і на секунду. Ліземо в даташит, потім у вихідник і виправляємо на CS12: 0 = 4, а після цього OCR1A = 31249; (Як я зрозумів, один такт йде чи то на скидання, чи то ще куди, тому зустрічаються поради скинути ще один з отриманої цифри).

Обробка переривання
Синтаксис функції обробки переривання дещо змінився, зараз виглядає так, як у прикладі нижче. Так що не дивуйтеся, якщо десь побачите дещо інший опис імені функції.

Власне, зараз воно складається із зарезервованого слова ISR та вказівки конкретного переривання, яке ця функція обробляє у дужках. А всередині цієї функції як бачите немає нічого фантастичного. Навіть обов'язкове RETI як бачите за нас автоматично вставляє компілятор.

ISR(TIMER1_COMPA_vect) ( digitalWrite(LEDPIN, !digitalRead(LEDPIN)); // LEDPIN=13. Цей рядок блимає світлодіодом на платі. Зручно та прикольно:) second++; if ((second %2) && lastshowval) ( // цей і наступні 7 рядків потрібні тільки для того, lastshowval = 0; // щоб можна було досягти цього кумедного ефекту, як на апаратному годиннику, showval = 0; // коли в режимі налаштування скажімо хвилин, значення параметра, що налаштовується, блимає ) if (!(second %2) && !lastshowval)( // тільки при відпущених кнопках, а поки кнопки натиснуті, воно просто горить. lastshowval = 1; showval = 1; ) if ( second>=120) ( // знову мої 120 секунд на хвилину. Ну а кому зараз легко? second-=120; minute++; if (minute>=60)( minute-=60; hour++; if (hour>=24)) ( hour-=24; day++; if (daylarge(day,month,year) // повертає true якщо значення дня // більше максимально можливого цього місяця цього року.) ( day=1; month++; if (month>12) (month = 1; year++;)))))))

Сподіваюся, ця стаття буде комусь корисна, тому що достатньо докладних інструкцій щодо роботи з перериваннями від таймера російською мовою досить мало.

З лічильником ітерацій головного циклу ми розібралися і з'ясували, що для точних тимчасових відліків він не годиться зовсім - витримка плаває та й вважати її складно. Що робити?

Очевидно, що потрібен якийсь зовнішній лічильник, який цокав би незалежно від роботи процесора, а процесор міг будь-якої миті подивитися, що в ньому таке натикало. Або щоб лічильник видавав події з переповнення чи спустошення — прапорець піднімав чи переривання генеріл. А проц це прочухає та обробить.

І такий лічильник є навіть не один — це периферійні таймери. У AVR їх може бути кілька штук та ще з різною розрядністю. У ATmega16 три, в ATmega128 чотири. А в нових МК серії AVR може навіть ще більше не впізнавав.

Причому таймер може бути не просто тупим лічильником, таймер є одним з найбільш крутих (в плані альтернативних функцій) периферійних девайсів.

Що вміють таймери

  • Тикати з різною швидкістю, підраховуючи час
  • Вважати вхідні ззовні імпульси (режим лічильника)
  • Тикати від зовнішнього кварцу на 32768гц
  • Генерувати кілька видів ШІМ сигналу
  • Видавати переривання (по півдесятки різних подій) та встановлювати прапори

Різні таймери мають різну функціональність та різну розрядність. Це докладніше дивитись у датасіті.

Джерело тиків таймера
Таймер/Лічильник (далі зватиму його Т/С) вважає або тактові імпульси від вбудованого тактового генератора, або з лічильного входу.

Подивися уважно на розпинування ніг ATmega16, бачиш там ніжки T1 та T0?

Так ось і є лічильні входи Timer 0 і Timer 1. При відповідному налаштуванні Т/С вважатиме або передній (перепад з 0-1), або задній (перепад 1-0) фронт імпульсів, що прийшли на ці входи.

Головне, щоб частота вхідних імпульсів не перевищувала тактову частоту процесора, інакше він не встигне обробити імпульси.

З іншого боку, Т/С2 здатний працювати у асинхронному режимі. Тобто Т/С вважає не тактові імпульси процесора, що не входять імпульси на ніжки, а імпульси свого власного генератора, що працює від окремого кварцу. Для цього Т/С2 є входи TOSC1 і TOSC2, на які можна повісити кварцовий резонатор.

Навіщо це взагалі треба? Та хоча б організувати годинник реального часу. Повісив на них годинниковий кварц на 32768 Гц і рахуй час - за секунду відбудеться 128 переповнень (т.к. Т/С2 восьмирозрядний). Отже, одне переповнення це 1/128 секунди. Причому на час обробки переривання з переповнення таймер не зупиняється, він також продовжує рахувати. Так що годинник зробити нікчемну справу!

Голова
Якщо таймер вважає імпульси від тактового генератора, або від свого внутрішнього, їх ще можна пропустити через предделитель.

Тобто ще до попадання в лічильний регістр частота імпульсів буде ділитися. Ділити можна на 8, 32, 64, 128, 256, 1024. Так що якщо повісиш на Т/С2 годинний кварц, та пропустиш через ділник на 128, то таймер у тебе буде цокати зі швидкістю один тик за секунду.

Зручно! Також зручно юзати дільник коли треба просто отримати великий інтервал, а єдине джерело тиків це тактовий генератор процесора на 8Мгц, вважати ці мегагерці задовбаєшся, а от якщо пропустити через предделитель, на 1024 то все вже куди райдужніше.

Але тут є одна особливість, справа в тому, що якщо ми запустимо Т/С з якимось звірячим предделителем, наприклад на 1024, то перший тик на рахунковий регістр прийде не обов'язково через 1024 імпульсу.

Це залежить від того, в якому стані знаходився ділник, а раптом він до моменту нашого включення вже дорахував майже до 1024 року? Значить, тик буде відразу ж. Підприємець працює весь час, незалежно від того, включений таймер чи ні.

Тому ділителі можна і потрібно скидати. Також треба враховувати і те, що предделитель єдиний всім лічильників, тому скидаючи його треба враховувати те, що з іншого таймера зіб'ється витримка до наступного тику, причому може збитися саме так.

Наприклад, перший таймер працює на виведенні 1:64, а другий на виведенні 1:1024 предделителя. У другого майже дотикалося в ділнику до 1024 року і ось ось має бути тик таймера, але тут ти взяв і скинув ділник, щоб запустити перший таймер точно з нуля. Що станеться? Правильно, у другого ділка тут же скинеться в 0 (голова то єдиний, регістр у нього один) і другого таймера доведеться чекати ще 1024 такту, щоб отримати таки омріяний імпульс!

А якщо ти будеш скидати предделитель у циклі, на благо першого таймера, частіше ніж раз у 1024 такту, то другий таймер так ніколи і не тикне, а ти вбиватимешся головою об стіл, намагаючись зрозуміти чого це в тебе другий таймер не працює, хоча повинен.

Для скидання представників достатньо записати біт PSR10 у регістрі SFIOR. Біт PSR10 буде скинутий автоматично на наступному такті.

Рахунковий регістр
Весь результат мук, описаних вище, накопичується в рахунковому регістрі TCNTх, де замість x номер таймера. він може бути як восьмирозрядним, так і шістнадцяти розрядним, у такому разі він складається з двох регістрів TCNTxH і TCNTxL - старший і молодший байти відповідно.

Причому тут є каверза, якщо у восьмирозрядний регістр треба покласти число, то немає проблем OUT TCNT0, Rx і ніяких цвяхів, то з двобайтними доведеться поизвращаться.

А справа все в чому - таймер вважає незалежно від процесора, тому ми можемо покласти спочатку один байт, він почне зважати, потім другий, і почнеться перерахунок вже з урахуванням другого байта.

Відчуваєте лажу? Ось! Таймер точний пристрій, тому вантажити його рахункові регістри треба одночасно! Але як? А інженери з Atmel вирішили проблему просто:
Запис у старший регістр (TCNTxH) ведеться спочатку регістр TEMP. Цей регістр є чисто службовим, і нам ніяк недоступний.

Що в результаті виходить: Записуємо старший байт у регістр TEMP (для нас це один хрін TCNTxH), а потім записуємо молодший байт. У цей момент, до реального TCNTxH, заноситься раніше записане нами значення. Тобто два байти, старший та молодший, записуються одночасно! Міняти порядок не можна! Тільки так

Виглядає це так:

CLI; Забороняємо переривання, обов'язково! OUT TCNT1H, R16; Старший байт записався спочатку в TEMP OUT TCNT1L, R17; А тепер записалося і до старшого і молодшого! SEI; Дозволяємо переривання

Навіщо забороняти переривання? Та щоб після запису першого байта, прога випадково не помчала не переривання, а там хтось наш таймер не згвалтував. Тоді в його регістрах буде не те, що ми послали тут (або в перериванні), а чорти що. От і спробуй потім таку багу відловити! Адже вона може вилізти в самий невідповідний момент, та хрін зловиш, адже переривання це майже випадкова величина. Тож такі моменти треба просікати відразу ж.

Читається все також, тільки у зворотному порядку. Спочатку молодший байт (при цьому старший пхається в TEMP), потім старший. Це гарантує те, що ми вважаємо саме той байт який був на даний момент у рахунковому регістрі, а не той, який у нас натикав, поки ми виколупували його побайтно з рахункового регістру.

Контрольні регістри
Усіх функцій таймерів я розписувати не буду, а то вийде неподімний трактат, краще розповім про основну — лічильну, а всякі ШІМ та інші генератори будуть в іншій статті. Так що наберіться терпіння, ну або гризіть даташит, теж корисно.

Отже, головним регістром є TCCRx
Для Т/С0 та Т/С2 це TCCR0 та TCCR2 відповідно, а для Т/С1 це TCCR1B

Нас поки що цікавлять лише перші три біти цього регістру:
CSx2.. CSx0, замість x підставляється номер таймера.
Вони відповідають установку предделителя і джерело тактового сигналу.

У різних таймерів трохи по-різному, тому опишу біти CS02.CS00 тільки для таймера 0

  • 000 - таймер зупинено
  • 001 - Преддільник дорівнює 1, тобто вимкнений. таймер вважає тактові імпульси
  • 010 - Преддільник дорівнює 8, тактова частота ділиться на 8
  • 011 - Преддільник дорівнює 64, тактова частота ділиться на 64
  • 100 - Преддільник дорівнює 256, тактова частота ділиться на 256
  • 101 - Преддільник дорівнює 1024, тактова частота ділиться на 1024
  • 110 - тактові імпульси йдуть від ніжки Т0 на переході з 1 на 0
  • 111 - тактові імпульси йдуть від ніжки Т0 на переході з 0 на 1

Переривання
Кожна апаратна подія має переривання, от і таймер не виняток. Як тільки відбувається переповнення або ще якась цікава подія, так відразу ж вилазить переривання.

За переривання від таймерів відповідають регістри TIMSК, TIFR. А у більш крутих AVR, таких як ATMega128, є ще ETIFR і ETIMSK - свого роду продовження, тому що таймерів там більше буде.

TIMSK це регістр масок. Тобто біти, що у ньому, локально дозволяють переривання. Якщо біт встановлений, то конкретне переривання дозволено. Якщо біт у нулі, це переривання накривається тазиком. По дефолту всі биті на нулі.

Зараз нас тут цікавлять лише переривання з переповнення. За них відповідають біти

  • TOIE0 - дозвіл на переривання з переповнення таймера 0
  • TOIE1 - дозвіл на переривання з переповнення таймера 1
  • TOIE2 - дозвіл на переривання з переповнення таймера 2

Про інші фічі і переривання таймера ми поговоримо пізніше, коли розбиратимемо ШІМ.

Реєстр TIFR – це безпосередньо прапоровий регістр. Коли якесь переривання спрацьовує, то вискакує там прапор, що маємо переривання. Цей прапор скидається апаратно, коли програма йде по вектору. Якщо переривання заборонені, то прапор так і стоятиме доти, доки переривання не дозволять і програма не піде на переривання.

Щоб цього не сталося, прапор можна скинути вручну. Для цього в регістрі TIFR до нього потрібно записати 1!

А тепер похимічимо
Ну перекроїмо програму на роботу з таймером. Введемо програмний таймер. Шарманка так і залишиться, нехай цокає. А ми додамо другу змінну, теж на чотири байти:

ORG $010 RETI; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RJMP Timer0_OV ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete

Додамо обробник переривання переповнення таймера 0, в секцію Interrupt. Так як наш цокаючий макрос активно працює з регістрами і псує прапори, то цю справу треба все зберегти в стеку спочатку:

До речі, давайте створимо ще один макрос, що пишає в стек прапоровий регістр SREG і другий — його звідти.

1 2 3 4 5 6 7 8 9 10 11 12 .MACRO PUSHF PUSH R16 IN R16,SREG PUSH R16 .ENDM .MACRO POPF POP R16 OUT SREG,R16 POP R16 .ENDM

MACRO PUSHF PUSH R16 IN R16,SREG PUSH R16 .ENDM .MACRO POPF POP R16 OUT SREG,R16 POP R16 .ENDM

Як побічний ефект він ще зберігає і R16, пам'ятаємо про це:)

1 2 3 4 5 6 7 8 9 10 11 12 13 Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI

Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI

Наразі ініціалізація таймера. Додай її до секції ініту локальної периферії (Internal Hardware Init).

; Internal Hardware Init ====================================== SETB DDRD,4,R16 ; DDRD.4 = 1 SETB DDRD,5,R16; DDRD.5 = 1 SETB DDRD,7,R16; DDRD.7 = 1 SETB PORTD,6,R16; Виведення PD6 на вхід з підтягом CLRB DDRD,6,R16; Щоб рахувати кнопку SETB TIMSK,TOIE0,R16; Дозволяємо переривання таймера OUTI TCCR0,1<

Залишилося переписати наш блок порівняння та перерахувати число. Тепер все просто, один тик один такт. Без проблем з різною довжиною коду. Для однієї секунди на 8Мгц має бути зроблено 8 мільйонів тиків. У хексах це 7A 12 00 з урахуванням, що молодший байт у нас TCNT0, то на наш лічильник залишається 7А 12 ну і ще старші два байти 00 00, їх можна не перевіряти. Маскувати не потрібно, таймер ми потім перевстановимо все одно.

Одна тільки проблема - молодший байт, який у таймері. Він цокає кожен такт і перевірити його на відповідність майже неможливо. Т.к. Найменша розбіжність та умова порівняння випаде в NoMatch, а підгадати так, щоб перевірка його значення збіглася саме з цим тактом… Простіше голку зі стога сіна витягнути з першої спроби навмання.

Тож точність і в цьому випадку обмежена — треба встигнути перевірити значення, перш ніж воно піде з діапазону. У разі діапазон буде, для простоти, 255 — величина молодшого байта, те, що у таймері.

Тоді наша секунда забезпечується з точністю 8000000 плюс мінус 256 тактів. Невелика похибка, лише 0,003%.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 ; Main ================================================== ======== Main: SBIS PIND,6; Якщо кнопка натиснута - перехід RJMP BT_Push SETB PORTD,5; Засвітимо LED2 CLRB PORTD,4; Погасимо LED1 Next: LDS R16, TCNT; Вантажимо числа в регістри LDS R17, TCNT + 1 CPI R16, 0x12; Порівнюємо побайтно. Перший байт BRCS NoMatch; Якщо менше – значить не натикало. CPI R17,0x7A; Другий байт BRCS NoMatch; Якщо менше – значить не натикало. ; Якщо збіглося то робимо екшн Match: INVB PORTD, 7, R16, R17; Інвертували LED3; Тепер треба обнулити лічильник, інакше за цю саму ітерацію головного циклу; ми сюди потрапимо ще не один раз - таймер не встигне натикати 255 значень, ; щоб число у перших двох байтах лічильника змінилося та умова спрацює. ; Звичайно, можна обійти це додатковим прапорцем, але простіше скинути лічильник:) CLR R16; Нам потрібен нуль CLI; Доступ до багатобайтної змінної; одночасно з переривання та фону; потрібний атомарний доступ. Заборона переривань OUTU TCNT0, R16; Нуль в рахунковий регістр таймера STS TCNT, R16; Нуль у перший байт лічильника в RAM STS TCNT + 1, R16; Нуль у другий байт лічильника в RAM STS TCNT+2, R16; Нуль в третій байт лічильника в RAM STS TCNT +3, R16; Нуль у перший байт лічильника в RAM SEI; Дозволяємо переривання знову. ; Не співпало - не робимо:) NoMatch: NOP INCM CCNT; Лічильник циклів потикає; Нехай, хоч і не використовується. JMP Main BT_Push: SETB PORTD,4; Засвітимо LED1 CLRB PORTD,5; Погасимо LED2 RJMP Next; End Main ================================================= =====

; Main ================================================== ======== Main: SBIS PIND,6; Якщо кнопка натиснута - перехід RJMP BT_Push SETB PORTD,5; Засвітимо LED2 CLRB PORTD,4; Погасимо LED1 Next: LDS R16, TCNT; Вантажимо числа в регістри LDS R17, TCNT + 1 CPI R16, 0x12; Порівнюємо побайтно. Перший байт BRCS NoMatch; Якщо менше – значить не натикало. CPI R17,0x7A; Другий байт BRCS NoMatch; Якщо менше – значить не натикало. ; Якщо збіглося то робимо екшн Match: INVB PORTD, 7, R16, R17; Інвертували LED3; Тепер треба обнулити лічильник, інакше за цю саму ітерацію головного циклу; ми сюди потрапимо ще не один раз - таймер не встигне натикати 255 значень, ; щоб число у перших двох байтах лічильника змінилося та умова спрацює. ; Звичайно, можна обійти це додатковим прапорцем, але простіше скинути лічильник:) CLR R16; Нам потрібен нуль CLI; Доступ до багатобайтної змінної; одночасно з переривання та фону; потрібний атомарний доступ. Заборона переривань OUTU TCNT0, R16; Нуль в рахунковий регістр таймера STS TCNT, R16; Нуль у перший байт лічильника в RAM STS TCNT + 1, R16; Нуль у другий байт лічильника в RAM STS TCNT+2, R16; Нуль в третій байт лічильника в RAM STS TCNT +3, R16; Нуль у перший байт лічильника в RAM SEI; Дозволяємо переривання знову. ; Не співпало - не робимо:) NoMatch: NOP INCM CCNT; Лічильник циклів потикає; Нехай, хоч і не використовується. JMP Main BT_Push: SETB PORTD,4; Засвітимо LED1 CLRB PORTD,5; Погасимо LED2 RJMP Next; End Main ================================================= =====

Ось як це виглядає у роботі

А якщо треба буде блимати другим діодиком з іншим періодом, то ми сміливо можемо вліпити в програму ще одну змінну, а в обробнику переривання таймера інкрементувати одразу дві перемінні. Перевіряючи їх по черзі у головному циклі програми.

Можна ще трохи оптимізувати процес перевірки. Зробити його швидшим.

Треба лише зробити рахунок не так на підвищення, але в зниження. Тобто. завантажуємо в змінну кількість і починаємо його декрементувати у перериванні. І там же, в обробнику, перевіряємо його на нуль. Якщо нуль, то виставляємо у пам'яті прапорець. А наша фонова програма цей прапорець ловить і запускає екшн, принагідно встановлюючи витримку.

А якщо треба точніше? Ну тут варіант тільки один - заюзати обробку події прямий у обробнику переривання, а значення в TCNT:TCNT0 щоразу підлаштовувати так, щоб переривання відбувалося точно в потрібний час.

Переривання дозволяють мікроконтролерам відгукуватися на події без необхідності постійно перевіряти виконання будь-яких умов, щоб визначити момент, коли відбулися важливі зміни. На додаток до можливості підключати джерела переривань до деяких контактів можна використовувати переривання, генеровані таймером.

Апаратні переривання

Для демонстрації використання переривань повернемося знову до цифрових входів. Часто для визначення моменту деякої вхідної події (наприклад, натискання кнопки) використовується такий код:

if (digitalRead(inputPin) == LOW)

// Виконати якісь дії

Цей код постійно перевіряє рівень напруги на контакті inputPin, і коли digitalRead повертає LOW, виконуються якісь дії, позначені коментарем // Виконати якісь дії. Це цілком робоче рішення, але якщо всередині функції loop потрібно виконати масу інших операцій? На всі ці операції потрібен час, тому є можливість пропустити коротке натискання на кнопку, поки процесор буде зайнятий чимось іншим. Насправді пропустити факт натискання на кнопку майже неможливо, тому що за мірками мікроконтролера вона залишається дуже довго натиснутою.

Але як бути з короткими імпульсами від датчика, які можуть тривати мільйонні частки секунди? Для прийому таких подій слід використовувати переривання, визначаючи функції, які будуть викликатись за цими подіями, незалежно від того, чим зайнятий мікроконтролер. Такі переривання називають апаратними перериваннями(hardware interrupts).

У Arduino Uno лише два контакти пов'язані з апаратними перериваннями, через що вони використовуються дуже економно. У Leonardo таких контактів п'ять, на великих платах, таких як Mega2560, їх набагато більше, а в Due всі контакти підтримують можливість переривання.

Далі розповідається, як працюють апаратні переривання. Щоб випробувати наведений приклад, вам знадобляться додаткова макетна плата, кнопка, опір на 1 кОм та кілька з'єднувальних проводів.

На рис. 3.1 зображено зібрану схему. Через опір на контакт D2 подається напруга HIGH, доки кнопка не буде натиснута, в цей момент відбудеться заземлення контакту D2 і рівень напруги на ньому впаде до LOW.

Завантажте в плату Arduino наступний скетч:

// sketch 03_01_interrupts

int ledPin = 13;

pinMode(ledPin, OUTPUT);

void stuffHapenned()

digitalWrite(ledPin, HIGH);

Мал. 3.1.Електрична схема для випробування переривань

Крім налаштування контакту LED на роботу в режимі цифрового виходу, функція setup за допомогою ще одного рядка пов'язує функцію переривання. Тепер у відповідь на кожне переривання автоматично буде викликатись ця функція. Розглянемо цей рядок уважніше, тому що аргументи функції, що викликається тут, виглядають дещо незвичайно:

attachInterrupt(0, stuffHapenned, FALLING);

Перший аргумент – 0 – це номер переривання. Було б зрозуміліше, якби номер переривання співпадав із номером контакту, але це не так. Arduino Uno переривання 0 пов'язане з контактом D2, а переривання 1 - з контактом D3. Ситуація стає ще заплутанішою через те, що в інших моделях Arduino ці переривання пов'язані з іншими контактами, а крім того, в Arduino Due потрібно вказувати номер контакту. На платі Arduino Due із перериваннями пов'язані всі контакти.

Я ще повернуся до цієї проблеми, а поки що перейдемо до другого аргументу. Цей аргумент - stuffHappened - представляє ім'я функції, яка повинна викликатись для обробки переривання. Ця функція визначена далі у скетчі. До таких функцій їх називають підпрограмами обробки переривань(Interrupt Service Routine, ISR), пред'являються особливі вимоги. Вони не можуть мати параметри і нічого не повинні повертати. У цьому є певний зміст: навіть при тому, що вони викликаються в різних місцях у скетчі, немає жодного рядка коду, що здійснює прямий виклик ISR, тому немає жодної можливості передати їм параметри або отримати значення, що повертається.

Останній параметр функції attachInterrupt - це константа, в даному випадку Falling. Вона означає, що підпрограма обробки переривання буде викликатися лише за зміни напруги на контакті D2 з рівня HIGH рівня LOW (тобто падіння - falling), що відбувається у момент натискання кнопки.

Зверніть увагу на відсутність будь-якого коду функції loop. У загальному випадку ця функція може містити код, що виконується, доки не відбулося переривання. Сама підпрограма обробки переривань просто включає світлодіод L.

Коли ви експериментуватимете, після скидання Arduino світлодіод L повинен згаснути. А після натискання на кнопку - одразу запалитись і залишатися запаленим до наступного скидання.

Поекспериментувавши, спробуйте змінити останній аргумент у викликунавплив Interrupt на RISING і вивантажте змінений скетч. Після перезапуску Arduino світлодіод повинен залишатися погашеним, тому що напруга на контакті хоч і має рівень HIGH, але з перезапуску залишалося на цьому рівні. До цього моменту напруга на контакті не падала рівня LOW, щоб потім піднятися (rising) рівня HIGH.

Після натискання та утримання кнопки у натиснутому стані світлодіод повинен залишатися погашеним, доки ви її не відпустите. Відпускання кнопки викликає переривання, пов'язане з контактом D2, тому що, поки кнопка утримувалася натиснутою, рівень напруги на контакті дорівнював LOW, а після відпускання піднявся до HIGH.

Якщо під час випробування з'ясується, що те, що відбувається у вас не відповідає опису, наведеному раніше, це, швидше за все, обумовлено ефектом брязкальця контактів у кнопці. Цей ефект викликається тим, що кнопка не забезпечує чіткий перехід між станами «ввімкнено»/«вимкнено», натомість у момент натискання відбувається багаторазовий перехід між цими станами, доки не зафіксується стан «ввімкнено». Спробуйте натискати кнопку енергійніше, це має допомогти отримати чіткий перехід між станами без ефекту брязкоту.

Інший спосіб випробувати цей варіант скетчу – натиснути кнопку і, утримуючи її, натиснути та відпустити кнопку скидання Reset на платі Arduino. Потім, коли скетч запуститься, відпустити кнопку на макетній платі і світлодіод L загориться.

Контакти із підтримкою переривань

Повернемося тепер до проблеми іменування переривань. У табл. 3.1 перераховано найбільш поширені моделі плат Arduino та наведено відповідність номерів переривань та контактів у них.

Таблиця 3.1.Контакти із підтримкою переривань у різних моделях Arduino

Модель Номер переривання Примітки
0 1 2 3 4 5
Uno D2 D3 - - - -
Leonardo D3 D2 D0 D1 D7 - Справді, в порівнянні з Uno перші два переривання призначені різним контактам
Mega2560 D2 D3 D21 D20 D19 D18
Due - - - - - - Замість номерів переривань функції attachInterrupt слід передавати номери контактів

Зміна контактів перших двох переривань у Uno та Leonardo створює пастку, в яку легко потрапити. У моделі Due замість номерів переривань функції attachInterrupt слід передавати номери контактів, що виглядає більш логічно.

Режими переривань

Режими переривань RISING (за позитивним перепадом) і FALLING (за негативним перепадом), що використовувалися в попередньому прикладі, найчастіше використовуються на практиці. Проте є ще кілька режимів. Ці режими перераховані та описані в табл. 3.2.

Таблиця 3.2.Режими переривань

Режим Дія Опис
LOW Переривання генерується за рівня напруги LOW У цьому режимі підпрограма обробки переривань буде викликатись постійно, поки на контакті зберігається низький рівень напруги
RISING Переривання генерується при позитивному перепаді напруги з рівня LOW до рівня HIGH -
FALLING Переривання генерується при негативному перепаді напруги з рівня HIGH до рівня LOW -
HIGH Переривання генерується за рівня напруги HIGH Цей режим підтримується тільки в моделі Arduino Due і, подібно до режиму LOW, рідко використовується на практиці

Включення внутрішнього імпедансу

У схемі в попередньому прикладі використовувався зовнішній опір, що «підтягує». Однак на практиці сигнали, що викликають переривання, часто заводяться з цифрових виходів датчиків, і в цьому випадку немає необхідності використовувати опір, що «підтягує».

Але якщо роль датчика грає кнопка, підключена так само, як макетна плата на рис. 3.1 є можливість позбутися опору, включивши внутрішній «підтягує» опір з номіналом близько 40 кОм. Для цього потрібно явно налаштувати режим INPUT_PULLUP для контакту, пов'язаного з перериванням, як показано у рядку, виділеному жирним шрифтом:

pinMode(ledPin, OUTPUT);

pinMode(2, INPUT_PULLUP);

attachInterrupt(0, stuffHapenned, FALLING);

Підпрограми обробки переривань

Іноді може здатися, що можливість обробляти переривання, поки виконується функція loop, дає простий спосіб обробки подій, таких як натискання клавіш. Але насправді накладаються дуже жорсткі обмеження те що, що можна чи не можна робити у підпрограмах обробки переривань.

Підпрограми обробки переривань мають бути короткими та швидкими настільки, наскільки це можливо. Якщо під час роботи підпрограми обробки переривань виникне інше переривання, цю підпрограму не буде перервано, а отриманий сигнал буде просто проігноровано. Це, наприклад, означає, що якщо переривання використовуються для вимірювання частоти, ви можете отримати неправильне значення.

Крім того, поки виконується підпрограма обробки переривань, код функції loop простоює.

На час обробки переривання автоматично вимикаються. Таке рішення оберігає від плутанини між підпрограмами, що переривають один одного, але має небажані побічні ефекти. Функція delay використовує таймери та переривання, тому вона не працюватиме в підпрограмах обробки переривань. Те саме стосується функції millis. Спроба використовувати millis для отримання числа мілісекунд, що пройшли з моменту останнього скидання плати, щоб таким способом виконати затримку, не призведе до успіху, оскільки вона повертатиме те саме значення, поки підпрограма обробки переривань не завершиться. Однак, ви можете використовувати функцію delayMicroseconds, яка не використовує переривань.

Переривання використовуються також у взаємодію через послідовні порти, тому не намагайтеся використовувати Serial.print або функції читання з послідовного порту. Втім, ви можете спробувати, і іноді вони навіть працюватимуть, але не чекайте від такого зв'язку високої надійності.

Оперативні змінні

Так як підпрограма обробки переривань не може мати параметрів і не може нічого повертати, потрібен якийсь спосіб передачі між нею та іншою програмою. Зазвичай для цього використовуються глобальні змінні, як показано в наступному прикладі:

// sketch 03_02_interrupt_flash

int ledPin = 13;

volatile boolean flashFast = false;

pinMode(ledPin, OUTPUT);

attachInterrupt(0, stuffHapenned, FALLING);

int period = 1000;

if (flashFast) період = 100;

digitalWrite(ledPin, HIGH);

digitalWrite(ledPin, LOW);

void stuffHapenned()

flashFast =! flashFast;

У цьому скетчі функція loop використовує глобальну змінну flashFast, щоб визначити період затримки. Підпрограма обробки змінює значення цієї змінної між true та false.

Зверніть увагу, що в оголошення змінної flashFast включено слово volatile. Ви можете успішно розробляти скетч і без специфікатора volatile, але він абсолютно необхідний, тому що без цього специфікатора компілятор C може генерувати машинний код, що кешує значення змінної в регістрі для збільшення продуктивності. Якщо, як у цьому випадку, код, що кешує, буде перерваний, він може не помітити зміни значення змінної.

На закінчення про підпрограми обробки переривань

Коли писатимете підпрограми обробки переривань, пам'ятайте такі правила.

Підпрограми мають діяти швидко.

Для передачі між підпрограмою обробки переривань та іншою програмою повинні використовуватися змінні, оголошені зі специфікатором volatile.

Не використовуйте delay, але можете використовувати delayMicroseconds.

Не чекайте високої надійності взаємодій через послідовні порти.

Не очікуйте, що значення, яке повертається функцією millis, зміниться.

Дозвіл та заборона переривань

За замовчуванням переривання в скетчах дозволено і, як згадувалося раніше, автоматично забороняються під час роботи підпрограми обробки переривань. Однак є можливість явно забороняти і дозволяти переривання в програмному коді, викликаючи функції nointerrupts і interrupts. Ці функції немає параметрів, і перша їх забороняє переривання, а друга - дозволяє.

Явне управління може знадобитися, щоб унеможливити переривання фрагмента коду, наприклад, що виводить послідовність даних або генерує послідовність імпульсів і точно витримує часові інтервали за допомогою функції delayMicroseconds.

Переривання від таймера

Виклик підпрограм обробки переривань можна організувати не лише за зовнішніми подіями, а й за внутрішніми подіями зміни часу. Така можливість є особливо корисною, коли потрібно виконувати деякі операції через певні інтервали часу.

Бібліотека TimerOne полегшує налаштування переривань від таймера. Її можна знайти та завантажити за адресою http://playground.arduino.cc/Code/Timer1.

Наступний приклад показує, як за допомогою TimerOne згенерувати послідовність імпульсів прямокутної форми із частотою 1 кГц. Якщо у вашому розпорядженні є осцилограф або мультиметр із можливістю вимірювання частоти, підключіть його до контакту 12, щоб побачити сигнал (рис. 3.2).

Мал. 3.2.Послідовність прямокутних імпульсів, що згенерована за допомогою таймера

// sketch_03_03_1kHz

#include

int outputPin = 12;

volatile int output = LOW;

pinMode(12, OUTPUT);

Timer1.initialize(500);

Timer1.attachInterrupt(toggleOutput);

void toggleOutput()

digitalWrite(outputPin, output);

output =! output;

Те саме можна було б реалізувати за допомогою delay, але застосування переривань від таймера дозволяє організувати виконання будь-яких інших операцій усередині loop. Крім того, використання функції delay не дозволить досягти високої точності, тому що час, необхідний зміну рівня напруги на контакті, не враховуватиметься у величині затримки.

ПРИМІТКА

Усі обмеження для підпрограм обробки зовнішніх переривань, про які розповідалося раніше, поширюються також підпрограми обробки переривань від таймера.

Представленим способом можна встановити будь-який інтервал між перериваннями в діапазоні від 1 до 8388480 мкс, тобто приблизно до 8,4 с. Розмір інтервалу передається функції initialize в мікросекундах.

Бібліотека TimerOne дає можливість використовувати таймер для генерування сигналів з широтно-імпульсною модуляцією (Pulse Width Modulation, PWM) на контактах 9 і 10 плати. Це може здатися надмірністю, тому що те саме робить функція analogWrite, але застосування переривань дозволяє забезпечити більш точне керування сигналом PWM. Зокрема, використовуючи такий підхід, можна організувати вимірювання протяжності позитивного імпульсу в діапазоні 0...1023 замість 0...255 функції analogWrite. Крім того, при використанні analogWrite частота проходження імпульсів у сигналі PWM становить 500 Гц, а за допомогою TimerOne можна цю частоту збільшити або зменшити.

Щоб згенерувати сигнал PWM із застосуванням бібліотеки TimerOne, використовуйте функцію Timer1.pwm, як показано на прикладі:

// sketch_03_04_pwm

#include

pinMode(9, OUTPUT);

pinMode(10, OUTPUT);

Timer1.initialize(1000);

Timer1.pwm(9, 512);

Timer1.pwm (10, 255);

Тут обраний період прямування імпульсів, що дорівнює 1000 мкс, тобто частота сигналу PWM становить 1 кГц. На рис. 3.3 показано форму сигналів на контактах 10 ( вгорі) та 9 ( внизу).

Мал. 3.3.Широтно-імпульсний сигнал із частотою 1 кГц, згенерований за допомогою TimerOne

Заради інтересу давайте подивимося, наскільки можна збільшити частоту сигналу PWM. Якщо зменшити тривалість періоду до 10, частота сигналу PWM має збільшитись до 100 кГц. Форма сигналів, отриманих із цими параметрами, показано на рис. 3.4.

Незважаючи на наявність суттєвих перехідних спотворень, що цілком очікувано, протяжність позитивних імпульсів все ж таки залишається досить близькою до 25 і 50% відповідно.

Мал. 3.4.Широтно-імпульсний сигнал із частотою 100 кГц, згенерований за допомогою TimerOne

На закінчення

Переривання, які іноді здаються ідеальним рішенням для непростих проектів, можуть ускладнити налагодження коду і не завжди виявляються найкращим способом вирішення важких завдань. Ретельно обміркуйте можливі рішення, перш ніж переходити до використання. У розділі 14 ми познайомимося з іншим прийомом подолання складнощів, пов'язаних з тим, що Arduino не може виконувати більше одного завдання одночасно.

Ми ще повернемося до переривань у розділі 5, де розглянемо можливість їх застосування зменшення споживання електроенергії платою Arduino за рахунок періодичного переведення її в режим енергозбереження, і в главі 13, де переривання будуть застосовуватися для збільшення точності обробки цифрових сигналів.

У наступному розділі ми познайомимося із прийомами збільшення продуктивності Arduino до максимуму.



Сподобалася стаття? Поділіться їй