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
}