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


Ö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 (1) {
          PORTB|=(1<<PORTB5);
          _delay_ms(1000);
          PORTB&=~(1<<PORTB5);
          _delay_ms(1000);
          uart_gonder(uart_oku());
 }

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.

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.


void uart_basla(uint32_t baud){

       uint16_t baudRate=0;
       baudRate=(F_CPU/baud/16)-1;
       if (baud>=112500){
              baudRate=(F_CPU/baud/8)-1;
              UCSR0A|=(1<<U2X0);
       }
  UBRR0H=(baudRate>>8);
  UBRR0L=baudRate;  
  UCSR0B|=(1<<RXEN0)|(1<<TXEN0)|(1<<RXCIE0);
  UCSR0C|=(1<<UCSZ01)|(1<<UCSZ00);
  sei();
}
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.

ISR (Kesme_ismi_vect){
      Yapılacak işler;
}

 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.
ISR (USART_RX_vect){
      gelen=UDR0;
}

 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.

volatile uint8_t uart_sira=0;
volatile uint8_t uart_gelen[64];

ISR (USART_RX_vect){
      uart_sira++;
      uart_gelen[uart_sira]=UDR0; 
     
      if (uart_sira>=64){
            uart_sira=0;
      }          
}

“İ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
1
2
3
4
5
6
7
8
9
veri
a
b
c






okuma
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 yapısı oldukça kullanışlı ve kolay bir yöntemdir.



Çok uzun yazarak devam etmek istemiyorum o yüzden burada kesiyorum sonraki bölümde dairesel belleğin programa nasıl ekleneceğini açıklayarak devam edeceğim.

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



Bu bölümde AVR de UART (Evrensel asenkron alıcı verici) birimini anlatmaya çalışacağım. Öğrendiğim ve kullandığım kısımlarını anlatacağım. Bir amatör olarak tamamını biliyorum diyemem. Şimdilik ihtiyaç duyduğum kadar kısmını öğrenebildim. Çok fazla kaynak bulunan protokolün detayına girmeden kullanımına yönelik uygulamalar paylaşacağım. Öğrenilmesi gereken registerleri açıklamaya çalışacağım. Verilerin dizi olarak alınması ve gönderilmesi, kesmelerin kullanımı ve RS485 protokolüne değineceğim. Her zaman olduğu gibi kullandığım mikro denetleyicinin datasheeti en önemli rehberimiz. Datasheette ki sırayla alıntılar yaparak başlayalım.



USART blok şemasında birimin registerleri ve ilişkileri görülmektedir. Zamanlama hesabının yapıldığı UBRR0H ve UBRR0L registerleri, verici-alıcı bölümünde verinin tutulduğu UDR0 registeri ve UCSR0A,UCSR0B ve UCSR0C isminde 3 adet kontrol ve durum registeri bulunmaktadır. Atmega328p de tek USART bulunmakta, farklı denetleyicilerde UCSR1A, UCSR2A şeklinde olabilir. Bu yüzden UCSRnA şeklinde şemaya yazılmış. Ben anlatımımı Atmega328p özelinde yapacağım bu yüzden kullanacağınız denetleyicinin datasheet kontrolünü mutlaka yapmalısınız. Şemada pin kontrolleri ve birimlere bağlı pinler de görülmektedir.




Yukarıda gördüğünüz tablo “Baud rate” hesaplama formülünü göstermektedir. Asenkron bir iletişim söz konusu olduğu için alıcı-verici arası veri alış-verişini kontrol eden saat sinyali söz konusu değildir. Bir bit verinin ne kadar sürede gönderildiği bilinirse alıcı gelen veriyi analiz eder ve doğru olarak alabilir. Bu gönderim süresini belirleyen "bir saniyede gönderilen bit sayısına" baud rate deniyor. 16mhz osilatör takılı olan bir denetleyiciyi 9600 baud rate ayarlamak istersek, formüldeki değerleri yerine koyalım;

UBBR0(H:L)=(16000000/16*9600)-1

UBBR0(H:L)=(16000000/153600)-1

UBBR0(H:L)=(104)-1 ve 
UBBR0(H:L)=103 ya da UBBR0(H:L)=0x67 buluruz. Bulunan değeri UBBR0(H:L) registerine yazmamız gerekir. Daha düşük bir baud rate seçmiş olsaydık daha büyük bir değer bulacaktık. Bulunan bu rakam 8 bitin üstünde olacaktır. Bu nedenle UBRR0;UBRR0H ve UBRR0L isminde iki adet registerden oluşan 12 bitlik bir registerdir.



Alıcı-verici arası doğru bilgi alış-verişi için önemli olan bir diğer konuda veri paketleri. UART protokolünde veri paketi aşağıdaki gibidir.



Alıcı ya da verici pinleri normalde “1” “HIGH” seviyededir. Veri gönderimi başlamadan önce “0” yapılır “Start biti”. Sonrasında kaç bit veri gönderilecek, eşlik biti var mı? Kaç stop biti olacak bilgisi iki taraf içinde aynı olmalıdır. Kontrol ve durum registerleri bu ayarları yapmamıza olanak vermektedir. 



Bu registerlerden ilki UCSR0A: 

RXC0: Veri alımı tamamlandı bayrak biti. Veri alımı tamamlandığında “1” olur. UDR0’dan veri okunuğunda “0” olur.

TXC0:  Veri gönderimi tamamlandı bayrak biti. UDR0 ‘a veri yazılan veri gönderildiğinde “1” olur.

UDRE0: Veri gönderme-alma belleği hazır-boş bayrak biti. UDR0 boş olduğunda “1” olur.

FE0: Çerçeve hatası ayarlı olandan farklı sırayla veri gelirse “1” olur.

DOR0: Veri üstüne yazma ya da taşma hatası. Bellek yeterli gelmediğinde “1” olur.

UPE0: Eşlik biti hatası olursa “1” olur.

U2XO: 2 kat hızlı iletişim ayarı için “1” yapılır. Kullanmadığım bir mod konu hakkında bilgim yok.

MPCM0: Çok işlemcili iletişim modu kullanmadığım bir mod konu hakkında bilgim yok.  


UCSR0B:

RXCIE0: Veri alımı tamamlandı kesmesini aktif etmek için “1” yapılır.

TXCIE0: Veri gönderimi tamamlandı kesmesini aktif etmek için “1” yapılır.

UDRIE0: Veri gönderme-alma belleği hazır-boş kesmesini aktif etmek için “1” yapılır.

REXEN0: Rx yani alıcı etkinleştirmek için “1” yapılır.

TXEN0: Tx yani verici etkinleştirmek için “1” yapılır.

UCSZ02: Veri paketi karakter boyutunu belirlemek için kullanılır. UCSR0C ile açıklayacağım.

RXB80: 9 bit veri alındığında 9. Bit buraya yazılır.

TXB80: 9 bit veri gönderilirken 9. Bit buraya yazılır.



UCSR0C:

UMSEL00 ve UMSEL01: USART mod seçimini belirleyen bitler. Asenkron için iki bit “0” olmalıdır.  


UPM00 ev UPM01: Eşlik biti seçimini belirleyen bitler. Tablodan istenen duruma göre seçim yapılır. Eşlik biti istenmiyorsa iki bit “0” yapılır.



USBS0: Stop bit sayısını belirler. “0” olursa bir, “1” olursa iki stop bit gönderilir.

UCSZ00 ve UCSZ01: UCSR0B de bulunan UCSZ02 ile beraber bu 3 bit alınan ya da gönderilen verinin boyutunu belirler. Tablodan seçimi yapabilirsiniz. 8 bit için UCZS00 ve UCSZ01 “1” yapılır, UCZS02 “0” yapılır.



UCPOLO: Senkron mod düşen ya da yükselen kenara göre saat düzenlemesi yapılmasını sağlayan bit. Konu hakkında bilgim yok.

Master SPI mod ile ilgili bitlere (UDORD0, UCPHA0) değinmeden geçeceğim.

Bu registerler dışında gelen ve gönderilen verinin yazıldığı UDR0 registeri bulunmaktadır.8 bite kadar olan paketlerde bu register kullanılır. 9 bit ayarlandığında daha önce değindiğim UCSR0B de bulunan RXB80 veTXB80 bitlerine veri yazılır.



Yüzeysel olarak değindiğimiz bu registerleri kullanarak UART iletişimi için gerekli ayarlamaları yapıyor ve iletişimi başlatıyoruz. Bunun için datasheette ya da internette kalıplaşmış birçok fonksiyon bulabilirsiniz. Benim kullandığım fonksiyonları paylaşacağım. Öncelikle UART iletişiminin iki taraf içinde olmazsa olmazı baud rate ve paket bilgisidir. Bir taraf 9600 ayarlıyken diğer taraf 4800 ayarlıysa gönderen taraf okuyanın bir bit beklediği sürede iki bit gönderecektir. Sonuçta iletişim gerçekleşmeyecektir. Aynı şekilde 8bit bir stop bekleyen alıcıya 7 bit, bir eşlik ve bir stop biti gönderilirse yine hatalı veri alınmış olacaktır. Bunun için bir başlatma fonksiyonu yazmamız gerekir ve burada kullanılan ayarlar iki tarafta da aynı olmalıdır.

Programın taşınabilir olması ve ayarların kolay değiştirilebilmesi için bazı değerleri ön tanımlı olarak yazabilirsiniz. Ben bunun yerine başlatma fonksiyonuna parametre olarak yazmayı tercih ediyorum.

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

void uart_basla(uint32_t baud){
  uint16_t baudRate=F_CPU/baud/16-1;
  UBRR0H=(baudRate>>8);
  UBRR0L=baudRate;
  UCSR0B|=(1<<RXEN0)|(1<<TXEN0);
  UCSR0C|=(1<<UCSZ01)|(1<<UCSZ00);
}

 “uart_basla” fonksiyonunda işaretsiz 32bit parametre bulunmakta bunun nedeni 16 bit ile 65535 sayısının üstüne çıkamıyor olmamız. Baud rate bu durumda en fazla 57600 olabilirdi.  32 bit kullanarak daha yüksek hızları parametre olarak girebildim. “#define F_CPU 16000000UL” denetleyicinin frekansını ön tanımladıktan sonra formülde parametre olarak gelen “baud” değeri ile hesaplama sonrası “baudRate” değerini buluyoruz. Bu değeri UBRR0(H:L) registerine yazmamız gerekiyor. 12 bit olan bu registere veriyi "H" ve "L" olarak iki bölümde yazıyoruz. Böylece hızımızı belirlemiş olduk. Veri paket boyutu, eşlik biti ve stop biti “UCSR0C|=(1<<UCSZ01)|(1<<UCSZ00);”gibi ayarlamaları yaparak ve alıcı-verici aktif ederek “UCSR0B|=(1<<RXEN0)|(1<<TXEN0);” fonksiyonu tamamlıyoruz. (eşlik biti yok, 8 bit ve 1 stop biti ayarlı).

UART birimi ayarlandı ve çalıştırıldı. Artık gelen veriyi okuyan fonksiyona sıra geldi.

uint8_t uart_oku(){
  while(!(UCSR0A & (1<<RXC0)));
  return UDR0;
}

Okuduğumuz veriyi döndüren bir fonksiyon yazıyoruz. Fonksiyonu çağırdığımızda UCSR0A kontrol registerinin RXC0 (7. Biti) kontrol ediliyor. Yukarıdaki tanımdan hatırlayacağınız gibi veri alındığında “1” olan bu biti while döngüsünde kontrol ediyoruz. Kodu biraz açalım, UCSR0A da bulunan tüm bitleri “0” olarak kabul edelim yani UCSR0A=0b 0000 0000 olsun. Veri gelmemişse 7. Bit de “0” olacaktır. Bu durumda “UCSR0A & (1<<RXC0)” işlemi sonucu (0b 0000 0000 &(0b 1000 0000)) =0 olacaktır. While şartında “!” değil operatörü bulunmakta dolayısıyla while şartı “1” olmakta ve döngü içinde kalmaktadır. Bunun anlamı veri gelene kadar RXC0 “1” olana kadar bekle demektir. RXC0 “1” olduğunda (0b 1000 0000 &(0b 1000 0000))=1 ve while şartı “0” olacak döngüden çıkılacaktır. Veri geldiğinde UDR0 veri registerine yazılacaktır. Fonksiyon sonunda “return UDR0;” ifadesiyle bu veriyi fonksiyon değeri olarak döndürerek okuma işlemini tamamlamış oluyoruz.

void uart_gonder(uint8_t uData){
  while(!(UCSR0A & (1<<UDRE0)));
  UDR0=uData;
}

Veri gönderim fonksiyonu da benzer şekilde çalışmaktadır. Fonksiyon çağrıldığında UCSR0A kontrol registerinin UDRE0 (5. Biti) kontrol ediliyor. Yukarıdaki benzer işlemler yapılarak UDRE0 biti “1” olana kadar bekleniyor. Hatırlayacağınız gibi bu bitin “1” olması durumu UDR0 registeri boşaldığında gerçekleşmektedir. Bu döngü ile yapılan da budur. UDR0 boş-hazır olunca UDR0 registerine parametre olarak gönderdiğimiz veriyi yazıyoruz. Ve UART birimi veriyi gönderiyor. Buraya kadar basit bir şekilde iletişim kurabiliriz. Aşağıda paylaşacağım kodu Arduino idesiyle Arduino Uno’ya yüklerseniz Serial.write(Serial.read()); koduyla aynı sonucu alırsınız  ama hem daha hızlı hem daha az yer kaplayacaktır.

Buraya kadar olan anlatım aslında datasheette mevcut olan bilgilerdir. Daha karmaşık bir program içinde bu şekilde kullanmanız doğru olmaz. Sonraki bölümde bu konuya açıklık getireceğim. 

/*
 * uart.c
 *
 * Created: 12.02.2019 22:05:41
 * Author : haluk
 */
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

void uart_basla(uint32_t baud){
  uint16_t baudRate=F_CPU/baud/16-1;
  UBRR0H=(baudRate>>8);
  UBRR0L=baudRate;
  UCSR0B|=(1<<RXEN0)|(1<<TXEN0);
  UCSR0C|=(1<<UCSZ01)|(1<<UCSZ00);
}
uint8_t uart_oku(void){
  while(!(UCSR0A & (1<<RXC0)));
  return UDR0;
}
void uart_gonder(uint8_t uData){
  while(!(UCSR0A & (1<<UDRE0)));
  UDR0=uData;
}
int main(void){
    uart_basla(19200); 
    while (1) {     
     uart_gonder(uart_oku());
   }
}