Hello and welcome to the Modern Embedded Systems Programming course. I'm Miro Samek, and in this lesson, I will bring the non-preemptive scheduler for the "superloop" introduced in the last two lessons to the logical conclusion. Specifically, today, I'll demonstrate and explain the professional version of such a scheduler called QV, which is available as one of the built-in kernels in the QP/C active object framework. Also, today, I will present QV and QP/C on the STM32 NUCLEO board using the popular STM32Cube development environment. Let's start today by reviewing what happened so far. In the last two lessons, #52 and #53, you've built a non-preemptive scheduler for the "superloop" with interrupts. The scheduler managed up to 32 prioritized tasks and supported a *safe* entry to the CPU low-power sleep mode. The central element of the design was the ready_set bitmask, where each bit corresponded to a task in the system. A 1-bit represented a task ready to run, while a 0-bit represented a task not ready to run. The scheduler was engaged in every pass through the "superloop." If the ready_set bitmask was 0, the scheduler called the go-to-sleep function, which entered the sleep mode atomically, that is, with interrupts still disabled, and had to enable interrupts internally. If ready_set was not zero, the scheduler quickly and deterministically found the highest-order 1-bit, cleared the bit, and called the corresponding task. The tasks were one-shot, run-to-completion functions that performed some work and returned to the "superloop" without blocking or polling. This was in contrast to the traditional real-time operating system (RTOS), where tasks were "mini-superloops" that did not return and had to block internally to wait for events. This video course has a whole segment of lessons covering the traditional RTOS. In the end, the little scheduler for the "superloop" is quite powerful for its tiny size and low complexity, but it still has shortcomings. First, the events are signaled to the tasks by just one bit in the ready_set bitmask, so if the task hasn't had a chance to run yet, any subsequent events for that task are lost. Second, the bits in the ready_set bitmask don't carry any additional data, so interrupt service routines and tasks must communicate via global variables that you must adequately protect against race conditions. Also, there is no obvious way to tell which data belongs to which event. And third, the tasks must run to completion and return to the "superloop" after every event, so they must somehow remember where the last event left off to pick up in the proper context for the next event. Such code quickly turns into "spaghetti" of tangled conditional logic. These problems are addressed in the non-preemptive QV kernel that is available in the QP/C active object framework. QV works similarly to the "superloop" scheduler but replaces the task functions with event-driven Active Objects. This video course introduced the concepts of event-driven programming and Active Objects in the dedicated segments of lessons. But to quickly refresh your memory, an Active Object encapsulates an event queue and a state machine. It processes the events from its queue, one at a time and to completion without blocking, which precisely matches the execution profile of the "superloop" scheduler. The Active Object's event queue addresses the problem of losing events because the queue stores all events in the order they occurred. Using events instead of global variables also addresses the problem of associating data with specific events because events can carry event parameters. Finally, Active Objects in the QP frameworks have internal hierarchical state machines--the best-known "spaghetti reducers." This video course discussed state machines in the dedicated segment of lessons. The QV kernel still uses the ready_set bitmask, but now a ready-bit is set when an event is posted to the corresponding Active Object's event queue and cleared only after the last event is removed from the queue. So, this is the theory behind the non-preemptive QV kernel. But now, I'd like to show you how to use it in practice. For that, I'll use the STM32 NUCLEO-C031C6 board, which is one of the boards supported by this video course along with the original TivaC LaunchPad. STM32 boards are typically used with the popular STM32Cube IDE, and consequently, I'll also use that IDE and demonstrate how to integrate the QP framework with the code generated by STM32CubeMX. In the following discussion, I assume that you already have the STM32Cube and CubeMX installed on your host computer. If not, please go to the ST.com website and get that software. The plan for today is to start with a blank STM32Cube IDE and create an STM32 project from scratch, showing you all the steps necessary to make it into a working QP application. However, please remember that the complete project is also available for download from the companion web page to this video course. As always, this link is provided in the video description below. To start, in the STM32Cube IDE click on 'Create a new STM32 project'. You'll be prompted to specify either just the microcontroller or the whole board. For this tutorial, I've chosen the NUCLEO-C031C6 board with the Cortex-M0+ CPU. Once you've selected the board, click 'Next' to proceed. Now, let's not use the default location but put the project in the directory lesson-54 and sub-directory stm32c031-cube. Also, let's name this project generically as "project." Now, you can just generate the code from these settings by clicking the "Generate Code" button in the top toolbar. As usual with Eclipse-based tools like the STM32Cube, it's a good idea to monitor what's happening on the disk because the project includes everything that is present in the project directory, whether you want it or not. Now, you can build the code by pressing the "hammer" tool in the top toolbar. The build should succeed and should create the Debug directory on the disk. Alright. Time to connect the NUCLEO-C031C6 board to your computer and debug the code. If you're running the debugger for the first time, you might need to make sure that the Eclipse "Debug Configuration" is available. Once the debugger launches and connects to the target, you can step through the code up to the obligatory while(1) superloop. However, the superloop is empty, so nothing visible happens even though the code runs correctly. So far, so good. Now, let's bring in the QP framework with the non-preemptive QV kernel, which is the subject of this video. The Cube IDE offers multiple options to incorporate such software components into your project. For example, popular RTOSes and middleware are provided as so-called "Software Packs." The QP/C framework is also available as a "Software Pack," and the two Quantum Leaps videos demonstrate how to apply it. Another option is to symbolically link external directories to the STM32 project without copying. This option is illustrated in stm32-cube examples in the QP framework. However, the simplest way for Eclipse projects is to literally copy all the software to the project directory, so I will show you this method. To get the QP/C software, you can either download the QP bundle from state-machine.com or get it from the Quantum Lepas GitHub. Once on GitHub, click on the qpc repo and go to releases. From there, choose the most recent release and download the qpc ZIP archive. Now, open the archive and unzip the qpc directory into your STM32Cube project directory like this. As I already mentioned, Eclipse projects automatically pick up everything present in the project directory, but to see the new qpc directory in your project you must execute the build. Interestingly, the build does not compile any files from the qpc folder because this new directory is "Excluded from the build." So now, you need to include the specific parts of the qpc framework in your build. For today's project, you include the following qpc sub-directories, being careful to apply it for all build configurations: src/qf - which contains the platform-independent source code for the active object framework and hierarchical state machines; src/qv - which contains the platform-independent source code for the non-preemptive QV kernel and ports/arm-cm/qv/gnu - which contains the platform-specific code for ARM Cortex-M, QV kernel, with the GNU compiler that is used in STM32Cube. When you try to build now, the selected qpc code is compiled, but you get errors about missing include files. That means you must add the qpc-related include paths to the compiler options. For this, go to the main project properties, C/C++ Build tab, Settings catogory. Here, again select "all build configurations" and choose the MCU GCC compiler tab, include-paths sub-category. Now, similarly to the existing directories in your project folder, add the following include path: qpc/include and qpc/ports/arm-cm/qv/gnu When you trigger the build now, you can still see errors, but this time, only one qp_config.h file is reported missing. This file is not part of the QP framework but rather the QP application you have yet to add. The application for today will be one of the standard examples provided in the qpc framework that you just downloaded and copied to the project. Specifically, the example is located in qpc/examples/arm-cm/real-time_nucleo-c031c6/qv. Select all the files for that example, copy them to the clipboard, and paste into the Core folder in your project. Next, adjust the file locations according to the structure established by CubeMX. Specifically, move the header files into the Inc folder and source files into the Src folder. For now, you just skip the new main.c file because the project already has the original main.c generated by CubeMX. Now, you need to merge the two main.c files. The application file consists of just a few function calls, which you copy and paste to the CubeMX main.c in the USER CODE right before the while(1) superloop. This is really all needed to initialize the framework and transfer control to it because QF_run() never returns to the original superloop. To complete the merge you still need to copy the include files into the right USER CODE section in the CubeMX main.c And finally, you can delete the merged main.c. Now, all files compile correctly, but the linking stage fails due to the multiple definitions of the SysTick_Handler ISR. Indeed, the QP application uses SysTick, and CubeMX has generated it, too, for internal timekeeping. You can reconcile this conflict by simply disabling the CubeMX code generation for the SystTick. To do this, open the project.ioc file, expand the System Core section and the NVIC sub-section. Once there, click on the Code Generation tab and uncheck the box next to Time Base System Tick Timer. As always, after making the changes, you must re-generate the CubeMX code. I'm really not sure why the CubeMX code generator has opened all main.c files located in the qpc/examples folder because they are excluded from the build. But you can just close them all via the File menu. More importantly, your project build finally works, so you have successfully integrated a non-trivial QP application with the STM32Cube project. I will explain this real-time application in the rest of this video, but one software development aspect I'd like to explain now is state machine modeling. Specifically, the state machine code for all active objects in this application has been generated by the QM modeling tool. You can edit this code manually and just ignore the modeling. However, if you'd like to use QM, you can adjust the code generation to the CubeMX code structure. As you can see, currently, all files for this model are generated in the same directory (denoted as dot) as the model file, which happens to be the Src directory. However, the app.h header file should go to the Inc directory, not the Src directory. You can easily adapt the QM code generation by adding a new directory and naming it ../Inc relative to the model file. Now, you can drag-and-drop the app.h header file there. And you can also change the order of the directories. Now you just generate code, and see that the app.h file has been re-generated. The other .c files have been all processed but were found to be unchanged, so they were not re-generated. But going back to the just finished successful build of the QP application, I can finally load it to the NUCLEO-C031C6 board and explain how it works. However, I'd like to note right away that this QP application is non-trivial because it consists of multiple interacting active objects. Consequently, my explanations might seem complicated. Still, please pay attention to the details and nuances because this is the only way to truly understand any real-time scheduler at a deeper level. One of the main tools I'll use is the cheap USB logic analyzer and the open-source pulse-view software described on the companion web page of this video course. If you'd like to follow along, here is how the logic analyzer is connected to the NUCLEO-C031C6 board. The numbers in the drawing are the pin numbers of the logic analyzer connector. So now, I reset the board and capture a free trace to see the basic behavior of this application. The top trace, labeled D1-SysTick shows the activity of the SysTick ISR, which turns the D1 line on upon the entry and off upon the exit. This creates a square wave that you can observe with the logic analyzer. The square wave repeats every 400 microseconds, which means that the SysTick ISR runs at a quite high rate of 2500 times per second. The trace righ below the SysTick labeled D3-Perodic4 corresponds to the highest-priority active object called Periodic4. As the name suggests, this AO runs periodically based on the periodic time event, which in turn is triggered from the SysTick ISR. The current period is 2 system clock ticks, but it's adjustable, as you will see later. Each time the Periodic4 state machine receives the TIMEOUT event, it executes a for loop that drives the GPIO D3 line on and off for an adjustable number of toggles. This activity emulates CPU load, which you can observe as an oscillating square wave in the logic analyzer, but in real life, this could be some CPU-bound computation. The traces labeled D4-Sporadic3 and D5-Sporadic2 correspond to the lower-priority active objects that run sporadically and have not been active in this scenario. I will explain their role later. The trace labeled D6-Periodic1 corresponds to the lowest-priority active object called Periodic1. It is similar to Periodic4, but toggles the GPIO D6 line and runs at a longer period of 5 system clock ticks. Finally, the bottom trace labeled D7-idle corresponds to the idle processing of the QV kernel, which runs at the absolute lowest priority 0. As I explained in the previous lessons 52 and 53, the idle processing is entered with interrupts still disabled and must enable interrupts before returning. In the Debug build configuration the function does not enter the low-power sleep mode, but it will do it in the Release configuration, which I will demonstrate later. The QV_onIdle function drives the GPIO D7 line high upon the entry and low right before the exit, so each spike in the logic analyzer view represents one call to the function. Now, let's trigger a more interesting behavior of this application. For that, set the trigger to the falling edge on the D0-Button line and start the logic analyzer. This time, the trace will be triggered only when you press the blue user button. Alright, so let me explain what's going on now. Again, the details of this are important to understand how the kernel works and also how active objects handle events. This scenario begins with detecting the button press in the SysTick ISR, which includes a switch debouncing code. That algorithm detects a reliable switch closure after seeing the changed GPIO input for two samples in a row. Upon detecting a button press, the code posts the two immutable SPORADIC events to the Sporadic2 active object. After the SysTick ISR completes, it returns to the idle callback, which returns to the superloop. At this point, the QV scheduler is engaged and finds out that Sporadic2 active object has events in its queue, so it dispatches the first event from the queue to the Sporadic2 state machine. That state machine handles the event by re-posting it to the higher-priority Sporadic3. The QV scheduler is engaged again and finds out that both Sporadic2 and Sporadic3 have events, but Sporadic3 is higher-priority, so the scheduler dispatches the event to the Sporadic3 state machine. That state machine handles the SPORADIC_A event by first posting a special PERIODIC event to the Periodic4 active object, and then emulating some CPU load in form of a for-loop toggling the GPIO D4 line. The logic analyzer trace illustrates an important property of this scheduler, which is technically called priority inversion. Specifically, event posting to the high-priority Periodic4 active object makes it ready to run at this instance, but due to the simplistic non-preemptive nature of the scheduler, the lower-priority Sporadic3 keeps running until it finishes its run-to-completion step. Only after that, the scheduler is engaged again and the high-priority Periodic4 processes the enqueued event. The processing consists here of changing the period of the time event and also the number of toggles emulating the CPU load incurred by the Periodic4 active object. Indeed, after that event, Periodic4 receives the time event every tick, while before, it received it only every other tick. However, this is not the end yet because Sporadic2 has still an event waiting in its queue. So now, the scheduler dispatches this event to the Sporadic2 state machine. This is SPORADIC type B event, so its processing consists of posting a special PERIODIC event to the lowest-priority Periodic1 active object and then some CPU load in form of the usual for-loop. This lengthy processing in Sporadic2 is interrupted by SysTick, which posts two time events to Periodic4 and Periodic1. But in this non-preemptive scheduler, the interrupt always returns to the originally interrupted task, so Sporadic2 continues. This is another example of priority inversion because the high-priority Periodic4 is ready to run but must wait for the completion of the lower-priority Sporadic2. So, only after Sporadic2 is done the high-priority Periodic4 is scheduled. Finally, the lowes-priority Periodic1 is scheduled twice in a row to process the two remaining events. When all events are processed, the scheduler goes back to calling the QV_onIdle callback. But speaking of the idle processing, I still promised to demonstrate the low-power sleep mode. For this, you must define the NDEBUG macro to enable the CPU-SLEEP code in the QV_onIdle callback. Let's do it only in the Release build configuration and thus leave the Debug configuration unchanged. Now, let's build the Release configuration. Before uploading this code to the NUCLEO board, you need to make sure that you use the Release build. If you do this the first time, the Release debug configuration is missing, and you must create it. Now, you can finally upload the board. Reset the board... and start the logic analyzer. As you can see, the idle trace is not toggling anymore, meaning that the QV_onIdle function is called much less frequently, thus not wasting as many CPU cycles. Moreover, most of the time the line is high, meaning that the system is inside the QV_onIdle function. Also, the interrupts preempt the "superloop" inside the QV_onIdle function, and the function returns only *after* the interrupt. Then the system processes the posted event and calls QV_onIdle() again. However, even though the idle behavior of the system is completely different, the active objects behave exactly the same. For example, here is the button press scenario again. The only difference is that the code runs now a bit faster because the Release build configuration uses higher compiler optimization than the Debug configuration before. This concludes this lesson about the non-preemptive QV kernel, which is the second example of a real-time kernel suitable for Active Objects. The first was the traditional real-time operating system (RTOS), presented in lesson 34, where you saw a rudimentary Active Object framework called uC/AO, built around uC/OS RTOS tasks structured as event loops. You can also check out another version of such a framework, built around FreeRTOS, called FreeACT. However, the options for executing Active Objects don't end here. In the following lessons, I will present yet another real-time kernel type for Active Objects, which is preemptive and fully compatible with Rate-Monotonic Scheduling/Analysis (RMA/RMS) methods introduced in lesson 26. If you like this video, please subscribe to stay tuned. This channel is getting really close to the very round number of 2-to-16th subscribers, so please help to break the 16-bit barrier! Finally, as always, all discussed projects are available for download from the companion web page and the dedicated GitHub repo. Thanks for watching!