In one of the previous notes, the author tried to argue that when programming a microcontroller, a simple task switch will be useful in situations where using the real-time operating system is too much, and the comprehensive loop for all required actions is too little ( Said just like the Count de La Fer). More precisely, not too little, but too confusing.
In a subsequent note, it was planned to streamline access to resources shared by several tasks using queues based on ring buffers (FIFO) and a separate task dedicated to this. Having scattered for different tasks those actions that are not related to each other, we are entitled to expect a more visible code. And if at the same time we get some convenience and simplicity, then why not try it?
Obviously, the microcontroller is not designed to solve any conceivable task of the user. Then, perhaps, such a task switcher will be quite enough in many situations. In short, a small experiment is unlikely to hurt. Therefore, in order not to be unfounded, your humble servant decided to write something and test his scribbles.
In microcontrollers, I must say, the requirement to reckon with time as something important and hard-set is more common than in general-purpose computers. Going beyond the framework in the first case is equivalent to inoperability, and in the second case, it only leads to an increase in the waiting time, which is quite acceptable if the nerves are in order. There are even two terms “soft real time” and “hard real time”.
Let me remind you, we were talking about controllers with the Cortex-M3,4,7 core. Today is a very common family. In the examples below, we used the STM32F303 microcontroller, which is part of the STM32F3DISCOVERY board.
The switch is a single assembler file.
The assembler is not afraid of the author, but, on the contrary, inspires hope that the maximum speed will be achieved.
Initially, the simplest logic of the switch operation was planned, which is presented in Figure 1 for eight tasks.
In this scheme, tasks in turn receive their portion of time and can only give up the remainder of their tick and, if necessary, then skip a few of their ticks. This logic proved to be good, because the size of the quantum can be made small. And this is precisely what is required in order not to urgently raise a task for which an interruption has just happened, and also to raise and then lower its priority. The packet that has just been received will quietly wait 200-300 microseconds until its task receives its tick. And if we have a Cortex-M7 operating at a frequency of 216 MHz, then 20 microseconds for one tick is quite reasonable, since it will take less than half a microsecond to switch. And any task from the example above will never be more than 140 microseconds late.
However, with an increase in the number of tasks, even with an extremely small size of the time quantum, the delay in the onset of the activity of the desired task may cease to please. Based on this, and also taking into account that only a small part of the tasks really require hard real time, it was decided to slightly modify the logic of the switch. It is shown in Figure 2.
Now we select only part of the tasks that receive a whole quantum, and select only one tick for the rest, in which they take turns in the game. In this case, the initialization subroutine receives an input parameter, namely the position number, starting from which all tasks will be affected in rights and will share one tick. At the same time, the old scheme remained available, for this it is enough to set the parameter value to zero or the total number of tasks. Switching costs increased by just a few assembler instructions.
Two similar schemes are used to allow access to shared resources. The first, which was mentioned in a previous note, uses several FIFOs (or circular buffers by the number of message producers) and a separate matching task. It is designed to communicate with the outside world and does not require expectations from tasks that generate messages. It is only necessary to ensure that the queues are not crowded.
The second scheme also uses a separate task to allow access, but introduces expectations because it manages the internal resource in both directions. These actions cannot be tied to time. Figure 3 shows the components of the second circuit.
The main elements in it are a buffer of requests, according to the number of desired tasks, and one access indicator. The operation of this design is quite simple. The task on the left sends an access request to a place specially allocated for it (for example, task 2 writes 1 to Request 2). Task - the dispatcher selects whom to allow and writes the number of the selected task in the resolution flag. The task that has received permission performs its actions and writes the sign of the end of access to the request, the value 0xFF. The scheduler, seeing that the request is cleared, resets the permission flag, resets the last request and resets the request from another task.
Two test projects under IAR and a description of the STM32F3DISCOVERY board used can be viewed here . In the first project, the ATS303 simply checked its performance and debugged it. All the LEDs installed on this board came in handy. No harm done.
The second draft of BTS303 tested the two mentioned resource allocation options. In it, tasks 1 and 2 generate test messages that are received by the operator. To communicate with the operator, I had to add a scarf with a TTL COM port, as shown in the photo below.
The operator uses a terminal emulator. I think the reader will excuse the author for the soft tube color. It looks like this.
To start the entire system, before resolving interruptions, preliminary steps are required in the body of the zero task main (), which are presented below.
void main_start_task_switcher(U8 border); U8 task_run_and_return_task_number((U32)t1_task); U8 task_run_and_return_task_number((U32)t2_task); U8 task_run_and_return_task_number((U32)t3_human_link); U8 task_run_and_return_task_number((U32)t4_human_answer); U8 task_run_and_return_task_number((U32)task_5); U8 task_run_and_return_task_number((U32)task_6); U8 task_run_and_return_task_number((U32)task_7);
In these lines, the switch first starts, and then, in turn, the remaining seven tasks.
Here is the minimum set of calls required for the job.
void task_wake_up_action(U8 taskNumber);
This call is used in an interrupt from a user hardware timer. Challenges from the tasks themselves speak for themselves.
void release_me_and_set_sleep_steps(U32 ticks); U8 get_my_number(void);
All these functions are in the assembler switch file. There are several more functions that are useful for testing, but not required.
In the BTS303 project, task 3 receives operator commands from the outside and sends him answers to them that come from task 4. Task 4 receives commands from the operator from task 3 and executes them with possible answers. Task 3 also receives messages from tasks 1 and 2 and sends it via UART to the terminal emulator (for example, putty).
Task 0 (main) does some auxiliary work, for example, checks the number of words left unaffected in the stacked area of each task. This information can be requested by the operator and get an idea of the use of the stack. Initially, for each task, a stack area of 512 bytes (128 words) is allocated and it is necessary to monitor (at least at the debugging stage) that these areas do not come close to overflow.
Tasks 5 and 6 do calculations on some common floating-point variable. To do this, they request access to it from task 7.
There is another additional feature that can be seen in test projects. It is designed so that you can awaken the task not after the number of ticks has expired, but after a specified time, and it looks like this.
void wake_me_up_after_milliSeconds(U32 timeMS);
For its implementation, a hardware timer is additionally required, which is also implemented in test cases.
As you can see, the list of all necessary calls fits on one page.
Source: https://habr.com/ru/post/461121/
All Articles