Çalışma Frekansı Ayarı
Master mode’da, SCL ‘yi master ürettiği için çalışma frekansını biz belirliyoruz. EFM8SB1’de I2C modülü iki farklı zamanlama kaynağına ihtiyaç duyar: Biri zaman aşımı denetimi için diğeri clock frekansını belirlemek için (baud-rate generation gibi düşünün).
Bizim için ikincisi önemli. Bunun için, diğer modüllerde olduğundan farklı olarak birden çok seçeneğimiz var. TMR0 pek çok uygulamada genel amaçlı sistem saati ya da harici sayaç, TMR1 uart baud rate generator veya harici sayaç olarak kullanılabildiği için ben TMR2 kullanıyorum. Ayrıca, I2C çalışmasında gerekli taşma hızları çok yüksek olduğu için, 16 bitlik bir sayaca da ihtiyacımız yok. Değerli kaynakları boşa harcamamak için split mode çalışabilen bir timer kullanmak daha mantıklı.
Seçtiğimiz saat kaynağının kesmesini başka işlerde kullanmaya devam edebiliriz. I2C modülü arka planda taşma bayrağına bakarak I2C işlemlerini zamanlayabilir.
Kullanmak istediğimiz SCL frekansının 3 katı hızıyla tick üreten bir sayaç ayarlamamız gerek:

Split mode yaptığın bir sayacın alt yarısı ile üst yarısını kullanmak arasında küçük bir fark var: Sayacın açma/kapama kontrolü yalnızca üst yarıya kumanda eder. Alt yarı sürekli çalışır. Eğer üst yarıyı başka bir iş için kullanacaksak bu I2C için daha uygun bir paylaşım olur.
Program Tasarımı
Aşağıdaki zaman diyagramına bakın. Bu, bir sensörden veri okumak için yapılması gerekenleri gösteriyor:

I2C, ne yazık ki kendi başına iş görme yeteneği düşük bir haberleşme modülüdür. Yukarıdaki gibi, belli bir register’dan 2 byte veri okumasından ibaret bir işin SPI ile ne kadar kolay olacağını ve durumlar tanımlamak gerekmeden tek satırda bitebileceğini düşünün.
I2C’de işe yarar bir konuşma (transaction) yapmak için birden çok durum çalıştırmak gerekir. EFM8SB1’de bunları donanıma yaptırmak için kullandığımız kumanda bitleri tek bir register’da yer alıyor.
// I2C kumanda bitleri:
sbit SMB_SI = SMB0CN0^0;
sbit SMB_ACK = SMB0CN0^1;
sbit SMB_ARBLOST = SMB0CN0^2;
sbit SMB_STO = SMB0CN0^4;
sbit SMB_STA = SMB0CN0^5;
I2C konuşması fazla işlemci meşguliyeti yaratan bir iş olduğu için bu tür çevresel birimlere sürücü yazarken yaklaşımımız ana uygulamanın çok-görevlilik durumuna göre olmalı. Eğer I2C erişimi önceden bilinen zamanlarda yapılmayacaksa ve başka işlerle çakışması olasılığı varsa çok-görevli bir kodlama daha uygun olur. Çok görevlilik, I2C işlerini tüm durumları kapsayan bir kesme fonksiyonu içinde çalıştırmak şeklinde olabilir. Veya ayrı bir thread içinde bu işlerin kotarıldığı task’ler şeklinde bir durum makinesi yazılabilir. Birincisi için Silabs’ın örnek i2c kodlarına bakabilirsiniz. İkincisi benim genellikle tercih ettiğim yöntemdir. Daha büyük işlemcilerde genellikle i2c thread’i içinde erişimleri yazarım.
Benim bu yazıda paylaşacağım yaklaşım yukarıdakilerden biraz daha farklı. Programın asıl işinin zaten bu i2c modülüne erişim olduğu durumlarda bu yöntem daha efektif olacaktır. Burada esas olan, işlemcinin i2c erişimi yaparken başka işinin olmayacak olması. Ek olarak, i2c okuması uygulamanın zamanlamasını kesin olarak bildiği anlarda başlar ve biter. Örneğin, zamanının çoğunu uyku modunda geçiren ve uyanıp, i2c üzerinden bir sensörle haberleşen bir uygulama için bu yöntem biçilmiş kaftandır.
Genel Amaçlı Fonksiyonlar
Aşağıda paylaştığım i2c fonksiyonlarını doğru sıra ile çağırarak, herhangi bir slave aygıt için ihtiyaç duyulan sürücü fonksiyonları gerçekleştirilebilir. Bu fonksiyonlar blocking olarak çalışırlar. Yani, düz bir sıra ile verilen i2c okuma/yazma işlemini yaparlar.
// START durumu üretir ve ardından verilen slave adresini yazdırır (read ya da write)
bool I2C_Start(unsigned char slave_address)
{
SMB0CN0 = 0x20; // START durumu başlat!
while (SMB_SI == 0) ;
if (SMB_ARBLOST) return(false);
SMB0DAT = slave_address;
SMB0CN0 = 0;
return(true);
// fonksiyon, işlemin bitmesini beklemeden hemen döner..
}
// ilk erişimde, yazma adreslemesi sonrasında erişilecek
// çip adresini (/yollanacak komutu) gönderir:
bool I2C_Register_Set(unsigned char reg_addr)
{
while (SMB_SI == 0) ; // yazma adreslemesinin bitmesini bekle
if (SMB_ACK) // yazma adreslemesine ACK aldıksa..
{
SMB0DAT = reg_addr;
SMB_SI = 0;
return(true);
}
else
{
SMB0CN0 = 0x10; // STOP durumu üret
return(false);
}
}
// Start'tan farkı, devam etmek için önceki işlemin sonuçlanmasını bekler
// ve slave adres yüklemesi sonrası da slave'den ACK bekler
bool I2C_ReStart(unsigned char slave_address)
{
while (SMB_SI == 0) ; // önceki yazma işleminin bitmesini bekle
if (SMB_ACK == 0)
{
SMB0CN0 = 0x10; // STOP durumu üret
return(false);
}
// önceki yazmaya ACK almışsak devam edelim:
SMB_STA = 1;
SMB_SI = 0; // Repeated-Start üret
while (SMB_SI == 0) ;
if (SMB_ARBLOST)
{
SMB0CN0 = 0x10; // STOP durumu üret
return(false);
}
// repeated start sonrası, verilen slave adresini yolla:
SMB0DAT = slave_address;
SMB0CN0 = 0;
while (SMB_SI == 0) ;
if (SMB_ACK) // re-start adreslemesine ACK aldık mı?
{
// slave'e adres yazması sonrası modülü hold durumda bırakıp dön:
return(true);
}
else
{
SMB0CN0 = 0x10;
return(false);
}
}
// bu fonksiyon i2c denetimini bir önceki adımdan hold durumda devralmıştır
// (ilk okuma için read address yazması sonrası,
// sonraki okumalar için master ACK üretimi sonrası)
// ack = 1 olursa daha veri okunacak demektir.
// slave'e ACK üretilir ve i2c makinesi hold durumda iken fonksiyon döner.
// ack = 0 olursa stop durumu üretilir ve durum makinesi de boşa çıkar.
unsigned char I2C_Byte_Read(bool ack)
{
unsigned char x;
SMB_SI = 0; // state machine'i sal..
while( SMB_SI == 0)
if (ack) SMB_ACK = 1; else SMB_ACK = 0;
x = SMB0DAT; // gelen veriyi al.
if (!ack)
{
SMB0CN0 = 0x10;
while (SMB_SI == 0) ;
SMB_STO = 0;
}
return(x);
}
// kendisinden önceki yazma (pointer set / data yazma) devam ederken çağrılır
// geri döndüğünde de yükleme yapılan yazma sürüyordur.
bool I2C_Byte_Write(unsigned char b)
{
while (SMB_SI == 0) ; // önceki yazma işleminin bitmesini bekle
if (SMB_ACK == 0)
{
SMB0CN0 = 0x10; // STOP durumu üret
return(false);
}
SMB0DAT = x;
SMB_SI = 0;
return(true);
}
void I2C_Stop(void)
{
while (SMB_SI == 0) ; // önceki işlemin bitmesini bekle
SMB0CN0 = 0x10;
SMB_SI = 0;
while(SMB_SI == 0) ;
SMB0CN0 = 0;
}