Novinka:STM32 Křížem krážem IV
(Kategorie: STM32)
Napsal gripennn
09.03.2018 00:00

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).


obr1. Schema zaponení. DAC_OUT (PA4), VREF (3.3V), nábojová pumpa PA9-


Hardware

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()


// 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í 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ěží.


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.pd-nb.cz/avr.html

By Michal Dudka (m.dudka@seznam.cz)




Tato novinka je z -MCU-mikroelektronika
( http://mcu.cz/news.php?extend.4033 )