7 Aralık 2025 Pazar

Arduino ve MAX7219 ile 8x8 LED Matrix Display Nasıl Kullanılır?


Merhaba. Bu yazıda MAX7219 entegresini ve bununla 8x8 LED Matrix'in nasıl sürüleceğini anlatacağım. Entegrenin, Arduino için yazılmış bir kütüphanesi olduğu için örnekleri Arduino ile yapacağım ama seri veri yolu protokolü mikrodenetleyiciden bağımsız olduğundan, başka denetleyicilere de kolayca uygulanabilir. MAX7219 da oldukça basit bir entegre, kütüphane olmadan doğrudan erişmek de kolay. Örneklerden birinin kütüphane olmadan nasıl yapılacağını da göstereceğim.


MAX7219 ve MAX7221 Entegreleri

MAX7219 ve MAX7221 entegreleri, SPI ile 7 segment display veya 8x8 LED matrix sürmek için arayüz sunan entegreler. Pin ve komut uyumlular, datasheet'leri ve yaptıkları iş hemen hemen aynıysa da MAX7221, SPI dışında başka seri protokolleri de destekliyor ve daha kararlı çalışıyor. Dolayısıyla daha pahalı.

Entegre, ondalık noktalı (decimal point) sekiz adet 7 segment display'i, bar-graph display'leri ve 8x8 LED matrixlerini destekliyor. Bunlar için farklı kod çözme (decode) seçenekleri var. 64 LED, 8 tane ortak katot üzerinden sürülebiliyor. Ben hazır kitlerden aldığım için entegre pin bağlantılarına değinmeyeceğim.

Entegrelerin güzel bir özelliği de sekiz taneye kadar kaskadlanabiliyor olmaları. Yani bu modülden sekiz tane alıp birinin DOUT bacağını diğerinin DIN bacağına zincirleme bağlayarak daha fazla sayıda LED display'i de sürebilirim. Kitin arka tarafındaki 5-pin konektör kaskadlama için. Elbette farklı display türleri de kaskadlanabiliyor.

Benzer şekilde dört tane 8x8 LED matrix display de kaskadlanmış hazır kit olarak satılıyor. Kaskadlamanın nasıl çalıştığını anlamak için bir tane de 4lü LED matrix display aldım. Bunu entegrenin yazmaçlarına değindiğim zaman ayrıntılı açıklayacağım.

Baskı devreye arkadan bakıldığı zaman kaskadlamanın nasıl ve ne kadar basitçe yapıldığı kolayca anlaşılıyor:

Modülün beş tane bacağı var. Vcc ve GND bacakları açıklama gerektirmiyor. DIN (Data In) verinin yazıldığı bacak. CS (Chip Select) bacağı active low. Bu bacak aktif olduğunda DIN'e bağlı shift register latch'ları açılıp veri okunuyor. CLK, DIN'den gelen veriye eş zamanlı saat sinyalini taşıyor.

Buradaki Türkçe'den ötürü bir parantez açma ihtiyacı hissettim. Normalde kullandığım dile özen göstermeye çalıştığımdan yukarıda cümle beni rahatsız ediyor. Diğer yandan yukarıdaki terimlerin oturmuş Türkçe karşılığını bulamıyorum. Shift register = kaydırma yazmacı, evet anlaşılabiliyor ama anlamı tam karşılamıyor gibi. Peki, Latch = Mandal(?). Türkçe Wikipedia'da Flip flop maddesinde parantez içinde "yaz-boz" verilmiş. Bazı yazarlar terimleri kendi Türkçeleştirmeye çalışıyor ama bu terimler kullanılmayıp yaygınlaşmadığında, yazılar sadece yazarının anlayabildiği anlamsız bir metinden öteye gitmiyor. O yüzden bu terimler beni rahatsız etse de olduğu gibi bırakacağım.

Yazmaçlar

Yazının başında MAX7219'un oldukça basit bir entegre olduğunu söylemiştim. Bütün işlemler toplam 14 yazmaçla yapılıyor. Bunların sekizi LED'leri kontrol eden veri yazmaçları. Yazmaçların listesini kolaylık olsun diye datasheet'ten alıp yana ekledim.

Bu adreslerin (veya başka bir bakış açısıyla komutların) hepsi 8-bit ve hepsinden sonra 8-bitlik bir veri alanı geliyor. Başka bir deyişle DIN'e (Data IN) gelen her paket 16-bit olmalı. Veri göndermek için CS' (nChipSelect) sinyalini lojik 0 yapmak ve veriyi DIN'den CLK ile senkron şekilde göndermek gerekiyor. CS'´yi tekrar lojik 1 yaparak veri gönderimi sonlandırılıyor. Datasheet'in "Serial Addressing Modes" bölümünde iletişim ayrıntılı anlatılıyor.

İlk yazmaç olan No-Op'u en son ele alacağım, çünkü No-Op aslında tam olarak No-Op işlemi yapmıyor.

Digit 0..7 yazmaçları, 8x8 LED Matrix'in bir LED satırını veya 7-segment display'in bir basamağını tutuyor. Örn. Digit 0'a 0x0F değerini göndermekle, yani veriyoluna 0x01 0x0F verisini sürmekle ilk satırın en sağdaki dört LED'in yakıp, en sol dörtlüyü söndürmüş oluyorum. Digit 1 için bu ikinci satır, Digit 2 için üçüncü satır vb.

Decode Mode (0x09) yazmacı entegrenin içindeki 7-segment decoder birimini kontrol ediyor. Buraya 0xFF değeri yazılırsa, Digit X yazmaçlarına gelen verilerin yalnızca düşük dört biti dikkate alınıp, 7-segment display için çözümleniyor, 0x00 yazılırsa herhangi bir kod çözme işlemi yapılmıyor. 8x8 LED Matrix için doğru olan mod bu.

Intensity (0x0A) yazmacı PWM ile LED'lerin parlaklığını ayarlamak için kullanılır.

Scan Limit (0x0B) yazmacı, bütün LED'ler kullanılmayacaksa (örn. ondalık noktası olmayan 7 segment display) bağlı olmayan pinleri deaktive ederek, kullanılan LED'in tarama hızını arttırmak için kullanılır. LED Matrix'te bütün LED'ler kullanılacaktır.

Shutdown (0x0C) yazmacına 0x0 değeri yazılırsa entegre kendini kapatır. Entegrenin tarama osilatörü kapanır ve LED'ler söner. Çekilen akım 150µA'e düşer. Normal çalışmada çekilen akım mA mertebesinde olup tüm LED'ler yanık olduğu zaman 300 mA kadardır. Entegreye elektrik geldiği zaman shutdown modunda başlar. Dolayısıyla ilk iş, buraya 0x1 değeri yazılmalıdır.

Display test (0x0F) yazmacına 0x1 değeri yazıldığı zaman tüm LED'ler yanar. Böylelikle bozuk LED olup olmadığı kontrol edilebilir. Test sırasında Digit yazmaçlarının değerine dokunulmaz. Buraya 0x0 değeri yazıldığında entegre normal çalışma moduna döner.

No-Op işlemi (0x0) entegrelerin kaskadlanmasında kullanılır. Bu komutun, komutu alan display üzerinde etkisi yoktur. No-Op işlemini alan entegre, bu işlemden sonra gelen işlemi DOUT bacağından gönderir. Örn. dört display'in zincirleme bağlandığı durumda, dördüncü display'e erişmek için önden üç tane No-Op ve ardından asıl işlem gönderilir. Bu durumda, birinci display ilk No-Op'u aldıktan sonra DOUT bacağından önce iki No-Op ardından asıl işlemi ikinci display'e gönderir. İkinci display aynı şekilde DOUT bacağından bir No-Op ve asıl işlemi üçüncü display'e gönderir. Üçüncü display kalan No-Op'u alır ve dördüncü display'e asıl işlem böylece gönderilmiş olur. Aşağıda bu işleme dair bir çizim var.



Datasheet'teki blok diyagramda bu adda bir yazmaç gösterilmiyor, fakat onuncu sayfada "No-Op Register" başlığı altında anlatıldığı için bu bir işlem mi yoksa bir yazmaç mı ben anlayamadım. Bu arada yukarıda görüldüğü üzere, No-Op'un anlamsız bile olsa, kendisini takip eden 8-bitlik veriyle birlikte gönderilmesi gerekmektedir.


Arduino LedControl Kütüphanesi

Aslında yazmaçları bu kadar ayrıntılı açıkladıktan sonra kütüphane kullanmak mantıksız gelebilir, ama LedControl kütüphanesi bazı işleri gerçekten kolaylaştırıyor. Örn. Digit yazmaçları LED'lere satır satır erişim sağlıyorken, kütüphanede setRow() yanısıra setColumn(), setLed() ve setChar() gibi fonksiyonlar var.

Kütüphaneyi yüklemek için Arduino IDE'de Tools -> Manage Libraries'i seçip oradan LedControl aratmak gerekiyor. Sonrasında kodlarda LedControl.h header dosyasını ekleyip bir LedControl nesnesi oluşturuluyor. Bunu oluştururken constructor fonksiyona hangi bacağın Arduino'nun hangi bacağına bağlı olduğunu ve kaç tane cihazın kaskadlandığı giriliyor. Örn.

#include "LedControl.h"
LedControl lc = LedControl(data = 12, clk = 11, chipSel = 10, 4);

Ben bütün örneklerde DIN'i Arduino'nun 12. bacağına, CLK'yı 11 ve CS'i 10. bacağına bağladım. Görseli aşağıda:

Fritzing'deki LED Matrix display görseli altı pinli. En üstte boşta duran pin, ikinci Vcc, dolayısıyla bağlı olmamasının bir önemi yok.


Kod Örnekleri

İlk örnekte basit bir karakter kaydırma işlemi var. Burada karakterler modül modül kaydırılıyor. Herhangi bir bit operasyonu yok. Kodu github hesabıma yükledim. MAX7219 entegresinin başlangıçta shutdown modunda açıldığını yazmıştım. setup() rutininin 61. satırındaki döngüde her display tek tek shutdown modundan çıkarılıyor, LED'ler en düşük parlaklığa ayarlanıyor. "dizi" dizisi display'de gösterilecek karakterlerin sıralarını tutuyor. En sonda döngüsellik için son 3 karakter tekrar ediyor.

table dizisi karakterlerin bitmap'lerini içeriyor. Bu bitmap'leri şuradan topluca indirip BIOS.F08 fontunu kullandım. Bu dosya klasik 8x8 BIOS fontunu içeriyor. Bunu GIMP'te açıp C source code veya C header olarak export ettim. Elbette tüm karakter tablosu 2 KB olduğu için (256 * 8), Arduino UNO'nun 2KB'lık RAM'ine hepsini aktarmak olanaksız, zaten gerek de yok. Yalnız kullanılacak karakterler yeterli. loop() rutinindeki for döngüsünde değerler setRow() fonksiyonuyla satır satır yazılıyor. Karakterler 20 tane oldukları halde, pointer'in değeri 16 olduğunda ekranda 16, 17, 18 ve 19. karakterler görünecekleri için, pointer'in 20-4'ü aşmaması gerekiyor.


İkinci örnek (counter) ilkinden daha da basit bir sayaç. Arduino, "a" dizisindeki değerleri sıfırıncı elemandan başlayarak işlemci saat hızında arttırıyor. 25. satırdaki for döngüsünde dizi elemanları byte büyüklüğünde bir taşmaya (overflow) karşı kontrol ediliyor. Eğer taşma varsa bu dizi elemanı sıfırlanıp, elde bir sonraki byte'a aktarılıyor (satır 29), ve bu ikili sayaç 32. satırda LED'lerle görselleştiriliyor.

Sıfırdan 256'ya bir sayma işlemi bir saniyenin altında tamamlanıyor. Onuncu bite karşılık gelen LED yaklaşık saniyede bir yanıp sönüyor. Dolayısıyla daha küçük bitleri görmezden gelirsek, geri kalan LED'lerin yanması için kabaca 2(64-10)=254 saniye gerekiyor, ki bu da 5.709 * 108 (571 milyon) yıl ediyor.


Üçüncü örnek, ikincinin aynısı ama bunu kütüphane kullanmadan yazdım. Bu kodda, önce pinler OUTPUT olarak ayarlanıyor. Ardından sırayla entegre shutdown modundan çıkarılıp (satır 32), tarama limiti 7'ye ayarlanıyor (satır 37), decoding devreden çıkarılıyor (satır 43) çünkü 8x8 LED display bağlı. 51. satırda parlaklık en düşüğe ayarlanıyor ve tüm LED'ler sıfırlanıyor (satır 60). loop() prosedüründeki mantık aynı. Tek fark, her dizi elemanı bir LED satırına karşılık gelecek şekilde Digit yazmaçlarına yazılıyor olması (satır 77).

Görüleceği gibi entegre kütüphane kullanmadan da kolayca programlanıyor. Burada amaç kütüphaneli veya kütüphanesiz hız testi yapmak değil. Zaten muhtemelen yakın sonuçlar alınacaktır. Hıza odaklanacak olsaydım zaten Assembly kullanıyor olurdum.


Dördüncü ve son örnekte yazıyı bit kaydırma işlemleri kullanarak yumuşak geçişlerle kaydırdım. Bunun için iki tane uzun kelime alıp bunu karakter dizisi haline getirdim (satır 16 veya 19). İlk örnekteki gibi bir karakter tablosu oluşturdum, elbette bu sefer daha fazla karakterle. setup() rutininde her zamanki gibi entegre başlatılıyor ve karakterler bitmap olarak "kayanyazi" dizisine aktarılıyor.

loop()'ta "kayanyazi" dizisinin ilk 4 karakteri display'e gönderiliyor. Yazıyı sola kaydıracağımdan, her satırın en soldaki bit'inin (MSB) carry_old değişkenine atıyorum (yani carry_old sıfırıncı sütunu içeriyor). Sonra tüm karakterler bir bit kaydırılıyor (satır 108) ama önce her karakterin ilk sütunu carry_new'e aktarılıyor (satır 106) ki, bu taşacak bitler sonraki karakterin en düşük anlamlı (LSB) bitine aktarılabilsin.

6 Kasım 2025 Perşembe

VGA Metin Kipinde Yumuşak Kaydırma İşlemi #2


Merhaba. Bu yazıda VGA konusuna devam edeceğim, ve göstermek istediğim, 1994 yılından bir örneğim var. Bu koda biraz geniş yer ayırmak için, önceki yazıya sıkıştırmak istemedim.

Söz konusu kodu github repo'ma yükledim. Bunu 90ların sonunda indirmiş olmalıyım ki zaten açıklama satırına göre 1994'te yazılmış bir Basic kodu (William Yu bu yazıyı okuyorsan bana ulaş). Bu kodda dikkat çekmek istediğim iki nokta var. İlki, 12. satırda dokuzuncu CRT denetleyici yazmacına erişen kod parçası:

OUT &H3D4, 9
OUT &H3D5, 1

Bu yazmaç aşağıdaki bitlerden oluşur [1]:

Maximum Scan Line Register (Index 09h)
76543210
SDLC9SVB9Maximum Scan Line

ve kodun değiştirdiği "Maximum Scan Line" alanı, grafik modda pikselleri dikey eksende bu değerin bir fazlası kadar genişletir. Yani yazılan 1 değeriyle, her piksel hemen altındaki diğer piksel de set edilmiş gibi iki kat büyük görünür. Bu alana 9 yazılsaydı pikseller 10 kat genişlikte olurdu. Ekran genişliği sabit olduğundan -bizim örneğimizde 640x480 piksel, mode 12h (satır 11)- pikselleri iki kat geniş yapmak demek, görünür ekranı küçültmek anlamına gelir. Yani bu örnekte ekranda 640x240 piksel çizilebilir. 10 kat genişletmiş olsaydık 640x48 piksel çözünürlük elde edecektik. Elbette VGA bellek büyüklüğü değişmediği için 240'dan büyük pikseller ekranda görünmez. Ancak bu alana tekrar 0 yazıldığında görünürler. Standart metin modunda bu alanda 15 değeri bulunur. Önceki yazıda da belirttiğim gibi bu bir karakterin piksel cinsinden yüksekliğidir. Bu alana daha büyük değerler yazılırsa satırların arası açılır, çok küçük değerler yazılırsa ekran okunmaz duruma gelir.

Bu koddaki hesaplar 640x240'a göre yapıldığından (örn. satır 30 ve 35) bu adım gerekli. Pikseller iki katı büyüdüğü için ekrandaki yazılar da iki kat büyüklükteler. 13 ve 20. satırlar arasındaki döngüde ekrana yıldızlar basılıyor, 22 ve 27. satırlar arasında gezegen çiziliyor. 35. satıra kadar olan bölümde bir üçgen (uzay aracı) çizdirilip, 43. satıra kadar olan bölümde bu üçgen ekranda blok olarak (GET / PUT) hareket ettiriliyor. Geri kalan grafik efektleri çok önemli da değil. Önemli olan kısım EarthQuake altprogramı (87 ile 94. satırlar). Burada sekizinci yazmaca sırayla değerler yazılıyor.

Delay = 5500       ' Increase this or decrease for earthquake delay

FOR X = 1 TO Delay
  OUT &H3D4, 8: OUT &H3D5, X
NEXT X

Sekizinci yazmacın olayı şu [1]:

Preset Row Scan Register (Index 08h)
76543210

Byte PanningPreset Row Scan

Benim gözlemime göre, burada "Preset Row Scan" alanının grafik modda hiç bir etkisi yok, veya DosBOX düzgün emule edemiyor. Bu alan, metin ekranda görüntünün orjinini piksel hassasiyetiyle kaydırmayı sağlıyor ve metin ekran için sorunsuz çalışıyor. Başka bir deyişle CRT denetleyici, görüntüyü bu alandaki değer kadar piksel yukarı kaydırıyor. Bu benim de yumuşak kaydırma için kullandığım yazmaç. "Byte Panning" alanıysa, görüntüyü bir karakter sola kaydırıyor. Dolayısıyla ekran, bu alandaki değere bağlı olarak 1, 2 veya 3 karakter genişliğinde kaydırılabiliyor. Mode 12h için bu, karakter başına 8 piksel (640 piksel / 80 karakter).

Bu yazmaç dışında dökümanda sözü edilen, ve benim de değinmek istediğim bir yazmaç çifti daha var. Bunlar Start Address Register Low ve High.

Start Address Low Register (Index 0Dh)
76543210
Start Address Low


Start Address High Register (Index 0Ch)
76543210
Start Address High

Bunların herhangi bir bit alanı yok. Normalde ekranın orjini olan sol üst köşenin VGA ekran modlarında (metin / grafik fark etmeksizin) adresi 0'dır. Bazen ekranın tam ortasının orjin alınması daha avantajlı olabilir. Mode 13h'da (320 x 200) ekranın tam ortasını orjin yapmak demek aslında görüntüyü 160 x 100 pikselinden çizdirmeye başlamak demektir. Bu pikselin doğrusal adresi 320 * 100 + 160 = 32160 = 7DA0h bulunur. Buradan;

OUT &H3D4, &HD: OUT &H3D5, &HA0
OUT &H3D4, &HC: OUT &H3D5, &H7D

kod parçasıyla orjin ortaya çekilir. Bundan sonra ekran belleğinin sıfırıncı offsetine yazılan piksel, ekranın tam ortasında görüntülenir. Qbasic'teki WINDOW komutunun da yaptığı kabaca budur. Elbette QBasic verilen negatif koordinatları kendi dönüştürür, daha düşük seviye dillerde bu işlem programcıya bırakılmıştır.

Bu yazmaca değerler birer birer arttırılarak yazılırsa ekranda sola kaydırma efekti elde edilir. Elbette kaydırılan aslında karakterler değil orjindir. Benzer şekilde, değerler yazmaca ekran genişliği kadar arttırılarak (metin ekranda satır başına düşen karakter sayısı, grafik ekranda satır başına düşen piksel kadar) yazılırsa yukarı kaydırma efekti elde edilir. Burada, karakterler bellekte bir yerden bir yere taşınmaz, işlemci sadece portlara değerleri yazmakla meşguldur. VGA'da tüm ekranı kaydırmanın en optimal yoludur. Ancak aşağı kaydırmada, en alttaki satırın en üste taşınması veya yukarı kaydırmada en üstteki satırın en alta taşınması gibi durumlarda bellek bloklarının taşınması kaçınılmazdır.


VGA Metin Modun Yapısı

VGA metin modu oldukça basittir. Ben 80x25 metin modundan bahsedeceğim. 40x25 metin modunun yapısı çok benzer olsa da bazı adres değerlerinin bu mod için tekrar hesaplanması gerekir. Monokrom mod video belleği 0xB000 segmentinden 0xB7FF segmentine ve renkli mod 0xB800 segmentinden 0xBFFF segmentine kadar olmak üzere 32 KB'tır. Her karakter için 1 word ayrılmıştır. Bu word'un düşük anlamlı byte'ı karakterin ASCII kodu, yüksek anlamlı byte'ı karakterin ve arkaplanının renk kodlarından oluşur [3]. Renk byte'ının düşük anlamlı 4 biti karakterin rengidir. Standart VGA renkleri 0: siyah, 1: mavi, 2: yeşil, 3: cyan, 4: kırmızı, 5: mor, 6: kahverengi / sarı, 7: gri olmak üzere 8 tanedir. Bunlara 8 eklenmesiyle aynı renklerin açıkları (parlakları - high intensity) elde edilir. 4, 5 ve 6. bitler arkaplan rengidir. En yüksek anlamlı bit (yedinci), karakterin ekranda yanıp sönmesini sağlar. Aşağıda bunlara ilişkin bir örnek var. 'u' ve 'g' karakterleri normalde yanıp sönüyorlar.

VGA metin modu 8 sayfadan oluşur. Her ekranın 80 x 25 = 2000 karakterden oluştuğunu ve her karakter için bir word ayrıldığını söylemiştim. Bu durumda görünen ekran 4000 byte (0x0FA0) uzunluktadır. Video belleği 32 KB olduğundan, 8 sayfaya bölünebilir. İlk sayfa 0xB800:0 adresinden başlar, sonraki 0xB800:0x0FA0, sonraki 0xB800:0x1F40 vb. Her bir ekran modunun kaç sayfadan oluştuğu şu tabloda görülebilir ve Int 10h/AH=05h ile sayfalar arasında geçiş yapılabilir.

Bu kadar ön bilgiden sonra artık kendi yumuşak kaydırma koduna gelebilirim.


SCROLL.C: VGA Metin Kipinde Yumuşak Kaydırma Efekti

Öncelikle bu kodda kaydırma için neden Start Address yazmacını kullanmadım? En kısa yanıt: Kullanabilirdim. Sıfırıncı sayfayı birinci ve ikinciye kopyalayıp, görünen sayfayı da birinci sayfa olarak ayarlarsam, aynı sayfanın bir üstte bir de altta birer kopyasını elde ederim. Sonrasında "Start Address Register"ini arttırarak veya azaltarak kaydırma efektini elde edebilirdim. Bu şimdilik başka bir yazıya kalsın.

Benim yazdığım kod, diğer sayfalarla en az şekilde (yalnızca 1 satır) etkileşime giriyor, yukarıdaki metoda göre farkı bu. Kodu github hesabıma yükledim.

Kodu Turbo C v3.0'da yazdım. Bu haliyle sorunsuz şekilde derlenip çalıştırılabiliyor. Önceki yazıda değindim, herşey DosBOX'ta yapıldı. Turbo C'de true ve false, built-in veri tipi olarak bulunmadığından, bunları yedinci ve sekizinci satırlardaki gibi tanımlıyorum. Onaltıncı ve onyedinci satırlarda iki tane gösterici tanımladım. İlki VGA metin ekran göstericisi [4]. İkincisi, ekran belleğinin lokal kopyasını tutan bir gösterici (pointer) ama adı her ne kadar DoubleBuff olsa da tam anlamıyla double buffering [2] yapmıyor. Bu konuya ileriki bir yazıda değineceğim. Bu bir short pointer ve 23. satırda 80x25 word uzunlukta bellek ayrılıyor. Neden word? Bunun iki nedeni var. Birincisi veriyi byte byte işlemektense word word işlemek daha hızlı. Karakterler renk koduyla birlikte tek seferde işlenebiliyor. İkinci nedense, kodun okunabilirliği.

Kodda yukarı ve aşağı ok tuşlarının kodu alınıyor ve bunun sonucuna göre fp fonksiyon göstericisine (function pointer) ScrollUp() veya ScrollDown() fonksiyonları atanıyor. Her iki fonksiyon da ekranı yalnız bir satır kaydırıyor. Bunların for döngüsünde 25 kere çağırılmasıyla tam ekran kaydırma elde ediliyor.


ScrollUp

Row scan alanı arttırılarak ekran yukarı kaydırılırken, birinci sayfanın normalde görünmeyen ilk satırı kısmen görünür hale gelir. Bundan ötürü kodun 60. satırında ekranın en üst satırını sonraki sayfanın en üst satırına kopyalıyorum. Ardından, ekranın ilk satırı en altta ve sonraki satırlar ara belleğin ilk satırından başlamak üzere (yani N+1. satır ara belleğin N. satırında) DoubleBuff dizisine kopyalıyorum.

Sonrasında row scan alanının ilk değerini alıp, bu değeri 0'dan 15'e kadar arttırıyorum (satır 75). Böylelikle ekran piksel piksel yukarı kayıyor görünüyor. Bu sırada, altmışıncı satırda en üstten kopyalanan, ve başta görünmeyen satır görünür olmaya başlıyor. Sekseninci satırda inline assembly ile DoubleBuff'ta dizdiğim ekranı görünür ekrana kopyalıyorum. Burada assembly'i hız için kullandım çünkü aynı işi yapan C kodu daha yavaş çalışıp ekranda titremeye (flicker) neden oluyordu.

En sonda row scan alanına tekrar eski değerini yazıyorum. Burada aslında eski değer değil doğrudan sıfır yazılması daha doğru olurdu.


ScrollDown

ScrollDown()'da ScrollUp()'takinden farklı bir yaklaşım kullandım. Row scan ekranı yukarı kaydırdığından, aşağı kaydırma efekti için öncelikle ekrandan kaybolacak olan en alt satırı lokal diziye alıp (satır 108), ekran belleğini kaydırdıktan sonra en üst satıra yazıp, row scan'e en büyük değeri yazıyorum (satır 113).

Ekran belleğini, hız için ScrollUp()'taki gibi assembly koduyla kaydırdım. SI'de 4000 ve DI'de 4160 değeri var. Yani SI birinci sayfanın ilk karakterini, DI birinci sayfada ikinci satırın ilk karakterini gösteriyor. CX'te sayaç olarak 2000 değeri varken (136. satırda 4000 shr ile ikiye bölünüyor) kopyalayınca, işlem birinci sayfanın ilk karakterinden başladığı için sonda bir karakter kopyalanmıyordu. SI ve DI'yi bir word azaltmak için 4 byte kod gerekiyor (satır 137 .. 140, dec komutları), ben onun yerine CX'i arttırıp (satır 141) bir karakter fazla kopyalıyorum, ama 4 yerine 1 byte'lık komutla hallediyorum. 132. satırdaki değeri başta azaltabilirdim ama onu azalttığımda (yanılmıyorsam) sayacı azalttığım için CX'i sonradan arttırmam gerekecekti. Son olarak direction flag'i set edip geri geri (pointer değeri azalacak şekilde) kopyalıyorum. Ekranı aşağı yönde kaydırdığım için, göstericiyi arttırarak kopyalarsam daha sonradan ihtiyacım olacak verinin üzerine yazmış olurum. Kopyalama (rep movsw, satır 148) bittikten sonra direction flag'i resetleyip eski durumuna getiriyorum.

Bundan hemen sonra en üst satır boş kaldığı için, fonksiyonun başında TempLine'a kopyaladığım satırı geri yazıyorum (156. satır. bunun neden assembly ile yapmamışım acaba?) ama row scan alanına en büyük değeri verdiğim için bu satırın yalnız bir pikseli görünüyor. Yüzaltmışıncı satırda row scan alanını yavaş yavaş azaltarak kaydırma işlemini tamamlıyorum.


waitlinefull ve waitlinehalf

Tüplü monitörler, görüntüyü ekranı tarayarak oluştururlar. Elektron tabancası elektronları normalde ekranın tam ortasına gönderir. Ekranda birşey göstermek için bu elektron demeti dikey ve yatay saptırma bobinleri tarafından saptırılır (renkli ekranlar için). Tarama ekranın en üst solundan başlar, üst sağ köşesine kadar gider. Bu sırada dikey bobinde gerilim sabit olup, yatay bobine testere dişi dalga uygulanır. Bu şekilde ekranın ilk piksel satırı çizdirilmiş olur. Sonra dikey bobindeki gerilim arttırılır ve işlem her satır için tekrarlanır. Ta ki tarama ekranın sağ alt köşesine ulaşıncaya kadar. Elbette bu sırada elektron tabancaları pikselin olduğu yerlere elektron göndererek anlamlı bir görüntü oluşturur. Eğer sürekli açık kalırlarsa tek renkli bir ekran elde edilir.

Saptırma Bobinleri (Kaynak: Wikipedia)

Bu tarama işlemine terminolojide "vertical retrace" (VR) denir ve bu işlem sürerken video belleği değiştirilirse görüntü titriyor (flicker) gibi görünür. Bu işlemin durumunu kontrol etmek için imdada VGA'nın 0x3DA kontrol yazmacı yetişir. Bu yazmacın üçüncü biti VR sırasında set edilir. Programcı ekrana birşey yazmadan bu biti kontrol edebilir (ve etmelidir). waitlinefull() ve waitlinehalf() fonksiyonlarında bu kontrol yapılıyor.

waitlinefull()'da ilk satırda (satır 173), eğer VR yoksa döngüde bekliyor. Çünkü VR o an aktif olmasa bile, video belleğindeki işlemler bitmeden başlayabilir ve yine görüntü titreşebilir. Eğer o anda VR zaten varsa bu satırın etkisi yoktur. Program sonraki satırdan devam eder. Sonraki satırda VR işlemi tamamlanana kadar döngüde kalınır.

waitlinehalf()'te yalnızca o an VR işlemi varsa döngüde kalınır, bir sonraki VR beklenmez. waitlinefull()'de bir sonraki VR başlayana dek beklemek, hızlı bilgisayarlarda çok sayıda CPU cycle beklemeye yol açar. Bu nedenle waitlinehalf()'te bu adım atılmıştır. waitlinehalf(), çok daha kısa süre bekler ama waitlinefull()'e kıyasla bazen titremeyi engelleyemeyebilir.

Ben iki fonksiyonu da koda koydum ve efekti tamamladıktan sonra ikisini de düzgün bir efekt elde edene kadar çeşitli satırlarda denedim. waitlinehalf() çok kısa sürede tamamlandığı için tüm beklemeleri waitlinefull() ile yaptım. İlk bölümün başında da belirttiğim gibi, DosBOX bir VGA emulasyonu olduğundan bu beklemelerin gerçek donanımda tekrar ayarlanması gerekecektir. Aslında waitlinefull()'u ben yalnızca kodda makinadan makinaya değişmeyen stabil bir bekleme süresi verdiği için tercih ettim.


Yazının başında da belirttiğim gibi ben burada VGA konusunun yalnız üzerini kazıdım. Smooth scroll efekti istediğim gibi oldu ama bu VGA ile yapılacakların küçük bir örneğiydi. Bu yazmaçlarla oynayarak çok çeşitli efektler oluşturmak mümkün. Sonraki yazılarda zaman bulabilirsem bir kaç efekte daha değinmek istiyorum. Son olarak aşağıda efektin videosu var.



[1]: http://www.osdever.net/FreeVGA/vga/crtcreg.htm#09
[2]: http://wiki.osdev.org/Double_Buffering
[3]: https://en.wikipedia.org/wiki/VGA_text_mode#Text_buffer
[4]: https://stackoverflow.com/questions/47588486/cannot-write-to-screen-memory-in-c

21 Ağustos 2025 Perşembe

VGA Metin Kipinde Yumuşak Kaydırma İşlemi #1


Merhaba. Bu yazıya, 80'lerin Monospace fontlarla yazılmış bilgisayar kitaplarından çıkmış gibi duran bir başlık seçtim, çünkü bu kez oldukça düşük seviye bir yaklaşımla 80'lerin teknolojisinden bahsedeceğim.

80'lerin Teknolojisi (temsili) [1]

Daha anlaşılabilir bir şekilde, VGA Text modunda smooth scroll nasıl yapılır, kesme (interrupt) kullanmadan doğrudan VGA belleği ve yazmaçları (register) nasıl kullanılır ona değindim. Ama önce VGA'ya kısa giriş yaptım ve bazı yazmaçları anlattım, arada biraz da geyik yaptım. Haliyle yazı düşündüğümden uzun oldu, ben de ikiye böldüm. Smooth scroll'un özü sonraki yazıya kaldı, çünkü kaydırmayla birlikte double buffering'den bahsetmek istiyorum. Bu da uzunca bir konu.

Uyarı: VGA yazmaçlarına, kullanılan donanıma uygun olmayan değerler yazmak donanıma kalıcı zarar verebilir. Burada verilen bilgiler gerçek bir tüplü monitörde denenmemiştir ve bunları kullanmanın riski yalnızca size aittir. Herhangi bir zarar ihtimali varsa, metnin yazarı sizi uyarmakta ve bundan sonrası için sorumluluk kabul etmemektedir.

Orjinal VGA Kart (wikipedia)
Yukarıdaki uyarıyı biraz açıklayayım. Gerçekten de VGA kartını, tüplü (CRT) monitörün desteklemediği bir frekansta çalıştırmak monitöre zarar verebilir [2]. Böyle birşeyle karşılaşmadım ama var olduğunu biliyorum. Öte yandan son CRT monitörümü yaklaşık on yıl önce çöpe attım. Ekran kartım gerçek bir VGA kart değil, yalnızca VGA uyumlu. Dolayısıyla eriştiğim yazmaçlar bile gerçek değil. Kodları DOSBox'ta geliştirip çalıştırdığım ve optimize ettiğim için, gerçek CRT'de muhtemelen timingler kayacak ve waitretrace'leri tekrar ayarlamak gerekecek (waitretrace sonraki yazıda). Bu kodlar 80386'da nasıl çalışırdı bilmiyorum.

Giriş

VGA kartına genellikle int 10h BIOS arayüzünü kullanarak erişiyoruz. Ekran modunu seçmekten, imleci hareket ettirmeye veya büyüklüğünü ayarlamaya kadar herşey bu kesmeyle yapılabiliyor. Kesmenin yaptığı da zaten VGA yazmaçlarına "doğru biçimde" erişmek. VGA BIOS, gerekirse int 10h rutinlerini karta uygun şekilde değiştirebiliyor.

Int 10h'un ciddi bir yavaşlık getirdiğini düşünmüyorum (putpixel vb. işlemler hariç), hatta kesmeyi tercih etmenin bir avantajı da kodda karmaşıklıktan kaçınmak. VGA'nın çok sayıda yazmacı var [3]*. Bunların bazısının işlevini anlamak için temel CRT bilgisi gerekiyor. Diğer yandan bu yazı için DOSBox kullandığımı belirttim. DOSBox bir çok VGA registerini doğru emüle edebilse de bazı (standart dışı) efektleri düzgün gösteremiyor. Dolayısıyla bazı DOS oyunları düzgün çalışmıyor (konfigürasyon hatalarını bir kenara). Yine de DOSBox'un hakkını teslim etmek gerek, Vmware veya VirtualBox'la karşılaştırıldığında daha uyumlu (evet elmayla armudu karşılaştırdım: biri DOS'a özel, diğerleri genel sanallaştırma).

* Adı geçen kaynakta 300+ yazmaçtan bahsediliyor ama 100 tane bile dökümante edilmemiş. Muhtemelen, farklı üreticilerin kendi ekledikleri standart dışı yazmaçlar sayılıyor. Standart yazmaçlar yaklaşık 60 tane, ama bu da az değil.


VGA Yazmaçları ve Karta Erişmek

Yukarıda da yazdığım gibi bir çok VGA yazmacı var. Şu linkte yazmaçlar altı grupta toplanıyor. Bu yazıda ben çoğunlukla CRT denetleyici (CRT controller - CRTC) yazmaçlarına erişeceğim. Gruplar kabaca erişimde kullanılan port numaralarına göre oluşturuluyor.

Bu yazmaçlara, kabaca 6 çift donanım portu üzerinden erişiliyor. Yani altmış yazmacın her birine bir port atanmamış. [3]'te portların bir listesi var. Genel olarak erişim, 0x3DX'e yazmaç index numarasını girdikten sonra, 0x3DX+1'den yazmaçtaki değeri okumak veya değere yazmak şeklinde. 0x3D0'ın bir istisnası var ama ona değinmeyeceğim. Yazının ileriki bölümlerinde bu yazmaçlarla ilgili örnek göstereceğim.

Burada tüm yazmaçlara yer verip yazıyı bir referans kitabı haline getirmek istemedim. O yüzden sadece ilgilendiğim kısma odaklanacağım. Örneğin: CRTC'ye, 0x3D4 ve 0x3D5 portları üzerinden ulaşılıyor. 0x3D4 yazmaç seçme, 0x3D5 ise veri okuma ve yazma portu [5].


Cursor Start Register (Index 0Ah)
76543210


CDCursor Scan Line Start


Şimdi [6]'daki imleci devre dışı bırakma kodunu ele alalım:

void disable_cursor()
{
    outb(0x3D4, 0x0A);
    outb(0x3D5, 0x20);
}

Önce 0x3D4 portuna 0xA yazmacına erişeceğimizi bildiriyoruz. Ardından bu yazmaca 0x3D5 portu aracılığıyla 0x20 değerini yazıyoruz. Bu değer 0xA'nın 5. bitini yani Cursor Disable bitini set ediyor [5]. Oldukça basit.

Cursor Scan Line Start, imlecin hangi pixelden başlayacağını gösteriyor. Standart 80x25 karakter ekran modunda (Mod 3), her karakter ve imlecin kendisi 8x16 pixelden oluşan bir görsel. Bu görseller değiştirilerek DOS'ta font yüklenebilir. Aşağıda, DOS için bir font düzenleme programından aldığım ekran görüntüsünde, örnek bir karakter yakından görülüyor. Font tablosu VGA BIOS'ta bulunuyor (meraklısına Int 10h / 1130h) ve özel fontlar bu alana geçici olarak yazılıyor (bilgisayar yeniden başlatılana kadar). Fontlar başlı başına bir yazı konusu, daha fazla detaylandırmayacağım.

Konuya geri dönersem, edit ortamında imleç 14. pixel satırından başlayıp 15.'de biter, ama edit'te Insert'e basıldığında 0. pixel satırından başlar. İşte bu etkiyi veren birinci öğe Cursor Start Register'in düşük anlamlı beş biti, ve ikinci öğe 0xB numaralı Cursor End Register'dir, daha doğrusu bunun düşük anlamlı beş biti:


 Cursor End Register (Index 0Bh)
76543210

Cursor SkewCursor Scan Line End


Bu yazmaç imlecin alt pixel satırını tutar. Peki eğer bir karakter 16 pixel uzunluğundaysa neden 5 bit? VGA, metin modunda aslında 32 pixele kadar olan karakterleri destekler [6]. Örn. VGA font tablosu 8 KB uzunluktadır: Karakterlerin sayısı (256) * yüksekliği (32 px) * genişliği (8 px) / bit per byte (8). Bu yüzden imleç için de yazmaçta 5 bit yer ayrılmıştır, ancak dördüncü bitin hiçbir metin modunda anlamı yoktur. Cursor Skew bitleri EGA uyumluluğu için bırakılmıştır, yine VGA'da bir anlamı yoktur.

Anlaşılması kolay başka bir yazmaç çifti de Cursor Location High (0xE) ve Cursor Location Low (0xF) yazmaçlarıdır. İmlecin ekrandaki doğrusal konum bilgisini tutarlar. Bu değer, ekranın karakter çözünürlüğüne (bizim örneğimizde 80) kalanlı bölündüğünde bölüm, imlecin y-eksenindeki; ve kalan, imlecin x-eksenindeki yerini verir. Tersi ifadeyle D = Y * 80 + X. Bu yazmaçlar byte uzunlukta olduklarından D'nin yüksek anlamlı byte'ı 0xE'ye, düşük anlamlı byte'ı 0xF'e yazılır.

Cursor Location High Register (Index 0Eh)
76543210
Cursor Location High


Cursor Location Low Register (Index 0Fh)
76543210
Cursor Location Low


80'lere Geri Dönüş: QBasic

Şimdi bu iki çift yazmaçla küçük bir demo yapacağım ve bunun için ilginç bir şekilde QBasic kullanacağım. Aslında ben de pek çok kişi gibi programlamayı Basic'le öğrendim. Biraz C64 Basic, sonrasında GW-Basic (o zamanın TRT'si sağolsun, TRT4 Açıköğretim Bilgisayar derslerı) ve son olarak QBasic. Ve iddia ediyorum ki 80'lerde doğup 90'larda bilgisayarı olan herkes aşağıdaki IDE'yi en az bir kere görmüştür. Ben 90'larda .bat dosyaların yetersiz kaldığı durumlarda betiklerimi QB ile yazardım. Sonrasında QBasic'in hızı bir çok şey için yetersiz gelince, bu beni C ve Assembly öğrenmeye itmişti. -Arada kısa bir Pascal dönemim de oldu.- Bu arada QBasic bir yorumlayıcıydı (interpreter) ve .exe dosya üretememesi benim için başka bir eksiklikti. C ile yakın zamanlarda Quick Basic v4.5 ile tanışsam da, C'nin açtığı ufuk bambaşkaydı. Ayrıca o zamanlarda Quick Basic 4.5 IDE'yi bulmak da -en azından benim için- oldukça zordu.

Yalnız ne QBasic ne Quick Basic (kısaca QB) yetersiz programlar değildi. C ile yapabildiğim herşeyi QB ile de yapabiliyordum. Bulabildiğim eski kodlarıma baktığımda, fare için QB - Int33h arayüzü ve disk işlemleri için Int13h arayüzü yazmışım. Yine başkalarının yazdığı inanılmaz kodlar var. Ama yavaşlığı bir yana, QB'deki programlama mantığı da biraz "başkaydı", ve programlamanın gitmekte olduğu yerden farklıydı. Visual Basic'le de şansımı denedim ama o ara Windows'un genel olarak bana göre olmadığını fark ettim. 2000'lerin başında görsel programlamada Win32 Assembly ile uğraştım, ama Win32API bana ağır gelmişti.

Ve neredeyse on yıldır blog yazıp, bu yazılarda bir sürü dilde örnek program yazmama rağmen, tek bir QB örneği vermediğimi fark ettim. Oysa bu tür basit kod parçaları için QB bence daha kolay, çünkü ne Assembly kadar çok satıra gerek var, ne de C'deki gibi include ekle, type cast'lere dikkat et, buffer'ı kontrol et gibi konular yok. Kısacası bu kadar retrospective yeter. Bu yazıdaki örneği QB ile veriyorum:


DECLARE SUB ENABLECURSOR (CURSTART%, CUREND%)
DECLARE SUB DISABLECURSOR ()
DECLARE SUB MOVECURSOR (CURSORX%, CURSORY%)

FOR X% = 0 TO 15
  FOR Y% = X% TO 15
    CALL ENABLECURSOR(X%, Y%)
    SLEEP 1
    CALL DISABLECURSOR
    SLEEP 1
  NEXT Y%
NEXT X%

CALL ENABLECURSOR(0, 15)

FOR Y% = 0 TO 10
  FOR X% = 0 TO 10
    CALL MOVECURSOR(X%, Y%)
    SLEEP 1
  NEXT X%
NEXT Y%

SUB DISABLECURSOR
    OUT &H3D4, &HA
    OUT &H3D5, &H20
END SUB

SUB ENABLECURSOR (CURSTART%, CUREND%)
    OUT &H3D4, &HA
    CS1% = INP(&H3D5)
    OUT &H3D5, (CS1% AND &HC0) OR CURSTART%

    OUT &H3D4, &HB
    CE1% = INP(&H3D5)
    OUT &H3D5, (CE1% AND &HE0) OR CUREND%
END SUB

SUB MOVECURSOR (CURSORX%, CURSORY%)
    POSITION% = CURSORY% * 80 + CURSORX%

    OUT &H3D4, &HF
    OUT &H3D5, POSITION% AND 255
    OUT &H3D4, &HE
    OUT &H3D5, POSITION% \ 256
END SUB


Kod biraz uzun ama genel olarak [4]'teki kodları içeriyor. İlk bölümde imlecin alabileceği kombinasyonları bir for döngüsünde oluşturdum. Parametreler ENABLECURSOR alt programında ilgili yazmaçlara gönderiliyor. Bu arada döngü içerisinde bir saniyelik bekleme (SLEEP) CTRL tuşu basılı tutularak geçilebilir.

İlk for döngüsünden sonra rahat görülebilmesi için imleci büyütüp, MOVECURSOR alt programıyla ekranın 10 x 10'luk bölümünde hareket ettirdim. MOVECURSOR'de ekranın 80 karakter genişlikte olduğunu varsayıp, (X, Y) koordinatlarından imlecin doğrusal konumunu hesaplattım.

Bir sonraki yazıda başta yumuşak kaydırma için gereken yazmaçlar olmak üzere diğer VGA yazmaçlarına değineceğim ve QB ile başka örnekler vereceğim. Ancak kaydırma işlemi yüksek hız gerektirdiği için onu Assembly + C ile yazdım, ve yazının başlarında değineceğimi söylediğim waitretrace fonksiyonunu kullandım.



[1]: DEC PDP8 Family User's Guide TSS/8 (1970). Link
[2]: https://retrocomputing.stackexc....damage-my-vga-card-by-programming-it-in-assembly-throu
[3]: http://wiki.osdev.org/VGA_Hardware
[4]: http://wiki.osdev.org/Text_Mode_Cursor
[5]: http://www.osdever.net/FreeVGA/vga/crtcreg.htm
[6]: https://en.wikipedia.org/wiki/VGA_text_mode#Fonts

10 Temmuz 2025 Perşembe

Python ile Görsellere Metin Eklemek

Merhaba. Bu yazıda ihtiyaca yönelik bir problemin çözümüne değindim. Problem, bir dizindeki görsellere toplu bir şekilde yazı eklemek. Örneğin bir telif notu veya adres bilgisi gibi. Sanıyorum bu, GIMP'te bir şablon (stencil) oluşturularak çözülebilir. Peki eklenecek metin sabit olmadığı durumda? Benim sorunum görsellere sıra numarası eklemekti. Her fotoğrafın köşesine birden başlayan ardışık sayılar... Bu durumda, şablon çözüm olmuyor. Ayrıca bu GIMP'le tek tek yapılabiliyorsa bile yüze yakın görsel için pratik değil. En pratik çözüm bunun için basit bir script yazmak. Bu arada, bu numaralandırmayı dosya adıyla yapabilirdim, ama dosya adındaki zaman damgasını korumak istediğim için buna dokunmak istemedim. Üstelik bu görselleri bir web sayfasında kullanmak isteseydim, dosya adları görünmüyor olacaktı. Son olarak bu script ile istenirse dosya adlarından, görselin kenarına 90'larda fotoğraf makinalarının yaptığına benzer şekilde fotoğrafın ne zaman çekildiği yazılabilir. 

Script deyince akl(ım)a ilk gelen bash maalesef bu iş için en iyi araç değil. Bildiğim kadarıyla bash için yazılmış bir görsel kütüphanesi yok. Yapılmaz değil elbette yapılır, ama nasıl ki balyoz varken duvar yıkmak için çekiç kullanılmıyorsa, her iş için ona uygun aracı seçmek çözümün ilk adımı. Python sever okurlar kızacaktır ama bash'tan sonra ikinci en iyi script dili python'un bu iş için Python Imaging Library (PIL) adında uygun bir kütüphanesi var. Ben kendi scriptimde PIL'in Pillow adındaki fork'unu kullandım. Kütüphane pip install pillow komutuyla kolayca yükleniyor. 

Yazıyı kısa tutmak için kodu yine github hesabıma yükledim. Kısa bir script ve içinde sadece ufak numaralar var. Öncelikle her normal python script'i gibi başta import'lar var. PIL kütüphanesindeki Image modülü görsellerle ilgili fonksiyonları içeriyor. ImageDraw, görsellerle ilgili basit 2B efektler, benim kodumda görseli döndürmek için gerekli ve son olarak ImageFont font ve diğer metin efekleri için. 


Exif Verileri ve Oryantasyon

Bu projedeki birinci zorluk, görsellerin bilgisayarda, telefonda çekildikleri şekilde görüntülenememeleri. Örn. aşağıda cep telefonumla çektiğim görseli ele alalım:


Yukarıda, tarayıcıda dikey formatta görünüyor. Kendi bilgisayarımda Gwenview ile açtığımda yine dikey formatta görünüyor. Ancak aşağıdaki kodla açtığımda

>>> from PIL import Image
>>> img = Image.open("image.jpg")
>>> img.show()

görsel yatay formatta görünüyor. 

ve GIMP'le açtığımda garip bir pencere görselin "Exif orientation metadata" içerdiğini ve bunu çevirmek isteyip istemediğimi soruyor. Peki neden?

Cep telefonunu yatay* tutup fotoğraf çekerken, telefon fotoğrafa herhangi bir döndürme işlemi uygulamaz. Cep telefonu veya dijital fotoğraf makinasıyla çekilmiş fotoğrafları python ile yukarıdaki gibi açtığımda, dikey açıyla çekilmemiş olan fotoğraflar bu yüzden çekildikleri açıda ekrana basılırlar. Kamera fotoğrafın çekildiği oryantasyonu da birlikte kaydedip, görüntüleme sırasında fotoları çevirir. Yönelim (oryantasyon) bilgisi kaydedilmemiş olsaydı, fotoğrafı düzgün görmek için her seferinde bizim telefonu ilk çekildiği açıya çevirmemiz gerekirdi. 

*: Bazı kameralarda dikey pozisyon default olup, yatay pozisyonun yönelimini kaydederken, bazılarında tam tersidir. 

Dolayısıyla bir görsel dosyasının içinde sadece piksellere ait bilgiler olmadığı yukarıdaki ifadeden de anlaşılabilir. Dosyada görsele ait metadatanın saklandığı Exif adlı bir alan vardır ve bugün tüm görsel formatları ve kameralar Exif'i destekler. Wikipedia'da Exif'te saklanan verilere ait örnek bir tablo var. Buna göre başlıca alanlar, kameranın marka ve modeli, görselin yönelimi, çekildiği gün ve saat, çözünürlük vb. Örn. çekildiği gün ve saat bilgisinin fotoğrafa gömülmesi, dosya adı 20230809_143341.jpg formatında olmasa bile fotoğrafın ne zaman çekildiğinin bulunmasına ve tarihe göre sıralabilmesine olanak sağlar. Öte yandan bazı telefonlar GPS'ten aldığı koordinat verisini Exif'e gömerek fotoğrafın nerede çekildiğini açık ederler. Bazı ortamlar böylece fotoğrafın çekildiği yeri paylaşılırken otomatik etiketleyebilir. Bunlar kişisel bilgilerin gizliliği konusunda hassas olanların tüylerini diken diken edecek durumlar. Bir söylentiye göre Ukraynalılar, Rus askerlerinden online foto isteyip bundan elde ettikleri koordinatlara saldırılar düzenlediler. 

Linux'ta exiftool adında bir araçla bu bilgiler görüntülenebilir (exiftool -list <dosyaadi>), değiştirilebilir veya tamamen silinebilir (exiftool -all= <dosyaadi>). Yukarıdaki örnek görselin tüm Exif verisi silinmiş haliyle orjinali arasında 77 KB civarı bir fark var. 

Exif konusunda gereğinden uzun bir açıklama yaptıktan sonra, tekrar Exif oryantasyon bilgisine geri döneyim. Pillow kütüphanesinde Exif'te saklanan veriyi okumak için Image.getexif() fonksiyonu var [1]. Bunun çıktısını ekrana yazdırdığımda (kodun içinde 22., 23. ve 24. satırlar, comment out edilmiş) dizindeki .jpg dosyaların 'Orientation' bilgisini görüyorum. [1]'de de belirtildiği gibi 2, 7, 4 ve 5 benim görsellerde yoktu. Keza 8 de bulunmadığı için bunu kendi kodumda uygulamadım, ancak bunu uygulamak gayet kolay. 1 değeri için if yapısında else'i kullandım (satır 31), bu nedenle 8 ve diğer değerlerde görsel döndürülmüyor. 

Döndürme için Pillow'da Image.rotate() fonksiyonu var [2]. Bunu kullanarak Orientation=3 için görseli 90 derece ve Orientation=6 için görseli 270 derece çevirdim. Burada 27. satır için bir parantez açmak gerek: Eğer Exif'te yönelim alanı bulunmuyorsa veya görselde Exif bilgisi yoksa kod hata verecektir. Bu nedenle getexif fonksiyonundan dönen değeri kontrol edip herşey tamamsa döndürmek daha doğru. Bendeki verilerin hepsinde Exif bulunduğu için sorun yaşamadım, bu nedenle bu kısmı hızlı ve çabuk şekilde halletmeyi tercih ettim.

Buraya kadar görselin yönelimini düzeltmiş olduk. Ana problemin çözümünde [3]'ten yararlandım. Burada nasıl yazı ekleneceği basitçe gösterilmiş, ben yalnızca kendi problemime göre parametreleri düzenledim. Satır 35'te yazı eklenmek üzere bir ImageDraw nesnesi oluşturuluyor. Sonraki satırda ImageFont.truetype() fonksiyonuyla bir font nesnesi oluşturuluyor. [3]'te kullanılan font benim makinamda olmadığından (ve fonksiyondan dönen değeri hata için kontrol etmediğimden), ben /usr/share/fonts/ altındaki fontlardan birini seçtim. İkinci parametre font büyüklüğü (punto). Bunu deneme yanılma yoluyla buldum. Bendeki görseller görece büyüktü (8 MP) bu nedenle 128 ancak görülebilir bir yazı üretebildi. Ondan sonraki satırda sırasıyla, görselin verilen koordinatına (25, 25 - sol üst), sirano değişkenini, bir önceki satırla oluşturduğum fontla ve kırmızı renkle ekledim. Bu adımda, yukarıda bahsettiğim, dosyanın adından veya Exif etiketinden fotoğrafın ne zaman çekildiği bilgisi alınarak yazdırılabilirdi. Aşağıda örnek bir numaralandırılmış fotoğraf görülüyor:


Son olarak, 40. satırdaki yorum kaldırılarak görsel ekranda gösterilebilir ve/veya 43. satırdaki yorum kaldırılarak "_enum" sonekiyle kaydedilebilir. Ben bu scripti yazarken yüze yakın görselin olduğu bir dizinde çalıştırdığım için her seferinde yüz fotoyu göstertmek veya kaydetmek istemediğimden o satırları comment out etmiştim. 


Not: Alternatif olarak bunu OpenCV'de cv2.putText() fonksiyonuyla yapabilmek de mümkün ama bu başka bir yazıya kalsın. 

 

[1]: https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
[2]: https://note.nkmk.me/en/python-pillow-rotate/
[3]: https://www.geeksforgeeks.org/python/adding-text-on-image-using-python-pil/ 

5 Ocak 2025 Pazar

Amazon S3 Objelerinde ETag Hesplaması


Merhaba. Bu yazıda S3'e yüklenen dosyaların ETag adı verilen hash'lerinin nasıl hesaplandıklarından bahsedeceğim. ETag genel olarak, bir S3 bucket'ına yüklenen her dosya için hesaplanan MD5 hash'ten başka birşey değil. S3'teki dosyaların gerçekte bizim anladığımız anlamda dosya olmadıklarını biliyoruz. S3, "Object Storage" olarak alınıyor ve dosyalar da doğru terminolojiyle obje olarak saklanıyor. 

Belli bir dosya büyüklüğünden sonra, aws s3 cp veya aws s3 sync ile yapılan yüklemeler, (muhtemelen) daha kolay saklanabilmesi için otomatik olarak eş büyüklükte parçalara bölünür. Buna multipart objeler denir. Peki ne kadar bir büyüklük? Genel geçer bir ölçü olmasa da benim dosyalarım şu anda 8 ila 16M arası parçalar olarak saklanıyor. Yazıyı hazırlarken kullandığım kaynakta, 5G'ye kadar olan dosyaların parçalanmadıklarından bahsedilmiş [1][2], ancak benim gözlemime göre bu artık doğru değil ama bu değer çok da önemli değil.

Eğer bir dosya bu eşikten küçükse, tek parça olarak saklanır ve objenin ETag'i MD5 hash'ine eşit. Buraya kadar bir sorun yok. Eğer dosya bu eşikten büyükse multipart obje olarak saklanınca işler biraz karışıyor. Bir objenin multipart olup olmadığı ETag'ına bakarak kolayca anlaşılabilir. Normal bir MD5 hash yalnızca hexadecimal basamaklardan oluşur. Dolayısıyla tire işareti ( - ) MD5 hash'e ait değildir. Eğer S3'teki bir dosyanın ETag'ında tire işareti varsa, bu multipart bir objedir ve dosyanın kaç parçaya bölündüğü tireden sonra gelen kısımdadır. Bunların hepsine ait somut örnekleri yazının ilerleyen kısmında vereceğim. 

Multipart objelerde ETag hesaplaması şöyle işliyor: Her bir parça ayrı ayrı MD5'le hash'leniyor, çıkan hash'ler uç uca eklenip tekrar hash'leniyor. Bu ETag'ın tireden önceki kısmı. Parça sayısı basitçe tireden sonra en sonra ekleniyor [3].

Ben bilgisayarlarımın disklerini düzenli olarak Clonezilla ile yedekliyorum. Yedekleri önce harici diske alıp, bunları S3'e kopyalıyorum. Harici diskte en yeni kopya duruyor, son üç kopya S3'te. Geriye doğru (FAT32) uyumluluk nedeniyle, yedekleri 4G'lik parçalara bölüyorum (her ne kadar yedeği FAT32 ortama almasam da). Zaten ETag karşılaştırma ihtiyacı S3'teki kopyaları doğrulamak istememden çıktı.

Bu noktada aws komut satırı arabiriminin yüklü ve ayarlı olduğunu varsayıyorum. Ayarlar .aws/config dosyasından yapılıyor ancak yazıyı uzatmamak için buna değinmeyeceğim. Önce küçük dosya örneğini ele alalım:

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/Info-lshw.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 40960,
    "ETag": "\"fe78f69cb9d41a23ba23b4783e542a7b\"",
    "ContentType": "text/plain",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Önceden belirttiğim gibi, bu multipart obje değil. Haliyle MD5 hash'i yani ETag'ı basitçe bulunabilir. Aşağıda büyük dosya örneği var: 

$ aws s3api head-object --bucket mybucket --key image_backup/2024-12-01-13-img/sda5.ntfs-ptcl-img.xz.ac
{
    "AcceptRanges": "bytes",
    "LastModified": "2024-12-03T17:00:58+00:00",
    "ContentLength": 4096008192,
    "ETag": "\"360f5e8babf8cd28673eaafd32eb405f-489\"",
    "ContentType": "application/vnd.nokia.n-gage.ac+xml",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Bu 4096 MB'lık bir dosya ve ETag'dan görüleceği gibi 489 parçadan oluşuyor. Burada önemli olan parçaların büyüklüklerini bulmak. ContentLength, 489'a bölününce 8M'ye çok yakın bir değer bulunuyor. Buradan aslında dosyanın 8M'lik parçalara bölündüğünü varsayabilirim ama bir programda kullanmak için bunun kesin değerini bulmak gerek. Bunun için aynı komuta --part-number parametresini ekleyip tek bir parçayı inceleyeceğim. Dosyalar sabit büyüklükte parçalandıklarından yalnızca en son parçanın boyutu farklı, ancak her parça için ETag değeri aynı. Başka bir deyişle --part-number her parçanın ayrı ayrı MD5 hash'ini vermiyor.

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 16777216,
    "ETag": "\"aba379cb0d00f21f53da5136fc5b0366-299\"",
    "ContentType": "audio/aac",
    "ServerSideEncryption": "AES256",
    "Metadata": {},
    "PartsCount": 299
}

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 299
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 401408,
    "ETag": "\"aba379cb0d00f21f53da5136fc5b0366-299\"",
    "ContentType": "audio/aac",
    "ServerSideEncryption": "AES256",
    "Metadata": {},
    "PartsCount": 299
}

Bu arada resmi AWS dökümantasyonuna göre (Aralık 2024 itibariyle) [4] default parça büyüklüğü 8 MB ancak yukarıda görüldüğü üzere Ekim 2023'te bir dosya 16 MB'lik parçalarla yüklenmiş. Dolayısıyla bu değeri sabit kabul etmek yerine, ContentLength alanından almak daha mantıklı. Görünüşe göre Amazon'dakiler canları sıkıldıkça default'u değiştiriyorlar. Bu arada aws komutu json çıktı üretiyor. bash script'le çalışırken, çıktıyı grep yerine jq ile parse etmek daha şık sonuç veriyor:

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1 | jq -r '.ETag'
"aba379cb0d00f21f53da5136fc5b0366-299"

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1 | jq -r '.ContentLength'
16777216

Ben, aldığım yedekteki tüm dosyaları tek tek karşılaştırmak için bir script hazırladım. Biraz uzun olduğu için buradan paylaşmayacağım, repo linkiyle ulaşılabilir. Script, kullanıcıdan basitçe bucket adını ve yedeklerin olduğu dizinin adını alıyor. Ben yedekleri image_backup adında bir dizinde, <YYYY-MM-DD-HH-img> formatlı alt dizinlerde tutuyorum, bu kısım (satır 12) ihtiyaca göre değiştirilebilir. Parça sayısı birse, doğrudan md5 alınıyor (satır 26). Birden fazla parça varsa, bu parçalar dd ile bölünüyor (satır 36), hepsinin ayrı ayrı hash'leri geçici bir dosyaya yazılıyor. Parçalar bittiği zaman oluşan dosyanın tekrar hash'i alınıp dosya siliniyor (satır 41-42). Dosyanın geri kalan kısmı bash string işlemleriyle hash'ler karşılaştırılıp aynı ise OK farklı ise FAIL yazdırılıyor.


[1]: https://stackoverflow.com/questions/45421156
[2]: https://stackoverflow.com/questions/6591047
[3]: https://stackoverflow.com/questions/12186993
[4]: https://docs.aws.amazon.com/cli/latest/topic/s3-config.html#multipart-chunksize