ARM Cortex-M çekirdeklerinde Context Switching süreci
Bu yazıyı okumadan önce konunun daha iyi anlaşılması için öncelikle aşağıdaki yazıyı okumanızı tavsiye ederim.
Gerçek zamanlı işletim sistemlerinde görev planlayıcısı (task scheduler) uygulamada seçilen planlayıcı ilkelerine (Round-Robin, Priority Based Preemptive, FIFO, Shortest Job First… gibi) göre görevleri sıraya sokan bir kod parçasıdır.
Hangi gerçek zamanlı işletim sistemi ve hangi planlayıcı ilkesi kullanılırsa kullanılsın çok görevli (multitasking) bir sistemde bir görevden diğerine geçişin gerektiği durumlarda context switching işlemi gerçekleşmektedir. Context switch sürecinde çalışmakta olan görevin durumu o görev için ayrılmış stack’e kaydedilir. Yeni çalışacak olan görevin de daha önceden kaydedilmiş durumu kendine ait stack’den alınarak çekirdeğin bu bilgiler dahilinde son kaldığı yerden devam etmesi sağlanır. Burada görevin durumunun kaydedilmesinden anlamamız gereken şey o göreve ait stack işaretçisi değerinin ve o anki çekirdek yazmaçlarının (registers) kaydedilmesidir.
ARM Cortex-M çekirdeklerinde context switching sürecinde çekirdek yazmaçlarının bir kısmı direk çekirdek tarafından otomatik olarak kaydedilmektedir. Geri kalan yazmaçların ise yazılımsal olarak işletim sistemi tarafından kaydedilmesi gerekmektedir. ARM Cortex-M çekirdeklerinde bir exception oluştuğunda işlemci modu thread modundan handler moduna otomatik olarak geçiş yapmaktadır. Bu geçiş sırasında da çekirdek aşağıda belirtilen yazmaçları o an sistem stack işaretçisi ne olarak seçilmiş (Main Stack Pointer veya Process Stack Pointer) ise o işaretçinin gösterdiği stack’in sonuna sırayla eklemektedir.
Yukarıda da görüldüğü gibi Floating Point kullanımına göre çekirdeğin stacking yazmaçları değişse de ortak olarak R0, R1, R2, R3, R12, LR (Link Register), PC (Program Counter), xPSR (Program Status Register) yazmaçları stack’e otomatik olarak kaydedilmektedir. Ancak R4 ile R11 arasındaki yazmaçların kaydedilmesi otomatik olarak gerçekleşmediği için bu yazmaçların kaydedilmesi yazılım tarafından yapılması gerekmektedir.
ARM Cortex-M çekirdeklerinin port edildiği gerçek zamanlı işletim sistemlerinde en iyi kullanım olarak iki farklı stack işaretçisi kullanılır. Bunlar Main Stack Pointer (MSP) ve Process Stack Pointer (PSP) olarak tanımlanmaktadır. Yazı başında referans verdiğim yazımda da anlattığım üzere MSP ve PSP’nin gerçek zamanlı işletim sistemlerinde ayrı stack işaretçileri olarak kullanılmasının nedeni işletim sistemi ve exception işleyicilerinin stack’ini kullanıcı uygulamalarının kullandığı stack’den ayırmaktır. Böylelikle işletim sistemi stack’i kullanıcı uygulamalarından korunmaktadır. Ayrıca kullanıcı uygulamaları PSP stack alanını tüketse bile her zaman işletim sisteminin ayakta olması için yeterli alan olacak ve sistem, hata ve kesme işleyicileri her zaman servis edilebilecektir.
Reset sonrasında varsayılan olarak stack işaretçisi görevini MSP üstlenmektedir. Çekirdeğin CONTROL yazmacı içinde SPSEL biti kullanılarak aşağıdaki kod bloğu ile PSP’nin stack işaretçisi görevini üstlenmesi sağlanabilmektedir. Çekirdeğin genel amaçlı ve özel yazmaçlarına C veya C++ dili ile doğrudan ulaşabilmemiz mümkün olmadığından dolayı inline assembly kullanarak bu yazmaçlara ulaşmaktayız.
Gömülü yazılım dünyasında bilgisi az fikri çok, hayatını büyük projelerde sürekli yazılım geliştirmiş gömülü yazılım ilahı gibi aktaran bazı insanlardan “Assembly mi kullanılıyor dostum artık oww ye men sana puanım 9 kanka”, “Kanka hangi devirdeyiz ya punch kartla programlama yapalım bari”, “Abi ne assembly’si ya assembly ve C gereksiz bu yüzden C++’a geçtim ledimi C++ ile yakıp söndürüyorum artık” gibi sözler duyarsanız hayatınızdan çalınan o üzücü zamana rağmen ufak bıyık altı bir gülümseme sonrasında muhabbet dışına kendinizi itebilmek için artık elinizden geleni yapabilirsiniz. Hem çekirdek seviyesinde hem de kullanıcı seviyesinde uygulama geliştirdiğinizde yerine göre assembly yerine göre C, yerine göre de C++ kullanacağınız zamanlar olacaktır. Bu yüzden programlama dili faşistlerinden mutlaka uzak durunuz. Hele ki bu muhabbet içerisinde programlama diline yazılım dili diyenlere karşı uzak durma öncelik seviyesinizi en üste çıkarmanızı tavsiye ederim.
ARM Cortex-M çekirdeklerinde Full-Descending stack yapılanması olduğu için örnek bir stack yerleşimi aşağıda belirtilmiştir.
ARM Cortex-M çekirdeklerine uyarlanmış gerçek zamanlı işletim sistemlerinde context switch işlemi PendSV (Pendable Service) exception içerisinde yapılmaktadır ve PendSV exception’u en düşük öncelikli exception olarak ayarlanmaktadır. Bunun nedeni ise sistem servisleri ve kesmeler içerisinde context switch işlemi yapılması gerekirse önce ilgili sistem exception’unun veya kesmenin tamamlanıp daha sonra context switch işleminin yapılmasıdır. Aynı zamanda eğer bir exception tamamlanmadan context switch işlemi yapılırsa yani handler moddan thread moda ilgili kod bloğu tamamlanmadan geçilirse bu ARM Cortex-M çekirdeklerinde Usage Fault oluşmasına sebebiyet verecektir.
1 ms tick ile round-robin planlayıcı ilkesine sahip bir sistem için örnek bir şema yukarıda belirtilmiştir.
PendSV exception’u ICSR (Interrupt Control and Status Register)’ı kullanılarak tetiklenmektedir. PENDSVSET biti set edilerek PendSV exception’u tetiklenir. Yüksek öncelikli kesmeler ve sistem exception’ları servis edildikten sonra PendSV ele alınır.
Gerçek zamanlı işletim sistemleri zaman ve görev planlamasını yapabilmek için bir tick’e ihtiyaç duyar. ARM Cortex-M çekirdeklerinde bu amaçla kullanabilecek SysTick servisi bulunmaktadır. Bu servis ile global tick yaratabilmemiz mümkündür. SYST_CSR (SysTick Control and Status Register), SYST_RVR (SysTick Reload Value Register) yazmaçları ile SysTick servisinin ayarlamaları yapılmakta ve zamanlayıcı değeri de SYST_CVR (SysTick Current Value Register) ile elde edilebilmektedir.
16MHz clock ile beslenen bir çekirdek için aşağıdaki kod bloğu ile 1ms’lik bir sistem tick’i oluşturabilmemiz mümkündür.
Vektör tablosunda SysTick Exception’un ele alınması için hangi handler fonksiyonu bağlandıysa o handler artık her 1 ms’de bir periyodik olarak çağrılmaktadır ve görev planlayıcısının periyodik olarak çağrılması bu zaman servisinin içerisinde yapılmaktadır. Böylelikle PendSV exception’u da yine buranın içerisinde tetiklenerek context switch gerekli ise bunun çekirdek tarafından listeye alınması sağlanmaktadır.
Eğer SysTick kullanılmak istenmiyor ise mikrodenetleyici içerisinde yer alan bir timer çevrebirimi de bu tick’in sağlanması için kullanabilir ancak bütün ARM Cortex-M çekirdeklerinde ortak olarak SysTick birimi bulunduğu için mikrodenetleyici bağımsız bir yapının oluşmasını sağlanması amacı ile global tick olarak SysTick birimi kullanılabilmektedir.
PendSV Handler içerisinde context switch işlemi yapacak olan kod bloğu çağrılmaktadır. Kiran Nayak adında teknik olarak çok beğendiğim bir hintli abinin yazdığı bir örneği referans alan Context Switch kod bloğu aşağıda yer almaktadır.
Context Switch işlemi PendSV_Handler içerisinde yapılmaktadır. Burada temel olarak izlenen adımlar aşağıdaki gibidir.
- O an çalışan görevin (örneğin Görev1) PSP değeri alınır.
- Görev1'e ait PSP değeri kullanılarak Görev1'e ait özel stack’e işlemcinin otomatik olarak kaydettiği yazmaçlar dışında kalan yazmaçlar (R4–11) kaydedilir.
- Stack’e veri yazıldığı için stack işaretçisi hareket ettiğinden dolayı Görev1'in güncel PSP değeri kaydedilir.
- Daha sonra çağrılacak yeni görev belirlenir (Görev2). Eğer çağrılacak yeni bir görev yoksa işlemciyi arka planda meşgul edecek olan idle görev çağrılır.
- Yeni görevin (Görev2) en sonki PSP değeri alınır.
- Bu PSP değerine göre Görev2'nin en son çalıştığı andaki durumu stack’den geri çekilir ve akış buradan devam eder. Böylelikle Görev1'den Görev2'ye geçiş yapılmış olunur.
- Bu süreç periyodik olarak sürekli devam eder.
Görev Planlayıcı ilkesine göre değişen kısım burada yeni görevin belirlemesi kısmıdır. Örneğin Round-robin ilkesinde belli zaman dilimlerinde sırayla bütün görevleri çağıracak bir algoritma yeni görevi belirlerken, priority based preemptive ilkede görevlerin önceliklerine göre yüksek öncelikli görev belirlenmektedir. Bunun haricindeki bütün adımlar görev planlayıcı ilkesinden bağımsız olarak ortak işlemektedir.
Şimdiye kadar anlatılan adımların hepsini içeren bir örnek aşağıda belirtilmiştir. Bu örnekte;
- Round-robin pre-emptive görev planlayıcı ilkesi kullanılmıştır.
- Kullanıcı görevleri stack işaretçisi olarak PSP kullanırken görev planlayıcısı MSP kullanmaktadır.
- 1 ms SysTick servisi bulunmaktadır ve görev planlayıcısı her 1 ms’de bir periyodik olarak çalışmaktadır.
- 3 tane ana görev, 1 tane de idle görev bulunmaktadır. Idle görev diğer görevlerin bloklu durumda olduğu senaryoda çalışmaktadır.
- 3 ana görev içerisinde 1 sn’de bir değişkeni 1 yükseltmesi sağlanarak test ortamı oluşturulmuştur. Bu görevler içerisinde led de yakıp söndürülebilirdi ancak mikrodenetleyici bağımsız bir örnek oluşması açısından bu şekilde oluşturulmuştur.
- Delay fonksiyonu busy wait yerine ilgili görevi blok durumuna çekerek başka görevlerin çalışmasına imkan vermektedir.
- Hata exceptionlarının hepsi (HardFault, MemManage Fault, BusFault, UsageFault) aktif edilmiştir.
Referanslar ve Faydalı Kaynaklar