Прерывание по переполнению таймера в avr. AVR. Учебный курс. Таймеры. Настройка блока сравнения для генерации ШИМ
Рассмотрим, как сделать таймер своими руками на микроконтроллере ATmega8, хотя код довольно просто адаптировать и для МК AVR других серий. Электронный таймер нужное устройство во всех областях, где требуется выполнение определенных действий через конкретный промежуток времени.
Управление таймера состоит всего из четырех кнопок:
— увеличение значения числа;
— уменьшение значения числа;
— старт таймера;
— сброс таймера.
В качестве индикатора срабатывания таймера применяется генератор звуковой частоты с динамиком. Генератор будет запускаться с помощью транзисторного ключа Q5, который в свою очередь открывается положительным потенциалом, поступающим из порта PC2 микроконтроллера.
Упрощенно таймер работает следующим образом. Кнопками «+» и «-» устанавливается требуемое количество секунд; кнопкой «старт» запускается таймер. Когда таймер отсчитает до нуля, на выводе PC2 микроконтроллера ATmega8 появится высокий потенциал, который откроет Q5. Далее транзисторный ключ запустит генератор и раздастся звук в динамике. Сброс таймера осуществляется при нажатии кнопки «сброс». Генератор звуковой частоты собран на двух транзисторах Q6 и Q7 разный полупроводниковых структур. С принципом работы и описанием схемы подобных генераторов можно ознакомиться, перейдя по .
Алгоритм работы таймера на микроконтроллере
Наш таймер будет отсчитывать обратное время ровно по одной секунде, хотя можно задать и любое другое время, например минуты, часы, сотые секунды и т.п.
Для формирования интервала времени в одну секунду мы воспользуемся первым таймер-счетчиком микроконтроллера ATmega8. Все его настройки мы определим в функцию start . Сначала разделим рабочую частоту микроконтроллера 1000000 Гц на 64 и получим новую частоту 15625 Гц. За это отвечают бит CS10, CS11 и CS12 регистра TCCR1B. Далее разрешаем прерывание по совпадению и в регистр сравнения (старший и младший) записываем двоичное число равное десятичному 15625. Затем обнуляем счетный регистр TCNT1 и устанавливаем в единицу бит WGM12 регистра TCCR1B, что вызывает сброс счетного регистра при совпадении текущего его значения с числом, записанным в регистры сравнения.
void start (void)
TCCR1B &= ~(1< TCCR1B |= (1< TIMSK |= (1< OCR1AH = 0b00111101;
OCR1AL = 0b000001001; // регистр сравнения 15625
TCNT1 = 0;
TCCR1B |= (1< Когда таймер отсчитает ровно одну секунду – вызовется прерывание. В теле функции прерывания мы будем снижать значение переменной на единицу. При достижении нуля на второй выход порта C микроконтроллера появится высокий потенциал, который откроет транзисторный ключ и запустит генератор, в результате чего мы услышим звук в динамике.
ISR (TIMER1_COMPA_vect)
Z—;
В МК ATMega16 есть три таймера/счетчика – два 8-битных (Timer/Counter0, Timer/Counter2) и один 16-битный (Timer/Counter1). Каждый из них содержит специальные регистры, одним из которых является счетный регистр TCNTn (n – это число 0, 1 или 2). Каждый раз, когда процессор выполняет одну команду, содержимое этого регистра увеличивается на единицу (либо каждые 8, 64, 256 или 1024 тактов). Потому он и называется счетным. Помимо него, есть еще и регистр сравнения OCRn (Output Compare Register), в который мы можем сами записать какое-либо число. У 8-битного счетчика эти регистры 8-битные. По мере выполнения программы содержимое TCNTn растет и в какой-то момент оно совпадет с содержимым OCRn. Тогда (если заданы специальные параметры) в регистре флагов прерываний TIFR (Timer/Counter Interrupt Flag Register) один из битов становится равен единице и процессор, видя запрос на прерывание, сразу же отрывается от выполнения бесконечного цикла и идет обслуживать прерывание таймера. После этого процесс повторяется. Ниже представлена временная диаграмма режима CTC (Clear Timer on Compare). В этом режиме счетный регистр очищается в момент совпадения содержимого TCNTn и OCRn, соответственно меняется и период вызова прерывания. Это далеко не единственных режим работы таймера/счетчика. Можно не очищать счетный регистр в момент совпадения, тогда это будет режим генерации широтно-импульсной модуляции, который мы рассмотрим в следующей статье. Можно менять направление счета, т. е. содержимое счетного регистра будет уменьшаться по мере выполнения программы. Также возможно производить счет не по количеству выполненных процессором команд, а по количеству изменений уровня напряжения на «ножке» T0 или T1 (режим счетчика), можно автоматически, без участия процессора, менять состояние ножек OCn в зависимости от состояния таймера. Таймер/Счетчик1 умеет производить сравнение сразу по двум каналам – А или В. Для запуска таймера нужно выставить соответствующие биты в регистре управления таймером TCCRn (Timer/Counter Control Register), после чего он сразу же начинает свою работу. Мы рассмотрим лишь некоторые режимы работы таймера. Если вам потребуется работа в другом режиме, то читайте Datasheet к ATMega16 – там все подробнейше по-английски написано, даны даже примеры программ на С и ассемблере (недаром же он занимает 357 страниц печатного текста!). Теперь займемся кнопками. Если мы собираемся использовать небольшое количество кнопок (до 9 штук), то подключать их следует между «землей» и выводами какого-либо порта микроконтроллера. При этом следует сделать эти выводы входами, для чего установить соответствующие биты в регистре DDRx и включить внутренний подтягивающий резистор установкой битов в регистре PORTx. При этом на данных «ножках» окажется напряжение 5 В. При нажатии кнопки вход МК замыкается на GND и напряжение на нем падает до нуля (а может быть и наоборот – вывод МК замкнут на землю в отжатом состоянии). При этом меняется регистр PINx, в котором хранится текущее состояние порта (в отличие от PORTx, в котором установлено состояние порта при отсутствии нагрузки, т. е. до нажатия каких-либо кнопок). Считывая периодически состояние PINx, можно определить, что нажата кнопка. ВНИМАНИЕ!
Если соответствующий бит в регистре DDRx будет установлен в 1 для вашей кнопки, то хорошее нажатие на кнопку может привести к небольшому пиротехническому эффекту – возникновению дыма вокруг МК. Естественно, МК после этого придется отправить в мусорное ведро… Перейдем к практической части. Создайте в IAR новое рабочее пространство и новый проект с именем, например, TimerButton. Установите опции проекта так, как это описано в предыдущей статье.
А теперь наберем следующий небольшой код. Давайте посмотрим, как это работает. В функциях init_timern задаются биты в регистрах TCCRn, OCRn и TIMSK, причем такой способ может кому-нибудь показаться странным или незнакомым. Придется объяснить сначала, что означает запись «(1 где a – это то число, двоичное представление которого нужно сдвинуть, а b показывает, на сколько битов нужно его сдвинуть. При этом возможна потеря значения, хранящегося в a (т.е. не всегда возможно восстановить из С то, что было в а). Рассмотрим пример: Что окажется в С после выполнения строки C = (22 2 в двоичном коде будет выглядеть как 00010110, а после сдвига влево на 3 бита получим С = 10110000. Аналогично существует и сдвиг вправо. Еще пример: Сначала выполнится действие во внутренних скобках (0xFF – это 255 в шестнадцатеричном коде), из 11111111 получится 11111100, потом произойдет сдвиг вправо и получим С = 00111111. Как видим, здесь две взаимно обратные операции привели к другому числу, т. к. мы потеряли два бита. Этого не произошло бы, если бы переменная С была типа int, т. к. int занимает 16 бит. Теперь рассмотрим еще два битовых оператора, широко применяющиеся при программировании МК. Это оператор «побитовое и» (&) и «побитовое или» (|). Как они действуют, думаю, будет понятно из примеров: Действие: Результат (в двоичном коде):
С = 0; // C = 00000000
C = (1 // C = 00100101
C |= (1 // C = 00101101
C &= (0xF0 >> 2); // C = 00101100
C = (C & 4) | 3; // C = 00000111
Чуть не забыл! Есть еще «побитовое исключающее или» (^). Оно сравнивает соответствующие биты в числе, и, если они одинаковые, возвращает 0, иначе единицу. Вернемся к нашей программе. Там написано «(1 При компиляции программы запись WGM01 просто заменяется на число 3, и в результате получается уже корректная запись. WGM01 называется макросом и он, в отличие от переменной, не занимает места в памяти (разве что в памяти программиста:-). Если заглянуть теперь в Datasheet, но нетрудно будет увидеть, что WGM01 – это имя третьего бита в регистре TCCR0. То же самое касается и остальных битов этого регистра. Это совпадение не случайно и относится ко всем регистрам МК (или почти ко всем). Т. е., написав «(1 Итого, строчка означает, что включен режим СТС, при срабатывании таймера0 меняется состояние «ножки» ОС0 (Она же PB3), содержимое счетчика увеличивается каждые 1024 такта. Аналогично для таймера2: TCCR2 = (1 В регистре TIMSK (Timer/counter Interrupt MaSK register) задается режим прерываний. Мы написали что означает прерывание таймера2 по совпадении TCNT2 и OCR2.
Самая последняя функция – это собственно функция прерывания совпадения таймера2. Прерывания объявляются следующим образом: где ВЕКТОР – это макрос вектора прерывания (по смыслу просто число, характеризующее это прерывание); эти макросы в порядке снижения приоритета перечислены в файле iom16.h. ТИП – тип возвращаемого функцией значения, в нашем случае void (ничего). ИМЯ – произвольное имя для этой функции. С прерываниями мы еще успеем наработаться в будущем. При выполнении нашей функции должны по очереди моргать светодиоды, подключенные к PB0 и PB1. Судя по всему, частота равна 11059200/(256*1024) = 42 Гц. Это быстро, но будет заметно невооруженным глазом. Кстати, применение таймеров дает возможность отсчитывать точные временные интервалы, не зависящие от сложности вашей программы и порядка ее выполнения (но если у Вас не более одного прерывания). Итак, сохраняем файл как «TimerDebug.c», добавляем его в проект, компилируем, прошиваем МК. Что же мы видим?
Светодиод, подключенный к выводу PB3, будет активно моргать, а на PB0 и PB1 нет ни каких изменений. В чем же дело? Неужели что-то неверно? Чтобы это выяснить, придется отладить нашу программу. Поскольку в IAR нет Debuggerа, придется использовать AVR Studio. Эту среду разработки можно скачать с сайта производителя http://atmel.com . Проблем с ее установкой, думаю, не должно быть. Перед запуском AVR Studio выберите в IAR режим Debug и создайте отладочный cof-файл (все опции проекта должны быть выставлены, как описано в предыдущей статье). Открыв AVR Studio, мы увидим окно приветствия, в котором выберем «Open». Теперь лезем в папку с проектом, там в Debug\Exe, выбираем там «TimerDebug.cof», создаем проект там, где предложат, выбираем дивайс ATMega16 и режим отладки Simulator. После этого, если все сделали правильно, сразу же идет процесс отладки Среда отладки здесь очень удобная, т.к. позволяет просматривать содержимое всех регистров МК, а также вручную устанавливать значения для них щелчками мыши. Например, если установить флаг прерывания в регистре TIFR в бите 7 (под черным квадратом в TIMSK), то следующим шагом программы (нажатие F10 или F11) должна быть обработка прерывания (флаг будет установлен автоматически и при совпадении регистров TCNT2 и OCR2). Но, к нашему удивлению, прерывания не будет! Возникает вопрос: почему? Откроем регистр CPU, SREG. Этот регистр определяет работу процессора, а конкретно седьмой его бит (I-бит, Interrupt bit) ответственен за обработку всех прерываний в МК. У нас он не установлен. Стоит его выставить, как сразу же пойдет выполняться прерывание (если одновременно установлен седьмой бит в TIFR). Можно заметить одну интересную особенность: как только процессор уходит в обработку прерывания, этот бит (флаг разрешения обработки прерываний) снимается, а при выходе из функции прерывания вновь автоматически устанавливается. Это не позволяет процессору, не выполнив одного прерывания, схватиться за другое (ведь ориентируется он в программе именно таким образом – по флагам). Значит, нужно добавить строчку кода для установки этого бита в единичное состояние. Добавим мы его в функцию init_timer2. Получится следующее: Теперь, выбрав конфигурацию Release и прошив МК нажатием F7 и запуском AVReal32.exe, с радостью увидим, что все работает как надо. Замечание:
при отладке программы следует уменьшать интервалы таймеров, если они слишком длинные, т. к. в процессе отладки в AVR Studio программа выполняется в тысячи раз медленнее, чем внутри МК и вы не дождетесь срабатывания таймера. В целом отладка полностью аналогична таковой в других системах программирования, таких, как Visual C++. Теперь, научившись отлаживать программы, создадим в IAR новый файл (а старый сохраним и удалим из проекта) и наберем следующий код: Сначала предлагаю взять уже готовый файл прошивки (файлы к статье, папка Release, файл TimerButton.hex или откомпилировать этот текст) и записать его в МК. После чего вынуть кабель прошивки, подключить к PC0 и PC1 кнопки и попробовать их понажимать. Увидим, что при нажатии на одну из кнопок увеличивается регистр PORTB (загораются светодиоды), а при нажатии на другую – уменьшается. Если не работает – попробуйте понажимать одну кнопку, удерживая другую – будет действовать. Дело в том, что я подключал кнопки следующим образом: при нажатии на кнопку вывод МК «болтается» в воздухе, а при отпускании замыкается на землю. Если вы подключили кнопки по-другому, то придется лишь чуть модернизировать программу. Давайте разберемся с кодом. Здесь работа с таймером организована несколько иначе. Он срабатывает каждые 11072 такта (то есть каждые 1,001175 мс) и увеличивает содержимое переменной counter. Есть еще функция delay(long unsigned int Pause_ms), которая берет в качестве параметра количество миллисекунд Pause_ms, сбрасывает counter и ждет, когда counter достигнет значения Pause_ms, после чего продолжает работу МК. Таким образом, написав delay(1500), мы сформируем задержку в программе в 1,5 секунды. Это очень удобно для формирования временных интервалов. С таймером вроде все понятно. Но для чего он используется? Рассмотрим бесконечный цикл while(1) в main(). В этом цикле проверяется состояние кнопок путем анализа содержимого регистра PINB. А зачем там стоит задержка на 50 мс? Это устранение т. н. «дребезга клавиш». Дело в том, что при нажатии на кнопку происходит удар одного контакта о другой, и, поскольку контакты металлические, удар этот упругий. Контакты, пружиня, замыкаются и размыкаются несколько раз, несмотря на то, что палец сделал лишь одно нажатие. Это приводит к тому, что МК фиксирует несколько нажатий. Давайте рассмотрим график зависимости напряжения на выходе PC0 от времени. Он может выглядеть так: Точка А – момент нажатия кнопки. Он может быть зафиксирован МК. Затем идут несколько замыканий и размыканий (их может и не быть, а может быть и 12 штук – это явление можно считать случайным). В точке B контакт уже надежно зафиксирован. Между A и B в среднем около 10 мс. Наконец, в точке D происходит размыкание. Как же избавиться от этого неприятного явления? Оказывается, очень просто. Нужно зафиксировать момент нажатия кнопки (точка А), через какое-то время, например, 50 мс (точка С) проверить, что кнопка действительно нажата, сделать действие, соответствующее этой кнопке и ждать момент ее отпускания (точка D). То есть нужно сделать паузу от А до С, такую, чтобы весь «дребезг» оказался внутри этой паузы. А попробуйте теперь убрать строчку, формирующую задержку, откомпилировать программу и зашить ее в МК. Путем простых нажиманий на кнопки сможете легко убедиться, что все эти «мучения» не были напрасными. А что же делать, если к МК нужно подключить, скажем, 40 кнопок? Ведь у него всего лишь 32 вывода. Казалось бы, никак. На самом деле это возможно. В таком случае используют алгоритм, называемый стробированием. Для этого нужно кнопки соединить в виде матрицы, как это показано на рисунке (рисунок взят из книги Мортона «МК AVR, вводный курс», где написано про программирование AVR на ассемблере). При подаче на вывод PB0 лог. 1 (+5В), а на выводы PB1 и PB2 лог. 0 разрешается обработка кнопок 1, 4 и 7. После этого состояние каждой из них можно узнать, проверив напряжение на одном из выводов PB3..PB5. Таким образом, подавая последовательно на выводы PB0..PB2 лог. 1, можно определить состояние всех кнопок. Понятное дело, что выводы PB0..PB2 должны быть выходами, а PB0..PB2 входами. Чтобы определить, какое количество выводов потребуется для массива из Х кнопок, нужно найти пару сомножителей Х, сумма которых наименьшая (для нашего случая с 40 кнопками это будут числа 5 и 8). Это означает, что к одному МК можно подключить до 256 кнопок (а с применение дешифраторов и того больше, но о дешифраторах потом). Лучше сделать меньшее число выводов выходами, а большее – входами. В этом случае опрос всех строк матрицы займет меньше времени. Подобный способ подключения (стробирование) свойственен не только для кнопок. Там можно подключать самые разнообразные устройства, начиная от матриц светодиодов и заканчивая микросхемами flash-памяти. © Киселев Роман Здравствуйте, Артем! Спасибо за ответ! //#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-х битным. Это для того, что-бы не делать отдельные пределы измерений, а просто увеличить время заряда конденсатора. П.С. могу скинуть схемку, если интересно. В этом уроке мы поговорим о таймерах. Данная тема непосредственно связана с темой тактирования микроконтроллера. Поэтому рекомендую перед прочтением данного урока ознакомиться с предыдущим. Итак, зачем нам таймер? При построении проектов на микроконтроллерах очень часто возникает необходимость измерение точных временных промежутков. Например, желание мигать светодиодом с определенной частотой, или опрашивать состояние кнопки с необходимыми временными промежутками. Решить поставленные задачи помогают именно таймеры. Но таймеры микроконтроллеров 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 #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
По сути, таймер микроконтроллера - это цифровой счетчик, только "навороченый". На вход счетчика подается тактовый сигнал, по перепадам которого счетчик увеличивает свое значение. При возникновении событий - переполнение счетчика или совпадение его значения с заданным - генерируется запрос на прерывание. Давайте разберем, как пользоваться таймером Т0 в режиме Normal. В этом режиме таймер считает от какого-то начального значения счетного регистра до максимально возможного (до 255 или 0xFF). Когда таймер Т0 досчитывает до максимума, то в следующий такт таймера возникает переполнение счетного регистра TCNT0 - он обнуляется и устанавливается флаг TOV0. Если в программе разрешены прерывания глобально (флаг I регистра SREG) и прерывание таймера Т0 по переполнению (флаг TOIE0 регистра TIMSK), то микроконтроллер вызовет соответствующий обработчик. Если значение счетного регистра совпадет с регистром сравнения OCR0, то установится флаг OCF0 и при разрешенном прерывании по событию совпадение, запустится его обработчик. Рассмотрим практическую задачу - нам нужно каждые 20 мс опрашивать кнопку. Частота микроконтроллера 8 МГц, микроконтроллер ATmega16. Первое, что нужно сделать - это определиться с выбором коэффициента предделителя таймера и рассчитать начальное значение для счетного регистра TCNT0. Таймер Т0 может тактироваться от внутреннего тактового сигнала микроконтроллера или от внешнего, который подается на вывод Т0. При работе от внутреннего тактового сигнала пользователь может выбирать коэффициенты деления частоты этого сигнала. У таймера Т0 есть пять возможных вариантов коэффициента предделителя - 1, 8, 64, 256, 1024. Для решения поставленной задачи, я рассуждаю следующим образом. Если бы один такт таймера Т0 имел период 1 мс, то мне бы это подошло. 20 тактов дают 20 мс. Какой коэффициент предделителя таймера позволит получить близкий к 1 мс период тактовой частоты? Можно посчитать. Тактовая частота микроконтроллера Fcpu = 8000000 Гц При k = 1024 период тактовой частоты таймера Т0 будет равен Tt0 = 1024/8000000 = 0.128 мс Это максимальный период тактового сигнала таймера, который мы можем получить при наших условиях (Fcpu = 8 МГц). При меньших коэффициентах - период получится еще меньше. Ну хорошо, пусть один такт таймера это 0.128 мс, хватит ли разрядности счетного регистра, чтобы отсчитать этот временной интервал и сколько для этого понадобится тактов? Делим требуемый интервал времени (20 мс) на длительность одного такта таймера и получаем ответ. n = t/Tto = 20 мс/ 0.128 мс = 156.25 Округлив до целого, получаем 156 тактов. Это меньше 255 (максимального значения счетного регистра), значит разрядности счетного регистра TCNT0 хватит. Начальное значение для счетного регистра TCNT0 вычисляем как разницу между максимальным числом тактов таймера Т0 и требуемым, то есть 256 - 156 = 100. (256 - это максимальное количество временных интервалов, которые может отсчитать любой 8-и разрядный таймер.) Думаю, теперь понятно, как рассчитывать начальное значение TCNT0 для режима Normal
: Вычисляем период одного такта таймера Tt0 = k/Fcpu, Можно автоматизировать эту процедуру с помощью макросов. Например, так: #define F_CPU 8000000UL Но с таким макросом нужно быть начеку, при определенных значениях time и k могут возникать ошибки. Теперь переходим к коду. Чтобы использовать таймер Т0 (да и любой другой тоже), его нужно настроить (инициализировать) и описать обработчик прерывания (если они используются).
Инициализация таймера состоит из следующих шагов:
Остановка таймера, В данной последовательности возможны вариации. Для нашей задачи код инициализации будет выглядеть так: TCCR0 = 0; Вторая строчка инициализации, по сути, бесполезна, она добавлена для наглядности. Чтобы четко видеть, какой режим таймера устанавливается. Сброс флагов прерываний в регистре TIFR выполняется записью 1 в соответствующий разряд.
Эту операцию нужно выполнять именно перезаписью регистра, а не с помощью побитового ИЛИ. И вот почему. Допустим, в регистре TIFR устанавлены два флага прерывания - TOV1 и TOV0. TOV0 нам нужно сбросить. При установке требуемого разряда с помощью ИЛИ
происходит примерно следующая вещь. //в R16 устанавливается разряд TOV0 //R16, равный 0b00000101, записывается в регистр TIFR В результате сброшены оба флага, а мы хотели сбросить один. Продолжаем. Синтаксис описания обработчиков прерывания у разных компиляторов немного отличается. Для IAR`a обработчик прерывания таймера Т0 по событию переполнение будет выглядеть так: /*здесь должен быть опрос кнопки*/ TIMER0_OVF_vect - это адрес вектора прерывания по событию переполнение. Он берется из заголовочных файлов на микроконтроллер. В данном случае я взял его из файла iom16.h. Первая строка обработчика (TCNT0 = T_POLL;) выполняет перезапись счетного регистра, то устанавливает его начальное значение. Если этого не сделать, таймер продолжит счет с 0. Перезапись счетного регистра нужно выполнять в начале обработчика прерывания. Весь код для нашей задачи будет выглядеть примерно так. (Код приведен для IAR`a. Для других компиляторов нужно изменить заголовочные файлы и обработчик прерывания.) #include #define T_POLL 100 int main(void) TCCR0 = 0; /*инициализация остальной периферии*/ Enable_interrupt(); /*обработчик прерывания T0 /*опрос кнопки*/ /*инверсия PB0 для отладки*/ В режиме Normal таймер Т0 может изменять состояние вывода OC0 при совпадении счетного регистра и регистра сравнения. Причем даже без прерываний. Варианты управления определяются разрядами COM01 и COM00 регистра TCCR0. Вот пример программы, генерирующей прямоугольный сигнала на выводе ОС0. #include int main(void) TCCR0 = 0; /*инициализация OC0*/ While(1); Вывод ОС0 будет менять свое состояние на противоположное при нулевом значении счетного регистра. Обработчик прерывания таймера (да и любой другой периферии) нужно делать как можно короче. И последнее. Может случится ситуация, что обработка прерывания таймера задержится (например, по вине другого обработчика) и регистр TCNT0 уже посчитает несколько тактов. Если просто перезаписать значение TCNT0, то следующее прерывание вызовется позже, чем нужно. Получится, что предыдущее (задержанное) и новое прерывания не выдержат требуемый интервал. Эту ситуацию можно сгладить, если выполнять перезапись счетного регистра вот так: TCNT0 = TCNT0 + startValue; Сложение текущего значения счетного регистра с инициализируемым, учтет эти лишние такты.
Правда есть одно НО! При больших значения startValue операция сложения может вызвать переполнение счетного регистра. Например, startValue =
250, а таймер успел досчитать до 10. Тогда операция сложения приведет к такому результату: 10 + 250 = 260 Берем 8 разрядов от 260 получаем 4. В TCNT0 запишется 4.
Июнь 2007
Я свою проблему решил, прибор — это измеритель емкости от 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);
}
}
Заранее спасибо!
Таймер Т0 в режиме Normal
Период тактового сигнала микроконтроллера Tcpu = 1/Fcpu
Период тактового сигнала таймера Т0 равен Tt0 = (1/Fcpu)/k = k/Fcpu
- вычисляем требуемое количество тактов для заданного интервала n = t/Tto,
- вычисляем начальное значение для счетного регистра TCNT0 = 256 - n.
#define TIME_MS(time, k) (256L - ((time)*(F_CPU))/(1000L*(k)))
- задание режима Normal в TCCR0 без старта,
- установка начального значения TCNT0,
- сброс флагов в регистре TIFR,
- разрешение прерывания по переполнению в TIMSK,
- установка предделителя в TCCR0, то есть старт таймера
/*значение для счетного регистра*/
#define T_POLL 100
TCCR0 = (0<
TIFR = (1<
//TIFR имеет значение 0b00000101
//установлены флаги
TOV1 и TOV0
//выполняется код
TIFR |= (1<
//TIFR копируется в R16
IN R16, 0x38
//хотя он и так уже установлен
ORI R16, 0x02
OUT 0x38, R16
{
TCNT0 = T_POLL;
#include
#include
{
/*инициализация таймера*/
TCCR0 = (0<
TIFR |= (1<
DDRB |= (1<
while(1);
по событию переполнение*/
#pragma vector = TIMER0_OVF_vect
__interrupt void TimerT0Ovf(void)
{
/*перезапись счетного регистра*/
TCNT0 = T_POLL;
PORTB ^= (1<Управление выводом OC0
#include
{
/*инициализация таймера Т0*/
TCCR0 = (0<
OCR0 = 0;
TIMSK = 0;
TCCR0 |= (1<
DDRB |= (1<
return 0;
}Несколько моментов относительно использования таймера
Если расчетное значение для счетного регистра (или регистра сравнения) округляется, то временной интервал будет отсчитываться таймером с погрешностью.