В предыдущем примере мы включали/выключали светодиод на ATtiny13 по нажатию кнопки. Теперь усложним задачу и напишем программу-генератор «бегущего огня». Нажатием кнопки будем менять направление бегущего огня. Все опыты я провожу на своей отладочной плате, соответственно код буду приводить применительно к ней. Светодиоды на моей плате подключены к выходам PB0-PB3.
/*
* tiny13_board_running_light
* Демо-прошивка отладочной платы на ATtiny13.
* Меняем направление бегущего огонька
* по внешнему прерыванию от нажатия кнопки.
*/
#define F_CPU 1200000UL
#define LED0 PB0
#define LED1 PB1
#define LED2 PB2
#define LED3 PB3
#define BUTTON PB4 // PCINT4
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
volatile _Bool direction = 0; // Направление бегущего огонька
// Обработчик прерывания PCINT0
ISR(PCINT0_vect)
{
_delay_us(50); // Антидребезг (использовать задержки в прерываниях некошерно, но пока и так сойдёт)
if ( (PINB & (1<<BUTTON)) == 0 ) // Если кнопка нажата
{
direction = !direction; // Меняем направление
while ( (PINB & (1<<BUTTON)) == 0 ) {} // Ждём отпускания кнопки
}
}
ISR(TIM0_COMPA_vect)
{
static char light = 1;
if (!direction) // прямое направление
{
if (light < 0x08) light <<= 1;
else light = 0x01;
}
else // обратное направление
{
if (light > 0x01) light >>= 1;
else light = 0x08;
}
PORTB = (PORTB & 0xF0) | light; // Подставляем в младшие 4 бита порта "B" значение переменной light
}
int main(void)
{
// Кнопка
DDRB &= ~(1<<BUTTON); // вход = 0
PORTB |= (1<<BUTTON); // подтянут = 1
// Светодиоды 0-3
DDRB |= (1<<LED0)|(1<<LED1)|(1<<LED2)|(1<<LED3); // выходы = 1
PORTB &= ~((1<<0)|(1<<1)|(1<<2)|(1<<3)); // по умолчанию выключены = 0
// Настройка внешних прерываний
GIMSK |= (1<<PCIE); // Разрешаем внешние прерывания PCINT0
PCMSK |= (1<<BUTTON); // Разрешаем по маске прерывания на ноге кнопки (PCINT4)
// Настройка прерываний по счётчику/таймеру
TCCR0A = 0x02; // режим подсчета импульсов (сброс при совпадении)
TCCR0B = (1 << CS02)|(0 << CS01)|(1 << CS00); // предделитель clk/1024 (101)
TCNT0 = 0x00; // начальное значение счётчика импульсов
OCR0A = 0xFF; // максимальный предел счета (0-255)
TIMSK0 |= (1 << OCIE0A); // разрешение прерывания по совпадению со значением регистра OCR0A
sei(); // Разрешаем прерывания глобально: SREG |= (1<<SREG_I)
while (1)
{
}
}
Как видим, бесконечный цикл у нас пустой, а все задачи обрабатываются прерываниями. Переключение направления бегущего огня осуществляется по прерыванию PCINT0, которое срабатывает при нажатии на кнопку. Обработчик прерывания PCINT0 уже рассматривался в предыдущем примере. В данном примере он работает аналогично, только не переключает светодиоды непосредственно, а инвертирует значение булевой переменной direction *, отвечающей за направление бегущего огня: 0 — прямое, 1 — обратное. А переключение светодиодов происходит при срабатывании прерывания по таймеру, в зависимости от состояния переменной direction. Настройку прерываний по таймеру здесь рассмотрим поподробнее.
Настройка прерываний по таймеру
В микроконтроллере ATtiny13 есть восьмибитный таймер/счётчик, который может срабатывать по переполнению, либо по совпадению с одним из двух задаваемых значений.
Режим работы таймера задаётся битами WGM01 (1) и WGM00 (0) регистра TCCR0A:
- 00 — обычный режим
- 01 — режим коррекции фазы ШИМ
- 10 — режим подсчета импульсов (сброс при совпадении)
- 11 — режим ШИМ
Режимы ШИМ нас пока не интересуют. Мы можем использовать либо обычный режим, либо режим сброса при совпадении (англ. CTC — Clear Timer on Compare Match).
Обычный режим самый простой — прерывание будет срабатывать каждый раз, когда счётчик переполнился (достиг максимального значения — 255) и его значение обнулилось.
В режиме CTC прерывание будет срабатывать каждый раз, когда значение счётчика совпадёт с заранее заданным значением регистра сравнения OCR0A или OCR0B (0-255), счётчик при этом будет сбрасываться. В этом режиме можно настроить частоту прерываний более точно. В коде выше как раз показан пример настройки режима CTC.
Выбираем режим CTC (WGM01=1, WGM00=0):
TCCR0A = 0x02;
Биты CS02 (2), CS01 (1), CS00 (0) регистра TCCR0B устанавливают режим тактирования и предделителя тактовой частоты таймера/счетчика T0:
- 000 — таймер/счетчик T0 остановлен
- 001 — тактовый генератор CLK
- 010 — CLK/8
- 011 — CLK/64
- 100 — CLK/256
- 101 — CLK/1024
- 110 — от внешнего источника на выводе T0 (7 ножка, PB2) по спаду сигнала
- 111 — от внешнего источника на выводе T0 (7 ножка, PB2) по возрастанию сигнала
Устанавливаем режим CLK/1024 (CS02=1, CS01=0, CS00=1):
TCCR0B = (1 << CS02)|(0 << CS01)|(1 << CS00);
То есть тактовая частота для счётчика, с учётом предделителя, будет равна 1200000/1024=1171,875 Гц.
Текущее значение счётчика хранится в регистре TCNT0. Начальное значение счётчика мы задаём равным 0:
TCNT0 = 0x00;
Устанавливаем максимальный предел счёта (0-255, в нашем случае — 255):
OCR0A = 255;
Счётчик будет срабатывать с частотой 1171,875/256=4,58 Гц или каждые 218 мс.
Таким образом, изменяя тактовую частоту и значение регистра сравнения, мы можем достаточно точно регулировать частоту срабатывания счётчика.
Управление прерываниями от таймера осуществляется в регистре TIMSK0. Установка битов OCIE0B (2) и/или OCIE0A (1) разрешает преравания по совпадению с регистрами сравнения B и A соответственно. Установка бита TOIE0 разрешает прерывания по переполнению счётчика.
Разрешаем прерывания по совпадению с регистром сравнения A:
TIMSK0 |= (1 << OCIE0A);
И не забываем разрешить прерывания глобально:
SREG |= (1 << SREG_I);
Или:
sei();
При срабатывании счётчика по совпадению с регистром A, будет вызываться функция-обработчик прерывания ISR(TIM0_COMPA_vect), которая включает следующий, либо предыдущий светодиод, в зависимости от состояния переменной direction.
* Так как глобальная переменная direction может быть в любой момент изменена обработчиком прерывания, то её нужно объявлять с ключевым словом volatile, которое указывает компилятору, что её нельзя оптимизировать.
Полезные ссылки:
ATtiny13 Datasheet (официальный)
ATtiny13 Datasheet (урезанный вариант)
Таймер/счетчик T0 (8 бит)