STM32F0 DMA

Direct Memory Acces česky "Přímý přístup do paměti" je geniální funkce umožňující přenášet data v rámci paměti (a všech periferií) bez intervence jádra. Je tak mocná, že bude prosakovat do všech dílů tutoriálu. Činnost DMA je v principu velice jednoduchá. Když to řeknu hodně zjednodušeně tak stačí nastavit odkud kam se mají data přenášet a kdo má dávat pokyny k přenosu (tzv requesty). DMA je užitečné pro periferie které potřebují pravidelné krmení daty (DA převodník, SPI, USART) a pro periferie které chrlí data (AD převodník, Timery, SPI, USART). Tím užitečnější čím větší je datový tok. Asi stěží by jste bez DMA zvládli sbírat z AD převodníku data s tempem 2MSPS, protože při 48MHz by vám na to zbývalo pouhých 24 strojových cyklů. A bůh s vámi pokud by jste k tomu chtěli využít přerušení, neboť vstup do rutiny přerušení spolkne 16 strojových cyklů. Ne jinak na tom bude DA převodník, který je za vhodných okolností schopný převádět až 5MSPS. A stejně tak by bez DMA vaše aplikace zařvala při pokusu vyždímat z SPI deklarovaných 18Mb/s. Kde lze DMA použít ale nejlépe zjistíte z příkladů. Než se k nim ale dostaneme mohli bychom si ve stručnosti projít možnosti DMA na čipech STM32F0.

Přenášet data lze z periferie do paměti (např AD převodník do RAM), z paměti do periferie (např z Flash do DA převodníku), z periferie do periferie (např z Timeru na SPI) a z paměti do paměti. Podle verze čipu máte k dispozici 7-12 kanálů. Lze tedy přenášet data mezi 7-12 vybranými místy a každý přenos může být řízen jiným requestem (tedy různými událostmi). Protože ale přenos probíhá přes jednu datovou sběrnici, lze v jednom okamžiku přenášet data pouze jednoho kanálu. Aby bylo možné některé kanály upřednostnit, jsou k dispozici čtyři úrovně priorit. Přenos může probíhat stále dokola nebo se zastavit po přenesení všech dat, kterých může být 1-65535. Během přenosu je možné sledovat kolik dat už je přeneseno a při přenesení jedné poloviny nebo všech dat lze navíc volat přerušení. Schopnosti DMA na výkonnějších čipech (F3,F4) tím zdaleka nekončí, ale my to nebudeme přehánět a budeme si hrát s tím co umí DMA na F0.

No a jako obvykle nebudeme zbytečně tlachat a mrkneme se jak se DMA konfiguruje. Nejprve vyřídíme otázku requestů. Request je požadavek na který DMA reaguje provedením jednoho přenosu ze zdrojové adresy do cílové adresy. Seznam requestů pro každý ze sedmi kanálů najdete v následující tabulce (věnujte pozornost tomu pro které čipy tabulka platí).


Tak například kanál 1 může přijímat requesty od ADC, TIM2_CH3 (Kanál3 Timeru 2), TIM17_CH1 a TIM17_UP (Update - přetečení Timeru 17). O tom který z těchto zdrojů bude posílat DMA kanálu 1 requesty rozhodujete vy tím, že v příslušné periferii povolíte generování requestů (detaily kdy request vzniká záleží na konfiguraci příslušné periferie). V jednom okamžiku smí být povolen pouze jeden zdroj pro jeden kanál. Jestliže tedy povolíte vytváření requestů pro kanál 1 jak ADC tak TIM2_CH3 tak si následky ponesete sami... Všimněte si poznámky (1) a (2). DMA requesty je možné určitým způsobem remapovat. Pro ujasnění ještě jeden příklad. Jestliže chci přenášet data na request od USART2_RX (tedy v reakci na přijatá data na USARTu) musím pro tento přenos použít kanál 5.

Když už máte kanál vybraný je čas ho konfigurovat. Jako obvykle to lze dělat pomocí struktury - LL_DMA_InitTypeDef. Většina položek struktury je samovysvětlující, my se nejprve vypořádáme s adresami. Adresy jsou dvě. Při přenosu mezi periferií a pamětí se jedná o adresu registru v periferii a o adresu proměnné nebo pole v paměti. Při přenosu mezi pamětí a pamětí (Tzv. M2M)) se pak adresy jmenují zdrojová (Source) a cílová (Destination). Adresa může být buď fixní (to je typicky v periferii) a nebo se může s každým přenosem inkrementovat (to je typicky v paměti). Což je naprosto logické, protože jestliže chcete postupně ukládat nebo číst pole z paměti, musí se adresa měnit tak aby polem procházela. Přenášet můžete 3 datové typy (Byte, Half-Word a Word). Podle toho který zvolíte se pak adresa inkrementuje o 1,2 nebo 4. Každé straně (cílové i zdrojové) je tedy nutné specifikovat tři položky. Pro zdrojovou adresu jsou to tyto:

A pro cílovou adresu jsou položky ekvivalentní

Zřídka se vám stát, že na straně periferie a na straně paměti nebudou stejné datové typy. V takovém případě DMA provede "ořezání" dat nebo doplnění nulami, podle toho jestli při přenosu konvertujete větší datový typ na menší nebo opačně. Přehlednou tabulku jak to probíhá najdete v datasheetu (zveřejňovat ji nebudu, protože tyto situace nastávají jen zřídka). Výkonnější čipy (F3,F4 atd.) obsahují v DMA paměť, která umožňuje provádět konverzi dat bez ořezání a ztrát (je pak možné například Word rozložit na dva po sobě jdoucí přenosy Bytu).

Když je vyřešena otázka adres a velikosti dat, je potřeba DMA říct kolik dat se bude přenášet. Na to je ve struktuře položka NbData. Ta může nabývat hodnot 0-65535. Pokud DMA není v tzv "kruhovém" (circular) módu, tak běží jen jednorázově a po přenesení specifikovaného množství dat se kanál vypne (a případně u toho zavolá přerušení). O použitím módu se rozhoduje v položce Mode. Nakonec zbývá specifikovat pouze prioritu ( položka Priority) a režim přenosu (periferie-paměť nebo paměť-paměť). Což řídí položka Direction. Strukturu pak stačí předhodit funkci LL_DMA_Init(). Upozornit vás ale musím že konfiguraci lze provádět pouze na vypnutém kanále ! Z toho logicky plyne, že po samotné konfiguraci je kanál ještě potřeba zapnout. Teprve pak DMA začne obsluhovat requesty a přenášet data. S pomocí funkce LL_DMA_GetDataLength() můžete sledovat kolik dat ještě zbývá přenést. Managment vlajek a přerušení od DMA si předvedeme v některé z ukázek. Závěrem ještě musím říct, že pokud pokazíte konfiguraci adres a DMA kanál se po spuštění pokusí zapsat do zakázaných oblastí paměti, skončí celá akce vypnutím kanálu a nastavením Error vlajky.

Seznam příkladů

Protože se DMA prolíná s periferiemi, bude většina příkladů rozpuštěná do ostatních tutoriálů. Aby jste je nemuseli složitě hledat, máte zde seznam s odkazy na příslušné ukázky.

7A - DMA Paměť->Paměť (M2M)

Kopírování mezi dvěma paměťovými buňkami (M2M - Memory To Memory) asi často nevyužijete, ale snadno se na takovém příkladu demonstruje používání a konfigurace DMA. Kdyby někdo z vás během své praxe narazil na nějaké uplatnění "M2M" režimu, dejte mi vědět a rád tento příklad doplním o motivaci z praxe. My si demonstračně zkusíme zkopírovat pole uint8 o velikosti 2048B umístěné ve Flash a pak i v RAM do jiného pole uint16 (v RAM). Zda je zdrojové pole v RAM či Flash se řídí pouze kvalifikátorem "const". Ve zdrojovém kódu tedy stačí přehodit komentář a můžete testovat přenos Flash->RAM nebo RAM->RAM. Aby to nebyla úplná nuda, zkusíme si u toho tupým způsobem změřit jak dlouho přenos trvá a pak si zkusíme zkopírovat pole "ručně" pomocí "forcyklu" ať můžeme rychlosti porovnat. Tupý způsob měření spočívá v tom, že před spuštěním přenosu zapneme timer a po skončení přenosu ho vypneme a podíváme se do kolika napočítal. Využijeme k tomu TIM6, protože se moc nepoužívá, zapadá prachem a potřebuje se protáhnout. Protože jsem se o konfiguraci DMA rozkecal už dříve, projdeme si zdrojový kód ukázky jen stručně.

Celý zdrojový kód ukázky

int main(void){
init_clock(); // 48MHz (HSE bypass)
DMA_init(); // konfiguruje DMA Channel1 (Mem2Mem)

// Využijeme TIM6 k orientačnímu měření doby přenosu
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6); // clock pro TIM6
LL_TIM_SetPrescaler(TIM6,47); // 1MHz clock pro TIM6
LL_TIM_GenerateEvent_UPDATE(TIM6); // vyvolat přepsání Prescaleru - pro jistotu
LL_TIM_SetCounter(TIM6,0);// vymazat counter
LL_TIM_EnableCounter(TIM6); // zahájit měření času
// u Mem2Mem se zahájí přenos okamžitě s povolením kanálu
LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1); // povolme kanál
while(!LL_DMA_IsActiveFlag_TC1(DMA1)){};
LL_TIM_DisableCounter(TIM6); // ukončit měření času
time_dma=LL_TIM_GetCounter(TIM6); // zjistit přibližnou dobu trvání přenosu
// jsme čistotní a uklidíme po sobě vlajky
LL_DMA_ClearFlag_GI1(DMA1);
LL_DMA_ClearFlag_HT1(DMA1);
LL_DMA_ClearFlag_TC1(DMA1);
// a nebo úklid provedeme elegantněji pomocí CMSIS
// DMA1->IFCR = DMA_IFCR_CGIF1 | DMA_IFCR_CHTIF1 | DMA_IFCR_CTCIF1;

// pro sorvnání ještě kopírování pomocí smyčky
LL_TIM_SetCounter(TIM6,0);
LL_TIM_EnableCounter(TIM6); // zahájit měření času
for(i=0;i<sizeof(buffer8);i++){
 buffer16[i]=buffer8[i];
}
LL_TIM_DisableCounter(TIM6); // ukončit měření času
time_cyklus=LL_TIM_GetCounter(TIM6); // zjistit přibližnou dobu trvání přenosu

 while (1){
  // není co dělat...
 }
}

void DMA_init(void){
LL_DMA_InitTypeDef dma;
// clock pro DMA
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
// přenos mezi pamětí a pamětí
dma.Direction = LL_DMA_DIRECTION_MEMORY_TO_MEMORY;
dma.MemoryOrM2MDstAddress = (uint32_t)buffer16; // cílová adresa
dma.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_HALFWORD; // cílový datový typ
dma.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; // inkrementovat cílovou adresu
dma.Mode = LL_DMA_MODE_NORMAL; // normální mód (ne kruhový)
dma.NbData = sizeof(buffer8); // počet přenášených dat
dma.PeriphOrM2MSrcAddress = (uint32_t)buffer8; // zdrojová adresa
dma.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE; // zdrojový datový typ
dma.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_INCREMENT; // inkrementovat zdrojovou adresu
dma.Priority = LL_DMA_PRIORITY_MEDIUM; // priorita přenosu střední
LL_DMA_Init(DMA1,LL_DMA_CHANNEL_1,&dma); // aplikovat konfiguraci na Channel1
// kanál ještě nespouštíme
}

Všimněte si že kopírujeme pole uint8 do pole uint16, je potřeba tedy příslušným způsobem zvolit "DataSize". Obě adresy jsou v paměti, takže logicky se musí obě inkrementovat. Celkový počet přenášených dat je 2048, cílové pole tedy musí mít 2048 nebo více položek. Prioritu kanálu jsme dali "Medium", ale momentálně to asi nemá žádný význam. Všimněte si že po skončení konfigurace ještě kanál nepovoluji. Přenos "M2M" nevyžaduje žádné vnější requesty, takže jakmile kanál povolíte okamžitě se zahájí kopírování. V tomto režimu také není možné zapínat kruhový (Circular) mód. Což je logické, k čemu by to asi bylo ? Dokončení přenosu poznáme podle vlajky TC1 (Transfer Complete channel 1). Spolu s touto vlajkou se nastaví také vlajka HT1 (Half Transfer channel 1) a GI1 (Global interrupt channel 1). Všechny vlajky po sobě uklidíme. Na čemž bude krásně vidět jak se tři funkce z LL knihoven dají jednoduše zamáznout jedním příkazem když se využije registrový přístup (CMSIS). Když budete ukázku zkoušet nezapomeňte si zapnout optimalizaci. Jinak dopadne "forcyklus" ve výkonových testech příšerně. Výsledky mého malého srovnání předkládám níže v tabulce.

typ přenosuDMA"forcyklus"
Flash->RAM~278us (7MB/s)~683us (2.86MB/s)
RAM->RAM~214us (9.13MB/s)~640us (3MB/s)

odkazy:

Home
V0.5 8.8.2017
By Michal Dudka (m.dudka@seznam.cz)