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
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