Bölüm 5: ACALL · LCALL · RET · PUSH · POP · Stack Pointer · Program Çökmeleri · CFG841
Bir program yazarken aynı işlemi birden fazla kez gerçekleştirmemiz gerekebilir. Bu işlemi her ihtiyaç duyulan yere tekrar tekrar yazmak, iki önemli soruna yol açar:
Çözüm: Tekrarlanan kod bloğunu bir kere yazıp, ihtiyaç duyulduğunda bu bloğa dallanmak. Bu bloğa alt program (subroutine) denir.
Ana program akışı bir ACALL veya LCALL komutuyla alt programı çağırır. Alt program biter bitmez RET komutu çalışır ve program akışı tam olarak çağrıldığı noktanın hemen sonrasına döner.
| Komut | İşlev | Hafıza Kullanımı | Kapsam |
|---|---|---|---|
ACALL <isim> |
Alt program çağrısı | 2 Byte (program hafızasında) | 2 kByte blok içi |
LCALL <isim> |
Alt program çağrısı | 3 Byte (program hafızasında) | Tüm 64 kByte |
RET |
Alt programdan çıkış/dönüş | 1 Byte | — |
Aşağıdaki örnek, P1 çıkışını belirleyen bir alt programın nasıl çağrılacağını göstermektedir.
org 00h ; Program başlangıç adresi sjmp ana ; ─── ANA PROGRAM ─────────────────────────────────────── ana: mov A, #55h ; Akümülatöre değer yükle acall cikis_ayarla ; Alt programı çağır ① mov A, #AAh ; Alt programdan döndükten sonra ② acall cikis_ayarla ; Alt programı tekrar çağır sjmp ana ; Sonsuz döngü ; ─── ALT PROGRAM ──────────────────────────────────────── cikis_ayarla: mov P1, A ; A'daki değeri P1 portuna yaz ret ; Ana programa geri dön ③ end
ACALL cikis_ayarla — Program akışı cikis_ayarla etiketine sıçrar.
Geri dönüş adresi yığına (stack'e) otomatik kaydedilir.
Dönüş noktası — ret çalıştığında PC, acall'dan
bir sonraki komut olan mov A, #AAh satırına yüklenir.
RET — Yığından dönüş adresini okuyarak ana programa döner. Hafızaya hiçbir şey yazmaz.
Ana program akışı, ACALL veya LCALL komutları kullanılmadan kontrolsüz biçimde alt program koduna girmemelidir. Aksi hâlde RET komutu yığından geçersiz bir adres okur ve program çöker — hatalı bir adrese dallanılır.
SP (Stack Pointer), yığının (stack'in) o an kullanılan en üst hücresini gösteren özel bir SFR (Özel Fonksiyon Saklayıcısı) kaydedicisidir. 8051 mimarisinde 8-bit genişliğindedir ve reset sonrasında 07h değerini alır.
8051 yığını yukarı doğru büyür: her PUSH işleminde SP önce 1 artırılır, ardından veri o adrese yazılır. POP işleminde ise veri okunur, sonra SP 1 azaltılır.
SP iki kritik görevi yerine getirir:
Reset sonrası SP = 07h'dir. Bu değer, Bank 0 saklayıcılarının
(00h–07h) hemen üstüne işaret eder. Eğer başka saklayıcı banklarını (Bank 1: 08h–0Fh,
Bank 2: 10h–17h) kullanıyorsanız, SP'yi bu bölgenin üstüne, örneğin
mov SP, #2Fh ile kaydırmanız gerekir.
ACALL (Absolute CALL) komutu program hafızasında 2 Byte yer kaplar ve çağrılacak alt program, ACALL komutunun bulunduğu 2 kByte'lık blok içinde olmalıdır.
; 1. PC önce 2 artırılır (ACALL 2 baytlık komut) (PC) ← (PC) + 2 ; 2. SP bir artırılır, PCL yığına atılır (SP) ← (SP) + 1 ; Yığındaki SP adresi ← PC'nin düşük byte'ı (PC7..0) ((SP)) ← PC7..0 ; 3. SP tekrar artırılır, PCH yığına atılır (SP) ← (SP) + 1 ; Yığındaki SP adresi ← PC'nin yüksek byte'ı (PC15..8) ((SP)) ← PC15..8 ; 4. PC, alt programın başlangıç adresine yüklenir (PC) ← alt_program_adresi
ACALL 2 baytlık bir komuttur. PC bu 2 baytı "tükettiği" için ACALL koşturulduğunda PC zaten bir sonraki komutun adresini göstermektedir. Bu adres dönüş adresi olarak yığına yazılır.
LCALL (Long CALL) komutu program hafızasında 3 Byte yer kaplar. Alt program 64 kByte'lık program belleğinin herhangi bir adresinde olabilir. Çalışma mantığı ACALL ile aynıdır; tek fark PC'nin 3 artırılmasıdır.
(PC) ← (PC) + 3 ; LCALL 3 baytlık komut (SP) ← (SP) + 1 ((SP)) ← PC7..0 ; Düşük byte yığına (SP) ← (SP) + 1 ((SP)) ← PC15..8 ; Yüksek byte yığına (PC) ← alt_program_adresi
RET komutu yığından 2 byte okuyarak PC'yi geri yükler ve SP'yi 2 azaltır. Böylece program akışı ACALL/LCALL'dan sonraki komuta döner.
PC15..8 ← ((SP)) ; Yığından yüksek byte alınır (SP) ← (SP) - 1 PC7..0 ← ((SP)) ; Yığından düşük byte alınır (SP) ← (SP) - 1 ; PC artık dönüş adresini gösteriyor — program oradan devam eder
Aşağıda hem acall hem de lcall kullanan bir örnek verilmiştir. Program hafıza adresleri yanında, yığın ve PC değişimleri adım adım takip edilmektedir.
org 00h sjmp x1 x1: ; … ana program komutları … ; Adres 0203h acall alt1 ; ① PC=0203h → SP+2 → yığın: 05h,02h → PC=1A02h ; Adres 0205h (acall 2 byte → sonraki komut 0205h) nop ; alt1'den döndükten sonra buraya gelinir ; Adres 03A2h lcall alt2 ; ② PC=03A2h → SP+2 → yığın: A5h,03h → PC=2B3Ch ; Adres 03A5h (lcall 3 byte → sonraki komut 03A5h) nop sjmp x1 ; ─── ALT PROGRAM 1 ───────────────────────────────────── alt1: ; Başlangıç adresi: 1A02h nop nop ret ; Yığından 0205h okunur → PC=0205h ; ─── ALT PROGRAM 2 ───────────────────────────────────── alt2: ; Başlangıç adresi: 2B3Ch nop nop ret ; Yığından 03A5h okunur → PC=03A5h end
| Adım | Komut | PC | SP | RAM[08h] | RAM[09h] | Açıklama |
|---|---|---|---|---|---|---|
| Başlangıç | — | 0203h | 07h | xxh | xxh | Reset sonrası durum |
| ① ACALL alt1 | acall | 1A02h | 09h | 05h | 02h | Dönüş adresi 0205h yığına yazıldı (PCL=05h → 08h, PCH=02h → 09h) |
| ① RET (alt1) | ret | 0205h | 07h | 05h | 02h | Yığından 0205h okundu, SP geri 07h'e döndü |
| ② LCALL alt2 | lcall | 2B3Ch | 09h | A5h | 03h | Dönüş adresi 03A5h yığına yazıldı (PCL=A5h → 08h, PCH=03h → 09h) |
| ② RET (alt2) | ret | 03A5h | 07h | A5h | 03h | Yığından 03A5h okundu, SP geri 07h'e döndü |
Bir alt programın içinden başka bir alt program çağrılabilir. Buna iç içe (nested) alt program denir. Her çağrı yığına ek 2 byte yazar.
org 00h sjmp ana ana: acall alt1 ; Adres 0203h: SP=09h, yığın: 05h,02h ; 0205h — dönüş buraya sjmp ana alt1: ; Adres 1A02h lcall alt2 ; Adres 1B20h: SP=0Bh, yığın: 23h,1B (üst 2 byte) ; 1B23h — alt1 içindeki dönüş noktası ret ; SP→09h, PC=0205h alt2: ; Adres 2B3Ch nop ret ; SP→09h, PC=1B23h end
| Durum | SP Değeri | Yığındaki Veri |
|---|---|---|
| Reset sonrası | 07h | — |
| acall alt1 çalıştı | 09h | 08h=05h, 09h=02h (dönüş: 0205h) |
| lcall alt2 çalıştı | 0Bh | 0Ah=23h, 0Bh=1Bh (dönüş: 1B23h) |
| ret (alt2) çalıştı | 09h | Üst 2 byte temizlendi (mantıksal) |
| ret (alt1) çalıştı | 07h | Yığın boş |
Her iç içe çağrı yığına 2 byte ekler. Dahili RAM yalnızca 256 byte'tır. Gereksiz derinlikte iç içe çağrıdan kaçınılmalıdır. Yığın taşarsa program çöker.
PUSH ve POP, programcı tarafından saklayıcı değerlerini yığına geçici olarak almak/geri yüklemek için kullanılan komutlardır.
| Komut | İşlem (Sözde Kod) | Adresleme Modu |
|---|---|---|
PUSH <kaynak> |
SP ← SP+1 ; RAM[SP] ← kaynak | Doğrudan (Direct) |
POP <hedef> |
hedef ← RAM[SP] ; SP ← SP-1 | Doğrudan (Direct) |
PUSH ve POP yalnızca doğrudan (direct) adresleme modunu kullanır.
Yani saklayıcıların SFR veya RAM adresini açıkça belirtmeniz gerekir. Örneğin
push ACC yerine push 0E0h da yazılabilir
(ACC'nin SFR adresi 0E0h'dir).
Alt program çalışırken ana programın kullandığı saklayıcılar (A, B, R0…R7 vb.) bozulabilir. PUSH ile bu değerleri yığına saklayabilir, alt program bitince POP ile geri yükleyebilirsiniz.
hesap_yap: ; ─ Alt programa girerken saklayıcıları koru ─ push ACC ; Akümülatörü yığına at (SP:07→08) push B ; B saklayıcısını yığına at (SP:08→09) push PSW ; Program Status Word'ü yığına at (SP:09→0A) ; ─ Alt programın kendi işlemleri ─ mov A, #10h mov B, #05h mul AB ; A×B = 0050h (A=50h, B=00h) ; ─ Ana programın saklayıcılarını geri yükle ─ pop PSW ; POP sırasına dikkat! LIFO — Son giren ilk çıkar pop B pop ACC ret
Yığın LIFO (Last In, First Out) yapısına sahiptir.
push ACC → push B → push PSW
sırasıyla itildiyse, mutlaka
pop PSW → pop B → pop ACC
sırasıyla çekilmelidir. Sıra bozulursa yanlış değerler yüklenir ve program hatalı çalışır.
Alt program çağrısı ve dönüşü sırasında dönüş adresi yığın üzerinden hesaplanır. Bu hesaplamanın hatalı olduğu durumlarda program akışı bozulur ve program çöker. Üç temel çökme nedeni şunlardır:
Programcı, yığın alanı olarak ayrılmış RAM bölgesine doğrudan bir MOV komutuyla veri yazarsa, yığındaki dönüş adresi bozulur.
alt1: ; SP = 09h, RAM[08h]=05h, RAM[09h]=02h (dönüş: 0205h) mov 09h, #0Ah ; ❌ 09h adresine 0Ah yazıldı! ; Yüksek byte bozuldu: 02h → 0Ah ret ; PC = 0A05h olur! (0205h değil) ; Program yanlış adrese gider → ÇÖKÜŞ
Alt programda bir adet PUSH kullanılıp karşılığı POP yapılmadan RET çalışırsa, SP doğru konumda değildir. RET, yığında dönüş adresi yerine PUSH edilen veriyi okur.
alt1: ; Giriş: SP=09h, RAM[08h]=05h, RAM[09h]=02h mov 30h, #0Ah push 30h ; SP → 0Ah, RAM[0Ah] = 0Ah ; ... pop yapılmadı! ret ; SP=0Ah → PC yüksek byte = RAM[0Ah] = 0Ah ; SP=09h → PC düşük byte = RAM[09h] = 02h ; PC = 0A02h! (0205h olması gerekirdi) → ÇÖKÜŞ
Benzer şekilde, gereğinden fazla POP yapılırsa SP gereğinden çok azalır ve RET yanlış byte'ları okur.
Reset sonrası SP = 07h'dir. Bu değer, saklayıcı bankı 0'ın son byte'ını (R7) gösterir. İlk PUSH işleminde SP = 08h olur ve bu artık Banka 1'in R0 saklayıcısıdır. Eğer programda Banka 1 kullanılıyorsa, yığın bu değerleri silecektir.
org 00h ; ─── SP'yi güvenli bölgeye ayarla ─────────────────────── mov SP, #2Fh ; Yığın 30h adresinden başlasın ; (00h–2Fh arası değişkenler için ayrıldı) sjmp ana ana: ; ... program kodları ...
Her programın başında SP'yi açıkça belirleyin.
Değişkenler için 20h–2Fh arasını kullanıyorsanız,
mov SP, #2Fh ile yığını bu bölgenin hemen üstüne başlatın.
Böylece yığın ve değişken alanlarının üst üste gelmesi önlenir.
Standart 8051/8052 mimarisinde SP 8-bit genişliğinde olup yalnızca dahili 256-byte RAM'i adresleyebilir. Ancak ADUC841 mikrodenetleyicisinde, CFG841 SFR'inin 7. biti olan EXSP biti setlenerek SP 11-bit genişliğine çıkarılabilir.
CFG841 SFR'i bit adreslenebilir değildir. Bu nedenle
setb CFG841.7 gibi bit komutları kullanılamaz.
Bunun yerine orl komutuyla ilgili bit setlenir.
| Özellik | 8-bit SP (EXSP=0) | 11-bit SP (EXSP=1) |
|---|---|---|
| Kapsam | 00h–FFh (256 byte dahili RAM) | 0000h–07FFh (2048 byte) |
| SP kaydedildiği yer | SP SFR (8-bit) | SP (düşük 8-bit) + SPH SFR (yüksek 3-bit) |
| Yığın alanı | Yalnızca dahili RAM | 00h–FFh dahili RAM + 100h–7FFh XRAM |
| Kullanım amacı | Standart uygulamalar | Derin iç içe çağrılar, büyük yığın ihtiyacı |
; ─── 11-bit SP'yi aktif et ────────────────────────────── orl cfg841, #80h ; CFG841.7 (EXSP) = 1 → 11-bit SP aktif ; ─── SP'yi XRAM bölgesine taşı ───────────────────────── mov sph, #01h ; SP yüksek 3-bit = 01h ┐ mov sp, #00h ; SP düşük 8-bit = 00h ┘ SP = 0100h ; Artık yığın XRAM'ın 0100h adresinden büyüyecek. ; Dahili RAM (00h–FFh) tamamen değişken ve saklayıcı ; için serbesttir.
CFG841'in 0. biti olan XRAMEN setlendiğinde, dahili XRAM kullanıma açılır ve harici veri hafızasının alt 2 kByte'lık alanıyla örtüşür. 11-bit SP ile birlikte XRAM'ı yığın için kullanmak istiyorsanız bu durumu göz önüne alınız.
| Komut / Kavram | Ne Yapar? | Dikkat Edilecek Nokta |
|---|---|---|
ACALL |
Alt program çağrısı (2-Byte, 2kB kapsam) | Alt program aynı 2kB blokta olmalı |
LCALL |
Alt program çağrısı (3-Byte, 64kB kapsam) | Her yerde kullanılabilir; biraz daha büyük kod |
RET |
Yığından dönüş adresini okur, alt programdan çıkar | Yığın bozuksa çökme kaçınılmaz |
PUSH |
SP++ → RAM[SP] = veri | Her PUSH'a karşılık mutlaka bir POP olmalı |
POP |
veri = RAM[SP] → SP-- | LIFO sırası korunmalı |
| SP | Yığının tepesini gösteren 8-bit SFR | Reset=07h; programda açıkça ayarlanmalı |
| EXSP (CFG841.7) | SP'yi 11-bit'e genişletir (ADUC841) | Bit adreslenemez; orl ile setlenir |