STM32 DHT11 ve Uart Kullanımı
|Merhabalar,
Bu konuda STM32 serisi kartlar ile STM32CubeMX kullanarak yapacağımız uygulamalara bir girş olması açısından DHT11 kullanımı ile ilgili basit bir uygulama yapacağız. Bu uygulamada herhangi bir external kütüphane kullanmadan sadece HAL kütüphanaleri kullanarak işlemleri gerçekleştireceğiz. Elimden geldiğince detaylı bir şekilde anlatımı yapmaya çalışacağım.
Uygulamada yapacağımız şey, DHT11′ den okuduğumuz sıcaklık ve nem verisini seri haberleşme ile bilgisayarda görmek. Bu işlemi Arduino’ da yapmak yeni başlamış biri için bile bir kaç dakika sürecek bir işlem. Ancak STM32 ile işler biraz daha kamaşık olacak. İlk olarak yukarıda da değindiğim STM32CubeMx’ Den bahsedeyim. Bu STM firmasının geliştirdiği bir yazılıp olup, STM32 serisi kartlar için konfigurasyonu kolaylaştırmak amacıyla kullanılır. Yani siz hangi GPIO’ u çıkış yapacağınızı hangi UART’ ı yada ADC’ yi kullanacağınızı STM32CubeMx yazılımını kullanarak bir arayüz üzerinden rahatlıkla karar verebilir ve gerekli ayarları basit bir şekilde yapabilirsiniz. Bağlantıdaki sayfanın en aşağısında bulunan “Get Software” butonundan üye olup yazılımı ücretsiz indirip kullanabilirsiniz.
DHT11 Çalışma Prensibi
Birazda DHT11′ i tanıyalım. DHT11 dijital bir sıcaklık nem sensörü. Dijital olduğu için sensör ile bir şekilde haberleşmemiz gerekecek. Gereken haberleşme hakkında bilgiyi en rahat bulabileceğimiz yer sensörün datasheet’ i. Bu datasheet’ e bağlantıdan ulaşabilirsiniz. Datasheet içerisinde cihazın boyutlarından, çalışma sıcaklık aralığına kadar bir çok bilgi var. Ancak bizim ilgilendiğimiz şeyler bunlar olmadığı için bu bilgileri atlıyoruz. Bizi ilgilendiren kısım “Data Timing Diagram” kısmından sonrası. Dahasheet’ den anlaşıldığı üzere DHT11 ile iletişime başlamak için data pinini 18 ms Low konumuna çekmek gerekiyor. Böylece DHT11 size data göndermeye başlıyacaktır. Sensör sıcaklık ve nem bilgisinden önce size bir adet senkronizasyon biti gönderecektir. Bu bit süreleri 80 us olan 1 adet low ve high durumundan oluşmakta. Bizim yapmamız gereken bu veri 50 us aralıklar ile geldimi diye kontrol etmek. Bunları algıladıktan sonra 40 bitlik sıcaklık ve nem verisini sensör göndermeye başlıyacaktır. 40 bitlik verinin nasıl gönderileceği yine datasheet’ de açıklanmış.
Resimdende anlaşılacağı üzere 50 uS low konumundan sonra gelen high olma durumunun süresine göre gelen data 1′ mi yoksa sıfır mı diye karar vereceğiz. Bu kısıma kadar anlaşılması çok güç bir durum yok. Sorun bunları STM32′ de nasıl yapacağımız. Datasheet’ deki açıklamalara göre mikrosaniye cinsinden zamanı ölçemeye ihtiyaç duymaktayız. Ancak STM32′ yi programlamak için kullanacağımız HAL kütüphanaleri içerisinde mikrosaniyelik bir gecikme bulunmamakta. O yüzden bunu bizim oluşturmamız gerekli. Bunun için de en kolay yol bir timer kullanmaktır. Amacının basit olması açısından kullanacağımız STM32 denetleyicisinin içerisindeki herhangi bir basic timer işimizi görecektir. Son olarak da sensörden aldığımız veriyi görmek için bir UART kullanmamız gerekli. Bunun için ST-Link’ e bağlı olan UART’ kullanmanız durumunda dışardan ek bağlantıya ve FTDI dönüştürücü kullanmanıza gerek kalmaz. Artık ne yapmamız gerektiğini bildiğimize göre STM32CubeMX açarak ayarlamalardan başlıayabiliriz. Ben elimdeki STM32F767 model numaralı kartı kullanacağım. Sizin bazı ayarlamaları elinizdeki karta göre yapmanız gerekebilir. Farklı olabilecek ayarları yeri geldikçe anlatmaya çalışacağım.
STM32CubeMX Ayarları
Öncelikle STM32CubeMX’ deki genel ayarlara bir bakalım. DHT11′ i bağlıyacağımız pini, Timer’ ı ve Uart’ ı aktif etmeliyiz. Burada ufak bir püf noktası olarak CubeMx arayüzünde denetleyici modelini seçimi “Board Selectror” sayfasından yaparsanız seçtiğiniz mikrodenetleyiciyi içeren kartın default ayarları yapılmış olarak gelir. Bu da sizi bazı clock ayarlaması yada St-Link’ e bağlı Uart ayarlaması gibi işlemlerden kurtarır. Ancak o anki projenizde kullanmıyacağınız bazı ek özellikler aktif olabilir. Bunları deaktif etmelisiniz. Benim kullandığım kart için Ethernet ve USB bağlantısı default’ da aktif olarak gelmişti, ancak ben onları deaktif ederekyukarıdaki resimde sağda görünen ayarlar kalacak şekilde ayarladım.
Seçtiğimiz bu arabrimlerin ayalarına gelecek olursak. Uart için default ayarları değişmedim. 115200 Baud rate ve 8 Bit olarak ayarlı. Ayarları yukarıdaki resimde de görülmekte. Timer kısmında ise bazı ayarlar yapmamız gerekli. Timer’ ı 1 uS’ lik zamanı ölçmek için kullanacağız. Bunu sağlamanın en kolay yolu normal bir timer’ ı yukarı sayma modunda her 1 us’ de 1 artacak şekilde ayarlıyarak yapabiliriz.
Bunun için prescaler oranını 108 olarak ayarladım. Ancak bunu neye göre belirledim. Bu Arduino ile timer konusunda da anlattığım timer frekansı formülünden gelmekte. Formülde
görülen osilatör hızı olarak yer alan pay kısmındaki 16 Mhz burda farklı bir değer olacak. Bu değeri STM32CubeMx’ in “Clock Configuration” sekmesinin altında ki timer frekansları kutusundaki değerden öğrenebiliriz.
Soldaki resimde de görüldüğü üzere benim bu değerim 108 Mhz olarak ayarlı. 108 * 10 ^ 6 / 108 = 1 Mhz olduğu için prescalar oranını 108 olarak belirledim. “Counter Period” olarak görünen 65535 ise bu timer’ in sayabileceği en yüksek değer olarak seçtim. Neden bunu seçtiğimi anlamak için timer’ in çalışma mantığına tekrar dönmeliyiz. Timer ayarlanan frekansda 0′ dan “counter period” un sonuna kadar sayar. Counter period dolduğu zaman ise timer interrupt’ ı oluşur ki üstteki formülde bize Arduino için Counter period’ unu veren formüldü. Bizim burada bu değeri seçmemizin sebebi de sayıcı olarak kullanacağımız timer’ i en uzun sürede saydırıp olabilecek en uzun süreyi ölçebilmek. 1 us’ de sayaç değeri 1 artan 16 bit timer en fazla 65 ms’ lik süreyi sayabilir. Kutuda yazacağını değere dikkat edin. Eğer sizin mikrodenetleyicinizin timer saat frekansı farklı ise 1 uS’ den daha uzun yada daha kısa gecikmeler oluşturabilirisiniz. Bu da sensörden veri okumanızda sorunlara sebep olur. Yazılım kısmına geçmeden önce yapmanız gerek son ayar zorunlu olmamak ile birlikte pinlere isim verebilirsiniz. Pinlere kullanacağınız amaca yönelik isimler verirseniz kullanım sırasında çok rahatlıkla pinlere erişebilirsiniz. Bunun için isim vermek istediğiniz pine sağ tıklayıp “Enter User Label” seçeneğine tıklamanız yeterlidir.
Artık yazılım aşamasına geçmeye hazırız. STM32CubeMx içerisindeki “Project Manager” sekmesinden projeyi oluşturacağımız klasörü ve kullanmak istediğimiz IDE’ yi seçebiliriz. Ben bu uygulamada Atollic kullanacağım. Size de bunu kullanmanızı tavsiye ederim. STM firmasının satın aldığı ve ücretsiz yaptığı bir IDE. Siz istediğiniz herhangi bir IDE’ yi de seçebilirsiniz. Bütün yazılımı main dosyasında yazacağımız için proje hiyerarşisi çok önemli değil.
Yazılım
STM32CubeMx ile oluşturduğunuz projede yazılım üzerinde düzenleme yaparken ekleme yapacağınız yerlere dikkat etmelisiniz. Projeyi oluşturduğunuzda kod satırları arasında bir çok yorum satırı olacaktır. Bu yorum satırları hem oradaki fonksiyonların ne işlevi olduğunu anlatabileceği gibi ek yazılımları nereye yazmanız gerektiğini size gösterirler. Buna dikkat etmezseniz ne olur? Yazılımını yine çalışır. Ancak CubeMx’ de sonradan bir değişiklik yapar ve projeyi export ederseniz yazdığını bütün kodlar silinecektir. Bu sorunla karşılaşmamak için aşağıdaki yorum satırları gibi “User Code” etiketine sahip alanlar içerisine değişiklik yapmalısınız.
/* USER CODE BEGIN 2 */ /* USER CODE END 2 */
Bizim yapmamız gereken ilk düzenleme de main fonksiyon içerisinde while’ ın hemen üstü olan konfigurasyonların yapıldığı yerden sonraki kısım. Buraya timer’ ı başlatma fonksiyonu ekleyeceiğim. Neden burası diye merek ederseniz, bu kısım CubeMx’ de yaptığımız ayarlamaların bittiği yer. Arduino için düşünürsek, seri haberleşmeyi başlatıp, pin ayarlarının yaptıktan sonraki kısım. Timer’ ı başlatmak için yine HAL kütüphanelerinin bir fonksiyonunu kullanacağınız. O da şu şekilde kullanılmalı.
/* USER CODE BEGIN 2 */ HAL_TIM_Base_Start(&htim6); //Timer' i baslat /* USER CODE END 2 */
Timer’ ı başlattıktan sonra timer’ ı kullanarak oluşturduğumuz gecikme fonksiyonumuzu tanımlayalım. Bu fonksiyonun temel mantığı timer’ ın kullandığı sayaç değerini fonksiyon çağırıldığı anda kaydedip, sayaç değeri kaydedilen değerden ne kadar arttığını kontrol etmek. Örnek verecek olursam 100 uS beklemek istiyoruz. O anda timer değeri 2153 olsun. 100 us geçmesi için timer değeri 2253 olana kadar beklememiz gerekir. Bu fonksiyon da tam olarak bunu bir while içerisinde yapıyor.
//give microsecond delay void mDelay(uint32_t mDelay) { uint16_t initTime = (uint16_t)__HAL_TIM_GET_COUNTER(&htim6); //get initial time while(((uint16_t)__HAL_TIM_GET_COUNTER(&htim6)-initTime) < mDelay){ //wait until (time now - inital time == 0) } }
Artık bu fonksiyon her çağırıldığında parametre olarak verilen gecikme süresi kadar yazılımın beklemesini sağlıyacak. Ancak kullandığımız timer 16 bitlik olduğu için maksimum 65 mS bekleyebileceğini daha önce de söylemiştik.
Gecikme fonksiyonumuzu da tanımladığımıza göre sıra geldi DHT11′ den veri okumaya. Daha önce de belirttiğimiz gibi DHT’ den gelen veriyi okumak öncelikle veri hattını belirli bir süre Low konumunda tutmalıyız. Daha sonra gelen veriyi okumaya başlamalıyız. Yani haberleşme için kullanacağımız pinin yönlendirmesini (direction) değiştirmemiz lazım. Bunu basit olarak şu şekilde bir fonksiyon ile oluşturabiliriz. Burada pin frekansının “GPIO_SPEED_FREQ_HIGH” gibi bir seçenek seçilmiş olduğundan emin olmalısınız. Aksi taktirde pin değişimlerinde meydana gelen gecikmeler zamanlama hatasına sebep olabilir.
#define OUTPUT 1 #define INPUT 0 //set DHT pin direction with given parameter void set_gpio_mode(uint8_t pMode) { GPIO_InitTypeDef GPIO_InitStruct = {0}; //if direction parameter OUTPUT if(pMode == OUTPUT) { /*Configure GPIO pins : LD3_Pin LD2_Pin */ GPIO_InitStruct.Pin = DHT11_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); }else if(pMode == INPUT) //else if direction parameter INPUT { GPIO_InitStruct.Pin = DHT11_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } }
Pin yönlendirmesini de halletiğimize göre veir okumaya hazırız. Veri okumak için yine bir fonksiyon tanımlayıp işlemlerimizi bu fonksiyon içerisinde yapıyoruz. Bu fonksiyon içerisinde veri pinini 20 mS Low konuma çekip hattı serbest konumda yani giriş yönlendirmeli olarak bırakmalıyız. Bunu şu şekilde sağlıyabiliriz.
//start comm set_gpio_mode(OUTPUT); //set pin direction as input HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); HAL_Delay(20); //wait 20 ms in Low state set_gpio_mode(INPUT); //set pin direction as input
Pini Low konuma çektikten sonra DHT’ den eşleşme verisini kontrol etmeliyiz. Bu eşleşme verisi 80 ms Low 80 ms High konumda gelen bir veri. Eğer bu veri gelmişse DHT veri göndermeye başlamış ve geri kalan veriyi okumaya geçebiliriz demektir. Ancak çok fazla beklememize rağmen gelmemişse fonksiyondan çıkabiliriz demektir. Bunu da basit olarak şu şekilde sağlıyabiliriz.
//check dht answer __HAL_TIM_SET_COUNTER(&htim6, 0); //set timer counter to zero while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) if((uint16_t)__HAL_TIM_GET_COUNTER(&htim6) > 500) return 0; __HAL_TIM_SET_COUNTER(&htim6, 0); while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) if((uint16_t)__HAL_TIM_GET_COUNTER(&htim6) > 500) return 0; mTime1 = (uint16_t)__HAL_TIM_GET_COUNTER(&htim6); __HAL_TIM_SET_COUNTER(&htim6, 0); while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) if((uint16_t)__HAL_TIM_GET_COUNTER(&htim6) > 500) return 0; mTime2 = (uint16_t)__HAL_TIM_GET_COUNTER(&htim6); //if answer is wrong return if(mTime1 < 75 && mTime1 > 85 && mTime2 < 75 && mTime2 > 85) { HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin); return 0; }
Eğer DHT’ den veri doğru olarak gelmişse sıra geldi sıcaklık ve nem bilgisini okumaya. Datasheet’ inde de yazdığı üzere 40 bit lik veri okuyacağız. yani 40 adet LOW ve HIGH durumu arka arkaya okumamız gerekli. HIGH olma süresine göre o verinin 0 mı 1 mi olduğuna karar vereceğiz. Bunu bir döngü içerisinde yaparak DHT’ den gelen bütün veriyi okuyalım. Burada veriyi olabildiğince hızlı yani herhangi bir uzun sürecek işlem olmaksızın yapmalıyız. Çünkü DHT bizim sıradaki veriyi almaya hazır olup olmadığımıza dikkat etmeden hızlıca (uS’ ler içerisinde) gönderecek.
for(int j = 0; j < 40; j++) { __HAL_TIM_SET_COUNTER(&htim6, 0); while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) if((uint16_t)__HAL_TIM_GET_COUNTER(&htim6) > 500) return 0; __HAL_TIM_SET_COUNTER(&htim6, 0); while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) if((uint16_t)__HAL_TIM_GET_COUNTER(&htim6) > 500) return 0; mTime1 = (uint16_t)__HAL_TIM_GET_COUNTER(&htim6); //check pass time in high state //if pass time 25uS set as LOW if(mTime1 > 20 && mTime1 < 30) { mBit = 0; } else if(mTime1 > 60 && mTime1 < 80) //if pass time 70 uS set as HIGH { mBit = 1; } //set i th data in data buffer mData[j] = mBit; }
40 bit veriyi de okuduğumuza göre içerisinden sıcaklık ve nem bilgisini çekebiliriz. Bu 40 bit verideki ilk 8 bit nem, 16-24. arasındaki veri sıcaklık son 8 bit ise eşlik verisi. Bu eşlik verisi sıcaklık ve nem değerinin toplamı olarak gönderilir. Biz okuduğumuz değerleri toplayıp bununla kıyaslayarak gelen veri doğru mu diye kontrol edebiliriz. Bu verileri tuttuğumuz dizinin içerisinden bir döngüde indisler yardımıyla aşağıdaki gibi çekebiliriz.
//get hum value from data buffer for(int i = 0; i < 8; i++) { humVal += mData[i]; humVal = humVal << 1; } //get temp value from data buffer for(int i = 16; i < 24; i++) { tempVal += mData[i]; tempVal = tempVal << 1; } //get parity value from data buffer for(int i = 32; i < 40; i++) { parityVal += mData[i]; parityVal = parityVal << 1; }
Son olarak da yapmamız gereken 8 kere sola kaydırdığımız veriyi bir kez sağa kaydırmak. Bu benim yazım sebebimden kaynaklı bir durum. İsterseniz veriyi daha farklı bir şekilde bir değişkende tutup veriyi ondan maskeleyebilirisiniz. Benim kullandığım yöntemde diziden değer okuyup bit kaydırma yapıyorum. Son bit olan 8. bit’ e sıra geldiğinde kaydırma yapmamız gerekiyor ama döngü içerisinde olduğu için yapıyor. Bunu geri almak bir kez sağa kaydırıyoruz.
parityVal = parityVal >> 1; humVal = humVal >> 1; tempVal = tempVal >> 1;
Elde ettiğimiz verileri UART üzerinden bilgisayara gönderelim. Bunun için yine HAL kütüphanelerinin bir fonksiyonu olan “HAL_UART_Transmit” fonksiyonunu kullanacağız. Bu fonksiyon bloklayıcı yapıdadır. Yani gönderme işlemi bitene kadar alt satırlardaki kodlar çalışmazlar. Kullanımı basit olması ve ak ayar gerektirmemesi sebebi ile bu uygulamada bu kullanımı tercih ettim.
//prepare character buffer to send with UART uint16_t len = sprintf(sendData, "Hum: %d Temp: %d\n\r", dhtVal[1], dhtVal[0]); //send data with UART HAL_UART_Transmit(&huart3, (uint8_t*)sendData, len, 100);
Yazılımda genel olarak anlatacaklarım bu kadardı. Artık bir seri port terminali ile UART üzerinden gönderdiğimiz data’ yı PC’ de görüntüleyebiliriz. Yazılımın tamamına bağlantıdan ulaşabilirsiniz.
bağlantı yaparken usb tll dönüştürücüye ihtiyacnımız yok mu?
Merhaba, ne ile ne arasındaki bağlantı için usb-ttl gerekli diye düşündünüz? DHT11 sensör için ise gerekli değil. Ancak uart iletişim için ise kullandığınız STM32 modeline değişiklik gösterir. İyi çalışmalar dilerim…
Emeğinize sağlık 2 gündür uğraşıyordum hatalarımı görmeme ve örnek olduğnuz için çok teşekkür eder. Yazarı tanırım. Bi ara İEEE kulüp odasında oturmuştuk stm sohbeti. Eywallah Oğuzhan abi.
İçerik için teşekkürler bir hata fark ettim. okuma kısmında pinleri giriş ve çıkış olarak ayarlarken HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); gibi bir komut kullanıyoruz, pinleri initilasition etmek için. burda farklı bir porta bağlantı yaparsanız bu kısımdaki gpiob kısmını dht yi bağladığınız porta göre değiştiriniz.
Merhaba, sorunun nerede olduğunu tam anlamadım. Daha açık bir şekilde mail atabilir misiniz?