11. Timer_0 в AVR ATMEGA16/32 пример использования

Давайте попробуем разобраться в том как нам может быть полезен таймер в микроконтроллере. В этом нам помогут несколько примеров. Смотрим схему.

При нажатии на кнопку должен загораться LED_1, при отпускании кнопки, LED_1 гаснет. А диод LED_0 должен просто мигать с интервалом 0.5 сек. Как выполнить задачу? Для начала выполним первую часть задачи. Пишем код

#include <avr/io.h>

//Битовые макросы
#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)          reg |= (1<<(bit))
#define BitIsClear(reg, bit)    ((reg & (1<<(bit))) == 0)
#define BitIsSet(reg, bit)       ((reg & (1<<(bit))) != 0)

//Макроопределения
#define LED_0 PC5
#define LED_1 PC4
#define SW    PC3
#define PORT_LED PORTC
#define DDR_LED DDRC
#define PIN_LED PINC

int main(void)
{
	
	DDR_LED = (1<<LED_0)|(1<<LED_1)|(0<<SW);  //Определяем входы и выходы порта C
	PORT_LED= (0<<LED_0)|(0<<LED_1)|(1<<SW);  //Включаем подтяжку для входов порта C
	
    while(1)
    {
		if (BitIsClear(PIN_LED,SW))//проверяем нажатие кнопки
		{
			SetBit(PORT_LED,LED_1);//Если нажата то вкл диод
		} 
		else
		{
			ClearBit(PORT_LED,LED_1);//отжата выкл диод
		}
    }
}

Загружаем прошивку в нашу отладочную плату GEEGROW Atmega 32 development board и радуемся результату.

Как видим при нажатии кнопки светодиод стабильно загорается и гаснет. Теперь в цикл while добавим мигание второго диода через задержку 500мсек.

 while(1)
    {
		if (BitIsClear(PIN_LED,SW))
		{
			SetBit(PORT_LED,LED_1);
		} 
		else
		{
			ClearBit(PORT_LED,LED_1);
		}
		
		_delay_ms(500);
		SetBit(PORT_LED,LED_0);
		_delay_ms(500);
		ClearBit(PORT_LED,LED_0);
    }

Загружаем  результат и видим что:

диод мигает как положено, а вот кнопка стало дико торомозить. Это и понятно, когда МК обрабатывает цикл задержки _delay_ms(500); в этот момент времени он не может обрабатывать пришедшие сигналы с других вводов. Поэтому все остальное игнорируется. Можно конечно повесить кнопки на прерывания и тогда проблема решится, но кнопок может быть больше чем одна и тогда никаких прерываний не напасешся. Во вторых прерывания будут вклиниваться во время обработки задержки, а это значит что задержа уже будет не 500мс, а больше. Да и в главном цикле программы никогда не используют циклические задержки (используют правда если надо, но очень быстрые), так как это будет тормозить всю фоновую программу. Вот тут то и приходят на выручку таймеры. Выбрасываем из главного цикла задержку, запускаем Timer0 в режиме СТС и ставим переменную в обработчике таймера чтоб она увеличивалась (декрементировалась) на 1 скажем раз в 4мсек. Что то типа того

#include <avr/io.h>
#include <avr/interrupt.h>

//Битовые макросы
#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)          reg |= (1<<(bit))
#define BitIsClear(reg, bit)    ((reg & (1<<(bit))) == 0)
#define BitIsSet(reg, bit)       ((reg & (1<<(bit))) != 0)

//Макроопределения
#define LED_0 PC5
#define LED_1 PC4
#define SW    PC3
#define PORT_LED PORTC
#define DDR_LED DDRC
#define PIN_LED PINC

uint8_t CountTim0;

ISR(TIMER0_COMP_vect)//прерывание по совпадению, режим СТС
{
	CountTim0++;//здесь мы бываем ровно каждые 4 мсек
}

int main(void)
{
	
	DDR_LED = (1<<LED_0)|(1<<LED_1)|(0<<SW);  //Определяем входы и выходы порта C
	PORT_LED= (0<<LED_0)|(0<<LED_1)|(1<<SW);  //Включаем подтяжку для входов порта C
	
	TCCR0=(0<<FOC0)|(1<<WGM01)|(0<<COM01)|(0<<COM00)|(1<<WGM00)|(1<<CS02)|(0<<CS01)|(0<<CS00);//режим СТС, разрядность 256
	TIMSK|=(1<<OCIE0);	//установка разрешений прерываний
	OCR0=249;
	sei();
	
    while(1)
    {
		if (BitIsClear(PIN_LED,SW))//проверяем кнопку
		{
			SetBit(PORT_LED,LED_1);
		} 
		else
		{
			ClearBit(PORT_LED,LED_1);
		}
		
		if (CountTim0>=125)//в этот секцию МК заглянет раз в 500мсек.
		{
			CountTim0=0;//сбросим переменную в 0;
			InvBit(PORT_LED,LED_0);//а тут инвертируем каждые 500мсек наш светодиод.
		}
    }
}

Проанализируем код. В регистр сравнения записано число 249. Это значит что 256*250(не забываем переход через 0 поэтому 250)=64000 тактов сожгет таймер за полный цикл. 64000/16000(1 мсек)=4 мсек будет длится полный цикл таймера до прерывания. Значит прерывание будет происходить ровно каждые 4 мсек, а в самом прерывании сам Timer0 будет сбрасываться автоматом в 0. Ну а в абработчике прерываний мы будем увеличивать переменную CountTim0 до 125. 125*4=500мсек. Это значит когда в нашей переменной будет 125 в главном цикле МК проверит ее и зайдет в блок кода и мигнет светодиодом. Смотрим на видео как это работает.

Как видим тормоза кнопки пропали, обе задачи работают параллельно и друг другу не мешают. Один диод мигает, второй сразу срабатывает при нажатии кнопки, что и требовалось сделать. Вот таким образом и используются таймеры для многозадачности.

Задача 2.
Предположим нам надо сделать так чтоб можно было кнопкой регулировать скорость мигания диода. Кнопку нажали - скорость мигания уменьшается, нажали снова - увеличивается. Опять на выручку приходит таймер. Для этого придется еще завести пару переменных в программе.

#include <avr/io.h>
#include <avr/interrupt.h>

//Битовые макросы
#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)          reg |= (1<<(bit))
#define BitIsClear(reg, bit)    ((reg & (1<<(bit))) == 0)
#define BitIsSet(reg, bit)       ((reg & (1<<(bit))) != 0)

//Макроопределения
#define LED_0 PC5
#define LED_1 PC4
#define SW    PC3
#define PORT_LED PORTC
#define DDR_LED DDRC
#define PIN_LED PINC

volatile uint8_t CountTim0;//переменная задержка для светодиода
volatile uint8_t CountSW;//переменная задержка для кнопки.
uint8_t DelLed_0=125;//переменная задержка для скорости мигания светодиода
uint8_t FlagSw;

ISR(TIMER0_COMP_vect)//Режим СТС, прерывание срабатывает раз в 4 мсек.
{
	CountTim0++;
	CountSW++;
}

int main(void)
{
	
	DDR_LED = (1<<LED_0)|(1<<LED_1)|(0<<SW);  //Определяем входы и выходы порта C
	PORT_LED= (0<<LED_0)|(0<<LED_1)|(1<<SW);  //Включаем подтяжку для входов порта C
	
	TCCR0=(0<<FOC0)|(1<<WGM01)|(0<<COM01)|(0<<COM00)|(1<<WGM00)|(1<<CS02)|(0<<CS01)|(0<<CS00);//режим СТС, разрядность 256
	TIMSK|=(1<<OCIE0);	//установка разрешений прерываний
	OCR0=249;
	sei();
	
    while(1)
    {
		
		if (BitIsClear(PIN_LED,SW) && CountSW>=25 && BitIsClear(FlagSw,0))//раз в 100 мсек обрабатываем нажатие кнопки 25*4=100мсек.
		{
			CountSW=0;
			SetBit(FlagSw,1);
			if (DelLed_0>10)
			{
				DelLed_0=DelLed_0-5;
			}
		}
		
		if (BitIsClear(PIN_LED,SW) && CountSW>=25 && BitIsSet(FlagSw,0))
		{
			CountSW=0;
			SetBit(FlagSw,1);
			if (DelLed_0<250)
			{
				DelLed_0=DelLed_0+5;
			}
		}
		
		if (BitIsSet(PIN_LED,SW) && BitIsSet(FlagSw,1))
		{
			InvBit(FlagSw,0);
			ClearBit(FlagSw,1);
		}
	
		
		if (CountTim0>=DelLed_0)//в этот секцию МК заглянет в зависимости от числа в переменной DelLed_1.
		{
			CountTim0=0;//сбросим переменную в 0;
			InvBit(PORT_LED,LED_0);//а тут инвертируем наш светодиод.
		}

    }	
}

Как видим код немного увеличился и усложнился. А что делать, кнопка то одна, а МК при одном и том же нажатии должен выполнять различные действия. Фактически он должен запоминать, что если в предыдущем действии скорость мигания увеличивали, то при следующем нажатии надо скорость уменьшать. Поэтому и код так усложнился. А вот если бы кнопки две было, то код был бы гораздо проще. Но суть сейчас не в коде, а в том что все это смогли сделать благодаря таймеру. Посмотрим в видео как в реальности работает наша программа.

Попробуем сейчас переключить Timer0 в режим Fast PWM,  и на аппартный вывод ОС0 подключить светодиод, и вот что выйдет, смотрим код и видео.

#define F_CPU 16000000UL /*Частота кварца микроконтроллера*/
#include <avr/io.h>
#include <util/delay.h>

uint8_t FlagSpeed;

int main(void)
{
	DDRB = (1<<PB3);
	PORTB= (0<<PB3);
	
	TCCR0=(0<<FOC0)|(1<<WGM01)|(1<<COM01)|(0<<COM00)|(1<<WGM00)|(1<<CS02)|(0<<CS01)|(0<<CS00);//режим FAST PWM, разрядность 256

    while(1)
    {
		_delay_ms(5);
		
		if (FlagSpeed==0)
		{
			OCR0++;
			if (OCR0==255)
			{
				FlagSpeed=1;
			}
		}
		
		if (FlagSpeed==1)
		{
			OCR0--;
			if (OCR0==0)
			{
				FlagSpeed=0;
			}
		}
		
    }	
}

Как видим с помощью таймера можно даже задавать уровень яркости сетодиода на цифровом выводе МК. А если на выводе подключить конденсатор через резистор то и вообще можно очень плавно менять напряжение на выходе от 0 до 5 вольт. Вот тебе и аналоговый выход через цифровой. А это значит можно плавно управлять вращением двигателя, вентилятора, регулировать яркость освещения да что угодно. И все это благодаря таймерам в МК.

А если нам надо подключить 8 светодиодов и чтоб они все плавно мигали, а аппаратный вывод ОС0 который всего один, уже занят и освободить его никак. Как решить задачу? Выход есть. Для этого надо включить оба прерывания таймера по переполнению и по совпадению. В прерывании по переполнению включать светодиоды, а в прерывании по сравнению выключать вот и все. Смотрим как это сделать схему, код и видео.

#define F_CPU 16000000UL /*Частота кварца микроконтроллера*/
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

uint8_t FlagSpeed;

ISR(TIMER0_OVF_vect)//по переполнению
{
	PORTC=0xFF;//все светодиоды включили
}

ISR(TIMER0_COMP_vect)//по сравнению
{
	PORTC=0x00;//все светодиоды выключили
}

int main(void)
{
	DDRC = 0xFF;//выводы порта С на выход.
	PORTC= 0x00;
	
	TCCR0=(0<<FOC0)|(1<<WGM01)|(0<<COM01)|(0<<COM00)|(1<<WGM00)|(1<<CS02)|(0<<CS01)|(0<<CS00);//режим СТС, разрядность 256
	TIMSK|=(1<<OCIE0)|(1<<TOIE0);	//установка разрешений прерываний
	
	sei();//включили флаг I
	
    while(1)
    {
		_delay_ms(5);
		
		if (FlagSpeed==0)
		{
			OCR0++;
			if (OCR0==255)
			{
				FlagSpeed=1;
			}
		}
		
		if (FlagSpeed==1)
		{
			OCR0--;
			if (OCR0==0)
			{
				FlagSpeed=0;
			}
		}

    }	
}

Итак видим что очень многие и сложные задачи можно реализовать только через таймеры МК. В следующей статье рассмотрим работу таймера - монстра T1.