Welcome to the Modern Embedded Systems Programming course. My name is Miro Samek and in this sixth lesson on RTOS I'll talk about the RTOS mechanisms for synchronization and communication among concurrent threads. Such mechanisms are the most complex elements of any RTOS, and are generally really tricky to develop by yourself. For that reason, today I will replace the toy MiROS RTOS with the professional-grade QXK RTOS included in the QP/C framework, parts of which you have been using since lesson 21. You will see the process of porting your existing application to a different RTOS, and once this is done, you will learn about semaphores and see how they work in practice. As usual, let's get started by making a copy of the previous lesson 26 directory and renaming it to lesson 27. Get inside the new lesson 27 directory and double-click on the uVision project "lesson" to open it. To remind you quickly what happened so far, in the last lesson you implemented preemptive priority-based scheduler and you learned about the Rate Monotonic Scheduling technique, which allows you to assign priorities to threads such that they all can meet their hard real-time deadlines. But your "blinky" threads still aren't very realistic in that they run completely independently of each other. You can compare this situation to trains running on completely independent circular tracks analogous to the endless while(1) loops of your threads. In real-life, neither threads nor trains run completely independently, but rather their tracks cross in various ways, which requires synchronization and communication to provide timely service and to avoid collisions. Trains have solved this problem with the the railroad semaphore, which can be either open or closed. If the semaphore is closed, any approaching train must stop and wait until the semaphore opens. If the semaphore is already open, any approaching train can simply pass through. The concept of a semaphore has been adapted for software already in the 1960's by the Dutch computer scientist Edsger Dijkstra. Dijkstra invented a software semaphore for a time sharing system he was designing at the time. The semaphore has been later extended to the priority-based schedulers and real-time operating systems--RTOSes, which went mainstream in the early 1980s. Today, you will see how a software semaphore works and how to use it for thread signaling. But at this point you are really reaching the limits of your home- grown MiROS RTOS, because it turns out that implementing semaphores, as well as all the other inter-thread communication mechanisms, is really complex and tricky to get right. So today, instead of implementing semaphores from scratch in the toy MIROS RTOS, I will show you how to move on to a professional-grade QXK RTOS included in the QP/C framework, parts of which you have been using since lesson 21. I use QP/C in this course, because it supports many different programming paradigms, the conventional priority-based preemptive RTOS being just one of them of interest for today's lesson. In the future lessons, you will learn about other, more modern paradigms that QP/C also supports, such as: object-oriented programming, event-driven programming, state machines, and component-based programming with active objects. The process of moving an application from one RTOS to another is called porting an application and is a valuable exercise in its own right. Porting an application is like replacing the foundation from under a house with minimal disturbance to the house itself. So, to start the porting, let's remove the MIROS files from the project and rename the group to QPC. Also, to make sure that none of the MIROS stuff is being used, let's go to the current lesson 27 project on disk and delete the files miros.h and miros.c. Next, you need to add the QPC source code to the QPC group. But first, let's make sure that you have QPC installed on your machine. You should have it in the "qpc" folder at the same level as the folders for the lessons. If you don't have QPC yet, please go back to lesson 21, where I showed how to get QPC from the companion web page to this video course state-machine.com/quickstart. Once you make sure that you have QPC installed, in the uVision IDE you right-click on the QPC group and choose "Add existing files to QPC group" pop-up menu. From there, you go one level up and go into qpc/src/qf sub- directory. You need to select all the files in the qf sub-directory, which contain the code for state machines, event-driven programming, and component-based programming. I will explain them in the future lessons. On top of this, you will need the specific RTOS kernel, which is the QXK preemptive blocking RTOS in this case. To select it, go to the qxk folder and select all the files. And finally, you will need the specific port of QXK for the ARM Cortex-M processor and the ARM-KEIL uVision toolchain you are using. For this, you go up to the ports directory and down to arm-cm, and inside there to qxk sub-directory for your specific QXK kernel. Inside the kernel directory, you see sub-directories for all supported toolchains, such as ARM-KEIL, ARM with CLANG, GNU, and IAR toolchains. As you can see, the ports directory is a bit complex, but this is typical for most professional RTOSes, which have been ported to many CPU types and toolchains. On the flip side, if you wish use a different CPU or toolchain, most likely you will find it here. So for today, you need to go inside the arm directory for the ARM- KEIL toolset you are using, and select the qxk_port.c file. The next step is to adapt your application to the new QPC RTOS, which is like adjusting a house to the new foundation. For that, you can use the compiler to show you exactly what needs to be adjusted. First, you obviously need to replace the nonexistent "miros.h" header file with "qpc.h". The next problem is that the compiler cannot find the "qf_port.h" header file. This header file is included from "qpc.h" and it needs to be taken from the same port directory, from which you took qxk_port.c. You provide this information to the compiler, by adding the ports directory to the compiler include search path, as follows: When you build the code now, you still have errors, but notice that all the files comprising the qpc source code compile correctly. The remaining errors are getting more interesting, because they are caused by the mismatch between the previous MiROS RTOS Application Programming Interface (API) and the new QXK RTOS API. The first such mismatch is the name of the thread control block OSThread. This data type still exists in QXK, but it is called QXThread instead. So, let's replace OSThread with QSThread and rebuild. The next mismatch is the OS_delay function. This function also exists in QXK, but it is called QSThread-underscore-delay. Again, let's replace the name and rebuild. Now, the compiler does not recognize the OS_init() function. The equivalent in QXK is QF_init, but QXK no longer needs the extra stack for the idle task, because it re-uses the main C stack for that purpose. This more clever design means that the idle stack space can be recovered. The next problem is the thread start functionality, which is implemented slightly differently in QXK. For reasons that will become clearer when I talk about emulating object-oriented programming in C in one of the future lessons, a thread can be started only after the QXThread object has been initialized. This initialization is accomplished by a special function called the "constructor". In this constructor, the QXThread object is associated with the thread function, and also with the specific system clock tick rate, here represented as zero. A QXK thread is started by means of the QXTHREAD_START macro (notice the all capital letters). The reason for using a macro at this point will also be explained in the lesson about object-oriented programming in C, but for now it is important only that QXTHREAD_START takes more parameters. The first two parameters are, as before, the thread object, and the thread priority. The next two parameters are the message queue buffer and size. The blinky1 thread does not use a queue at this point, but QXThreads in general can have dedicated queues. The next two parameters are the stack buffer and size, as before. And finally, the last parameter is a pointer argument passed to the thread. It will also be ignored at this point. When you build now, the compiler complains about the incompatibility of the thread function pointer passed into the QXThread constructor. This is because in QXK, a thread function has slightly different signature. It takes one parameter, called "me" by convention, which allows you to access the associated thread object inside the thread function. Of course, you need to adjust all your thread function signatures the same way. Now the compiler finally likes the initialization and starting of your first blinky1 thread, so you simply need to repeat these adjustments for all the remaining threads. The last warning in main is that the OS_run() function is not recognized. The equivalent function in QXK is QF_run(). Please note that QF_run() also never actually returns back to main. The build diagnostics now change from compilation to linking errors about functions called from bsp.c. So, let's go there and adjust it for QXK as well. The first modification needs to be done inside the SysTick interrupt handler. Here, the undefined call to the scheduler needs to be replaced with the QXK macro QXK_ISR_EXIT, which also performs preemptive scheduling inside a critical section. Additionally, QXK provides a matching QXK_ISR_ENTRY macro to be called upon the entry to the ISR, before any other QXK API call. The next undefined call to OS_tick needs to be replaced with the macro QF_TICK_X(), which services the timeouts at the specific clock tick rate, here specified as zero. This is the same clock tick rate as the one set in the thread constructors. The next category of undefined symbols originate from the QXK kernel, which means that these are callback functions defined in QXK, but not implemented there. The first of these callback function is QF_onCleanup(), which is only provided for the situation when an application exits. Deeply embedded applications like your blinky never really exit, so this callback function can be defined as empty. On the other hand, the undefined symbol QF_onStartup() must be defined as the replacement for OS_onStartup(). Here, the most important difference is that you cannot set the SysTick interrupt priority to zero, because this highest interrupt priority in ARM Cortex-M is never disabled in QXK. At this point I need to digress and explain the concept of interrupt latency and the more advanced interrupt disabling policy used in QPC compared to the simplistic MiROS RTOS. As I explained already in lesson 17 about interrupts, the interrupt requests are ASYNCHRONOUS, meaning that in general they are not correlated with the execution of the code. I also explained that the CPU can recognize an interrupt only at the instruction boundaries, after which the CPU still needs to perform interrupt entry. All this obviously takes some time. This time delay from the interrupt request to the first instruction of the interrupt service routine (ISR) is called the INTERRUPT LATENCY. But this picture still does not show all the delays contributing to the interrupt latency. As I explained in lesson 20 about race conditions, and lesson 23 about RTOS, an RTOS needs to occasionally DISABLE interrupts to prevent race conditions around its own variables. These critical sections of code, shown here as black boxes, obviously also contribute to the interrupt latency. So, at the end of the day, and as usual for REAL-TIME performance, the most interesting and important measure is the worst-case, MAXIMUM INTERRUPT LATENCY, which consists of the longest critical section, plus the interrupt entry time. But such maximum interrupt latency might be just too long for certain ISRs. If these ISRs do not make any RTOS API calls, they don't run the risk of interfering with the RTOS, so they really don't need to be penalized by the critical sections of the RTOS. This leads to the concept of "kernel unaware" interrupts, which are never disabled by the kernel, but also can never interact with the kernel. The interrupt latency of such kernel-unaware interrupts is sometimes promoted as "zero interrupt latency", which does NOT mean that such interrupts are handled instantaneously. This is impossible. It only means that the presence of the RTOS has zero- impact on the interrupt latency. In contrast, "kernel aware" interrupts can call RTOS API, but in exchange they have longer maximum interrupt latency. Of course, all this discussion is relevant only for processors that can disable interrupts selectively, so that some of the interrupts can remain enabled, while others are disabled. As it turns out, the ARM Cortex-M3, M4 and M7 CPUs, but not the Cortex-M0 or M0+, allow you to disable interrupts selectively. Specifically, instead of globally disabling all interrupts with the PRIMASK register, Cortex-M3 and higher provide also the BASEPRI register, which allows you to mask interrupts selectively only up to the specified interrupt priority level. This means that interrupts above the level set in the BASEPRI register are never disabled. The QPC port to ARM Cortex-M3 and higher CPUs, employs this more selective interrupt disabling method. As described in the Application Note "Setting ARM Cortex-M Interrupt Priorities in QP" the QP port provides a constant QF_AWARE_ISR_CMSIS_PRI, which separates kernel-aware interrupts from kernel-unaware interrupts. Here is how the two interrupt groups look for NVIC with 3-bits of interrupt priority, and here for NVIC with 4-bits of interrupt priority. So now, coming finally back to your code, you can see that leaving the SysTick priority at zero would make it to a kernel-unaware interrupt. This would be incorrect, since SysTick apparently calls QPC APIs. Therefore, SysTick must be a kernel-aware interrupt with priority QF_AWARE_ISR_CMSIS_PRI or a bigger number, which would correspond to the lower interrupt priority in Cortex-M. Please note that I also adjust the comment to explain the situation. One more build shows that the compiler accepts all our changes, but the linker still complains about the undefined OS_onIdle() symbol. This callback function the MiROS RTOS is called QXK_onIdle() in QXK and has exactly the same semantic, so nothing except the name needs to change. The code finally builds with zero errors and zero warnings, so I'm sure you are curious if this whole thing still works. After you load the program to the debugger, the interesting question is how to best verify that your RTOS application works. Well, I typically focus on the threads, interrupts, and the idle callback. When you run the program, the first breakpoint hit is inside the blinky1 thread. This seems very reasonable, since blinky1 is your highest priority thread. The next breakpoint hit is inside the SysTick_Handler, which confirms that the interrupts are serviced. Another interesting aspect is to watch how the interrupt returns. Here in the disassembly view you can see that the interrupt returns via the POP to PC instruction, which returns to a BSP function, called from the blinky1 thread. Next, you hit the breakpoint in blinky2 thread, which proves that this lower-priority thread also starts executing. But the question now is how to look into the QXK RTOS variables, since the MiROS RTOS variables are no longer valid, so you should delete them from the view. To find out which variables to watch in QXK, Open the qpc source code, include directory, qxk.h header file where at the top you can find the structure defining the global attributes of the QXK kernel. Copy the QXK_attr_ variable name to the clipboard and paste into the Watch1 window. When you expand the structure, you can see that the curr data member points to the blinky2 thread, which makes perfect sense. Finally, you reach your last breakpoint inside the idle callback. Here you can verify that the red LED on your LaunchPad board is turned on and off. When you exit the debugger and let the application run free, you can see some activity of the LEDs, but to verify that nothing really changed compared to MiROS RTOS, I will use the logic analyzer view. So, here is the same setup as in the previous lesson 26. The top trace labeled ISR shows the activity of SysTick, which fires every millisecond and toggles the TEST pin. The trace below, labeled T1, shows the activity of the blinky1 thread, which runs for about 1.2 milliseconds and toggles the Green LED. As you can see blinky1 always meets its deadline of 2 time ticks. The trace below, labeled T2, corresponds to blinky2, which toggles the Blue LED. This tread also meets its deadline of 54 time ticks. And finally, the bottom trace labeled IDL corresponds to the idle thread that toggles the Red LED in the QXK_onIdle callback. It runs only when no other thread or ISR are active. In summary, the QXK RTOS executes your application and behaves exactly as the last version of the MiROS RTOS in the previous lesson. So, now that you have verified your port to QXK, let's see what kind of problems this RTOS will allow you to solve. For example, up till now, the blinky2 thread has been based on the blocking delay() function. But suppose that you would like to base the blinking of the Blue LED on the button press instead. Specifically, the Blue LED should start toggling only after you press the SW1 switch on your LaunchPad board. In the train analogy, this problem would be solved by applying a railroad semaphore. Such a semaphore would be initially in the closed state, so any approaching train would need to wait. Pressing the button would signal the semaphore, which would release the train from waiting and let it continue around the track. The QXK RTOS kernel supports such a semaphore concept in software. In particular, to add a semaphore for signaling of switch SW1, first you need to define a semaphore object, which I will name SW1_sema, of the type QXSemaphore. To find out what QXSemaphore is, you can open the online documentation. Starting from the companion web-page to this video course, use the top menu: Products, QP/C framework. You can start typing QXSema into the search box and click on QXSemaphore. As you can see, QXSemaphore is an kernel object that consists of the QXSemaphore structure plus functions starting with QXSemahpre- underscore prefix that operate on this structure. The most important element of a semaphore is the count data member, which is an up/down-counter that keeps track of the number of times the semaphore has been signaled and waited on. The semaphore stores also the maximum count value as well as the waitSet that remembers which threads are waiting on this semaphore. The waitSet data member is similar to the ready-set bitmask introduced in the MiROS RTOS, except that in QXK it can hold up to 64 priority levels. Before a semaphore can be used, it needs to be initialized with the QXSemaphore_init() function. As shown in the usage example, a good place to perform the initialization is the top of the main function. In fact, let's just copy the usage example and paste it into your main function. QXSemaphore_init() takes the pointer to the semaphore you wish to initialize as well as the initial semaphore count and the maximum semaphore count. Most often, for signaling you would set the maximum count of one, meaning that the semaphore count would be allowed to take only two values zero, meaning that the semaphore is not signaled and one, meaning that it is signaled. Such a semaphore is called the binary semaphore. Also, initially your SW1 semaphore is not signaled, so you set the initial count to zero. Once the semaphore is initialized, you can use it to synchronize your thread. You do this with the QXSemaphore_wait() function. Again, let's just copy the usage example and paste it into your blinky2 thread function. The wait function takes the pointer to your semaphore object, and also allows you to specify a timeout for how long you wish to wait on the semaphore. If you are willing to wait indefinitely, you use a special timeout value QXTHREAD_NO_TIMEOUT. Also now, that you have the semaphore as the blocking mechanism, you don't need the blocking delay anymore, so you can delete it. At this point, you are done with the waiting part of the problem and you can check whether your code compiles. But you still need to implement the signaling of the semaphore when the SW1 button is pressed. Let's start with taking a look at how this SW1 button is connected to your TivaC microcontroller. For this, you can go to the companion wep-page to this course, and click on the Tiva LaunchPad User Manual. In the Table of Contents, scroll down to the Schematics section, where on the second page you can find the SW1 switch, and trace its connection to the PF4 pin, which stands for Pin-4 in the GPIO-F group. With this information, you can now go to the board support package bsp.c, which is the logical place for any code specific to the board. You define here the BTN_SW1 pin as bit-4, similar as you did for the LEDs. Notice, however, that pin-4 in the GPIO-F group is already used as the TEST_PIN, that the SysTick handler toggles for the logic analyzer view. This is obviously a conflict, so let's just remove all uses of the TEST_PIN from the code. Instead, let's configure the BTN_SW1 pin similarly as you did for the LEDs. the pin direction should be input and the pin should be configured as digital additionally, since this is an input pin, it needs to be configured with the pull-up resistor enabled. Pull-up resistor enabled means that the pin will be normally at the high-level, while pressing of the SW1 switch will bring it down to the low-level. The final part of the GPIO pin configuration is setting it up to generate interrupts to the CPU. Here I simply copy and paste the code and let you read the comments and possibly also consult the TivaC Datasheet. The only thing I'd like to note is that to sense the pressing the SW1 button, the MCU needs to detect the falling edge in the voltage supplied to the pin. Now you are finally ready to write the GPIO interrupt handler for the SW1 switch. This would be your first ISR unrelated to the clock tick, because so far you've been only re-using the SysTick interrupt introduced already back in lesson 16. But actually, let's begin with copying and pasting SysTick as your starting point. The first thing you need to change is the name, which you can look up in the startup code, inside the interrupt vector table. Inside the ISR body, you leave the QXK_ISR_ENTRY and QXK_ISR_EXIT macros, because this will definitely be a "kernel aware" interrupt. But you need to delete the QF_TICK call and replace it with the signaling on the semaphore. However, before you can signal the semaphore, you need to make sure that the interrupt is coming from the specific SW1 pin, because this ISR will also fire for other pins of GPIO-F, if they are configured to trigger this interrupt. Also, after detecting the source of the interrupt, this GPIO peripheral needs to be explicitly cleared in software, so that it is ready for the next interrupt. So, finally, inside the if-statement, you can signal the semaphore by means of the QXSemaphore_signal() function. The only parameter you need to supply here is the pointer to the semaphore object. But you are not quite done with this interrupt yet. As I explained earlier for SysTick, the very important step for any "kernel aware" interrupt is to explicitly set its priority to be below the "kernel aware" level. You configure the interrupt priority in the QF_onStartup() callback. The GPIOF priority can be the same as SysTick, or perhaps one level lower, which means bigger priority number in Cortex-M. And one last step for the GPIO interrupt is to explicitly enable it in the NVIC, as follows. When you try to build the code, the compiler complains that the SW1_sema object is undefined. Well, indeed, the semaphore object is defined only in main.c, and so bsp.c does not know about it. You can easily fix it by placing the declaration of the semaphore object into bsp.h header file, which is included in both main.c and bsp.c. However, when build the code again, the compiler still complains, but this time about the QXSemaphore type being undefined. This type is defined in the qpc.h header file, so you can fix this problem by making sure that bsp.h is included after qpc.h. While you are at it, you can also remove the stdint.h header file, because it also is already included from qpc.h. One more build, and... Hallelujah! the code finally builds with zero errors and zero warnings! I'm not sure about you, but I'm really curious how the code will work. So, let's load the code into the board and open the debugger to perform the basic sanity checks. To do this, I set breakpoints at the most important junctures, that is, at semaphore wait inside blinky2 and semaphore signal inside the GIPO ISR. When I run the code, the first breakpoint hit is at the semaphore wait. This makes sense, because the blinky2 thread should be blocked on the semaphore. To verify that the thread is indeed blocked I move the breakpoint to the first instruction AFTER the semaphore wait. When I continue now, the program runs without hitting any breakpoints. This makes sense again, because the semaphore has blocked the thread so the breakpoint past the wait call cannot be reached. Now, I press the SW1 button and... As you can see, the code hits the breakpoint at semaphore signal inside the GPIO ISR. This is excellent, because it means that the GPIO interrupt has been configured correctly. When you continue from signaling the semaphore, you immediately hit the breakpoint inside blinky2. This means that the semaphore wait has been unblocked and the code post it started to run. When you run the program from here, again no breakpoint is hit... until SW1 is pressed again At which point the breakpoint at semaphore signal is hit. When you continue, again you immediately hit the breakpoint past semaphore wait. This proves that indeed the execution of the blinky2 thread is synchronized with signaling the semaphore and, by this mechanism, to the pressing of the SW1 button. OK, so let's run the code free and leave the debugger to inspect the timing of this code in more detail using the logic analyzer. When you open the logic analyzer with the identical setup as before, ... at first you don't see any activity. But, let's change the trigger to AUTO. And now, as you can see, the Green-LED is toggled from your blinky-1 thread as before, and so is the Red-LED toggled from the idle callback. On the other hand, the ISR pin is not toggling, but rather is stuck high. And also the Blue-LED is not active at all. But this is exactly what you changed. In particular, the previous trigger was setup to use the rising edge of Pin-4, which was toggled from the SysTick interrupt. Now this pin is used for the SW1 switch, which, as you can see, is normally high and should go low only when you press the switch. Therefore, let's adjust the trigger to the falling edge of pin-4, which as you recall, is also the trigger for your GPIO-F interrupt. So, let's run with these adjustments while repeatedly pressing the SW1 button. As you go through the collected traces, you can see that most of the time, pressing of the button caused the ISR line to go low and the Blue-LED started to toggle for about 8.5 milliseonds, give or take a few milliseconds for preemption. When the button press came while the blinky-1 thread was not running, the blinky-2 thread started to toggle the Blue-LED immediately. However, when the button press happened while blinky-1 was running, the blinky-2 thread had to wait until blinky-1 voluntarily blocked. This is because QXK is a preemptive, priority-based kernel, fully compliant with Rate Monotonic Scheduling (RMS), and so it always executed the higher-priority blinky-1 thread as long as it wanted to run, before executing the lower-priority blinky-2 thread. For today's lesson, which is about understanding semaphores, I'd like you to remember that unblocking of a semaphore that is waiting inside a lower-priority thread might be delayed by all higher- priority threads. Now, when I was pressing the SW1 switch and collecting the traces, you might have noticed some anomalies. For example, in trace 12, you can see that the blinky-2 thread is running twice as long as usual. I'm sure you want to know why. Well, when you zoom in, you can see some that the falling edge of the ISR line is not clean, but rather shows some strange spikes. These spikes are even better visible in the analog waveform of the same signal in the upper part of the plot, which I specifically provided for today by probing the PF-4 pin from the bottom of the board. Well, it turns out that this is a well know property of all mechanical switches, which sometimes momentarily bounce before establishing a permanent contact. The problem is that to the fast CPU, these bounces look like multiple presses and releases of the switch, instead of the expected single press or single release. The subject of filtering out the noise created by mechanical switches is quite interesting, and you can read about it by searching the web for "debouncing". Let me only mention here that you should definitely NOT release any production-quality software with the current implementation, and instead you should properly "debounce" all your switches in software. However, for this lesson, I chose exactly to use the noisy signal, because it provides more extreme stress test for your semaphore, and in doing so, it will allow you to gain a deeper understanding of how a semaphore works. Specifically, in this particular situation you see three falling edges in the ISR line, which means that the semaphore was signalled three times from the GPIO-F interrupt. However, the blinky-2 thread went only 2 times around its while(1) loop. So, let's find out why. A good way of thinking about a semaphore is that it is a protocol of exchanging tokens according to the following rules: The signal operation adds a token to the semaphore, but only up to the maximum configured count number. The wait operation removes a token from a semaphore if any tokens are available, or it blocks, if no tokens are available. So, let's see how this works in this particular case. Before the switch was pressed, the semaphore had no tokens, so the blinky-2 thread blocked on the semaphore-wait operation. The first falling edge caused the GPIO-F interrupt to signal the semaphore, which added one token. This has immediately unblocked the blinky-2 thread, which still inside the wait-operation took the token out of the semaphore. The next bounce, caused the the GPIO-F ISR to signal the semaphore, which again added one token to the empty semaphore. But this time, the blinky-2 thread was already running and didn't yet execute the semaphore wait-operation to remove the token. So, the token stayed in the semaphore. The third bounce, caused the GPIO-F ISR to signal the semaphore again, but this time the semaphore already had one token and could not accept another, because it was configured as a binary semaphore -- with the capacity to hold at most one token. The bliny-2 thread kept running and looped back to the top of its while(1) loop and called the semaphore wait operation. This time the token was immediately available in the semaphore, so the wait-operation just removed the token, but didn't block. Blinky- 2 continued through the loop the second time. Eventually, blinky-2 looped back to the top of its while(1) loop and called semaphore-wait operation for the third time. This time, however, the semaphore was empty and so blinky-2 blocked. But this is only one way the scenario can play out. It turns out that due to thread preemption, essentially the same three bounces of the switch can lead to quite a different outcome of blinky-2 going just once though its while(1) loop instead of twice. An example of such a scenario is trace number 6. Here the switch bounced while it was released, as opposed to being depressed, but still it produced three falling edges and, consequently the semaphore was signaled three times. But, let's analyze this trace in detail. As in the previous case, initially the semaphore had no tokens, so the blinky-2 thread blocked on the semaphore-wait operation. The first falling edge caused the GPIO-F interrupt to signal the semaphore, which added one token. But this time, the blinky-2 thread could not run immediately, because it could not preempt the higher-priority blinky-1, which happened to be running. Therefore, the wait operation was NOT performed, and the token was NOT removed from the semaphore. The next falling edge caused the GPIO-F interrupt to signal the semaphore, but this time the semaphore already had one token and could not accept another. The same exact thing happened by the third bounce of the switch as well. The high-priority blinky-1 thread kept running, but finally blocked on the delay() operation. Only at this point the preemptive QXK kernel scheduled blinky-2, which unblocked and removed the token from the semaphore. Finally, blinky-2 looped back to the top of its while(1) loop and called the semaphore-wait operation. This time, the semaphore was empty and so blinky-2 blocked. This concludes this lesson on semaphores as the first of the many inter-thread synchronization mechanisms you need to learn. In the next lesson, I'm going to talk about sharing of resources among threads, and about the RTOS mechanisms for guaranteeing the mutually exclusive access to such shared resources. If you like this channel, please subscribe to stay tuned. You can also visit state-machine.com/quickstart for the class notes and project file downloads.