Многозадачный программный таймер, ver 2.0

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


Поскольку мой таймер не отличается большой приоритетностью и частотой срабатывания — его прерыванию был назначен минимальный приоритет — 15.
Все остальные прерывания могут его «обскакивать» без проблем. Проблемы начинаются, когда вот в этих самых прерываниях имеется необходимость управлять программным таймером. И еще не надо забывать и об основном цикле. Эти функции нужны и для основного цикла тоже.
И что же теперь делать?
Почитав замечательную статью на нашем форуме Атомарные операции в Cortex-M3 у меня возникла идея, как это воплотить. Хорошая это идея или нет — решать вам.
Итак, сначала идет заголовок:
#ifndef SYSTEM_TIMER_H
#define SYSTEM_TIMER_H

//-------- <<< Use Configuration Wizard in Context Menu >>> --------------------

// <h> System Timer Initialization
//   <o> Count: <1-20>
//   <i> System-Timer count setup
#define SYSTEM_TIMER_COUNT                25
//   <o> Frequency Hz <5-2000>
//   <i> System-Timer Frequency setup
#define SYSTEM_TIMER_FREQUENCY            500
// </h>

typedef enum
{
 tm_Free,
 tm_Stop,
 tm_Repeat,
 tm_Once
} Timer_Mode_t;

#include <stdint.h>

void Init_System_Timer(void);
uint8_t System_Timer_Get_Status(int8_t TimerId);
void System_Timer_Reset_Status(int8_t TimerId);
int8_t System_Timer_Add(Timer_Mode_t mode, uint16_t compare, void (*handler)());
int8_t System_Timer_Set_Mode(int8_t TimerId, Timer_Mode_t mode);
void System_Timer_Clear_Counter(int8_t TimerId); 
void System_Timer_Set_Compare(int8_t TimerId, uint16_t compare); 
uint16_t System_Timer_Get_Counter(int8_t TimerId);
void System_Timer_Delete(int8_t TimerId);
void System_Timer_Lock(int8_t TimerId);
void System_Timer_Unlock(int8_t TimerId);
#endif


далее само «тело»:
#include "stm32f10x.h"
#include "system_timer.h"
#include "RTE_Device.h"
#include "mutex.h"
#include "bitbanding.h"

typedef struct
{
    uint8_t timer_mode;
		uint8_t timer_status;
		// 0..6 bit - lock counter; 7 bit - timer event;
    uint16_t counter;
    uint16_t compare;
    void (*handler)();
} System_Timer_t;

volatile System_Timer_t System_Timers[SYSTEM_TIMER_COUNT];

uint8_t System_Timer_Get_Status(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    if (System_Timers[TimerId].timer_mode != tm_Free)
      return System_Timers[TimerId].timer_status & 0x80;
  return 0;
}

void System_Timer_Reset_Status(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    BIT_BAND_SRAM(&System_Timers[TimerId].timer_status,7) = 0;		
}	

void System_Timer_Lock(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    Atomic8AddAndFetch(&(System_Timers[TimerId].timer_status), 1);
}

void System_Timer_Unlock(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    Atomic8AddAndFetch(&(System_Timers[TimerId].timer_status), -1);
}

void System_Timer_Delete(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {
    System_Timers[TimerId].timer_mode = tm_Free;
    __CLREX():
  }
}

void Init_System_Timer(void)
{
  uint8_t i;
  NVIC_SetPriority(SysTick_IRQn, 15);
  for(i = 0; i < SYSTEM_TIMER_COUNT; i++)
    System_Timer_Delete(i);
  SysTick_Config(RTE_SYSCLK / SYSTEM_TIMER_FREQUENCY -1);
}

int8_t System_Timer_Set_Mode(int8_t TimerId, Timer_Mode_t mode) //if OK - return 1
{
  int8_t temp = 0;
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {  
    if (mode != tm_Free)
    {
      do
      {
         temp = __LDREXB(&System_Timers[TimerId].timer_mode);
         if (temp != tm_Free)
           temp = mode;
      }while(__STREXB(temp, &System_Timers[TimerId].timer_mode));
    }
  }
return temp != 0;
}

void System_Timer_Clear_Counter(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {  
    System_Timers[TimerId].counter = 1;
    __CLREX():
  }
}

void System_Timer_Set_Compare(int8_t TimerId, uint16_t compare)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {  
    System_Timers[TimerId].compare = compare;
  }
}

uint16_t System_Timer_Get_Counter(int8_t TimerId)
{
  uint16_t counter = 0;
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {
    counter = System_Timers[TimerId].counter;
  }
  return counter;
}

int8_t System_Timer_Add(Timer_Mode_t mode, uint16_t compare, void (*handler)())
{
  uint8_t i;
  Timer_Mode_t temp;
  uint8_t result;
  if(mode != tm_Free)
  {
    for (i = 0; i < SYSTEM_TIMER_COUNT; i++)
    {
      do
      {
        result = 2;
        temp = (Timer_Mode_t)__LDREXB(&System_Timers[i].timer_mode);
        if (temp == tm_Free)
          result = __STREXB(tm_Stop, &System_Timers[i].timer_mode);
        else
          __CLREX();
      }while(result == 1);
      if (!result)
      {
        System_Timers[i].counter = 1;
        System_Timers[i].compare = compare;
        System_Timers[i].handler = handler;
        System_Timers[i].timer_status = 0;
        System_Timers[i].timer_mode = mode;
        return i;
      }
    }
  }
  return -1;
}

void SysTick_Handler(void)
{
  uint8_t i;
  void (*handler)();
  Timer_Mode_t mode; 
  for(i = 0; i < SYSTEM_TIMER_COUNT; i++)
  {
    if ((!(System_Timers[i].timer_status & 0x7F)) && (System_Timers[i].timer_mode > tm_Stop))
    {
      if(System_Timers[i].counter >= System_Timers[i].compare)
      {
        System_Timers[i].counter = 1;
        handler = System_Timers[i].handler;
        do
        {
          mode = (Timer_Mode_t)__LDREXB(&(System_Timers[i].timer_mode));
          if (mode == tm_Once)
            mode = tm_Stop;
        }while(__STREXB(mode, &System_Timers[i].timer_mode));
        BIT_BAND_SRAM(&System_Timers[i].timer_status,7) = 1;
        if ((handler) && (mode))
          handler();
      }
      else
        Atomic16AddAndFetch(&System_Timers[i].counter, 1);
    }
  }
}

А теперь разберемся что да как:
Функция SysTick_Handler() является тем самым «сердцем» нашего таймера. Здесь происходит периодичное выполнение всех назначенных задач. Все остальные функции — доступны извне, то есть, могут вызываться как и из основного цикла, так и из более приоритетных прерываний. Следовательно — работа этого «сердца» может прерывать свои же интерфейсные функции, и может быть прервана ими же. Для решения этих задач требуется синхронизация, и здесь активно используется связка команд процессора LDREX и STREX. Теория работы этих команд хорошо изложена в вышеуказанной статье, поэтому я не буду вдаваться в подробности. Итак, разберем функцию SysTick_Handler:
void SysTick_Handler(void)
{
  uint8_t i;
  void (*handler)();
  Timer_Mode_t mode; 
  for(i = 0; i < SYSTEM_TIMER_COUNT; i++)
  {
    if ((!(System_Timers[i].timer_status & 0x7F)) && (System_Timers[i].timer_mode > tm_Stop))
    {
      if(System_Timers[i].counter >= System_Timers[i].compare)
      {
    	System_Timers[i].counter = 1;
	handler = System_Timers[i].handler;
До сих пор нет ничего необычного. Организуется цикл, проверяется режим работы таймеров, сравниваются счетчики, проверка блокировки таймеров и сброс счетчиков в начало (==1) при удачном сравнении. Отмечу лишь, что мы сохраняем указатель на функцию в отдельную переменную. Для чего это — будет разъяснено позже.
Дальше нам нужно переключить режим таймера с «однократный» (tm_Once) на «остановлен» (tm_Stop).
Только вот незадача: А вдруг в тот самый момент, когда мы производим «чтение — модификация — запись»(далее ЧМЗ) у нас этот самый таймер был удален вызовом функции System_Timer_Delete(TimerId)? Вот здесь нам на помощь и придут те самые команды процессора LDREX STREX. Смотрим дальше:

        do
        {
          mode = (Timer_Mode_t)__LDREXB(&(System_Timers[i].timer_mode));
          if (mode == tm_Once)
            mode = tm_Stop;
        }while(__STREXB(mode, &System_Timers[i].timer_mode));
Ну что, кажется теперь все будет работать. Если в процессе ЧМЗ флаг эксклюзивного доступа будет сброшен — то однозначно произойдет все сначала. Цикл повторится и данные будут правильно обработаны. Кстати сказать, не нужно забывать, что все подобные переменные должны иметь приписочку "volatile" иначе все будет напрасно.
Едем дальше:

        BIT_BAND_SRAM(&System_Timers[i].timer_status,7) = 1;
        if ((handler) && (mode))
          handler();
Здесь, собственно, устанавливаем событие вызова (7 бит статуса). Используется модуль «bitbanding.h» далее — вызов подпрограмм. На последнем рубеже снова удостоверимся, что удаления не было. После чего осуществляем вызов. Даже если таймер удалился после последней проверки — мы успели сохранить указатель на функцию, и счетчик команд не уйдет абы куда, а последний раз (на прощание) обратится к законной функции. Далее:

      }
      else
        Atomic16AddAndFetch(&System_Timers[i].counter, 1);
    }
  }
}
А здесь у меня используется немного модифицированная функция из статьи уважаемого neiver'а. и выглядит так:
uint16_t Atomic16AddAndFetch(volatile uint16_t * ptr, int16_t value)
{
  uint16_t oldValue, newValue;
  do
  {
    oldValue = __LDREXH(ptr);
    newValue = oldValue + value;
  }while(__STREXH(newValue, ptr));
  return newValue;
}
Она атомарно инкрементирует счетчик, позволяя внедренному высокоприоритетному прерыванию сбрасывать его без последствий.
Так, с главной функцией разобрались.

Теперь поговорим об интерфейсных функциях:
uint8_t System_Timer_Get_Status(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    if (System_Timers[TimerId].timer_mode != tm_Free)
      return System_Timers[TimerId].timer_status & 0x80;
  return 0;
}
Здесь мы получаем положительное значение (0x80) если таймер сработал или 0 — если нет.
void System_Timer_Reset_Status(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    BIT_BAND_SRAM(&System_Timers[TimerId].timer_status,7) = 0;		
}
Здесь сбрасываем атомарно событие таймера.
void System_Timer_Lock(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    Atomic8AddAndFetch(&(System_Timers[TimerId].timer_status), 1);
}

void System_Timer_Unlock(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
    Atomic8AddAndFetch(&(System_Timers[TimerId].timer_status), -1);
}
Здесь, собственно функция блокировки/разблокировки одного из программных таймеров. Поскольку эти функции могут быть прерваны самими же собой или своими «антиподами» из основного цикла и какого-нибудь прерывания, необходимо не потерять кол-во вхождений подобных функций. С этим опять же хорошо справляется функция Atomic8AddAndFetch(). Вот ее код:
uint8_t Atomic8AddAndFetch(volatile uint8_t * ptr, int8_t value)
{
  uint8_t oldValue, newValue;
  do
  {
    oldValue = __LDREXB(ptr);
    newValue = oldValue + value;
  }while(__STREXB(newValue, ptr));
  return newValue;
}
Функции блокировки/разблокировки не поддерживают более 127 вложений lock/unlock, так что будьте внимательней. Дополнительных проверок в целях экономии ресурсов процессора нет.
Едем далее:
void System_Timer_Delete(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {
    System_Timers[TimerId].timer_mode = tm_Free;
    __CLREX();
  }
}
Здесь нет никаких блокировок и прочее. Здесь главное, timer_mode выполняется атомарной операцией. И добавляем сброс эксклюзива, на случай, если удаление будет вызвано в высокоприоритетном прерывании.
Функция Init_System_Timer() не участвует в процессе работы, ее рассматривать не будем.
Едем дальше:
int8_t System_Timer_Set_Mode(int8_t TimerId, Timer_Mode_t mode) //if OK - return 1
{
  int8_t temp = 0;
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {  
    if (mode != tm_Free)
    {
      do
      {
	 temp = __LDREXB(&System_Timers[TimerId].timer_mode);
	 if (temp != tm_Free)
	   temp = mode;
      }while(__STREXB(temp, &System_Timers[TimerId].timer_mode));
    }
  }
return temp != 0;
}
Смысл работы этой функции в следующем: Изменяя режим работы таймера, не напороться на подлянку — если этот самый таймер был удален… Функция возвращает «0», если изменение не удалось по той самой причине, если все ОК — возвращаем единичку

Дальше и дальше:
void System_Timer_Clear_Counter(int8_t TimerId)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {  
    System_Timers[TimerId].counter = 1;
    __CLREX();
  }
}
Функции сброса счетчика. Поскольку мы записываем в поле сounter, необходимо поставить сброс эксклюзивного доступа, чтобы обозначить, что изменения в данной ячейке производились.

void System_Timer_Set_Compare(int8_t TimerId, uint16_t compare)
{
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {  
    System_Timers[TimerId].compare = compare;
  }
}

uint16_t System_Timer_Get_Counter(int8_t TimerId)
{
  uint16_t counter = 0;
  if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))
  {
    counter = System_Timers[TimerId].counter;
  }
  return counter;
}
Функции сброса счетчика, установки значения сравнения и считывания счетчика — производятся де-факто атомарно, поэтому тут городить ничего не требуется.Главное, чтобы в программе не путать местами используемые функции. Например, сначала мы сбрасываем счетчик, а уж потом изменяем значение сравнения (ну если это потребуется конечно)… И да, не забываем, что это все — volatile.

Следующая функция требует более внимательного рассмотрения. System_Timer_Add() — позволяет добавлять новые таймеры уже в процессе работы. Предполагается, что эта функция будет перекрыта и сама собою, и прерыванием таймера SysTick. Алгоритм работы следующий. Функция ищет пустой программный таймер в массиве таймеров и занимает его. Если учитывать, что место может быть занято дважды(трижды, пятижды))) — то нужно как то от этого защититься. Смотрим код:
int8_t System_Timer_Add(Timer_Mode_t mode, uint16_t compare, void (*handler)())
{
  uint8_t i;
  Timer_Mode_t temp;
  uint8_t result;
  if(mode != tm_Free)
  {
    for (i = 0; i < SYSTEM_TIMER_COUNT; i++)
    {
Организовываем первоначальные проверки и запускаем цикл. Далее:

      do
      {
	result = 2;
	temp = (Timer_Mode_t)__LDREXB(&System_Timers[i].timer_mode);
	if (temp == tm_Free)
	  result = __STREXB(tm_Stop, &System_Timers[i].timer_mode);
	else
	  __CLREX();
      }while(result == 1);
Тот самый блок синхронизации. Сначала в temp помещаем значение режима таймера через LDREX.
если temp — означает «пустой» таймер, то мы его пытаемся занять через запись STREX. Устанавливаем режим таймера как «остановлен», чтобы избежать несанкционированной обработки от прерывания SysTick.
Итак, запись удачна? — значит в result — 0, если не удалось записать — значит в result — единица и повтор по циклу. Впрочем, если таймер уже занят, то мы так же выкатываемся из цикла со значением в result == 2
Ну а дальше все просто:

      if (!result)
      {
	System_Timers[i].counter = 1;
	System_Timers[i].compare = compare;
	System_Timers[i].handler = handler;
        System_Timers[i].timer_status = 0;
	System_Timers[i].timer_mode = mode;
	return i;
      }
    }
  }
  return -1;
}
Главное, чтобы timer_mode — заполнялся последним. И здесь снова вспомним про заветный volatile

Ну вот, подробный обзор завершен, прошу ваше мнение, господа!

UPDATE:
В связи с развенчанием коллективного заблуждения по поводу работы пары LDREX — STREX нынешний код мною был подправлен.
Сделаю небольшой экскурс в историю.
Мною было замечено, что цикл LDREX и STREX работает не всегда так как предполагалось. Оглашу несколько заблуждений, витающих в рунете:
1. LDREX — STREX контролируют определенную ячейку в памяти и не допускают ее изменения в промежутке между этими командами.
2. LDREX — STREX не терпит обращения к памяти внутри блока.
Тест:
ptr = &Counters[channel];
do
{
  oldValue = __LDREXH(ptr);
  newValue = (oldValue < CHECK_COUNTER)? CHECK_COUNTER : oldValue;
  (*ptr)++;
}while(__STREXH(newValue, ptr));
Результат отрицательный. Команда STREX успешно выполняется и глазом не моргнув, что ячейка подверглась изменению.

3. LDREX — STREX не срабатывает, если пришло прерывание внутри блока.
Тест:
ptr = &Counters[channel];
do
{
  oldValue = __LDREXH(ptr);
  newValue = (oldValue < CHECK_COUNTER)? CHECK_COUNTER : oldValue;
  EXTI->SWIER = 0x00000001;//программно вызываем прерывание
  __NOP();
}while(__STREXH(newValue, ptr));

void EXTI0_IRQHandler(void)
{
	EXTI->SWIER = 0x00000000;
	EXTI->PR = 0x00000001;
	(*ptr)++;
}
Результат абсолютно аналогичен. Система не отреагировала ни на прерывание, ни на изменение в нем ячейки памяти.

Как же оно работает? А вот так:
ptr = &Counters[channel];
do
{
  oldValue = __LDREXH(ptr);
  newValue = (oldValue < CHECK_COUNTER)? CHECK_COUNTER : oldValue;
  __CLREX(); // сбрасываем флаг эксклюзива
}while(__STREXH(newValue, ptr));
И так:
ptr = &Counters[channel];
do
{
  oldValue = __LDREXH(ptr);
  newValue = (oldValue < CHECK_COUNTER)? CHECK_COUNTER : oldValue;
  __STREXH(__LDREXH(ptr) + 1, ptr); //Инструкции LDREX - STREX. После выполнения сбрасывается флаг эксклюзива
}while(__STREXH(newValue, ptr));
И даже вот так:
ptr = &Counters[channel];
ptr2 = &Counters[(channel + 1)&0x07];
do
{
  oldValue = __LDREXH(ptr);
  newValue = (oldValue < CHECK_COUNTER)? CHECK_COUNTER : oldValue;
  __STREXH(__LDREXH(ptr2) + 1, ptr2);
}while(__STREXH(newValue, ptr));
Здесь видно, что изменяется внутри цикла другая(!) ячейка памяти, однако STREX уже выдает ошибку записи. Отсюда вывод:
1. LDREX при чтении взводит некий флаг эксклюзива. STREX выполняет сохранение только в случае, если этот флаг возведен, и после сбрасывает флаг. Все! Нет ничего более, кроме этого.

Как правильно пользоваться:
1. Определить для себя ячейку(и) памяти, которая(ые) будет(ут) контролироваться эксклюзивным методом.
2. Если требуется произвести ЧМЗ — выполнять только через пару эксклюзивного доступа. Исключением может быть только самое высокоприоритетное прерывание, где можно указать только сброс флага:
(*ptr)++;
__CLREX();

3. Если требуется только запись — то можно ограничиться сбросом эксклюзивного флага:
(*ptr) = value;
__CLREX();

4. Если читаем — то ничего дополнительного писать не требуется.

Теперь вернемся к теме.
Последние изменения коснулись функций
System_Timer_Clear_Counter();
System_Timer_Delete();
, где было добавлено
__CLREX();
в связи с последними замечаниями.

Успехов в программировании!
  • +3
  • 16 апреля 2014, 15:23
  • Mihail
  • 1
Файлы в топике: system_timer.zip

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

RSS свернуть / развернуть
Жду отзывы!
0
Прекрысный пример.
Один вопрос — вы когда говорите делаете паузы между словами?
0
  • avatar
  • x893
  • 16 апреля 2014, 16:26
не поверите, я говорю довольно неторопливо. Да, текста здесь не очень много, не хотел утруждать изобилием слов. Текст написан в стиле datasheet. Каждое слово — выверенное. Ну по крайней мере старался так сделать))
0
Я немного про другое — хотя таких много
if ((TimerId>=0)&&(TimerId<SYSTEM_TIMER_COUNT))
if ((TimerId >= 0) && (TimerId < SYSTEM_TIMER_COUNT))
0
Ох, да это же гораздо приятнее, чем
if ( ( TimerId >= 0 ) && ( TimerId < SYSTEM_TIMER_COUNT ) )
Пробельные символы в С стоит использовать не для разделения токенов, а для разделения логически независимых участков. И ежу ясно, что код
if ((TimerId>=0)&&(TimerId<SYSTEM_TIMER_COUNT))
никто не будет переписывать по частям, а удалит и напишет заново => это один блок, и пробелы не нужны.
0
ИМХО, вы не о том спорите. Ставить (в данном случае) пробелы или нет — это мелочи.

Лучше задуматься над тем — а зачем индекс счетчика в массиве сделан как знаковый тип. Логичнее сделать его uint8_t, тогда и проверка (TimerId >= 0) не нужна в принципе.

Или (если все оставлять как есть) зачем копипастить в начале каждой функции строку

if ((TimerId < SYSTEM_TIMER_COUNT) && (TimerId >= 0))


Можно повторяющийся код вынести в макрос или инлайн функцию. Типа

if(IsTimerIndexValid(TimerId)) {}
0
Ну так для этого есть
assert_param(...);
0
макрос не проверяет типов, а inline слабый аттрибут, и как быть уверенным, что компилятор не будет вертать её на оптимизаторе одним разработчикам известно.
0
int8_t — для того, чтобы был ответ -1 в случае неудачи добавления таймера. Можно сделать и функцию одну, вынести ее, только насколько это целесообразно? Не такая уж и сложная проверка… И места при компиляции много не занимает.
0
int8_t — для того, чтобы был ответ -1 в случае неудачи добавления таймера

С возвращаемым значением для System_Timer_Add() все понятно.
Я имел ввиду зачем делать индекс int8_t в функциях, где индекс передается как аргумент.
0
ну так она возвращает индекс. Код будет выглядеть так:

TimerId =  System_Timer_Add(...)
if (TimerId > 0)
   ...
0
Код будет выглядеть так:

Ну, да, вы же должны обрабатывать ошибку а не подавлять. Если таймер не добавился – нужно это сразу обработать а не подавлять ошибку передавая дальше в функции не корректный индекс.

Но, даже если вы не добавите проверку, то ваш код будет работать как и раньше. Ели вы передадите в функцию типа

void System_Timer_Clear_Counter(uint8_t TimerId)
{
  if (TimerId < SYSTEM_TIMER_COUNT)  {  
    …
  }
}

значение -1, то -1 будет приведено к 0xFF и не продет проверка if (TimerId < SYSTEM_TIMER_COUNT) (ведь вы ограничиваете значение SYSTEM_TIMER_COUNT до 127)

Но правильнее именно не ленится и проверять/обрабатывать ошибку добавления таймера.
+1
Обычно делается так:
void System_Timer_Clear_Counter(int8_t TimerId)
{
  if ((uint8_t)TimerId < SYSTEM_TIMER_COUNT)  {  
    …
  }
}
И параметр остается знаковым (дабы ошибку проверять id < 0) и сократить условие до одного сравнения.
0
Это не принципиально если код не смотреть
0
ааа… извиняюсь, подправлю)
0
можно авто-форматом — смотреть чуть удобнее. Спасибо
0
это где такое — автоформат? было бы интересно
0
В Eclipse/Visual Studio или в других IDE
0
пишу в KEIL, не видел в нем этой наиполезнейшей функции(((
0
там нет — но есть visual studo (и еще много)
0
А почему бы не приаттачить сорцы архивом? Их, конечно, можно скопипастить из первых двух <code>, но скачать — чуть-чуть удобней.
0
  • avatar
  • Vga
  • 17 апреля 2014, 14:19
сделаю
0
Я, кстати, пришёл к тому, что нужны два типа периодически вызываемых функций:
1. Быстрые, короткие, которые вызываются прямо из прерывания (systick'a), они нужны редко и в особых случаях (например, fatfs таймер 1 кГц);
2. Более тяжёлые, которые выполняются из главного цикла — работа с файлами, расчётами и т.д. В кортексах это менее важно, а вот в арм тдми7 без них очень тяжело — работа с файлами из режима прерывания через mci невозможна.

Вторые отличаются от первых только тем, что в прерывании устанавливается флаг, а в главном цикле инициируется выполнение:
void main(void)
{
    timer_Init();
    while(1)
    {
        ...
        timer_Main();
        ...
    }
}

// Простенький пример:
void timer_Main(void)
{
    int i;
	
    for(i = 0; i < TIMER_HANDLERS; i++)
    {
        // Если сработало в сустике или где ещё - вызовем.
        if(Handlers[i].Fired)
        {
            Handlers[i].Fired = false;
            Handlers[i].Handler();
            // Чтоб быстрее главный цикл крутился, обрабатываем не более одной сработавшей функции за раз
            break;
        }
    }
}
0
да, установка флага — это полезная весчь… Прикручу попозжа…
0
Ага. Ещё один бонус — не надо парится (в отличие от вызываемых напрямую из сустика функциях) о порядке доступа к данным, об атомарности операций и прочей фигне. За счёт того, что функции вызываются из основного потока кода, они автоматически разнесены по времени и не могут перекрываться чисто физически с другим кодом того же потока.

Ладно прерывания — там уж ничего не сделаешь, но программных таймеров бывает много и незачем городить контроль доступа к переменным ещё и в них.
0
да, это нужно понимать. В многопоточных приложениях в таких диспетчерских функциях нужно заботиться о том, чтобы данные не перекрывались. Это отдельный геморрой, который необходимо будет делать… Все зависит от того, оправданно ли это…
0
Ну это простая версия, где данные не перекрываются заведомо, в том и плюс, за то и люблю. Как это делать из прерывания — не представляю.
0
ну наверное, двойное буферирование, плюс флажок, какой из буферов в данный момент занят на запись…
0
Ээ? Я не понял.

Я говорил про вызываемые функции, которые заранее неизвестно какие, и неизвестно с чем они работают (какими данными). Подход гарантирует, что код из главного цикла и из программного таймера не будет пересекаться по времени. И в них, во внешних функциях, атомарность доступа по отношению к параллельному коду главного цикла обеспечивается автоматически.
0

void Switches_Read(void)
{
  Switches_New.ADDR = ((GPIOA->IDR)&0xFF) | ((GPIOE->IDR)&0xFF00);
  Switches_New.IND = (GPIOD->IDR)>>8;
  Switches_New.CRC8 = (((GPIOC->IDR)>>6)&0x0F)|(((GPIOA->IDR)>>4)&0xF0);
  if ((Switches_New.ADDR == Switches_Old.ADDR)&&
    (Switches_New.IND == Switches_Old.IND)&&(Switches_New.CRC8 == Switches_Old.CRC8))
  {
    if (Switches_Counter>=5)
    {
      Switch_Interrupt = 1;
      Switches = Switches_New;
      Switch_Interrupt = 2;
      Switches2 = Switches_New;
      Switch_Interrupt = 0;
      Switches_Status = Switches_Verify();
    }
    else
      Switches_Counter++;
    Switches_Status = Switches_Verify_OK;
    Switches_Counter = 5;
  }
  else
    Switches_Counter=0;
  Switches_Old = Switches_New;
}

uint16_t GetADDR()
{
  uint8_t interrupt;
  uint16_t addr;
  do
  {
    interrupt = __LDREXB(&Switch_Interrupt);
    if (interrupt ==1)
    {
      addr = Switches2.ADDR;
      __CLREX();
      break;
    }
    else
    {
      addr = Switches.ADDR;
      if (interrupt==2)
      {
        __CLREX();
        break;
      }
    }
  }while(__STREXB(interrupt,&Switch_Interrupt));
  return addr;
}


Вот две функции. Верхняя выполняет чтение информации с портов в прерывании программного таймера. А нижняя — читает эти данные, при чем не важно, откуда эта функция вызвана — она всегда прочитает адекватные данные
0
соответственно Switch_Interrupt — объявлен как volatile
0
Вот этого я и хотел бы избежать — код ото таких костылей становится непортируемым.
0
это неизбежная плата за универсальность функции. В моем случае из высокоприоритетных прерываний нужна инфа от низкоприоритетных прерываний. Как еще можно это выполнить?
0
Так я ж говорю не про прерывания, а про взаимодействие обычного кода и программных таймеров. С прерываниями никуда не денешься.
0
и здесь еще одно нужно понимать, куда портировать… Если на кристал без приоритетов, то понятно, нафиг оно не нужно там… Но ради портируемости уравнивать возможности до самого хилого — ну скажем так… не совсем правильное решение. Данный код будет работать на всех ARM-ах. Для AVR конечно это абсолютно не подходит… Однако эффективность работы этого кода выше, так как скорость реакции выше… В общем, тут философский вопрос.
0
Есть много кода неспециализированного, где никакой суперскорости не надо =)
Зато его можно написать раз — и таскать из проекта в проект (на любой платформе).
0
Рассматривалась ли возможность использовать atomic-и из C++11?
0
пример был сделан на С. Если компилятор позволяет — то можно наверное. Однако, что это atomic? Черный ящик, по крайней мере для меня.
0
Главное, чтобы timer_mode — заполнялся последним. И здесь снова вспомним про заветный volatile

И здесь вспомним про барьеры компилятора(
asm volatile ("" ::: «memory»);
), т.к. использование volatile по стандарту не запрещает компилятору(оптимизатору) изменять порядок выполнения.
Рекомендую почитать we.easyelectronics.ru/Soft/skolzkaya-dorozhka-dlya-poklonnikov-volatile.html
Особенно раздел
2. volatile помогает создать lockless код
+1
  • avatar
  • _dx
  • 25 апреля 2014, 13:10
Эту статью я читал.
т.к. использование volatile по стандарту не запрещает компилятору(оптимизатору) изменять порядок выполнения.
Это справедливо лишь если одна из инструкций или обе — не volatile. Если бы в примере в статье буффер был указан volatile — пример бы стал рабочим. Обращаю внимание, что у меня вся структура объявлена как volatile, а это значит, что изменение порядка в моем случае — исключено.
0
Ну, может быть вы и правы… должно работать. я просто уже принял за правило вставлять барьер там где реально важно чтобы порядок соблюдался, потому сразу и подметил.
0
Барьер — это дополнительные инструкции процессора. Совсем не эффективно…
0
Барьер — это дополнительные инструкции процессора. Совсем не эффективно…

Хм, вроде как (в данном случае) барьер перед

System_Timers[i].timer_mode = mode


не должен повлиять на генерируемый код (не должно быть дополнительных инструкций).

Но, в принципе, я с Вами согласен, volatile всей структуры обеспечит правильную последовательность операций и корректную работу программы (отбросим экзотику вроде выполнения вашего кода в разных потоках на SMP).
0
Правильно, барьер дает оверхед там, где он нужен.
Опять же, при вдумчивом его использовании с части переменных удаётся снять volatile и тем самым даже уменьшить код/повысить скорость. Сейчас не хочется сильно вникать в код таймера и предлагать что-то конкретное на примерах, но я в своих программах этот факт проверял неоднократно.
0
Забыл добавить что барьер это не инструкция а указание компилятору закончить все свои мутки с оптимизацией и исполнить следующую за барьером инструкцию с «чистой совестью» ) Это не какая-то отдельная магическая инструкция.
Если конкретнее, то оптимизатор не будет перемешивать операции до барьера и после. Таким образом гарантируется, что на момент исполнения кода после барьера, все операции с памятью которые были до барьера будут завершены.
Да, иногда это приводит к дополнительным инструкциям, но это плата за эти гарантии. Работа с volatile тоже практически вызывает раздувание кода и снижение производительности. Короче барьер это такой «местный volatile» по требованию)
0
Оно и понятно… То есть если структура не volatile — то он может в регистрах все операции производить, а после барьера он сбрасывает все в память. Такие вещи хороши, если в этих переменных производятся какие-то там вычисления или сложные преобразования. В моем же случае — это всего лишь однократная запись в память. Использование барьера в данном случае — абсолютно ненужная вещь… Налицо избыток операций.
0
Забыл добавить что барьер это не инструкция а указание компилятору закончить все свои мутки

Ну, дык я о этом и говорил.

Хм, вроде как (в данном случае) барьер перед … не должен повлиять на генерируемый код

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

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

Вот, даже в данном случае (если упростить, и не вдаваться в подробности):

        System_Timers[i].counter = 1;
        System_Timers[i].compare = compare;
        System_Timers[i].handler = handler;
        System_Timers[i].timer_lock = 0;
        System_Timers[i].timer_mode = mode;


Важно, чтобы операция
System_Timers[i].timer_mode = mode


была последней, т. к. она является «триггером» для обработки таймера. Последовательность заполнения остальных полей структуры не имеет значения. Мы можем их менять
        System_Timers[i].handler = handler;
        System_Timers[i].timer_lock = 0;
        System_Timers[i].counter = 1;
        System_Timers[i].compare = compare;
        System_Timers[i].timer_mode = mode;

И ничего не изменится. Ели мы захотим избавься от volatile структуры, и поставит барьер перед триггером

        System_Timers[i].counter = 1;
        System_Timers[i].compare = compare;
        System_Timers[i].handler = handler;
        System_Timers[i].timer_lock = 0;
        asm volatile ("" ::: «memory»);
        System_Timers[i].timer_mode = mode;


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

asm volatile ("" ::: "memory");


Зато есть команда процессора __DSB();

Может ли она полнофункционально заменить вышеуказанную строчку?
0
Может ли она полнофункционально заменить вышеуказанную строчку?
Вряд ли:
asm volatile ("" ::: «memory»); — барьер компилятора
__DSB — барьер ядра
Первое скидывает переменные из регистров в ОЗУ, второе — тормозит ядро пока все записи данных через конвеер не пройдут.
0
кейл не понимает инструкцию
А зачем вам эта инструкция, может и без неё можно обойтись?
0
да, но тут такая фишка, что __DSB() воспринимается как функция, а это своего рода барьер тоже. То есть все что после нее не может появиться в выполнении до.
а = 1;
__DSB();
b = 2;


не может превратиться в
a = 1;
b = 2;
__DCB();

и тем более
b = 2;
a = 1;
__DSB();
0
А зачем вам эта инструкция, может и без неё можно обойтись?
Обойтись можно и я обошелся без этого здесь тем что назначил структуре volatile. Однако есть и дополнительные расходы, связанные с этим. Почитайте обсуждение здесь.
0
Так, я сам нашел. Барьер в KEIL выглядит так:
__memory_changed();
0
Не встречался раньше с таким понятием для мемори — можно на пальцах, что это конкретно означает для функционала?
0
здесь в комментах уже все разжевано.
0
Надо переиновать — Многострадальный программный таймер
+1
Согласен. Без страданий мало что вообще может возникнуть
0
Но все же восприму вашу рекомендацию как шутку))
0
Пробежался по коду по диагонали. Почему-то знак захотелось поправить
if(System_Timers[i].counter == System_Timers[i].compare)
Может и ошибаюсь
Не очень понимаю привязанность к «мелким» типам на 32-бит машинке. Экономия сомнительна. Компилятору окучивать части слова как-бы чуток сложнее, да и в выражениях char и short преобразуются к типу int, unsigned char и unsigned short — к unsigned int, что может приводить к «неожиданному поведению» некоторых выражений
0
>= используется в том случае, если изменился compare в меньшую сторону и стал меньше текущего counter. В вашем случае таймер сработает лишь после переполнения счетчика, а это плохо.
0
Аппаратный Compare обычно взводит флаг(триггер) когда ==. И довольно часто в обработчике встречаются конструкции типа Compare = Counter+Period; В идеале это эквивалентно Compare += Period;
0
причем здесь аппаратный? да взводит при равных величинах. И это уже забота программиста, чтобы при изменении числа сравнения не допустить ухода счетчика из требуемого интервала.
0
Существуют устоявшиеся термины. Сравнение обычно работает «плохо»(С). А если Вы инкрементируете счетчик в обработчике, то, кроме случая с ручной/асинхронной переустановкой счетчика, бояться превышения порога сравнения не имеет смысла.
0
ну так в этом таймере как раз такая возможность имеется. И при значке >= точно ничего бояться не стоит.
0
cortex-m 3 хорошо воспринимает 1, 2 и 4х байтовые переменные. Тем более запись констант типа char производится в одну операцию процессора а не в две, не используя дополнительный регистр или чтение из памяти. А если так — спрашивается, а нафига городить лишку?
0
Обновление статьи:
Добавлен флаг срабатывания таймеров и соответственно две новые функции:

uint8_t System_Timer_Get_Status(int8_t TimerId);
void System_Timer_Reset_Status(int8_t TimerId);
0
Читаем UPDATE. Очень познавательно
0
Скажите чем такой вариант таймера лучше, от простого инкремента временной переменной в таймере?
Я вижу несколько преимуществ, но они не критичны.
1) Выделение в отдельный модуль.
2) Легко добавить новую временную переменную.
3) Просто запустить функции на циклический опрос.
Если ли ещё какие то преимущества, которые я не замечаю?

До недавнего времени использовал модуль в котором объявлен массив временных переменных, который инкриминируются в таймере по 1ms и с функциями взаимодействия ResetTimers(index) GetTimers(index).

Но в последнее время мне надоело что при подключения библиотек надо в модуле timer прописывать/корректировать переменные. По этому решил в каждом модуле заводить некоторую функцию SystemTimerModul, в ней прописывать инкремент временных переменных используемых данным модулем а саму функцию закидывать в таймер.
0
ну конкретно у этого таймера еще есть возможность вызова процедур, хотя я уже данным таймером не пользуюсь. Устарел))
0
А что новее?
0
вообще, оказалось правильнее в прерывании иметь всего лишь один счетчик, а таймеры основываются на структурах, в которых хранится время старта этого таймера и интервал. По интервалу вычисляется время, когда сработает этот «таймер». таким образом нет нагрузки на само прерывание, и имеется возможность условно бесконечного числа структур, каждая отвечающая за свою функцию. Таймер больше не нужен? удалил данные структуры и все. Важное замечание только, что счетчик в прерывании, а так же поля структуры должны иметь одинаковую размерность, чтобы не было проблем с переполнением.
+1
А обработка всех таймеров тогда где происходит? Периодически вызывается функция обработчик из главного цикла программы?
0
нет. обработка таймера происходит прям по месту программы.

if Timer_Expired(&Timer)
  Something...
0
Реализация напоминает вызов таймера в S7-STL.

		L   "DB_005_Parametrs_All".Delay_debounce ; // задержка антидребезга
		A	"DB_001_DPIO".INP.DI_10; // проверка включения датчика	
		SD	"Timer_10_Enabled_LIS03"; // таймер 10 с задержкой включения
		AN	"DB_001_DPIO".INP.DI_10; // 
		R	"Timer_10_Enabled_LIS03"; // сброс при дребезге	
		A	"Timer_10_Enabled_LIS03"; //
		S	"MRK_033_Level_LowLow"						; //	
0
сей мнемокод непостижим для меня))
0
Интересно посмотреть на вашу последнюю реализацию — может сделаете статью продолжение? Мне интересны были ваши предыдущие статьи и какое получил развитие ваш программируемый итаймер.
0
напишу как нибудь, но там пойдет в уклон на безопасность. Специфика у меня такая — ответственные изделия делать.
0
Пожалуйста — будем ждать.
0
написал вот здесь
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.