OSBoy notes.

Записки обо всём...

ATtiny13 - бегущий огонь

В предыдущем примере мы включали/выключали светодиод на 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 бит)

Теги : attiny13, бегущий огонь, прерывания по таймеру, AVR, микроконтроллеры, программирование, си, C

Комментариев: 10

Талич
#Ответить
Дружище, продолжай, весьма пользительно.) Как раз для меня. Есть вопрос, как раз по ШИМ. Собрал схему по ссылке http://radiokot.ru/konkursCatDay2014/53/ . Код написан в кодвижене, поглядел мельком. Минус один у кода: при включении леды на долю секунды вспыхивают довольно ярко, далее работает с минимального уровня яркости. Вот думаю, собрать голову в руки и с помощью твоих статей переделать, чтобы как в лампе от blitzwolf, кошерно было).
OSBoy
#Ответить
Я бегло посмотрел код по ссылке - сам не пробовал, но по-моему, там косяк в том, что ещё до конфигурирования таймера ШИМ на PB0 зачем то высокий уровень подаётся. Попробуйте вот эту строку:
PORTB=(0<<PORTB5) | (0<<PORTB4) | (0<<PORTB3) | (0<<PORTB2) | (0<<PORTB1) | (1<<PORTB0);
Заменить на:
PORTB=0;
ILT
#Ответить
А это правильно, что внутри функции обработчика прерываний задается переменная light = 1? Мне кажется ее значение надо задать вне функции или нет ?

static char light = 1;
Игорь
#Ответить
Ну кстати это бы могло бы стать хорошим примером так сказать
Совместить ШИМ и режимы прерывания
А то вот как то у меня не получается с 2 кнопок ( inc/dec) на тиньке шим реализовать :)))
OSBoy
#Ответить
Вот, пожалуйста! :)
osboy. ru/blog/microcontrollers/tiny13-pcint-pwm.html
Игорь
#Ответить
Спасибо огромное
Надо сказать это несколько другой способ это реализовать но ваш красивее
Хотя и используется тот самый "костыль" за который тиньку и ругают
Т.е математическое вычисления таймера :))
Отличный пример
Ещё раз спасибо! Очень пригодилось
OSBoy
#Ответить
Уж не знаю, насколько это правильно или нет, но по-моему, какой смысл создавать глобальную переменную, если она используется только в пределах одной функции?
ILT
#Ответить
Просто при вызове функции ее значение всегда получается равно 1 и бегущего огня не получится? Т.е. я хочу спросить, что ее значение изменится один раз в зависимости от direction?
OSBoy
#Ответить
А Вы обратите внимание на ключевое слово ststic! Оно как раз отвечает за то, что значение переменной будет сохраняться. А 1 присваивается только первый раз, при инициализации переменной .
Антон
#Ответить
Полезно! Спасибо.

Добавить комментарий

Отменить