Modern microcontrollers have a fairly large performance and this gives many programmers the opportunity to think in approximately the following vein: - “It's okay if 1-5% of the performance goes to operating system maintenance. But my code will be easily debugged and explicit! ” These thoughts are supported by a large amount of non-volatile (flash) memory for storing the operating system code and operational (RAM / SRAM) memory for allocating its own stack for each task. However, in most cases this idea is erroneous. And in this article I will explain why.
About the projects I work with
In my practice, I often have to work with a “designer”. I described this approach in detail in my previous article on the
use of C ++ in microcontrollers . Then I did not tell the most important thing. Most of the “blocks” of this “constructor” are somehow tied to a real-time operating system. Most of the "blocks" have their own flow (task, in terms of the real-time operating system FreeRTOS used). And that, on average, the project has about 10-15 tasks. Sometimes this value reaches 35-40.
Where so much?
Here is a short list of tasks that are encountered
in each project:
- ADC maintenance (each module is serviced by its own flow);
- wdt maintenance (if the OS crashes, the task will not reset it and the device will reboot);
- work with settings pages (a separate stream controls the work with flash memory);
- maintenance of the protocol of interaction with the outside world (downstream to the interface. For example, uart);
Then there are already specific things for each device, such as a stream for servicing thermistors (receiving data from the ADC measurement stream and converting this data to temperature), polling the external peripherals and so on.
')
Apparent simplicity
Despite the fact that there are many tasks in the project, each of them is “hidden” inside an object of the corresponding class (remember that the constructor is in C ++, but this can also be imitated in C using “programming in C in an object-oriented style.” But
it’s better not to necessary ). Since the objects of this “constructor” are global and FreeRTOS 9 is used in the projects, which supports the creation of their own entities in buffers allocated by the user, the memory usage can be controlled even at the build stage. So from the point of view of monitoring memory leaks - everything is more or less normal. But there are the following nuances:
- it needs to be clearly understood how much a stack is needed for each thread. Wherein:
- critical cases must be taken into account (for example, nesting with a certain behavior);
- if functions from standard libraries are used, then also know how they are arranged, or at least have an idea of ​​how much they will consume the stack;
Apart from this fact, it seems that using the operating system will only improve the logic of the code and make it clearer.
Abuse of operating system functionality
The main problems begin at the moment when you begin to forget what you are writing specifically for the microcontroller. The OS imposes its costs on working with its own entities (such as semaphores, mutexes, queues). Here is an example of a
UART class for implementing a terminal function . In the interrupt, the byte is received, after which, if it passes the range by valid input characters, it is added to the queue with the corresponding replacements (for example, '\ n' changes to the sequence "\ n \ r"). This was done in order to secure the port for sending (since the port can work not only as a terminal. Log data can also be sent through it). On the one hand, this ensures that the response will be sent as soon as possible and will not interfere with sending more priority data (in addition, while priority data is being sent, it accumulates in the buffer, which allows DMA to be used to send the response). However, starting from this moment you get on a slippery track. Instead of writing a bunch through a queue, one could just correctly configure the interruption on a non-empty buffer that is not currently working on the UART and when the DMA ends. This approach requires a clear understanding of how peripherals work. However, it reduces costs to an absolute minimum, making the need for such a solution zero.
Ignoring the microcontroller hardware functionality
In my practice, I met a project with 18 software timers of the operating system tuned to the same frequency. At the same time, there were about 10 timers in the microcontroller, of which only systic was used. To clock the scheduler of the operating system. This decision was explained by the lack of desire to "mess with the hardware" of the microcontroller. At the same time, about 10 kb was allocated under the stack for the function called by the software timer. In fact, about 1 kb was used (short). This was due to the "ambiguity of what is happening inside the called libraries."
In this case, it was possible to safely select TIM6 (in the case of using stm32f4), which would generate an interrupt with a given frequency and inside it would simply call the required functions.
Using an infinite loop instead of a state machine
As a separate column, I would single out the inability of some programmers to write compact finite state machines, and instead create a stream in which there is an infinite loop that starts its work by getting something from the queue. Interestingly, how to create compact finite state machines by means of the language itself is written in
this article .
Ignoring the "hardware scheduler"
Many thirty-two bit microcontrollers have a sophisticated interrupt controller with a customizable priority system. In the case of stm32f4, it has the name NVIC, and has the ability to set interrupt priorities with 16 levels (without considering even the sublevels).
Most of the applications under FreeRTOS that I had to deal with could be written as state machines called in interrupts with correctly configured priorities. And in case the processor returns to "normal execution" - go to "sleep". In this case, there would be no need to block access to most resources (variables and others). Applications would lose an extra level of abstraction. And in this case - far from free. However, this approach requires thoughtful architecture planning for each project. In projects, "designers" - all interrupts have one priority and, in fact, are needed in order to "filter" the data. Then put the leftovers in the queue, from where the object stream of the corresponding class will pick them up.
Summary
In this article, I talked about the basic problems that have to be encountered when using the operating system in projects for microcontrollers, and also examined common cases of using the operating system when this could have been avoided without losing the readability and logic of the code.