Vaja 6: DMA

Namen tokratne vaje je, da se spoznate s konceptom neposrednega dostopa V/I naprav do pomnilnika ter prenosom med lokacijami v pomnilniku brez CPE.

Neposreden dostop do pomnilnika

Na dosedanjih vajah je v prenosu med registrom vhodno/izhodne naprave vedno posredovala centralno procesna enota. Na zadnji vaji smo na primer izvedli ukaz:

uint8_t nov_znak;
// ...
nov_znak = USART3->RDR

Iz registra RDR naprave USART3, se vsebina najprej prenese v register centralno-procesne enote, nato pa shrani v pomnilnik na lokacijo globalne spremenljivke. Temu pristopu rečemo programski vhod/izhod (z ali brez uporabe prekinitev).

Če želimo CPE razbremeniti prenašanja podatkov med registri in pomnilnikom, da v tem času lahko počne kaj drugega, lahko uporabimo pristop, ki mu rečemo neposredni dostop do pomnilnika (angl. Direct Memory Access). V tem primeru za prenose med V/I napravi in pomnilnikom skrbi ločena naprava - naprava DMA. Prenosi lahko potekajo v od V/I naprave proti pomnilniku ali obratno, kot tudi iz pomnilnika v pomnilnik (med dvemi lokacijami v pomnilniku).

Uporabnik mora za izvedbo prenosa inicializacirati DMA krmilnik. Za to je potrebno nastaviti izvorni in ciljni naslov (iz kje in kam prenašamo), število byte-ov v prenosu ter način prenosa. Ko so podatki na izvorni strani pripravljeni, DMA zaprosi CPE za prevzem vodila in s tem postane gospodar vodila. Po zaključku prenosa DMA naprava sproži prekinitev.

Za boljšo učinkovitost, DMA naprava običajno ne prenaša vsakega byte-a/worda posebej, ampak si jih shranjuje v medpomnilnik (angl. FIFO buffer), ki jih potem shrani na ciljno lokacijo v kosu.

DMA v STM32H750

V STM32H750 imamo na voljo dve DMA napravi: DMA1 in DMA2. Obe omogočota prenos med V/I napravo in pomnilnikom, med dvemi V/I napravami kot tudi prenos od pomnilnika do pomnilnika. Slika 6.1 prikazuje poenostavljeno shemo DMA naprav v kontrolerjih STM32H7. Vsaka izmed DMA naprav podpira osem tokov (angl. streams). Tok/Stream je aktiven DMA prenos med izvorom in ciljem. Izvor DMA zahtev za posamezen tok izberemo v t.i. DMAMUX bloku, kjer imamo osem 128-na-1 multiplekserjev. V STM32H750 je sicer v praksi na voljo 115 od 128ih vhodnih linij multiplekserja.

Tokove upravlja arbiter, ki na podlagi prioritete določa kateri izmed njih je v danem trenutku aktiven. Prioriteto lahko definiramo na štirih nivojih: zelo visoka (angl. very high), visoka (high), srednja (medium), nizka (low). V primeru, da sta dve aktivni zahtevi po prenosu na dveh tokovih, arbiter izbere tok z višjo prioriteto. V primeru enakih prioritet, ima prednost tok z nižko številsko oznako.

Slika 6.1: Blok diagram DMAMUX bloka in DMA naprave

Ko je V/I naprava pripravljena, pošlje DMA zahtevo v DMAMUX blok, iz katerega gre ta proti izbranemu DMA toku. Arbiter potem odloči kdaj bo izbrani DMA tok na vrsti za izvedbo prenosa.

Nastavitve prenosa

Za izvedbo DMA prenosa v STM32H7 moramo definirati sledeče nastavitve:

  • izvorni in ciljni (ponorni) naslov

Določata kje naj DMA naprava bere podatke in kam naj jih pošilja. Če prenos poteka iz V/I naprave v pomnilnik, je izvorni naslov register naprave (na primer register RDR naprave USART), ciljni pa lokacija v pomnilniku. V primeru prenosa v obratni smeri naprava bere podatek iz pomnilnika in ga pošilja v register V/I naprave. V primeru prenosa pomnilnik-pomnilnik sta oba naslova lokaciji v pomnilniku.

  • dolžina prenosa in velikost podatkov

Določamo koliko podatkov želimo prenesti in koliko je velik posamezen podatek - byte, halfword (16-bitov), word (32-bitov). Velikost podatkov definiramo tako za izvorno kot ciljno stran.

  • inkrementiranje naslovov

Za oba naslova lahko izberemo, da po uspešno zaključenem prenosu podatka DMA naprava avtomatsko poveča izvorni ali ciljni naslov. To je uporabno v primeru, da pošiljamo ali shranjujemo podatek v polju.

  • izbrani tok/stream

Izberemo enega od sedmih tokov, ki ga želimo uporabiti.

  • smer in naprava, ki bo zahtevala prenos

Smer je lahko od V/I naprave do pomnilnika, od pomnilnika do V/I naprave in pomnilnik-pomnilnik. V primeru, da bo DMA zahteve prožila V/I naprava, je to napravo potrebno tudi določiti.

  • način prenosa

Imamo dve možnosti prenosa: normalen/običajen (angl. normal) in krožni (angl. circular). Pri normalnem prenosu se prenos zaključi ko se prenese izbrano število podatkov. Pri krožnem pa

  • prioriteta

Določimo pomembnost toka. Kot že rečeno, imamo štiri nivoje prioritete: zelo visoka (angl. very high), visoka (high), srednja (medium), nizka (low).

  • uporaba FIFO medpomnilnika (angl. FIFO buffer)

FIFO buffer ima prostora za 4 32-bitne podatke. Če buffer uporabimo, je potrebno nastaviti tudi pri koliko podatkih v buffer-ju ga izpraznimo (pošljemo podatke na cilj). Lahko ga praznimo pri 1/4, 1/2, 3/4 polnem bufferju ali šele ko je buffer povsem poln.

Programski vmesnik za DMA prenose

Kot vedno do sedaj je prvi korak vklop ure izbrane DMA naprave s klicem makroja __HAL_RCC_DMAx_CLK_ENABLE(). Sledi inicializacija DMA naprave in izbrani DMA stream. Za to uporabimo strukturo DMA_HandleTypeDef, pri katerem sta pomembna elementa Instance, v katerem izberemo DMA napravo in stream. Možnosti so: DMA1_Stream0, DMA1_Stream1, DMA1_Stream2, DMA1_Stream3, DMA1_Stream4, DMA1_Stream5, DMA1_Stream6, DMA1_Stream7, DMA2_Stream0, DMA2_Stream1, DMA2_Stream2, DMA2_Stream3, DMA2_Stream4, DMA2_Stream5, DMA2_Stream6, DMA2_Stream7.

Drugi element strukture je Init, v katerem določimo nastavitve prenosa:

  • Request: pri prenosih pomnilnik-pomnilnik izberemo DMA_REQUEST_MEM2MEM. V primeru prenosa med V/I napravo in pomnilnikom v katerokoli smer, pa določimo napravo, ki bo prožila zahteve. Primera: DMA_REQUEST_USART1_RX - zahteve se bodo prožile ob prejetih podatkih na napravi USART1, DMA_REQUEST_USART1_TX - zahteve se bodo prožile vsakič ko bo naprava USART1 pripravljena na prenos.

  • Direction: določimo smer prenosa. Možne nastavitve: DMA_PERIPH_TO_MEMORY, DMA_MEMORY_TO_PERIPH, DMA_MEMORY_TO_MEMORY.

  • PeriphInc: Vklopimo/izklopimo povečevanje naslova na strani periferne V/I naprave: DMA_PINC_ENABLE, DMA_PINC_DISABLE. V primeru prenosa pomnilnik-pomnilnik se ta nastavitev uporabi za ciljni naslov.

  • MemInc: Vklopimo/izklopimo povečevanje naslova na strani pomnilnika: DMA_MINC_ENABLE, DMA_MINC_DISABLE. V primeru prenosa pomnilnik-pomnilnik se ta nastavitev uporabi za izvorni naslov.

  • PeriphDataAlignment, MemDataAlignment: Velikost posameznega podatka: DMA_MDATAALIGN_BYTE, DMA_MDATAALIGN_HALFWORD, DMA_MDATAALIGN_WORD.

  • Mode: Izberemo način prenosa: DMA_NORMAL, DMA_CIRCULAR.

  • Priority: Prioriteta toka: DMA_PRIORITY_LOW, DMA_PRIORITY_MEDIUM, DMA_PRIORITY_HIGH, DMA_PRIORITY_VERY_HIGH.

  • FIFOMode: Vklopimo/izklopimo uporabo FIFO medpomnilnika: DMA_FIFOMODE_ENABLE, DMA_FIFOMODE_DISABLE.

  • FIFOThreshold: Meja pri kateri se FIFO buffer izprazni - DMA_FIFO_THRESHOLD_1QUARTERFULL, DMA_FIFO_THRESHOLD_HALFFULL, DMA_FIFO_THRESHOLD_3QUARTERSFULL, DMA_FIFO_THRESHOLD_FULL.

Za zagon prenosa uporabimo funkcijo HAL_DMA_Start.

Primer prenosa pomnilnik - pomnilnik

Primer brez uporabe prekinitev:

__HAL_RCC_DMA1_CLK_ENABLE();

DMA_HandleTypeDef dma1_struct = {0};

dma1_struct.Instance = DMA1_Stream0;
dma1_struct.Init.Request = DMA_REQUEST_MEM2MEM;
dma1_struct.Init.Direction = DMA_MEMORY_TO_MEMORY;
dma1_struct.Init.PeriphInc = DMA_PINC_ENABLE;
dma1_struct.Init.MemInc = DMA_MINC_ENABLE;
dma1_struct.Init.PeriphDataAlignment = DMA_MDATAALIGN_WORD;
dma1_struct.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
dma1_struct.Init.Mode = DMA_NORMAL;
dma1_struct.Init.Priority = DMA_PRIORITY_LOW;
dma1_struct.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
dma1_struct.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_1QUARTERFULL;
HAL_DMA_Init(&dma1_struct);

// source in destination definiramo kot polji dolzine SIZE
// uint32_t source[SIZE];
// uint32_t destination[SIZE];
// SIZE * 4, ker je dolžina prenosa podana v bytih
HAL_DMA_Start(&dma1_struct, (uint32_t)source, (uint32_t)destination, SIZE * 4);

Po zagonu DMA prenosa lahko na konec prenosa počakamo s funkcijo HAL_DMA_PollForTransfer:

// čakamo dokler se prenos ne konča
HAL_DMA_PollForTransfer(&dma1_struct, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

Obstaja tudi možnost uporabe prekinitev. V tem primeru DMA stream ob zaključku prenosa sproži prekinitev.

Naloga 6.1

Izmerite čas prenosa 32kB in 50kB podatkov med dvemi lokacijami v pomnilniku z zanko in DMA prenosom. Vse štiri izmerjene čase (32kB zanka, 50kB zanka, 32kB DMA, 50kB DMA) zapišite v komentar programa. Za merjenje časa uporabite spodaj pripravljene funkcije. Merjenje časa ponovite večkrat in zapišite povprečje.

Merjenje časa

Spodnje funkcije vam omogočajo merjenje časa v mikrosekundah. Za delovanje je potrebno odkomentirati HAL_TIM_MODULE_ENABLED v stm32h7xx_hal_conf.h.

Uporaba spodnjih funkcij:

start_timer();
HAL_Delay(200);
stop_timer();
// izmeriti bi morali okoli 200 000 mikrosekund
// (zaradi nenatancosti HAL_Delay jih boste izmerili nekaj vec)
uint32_t izmerjen_cas_v_us = get_time();
void start_timer() {
  __HAL_RCC_TIM2_CLK_ENABLE();
  TIM_HandleTypeDef timer = {0};
  timer.Instance = TIM2;
  timer.Init.CounterMode = TIM_COUNTERMODE_UP;
  timer.Init.Period = 100000000;
  timer.Init.Prescaler = 64 -1;
  HAL_TIM_Base_Init(&timer);

  HAL_TIM_Base_Start(&timer);
  __HAL_TIM_SetCounter(&timer, 0);
}

void stop_timer() {
  TIM_HandleTypeDef timer = {0};
  timer.Instance = TIM2;
  HAL_TIM_Base_Stop(&timer);
}

uint32_t get_time() {
  TIM_HandleTypeDef timer = {0};
  timer.Instance = TIM2;
  return __HAL_TIM_GetCounter(&timer);
}

Dodatno - Primer z uporabo prekinitev:

DMA prenos z uporabo prekinitev:

// definirajte kot globalno spremenljivko
DMA_HandleTypeDef dma1_struct = {0};

__HAL_RCC_DMA1_CLK_ENABLE();

dma1_struct.Instance = DMA1_Stream0;
dma1_struct.Init.Request = DMA_REQUEST_MEM2MEM;
dma1_struct.Init.Direction = DMA_MEMORY_TO_MEMORY;
dma1_struct.Init.PeriphInc = DMA_PINC_ENABLE;
dma1_struct.Init.MemInc = DMA_MINC_ENABLE;
dma1_struct.Init.PeriphDataAlignment = DMA_MDATAALIGN_WORD;
dma1_struct.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
dma1_struct.Init.Mode = DMA_NORMAL;
dma1_struct.Init.Priority = DMA_PRIORITY_LOW;
dma1_struct.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
dma1_struct.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_1QUARTERFULL;
HAL_DMA_Init(&dma1_struct);

// vsak stream ima svojo prekinitveno oznako
HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 10, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream0_IRQn);

// source in destination definiramo kot polji dolzine SIZE
// uint32_t source[SIZE];
// uint32_t destination[SIZE];
HAL_DMA_Start_IT(&dma1_struct, (uint32_t)source, (uint32_t)destination, SIZE);

// ...
// ...

void DMA1_Stream0_IRQHandler(void) {
  // dma1_struct je struktura tipa DMA_HandleTypeDef
  // HAL_DMA_IRQHandler pobriše zastavice in nastavi
  // element State glede na prebrane zastavice 
    HAL_DMA_IRQHandler(&dma1_struct);
  // preverimo da se je prenos uspešno zaključil
  if (dma1_struct.State == HAL_DMA_STATE_READY) {
    // ...
  }
}