- Прерывания по таймеру arduino uno. Прерывания и многозадачность в Arduino
- Таймеры-счетчики. Прерывания
- Timer1
- Timer3
- Методы библиотек TimerOne и TimerThree
- Настройка
- Управление запуском
- Управление выходным ШИМ сигналом
- Прерывания
- Остальные
- Пример 1
- Модифицированные библиотеки от Paul Stoffregen
- Пример 2
- Проблемы с контекстом прерываний
Прерывания по таймеру arduino uno. Прерывания и многозадачность в Arduino
В ходе реализации проекта может потребоваться несколько прерываний, но если каждое из них будет иметь максимальный приоритет, то фактически его не будет ни у одной из функций. По этой же причине не рекомендуется использовать более десятка прерываний.
Обработчики должны применяться только к тем процессам, которые имеют максимальную чувствительность ко временным интервалам. Не стоит забывать, что пока программа находится в обработчике прерывания – все другие прерывания отключены. Большое количество прерываний ведет к ухудшению их ответа.
В момент, когда действует одно прерывание, а остальные отключаются, возникает два важных нюанса, которые должен учитывать схемотехник. Во-первых, время прерывание должно быть максимально коротким.
Это позволит не пропустить все остальные запланированные прерывания. Во-вторых, при обработке прерывания программный код не должен требовать активности от других прерываний. Если этого не предотвратить, то программа просто зависнет.
Не стоит использовать длительную обработку в loop()
, лучше разработать код для обработчика прерывания с установкой переменной volatile. Она подскажет программе, что дальнейшая обработка не нужна.
Если вызов функции Update()
все же необходим, то предварительно необходимо будет проверить переменную состояния. Это позволит выяснить, необходима ли последующая обработка.
Перед тем, как заняться конфигурацией таймера, следует произвести проверку кода. Таймеры Anduino стоит отнести к ограниченным ресурсам, ведь их всего три, а применяются они для выполнения самых разных функций. Если запутаться с использованием таймеров, то ряд операций может просто перестать работать.
Какими функциями оперирует тот или иной таймер?
Для микроконтроллера Arduino Uno у каждого из трех таймеров свои операции.
Так Timer0
отвечает за ШИМ на пятом и шестом пине, функции millis()
, micros()
, delay()
.
Другой таймер – Timer1,
используется с ШИМ на девятом и десятом пине, с библиотеками WaveHC и Servo.
Timer2
работает с ШИМ на 11 и 13 пинах, а также с Tone
.
Схемотехник должен позаботиться о безопасном использовании обрабатываемых совместно данных. Ведь прерывание останавливает на миллисекунду все операции процессора, а обмен данных между loop()
и обработчиками прерываний должен быть постоянным. Может возникнуть ситуация, когда компилятор ради достижения своей максимальной производительности начнет оптимизацию кода.
Результатом этого процесса будет сохранение в регистре копии основных переменных кода, что позволит обеспечить максимальную скорость доступа к ним.
Недостатком этого процесса может стать подмена реальных значений сохраненными копиями, что может привести к потере функциональности.
Чтобы этого не произошло нужно использовать переменную voltatile
,
которая поможет предотвратить ненужные оптимизации. При использовании больших массивов, которым требуются циклы для обновлений, нужно отключить прерывания на момент этих обновлений.
Урок 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
|=
(1WGM12
);
Из комментария мы видим, что мы работает с битами режима, и установим мы из них только бит WGM12, остальные оставим нули. Исходя из этого мы сконфигурировали вот такой режим:
Также у таймера существует ещё вот такой регистр — TIMSK
. Данный регистр отвечает за маски прерываний — Interrupt Mask
. Доступен данный регистр для всех таймеров, не только для первого, он общий. В данном регистре мы установим бит OCIE1A
, который включит нужный нам тип прерывания TIMER1 COMPA
TCCR1B
|=
(1WGM12
);
// устанавливаем режим СТС (сброс по совпадению)
TIMSK
|=
(1OCIE1A
);
Теперь давайте поиграемся с самими регистрами сравнения OCR1A(H и L)
. Для этого придётся немного посчитать. Регистр OCR1AH
хранит старшую часть числа для сравнения, а регистр OCR1AL
— младшую.
Но прежде чем посчитать, давайте пока напишем код с любыми значениями данного регистра и потом поправим, так как дальше мы будем инициализировать делитель и он тоже будет учавствовать в расчёте требуемого времени счёта. Без делителя таймер будет слишком быстро считать.
TIMSK
|=
(1OCIE1A
);
//устанавливаем бит разрешения прерывания 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
// {
// while (butstate==0)
// {
// if (!(PINB&0b00000001))
// {
// if(butcount
// {
// 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 258
С счетчиком итераций главного цикла мы разобрались и выяснили, что для точных временных отсчетов он не годится совершенно — выдержка плавает, да и считать ее сложно. Что делать?
Очевидно, что нужен какой то внешний счетчик, который тикал бы независимо от работы процессора, а процессор мог в любой момент посмотреть что в нем такое натикало. Либо чтобы счетчик выдавал события по переполнению или опустошению — флажок поднимал или прерывание генерил. А проц это прочухает и обработает.
И такой счетчик есть, даже не один — это периферийные таймеры. В 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 каждый раз подстраивать так, чтобы прерывание происходило точно в нужное время.
Timer1
Данная библиотека представляет собой набор функций для настройки аппаратного 16-битного таймера Timer1 в ATMega168/328. В микроконтроллере доступно 3 аппаратных таймера, которые могут быть настроены различными способами для получения различных функциональных возможностей. Начало разработки данной библиотеки было вызвано необходимостью быстро и легко установить период или частоту ШИМ сигнала, но позже она разраслась, включив в себя обработку прерываний по переполнению таймера и другие функции. Она может быть легко расширена или портирована для работы с другими таймерами.
Точность таймера зависит от тактовой частоты процессора. Тактовая частота таймера Timer1 определяется установкой предварительного делителя частоты. Этот делитель может быть установлен в значения 1, 8, 64, 256 или 1024.
- Максимальный период = (Делитель / Частота) × 2 17
- Длительность одного отсчета = (Делитель / Частота)
Для установки просто распакуйте и поместите файлы в каталог Arduino/hardware/libraries/Timer1/ .
Timer3
Обратите внимание, что библиотека Timer1 может использоваться на Arduino Mega, но она не поддерживает все три выходных вывода OCR1A , OCR1B и OCR1C . Поддерживаются только A и B . OCR1A подключен к выводу 11 на Mega, а OCR1B – к выводу 12. С помощью одного из трех вызовов, которые задают вывод, значение 1 задаст вывод 11 на Mega, а 2 – задаст вывод 12. Библиотека Timer3 была протестирована только на Mega.
Библиотеку для таймера Timer3 можно здесь ()
Для установки просто распакуйте и поместите файлы в каталог Arduino/hardware/libraries/Timer3/ .
Методы библиотек TimerOne и TimerThree
Настройка
void initialize(long microseconds=1000000); Вы должны вызвать этот метод первым, перед использованием любых других методов библиотеки. При желании можно задать период таймера (в микросекундах), по умолчанию период устанавливается равным 1 секунде. Обратите внимание, что это нарушает работу analogWrite() на цифровых выводах 9 и 10 на Arduino. void setPeriod(long microseconds); Устанавливает период в микросекундах. Минимальный период и максимальная частота, поддерживаемые данной библиотекой, равны 1 микросекунде и 1 МГц, соответственно. Максимальный период равен 8388480 микросекунд, или примерно 8,3 секунды. Обратите внимание, что установка периода изменит частоту срабатывания прикрепленного прерывания и частоту, и коэффициент заполнения на обоих ШИМ выходах.
Управление запуском
void start(); Запускает таймер, начиная новый период. void stop(); Останавливает таймер. void restart(); Перезапускает таймер, обнуляя счетчик и начиная новый период.
Управление выходным ШИМ сигналом
void pwm(char pin, int duty, long microseconds=-1); Генерирует ШИМ сигнал на заданном выводе pin . Выходными выводами таймера Timer1 являются выводы PORTB 1 и 2, поэтому вы должны выбрать один из них, всё остальное игнорируется. На Arduino это цифровые выводы 9 и 10, эти псевдонимы также работают. Выходными выводами таймера Timer3 являются выводы PORTE , соответствующие выводам 2, 3 и 5 на Arduino Mega. Коэффициент заполнения duty задается, как 10-битное значение в диапазоне от 0 до 1023 (0 соответствует постоянному логическому нулю на выходе, а 1023 – постоянной логической единице). Обратите внимание, что при необходимости в этой функции можно установить и период, добавив значение в микросекундах в качестве последнего аргумента. void setPwmDuty(char pin, int duty); Быстрый способ для настройки коэффициента заполнения ШИМ сигнала, если вы уже настроили его, вызвав ранее метод pwm() . Этот метод позволяет избежать лишних действий по включению режима ШИМ для вывода, изменению состояния регистра, управляющего направлением движения данных, проверки необязательного значения периода и прочих действий, которые являются обязательными при вызове pwm() . void disablePwm(char pin); Выключает ШИМ на заданном выводе, после чего вы можете использовать этот вывод для чего-либо другого.
Прерывания
void attachInterrupt(void (*isr)(), long microseconds=-1); Вызывает функцию через заданный в микросекундах интервал. Будьте осторожны при попытке выполнить слишком сложный обработчик прерывания при слишком большой тактовой частоте, так как CPU может никогда не вернуться в основной цикл программы, и ваша программа будет «заперта». Обратите внимание, что при необходимости в этой функции можно установить и период, добавив значение в микросекундах в качестве последнего аргумента. void detachInterrupt(); Отключает прикрепленное прерывание.
Остальные
unsigned long read(); Считывает время с момента последнего переполнения в микросекундах.
Пример 1
В примере ШИМ сигнал с коэффициентом заполнения 50% подается на вывод 9, а прикрепленный обработчик прерывания переключает состояние цифрового вывода 10 каждые полсекунды.
#include “TimerOne.h”
void setup()
{
pinMode(10, OUTPUT);
Timer1.initialize(500000); // инициализировать timer1, и установить период 1/2 сек.
Timer1.pwm(9, 512); // задать шим сигнал на выводе 9, коэффициент заполнения 50%
Timer1.attachInterrupt(callback); // прикрепить callback(), как обработчик прерывания по переполнению таймера
}
void callback()
{
digitalWrite(10, digitalRead(10) ^ 1);
}
void loop()
{
// ваша программа…
}
Модифицированные библиотеки от Paul Stoffregen
Также доступны отдельно поддерживаемые и обновляемые копии TimerOne и TimerThree , которые отличается поддержкой большего количества оборудования и оптимизацией для получения более эффективного кода.
Плата | ШИМ выводы TimerOne | ШИМ выводы TimerThree |
---|---|---|
Teensy 3.1 | 3, 4 | 25, 32 |
Teensy 3.0 | 3, 4 | |
Teensy 2.0 | 4, 14, 15 | 9 |
Teensy++ 2.0 | 25, 26, 27 | 14, 15, 16 |
Arduino Uno | 9, 10 | |
Arduino Leonardo | 9, 10, 11 | 5 |
Arduino Mega | 11, 12, 13 | 2, 3, 5 |
Wiring-S | 4, 5 | |
Sanguino | 12, 13 |
Методы модифицированных библиотек аналогичны описанным выше, но добавлен еще один метод управления запуском таймера:
Void resume(); Возобновляет работу остановленного таймера. Новый период не начинается.
Пример 2
#include
// Данный пример использует прерывание таймера, чтобы
// помигать светодиодом, а также продемонстрировать, как
// делить переменную между обработчиком прерывания и
// основной программой.
const int led = LED_BUILTIN; // вывод со светодиодом
void setup(void)
{
pinMode(led, OUTPUT);
Timer1.initialize(150000);
Timer1.attachInterrupt(blinkLED); // вызывать blinkLED каждые 0.15 сек.
Serial.begin(9600);
}
// Обработчик прерывания будет мигать светодиодом и
// сохранять данные о том, сколько раз мигнул.
int ledState = LOW;
volatile unsigned long blinkCount = 0; // используйте volatile для общих переменных
void blinkLED(void)
{
if (ledState == LOW) {
ledState = HIGH;
blinkCount = blinkCount + 1; // увеличить значение при включении светодиода
} else {
ledState = LOW;
}
digitalWrite(led, ledState);
}
// Основная программа будет печатать счетчик миганий
// в Arduino Serial Monitor
void loop(void)
{
unsigned long blinkCopy; // хранит копию blinkCount
// чтобы прочитать переменную, которая записана в обработчике
// прерывания, мы должны временно отключить прерывания, чтобы
// быть уверенными, что она не изменится, пока мы считываем ее.
// Чтобы минимизировать время, когда прерывания отключены,
// просто быстро копируем, а затем используем копию, позволяя
// прерываниям продолжать работу.
noInterrupts();
blinkCopy = blinkCount;
interrupts();
Serial.print(“blinkCount = “);
Serial.println(blinkCopy);
delay(100);
}
Проблемы с контекстом прерываний
Для обмена данными между кодом обработчика прерывания и остальной частью вашей программы необходимо принять дополнительные меры.
С учетом всего сказанного напишем программу, переключающую светодиод. В данном случае она будет это делать по событию переполнения таймера‑счетчика Timer 1
(вектор у нас обозначен: TIM1_OVF). Так как счетчик 16‑разрядный, то событие переполнения будет возникать при каждом 65 536‑м импульсе входной частоты. Если мы зададим коэффициент деления тактовой частоты на входе Timer 1
равным 64, то при 4 МГц частоты генератора мы получим примерно 1 Гц: 4 000 000/64/65 536 = 0,953674 Гц.
Это не совсем то, что нам требуется, и к тому же частота неточно равна одному герцу. Для того чтобы светодиод переключался точно раз в полсекунды (т. е. период его был равен секунде), программу придется немного усложнить, загружая каждый раз в счетные регистры определенное значение, которое рассчитывается просто: если период одного тактового импульса таймера равен 16 мкс (частота 4 000 000/64), то для получения 500 000 микросекунд надо отсчитать таких импульсов 31 250. Так как счетчик суммирующий, а прерывание возникает при достижении числа 65 536, то нужно предварительно загружать в него необходимое число 65 536 – 31250 = 34 286.
Это не единственный способ, но наиболее универсальный, годящийся для всех таймеров. Кстати, именно таким способом реализован отсчет времени в Arduino
(см. главу 21
). Иной способ – использовать прерывание по достижению определенного числа, загруженного в регистр сравнения А
или В
. Как это делается, мы увидим далее в этой главе. Для того чтобы осуществить само переключение из красного в зеленый, нам придется поступить как раньше, т. е. по каждому событию переполнения перебрасывать два бита в регистре PortD
.
Полностью программа тогда будет выглядеть так:
Я не буду комментировать подробно каждый оператор, т. к. это заняло бы слишком много места. После выполнения всех команд начальной установки МК зацикливается, но бесконечный цикл будет прерываться возникновением прерывания – здесь все аналогично операционной системе Windows, которая также представляет собой бесконечный цикл ожидания событий. Как вы узнаете из последующих глав, в Arduino
такой цикл – одна из главных составляющих любой программы, как раз потому что прерывания там почти не используются. Внутрь бесконечного цикла здесь можно поставить знакомую команду sleep
, без дополнительных настроек режима энергопотребления она будет экономить около 30 % питания. А вот сэкономить еще больше просто так не получится, поскольку придется останавливать процессорное ядро, и таймер перестанет работать.
Заметки на полях
Кстати, а как остановить запущенный таймер, если это потребуется? Очень просто: если обнулить регистр TCCR1B (тот, в котором задается коэффициент деления тактовой частоты), то таймер остановится. Чтобы запустить его опять с коэффициентом 1/64, нужно снова записать в этот регистр значение 0b00000011.
Обратите внимание, что оператор reti
(окончание обработки прерывания) при обработке прерывания таймера встречается дважды – это вполне нормальный прием, когда подпрограмма разветвляется. Можно, конечно, и пометить последний оператор reti
меткой, и тогда текст процедуры стал бы неотличим от первого варианта, но так будет корректнее.
Обратите также внимание на форму записи ldi temp, (1 . Поскольку бит, обозначаемый как TOIE1, в регистре TIMSK имеет номер 7, то эта запись эквивалентна записи ldi temp,0b10000000 – можно писать и так, и так, и еще кучей разных способов. Например, для запуска таймера с коэффициентом 1/64 требуется, как видно из текста программы, установить младшие два бита регистра TCCR1B. Здесь мы устанавливаем их в temp
напрямую, но поскольку эти биты называются CS11 и CS10, то можно записать так:
ldi temp, (1
или даже так:
ldi temp, (3
Подробно этот способ записи приведен в описании AVR‑ассемблера на сайте Atmel
.
Подробности
В этой программе есть один тонкий момент, связанный с загрузкой счетных регистров таймера. При чтении и записи 16‑разрядных регистров Timer 1 их содержимое может измениться в промежутке между чтением или записью отдельных 8‑разрядных «половинок» (ведь, например, в данном случае таймер продолжает считать, пока идет обработка прерывания). Потому в 16‑разрядных таймерах AVR предусмотрен специальный механизм чтения и записи таких регистров. При записи первым загружается значение старшего байта, которое автоматически помещается в некий (недоступный для программиста) буферный регистр. Затем, когда поступает команда на запись младшего байта, оба значения объединяются, и запись производится одновременно в обе «половинки» 16‑разрядного регистра. Наоборот, при чтении первым должен быть прочитан младший байт, при этом значение старшего автоматически фиксируется помещением в тот же буферный регистр, и при следующей операции чтения старшего байта его значение извлекается оттуда. Таким образом и при чтении значения оба байта соответствуют одному и тому же моменту времени.