{"id":143,"date":"2025-09-24T09:53:20","date_gmt":"2025-09-24T07:53:20","guid":{"rendered":"https:\/\/microsproject.dev\/?p=143"},"modified":"2025-09-25T14:09:09","modified_gmt":"2025-09-25T12:09:09","slug":"micros-internals-yielding-with-systick-and-pendsv","status":"publish","type":"post","link":"https:\/\/microsproject.dev\/index.php\/2025\/09\/24\/micros-internals-yielding-with-systick-and-pendsv\/","title":{"rendered":"\u2699\ufe0f MicrOS Internals: Yielding with SysTick and PendSV"},"content":{"rendered":"\n<p>In the last article, we looked at how PendSV swaps thread contexts using prebuilt stack frames. Now it\u2019s time to give our kernel a proper <strong>yield mechanism<\/strong> \u2014 so threads can give up the CPU voluntarily, or automatically via a timer tick.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Yield API<\/h2>\n\n\n\n<p>MicrOS provides <code>task_yield()<\/code> as the official way to request a context switch.<br>It simply sets the <strong>PendSV pending bit<\/strong> in the System Control Block:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#include &lt;stdint.h>\n#include \"sched.h\"\n\n#define SCB_ICSR   (*(volatile uint32_t *)0xE000ED04)\n#define PENDSVSET  (1UL &lt;&lt; 28)\n\nvoid task_yield(void) {\n    SCB_ICSR = PENDSVSET;   \/\/ trigger PendSV\n}\n<\/code><\/pre>\n\n\n\n<p>This is the entire API: calling it doesn\u2019t immediately switch, but ensures PendSV will run as soon as possible.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Yielding from SysTick<\/h2>\n\n\n\n<p>To demonstrate preemption, let\u2019s connect <code>SysTick<\/code> with <code>task_yield()<\/code>.<br>Instead of switching every single tick, we\u2019ll yield <strong>every 10 ticks<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#include \"sched.h\"\n#include \"cmsis_device.h\"   \/\/ CMSIS header for SysTick\n\nstatic volatile int tick_count = 0;\n\nvoid SysTick_Handler(void) {\n    tick_count++;\n    if (tick_count >= 10) {\n        tick_count = 0;\n        task_yield();       \/\/ request context switch\n    }\n}\n\nvoid system_init(void) {\n    SysTick->LOAD  = (SystemCoreClock \/ 1000) - 1; \/\/ 1 ms period\n    SysTick->VAL   = 0;\n    SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |\n                     SysTick_CTRL_TICKINT_Msk   |\n                     SysTick_CTRL_ENABLE_Msk;\n}\n<\/code><\/pre>\n\n\n\n<p>This way, tasks get time slices of 10 ms each before the scheduler rotates them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Priority Setup<\/h2>\n\n\n\n<p>PendSV must be lowest priority to prevent it from preempting ISRs. Configure it once during boot:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#define SCB_SHPR3   (*(volatile uint32_t *)0xE000ED20)\n\nvoid pend_sv_init(void) {\n    SCB_SHPR3 |= (0xFFU &lt;&lt; 16);  \/\/ PendSV priority = lowest\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A Test Example<\/h2>\n\n\n\n<p>Two simple tasks alternating every 10 ms slice:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#include \"thread.h\"\n#include \"sched.h\"\n\nvoid taskA(void *arg) {\n    while (1) {\n        \/\/ pretend work\n        for (volatile int i = 0; i &lt; 100000; i++);\n    }\n}\n\nvoid taskB(void *arg) {\n    while (1) {\n        \/\/ pretend work\n        for (volatile int i = 0; i &lt; 100000; i++);\n    }\n}\n\nint main(void) {\n    pend_sv_init();\n    system_init();\n\n    micros_thread_create(0, taskA, 0);\n    micros_thread_create(1, taskB, 0);\n\n    \/\/ start first thread\n    micros_current = 0;\n    __set_PSP((uint32_t)micros_threads&#91;0].sp);\n    task_yield();   \/\/ manually kick off PendSV\n\n    while (1) {}\n}\n<\/code><\/pre>\n\n\n\n<p>In QEMU, you can watch registers or add <code>printf()<\/code> in each task to see them swap every ~10 ms.<\/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<ol class=\"wp-block-list\">\n<li><strong>SysTick<\/strong> fires every millisecond.<\/li>\n\n\n\n<li>A counter increments inside <code>SysTick_Handler<\/code>.<\/li>\n\n\n\n<li>Every 10th tick, it calls <code>task_yield()<\/code>.<\/li>\n\n\n\n<li><code>task_yield()<\/code> sets the PendSV pending bit.<\/li>\n\n\n\n<li>When interrupts unwind, PendSV runs, saves the old thread\u2019s context, and restores the next one.<\/li>\n\n\n\n<li>Control resumes in the other task.<\/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\">Takeaway<\/h2>\n\n\n\n<p>With less than 50 lines of code, we established <strong>preemptive multitasking<\/strong> driven by <code>task_yield()<\/code> and <code>SysTick<\/code>. Tasks don\u2019t need to know about the scheduler \u2014 they automatically rotate based on time slices.<\/p>\n\n\n\n<p>This is the building block for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>round-robin scheduling<\/strong>,<\/li>\n\n\n\n<li><strong>priority-based preemption<\/strong>, and eventually<\/li>\n\n\n\n<li><strong>sleep\/wake mechanisms<\/strong>.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In the last article, we looked at how PendSV swaps thread contexts using prebuilt stack frames. Now it\u2019s time to give our kernel a proper yield mechanism \u2014 so threads can give up the CPU voluntarily, or automatically via a timer tick. The Yield API MicrOS provides task_yield() as the official way to request a [&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-143","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\/143","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=143"}],"version-history":[{"count":1,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/posts\/143\/revisions"}],"predecessor-version":[{"id":144,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/posts\/143\/revisions\/144"}],"wp:attachment":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/media?parent=143"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/categories?post=143"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/tags?post=143"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}