Опитування кнопок avr. Підключення кнопки до AVR. Апаратне придушення брязкоту контактів

03.11.2021

Описано простий експеримент із підключенням кнопки до AVR мікроконтролера, розібрано не складну програму мовою Сі для обробки натискань кнопки. Розберемося з особливостями підключення кнопки до портів МК, і навіть з способами зчитування станів кнопки мовою Сі.

У попередніх статтях були розглянуті експерименти зі світлодіодами, які підключалися до портів мікроконтролера, налаштованих на виведення (Output).

У цій статті ми підключимо до мікроконтролера кнопку, контакти якої при натисканні замикаються, а при відтисканні - розмикаються (кнопка, що замикає).

Принципова схема експерименту

Для того, щоб можна було хоч якось спостерігати і керувати чимось за допомогою кнопки, ми підключимо до мікроконтролера ще два світлодіоди. Схемка дуже проста, ось вона:

Мал. 1. Принципова схема експерименту з мікроконтролером ATtiny2313 та кнопкою.

Як бачимо, до двох портів PB0 і PB1 через резистори, що обмежують, підключені два світлодіоди, а до порту PD2 - кнопка і вона також з обмежуючим резистором. Для підключення програматора до МК використовується роз'єм Conn 1 (AVR-ISP), а для підключення схеми до окремого джерела живлення +5В призначені два контакти – P1 та P2.

Мал. 2. Зібрана на безпаєчній макетній панелі схема експерименту з мікроконтролером та кнопкою.

Для безпечного використання порту з кнопкою, послідовно їй підключений резистор з опором на 1 КОм (можна підключити і на інший опір 600 Ом - 2 КОм). Прийміть це, як правило, гарного тону в роботі з пінами, яке оберігає порт МК від виходу з ладу у разі помилкової подачі на пін високого рівня та при замкнутій кнопці.

Структура портів введення-виведення в AVR мікроконтролерах

Піни мікроконтролера є універсальними GPIO (General Purpose Input Output), до них можна підключати як виконавчі пристрої (індикатори, силові ключі), і різноманітні цифрові датчики (кнопки, перемикачі).

Декілька пінів в МК можуть бути підключені до АЦП/ЦАП (Аналогово-Цифровий-Перетворювач і навпаки), з їх допомогою можна виконувати аналіз та генерацію аналогових сигналів. Звичайні GPIO не вміють працювати з аналоговими сигналами, у них на вході/виході може бути лише 0(0В) або 1(+5В).

До кожного піна GPIO всередині мікроконтролера підключено кілька блоків та електронних компонентів, про які корисно знати:

  • Між піном порту та кожною з шин живлення (GND і VCC) підключено за діодом. Вони використовуються для "гасіння" короткочасних перешкод, стрибків напруги щодо піна та кожної з шин живлення;
  • Також між піном та GND включений конденсатор. Точно не знаю навіщо він потрібен, можливо для захисту від перешкод, для запобігання брязкіт контактів при використанні кнопок і перемикачів підключених до піну, або ще для чогось;
  • До кожного піну підключено електронний ключ із резистором - це підтяжка піна до напруги джерела живлення (Pull-UP). Цей електронний ключ включається програмно і служить для встановлення за замовчуванням високого логічного рівня 1 (+5В) під час роботи з піном у режимі введення (Input);
  • Між піном та кожною з шин живлення (GND і VCC) включені ще два електронні ключі (без резисторів), вони потрібні для встановлення на піні високого (+5В) або низького (0В) логічного рівня під час роботи піна в режимі виведення (Output).

Для програмного управління та конфігурування кожного з портів застосовуються три спеціальні регістри, наприклад для порту "B":

  • DDRB – регістр (8 біт) для встановлення режимів роботи пінів – на введення або виведення. Здійснюється установкою відповідних біт у регістрі;
  • PORTB - регістр для керування станом пінів порту в режимі виведення - високий або низький рівень. Також використовується в режимі введення, застосовується для включення підтягуючих резисторів (Pull-UP) та встановлення високого рівня на вході за замовчуванням;
  • PINB - регістр, що містить логічні стани пінів у порті, використовується для читання значень портів, які налаштовані в режимі введення.

Більш детально дізнатися про пристрій портів для конкретної моделі мікроконтролера можна з його даташита, в розділі "I/O-Ports", також там можуть бути наведені приклади коду на Сі та Асемблері для роботи з портами.

Пін RESET як порт введення-виведення

Корисно знати, що пін "RESET" мікросхеми (у нас на схемі це пін під номером 1), який призначений для скидання виконання програми мікроконтролера (перезавантаження), також можна використовувати для підключення кнопок, світлодіодів та інших пристроїв введення-виводу, тобто він може бути перетворений на звичайний GPIO.

Це може бути корисно, якщо у мікросхеми не вистачає пінів для вашої конструкції. Наприклад, при складанні якогось пристрою на чіпі ATtiny13 (8 висновків, 2шт - живлення, 5шт - порти вводу-виводу, 1шт -для RESET) у вас виявилося що не вистачає одного піна для світлодіода. Тут може бути кілька варіантів вирішення проблеми:

  1. Перепрограмування піна з RESET під порт вводу-виводу;
  2. Підключення світлодіода до одного із сусідніх вже використаних пінів, застосувавши деякі хістрости у схемному рішенні та з урахуванням можливості його загального використання;
  3. Використання іншого МК має більше пінів, наприклад ATtiny2313.

Що з цих варіантів простіше і дешевше за фінансами/часом – судіть з власного випадку.

Для перетворення піна "RESET" на порт вводу-виводу доведеться змінити спеціальний фьюз - RSTDISBL (Reset Disable). Але перш ніж це зробити потрібно пам'ятати, що після цієї операції перепрограмувати мікроконтролер стане можливим тільки із застосуванням високовольтного програматора (на 12В), звичайний USB ISP або інший програматор з живленням від 5В зробити свою роботу вже не зможе.

Програма на Сі

Отже, у нас є одна кнопка та два світлодіоди які підключені до мікроконтролера, що ж з ними можна зробити? - а зробимо ми ось що (алгоритм):

  1. Після включення живлення світлодіоди блиматимуть поперемінно та із затримкою в 300 мілісекунд;
  2. При натисканні та утриманні кнопки світиться тільки синій світлодіод;
  3. Після відтискання кнопки синій світлодіод блимне 3 рази із затримкою 500 мілісекунд, після чого світлодіоди знову блиматимуть по черзі та із затримкою 300 мілісекунд.

Приклад реалізації такого алгоритму мовою Сі під AVR наведено нижче. Створимо новий файл для нашої програми та відкриємо його для редагування:

Nano /tmp/avr-switch-test.c

Помістимо наступний код у тіло файлу:

/* Експеримент з кнопкою на ATtiny2313 * https://сайт */ #define F_CPU 1000000UL // Частота ядра = 1 МГц #include #include // -- Макроси для керування світлодіодами -- #define LED_BLUE_ON PORTB |= (1<< PB0) // Засвечиваем синий диод #define LED_BLUE_OFF PORTB &= ~(1 << PB0) // Гасим синий диод #define LED_RED_ON PORTB |= (1 << PB1) // Засвечиваем красный диод #define LED_RED_OFF PORTB &= ~(1 << PB1) // Гасим красный диод // Основная программа void main(void) { DDRD |= (0 << PD2); // Пин 6 - на вход PORTD |= (1 << PD2); // Включаем подтягивающий (Pull-UP) резистор для пина 6 DDRB |= (1 << PB0); // Пин 12 - на вывод DDRB |= (1 << PB1); // пин 13 - на вывод // -- Бесконечный цикл -- while(1) { _delay_ms(300); // Задержка 300 мс LED_BLUE_ON; // Включаем синий диод LED_RED_OFF; // Гасим красный диод _delay_ms(300); LED_RED_ON; // Включаем красный диод LED_BLUE_OFF; // Гасим синий диод if(!(PIND & (1 << PD2))) { // Проверяем нажата ли кнопка _delay_ms(50); // Задержка 50 мс (дребезг контактов) LED_RED_OFF; LED_BLUE_ON; while(!(PIND & (1 << PD2))); // Ждем пока кнопка не будет отпущена _delay_ms(500); // Дальше мигаем синим диодом LED_BLUE_OFF; _delay_ms(500); LED_BLUE_ON; _delay_ms(500); LED_BLUE_OFF; _delay_ms(500); LED_BLUE_ON; _delay_ms(500); LED_BLUE_OFF; _delay_ms(200); } // Конец блока работы с кнопкой } // Конец блока с вечным циклом }

Насамперед ми задаємо константу F_CPU, яка вкаже компілятору робочу частоту ядра мікроконтролера, це потрібно, щоб деякі підпрограми та функції працювали коректно. У прикладі використовується функція затримки за часом - "_delay_ms" з бібліотеки "util/delay.h", яка прораховує час витрачений на неодружені такти, спираючись на значення в константі F_CPU.

Переглянути код бібліотеки "delay" для організації затримки за часом і в якому використовується константа F_CPU, можна в GNU Linux за допомогою будь-якого текстового редактора, наприклад можна виконати таку команду:

Nano /usr/lib/avr/include/util/delay.h

Заводська встановлена ​​частота внутрішнього RC генератора в мікроконтролері ATtiny2313 дорівнює 8000000Гц (8МГц), також за умовчанням встановлений ф'юз поділу частоти - CKDIV8 (Clock Divide by 8), тому реальна робоча частота кристала = 800000.

Подивитися які ф'юзи встановлені в мікроконтролері можна за допомогою avrdude або графічної оболонки до нього під назвою AVR8 Burn-O-Mat.

Далі у програмі визначені макроси для управління станом портів, до яких підключені світлодіоди: LED_BLUE_ON, LED_BLUE_OFF, LED_RED_ON, LED_RED_OFF. Викликавши подібний макрос у будь-якому місці програми, ми дуже просто можемо запалити або погасити кожен із світлодіодів, не доведеться повторювати його код, що у свою чергу спростить програму і зробить її наочнішою.

В основній програмі "void main(void)" ми починаємо роботу з конфігурації портів:

  • DDRD |= (0<< PD2) - установка разряда PD2 регистра DDRD на ввод, к нему подключена кнопка (пин 6);
  • PORTD |= (1<< PD2) - включение подтягивающего резистора для пина к которому привязан разряд PD2 регистра PORTD (пин 6);
  • DDRB | = (1<< PB0) - установка разряда PB0 в регистре DDRB на вывод, к нему подключен СИНИЙ светодиод (пин 12);
  • DDRB | = (1<< PB1) - установка разряда PB1 в регистре DDRB на вывод, к нему подключен КРАСНЫЙ светодиод (пин 13).

Далі, використовуючи макроси, ми гасимо червоний світлодіод та запалюємо синій. Тепер за допомогою ще одного вічного циклу але ж з умовою ми виконаємо очікування до того моменту, поки кнопка не буде віджата: "while (! (PIND & (1)<< PD2)));".

При віджатій кнопці на піні 6 з'явиться високий рівень (це зробить внутрішній резистор, що підтягує, який ми включили раніше), а в розряді PD2 регістра PIND буде встановлена ​​логічна 1.

Після цього виконується триразове миготіння (ввімкнення-вимикання) синього світлодіода із затримкою в 0,5 секунди і основний вічний цикл починає роботу по новому – по черзі запалюватимуться два світлодіоди.

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

Налаштування Geany під ATtiny2313

У попередніх публікаціях я проводив експерименти з мікроконтролером ATMega8, тут використовується менш "нафарширований" МК - ATTiny2313.

Для компіляції програми та прошивки її в МК слід трошки переналаштувати команди для збирання в інтегрованому середовищі програмування Geany.

Ідемо в меню Build - Set Build Commands. У команді для компіляції (C commands) потрібно змінити модель чіпа, що застосовується: "-mmcu=attiny2313". У команді для прошивки МК потрібно змінити тип чіпа для avrdude: "-p t2313".

Мал. 3. Переналаштування Geany для роботи з мікроконтролером ATTiny2313.

Всі команди наведені для ОС GNU Linux, якщо у вас Windows, то можливо доведеться прописати повні шляхи до бінарних файлів "avr-gcc.exe", "avr-objcopy.exe", "avrdude.exe".

Більш детально про те, як налаштувати Geany в GNU Linux, я розглядав в одній з попередніх статей циклу.

Компіляція та прошивка програми в МК

Компіляцію, складання та прошивку програми можна виконати натиснувши в середовищі Geany по черзі три кнопки: "Compile", "Build" та "Run". Також усі ці операції можна виконати з консолі, ось команди для цих дій (виконувати послідовно):

Avr-gcc -mmcu=attiny2313 -Os /tmp/avr-switch-test.c -o /tmp/avr-switch-test.o avr-objcopy -j .text -j .data -O ihex /tmp/avr- switch-test.o /tmp/avr-switch-test.hex avrdude -c usbasp -p t2313 -P usb -U flash:w:/tmp/avr-switch-test.hex

Усі команди майже повністю (за винятком підстановок імен файлів) ідентичні тим, які ми виправляли у налаштуваннях Geany.

Висновок

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

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

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

Отже, що вміє наша кнопка?

  • Її можна натискати коротко
  • Можна натискати довго
  • Можна робити різні комбінації натискань
  • Її можна відпускати у потрібний момент


Не густо, але нічого. Для однієї кнопки щось. Головний затик при написанні цього не просрать масу системних ресурсів (час процесора, таймери та тиди) на обробку цієї нещасної кнопки. Адже нам доведеться відслідковувати факт натискання, факт натискання, час натискання, кількість натискань з різним часом. Я бачив такі педалі реалізації цього інтерфейсу, що просто диву давався як можна нагородити стільки дурнів і гальм у цій одній сосні, та ще витратити всі таймери:)

Тож ТЗ виставимо наступне:

  • Жодних апаратних таймерів, крім таймера диспетчера.
  • Жодних тимчасових затримок, тільки виклик себе ж за таймером.
  • Жодних очікувань натискання-відтискання в циклі. Зайшли, перевірили – віддали управління.
  • Введемо часовий інтервал mode_time, протягом якого відстежуватимемо комбінацію натискань. Скажімо 2с
  • На виході матимемо кількість коротких і довгих натискань за цей інтервал

Алгоритм
Зробимо все на кінцевому автоматі. У нього будуть три стани:

  • Up - кнопка не натиснута
  • Dn – кнопка натиснута
  • Al – кнопка відпущена після тривалого натискання

А також буде одна службова процедура, яка через mode_time (2c) після першого екшну з кнопкою згрібе всі результати і щось з ними зробить. Що це вже не важливо. Від програми залежить.
І вся ця дрібниця буде крутитися в циклі, викликаючи сама себе через диспетчер (або якимось чином) раз на 20мс.

Up
Входимо.
Чи не натиснута кнопка? Якщо ні – виходимо. Якщо натиснуто, то переводимо автомат у положення Dn
Чи перевіряємо вперше за інтервал ми тут? Якщо перший, то поставимо нашу службову процедуру на відкладений запуск (через 2с), піднімемо прапор, що процес пішов.
Виходимо.

Dn
Входимо.
Ще натиснуто? Якщо ні, значить кнопка вже відпущена, скидаємося в стан в Up і зараховуємо одне коротке натискання, збільшуючи лічильник коротких натискань cnt_s. Якщо ще натиснута, то клацаємо лічильником часу вимірювання тривалості натискання Timе. Вимірювання тривалості у нас йде в ітераціях автомата. Одна ітерація 20мс. У ліміт довгого натискання заклав 20 ітерацій, що дає близько 400мс. Все, що більше 0.4с вважаємо довгим натисканням. Як натикає більше 20 ітерацій, то зараховуємо одне довге натискання і перекидаємо автомат у стан Al. Виходимо.

Al
Входимо.
Ще не відпустили? Якщо ні, то виходимо. Якщо кнопку відпущено, то перекидаємося в Up, скинувши змінну Time.


За час mode_time, за дві секунди, скільки встигнемо натикати — все наше. Запуститься процедура аналізу зібраних даних та розгребе натикане. Там уже все просто. Банальним case'ом робимо потрібний нам екшн. Я ось, наприклад, прапорці виставляю які перехоплює інше завдання. Головне не блокувати це завдання нічим важким, щоб не проґавити наступну комбінацію.

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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; // Змінні – прапорці натиснутих кнопок. // Ефективніше їх зробити бітовими полями // Але мені було ліньки. Оптимізуйте самі:) u16 bt_mode_time = 2000; // Тривалість послідовності аналізу // Зроблено змінною, а чи не константою // щоб можна було міняти на льоту. Знижуючи // там, де треба чекати тривалих комбінацій // що сильно підвищує чуйність інтерфейсу u08 bt_cnt; // Прапор, що сигналізує, що йде аналіз послідовності u08 bt_cnt_s; // Лічильник коротких натискань u08 bt_cnt_l; // Лічильник довгих натискань void bt_scan(void ) // Ця функція сканер. Вона повинна викликатися раз на 20мс { #define up 0 // Дефайни станів автомата. 0 – по дефолту.#define dn 1 #define al 3 static u08 bt_state = 0; // Змінна стан автомата static u08 bt_time = 0; // Змінна час роботи автомата switch (bt_state) // Власне автомат( case up: ( if (Close) // Якщо натиснуто кнопку(bt_state = dn; // Стадію в Down bt_time = 0; // Обнулюємо лічильник часу if (bt_cnt== 0 ) // Якщо вперше у комбінації(SetTimerTask(bt_ok, bt_mode_time); // Запускаємо процедуру аналізу bt_cnt = 1; // Підрахунок пішов! ) ) break ; // Вихід ) case dn: ( if (Close) // Все ще натиснуто?( if (20 > bt_time) // Натиснуто менше ніж 20 * 20мс?(// Так bt_time++; // Збільшуємо лічильник ітерацій) else (bt_state = al; // Ні, вже більше! Та у нас довгий натиск! Переходимо до АЛ bt_time = 0; // Скидаємо час виміру натискання bt_cnt_l++; // Зараховуємо одне довге натискання)) else (bt_state = up; // Кнопку відпущено? bt_time = 0; // Час виміру в нуль bt_cnt_s++; // Лічильник коротких натискань) break ; // Вихід) case al: // А тут ми якщо було довге натискання(if (Open) // Відпустили? (bt_state = up; // Так! Стадію в Up bt_time = 0; // Час у 0 bt_al = 1; // Зафіксували подію "Відпускання після довгого") break ; )) SetTimerTask(bt_scan, 20); // Зациклились через диспетчер. } // А це функція яка через 2 секунди спрацює та підбере усі результати підрахунку void bt_ok(void ) // Ловимо дешифрування подій тут( switch (bt_cnt_s) // Дивимося скільки натискань коротких(Case 1: bt1 = 1; break; // Такий прапорець і ставимо case 2: bt2 = 1; break; case 3: bt3 = 1; break; case 4: bt4 = 1; break; case 5: bt5 = 1; break; default: break; ) switch (bt_cnt_l) // Дивимося скільки натискань довгих(Case 1: bt_l = 1; break; // Такий прапорець і ставимо case 2: bt_l2 = 1; break; default: break; ) bt_cnt = 0; // Скидаємо лічильники bt_cnt_s = 0; bt_cnt_l = 0; )

#include u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; // Змінні – прапорці натиснутих кнопок. // Ефективніше їх зробити бітовими полями // Але мені було ліньки. Оптимізуйте самі:) u16 bt_mode_time = 2000; // Тривалість послідовності аналізу // Зроблено змінної, а чи не константою // щоб було змінювати на лету. Знижуючи // там, де не слід чекати тривалих комбінацій // що сильно підвищує чуйність інтерфейсу u08 bt_cnt; // Прапор, що сигналізує, що йде аналіз послідовності u08 bt_cnt_s; // Лічильник коротких натискань u08 bt_cnt_l; // Лічильник довгих натискань bt_scan(void) // Ця функція сканер. Вона повинна викликатися раз на 20мс ( #define up 0 // Дефайни станів автомата. 0 - по дефолту. #define dn 1 #define al 3 static u08 bt_state=0; // Змінна стан автомата static u08 bt_time =0; // Змінна часу роботи автомата switch(bt_state) // Власне автомат ( case up: ( if(Close) // Якщо натиснута кнопка ( bt_state = dn; // Стадію в Down bt_time = 0; // Обнулюємо лічильник часу if(bt_cnt==0) ) // Якщо вперше в комбінації ( SetTimerTask(bt_ok,bt_mode_time); // Запускаємо процедуру аналізу bt_cnt =1; // Підрахунок пішов! ) ) break; // Вихід ) case dn: ( if(Close) // Все ще натиснуто ? в АЛ bt_time = 0; // Скидаємо час виміру натискання bt_cnt_l++; // Зараховуємо одне довге натискання)) else (bt_state = up; // Кнопка відпущена? bt_cnt_s++; // Лічильник коротких натискань) break; // Вихід ) case al: // А тут ми були довге натискання ( if(Open) // Відпустили? ( bt_state = up; // Так! Стадію в Up bt_time = 0; // Час в 0 bt_al = 1; // Зафіксували подію "Опускання після довгого") break;)) SetTimerTask (bt_scan, 20); // Зациклились через диспетчер. ) // А це функція яка через 2 секунди спрацює і підбере всі результати підрахунку void bt_ok(void) // Ловимо дешифрування подій тут ( switch(bt_cnt_s) // Дивимося скільки натискань коротких ( case 1: bt1 = 1; break; // Такий прапорець і ставимо case 2: bt2 = 1; break; case 3: bt3 = 1; break; case 4: bt4 = 1; break; case 5: bt5 = 1; break; default: break; / Дивимося скільки натискань довгих (case 1: bt_l = 1; break; // Такий прапорець і ставимо case 2: bt_l2 = 1; break; default: break;) bt_cnt = 0; // Скидаємо лічильники bt_cnt_s = 0; bt_c ;)

Код написаний так, що на AVR там зав'язана буквально пара рядків. Принаймні у коді обробника натискання кнопки. Усі прив'язки на залізо йдуть у хідері, та й їх там нічого:

1 2 3 4 5 6 7 8 9 10 11 #include #include #define Open (BTN_PIN & 1<#define Close (!Open) extern void bt_scan(void ) ; void bt_ok(void); extern u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; extern u16 bt_mode_time;

#include #include #define Open (BTN_PIN & 1<

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

Все описане у статті м'ясо лежить у двох файлах button.cі button.h

Відео роботи

Дрібняк
Боротися з брязкотом тут уже не обов'язково. Т.к. частота сканування невелика, так що навіть голима і наглухо окислена кнопка моделі ТМ2 не давала брязкоту - він закінчувався раніше, ніж наступав наступний скан. А ось що тут можна докурити, то це захист від хибних спрацювань в результаті наведень. Адже варто на заваді продавити лінію в момент зчитування і зарахується спрацювання одноразового натискання. Це можна уникнути, зробивши перевірочні стани автомата. Скажімо додавши в Up лічильник ітерацій, щоб протягом, скажімо, двох-трьох ітерацій підтвердити, що кнопка таки натиснута і лише тоді переходити до Dn.

Варіанти
Щоправда, у своєму проекті я дещо змінив обробку. Т.к. мені не потрібні були множинні довгі натискання, то я зробив виставлення прапора «Довге натискання» відразу ж у обробнику AL і, заразом, прибрав підрахунок числа довгих натискань. Що дозволило підвищити чуйність роботи інтерфейсу приладу, де тривалим натисканням здійснювався вхід до пункту меню, а комбінацій із двома довгими натисканнями не використовувалися взагалі.

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

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

Схема

Схема підключення кнопок традиційна один контакт кнопки з'єднаний з виведенням мікроконтролера, інший з нулем живлення. Підтягуючі резистори внутрішні. Кнопки підключені до звичайних висновків мікроконтролера, які не мають функцій зовнішніх переривань.

Заголовний файл

Заголовковий файл містить символічні імена регістрів та висновків порту, до якого підключені кнопки. Коди, які будуть записуватися в буфер при натисканні кнопок. Прототипи функції.


void BUT_Init(void);

Налаштовує висновки до яких підключені кнопки на вхід і включає резистори, що підтягують. Викликається зазвичай на початку main`a.


void BUT_Debrief(void);

Виконує одноразове опитування кнопок. У функції реалізований антибрезговий захист – щоб номер кнопки був занесений у буфер, вона повинна бути натиснута протягом заданого числа циклів опитування. Номер кнопки заноситься до буфера одноразово, навіть якщо кнопка довго утримується. Ця функція викликається у перериванні таймера.


unsigned char BUT_GetKey(void);

Повертає вміст буфера кнопки, при цьому буфер очищається. Викликається із основного циклу програми.

void BUT_SetKey(unsigned char key);

Записує у буфер значення. Використовується для натискання кнопки.

Сішній файл

Цей файл містить реалізацію всіх функцій. З них інтерес, мабуть, є лише функцією опитування.


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

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

Дрібнення контактів можна усунути апаратно, за допомогою пасивного НЧ фільтра. Але набагато простіше побороти брязкіт, зробивши програму мікроконтролера несприйнятливою до нього. Варіант подібного алгоритму реалізований у функції BUT_Debrief()

#define THRESHOLD 20

volatile unsigned char pressedKey = 0;
unsigned char comp = 0;

void BUT_Debrief( void )
{
unsigned char key;

//Послідовне опитування висновків мк

if(BitIsClear(PIN_BUTTON, ENTER))
key = KEY_ENTER;
else if(BitIsClear(PIN_BUTTON, CANCEL))
key = KEY_CANCEL;
else if(BitIsClear(PIN_BUTTON, UP))
key = KEY_UP;
else if(BitIsClear(PIN_BUTTON, DOWN))
key = KEY_DOWN;
else{
key = KEY_NULL;
}

//якщо у тимчасовій змінній щось є
if(key) (

//і якщо кнопка утримується довго
//записати її номер у буфер
if(comp == THRESHOLD) (
comp = THRESHOLD + 10;
pressedKey = key;
return ;
}
else if(comp< (THRESHOLD+5)) comp++;

}
else comp=0;
}

На початку функції виконується послідовне опитування висновків. Якщо якась із кнопка натиснута, у тимчасову змінну key заноситься відповідний код. Потім перевіряється значення змінної comp, щоб з'ясувати, як довго кнопка знаходиться в натиснутому стані. Поки comp не досягне якогось порогового значення THRESHOLD, вона інкрементуватиметься. Як тільки значення comp стане рівним THRESHOLD, тимчасова змінна key буде скопійована в кнопковий буфер pressedKey. При цьому comp отримає нове значення, щоб при наступних опитуваннях кнопок мікроконтролер не інкрементував цю змінну і не записував у буфер. Таким чином, як довго користувач не утримував кнопку, мікроконтролер зафіксує факт її натискання тільки один раз.

Досить часто кнопки використовуються для встановлення якихось лінійно змінних величин. У цьому випадку зручно використовувати такий алгоритм – коротке натискання на кнопку повільно змінює величину, тривале натискання швидко. Підправимо частину функції опитування та отримаємо простий варіант реалізації цього алгоритму.

#define THRESHOLD2 300
...
if(key)
{
if(comp > THRESHOLD2)
{
comp = THRESHOLD2 - 40;
pressedKey = key;
return ;
}
else comp++;

if(comp == THRESHOLD)
{
pressedKey = key;
return;
}
}
else comp=0;
...

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

На цьому поки що все, але тема кнопок не закрита. Далі буде...

Завдання: Розробимо програму керування одним світлодіодом. При натисканні на кнопку світлодіод світиться, при відпусканні гасне.

Для початку розробимо важливу схему пристрою. Для підключення до мікроконтролера будь-яких зовнішніх пристроїв використовуються порти вводу-виводу. Кожен із портів здатний працювати як на вхід так і на вихід. Підключимо світлодіод до одного з портів, а кнопку до іншого. Для цього досвіду ми будемо використовувати контролер Atmega8. Ця мікросхема містить 3 порти вводу-виводу, має 2 восьмирозрядні і 1 шістнадцятирозрядний таймер/лічильник. Також на борту є 3-х канальний ШІМ, 6-ти канальний 10-ти бітний аналого-цифровий перетворювач та багато іншого. На мою думку, мікроконтролер чудово підходить для вивчення основ програмування.

Для підключення світлодіода ми використовуватимемо лінію PB0, а для зчитування інформації з кнопки скористаємося лінією PD0. Схема наведено на рис.1.

Мал. 1

Через резистор R2 на вхід PD0 подається плюс напруги живлення, що відповідає сигналу логічної одиниці. При замиканні кнопки напруга знижується до нуля, що відповідає логічному нулю. Надалі R2 можна виключити зі схеми, замінюючи його на внутрішній резистор навантаження, ввівши необхідні налаштування в програмі. Світлодіод підключений до виходу порту PB0 через резистор R3, що обмежує струм. Для того, щоб запалити світлодіод, треба подати в лінію PB0 сигнал логічної одиниці. Задає тактовий генератор використовуватимемо внутрішній на 4MHz, так як у пристрої немає високих вимог до стабільності частоти.

Тепер пишемо програму. Для написання програм я використовую програмне середовище AVR Studioі WinAvr.Відкриваємо AVR Studio, спливає віконце вітання, натискаємо кнопку "Створити новий проект" (New project), далі вибираємо тип проекту - AVR GCC, пишемо ім'я проекту наприклад "cod1", ставимо обидві галочки "Створити папку проекту" та "Створити файл ініціалізації" , натискаємо кнопку "Далі", у лівому вікні вибираємо "AVR Simulator", а в правому тип мікроконтролера "Atmega8", натискаємо кнопку "Фініш", відкривається редактор та дерево категорій проекту - початкові установки закінчені.

Для початку додамо стандартний текст описів Atmega8 за допомогою оператора приєднання зовнішніх файлів: #include

синтаксис директиви #include

#include<имя_файла.h>
#include “ім'я_файлу.h”

Кутові дужки< и >вказують компілятору, що файли, що підключаються, потрібно спочатку шукати в стандартній папці WinAvr з ім'ям include. Подвійні лапки і вказують компілятору починати пошук з директорії, в якій зберігається проект.

Для кожного типу мікроконтролера є свій заголовний файл. Для ATMega8 цей файл називається iom8.h, для ATtiny2313 – iotn2313.h. На початку кожної програми ми повинні підключати заголовний файл мікроконтролера, який ми використовуємо. Але є й загальний заголовний файл io.h. Препроцесор обробляє цей файл і в залежності від налаштувань проекту включає в нашу програму потрібний файл заголовка.

Для нас перший рядок програми виглядатиме ось так:

#include

Будь-яка програма мовою Сі повинна обов'язково містити одну головну функцію. Вона має ім'я main. Виконання програми завжди починається із виконання функції main. Функція має заголовок - int main(void) і тіло - воно обмежене фігурними дужками ().

int main(void)
{
тіло функції
}

У тіло функції ми і додаватимемо наш код. Перед ім'ям функції вказується тип значення, що повертається. Якщо функція не повертає значення – використовується ключове void.

int- це ціле 2-байтне число, діапазон значень від - 32768 до 32767

Після імені функції у дужках () вказуються параметри, які передаються функції під час її виклику. Якщо функція без параметрів – використовується ключове слово void. Функція mainмістить у собі набір команд, налаштування системи та головний цикл програми.

Далі налаштовуємо порт Dна вхід. Режим роботи порту визначається вмістом регістру DDRD(Регістр напряму передачі інформації). Записуємо в цей регістр число "0x00" (0b0000000 - у двійковому вигляді), крім кнопки до цього порту нічого не підключено, тому налаштовуємо весь порт D на вхід. Налаштувати порт порозрядно можна записавши в кожен біт регістра числа 0 або 1 (0-вхід, 1-вихід), наприклад DDRD = 0x81 (0b10000001) - перша та остання лінія порту D працюють на вихід, решта на вхід. Необхідно також підключити внутрішній резистор навантаження. Увімкненням та вимкненням внутрішніх резисторів керує регістр PORTx, якщо порт знаходиться в режимі введення. Запишемо туди одиниці.

Налаштовуємо порт Bна вихід. Режим роботи порту визначається вмістом регістру DDRB. Нічого, крім світлодіода до порту Bне підключено, тому весь порт можна налаштувати на вихід. Це робиться записом у регістр DDRBчисла "0xFF". Для того, щоб при першому включенні світлодіод не спалахнув запишемо в порт Bлогічні нулі. Це робиться записом PORTB= 0x00;

Для присвоєння значень використовується символ "=" і називається оператором присвоювання, не можна плутати зі знаком "рівно"

Налаштування портів виглядатиме так:

DDRD = 0x00;
PORTD = 0xFF;
DDRB = 0xFF;
PORTB = 0x00;

Пишемо основний цикл програми. while( " поки " з англ.) - ця команда організує цикл, багаторазово повторюючи тіло циклу до тих пір поки виконується умова, т. е поки вираз у дужках є істинним. У мові Сі прийнято вважати, що вираз істинний, якщо він не дорівнює нулю, і хибно, якщо дорівнює.

Команда виглядає так:

while (умова)
{
тіло циклу
}

У нашому випадку основний цикл складатиметься лише з однієї команди. Ця команда надає регістру PORTBзначення регістра, що інвертується PORTD.

PORTB = ~PIND; //Взяти значення з порту D, проінвертувати його і присвоїти PORTB (записати в PORTB)

// вирази мовою Сі читаються справа наліво

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

#include int main (void) (DDRD = 0x00; // порт D - вхід PORTD = 0xFF; // підключаємо навантажувальний резистор DDRB = 0xFF; // порт B - вихід PORTB = 0x00; // встановлюємо 0 на виході while (1) ( PORTB = ~PIND; //~ знак порозрядного інвертування)))

У мові Сі широко використовуються коментарі. Є два способи написання.

/*Коментар*/
//Коментар

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

Якщо використовуючи цю ж програму і підключити до мікроконтролера 8 кнопок і 8 світлодіодів, як показано на малюнку 2, то буде зрозуміло, що кожен біт порту Dвідповідає своєму биту порту B. Натискаючи кнопку SB1 – загоряється HL1, натискаючи кнопку SB2 – загоряється HL2 тощо.

Малюнок 2

У статті були використані матеріали із книги Бєлова А.В. "Самовчитель розробника пристроїв на AVR"




У МК ATMega16 є три таймери/лічильники - два 8-бітних (Timer/Counter0, Timer/Counter2) і один 16-бітний (Timer/Counter1). Кожен містить спеціальні регістри, одним із яких є лічильний регістр TCNTn (n – це число 0, 1 чи 2). Щоразу, коли процесор виконує одну команду, вміст цього регістру збільшується на одиницю (чи кожні 8, 64, 256 чи 1024 тактів). Тому він і називається рахунковим. Крім нього, є ще й регістр порівняння OCRn (Output Compare Register), до якого ми можемо самі записати якесь число. У 8-бітного лічильника ці регістри 8-бітні. У міру виконання програми вміст TCNTn зростає і в якийсь момент воно збігатиметься із вмістом OCRn. Тоді (якщо задані спеціальні параметри) в регістрі прапорів переривань TIFR (Timer/Counter Interrupt Flag Register) один з бітів дорівнює одиниці і процесор, бачачи запит на переривання, відразу ж відривається від виконання нескінченного циклу і йде обслуговувати переривання таймера. Після цього процес повторюється.

Нижче представлена ​​часова діаграма режиму CTC (Clear Timer on Compare). У цьому режимі рахунковий регістр очищається в момент збігу вмісту TCNTn і OCRn, відповідно змінюється період виклику переривання.

Це далеко не єдиний режим роботи таймера/лічильника. Можна не очищати лічильний регістр у момент збігу, тоді це буде режим генерації широтно-імпульсної модуляції, який ми розглянемо наступною статтею. Можна змінювати напрямок рахунку, т. е. вміст рахункового регістру буде зменшуватися у міру виконання програми. Також можливо робити рахунок не за кількістю виконаних процесором команд, а за кількістю змін рівня напруги на «ніжці» T0 або T1 (режим лічильника) можна автоматично, без участі процесора, змінювати стан ніжок OCn в залежності від стану таймера. Таймер/Лічильник1 вміє робити порівняння відразу двома каналами – А чи У.

Для запуску таймера потрібно виставити відповідні біти в регістрі керування таймером TCCRn (Timer/Counter Control Register), після чого він відразу ж починає свою роботу.

Ми розглянемо лише деякі режими роботи таймера. Якщо вам знадобиться робота в іншому режимі, то читайте Datasheet до ATMega16 – там все докладніше по-англійськи написано, дано навіть приклади програм на С та асемблері (недарма ж він займає 357 сторінок друкованого тексту!).

Тепер займемося кнопками.

Якщо ми збираємося використовувати невелику кількість кнопок (до 9 штук), підключати їх слід між «землею» і висновками будь-якого порту мікроконтролера. При цьому слід зробити ці висновки входами, для чого встановити відповідні біти в регістрі DDRx і включити внутрішній резистор підтягує установкою бітів в регістрі PORTx. При цьому на даних "ніжках" виявиться напруга 5 В. При натисканні кнопки вхід МК замикається на GND і напруга на ньому падає до нуля (а може бути навпаки - висновок МК замкнутий на землю в віджатому стані). При цьому змінюється регістр PINx, в якому зберігається стан порту (на відміну від PORTx, в якому встановлено стан порту за відсутності навантаження, тобто до натискання будь-яких кнопок). Періодично зчитуючи стан PINx, можна визначити, що натиснута кнопка.

УВАГА!Якщо відповідний біт у регістрі DDRx буде встановлений в 1 для вашої кнопки, то гарне натискання на кнопку може призвести до невеликого піротехнічного ефекту - диму навколо МК. Природно, МК після цього доведеться відправити у відро для сміття…

Перейдемо до практичної частини. Створіть в IAR новий робочий простір та новий проект з ім'ям, наприклад TimerButton. Встановіть опції проекту так, як описано в попередній статті. А тепер наберемо наступний невеликий код.

#include"iom16.h" void init_timer0( void) //Ініціалізація таймера/лічильника0(OCR0 = 255; //Вміст регістру порівняння //Задаємо режим роботи таймера TCCR0 = (1 void init_timer2( void) //Ініціалізація таймера/лічильника2(OCR2 = 255; TCCR2 = (1 //Встановлюємо для нього переривання збігу) void main ( void) ( DDRB = 255; init_timer0(); init_timer2(); while(1) { } } #pragma vector = TIMER2_COMP_vect // Переривання по таймеру2 __interrupt void flashing() ( if((PORTB & 3) == 1) ( PORTB &= (0xFF // Відключення висновків PB0, PB1 PORTB |= 2; // Включення PB1 } else( PORTB &= (0xFF // Відключення висновків PB0, PB1 PORTB |= 1; // Включення PB0 } }

Погляньмо, як це працює. У функціях init_timern задаються біти в регістрах TCCRn, OCRn і TIMSK, причому такий спосіб може комусь здатися дивним або незнайомим. Доведеться пояснити спочатку, що означає запис "(1

де a - це число, двійкове уявлення якого треба зрушити, а b показує, на скільки бітів потрібно його зрушити. При цьому можлива втрата значення, що зберігається в a (тобто не завжди можливо відновити те, що було в а). Розглянемо приклад:

Що виявиться в після виконання рядка C = (22

2 у двійковому коді буде виглядати як 00010110, а після зсуву вліво на 3 біти отримаємо С = 10110000.

Аналогічно існує і зсув праворуч. Ще приклад:

char C; … C = ((0xFF > 2);

Спочатку виконається дія у внутрішніх дужках (0xFF - це 255 в шістнадцятковому коді), з 11111111 вийде 111111100, потім відбудеться зрушення вправо і отримаємо С = 00111111. Як бачимо, тут дві взаємно до. два біти. Цього не сталося б, якби змінна була типу int, тому що int займає 16 біт.

Тепер розглянемо ще два бітові оператори, що широко застосовуються при програмуванні МК. Це оператор «побітове та» (&) та «побітове або» (|). Як вони діють, гадаю, буде зрозуміло з прикладів:

Дія: Результат (у двійковому коді): С = 0; // C = 00000000 C = (1 // C = 00100101 C | = (1 // C = 00101101 C & = (0xF0 >> 2)); // C = 00101100 C = (C і 4) | 3; // C = 00000111

Мало не забув! Є ще «побітове виключне або» (^). Воно порівнює відповідні біти в числі, і якщо вони однакові, повертає 0, інакше одиницю.

Повернімося до нашої програми. Там написано «(1

/* Timer/Counter 0 Control Register */ #define FOC0 7 #define WGM00 6 #define COM01 5 #define COM00 4 #define WGM01 3 #define CS02 2 #define CS01 1 #define CS00 0

При компіляції програми запис WGM01 просто замінюється на число 3, і в результаті виходить коректний запис. WGM01 називається макросом і він на відміну від змінної не займає місця в пам'яті (хіба що в пам'яті програміста:-).

Якщо заглянути тепер у Datasheet, але неважко буде побачити, що WGM01 це ім'я третього біта в регістрі TCCR0. Те саме стосується й інших бітів цього регістру. Цей збіг не випадково і відноситься до всіх регістрів МК (або майже до всіх). Т. е., написавши «(1

Разом, рядок

означає, що включений режим СТС, при спрацьовуванні таймера0 змінюється стан «ніжки» ОС0 (Вона ж PB3), вміст лічильника збільшується кожні 1024 такту.

Аналогічно для таймера2: TCCR2 = (1

У регістрі TIMSK (Timer/counter Interrupt MaSK register) задається режим переривань. Ми написали

що означає переривання таймера2 за збігом TCNT2 та OCR2. Остання функція – це власне функція переривання збігу таймера2. Переривання оголошуються наступним чином:

#pragma vector= ВЕКТОР __interruptТИП ІМ'Я()

де ВЕКТОР - це макрос вектора переривання (за змістом просто число, що характеризує це переривання); ці макроси у порядку зниження пріоритету перераховані у файлі iom16.h. ТИП – тип значення, що повертається функцією, в нашому випадку void (нічого). Ім'я – довільне ім'я цієї функції. З перериваннями ми ще встигнемо напрацюватись у майбутньому.

При виконанні нашої функції повинні по черзі моргати світлодіоди, підключені до PB0 та PB1. Зважаючи на все, частота дорівнює 11059200/(256*1024) = 42 Гц. Це швидко, але буде помітно неозброєним оком. До речі, застосування таймерів дає можливість відраховувати точні часові інтервали, що не залежать від складності вашої програми та порядку її виконання (але якщо у вас не більше одного переривання).

Отже, зберігаємо файл як "TimerDebug.c", додаємо його в проект, компілюємо, прошиваємо МК. Що ми бачимо? Світлодіод, підключений до виведення PB3, активно моргатиме, а на PB0 і PB1 немає жодних змін. У чому ж справа? Невже щось не так?

Щоб це з'ясувати, доведеться налагодити нашу програму. Оскільки в IAR немає Debugger, доведеться використовувати AVR Studio. Це середовище розробки можна завантажити з сайту виробника http://atmel.com. Проблем із її встановленням, гадаю, не повинно бути. Перед запуском AVR Studio виберіть у IAR режим Debug і створіть налагоджувальний cof-файл (всі опції проекту мають бути виставлені, як описано в попередній статті).

Відкривши AVR Studio, ми побачимо вікно привітання, в якому виберемо Open. Тепер ліземо в папку з проектом, там Debug\Exe, вибираємо там «TimerDebug.cof», створюємо проект там, де запропонують, вибираємо дивайс ATMega16 і режим налагодження Simulator. Після цього, якщо все зробили правильно, відразу йде процес налагодження

Середовище налагодження тут дуже зручне, т.к. дозволяє переглядати вміст всіх регістрів МК, і навіть вручну встановлювати значення їм клацаннями миші. Наприклад, якщо встановити прапор переривання у регістрі TIFR у біті 7 (під чорним квадратом у TIMSK), то наступним кроком програми (натискання F10 або F11) має бути обробка переривання (прапор буде встановлений автоматично і при збігу регістрів TCNT2 та OCR2). Але, на наш подив, переривання не буде!

Постає питання: чому?

Відкриємо регістр CPU, SREG. Цей регістр визначає роботу процесора, саме сьомий його біт (I-біт, Interrupt bit) відповідальний за обробку всіх переривань в МК. У нас його не встановлено. Варто його виставити, як відразу піде виконуватися переривання (якщо одночасно встановлено сьомий біт в TIFR).

Можна помітити одну цікаву особливість: як тільки процесор йде в обробку переривання, цей біт (прапор роздільної здатності обробки зривань) знімається, а при виході з функції переривання знову автоматично встановлюється. Це не дозволяє процесору, не виконавши одного переривання, схопитися за інше (адже він орієнтується в програмі саме таким чином - по прапорах).

Отже, необхідно додати рядок коду установки цього біта в одиничний стан. Додамо ми його до функції init_timer2. Вийде наступне:

void init_timer2( void) (SREG |= (1 //Додали цей рядок OCR2 = 255; TCCR2 = (1

Тепер, обравши конфігурацію Release і прошивши МК натисканням F7 та запуском AVReal32.exe, з радістю побачимо, що все працює як слід.

Примітка:при налагодженні програми слід зменшувати інтервали таймерів, якщо вони занадто довгі, тому що в процесі налагодження в AVR Studio програма виконується в тисячі разів повільніше, ніж усередині МК, і ви не дочекаєтесь спрацювання таймера. Загалом налагодження повністю аналогічне такої в інших системах програмування, таких, як Visual C++.

Тепер, навчившись налагоджувати програми, створимо в IAR новий файл (а старий збережемо та видалимо з проекту) і наберемо наступний код:

#include"iom16.h" long unsigned int counter = 0; //Лічильник для формування часових інтервалів unsigned char B0Pressed = 0; //Тут зберігається стан кнопки0 (0 - не натиснута, 1 - натиснута) unsigned char B1Pressed = 0; //Тут зберігається стан кнопки1 (0 - не натиснута, 1 - натиснута) //Ініціалізація таймера2 //Потрібно кожні 11059 тактів (1 мс) збільшувати counter. У нас виходить кожні 1,001175 мс void init_timer2() ( OCR2 = 173; TCCR2 = (1 //Ініціалізація портів введення/виводу init_io_ports() ( DDRA =(1//формування затримки в Pause_ms мілісекунд) void delay( long unsigned int Pause_ms) (counter = 0; while(counter void main() ( SREG |= (1 // Дозволяємо переривання init_timer2()); //Включаємо таймер2 на кожні 64 такти, рахувати до 173 init_io_ports(); //Включаємо порти введення/виводу while(1) { //Обробка кнопки 0 if(B0Pressed == 1) { // уповідує PORTB, чекає відпускання PORTB++; B0Pressed = 0; while((PINC & (1 else ( if((PINC & (1 //Фіксує натискання ( delay(50)); if((PINC & (1 // Перевіряє натискання ( B0Pressed = 1;)) } } } //Обробка кнопки 1 if(B1Pressed == 1) //Якщо відбулося натискання на кнопку, { // зменшує PORTB, чекає на відпускання PORTB--; B1Pressed = 0; while((PINC & (1 else ( if((PINC & (1 //Фіксує натискання ( delay(200)); //Усунення "брязкоту клавіш" if((PINC & (1 // Перевіряє натискання ( B1Pressed = 1;)) //Встановлює прапор "кнопка натиснута" } } } } } // Переривання по таймеру 2, при цьому збільшення лічильника counter #pragma vector= TIMER2_COMP_vect __interrupt void inc_delay_counter() ( counter++; )

Спочатку пропоную взяти готовий файл прошивки (файли до статті, папка Release, файл TimerButton.hex або відкомпілювати цей текст) і записати його в МК. Після цього вийняти кабель прошивки, підключити до PC0 і PC1 кнопки і спробувати натискати їх. Побачимо, що при натисканні на одну з кнопок збільшується регістр PORTB (загоряються світлодіоди), а при натисканні на іншу зменшується. Якщо не працює – спробуйте натискати одну кнопку, утримуючи іншу – діятиме. Справа в тому, що я підключав кнопки наступним чином: при натисканні на кнопку висновок МК «бовтається» у повітрі, а при відпусканні замикається на землю. Якщо ви підключили кнопки по-іншому, доведеться лише трохи модернізувати програму.

Давайте розберемося з кодом. Тут роботу з таймером організовано трохи інакше. Він спрацьовує кожні 11072 такти (тобто кожні 1,001175 мс) і збільшує вміст змінної counter. Є ще функція delay(long unsigned int Pause_ms), яка бере як параметр кількість мілісекунд Pause_ms, скидає counter і чекає, коли counter досягне значення Pause_ms, після чого продовжує роботу МК. Таким чином, написавши delay(1500), ми сформуємо затримку у програмі в 1,5 секунди. Це дуже зручно для формування часових інтервалів.

З таймером начебто все зрозуміло. Але навіщо він використовується? Розглянемо нескінченний цикл while(1) у main(). У цьому циклі перевіряється стан кнопок шляхом аналізу вмісту регістру PINB. А навіщо там вартує затримка на 50 мс? Це усунення т. зв. «брязкіт клавіш». Справа в тому, що при натисканні на кнопку відбувається удар одного контакту про інший, і, оскільки металеві контакти, удар цей пружний. Контакти, пружиня, замикаються та розмикаються кілька разів, незважаючи на те, що палець зробив лише одне натискання. Це призводить до того, що МК фіксує кілька натискань. Розгляньмо графік залежності напруги на виході PC0 від часу. Він може виглядати так:

Точка А – момент натискання кнопки. Він може бути зафіксований МК. Потім йдуть кілька замикань та розмикань (їх може і не бути, а може бути і 12 штук – це явище можна вважати випадковим). У точці B контакт вже надійно зафіксовано. Між A та B в середньому близько 10 мс. Нарешті, у точці D відбувається розмикання. Як же позбутися цього неприємного явища? Виявляється дуже просто. Потрібно зафіксувати момент натискання кнопки (точка А), через якийсь час, наприклад, 50 мс (точка С) перевірити, що кнопка дійсно натиснута, зробити дію, що відповідає цій кнопці та чекати на момент її відпускання (точка D). Тобто потрібно зробити паузу від А до С, таку, щоб весь «брязкіт» виявився всередині цієї паузи. А спробуйте тепер усунути рядок, що формує затримку, відкомпілювати програму і зашити її в МК. Шляхом простих натискань на кнопки зможете легко переконатися, що всі ці муки не були марними.

А що робити, якщо до МК потрібно підключити, скажімо, 40 кнопок? Адже в нього лише 32 висновки. Здавалося б, аж ніяк. Насправді, це можливо. У разі використовують алгоритм, званий стробированием. Для цього потрібно кнопки з'єднати у вигляді матриці, як це показано на малюнку (малюнок взятий з книги Мортона "МК AVR, вступний курс", де написано про програмування AVR на асемблері).

При подачі висновку PB0 лог. 1 (+5В), а на висновки PB1 та PB2 лог. 0 дозволяється обробка кнопок 1, 4 і 7. Після цього стан кожної з них можна дізнатися, перевіривши напругу одному з висновків PB3..PB5. Таким чином, подаючи послідовно висновки PB0..PB2 лог. 1, можна визначити стан усіх кнопок. Зрозуміло, що висновки PB0..PB2 мають бути виходами, а PB0..PB2 входами. Щоб визначити, яка кількість висновків знадобиться для масиву з кнопок Х, потрібно знайти пару співмножників Х, сума яких найменша (для нашого випадку з 40 кнопками це будуть числа 5 і 8). Це означає, що до одного МК можна підключити до 256 кнопок (а із застосування дешифраторів і того більше, але про дешифраторів потім). Краще зробити менше висновків виходами, а більше – входами. В цьому випадку опитування всіх рядків матриці займе менше часу. Подібний спосіб підключення (стробування) властивий не лише кнопкам. Там можна підключати різноманітні пристрої, починаючи від матриць світлодіодів і закінчуючи мікросхемами flash-пам'яті.

© Кисельов Роман
Червень 2007