C ++ Wrapper for all Real-Time Operating Systems for CortexM4 (Part 2)
Check out this second installment on how to refine tasks in the C++ wrapper for real-time operating systems on the CortexM4. Click here for more!
Join the DZone community and get the full member experience.
Join For FreeIn our last installment, we looked at everything we needed to get started with real-time operating systems. This post will work on refining that project and building on some of the code we already implemented. Let's get into it!
Continuing to Refine the Task
The task now has almost everything you need. We need to add the method Sleep ()
. This method suspends the execution of the task at a specified time. In most cases, this is enough, but if you need a clearly deterministic time, then Sleep ()
can bring you problems. For example, you want to do some calculation and blink the LED and do it exactly every 100 ms.
void MyTask::Execute() {
while(true) {
DoCalculation(); //It takes about 10ms
Led1.Toggle() ;
Sleep(100ms) ;
}
}
This code will blink the LED once every 110 ms. But, you want to fold once in 100ms. You can roughly calculate the calculation time and put Sleep (90ms)
. But, if the calculation time depends on the input parameters, then the blinking will not be deterministic at all. For such cases, there are special methods in "all" operating systems, such as DelayUntil ()
. It works by this principle. First, you need to remember the current value of the operating system tick counter. Then, to add to this value and the number of ticks that you want to pause the task, as soon as the tick counter reaches this value, the task is unlocked. Thus, the task will be locked exactly to the value that you set, and your LED will blink exactly every 100ms — regardless of the duration of the calculation.
This mechanism is implemented differently in different operating systems, but it has one algorithm. As a result, the mechanism, say, implemented on FreeRTOS, will be simplified to the state shown in the following picture:
As you can see, the readout of the initial state of the operating system counter tickers occurs before entering the infinite loop, and we need to figure something out to implement it. Help comes from the template design, Template
method. It is very easy to implement; we just need to add another non-virtual method, where we first call the method that reads and stores the operating system tick counter and then calls the virtual Execute ()
method that will be implemented in the child, i.e. in your implementation of the task. Since we do not need this method to stick out for the user (it's just a helper), then we'll hide it in the private section.
class Thread {
public:
virtual void Execute() = 0 ;
friend class Rtos ;
private:
void Run() {
lastWakeTime = wGetTicks() ;
Execute();
} ...
tTime lastWakeTime = 0ms ;
...
}
Accordingly, in the static Run
method of the RTOS class, you will now need to call the Execute ()
method, but the Run ()
method of the Thread object. We just made the RTOS class friendly to access the private Run ()
method in the Thread class.
static void Run(void *pContext ) {
static_cast<Thread*>(pContext)->Run() ;
}
The only restriction for the SleepUntil ()
method is that it cannot be used in conjunction with other methods that block the task. Alternatively, to solve the problem of working in conjunction with other methods blocking the task, you can dub the method of updating the memorized ticks of the system and call it before SleepUntil ()
. But, for now, just keep this nuance in mind. The extreme version of the classes appear in the following picture:
/*******************************************************************************
* 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
/*******************************************************************************
* 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
Developments
So, once the task is created, it can be sent to an event. But, you want to implement an event that cannot be sent to a specific task. But, to any subscriber who decides to wait for this event, roughly speaking, we need to implement a wrapper over the Event
.
In general, the mechanism of events assumes very many options. You can send the event setting bits, and some tasks can wait for the installation of one bit, while others can install others. You can expect all of them at once. However, you cannot clear bits after receiving an event or options, but in my work, it is necessary to send and receive the event and discard all the bits. However, we still need to offer a simple interface to support additional functionality. The structure of the event is similar to the tasks. They also have a certain context that needs to be stored and the identifier. Also, I wanted the event to be able to adjust the waiting time and the mask, so I added two additional private fields.
You can use it like this:
OsWrapper :: Event event {10000ms, 3}; // create an event, wait for the event 10000ms, set bits number 0 and bit number 1.
void SomeTask :: Execute () {
while (true) {
using OsWrapper :: operator "" ms;
Sleep (1000ms);
event.Signal (); // Send the event with bit 0 and bit 1 set.
Sleep (1000ms);
event.SetMaskBits (4) // Now set bit 2 only.
event.Signal (); // Send the event with bit 2 set.
}
};};
void AnotherTask :: Execute () {
while (true) {
using namespace :: OsWrapper;
// We check that the event did not work according to the timeout, the timeout if that is 10000ms
if ((event.Wait () & defaultTaskMaskBits)! = 0) {
GPIOC-> ODR ^ = (1 << 5);
}
}
};};
Mutex, Semaphores, and Queues
And, I have not implemented them yet, or rather, the mutexes have already been done. But, I have not checked. The queues are waiting for their turn. I hope to finish it in the near future.
How Can We All Use This?
The basis is made to understand how all this can be used. I bring a small piece of code that does the following — the LedTask
task blinks once in exactly two seconds with the LED, and every two seconds, it sends a signal to the task myTask
, which waits 10 seconds for the event. As soon as the event has come, she blinks another LED. In general, as a result, two LEDs blink once every two seconds. I did not directly notify the task, but I did it via an event. Unfortunately, it is not a clever solution to blink two LEDs.
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;
}
Conclusion
I'll venture to give my subjective view of the future of firmware for microcontrollers. I believe that the time will come for C ++ where there will be more and more operating systems providing the C ++ interface. Manufacturers already need to rewrite or wrap everything in C ++.mFrom this point of view, I would recommend using an RTOS. For example, the above-mentioned MAX RTOS can save you so much time — you can not even imagine— and there are still such unique chips, for example, running on different microcontrollers. If it had a security certificate, it would be better to find a different solution.
But, in the meantime, most of us use traditional Sisnye OSes. You can use the wrapper as an initial start to your transition to a happy future with C ++ :)
I assembled a small test project in Clion. I had to tinker with its settings; it is still not entirely intended for developing software for microcontrollers and is almost not friendly with the IAR toolchain. But still, it turned out to compile, link to elf format, converts to hex format, flash, and start debugging with GDB. And, it was worth it — it is just an excellent environment and corrects mistakes on the fly. If you need to change the signature of the method, then refactoring will occur in two seconds. In general, you don't even need to think — it will say where it should be and is best for making or naming the parameter. I even got the impression that the wrapper was written by Clion herself. In general, it contains all the bugs associated with the IAR toolchain that you can take.
But, in the old-fashioned project for IAR, I still created the version 8.30.1. I also checked out how it all works using the following equipment: XNUCLEO-F411RE, ST-Link debugger. And, yet, once again, look at how debugging looks in Clion — well, it's pretty, but so far it's buggy:
You can take the IAR project here: IAR project 8.30.1. While this is an incomplete version, without queues and semaphores, I will provide a more complete version in GitHub whenever I can. But, I think that this one can already be used for small projects in conjunction with FreeRtos. Happy coding!
Opinions expressed by DZone contributors are their own.
Comments