Kurs STM32 LL cz. 15. Konwersja ADC Single Channel i Multi Channel w trybie przerwań
Konwerter ADC oferuje możliwość wywołania dość sporej ilości przerwań, które odnoszą się nie tylko do zakończenia konwersji i obsługi błędów, ale także etapów konfiguracji. W dzisiejszej lekcji pokaże, w jaki sposób wykorzystać przerwania do obsługi odczytu danych z przetwornika.
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
Źródła przerwań są bezpośrednio powiązane z flagami dostępnymi w rejestrze ADC_ISR.
Wśród dostępnych źródeł przerwań możemy znaleźć:
- Zakończenie kalibracji (EOCAL)
- Uruchomienie ADC (ADRDY)
- Zakończenie konwersji (EOC)
- Zakończenie sekwencji konwersji (EOS)
- Zdarzenie od Analog Watchdog (AWD1, AWD2, AWD3)
- Zakończenie konfiguracji kanałów ADC (CCRDY)
- Zakończenie fazy próbkowania (EOSMP)
- Wystąpienie nadpisania danych (OVR)
Nas na tym etapie nauki będą interesowały dwa przerwania: zakończenie konwersji (EOC) oraz zakończenie sekwencji konwersji (EOS), które będą informowały o zakończeniu przetwarzania danych przez ADC kanału lub całej grupy kanałów.
Do obsługi tych przerwań będziemy potrzebowali dwóch rejestrów: ADC_ISR do odczytu flagi, które zdarzenie wystąpiło oraz ADC_IER, czyli rejestr włączający generowanie wybranych przerwań.
Rejestr ADC_ISR – rejestr statusu flag ADC
Bity EOC i EOS – koniec konwersji i koniec sekwencji konwersji
Rejestr ADC_IER – rejestr włączający przerwania ADC
Bity EOCIE i EOSIE – włączenie przerwania od końca konwersji i końca sekwencji konwersji
Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.
[PROGRAM] Konwersja w trybie przerwań (Single Channel)
Przykład wykorzystujący przerwania będzie analogiczny, jak w przypadku trybu polling. Wykorzystamy potencjometr podłączony do kanału 4 ADC, czyli do pinu PA4.
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 sekwencera 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 możemy skonfigurować kontroler NVIC.
NVIC_SetPriority(ADC1_COMP_IRQn, 1);
NVIC_EnableIRQ(ADC1_COMP_IRQn);
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)
;
Pozostało tylko uruchomić przerwanie od końca konwersji.
LL_ADC_EnableIT_EOC(ADC1);
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);
Dane odczytujemy w przerwaniu od EOC.
void ADC_COMP_IRQHandler(void)
{
if(LL_ADC_IsActiveFlag_EOC(ADC1) != 0)
{
adc_data = LL_ADC_REG_ReadConversionData12(ADC1);
voltage_mv = CONVERT_ADC_TO_MV(adc_data);
LL_ADC_ClearFlag_EOC(ADC1);
}
}
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ę zmieniała.
[PROGRAM] Konwersja w trybie przerwań (Multi Channel)
Czas na przykład z przerwaniami przy pomiarze kilku kanałów. 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);
Konfigurujemy kontroler przerwań NVIC.
NVIC_SetPriority(ADC1_COMP_IRQn, 1);
NVIC_EnableIRQ(ADC1_COMP_IRQn);
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)
;
Tym razem wykorzystamy oba źródła przerwań – końca konwersji i końca sekwencji.
LL_ADC_EnableIT_EOC(ADC1);
LL_ADC_EnableIT_EOS(ADC1);
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);
W obsłudze przerwania sprawdzamy obie flagi: EOC i EOS.W przypadku wystąpienia przerwania od końca konwersji, odczytujemy dane i zapisujemy do tablicy. Przerwanie końca sekwencji posłuży nam do wyzerowania licznika konwersji.
void ADC_COMP_IRQHandler(void)
{
if(LL_ADC_IsActiveFlag_EOC(ADC1) != 0)
{
adc_data[conv_cnt] = LL_ADC_REG_ReadConversionData12(ADC1);
voltage_mv[conv_cnt] = CONVERT_ADC_TO_MV(adc_data[conv_cnt]);
conv_cnt++;
LL_ADC_ClearFlag_EOC(ADC1);
}
if(LL_ADC_IsActiveFlag_EOS(ADC1) != 0)
{
conv_cnt = 0;
LL_ADC_ClearFlag_EOS(ADC1);
}
}
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[0] będzie odpowiadała wejściu A0, adc_data[1] wejściu A1, a adc_data[2] 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!