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 DMAV 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é rysyCí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). ![]() obr1. Schema zaponení. DAC_OUT (PA4), VREF (3.3V), nábojová pumpa PA9- HardwareNa 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 F303V 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 Komentář ke zdrojovému kóduTIM1 - signál pro nábojovou pumpuKonfigurace 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() // Generuje komplementární signál pro nábojovou pumpu void tim1_init(void){ GPIO_InitTypeDef gp; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef ocinit; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); // TIM1_CH2 (PA9) a TIM1_CH2N (PA12) gp.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_12; gp.GPIO_Mode = GPIO_Mode_AF; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_NOPULL; gp.GPIO_Speed = GPIO_Speed_Level_3; GPIO_Init(GPIOA, &gp); // mapujeme piny (AF) k TIM1 GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_6); tim.TIM_Prescaler = 71; // 72MHz redukujeme na 1MHz tim.TIM_Period = 9; // 100kHz na nábojovou pumpu stačí tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &tim); // 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í waveformyWaveformy 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 TIM6Tady 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:
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ěží. void dac_init(void){ GPIO_InitTypeDef gp; DAC_InitTypeDef dac; DMA_InitTypeDef dma; // zapnout GPIOA, DAC1, SYSCFG a DMA1 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // kvůli remapu DMA requestů RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA4 (DAC1_OUT1) - analog, PA5 (DAC1_OUT2) - analog gp.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; gp.GPIO_Mode = GPIO_Mode_AN; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_NOPULL; gp.GPIO_Speed = GPIO_Speed_Level_1; GPIO_Init(GPIOA, &gp); // konfigurace DAC1_ch1 (s bufferem) dac.DAC_Buffer_Switch = DAC_BufferSwitch_Disable; // buffer zapnutý (přemosťovací switch vypnutý) dac.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; // má vliv jen pro "WaveGeneration" dac.DAC_Trigger = DAC_Trigger_T6_TRGO; // trigger z TIM6 dac.DAC_WaveGeneration = DAC_WaveGeneration_None; // WaveGeneration mód nepoužíváme DAC_Init(DAC1,DAC_Channel_1,&dac); // konfigurujeme DAC1_ch1 // konfigurace DAC1_ch2 (bez bufferu) dac.DAC_Buffer_Switch = DAC_BufferSwitch_Enable; // výstup zapnutý dac.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; dac.DAC_Trigger = DAC_Trigger_T6_TRGO; // trigger z TIM6 dac.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_Init(DAC1,DAC_Channel_2,&dac); // konfigurujeme DAC1_ch2 // 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); // spustit DAC DAC_Cmd(DAC1,DAC_Channel_1,ENABLE); DAC_Cmd(DAC1,DAC_Channel_2,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 PS: Naučte mě se vkládat soubory ke stažení ... Rozcestník na další díly seriálu naleznete na www.elektromys.eu/avr.htmlBy Michal Dudka (m.dudka@seznam.cz) |