Bu yazıda TDD( Test-Driven Development) prensiplerine göre geliştirmeye çalıştığım bir kütüphaneden bahsetmek istiyorum. Bir önceki yazıda bahsettiğim üzere TDD için başlıca kaynağım James Grenning’in Test-Driven Development for Embedded C kitabı olacak. TDD için kullandığım unit test framework’ü ise CppUTest olacak. CppUTest’in CubeIDE ile nasıl kullanılabileceğine dair bilgileri önceki yazıda paylaşmıştım. Bu yüzden o aşamaları geçip doğrudan bu yazının ana konusuna girmek istiyorum.
Burada bahsettiğim kodlara bu github linki üzerinden ulaşabilirsiniz. Bu yazıda asıl amacım gömülü yazılımda TDD yaklaşımı ile ilgili geniş kapsamlı örnek kodlar gösterebilmek. Bu yazıda github’daki kodların hepsini gösteremeyeceğim çünkü yazı çok uzayacak. Göstereceğim kodların bile sizi tatmin edeceğini düşünüyorum. Test kodlarının hepsini incelemek isterseniz github üzerinden erişebileceksiniz. Öyle çok fazla teorik bilgiye de boğmak istemiyorum. Bu yüzden teorik kısımlara çok az değinip kodlar üzerinden anlatmaya çalışacağım. Zaten çoğu kaynak sadece teorik olarak bahsediyor. Eğer verdiğim teorik bilgiler yeterli olmaz ise diğer kaynakları inceleyip gelebilirsiniz. Burada asıl amacım sizi kod örneklerine boğmak:)
Test odaklı veya güdümlü geliştirme, nasıl çevirirseniz çevirin, yazılım geliştirirken önce test kodlarının yazıldığı daha sonra üretim kodlarının yazıldığı bir yazılım geliştirme yaklaşımıdır. James Grenning’in kitapta iddia ettiği üzere böyle bir yaklaşımla yazılım geliştirildiğinde sonradan oluşabilecek hataların(bug) testler ile önceden önlenebileceği ve böylece bu hataların çözümüne ayrılan zamandan feragat edilebileceğini söylemektedir. Aşağıda kitaptan alınmış figürlere baktığımızda yazarın Debug-Later Programming dediği şekilde bir yazılım geliştirildiğinde oluşabilecek bir hatanın fark edilmesi ve bu hatanın çözülmesine kadar geçen zamanlar temsil edilmiştir. Burada Td yazılımı geliştirme süresi, Tfind hatanın bulunma süresini ve Tfix ise o hatanın giderilme süresini temsil etmektedir.
TDD prensibine göre bir geliştirmede ise aşağıdaki gibi süreç olacağını iddia etmektedir. Burada önce testleri yazıp daha sonra üretim kodları yazılacağı için hatanın daha yazılım geliştirme sürecinin başlarında bulunabileceğini söylemektedir.
Gömülü Sistemler İçin Faydaları
Ortada daha donanım yok iken üretim kodu geliştirmek.
Hedef donanım üzerindeki debug süresini azaltmak.
Modülleri birbirinden ve donanımdan ayırarak yazılım tasarımını geliştirmek. Test edilebilir kod zorunlu olarak modülerdir.
Bu son maddeyi bu yazıdaki kütüphaneyi yazarken çok iyi anladım. Bu kadar teorik bilgi yeter :). Birazda uygulama tarafına girelim.
Öncelikle bu kütüphanenin özelliklerinden biraz bahsetmek istiyorum. Bu kütüphaneyi geliştirmeye başlarken asıl amacım şuan için TCP/IP üzerinden haberleşme yapabilmekti. ESP8266’yı AT komutları ile kullanabilecek bir kütüphane geliştirmek istedim. Bildiğiniz üzere AT komutları ile haberleşmek başlı başına bir zulüm. Bu yazıda amacım aynı zamanda bunu en verimli bir şekilde yapabilecek bir kütüphane geliştirmekti. Bu kütüphane arka planda ring buffer kullanacak. Ring buffer kütüphanesinin doğrudan donanım ile ilgisi yok bu yüzden testlerini yazmak çok basit ve hızlı oldu. Asıl zor kısımlar donanımla doğrudan ilişkili fonksiyonların testlerini yazmak. Örneğin; UART üzerinden veri alıp gönderen bir fonksiyonun testi nasıl yazılabilir? Merak etmeyin bunu da bu yazıda göreceğiz.
Kısaca bu kütüphane ESP8266(ESP-01 modülü) ile haberleşmek için AT komutlarını kullanacak. UART üzerinden okunan verileri tutmak için ring buffer kullanacak. Bu yüzden bu kütüphaneyi geliştirmeden önce basit bir ring buffer kütüphanesi geliştirdim. Önce ring buffer testlerini yazdım daha sonra ESP8266 kodlarının testlerini yazdım.
CppUTest ile bir test oluşturmak için başlıca kuracağımız yapı aşağıdaki gibi olacak. Bir testin önce bir test grubu olmalıdır. Bu yüzden ilk olarak aşağıdaki gibi bir test grubu oluşturmak gerekmektedir. Daha sonra bu test grubu ile çalışacak bir TEST makrosu oluşturmak gerekiyor.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TEST_GROUP(RingBuffer_Test_Group)
{
voidsetup()
{
}
voidteardown()
{
}
};
TEST(RingBuffer_Test_Group,InitTest)
{
}
Bu test group içine opsiyonel olarak setup ve teardown fonksiyonları eklenebilir. Alttaki TEST makrosu ise testlerimizi yazdığımız kısım olacak. Burada ilk parametre test grubunun adı ve ikinci parametre ise test yapılacak olan işleme verilen isim olacak. Bu ismi vermek size kalmış. Kodun okunabilirliği açısından ve tam olarak hangi özelliğin test edildiğinin anlaşılabilmesi için düzgün bir isim verilmesi gerektiği söylenmektedir.
Burada aynı test grubuna ait testlerden her birinin öncesinde önce setup fonksiyonu çağrılır. Bu fonksiyon içinde genel ayarlamalar yapılabilir. Her test sonunda ise teardown fonksiyonu çağrılır. Burada ise testlerden sonra yapmak istediğimiz işlemleri yapabiliriz.
İlk olarak ring buffer kütüphanesi için bir “init” fonksiyonu oluşturmak istiyorum. Bu fonksiyon içine buffer’ın boyutunu belirleyecek bir değer alsın ve dönüş değeri olarak ise ring buffer için tanımlanmış bir structure döndürsün. Bunun için önce fonksiyonları temel olarak oluşturup içlerini boş bırakıyorum. TDD de amaç önce testi başarısız kılmak daha sonra bu testi geçecek bir kod yazmaktı unutmayalım.
C
1
2
3
4
5
6
7
typedefstruct
{
uint32_t head;
uint32_t tail;
uint32_t size;
uint8_t*buffer;
}RingBuffer;
1
2
3
4
5
RingBuffer*ringBuffer_init(uint32_t size)
{
RingBuffer*rBuf;
returnrBuf;
}
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEST_GROUP(RingBuffer_Test_Group)
{
};
TEST(RingBuffer_Test_Group,InitTest)
{
// Arrange
RingBuffer*testBuf;
// Act
testBuf=ringBuffer_init(50);
// Assert
LONGS_EQUAL(50,testBuf->size);
}
Bir test basitçe üç kısımdan oluşur. Arrange, Act ve Assert. Burada önce testler ile ilgili tanımlamalar,ayarlamalar daha sonra test edilecek kısım ve son olarakta beklentiler kontrol edilir. Yukarıdaki test kodu basitçe ringBuffer_init() fonksiyonunun içine aldığı parametreye göre ring buffer’ın boyutunu kontrol eder. Bu testi çalıştırdığımızda aşağıdaki gibi bir sonuç alırız.
İlk testimizi yazdık ve şimdi bu testi geçecek kodları aşağıdaki gibi yazıyorum. Burada buffer’ı dinamik olarak oluşturuyorum.
Bu aşamadan sonra testleri bir daha çalıştıralım. Bu sefer aşağıdaki gibi bir sonuç alıyoruz.
Şuan bu testten geçtik ama testler sadece bu kadar mı olmalı tabi ki hayır. Bu fonksiyon burada sadece “size” değişkeninin değerini değiştirmiyor. Sadece “size” değişkeninin değerini test edip geçersek diğer taraflarda oluşabilecek hatalar gözden kaçabilir. Örneğin; bu fonksiyonda dinamik olarak bir buffer oluşturduk ama gerçekten bir buffer hafızada oluştu mu? Bu yüzden bir fonksiyonun bütün özelliklerinin test edilmesi daha iyi olacaktır. Böylesi TDD’ye daha uygundur. Bunu sadece tek TEST makrosu altında veya ayrı ayrı makrolar içinde yapabiliriz. Ayrı test makroları içinde yapılması önerilir.
Ring buffer kodlarını test ederken ringBuffer_init fonksiyonunu her test için kullanacağım çünkü hangi fonksiyonu test edeceksem öncesinde bu fonksiyonun çağrılması gerekmektedir. Bunun için şöyle bir yol izleyebiliriz. Yukarıda bahsettiğimiz setup() fonksiyonunun içinde ringBuffer_init fonksiyonunu çağırabiliriz. Böylece her testten önce bu fonksiyon çağrılarak bir ring buffer oluşturulacaktır. Bu yüzden kodu aşağıdaki gibi düzenliyorum ve ekstra assertion makroları ekliyorum.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "CppUTest/TestHarness.h"
#include "ring_buffer.h"
TEST_GROUP(RingBuffer_Test_Group)
{
RingBuffer*testBuf;
voidsetup()
{
testBuf=ringBuffer_init(50);
}
voidteardown()
{
}
};
TEST(RingBuffer_Test_Group,InitTest)
{
LONGS_EQUAL(50,testBuf->size);
CHECK(testBuf->buffer!=NULL);
LONGS_EQUAL(0,testBuf->head);
LONGS_EQUAL(0,testBuf->tail);
}
İkinci fonksiyon olarak ringBuffer_push() fonksiyonu için testler yazmışım. Neden init fonksiyonundan sonra de-init fonksiyonunu yazmamışım orası da muamma. Neyse bu fonksiyon isminden de anlaşıldığı gibi ring buffer içerisine integer bir değer eklesin ve bu değer işaretsiz 8 bit bir sayı olsun( UART’tan 8 bitlik veriler alacağımız için).
Bu fonksiyonu test etmek için öncelikle içine iki farklı değer gönderiyorum ve fonksiyona geçtiğim değerler gerçekten ring buffer’a işlenmiş mi bunu kontrol ediyorum. Bu testi çalıştırdığımda aşağıdaki gibi hata çıktısı alıyoruz çünkü push fonksiyonuna yukarıda görüldüğü gibi hiçbir şey yazmadım.
Şimdi push fonksiyonunun içini testi geçecek şekilde aşağıdaki gibi dolduruyorum.
Bu aşamadan sonra testleri tekrar çalıştırıyorum. Testler başarılı fakat ben bu fonksiyonunun bir özelliğini daha test etmek istiyorum. Ring buffer mantığına göre buffer dolduktan sonra tekrar ilk elementten itibaren buffer’a kaydetmesini istiyorum. Zaten yukarıdaki fonksiyonu buna göre yazmıştım. Bu özelliği test etmek için ise aşağıdaki test makrosunu oluşturuyorum. Tabi burada biraz TDD’ye aykırı hareket etmiş oldum. Bunun farkındayım:)
ringBuffer_push(testBuf,0xFF);// This data must be the first element of buffer.
LONGS_EQUAL(0xFF,testBuf->buffer[0]);
}
Testleri tekrar çalıştırdığımda aşağıdaki gibi bir çıktı alıyorum. Gördüğünüz gibi şuan için testler başarılı.
Burada konsol çıktılarını daha iyi bir hale getirebiliriz. Bunun için main.cpp içindeki RunAllTests metoduna verbose modunda çalışacak şekilde “-v” parametresini geçiyorum.
Bu işlemi yaptıktan sonra test çıktıları aşağıdaki gibi görünüyor.
Şimdi biraz da ESP8266 driver testlerine değinelim. Bu kütüphanenin arka planda UART üzerinden haberleşeceğini söylemiştim. Peki UART kullanan bir fonksiyonun testi nasıl yapılır? Tamam burada testler yine donanım üzerinde koşuyor ama ya elimizin altında bir donanım olmasaydı? Kodu bilgisayarda test edip donanım elimize ulaşana kadar biz kodlarımızı geliştirmeye devam etmek isteseydik bunu nasıl yapabilirdik? Kitapta bahsettiği gibi “Test edilebilir kod doğal olarak modülerdir.” sözünü burada daha iyi anlıyoruz. Kitapta bahsettiği üzere bu tarz fonksiyonları test edebilmek için o fonksiyonları doğrudan kendimizin modellemesi veya mock kütüphanelerinden faydanabileceğimizden bahsetmiş. Burada basitçe kütüphanenin UART ile olan ilişkisini fonksiyon işaretçileri ile modelleyip kullanabiliriz. Bu işlemi structure üzerinden yapmak istiyorum. Bunun için esp8266.h dosyası içine aşağıdaki gibi bir structure oluşturuyorum.
C
1
2
3
4
5
6
typedefstruct
{
void(*UART_Transmit)(uint8_t*);
uint8_t(*UART_Receive)(void);
uint32_t(*getTick)(void);
}Esp_Init_Typedef;
Bu structure üzerinden arka planda UART işlemlerini yapmak istiyorum. Bu kütüphaneyi kullanırken ise bu fonksiyon işaretçilerine benim implemente ettiğim fonksiyonların adresini atamak istiyorum. Bunu da init fonksiyonu içinde yapabilirim. Şimdi tests.cpp dosyası içinde bu fonksiyonları aşağıdaki gibi mock kütüphanesi ile modellersem bu fonksiyonların testini yapabilirim.
Bu fonksiyonların içeriğinden aşağıda birazdan bahsedeceğim. Daha sonra ESP_Init() fonksiyonunu içi boş olacak şekilde oluşturuyorum.
1
2
3
4
5
6
7
int32_t ESP_Init(void(*UART_Transmit)(uint8_t*),
uint8_t(*UART_Receive)(void),
uint32_t(*getTick)(void),
uint32_t UART_Buffer_Size)
{
return-1;
}
Test fonksiyonunu ise aşağıdaki gibi oluşturabiliriz. Burada Init fonksiyonu eğer bir hata yok ise 1, var ise -1 değerini dönsün istiyorum. Bu yüzden başlangıçta testin başarısız olacağı bir testi aşağıdaki gibi yazıyorum.
1
2
3
4
5
6
7
8
TEST(EspDriver_Test_Group,Esp_Init_Test)
{
intresult=ESP_Init(UART_Transmit_Fake,
UART_Receive_Fake,
getTick_Fake,
100);// buffer size
LONGS_EQUAL(1,result);
}
Bu aşamada testlerimizi çalıştırdığımızda aşağıdaki gibi bir çıktı alırız.
Test başarısız olduğuna göre şimdi init fonksiyonun içini doldurabiliriz.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int32_t ESP_Init(void(*UART_Transmit)(uint8_t*),
uint8_t(*UART_Receive)(void),
uint32_t(*getTick)(void),
uint32_t UART_Buffer_Size)
{
if(UART_Transmit!=NULL&&
UART_Receive!=NULL&&
getTick!=NULL)
{
ESP8266.UART_Receive=UART_Receive;
ESP8266.UART_Transmit=UART_Transmit;
ESP8266.getTick=getTick;
rx_buffer=ringBuffer_init(UART_Buffer_Size);
if(rx_buffer!=NULL)
return1;
else
return-1;
}
else
{
return-1;
}
}
Testlerimi tekrar çalıştırdığımda sonuçların aşağıdaki gibi başarılı olduğunu görüyoruz.
Şimdi orada bazı mock metodları kullandık. Ne olduğunu, nasıl çalıştığını ilk başta anlamak biraz zor ama gözünüz korkmasın. Sıradaki test ile basit bir yapısının olduğunu göreceksiniz. Sıradaki testi Send_AT_Command() fonksiyonu için yazmak istiyorum. Bu fonksiyon UART üzerinden AT komutları göndersin. Bu fonksiyon için testleri aşağıdaki gibi yazıyorum.
C
1
2
3
voidSend_AT_Command(char*cmd)
{
}
C
1
2
3
4
5
6
7
8
9
10
TEST(EspDriver_Test_Group,Send_AT_Command_Test)
{
mock().expectOneCall("UART_Transmit_Fake").withStringParameter("data","Test");// UART_Transmit_Fake function waits "Test" string.
// There is no assertion macro here because the function returns nothing.
// Mocking library checks if the function has been called.
}
Mock kullanımı için başlıca üç aşama bulunmaktadır. İlk olarak expectOneCall tarzı metotlar ile birazdan hangi fonksiyonun çağrılacağı bildirilmektedir. Burada expectOneCall metodunun içine birazdan çağıracağımız fonksiyonun adı ve parametre olarak ise “Test” içeren bir string alacağını bildiriyoruz. UART transmit için yazdığımız mock fonksiyonunu bir hatırlayalım tekrar.
Bu fonksiyonda ise actualCall metodu yukarıdaki gibi kullanılabilir. Burada kontrolü yapılan olay ise şudur. UART_Transmit_Fake fonksiyonu içine aldığı parametre ile çağrılması bekleniyor mu? Yani ben bu fonksiyonu her çağırdığımda mock kütüphanesi ile bu fonksiyonun çağrılması bekleniyor mu beklenmiyor mu gibi bir test yapabiliriz. Burada bu işlemi ring buffer kullanarak ta yapabiliriz. Örneğin; Bu fonksiyon aldığı her parametreyi bir tx buffer’a koyup, testlerde ise gerçekten tx buffer’a işlenip işlenmediği test edilebilir. Burada UART transmit işlemini polling metodu, receive işlemini kesme ile yapmak istiyorum.
Mock işleminin son aşamasında ise beklentiler kontrol edilir. Bunu mock().checkExpectations() metodunu çağırarak yapabiliriz. Ben bunu TEST_GROUP makrosu içinde teardown() fonksiyonunun içinde aşağıdaki gibi yapıyorum. Daha sonra mock().clear() metodunu çağrılması gerekmektedir.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEST_GROUP(EspDriver_Test_Group)
{
voidsetup()
{
ESP_Init(UART_Transmit_Fake,
UART_Receive_Fake,
getTick_Fake,
100);// buffer size
}
voidteardown()
{
mock().checkExpectations();
mock().clear();
}
};
Şimdi bu testi çağırdığımızda aşağıdaki gibi bir çıktı alırız. Burada söylenen UART_Transmit_Fake fonksiyonunu çağıracağını söyledin ama çağırmadın gibisinden bir hata mesajıdır.
Send_AT_Command fonksiyonunu aşağıdaki gibi düzenliyorum. Burada aslında içinde sadece bir satır kod var peki bu kod nedir? Bu kod ESP8266 structure’ı içinde bulunan UART_Transmit fonksiyonunu çağırır. Bu fonksiyona ise ESP_Init() fonksiyonu ile bizim mock kullanan UART_Transmit_Fake fonksiyonun adresini atamıştık değil mi? Yani bu fonksiyon çağrıldığında dolaylı olarak UART_Transmit_Fake fonksiyonu çağrılır.
C
1
2
3
4
voidSend_AT_Command(char*cmd)
{
ESP8266.UART_Transmit((uint8_t*)cmd);
}
Bu bir satır kodu ekledikten sonra testimin geçeceğini düşünüyorum. Testlerimi tekrar çalıştırdığımda aşağıdaki gibi bir çıktı veriyor.
Burada UART üzerinden veri gönderme işlemini mock kütüphanesi ile modellediğimi düşünüyorum. Kütüphanedeki çoğu fonksiyon UART üzerinden ESP’ye komut göndermek için bu fonksiyonu çağıracak.
Şimdi UART üzerinden bir veri gelmesini nasıl modelleyebiliriz biraz da buna bakalım. Kütüphaneyi tasarlarken şöyle düşündüm. Bir ESP_UART_ReceiveHandler() fonksiyonum olsun, kullanıcı bu fonksiyonu alıp kendi UART interrupt fonksiyonu içerisinde aşağıdaki gibi kullansın. Kütüphane ise gelen verileri otomatik olarak arkaplanda ring buffer içerisine yazsın.
Test fonksiyonunu biraz inceleyelim. Burada mock kütüphanesine diyorum ki birazdan UART_Receive_Fake
fonksiyonunu çağıracağım ve dönüş değeri olarak “O” karakterinin integer değerini döndür. Bunu bu şekilde yapmamın sebebi kesme fonksiyonunun her byte geldiğinde çalıştırılmasıdır. Hemen ardından ESP_UART_ReceiveHandler() fonksiyonunu çağırıyorum. Normalde bu fonksiyon kesme oluştuğunda çağrılacak fakat burada sanki kesme oluşmuş gibi bu fonksiyonu çağırıyorum. Testimiz basitçe bu şekilde. Şimdi testi çalıştırıp başarısız olduğunu bir görelim.
Testi geçecek şekilde fonksiyonumu aşağıdaki gibi düzenliyorum. Burada ise ring buffer kütüphanesinden faydalanıp UART_Receive() fonksiyonunun döndürdüğü değeri buffera ekliyorum.
Testi bu aşamadan sonra tekrar çalıştırdığımda aşağıdaki gibi geçtiğini gözlemleriz.
Bu test fonksiyonumu biraz daha gerçeğe yakın bir şekilde test etmek istiyorum. Bunu receive handler fonksiyonunu yazmadan da yapabiliriz. Test fonksiyonunu aşağıdaki gibi düzenliyorum. Burada sanki ESP8266’dan “OK” mesajı gelmiş gibi modelliyorum.
STRCMP_EQUAL("OK\r\n",(char*)rx_buffer->buffer);// check the ring buffer.
}
Test fonksiyonunu biraz inceleyelim. Burada mock kütüphanesine diyorum ki birazdan UART_Receive_Fake
fonksiyonunu çağıracağım ve dönüş değeri olarak bana response dizisinin elemanlarını teker teker döndür. Son olarak ise ring buffer’a bu string işlenmiş mi diye kontrol ediyoruz. Test kodunu bu hale getirdiğimizde yine testlerden geçtiğini görüyorum. Bu iki işlemi ayrı ayrı testler olarak ta yapabiliriz.
Bir sonraki testi Read_Response() fonksiyonu için yapmak istiyorum. Bu fonksiyon char pointer bir parametre alsın ve buffer içerisinde böyle bir string değerin olup olmadığını kontrol eden bir fonksiyon olsun.
Bu testi geçecek kodu ise aşağıdaki gibi düzenliyorum. Burada da yine ring buffer için yazdığım fonksiyonu kullanıyorum. Bu fonksiyon eğer buffer içerisinde beklenen string var ise 1, yok ise 0 döndürür.
Buraya kadar aslında UART üzerinden kesme ile verileri alan ve polling yöntemi ile UART üzerinden mesaj gönderen fonksiyonların testini basitçe yapmış olduk. Bunu bu şekilde gömülü sistemlerde kullandığımız başka haberleşme çevre birimlerine de uyarlayabiliriz.
Peki yukarıda getTick_Fake fonksiyonu oluşturduk ve bunu ne için kullanacağız. Bu kütüphaneyi yazarken hedeflerimden biri bunu en verimli bir şekilde yapabilmekti. AT komutları ile haberleşme yapıldığında her komutun cevabı aynı zamanda gelmiyor. Bazen bu cevaplar çok hızlı olurken bazen üç-beş saniye sürebiliyor. Buradaki amacım öyle bir kütüphane yazayım ki ESP8266’dan cevap gelene kadar beklesin ve cevap gelir gelmez hemen sıradaki komutu göndersin. Buradaki beklemeyi yapabilmek için kütüphaneye zamanla ilgili bir fonksiyon vermem gerekir. Bu fonksiyon tick değerini bana döndürsün. Yani burada zamanla ilişkili bir fonksiyonu modellemem gerekti. Bunu aşağıdaki gibi yapabiliriz.
C
1
2
3
4
5
6
7
8
9
10
11
uint32_t time=0;
uint32_t getTick_Fake(void)
{
if(time==0xFFFFFFFF)
time=0;
else
time+=1;
returntime;
}
Burada global bir değişken olan time, getTick_Fake() fonksiyonu her çağrıldığında bir artacak şekilde modelleyebiliriz. Normalde bu fonksiyonu nihai projede implemente edip ESP_Init() fonksiyonuna geçeceğim. Oradaki çalışma mantığı da buna benzer olacak. Aradaki tek fark time değişkeni bir timer tarafından arttırılacak. Bunu aşağıda test kodu ve üretim kodunu incelediğimizde daha iyi anlayacağız.
Yukarıda bahsettiğim bekleme işini yapmak için Wait_Response() isminde bir fonksiyon oluşturuyorum. Bu fonksiyon char pointer ve bir timeout değeri alsın. Dönüş değeri olarak ise Status değeri döndürsün.
C
1
2
3
4
5
6
7
8
typedefenum
{
FOUND=0,
TIMEOUT_ERROR,
STATUS_OK,
STATUS_ERROR,
IDLE,
}Status;
C
1
2
3
4
Status Wait_Response(char*response,uint32_t timeout)
{
returnSTATUS_ERROR;
}
Burada iki adet test fonksiyonu yazmak istiyorum. Yani yukarıda yaptığım gibi her zaman tek bir test yazıp onun geçtiği görüp aynı fonksiyonu test eden başka testler yazmak zorunda değiliz. Aşağıdaki gibi önce fonksiyonun bütün özelliklerini test eden testleri yazıp daha sonra üretim kodunu yazmak TDD’ye daha uygundur.
Burada hem timeout durumunu hem de fonksiyonun normal şartlar altındaki durumunu(beklenen cevap geldiyse) test ediyorum. Yukarıdaki test kodlarında ESP_UART_ReceiveHandler() fonksiyonu hiç çağrılmadığı için buffer başlangıçta olduğu gibi boş kalacaktır. Bu yüzden Wait_Response() fonksiyonu timeout süresi olarak 1000 girildiği için arkaplanda getTick_Fake() fonksiyonunu 1000 kere çağırıp eğer hala “OK” mesajı gelmedi ise TIMEOUT_ERROR değerini dönecektir.
Alttaki test kodunda ise for döngüsü içerisinde ESP_UART_ReceiveHandler() fonksiyonu çağrılarak bir nevi kesme işlemi modellenmiştir. Normalde kesme fonksiyonları main içerisinden çağrılmazlar. Böylece fonksiyon timeout süresi dolmadan buffer’da bir “OK” mesajını bulacak ve buna karşılık FOUND değerini dönecektir.
Testi geçecek kodları ise aşağıdaki gibi yazıyorum.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Status Wait_Response(char*response,uint32_t timeout)
Testler tekrar çalıştırıldığında sonuçlar aşağıdaki gibi oluyor. Burada ise zamanla ilişkili bir fonksiyonu nasıl modelleyebileceğimizi ve testlerinin nasıl yapılabileceğini gördük.
Son olarak bir fonksiyonun daha testinden bahsedip yazıyı bitirmeyi planlıyorum. Aksi takdirde yazı çok uzayacak. Bu kütüphanenin testlerini yazarken bu aşamadan sonra yazdığım fonksiyonların içeriğinin sürekli birbirini tekrar ettiğini gördüm. Örneğin; Connect_Wifi() , Connect_TCP_Server() gibi fonksiyonların içinde sürekli şöyle bir döngü olduğunu farkettim. Önce bir AT komutu gönderip bunun cevabının “OK” veya “SEND OK” olması gerektiği , ardından başka bir komut gönderip bunun da aynı şekilde beklenen bir cevap olması gerektiği gibi birbirini takip eden komut-cevap silsilesi olarak devam ettiğini gördüm. Ayrıca fonksiyonların içindeki kodlar gittikçe uzamaya başlamıştı. Bu da okunabilirliği oldukça azaltıyordu. Burada bu komutları işleyebilecek başka bir fonksiyon olması gerektiğini düşündüm. Bu fonksiyona Command_Process() ismini verdim. Bu fonksiyon içine iki adet iki boyutlu char dizisi alsın ve bunlardan biri sırası ile gönderilmek istenen AT komutlarını içersin, diğeri ise sırası ile gönderilen komutlara hangi cevabın gelmesi gerektiğini içersin. Son parametre olarak ise dizide kaç adet komut olduğunu bildiren integer bir değer olsun. Böylece bu fonksiyonu yukarıda bahsettiiğim fonksiyonların içinde kullanabilecektim ve kodumun okunabilirliğinin artacağını düşündüm.
C
1
2
3
4
Status Command_Process(char**commandArray,char**responseArray,uint8_t numberOfCommands)
Test fonksiyonu biraz kafa karıştırıcı kabul ediyorum fakat burada yaptığımız işlemler yukarıdakilere benzer işlemler. Sadece satır olarak biraz fazla. Buradaki diziler bizim fonksiyona geçtiğimiz komutlar ve bu komutlara karşılık beklenen cevapları içermektedir. Bunları ön işlemci direktifi olarak aşağıdaki gibi tanımlıyorum.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
/* AT commands definitions ------------------------------------------------------------------*/
#define AT_CWMODE_STATION "AT+CWMODE=1\r\n"
#define AT_CWQAP "AT+CWQAP\r\n"
#define AT_CWJAP "AT+CWJAP="
#define AT_CIPCLOSE "AT+CIPCLOSE\r\n"
#define AT_CIPMUX_SINGLE "AT+CIPMUX=0\r\n"
#define AT_CIPSTART_TCP "AT+CIPSTART=\"TCP\","
#define AT_CIPSEND "AT+CIPSEND="
#define AT_RESPONSE_OK "OK"
#define AT_RESPONSE_ERROR "ERROR"
#define AT_RESPONSE_SEND_OK "SEND OK"
#define AT_RESPONSE_GREATER_THAN ">"
Daha sonra bu komutları mock fonksiyonları ile birazdan çağıracağımızı bildiriyorum. Bir sonsuz döngünün içinde Command_Process() fonksiyonunu çağırıyorum. Bu fonksiyon sırası ile bu komutları gönderecek ve o komuta karşılık gelen cevabı belli bir timeout süresi boyunca bekleyecek. Bu bekleme sırasında ise IDLE değerini dönecek. Eğer IDLE’dan başka bir değer döndü ise ya hata oluşmuş yada komutlar başarılı olarak işlenmiş demektir.
Command_Process() fonksiyonunun içini aşağıdaki gibi yazıyorum.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Status Command_Process(char**commandArray,char**responseArray,uint8_t numberOfCommands)
Bu fonksiyonları neden böyle bir state döndürecek şekilde yazdığımdan bahsetmek istiyorum. İlk olarak böyle bir şekilde yazmasaydım bu fonksiyonların testlerini tam olarak yapamıyordum. İkinci olarak ise bu şekilde tasarlamak state-machine veya RTOS içeren bir projede verimli bir şekilde kullanılabilecek şekilde olmasıydı. Örneğin; bu fonksiyonları RTOS içeren bir projede herhangi bir task içerisinde kullandığımda atıyorum Connect_Wifi() fonksiyonu için bir taskı sürekli olarak çalıştırmam gerekmeyecekti. Yani bu fonksiyon IDLE dönüyorsa task yield fonksiyonlarını çağırıp bir sonraki task’ı çalıştırabiliriz. Böylece wifi’ye bağlanmak için geçen süreyi diğer task’ları çalıştırarak değerlendirebiliriz.
Diğer fonksiyonların testlerinden yazıyı daha fazla uzatmamak için bahsetmeyeceğim. Diğer test kodlarını yazıda verdiğim github linkinden inceleyebilirsiniz. Sormak istediğiniz kısımlar olursa yorumlarda tartışabiliriz. Aşağıda bu kütüphanenin donanım üzerindeki çalışması ile ilgili de basit bir örnek vermek istiyorum.
Ben TDD’yi bir süredir araştırıyorum ama ilk kez bu kütüphaneleri geliştirirken deneyimledim. Bu kütüphaneye başlamadan önce merak ettiğim bir şey vardı. Ben kodları donanım ile ilişkisi olmadan TDD prensiplerine göre geliştirirsem gerçekten de nihai kodlar donanım üzerinde bir hata olmadan çalışır mıydı? Yani donanım elimizde yok iken geliştirdiğim bir kütüphane ilk denemede donanım üzerinde çalışır mıydı? Bunu bu kütüphaneyi geliştirirken denedim. Donanım üzerinde bu kodları en son denedim. Peki çalıştı mı? Tabi ki hayır 🙂 Tek bir hata yüzünden ilk denemede çalışmadı fakat TDD sayesinde bu hatayı bulmam çok hızlı oldu. Zaten entegrasyon testleri de bunun için var değil mi?
TDD’yi hem iş hayatında hem de kendi projelerimde kullanmayı bir alışkanlık haline getirmek istiyorum. Buradaki deneyimlerime göre rahatça söyleyebilirim ki TDD donanımla çok içli dışlı olduğumuz gömülü yazılımda daha elimizin altında hedef donanım yok iken gerçekten de yazılımı belli bir aşamaya kadar getirmede oldukça faydalı olacak gibi görünüyor. Tıpkı kitapta bahsedildiği gibi.
Bir sonraki yazıda bu kütüphanenin üzerine bir şeyler daha koyup bir MQTT kütüphanesi geliştirmek istiyorum. O kütüphaneyi de TDD prensiplerine göre geliştireceğim ama yazıda TDD’den ziyade MQTT üzerinden kriptolanmış mesajlar gönderme ve alma üzerine bir uygulamadan bahsetmek istiyorum. Sonraki yazıda görüşmek üzere.
Mehmet abi, yine çok güzel bir yazı olmuş. Abi hiç foc (Field Oriented Control) konusunda çalışma yaptın mı? Bir süredir bu konuyla ilgileniyorum eğer önceden konuyla alakalı araştırma yaptıysan anlayamadığım yerde sana danışmak isterim. Saygılarımla
Teşekkür ederim. Daha önce hiç foc ile ilgili bir çalışmam olmadı ama yakın zamanda olacak gibi. Bana mail atabilirsin bu konuda fikir alışverişi yapabiliriz.
Mehmet abi, nerelerdesin? Abi beni güzel bilgilerinden 7 ay mahrum ettin 🙂 Yeni yazı gelir mi yakında.
😀Teşekkür ederim böyle sıkı bir takipçim olduğun için. Bu aralar işten güçten vakit buldukça MQTT kütüphanesi yazmaya çalışıyorum ama ne zaman biter emin değilim. Bu yazının sonunda bahsettiğim konu üzerine çalışıyorum ama dediğim gibi işlerden vakit buldukça. O yazıda QT ile yazılmış bir arayüzü de yazıya koymayı düşünüyorum. Umarım en kısa zamanda yayınlayabilirim.
Merhaba Mehmet bey.
Stm32 F41 serisi kart kullanıyoruz. Üzerinde sim800L ve Gps için Neo6 modül var ama ikisini birlikte kullanmadık. Biz projede her ikisini aynı anda kullanmak istiyoruz. Ayrı ayrı çalışıyor ikisini aynı anda çalıştırmayı denediğimizde Neo6 hep aynı noktayı gösteriyor. İkinin aynı çalışması mümkün mü bu noktada yardımcı olabilirmisiniz.
Merhaba,
Evet ikisinin aynı anda çalışması mümkün. Kodu ve bağlantı şemasını görmeden kesin bir şey diyemem fakat birkaç tavsiye verebilirim. Bu söyleyeceğim çok basit bir şey ama bazen en basit konulurda bile hata yapılabiliyor. Umarım iki modülü de aynı UART birimine bağlamamışsınızdır çünkü UART, SPI veya I2C gibi birden fazla cihazla aynı anda haberleşmeyi desteklemez. Yani iki cihazı aynı anda kullanıyorsanız birini atıyorun UART1 diğerini UART2 çevrebirimine bağlamanız gerekir. Bunların iki ayrı çevrebirimine bağlı olduğunu farz ediyorum. Bu durumda eğer HAL kütüphanesinin UART fonksiyonlarını kullanıyorsanız HAL kütüphanesi error durumuna düşüp veri okumayı sonlandırabiliyor. Bunun için debug altında huart.ErrorCode değişkenini izleyip bu hata koduna göre bir çözüm aramanız gerekmektedir. GPS in aynı konumu sürekli göstermesi olayında şöyle bir hata yapıyor olabilirsiniz. Gelen verileri okuyup parçalayıp konum bilgisini çektikten sonra buffer’ı temizleyin çünkü bir sonraki veri geldiğinde( NMEA veya UBX) önceki verinin sonuna yazılmaya başlayacak ve siz konum bilgilerini çekmek için buffer üzerinde tekrar dolaştığınızda daha önce gelen konum bilgisini yakalayacak ve sonrakine bakmayakcatır. Bu yüzden GPS ten her veri aldığınızda UART buffer’ı temizleyin. UART’tan okuma yaparken ring buffer kullanabilirsiniz. Kodları ve şemayı görmeden söyleyebileceklerim bu kadar. Eğer gizli değilse kodları mehmettopuz127@gmail.com adresine gönderirseniz daha detaylı yorum yapabilirim.
Bu arada geç cevap verdiğim için kusura bakmayın yoğunluktan anca bugün cevaplayabildim.
Kolay gelsin…
Mehmet abi, yine çok güzel bir yazı olmuş. Abi hiç foc (Field Oriented Control) konusunda çalışma yaptın mı? Bir süredir bu konuyla ilgileniyorum eğer önceden konuyla alakalı araştırma yaptıysan anlayamadığım yerde sana danışmak isterim. Saygılarımla
Teşekkür ederim. Daha önce hiç foc ile ilgili bir çalışmam olmadı ama yakın zamanda olacak gibi. Bana mail atabilirsin bu konuda fikir alışverişi yapabiliriz.
Mehmet abi, nerelerdesin? Abi beni güzel bilgilerinden 7 ay mahrum ettin 🙂 Yeni yazı gelir mi yakında.
😀Teşekkür ederim böyle sıkı bir takipçim olduğun için. Bu aralar işten güçten vakit buldukça MQTT kütüphanesi yazmaya çalışıyorum ama ne zaman biter emin değilim. Bu yazının sonunda bahsettiğim konu üzerine çalışıyorum ama dediğim gibi işlerden vakit buldukça. O yazıda QT ile yazılmış bir arayüzü de yazıya koymayı düşünüyorum. Umarım en kısa zamanda yayınlayabilirim.
Merhaba Mehmet bey.
Stm32 F41 serisi kart kullanıyoruz. Üzerinde sim800L ve Gps için Neo6 modül var ama ikisini birlikte kullanmadık. Biz projede her ikisini aynı anda kullanmak istiyoruz. Ayrı ayrı çalışıyor ikisini aynı anda çalıştırmayı denediğimizde Neo6 hep aynı noktayı gösteriyor. İkinin aynı çalışması mümkün mü bu noktada yardımcı olabilirmisiniz.
Merhaba,
Evet ikisinin aynı anda çalışması mümkün. Kodu ve bağlantı şemasını görmeden kesin bir şey diyemem fakat birkaç tavsiye verebilirim. Bu söyleyeceğim çok basit bir şey ama bazen en basit konulurda bile hata yapılabiliyor. Umarım iki modülü de aynı UART birimine bağlamamışsınızdır çünkü UART, SPI veya I2C gibi birden fazla cihazla aynı anda haberleşmeyi desteklemez. Yani iki cihazı aynı anda kullanıyorsanız birini atıyorun UART1 diğerini UART2 çevrebirimine bağlamanız gerekir. Bunların iki ayrı çevrebirimine bağlı olduğunu farz ediyorum. Bu durumda eğer HAL kütüphanesinin UART fonksiyonlarını kullanıyorsanız HAL kütüphanesi error durumuna düşüp veri okumayı sonlandırabiliyor. Bunun için debug altında huart.ErrorCode değişkenini izleyip bu hata koduna göre bir çözüm aramanız gerekmektedir. GPS in aynı konumu sürekli göstermesi olayında şöyle bir hata yapıyor olabilirsiniz. Gelen verileri okuyup parçalayıp konum bilgisini çektikten sonra buffer’ı temizleyin çünkü bir sonraki veri geldiğinde( NMEA veya UBX) önceki verinin sonuna yazılmaya başlayacak ve siz konum bilgilerini çekmek için buffer üzerinde tekrar dolaştığınızda daha önce gelen konum bilgisini yakalayacak ve sonrakine bakmayakcatır. Bu yüzden GPS ten her veri aldığınızda UART buffer’ı temizleyin. UART’tan okuma yaparken ring buffer kullanabilirsiniz. Kodları ve şemayı görmeden söyleyebileceklerim bu kadar. Eğer gizli değilse kodları mehmettopuz127@gmail.com adresine gönderirseniz daha detaylı yorum yapabilirim.
Bu arada geç cevap verdiğim için kusura bakmayın yoğunluktan anca bugün cevaplayabildim.
Kolay gelsin…