Sayfanın PDF versiyonu: Gömülü Sistemlerde İşletim Sistemi Kullanımı(.pdf)
Gömülü Sistemlerde İşletim Sistemi Kullanımı
Gömülü sistemlerde işletim sistemi kullanarak, işlemci gücünü ve diğer kaynakları ihtiyaç duyan yazılım parçalarına (Task) etkin şekilde paylaştırabiliriz.

Bir diğer faydası da yazılımı bağımsız bloklar halinde geliştirebilmemize olanak sağlaması, geliştirme ve test sürelerini kısaltmasıdır.

Bu yazıda, işletim sistemleri hakkında teknik bilgiler ile başlayıp, Coocox IDE ile STM32F100 mikrodenetleyicisi üzerinde (STM32VLDiscovery) FreeRTOS ile bir uygulama geliştireceğiz.
İşletim Sistemi Nedir?
Bilgisayar teknolojisinin ilk yıllarında işletim sistemi, donanımı kontrol eden yazılımdan ibaretti. Fakat bilgisayar teknolojisinin evrimi, daha zengin bir tanımı da gerektirmiştir.

Bugünün bilgisayarları, aynı anda birçok yazılımı birbirlerini etkilemeksizin, aynı donanım üzerinde çalıştırabilmektedir. Her yazılım, diğer yazılımlardan bağımsız, sadece kendileri varmış gibi çalışabilirler. Hatta yazılımlar kendi aralarında da haberleşebilirler.

İşte bu özellikleri sağlayan yazılım parçası işletim sistemidir.
İşletim Sisteminin Görevi
İşletim Sistemi, çalıştırılacak yazılımları yükler ve çalıştırır, işlemci, bellek(RAM) ve IO birimleri gibi kaynakları, çalıştırdığı yazılımlara, belirlenen kurallara göre paylaştırır.

Örneğin şu an kullandığım bilgisayarda, bir tarafta web browser açık iken, diğer tarafta da bu dökümanı yazabiliyorum. Fakat işlemci bir tane olduğuna göre, iki yazılım aynı anda nasıl çalışabiliyor?


İşletim sistemi, çalışan yazılımlar arasında, ihtiyaç duyulan kaynağı - bu örnekte işlemci - paylaştırır. Aslında, işlemci üzerinde bir anda sadece bir yazılım çalışır – tek çekirdekli işlemciler için geçerli – fakat, yazılımlar arası geçiş o kadar hızlı yapılır ki, dışarıdan bakan bizler için tüm yazılımlar, aynı anda (paralel olarak) çalışıyormuş gibi görünür.
Çoklu Görev (Multi-Tasking)
Multi-tasking, birden fazla görevi (Task) aynı anda çalıştırabilmek demektir. Daha önce söylediğim gibi, aynı anda çalıştırma diye bir şey yok aslında, sadece görevler arası geçiş çok hızlı yapıldığında böyle algılanıyor.
Görev Düzenleme (Scheduling)
Görev düzenleme, işletim sisteminin, çalışması gereken görevi tespit edip, o görevi çalıştırması demektir. Çalışması gereken görev nasıl tespit edilebiliyor? Bunun için de farklı algoritmalar kullanılıyor.

Örneğin her görev, sıra ile (FIFO-FCFS First Come First Served) veya belirli bir periyoda göre çalıştırılabilir.

Alternatif olarak, en kısa süreceği düşünülen görev ilk olarak çalıştırılabilir (Shortest Task First).

Ya da, biraz daha ileri giderek, çalıştırılacak görevler arasında öncelikler belirleyip, görevlerin bu önceliklere göre çalışmasını isteyebiliriz.
Öncelik (Priority)
Her görev için bir öncelik belirlenebilir. Bir anda iki görevin de çalışması gerekirse, işletim sistemi önceliği yüksek olan görevi çalıştırdıktan sonra diğer görevi çalıştırır.


Bazı görevlerin ise sadece işlemci boşta (IDLE) iken çalışmasını isteyebiliriz. Bu durumda, o görev için sistemdeki en düşük önceliği tanımlarız. Örnek uygulamamızda bunu da deneyeceğiz.

Şimdi hayatın içinden bir örnek verelim; Bir ofis çalışanını düşünün, gün içinde yapması gereken evrak işleri var (Task 1) ve evrak işi bitene kadar bunu yapmak zorunda. Tam bu işini yaparken telefon çalarsa (Task 2) mevcut işini bırakıp telefona bakmalı ve görüşmesi bittiğinde de evrak işine devam etmelidir.

Peki, bu arkadaşımızın bir toplantıya gitmesi gerekiyorsa (Task 3), evrak işini bırakmalı ve toplantıya katılmalıdır. Hatta toplantı sırasında telefonu çalarsa (Task2) telefonuna bakamaz. Ancak toplantıdan çıktığında arayan kişiyi geri arayabilir. Görüşmesi bittiğinde tekrar evrak işlerine geri dönebilir.

Peki evrak işleri de bittiğinde ne yapacak? Boş oturabilir veya sosyal medyada vakit geçirebilir (IDLE Task).
Preemption
Preemption, işletim sisteminin, bir görev çalışırken, bu görevin bitmesini beklemeden daha yüksek öncelikli diğer görevi çalıştırabilmesidir. Yani işletim sistemi, bir görevi gerekli gördüğü durumda durdurabilir ve diğer görevi çalıştırabilir.

Bunu daha iyi anlayabilmek için alternatifi olan “Cooperation” dan bahsetmemiz gerekiyor.
Cooperation
Cooperation adından da anlaşılacağı gibi bir işin yardımlaşarak yapılmasıdır. Peki yardımlaşan kimlerdir?

Bir görevin işletim sistemi tarafından, gerekli olduğu zaman durdurulamadığı, görevin ne zaman durdurulacağının görevin kendisi tarafından belirlendiği yönteme Cooperation denmektedir. Yardımlaşan birimler de çalışan görev ve işletim sistemidir. İşletim sistemi bağımsız olarak her şeyi yönetemez. Sorumluluklar işletim sistemi ve görev arasında paylaştırılmıştır.

İşletim sistemi, hangi görevin çalışması gerektiğini hesaplar, çalışan görev ise kontrolü ne zaman işletim sistemine vereceğini bilir.
FreeRTOS
Gömülü sistemlerde kullanılabilecek çok sayıda işletim sistemi bulabilirsiniz. Hatta kendiniz de bir tane hazırlayabilirsiniz. Ancak, tekerlek çoktan keşfedilmiş, tekrar keşfetmeye çalışmayalım.

Bulabileceğiniz çok sayıdaki işletim sistemlerinden biri de FreeRTOS tur. Hem bedava olduğu için, hem de birçok mikrodenetleyici üzerinde çalışabildiği için büyük olasılıkla sizin de işinizi görecektir.
Özellikleri
FreeRTOS yukarıda bahsettiğimiz Çoklu Görev (Multi-tasking), Önceliklere göre Görev Düzenleme (Priority Scheduling) ve Preemption özelliklerine sahiptir.

Buna ek olarak, bizim burada kullanmayacağımız aşağıdaki özellikleri de vardır;
  1. Inter-Process Communication (IPC): Görevler arası iletişimi sağlar.
    1. Stream, Message Buffers
    2. Queue
    3. Semaphore
    4. Mutex
Nereden Edinebilirsiniz?
FreeRTOS’ u web sitesinden ücretsiz olarak indirebilirsiniz. Muhtemelen, kullanacağınız mikrodenetleyici için port edilmiş bir versiyonunu da burada bulabilirsiniz.

Bizim kullanacağımız versiyon ise örnek proje kodu içerisinde mevcut.
Örnek Uygulama
Bu kadar teknik bilgiden sonra, bir örnek uygulama yapalım.

Önce, kullanacağımız mikrodenetleyici için bir proje oluşturuyoruz. Projenin içerisine ek olarak sadece start-up kodlarını (Repository’ den) eklememiz yeterli.


FreeRTOS’ u Projeye Ekleme
İlk olarak, FreeRTOS kodlarını buradan indirip proje klasörüne açıyoruz (extract yapıyoruz).


Ardından, FreeRTOS klasörünü olduğu gibi, CoIDE içerisindeki projeye sürükle-bırak yöntemiyle ekliyoruz.


CoIDE, aşağıdaki soruyu sorduğunda, emin olduğumuzu söylüyoruz.


Artık FreeRTOS projemize eklendi ve neredeyse kullanıma hazır.


FreeRTOS Ayarları
FreeRTOS klasörü altındaki “FreeRTOSConfig.h” dosyası üzerinde birkaç ayar yapmamız gerekiyor. Herkesin uygulaması ve gereksinimleri farklı olacağı için, ayarlama imkanı verilmesi de güzel bir şey.

Aşağıdaki resimdeki gibi iki değeri değiştiriyoruz.


#define configMAX_TASK_COUNT 16

Bu satır ile tanımlayabileceğimiz maksimum görev (Task) sayısını belirtiyoruz. Bu değeri kullanmayacağımız kadar çok girersek, mikrodenetleyicinin RAM’ i yetmeyecektir. Kullanacağımızdan küçük tanımlarsak da, tahmin edebileceğiniz gibi, bazı görevleri tanımlayamayacağız.

Benim tavsiyem, geliştirmeye başlarken ortalama bir değer ile başlayıp, geliştirme bittiğinde (uygulama sahaya çıkacağı zaman), kullanılan görev sayısını buraya girmenizdir. Ama unutmayın, buradaki değer, her zaman kullandığınız görev sayısından büyük olmalı.

#define configTICK_RATE_HZ (( portTickType) 1000 )

Bu satır ile de, işletim sisteminin saniyede kaç kez kontrolü ele alıp, çalıştırılacak görev olup olmadığını kontrol etmesini istediğimizi yazıyoruz. Saniyede 100 veya 1000 kez kontrol etmesi uygun.

Bu değeri arttırırsanız, işletim sistemi gereğinden fazla çalışacak ve görevleri çalıştırması için gereken işlem gücünü kendisine ayırmış olacaktır.

Son adım olarak, iki header dosyasını main.c ye ekliyoruz.

#include "FreeRTOS.h"
#include "task.h"
Görev (Task) Oluşturma
PC işletim sistemleri için görev, Mozilla Firefox, Adobe Reader gibi çalıştırılabilir (.exe) programlar iken, gömülü sistemlerde sonsuz döngü içeren bir fonksiyondur. Basitçe;
void ledTask( void * param ){    
    //Do some initialization    
    for(;;){
        // Do some work here
    }
}                        
                    

Fonksiyon adı olarak istediğinizi verebilirsiniz. Parametre olarak bir void * param alıyor. Bunu nasıl kullanacağımızı ileride açıklayacağım.

Farkettiyseniz, dönüş değeri de yok. Bunun sebebi, bu fonksiyonun hiçbir değer dönmemesini istememiz değil, hiç dönmemesini ve sonsuz döngü içerisinde kalmasını istememiz. Nasıl olsa hiçbir zaman dönmeyecek.

Şimdi bu fonksiyonu FreeRTOS’a bildirelim;

xTaskCreate( ledTask, (char *)"test",configMINIMAL_STACK_SIZE, NULL, ( ( unsigned portBASE_TYPE ) 1 ), NULL );

Görevleri ekledikten sonra FreeRTOS’ u çalıştırıyoruz;

vTaskStartScheduler();

Görevi oluşturduk ama bu şu anda hiçbir iş yapmıyor. Burayı biraz geliştirmemiz gerekiyor. Bizim görevimiz (task), -tabi eğer kabul ederse:)- belirli aralıklarla bir LED’ i yakıp söndürsün.

Kullandığım STM32VLDiscovery kartında, PC8 ve PC9 pinleri LED lere bağlı. Dolayısıyla bunları kullanacağız.
#include "FreeRTOS.h"
#include "task.h"

void ledTask( void * param ){
    //Do some initialization
    for(;;){
        // Do some work here
        GPIOC->ODR |= 0x00000100; //Set PC8
        GPIOC->ODR &= 0xFFFFFEFF; //Reset PC8
    }
}

int main(void)
{
    //Initialize LED pins
    RCC->APB2ENR |= 0x00000010; //Enable GPIOC clock
    //Set PC8 and PC9 as outputs
    GPIOC->CRH &= 0xFFFFFF00;
    GPIOC->CRH |= 0x00000033;
    xTaskCreate( ledTask, (signed char *)"test", configMINIMAL_STACK_SIZE, NULL, ( ( unsigned portBASE_TYPE ) 1 ), NULL );
    vTaskStartScheduler();
    //should never come here
    while(1)
    {
    }
}
Görevimiz içerisinde PC8 pinine bağlı LED’ i yakıp söndürüyoruz ama bu çok hızlı olduğundan gözle görülemeyecektir. Ufak bir ekleme yaparak bunu çözelim.

void ledTask( void * param ){
    //Do some initialization
    for(;;){
        // Do some work here
        GPIOC->ODR |= 0x00000100; //Set PC8
        vTaskDelay(1000);//Wait 1000 OS Ticks
        GPIOC->ODR &= 0xFFFFFEFF; //Reset PC8
        vTaskDelay(1000);//Wait 1000 OS Ticks
    }
}
                    
vTaskDelay() fonksiyonu ile çalışan görevi, istediğimiz kadar bekletebiliriz. Burada dikkat edilmesi gereken nokta, fonksiyonun parametresinin bir süre değil, OS_Ticks cinsinden olmasıdır. Yani bizim daha önce

configTICK_RATE_HZ = 1000

verdiğimiz değer cinsindendir. Saniyede 1000 OS_Ticks olarak ayarı yapmıştık. Buna göre eğer 1000 Tick bekle dersek, görev 1 saniye boyunca çalışmayacaktır.

Bir görev çalışmıyorsa, işlemci gücü harcamıyor, kontrolü işletim sistemine devretmiş demektir.

Bu haliyle test edersek, PC8’ e bağlı LED’in 1 saniye yanık, 1 saniye sönük olduğunu göreceğiz.
Göreve Parametre Aktarma
Her görevin bir parametresi var demiştik. Bu parametreyi kullanmak için, xTaskCreate() fonksiyonunun 4. parametresini (şu ana kadar “NULL” vermiştik) kullanacağız. Parametrenin türü “void *” olduğu için aslında istediğimiz türde veriyi parametre olarak gönderebiliriz. NULL yerine (void *)200 yazdığımızda, bu değeri parametre olarak ledTask fonksiyonuna iletmiş olacağız. Peki bu değeri nasıl kullanacağız? vtaskDelay() fonksiyonları, sabit bir değer yerine bu parametreyi kullanabilirler. vTaskDelay((int)param);//Wait some OS Ticks Bu sayede, görevleri parametrik olarak çalıştırabiliriz.
Öncelik (Priority) Mekanizması
Öncelik mekanizmasını anlayabilmek için birden fazla görevimiz olmalı. Bunun için oluşturduğumuz görevi kopyalayarak ikinci bir görev yaratıyoruz.

Görevlerimiz bu hale geldi;
                  
void ledTask1( void * param ){
    //Do some initialization
    for(;;){
        // Do some work here
        GPIOC->ODR |= 0x00000100; //Set PC8
        vTaskDelay((int)param);//Wait some OS Ticks
        GPIOC->ODR &= 0xFFFFFEFF; //Reset PC8
        vTaskDelay((int)param);//Wait some OS Ticks
    }
}

void ledTask2( void * param ){
    //Do some initialization
    for(;;){
        // Do some work here
        GPIOC->ODR |= 0x00000200; //Set PC9
        vTaskDelay((int)param);//Wait some OS Ticks
        GPIOC->ODR &= 0xFFFFFDFF; //Reset PC9
        vTaskDelay((int)param);//Wait some OS Ticks
    }
}
main() içerisinde bu iki görevi de FreeRTOS’a bildirmeliyiz.
 
xTaskCreate( ledTask1, (signed char *)"test1", configMINIMAL_STACK_SIZE, (void *)200, ( ( unsigned portBASE_TYPE ) 1 ), NULL );
xTaskCreate( ledTask2, (signed char *)"test2", configMINIMAL_STACK_SIZE, (void *)500, ( ( unsigned portBASE_TYPE ) 1 ), NULL );                    
Parametre olarak da, ilk göreve 200, ikinciye 500 verdik. Şimdi yazılımı derleyip yüklediğinizde, her LED bağımsız olarak verilen sürelerde yanıp sönecektir.

Her iki görevin de öncelikleri 1 olduğu için, öncelik mekanizmasının bir anlamı yok şu an. Peki, bir görevi diğerinden daha öncelikli yaparsak ne olur? İkinci görevin önceliğini 2 yapalım ve deneyelim. ( ( unsigned portBASE_TYPE ) 2 )


Yine bir şey değişmemiş durumda. Bunun sebebi, öncelik mekanizmasının, işlemci gücü yetersiz kaldığında anlamlı olmasıdır. İşlemci, zaten zamanın çoğunda boşta olduğu için bir fark göremedik.

Test için, ilk görev içerisindeki vTaskDelay() fonksiyonlarını kaldıralım.
 
void ledTask1( void * param ){
    //Do some initialization
    for(;;){
        // Do some work here
        GPIOC->ODR |= 0x00000100; //Set PC8
        GPIOC->ODR &= 0xFFFFFEFF; //Reset PC8
    }
}



İlk görev düşük öncelikli ve işlemcinin kalan tüm zamanını tüketiyor (IDLE task). Ama yüksek öncelikli görevin çalışmasını engellemiyor.

Son bir adım daha atalım ve görevlerin önceliklerini ters çevirelim. Yani ilk görev yüksek öncelikli olsun. Bu durumda ikinci görev hiç çalışamayacaktır.


Sıra Sizde
Projenin kaynak kodlarına buradan ulaşabilirsiniz.

Burada anlatılanlara ek olarak yapabilecekleriniz arasında,
  • Aynı görevi (Task) farklı parametreler ile aynı anda çalıştırmayı deneyebilirsiniz,
  • FreeRTOS IPC mekanizmalarını deneyebilirsiniz.
Ne kadar çok denerseniz, o kadar öğrenirsiniz.