STM32 HAL ve CubeMX ile ADC Uygulaması -2-

Önceki başlıkta örnek bir ADC projesi oluşturmuş ve main.c dosyası dışında bütün dosyaları incelemiştik. Şimdi ise sıra main.c yani ana program dosyasını incelemeye geldi. Projeyi oluştururken mümkün mertebe ADC’nin oldukça basit ve yalın bir kod ile çalıştırılmasına dikkat ettim. Bazı ayrıntıya sebep olacak özellikleri kullanmaktan kaçındım. Örneğin saat ayarlarını eğer asenkron saat olarak belirleseydik sistem saat ayarlama fonksiyonunda yeni komutlar belirecekti. Bu uygulamanın ADC hakkında en yalın uygulamalardan biri olduğunu öncelikle bilmeniz gerekir.

main.c dosyasını açtığımızda karşımıza yapı tipi tanımlaması ve fonksiyon prototipleri çıkmakta.

Burada ADC_HandleTypeDef adı altına tanımlanan hadc1 yapısını ilk defa görmekteyiz. Bu yüzden bu tip tanımlamasını incelememiz gerekir. SystemClock_Config() fonksiyonunu bildiğimiz için bunu geçeceğiz. Bu fonksiyondaki kodların aynı kaldığını söyleyelim. MX_GPIO_Init() fonksiyonu ise CubeMX programında tanımladığımız led ve düğmeye bağlı ayakların giriş ve çıkış ayarını yapmaktadır. Yani bu fonksiyon ADC ile alakalı olmayıp genel maksatlı giriş ve çıkış birimi ile alakalıdır. Biz ADC ayağını önceki MSP dosyasında tanımladığımız için burada ADC ile alakalı bir kod yer almamaktadır. Kendi yazdığımız programdan başka inceleyeceğimiz asıl fonksiyon ise MX_ADC1_Init() fonksiyonu olacaktır. MX ile başlayan fonksiyonları CubeMX programı üretmiştir.

Ana programda HAL ve sistem saati başlatma fonksiyonlarının hemen ardından MX_GPIO_Init() ve MX_ADC1_Init() fonksiyonları başlatılmaktadır. GPIO fonksiyonunu bildiğimiz için hemen MX_ADC1_Init() fonksiyonunu incelemeye başlayalım.

Görüldüğü gibi bir ADC için oldukça kalabalık bir kod bloku yer almaktadır. Burada önceden tanımladığımız ADC_HandleTypeDef tipinde global yapı tipi değişkeni olan hadc1 yapısını unutmamamız gereklidir. Öncelikle bu yapı tipinin içine bakalım ve sonrasında programa dönelim.

Bu yapı tipinin ADC_TypeDef, ADC_InitTypeDef, DMA_HandleTypeDef, HAL_LockTypeDef adında tip değişkenleri ile beraber iki adet 32 bit tam sayı değişkenini bulundurduğunu görüyoruz. Yapı içerisinde yine karmaşık yapıları almakta ve içinden çıkılmaz gibi görülmektedir. Burada tamamını incelemeye kalksak işin içinden gerçekten çıkılmaz fakat biz program üzerinden ilerleyerek ilk olarak kısmen inceleyeceğiz.

Burada işaretçi olarak belirtilen *Instance değerinin ADC yazmaç adlarına çıktığını görmekteyiz. ADC_InitTypeDef ise ADC için gerekli parametreleri içeren yapı değişkeni olarak karşımıza çıkıyor. DMA doğrudan hafıza erişimi, Lock 1 ve 0 değerlerini içeren enum (numaralandırma) tipi ve State ile ErrorCode de durum değerlerini depolayan tam sayılardır.

Şimdi MX_ADC1_Init() fonksiyonuna geri dönelim ve tanımlanan iki farklı yapı tipi değişkenine bakalım.

Bunlar hadc1’in yanında bizim konfigürasyon yapacağımız diğer yapı değişkenleridir. Görüldüğü gibi HAL kütüphanesi bizi oldukça uğraştırmakta. Şimdi bu yapı tiplerinin yapısına bakarak hakkında fikir edinelim. Öncelikle MultiModeTypeDef yapısına her zaman olduğu gibi CTRL + Sol Tık yaparak bakıyoruz. Bu çoklu mod yapısı stm32f3xx_hal_adc_ex.h dosyasında yer almaktadır.

ADC_MultiModeTypeDef yapısı görüldüğü gibi çok karmaşık bir yapı değildir. Üç adet 32 bit tam sayı değerine belli sabitleri atamak gereklidir. Bunların açıklaması ise yorum kısmında belirtilmiştir. Buna göre Mode değişkeninin görevi ADC’nin bağımsız veya çoklu modda çalışıp çalışmayacağını belirlemektir. DMAAccessMode ise çoklu ADC modunda DMA erişim modunu ayarlamaya yarar. TwoSamplingDelay ise 2 adet örnekleme süreci arasındaki bekleme değerini içerir. Alacağı bütün değerleri şimdi kütüphane kılavuzunda görebilsek de biz kod üzerinde inceleme yapacağımız için bunu sonraya bırakıyoruz. Şimdi diğer yapı tipi olan ADC_ChannelConfTypeDef yapısını inceleyelim ve aldığı değerleri görelim.

Burada kanal ayarları için gereken değerlerin olduğunu yapı tipinin adından anlamamız da mümkündür. Yapının üye değişkenlerine baktığımızda kanal, mevki, örnekleme zamanı, tekli veya diferansiyel, offset sayısı ve offset değerini aldığını görüyoruz. Bu değişkenlerin aldığı parametreleri birazdan kod üzerinde inceleyeceğiz. Fakat bu yapılara önceden bakmak ve belli bir fikir edinmek her zaman daha faydalıdır. Yapı tiplerini incelemeyi bitirdiğimize göre artık kodları satır satır inceleyerek ne yaptıklarını öğrenebiliriz. Şimdi ilk kodu inceleyerek başlayalım.

Bu kodda hadc1 adında bir yapı tipi tanımlamıştık. ADC_HandleTypeDef olan bu yapı tipinde *Instance adı verilen ADC_TypeDef şeklinde bir değişken vardı. ADC_TypeDef ise ADC yazmaçlarını barındıran bir yapı değişkenidir.

Fakat burada ADC1 adını görememekteyiz. Demek ki programın başka yerlerinde ADC1 tanımlaması buradaki yazmaçlardan biriyle örtüşüyor. Bunu bulmak için ADC1’in kaynağına bakıyoruz ve şöyle bir tanımlamayı görüyoruz.

Bu temel adresin ADC1_BASE adresi üzerine olduğunu görüyoruz. Fakat bu adresin ne olduğu konusunda yine stm32f303xc.h dosyasında şöyle bir tanım görmekteyiz.

Sıfır değerinin AHB3PERIPH_BASE değişkeni ile toplandığını görüyoruz. Bunlar hafıza haritaları olduğu için çoktan alt seviyeye inmiş durumdayız. Bu yüzden işi sonuna kadar takip edelim.

Görüldüğü gibi AHB3PERIPH_BASE değerinin de PERIPH_BASE değeri ile bir değerin toplamı olduğunu görüyoruz. En son elimizde PERIPH_BASE kalıyor ve o da yine on altılık bir değer olarak karşımıza çıkıyor.

Bunlar STM32 mikrodenetleyicinin bellek haritalandırmasına ait adres değerleri ile ilgili olduğu için bizim değerlerle işimiz yok. Fakat değişkene değer aktarırken ADC1 derken aynı AVR’deki PORTB, PORTC gibi düşünmek gereklidir. Yani bu birimlerin buradaki adları ilgili yazmaçların adresleri olarak karşımıza çıkar. GPIOx de bu şekildedir. Bunların içindeki yazmaçlara erişmek için neden ok (->) operatörünü kullandığımızı da bu şekilde anlayabiliriz. Her bir birim kendisiyle alakalı yazmaçların olduğu yapıyı içerdiğinden önce birimin adını sonra ok operatörünü ve sonrasında yazmaç adını yazarız. AVR’de olduğu gibi tüm yazmaçlar açık değildir.

Bu birimlerin adlarını nereden öğrenip de program yazacağız derseniz kullandığınız mikrodenetleyicinin başlık dosyasına bakabilirsiniz. Burada bütün çevre birimlerine ait tanımlamalar yer almaktadır. Benim kullandığım mikrodenetleyici için yazılmış stm32f303xc.h dosyasında GPIO ve ADC tanımlamaları şu şekildedir.

Bu kadar ayrıntıya girmekle yeni bir şey daha öğrenme fırsatı yakaladık. Instance değişkeninin ADC_TypeDef olması aslında birim adı olduğunu göstermekte. O yüzden burada ADC1 yazıyoruz. Bizim kullanacağımız A1 ayağı ise ADC1 birimine denk geliyor. Şimdi bir sonraki satıra geçelim.

Burada hadc1 yapısının değerlerini değil hadc1 yapısının içindeki Init yapısının üye değerleri üzerinden işlem yapıldığını görmekteyiz. O yüzden Init yapısını öncelikle inceleyip sonra atanan değerlerin ne anlama geldiğine bakalım. Init yapısı ADC_HandleTypeDef tipindeki hadc1 yapısının içinde yer alan ADC_InitTypeDef tipindeki bir yapı değişkenidir. O yüzden ADC_InitTypeDef yapısını incelememiz gerekir.

Kalabalık yapmaması için uzun İngilizce açıklamaları silmek zorunda kaldık. Burada ADC tanımlamak için gereken ön ayarları görmekteyiz. Bu değerler oldukça fazla olup pek çok parametre almaktadırlar. Buradan ClockPreScaler değerinin saat sinyalini bölme, Resolution değerinin ise çözünürlük olduğunu hemen anlayabiliriz. Fakat aldığı parametreleri öğrenmek için yine kütüphane kaynak koduna veya kütüphane kılavuzuna bakmak gerekecektir. Burada pek çok ayar olduğu için hepsini tek tek incelemeyip programda incelemeye devam edeceğiz.

Programımızda hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1; komutuyla ADC ayarlarını ayarlamaya başladık. Burada ClockPrescaler’in alabileceği frekans bölme değerlerine bakalım.

#if defined ile başlayan karar yapısının uyumluluk için yazıldığını hemen görebilirsiniz. Bu durumda aygıta yönelik olan fonksiyonları içerdiği için sürücü dosyamız stm32f3xx_hal_adc_ex.h dosyası oluyor. Ex ile biten dosyalar aygıta ve mikrodenetleyici ailesine özel komutları içermektedir. Burada kullandığımız aygıta göre 1, 2 veya 4 olarak bölme seçeneğini seçebileceğiniz görünüyor. Burada da ADC_CLOCK_SYNC_PLCK_DIV1 parametresini yazarak saat hızını 1’e bölüyoruz.

Buradan senkronize saat seçtiğimiz için saat hızımız sistem saatine eşitlenmiş oluyor. Yani ADC 48 MHz’de çalışacak.

Resolution kelimesinin çözünürlük anlamına geldiğini hemen fark edebiliriz. Burada çözünürlüğün hangi parametreleri aldığını ise datasheetten okuduğumuzdan 6, 8, 10, 12-bit şeklinde parametre alması gerektiğini düşünüyoruz. Bu parametre listesine bakarak kullanabileceğimiz parametreleri bir görelim.

Tahmin ettiğimiz gibi HAL kütüphanesi datasheette yazan özellikleri burada da  kullanmamızı sağlıyor. Gerçek bir kütüphaneden beklenen de budur. Yoksa bir analogRead() fonksiyonu ile işi bitirebilirlerdi. 🙂
Programımızda da 12 bit çözünürlüğü seçiyoruz. Çözünürlüğün düşük olmasının bir avantajı daha hızlı okuma yapılabilmesidir. 10-bit çözünürlük uygulamalar biraz hassaslaşınca yetersiz kalmaktaydı. STM32’nin 12-bit çözünürlüğü oldukça işimize yarayacaktır. STM32F3 serisinin bazı modellerinde 16-bit sigma-delta ADC birimi olduğunu da söyleyelim. Bu ADC birimini harici bir modül olarak taktığımızda iletişim hızıyla beraber çevirim hızı da oldukça düşecek daha çok daha yavaş örnek almaya başlayacaktır. Örneğin I2C ile 16-bit bir ADC modülü kullandığımızda 1Mbit hızda saniyede alacağımız örnek sayısı bellidir. Bir defa yapılan ölçümlerde bunun önemi olmasa da osiloskopta yaptığımız gibi sinyal okuma ve yorumlama esnasında yüksek hızlı ADC birimlerine ihtiyaç duyarız. Benim de STM32 mikrodenetleyicileri tercih etmemdeki en önemli sebeplerden biri F3 serisinin gelişmiş analog çevre birimleri ve 5MSPS (Mega Sample Per Second) yani saniyede 5 milyon ölçüme varan ADC birimiydi.

Bu komutta ise ADC’nin tarama modunda çalışması devre dışı bırakılmaktadır. Tarama modunun etkinleştirilmesi ise ADC_SCAN_ENABLE parametresiyle olmaktadır.

Bu komutta ADC’nin art arda ölçüm yapması devre dışı bırakılmıştır. Yani basit tek bir ölçüm yapacağız. Bu devamlı ölçüm yapma modu pek çok örneği peş peşe almak istediğimiz zaman kullanılmalıdır. Bu da sinyal okuma ve yorumlama esnasında bize lazım olacaktır. Herhangi bir düz sinyali okumak için onlarca örnek almaya gerek yoktur.

Burada da yine farklı çevirim modlarından birini devre dışı bıraktık. Bu modlar süreyle alakalı olup bizim ise süre ile işimiz şimdilik yoktur.

Burada ADC’nin harici tetiklemesinin kenar ayarı yapılmaktadır. Sinyal yükselen, alçalan veya hem yükselen hem de alçalan kenar olarak seçilebilir. Biz herhangi bir dış sinyale göre ölçüm yapılmayacağı için NONE olarak seçtik. Aşağıda aldığı parametreler mevcuttur.

Şimdi ise harici tetikleme hakkında bir sonraki komutu inceleyelim.

Biz burada ADC_SOFTWARE_START diyerek ADC’yi yazılımın başlatacağını haber veriyoruz. Yani harici tetikleme burada devre dışı bırakılmış oluyor. Biz de düz bir şekilde kullanmak istiyoruz.

Bu komut ise ADC’nin verisini sola mı yoksa sağa mı hizalı olacağını belirler. AVR mikrodenetleyicilerde de buna benzer bir özelliği görmüştük. Klasik olarak biz her zaman sağa hizalı veriyi kullandığımızdan ADC verisini sağa hizalıyoruz.  ADC_DATAALIGN_LEFT deseydik sola hizalayacaktı.

Bu komut derecelendirilecek çevirilerin kaç adet olacağını belirler. Daha derecelendirme işlemine gelmediğimiz için tek bir ölçüm yapacağımızdan bunu bir olarak belirliyoruz. Bu değer 1 ve 16 arasında olabilir.

Burada DMA yani doğrudan hafıza erişim biriminin devamlı talep göndermesi devre dışı bırakılmıştır. DMA kullanmasak da bunu devre dışı bırakıyoruz.

Bu çevirim bitimini tipini belirlemek için kullanılır. Biz tek bir çevirimde çevrimin bittiğini haber almak istediğimizden ADC_EOC_SINGLE_CONV değerini kullanıyoruz.

Bu değer ise güç tasarrufu ile ilgili bir parametreyi almaktadır. Eğer etkinleştirirsek çevirim yalnızca gerekli olduğu zaman yapılır. Şimdilik bununla uğraşmayacağımız için bunu da devre dışı bırakıyoruz.

Eğer ADC verisi üst üste okunursa üstüne yazılıp yazılmamasını kararlaştırır. Eğer hassas bir veriyi çevirip de okumayı ihmal etme ihtimalimiz olursa okunmadan yazılmasına engel olabiliriz. Bunun için de ADC_OVR_DATA_PRESERVED parametresini kullanmalıyız. Bu komutta okuduğumuz her veri öncekinin üzerine yazılacaktır.

Buraya kadar ADC’yi tanımlamak için değerleri girdik ve HAL_ADC_Init(&hadc1) fonksiyonu ile değerleri gönderdik. Artık ADC birimi başlatılmış ve ayarları da yapılmış durumdadır. Fakat burada kanal ayarlarını ve mod ayarlarını yapmadık. Bunları ise kodun devamında yapacağız. Ondan sonra ise yazdığımız koda sıra gelecek.

Burada tanımladığımız multimode yapısına sadece Mode değeri olacak ADC_MODE_INPEDENDENT yani bağımsız ADC modunu yüklüyoruz ve HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) fonksiyonu ile bu değeri yükleyerek ayar yapıyoruz. Şimdi ise incelememiz gereken bir tek kanal ayarı kalmakta.

Bu komut ADC kanallarını seçmeye yaramaktadır. ADC’nin kaç kanal olacağı ve hangi kanallar olacağı Channel değişkenine atadığımız değerlerle belirlenir. Bu değerlerin ne olabileceğini kaynak dosyasından görelim.

Görüldüğü gibi ayaklara bağlı olan ADC kanallarının yanında mikrodenetleyicinin içinde yer alan ADC kanalları da bulunmaktadır. Bunlar mikrodenetleyicinin içindeki sıcaklık algılayıcısına ya da opamplara bağlıdır. Bunların farklı adda olması programlamada kolaylık sağlayacaktır.

Burada Rank adı verilen sıralama yapılmaktadır. Biz 1. sırayı seçerek bu sıralama işlemini kullanmayacağız. Sıralama işlemi şu an basit bir ölçüm için gerekli değildir.

Bu komutla seçili kanalın tekli mi yoksa diferansiyel yani iki değer arasındaki farkı ölçer şekilde mi olduğunu belirliyoruz. Biz tek bir ayaktan tek bir değeri ölçeceğimiz için ADC_SINGLE_ENDED parametresini kullanıyoruz.

Burada ADC örnekleme hızını belirlemekteyiz. Bu örnekleme hızı ADC saat sinyalinin çevirimlerine göre belirlenmektedir. Yani ADC’nin hızı öncelikle ADC saatine giden sinyalin frekansı sonra ise örnekleme zamanına bağlıdır. Örnekleme yavaş olduğunda daha isabetli sonuç elde edilir. Örnekleme zamanının parametrelerini aşağıda görebiliriz.

Bu örnekleme zamanı daha ileri bir seviye olduğu için referans kılavuzundan bakıp ayrıntılı olarak incelemek gerekli. Biz ise şu an ilk programımı yapıyoruz.

Burada ise offset değerlerini sıfırladık çünkü okuduğumuz ham veri olacak. Bu offset değerleri belli bir değer kadar eksiltmek için kullanılabilir. Buraya kadar kanal ayarlarını da görmüş olduk ve artık HAL_ADC_ConfigChannel(&hadc1, &sConfig) fonksiyonuyla hadc1 yapımıza değeri atıyoruz. Artık bütün ayarlamalar yapıldığına göre main fonksiyonu içerisine kendi programımızı yazmaya başlayabiliriz. Benim yazdığım program A1 ayağından bir potansiyometreye bağlı ve belli bir değerden sonra kart üzerindeki ledi yakıyor. Bu değeri 12-bit ham veri yani 0-4096 arasında orta bir değer olan 2000 olarak belirledim ve basit bir program yazdım. Ana program şu şekildedir.

Burada öğreneceğimiz dört yeni fonksiyon bizleri karşılıyor. Bu fonksiyonlar sırayla ADC biriminde ölçümü başlatır ve ölçüm bitene kadar bekletir ardından da değeri okuduktan sonra ADC’yi kapatır. Tek bir ölçüm için dört ayrı HAL fonksiyonu kullanmamız gereklidir. Şimdi bu fonksiyonları tek tek inceleyelim.

HAL_ADC_Start(&hadc1);  Bizim tanımladığımız hadc1 yapısında yer alan ayarlara göre ADC birimini başlatıp ölçüm yapacaktır. hadc1 yapısında ise ADC1 birimini belirttik ve kanal ayarlarında ise 2 numaralı kanalı seçtik. Belirlediğimiz ayaktan alınan analog değer şimdi okunmaya başlayacaktır.

HAL_ADC_PollForConversion(&hadc1 , 5000); Bu belirlenen ADC için belli bir bekleme süresi ile ölçümün tamamlanması için programı bekletir. Zaman aşımı bizim belirlediğimiz bir değer olabilir.

uint32_t deger = HAL_ADC_GetValue(&hadc1);  Bu fonksiyon belirlenen ADC birimden okunan değeri geri döndürür. 12 bitlik tam sayı değerini deger değişkenine atamış olduk.

HAL_ADC_Stop(&hadc1);  Artık ADC birimini ve ölçümü sonlandırıyoruz ve değerimiz üzerinde işlem yapmaya başlıyoruz.

Eğer deger değişkeninin değeri 2000’den büyükse E portundaki 10 numaralı ayak yanacaktır ve değilse sönecektir. Programın nasıl çalıştığını videodan izleyebilirsiniz. Buraya kadar sadece ADC’de ilk programı yazmış olduk ve programın nasıl çalıştığını öğrendik. Görüldüğü gibi oldukça karmaşık bir platformla karşı karşıyayız fakat sizin için olabildiği kadar basit bir şekilde anlatmaya çalıştık.

Bizi Facebook grubumuzda takip etmeyi unutmayın. Bilgili ve öğrenmeye hevesli bir topluluk oluşturmak istiyoruz.

https://www.facebook.com/groups/1233336523490761/

UYARI!!

 

 

 

 

Gökhan Dökmetaş

"Arduino Eğitim Kitabı" ve "Arduino ve Raspberry PI ile Nesnelerin İnterneti" kitaplarının yazarı. Başkent Teknoloji ve Dedektör Merkezi'nde Ar-ge Sorumlusu. Araştırmacı-Yazar.

You may also like...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.