niedziela, 18 czerwca 2017

[13] STM32F4 - Cube Mx - DAC

Ten post chciałbym poświęcić na omówienie sposobu uruchomienia przetwornika cyfrowo analogowego DAC w mikrokontrolerach STM32F4.

[Źródło: http://www.st.com/en/evaluation-tools/stm32f4discovery.html]

Cube Mx:


Aby uruchomić układ w podstawowej konfiguracji należy przygotować część w następujący sposób.

Uruchomienie DAC'a jest dosyć proste:


W tym przypadku wykorzystuje jedno wyjście, do generacji sygnału. 


Kolejnym elementem jest włączenie zegarów. Podobnie jak w poprzednich projektach uruchamiam zegar z maksymalną częstotliwością taktowania. Dodatkowym elementem jest włączenie RNG, które posłuży przy generacji szumu. Jego częstotliwość będzie wynosiła 42 MHz.


Włączenie RNG wykonuje się poprzez odpowiednie wybranie opcji w zakładce pinout:



Kolejnym elementem jaki powinien zostać uruchomiony jest jeden z timerów. Wykorzystam timer 6, który posłuży jako sygnał wyzwalania dla DAC'a.


Timer ustawiony został na zliczanie w górę do momentu jego przepełnienia. W celu zwiększania częstotliwości sygnału należy modyfikować parametry dzielnika oraz okresu dla timera.


Dodatkowo w celu debugu oraz do przesyłania informacji o sygnale należało by uruchomić jeden z układów USART. Do tego celu wybrałem USART2 w trybie asynchonicznym. Domyślnie sygnał TX umieszczony jest na pinie PA2, RX natomiast na pinie PA3.



Uruchomienie DAC'a będzie zależne od wybranej konfiguracji.

Podstawowa generacja:


Generacja sygnału trójkątnego:


Generacja sygnału trójkątnego z DMA:



Sygnał sinus DMA:


Dla szumu:



Podstawowy sposób generacji:


Podstawowy sposób generacji sygnału DAC jest dosyć przydatny gdy chcemy wygenerować sygnał o odpowiedniej wartości, który można wykorzystać do sterowania np. przetwornicą LM2596.

Włączenie DACa odbywa się w następujący sposób:

  1.   HAL_DAC_Start(&hdac,DAC_CHANNEL_1);

Jako parametr podaje się wskaźnik do zdefiniowanego DACa oraz nr wykorzystywanego kanału.

Ustawianie danych odbywa się poprzez funkcje SetValue:

  1.     HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,1365);

Jako argumenty wprowadza się wskaźnik do struktury z danymi do konwertera. Jako drugi argument podaje się numer kanału do którego będzie wprowadzona wartość. Następnie podawana jest format danych, w tym wypadku 12 bitowy. Ostatnim elementem jest dana jaka zostanie wpisana. Obliczanie wartości napięcia odbywa się na postawie tego wzoru:

Wartość Wyjściowa [V] = Wartość referencyjna [V] * (Wartość wpisana [])/4095

Wartość 4095 oznacza wybraną rozdzielczość, w Cube wybrana została 12 bitowa.

Przykładowa funkcja ustawiająca wartości może wyglądać następująco:

  1. void TestValuesDac(void)
  2. {
  3.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x000);
  4.         HAL_Delay(10);
  5.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x300);
  6.         HAL_Delay(10);
  7.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x600);
  8.         HAL_Delay(10);
  9.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x900);
  10.         HAL_Delay(10);
  11.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0xB00);
  12.         HAL_Delay(10);
  13.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0xD00);
  14.         HAL_Delay(10);
  15.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0xFFF);
  16.         HAL_Delay(10);
  17.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0xD00);
  18.         HAL_Delay(10);
  19.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0xB00);
  20.         HAL_Delay(10);
  21.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x900);
  22.         HAL_Delay(10);
  23.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x600);
  24.         HAL_Delay(10);
  25.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)0x300);
  26.         HAL_Delay(10);
  27. }

lub w bardziej skróconej wersji:

  1. void TestValuesDac(void)
  2. {
  3.     for(uint16_t i=0; i<0xFFF; i+=0x100){
  4.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)i);
  5.         HAL_Delay(10);
  6.     }
  7.    
  8.     for(uint16_t i=0xFFF; i>0x000; i-=0x100){
  9.         HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,(uint32_t)i);
  10.         HAL_Delay(10);
  11.     }
  12. }

Generacja sygnału trójkątnego:

Kolejnym sygnałem jaki można wygenerować jest sygnał trójkątny. Do jego wyzwalania wykorzystywany jest jeden z timerów, w moim przypadku 6. Maksymalna amplituda wynosi 4095. Nie ma potrzeby dodatkowego definiowania próbek, wystarczy uruchomić samego DACa oraz timer. Po tych dwóch liniach kodu przebieg zaczyna być generowany na wyjściu 1.

  1.     HAL_TIM_Base_Start(&htim6);    
  2.     HAL_DAC_Start(&hdac,DAC_CHANNEL_1);

Generacja sygnału trójkątnego z wykorzystaniem DMA:

Innym, dosyć dobrym, sposobem jest wykorzystanie DMA, które pozwala na przesyłanie danych na wyjście bez udziału głównej części mikrokontrolera. Pozwoli to na jego odciążenie, oraz wykonywanie innych operacji gdy dane na wyjściu są ustawiane.

Przy generacji sygnału trójkątnego przez DMA należy zdefiniować odpowiednie próbki, które będą wgrywane na wyprowadzenie.

  1. uint16_t Triangle[32] ={
  2.     0, 256, 512, 768, 1024, 1279, 1535, 1791,
  3.     2047, 2303, 2559, 2815, 3071, 3326, 3582, 3838,
  4.     4095, 3838, 3582, 3326, 3071, 2815, 2559, 2303,
  5.     2047, 1791, 1535, 1279, 1024, 768, 512, 256
  6. };

W zależności od wymaganej dokładności przebiegu można zwiększać lub zmniejszyć przez dobranie ilości próbek. Oczywiście ilość danych musi zostać zapisana na maksymalnie 12 bitach.

Po zdefiniowaniu próbek, uruchamia się timer, DACa po czym podaje się bufor oraz ilość danych do wygenerowania sygnału.

  1. HAL_TIM_Base_Start(&htim6);
  2. HAL_DAC_Start (&hdac, DAC_CHANNEL_1);
  3. HAL_DAC_Start_DMA (&hdac, DAC_CHANNEL_1,(uint32_t*)Triangle,32,DAC_ALIGN_12B_R);

Generacja sygnału sinusoidalnego z wykorzystaniem DMA:


W tym przypadku postępuje się właściwie identycznie. Okres sygnału modyfikuje się poprzez zmianę parametrów timera. Dodatkowo przedstawię w jaki sposób pobrać wartości sygnału do bufora i wyświetlić przez USART.

  1. const uint16_t Sinus2[128] ={
  2.    2048, 2145, 2242, 2339, 2435, 2530, 2624, 2717, 2808, 2897,
  3.    2984, 3069, 3151, 3230, 3307, 3381, 3451, 3518, 3581, 3640,
  4.    3696, 3748, 3795, 3838, 3877, 3911, 3941, 3966, 3986, 4002,
  5.    4013, 4019, 4020, 4016, 4008, 3995, 3977, 3954, 3926, 3894,
  6.    3858, 3817, 3772, 3722, 3669, 3611, 3550, 3485, 3416, 3344,
  7.    3269, 3191, 3110, 3027, 2941, 2853, 2763, 2671, 2578, 2483,
  8.    2387, 2291, 2194, 2096, 1999, 1901, 1804, 1708, 1612, 1517,
  9.    1424, 1332, 1242, 1154, 1068, 985, 904, 826, 751, 679,
  10.    610, 545, 484, 426, 373, 323, 278, 237, 201, 169,
  11.    141, 118, 100, 87, 79, 75, 76, 82, 93, 109,
  12.    129, 154, 184, 218, 257, 300, 347, 399, 455, 514,
  13.    577, 644, 714, 788, 865, 944, 1026, 1111, 1198, 1287,
  14.    1378, 1471, 1565, 1660, 1756, 1853, 1950, 2047
  15. };

Generacja sygnału wygląda właściwie tak samo jak dla sygnału trójkątnego:

  1. HAL_TIM_Base_Start(&htim6);
  2. HAL_DAC_Start_DMA (&hdac, DAC_CHANNEL_1,(uint32_t*)Sinus2,128,DAC_ALIGN_12B_R);

Wartości sygnałów można obliczyć posługując się ustawieniami timera (tim 6 taktowany z APB1) oraz taktowaniem całego systemu.

W związku z tym, że timer 6 taktowany jest z APB1, co  oznacza, że wartość taktowania wynosi 42MHz. Ponieważ zastosowano minimalny dopuszczalny dzielnik dla tej linii, czyli 4.

Do obliczania danych należy wykorzystać rejestr DWT oraz SCB.

  1. #define    DWT_CYCCNT       *(volatile unsigned long *)0xE0001004
  2. #define    DWT_CTRL         *(volatile unsigned long *)0xE0001000
  3. #define    SCB_DEMCR        *(volatile unsigned long *)0xE000EDFC

Potrzebne będą adresy następujących rejestrów SCB_DEMCR, DWT_CYCCNT oraz DWT_CRTL.

Prędkość zegara systemowego można pobrać z funkcji, która właściwie tylko zwraca tą wartość lub pobrać ją bezpośrednio ze zmiennej zdefiniowanej w pliku system_stm32f4xx.c:

  1. uint32_t systemFrequency = HAL_RCC_GetHCLKFreq();
  2. //lub
  3. uint32_t systemFrequency = SystemCoreClock;

Uruchomienie timera następuje w następujący sposób:

  1. //Reset
  2. SCB_DEMCR   |= 0x01000000;
  3. DWT_CYCCNT   = 0;   //Wyzerowanie licznika
  4. DWT_CTRL     = 0;   //Wyzerowanie rejestru kontrolnego
  5. //------------------------
  6. DWT_CTRL    |= 1;           //Włączenie licznika
  7. DWT_CYCCNT   = 0;           //Wyzerowanie licznika

Obliczenia wykonuje się w następujący sposób:

  1. char buffer[30];
  2. uint32_t clkCounter = 0;
  3. uint8_t calculateSize = 0;
  4. sprintf((char*)buffer,"%u\r\n",clkCounter);
  5. for(uint8_t i = 0; i<30;i++){
  6.     if(buffer[i] == '\n'){
  7.         calculateSize = i;
  8.         break;
  9. }
  10. HAL_UART_Transmit(&huart2,(uint8_t*)buffer,calculateSize,0xFFFF);
  11. sprintf((char*)str,"%u\r\n",(clk_counter*1000)/(systemFrequency/1000));
  12. for(uint8_t i = 0; i<30;i++){
  13.     if(buffer[i] == '\n'){
  14.         calculateSize = i;
  15.         break;
  16. }
  17. HAL_UART_Transmit(&huart2,(uint8_t*)buffer,calculateSize,0xFFFF);
  18. sprintf((char*)str,"%u\r\n",systemFrequency/clkCounter);
  19. for(uint8_t i = 0; i<30;i++){
  20.     if(buffer[i] == '\n'){
  21.         calculateSize = i;
  22.         break;
  23. }
  24. HAL_UART_Transmit(&huart2,(uint8_t*)buffer,calculateSize,0xFFFF);

Dodatkowo zmienne trzeba uwzględnić w obsłudze przerwania, może to być przerwanie od DMA, gdzie uaktualniana będzie wartość zliczająca cykle zegara oraz resetowany będzie licznik DWT.

  1. void DMA1_Stream5_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN DMA1_Stream5_IRQn 0 */
  4.   /* USER CODE END DMA1_Stream5_IRQn 0 */
  5.   clkCounter=DWT_CYCCNT;
  6.   DWT_CYCCNT=0;
  7.   HAL_DMA_IRQHandler(&hdma_dac1);
  8.   /* USER CODE BEGIN DMA1_Stream5_IRQn 1 */
  9.   /* USER CODE END DMA1_Stream5_IRQn 1 */
  10. }

Generacja szumu:


Szum generuje się w standardowy sposób. Aby uzyskać pewną przypadkowość dobrze jest zastosować generator liczb losowych, który jest dostępny w mikrokontrolerze STM32F4.

Na samym początku włączany jest timer 6 w trybie przerwań. Do tego celu służy funkcja HAL_TIM_Base_Start_IT.

Dzięki temu przy odpowiednim ustawieniu timera oraz generowaniu liczb losowych na 12 bitach można uzyskać sygnał szumu poprzez podawanie liczb losowych w procedurze obsługi przerwania.

  1. //W main
  2. HAL_TIM_Base_Start_IT(&htim6);
  3. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim6) {
  4.    uint16_t valueToWrite = (HAL_RNG_GetRandomNumber(&hrng))&0x00000FFF;
  5.    HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R,valueToWrite);
  6. }

Innym sposobem jest najzwyczajniejsze włączenie generacji szumu z wykorzystaniem DMA, bądź włączenie samego układu odpowiedzialnego za generację szumu.

Generację na drugim kanale wykonuje się analogicznie do pierwszego, oba mogą działać w tym samym czasie i generować różne przebiegi.

Bibliografia:


[1] STM32 DAC