0

FreeRTOS Notları #6: Mutex

    Bir multitasking yapan sistemde birden fazla taskın aynı kaynağa erişmesinde sorunlar çıkabilir. Örneğin; 2×16 LCD ekrana yazı yazan iki task düşünelim. Bunlardan biri “Hello world” yazsın. Diğer task ise “Mehmet” yazsın. Şimdi task1 in çalıştığı durumu düşünelim. En temelde ekrana bu yazılar karakter karakter gönderilecektir. Task1 çalışmaya başladı ve “Hello w” kısmına kadar yazdı. Tam burada “o” karakterini yazacak iken bir context switching oluştu ve Task2 yürütülmeye başlandı. Bu durumda ekrana Task2 nin yazdırmak istedikleri yazılacaktır değil mi? Diyelim ki Task2 yazmak istediğinin hepsini yazdı. Ekranda artık “Hello wMehmet” yazacaktır. Tekrar context switching olduğunda Task1 kaldığı yerden çalışmaya devam edecek ve LCD ekranda yazan cümlenin son hali “Hello wMehmetorld” şeklinde olacaktır. Bu sorun bir taskın aynı kaynak ile işini bitirmeden diğer taskların o kaynağa erişmek istemesinden kaynaklanır. RTOS’da bu sorunun önüne geçmek için birkaç yöntem vardır. Bunlardan biride mutex(mutual exclusion) dir.
    Veri tutarlılığını sağlamak ve tasklar arasında paylaşılmış kaynakların korunması için mutex tekniği kullanılır. Böylelikle task bir kaynak ile işini bitirmeden diğer tasklar o kaynağa erişemez.

Kritik Alanlar ve Scheduler’ı Askıya Alma(Suspend)

    Kod içerisindeki basit kırıtik alanlar için taskENTERCRITICAL() ve taskEXİTCRITICAL()  makroları kullanılabilir.

    Bu makroları kullanmak mutex’in çok ilkel bir versiyonudur. Bu makrolar interruptları kapatır. Context switching ise systick timer kesmesine göre yapıldığı için taskEXİTCRITICAL() makrosu çağrılmadan diğer tasklar çalışmaz.
    Krıtik alanlar olabildiğince kısa tutulmalıdır. Aksi takdirde interrupt cevap süresini kötü yönde etkiler.

    Krıtik alanlar için scheduler’ın askıya alınması (suspend) işlemi de  kullanılabilir. Suspend etmek sadece taskların aynı kaynağa erişmesini önler. İnterruptlar o kaynağa hala erişebilir.

Mutex(Mutual Exclusion)

FreeRTOS-mutex

    Mutex tasklar arasında paylaşılmış bir kaynağa erişmek için kullanılan özel tipte bir binary semaphore‘dur. Burada token üzerinden gidecek olursak; Bir task korunmak istenen bir kaynağa ulaşmak istediğinde token’ı ilk önce elde etmelidir(take). Kaynak ile işini bitirdikten sonra ise token’ı diğer taskların aynı kaynağa erişebilmesi için geri bırakmalıdır(give). Mutex’ler aynı semaphore API fonksiyonlarını kullanır. Bu yüzden bir blok süresinin tanımlanmasına izin verir. Binary semaphore’un aksine mutex’lerde öncelik kalıtım mekanizması(priority inheritance) vardır. Eğer bir task mutex’i elde etmiş ise daha yüksek öncelikli bir task aynı mutex’i elde edemeyecek ve bloklanacaktır. Bu mekanizma yüksek öncelikli taskın mümkün olan en kısa süre bloklanmasını sağlamak için tasarlanmıştır.
Mutex priority inheritance
    Priority Inheritance kavramını daha iyi anlamak için yukarıdaki grafik üzerinden gidelim.
  1. LP task mutex’i elde etmiş ve işlemlerini yaparken bir context switching isteği gelmiş.
  2. HP task aynı mutex’i elde etmek istediğinde mutex hali hazırda başka bir task tarafından tutulduğu için elde edememiş ve bloklanmış.
  3. HP task bloklandığında sıradaki task olan MP task çalışması gerekirken LP task çalışmaya devam etmiş. Burada HP task önceliğini miras olarak LP task’a vermiş diyebiliriz. Buradaki amaç HP taskı olabildiğince az bloklanmış durumda tutmaktır.
  4. LP task mutex’i bıraktığında HP task mutex’i elde edebilmiş ve blocked stateden çıkıp işlemlerini yapmış.
    Mutex’ler interrupt içerisinden kullanılmamalıdır çünkü;
  • Tasklar arasında geçerli bir öncelik kalıtım mekanizması içerirler, interruptlar arasında değil.
  • Bir interrupt mutex tarafından korunan bir kaynağın ulaşılabilir olmasını beklemek için engellenemez.
    FreeRTOS da mutex kullanabilmek için FreeRTOSConfig.h dosyası içindeki configUSE_MUTEXES makrosu 1 olarak ayarlanmalıdır.

xSemaphoreCreateMutex() Fonksiyonu

    FreeRTOS da mutex oluşturabilmek için xSemaphoreCreateMutex() fonksiyonu kullanılır.

  • Return Değeri: Eğer mutex oluşturulamadı ise NULL, oluşturuldu ise handle döndürür.

    Mutex için semaphore fonksiyonları aşağıdaki gibi kullanılır.

    Binary semaphore’un çalışma mantığını hatırlayalım. Normalde xSemaphoreTake() satırında task eğer semaphore mevcut değil ise süresiz olarak bloklanıyordu ta ki başka bir task veya interrupt tarafından xSemaphoreGive() fonksiyonu ile verilene kadar. Mutex te ise xSemaphoreTake() fonksiyonu kullanıldığında task mutex’e sahip olur ve task bloklanmadan işlemlerine devam eder. Daha sonra aynı task içerisinde xSemaphoreGive() fonksiyonu kullanılana kadar yapılan işlemlere başka bir task erişemez. Bu binary semaphore ile mutex arasındaki farktır. Burada önemli bir nokta var. Örneğin bir kaynak iki task tarafından kullanılıyor ise iki task ta mutex’i elde etmek istemelidir. Mutex’i ilk elde eden task işlemlerini yapar. Diğer task ise mutex’i elde edemediği için blocked state e girer. Böylece iki taskın aynı kaynağa erişmesi engellenmiş olur.

Recursive Mutex

    Recursive mutex counting semaphore gibi çalışır. Eğer bir task mutex’i beş defa elde ederse mutex’i elde etmek isteyen bir başka task mutexin beş defa verilemesini(give) beklemek zorundadır. Recursive Mutex için başlıca API fonksiyonları şunlardır; xSemaphoreCreateRecursiveMutex(), xSemaphoreTakeRecursive(), xSemaphoreGiveRecursive().

FreeRTOS Mutex Kullanımına Ait Örnek

    Amaç; Uart üzerinden string gönderen iki task oluştur. Task fonksiyonları birebir aynı olsun(Göndereceği stringler hariç). Mutex kullanmadan verilerin nasıl gideceği ve mutex ile verilerin nasıl gideceğini gözlemle. Burada ortak kullanılan kaynak UART1 dir.
    Öncelikle mutex’i kullanabilmek için semaphore kütüphanesini projeye dahil ediyoruz. Mutexin aynı semaphore API fonksiyonlarını kullandığını hatırlayalım.

    Mutex için kullanılacak olan handle’ı main fonksiyonunun üzerinde tanımlıyoruz çünkü farklı task fonksiyonları içerisinden bu handle’ı çağırmamız gerekecek.

    main fonksiyon içerisinde Mutex oluşturma işlemini yapıyoruz.

    İki adet task oluşturuyoruz.

    Burada önce mutex kullanmadığımız durumu görebilmek için aşağıdaki gibi task fonksiyonlarını oluşturduk.

    Bu fonksiyonları bir inceleyelim. Burada task fonksiyonları gönderdikleri mesajlar hariç birebir aynıdır değil mi? Yani burada aynı UART üzerinden farklı mesajlar aynı anda gönderilmeye çalışılabilir. Burada HAL kütüphanesinin UART fonksiyonlarını kullandığımız için UART a iki taskın ayna anda erişmesi aslında HAL kütüphanesi tarafından engellenir çünkü HAL kütüphanesi transmit fonksiyonunu kullanırken __HAL_LOCK(huart) ile UART’ı kilitler. Yani Tasklardan biri UART’ı kullanırken bir context switching oluşsa bile diğer task UART kilitli olduğu için erişemeyecek ve transmit fonksiyonu HAL_BUSY değerini döndürecektir. Fakat HAL kütüphanesi kullanmadığımız durumda yani transmit fonksiyonunu register seviyesinde veya SPL ile kendimiz yazdığımızda(HAL’daki gibi bir lock mekanizması yok ise) UART’tan veri gönderirken bir context switching oluştuğunda gönderilen stringin yarısı Task1 in göndermek istediği, diğer yarısı da Task2 nin göndermek istediği string olarak karşı tarafa iletilecektir. Yukarıdaki task fonksiyonlarında UART başka bir task tarafından kullanılırken diğer taskın  UART’a erişip erişemeyeceğini görebilmek için integer değeri her seferinde arttırılarak UART üzerinden gönderilmiştir. Yukarıdaki task’ların çıktısı aşağıdaki gibi olur.
    Yukarıda görüldüğü gibi normalde  her taskın sayacı doğrusal olarak artması gerekirken, birer atlayarak sayac artmış. Aslında sayac birer birer artıyor ama UART’a sadece tek bir task erişebildiği için diğer task’ın gönderdiği string te sayacın bir fazla olduğunu görüyoruz. Yani normalde göndermek istediğimiz stringler ;
Task1 is running: 0
Task2 is running: 0
Task1 is running: 1
Task2 is running: 1
 şeklinde olması gerekirken yukarıdaki resimdeki gibi olmuştur. Burada HAL kütüphanesi UART’ı kilitlemeseydi iki fonksiyonda aynı UART ile string göndermeye çalışacak ve yazının başında verdiğim örneğe benzer bir durum olacaktı. Şimdi bu sorunu mutex kullanarak giderelim.

    Yukarıda görüldüğü gibi mutex’i sadece Uart_Print() fonskiyonundan önce(take) ve sonra(give) kullanarak bu sorunu çözebiliriz. Yukarıda önce Task1 çalışacak ve mutex’i veya token’ı elde edecektir. Task1 UART üzerinden string gönderirken bir context switching oluşsa bile Task2 mutex’i elde edemeyeceği için UART’ı kullanamayacak ve blocked state’e girecektir. Task1 tekrardan kaldığı yerden çalışıp mutex’i bıraktığında Task2 mutex’i elde edecek ve UART üzerinden string gönderecektir. Mutex kullanılmış halinin çıktısı aşağıdaki gibi olur.  FreeRTOS da mutexin kullanım amacı basitçe böyledir.

Tüm Kodlar (FreeRTOS ile)

CMSIS-RTOS ile Mutex Kullanımı

    Mutex için kullanılan handle tipi osMutexId dir ve aşağıdaki gibi tanımlanır.

    Mutex tanımlayabilmek için osMutexDef() makrosu kullanılır.

    Mutex oluşturmak için osMutexCreate() fonksiyonu kullanılır.

    osMutexCreate() fonksiyonuna parametre osMutex makrosu referansı ile girilir. osMutex() ise parametre olarak osMutexDef() makrosuna girilen mutex ismini alır.

    CMSIS-RTOS da mutex aşağıdaki gibi oluşturulabilir.

    CMSIS-RTOS da xSemaphoreTake() fonksiyonu yerine osMutexWait() fonksiyonu kullanılır.

    Bu fonksiyon osStatus tipinde bir değer döndürür. osMutexWait fonksiyonu bu değeri osErrorOS veya osOK şeklinde döndürür. Bu fonksiyon aşağıdaki gibi kullanılabilir.

    Mutex’i bırakmak için CMSIS-RTOS da osMutexRelease() fonksiyonu kullanılır. Aldığı parametre ise mutex handle’ı dır.

    Bu bilgiler ışığında task fonksiyonları aşağıdaki gibi düzenlenebilir.

CubeMX Ayarları

    “FreeRTOS Configuration” penceresinde “Config parameters” sekmesindeki “USE_MUTEXES” kutucuğu “Enabled” olarak aşağıdaki gibi ayarlanmalıdır.

    CubeMX’de Mutex “Timers and Semaphores” sekmesinde aşağıdaki gibi oluşturulur.

Tüm Kodlar (CMSIS-RTOS ile)

Kaynaklar

Mehmet Topuz

Bir cevap yazın

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