📜 ⬆️ ⬇️

Overview of one Russian RTOS, part 3. Structure of the simplest program

I continue to publish a series of articles from the Book of Knowledge of the MAKS RTOS. This is an informal programmer’s guide for those who prefer a living language to a dry language of documentation.

In this part it is time to put the theory on the real code. Let us consider how everything said earlier is written in C ++ (it is he who is the main one for developing programs for the MAKS RTOS). Here we will talk only about the minimum necessary things, without which no program is possible.

Content (published and unpublished articles):
')
Part 1. General information
Part 2. Core MAX MAX
Part 3. The structure of the simplest program (this article)
Part 4. Useful theory
Part 5. The first application
Part 6. Thread synchronization tools
Part 7. Means of data exchange between tasks
Part 8. Work with interruptions

Code


Since the RTOS MAX has an object-oriented model, the program must also contain classes. In this case, the base classes are already included in the OS, the application programmer only has to create heirs from them and add the required functionality.

To implement an application, you should make an inheritor from the Application class (be sure to overlap the virtual function Initialize () in it) and one or several heirs of the Task class (necessarily overlap in it the virtual function Execute ()). And all this will be managed by the scheduler implemented in the Scheduler class.

image

Fig. 1. Minimum classes required for work (gray - already available, white - should be added)

Application class


At first glance, the class seems completely unnecessary layer. It is necessary to override the Initialize () method, in which the application is initialized. It is convenient to create tasks inside this function (although this is not a dogma, tasks can be created anywhere, just inside this function is most convenient).

void VPortUSBApp::Initialize() { Task::Add(vport = new VPortUSBTask, Task::PriorityNormal, Task::ModeUnprivileged, 400); Task::Add(new HelloTask, Task::PriorityNormal, Task::ModeUnprivileged, 400); } 

It would seem, why it is impossible to initialize the application in the function main (), and this class to throw out, as extra? But let's not rush. Firstly, this function is always called in the privileged mode, so it can be configured hardware, including programming NVIC, which can not be done in normal mode. In addition, this class performs much more functions than just initializing an application.

First of all, it is through the object of this class that the OS finds the application. I wanted to write that “it finds an application without global variables”, but if you go into the chicking, then the static variable member of the Application class

 static Application * m_app; 

still is global. But how is it in the classics: "Samos is a son of a bitch, but this is our son of a bitch." The variable is global, but it is well structured and belongs to the application class. Accordingly, her name is isolated from all other classes. There is a function to access it.

 inline Application & App() { return * Application::m_app; } 

which can be overridden to return not the original, but the inherited class type. For example, one of the tests describes a class with the following overlap:

 class AlarmMngApp : public Application { ... public: ... static inline AlarmMngApp & App() { return * (AlarmMngApp *) m_app; } 

Thus, this class contains the functionality by which the application can always find the thread pulling which it will come to the desired part. This can be useful, for example, in interrupt handlers.

The next obvious thing when using the Application class is its constructor. In the constructor type of multitasking is transferred.

  /// @brief   /// @param use_preemption   /// (true - , false - ). Application(bool use_preemption = true); 

The Application class contains the OnAlarm () virtual function. It will be called to report exceptions. Their list is quite large:

 AR_NMI_RAISED, ///<    (Non Maskable Interrupt, NMI) AR_HARD_FAULT, ///<   (  Hard Fault) AR_MEMORY_FAULT, ///<      (MemManage interrupt) AR_NOT_IN_PRIVILEGED, ///<        ... 

Expand
 AR_NMI_RAISED, ///<    (Non Maskable Interrupt, NMI) AR_HARD_FAULT, ///<   (  Hard Fault) AR_MEMORY_FAULT, ///<      (MemManage interrupt) AR_NOT_IN_PRIVILEGED, ///<         AR_BAD_SVC_NUMBER, ///<    SVC     AR_COUNTER_OVERFLOW, ///<    AR_STACK_CORRUPTED, ///<       AR_STACK_OVERFLOW, ///<     AR_STACK_UNDERFLOW, ///<       AR_SCHED_NOT_ON_PAUSE, ///<          AR_MEM_LOCKED, ///<    AR_USER_REQUEST, ///<   AR_ASSERT_FAILED, ///<   ASSERT   AR_STACK_ENLARGED, ///<       AR_OUT_OF_MEMORY, ///<  ""  AR_SPRINTF_TRUNC, ///<   sprintf   -     AR_DOUBLE_PRN_FMT, ///<     PrnFmt       AR_NESTED_MUTEX_LOCK, ///<            AR_OWNED_MUTEX_DESTR, ///< ,    ,  AR_BLOCKING_MUTEX_DESTR, ///< ,     ,  AR_NO_GRAPH_GUARD, ///<       GraphGuard AR_UNKNOWN ///<   


Having blocked the function, it is possible to provide error handling (or emergency shutdown of the equipment so that it does not fail). The following values ​​are defined for the result of the function:

 AA_CONTINUE, ///<    AA_RESTART_TASK, ///<     AA_KILL_TASK, ///<    ,   AA_CRASH ///<   

Next, consider the Run () method. It should be called in order for the OS to start the application. Actually, the typical function main () should look like this:

 #include "DefaultApp.h" int main() { MaksInit(); static DefaultApp app; app.Run(); return 0; } 

The MAX RTOS supports only one application. Therefore, you should declare only one instance of the class inherited from Application.

Task class


Directly task code. A class in its pure form is never used; for work, a successor from it should be created (or use ready-made successors, which will be discussed at the end of the section).

Execute () function


The most important function in the task is, of course, the virtual function Execute (). In a classic procedural-oriented OS, the programmer must implement the thread function and then pass it as an argument to the CreateThread () function. With the object-oriented approach, the algorithm is simpler:

  1. Create an inheritor from the Task class,
  2. The thread function will be named Execute (). Enough to block it. Nothing more is required.

Those who are used to working with classic OSs will notice that the Execute () function has no arguments, and it’s customary to pass a single argument to the streaming function. Most often it is a pointer to a whole structure containing certain parameters. The object-oriented approach eliminates all the difficulties associated with such a mechanism. A class is designed for the task. It can contain an unlimited (within system resources) number of member variables. You can place completely arbitrary parameters in them by any available means, for example, make public variables and fill them until the class is executed, or pass the parameters to the constructor, and he will fill in the fields, or make functions that fill the parameters (make initialization function or setter functions).

Thus, instead of a single pointer, which is processed strictly in the stream function, we get the broadest possibilities for initializing the data, separating them from the working code itself. The Execute () function itself, respectively, was left without parameters.

So. The first rule of development of any task class is to create a successor class from Task and block the Execute () function in it.

When exiting the Execute () function, the task is deleted from the scheduler, but not deleted from memory, since it can be on the heap as well as on the stack, and the delete statement applied to the stack object will cause an error. Thus, deleting the object of the task at the end of working with it is an application programmer.

Class constructor


Now let's talk about the class constructor. All constructors of the Task class are in the protected section, so they cannot be called directly. To do this, in the heir class, implement its own constructor, which will call one or another constructor of the Task class.
Examples of such heir constructors:

class TaskYieldBeforeTestTask_1: public Task
{
public:
000000 explicit TaskYieldBeforeTestTask_1():
000000 000000 Task()
000000 {
000000 }


The option is a bit more complicated:

000000 explicit MessageQueuePeekTestTask_1(const char* name):
000000 000000 Task(name)
000000 {
000000 }

It is worth paying attention to such a parameter as “task name”. This parameter is optional, but sometimes very useful. Moreover, there are two alternative methods for its storage. The easiest method is to declare a constant in the file maksconfig.h

 #define MAKS_TASK_NAME_LENGTH 0 

and ignore the name (by default, nullptr pointer is used). Very often this option is the most convenient.

The second option: do not override the MAKS_TASK_NAME_LENGTH constant; in this case, when creating a task, memory for storing the name of the task will be allocated on the heap. The name itself can be used, for example, to be recorded in the event log. What threatens the use of dynamic memory - described in one of the following sections (although this does not apply to the case of adding tasks at the initialization stage).

Finally, the third option: override the constant MAKS_TASK_NAME_LENGTH with a positive number. In this case, the task name will be stored in a member variable of the Task class. This saves you from working with a bunch, but if for the sake of one task about 20 characters are reserved, then all tasks will spend as much, even if their names are shorter. For the "big" machines crazy statement, there developers think megabytes (having available gigabytes or even tens of gigabytes). But for weak controllers saving every byte is still relevant.

Now it's time to figure out what constructors are in the Task class. There are only two of them. The first is as follows:

Task (const char * name = nullptr)

The task created through this constructor will receive the stack allocated by the operating system from the heap.

However, it is not always necessary to allocate the task stack from the main heap. The fact is that the microcontroller can work with two or more physical RAM devices. The simplest case is the internal static RAM of the controller for tens or hundreds of kilobytes and external dynamic RAM for units or tens of megabytes. Internal RAM will run faster than external. However, depending on the situation, the programmer can place the heap in the external or internal memory, because it is wonderful when the heap has a size of several megabytes! The stack is best placed in the internal memory of the controller. Accordingly, sometimes it is better not to trust the allocation on the heap, but specify the location of the task stack on your own, being sure that it is located in fast RAM. And the designer of the second type will help in this:

Task (size_t stack_len, uint32_t * stack_mem, const char * name = nullptr)

According to his arguments, it is clear that in addition to the name of the task, it also passes a pointer to the RAM, where the task stack will be placed, and the stack size is clearly indicated in 32-bit words ( not in bytes)

Usage example:

 Class MyTask : public Task { Private: uint32_t m_stack[100 * sizeof(uint32_t)]; public: MyTask() : Task(m_stack) {} }; 

It is quite simple to tell the compiler in which memory to place variables declared in a particular function, but the description of this will take several sheets, and it will greatly confuse the reader. Therefore, we will bring this information to the level of a video lesson / webinar.

Thus, the second thing that should be implemented in the successor class from Task is the constructor. You can even have a dummy constructor that simply calls the constructor of the ancestor class.

Add () function


The minimum necessary part of the class code that implements the task is written. You can add it to the scheduler. For this, the Add () family of functions is used. Consider them in more detail.

Here is the option with the smallest number of arguments, where the programmer trusts the operating system to deal with all the parameters on his own:

static Result Add (Task * task, size_t stack_size = Task :: ENOUGH_STACK_SIZE)

Adds a task with the ability to specify the required stack size (in 32-bit words).

Call example:

 Task::Add(new MessageQueueDebugTestTask_1("MessageQueueDebugTestTask_1")); 

If you need to explicitly set the task operation mode (privileged or non-privileged), you can use the following option of the Add function:

static Result Add (Task * task, Task :: Mode mode, size_t stack_size = Task :: ENOUGH_STACK_SIZE)

Call example:

 Task::Add(new RF_SendTask, Task::ModePrivileged); 

For example, another option with an explicit indication of the size of the stack:

 Task::Add(new MutexIsLockedTestTask_1("MutexIsLockedTestTask "), Task::ModePrivileged, 0x200); 

There is also the option of adding a task with priority:

static Result Add (Task * task, Task :: Mode mode, Task :: Priority priority, size_t stack_size = Task :: ENOUGH_STACK_SIZE)

Call example:

 Task::Add(new EventBasicTestManagementTask(), Task::PriorityRealtime); 

The most complete option: and indicating the priority, and indicating the mode of operation:

static Result Add (Task * task, Task :: Priority priority, Task :: Mode mode, size_t stack_size = Task :: ENOUGH_STACK_SIZE);

Call example:

 Task::Add(new NeigbourDetectionService(), Task::PriorityAboveNormal, Task::ModePrivileged); 

Recall that the most convenient place to call the Add () function in a typical case is the Initialize () function of the task class.

 void RFApplication::Initialize() { button.rise(&button_pressed); button.fall(&button_released); Task::Add(new SenderTask(), Task::PriorityNormal, Task::ModePrivileged, 0x100); Task::Add(new ReceiverTask(), Task::PriorityNormal, Task::ModePrivileged, 0x100); } 

However, this function can be called at any place in the code. In the OS testing classes, you can find similar constructions:

 int EventIntTestMain::RunStep(int step_num) { switch ( step_num ) { default : _ASSERT(false); case 1 : Task::Add(new EventBasicTestManagementTask(), Task::PriorityRealtime); return 1; case 2 : Task::Add(new EventUnblockOrderTestManagementTask(), Task::PriorityRealtime); return 1; case 3 : Task::Add(new EventTypeTestManagementTask(), Task::PriorityRealtime); return 1; case 4 : Task::Add(new EventProcessingTestManagementTask(), Task::PriorityRealtime); return 1; } } 

And they are quite acceptable.

Thus, after the class derived from the Task class is created, the constructor is redefined (you can use the dummy constructor, the calling constructor of the Task class) and the Execute function, this class should be connected to the scheduler using the Add () function. If the scheduler is running, the task will start running. Or it will start to be executed from the moment the scheduler starts.

Functions that are conveniently called from the task class


There are a number of functions that the class derived from the Task class can call to ensure its own functioning within the OS. Let us briefly review their list:

Delay ()Blocks a task for a specified time, specified in milliseconds.
CpuDelay ()Performs a delay in milliseconds without blocking the task. Accordingly, the control of other tasks for the delay time is not forcibly transferred (the task can be taken away control when switching on the system timer). But with cooperative multitasking, only this function is possible.
Yield ()Forcibly gives control to the scheduler so that he starts the next task. With cooperative multitasking, task switching is performed by this very function. With displacing, a function can be called if the task sees that it has nothing more to do and can give the rest of the time quantum to other tasks.
GetPriority ()Returns the current priority of the task.
SetPriority ()Sets the current priority of the task. If a task lowers its priority, then with preemptive multitasking, it may well be pushed out without waiting for the completion of the time slice.

Functions usually called externally


Some functions, on the contrary, are intended to be called from outside. For example, a function that allows you to find out the status of the task: if the task calls it, it will always receive “Active”. Finding the status of the task makes sense from somewhere outside. Similarly, the other functions of this group.

GetState ()Returns the status of the task (active, blocked, etc.
GetName ()Returns the name of the task.
Remove ()Removes a task. It can also be called from the task itself, then it will forcefully trigger a context switch. The task object remains in memory.
Delete ()Same as Remove (), but with the removal of the object. Accordingly, the object must be created using the operator new, and not on the stack.
Getcurrent ()Returns a pointer to the current task.


Class Scheduler


Once this class is mentioned as a minimally necessary component, we will consider its interface functions, although it simply does its work hidden from the application programmer. Nevertheless, it still contains some useful functions.

GetInstance ()A static function with which you can get a reference to the scheduler object in order to access it in the future.
GetTickCount ()Returns the number of system ticks that have passed since the scheduler started.
Pause ()Suspends switching tasks by the scheduler, or includes working again (a specific action is passed in the function argument).
ProceedirqThe function will be discussed in the section on interrupts.

Ask questions and leave comments - this is what inspires writing and publishing articles.

Here we will stop, as the big block of the useful theory further follows.

Source: https://habr.com/ru/post/336944/


All Articles