Программный таймер avr. Прерывания в atmega8

09.04.2024 Windows 10

Таймер-счетчик является одним из самых ходовых ресурсов AVR микроконтроллера. Его основное назначение - отсчитывать заданные временные интервалы. Кроме того, таймеры-счетчики могут выполнять ряд дополнительных функций, как то - формирование ШИМ сигналов, подсчет длительности и количества входящих импульсов. Для этого существуют специальные режимы работы таймера-счетчика.

В зависимости от модели микроконтроллера количество таймеров и набор их функций может отличаться. Например, у микроконтроллера Atmega16 три таймера-счетчика - два 8-ми разрядных таймера-счетчика Т0 и Т2, и один 16-ти разрядный - Т1. В этой статье, на примере ATmega16, мы разберем как использовать таймер-счетчик Т0.

Используемые выводы

Таймер-счетчик Т0 использует два вывода микроконтроллера ATmega16. Вывод T0 (PB0) - это вход внешнего тактового сигнала. Он может применяться, например, для подсчета импульсов. Вывод OC0 (PB3) - это выход схемы сравнения таймера-счетчика. На этом выводе с помощью таймера может формировать меандр или ШИМ сигнал. Также он может просто менять свое состояние при срабатывании схемы сравнения, но об этом поговорим позже.


Выводы T0 и OC0 задействуются только при соответствующих настройках таймера, в обычном состоянии это выводы общего назначения.

Регистры таймера-счетчика Т0

Хоть это и скучно, но регистры - это то, без чего невозможно программировать микроконтроллеры, конечно, если вы не сидите плотно на Arduino. Так вот, таймер Т0 имеет в своем составе три регистра:

Счетный регистр TCNT0,
- регистр сравнения OCR0,
- конфигурационный регистр TCCR0.

Кроме того, есть еще три регистра, относящиеся ко всем трем таймерам ATmega16:

Конфигурационный регистр TIMSK,
- статусный регистр TIFR.
- регистр специальных функций SFIOR

Начнем с самого простого.

Это 8-ми разрядный счетный регистр. Когда таймер работает, по каждому импульсу тактового сигнала значение TCNT0 изменяется на единицу. В зависимости от режима работы таймера, счетный регистр может или увеличиваться, или уменьшаться.
Регистр TCNT0 можно как читать, так и записывать. Последнее используется когда требуется задать его начальное значение. Когда таймер работает, изменять его содержимое TCNT0 не рекомендуется, так как это блокирует схему сравнения на один такт.

Это 8-ми разрядный регистр сравнения. Его значение постоянно сравнивается со счетным регистром TCNT0, и в случае совпадения таймер может выполнять какие-то действия - вызывать прерывание, менять состояние вывода OC0 и т.д. в зависимости от режима работы.

TCCR0 (Timer/Counter Control Register)


Это конфигурационный регистр таймера-счетчика Т0, он определяет источник тактирования таймера, коэффициент предделителя, режим работы таймера-счетчика Т0 и поведение вывода OC0. По сути, самый важный регистр.

Биты CS02, CS01, CS00 (Clock Select) - определяют источник тактовой частоты для таймера Т0 и задают коэффициент предделителя. Все возможные состояния описаны в таблице ниже.


Как видите, таймер-счетчик может быть остановлен, может тактироваться от внутренней частоты и также может тактироваться от сигнала на выводе Т0.

Биты WGM10, WGM00 (Wave Generator Mode) - определяют режим работы таймера-счетчика Т0. Всего их может быть четыре - нормальный режим (normal), сброс таймера при совпадении (CTC), и два режима широтно-импульсной модуляции (FastPWM и Phase Correct PWM). Все возможные значения описаны в таблице ниже.

Более подробно будем разбирать режимы в коде. Сейчас все нюансы все равно не запомнятся.

Биты COM01, COM00 (Compare Match Output Mode) - определяют поведение вывода OC0. Если хоть один из этих битов установлен в 1, то вывод OC0 перестает функционировать как обычный вывод общего назначения и подключается к схеме сравнения таймера счетчика Т0. Однако при этом он должен быть еще настроен как выход.
Поведение вывода OC0 зависит от режима работы таймера-счетчика Т0. В режимах normal и СTC вывод OC0 ведет себя одинаково, а вот в режимах широтно-импульсной модуляции его поведение отличается. Не будем сейчас забивать себе голову всеми этими вариантами и разбирать таблицы для каждого режима, оставим это на практическую часть.

И последний бит регистра TCCR0 - это бит FOC0 (Force Output Compare) .Этот бит предназначен для принудительного изменения состояния вывода OC0. Он работает только для режимов Normal и CTC. При установки бита FOC0 в единицу состояние вывода меняется соответственно значениям битов COM01, COM00. FOC0 бит не вызывает прерывания и не сбрасывает таймер в CTC режиме.

TIMSK (Timer/Counter Interrupt Mask Register)


Общий регистр для всех трех таймеров ATmega16, он содержит флаги разрешения прерываний. Таймер Т0 может вызывать прерывания при переполнении счетного регистра TCNT0 и при совпадении счетного регистра с регистром сравнения OCR0. Соответственно для таймера Т0 в регистре TIMSK зарезервированы два бита - это TOIE0 и OCIE0. Остальные биты относятся к другим таймерам.

TOIE0 - 0-е значение бита запрещает прерывание по событию переполнение, 1 - разрешает.
OCIE0 - 0-е значение запрещает прерывания по событию совпадение, а 1 разрешает.

Естественно прерывания будут вызываться, только если установлен бит глобального разрешения прерываний - бит I регистра SREG.

TIFR (Timer/Counter0 Interrupt Flag Register)


Общий для всех трех таймеров-счетчиков регистр. Содержит статусные флаги, которые устанавливаются при возникновении событий. Для таймера Т0 - это переполнение счетного регистра TCNT0 и совпадение счетного регистра с регистром сравнения OCR0.

Если в эти моменты в регистре TIMSK разрешены прерывания и установлен бит I, то микроконтроллер вызовет соответствующий обработчик.
Флаги автоматически очищаются при запуске обработчика прерывания. Также это можно сделать программно, записав 1 в соответствующий флаг.

TOV0 - устанавливается в 1 при переполнении счетного регистра.
OCF0 - устанавливается в 1 при совпадении счетного регистра с регистром сравнения

SFIOR (Special Function IO Register)


Начинающему про этот регистр в принципе можно и не знать, один из его разрядов сбросывает 10-ти разрядный двоичный счетчик, который делит входную частоту для таймера Т0 и таймера Т1.

Сброс осуществляется при установке бита PSR10 (Prescaler Reset Timer/Counter1 и Timer/Counter0) в единицу.

Заключение

Одним из преимуществ микроконтроллера ATmega8 является широкий диапазон различных прерываний.

Прерывание представляет собой событие, при наступлении которого выполнение основной программы приостанавливается и вызывается функция, обрабатывающая прерывание определённого типа.

Прерывания делятся на внутренние и внешние. К источникам внутренних прерываний относятся встроенные модули микроконтроллера (таймеры, приёмопередатчик USART и т.д). Внешние прерывания возникают при поступлении внешних сигналов на выводы микроконтроллера (например сигналы на выводы RESET и INT). Характер сигналов, приводящих к возникновению прерывания задаётся в регистре управления MCUCR , в частности в разрядах - ISC00 (бит 0) и ISC01 (бит 1) для входа INT 0; ISC10 (бит2) и ISC11 (бит3) для входа INT1.

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

Векторы прерываний в Atmega8

Адрес Источник прерывания Описание
0x0000 RESET Сигнал сброса
0x0001 INT0 Внешний запрос на прерывание по входу INT0
0x0002 INT1 Внешний запрос на прерывание по входу INT1
0x0003 T/C1 Захват по таймеру T/C1
0x0004 T/C1 Совпадение с регистром сравнения A таймера T/C1
0x0005 T/C1 Совпадение с регистром сравнения B таймера T/C1
0x0006 T/C1 Переполнение счётчика T/C1
0x0007 T/C0 Переполнение счётчика T/C0
0x0008 SPI Передача данных по интерфейсу SPI завершена
0x0009 UART Приём данных приёмопередптчиком UART завершен
0x000A UART Регистр данных UART пуст
0x000B UART Передача данных приёмопередптчиком UART завершена
0x000C ANA_COMP Прерывание от аналогового компаратора

Управления прерываниями

За управление прерываниями в ATmega8 отвечают 4 регистра:

GIMSK (он же GICR) - запрет/разрешение прерываний по сигналам на входах INT0, INT1

GIFR - управление всеми внешними прерываниями

TIMSK , TIFR - управление прерываниями от таймеров/счётчиков

Регистр GIMSK(GICR)

INTFx=1: произошло прерывание на входе INTx. При входе в подпрограмму обработки прерывания INTFx автоматически сбрасывается в сотояние лог. 0

Регистр TIMSK

7 6 5 4 3 2 1 0
TOIE1
OCIE1A
OCIE1B
-
TICIE
-
TOIE0
-

TOIE1=1 : прерывание по переполнению T/C1 разрешено

OCIE1A=1 : прерывание при совпадении регистра сравнения A с содержимым счётчика T/C1 разрешено

OCIE1B=1 : прерывание при совпадении регистра сравнения B с содержимым счётчика T/C1 разрешено

TICIE=1 : разрешено прерывание при выполнении условия захвата

TOIE0=1 : прерывание по переполнению T/C0 разрешено

Регистр TIFR

7 6 5 4 3 2 1 0
TOV1
OCF1A
OCF1B
-
ICF1
-
TOV0
-

TOV1=1 : произошло переполнение T/C1

OCF1A=1 : произошло совпадение регистра сравнения A с содержимым счётчика T/C1 разрешено

OCF1B=1 : произошло совпадение регистра сравнения B с содержимым счётчика T/C1 разрешено

ICF=1 : выполнилось условия захвата

TOV0=1 : произошло переполнение T/C0

При входе в подпрограмму обработки прерывания соответствующий прерыванию флаг регистра TIFR автоматически сбрасывается в сотояние лог. 0

Прерывания работают только тогда, когда в регистре состояния SREG разрешены общие прерывания (бит 7 = 1). В случае наступления прерывания этот бит автоматически сбрасывается в 0, блокируя выполнение последующих прерываний.

В данном примере вывод INT0 включён в режиме входа с подтяжкой. При замыкании вывода на землю при помощи кнопки на нём устанавливается лог.0 (фронт сигнала ниспадает с напряжения питания до 0) и срабатывает обработчик прерывания, включающий лампочку, подключённую к нулевому выводу порта B

void lampON()
{
PORTB.0=1;
DDRB.0=1;
}

interrupt void ext_int0_isr(void)
{
lampON();
}

DDRD.2=0;
PORTD.2=1;

SREG|= (1 while(1) {

На приведённом примере также видно, как задаются векторы прерываний в Code Vision AVR (interrupt void ext_int0_isr(void)). Аналогично задаются вектора прерываний и для других случаев:

EXT_INT0 2
EXT_INT1 3
TIM2_COMP 4
TIM2_OVF 5
TIM1_CAPT 6
TIM1_COMPA 7
TIM1_COMPB 8
TIM1_OVF 9
TIM0_OVF 10
SPI_STC 11
USART_RXC 12
USART_DRE 13
USART_TXC 14
ADC_INT 15
EE_RDY 16
ANA_COMP 17
TWI 18
SPM_READY 19

В последнее время все больше и больше начинающих сталкиваются с проблемой освоения Таймеров/Счетчиков (далее Т/С) на этапе изучения микроконтроллеров. В данной статье я постараюсь развеять страхи перед данными модулями и доступно объяснить, как и с чем употребляют те самые Т/С.

За основу мы возьмем очень популярную среди разработчиков устройств на МК книгу, автором которой является А.В. Евстифеев. По ссылкам в конце статьи Вы сможете найти проект в и проект в . В этой статье мы разберем работу 8-ми битного Т/С Т2, который входит в состав Т/С МК Atmega8.

Итак, что же такое Таймер/Счетчик? Т/С - это один из модулей МК AVR с помощью которого можно отмерять определенные промежутки времени, организовать ШИМ и многие другие задачи. В зависимости от модели МК, количество Т/С может составлять 4 и более. Пример тому - МК Atmega640х, 1280х/1281х, 2560х/2561х, которые содержат на своем борту 6 Т/С: два 8-ми битных и четыре 16-ти битных. МК Atmega8 содержит в себе три Т/С: Т0 и Т2 с разрядностью 8 бит, Т1 с разрядностью 16 бит.

Давайте подробнее рассмотрим Т/С Т2 микроконтроллера Atmega8.

Этот таймер может работать в нескольких режимах: Normal, Phase correct PWM, CTC (сброс при совпадении), Fast PWM. Подробнее о каждом режиме Вы можете прочитать в книге.

Данный Т/С состоит из регистра управления, счетного регистра, регистра сравнения, регистра состояния асинхронного режима. Структурная схема Т2 приведена на рис.1

Рассмотрим в теории как же работает данный модуль. Чтобы для начала Вам было понятнее, мы не будем рассматривать все лишние примочки таймера и рассмотрим самый обычный его режим - NORMAL. Для себя определим что МК тактируется от внутреннего RC-генератора с частотой 1МГц и таймер настроен на работу в режиме NORMAL.

Тактовые импульсы поступают на вход clk i\o и попадают в предделитель таймера. Предделитель может быть настроен, по Вашим потребностям, на прямой проход тактовых импульсов или делить входящие импульсы, пропуская только их определенную часть. Поделить входящие импульсы можно на /8, /64, /256, /1024. Так как у нас Т\С может работать в асинхронном режиме, то при включении его в этот режим количество предделителей существенно вырастает, но мы их рассматривать пока не будем. С предделителя тактовые импульсы поступают в блок управления и уже с него попадают в счетный регистр. Счетный регистр в свою очередь совершает инкремент на каждый входящий импульс. Счетный регистр Т2 8-ми битный, поэтому он может считать только до 255. Когда наступает переполнение счетного регистра, он сбрасывается в 0 и в этом же такте начинает считать заново. Так же в момент переполнения счетного регистра устанавливается флаг TOV2 (флаг прерывания по переполнению) регистра TIFR.

Теперь, раз уж мы затронули такие слова, как РЕГИСТР, самое время с ними познакомится. Для начала мы затронем только те регистры, с которыми будем непосредственно работать, дабы не забивать мозг лишней информацией.

TCNT2 - счетный регистр, о его работе мы уже говорили.

TCCR2 - регистр управления таймером.

TIMSK - регистр маски прерываний(в Atmega8 этот регистр является единственным для всех таймеров).

TIFR - регистр флагов прерываний(в Atmega8 этот регистр является единственным для всех таймеров).

А теперь о каждом подробно:

Регистр управления TCCR2. Содержимое этого регистра вы можете посмотреть на рис.2.


рис.2

Биты 0-2 отвечают за тактирование таймера. Установка определенных комбинаций в этих битах настраивает предделитель данного таймера. Если все три бита сброшены - таймер выключен.

Биты 3,6 отвечают за режим работы таймера.

Биты 4,5 нужны для настройки поведения вывода ОСn (проще говоря, используются при настройке ШИМ)

И последний бит этого регистра - бит 7. С его помощью мы можем принудительно изменять состояние вывода ОСn.

Регистр маски прерываний - TIMSK. Его мы видим на рисунке №3

Из этого регистра нас интересуют только два последних бита, биты 6 и 7. Этими битами мы разрешаем работу прерываний.

Бит 6, если в него записать единицу, разрешает прерывание по событию "Переполнение Т\С Т2"

Бит 7, если в него записать еди ницу, разрешает прерывание по событию "Совпадение счетного регистра с регистром сравнения"

Регистр флагов прерываний TIFR. Его мы видим на рисунке №4

рис.4

В этом регистре нас так же интересуют два последних бита: биты 6 и 7.

Бит 6 - флаг, устанавливается по событию "Переполнение Т\С Т2"
Бит 7 - флаг, устанавливается по событию "Совпадение счетного регистра с регистром сравнения"

Эти биты сбрасываются автоматически при выходе из обработчика прерывания, но для надежности их можно сбрасывать самостоятельно, сбрасывая эти биты в "0".

Остальные биты регистров TIMSK и TIFR используются Т\С Т0 и Т1. Как вы уже заметили, у битов этих регистров даже названия совпадают, за исключением цифры в конце названия, которая и указывает к какому таймеру данный бит применИм.

Осталось рассмотреть две несложные таблички, а именно: таблица, в которой описано управление тактовым сигналом (рис. 6), и таблица, в которой описано, как в общем настроить таймер (рис.5).

О том, что находится в этих таблицах, я писал выше, однако привожу Вам их для наглядности.

Вот мы и закончили с теорией, и пора приступить к практической части. Сразу оговорюсь.

ЧАСЫ, КОТОРЫЕ ПОЛУЧАТСЯ В ХОДЕ ИЗУЧЕНИЯ ДАННОЙ СТАТЬИ, НЕ ОБЛАДАЮТ ВЫСОКОЙ ТОЧНОСТЬЮ. ДАННАЯ СТАТЬЯ ОРИЕНТИРОВАННА НА ОБЩИЕ ПРИНЦИПЫ РАБОТЫ С ТАЙМЕРАМИ.

Открываем Studio 6, создаем проект и выбираем Atmega8.

В самом начале указываем частоту тактирования и подключаем нужные нам для работы библиотеки

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

В первой строчке мы указываем частоту. Это необходимо для того, чтобы компилятор нас лучше понимал, если вдруг мы захотим использовать функции _delay_().

Во второй строчке кода подключается библиотека с общим описанием регистров нашего МК. Так же в ней всем регистрам присвоены читабельные имена.

В третьей строке подключается библиотека для работы с векторами прерываний.

TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7);

На этом настройка нашего таймера закончена. Давайте подробнее рассмотрим последние три строки кода.

В первой строке мы разрешили прерывания по событию "Переполнение таймера\счетчика Т2"

И в третьей строкой мы глобально разрешили прерывания. Это можно было также написать следующим образом:

Asm("sei");

Остается добавить обработчик прерывания и код наших часов реального времени.

ISR (TIMER2_OVF_vect) { takt++; if (takt>=4){sek++; takt=0x00;} if (sek>=60) {min++; sek=0x00;} if (min>=60) {hour++; min=0x00;} if (hour>=24) {hour=0х00}; }

В коде, который находится в обработчике прерывания, нет ничего сложного и нового для Вас. Внимание обратим только на переменную takt и волшебную цифру "4". Откуда взялась эта цифра? Давайте рассмотрим подробно этот момент.

Мы знаем, что наш МК работает от внутреннего генератора с частотой 1МГц, таймер тактируется с предделителем \1024, считать наш таймер может до 255. Зная эти параметры мы можем посчитать сколько переполнений он совершит за 1 секунду

1 000 000 \ 1024 \ 256 = 3,814697.....

Ну, а так как мы учимся работать с таймерами и не ставили цель получить суперточный ход часов, мы округляем наш результат и получаем "4". Т.е. за 1 секунду таймер переполнится ~4 раза.

Почему мы делили на 256 если таймер считает только до 255? Потому что "0" это тоже число. Думаю, здесь все понятно.

Не забываем, что все переменные нужно объявить как глобальные.

Вот весь листинг программы которая у нас получилась.

#define F_CPU 1000000UL #include < avr/io.h > #include < avr/interrupt.h > unsigned char takt = 0; unsigned char sek = 0; unsigned char min=0; unsigned char hour=0; ISR (TIMER2_OVF_vect) { takt++; if (takt>=4){sek++; takt=0x00;} if (sek>=60) {min++; sek=0x00;} if (min>=60) {hour++; min=0x00;} if (hour>=24) {hour=0х00}; } int main(void) { TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7); while(1) { } }

А как же вывод информации пользователю? А тут кому как нравится. Можете использовать семисегментные индикаторы, графические или знакогенерирующие дисплеи и т.д.

В архиве Вы найдете проект с выводом информации на дисплей от nokia5110, проект в Proteus 7 и все нужные файлы и библиотеки для работы.

Обращаю внимание на то, что библиотека LCD_5110 для работы с дисплеем написана участником форума и предоставлена с его разрешения.

С счетчиком итераций главного цикла мы разобрались и выяснили, что для точных временных отсчетов он не годится совершенно — выдержка плавает, да и считать ее сложно. Что делать?

Очевидно, что нужен какой то внешний счетчик, который тикал бы независимо от работы процессора, а процессор мог в любой момент посмотреть что в нем такое натикало. Либо чтобы счетчик выдавал события по переполнению или опустошению — флажок поднимал или прерывание генерил. А проц это прочухает и обработает.

И такой счетчик есть, даже не один — это периферийные таймеры. В AVR их может быть несколько штук да еще с разной разрядностью. В ATmega16 три, в ATmega128 четыре. А в новых МК серии AVR может даже еще больше, не узнавал.

Причем таймер может быть не просто тупым счетчиком, таймер является одним из самых навороченных (в плане альтернативных функций) периферийных девайсов.

Что умееют таймеры

  • Тикать с разной скоростью, подсчитывая время
  • Считать входящие извне импульсы (режим счетчика)
  • Тикать от внешнего кварца на 32768гц
  • Генерировать несколько видов ШИМ сигнала
  • Выдавать прерывания (по полудесятку разных событий) и устанавливать флаги

Разные таймеры имеют разную функциональность и разную разрядность. Это подробней смотреть в даташите.

Источник тиков таймера
Таймер/Счетчик (далее буду звать его Т/С) считает либо тактовые импульсы от встроенного тактового генератора, либо со счетного входа.

Погляди внимательно на распиновку ног ATmega16, видишь там ножки T1 и T0?

Так вот это и есть счетные входы Timer 0 и Timer 1. При соответствующей настройке Т/С будет считать либо передний (перепад с 0-1), либо задний (перепад 1-0) фронт импульсов, пришедших на эти входы.

Главное, чтобы частота входящих импульсов не превышала тактовую частоту процессора, иначе он не успеет обработать импульсы.

Кроме того, Т/С2 способен работать в асинхронном режиме. То есть Т/С считает не тактовые импульсы процессора, не входящие импульсы на ножки, а импульсы своего собственного собственного генератора, работающего от отдельного кварца. Для этого у Т/С2 есть входы TOSC1 и TOSC2, на которые можно повесить кварцевый резонатор.

Зачем это вообще надо? Да хотя бы организовать часы реального времени. Повесил на них часовой кварц на 32768 Гц да считай время — за секунду произойдет 128 переполнений (т.к. Т/С2 восьми разрядный). Так что одно переполнение это 1/128 секунды. Причем на время обработки прерывания по переполнению таймер не останавливается, он также продолжает считать. Так что часы сделать плевое дело!

Предделитель
Если таймер считает импульсы от тактового генератора, или от своего внутреннего, то их еще можно пропустить через предделитель.

То есть еще до попадания в счетный регистр частота импульсов будет делиться. Делить можно на 8, 32, 64, 128, 256, 1024. Так что если повесишь на Т/С2 часовой кварц, да пропустишь через предделитель на 128, то таймер у тебя будет тикать со скоростью один тик в секунду.

Удобно! Также удобно юзать предделитель когда надо просто получить большой интервал, а единственный источник тиков это тактовый генератор процессора на 8Мгц, считать эти мегагерцы задолбаешься, а вот если пропустить через предделитель, на 1024 то все уже куда радужней.

Но тут есть одна особенность, дело в том, что если мы запустим Т/С с каким нибудь зверским предделителем, например на 1024, то первый тик на счетный регистр придет не обязательно через 1024 импульса.

Это зависит от того в каком состоянии находился предделитель, а вдруг он к моменту нашего включения уже досчитал почти до 1024? Значит тик будет сразу же. Предделитель работает все время, вне зависимости от того включен таймер или нет.

Поэтому предделители можно и нужно сбрасывать. Также надо учитывать и то, что предделитель един для всех счетчиков, поэтому сбрасывая его надо учитывать то, что у другого таймера собьется выдержка до следующего тика, причем может сбиться конкретно так.

Например первый таймер работает на выводе 1:64, а второй на выводе 1:1024 предделителя. У второго почти дотикало в предделителе до 1024 и вот вот должен быть тик таймера, но тут ты взял и сбросил предделитель, чтобы запустить первый таймер точно с нуля. Что произойдет? Правильно, у второго делилка тут же скинется в 0 (предделитель то единый, регистр у него один) и второму таймеру придется ждать еще 1024 такта, чтобы получить таки вожделенный импульс!

А если ты будешь сбрасывать предделитель в цикле, во благо первого таймера, чаще чем раз в 1024 такта, то второй таймер так никогда и не тикнет, а ты будешь убиваться головой об стол, пытаясь понять чего это у тебя второй таймер не работает, хотя должен.

Для сброса предделителей достаточно записать бит PSR10 в регистре SFIOR. Бит PSR10 будет сброшен автоматически на следующем такте.

Счетный регистр
Весь результат мучений, описанных выше, накапливается в счетном регистре TCNTх, где вместо х номер таймера. он может быть как восьмиразрядным, так и шестнадцати разрядным, в таком случае он состоит из двух регистров TCNTxH и TCNTxL — старший и младший байты соответственно.

Причем тут есть подвох, если в восьмиразрядный регистр надо положить число, то нет проблем OUT TCNT0,Rx и никаких гвоздей, то с двухбайтными придется поизвращаться.

А дело все в чем - таймер считает независимо от процессора, поэтому мы можем положить вначале один байт, он начнет считаться, потом второй, и начнется пересчет уже с учетом второго байта.

Чувствуете лажу? Вот! Таймер точное устройство, поэтому грузить его счетные регистры надо одновременно! Но как? А инженеры из Atmel решили проблему просто:
Запись в старший регистр (TCNTxH) ведется вначале в регистр TEMP. Этот регистр чисто служебный, и нам никак недоступен.

Что в итоге получается: Записываем старший байт в регистр TEMP (для нас это один хрен TCNTxH), а затем записываем младший байт. В этот момент, в реальный TCNTxH, заносится ранее записанное нами значение. То есть два байта, старший и младший, записываются одновременно! Менять порядок нельзя! Только так

Выглядит это так:

CLI ; Запрещаем прерывания, в обязательном порядке! OUT TCNT1H,R16 ; Старшей байт записался вначале в TEMP OUT TCNT1L,R17 ; А теперь записалось и в старший и младший! SEI ; Разрешаем прерывания

Зачем запрещать прерывания? Да чтобы после записи первого байта, прога случайно не умчалась не прерывание, а там кто нибудь наш таймер не изнасиловал. Тогда в его регистрах будет не то что мы послали тут (или в прерывании), а черти что. Вот и попробуй потом такую багу отловить! А ведь она может вылезти в самый неподходящий момент, да хрен поймаешь, ведь прерывание это почти случайная величина. Так что такие моменты надо просекать сразу же.

Читается все также, только в обратном порядке. Сначала младший байт (при этом старший пихается в TEMP), потом старший. Это гарантирует то, что мы считаем именно тот байт который был на данный момент в счетном регистре, а не тот который у нас натикал пока мы выковыривали его побайтно из счетного регистра.

Контрольные регистры
Всех функций таймеров я расписывать не буду, а то получится неподьемный трактат, лучше расскажу о основной — счетной, а всякие ШИМ и прочие генераторы будут в другой статье. Так что наберитесь терпения, ну или грызите даташит, тоже полезно.

Итак, главным регистром является TCCRx
Для Т/С0 и Т/С2 это TCCR0 и TCCR2 соответственно, а для Т/С1 это TCCR1B

Нас пока интересуют только первые три бита этого регистра:
CSx2.. CSx0, вместо х подставляется номер таймера.
Они отвечают за установку предделителя и источник тактового сигнала.

У разных таймеров немного по разному, поэтому опишу биты CS02..CS00 только для таймера 0

  • 000 — таймер остановлен
  • 001 — предделитель равен 1, то есть выключен. таймер считает тактовые импульсы
  • 010 — предделитель равен 8, тактовая частота делится на 8
  • 011 — предделитель равен 64, тактовая частота делится на 64
  • 100 — предделитель равен 256, тактовая частота делится на 256
  • 101 — предделитель равен 1024, тактовая частота делится на 1024
  • 110 — тактовые импульсы идут от ножки Т0 на переходе с 1 на 0
  • 111 — тактовые импульсы идут от ножки Т0 на переходе с 0 на 1

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

За прерывания от таймеров отвечают регистры TIMSК, TIFR. А у более крутых AVR, таких как ATMega128, есть еще ETIFR и ETIMSK — своего рода продолжение, так как таймеров там поболее будет.

TIMSK это регистр масок. То есть биты, находящиеся в нем, локально разрешают прерывания. Если бит установлен, значит конкретное прерывание разрешено. Если бит в нуле, значит данное прерывание накрывается тазиком. По дефолту все биты в нуле.

На данный момент нас тут интересуют только прерывания по переполнению. За них отвечают биты

  • TOIE0 — разрешение на прерывание по переполнению таймера 0
  • TOIE1 — разрешение на прерывание по переполнению таймера 1
  • TOIE2 — разрешение на прерывание по переполнению таймера 2

О остальных фичах и прерываниях таймера мы поговорим попозжа, когда будем разбирать ШИМ.

Регистр TIFR это непосредственно флаговый регистр. Когда какое то прерывание срабатывает, то выскакивает там флаг, что у нас есть прерывание. Этот флаг сбрасывается аппаратно когда программа уходит по вектору. Если прерывания запрещены, то флаг так и будет стоять до тех пор пока прерывания не разрешат и программа не уйдет на прерывание.

Чтобы этого не произошло флаг можно сбросить вручную. Для этого в регистре TIFR в него нужно записать 1!

А теперь похимичим
Ну перекроим программу на работу с таймером. Введем программный таймер. Шарманка так и останется, пускай тикает. А мы добавим вторую переменную, тоже на четыре байта:

ORG $010 RETI ; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RJMP Timer0_OV ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete

Добавим обработчик прерывания по переполнению таймера 0, в секцию Interrupt. Так как наш тикающий макрос активно работает с регистрами и портит флаги, то надо это дело все сохранить в стеке сначала:

Кстати, давайте создадим еще один макрос, пихающий в стек флаговый регистр SREG и второй — достающий его оттуда.

1 2 3 4 5 6 7 8 9 10 11 12 .MACRO PUSHF PUSH R16 IN R16,SREG PUSH R16 .ENDM .MACRO POPF POP R16 OUT SREG,R16 POP R16 .ENDM

MACRO PUSHF PUSH R16 IN R16,SREG PUSH R16 .ENDM .MACRO POPF POP R16 OUT SREG,R16 POP R16 .ENDM

Как побочный эффект он еще сохраняет и R16, помним об этом:)

1 2 3 4 5 6 7 8 9 10 11 12 13 Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI

Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI

Теперь инициализация таймера. Добавь ее в секцию инита локальной периферии (Internal Hardware Init).

; Internal Hardware Init ====================================== SETB DDRD,4,R16 ; DDRD.4 = 1 SETB DDRD,5,R16 ; DDRD.5 = 1 SETB DDRD,7,R16 ; DDRD.7 = 1 SETB PORTD,6,R16 ; Вывод PD6 на вход с подтягом CLRB DDRD,6,R16 ; Чтобы считать кнопку SETB TIMSK,TOIE0,R16 ; Разрешаем прерывание таймера OUTI TCCR0,1<

Осталось переписать наш блок сравнения и пересчитать число. Теперь все просто, один тик один такт. Без всяких заморочек с разной длиной кода. Для одной секунды на 8Мгц должно быть сделано 8 миллионов тиков. В хексах это 7A 12 00 с учетом, что младший байт у нас TCNT0, то на наш счетчик остается 7А 12 ну и еще старшие два байта 00 00, их можно не проверять. Маскировать не нужно, таймер мы потом переустановим все равно.

Одна только проблема — младший байт, тот что в таймере. Он тикает каждый такт и проверить его на соответствие будет почти невозможно. Т.к. малейшее несовпадение и условие сравнение выпадет в NoMatch, а подгадать так, чтобы проверка его значения совпала именно с этим тактом… Проще иголку из стога сена вытащить с первой попытки наугад.

Так что точность и в этом случае ограничена — надо успеть проверить значение до того как оно уйдет из диапазона. В данном случае диапазон будет, для простоты, 255 — величина младшего байта, того, что в таймере.

Тогда наша секунда обеспечивается с точностью 8000 000 плюс минус 256 тактов. Не велика погрешность, всего 0,003%.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 ; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: LDS R16,TCNT ; Грузим числа в регистры LDS R17,TCNT+1 CPI R16,0x12 ; Сравниванем побайтно. Первый байт BRCS NoMatch ; Если меньше -- значит не натикало. CPI R17,0x7A ; Второй байт BRCS NoMatch ; Если меньше -- значит не натикало. ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Теперь надо обнулить счетчик, иначе за эту же итерацию главного цикла; мы сюда попадем еще не один раз -- таймер то не успеет натикать 255 значений, ; чтобы число в первых двух байтах счетчика изменилось и условие сработает. ; Конечно, можно обойти это доп флажком, но проще сбросить счетчик:) CLR R16 ; Нам нужен ноль CLI ; Доступ к многобайтной переменной; одновременно из прерывания и фона; нужен атомарный доступ. Запрет прерываний OUTU TCNT0,R16 ; Ноль в счетный регистр таймера STS TCNT,R16 ; Ноль в первый байт счетчика в RAM STS TCNT+1,R16 ; Ноль в второй байт счетчика в RAM STS TCNT+2,R16 ; Ноль в третий байт счетчика в RAM STS TCNT+3,R16 ; Ноль в первый байт счетчика в RAM SEI ; Разрешаем прерывания снова. ; Не совпало - не делаем:) NoMatch: NOP INCM CCNT ; Счетчик циклов по тикает; Пускай, хоть и не используется. JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: LDS R16,TCNT ; Грузим числа в регистры LDS R17,TCNT+1 CPI R16,0x12 ; Сравниванем побайтно. Первый байт BRCS NoMatch ; Если меньше -- значит не натикало. CPI R17,0x7A ; Второй байт BRCS NoMatch ; Если меньше -- значит не натикало. ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Теперь надо обнулить счетчик, иначе за эту же итерацию главного цикла; мы сюда попадем еще не один раз -- таймер то не успеет натикать 255 значений, ; чтобы число в первых двух байтах счетчика изменилось и условие сработает. ; Конечно, можно обойти это доп флажком, но проще сбросить счетчик:) CLR R16 ; Нам нужен ноль CLI ; Доступ к многобайтной переменной; одновременно из прерывания и фона; нужен атомарный доступ. Запрет прерываний OUTU TCNT0,R16 ; Ноль в счетный регистр таймера STS TCNT,R16 ; Ноль в первый байт счетчика в RAM STS TCNT+1,R16 ; Ноль в второй байт счетчика в RAM STS TCNT+2,R16 ; Ноль в третий байт счетчика в RAM STS TCNT+3,R16 ; Ноль в первый байт счетчика в RAM SEI ; Разрешаем прерывания снова. ; Не совпало - не делаем:) NoMatch: NOP INCM CCNT ; Счетчик циклов по тикает; Пускай, хоть и не используется. JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

Вот как это выглядит в работе

А если надо будет помигать вторым диодиком с другим периодом, то мы смело можем влепить в программу еще одну переменную, а в обработчике прерывания таймера инкрементировать сразу две пееременных. Проверяя их по очереди в главном цикле программы.

Можно еще немного оптимизировать процесс проверки. Сделать его более быстрым.

Надо только сделать счет не на повышение, а на понижение. Т.е. загружаем в переменную число и начинаем его декрементировать в прерывании. И там же, в обработчике, проверяем его на ноль. Если ноль, то выставляем в памяти флажок. А наша фоновая программа этот флажок ловит и запускает экшн, попутно переустанавливая выдержку.

А что если надо точней? Ну тут вариант только один — заюзать обработку события прям в обработчике прерывания, а значение в TCNT:TCNT0 каждый раз подстраивать так, чтобы прерывание происходило точно в нужное время.

Таймеры — еще один классический модуль, присутствующий практически во всех МК. Он позволяет решать множество задач, самая распространная из которых — задание стабильных временных интервалов. Второе по популярности применение — генерация ШИМ (о нем далее) на выводе МК. Несмотря на то, что, как уже было сказано, применение таймеров отнюдь не ограничивается этими задачами, здесь будут рассмотрены только эти две как наиболее распространенные.

Сам по себе таймер представляет собой двоичный счетчик, подключенный к системе тактирования микроконтроллера через дополнительный делитель. К нему, в свою очередь, подключены блоки сравнения (их может быть много), которые способны выполнять разные полезные функции и генерировать прерывания — в зависимости от настроек. Упрощенно устройство таймера можно представить следующим образом:

Настройка таймера, как и всей остальной периферии, производится через его регистры.

Генерация временных интервалов с помощью таймера.

Прерывания.

Из названия явствует, что главным назначением блоков сравнения является постоянное сравнение текущего значения таймера со значением, заданным в регистре OCRnX. Уже упоминалось, что имена регистров часто несут в себе глубокий сакральный смысл — и регистры сравнения не являются исключением. Так, n обозначает номер таймера, X — букву (тоже способ нумерации, блоков сравнения может быть много) регистра сравнения. Таким образом, OCR1A можно понять как O utput C ompare R egister of 1 st timer, unit A . К слову, искушенному эмбеддеру это даcт возможность предположить, что, возможно, существует таймер 0 и регистр сравнения B…

Итак, блоки сравнения могут генерировать прерывания при каждом совпадении значения таймера (к слову, оно находится в регистре TCNTn T imer/C ouNT er #n ) с заданым числом. Читателю уже должно быть знакомо понятие прерывания, однако на всякий случай освежим его в памяти, а заодно и поговорим о том, как его описать на С. Так вот, вышесказанное значит, что, как только случится описанное событие, процессор сохранит номер текущей команды в стеке и перейдет к выполению специально определенного кода, а после вернется обратно. Все происходит почти так же, как и при вызове обычной функции, только вызывается она на аппаратном уровне. Объявляются такие функции с помощью макроса, объявленного в avr/interrupt.h (ISR — «I nterrupt S ervice R outine», «обработчик прерывания»):

ISR (< имя вектора прерывания> ) { /*код обработчка прерывания*/ }

Каждому прерыванию (естесственно, их много) соответствует т.н. вектор прерывания — константа, также объявленная в avr/interrupt. Например, обработчик прерывания по совпадению значения таймера со значением регистра OCR1A будет иметь следующий вид:

ISR (TIMER1_COMPA_vect) { /*код обработчика*/ }

Несомненно, проницательный читатель уже догадался, каким образом формируются имена векторов. Тем не менее, полный список этих констант можно посмотреть в документации на avr-libc (библиотека стандартных функций для AVR-GCC).

Даташит (от англ. datasheet) — файл технической документации, описание конкретного прибора (микросхемы, транзистора и т.д.). Содержит всю информацию о характеристиках и применении компонента. Почти всегда имеет формат PDF. Обычно гуглится как «<название компонента> pdf».

Последние три бита управляют предделителем, упомянутым в самом начале (остальные же нас пока не интересуют):

Сконфигурируем таймер так, чтобы прерывания происходили два раза в секунду. Выберем предделитель 64; для этого установим биты CS11 и CS10:

TCCR1B= (1 < < CS11) | (1 < < CS10) ;

Тогда частота счета составит 8МГц/64=125КГц, т.е. каждые 8мкС к значению TCNT1 будет прибавляться единица. Мы хотим, чтобы прерывания происходили с периодом 500мС. Очевидно, что за это время таймер досчитает до значения 500мС/8мкС=62500, или 0xF424. Таймер 1 — шестнадцатибитный, так что все в порядке.

OCR1A= 0xF424 ;

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

Осталось только разрешить прерывание по совпадению — за него отвечает бит в регистре TIMSK1:

Про него написано следующее:

Итак, устанавливаем нужное значение:

TIMSK1= (1 < < OCIE1A) ;

Кроме того, следует помнить, что перед использованием прерываний необходимо их глобально разрешить вызовом функции sei() . Для глобального запрета прерываний служит функция cli() . Эти функции устанавливают/очищают бит I в регистре SREG , управляя самой возможностью использования такого механизма, как прерывания. Регистры же вроде TIMSKn — не более чем локальные настройки конкретного модуля.

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

Итак, программу, мигающую светодиодом, с использованием прерываний можно переписать следующим образом:

# include < avr/io.h > # include < avr/interrupt.h > ISR (TIMER1_COMPA_vect) { TCNT1= 0 ; if (PORTB & (1 < < PB0) ) PORTB& = ~ (1 < < PB0) ; else PORTB| = (1 < < PB0) ; } void main (void ) { DDRB= 0xFF ; PORTB= 0 ; OCR1A= 0xF424 ; TIMSK1= (1 < < OCIE1A) ; TCCR1B= (1 < < CS11) | (1 < < CS10) ; sei() ; while (1 ) ; }

Видно, что теперь в промежутках между переключениями светодиодов процессор абсолютно свободен для выполнения других задач, в то время как в первом примере он был занят бесполезным подсчетом тактов (функции _delay_xx() работают именно так). Таким образом, прерывания позволяют организовать примитивную многозадачность.

Генерация ШИМ с помощью таймера.

При определеных настройках блоки сравнения позволяют организовать аппаратную генерацию ШИМ-сигнала на ножках МК, обозначенных как OСnX:

ШИМ (PWM) — Ш иротно-И мпульсная М одуляция (P ulse W idth M odulation). ШИМ-сигнал представляет собой последовательность прямоугольных импульсов с изменяющейся длительностью:

Для ШИМ вводятся две родственные характеристики — коэффициент заполнения (duty cycle, D) и скважность — величина, обратная коэффицинту заполнения. Коэффициент заполнения представляет собой отношение времени импульса к длительности периода:

Коэффициент заполнения часто выражается в процентах, но так же распространена запись в десятичных дробях.

Значение ШИМ для народного хозяйства заключается в том, что действующее значение напряжения такого сигнала прямо пропоционально коэффициенту заполнения:

— с этим интегралом пособие смотрится солиднее; зависимость же выражается следующей формулой:

U avg — среднее значение напряжения (тут — оно же действующее);
D — коэффициент заполнения;
U p-p — амплитуда импульса.

Таким образом, ШИМ является простым способом получить аналоговый сигнал с помощью микроконтроллера — для этого такую последовательность импульсов надо подать на фильтр низких частот (который, кстати, и является физическим воплощением интеграла, записанного выше).

Наиболее употребительным режимом ШИМ является т.н. Fast PWM (об остальных режимах можно прочесть непосредственно в документации), поэтому рассмотрим его. В этом случае блоки сравнения работают следующим образом: с обнулением таймера на выход OCnX подается высокий уровень; как только таймер досчитает до числа, записанного в OCRnX, OCnX переводится в состояние низкого уровня. Все это повторяется с периодом переполнения счетчика. Получается, что ширина выходного импульса зависит от значения OCRnX, а выходная частота равна тактовой частоте таймера, поделенной на его максимальное значение. Рисунок из даташита поясняет сказанное:

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

Настройка блока сравнения для генерации ШИМ.

Здесь нам опять поможет документация. Итак, сначала надо перевести блок сравнения в режим генерации ШИМ и выбрать интересующий выход из доступных. Эти настройки доступны в регистре TCCR0A:

Нас интересуют биты WGMxx и COMnXn. Про них сказано следующее:

Т.е., нас интересуют биты WGM00 и WGM01 — Fast PWM mode,

а также COM0A1 — non-inverting PWM на выводе OC0A. Настраиваем:

TCCR0A= (1 < < COM0A1) | (1 < < WGM01) | (1 < < WGM00) ;

Естесственно, кроме этого выбранная ножка должна быть настроена на выход с помощью регистра DDR соответствующего порта.

OCR0A= 128 ;

И, наконец, включить таймер, выбрав делитель. Тут все так же:


Обычно для ШИМ выбирается максимально возможная частота (для того, чтобы получить максимальное качество выходного сигнала). Т.е., целесообразно установить минимальное значение делителя:

TCCR0B= (1 < < CS00) ;

На этом этапе настройка ШИМ завершается, и на выбранной ножке можно увидеть сигнал.

Как упомянуто выше, ШИМ — простой способ получения аналогового сигнала с помощью МК. Например, можно организовать плавное мигание светодиода (в этом случае роль интегратора-ФНЧ выполняет глаз наблюдателя, так что светодиод можно подключить к ножке МК через обычный резистор).

Некоторые моменты в предлагаемом примере требуют пояснения.

В списке включаемых файлов присутствует загадочный stdint.h — в этом файле объявлены типы с явно указанной разрядностью, например

uint8_t u nsigned 8 -bit int eger t ype
uint16_t u nsigned 16 -bit int eger t ype
uint32_t u nsigned 32 -bit int eger t ype
int8_t — signed 8 -bit int eger t ype

и так далее. Такие типы способствуют единообразию и удобочитаемости программы. Кроме того, гарантируется, что при портировании кода разрядность данных останется указанной. И, кстати, uint8_t писать гораздо быстрее, чем unsigned char.

Модификатор volatile означает, что компилятору запрещается оптимизировать данную переменную. Например, если скомпилировать следующий пример:

void main (void ) { unsigned char i= 0 ; while (1 ) { i+ + ; } }

после чего изучить дизассемблированный код, можно обнаружить, что на самом деле никакой переменной создано не было, и программа представляет собой пустой цикл. Это произошло потому, что оптимизатор посчитал переменную неиспользуемой, и не включил ее в результирующий код. Если бы подобным образом объявленная переменная использовалась, например, в прерывании, такая вольность оптимизатора вызвала бы некорректную работу программы. Применение volatile исключает такое поведение.

#include #include #include volatile uint8_t pwm_value= 0 , dn_count= 0 ; ISR (TIMER1_COMPA_vect) { TCNT1= 0 ; if (dn_count) //плавно меняем яркость диода, по шагу за раз pwm_value--; else pwm_value++; if (pwm_value== 0 ) //проверка границ, переключение разгорание/затухание dn_count= 0 ; if (pwm_value== 0xFF ) dn_count= 1 ; OCR0A= pwm_value; //устанавливаем новый коэфф. заполнения } void main(void ) { DDRD= 0xFF ; //настройка порта на выход PORTD= 0 ; OCR1A= 0xF424 ; //константа, определяющая частоту прерываний TIMSK1= (1 << OCIE1A) ; //разрешаем прерывание по совпадению канала А TCCR1B= (1 << CS11) | (1 << CS10) ; //запускаем таймер 1 TCCR0A= (1 << COM0A1) | (1 << WGM01) | (1 << WGM00) ; //таймер 0 будет генерировать ШИМ OCR0A= 128 ; //начальное значение ШИМ TCCR0B= (1 << CS00) ; //запускаем таймер 0 sei() ; //разрешаем прерывания while (1 ) ; //все, дальше процесс идет на прерываниях и аппаратном ШИМе }