Konwerter cyfrowo-analogowy – podstawy DAC
Bardzo często przy pracy z mikrokontrolerami mamy do czynienia z konwerterem analogowo-cyfrowym (ADC). Sięgamy po niego, gdy potrzebujemy zmierzyć napięcie na baterii czy wykonać pomiar przy użyciu czujnika z wyjściem analogowym. Rzadziej używanym układem peryferyjnym jest konwerter przetwarzający dane cyfrowe na wyjście analogowe. Jednak DAC również często przydaje się w robotyce i może sprawdzić się np. do generowania dźwięków.
W artykule postaram się przybliżyć możliwości i zasadę działania konwertera DAC wbudowanego w mikrokontrolery STM32. W tej części zajmiemy się podstawami DAC – trybem polling i generowaniem za jego pomocą sygnału sinusoidalnego oraz generowaniem szumu i sygnału trójkątnego za pomocą wbudowanych funkcji Noise i Triangular.
Konwerter DAC
Jak już wspomniałem, konwerter DAC to konwerter cyfrowo-analogowy (Digital to Analog Converter), którego zadaniem jest generowanie na wyjściu sygnału analogowego o poziomie napięcia odpowiadającemu podanej przez nas wartości cyfrowej. Jest więc odwrotnością przetwornika ADC.
W mikrokontrolerach STM32 dostępny jest najczęściej jeden lub dwa przetworniki DAC, przy czym każdy z nich może mieć do trzech kanałów. Mikrokontroler STM32L47RG, którego będę używał (płytka Nucleo-L476RG), wyposażony jest w jeden przetwornik DAC z dwoma kanałami.
Najpierw przyjrzymy się jak zbudowany jest konwerter DAC w STM32.
Zacznijmy od tego, w jaki sposób obliczyć wartość, jaką powinniśmy wpisać do rejestru, aby otrzymać żądane napięcie na wyjściu. Opisuje to poniższy wzór, gdzie DOR to wartość wpisana do rejestru danych, a Vref jest napięciem odniesienia.
Przetwornik DAC w STM32 jest 12-bitowy, ale dane możemy przechowywać w rejestrach w różnych sposób. STM32 oferuje trzy sposoby wyrównania danych:
- 8-bitowe z wyrównaniem do prawej
- 12-bitowe z wyrównaniem do lewej
- 12-bitowe z wyrównaniem do prawej
Jak dane umieszczane są w rejestrach, przedstawione zostało na poniższej grafice.
Każdy z kanałów może mieć różne tryby przełączania danych (triggering). Podstawowy tryb to przełączanie programowe, czyli ręczne wysyłanie kolejnych danych na wyjście przez programistę poprzez wpisanie odpowiedniego bitu w rejestrze. Dostępne jest też przełączanie na zdarzenie zewnętrzne EXTI, które polega na przesyłaniu kolejnych danych na wyjście na podstawie zewnętrznego źródła. Najczęściej używanym trybem jest zastosowanie timera. Każdy z kanałów może być przełączany przez jeden z kilku dostępnych timerów połączonych z konwerterem DAC.
Kolejnym ważnym elementem konwertera DAC jest możliwość połączenia go z kontrolerem DMA (Direct Memory Access). Daje to duże możliwości w przypadku ciągłego generowania danych na wyjściu DAC (np. w przypadku odtwarzania dźwięku), aby odciążyć procesor.
Konwerter DAC ma też możliwość generowania za pomocą wbudowanych funkcji szumu oraz sygnału prostokątnego. Szczegółowy opis, na czym polegają te tryby, przedstawię w dalszej części artykułu.
Ciekawym zastosowaniem DAC może być wykorzystanie go jako źródła dla wewnętrznego układu peryferyjnego mikrokontrolera, jak np. komparator. Możliwe jest to dzięki temu, że wyjście możemy skonfigurować w jednym z dwóch trybów – będzie podłączone do zewnętrznego pinu lub do wewnętrznego pinu innego peryferia.
Konwerter DAC ma też możliwość zastosowania funkcji Sample and Hold. W tym trybie rdzeń przetwornika cyfrowo-analogowego konwertuje dane podczas wyzwalanej konwersji, a następnie utrzymuje przekonwertowane napięcie na kondensatorze. Umożliwia to
zmniejszenie całkowitego zużycia energii.
Ważnym elementem, który należy wziąć pod uwagę w trakcie konfigurowania konwertera DAC, jest buforowanie wyjścia. Aby sterować zewnętrznymi obciążeniami bez użycia zewnętrznego wzmacniacza, kanały DAC mają wbudowane bufory wyjściowe, które można włączać i wyłączać w zależności od aplikacji użytkownika. Gdy wyjście DAC nie jest buforowane, a w obwodzie aplikacji użytkownika występuje obciążenie, napięcie wyjściowe jest niższe niż pożądane napięcie, ze względu na znaczną impedancję wyjściową DAC. Pokazuje to poniższy schemat (na górze wyjście bez buforu, na dole z zastosowaniem buforu).
Buforowanie może być wykorzystane z fabryczną kalibracją offsetu napięcia lub z kalibracją wykonaną przez użytkownika. Szczegóły odnośnie procedury kalibracji można znaleźć w dokumentacji (Reference Manual) mikrokontrolera.
Tryb polling i generowanie sinusoidy
Podstawowym trybem pracy konwertera DAC dostępnym w ramach bibliotek HAL jest tryb Polling. Polega on na wpisywaniu kolejnych danych do przetwornika DAC, a następnie przełączanie ich na wyjście. Aby zaprezentować sposób działania trybu Polling, wykonamy prosty projekt generujący na wyjściu sygnał sinusoidalny.
Konfiguracja mikrokontrolera
Konfiguracja w przypadku trybu polling jest bardzo prosta. Wybieramy DAC i ustawiamy kanał 1 jako „Only to external pin”. Następnie przechodzimy do konfiguracji kanału. Wybieramy wyjście z buforowaniem oraz przełączanie jako „None”, aby każde wpisanie danych do rejestru spowodowało wystawienie jej na wyjściu DAC. User Trimming pozostawiamy jako ustawienia fabryczne. Nie będziemy wykorzystywali trybu Sample and Hold.
Wyjście kanału DAC1_OUT1 wyprowadzone jest na pinie PA4.
Na płytce Nucleo kanał DAC_OUT1 dostępny jest na złączu CN7 lub CN8 jako A2.
Implementacja
Sinusoidę będziemy generowali przy pomocy 100 próbek na okres.
#define NBR_OF_SAMPLES 100
Wartości dla każdej próbki wygenerujemy raz przed pętlą while(1), aby nie marnować taktów procesora na niepotrzebne ciągłe obliczanie wartości sinusa. Wykorzystam do tego poniższy wzór.
Kod będzie wyglądał następująco.
uint16_t buffer[NBR_OF_SAMPLES];
uint16_t sin_amp = 0x0FFF;
uint32_t i = 0;
for(i = 0; i < NBR_OF_SAMPLES; i++)
{
buffer[i] = ((sin_amp + 1)/2) * sin((2 * M_PI * i)/NBR_OF_SAMPLES + 1) + (sin_amp + 1)/2;
}
Teraz wystarczy, że co 1 ms podamy kolejną wartość na DAC z bufora.
uint32_t time = HAL_GetTick();
uint32_t max_time = 1;
i = 0;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if((HAL_GetTick() - time) >= max_time)
{
time = HAL_GetTick();
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, buffer[i]);
i++;
if(i >= 100)
{
i = 0;
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
W ten sposób otrzymamy wykres, jak na grafice poniżej.
Modyfikując wartość zmiennej sin_amp, możemy sterować wartością amplitudy.
Uzyskaliśmy w ten sposób sinusoidę o częstotliwości 10 Hz. Chcąc uzyskać wyższe częstotliwości, powinniśmy częściej przełączać kolejne dane na wyjściu DAC. Najlepszym rozwiązaniem byłoby zastosowanie timera, DMA i sprzętowego przełączania wyjścia DAC. Przykład takiej konfiguracji przedstawię w kolejnym artykule.
Generowanie szumu i sygnału trójkątnego
Ciekawym elementem przetwornika DAC jest możliwość generowania szumu oraz sygnału trójkątnego za pomocą wbudowanych funkcji.
Generator szumu wykorzystuje wartość wpisaną do rejestru danych konwertera DAC oraz dedykowany algorytm i na jego podstawie generuje pseudolosowy szum na wyjściu. Dodatkowo możemy zmieniać kształt szumu poprzez maskowanie danych za pomocą pojedynczych bitów.
Sygnał trójkątny generowany jest poprzez inkrementację i dekrementację licznika. Parametrami, jakie możemy modyfikować, jest amplituda (różnica między minimalną i maksymalną wartością) oraz offset (przesunięcie względem 0). Offset możemy ustawić, podając wartość do rejestru danych przed wywołaniem funkcji start.
Konfiguracja mikrokontrolera
Do generowania szumu oraz sygnału trójkątnego wykorzystamy dwa wyjścia konwertera DAC oraz przełączanie programowe. Wybieramy zatem DAC i ustawiamy oba wyjścia jako „Only to external pin”. Następnie przechodzimy do konfiguracji poszczególnych kanałów.
Kanał 1 konfigurujemy jako wyjście buforowane, z przełączaniem programowym. Jako tryb generowania wybieramy „Triangle wave generation”, a amplitudę jako maksymalną, czyli 4095. W przypadku kanału 2 wybieramy wyjście buforowane, z przełączaniem programowym. Jako tryb generowania wybieramy „Noise wave generation” z maską „Unmask DAC channel LFSR bits[11:0]”. Pełna konfiguracja widoczna jest na grafice poniżej.
Wyjścia kanału DAC1_OUT1 oraz DAC1_OUT2 wyprowadzone są na pinach PA4 oraz PA5.
Na płytce Nucleo kanał DAC_OUT1 dostępny jest na złączu CN7 lub CN8 jako A2, a kanał DAC_OUT2 na CN10 lub CN5 jako D13. Pin PA5 domyślnie skonfigurowany jest jako wyjście i podłączony do diody LD2. Należy go przekonfigurować, jeżeli chcemy używać pinu jako wyjście DAC.
Implementacja
Obsługa z poziomu kodu jest dość prosta. Na początku możemy ustawić wartości rejestrów danych dla obu kanałów. W przypadku sygnału trójkątnego oznacza on offset. Dla szumu będzie to wartość początkowa dla algorytmu pseudolosowego.
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0x0000);
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, 0x0FFF);
Następnie włączamy generowanie sygnałów.
HAL_DACEx_TriangleWaveGenerate(&hdac1, DAC_CHANNEL_1, DAC_TRIANGLEAMPLITUDE_4095);
HAL_DACEx_NoiseWaveGenerate(&hdac1, DAC_CHANNEL_2, DAC_LFSRUNMASK_BITS11_0);
Na koniec uruchamiamy kanały konwertera.
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
HAL_DAC_Start(&hdac1, DAC_CHANNEL_2);
Ponieważ wykorzystujemy przełączanie programowe, musimy co jakiś czas ustawić bit SWTRIG1 oraz SWTRIG2 rejestru SWTRIG. Przy pomocy HAL Library moglibyśmy zrobić to za pomocą funkcji HAL_DAC_Start(), jednak dla serii L4 biblioteka zawiera błąd, który powoduje, że bit nie jest prawidłowo ustawiany. Zrobię to zatem przy pomocy makra SET_BIT(). Przełączać będziemy co 1 ms, czyli z częstotliwością 1000 Hz.
uint32_t time = HAL_GetTick();
uint32_t max_time = 1;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if((HAL_GetTick() - time) >= max_time)
{
time = HAL_GetTick();
SET_BIT(hdac1.Instance->SWTRIGR, DAC_SWTRIGR_SWTRIG1);
SET_BIT(hdac1.Instance->SWTRIGR, DAC_SWTRIGR_SWTRIG2);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
W ten sposób uzyskamy na wyjściach sygnały przedstawione na poniższych wykresach.
Zmieniając parametry możemy sterować kształtem szumu. Dla wartości 0x03FF w rejestrze danych uzyskamy przebieg jak poniżej.
W przypadku sygnału trójkątnego ustawiając wartość powyżej 0 możemy sterować przesunięciem względem 0 (offset), zaś zmieniając amplitudę, sterujemy różnicą między wartością minimalną a maksymalną. Przykładowy wykres dla wartości offsetu 0x0800 oraz amplitudy 2047.
Należy pamiętać, że jeśli amplituda powiększona o offset przekroczy wartość maksymalną 4095, wykres będzie zniekształcony.
Podsumowanie
Konwerter DAC jest ciekawym układem peryferyjnym i znakomicie sprawdzi się, gdy chcemy wygenerować dowolny przebieg na wyjściu mikrokontrolera. Może być również wykorzystany jako wyjście audio do generowania dźwięków. Dzisiaj przedstawiłem podstawy używania przetwornika DAC oraz podstawowe funkcje dostępne w STM32. W kolejnym artykule postaram się przybliżyć bardziej zaawansowane zastosowanie DAC w połączeniu z DMA i układem czasowym.
Chciałbym jeszcze zapytać jak w prosty sposób można manipulować częstotliwością generowanego sygnału używając timer sprzętowego ale w połączeniu wyżej przedstawionym sposobem generowania sygnałów. Chciałbym po prostu zwiększyć częstotliwość generowanego sygnału bo HAL_GetTick() ma pewne ograniczenia. Czy umiałby pan podsunąć mi jakiś proste rozwiązanie ?
Jeżeli chodzi o sygnał sinus (lub inny generowany na podstawie wcześniej przygotowanej tablicy), najlepszym sposobem jest użycie timera sprzętowego i DMA. Przykład znajdziesz w kolejnym artykule, czyli https://www.stm32wrobotyce.pl/2021/10/08/konwerter-cyfrowo-analogowy-odtwarzacz-wave/. Wystarczy zamiast dźwięku wysyłać przez DMA przygotowana odpowiednio tablicę z wartościami sinusa. Można też podawać do DAC kolejne wartości przy wystąpieniu przerwania od timera sprzętowego, ale to będzie znacznie mniej wydajne rozwiazanie.
Chciałbym jeszcze zapytać gdzie powinienem szukać podobnych wzorów jak na sin ale do generowania np cos , przebiegu prostokątnego oraz trójkąta ale bez użycia wbudowanych funkcji.
Nie mam gotowych przykładów takich funkcji pod ręką, ale w sieci można łatwo znaleźć je pod hasłami: Triangular Wave, Cosine Wave itd., również z przykładami w C dopisując np. „C program”
Mam takie pytanie czy można zwizualizować symulacje używając cubeMonitora ??
Tak, jak najbardziej. Przykład użycia CubeMonitora pokazałem m.in. w tym artykule.