Kurs STM32 LL cz. 14. Konwersja ADC Single Channel i Multi Channel w trybie Polling

W poprzedniej lekcji poznaliśmy budowę i zasadę działania konwertera ADC w STM32. Mamy solidne podstawy teoretyczne. Możemy więc kontynuować naukę i przejść do praktycznych przykładów.

W dzisiejszej części poznamy podstawowe zasady konwersji analogowo-cyfrowej w trybie Polling. Pokaże Ci, jak skonfigurować i odczytać dane z konwertera zarówno w przypadku jednego kanału, jak i kilku kanałów.

Procedura konfiguracji ADC

Do prawidłowej pracy konwerter ADC wymaga zachowania kolejności uruchamiania poszczególnych elementów bloku przetwornika. W niektórych momentach musimy również poczekać na włączenie danej części, aby móc poprawnie skonfigurować resztę parametrów. Podstawowa konfiguracja wymaga wykonania następujących kroków:

  1. Podstawowe parametry konwertera ADC należy skonfigurować przy wyłączonym ADC.
  2. Najpierw ustawiamy rozdzielczość, wyrównanie danych oraz zegar taktujący ADC.
  3. Następnie wybieramy tryb pracy: single, continuous lub discontinuous.
  4. Teraz czas na konfigurację kanałów. Najpierw wybieramy tryb konfiguracji: stały lub pełna konfiguracja. Należy poczekać, aż flaga CCRDY (Channel Configuration Ready) zostanie ustawiona. Musimy ją potem wyczyścić.
  5. Następnie wybieramy czas próbkowania. Ustawiamy jeden lub dwa czasy wspólne, a następnie wybieramy, który z tych dwóch czasów próbkowania ma być przypisany do każdego z wybranych kanałów.
  6. W dalszej kolejności włączamy kanały – przy stałej konfiguracji włączamy wybrane kanały, przy pełnej konfiguracji wpisujemy numery kanałów do poszczególnych elementów sekwensera. Należy poczekać, aż flaga CCRDY (Channel Configuration Ready) zostanie ustawiona. Musimy ją potem wyczyścić.
  7. Teraz możemy przejść do włączenia ADC. Najpierw uruchamiamy wewnętrzny stabilizator ADC i czekamy aż będzie gotowy (trwa to ok. 20 us).
  8. Następnie włączamy konwerter ADC i czekamy, aż się uruchomi (flaga ADCRDY).

Po wykonaniu procedury konwerter będzie  uruchomiony. Pozostaje wywołać start konwersji i odczytać dane.

[PROGRAM] Konwersja w trybie pooling (Single Channel)

Poznaliśmy podstawowe elementy konwertera ADC. Możemy przystąpić do napisania pierwszego programu. Wykorzystamy do tego celu potencjometr podłączony do kanału 4 ADC, czyli do pinu PA4.

Najpierw musimy skonfigurować zegary w STM32. Robimy to analogicznie jak w dotychczasowych programach. Następnie konfigurujemy pin PA4 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);

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

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

Czekamy, aż się wykona.

 while (LL_ADC_IsActiveFlag_EOC(ADC1) == 0)
    ;

I odczytujemy dane.

adc_data = LL_ADC_REG_ReadConversionData12(ADC1);

Po wszystkim czyścimy flagę zakończenia konwersji (właściwie to odczyt danych z rejestru ADC_DR już czyści flagę, ale dodałem jeszcze czyszczenie, aby było to lepiej widoczne).

LL_ADC_ClearFlag_EOC(ADC1);

Możemy jeszcze przeliczyć dane z rejestru ADC na wartość w miliwoltach. Do tego celu stworzyłem makro.

#define ADC_VREF_MV	3300
#define ADC_MAX_VALUE	4096
#define CONVERT_ADC_TO_MV(x) ((ADC_VREF_MV * x) / (ADC_MAX_VALUE - 1))

Aby przeliczyć wartość pomiaru ADC na miliwolty, wystarczy wywołać kod.

voltage_mv = CONVERT_ADC_TO_MV(adc_data);

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 pooling (Multi Channel)

Pierwszy przykład z ADC uruchomiony. Pomiar na jednym kanale działa bez zarzutu. Czas uruchomić kilka kanałów jednocześnie. 

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 wybranych kanałów.

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łu 0, 1 i 4, włączamy je w rejestrze sekwencera CHSELR. Czekamy, aż konfiguracja kanałów 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);

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

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

Czekamy, aż się wykona.

 while (LL_ADC_IsActiveFlag_EOC(ADC1) == 0)
	 ;

I odczytujemy dane.

adc_data = LL_ADC_REG_ReadConversionData12(ADC1);

Następnie powtarzamy procedurę czekania na flagę dla pozostałych kanałów. Wybraliśmy tryb single, zatem cała sekwencja pomiaru trzech kanałów wykona się po jednym wywołaniu startu konwersji.

while (LL_ADC_IsActiveFlag_EOC(ADC1) == 0)
    ;
adc_data_pot2 = LL_ADC_REG_ReadConversionData12(ADC1);
 
while (LL_ADC_IsActiveFlag_EOC(ADC1) == 0)
    ;
 adc_data_pot3 = LL_ADC_REG_ReadConversionData12(ADC1);

Flaga EOC będzie czyszczona po każdym odczytaniu danych z rejestru ADC_DR (funkcja ReadConversionData12) automatycznie. Ponieważ wybraliśmy tryb konfiguracji stałej (fixed), pomiary będą wykonywane zgodnie z kolejnością numerów kanałów – najpierw kanał 0, potem 1 a na koniec 4.

Możemy jeszcze przeliczyć dane z rejestru ADC na wartość w miliwoltach. Do tego celu stworzyłem makro.

#define ADC_VREF_MV	3300
#define ADC_MAX_VALUE	4096
#define CONVERT_ADC_TO_MV(x) ((ADC_VREF_MV * x) / (ADC_MAX_VALUE - 1))

Aby przeliczyć wartość pomiaru ADC na miliwolty, wystarczy wywołać kod.

voltage_mv_pot1 = CONVERT_ADC_TO_MV(adc_data_pot1);
voltage_mv_pot2 = CONVERT_ADC_TO_MV(adc_data_pot2);
voltage_mv_pot3 = CONVERT_ADC_TO_MV(adc_data_pot3);

Teraz możemy uruchomić debugger Run -> Debug (F11) i wpisać w pole Live expression zmienne. Jeżeli zmienimy położenie potencjometrów, odpowiadająca mu wartość wartość odczytana z przetwornika również będzie się zmieniała (adc_data_pot1 będzie odpowiadała wejściu A0, adc_data_pot2 wejściu A1, a adc_data_pot3 wejściu A4).

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

Komentarz

  1. Warto napisać, że powodzenie zależy od ustawień ADC. Jest to dość szybkie peryferium i niski sampling time spowoduje , że flaga EOC bedzie bardzo szybko podnoszona, szybciej niz odczyt i zapis z rejestru DR, co bedzie prowadzić do overrunu.

Dodaj komentarz

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