316 aflæsninger
316 aflæsninger

At komme i gang med RTOS: En praktisk guide til begyndere ved hjælp af Cortex-M4

ved Manoj Gowda15m2025/06/04
Read on Terminal Reader

For langt; At læse

Opbygning af en minimal RTOS er rodet, ofte frustrerende og absolut oplysende.
featured image - At komme i gang med RTOS: En praktisk guide til begyndere ved hjælp af Cortex-M4
Manoj Gowda HackerNoon profile picture

Jeg husker stadig at stirre på mine TM4C123GH6PM's LED'er en aften, venter på et simpelt "hjertebeat" blink, der aldrig kom. Jeg troede, jeg havde gjort alt rigtigt: oprettet SysTick, hængt PendSV, og initialiseret mine Thread Control Blocks (TCB'er). Men i stedet for et stadigt blink, blinkede LED'erne en gang, og derefter frossede - hånede mig. Det øjeblik krystalliserede, hvad opbygningen af en lille RTOS virkelig betyder: kæmper med hardware quirks, jager elusive bugs og patcher sammen bare nok kode til at få alt til at køre glat. I denne artikel vil jeg gå dig gennem min rejse med at fremstille en minimal, prioriteret planlægger på en ARM Cortex


Lidt baggrundshistorie

For omkring et år siden tildelte min klasse af indlejrede systemer os at bygge en simpel RTOS fra bunden på en TM4C123GH6PM (ARM Cortex-M4). jeg havde brugt FreeRTOS før, men jeg forstod aldrig fuldt ud, hvad der foregik bag kulisserne.

Why?

  • Jeg ønskede at se præcis, hvordan CPU skifter fra en tråd til en anden.
  • Jeg var nødt til at lære, hvorfor lavprioriterede opgaver utilsigtet kan sulte de højere prioriterede opgaver (hello, prioritering inversion).
  • Jeg længtes efter rigtige debuggerhistorier - som den tid, jeg brugte en halv dag på at undre mig over, hvorfor PendSV aldrig kørte (det viser sig, at jeg havde givet det samme prioritet som SysTick).

Spoiler: Min RTOS var ikke perfekt. men i jagten på dens mangler lærte jeg mere om ARM internals end fra nogen lærebog.


Læs mere om Cortex-M4 World

Før jeg skrev en enkelt linje kode, måtte jeg pakke mit hoved rundt om, hvordan Cortex-M4 håndterer afbrydelser og kontekstskifte.

  1. PendSV’s “Gentle” Role
    • PendSV is designed to be the lowest-priority exception—meaning it only runs after all other interrupts finish. My first mistake was setting PendSV’s priority to 0 (highest). Of course, it never ran because every other interrupt always preempted it. Once I moved it to 0xFF, scheduling finally kicked in.
    • Note to self (and you): write down your NVIC priorities on a Post-it. It’s easy to confuse “0 is highest” with “0 is lowest.”
  2. SysTick as the Heartbeat
    • I aimed for a 1 ms tick. On a 16 MHz clock, that meant LOAD = 16 000 – 1.
    • I initially tried to do all scheduling decisions inside the SysTick ISR, but that got messy. Instead, I now just decrement sleep counters there and set the PendSV pending bit. Let the “real” context switch happen in PendSV.
  3. Exception Stack Frame
    • When any exception fires, hardware auto-pushes R0–R3, R12, LR, PC, and xPSR. That means my “fake” initial stack for each thread must match this exact layout—else, on the very first run, the CPU will attempt to pop garbage and crash.
    • I once forgot to set the Thumb bit (0x01000000) in xPSR. The result was an immediate hard fault. Lesson: that Thumb flag is non-negotiable.

Brug af Thread Control Block (TCB)

Hver tråd i min RTOS har:

typedef enum { READY, RUNNING, BLOCKED, SLEEPING } state_t;

typedef struct {
    uint32_t *stack_ptr;   // Saved PSP for context switches
    uint8_t   priority;    // 0 = highest, larger = lower priority
    state_t   state;       // READY, RUNNING, BLOCKED, or SLEEPING
    uint32_t  sleep_ticks; // How many SysTick ticks remain, if sleeping
} tcb_t;

I praksis erklærede jeg:

#define MAX_THREADS 9
#define STACK_WORDS 256

uint32_t    thread_stacks[MAX_THREADS][STACK_WORDS];
tcb_t       tcbs[MAX_THREADS];
uint8_t     thread_count = 0;
int         current_thread = -1;

A Stack-Overflow skrækhistorie

Da jeg først tildelteSTACK_WORDS = 128, alt syntes godt - indtil min "arbejder" tråd, der gjorde et par nestede funktion opkald, begyndte at ødelægge hukommelsen.0xDEADBEEFved opstart og tjekke, hvor langt det blev overskrevet, fandt jeg ud af, at 128 ord ikke var nok under optimerede build-flag.


Skab en ny tråd

Oprettelse af en tråd betød udskæring af dens stabel og simulering af hardware stabling, der sker på undtagelsesindtastning.

void rtos_create_thread(void (*fn)(void), uint8_t prio) {
    int id = thread_count++;
    tcb_t *t = &tcbs[id];
    uint32_t *stk = &thread_stacks[id][STACK_WORDS - 1];

    // Simulate hardware stacking (xPSR, PC, LR, R12, R3, R2, R1, R0)
    *(--stk) = 0x01000000;            // xPSR: Thumb bit set
    *(--stk) = (uint32_t)fn;          // PC → thread entry point
    *(--stk) = 0xFFFFFFFD;            // LR → return with PSP in Thread mode
    *(--stk) = 0x12121212;            // R12 (just a marker)
    *(--stk) = 0x03030303;            // R3
    *(--stk) = 0x02020202;            // R2
    *(--stk) = 0x01010101;            // R1
    *(--stk) = 0x00000000;            // R0

    // Save space for R4–R11 (popped by the context switch)
    for (int r = 4; r <= 11; r++) {
        *(--stk) = 0x0; // or use a pattern if you want to measure usage
    }

    t->stack_ptr   = stk;
    t->priority    = prio;
    t->state       = READY;
    t->sleep_ticks = 0;
}

Pitfalls at holde øje med

  • Manglende det betyder, at din CPU vil forsøge at fortolke kode som ARM-instruktioner – øjeblikkelig fejl.
  • Den magiske 0xFFFFFFFD. Dette fortæller CPU, "På undtagelse tilbage, brug PSP og gå til Thread mode." jeg husker at kigge op ARM ARM (Architecture Reference Manual) mindst tre gange for at få dette rigtigt.
  • Tryk R4–R11 manuelt skal følge den nøjagtige rækkefølge, som håndtereren forventer. En simpel typo (f.eks. at trykke R11 først i stedet for R4) kaster hele rammen ud.

Læs mere om: SysTick Handler

Her er min sidste SysTick ISR, trimmet til essentials:

void SysTick_Handler(void) {
    for (int i = 0; i < thread_count; i++) {
        if (tcbs[i].state == SLEEPING) {
            if (--tcbs[i].sleep_ticks == 0) {
                tcbs[i].state = READY;
            }
        }
    }
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; // Pend PendSV for scheduling
}

Et par noter:

  • I en tidlig version forsøgte jeg at ringe rtos_schedule() lige inde i SysTick. Det førte til nestede afbrydelser og stack forvirring.
  • Jeg fandt ud af, at hvis SysTick_IRQn og PendSV_IRQn deler samme prioritet, kører PendSV undertiden aldrig.

Den store switch: PendSV Handler

Når PendSV endelig brænder, gør det den faktiske kontekstskift. Min implementering i GCC inline assembly (ARM syntax) ser sådan ud:

__attribute__((naked)) void PendSV_Handler(void) {
    __asm volatile(
        "MRS   R0, PSP                \n" // Get current PSP
        "STMDB R0!, {R4-R11}          \n" // Push R4–R11 onto stack
        "LDR   R1, =current_thread    \n"
        "LDR   R2, [R1]               \n"
        "STR   R0, [R2, #0]           \n" // Save updated PSP into TCB

        "BL    rtos_schedule          \n" // Decide next thread

        "LDR   R1, =current_thread    \n"
        "LDR   R2, [R1]               \n"
        "LDR   R0, [R2, #0]           \n" // Load next thread’s PSP
        "LDMIA R0!, {R4-R11}          \n" // Pop R4–R11 from its stack
        "MSR   PSP, R0                \n" // Update PSP to new thread
        "BX    LR                     \n" // Exit exception, restore R0–R3, R12, LR, PC, xPSR
    );
}

Hvordan jeg sporede ned, at den uhyggelige 32-byte offset

I første omgang var min "save/restore" kode slået fra med 32 byte. Symptomet? Tråde ville køre, så på en eller anden måde komme tilbage til livet på det forkerte sted - gurglede instruktioner, tilfældige spring. Jeg tilføjede et par GPIO toggles (toggling en LED pin lige før og efterSTMDB) for at måle præcis, hvor mange byte der blev skubbet. I min debugger sammenlignede jeg derefter den numeriske PSP-værdi med dentcbs[].stack_ptrJeg forventede. sikkert nok, jeg ville tilfældigvis have brugtSTMDB R0!, {R4-R11, R12}I stedet for{R4-R11}Fjernelse af det ekstra trykte register fikserede det.


Vælg den næste tråd: Scheduler Logic

Min tidsplan er bevidst enkel. Den scanner alle TCB'er for at finde den højeste prioritet READY tråd, med en lille rund-robin tweak for tråde af lige prioritet:

void rtos_schedule(void) {
    int next = -1;
    uint8_t best_prio = 0xFF;

    for (int i = 0; i < thread_count; i++) {
        if (tcbs[i].state == READY) {
            if (tcbs[i].priority < best_prio) {
                best_prio = tcbs[i].priority;
                next = i;
            } else if (tcbs[i].priority == best_prio) {
                // Simple round-robin: if i is after current, pick it
                if (i > current_thread) {
                    next = i;
                    break;
                }
            }
        }
    }
    if (next < 0) {
        // No READY threads—fall back to idle thread (ID 0)
        next = 0;
    }
    current_thread = next;
}

What I Learned Here:

  • Hvis to tråde deler prioritet 1, men jeg altid vælger den lavere ID, vil den anden aldrig få CPU-tid.
  • Idle thread (ID 0) er et specielt tilfælde: ALTID READY, altid laveste prioritet (prioritet = 255), så hvis intet andet er kørende, spinner det bare (eller kalder __WFI() for at spare strøm).

Kernel Primitives: Udbytte, Søvn og Semaphores

Efter at have drejet det grundlæggende, var det næste skridt at få tråde til at interagere ordentligt.

afkast

void rtos_yield(void) {
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

Jeg kan godt lide sprinklerrtos_yield()i slutningen af lange løb, så andre tråde får et retfærdigt skud.yield()Dette betød, at nogle opgaver huggede CPU'en under visse prioriterede konfigurationer.

Søvn

void rtos_sleep(uint32_t ticks) {
    tcb_t *self = &tcbs[current_thread];
    self->sleep_ticks = ticks;
    self->state       = SLEEPING;
    rtos_yield();
}

Min “LED blinking” tråd kalderrtos_sleep(500)Når jeg ser det blinke hvert halve sekund, ved jeg, at SysTick og PendSV gør deres arbejde korrekt.

Semifinale

Til at begynde med prøvede jeg en naiv tilgang:

typedef struct {
    volatile int count;
    int waiting_queue[MAX_THREADS];
    int head, tail;
} semaphore_t;

void rtos_sem_wait(semaphore_t *sem) {
    __disable_irq();
    sem->count--;
    if (sem->count < 0) {
        sem->waiting_queue[sem->tail++] = current_thread;
        tcbs[current_thread].state = BLOCKED;
        __enable_irq();
        rtos_yield();
    } else {
        __enable_irq();
    }
}

void rtos_sem_post(semaphore_t *sem) {
    __disable_irq();
    sem->count++;
    if (sem->count <= 0) {
        int tid = sem->waiting_queue[sem->head++];
        tcbs[tid].state = READY;
    }
    __enable_irq();
}

Omvendelse af mareridt

En dag havde jeg tre tråde:

  • T0 (Prioritet 2): holder semaphoren.
  • T1 (Prioritet 1): Venter på den semaphore.
  • T2 (prioritet 3): klar til at køre og højere end T0 men lavere end T1.

Fordi T1 var blokeret, fortsatte T2 med at køre - aldrig give T0 en chance for at frigive semaphoren til T1. T1 sultede. Min hurtige hack var at midlertidigt øge T0's prioritet, når T1 blev blokeret - det er en rudimentær prioriteret arv. En fuld løsning ville spore, hvilken tråd holder semaphoren og løfte sin prioritet automatisk.


Kicking alt af:rtos_start()

afslutningsskemaet start()

imain(), efter grundlæggende hardware init (ure, GPIO for LEDs, UART for shell), jeg gjorde:

// 1. Initialize SysTick and PendSV priorities
systick_init(16000);        // 1 ms tick on a 16 MHz system
NVIC_SetPriority(PendSV_IRQn, 0xFF);

// 2. Create threads (ID 0 = idle thread)
rtos_create_thread(idle_thread, 255);
rtos_create_thread(shell_thread, 3);
rtos_create_thread(worker1, 1);
rtos_create_thread(worker2, 2);

// 3. Switch to PSP and unprivileged Thread mode
current_thread = 0;
__set_PSP((uint32_t)tcbs[0].stack_ptr);
__set_CONTROL(0x02); // Use PSP, unprivileged
__ISB();

// 4. Pend PendSV to start first context switch
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;

// 5. Idle loop
while (1) {
    __WFI();  // Save power until next interrupt
}

Et par afsluttende noter:

  • Idle Thread (ID 0): Det slukker simpelthen en LED ved lav prioritet. Hvis noget går galt, ved jeg, at jeg i det mindste har fået til den idle tråd.
  • UART Interrupt Priority: UARTs TX-afbrydelse havde brug for en højere prioritet end PendSV; ellers ville lange printf-opkald blive afbrudt i midten af transmitteringen, hvilket manglede output.

Anmeldelse af The UART Shell: Peeking Under the Hood

Jeg byggede en lille shell, så jeg kunne skrive kommandoer over UART og inspicere trådstater:

void shell_thread(void) {
    char buf[64];
    while (1) {
        uart_print("> ");
        uart_read_line(buf, sizeof(buf));

        if (strncmp(buf, "ps", 2) == 0) {
            for (int i = 0; i < thread_count; i++) {
                uart_printf("TID %d: state=%d prio=%d\n",
                    i, tcbs[i].state, tcbs[i].priority);
            }
        } else if (strncmp(buf, "sleep ", 6) == 0) {
            uint32_t msec = atoi(&buf[6]);
            rtos_sleep(msec);
        } else if (strncmp(buf, "kill ", 5) == 0) {
            int tid = atoi(&buf[5]);
            if (tid >= 1 && tid < thread_count) { // don’t kill idle
                tcbs[tid].state = BLOCKED; // crude kill
                uart_printf("Killed thread %d\n", tid);
            }
        } else {
            uart_print("Unknown command\n");
        }
    }
}

Denne shell var mit sikkerhedsnet: hvis LED'erne opførte sig mærkeligt, ville jeg hoppe ind i min serielle terminal, skriveps, og se straks, hvilke tråde der var klar, blokeret eller sovende.


Lærdomme lært undervejs

  1. Jeg brugte en hel eftermiddag på at overbevise mig om, at min PendSV-kode var forkert, indtil jeg indså, at jeg havde sat SysTick og PendSV til samme prioritet.Når jeg gav SysTick en højere prioritet, begyndte PendSV at skyde pålideligt. Lektion: dobbelt- og tredobbelt-tjek dine NVIC_SetPriority() opkald.
  2. Præfyld hver tråds stack med et kendt mønster (0xDEADBEEF) ved opstart. Efter at have kørt, tjek hukommelsen for at se, hvor dybt mønsteret blev overskrevet. Hvis din stackpointer nogensinde går ind i det mønster, ved du, at du har brug for en større stack. Jeg lærte dette på den hårde måde, da en dybere opkaldskæde i min arbejdstøj forårsagede en tavs overskrivning.
  3. Brug LEDs & GPIO til Debugg Hvis du ikke har en fancy debugger, skal du bare skifte en GPIO pin (hook det til en LED). Jeg placerede en LED-skifte i begyndelsen af PendSV_Handler og en anden i slutningen.
  4. Min allerførste RTOS-version forsøgte at understøtte dynamisk opgaveoprettelse i løbetid – en massiv fejl. Jeg endte med hukommelsesfragmentering og mærkelige nedbrud. Ved at fryse antallet af tråde ved opstart (kun ni slots i mit tilfælde) undgik jeg en verden af smerte.
  5. Jeg skrev en kort note: "Link register værdi, der indikerer returnering til Thread mode ved hjælp af PSP."

Næste skridt: Hvor skal du hen herfra

Hvis du beslutter dig for at bygge på dette fundament, her er et par ideer:

  • I stedet for min hurtige "boost-and-forget" hack, implementer en ordentlig protokol, hvor den højeste prioritet blokerede tråds prioritet er arvet, indtil semaphoren frigives.
  • Inter-Thread Message Queues Lad tråde sende små beskeder eller pointers til hinanden sikkert. Tænk på det som en lille postkasse til overførsel af data mellem opgaver.
  • Dynamic Task Creation/Deletion Tack på en lille heap manager eller hukommelsespool, så du kan oprette og dræbe tråde på flyet - tænk på den ekstra kompleksitet!
  • Profilering Hooks Udvid din SysTick handler (eller brug en anden timer) til at logge, hvor mange ticks hver tråd kører.
  • Real-time sikkerhedskontrol Indtast stack brug grænser og registrere, når en tråd stack pointer krydser ind i 0xDEADBEEF regionen - udløser en sikker slukning eller nulstilling i stedet for tilfældige nedbrud.

Afsluttende tanker

Fra at forfølge en misplaceret PendSV-prioritet til at kæmpe med stack overflow, tvang hver fejl mig til at forstå Cortex-M4-internals dybere.Hvis du prøver dette på din egen TM4C eller nogen anden Cortex-M-del, forvent et par nætter med debugging - men også den dybe tilfredshed, når den første pålidelige LED-blink endelig dukker op.

Hvis du giver dette et skud, så lad mig vide, hvilken del, der kørte dig op ad væggen.Jeg vil elske at høre dine egne "LED blinkende" historier eller andre tricks, du opdagede, mens du jagede spøgelser i din tidsplan.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks