ОБРАБОТЧИКИ НАЖАТИЯ КНОПОК ДЛЯ МИКРОКОНТРОЛЛЕРОВ ATMEGA
![]() |
![]() |
В этой статье рассмотрим несколько вариантов нажатия кнопок.
Cразу ометим, что пины микроконтроллеров семейства Атмега представлены масками по 8 бит на каждый порт, а в целом каждый порт - это байт.
Далее для тестов нам сначала надо инициировать пин диода как выход:
DDRB |= (1 << PB0);
А кнопочку как вход:
DDRB &= ~(1 << PB3);
Строка PORTB |= (1 << PB3) работает с регистром PORTB микроконтроллера AVR (например, ATmega) и выполняет следующее:
- 1 << PB3: Сдвигает бит 1 на позицию, указанную в PB3 (например, если PB3 = 3, то это 0b00001000).
- PORTB |= ...: Выполняет побитовое ИЛИ с текущим значением регистра PORTB, устанавливая бит PB3 в 1, не затрагивая остальные биты.
Контекст и эффект:
- Если пин PB3 настроен как выход (в регистре DDRB бит PB3 = 1), эта операция устанавливает на пине PB3 высокий уровень напряжения (логическая 1, обычно VCC).
- Если пин PB3 настроен как вход (в регистре DDRB бит PB3 = 0), эта операция включает внутренний подтягивающий резистор для пина PB3, подтягивая его к VCC (логическая 1) в случае, если внешний сигнал не подключён.
Схема подключения кнопки без внешней подтяжки такая:
Здесь подразумевается, то у нас настроен Pull-UP резистор, то есть, у нас есть подтяжка питания к пину PD2 (на картинке, а в нашем примере будет PB3). Этот резистор встроен в сам микроконтроллер и подключен к VCC. Таким образом на PD2 (или PB2, как у нас деалее) по умолчанию будет выскокий логический уровень, т. е., близко к 5 Вольтам. KNC - это кнопка, которая замыкает цепь на GND. То есть, когда мы ее нажимаем, то напряжение на PD2 (или на PB2, как у нас) падает до 0 Вольт. Данное событие интерпретируется как нажатие. Еще разок:
PORTB |= (1 << PB3); // Включение внутренней подтяжки к VCC // ИЛИ PORTB &= ~(1 << PB3); // Отключение подтяжки
Схема подключения с внешней подтяжкой от линии VCC:
Тут мы видем уже вполне православную схему, потому что в ней уже есть все что надо: подтяжка через R1, защитный резистор R2, ограничивающий ток на ногу микроконтроллера далее по схеме и какой-нибудь Зенер D1 на выбор. Диод Зенера выравнивает напряжение до определенного номинала в небольших пределах, однако он не рассчитан на большие скачки; если на линии происходит скочкообразное превышение напряжения с большой амплитудой, то лучше использовать диоды типа TVS. Об этом подробней В ДРУГОЙ СТАТЬЕ.
Так, теперь в бой! Это самая тупорылая кнопка в основном цыкле, вообще без затей, в том числе без внешней подтяжки, используем только встроенную в микроконтроллер, по первой схеме:
#include <avr/io.h> #include <util/delay.h> int main(void) { // Настройка пинов DDRB |= (1 << PB0); // PB0 как выход (светодиод) DDRB &= ~(1 << PB3); // PB3 как вход (кнопка) PORTB |= (1 << PB3); // Включаем внутреннюю подтяжку while (1) { // Если кнопка нажата (PB3 = 0, т.к. кнопка подтянута к VCC и замыкает на GND) if (!(PINB & (1 << PB3))) { PORTB |= (1 << PB0); // Включаем светодиод _delay_ms(20); // Задержка для антидребезга } else { PORTB &= ~(1 << PB0); // Выключаем светодиод } } }
Теперь вариант в режиме "toggle". "toggle" - это когда одно нажатие инвертирует состояние диода, сохраняя его новое состояние до следующего нажатия кнопки (или иного триггера). Тут мы переключаем режим вкл/выкл путем инвертировани состояния пина:
меточка
/* * ПРОСТЕЙШИЙ ПРИМЕР, СПЕЦИАЛЬНО АДАПТИРОВАННЫЙ ДЛЯ САМЫХ МАЛЕНЬКИХ И ТУПЫХ * Кнопка на PB3 включает/выключает светодиод на PB0 * Для микроконтроллеров AVR (ATmega328P и аналоги) */ // Подключаем необходимые библиотеки #include <avr/io.h> // Для работы с регистрами микроконтроллера #include <util/delay.h> // Для функций задержки int main(void) { /***************************************************** * НАСТРОЙКА ПИНОВ МИКРОКОНТРОЛЛЕРА *****************************************************/ // 1. Настраиваем PB0 как выход (к нему подключен светодиод) // DDRB - регистр направления порта B (Data Direction Register) // |= (1 << PB0) - устанавливаем только бит PB0 в 1, остальные не трогаем DDRB |= (1 << PB0); // 2. Настраиваем PB3 как вход (к нему подключена кнопка) // &= ~(1 << PB3) - сбрасываем только бит PB3 в 0 (делаем входом) DDRB &= ~(1 << PB3); // 3. Включаем внутренний подтягивающий резистор для PB3 // Это нужно, чтобы при разомкнутой кнопке на пине был четкий HIGH // Без этого пин будет "плавать" и давать случайные значения PORTB |= (1 << PB3); /***************************************************** * ПЕРЕМЕННЫЕ ДЛЯ РАБОТЫ ПРОГРАММЫ *****************************************************/ // Флаг, который запоминает, что кнопка была нажата // Нужен для обработки только одного нажатия (а не множественных) uint8_t button_was_pressed = 0; // Переменная для хранения состояния светодиода (можно и без нее) uint8_t led_state = 0; /***************************************************** * ОСНОВНОЙ ЦИКЛ ПРОГРАММЫ *****************************************************/ while (1) { // 1. Проверяем, нажата ли кнопка // PINB - регистр для чтения состояния пинов порта B // (1 << PB3) - создаем маску для бита PB3 // !(PINB & (1 << PB3)) - проверяем, равен ли PB3 0 (нажатие) if (!(PINB & (1 << PB3))) { // 2. Если кнопка нажата И ранее не была нажата if (!button_was_pressed) { // 3. Меняем состояние светодиода (toggle) // PORTB ^= (1 << PB0) - инвертируем только бит PB0 PORTB ^= (1 << PB0); // 4. Запоминаем, что кнопка была нажата button_was_pressed = 1; // 5. Задержка для защиты от дребезга контактов // (кнопка физически "дребезжит" при нажатии) _delay_ms(50); } } else { // 6. Если кнопка отпущена - сбрасываем флаг button_was_pressed = 0; } // Небольшая задержка для стабильности работы _delay_ms(10); } }
В этом примере добавлены задержки, вроде _delay_ms(); Здесь это Ок. Но на будущее мы должны запомнить и понять, что таких задержек мы должны избегать, так как каждая миллесекунда подобной задержки - это хуева туча пропущенных тактов, которые можно было бы использовать с пользой для дела.
Еще один пример обработчика кнопки с дефайнами портов. Такой прием полезен при создании большой программы, которую, вероятно, потом придется использовать на устройстве с другой топологией. Имеется в виду тот факт, что определить порты в "шапке" один раз гораздо проще, чем переделывать это везде, по всей программе.
#include <avr/io.h> #include <util/delay.h> // Для задержек // Определение пина для удобства #define BUTTON_PIN PIND2 #define BUTTON_PORT PORTD #define BUTTON_DDR DDRD #define BUTTON_INPUT_REG PIND // Регистр для чтения состояния пина int main(void) { // 1. Настройка пина как ВХОД // Устанавливаем соответствующий бит в регистре DDRx в 0 BUTTON_DDR &= ~(1 << BUTTON_PIN); // 2. Отключение внутреннего Pull-Up резистора для этого пина (по умолчанию он выключен, но хорошая практика явно его отключить) // Устанавливаем соответствующий бит в регистре PORTx в 0, когда пин настроен как вход BUTTON_PORT &= ~(1 << BUTTON_PIN); // Настройка порта B (например, для светодиода, чтобы видеть результат) DDRB |= (1 << PB0); // PB0 как выход для светодиода while (1) { // Чтение состояния пина // Если кнопка нажата, пин будет HIGH if (BUTTON_INPUT_REG & (1 << BUTTON_PIN)) { // Кнопка нажата (пин HIGH) PORTB |= (1 << PB0); // Включаем светодиод } else { // Кнопка отпущена (пин LOW благодаря внешнему pull-down) PORTB &= ~(1 << PB0); // Выключаем светодиод } _delay_ms(50); // Небольшая задержка для стабильности или антидребезга } }
Далее рассмотрим примеры, близкие к боевым, то есть, к рабочим. Тут уже используются таймеры, прерывания и всякие такие прелести.
1 - это несколько коротких и 2 варианта длинных нажатий с обработкой по таймеру. Здесь у нас везде будут использоваться прерывания по аппаратному таймеру. Итак, поехали:
1. Короткие нажатия.
#define F_CPU 16000000UL #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <stdlib.h> #include <stdbool.h> #define Reset PD3 //Кнопочка #define BAUD 9600 //Скорость UART #define UBRR_VALUE ((F_CPU / (16UL * BAUD)) - 1) volatile uint16_t interval = 0; bool buttonFlagReset = false; //Флаги обработки кнопок bool buttonPressed = false; bool buttonReleased = false; volatile uint16_t iteration = 0; // Количество повторных нажатий кнопки char iterationStr[10]; // Массив для перевода количества итераций в строку для передачи по UART void uart_init() { //Инициация функции UART UBRR0H = (uint8_t)(UBRR_VALUE >> 8); UBRR0L = (uint8_t)UBRR_VALUE; UCSR0B = (1 << TXEN0); // Включаем только передачу (TX) UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // Устанавливаем 8 бит данных } //Отправка символа по UART. На самом деле, это пример говнокода. Здесь такое фактически не нужно. void uart_send_char(char UARTmessage) { while (!(UCSR0A & (1 << UDRE0))); UDR0 = UARTmessage; } //Здесь отправка стринга по UART. А вот это норм. void uart_send_string(const char* UARTmessage) { for (size_t i = 0; UARTmessage[i] != '\0'; ++i) { uart_send_char(UARTmessage[i]); } } //Функция реакции на нажатия. void actOnButton(uint8_t count) { switch (count) { case 1: uart_send_string("Act on Button - Iteration 1\r\n"); // Ваш код для первой итерации break; case 2: uart_send_string("Act on Button - Iteration 2\r\n"); // Ваш код для второй итерации break; case 3: uart_send_string("Act on Button - Iteration 3\r\n"); // Ваш код для второй итерации break; case 4: uart_send_string("Act on Button - Iteration 4\r\n"); // Ваш код для второй итерации break; case 5: uart_send_string("Act on Button - Iteration 5\r\n"); // Ваш код для второй итерации break; // Добавьте нужные вам варианты здесь default: uart_send_string("Act on Button - Default\r\n"); // Действие по умолчанию break; } } //Функция обработки нажатий. Тут мы выясняем как именно была нажата кнопочка. void buttonCheck() { sei(); // Кнопка нажата if (PIND & (1 << Reset)) { if (!buttonPressed) { buttonPressed = true; buttonReleased = false; interval = 0; //uart_send_string("Reset button clicked\r\n"); iteration++; utoa(iteration, iterationStr, 10); uart_send_string("Iteration "); uart_send_string(iterationStr); uart_send_string("\r\n"); } } else { // Кнопка отпущена if (buttonPressed) { buttonReleased = true; buttonPressed = false; } } // Если кнопка была отпущена и прошло более 350 миллисекунд, сбрасываем счетчик и вызываем actOnButton if (buttonReleased && (interval > 350)) { buttonReleased = false; // Если интервал больше 350 миллисекунд и счетчик не равен 0, сбрасываем его if (iteration != 0) { utoa(iteration, iterationStr, 10); // Преобразование uint16_t в строку uart_send_string("Iteration count Reset\r\n"); uart_send_string("Starting action "); uart_send_string(iterationStr); uart_send_string("\r\n"); actOnButton(iteration); iteration = 0; } } } //Инициации прерываний, в данном случае это IMER1_COMPA_vect. Таймер тикает с интервалом в одну миллисекунду. void setupInterrupts() { TCCR1A = 0; TCCR1B = 0; OCR1A = 15; TCCR1B |= (1 << WGM12); TCCR1B |= (1 << CS10); TCCR1B |= (1 << CS12); TIMSK1 |= (1 << OCIE1A); sei(); } //Точка входа в программу int main(void) { setupInterrupts(); uart_init(); uart_send_string("Controller is operational!\r\n"); DDRD |= (1 << Reset); while (1) { buttonCheck(); } } //Обработчик прерываний. Здесь тупо прибавляем 1 каждую миллисекунду. ISR(TIMER1_COMPA_vect) { interval++; }
3. Обработчик долгих нажатий Б) Здесь мы инкрементируем значение переменной num в зависимости от продолжительности нажатия кнопки.
#define F_CPU 16000000UL #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <stdlib.h> #include <stdbool.h> #define KNC PD2 #define Dwn PB2 #define Up PB1 #define BAUD 9600 #define UBRR_VALUE ((F_CPU / (16UL * BAUD)) - 1) volatile uint16_t milliseconds1 = 0; volatile uint16_t milliseconds2 = 0; volatile uint16_t num = 0; char numStr[6]; bool buttonFlagUp = false; bool buttonFlagDwn = false; void uart_init() { UBRR0H = (uint8_t)(UBRR_VALUE >> 8); UBRR0L = (uint8_t)UBRR_VALUE; UCSR0B = (1 << TXEN0); // Включаем только передачу (TX) UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // Устанавливаем 8 бит данных } void uart_send_char(char UARTmessage) { while (!(UCSR0A & (1 << UDRE0))); UDR0 = UARTmessage; } void uart_send_string(const char* UARTmessage) { for (size_t i = 0; UARTmessage[i] != '\0'; ++i) { uart_send_char(UARTmessage[i]); } } void buttonCheck(){ sei(); //Сначала убавляем if (PINB & (1 << Dwn) && (buttonFlagDwn == false)) { buttonFlagDwn = true; milliseconds1 = 0; if(milliseconds1 < 300){ num--; utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("num = "); uart_send_string(numStr); uart_send_string("\r\n"); } } if((PINB & (1 << Dwn)) && (buttonFlagDwn == true)){ if((milliseconds1 >= 1000) && (milliseconds1 < 3000)){ num -= 10; utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("num = "); uart_send_string(numStr); uart_send_string("\r\n"); } } if((PINB & (1 << Dwn)) && (buttonFlagDwn == true)){ if((milliseconds1 >= 3000)){ num -= 100; utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("num = "); uart_send_string(numStr); uart_send_string("\r\n"); } } if(!(PINB & (1 << Dwn)) && (buttonFlagDwn == true)){ buttonFlagDwn = false; } //Теперь прибавляем if (PINB & (1 << Up) && (buttonFlagUp == false)) { buttonFlagUp = true; milliseconds2 = 0; if(milliseconds2 < 300){ num++; utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("num = "); uart_send_string(numStr); uart_send_string("\r\n"); } } if((PINB & (1 << Up)) && (buttonFlagUp == true)){ if((milliseconds2 >= 1000) && (milliseconds2 < 3000)){ num += 10; utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("num = "); uart_send_string(numStr); uart_send_string("\r\n"); } } if((PINB & (1 << Up)) && (buttonFlagUp == true)){ if((milliseconds2 >= 3000)){ num += 100; utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("num = "); uart_send_string(numStr); uart_send_string("\r\n"); } } if(!(PINB & (1 << Up)) && (buttonFlagUp == true)){ buttonFlagUp = false; } } void setupInterrupts() { TCCR1A = 0; TCCR1B = 0; OCR1A = 15; TCCR1B |= (1 << WGM12); TCCR1B |= (1 << CS10); TCCR1B |= (1 << CS12); TIMSK1 |= (1 << OCIE1A); sei(); } int main(void) { setupInterrupts(); uart_init(); num = 9999; uart_send_string("Controller is operational!\r\n"); utoa(num, numStr, 10); // Преобразование uint16_t в строку uart_send_string("Num: "); uart_send_string(numStr); uart_send_string("\r\n"); DDRB |= (1 << Dwn) | (1 << Up); while(1) { buttonCheck(); } } ISR(TIMER1_COMPA_vect) { milliseconds1++; milliseconds2++; }
Ну и все, пожалуй. Хорошая получилась статься, я собой доволен.
Исходники:
buttonCheck1.cpp
buttonCheck2.cpp
buttonCheck3.cpp