Super-loop ne zaman yalan söylemeye başlar?
İlk projenizde sorun yoktu: bir while(1) döngüsü, birkaç görev, her şey sırayla. Ama proje büyüdükçe döngünün turu uzadı. Motor kontrol geç tetiklendi. UART verisi kayboldu. Düğmeye basıldı, cihaz birkaç döngü sonra fark etti. Kodu düzelttiniz, ama sonraki görev gelince aynı şey oldu.
Bu noktada iki yol var: super-loop'u daha fazla yamayarak ilerlemek ya da bir RTOS'un gerçekte ne sağladığını anlamak.
Gömülü sistem geliştirmeye başladığınızda ilk yazdığınız program muhtemelen şöyle görünüyordu: bir while(1) döngüsü içinde LED yakıp söndür, belki bir UART'tan veri oku. Bu bare-metal programlamadır — doğrudan donanım, doğrudan kontrol, araya giren hiçbir soyutlama katmanı yok.
Proje büyüdükçe şu soru kaçınılmaz biçimde ortaya çıkar: "Bir RTOS'a geçmeli miyim?"
Kısa cevap: çoğu zaman gerekmez. Ama gerekenler için RTOS olmadan yaşamak gerçekten zordur.
Bare-metal programlamada temel yapı super-loop'tur: sonsuz bir ana döngü, donanımsal olayları kesmeler (interrupt) aracılığıyla yakalar.
int main(void) {
system_init();
while (1) {
if (uart_data_ready()) {
process_uart();
}
update_display();
read_sensors();
run_control_loop();
}
}
void USART1_IRQHandler(void) {
uart_buffer_push(USART1->RDR);
}
Bu yapı tahmin edilebilir, şeffaf ve verimsizdir — son kelimeyi olumsuz anlamda söylemiyorum. Tek bir görev yapılıyorsa, ya da görevler birbirini kesmiyorsa super-loop mükemmel bir çözümdür.
Şu senaryoyu düşünün: STM32 tabanlı bir cihaz geliştiriyorsunuz. Aynı anda şunları yapması gerekiyor:
Super-loop bu durumda güçlükle karşılaşır çünkü döngünün her turu ne kadar sürer? Bilemezsiniz — read_sensors() ne kadar tutacak, update_display() ne kadar tutacak? Bir görev uzun sürerse diğerleri beklemek zorunda kalır.
while (1) {
run_motor_control(); /* 8 ms sürüyor */
update_display(); /* 6 ms sürüyor */
read_uart(); /* anlık, ama bazen bekleme var */
/* Toplam: ~14 ms — motor kontrolü 10 ms'de bir çalışması gerekiyordu! */
}
Motor kontrolü 10 ms'de bir çalışması gerekiyor, ama döngü 14 ms sürüyor. Geçici çözüm olarak her görevi ayrı bir sayaç ve zamanlayıcı kesmeyle tetikleyebilirsiniz — ama bu yaklaşım kaç göreve kadar ölçeklenir?
update_display() SPI üzerinden çalışıyor ve 6 ms boyunca meşgul-bekleyerek (busy-wait) bekliyorsa, bu süre boyunca başka hiçbir şey işlenemiyor. DMA kullanmak çözüm sunar ama kodu karmaşıklaştırır; tamamlanma callback'ini yönetmek zorunda kalırsınız.
Düğmeye basma olayı kritik — 50 ms içinde yanıt verilmeli. Ama o an update_display() içindeyseniz ve o 6 ms boyunca döngü oraya kilitliyse ne yaparsınız? Kesme kullanırsınız elbette. Ama kesme içinde ne kadar iş yapılabilir? Ne kadar veri paylaşılabilir? Senkronizasyon olmadan yarış koşulları (race conditions) kapıda bekler.
Bir RTOS (Real-Time Operating System), tek bir yürütme akışı yerine birden fazla görev (task) çalıştırmanıza olanak tanır. Her görevin kendi yığını (stack) ve önceliği vardır. RTOS çekirdeği, görevler arasındaki geçişi (context switch) yönetir.
En yaygın gömülü RTOS'lardan biri FreeRTOS'tur. STM32 için ücretsiz, açık kaynaklı ve geniş topluluk desteğine sahiptir.
#include "FreeRTOS.h"
#include "task.h"
/* Motor kontrol görevi — her 10 ms'de bir çalışır */
void motor_control_task(void *params) {
TickType_t last_wake = xTaskGetTickCount();
while (1) {
run_motor_control();
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(10)); /* Tam 10 ms bekle */
}
}
/* Ekran güncelleme görevi — her 1000 ms'de bir çalışır */
void display_task(void *params) {
while (1) {
update_display();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* UART okuma görevi — veri geldiğinde uyandırılır */
void uart_task(void *params) {
while (1) {
uint8_t byte;
if (xQueueReceive(uart_queue, &byte, portMAX_DELAY)) {
process_uart_byte(byte);
}
}
}
int main(void) {
system_init();
xTaskCreate(motor_control_task, "Motor", 256, NULL, 3, NULL); /* Öncelik 3 */
xTaskCreate(display_task, "Display", 256, NULL, 1, NULL); /* Öncelik 1 */
xTaskCreate(uart_task, "UART", 256, NULL, 2, NULL); /* Öncelik 2 */
vTaskStartScheduler();
}
Burada olan şu: RTOS çekirdeği her görevi önceliğine göre zamanlıyor. Motor kontrol görevi (öncelik 3) en yüksek önceliğe sahip — vTaskDelayUntil ile tam olarak 10 ms'de bir uyandırılıyor, diğer görevler ne yapıyor olursa olsun.
Görev önceliklendirme: Kritik görevler düşük öncelikli görevleri keser. Motor kontrol, ekran güncellemesinin bitmesini beklemez.
Bloke etmeyen bekleme: vTaskDelay() çağrıldığında görev uyku moduna geçer — CPU zamanını tüketmez. Diğer görevler o sürede çalışabilir.
Senkronizasyon nesneleri: Görevler arasında güvenli veri paylaşımı için kuyruklar (queue), semaforlar (semaphore) ve muteksler (mutex) kullanılabilir.
/* Kuyruk üzerinden güvenli veri paylaşımı */
QueueHandle_t uart_queue;
void USART1_IRQHandler(void) {
uint8_t byte = USART1->RDR;
BaseType_t higher_priority_woken = pdFALSE;
xQueueSendFromISR(uart_queue, &byte, &higher_priority_woken);
portYIELD_FROM_ISR(higher_priority_woken);
}
Kesme (IRQ) ile görev arasındaki veri transferi kuyruk üzerinden yapılıyor — yarış koşulu riski ortadan kalkıyor.
RTOS bedava değildir. Her şeyin bir bedeli vardır.
| Kaynak | Bare-metal | FreeRTOS (tipik) |
|---|---|---|
| Çekirdek Flash | 0 KB | ~5–10 KB |
| Her görev stack'i | — | 256–1024 byte (göreve göre) |
| Görev kontrol bloğu (TCB) | — | ~100 byte/görev |
| Kuyruk, semafor nesneleri | — | ~80 byte/nesne |
STM32F070RB'nin 16 KB SRAM'i var. Üç görev ve birkaç senkronizasyon nesnesiyle kolayca 4–6 KB harcarsınız. Küçük değil, ama yönetilebilir.
Context switch — bir görevden diğerine geçiş — birkaç mikrosaniye sürer. Çoğu uygulama için önemsizdir. Ama mikrosaniye hassasiyeti gerektiren sinyal işleme veya motor kontrolü yapıyorsanız, bu gecikme sorun yaratabilir. Bu durumda o kritik görev için kesmeler tercih edilmelidir.
Hata ayıklama zorlaşır. Yarış koşulları hâlâ mümkündür — şimdi de kilitlenme (deadlock) ve öncelik terslemesi (priority inversion) riskleri vardır. RTOS araçları (FreeRTOS+Trace, Segger SystemView) bu sorunları görünür kılmaya yardımcı olur ama öğrenme eğrisi gerçektir.
İkisi de birbirini dışlamaz. Sık kullanılan bir desen şudur: RTOS ile çalışırken zamanlama açısından kritik olan kısımları kesmeler ve donanım zamanlayıcıları aracılığıyla yönetmek, geriye kalan her şeyi görevlere bırakmak.
/* Kritik: donanım zamanlayıcısıyla tam zamanlama — RTOS'tan bağımsız */
void TIM2_IRQHandler(void) {
run_motor_foc_algorithm(); /* Her 100 µs'de bir, kesinlikle */
}
/* Kritik olmayan: RTOS görevi */
void telemetry_task(void *params) {
while (1) {
send_status_over_uart();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
Motor kontrol algoritması kesme içinde çalışıyor — RTOS scheduler'ı ne yaparsa yapsın, bu kod tam 100 µs'de bir tetikleniyor. Telemetri görevi ise RTOS üzerinde, kesintiye uğrayabilir bir görev olarak çalışıyor.
| Kriter | Bare-Metal | RTOS |
|---|---|---|
| Bellek kullanımı | Minimum | +5–15 KB Flash, +birkaç KB SRAM |
| Zamanlama kesinliği | Tam kontrol | Görev düzeyinde; ISR ile desteklenir |
| Eşzamanlı görev yönetimi | Zor, elle yazılır | Yerleşik |
| Hata ayıklama kolaylığı | Yüksek | Orta (araç gerektirir) |
| Ölçeklenebilirlik | Sınırlı | İyi |
| Öğrenme eğrisi | Düşük | Orta |
| İdeal kullanım | Tek amaçlı, zamanlama-kritik | Çok görevli, bloke edebilen işlemler içeren |
Bare-metal ile başlayın. Zamanlama çakışmalarını ve senkronizasyon sorunlarını super-loop'la çözmekte güçlük çekmeye başladığınızda, RTOS geçişini değerlendirin. Bir RTOS'un ne zaman gerçekten gerekli hale geldiğini anlatmak istiyorsanız, "Ne Zaman Bir RTOS'a İhtiyaç Duyarsınız?" başlıklı yazımıza da göz atabilirsiniz.
Kitap serisini incele →