Sayın
@Gokrtl'ın yoğun emeklerine dair bu başlıkta parça parça ürettiğim bazen neticesiz kalmış denemeler ve bunlara dair düşüncelerimi biraz daha elle tutulur hale getirmek üzere burada toparlamak istedim. Sayın
@Gokrtl'a ve katkısı bulunan diğer forumdaşlara saygı ve teşekkürlerimle...
Amaç
2si tam ve 2si ondalık olmak üzere 4 basamaklı bir sayıyı Rotary Encoder ile üreterek Serial monitörde yazdırmak.
Araçlar
1- Arduino UNO / Raspberry Pi Pico
2- Arduino IDE Serial Monitör
3- Rotary Encoder (with Push Button)
4- Arduino IDE Kodlama Ortamı
Araçların İncelenmesi
Arduino UNO / Raspberry Pi Pico
C dili ile kodlanabilen ve Arduino IDE üzerinden programlanabilen, doğrudan ve kod vasıtasıyla kontrol edilebilen Giriş-Çıkış pinleri ve pek çok işlemsel özellik barındıran geliştirme kartlarıdır. Zaten tanınıyor ve yaygın biçimde kullanılıyor olduğu için burada detaylandırmaya ihtiyaç görülmemiştir.
Arduino IDE Serial Monitör
USB üzerinden bağlı bulunan geliştirme kartı ile yapılan serial haberleşmesinin kullanıcı arayüzü ile görüntülenmesini sağlayan araçtır.
Rotary Encoder (with Push Button)
Rotary Encoder, üzerinde bulunan milin kendi ekseni etrafında dönüşü ile bu mile temaslı (A ve B isimli)
iki butonun aralarında 90° faz farkı bulunacak biçimde sıralı olarak tetiklenmesini sağlayan bir elektro-mekanik araçtır. Milin ekseni etrafında dönüşünde
tick adını verdiğimiz hareket meydana gelir. Ayrıca milin eksenine dik olarak yerleştirilmiş diğer bir
push button daha vardır.
a- Butonlar: A ve B butonu birer uçları birbirine ve Common isimli bir pine bağlıdır. A butonunun diğer ucu A Pini ve B butonunun diğer ucu B Pini olarak adlandırılır. Bu butonlar normal hallerinde açık-açık pozisyondadır.
Common pini A ve B pinlerinin geliştirme kartı üzerinde bağlanacağı pinlerin Pull-up yahut Pull-down yapılmasına bağlı olarak ters polarlanır. Yani A-B pinleri +5/3.3volta (HIGH/1) çekilirse Common 0volta (LOW/0); A-B pinleri 0volta (LOW/0) çekilirse Common +5/3.3volta (HIGH/1) çekilir. Böylece pinlerin tetiklenmeleri esnasında geliştirme kartında bağlı bulundukları pinlerdeki lojik durum terslendiğinden Encoder’ın hareketi geliştirme kartı tarafından algılanır.
b- Tick: Rotary Encoderler, üzerlerindeki milin 360°lik bir turu farklı sayıdaki eşit açısal aralıklarda tamamlanacak şekilde üretilebilmektedir. Bu projede kullanılacak Encoder ise 360°lik bir turu 20 adımda tamamlayabilmektedir. Bu adımların her birisi “tick” olarak isimlendirilir. Her tick, ilk hallerinde açık-açık pozisyonda bulunan A ve B butonlarında sırasıyla kapalı-açık, kapalı-kapalı, açık-kapalı ve açık-açık durumlarını üreterek tamamlanır. Görüldüğü üzere bir tick 4 farklı durum üretmektedir. Demek ki Encoder’ımız 360°lik bir turda toplam 80 farklı pin durumu üretebilmektedir. Bu da bir tick 18° ve her bir buton hareketi 4,5° açı bilgisi üretildiğini gösterir.
c- Push Button: Encoder üzerinde bulunan milin dikey eksende bastırılmasıyla temasa geçen diğer bir button daha vardır. Bu buton ise bir pini geliştirme kartına bağlanarak Pull-up yahut Pull-down yapılır. Diğer pini ise bu duruma ters şekilde polarlanarak butonun tetiklenmesine bağlı olarak geliştirme kartını pini lojik olarak tersleneceğinden hareket algılanır.
d- Rotary Encoder Modülü: Bu modülün hazırlanmasında bilhassa A ve B pinlerinin bir kondansatör ile debounce edilmesi hızlı dönüşlerde sorun meydana getirmekte. Çünkü kondansatörler sıklaşan palslerin arasında LOW-HIGH geçişini zamana yayarak olumsuz bir etki gösteriyor. Bunun kondansatörlerin t süresi üzerinden hesaplanmasıyla nasıl etkileri olduğu görülebilir. Nitekim Sayın
@Gokrtl tarafından paylaşılan osiloskop incelemesine dair videonun 15. saniyesinden itibaren bu etkinin palsler arasındaki 90°lik faz farkını etkilediği görülecektir. Ancak Buton pininin böyle bir kondansatörle debounce edilmesine ihtiyaç görülebilir. Diğer taraftan Modülde kullanılacak dirençler hakkında da bazı durtumları değerlendirmek icap ediyor. Modülde 10Kohm Pull-up direnci kullanıldığında Pico pinleri 50Kohm ile internal Pullup yapılırsa eşdeğer direncin 1/50+1/10 = 6/50 = 8,33 Kohm'a karşılık geldiğini görürüz. Arduino UNO'nun internal pullup dirençlerinin 20Kohm ile 50Kohm arasında olduğu belirtiliyor. Bu durumda 1/20+1/10 = 3/20 = 6,66Kohm ile 8,33 Kohm eşdeğer direnç gibi davranacaktır. Bu kadar düşük pullup direncinin modülün 1 tick hareketi esnasında meydana gelen gürültüye sebep olması mümkün gibi görünüyor.
Arduino UNO IDE Kodlama Ortamı
Bilindiği gibi bu ortam C ve C++ dilinde ve kendine özgü metotlarla üretilen kodun, bir derleyici vasıtasıyla makine diline çevirerek geliştirme kartına yüklenmesini sağlar.
Sistematik Çözümleme
Bilindiği gibi elektronik devreler harici bir müdahaleye ihtiyaç duymayacak biçimde tasarlandıklarında, içerdikleri komponentlerin yapılarına bağlı bir senkronizasyon ile çalışırlar. Burada kullandığımız Arduino UNO / Raspberry Pi Pico gibi geliştirme kartları ise temel komponent olan işlemci bakımından işlemlerini üretmek ve yürütebilmek adına diğer unsurlarla senkronizasyonu sağlamak üzere kendi saatlerini kullanırlar.
Ancak dışarıdan işlemcilerin bu zamanlamasına senkronize yanıt veremeyecek müdahaleler öngörülüyor ise işlemcinin kendi zamanlamasını bu müdahaleye uydurması sağlanmaktadır. Bu müdahaleler neler olabilir? Bir kullanıcın sisteme işleme ihtiyacı duyacağı veriler, işlemci tarafından kontrol edilen çeşitli elektronik ve mekanik araçların durumlarına dair veriler olabilir. Ancak SPI, I2C, UART, Serial gibi haberleşme biçimleri işlemcilerin iletişime geçtikleri diğer cihazlarla aralarında senkronizasyon temin ederek gerçekleştiğinden burada çözümlenmesi gereken konular değildir. Bu bahiste ana mesele şöylece şekillenmektedir:
1- Rotary Encoder ile kullanıcı tarafından üretilen hareketin işlemci tarafından doğru algılanması,
2- Bu hareketin sonucunun görüntülenmesi yoluyla kullanıcı tarafından doğruluğunun teyit edilebilmesi,
3- Hareketle üretilen veriler üzerinden amaçlanan işlemlerin gerçekleştirilmesi.
Bu üç meselenin temelinde yatan nokta şudur. Biz insanlar, saniyenin 1/6’sından daha kısa sürede meydana gelen olayları algılayamıyor ve tepki üretemiyoruz. Her ne kadar bazı ritmik durumlarda bu türden kısa süreleri yakalayabilsek de saniyenin 1/100’ünden 1/1000000’una kadar kısa zamanlarda işlem gerçekleştiren elektronik cihazlarla doğrudan iletişim kurabilmemiz imkânsız. Dolayısıyla bu cihazlar insanlar tarafından kullanıma elverişli arayüzlerle donatılır ve cihazların, bu arayüzler üzerinden aldıkları tepkilere zamansal olarak hizalanması temin edilir. Burada söz, insan-işlemci senkronizasyon vasıtası olan Hardware Interrupt’a gelmektedir. Ki bu vasıta işlemciler tarafından pek çok çeşit cihazın kontrolünde de kullanılmaktadır.
Esasında Arduino gibi geliştirme ortamlarında bir Interrupt’a bağlı olmadan da pek çok buton ve encoder uygulaması tasarlanıp çalıştırılabilmektedir. Öyleyse neden Interrupt gibi karmaşık olaylar silsilesi bir konuya girmek durumunda kalıyoruz? Şöyle izah edelim:
1- Ana kod bloğunu oluşturan Main Loop içerisinde bir butonun durumunu kontrol etmek için bazı yöntemler bulunmakta: Maksat bunlardan birisi olan IF-ELSE yapısıyla Main Loop’un her döngüsünde bir defa kontrol ederek; diğeri olan WHILE yapısıyla da butonda bir hareket meydana geldiğini kaçırmamak üzere Main Loop döngüsünü bekleterek gerçekleşir.
Aşağıda iki yöntem üzerinden örnek verilecek olup önce ikisinde de kullanılması muhtemel değişken tanımlamalarını yapalım:
Kod:
int Button = 3;
int Led = 4;
void setup() {
pinMode(Button, INPUT_PULLUP);
pinMode(Led, OUTPUT);
digitalWrite(Led, LOW);
}
a- IF-ELSE ile buton kontrolü: Burada Main Loop’a ait diğer kod örneklerine değinilmeyecektir.
Kod:
void loop() {
if (Button == LOW) {
digitalWrite(Led, HIGH);
}
//Diğer kodlar
}
Burada örneği verildiği şekliyle butonun basılı olup-olmadığı, Main Loop’un her döngüsünde bir anlığına kontrol edilir. Main Loop’un her döngüsü ne kadar zamanda tamamlanırsa butondaki hareket de ancak o kadar sürede bir kontrol edilebilir. Yani main loop 1 saniye sürerse butona basıldığı saniyede 1 defa kontrol edilebilir. Bizim ise butona bastığımızda 1 saniye kaybetmek istemiyorsak main loop döngüsünün tam da bu kontrol koduna geldiği ânı yakalamamız gerekir.
b- WHILE ile gerçekleştirilen diğer çözüm, IF-ELSE bloğu ile yapılan kontrolünde temeli oluşturan “zamanını yakalama” problemini aşıyor gibi. Bu kod ise şöyledir:
Kod:
void loop() {
WHILE (Button == HIGH) {
delay(100);
}
pinMode(Led, OUTPUT);
//Diğer Kodlar
}
Burada görüleceği üzere butonun basıldığını anlamak üzere Main Loop döngüsü bekletilmektedir. Ancak buton basıldığında gerekli işlem yapıldıktan sonra Main Loop’un döngüsünü devam ettirmesine izin verilir.
Bu iki yöntemin de temelinde yatan problem bir TRIGGER problemidir. Yani bize butonun basıldığını takip ederek haber verecek bir tetikleyici eksikliğidir. Yazılım ve özellikle elektronik cihaz haberleşmelerine aşina olanlar TRIGGER kavramını da biliyorlardır. İşte Hardware Interrupt, bu türden bir TRIGGER görevi görmekte, programın ana döngüsündeki akışı herhangi bir aksamaya uğratmadan buton vesair olaylarda işlemci zamanlamasını bu olayların zamanına hizalamaktadır.
2- Interrupt karmaşasına girmek üzere ikinci nedenimiz ise, Main Loop gibi yoğun kodlar arasında yaşanacak aykırı durumlarda bizim buton kontrolümüze ne şekilde sıra geleceğinin çoğu kere öngörülebilir olmamasıdır. Oysa işleyişi doğru kavrandığında Hardware Interrupt, buton, Rotary Encoder vb. ihtiyaçların hemen hepsinde öngörülebilir ve sistem kaynaklarını doğru kullanabilen bir vasıtadır. Bu karmaşayı aşmanın yolu ise Interrupt işleyişi ile buna bağlı kullanılacak komponentlerin iyi kavranmasıdır.
Gelin Interrupt-Rotary Encoder entegrasyonunu bu bağlamda değerlendirelim.
Interrupt-Rotary Encoder Entegrasyonunu
Rotary Encoder Çalışma Mantığı ve Vazifesi
A- Dönüş Hareketinin (Tick) Kullanımı:
Daha yukarıda anlattığımız gibi Rotary Encoder bir tick esnasında A ve B pinlerinin durumu itibariyle şu çıktıları verir. A-B pinleri Pull-up yapıldığı ve Encoder Saat yönünde döndürüldüğünde:
Ân | A Pini (CLK) | B Pini (DATA) | BINARY |
0 | 1 | 1 | 11 |
1 | 0 | 1 | 01 |
2 | 0 | 0 | 00 |
3 | 1 | 0 | 10 |
A-B pinleri Pull-up yapıldığı ve Encoder Saatin tersi yönde döndürüldüğünde:
Ân | A Pini (CLK) | B Pini (DATA) | BINARY |
0 | 1 | 1 | 11 |
1 | 1 | 0 | 10 |
2 | 0 | 0 | 00 |
3 | 0 | 1 | 01 |
Görüleceği üzere bir tick sürecinde Encoder’dan 4 defa veri alınabilmekte ve buna binaen bir Tick’te 4 işlem yapma imkânı bulunmaktadır.
2si tam ve 2si ondalık olmak üzere 4 basamaklı bir sayının her basamağını ayrı ayrı ayarlamak söz konusu olduğuna göre:
1- Tamamlanması neredeyse 1 saniye dahi sürmeyen 1 tick ile basamak rakamlarında 4 artış gerçekleştirmek pek de kullanışlı değildir.
2- Saniyenin ¼’lük kısmında ürettiğimiz her bir rakamın ekranda yazdırılmasıyla kaybolması aynı kısa sürede gerçekleşeceğinden Encoder ile elde edilen etkinin gözlenmesi zorlaşacaktır.
3- Bir basamak değeri 3’e ayarlanacak olursa Encoder milinin, bir Tick’in ¾’lük kısmında sabitlenmesi gerekecektir. Ancak Encoder mili bir tick tamamlanacak şekilde tasarlanmış olduğundan bu da mekanik olarak imkân dışı bulunacaktır.
4- Diğer taraftan biz bu dört basamaklı sayıyı basamak basamak ayırmadan sürekli encoder çevirerek oluşturmak isteseydik, bir Tick'te 4 işlem yapma imkânı bize büyük katkı sunardı. 00.00-39.99 arasındaki 4000 farklı sayıyı bir turda 80 (1 tur 20 Tick, 1 Tick 4 defa işlem yapma) veri üreten encoder ile 50 turda oluşturabilirdik. Ancak yapmaya çalıştığımız şey her basamakta 0-9 arasında toplamda 10 farklı değer döndürmek olduğuna göre bu kadar baş döndürücü bir hıza zaten ihtiyaç da yok.
Görüldüğü üzere hem döndürme hareketinde kendi kontrolümüzü sağlamak hem de Rotary Encoder üzerinden çalıştıracağımız Interrupt kesmelerini sağlıklı kullanabilmek önemlidir. Bu nedenle Interrupt Mode (yani tetiklenme biçimi) ayarında, bir Tick’te 1 rakam ilerleme çözünürlüğü sağlayacak tercihte bulunmamız gerekiyor. CLK görevi üstelenen A pinimizi geliştirme kartımızın 0 nolu Interrupt pini olan 2 nolu dijital pine bağlayacağız. Ve bu pini de Pull-up yapacağız.
Yukarıdaki iki tabloda vurgulandığı üzere, CLK pini FALLING hâlinde iken DATA pini 1 ise Saat Yönünde; DATA pini 0 ise Saatin Tersi Yönde bir dönme hareketi tespit edilmektedir. Bu nedenle CLK pininin FALLLING hâlinde DATA pininin durumunun okunabilmesi gerektiğinden onu da geliştirme kartımızın 7 nolu dijital pinine bağlıyoruz.
Bu da A pininin 0’dan 1’e geçtiği 1. ânı bize gösteren FALLING hâlidir. A pini FALLING durumuna uğradığında çalışacak fonksiyonumuz ise Encoding() olacaktır.
B- Push Button Kullanımı
Rotary Encoder’ın dönüş hareketiyle basamaklara gelen sayıları değiştireceğiz. Ancak hangi basamakta işlem yaptığımızı ise Encoder’ın butonuna basarak seçeceğiz. Bu nedenle Push Button pinlerinden birisini geliştirmek kartımızın 1 nolu Interrupt pini olan 3 nolu dijital pine bağlayacağız. Bu pin Pull-up yapılacaktır. Diğeri ise GND’ye bağlanarak butona tıklanması durumunda bağlı bulunduğu Interrupt pinini terslemesi sağlanacaktır. Böylece buton hareketi algılanacaktır. Butona dair Interrupt ise Bt_Pushed fonksiyonunu çağıracaktır.
Buraya kadar elde ettiğimiz veriler üzerinden pin tanımlamaları ve Interrupt yapılandırmasına ait kodları şöylece düzenleyebiliriz:
C- Rotary Encoder Pinlerine Dair Kodlar
Encoder Pinlerine Dair Değişkenler
Kod:
int RE_A = 2;
int RE_Bt = 3;
int RE_B = 7;
Encoding() fonksiyonu içerisinde B pininin durumunu tutacak olan PinB değişkeni:
Dönüş yönü bilgisini taşıyacak olan değişken tanımlanıyor:
Tick Gerçekleştiği Bilgisini taşıyacak değişken:
Kod:
volatile bool ticked = 0;
Butona Basıldığı Bilgisini taşıyacak değişken:
Kod:
volatile bool pushed = 0;
Buton ile işlenecek basamak verisine tutacak değişken tanımlanıyor:
Kod:
volatile int BasamakNo = 0;
Basamaklara ait rakam verisini taşıyacak değişkenler tanımlanıyor:
Kod:
int basamak0 = 0, basamak1 = 0, basamak2 = 0, basamak3 = 0;
Encoder ile üretilerek Serial monitöre yazdırılacak sayıyı tutacak değişken tanımlanıyor:
setup() fonksiyonu:
Encoder Pinlerinin yapılandırılması:
Kod:
pinMode(RE_A, INPUT_PULLUP);
pinMode(RE_Bt, INPUT_PULLUP);
pinMode(RE_B, INPUT_PULLUP);
Interrupt Kesmelerinin yapılandırılması:
Kod:
attachInterrupt(digitalPinToInterrupt(RE_A), Encoding, FALLING);
attachInterrupt(digitalPinToInterrupt(RE_Bt), BT_Pushed, FALLING);
Serial Monitör kurulumu:
Encoding ve Bt_Pushed ISR Fonksiyonları
A- Encoding()
Encoder mili dönme hareketine başladığı anda CLK pini LOW’a çekilecek ve Encoding() isimli fonksiyon çağırılacak. Bu nedenle Encoding() fonksiyonu yazılıyor:
RE_A şeklinde isimlendirilen CLK pininin FALLING hâlinde fonksiyon çağırıldığına göre RE_B ile isimlendirilen DATA (B) pininin durumunu kontrol ederek dönüş yönünü tespit edilecek bu nedenle önce bu pinin durumu okunarak PinB değişkenine atanıyor:
Kod:
PinB = digitalRead(RE_B);
Interrupt kesmesi meydana geldiğinde işlemler tamamlanana kadar başka kesme meydana gelmemesi amacıyla tüm kesmeler deaktive ediliyor. Kesmeler, Interruptlar ile amaçlanan işlemler gerçekleştirildiğinde yeniden aktive edilecek:
RE_A şeklinde isimlendirilen CLK pininin FALLING hâlinde fonksiyon çağırıldığına göre RE_B ile isimlendirilen DATA (B) pininin PinB değişkenine aktarılan durumu kontrol edilerek dönüş yönü tespit ediliyor:
Kod:
if (PinB == 1) {Yon = “SAG”;} {Yon = “SOL”;}
Tick gerçekleştiği bilgisi ilgili değişkene işleniyor ve Encoding() fonksiyonundan çıkılıyor:
B- Bt_Pushed()
Encoder butonuna basılmasıyla RE_Bt pini LOW’a çekileceğinden Bt_Pushed() isimli fonksiyon çağırılacak. Bu nedenle Bt_Pushed() fonksiyonu yazılıyor.
Interrupt kesmesi meydana geldiğinde işlemler tamamlanana kadar başka kesme meydana gelmemesi amacıyla tüm kesmeler deaktive ediliyor. Kesmeler, Interruptlar ile amaçlanan işlemler gerçekleştirildiğinde yeniden aktive edilecek:
Butona basıldığı bilgisi pushed değişkenine ve BasamakNo değişkenine işleniyor:
Kod:
pushed = 1;
BasamakNo ++;
BasamakNo değişkeni proje amacında bahsedilen 4 basamaklı sayının basamaklarını seçmek üzere kullanılacağından 0 ile 3 arasında 4 farklı değer alacak şekilde IF-ELSE bloğu ile düzenleniyor ve Bt_Pushed() fonksiyonundan çıkılıyor:
Kod:
if (BasamakNo >=3) { BasamakNo = 0}
}
Diğer Fonksiyonlar
Burada Interruptlarla ilgili zaman gecikmelerine ihtiyaç duyulması hâlinde kullanılacak bir Gecikme() fonksiyonu tanımlanıyor. Buna gerek duyulmasının sebebi Interruptlar içerisinde delay(), millis(), micros() gibi bilinen diğer zaman fonksiyonlarının çalışmaması. Bu fonksiyonda Interrupt esnasında çalışabilen tek zaman fonksiyonu olan delayMicroseconds() kullanılacaktır.
Interrupt kesmelerinin aktive ve deaktive edilmesi birkaç yerde tekrar edeceğinden kod karmaşasına yol açmamak üzere bunların birer fonksiyonla çağrılması amaçlandı.
A- Gecikme()
Fonksiyon içerisinde, çağrıldığı zaman ne kadar süre gecikme sağlanmasını hesaplamakta kullanılacak bir de değişken tanımlaması yapılıyor.
Kod:
void Gecikme(volatile int Sure){
Gecikmeyi sağlayacak olan for döngüsü kurularak, delayMicroseconds() fonksiyonu ile 1 milisaniye karşılığı 1000 mikrosaniye gecikme ayarlanıyor. Bu sayede fonksiyon çağrıldığında belirtilecek Sure değişkeni kadar milisaniye gecikme temin edilecektir. Sonra da fonksiyondan çıkılıyor:
Kod:
for (int a = 0; a <= Sure; a++) {delayMicroseconds(1000);}
}
B- IrqStart() ve IrqStop()
IrqStart() fonksiyonu tanımlanıyor ve Interrupt kesmelerini yapılandıran ve aktive eden kodlar eklenerek fonksiyondan çıkılıyor:
Kod:
void IrqStart(){
attachInterrupt(digitalPinToInterrupt(RE_A), Encoding, FALLING);
attachInterrupt(digitalPinToInterrupt(RE_Bt), BT_Pushed, FALLING);
}
IrqStop() fonksiyonu tanımlanıyor ve Interrupt kesmelerini deaktive eden kodlar eklenerek fonksiyondan çıkılıyor:
Kod:
void IrqStop(){
detachInterrupt(digitalPinToInterrupt(RE_A));
detachInterrupt(digitalPinToInterrupt(RE_Bt));
}