Программный таймер. Применение HAL

Сначала небольшая предыстория. Пользовался я раньше, значит, только прAVRославными контроллерами, да команды им посылал исключительно на богоугодном ассемблере. Все бы хорошо, но с портируемостью у асма, как известно, есть проблемы. В большинстве случаев, перенос какого-то куска интеллектуального труда с одного камня на другой подразумевал написание с нуля этого самого куска, но в реалиях периферии текущего мк. Вроде бы и несложно, да и обычно не особо напряжно, но как всегда есть НО. Проснулся интерес у меня к STMовским камням — обилие вкусной периферии плюс интересные цены прельщали (речь идет о STM8 и STM32). Вот тут-то я и задумался о том, что тут пора вспоминать С, да и изобретать свои велосипеды кроссплатформенно. Ибо каждый раз переписывать какое-нибудь типовое программное решение под другую архитектуру или модель контроллера, возможно и полезно с точки зрения тренировки и оптимизации, но грустно с точки зрения потраченного времени и сил. Такие вот дела.

А начал я, как следует из заголовка, с самого простого и необходимого модуля — программных таймеров.

Возникает резонный вопрос — а зачем, собственно, это нужно? Ответ вообщем-то лежит на поверхности. Часто возникают рутинные задачи типа проверить состояние кнопок, опросить датчик, светодиодиком поморгать в конце концов. Задачи эти как правило неспешные и не требуют высокой точности, но требуют привязки к временной оси. А значит нужно использовать таймеры. Отдавать каждой задаче по аппаратному таймеру весьма расточительно, да и обычно невозможно (ну не ПЛИСина у нас в конце то концов). Поэтому, как правило, расширяют возможности одного аппаратного таймера введением программных, которые будут тикать, к примеру, каждое переполнение первого.

У такой тривиальной задачи, как водится, имеется множество решений. Вот некоторые из них:
  • Псевдо-таймеры. Это когда в обработчике прерывания напрямую заводят переменную счетчик или несколько таких, и просто инкрементируют/декрементируют их с последующим сравнением со значениями соответствующих периодов. С внедрением на ходу и без какой-либо структуризации или типизации (хотя в асме по другому вообщем-то и не сделаешь).
  • Глобальное время. Создают большую переменную счетчик, например, 32-битную, и постоянно инкрементируют её каждое переполнение аппаратного таймера. Соответственно, для выполнения каких-то периодических задач используют минимум 2 параметра — текущее значение времени и значение времени, при котором задача должна завершится.
  • Служба таймеров. Логическое продолжение псевдо-таймеров, но уже причесанное и структурированное.

Мне больше всего по душе последний вариант. Когда я составил список желаемых функций, которые мне хотелось бы иметь в подобной службе, я отправился погуглить, какие реализации подобного уже существуют на просторах сети. И наткнулся на статью «Многозадачный программный таймер» за авторством Mihail . Его реализация оказалась на мой взгляд весьма удачной и отвечала практически всем моим требованиям. Поэтому при написании своего таймера я во многом вдохновлялся именно этой статьей и комментариями к ней. Единственное, что меня совершенно не устраивало, так это железная привязка к STM32. Мне же нужна была легкая и непринужденная портируемость, которую я и собрался получить за счет так называемого HAL.

Hardware Abstraction Layer (HAL) — уровень аппаратной абстракции, который отделяет алгоритм от железа, позволяя при переносе программы на другое железо переписать только HAL часть, не трогая функционал.

Описание модуля SwTimer
Ну вот и добрались до «мяса». Структура модуля программных таймеров SwTimer (Software Timer) представлена на схеме.
структурная схема
Все весьма просто:
  • swtimer.h — заголовочный файл, который и включается в нужный проект. Является единственным интерфейсом доступа к модулю SwTimer. В нем делаются основные настройки и представлены прототипы функций управления.
  • swtimer.c — вся жезезонезависимая логика работы находится здесь.
  • <MCU_МODEL>.C — тот самый HAL модуль, позволяющий работать модулю с соответствующей моделью контроллера. Все HALы рассортированы по семействам и лежат в папке HAL (Капитан Очевидность активно помогал в написании).

Работа с модулем SwTimer

Вообщем-то все банально и элементарно. Для того, чтобы использовать в проекте модуль необходимо скопировать в папку с проектом вышеупомянутые три файла и заинклудить заголовочный в нужные места. Далее необходимо произвести настройку, изменив соответсвующие дефайны в полях «Настройки» в файле swtimer.h.

swtimer.h


#ifndef SWTIMER_H
#define SWTIMER_H

	#include <stdint.h>

//**********	НАСТРОЙКИ	**********
	#define SWTIMER_SIZE 		8		//разрядность программных таймеров (8, 16 или 32 бит)
	#define SWTIMER_MAX		5		//максимальное количество программных таймеров
	#define SWTIMER_FASTINT		0		//0 - обслуживание массива таймеров идет прямо в перывании, 1 - выставляется лишь флаг на обслуживание(само обслуживание вызывается SwTimer_Update())
	#define SYSTIMER_T		1000	        //[мкс] - период переполнения аппаратного таймера
	#define SYSCLK			16		//[МГц] - системная таковая частота мк после делителей и прочего
	/*
		На самом деле точное значение периода переполнения часто установить нельзя. Особенно, если нет
		в таймере компаратора или регистра перезагрузки. Тогда приходится работать с его переполнением,
		а значит при этом COMP=255 всегда.
		Поэтому стоит иметь ввиду, что реальное значение периода в общем случае можно определеить по:
			
			T_REAL = (TIMER_DIV*COMP)/SYSCLK
	
	*/
//***********************************

	#define __SWTIMER_SIZE(N)		uint##N##_t
	#define _SWTIMER_SIZE(N)		__SWTIMER_SIZE(N)
	#define TSwTimerSize			_SWTIMER_SIZE(SWTIMER_SIZE)
			
	#define SWTIMER_NO_OVF			0
	#define SWTIMER_OVF			1
	
	#define SWTIMER_NO_HANDLER		0			
	
	typedef enum
	{
		SWTIMER_MODE_EMPTY,
		SWTIMER_MODE_STOP,
		SWTIMER_MODE_CYCLE,
		SWTIMER_MODE_SINGLE
	} TSwTimerMode;
	
	typedef void (*TSwTimerFunc)(void);

	
	void	 		SwTimer_Init(void);
	void			SwTimer_Sync(void);

	uint8_t 		SwTimer_Add(TSwTimerMode mode, TSwTimerSize compare, TSwTimerFunc handler);
	uint8_t 		SwTimer_Delete(uint8_t swtimer_id);

	TSwTimerSize 	SwTimer_GetCounter(uint8_t swtimer_id);
	uint8_t	 		SwTimer_SetCounter(uint8_t swtimer_id, TSwTimerSize counter);

	uint8_t 		SwTimer_GetStatus(uint8_t swtimer_id);

	TSwTimerMode	SwTimer_GetMode(uint8_t swtimer_id);
	uint8_t	 		SwTimer_SetMode(uint8_t swtimer_id, TSwTimerMode mode);

	TSwTimerSize 	SwTimer_GetCompare(uint8_t swtimer_id);
	uint8_t	 		SwTimer_SetCompare(uint8_t swtimer_id, TSwTimerSize compare);
	
	#if (SWTIMER_FASTINT)
		uint8_t 		SwTimer_GetIntFlag(void);
		void			SwTimer_Update(void);
	#endif

#endif


Ну и осталось лишь рассмотреть доступные функции и их аргументы, чтобы окончательно снять всякую неопределенность.

void SwTimer_Init(void);
Инициализирует и подготавливает модуль к работе. Необходимо вызвать перед какими-либо действиями с модулем.

void SwTimer_Sync(void);
Синхронизирует счет таймеров. Проще объяснить на примере. Создал в разное время 2 таймера — один с периодом 5 мс, другой — 10 мс. Каждый из них при переполнении вызывает функцию инвертирующую состояние своего вывода. На левой картинке осциллограмма до вызова SwTimer_Sync(), на правой — после. Очевидно, что если периоды не кратны, то ничего не выйдет.допосле

uint8_t 	SwTimer_Add(TSwTimerMode mode, TSwTimerSize compare, TSwTimerFunc handler);
/*      TSwTimerMode mode - режим таймера. Выше в enum перечислены все возможные.
        TSwTimerSize compare - значение для сравнения. НУЛЬ СЧИТАЕТСЯ! Т.е. если хотим переполнения 
            программного каждые 10мс при переполении аппартного каждую 1мс - пишем 9.   
        TSwTimerFunc handler - обработчик этого программного таймера, если не нужен, то пишем 0.
*/
Добавляет таймер в первую попавшуюся пустую ячейку, если быть точнее то в ту где встретит режим SWTIMER_MODE_EMPTY. Возвращает номер таймера при удачном создании, либо 255 — при неудачном.

uint8_t 	SwTimer_Delete(uint8_t swtimer_id);
/*     uint8_t swtimer_id - номер таймера, полученный после SwTimer_Add  */ 
Удаляет таймер, оставляя после него пустую ячейку. Возвращает 0 при успешном удалении, 255 — при провальном.

uint8_t 	SwTimer_GetIntFlag(void); 
Возвращает 1, если сработало прерывание аппаратного таймера, 0 в обратном случае (работает при SWTIMER_FASTINT 1).

uint8_t 	SwTimer_Update(void); 
Производит обработку структур таймеров — инкрементирует, ставит флаги, заходит в обработчики и прочее. Так как обработка выведена за пределы прерывания, очевидно, что её надо вызывать вручную. Вышесказанное опять же справедливо только при SWTIMER_FASTINT 1.

TSwTimerSize 	SwTimer_GetCounter(uint8_t swtimer_id);
uint8_t	 	SwTimer_SetCounter(uint8_t swtimer_id, TSwTimerSize counter);

uint8_t 	SwTimer_GetStatus(uint8_t swtimer_id);

TSwTimerMode	SwTimer_GetMode(uint8_t swtimer_id);
uint8_t	 	SwTimer_SetMode(uint8_t swtimer_id, TSwTimerMode mode);

TSwTimerSize 	SwTimer_GetCompare(uint8_t swtimer_id);
uint8_t	        SwTimer_SetCompare(uint8_t swtimer_id, TSwTimerSize compare);
Все эти функции как две капли воды похожи. Там где SwTimer_Get… — получаем возвратом, то что запросили. Там где SwTimer_Set… — загружаем новое значение в соответствующую переменную(успех = возврат 0). При неудачной попытке Get или Set получаем 255. Стоит упомянуть что SwTimer_GetStatus() возвращает текущий флаг статуса таймера (1-переполнен, 0-нет события) и автоматически сбрасывает его, если тот был установлен. Этот флаг, позволяет расположить обработчик таймера где-нибудь в main() например, а не вызывать его, будучи в прерывании.
Ну вообщем то описание фукнций на этом и кончается. Если какие-то непонятки, то лучше сразу в swtimer.c — код весьма мал и прозрачен.

Создание своего HAL-файла

Структура выстроена таким образом, что портирование происходит весьма легко и непринуженно — необходимо лишь оформить один небольшой файл. Наверное, лучше сразу выложить скелет файла, чем еще 20 строчек объяснять что к чему :) Единственное что — название функций не менять, ну окромя обработчика прерывания (если потребуется). Кстати, примеры готовых можно подсмотреть в прикрепленных файлах.
HAL_SAMPLE.C

#include "swtimer.h"
//#include "<MCU_MODEL.h>"

void SwTimer_IntHandler(void);

//Эта функция должна быть оформлена как прерывание от задействованного аппаратного таймера.
//Как правило это прерывание переполнения(обновления) или компаратора.
//Больше никаких изменений не требуется, разве что организовать сброс флагов прерывания(если требуется).
void HAL_SwTimer_IntHandler(void)
{
	SwTimer_IntHandler();
	return;
}
	
//В этой функции необходимо инциализировать аппаратный таймер, включить его прерывание переполнения(обновления) или компаратора и запустить его
void HAL_SwTimer_Init(void)
{
	
	return;
}

//В этой функции необходимо обнулить счетчик аппаратного таймера и сбросить его флаги(если есть)
void HAL_SwTimer_Sync(void)
{

	return;
}

//В этой функции необходимо отключить прерывание аппаратного таймера
void HAL_SwTimer_Lock(void)
{

	return;
}

//В этой функции необходимо возобновить прерывания аппаратного таймера
void HAL_SwTimer_Unlock(void)
{

	return;
}


Пример использования

Сей опус, можно было бы считать неполным если бы не было примера :) Сделал на том, что оказалось ближе всего — STM8L-Discovery (stm8l152c6t6). Проект сделан в IAR. В нем завел 2 таймера — один переполнялся каждые 100мс, дергая вывод B0 из обработчика в прерывании, другой каждые 50, но дергал уже B3 из main().

#include "iostm8l152c6.h"
#include "swtimer.h"

uint8_t TMR1;
uint8_t TMR2;

void StupidTask1(void)
{
    PB_ODR ^= (1<<0);
}
void StupidTask2(void)
{
    PB_ODR ^= (1<<3);
}

int SystemInit(void)
{
    CLK_CKDIVR_bit.CKM = 0;             //делитель в положение /1
    PB_DDR_bit.DDR0  = 1;               //на выход
    PB_CR1_bit.C10   = 1;               //push-pull
    PB_DDR_bit.DDR3  = 1;               //на выход
    PB_CR1_bit.C13   = 1;               //push-pull
    
    SwTimer_Init();
    TMR1 = SwTimer_Add(SWTIMER_MODE_CYCLE, 99, StupidTask1);            //100мс
    TMR2 = SwTimer_Add(SWTIMER_MODE_CYCLE, 49, SWTIMER_NO_HANDLER);     //50мс
    
    asm("RIM");
    return 0;
}

int main( void )
{
  SystemInit();
  while(1)
  {
      if(SwTimer_GetStatus(TMR2)==1)
      {
        StupidTask2();
      }
  }
  return 0;
}

тест таймеров

Вообщем то, на этом и все. Все нужные файлы прикреплены. Жду предложений и замечаний в комментах.

UPDATE1
Пользователь teplofizik натолкнул на идею ввести такую опцию как SWTIMER_FASTINT в хедер.
Раньше только обработчики таймеров можно было вынести за прерывание, теперь же в зависимости от значения упомянутого дефайна обслуживание массива таймеров может идти как в прерывании, так и вне его.
Наличие флага произошедшего прерывания можно проверить по SwTimer_GetIntFlag(), запустить обслуживание таймеров по SwTimer_Update().
Таким образом любители быстрых прерываний, могут выкрикнуть «УРА!» и подбросить шапки в небо :)
Прикрепленные файлы обновлены.
  • +4
  • 20 ноября 2014, 23:28
  • 1essor1
  • 2
Файлы в топике: SwTimer STM8 project.zip, SwTimer module.zip

Комментарии (27)

RSS свернуть / развернуть
спасибо.
+1
А начал я, как следует из заголовка, с самого простого и необходимого модуля — программных таймеров.

Возникает резонный вопрос — а зачем, собственно, это нужно?
Правильная постановка вопроса. Не нужно это…

Почему не использовать ОС с вытеснением? Ну неправильно это — в прерывании функции по списку вызывать — пережаваёте это дело задаче с соответствующим приоритетом.
0
Почему не использовать ОС с вытеснением?
Ну потому что как минимум надо знать как ей пользоваться)

Ну неправильно это — в прерывании функции по списку вызывать — пережаваёте это дело задаче с соответствующим приоритетом.
Ну а насчет неправильно… Как говорит один преподаватель в ВУЗе: «Инженерное искусство — это искусство компромиссов». Т.е. всегда какие-то характеристики улучшают за счет допустимого ухудшения других. В моем случае, целью было получить максимальную простоту использования, поэтому умышленно пришлось идти на очевидное ухудшение производительности и оптимизации. Но даже в таком случае, этот модуль применим в большинстве случаев без каких-либо последствий.
0
Не скажите, есть большое количество задач мелких, которые времени почти не занимают, но требуют регулярного запуска. Да и ос может быть перебором.

А вызывать можно и не из прерывания, я уже год так делаю. прерывание только расставляет флаги на запуск, а процедуры вызываются из основного цикла — не нагружаем прерывания тяжёлыми задачами, да ещё и избегаем конкуренции в потоках, шик.
+3
О, а это идея — можно доп настройку препроцессора ввести, которой можно будет выбрать — обработка массива таймеров в прерывании, либо выставление флага, для вызова из мэйна.
0
Вытесняйка, по моему, в avr & stm8, перебор. Вполне хватает кооперативки аля DI, там и прог таймера и легко доработать под себя и рам жрет мало.
0
Вытесняйка, по моему, в avr & stm8, перебор.
АВРы разные бывают: ОЗУ до 8-16кБ, FLASH — до 256 кБ и можно ешё и внешнюю RAM подключить. Вполне себе достаточно для вытесняющеё ОСи.

Вполне хватает кооперативки
У кооперативки код не такой наглядный.

кооперативки аля DI,
Где на неё можно посмотреть?

рам жрет мало.
Рамы хватает.
0
Вполне себе достаточно для вытесняющеё ОСи.
Достаточно, но не обязательно лепить её всюду.

У кооперативки код не такой наглядный.
Смотря как оформить. Событийная модель тоже неплоха и сейчас для многих естественна, особенно для тех, кто пришёл с программирования для ПК).
0
Смотря как оформить.
Для меня вобщем-то вопрос — как кооперативку наглядно оформить. Всё как-то хуже RTOS выглядит.
0
Как я уже сказал, мне нравятся событийные подходы. Тикнул таймер, нажали кнопку, пришёл пакет — событие.

Конечно, при превышении определённого уровня сложности надо переходить на другие, более удобные для больших масштабов, модели, но пока проект небольшой это весьма удобно. И модульно.
0
А лепить вытеснялку куда ни попадя это правильно?
Правильно это когда все работает и удовлетворяет требованиям ТЗ, в том числе по надежности и себестоимости. А как оно реализовано совершенно неважно.
Какие-то задачи можно решить таким способом, а какие-то нет, а какие-то так решать можно, но не оптимально, но и с вытеснялками, кооперативками, планировщиками задач та же история.
Когда есть программные приоритеты прерываний, то вообще можно всю логику построить на прерываниях. И некоторые даже так пишут реальные проекты (хотя я и не сторонник такого подхода по многим причинам). И они работают, обеспечивая отличное время реакции, распараллеливая кучу «процессов» в МК.
Я тоже, как и Вы, не стану делать свои проекты таким способом (вызов обработчиков таймеров из прерываний), ибо цена ошибки бывает очень высока, а уследить за всеми нюансами при таком подходе весьма непросто, если обьем кода нетривиальный. Но кому-то это может подойти.
К тому же, автор заложил возможность использовать таймерную службу и без вызова обработчиков в прерывании, а простым опросом статуса, за что ему огроменный плюс. Тот, кто никогда не станет использовать какую-либо из фич этой библиотеки, может выпилить ее без особого труда. Код маленький и весьма читаемо написан. Хоть и не люблю проги без комментариев или почти без них. Но в данном случае и так все прозрачно.

Спасибо 1essor1 за библиотеку. Давно хотел сделать тоже нечто подобное, чтобы легко перетаскивалось на другие архитектуры и позволяло одинаково работать с программными таймерами. А то изобретаю каждый раз заново. Думаю, что мне Ваши наработки очень даже пригодятся. Спасибо.
+2
Правильно это когда все работает и удовлетворяет требованиям ТЗ
Ну это да, ТЗ это главное.

А как оно реализовано совершенно неважно.
Ну не совсем уж неважно, программу как правило надо бывает дорабатывать, исправлять ошибки и тд.
0
Ну не совсем уж неважно, программу как правило надо бывает дорабатывать, исправлять ошибки и тд.
Да, конечно, тут согласен.
0
Благодарю за развернутый комментарий! Если хоть один человек воспользуется, то значит что статья уже была писана не зря)
0
По-моему, полезный модуль. Как насчет добавить параметр swtimer_id для хэндлера? Можно будет тогда использовать один обработчик на несколько таймеров, мне это видится полезным. Еще было бы круто добавить каскадирование таймеров — например, завести некое поле ID для указания опорного таймера, по событиям которого будет тикать другой. Еще можно расширить статус, добавить к примеру, флажок, который будет переключаться каждое переполнение таймера — подходит для всяческих мигалок.
+1
Один обрабочик на нескольких — указывай при создании один и тот же обработчик в поле Handler.

Каскадирование — честно говоря не пойму зачем? Чтоб получить период переполнения больше максимально возможного?

Доп. флажок — мне вообщем-то кажется излишеством) В примере показан простейший вариант мигалки, флажок все несколько усложнит.
0
А как в обработчике различать, от какого таймера произошел вызов? Если, допустим, нужно от нескольких таймеров выполнять одни и те же действия, но знать, который сработал. Можно для каждого таймера задать свой обработчик, в нем уже вызывать нужную функцию с разными аргументами, но зачем плодить обработчики?
Каскадирование полезно в службе времени, самый простой пример — счетчики минут и часов.
Доп. флажок — просто идея. Можно конечно и XORить.
Кстати, еще мысль — можно добавить регистр сравнения, и обработчик по событию сравнения. Получится практически готовый PWM — генератор
0
Хорошо, добавление аргумента id в хендлер, повысит гибкость, но я все равно не могу придумать ситуацию, когда в один и тот же обработчик надо попадать из разных таймеров. А если их переполнения совпадут при кратных периодах — действие будет выполнено 2 раза подряд?

Про каскадирование убедил, добавлю)

Регистр сравнения и еще обработчик считаю не нужен — нагромождение лишнее никчему, лучше уж полностью железный шим юзать.
0
Приз за способность вложить максимальное количество слов в самую короткую мысль.
0
Подробное описание да свободным стилем по умолчанию не может быть коротким.
0
Спасибо за вашу высокую оценку к моему «творчеству», хотелось бы добавить еще несколько моментов:

Поскольку в STM32 имеются приоритеты прерываний, поэтому было бы логичнее заимствовать некоторые ссылочки из моей относительно обновленной версии… Но дело даже не в этом, а в том, что в AVR — этих приоритетов нет. Следовательно, если функция обработки таймера будет находиться в прерывании — это надолго может задержать срабатывания других прерываний. Как выход из этой ситуации — размещение в основном цикле.
Однако мое предложение, оставив эту полезную функцию (перемещение обработчика в main) следует улучшить работу в режиме работы обработчика в прерывании, а именно:
При входе в прерывание 1 — запрет прерывания по таймеру и разрешение глобального прерывания. Эти действия так же можно оформить в HAL функции. В STM32 например, функция будет пустой, а для AVR — типа {TIMSK &= ~(1<<bit); sei();}
По выходу имеем обратный процесс: {TIMSK |= 1<<bit; cli();} Эти функции нужно заключить в условие #if FASTINT == 1 #endif, чтобы при вызове из основного цикла они не выполнялись.

Что это дает:

При входе в прерывание — буквально через несколько тактов мы разрешаем прерывание, что позволяет более важным прерываниям вклиниваться в работу обработки таймера. Своего рода получаем псевдоприоритетность, с минимальным приоритетом для таймера
0
Получается, плюсы по скорости срабатывания таймера мы имеем, а минусы из-за задержки этого блока — практически сводим к нулю. Чем не вариант?
0
Замечания ценные, благодарю) В ближайшее время исправлю.
0
ошибнулся в последовательности при выходе из функции. Правильно так: {cli(); TIMSK |= 1<<bit;}
0
По выходу имеем обратный процесс: {TIMSK |= 1<<bit; cli();}
Правильнее так:

cli();
TIMSK |= 1<<bit; 
0
я уже ответил на это.
0
Упс, не успел.
:)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.