Статическое распределение объектов FreeRTOS

    По умолчанию все объекты в системе FreeRTOS распределяются динамически — очереди, семафоры, таймеры, задачи (потоки), и мьютексы. Программист видит только «кучу» — область где динамически выделяется память по запросу программы или системы, а что там творится внутри – не ясно. Сколько еще осталось? Неизвестно. Не занимает ли что нибудь больше чем нужно? Кто его знает? Лично я предпочитаю решать вопросы организации памяти еще на этапе написания прошивки, не доводя до ошибок во время выполнения, когда память неожиданно закончилась.

    Эта статья является логическим продолжением вчерашней про статическое распределение объектов в памяти микроконтроллера, только теперь применительно к объектам FreeRTOS. Сегодня мы научимся размещать объекты FreeRTOS статически, что позволит более четко понимать что происходит в оперативной памяти микроконтроллера, как именно расположены и сколько занимают наши объекты.

    Но просто взять и начать размещать объекты FreeRTOS статически много ума не требуется — FreeRTOS начиная с версии 9.0 как раз предоставляет функции создания объектов размещенных статически. Такие функции имеют суффикс Static в названии и на эти функции имеется отличная документация с примерами. Мы же напишем удобные и красивые C++ обертки над функциями FreeRTOS, которые не только будут размещать объекты статически, но и скрывать все потроха, а также предоставлять более удобный интерфейс.

    Статья рассчитана на начинающих программистов, но которые уже знакомы с основами FreeRTOS и примитивами синхронизации многопоточный программ. Поехали.

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

    На мой взгляд, любая прошивка где нужно одновременно делать две (и более) задачи решается намного проще и элегантнее, если использовать FreeRTOS. Например, считывать показания с медленных датчиков и одновременно обслуживать дисплей. Только чтобы без тормозов, пока считываются датчики. В общем must have! Всячески рекомендую к изучению.

    Как я уже сказал и писал в прошлой статье, мне не очень нравится подход с созданием объектов динамически в случае если мы еще на этапе компиляции знаем их количество и размер. Если такие объекты размещать статически, то мы можем получить более четкую и понятную картину распределения памяти в микроконтроллере, а значит и избежать сюрпризов когда память внезапно закончилась.

    Вопросы организации памяти FreeRTOS будем рассматривать на примере платы BluePill на микроконтроллере STM32F103C8T6. Чтобы не париться с компилятором и системой сборки работать будем в среде ArduinoIDE, установив поддержку для этой платы. Есть несколько реализаций Arduino под STM32 — в принципе подойдет любая. У меня установлена stm32duino согласно инструкции из Readme.md проекта, бутлоадер как сказано в этой статье. FreeRTOS версии 10.0 установлена через менеджер библиотек ArduinoIDE. Компилятор — gcc 8.2

    Придумаем себе небольшую подопытную задачку. Особого практического смысла в этой задаче может и не быть, но зато будут использоваться все примитивы синхронизации, которые есть во FreeRTOS. Что нибудь вроде такого:

    • 2 задачи (потока) работают параллельно
    • также работает таймер, который время от времени посылает нотификацию первой задаче используя семафор в режиме signal-wait
    • первая задача, получив нотификацию от таймера, посылает сообщение (случайное число) второй задаче через очередь
    • вторая, получив сообщение, печатает его в консоль
    • пускай первая задача тоже что нибудь печатает в консоль, а чтобы они не подрались консоль будет защищена мьютексом.
    • размер очереди можно было бы ограничить одним элементом, но для того, чтобы было интереснее поставим 1000

    Стандартная реализация (согласно документации и туториалам) может выглядеть так.

    #include <STM32FreeRTOS.h>
     
    TimerHandle_t xTimer;
    xSemaphoreHandle xSemaphore;
    xSemaphoreHandle xMutex;
    xQueueHandle xQueue;
     
    void vTimerCallback(TimerHandle_t pxTimer)
    {
      xSemaphoreGive(xSemaphore);
    }
     
    void vTask1(void *)
    {
      while(1)
      {
    	xSemaphoreTake(xSemaphore, portMAX_DELAY);
    	int value = random(1000);
    	xQueueSend(xQueue, &value, portMAX_DELAY);
     
    	xSemaphoreTake(xMutex, portMAX_DELAY);
    	Serial.println("Test");
    	xSemaphoreGive(xMutex);
      }
    }
     
    void vTask2(void *)
    {
      while(1)
      {
    	int value;
    	xQueueReceive(xQueue, &value, portMAX_DELAY);
     
    	xSemaphoreTake(xMutex, portMAX_DELAY);
    	Serial.println(value);
    	xSemaphoreGive(xMutex);
      }
    }
     
    void setup()
    {
      Serial.begin(9600);
     
      vSemaphoreCreateBinary(xSemaphore);
      xQueue = xQueueCreate(1000, sizeof(int));
      xMutex = xSemaphoreCreateMutex();
     
      xTimer = xTimerCreate("Timer", 1000, pdTRUE, NULL, vTimerCallback);
      xTimerStart(xTimer, 0);
     
      xTaskCreate(vTask1, "Task 1", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL);
      xTaskCreate(vTask2, "Task 2", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL);
      vTaskStartScheduler();
    }
     
    void loop() {}

    Давайте посмотрим что творится в памяти микроконтроллера, если скомпилировать такой код. По умолчанию все объекты FreeRTOS размещаются в динамической памяти. FreeRTOS предоставляет аж целых 5 реализаций менеджеров памяти, которые отличаются сложностью реализации, но в целом задача у них одна и та же – нарезать кусочки памяти для нужд FreeRTOS и пользователя. Кусочки нарезаются либо из общей кучи микроконтроллера (с помощью malloc) или используют свою отдельную кучу. Какая именно куча используется для нас не важно – все равно внутрь кучи заглянуть мы не сможем.

    Например, для кучи имени FreeRTOS это будет выглядеть так (вывод утилиты objdump)

    ...
    200009dc l 	O .bss      	00002000 ucHeap
    ...
    

    Т.е. видим один большой кусок, внутри которого нарезаются все объекты FreeRTOS – семафоры, мьютексы, таймеры, очереди, и даже сами задачи. Последние 2 пункта очень важны. В зависимости от количества элементов очередь может быть достаточно большой, а задачи гарантировано будут занимать много места из-за стека, который также выделяется вместе с задачей.

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

    Размер стека задачи задается при создании задачи параметром функции xTaskCreate. Размер стека не может быть меньше параметра configMINIMAL_STACK_SIZE (задается в конфигурационном файле FreeRTOSConfig.h) – это тот самый резерв для прерываний. Размер кучи задается параметром configTOTAL_HEAP_SIZE и в данном случае равен 8кб.

    А вот теперь попробуйте угадать, поместятся ли все наши объекты в кучу на 8кб? А еще парочка объектов? А еще несколько задач?
    При определенных настройках FreeRTOS все объекты в кучу не поместились. Причем выглядит это так: программа просто не работает. Т.е. все компилируется, заливается, но потом микроконтроллер просто висит и все. И пойди догадайся что проблема именно в размере кучи. Пришлось кучу увеличить до 12кб.

    Стоп, а что за переменные xTimer, xQueue, xSemaphore, и xMutex? Разве они не описывают нужные нам объекты? Нет, это только хендлы – указатели на некую (непрозрачную) структуру, которая и описывает сами объекты синхронизации

    200009cc g 	O .bss     	00000004 xTimer
    200009d0 g     O .bss    	00000004 xSemaphore
    200009cc g 	O .bss     	00000004 xQueue
    200009d4 g 	O .bss    	00000004 xMutex
    

    Как я уже упоминал, предлагаю чинить весь этот беспорядок тем же самым способом, что и в предыдущей статье — распределим все наши объекты статически на этапе компиляции. Функции статического распределения становятся доступны если в файле конфигурации FreeRTOS параметр configSUPPORT_STATIC_ALLOCATION установлен в 1.

    Начнем с очередей. Вот как предлагает аллоцировать очереди документация на FreeRTOS

    struct AMessage
     {
                	char ucMessageID;
                	char ucData[ 20 ];
     };
     
     #define QUEUE_LENGTH 10
     #define ITEM_SIZE sizeof( uint32_t )
     
     // xQueueBuffer will hold the queue structure.
     StaticQueue_t xQueueBuffer;
     
     // ucQueueStorage will hold the items posted to the queue.  Must be at least
     // [(queue length) * ( queue item size)] bytes long.
     uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
     
     void vATask( void *pvParameters )
     {
     QueueHandle_t xQueue1;
     
                	// Create a queue capable of containing 10 uint32_t values.
                	xQueue1 = xQueueCreate( QUEUE_LENGTH, // The number of items the queue can hold.
                                                                                                                	ITEM_SIZE       	  // The size of each item in the queue
                                                                                                                	&( ucQueueStorage[ 0 ] ), // The buffer that will hold the items in the queue.
                                                                                                                	&xQueueBuffer ); // The buffer that will hold the queue structure.
     
                	// The queue is guaranteed to be created successfully as no dynamic memory
                	// allocation is used.  Therefore xQueue1 is now a handle to a valid queue.
     
                	// ... Rest of task code.
     }

    В этом примере очередь описывается тремя переменными:

    • Массив ucQueueStorage — это место в котором будут размещаться элементы очереди. Размер очереди задается пользователем для каждой очереди индивидуально.
    • Структура xQueueBuffer – тут живет описание и состояние очереди, текущий размер, списки ожидающих задач, а также другие атрибуты и поля, нужные FreeRTOS для работы с очередью. Название для переменной, на мой взгляд, не совсем удачное, в самой FreeRTOS эта штука называется QueueDefinition (описание очереди).
    • Переменная xQueue1 – это идентификатор очереди (handle). Все функции управления очередью, а также некоторые другие (например, внутренние функции работы с таймерами, семафорами и мьютексами) принимают вот такой хендл. На деле это просто указатель на QueueDefinition, но мы этого (как бы) не знаем, а потому хендл придется тягать за собой отдельно.

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

    template<class T, size_t size>
    class Queue
    {
      QueueHandle_t xHandle;
      StaticQueue_t x QueueDefinition;
      T         	xStorage[size];
     
    public:
      Queue()
      {
    	xHandle = xQueueCreateStatic(size,
                     	sizeof(T),
                         reinterpret_cast<uint8_t*>(xStorage),
                     	&xQueueDefinition);
      }
     
      bool receive(T * val, TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xQueueReceive(xHandle, val, xTicksToWait);
      }
     
      bool send(const T & val, TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xQueueSend(xHandle, &val, xTicksToWait);
      }
    };

    Заодно в этот класс поселились также функции отправки и приема сообщений, причем сразу удобного нам типа.

    Очередь будет объявляться как глобальная переменная, как-то так

    Queue<int, 1000> xQueue;

    Отправка сообщения

    	xQueue.send(value);

    Прием сообщения

    	int value;
    	xQueue.receive(&value);
    

    Разберемся теперь с семафорами. И хотя технически (внутри FreeRTOS) семафоры и мутексы реализованы через очереди, семантически это 3 разных примитива. А потому будем реализовывать их отдельными классами.

    Реализация класса семафора будет достаточно тривиальна – она просто хранит несколько переменных и объявляет несколько функций.

    class Sema
    {
      SemaphoreHandle_t xSema;
      StaticSemaphore_t xSemaControlBlock;
     
    public:
      Sema()
      {
    	xSema = xSemaphoreCreateBinaryStatic(&xSemaControlBlock);
      }
     
      BaseType_t give()
      {
    	return xSemaphoreGive(xSema);
      }
     
      BaseType_t take(TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xSemaphoreTake(xSema, xTicksToWait);
      }
    };

    Объявление семафора

    Sema xSema;

    Захват семафора

      xSema.take();

    Отпускание семафора

      xSema.give();

    Теперь мьютекс

    class Mutex
    {
      SemaphoreHandle_t xMutex;
      StaticSemaphore_t xMutexControlBlock;
     
    public:
      Mutex()
      {
    	xMutex = xSemaphoreCreateMutexStatic(&xSemaControlBlock);
      }
     
      BaseType_t lock(TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xSemaphoreTake(xMutex, xTicksToWait);
      }
     
      BaseType_t unlock()
      {
    	return xSemaphoreGive(xMutex);
      } 
    };

    Как вы можете заметить класс мьютекса практически идентичен классу семафора. Но как я уже сказал семантически это разные сущности. Более того, интерфейсы этих классов не полные, и расширяться они будут совсем в разные стороны. Так, у семафора могут добавится методы giveFromISR() и takeFromISR() для работы с семафором в прерывании, тогда как у мьютекса разве что метод tryLock() добавится — семантически у него нет других операций.

    Я надеюсь, вы знаете в чем разница между бинарным семафором и мьютексом
    Этот вопрос я всегда задаю на собеседованиях и, к сожалению, 90% кандидатов не понимают этой разницы. На самом деле семафор можно захватывать и отпускать из разных потоков. Выше я уже упоминал режим семафора signal-wait, когда один поток посылает сигнал (вызывает give()), а другой ждет сигнала (функцией take()).

    Мьютекс же, напротив, можно отпускать только из того же потока (задачи), который его захватил. Не уверен, что FreeRTOS это отслеживает, но некоторые операционные системы (например, Linux) за этим следят довольно строго.

    Мьютексом можно пользоваться в стиле С, т.е. напрямую вызывать lock()/unlock(). Но раз уж мы пишем на C++, то можно воспользоваться прелестями RAII и написать более удобную обертку, которая сама будет захватывать и отпускать мьютекс.

    class MutexLocker
    {
      Mutex & mtx;
     
    public:
      MutexLocker(Mutex & mutex)
    	: mtx(mutex)
      {
    	mtx.lock();
      }
     
      ~MutexLocker()
      {
    	mtx.unlock();
      }
    };

    При выходе из области видимости мьютекс будет автоматически освобожден.

    Это особенно удобно, если выходов из функции несколько и не нужно постоянно помнить о необходимости освобождения ресурсов.

    	MutexLocker lock(xMutex);
    	Serial.println(value);
      } // mutex will be unlocked here

    Теперь очередь таймеров.

    class Timer
    {
      TimerHandle_t xTimer;
      StaticTimer_t xTimerControlBlock;
     
    public:
      Timer(const char * const pcTimerName,
        	const TickType_t xTimerPeriodInTicks,
        	const UBaseType_t uxAutoReload,
        	void * const pvTimerID,
        	TimerCallbackFunction_t pxCallbackFunction)
    	{
      	xTimer = xTimerCreateStatic(pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, &xTimerControlBlock);
    	}
     
    	void start(TickType_t xTicksToWait = 0)
    	{
      	xTimerStart(xTimer, xTicksToWait);
    	}
    };

    В целом тут все аналогично предыдущим классам, не буду подробно останавливаться. Возможно, API оставляет желать лучшего, ну или как минимум требует расширения. Но моя цель показать принцип, а не доводить до состояния production ready.

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

    template<const uint32_t ulStackDepth>
    class Task
    {
    protected:
      StaticTask_t xTaskControlBlock;
      StackType_t xStack[ ulStackDepth ];
      TaskHandle_t xTask;
     
    public:
      Task(TaskFunction_t pxTaskCode,
       	const char * const pcName,
       	void * const pvParameters,
       	UBaseType_t uxPriority)
      {
    	xTask = xTaskCreateStatic(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, xStack, &xTaskControlBlock);
      }
    };

    Поскольку объекты задач теперь объявляются как глобальные переменные, то и инициализироваться они будут как глобальные переменные – до вызова main(). А значит и параметры, которые передаются в задачи также должны быть известны на этом этапе. Этот нюанс нужно учитывать, если в Вашем случае передается нечто, что нужно вычислять перед созданием задачи (у меня же там просто NULL). Если Вам это все равно не подходит — рассмотрите вариант с локальными статическими переменными из прошлой статьи.

    Компилируем и получаем ошибку:

    tasks.c:(.text.vTaskStartScheduler+0x10): undefined reference to `vApplicationGetIdleTaskMemory'
    timers.c:(.text.xTimerCreateTimerTask+0x1a): undefined reference to `vApplicationGetTimerTaskMemory'

    Дело вот в чем. В каждой ОС есть специальная задача – Idle Task (задача по умолчанию, задача ничего не делания). Операционная система исполняет эту задачу если все другие задачи выполняться не могут (например спят, или чего-то ждут). В целом, это самая обычная задача, только с самым низким приоритетом. Но вот создается она внутри ядра FreeRTOS и влиять на ее создание мы не можем. Но раз уж мы начали размещать задачи статически, то нужно как-то сказать ОС где нужно разместить управляющий блок и стек этой задачи. Вот для этого FreeRTOS и просит нас определить специальную функцию vApplicationGetIdleTaskMemory().

    Аналогичная ситуация и с задачей таймеров. Таймеры в системе FreeRTOS живут не сами по себе – в ОС крутится специальная задача, которая и обслуживает эти таймеры. И эта задача также требует управляющий блок и стек. И точно также ОС просит нас указать где они находятся с помощью функции vApplicationGetTimerTaskMemory().

    Сами функции тривиальны и просто возвращают соответствующие указатели на статически размещенные объекты.

    extern "C" void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize)
    {
      static StaticTask_t Idle_TCB;
      static StackType_t  Idle_Stack[configMINIMAL_STACK_SIZE];
     
      *ppxIdleTaskTCBBuffer = &Idle_TCB;
      *ppxIdleTaskStackBuffer = Idle_Stack;
      *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
    }
     
    extern "C" void vApplicationGetTimerTaskMemory (StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, uint32_t *pulTimerTaskStackSize)
    {
      static StaticTask_t Timer_TCB;
      static StackType_t  Timer_Stack[configTIMER_TASK_STACK_DEPTH];
     
      *ppxTimerTaskTCBBuffer   = &Timer_TCB;
      *ppxTimerTaskStackBuffer = Timer_Stack;
      *pulTimerTaskStackSize   = configTIMER_TASK_STACK_DEPTH;
    }

    Давайте посмотрим что у нас получилось.

    Код хелперов спрячу под спойлер, Вы его только что видели
    template<class T, size_t size>
    class Queue
    {
      QueueHandle_t xHandle;
      StaticQueue_t xQueueDefinition;
      T         	xStorage[size];
     
    public:
      Queue()
      {
    	xHandle = xQueueCreateStatic(size,
                     	sizeof(T),
                         reinterpret_cast<uint8_t*>(xStorage),
                     	&xQueueDefinition);
      }
     
      bool receive(T * val, TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xQueueReceive(xHandle, val, xTicksToWait);
      }
     
      bool send(const T & val, TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xQueueSend(xHandle, &val, xTicksToWait);
      }
    };
     
    class Sema
    {
      SemaphoreHandle_t xSema;
      StaticSemaphore_t xSemaControlBlock;
     
    public:
      Sema()
      {
    	xSema = xSemaphoreCreateBinaryStatic(&xSemaControlBlock);
      }
     
      BaseType_t give()
      {
    	return xSemaphoreGive(xSema);
      }
     
      BaseType_t take(TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xSemaphoreTake(xSema, xTicksToWait);
      }
    };
     
    class Mutex
    {
      SemaphoreHandle_t xMutex;
      StaticSemaphore_t xMutexControlBlock;
     
    public:
      Mutex()
      {
    	xMutex = xSemaphoreCreateMutexStatic(&xMutexControlBlock);
      }
     
      BaseType_t lock(TickType_t xTicksToWait = portMAX_DELAY)
      {
    	return xSemaphoreTake(xMutex, xTicksToWait);
      }
     
      BaseType_t unlock()
      {
    	return xSemaphoreGive(xMutex);
      } 
    };
     
    class MutexLocker
    {
      Mutex & mtx;
     
    public:
      MutexLocker(Mutex & mutex)
    	: mtx(mutex)
      {
    	mtx.lock();
      }
     
      ~MutexLocker()
      {
    	mtx.unlock();
      }
    };
     
    class Timer
    {
      TimerHandle_t xTimer;
      StaticTimer_t xTimerControlBlock;
     
    public:
      Timer(const char * const pcTimerName,
        	const TickType_t xTimerPeriodInTicks,
        	const UBaseType_t uxAutoReload,
        	void * const pvTimerID,
        	TimerCallbackFunction_t pxCallbackFunction)
    	{
      	xTimer = xTimerCreateStatic(pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, &xTimerControlBlock);
    	}
     
    	void start(TickType_t xTicksToWait = 0)
    	{
      	xTimerStart(xTimer, xTicksToWait);
    	}
    };
     
    template<const uint32_t ulStackDepth>
    class Task
    {
    protected:
      StaticTask_t xTaskControlBlock;
      StackType_t xStack[ ulStackDepth ];
      TaskHandle_t xTask;
     
    public:
      Task(TaskFunction_t pxTaskCode,
       	const char * const pcName,
       	void * const pvParameters,
       	UBaseType_t uxPriority)
      {
    	xTask = xTaskCreateStatic(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, xStack, &xTaskControlBlock);
      }
    };
    
    extern "C" void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize)
    {
      static StaticTask_t Idle_TCB;
      static StackType_t  Idle_Stack[configMINIMAL_STACK_SIZE];
     
      *ppxIdleTaskTCBBuffer = &Idle_TCB;
      *ppxIdleTaskStackBuffer = Idle_Stack;
      *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
    }
     
    extern "C" void vApplicationGetTimerTaskMemory (StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, uint32_t *pulTimerTaskStackSize)
    {
      static StaticTask_t Timer_TCB;
      static StackType_t  Timer_Stack[configTIMER_TASK_STACK_DEPTH];
     
      *ppxTimerTaskTCBBuffer   = &Timer_TCB;
      *ppxTimerTaskStackBuffer = Timer_Stack;
      *pulTimerTaskStackSize   = configTIMER_TASK_STACK_DEPTH;
    }


    Код основной программы целиком.

    Timer xTimer("Timer", 1000, pdTRUE, NULL, vTimerCallback);
    Sema xSema;
    Mutex xMutex;
    Queue<int, 1000> xQueue;
     
    Task<configMINIMAL_STACK_SIZE> task1(vTask1, "Task 1", NULL, tskIDLE_PRIORITY);
    Task<configMINIMAL_STACK_SIZE> task2(vTask2, "Task 2", NULL, tskIDLE_PRIORITY);
     
     
    void vTimerCallback(TimerHandle_t pxTimer)
    {
      xSema.give();
     
      MutexLocker lock(xMutex);
      Serial.println("Test");
    }
     
    void vTask1(void *)
    {
      while(1)
      {
    	xSema.take();
    	int value = random(1000);
    	xQueue.send(value);
      }
    }
     
    void vTask2(void *)
    {
      while(1)
      {
    	int value;
    	xQueue.receive(&value);
     
    	MutexLocker lock(xMutex);
    	Serial.println(value);
      }
    }
     
    void setup()
    {
      Serial.begin(9600);
     
      xTimer.start();
     
      vTaskStartScheduler();
    }
     
    void loop() {}

    Можно дизассемблировать полученный бинарник и посмотреть что и как там расположилось (вывод objdump’а немного подкрашен для лучшей читаемости):

    0x200000b0    	.bss    	512     	vApplicationGetIdleTaskMemory::Idle_Stack
    0x200002b0    	.bss    	92       	vApplicationGetIdleTaskMemory::Idle_TCB
    0x2000030c     	.bss    	1024   	vApplicationGetTimerTaskMemory::Timer_Stack
    0x2000070c     	.bss    	92       	vApplicationGetTimerTaskMemory::Timer_TCB
    0x200009c8     	.bss    	608     	task1
    0x20000c28     	.bss    	608     	task2
    0x20000e88    	.bss    	84       	xMutex
    0x20000edc    	.bss    	4084   	xQueue
    0x20001ed0    	.bss    	84       	xSema
    0x20001f24     	.bss    	48       	xTimer

    Цель достигнута — теперь все как на ладони. Каждый объект виден и понятен его размер (ну разве что составные объекты типа Task считают все свои запчасти одним куском). Статистика компилятора также предельно точна и на этот раз весьма полезна.

    Sketch uses 20,800 bytes (15%) of program storage space. Maximum is 131,072 bytes.
    Global variables use 9,332 bytes (45%) of dynamic memory, leaving 11,148 bytes for local variables. Maximum is 20,480 bytes.

    Заключение


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

    Очевидно, что предложенный способ подходит только для размещения объектов время жизни которых сравнимо с временем жизни всего приложения. В противном случае стОит использовать динамическую память.

    Помимо статического размещения объектов FreeRTOS мы еще написали удобные обертки над примитивами FreeRTOS, что позволило несколько упростить клиентский код, а также энкапсулировать

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

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

    Пример из статьи находится тут.

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

    Подробнее
    Реклама

    Комментарии 28

      0
      Там можно функции задачи тоже в класс засунуть, чтобы совсем было по С++, чтобы можно использовать для функции задачи кучу всяких входных данных (приватных атрибутов класса задачи).
      class LedTask : public OsWrapper::Thread {
      public:
        virtual void Execute() override {
          while(true)   {      
            GPIOC->ODR ^= (1 << 5) ;
            using OsWrapper::operator""ms ;
            SleepUntil(2000ms);
            event.Signal() ;
           }
        }
        using tLedStack = std::array<OsWrapper::tStack, 
                                    static_cast<tU16>(OsWrapper::StackDepth::minimal)> ;
        tLedStack Stack; 
      } ;
      
      MyTask myTask;
      LedTask ledTask;
      
      int main() {
        using namespace OsWrapper ;  
        Rtos::CreateThread(myTask, MyTask::Stack.data(), "myTask", 
                           ThreadPriority::lowest, MyTask::Stack.size()) ;
        Rtos::CreateThread(ledTask, LedTask::Stack.data()) ;
        Rtos::Start();
        
        return 0;
      }

      Единственное тут виртуальная функция одна нужна, табличка будет небольшая в ПЗУ лежать для виртуальных функций байтов на 8.
        0
        Да, эту конструкцию можно до ума доводить еще долго. Но мне было лень потому что
        1. примерно в середине работы над статьей я наткнулся на библиотеку frt где это уже было реализовано
        2. у меня были отдельностоящие функции, которые не нужно было никак переделывать, а объявлять класс, перегружать метод — это нужно было кнопочки лишние жать :)
        0
        Причем стек должен быть достаточно большой, чтобы в нем поместились не только вызовы и локальные переменные самой задачи, но и стек прерывания, если такое возникнет.


        Не обязательно. Насколько я знаю, FreeRTOS используется PSP регистр а прерывания используют исключительно MSP регистр. Значит можно выделить отдельную область для стека прерываний. Код FreeRTOS открытый, если выдруг что, дописать функционал не проблема.
          0
          Спасибо за инфу, нужно будет покопать. Я пока не спец в архитектуре АРМ на уровне регистров…

          Но если это такая очевидная штука, то разве небыло никого, кто бы уже это реализовал?
            0
            Будете копать «регистры АРМ», начните с книжки Джозефа Ю (Yiu, если я не путаю).
            Прекрасная выжимка из официальной документации на ядро. Есть на английском и русском.
              0
              Там вся магия заключается в линкер скрипте, так как практически во всех примерах стек устанавливается на конец RAM памяти, то и FreeRTOS его не затрагивает (но из-за того, что FreeRTOS заточена на использование PSP регистра, у нее есть свои ограничения и костыли).

              Я это к тому, что для стека задач, резервировать место под прерывания не стоит. Если мы говорим про Cortex-M (а именно на них и ссылается статья). Банально легко проверить работу. Создать задачу и программно вызвать прерывание и посмотреть, на какой адрес будет установлен SP регистр.
              0
              Более точной будет фраза «порты FreeRTOS на Cortex-M3 используют process stack pointer для задач и main stack pointer для прерываний». Не одними кортексами жива фриртос (при желании её можно даже на атмегу взгромоздить).
              И да, ничего дописывать не надо, в портах на всевозможные Cortex-Mx, что идут в комплекте, всё уже сделано. Я правда, бегло пробежался по документации — кажется, этот вопрос нигде детально не прописан. Есть достаточно подробный раздел про магию приоритетов прерываний (перед использованием сервисов ОС из прерываний его надо внимательно прочитать — иначе программа будет ИНОГДА не работать), а вот про стек документация как-то умалчивает…
              0
              У Вас задача создаться конструктором еще до инициализации железа, и в будущем могут быть проблемы.

              Мой пример: я создал клас ADC, и в конструкторе сделал каллибрацию SDADC на STM32 HAL (ненавижу HAL, но пока нет LL). Объект класса, как и у Вас создал глобальным. Проблема в том что функция каллибрации использует HAL таймер для задержки, который инициализируется в main(то-есть после вызова конструктора). И таймер всегда возвращал 0 — контроллер зависав в конструкторе SDADC.

              Перенес инициализацию init, и в main делаю adc.init(). Все заработало.
                0
                Дельное замечание. Действительно, если пытаться работать с железом в конструкторе, то работать будет неправильно.

                Технически, речь в статье только о создании объекта задачи, а это можно делать и до main(). Сама задача не стартанет до вызова vTaskStartScheduler(). Речь не шла о том, чтобы делать что нибудь еще.

                Так что работу с железом нужно учитывать в архитектуре. Есть несколько путей решения:
                — Делать инициализацию железа уже в самой задаче. К этому времени базовая настройка уже должна быть выполнена, HAL таймер запущен
                — Разделить создание класса от инициализации, т.е. в конструкторе ничего не делать (ну или создавать только саму задачу), а все тяжелые штуки делать в каком нибудь методе init() и вызывать его явно в мейне
                — Использовать отложеное создание объектов используя статическую переменную функции, но тогда опять же нужно будет в мейне явно инициировать создание объекта.
                  +1
                  А это, кстати, одна из ключевых проблем использования С++ для контроллеров. Неясность последовательности инициализации памяти, при работе со статическими объектами. Т.е. то самое «подкапотное» пространство и накладные расходы на ООП которых в С нет.

                  Так, просто ремарочка.
                  0
                  Это, в принципе, кмк, применимо где угодно — конструктор должен инициализировать сам объект (создавать объекты внутри него, инициализировать поля нулями и пр), а функциональная инициализация должна производиться отдельно. С подобным часто сталкиваешься и в обычном прикладном или системном программировании.
                  –1
                  На мой взгляд, любая прошивка где нужно одновременно делать две (и более) задачи решается намного проще и элегантнее, если использовать FreeRTOS.

                  И сразу — код, в котором вместо простоты и элегантности пара костылей (костыли я выделил).

                  xSemaphoreTake(xSemaphore, portMAX_DELAY);
                  int value = random(1000);
                  xQueueSend(xQueue, &value, portMAX_DELAY);

                  xSemaphoreTake(xMutex, portMAX_DELAY);
                  Serial.println("Test");
                  xSemaphoreGive(xMutex);


                  Увы, это взаимоисключающие параграфы. Настоящая многозадачность нужна в редких случаях:

                  1. Вам важно, чтобы при зацикливании в одной нити, все остальные продолжали бы работать.
                  2. У вас долго выполняются расчеты в одной из нитей.
                  3. У вас долгое ожидание в одной из нитей и его не избежать.
                  4. Время простоя очень мало, процессор еле справляется
                  5. Очень высокоскоростные процессы


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

                  Мьютексы и критические секции — не нужны, задачи выполняются по очереди. Очереди FreeRTOS не нужны, достаточно кольцевого буфера. Нет проблемы «как одним watchdog сделать перезапуск при зависании любого из 20 тредов». Более того, даже кольцевой буфер не нужен, если мы запускаем читателя сразу после писателя.

                  Упрощенный код
                  static int value;
                  
                  void vTask1(struct deferred_executor_task *from_task) {
                      value = random(1000);
                      Serial.println("Test");
                      deferred_executor_add_task (&from_task, 0);     // запускаем сразу
                      deferred_executor_add_task (&from_task, 1000);// запускаем через секунду
                  }
                  
                  void vTask2(struct deferred_executor_task *from_task) {
                  	Serial.println(value)
                  }
                  
                  static struct deferred_executor defexec;
                  static struct deferred_executor_task vTask1_task;
                  static struct deferred_executor_task vTask2_task;
                  
                  void setup() {
                        Serial.begin(9600);
                        deferred_executor_init (&defexec, "MAIN_DEFEXEC", 
                                                            MAIN_DEFEXEC_STACK_SIZE,
                                                           MAIN_DEFEXEC_PRIORITY);
                        deferred_executor_task_init (&vTask1_task,  &defexec,  vTask1);
                        deferred_executor_task_init (&vTask2_task,  &defexec,  vTask2);
                        deferred_executor_add_task (&vTask1_task, 0);// запускаем сразу
                        vTaskStartScheduler();
                  }


                  Из преимуществ — почти без переделки кода все будет работать под linux. А если дописать диспетчер — то и под Windows. Так что отладка — упрощается в разы. Ну и самое главное — нет никаких гонок.

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

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

                  В вашем примере, если и нужен тред — то для вывода на консоль. То есть блокировки надо бы заменить на отправку в очередь. Правда, мы в аналогичной ситуации обходимся драйвером. printf работают параллельно и независимо, но вызывают _write, которая запихивает байты в очередь к драйверу. А уже драйвер работает с портом.

                  P.S. Авторство идеи и обе реализации — моего коллеги.
                    0
                    Кооперативная (не корпоративная!) многозадачность это действительно хороший подход в некоторых случаях, я сам таким балуюсь время от времени. Тем не менее статья носит более академический характер и, вообще-то, рассматривает организацию памяти применительно к примитивам FreeRTOS, которые работают в режиме вытесняющей многозадачности.

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

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


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

                    Если брать реальный пример, вот мой хобби проект. Рисование экрана занимает примерно 50-70мс. Если делать кооперативную многозадачность, то пока рисуем можно пропустить важные события. Например переполнение буфера UART, который на скорости всего-то 9600 за это время успевает переполнится. Можно поделить рисование на несколько более мелких этапов, но это существенно усложнит логику. А если задача еще более длительная и не пилится легко на кусочки разумной (малой) длительности? С вытесняющей многозадачность таких проблем нет — поток работает себе в фоне и в ус не дует. Именно на такие случаи я и намекал, когда говорил про элегантность.

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

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

                    в одной из моих программ их было чуть более 20

                    Если уж меряться, то в моем текущем рабочем проекте их более 400 :) Со временем привыкаешь писать код, который железобетонно работает в таких условиях.
                      0
                      Ну корпоративной не я первый её обозвал. Это отсылка к корпорации микрософт и дурной реализации многозадачности в Windows 3.11. Но вы правы — действительно кооперативная.

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

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

                      Ну а если строго по определению — костыль, это «неполное решением, не отвечающие требованиям к дальнейшему развитию системы». Что будет, если у вас на COM-порту flow-control, и с другой стороны на пару минут запретили передачу?

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

                      Костыльность таймера + семафора в том, что это дурная замена для vTaskDelayUntil. То есть у вас не обосновано, почему именно такое странное решение. Между прочим, если vTask1 слишком долго будет сидеть в функции вывода, то у вас уменьшится число генераций value. Причем это ещё зависит от поведения vTask2 — он тоже может слишком надолго занять критсекцию.

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

                      Так что железобетонный — это такой, который при падении губит кучу народу. Ибо слишком тяжел. :-)

                      Вы лучше численные данные скажите — какая у вас наработка на видимый пользователю отказ? У меня (если уж меряться) — больше 15 лет без видимых пользователю сбоев в 365*24. Не то, что багов не было, просто было написано хорошее и многоуровневое восстановление после отказов.

                      А вот что реально интересно — как устроен watchdog для 400 тредов? Баг, помеха по питанию, ТЗЧ — и какой-то тред завис. Что у вас дальше происходит? Есть ли перезапуск отказавших тредов и как останавливается отказавший тред? А как идет штатное завершение приложения? Кстати, из 400 тредов, сколько однотипных (разные инстансы одного кода)? У меня однотипных штук 12 (из 20) было.
                        –1
                        Честно говоря, не понимаю, как можно медленно читать из датчика. Можно пример дапчтика с медленным чтением?

                        Признаю, датчик с медленным чтением это я как пример придумал, хотя уверен такие существуют.

                        Из того что сам лично сталкивался на практике:
                        • получение/отправка данных по какому нибудь очень болтливому протоколу (когда идет много запросов и ответов) через медленный канал связи с неопределенными интервалами между пакетами (например через ethernet или WiFi)
                        • Вышеупомянутое рисование на дисплее. Я уже упоминал пример из своего домашнего хобби проекта. И хорошо, если есть экранный буфер и можно быстренько нарисовать в картинку в памяти, а потом заливать в дисплей через DMA (пусть и медленно, но зато процессор не отвлекает).

                          Хуже когда памяти на экранный буфер не хватает. Тогда каждый элемент на экране нужно рисовать отдельно и это может растянуться надолго.
                        • Чтение (или того хуже запись) SD карты через SPI. Тут уж реально посылаем карте команду и начинаем ждать пока карта одуплится. Может сразу ответить, а может потупить чуток.


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

                        Вам видимо очень сильно везло. Если у вас алгоритм вида
                        Сделать А -> Сделать B -> Сделать C
                        то сделать конечный автомат на него действительно не сложно.

                        Даже это можно сделать конечным автоматом
                        Сделать А -> (Сделать B -> Сделать C) * 5 раз -> Сделать D
                        Но тут уже возникает вопрос: как это будет выглядеть в коде? Стейт машина? Вы точно уверенны, что это удобнее для чтения чем просто линейный алгоритм с циклом"?

                        Я уже упоминал про SD карту. Работа с ней выглядит так:

                        • Программа: эй библиотека SD Fat, я хочу записать 10кб в файл file.txt
                          • Sd Fat: эй драйвер карты, нужно записать первый сектор
                            • Драйвер: эй карта, вот тебе команда на запись
                            • Карта: ждите, мне нужно морально приготовится… ок, давайте
                            • Драйвер: эй карта, вот поток данных
                            • Карта: ок, пишу… еще нет… еще нет… еще варится обед… все!
                            • Драйвер: ок, библиотека SD Fat, мы записали
                          • Sd Fat: супер! Драйвер, нужно еще сектор записать...

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

                        Вроде пытались сделать многозадачность, а получили — почти однозадачность

                        Похоже, Вы не поняли суть примера. В нем не было идеи «пер-анус» по таймеру писать в консоль. Цель задачи чисто синтетическая: чтобы просто присутствовали все примитивы синхронизации. Не пытайтесь там логику и дизайн искать.

                        Если сильно хочется дизайна и реального применения, то в этой программе 3 различных (и с точки зрения семантики независимых) куска. Для каждого свой примитив. Еще раз подчеркну, рассмотрите эти 3 примера независимо:

                        • Что-то нужно по таймеру делать. Собственно запускаем таймер и делаем что нибудь полезное в обработчике
                        • Один поток что нибудь хочет сигнализировать другому. Например у него произошло некоторое событие (пришли данные, закончилось вычисление, опустошился буфер). Можно было бы и флажок какой-то использовать, но тогда ожидающая сторона будет вынуждена его в активном цикле опрашивать.

                          Чтобы такого небыло используют семафор. Ожидающий поток тупо спит на семафоре и не потребляет ресурсы процессора. Когда событие произойдет первый поток просигнализирует об этом, а второй, соответственно, проснется и пойдет обрабатывать это событие. Очереди это сюда же.
                        • 2 или более потоков хотят использовать один ресурс. Чтобы быть уверенными что этот ресурс никем другим в данный момент не занят используют мьютекс.


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

                        Между прочим, если vTask1 слишком долго будет сидеть в функции вывода, то у вас уменьшится число генераций value. Причем это ещё зависит от поведения vTask2 — он тоже может слишком надолго занять критсекцию.

                        Если кто нибудь долго занимает ресурс то это уже проблема дизайна. И уже не важно, кооперативная у Вас многозадачность, или вытесняющая.

                        Стандартные практики — делать критические секции как можно короче (вариантов масса). А кому критические секции это все еще долго, то тогда добро пожаловать в lock-free алгоритмы.

                        Так что железобетонный — это такой, который при падении губит кучу народу. Ибо слишком тяжел. :-)

                        Сойдемся на том, что это Ваш личный негативный опыт :)
                        Пожалуйста, мысленно замените в моих постах слово «железобетонный» на «очень надежный».

                        А вот что реально интересно — как устроен watchdog для 400 тредов?

                        Вот тут нам будет сложновато померяться — в моем случае это не эмбеддед и не микроконтроллер и не FreeRTOS (хотя с точки зрения примитивов синхронизации все тоже самое). Тут речь уже про десктопное и серверное приложение (2 варианта одного кода). У меня нет проблем с питанием, мне не нужно держать watchdog, а если произойдет критическая ошибка то ничего не взорвется. В крайнем случае приложение перезапустят. А критический баг можно починить в следующей версии приложения, которое пользователи получат через пару недель (или дней, если совсем грустно).

                        Тем не менее креши и зависания прямо влияют на поток говна на пользовательских форумах, а те в свою очередь на продажи приложения. Так что такие штуки отлавливаются и чинятся в первую очередь.

                        Есть ли перезапуск отказавших тредов и как останавливается отказавший тред?

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

                        А как идет штатное завершение приложения?

                        Штатно тушим все потоки. В каждом потоке так или иначе есть вечный цикл с условием выхода по флажку или conditional variable.

                        Кстати, из 400 тредов, сколько однотипных (разные инстансы одного кода)? У меня однотипных штук 12 (из 20) было.

                        Есть несколько комплектов по 16/32/64 потоков (в зависимости от количества ядер) — это как правило thread pool'ы от разных компонентов или библиотек — на них выполняются коротенькие задачки вроде defferred executor tasks, что Вы предлагали. Остальные потоки обслуживают те или иные асинхронные задачи. Кто-то ждет чтения диска или передачи по сети, кто-то занимается рисованием на экране или бек-буферах (например пару потоков рисуют в бекбуфер, а еще один выводит результаты), кто-то занимается хитрым распараллеливанием вычислений, пару десятков потоков работают с разными железками и периферией. Совсем разных, наверное, сотни полторы будет. Да, машинки требуются соответствующие — от 20 ядер.
                          0
                          Признаю, датчик с медленным чтением это я как пример придумал, хотя уверен такие существуют
                          Ваши примеры — это как раз быстрые устройства с большим обменом данных. Под медленным я понимал выполнение одной машинной команды за огромное время. То есть что-то вроде виртуальной памяти на магнитной ленте. Читаешь ячейку и упс — десяток секунд ожидания. Теоретически на процессорах с отображением регистров в память так можно сделать. Практически — не думаю, что такие устройства есть.

                          Но тут уже возникает вопрос: как это будет выглядеть в коде? Стейт машина? Вы точно уверенны, что это удобнее для чтения чем просто линейный алгоритм с циклом"?
                          Конечно! Несколько реальных примеров.

                          Битовый протокол RTCM 104 2.2, он же ГОСТ Р 53612-2009. Протокол битовый, но от радиоприемника получаем 6-битные байты. С какого бита начинать расшифровку — непонятно. Чтобы быть уверенным в синхронизации — нужно прочесть пяток 30 битных слов. Одно из решений — параллельно запускаем 30 конечных автоматов, со сдвигом на бит. Какой первый прочтет 5 слов подряд — тот читает с нужной точки.

                          Другой пример. GNSS-приемник одновременно общается по трем протоколам на одном COM-порту. Ну все три нам не надо, но два — обязательно. Ибо информация там разная. Транспортные конверты — само собой, разные. Некую уверенность в распознавании протокола дает прием 6-8 байт. Но не полную — а вдург какие-то байты пропали или исказились? Так что пока CRC не сошлось — работают два конечных автомата.

                          Ну и давайте совсем простой пример. Обычный транспортный протокол переменной длины. Заголовок, номер пакета, длина, тело и CRC. Вроде все просто, можно линейно. Но… представьте, что исказилось поле длины. Было 4 байта, стало 260 (0x104). Сколько пакетов вы потеряете, пока не поймете, что CRC не сошлось? А конечный автомат легко запускается как на принятые из порта байты, так и на те, что мы записали в буфер, считая телом пакета. CRC не сошлось — делаем второй проход.

                          Ну и последнее. Глядя на наши некоторые конечные автоматы, я представляю себе, какая «каракатица» получится в «линейном» коде. То есть жуть со многими goto, не иначе. Ну как пример — протокол с данными неопределенной длины (не путать с переменной). Это означает, что конец пакета маркируется специальным символом, а в теле пакета этот символ ескейпится. В итоге из многих состояний есть два перехода — в начало, на обнаружение нового заголовка, и в конец, на обработку конца пакета.

                          Вы точно этот трехслойный (на деле больше слоев) механизм, который пишут 3 разные команды хотите на машину состояний разобрать? Как по мне пускай уже будет в одном потоке, прерывается где нужно.
                          Я бы сделал классически, как в RSX-11. Слой прерываний, слой отложенной обработки (через тот же диспетчер) и слой библиотеки.

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

                          Цель задачи чисто синтетическая: чтобы просто присутствовали все примитивы синхронизации.
                          Это философский вопрос — можно ли показать преимущества многопоточности там, где более всего видны её недостатки.

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

                          Нет огромной глобальной машины состояния в стиле «если мы ждем ответа по сети, а пользователь еще не нажал кнопку, но при этом уже не дорисовалась картинка на экране, то делаем то тогда поговорим с датчиком».
                          Так её нигде нет. Ни в однотредной модели, ни в многотредной, ни в асинхронной модели с диспетчером. Есть много мелких конечных lock-free кусочков. Если времена ожидания примерно одинаковы — можно их в тесный цикл запихать. Если разные — в диспетчер. Если кто-то из приоритетный, а другой — жадный до CPU, то в разные нити. Это кирпичики, как ни компануй — они будут работать.

                          А кому критические секции это все еще долго, то тогда добро пожаловать в lock-free алгоритмы.
                          Самый важный момент, ибо для меня наоборот. Пишите все в неблокирующем стиле с нулевым ожиданием. А уж если библиотека не позволяет — ну тогда, как исключение, можно и многопоточность.

                          Пожалуйста, мысленно замените в моих постах слово «железобетонный» на «очень надежный».
                          В крайнем случае приложение перезапустят.
                          Что в лоб — что по лбу. Если упадет 31 декабря — будет висеть 2 недели до перезапуска. То есть опять — «губящее при падении все живое вокруг». :-)

                          Тут речь уже про десктопное и серверное приложение (2 варианта одного кода).
                          Ну так и у меня тоже — сервис. Только питание ненадежно — поэтому UPS. Сервера ненадежны — поэтому их два. Диски ненадежны — поэтому пишут оба сервера. Сети ненадежны — поэтому их две. Код ненадежен, поэтому устроен как многослойный ванька-встанька. И на последнем уровне — программный wahchdog и перезапуск винды. Зато — никаких «перезапустят». Где бы и как бы не сломалось — поднимется. 30% цены проекта ушло на это.

                          Тем не менее креши и зависания прямо влияют на поток говна на пользовательских форумах, а те в свою очередь на продажи приложения. Так что такие штуки отлавливаются и чинятся в первую очередь.
                          Ну то есть они все-таки есть? :-)

                          Упавшие (закрешившиеся) треды не восстанавливаются. Возникает глобальное исключение, программа пытается сохранить пользовательские данные и максимально корректно завершиться. Повисшие треды отлавливаем таймаутами. Но как я уже сказал выше у нас задача написать такой пуленепробиваемый код, чтобы пользователь никогда с этим не столкнулся.
                          А у меня была другая задача. Пуленепробиваемый код — это удорожание цены проекта в разы. А у меня было 2 часа на отладку в месяц. В смысле на комплексную — на живом стане. Поэтому обошелся вылизыванием до уровня 1 ошибка на 500 строк. А дальше — ванька-встанька. Exception сначала пытаемся исправить в рамках процедуры, потом — пересозданием объектов, потом — перезапуском тредом, потом — рестартом сервиса, потом — ребутом винды с передачей управления резервному серверу. И все это — с контролем попыток. Пару раз сделали, не помогло — значит эскалируем дальше.

                          В каждом потоке так или иначе есть вечный цикл с условием выхода по флажку или conditional variable.
                          АГА! Теперь чувствуете, почему xSemaphoreTake(xSemaphore, portMAX_DELAY); — это костыль? Он несовместим с выходом по флажку из-за portMAX_DELAY. Чтобы было совместимо — надо хитрее. Прочли состояние — и пошли дальше. И ожидание мелкими квантами.

                          Повисшие треды отлавливаем таймаутами.
                          Кто будет сторожить сторожей? Если не трудно, расскажите, кто отлавливает и как вы боретесь с тем, чтобы ловец таймаутов сам не завис. У меня-то это было довольно тупо устроено — все потихонечку следили за соседями по функциональности.

                          А у вас небось TDD? У меня-то был мой собственный bug driven development, это когда каждый баг закрывается трижды-четырежды. Сначала — проверка всех аналогичных мест в коде. Потом — корректная диагностика в лог. Потом — восстановление без исправления ошибки. А уж потом — исправление.

                          Ну и понятно, что 10% кода — это assert. Так что основные падения — на них. Упал — отжался и побежал дальше.

                          А идеал — идеал, это пластичный код, деградирующий при наличии ошибок. От царапин и ушибов вы же не умираете? Вот и код не должен умирать, а должен мягко деградировать.

                          Главное отличие в идеалогии — я считаю себя дураком, считаю, что в коде всегда будут ошибки. Поэтому упор — на робастность, то есть возможность работы несмотря на ошибки.

                          P.S. Глянул вашу статью про SD-карту — мы там уже переписывались. Кстати, упомянутый коллега — тот же.

                          P.P.S. Ещё одна идеологическая фишка — нужны регрессионные тесты. А это значит, что мы должны уметь работать по файлам. То есть отделить конечный автоматы дешифровки от конечных автоматов приема данных. В линейном коде с этим хуже. Сейчас 99% GNSS-кода одинаково работают в Windows, linux и FreeRTOS. Только оставшийся 1% отлаживается на железе.
                            –1
                            Конечно! Несколько реальных примеров.

                            Обращаю Ваше внимание, что все 4 примера из одной области — парсинга входных данных.

                            А так да, парсить входной поток стейт машиной это удобно. Даже тулы специализированные для этого есть (yacc'у уже сто лет в обед).

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

                            Не имею ничего против. Но тогда уже корутины с диспетчерами будут лишними :)

                            Это философский вопрос — можно ли показать преимущества многопоточности там, где более всего видны её недостатки.

                            Еще разок, этот пример НЕ призван показывать преимущества многопоточности. Это просто код, где есть ее использование.

                            Можно было сделать функциональность по другому? однозначно да! Но тогда статьи бы не было :)

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

                            В том-то и гадость, что реально получается обратное. Ибо одним делом (выводом на консоль) занимаются два потока сразу. Ну и активно дерутся за ресурс.

                            Не путайте семантику с инструментами. Вряд ли кто нибудь в здравом уме будет писать поток, чьей основной обязанностью будет захват критической секции.

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

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

                            В крайнем случае приложение перезапустят.

                            Что в лоб — что по лбу. Если упадет 31 декабря — будет висеть 2 недели до перезапуска. То есть опять — «губящее при падении все живое вокруг». :-)

                            Имелось в виду если это десктопное приложение (оно оконное, с UI), то пользователь его и перезапустит. Если пользователь не перезапускает — значит он в отпуске или оно ему не нужно :)

                            В серверном варианте оно как-то перезапускается автоматически. Но это уже сфера ответственности девопсов, я не в курсе деталей что там да как.

                            Так что такие штуки отлавливаются и чинятся в первую очередь.

                            Ну то есть они все-таки есть? :-)

                            Ну конечно бывают.
                            Если полстотни девов педалят код, делая десятки коммитов в день, то рано или поздно кто нибудь накосячит :) Обычно это отлавливается на тестировании, но случаи разные бывают.

                            portMAX_DELAY — это костыль

                            Аааа, вот Вы про что. Не обратил внимание на выделение.

                            Тем не менее я не считаю что отсутствие таймаутов это всегда костыль. Я бы относился к таймаутам как к инструменту — где-то он уместен, где-то нет. И вообще это палка о двух концах.

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

                            Кто будет сторожить сторожей?

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

                            А у вас небось TDD?

                            К сожалению нам до ТДД еще оооооооой как далеко.
                            Но автоматическое тестирование мы любим, да.
                              0
                              Обращаю Ваше внимание, что все 4 примера из одной области — парсинга входных данных.
                              Ну и что? Это ровно ответ на ваш вопрос

                              Но тут уже возникает вопрос: как это будет выглядеть в коде? Стейт машина? Вы точно уверенны, что это удобнее для чтения чем просто линейный алгоритм с циклом"

                              Да, это удобнее, чем линейный алгоритм с циклами. Да, мы можем каждый конечный автомат попытаться превратить в линейный алгоритм, просто в куче мест вставив прием с порта с ожиданием. Ваше Сделать А -> (Сделать B -> Сделать C) * 5 раз -> Сделать D — это и есть приема пакета, где А — заголовок, B и С — четный и нечетный байты тела, D — хвост.

                              И ровно то же самое — при приеме длинных команд по SPI. Тоже проще с конечным автоматом.

                              Ну вот вам жуткий линейный код из HAL STM32H7

                              HAL_RCC_OscConfig
                              __weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef  *RCC_OscInitStruct)
                              {
                                uint32_t tickstart = 0;
                              
                                /* Check the parameters */
                                assert_param(IS_RCC_OSCILLATORTYPE(RCC_OscInitStruct->OscillatorType));
                                /*------------------------------- HSE Configuration ------------------------*/
                                if(((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSE) == RCC_OSCILLATORTYPE_HSE)
                                {
                                  /* Check the parameters */
                                  assert_param(IS_RCC_HSE(RCC_OscInitStruct->HSEState));
                                  /* When the HSE is used as system clock or clock source for PLL in these cases HSE will not disabled */
                                  if((__HAL_RCC_GET_SYSCLK_SOURCE() == RCC_CFGR_SWS_HSE) || ((__HAL_RCC_GET_SYSCLK_SOURCE() == RCC_CFGR_SWS_PLL1) && ((RCC->PLLCKSELR & RCC_PLLCKSELR_PLLSRC) == RCC_PLLCKSELR_PLLSRC_HSE)))
                                  {
                                    if((__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET) && (RCC_OscInitStruct->HSEState == RCC_HSE_OFF))
                                    {
                                      return HAL_ERROR;
                                    }
                                  }
                                  else
                                  {
                                    /* Set the new HSE configuration ---------------------------------------*/
                                    __HAL_RCC_HSE_CONFIG(RCC_OscInitStruct->HSEState);
                              
                                    /* Check the HSE State */
                                    if(RCC_OscInitStruct->HSEState != RCC_HSE_OFF)
                                    {
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till HSE is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) == RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > HSE_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                    }
                                    else
                                    {
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till HSE is bypassed or disabled */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > HSE_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                    }
                                  }
                                }
                                /*----------------------------- HSI Configuration --------------------------*/
                                if(((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSI) == RCC_OSCILLATORTYPE_HSI)
                                {
                                  /* Check the parameters */
                                  assert_param(IS_RCC_HSI(RCC_OscInitStruct->HSIState));
                                  assert_param(IS_RCC_CALIBRATION_VALUE(RCC_OscInitStruct->HSICalibrationValue));
                              
                                  /* When the HSI is used as system clock it will not disabled */
                                  if((__HAL_RCC_GET_SYSCLK_SOURCE() == RCC_CFGR_SWS_HSI) || ((__HAL_RCC_GET_SYSCLK_SOURCE() == RCC_CFGR_SWS_PLL1) && ((RCC->PLLCKSELR & RCC_PLLCKSELR_PLLSRC) == RCC_PLLCKSELR_PLLSRC_HSI)))
                                  {
                                    /* When HSI is used as system clock it will not disabled */
                                    if((__HAL_RCC_GET_FLAG(RCC_FLAG_HSIRDY) != RESET) && (RCC_OscInitStruct->HSIState == RCC_HSI_OFF))
                                    {
                                      return HAL_ERROR;
                                    }
                                    /* Otherwise, just the calibration is allowed */
                                    else
                                    {
                                    /* Enable the Internal High Speed oscillator (HSI, HSIDIV2,HSIDIV4, or HSIDIV8) */
                                      __HAL_RCC_HSI_CONFIG(RCC_OscInitStruct->HSIState);
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till HSI is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSIRDY) == RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > HSI_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                      /* Adjusts the Internal High Speed oscillator (HSI) calibration value.*/
                                      __HAL_RCC_HSI_CALIBRATIONVALUE_ADJUST(RCC_OscInitStruct->HSICalibrationValue);
                                    }
                                  }
                                  
                                  else
                                  {
                                    /* Check the HSI State */
                                    if((RCC_OscInitStruct->HSIState)!= RCC_HSI_OFF)
                                    {
                                   /* Enable the Internal High Speed oscillator (HSI, HSIDIV2,HSIDIV4, or HSIDIV8) */
                                      __HAL_RCC_HSI_CONFIG(RCC_OscInitStruct->HSIState);
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till HSI is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSIRDY) == RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > HSI_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                              
                                      /* Adjusts the Internal High Speed oscillator (HSI) calibration value.*/
                                      __HAL_RCC_HSI_CALIBRATIONVALUE_ADJUST(RCC_OscInitStruct->HSICalibrationValue);
                                    }
                                    else
                                    {
                                      /* Disable the Internal High Speed oscillator (HSI). */
                                      __HAL_RCC_HSI_DISABLE();
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till HSI is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSIRDY) != RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > HSI_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                    }
                                  }
                                }
                                /*----------------------------- CSI Configuration --------------------------*/
                                if(((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_CSI) == RCC_OSCILLATORTYPE_CSI)
                                {
                                  /* Check the parameters */
                                  assert_param(IS_RCC_CSI(RCC_OscInitStruct->CSIState));
                                  assert_param(IS_RCC_CALIBRATION_VALUE(RCC_OscInitStruct->CSICalibrationValue));
                              
                                  /* When the CSI is used as system clock it will not disabled */
                                  if((__HAL_RCC_GET_SYSCLK_SOURCE() == RCC_CFGR_SWS_CSI) || ((__HAL_RCC_GET_SYSCLK_SOURCE() == RCC_CFGR_SWS_PLL1) && ((RCC->PLLCKSELR & RCC_PLLCKSELR_PLLSRC) == RCC_PLLCKSELR_PLLSRC_CSI)))
                                  {
                                    /* When CSI is used as system clock it will not disabled */
                                    if((__HAL_RCC_GET_FLAG(RCC_FLAG_CSIRDY) != RESET) && (RCC_OscInitStruct->CSIState != RCC_CSI_ON))
                                    {
                                      return HAL_ERROR;
                                    }
                                    /* Otherwise, just the calibration is allowed */
                                    else
                                    {
                                      /* Adjusts the Internal High Speed oscillator (CSI) calibration value.*/
                                      __HAL_RCC_CSI_CALIBRATIONVALUE_ADJUST(RCC_OscInitStruct->CSICalibrationValue);
                                    }
                                  }
                                  else
                                  {
                                    /* Check the CSI State */
                                    if((RCC_OscInitStruct->CSIState)!= RCC_CSI_OFF)
                                    {
                                      /* Enable the Internal High Speed oscillator (CSI). */
                                      __HAL_RCC_CSI_ENABLE();
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till CSI is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_CSIRDY) == RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > CSI_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                              
                                      /* Adjusts the Internal High Speed oscillator (CSI) calibration value.*/
                                      __HAL_RCC_CSI_CALIBRATIONVALUE_ADJUST(RCC_OscInitStruct->CSICalibrationValue);
                                    }
                                    else
                                    {
                                      /* Disable the Internal High Speed oscillator (CSI). */
                                      __HAL_RCC_CSI_DISABLE();
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till CSI is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_CSIRDY) != RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > CSI_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                    }
                                  }
                                }
                                /*------------------------------ LSI Configuration -------------------------*/
                                if(((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_LSI) == RCC_OSCILLATORTYPE_LSI)
                                {
                                  /* Check the parameters */
                                  assert_param(IS_RCC_LSI(RCC_OscInitStruct->LSIState));
                              
                                  /* Check the LSI State */
                                  if((RCC_OscInitStruct->LSIState)!= RCC_LSI_OFF)
                                  {
                                    /* Enable the Internal Low Speed oscillator (LSI). */
                                    __HAL_RCC_LSI_ENABLE();
                              
                                    /* Get Start Tick*/
                                    tickstart = HAL_GetTick();
                              
                                    /* Wait till LSI is ready */
                                    while(__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET)
                                    {
                                      if((int32_t) (HAL_GetTick() - tickstart ) > LSI_TIMEOUT_VALUE)
                                      {
                                        return HAL_TIMEOUT;
                                      }
                                    }
                                  }
                                  else
                                  {
                                    /* Disable the Internal Low Speed oscillator (LSI). */
                                    __HAL_RCC_LSI_DISABLE();
                              
                                    /* Get Start Tick*/
                                    tickstart = HAL_GetTick();
                              
                                    /* Wait till LSI is ready */
                                    while(__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) != RESET)
                                    {
                                      if((int32_t) (HAL_GetTick() - tickstart ) > LSI_TIMEOUT_VALUE)
                                      {
                                        return HAL_TIMEOUT;
                                      }
                                    }
                                  }
                                }
                              
                                /*------------------------------ HSI48 Configuration -------------------------*/ 
                                if(((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSI48) == RCC_OSCILLATORTYPE_HSI48)
                                {
                                  /* Check the parameters */
                                  assert_param(IS_RCC_HSI48(RCC_OscInitStruct->HSI48State));
                                  
                                  /* Check the HSI48 State */
                                  if((RCC_OscInitStruct->HSI48State)!= RCC_HSI48_OFF)
                                  {
                                    /* Enable the Internal Low Speed oscillator (HSI48). */
                                    __HAL_RCC_HSI48_ENABLE();
                                    
                                    /* Get time-out */
                                    tickstart = HAL_GetTick();
                                    
                                    /* Wait till HSI48 is ready */  
                                    while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSI48RDY) == RESET)
                                    {
                                      if((HAL_GetTick() - tickstart ) > HSI48_TIMEOUT_VALUE)
                                      {
                                        return HAL_TIMEOUT;
                                      }      
                                    } 
                                  }
                                  else
                                  {
                                    /* Disable the Internal Low Speed oscillator (HSI48). */
                                    __HAL_RCC_HSI48_DISABLE();
                                    
                                    /* Get time-out */
                                    tickstart = HAL_GetTick();
                                    
                                    /* Wait till HSI48 is ready */  
                                    while(__HAL_RCC_GET_FLAG(RCC_FLAG_HSI48RDY) != RESET)
                                    {
                                      if((HAL_GetTick() - tickstart ) > HSI48_TIMEOUT_VALUE)
                                      {
                                        return HAL_TIMEOUT;
                                      }      
                                    } 
                                  }
                                }
                                /*------------------------------ LSE Configuration -------------------------*/
                                if(((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_LSE) == RCC_OSCILLATORTYPE_LSE)
                                {
                                  /* Check the parameters */
                                  assert_param(IS_RCC_LSE(RCC_OscInitStruct->LSEState));
                              
                                  /* Enable write access to Backup domain */
                                  PWR->CR1 |= PWR_CR1_DBP;
                              
                                  /* Wait for Backup domain Write protection disable */
                                  tickstart = HAL_GetTick();
                              
                                  while((PWR->CR1 & PWR_CR1_DBP) == RESET)
                                  {
                                    if((int32_t) (HAL_GetTick() - tickstart ) > RCC_DBP_TIMEOUT_VALUE)
                                    {
                                      return HAL_TIMEOUT;
                                    }
                                  }
                              
                                  /* Set the new LSE configuration -----------------------------------------*/
                                  __HAL_RCC_LSE_CONFIG(RCC_OscInitStruct->LSEState);
                                  /* Check the LSE State */
                                  if((RCC_OscInitStruct->LSEState) != RCC_LSE_OFF)
                                  {
                                    /* Get Start Tick*/
                                    tickstart = HAL_GetTick();
                              
                                    /* Wait till LSE is ready */
                                    while(__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET)
                                    {
                                      if((int32_t) (HAL_GetTick() - tickstart ) > RCC_LSE_TIMEOUT_VALUE)
                                      {
                                        return HAL_TIMEOUT;
                                      }
                                    }
                                  }
                                  else
                                  {
                                    /* Get Start Tick*/
                                    tickstart = HAL_GetTick();
                              
                                    /* Wait till LSE is ready */
                                    while(__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) != RESET)
                                    {
                                      if((int32_t) (HAL_GetTick() - tickstart ) > RCC_LSE_TIMEOUT_VALUE)
                                      {
                                        return HAL_TIMEOUT;
                                      }
                                    }
                                  }
                                }
                                /*-------------------------------- PLL Configuration -----------------------*/
                                /* Check the parameters */
                                assert_param(IS_RCC_PLL(RCC_OscInitStruct->PLL.PLLState));
                                if ((RCC_OscInitStruct->PLL.PLLState) != RCC_PLL_NONE)
                                {
                                  /* Check if the PLL is used as system clock or not */
                                  if(__HAL_RCC_GET_SYSCLK_SOURCE() != RCC_CFGR_SWS_PLL1)
                                  {
                                    if((RCC_OscInitStruct->PLL.PLLState) == RCC_PLL_ON)
                                    {
                                      /* Check the parameters */
                                      assert_param(IS_RCC_PLLSOURCE(RCC_OscInitStruct->PLL.PLLSource));
                                      assert_param(IS_RCC_PLLM_VALUE(RCC_OscInitStruct->PLL.PLLM));
                                      assert_param(IS_RCC_PLLN_VALUE(RCC_OscInitStruct->PLL.PLLN));
                                      assert_param(IS_RCC_PLLP_VALUE(RCC_OscInitStruct->PLL.PLLP));
                                      assert_param(IS_RCC_PLLQ_VALUE(RCC_OscInitStruct->PLL.PLLQ));
                                      assert_param(IS_RCC_PLLQ_VALUE(RCC_OscInitStruct->PLL.PLLR));
                              
                                      /* Disable the main PLL. */
                                      __HAL_RCC_PLL_DISABLE();
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till PLL is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_PLLRDY) != RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > PLL_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                              
                                      /* Configure the main PLL clock source, multiplication and division factors. */
                                      __HAL_RCC_PLL_CONFIG(RCC_OscInitStruct->PLL.PLLSource,
                                                           RCC_OscInitStruct->PLL.PLLM,
                                                           RCC_OscInitStruct->PLL.PLLN,
                                                           RCC_OscInitStruct->PLL.PLLP,
                                                           RCC_OscInitStruct->PLL.PLLQ,
                                                           RCC_OscInitStruct->PLL.PLLR);
                              
                                       /* Configure PLL  PLL1FRACN */
                                       __HAL_RCC_PLLFRACN_CONFIG(RCC_OscInitStruct->PLL.PLLFRACN);
                              
                                      /* Select PLL1 input reference frequency range: VCI */ 
                                      __HAL_RCC_PLL_VCIRANGE(RCC_OscInitStruct->PLL.PLLRGE) ;
                              
                                      /* Select PLL1 output frequency range : VCO */
                                      __HAL_RCC_PLL_VCORANGE(RCC_OscInitStruct->PLL.PLLVCOSEL) ;
                              
                                      /* Enable PLL System Clock output. */
                                       __HAL_RCC_PLLCLKOUT_ENABLE(RCC_PLL1_DIVP);
                              
                                      /* Enable PLL1Q Clock output. */
                                       __HAL_RCC_PLLCLKOUT_ENABLE(RCC_PLL1_DIVQ);
                               
                                      /* Enable PLL1R  Clock output. */
                                       __HAL_RCC_PLLCLKOUT_ENABLE(RCC_PLL1_DIVR);
                              
                                      /* Enable PLL1FRACN . */
                                       __HAL_RCC_PLLFRACN_ENABLE();
                              
                                      /* Enable the main PLL. */
                                      __HAL_RCC_PLL_ENABLE();
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till PLL is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_PLLRDY) == RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > PLL_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                    }
                                    else
                                    {
                                      /* Disable the main PLL. */
                                      __HAL_RCC_PLL_DISABLE();
                              
                                      /* Get Start Tick*/
                                      tickstart = HAL_GetTick();
                              
                                      /* Wait till PLL is ready */
                                      while(__HAL_RCC_GET_FLAG(RCC_FLAG_PLLRDY) != RESET)
                                      {
                                        if((int32_t) (HAL_GetTick() - tickstart ) > PLL_TIMEOUT_VALUE)
                                        {
                                          return HAL_TIMEOUT;
                                        }
                                      }
                                    }
                                  }
                                  else
                                  {
                                    return HAL_ERROR;
                                  }
                                }
                                return HAL_OK;
                              }
                              


                              Как вы будете отлаживать, если он выдаст HAL_TIMEOUT? Ну только кучу точек останова выставит. А точки останова — меняют тайминг и код работает иначе. В худшем случае — имеем гейзенбаг. А у конечного автомата, помимо простой структуры кода, всегда будет последнее состояние, до которого дошли. И узнать место ошибки — это не 2 часа возни с точками останова, а 5 минут.

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

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

                              Вряд ли кто нибудь в здравом уме будет писать поток, чьей основной обязанностью будет захват критической секции.
                              Почему??? В учебниках по дельфи есть такое. Полюбуйтесь. Synchronize — это аналог захвата критической секции.

                              Вот вам код из учебника
                              procedure TMyThread.Execute;
                              begin
                              while True do
                                begin
                                  Synchronize(UpdateCaption);
                                  sleep(100);
                                end;
                              end;



                              Еще разок, этот пример НЕ призван показывать преимущества многопоточности. Это просто код, где есть ее использование.
                              Беда в том, что джуны потом так и пишут — ровно как в учебниках и статьях. И им сложно объяснить, что в учебнике — лажа.

                              А то, что все они, например, пишут в лог или в консоль (захватывая по пути критическую секцию) — это вторично, это не основная задача. В большинстве случаев с такими накладными расходами можно смириться.
                              А вы проверяли?:-) Компилятор Pascal-fast (Паскаль-Москаль) при компиляции выдавал количество откомпилированных строк. Сделав выдачу номера каждой сотой строки (вместо каждой) егоускорили в два раза. Это я к тому, что всегда просчитывать надо. А ещё лучше — мерять.

                              Гораздо сложнее когда оно то работает, то не работает, то опять работает после таймаута
                              Согласен.

                              Главное не повредить пользовательские данные.
                              Обычно считается, что при любом access violation все данные в куче могут быть запорчены и сохранять нечего. Например, так ведет себя MS word. С другой стороны, мой опыт показывает, что при некоторой дисциплине написания, глобальная порча кучи — это дикая редкость, а access violation происходит от локальных причин. То есть можно проверить структуру кучи и сохранить, если она не нарушена. Или вообще всегда сохранять, но предупредить пользователя, что данным могут быть попорчены.
                                +1
                                Почему??? В учебниках по дельфи есть такое. Полюбуйтесь. Synchronize — это аналог захвата критической секции.

                                Но основная обязанность этого потока — она в UpdateCaption, а не в захвате CS как таковом.
                                Человек правильно сказал «Не путайте семантику с инструментами».
                                  0
                                  Возможно и путаю, не спорю. Можете определить эти термины четче?

                                  Если мне надо изменить заголовок по таймеру — я просто встрою компонент «Таймер» в форму. Если мне надо обновить заголовок их другого треда — пошлю сообщение (или своё или системное). А тут цель была — показать, как работает Synchronize.

                                  И к сожалению, куча джунов воспринимают такие примеры как правильный метод работы. То есть в лучшем случае пишется тред, при ошибках в нем говориться «дельфи — гавно, там баги» и смысловая часть треда упихивается в synchronize. В худшем — так пишется изначально, ибо так в учебнике, а «учебник всегда прав».

                                  Ровно поэтому я за то, чтобы примеры в учебниках и статьях не учили заведомо неверному написанию кода. Кто уже умеет работать с тредами — тому подобные статьи не нужны. А кто не умеет — берет дурной пример за образец.

                                  В качестве примера для synchronize я бы написал тяжелый математический расчет (нахождение простых чисел больше миллиона?), который раз в несколько секунд выводит очередной результат в TMemo через synchronize.

                                  А в качестве примера на треды — высокоскоростной PID-регулятор и прием уставок для него с консоли.
                                    0
                                    Ну вообще-то экранная форма, что в Delphi, что без — это уже конечный автомат, который дергает оконный менеджер.
                                  0
                                  Ну вот вам жуткий линейный код из HAL STM32H7

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

                                  Точно также я мог бы предложить подебажиться в табличном автомате имени yacc'а.

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

                                  Что в линейном код, что в конечном автомате, если мы уже очутились в точке «все плохо», то даже имея значения регистров не всегда ясно что именно привело систему в такое состояние. Более того, если в функции 5 выходов то расставить бряки на всех можно быстро понять в какой именно ветке проблема.

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

                                    Точно также я мог бы предложить подебажиться в табличном автомате имени yacc'а.
                                    По мне там так все довольно просто. В yytranslate я бы добавил комментариями символы ASCII, а состояния именовал бы не кодами, а сделал бы enum. Но там чуть дальше (397-441 и ниже) — приличный механизм распечатки.

                                    Но это я писал лексические анализаторы и понимаю, как они устроены.

                                    Что в линейном код, что в конечном автомате, если мы уже очутились в точке «все плохо», то даже имея значения регистров не всегда ясно что именно привело систему в такое состояние.
                                    А для этого конечный автомат может хранить или печатать историю состояний.

                                    Более того, если в функции 5 выходов то расставить бряки на всех можно быстро понять в какой именно ветке проблема
                                    Да, но скорее всего в точке выхода уже будет не посмотреть значения из-за оптимизации. А неоптимизированный код — будет работать иначе.

                                    Есть задачи где лучше будет работать конечный автомат, а есть вещи где линейный алгоритм самое оно.
                                    Конечно! Более того, типов алгоритмов больше. Скажем синтаксический анализатор проще всего писать рекурсивным спуском, несмотря на его сумбурные репорты об ошибках в разбираемом тексте.

                                    Я о немного о другом. О том, что внешние устройства (ну и внутренние, вообще-то тоже) изначально имеют разные состояния. И конечный автомат естественным путем ложится на эти состояния. Точно так же, как на БНФ естественно ложится рекурсивный спуск.
                                      0
                                      Ну как минимум по семантике эталонный.

                                      Мммм, скорее все таки референсный. Типа «смотрите, с таким кодом наше железо работает (при некоторых условиях)». А до оптимальности и красоты там далеко. Да и багов там есть, в т.ч. и по семантике.

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

                                      /**
                                        * @brief  Initialize the DMA according to the specified
                                        *         parameters in the DMA_InitTypeDef and initialize the associated handle.
                                        * @param  hdma: Pointer to a DMA_HandleTypeDef structure that contains
                                        *               the configuration information for the specified DMA Channel.
                                        * @retval HAL status
                                        */
                                      HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma)
                                      

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

                                      Эталонный это почти как идеальный. Мало того, что взял и работает, так еще и без подводных камней. И интерфейс чтобы был удобный, и чтобы лишнего ничего не делал.

                                      Была бы эта реализация эталонной, то не было бы такого количества велосипедов по ручной работе с периферией
                                        0
                                        Написано, что инициализирует DMA в соответствии с параметрами в DMA_InitTypeDef, ну и заодно хендл. Так что регистры менять должен.

                                        Теперь придется менять логику работы моего кода, чтобы объехать такой нюанс.
                                        А зачем? Планируете менять процессоры? Хотите исправления ошибок из более поздних версий STMcube? Хотите настройки из cube?

                                        То есть почему надо использовать процедуры из cube, а не скопировать нужное в свой код?

                                        Что касается кода — он образцовый (тот, который нужно брать за образец). Не потому, что дико хорош, а потому, что его писали люди, знающие не только официальную документацию, но и те тонкости, что в документацию не попали. А уж как это переводить на английский — it's depend…
                                          0
                                          А зачем? Планируете менять процессоры? Хотите исправления ошибок из более поздних версий STMcube? Хотите настройки из cube?

                                          Да, сейчас у меня явно неудачный процессор. Поколупаюсь еще немного с этим, потом буду переползать на другой. Причем на какой — неясно. Буду экспериментировать.

                                          При всей моей нелюбви к HAL, настройки из куба — это удобно. Если у тебя ничего не работает можно в пару кликов сделать референсный проект в кубе и посмотреть как надо. А если самому с регистрами работать, то будет ооой как сложно побитово конфигурации сравнивать.

                                          А так да, генерирую кубом, а потом копирую кусками в свой код.

                                          Что касается кода — он образцовый (тот, который нужно брать за образец).

                                          Ок, с этим согласен. Это как раз то, что я имел в виду под словом референсный. Видимо это у меня калька с английского reference.
                                            0
                                            Если у тебя ничего не работает можно в пару кликов сделать референсный проект в кубе и посмотреть как надо.
                                            Это только, если оно работает в кубе. :-) А если в кубе не работает (или настройки неверные, или баги, ибо процессор новый и куб для него ещё сырой), то цикл отладки быстрее на своих сорцах. Перегенерация куба и перекомпиляция нагенеренного — это довольно долго. Мы просто влетели в эту ситуацию с CAN на H7. Но у нас был специалист по CAN, которому проще было поднять CAN с нуля, чем разбираться в кубе. Тем более, что в тот момент только-только в кубе пофиксили другие баги для H7

                                            Это как раз то, что я имел в виду под словом референсный
                                            Ага, а для меня образец — это эталон (или paatern). А Reference — ссылка или ссылочный. Но это я слабо английский знаю.

                                              0
                                              Но это я слабо английский знаю.

                                              Это да, но высказывать суждения это не мешает(

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

                    Самое читаемое