Как стать автором
Обновить

С++ обертка для «всех» Операционных Систем Реального Времени для CortexM4

Время на прочтение27 мин
Количество просмотров21K
image

Я уже рассказывал о том как можно использовать FreeRtos для проектов, написанных на С++ в статье STM32, C++ и FreeRTOS. Разработка с нуля. Часть 1. С тех пор прошло целых 3 года, я серьезно постарел, потерял кучу нейронных связей, поэтому решил встряхнуть стариной для того, чтобы эти связи восстановить и замахнуться на обертку для «любой» популярной ОСРВ. Это конечно шутка, я намеренно взял «всех» в кавычки, но в каждой шутке есть доля правды.

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

На мой взгляд, идеально подходит для моих проектов отечественная ОСРВ МАКС, написанная на С++ и пользоваться ей одно удовольствие.

Но загвоздка состоит в том, что наши устройства должны соответствовать стандарту IEC_61508, одно из требований которого звучит, как E.29 Application of proven-in-use target library. Ну или простыми словами, если вы делаете устройство на соответствие уровню SIL3, то будьте добры (Higher Recommended) используйте и библиотеки которые соответствуют этому уровню и проверены временем.

Касательно нашей задачи, это означает, что использовать ОСРВ МАКС для таких устройств можно, но балы за надежность не прибавятся. Поэтому производители ОСРВ делают специальные версии своих операционнок соответствующие стандартам IEC_61508, например, FreeRTOS имеет клона SafeRTOS, а embOs имеет клона embOS-Safe, конечно же производители очень хорошо на этом зарабатывают, ведь лицензии на эти операционки стоят по несколько тысяч, а то и десятки тысяч долларов.

Кстати, хороший пример, это компилятор IAR, лицензия на который стоит порядка 1500 долларов, зато IAR Certified versions стоит уже порядка 10000 баксов, хотя я проверял на нескольких проектах — выходной файл версии без сертификата и с сертификатом полностью идентичны. Ну вы поняли, что за спокойствие нужно платить.

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

image

Ну или вот такая…

image

Поэтому я решил написать обертку, которая подошла бы как FreeRTOS, так и скажем embOS ну и для всех остальных тоже :) и для начала я определил что вообще мне нужно для полного счастья:

  • Задачи
  • Критические секции
  • События и нотификация задач
  • Семафоры и мьютексы
  • Очереди

Обертка должна быть SIL3 идейной, а этот уровень накладывает ну очень много всяких High Recommended вещей, и если им полностью следовать, то выйдет, что код лучше не писать вообще.

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

  • никаких макросов, ну кроме защиты от двойного включения заголовочных файлов. Макросы это зло, если посчитать сколько времени потрачено на поиск ошибок связанных с макросами, то выйдет, что вселенная не так уж и стара, а сколько хорошего можно было сделать за это время, наверное лучше их просто надо запретить на законодательном уровне, как запретили торренты или отнимать премию за каждый написанный макрос
  • не использовать указатели, конечно по возможности. Можно было бы попробовать вообще не использовать их, но все таки есть места где без них никак. В любом случае пользователь обертки, по возможности, не должен вообще знать, что такое указатель, так как про них он слышал только от своего дедушки, потому что сейчас он работает исключительно с ссылками
  • не использовать динамическое выделение памяти — тут все понятно, просто использование кучи приводит, во-первых, к тому, что нужно резервировать ОЗУ под эту кучу, а во-вторых, при частом использовании кучи она дефрагментируется и новые объекты на ней создаются все дольше и дольше. Поэтому собственно и FreeRTOS я настроил только на статически выделяемую память, установив configSUPPORT_STATIC_ALLOCATION 1. Но если вы захотите работать в режиме по умолчанию. А по умолчанию FreeRTOS использует для создание элементов операционки динамически выделяемую память, то достаточно установить configSUPPORT_STATIC_ALLOCATION 0, а
    configSUPPORT_DYNAMIC_ALLOCATION 1 и не забыть подключить реализацию собственных маллоков и каллоков из менеджера памяти, например вот этот файл FreeRtos/portable/MemMang/heap_1.c. Но учтите, что придется выделить под кучу операционки ОЗУ с запасом, так как точный размер необходимого ОЗУ посчитать вам не удастся, у меня при всех настройках (Idle включены, задача программных таймеров включена, мои две задачи, очереди, размер очереди для таймеров 10 и так далее, скажем так, что точно не самые оптимальные настройки) заработало когда я выделил памяти вот так:
    7 357 bytes of readonly code memory
    535 bytes of readonly data memory
    6 053 bytes of readwrite data memory

    Статическое распределение памяти «немножко» компактнее:
    7 329 bytes of readonly code memory
    535 bytes of readonly data memory
    3 877 bytes of readwrite data memory

    Вы можете подумаете, «огого… себе», но сейчас нас не интересует вопрос, сформулировнный в статье «Я выделил целых 3КБ операционной системе и запустил всего 3 задачи со стеком по 128Б, а на четвертую уже почему-то не хватает памяти» , в данной ситуации я это сделал намеренно, для наглядности, чтобы показать разницу между динамическим и статическим выделением памяти при одинаковых настройках.
  • не кастить типы, по возможности. Привидение типов к другим типам уже само по себе означает тот факт, что в дизайне что-то не так, но как обычно бывает, иногда для удобства использования все таки приходится кастить (например, enum приходится приводить к целочисленным ), а иногда не обойтись без этого, но надо этого избегать.
  • простота и удобство. Для пользователя обертки, все сложности должны быть скрыты, у него итак жизнь не масло, и не хочется еще ему её усложнять — создал задачу, реализовав в ней все что нужно, запустил и ушел радоваться жизни.

С этого и начнем, итак ставим перед собой задачу — создать задачу (получилось, прямо из серии «запрещено запрещать»).

Создание задачи


Путем долгих исследований, великобританские ученые (Вся правда об ОСРВ от Колина Уоллса. Статья #4. Задачи, переключение контекста и прерывания) (кстати, если вы не знали, ассемблер для ARM придумала тоже великобританская ученая, что-то я и не удивился ни разу :) ), так вот великобританские ученые выяснили, что для большинства «всех» ОСРВ у задачи имеется имя, стек, размер стека, «блок управления», идентификатор или указатель на «блок управления», приоритет, функция которая выполняется в задаче. Собственно все, и можно было все это запихать в класс, но это было правильно, если бы мы писали с вами операционку, но мы делаем обертку, поэтому хранить в обертке все эти вещи смысла нет, все это за вас сделает SIL3 идейная операционка, которую мы оборачиваем. По сути нам нужна только функция которая выполняется в задаче и структура хранящая «блок управления» , который заполняется при создании задачи и идентификатор задачи. Поэтому класс задачи, назовем его Thread может выглядеть очень просто:

 class Thread  {
    public:
      virtual void Execute() = 0 ;     
   private:
      tTaskHandle taskHandle ;
      tTaskContext context ;
  } ;

Хочется чтобы я просто объявил класс моей задачи, где смог бы реализовать, все что нужно и потом передал указатель на объект этого класса обертке, которая и создала бы с помощью API ОСРВ задачу, где запустила бы метод Execute():

class MyTask : public Thread {
public:
   virtual void Execute() override { 
      while(true) {
       //do something..
      }  
   } ;
using tMyTaskStack = std::array<OsWrapper::tStack, 
                     static_cast<tU16>(OsWrapper::StackDepth::minimal)> ;
  inline static tMyTaskStack Stack;  //!C++17
} ;

MyTask myDesiredTask
int main() {
  Rtos::CreateThread(myTask, MyTask::Stack.data(), "myTask") ;
}

Во «всех» ОСРВ, для того, чтобы задача создалась, необходимо передать указатель на функцию которая будет запускаться планировщиком. В нашем случае это функция Execute(), но передать указатель на этот метод я не могу, так как он не статический. Поэтому смотрим как создаётся задача в API «всех» операционок и замечаем, что можно создать задачу передав в функцию задачи параметр, например для embOS это:

void OS_TASK_CreateEx( OS_TASK* pTask,  const char* pName,  OS_PRIO Priority, 
       void (*pRoutine)(void * pVoid ), void OS_STACKPTR *pStack,  OS_UINT StackSize, OS_UINT TimeSlice, void* pContext);

void* pContext — это и есть ключ к решению. Пусть у нас будет статический метод, указатель на который мы будем передавать в качестве указателя на метод, вызываемый планировщиком, а в качестве параметра будем передавать указатель на объект типа Thread у которого можно вызвать метод Execute() напрямую. Это как раз тот момент, когда без указателя и приведения к типам никак, но этот код будет скрыт от пользователя:

static void Run(void *pContext ) {
   static_cast<Thread*>(pContext)->Execute() ;
 }

Т.е. алгоритм работы такой, планировщик запускает метод Run, в метод Run передается указатель на объект типа Thread. В методе Run напрямую вызывается метод Execute(), конкретного объекта класса Thread, являющийся как раз нашей реализацией задачи.

Задача почти решена, теперь нам нужно реализовать методы. Все операционки имеют различные API, поэтому чтобы реализовать, например, функцию создания задачи для embOS нужно вызвать метод void OS_TASK_CreateEx(..), а для FreeRTOS в режиме динамического выделения памяти — это xTaskCreate(..) и хотя суть у них одна и та же, но синтаксис и параметры разные. Мы же не хотим, каждый раз для новой операционки бегать по файлам и писать код для каждого из методов класса, поэтому нужно как то вынести это в один файл и… оформить в виде макросов. Отлично, но стоп, макросы же я запретил сам себе — нужен другой подход.

Самое простое, что пришло мне в голову, это сделать отдельный файл для каждой операционки с inline функциями. Если мы хотим использовать любую другую операционку, нам надо будет просто реализовать каждую из этих функций используя API этой операционки. Получился следующий файл rtosFreeRtos.cpp

#include "rtos.hpp" 
//For  FreeRTOS functions  prototypes
#include <FreeRTOS.h>      
//For xTaskCreate 
#include <task.h>
namespace OsWrapper {
     void wCreateThread(Thread & thread, const char * pName,
                            ThreadPriority prior,const tU16 stackDepth, 
                            tStack *pStack)  {
#if (configSUPPORT_STATIC_ALLOCATION == 1)
  if (pStack != nullptr)  {
    thread.handle = xTaskCreateStatic(static_cast<TaskFunction_t>(Rtos::Run),
                                           pName,
                                           stackDepth,
                                           &thread,
                                           static_cast<uint32_t>(prior),
                                           pStack,
                                           &thread.taskControlBlock);
  }
#else
    thread.handle =  (xTaskCreate(static_cast<TaskFunction_t>(Rtos::Run),
                        pName, stackDepth, &thread, static_cast<uint32_t>(prior),
                        &thread.handle) == pdTRUE) ?  thread.handle : nullptr ;
#endif
  }

Точно также может выглядеть и файл для embOS rtosEmbOS.cpp

#include "rtos.hpp" 
//For  embOS functions  prototypes
#include <rtos.h>      
namespace OsWrapper {
  void wCreateThread(Thread &thread, const char * pName, 
                            ThreadPriority prior,const tU16 stackDepth, 
                            tStack *pStack)  {
   constexpr OS_UINT timeSliceNull = 0 ;
    if (pStack != nullptr)  {
       OS_CreateTaskEx(&(thread.handle), 
                          pName, 
                          static_cast<OS_PRIO>(prior), 
                          Rtos::Run,
                          pStack,
                          ((stackSize == 0U) ? sizeof(pStack) : stackSize), 
                          timeSliceNull,
                          &thread) ;
    }     
  }

Типы у разных операционок тоже разные, особенно структуры контекста задач, поэтому создадим файл rtosdefs.hpp с нашими собственными оберточными псевдонимами.


#include <FreeRTOS.h>      //For TaskHandle_t

namespace OsWrapper {
  using tTaskContext = StaticTask_t;
  using tTaskHandle = TaskHandle_t;
  using tStack = StackType_t ;  
}

Для EmbOS это может выглядеть вот так:

#include <rtos.h>      //For OS_TASK

namespace OsWrapper {
  using tTaskContext = OS_TASK;
  using tTaskHandle = OS_TASK;
  using tStack = tU16 // вообще он void, но для нашего ядра это tU16 ; 
}

В итоге, для переделки под другую любую ОСРВ достаточно сделать изменения только в этих двух файлах rtosdefs.cpp и rtos.cpp. Теперь классы Thread и Rtos стали выглядят как c картинки

image

Запуск операционки и доработка задачи


Для Cortex M4 «все» операционки используют 3 прерывания, прерывания System tick timer, System Service call via SWI instruction, Pendable request for system service, которые собственно в основном для ОСРВ и были придуманы. Некоторые ОСРВ используют еще другие системные прерывания, но этих будет достаточно для большинства «всех» операционок. А если нет, то можно будет добавить, поэтому просто определим три обработчика этих прерываний и для запуска ОСРВ нам понадобится еще метод старт:

  
static void HandleSvcInterrupt() ;
static void HandleSvInterrupt() ;
static void HandleSysTickInterrupt() ;
static void Start() ;

Первое что мне нужно было и без чего я жить не могу, о чем мечтаю — это механизм нотификации задач. Мне вообще нравится Event-driven programming, поэтому нужно скорее реализовать обертку для нотификации задач.

Все оказалось довольно просто, любая операционка это умеет, ну за исключением может быть uc-OS-II и III, хотя может я плохо читал, но, по-моему механизм событий там вообще мудреный, ну да ладно, «все» то остальные то точно могут.

Для того, чтобы нотифицировать задачу нужно просто послать событие не в пустоту, а конкретно задаче, для этого в методе нотификации должен быть указатель на контекст задачи или идентификатор задачи. Такие я как раз храню в классе Thread, значит и метод оповещения должен быть у класса Thread. Там же должен быть и метод ожидания оповещения. Заодно уж добавим и метод Sleep(..) который приостанавливает выполнение вызывающей задачи. Теперь оба класса выглядят так:

image

rtos.hpp
/*******************************************************************************
* Filename  	: Rtos.hpp
* 
* Details   	: Rtos class is used to create tasks, work with special Rtos 
* functions and also it contains a special static method Run. In this method 
* the pointer on Thread should be pass. This method is input point as 
* the task of Rtos. In the body of the method, the method of concrete Thread 
* will run. 
*******************************************************************************/
#ifndef __RTOS_HPP
#define __RTOS_HPP

#include "thread.hpp"        // for Thread
#include "../../Common/susudefs.hpp"
#include "FreeRtos/rtosdefs.hpp"

namespace OsWrapper 
{
 
  extern void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *) ;
  extern void wStart() ;
  extern void wHandleSvcInterrupt() ;
  extern void wHandleSvInterrupt() ;
  extern void wHandleSysTickInterrupt() ;  
  extern void wEnterCriticalSection();
  extern void wLeaveCriticalSection();
  
  class Rtos
  {
    public:    
      
      static void CreateThread(Thread &thread ,
                               tStack * pStack = nullptr,
                               const char * pName = nullptr,
                               ThreadPriority prior = ThreadPriority::normal,
                               const tU16 stackDepth = static_cast<tU16>(StackDepth::minimal)) ;
      static void Start() ;       
      static void HandleSvcInterrupt() ;
      static void HandleSvInterrupt() ;
      static void HandleSysTickInterrupt() ;
      
      friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *);
    private:
      //cstat !MISRAC++2008-7-1-2 To prevent reinterpet_cast in the CreateTask
      static void Run(void *pContext ) 
      {
        static_cast<Thread*>(pContext)->Execute() ;
      }
  } ;
} ;
#endif // __RTOS_HPP



thread.hpp
/*******************************************************************************
* Filename  	: thread.hpp
* 
* Details   	: Base class for any Taskis which contains the pure virtual 
* method Execute().  Any active classes which will have a method for running as 
* a task of RTOS should inherit the Thread and override the Execute() method. 
* For example:
*            class MyTask : public OsWrapper::Thread
*            {
*            public:
*               virtual void Execute() override { 
*                 while(true) {
*                    //do something..
*                 }  
*            } ;
*
*******************************************************************************/
#ifndef __THREAD_HPP
#define __THREAD_HPP  

#include "FreeRtos/rtosdefs.hpp"
#include "../../Common/susudefs.hpp"

namespace OsWrapper 
{
  extern void wSleep(const tTime) ;
  extern void wSleepUntil(tTime &, const tTime) ;
  extern tTime wGetTicks() ;
  extern void wSignal(tTaskHandle const &, const tTaskEventMask) ;
  extern tTaskEventMask wWaitForSignal(const tTaskEventMask, tTime) ;
  constexpr tTaskEventMask defaultTaskMaskBits = 0b010101010 ;

  enum class ThreadPriority
  {
      clear = 0,
      lowest = 10,
      belowNormal = 20,
      normal = 30,
      aboveNormal = 80,
      highest = 90,
      priorityMax = 255
  } ;

  enum class StackDepth: tU16
  {
      minimal = 128U,
      medium = 256U,
      big = 512U,
      biggest = 1024U
  };
  
  class Thread
  {
    public:
      virtual void Execute() = 0 ;

      inline tTaskHandle GetTaskHanlde() const
      {
        return handle;
      }
      static void Sleep(const tTime timeOut = 1000ms)
      {
        wSleep(timeOut) ;
      };

      inline void Signal(const tTaskEventMask mask = defaultTaskMaskBits)
      {
        wSignal(handle, mask);
      };
      
      inline tTaskEventMask WaitForSignal(tTime timeOut = 1000ms,
                                      const tTaskEventMask mask = defaultTaskMaskBits)
      {
        return wWaitForSignal(mask, timeOut) ;
      }
      friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *);
    private:
      tTaskHandle handle ;
      tTaskContext context ;
  } ;
} ;
#endif // __THREAD_HPP


Начал реализовывать, и тут меня подстерегала первая неприятность, оказывается «любая» операционка по разному вызывает свои функции из прерываний. Например, FreeRTOS имеет специальные реализации функций для исполнения их из прерываний, скажем, если есть функция xTaskNotify(..), то вызывать из прерывания её нельзя, а нужно вызывать xTaskNotifyFromISR(..).
У embOS если вы вызываете любую функцию из прерывания будьте любезны использовать OS_InInterrupt() при входе в прерывание и OS_LeaveInterrupt() при выходе. Пришлось сделать класс InterruptEntry, у которого есть только конструктор и деструктор:

namespace OsWrapper {
  extern void wEnterInterrupt() ;
  extern void wLeaveInterrupt() ;
  
  class InterruptEntry   {
    public:
      inline InterruptEntry()   {
        wEnterInterrupt() ;
      }	  
      inline ~InterruptEntry()  {
        wLeaveInterrupt() ;
      }
  } ;
} ;

использовать его можно так:

void Button::HandleInterrupt() {
  const OsWrapper::InterruptEntry ie;
  EXTI->PR = EXTI_PR_PR13 ;
  myDesiredTask.Signal();   
}

void myDesiredTask::Execute() { 
  while(true)  {
    if (WaitForSignal(100000ms) == defaultTaskMaskBits)    {
        GPIOC->ODR ^= (1 << 5) ;    
    }
  }
} ;

Очевидно, что для FreeRTOS и конструктор и деструктор будут пустые. А для нотификации можно использовать функцию xTaskNotifyFromISR(..), которой без разницы откуда она будет вызвана, небольшие накладные расходы, но что не сделаешь ради универсальности. Можно конечно создать отдельные методы для вызова из прерываний, но пока я решил просто сделать универсально.
Такой же трюк, как с InterruptEntry можно сделать и с критической секцией:

namespace OsWrapper{
  class CriticalSection  {
    public:
      inline CriticalSection()      {
        wEnterCriticalSection() ;
      } 
      
      inline ~CriticalSection()      {
        wLeaveCriticalSection() ;
      } 
  } ;
} ;

Теперь просто добавим реализацию функций с помощью API FreeRtos в файл и запустим проверить, хотя можно было и не запускать, итак понятно, что работать будет :)
rtosFreeRtos.cpp
/*******************************************************************************
* Filename  	: rtosFreeRtos.cpp
* 
* Details   	: This file containce implementation of functions of concrete 
*                 FreeRTOS to support another RTOS create the same file with the
*                 same functions but another name< for example rtosEmbOS.cpp and
*                 implement these functions using EmbOS API.
*
*******************************************************************************/

#include "../thread.hpp"
#include "../mutex.hpp"
#include "../rtos.hpp"
#include "../../../Common/susudefs.hpp"
#include "rtosdefs.hpp"
#include "../event.hpp"

#include <limits>

namespace OsWrapper
{
/*****************************************************************************
 * Function Name: wCreateThread
 * Description: Creates a new task and passes a parameter to the task. The 
 * function should call appropriate RTOS API function to create a task. 
 *
 * Assumptions: RTOS API create task function should get a parameter to pass the
 * paramete to task.      
 * Some RTOS does not use pStack pointer so it should be set to nullptr
 *
 * Parameters: [in] thread - refernce on Thread object
 *             [in] pName - name of task
 *             [in] prior - task priority
 *             [in] stackDepth - size of Stack  
 *             [in] pStack - pointer on task stack
 * Returns: No
 ****************************************************************************/
  void wCreateThread(Thread & thread, const char * pName,
                            ThreadPriority prior, const tU16 stackDepth,
                            tStack *pStack)
  {
#if (configSUPPORT_STATIC_ALLOCATION == 1)
  if (pStack != nullptr)
  {
    thread.handle = xTaskCreateStatic(static_cast<TaskFunction_t>(Rtos::Run),
                                           pName,
                                           stackDepth,
                                           &thread,
                                           static_cast<uint32_t>(prior),
                                           pStack,
                                           &thread.context);
  }
#else
  thread.handle =  (xTaskCreate(static_cast<TaskFunction_t>(Rtos::Run),
                        pName, stackDepth, &thread, static_cast<uint32_t>(prior),
                        &thread.handle) == pdTRUE) ?  thread.handle : nullptr ;
#endif
  }

/*****************************************************************************
 * Function Name: wStart()
 * Description: Starts the RTOS scheduler
 *
 * Assumptions: No
 * Parameters: No
 * Returns: No
 ****************************************************************************/  
  void wStart() 
  {
    vTaskStartScheduler() ;
  }
  
  
/*****************************************************************************
 * Function Name: wHandleSvcInterrupt()
 * Description: Handle of SVC Interrupt. The function should call appropriate 
 * RTOS function to handle the interrupt
 *
 * Assumptions: No
 * Parameters: No
 * Returns: No
 ****************************************************************************/  
  void wHandleSvcInterrupt()
  {
    vPortSVCHandler() ;
  }     
  
/*****************************************************************************
 * Function Name: wHandleSvInterrupt()
 * Description: Handle of SV Interrupt. The function should call appropriate 
 * RTOS function to handle the interrupt
 *
 * Assumptions: No
 * Parameters: No
 * Returns: No
 ****************************************************************************/      
  void wHandleSvInterrupt()
  {
    xPortPendSVHandler() ;
  }   

/*****************************************************************************
 * Function Name: wHandleSysTickInterrupt()
 * Description: Handle of System Timer Interrupt. The function should call 
 *  appropriate RTOS function to handle the interrupt
 *
 * Assumptions: No
 * Parameters: No
 * Returns: No
 ****************************************************************************/  
  void wHandleSysTickInterrupt()
  {
    xPortSysTickHandler() ;
  }

 /*****************************************************************************
 * Function Name: wSleep()
 * Description: Suspends the calling task for a specified period of time, 
 *  or waits actively when called from main()
 *
 * Assumptions: No
 * Parameters: [in] timeOut - specifies the time interval in system ticks
 * Returns: No
 ****************************************************************************/  
  void wSleep(const tTime timeOut)
  {
    vTaskDelay(timeOut) ;
  }
  
  /*****************************************************************************
  * Function Name: wEnterCriticalSection()
  * Description: Basic critical section implementation that works by simply 
  * disabling interrupts
  *
  * Assumptions: No
  * Parameters: No
  * Returns: No
  ****************************************************************************/ 
  void wEnterCriticalSection()
  {
    taskENTER_CRITICAL() ;
  }
  
  /*****************************************************************************
  * Function Name: wLeaveCriticalSection()
  * Description: Leave critical section implementation that works by simply 
  * enabling interrupts
  *
  * Assumptions: No
  * Parameters: No
  * Returns: No
  ****************************************************************************/ 
  void wLeaveCriticalSection()
  {
    taskEXIT_CRITICAL() ;
  }
  
  /****************************************************************************
  * Function Name: wEnterInterrupt()
  * Description: Some RTOS requires to inform the kernel  that interrupt code 
  * is executing 
  *
  * Assumptions: No
  * Parameters: No
  * Returns: No
  ****************************************************************************/
  void wEnterInterrupt() 
  {
    
  }
  
  /****************************************************************************
  * Function Name: wLeaveInterrupt()
  * Description: Some RTOS requires to inform that the end of the interrupt r
  * outine has been reached; executes task switching within ISR 
  *
  * Assumptions: No
  * Parameters: No
  * Returns: No
  ****************************************************************************/
  void wLeaveInterrupt() 
  {
    
  }
  
  /****************************************************************************
  * Function Name: wSignal()
  * Description: Signals event(s) to a specified task
  *
  * Assumptions: No
  * Parameters: [in] taskHandle - Reference to the task structure
  *             [in] mask - The event bit mask containing the event bits, 
  *             which shall be signaled.
  * Returns: No
  ****************************************************************************/
  void wSignal(tTaskHandle const &taskHandle, const tTaskEventMask mask)
  {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE ;
    xTaskNotifyFromISR(taskHandle, mask, eSetBits, &xHigherPriorityTaskWoken) ;
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken ) ;   
  }
  
  /****************************************************************************
  * Function Name: wWaitForSignal()
  * Description: Waits for the specified events for a given time, and clears 
  * the event memory when the function returns
  *
  * Assumptions: No
  * Parameters: [in] mask - The event bit mask containing the event bits, 
  *             which shall be waited for
  *             [in] timeOut - Maximum time in system ticks waiting for events 
  *             to be signaled.
  * Returns: Set bits
  ****************************************************************************/  
  tTaskEventMask wWaitForSignal(const tTaskEventMask mask, tTime timeOut)
  {
    uint32_t ulNotifiedValue = 0U ;
    xTaskNotifyWait( 0U,               
                     std::numeric_limits<uint32_t>::max(),          
                     &ulNotifiedValue, 
                     timeOut);
    return (ulNotifiedValue & mask) ;
  }

/****************************************************************************
  * Function Name: wCreateEvent()
  * Description:  Create an Event object
  *
  * Assumptions: No
  * Parameters: [in] event - reference on tEvent object
  *
  * Returns: Handle of created Event
  ****************************************************************************/
  tEventHandle wCreateEvent(tEvent &event)
  {
#if (configSUPPORT_STATIC_ALLOCATION == 1)
    return xEventGroupCreateStatic(&event);
#else
    return xEventGroupCreate();
#endif
  }

  /****************************************************************************
  * Function Name: wDeleteEvent()
  * Description:  Create an Event object
  *
  * Assumptions: No
  * Parameters: [in] eventHandle - reference on tEventHandle object
  *
  * Returns: No
  ****************************************************************************/ 
  void wDeleteEvent(tEventHandle &eventHandle)
  {
    vEventGroupDelete(eventHandle);
  }
  
  /****************************************************************************
  * Function Name: wSignalEvent()
  * Description:  Sets an  resumes tasks which are waiting at the event object 
  *
  * Assumptions: No
  * Parameters: [in] event - reference on eventHandle object
  *             [in] mask - The event bit mask containing the event bits, 
  *             which shall be signaled
  *
  * Returns: No
  ****************************************************************************/
  void wSignalEvent(tEventHandle const &eventHandle, const tEventBits mask)
  {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xEventGroupSetBitsFromISR(eventHandle, mask, &xHigherPriorityTaskWoken) ;

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken) ;

  }
  
  /****************************************************************************
  * Function Name: wWaitEvent()
  * Description:  Waits for an event and suspends the task for a specified time 
  * or until the event has been signaled. 
  *
  * Assumptions: No
  * Parameters: [in] event - Reference on eventHandle object
  *             [in] mask - The event bit mask containing the event bits, 
  *             which shall be signaled
  *             [in] timeOut - Maximum time in RTOS system ticks until the 
  *             event must be signaled. 
  *             [in] mode - Indicate mask bit behaviour
  *
  * Returns: Set bits
  ****************************************************************************/
  tEventBits wWaitEvent(tEventHandle const &eventHandle, const tEventBits mask,
                        const tTime timeOut, OsWrapper::EventMode mode)
  {
    BaseType_t xWaitForAllBits = pdFALSE ; 
    if (mode == OsWrapper::EventMode::waitAnyBits)
    {
      xWaitForAllBits = pdFALSE;
    }
    return xEventGroupWaitBits(eventHandle, mask, pdTRUE, xWaitForAllBits, timeOut) ;
  }

/****************************************************************************
* Function Name: wCreateMutex()
* Description:  Create an mutex. Mutexes are used for managing resources by
* avoiding conflicts caused by simultaneous use of a resource. The resource
* managed can be of any kind: a part of the program that is not reentrant, a
* piece of hardware like the display, a flash prom that can only be written to
* by a single task at a time, a motor in a CNC control that can only be
* controlled by one task at a time, and a lot more.
*
* Assumptions: No
* Parameters: [in] mutex - Reference on tMutex structure
*             [in] mode - Indicate mask bit behaviour
*
* Returns: Mutex handle
****************************************************************************/
  tMutexHandle wCreateMutex(tMutex &mutex)
  {
#if (configSUPPORT_STATIC_ALLOCATION == 1)
    return xSemaphoreCreateMutexStatic(&mutex) ;
#else
    return xSemaphoreCreateMutex();
#endif
  }

/****************************************************************************
* Function Name: wDeleteMutex()
* Description:  Delete the mutex.
*
* Assumptions: No
* Parameters: [in] mutex - handle of mutex
*
* Returns: Mutex handle
****************************************************************************/
  void wDeleteMutex(tMutexHandle &handle)
  {
    vSemaphoreDelete(handle) ;
  }

/****************************************************************************
* Function Name: wLockMutex()
* Description:  Claim the resource
*
* Assumptions: No
* Parameters: [in] handle - handle of mutex
*             [in] timeOut - Maximum time until the mutex should be available
*
* Returns: true if resource has been claimed, false if timeout is expired
****************************************************************************/
  bool wLockMutex(tMutexHandle const &handle, tTime timeOut)
  {
    return static_cast<bool>(xSemaphoreTake(handle, timeOut)) ;
  }

/****************************************************************************
* Function Name: wUnLockMutex()
* Description:  Releases a mutex currently in use by a task
*
* Assumptions: No
* Parameters: [in] handle - handle of mutex
*
* Returns: No
****************************************************************************/
  void wUnLockMutex(tMutexHandle const &handle)
  {
    BaseType_t  xHigherPriorityTaskWoken = pdFALSE ;
    xSemaphoreGiveFromISR(handle, &xHigherPriorityTaskWoken) ;

    portYIELD_FROM_ISR( xHigherPriorityTaskWoken ) ;
  }

/****************************************************************************
* Function Name: wSleepUntil()
* Description:  Suspends the calling task until a specified time, or waits
* actively when called from main()
*
* Assumptions: No
* Parameters: [in] last - Refence to a variable that holds the time at which
*             the task was last unblocked. The variable must be initialised
*             with the current time prior to its first use
*             [in] timeOut - Time to delay until, the task will be unblocked
*             at time
*
* Returns: No
****************************************************************************/
  void wSleepUntil(tTime & last, const tTime timeOut)
  {
    vTaskDelayUntil( &last, timeOut) ;
  }

/****************************************************************************
* Function Name: wGetTicks()
* Description:  Returns the current system time in ticks as a native integer
* value
*
* Assumptions: No
* Parameters:  No
*
* Returns: Current system time in ticks
****************************************************************************/
  tTime wGetTicks()
  {
    return xTaskGetTickCount();
  }
}


image

Продолжаем дорабатывать задачу


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

void MyTask::Execute() {
    while(true)  {
       DoCalculation(); //It takes about 10ms
       Led1.Toggle() ;
       Sleep(100ms) ; 
   }
}

Этот код будет моргать светодиодом раз в 110 ms. Но вы же хотите раз в 100ms, можно примерно посчитать время расчета и поставить Sleep(90ms). Но что если время расчета зависит от входных параметров, то моргание будет вообще не детерминированным. Для таких вот случаев во «всех» операционках существуют специальные методы, типа DelayUntil(). Работает он по такому принципу — вначале нужно запомнить текущее значение счетчика тиков операционной системы, затем прибавить к этому значению количество тиков на которое нужно приостановить выполнение задачи, как только счетчик тиков доберется до этого значения, задача разблокируется. Таким образом задача будет заблокирована ровно на то значение которое вы задали и ваш светодиод будет моргать ровно каждые 100ms не зависимо от продолжительности расчета.
Этот механизм по разному реализован в разных операционках, но алгоритм у него один. В итоге механизм, скажем, реализованный на FreeRTOS, будет упрощен до состояния, показанного на следующей картинке:

image

Как видно считывание начального состояния счетчика Тиков операционки происходи до входа в бесконечный цикл, и нам нужно что-то придумать, чтобы это реализовать. На помощь приходит шаблон проектирования Шаблонный метод. Реализуется он очень просто, нам просто нужно добавить еще один не виртуальный метод, где вызвать вначале метод, считывающий и запоминающий счетчик Тиков операционки, а затем вызвать виртуальный метод Execute(), который будет реализован в потомке, т.е. в вашей реализации задачи. Так как нам не нужно, чтобы этот метод торчал наружу для пользователя (он ведь просто вспомогательный), то скроем его в приватной секции.

 class Thread  {
    public:
      virtual void Execute() = 0 ;
      friend class Rtos ;
   private:
      void Run()      {
        lastWakeTime = wGetTicks() ;
        Execute();
      } ...       
      tTime lastWakeTime = 0ms ;
...
}       

Соответственно в статическом методе Run класса Rtos, нужно будет теперь вызвать не Execute(), а метод Run() объекта Thread. Мы как раз сделали класс Rtos дружественным, чтобы получить доступ к приватному методу Run() в классе Thread.

static void Run(void *pContext )      {
        static_cast<Thread*>(pContext)->Run() ;
      }

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

thread.hpp
/*******************************************************************************
* Filename  	: thread.hpp
* 
* Details   	: Base class for any Taskis which contains the pure virtual 
* method Execute().  Any active classes which will have a method for running as 
* a task of RTOS should inherit the Thread and override the Execute() method. 
* For example:
*            class MyTask : public OsWrapper::Thread
*            {
*            public:
*               virtual void Execute() override { 
*                 while(true) {
*                    //do something..
*                 }  
*            } ;
*
* Author        : Sergey Kolody
*******************************************************************************/
#ifndef __THREAD_HPP
#define __THREAD_HPP  

#include "FreeRtos/rtosdefs.hpp"
#include "../../Common/susudefs.hpp"

namespace OsWrapper 
{
  extern void wSleep(const tTime) ;
  extern void wSleepUntil(tTime &, const tTime) ;
  extern tTime wGetTicks() ;
  extern void wSignal(tTaskHandle const &, const tTaskEventMask) ;
  extern tTaskEventMask wWaitForSignal(const tTaskEventMask, tTime) ;
  constexpr tTaskEventMask defaultTaskMaskBits = 0b010101010 ;

  enum class ThreadPriority
  {
      clear = 0,
      lowest = 10,
      belowNormal = 20,
      normal = 30,
      aboveNormal = 80,
      highest = 90,
      priorityMax = 255
  } ;

  enum class StackDepth: tU16
  {
      minimal = 128U,
      medium = 256U,
      big = 512U,
      biggest = 1024U
  };
  
  class Thread
  {
    public:
      virtual void Execute() = 0 ;

      inline tTaskHandle GetTaskHanlde() const
      {
        return handle;
      }
      static void Sleep(const tTime timeOut = 1000ms)
      {
        wSleep(timeOut) ;
      };

      void SleepUntil(const tTime timeOut = 1000ms)
      {
        wSleepUntil(lastWakeTime, timeOut);
      };

      inline void Signal(const tTaskEventMask mask = defaultTaskMaskBits)
      {
        wSignal(handle, mask);
      };
      
      inline tTaskEventMask WaitForSignal(tTime timeOut = 1000ms,
                                      const tTaskEventMask mask = defaultTaskMaskBits)
      {
        return wWaitForSignal(mask, timeOut) ;
      }
      friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *);
      friend class Rtos ;
    private:
      tTaskHandle handle ;
      tTaskContext context ;
      tTime lastWakeTime = 0ms ;
      void Run()
      {
        lastWakeTime = wGetTicks() ;
        Execute();
      }
  } ;
} ;
#endif // __THREAD_HPP



rtos.hpp
/*******************************************************************************
* Filename  	: Rtos.hpp
* 
* Details   	: Rtos class is used to create tasks, work with special Rtos 
* functions and also it contains a special static method Run. In this method 
* the pointer on Thread should be pass. This method is input point as 
* the task of Rtos. In the body of the method, the method of concrete Thread 
* will run. 
*******************************************************************************/

#ifndef __RTOS_HPP
#define __RTOS_HPP

#include "thread.hpp"        // for Thread
#include "../../Common/susudefs.hpp"
#include "FreeRtos/rtosdefs.hpp"

namespace OsWrapper 
{
 
  extern void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *) ;
  extern void wStart() ;
  extern void wHandleSvcInterrupt() ;
  extern void wHandleSvInterrupt() ;
  extern void wHandleSysTickInterrupt() ;  
  extern void wEnterCriticalSection();
  extern void wLeaveCriticalSection();

  
  class Rtos
  {
    public:    
      
      static void CreateThread(Thread &thread ,
                               tStack * pStack = nullptr,
                               const char * pName = nullptr,
                               ThreadPriority prior = ThreadPriority::normal,
                               const tU16 stackDepth = static_cast<tU16>(StackDepth::minimal)) ;
      static void Start() ;       
      static void HandleSvcInterrupt() ;
      static void HandleSvInterrupt() ;
      static void HandleSysTickInterrupt() ;
      
      friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *);
      friend  class Thread ;
    private:
      //cstat !MISRAC++2008-7-1-2 To prevent reinterpet_cast in the CreateTask
      static void Run(void *pContext ) 
      {
        static_cast<Thread*>(pContext)->Run() ;
      }
  } ;
} ;
#endif // __RTOS_HPP



События


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

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



Использовать это можно так:

OsWrapper::Event event{10000ms, 3}; // создаем событие, время ожидания события 10000ms,  устанавливать биты номер 0 и бит номер 1.

void SomeTask::Execute() { 
    while(true)  {
      using OsWrapper::operator""ms ;
      Sleep(1000ms);
      event.Signal() ; // Посылаем событие с установленным битом 0 и битом 1.
      Sleep(1000ms);
      event.SetMaskBits(4) //Теперь устанавливаем только бит 2.
      event.Signal() ; // Посылаем событие с установленным битом 2.
     }
  } ;

void AnotherTask::Execute() { 
  while(true)  {
    using namespace::OsWrapper ;
      //Проверяем, что событие сработало не по таймауту, таймаут если что 10000ms
      if ((event.Wait() & defaultTaskMaskBits) != 0)   {
        GPIOC->ODR ^= (1 << 5) ;
      }
    }  
} ;


Мьютекс, Семафоры и Очереди


Изначально при написании статьи я их еще не реализовал, но как и обещал разработал мьютексы и майл боксы, исходники лежит тут: GitHub OsWrapper. Пример использования майлбокса, выглядит следующим образом:
OsWrapper::MailBox<tU32, 10> queue; // объявление очереди на 10 элементов типа int

void ReceiveTask::Execute() { 
  tU32 item;
  while(true)  {
    using OsWrapper::operator""ms ;    
    if (queue.Get(item, 10000ms)) { //ожидаем элемента из очереди
      GPIOC->ODR ^= (1 << 9);
    }
  }
} ;

void SendTask::Execute() { 
   tU32 item = 0U;
    while(true)   {
      queue.Put(item);
      item ++;
      SleepUntil(1000ms);
     }
  } ;


Как всем этим делом пользоваться


Основа сделана, чтобы понять как всем этим можно пользоваться, я привожу небольшой кусок кода, который делает следующее: Задача LedTask, моргает раз в ровно 2 секунды светодиодом, и каждые же 2 секунды посылает сигнал задаче myTask, которая ждет 10 секунд события, как только событие пришло, она моргает другим светодиодом. В общем в итоге два светодиода моргают раз в 2 секунды. Я не стал напрямую нотифицировать задачу, а сделал это через event. Неправда ли изысканное решение для того, чтобы моргнуть двумя светодиодами :)

using OsWrapper::operator""ms ;
OsWrapper::Event event{10000ms, 1};

class MyTask : public OsWrapper::Thread {
public:
  virtual void Execute() override {
    while(true) {    
      if (event.Wait() != 0) {
        GPIOC->ODR ^= (1 << 9);    
      }
    }
  }
  using tMyTaskStack = std::array<OsWrapper::tStack, 
                                   static_cast<tU16>(OsWrapper::StackDepth::minimal)> ;
  inline static tMyTaskStack Stack; //C++17 фишка в IAR 8.30  
} ;

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)> ;
  inline static tLedStack Stack; //C++17 фишка в IAR 8.30
} ;

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;
}


Заключение


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

Но а пока большинство из нас использует традиционные Сишные операционки, можете использовать обертку, как начальный старт к переходу к счастливому будущему с С++ :)

Я собрал небольшой тестовый проект в Clion. Пришлось повозиться с его настройками, он все еще не совсем предназначен для разработки ПО под микроконтроллеры, и почти не дружит с IAR toolchain, но все же, получилось откомпилировать, отлинковать в elf формат, преобразовать в hex формат, прошить, и запустить отладку с помощью GDB. И это того стоило — просто превосходная среда, и ошибки на ходу правит, и если надо поменять сигнатуру метода, то рефакторинг в 2 секунды, и вообще думать уже не надо, она сама скажет, что где должно быть, как лучше сделать или назвать параметр. У меня даже сложилось впечатление, что обертку написала сама Clion. В общем когда все баги, связанные с IAR toolchain пофиксят, можно брать.

Но по старинке проект для IAR я все таки создал для версии 8.30.1, на нем же проверил как все это работает. Использовал следующее оборудование: XNUCLEO-F411RE, отладчик ST-Link. И все же, еще раз, посмотрите как отладка выглядит в Clion — ну симпатично же, но пока глючно :)

image

Проект на IAR вы можете взять тут: Проект на IAR 8.30.1 Пока это неполная версия, без очередей и семафоров, более полную я положу в github, когда руки дойдут, начинается пора учебы и времени будет катастрофически мало, Но я думаю что уже эту можно использовать для небольших проектов в связке с FreeRtos.

З.Ы. Как и обещал ссылка на GitHub Проект обертки
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 35: ↑34 и ↓1+33
Комментарии35

Публикации

Истории

Работа

Программист C++
126 вакансий
QT разработчик
8 вакансий

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань