STM32 ADC Kullanımı (Polling, Interrupt ve DMA)

Merhabalar,

Bu konuda STM32 serisi kartlarda CubeMX ile ADC kullanımının nasıl yapıldığını anlatmaya çalışacağım. Daha önceden Arduino ve benzeri mikrodenetleyici tabanlı bir platform kullanmış olanlar anlatacağım konuları daha kolay anlayacaklardır. Ancak ilk defa bir mikrodenetleyici kullanacaklar için biraz karmaşık gelebilir. ADC bir çok kişinin de bildiği üzere “Analog Digital Converter” in baş harflerinden oluşan analog veriyi dijital veriye dönüştürme aracıdır. İşlemlerinin tamamını dijital olarak yapan mikrodenetleyici tabanlı sistemler için, analog sinyaller ile çalışılması gerektiğinde ADC çok önemli bir donanımdır. Aslında işin en özeti ADC’ ler analog değer olarak sürekli olan veriyi, zaman ve değer uzayında ayrık hale getirmeye yararlar. Zaman uzayında ayrık demek belirli zaman aralıklarında analog sinyalden veri alınması(örneklenmesi) anlamına gelmektedir. Değer uzayında ayrık olması ise alınan örneklerin ADC’ nin çözünürlüğüne göre sadece belirli değerleri alabilmesidir(kuantalanması). Burada bahsettiğimizi çözünürlük ne anlama gelmektedir. Biraz ondan bahsedecek olursam aşağıdaki resim yardımcı olabilir.

1fig_qual

Resimde analog bir sinüs işaretinin 2 farklı çözünürlükte örneklenmiş hali görünmekte. 3 bitlik olan örneklemeye dikkat edecek olursak alabildiği değerler 2^3 = 8 tanedir. Giriş sinyalinin değeri ne olursa olsun çıkış değerinin bu 8 seviye dışında olması imkansıdır. Aslında aynı durum 16 birlik çözünürlük kullanılması durumunda da vardır. Ancak 16 bit kullanılması durumunda alınabilecek değerlerin sayısı artacak ve seviyeler arası fark oldukça küçülecektir. ADC’ nin çözünürlüğe bağlı olarak seviyeler arasındaki fark aşağıdaki şekilde hesaplanabilir.

adc_resolution2

Formülde belirtilen Vmax ve Vmin giriş işaretinin max ve min değerleri değil, kullanılan ADC’ nin ölçebileceği en yüksek ve en düşük gerilim değerleridir. Genelde ADC’ lerde en düşük değer 0 olarak alınır. Ancak ADC’ lerde bulunan Vref girişleri ile bu değer değiştirilebilir. Biz bu konuda buna değinmeyeceğiz. Payda da verilen 2^n ifadesindeki n ise çözünürlük olup bit değeri olarak ifade edilir. Örneğin 10 bit çözünürlüklü bir ADC için payda 1024 değerini alacaktır.  10 bitlik 5V girişli bir ADC için adım aralığı yaklaşık olarak 4.8 mV çıkacaktır. Bu aynı zamanda şu anlama gelmektedir. Bu ADC en az 4.8 mV’ lık bir gerilim ölçebilir. Bunun daha aşağısındaki gerilim değerleri için daha hassas bir ADC kullanılmalı yada ölçülecek işaret yükseltilmelidir. Şimdi gelelim STM32′ de ADC kullanımının nasıl yapıldığına.

STM32′ de ADC’ yi 3 farklı şekilde kullanabiliriz. İlki Arduino’ da da kulladığımız “polling” kullanımı. ADC polling olarak kullanırken analog dijital çevrim süresince farklı bir işlem yapmaz ve çevrimin bitmesini bekler. Yapılacak ölçümün çok hızlı olmasının gerekmediği yada uzun zaman aralıklarında tek ölçüm yapılmasının yeterli olduğu durumlarda sıklıkla kullanılır. Diğer bir kullanım şekli ise “interrupt” lı kullanımdır. Bu kullanım şeklinde denetleyici ADC’ ye çevrim işleminin başlaması için komut verir ve yazılımda belirtilmiş diğer işlemler yapmaya devam eder. Analog dijital çevrim tamamlandığı zaman ise bir interrupt meydana gelir ve interrupt vektörünün işaret ettiği fonksiyon çağrılır. Böylece çevrim işleminin uzun olduğu durumlarda ADC çevriminin beklemek gerekmez. Bloklayıcı işlemlerin olmaması gereken hassas uygulamalarda sıklıkla kullanılır. Son kullanım şekli ise DMA (Direct Memeory Acces) ile kullanımdır. Bu kullanım şeklinde ADC’ ye dönüştürmeye başlaması için mikrodenetleyici tarafından komut verilir ve diğer işlemlere yapılmaya devam eder. Interrupt ile kullanımından en büyük farkı, ADC’ nin çevrimi tamamladıktan sonra elde ettiği değeri hafıza bölgesine DMA tarafından yazılmasıdır. Böylece mikrodenetleyici hiç bir şekilde ADC işlemleri ile meşgul olmaz. Özellikle çok sayıda ölçümün ard arda ve hızlı yapılmasının istendiği durumlarda DMA kullanılır. 

Bu kullanım şekillerininin STM32′ de kullanımına gelmeden STM32 CubeMX ile ayarlamalarımızı yapalım. Ayarları her seferinde ayrı ayrı göstermemek için sadece bir resim koyacağım ve Polling, Interrupt ve DMA kullanımı için gerekli ayarların hepsi bu resimde olacak. Yani bu resimdeki ayarların hepsini polling yada interrupt kullanımı için yapmanıza gerek yok.  Ancak bu ayarlar ile üç kullanım şekli ile ADC kullanabilirsiniz. Resimdeki ayarların hepsini elimden geldiğince açıklamaya çalışacağım.

adc_settings3

  • Clock Prescaler: Bu ayar ADC’ nin çalışmak için ihtiyaç duyduğu saat darbesini PCLK2 hattından kaç ile bölerek kullanacağını belirtir.
  • Resolution: Yukarıda belirttiğimiz çözünürlük ayarı. Daha düşük çözünürlük seçerek daha az saat derbesi gerektiren daha hızlı ölçümler yapılabilir.
  • Data Alignment: Analog dijital çevrim sonunda elde edilen verinin en değerli biti sağda mı yoksa solda mı olacağını belirler.
  • Scan Conversion: Aktif edilmesi durumunda aynı ADC’ de birden fazla kanal kullanılacağı zaman ölçümleri arka arkaya yapar. Böylece her kanal için ölçüm sonrası ayrı ayrı sayısal dönüştürme yapmaz, aktif edilen tüm kanallar için ölçüm bitince tek sefer dönüştürme yapar. Bu da dönüştürme işleminin daha hızlı olmasını sağlar.
  • Continuous Conversion: ADC dönüşümü yazılımda bir kere başlatıldığında tekrar başlatmaya gerek kalmaz. Dönüştürme tamamlanınca tekrar çevrim başlar.
  • DMA Continuous Request: ADC’ nin DMA ile kullanımı için yapılması gereken ayar.
  • Number of Covnersion: ADC başlatıldığında yapılacak çevrim sayısı. Aktif edilmek istenen kanal sayısı kadar seçilmelidir.
  • Channel: Aktif edilen ADC kanalının seçimi
  • Sampling Time: ADC ile yapılacak 1 çevrim işlemi için gereken cycle sayısı. Cycle sayısı artarsa çevrim süresi uzar ama ölçüm daha doğru gerçekleşir.

Aşağıdaki resimlerde Interrupt (NVIC) ve DMA mevcut. Yukarıda da belirttiğim gibi ADC’ yi polling olarak kullanırken bu ayarlara ihtiyacınız olmayacaktır.

adc_int

adc_dma_set

Polling Kullanımı

ADC’ nin polling olarak kullanımı ile başlayalım. Üstte de belirttiğim gibi bu moddaki kullanım Arduino’ daki kullanım ile benzerdir. Yazılım herhangi bir yerinde ADC’ ye çevrime başlaması için komut verilir, çevrimin tamamlanması beklenir ve dijitale dönüştürülmüş değer elde edilir. Okuduğumuz değerleri göndermek için de UART kullanabiliriz. Yada Debug modunda yazılımı belirli noktalarda durdurarak ADC değerini okuyabilirsiniz. 

  while (1)
  {
	  HAL_ADC_Start(&hadc1);							//start adc conversation
	  HAL_ADC_PollForConversion(&hadc1, 1000);			//wait for first conversation end
	  uint16_t adcVal1 = HAL_ADC_GetValue(&hadc1);		//get converted channel 1 value
	  HAL_ADC_PollForConversion(&hadc1, 1000);			//wait for second conversation end
	  uint16_t adcVal2 = HAL_ADC_GetValue(&hadc1);		//get converted channel 2 value
	  HAL_ADC_Stop(&hadc1);								//stop adc
	  
	  //send read values over serial port
	  char txt[30];
	  sprintf(txt, "ADC Val: %d %d\n\r", adcVal1, adcVal2);			//print value in a text
	  HAL_UART_Transmit(&huart3, (uint8_t *)txt, strlen(txt), 100);		//send text over UART
	  HAL_Delay(500);									//wait 100ms
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

Yukarıda kullanılan fonksiyonların hepsi “Drivers/STM32Fxxx_HAL_Driver/stm32fxxx_hal_adc.c” isimli dosyanın içerisinde bulunmaktadır. Dosyanın başında ADC kullanımı ile ilgili ek açıklamalar da mevcuttur. İncelemenizi tavsiye ederim. Ayrıca ben projemi oluştururken çevrim yapılması için iki kanal aktif ettim. Bu yüzden iki farklı “pollconversion” ve “getvalue” fonksiyonları kullanılmıştır.

ADC’ yi polling olarak kullanırken yazılması gereken yazılım bu kadar. Dikkat edilmesi gereken nokta “HAL_ADC_PollForConversion” fonksiyonunda ikinci parametre olan timeout seçilirken sampling time’ a dikkat edilmelidir. Sampling time değerinin 470 cycle seçilmişse timeout 10,20 gibi küçük değerler seçilmesi durumunda fonksiyon her zaman timeout’ a düşecektir.

Interrupt Kullanımı

Interrupt kullanımı kısmı buna göre bir tık daha karışık olacaktır. Yukarıda da bahsettiğim gibi Interrupt ile kullanırken ADC çevrim işlemini arka planda yapar ve bittiği zaman bir interrupt oluşturur. Böylece dönüştürme işlemi bittiğini anlayıp değeri ADC’ den isteyebiliriz. Öncelikle ihtiyacımız olan şeylere bir bakalım. Bir interrupt fonksiyonu, ADC verilerini kayıt etmek için bir değişken yada birden fazla kanal kullanılıyorsa bir dizi, son olarak da dönüştürme işleminin tamamlandığını anlamak için bir bayrak değişken.

/* USER CODE BEGIN PV */
uint8_t convEnd = 0,			//conv end flag
		cnt = 0;				//conv counter

uint16_t adcVal[2] = {0};		//to store converted values
/* USER CODE END PV */

İhtiyacımız olan değişkenleri global olarak main fonksionun üst tarafında bir yerde oluşturduk. ADC verilerinin tutulacağı değişkenleri önceki örneklerde de olduğu gibi 16 bit olarak tanımladım. Ancak burada özel bir durum daha var o da DMA’ nın veri boyutu seçimi. Daha önce de belirttiğim gibi 12 bitlik ADC kullandığımız için 16 bitlik bir değişken bu veriyi saklamak için yeterli oluyor. Bu yüzden DMA’ da Half-Word yani 16 bit seçimini yaptım.  İki ADC kanalı kullandığım iki elemanlı bir dizi tanımladım. Kaç kere çevrim yapıldığını sayabilmek için de bir değişken tanımladım. Şimdi ADC interrupt fonksiyonunu oluşturalım.

/* USER CODE BEGIN PFP */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc == &hadc1)
	{
		adcVal[cnt++] = HAL_ADC_GetValue(hadc);

		if(cnt == 2)
		{
			cnt = 0;
			convEnd = 1;
		}
	}
}
/* USER CODE END PFP */

Interrupt callback fonksiyonunu yukarıdaki gibi tanımladık. Burada önemli olan nokta bu fonksiyonu biz herhangi yerden yazılımsal olarak çağırmayacağız. ADC çevrimi tamamlandığında bu fonksiyon çalışmaya başlayacaktır. Bunu sağlayan şey STM32′ nin startup dosyasındaki vektör tablosudur. O dosyada bu fonksiyonları çağıran yapılar bulunmaktadır. Bunun çok detayına girmeyeceğim. STM32′ lerin bir çoğunda birden fazla ADC mevcuttur ancak her ADC için ayrı bir interrupt fonksiyonu bulunmaz. Interrupt fonksiyonunun parametresi yardımı ile interrupt’ un hangi ADC’ den geldiğinin ayrımını yapabiliriz. Daha sonra çevrim tamamlanan ADC değerini bir değişkene atamalıyız. Ben iki kanal kullandığım için bu işlemde bir dizi kullandım ve her fonksiyona girdiğimde indis değerini arttırdım. Eğer indis değeri 2 olmuşsa da dönüştürme işleminin tüm kanallar için tamamlandığını belirten bayrak değişkeni 1 yaptım. Böylece yazılım içerisindeki farklı fonksiyonlarda da ADC çevrimin bittiğini anlayabiliriz. 

Interrupt kısmını da ayarladıktan sonra sıra geldi main fonksiyonuna. Bu kısımda ADC’ yi başlatmalı ve çevrim tamamlandığında veriyi UART’ dan göndermeliyiz.

  HAL_ADC_Start_IT(&hadc1);					//start ADC conversation
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if(convEnd == 1)					//check is conv completed
	  {
		  char txt[30];
		  sprintf(txt, "ADC Val: %d %d\n\r", adcVal[0], adcVal[1]);			//print value in a text
		  HAL_UART_Transmit(&huart3, (uint8_t *)txt, strlen(txt), 100);		//send text over UART
		  HAL_Delay(500);									//wait 100ms
		  convEnd = 0;
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

Dikkat ederseniz ADC’ yi başlat işini while döngüsünün dışında sadece 1 kez yaptım. ADC’ nin ayarlarında “continuous conversion” ayarı açık olduğu için, ADC tüm kanalların dönüştürme işlemini bitirdikten sonra 1. kanala geri dönüp dönüştürme yapmaya devam edebiliyor. Tek kanal kullanıyorsak da bu işlemi aynı şekilde yapmamız halinde sadece tek kanalın değeri sürekli olarak arka planda okunmaya devam eder. Ancak burada sadece analog dijital işlemi gerçekleşmekte. Bu işlem tamamlanınca interrupt fonksiyonuna gidip ADC değerini bir değişkene kayıt etmemiz gerekmekte. Eğer değişkene kayıt etme işleminin de arka planda gerçekleşmesini istiyorsak DMA kullanmalıyız.

DMA Kullanımı

Yukarıda da bahsettiğimiz gibi DMA kullanımında mikrodenetleyici ADC işlemleriyle meşgul olmaz. Bu getirdiği kolaylıkların yanı sıra bazı zorluklarda getirir. Bunların en başında özellikle çok sayıda çevrim yaparken DMA’ ya kullanması için verilen dizinin başka yerlerde kullanılmaması gerekir. Örneğin 500 çevrim yapacak olalım. ADC’ nin sampling time değeri küçük seçilmişse çevrim çok hızlı gerçekleşecektir. Veriler ise diziye tüm çevrimlerin bitmesini beklemeden sırası geldikçe yazılacaktır. Yazılım içerisinde 500 elemanlı diziyi kullanırken ilk 200 eleman yeni çevrimden, son 300 eleman ise önceki çevrimden kalmış olabilir ki bu da istenmeyen sonuçlara sebep olabilir. Bunu önlemenin en kolay yolu da çevrim işlemi tamamlandığında interrupt fonksiyonunda veriyi aynı boyutlu farklı bir diziye kayıt etmektir. Böylece dönüşüm tamamlanana kadar dizi içerisindeki değerler değişmez. Şimdi gelelim yazılım kısmına.

/* USER CODE BEGIN PV */
#define BUFF_SIZE 100
uint8_t convEnd = 0;			//conv end flag

uint16_t adcBuff[BUFF_SIZE] = {0};		//to store converted values
uint16_t adcData[BUFF_SIZE] = {0};		//to use as a second buffer
/* USER CODE END PV */

Yukarıda verilen yazılımda dizilerin uzunluğu 100 olarak tanımlanmış. Bunun sebebi DMA ile kullanırken 100 dönüşüm yaptıracak olmamız. Bunun boyutunu seçmek tamamen keyfidir. Tek dönüşüm için ADC kullanmak çok mantıklı değildir. Çok fazla dönüşüm yapmak isterseniz de dönüşüm süresinin çok uzayacağına dikkat etmelisiniz. Şimdide interrupt fonksiyonun içerisine bakalım. 

/* USER CODE BEGIN PFP */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc == &hadc1)								//check which adc made interrupt
	{
		for(int i = 0; i < BUFF_SIZE; i++)			
		{
			adcData[i] = adcBuff[i];				//copy data to second buffer
		}
		convEnd = 1;								//set conv end flag
	}
}
/* USER CODE END PFP */

ADC Interrupt içerisinde ilk olarak veriyi ikinci buffer’ a kopyalama işlemini gerçekleştirdik. Sonra da dönüştürmenin tamamlandığını anlayabilmemiz için bir bayrak değişkeni 1 olarak ayarladık. Bu fonksiyona 100 çevrim tamamlandığında gireceğinden bütün veriyi farklı bir buffer’ a kaydedebiliriz. Burada yine yapılacak dönüşüm sayısının çok fazla olması kopyalama işleminin uzun sürmesine sebep olacaktır. Bu sorunun önüne geçmek için “HAL_ADC_ConvHalfCpltCallback” fonksiyonu kullanılabilir. Bu fonksiyon yapılacak toplam çevrim sayısının yarısı olunca çalışan interrupt fonksiyonudur. Bizim uygulamamızda buna ihtiyacımız olmadığı için daha fazla detay vermeyeceğim. Şimdi main fonksiyonuna gelelim.

  /* USER CODE BEGIN 2 */
  HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuff, BUFF_SIZE);			//start ADC conversation
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if(convEnd == 1)					//check is conv completed
	  {
		  char txt[30];
		  sprintf(txt, "ADC Val: %d %d\n\r", adcData[0], adcData[1]);			//print value in a text
		  HAL_UART_Transmit(&huart3, (uint8_t *)txt, strlen(txt), 100);		//send text over UART
		  HAL_Delay(500);									//wait 100ms
		  convEnd = 0;
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

Main fonksiyon içerisindeki yazılım interrupt kısmı ile neredeyse aynı. Sadece ADC’ yi başlattığımız fonksiyon DMA kullandığımız için farklı. DMA ile kullanırken parametre olarak buffer’ ı ve buffer’ ın uzunluğunu vermemiz gerekiyor. Böylece ADC, buffer uzunluğu kadar çevrim yapar ve buffer’ ın içerisine sırası ile kayıt edebilir. Başta da belirttiğimiz gibi genellikle çok sayıda dönüştürme işleminin yapılması gereken durumlarda DMA’ lı yöntemi kullanmak daha faydalıdır. 

Üstteki 3 uygulamada da ADC ile dijitale çevrdiğimiz veriyi byte olarak UART üzerinden gönderdik. UART’ dan gönderdiğimiz veriyi görebilmek için veriyi byte olarak görüntüleyen bir araç lazım. Buna en güzel örnek Serial Plot uygulamasıdır. Bu uygulama Windows, Linux ve Mac de çalışabilir. Seri port üzerinden gelen byte yada string veriyi grafik olarak çizdirebilir. Uygulamaya bağlantıdan ulaşabilirsiniz.

Bu kısımdan sonra son olarak da ADC ile ilgili süre işlemlerinin nasıl hesaplandığına bakacağız.

ADC Çevrim Süresi Hesaplama

ADC çevrim süresi hesaplanırken dikkat edilmesi gereken bazı parametreler var. Bunlardan ilki sistemin saat frekansıdır. STM32 üzerindeki ADC veri yolunun saat hızı bu frekansa göre değişiklik gösterecektir. ADC’ nin hangi saati kullandığını görmek için ADC ayarlarının bulunduğu resme bakabilirsiniz. Benim kullandığım mikrodenetleyici için PCLK2 kullandığını yazmış. stm32_adc_clock

Clock ayarlarının bulunduğu sekmede PCLK2 peripheral veri yolunun saati 108 Mhz olduğu gözükmekte. ADC bir “peripheral” olduğu için bu saate bakmalıyız. Ancak bu veri yolunu kullanan bir timer kullanıyor olsaydık 216 Mhz ile hesaplamalarımızı yapmamız gerekecekti. Öncelikle 1 cycle için gerekli süreyi hesaplayalım. Bu süre 1 / adc saat frekansı olarak hesaplanabilir.

ADC Saat frekansı “Clock Prescaler” ayarı kullanılarak “PCLK2 peripheral” değerinin 4’de biri olarak yukarıda seçmiştik. Bu sebeple ADC 27 Mhz saat ile çalışmaktadır.

108 * 10^6 / 4 = 27 * 10^6

1 / (27 * 10^6) = 0.370 nano saniye 

ADC ayarlarında iki kanal aktif etmiştim ve örnekleme süresini 144 cycle olarak seçmiştik. Çözünürlük 12 bir olarak seçildiği için de kuantalama kısmında 15 cycle süre gerekli. ADC ayarlarındaki scan conversion aktif olduğu için 2 kanalın örneklemesi tamamlandıktan sonra kuantalama işlemi gerçekleşecek. Böylece her örneklemeden sonra ek kuantalama süresi gerekmiyecek. Toplamda;

144 + 144 + 15 =  303 cycle kadar dönüşüm süresine ihtiyacımız var. 1 cycle için gerekli süre ile bunu çarpacak olursak.

0.370 * 10^(-9) * 303 = 11.22 mikro saniye

İki kanal ile dönüşüm yaparken süre hesabı da basit olarak yukarıdaki şekilde yapılabilir.

CubeMx ayarlarına ve yazılımın son haline bağlantıdan ulaşabilirsiniz.

23 Comments

Add a Comment

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir