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.
Lista lekcji „Kurs STM32 Low Layer”
- Kurs STM32 LL cz. 1. Biblioteki Low Layer, Nucleo-G071RB, STM32CubeIDE
- Kurs STM32 LL cz. 2. Przygotowanie projektu
- Kurs STM32 LL cz. 3. Wewnętrzne i zewnętrzne źródła zegara
- Kurs STM32 LL cz. 4. Pętla PLL i taktowanie układów peryferyjnych
- Kurs STM32 LL cz. 5. Budowa GPIO i sterowanie wyjściem
- Kurs STM32 LL cz. 6. Wyjście GPIO i przerwania EXTI
- Kurs STM32 LL cz. 7. Interfejs USART, transmisja danych w trybie polling
- Kurs STM32 LL cz. 8. Komunikacja USART w trybie przerwań
- Kurs STM32 LL cz. 9. Kontroler DMA, komunikacja USART w trybie DMA
- Kurs STM32 LL cz. 10. Rodzaje i budowa Timerów, Timer w funkcji licznika
- Kurs STM32 LL cz. 11. Timer w trybie Input Capture
- Kurs STM32 LL cz. 12. Timer w trybie Output Compare i PWM
- Kurs STM32 LL cz. 13. Wstęp do konwertera ADC
- Kurs STM32 LL cz. 14. Konwersja ADC Single Channel i Multi Channel w trybie Polling
- Kurs STM32 LL cz. 15. Konwersja ADC Single Channel i Multi Channel w trybie przerwań
- Kurs STM32 LL cz. 16. Konwersja ADC Single Channel i Multi Channel w trybie DMA
- Kurs STM32 LL cz. 17. Wstęp do magistrali I2C
- Kurs STM32 LL cz. 18. Komunikacja I2C w trybie polling
- Kurs STM32 LL cz. 19. Komunikacja I2C w trybie przerwań
- Kurs STM32 LL cz. 20. Komunikacja I2C w trybie DMA
- Kurs STM32 LL cz. 21. Wprowadzenie do interfejsu SPI
- Kurs STM32 LL cz. 22. Komunikacja SPI w trybie polling
- Kurs STM32 LL cz. 23. Komunikacja SPI w trybie przerwań
- Kurs STM32 LL cz. 24. Komunikacja SPI w trybie DMA
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:
- Podstawowe parametry konwertera ADC należy skonfigurować przy wyłączonym ADC.
- Najpierw ustawiamy rozdzielczość, wyrównanie danych oraz zegar taktujący ADC.
- Następnie wybieramy tryb pracy: single, continuous lub discontinuous.
- 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ć.
- 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.
- 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ć.
- 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).
- 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).
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.