ОБРАБОТЧИКИ НАЖАТИЯ КНОПОК ДЛЯ МИКРОКОНТРОЛЛЕРОВ ATMEGA

 

Atmega DIP 1 Atmega square 1

 

UNO PINOUT 1

В этой статье рассмотрим несколько вариантов нажатия кнопок.

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) в случае, если внешний сигнал не подключён.

Схема подключения кнопки без внешней подтяжки такая: 

Simple Arduino Button Pull Up mode

Здесь подразумевается, то у нас настроен Pull-UP резистор,  то есть, у нас есть подтяжка питания к пину PD2 (на картинке, а в нашем примере будет PB3). Этот резистор встроен в сам микроконтроллер и подключен к VCC. Таким образом на PD2 (или PB2, как у нас деалее) по умолчанию будет  выскокий логический уровень, т. е., близко к 5 Вольтам. KNC - это кнопка, которая замыкает цепь на GND. То есть, когда мы ее нажимаем, то напряжение на PD2 (или на PB2, как у нас) падает до 0 Вольт. Данное событие интерпретируется как нажатие. Еще разок:

PORTB |= (1 << PB3);    // Включение внутренней подтяжки к VCC
// ИЛИ
PORTB &= ~(1 << PB3);   // Отключение подтяжки

Схема подключения с внешней подтяжкой от линии VCC:

KNC circuit 1

Тут мы видем уже вполне православную схему, потому что в ней уже есть все что надо: подтяжка через 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