Vaja 7: 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->RDRIz 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 8.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.

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 osmih 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 tok/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 izberemoDMA_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 napraviUSART1,DMA_REQUEST_USART1_TX- zahteve se bodo prožile vsakič ko bo napravaUSART1pripravljena 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 (s FIFO bufferjem in brez). Vseh šest izmerjenih časov (32kB zanka, 50kB zanka, 32kB DMA brez FIFO, 50kB DMA brez FIFO, 32kB DMA s FIFO, 50kB DMA s FIFO) 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 na nivoju mikrosekund 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) {
// ...
}
}