Pull to refresh

Начинающим: счетчик на микроконтроллере с шагом 2/3 микросекунды и переполнением в несколько суток

Programming microcontrollers *
Часто при работе микроконтроллерного устройства есть необходимость отсчитывать «антропоморфное» время – сколько долей секунды светиться светодиоду, максимальный промежуток времени double-click и т. д. В общем, отсчитывать не только нано- и микросекунды, но и десятки миллисекунд, а то и секунды, минуты и даже часы (боюсь сказать о сутках…).
В то же время в микроконтроллерах нередко нужно одновременно с этим иметь дело с микросекундами – периоды импульсов, антидребезговое ожидание и т. п.
Есть также устройства, которые работают непрерывно многие часы и даже сутки – авиационная техника, автомобильная, скважинные устройства (там речь идет иной раз о непрерывной работе несколько дней). В этих случаях недопустимо переполнение таймеров и 8-битных переменных.
Хотелось бы это все объединить в одно изящное и универсальное решение – иметь средство измерения времени с точностью до микросекунды, не переполняющееся несколько дней.
Почему бы и нет? Помучился я некоторое время и родил решение для 8-битных микроконтроллеров AVR. Для этого я задействовал 8-битный таймер-счетчик и 4х-байтную переменную. С PIC-ами и AT89 я сейчас не работаю, а с другими embedded-платформами не дружу. Впрочем, если читатели помогут – сделаю и для них.
Достоинства – код в высшей степени повторяемый (я уже 5-ое устройство с ним делаю); простота в работе (не используются прерывания для клиентской части работы); клиентская часть кода условно платформенно-независимая; в прерывании – одна операция суммирования (но, правда, для 4-хбайтной величины); нет внешнего устройства — таймера реального времени.
Недостаток я нашел один – занят один такой полезный и всегда нужный таймер…
Статья будет интересна в первую очередь начинающим — Америку я тут не открыл.


Теория


Итак, я имею в своем распоряжении устройство на основе Atmega16A с кварцем 12MHz. Берем его таймер-счетчик 0. Это восьмиразрядный таймер – нам и хватит. Почему? Считаем:
  1. берем 12 MHz от кварца и берем коэффициент деления на 8 – получаем частоту 1500 KHz;
  2. берем режим CTC (сброс при совпадении) и ставим прерывание на совпадение с 150 – получаем частоту срабатывания прерывания 10 KHz;
  3. на этом самом прерывании инкрементируем переменную (получается инкремент каждые 0.1 миллисекунды);
  4. если это беззнаковая 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 минут. Но об это уже в другой раз.
Tags:
Hubs:
Total votes 32: ↑30 and ↓2 +28
Views 20K
Comments Comments 43