Θυμάμαι ακόμα να κοιτάζω τα LEDs του TM4C123GH6PM μια νύχτα, περιμένοντας μια απλή «καρδιά» που δεν ήρθε ποτέ. Νόμιζα ότι έκανα τα πάντα σωστά: εγκατέστησα το SysTick, αναγκάστηκα το PendSV και ξεκίνησα τα Thread Control Blocks (TCBs). Αλλά αντί για μια σταθερή αναβοσβήνωση, τα LEDs αναβοσβήλησαν μία φορά, στη συνέχεια με παγώσανε – κοροϊδεύοντας. Αυτή η στιγμή κρυστάλλευσε αυτό που πραγματικά σημαίνει η οικοδόμηση ενός μικρού RTOS: καταπολέμηση των σκουπιδιών υλικού, κυνηγώντας απαράδεκτα σφάλματα και τοποθέτηση αρκετού κώδικα για
Ένα κομμάτι backstory
Πριν από περίπου ένα χρόνο, η τάξη ενσωματωμένων συστημάτων μου μας ανέθεσε να δημιουργήσουμε ένα απλό RTOS από το μηδέν σε ένα TM4C123GH6PM (ARM Cortex-M4). είχα χρησιμοποιήσει το FreeRTOS πριν, αλλά ποτέ δεν κατάλαβα πλήρως τι συνέβαινε πίσω από τις σκηνές.
Why?
- Ήθελα να δω ακριβώς πώς η CPU μετακινείται από το ένα νήμα στο άλλο.
- Έπρεπε να μάθω γιατί οι εργασίες χαμηλής προτεραιότητας μπορούν να λιμοκτονήσουν ακούσια τις εργασίες υψηλότερης προτεραιότητας (γεια σας, αντιστροφή προτεραιότητας).
- Μου άρεσαν πραγματικές ιστορίες χειραγώγησης - όπως εκείνη η ώρα που ξόδεψα μισή μέρα αναρωτιέμαι γιατί το PendSV δεν έτρεξε ποτέ (διαπιστώνεται ότι το είχα δώσει την ίδια προτεραιότητα με το SysTick).
Spoiler: Το RTOS μου δεν ήταν τέλειο, αλλά κυνηγώντας τα ελαττώματά του, έμαθα περισσότερα για τα εσωτερικά του ARM από οποιοδήποτε εγχειρίδιο.
Ανακαλύψτε το Cortex-M4 World
Πριν γράψω μια ενιαία γραμμή κώδικα, έπρεπε να τυλίξω το κεφάλι μου γύρω από το πώς το Cortex-M4 χειρίζεται τις διακοπές και την αλλαγή του πλαισίου.
- 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.
Εφαρμογή του Thread Control Block (TCB)
Κάθε τοίχος στο RTOS μου έχει:
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;
Στην πράξη δηλώνω:
#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
Όταν πρώτα αναθέτωSTACK_WORDS = 128
, όλα φαινόταν εντάξει - μέχρι το "εργατικό" νήμα μου, το οποίο έκανε μερικές συνδεδεμένες κλήσεις λειτουργίας, άρχισε να καταστρέφει τη μνήμη. το LED θα αναβοσβήνει δύο φορές, στη συνέχεια εξαφανίζεται.0xDEADBEEF
κατά την εκκίνηση και ελέγχοντας πόσο μακριά έχει υπεργραφεί, ανακάλυψα ότι 128 λέξεις δεν ήταν αρκετές κάτω από βελτιστοποιημένες σημαίες κατασκευής.
Ανοίγοντας ένα νέο νήμα
Η δημιουργία ενός νήματος σήμαινε να σκαλιστεί το στοίβα του και να προσομοιωθεί η συσσώρευση υλικού που συμβαίνει κατά την είσοδο εξαιρέσεων.
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;
}
Πύργοι για να προσέξετε
- Χωρίς αυτό, η CPU σας θα προσπαθήσει να ερμηνεύσει τον κώδικα ως οδηγίες ARM - άμεσο σφάλμα.
- Το Magic 0xFFFFFFFD. Αυτό λέει στην CPU, "Σε εξαίρεση επιστρέψτε, χρησιμοποιήστε το PSP και μεταβείτε στη λειτουργία Thread." θυμάμαι να ψάχνω το ARM ARM (Architecture Reference Manual) τουλάχιστον τρεις φορές για να το κάνω σωστά.
- Πιέζοντας το R4–R11 με μη αυτόματο τρόπο πρέπει να ακολουθεί την ακριβή σειρά που αναμένει ο χειριστής.Μια απλή πληκτρολόγηση (π.χ. πιέζοντας το R11 πρώτα αντί για το R4) πετάει ολόκληρο το πλαίσιο.
Αφήνοντας τον χρόνο να περάσει: SysTick Handler
Εδώ είναι το τελευταίο μου SysTick ISR, κολλημένο στα βασικά:
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
}
Ένα ζευγάρι σημειώσεις:
- Σε μια πρώιμη έκδοση, προσπάθησα να καλέσω rtos_schedule() ακριβώς μέσα στο SysTick. Αυτό οδήγησε σε διακοπές και σύγχυση στοίβα.
- Ανακάλυψα ότι αν το SysTick_IRQn και το PendSV_IRQn μοιράζονται την ίδια προτεραιότητα, το PendSV μερικές φορές δεν εκτελείται.
Ο μεγάλος διακόπτης: PendSV Handler
Όταν το PendSV τελικά πυροδοτείται, κάνει την πραγματική αλλαγή του πλαισίου. η εφαρμογή μου στο GCC inline assembly (ARM σύνταξη) μοιάζει με αυτό:
__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
);
}
Πώς παρακολούθησα αυτό το κακόβουλο 32-byte offset
Στην αρχή, ο κώδικας μου "αποθήκευση / επαναφορά" ήταν απενεργοποιημένος με 32 bytes. το σύμπτωμα; Τα νήματα θα τρέξουν, στη συνέχεια με κάποιο τρόπο θα επιστρέψουν στη ζωή στο λάθος μέρος - γρατζουνισμένες οδηγίες, τυχαία άλματα. πρόσθεσα μερικά τσιμπήματα GPIO (επικαλύπτοντας ένα pin LED λίγο πριν και μετάSTMDB
) για να μετρήσετε με ακρίβεια πόσα bytes πιέστηκαν.Στο debugger μου, στη συνέχεια συνέκρινα την αριθμητική τιμή PSP με τοtcbs[].stack_ptr
Σίγουρα αρκετά, είχα τυχαία χρησιμοποιήσειSTMDB R0!, {R4-R11, R12}
Αντί να{R4-R11}
Η απομάκρυνση αυτού του πρόσθετου μητρώου το επιδιορθώνει.
Επιλέξτε το επόμενο θέμα: Λογική προγραμματιστή
Ο προγραμματιστής μου είναι σκόπιμα απλός. Σαρώνει όλα τα TCBs για να βρει το νήμα READY με την υψηλότερη προτεραιότητα, με μια μικρή προσαρμογή του στρογγυλού ρουμπίνι για νήματα ίσης προτεραιότητας:
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:
- Εάν δύο νήματα μοιράζονται προτεραιότητα 1, αλλά πάντα επιλέγω το χαμηλότερο αναγνωριστικό, το άλλο δεν θα πάρει ποτέ χρόνο CPU.
- Το άδικο νήμα (ID 0) είναι μια ειδική περίπτωση: πάντα ΕΤΟΙΜΟ, πάντα χαμηλότερη προτεραιότητα (προτεραιότητα = 255), έτσι ώστε αν τίποτα άλλο δεν είναι τρέξιμο, απλά περιστρέφεται (ή καλεί __WFI() για να εξοικονομήσει ενέργεια).
Πρωτόγονες πυρήνες: απόδοση, ύπνος και ημιφορίες
Μετά την ανίχνευση των βασικών στοιχείων, το επόμενο βήμα ήταν να κάνουν τα νήματα να αλληλεπιδρούν σωστά.
Κέρδη
void rtos_yield(void) {
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
Μου αρέσει το σπρινγκrtos_yield()
στο τέλος των μεγάλων κυκλωμάτων έτσι ώστε τα άλλα νήματα να πάρουν μια δίκαιη σφαίρα.yield()
Αυτό σήμαινε ότι ορισμένες εργασίες κατέστρεψαν την CPU κάτω από ορισμένες διαμορφώσεις προτεραιότητας.
Ύπνος
void rtos_sleep(uint32_t ticks) {
tcb_t *self = &tcbs[current_thread];
self->sleep_ticks = ticks;
self->state = SLEEPING;
rtos_yield();
}
Το “Led Blinking” της σειράςrtos_sleep(500)
Όταν το βλέπω να αναβοσβήνει κάθε μισό δευτερόλεπτο, ξέρω ότι το SysTick και το PendSV κάνουν τη δουλειά τους σωστά.
ΣΕΜΑΦΟΡΕΣ
Αρχικά, δοκίμασα μια αφελής προσέγγιση:
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();
}
Προτεραιότητα Επανάσταση Εφιάλτης
Μια μέρα, είχα τρία threads:
- T0 (Προτεραιότητα 2): κρατά το σμήνος.
- T1 (Προτεραιότητα 1): Περιμένοντας αυτό το φως.
- T2 (Προτεραιότητα 3): έτοιμο να τρέξει και υψηλότερο από το T0 αλλά χαμηλότερο από το T1.
Επειδή το T1 ήταν μπλοκαρισμένο, το T2 συνέχισε να τρέχει - ποτέ δεν έδινε στον T0 την ευκαιρία να απελευθερώσει το φως για το T1. Το T1 λιμοκτόνησε. Το γρήγορο hack μου ήταν να αυξήσω προσωρινά την προτεραιότητα του T0 όταν το T1 μπλοκαρίστηκε - είναι μια στοιχειώδης κληρονομιά προτεραιότητας. μια πλήρης λύση θα παρακολουθούσε ποιο νήμα κρατά το φως και θα ανυψώνει αυτόματα την προτεραιότητά του.
Αφαίρεσε τα πάντα:ΑΡΧΙΚΗ ΕΠΙΧΕΙΡΗΣΗ(
ΑΡΧΙΚΗ ΕΠΙΧΕΙΡΗΣΗ(
στηνmain()
, μετά από βασική init υλικού (ωρολόγια, GPIO για LEDs, UART για το κέλυφος), έκανα:
// 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
}
Ένα ζευγάρι τελικών σημειώσεων:
- Idle Thread (ID 0): Απλά μετακινεί ένα LED σε χαμηλή προτεραιότητα. αν κάτι πάει στραβά, ξέρω ότι τουλάχιστον έχω φτάσει στο άδικο νήμα.
- Προτεραιότητα διακοπής UART: Η διακοπή TX του UART χρειάστηκε υψηλότερη προτεραιότητα από το PendSV. διαφορετικά, οι μακρές κλήσεις printf θα διακόπτονταν στη μέση της μετάδοσης, μειώνοντας την εξαγωγή.
The UART Shell: Peeking Under the Hood – Το καπάκι κάτω από το καπάκι
Κατασκεύασα ένα μικρό κέλυφος έτσι ώστε να μπορώ να πληκτρολογήσω εντολές πάνω από το UART και να εξετάσω τις καταστάσεις του νήματος:
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");
}
}
}
Αυτό το κέλυφος ήταν το δίχτυ ασφαλείας μου: αν τα LEDs συμπεριφέρονταν παράξενα, θα πήγαινα στο σειριακό τερματικό μου, πληκτρολογήστεps
, και αμέσως δείτε ποια νήματα ήταν έτοιμα, μπλοκαρισμένα ή κοιμούνται. με έσωσε ώρες μαντείας.
Τα μαθήματα που μαθαίνουμε στο δρόμο
- Οι προτεραιότητες διακοπής είναι όλα όσα πέρασα ένα ολόκληρο απόγευμα πείθοντας τον κώδικα PendSV μου ότι ήταν λάθος - μέχρι που συνειδητοποίησα ότι είχα ορίσει το SysTick και το PendSV στην ίδια προτεραιότητα. Μόλις έδωσα στο SysTick υψηλότερη προτεραιότητα, το PendSV άρχισε να πυροβολεί αξιόπιστα. Μάθημα: διπλό και τριπλό έλεγχο των κλήσεων NVIC_SetPriority() σας.
- Μετρήστε τη χρήση του Stack σας Προ-συμπληρώστε κάθε στοίβα του νήματος με ένα γνωστό μοτίβο (0xDEADBEEF) κατά την εκκίνηση. Μετά την εκκίνηση, ελέγξτε τη μνήμη για να δείτε πόσο βαθιά έχει υπεργραφεί το μοτίβο. Εάν ο δείκτης στοίβα σας μπαίνει ποτέ σε αυτό το μοτίβο, ξέρετε ότι χρειάζεστε ένα μεγαλύτερο στοίβα. το έμαθα με τον σκληρό τρόπο όταν μια βαθύτερη αλυσίδα κλήσεων στο νήμα του εργαζόμενου μου προκάλεσε μια σιωπηλή υπεργραφή.
- Χρησιμοποιήστε LEDs & GPIO για Debug Αν δεν έχετε ένα φανταστικό debugger, απλά μετακινήστε ένα pin GPIO (συνδέστε το σε ένα LED). Έβαλα ένα LED switch στην αρχή του PendSV_Handler και ένα άλλο στο τέλος του.
- Η πρώτη μου έκδοση RTOS προσπάθησε να υποστηρίξει τη δημιουργία δυναμικών εργασιών στο runtime – ένα τεράστιο σφάλμα. κατέληξα σε κατακερματισμό της μνήμης και παράξενες συντριβές.
- Έγραψα μια σύντομη σημείωση: «Αξία μητρώου σύνδεσης που υποδεικνύει την επιστροφή στη λειτουργία Thread χρησιμοποιώντας το PSP».
Επόμενο Επόμενο άρθρο: Πού να πάτε από εδώ
Αν αποφασίσετε να οικοδομήσετε σε αυτό το θεμέλιο, εδώ είναι μερικές ιδέες:
- Πλήρης κληρονομιά προτεραιότητας για Semaphores Αντί για το γρήγορο μου hack "boost-and-forget", εφαρμόστε ένα κατάλληλο πρωτόκολλο όπου η προτεραιότητα του αποκλεισμένου νήματος υψηλής προτεραιότητας κληρονομείται μέχρι να απελευθερωθεί το semaphore.
- Αφήστε τα νήματα να στέλνουν μικρά μηνύματα ή δείκτες μεταξύ τους με ασφάλεια. Σκεφτείτε το σαν ένα μικρό γραμματοκιβώτιο για τη μετάδοση δεδομένων μεταξύ των εργασιών.
- Δυναμική δημιουργία / διαγραφή εργασιών Χρησιμοποιήστε έναν μικρό διαχειριστή ομαδικών στοιχείων ή μια πισίνα μνήμης, ώστε να μπορείτε να δημιουργήσετε και να σκοτώσετε τους άξονες στην πτήση - σκεφτείτε την επιπλέον πολυπλοκότητα!
- Επεκτείνετε τον χειριστή SysTick (ή χρησιμοποιήστε έναν άλλο χρονοδιακόπτη) για να καταγράψετε πόσα τσιμπήματα εκτελεί κάθε νήμα.
- Έλεγχοι ασφαλείας σε πραγματικό χρόνο Εισάγετε όρια χρήσης στοίβας και ανιχνεύετε όταν ο δείκτης στοίβας ενός νήματος διασχίζει την περιοχή 0xDEADBEEF - προκαλώντας ασφαλή διακοπή λειτουργίας ή επαναφορά αντί για τυχαίες συντριβές.
Τελικές Σκέψεις
Η οικοδόμηση ενός ελάχιστου RTOS είναι ακατάλληλη, συχνά απογοητευτική και απολύτως διαφωτιστική.Από το να κυνηγάτε μια λανθασμένη προτεραιότητα PendSV έως την καταπολέμηση των υπερβολικών ροών, κάθε σφάλμα με ανάγκασε να κατανοήσω τα εσωτερικά του Cortex-M4 πιο βαθιά.Αν δοκιμάσετε αυτό στο δικό σας TM4C ή σε οποιοδήποτε άλλο μέρος του Cortex-M, περιμένετε μερικές νύχτες χειραγώγησης - αλλά και τη βαθιά ικανοποίηση όταν εμφανίζεται τελικά η πρώτη αξιόπιστη λάμψη LED.
Αν δώσετε αυτό ένα shot, παρακαλώ αφήστε με να ξέρω ποιο μέρος σας οδήγησε στον τοίχο. θα ήθελα να ακούσω τις δικές σας ιστορίες "LED flashing" ή οποιαδήποτε άλλα κόλπα που ανακαλύψατε ενώ κυνηγάτε φαντάσματα στον προγραμματιστή σας.