AVR UART Kullanımı Bölüm 2

C ile AVR UART :

Önceki bölümde UART kullanımını anlatmaya çalıştım. Bölümün sonunda paylaştığım kodun karmaşık bir program içinde işe yaramayacağını belirttim. Bunun nedenini aşağıdaki kodlarda görebilirsiniz.

While (loop) içine 1 saniye aralıklarla ledin yanıp sönmesini sağlayan bir ilave yaptım. “delay” kullanıldığında mikro denetleyici hiçbir işlem yapmıyor. Mecbur kalmadıkça benzer işlemleri zamanlayıcı ile yapmak doğru olacaktır. Bu notu da aktarmış olayım. Bu basit ilave ve 2 saniyelik bekleme, gelen verinin tam olarak alınamamasına neden olacaktır. Delay içinde ya da başka bir fonksiyon içinde komutlar çalışırken gelen verinin alınamaması normaldir. Bu durumda yapılması gereken kesmeleri kullanmaktır. Bazen bir şeye daldığınızda size seslenen insanları duymazsınız ya da söylenenleri anlamazsınız. Kişinin gelip dokunmasıyla ancak daldığınız konudan sıyrılıp cevap verebilirsiniz. Mikro denetleyici de aynı şekilde ana programı döngü halinde satır satır işlerken ve hatta bu satırlardan bazıları durmasını söylerken dışarıdan gelen çağrıları algılamayacaktır. Bunun yapmak için parmak ucuyla dürtmek (kesme/interrupt) gerekir.

Uart Kesme:

Kesme (interrupt) konusunu araştırdığınızda verilen örnekler de bu şekildedir. Komutlar işlenmeye devam ederken ayarlanmış/tanımlanmış kesme gerçekleştiğinde denetleyici kesme rutini içindeki satırları işler, sonrasında ana programda kaldığı satıra geri döner. Kesmeler kitap okurken zilin çalması kapıyı açmamız ve tekrar kitap okumaya kaldığımız yerden devam etmemiz gibi çalışır. Uart biriminde ayarlayıp kullanabileceğimiz 3 adet kesme bulunuyor.

USART_RX kesmesi veri alımı tamamlandığında, USART_TX veri gönderimi tamamlandığında ve USART_UDRE kesmesi veri kaydedici register UDR0 boş ise devreye girer. Bu kesmelerin nasıl ayarlanacağı konusunu açıklayayım. AVR de kesme ayarları ilgili register bitleri “1” yapılarak ayarlanır. Uart ya da başka bir konuda datasheet içinde hangi kesmenin hangi register ile yapılacağı belirtilmiştir. Önceki bölümde Uart kontrol ve durum registerlerini anlatırken UCSR0B registerini de tanımladım. Bu registerin 7,6 ve 5. Bitleri yukarıda saydığım kesmeleri aktif eder. USART_RX için RXCIE0, USART_TX için TXCIE0 ve son olarak USART_UDRE kesmesi için UDRIE0 bitlerini “1” yapmak gerekir. Örnek olarak “RX” kesmesi için “UCSR0B|=(1<<RXCIE0);”yazmamız gerekir. RX kesmesinin eklenmiş haliyle “uart_basla” fonksiyonu aşağıdaki gibi olacaktır.

Güncelleme

Baud rate 115200 ve üstü olduğunda iletişim sorunları çıkıyor. Düzeltme için UART hızı iki katına çıkartıyoruz. uart_basla() fonksiyonu güncel hali:

Kesme ayarlaması yapıldı ama kesmelerin açılması gerekiyor. Bunun için eklememiz gereken “#include <avr/interrupt.h>” isimli bir kütüphane var. SREG isimli durum registerinin  7. Biti “1” olursa kesmeler çalışır. Bunu elle ya da “sei();” (tüm kesmeleri açar) yazarak yapabiliriz. (“cli();” tüm kesmeleri kapatır)

Atık kesmeler ayarlandı ve açıldı. Şimdi kesme oluştuğunda denetleyicinin ne yapacağını bildirmemiz gerekiyor. Kesme rutini AVR de aşağıdaki şekilde yapılıyor.

Kesmelerle çalışırken dikkat edilmesi gereken bazı konular var. Kesme içinde başka bir fonksiyonun çağrılması doğru değil. Mümkün olduğunca az işlem yapılmalı olabildiğince hızlı ana programa dönülmeli. Kesme içinde tüm kesmeler kapatılır bu nedenle kesme gerektiren komutlar çalışmaz. Örnek olarak zamanlama kesmesi kullanan bir “delay” komutu kullanırsanız amacınıza ulaşamazsınız. Kesme içinde kesme ayarlanabilir. Kesme içinde kullanılacak olan global değişkenlerin “volatile” olarak tanımlanması gerekir. Kesme içinde tekrar yazılan değişkeni ana programda okumak istersek kesmeleri kapatmamız gerekir aksi halde daha biz okuyamadan kesme devreye girerek okumaya çalıştığımız değeri değiştirebilir.

Tüm bu işlemler sonunda kesme açıkken ana program ne olursa olsun RX bacağına veri geldiği anda tüm işlemleri bırakır gelen veriyi alır. USART_RX kesme rutini içine yazacağımız komutlar “uart_oku()” fonksiyonu gibi UDR0 registerindeki veriyi okuyacaktır. Aşağıda kesme içinde kullanılan değişken “volatile” olarak tanımlanmalıdır.

Uart Bellek Kullanımı:

Kesme kullanımı ile veri alımı konusunda bir kaybımız olmaz. Gelen veriyi bir değişken içine yazdık. Ama bu yazdığımız komutlar tam olarak yeterli olmayacaktır. Veri biz okudukça geliyorsa bir sorun olmaz ama sürekli geliyor ve ana programda biz bunu aynı hızda okuyamıyorsak yeni veriler öncekilerin üstüne yazılır ve biz doğru okuma yapamayız. Gelen veri sürekli kaydırılarak yazılmalı, bu sayede biz okuyana kadar veri doğru şekilde saklanmış olur. Bu kaydırma işlemi adresi belli olarak yapılmalı ki yazılan yeri bilelim ve okuyabilelim. Bunu dizi tanımlayarak ve gelen her veriyi dizinin bir elemanına kaydederek yapabiliriz. Aşağıda bir örneğini paylaşıyorum. uart_gelen isimli bir dizi ve gelen verinin sırasını gösteren uart_sira değişkeni tanımlandı. Her veri gelişinde kesme devreye girecek sıra numarası bir artacak ve diziye eklenecek. Veriyi okurken dizinin elemanları “uart_gelen[uart_sira]”  şeklinde okuyacağız.

“İf “ şartı ile sıra sınırlandırıldı böyle yapmamış olsaydık dizinin eleman sınırları dışına taşacak ve gelen veriyi okuyamayacaktık. Bu sınırlama ile sürekli başa dönmüş olduk. Veri yazarken bu sorun olmayacaktır. Belirlediğimiz limite gelince başa alacak fakat okuma yaparken uart_sira artacak ve biz en son yazılan verinin sırasını biliyor olacağız. Örnek olarak “a”,”b” ve “c” karakterleri uart ile alındığında uart_gelen []={“a”,”b”,”c”,\0} şeklinde diziye eklenecektir. Gelen bu veri aralıksız olarak gelecektir. Bizim ana programımızda mevcut olan beklemeler nedeniyle gecikmeli olarak okuma yapmamız sonucu uart_sira ilerleyecek ve “uart_gelen[0]”  ve “uart_gelen[2]”  okuyabileceğiz. “uart_gelen[1]”  yani “b” karakterini kaçıracağız. Daha büyük bir veri paketinde daha büyük kayıplar yaşanır.

Gördüğünüz gibi verilerde yazma sırası “uart_sira” ile okuma yapamayız. Bunun çözümü okumak için de bir sıra numarası belirlemektir. Böylece ilk kayıttan itibaren hiç atlama yapmadan alınan veriyi okuyabiliriz. Buna ilk giren ilk çıkar “first in first out” “FIFO” deniyor. Birçok farklı disiplin bu sistemi kullanıyor. Aşağıdaki tabloda 3 adet veri kaydı yapılmış yazma sırası “3” olmuş henüz okuma yapılmamış okuma sırası “1” veri gelmeye devam ederse yazma sırası artacaktır. Okuma yaptığımız zaman kaldığımız yerden devam edeceğimiz için atlama yapmadan tüm veriyi okuyabiliriz.

Yazma sırası 1 2 3 4 5 6 7 8 9
Gelen Veriler a b c
Okuma sırası 1 2 3 4 5 6 7 8 9

Bir özet geçersek veriyi bir belleğe yazıyoruz, bu belleğin sınırlarını tayin ediyor ve yazdığımız her veriyi bir sırayla ilerletiyoruz. Sona gelince tekrar başa alıyoruz aslında bir sonu olmayan döngü oluşturuyoruz. Yazılan veriyi okumak içinde bir sıra belirliyoruz aynı şekilde okumaya devam ettikçe sırayı ilerletiyoruz. Bu yaptığımız döngüye “ring buffer, circular buffer” deniyor. Türkçe olarak dairesel ya da halka bellek diyebiliriz.

Dairesel Bellek:

https://en.wikipedia.org/wiki/Circular_buffer 

Daha önce sıra olarak belirttiğim bölümleri adres olarak adlandıracağım. Yeni bir veri geldiğinde yazma adresi bir artarak sonraki adrese kayıt yapar. Veri gelmeye devam ettikçe adres artarak devam eder. Son olarak “i” karakteri aldık ve ayırdığımız belleğin sınırına geldik bu durumda adres başa alınacak ve yazmaya devam edilecektir. Ana program içinde okuma yapıldıkça okuma sırasını belirleyen adres de artacaktır.  

Yazma adresi 1 2 3 4 5 6 7 8 9
Gelen Veriler a b c d e f g h i
Okuma adresi 1 2 3 4 5 6 7 8 9

 

Veri gelmeye devam etti, adresi başa aldık yazmaya ve okumaya devam ettik. Birçok programda bu adres verilerinden yazma önde devam ettiği için “head” “baş” okuma arkadan geldiği için “tail” “kuyruk” olarak adlandırılıyor.

Yazma adresi 1 2 3 4 5 6 7 8 9
Gelen Veriler j k c d e f g h i
Okuma adresi 1 2 3 4 5 6 7 8 9

Bu döngü halinde devam edecektir. C diliyle bunu programa nasıl aktaracağız konusunu açıklayayım. Önceki bölümden hatırlayacağınız kesme rutini komutları aşağıdaki gibidir.

Bu komutları kısaca tekrar edeceğim. Bir bellek ve bu belleğin elemanlarını yazıp okuyabilmek için sıra/adres verisini tutan bir değişken tanımladık. Veri geldiğinde adresi artırdık ve belleğe yazdık. Bellek sınırına ulaşınca da sıfırlayarak başa döndük. Bu başa dönme sıfırlama işlemini farklı şekilde yapacağım. Daha öncede bahsetmiştim onluk sistemde “10”katları şeklinde devam ettikçe sona “0” ekliyorduk. 10, 100, 1000… şeklinde ikilik sistemde de “2” nin katlarına “0” ekliyoruz.

2=10 (eşitliğin sağı ikilik)

4=100

8=1000

16=10000

Şeklinde devam ediyor ve nasıl onluk sistemde “10” katlarının “1” eksiği 9, 99, 999, 9999… şeklinde devam ediyorsa ikilik sistemde de;

2-1=1

4-1=11

8-1=111

16-1=1111

Olarak devam eder. Bitsel “ve” “&” işleminde operatörün iki tarafında da “1” ise sonuç “1” olur. “1011&0111” işleminin sonucu “0011” dir. Bellek tanımlarken “2” nin katları şeklinde tanımlarsak. “#define UART_Rx_Boyut 16” bellek adres sınır “16” olacaktır ve bu sınıra gelince sıfırlamak gerekir. Bunun için “&” operatörünü kullanıyoruz. Bunu yapmak için bellek sınırının bir eksiğini bit mask olarak kullanıyoruz. Bunun tanımlaması “#define UART_Rx_Mask (UART_Rx_Boyut-1)” şeklinde olacaktır. Bu tanımlara göre;

UART_Rx_Boyut=00010000 ///16

UART_Rx_Mask =00001111 /// 15 olacaktır. 

Adres 12=00001100 olsa “00001100&00001111=00001100” yine “12” olacaktır. Adres 16=00010000 olsa “00010000&00001111=0” olacaktır gördüğünüz gibi maskeleme yaparak sınıra gelince adresi sıfırlamış olduk. Aşağıda yeni adlandırılmış değişken ve kesme içi komutu görebilirsiniz.

“rx_ring[rx_bas]” veriyi yazdığımız bellek ve adresi belirleyen kodumuz bu şekilde “rx_bas” kesme ile “1” artırılıyor ve “&” işlemi yapılıyor, işlem sonucuna göre veri belleğe yazılıyor. Okuma işlemi içinde yapılanlar aynı şekildedir. “kuyruk” değişkeni okuma yaptıkça artırılır ve tekrar başa döndürülür.

“uart_oku()” fonksiyonu çağrılınca okuma adresini tutan “rx_son” “1” artırılır. Eğer baş ve son/kuyruk eşitse veri yok demektir. Değilse “rx_ring[rx_son]” adresteki veriyi döndürür. “(rx_son==rx_bas)” eşitlik sorgusunu kullanabilirsiniz. Arduino da meşhur Serial.available() fonksiyonu aynı sorgulamayı yapar. Aşağıda bu şekilde bir fonksiyon paylaşıyorum “uart_varsa()” 🙂

Bu bellek düzenlemesi ve kesme sayesinde veriyi kayıpsız alıp okuyabildik. Bellek sınırının düşük tutulması taşmalara neden olabilir. Bu durumu önlemek ve uart biriminin hata bayraklarının kontrol etmek için çeşitli sorgular yapabilirsiniz ben bu kısımları kullanmıyorum.

Veriyi almak için kesmeleri kullandık göndermek için yine kesme kullanabiliriz. Göndereceğimiz veriyi de almak için kullandığımız dairesel bellek gibi bellekte tutabiliriz. İlk olarak gönderim yaparken USART_UDRE kesmesini kullanıyoruz. Bu kesme UDRE0 boş olduğunda devreye giriyor. Bu yüzden bu kesmeyi açık tutamayız çünkü veri almıyor ya da göndermiyorsak UDRE0 boş demektir ve sürekli kesme yaşanacaktır. Veriyi gönderirken kesmeyi açıp gönderim sonrası kapatmalıyız.

İlk bölümde paylaştığım uart veri gönderme fonksiyonu yukarıdaki gibidir. Kesme kullanarak gönderim yaparken artık bu fonksiyonda kesmeyi açıp kapatan komut olmalıdır. Bunu kolay yoldan yapmak için makro tanımlıyorum.

“#define UART_Bos_On UCSR0B|=(1<<UDRIE0)” ve “#define UART_Bos_Off UCSR0B&=~(1<<UDRIE0)” tanımlamasını yapıyorum. Bu tanımlarda rx kesmesinde olduğu gibi udre0 kesmesi için ilgili bit “1” ve “0” yapılır. Gönderilecek veriyi gelen kısmında olduğu gibi giden ve gidecek gibi isimlendirdiğimizi farz edelim. Bu durumda giden “kuyruk” gidecek “baş”  olacaktır. Gelen kısmında olduğu gibi yine artarak devam edecek sınıra gelince başa dönecektir. Tanımlamalar ve fonksiyon bu şekle gelir.

Gördüğünüz gibi fonksiyonun sonunda kesmeyi açıyoruz UDRE0 boş olduğundan hemen kesme devreye girecektir. Kesme içi komutlar aşağıdaki gibi olacaktır.

Kesme içini incelediğimizde gidecek olan veri adresini tutan “kuyruk” “tx_son” artırılmış bellekteki veri UDR0 registerine yazılmış. Baş ve kuyruk (tx_son==tx_bas) eşit olana kadar bu devam ettirilmiş. Eşitlik sağlandığında yani bellekte gidecek veri kalmadığında kesme kapatılmış. Alırken olduğu gibi kayıpsız olarak veriyi göndermiş olduk. Aşağıda paylaştığım programı Arduino idesinde karta yükleyebilir serial ekran da deneme yapabilirsiniz.

Bu yazı Haluk ŞİMŞEK tarafından yazılmıştır. 

You may also like...

8 Responses

  1. emre dedi ki:

    merhaba ,string veri almak icin nasıl bir fonksiyon yazmalıyım?

    • C dilinde string ifadeler \0 işareti ile biter. Bu while döngüsü için şart ifadesi olarak kullanılıp sıra ile karakterler birer birer gönderilir ve alınır. Bir anda bütün string ifadeyi gönderme şansı yok bunu karakter karakter ele alacağız.

      • A.Kadir dedi ki:

        (Her VERİ gelişinde kesme devreye girecek sıra numarası bir artacak ve diziye eklenecek) olarak ifade etmiş oldığunuz VERİ belirlemiş olduğumuz 7-7 veye 9 bitlik paketlerimi kasdediyorusunuz gökhan bey.

        • Haluk Şimşek dedi ki:

          A. Kadir bey, tam olarak dediğiniz gibi UART başlangıçta ayarlayabildiğimiz kadar bitten oluşan veriyi kastediyorum. Genelde kullanılan 8 bit “bir karakter” den oluşan veri.

    • Haluk Şimşek dedi ki:

      Yukarıda UART bellek kullanımı başlıklı bölümde sorunuzun cevabını bulabilirsiniz.

  2. cemal dedi ki:

    Yukarıda bahsettiğiniz şeyler, usart bellek kullanımında kaydırarak yazma olayları vs, usart derslerinde paylaşılan uart kütüphanesinde mevcut değil mi? Ne olduğunu tam anlayamasam da kütüphane dosyasında benzer şeyler gördüm sanırım.

    • Haluk Şimşek dedi ki:

      Cemal bey, evet bahsettiginiz kütüphane içinde mevcut birçok kütüphanede de kullanılmaktadır. Burada amacım arka planda neler olduğunu aktarmaktır.

  3. cemal dedi ki:

    teşekkürler. gayet faydalı bir ders olmuş. emeğinize sağlık. dediğim kısmı anlamakta biraz zorlanıyorum ama bir çok defa okuyup anlayacağım inşallah. 🙂

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.