26 Şubat 2019 Salı

10 - MODBUS RTU SLAVE ÖRNEĞİ

Bir önceki yazıda Modbus hakkında teorik bilgiler bulunmaktadır. Teoride olan bilgilerin yazılıma aktarılmasında bir çok ince detay bulunmaktadır. Öncelikle herhangi bir işlemci için modbus protokolunu doğru bir şekilde yapmak için gerekli altyapıya ihtiyaç duyulmaktadır. Bunları sıralayacak olursak:

1) Modbus yazılımsal olarak Usart altyapısını kullanır. Bu yüzden öncelikle Usart konfigürasyon ayarları yapılmalıdır.

2) Her ne kadar yazılımsal olarak Usart kullanılsa da donanımsal olarak RS232/RS485 veya RS422 donanımına ihtiyaç duyar. RS232 hemen hemen gelişmiş bütün işlemcilerde dahili olarak mevcuttur. Ancak RS232 hem uzun mesafelerde sağlıklı bir iletişim için sıkıntılıdır hem de veri kaybı/parazit vb çevresel faktörlerden kolayca etkilenebilir. RS485 veya RS422 ise bu konuda daha güvenilirdir. Ayrıca RS485 ve RS422 iletişim için GND'ye ihtiyaç duymaz. Bir diğer güzel yanı ise uzun mesafelerde sorunsuz çalışabilmektedir. Detaylı bilgileri herhangi bir site üzerinden edinebilirsiniz. 

     Ancak RS485 ve RS422 birçok güzel yanı olsa da bir de ihtiyaç duyduğu donanım desteği vardır. İşlemciye ek olarak bir entegreye ihtiyaç duyar. Örneğin Max485 entegresi RS485 için hem ucuz hem de güvenilir bir entegredir. Ayrıca bir de DE/RE pini vardır. Lojik olarak 1 ve 0 yapılarak iletişim hangi yönde ( gelen data / iletilen data ) olduğu belirlenir. MAX485 entegresinde DE ve RE pini birleştirilerek kullanılır. Lojik 1 olursa veri gönderilir, lojik 0 yapılırsa veri alınır. İşlemcideden direkt olarak kontrol edilir. Daha fazlası için araştırma yapılmasını öneririm. Burada da anlatılabilir ancak konu çok fazla uzayacaktır. 

3) Yine bir önceki yazıda haberleşmenin bittiğini anlamak için bir zamana ihtiyaç duyulur demiştik. Bu zaman dilimi 3,5 ( üç buçuk ) karakter iletim süresi kadardır. Örneğin,

                               Sistemin hızı 9600 baudrate olsun.

                                  3.5 karakter = 28 bit, 28/9600 = 0.00290 saniye.

Bunu sağlamanın en kolay ve güvenilir yolu ise bir Timer birimi kullanmaktır. Bu yüzden yazılımda bir de timer konfigürasyon ayarları yapılmalıdır.

Bu kadar bilgi şimdilik yeterli olacaktır. Artık kod yazmaya başlayabiliriz. Bu örnekte Modbus haberleşmesi için -- Read Holding Register -- fonksiyonunun örneği anlatılacaktır.

KOD KISMI:

Başlnagıç olarak kütüphaneler eklenir.

#include "stm32f4xx.h"
#include "stm32f4xx_gpio.h" // GPIO kütüphanesi
#include "stm32f4xx_rcc.h" // RCC clock kütüphanesi
#include "stm32f4xx_usart.h" // Keil::Device:StdPeriph Drivers:USART
#include "stm32f4xx_it.h"
#include "stm32f4xx_tim.h"

/********************************************************************************/
void GPIO_Init(void);
void USART_Init(void);
void TIM6_Init(void);
void USART1_IRQHandler(void);
void TIM6_DAC_IRQHandler(void);
void ReadHoldingRegister();
char CRC_check(char package[],unsigned int package_length);
void Get_CRC(unsigned char package[],unsigned int package_length);
/********************************************************************************/



/********************************************************************************/
/* Gerekli değişken tanımlamaları */

unsigned short Holding_Register_Array[100];
unsigned char Modbus_Receive_Array[100];
unsigned char Modbus_Trans_Array[100];
unsigned short modbus_receive_counter = 0;
unsigned int modbus_data_counter = 0;
unsigned char modbus_slave_addr = 1; //Slave adresimiz 1 olsun
unsigned short Holding_Register_Adress_Value = 255;
unsigned char frame_length;
unsigned short Tx_count= 0;
/********************************************************************************/

/* Led için konfigürasyon ayarlamaları */
void GPIO_Init(void)
{
   RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
     
    GPIO_InitTypeDef  GPIO_InitStructure; // Port yönlendirmesi

   /* PD12, 13, 14 ve PD15 pinleri kullan1lacak */
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;  //Pinler cikis olarak belirlendi
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

}

/* USART1 ayarlamaları */
/* usart kesmesi de ayarlandı. gelen data direkt olarak kesme fonksiyonu ile alınacaktır */
void USART_Init(void)
{
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
     RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

     GPIO_InitTypeDef GPIO_InitStructure;
     USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

     GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;
     GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
     GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
     GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6 | GPIO_Pin_7;
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
     GPIO_Init(GPIOB, &GPIO_InitStructure);

// GPIO ALTERNATE FUNCTION olarak tanimlanan pinin konfigürasyonlari
// pin ile baglant1 kuracagi modül belirlenir.
     GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);
     GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);


     // USART1 Reset
     USART_DeInit(USART1);
     // USART konfigürasyonlari:
     USART_InitStructure.USART_BaudRate = 9600;
     USART_InitStructure.USART_HardwareFlowControl= USART_HardwareFlowControl_None;
     USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
     USART_InitStructure.USART_Parity = USART_Parity_No;
     USART_InitStructure.USART_StopBits = USART_StopBits_1;
     USART_InitStructure.USART_WordLength = USART_WordLength_8b;
     USART_Init(USART1, &USART_InitStructure);

     USART_ITConfig(USART1, USART_IT_RXNE,ENABLE);

     // NVIC Init
     NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
     NVIC_Init(&NVIC_InitStructure);

     USART_Cmd(USART1, ENABLE);
}


void TIM6_Init(void)
{
/*
3ms zaman için:

TIM6 APB1’e bağlı gözüküyor. APB1’in çalışma hızı 42 MHz. Ancak Datasheet’e bakıldığında önemli bir uyarı ile karşılaşıyoruz:

       APB2’ye bağlı olan zamanlayıcılar için clock hattı 168Mhz’e kadar olabilir, APB1 içinse 84Mhz olabilir. Burada bir ikilem var gibi duruyor ancak datasheet’te bunu şöyle açıklıyor:  APB ön bölücü değeri 1 ise timer clock hattı da aynı frekansta çalışır, ancak ön bölücü değeri 1 değilse veya 1’den farklı bir değerde ise timer clock hattı APB hattının 2 katı hızda çalışır.

Timer6’ye baktığımızda APB1 bus hattına bağlıdır. Bu hattın clock frekansı 42 Mhz. Daha önce CMSIS klasöründe oluşturduğumuz system_stm32f4xx.c dosyasında ön bölücü değeri 4 olarak kullanıldığı için bu hat 42*2 yani 84 Mhz clock frekansında çalışmaktadır. Daha detaylı bilgiye datasheet’ten bakılabilir.

Prescaler değeri ise:      Timer hızı  = Bus hızı / (prescaler + 1)  formülünden bulunur.

O halde 3ms’lik timer kesmesi için:

3sn = 1/0.003 = 333Hz frekansa eşittir. Biz bunu biraz daha tolere ederek 300 Hz diyelim

Öncelikle 30Khz (30000Hz) için:        30000 = 84Mhz(buz hızı) /(prescaler+1)
                                                           30000 = 84000000 / (PRSC + 1) = > PRSC = 2800 - 1 olmaktadır.

Periyodu da biz 100’e kadar saydırırsak:           30000hz / 100 = 300Hz
                                                                           300Hz = 1/300 = 0.0033 sn demektir.

Prescaler değeri: 2800 - 1
Periyod: 100 -1 = 99 (sayıma 0’dan başladığı için 1 eksiği olur)
0.0033 sn için değerlerimizi yukarıdaki gibi hesaplarız.
*/
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
     TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure; // Struct
     NVIC_InitTypeDef NVIC_InitStructure;

     TIM_DeInit(TIM6);

     TIM_TimeBaseStructure.TIM_Period = 100 -1;
     TIM_TimeBaseStructure.TIM_Prescaler = 2800-1; /
     TIM_TimeBaseStructure.TIM_ClockDivision =0; // TIM_CKD_DIV1
     TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
     TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
     TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);

     TIM_ClearITPendingBit(TIM6,TIM_IT_Update); // Clear Update interrupt flag

     TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE); // Interrupt

     NVIC_InitStructure.NVIC_IRQChannel = TIM6_DAC_IRQn; // NVIC Ayarlari
     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
     NVIC_Init(&NVIC_InitStructure);

     TIM6->CR1 &=~ 0x0001;            // Counter Disable
      // TIM6'yı daha aktif etmedik dikkat edin. !!
}

/********************************************************************************/



int main(void)
{
     GPIO_Init();
     USART_Init();

/* modbus paketinin bitmesini anlamak için 3,5 karakter boşluk zamanı olması gerekiyordu.
    Bu zamanı timer6 birimi ile yapmak istiyoruz. o yüzden timer6 yı yaklaşık olarak 3,5 karakter            süresine ayarlamamız gerekli.
    yukarıdaki hesabımıza göre bize yaklaşık olarak 9600 baudrate hızında,
    3,5 karakter = 28 bit. 28/9600 = 0.0029 =~0.003 saniye = 3 ms' lik bir zaman lazım.
    bunun hesaplamasını alttaki fonksiyon içerisinde yapalım
*/
     TIM6_Init();

/* Bütün ayarlamaları yaptık. artık iş gelen sorguya cevap vermek. Bu yüzden usart kesmesinden gelen veri takip edilecek. verinin bittiği timer ile anlaşılacak. Gelen veri eğer doğru ise bir cevap gönderilecek. bunun için biz while döngüsü içerisinde herhangi bir işlem yapmayacağız, çünkü bütün işlemlerimiz kesmelerle yapılmaktadır
*/
     while(1)
     {



     }

}


/********************************************************************************/
/* USART1 KESME FONKSİYONU */

void USART1_IRQHandler(void)
{
     if( USART_GetITStatus(USART1, USART_IT_RXNE) )
     {
          USART_ClearITPendingBit(USART1, USART_IT_RXNE);//kesme bayrağı temizlenir

          // Clear & Start Timer
          TIM6->CNT = 0; // TIM6 Clear
          TIM_Cmd(TIM6, ENABLE); //  Enable TIM6

/* Tim6 enable edildi, yani her veri geldiğinde timer counter'i sıfırlandı ve tekrardan timer aktif edildi. bu sayede timer her veri geldiğinde baştan saymaya başlıyor. eğer başka bir veri gelmezse timer kesmesine giriliyor ve iletimin bittiği yani paketin alındığı tespit ediliyor. Eğer veri gelirse işlem başa dönüyor timer counteri tekrardan sıfırlanıp saymaya başlıyor*/

          // Gelen veriyi oku ve diziye atamasını yap
          Modbus_Receive_Array[modbus_receive_counter ] = USART_ReceiveData(USART1);

          // Gelen paketteki veri sayacini arttir
          modbus_receive_counter ++;

/* Eğer yeni gelen veri yoksa timer6 kesmesi aktif olacaktır ve bu sayede iletimin tamamlandığı anlaşılacaktır. Burası gelen verinin bir diziye atanması içindi. Gelen verinin bittiğini varsayarsak timer6 kesme fonksiyonunda gelen veriyi anlamlandırma işlemini yaparız */
 
     }
     else if( USART_GetITStatus(USART1, USART_IT_TXE) )
     {
          USART_ClearITPendingBit(USART1, USART_IT_TXE);
     }


}


/********************************************************************************/
/* TIMER6 KESME FONKSİYONU */
void TIM6_DAC_IRQHandler(void)
{

/* Yukarıda da anlatıldığı üzere, eğer program bu kesme fonksiyonuna girmiş ise Master bir cihazdan bir sorgu gelmiş demektir. Gelen veri paketini usart kesmesinde almıştık. şimdi paketi inceleyelim
*//

TIM_ClearITPendingBit(TIM6,TIM_IT_Update); // Clear Update interrupt flag

// Counter Disable
TIM6->CR1 &=~ 0x0001;

        //eğer mesaj bize gelmiş ise, slave adresimiz 1
if (Modbus_Receive_Array[0]  == modbus_slave_addr )
        {
                //CRC dogru ise, yani paketimiz güvenli bir şekilde bize ulaştı mı
if(CRC_check((char*)Modbus_Receive_Array,modbus_receive_counter )==1)
{
       // Modbus haberleşmesinde 3 numaralı fonksiyon Read Holding Register fonksiyonu
       if ( (Modbus_Receive_Array[1] == 3)  ) // fonksiyon kodu 3 ise
              {
              ReadHoldingRegister();
                 for(Tx_count=0;Tx_count<frame_length ;Tx_count++)
                {
                      while(!(USART_GetFlagStatus(USART1, USART_FLAG_TXE)));
                      USART_SendData(USART1, Modbus_Trans_Array[Tx_count]);
                   // veriyi gönderdik ve yeni sorguyu bekliyoruz
                }
           
              }

}

}

modbus_receive_counter = 0;




}

void ReadHoldingRegister()
{
    unsigned short modbus_addr, start_addr;
    unsigned short modbus_frame_value;
    unsigned short byte_lenght;
    volatile unsigned char i;
    volatile unsigned char tx_count;
    volatile unsigned short CRC_Data = 0;

    byte_lenght = Modbus_Receive_Array[5]*2;
    modbus_addr = Modbus_Receive_Array[2]*256 + Modbus_Receive_Array[3];
    start_addr=  Modbus_Receive_Array[2]*256 + Modbus_Receive_Array[3];
    modbus_frame_value = Modbus_Receive_Array[4]*256 + Modbus_Receive_Array[5];

if( modbus_addr   <= Holding_Register_Adress_Value ) //data adresi istenilen aralıkta mı
{
   Modbus_Trans_Array[0] = modbus_slave_addr ; //slave adresi
   Modbus_Trans_Array[1] = 03; // fonksiyon kodu
   Modbus_Trans_Array[2] = byte_lenght; // istenilen data sayısı(byte olarak)
 
   //a = MB_Rx[3];
   //a_tx = 3;
   tx_count = 3;
 
   for(i=0; i < modbus_frame_value ; i++)
   {
      Modbus_Trans_Array[tx_count ] = Holding_Register_Array[start_addr] >> 8;
      tx_count ++;
      Modbus_Trans_Array[tx_count ] = Holding_Register_Array[start_addr];
      tx_count ++;
      start_addr++;
    }

     frame_length = tx_count +2;
    Get_CRC(Modbus_Trans_Array , frame_length );

}
else
{
    // Hatali Data Degeri Cevabi Hazirla
    Modbus_Trans_Array[0] = Modbus_Receive_Array[0];
    Modbus_Trans_Array[1] = (Modbus_Receive_Array[1] + 128);
    Modbus_Trans_Array[2] = 3; // 3. Byte : "03" ILLEGAL Data Value
     Get_CRC(Modbus_Trans_Array , 5);
    frame_length = 5;
}

}

char CRC_check(char package[],unsigned int package_length)
{
volatile unsigned int crc[2];
volatile unsigned int CRCFull = 0xFFFF;
volatile unsigned int CRCHigh = 0xFF, CRCLow = 0xFF;
volatile unsigned int CRCLSB;
volatile unsigned int i=0;
volatile unsigned int j=0;
char CRC_OK=0;

    for (i = 0; i < package_length-2; i++)
    {
        CRCFull = (unsigned int)(CRCFull ^ package[i]);

        for (j = 0; j < 8; j++)
        {
            CRCLSB =  (unsigned int)( CRCFull & 0x0001);
            CRCFull = (unsigned int)((CRCFull >> 1) & 0x7FFF);

            if (CRCLSB == 1)
                CRCFull = (unsigned int)(CRCFull ^ 0xA001);
        }
    }
    crc[1] = CRCHigh = (unsigned int)((CRCFull >> 8) & 0xFF);
    crc[0] = CRCLow  = (unsigned int)( CRCFull & 0xFF);

    if((crc[0] == package[package_length-2]) && (crc[1] == package[package_length-1]))
    CRC_OK = 1;
    else
    CRC_OK = 0;

    return CRC_OK;
}


void Get_CRC(unsigned char package[],unsigned int package_length)
{
    volatile unsigned int crc[2];
    volatile unsigned int CRCFull = 0xFFFF;
    volatile unsigned int CRCHigh = 0xFF, CRCLow = 0xFF;
    volatile unsigned int CRCLSB;
    volatile unsigned int i=0;
    volatile unsigned int j=0;
    volatile char CRC_OK=0;

    for (i = 0; i < package_length-2; i++)
    {
        CRCFull = (unsigned int)(CRCFull ^ package[i]);

        for (j = 0; j < 8; j++)
        {
            CRCLSB = (unsigned int)(CRCFull & 0x0001);
            CRCFull = (unsigned int)((CRCFull >> 1) & 0x7FFF);

            if (CRCLSB == 1)
                CRCFull = (unsigned int)(CRCFull ^ 0xA001);
        }
    }
    crc[1] = CRCHigh = (unsigned int)((CRCFull >> 8) & 0xFF);
    crc[0] = CRCLow  = (unsigned int)( CRCFull & 0xFF);
    package[package_length-2] = crc[0];
    package[package_length-1] = crc[1];
}


/* 
Anlamadığınız yerleri sorabilirsiniz.
İyi Çalışmalar

*/