W tym poście chciałbym przedstawić sposób generacji sygnałów za pomocą DAC oraz DMA.
Za pomocą konwertera cyfrowo analogowego można wygenerować sygnał prostokątny, piłokształtny trójkątny oraz sinusoidalny.
Sposób działania
Poniżej przedstawiam schemat blokowy pojedynczego konwertera cyfrowo analogowego. Dla przypomnienia w STM32 znajdują się dwa takie układy.
Rys. 1. Schemat DAC
VDDA - jest to napięcie wejściowe, zasilanie analogowe części układu
VSSA - wejściowe, uziemienie części analogowej
VREF+ - napięcie referencyjne dodatnie z zakresi od 1,8V do VDDA
Przed konfiguracją należy ustawić piny wyjściowe jako analogowe. Spowodowane jest to tym, że DAC po odpowiedniej konfiguracji generuje sygnał na wybranym pinie.
Sama konwersja odbywa się w części znajdującej się na samym dole rysunku. Działa ona we współpracy z liniami zasilającymi oraz referencyjnymi mikrokontrolera. Dane wyjściowe dostępne są na pinie oznaczonym jako DAC_OUTx. Natomiast dane wejściowe podawana są do 32-bitowego rejestru przechowującego DHR. Z niego przesyłane są do rejestru DOR gdzie zostają bezpośrednio zamienione na wartości analogowe. Dostęp uzyskuje się do 12 bitów DAC_DHR12R1, DAC_DHR12R2 odpowiednio dla DAC1 oraz dla DAC2.
Dane nie mogą być bezpośrednio przesłane do DOR z pominięciem DHR.
VSSA - wejściowe, uziemienie części analogowej
VREF+ - napięcie referencyjne dodatnie z zakresi od 1,8V do VDDA
Przed konfiguracją należy ustawić piny wyjściowe jako analogowe. Spowodowane jest to tym, że DAC po odpowiedniej konfiguracji generuje sygnał na wybranym pinie.
Sama konwersja odbywa się w części znajdującej się na samym dole rysunku. Działa ona we współpracy z liniami zasilającymi oraz referencyjnymi mikrokontrolera. Dane wyjściowe dostępne są na pinie oznaczonym jako DAC_OUTx. Natomiast dane wejściowe podawana są do 32-bitowego rejestru przechowującego DHR. Z niego przesyłane są do rejestru DOR gdzie zostają bezpośrednio zamienione na wartości analogowe. Dostęp uzyskuje się do 12 bitów DAC_DHR12R1, DAC_DHR12R2 odpowiednio dla DAC1 oraz dla DAC2.
Dane nie mogą być bezpośrednio przesłane do DOR z pominięciem DHR.
Transfer danych pomiędzy DHR a DOR może przebiegać automatycznie lub poprzez użycie wyzwalania programowego SWTRIGx, sprzętowego TIMx_TRGO bądź z przerwania EXTI_9. Wybór definiowany jest przez rejestr TSELx[2:0].
Dodatkowe układy sprzętowe są umieszczone w jednostce kontrolnej, takie jak generator szumu lub sygnału trójkątnego. Informacje o ich użyciu są dodawane do rejestru DHR.
Bity rozmieszczone pomiędzy rejestrami kontrolnym, sygnału programowego oraz statusu definiują sposoby działania DAC.
Istnieje też możliwość transferu danych za pomocą DMA.
Programowanie
W związku z tym, że przetwornik jest 12 bitowy, dobrze jest sygnał sinusoidany generować w zakresie od 0 do 4095, ze środkiem znajdującym się pomiędzy tymi wartościami. Aby tego dokonać należy stworzyć tablice z zdefiniowanymi próbkami sygnałów.
- //Przebieg sinusoidalny 32 próbki
- uint32_t Sinus[32] =
- {
- 2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056,
- 4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447,
- 2047, 1647, 1263, 909, 599, 344, 155, 38,
- 0, 38, 155, 344, 599, 909, 1263, 1647
- };
- //Przebieg sinusoidalny 128 próbek
- const uint32_t Sinus2[128] =
- {
- 2048, 2145, 2242, 2339, 2435, 2530, 2624, 2717, 2808, 2897,
- 2984, 3069, 3151, 3230, 3307, 3381, 3451, 3518, 3581, 3640,
- 3696, 3748, 3795, 3838, 3877, 3911, 3941, 3966, 3986, 4002,
- 4013, 4019, 4020, 4016, 4008, 3995, 3977, 3954, 3926, 3894,
- 3858, 3817, 3772, 3722, 3669, 3611, 3550, 3485, 3416, 3344,
- 3269, 3191, 3110, 3027, 2941, 2853, 2763, 2671, 2578, 2483,
- 2387, 2291, 2194, 2096, 1999, 1901, 1804, 1708, 1612, 1517,
- 1424, 1332, 1242, 1154, 1068, 985, 904, 826, 751, 679,
- 610, 545, 484, 426, 373, 323, 278, 237, 201, 169,
- 141, 118, 100, 87, 79, 75, 76, 82, 93, 109,
- 129, 154, 184, 218, 257, 300, 347, 399, 455, 514,
- 577, 644, 714, 788, 865, 944, 1026, 1111, 1198, 1287,
- 1378, 1471, 1565, 1660, 1756, 1853, 1950, 2047
- };
- //Przebieg trojkatny
- uint32_t Triangle[32] =
- {
- 0, 256, 512, 768, 1024, 1279, 1535, 1791,
- 2047, 2303, 2559, 2815, 3071, 3326, 3582, 3838,
- 4095, 3838, 3582, 3326, 3071, 2815, 2559, 2303,
- 2047, 1791, 1535, 1279, 1024, 768, 512, 256
- };
- //Przebieg piloksztaltny
- uint32_t Sawtooth[32] =
- {
- 0, 132, 264, 396, 528, 660, 792, 924,
- 1057, 1189, 1321, 1453, 1585, 1717, 1849, 1981,
- 2113, 2245, 2377, 2509, 2641, 2773, 2905, 3037,
- 3170, 3302, 3434, 3566, 3698, 3830, 3962, 4095
- };
- //Przebieg prostokatny
- uint16_t Square[2] =
- {
- 0, 4095
- };
Kolejnym elementem jest odpowiednie odpowiednie włączenie DAC-a, timera oraz DMA. Jeśli chodzi o liczniki, to do dyspozycji są TIM2, TIM3, TIM4, TIM6, TIM7, TIM15. Dodatkowo wyzwalanie może nastąpić programowo bądź z przerwania EXTI_9.
Rys. 1. Sposoby wyzwalania DAC
Poniżej przedstawiam funkcje inicjujące pracę poszczególnych układów.
Kolejnym elementem jest konfiguracja timera:
Do deklaracji DMA trzeba znać deklarację odpowiednich rejestrów dla DAC, które przechowują dane. Ich deklaracje są następujące:
Pozwala to na odwołania do rejestru kontrolnego, przechowującego dane, wyjściowego bądź rejestru przechowującego status pracy konwertera.
Następnym elementem w kolejce jest odpowiednie ustawienie portów GPIO, DAC oraz DMA.
- //Deklaracja timera dla DAC1 PA4
- static void TIMConfig_DAC1(void)
- {
- TIM_TimeBaseInitTypeDef TimerInitStructure;
- //Wlaczenie zegara dla timera 6
- RCC_APB1PeriphClockCmd(Timer_Clock1, ENABLE);
- TIM_TimeBaseStructInit(&TimerInitStructure);
- //Ustawienie okresu jako:
- //zegar (APB1 Tim6/ilosc probek)*czestotliwosc sygnalu
- TimerInitStructure.TIM_Period = (uint16_t)(TIM_APB_DAC1/Num_Samp_DAC1)*Signal_Freq_DAC1;
- TimerInitStructure.TIM_Prescaler = 0;
- TimerInitStructure.TIM_ClockDivision = 0;
- //Zliczanie w gore
- TimerInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
- TIM_TimeBaseInit(Tim_Number1, &TimerInitStructure);
- TIM_SelectOutputTrigger(Tim_Number1, TIM_TRGOSource_Update);
- TIM_Cmd(Tim_Number1, ENABLE);
- }
- //Deklaracja timera dla DAC2 PA5
- static void TIMConfig_DAC2(void)
- {
- TIM_TimeBaseInitTypeDef TimerInitStructure;
- //Wlaczenie zegara dla wybranego timera
- RCC_APB1PeriphClockCmd(Timer_Clock2, ENABLE);
- TIM_TimeBaseStructInit(&TimerInitStructure);
- //Ustawienie okresu jako:
- //zegar (APB1 Tim6/ilosc probek)*czestotliwosc sygnalu
- TimerInitStructure.TIM_Period = (uint16_t)(TIM_APB_DAC2/Num_Samp_DAC2)*Signal_Freq_DAC2;
- TimerInitStructure.TIM_Prescaler = 0;
- TimerInitStructure.TIM_ClockDivision = 0;
- //Zliczanie w gore
- TimerInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
- TIM_TimeBaseInit(Tim_Number2, &TimerInitStructure);
- TIM_SelectOutputTrigger(Tim_Number2, TIM_TRGOSource_Update);
- TIM_Cmd(Tim_Number2, ENABLE);
- }
Do deklaracji DMA trzeba znać deklarację odpowiednich rejestrów dla DAC, które przechowują dane. Ich deklaracje są następujące:
- DAC->CR = 0x00010001 // Control Register
- DAC->SWTRIGR // SoftWare TRIGger Register
- DAC->DHR12R1 // Data Holding Register, 12bit, Right aligned, ch1
- DAC->DHR12L1 // Data Holding Register, 12bit, Left aligned, ch1
- DAC->DHR8R1 // Data Holding Register, 8bit, Right aligned, ch1
- DAC->DHR12R2 // Data Holding Register, 12bit, Right aligned, ch2
- DAC->DHR12L2 // Data Holding Register, 12bit, Left aligned, ch2
- DAC->DHR8R2 // Data Holding Register, 8bit, Right aligned, ch2
- DAC->DHR12RD // Data Holding Register, 12bit, Right aligned, Dual
- DAC->DHR12LD // Data Holding Register, 12bit, Left aligned, Dual
- DAC->DHR8RD // Data Holding Register, 8bit, Right aligned, Dual
- DAC->DOR1 // Data Output Register, ch1, read only
- DAC->DOR2 // Data Output Register, ch2, read only
- DAC->SR // Status Register
Pozwala to na odwołania do rejestru kontrolnego, przechowującego dane, wyjściowego bądź rejestru przechowującego status pracy konwertera.
Następnym elementem w kolejce jest odpowiednie ustawienie portów GPIO, DAC oraz DMA.
- //Wlaczenie DAC1
- void DAC1Init()
- {
- DAC_InitTypeDef DACInit;
- GPIO_InitTypeDef GPIOInit;
- DMA_InitTypeDef DMAInit;
- //Wlaczenie zegara dla portu GPIOA
- //RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
- //Wlaczenie zegara dla DAC
- //RCC->APB1ENR |= RCC_APB1ENR_DACEN;
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
- //Wlaczenie zegara dla DMA
- //RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
- //Pozostale ustawienia wyprowadzen GPIO
- GPIOInit.GPIO_Pin = GPIO_Pin_4;
- GPIOInit.GPIO_Mode = GPIO_Mode_AN;
- GPIOInit.GPIO_OType = GPIO_OType_PP;
- GPIOInit.GPIO_PuPd = GPIO_PuPd_NOPULL;
- GPIOInit.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOA, &GPIOInit);
- //Ustawienie opcji dla DAC
- //Sygnal wyzwalajacy
- DACInit.DAC_Trigger = DAC1_Trigger;
- //Generacja sygnalu wylaczona
- DACInit.DAC_WaveGeneration = DAC_WaveGeneration_None;
- //Bufor wyjsciowy wlaczony
- DACInit.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
- DAC_Init(DAC_Channel_1, &DACInit);
- DMA_DeInit(DMA1_Stream5);
- DMAInit.DMA_Channel = DMA_Channel_7;
- //Data holding register, 12 bit, right aligned ch1
- DMAInit.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1;
- //Jaki sygnal bedzie generowany
- DMAInit.DMA_Memory0BaseAddr = (uint32_t)Signal_DAC1;
- //Jak dane beda generowane
- DMAInit.DMA_DIR = DMA_DIR_MemoryToPeripheral;
- //Liczba próbek sygnaBu
- DMAInit.DMA_BufferSize = Num_Samp_DAC1;
- //Wylaczony automatyczny zwiekszanie adresu
- DMAInit.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
- //Wylaczony transfer z pamieci do pamieci
- DMAInit.DMA_MemoryInc = DMA_MemoryInc_Enable;
- //Rozmiar przeslanych danych 16b
- DMAInit.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
- //Rozmiar danych po stronie pamieci
- DMAInit.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
- //Ciagly tryb pracy
- DMAInit.DMA_Mode = DMA_Mode_Circular;
- //Priprytet wysoki
- DMAInit.DMA_Priority = DMA_Priority_High;
- //Tryb fifo wylaczony
- DMAInit.DMA_FIFOMode = DMA_FIFOMode_Disable;
- DMAInit.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
- //Pojedyncze wyzwalanie
- DMAInit.DMA_MemoryBurst = DMA_MemoryBurst_Single;
- DMAInit.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
- //Wylaczenie DMA
- DMA_DeInit(DMA1_Stream5);
- //Inicjacja i wlaczenie DMA
- DMA_Init(DMA1_Stream5, &DMAInit);
- DMA_Cmd(DMA1_Stream5, ENABLE);
- //Wlaczenie DAC-a
- DAC_Cmd(DAC_Channel_1, ENABLE);
- DAC_DMACmd(DAC_Channel_1, ENABLE);
- //Inicjacja i wlaczenie wybranego pinu DAC
- DAC_Init(DAC_Channel_1, &DACInit);
- //Wlaczenie kanalu pierwszego
- //DAC->CR |= DAC_CR_EN1;
- DAC_Cmd(DAC_Channel_1, ENABLE);
- }
- //Wlaczenie DAC2
- void DAC2Init(void)
- {
- DAC_InitTypeDef DACInit;
- GPIO_InitTypeDef GPIOInit;
- DMA_InitTypeDef DMAInit;
- //Wlaczenie zegara dla portu GPIOA
- //RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
- //Wlaczenie zegara dla DAC
- //RCC->APB1ENR |= RCC_APB1ENR_DACEN;
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
- //Wlaczenie zegara dla DMA
- //RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
- //Pozostale ustawienia wyprowadzen GPIO
- GPIOInit.GPIO_Pin = GPIO_Pin_5;
- GPIOInit.GPIO_Mode = GPIO_Mode_AN;
- GPIOInit.GPIO_OType = GPIO_OType_PP;
- GPIOInit.GPIO_PuPd = GPIO_PuPd_NOPULL;
- GPIOInit.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOA, &GPIOInit);
- //Ustawienie opcji dla DAC
- //Sygnal wyzwalajacy
- DACInit.DAC_Trigger = DAC1_Trigger;
- //Generacja sygnalu wylaczona
- DACInit.DAC_WaveGeneration = DAC_WaveGeneration_None;
- //Bufor wyjsciowy wlaczony
- DACInit.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
- DAC_Init(DAC_Channel_2, &DACInit);
- DMA_DeInit(DMA1_Stream6);
- DMAInit.DMA_Channel = DMA_Channel_7;
- //Data holding register, 12 bit, right aligned ch1
- DMAInit.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R2;
- //Jaki sygnal bedzie generowany
- DMAInit.DMA_Memory0BaseAddr = (uint32_t)Signal_DAC2;
- //Jak dane beda generowane
- DMAInit.DMA_DIR = DMA_DIR_MemoryToPeripheral;
- //Liczba próbek sygnaBu
- DMAInit.DMA_BufferSize = Num_Samp_DAC2;
- //Wylaczony automatyczny zwiekszanie adresu
- DMAInit.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
- //Wylaczony transfer z pamieci do pamieci
- DMAInit.DMA_MemoryInc = DMA_MemoryInc_Enable;
- //Rozmiar przeslanych danych 16b
- DMAInit.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
- //Rozmiar danych po stronie pamieci
- DMAInit.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
- //Ciagly tryb pracy
- DMAInit.DMA_Mode = DMA_Mode_Circular;
- //Priprytet wysoki
- DMAInit.DMA_Priority = DMA_Priority_High;
- //Tryb fifo wylaczony
- DMAInit.DMA_FIFOMode = DMA_FIFOMode_Disable;
- DMAInit.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
- //Pojedyncze wyzwalanie
- DMAInit.DMA_MemoryBurst = DMA_MemoryBurst_Single;
- DMAInit.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
- //Wylaczenie DMA
- DMA_DeInit(DMA1_Stream6);
- //Inicjacja i wlaczenie DMA
- DMA_Init(DMA1_Stream6, &DMAInit);
- DMA_Cmd(DMA1_Stream6, ENABLE);
- //Wlaczenie DAC-a
- DAC_Cmd(DAC_Channel_2, ENABLE);
- DAC_DMACmd(DAC_Channel_2, ENABLE);
- //Inicjacja i wlaczenie wybranego pinu DAC
- DAC_Init(DAC_Channel_2, &DACInit);
- //Wlaczenie kanalu pierwszego
- //DAC->CR |= DAC_CR_EN2;
- DAC_Cmd(DAC_Channel_2, ENABLE);
- }
Odpowiednie sygnały będą generowane w zależności od ustawienia wartości początkowej w polach define.
- //Timer DAC1
- #define Tim_Number1 TIM6
- #define Timer_Clock1 RCC_APB1Periph_TIM6
- #define TIM_APB_DAC1 42000000
- #define Num_Samp_DAC1 32
- #define Signal_Freq_DAC1 5000
- #define DAC1_Trigger DAC_Trigger_T6_TRGO
- //Definicja generowanego sygnalu:
- //Do wyboru Sinus, Sinus2, Triangle, Sawtooth, Square
- #define Signal_DAC1 Sinus
- //Timer DAC2
- #define Tim_Number2 TIM5
- #define Timer_Clock2 RCC_APB1Periph_TIM5
- #define TIM_APB_DAC2 42000000
- #define Num_Samp_DAC2 32
- #define Signal_Freq_DAC2 5000
- #define DAC2_Trigger DAC_Trigger_T5_TRGO
- //Definicja generowanego sygnalu:
- //Do wyboru Sinus, Sinus2, Triangle, Sawtooth, Square
- #define Signal_DAC2 Sinus
Główna funkcja programu polega na wywołaniu funkcji inicjujących z odpowiednimi parametrami dobranymi na samym początku programu.
Bibliografia
[1] RM0090 - Reference manual