STM32F10X PWM+DMA ile Sinüs oluşturma (Örnek)

Başlatan Klein, 11 Mart 2013, 13:37:23

Klein

Başlık  "Sinüs oluşturma" ancak , sinüs ile sınırlı değiliz. İstediğimiz dalga şeklini oluşturmamız mümkün. HAtta çok karmaşık dalga şekilleri oluşturmak mümkün.

Neden DMA kullanmalıyız?
PWM ile herhangi bir dalga şeklini oluşturmak için , oluşturacağımız dalganın şekline göre ,PWM değerini belirli zamanlarda belirli sırada değiştirmemiz gerek. Bunun için kesmeleri kullanabiliriz. Her PWM kesmesi oluştuğunda kesme rutinine girip  PWM değerini hesaplayabilir veya bir tablodan çekeceğimiz değeri PWM registerine yazabiliriz.
Ancak oluşturacağımız dalganın frekansı ve çözünürlüğü ne kadar yüksekse , PWM frekansımız ve kesmeye girme sıklığımız o kadar yüksek olacaktır.
Örneğin:
1 derece çözünürlükte 360 derecelik 300Hz sinüs oluşturmak için ne sıklıkta kesmeye gireceğimize bakalım.
Bir periyodu tamamlamak için periyot boyunca 360 kez kesmeye gideceğiz. Frekansımız 300Hz  olacağı için bu işlemi saniyede 300 kez tekrarlayacağız.  Bu da saniyede 360*300 = 108000 kez kesme rutinini çalıştıracağız demektir. Buna bir de kesme rutininde yapacağımız işlemleri katarsak, bu iş için ne kadar büyük CPU zamanı kullanacağımız görülecektir.

İşte bu yüzden bu tip işleri DMA'ya bırakmak CPU'yu bize geri kazandıracaktır. PWM modülüne tablomuzun adresini , Hangi PWM kanalı ile çalışacağını bildiriyoruz. Bundan sonrasını DMA hallediyor. Biz hiç bir işe karışmıyoruz.

Örnekteki
void hsi_clock_init(void);

rutini  Dahili osilatörle çalışmak için koyduğum bir rutin. Siz Harici osilatörle çalışıyorsanız bu rutini çağırmanız gerekmiyor.

Örneğimiz 48Mhz Saat hızında çalışıyor.  Timer3'ün 4. PWM kanalından çıkış alıyor.
Sinüs frekansımız 50Hz. Çözünürlüğümüz 360 derece. 
Tabloyu değiştirerek istediğimiz dalga şeklini oluşturmamız mümkün.

#include "stm32f10x.h"
const uint16_t SineTable[] ={
		180,183,186,189,192,195,198,201,205,208,211,214,217,220,223,226,229,232,235,238,
		241,244,247,250,253,256,258,261,264,267,270,272,275,278,280,283,285,288,290,293,
		295,298,300,302,305,307,309,311,313,315,317,319,321,323,325,327,329,330,332,334,
		335,337,338,340,341,343,344,345,346,348,349,350,351,352,353,353,354,355,356,356,
		357,357,358,358,359,359,359,359,359,359,360,359,359,359,359,359,359,358,358,357,
		357,356,356,355,354,353,353,352,351,350,349,348,346,345,344,343,341,340,338,337,
		335,334,332,330,329,327,325,323,321,319,317,315,313,311,309,307,305,302,300,298,
		295,293,290,288,285,283,280,278,275,272,270,267,264,261,258,256,253,250,247,244,
		241,238,235,232,229,226,223,220,217,214,211,208,205,201,198,195,192,189,186,183,
		180,176,173,170,167,164,161,158,154,151,148,145,142,139,136,133,130,127,124,121,
		118,115,112,109,106,103,101,98,95,92,90,87,84,81,79,76,74,71,69,66,64,61,59,57,
		54,52,50,48,46,44,42,40,38,36,34,32,30,29,27,25,24,22,21,19,18,16,15,14,13,11,
		10,9,8,7,6,6,5,4,3,3,2,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,3,4,5,6,6,7,8,
		9,10,11,13,14,15,16,18,19,21,22,24,25,27,29,30,32,34,36,38,40,42,44,46,48,50,52,
		54,57,59,61,64,66,69,71,74,76,79,81,84,87,89,92,95,98,101,103,106,109,112,115,
		118,121,124,127,130,133,136,139,142,145,148,151,154,158,161,164,167,170,173,176
};

void hsi_clock_init(void)
{
	RCC_HSICmd(ENABLE);
	RCC_HCLKConfig(RCC_SYSCLK_Div1);
	RCC_PCLK1Config(RCC_HCLK_Div1);
	RCC_PCLK2Config(RCC_HCLK_Div1);
	RCC_PLLConfig(RCC_CFGR_PLLSRC_HSI_Div2,RCC_CFGR_PLLMULL12);
	RCC_PLLCmd(ENABLE);
	while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
	RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
	while (RCC_GetSYSCLKSource() != 0x08);
}

void timer_pwm_mode_init(void){
TIM_TimeBaseInitTypeDef TIM_TBInitStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;

         TIM_TBInitStruct.TIM_Period =381;
         TIM_TBInitStruct.TIM_Prescaler =6;
         TIM_TBInitStruct.TIM_ClockDivision = 0;
         TIM_TBInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
         TIM_TimeBaseInit(TIM3, &TIM_TBInitStruct);


         TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
	     TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;

         TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
	     TIM_OCInitStruct.TIM_OCIdleState = TIM_OCIdleState_Set;
	     TIM_OCInitStruct.TIM_Pulse = SineTable[0];
         TIM_OC4Init(TIM3, &TIM_OCInitStruct);

         TIM_DMACmd(TIM3,TIM_DMA_Update,ENABLE);
         TIM_Cmd(TIM3,ENABLE);
		 TIM_CtrlPWMOutputs(TIM3,ENABLE);
}

void rcc_init()
{
	 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
	 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO,ENABLE);
	 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
}

void gpio_init()
{
 GPIO_InitTypeDef  GPIO_InitStructure;


 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 ;  // PWM
 GPIO_Init(GPIOB, &GPIO_InitStructure);
}

void tim_dma_config(void)
{
DMA_InitTypeDef DMA_InitStructure1;

	  DMA_DeInit ( DMA1_Channel3);
	  DMA_Cmd(DMA1_Channel3,DISABLE);

	  DMA_InitStructure1.DMA_PeripheralBaseAddr = 0x40000440;
	  DMA_InitStructure1.DMA_MemoryBaseAddr = (uint32_t)SineTable;
	  DMA_InitStructure1.DMA_DIR = DMA_DIR_PeripheralDST;
	  DMA_InitStructure1.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	  DMA_InitStructure1.DMA_MemoryInc = DMA_MemoryInc_Enable;
	  DMA_InitStructure1.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	  DMA_InitStructure1.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	  DMA_InitStructure1.DMA_Mode = DMA_Mode_Circular;
	  DMA_InitStructure1.DMA_Priority = DMA_Priority_Medium;
	  DMA_InitStructure1.DMA_M2M = DMA_M2M_Disable;
	  DMA_InitStructure1.DMA_BufferSize = 360;
	  DMA_Init(DMA1_Channel3, &DMA_InitStructure1);

	  DMA_Cmd(DMA1_Channel3,ENABLE);
}

int main(void)
{
	hsi_clock_init();
	rcc_init();
	gpio_init();
	timer_pwm_mode_init();
	tim_dma_config();
    while(1)
    {
    }
}

SpeedyX

const uint16_t sinTable[32] = {
  2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056, 4095, 4056,
  3939, 3750, 3495, 3185, 2831, 2447, 2047, 1647, 1263, 909,
  599, 344, 155, 38, 0, 38, 155, 344, 599, 909, 1263, 1647};
 
uint32_t sinCosTable[32];
 
int main(void) {
  GPIO_InitTypeDef GPIO_InitStructure;
  DAC_InitTypeDef DAC_InitStructure;
  TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
  DMA_InitTypeDef DMA_InitStructure;
 
  SystemInit();
 
  for(uint8_t i = 0; i < 32; i++){
    sinCosTable[i] = sinTable[i] << 16;
  }
  for(uint8_t i = 8; i < 32; i++){
    sinCosTable[i - 8] |= sinTable[i];
  }
  for(uint8_t i = 0; i < 8; i++){
    sinCosTable[i + 24] |= sinTable[i];
  }
 
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM2, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
 
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
 
  TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
  TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseInitStructure.TIM_Period = 74;
  TIM_TimeBaseInitStructure.TIM_Prescaler = 0;
  TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
 
  TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
 
  DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0;
  DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable;
  DAC_InitStructure.DAC_Trigger = DAC_Trigger_T2_TRGO;
  DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
  DAC_Init(DAC_Channel_1, &DAC_InitStructure);
  DAC_Init(DAC_Channel_2, &DAC_InitStructure);
 
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(DAC->DHR12RD);
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&sinCosTable;
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
  DMA_InitStructure.DMA_BufferSize = 32;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
  DMA_Init(DMA1_Channel3, &DMA_InitStructure);
 
  DMA_Cmd(DMA1_Channel3, ENABLE);
 
  DAC_Cmd(DAC_Channel_1, ENABLE);
  DAC_Cmd(DAC_Channel_2, ENABLE);
 
  DAC_DMACmd(DAC_Channel_1, ENABLE);
 
  TIM_Cmd(TIM2, ENABLE);
 
  while (1) {}
}

muhittin_kaplan

Hocam Ellerinize Sağlık..
İlk Fırsatta deneyeceğim.

kantirici

hocam bununla ilgili aklıma takılan bir iki soru var.Öncelikle oluşturacacagımız tablo için frekansın nasıl br etkisi oluyor.Mesela 50Hz'lik bir sinüs için 10 bit ve 32 örnek durumunda 1023*(1+sin(2*pi*n/32))/2 ile tablo degerlerini oluşturursak sinüs frekansının burada etkisi nasıl oluyor.
Diger sorusu ise pwm frekansının oluşturulacak sinüs frekansına eşit olması gerekiyormu? teşekkürler.

Klein

PWM frekansı  = sinüs frekansı * örnekleme sayısı.  olmalı.

Tablonun sinüs frekansına doğrudan bir etkisi yok. Ancak tablodaki eleman sayımız  bir periyottaki örnekleme sayımız olduğu için , doğal olarak örnekleme sayımız ne kadar yükselirse aynı dalga frekansı için PWM frekansımız da okadar yükselecektir.



fractal

tam olarak neden dma ihtiyacımız var anlamadım.zaten bir timer her türlü durumda çalışmayacakmı.?.örneğin 50hz-ile 500hz arasında sinüs için ben şöyle yapıyorum.max frekansa göre hesaplıyorum.T=2ms.ben T/2 kullanıyorum.çünkü tam köprü var.ve yarım period için hesaplama yapıyorum sadece yani 180 dereceye kadar.sonra zaten akım yön değiştiriyor.ne kadar örnekleme yapacaksam  1ms/örnekleme adedim    bana timer kesme zamanını veriyor.örneğin 32 örnek varsa yarım period için ozaman yaklaşık 32 ms de bir kesme yaparak diziden değerleri okuyarak güncelleme yapıyorum.hangi frekansı kullanıcaksam ilk önçe onun tablosunu oluşturuyorum tabi.şimdi bu dma hangi noktada devreye giriyor?
Restantum cogniscutur Quantum deligutur

Klein

32 mikrosaniye olacaktı sanırım.

Bu kod interrupt içinde tablodan çektiği veriyi PWM registerine yazan basit bir kod.
void TIM2_IRQHandler(void)
{
	TIM3->CCR4 = sine_table[sine_index];
	if(++sine_index == 31) sine_index =0;
	TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}


Bu da disassembly hali.
08000b78:   mov.w r3, #1024 ; 0x400
08000b7c:   movt r3, #16384 ; 0x4000
08000b80:   movw r2, #64    ; 0x40
08000b84:   movt r2, #8192  ; 0x2000
08000b88:   ldrh r2, [r2, #0]
08000b8a:   mov r1, r2
08000b8c:   movw r2, #0
08000b90:   movt r2, #8192  ; 0x2000
08000b94:   ldrh.w r2, [r2, r1, lsl #1]
08000b98:   strh.w r2, [r3, #64]    ; 0x40
128       	if(++sine_index == 31) sine_index =0;
08000b9c:   movw r3, #64    ; 0x40
08000ba0:   movt r3, #8192  ; 0x2000
08000ba4:   ldrh r3, [r3, #0]
08000ba6:   add.w r3, r3, #1
08000baa:   uxth r2, r3
08000bac:   movw r3, #64    ; 0x40
08000bb0:   movt r3, #8192  ; 0x2000
08000bb4:   strh r2, [r3, #0]
08000bb6:   movw r3, #64    ; 0x40
08000bba:   movt r3, #8192  ; 0x2000
08000bbe:   ldrh r3, [r3, #0]
08000bc0:   cmp r3, #31
08000bc2:   bne.n 0x8000bd2 <TIM2_IRQHandler+94>
08000bc4:   movw r3, #64    ; 0x40
08000bc8:   movt r3, #8192  ; 0x2000
08000bcc:   mov.w r2, #0
08000bd0:   strh r2, [r3, #0]
129       	TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
08000bd2:   mov.w r0, #1073741824   ; 0x40000000
08000bd6:   mov.w r1, #1
08000bda:   bl 0x80003e0 <TIM_ClearITPendingBit>
130       }


pending biti temizlerken  , kesmeye girip çıkarken çalıştırdıkları hariç 36 komut işletiliyor.

500Hz 32 örnekli sinüs üretebilmek için, saniyede 32000 kez kesmeye gidiyorsunuz.

1 saniyelik periyodu tamamlamak için 32000*36 = 1.152.000 kez kesmeye gideceksiniz.

peki sinüs 500 Hz değil de 50Khz olsaydı?

saniyede 3.200.000 kez kesmeye gidecektiniz.
1 saniyede kaç komut işletmemiz gerekecekti?
 
3.200.000*36 =  115.200.000 komut.

İşlemcimiz zaten 72MHz.  saniyede 72 milyon komut işletebiliyor. 115 milyon komutu nasıl işletecekti?

Bazien hız yüzünden 1 baytın hesabını yapıyoruz. Bu kadar küçük hesaplar yaparken , CPU kaynaklarını bu kadar hoyratça kullanmamak gerek.
CPU Mühendisleri bu donanımları biz kullanalım diye koyuyor.  Bir işi hardware yapabiliyorsa , ona yaptırmak en iyisi.

z

Alıntı yapılan: fractal - 13 Mart 2013, 08:38:46
tam olarak neden dma ihtiyacımız var anlamadım.

Bir yerde duran yada bir yerden gelen verileri, veriler üzerinde hiç işlem yapamadan bir başka yere taşıyacaksan, işlemciyi bu işe bulaştırmadan DMA aracılığıyla yaparsın.

Fakat taşınacak veriler üzerinde gerçek zamanda işlem yapılacak ve taşınacaksa bu durumda DMA fazla bir işe yaramaz.
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

fractal

hocam dma olayının bu örnekte kullanıldığı noktayı tam olarak anlayamadım.dma ile pwm güncelleme zamanı nasıl yapılıyor.bu dma nasıl bir şeyki benim istediğim güncelleme zamanını bilsin ona göre güncellesin.pwm örneğini kastederek konuşuyorum.örnekleme sayısı değiştikçe dma sız durumda güncellemek için timer kesme zamanı değişcekti.bu iş dma ile nasıl oluyor?
Restantum cogniscutur Quantum deligutur

z

Örneği incelemedim. Fakat soruna cevap vereyim.

DMA ünitesine dataların alınacağı yığının yada ADC vs gibi ünitelerin adresini verirsin.
Dataların taşınacağı adresi yada PWM modülü gibi ünitelerin adresini verirsin.

Sonrada dersinki DMA seni ADC yada PWM dürtükleyecek her dürtüklediğinde sıradaki veriyi oku ve hedefe yaz.

Buradaki dürtüklemenin ne olacağı seçilebilir. Atıyorum PWM Count= PWM Peryod olduğunda DMA uyarılsın gibi.

Bu durumda CPUnun hiç müdahalesi olmadan  bir taraftaki veriler diğer tarafa aktarılır.

Eğer RAM'dan RAM'a aktarım yapılacaksa dürtüklemeye gerek kalmaz. DMA ünitesine al şunları taşı dersin olur biter.



Bana e^st de diyebilirsiniz.   www.cncdesigner.com

Klein

Şöyle:

TIM_DMACmd(TIM3,TIM_DMA_Update,ENABLE);


TIM3'e diyoruz ki  sen her update olduğunda. ( yani her PWM periyodunu tamamladığında) DMA'yı tetikle.

DMA tetiklemeyi aldığında

DMA_InitStructure1.DMA_DIR = DMA_DIR_PeripheralDST;

Hedef olarak Peripherial aygıt, kaynak olarak da memory  belirttiğimiz için.

DMA_InitStructure1.DMA_MemoryBaseAddr = (uint32_t)SineTable;

Burada belirttiğimiz adresteki ( SineTable) veriyi alıp

DMA_InitStructure1.DMA_PeripheralBaseAddr = 0x40000440;

Burada belirttiğimiz adrese (TIM3->CCMR3) yazıyor.

DMA tekrar tetiklediğinde
      
DMA_InitStructure1.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure1.DMA_MemoryInc = DMA_MemoryInc_Enable;

Bellek adresini artır ama , peripherial adresi sabit kalsın dediğimiz için,
bellek adresi artıyor ve  , SineTable'nin bir sonraki elemanı TIM3->CCMR registerine yazılıyor.


      
DMA_InitStructure1.DMA_BufferSize = 360;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

DMA bellek büyüklüğünü 360 verdiğimiz için 360 kez tetiklendiğinde 1 sinüs periyodu tamamlanmış oluyor.
Eğer sinüsün 1 periyodundaki örnek sayımız 32 olsaydı. Bu değer 32 olacaktı ve 32. tetiklemeden sonra sinüs periyodu tamamlanacaktı.

      
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

DMA modu Circular olduğu için bir sonraki tetiklemede başa dönüp , tüm adımları yeniden işletiyor.



fractal

hocam mükemmel anlatmışsınız.teşekkürler herkese..konuyu dma üzerine   kilitlemek istemem ama dma ile aynı anda haberleşme için aynı anda pwm  için kullanabilirmiyim.yada şöyle adc ile bir şeyler  okuyup örneğin akım hesabı yapıcam haberleşme ile bu bilgiyi sürekli bir ekrana basıcam.dma ile bu 3 şeyi sırayla cpu yu yormadan cözebilirmiyim.yoksa dma sadece bir şey içinmi kullanılır.dmabirkaç şey için kullanılırsa birbirlerini etkilerlermi?
Restantum cogniscutur Quantum deligutur

Klein

#12
Kullanabilirsiiz tabi ki.
2 tane DMA modülümüz var. Birinde 7 diğerinde 5 kanal var.
Ancak DMA kullanabilen donanımımız bu sayıdan fazla olduğu için , bazı kanallar ortak.


Tablodan da görüldüğü gibi.
ADC1 , TIM2_CH3, TIM4_CH1 Aynı kanalı kullanıyor.
SPI1_RX, USART3_TX, TIM1_CH1, TIM2_CH3 aynı kanalı kullanıyor.
...
...

Eğer hem USART3_TX , hem de SPI1_RX  kanalını  kullanacaksanız, DMA işini bitirdikten sonra, ilgili kanalın ayarlarını değiştirip diğer donanım için kullanabilirsiniz.
Fakat PWM örneğinde olduğu gibi Circular buffer şeklinde kesintisiz kullanım varsa ,o zaman aynı kanalı kullanamazsınız.

Bunu biraz tasarımı yaparken planlamak gerekiyor.
Örneğin DMA ile hem  PWM hem de USART TX kullanacaksınız diyelim.
Bu durumda kullanacağınız USART modülü USART3 ise , tasarımı yaparken PWM çıkışını TIM1_CH1'den değil TIM1_CH2'den almaya dikkat edeceksiniz. 
ADC kullanacaksanız TIM2_CH1 kullanmayacaksınız.

izzethoca

hocam çok teşekkür ederim. DMA konusunu az daha açmanız mümkünmü? Nedir nasıl kullanılır gibi

Klein

DMA Özünde basit işlevi olan bir donanım.
Tetikleme sinyali gelince şurdaki bilgiyi alıp , buraya kopyalayan bir donanım. Yaptığı tüm iş bu.
DMA'nın karışık gibi algılanmasına sebep olan şey , bir çok çevre birim ve bellek  ile ilişkilendirilebiliyor olması,
bu çevre birimleri tetikleyebilme ve bu birimler tarafından tetiklenebilme kabiliyetine sahip olması. 

DMA ile yapılmış bir kaç örnek sitede mevcut. Bunlar incelendiğinde aslında yapının ne kadar basit olduğu görülebilir.