Kurs STM32 LL cz. 19. Komunikacja I2C w trybie przerwań

Przerwania pozwalają nam uniknąć niepotrzebnego sprawdzania flag i oczekiwania na wysłanie lub odebranie danych. Przy ich użyciu możemy znacznie ograniczyć zaangażowanie CPU w proces komunikacji. Podobnie jak dla innych układów peryferyjnych wygląda to w przypadku I2C.
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
Przerwania magistrali I2C
Magistrala I2C pozwala na obsługę zarówno zdarzeń od transmisji, jak i błędów transmisji I2C. Wszystkie przerwania dostępne w STM32 przy komunikacji przez I2C przedstawia poniższa tabela.
Nie będziemy zajmowali się dzisiaj wszystkimi przerwaniami. Obsłużymy tylko podstawowe, niezbędne do zrealizowania przesyłania i odbierania danych. Interesować nas będzie przerwanie od przesłania bajtu (TXIS), odebrania (RXNE) oraz od flagi STOP (STOPF).
Aby włączy poszczególne przerwania, należy ustawić odpowiadający im bit w rejestrze CR1. Flagi czyścimy analogicznie jak przy transmisji danych bez przerwań.

Pamięć EEPROM AT24CXX
Wykorzystanie przerwań w przypadku czujników rzadko kiedy da nam widoczną korzyść w postaci szybszej komunikacji. Aby pobrać temperaturę z HTS221 musimy odczytać tylko kilka rejestrów. Dodatkowo często do dalszej realizacji programu potrzebujemy tych danych, dlatego tak czy inaczej, poczekać na nie musimy.
Co innego, gdy mamy do czynienia z układem pamięci. Interfejs I2C jest bardzo popularny w przypadku nieulotnej pamięci EEPROM, które zazwyczaj wykorzystywane są do zapisywania danych konfiguracyjnych. Nie wymagają dużych prędkości działania, dlatego wykorzystują magistralę I2C. Gdy chcemy zapisać lub odczytać np. 128 B danych, wykorzystanie przerwań może znacząco odciążyć jednostkę obliczeniową mikrokontrolera.
W przykładzie wykorzystamy popularną pamięć z serii 24Cxx firmy Atmel, a konkretnie układ o pojemności 2 kbit (256 B), czyli AT24C02.
Proces zapisu danych z pamięci jest bardzo prosty. Przesyłamy adres urządzenia, adres komórki pamięci i następnie bajty do zapisania w pamięci.
Analogicznie odczytujemy dane z pamięci.
Adres urządzenia w przypadku układów EEPROM 24Cxx może być konfigurowany za pomocą pinów adresowych A0, A1 i A2. Tabela poniżej przedstawia adresy w zależności od ich stanu.
Korzystam z modułu, na którym piny A0, A1 i A2 są zwarte do masy. Adres urządzenia będzie w takim wypadku wynosił 0xA0 (0b10100000).
Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.
[PROGRAM] Zapis i odczyt danych z pamięci EEPROM w trybie przerwań
Wiemy już jakie przerwania przydadzą nam się w podstawowej komunikacji oraz znamy zasadę działania pamięci EEPROM. Czas zatem na przykład.
Inicjalizacja
Inicjalizacja I2C będzie wyglądała bardzo podobnie jak w przypadku zwykłej komunikacji w trybie polling. Konfigurujemy piny SDA i SCL w trybie funkcji alternatywnej jako wyjścia Open-Drain.
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
LL_GPIO_SetPinSpeed(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinOutputType(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetAFPin_8_15(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_AF_6);
LL_GPIO_SetPinMode(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinOutputType(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_SPEED_FREQ_VERY_HIGH);
LL_GPIO_SetAFPin_8_15(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_AF_6);
LL_GPIO_SetPinMode(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_MODE_ALTERNATE);
Następnie włączamy taktowanie I2C.
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);
Analogicznie jak w poprzedniej części, konfigurujemy filtry, rejestr TIMINGR, oraz tryb pracy I2C przy wyłączonym układzie peryferyjnym. Następnie włączamy I2C.
LL_I2C_Disable(i2c);
LL_I2C_ConfigFilters(i2c, LL_I2C_ANALOGFILTER_ENABLE, 0x00);
LL_I2C_SetTiming(i2c, 0x10707DBC);
LL_I2C_EnableClockStretching(i2c);
LL_I2C_SetMode(i2c, LL_I2C_MODE_I2C);
LL_I2C_Enable(i2c);
Aby skorzystać z przerwań, włączamy je w kontrolerze NVIC. Przerwania w rejestrach I2C włączymy trochę później w momencie transmisji danych.
NVIC_SetPriority(I2C1_IRQn, 0);
NVIC_EnableIRQ(I2C1_IRQn);
Wysyłanie danych
Transmisję rozpoczynamy w identyczny sposób, jak w przypadku trybu polling. Ustawiamy adres urządzenia slave oraz ilość danych do wysłanie. Korzystamy z trybu AUTOEND, aby wygenerować po transmisji sygnał STOP.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)(size+1), LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
Adres komórki pamięci EEPROM wyślemy “ręcznie”. W przerwaniach będziemy wysyłali dane, które chcemy w pamięci zapisać.
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, reg_addr);
Teraz włączamy przerwania od wysłania danych oraz od sygnału STOP.
LL_I2C_EnableIT_TX(I2C1);
LL_I2C_EnableIT_STOP(I2C1);
Jak już wspomniałem, wysyłanie danych będzie odbywało się w obsłudze przerwania. W funkcji I2C1_IRQHandler() sprawdzamy flagę od TX oraz upewniamy się, że przerwania od wysyłania są włączone (czyli jesteśmy w trybie wysyłania). Jeżeli wszystko się zgadza, przesyłamy bajt danych, zwiększamy wskaźnik danych oraz zmniejszamy licznik przechowujący ilość danych do wysłania. Dodatkowo sprawdzamy czy był to ostatni bajt – jeżeli tak to wyłączamy przerwania.
if(LL_I2C_IsActiveFlag_TXIS(I2C1) && LL_I2C_IsEnabledIT_TX(I2C1))
{
if(tx_buffer.count > 0)
{
LL_I2C_TransmitData8(i2c, *tx_buffer.data_ptr);
tx_buffer.data_ptr++;
tx_buffer.count--;
}
if(tx_buffer.count <= 0)
{
LL_I2C_DisableIT_TX(I2C1);
}
}
Odbieranie danych
Odbieranie danych będzie wyglądało podobnie. Najpierw wysyłamy jeden bajt z adresem komórki w pamięci EEPROM. Nie chcemy generować sygnału STOP po zakończeniu wysyłania.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, 1, LL_I2C_MODE_SOFTEND, LL_I2C_GENERATE_START_WRITE);
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, reg_addr);
Czekamy na zakończenie transferu.
while(LL_I2C_IsActiveFlag_TC(i2c) == 0)
;
Teraz inicjalizujemy odbiór. Tym razem chcemy, aby po odebraniu wszystkich danych został wygenerowany sygnał STOP.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_READ);
Na koniec włączamy przerwania od RX oraz STOP.
LL_I2C_EnableIT_RX(I2C1);
LL_I2C_EnableIT_STOP(I2C1);
Odbieranie danych realizujemy w funkcji obsługi przerwania I2C1_IRQHandler();
Sprawdzamy flagę RX. Jeżeli zgadza się tryb pracy, odbieramy daną i zwiększamy wskaźnik. Jeżeli odebrane zostały już wszystkie dane, wyłączamy przerwanie.
if(LL_I2C_IsActiveFlag_RXNE(I2C1) && LL_I2C_IsEnabledIT_RX(I2C1))
{
if(rx_buffer.count > 0)
{
*rx_buffer.data_ptr = LL_I2C_ReceiveData8(i2c);
rx_buffer.data_ptr++;
rx_buffer.count--;
}
if(rx_buffer.count <= 0)
{
LL_I2C_DisableIT_RX(I2C1);
}
}
Jeżeli potrzebujemy wykonać jeszcze jakieś czynności po zakończeniu transmisji (po sygnale STOP), możemy skorzystać jeszcze z tego przerwania.
if(LL_I2C_IsActiveFlag_STOP(I2C1) && LL_I2C_IsEnabledIT_STOP(I2C1))
{
LL_I2C_ClearFlag_STOP(I2C1);
LL_I2C_DisableIT_STOP(I2C1);
}
Test komunikacji
W funkcji main() przetestujemy działanie komunikacji. Przed pętlą główną inicjalizujemy interfejs I2C i wysyłamy 8 bajtów danych pod adres początkowy 0x00. W ten sposób umieścimy dane pod adresami 0x00-0x07.
i2c_init();
static uint8_t data_w[8] = {0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18};
i2c_reg_write_it(EEPROM_ADDR, 0x00, data_w, sizeof(data_w));
Następnie w pętli głównej co 1s będziemy odczytywali dane spod adresu 0x00.
static uint8_t data_r[8] = {0};
i2c_reg_read_it(EEPROM_ADDR, 0x00, data_r, sizeof(data_r));
Możemy uruchomić przykład i podejrzeć tablicę data_r[] w oknie Live Expression. Zobaczymy, że odczytaliśmy zapisane dane.
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!