Linux Altında I2C İşlemleri

Linux üzerinde, bir önceki bölümde anlatılan detaylarla uğraşmaksızın, bir sürücü (driver) üzerinden I2C protokolünü kullanmak mümkündür.

Not: Linux yüklü sistem üzerinde bir I2C controller chip'i olduğunu varsayıyoruz. Ayrıca, yazılımsal olarak, GPIO pinleri üzerinden, I2C protokolünü oluşturmak da mümkündür (bit banging).

I2C protokülünü kullanabilmek için ilk olarak aygıt sürücüsüyle etkileşime geçebiliyor olmalıyız. Bu amaçla /dev altında gerekli aygıt düğümleri bulunuyor olmalıdır. I2C sürücüsüne, bu aygıt düğümleri üzerinden, standart Giriş/Çıkış sistem çağrılarını kullanarak erişeceğiz.

Not: I2C desteği kernel içinde built-in olabileceği gibi modül şeklinde de bulunabilir. Bu durumda gerekli modül (i2c-dev.ko) kernel adres alanına yüklenmelidir.

IC2 protokolünü kullanabilmek için birden çok yöntem olmakla birlikte, temel olarak open, read, write ve ioctl sistem çağrılarının kullanılması yeterlidir. Burada üç farklı yöntemden bahsedeceğiz. Her üç yöntemde de ilk olarak open sistem çağrısıyla aygıt dosyası açılmakta ve elde edilen handle ile işlemlere devam edilmektedir. Temelde birbirine benzeyen bu yöntemlerin detaylarına geçelim.

read, write Sistem Çağrıları İle Okuma/Yazma

Burada yapılan işlemler standart dosyalar üzerinde yapılanlara benzemektedir. İşlem adımları aşağıdaki gibidir:

  1. open ile aygıt dosyasını aç ve daha sonraki çağrılarda kullanılacak handle değerini sakla
  2. ioctl çağrısı ile iletişime geçilecek aygıtı adresle
  3. read, write fonksiyonlarıyla I2C üzerinden okuma ve yazma işlemlerini gerçekleştir
  4. Dosyayı kapat

Şimdi bu adımlara daha yakından bakalım. Her bir adıma ilişkin örnek kod aşağıdaki gibidir.

Aygıt dosyasını aşağıdaki gibi açabiliriz.

/* I2C aygıt dosyasının /dev/i2c-1 olduğu varsayılmıştır */
#define I2C_DEVICE "/dev/i2c-1"

int i2c_fd;
i2c_fd = open(I2C_DEVICE, O_RDWR);
if (i2c_fd < 0) {
    perror("Opening " I2C_DEVICE);
    exit(1);
}

İletişime geçilecek slave aygıt ioctl ile aşağıdaki gibi belirtilebilir. I2C_SLAVE istek kodu, slave adresinin gönderildiğini belirtmektedir. Fonksiyon prototipi ve örnek kullanımı şu şekildedir:

int ioctl(i2c_fd, I2C_SLAVE, SLAVE_ADDR);
/* Slave adresinin 0x20 olduğunu kabul edelim. */
#define SLAVE_ADDR 0x20

int rc;
rc = ioctl(i2c_fd, I2C_SLAVE, SLAVE_ADDR);
if (rc < 0) {
    perror("ioctl slave addressing");
    exit(1);
}

Devamında I2C üzerinden read çağrısı ile 1 byte bilgi okuyalım:

char data[] = {0};
rc = read(i2c_fd, data, 1)
if (rc < 0) {
    perror("reading");
    exit(1);
}

I2C üzerinden 1 byte bilgi gönderelim:

char data[] = {0};
rc = write(i2c_fd, data, 1)
if (rc < 0) {
    perror("writing");
    exit(1);
}

Bu yöntem oldukça anlaşılır ve basit olmasına karşın, her bir yazma ve okuma işlemi ayrı ayrı yapılmalıdır. Tek seferde, atomik olarak, yazma ve okuma işlemleri yapılamamakta, ayrıca her bir okuma ve yazma işleminde I2C protokolündeki bütün aşamalardan geçilmektedir. Yani her bir okuma veya yazma işleminde önce başlangıç durumu oluşturulmalı, slave aygıt yeniden adreslenmeli, bilgi alışverişi sağlanmalı ve iletişim sonlandırılmalıdır. Bir sonraki yöntemde ise bu dezavantajlar bertaraf edilmiştir.

ioctl Çağrısı İle Okuma Okuma/Yazma

Bu yöntemde read, write çağrıları yerine sadece ioctl çağrısı kullanılmaktadır. Bu sayede iletişim sonlandırılmadan birleşik (combined) okuma/yazma işlemleri yapılabilmektedir. I2C aşamalarını anlattığımız bölümdeki başlangıç aşamasının tekrarlanması (repeated start) bahsini hatırlayınız.

Bu yöntem için, i2c-tools kaynak kodundan çıkan, i2c-dev.h başlık dosyasına ihtiyaç duyulmaktadır. i2c-tools, I2C ile ilgili birçok aracı barındıran açık kaynak kodlu bir projedir. Bu aşamada i2c-dev.h içindeki sadece sembolik sabitlere ve structure tanımlarına ihtiyaç duymaktayız.

Not: i2c-tools kaynak koduna aşağıdaki gibi erişebilirsiniz.

wget -c https://github.com/groeck/i2c-tools/archive/master.zip

Ayrıca kernel içinde de i2c-dev.h isimli bir başka başlık dosyası daha bulunmaktadır. Bu dosya kernel içerisindeki I2C sürücüsü tarafından kullanılmakta olup, uygulama örneğimizde bu başlık dosyasını değil, i2c-tools paketinden çıkanı kullanacağız.

İlk olarak, okuma veya yazma yapılacak tampon alanlarının başlangıç adresleri, uzunlukları ve iletişim kurulacak slave adreslerini gösteren veri alanı hazırlanmalıdır. Bu amaçla, i2c-dev.h içinde i2c_rdwr_ioctl_data isimli bir yapı türü tanımlanmıştır. ioctl çağrısına, bu yapı türünden bir adres geçirilmelidir.

i2c_rdwr_ioctl_data, gerekli verilere erişimi sağlayan, i2c_msg isimli bir yapı elemanına sahiptir.

/* I2C_RDWR ioctl çağrılarında kullanılan yapı */
struct i2c_rdwr_ioctl_data {
    struct i2c_msg *msgs;   /* mesajlaşmak için gerekli verilerin başlangıç adresi */
    __u32 nmsgs;            /* mesaj sayısı */
};

 /* I2C_RDWR ioctl çağrısı için gerekli mesaj alanlarını tutan yapı */
struct i2c_msg {
    __u16 addr;             /* slave adresi */
    unsigned short flags;   /* Okuma yazma ve diğer seçenekler */
#define I2C_M_TEN   0x10
#define I2C_M_RD    0x01    /* Okuma işlemi yapılacağını gösteren sembolik sabit */
#define I2C_M_NOSTART       0x4000
#define I2C_M_REV_DIR_ADDR  0x2000
#define I2C_M_IGNORE_NAK    0x1000
#define I2C_M_NO_RD_ACK     0x0800
    short len;              /* tampon alanın uzunluğu */
    char *buf;              /* tampon alanın başlangıç adresi */
};

Örnek bir kullanım aşağıdaki gibidir, flags'e atanan I2C_M_RD okuma, 0 değeri ise yazma işlemi yapılacağını gösterir.

#define I2C_DEVICE "/dev/i2c-1"
#define SLAVE_ADDR 0x20

int i2c_fd;
struct i2c_rdwr_ioctl_data msgset;
struct i2c_msg iomsgs[2];

/*yazma işleminde kullanılacak tampon alanı*/
unsigned char wbuf[1] = {0x15};

/*okuma işleminde kullanılacak tampon alanı*/
unsigned char rbuf[1];

int rc;

int i2c_fd;
i2c_fd = open(I2C_DEVICE, O_RDWR);
if (i2c_fd < 0) {
    perror("Opening " I2C_DEVICE);
    exit(0);
}

iomsgs[0].addr = SLAVE_ADDR;
/*yazma işlemi*/
iomsgs[0].flags = 0;
iomsgs[0].buf = wbuf;
iomsgs[0].len = 1;

iomsgs[1].addr = SLAVE_ADDR;
/*okuma işlemi*/
iomsgs[1].flags = I2C_M_RD;
iomsgs[1].buf = rbuf;
iomsgs[1].len= 1;

msgset.msgs = iomsgs;
/*mesaj sayısı*/
msgset.nmsgs = 2;

rc = ioctl(i2c_fd, I2C_RDWR, &msgset);
if (rc < 0) {
    perror("ioctl I2C_RDWR");
    exit(0);
}

Örnekte, 0x20 adresli slave aygıta 1 byte'lık 0x15 değeri gönderilmiş, ardından iletişim sonlandırılmadan 1 byte'lık veri okunmuştur.

i2c-dev.h Fonksiyonları İle Okuma/Yazma

ic2-tools paketinden çıkan i2c-dev.h başlık dosyası ayrıca, i2c_smbus_ önekiyle başlayan, inline fonksiyon tanımları da içermektedir. Veri okuma ve yazmaya ilişkin en temel fonksiyon kümesi aşağıdaki gibidir:

__s32 i2c_smbus_read_byte(int file);
__s32 i2c_smbus_read_byte_data(int file, __u8 command);
__s32 i2c_smbus_write_byte(int file, __u8 value);
__s32 i2c_smbus_write_byte_data(int file, __u8 command, __u8 value);
__s32 i2c_smbus_read_word_data(int file, __u8 command);
__s32 i2c_smbus_write_word_data(int file, __u8 command, __u16 value);

Bu fonksiyonlar SMBus protokolü ile I2C veriyolunu kullanmaktadır. SMBus, 1995 yılında Intel tarafından tanımlanmış, I2C protokülünü temel alan bir protokoldür. I2C aygıtları, garanti olmamakla birlikte, çoğunlukla bu protokolü de desteklemektedir. Bir sonraki bölümde SMBus desteğinin nasıl kontrol edileceğinden bahsedeceğiz.

İsminde byte geçen fonksiyonlar 1 byte, word geçenler ise 2 byte'lık okuma veya yazma fonksiyonlarıdır. Ayrıca, sonu _data ile biten fonksiyonlar mevcuttur. Örneğin;

__s32 i2c_smbus_read_byte(int file);
__s32 i2c_smbus_read_byte_data(int file, __u8 command);

i2c_smbus_read_byte, slave aygıttan 1 byte veri okumaktadır. i2c_smbus_read_byte_data ise öncesinde 1 byte'lık bir veri göndermekte, sonrasında cevap olarak gelen veriyi okumaktadır. Master aygıt ilk olarak slave aygıta okuma işlemine ilişkin bir komut göndermekte ve sonrasında gelen veriyi okumaktadır. Örneğin, master tarafından gönderilen bilgi, okuma yapılacak olan slave aygıtın register adresi olabilir. İlerideki örneklerimizde bu kullanıma değineceğiz.

Fonksiyonlardaki komut parametresine ilave olarak kullanılan file ve value parametreleri ise sırasıyla, open ile elde edilmiş handle değerini ve slave aygıta gönderilecek değeri göstermektedir.

Parametre Anlamı
file open ile elde edilen handle
command Okuma veya yazma öncesi gönderilen komut
value Gönderilecek veri

Sürücü Desteğinin Test Edilmesi

Kullanılacak I2C sürücüsü, tanımlı tüm I2C özelliklerini desteklemeyebilir. Bu sebeple uygulama içinde kullanılacak olan özelliklerin desteklenip desteklenmediğini kontrol etmek doğru bir yaklaşım olacaktır.

I2C_FUNC ioctl çağrısı ile sürücünün yetenekleri hakkında bilgi alınabilir. ioctl çağrısına ayrıca long türünden bir değişkenin adresi geçirilmelidir. Desteklenen özellikleri temsil eden değer bu adrese yazılacaktır. Örnek bir kullanım aşağıdaki gibidir:

long funcs;
int rc;
rc = ioctl(fd, I2C_FUNCS, &funcs);
if (rc < 0) {
    perror("ioctl I2C_FUNCS");
    exit(0);
}

Bu aşamadan sonra func değerini, i2c-dev.h içindeki sembolik sabitlerle bit and işlemine tabi tutarak herhangi bir özelliğin desteklenip desteklenmediği bilgisine erişebiliriz.

Örneğin I2C_FUNC_I2C sembolik sabiti en temel seviyedeki I2C komutlarının desteklendiğini göstermektedir. Bu desteğin mevcut sürücüde var olup olmadığını aşağıdaki gibi kontrol edebiliriz:

if (!(funcs & I2C_FUNC_I2C)) {
    puts("i2c support not available");
}

Diğer bazı sembolik sabitler ise aşağıdaki gibidir.

Sembolik Sabit Anlamı
I2C_FUNC_I2C Temel I2C komut desteği
I2C_FUNC_10BIT_ADDR 10bit'lik Slave adres desteği
I2C_FUNC_SMBUS_BYTE SMBus read_byte ve write_byte desteği
I2C_FUNC_SMBUS_BYTE_DATA SMBus read_byte_data ve write_byte_data desteği
I2C_FUNC_SMBUS_WORD_DATA SMBus read_word_data ve write_word_data desteği

I2C Bus Hızının Değiştirilmesi

Linux tarafında I2C bus hızını değiştirmek isterseniz, kullandığınız kartın I2C sürücü kaynak kodlarını incelemeniz gereklidir. Temel olarak aşağıdaki senaryolarlardan biriyle karşılaşacak olsanız da en doğru kaynak kullandığınız I2C sürücüsünün kodu olacaktır.

Bazı kartlarda I2C desteği modül olarak derlendiğinde, modül yükleme aşamasında bir parametre vermek suretiyle bus hızı değiştirilebilmektedir. Örnek olarak Raspberry Pi kartında I2C sürücüsüne ait modül ismi i2c_bcm2708 şeklinde olup modül yükleme aşamasında aşağıdaki gibi baudrate parametresini vermek suretiyle değiştirilebilmektedir:

# modprobe  i2c_bcm2708 baudrate=400000

Modül şeklinde yapılıyor olmasının bir avantajı da, modülü sistemden kaldırıp (modprobe -r i2c_bcm2708) ardından farklı bir baudrate parametresi ile yeniden yükleme imkanının bulunmasıdır.

Bazı kartlarda ise baudrate değeri sürücü kodu içerisinde sabit olarak tanımlanmıştır. Platformunuzda bu şekilde bir I2C sürücüsü kullanıyorsanız tek yöntem, sürücü kaynak kodunu değiştirerek sistemi yeni derlediğiniz çekirdek ile açmak olacaktır.

Device Tree Block kullanan yeni nesil bir kart kullanıyor iseniz, kartınıza uygun dtb dosyasının üretim sürecinde kullanılan dtsi kaynak dosyalarında gerekli güncellemeleri yapıp, DTC derleyicisi ile dtb dosyasını yeniden üretmelisiniz.

Örnek bir dtsi dosyası içerisinde I2C bloğu aşağıdaki gibidir:

&i2c1 {
        clock-frequency = <100000>;
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_i2c1_2>;
        status = "okay";
        wm8903: wm8903@1a {
                compatible = "wlf,wm8903";
                reg = <0x1a>;

                clocks = <&clks 201>;
                amic-mono;
                gpio-cfg = <0xffffffff 0xffffffff 0 0xffffffff 0xffffffff>;
       };

};

Yukarıdaki örnekte clock-frequency değerini değiştirebilirsiniz.

results matching ""

    No results matching ""