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.
1 2 3 4 5 |
ADC_HandleTypeDef hadc1; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_ADC1_Init(void); |
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.
1 2 3 4 5 6 7 8 |
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); |
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.
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 |
static void MX_ADC1_Init(void) { ADC_MultiModeTypeDef multimode = {0}; ADC_ChannelConfTypeDef sConfig = {0}; /* USER CODE END ADC1_Init 1 */ /**Common config */ hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = DISABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; hadc1.Init.LowPowerAutoWait = DISABLE; hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } /**Configure the ADC multi-mode */ multimode.Mode = ADC_MODE_INDEPENDENT; if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK) { Error_Handler(); } /**Configure Regular Channel */ sConfig.Channel = ADC_CHANNEL_2; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SingleDiff = ADC_SINGLE_ENDED; sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; sConfig.OffsetNumber = ADC_OFFSET_NONE; sConfig.Offset = 0; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } } |
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.
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 |
typedef struct __ADC_HandleTypeDef { ADC_TypeDef *Instance; /*!< Register base address */ ADC_InitTypeDef Init; /*!< ADC required parameters */ DMA_HandleTypeDef *DMA_Handle; /*!< Pointer DMA Handler */ HAL_LockTypeDef Lock; /*!< ADC locking object */ __IO uint32_t State; /*!< ADC communication state (bitmap of ADC states) */ __IO uint32_t ErrorCode; /*!< ADC Error code */ #if defined(STM32F302xE) || defined(STM32F303xE) || defined(STM32F398xx) || \ defined(STM32F302xC) || defined(STM32F303xC) || defined(STM32F358xx) || \ defined(STM32F303x8) || defined(STM32F334x8) || defined(STM32F328xx) || \ defined(STM32F301x8) || defined(STM32F302x8) || defined(STM32F318xx) ADC_InjectionConfigTypeDef InjectionConfig ; /*!< ADC injected channel configuration build-up structure */ #endif /* STM32F302xE || STM32F303xE || STM32F398xx || */ /* STM32F302xC || STM32F303xC || STM32F358xx || */ /* STM32F303x8 || STM32F334x8 || STM32F328xx || */ /* STM32F301x8 || STM32F302x8 || STM32F318xx */ }ADC_HandleTypeDef; |
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.
1 2 |
ADC_MultiModeTypeDef multimode = {0}; ADC_ChannelConfTypeDef sConfig = {0}; |
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.
1 2 3 4 5 6 |
typedef struct { uint32_t Mode; uint32_t DMAAccessMode; uint32_t TwoSamplingDelay; }ADC_MultiModeTypeDef; |
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.
1 2 3 4 5 6 7 8 9 |
typedef struct { uint32_t Channel; uint32_t Rank; uint32_t SamplingTime; uint32_t SingleDiff; uint32_t OffsetNumber; uint32_t Offset; }ADC_ChannelConfTypeDef; |
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.
1 |
hadc1.Instance = ADC1; |
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.
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 |
typedef struct { __IO uint32_t ISR; /*!< ADC Interrupt and Status Register, Address offset: 0x00 */ __IO uint32_t IER; /*!< ADC Interrupt Enable Register, Address offset: 0x04 */ __IO uint32_t CR; /*!< ADC control register, Address offset: 0x08 */ __IO uint32_t CFGR; /*!< ADC Configuration register, Address offset: 0x0C */ uint32_t RESERVED0; /*!< Reserved, 0x010 */ __IO uint32_t SMPR1; /*!< ADC sample time register 1, Address offset: 0x14 */ __IO uint32_t SMPR2; /*!< ADC sample time register 2, Address offset: 0x18 */ uint32_t RESERVED1; /*!< Reserved, 0x01C */ __IO uint32_t TR1; /*!< ADC watchdog threshold register 1, Address offset: 0x20 */ __IO uint32_t TR2; /*!< ADC watchdog threshold register 2, Address offset: 0x24 */ __IO uint32_t TR3; /*!< ADC watchdog threshold register 3, Address offset: 0x28 */ uint32_t RESERVED2; /*!< Reserved, 0x02C */ __IO uint32_t SQR1; /*!< ADC regular sequence register 1, Address offset: 0x30 */ __IO uint32_t SQR2; /*!< ADC regular sequence register 2, Address offset: 0x34 */ __IO uint32_t SQR3; /*!< ADC regular sequence register 3, Address offset: 0x38 */ __IO uint32_t SQR4; /*!< ADC regular sequence register 4, Address offset: 0x3C */ __IO uint32_t DR; /*!< ADC regular data register, Address offset: 0x40 */ uint32_t RESERVED3; /*!< Reserved, 0x044 */ uint32_t RESERVED4; /*!< Reserved, 0x048 */ __IO uint32_t JSQR; /*!< ADC injected sequence register, Address offset: 0x4C */ uint32_t RESERVED5[4]; /*!< Reserved, 0x050 - 0x05C */ __IO uint32_t OFR1; /*!< ADC offset register 1, Address offset: 0x60 */ __IO uint32_t OFR2; /*!< ADC offset register 2, Address offset: 0x64 */ __IO uint32_t OFR3; /*!< ADC offset register 3, Address offset: 0x68 */ __IO uint32_t OFR4; /*!< ADC offset register 4, Address offset: 0x6C */ uint32_t RESERVED6[4]; /*!< Reserved, 0x070 - 0x07C */ __IO uint32_t JDR1; /*!< ADC injected data register 1, Address offset: 0x80 */ __IO uint32_t JDR2; /*!< ADC injected data register 2, Address offset: 0x84 */ __IO uint32_t JDR3; /*!< ADC injected data register 3, Address offset: 0x88 */ __IO uint32_t JDR4; /*!< ADC injected data register 4, Address offset: 0x8C */ uint32_t RESERVED7[4]; /*!< Reserved, 0x090 - 0x09C */ __IO uint32_t AWD2CR; /*!< ADC Analog Watchdog 2 Configuration Register, Address offset: 0xA0 */ __IO uint32_t AWD3CR; /*!< ADC Analog Watchdog 3 Configuration Register, Address offset: 0xA4 */ uint32_t RESERVED8; /*!< Reserved, 0x0A8 */ uint32_t RESERVED9; /*!< Reserved, 0x0AC */ __IO uint32_t DIFSEL; /*!< ADC Differential Mode Selection Register, Address offset: 0xB0 */ __IO uint32_t CALFACT; /*!< ADC Calibration Factors, Address offset: 0xB4 */ } ADC_TypeDef; |
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.
1 |
#define ADC1 ((ADC_TypeDef *) ADC1_BASE) |
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.
1 |
#define ADC1_BASE (AHB3PERIPH_BASE + 0x00000000U) |
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.
1 |
#define AHB3PERIPH_BASE (PERIPH_BASE + 0x10000000U) |
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.
1 |
#define PERIPH_BASE ((uint32_t)0x40000000U) |
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.
1 2 3 4 5 6 7 8 9 10 |
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) #define ADC1 ((ADC_TypeDef *) ADC1_BASE) #define ADC2 ((ADC_TypeDef *) ADC2_BASE) #define ADC3 ((ADC_TypeDef *) ADC3_BASE) #define ADC4 ((ADC_TypeDef *) ADC4_BASE) |
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.
1 |
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1; |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
typedef struct { uint32_t ClockPrescaler; uint32_t Resolution; uint32_t DataAlign; uint32_t ScanConvMode; uint32_t EOCSelection; uint32_t LowPowerAutoWait; uint32_t ContinuousConvMode; uint32_t NbrOfConversion; uint32_t DiscontinuousConvMode; uint32_t NbrOfDiscConversion; uint32_t ExternalTrigConvEdge; uint32_t DMAContinuousRequests; uint32_t Overrun; }ADC_InitTypeDef; |
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.
1 2 3 4 5 6 |
#if defined(STM32F302xE) || defined(STM32F303xE) || defined(STM32F398xx) || \ defined(STM32F302xC) || defined(STM32F303xC) || defined(STM32F358xx) || \ defined(STM32F303x8) || defined(STM32F334x8) || defined(STM32F328xx) #define ADC_CLOCK_SYNC_PCLK_DIV1 ((uint32_t)ADC12_CCR_CKMODE_0) /*!< ADC synchronous clock derived from AHB clock without prescaler */ #define ADC_CLOCK_SYNC_PCLK_DIV2 ((uint32_t)ADC12_CCR_CKMODE_1) /*!< ADC synchronous clock derived from AHB clock divided by a prescaler of 2U */ #define ADC_CLOCK_SYNC_PCLK_DIV4 ((uint32_t)ADC12_CCR_CKMODE) /*!< ADC synchronous clock derived from AHB clock divided by a prescaler of 4U */ |
#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.
1 |
hadc1.Init.Resolution = ADC_RESOLUTION_12B; |
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.
1 2 3 4 |
#define ADC_RESOLUTION_12B (0x00000000U) /*!< ADC 12-bit resolution */ #define ADC_RESOLUTION_10B ((uint32_t)ADC_CFGR_RES_0) /*!< ADC 10-bit resolution */ #define ADC_RESOLUTION_8B ((uint32_t)ADC_CFGR_RES_1) /*!< ADC 8-bit resolution */ #define ADC_RESOLUTION_6B ((uint32_t)ADC_CFGR_RES) /*!< ADC 6-bit resolution */ |
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.
1 |
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; |
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.
1 |
hadc1.Init.ContinuousConvMode = DISABLE; |
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.
1 |
hadc1.Init.DiscontinuousConvMode = DISABLE; |
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.
1 |
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; |
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.
1 2 3 4 |
#define ADC_EXTERNALTRIGCONVEDGE_NONE (0x00000000U) #define ADC_EXTERNALTRIGCONVEDGE_RISING ((uint32_t)ADC_CFGR_EXTEN_0) #define ADC_EXTERNALTRIGCONVEDGE_FALLING ((uint32_t)ADC_CFGR_EXTEN_1) #define ADC_EXTERNALTRIGCONVEDGE_RISINGFALLING ((uint32_t)ADC_CFGR_EXTEN) |
Şimdi ise harici tetikleme hakkında bir sonraki komutu inceleyelim.
1 |
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; |
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.
1 |
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; |
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ı.
1 |
hadc1.Init.NbrOfConversion = 1; |
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.
1 |
hadc1.Init.DMAContinuousRequests = DISABLE; |
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.
1 |
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; |
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.
1 |
hadc1.Init.LowPowerAutoWait = DISABLE; |
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.
1 |
hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; |
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.
1 |
multimode.Mode = ADC_MODE_INDEPENDENT; |
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.
1 |
sConfig.Channel = ADC_CHANNEL_2; |
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.
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 |
#define ADC_CHANNEL_1 ((uint32_t)(ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_2 ((uint32_t)(ADC_SQR3_SQ10_1)) #define ADC_CHANNEL_3 ((uint32_t)(ADC_SQR3_SQ10_1 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_4 ((uint32_t)(ADC_SQR3_SQ10_2)) #define ADC_CHANNEL_5 ((uint32_t)(ADC_SQR3_SQ10_2 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_6 ((uint32_t)(ADC_SQR3_SQ10_2 | ADC_SQR3_SQ10_1)) #define ADC_CHANNEL_7 ((uint32_t)(ADC_SQR3_SQ10_2 | ADC_SQR3_SQ10_1 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_8 ((uint32_t)(ADC_SQR3_SQ10_3)) #define ADC_CHANNEL_9 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_10 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_1)) #define ADC_CHANNEL_11 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_1 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_12 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_2)) #define ADC_CHANNEL_13 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_2 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_14 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_2 | ADC_SQR3_SQ10_1)) #define ADC_CHANNEL_15 ((uint32_t)(ADC_SQR3_SQ10_3 | ADC_SQR3_SQ10_2 | ADC_SQR3_SQ10_1 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_16 ((uint32_t)(ADC_SQR3_SQ10_4)) #define ADC_CHANNEL_17 ((uint32_t)(ADC_SQR3_SQ10_4 | ADC_SQR3_SQ10_0)) #define ADC_CHANNEL_18 ((uint32_t)(ADC_SQR3_SQ10_4 | ADC_SQR3_SQ10_1)) /* Note: Vopamp1, TempSensor and Vbat internal channels available on ADC1 only */ #define ADC_CHANNEL_VOPAMP1 ADC_CHANNEL_15 #define ADC_CHANNEL_TEMPSENSOR ADC_CHANNEL_16 #define ADC_CHANNEL_VBAT ADC_CHANNEL_17 /* Note: Vopamp2/3U/4 internal channels available on ADC2/3U/4 respectively */ #define ADC_CHANNEL_VOPAMP2 ADC_CHANNEL_17 #define ADC_CHANNEL_VOPAMP3 ADC_CHANNEL_17 #define ADC_CHANNEL_VOPAMP4 ADC_CHANNEL_17 /* Note: VrefInt internal channels available on all ADCs, but only */ /* one ADC is allowed to be connected to VrefInt at the same time. */ #define ADC_CHANNEL_VREFINT ((uint32_t)ADC_CHANNEL_18) /** |
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.
1 |
sConfig.Rank = ADC_REGULAR_RANK_1; |
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.
1 |
sConfig.SingleDiff = ADC_SINGLE_ENDED; |
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.
1 |
sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; |
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.
1 2 3 4 5 6 7 8 |
#define ADC_SAMPLETIME_1CYCLE_5 (0x00000000U) /*!< Sampling time 1.5 ADC clock cycle */ #define ADC_SAMPLETIME_2CYCLES_5 ((uint32_t)ADC_SMPR2_SMP10_0) /*!< Sampling time 2.5 ADC clock cycles */ #define ADC_SAMPLETIME_4CYCLES_5 ((uint32_t)ADC_SMPR2_SMP10_1) /*!< Sampling time 4.5 ADC clock cycles */ #define ADC_SAMPLETIME_7CYCLES_5 ((uint32_t)(ADC_SMPR2_SMP10_1 | ADC_SMPR2_SMP10_0)) /*!< Sampling time 7.5 ADC clock cycles */ #define ADC_SAMPLETIME_19CYCLES_5 ((uint32_t)ADC_SMPR2_SMP10_2) /*!< Sampling time 19.5 ADC clock cycles */ #define ADC_SAMPLETIME_61CYCLES_5 ((uint32_t)(ADC_SMPR2_SMP10_2 | ADC_SMPR2_SMP10_0)) /*!< Sampling time 61.5 ADC clock cycles */ #define ADC_SAMPLETIME_181CYCLES_5 ((uint32_t)(ADC_SMPR2_SMP10_2 | ADC_SMPR2_SMP10_1)) /*!< Sampling time 181.5 ADC clock cycles */ #define ADC_SAMPLETIME_601CYCLES_5 ((uint32_t)ADC_SMPR2_SMP10) /*!< Sampling time 601.5 ADC clock cycles */ |
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.
1 2 |
sConfig.OffsetNumber = ADC_OFFSET_NONE; sConfig.Offset = 0; |
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.
1 2 3 4 5 6 7 8 9 10 11 |
while (1) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1 , 5000); uint32_t deger = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); if (deger>2000) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10 , GPIO_PIN_SET); else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10 , GPIO_PIN_RESET); } |
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.
1 2 3 4 5 |
if (deger>2000) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10 , GPIO_PIN_SET); else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10 , GPIO_PIN_RESET); } |
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!!
Bu sitede yayınlanan yazılar orjinal içerik olup faydalanılan kaynaklar belirtilmiştir. Yazarın izni olmaksızın tamamen alıntı yapılamaz, kopyalanamaz. Kaynak göstermek kaydıyla (Yazının adı, yazar adı ve link) kısmen alıntı yapılabilir.
Son Yorumlar