27 Aralık 2018 Perşembe

Arduino ile I2C: Üç Küçük Örnek


Merhaba. Tek cümlede değindiğim bir yazı (Mayıs 2015) dışında Arduino'dan bahsetmediğimi farkettim. Biraz bu nedenle ama daha çok kendime dökümantasyon olması için bu yazıyı yazmaya karar verdim. Düşündüğüm asıl yazıya başlayamadığımdan zaman doldurmak için yazıyorum. Bu yazıda Arduino ile I2C veriyolunun kullanımına üç örnek vereceğim.

Arduino, I2C veriyolu için kullanışlı bir arayüz sunuyor. Önceden hem PC seri portu hem de PIC serisi mikrodenetleyicilerle I2C kullanmaya çalıştım. Bunlardan sonra, Arduino'nun inanılmaz kolay olması beni şaşırttı. I2C veriyolu iletişimde iki pin kullanıyor. Yapısı eski Akbil'lere benzer. Örneklerde kullandığım entegrelerden ikisi Akbil'in de üreticisi olan Dallas Semiconductor'e (sonradan Maxim tarafından satın alındı) ait.

Arduino UNO, I2C için A4 ve A5 pinlerini kullanır. Bunların görevi sırayla SDA (Serial Data) ve SCL (Serial Clock). I2C'de iletişim, master ve slave olarak adlandırılan iki cihaz arasında gerçekleşir. Master, saat sinyalini üretir ve cihazlara adresleriyle ulaşır. Slave, veriyolunda kendi adresini gördüğünde veri üretir veya alır. SCL, iletişim için kılavuz saat sinyalidir ve SDA verinin aktığı pindir. I2C'de protokolle ilgili (iletim başlangıcı ve bitişi gibi) ayrıntılara yazıyı uzatmamak için yer vermeyip örneklere geçeceğim.

Kaynaklar:
https://www.arduino.cc/en/Reference/Board
https://www.arduino.cc/en/Reference/Wire
https://en.wikipedia.org/wiki/I%C2%B2C


AT2432 Entegresiyle Harici EEPROM
Konuya girmeden bir ara bilgi: ATmega328P mikrodenetleyicisinin (yani Arduino UNO) 1KB dahili EEPROM'u var. Bunu Arduino'yla tanışmamdan üç yıl sonra öğrendim. Sanırım EEPROM'ların belli bir okuma/yazma ömrü olduğundan kimse dahili EEPROM'u kullanmıyor (en azından ben karşılaşmadım). EEPROM kitaplığını kullanarak buna erişmek kolay (kaynak) fakat ben harici EEPROM'u tercih ediyorum.

Harici EEPROM olarak Atmel'in AT24C32 entegresini kullandım. 8 x 4096 byte'dan 32K'lık bir EEPROM (Datasheet, Google Drive). Çipte A0, A1 ve A2 olmak üzere üç adres bacağı var. Bunun anlamı şu: Her slave'in bir I2C adresi olduğunu yukarıda yazmıştım. Bu adres 7 bittir ve bu çip için 1010XXXb'dir. Bu çipte son üç bit tasarımcıya bırakılmıştır. Üç adres pini de toprağa bağlanırsa adres 1010000b; Vcc'ye bağlanırsa 1010111b olur. Böylelikle bu çipten sekiz tanesi farklı adreslerle birlikte kullanılabilir.

AT2432 EEPROM
I2C adresleri tekil olmalıdır. Yani her çipin farklı adresi olması koşuluyla teoride 128 çip aynı veriyoluna bağlanabilir. Birden fazlasının kullanımı olanaklı olan çiplerde adres bacakları bulunur.

Çipteki WP bacağı, yazma koruması (Write Protect) içindir. Yazma korumasını kullanmadığımdan toprağa bağladım. SCL ve SDA pinlerini 4.7K'lık pull-up dirençlerle Arduino'nun sırayla A5 ve A4 bacaklarına bağladım. Şeması yanda.

Fritzing, Arduino projesi'nin neredeyse resmi çizim programı. Her kitapta veya projede Fritzing çizimleri görülebiliyor. Ben PCAD ve son zamanda Eagle kullandığım için Fritzing'de çizmeye alışkın değilim. Ama Arduino ve birçok shield'ının blok şeması Fritzing'de hazır geldiğinden Arduino çizimleri için daha iyi bir seçenek yok (veya ben Eagle için Arduino blok şeması bulamadım). Fritzing'in 'Breadboard' görünümünde parçalar breadboard'a takılır gibi çizilebiliyor. Bir çok Arduino kitabı, "daha kullanıcı dostu" görünme amacıyla breadboard görünümü çizimlerine yer veriyor. Ben -eski kafalı olduğumdan olabilir- bu çizimleri kullanışsız (kablonun geçtiği veya değdiği yerler belli değil) buluyorum. Burada yalnız devre şemalarına yer vereceğim. Yine de tekrar söylüyorum, Fritzing'i henüz öğrenme aşamasındayım.

Projenin kodu aşağıda:

/* AT24C32 I2C EEPROM Ornegi */

#define MX24C32  B1010000    // 7-bit I2C adresi
#include <Wire.h>

void setup()  {
  Serial.begin(9600);
  Wire.begin();
}

void loop()  {
  char merhaba[8] = {'M', 'e', 'r', 'h', 'a', 'b', 'a'};
  byte addr = 0;
  int X;
  char t;

  // Kod 'Merhaba' string'ini EEPROM'a yaziyor ve yalniz
  // bir kez calistirilmasi yeterli, bu nedenle comment out
  //for(int i = 0; i < 7; i++)  {
  //  Wire.beginTransmission(MX24C32);
  //  Wire.write(0x00);         // Chip'e yazma komutunu gonder
  //  Wire.write(addr++);       // Chip'e yazilacak adresi gonder
  //  Wire.write(byte(merhaba[i]));     // ilgili adrese byte'i gonder
  //  Wire.endTransmission();
  //  delay(100);               // Chip hazir olana kadar bekle.
  //}

  // Asagidaki kod chip'in 00 adresine veri bulundurmayan bos
  // yazma komutu gonderiyor. Amaci 0x00 adresine seek yapmak
  Wire.beginTransmission(MX24C32);
  Wire.write(0x00);
  Wire.write(0x00);
  Wire.endTransmission();

  delay(100);

  // chip'ten 10 byte okunacak
  for(int i = 0; i < 10; i++)  {
    Wire.requestFrom(MX24C32, 1);       // Chip'ten bir byte iste
    X = Wire.available();               // Byte geldi (mi?)
    Serial.print("X = "); Serial.println(X);
    if(X >= 1)  {
      t = Wire.read();                  // Chip byte'i bus'a surduyse oku
      Serial.print("t= "); Serial.println(t);
    }
  }

  while(1);
}

Wire.write() fonksiyonuyla çipe özgü komutları gönderiyorum. Bu çipte Wire.beginTransmission() sonrası ilk iki byte adres sonraki byte veri olmak üzere yazma işlemi yapılıyor (belge s.9: "A write operation requires two 8-bit data word addresses following the device address word and acknowledgment."). Okuma, son yazma işleminin kaldığı yerden yapılıyor, o nedenle 00H'dan okumak için adres byte'ı 00H olan ve data byte'ı olmayan bir yazma komutu göndermek gerekiyor. Genel kullanım böyle, daha ayrıntılı bilgi belgede var.

Benzeri başka bir proje:
https://playground.arduino.cc/code/I2CEEPROM


DS1621 Entegresiyle Termometre
DS1621, Dallas'ın I2C termometre entegresi. (Ara not: Dallas'ın Akbil'in aynı paketinde termometreli çipi var) Bu entegre de üç adres bacağıyla aynı veriyolunda sekiz aygıta kadar destekliyor. Tout, sıcaklık alarm bacağı. Sıcaklık programlanan eşik değerini aştığında bacak lojik 1 oluyor (datasheet, google drive). Devrenin çizimi aşağıda:

DS1621 Termometre

Bu çip için hazır bir kitaplık var ama ben kullanmadım:
https://github.com/martinhansdk/DS1621-temperature-probe-library-for-Arduino

/* DS1621 sicaklik sensorunun (I2C bus) Arduino ile kullanilmasi
 *
 * sicaklik sensorunun I2C ID'si A0 = A1 = A2 = GND ise 0x48 olacak
 */

#define DS1621 B1001000
#include <Wire.h>

void setup()   {
  Serial.begin(9600);
  Wire.begin();
  Wire.beginTransmission(DS1621);
  Wire.write(0xAC);     // Access Config komutu
  Wire.write(0x02);     // Config register'a 02 yaziliyor.
                        // Output polarity bit = 1 => Active High
  delay(10);

  Wire.beginTransmission(DS1621);
  Wire.write(0xEE);     // Start Convert: sicaklik okumaya basla
  Wire.endTransmission();       // Stop bit

}

void loop()  {
  byte SH, SL, X;
  // SH: Sicaklik yuksek anlamli byte
  // SL: Sicaklik dusuk anlamli byte


  Wire.beginTransmission(DS1621);
  Wire.write(0xAA);     // Read Temperature komutu
  Wire.endTransmission();


  Wire.requestFrom(DS1621, 2);
  // Read Temperature komutunu gonderdikten
  // sonra chip'ten sicaklik icin 2 byte iste
  X = Wire.available();
  if(X >= 2)  {                 // Chip'ten 2 byte geldiyse
    // Yuksek anlamli byte sicakligin tamsayi degeri SH
    SH = Wire.read();
    // dusuk anlamli byte SL sicakligin ondaligi 0x80 => 0.5
    SL = Wire.read();
    Serial.print(SH, HEX);
    Serial.print("    ");
    Serial.println(SL, HEX);
  }
  else
    Serial.println(X);
  //Wire.endTransmission();

  delay(500);

}

0xEE, 0xAC çip komutları. Belgenin onuncu sayfasında tüm komutlar var. Config register'a çalışma modları yazılıyor. Örn. 0x02: One Shot mode = 0, Polarity = 1. 0xEE komutuyla sıcaklık okuma çevrimi başlatılıyor ve 0xAA komutuyla sıcaklık değerleri çipten veriyoluna çekiliyor. Config register'da bir busy flag olsa da ben basitlik açısından beklemeleri delay() ile yaptım.

Bu çipin seri porttan bilgisayara bağlandığı eski bir proje var. Sonraki yazılardan birinde bunu da anlatacağım.


DS1307 Entegresiyle Gerçek Zaman Saati (RTC)
DS1307, yine Dallas firmasına ait bir saat entegresi. Bir devrede, birden fazla RTC kullanmak anlamsız olduğundan bu entegrede adres bacağı yok. Saati pille beslemek için VBAT girişi var. Arduino kapatılsa bile saat, pille çalışmaya devam ediyor. Kendime "Pili Vcc'ye bağlama" diye not almışım ama neden hatırlamıyorum. Entegre, içinde bulunan sayıcının değerleri arttırıp saniyede taşma olunca dakikayı, dakikada taşma olunca saati vb. arttırdığı 64 byte RAM olarak düşünülebilir.

DS1307 Gerçek Zaman Saati
Bu entegrenin kullanıma hazır kitleri hatta ilk anlattığım AT2432'yle birlikte kitleri var (1, 2). EEPROM'lu kitleri Türkiye'de görmedim ama EEPROM'suz olanları bilindik satıcılarda bulunuyor. Bu entegre için hazır bir kitaplık da var. Ben kendi kodumu yazdım ama iki fonksiyonu bu kitaplıktan aldım.

Entegre, 32.768 KHz kristal osilatöre gerek duyuyor. X1 ve X2 bacakları kristal osilatöre bağlı. Yedinci bacak kare dalga çıkışı, kullanmadığımdan bağlamadım. Kodun içinde gerekli açıklamalar bulunuyor:



/* DS1307 RTC entegresi icin kod. Pili Vcc ye baglama.
 * SDA, SCL'deki pull-up direncleri unutma.
 */

#include <Wire.h>
#define DS1307 B1101000

// iki fonksiyonu jeelabs'in RTC kitapligi kaynak kodundan aldim:
// https://jeelabs.org/2010/02/05/new-date-time-rtc-library/
static uint8_t bcd2bin (uint8_t val) { return val - 6 * (val >> 4); }
static uint8_t bin2bcd (uint8_t val) { return val + 6 * (val / 10); }

void setup()  {
  Serial.begin(9600);
  Wire.begin();

  // Begin initialization
  Wire.beginTransmission(DS1307);
  Wire.write(0);        // once adress byte olarak 0 gönder
  Wire.endTransmission();

  Wire.requestFrom(DS1307, 1);
  int ss = Wire.read(); // sonra bir karakter oku.
  Serial.println(ss);
  // End initialization
  // Eger chip bir karakter dondurmusse calisiyordur


  /*   //  Daha once ayarlanmamissa saati ayarla
  Wire.beginTransmission(DS1307);
  Wire.write(0);
  Wire.write(0x0);  // saniye
  Wire.write(0x21); // dakika
  Wire.write(0x0); // saat
  Wire.write(0);    // haftanin gunu
  Wire.write(0x24);    // gun
  Wire.write(0x12);    // ay
  Wire.write(0x18); // yil
  Wire.write(0x10); // config register SQ Wave out @1Hz
  Wire.endTransmission();
  // */


}

void loop()  {
  Wire.beginTransmission(DS1307);
  Wire.write(0);        // address byte olarak 0 gonderiliyor
  Wire.endTransmission();

  Wire.requestFrom(DS1307, 7);  // RTC'nin 7 byte'ini oku
  /* Sozkonusu 7 byte:
   * 00H: CH Bit + Saniye BCD (00H adresinin 7. biti Clock Halt (CH)
   *      bitidir. Bu bit osilatoru durdurur. "Please note that the
   *      initial power-on state of all registers is not defined.
   *      Therefore, it is important to enable the oscillator (CH
   *      bit = 0) during initial configuration.")
   * 01H: Dakika BCD
   * 02H: Saat BCD (Saat yazmacinin 6. biti 12/24H modunu secer. Set
   *      edilmisse 12H modu secilir. Bu modda 5. bit birse PM sifirsa
   *      AM olur. 24H modunda 4. ve 5. bitler saatin onlar basamagidir.)
   * 03H: Haftanin gunu. Bu kodda kullanilmamaktadir.
   * 04H: Ayin gunu BCD
   * 05H: Ay (BCD)
   * 06H: Yil (BCD)
   * 07H: Kontrol yazmaci
   */
  uint8_t ss = bcd2bin(Wire.read() & 0x7F);     // CH bit olmadan saniye
  uint8_t mm = bcd2bin(Wire.read());            // Dakika
  uint8_t hh = bcd2bin(Wire.read());            // Saat 24H modunda
  Wire.read();                                  // Haftanin gununu okuma
  uint8_t d = bcd2bin(Wire.read());             // Gun
  uint8_t m = bcd2bin(Wire.read());             // Ay
  uint16_t y = bcd2bin(Wire.read());            // Yil.

  Serial.print(d); Serial.print(".");
  Serial.print(m); Serial.print(".");
  Serial.print(y); Serial.print("   ");
  Serial.print(hh); Serial.print(":");
  Serial.print(mm); Serial.print(":");
  Serial.println(ss);

    delay(1000);  // Her saniye oku
}

29 Kasım 2018 Perşembe

Karakter LCD'de Özel Karakterleri Tanımlama


Merhaba. Bu yazı LCD yazısının devamı gibi ama donanımla değil yalnız yazılımla ilgili olacak. Önceki yazıda söz ettiğim sorunların çözümüne değineceğim ve LCD'de özel karakterler nasıl tanımlanıyor onu göstereceğim. Maalesef özel karakter tanımlama konusunda internette kaynak az sayıda. Bu iş örneğin Arduino gibi daha yüksek seviyeli platformlarda bir fonksiyonla yapılabiliyor.

Bu anlatımda önceki bölümdeki olay sırasına uymaya çalışacağım.

Pinlerle İlgili Sorun
İlk yaşanan sorunda, ekrana hiç karakter yazdıramamıştım. Aklıma, aslında karakterlerin ekrana yazıldığı ama göremediğim geldi, çok önce bunu yaşamıştım. LCD kontrast (3) pinini 4.7K - 2.2K değerinde bir dirençle toprağa bağlamak basit bir devre için yeterli, ben de öyle yapacaktım ama "acaba göremiyor muyum?" düşüncesiyle kontrastı ayarlamak istediğimden sabit direnci potansiyometreyle değiştirdim. Arka ışıklandırma da, LCD pin 15 Vcc ve pin 16 GND olmak üzere LED'lere bağlı. Yeşil ekranlarda ışıklandırmaya gerek olmuyor. Ben de ışıklandırmayı kullanmak istemediğimden yeşil LCD seçmiştim. 

Devreyi kolaya kaçmadan tekrar kurduğum halde sorun çözülmemişti. Kodun sonundaki outb(0, BASE) satırını outb(255, BASE) olarak değiştirip veri pinlerine gelen gerilimi kontrol ettim. Bunlarda sorun yoktu. Sonra koddaki delay parametresini voltmetrenin okuyabileceği yavaşlığa getirip (3 sn) E ve RS pinlerine baktım. E, her adımda kapanıp açıldığı halde RS'de hareket yoktu. Kullandığım pin bağlantı şemasını aşağıda var:

PP Sinyal
DB25 Pin
Centronics Pin
IC In
IC Out
LCD Sinyal (Pin)
nStrobe(C0)
1
1
IC2_17
IC2_3
E (6)
nSelect (C3)
17
36
IC2_15
IC2_5
RS (4)
Data0 (D0)
2
2
IC1_2
IC1_18
D0 (7)
Data1 (D1)
3
3
IC1_4
IC1_16
D1 (8)
Data2 (D2)
4
4
IC1_6
IC1_14
D2 (9)
Data3 (D3)
5
5
IC1_8
IC1_12
D3 (10)
Data4 (D4)
6
6
IC1_17
IC1_3
D4 (11)
Data5 (D5)
7
7
IC1_15
IC1_5
D5 (12)
Data6 (D6)
8
8
IC1_13
IC1_7
D6 (13)
Data7 (D7)
9
9
IC1_11
IC1_9
D7 (14)

Kurulum sırasında yararlandığım kaynakta [ http://www.interfacebus.com/Design_ieee1284b_Connector_PinOuts.html ] nSelect için 13. pini gösteriyor. Burayı yanlış anlamışım çünkü aslında 13, bir giriş pini. Benim normalde kullandığım kaynakta [ https://www.lammertbies.nl/comm/cable/parallel.html ] yönler de gösteriliyordu. Çıkışı 36. pinden değil 13. pinden alıyormuşum, bu pin de giriş olduğundan hep lojik 1'deymiş. Bu arada jumper kabloların (renkli olanlar) ucundaki plastiğin eni, Centronics'in port aralıklarından fazla olduğundan çift sayılı portlarda tel; tek sayılılarda jumper kablo kullandım (örnek). Önce temassızlıktan şüphelendim ama öyle olsa kod her çalıştırıldığında farklı sonuçlar üretirdi. Sinyallerin portlarla ilişkisine de ileriki paragraflarda değineceğim.

Bu arada LCD'deki DELAY parametresinde üst sınır olmamasına dair şöyle bir video var:


lcdKomut() Fonksiyonundaki Sorun
Fonksiyon ilk haliyle aşağıdaki gibiydi:

void lcdKomut(unsigned char veri)    {
    outb(veri, BASE);
    outb(8   , CTRL);    // RS = 0; E = 1
    usleep(DELAY);
    outb(9   , CTRL);    // RS = 0; E =0
    usleep(DELAY);
}

Nedense bu haliyle her komut gönderdiğimde ardından gönderdiğim veri iki kere gidiyordu. Yani Deneme123 yerine DDeneme1123 oluyordu. Bunun, paralel port denetleyicisinin pini yeterince hızlı lojik 0'a çekememesi, daha E = 0 olmadan bir sonraki C komutunun veriyi veriyoluna sürmesi olduğunu düşünsem de aynı davranışı uzun DELAY parametrelerinde bile gözlemledim. Koddaki 9'u 1'le değiştirdim ve sorun düzeldi. Bununla ilgili olarak çok sevdiğim bir görsel var:


Sorun çözüldüğü için uzun uzun araştırmak istemedim. Belki osiloskopla gidip gelen sinyalleri tekrar incelemek gerekir ama dediğim gibi uğraşmak istemedim.

Paralel Port Durum ve Kontrol Sinyalleri
Sinyallerden bahsetmişken durum ve kontrol sinyallerine de kısaca değinmem gerekir. Paralel portta üç grup sinyal bulunur: veri, durum ve kontrol. Bu gruplardaki sinyaller sırayla BASE, BASE+1 ve BASE+2 portlarıyla kontrol edilir. Veri sinyalleri basittir: BASE porta hangi byte yazılırsa, [2-9] pinlerinin değeri ona göre ayarlanır ve öyle kalır. Durum sinyalleri yazıcının durumunu okuma amaçlı olduğundan giriş için kullanılabilir. Örn. yazıcı bir kesme isteği ürettiğinde, kağıt bittiğinde veya sıkıştığında vb.

nStrobe ve nSelect, kontrol sinyal grubundadır. Kontrol grubu, bağlı yazıcıyı kontrol etmek için vardır ve "active low" sinyallerdir. Bu nedenle sinyalin başına "n" veya "~" ile gösterilir. nStrobe, bilgisayar yazıcıya veri gönderirken saat sinyali olarak kullanılır ve her veri değişiminde set etmek gerekir. nSelect, yazıcı seçildiği zaman set edilir. Kontrol yazmacı 0x378 + 2 = 0x37A portunda, sırayla nStrobe ve nSelect sinyali bu portun 0. ve 3. bitlerindedir. Dolayısıyla nStrobe'u lojik 0 yapmak için porta 1 yazmak, nSelect'i lojik 0 yapmak için porta 8 yazmak gerekir.

LCD Komutları ve Özel Karakter Tanımlama
Önceki yazıda LCD komutlarından bazılarını kullandım ama hepsine değinmedim. Aslında LCD'lerle uğraşırken 3-4 tane komut yeterli. Ben LCD'lerle çalışırken şu görseli kullanıyorum: https://goo.gl/images/QHtKee . Google görsellerinde "lcd commands" aratınca benzeri bir çok sonuç çıkıyor. En doğrusu, her zaman LCD'lerin datasheet'ini esas almak ama hızlıca birşeyler yapmak için 60 sayfa belge okumak zor. Ben drive'a bir tane datasheet yükledim. Bununla CGRAM'i programlayıp LCD'de kendi karakterlerimi oluşturacağım.

CGRAM, character generator RAM adında fontların tutulduğu bellek. DDRAM ise ekranda görünen hücrelerdeki karakterin kodunu tutan bellek. DDRAM'e bir veri yazıldığında CGRAM/CGROM look-up tablosu olarak kullanılıyor. CGRAM programlanabilir olup kullanıcıya kendi karakterlerini oluşturma olanağı veriyor. Datasheet'te, CGRAM'i adreslemek için 6 bit ayrılmış görünüyor ama standart bir LCD'de bu bitlerin hepsinin kullanıldığına emin değilim. CGRAM'in adresi sıfırdan başlıyor, bu arada.

Sayfa 24, Tablo6'da komut tablosu var. Önce komutla, kaçıncı karaktere yazacağımı bildirmem gerek. CGRAM'i adreslemek için 0b01XX XXXX komutunu vermeliyim. Buradaki X'ler adres olduğundan sıfırıncı adres için operand 0x40. s31'de CGRAM'e verinin lcdVeri() ile gönderilebileceği yazıyor. Yazılacak karakter 5 bit genişliğinde bitmap verisi olarak gönderilmeli, bu da önceki sayfalarda var. Dikkat edilmesi gereken bir konu da CGRAM'a yazılırken LCD ekran kapatılmalı.

Karakter tanımlamak için eski kodu biraz değiştirdim. Tanımlamalar, lcdVeri() ve lcdKomut() fonksiyonları aynı. Sadece main()'i aşağıya yapıştırdım:

int main(int argc, char* argv[])    {
    int i;

    if(ioperm(BASE, 3, 1))    {
        fprintf(stderr, "Access denied to %x\n", BASE);
        return 1;
    }

    lcdKomut(0x38);    // 8 bit, 2 satir, 5x7 px
    lcdKomut(0x08);    // ekrani kapat

    const unsigned char ozelkarakter[] = {
        // gulen surat gibi birsey
        0B01110, 0B10001, 0B11011, 0B10001,
        0B11011, 0B10101, 0B10001, 0B01110,
        // gulen suratin tersi
        0B10001, 0B01110, 0B00100, 0B01110,
        0B00100, 0B01010, 0B01110, 0B10001,
        // maca
        0B00100, 0B01110, 0B11111, 0B11111,
        0B10101, 0B00100, 0B01110, 0B00000,
        // sinek
        0B00000, 0B01110, 0B10101, 0B11111,
        0B10101, 0B00100, 0B01110, 0B00000,
        // kupa (kalp)
        0B00000, 0B00000, 0B01010, 0B11111,
        0B11111, 0B01110, 0B00100, 0B00000,
        // karo
        0B00000, 0B00100, 0B01110, 0B11111,
        0B11111, 0B01110, 0B00100, 0B00000
    };



    lcdKomut(0x40);   // 0. CGRAM adresi
    for (i = 0; i <= 47 ; i++)
      lcdVeri(ozelkarakter[i]);

    lcdKomut(0x01);    // ekrani sil
    //lcdKomut(0x80);    // satirbasi
    lcdKomut(0x0F);    // ekrani ac, kursor blink

    lcdVeri('D'); lcdVeri('e'); lcdVeri('n');
    lcdVeri('e'); lcdVeri('m'); lcdVeri('e');
    lcdKomut(0xC0);    // ikinci satir
    lcdVeri('1'); lcdVeri('2'); lcdVeri('3');
    lcdVeri(' '); lcdVeri(' '); lcdVeri(' ');

    // ozel karakterler
    lcdVeri(0x00); lcdVeri(0x01); lcdVeri(0x02);
    lcdVeri(0x03); lcdVeri(0x04); lcdVeri(0x05);
 
    outb(0, BASE);
    return 0;
}

Bu kodla özel karakterler tanımlandı ve ekrana basıldı:


8 Kasım 2018 Perşembe

Calculator the Game ve Çözümü (Function Pointers)


Merhaba. Bu yazı uzunca bir aradan sonra yine bir oyunla ilgili. Bir miktar yazılımdan bahsedeceğim ve Matlab kullanacağım. 

Yaz başında telefonuma "Calculator: The Game" adında bir oyun indirmiştim. Oyunun amacı bir sayıyla başlayıp verilen işlemler aracılığıyla ve sınırlı sayıda işlemle hedef sayıya ulaşmak. Oyunun videosu: https://www.youtube.com/watch?v=w5yyY341-4A Oyun, Google Play'de 5 milyondan fazla indirilmiş yani bilindik bir oyun olduğunu varsayıyorum.

Yandaki ekran görüntüsü ilk bölümden. Bu bölüm sıfırla başlıyor. Verilen işlem +1 ve amaç iki hamlede 2 elde etmek. Açık ki, başka da işlem olmadığından yapılması gereken iki kere +1'e basmak.

Bölüm 30
İlerleyen seviyelerde, dört işlem dışında işlemler ekleniyor. Örn. kaydırma '<<' işlemi 4321'i 432 yapıyor. Sayı ekleme, üzerinde sayı olan yan görüntüdeki gibi mor tuşlar. Mor 5, 432'den 4325 yapıyor. Yine yanda görülen dönüşüm tuşları sırayla sayının içindeki 1'leri 2 ( 1 => 2 ) ve 2'leri 3 yapıyor ( 2 => 3 ). Yandaki bölümün çözümü: 1, 2, 2=>3, 1=>2, 2, 1. SUM düğmesi sayının basamak değerlerinin toplamı olan sayıyı veriyor. Inv10 her basamağı 10'dan çıkarıyor. [+]1 bütün tuşların değerine 1 ekliyor. Store, aynı sayıyı birden fazla kullanmak için hafızaya alıyor. Shift, assembly'deki rotation'a karşılık geliyor. Mirror, sayının ayna görüntüsünü sayının sonuna ekliyor.

Örnekler:
4325 (SUM) -> 14 (SUM) -> 5
4325 (Inv10) -> 6785
4325 (Shift>) -> 5432 (<Shift) -> 4325
25 (Mirror) -> 2552 -> (Mirror) 25522552

Portallar, bir tuş olmayıp şunu yapıyor: Yüzler basamağından birler basamağına bir portal, işlem sonucu çıkan sayı 100'den büyükse yüzler basamağındaki sayıyı birler basamağına ekler.

Bölüm 60
Bu oyunu uzunca zaman oynadım. Oynarken bazı bölümlerin çözümü için bütün olasılıkları denemenin daha pratik olduğunu farkettim. Örneğin yukarıdaki ekran görüntüsü (Bölüm 30) ele alındığında, çözüm zor olmasa da denemek için 4 tuşa 6 kere basmak, 46 = 4096 farklı kombinasyon gerektirir. Altmışıncı bölüm daha zor görünüyor ama iki tuşa 5 kere basmanın 25 = 32 farklı kombinasyonu var. Bazı bölümleri akıl yürüterek çözmek yerine denemek daha hızlı sonuç veriyor.

Deneyerek (brute force) çözerken, bilgisayar kullanmamak düşünülemez, öyle ki 4096 çokmuş gibi görünse de bilgisayarda bir kaç saniyede denenebilir. Önemli olan soruna doğru şekilde yaklaşmak.

İşlemler kodun içinde yazılırsa, doğrusal olarak çalışan bir kod farklı sıralarla çağırılamaz. Tuşlara ait işlemlerin fonksiyon olarak tanımlanması gerekir. Tuş sayısı kadar fonksiyon bulunmalı veya başka bir deyişle her tuş fonksiyon olarak yazılmalı.

Tuşlara nasıl basılabilir? Örneğin beş tane tuş da olsa birine ardarda basmak sorunu çözüyor olabilir veya önce ilkine sonra ikincisine vs. Tuşların numaralandırıldığını düşünelim. Şimdilik tuş sıralamasının önemi olmasın. Beş tuş için beş farklı rakam kullanılsın. İşlem sayısı üçse, birinci tuşa ardarda basmak 111'e, tuşlara sırayla basmak 123'e karşılık gelir. Genellenirse, tuş sayısı kadar rakam ve hamle sayısı kadar basamak olur. Bu örnekte, tüm dizilimler 53 = 125 olduğundan burada hepsini saymaya gerek yok ama 125, 134, 225, 334, 415, 531, 555 şeklinde örneklendirilebilir. Bu arada tuşlar numaralandırılırken 5 yerine 0 kullanılsaydı problemin 5lik sayı sistemiyle olan bağlantısı fark edilebilir.

125, sırayla 1., 2. ve 5. tuşlara basmak veya 1., 2. ve 5. fonksiyonları çağırmak anlamına gelir. O halde sayının değerlerine göre fonksiyonlar belirli sıralarla nasıl çağırılmalı? Bunu yapmanın en kolay yolu fonksiyonların kendisiyle değil göstericileriyle (pointer) çalışmak. Bu biraz karışık gelebilir. Hazır karışık demişken, C'de bu aşağıdaki gibi yapılır: 

#include<stdio.h>

void fonk1(int i)    {
  fprintf(stdout, "Fonk1: %d\n", i);
}

void fonk2(int i)    {
  fprintf(stdout, "Fonk2: %d\n", i);
}

void fonk3(int i)    {
  fprintf(stdout, "Fonk3: %d\n", i);
}


int main(int argc, char* argv[])    {
  void (*fparray[3])(int i);
  int i;
 
  fparray[0] = fonk1;
  fparray[1] = fonk2;
  fparray[2] = fonk3;
 
  for(i = 0; i < 3; i++)    {
    (*fparray[i])(i);
  }
 
  return 0;
}

Yukarıda tamsayı argüman alan basit üç fonksiyon tanımlanıyor. main() içinde bu fonksiyonlar için üç elemanlı void fonksiyon pointer dizisi oluşturulup göstericiler bu diziye atanıyor. C'de fonksiyonun adı zaten o fonksiyonun göstericisi yani ilk komutunun adresi olduğu düşünülürse atamalar için herhangi bir operatöre (*, &) gerek olmadığı görülür. For döngüsünde dizinin elemanları sırayla i argümanıyla çağırılır.

Bölüm 192
C'de fonksiyon göstericileri biraz zor (en azından benim için). Hangi parantez nerede olmalı kafa karıştırıcı olabiliyor. Ben çözüm için C yerine Matlab kullandım. Matlab'de bunu kodlamak daha kolay. C'deki & operatörü benzeri, Matlab'de fonksiyon göstericiler için @ bulunuyor. Göstericiler için matris değişken yerine hücre (cell) değişken ve argümanların fonksiyona uygulanması için feval komutunu kullanmak gerekiyor. Ben kodda sayı sistemleriyle uğraşıp tek for döngüsünde halletmek yerine tuş sayısı kadar içiçe for döngüsü kullandım.

Örneğin 192. seviye zordu. Bunun için kod yazmam gerekti. Bu bölümde beş tuş ve altı hamle var yani çok sayıda dizilim var. Binler basamağından birler basamağına bir portal bulunuyor. Önce tuşlara fonksiyon yazmakla başladım. Hatırlatma: Matlab'de fonksiyonlar kendi adlarını taşıyan dosyalara kaydedilmeli. İlk fonksiyon tus1.m dosyasında, ikinci fonksiyon tus2.m dosyasında vb.

function deger = tus1(deger)
  % +8
  deger = deger + 8;
endfunction

function deger = tus2(deger)
  % *4
  deger = deger * 4;
endfunction

function deger = tus3(deger)
  % Inv10
  s_deger = int2str(deger); % Sayiyi string'e donustur
  for k1 = 1:length(s_deger)
    if (s_deger(k1) != '0')
      s_deger(k1) = int2str(10 - str2num(s_deger(k1))); % karakterleri 10dan cikar
    endif
  endfor

  deger = str2num(s_deger); % tekrar sayıya dönüştür.

endfunction

function deger = tus4(deger)
  % sonuna 9 ekle
  deger = deger * 10 + 9;
endfunction

function deger = tus5(deger)
  % 7 => 0
  if( floor(deger / 100) == 7 )
    deger = deger - 700;
  endif
 
  if( mod(floor(deger / 10), 10) == 7 )
    deger = deger - 70;
  endif
 
  if(mod(deger, 10) == 7)
    deger = deger - 7;
  endif
 
endfunction


Portalı, her fonksiyondan dönen değerin girdiği ayrı bir fonksiyon olarak düşündüm. Fonksiyonun sonucu portal fonksiyonuna giriyor ve düzenlenmiş olarak çıkıyor. Portal fonksiyonuna "girdicikti" adını verdim:

function deger = girdicikti(deger)
  if(deger > 999)
    binler = floor(deger / 1000);
    deger = mod(deger, 1000) + binler;
    if(deger > 999)
      girdicikti(deger)
    endif
  endif
endfunction

Ana fonksiyon aşağıdaki gibi:

ilkdeger = 189;

% Function array
f_a = { @tus1, @tus2, @tus3, @tus4, @tus5 };

for k1 = 1:5
  for k2 = 1:5
    for k3 = 1:5
      for k4 = 1:5
        for k5 = 1:5
          %for k6 = 1:5
           
            adim1 = girdicikti( feval (f_a{k1}, ilkdeger) );
            adim2 = girdicikti( feval (f_a{k2}, adim1) );
            adim3 = girdicikti( feval (f_a{k3}, adim2) );
            adim4 = girdicikti( feval (f_a{k4}, adim3) );
            adim5 = girdicikti( feval (f_a{k5}, adim4) );

            if(adim5 == 500)
              printf("%d %d %d %d %d\n", k1, k2, k3, k4, k5);
              break
            endif
          %endfor
        endfor
      endfor
    endfor
  endfor
endfor

ilkdeger değişkeni bir fonksiyona giriyor. Bir tuşa basılması (fonksiyon) sonucu adim1 değişkeni oluşuyor. Sonra adim1'den ikinci fonksiyonla adim2 oluşuyor... İlginç bir şekilde bu bölüm iki farklı şekilde ve 6 yerine 5 işlemle çözüldü. Benim bilgisayarımda çözüm ortalama 15.96 sn sürdü. Program iki farklı çıktı üretiyor:

Bölüm 199
2 5 4 1 5: 189 (x4) -> 756 (7 => 0) -> 056 (9) -> 569 (+8) -> 577 (7 => 0) -> 500
4 2 1 4 2: 189 (9) -> 900 (x4) -> 603 (+8) -> 611 (9) -> 125 (x4) -> 500

199. seviye için de bir kod yazdım. Bu bölümde 46 = 4096 kombinasyon var. tus3 bir öncekiyle aynı olduğundan yazmadım.


function deger = tus1(deger)
  % sonuna 7 ekleme
  deger = deger * 10 + 7;
endfunction

function deger = tus2(deger)
  % 3 => 5
  deger = str2num(strrep(num2str(deger), '3', '5'));
endfunction

function deger = tus4(deger)
  % shift >          3002 => 2300
  if(deger > 999)
    deger = mod(deger, 10) * 1000  +  floor(deger / 10);
  elseif(deger > 99)
    deger = mod(deger, 10) * 100  +  floor(deger / 10);
  elseif(deger > 9)
    deger = mod(deger, 10) * 10  +  floor(deger / 10);
  endif
endfunction

Onbinler basamağından birler basamağına portal:

function deger = girdicikti(deger)
  if(deger > 9999)
    onbinler = floor(deger / 10000);
    % onbinler basamagini al
    deger = mod(deger, 1000) + onbinler;
    % birler basamagina ekle
    if(deger > 9999)
      girdicikti(deger)
    endif
  endif
endfunction

Ana fonksiyonda bir öncekinden farklı olarak tuş sayısını değişkene atayıp for'ları bu değişkene kadar çalıştırdım:

clc
clear

ilkdeger = 3002;

f_a = { @tus1, @tus2, @tus3, @tus4 };
lfa = length(f_a);

for k1 = 1:lfa
  for k2 = 1:lfa
    for k3 = 1:lfa
      for k4 = 1:lfa
        for k5 = 1:lfa
          for k6 = 1:lfa
            adim1 = girdicikti( feval (f_a{k1}, ilkdeger) );
            adim2 = girdicikti( feval (f_a{k2}, adim1) );
            adim3 = girdicikti( feval (f_a{k3}, adim2) );
            adim4 = girdicikti( feval (f_a{k4}, adim3) );
            adim5 = girdicikti( feval (f_a{k5}, adim4) );
            adim6 = girdicikti( feval (f_a{k6}, adim5) );
            if(adim6 == 3507)
              printf("%d %d %d %d %d %d\n", k1, k2, k3, k4, k5, k6);
              return
            endif
          endfor
        endfor
      endfor
    endfor
  endfor
endfor


Bu bölüm altı adımda çözüldü. Çözüm "1 1 2 3 4 1" ve ortalama 0.68 sn'de çözülüyor:

3002 (7) -> 30 (7) -> 307 (3 => 5) -> 507 (Inv10) -> 503 (Shift>) -> 350 (7) -> 3507

Bu bölümle birlikte oyun bitiyor ve oyun sonu videosu başlıyor. Bu yazıda anlattığım yaklaşım oyunun bir çok bölümüne uygulanabilir ama Store ve daha önemlisi [+]2 gibi tuşlara uygulamak zor. İkinci tuş için global bir artım değişkeni tanımlanarak her fonksiyondaki değerler buna göre düzenlenebilir. Benim zorlandığım sorularda bu tür tuşlardan bulunmadığından kodunu yazmam gerekmedi. 

30 Ekim 2018 Salı

Karakter LCD için 3.3V TTL Sinyalleri 5V'a Yükseltelim


Merhaba. Bir önceki yazıda, anlatacağım olayların birbirinden bağımsız olduğunu söylemiştim ama bu kısmen doğru. Önceki yazıda hazırladığım masaüstü makina bana paralel portu için gerekliydi. Maalesef artık paralel portlu makinalar bulunmuyor. Paralel porta ihtiyacınız varsa bunun için özel üretilen PCI kartlardan almak gerekiyor. USB-Paralel port çeviriciler standart paralel port arayüzü sunamıyor. Standart arayüzden kastım 0x378 veya 0x3BC portu. Paralel port, LCD devresi için gerekliydi. Bu yazıda hepsinden biraz bahsedeceğim. Bu arada benim favorim 0x3BC ama kullandığım BIOS port seçimini desteklemiyor. Bu yüzden 0x378'i kullanmak zorunda kaldım.

Paralel portu kullanmadan önce BIOS'tan, açık olduğunu ve uygun bir modda çalıştığını kontrol ettim. Aslında uygun mod için belli bir kastım yok, port açıksa en basit "Output only" bile yeterli.

Sözünü ettiğim makinaya USB ile CentOS kurdum. dmesg çıktısından paralel portunu da kontrol ettim.

[root@xxxxxxxx ~]# dmesg | grep -i parport
parport_pc 00:08: reported by Plug and Play ACPI
parport0: PC-style at 0x378, irq 7 [PCSPP]

Köşeli parantezde portun modu yazıyor. Benim portum standart paralel port (SPP) olarak çalışıyor. Çift yönlü paralel port için [PCSPP, Tristate], Enhanced Parallel Port (EPP) için [PCSPP, Tristate, EPP] ve Extended Capabilities Port (ECP) için [PCSPP, Tristate] ile birlikte parantez içinde ikincil port adresi yazmalı. Aklımda kaldığı kadarıyla SPP, tek yönlü 50KB/s hızla çalışıyor. Çift yönlü mod aynı hızda ama adı üzerinde çift yönlü; EPP veya ECP'den biri 500KB/s'den 2MB/s'ye kadar hızlı çalışıyor. Yazının konusu bu olmadığından fazla detayına girmek istemiyorum.

Yapmak istediğim devrenin şeması şöyleydi:
Devre şeması (orjinali: http://www.aljaz.info/elektro/lcd/lcd-lpt.htm)
İlgili kaynağı takip etmedim ama benim kullandığım kaynaktaki devre şeması da pull-up dirençler dışında bunun aynısınıydı. Proje basit olduğundan herkesin aynı devreyi kurması çok da şaşırtıcı değil.

Devreyi kurup LCD'ye yazması gereken kodu* çalıştırdığımda ekranda bir çıktı yoktu. Farklı şekilde denesem de sonuç değişmedi. LCD yerine LED'ler bağladım ve kodun çalışma hızını saniyede bir karaktere kadar düşürdüm. Fazla ayrıntısına girmeyeceğim. En son port gerilimini ölçtüğümde sorunun portun 3.3V çıkış vermesi olduğunu buldum. Aslında emin olmak için LCD'nin datasheet'ine bakıp 3.3V'un TTL'deki kararsız bölgeye düştüğüne emin olmam gerekirdi ama bunu yapmadım.
(*): LCD kodunu yazının sonlarına doğru bulabilirsiniz.


Paralel portla 366 Celeron makinada da uğraştığımı düşününce çok sürpriz değil. Test için aşağıdaki kodu kullandım.

#include<stdio.h>
#include<sys/io.h>

#define BASE 0x378

int main(int argc, char* argv[])        {
    if(ioperm(BASE, 1, 1))      {
        fprintf(stderr, "Access denied to %x\n", BASE);
        return 1;
    }
    outb(1, BASE);
    return 0;
}

https://commons.wikimedia.org/wiki/File:IEEE_1284_36pin_plughead.jpgKod en düşük anlamlı biti set edip çıkıyor. En düşük anlamlı bit, hem DB25 paralel portta hem de Centronics portta ikinci pine karşılık geliyor. Bu arada "Centronics port", aslında Centronics firması tarafından geliştirilen ve çift yönlü paralel portu tanımlayan IEEE 1284 standardı için kullanılıyor. Benim kastettiğim, port değil yanda fotoğrafını koyduğum bağlantı. Bu bağlantı yukarıdaki benim fotoğrafta da görülebiliyor.

Bu bağlantının asıl adı Micro Ribbon Connector'müş. Fotoğraftaki, yazıcılarda kullanılan 36 pin'li konektör. Ben ağız alışkanlığıyla Centronics port demeye devam edebilirim.

Pinleri karıştırmamak için porta 1 yerine 255 gönderilirse tüm data pinlerindenden [2-9] +3.3V alınabilir.

74HC244 Bağlantı şeması
Internette bulabildiğim kadarıyla bu gerilim farkından özellikle CNC'ciler şikayetçi. 6502.org forumunda, port 1K'lık veya 4.7K'lık pull-up dirençlerle bağlanarak 5V'a çıkarılabileceği yazıyordu [ burada ], bunu güvenilir bulmadığım için denemedim. Bunun yerine aklıma 8-bit tristate buffer entegresiyle girişleri yükseltmek geldi. Bu iş için 74HC244 entegresini kullandım. Datasheet'ine baktığımda lojik 1 için minimum giriş gerilimi 2.0V. Yani paralel porttan aldığım 3.3V'u girişe lojik 1 olarak uygulayıp çıkışta 5V alabilirim.

Bunun için yukarıda şemasını çizdiğim basit bir devre kurdum. Entegrenin enable pinini kullanmadığım için bunu toprağa bağladım. Bir tane de 100nF kondansatör koydum. Yalnız LCD'yi 8 bit modda çalıştırdığımdan LCD'nin RS ve E pinleriyle birlikte 10 bite ihtiyacım vardı. Bu nedenle iki tane 74HC244 kullandım. +5V'u sabit disk bağlantısından, GND'yi paralel port şasisinden kullandım. İlk denemeyi tek pinle yaptım. Sonuç olumlu.


Fotoğraftaki twisted pair kablo +5V. Eski CAT5 kabloları böyle değerlendiriyorum. Şemadaki 100nF kondansatörü breadboard'da koymaya üşendim. Üstte girişten okuduğum, altta da çıkıştan okuduğum gerilim görülüyor. O halde artık tüm pinleri entegreler üzerinden LCD'ye bağlayıp deneyebilirim.


Devrenin son hali yukarıdaki fotoğrafta. Aslında kontrastı sabitleyip arka ışıklandırmayı kullanmayacaktım ama pinlerden birini hatalı bağladığım için sorun yaşadım. Ekranı mı göremiyorum yoksa veride mi sorun var, anlamak için bağladım, sonra da geri sökmedim. Kod aşağıda: 

#include<stdio.h>
#include<sys/io.h>

#define BASE 0x378    // Base port adresi
#define CTRL 0x37A    // Kontrol portu adresi
#define DELAY 3000    // Busy wait icin

void lcdKomut(unsigned char veri)    {
    outb(veri, BASE);
    outb(8   , CTRL);    // RS = 0; E = 1
    usleep(DELAY);
    outb(1   , CTRL);    // RS = 1; E = 0
    usleep(DELAY);
}

void lcdVeri(unsigned char veri)    {
    outb(veri, BASE);
    outb(0, CTRL);    // RS = 1, E = 1
    usleep(DELAY);
    outb(1, CTRL);    // RS = 1, E = 0
    usleep(DELAY);
}


int main(int argc, char* argv[])    {
    if(ioperm(BASE, 3, 1))    {
        fprintf(stderr, "Access denied to %x\n", BASE);
        return 1;
    }

    lcdKomut(0x38);    // 8 bit, 2 satir, 5x7 px
    lcdKomut(0x01);    // ekrani sil
    //lcdKomut(0x80);    // satirbasi
    lcdKomut(0x0F);    // ekrani ac, kursor blink

    lcdVeri('D'); lcdVeri('e'); lcdVeri('n');
    lcdVeri('e'); lcdVeri('m'); lcdVeri('e');
    lcdKomut(0xC0);    // ikinci satir
    lcdVeri('1'); lcdVeri('2'); lcdVeri('3');
 
    outb(0, BASE);
    return 0;
}

Aslında paralel portun kontrol yazmacı, veri yazmacının 2 fazlası. Bu nedenle ikinci define, "#define CTRL BASE+2" olsa daha iyi. DELAY parametresi, LCD ekranın komutları işlemesi için gereken süre. Bunu 300 µs'ye kadar indirdiğimde bu kodda sorun yaşamadım ama başka kodlarda yaşadım. 3 ms, deneme yanılmayla bulduğum bir değer. Eğer R/W pinini kullanıyor olsaydım LCD ekranın busy flag'ini D7 bitinden okuyabilirdim. DELAY için üst sınır bulunmuyor.

lcdKomut() fonksiyonu komut göndermek için, lcdVeri() fonksiyonu da veri göndermek için iki fonksiyon. Bunların çalışması sırasında pinlerin değerini kodun açıklamasına yazdım. Komut gönderildikten sonra E = 0 olması RS pininin değerinden önemli. E = 0'a hemen çekilmezse anlayamadığım biçimde kendisinden sonra gelen veri iki kere gönderiliyordu. Bu sorunu ayrıntılı ele almak üzere ileri bir yazıya sakladım.

main() fonksiyonunda LCD'ye temel komutları gönderdim. 0x80 komutunu kaldırdım çünkü hatırladığım kadarıyla 0x01 komutu zaten 0x80'i içeriyor. Veri gönderme fonksiyonuyla ekrana yazacağım verileri gönderdim. Kürsör veri gönderildikçe kendiliğinden ilerliyor. 0xC0 komutu ikinci satır başına gidiyor. Eğer ikiden fazla satır olsaydı diğer satırlara gitmek için başka komutlar var.

Bu yazı TTL gerilim değeriyle ilgili olduğundan burada bitiriyorum. Pin  bağlantı tablosu, bağlantıda yaşadığım sorun ve başka LCD komutlarıyla ilgili ayrı bir yazı daha yayınlayacağım.

24 Temmuz 2018 Salı

Huawei E5573 Mobile Wifi'ın CentOS'da Çalıştırılması (Multimode USB)


Merhaba. Bu ve bundan sonraki iki yazı yakın zamanda başımdan geçen ve birbiriyle tamamen ilgisiz üç olaydan bahsedeceğim. Bunlardan ilki multimode USB hakkında olacak. Diğeri LCD'deki TTL sinyal seviyesiyle sonuncusu da bir oyunla ilgili. Bu yazıda yaşadığım olay üzerinden CentOS'ta multimode USB kullanımını anlatacağım.

Aşağıdaki cihaz Huawei E5573 taşınabilir WiFi erişim noktası. Wifi repeater olarak kullanılabildiği gibi SIM kart takarak 4G bağlantıyı Wifi'a dönüştürebiliyor. Ayrıntılı bilgi için: https://consumer.huawei.com/lk/support/mobile-broadband/e5573/
VINN tabir edilen cihazlar bilgisayara yazılım/sürücü kurulumu gerektirdiğinden, telefonumu Wifi Hotspot olarak kullanmayı tercih ediyordum. Bunun da dezavantajı, 3G şebekede arama geldiğinde bağlantının kesilmesi.

Huawei E5573 Wifi Erişim Noktası

Linux Mint altında bu cihazı bağladığımda kendisini ethernet arabirimi (ethX) gibi tanıtabiliyor. Buna USB tethering deniyor. Minimal CentOS gibi Wifi sürücüleri olmayan dağıtımlarda kolaylık sağlayabilir veya Wifi arabirimi olmayan masaüstü makinada ethernet kartı gibi davranabilir. Bu arada benim yaşadığım olayda masaüstü bir makinaya minimal CentOS kurmuştum.

Cihazın daha önce önemsemediğim bir özelliği, makinanın farklı USB portlarına taktığımda veya başka işletim sistemlerinde (Windows) harici CD sürücü gibi davramasıydı. CD'de Windows sürücüleri var ama Windows'ta zaten Wifi olarak kullanıyorum. Tethering özelliğini tesadüfen farkettim ama Windows'ta çalıştırmaya ihtiyaç duymadım. Yukarıda söz ettiğim masaüstü makina için ihtiyaç oldu ama CentOS'ta farklı ethernet kartı olarak tanıtamadım.

İlkin, CentOS'ta tethering özelliğini sağlayan sürücülerin yüklü olmadığını düşündüm (minimal). Mint'ten kontrol ettiğimde cdc_ether sürücüsünün yüklendiğini gördüm:

[239053.520493] usb 1-2: new high-speed USB device number 9 using xhci_hcd
[239053.650038] usb 1-2: New USB device found, idVendor=12d1, idProduct=1f01
[239053.650045] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[239053.650048] usb 1-2: Product: HUAWEI_MOBILE
[239053.650051] usb 1-2: Manufacturer: HUAWEI_MOBILE
[239053.650054] usb 1-2: SerialNumber: 0123456789ABCDEF
[239053.734303] usb-storage 1-2:1.0: USB Mass Storage device detected
[239053.734728] scsi host16: usb-storage 1-2:1.0
[239054.738238] scsi 16:0:0:0: CD-ROM            HUAWEI   Mass Storage     2.31 PQ: 0 ANSI: 2
[239054.739637] sr 16:0:0:0: [sr1] scsi-1 drive
[239054.741976] sr 16:0:0:0: Attached scsi CD-ROM sr1
[239054.742188] sr 16:0:0:0: Attached scsi generic sg2 type 5
[239054.811184] systemd-udevd[7829]: Failed to apply ACL on /dev/sr1: No such file or directory
[239054.811195] systemd-udevd[7829]: Failed to apply ACL on /dev/sr1: No such file or directory
[239054.825401] usb 1-2: USB disconnect, device number 9
[239055.264962] usb 1-2: new high-speed USB device number 10 using xhci_hcd
[239055.394110] usb 1-2: New USB device found, idVendor=12d1, idProduct=14db
[239055.394116] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[239055.394119] usb 1-2: Product: HUAWEI_MOBILE
[239055.394122] usb 1-2: Manufacturer: HUAWEI_MOBILE
[239055.547598] cdc_ether 1-2:1.0 eth1: register 'cdc_ether' at usb-0000:00:14.0-2, CDC Ethernet Device, XX:XX:XX:XX:XX:XX
[239055.584282] cdc_ether 1-2:1.0 eth1: kevent 12 may have been dropped
[239055.599362] cdc_ether 1-2:1.0 eth1: kevent 12 may have been dropped

[239055.601286] IPv6: ADDRCONF(NETDEV_UP): eth1: link is not ready
[239055.721397] IPv6: ADDRCONF(NETDEV_CHANGE): eth1: link becomes ready
[239055.721411] cdc_ether 1-2:1.0 eth1: kevent 12 may have been dropped


CentOS'taki dmesg çıktısı şöyle:



CentOS'ta lsmod çıktısında cdc_ether sürücüsünü göremedim. Aslında görememiş olmam normal çünkü henüz USB'yi takmamıştım. Sürücü modprobe ile yüklendi ama USB'yi takınca yeni bir arabirim oluşmadı. Sürücüyü google'da aratıp karşılaştığım diğer bağlantıları kurcaladım. Sürücüyle ilgili ilk sonuç https://www.linuxquestions.org/questions/linux-networking-3/how-do-i-configure-a-cdc-ethernet-device-a-4g-usb-dongle-835856/ bağlantısıydı. Burada başlığı açan benim yaşadığıma benzer bir sorun yaşamış. Buna verilen yanıt önemli. Yanıttan öğrendiğim üzere bu tür cihazlara "USB dual mode" veya genel olarak "multimode" cihazlar deniyor. Tek USB'de birden çok aygıt sınıfı barındırabiliyor ve o yanıttaki usb modeswitch bağlantısı daha da önemli. Bu bağlantıda multimode cihazları linux altında kullanabilmek için bir uygulama var.

Uygulamayı indirdim. Masaüstü makinamda henüz internet olmadığından indirdiklerimi USB diskle aktardım. Derlemeden önce libusb1-devel paketinin bulunması gerekiyor. Ben fazladan libusb-devel'i de yüklemiştim. Bir de lsusb komutu için usbutils paketi gerekli. Bunların hepsinin rpm dosyalarını indirip onları da aynı USB diskle aktardım. Bundan sonrası make/make install.

usb_modeswitch'in parametrelerine baktığımda iki gerekli parametre var:
-v vendor ID of original mode (mandatory)
-p product ID of original mode (mandatory)

Ve iki tane de seçimlik ama önemli parametre var:
-V target mode vendor ID (optional)
-P target mode product ID (optional)

-v ve -p'ye vereceğim ID'leri lsusb ile buldum. Çıktıda görüleceği üzere CD sürücünün USB id'si 12d1:1f01:


Buna karşılık Mint'teki çıktı aşağıdaki gibi:

Bus 001 Device 010: ID 12d1:14db Huawei Technologies Co., Ltd. E353/E3131.

Lazım olabilir diye Mint'ten dmesg, lsusb ve lsusb -v çıktılarını alıp CentOS'a döndüm. Artık -V ve -P'ye vereceğim ID'leri de biliyordum ama komutu aşağıdaki gibi verdim ve uyarı aldım:

usb_modeswitch -v 12d1 -p 1f01 -V 12d1 -P 14db
[SNIP]
Warning: no switching method given. See documentation.

Segmentation fault.


Tekrar usb_modeswitch çıktısına baktığımda Huawei'yle ilgili üç parametre dikkatimi çekti : -H, -J ve -X. Hakkında hiç açıklama olmadığından ve yalnızca üç tane olduğundan deneme yanılmayla benim kullanmam gerekenin -J olduğunu buldum. Bu arada -V ve -P'ye de gerek yokmuş. Target mode'u kendisi bulabiliyor.


Yazılımın biraz sorunlu olduğu çıkışta "Segmentation fault" vermesinden belli ancak sonuçta cihaz artık yeni bir ethernet kartı gibi çalışabiliyordu. 


30 Haziran 2018 Cumartesi

LAPACK ve BLAS ile Matris İşlemleri


"A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away."
- Antoine de Saint-Exupery

Merhaba. Bu yazıda matris işlemlerinin kütüphanelerle nasıl yapılacağına değineceğim. Şubat ayındaki yazıda bundan bahsetmiştim. Aslında kod uzun zamandır hazırdı ama yazıyı ancak hazırlayabildim. Kütüphanenin ne kadar azını derleyerek kodu çalıştırabilirim, en az sayıda komutla, ne kadar az paket kurarak derleyebilirim sorusu bir tam günümü aldı. Yazının başındaki alıntıya da, yapmaya çalıştığımla doğrudan ilgili olduğundan yer vermeyi uygun buldum.

Her zamanki gibi, yazı minimal bir Centos6.9 makinayla başlıyor. Kurulumu ve diğer adımları hem daha önceki yazılarda anlattım hem de bu yazının amacıyla bağdaşmıyor. Bir makinanın kurulu olduğunu varsayarak devam ediyorum.

yum install gcc gcc-gfortran
Makinayı kurar kurmaz yum update ile güncellemelerini geçip makinayı yeniden başlattım. Bu adım kesinlikle gerekli değil ama olsa çok iyi olur. Kütüphaneyi derlemek için gereken gcc ve gcc-gfortran paketlerini yum'la kurdum.

Ardından LAPACK kütüphanesini indirdim. LAPACK, Linear Algebra Package kelimelerinin kısaltması. Fortran'la geliştirilmiş ve içinde doğrusal denklem çözümleri, en küçük kareler, özdeğer/özvektör hesaplamaları, matris çarpanlara ayırma algoritmaları gibi önemli matris işlemleri için fonksiyonlar var. İşlemler saydıklarımla sınırlı değil, öğrenim hayatımdan aklımda kalanları bunlar. Gündelik hayatta karşılaşılmasa da birçok bilimsel hesaplama programı bu kütüphane üzerine kurulu.

İkinci önemli kütüphane BLAS. Basic Linear Algebra Subroutines'in kısaltması olan BLAS vektör ve matris işlemleri sunan daha alt seviye bir kütüphane. LAPACK, derlenebilmek için BLAS'a ihtiyaç duyuyuyor.

İlkin açıklığa kavuşturulması gereken, basit bir matris çarpma veya toplama işlemi için gerçekten bu kütüphanelere ihtiyacımız olup olmadığı.

Matris Çarpımı Nedir?
Bu sorunun yanıtı için matris cebirinin en alt seviye tanım ve teoremlerine girmek istemem. Yapabildiğim kadar çevresinden dolaşmaya çalışacağım. Matrislerde çarpma, birinci matris (m x n) ve ikinci matris (n x p) boyutlu olmak üzere tanımlıdır. Kolaylık için birinci matrise A, ikinci matrise B diyip m = n = p kabulüyle matrislerin kare olduğunu düşünelim. Sonuç matrisine C diyelim. C, (p x p) boyutlu bir matristir. C'nin birinci elemanı, A'nın birinci satır elemanlarıyla 

B'nin birinci sütun elemanlarının tek tek çarpımlarının toplamıdır. C matrisinde birinci elemanın sağında bulunan C'nin ikinci elemanı; A'nın ikinci satır elemanlarıyla B'nin birinci sütun elemanlarının tek tek çarpımları toplamıdır. Sözle ifade etmek kolay olmadığından matematiksel ifadelere başvurmak zorundayım. A matrisi yandaki ve B matrisi aşağıdaki gibi olsun:


A ile B'nin çarpımı aşağıdaki gibi tanımlıdır:


 Elde edilecek elemanların C matrisindeki yerleri şöyledir:


Eğer işlem yalancı kodla ifade edilmek istenirse aşağıdaki gibi olur:

for i = 1 to p
    for j = 1 to p
        c(i)(j) = 0
        for k = 1 to p
            c(i)(j) = c(i)(j) + a(i)(j) * b(i)(j)

Bu işlemin karmaşıklığı p'nin küpüne bağlı olarak değişir. Büyük O gösterimiyle O(n3) olarak ifade edilir. Başka bir deyişle (p x p) boyutlu iki matris birim zamanda çarpılıyorsa, (2p x 2p) boyutlu iki matris sekiz birim zamanda çarpılır. Literatürde bu işleme çeşitli yaklaşımlar geliştirilmiş ve işlemin karmaşıklığı farklı algoritmalarla O(n2.37)'ye kadar indirilmiştir (ek okuma: Matrix multiplication algorithm).

Konunun kodlama ve optimizasyon boyutu teorisinden oldukça farklıdır. Örn. yukarıda verdiğim yalancı kod C'nin satır yerleşimli dizilerinde cache bellek yerleşiminden ötürü optimal değildir (bilimsel hesaplama veya sayısal algoritmalar dersinin giriş konusudur) bunun yerine indisleri i, k, j olarak sıralamak daha iyi cache hit oranı sağlar. Başka bir deyişle işlemlerin daha fazlası cache bellek içinde yapılır ve algoritma daha hızlı çalışır.

Bu algoritmaların uygulamalarını üniversitedeyken araştırmıştım. Önce yukarıdakine benzer bir kodla başlayıp cache kullanımını iyileştirdim ve değişkenleri işlemciye yakın tutarak kodu hızlandırdım. Sonra algoritmayı değiştirip Strassen Algoritması'nı ekledim. Üçüncü adımda bu algoritmayı özyinelemeli (recursive) duruma getirip teoride elde edilebilecek sınıra getirdim. Kodu derlerken bazı mikroişlemci optimizasyonlarını da devreye alıp elde edebildiğim en iyi sonucu buldum.

Karşılaştırmak için aynı matrisleri BLAS'la çarptığımda aynı derleyici parametrelerini de kullanmama rağmen maalesef BLAS yaklaşık 1/5 oranında daha hızlı çalışıyordu (yani 5 sn yerine 4 sn). Daha da üzücü olan, BLAS'ın yaklaşık 60sn'de çarptığı iki matrisi Matlab'in bir saniyeden biraz daha uzun bir sürede çarpmasıydı. Matlab'in kodu, güncel mikroişlemci mimarisine çok daha uygun yazılmıştı. Araştırmada kullandığım işlemci SSE2 destekliydi ve matris çarpımında yapılan çarpmaların sayısını işlemcinin çekirdek saat hızına böldüğümde neredeyse bire yakın bir oran çıkıyordu. Yani Matlab çarpma yaparken işlemciyi o kadar verimli kullanılıyordu ki başka hiçbir iş yapmıyordu.

Uzun lafın kısası, gerçekten birşeyler yapabilmek için bu kütüphanelere gerek var. BLAS, otuz yıllık bir kütüphane ve bu zamanda yüzlerce insan tarafından geliştirildi. Basit bir araştırma projesinin bir kaç ayda bu kütüphaneyi geçmesini beklemek insafsızlık olurdu. BLAS terminolojisinde birinci seviye işlemler vektörlerle ilgili çarpımlar, ikinci seviye işlemler matrislerin vektörlerle işlemleri ve üçüncü seviye işlemler matris-matris işlemleri olarak adlandırılır. LAPACK, BLAS fonksiyonlarını kullanır ve daha karmaşık işlemleri yapmaya olanak sağlar.

Açık konuşmak gerekirse üniversitedeyken bu kütüphanelerin adını çok duydum. Keza iş hayatında çok kere bu kütüphaneleri ve bunlara bağlı çalışan programları derledim ancak ne derslerde ne de uygulamalarda kütüphanelerin kullanımı verilmedi. Bu kelimeler bilinmeyen diyarlara ait Ebabil kuşu gibi kullanılırdı. BLAS'ı üniversitede bir kaç projemde kullandım. LAPACK'ı hiç kullanmadım. Bugün dışarıdan bir gözle baktığımda bu durumu oldukça olumsuz görüyorum.


Uzun bir teorik aradan sonra tekrar yazının konusuna döneyim. Yazıyı yazdığım zaman itibariyle LAPACK'ın en yenisi 3.8.0 idi. Bunu indirip açtım:

curl http://www.netlib.org/lapack/lapack-3.8.0.tar.gz > lapack-3.8.0.tar.gz
tar xvfz lapack-3.8.0.tar.gz
Not: İndirmek için curl kullandım çünkü minimal CentOS'da wget yok.

Lapack açıldıktan sonra oluşan dizine girip Makefile'ın içerdiği (include) ayar dosyasını örnek dosyadan kopyalayıp Makefile'daki hedefleri derledim: 

cp make.inc.example make.inc
make lib
make blaslib
make cblaslib
make lapackelib

"lib" LAPACK'ın Fortran kütüphanesi. "blaslib", LAPACK ile birlikte gelen BLAS'ın Fortran kütüphanesi. "cblaslib" BLAS'ın C arabirimi ve "lapackelib" de LAPACK'ın C arabirimi. Bu hedeflerin derlenmesi gerek.

Yazdığım kodu Google Drive'da paylaştım. Aşağıdaki bağlantıdan indirilebilir: 

En baştaki matrisyaz fonksiyonu, matrisi ekrana yazdırıyor. Çıktıyı Matlab'a uyumlu yazdırıyorum ki sonuçların doğruluğunu kontrol edebileyim. main fonksiyonunun içinde boyut = 3 olarak tanımladım. Bu değişken matrislerin satır/sütun sayılarını tutuyor. Aşağıdaki kod sayesinde programa argüman olarak bir sayı vererek boyutu değiştirmek mümkün.

if(argc > 1)   {
     boyut = atoi(argv[1]);
}

Lapack'ın kendi lapack_int tamsayı tanımlaması olduğundan boyutu bu değişkene atadım. matrisA, üzerinde işlem yaptığım matris. matrisB, ilk başta matrisA'ya eşit. matrisA'nın tersini aldıktan sonra sağlama için matrisB'yle çarpıp sonucun birim matris olduğunu göstereceğim. Matrisleri malloc ile oluşturup matrisA ile matrisB'nin içeriğini rastgele doldurdum. ipiv vektörü işleme girecek olan matrislerin satır sırasını değiştirmekte kullanılacak vektör. Ben boş bırakıyorum, kullanmayacağım. 

Kodda üç basit işlem var. İlkin dgetrf fonksiyonuyla matrisi LU çarpanlarına ayırdım. LAPACK'ın fonksiyon adlandırması şöyle: Baştaki 'd' double anlamına geliyor. 's' single, 'c' complex ve 'z' double complex anlamında. İlk harfler işleme girecek matris elemanlarının değerliklerini (precision) belirtiyor. Sonraki iki karakter 'ge' genel matrislerle ilgili fonksiyon anlamında. Matrisin simetik, köşegen, üç-köşegen, üçgen matris vb. türleri farklı iki karakterle kodlanıyor. Ayrıntılı liste LAPACK belgelendirmesinde veya Wikipedia sayfasında bulunabilir. Son üç karakterse fonksiyonun kısaltması. Örn. yanlış hatırlamıyorsam 'trf' triangular factorization'ın kısaltması. LU ayrıştırmasını yapan kod aşağıda: 

LAPACKE_dgetrf(LAPACK_ROW_MAJOR, lapack_boyut, lapack_boyut, matrisA, lapack_boyut, ipiv);

C'de bir malloc yapıldığında bellekteki ardışık iki elemanın, matrisin bir satırındaki ardışık iki eleman olduğu düşünülür. Örn. a11 ve a12 gibi. Fortran'daysa bellekteki ardışık iki elemanın, matrisin sütunlarındaki ardışık elemanlar olduğu düşünülür, a11 ve a21 olarak. LAPACK_ROW_MAJOR değeri matrisA'nın satır olarak işlenmesi gerektiğini gösterir. Sonra gelen lapack_boyut değişkenleri matrisin satır ve sütun sayısı; matrisA, matrisin göstericisidir. Bir sonraki lapack_boyut argümanı LDA olarak ifade edilir. Açılımı Leading Dimension of Array olup matrisin tümü değil de bir parçası üzerinde işlem yaparken anlamlıdır. Ben tümünde işlem yapacağım için aynı boyutu verdim (konuyu dağıtmamak adına bunu en son açıklayacağım). Son olarak ipiv adlı boş vektöru fonksiyona verdim. 


Ardından, dgetri fonksiyonuyla yukarıda ayrıştırdığım matrisin çarpımsal tersini alıyorum.

LAPACKE_dgetri(LAPACK_ROW_MAJOR, lapack_boyut, matrisA, lapack_boyut, ipiv);

'tri', triangular inverse olması gerekiyor. Cebire girmek istememiştim ama burada kısaca bahsetmeliyim. Sayılardaki çarpmada birim eleman (ilkokuldaki adıyla etkisiz eleman) '1'dir. Bir sayı birim elemanla çarpıldığında sonuç kendisidir. Bir elemanın işlemsel tersi, söz konusu elemanla işleme girdiğinde birim elemanı veren elemandır. Başka bir deyişle sayı, tersiyle çarpıldığında birim elemanı vermelidir. Çarpmada x sayısının tersi 1/x'tir. Sıfır dışında her elemanın, çarpma işlemine göre tersi vardır. Matrislerde ufak istisnalar dışında yukarıdaki kurallar aynen geçerlidir. Matrislerde birim eleman -birim matris- köşegeninde 1'ler diğer elemanları 0'lar olan matristir ve hepsi değilse de birçok matrisin çarpımsal tersi bulunabilir. (Meraklısına: https://proofwiki.org/wiki/Real_Numbers_form_Ring)

Yalnızca kare matrislerin tersi (aslında tek bir tersi demek daha doğru sanırım) olabileceğinden burada lapack_boyut argümanı bir kere veriliyor. Dördüncü argüman yine LDA.


Buraya kadar yapılan, LAPACK fonksiyonlarıyla matrisA'nın tersini bulmaktı. Şimdi üçüncü seviye bir BLAS fonksiyonu kullanarak matrisB'de sakladığım ilk değeri, tersi olduğunu iddia ettiğim matrisA ile çarpacağım ve doğruysa birim matrisi elde edeceğim. LAPACK fonksiyonları gibi BLAS fonksiyonlarının da adlandırma kuralları var. Matris çarpımı için dgemm fonksiyonunu kullandım. Double GEneral Matrix Multiplication ifadesinin kısaltması. Çarpımı yapan kod şöyle:

cblas_dgemm( CblasRowMajor, CblasNoTrans, CblasNoTrans, lapack_boyut, lapack_boyut, lapack_boyut, 1.0, matrisA, lapack_boyut, matrisB, lapack_boyut, 0.0, matrisC, lapack_boyut);

CBLAS'ın da LAPACK'a benzeyen öntanımlı değerleri var. CblasRowMajor, bunlardan biri ve görevi LAPACK_ROW_MAJOR ile aynı. İki CblasNoTrans argümanı sırasıyla işleme giren matrisA'nın ve matrisB'nin olduğu gibi işleme sokulması gerektiğini belirtiyor. Eğer bu argüman CblasTrans verilirse matrislerin devriği (transpozesi) işleme sokulur. Devrik işlemi, satırların sütun, sütunların satır durumuna gelmesidir. Türkçe wikipedia'da bu Tersçapraz olarak ifade ediliyor. Matris çarpımının tanımını verirken, çarpımın (m x n) boyutlu matrislerle (n x p) boyutlu matrisler arasında tanımlı olduğunu yazmıştım. Bu durumda sonuç (m x p) boyutlu bir matris olur. Bu fonksiyondaki birbirini tekrar eden üç lapack_boyut argümanı (dört, beş ve altıncı argümanlar) sırayla m, n ve p sayılarına karşılık gelir ama basitlik için bu örnekte m = n = p alınmıştır. 

dgemm aslında aşağıdaki işlemi yapar: 

dgemm

Yani A'nın elemanlarını alfa ile çarpıp B matrisiyle matris çarpımı yapar, C'nin elemanlarını beta ile çarpıp sonucu toplar ve C matrisine yazar. Yapmak istediğim yalnız matris çarpımı olduğundan alfa, 1.0 ve beta 0.0 olarak alınmalıdır. İşte matrisA argümanının önündeki 1.0, alfa parametresi olup matrisA'dan sonraki lapack_boyut argümanı da LDA'dir. Sonrasında matrisB'nin göstericisi ve matrisB'ye ait LDB argümanı bulunur. Son bölümde 0.0 değerli beta sabiti, matrisC'nin göstericisi ve matrisC'ye ait LDC yer alır. 

Kaynaklar:

Kodun içinde kolaylık olsun diye fonksiyonların prototiplerini de açıklama olarak koydum.

Kodu aşağıdaki komutla derledim:
gcc matris.c -I/root/lapack-3.8.0/CBLAS/include \
-I/root/lapack-3.8.0/LAPACKE/include -L/root/lapack-3.8.0 \
-llapacke -llapack -lrefblas -lcblas -lgfortran -o matris.x

Kodun çıktısı aşağıdaki gibi:

A matrisi = [
0.000000  4.000000  0.000000  ;
3.000000  7.000000  1.000000  ;
5.000000  5.000000  1.000000    ]

LU sonrasi matrisA = [
5.000000  5.000000  1.000000  ;
0.000000  4.000000  0.000000  ;
0.600000  1.000000  0.400000    ]

matrisA'nin tersi = [
0.250000  -0.500000  0.500000  ;
0.250000  -0.000000  0.000000  ;
-2.500000  2.500000  -1.500000    ]

carpim = [
1.000000  -0.000000  0.000000  ;
0.000000  1.000000  0.000000  ;
0.000000  0.000000  1.000000    ]

Sonuçlar GNU Octave'daki sonuçlarla tamamen tutarlı:

Son olarak dikkat çekmek istediğim bir nokta var. Çıktıda ikinci eleman -0 olarak görünüyor. Bu ayrı bir yazının konusu, o yüzden gelecek yazılardan birinde değineceğim.



Ek: LDA nedir? Ne zaman gereklidir? 
Tekrar LU ayrıştırma fonksiyonuna dönelim:

LAPACKE_dgetrf(LAPACK_ROW_MAJOR, lapack_boyut, lapack_boyut, matrisA, lapack_boyut, ipiv);

Varsayalım ki görece büyük bir matris var. Örn. 20 x 20. Bunun sol üstündeki 5 x 6'lık parçasıyla işlem yapmak istiyorum. Matlab'deki ifadesiyle A(1:5, 1:6). Bu büyük bir matrisin parçası olduğundan, parçanın satır sonundaki elemandan hemen sonra bellekte bir sonraki satırın ilk elemanı bulunmaz. Bir sonraki ilk eleman 20 - 6 = 14 birim (float, complex vb) uzaklıktadır. İşte LDA alt alta olan sütun elemanları arasındaki mesafenin birimidir. Yani yukarıdaki işlem için fonksiyon:

LAPACKE_dgetrf(LAPACK_ROW_MAJOR, 5, 6, matrisA, 20, ipiv );

olmalıdır. Gösterimi basit tutmak için işleme giren matrisi sol üstten seçtim aksi halde parçanın ilk elemanının bellek adresini bulmak için matrisA ile bazı pointer aritmetiği işlemlerine girmem gerekecekti. Dikkat edilmesi gereken bir konu daha var. LDA, LAPACK_ROW_MAJOR seçildiği için bir sonraki satırın aynı sıradaki elemanı arasındaki uzaklığı belirtiyor. Eğer LAPACK_COL_MAJOR seçilseydi o zaman bir sonraki sütunun aynı sıradaki elemanı arasındaki uzaklığı gösterirdi. Özetle LDA büyük bir matrisin parçaları üzerinde işlem yaparken, asıl matrisin satır veya sütun sayısı olmalıdır. Bunlardan hangisinin olması gerektiğini LAPACK_ROW_MAJOR veya LAPACK_COL_MAJOR argümanı belirler.