Avr программирование таймера на ассемблере простой пример. Программные таймеры на ассемблере

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

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

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

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

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

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

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

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

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

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

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

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

Решить поставленные задачи помогают именно таймеры. Но таймеры микроконтроллеров 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

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

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

И такой счетчик есть, даже не один — это периферийные таймеры. В 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 каждый раз подстраивать так, чтобы прерывание происходило точно в нужное время.

Одним из преимуществ микроконтроллера 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

Здравствуйте, Артем! Спасибо за ответ!
Я свою проблему решил, прибор — это измеритель емкости от 1пФ+-0,25 пФ до 1,5 мкФ+-2,5нФ. Все работает, но интересно было бы сделать все не на Arduino IDE.
Вот мой код:

//#include // библиотека для работы с компаратором //#include // библиотека для ускореной работы с портами //#include // библиотека для LCD LiquidCrystal lcd(14 ,15 ,16 ,17 ,18 ,19 ) ; // инициализация LCD volatile unsigned int R = 1995 ; // резистор заряда конд. первого предела volatile unsigned int T = 0 ; // время заряда конд. плюс входная емкость плюс время виполнения команд volatile double C = 0.0 ; // вычесленное значение емкости void Timer1_Init( void ) { TCNT1 = 0 ; TCCR1A = 0 ; // Bits: COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM10 TCCR1B = 0 ; // Bits: ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10 TCCR1C = 0 ; // Bits: FOC1A FOC1B - - - - - - TIMSK1 = 0 ; // Bits: - - ICIE1 - - OCIE1B OCIE1A TOIE1 TIFR1 = 0 ; // Bits: – – ICF1 – - OCF1B OCF1A TOV1 } volatile double calib = 122.0 ; // значение Т без конденсатора (калибрация) void setup() { lcd.begin (16 ,2 ) ; // вкл. LCD D8_Out; // назначаем D8 на выход D8_Low; // на D8 устанавливаем 0 (минус конденсатора) D12_Out; // назначаем D12 на выход D12_High; // на D12 устанавливаем 1 (заряд конд. резистором первого предела) D1_Out; // назначаем D1 на выход D1_High; // на D1 устанавливаем 1 (заряд конд. резистором второго предела) cli() ; // Global disable interrupts Timer1_Init() ; // Timer/Counter 1 initialization TIMSK0 & amp; = ~(1 & lt; & lt; TOIE0) ; // Disable Timer 0. TIMSK2 & amp; = ~(1 & lt; & lt; TOIE2) ; // Disable Timer 2. sei() ; // Global anable interrupts ADCSRA = 0 ; // Disable ADC PRR = B10010111; // Disable USART0, TWI, SPI } void loop() { D9_Out; // назначаем D9 на выход D9_Low; // на D9 устанавливаем 0 (разряд конд. через резистор 100 Ом) for (int i= 0 ; i& lt; 250 ; i++ ) // ждем пока разрядится конд. 1 cек { TCCR1B = (1 & lt; & lt; CS10) ; // запускаем таймер Fxtal/1 while ( TCNT1 & lt; 64000 ) { } TCCR1B = 0 ; //останавливаем таймер TCNT1 = 0 ; // сброс счетчика } D9_In; // делаем D9 входом, что б не мишать измерениям analogComparator.setOn (AIN0, AIN1) ; // компаратор вкл. TCCR1B = (1 & lt; & lt; CS11) ; // запускаем таймер Fxtal/8 while ((((ACSR & amp; & amp; (1 & lt; & lt; ACO) ) == 0 ) ) & amp; & amp; (TCNT1= 14001 ) // выбираем придел измерений { D1_Low; R = 20.0 ; calib = 6.0 ; lcd.print ("Range 700nF" ) ; lcd.setCursor (0 ,1 ) ; lcd.print (C/ 1000 ) ; lcd.print ("nF " ) ; // lcd.print(T); } if ( C & lt; = 14000 ) // выбираем придел измерений { D1_High; R = 1995.0 ; calib = 122.0 ; lcd.print (& Range 7nF& ) ; lcd.setCursor (0 ,1 ) ; lcd.print (C) ; lcd.print (& pF & ) ; // lcd.print(T); } }

//#include // библиотека для работы с компаратором //#include // библиотека для ускореной работы с портами //#include // библиотека для LCD LiquidCrystal lcd(14,15,16,17,18,19); // инициализация LCD volatile unsigned int R = 1995; // резистор заряда конд. первого предела volatile unsigned int T = 0; // время заряда конд. плюс входная емкость плюс время виполнения команд volatile double C = 0.0; // вычесленное значение емкости void Timer1_Init(void) // Timer/Counter 1 initialization { TCNT1 = 0; TCCR1A = 0; // Bits: COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM10 TCCR1B = 0; // Bits: ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10 TCCR1C = 0; // Bits: FOC1A FOC1B - - - - - - TIMSK1 = 0; // Bits: - - ICIE1 - - OCIE1B OCIE1A TOIE1 TIFR1 = 0; // Bits: – – ICF1 – - OCF1B OCF1A TOV1 } volatile double calib = 122.0; // значение Т без конденсатора (калибрация) void setup() { lcd.begin(16,2); // вкл. LCD D8_Out; // назначаем D8 на выход D8_Low; // на D8 устанавливаем 0 (минус конденсатора) D12_Out; // назначаем D12 на выход D12_High; // на D12 устанавливаем 1 (заряд конд. резистором первого предела) D1_Out; // назначаем D1 на выход D1_High; // на D1 устанавливаем 1 (заряд конд. резистором второго предела) cli(); // Global disable interrupts Timer1_Init(); // Timer/Counter 1 initialization TIMSK0 &= ~(1 << TOIE0); // Disable Timer 0. TIMSK2 &= ~(1 << TOIE2); // Disable Timer 2. sei(); // Global anable interrupts ADCSRA = 0; // Disable ADC PRR = B10010111; // Disable USART0, TWI, SPI } void loop() { D9_Out; // назначаем D9 на выход D9_Low; // на D9 устанавливаем 0 (разряд конд. через резистор 100 Ом) for (int i=0; i<250; i++) // ждем пока разрядится конд. 1 cек { TCCR1B = (1<

Еще вопрос: как сделать что-бы значение таймера при переполнении продолжало увеличиваться, а не обнулялось и начиналось сначала? То есть, я хочу сделать таймер 32-х битным. Это для того, что-бы не делать отдельные пределы измерений, а просто увеличить время заряда конденсатора.
Заранее спасибо!

П.С. могу скинуть схемку, если интересно.

Публикации по теме