297 показания

Започнете с RTOS: Ръководство за начинаещи с Cortex-M4

от manojgowda...15m2025/06/04
Read on Terminal Reader

Твърде дълго; Чета

Изграждането на минимална RTOS е объркващо, често разочароващо и абсолютно просветляващо.
featured image - Започнете с RTOS: Ръководство за начинаещи с Cortex-M4
Manoj Gowda HackerNoon profile picture

Все още си спомням да гледам светодиодите на TM4C123GH6PM една вечер, в очакване на прост „сърдечен удар“, който никога не е дошъл. Мислех, че съм направил всичко правилно: създадох SysTick, преследвах PendSV и инициализирах моите блокчета за контрол на нишките (TCBs). Но вместо стабилно мигане, светодиодите се въртяха веднъж, след това ме замразяваха. Този момент кристализираше това, което изграждането на малка RTOS наистина означава: борбата с хардуерните страсти, преследването на неуловими бъгове и поставянето на достатъчно код, за да направя всичко гладко.


Малко backstory

Преди около година, моят клас вградени системи ни възложи да изградим проста RTOS от нулата на TM4C123GH6PM (ARM Cortex-M4). Аз използвах FreeRTOS преди, но никога не разбрах напълно какво се случва зад кулисите.

Why?

  • Исках да видя как точно процесорът преминава от една нишка към друга.
  • Трябваше да науча защо задачите с нисък приоритет могат случайно да потискат задачите с по-висок приоритет (здравейте, преобръщане на приоритета).
  • Исках истински истории за дебютиране - като това време прекарах половин ден, за да се чудя защо PendSV никога не работи (излиза, че съм му дал същия приоритет като SysTick).

Спойлер: Моят RTOS не беше перфектен, но в преследването на неговите недостатъци научих повече за ARM интерналите, отколкото от която и да е учебница.


Потапяне в света на Cortex-M4

Преди да напиша един ред код, трябваше да завъртя главата си около това как Cortex-M4 се справя с прекъсванията и превключването на контекста.

  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.

Създаване на блок за контрол на нишките (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;

A Stack-Overflow Horror Story / История на ужасите

Когато за пръв път разпределих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;
}

Пъзели, за да гледате

  • Липсата му означава, че вашият процесор ще се опита да интерпретира кода като ARM инструкции – непосредствена грешка.
  • Magic 0xFFFFFFFD. Това казва на процесора: "На изключение се върнете, използвайте PSP и отидете в режим Thread."
  • Ръчно натискане 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-битов компенсатор

Първоначално моят код "спасяване / възстановяване" беше изключен с 32 байта. Симптомът? нишките ще се изпълняват, след това по някакъв начин се връщат към живота на грешното място - нарязани инструкции, случайни скокове. добавих няколко GPIO скоби (сгъване на LED пин точно преди и следSTMDB) за да се измери точно колко байта са били натиснати. В моя дебугер, аз след това сравнявам цифровата стойност на 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, но винаги избирам по-ниския ID, другият никога няма да получи CPU време.
  • Бездейната нишка (ID 0) е специален случай: винаги готова, винаги с най-нисък приоритет (приоритет = 255), така че ако нищо друго не може да се изпълни, тя просто се върти (или се обажда __WFI() за спестяване на мощност).

Kernel Primitives: Ръст, сън и семафори

След като сте измисляли основите, следващата стъпка беше да направите нишките да взаимодействат правилно.

Доходите

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

Харесва ми спринцовкатаrtos_yield()в края на дългите кръгове, така че другите нишки получават справедлив удар.yield()Това означава, че някои задачи са преместили процесора под определени конфигурации с приоритет.

Сън

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

Моите “LED Blinking” thread callsrtos_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();
}

Инверсионният кошмар

Един ден имах три трика:

  • T0 (приоритет 2): задържа семафора.
  • T1 (Приоритет 1): изчакайте този светофар.
  • T2 (Приоритет 3): готов за работа и по-висок от T0 но по-нисък от T1.

Тъй като T1 беше блокиран, T2 продължи да работи – никога не давайки на T0 шанс да пусне семафора за T1. T1 гладува. Моят бърз хак беше временно да повиша приоритета на 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 биха били прекъснати в средата на предаването, което ще намали изхода.

Оригинално име: 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");
        }
    }
}

Тази обвивка беше моята мрежа за безопасност: ако светодиодите се държаха странно, щях да скоча в сериалния си терминал,ps, и веднага вижте кои нишки са готови, блокирани или спящи.


Уроци, които научихме по пътя

  1. Прекъсване на приоритетите са всичко, което прекарах цял следобед, за да се убедя, че кодът ми PendSV е грешен, докато не осъзнах, че съм поставил SysTick и PendSV на един и същ приоритет.
  2. Измерете използването на вашия стак Предварително попълнете стака на всяка нишка с известен модел (0xDEADBEEF) при стартиране. След като стартирате, проверете паметта, за да видите колко дълбоко е надписан моделът. Ако вашият показател за стак някога влезе в този модел, знаете, че имате нужда от по-голям стак. Научих това по труден начин, когато по-дълбока верига за обаждания в работната ми нишка предизвика мълчаливо надписване.
  3. Използвайте LEDs & GPIO да дебютирате Ако нямате фантастичен дебютер, просто преместете пин GPIO (прикрепете го към LED). Поставих един LED преход в самото начало на PendSV_Handler и друг в края му.
  4. Първата ми версия на RTOS се опита да поддържа динамично създаване на задачи по време на изпълнение - масивна грешка. завърших с фрагментация на паметта и странни сривове.
  5. Документ Всеки магически номер, че 0xFFFFFFFD стойност в "фалшива" стек рамка? написах кратка бележка: "Свързване регистрационна стойност, която показва връщане към режим на нишка с помощта на PSP."

Следваща Статия Къде да отидем от тук

Ако решите да изградите върху тази основа, ето няколко идеи:

  • Пълно приоритетно наследяване за Semaphores Вместо моя бърз хак "засилване и забравяне", изпълнете правилния протокол, където приоритетът на блокираната нишка с най-висок приоритет се наследява, докато се освободи semaphore.
  • Междинни редове за съобщения Позволете на нишките да изпращат малки съобщения или указатели един на друг безопасно.
  • Dynamic Task Creation/Deletion Tack на малък мениджър на купчина или на базата на паметта, така че можете да създавате и убивате нишки по време на полета – имайте предвид допълнителната сложност!
  • Разширете вашия SysTick манипулатор (или използвайте друг таймер), за да запишете колко тикове изпълнява всяка нишка.
  • Проверки за безопасност в реално време Въвеждане на ограничения за използване на стека и откриване, когато указателят на стека на нишка пресича региона 0xDEADBEEF – задейства безопасно изключване или нулиране вместо случайни сривове.

Заключителни мисли

Изграждането на минимална RTOS е объркващо, често разочароващо и абсолютно просветляващо.От преследването на неправилно поставен приоритет на PendSV до борбата с натрупването на излишъци, всеки бъг ме принуждава да разбера вътрешните работи на Cortex-M4 по-дълбоко.Ако опитате това на собствения си TM4C или на всяка друга част на Cortex-M, очаквайте няколко нощи дебютиране - но също така и дълбокото удовлетворение, когато най-накрая се появи този първи надежден светодиод.

Ако дадете тази снимка, моля, кажете ми коя част ви е качила на стената.Бих искал да чуя вашите собствени "LED мигащи" истории или каквито и да е други трикове, които сте открили, докато преследвате призраци във вашия графика.

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.

ЗАКАЧВАЙТЕ ЕТИКЕТИ

ТАЗИ СТАТИЯ Е ПРЕДСТАВЕНА В...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks