poniedziałek, 30 października 2017

[7] ESP32 - I2C RTC

W tym poście przedstawię sposób konfiguracji I2C w układzie ESP32 na przykładzie zegara RTC.  Przygotowany program został przetestowany z układami DS1307 oraz DS3231. Na obu działał on poprawnie. Dodatkowo do programu została dołączona funkcja odpowiedzialna za skanowanie szyny I2C w celu zdefiniowania adresów podłączonych urządzeń.

[Źródło: www.banggood.com]

Konfiguracja I2C:


ESP32 posiada dwa kontrolery I2C z pełnymi możliwościami. Oba oferują prędkość pracy zwykłą oraz przyspieszoną. Można je także skonfigurować do pracy w trybie master oraz slave. Do jednej magistrali może zostać podłączone do 127 urządzeń.

Ważnym elementem ESP32 jest możliwość prawie dowolnego wyboru pinów do komunikacji poprzez interfejs I2C. Nie są one przypisane na stałe, wybiera się jej w momencie deklaracji struktury dla I2C. W przypadku wybrania pinu, który nie może zostać wybrany nastąpi wywołanie błędu podczas inicjalizacji sterownika.

Główna struktura wygląda następująco:

  1. typedef struct{
  2.   i2c_mode_t mode
  3.   gpio_num_t sda_io_num;
  4.   gpio_pullup_t sda_pullup_en;
  5.   gpio_num_t scl_io_num;
  6.   gpio_pullup_t scl_pullup_en;
  7.   union {
  8.     struct {
  9.       uint32_t clk_speed;
  10.     } master;
  11.     struct {
  12.       uint8_t addr_10bit_en;
  13.       uint16_t slave_addr;
  14.     } slave;
  15.   };
  16. } i2c_config_t;

Do struktury można wprowadzić następujące informacje:

  • mode - określenie trybu pracy I2C, czy ma działać jako master czy jako slave. Czyli wprowadza się parametr I2C_MODE_MASTER lub I2C_MODE_SLAVE
  • sda_io_num - pin podłączony pod linie danych
  • scl_io_num - pin podłączony pod linie zegara
  • sda_pullup_enable - pozwala na włączenie bądź wyłączenie rezystora podciągającego dla linii SDA
  • scl_pullup_enable - pozwala na włączenie bądź wyłączenie rezystora podciągającego dla linii SCL
  • clk_speed - podaje się prędkość w hz dla zwykłej szybkości podaje się prędkość 100kHz natomiast dla truybu szybkiego wynosi 400kHz.

W przypadku trybu slave dodatkowo są dwa pola do wprowadzenia czyli:

  • addr_10bit_en - definiuje czy wykorzystywany jest 10 bitowy adres zamiast 7 bitowego. Jeśli wartość wynosi 1 to funkcja jest uruchomiona.
  • slave_addr - adres urządzenia kiedy jest w trybie slave.

Poniżej przedstawię dwie funkcje, które będą odpowiedzialne za uruchomienie I2C oraz drugą, bardziej do debugowania, która pozwoli na ustalić adres urządzeń jakie są podłączone na linii.

  1. void configI2C(void)
  2. {
  3.     i2c_config_t config;
  4.     config.mode = I2C_MODE_MASTER;
  5.     config.sda_io_num = SDA_PIN;
  6.     config.scl_io_num = SDL_PIN;
  7.     config.sda_pullup_en = GPIO_PULLUP_ENABLE;
  8.     config.scl_pullup_en = GPIO_PULLUP_ENABLE;
  9.     config.master.clk_speed = 100000;
  10.     ESP_ERROR_CHECK(i2c_param_config(I2C_NUM_0, &config));
  11.     ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0));
  12. }


W funkcji powyżej wywołuje się strukturę odpowiedzialną za przechowywanie danych do komunikacji. Po czym wprowadza się wymagane dane. Funkcja i2c_param_config pobiera dwa parametry. Pierwszy określa port I2C, tutaj do wyboru jest pierwszy lub drugi port I2C. Następnie uruchamia się sterownik i2c_driver_install. Podaje się do niej numer portu, trybu pracy, a następnie jeśli działa jako slave rozmiary i adresy buforów.

Komunikacja układu master z slave odbywa się w następujący sposób. Najpierw tworzony jest link (i2c_cmd_link_create). Zawiera on listę komend, zadań jakie mają być wykonywane podczas komunikacji z układem podrzędnym.

Na samym początku przesyłana jest sekwencja startu. Po czym przesyłany jest 7 bitowy adres. Następnie wysyłany jest kod operacji zapisu albo odczytu (0 dla zapisu, 1 dla odczytu). Jeśli zostały wysłane pełne 8 bitów w jednym kierunku, to urządzenie podrzędne wysyła ono do mastera potwierdzenie ACK bądź brak potwierdzenia NACK.

W funkcjach ustawia się wartość true, która informuje że przesyłany będzie ACK, wartość false określa o braku potwierdzenia NACK.

Rozpoczęcie przesyłania danych następuje od komendy i2c_master_cmd_begin(), która jest funkcją blokującą. Jako parametr podaje się numer kontrolera. Następnie podawana jest lista komend po czym maksymalny czas oczekiwania (1000/ portTICK_RATE_MS). Wynosi ona jedną sekundę.

Funkcja pozwalająca na pobranie adresu urządzenia I2C:

  1. void i2cScanner(void)
  2. {
  3.     printf("i2c scanner start\r\n");
  4.     configI2C();
  5.     int devicesFounded = 0;
  6.     for(int address = 1; address < 127; address++)
  7.     {
  8.         i2c_cmd_handle_t cmd = i2c_cmd_link_create();
  9.         i2c_master_start(cmd);
  10.         i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, 1);
  11.         i2c_master_stop(cmd);
  12.         if(i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS) == ESP_OK) {
  13.             printf("-- address 0x%02x conneted to line\r\n", address);
  14.             devicesFounded++;
  15.         }
  16.         i2c_cmd_link_delete(cmd);
  17.     }
  18.     if(devicesFounded == 0)
  19.     {
  20.         printf("no devices found\r\n");
  21.     }
  22.     printf("Scan completed\r\n");
  23. }

Funkcja powyżej uruchamia konfigurację I2C. Po czym definiuje się zmienną przechowującą ilość znalezionych urządzeń. Następnie pętla wysyła informacje po adresach o 1 do 127 tak aby uzyskać odpowiedź od podłączonych urządzeń. Jeśli znajdzie się urządzenie to jego adres zostanie wypisany na terminalu.

Komunikacja z RTC:


Tutaj potrzebne będzie przygotowanie funkcji odczytującej dane, zapisującej dane, przetwarzającej odebrane informacje oraz zamieniające odebrane dane z formatu BCD na dziesiętny oraz odwrotnie.

Zapis danych do układu:

  1. void writeValue(time_t newTime)
  2. {
  3.     struct tm tm;
  4.     gmtime_r(&newTime, &tm);
  5.     esp_err_t errRc;
  6.     i2c_cmd_handle_t cmd = i2c_cmd_link_create();
  7.     ESP_ERROR_CHECK(i2c_master_start(cmd));
  8.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, (addressI2C<<1) | I2C_MASTER_WRITE, 1 /* expect ack */));
  9.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x0, 1));
  10.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_sec), 1));        // seconds
  11.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_min), 1 ));       // minutes
  12.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_hour), 1 ));      // hours
  13.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_wday+1), 1 ));    // week day
  14.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_mday), 1));       // date of month
  15.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_mon), 1));        // month
  16.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(tm.tm_year-100), 1));   // year
  17.     ESP_ERROR_CHECK(i2c_master_stop(cmd));
  18.     errRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000);
  19.     if (errRc != 0) {
  20.         ESP_LOGE(TAG, "i2c_master_cmd_begin: %d", errRc);
  21.     }
  22.     i2c_cmd_link_delete(cmd);
  23. }

Tutaj wprowadzane są dane zawierające od początku sekundy, minuty, godziny po czym dzień tygodnia zwiększony o jeden, dzień w miesiącu, miesiąc oraz rok z przesunięciem o 100.

Lub można ustawić czas bez korzystania z wewnętrznego RTC:

  1. void writeValueString(uint8_t hour, uint8_t minutes, uint8_t seconds, uint8_t wday, uint8_t moday, uint8_t month, uint8_t year)
  2. {
  3.     esp_err_t errRc;
  4.  
  5.     i2c_cmd_handle_t cmd = i2c_cmd_link_create();
  6.  
  7.     ESP_ERROR_CHECK(i2c_master_start(cmd));
  8.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, (addressI2C<<1) | I2C_MASTER_WRITE, 1 /* expect ack */));
  9.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x0, 1));
  10.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(seconds), 1));      // seconds
  11.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(minutes), 1 ));     // minutes
  12.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(hour), 1 ));        // hours
  13.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(wday+1), 1 ));      // week day
  14.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(moday), 1));        // date of month
  15.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(month), 1));        // month
  16.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, intToBCD(year), 1));         // year
  17.     ESP_ERROR_CHECK(i2c_master_stop(cmd));
  18.     errRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000);
  19.     if (errRc != 0) {
  20.         ESP_LOGE(TAG, "i2c_master_cmd_begin: %d", errRc);
  21.     }
  22.     i2c_cmd_link_delete(cmd);
  23. }

Odczytanie danych:

  1. static time_t readValue(void)
  2. {
  3.     uint8_t data[7] = {0};
  4.     uint8_t datar[1] = {0};
  5.     clearDS3231Buffer();
  6.     i2c_cmd_handle_t cmd = i2c_cmd_link_create();
  7.     printf("x...\r\n");
  8.     ESP_ERROR_CHECK(i2c_master_start(cmd));     /* Start */
  9.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, (addressI2C<<1) | I2C_MASTER_WRITE, 1 /* expect ack */));
  10.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x0, 1));
  11.     ESP_ERROR_CHECK(i2c_master_start(cmd));
  12.     ESP_ERROR_CHECK(i2c_master_write_byte(cmd, (addressI2C<<1) | I2C_MASTER_READ, 1 /* expect ack */));
  13.     ESP_ERROR_CHECK(i2c_master_read(cmd, data, 7, 0));
  14.     ESP_ERROR_CHECK(i2c_master_read(cmd, datar, 1, 1));
  15.     ESP_ERROR_CHECK(i2c_master_stop(cmd));
  16.     ESP_ERROR_CHECK(i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000/portTICK_PERIOD_MS));
  17.     i2c_cmd_link_delete(cmd);
  18.     int i;
  19.     printf("Printf data: \n");
  20.     for (i=0; i<7; i++) {
  21.         printf("[%d]:%2x;", i, data[i]);
  22.         //ESP_LOGD(TAG, "%d: 0x%.2x", i, data[i]);
  23.     }
  24.     struct tm tm;
  25.     tm.tm_sec  = bcdToInt(data[0]);
  26.     tm.tm_min  = bcdToInt(data[1]);
  27.     tm.tm_hour = bcdToInt(data[2]);
  28.     tm.tm_mday = bcdToInt(data[4]);
  29.     tm.tm_mon  = bcdToInt(data[5]);
  30.     tm.tm_year = bcdToInt(data[6]);
  31.     time_t readTime = mktime(&tm);
  32.     DS3231_Time_t.year          = bcdToInt(data[6]);
  33.     DS3231_Time_t.month         = bcdToInt(data[5]);
  34.     DS3231_Time_t.dayInMonth    = bcdToInt(data[4]);
  35.     DS3231_Time_t.day           = bcdToInt(data[3]);
  36.     DS3231_Time_t.hours         = bcdToInt(data[2]);
  37.     DS3231_Time_t.minutes       = bcdToInt(data[1]);
  38.     DS3231_Time_t.seconds       = bcdToInt(data[0]);
  39.     printf("Sec:%u, Minut:%u, Hour:%u, DayMonth:%u, Month:%u, Year:%u\r\n",
  40.             DS3231_Time_t.seconds, DS3231_Time_t.minutes, DS3231_Time_t.hours,
  41.             DS3231_Time_t.dayInMonth, DS3231_Time_t.month, DS3231_Time_t.year);
  42.     return readTime;
  43. }

W przypadku odczytu danych dane umieszczam w dwóch strukturach. Jedna przechowująca czas w RTC systemowym czyli wewnętrznym dla układu ESP oraz drugim zdefiniowanym w programie, który przechowuje jedynie odczytaną wartość.

Zmiana formatu danych z całkowitego na BCD(kod dwójkowo-dziesiętny). W takim formacie należy dane przesłać do RTC. Dane odczytane są w formacie BCD w związku z czym należy je zmienić na całkowite.

  1. static inline uint8_t intToBCD(uint8_t num)
  2. {
  3.     return ((num / 10) << 4) | (num % 10);
  4. }
  5. static inline uint8_t bcdToInt(uint8_t bcd)
  6. {
  7.     return ((bcd >> 4) * 10) + (bcd & 0x0f);
  8. }

Pliki z projektem można znaleźć na dysku Google pod tym linkiem.