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.

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!
Repozytorium GitHub

Dodaj komentarz

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