Skip to main content

Playground · research instrument

On-device AI

RTOS Scheduling Simulator

Preemptive RMS vs EDF playground: edit tasks (T,C,D,mutex), watch Gantt of Running/Ready/Blocked states, priority inheritance chains, ISR nesting, and live schedulability analysis. Tension audio (220Hz vs 231Hz beating) rises with deadline miss ratio.

Independent research instrument — not claimed as MakerPortal shipped product code. Methods, equations, assumptions, and limitations are disclosed so you can inspect what the page does and does not establish.

Tension 0%

The task set, scheduler and ISR load are encoded in the URL — share a link to a configuration that misses its deadline.

Tasks — T period, C WCET, D deadline

Auto prio RMS: shorter T = higher. Manual 1..99 (99 high) overrides. Mutex M1/M2/M3 shows blocking. cs 20% offset 40% dur default.

U ΣC/T
LL bound n(2^(1/n)-1)
Misses / Jobs
Worst blocking ms
Avg jitter ms
Idle %
Status

Blocking chains

Running Ready Blocked ISR Idle

Gantt timeline — preemptive + inheritance

t=0 ms
X: time ms (tick 1ms)Y: tasks (color = task)Dashed = deadline, Red X = miss
SponsorSponsor this simulator. Reach engineering practitioners.
Details ↗
CPU Util
Deadline misses
0
Cursor response R

How to read: Each row is a task timeline. Segment colors: solid Running (task color), amber Ready waiting for CPU, red Blocked on mutex (label e.g., M1 held by T2). Top darker row is global running. Vertical dashed lines are per-job deadlines. Red markers are misses. Priority inheritance bubbles pulse when low-prio task inherits high priority. ISR row (purple) preempts always.

Anatomy of an RTOS

TCB, ready queue, scheduler, mutex

  1. TCB (Task Control Block): id, T period, C WCET, D relative deadline, basePrio, mutex M, csStart/Dur, stack pointer, holding set, blockedOn, effective prio/deadline.
  2. Ready queue: intrusively linked list sorted by effective priority (RMS) or effective deadline (EDF). O(n) insert, O(1) pop top. Tick interrupt 1ms moves released jobs from sleep list to ready.
  3. Scheduler: preemptive: if top ready effPrio > running effPrio → context switch. ISR entry pushes context, raises BASEPRI mask.
  4. Mutex & PI: each mutex has holder + FIFO/prio queue. On block, holder.effPrio = max(holder.effPrio, waiter.effPrio). Chain propagates via fixpoint. Release wakes highest waiter.
  5. ISR nesting: ISR job taskId -1, prio 1e9, T_isr period, C_isr ~2ms. If new ISR arrives while one runs, queues (same priority FIFO) showing nested purple burst.

Effective priority inheritance

Pieff=max(Pibase,maxk:k waits for μheld(i)Pkeff)P^{eff}_i = \max\left(P^{base}_i, \max_{k: k \text{ waits for } \mu \in held(i)} P^{eff}_k\right)

Fixpoint iteration propagates along blocking chains, bounding blocking to one CS under PCP.

Visual-audio coupling

  • Gantt per-task rows: 36px high, 1ms = 3-5px depending on horizon. Running = solid task color, Ready = amber @45% alpha, Blocked = red stripe.
  • Mutex hold indicator: lock glyph 🔒M1 drawn on Running segment where CS active, with small duration bar.
  • Deadline dashed vertical per job per task; miss renders red X and tints row background pink transient.
  • Blocking chain list: each block event logs waiter → holder (M). Canvas draws bezier arrow between rows for current cursor time if blocking active.
  • Audio: two oscillators f1=220Hz, f2=330−m·99Hz (m=miss ratio). Beating f_beat=|f1−f2|. At m=1 →110Hz→11Hz roughness → perceptual tension. Gain 0.02+m·0.14.

Beating roughness

f1=220Hz,  f2=33099m,  fbeat=f1f2,  A=0.02+0.14mf_1=220\,\text{Hz},\; f_2=330-99\cdot m,\; f_{beat}=|f_1-f_2|,\; A=0.02+0.14 m

m ∈ [0,1] miss ratio. 11Hz beating at m=1 sits in maximal dissonance ~15Hz per Helmholtz.

Equations & schedulability

Utilization

U=i=1nCiTiU=\sum_{i=1}^{n}\frac{C_i}{T_i}

Necessary condition for any scheduler: U ≤1 (EDF optimal). RMS needs stricter bound.

LL bound: Ull=n(21/n1)U_{ll}=n(2^{1/n}-1)

Response Time Analysis

Ri=Ci+Bi+jhp(i)RiTjCjR_i = C_i + B_i + \sum_{j\in hp(i)} \left\lceil\frac{R_i}{T_j}\right\rceil C_j

Iterate R_i(k+1) until convergence. If R_i ≤ D_i for all i, schedulable. B_i is worst blocking from lower priority tasks.

EDF & SRP ceiling

dieff=min(di,minkblockedBy(i)dkeff)d_i^{eff}=\min(d_i,\min_{k\in blockedBy(i)} d_k^{eff})

Stack Resource Policy: preemption level π_i, system ceiling Π = max locked resource ceiling. Job blocks if π_i ≤ Π.

beating: y(t)=sin(2πf1t)+sin(2πf2t)=2sin(π(f1+f2)t)cos(π(f1f2)t)\text{beating: }y(t)=\sin(2\pi f_1t)+\sin(2\pi f_2t)=2\sin(\pi(f_1+f_2)t)\cos(\pi(f_1-f_2)t)

Worst blocking chain example

T_high(5ms/1ms) needs M1 held by T_low(100ms/20ms, CS 8ms). Without PI, T_med(20ms/5ms) preempts T_low for 60ms → H blocks 68ms → miss. With PI, T_low inherits H prio → blocks only 8ms. Simulator shows red blocked segment length difference.

ISR nesting

ISR period T_isr with WCET C_isr. Utilization U_isr = C_isr/T_isr adds to total U. If T_isr < C_isr, instances queue (nest depth >1). Best practice: ISR defers to task via semaphore, keep C_isr ≤ 0.2·T_task_min.

Gear behind this build

RTOS stack · 14 picks

Hardware picks14

More gear across every app: the full Gear list →

Hardware Kit Builder

Build the physical hardware platform. Select your components below to generate a live, real-time bill of materials and build instructions.

Build this lab

RTOS lab stack (SparkFun-first)

Teensy 4.0 + RedBoard Plus + logic analyzer — schedule real tasks after you tune the simulator. Prefer SparkFun Originals (RedBoard) for the 10% path.

Prices shown were retrieved from the Amazon Product Advertising API on 19 July 2026 and are indicative only — the price and availability on Amazon at the time of purchase apply.

Estimated total

$66

Prices from Amazon catalog cache · may change

Open primary listing ↗

Kit Total

Buy ↗

Core solver — TypeScript

// RTOS core model: TCB, ready queue, PI inheritance, RMS/EDF
type TaskDef = {
  id:number; name:string;
  T:number; C:number; D:number;       // period, WCET, deadline ms
  basePrio:number;                     // RMS: 1/T, manual: 1..99 mapped
  mutex:string|null;
  csStart:number; // fraction 0..1 where lock attempted
  csDur:number;   // fraction held
  color:string;
};
type Job = {
  jobId:number; taskId:number;
  release:number; absDeadline:number;
  executed:number; remaining:number;
  state:'Ready'|'Running'|'Blocked'|'Done';
  blockedOn:string|null;
  holds:Set<string>;
  basePrio:number; effPrio:number;
  effDeadline:number;
  blockedTime:number;
  firstStart:number|null;
  completion:number|null;
  missed:boolean;
};

function basePriority(t:TaskDef, mode:'RMS'|'EDF'){
  if(t.basePrio>=100) return t.basePrio; // manual mapped 100..9900
  return mode==='RMS' ? (1_000_000 / t.T) : (1_000_000 / t.T); // rate monotonic base
}

function simulate(tasks:TaskDef[], horizon:number, mode:'RMS'|'EDF', isrPeriod:number, isrC:number){
  const mutexState = new Map<string,{holder:Job|null, queue:Job[]}>();
  let jobs:Job[]=[]; let jid=0;
  const log:number[] = new Array(horizon).fill(-1);
  const perTask: Record<number, Job['state'][]> = {};
  tasks.forEach(t=> perTask[t.id]= new Array(horizon).fill('Idle' as any));
  perTask[-1] = new Array(horizon).fill('Idle' as any); // the ISR's own row
  const events:{t:number, waiter:number, holder:number, mutex:string}[]=[];
  let totalMiss=0;

  for(let t=0; t<horizon; t++){
    // releases
    tasks.forEach(td=>{
      if(t % td.T===0){
        const j:Job={
          jobId:jid++, taskId:td.id, release:t, absDeadline:t+td.D,
          executed:0, remaining:td.C, state:'Ready',
          blockedOn:null, holds:new Set(), basePrio:basePriority(td,mode),
          effPrio:basePriority(td,mode), effDeadline:t+td.D,
          blockedTime:0, firstStart:null, completion:null, missed:false
        };
        jobs.push(j);
      }
    });
    if(isrPeriod>0 && t % isrPeriod===0){
      const ij:Job={
        jobId:jid++, taskId:-1, release:t, absDeadline:t+isrPeriod,
        executed:0, remaining:isrC, state:'Ready', blockedOn:null,
        holds:new Set(), basePrio:1e9, effPrio:1e9, effDeadline:t+isrPeriod,
        blockedTime:0, firstStart:null, completion:null, missed:false
      };
      jobs.push(ij);
    }

    // active jobs
    const active = jobs.filter(j=> j.release<=t && j.remaining>0);

    // try unblock / release expired CS
    active.forEach(j=>{
      if(j.taskId===-1) return;
      const td = tasks.find(x=>x.id===j.taskId);
      if(!td || !td.mutex) return;
      const m = td.mutex;
      // release check
      if(j.holds.has(m)){
        const endFrac = td.csStart+td.csDur;
        if(j.executed >= endFrac*td.C){
          j.holds.delete(m);
          const st = mutexState.get(m);
          if(st) st.holder=null;
        }
      }
    });

    // blocked jobs attempting acquisition after release
    active.filter(j=> j.state==='Blocked' && j.blockedOn).forEach(j=>{
      const st = mutexState.get(j.blockedOn!);
      if(st && !st.holder){
        // arbitration: highest effPrio waiter gets lock
        const candidates = st.queue.slice().sort((a,b)=> b.effPrio-a.effPrio);
        if(candidates[0]===j){
          st.holder=j; j.holds.add(j.blockedOn!); j.blockedOn=null; j.state='Ready';
          st.queue = st.queue.filter(x=>x!==j);
        }
      }
    });

    // new lock attempts
    active.filter(j=> j.state!=='Blocked' && j.remaining>0 && !j.blockedOn).forEach(j=>{
      if(j.taskId===-1) return;
      const td = tasks.find(x=>x.id===j.taskId);
      if(!td?.mutex) return;
      if(j.holds.has(td.mutex)) return;
      if(j.executed < td.csStart*td.C) return;
      if(j.executed >= (td.csStart+td.csDur)*td.C) return;
      let st = mutexState.get(td.mutex);
      if(!st){ st={holder:null, queue:[]}; mutexState.set(td.mutex,st); }
      if(!st.holder){
        st.holder=j; j.holds.add(td.mutex);
      }else if(st.holder!==j){
        if(!st.queue.includes(j)) st.queue.push(j);
        j.state='Blocked'; j.blockedOn=td.mutex;
        events.push({t, waiter:j.taskId, holder:st.holder.taskId, mutex:td.mutex});
      }
    });

    // priority / deadline inheritance fixpoint
    let changed=true;
    for(let iter=0; iter<10 && changed; iter++){
      changed=false;
      for(const [mname, st] of mutexState){
        if(!st.holder || st.queue.length===0) continue;
        if(mode==='RMS'){
          const maxWait = Math.max(...st.queue.map(q=> q.effPrio));
          if(maxWait > st.holder.effPrio){ st.holder.effPrio=maxWait; changed=true; }
        }else{
          const minDead = Math.min(...st.queue.map(q=> q.effDeadline));
          if(minDead < st.holder.effDeadline){ st.holder.effDeadline=minDead; changed=true; }
        }
      }
    }

    // deadline miss detection
    active.forEach(j=>{
      if(!j.missed && t >= j.absDeadline && j.remaining>0){
        j.missed=true; totalMiss++;
      }
      if(j.state==='Blocked') j.blockedTime++;
    });

    // scheduling selection
    const cand = active.filter(j=> j.state!=='Blocked');
    if(cand.length>0){
      cand.sort((a,b)=>{
        if(mode==='RMS') return b.effPrio - a.effPrio;
        const d = a.effDeadline - b.effDeadline;
        return d!==0? d : b.effPrio - a.effPrio;
      });
      const run = cand[0];
      run.state='Running';
      if(run.firstStart==null) run.firstStart=t;
      cand.slice(1).forEach(j=>{ if(j.state==='Running') j.state='Ready'; if(j.state!=='Blocked') j.state='Ready'; });
      run.executed++; run.remaining--;
      if(run.remaining<=0){
        run.remaining=0; run.state='Done'; run.completion=t+1;
        for(const m of [...run.holds]){ const st=mutexState.get(m); if(st?.holder===run) st.holder=null; }
        run.holds.clear();
      }
      log[t]=run.taskId;
    }else{
      log[t]=-1;
    }

    // per-task aggregate timeline for drawing
    tasks.forEach(td=>{
      const js = active.filter(j=> j.taskId===td.id);
      let st:'Idle'|'Ready'|'Running'|'Blocked' = 'Idle';
      if(js.some(j=> j.state==='Running')) st='Running';
      else if(js.some(j=> j.state==='Blocked')) st='Blocked';
      else if(js.some(j=> j.state==='Ready')) st='Ready';
      else if(js.length>0) st='Ready'; // a job that finished THIS tick is still occupying the row
      perTask[td.id][t]=st as any;
    });
    // ISR row
    {
      const isrActive = active.filter(j=> j.taskId===-1);
      let st:'Idle'|'Ready'|'Running'|'Blocked' = 'Idle';
      if(isrActive.some(j=> j.state==='Running')) st='Running';
      else if(isrActive.some(j=> j.state==='Blocked')) st='Blocked';
      else if(isrActive.some(j=> j.state==='Ready')) st='Ready';
      perTask[-1][t]=st as any;
    }
  }

  const U = tasks.reduce((s,t)=> s + t.C/t.T, 0);
  const bound = tasks.length * (Math.pow(2,1/tasks.length)-1);
  return {log, perTask, jobs, events, U, bound, totalMiss};
}

// Utilization: U = Σ C_i / T_i
// Liu & Layland: U ≤ n(2^{1/n}-1)
// Response time iteration: R_i^{k+1}= C_i + B_i + Σ ceil(R_i^k/T_j) C_j

Export · Soft gate

Export FreeRTOSConfig + task set

Download your tuned task periods (T), WCET (C), deadlines (D) and mutex map — free with watermark, clean after email unlock. Great for heading to SparkFun RedBoard / Teensy 4.0 bench with hiletgo logic analyzer.

File · FreeRTOSConfig.h

FreeRTOSConfig.htext/x-c-header+ watermark line on free path

Free download adds a small footer: // Export from makerportal.ai — free watermarked build. Unlock c…Clean export removes footer. Both are generated fresh from your current sim tuning.

Privacy: email stays in your browser localStorage (mp_export_email_rtos-scheduler) + unlock flag (mp_export_unlock_rtos-scheduler). If Buttondown username is configured, we also POST to Buttondown (privacy-first mode, no tracking pixels per D-014). See privacy → affiliates & email.

Unlock clean export

Soft gate — no hard paywall, no Clerk. Email stays local unless you explicitly check the newsletter box. Unsubscribe anytime. RSS at /rss.xml.

Export → Fab bonusAfter export, your tuned stackup can be ordered via PCBWay/JLCPCB CTA (when live) — see /privacy#affiliates for live merchants.

Frequently asked questions

What does RMS guarantee and when does it fail?

Rate Monotonic Scheduling assigns fixed priority by period: shorter T → higher priority. Liu & Layland bound U ≤ n(2^{1/n}-1) is sufficient but not necessary; if U exceeds bound, tasks may still be schedulable but must pass exact response time analysis R_i = C_i + Σ ceil(R_i/T_j)C_j for all higher priority j. Failure mode is deadline miss of low priority tasks even when U<1 if harmonicity poor.

How does priority inheritance fix unbounded blocking?

When high priority job H blocks on mutex held by low L, L inherits H priority (or earliest deadline in EDF). This bounds blocking to one critical section. Without it, medium tasks can preempt L indefinitely, causing chain blocking. SRP (Stack Resource Policy) and PCP build on this with ceiling: lock only if priority > system ceiling.

EDF vs RMS under mutexes?

EDF is optimal for independent tasks: U≤1 schedulable. With mutexes, it needs deadline inheritance: holder inherits min(deadline) of waiters, called DIPP or SRP. RMS uses priority inheritance. EDF generally yields lower jitter for mixed periods but needs more dynamic queue; RMS is simpler for static analysis and certification.

What is response time analysis RTA?

Iterative equation R_i^{k+1}= C_i + B_i + Σ_{j∈hp(i)} ceil(R_i^k/T_j)*C_j where B_i is worst blocking time from lower priority tasks holding mutex. Converges if R_i≤D_i. Our simulator logs actual response vs analytic bound; jitter = std(R_i) across jobs.

Why audio tension mapping?

Deadline misses correlate with urgency. We map miss ratio m = misses/jobs to beating frequency: two oscillators f1=220Hz, f2 = 330 - m·99Hz. At m=0, f2=330Hz perfect fifth consonant. At m=1, f2=231Hz → 11Hz beating roughness (Helmholtz dissonance curve peaks ~15-30Hz beating). Gain also scales with m, so lock-ups sound tense.

ISR nesting how?

ISR task has priority 1e9, preempts all. If ISR period < C_ISR, multiple ISR jobs queue (nesting). We show purple bars always on top. ISR holding mutex can cause priority inversion even for kernel tasks; our mutex manager allows it to demonstrate why ISR should use lock-free queues.

Shareable still

The instrument, captured—not illustrated.

This 16:9 frame is rendered from the real browser instrument above. It is the page's canonical preview for image search, link unfurls, and posts that need to show what the tool actually does.

Download 1280 × 720 JPEG
RTOS Scheduler Visualizer — live MakerPortal instrument screenshot
Canonical capture · real UI · no generated scientific artwork