Реализация функции задержки меньше 1мс на FreeRTOS с помощью таймера и Task Notification

Есть в FreeRTOS встроенная функция vTaskDelay которая на N тиков системного таймера отдает управление другим задачам. В результате можно делать тупые циклы с ожиданием чего-либо и не париться по поводу процессорного времени. Очень удобно. Но есть проблема, минимальное время которая эта задержка может организовать составляет 1 тик системного таймера. Обычно это около 1 миллисекунда. Но иногда требуются задержки меньше. Да, можно повысить скорость тиков системного таймера. Даже в 10 или 100 раз, при 72 Мегагерцах какого-нибудь STM32 это вполне себе работает. Правда на переключение контекста будет уходить больше процессорного времени. Впрочем, всегда можно работать в кооперативном режиме, а не вытесняющем. Тут в принципе нет вытеснения, а управление передаешь вручную через функцию taskYIELD или любую другую с ожиданием. Те же Delay, Очереди, Семафоры и мало ли что еще.

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

Таймер взял самый бомжовскйи. На STM32F103C8T6 нет, к сожалению, Basic Timers ТIM6 и ТIM7 — это самые простые, самые примитивные считалки. В них нет ни завата, ни регистров сравнения для ШИМ и их не жалко отдать под такое дело, но они есть либо в самых жирных, либо в самых нищих вариация серии F10x. В моей нету. Ну окей, возьмем другой таймер. Общего назначения. Я взял Timer 2.

Настраиваются таймеры элеменатрно, тут не нужны даже никакие библиотеки. Главное понять откуда берется тактирование, какая величина и что надо включить. Смотрим в RM0008 структуру тактирования таймера 2. Раздел 7.2 Clocks

У меня в системе предделители обычно настроены на максимальную частоту и на этой шине 36 мегагерц. Тактирование нашего дополнительного таймера я хочу видеть с частотой 10 килогерц. Та что делим 36 мегагерц на 36, а потом еще на 100. И получим искомое.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;    	// Подаем тактирование на таймер от шины APB1
    TIM2->PSC = 35;				// Частота этой шины 36 мегагерц. Так что в предделитель записываем 36-1, получим 1МГц
    TIM2->ARR = 99;				// Потолком счета таймера укажем 100-1. Получим деление на 100 в частоте вызова прерываний. 
 
 
    TIM2->CR1   |= TIM_CR1_ARPE | TIM_CR1_URS | TIM_CR1_CEN;  		//  ARPE=1 - буфферизируем регистр предзагрузки таймера опциональная вещь.  
									// URS=1	- разрешаем из событий таймера только события от переполнения
									// CEN=1 - запускаем таймер
    TIM2->DIER  |= TIM_DIER_UIE;					// UIE=1 - Разрешаем прерывание от переполнения
 
    NVIC_SetPriority(TIM2_IRQn,14);				// Очень важный момент!!! Надо правильно выставить приоритеты. Они должны быть в диапазоне между
								// configKERNEL_INTERRUPT_PRIORITY  и configMAX_SYSCALL_INTERRUPT_PRIORITY
    NVIC_EnableIRQ(TIM2_IRQn);					// Разрешаем прерывания от таймера 2 через NVIC

При настройке прерываний важно учесть один тонкий момент. Дело в том, что у нас будут использоваться API функции RTOS вида ***FromISR. А если такие функции выполняются из обработчика прерываний, то приоритет этого обработчика должен быть не выше максимального приоритета RTOS. То есть, в конфиге RTOS есть такие строчки:

1
2
3
4
5
6
7
8
9
10
/* This is the raw value as per the Cortex-M3 NVIC.  Values can be 255
(lowest) to 0 (1?) (highest). */
 
#define configKERNEL_INTERRUPT_PRIORITY 		255
 
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
 
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
 
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	191 	/* equivalent to 0xb0, or priority 11. */

Что такое 255 и 191? Это код приоритета системы NVIC. Про работу NVIC я уже писал ранее. У STM32 приоритет прерывания задается старшим ниблом этого байта. Причем 0 это высший приоритет, а F низший. Т.е. 255 = 0xFF или F_ если закрыть младший нибл, или 15 в десятичной системе — низший приоритет у прерывания ядра. 191 = 0xBF, т.е. приоритет системных вызовов SYSCALL равен B_ или 11. Выше на 4 ступеньки.

В градациях нотации функции группы NVIC из CMSIS это от 0 до 15. Где 0 высший, а 15 низший. И если обработчик прерывания использует функции ***FromISR, то его приоритет должен быть между KERNEL_INTERRUPT_PRIORITY и MAX_SYSCALL_INTERRUPT_PRIORITY. Поэтому я взял и сделал его 14.

Что будет если оставить дефолтный приоритет в 0, т.е. наивысший? А будет весело! У вас будет рандомно при вызове API функций вызываться Hard Fault Handler. Причем не в момент их вызова из прерывания, там то обычно все на ура проходит. А где то в недрах диспетчера. При обработке этих очередей, таймеров, мутексов и прочего, что вы там из обработчика запнули. Т.е. система будет хаотично вешаться. Причем хаотично это не на раз два, а скажем, на 100 000 вызов прерывания. Или на 1000, или на 100500й. В общем, далеко не сразу ее скрючит. Но гарантированно.

Я три дня убил на то, чтобы понять какого хрена у меня вылезает хардфаулт. Перетряхнул очереди, стек, вызовы. Докопался до того, что ранее проверенная функция проверки семафора делает такое западло. Причем не всегда. А только под фазу луны. Оттрасировал почти все ядро FreeRTOS. Заменил красивую систему на семафорах убогим поделием из миллиона костылей и десятка структур, чтобы убрать из прерывания апи вызовы, ну чисто поприколу, локализовать проблему и… помогло. Стал детально копать и только потом заметил, что выставил приоритет не тому прерыванию. Просто опечатка, вместо таймера 2 поставил на таймер 3. Еще ведь проверил пару раз и в шары продолбился два раза же.

Инициализация готова. Теперь надо добавить обработчик прерывания. А также разрешить глобальные прерывания. У меня они уже разрешены в RTOS.

1
2
3
4
5
6
7
8
9
10
void TIM2_IRQHandler()
{
    if(TIM2->SR & TIM_SR_UIF)           // Проверяем, что это нас именно переполнение вызывало. Т.к. у таймера несколько видов событий и переполнение одно из
    {
        TIM2->SR = ~TIM_SR_UIF;        //сбросить флаг. Да тут, в отличии от AVR это надо делать вручную. 
 
	// Тут делаем свои дела
 
    }
}

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

Разбудить можно по разному. Можно через отправки сообщения в очередь, можно через семафор, но я предпочел другой способ. Уж не знаю когда и в какой версии, но во FreeRTOS появилась такая фича как FreeRTOS Task Notification. Т.е. задача в нужное время укладывается спать и разбудить ее можно послав ей уведомление. Которое просто пнет ее сапогом. Уведомление будем слать из прерывания.

Чем Notify отличается тогда от семафора? Концептуально — ничем. Но, работает быстрей, жрет меньше памяти, и в качестве цели для адресации использует не имя семфаора, а имя задачи. Минусы тоже есть — в отличии от семафора, мы не имеем обратной связи. Т.е. если мы ставим уже где-то поднятый семафор, то от функции xSemaphoreGive получим False в ответе. А извещение шлется и шлется. Никакого ответа нет. Но и не надо. Вот и все ограничения. Ну и передача адресата через handle задачи тоже удобно получается. Не нужно заводить семафор под каждый чих, не нужно париться по поводу того откуда это у нас что вызывается. В качестве индентификатора используется handle задачи, который всегда с тобой. Только руку протяни.

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

В статичных переменных задаем массивчик:

#define DelayCOUNT 10

static struct
{
TaskHandle_t Message;
uint16_t Time;
}
delays100us[DelayCOUNT];

Первый элемент это заголовок задачи, второй время. Мне хватит 65535 тиков. А так хоть до какого можно влепить. Количество одновременно работающих задержек задается с небольшим запасом. Причем речь о одновременно работающих, т.е. по потокам. А если у вас в одном процессе несколько задержек одна за другой, то они все лягут в один слот. Добавляем в инициализацию либы еще и обнуление массива. Ну на всякий случай:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Delay_100usInit(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;    // Тактирование на таймер 6
 
    TIM2->PSC = 36;			// Шина на 36мгц, делим. Получаем 1Мгц
    TIM2->ARR = 100;			// Тикать будет с частотой 10Кгц.
 
    TIM2->CR1   |= TIM_CR1_ARPE | TIM_CR1_URS | TIM_CR1_CEN;	// Запуск таймера
    TIM2->DIER  |= TIM_DIER_UIE;					// И прерываний
 
    NVIC_SetPriority(TIM2_IRQn,14);				// Настройка приоритета прерывания Таймера 2
    NVIC_EnableIRQ(TIM2_IRQn);					// Разрешаем прерывание таймера 2
 
// Flush all Delays
 
    uint8_t index;
    for(index=0; index < DelayCOUNT; index++)			// Обнуляем буфер. 
    {
        delays100us[index].Message = NULL;
        delays100us[index].Time = 0;
    }
 
}

Добавляем функцию постановки задержки в массив:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool Delay_100us(uint16_t Time)
 
{
uint8_t		index = 0;		
bool        out=false;			// Это выходная переменная. Она даст False если не удастся найти свободный слот. Или мы нотификацию не дождемся
TaskHandle_t	xMessage;
 
xMessage = xTaskGetCurrentTaskHandle();			// Берем адрес текущей задачи. Чтобы не использовать API в критической секции. 
 
    taskENTER_CRITICAL();				// Критическая секция, чтобы прерывание не вмешалось. 
    for(index=0; index < DelayCOUNT; ++index)		// Ищем свободную ячейку под задержку. 
        {
        if (delays100us[index].Message == NULL)		// Если хэндл пустой, значит нашли. 
            {
            delays100us[index].Message = xMessage;	// Кладем туда хэндл текущей задачи. Чтобы ОС Знала кого лягнуть.
            delays100us[index].Time = Time;				// И на сколько тиков ложимся спать. 
            out = true;							// Говорим, что жизнь удалась. 
            break;							// Выход
            }
        }
    taskEXIT_CRITICAL();					// Конец критической секции
 
 
	if (out)
	{
		return ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // Засыпаем пока не придет нотификация, по результату выдаем ответ. 
	}
	else
	{
		return out;					// Выходим с False так как не нашли пустой слот. 
	}
}

ulTaskNotifyTake(pdTRUE, portMAX_DELAY) отдаст управление на время portMAX_DELAY (т.е. навечно) до прихода оповещения. Можно сделать не portMAX_DELAY, а сколько то там тиков таймера. И тогда, если извещение не придет, задача разморозится, но вернет False и мы будем знать, что все проебали. Либо потому, что не нашли ячейку, либо потому что не пришло извещение и мы выпали по таймауту. Тут уже простор для реакции.

Теперь дописываем наш обработчик:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void TIM2_IRQHandler()
{
    if(TIM2->SR & TIM_SR_UIF)           // Проверяем, что это нас переполнение вызывало
    {
        TIM2->SR &= ~TIM_SR_UIF;        //сбросить флаг
 
        uint8_t index;
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;			
 
  //      BaseType_t uxSavedInterruptStatus;				// Переменная куда положим статус флагов прерываний для критической секции
  //      uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();		// Вход в критическую секцию. Но дается мне это зря. Приоритет фона же ниже. 
 
        for(index=0; index < DelayCOUNT ;index++)			// Прочесываем массив задержек
        {
            if(delays100us[index].Message == NULL) continue;	// НА предмет не пустых ячеек
 
            if(delays100us[index].Time != 0)			// Если время не равно нулю
            {
                delays100us[index].Time --;			// Уменьшаем
            }
            else							// А если в данном слоте время вышло.
            {
                vTaskNotifyGiveFromISR(delays100us[index].Message,&xHigherPriorityTaskWoken);	// То отправляем уведомление задаче которая это выставила.
 
                delays100us[index].Message = NULL;			// Обнуляем слот. Чтобы ничего не вышло там. 
            }
        }
  //      taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);		// Выход из критической секции. 
    }
}

xHigherPriorityTaskWoken это возвращемое значение TRUE или FALSE о том, не разблокировали ли мы своим извещением (семафором, сообщением) более важную задачу чем та которую прервало это прерывание? Если для нас это критично, то можно выйти из прерывания не просто завершиф функцию, а с понтом — через функцию которая сразу отдаст управление передовикам производства. И лишь потом вернется к тому месту где прерывались. Если же забить, как я, то стахановцы подождут пока наша задача вернется с прерывания, а после только им дадут в руки баян. Вопрос лишь в скорости реакции.

Ну, а юзается функция просто и без понтов:

1
Delay_100us(N);

Сырки библиотечки High Density Delay:

delay10us.c
delay10us.h

33 thoughts on “Реализация функции задержки меньше 1мс на FreeRTOS с помощью таймера и Task Notification”

  1. Вы допустили в коде ужасную типовую ошибку, из-за которой рискуете провести ещё месяц-другой в борьбе с редко и случайно проявляющимся глюком. Правда, конкретно в этом примере ошибка не проявится, но так в принципе нельзя писать. Попробуйте найти сами:-).

      1. Хорошо, тогда подскажу строчку, с которой кое-что не так, внимательно посмотрите описание регистра в мануале, и подумайте, что может случиться, если в приложении будут независимо использоваться несколько флагов прерываний:
        TIM2->SR &= ~TIM_SR_UIF; //сбросить флаг

        1. И что с ней не так? TIM_SR_UIF это вообще то не номер флага, а маска флага. А сам флаг UIF регистра RC в мануале значится как rc_w0 т.е. доступен для чтения и сбрасывается записью 0.

          Ну, а дальше ~ инвертируем маску, получаем FFFFFE и записываем ее в регистр, сбрасывая только флаг UIF. Единственно, что & вот тут не нужна. Т.к. записи в регистр нет, только сброс. Но она ни на что не влияет. В чем проблема то?

          1. Например, третий канал будет использоваться для захвата времени переключения внешнего сигнала. Обрабатывая событие от UIF вы прочитаете из SR значение SR_UIF. И далее запишете в SR нуль. Однако между чтением и записью SR может произойти событие захвата по третьему каналу, бит SR_CC3IF установится, и вы тут же его сбросите, так и не узнав, что возникло ещё какое-то событие. В результате получаем пропуск обработки прерывания. Поэтому нули в SR надо записывать только в те флаги, которые вы уже собрались обрабатывать и их надо немедленно сбросить, и в зарезервированные биты.

            1. А где вы увидели, что я записываю в SR нуль? Ну где? Я нуль записываю ТОЛЬКО в бит UIF. Если в CR будут другие биты, то ничего с ними не случится. Они как стояли так и будут стоять.

              А, понял о чем вы. Да, если в текущем виде, с & то будет чтение вначале. А это лишнее и можно между этими двумя тактами словить. Учитывая, что там clear by 0, то & не нужен от слова совсем.

        1. Я вот что имел в виду (если я правильно понял механизм):
          return (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) && out);
          действительно даст чистый bool или зависнет
          если out = false (нету слота)
          и компилятор вызовет сначала ulTaskNotifyTake() для вычисления выражения и не дождется «сапога» из прерывания,
          а я бы не положил ничего на рельсы что будет наоборот.
          Это типичная ситуация с побочным эффектом в выражении.
          Кроме того стандарт С99 прямо заявляет:
          (4). Unlike the bitwise binary & operator, the && operator guarantees left-to-right evaluation; there is a sequence point after the evaluation of the first operand. If the first operand compares equal to 0, the second operand is not evaluated.
          Другими словами, для операции && выражение вычисляется слева направо и т.д.
          Пока запас слотов избыточен проблем не будет, однако проверка на свободный слот может просто не отработать.
          Я бы написал так (тупо но надежно):
          if (out)
          {
          return ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
          }
          else
          {
          return out;
          }

    1. переменная xHigherPriorityTaskWoken в обработчике не используется дальше. по идеи, должно быть переключение контекста: portYIELD_FROM_ISR( xHigherPriorityTaskWoken );

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

        1. Как это не влияет? Например, до вызова функции ulTaskNotifyTake(pdTRUE, portMAX_DELAY) более низкоприоритетная задача была в состоянии «готова», после вызова управление перейдет к ней. Без portYIELD_FROM_ISR(xHigherPriorityTaskWoken) после выхода из прерывания таймера управление так и останется у низкоприоритетной, а к «уведомленной» задаче управление перейдет только в следующем системном тике. Т.о. низкоприоритетная задача помешала верно отсчитать задержку.

          «xHigherPriorityTaskWoken это возвращемое значение TRUE или FALSE о том, не разблокировали ли мы своим извещением (семафором, сообщением) более важную задачу чем та которую прервало это прерывание?», — тут не совсем верно. Верно: «не является ли разблокированная нашим извещением (семафором, сообщением) более важной, чем та что выполняется сейчас»

          Ну и структура delays100us[DelayCOUNT] таки все равно не упакована по всей видимости и все равно будет занимать 8 байт. Так что можно и 32-битное время сделать.

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

            = тут не совсем верно. Вы то же самое сказали, только другими словами. Один в один смысл.

  2. Настройте assert на вывод в консоль отладчика или карта и будете видеть предупреждения об ошибках в уровнях — фриртос их проверяет

  3. >TIM2->PSC = 36; // Шина на 36мгц, делим. Получаем 1Мгц
    The counter clock frequency CK_CNT is equal to fCK_PSC / (PSC[15:0] + 1).
    ARR тоже -1 надо.

    1. Абсолютно верно! Таймер считает от нуля до указанного значения N включительно, поэтому получается коэффициент деления N + 1. Это позволяет, указав 65535, получить делитель на 65536 (бывает нужно), в то же время, указав 0 получаем делитель на 1 (как бы отключаем деление вообще).

  4. Помните, что когда Вы используете функцию loop() Arduino вместе с библиотекой Arduino_FreeRTOS, функция loop() никогда не должна блокироваться (переходить в состояние Block), или переходить в пустое занятое ожидание с использованием функции delay(), или иметь в своем теле какую-либо другую функцию задержки, так как функция loop() вызывается задачей Idle Task системы FreeRTOS (эта задача никогда не должна получать блокировку).

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

      1. Это больше связано с тем, что реализация ISR CS в теории может привести к dead lock. В твоем случае я бы блокировал только копирование одного айтема из статического массива в локальную стековую переменну, а дальше все действия производил над локальной переменной. Ну и в конце так же обновил бы айтем статического массива под защитой торого вызова CS. Это защитит от того, что в будущих релизах freeRTOS что-то поменяется. А дебажить dead lock’и в ISR без exclusive monitor то еще занятие…

        1. Не, ну понятно , что концептуально апи функция с потенциально ждущей функцией, типа раздачи в очередь, может локнуть при занятой очереди. Тк отдаст управление шедулеру, а тот повиснет. Но вот тупо раздающие мессаги или функции возврата хэндла никакого потенциального криминала в себе иметь не должны. Им походу было просто лень описывать все случаи :)

          1. Я думаю, они подстраховались на предмет возможных изменений в будущем. Ну и блочить прерывания на время прокрутки цикла у которого есть выходы в ядро сильно накладно. А без CS опасно — пока цикл крутится есть вероятность изменения статичсекого массива из прерываний с высшим приоритетом так как блокировки на вызов из прерывания в функциях задержки нету. Если же все фунции установки задержек работают только из unprivileged, то и смысла в CS нет. Но тогда надо блочить установку задержки из прерывания, что бы случайно не вызвать их оттуда.

            1. Ну выход в ядро там только при совпадении. Т.е. статистически хорошо если один из будет. А чаще и без него. Но с другой стороны, вот да, если так подумать, то фон всегда ниже, а если не дергать эту функцию из прерывания, то и пофиг на КС. Проще действительно блочить запуск из прерывания, чем морочиться с критической секцией там.

              1. Тут есть один тонкий момент:
                Delay_100us()
                {
                ….
                delays100us[index].Message = xMessage;
                !!! А тут фигась и прерывание!
                delays100us[index].Time = Time; // <- а это уже может быть бесполезно, в прерывании Message = NULL;
                ….
                А тут повиснем на всегда, так как ответ уже пришел, и мы будем ждать следующего:
                return ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // Засыпаем пока не придет нотификация, по результату выдаем ответ.

                }
                Так что надо механизм блокировки именно твоего прерывания на время добавления айтема.

                  1. Так это если от них (CS) отказаться. Ибо нафига блочить все прерывания, мне не очень понятно.
                    А вот маскировать исключительно прерывание таймера, используемого для счета задержек — уже более кошерно.

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

Добавить комментарий

Ваш e-mail не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.