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





Таймеры счетчики микроконтроллеров AVR (часы реального времени). Урок AVR 7

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

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

Таймеры общего назначения умеют:

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

Таймеры счетчики могут тактировать от внутреннего генератора тактовой частоты и от счетного входа. Давайте рассмотрим функционал таймера-счетчика 1 в микроконтроллере atmega8 . Запускаем CodeVision AVR, создаем новый проект и соглашаемся на предложение запустить Code WizardAVR

Давайте на примере timer2 реализуем часы реального времени с выводом на lcd дисплей, для этого выставляем таймер как показано на скриншоте

здесь выставляется внешний источник тактирования таймера, в качестве внешнего источника мы будем использовать часовой кварц на 32768 герц, далее установим предделитель на 128, то есть таймер будет работать на частоте 32768/128=256, а счетный регистр то в нас 8-битный (максимальное число 255), получается, что он будет переполнятся раз в секунду, далее мы выставляем галочку возле Overflow interrupt и кликаем на file->Generate, save and exit.

Code Wizard cгенерировал вот такой код

#include // Timer2 overflow interrupt service routine interrupt void timer2_ovf_isr(void) { } void main(void) { // Input/Output Ports initialization // Port B initialization PORTB=0x00; DDRB=0x00; // Port C initialization PORTC=0x00; DDRC=0x00; // Port D initialization PORTD=0x00; DDRD=0x00; // Timer/Counter 0 initialization // Clock source: System Clock // Clock value: Timer 0 Stopped TCCR0=0x00; TCNT0=0x00; // Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 125,000 kHz // Mode: Fast PWM top=00FFh // OC1A output: Discon. // OC1B output: Discon. // Noise Canceler: Off // Input Capture on Falling Edge // Timer1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: Off // Compare B Match Interrupt: Off TCCR1A=0x01; TCCR1B=0x0A; TCNT1H=0x00; TCNT1L=0x00; ICR1H=0x00; ICR1L=0x00; OCR1AH=0x00; OCR1AL=0x00; OCR1BH=0x00; OCR1BL=0x00; // Timer/Counter 2 initialization // Clock source: TOSC1 pin // Clock value: PCK2/128 // Mode: Normal top=FFh // OC2 output: Disconnected ASSR=0x08; TCCR2=0x05; TCNT2=0x00; OCR2=0x00; // External Interrupt(s) initialization // INT0: Off // INT1: Off MCUCR=0x00; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x40; // Analog Comparator initialization // Analog Comparator: Off // Analog Comparator Input Capture by Timer/Counter 1: Off ACSR=0x80; SFIOR=0x00; // Global enable interrupts #asm("sei") while (1) { }; }

#include #include // Alphanumeric LCD Module functions #asm .equ __lcd_port=0x12 ;PORTD #endasm #include unsigned char second=0; //переменная для хранения секунд unsigned char minute=0; //переменная для хранения минут unsigned char hour=0; //переменная для хранения часов char lcd_buffer; //переменная буфер для вывода на дисплей // Timer2 overflow interrupt service routine interrupt void timer2_ovf_isr(void) { if (++second==59) //увеличиваем количество секунд на 1 и проверяем равенство 59 {second = 0; if (++minute==59) {minute = 0; if (++hour==59) { hour = 0; } } } lcd_clear(); //чистим дисплей перед выводом lcd_gotoxy(0,0); // переводим курсор в точку x=0 y=0 sprintf(lcd_buffer,"%i:%i:%i",hour,minute,second); // формируем строку для вывода lcd_puts(lcd_buffer); // выводим строку на дисплей } void main(void) { // Timer/Counter 2 initialization // Clock source: TOSC1 pin // Clock value: PCK2/128 // Mode: Normal top=FFh // OC2 output: Disconnected ASSR=0x08; TCCR2=0x05; TCNT2=0x00; OCR2=0x00; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x40; // LCD module initialization lcd_init(16); // Global enable interrupts #asm("sei") while (1) { }; }

Программа готова, теперь составим схему в Proteus

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

Давайте разберем, как пользоваться таймером Т0 в режиме Normal. В этом режиме таймер считает от какого-то начального значения счетного регистра до максимально возможного (до 255 или 0xFF). Когда таймер Т0 досчитывает до максимума, то в следующий такт таймера возникает переполнение счетного регистра TCNT0 - он обнуляется и устанавливается флаг TOV0. Если в программе разрешены прерывания глобально (флаг I регистра SREG) и прерывание таймера Т0 по переполнению (флаг TOIE0 регистра TIMSK), то микроконтроллер вызовет соответствующий обработчик. Если значение счетного регистра совпадет с регистром сравнения OCR0, то установится флаг OCF0 и при разрешенном прерывании по событию совпадение, запустится его обработчик.

Таймер Т0 в режиме Normal

Рассмотрим практическую задачу - нам нужно каждые 20 мс опрашивать кнопку. Частота микроконтроллера 8 МГц, микроконтроллер ATmega16.

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

Таймер Т0 может тактироваться от внутреннего тактового сигнала микроконтроллера или от внешнего, который подается на вывод Т0. При работе от внутреннего тактового сигнала пользователь может выбирать коэффициенты деления частоты этого сигнала. У таймера Т0 есть пять возможных вариантов коэффициента предделителя - 1, 8, 64, 256, 1024.

Для решения поставленной задачи, я рассуждаю следующим образом. Если бы один такт таймера Т0 имел период 1 мс, то мне бы это подошло. 20 тактов дают 20 мс. Какой коэффициент предделителя таймера позволит получить близкий к 1 мс период тактовой частоты? Можно посчитать.

Тактовая частота микроконтроллера Fcpu = 8000000 Гц
Период тактового сигнала микроконтроллера Tcpu = 1/Fcpu
Период тактового сигнала таймера Т0 равен Tt0 = (1/Fcpu)/k = k/Fcpu

При 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,
- вычисляем требуемое количество тактов для заданного интервала n = t/Tto,
- вычисляем начальное значение для счетного регистра TCNT0 = 256 - n.

Можно автоматизировать эту процедуру с помощью макросов. Например, так:

#define F_CPU 8000000UL
#define TIME_MS(time, k) (256L - ((time)*(F_CPU))/(1000L*(k)))

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

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

Инициализация таймера состоит из следующих шагов:

Остановка таймера,
- задание режима Normal в TCCR0 без старта,
- установка начального значения TCNT0,
- сброс флагов в регистре TIFR,
- разрешение прерывания по переполнению в TIMSK,
- установка предделителя в TCCR0, то есть старт таймера

В данной последовательности возможны вариации.

Для нашей задачи код инициализации будет выглядеть так:


/*значение для счетного регистра*/
#define T_POLL 100

TCCR0 = 0;
TCCR0 = (0< TCNT0 = T_POLL;
TIFR = (1< TIMSK |= (1< TCCR0 |= (1<

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

Сброс флагов прерываний в регистре TIFR выполняется записью 1 в соответствующий разряд. Эту операцию нужно выполнять именно перезаписью регистра, а не с помощью побитового ИЛИ. И вот почему.

Допустим, в регистре TIFR устанавлены два флага прерывания - TOV1 и TOV0. TOV0 нам нужно сбросить. При установке требуемого разряда с помощью ИЛИ происходит примерно следующая вещь.


//TIFR имеет значение 0b00000101
//установлены флаги TOV1 и TOV0
//выполняется код TIFR |= (1<
//TIFR копируется в R16
IN R16, 0x38

//в R16 устанавливается разряд TOV0
//хотя он и так уже установлен
ORI R16, 0x02

//R16, равный 0b00000101, записывается в регистр TIFR
OUT 0x38, R16

В результате сброшены оба флага, а мы хотели сбросить один.

Продолжаем.

Синтаксис описания обработчиков прерывания у разных компиляторов немного отличается. Для IAR`a обработчик прерывания таймера Т0 по событию переполнение будет выглядеть так:



{
TCNT0 = T_POLL;

/*здесь должен быть опрос кнопки*/

TIMER0_OVF_vect - это адрес вектора прерывания по событию переполнение. Он берется из заголовочных файлов на микроконтроллер. В данном случае я взял его из файла iom16.h.

Первая строка обработчика (TCNT0 = T_POLL;) выполняет перезапись счетного регистра, то устанавливает его начальное значение. Если этого не сделать, таймер продолжит счет с 0. Перезапись счетного регистра нужно выполнять в начале обработчика прерывания.

Весь код для нашей задачи будет выглядеть примерно так. (Код приведен для IAR`a. Для других компиляторов нужно изменить заголовочные файлы и обработчик прерывания.)

#include
#include
#include

#define T_POLL 100

int main(void)
{
/*инициализация таймера*/

TCCR0 = 0;
TCCR0 = (0< TCNT0 = T_POLL;
TIFR |= (1< TIMSK |= (1< TCCR0 |= (1<

/*инициализация остальной периферии*/
DDRB |= (1<

Enable_interrupt();
while(1);

/*обработчик прерывания T0
по событию переполнение*/
#pragma vector = TIMER0_OVF_vect
__interrupt void TimerT0Ovf(void)
{
/*перезапись счетного регистра*/
TCNT0 = T_POLL;

/*опрос кнопки*/

/*инверсия PB0 для отладки*/
PORTB ^= (1<

Управление выводом OC0

В режиме Normal таймер Т0 может изменять состояние вывода OC0 при совпадении счетного регистра и регистра сравнения. Причем даже без прерываний. Варианты управления определяются разрядами COM01 и COM00 регистра TCCR0.

Вот пример программы, генерирующей прямоугольный сигнала на выводе ОС0.

#include
#include

int main(void)
{
/*инициализация таймера Т0*/

TCCR0 = 0;
TCCR0 = (0< TCNT0 = 0;
OCR0 = 0;
TIMSK = 0;
TCCR0 |= (1<

/*инициализация OC0*/
DDRB |= (1<

While(1);
return 0;
}

Вывод ОС0 будет менять свое состояние на противоположное при нулевом значении счетного регистра.

Несколько моментов относительно использования таймера

Обработчик прерывания таймера (да и любой другой периферии) нужно делать как можно короче.

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

И последнее. Может случится ситуация, что обработка прерывания таймера задержится (например, по вине другого обработчика) и регистр TCNT0 уже посчитает несколько тактов. Если просто перезаписать значение TCNT0, то следующее прерывание вызовется позже, чем нужно. Получится, что предыдущее (задержанное) и новое прерывания не выдержат требуемый интервал.

Эту ситуацию можно сгладить, если выполнять перезапись счетного регистра вот так:

TCNT0 = TCNT0 + startValue;

Сложение текущего значения счетного регистра с инициализируемым, учтет эти лишние такты. Правда есть одно НО! При больших значения startValue операция сложения может вызвать переполнение счетного регистра.

Например, startValue = 250, а таймер успел досчитать до 10. Тогда операция сложения приведет к такому результату:

10 + 250 = 260

Берем 8 разрядов от 260 получаем 4. В TCNT0 запишется 4.

Рассмотрим, как сделать таймер своими руками на микроконтроллере 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—;


Описание работы таймера/счётчика 1.
Прерывания от TC1

Таймер/счётчик 1 (TC1) представляет из себя 16-битный модуль, содержащий 10 8-битных регистров. Эти регистры фактически являются набором из 5 16-битных регистров. Счёт происходит в регистрах TCNT1H (Timer counter 1 High byte) и TCNT1L (Low byte), вместе составляющих 16-битный регистр TCNT1. ВНИМАНИЕ! Если использовать прямое чтение 8-битных регистров TCNT1H и TCNT1L, то нельзя быть уверенным, что эти регистры прочитались одновременно. Может произойти следующая ситуация: Счётчик содержал значение $01FF, Вы считали TCNT1H (содержащий значение 01 в какую-то переменную). За это время произошёл счётный импульс, и содержимое TCNT1L стало равно $00, а в TCNT1H записалось значение $02. Теперь Вы читаете значение TCNT1L в другую переменную, получаете в этой переменной значение $00 (ведь таймер-счётчик уже произвёл счёт). 16-битное значение этих переменных получилось $0100, но на момент считывания старшего байта содержимое счётчика было $01FF, и младший байт у Вас должен был прочитаться как FF. Для предотвращения такой ситуации служит временный регистр, содержащийся в блоке таймера-счётчика. Этот регистр прозрачный, т.е. действует автоматически. При считывании значения регистра TCNT1L в переменную, содержимое TCNT1H попадает в этот регистр. Затем при чтении старшего байта в переменную, считывается значение временного регистра. Временный регистр абсолютно прозрачен для пользователя, но для его корректной работы необходимо соблюдать такую последовательность действий:
Для 16-битной операции записи, старший байт должен быть записан первым. Младший - вторым.
Для операции 16-битного чтения, младший байт должен быть прочитан первым, а содержимое старшего байта считывается вторым.
Регистр TCCR1A служит для задания режимов работы таймера/счётчика 1:

Биты COM1A1,COM1A0, COM1B1 и COM1B0 - контролируют поведение выводов OC1A и OC1B.
Биты FOC1A, FOC1B, WGM11 и WGM10 служат для задания работы ТС1 как широтно-импульсного модулятора.
Скорость счёта ТС1 можно установить в регистре TCCR1B :

Где биты ICNC1, ICES1, WGM13 и WGM12 также служат для PWM, а CS12, CS11 и CS10 настраивают скорость счёта следующим образом:


В случае, если в эти биты записаны значения 000, ТС0 остановлен. Если записано 001, то тактовая частота процессора подаётся через схему делителя без изменений, и на каждый такт процессора ТС1 увеличивает значение в регистре TCNT1. Соответственно, если в CSxx Записано 101, то увеличение значения в TCNT1 происходит на каждый 1024-ый такт процессора.

16-битные регистры OCR1A и OCR1B служат для задания значения, при достижении которого в режиме счёта, ТС1 генерирует соответствующие прерывания.

Обработка прерываний от TC1

ТС1 при переполнении значения TCNT1 посылает процессору сигнал Timer/Counter 1 Overflow. Также процессору посылается сигнал Timer/Counter 1 A или B Compare Match при совпадении значений в регистрах TCNT1 и OCR1A и OCR1B соответственно. Реакция процессора на эти сигналы (вызов соответствующих прерываний) зависит от значения регистров TIMSK и флага I в Status регистре процессора.
Для задания реакции на события TC1 в регистре TIMSK служат четыре бита:

Бит 2 - TOIE1 - Когда этот бит установлен в 1 и разрешены прерывания, процессор реагирует на сигнал переполнения ТС1 и вызывает прерывание по вектору $010 (OVF1addr).
Бит 3 - OCIE1B - Когда этот бит установлен в 1 и разрешены прерывания, процессор реагирует вызовом прерывания по вектору $00E (OC1Baddr) на событие совпадения счёта с константой в регистре OCR1B. Бит 4 - OCIE1A - Когда этот бит установлен в 1 и разрешены прерывания, процессор реагирует вызовом прерывания по вектору $00C (OC1Aaddr) на событие совпадения счёта с константой в регистре OCR1A. Бит 5 - TICIE1 - Если установлен этот бит и разрешены прерывания, разрешено прерывание захвата ТС1, расположенного по вектору $00A (ICP1addr).

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

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

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

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

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

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

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

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