Pull to refresh

Программный многозадачный таймер на МК

Reading time 7 min
Views 16K
В различного рода сложности реализуемых алгоритмов при программировании МК, всегда возникают рутинные циклические и не очень задачи. Одни требуют повышенной точности, другие таким критерием не обязаны обладать. Аппаратных таймеров на борту МК может быть приличное количество, например STM32F4 — аж 14 штук, и это не считая SysTick (системного), а в других и пара тройка за счастье: тот же PIC16, например.

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

Вместо предисловия


Спросим у ГУГЛА что он об этом думает?

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

  1. Программные таймеры. Часть 1
  2. Многозадачный программный таймер.
  3. Многозадачный программный таймер, ver 2.0
  4. Программный таймер. Применение HAL

Данные статьи и послужили отправной точкой в реализации алгоритма.

Предисловие


Как разработчик АСУ ТП, я часто программирую ПЛК различных фирм. Для любого ПЛК в среде разработки заготовлены библиотеки для программных таймеров. Почти все они имеют однотипную функциональность. Использование таймеров в программе ПЛК требуется во многих родах задачах, все их описывать смысла нет, поэтому я покажу пару примеров из АСУ ТП и эти примеры «грубо портируем» в данный модуль.

Мини ТЗ


Какого вида таймеры нужны в данном модуле? Я остановил выбор на четырех видах:

  • Таймер с задержкой на включение
  • Таймер с задержкой на выключение
  • Циклический таймер
  • Одиночный таймер

Первые два таймера явно перекочевали из АСУ ТП и программирования ПЛК. Два последних являются логическим расширением аппаратного таймера любого МК. Рассмотрим каждый вид таймера по отдельности.

Таймер с задержкой на включение

Банальный пример такого таймера может служить реализация реле времени. На запускающий (управляющий) вход таймера приходит сигнал высокого (активного) уровня и таймер послушно начинает отсчет времени, по истечении которого и наличии активного уровня на входе переводит свой выход тоже в активное состояние. Как только мы снимем сигнал на входе таймера, выход также становиться неактивным. Временная диаграмма представлена ниже.


В мире МК данный таймер найдет себе применение если необходимо обеспечить антидребез «сухого» контакта, для детектирования длинного нажатия на клавишу и т.д. Сфера деятельности данного таймера явно этим не исчерпывается.

Таймер с задержкой на выключение

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


Нужно это бывает, когда необходимо остановить задачу не одновременно с её «родителем», а немного позже. Данный вид таймера может показаться кому то экзотичном, при программировании МК, может и так, но как говорится пусть будет.

Циклический таймер

Ну тут все банально. Каждое переполнение данного таймера должно срабатывать событие, по которому выполняем ту или иную рутинную задачу. Опрос датчика инерционной среды, мигание светодиодом, говорящий нам что МК в «порядке», для организации равных промежутков времени для разного рода фильтров и т.д. Временная диаграмма ниже.



Одиночный таймер

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



Теперь все это реализуем в коде.

Модуль SwTimer


Название модуля говорит само за себя. Модуль состоит из двух файлов: хидера и сорца.

Рассмотрим хидер:

#ifndef SW_TIMER_H_
#define SW_TIMER_H_

#define SwTimerCount  64        //Количество программных таймеров

/*Режимы работы таймеров*/
typedef enum
        {
                SWTIMER_MODE_EMPTY,
                SWTIMER_MODE_WAIT_ON,
                SWTIMER_MODE_WAIT_OFF,
                SWTIMER_MODE_CYCLE,
                SWTIMER_MODE_SINGLE
} SwTimerMode;


/*Стурктура программного таймера*/
typedef struct
    {
        unsigned LocalCount:32; //Переменная для отсчета таймера
        unsigned Count:24;              //Переменная для хранения задержки
        unsigned Mode:3;                //Режим работы
        unsigned On:1;                  //Разрешиющий бит
        unsigned Reset:1;               //Сброс счета таймера без его выключения
        unsigned Off:1;                 //Останавливающий бит
        unsigned Out:1;                 //Выход таймера
        unsigned Status:1;              //Состояние таймера
}SW_TIMER;

#if (SwTimerCount>0)
volatile SW_TIMER TIM[SwTimerCount]; //Объявление софтовых таймеров
#endif


void SwTimerWork(volatile SW_TIMER* TIM, unsigned char Count);   //Сама функция для обработки таймеров
void OnSwTimer(volatile SW_TIMER* TIM, SwTimerMode Mode, unsigned int SwCount);  //Подготовливает выбранный из массива таймер
unsigned char GetStatusSwTimer(volatile SW_TIMER* TIM);  //Считывание статуса таймера
#endif /* SW_TIMER_H_ */

Данный хидер содержит дефайн, указывающий количество софтовых таймеров в массиве. Объявлен enum для «осознанного» описания режимов работы таймеров. Далее следует сама структура программного таймера. Единственное на что, хотелось бы обратить внимание, то что сам таймер является 24-х битным. В данной структуре это позволяет программному таймеру занимать место в 8 байт. 24 бита при переполнении аппаратного таймера в 1 мс позволяет достичь задержки в 4,66 часа или 16 777 секунд. Вполне достаточно.

Функций немного.

Главная функция, обеспечивающая работу всего модуля:

void SwTimerWork(volatile SW_TIMER* TIM, unsigned char Count);

Данная функция должна вызываться при переполнении аппаратного таймера. В ней организован весь алгоритм работы модуля. Заглянем в код:

void SwTimerWork(volatile SW_TIMER* TIM, unsigned char Count){
	unsigned short i=0;
		for (i=0; i<Count; i++){

			if (TIM->Mode==SWTIMER_MODE_EMPTY) {
				TIM++;
				continue;
			}

			if (TIM->Mode==SWTIMER_MODE_WAIT_ON){ //Если таймер на задержку включения
				if (TIM->On){
					if (TIM->LocalCount>0) TIM->LocalCount--;
						else {
							TIM->Out=1;
							TIM->Status=1;
						}
				}
				else {
					TIM->Out=0;
					TIM->LocalCount=TIM->Count-1;
				}
			}
			if (TIM->Mode==SWTIMER_MODE_WAIT_OFF){ //Если таймер на задержку выключения
				if (TIM->On){
					TIM->Out=1;
					TIM->Status=1;
					TIM->LocalCount=TIM->Count-1;
				}
				else {
					if (TIM->LocalCount>0) TIM->LocalCount--;
						else TIM->Out=0;
				}
			}
			if (TIM->Mode==SWTIMER_MODE_CYCLE){
				if (TIM->Off){
					if (TIM->On){
						TIM->Off=0;
						if (TIM->LocalCount>0) TIM->LocalCount--;
					}
				}
				else{
					if (TIM->LocalCount>0) {
						TIM->LocalCount--;
						TIM->Out=0;
					}
					else {
						TIM->Out=1;
						TIM->Status=1;
						TIM->LocalCount=TIM->Count-1;
					}
				}
				if (TIM->Reset){
					TIM->LocalCount=TIM->Count-1;
					TIM->Out=0;
					TIM->Status=0;
				}
			}
			if (TIM->Mode==SWTIMER_MODE_SINGLE){
				if (TIM->Off){
					if (TIM->On){
						TIM->Off=0;
						if (TIM->LocalCount>0) TIM->LocalCount--;
					}
				}
				else{
					if (TIM->LocalCount>0) {
						TIM->LocalCount--;
						TIM->Out=0;
					}
					else {
						TIM->Out=1;
						TIM->Status=1;
						TIM->LocalCount=TIM->Count-1;
						TIM->Off=1;
					}
				}
				if (TIM->Reset){
					TIM->LocalCount=TIM->Count-1;
					TIM->Out=0;
					TIM->Status=0;
				}
			}

			TIM++;
		}
}

В цикле проходимся по всему массиву таймеров. Если таймер пуст = EMPTY, то переходим к следующему таймеру. В зависимости от режима работы таймера организована своя логика.

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

/* USER CODE BEGIN 0 */
#include "sw_timer.h"
/* USER CODE END 0 */

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();

  /* USER CODE BEGIN SysTick_IRQn 1 */
  SwTimerWork(TIM,SwTimerCount);
  /* USER CODE END SysTick_IRQn 1 */
}

А вот из основного цикла:
/* USER CODE BEGIN 0 */
extern uint8_t FlagSwTimer;
/* USER CODE END 0 */

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();

  /* USER CODE BEGIN SysTick_IRQn 1 */
  FlagSwTimer=1;
  /* USER CODE END SysTick_IRQn 1 */
}

/*В майне*/
while(1){
        if (FlagSwTimer){
                #if (SwTimerCount>0)

                        //Обработчик программных таймеров из sw_timer.c
                        SwTimerWork(TIM,SwTimerCount);

                #endif
        FlagSwTimer=0;
}

Остальные функции:

void OnSwTimer(volatile SW_TIMER* TIM, SwTimerMode Mode, unsigned int SwCount);

Данная функция устанавливает режим работы конкретного таймера, устанавливает необходимую задержку.

unsigned char GetStatusSwTimer(volatile SW_TIMER* TIM);

Считываем состояние статуса таймера. Возвращает -1, если указанный таймер пустой.

Применение


#define I_DI_READ_0() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) //Считывание кнопки

        OnSwTimer(&TIM[0],SWTIMER_MODE_WAIT_OFF,1000);
        OnSwTimer(&TIM[1],SWTIMER_MODE_SINGLE,1000);
        OnSwTimer(&TIM[2],SWTIMER_MODE_WAIT_ON,1000);
        OnSwTimer(&TIM[3],SWTIMER_MODE_CYCLE,1000);
        TIM[3].Off=0;    
        TIM[3].On=1;    //Запустили циклический таймер
        TIM[1].Off=0;
        TIM[1].On=1;    //Запустили одиночный таймер

Проинициализируем несколько таймеров с разными режимами. В основном цикле используем на наше усмотрение:

    while (1){
        TIM[2].On=I_DI_READ_0();    //Считываем кнопку, и пока нажата таймер отсчитывает время
        HAL_GPIO_WritePin(GPIOD,GPIO_PIN_15,TIM[2].Out);    //Как до тикал, зажгли светодиод
        TIM[0].On=I_DI_READ_0();    //Тот же фокус, но с таймером с задержкой на выключение
        HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,TIM[0].Out);    //Пока тикает, горит светодиод
        if (GetStatusSwTimer(&TIM[3])){
            HAL_GPIO_TogglePin(GPIOD,GPIO_PIN_13);    //Обрабатываем флаг циклического таймера, инвертируем выход
        }
        if (GetStatusSwTimer(&TIM[1])){
            HAL_GPIO_TogglePin(GPIOD,GPIO_PIN_12);    //Обрабатываем флаг одиночного таймера, инвертируем выход
        }
        TIM[3].Reset=I_DI_READ_0();    //Пока нажата кнопка, ресетим циклический таймер
}

Если необходимо остановить циклический или одиночный таймер, то необходимо сбросить бит включения и выставить бит отключения.

    TIM[3].On=0;
    TIM[3].Off=1;

Повторное включение через установку запускающего бита On.

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

    if (GetStatusSwTimer(&TIM[1]))     //Можно так
    if (TIM[3].Status){                //Или так
        TIM[3].Status=0;                //Сбрасываем бит, так как он сбросится только после очередного вызова
                                        //основной функции обработки массива, то есть через переполнение аппаратного таймера
        /*UserCode*/
    }


Если таймер больше не нужен, то сократить время выполнения функции обработки массива таймеров можно если не просто остановить таймер, а удалить его, то есть перевести в режим ПУСТО. Для этого вызываем функцию подготовки таймера с режимом SWTIMER_MODE_EMPTY. Или прямо это указываем.

    OnSwTimer(&TIM[3],SWTIMER_MODE_EMPTY,0);    //Можно так
    TIM[3].Mode=SWTIMER_MODE_EMPTY;            //Или так

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

Данная статья является переработанным материалом урока STM32. Уроки по программированию STM32F4. Урок № 4. Программный многозадачный таймер STM32F4. автором которого я и являюсь.

Видео, демонстрирующие функции данного модуля программного многозадачного таймера:

Наверное, на этом всё.
Tags:
Hubs:
+13
Comments 1
Comments Comments 1

Articles