Programowanie modułów ESP32 w środowisku ESP-IDF (6). Interfejs I²C

Programowanie modułów ESP32 w środowisku ESP-IDF (6). Interfejs I²C

Protokół I²C opracowany został jeszcze w latach 80. minionego wieku, ale wciąż jest stosowany w praktyce. Służy do wymiany danych (najczęściej w ramach jednej płytki drukowanej) pomiędzy układami scalonymi, wyposażonymi w interfejs pozwalający na podłączenie i komunikację za pomocą wspólnej magistrali. ESP32 wspiera protokół I²C od strony sprzętowej, zaś środowisko ESP-IDF zapewnia niezbędne wsparcie programistyczne.

Główne cechy protokołu I²C

Protokół I²C jest szeregowym, synchronicznym protokołem dwukierunkowej wymiany danych. Od strony elektrycznej magistrala składa się z dwóch linii: do przesyłania danych (oznaczanej najczęściej jako SDA) oraz zegarowych impulsów synchronizujących (oznaczanej jako SCL). Interfejsy wszystkich układów dołączonych do magistrali są typu otwarty dren, więc mogą aktywnie wymusić na magistrali jedynie stan niski. Z tego powodu linie SDA i SCL muszą być podciągane do napięcia zasilania za pomocą osobnych oporników. Układy podłączone do magistrali pełnią funkcję mastera (układu nadrzędnego) lub slave’a (układu podrzędnego). Na rysunku 1 pokazano schemat typowej magistrali I²C z podłączonymi układami.

Rysunek 1. Schemat typowej magistrali I²C z podłączonymi układami

Każdą transmisję – zarówno zapisu, jak i odczytu danych pomiędzy masterem a slave’em – zawsze inicjuje urządzenie nadrzędne. Przesył danych otwiera sekwencja START, po której wysyłany jest 7-bitowy (lub, w trybie rozszerzonym, 10-bitowy) identyfikator układu, do którego kierowana jest transmisja.

Poziom ostatniego, ósmego bitu identyfikatora określa, czy chodzi o zapis (poziom niski), czy odczyt (poziom wysoki). Urządzenie, którego identyfikator został wysłany, potwierdza swoją gotowość sygnałem ACK, czyli wymusza na linii SDA stan niski w czasie 9. taktu zegara. W następnej kolejności, w przypadku zapisu, wysyłane są przez mastera bajty danych. W przypadku odczytu to zaadresowane urządzenie podrzędne wysyła bajty danych w takt zegara SCL. Transmisję zamyka sekwencja STOP, po której magistrala powinna zostać zwolniona.

Interfejsy API sterowników I²C

ESP32 ma wbudowane dwa kontrolery sprzętowe odpowiedzialne za obsługę komunikacji za pośrednictwem magistrali I²C. Pojedynczy kontroler I²C może działać jako master lub slave. Do podłączenia do magistrali I²C służą predefiniowane porty: GPIO21 jako SDA i GPIO22 jako SCL, jednak można te funkcje zmienić programowo i wybrać inne linie wejścia/wyjścia.

ESP-IDF dostarcza bibliotekę driver/i2c.h, która steruje blokami peryferyjnymi I²C modułu ESP32. Najistotniejsze procedury biblioteki to:

i2c_param_config() – inicjalizacja sterownika I²C. Funkcja ma dwa parametry:

  • i2c_port_t – numer portu: I²C_NUM_0 lub I²C_NUM_1,
  • i2c_config_t – wskaźnik na strukturę zawierającą takie parametry konfiguracyjne, jak: tryb pracy, numery użytych portów GPIO do podłączania linii SDA i SCL, prędkość zegara. Przykład struktury można zobaczyć na listingu 1.
i2c_config_t conf = {
.mode = I²C_MODE_MASTER, //tryb pracy
.sda_io_num = 21, //numer GPIO połączony z SDA
.scl_io_num = 22, //numer GPIO połączony z SCL
.sda_pullup_en = GPIO_PULLUP_ENABLE, //włączenie wewnętrznego podciągania SDA
.scl_pullup_en = GPIO_PULLUP_ENABLE, //włączenie wewnętrznego podciągania SCL
.master.clk_speed = 400000, //szybkość zegara
};

Listing 1. Przykładowa definicja struktury i2c_config_t

i2c_driver_install() – instalacja sterownika. Funkcja ma 5 parametrów:

  • i2c_port_t – numer portu I²C,
  • i2c_mode_t – tryb pracy: master lub slave,
  • kolejne 2 parametry określają rozmiar buforów: nadawczego i odbiorczego. Jeżeli urządzenie będzie pracować w trybie mastera, mogą być one ustawione na 0,
  • ostatni parametr to flagi związane z przerwaniami. Jeżeli w obsłudze magistrali I²C nie korzystamy z przerwań, wartość parametru może wynosić 0.

i2c_master_write_to_device() – wysłanie danych do urządzenia podrzędnego. Funkcja może być wywoływana tylko w celu obsługi układów pracujących w trybie master i przyjmuje 5 parametrów:

  • i2c_port_t – numer portu I²C,
  • device_address – 7-bitowy adres urządzenia podrzędnego, do którego będą zapisywane bajty danych,
  • write_buffer – wskaźnik do bufora z bajtami do wysłania,
  • write_size – rozmiar bufora zapisu (wyrażony w bajtach),
  • ticks_to_wait – maksymalny czas oczekiwania na zakończenie przesyłania danych.

Funkcja jest dostępna, począwszy od wersji ESP-IDF v.4.4.

Alternatywnie można używać funkcji i2c_cmd_link_create(), i2c_master_start(), i2c_master_write_byte(), i2c_master_write(), i2c_master_stop(), i2c_master_cmd_begin(), i2c_cmd_link_delete().

i2c_master_read_from_device() – odczyt danych z urządzenia podrzędnego. Funkcja może być wywoływana tylko do obsługi układów pracujących w trybie master i ma 5 parametrów:

  • i2c_port_t – numer portu,
  • device_address – 7-bitowy adres urządzenia podrzędnego, z którego będą odczytywane bajty danych,
  • read_buffer – wskaźnik do bufora odczytywanych danych,
  • read_size – rozmiar bufora odczytu (w bajtach),
  • ticks_to_wait – maksymalny czas oczekiwania na zakończenie odczytu danych.

Funkcja jest dostępna od ESP-IDF v.4.4. Alternatywnie można używać funkcji i2c_master_start(), i2c_master_write(), i2c_master_read(), itd.

Przykład oprogramowania korzystającego z interfejsu I²C

W katalogu /esp-idf/esp-idf-v4.4/examples/peripherals/i2c znajdują się przykłady programów dla środowiska ESP-IDF v.4.4, które w swoim działaniu implementują obsługę interfejsu I²C. Program i2c_simple pokazuje, jak nawiązać komunikację z dołączonym do magistrali I²C układem MPU9250 i jak odczytać jego rejestr zawierający kod identyfikujący. W przykładzie i2c_self_test pokazano sposób odczytu danych z sensora BH1750, a dodatkowo zademonstrowano komunikację pomiędzy dwoma modułami ESP32 dołączonymi do magistrali współdzielonej z sensorem: jeden z modułów pełni funkcję nadrzędną, a drugi – podrzędną. Program i2c_tools zawiera zestaw narzędzi programistycznych pozwalających na: konfigurację portów GPIO ESP32 modułu dołączanych do magistrali I²C, skanowanie magistrali w poszukiwaniu dołączonych układów z interfejsem I²C, a także odczytywanie i zapis danych do rejestrów dostępnych za pośrednictwem I²C.

Jako przykład użycia biblioteki driver/i2c.h posłuży nam jeszcze inny program, dostępny w internecie pod adresem [1]. Jest to aplikacja prostego skanera dołączonych do magistrali układów z interfejsem I²C. Program pozwala wykryć adresy wszystkich układów, które potwierdzą sygnałem ACK wysłany adres odczytu. Program kolejno wysyła w pętli 7-bitowe adresy w zakresie od 1 do 127 i oczekuje na potwierdzenie. Jeżeli w odpowiedzi na wysłany adres pojawi się potwierdzenie (sygnał ACK), moduł wyśle na konsolę komunikat informujący o adresie wykrytego układu.

Na listingu 2 pokazano pliki nagłówkowe wszystkich dołączanych bibliotek, w tym najważniejszej dla nas w tym momencie biblioteki funkcji do obsługi interfejsu (i2c.h). Dodatkowo zdefiniowano porty GPIO 4 i 5 do obsługi linii SCL i SDA, numer użytego kontrolera (portu), a także szybkość zegara SCL (100 kHz). Zadeklarowano także fakt, że bufory transmisji Rx i Tx nie będą używane.

#include <stdio.h>
#include „driver/i2c.h”
#include „esp_log.h”

#define I²C_MASTER_SCL_IO 4 // Wybór pinu SCL
#define I²C_MASTER_SDA_IO 5 // Wybór pinu SDA
#define I²C_MASTER_NUM I²C_NUM_0 // Wybór portu I²C
#define I²C_MASTER_FREQ_HZ 100000 // Ustawienie częstotliwości zegara I²C frequency (100 kHz)
#define I²C_MASTER_TX_BUF_DISABLE 0 // Wyłączenie bufora TX
#define I²C_MASTER_RX_BUF_DISABLE 0 // Wyłączenie bufora RX

static const char *TAG = „i2c_scanner”;

Listing 2. Dołączenie plików nagłówkowych i definicja najważniejszych stałych (na podstawie: [1])

Listing 3 zawiera procedurę inicjalizacji obsługi magistrali I²C. Na początku procedury i2c_master_init() wywoływana jest funkcja i2c_param_config(), ustawiająca parametry sterownika. Kolejna funkcja i2c_driver_install() instaluje i inicjalizuje działanie kontrolera.

void i2c_master_init() {
// Konfiguracja ustawień mastera I²C
i2c_config_t conf;
conf.mode = I²C_MODE_MASTER;
conf.sda_io_num = I²C_MASTER_SDA_IO;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_io_num = I²C_MASTER_SCL_IO;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = I²C_MASTER_FREQ_HZ;
conf.clk_flags = 0;
// Zastosowanie konfiguracji do drivera I²C
i2c_param_config(I²C_MASTER_NUM, &conf);
// Instalacja drivera I²C
i2c_driver_install(I²C_MASTER_NUM, conf.mode, I²C_MASTER_RX_BUF_DISABLE, I²C_MASTER_TX_BUF_DISABLE, 0);
}

Listing 3. Ciało funkcji inicjalizującej kontroler I²C (na podstawie: [1])

Pokazana na listingu 4 funkcja i2c_scanner() realizuje podstawowe zadanie programu, którym jest wyszukiwanie dołączonych do magistrali I²C układów. Do wykonania tej operacji użyte zostały funkcje API realizujące kolejne kroki zaadresowania i uzyskania potwierdzenia od urządzenia podrzędnego.

void i2c_scanner() {
printf(„Scanning I²C bus...\n”);
// Iteracja po wszystkich możliwych adresach I²C
for (int addr = 1; addr < 127; addr++) {
// Utworzenie uchwytu co listy poleceń I²C
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
// Wysyłka adresu I²C z bitem zapisu
i2c_master_write_byte(cmd, (addr << 1) | I²C_MASTER_WRITE, true);
i2c_master_stop(cmd);

// Wykonanie poleceń I²C i sprawdzenie odpowiedzi
esp_err_t ret = i2c_master_cmd_begin(I²C_MASTER_NUM, cmd, pdMS_TO_TICKS(1000));
i2c_cmd_link_delete(cmd);

// Jeżeli urządzenie odpowiada, zwracamy jego adres do konsoli
if (ret == ESP_OK) {
printf(„Found device at address 0x%02x\n”, addr);
}
}
printf(„I²C scan complete.\n”);
}

Listing 4. Ciało funkcji realizującej skanowanie szyny I²C (na podstawie: [1])

i2c_cmd_link_create() – funkcja utworzenia i inicjalizacji listy poleceń I²C. W przypadku powodzenia zwracany jest uchwyt do bufora listy, zaś w przypadku wystąpienia błędu – wartość 0.

i2c_master_start() – polecenie wygenerowania sekwencji START. Parametrem jest uchwyt do bufora listy. Funkcja może być wywoływana tylko w przypadku urządzenia pracującego w trybie master.

i2c_master_write_byte() – funkcja wysyłania pojedynczego bajtu na magistralę I²C, wywoływana z trzema parametrami:

  • cmd_handle – uchwyt do bufora listy,
  • dane – wysyłany bajt danych. W omawianym przykładowym programie na pierwszych 7 bitach znajduje się adres urządzenia podrzędnego, a ostatni, ósmy bit przyjmuje poziom niski, wskazując, że mamy do czynienia z operacją zapisu,
  • ack_en – flaga włączenia oczekiwania na potwierdzenie ACK. W omawianym przykładzie parametr ma wartość 1 (true).

Funkcja może być wywoływana tylko w przypadku urządzenia pracującego w trybie master.

i2c_master_stop() polecenie wygenerowania sekwencji STOP. Parametrem jest uchwyt do bufora listy. Funkcja może być wywoływana tylko w odniesieniu do bloku skonfigurowanego jako master.

i2c_master_cmd_begin() funkcja wysyłania wszystkich poleceń I²C z listy, wywoływana z 3 parametrami:

  • i2c_num – numer portu I²C, który ma być zastosowany do transmisji (0 lub 1),
  • cmd_handle – uchwyt do bufora listy,
  • ticks_to_wait – maksymalna liczba taktów oczekiwania przed zgłoszeniem osiągnięcia limitu czasu.

Funkcja zwraca predefiniowaną wartość ESP_OK, jeżeli wszystkie polecenia zakończyły się powodzeniem. W przypadku omawianego przykładowego programu oznacza to odebranie potwierdzenia ACK od urządzenia podrzędnego.

Funkcja może być wywoływana tylko w odniesieniu do bloku I²C pracującego w trybie master.

i2c_cmd_link_delete() funkcja usuwania używanej listy poleceń, wywoływana z jednym parametrem:

  • cmd_handle – uchwyt do bufora listy do usunięcia.

Wszystkie polecenia wpisywane są na listę poleceń cmd_handle, następnie zostają one zrealizowane, po czym lista jest usuwana w pętli, której licznik stanowi adres wywoływanego urządzenia podrzędnego. Zakres adresu od 1 do 127 wyczerpuje wszystkie dostępne adresy urządzeń w trybie 7-bitowym, które mogą być podłączone do magistrali I²C. Jeżeli urządzenie jest fizycznie podłączone, zostanie wykryte, a na na konsolę trafi stosowny komunikat, po czym pętla przejdzie do poszukiwania urządzenia o kolejnym numerze. Po przetestowaniu wszystkich adresów na konsolę wysłane zostaje powiadomienie o tym fakcie, a program opuści funkcję i2c_scanner().

Listing 5 zawiera funkcję main(), odpowiedzialną za inicjalizację magistrali I²C i jednokrotne jej przeszukanie.

void app_main() {
// Inicjalizacja mastera I²C
i2c_master_init();
// Start skanowania magistrali I²C
i2c_scanner();
}

Listing 5. Ciało funkcji main (na podstawie: [1])

Efekt działania programu pokazany został na rysunku 2.

Rysunek 2. Efekt działania programu

Program wykrył, że do magistrali przyłączone zostały dwa układy o adresach 7-bitowych: 0x3C i 0x60. W celu polepszenia pewności działania programu wskazane jest podciągnięcie linii SDA i SCL do poziomu napięcia zasilającego przez dwa oporniki, np. 10 kΩ.

Ryszard Szymaniak, EP

[1] https://saludpcb.com/i2c-scanner-using-esp32-idf/

Artykuł ukazał się w
Elektronika Praktyczna
grudzień 2024
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik marzec 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec - kwiecień 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna marzec 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich kwiecień 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów