STM32 ile Gerçek Zamanlı Gömülü Sistem Geliştirme
Giriş: Gerçek Zamanlı Sistemler Neden Önemli?
Gömülü sistemler dünyasında gerçek zamanlı (real-time) kavramı, bir işlemin sadece doğru sonuç üretmesini değil, aynı zamanda belirlenen zaman sınırları içinde bu sonucu üretmesini ifade eder. Bir antilock fren sistemi, bir kalp pili veya bir endüstriyel robot kolu düşünün; bu sistemlerde milisaniyelik gecikmeler bile felaket anlamına gelebilir. İşte tam da bu noktada STM32 mikrodenetleyici ailesi, sunduğu güçlü donanım özellikleri ve geniş ekosistemiyle gerçek zamanlı gömülü sistem geliştirme için en popüler platformlardan biri haline gelmiştir.
Bu yazıda STM32 mikrodenetleyicileri kullanarak gerçek zamanlı sistem geliştirmenin temel prensiplerini, RTOS entegrasyonunu, zamanlayıcı ve kesme yönetimini, pratik kod örnekleriyle birlikte derinlemesine inceleyeceğiz.
STM32 Ailesine Genel Bakış
STMicroelectronics tarafından üretilen STM32 serisi, ARM Cortex-M çekirdeklerine dayanan 32-bit mikrodenetleyicilerdir. Aile içinde farklı ihtiyaçlara yönelik birçok alt seri bulunur:
- STM32F0 / STM32G0: Düşük maliyetli, giriş seviyesi uygulamalar için (Cortex-M0/M0+)
- STM32F1 / STM32F4: Genel amaçlı, orta-yüksek performans gerektiren projeler için (Cortex-M3/M4)
- STM32F7 / STM32H7: Yüksek performans, DSP ve çift çekirdek desteği ile zorlu gerçek zamanlı uygulamalar için (Cortex-M7)
- STM32L0 / STM32L4 / STM32U5: Ultra düşük güç tüketimi gerektiren IoT ve bataryalı sistemler için
- STM32WB / STM32WL: Kablosuz haberleşme (BLE, LoRa) entegre edilmiş çözümler
Gerçek zamanlı uygulamalar için özellikle STM32F4 ve STM32H7 serileri öne çıkar. Bu çekirdekler tek duyarlıklı kayan nokta birimi (FPU), yüksek saat hızları (480 MHz'e kadar) ve zengin zamanlayıcı alt sistemleri sunar.
Geliştirme Ortamının Kurulumu
STM32 ile geliştirmeye başlamak için temel araç zincirine ihtiyacınız vardır:
- STM32CubeMX: Pin konfigürasyonu, saat ağacı ayarları ve çevresel birim başlatma kodunu grafiksel arayüzle oluşturmanızı sağlar.
- STM32CubeIDE: Eclipse tabanlı, ücretsiz entegre geliştirme ortamı. Derleme, hata ayıklama ve flash programlama işlemlerini tek çatı altında toplar.
- ARM GCC Toolchain: Komut satırından çalışmayı tercih edenler için
arm-none-eabi-gccderleyicisi. - ST-Link veya J-Link: Donanım hata ayıklayıcı ve programlayıcı.
- HAL / LL Kütüphaneleri: STM32 donanım soyutlama katmanı (HAL) veya daha düşük seviyeli Low-Layer (LL) sürücüleri.
Proje oluşturma sürecinde CubeMX ile çevresel birimleri yapılandırdıktan sonra, oluşturulan kodu CubeIDE ya da tercih ettiğiniz editörde (VS Code + CMake gibi) düzenleyebilirsiniz.
Bare-Metal Gerçek Zamanlı Programlama
Bir RTOS kullanmadan, yalnızca kesme (interrupt) mekanizmaları ve zamanlayıcılarla gerçek zamanlı davranış elde etmek mümkündür. Bu yaklaşıma bare-metal programlama denir. Küçük ve belirli görevlerde oldukça etkilidir.
Zamanlayıcı (Timer) Kullanımı
STM32'nin gelişmiş zamanlayıcıları (TIM1, TIM2, vb.) periyodik görevleri hassas bir şekilde zamanlamak için kullanılır. Aşağıdaki örnek, TIM2 zamanlayıcısını 1 ms periyotla kesme üretecek şekilde yapılandırır:
/* TIM2 zamanlayıcısını 1 ms periyotla yapılandır */
void MX_TIM2_Init(void)
{
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = (SystemCoreClock / 1000000) - 1; /* 1 MHz sayıcı */
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1000 - 1; /* 1 ms periyot */
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2);
}
/* Zamanlayıcı kesme geri çağırma fonksiyonu */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
/* Her 1 ms'de çalışacak kritik görev */
Sensor_Oku_Ve_Filtrele();
}
}
Kesme Öncelikleri ve NVIC
Gerçek zamanlı sistemlerde kesme öncelik yönetimi hayati önem taşır. ARM Cortex-M mimarisi, Nested Vectored Interrupt Controller (NVIC) üzerinden kesme önceliklerini yönetir. Düşük sayısal değer, yüksek öncelik anlamına gelir:
/* Kesme önceliklerini yapılandır */
void Interrupt_Priority_Config(void)
{
/* Motor kontrol kesmesi - en yüksek öncelik */
HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);
/* Sensör okuma kesmesi - orta öncelik */
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
/* UART haberleşme kesmesi - düşük öncelik */
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
Bu yapılandırmada motor kontrol kesmesi, sensör okuma veya UART haberleşme kesmelerini kesebilir (preempt edebilir), böylece kritik kontrol döngüsünün zamanlaması garanti altına alınır.
FreeRTOS ile Gerçek Zamanlı İşletim Sistemi Entegrasyonu
Birden fazla bağımsız görevin eşzamanlı yönetilmesi gerektiğinde, bare-metal yaklaşım karmaşıklaşır. Bu noktada FreeRTOS devreye girer. STM32CubeMX, FreeRTOS'u doğrudan destekler ve entegrasyonu son derece kolaylaştırır.
Görev (Task) Oluşturma
/* Görev tanıtıcıları */
TaskHandle_t xMotorTaskHandle;
TaskHandle_t xSensorTaskHandle;
TaskHandle_t xIletisimTaskHandle;
int main(void)
{
HAL_Init();
SystemClock_Config();
/* Görevleri oluştur */
xTaskCreate(
Motor_Kontrol_Gorevi, /* Görev fonksiyonu */
"MotorCtrl", /* Görev adı */
256, /* Yığın boyutu (word) */
NULL, /* Parametre */
3, /* Öncelik (yüksek) */
&xMotorTaskHandle /* Tanıtıcı */
);
xTaskCreate(
Sensor_Okuma_Gorevi,
"SensorRead",
256,
NULL,
2, /* Öncelik (orta) */
&xSensorTaskHandle
);
xTaskCreate(
Iletisim_Gorevi,
"CommTask",
512,
NULL,
1, /* Öncelik (düşük) */
&xIletisimTaskHandle
);
/* Zamanlayıcıyı başlat */
vTaskStartScheduler();
/* Buraya asla ulaşılmamalı */
while (1) {}
}
/* Motor kontrol görevi - 1 ms periyot */
void Motor_Kontrol_Gorevi(void *pvParameters)
{
TickType_t xSonUyanmaZamani = xTaskGetTickCount();
for (;;)
{
/* PID kontrol döngüsü */
float hata = hedef_hiz - mevcut_hiz;
float kontrol_sinyali = PID_Hesapla(hata);
PWM_Ayarla(kontrol_sinyali);
/* Tam periyodik uyandırma */
vTaskDelayUntil(&xSonUyanmaZamani, pdMS_TO_TICKS(1));
}
}
/* Sensör okuma görevi - 10 ms periyot */
void Sensor_Okuma_Gorevi(void *pvParameters)
{
TickType_t xSonUyanmaZamani = xTaskGetTickCount();
for (;;)
{
uint16_t adc_ham = ADC_Oku(ADC_CHANNEL_0);
float sicaklik = Ham_Veriyi_Donustur(adc_ham);
Kalman_Filtre_Guncelle(sicaklik);
vTaskDelayUntil(&xSonUyanmaZamani, pdMS_TO_TICKS(10));
}
}
Burada vTaskDelayUntil fonksiyonunun kullanımına dikkat edin. vTaskDelay fonksiyonundan farklı olarak, vTaskDelayUntil mutlak zamanlama sağlar; yani görevin çalışma süresi ne olursa olsun periyot sabit kalır. Bu, gerçek zamanlı sistemlerde deterministik zamanlama için kritik bir ayrıntıdır.
Görevler Arası İletişim: Kuyruklar ve Semaforlar
Görevler arasında güvenli veri aktarımı için FreeRTOS kuyrukları (queue) ve senkronizasyon için semaforlar kullanılır:
/* Sensör verisi kuyruğu */
QueueHandle_t xSensorKuyrugu;
void Sistem_Baslat(void)
{
/* 10 elemanlık kuyruk oluştur */
xSensorKuyrugu = xQueueCreate(10, sizeof(SensorVerisi_t));
}
/* Sensör görevi - veriyi kuyruğa yazar */
void Sensor_Okuma_Gorevi(void *pvParameters)
{
SensorVerisi_t veri;
for (;;)
{
veri.sicaklik = Sicaklik_Oku();
veri.basinc = Basinc_Oku();
veri.zaman_damgasi = xTaskGetTickCount();
/* Kuyruğa yaz, 10 ms bekle */
xQueueSend(xSensorKuyrugu, &veri, pdMS_TO_TICKS(10));
vTaskDelay(pdMS_TO_TICKS(50));
}
}
/* İletişim görevi - kuyruktan okur ve gönderir */
void Iletisim_Gorevi(void *pvParameters)
{
SensorVerisi_t alinan_veri;
for (;;)
{
/* Kuyruktan oku, veri gelene kadar bekle */
if (xQueueReceive(xSensorKuyrugu, &alinan_veri, portMAX_DELAY) == pdTRUE)
{
UART_Veri_Gonder(&alinan_veri);
}
}
}
DMA ile CPU Yükünü Azaltma
Gerçek zamanlı sistemlerde CPU'nun kritik görevlere ayrılması gerekir. Direct Memory Access (DMA) kullanarak ADC okumaları, UART transferleri ve SPI haberleşmesi gibi veri aktarım işlemlerini CPU müdahalesi olmadan gerçekleştirebilirsiniz:
/* ADC'yi DMA ile sürekli dönüşüm modunda başlat */
uint16_t adc_buffer[4]; /* 4 kanallık ADC tamponu */
void ADC_DMA_Baslat(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, 4);
}
/* DMA transfer tamamlandığında çağrılır */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
/* adc_buffer artık güncel verilerle dolu */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* ISR içinden görev bildirimle */
vTaskNotifyGiveFromISR(
xSensorTaskHandle,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
Bu yaklaşımla ADC dönüşümleri arka planda DMA tarafından yönetilir, CPU yalnızca veriler hazır olduğunda bilgilendirilir ve işlem yapar.
Zamanlama Analizi ve Hata Ayıklama
Gerçek zamanlı bir sistemin doğru çalıştığını kanıtlamak için zamanlama analizi şarttır. STM32'nin Data Watchpoint and Trace (DWT) birimi, saat döngüsü seviyesinde ölçüm yapmanıza olanak tanır:
/* DWT sayacını etkinleştir */
void DWT_Baslat(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
/* Çalışma süresini ölç */
void Kritik_Gorev_Calistir(void)
{
uint32_t baslangic = DWT->CYCCNT;
/* Kritik görev kodu */
PID_Hesapla_Ve_Uygula();
uint32_t bitis = DWT->CYCCNT;
uint32_t gecen_cevrim = bitis - baslangic;
float gecen_us = (float)gecen_cevrim / (SystemCoreClock / 1000000.0f);
/* Zamanlama ihlali kontrolü */
if (gecen_us > MAX_IZIN_VERILEN_US)
{
Hata_Bildir(ZAMANLAMA_IHLALI);
}
}
Watchdog ile Güvenilirlik
Gerçek zamanlı gömülü sistemlerde yazılımın kilitlenmesi veya beklenmedik durumlarla karşılaşması olasıdır. Independent Watchdog (IWDG) zamanlayıcısı, sistem kilitlenmelerinde otomatik sıfırlama sağlayarak güvenilirliği artırır:
/* IWDG'yi 1 saniye zaman aşımı ile yapılandır */
IWDG_HandleTypeDef hiwdg;
void IWDG_Baslat(void)
{
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
hiwdg.Init.Reload = 500; /* ~1 saniye */
HAL_IWDG_Init(&hiwdg);
}
/* Ana döngüde veya periyodik görevde besle */
void Watchdog_Besle(void)
{
HAL_IWDG_Refresh(&hiwdg);
}
En İyi Uygulamalar ve Dikkat Edilmesi Gerekenler
- Dinamik bellek ayırmaktan kaçının:
mallocvefreekullanımı bellek parçalanmasına ve belirsiz çalışma sürelerine yol açar. Statik ayırma veya FreeRTOS bellek havuzlarını tercih edin. - Kesme servis rutinlerini kısa tutun: ISR içinde uzun işlemler yapmak yerine, bir bayrak veya semafor kaldırarak asıl işi görev bağlamında yapın.
- Öncelik tersine dönüşüne (priority inversion) dikkat edin: FreeRTOS'un mutex mekanizması öncelik kalıtımını destekler; paylaşılan kaynaklarda
xSemaphoreCreateMutexkullanın. - Yığın taşmasını izleyin: FreeRTOS yapılandırmasında
configCHECK_FOR_STACK_OVERFLOWseçeneğini etkinleştirin vevApplicationStackOverflowHookgeri çağırma fonksiyonunu tanımlayın. - Saat ağacını doğru yapılandırın: Yanlış saat yapılandırması tüm zamanlama hesaplamalarını bozar. CubeMX'in saat ağacı görselleştirmesini mutlaka doğrulayın.
- volatile anahtar kelimesini doğru kullanın: ISR ile ana kod arasında paylaşılan değişkenleri
volatileolarak tanımlayın, aksi halde derleyici optimizasyonları beklenmedik davranışlara neden olabilir. - Hata yönetimi stratejisi belirleyin: Hard Fault, Bus Fault gibi donanım hatalarını yakalayan handler fonksiyonlarını mutlaka tanımlayın ve hata durumunda sistemi güvenli bir konuma getirin.
Sonuç
STM32 mikrodenetleyicileri, zengin çevresel birimleri, güçlü kesme altyapısı, DMA desteği ve FreeRTOS gibi RTOS çözümleriyle gerçek zamanlı gömülü sistem geliştirme için mükemmel bir platform sunar. Başarılı bir gerçek zamanlı sistem tasarımı, donanımı iyi tanımak, zamanlama gereksinimlerini doğru analiz etmek ve uygun yazılım mimarisini seçmekle başlar.
Bare-metal yaklaşımdan RTOS tabanlı çoklu görev yönetimine, DMA transferlerinden watchdog güvenlik mekanizmalarına kadar bu yazıda ele aldığımız konular, güvenilir ve deterministik gömülü sistemler inşa etmeniz için sağlam bir temel oluşturacaktır. Her projede en kritik adım, sistemin zamanlama gereksinimlerini baştan belirlemek ve tasarım kararlarını bu gereksinimlere göre şekillendirmektir.