В предыдущем примере мы помигали светодиодом на ATtiny13. Теперь напишем программу, которая будет включать/выключать светодиод по нажатию кнопки. Все опыты я провожу на своей отладочной плате, соответственно код буду приводить применительно к ней.
В принципе, можно в теле самой программы проверять, была ли нажата кнопка, и по нажатию кнопки выполнять определённое действие. Однако, в этом случае фоновая программа будет постоянно прерываться и значительная часть процессорного времени будет тратиться на проверку состояния кнопки, а на практике это не всегда допустимо. Поэтому, рассмотрим сразу другой вариант: с использованием внешних прерываний.
В микроконтроллере ATtiny13 есть два типа внешних прерваний: аппаратное (INT0, на ножке INT0) и программное (PCINT0, на ножках PCINT0-5, которое можно разрешить по маске только на нужных ножках).
INT0 может работать в четырёх рехимах:
- По низкому уровню на ножке;
- По изменению уровня на ножке;
- По спадающему фронту;
- По нарастающему фронту.
PCINT0 может работать только по изменению уровня на любой из ножек, настроенных по маске.
Так как ножка INT0 (она же PB1) у нас занята светодиодом*, а кнопки у нас подключены к ножкам PCINT3, PCINT4 (PB3 и PB4 соответственно), будем использовать программное прерывание.
/*
* tiny13_board_switch
* Демо-прошивка отладочной платы на ATtiny13.
* Включаем/выключаем светодиод по внешнему прерыванию
* от нажатия кнопки.
*/
#define F_CPU 1200000UL
#define LED PB2
#define BUTTON1 PB3 // PCINT3
#define BUTTON2 PB4 // PCINT4
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
// Обработчик прерывания PCINT0
ISR(PCINT0_vect)
{
_delay_ms (50); // антидребезг (использовать задержки в прерываниях некошерно, но пока и так сойдёт)
if ( (PINB & (1<<BUTTON1)) == 0 || (PINB & (1<<BUTTON2)) == 0 ) // если нажата одна из кнопок
{
PORTB ^= (1<<LED); //переключаем состояние светодиода (вкл./выкл.)
while ( (PINB & (1<<BUTTON1)) == 0 || (PINB & (1<<BUTTON2)) == 0 ) {} // ждём отпускания кнопки
}
}
int main(void)
{
// Пины кнопок
DDRB &= ~((1<<BUTTON1)|(1<<BUTTON2)); // входы
PORTB |= (1<<BUTTON1)|(1<<BUTTON2); // подтянуты
// Пин светодиода
DDRB |= (1<<LED); // выход
PORTB &= ~(1<<LED); // выключен
// Настройка прерываний
GIMSK |= (1<<PCIE); // Разрешаем внешние прерывания PCINT0.
PCMSK |= (1<<BUTTON1)|(1<<BUTTON2); // Разрешаем по маске прерывания на ногак кнопок (PCINT3, PCINT4)
sei(); // Разрешаем прерывания глобально: SREG |= (1<<SREG_I)
while (1)
{
}
}
В начале программы мы указали тактовую частоту процессора, указали к каким ножкам у нас подключены кнопки и светодиод, которым мы хотим управлять.
Затем, подключили необходимые заголовочные файлы. Тут, помимо прочего, мы подключили библиотеку avr/interrupt.h, которая отвечает за работу прерываний.
Функция ISR(PCINT0_vect) — это обработчик прерывания PCINT0. (вектор соответствующего прерывания указывается как аргумент, в скобках). Данная функция будет выполняться каждый раз при срабатывании прерывания PCINT0.
Дальше у нас идёт основное тело программы: функция main(). В ней мы первым делом настраиваем пины ввода/вывода:
Пины, к которым подключены кнопки, сконфигурируем как входы:
DDRB &= ~((1<<BUTTON1)|(1<<BUTTON2));
И устанавливаем на входах по умолчанию высокий логический уровень:
PORTB |= (1<<BUTTON1)|(1<<BUTTON2);
Этим самым мы подключаем ко входам встроенные в микроконтроллер подтягивающие резисторы, то есть подтягиваем входы к питающему напряжению. При нажатии на управляющую кнопку, соответствующий вход будет притягиваться к общему проводу (GND), то есть на него будет подан низкий логический уровень.
Пин, к которому подключен светодиод, сконфигурируем как выход и по умолчанию светодиод погасим:
DDRB |= (1<<LED);
PORTB &= ~(1<<LED);
Затем настраиваем прерывания:
Разрешим прерывание PCINT0. За это отвечает бит PCIE регистра GIMSK. Поэтому записываем в него единицу:
GIMSK |= (1<<PCIE);
Установкой битов PCINT0-5 регистра PCMSK можно задать, при изменении состояния каких входов будет срабатывать прерывание PCINT0:
PCMSK |= (1<<BUTTON1)|(1<<BUTTON2);
И, наконец, мы должны разрешить прерывания глобально. Это делается установкой флага SREG_I в регистре SREG:
SREG |= (1<<SREG_I);
Или можно использовать макрос, который подставит ассемблерную команду SEI:
sei();
Далее, мы видим, что бесконечный цикл у нас пустой, то есть в фоне никаких задач не выполняется, т.к. нашу задачу включения-выключения светодиода мы реализовали на прерываниях.
Прерывания у нас будут обрабатываться следующим образом:
При нажатии на любую из управляющих кнопок срабатывает прерывание PCINT0 и вызывается функция-обработчик прерывания ISR(PCINT0_vect).
В теле обработчика будет происходить следующее.
Чтобы отфильтровать дребезг контактов кнопки, выдерживаем паузу 50 мс **. Это нужно для того, чтобы прерывание не сработало несколько раз подряд при однократном нажатии.
Т.к. прерывание PCINT0 наступает при любом изменении логического уровня на входе, то нужно в обработчике проверять, была ли кнопка нажата, либо отпущена. Если любая из кнопок нажата, то выполняем нужное действие (инвертируем состояние выхода, к которому подключен светодиод):
PORTB ^= (1<<LED);
После этого ожидаем, пока обе кнопки не будут отпущены**:
while ( (PINB & (1<<BUTTON1)) == 0 || (PINB & (1<<BUTTON2)) == 0 ) {}
Если же обе кнопки отпущены, то обработчик, не выполняя никаких действий, будет возвращать МК к выполнению основного цикла (ожиданию). Таким образом, прерывание будет однократно срабатывать только при нажатии на кнопку.
* Несмотря на то, что к ножке INT0 на моей плате подключен светодиод, ничто не мешает снять его джампер и джампер любой кнопки, после чего подключить кнопку к ножке INT0 проводом-перемычкой. При таком подключении можно будет использовать аппаратное прерывание INT0 по нажатию кнопки.
** Обработчики прерываний должны выполняться как можно быстрее. Использовать задержки в прерываниях не рекомендуется и считается дурным тоном, т.к. при этом приостанавливается выполнение фоновых задач. То же самое касается и ожидания отпускания кнопки в нашем примере! Но, поскольку это всего лишь простой пример и в фоне у нас ничего не выполняется, то в данном случае можно оставить и так. Вообще, борьба с дребезгом контактов — это целая отдельная тема и вариантов тут может быть масса, как программных, так и аппаратных… Но пока, для данного конкретного случая, программно мне удалось добиться наиболее чёткого однократного срабатывания на нажатие кнопки именно способом, описанным выше: с помощью задержки и последующего ожидания отпускания…
Полезные ссылки:
ATtiny13 Datasheet (официальный)
ATtiny13 Datasheet (урезанный вариант)
EasyElectronics — Программирование на Си. Часть 3: Прерывания
Сергей говорит:
«Данная функция будет выполняться каждый раз при срабатывании прерывания PCINT0.» В других восьми источниках не попадались таки обозначения такого прерывания.
OSBoy говорит:
Не знаю, какие источники имеются ввиду, но в datasheet на attiny13 есть чёрным по белому: «PCINT0: Pin Change Interrupt 0».