323 lasījumi
323 lasījumi

Sākt ar RTOS: rokasgrāmata iesācējiem, kas izmanto Cortex-M4

autors Manoj Gowda15m2025/06/04
Read on Terminal Reader

Pārāk ilgi; Lasīt

Izveidot minimālu RTOS ir neskaidrs, bieži vien vilinošs, un absolūti apgaismojums.
featured image - Sākt ar RTOS: rokasgrāmata iesācējiem, kas izmanto Cortex-M4
Manoj Gowda HackerNoon profile picture

Es joprojām atceros vienu vakaru, kad es skatījos uz TM4C123GH6PM LED, gaidot vienkāršu “sirdsdarbības” mirgošanu, kas nekad nav pienācis. Es domāju, ka es visu izdarīju pareizi: iestatīju SysTick, atstāja PendSV un sākotnēji iestatīja manu Thread Control Blocks (TCBs). Bet tā vietā, lai pastāvīgi mirdzētu, LED vienreiz pagriezās, pēc tam saldēja - izsmēja mani. Šis brīdis kristalizēja to, ko īsti nozīmē izveidot mazu RTOS: cīņa ar aparatūras trūkumiem, vajāšana neizbēgamiem kļūdām un pietiekami daudz koda, lai viss darbotos gludi. Šajā rakstā es jūs vadīšu caur manu ceļu, veidojot minim


Nedaudz backstory

Aptuveni pirms gada mana iebūvēto sistēmu klase mums piešķīra vienkāršu RTOS no nulles uz TM4C123GH6PM (ARM Cortex-M4). Es biju lietojis FreeRTOS pirms tam, bet es nekad pilnībā nesapratu, kas notiek aiz skatuves.

Why?

  • Es gribēju redzēt, kā tieši CPU pāriet no vienas vielas uz otru.
  • Man vajadzēja uzzināt, kāpēc zemas prioritātes uzdevumi var nejauši izsalkt augstākas prioritātes uzdevumus (hello, prioritātes reversija).
  • Es vēlējos reālus debugging stāstus - piemēram, to laiku, kad es pavadīju pusi dienas, domājot, kāpēc PendSV nekad nedarbojās (izrādās, ka es tam piešķīru tādu pašu prioritāti kā SysTick).

Spoiler: Mans RTOS nebija ideāls, bet, izsekojot tā trūkumiem, es uzzināju vairāk par ARM internāliem nekā no jebkuras mācību grāmatas.


Niršana Cortex-M4 pasaulē

Pirms rakstot vienu kodu, man nācās iesaiņot galvu ap to, kā Cortex-M4 izturas pret pārtraukumiem un kontekstu maiņu.

  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.

Šūnu kontroles bloka (TCB) izveide

Katrs triks manā RTOS tur:

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;

Praksē es paziņoju:

#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;

Stack-Overflow šausmu stāsts

Kad es pirmo reizi piešķīruSTACK_WORDS = 128, viss likās labi - līdz brīdim, kad mans "darbinieks" virziens, kas veica dažus savienotus funkciju zvanus, sāka sabojāt atmiņu.0xDEADBEEFuzsākšanas brīdī un pārbaudot, cik tā ir pārrakstīta, es atklāju, ka 128 vārdi nav pietiekami optimizētu veidošanas karogu ziņā.


Izveidot jaunu virzienu

Šūnu izveide nozīmēja izgriezt tā kaudzes un simulēt aparatūras kaudzes, kas notiek pie izņēmuma ieraksta.

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, lai uzraudzītu

  • Trūkst tā nozīmē, ka jūsu CPU centīsies interpretēt kodu kā ARM norādījumus — tūlītēju kļūdu.
  • Magic 0xFFFFFFFD. Tas saka CPU, "Par izņēmumu atgriešanās, izmantojiet PSP un dodieties uz tīkla režīmu." Es atceros, meklējot ARM ARM (Architecture Reference Manual) vismaz trīs reizes, lai iegūtu to pareizi.
  • Stack order. Pushing R4–R11 manuāli ir jāievēro precīza kārtība, ko rīkotājs sagaida. Vienkārša tipogrāfija (piemēram, nospiežot R11 vispirms, nevis R4) izmet visu rāmi.

Laika pārsniegšana: SysTick Handler

Šeit ir mans pēdējais SysTick ISR, sagriezts būtībā:

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
}

Vēl pāris piezīmes:

  • Agrīnā versijā es mēģināju izsaukt rtos_schedule() tieši SysTick iekšpusē. kas noveda pie nested pārtraukumiem un kaudzes neskaidrībām.
  • Es atklāju, ka, ja SysTick_IRQn un PendSV_IRQn dalās ar vienu un to pašu prioritāti, PendSV dažreiz nekad nestrādā.

Lielais pārslēdzējs: PendSV Handler

Kad PendSV beidzot aizdedzina, tas veic faktisko konteksta pārslēgšanu. mans GCC inline montāžas (ARM sintakse) īstenošana izskatās šādi:

__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
    );
}

Kā es izsekojis šo briesmīgo 32-bitu kompensāciju

Sākumā mans "glabāt / atjaunot" kods bija izslēgts par 32 baitiem. simptoms? Vārdi darbotos, tad kaut kādā veidā atgriežas dzīvē nepareizā vietā - garbled instrukcijas, izlases lēcieni.STMDB) lai precīzi izmērītu, cik daudz bajtu tika nospiests. manā debugger, es pēc tam salīdzināju ciparu PSP vērtību artcbs[].stack_ptrEs gaidīju. droši pietiekami, es būtu nejauši izmantojisSTMDB R0!, {R4-R11, R12}Tā vietā, lai{R4-R11}— nospiežot vienu papildu reģistru. Noņemot šo papildu nospiežamo reģistru, tas tika fiksēts.


Izvēloties nākamo tēmu: Plānotāja loģika

Tas skenē visus TCBs, lai atrastu augstākās prioritātes READY virzienu, ar nelielu apļveida tweak vienādas prioritātes virzieniem:

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:

  • Ja divām virknēm ir prioritāte 1, bet es vienmēr izvēlos zemāko ID, otrais nekad nesaņems CPU laiku.
  • Bezdarbs virziens (ID 0) ir īpašs gadījums: vienmēr gatavs, vienmēr zemākā prioritāte (prioritāte = 255), lai, ja nekas cits nav izpildāms, tas vienkārši griežas (vai izsauc __WFI() lai ietaupītu enerģiju).

Kernel Primitives: ienesīgums, miegs un semaphores

Pēc pamatprincipiem, nākamais solis bija padarīt virzienus pareizi mijiedarboties.

Ieguvumi

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

Man patīk sprinklerisrtos_yield()garu loku beigās, lai citas virves iegūtu taisnīgu šāvienu. agrīnā testā, izlaižotyield()Tas nozīmēja, ka daži uzdevumi sagrāba CPU noteiktās prioritārajās konfigurācijās.

gulēt

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

Mana “LED blinking” tīkla zvanurtos_sleep(500)Kad es skatījos, kā tas mirgo ik pēc pusotra sekundes, es zinu, ka SysTick un PendSV dara savu darbu pareizi.

Semināri

Sākumā es mēģināju naivi pieeju:

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();
}

Prioritāte Inversion Nightmare

Kādu dienu man bija trīs virzieni:

  • T0 (Prioritāte 2): tur semaporu.
  • T1 (prioritāte 1): gaidot šo semaporu.
  • T2 (prioritāte 3): gatavs darboties un augstāks par T0, bet zemāks par T1.

Tā kā T1 tika bloķēts, T2 turpināja darboties - nekad nedodot T0 iespēju atbrīvot semaporu T1. T1 nomira. Mans ātrs hack bija īslaicīgi palielināt T0 prioritāti, kad T1 tika bloķēts - tas ir rudimentārs prioritātes mantojums. Pilns risinājums izsekotu, kurš pavediens tur semaporu un automātiski paaugstinātu savu prioritāti.


Izslēgt visu no:Rotaļlietas sākums()

Rotaļlietas sākums()

uzmain(), pēc pamata aparatūras init (stundas, GPIO LED, UART shell), es darīju:

// 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
}

Pāris pēdējās piezīmes:

  • Idle Thread (ID 0): Tas vienkārši pārslēdz LED ar zemu prioritāti.
  • UART pārtraukšanas prioritāte: UART TX pārtraukšanai bija nepieciešama augstāka prioritāte nekā PendSV; pretējā gadījumā ilgie printf zvanījumi tiktu pārtraukti vidū, apgrūtinot izeju.

Nosaukums oriģinālvalodā: Peeking Under the Hood

Es uzbūvēju nelielu apvalku, lai es varētu ievadīt komandas UART un pārbaudīt virziena stāvokli:

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");
        }
    }
}

Šis apvalks bija mans drošības tīkls: ja LED uzvedās dīvaini, es lēkt uz manu sērijveida terminālu, ierakstietps, un uzreiz redzēt, kuri virzieni bija gatavi, bloķēti vai guļ.


Mācības, ko iemācījāmies pa ceļam

  1. Interrupt Priorities ir viss, ko es pavadīju visu pēcpusdienu, pārliecinoties, ka mans PendSV kods bija nepareizs, līdz es sapratu, ka es būtu iestatījis SysTick un PendSV vienai prioritātei.
  2. Mērciet savu kaudzes izmantošanu Pirms katra tīkla kaudzes aizpildīšanas ar zināmu modeli (0xDEADBEEF) sākumā. Pēc darbināšanas pārbaudiet atmiņu, lai redzētu, cik dziļi modelis ir pārrakstīts. Ja jūsu kaudzes rādītājs kādreiz nonāk šajā modelī, jūs zināt, ka jums ir nepieciešams lielāks kaudze. Es to uzzināju grūti, kad dziļāka zvanu ķēde manā strūklā izraisīja klusu pārrakstīšanu.
  3. Izmantojiet LED un GPIO, lai pārveidotu Ja jums nav fancy debugger, vienkārši pārslēdziet GPIO pin (pieķeriet to uz LED). Es ievietoju vienu LED pārslēgšanu PendSV_Handler pašā sākumā un citu tā beigās.
  4. Keep It Simple at First Mana pirmā RTOS versija mēģināja atbalstīt dinamisko uzdevumu izveidi izpildes laikā — masveida kļūda. es beidzās ar atmiņas fragmentāciju un dīvainiem crashes.
  5. Es uzrakstīju īsu piezīmi: "Saite reģistrācijas vērtība, kas norāda atgriešanos tīkla režīmā, izmantojot PSP." Bez šī komentāra es būtu Google "ARM izņēmuma atgriešanās vērtības" katru reizi, kad es pārskatīju kodu.

Nākamais solis: kur doties no šejienes

Ja jūs nolemjat veidot uz šī pamata, šeit ir dažas idejas:

  • Pilna prioritātes mantošana Semaphores Tā vietā, lai manu ātru "palielinātu un aizmirstu" hack, īsteno atbilstošu protokolu, kurā augstākās prioritātes bloķētās virves prioritāte tiek mantota, līdz semaphore tiek atbrīvots.
  • Inter-Thread Message Queues Ļaujiet virzieniem droši nosūtīt mazus ziņojumus vai rādītājus viens otram.
  • Dynamic Task Creation/Deletion Tack uz neliela kaudzes pārvaldnieka vai atmiņas baseina, lai jūs varētu izveidot un nogalināt virzienus uz lēciena - paturiet prātā papildu sarežģītību!
  • Profilēšanas āķi Paplašiniet savu SysTick manipulatoru (vai izmantojiet citu laika posmu), lai reģistrētu, cik daudz ticks katrs virziens darbojas.
  • Reāllaika drošības pārbaudes Ievadiet kaudzes lietošanas ierobežojumus un atklājiet, kad vītnes kaudzes rādītājs šķērso 0xDEADBEEF reģionu, izraisot drošu izslēgšanu vai atkārtotu iestatījumu, nevis nejaušas avārijas.

Galīgās domas

Izveidot minimālu RTOS ir neskaidrs, bieži vien vilinošs, un absolūti apgaismojums. no vajāšanas nepareizu PendSV prioritāti, lai cīnītos ar kaudzes pārplūdi, katra kļūda piespieda mani saprast Cortex-M4 iekšējos dziļāk.

Ja jūs sniedzat šo šāvienu, lūdzu, ļaujiet man zināt, kura daļa jūs izvilka no sienas.Es gribētu dzirdēt savus "LED mirgojošos" stāstus vai jebkurus citus trikus, kurus jūs atklājāt, medījot garus savā plānotājā.

L O A D I N G
. . . comments & more!

About Author

Manoj Gowda HackerNoon profile picture
Manoj Gowda@manojgowda
I'm Manoj Gowda—embedded software engineer by day, bug whisperer by night, making cars smarter one crash log at a time.

PAKARINĀT TAGUS

ŠIS RAKSTS TIKS PĀRSTRĀDĀTS...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks