 So, a question to you. What is an operating system? What do you expect from an operating system? Simultaneous running of the tasks, implementing different modules, hardware resources management, interrupt and scheduling of the tasks. All these operating systems should handle. However, now let's focus on a microcontroller. A thing running, for example, at 72 MHz, having one single core and doing everything it can to manage your application. So, a question, can a single core of Cortex-M4, for example, execute tasks in parallel? No, it can't. There is one core, one execution pipeline for instruction. So, it always will execute one instruction at a time, which means that it can switch between the tasks very quickly, but it can't execute them truly in parallel. So, it's from the user perspective that they run in parallel because they, for example, switch every millisecond or faster and at that moment, you as a user have the feeling of the parallel execution, but for the microcontroller, it's a heavy job of doing part of your tasks, switch to another one, do another task, switch to another one, do it, switch to another one, check if it is synchronized and waiting for a good thing and then coming back to the first one. So, it's always doing a part of the job and coming to another job. The Friartos can't be compared to something like Windows because it's a tiny scheduler, it's a tiny thing to switch the tasks and manage synchronization between them. It's nothing that would contain peripheral drivers. It's nothing that would give you for free networking. So, if you want to implement something big, it gives you the tools to create it, but it doesn't give you all the ecosystem integrated inside just like it is. So, it needs a little bit of work. In the best case, clicking on I want the Ethernet stack in a QBemix and let it generate your code. So, this is the minimum you need to do to have a working communication stack on top. But the Friartos itself is not responsible for the connectivity. So, the operating system is something that takes care about the environment where your applications are running. Same definition is valid for the Friartos, but you can't expect a really big implementation, especially if you run on small microcontrollers and you on top expect, for example, a low current consumption. So, a question, what is a task, is explained on the slide. You can see that in terms of the definition on a microcontroller, it's a C function. The C function typically has got some initialization code and then it has got a never-ending loop. In this never-ending loop, your task or mini-application should be executed and it should manage its communication and lifetime. It should as well manage its own timing. This mini-program or mini-application has as well associated its own stack in a place where it can place local variables and, for example, return address and context when it gets into the interrupt or into sub-functions. So, each of the mini-applications has its own stack defined in the memory and it has as well a priority. The priority can be changed dynamically so you can increase it and decrease it in the application lifetime. But it has a very big meaning when the core of the Friartos schedules which task will operate next. So, the whole idea behind the Friartos is to give you a time slice. This is a time that the microcontroller spends executing your task and when it's time slice elapses, for example, after a millisecond of periodical interrupt, the scheduler will take the list of the running tasks and it will look which one has currently the highest priority. And then it will switch to this one and execute it maybe for another millisecond and then it will again enter this switching process and it will check, hey, is there another task that is alive that has highest priority than this one? And if yes, Bingo will switch to it and we will execute it. So, this is the way how the Friartos or typically any other Artos takes care about task switching. You will later see that there exists a preemptive task switching and cooperative task switching. This has quite a big impact on the design of your application. So, you will see later in detail. Further, each of the tasks can operate in four different states. The first one is when it is created, it becomes ready. What that means? Red means that such task is able to be executed. So, the scheduler can pick it up and it can start execution. However, when I have got a task with a higher priority, I can put this task aside and it can be as well put back to ready state. So, it comes from ready to running and back from running to ready. And that's the decision of the scheduler which task will execute. There is one more possibility when the task asks for a resource that is not available. It can be data in the queue, it can be a mutex, it can be waiting for a semaphore or for a signal. So, if such resource is not available, the task can become blocked. At that moment, the scheduler will remember that the task waits for something. And it will put in a specific queue in a specific list telling that it's blocked and it waits for a resource. And when the resource becomes available, like somebody releases the mutex or you get a semaphore or you send a message to this task, the kernel will evaluate it. It will notice that this task received the message. It will unblock it and it can move it back to the ready state. And if this task currently when it's unblocked has the highest priority, it will come back to the running state immediately. Additionally, you can force the task to a suspended state where it's removed from the scheduling process and it doesn't consume any CPU time. It has one advantage, you don't bother with this task, you don't waste the time evaluating its priority. So, it's very effective when you simply need to remove this task and don't care about it. Still, it's there. You have it at hand, you don't need to recreate it. You can execute it any time you want when you remove it from the suspended state. These are the main states that you can expect from any of your many tasks or many applications. The transition between all these comes in terms of API calls to the kernel or by the scheduler itself. So, you can, for example, force yourself from the running state to the ready. If you call the function OS yield, you just give up the CPU time and you let the scheduler select another task if it is available. So, you get, for example, into the blocked state if you wait for some time. You can say, okay, now I'm toggling the LED. So, let's put the task into the blocked state by calling a delay function. So, you want to toggle the LED every 200 milliseconds. So, your function will look like LED toggle and OS delay 200 milliseconds. Then the task will be automatically put into the blocked state for 200 milliseconds and the scheduler will put it back to red after the 200 milliseconds elapsed. So, it gives you the chance for timing the task execution. Now, when you know how the tasks change their lifetime and how they cooperate through the scheduler, you probably should understand that if you have already a CPU running at 100% of your time in a bare metal application, putting a free RTOS may not be a big help for you. Because performance-wise, you will spend approximately 0.1, 0.5% of the time in task switching. But free RTOS can help you in simplifying your application and splitting it into much more simple tasks. But it will not help you to accelerate it, especially if you already use all the microcontroller time and you will put the free RTOS on top of this. Priority of the tasks, it will help you in managing the communication between them, but it will not increase the performance. Of course, if you have functions that, for example, spend too much time by synchronization of the data between the tasks, free RTOS can be more effective. If you need to wait for a given resource, then, for example, switching in the state machine, whether it will go this way or this way, the free RTOS can give you a little bit more performance. But overall, it will not increase the performance. One more thing, the task should never end. If you leave the task function, you crash the free RTOS. That's because it doesn't have any return address. It doesn't know where to return. The next thing you should expect from the free RTOS is its scheduler. This is a background part of code that takes care about switching between the tasks. The scheduler is a thing that gets called by different API functions. At the end of interrupts and whenever you, for example, call a blocking function. When it's activated, it decides what other tasks should be running. It can decide that it will return back to the original task if it's the only one with the highest priority. But if by internal or external API calls, another task became ready and it gets higher priority than the current one, then the scheduler will choose the task with the higher priority. It's always about priority. And if you have two or more tasks with the same priority, then it will move in a under bin. So if I have got three tasks with the same priority and task number one finishes, then it will look into the task number two. And if task number two finishes, it will look in task number three. And when three is finished, then it will look again in task number one. But if you have a task number five with higher priority, it will always jump to task number five if five is ready. Now, what is differentiation between a preemptive and cooperative mode? This is a very important thing to understand. And this is as well about fine-tuning your application. In preemptive mode, if a task gets a higher priority and becomes ready, it is immediately switched over. So you can see here that if task number two is awakened or if the scheduler gets, for example, a time-based tick, it can immediately jump to the task number two if it has a higher priority than task number one. So in preemptive mode, the task switching happens immediately when the kernel knows it, when it recognizes it. And it can be on the function call, it can be on the periodical interrupt on the time-base, or from interrupts by sending signals, messages, or other synchronization objects. So in preemption, the task switching happens anytime in your application lifetime. The cooperative mode is different. In cooperative mode, the kernel will not switch immediately, but it will switch only when the original task gave up its time by getting into the blocked state. So in cooperative, you always must create functions by the way of calling OS delay, OS yield, or waiting for some synchronization object. Otherwise, even if the task one has a lower priority than task two, the task one, if it never gives a CPU time, it will never switch to task two. So even if task two has a higher priority, it must wait until task one finishes its job. The truth is that the preemptive gives you a feeling of faster responsiveness. But on the other side, I could say that the preemptive mode is a little bit less efficient because the task switching takes some time and additionally, if you need to synchronize the tasks, for example, their timing, it can be a little bit more difficult to handle. While in cooperative mode, if you are waiting for something, if you are stuck in a task that, for example, does an FFT calculation, then you are stuck for this time. But the FFT is finished as one block and you have got the result in the shortest possible time. So when I do my applications, I prefer the cooperative because it seems to me to be a little bit more stable. I can tell you that I've got some demo application created that runs approximately 22 tasks. And in the cooperative mode, it works. If I switch it to the preemptive, it doesn't crash but doesn't work well. So you have to test it. You have to think well how you design the application. Further, let's talk about the heap of the operating system. You already know that when you have a task, it contains a stack. Stack is part of memory where it places its local variables and context when it jumps to sub-functions or interrupts. However, these stacks need to be placed in a RAM memory and managed. And when you create a new task, its stack has to be dynamically allocated. And when you kill the task, it can be deallocated. Additionally, a lot of other objects or effectively all RTOS objects need some part of the memory that can be allocated dynamically. This allocation happens on a chunk of memory that's managed globally by the free RTOS in the heap. So free RTOS has its own heap management. And we will see later which methods can be used for the heap management. But right now, you can notice what the heap contains. So when it's created, it's initialized during initialization of the free RTOS. And then free RTOS takes care about putting different objects and allocating the memory in the heap to these objects. So the first part of the heap is dedicated for the main application, but it doesn't need to be used. It can be used, for example, for printf function and some other RTLC functions, but it's not used by free RTOS itself. Free RTOS then defines a block of memory. Normally, it can be a variable that you define as a static variable with some size. It can be as well placed anywhere in the memory, SD RAM, internal SRAM, external SRAM, and so on. But that's what the free RTOS will take care of in terms of its content and allocation. And when you look at the global RAM here that we have, we still need to allocate a stack for the interrupts and for the main application before we start and jump the scheduler. So if we look into the heap, you can see that the heap contains the data for the tasks that we define. So when I create a task A, I am creating a block of memory in the heap of the free RTOS that belongs to this task. And it consists of two parts. The first one is the task control block. This is a chunk of memory that describes the task. It tells to the scheduler where the task resides in the memory. It gives a snapshot of the CPU registers stored in this block. It contains the information of where the allocation of the stack for this task is. And it gives all the states like whether the task is ready, whether it's waiting for something. So all this information is stored in the task control block. And the second part of the task allocated memory is its stack. So when you create a task, it creates the TCB and the stack. And when it launches the task, it puts the top of the stack to the top of the area that it allocated in the heap. The advantage, each task can work as an independent many application with its own dedicated memory, which is a stack of the task. Now what happens when you create a second task? When you create a task B, the RTOS against creates or takes part of the memory from its heap and it creates its own TCB and its own stack for the task B. And now what happens when the scheduler switches from task A to task B? Who will decide how big stack you allocate to the task, right? You as a programmer. How you can do it? When you compile your application, the compiler and linker can give you a call tree with an estimated depth of the stack. So you can look in the map file or in the file that lists the call tree and you can try to deduct. Unfortunately, you have to count for recursive functions and then you don't know how deep stack you will need. So unfortunately, it's you. Of course, you can help yourself by this call tree. But another possibility is when you debug your application, you can simply give it a big stack. So as much memory as you can and when you run your application and you test it in different scenarios, in different corner cases, the stack has got, when it's created, it has got a special pattern written into it. So the free RTOS is able to check periodically and at the moment of switching to this task, how much of the stack was overwritten. So it checks from the bottom of the stack and goes upwards and looks how much dead beef is in there and it gives you this amount in the task description. So in runtime, you can observe how much stack is used for a given task and if you see there is too much unnecessary space, you can reduce it. When you create a new version of your application. So by this, you can adapt the stack size. And additionally, when a task switcher recognizes that the stack was corrupted, which means that even the bottom safety part was overwritten, it will give you or it can give you a hook that there was a stack overflow. So the free RTOS can help you in debugging such situation by telling you how much stack is used or whether the stack overflow happened. So if you have stack overflow, you should increase the stack size. But on the other side, if you see that only small part of the stack is used, you can reduce it. CortexM itself doesn't have any means of preventing the application from overflowing your stack. There is nothing in the core. On the other side, it has got a memory protection unit. So if you use the free RTOS-MPU extension, you can define for each task how big stack it has. And when you switch to it, the MPU can define regions where the task is accessible in the memory. And if it guards the stack and it runs out of the stack, you get memory protection exception. So it's possible, but the dash MPU is a specific port of the free RTOS. And it has got some support when the task creation is done, but you as well need to help yourself because it doesn't know where you have got, for example, global variables. So you need to split among the eight regions you have. Four of them are user-selectable and four of them are selectable by the free RTOS. So it can help you in this matter. But natively, without the MPU extension, this is not used. And only when you do tasks which you can recognize what happens with the stack. So I will come back to the question, what defines a context of your task in a run state? And it's executed on the core. So if you do a task switch, the scheduler will take these 17 registers. It will put them in the TCP task control block. And then it will load the same 17 registers, R0 to R15 plus the program status register from the task control block of the other task. It will simply load the registers. It will do the interrupt return. And a second task is running. That's simple it is. So whenever you get a task switch, the context switcher takes these registers, puts them in the appropriate TCP, marks the task as ready or blocked, depending. And it chooses another task, loads its own registers, and that's done. New task is running. This, however, is valid only under one condition. You don't use a floating point unit. If you use a floating point unit, there is another set of registers that need to be saved. Another 17 registers. So if you use Cortex M4, the free RTOS will require that you enable the support for a floating point unit. And it will as well deal with the floating point registers on top of the standard ones. So you need to be careful whether you enable the floating point unit or let it disabled. Additionally, when all the tasks are created and you create a new object like Q1 in this example, it's as well put on your heap. So it resides in the same heap. And you can see that by defining other objects, especially queues that can hold quite a lot of information, the amount of heap in your application can be quite quickly exhausted. So I give you one set hint. You will always fight for your memory, especially for a RAM. There is always lack of RAM for the full extent of your application. So be careful how much stack you give to your tasks and try to use as low amount as possible, but still a reliable amount. Do you have any idea how you can save some memory if you really have a lack of heap? For example, many communication tasks, a graphical task and so on. I had an application that needed a lot of memory, but not at the same time. So I was operating with graphics, but once the graphical object was created, I didn't need this memory. Then I needed memory for a communication task, again dynamically allocated and allocated quite quickly. So if I would do this in separate tasks, if I would need this memory, each of the tasks would need to have a very big stack. So instead of that, I decided to create one super task that received commands from different tasks and it used one common stack that was shared between the tasks. So there was a switch. If I get this command, please use the memory this way and when you are finished, release it. When you get this command, again use your own stack this way and then release it. So by this way, I was able to share the memory in time, still giving many different tasks the chance to operate with a big chunk of memory. So this way, I was able to save my application until I came to another way of memory allocation, but you will see later.