V rámci přípravy ukázek pro předmět "Mikrokontroléry v měřicí technice" jsem sesmolil ukázkový příklad na použití DAC. Postupem času nabobtnal do takového rozměru, že by si zasloužil zveřejnění a případně posloužil začátečníkům s STM32 z řad veřejnosti.
STM32F3 Bipolární DAC s DMA
V rámci přípravy ukázek pro předmět "Mikrokontroléry v měřicí technice" jsem sesmolil ukázkový příklad na použití DAC. Postupem času nabobtnal do takového rozměru, že by si zasloužil zveřejnění a případně posloužil začátečníkům s STM32 z řad veřejnosti. Nebudu se dlouho rozkecávat a půjdu rovnou k věci.
Hrubé rysy
Cílem příkladu je seznámit čtenáře s použitím DAC ke generování "waveforem" (česky řekněme napěťových průběhů). Jako demonstrační průběh jsem konzervativně zvolil sinus, ale je na vás a vašem algoritmu jaký průběh bude generovat. Generování dvou waveforem probíhá pomocí DMA. Tento postup odlehčuje procesoru, umožňuje generovat s vyšší vzorkovací frekvencí a žere hodně RAM. Druhou možností je šetřit paměť, nepoužít DMA a dopočítávat vzorky v rutině přerušení a redukovat tak maximální vzorkovací frekvenci (ukázka této varianty je k dispozici také). Co se hardwaru týče tak SW běží na kitu STM32F303K8 Nucleo a aby to nebyla taková nuda je ukázka uzpůsobená ke generování bipolárního výstupu. K tomu slouží operační zesilovač napájený záporným napětím, které si čip vyrábí pomocí nábojové pumpy (viz schema).
Na kitu máme k dispozici 5V napětí z USB - to slouží jako kladné napájecí napětí operačního zesilovače (LM358). Ke generování záporného napětí slouží napěťová pumpa tvořená dvěma schottkyho diodami a dvojicí elektrolytických kondenzátorů. K jejímu buzení je potřeba komplementárního obdélníkového průběhu. Ty generujeme přímo na pinech procesoru. Kit má vyvedený i výstup VREF což není nic jiného než filtrované napájení DAC (tedy 3.3V). Zesilovač je v rámci jednoduchosti zapojen tak aby výstupní rozsah DAC (přibližně 0-3.3V) "překládal" na rozsah -3.3V až +3.3V. Dolní část rozsahu není možné se záporným napájecím napětím -2.7V dosáhnout. Aplikace s tím tedy musí počítat. Úprava zapojení tak aby výstup pokrýval rozsah například +-2.5V lze dosáhnout zapojením OZ jako diferenciálního zesilovače (což bude v konečném důsledku vyžadovat dva trimry).
Poznámka k DAC na čipech F303
V analogové výbavě různých čipů řady F3 je slušný bordel. Kdo čekal že alespoň všechny F303 na tom budou stejně tak se přepočítal. Například F303K6, F303K8 a F328 mají 3 DAC kanály a jeden vybavený bufferem. Ostatní F303 mají dva DAC kanály (oba s bufferem). Podobný bordel je v počtech DMA a jejich kanálech, Komparátorech a Operačních zesilovačích (ano čipy mají ve výbavě i vlastní OZ). Považuji tedy za nutné zmínit se o tom co máme na F303K8 k dispozici. Máme dva DAC převodníky. DAC1 má dva kanály a první z nich (DAC1 Ch1) je vybaven výstupním bufferem, druhý kanál není. Druhý převodník (DAC2) má jen jeden kanál (DAC2 Ch1) a nemá buffer. Se zapínáním bufferu si ještě užijeme v komentářích ke zdrojáku). Naše ukázka bude využívat "duální" režim DAC aby generovala dvě waveformy, takže použijeme DAC1, který má výstupy na PA4 (Ch1) a PA5 (CH2).
Jedovatá poznámka k DMA na čipech F303
Čipy F303K6, F303K8 a F328 mají jen jedno DMA (7 kanálů), ostatní F303 mají dvě DMA. Requesty pro DMA od vybraných periferií je možné "remapovat" (tedy přivádět na jiné kanály DMA). Když si v reference manualu k F303 prohlédnete tabulku "DMA1 request mapping" uvidíte, že request od DAC_Ch1 vede na kanál 3. No a on tam nevede Různě po datasheetu jsou rozmístěny střípky informace že request od DAC_Ch1 vede na DMA2 ... přesně na to DMA2 které není na čipu přítomno Musíte tedy v SYSCFG přemapovat tento request na DMA1.
Komentář ke zdrojovému kódu
TIM1 - signál pro nábojovou pumpu
Konfigurace TIM1 je vcelku přímočará. Chceme vytvořit obdélníkový signál (viz oscilogram na konci článku). Využijeme výstup TIM1_CH2 (PA9). U Advanced timerů (TIM1,TIM8 a TIM20) se často zapomíná, že PWM výstupy je potřeba ještě extra povolit pomocí funkce TIM_CtrlPWMOutputs()
// konfigurace komplementárních výstupů ocinit.TIM_OCMode = TIM_OCMode_PWM1; // režim PWM ocinit.TIM_OCIdleState = TIM_OCIdleState_Reset; // nehraje roli ocinit.TIM_OCNIdleState = TIM_OCNIdleState_Reset; // nehraje roli ocinit.TIM_OCPolarity = TIM_OCPolarity_High; // nehraje roli ocinit.TIM_OCNPolarity = TIM_OCNPolarity_High; ocinit.TIM_OutputState = TIM_OutputState_Enable; // zapnout výstup na PA9 ocinit.TIM_OutputNState = TIM_OutputNState_Disable;
ocinit.TIM_Pulse = 5; // 50% PWM TIM_OC2Init(TIM1, &ocinit); // aplikujeme nastavení na kanál 2 (tedy na CH2 a CH2N) TIM_CtrlPWMOutputs(TIM1,ENABLE); // povolujeme timeru ovládat výstupy (specialita Advanced timerů)
TIM_Cmd(TIM1, ENABLE); // spustíme timer }
Vytváření waveformy
Waveformy vytváří a ukládá do pole funkce "create_waveform" a lze volit počet vzorků a parametry sinusovky. Obě waveformy jsou uloženy v jednom poli ve formátu který odpovídá "duálnímu režimu" DAC s 12bit daty zarovnanými vpravo. Funkce by si zasloužila o něco chytřejší ošetření vstupních dat. Tu jsem vynechal aby nezastřela jádro věci, konec konců je k dispozici v předchozích příkladech. Pro úplnost nastíním o co jde. DAC se zapnutým bufferem nemá výstupní rozsah "rail-to-rail". Jinak řečeno není možné generovat napětí menší jak přibližně 0.1V a větší jak Vref-50mV (datasheet je mnohem konzervativnější a hovoří o 0.2V z obou stran). Funkce generující waveformu by s tím mohla počítat (zvláště když její argumenty obsahují napětí ve voltech) a upozorňovat návratovou hodnotou, že bude požadovaná waveforma zkreslená.
/* vytvoří "sinusovky" podle zadaných parametrů a uloží je ve formátu vhodném pro DAC * 1.arg - pole kam se má waveforma uložit * 2.arg - počet vzorků na jednu periodu * 3.arg - parametry waveformy na DAC Ch2 * 4.arg - parametry waveformy na DAC Ch1 */ void create_waveform(uint32_t* array, uint16_t samples, waveform* w1, waveform* w2){ float u1,u2; uint32_t dac_val1,dac_val2,i;
for(i=0;i<samples;i++){ // trocha matematiky u1 = w1->amplitude*sinf(2*PI*i/samples + w1->phase) + w1->offset; u2 = w2->amplitude*sinf(2*PI*i/samples + w2->phase) + w2->offset; dac_val1 = (uint16_t)(u1*(DAC_MAX/VREF)); dac_val2 = (uint16_t)(u2*(DAC_MAX/VREF)); // ošetřit "horní saturaci" DAC (ať pak do DAC omylem nenasypeme vyložené nesmysly) dac_val1 &= 0xfff; dac_val2 &= 0xfff; array[i]=(dac_val1<<16) | dac_val2; // DAC pak předáváme data ve formátu 12bit right aligned } }
konfigurace DAC, DMA a TIM6
Tady se skrývá jádro věci, než se k němu dostanu, upozorním vás na další malý podraz. Už jsem se zmínil že F303K8 má celkem tři DAC kanály a jen jeden z nich má buffer (DAC1_Ch1). Buffer je možné zapínat nebo vypínat. Ostatní kanály buffer nemají a uživatel je může odpojovat nebo připojovat k výstupu. K realizaci obou těchto funkcí slouží jeden a ten samý bit v registru DAC1->CR (DAC2->CR). V SPL k jeho konfiguraci slouží položka "DAC_Buffer_Switch", která může mít "hodnotu" Enable nebo Disable. Z hlediska připojení / odpojení DAC1_Ch2 (PA5) a DAC2_Ch1 (PA6) je konfigurace čitelná (Enable připojuje, Disable odpojuje). Z hlediska bufferu (na DAC1_Ch1 - PA4) je ale situace matoucí. Enable totiž buffer vypíná a Disable ho zapíná. Pro přehlednost to raději shrnu:
DAC2_OUT1 žádný buffer nemá, výstup na PA6 Switch_Disable = výstup DAC je odpojený Switch_Enable = výstup DAC je připojený
DAC1_OUT2 žádný buffer nemá, výstup na PA5 Switch_Disable = výstup DAC je odpojený Switch_Enable = výstup DAC je připojený
DAC1_OUT1 má buffer, výstup na PA4 Switch_Disable = buffer je zapnutý Switch_Enable = buffer vypnutý
Oba použité výstupy DAC (PA4 a PA5) nastavíme jako analogové. DAC převod je spouštěný (trigrovaný) TRGO signálem z TIM6 (jako TRGO volíme "update" událost TIM6). Periodu TIM6 můžeme volit a spolu s počtem vzorků waveformy určuje periodu signálu. Konfigurace DMA je nezáludná. Zdrojem dat bude pole "wave" do kterého vám už teď známá funkce create_waveform() připravila data. Přenášet budeme data pro oba kanály zároveň - tedy 32bitová slova. DAC podporuje tři přirozené formáty ukládání dat jejich nákres najdete v datasheetu pod názvem "Data registers in dual DAC channel mode". My použijeme vpravo zarovnaná 12bit data a ty je nutné zapisovat do registru DAC_DHR12RD, jehož adresa je 0x40007420. Protože chceme generovat periodický signál, zvolíme "kruhový" režim DMA a přidělíme mu patřičně vysokou prioritu. Při konfiguraci nesmíme zapomenou "remapovat" request od DAC_Ch1 (o tom už byla řeč). K tomu ale musí běžet SYSCFG, takže mu nezapomeneme zapnout clock. Requesty tedy vedou na DMA1_Channel 3. Vzhledem k tomu že převod obou kanálů probíhá zároveň stačí aby DMA requesty posílal jeden z kanálů - my jsme zvolili DAC_Ch1. Poté DAC spustíme. V tuto chvíli ještě nic neběží.
// konfigurace DMA dma.DMA_BufferSize = MAX_SAMPLES; // nemá moc smysl, stejně pak délku přenosu upravíme podle konkrétní zprávy dma.DMA_DIR = DMA_DIR_PeripheralDST; // cíl přenosu - periferie dma.DMA_M2M = DMA_M2M_Disable; // režim Memory-To-Memory nechceme dma.DMA_MemoryBaseAddr = (uint32_t)(wave); // adresa pole s daty k odeslání dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; // data jsou 32bit dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // zdrojovou adresu inkrementovat dma.DMA_Mode = DMA_Mode_Circular; // po skončení celého přenosu zahájit další (stále dokola) dma.DMA_PeripheralBaseAddr = (uint32_t)DAC_DUAL_DATA_REG; // cíl přenosu (DAC registr pro duální data 12b right aligned) dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; // data jsou 32bit dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // vše posílat na jednu adresu dma.DMA_Priority = DMA_Priority_High; // priorita relativně vysoká DMA_Init(DMA1_Channel3, &dma); // aplikovat nastavení na channel3 - tam povedou requesty od DAC1 // DMA kanál ještě nezapínám // hnusný podraz, DAC1 requesty je potřeba remapovat aby vedly do DMA1Ch3 (ačkoli jiné DMA nemáme :D ) SYSCFG_DMAChannelRemapConfig(SYSCFG_DMARemap_TIM6DAC1Ch1, ENABLE);
Před začátkem generování vytvoříme waveformu se zvolenými parametry. Námi volený počet vzorků předáme DMA. To lze provést jen s vypnutým DMA kanálem. Teprve po té povolíme DAC posílat DMA requesty a povolíme vybraný DMA kanál. Nakonec spustíme TIM6 a vše se rozběhne. Pokud bychom chtěli waveformu rekonfigurovat, musíme nejprve zastavit TIM6, tím zamezíme tomu aby DAC posílalo další requesty. Poté vypneme DMA kanál, přepíšeme v paměti waveformu a rekonfigurujeme DMA na nový počet vzorků a spustíme opět jako o pár řádků výše. A to je všechno. Kdo chce může si v DAC kontrolovat vlajku "underrun" (případně si od ní povolit přerušení). Ta signalizuje situaci kdy DAC nedostalo včas data a výstupní waveforma je zmršená. Pokud to nastane vypne se vám i náš DMA kanál a je na vašem SW aby na to nějak reagovalo. Oficielní maximum rychlosti pro DAC je 1MSPS, ale nepochybuji že pojede o poznání rychleji.
int main(void){ SystemCoreClockUpdate(); // 72MHz DBGMCU_APB1PeriphConfig(DBGMCU_TIM6_STOP, ENABLE); // při zastavení programu, zastaví i TIM6 tim1_init(); // PWM pro nábojovou pumpu dac_init(); tim6_init(); // konfigurace a tvorba waveformy (w1 nás teď "nezajímá") w2.amplitude = 1.0; // Vpp = 2V w2.offset = 1.25; // Voff = 1.25V w2.phase = 0; w1.amplitude = 1.25; // Vpp = 2.5V (na výstupu DAC) w1.offset = VREF/2; // Voff = 1.65V (Na výstupu DAC) w1.phase = PI/2; samples = 100; // 100 samples with T(tim6)=1us => T=100us (f=10kHz) create_waveform(wave,samples,&w2,&w1); // spuštění celého procesu (DMA kanál musí být v tomto okamžiku vypnutý) DMA_SetCurrDataCounter(DMA1_Channel3,samples); // nastavíme délku přenosu DMA_Cmd(DMA1_Channel3, ENABLE); // spustíme DMA DAC_DMACmd(DAC1,DAC_Channel_1,ENABLE); // povolíme DAC posílat requesty do DMA (DAC je hladové a hned jeden pošle) TIM_Cmd(TIM6, ENABLE); // spustit TIM6 - rozběh waveformy
while(1){ asm("nop"); // můžeme kontrolovat "underrun" na DAC } }
Modrý průběh označený CH2 je signál pro nábojovou pumpu. Průběh CH2N tam nepatří, dělejte že jste ho neviděli...
Doufám že ukázka alespoň některým z vás přinesla jistý vhled do základního ovládání DAC na STM32 a budu se těšit u dalších "řešených" příkladů. Celý zdrojový kód je ke stažení zde
Autorovi nezávidím. Vejforma je příšerný slovo. Česky, pokud se nepletu, se tomu říká funkce, máme generátor funkcí, že. Ovšem funkce vytvoř_funkci() na srozumitelnosti nepřidá. Napěťový průběh to taky není, když ve vytvoř_napěťový_průběh se tvoří pole bezrozměrných čísel. Průběh by asi šel. Ovšem jen pokud nebude příliš zkreslený. Pak je to průser.
Pěkný příklad. Akorát mi ušel smysl generování signálu TIM1_CH2N. Kdybych místo něj použil GND, tak to bude fungovat stejně a dokonce se sníží vnitřní odpor výstupního portu (pojede to jen přes jeden), přes který se nabíjí kondenzátor v nábojové pumpě.
Keď sa nezapojí TIM1, akože sa potom bude generovať to záporné napájacie napätie? Zapojením diódy na zem zapojenie nebude fungovať, lebo OZ nebude mať napájanie. Kmitaním na TIM1 sa síce generuje veľmi mäkké napätie, ale na demonštračné účely to bude bohate dostačovať.
Tenhle server porad nese stafetu, se kterou obetave prisel Kosta, a velice si vazim jeho prace, kterou vykonali spolu s Mardem (nejen) pro komunitu STM ARMu. Tak snad dokazeme na tuto (dnes uz vzacnou) ideu navazat.
@pako: suhlasim Fulda ma pekne veci aj preto by mali byt v clankoch, aby k nim bol pristup aj o rok, dva. To ale zalezi na nom, ak by to chcel, tak to asi davno urobi. To je asi bezne aj na inych weboch (aspon kam ja chodim). Chapem, ze tento nas redakcny system je strasny, ale skopirovat embeded code z youtube a vlozit do clanku asi nebude vyzadovat vysokoskolke vzdelanie. Na druhej strane dobra sprava, chiptron ma konecne shoubox, blahozelam. ... idem sa zaregistrovat
to joedoe: Prosim vas, zrovna Fulda ma casto zajimave prispevky, a tady uz se 14 dni web ani nehne, tak budme radi za kazdy uzitecny prispevek. Vzdyt stacilo jen pozadat autory, aby clanecek vytvorili a zpravu v chatboxu ponechat. Takhle si jen vytvarime nepratele.
@bobricius: ... asi mame rozdielny nazor na danu vec. FYI: @gripennn s tym problem nema. Fuldov prispevok nebol zmazany ale blokovany ako vas a moja sprava bola pre vas obidchoch.
Sorry ale robit clanok kvoli 3 riadkom, to nemyslite vazne. A to zmazanie Fuldoveho prispevku je uz kus vela ne? Sak jedine co na tej stranke zije je chatbox. Na novinky chodim k chiptronovi
@fulda & @bobricius: ak chcete robit odkazy na svoje aktivity, tak urobte kratky clanok a tam si dajte odkazy kam chcete. Rad vam s tym pomozem, kludne ma kontaktujte. Tento "pristup" sme tu uz riesili niekolkokrat. Dakujem
V rámci mé přirozené skromnosti vás upozorním na můj malý instructables projekt odkaz vánoční stromek s charlieplexingem. A protože jsem hodně skromnej, tak připomenu, že je součástí PCB contestu a že pro něj můžete hlasovat: odkaz
Neměl byste někdo schéma zapojení (domácí) telefonní ústředny ALPHATEL T-LINE. Po výměně akumulátoru v UPS, resp. jejím vypínaní mi asi v ústředně odešel zdroj. Děkuji.