📜 ⬆️ ⬇️

Overview of one Russian RTOS, part 2. Core MAX RTOS

I continue to lay out the chapters of the Knowledge Book of the MAKS RTOS. The first part was common. Today is the second part, dedicated to the core and priority of tasks.

Content (published and unpublished articles):

Part 1. General information
Part 2. Kernel of the MAKS RTOS (this article)
Part 3. The structure of the simplest program
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
')

Task


As already mentioned , the task in the MAKS RTOS is analogous to a stream in a general purpose OS. At the same time, an arbitrary number of tasks can be executed in the system (within the available resources, of course). In a general-purpose OS, it would be possible to stop theorizing and move on to practice, but in the case of a real-time OS, the programmer must be sure that he did everything right and the tasks are guaranteed to receive as much processor time as they need. And to do everything right, you need to know some theory. Therefore, we consider the work tasks in more detail.

Types of multitasking


In the MAKS RTOS (as in most other real-time operating systems) three different types of multitasking are possible: displacing, cooperative, mixed. Why so much? The fact is that in different systems it is more convenient to use different types. Therefore, the choice of the most convenient type remains for the developer of a specific application program.

Preemptive multitasking


This type is the most familiar for many programs, moreover, displacing multitasking is similar to that used in general-purpose OS. The scheduler gives each task a fixed time quantum (set using the MAKS_TICK_RATE_HZ constant, which is 1000 Hz by default, that is, the quantum is equal to one millisecond by default), and then displaces this task, putting the next one to execution. The principle of sorting the tasks will be discussed below, but here we just note that tasks are executed in turn, according to the system timer. In general, this scheme looks pretty nice (no wonder it is used in general-purpose operating systems) until the battles for equipment begin.

Problems of preemptive multitasking when working with equipment


One of the main tasks of microcontrollers is working with equipment. And this feature imposes a number of responsibilities on the programmer. It may happen that the same SPI bus serves several devices separated by different CS lines. Until you have finished working with one device, you cannot begin to work with another.

An example of using SPI bus with multiple devices

Fig. 1. An example of using a single SPI bus with multiple devices

The I2C bus is originally designed for connecting many devices.

Sometimes it is required to maintain any time diagrams with a sufficiently high accuracy. If the time quantum leaves, the diagram will be distorted.

But with preemptive multitasking, the planner is merciless. No one knows when the timer to switch tasks will work, so in the cases described above, it is necessary to use synchronization mechanisms. With a certain scheme, it is enough to implement several synchronization primitives. But it may turn out that the programmer will be forced to think not so much about the program as about synchronization. And in this case it is better to abandon the preemptive multitasking in favor of the cooperative.

Cooperative multitasking


With cooperative multitasking, switching is performed not by timer, but by command from the task itself. When she did everything she was supposed to do in a given time slice, she calls the task switching function Yield () herself. Calling this function serves as a signal to the scheduler that it is time to transfer control to the next task. As a result, the task has a guarantee that its execution will not be interrupted until she herself requests the system about it. But needless to say, the other side of the coin is a big burden of responsibility on the programmer. It is the application programmer who must ensure that the task does not take the processor too long. It is also customary to write that the hang of the task will hang the entire system, but in my opinion, this is even better. The hardware must either work or not. If one of the subsystems fails - the equipment becomes unsafe, the consequences can be terrible. So it’s better that the device fails completely, it will serve as a signal to search for an error. However, in the case of preemptive multitasking, the hang of one task will not interrupt the work as a whole, and in the case of cooperative - the task simply will not give control to the other.

Also cooperative multitasking can be used to quickly port applications from single-tasking systems. Consider a fragment of the Marlin “firmware” for a 3D printer (in fact, a CNC machine):

void loop() { if(buflen < (BUFSIZE-1)) get_command(); #ifdef SDSUPPORT card.checkautostart(false); #endif if(buflen) { ... process_commands(); //SDSUPPORT buflen = (buflen-1); bufindr = (bufindr + 1)%BUFSIZE; } //check heater every n milliseconds manage_heater(); manage_inactivity(); checkHitEndstops(); lcd_update(); } 

The loop () function is called by the Arduino runtime in an infinite loop. Such a cycle is an analogue of the scheduler that switches tasks. If you redo everything on a real scheduler, then you can easily throw out the loop () function, and form its components in the form of tasks that spin in an infinite loop, calling Yield () before the next iteration. This will ensure that the tasks do not conflict with each other over the equipment and create each other timeouts. And in the future, as the system adapts, you can try to switch to preemptive multitasking.

Mixed multitasking


The last possible option, when the system works in the preemptive multitasking mode, but some (or some) of the tasks call the Yield () function. In particular, if the task got control, but found out that the data for it is not yet ready. Then, in order not to occupy resources, it can call the function Yield () to give control to another task. However, in the current version of the MAX RTOS, this is permissible, but should be used with caution. I would rather highlight the cause in red to enhance the impact.

Please note that mixed multitasking in the current MAXROS RTX should be used with caution. The point is that the preemptive task switching takes place on a timer, which operates at a fixed frequency. Forced task switching does not affect this timer. Suppose task A is always executed for 700 µs, after which it transfers control to the scheduler using the Yield () function. The scheduler assigns task B. But after 300 ms an interrupt from the timer arrives, and the scheduler transfers control to the next task B. Since the transfer of control occurs sequentially, task B will be forever impaired. She will always be given a defective time quantum, and the remainder of the quantum of task A.

Task status


Now that we are familiar with the types of multitasking, we can consider the problem and the logic of transferring control of it closer. The graph of task states and transitions between them is shown in Fig. 2

Fig.  2. Task states and transition graph between them

Fig. 2. Task states and transition graph between them

runningThe most pleasant condition. The task is executed. Every moment in time, in this state there can be only one task.
readyWhen a task has a time slice, or it has independently called the function Yield (), it enters this state and will stay in it until the scheduler again puts it to execution.
blockedThe task will go to this state if it has requested any busy resource. These can be synchronization primitives (mutex or semaphore), it can be waiting for an event, it can be waiting for a message from a queue. Until the resource is released, the task will be in a locked state, without spending processor cycles. This is an extremely important state for the task, because the fewer tasks are executed, the more processor time is left for the others. It is highly recommended not to organize waiting for something in the loop, but to block tasks so that the scheduler knows that they should not be called.

When the resource is released, depending on the situation, the task can either immediately go to execution, or go into a state of readiness for planning. The latter will happen if the resource is released, which several tasks were waiting for at once (after all, only one can be executed), or if tasks with a higher priority are currently being executed.
inactiveThe state in which the task passes at the time of creation, or at the end of its work. That is, if the task is not yet connected to the scheduler, or, on the contrary, it is already disconnected from the scheduler. An auxiliary state that has no relation to the work of the task.

Task priority


Each task is assigned its own priority.

Priority is an integral and extremely important property of the task. In general, it would be possible to assign priorities to just numeric values. But, firstly, it is accepted to call priorities in general-purpose OS (another thing is that there it makes sense, but here, in principle, it would be possible to manage), and secondly, the object-oriented approach is closely connected with typification. Some abstract numbers are easily confused, and when naming you can associate names with a specific type of "priority."

So, both for the purposes of observing traditions and for ensuring strict typing, the following priorities of tasks are identified in the MAKS RTOS:

  enum Priority { PriorityIdle, ///<   (   IDLE) PriorityLow, ///<   PriorityBelowNormal, ///<    PriorityNormal, ///<   (  ) PriorityAboveNormal, ///<    PriorityHigh, ///<   PriorityRealtime, ///<   ( ) PriorityMax = MAKS_MAX_TASK_PRIORITY }; 

Task switching order


When describing a general-purpose operating system, usually this section has a huge size, while the authors add that they describe the mechanism briefly, since the mechanism changes from version to version. This is due to the fact that the general purpose OS should try to ensure the operation of all threads of all processes. In the case of real-time OS, everything is simple. The tasks are switched using the Round Robin algorithm, the diagram is shown in Fig. 3. The essence of this algorithm is that the list task is executed in order, and when the end is reached, the OS switches to the top of the list. Inside the list, when selecting a task for execution, the task next in the list is taken. If it has a ready state, it is put to execution. If bloked - proceed to the next task.

Fig.  3. Switching tasks according to the Round Robin algorithm

Fig. 3. Switching tasks according to the Round Robin algorithm

It should be noted that tasks with the highest priority are executed, that is, there can be several lists in a real system. For example, a task list with priority PriorityNormal and a list with priority PriorityHigh. At the same time, if all the tasks are not in the waiting state, the scheduler will always put on execution only tasks from the list with a higher priority.

Fig.  4. An example of performing tasks 1-6

Fig. 4. Example of execution of tasks 1-6 (with a higher priority) and complete idleness of tasks 7-12 (their priority is low for work)

However, this does not mean that some tasks are ignored: with a properly designed program, time will be transferred to all tasks. Simply, tasks with higher priorities should be designed to be in the blocked state as often as possible - waiting for any resources (semaphores, data from the queue, etc.).

is 5. An example of performing tasks 7-12

Fig. 5. Example of performing tasks 7-12 (with low priority), since all high-priority tasks are blocked

Tasks with normal priority are “workhorses.” They constantly perform repetitive actions: they monitor slow equipment, provide input of control actions, display of results, etc.

Tasks with a priority below normal can be entered into the program, if there is a guaranteed chance that even tasks with normal priority once all can go into the blocked state (that is, a simple call to the Yield function does not help, you must either wait for resources or call the function Delay (admittedly, make all tasks fall asleep with the current priority)).

Practical examples of the application of tasks with high priority will be given below. They are designed to handle "fast" equipment. In the meantime, enough to remember a few basic rules:


Energy saving features


When all tasks are in a locked state, the processor must do something, or it should be put to sleep. The system has a special preprocessor constant that allows you to choose one of the behaviors.

MAKS_SLEEP_ON_IDLE

If this constant is defined with a value of 1, then the scheduler, not finding any active tasks at any of the priority levels, will lull the processor, which will lead to a decrease in power consumption. Awakening will occur when any interrupt request (including system timer) arrives. If the determination value is zero, in such cases the idle task will be executed, forcibly launched by the operating system itself and having the lowest possible priority:

  for(;;) { #if MAKS_DEBUG ++ IdleTaskCnt; #endif } 

Normal and privileged task operation modes


The current equipment on which the MAX RTOS can operate does not support memory virtualization, but supports some elements of its protection. Thanks to this, you can catch some errors in the program. Classical is the protection of a section of memory in the region of zero address in order to record attempts to access by null pointers (for example, the memory manager did not allocate memory, returned a null pointer, and the program, without checking, began to use it).

In addition, you can prevent a user program from changing interrupt priorities by blocking access to NVIC hardware.

Usually in the literature they write that memory protection allows you to keep the system working when an application fails, but I do not agree that the equipment should be maintained. On the contrary, when fixing such failures, it is necessary to transfer the equipment to the off state as soon as possible and signal the problem. Just all the tips on maintaining performance relate to general-purpose OS. Solitaire flew out - they launched a miner, there won't be much difference. In the case of equipment, one application is running in the system. Launching another is impossible. And the failure of any of the subsystems can lead to the fact that the mechanical or electrical part of the system will go astray. That is why you should transfer everything to a safe state as soon as possible and inform the developers about the problem.

But, as they say in a well-known joke, some say that they should drink less, others - that they should drink more, but everyone agrees on one thing: it is necessary to drink. Also here: for whatever purpose it is done (to ensure survivability at any cost, or to ensure a safe shutdown), and protecting the memory is an extremely useful thing.

Accordingly, to ensure this protection, the application can be transferred to normal operation. The OS will also work in a privileged mode.
However, given that applications running under the MAKS RTOS can be quite simple, their operation in privileged mode is also allowed. In this case, the application programmer has access to the same as the system programmer, but the hardware does not track any wrong actions.

First, you can start individual tasks with privileged access by specifying it in the arguments of the Task :: Add () function of the task. In addition, you can set the conditional compilation option MAKS_PROFILING_ENABLED to 1, after which all tasks will run in privileged mode.

In the next article I will introduce the structure of the simplest program running under the MAKS RTOS.

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


All Articles