Таймеры atmega 16. Прерывания в atmega8

  • Дата: 23.01.2024

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

За основу мы возьмем очень популярную среди разработчиков устройств на МК книгу, автором которой является А.В. Евстифеев. По ссылкам в конце статьи Вы сможете найти проект в и проект в . В этой статье мы разберем работу 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 для работы с дисплеем написана участником форума и предоставлена с его разрешения.

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

Для чего это надо?

Отсчитывать время программно, в теле основной программы - не самый лучший способ. Чтобы отсчитать секунду, программа только и будет делать, что считать эту самую секунду.

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

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

Лучше всего доверить отсчёт минимального интервала времени таймеру с компаратором. То есть таймеру с предустановкой числа для отсчёта.

Создаём проект в MPLAB IDE с таймером TMR2

Выберем контроллер 16F628A. Скачаем на него «даташит» (Data Sheet). В нём имеется таймер с компаратором TMR2. Создадим в MPLAB-е проект. Шаблон найдёте у себя на системном диске, например:

C:\Program Files\Microchip\MPASM Suite\Template\Code\16F628ATEMP.ASM

Лишние комментарии в шаблоне можно убрать и добавить свои.

Для создания проекта лучше воспользоваться Мастером проектов в меню MPLAB / Project / Project Wizard…

Попробуем скомпилировать то, что получилось.

Так, MPLAB ругается на свой же шаблон!!!??? Проверим в файле P16F628A.INC как должно быть.

Найти его можно там же, в каталоге Microchip:

C:\Program Files\Microchip\MPASM Suite\ P16F628A.INC

Можно скопировать этот файл, на всякий случай, в свой проект, пригодится.

Посмотрим как там записано и поправим в шаблоне:

CONFIG _CP_OFF & _DATA_CP_OFF

На

CONFIG _CP_OFF & DATA_CP_OFF

Разница небольшая, но существенная. Теперь всё нормально, компилируется.

В программировании мелочей не бывает. Так что не верьте всему, что пишут, проверяйте:-)

Включимвменюсимулятор Debugger / Select Tool / MPLAB SIM

В Debugger / Settings… выберемчастотукварца 4 MHz

Там в же в Debugger берём секундомер Stopwatch

В начале программы main настроим контроллер.

Перед таймером установим предделитель:4 и число.249 в регистр предустановки.

Теперь у нас: 4 Mhz / 4 = 1 машинный цикл = 1 микросекунда.

Умножаем на предделитель 4 и на предустановленное число 250 (от 0 до 249), получаем = 1 миллисекунда.

В начале подпрограммы прерывания, начинающегося с ORG 0x004 , добавим проверку на прерывание от TMR2, чтобы не путать с другими прерываниями. У нас пока других прерываний нет, но, возможно, потом появятся. Так что лучше сделать сразу:

Bcf PIR1,TMR2IF ; Сброс прерывания по таймеру TMR2.

; И сразу ставим метку, двойным щелчком на строке со сбросом прерывания от TMR2:

Компилируем программу и запускаем симулятор. Программа остановилась на нашей метке.

Сбрасываем секундомер и снова запускаем симулятор.

На этот раз на секундомере показывает 1000 МЦ (машинных циклов), а ниже 1 миллисекунда.

То что нам надо. Прерывания происходят точно через равные промежутки времени в 1 миллисекунду.

Что мы имеем.

Произошло событие, контроллер отсчитал 1 мсек. Так и озаглавим эти строчки:

Event_Time_1ms

Btfss PIR1,TMR2IF ; Проверка прерывания от TMR2.

Goto other_interrupts ; иначе переходим на проверку других прерываний.

Bcf PIR1,TMR2IF ; Сброс прерывания по таймеру.

Всё что ниже, будет происходить с периодичностью в 1 мсек.

Здесь можно поднимать флаги, с этой периодичностью, для подпрограмм, которым нужны такие флаги.

Эти подпрограммы смогут совершать действия каждую миллисекунду или отсчитывать любое необходимое

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

каждые 17 миллисекунд.

В прерывании удобнее будет размещать счётчики кратные каким то стандартным или удобным интервалам

времени. Например: 10 мсек, 0,1 сек, 1 сек, 1 мин, 1 час и т.д.

Впрочем, при желании счётчик на те же 17 миллисекунд можно добавить тут же, в прерывании.

Добавляем счётчики таймеров

Для каждого интервала времени потребуется регистр для счёта:

Reg_Time_10ms ; Регистры счётчиков времени.

Reg_Time_01sec

Reg_Time_1sec

Reg_Time_1min

Reg_Time_1hour

Reg_Time_1day

В эти регистры будем загружать константы. Назовём их соответствующим образом.

#Define TIME_10ms .10 ; Константы для регистров счётчиков времени.

#Define TIME_01sec .10 ;

#Define TIME_1sec .10 ;

#Define TIME_1min .60 ;

#Define TIME_1hour .60 ;

#Define TIME_1day .24 ;

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

Ограничимся днями, т.к. они все одинаковые, по 24 часа. Недели тоже одинаковые, но неделями редко считают,

разве что беременность:-).

С месяцами сложнее, там количество дней разное, счёт усложнится и для примера не подходит. Так что далее,

проще использовать микросхемы реального времени типа PCF8583 и т.п.

Счетчики могут быть с другими интервалами, не 10 ms, а 100 ms например. Это как вам удобно.

Смотрите прилагаемый файл Time_intervals1.asm

Счётчики запишем в таком виде:

Event_Time_10ms

Decfsz reg_Time_10ms,F ; Вычитаем 1, если не ноль, пропускаем следующую инструкцию.

Goto other_interrupts ; иначе переходим на проверку других прерываний.

Movlw TIME_10ms ; Константу загружаем

Movwf reg_Time_10ms ; обратно в регистр.

Все остальные счётчики - такие же.

Теперь, если выставить точку останова в конце любого счётчика (ставьте точки останова только по одной),

программа будет останавливаться там

с соответствующей периодичностью.

Как использовать программные счётчики?

Создадим в Proteus-е модель со светодиодами и помигаем ими. Смотрите файл Time_intervals1.DSN

Можно конечно переключать пины порта прямо в прерывании, но сделаем по-другому.

Выделим ещё один регистр для индикатора и назовём его indicator.

Индикатор будет переключаться только по необходимости.

Пишем подпрограмму LED_Indicator.

В начале подпрограммы проверяется флаг события EV_indicator и подпрограмма продолжит выполнение,

только если это событие было, и флаг был поднят.

Организуем переключение светодиода один раз в секунду. Для этого выставим флаг LED_1sec после

прерывания в 1 сек.

Это событие надо обработать. Напишем ещё одну программу Switch_ LED_1sec

Как и предыдущая подпрограмма, она будет проверять флаг своего события EV_ LED_1sec.

Переключим светодиод на индикаторе маской LED_1sec.

Movlw LED_1sec ; Переключим светодиод LED_1sec

Xorwf indicator,F ; на индикаторе.

В конце подпрограммы поднимем флаг события для индикатора EV_indicator.

Что если светодиод надо переключать не каждую секунду, а с частотой в 1 сек, т.е. переключать каждые 0,5 сек? Выделяем регистр и делаем врезку для отдельного счётчика. Тактовую частоту можно выбрать 0,1 сек, умножив на 5, или 0,01 сек, умножив на 50, её и возьмём. Константа у нас получается 50. Счётчик располагаем в месте поднятия флага 10 мсек. В конце счётчика как всегда поднимаем флаг. Да, и не забудьте про предустановку в начале программы.

Event_Time_05sec

Decfsz reg_Time_05sec,F ; Вычитаем 1, если не ноль, пропускаем следующую инструкцию

Goto Event_Time_01sec ; иначе переходим к следующему счётчику.

Movlw TIME_05sec

Movwf reg_Time_05sec

Bsf event,EV_LED_05sec ; Поднимаем этот флаг события раз в 0,5 сек.

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

Почему для каждого светодиода отдельная подпрограмма? Это для примера. События у нас разные, а к этому выводу может быть подключен не светодиод, а насос или вентилятор, сигнализация или нагреватель. И всё будет в программе подключаться отдельно, и работать независимо, не мешая друг другу. Ничто вам не мешает подключить блок светодиодов и выделить для этого блока только одну подпрограмму. Блок этот может содержать сотни светодиодов, а управляться будет по 2-3 проводам одной подпрограммой, которая будет вызываться всё тем же одним флагом. Я ещё не рассказал, как сделать задержки? Точно так же. Можно выделить один или несколько регистров, в зависимости от того, какие интервалы времени вам надо отсчитывать, и с какой точностью. Если на входе счётчика такты в 1 мсек, то и точность будет соответственная. Если точность нужна больше, то считайте машинные циклы. Как делать врезки, мы уже знаем. А запускается счётчик элементарно. Загружаете константы в счётчик и сбрасываете флаг. В конце счёта флаг подымется.

Подведём итог Кажется, что получилось слишком много кода. На самом деле это не так. Некоторые строки написаны про запас, для отсчёта долей секунд, для минут, часов и дней. Пока они не используются, и вы можете их удалить, или использовать в своей программе. То же, что используется, выполняется точно в определённое время и очень быстро. Например, переключатель светодиода срабатывает только раз в секунду. Подпрограмма индикатора тоже срабатывает по мере необходимости. Для примера я сделал ещё одну программку (см. в папке Flasher101). Там 8 таймеров переключают 8 светодиодов. Первый светодиод мигает раз в секунду, а каждый следующий - на 1% дольше. То есть через 1% х 100 включений, они опять мигают вместе. Получаются интересные визуальные эффекты. И ещё один таймер выключает всю эту мигалку через 5 минут. Получилось просто, точно и эффективно.

С обычными задержками программы это было бы сложно сделать, запутаешься. А добавить в такую программу что то ещё, вообще было бы невозможно, не меняя предыдущий алгоритм. Кроме того, подпрограмму индикатора можно вызывать не из других подпрограмм, а по времени. Например, каждые 10 мсек или чаще. Это бывает необходимо при динамической индикации. А можно наоборот, вызывать раз в секунду, чтобы показания не мельтешили, и их было удобно считывать. Это бывает необходимо при быстро меняющихся параметрах выводимых на индикацию. Так же можно объединить динамическую индикацию и смену текущих показаний раз в секунду. То есть - имеем временные промежутки в любых сочетаниях. И не только для индикации, а и для сканирования клавиатуры, энкодера, работы с датчиками и т.п. И при всём при этом, у контроллера останется куча машинного времени на другие нужды и вычисления. А подпрограммы, тщательно отлаженные под работу с флагами, будут легко переноситься в другие проекты.

Всем удачи! Исходные файлы, проекты в Протеусе и другие материалы к данной статье можно взять

Урок 10

Таймеры-счетчики. Прерывания

Сегодня мы узнаем, что такое таймеры-счётчики в микроконтроллерах и для чего они нужны, а также что такое прерывания и для чего они тоже нужны.

Таймеры-счётчики — это такие устройства или модули в микроконтроллере, которые, как видно из названия, постоянно что-то считают. Считают они либо до определённой величины, либо до такой величины, сколько они битности. Считают они постоянно с одной скоростью, со скоростью тактовой частоты микроконтроллера, поправленной на делители частоты, которые мы будем конфигурировать в определённых регистрах.

И вот эти таймеры-счётчики постоянно считают, если мы их инициализируем.

Таймеров в МК Atmega8 три.

Два из них — это восьмибитные таймеры, то есть такие, которые могут максимально досчитать только до 255. Данной величины нам будет маловато. Даже если мы применим максимальный делитель частоты, то мы не то что секунду не отсчитаем, мы даже полсекунды не сможем посчитать. А у нас задача именно такая, чтобы досчитывать до 1 секунды, чтобы управлять наращиванием счёта светодиодного индикатора. Можно конечно применить ещё наращивание переменной до определенной величины, но хотелось бы полностью аппаратного счёта.

Но есть ещё один таймер — это полноправный 16-битный таймер. Он не только 16-битный , но есть в нём ещё определённые прелести, которых нет у других таймеров. С данными опциями мы познакомимся позже.

Вот этот 16-битный таймер мы и будем сегодня изучать и использовать. Также, познакомившись с данным таймером, вам ничего не будет стоить самостоятельно изучить работу двух других, так как они значительно проще. Но тем не менее 8-битные таймеры в дальнейшем мы также будем рассматривать, так как для достижения более сложных задач нам одного таймера будет недостаточно.

Теперь коротко о прерываниях.

Прерывания (Interrupts ) — это такие механизмы, которые прерывают код в зависимости от определённых условий или определённой обстановки, которые будут диктовать некоторые устройства, модули и шины, находящиеся в микроконтроллере.

В нашем контроллере Atmega8 существует 19 видов прерываний. Вот они все находятся в таблице в технической документации на контроллер

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

На данный момент мы будем обрабатывать прерывание, которое находится в таблице, размещённой выше на 7 позиции — TIMER1 COMPA , вызываемое по адресу 0x006.

Теперь давайте рассмотрим наш 16-битный таймер или TIMER1 .

Вот его структурная схема

Мы видим там регистр TCNTn , в котором постоянно меняется число, то есть оно постоянно наращивается. Практически это и есть счётчик. То есть данный регистр и хранит число, до которого и досчитал таймер.

А в регистры OCRnA и OCRnB (буквы n — это номер таймера, в нашем случае будет 1) — это регистры, в которые мы заносим число, с которым будет сравниваться чило в регистре TCNTn.

Например, занесли мы какое-нибудь число в регистр OCRnA и как только данное число совпало со значением в регистре счёта, то возникнет прерывание и мы его сможем обработать. Таймеры с прерываниями очень похожи на обычную задержку в коде, только когда мы находимся в задержке, то мы в это время не можем выполнять никакой код (ну опять же образно "мы", на самом деле АЛУ). А когда считает таймер, то весь код нашей программы в это время спокойно выполняется. Так что мы выигрываем колоссально, не давая простаивать огромным ресурсам контроллера по секунде или даже по полсекунды. В это время мы можем обрабатывать нажатия кнопок, которые мы также можем обрабатывать в таймере и многое другое.

Есть также регистр TCCR. Данный регистр — это регистр управления. Там настраиваются определенные биты, отвечающие за конфигурацию таймера.

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

Он состоит из двух половинок, так как у нас конотроллер 8-битный и в нем не может быть 16-битных регистров. Поэтому в одной половинке регистра (а физически в одном регистре) хранится старшая часть регистра, а в другом — младшая. Можно также назвать это регистровой парой, состоящей из двух отдельных регистров TCCR1A и TCCR1B. Цифра 1 означает то, что регистр принадлежит именно таймеру 1.

Даный регист TCCR отвечает за установку делителя, чтобы таймер не так быстро считал, также он отвечает (вернее его определённые биты) за установку определённого режима.

За установку режима отвечают биты WGM

Мы видим здесь очень много разновидностей режимов.

Normal — это обычный режим, таймер считает до конца.

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

CTC — это сброс по совпадению, как раз то что нам будет нужно. Здесь то и сравнивются регистры TCNT и OCR. Таких режима два, нам нужен первый, второй работает с другим регистром.

Все разновидности режимов мы в данном занятии изучать не будем. Когда нам эти режимы потребуются, тогда и разберёмся.

Ну давайте не будем томить себя документацией и наконец-то попробуем что-то в какие-нибудь регистры занести.

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

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

Добавим ещё одну функцию, благо добавлять функции мы на прошлом занятии научились. Код функции разместим после функции segchar и до функции main. После из-за того, что мы будем внутри нашей новой функции вызывать функцию segchar.

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

Поэтому первую функцию мы назвовём timer_ini

//———————————————

void timer_ini ( void )

{

}

//———————————————

Также давайте наши функции, а также какие-то законченные блоки с объявлением глобальных переменных, с прототипами функций будем отделять друг от друга вот такими чёрточками, которые за счет наличия двух слешей впереди компилятор обрабатывать не будет и примет их за комментарии. За счёт этих отчерчиваний мы будем видеть, где заканчивается одна функция и начинается другая.

Данная функция, как мы видим не имеет ни каких аргументов — ни входных, не возвращаемых. Давайте сразу данную функцию вызовем в функции main()

unsigned char butcount=0, butstate=0;

timer_ini ();

Теперь мы данную функцию начнём потихонечку наполнять кодом.

Начнем с регистра управления таймером, например с TCCR1B. Используя нашу любимую операцию "ИЛИ", мы в определённый бит регистра занесём единичку

void timer_ini ( void )

TCCR1B |= (1<< WGM12 );

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

Также у таймера существует ещё вот такой регистр — TIMSK . Данный регистр отвечает за маски прерываний — Interrupt Mask . Доступен данный регистр для всех таймеров, не только для первого, он общий. В данном регистре мы установим бит OCIE1A , который включит нужный нам тип прерывания TIMER1 COMPA

TCCR1B |= (1<< WGM12 ); // устанавливаем режим СТС (сброс по совпадению)

TIMSK |= (1<< OCIE1A );

Теперь давайте поиграемся с самими регистрами сравнения OCR1A(H и L) . Для этого придётся немного посчитать. Регистр OCR1AH хранит старшую часть числа для сравнения, а регистр OCR1AL — младшую.

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

TIMSK |= (1<< OCIE1A ); //устанавливаем бит разрешения прерывания 1ого счетчика по совпадению с OCR1A(H и L)

OCR1AH = 0b10000000;

OCR1AL = 0b00000000;

TCCR1B |= ( ); //установим делитель.

Пока никакой делитель не устанавливаем, так как мы его ещё не посчитали. Давайте мы этим и займёмся.

Пока у нас в регистре OCR1A находится число 0b1000000000000000, что соответствует десятичному числу 32768.

Микроконтроллер у нас работает, как мы договорились, на частоте 8000000 Гц.

Разделим 8000000 на 32768, получим приблизительно 244,14. Вот с такой частотой в герцах и будет работать наш таймер, если мы не применим делитель. То есть цифры наши будут меняться 244 раза в секунду, поэтому мы их даже не увидим. Поэтому нужно будет применить делитель частоты таймера. Выберем делитель на 256. Он нам как раз подойдёт, а ровно до 1 Гц мы скорректируем затем числом сравнения.

Вот какие существуют делители для 1 таймера

Я выделил в таблице требуемый нам делитель. Мы видим, что нам требуется установить только бит CS12 .

Так как делитель частоты у нас 256, то на этот делитель мы поделим 8000000, получится 31250, вот такое вот мы и должны занести число в TCNT. До такого числа и будет считать наш таймер, чтобы досчитать до 1 секунды. Число 31250 — это в двоичном представлении 0b0111101000010010. Занесём данное число в регистровую пару, и также применим делитель

OCR1AH = 0b01111010 ; //записываем в регистр число для сравнения

OCR1AL = 0b00010010 ;

TCCR1B |= (1<< CS12 ); //установим делитель.

С данной функцией всё.

Теперь следующая функция — обработчик прерывания от таймера по совпадению. Пишется она вот так

ISR ( TIMER1_COMPA_vect )

{

}

И тело этой функции будет выполняться само по факту наступления совпадения чисел.

Нам нужна будет переменная. Объявим её глобально, в начале файла

#include

//———————————————

unsigned char i ;

//———————————————

Соответственно, из кода в функции main() мы такую же переменную уберём

int main ( void )

unsigned char i ;

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

while (1)

{

// for(i=0;i<10;i++)

// {

// while (butstate==0)

// {

// if (!(PINB&0b00000001))

// {

// if(butcount < 5)

// {

// butcount++;

// }

// else

// {

// i=0;

// butstate=1;

// }

// }

// else

// {

// if(butcount > 0)

// {

// butcount—;

// }

// else

// {

// butstate=1;

// }

// }

// }

// segchar(i);

// _delay_ms(500);

// butstate=0;

// }

Теперь, собственно, тело функции-обработчика. Здесь мы будем вызывать функцию segchar. Затем будем наращивать на 1 переменную i . И чтобы она не ушла за пределы однозначного числа, будем её обнулять при данном условии

ISR ( TIMER1_COMPA_vect )

if ( i >9) i =0;

segchar ( i );

i ++;

Теперь немного исправим код вначале функции main(). Порт D , отвечающий за состояние сегментов, забьём единичками, чтобы при включении у нас не светился индикатор, так как он с общим анодом. Затем мы здесь занесём число 0 в глобавльную переменную i, просто для порядка. Вообще, как правило, при старте в неициализированных переменных и так всегда нули. Но мы всё же проинициализируем её. И, самое главное, чтобы прерывание от таймера работало, её недостаточно включить в инициализации таймера. Также вообще для работы всех прерываний необходимо разрешить глобальные прерывания. Для этого существует специальная функция sei() — Set Interrupt .

Теперь код будет вот таким

DDRB = 0x00;

PORTD = 0b11111111 ;

PORTB = 0b00000001;

i =0;

sei ();

while (1)

Также ещё мы обязаны подключить файл библиотеки прерываний вначале файла

#include

#include

#include

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

int main ( void )

//unsigned char butcount=0, butstate=0;

timer_ini ();

Соберём наш код и проверим его работоспособность сначала в протеусе. Если всё нормально работает, то проверим также в живой схеме

Всё у нас работает. Отлично!

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

Тем не менее сегодня мы с вами много чему научились. Мы узнали о прерываниях, также научились их обрабатывать, Научились работать с таймерами, конфигурировать несколько новых регистров микроконтроллера, до этого мы работали только с регистрами портов. Также за счёт всего этого мы значительно разгрузили арифметическо-логическое устройство нашего микроконтроллера.

Смотреть ВИДЕОУРОК

Post Views: 17 413

В этом уроке мы поговорим о таймерах.

Данная тема непосредственно связана с темой тактирования микроконтроллера. Поэтому рекомендую перед прочтением данного урока ознакомиться с предыдущим.

Итак, зачем нам таймер?

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

Решить поставленные задачи помогают именно таймеры. Но таймеры микроконтроллеров AVR не знают что такое секунда, минута, час. Однако они прекрасно знают, что такое такт! Работают они именно благодаря наличию тактирования контроллера. То есть, таймер считает количество тактов контроллера, отмеряя тем самым промежутки времени. Допустим, контроллер работает при тактовой частоте 8МГц, то есть когда таймер досчитает до 8 000 000, пройдет одна секунда, досчитав до 16 000 000, пройдет 2 секунды и так далее.

Однако, тут возникает первое препятствие. Регистры то у нас 8 битные, то есть досчитать мы можем максимум до 255, а взяв 16 битный таймер, мы, досчитаем максимум до 65535. То есть за одну секунду мы должны обнулить таймер огромное количество раз! Конечно, можно заняться этим, если больше заняться нечем. Но ведь просто измерять время, используя мощный микроконтроллер совсем не интересно, хочется сделать нечто большее. Тут нам на помощь приходит предделитель. В общем виде это промежуточное звено между таймером и тактовой частотой контроллера. Предделитель облегчает нашу задачу позволяя поделить тактовую частоту на определенное число, перед подачей её на таймер. То есть установив предделитель на 8, за 1 секунду наш таймер досчитает до 1 000 000, вместо 8 000 000 (Разумеется, при частоте тактирования контроллера 8МГц). Уже интереснее, не так ли? А поделить мы можем и не только на 8, но и на 64 и даже на 1024.

Теперь настало время собрать схему, настроить наш таймер, предделитель, и сделать уже хоть что-то полезное!

А делать мы сегодня будем “бегущие огни” из светодиодов. То есть поочередно будем зажигать 3 светодиода, с периодом 0.75 секунды (То есть время работы одного светодиода 0.25 секунды). Соберем следующую схему:

Номиналы резисторов R 1-R 3 рассчитайте самостоятельно.

Далее, рассмотрим регистры отвечающие за работу таймеров. Всего AtMega 8 имеет в своем составе 3 таймера.Два 8 битных(Timer 0,Timer 2) и один 16 битный(Timer 1).Рассматривать будем на примере 16 битного таймера 1.

Пара регистров 8 битных регистров TCNT 1H и TCNT 1L , вместе образуют 16 битный регистр TCNT 1. Данный регистр открыт как для записи, так и для чтения. При работе таймера 1, значение данного регистра при каждом счете изменяется на единицу. То есть в регистре TCNT 1 записано число тактов, которые сосчитал таймер. Так же мы можем записать сюда любое число в диапазоне от 0 до 2 в 16 степени. В таком случае отсчет тактов будет вестись не с 0, а с записанного нами числа.

Регистр TIMSK отвечает за прерывания, генерируемые при работе таймеров микроконтроллера. Прерывание – обработчик специального сигнала, поступающего при изменении чего либо . Любое прерывания микроконтроллера может быть разрешено или запрещено. При возникновении разрешенного прерывания, ход основной программы прерывается, и происходит обработка данного сигнала. При возникновении запрещенного прерывания, ход программы не прерывается, а прерывание игнорируется. За разрешение прерывания переполнения счетного регистра TCNT 1 таймера 1 отвечает бит TOIE 1(Timer 1 Overflow Interrupt Enable ).При записи 1 в данный бит прерывание разрешено, а при записи 0 – запрещено. Данное прерывание генерируется таймером 1 при достижении максимального значения регистра TCNT 1. Подробнее о прерываниях поговорим в следующем уроке.

Регистр TCCR 1B отвечает за конфигурацию таймера 1. В данном случае битами CS 10-CS 12 мы задаем значение предделителя согласно следующей таблицы.

Остальные биты пока нас не интересуют.

Так же существует регистр TCCR 1A , который позволяет настроить другие режимы работы таймера, например ШИМ, но о них в отдельной статье.

А теперь код на C :

#define F_CPU 16000000UL #include #include uint8_t num=0; ISR(TIMER1_OVF_vect) { PORTD=(1<2) { num=0; } TCNT1=61630;//Начальное значение таймера } int main(void) { DDRD|=(1<

#define F_CPU 16000000UL

#include

#include

uint8_t num = ;

ISR (TIMER1_OVF_vect )

PORTD = (1 << num ) ;

num ++ ;

if (num > 2 )

num = ;

TCNT1 = 61630 ; //Начальное значение таймера

int main (void )

DDRD |= (1 << PD0 ) | (1 << PD1 ) | (1 << PD2 ) ;

TCCR1B |= (1 << CS12 ) | (1 << CS10 ) ; //Предделитель = 1024

TIMSK |= (1 << TOIE1 ) ; //Разрешить прерывание по переполнению таймера 1

TCNT1 = 61630 ; //Начальное значение таймера

sei () ; //Разрешить прерывания

while (1 )

//Основной цикл программы, он пуст, так как вся работа в прерывании

Код на ASM :

Assembly (x86)

Include "m8def.inc" rjmp start .org OVF1addr rjmp TIM1_OVF start: ldi R16,LOW(RamEnd) out SPL,R16 ldi R16,HIGH(RamEnd) out SPH,R16 ldi R16,1 ldi R17,0b00000111 out DDRD,R17 ldi R17,0b00000101 out TCCR1B,R17 ldi R17,0b11110000 out TCNT1H,R17 ldi R17,0b10111110 out TCNT1l,R17 ldi R17,0b00000100 out TIMSK,R17 sei main_loop: nop rjmp main_loop TIM1_OVF: out PORTD,R16 lsl R16 cpi R16,8 brlo label_1 ldi R16,1 label_1: ldi R17,0b10111110 out TCNT1L,R17 ldi R17,0b11110000 out TCNT1H,R17 reti

Include "m8def.inc"

Rjmp start

Org OVF 1addr

Rjmp TIM 1_ OVF

start :

Ldi R 16, LOW (RamEnd )

Out SPL , R 16

Ldi R 16, HIGH (RamEnd )

Out SPH , R 16

Ldi R 16, 1

Ldi R 17, 0b00000111

Out DDRD , R 17

Ldi R 17, 0b00000101

Out TCCR 1B , R 17

Ldi R 17, 0b11110000

Out TCNT 1H , R 17

Ldi R 17, 0b10111110

Прежде чем приступить к изучению таймера определимся с базовым понятием «частота». Простым языком, это количество повторений, в секунду. Это значит, что если вы за секунду хлопнете в ладошки 2 раза, то частота хлопков будет равна 2Гц. Если за 3 раза, значит 3Гц.

Каждый микроконтроллер работает на определенной частоте. Большинство инструкций выполняется за один такт, поэтому чем выше частота, тем быстрее работает микроконтроллер. Если нет источника тактирования, соответственно ничего работать не будет. На случай отсутствия внешнего источника тактирования, в большинстве микроконтроллеров имеется свой внутренний генератор. Обычно на него «с завода» настроены.

Частота внутреннего источника может изменяться («плавать») из за температуры и т.п., поэтому считается непригодным для серьезных проектов, а у нас ведь именно такие 🙂 Поэтому применяется стабильный источник внешней частоты — кварцевый резонатор (кварц). Один из вариантов исполнения кварцевого резонатора:

Теперь, кое что о таймере. Таймер работает на той же частоте, что и микроконтроллер. Иногда это может быть слишком быстро, поэтому используют предделитель который уменьшает количество тиков в 8/64/256/1024… раз. Включается это все программно.

Допустим, мы выбрали предделитель 1024, частота микроконтроллера 8 МГц, значит после предделителя частота таймера станет:
8 000 000 / 1024 = 7813 Гц — это частота, на которой работает наш таймер. По простому говоря, за одну секунду таймер тикнет 7813 раз.

К количеству тиков можно привязать выполнение кода. Эта фича есть не для всех таймеров, читайте документацию на свой камень. Допустим, нам нужно, чтобы раз в 0,5 секунды выполнялся наш код. За одну секунду 7813 тиков, за пол секунды в 2 раза меньше — 3906. Это значение вносится в регистр сравнения, и с каждым тиком проверяется достаточно ли оттикало или нет, как в будильнике, только очень быстро.

Но вот у нас совпали эти 2 значения и что дальше? Для этого существует такая полезная штука как прерывание по совпадению. Это значит, что при совпадении таймера и регистра сравнения, ваша текущая программа остановится. После этого выполнится участок кода, который абсолютно не связан с основной программой. Внутри этого участка вы можете писать что угодно и не беспокоиться о том, что он как то повлияет на программу, выполнится он только когда значение таймера совпадет с регистром сравнения.

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

Вот теперь мы готовы написать нашу программу. Поэтому создаем проект с помощью мастера проектов. Сразу прицепим LCD, мы же уже это умеем).

Переходим на вкладку Timers и тут остановимся поподробнее:

Выбираем частоту 7813 и устанавливаем галочку напротив пункта Interrupt on: Compare A Match. Таким образом мы указали, что при совпадении значения выполнять прерывание (то о чем было написано выше). Прерывание будем выполнять 1 раз в секунду, т.е. нам нужно тикнуть 7813 раз, поэтому переводим число 7813 в шестнадцатеричную систему и получим 1e85. Именно его и записываем в регистр сравнения Comp A. Регистр сравнения Comp A 16 битный, поэтому число больше 2^16=65536 мы записать не можем.

Генерим, сохраняем, вычищаем наш код. Появится новый непонятный кусок кода

// Timer 1 output compare A interrupt service routine
interrupt void timer1_compa_isr(void)
{

Это то самое прерывание. Именно внутри этих скобок мы можем писать тот код, который мы хотели бы выполнять через определенные промежутки времени. У нас это одна секунда. Итак логично создать переменную, которую мы будем увеличивать 1 раз в секунду, т.е. 1 раз за прерывание. Поэтому проинициализируем переменную int s =0; а в прерывании будем ее увеличивать от 0 до 59. Значение переменной выведем на жк дисплей. Никаких хитростей, все очень просто.
Получившийся код.

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 #include #asm .equ __lcd_port= 0x18 ; PORTB #endasm #include int s = 0 ; // переменная для хранения секунд // Обработка прерывания по совпадению interrupt [ TIM1_COMPA] void timer1_compa_isr(void ) { s++; // увеличиваем переменную каждую секунду if (s> 59 ) // обнуляем секунды после 59 { s= 0 ; } TCNT1= 0 ; //обнуляем таймер } void main(void ) { TCCR1A= 0x00 ; //настройка таймера TCCR1B= 0x05 ; TCNT1= 0x00 ; //здесь увеличиваются тики OCR1A= 0x1E85 ; //записываем число в регистр сравнения TIMSK= 0x10 ; //запускаем таймер lcd_init(8 ) ; #asm("sei") while (1 ) { lcd_gotoxy(0 , 0 ) ; //вывод в 0 координате X и Y lcd_putchar(s/ 10 + 0x30 ) ; //вывод десятков секунд lcd_putchar(s% 10 + 0x30 ) ; //вывод секунд } ; }

#include #asm .equ __lcd_port=0x18 ;PORTB #endasm #include int s = 0; // переменная для хранения секунд // Обработка прерывания по совпадению interrupt void timer1_compa_isr(void) { s++; // увеличиваем переменную каждую секунду if(s>59) // обнуляем секунды после 59 { s=0; } TCNT1=0; //обнуляем таймер } void main(void) { TCCR1A=0x00; //настройка таймера TCCR1B=0x05; TCNT1=0x00; //здесь увеличиваются тики OCR1A=0x1E85; //записываем число в регистр сравнения TIMSK=0x10; //запускаем таймер lcd_init(8); #asm("sei") while (1) { lcd_gotoxy(0,0); //вывод в 0 координате X и Y lcd_putchar(s/10+0x30); //вывод десятков секунд lcd_putchar(s%10+0x30); //вывод секунд }; }