{"id":141,"date":"2025-09-24T09:44:42","date_gmt":"2025-09-24T07:44:42","guid":{"rendered":"https:\/\/microsproject.dev\/?p=141"},"modified":"2025-09-25T14:09:09","modified_gmt":"2025-09-25T12:09:09","slug":"micros-internals-how-stack-frames-and-pendsv-drive-context-switching","status":"publish","type":"post","link":"https:\/\/microsproject.dev\/index.php\/2025\/09\/24\/micros-internals-how-stack-frames-and-pendsv-drive-context-switching\/","title":{"rendered":"MicrOS Internals: How Stack Frames and PendSV Drive Context Switching"},"content":{"rendered":"\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>One of the most magical parts of an RTOS is when the CPU suddenly stops running one function and starts running another \u2014 as if by sleight of hand. In <strong>MicrOS<\/strong>, this magic is powered by the <strong>PendSV exception<\/strong> and carefully constructed <strong>stack frames<\/strong> for each thread.<\/p>\n\n\n\n<p>This post goes under the hood and shows how threads are \u201cbootstrapped\u201d and how PendSV safely swaps them, cycle after cycle.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Hardware\u2019s Role: The Automatic Exception Frame<\/h2>\n\n\n\n<p>On Cortex-M, any exception (IRQ, SysTick, PendSV) triggers <strong>automatic stacking<\/strong>. The hardware pushes these 8 words onto the current stack pointer (PSP if in thread mode, MSP if in handler mode):<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Offset<\/th><th>Register<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>0x00<\/td><td>R0<\/td><td>Argument register<\/td><\/tr><tr><td>0x04<\/td><td>R1<\/td><td>Volatile<\/td><\/tr><tr><td>0x08<\/td><td>R2<\/td><td>Volatile<\/td><\/tr><tr><td>0x0C<\/td><td>R3<\/td><td>Volatile<\/td><\/tr><tr><td>0x10<\/td><td>R12<\/td><td>Scratch<\/td><\/tr><tr><td>0x14<\/td><td>LR<\/td><td>Link register<\/td><\/tr><tr><td>0x18<\/td><td>PC<\/td><td>Resume address<\/td><\/tr><tr><td>0x1C<\/td><td>xPSR<\/td><td>Program status word (must have Thumb bit set)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>This is called the <strong>exception stack frame<\/strong>. When the exception returns, the CPU automatically <strong>pops<\/strong> these values back, restoring execution exactly where it left off.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Creating a Task: Building the Initial Frame<\/h2>\n\n\n\n<p>When MicrOS creates a thread, it doesn\u2019t just give it a stack. It <strong>pre-loads<\/strong> the stack with a fake exception frame so that when the scheduler runs it for the first time, the CPU \u201creturns\u201d directly into the task function.<\/p>\n\n\n\n<p>For example, in <code>micros_thread_create()<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>High memory\n+--------------------+\n|    free stack      |\n|        ...         |\n+--------------------+\n| R11 (dummy)        |\n| R10                |\n| R9                 |\n| R8                 |\n| R7                 |\n| R6                 |\n| R5                 |\n| R4                 |  &lt;- SW-saved area\n+--------------------+\n| R0 = arg           |\n| R1 (dummy)         |\n| R2 (dummy)         |\n| R3 (dummy)         |\n| R12 (dummy)        |\n| LR = 0xFFFFFFFD    |  &lt;- tells CPU: return to thread, use PSP\n| PC = entry()       |  &lt;- task entry point\n| xPSR = 0x01000000  |  &lt;- Thumb bit set\n+--------------------+\nLow memory\n<\/code><\/pre>\n\n\n\n<p>The very first exception return the CPU executes lands inside your task function, with the argument already loaded into R0.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Switching Context in PendSV<\/h2>\n\n\n\n<p>Now, how do we swap between two such prepared contexts? Enter <strong>PendSV_Handler<\/strong>.<\/p>\n\n\n\n<p>MicrOS\u2019s PendSV does:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Save callee-saved registers<\/strong>\n<ul class=\"wp-block-list\">\n<li>On exception entry, hardware already pushed <code>R0\u2013R3, R12, LR, PC, xPSR<\/code>.<\/li>\n\n\n\n<li>PendSV saves the remaining <code>R4\u2013R11<\/code> onto the PSP.<\/li>\n\n\n\n<li>Now the outgoing task\u2019s stack contains a full snapshot of its CPU state.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Call the scheduler in C<\/strong>\n<ul class=\"wp-block-list\">\n<li>The saved PSP is passed into <code>micros_context_switch(old_sp)<\/code>.<\/li>\n\n\n\n<li>That function updates the TCB (<code>thread->sp = old_sp<\/code>) and chooses the next runnable thread.<\/li>\n\n\n\n<li>It returns the PSP of the new thread.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Restore callee-saved registers<\/strong>\n<ul class=\"wp-block-list\">\n<li>PendSV pops <code>R4\u2013R11<\/code> from the new thread\u2019s stack.<\/li>\n\n\n\n<li>PSP is updated to point to the new thread\u2019s stack frame.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Return from exception<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>BX lr<\/code> exits PendSV.<\/li>\n\n\n\n<li>Hardware automatically pops the rest of the frame (<code>R0\u2013R3, R12, LR, PC, xPSR<\/code>) from the PSP.<\/li>\n\n\n\n<li>The CPU continues execution in the new thread.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Visualizing the Context Switch<\/h2>\n\n\n\n<p>Let\u2019s see the stack layouts for <strong>Thread A<\/strong> (outgoing) and <strong>Thread B<\/strong> (incoming):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Thread A stack before switch (PSP):\n   +--------------------+\n   | R11                |\n   | R10                |\n   | ...                |\n   | R4                 |  &lt;- saved by PendSV\n   +--------------------+\n   | R0                 |\n   | R1                 |\n   | R2                 |\n   | R3                 |\n   | R12                |\n   | LR                 |\n   | PC                 |\n   | xPSR               |  &lt;- stacked by HW\n   +--------------------+\n   PSP -&gt; bottom\n\nThread B stack ready to run (PSP):\n   +--------------------+\n   | R11                |\n   | R10                |\n   | ...                |\n   | R4                 |  &lt;- prebuilt by create()\n   +--------------------+\n   | R0 = arg           |\n   | R1..R3 dummy       |\n   | R12 dummy          |\n   | LR = 0xFFFFFFFD    |\n   | PC = entry()       |\n   | xPSR = 0x01000000  |\n   +--------------------+\n   PSP -&gt; bottom\n<\/code><\/pre>\n\n\n\n<p>PendSV saves A\u2019s R4\u2013R11, calls the scheduler, loads B\u2019s PSP, restores B\u2019s R4\u2013R11, and returns. Hardware pops the rest of B\u2019s frame \u2014 and suddenly <strong>Thread B is running<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Subtleties: LR and PSP<\/h2>\n\n\n\n<p>Two registers are especially important:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>LR (EXC_RETURN)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Value <code>0xFFFFFFFD<\/code> means: <em>return to Thread mode, use PSP, pop registers<\/em>.<\/li>\n\n\n\n<li>If wrong, the CPU may try to restore from MSP or crash with a HardFault.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>PSP (Process Stack Pointer)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Always points to the top of the current thread\u2019s frame.<\/li>\n\n\n\n<li>Updated in the TCB during <code>micros_context_switch()<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>A mismatch here is the #1 reason for \u201clockup\u201d errors in QEMU.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Works<\/h2>\n\n\n\n<p>The beauty of Cortex-M is that the CPU does half the work. Hardware saves and restores volatile registers automatically; the RTOS only needs to manage the callee-saved ones (<code>R4\u2013R11<\/code>) and PSP bookkeeping.<\/p>\n\n\n\n<p>This is why MicrOS can implement multitasking with just a few dozen lines of C and a small naked handler.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Takeaway<\/h2>\n\n\n\n<p>In MicrOS today:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Each task is born with a prebuilt exception frame.<\/li>\n\n\n\n<li>PendSV saves the old frame, picks a new one, and restores it.<\/li>\n\n\n\n<li>LR and PSP hold the key to correct execution flow.<\/li>\n<\/ul>\n\n\n\n<p>Understanding this mechanism makes context switching less of a black box and more of a predictable, elegant process.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udc49 Next post: wiring <strong>SysTick + PendSV<\/strong> together for preemptive round-robin scheduling in MicrOS.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>One of the most magical parts of an RTOS is when the CPU suddenly stops running one function and starts running another \u2014 as if by sleight of hand. In MicrOS, this magic is powered by the PendSV exception and carefully constructed stack frames for each thread. This post goes under the hood and shows [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,6],"tags":[],"class_list":["post-141","post","type-post","status-publish","format-standard","hentry","category-context-switch","category-development"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/posts\/141","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/comments?post=141"}],"version-history":[{"count":1,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/posts\/141\/revisions"}],"predecessor-version":[{"id":142,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/posts\/141\/revisions\/142"}],"wp:attachment":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/media?parent=141"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/categories?post=141"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/tags?post=141"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}