Kampis Elektroecke

XMega – DMA

XMega Blockdiagramm

Mit Hilfe des DMA-Controllers können sowohl kleine als auch große Datenmengen mit einer minimalen CPU Interaktion kopiert werden. Dadurch ist es z. B. möglich hochfrequente Messergebnisse des ADCs in den Speicher zu schreiben, ohne das die CPU blockiert wird. In diesem Teil des Tutorials zeige ich, wie mit Hilfe des DMA-Controllers der ADC ausgelesen und das Resultat automatisch zum DAC kopiert wird, der das Ergebnis als analoge Spannung ausgibt.

DAC und ADC konfigurieren:

Für dieses Projekt werden, neben dem DMA, auch der DAC und der ADC benötigt. Die Konfiguration des DAC soll folgendermaßen aussehen:

  • DACB
  • 1 V Referenzspannung
  • Kanal 0
DACB.CTRLC &= ~(DAC_REFSEL1_bm | DAC_REFSEL0_bm | DAC_LEFTADJ_bm);
DACB.CTRLB &= ~(DAC_CH1TRIG_bm | DAC_CH0TRIG_bm | DAC_CHSEL1_bm | DAC_CHSEL0_bm);
DACB.CTRLA |= DAC_CH0EN_bm | DAC_ENABLE_bm;

Und für den ADC:

  • ADCA
  • 1 V Referenzspannung
  • Ungetriggerter Modus
  • Unsigned 12-Bit Modus
  • Taktvorteiler 32
  • Pin 0, Kanal 0
  • Single-ended mit Gain x1
ADCA.CTRLB = ADC_FREERUN_bm;
ADCA.REFCTRL = ADC_REFSEL_INT1V_gc;
ADCA.PRESCALER = ADC_PRESCALER_DIV32_gc;
ADCA.CTRLA = ADC_ENABLE_bm;
	
PORTA.DIR &= ~(0x01 << 0x00);
ADCA.CH0.CTRL = ADC_CH_INPUTMODE0_bm;
ADCA.CH0.MUXCTRL = 0x00;

Damit sind die benötigten Peripheriegeräte einsatzbereit. Nun muss der DMA-Controller konfiguriert werden…

Konfiguration des DMA-Controllers:

Mit dem DMA können Datenblöcke mit einer Größe von 1 Byte bis 64 kB ohne CPU Unterstützung übertragen werden. Da die CPU und der DMA-Controller den gleichen Datenbus verwenden, wird jeder Datenblock in kleinere Segmente zerlegt. Der DMA-Controller meldet dann einen Datentransfer an, wodurch der Datenbus von der CPU getrennt und vom DMA-Controller kontrolliert wird. Während dieser Phase sendet der DMA-Controller eine bestimmte Menge Daten und übergibt danach den Bus wieder an die CPU.

Diese Transferart wird Burst transfer genannt und der DMA-Controller kann 1, 2, 4 oder 8 Bytes in einem Burst übertragen. Eine komplette DMA transaction besteht zudem aus 1256 Blöcken. Über das REPCNT-Register kann eine DMA-Übertragung mit identischen Einstellungen bis zu 255 mal (insgesamt 256 Übertragungen) wiederholt werden. Der Wert 0 gibt dabei unbegrenzte Wiederholungen an.

In diesem Beispiel sollen die Daten vom ADC an den DAC gesendet werden. Der ADC besitzt eine Auflösung von
12-Bit, was einer Datenmenge von 2 Byte entspricht. Ich habe mich für eine Burst-Länge von 2 Byte entschieden, wodurch das komplette ADC Ergebnis in einem Rutsch kopiert werden kann.

Zuerst werden ein paar allgemeine Einstellungen am DMA-Controller und dem verwendeten Kanal (hier Kanal 0) vorgenommen. Es werden der DMA-Controller und die Interrupts für eine komplette DMA-Übertragung mit der Priorität Niedrig aktiviert.

DMA.CTRL = DMA_ENABLE_bm;
DMA.CH0.CTRLB = DMA_CH_TRNINTLVL_LO_gc;

Die Blockänge (hier 2 Byte) wird über das TRFCNT-Register des DMA-Kanals festgelegt und im CTRLA-Register des jeweiligen Kanals wird die Länge des Burst (hier 2 Byte) eingestellt.

DMA.CH0.TRFCNT = 2;
DMA.CH0.CTRLA = DMA_CH_BURSTLEN_2BYTE_gc;

Als nächstes wird der DMA so konfiguriert, dass die Übertragung direkt durch den ADC angestoßen wird. Dazu wird der Auslöser (hier Kanal 0 des ADCA) in das TRIGSRC-Register des DMA-Kanals geschrieben:

DMA.CH0.TRIGSRC = DMA_CH_TRIGSRC_ADCA_CH0_gc;

Als Quell- und Zieladresse werden die Datenregister des verwendeten ADC-Kanals, bzw. des verwendeten DAC-Kanals angegeben:

DMA.CH0.SRCADDR0 = ((uintptr_t)&ADCA.CH0.RES) & 0xFF;
DMA.CH0.SRCADDR1 = (((uintptr_t)&ADCA.CH0.RES) >> 0x08) & 0xFF;
DMA.CH0.SRCADDR2 = (((uintptr_t)&ADCA.CH0.RES) >> 0x0F) & 0xFF;

DMA.CH0.DESTADDR0 = ((uintptr_t)&DACB.CH0DATA) & 0xFF;
DMA.CH0.DESTADDR1 = ((uintptr_t)&DACB.CH0DATA >> 0x08) & 0xFF;
DMA.CH0.DESTADDR2 = ((uintptr_t)&DACB.CH0DATA >> 0x0F) & 0xFF;

Jetzt muss noch der Addressmodus über das ADDRCTRL-Register des DMA-Kanals konfiguriert werden. Der DMA-Controller ist in der Lage sowohl die Quell-, als auch die Zieladresse bei jedem Burst- oder Blocktransfer, bzw. jeder abgeschlossenen Übertragung zu erhöhen, bzw. zu verringern oder neu zu laden. Da in diesem Beispiel permanent aus dem Datenregister des ADC gelesen und in das Datenregister des DAC geschrieben werden soll, wird hier der Reload-Modus verwendet. Zudem muss nach jedem gesendeten Byte der Adresszeiger des DMA-Controllers sowohl für die Quell-, als auch für die Zieladresse erhöht werden.

DMA.CH0.ADDRCTRL = DMA_CH_SRCRELOAD_BURST_gc | DMA_CH_DESTRELOAD_BURST_gc | DMA_CH_SRCDIR_INC_gc | DMA_CH_DESTDIR_INC_gc ;

Jetzt müssen noch die entsprechenden Interruptlevel freigegeben und die globalen Interrupts aktiviert werden.

PMIC.CTRL = PMIC_LOLVLEN_bm;
sei();

Der DMA-Kanal wird dann durch das Setzen des ENABLE-Bits in den Bereitschaftsmodus gesetzt.

DMA.CH0.CTRLA |= DMA_CH_ENABLE_bm;

In diesem Modus wartet der DMA-Controller auf ein Triggersignal (falls einer ausgewählt worden ist, ansonsten kann es auch per Software ausgelöst werden) und sobald ein Signal kommt, wird die Übertragung gestartet.

Sobald der ADC mit der Wandlung fertig ist, wird die Übertragung mittels DMA angestoßen. Der DMA-Controller generiert nach einer erfolgreichen Übertragung einen Transaction Complete-Interrupt. In der jeweiligen ISR muss dann das Interruptflag gelöscht und der DMA-Kanal erneut aktiviert werden.

ISR(DMA_CH0_vect)
{
	cli();
	DMA.CH0.CTRLB |= DMA_CH_TRNIF_bm;
	DMA.CH0.CTRLA |= DMA_CH_ENABLE_bm;
	sei();
}

Die ISR ist das einzige mal wo die CPU aktiv werden muss. Alles andere läuft vollkommen automatisch ab.

Der DMA im Einsatz:

Zum Abschluss soll das erste Programm in der Praxis ausprobiert werden. Dafür habe ich mein XMega-A3BU Xplained verwendet, da dieses Board über einen integrierten analogen Lichtsensor verfügt und dieser direkt mit dem ADC ausgelesen werden kann. Die analoge Spannung des DAC kann dann über Pin 2 des Headers J2 abgegriffen werden. Das ganze sieht dann folgendermaßen aus…

Huch… Woher kommt den dieses seltsame Dreieckssignal in der Ausgangsspannung vom DAC?

Das abgebildete Dreieckssignal resultiert aus der Netzfrequenz (50 Hz) der Leuchtstoffröhren, die diesen Raum beleuchten. Der ADC arbeitet so schnell, dass das Glimmern als Belichtungsdifferenz in der analogen Spannung des Lichtsensors sichtbar wird.

Daten per DMA in das SRAM kopieren:

Der DMA ist zudem ein hervoragendes Werkzeug, wenn z. B. Daten vom ADC automatisch in das SRAM kopiert werden sollen. Im nächsten Beispiel zeige ich, wie die Ergebnisse von fünf ADC-Messungen automatisch in das SRAM kopiert werden können. Zuerst wird wieder der ADC konfiguriert.

ADCA.REFCTRL = ADC_REFSEL_INT1V_gc;
ADCA.PRESCALER = ADC_PRESCALER_DIV32_gc;
ADCA.CTRLA = ADC_ENABLE_bm;
PORTA.DIR &= ~(0x01 << 0x00);
ADCA.CH0.CTRL = ADC_CH_INPUTMODE0_bm;
ADCA.CH0.MUXCTRL = 0x00;

Als nächstes wird der DMA-Controller aktiviert und der Trigger für den verwendeten Kanal konfiguriert.

DMA.CTRL = DMA_ENABLE_bm;
DMA.CH0.TRIGSRC = DMA_CH_TRIGSRC_ADCA_CH0_gc;

Der Kanal soll 10 Bytes (5 ADC Ergebnisse á 2 Byte) übertragen, wobei die Burstlänge auf 2 Byte festgelegt wird.

DMA.CH0.TRFCNT = 10;
DMA.CH0.CTRLA = DMA_CH_BURSTLEN_2BYTE_gc;

Die Ausgangsadresse ist wieder das RES-Register des ADCs und die Zieladresse ist das Array für die Daten im SRAM.

DMA.CH0.SRCADDR0 = ((uintptr_t)&ADCA.CH0.RES) & 0xFF;
DMA.CH0.SRCADDR1 = (((uintptr_t)&ADCA.CH0.RES) >> 0x08) & 0xFF;
DMA.CH0.SRCADDR2 = (((uintptr_t)&ADCA.CH0.RES) >> 0x0F) & 0xFF;

DMA.CH0.DESTADDR0 = ((uintptr_t)Destination) & 0xFF;
DMA.CH0.DESTADDR1 = ((uintptr_t)Destination >> 0x08) & 0xFF;
DMA.CH0.DESTADDR2 = ((uintptr_t)Destination >> 0x0F) & 0xFF;

Für die Adressmodi werden nun folgende Einstellungen verwendet:

  • Start- und Zieladresse sollen Inkrementiert werden
  • Die Startadresse soll nach jedem Burst (also nach 2 Bytes) neu geladen werden
  • Die Zieladresse soll nach jeder Transaktion (also nach 10 Bytes) neu geladen werden
DMA.CH0.ADDRCTRL = DMA_CH_SRCRELOAD_BURST_gc | DMA_CH_DESTRELOAD_TRANSACTION_gc | DMA_CH_SRCDIR_INC_gc | DMA_CH_DESTDIR_INC_gc;

Als nächstes muss noch der sogenannte Single-Shot-Modus über das SINGLE-Bit im CTRLA-Register des DMA-Kanals aktiviert werden. Der Single-Shot-Modus bewirkt, dass der DMA-Controller bei jedem Trigger einen Bursttransfer anstatt eines Blocktransfers durchführt. Auf diese Weise werden bei jedem Triggersignal 2 Bytes (ein Burst) übertragen und nicht die kompletten 10 Bytes (ein Block) übertragen.

DMA.CH0.CTRLA |= DMA_CH_SINGLE_bm;

Nach Abschluss der Transaktion soll der Mikrocontroller wieder in die ISR des DMA-Kanals springen. Daher werden noch die Interrupts aktiviert.

DMA.CH0.CTRLB = DMA_CH_TRNINTLVL_LO_gc;
PMIC.CTRL = PMIC_LOLVLEN_bm;
sei();

Zum Schluss wird noch der DMA-Kanal aktiviert und anschließend werden fünf einzelne ADC-Messungen gestartet.

DMA.CH0.CTRLA |= DMA_CH_ENABLE_bm;
		
for(uint8_t i = 0x00; i < 5; i++)
{
	ADCA.CH0.CTRL |= ADC_CH_START_bm;
	for(uint16_t i = 0x00; i < 0xFFFF; i++);
}

Sobald alle Messungen durchgeführt worden und 10 Bytes übertragen wurden springt der DMA-Kanal in die ISR.

Das komplette Projekt findet ihr in meinem GitHub-Repository.

2 Kommentare

  1. Hallo Daniel,
    vielen Dank für diese super Seite.

    Ich habe gerade etwas mit dem ADC zu DAC DMA Beispiel rumgespielt.
    Kann es sein, dass es in der DAC Einstellung statt:
    DACB.CTRLA &= DAC_CH0EN_bm | DAC_ENABLE_bm;
    DACB.CTRLA |= DAC_CH0EN_bm | DAC_ENABLE_bm;
    heissen muss?

    Gruss
    Thore

    1. Hallo Thore,

      vielen Dank für den Hinweis. Du hast natürlich komplett recht! Da hat sich ein Schreibfehler eingeschlichen :)
      Ist aber schon korrigiert.

      Gruß
      Daniel

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert