4

FreeRTOS Notları #2: Task veya Thread

    RTOS kullanan gerçek zamanlı uygulamalarda birbirinden bağımsız iş parçacıkları bulunabilir. Bu iş parçacıkları FreeRTOS da “task“, CMSIS_RTOS da ise”thread” olarak isimlendirilir. Bu task’ların kendilerine ait sonsuz döngüleri olabilir. Her task’ı main fonksiyonu ve bunun içinde sonsuz döngüsü olan bir program gibi düşünebiliriz.

Örnek Bir Task Fonksiyonu

Task Durumları

    Birçok task bulunan ve tek çekirdekli bir işlemciye sahip bir sistemde herhangi bir zamanda sadece tek bir task CPU üzerinde çalıştırılır. Buna göre bir task iki ana durumdan birinde bulunabilir. Bunlar “running state” ve “not running state” dir. “Not running State” in alt durumları da vardır. Bunlar “Ready“,”Blocked” ve “Suspended” durumlarıdır.

Task Durumları

    Bir task “running state” durumunda olduğu zaman işlemci o task’ın kodlarını yürütür. Bir task “not running state” durumuna geçtiğinde task bir nevi uykudadır.  Tekrar “running state” durumuna girdiğinde kaldığı yerden kodları yürütmeye devam eder. Bir task’ın “Not running state” den “running state”e geçmesi “switched in” veya “swaped in” olarak adlandırılır. Tam tersi durumda ise “switched out” veya “swapped out” olarak adlandırılır. Burada task’ları switch in veya out yapmakta sadece scheduler(zamanlayıcı) yetkilidir.

Task Priorities( Öncelikleri)

    Her task’ın 0 dan başlayarak (configMAX_PRIORITIES-1) ‘e kadar tanımlanabilen öncelikleri vardır. Burada tanımlamaların başında küçük harflerle yazılan kısım hangi dosya içerisinde tanımlandığını işaret eder. Yani configMAX_PRIORITIES , FreeRTOSConfig.h dosyası içerisinde tanımlıdır. RAM kullanımı açısından configMAX_PRIORITIES olabildiğince küçük tanımlanmalıdır. Öncelik numaraları arttıkça task öncelikleri de artar yani öncelik için atanan numaralar ile task öncelikleri doğru orantılıdır. Birden fazla task aynı önceliği paylaşabilir.
Schedular başladıktan sonra task öncelikleri vTaskPrioritySet() fonksiyonu ile değiştirilebilir.

FreeRTOS ile Task Kullanımına Ait Bir Örnek

    Amaç: iki adet task oluştur. Taskın biri UART üzerinden string göndersin. Diğeri led toggle etsin.
    FreeRTOS u projeye CubeMX ile dahil edeceğim. Aynı proje üzerinde önce FreeRTOS ile daha sonra CMSIS-RTOS ile task oluşturma fonksiyonlarını kullanmış olacağız.
    FreeRTOS da bir task oluşturmak için aşağıdaki fonksiyon kullanılır.

  • pvTaskCode: Bu parametre Task için oluşturulmuş fonksiyonu temsil eder.
  • pcName: String olarak bir task’a isim vermek için kullanılır. Bu parametre freertos tarafından kullanılmaz. Debug vb. olaylar için kullanılabilir.
  • usStackDepth: Task için ayrılacak bellek boyutu. Burada girilen parametre byte cinsinden değildir.?Örneğin 32 bitlik bir işlemci için  usStackDepth 100 girilirse stack olarak 100×4 byte=400 byte yer ayrılırır.
  • pvParameters: Task fonksiyonuna bir parametre göndermek icin kullanılır. Burada parametre tipi void* olarak belirlenmiştir. Fonksiyona gönderilicek parametrenin tipinin kesin olmadığı(int ,float,char vb) zamanlarda void* ile parametre geçilir. Yani parametre olarak ister int ister float isterse string gönderilebilir.
  • uxPriority: Task önceliğini ifade eder. Öncelik için girilen sayı arttıkça öncelik de artar.
  • pxCreatedTask: Bir handle oluşturmak için kullanılır. Bu handle daha sonra task silmek,taskın önceliğini değiştirmek için kullanılabilir.
  • Return Değeri: xTaskCreate() fonksiyonu iki farklı değer döndürür. Eğer task sorunsuz bir şekilde oluşturulduysa pdPASS ,  task oluşturmada sorun çıktıysa (bellek ayırma ile ilgili) pdFAIL değerini döndürür.

    Yukarıda iki adet task oluşturulmuştur. xTaskCreate() fonksiyonundan önce Task1 ve Task2( isimler farklı da olabilir) isimli iki adet task fonksiyonu oluşturmamız gerekir. Bu fonksiyonlar aşağıdaki gibi tanımlanabilir.

    Task fonksiyonları herhangi bir değer döndürmediği için void tipinde tanımlıdır ve her iki task’ın da bir sonsuz döngüsü vardır. Tasklardan biri C13 e bağlı ledi bir saniye aralıklarla toggle ederken diğer task UART ile usb-ttl üzerinden bilgisayara string göndermektedir. Bu işlemler için kullandığım fonksiyonlar yabancı gelebilir. Bunlar benim tanımladığım macro fonksiyonlardır.

    Yukarıdaki task fonksiyonlarının içinde gecikme fonksiyonu olarak FreeRTOS API sinin vTaskDelay() fonksiyonunu kullandık. Bu delay fonksiyonu TickType_t  tipinde bir parametre alır ve bu milisaniye cinsinden değildir. Bu yüzden delay fonksiyonuna parametre girilirken pdMS_TO_TICKS() ile kullanılabilir. Bu fonksiyon milisaniyeyi tick e çevirir. Bunun yerine 1000 / portTICKRATEMS şeklinde de kullanılabilir.
    Yeri gelmişken delay fonksiyonlarına değinelim. Bu örnekte HAL kütüphanesinin veya kendi oluşturduğumuz bir delay fonksiyonunu kullanmak mı daha mantıklıdır yoksa FreeRTOS un bize sağladığı delay fonksiyonunu kullanmak mı? FreeRTOS’un vTaskDelay() fonksiyonu bir task içerisinde çağırıldığında task, fonksiyon içine girilen tick boyunca bloklanır. Yani vTaskDelay() fonksiyonu çağırıldığında task çalıştırılmaz ve sıradaki taskın kodları yürütülür. Diğer delay fonksiyonları( örneğin; HAL_Delay) kullanılsaydı program task içerisinde çalışmaya devam eder. Çünkü bu delay fonksiyonları işlemciyi sonsuz bir döngü içerisinde oyalar. Bu yüzden FreeRTOS da bu fonksiyonları kullanmak yerine RTOS un sağladığı delay fonksiyonunu kullanmak verim açısından daha iyidir.
    Scheduler task’lar arasında geçiş yapabilmesi için bir timer kaynağına ihtiyac duyar. Arm tabanlı mikrodenetleyicilerde FreeRTOS bunun için systick timerı kullanılır. FreeRTOSConfig dosyasına gittimizde aşağıdaki gibi bir tanımlama görürüz.
    Bu satır task’lar arası geçişin kaç saniyede bir olacağını belirlemek için kullanılır. Belirlenen bu değere göre belli bir zaman sonra tick kesmesi oluşur ve bu kesmenin içerisinde bir task tan diğerine geçiş işlemi(context switching) yapılır. Örneğin yukarıdaki satırda tick rate 1000 hz olarak belirlenmiş. Bu demektir ki aynı öncelikteki her task arası geçiş 1 ms de bir olacak. Normalde tick rate 100 hz civarlarında tanımlanır.

    Yukarıdaki task fonskiyonlarındaki işlemler(gecikme fonksiyonu hariç) 1 ms den kısa sürdüğü için task’ın içerisindeki işlemler yapıldıktan sonra bir sonraki task yürütülecektir. Eğer task fonksiyonunun içindeki kodların işlenme süresi 1 ms den fazla olsaydı kodlar tick kesmesi oluşana kadar yürütülecek ve diğer task’a geçiş yapılacaktı. Bir sonraki tick kesmesinden sonra önceki task’ta kodlar kaldığı yerden yürütülmeye devam edecekti.

Task Fonksiyonuna Parametre Gönderme

    Bu örnekte task’ların yaptıkları işlemler aynı fakat task2’nin UART üzerinden gönderecek olduğu stringi task oluşturulurken parametre olarak task fonksiyonuna gönderelim. Bu işlemi aşağıdaki gibi yapabiliriz.

UART üzerinden gönderilen string

 Heap Size Seçimi

    Task fonksiyonlarının içerisinde yapılacak işlemlerin sayısı arttıkça daha fazla stack ihtiyacı duyulacaktır. Yukarıdaki task’lar içerisinde çok büyük işlemler yapılmadığı için usStackDepth parametresi 100 girilmiştir. Bu demektir ki heap içerisinde 100×4= 400 byte lık bir alan Task1 ve Task2 için ayrı ayrı ayrılmıştır. Her oluşturulan RTOS nesnesi heap içerisinde yer kaplamaktadır. Her nesnenin boyutunu hesaplamak böyle zor olacaktır kaldı ki sadece taskların stack boyutu değil aynı zamanda TCB(task control block) lerin heap içerisinde kapladığı boyutu da hesaplamak gerekir. Bunun yerine aşağıdaki fonksiyon ile tanımlanan heap in ne kadarının boş olduğu çekilebilir.
    Bu fonksiyonun kod içerisinde kullanım yeri önemlidir. Bütün RTOS nesneleri oluşturulduktan sonra bu fonksiyonun çağrılması daha iyi sonuç verecektir. Bu fonksiyonu vTaskStartScheduler() dan sonra çağırmak daha iyi olacaktır fakat program main içerisinde hiç bir zaman bu fonksiyondan sonrasına geçmez. vTaskStartScheduler() fonksiyonundan önce de çağıramayız çünkü bu scheduler başlatıldığında idle task oluşturulur ve bu da heap içerisinde bir alan kaplar. xPortGetFreeHeapSize() fonksiyonunu oluşturulan task fonksiyonlarından birinin içerisinde çağırmak daha iyidir.

    Ben FreeRTOS ile proje geliştirirken başlangıç olarak heap boyutunu olabildiğince yüksek(mikrodenetleyicinin RAM boyutu göz önüne alınarak) tanımlıyorum fakat heap boyutunun gereğinden fazla tanımlanması ise gereksiz RAM kullanımına yol açacaktır. Bunun için tüm RTOS nesnelerini oluşturduktan sonra kullanılmayan heap boyutunu öğrenip heap boyutunu buna göre azaltıyorum. Örneğin; Yukarıdaki task1 ve task2 örnekleri için başlangıçta 5 kb olacak şekilde aşağıdaki gibi tanımladım.

    Daha sonra Debug ekranından “size” değişkenin boyutunu öğrendim.

Debug Ekranında Boş Heap Boyutunun Görüntülenmesi

    Yukarıda görüldüğü gibi heap içerisinde 3.5 kb a yakın boş bir alan bulunmaktır ve bu RAM de yer kaplamaktadır. Artık heap içerisindeki boş alanı bulduğumuza göre heap boyutunu tekrar düzenleyebiliriz.

    Bu tanımlamadan sonra tekrar debug yapıldığında “size” değişkeni sıfır olur. Böylelikle RAM kullanımını olabildiğince azaltabiliriz. Burada boş alanın hepsini çıkarmamız task içerisinde başka bir RTOS nesnesi oluşturduğumuz projelerde sorun çıkarabilir. Bu yüzden burada 3480 yerine 3400 gibi daha düşük bir değer kullanılabilir. Yukarıdaki gibi bir tanımlamanın basit led yakıp söndüren ve uart üzerinden string gönderen bir program için kullanılmasında bir sakınca yoktur.

Tüm Kodlar(FreeRTOS ile)

CMSIS-RTOS ile Task(Thread) Kullanımı

    Aynı uygulamayı CubeMx ile CMSIS-RTOS kullanarak yapalım. Burada CMSIS-RTOS diyorum ama aslında arkaplanda FreeRTOS kullanılıyor.
    CubeMx ile aşağıdaki gibi FreeRTOS aktif edilebilir. FreeRTOS  systick timerı kullandığı için “TimeBase Source” systick timerdan farklı seçilmelidir. Ben aşağıda görüldüğü gibi Timer1 olarak seçtim. Yani HAL kütüphanesi Timer1’i kullanacak.

CubeMX Pin Ayarları

    TICK_RATE_HZ bir önceki örnekte olduğu gibi 1 khz olarak ayarlandı.
    USE_PREEMPTION  Enabled ise FreeRTOS pre-emptive(öncelik) olarak ,Disabled ise co-operative(işbirliği) olarak çalışır.
    FreeRTOS da bu FreeRTOSConfig.h dosyası içerisinde aşağıdaki gibi tanımlanır.

    MINIMAL_STACK_SIZE , Idle task için tanımlanır. Burada default olarak 128 olarak tanımlanmış o yüzden değiştirmedim.
    TOTAL_HEAP_SIZE ı 5000 byte(tam olarak 5 kb değil) olarak tanımladım. Bir önceki örnekte de, bu örnekte de heap_4 kullanılmıştır.
    Başlangıçta CubeMx “defaultTask” isminde bir task oluşturur. Bu task idle task değildir. O yüzden bunu yeniden düzenleyebiliriz.
    defaultTask’ı aşağıdaki gibi düzenleyebiliriz. Burada öncelikler sayı olarak değil isim olarak karşımıza çıkmaktadır. Bu örnekte önceliği “osPriorityNormal” olarak ayarladım.
    Task2’nin ayarları ise aşağıdaki gibidir. Burada her iki task’ın önceliğini aynı olarak ayarladım.
    FreeRTOS Heap Usage sekmesinden taskların kullandığı alanları ve kullanılmayan alanları görebiliriz. CubeMx bize böyle bir güzellik yapmış.
    Bu ayarlamalardan sonra kod kısmına geçebiliriz. CubeMX in oluşturduğu kodları bir inceleyelim.
main fonksiyon üzerinde ilk olarak aşağıdaki kodlar oluşturulmuş.
    Buradan anlıyoruz ki task oluşturulurken handle parametresi kullanılacak.Daha sonra task fonksiyonlarının main üzerinde aşağıdaki gibi tanımlaması karşımıza çıkıyor.
    CubeMX fonksiyonları main fonksiyonun altında aşağıdaki tanımlar. Başlangıçta bu fonksiyonların içinde sadece sonsuz döngü ve bu döngünün içerisinde osDelay(1) şeklinde bir gecikme bulunur.
    Yukarıda görüldüğü gibi task1 ve task2 fonksiyonları bir önceki örnek ile aynı işlemleri yapmaktadır. Burada delay fonksiyonu CMSIS-RTOS’a özeldir. FreeRTOS da olduğu gibi tick i milisaniyeye çevirmemiz gerekmez. main fonskiyon içerisinde aşağıdaki task tanımlamaları yapılmış. Buradan sonra artık task yerine thread olarak isimlendirsek daha doğru olur.
    osThreadDef  fonksiyonu cmsis_os.h dosyası içerisinde tanımlanmış aşağıdaki gibi bir makrodur.
    Burada instance parametresi var. Bu parametre osThreadCreate() fonksiyonun kaç kere çağrılacağını bildirir. CMSIS-RTOS da tek bir thread fonksiyonu farklı thread id ve farklı parametreler gönderilerek iki farklı thread fonksiyonu gibi kullanılabilir. Örnek olarak aşağıdaki gibi instance parametresi 2 olarak girilirse osThreadCreate() fonksiyonu 2 kere çağrılabilir.
    Bunun kullanım amacını basitçe şöyle anlayabiliriz. UART üzerinden haberleşen bir task düşünelim. Aynı task kodlarını değiştirmeden aynı fonksiyonu farklı bir UART ( UART2,UART3 vb.) ile kullanmak istediğimizde instance parametresini kullanabiliriz.
    Task fonksiyonu ise aşağıdaki gibi olabilir. Projede aşağıdaki gibi sadece tek bir task fonksiyonu bulunsa da osThreadCreate fonksiyonu iki kere kullanıldığı için sanki iki adet task varmış gibi program çalışır. Task ilk çalıştığında UART1 den veri gönderirken , scheduler diğer task’ı(aslında aynı fonksiyon) çalıştırdığında UART2 den veri gönderilir.
    osThreadCreate fonksiyonu ise cmsis_os.c içerisinde aşağıdaki tanımlıdır.
    Thread’ler tanımlandıktan sonra aşağıdaki kod ile scheduler başlatılır.

Tüm Kodlar( CMSIS-RTOS ile)

Kaynaklar

Mehmet Topuz

4 Comments

  1. Öncelikle size çok ama çok teşekkür ederim, acaba iot ile alakalı da böyle detaylı bir yazı yazmanız mümkün mü? Özellikle STM32 ile iot kullanımı hakkında. Teşekkür ederim.

    • Çünkü IOT kavramı FreeRTOS gibi böyle sekiz yazıda anlatılacak bir konu değil. Daha çok IOT projelerinde kullanılan haberleşme protokolleri vb. konular ile ilgili basit projeler paylaşabilirim belki ilerleyen zamanlarda.

Buğra için bir cevap yazın Cevabı iptal et

E-posta hesabınız yayımlanmayacak.