Kurs STM32 LL cz. 18. Komunikacja I2C w trybie polling
Poznaliśmy podstawy teoretyczne potrzebne do obsługi komunikacji I2C. Czas zatem na przykład. Dzisiaj napiszemy funkcje potrzebne do wysyłania i odbierania danych w trybie master. Zaimplementujemy przepływ danych w przypadku transmisji poniżej 255 bajtó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
Na początek powinniśmy skonfigurować I2C. Zaczynamy oczywiście od ustawienia pinów w trybie funkcji alternatywnej. Zgodnie z opisem części sprzętowej I2C, korzystamy z wyjść typu Open Drain.
#define I2C1_SDA_Pin LL_GPIO_PIN_9
#define I2C1_SDA_GPIO_Port GPIOB
#define I2C1_SCL_Pin LL_GPIO_PIN_8
#define I2C1_SCL_GPIO_Port GPIOB
I2C_TypeDef *i2c = I2C1;
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);
Teraz upewniamy się, że I2C jest wyłączony. Przy włączonym I2C część konfiguracji nie jest możliwa.
LL_I2C_Disable(i2c);
Potem ustawiamy filtry. Włączymy filtr analogowy. Z filtru cyfrowego nie będziemy korzystali (wartość 0x00).
LL_I2C_ConfigFilters(i2c, LL_I2C_ANALOGFILTER_ENABLE, 0x00);
Teraz ustawimy wartość rejestru TIMINGR. Jak już wspomniałem w poprzedniej części, do ustalenia potrzebnej wartości skorzystamy z kalkulatora w STM32CubeMX. Tworzymy projekt dla naszego mikrokontrolera i ustawiamy zegary w zakładce Clock Configuration. Korzystamy z oscylatora HSI16 i pętli PLL, na wyjściu której mamy 64 MHz.
Wybieramy interfejs I2C i ustawiamy filtr analogowy oraz tryb Standard Mode (100 kHz).
Wartość Timing wpisujemy do rejestru TIMINGR.
LL_I2C_SetTiming(i2c, 0x10707DBC);
Włączamy ClockStretching (wymagane dla trybu master) i wybieramy komunikację I2C.
LL_I2C_EnableClockStretching(i2c);
LL_I2C_SetMode(i2c, LL_I2C_MODE_I2C);
Teraz możemy włączyć interfejs.
LL_I2C_Enable(i2c);
Wysyłanie danych
Do poprawnego rozpoczęcie wysyłania danych potrzebujemy informacji o adresie urządzenia slave oraz ilości danych do wysłania. Biblioteki Low Layer udostępniają nam funkcję LL_I2C_HandleTransfer(), która konfiguruje wszystkie potrzebne bity rejestru CR2, w tym rodzaj adresowania (7 lub 10-bitowe), ustawia adres slave, ilość danych do przesłania oraz wybiera tryb generowania bitu STOP – automatycznie lub ręcznie. Dodatkowo funkcja ta wygeneruje bit START oraz odpowiednio ustawi kierunek transmisji.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
Przed wysłaniem każdego bajtu musimy poczekać, aż flaga TXIS zostanie ustawiona, co będzie oznaczało, że rejestr nadawczy jest pusty.
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
Następnie wysyłany kolejne bajty danych.
LL_I2C_TransmitData8(i2c, *data_ptr);
Korzystamy z trybu AUTOEND, dlatego po zakończeniu (wysłaniu zadeklarowanej liczby danych) automatycznie wygeneruje się sygnał STOP. Czekamy aż pojawi się flaga informująca o tym i czyścimy flagę.
while(LL_I2C_IsActiveFlag_STOP(i2c) == 0)
;
LL_I2C_ClearFlag_STOP(i2c);
Odbieranie danych
W analogiczny sposób realizujemy odbieranie danych. Tym razem wybieramy kierunek transmisji jako READ.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_READ);
W przypadku odbioru czekamy na flagę RXNE, a potem odczytujemy dane z rejestru.
while(LL_I2C_IsActiveFlag_RXNE(i2c) == 0)
;
*data_ptr = LL_I2C_ReceiveData8(i2c);
Podobnie jak przy wysyłaniu, po odebraniu wszystkich danych czekamy na wygenerowanie STOP i czyścimy flagę.
while(LL_I2C_IsActiveFlag_STOP(i2c) == 0)
;
LL_I2C_ClearFlag_STOP(i2c);
Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.
Czujnik HTS221
Opracowaliśmy podstawowe funkcje do obsługi I2C, dlatego czas na praktykę. Do tego celu użyjemy czujnika temperatury i wilgotności HTS221. Ja korzystam z shieldu X-NUCLEO-IKS01A2 dedykowanego dla płytek Nucleo.
Jednym z czujników dostępnych na tej płytce jest właśnie HTS221. Został on umieszczony w lewym dolnym rogu PCB. Domyślnie czujnik podłączony jest do interfejsu I2C1 i dostępny pod adresem 0xBF.
HTS221 to kompaktowy czujnik wilgotności względnej i temperatury. Zawiera element pomiarowy i układ dostarczający informacji pomiarowych poprzez cyfrowe interfejsy szeregowe I2C lub SPI.
HTS221 jest dostępny w małej obudowie i pozwala na pomiar temperatury w zakresie -40 °C do +120 °C oraz wilgotności od 0 do 100%. Ma konfigurowalną częstotliwość pomiarów od 1 Hz do 12,5 Hz i 16-bitową rozdzielczość, co sprawia, że sprawdzi się nie tylko w projektach hobbystycznych, ale także zastosowaniach profesjonalnych.
Więcej informacji o czujniku można znaleźć w jego dokumentacji. Nie chciałbym się dzisiaj skupiać na obsłudze samego czujnika, a raczej pokazać, jak w praktyce zastosować interfejs I2C. Dlatego do obsługi HTS221 użyjemy gotowej biblioteki od firmy ST. Jest ona dostępna w ramach pakietu X-CUBE-MEMS1.
Biblioteka do HTS221 dostępna jest w formie czterech plików:
- dwóch plików nagłówkowych: hts221.h oraz hts221_reg.h
- dwóch plików źródłowych: hts221.c oraz hts221_reg.c
Pliki z sufiksem “_reg” dotyczą obsługi rejestrów czujnika. Znajdziemy w nich definicje rejestrów oraz funkcje do podstawowych operacji na poszczególnych bitach.
Pliki “hts221.c/.h” zawierają funkcje do obsługi czujnika, takie jak inicjalizacja, konfiguracja parametrów pracy czy funkcje do odczytu temperatury i wilgotności.
Nam do obsługi będzie potrzebna znajomość kilku struktur umieszczonych w bibliotece. Używać będziemy struktur HTS221_COMMON_Driver, HTS221_HUM_Driver oraz HTS221_TEMP_Driver.
HTS221_COMMON_Driver to struktura do ogólnej obsługi HTS221. Zawiera funkcje do inicjalizacji czujnika, czy odczytania aktywnych funkcjonalności.
HTS221_HUM_Driver oraz HTS221_TEMP_Driver to sterowniki to obsługi czujników – odpowiednio wilgotności i temperatury. Zawierają funkcje włączające czujnik, konfigurujące częstotliwość pomiarów, czy odczytujące pomiar.
Zanim jednak wywołamy inicjalizację i odczyt, musimy skonfigurować obsługę samego interfejsu I2C. Biblioteka została napisana w ten sposób, że możemy sami wybrać, za pomocą jakich funkcji będzie obsługiwana komunikacja przez I2C. Ustawiamy to za pomocą struktury HTS221_IO_t. Z jej pomocą wybieramy to, czy korzystamy z I2C czy SPI, podajemy adres HTS221 oraz podajemy funkcje do inicjalizacji, odczytu i zapisu I2C.
typedef struct
{
HTS221_Init_Func Init;
HTS221_DeInit_Func DeInit;
uint32_t BusType; /*0 means I2C, 1 means SPI-3-Wires */
uint8_t Address;
HTS221_WriteReg_Func WriteReg;
HTS221_ReadReg_Func ReadReg;
HTS221_GetTick_Func GetTick;
} HTS221_IO_t;
Za przechowywanie tych informacji i przekazywanie ich do funkcji bibliotecznych odpowiada natomiast struktura HTS221_Object_t, która jest obiektem czujnika. Oprócz struktury HTS221_IO_t przechowuje ona także informacje o statusie działania obiektu, czyli flagi inicjalizacji oraz aktywności funkcji pomiaru wilgotności i temperatury.
typedef struct
{
HTS221_IO_t IO;
stmdev_ctx_t Ctx;
uint8_t is_initialized;
uint8_t hum_is_enabled;
uint8_t temp_is_enabled;
} HTS221_Object_t;
Choć na pierwszy rzut oka może się to wydawać, że autor biblioteki niepotrzebnie tak skomplikował obsługę, ma ona swoje zalety. Pozwala korzystać z kilku czujników w różnej konfiguracji. Warto poświęcić chwilę i lepiej zapoznać się z biblioteką, aby dobrze zrozumieć jej działanie. Ja postaram się przedstawić tylko niezbędne elementy potrzebne do uruchomienia czujnika.
Musimy teraz wypełnić pola zgodnie z budową przedstawionych struktur. Tworzymy dwie zmienne.
static HTS221_Object_t hts221_obj;
static HTS221_IO_t io_ctx;
Teraz wypełniamy ich pola, podając adresy i nazwy funkcji.
io_ctx.BusType = HTS221_I2C_BUS;
io_ctx.Address = HTS221_I2C_ADDRESS;
io_ctx.Init = i2c_init;
io_ctx.ReadReg = i2c_reg_read;
io_ctx.WriteReg = i2c_reg_write;
HTS221_RegisterBusIO(&hts221_obj, &io_ctx);
O ile funkcja i2c_init() jest już znajoma, o tyle funkcji i2c_reg_read() i i2c_reg_write() jeszcze nie poznaliśmy. Dlatego teraz przyjrzymy się im bliżej i postaram się wyjaśnić dlatego wyglądają tak, a nie inaczej.
Korzystając z zewnętrznej biblioteki musimy się do niej dostosować. Jeżeli przyjrzymy się bliżej polom struktury HTS221_IO_t zobaczymy, że ReadReg i WriteReg to wskaźniki na funkcje.
typedef int32_t (*HTS221_WriteReg_Func)(uint16_t, uint16_t, uint8_t *, uint16_t);
typedef int32_t (*HTS221_ReadReg_Func)(uint16_t, uint16_t, uint8_t *, uint16_t);
Przyjmują one 4 argumenty, gdzie:
- pierwszy to adres urządzenia slave
- drugi to adres rejestru układu
- trzeci to wskaźnik na dane do wysłania lub odebrania
- czwarty to ilość danych do wysłania lub odebrania
Musimy zatem zmodyfikować nasze funkcje utworzone w poprzedniej części tak, aby były kompatybilne z wymaganiami biblioteki. Dodatkowo funkcje, które napisaliśmy wcześniej zakładają tylko komunikację w jedną stronę tzn. albo wysłanie albo odczyt danych. Tutaj w przypadku odczytu najpierw musimy wysłać adres rejestru, a dopiero później dane odczytać. Powinno to odbyć się w ramach jednej ramki bez znaku stopu w trakcie dlatego dotychczasowe rozwiązanie musi ulec modyfikacji. Jak zatem powinny wyglądać funkcje do wysyłania i odbierania danych z czujnika?
Funkcja i2c_reg_write będzie zmodyfikowana w niewielkim stopniu. Przy użyciu funkcji LL_I2C_HandleTransfer() musimy podać rozmiar o jeden większy niż użytkownik przekaże do funkcji. Argument size oznacza ilość danych bez uwzględnienia adresu rejestru, a my ten adres na początku chcemy przesłać. Jest on taką samą daną, jak każda inna.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)(size+1), LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
Teraz czekamy na falgę TXIS i wysyłamy adres rejestru.
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, reg_addr);
Następnie przesyłamy kolejne dane analogicznie, jak to robiliśmy dotychczas.
while(data_count > 0)
{
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, *data_ptr);
data_ptr++;
data_count--;
}
Na koniec czekamy na flagę STOP i ją czyścimy.
while(LL_I2C_IsActiveFlag_STOP(i2c) == 0)
;
LL_I2C_ClearFlag_STOP(i2c);
W przypadku funkcji i2c_reg_read musimy wprowadzić trochę więcej zmian. Na początku musimy wysłać do urządzenia slave adres rejestru, z którego chcemy dane odczytać. Ponieważ potem będziemy jeszcze odbierali dane, nie chcemy generować po wysłaniu adresu sygnału STOP. Dlatego korzystamy z flagi LL_I2C_MODE_SOFTEND.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, 1, LL_I2C_MODE_SOFTEND, LL_I2C_GENERATE_START_WRITE);
Teraz czekamy na flagę TXIS i przesyłamy adres rejestru.
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, reg_addr);
Sygnał STOP nie będzie generowany, dlatego koniec wysyłania danych zasygnalizuje flaga TC (Transfer Complete).
while(LL_I2C_IsActiveFlag_TC(i2c) == 0)
;
Teraz przechodzimy do odbierania danych. Musimy ponownie skonfigurować transfer danych. Teraz już chcemy wygenerować po odebraniu danych sygnał STOP. Kierunek transferu ustawiamy jako READ.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)read_size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_READ);
Odbiór poszczególnych bajtów realizujemy analogicznie, jak w poprzedniej części.
while(read_data_count > 0)
{
while(LL_I2C_IsActiveFlag_RXNE(i2c) == 0)
;
*read_data_ptr = LL_I2C_ReceiveData8(i2c);
read_data_ptr++;
read_data_count--;
}
Na koniec oczywiście czekamy na flagę STOP i ją czyścimy.
while(LL_I2C_IsActiveFlag_STOP(i2c) == 0)
;
LL_I2C_ClearFlag_STOP(i2c);
Mamy już napisane funkcje do odczytu i wysyłania danych kompatybilne z biblioteką. Możemy przejść do wywołania odczytu temperatury i wilgotności.
Na początku konfigurujemy sterownik czujnika. Wywołujemy funkcję, która zainicjalizuje interfejs I2C i czujnik.
HTS221_COMMON_Driver.Init(&hts221_obj);
Teraz włączamy czujniki temperatury i wilgotności i ustawiamy częstotliwość pomiaru na 1 Hz.
HTS221_TEMP_Driver.Enable(&hts221_obj);
HTS221_TEMP_Driver.SetOutputDataRate(&hts221_obj, 1.0f);
HTS221_HUM_Driver.Enable(&hts221_obj);
HTS221_HUM_Driver.SetOutputDataRate(&hts221_obj, 1.0f);
Odczyt temperatury i wilgotności wywołujemy za pomocą funkcji GetTemperature i GetHumidity. Zmienne temperature i humnidity powinny być typu float.
HTS221_TEMP_Driver.GetTemperature(&hts221_obj, &temperature);
HTS221_HUM_Driver.GetHumidity(&hts221_obj, &humidity);
Teraz możemy wejść w tryb debugowania i dodać zmienne temperaturę oraz humidity do pola Live Expression. Po uruchomieniu programu otrzymamy odczyt temperatury w °C i wilgotności w %.
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!