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


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


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

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


Основа сделана, чтобы понять как всем этим можно пользоваться, я привожу небольшой кусок кода, который делает следующее: Задача 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.

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


  1. NordicEnergy
    21.08.2018 18:34

    Сейчас прибегут разные интересные личности и заклюют вас за отказ от макросов)) Без простыни дефайнов ну никак жить нельзя, это быстро и классно, а тут бац и отказ от них…

    Интересен момент по ide. Для Clion вы использовали плагин (про него на хабре как-то статья была) или собирали тулчейн + openOCD/GDB?


    1. ArbeitMachtFrei
      21.08.2018 19:54

      Ну, за отказ от макросов поклевать немного и можно :) В Contiki OS, не к ночи будь помянута, многозадачность построена на «протопотоках» (protothreads), очень забавной конструкции на чистых макросах, подобный же механизм используется в «со-процедурах» (coroutines) FreeRTOS. Сомневаюсь, что это можно обернуть в C++.


      1. 0xd34df00d
        22.08.2018 02:31

        Корутины в C++20 запиливают, например.


        1. ArbeitMachtFrei
          22.08.2018 07:20

          Ага, только на первый взгляд встроенные в язык coroutines мало чем отличаются от реализации их на макросах. Ну и если честно — как-то боязно использовать новые фичи C++ с компиляторами типа того же IAR.


          1. mayorovp
            22.08.2018 14:09

            Реализация на макросах не огромный ли switch внутрях использует? Если так, то у встроенных в язык сопрограмм есть два преимущества: в них можно использовать switch и в них нет логики по декодированию адреса инструкции из номера состояния.


    1. lamerok Автор
      21.08.2018 20:00
      +1

      По поводу Clion, да собирал тулчейн, + OpenOCD/GDB, плагином не пользовался. У меня STLink китайский через openOCD не прошивается, ресет не выведен на микронтроллер, приходилось при прошивке кнопку ресет рукой нажимать, поэтому плагин не подошёл, я подключился к STLink-Utility, и она после сборки и конвертации в двоичный файл прошивает, а потом уже запускается OpenOCD, чисто на отладку.


      1. anonymous
        22.08.2018 06:03

        я для этого openocd запускал с дополнительным флагом -c “reset_config none separate”, чтобы работать без выведенного ресета. тогда идет сброс через swd


        1. lamerok Автор
          22.08.2018 07:32

          О, спасибо, надо попробовать.


    1. fallenworld
      22.08.2018 10:33

      Без простыни дефайнов ну никак жить нельзя


      Есть задачи, которые не решаются без макросов(некоторые пока что)
      Пара примеров:
      * Q_OBJECT
      * SCOPED_EXIT
      * библиотека nan — там можно без макросов, но какой смысл
      * включение различных участков кода
      * поключение различных заголовочных файлов


    1. Konachan700
      22.08.2018 11:10

      А как без дефайнов-то? Прямо так циферками все адреса регистров и константы писать? Как жить без #ifdef, которым можно вырезать тяжелые, объемные, но нужные только для разработки/дебага участки?
      В остальном да, длинные макросы зло, ибо их почти невозможно дебажить.


  1. AntonSazonov
    21.08.2018 20:00

    У вас в исходниках thread.hpp опечаточка закралась: GetTaskHanlde


    1. lamerok Автор
      21.08.2018 20:01

      Спасибо, удалить надо этот метод вообще, не нужен он.


  1. Nick_Shl
    21.08.2018 20:12

    Какая проблема с "дуализмом" функций? Если бы вы пользовались CubeMX и включили в нем FreeRTOS, то заметили бы, что он генерирует ещё и обёртку cmsis_rtos.c в которой уже дуализма нет. Внутри функции обертки делается анализ регистра и далее вызывается соответствующая функция. Проще всего(для последующей разработки с использованием обертки) этот код утащить в свою обёртку.


  1. vasimv
    22.08.2018 00:53

    От операционной системы ожидаешь хоть какой-то изоляции от аппаратуры. А тут сразу «EXTI->PR = EXTI_PR_PR12;»… Наверное, стоило бы написать, что freertos занимается исключительно многозадачностью (треды, шедулинг, семафоры и т.д.), далеко не всем тем, чем обычные операционные системы занимаются.


    1. olartamonov
      22.08.2018 06:45

      Конкретно с FreeRTOS вечная путаница из-за того, что это не операционная система, а только ядро операционной системы, о чём чёрным по белому прямо в заголовке её сайта и сказано. Всё прочее там — обвес на усмотрение пользователя.

      А ядро — это действительно треды, шедулер и IPC. Остальное не его забота.


      1. Ryppka
        22.08.2018 09:24

        Перефразируя агента Смита: «Мистер Андерсен, зачем Вам IPC, если у Вас нет процессов?».


    1. lamerok Автор
      22.08.2018 10:38

      Да это ядро, которое занимается многозадачностью, но как бы это и подразумевает под собой определение ОСРВ, по поводу прерываний и флагов, да в жизни так не используется. Есть промежуточный класс InterruptController, который все эти флаги скидывает, а потом вызывает обработчик конкртеного объекта, можно его сделать на основе механизма подписки, в итоге обработчик прерывания скажем кнопки из этого примера выглядел бы просто как:

      void Button::HandleInterrupt() {       
               myTask.Signal();   
      }

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


  1. olartamonov
    22.08.2018 06:47

    Для Cortex M4 «все» операционки используют 3 прерывания, прерывания System tick timer, System Service call via SWI instruction, Pendable request for system service, которые собственно в основном для ОСРВ и были придуманы


    Есть нюанс. Tickless-системы — а это направление модное и правильное, вон Mbed сделали tickless с полгода назад — SysTick не используют, т.к. им by design не нужен регулярно тикающий таймер. За счёт этого у них ниже энергопотребление.


    1. Dima_Sharihin
      22.08.2018 08:02

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


      1. ArbeitMachtFrei
        22.08.2018 11:01

        SysTick может не работать в режимах типа deep sleep; если же он все же работает — то относительно часто (при тактовой частоте всего 16 МГц — раз в секунду) генерирует прерывание и будит ядро.


  1. bugdesigner
    22.08.2018 07:39

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


    1. lamerok Автор
      22.08.2018 10:28

      Спасибо, по поводу CMSIS-RTOS, я почитаю, чего-то CMSIS в виду спицифики работы, а именно опять же надежных применений нам запрещено использовать, я не использовал никогда, только заголовочник для упрощения доступа к регистрам модулей. А против динамического выделения памяти, вприципе я ничего против не имеею, иногда это упрощает жизнь, но опять же потому как SIL3 не рекомендует использовать динамическое выделение памяти приходится следовать эти рекомендациям.


  1. esaulenka
    22.08.2018 10:30

    Занятно, что список требований к ОС («никаких макросов», «никаких приведений типов») сильно совпадает с моими личными претензиями к FreeRTOS.
    И все эти наслоения макросов из кишков фриртос никуда не деваются. И если использование операционки с этой прослойкой, действительно, должно упроститься, при отладке, если туда понадобится-таки лезть, мы опять упрёмся в void*, который надо привести к нужному типу, достать оттуда ещё один указатель, ещё раз привести…


    1. bugdesigner
      22.08.2018 10:49

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


      1. esaulenka
        22.08.2018 11:34

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

        В общем, моё мнение — эту обёртку можно использовать в каких-то особо странных случаях («мне нужно только freertos, потому что у неё сертификат, но самой freertos я видеть не хочу»). Иначе, если не хочется видеть фриртос — ей можно просто не пользоваться, благо альтернативы есть…


        1. bugdesigner
          22.08.2018 11:47

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


          1. esaulenka
            22.08.2018 11:56

            «Работать в команде» и «разные архитектуры» — это не про то. Командно переносить проект между архитектурами можно вместе с операционкой.
            Тут про «переносить код на разные ос». И сейчас оно больше напоминает картинку «есть 14 конкурирующих стандартов»…


  1. esaulenka
    22.08.2018 11:39

    сначала мы использовали одну операционную систему, потом я для своих нужд начал использовать FreeRTOS, потом мы перешли на другую

    А можно раскрыть тему: ЗАЧЕМ?!


    1. lamerok Автор
      22.08.2018 14:25

      Да, можно потому что вначале пользовали операционку, которая довольная старая, но очень популярная, после того, как мы наткнулись на 1001 баг в ней, мы решили от неё отказаться и перейти на более надежную с сертификатом надежности. FreeRtos я использую для обучения студентов. Сейчас договариваемся по использованию МАКС ОСРВ в университете.


  1. esaulenka
    22.08.2018 13:00

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

    Для работы используются два — для переключения задач (обычно — PendSV_Handler) и для системного таймера (и тот только в обычном, не tickless, режиме).
    SWI + SVC_Handler — это такой фриртосный костылик для запуска самой первой задачи.


  1. alexismailov
    23.08.2018 12:41

    Статья хорошая, но есть моменты которые врятли смогут претендовать на долгосрочную перспективу:
    1. Зачем создавать враппер над «тредами», «семафорами», «мьютексами» и тп. если это уже было сделано в boost а также в стандартной библиотеке начиная с C++11.
    (для использования достаточно создать POSIX совместимый враппер на С)
    2. Любое упоминание об отказе от чего либо это всегда холивар. Вот выше у вас написано что вы не любите приведение типов и тут же приводите код который ими пестрит.
    (С привидениями нет проблем если соблюдать правила описанные в соответсвующем стандарте)
    3. Без динамической памяти жить в современном мире разработки очень и очень трудно. Например банальный паттерн PIMPL без ухищрений. Если необходимо уйти от фрагментации можно использовать memory pool, а также placement new.
    Как жить без std::string и std::vector?
    В любом случае идея написать враппер очень правильная и нужная. Сам писал в свое время(свой клас тред с шаблонным параметром размера стека, мьютексы и тп). Посмотрите в сторону использования библиотеки boost она не такая страшная как ее обычно рисуют в мире embedded.


    1. lamerok Автор
      23.08.2018 13:08

      1, Потому что IAR не совсем поддерживает thread (With C11 enabled, the IAR C/C++ Compiler for Arm can compile all C11 source code files, except for those that depend on thread-related system header files. The IAR C/C++ compiler does not support source code that depends on thread-related system headers.), и вообще микропроцессор не всегда имеет 512 кБайт ПЗУ и 64 кб ОЗУ, бывает и 32 кБ ПЗУ и 8 кБ ОЗУ — это именно для этих целей. Если вы выпускаете 100 000 датчиков в год, то 30 центов сэкономленных на процессоре, это 30 000 долларов, а если доллар уже 100 000. Представляете потери из-за того, что я решил выпендриться и использовать навороченную универсальную библиотеку или вектор?
      2. Написано, что не отказ, а по возможности, это не моя прихоть, а требования стандартов надежности, да приведение типов есть, но они все из-за того, что по другому никак. Но избегать надо.
      3. Вектор помогает, согласен, но жить без него можно :) также как и без стринга, когда у вас ограничения в ОЗУ. Boost хорошая библиоткеа, но половина из неё не будет работать с IAR, как не работает полностью atomic.
      Мое мнение что мир emdedded разделен все таки на три части — 1 — это те кто делает, что-то на основе, чего-то типа Linux, вторые это те кто юзает большие микроконтроллеры с 1 МБ ПЗУ и 512 ОЗУ и третьи это те кто юзает небольшие до 256 кБайт ПЗУ и 32 кБ ОЗУ микро для датчиков — вот, я думаю, что как раз моя статься ориентирована на них, чтобы продвигать переход на С++. А так я полностью согласен с вами, чем больше уже готовых решений принимать, тем лучше, но все таки специфика есть везде…


      1. vasimv
        23.08.2018 22:25

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


    1. esaulenka
      24.08.2018 09:43
      +1

      враппер над «тредами», «семафорами», «мьютексами» и тп. если это уже было сделано в boost а также в стандартной библиотеке начиная с C++11

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

      С привидениями нет проблем

      Да нету никаких проблем с приведениями типов. Если никогда не ошибаться. А если в макрос, внутри которого cast на cast'е, передать что-то не то, компилятор это тихо сожрёт без каких-то варнингов.

      Как жить без std::string и std::vector?

      Продумать и описать протоколы, по которым идёт взаимодействие с внешним миром, статически выделить необходимые буферы (хоть сишные, хоть std::array), и жить…

      Посмотрите в сторону использования библиотеки boost

      В описываемом случае (контроллер относительно мелкий, malloc по каким-то соображениям не используется) лучше смотреть куда-то в район etlcpp.com. Бустом, правда, я никогда не пользовался — посмотрел, испугался, и закрыл.