Kurs STM32 LL cz. 16. Konwersja ADC Single Channel i Multi Channel w trybie DMA

Kontroler DMA daje możliwość znacznego odciążenia jednostki obliczeniowej mikrokontrolera w trakcie konwersji sygnałów analogowych. W szczególności wtedy, gdy konwersja ADC odbywa się bardzo często i na większej ilości kanałów.

Kontroler DMA

Direct Memory Access (DMA), czyli mechanizm bezpośredniego dostępu do pamięci to technika przesyłania danych z pominięciem jednostki CPU.

DMA pozwala na przesyłanie danych:

  • z pamięci do pamięci
  • z pamięci do układu peryferyjnego
  • z układu peryferyjnego do pamięci
  • z układu peryferyjnego do układu peryferyjnego

W mikrokontrolerach STM32 możemy mieć do dyspozycji jeden lub dwa kontrolery DMA. Każdy kontroler ma kilka kanałów, a każdy kanał może obsługiwać różne rodzaje transferów. W STM32G071 dostępny jest jeden kontroler DMA1 z 7 kanałami.

To, jak zbudowany jest przepływ danych w STM32, przedstawia poniższa grafika.

Kontroler DMA podłączony jest do szyny AHB (Advanced High-Performance Bus), która komunikuje go z pamięcią SRAM  i Flash oraz za pośrednictwem szyny APB (Advanced Peripheral Bus) z układami peryferyjnymi. Układem nadzorującym współpracę między szynami jest Bus Matrix.

W przypadku, gdy dwa kanały DMA chcą wykonywać transfer jednocześnie, Arbiter rozdziela czas dostępu do Bus Matrix dla każdego kanału.

Gdy dostępne są dwa oddzielne kontrolery DMA, mogą one korzystać z Bux Matrix jednocześnie, dopóki nie korzystają z tych samych układów peryferyjnych.

Szyna AHB implementuje algorytm karuzelowy (round robin), według którego przydziela dla każdego z układów przedział czasowy na wykonanie operacji, nie uwzględniając żadnych priorytetów. Kontroler DMA może więc spowolnić pracę CPU, jeżeli oba elementy potrzebują dostępu do tego samego obszaru pamięci lub układu peryferyjnego (zdarza się to bardzo rzadko). Nie może jednak zablokować CPU, ponieważ arbiter rozdziela po równo cykle pracy pomiędzy poszczególne elementy mikrokontrolera.

W starszych mikrokontrolerach (seria F i L) kontroler DMA podzielony jest na kanały. Do każdego z kanałów dołączonych jest kilka układów peryferyjnych i tylko jeden z nich może być jednocześnie obsługiwany jest dany kanał DMA.

W nowszych układach (seria G) do każdego z kanałów można podpiąć dowolny układ peryferyjny z dostępnych. Za zarządzanie odpowiada DMAMUX. DMAMUX pozwala także na użycie generatora DMA do układów peryferyjnych, które nie mają funkcji DMA.

DMA generuje trzy rodzaje przerwań:

  • po wykonaniu połowy transferu
  • po wykonaniu całego transferu
  • w przypadku błędu

DMA może przesyłać dane po 8, 16 lub 32 bity (odpowiednio Byte, Half Word i Word). Szerokość danych zależna jest od rodzaju wysyłanych przez nas danych (po stronie pamięci) oraz od rozmiaru rejestru. Dodatkowo przed rozpoczęciem transferu DMA ustalany jest rozmiar, czyli ilość danych do przesłania.

Każdy transfer DMA może mieć przypisany jeden z 4 priorytetów, według których kontroler wykonuje transfery w przypadku, gdy jednocześnie wystąpi potrzeba obsługi dwóch lub więcej kanałów.

  • very high
  • high
  • medium
  • low

Poza tym kontroler DMA ma możliwość automatycznej inkrementacji adresów po stronie układu peryferyjnego i pamięci oraz możliwość cyklicznego transferu (po zakończeniu jednego rozpoczyna się kolejny od początku skonfigurowanego adresu – na zasadzie bufora kołowego). Pozwala to dobrać sposób przesyłania danych według potrzeb.

Rejestry DMA

DMA_ISR – rejestr z flagami przerwań dla każdego z 7 kanałów

DMA_IFCR – czyszczenie flag przerwań

DMA_CCRx – rejestr konfiguracyjny dla kanału x

Bity MEM2MEM – włączenie transferu pamięć-pamięć

Bit PL[1:0] – wybór priorytetu

Bity MSIZE[1:0] – rozmiar danych po stronie pamięci

Bity PSIZE[1:0] – rozmiar danych po stronie układu peryferyjnego

Bit MINC – włączenie zwiększania adresu po stronie pamięci

Bit PINC – włączenie zwiększania adresu po stronie układu peryferyjnego

Bit CIRC – tryb kołowy

Bit DIR – kierunek transferu

Bit TEIE – włączenie przerwania od błędu

Bit HTIE – włączenie przerwania od połowy transferu

Bit TCIE – włączenie przerwania od pełnego transferu

Bit EN – włączenie kanału

Rejestr DMA_CNDTRx – rozmiar transferu dla kanału x

DMA_CPARx – adres układu peryferyjnego dla kanału x

DMA_CMARx – adres układu pamięci dla kanału x

Multiplekser DMA

W układach z serii G do każdego z kanałów można podpiąć dowolny układ peryferyjny z puli dostępnych. Za zarządzanie przepływem danych odpowiada multiplekser, czyli DMAMUX.

DMAMUX ma za zadanie przekierować na wybrany kanał sygnał z układu peryferyjnego, którego chcemy użyć. Dzięki temu mamy dużą elastyczność pod względem używania DMA. Nie pojawi się problem często występujący w poprzednich seriach STM32 (np. F1), gdzie nie mogliśmy użyć dwóch peryferiów w trybie DMA, bo były podłączone do tego samego kanału.

W STM32G071 mamy do dyspozycji 77 wejść mogących korzystać z kontrolera DMA. Pełną listę przedstawia tabela.

Rodzaj wejścia dla kanału x konfigurujemy przy pomocy rejestru DMAMUX_CxCR i bitów DMAREQ_ID[6:0].

DMAMUX ma możliwość także konfiguracji wyzwalaczy i synchronizacji. Nie będziemy jednak korzystali z tych funkcjonalności, dlatego pominąłem ich opis.

Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.

Kontroler DMA i konwerter ADC w STM32

Dotychczas odczytaliśmy dane z rejestru po każdej konwersji ręcznie. Nawet w przypadku przerwań programowo przenosiliśmy dane z rejestru ADC_DR do tablicy. Jeżeli nasz program nie jest mocno obciążony obliczeniowo, takie rozwiązanie jest jak najbardziej poprawne. Zazwyczaj jednak w przypadku konwertera ADC wykorzystuje się dobrodziejstwa, jakie oferuje nam DMA. Dzięki temu mamy pewność, że wszystkie dane zostaną na czas przepisane z rejestru danych ADC do odpowiedniej zmiennej.

Jak to się dzieje, że DMA poprawnie przeniesie nam dane do tablicy po każdej konwersji? Kontroler DMA po włączeniu jest sprzęgnięty z ADC w taki sposób, że każde zakończenie konwersji na kanale automatycznie wysyła żądanie do DMA, aby dokonać transferu danych.

Transfer DMA może odbywać się w dwóch trybach:

  • pojedynczy transfer (one shot mode) – przesyłana jest określona ilość danych tzn. jeżeli skonfigurowaliśmy 3 kanały ADC, to przesyłane trzy dane i transfer zostaje zakończony nawet w przypadku, gdy konwersja będzie odbywała się dalej
  • transfer cykliczny (circular mode) – zakończenie transferu jednej sekwencji powoduje, że kontroler DMA zaczyna transfer od nowa automatycznie wracając do pierwszego elementu tablicy, którą ustawiliśmy jako docelowe miejsce transferu danych

Aby uruchomić transfer DMA, musimy włączyć DMA w rejestrach ADC oraz skonfigurować kontroler DMA.

Rejestr ADC_CFGR1

Bit DMACFG – konfiguracja trybu pracy DMA (pojedynczy lub cykliczny transfer)

Bit DMAEN – włączenie transferu DMA

[PROGRAM] Konwersja w trybie DMA (Single Channel)

Przejdźmy do przykładu. Tak jak w przypadku poprzednich trybów, przy DMA również użyjemy potencjometru podłączonego do pinu PA4. Konfigurujemy go w trybie analogowym.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinPull(ADC_Input_GPIO_Port, ADC_Input_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(ADC_Input_GPIO_Port, ADC_Input_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(ADC_Input_GPIO_Port, ADC_Input_Pin, LL_GPIO_MODE_ANALOG);

Teraz przystępujemy do konfiguracji przetwornika ADC. Na początku włączamy taktowanie ADC.

LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_ADC);

Wybieramy rozdzielczość oraz sposób wyrównania danych.

LL_ADC_SetResolution(ADC1, LL_ADC_RESOLUTION_12B);
LL_ADC_SetDataAlignment(ADC1, LL_ADC_DATA_ALIGN_RIGHT);

Następnie ustawiamy zegar dla ADC. Wybierzemy taktowanie z sygnału APB z dzielnikiem 4. ADC możemy być taktowany maksymalnie z częstotliwością 35 MHz, dlatego ustawimy mniejszą wartość, czyli 16 MHz.

LL_ADC_SetClock(ADC1, LL_ADC_CLOCK_SYNC_PCLK_DIV4);

Teraz możemy ustawić tryb konwersji. Do pomiarów wykorzystamy tryb pojedynczego pomiaru.

LL_ADC_REG_SetContinuousMode(ADC1, LL_ADC_REG_CONV_SINGLE);

Sposób konfiguracji kanałów ustawiamy jako stały. Czekamy na wykonanie konfiguracji i czyścimy flagę.

if(LL_ADC_REG_GetSequencerConfigurable(ADC1) != LL_ADC_REG_SEQ_FIXED)
{
	LL_ADC_REG_SetSequencerConfigurable(ADC1, LL_ADC_REG_SEQ_FIXED);
	while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
		;
	LL_ADC_ClearFlag_CCRDY(ADC1);
}

Czas próbkowania ustawiamy najpierw w pierwszym rejestrze SMP1, a następnie przypisujemy go do wybranego kanału.

LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_COMMON_1, LL_ADC_SAMPLINGTIME_39CYCLES_5);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_4, LL_ADC_SAMPLINGTIME_COMMON_1);

Aby korzystać z kanału 4, włączamy go w rejestrze sekwensera CHSELR. Czekamy, aż konfiguracja kanałów się wykona i czyścimy flagę.

LL_ADC_REG_SetSequencerChannels(ADC1, LL_ADC_CHANNEL_4);
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
	;
LL_ADC_ClearFlag_CCRDY(ADC1);

Teraz przejdź do konfiguracji kontrolera DMA. Włączamy zegar dla DMA1.

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

Następnie konfigurujemy parametry pracy DMA. Ustawiamy źródło dla kanału 1 w multiplekserze DMAMUX.

LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_ADC1);

Teraz kolejno ustawiamy kierunek transmisji, priorytet i tryb pracy. Tryb cykliczny pozwoli nam na włączenie kontrolera DMA raz, a potem wywoływanie tylko startu konwersji ADC. Dane będą przesyłane do zmiennej automatycznie.

LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR);

Na koniec konfigurujemy, czy adresy mają być inkrementowane oraz rozmiar pojedynczego elementu w transmisji po stronie układu peryferyjnego i pamięci. Rejestr ADC jest 16-bitowy dlatego wybieramy długość jako pół słowa (halfword). Dane będziemy przechowywali w zmiennej 32-bitowej zatem po stronie pamięci wybieramy pełne słowo (word).

LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_NOINCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_HALFWORD);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_WORD);

Pozostało jeszcze skonfigurowanie i włączenie przerwania od DMA1 na kanale 1.

NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);

Na koniec konfigurujemy rejestry DMA po stronie ADC. Włączamy transfer DMA i wybieramy tryb cykliczny oznaczony w bibliotece jako UNLIMITED.

LL_ADC_REG_SetDMATransfer(ADC1, LL_ADC_REG_DMA_TRANSFER_UNLIMITED);

Pora na uruchomienie ADC. Najpierw włączamy wewnętrzny stabilizator i czekamy wymagane minimum 20 us (ja wykorzystałem delay o czasie 1 ms).

LL_ADC_EnableInternalRegulator(ADC1);
LL_mDelay(1);

Teraz możemy włączyć przetwornik i poczekać, aż się uruchomi.

LL_ADC_ClearFlag_ADRDY(ADC1);
LL_ADC_Enable(ADC1);
while (LL_ADC_IsActiveFlag_ADRDY(ADC1) == 0)
	;

Przed pętlą główną uruchamiamy jeszcze transfer DMA. Wywołujemy funkcję ustawiającą adresy pamięci (zmiennej adc_data), układu peryferyjnego (czyli rejestru ADC_DR) oraz typ transmisji (pamięć-pamięć, peryferium-pamięć lub pamięć-peryferium). 

LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA), (uint32_t)&adc_data, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));

Musimy jeszcze poinformować kontroler, ile danych chcemy przesłać. Po zakończeniu transmisji licznik ten spadnie do 0, ale zastosowanie trybu cyklicznego spowoduje, że wartość ta załaduje się ponownie. Teraz używamy jednego potencjometru, dlatego długość będzie wynosiła 1.

#define NUMBER_OF_CONVERSION 1

LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, NUMBER_OF_CONVERSION);

Teraz, gdy mamy już wszystko skonfigurowane, włączamy przerwania od DMA TX i włączamy kanał 1 DMA1.

LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

Konwersję będziemy wykonywali co 100 ms. Do tego celu wykorzystamy timer programowy oparty na SysTick. Konstrukcję znamy już z poprzednich rozdziałów.

W pętli co 100 ms startujemy konwersję.

LL_ADC_REG_StartConversion(ADC1);

Po wykonaniu i przesłaniu danych do zmiennej, otrzymamy przerwanie od końca transmisji DMA. Możemy w nim przeliczyć dane z ADC na miliwolty.

void DMA_Channel1_IRQHandler(void)
{
	if (LL_DMA_IsActiveFlag_TC1(DMA1))
	{
		LL_DMA_ClearFlag_TC1(DMA1);
                voltage_mv = CONVERT_ADC_TO_MV(adc_data);
	}
}

Nie musimy czyścić flagi od końca konwersji, ponieważ zostanie ona automatycznie wyczyszczona po odczycie danych przez DMA.

Teraz możemy uruchomić debugger Run -> Debug (F11) i wpisać w pole Live expression zmienne adc_data i voltage_mv. Jeżeli zmienimy położenie potencjometru, wartość odczytana z przetwornika również będzie się odpowiednio zmieniała.

[PROGRAM] Konwersja w trybie DMA (Multi Channel)

Teraz przejdźmy do przykładu z trzema potencjometrami. Do wejść analogowych ADC_IN0 oraz ADC_IN1 (odpowiednio na pinach PA0 i PA1) podłączamy jeszcze dwa potencjometry. Dodajemy też definicje pinów, aby łatwiej było operować na nazwach.

#define ADC_Pot3_Pin LL_GPIO_PIN_4
#define ADC_Pot3_GPIO_Port GPIOA
#define ADC_Pot2_Pin LL_GPIO_PIN_1
#define ADC_Pot2_GPIO_Port GPIOA
#define ADC_Pot1_Pin LL_GPIO_PIN_0
#define ADC_Pot1_GPIO_Port GPIOA

Po ustawieniu zegarów konfigurujemy PA0, PA1 i PA4 (dostępne na Nucleo na złączu Arduino pod nazwami A0, A1 i A2) jako wejścia analogowe.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);

LL_GPIO_SetPinPull(ADC_Pot1_GPIO_Port, ADC_Pot1_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(ADC_Pot1_GPIO_Port, ADC_Pot1_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(ADC_Pot1_GPIO_Port, ADC_Pot1_Pin, LL_GPIO_MODE_ANALOG);

LL_GPIO_SetPinPull(ADC_Pot2_GPIO_Port, ADC_Pot2_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(ADC_Pot2_GPIO_Port, ADC_Pot2_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(ADC_Pot2_GPIO_Port, ADC_Pot2_Pin, LL_GPIO_MODE_ANALOG);

LL_GPIO_SetPinPull(ADC_Pot3_GPIO_Port, ADC_Pot3_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(ADC_Pot3_GPIO_Port, ADC_Pot3_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(ADC_Pot3_GPIO_Port, ADC_Pot3_Pin, LL_GPIO_MODE_ANALOG);

Teraz przystępujemy do konfiguracji przetwornika ADC. Na początku włączamy taktowanie ADC.

LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_ADC);

Wybieramy rozdzielczość oraz sposób wyrównania danych.

LL_ADC_SetResolution(ADC1, LL_ADC_RESOLUTION_12B);
LL_ADC_SetDataAlignment(ADC1, LL_ADC_DATA_ALIGN_RIGHT);

Następnie ustawiamy zegar dla ADC. Wybierzemy taktowanie z sygnału APB z dzielnikiem 4. ADC możemy być taktowany maksymalnie z częstotliwością 35 MHz, dlatego ustawimy mniejszą wartość, czyli 16 MHz.

LL_ADC_SetClock(ADC1, LL_ADC_CLOCK_SYNC_PCLK_DIV4);

Teraz możemy ustawić tryb konwersji. Do pomiarów wykorzystamy tryb pojedynczego pomiaru.

LL_ADC_REG_SetContinuousMode(ADC1, LL_ADC_REG_CONV_SINGLE);

Sposób konfiguracji kanałów ustawiamy jako stały. Czekamy na wykonanie konfiguracji i czyścimy flagę.

if(LL_ADC_REG_GetSequencerConfigurable(ADC1) != LL_ADC_REG_SEQ_FIXED)
{
	LL_ADC_REG_SetSequencerConfigurable(ADC1, LL_ADC_REG_SEQ_FIXED);
	while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
		;
	LL_ADC_ClearFlag_CCRDY(ADC1);
}

Czas próbkowania ustawiamy najpierw w pierwszym rejestrze SMP1, a następnie przypisujemy go do wybranego kanału.

LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_COMMON_1, LL_ADC_SAMPLINGTIME_39CYCLES_5);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_0, LL_ADC_SAMPLINGTIME_COMMON_1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_COMMON_1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_4, LL_ADC_SAMPLINGTIME_COMMON_1);

Aby korzystać z kanałów 0, 1 i 4, włączamy je w rejestrze sekwencera CHSELR. Czekamy, aż konfiguracja się wykona i czyścimy flagę.

LL_ADC_REG_SetSequencerChannels(ADC1, LL_ADC_CHANNEL_0 | LL_ADC_CHANNEL_1 | LL_ADC_CHANNEL_4);
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
	;
LL_ADC_ClearFlag_CCRDY(ADC1);

Teraz przejdź do konfiguracji kontrolera DMA. Włączamy zegar dla DMA1.

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

Następnie konfigurujemy parametry pracy DMA. Ustawiamy źródło dla kanału 1 w multiplekserze DMAMUX.

LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_ADC1);

Teraz kolejno ustawiamy kierunek transmisji, priorytet i tryb pracy. Tryb cykliczny pozwoli nam na włączenie kontrolera DMA raz, a potem wywoływanie tylko startu konwersji ADC. Dane będą przesyłane do zmiennej automatycznie.

LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR);

Na koniec konfigurujemy, czy adresy mają być inkrementowane oraz rozmiar pojedynczego elementu w transmisji po stronie układu peryferyjnego i pamięci. Rejestr ADC jest 16-bitowy dlatego wybieramy długość jako pół słowa (halfword). Dane będziemy przechowywali w zmiennej 32-bitowej zatem po stronie pamięci wybieramy pełne słowo (word).

LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_NOINCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_HALFWORD);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_WORD);

Pozostało jeszcze skonfigurowanie i włączenie przerwania od DMA1 na kanale 1.

NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);

Na koniec konfigurujemy rejestry DMA po stronie ADC. Włączamy transfer DMA i wybieramy tryb cykliczny oznaczony w bibliotece jako UNLIMITED.

LL_ADC_REG_SetDMATransfer(ADC1, LL_ADC_REG_DMA_TRANSFER_UNLIMITED);

Pora na uruchomienie ADC. Najpierw włączamy wewnętrzny stabilizator i czekamy wymagane minimum 20 us (ja wykorzystałem delay o czasie 1 ms).

LL_ADC_EnableInternalRegulator(ADC1);
LL_mDelay(1);

Teraz możemy włączyć przetwornik i poczekać, aż się uruchomi.

LL_ADC_ClearFlag_ADRDY(ADC1);
LL_ADC_Enable(ADC1);
while (LL_ADC_IsActiveFlag_ADRDY(ADC1) == 0)
	;

Przed pętlą główną uruchamiamy jeszcze transfer DMA. Wywołujemy funkcję ustawiającą adresy pamięci (tablicy adc_data), układu peryferyjnego (czyli rejestru ADC_DR) oraz typ transmisji (pamięć-pamięć, peryferium-pamięć lub pamięć-peryferium). 

LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA), (uint32_t)adc_data, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));

Musimy jeszcze poinformować kontroler, ile danych chcemy przesłać. Po zakończeniu transmisji licznik ten spadnie do 0, ale zastosowanie trybu cyklicznego spowoduje, że wartość ta załaduje się ponownie. Teraz używamy trzech potencjometrów, dlatego długość będzie wynosiła 3.

#define NUMBER_OF_CONVERSION 3

LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, NUMBER_OF_CONVERSION);

Teraz, gdy mamy już wszystko skonfigurowane, włączamy przerwania od DMA TX i włączamy kanał 1 DMA1.

LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

Konwersję będziemy wykonywali co 100 ms. Do tego celu wykorzystamy timer programowy oparty na SysTick. Konstrukcję znamy już z poprzednich rozdziałów. W pętli co 100 ms startujemy konwersję.

LL_ADC_REG_StartConversion(ADC1);

Po wykonaniu i przesłaniu danych do zmiennej, otrzymamy przerwanie od końca transmisji DMA. Możemy w nim przeliczyć dane z ADC na miliwolty.

void DMA_Channel1_IRQHandler(void)
{
	if (LL_DMA_IsActiveFlag_TC1(DMA1))
	{
		LL_DMA_ClearFlag_TC1(DMA1);
                for (int i = 0; i < NUMBER_OF_CONVERSION; ++i)
                {
                	voltage_mv[i] = CONVERT_ADC_TO_MV(adc_data[i]);
                }
	}
}

Nie musimy czyścić flagi od końca konwersji, ponieważ zostanie ona automatycznie wyczyszczona po odczycie danych przez DMA.

Teraz możemy uruchomić debugger Run -> Debug (F11) i wpisać w pole Live expression zmienne adc_data i voltage_mv. Jeżeli zmienimy położenie jednego z potencjometrów, wartość odczytana z przetwornika również będzie się odpowiednio zmieniała.

Chciałbyś otrzymywać na bieżąco informacje o nowych artykułach z kursu? Zapisz się do newslettera!

TO NIE TYLKO MAIL Z INFORMACJĄ O NOWEJ LEKCJI, ALE TAKŻE DODATKOWE MATERIAŁY. NIE PRZEGAP NOWEJ TREŚCI I DODATKOWYCH BONUSÓW. PRZEJDŹ DO STRONY KURSU I PODAJ SWÓJ ADRES E-MAIL. NIE ZAPOMNIJ POTWIERDZIĆ CHĘCI DOŁĄCZENIA W PIERWSZEJ WIADOMOŚCI!
Repozytorium GitHub

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *