![](https://habrastorage.org/storage/0dc42ccd/1dfbc8c9/5a635ecd/51698b71.jpg)
В то же время в микроконтроллерах нередко нужно одновременно с этим иметь дело с микросекундами – периоды импульсов, антидребезговое ожидание и т. п.
Есть также устройства, которые работают непрерывно многие часы и даже сутки – авиационная техника, автомобильная, скважинные устройства (там речь идет иной раз о непрерывной работе несколько дней). В этих случаях недопустимо переполнение таймеров и 8-битных переменных.
Хотелось бы это все объединить в одно изящное и универсальное решение – иметь средство измерения времени с точностью до микросекунды, не переполняющееся несколько дней.
Почему бы и нет? Помучился я некоторое время и родил решение для 8-битных микроконтроллеров AVR. Для этого я задействовал 8-битный таймер-счетчик и 4х-байтную переменную. С PIC-ами и AT89 я сейчас не работаю, а с другими embedded-платформами не дружу. Впрочем, если читатели помогут – сделаю и для них.
Достоинства – код в высшей степени повторяемый (я уже 5-ое устройство с ним делаю); простота в работе (не используются прерывания для клиентской части работы); клиентская часть кода условно платформенно-независимая; в прерывании – одна операция суммирования (но, правда, для 4-хбайтной величины); нет внешнего устройства — таймера реального времени.
Недостаток я нашел один – занят один такой полезный и всегда нужный таймер…
Статья будет интересна в первую очередь начинающим — Америку я тут не открыл.
Теория
Итак, я имею в своем распоряжении устройство на основе Atmega16A с кварцем 12MHz. Берем его таймер-счетчик 0. Это восьмиразрядный таймер – нам и хватит. Почему? Считаем:
- берем 12 MHz от кварца и берем коэффициент деления на 8 – получаем частоту 1500 KHz;
- берем режим CTC (сброс при совпадении) и ставим прерывание на совпадение с 150 – получаем частоту срабатывания прерывания 10 KHz;
- на этом самом прерывании инкрементируем переменную (получается инкремент каждые 0.1 миллисекунды);
- если это беззнаковая 32х-битная величина, то она переполнится приблизительно после
- 429496729.6 миллисекунд;
- 42949.7 секунд;
- 7158.3 минут;
- 119.3 часов;
- 4.97 суток.
Другими словами, такое решение мне создает таймер с точностью до 0.1 миллисекунды на (почти) 5 суток (надо тут, правда, учитывать, что реальные кварцы имеют погрешность — об это далее). А если еще анализировать значение собственно таймера 0 – он инкрементируется каждые 2/3 микросекунды – то можно получит счетчик с точностью до 0.67 микросекунды.
Хватит? Мне – за глаза. Используя счетчик по 0.1 миллисекунды, я в своих проектах:
- считаю длительности свечения и пауз между ними светодиодов;
- учитываю timeouts при работе с UART, USB;
- задаю всевозможные ситуации в тестовом оборудовании – сложные пространственно-временнЫе комбинации;
- выдерживаю заданные промежутки времени при опросе АЦП и прочих датчиков;
- сообщаю компьютеру время своей (устройства) работы и с заданным интервалом времени передаю информацию;
- с учетом счетчика до микросекунды я осуществляю антидребезговый контроль при нажатии клавиш, анализ импульсов в протяженных линиях.
И все это спокойно влазит В ОДИН КОНТРОЛЛЕР ATmega16! Причем это не Ассемблер, а межплатформенный Си! И никакого внешнего счетчика реального времени!
Неплохо, да?
Настройка для AVR
Как это все сделать в AVR?
Прежде всего, заводим внешнюю переменную, которую я называю «ДециМиллиСекунда»:
// в main.h
typedef unsigned long dword; // беззнаковое 32х-битное целое
extern volatile dword dmsec; // 0.1msec
// в main.c
volatile dword dmsec;
Как верно подметил @no-smoking, эта переменная должна быть volatile, чтобы ее компилятор не попытался оптимизировать.
Инициализацию этой переменной я делаю в функции:
dmsec = 0;
Далее я задаю режим работы таймера 0:
// . таймер 0 – 0.1msec
Timer0_Mode (TIMER_Mode_CTC | TIMER0_Clk_8);
Timer0_Cntr (149);
Timer_Int (Timer0_Cmp);
При этом в каком-нибудь MCU_init.h объявляю все, что надо:
// в mcu_init.h
#include <mega16.h>
// . TIMSK
#define Timer0_Cmp (1 << 1) // совпадение таймера 0
// . TCCRn
#define WGM1 (1 << 3)
#define CS1 (1 << 1)
// . источник сигнала для таймера 0
#define TIMER0_Clk_8 CS1 // предделитель 8
// . режим работы таймера
#define TIMER_Mode_CTC WGM1 // CTC (сброс при совпадении)
// . настройка таймера
#define Timer_Int(Mode) TIMSK = (Mode)
#define Timer0_Mode(Mode) TCCR0 = (Mode)
#define Timer0_Cntr(Cntr) OCR0 = (Cntr)
Ну и далее, когда можно, разрешаю прерывания:
#asm ("SEI")
Осталось описать прерывание. Это проще, чем все предыдущее:
#include <mega16.h>
interrupt [TIM0_COMP] Timer0_Compare (void)
{
++dmsec;
}
Все, таймер описан, настроен и запущен!
Настройка для PIC
Вот что мне подсказали уважаемые PICоманы:
На пиках это легко повторяется при помощи модуля Timer2. Именно в нем есть аналогичная функция прерывания по совпадению.
PR2 = 75 — значение, при котором таймер обнулится и сгенерирует прерывание
T2CON.T2CKPS = 2 — прескалер 1:16
T2CON.T2OUTPS = 0 — без постскалера
T2CON.TMR2ON = on — таймер включен
IPR1.TMR2IP = 1 --высокоприоритетное прерывание
PIR1.TMR2IF = off --сбрасываем флаг прерывания
PIE1.TMR2IE = on --включаем прерывание по совпадению TMR2 и PR2
INTCON.GIE = on --включаем обработку прерываний
Как видно, прескалер тут в 2 раза больше, потому PR2 в 2 раза меньше.
Данные настройки будут генерировать прерывания с частотой 10 kHz при системной частоте в 48 MHz (на таймер идет Fosc/4) — стандартная частота для USB Full Speed.
Использование
Код для клиента этого таймера получается кросс-платформенным (если не считать обращения к значению таймера 0 в AVR).
Вот фрагмент кода обмена по USB:
#include "main.h" // тут переменная dmsec, next_USB_timeout
#include "FT245R.h" // тут функции работы с модулем USB
#include "..\Protocol.h" // тут протокол обмена микроконтроллер - компьютер
// **
// ** Анализ пакетов по USB
// **
void AnalyzeUSB (void)
{
#define RECEIVE_BYTE(B) while (!FT245R_IsToRead)\
{ if (dmsec > end_analyze) return; }\
B = FT245_ReadByte ();
#define RECEIVE_WORD(W) // аналогично для 2х байт
#define RECEIVE_DWORD(W) // аналогично для 4х байт
dword end_analyze, d;
NewAnalyze:
if (!FT245R_IsToRead) // нет пакетов?
return;
end_analyze = dmsec + max_USB_timeout; // timeout для текущего анализа
next_USB_timeout = dmsec + MaxSilence_PC_DEV; // timeout для общего обмена
RECEIVE_BYTE (b) // заголовок пакета
switch (b)
{
case SetFullState:
RECEIVE_DWORD (d); // читаем слово
is_initialized = 1; // обрабатываем
ChangeIndicator ();
break;
} // switch (pack)
goto NewAnalyze;
#undef RECEIVE_BYTE // отменяем #define
#undef RECEIVE_WORD
#undef RECEIVE_DWORD
}
Макрофункции RECEIVE_BYTE, RECEIVE_WORD, RECEIVE_DWORD реализуют процедуры чтения с учетом timeout для данной фазы обмена. В итоге, если чего зависло на другой стороне, то микроконтроллер не впадет в «спячку». Обратите внимание – WatchDog не понадобился! И все благодаря переменной/константе max_USB_timeout, которая задает timeout с точностью до 0.1 миллисекунды.
Точно также реализуется анализ «тишины в эфире» переменной next_USB_timeout. Это позволяет микроконтроллеру 1) узнать, что компьютер куда-то исчез, 2) как-то об этом сигнализировать (в моем случае загорается светодиод «ошибка»). Константа/переменная MaxSilence_PC_DEV позволяет варьировать понятие «тишины» в широчайших пределах – от доли миллисекунды до нескольких суток.
Аналогично реализуются все остальные моменты.
Если же вам нужно использовать счетчик микросекунд, то там появляется функция сравнения:
#define GetUSec(A,B) { #asm ("CLI"); A = dmsec; B = TCNT0; #asm ("SEI"); }
// **
// ** Разница во времени между событиями с точностью до 2/3usec
// **
dword Difference (dword prev_dmsec, byte prev_usec)
{
dword cur_dmsec;
byte cur_usec;
dword dif;
// . засекаем текущее время
GetUSec (cur_dmsec, cur_usec);
// вычисляем разницу
dif = cur_dmsec - prev_dmsec;
dif <<= 8;
if (cur_usec < prev_usec)
dif += 255 + (dword) cur_usec - prev_usec;
else
dif += cur_usec - prev_usec;
return dif;
}
Функции передается предыдущий момент времени – предыдущее значение dmsec и таймера 0.
Вначале мы макросом GetUSec останавливаем прерывания, чтобы в момент копирования не испортилось значение dmsec и счетчика. И копируем текущее время.
Далее мы приводим разницу во времени к формату 2/3 микросекунды с учетом переполнения.
Ну и возвращаем это время.
А далее мы это используем в обычном if для контроля антидребезга и прочих мероприятий. Только не забудьте также приостановить прерывания при засекании текущего момента времени – а лучше используйте макрос GetUSec.
Результаты
Этот таймер оказался для меня в высшей степени удобным решением. Думаю, он и вам пригодится. А применил я его в следующих своих проектах:
- Коммутатор фехтовальных ситуаций. Это здоровенная плата пол на пол метра с тремя контроллерами — ATmega128 как центральный и ATmega64 как два вспомогательных (правая и левая стороны). Между тремя контроллерами и их компонентами нет гальванической связи — питание на основе ионисторов, связь через опторазвязки. Центральный контроллер заряжает группы одних ионисторов и питает в это время обе стороны от других ионисторов. Тут пришлось сделать многоступенчатый алгоритм коммутации всего этого с тем, чтобы минимизировать взаимосвязь. В частности, речь идет о слаженной работе 8 реле — тут работают таймеры на 3.3мсек (гарантированное время срабатывания реле). Ну и, собственно, обе стороны управляют 10 реле и еше с пол сотни мультиплексоров. Все это хозяйство работает с четко заданными временнЫми характеристиками (с точностью до 1 мсек, максимальные длительности — 6 секунд). Ну и, в конце концов, банальные timeout для USB, UART.
- Датчик глубины. Здесь я решаю другую задачу (проект в работе). Имеются два проводника (многометровые), задающие ситуацию «сдвиг вверх на 1 см» и «сдвиг вниз на 1 см». Способов задания направления множество. В любом случае это определенные комбинации импульсов. С помощью этого таймера я определяю дребезг, длительность устойчивого импульса. С компьютера задается максимально допустимое время дребезга (10 микросекунд тут хватает), антидребезговое ожидание, минимальная/максимальная длительность импульса. Ну и есть режим отладки — датчик становится логическим анализатором. Это позволяет отлаживать работу линии и корректировать коэффициенты. Ну и опять же timeout, светодиоды.
- Датчик аналоговых сигналов. Банальное 8-ми канальный АЦП. Здесь я использую таймер для выдерживания необходимых пауз.
Уважаемые хабрапользователи с других платформ могут мне подсказать код инициализации соответствующего таймера, а также правила доступа к нему — я это тут добавлю. Возможно, что для других платформ будет необходимо подобрать другие времена. Но в любом случае это должно быть нечто в пределах нескольких единиц микросекунд для самого таймера и нечто кратное 100 микросекунд для переменной-счетчика. Ибо, как оказалось, иногда одной миллисекунды не хватает.
UPD
Несколько слов о стабильности кварцевых резонаторов
Как меня в комментариях абсолютно точно поправил nerudo, считать-то можно почти 5 суток с шагом в 2/3 микросекунды, но погрешность этих вычислений не нулевая…
Возьмем тот самый кварц HC49/S на 12 MHz, что я использую. У производителя KSS заявленная точность +-15… +-50 x 1e-6 — это та самая ppm. Если не ошибаюсь, это означает, что за 1 секунду набежит погрешность 15 микросекунд. Значит, за 4.97 суток мы получим погрешность в 268 миллисекунд. Если же взять кварц покруче — скажем, с 1 ppm, то мы получим 18 миллисекунд за то же время.
Не фонтан — с одной стороны. С другой стороны — мы же не делаем хронометр высокого класса! По крайней мере, я себе такой задачи не ставлю. Надо учитывать этот факт.
Столкнулись мы и с этой проблемой — как между двумя разнесенными устройствами добиться точности в 1 миллисекунду за 45 минут. Но об это уже в другой раз.