AVR RS485 Kullanımı



RS485:

RS485 iki ya da daha fazla cihazın seri iletişimini sağlayan fiziksel altyapı standardıdır. Seri iletişimde hat "1" ya da "0" olarak veriyi iletir. ASCII "A" göndermek istediğimizde Tx (transmitter) "0100 0001" şeklinde "1" ya da "0" olacaktır. "1" ya da "0" olarak adlandırdığımız durum GND ile Tx arasında ölçülen potansiyel farktır. (TTL seviyede 2,7v üstü ) Bu iki kablo ile iki cihazı tek taraflı (simplex) haberleştirebiliriz. Mesafe uzayınca bu hat üstünde oluşacak kayıplar ve gürültüler nedeniyle iletişim sağlıklı olmayacaktır.

Uzun mesafeler ve daha hızlı haberleşme ihtiyacı RS485 standardının geliştirilmesini sağlamıştır.  1000m gibi bir mesafe ve 10Mbps gibi hızlarla iki ya da daha fazla cihazın haberleşmesine imkan sağlar. Bu standardın RS422 ye göre hız ve mesafeyi artıran özelliği, birbirine sarılı iki hattın gürültüleri en aza indirmesidir. "1" ya da "0" olma durumu Gnd ye göre değil iki kablo arasındaki farka göre bulunmaktadır. A ve B olarak adlandırılan bu iki hat arasındaki fark 200mV tan büyükse  "1" küçükse "0" olarak belirlenir. Hattın karalı olması ve 200mV farkın oluşabilmesi için sonlandırma  ile pull up-pull down dirençler kullanılır.

Max485:

RS485 standardı ile iletişimin gerçekleşmesi için gerekli olan sürücüler vardır. Bunlardan Max485'i inceleyelim. Datesheet buradan ulaşabilirsiniz. Aşağıda pin ve bağlantı şeması bulunmaktadır. Sırasıyla bu pinleri açıklayayım. 

RO: Mikro denetleyicinin (Receive) RX pini  bu pine bağlanır.

RE: Alıcı (Receiver) aktifleştiren pindir. "0" ise veri okumaya hazırdır. "1" olursa RO pininde "high z" yani ne "1" ne de "0" durumu oluşur.

DE: Veri gönderimi (Driver output) aktifleştiren pindir.  "1" ise veri gönderilebilir, "0" olursa A ve B "high z" durumundadır. DE ve RE birbirine bağlanabilir, farklı modelerde (481-483) kapatma modu için ayrıca kontrol edilebilir.

DI: Mikro denetleyicinin (Transmitter) TX pini  bu pine bağlanır. 

Vcc-Gnd: Pozitif +5V ve negatif besleme pinleridir. (12V a kadar besleme yapılabilir.)

A-B: iletişimi sağlayan hattın bağlandığı pinlerdir.  Burgulu tip kablo ile diğer sürücünün aynı isimli pinlerine bağlanır.
Yukarıdaki tabloda RE ve DE pinlerine göre veri gönderme ve alma durumları gösterilmiştir. DE pin "1" ve DI "1" olduğunda A "1" - B"0" ve DI "0" olduğunda A"0" - B"1" olarak gönderim yapılır. Gönderim modunda (DE"1") RE nin durumu önemli değildir. Veri alırken DE ve RE pinleri "0" yapılır. A - B  <200mV ise "0", A - B  >200mV "1" olarak veri okunur. Verinin sağlıklı okunması ve gönderilmesi için kullanılacak dirençlerin hesaplanması önemlidir. A ve B arasındaki fark iletişim yokken 200mV'tan fazla olmalıdır. Bazı kaynaklarda bu hesaplamayı 200mV eşit olarak yapılması tavsiye edilmiş.

Dirençler:

Master cihazda A hattı "+" ve B hattı "-" uçlara uygun direnç ile bağlanmalıdır.(Bu bağlantı tek noktadan yapılmalıdır.) Hat sonu ve hat başı genellikle 120 ohm direnç bağlanmaktadır. Ben 3 adet cihazın bağlı olduğu bir sistem için hesap yapacağım.  3 Adet cihazın içinde bulunan 12 Kohm dirençler ve 120 ohm sonlandırma dirençleri paralel bağlı dirençlerdir ve paralel direnç değeri 59,113 ohm olur.

1/R(eşdeğer)=(1/R1)+(1/R2)+(1/R3)+......

1/R(eşdeğer)= (1/12000)+(1/12000)+(1/12000)+(1/120)+(1/120)=59,113ohm

250mV fark oluşturmak için 4,22 mA akıma ihtiyaç vardır.

V=I x R

0,250=Ix59,113

I=0,250/59,113=0,004229 A=4,22mA

Sistemin 5V ile beslenmesi durumunda 1182 ohm direnç bulunur.

V=I x R

5=0,004229 x R

R=5/0,004229= 1182ohm

Bunun 59 ohm kısmı paralel dirençlerden geliyordu bunu düştüğümüzde 561 ohm pull-up ve pull-down dirençlere ihtiyacımız olur. (https://320volt.com/rs485-balanced-data-transmission-hakkinda-bilgiler/)
Uygulama notlarında yer alan şema yukarıdaki gibidir. RS485 altayapısıyla ilgili ihtiyacmız olacak bilgileri kabaca anlattım artık AVR için C diliyle programın yazılması ve çalışmasına geçebiliriz.

C ile AVR RS485 Programlama

Uart kullanımı  kısmında paylaştığım programa ilave bir kaç komut ile bu işlemi yerine getirebiliriz. Öncelikle sürücünün DE ve RE pinleri gönderme-alma ayrımını yapıyor. Ben cihazın veri almaya hazır olmasını, veri göndereceğim zaman gönderme pozisyonuna geçmesini istiyorum. Bu nedenle bu iki pin "0" olacak. Veri gönderme durumunda DE pini "1" olacak. Bu işlemin kolay olması ve ilerde mikro denetleyicide pin değişikliği yapmak isteyebileceğim için makro tanımlayacağım.


#define MAX_PIN PORTD2
#define MAX_OUT DDRD|=(1<<MAX_PIN)
#define MAX_AL PORTD&=~(1<<MAX_PIN)
#define MAX_VER PORTD|=(1<<MAX_PIN)
MAX_PIN olarak PORTD PD2 pini seçildi burada yapılan tanımlama ile MAX_PIN görülen her yere PORTD2 yazılacaktır, pin değiştirmek için bu tanımdaki kısmı değiştireceğiz. MAX_OUT ile ilgili pini çıkış olarak tanımlıyoruz. MAX_AL ve MAX_VER ile pin "0" ve "1" yapılarak sürücünün durumuna karar veriyoruz. Veri gönderme sırasında işlem yapacağız, gönderme için yazdığımız fonksiyona ekleme yapacağız.


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

Fonksiyonun ilk hali yukarıdadır. Eklemelerin yapılmış ve sürücü ile veri gönderimi yapabilen hali ise aşağıdadır.


void uart_gonder(uint8_t uData){
  MAX_VER;
  while(!(UCSR0A & (1<<UDRE0)));
      UDR0=uData; 
  _delay_us(200);
  MAX_AL;
}
Fonksiyon çağrıldığında MAX_VER ile DE pini "1" yapılıyor. Ardından uart birimimin veri gönderimine hazır olması bekleniyor ve veri gönderiliyor. Burada önemli olan bir nokta Baud Rate eğer düşük ise (9600) bekleme " _delay_us(200); " süresi daha uzun olmalı. Sürücü veri gönderimi tamamlanmadan alma pozisyonuna geçerse iletişim gerçekleşemez. İşlem tamamlanınca sürücü MAX_AL ile veri alma durumuna geçiriliyor. Tx kesmesi kullanırsak bu beklemeye gerek kalmaz.

Umarım faydalı bir yazı olmuştur.
/*
 * RS485.c
 *
 * Created: 13.02.2019 22:06:14
 * Author : haluk
 */
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

#define MAX_PIN PORTD2
#define MAX_OUT DDRD|=(1<<MAX_PIN)
#define MAX_AL PORTD&=~(1<<MAX_PIN)
#define MAX_VER PORTD|=(1<<MAX_PIN)

char veri[25];
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(){
  while(!(UCSR0A & (1<<RXC0)));
  return UDR0;
}
void uart_gonder(uint8_t uData){
  MAX_VER;
  while(!(UCSR0A & (1<<UDRE0)));
       UDR0=uData; 
  _delay_us(200);
  MAX_AL;
}
void uart_dizi(char*str){
  uint8_t i=0;
  while(str[i]) {
    uart_gonder (str[i]);
    i++;
  }
}

int main(void){
       MAX_OUT;
       MAX_AL;
   uart_basla(19200); 
    while (1) {
             uart_gonder('A');         
              _delay_ms(1000);
       }
 }

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


Dairesel bellek (ring buffer) konusuna giriş yaptığımız bir önceki bölümde paylaştığım tabloyu tekrar paylaşıyorum. Bu tablo üstünde anlatmaya devam edeceğim.

Yazma adresi
1
2
3
4
5
6
7
8
9
Veri
a
b
c






Okuma adresi
1
2
3
4
5
6
7
8
9

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
Veri
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
Veri
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.

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;
      }          
}

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.

#define UART_Rx_Boyut 16
#define UART_Rx_Mask (UART_Rx_Boyut-1)
volatile uint8_t rx_bas=0;
volatile uint8_t rx_ring[UART_Rx_Boyut];

ISR (USART_RX_vect){   
            rx_bas=(rx_bas+1) & UART_Rx_Mask;
            rx_ring[rx_bas]= UDR0;      
     
}



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.



volatile uint8_t rx_son=0;

uint8_t uart_oku(){         
      rx_son=(rx_son+1) & UART_Rx_Mask;
           
      return rx_ring[rx_son];
}

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()J

uint8_t uart_varsa(){
      if (rx_son==rx_bas){
            return 0;
      }
      return 1;  
}

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 giriyordu 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.

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

İ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.

#define UART_Tx_Boyut 16
#define UART_Tx_Mask (UART_Tx_Boyut-1)
#define UART_Bos_On UCSR0B|=(1<<UDRIE0)
#define UART_Bos_Off UCSR0B&=~(1<<UDRIE0)
volatile uint8_t tx_ring[UART_Tx_Boyut];
volatile uint8_t tx_bas=0,tx_son=0;

void uart_gonder(uint8_t uData){
      tx_bas=(tx_bas+1)&UART_Tx_Mask;
      while (tx_bas==tx_son);
      tx_ring[tx_bas]=uData;
      UART_Bos_On;
      }



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.

ISR (USART_UDRE_vect){
      tx_son=(tx_son+1)&UART_Tx_Mask;
      UDR0=tx_ring[tx_son];
      if (tx_son==tx_bas){
      UART_Bos_Off;
  }  
}

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.

/*
 * uart_kesme_ring.c
 *
 * Created: 19.03.2019 23:16:31
 * Author : haluk
 */
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define UART_Rx_Boyut 128
#define UART_Tx_Boyut 128
#define UART_Rx_Mask (UART_Rx_Boyut-1)
#define UART_Tx_Mask (UART_Tx_Boyut-1)
#define UART_Bos_On UCSR0B|=(1<<UDRIE0)
#define UART_Bos_Off UCSR0B&=~(1<<UDRIE0)


volatile uint8_t rx_bas=0,rx_son=0,tx_bas=0,tx_son=0;
volatile uint8_t rx_ring[UART_Rx_Boyut];
volatile uint8_t tx_ring[UART_Tx_Boyut];



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();
}
uint8_t uart_oku(){       
       rx_son=(rx_son+1) & UART_Rx_Mask;
       return rx_ring[rx_son];
}
uint8_t uart_varsa(){
       if (rx_son==rx_bas){
             return 0;
       }
       return 1;   
}
void uart_gonder(uint8_t uData){
       tx_bas=(tx_bas+1)&UART_Tx_Mask;

       while (tx_bas==tx_son);
       tx_ring[tx_bas]=uData;
       UART_Bos_On;
       }
void uart_dizi_gonder(char*str){
       uint8_t i=0;
       while(str[i]) {
             uart_gonder (str[i]);
             i++;
       }
}
ISR (USART_RX_vect){
       rx_bas=(rx_bas+1) & UART_Rx_Mask;
       rx_ring[rx_bas]= UDR0;    
}
ISR (USART_UDRE_vect){
       tx_son=(tx_son+1)&UART_Tx_Mask;
       UDR0=tx_ring[tx_son];
       if (tx_son==tx_bas)
       UART_Bos_Off;
}
int main(void){
  uart_basla(19200);
  DDRB|=(1<<PORTB5);
  sei();

    while (1) {
             PORTB|=(1<<PORTB5);
             _delay_ms(1000);
             PORTB&=~(1<<PORTB5);
             _delay_ms(1000);

             if (uart_varsa()){
              uart_gonder(uart_oku());
            
              }
       }
 }