Unë ende e mbaj mend duke shikuar në LEDs e mia TM4C123GH6PM një mbrëmje, duke pritur për një të thjeshtë "beat" flash që nuk erdhi kurrë. Unë mendova se kam bërë gjithçka të drejtë: ngritur SysTick, pended PendSV, dhe filloi Blocks tim Thread Control (TCBs). Por në vend të një flash të qëndrueshme, LEDs twitched një herë, pastaj ngrirë-mocking mua. Ky moment kristalizuar atë që ndërtimin e një RTOS të vogël me të vërtetë do të thotë: duke luftuar me quirks hardware, ndjekur gabime të pakapshme, dhe patching së bashku vetëm kod të mjaftueshme për të bërë çdo gjë të funksionojë pa probleme. Në këtë artikull, unë do të ecin ju përmes udhëtimit tim të krijimit të një minimale
Një pjesë e backstory
Rreth një vit më parë, klasa ime e embedded-systems na caktoi të ndërtojmë një RTOS të thjeshtë nga zero në një TM4C123GH6PM (ARM Cortex-M4). Unë kam qenë duke përdorur FreeRTOS më parë, por unë kurrë nuk e kuptova plotësisht se çfarë po ndodhte prapa skenave.
Why?
- Unë doja të shihja saktësisht se si CPU kalon nga një thread në tjetrin.
- Unë kam nevojë për të mësuar se pse detyrat me prioritet të ulët mund të uritur aksidentalisht ato me prioritet më të lartë (përshëndetje, inversion prioritet).
- Kam etur për histori të vërteta debugging-si ajo kohë unë kaloi gjysmë të ditës duke menduar se pse PendSV kurrë nuk u drejtua (mësohet se unë e kisha dhënë atë të njëjtën prioritet si SysTick).
Spoiler: RTOS-i im nuk ishte i përsosur, por në ndjekjen e mangësive të tij, mësova më shumë për ARM internals se nga ndonjë libër mësimor.
Zhytje në botën e Cortex-M4
Para se të shkruaja një linjë të vetme të kodit, unë duhej të mbështillja kokën rreth asaj se si Cortex-M4 merret me ndërprerjet dhe ndërrimin e kontekstit.
- 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.”
- 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.
- I aimed for a 1 ms tick. On a 16 MHz clock, that meant
- 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.
Blloku i kontrollit të fijeve (TCB)
Çdo thread në RTOS-in tim mban:
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;
Në praktikë, unë deklarova:
#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;
Një Stack-Overflow Horror Story
Kur i dhashë për herë të parëSTACK_WORDS = 128
, gjithçka dukej në rregull – derisa tela ime “punëtore”, e cila bëri disa thirrje funksionale të ngulitura, filloi të prishë kujtesën.0xDEADBEEF
Në fillim dhe duke kontrolluar se sa larg është mbivlerësuar, kam zbuluar se 128 fjalë nuk ishin të mjaftueshme nën flamujt e optimizuar të ndërtimit.
Përdorni një fije të re
Krijimi i një fije nënkuptonte nxjerrjen e grumbullit të saj dhe simulimin e grumbullimit të hardware që ndodh në hyrjen e përjashtimit.
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 për të shikuar për
- Thumb Bit në xPSR. Mungesa e saj do të thotë se CPU juaj do të përpiqet të interpretojë kodin si udhëzime ARM – gabim i menjëhershëm.
- Magic 0xFFFFFFFD. Kjo i thotë CPU, "Në përjashtim të kthimit, përdorni PSP dhe shkoni në modalitetin Thread." Unë kujtoj duke kërkuar deri ARM ARM (Architecture Reference Manual) të paktën tre herë për të marrë këtë të drejtë.
- Shtypja R4–R11 manualisht duhet të ndjekë urdhrin e saktë që menaxheri pret. Një tipo e thjeshtë (p.sh., shtypja e R11 së pari në vend të R4) hedh të gjithë kornizën.
Le të kalojë koha: SysTick Handler
Këtu është SysTick ISR im përfundimtar, i prerë në thelbësore:
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
}
Disa nga shënimet:
- Në një version të hershëm, unë u përpoq për të thirrur rtos_schedule() direkt brenda SysTick. Kjo çoi në ndërprerje të ngulitura dhe konfuzion grumbull. Tani unë vetëm pend PendSV dhe le të trajtojë ngritjen e rëndë.
- Kam zbuluar se nëse SysTick_IRQn dhe PendSV_IRQn ndajnë të njëjtën prioritet, PendSV nganjëherë nuk funksionon. Gjithmonë jepni PendSV prioritetin absolut më të ulët numerik (p.sh., NVIC_SetPriority(PendSV_IRQn, 0xFF)), dhe mbani SysTick pak më të lartë (p.sh., 2 ose 3).
Ndërrimi i madh: PendSV Handler
Kur PendSV përfundimisht digjet, ajo bën kontekstin aktual. implementimi im në asamblenë e linjës GCC (sintax ARM) duket si kjo:
__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
);
}
Si e gjurmoj atë 32-byte të dëmtuar
Në fillim, kodi im "shpëtuar / rivendosur" ishte i fikur nga 32 bytes. Simptomi? Threads do të kandidojë, pastaj disi të kthehen në jetë në vendin e gabuar - udhëzime gërvishtur, kërcime të rastësishme. I shtuar disa GPIO toggles (gërvishtur një pin LED vetëm para dhe pasSTMDB
) për të matur saktësisht se sa bytes janë shtyrë. në debugger tim, unë pastaj krahasuar vlerën numerike PSP metcbs[].stack_ptr
Unë e prisja. mjaft e sigurt, kam përdorur aksidentalishtSTMDB R0!, {R4-R11, R12}
Në vend të{R4-R11}
Duke shtyrë një regjistër shtesë, heqja e regjistrit shtesë të shtyrë e rregulloi atë.
Picking the Next Thread: Logjika e planifikimit
Planifikuesi im është qëllimisht i thjeshtë. Skanon të gjitha TCB-të për të gjetur fije READY me prioritet më të lartë, me një përshtatje të vogël të rrumbullakët për fije me prioritet të barabartë:
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:
- Nëse dy fije ndajnë prioritetin 1, por unë gjithmonë zgjedh ID më të ulët, tjetri kurrë nuk do të marrë kohë CPU.
- Fusha e zbrazët (ID 0) është një rast i veçantë: gjithmonë gati, gjithmonë me prioritet më të ulët (prioritet = 255), kështu që nëse asgjë tjetër nuk është e drejtueshme, ajo thjesht rrotullohet (ose thërret __WFI() për të kursyer energji).
Kernel Primitives: Fitimi, gjumi dhe semaphores
Pas threading themelore, hapi i ardhshëm ishte për të bërë threads ndërveprojnë siç duhet.
Të ardhurat
void rtos_yield(void) {
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
Më pëlqen sprinklingrtos_yield()
në fund të qarqeve të gjata në mënyrë që të tjerët të marrin një goditje të drejtë.yield()
Kjo do të thotë se disa detyra kanë penguar CPU-në nën konfigurime të caktuara prioritare.
gjumë
void rtos_sleep(uint32_t ticks) {
tcb_t *self = &tcbs[current_thread];
self->sleep_ticks = ticks;
self->state = SLEEPING;
rtos_yield();
}
Fjalë kyçe “LED blinking” thread callsrtos_sleep(500)
Kur e shikoj çdo gjysmë sekonde, e di që SysTick dhe PendSV po bëjnë punën e tyre siç duhet.
Semantikë
Në fillim, unë u përpoqa një qasje naive:
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();
}
Prioritetin e Reversion Nightmare
Një ditë, unë kam pasur tre thread:
- T0 (Prioritet 2): mban semaforin.
- T1 (Prioritet 1): duke pritur në atë semafor.
- T2 (Prioritet 3): gati për të kandiduar dhe më i lartë se T0 por më i ulët se T1.
Për shkak se T1 ishte bllokuar, T2 vazhdoi të vraponte – kurrë nuk i dha T0 një shans për të lëshuar semaforin për T1. T1 u uritur. hakimi im i shpejtë ishte për të rritur përkohësisht prioritetin e T0 kur T1 u bllokua – kjo është një trashëgimi rudimentare e prioritetit. Një zgjidhje e plotë do të gjurmonte se cili fije mban semaforin dhe do të ngrejë prioritetin e tij automatikisht.
Për të shkarkuar gjithçka:Kërkoni të filloni ()
Kërkoni të filloni ()
nëmain()
, pas init bazë hardware (orë, GPIO për LEDs, UART për shell), unë bëra:
// 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
}
Disa shënime përfundimtare:
- Idle Thread (ID 0): Ajo thjesht kalon një LED në prioritet të ulët. në qoftë se diçka shkon keq, unë e di se unë të paktën kam arritur në thread idle.
- Prioriteti i ndërprerjes së UART: ndërprerja TX e UART-it kishte nevojë për një prioritet më të lartë se PendSV; përndryshe, thirrjet e gjata të printimit do të ndërprereshin në mes të transmetimit, duke penguar prodhimin.
The UART Shell: Peeking nën kapak
Kam ndërtuar një shell të vogël në mënyrë që të mund të shkruaj urdhra mbi UART dhe të inspektojë gjendjet e thread:
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");
}
}
}
Kjo shell ishte rrjeti im i sigurisë: nëse LED-të sillen çuditshëm, unë do të hidhesha në terminalin tim serik, shkruajps
, dhe menjëherë të shohim se cilat fusha ishin gati, bllokuar, ose duke fjetur.
Mësimet e mësuara gjatë rrugës
- Prioritetet e ndërprerjes janë gjithçka që kam kaluar një pasdite të tërë duke u bindur se kodi im PendSV ishte i gabuar – derisa e kuptova se do të vendosja SysTick dhe PendSV në të njëjtën prioritet. Pasi i dhashë SysTick një prioritet më të lartë, PendSV filloi të xhironte në mënyrë të besueshme.
- Matni Përdorimin e Stack-it tuaj Para-mbushni stack-in e secilit thread me një model të njohur (0xDEADBEEF) në nisje. Pas drejtimit, kontrolloni kujtesën për të parë se sa e thellë është mbivendosur modeli. Nëse treguesi juaj i stack-it ndonjëherë ecën në atë model, ju e dini se keni nevojë për një stack më të madh. E mësova këtë në mënyrën e vështirë kur një zinxhir më i thellë i thirrjes në thread-in tim të punonjësve shkaktoi një mbivendosje të heshtur.
- Përdorni LEDs & GPIO për Debug Nëse nuk keni një debugger fancy, thjesht kaloni një pin GPIO (ngjiteni atë në një LED). Unë vendosa një LED switch në fillim të PendSV_Handler dhe një tjetër në fund të saj.
- Keep It Simple at First Versioni im i parë i RTOS u përpoq të mbështeste krijimin dinamik të detyrave në kohën e drejtimit – gabim masiv. Unë përfundova me fragmentimin e kujtesës dhe rrëzime të çuditshme. Duke ngrirë numrin e telave në nisje (vetëm nëntë lojëra elektronike në rastin tim), unë shmangur një botë dhimbje.
- Dokumenti Çdo Numër Magjik Që 0xFFFFFFFD vlerë në "fake" kornizë stack? kam shkruar një shënim të shkurtër: "Lidhje regjistrimit vlerë që tregon kthimin në mënyrë Thread duke përdorur PSP."
Hapi i ardhshëm: Ku të shkoni nga këtu
Nëse vendosni të ndërtoni mbi këtë themel, këtu janë disa ide:
- Në vend të hack tim të shpejtë "boost-and-forget", zbatoni një protokoll të duhur ku prioriteti i thread bllokuar me prioritet më të lartë trashëgohet deri në lirimin e semaphore.
- Rreshtat e mesazheve të ndërlidhura Le të dërgojnë mesazhe të vogla ose tregues njëri-tjetrit në mënyrë të sigurtë.
- Dynamic Task Creation / Deletion Tack në një menaxher të vogël të grumbullimit ose të kujtesës, kështu që ju mund të krijoni dhe të vrisni fusha në fluturim - mendoni kompleksitetin shtesë!
- Profiling Hooks Zgjeroni menaxherin tuaj SysTick (ose përdorni një timer tjetër) për të regjistruar se sa ticks çdo fije punon.
- Kontrollet e Sigurisë në kohë reale Futni kufizimet e përdorimit të grumbullimit dhe zbuloni kur drejtuesi i grumbullimit të një fije kalon në rajonin 0xDEADBEEF - shkakton një mbyllje të sigurt ose rivendosje në vend të aksidenteve të rastësishme.
Mendimet e fundit
Ndërtimi i një RTOS minimale është i çrregullt, shpesh frustruese dhe absolutisht ndriçues. Nga ndjekja e një prioriteti të humbur PendSV për të luftuar me mbingarkesat e grumbulluara, çdo gabim më detyroi të kuptoja më thellë brendësinë e Cortex-M4. Nëse e provoni këtë në TM4C tuaj ose në ndonjë pjesë tjetër të Cortex-M, prisni disa netë debugging - por edhe kënaqësinë e thellë kur ndizet më në fund ajo LED e parë e besueshme.
Nëse ju jepni këtë një goditje, ju lutem më lejoni të di se cila pjesë ju çoi deri në mur. unë do të doja të dëgjojë historitë tuaja "LED flashing" ose ndonjë truket e tjera që keni zbuluar ndërsa ndjekin fantazmat në orarin tuaj.