25 Kasım 2015 Çarşamba

On-Line FIR filtre ve STM32F407



Figür 1’de gösterilen bir ADC, bir işlemci ve bir DAC bulunan uygulamada gerçek zamanlı olarak analog sinyallerin dijital filtrelemesidir. Bu yazımızda bir FIR filtre örneği verilecektir.

Figür 1. FIR filtrelemeye dâhil olan bloklar

                FIR (Finite Impulse Response) filtreleme giriş sinyalinin önceden belirlenmiş katsayılar ile konvolüsyon (convolution) kullanılarak yapılır. Konvolüsyon formülü aşağıda verilmiştir.


hm katsayısı filtrenin özelliklerini tanımlar ve istenilen filtrenin istenilen frekans karakteristiğinin Fourier dönüşümünün tersinin hesaplanmasıdır. “hm katsayıları “-M”den “+M”e (teoride “M sonsuza yakındır), “m indeksi negatif ve pozitif için bu metot kullanılarak hesaplanmıştır. Efektif olarak bunun anlamı; “k” zamanında filtre cevabının hesaplanması, “k” zamanındaki “x” örneklemesinin bilinmesi daha önceki örneklemeler (xk-1, xk-2, … , xk-m) ve gelecek örneklemeler ile hesaplanır. Biz uygulamada konvolüsyon formülünü direkt olarak kullanmayacağız fakat formülü değiştirerek veya yeniden düzenleyerek bir gecikme ile hesaplayacağız.



Bu durum grafiksel olarak figür 2’de gösterilmiştir. Örnek girişleri “x” çizimde kutu başına bir dairesel tampon (circular buffer) içerisinde saklanır. “k-1M”den “k” ya olan geçmiş indeksli örnekler konvolüsyon hesabı için kullanılır. Filtreden anlık çıkış olarak kullanılan “yk” sonucu aslında filtrelenmiş “x” giriş sinyalinin gecikme eklenmiş sonucudur. Gereksinim duyulan bu gecikme konvolüsyon hesaplamasında kullanılan katsayı sayısına bağlıdır.


Figür 2. FIR filtrelemede kullanılan örneklemeler

                “hm” katsayıları alçak geçiren (low pass) FIR filtre için aşağıdaki şekilde hesaplanır;



Burada “fs” örnekleme frekansı ve “fc” filtrenin köşe frekansıdır. Filtrenin katsayılarının fazla olması filtrenin keskinliği için en iyisidir. Ancak, konvolüsyon formülündeki birçok çarpma işleminde kullanılan katsayıların sayısının fazla olmasıdır bu durumda “m” seçilirken dikkatli olunmalıdır.

Köşe frekansları üzerindeki frekanslarda zayıflama düşük olabilir fakat “M” indeksine yakın katsayı değerlerinin azaltılması ile geliştirilebilir bu süreç “windowing” olarak da bilinir. Ortak pencere fonksiyonu yükseltilmiş kosinüs fonksiyonudur. Bu “von Hann window” olarak isimlendirilmiştir.



                FIR filtreleme gerçek zamanlı olarak uygulanabilir. Giriş sinyali düzenli aralıklarda örneklenmiş olmalıdır. Bu mikrodenetleyicinin bünyesinde bulunan bir timer ile elde edilebilir. Timer’ı başlatarak örneklemeyi başlatabilirsiniz ve daha önceki yazımızda anlattığımız “dairesel tampon” kullanarak alınan örnekleme değerleri “x” dairesel tamponunda saklanabilir. Kayıt edilmiş olan örnekleme değerleri yeni bir örneklemeden sonra konvolüsyon hesaplaması ile hemen hesaplanacaktır. Konvolüsyon sonucu bir DAC kullanılarak geri analog sinyale çevrilir veya “y” dairesel tampon içerisine kayıt edilir. Önemli olan nokta mevcut olan giriş sinyalinin yeni örneklemesinden sonra acil bir şekilde konvolüsyon hesaplamasının yapılmasıdır. Bu işlem kesme fonksiyonu içerisinde yapılır. Bir sonraki kesme isteğinden önce hesaplama bitirilmek zorundadır. Her çarpımda harcanan zamandan sonra konvolüsyon formülündeki katsayıların sayısı bunu limitleyecektir.

                Konvolüsyon formülünde kullanılan katsayı değerleri filtreleme boyunca aynı kalmalı ve filtreleme öncesinde bir defaya mahsus hesaplanması gerekmektedir.

                Figür 3’de bahsedilen program listelenmiştir. Programda ilk olarak 2 (iki) dairesel tampon “x1” ,“x2” ve “xPtr isimli işaretleyici tanımlanmıştır. Bu değişkenler diğer kesme fonksiyonlarında da kullanılacağından dolayı genel (global) olarak tanımlanmak zorundadır. Dairesel tampon uzunlukları abartılıdır fakat bu mikrodenetleyici için uzun tamponlar problem teşkil etmez. Tamponlar 32-bit integer olarak tanımlanmıştır. Bu durumda ADC’lerden gelen sonuçlar 16-bit olduğu için RAM tarafında tasarruf sağlanmış olacaktır. Daha sonra “w” ile isimlendirilen katsayı dizini tanımlanır ve veri tipi olarak “foat (kayan noktalı)” değer atanır. Katsayılar yukarıdaki formülü takiben az bir değere sahip olacağından dolayı floating yani kayan noktalı sayı olmak zorundadır. Bu dizin uzunluğunda abartılı olarak verilmiştir.

Programın “main” kısmında ilk olarak özelliklerin kurulumu gerçekleştirilir. “main” kısmında ADC ve DAC kurulumlarının fonksiyonları işlemin ana çatısının daha iyi anlaşılabilmesi için gizlenmiştir. Daha önceki ADC ve DAC ile ilgili yazılarımızda bu fonksiyonlar verilmişti. ADC 100uS zaman aralıklarında çevrime başlaması için timer bu çalışma şekline göre kurulumu yapılmıştır ve ADC’nin kesme istekleri aktif edilmiştir.

Ana program konvolüsyonun kullandığı katsayı değerlerini hesaplayarak devam eder. Yukarıdaki gördüğümüz formüldeki katsayı değerleri 0 (sıfır) indeksi ile merkezi katsayısı etrafında simetriktir, buna rağmen sadece pozitif “m” indeksleri için katsayıları hesaplamaya ihtiyacımız vardır ve sonraki 3(üç) program satırında bu işlem yapılmıştır. Pozitif indeksler ve indeks 0(sıfır) için bir merkez için 63 adet katsayı vardır.

Mikrodenetleyici filtreleme işlemini başlatmaya hazırdır. Program zaman gecikmesi ile beraber sonsuz döngü içerisinde devam eder.

#include "stm32f4xx.h"
#include "math.h"
#define pi 3.14159

int x1[4096], x2[4096], xPtr; // dairesel tamponların tanımlamaları
float w[1024]; // FIR ağırlıklarının tanımlaması

int main ()
{
GPIO_setup(); // pin kurulumlarının yapılması
DAC_setup(); // DAC kurulumunun yapılması
ADC_setup(); // ADC kurulumunun yapılması
Timer2_setup(); // Timer 2 kurulumunun yapılması
NVIC_EnableIRQ(ADC_IRQn); // NVIC ADC kesmesinin aktif edilmesi
w[0] = 2.0 * 100.0 / 10000.0;
for (short k = 1; k < 64; k++) // FIR ağırlıkları
w[k] = (w[0] * (sin(pi * k * w[0])) / (pi * k * w[0]));
for (short k = 1; k < 64; k++) // hanning window oluşturuluyor
w[k] = (w[k] * cos(pi/2 * k / 62.0));
// zaman gecikmesi
while (1)
{
if (GPIOE->IDR & 0x0001) GPIOA->ODR |= 0x0040; // LED yak
else GPIOA->ODR &= ~0x0040; // değil ise LED söndür
};
}
// kesme fonksiyonu
void ADC_IRQHandler(void) // geçiş yaklaşık 42us’dir
{
GPIOE->ODR |= 0x0100; // PE08 high yap
x1[xPtr] = ADC1->DR; // ADC1 sonucunu x1 dairesel tamponuna at
x2[xPtr] = ADC2->DR; // ADC2 sonucunu x2 dairesel tamponuna at
float conv = (float)x1[(xPtr - 100) & 4095] * w[0]; // merkezi ağırlığı al
for (int k = 1; k < 64; k++)
conv += w[k] * (x1[(xPtr - 100 + k) & 4095] + x1[(xPtr - 100 - k) & 4095]);
DAC->DHR12R1 = (int)conv; //sonucu DAC’a at
DAC->DHR12R2 = x1[(xPtr - 100) & 4095]; // orijinal değeri DAC’a at
xPtr = (xPtr + 1) & 4095; // dairesel tampon işaretçisini artır.
GPIOE->ODR &= ~0x0100; // PE08 low yap
}
Figür 3. FIR filtreleme uygulaması program kodları

Önemli işlemler kesme fonksiyonunda işletilir. ADC’den yeni bir örnekleme geldiğinde kesme fonksiyonu çağırılır. Her iki ADC’nin sonuçları “xPtr” işaretçisi kullanılarak dairesel tamponlarda saklanır. Sonra float değişken olan “w[0]” merkezi ağırlığı içerisinde ve önceki merkezi örnek “x1[(xPtr-100)&4095]” buraya saklanır. Burada biz anlık örneklemeden değerini 100 olarak vermeyi konvolüsyon uzunluğunu kapsamaya yeterli olduğunu varsayıyoruz. Bu değer “M” değerinde büyük olmak zorundadır.

Konvolüsyonun geri kalanı 63 dahil  “for” döngüsü içerisinde uygulanmaktadır. Katsayı değerleri merkezi katsayı etrafında simetriktir ve katsayının aynı değeri ile giriş örneklerinin çarpım bölümlerini oluşturmak için zaman gecikmesi gerekmektedir. İlk iki giriş örneklemesini eklemek ve sonra katsayı ile toplayıp çarpmak en iyisidir. Simetriyi vurgulamak ve AND fonksiyonu dairesel tamponu içerisinde işaretçiyi tutmak için “[xPtr-100+k]” ve “[xPtr-100-k]” girdi örnekleri olarak endekslenir.

İlk konvolüsyon hesaplanır, sonuç integer değere çevrilir ve DAC çevrimine gönderilir. Filtrelenmemiş orijinal sinyal ikinci DAC çevrimine karşılaştırma amaçlı olarak gönderilir. Bu sinyal filtrelenmiş olan sinyallin gecikmiş haliyle eşit olmak zorundadır. Konvolüsyonun kullandığı merkezi örnek DAC’a gönderilir.

İki GPIOE fonksiyonu port E üzerinde bir darbe sinyali oluşturmak için kullanılır. Darbe sinyali kesme fonksiyonunun işletilmesinde ne kadar zaman geçtiğini ölçmek için kullanılmaktadır (bu uygulamada 42uS).

Yürütme hızı integer tipte değişkenlerin ve integer matematik işlemlerinin kullanılmasıyla artırılabilir. Sadece figür 3’te gösterilen birkaç program satırını değiştirmek gerekebilir ve bunlar figür 4’de gösterilmiştir. Katsayılar için tanımlanmış olan dizi “int” olarak değiştirildi. Bundan önceki durumda katsayı değerleri 1 (bir) den küçüktü ve bu değerleri direkt olarak integer’a çevrilemezler. Fakat bu değerler 65536 ile çarpılabilir ve integer olarak saklanır (katsayı değerini 16-bit sola kaydırmak eşdeğerdir). Bundan sonra integer tipte katsayılara sahibiz ve integer tip giriş örneklerine tüm hesaplamalarda integer kullanabiliriz.  Beklendiği gibi bu 65536 kez daha büyük olan konvolüsyon sonucunu üretir ve 65536’ya bölünerek (16-bit sağa kaydırma ile) doğru sonuca ulaşılır.

int w[1024]; // FIR ağırlıklarının tanımlanması
// ana program içerisine
float w0 = 2.0 * 100.0 / 10000.0;
w[0] = (int)(w0 * 65536);
for (short k = 1; k < 64; k++) // FIR ağırlıkları
w[k] = (int)(w0 * 65536 * (sin(pi * k * w0)) / (pi * k * w0));
for (short k = 1; k < 64; k++) // Hanning window
w[k] = (int)((float)w[k] * cos(pi/2 * k / 63.0));
// kesme fonksiyonu içerisine
int conv = x1[(xPtr - 100) & 4095] * w[0]; // merkezi ağarlık al
for (int k = 1; k < 64; k++) // zaman gecikmesi
conv += w[k] * (x1[(xPtr - 100 + k) & 4095] + x1[(xPtr - 100 - k) & 4095]);
DAC->DHR12R1 = conv >> 16; // sonuç -> DAC
Figür 4. Integer filtreleme

Katsayıların hesaplanması ana program içerisinde değiştirildi. İlk olarak merkezi katsayı değeri “float” olarak hesaplandı fakat hemen integer tipteki değere çevrildi ve katsayı dizininin 0 (sıfır)ncı pozisyonuna saklandı. Sonra 1’den 63’e kadar indekslenen katsayı değerleri hesaplandı tümü 65536 ile çarpıldı. Hesaplanan katsayıları windowing yapmak amacıyla kayan nokta sayıları tekrar çevrilir, ağırlık ile çarpılır ve tekrar integer’a çevrilir.

Kesme fonksiyonu içerisinde ara değişken olan “conv” integer olarak tanımlanır ve hesaplama integer aritmetik belirtilmeden düzenli bir şekilde yapılır. Tün değerler integer olduğunda otomatik olarak bitmiş olur. Tek fark konvolüsyon sonucunun DAC’a gönderilmesi noktasındadır. O ilk 65536 değerine bölünür (16-bit sağa kaydırılır) ve ardından DAC’a yazılır.

Hızdaki önemli gelişme; kesme fonksiyonunun integer aritmetik kullanmasıyla birlikte sadece 10uS’dir.

FIR filtrelemede gerçekleştirilebilecek ilginç bir fonksiyon, 90 derece için bilinmeyen bir frekans ile bir sinyalin kaydırılmasıdır. Bu fonksiyon “Hilbert dönüşümü” olarak isimlendirilir. Hilbert dönüşümü için katsayılar şu formül ile hesaplanır.


FIR filtrelemenin integer sürümü için aynı program kullanılabilir fakat katsayılar yukarıdaki formül ile hesaplanmalıdır. Bu durum figür 5’de gösterilmiştir. Buradaki katsayılar 65535 ile çarpılır ve konvolüsyon sonucu aynı faktöre bölünmelidir.

// ana program içerisinde
w[0] = 0;
for (short k = 1; k < 64; k++) // FIR ağırlıkları
w[k] = (int)(65536.0 * (-1.0 + cos(pi * k)) / (pi * k));
for (short k = 1; k < 64; k++) // Hanning window
w[k] = (int)((float)w[k] * cos(pi/2 * k / 63.0));
Figür 5. Hilbert dönüşümü ile katsayıların hesaplanması

Figür 6’da özelliklerin kurulumu ile ilgili fonksiyonlar gösterilmiştir. Figür 3’de bunlar ana program dışına atılmıştı.

void ADC_setup(void)
{
RCC->APB2ENR |= 0x00000100; // ADC1 için clock
RCC->APB2ENR |= 0x00000200; // ADC2 için clock
ADC->CCR = 0x00000006; // Regular simultaneous mode
ADC1->CR2 = 0x00000001; // ADC1 açık
ADC1->SQR3 = 0x00000002; // PA02 giriş olarak kullan
ADC2->CR2 = 0x00000001; // ADC2 açık
ADC2->SQR3 = 0x00000003; // PA03 giriş olarak kullan
GPIOA->MODER |= 0x000000f0; // PA02, PA03 analog giriş yap
ADC1->CR2 |= 0x06000000; // TIM2, TRG0’da SC kaynağını kullan
ADC1->CR2 |= 0x10000000; // harici SC kaynağı, yükselen kenar
ADC1->CR1 |= 0x00000020; // EOC için ADC kesmesi aktif
}

void DAC_setup(void)
{
RCC->APB1ENR |= 0x20000000; // DAC için clock aktif
DAC->CR |= 0x00010001; // DAC kontrol register, her iki kanalda açık
GPIOA->MODER |= 0x00000f00; // PA04, PA05 analog çıkışlar
}

void GPIO_setup(void)
{
RCC->AHB1ENR |= 0x00000001; // GPIOA için clock aktif
RCC->AHB1ENR |= 0x00000010; // GPIOE için clock aktif
GPIOE->MODER |= 0x00010000; // PE08 pin’i çıkış: zaman izleme için
GPIOE->MODER |= 0x00040000; // PE09 pin’i çıkış: toggle
GPIOA->MODER |= 0x00001000; // PA06 pin’i çıkış: LED
}

void Timer2_setup(void)
{
RCC->APB1ENR |= 0x0001; // Timer 2 için clock aktif
TIM2->ARR = 8400; // otomatik geri yükleme değeri: 8400 == 100us
TIM2->CR2 |= 0x0020; // TRGO’ı güncelleme olayı olarak seç (UE)
TIM2->CR1 |= 0x0001; // sayacı başlat
}
Figür 6. Kurulum fonksiyonları listesi

Şimdilik bu kadar arkadaşlar Gülen
Sorularınız ve önerileriniz için m.hakki.kaplan@gmail.com adresinden her zaman bana ulaşabilirsiniz.
Hakkı KAPLAN

Hiç yorum yok:

Yorum Gönder