Skip to content

Raspberry Pi’ye İşletim Sistemi Yazmak Ders 3

Ders 3, assembly’de fonksiyonların nasıl kullanıldığını öğretecektir. Fonksiyonlar, tekrar tekrar kullanılabilirdir ve daha okunabilir bir yazım şekli sunar.

Tekrar Kullanılabilir Kod

Daha önce yazdığımız kodlar, işletim sisteminde istediğimiz şeyler yaptıran ve belli bir sıraya göre yazılmış kodlardı. Bu şekilde yazmak kısa kodlar için iyi olabilir. Fakat tüm bir sistemi yazmak istediğinizde kod satırları uzayacak ve okunabilirlik azalacaktır ve de bakım zorlaşacaktır. Bu yüzden fonksiyon kullanıyoruz.

Fonksiyonlar, belli sorulara cevap olacak hesaplamalar yapan veya belli bir işi gerçekleştiren, tekrar kullanılabilir kod parçalarıdır.

Matematik derslerinizden fonksiyon konseptini hatırlıyorsanız şanslı sayılırsınız. Örneğin cos fonksiyonuna giren bir sayı sonucunda başka bir sayıyı verir. Yani f(x)=cos(x), x=90 veya pi/2 için cos(90)=0 sonucunu alacağız. x’in değişimine göre de değer -1 ve 1 arasında değişiklik gösterecektir. Kod içinde de benzer şekildedir. Fakat bir giriş değil birden fazla değişken girişi olabilir.

Assembly’e göre yüksek seviyeli diller olan c ve c++’da fonksiyonlar dilin bir parçasıdır. Assemblyde ise bize sadece fikir verirler.

Fonksiyon kullanımı registerlar üzerinden yapıldığı için, karmaşıklık azalsın ve geliştiriciler birbirlerinin yazdığı kodları anlasın diye bir standart ortaya konulmuştur. Fonksiyonlar bu standarta göre yazılmalıdır. Standardın adı, Application Binary Interface(ABI)’dir. Standart bize r0-r3 arası registerların fonksiyona giriş değeri olarak kullanılabileceğini bize söyler. Fonksiyon giriş değerine ihtiyaç duymuyorsa o zaman ne değer aldığı farketmez. Eğer sadece bir giriş değerine ihtiyaç varsa r0 girer. Eğer iki giriş değerine ihtiyaç varsa r1 gider ve böyle devam eder. Çıkış daima r0’da olacaktır. Eğer çıkış yoksa r0’ın ne değer aldığı farketmez.
Ayrıca fonksiyon çalıştıktan sonra, r4’ten r12’ye kadar aynı değerler almalı. Bunun manası, fonksiyonu çağırdığınızda r4’ten r12’ye kadar olan değerlerin değişmeyeceğinden emin olabilirsiniz. Fakat r0-r3 arasındaki registerların değişmediğinden emin olamazsınız.
Fonksiyon tamamlandığında geri geldiği yerin bir altına dallanır. Bunun manası kodun başlangıç adresini bilmeniz gerektiğidir. Bunun için özel bir register’a sahibiz. lr isimli register fonksiyon bittiğinde geri dönüş için adres tutar.

Tablo 1.1 ARM ABI Register Kullanımı
Register Görev Korumalı Kurallar
r0 Argüman ve sonuç No r0 ve r1 fonksiyonlara değer geçmek için ve fonksiyonlardan sonuç döndürmek için kullanılır. Fonksiyon dönüş değeri için onları kullanmıyorsa, fonksiyondan sonra herhangi bir değer alabilir.
r1 Argüman ve sonuç No
r2 Argüman No r2 ve r3 argüman olarak kullanılırlar. Fonksiyon çağrıldıktan sonra değerleri herhangi bir şey olabilir.
r3 Argüman No
r4 Genel Amaçlı Yes r4-r12 registerları çalışan değerler için kullanılırlar. Değerleri fonksiyon çağrıldıktan sonra aynı olmalıdır.
r5 Genel Amaçlı Yes
r6 Genel Amaçlı Yes
r7 Genel Amaçlı Yes
r8 Genel Amaçlı Yes
r9 Genel Amaçlı Yes
r10 Genel Amaçlı Yes
r11 Genel Amaçlı Yes
r12 Genel Amaçlı Yes
lr Dönüş adresi No lr kısaca dönüş adresini tutar.
sp Yığın işaretçisi Yes sp, stack pointer manasına gelir. Değişmemesini istediğimiz değeri yığına iter sonra da işlem bittikten sonra geri çekebiliriz.

r0-r3 arası registerlardan daha fazla register’a ihtiyaç duyan fonksiyonlar kullanıldığında r4-r12 arası registerlar aynı kalması gerektiğinden dolayı yığın belleğe itilerek ve işlem bittikten sonra geri çekilerek aynı korunabilirler.

Stack’a itmek için push komutu kullanılır. Stack’tan çekmek için ise pop komutu kullanılır. Örneğin push {r0} dediğimizde bu stack’a itilecektir. sub r0,#4 komutu r0’dan 4 çıkaracaktır ve sonuç r0’da olacaktır. Fakat biz eski sayıyı geri getirmek istiyoruz o zaman pop {r0} diyoruz. Sonuç olarak r0 eski haline dönüyor.

İlk Fonksiyonumuz

Giriş değeri almayan çıkış  değeri olarak GPIO adresi veren bir fonksiyon yazacağız. Daha önceki derslerimizde GPIO adresini değer olarak yazmıştık. Fakat bunun fonksiyon olarak yer alması daha iyidir. Çünkü bunu kullanmaya sürekli ihtiyaç duyacağız ve her seferinde adresi aklımızda tutup yazmamız akıllıca değildir.

source klasörümüzü açalım ve gpio.s şeklinde bir dosya oluşturup içine:


.globl GetGpioAddress
 GetGpioAddress:
 ldr r0,=0x20200000
 mov pc,lr

şeklinde yazalım. .globl etkiteki bu fonksiyonun her yerden erişilebilir olduğunu anlatmak için yazılmıştır. Artık GetGpioAddress etiketi tüm dosyalar için kullanılabilir bir etiket haline gelmiştir.

ldr r0,=0x20200000 ne yaptığını daha önceki derslerimizde söylemiştik. r0’a belirtilen adresi yükler.

mov pc,lr ise geri dönüş adresini program counter’a atar. Program Counter denilen register özel bir register’dır. Bir sonraki çalışacak komutun adresi burada yer almaktadır. Bu yüzden buraya geri dönüş değerini atıyoruz ki geri dönsün. 8086’da ret komutunun yaptığı aslında budur.

Büyük Bir Fonksiyon

Şimdi daha büyük bir fonksiyonu uygulayacağız. İlk işimiz pin 16’ya çıkış yetkisi vermekti. Fonksiyon kullanılsa daha iyi olurdu. Giriş olarak basitçe, bir pin ve bir fonksiyon belirtebilirdik, böylelikle bu fonksiyon değerler için pin fonksiyonunu ayarlayabilirdi. Bu şekilde sadece led değil her GPIO pini kullanılabilirdi.

Aşağıdaki fonksiyonu gpio.s içinde GetGpioAddress fonksiyonunun altına kopyalayalım.


.globl SetGpioFunction
 SetGpioFunction:
 cmp r0,#53
 cmpls r1,#7
 movhi pc,lr

Burada amacımız ilk 54 pine sahip olduğumuz için değerin 0-53 arasında olmasını sağlamak ve her pin için 8 fonksiyon olduğundan dolayı aralıkları buna göre belirleyeceğiz.

.globl bildiğimiz bir tanımdı ve her yerden çağrılabilmeyi sağlıyordu, fonksiyon tanımlamak için kullanıyorduk.

cmp r0,53, r0 ile 53 değerini karşılaştırır. Burada yeni bir kısaltma olan cmpls ile karşılaşıyoruz. Bu bir üstteki karşılaştırma küçük ve aynı ise bu karşılaştırmayı yap demektir.r1 ile 7 karşılaştırılacak eğer büyükse fonksiyon movhi çalışacak ve fonksiyon bitecektir.

ls ön eki low veya same(düşük veya aynı), hi ön eki high(yüksek), gt ön eki grater(büyük) manalarına gelmektedir.

r0 eğer 53’ten büyükse hemen altındaki cmpls ile başlayan satır çalışmayacaktır. Fakat movhi fonksiyonu çalışır. Eğer r0 53’den küçük veya eşitse, r1 ile 7 karşılaştırılır eğer r1 küçükse movhi ile başlayan satır çalışmaz.

Şimdi yukarıdaki kodun altına şu kodu ekleyelim:

push {lr}
 mov r2,r0
 bl GetGpioAddress

Burada link register’ı korumak adına stack(yığın)’a itiyoruz. r0’ın değerini r2’ye atıyoruz ve Gpio adresini kullanacağımız fonksiyonu çalıştırıyoruz. Aslında fonksiyonlar call komutu ile de çağrılabilir. GetGpioAddress fonksiyonunda r0 değişikliğe uğrayacağı için onun değişmemesini sağlamamız gerekiyor. r2’nin değişmeyeceğini bildiğimiz için oraya atıyoruz. Sonra da bl komutuyla fonksiyonumuza dallanıyoruz.

Biliyoruz ki bl fonksiyonu çağrılırken sıradaki komutun adresi lr’ye atılır. Fonksiyon bittiğinde ve geri döndüğünde bileceğiz ki r0 GPIO’nun adresini tutacak. r1 fonksiyon kodunu ve r2 ise GPIO pin numarasını tutacak. Daha önce GPIO fonksiyonlarının 10’lu bloklar halinde saklandığını söylemiştik, bu yüzden bizim pin numaramasın hangi on içinde olduğunu bulmamız gerekiyor. Bölme yapmak kulağa hoş geliyor fakat aslında bölme işlemi yavaş çalışır. Biz onun yerine tekrarlı çıkarma işlemi yapacağız.

Yukarıdaki kodun altına şu kodu yazalım:


functionLoop$:

cmp r2,#9
 subhi r2,#10
 addhi r0,#4
 bhi functionLoop$

cmp satırında r2’de sakladığımız pin numarasını 9 ile karşılaştırıyoruz. Eğer büyükse r2’den 10’u çıkarıyoruz. r0’a 4 ekliyoruz ve bu işlem r2 9’a eşit veya küçük olana kadar sürüyor. En sonunda r2’de 10’a bölümden kalan kalacaktır.

Son olarak aşağıdak kodu yazalım.


add r2, r2,lsl #1
 lsl r1,r2
 str r1,[r0]
 pop {pc}

Çarpma işlemi assembly’de büyük ve yavaş bir talimattır. Bu yüzden 3 ile çarpmak için daha hızlı bir kod parçası yazabiliriz. yukarıdaki kodda ilk satırda bu yapıldı. r2 önce 2 ile çarpılıyor bunu r2,lsl 1 ile yapıyoruz, sola öteleme 2 ile çarpma yapmak demektir. Sonra bunu tekrar r2 ile topluyoruz sonuç 3 ile çarpım oluyor. Sonra r1’i, r2 kadar sola kaydırıyoruz. Bunu pin başına 3 bit olduğu için yapıyoruz. Daha önceki derslerimizde bundan bahsetmiştik.

str ile başlayan satırda r0’ın adresini r1’e yüklüyoruz. Daha sonra da program counter’a, stack(yığın)’daki lr’yi atıyoruz. Böylelikle fonksiyon çalıştığı yere dönebilir.

Bu fonksiyonun düzgün çalışmadığını farketmiş olabilirsiniz. Buna rağmen istenilen değeri GPIO pininin fonksiyonuna ayarlar. Bütün pinler 10’nun fonksiyonlarının aynı bloktaki pinlerinin sıfıra gitmesinden dolayı sorun yaşanır. İpucunu verdikten sonra sizi kodu düzeltmeyle başbaşa bırakıyorum. Çözüme buradan ulaşabilirsiniz.

Bir Başka Fonksiyon

GPIO pin numarası olarak r0’daki birinci girişi alan ve ikinci değer olarak r1’deki değeri alan SetGpio fonksiyonunu yazacağız. Bu fonksiyon değer sıfır ise pin’i kapatacak, sıfır değilse pini açacaktır.

gpio.s dosyasının sonuna aşağıdaki kodu kopyalayın:

.globl SetGpio
 SetGpio:
 pinNum .req r0
 pinVal .req r1

.globl’in ne işe yaradığını artık biliyoruz. Fonksiyona her yerden erişilebilir olduğunu belirtiyor.
r0 ve r1 gibi registerların ne işe yaradıklarını her zaman aklımızda tutmak zorunda değiliz. Register isimlendirme komutu bize bu konuda yardımcı olacak. pinNum ile başlayan satırda r0’ın artık pinNum ile temsil edileceği belirtilmiştir. Aynı şekilde pinVal ile başlayan satırda ise r1 artık pinVal tarafından temsil edilebilecek demektir.


cmp pinNum,#53
 movhi pc,lr
 push {lr}
 mov r2,pinNum
 .unreq pinNum
 pinNum .req r2
 bl GetGpioAddress
 gpioAddr .req r0

İlk satırda pinNum’daki değerle yani r0 ile 53 değeri karşılaştırılıyor. Eğer büyükse bir altındaki satır işlenir ve fonksiyon biter. Değilse fonksiyon devam eder. push {lr} satırı ile stack(yığın)’a lr itilir. mov ile başlayan satırda pinNum’daki değer r2’ye aktarılır. .unreq isimlendirmeyi serbest bırakmak demektir. Bundan sonra pinNum r0’ı temsil etmeyecek demektir. Sonraki satırda pinNum artık r2’yi temsil etmiş olacaktır. Son olarak gpioAddr ismi ise r0’ı temsil eder hale getirilmiştir.

Aşağıdaki kodu yukarıdaki kodun altına kopyalayın:


pinBank .req r3
 lsr pinBank,pinNum,#5
 lsl pinBank,#2
 add gpioAddr,pinBank
 .unreq pinBank

pinBank’ı r3’ü temsil edecek şekilde ayarlıyoruz. Daha sonra bölme işlemi yapmamız gerekiyor. GPIO controller 4 byte’lık 2 sete sahiptir. Bunlardan ilk set 32 pini, diğer set ise kalan 22 pini kontrol eder. Pin numarasını 32 ile bölüyoruz. Bunu öteleme yaparak gerçekleştirmek çok kolay. shr kullandığımız zaman her sağa kaymada 2’ye bölme işlemi yapar. 32’ye bölmek istediğimiz için pin numarasını 5 öteliyoruz. (lsr hedef,kaynak,#değer kullanımı kaynaktaki değeri 5 öteleyip hedefe yazar) . Daha sonra 4 byte’lık setler olduğundan 4 ile çarpma yapmamız gerekecek. Bunun için de sola doğru iki öteleme yapmamız yetecektir. Daha önce gpioAddr isminin r0’ı temsil ettiğini biliyoruz. Buna pinBank sayısını ekliyoruz. Peki 32 ile bölüm 4 ile çarpmak yerine 8’e neden bölmüyoruz? Sadece 3 kez sağa kaydırmanın neden yeterli olmadığını merak ediyor olmalısınız. 32’ye böldüğümüzde yuvarlanan bazı değerler 8’e böldüğümüzde yuvarlanmayacaktı. Bu yüzden böyle bir yol izledik. Son olarak pinBank ile işimiz bittiği için .unreq kullanıyoruz.


and pinNum,#31
 setBit .req r3
 mov setBit,#1
 lsl setBit,pinNum
 .unreq pinNum

Bu fonksiyon doğru bit kümesi ile sayı türetmek içindir.GPIO denetleyicisinin pinini açmak ve kapatmak için, ona pin numarasının 32’ye bölümünden kalan bit kümesiyle bir sayı veririz. Açık bir ifade ile eğer 16. pini ayarlamak istiyorsanız, o zaman 16. sayının 1 olması gereklidir. Eğer 45. pini ayarlamak istiyorsanız. İlk 32 bitten sonra hangi bitlerin 1 olacağını bilmeniz gerekir. Bunun için 32’ye bölmemiz ve kalanı da saklamamız gerekir. Şimdi fonksiyonu açıklayalım:

and ile başlayan satırda #31 değeriyle 32’ye bölümden kalan pinNum değişkenine aktarılıyor. Daha sonra setBit, r3’ü temsil edecek şekilde ayarlanıyor. mov satırında setBit’e 1 değişkeni atanıyor. Bu bir sayısı belirlenin pin sayısı kadar öteleniyor ve pinNum .unreq kullanılarak serbest bırakılıyor.


teq pinVal,#0
 .unreq pinVal
 streq setBit,[gpioAddr,#40]
 strne setBit,[gpioAddr,#28]
 .unreq setBit
 .unreq gpioAddr
 pop {pc}

Metodumuzun son kısmına geldik. Burada teq komutunu görüyoruz. Bu komut test equal’ın kısaltılmış şeklidir. pinVal değeri ile #0 değerini karşılaştırmaya yarar. Kodda .unreq satırı her durumda çalışacaktır daha sonra eğer pinVal 0’a eşitse o zaman gpioAddr değişkenine 40 eklenip setBit’e atanacak, eğer eşit değilse gpioAddr değişkenine 28 eklenip setBit’e atanacak.

En son her fonksiyonda olduğu gibi stack(yığın)’a ittiğimiz lr’yi pop{pc} olarak çekiyoruz ve kaldığımız yere geri dönüyoruz.

Yeni Bir Başlangıç

Sonunda, çalışan GPIO fonksiyonlarına sahip olduk. Yukarıda yazdığımız kodları kullanabilmek için main.s dosyamızda değişiklik yapmamız gerekecek. Ne yazık ki, şimdi kullandığımızdan daha büyük bir işletim sistemine sahibiz. Bu eğer 0x100 adresi özel bir adres olmasaydı ve işletim sistemi yüklendiğinde değişmeseydi iyi bir şey olabilirdi. Daha önce yazdığımız ufak kodlarda bu sorun değildi fakat şimdi Rpi açıldığında işletim sistemimizin üzerine tekrar yazılma olacak bu da değişikliklere neden olacak. Neyseki bu zor düzeltilebilen bir sorun değil.

_start: ‘tan sonra aşağıdaki kodu giriniz.


b main

 .section .text
 main:
 mov sp,#0x8000

Buradaki anahtar değişiklik .text bölgesinde yapıldı. makefile ve linker script’leri yeniden dizayn edildi, .text bölgesindeki code 0x8000 adresine yerleştirildi. Bu üzerine yazılan bölgeyi dışarı taşıdığı gibi, stack için bize biraz boşluk da bırakır. Stack bellek aşağı doğru büyüyen bir hafıza olur. Her yeni değer düşük adreste yer alır. Buna göre stack’ın en üstü en düşük adrestir.

Yukarıdaki hafıza diyagramındaki ATAGs bölmesi, Raspberry Pi’nin ne kadar hafızaya sahip olduğu ve varsayılan ekran çözünürlüğünün ne olduğu gibi bilgilere benzer bilgilerin tutulduğu bölgedir.

Aşağıdaki kod ile Gpio’unun fonksiyonlarını ayarlayan kodu değiştiriniz.


pinNum .req r0
 pinFunc .req r1
 mov pinNum,#16
 mov pinFunc,#1
 bl SetGpioFunction
 .unreq pinNum
 .unreq pinFunc

Ayrıca OK LED’ini değiştiren eski kodu da aşağıdakiyle değiştiriniz.


pinNum .req r0
 pinVal .req r1
 mov pinNum,#16
 mov pinVal,#0
 bl SetGpio
 .unreq pinNum
 .unreq pinVal

Sonuç

Bu derste çok fazla kod yazıldı. Eğer düzgün çalıştırabildiyseniz tebrikler. Hata alıyorsanız çözümleri inceleyebilirsiniz.

Bu bölümün çözümü için tıklayınız.

Be First to Comment

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir