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.
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:
[code]
.globl GetGpioAddress
GetGpioAddress:
ldr r0,=0x20200000
mov pc,lr
[/code]
ş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.
[code]
.globl SetGpioFunction
SetGpioFunction:
cmp r0,#53
cmpls r1,#7
movhi pc,lr
[/code]
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:
[code]
push {lr}
mov r2,r0
bl GetGpioAddress
[/code]
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:
[code]
functionLoop$:
cmp r2,#9
subhi r2,#10
addhi r0,#4
bhi functionLoop$
[/code]
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.
[code]
add r2, r2,lsl #1
lsl r1,r2
str r1,[r0]
pop {pc}
[/code]
Ç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:
[code]
.globl SetGpio
SetGpio:
pinNum .req r0
pinVal .req r1
[/code]
.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.
[code]
cmp pinNum,#53
movhi pc,lr
push {lr}
mov r2,pinNum
.unreq pinNum
pinNum .req r2
bl GetGpioAddress
gpioAddr .req r0
[/code]
İ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:
[code]
pinBank .req r3
lsr pinBank,pinNum,#5
lsl pinBank,#2
add gpioAddr,pinBank
.unreq pinBank
[/code]
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.
[code]
and pinNum,#31
setBit .req r3
mov setBit,#1
lsl setBit,pinNum
.unreq pinNum
[/code]
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.
[code]
teq pinVal,#0
.unreq pinVal
streq setBit,[gpioAddr,#40]
strne setBit,[gpioAddr,#28]
.unreq setBit
.unreq gpioAddr
pop {pc}
[/code]
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.
[code]
b main
.section .text
main:
mov sp,#0x8000
[/code]
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.
[code]
pinNum .req r0
pinFunc .req r1
mov pinNum,#16
mov pinFunc,#1
bl SetGpioFunction
.unreq pinNum
.unreq pinFunc
[/code]
Ayrıca OK LED’ini değiştiren eski kodu da aşağıdakiyle değiştiriniz.
[code]
pinNum .req r0
pinVal .req r1
mov pinNum,#16
mov pinVal,#0
bl SetGpio
.unreq pinNum
.unreq pinVal
[/code]
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.