Programowanie modułów ESP32 w środowisku ESP-IDF (5). Interfejs SPI

Programowanie modułów ESP32 w środowisku ESP-IDF (5). Interfejs SPI

Moduły ESP32 wyposażone zostały w kilka różnych rodzajów interfejsów szeregowych do wymiany danych. Jednym z nich jest SPI. Ten szybki blok komunikacyjny pozwala na transmisję danych do/z takich układów jak pamięci, przetworniki czy moduły wykonawcze, za pośrednictwem 4-przewodowej magistrali.

Przypomnienie podstawowych informacji o SPI

SPI to skrót od Serial Peripheral Interface. Do magistrali dołączane są równolegle układy z interfejsem SPI, z których jeden zawsze pełni funkcję nadrzędną (master) a pozostałe – podrzędną (slave). Magistrala składa się z czterech linii. Linią MOSI przesyłane są szeregowo dane od mastera do slave’a, a linią MISO – od slave’a do mastera. Na linię SCL podawane są impulsy zegara, który synchronizuje transmisję danych. Linia Slave Select (SS) służy do wybierania aktywnego urządzenia podrzędnego (slave). Połączenia pomiędzy urządzeniem nadrzędnym i podrzędnym oraz kierunki przepływu sygnałów pokazane zostały na rysunku 1. Przesył danych zawsze inicjuje master, wystawiając na linii SCL impulsy zegara i wybierając poziomem napięcia na wyprowadzeniu SS odpowiednie urządzenie podrzędne. Po zakończeniu przesyłania wszystkich bitów transmisji master dezaktywuje linię SS. Może potem przeprowadzić nowy cykl wymiany danych z tym samym urządzeniem lub aktywować inną linię SS, jeżeli do magistrali podłączone są jeszcze inne slave’y.

Rysunek 1.

Moduły ESP32 mają 4 niezależne sterowniki SPI, przy czym dwa pierwsze (SPI0 i SPI1) używane są przez system do dostępu do wewnętrznej pamięci podręcznej FLASH i nie powinny być wykorzystywane przez oprogramowanie użytkownika. Natomiast dwa pozostałe sterowniki HSPI (SPI2) i VSPI (SPI3) mogą być używane i konfigurowane do pracy w trybie nadrzędnym (master) lub podrzędnym (slave). Stabilna szybkość transmisji sięga 10 MB/s.

Interfejsy API sterowników SPI

ESP-IDF dostarcza biblioteki, które sterują urządzeniami peryferyjnymi SPI modułu ESP32. W przypadku trybu slave jest to biblioteka driver/spi_slave.h, której najistotniejsze procedury to:

spi_slave_initialize() – inicjalizcja sterownika SPI jako urządzenia podrzędnego. Funkcja ma cztery parametry:

  • host – wybór bloku SPI; parametr może przyjąć wartość SPI2_HOST lub SPI3_HOST,
  • bus_config – wskaźnik na strukturę spi_bus_config_t która służy między innymi do przypisania funkcji linii magistrali SPI do portów GPIO,
  • slave_config – wskaźnik na strukturę spi_slave_interface_config_t, zawierającą szczegóły interfejsu podrzędnego,
  • dma_chan – wybór kanału DMA; dopuszczalne wartości to SPI_DMA_DISABLED i SPI_DMA_CH_AUTO.

spi_slave_transmit() wykonuje transakcję przez urządzenie podrzędne. Funkcja ma trzy parametry:

  • host – numer sterownika SPI,
  • trans_desc – wskaźnik na zmienną typu spi_slave_transaction_t, która zawiera opis wykonywanej transakcji, między innymi rozmiar danych (liczbę bitów) i wskaźniki na bufory, odbiorczy i nadawczy,
  • ticks_to_wait – limit czasu oczekiwania na zakończenie transakcji.

Do obsługi trybu master przeznaczona jest biblioteka driver/spi_master.h, której najistotniejsze procedury to:

spi_bus_initialize() inicjalizacja sterownika w trybie master. Funkcja ma trzy parametry:

  • host_id – parametr może przyjąć wartość SPI2_HOST lub SPI3_HOST,
  • bus_config – wskaźnik na strukturę typu spi_bus_config_t. Zmienne struktury konfigurują wyprowadzenia GPIO jako line MISO, MOSI, CLK. Przypisanie wartości –1 oznacza, że dana linia magistrali nie będzie używana. Zmienna max_transfer_sz określa maksymalny rozmiar transmisji w bajtach. Na listingu 1 pokazano przykładową inicjalizację struktury spi_bus_config_t.
  • dma_chan – jest to zmienna wskazująca, który kanał DMA ma być używany podczas transmisji. DMA (Direct Memory Access) to mechanizm umożliwiający instancji SPI bezpośredni dostęp do pamięci RAM jako bufora transferu. Można wybrać pomiędzy SPI_DMA_DISABLED, SPI_DMA_CH1, SPI_DMA_CH2, SPI_DMA_CH_AUTO.
spi_bus_config_t buscfg={
.miso_io_num = -1,
.mosi_io_num = 32,
.sclk_io_num = 33,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};

Listing 1.

spi_bus_add_device() – rejestracja urządzenia podrzędnego. Funkcja ma 3 parametry:

  • host_id – taka sama wartość, jaką podano w spi_bus_initialize(),
  • dev_config – wskaźnik na stałą strukturę typu spi_device_interface_config_t. Struktura podaje informacje o urządzeniu podrzędnym, takie jak tryb SPI, w którym pracuje, prędkość zegara urządzenia podrzędnego, numer portu GPIO, który ma być wykorzystywany do sterowania wyprowadzeniem SS urządzenia podrzędnego itd. Na listingu 2 pokazano przykładową inicjalizację struktury spi_device_interface_config_t.
  • handle – wskaźnik do zmiennej typu spi_device_handle_t. Po wywołaniu spi_bus_add_device zwrócony zostanie uchwyt pozwalający użyć zmiennej do odwołania się do bieżącego urządzenia podrzędnego.
spi_device_interface_config_t devcfg={
.clock_speed_hz = 1000000, // 1 MHz
.mode = 0, //SPI mode 0
.spics_io_num = 25, // CS Pin
.queue_size = 1,
.flags = SPI_DEVICE_HALFDUPLEX,
.pre_cb = NULL,
.post_cb = NULL,
};

Listing 2.

spi_device_polling_transmit() – przesyłanie danych do urządzenia podrzędnego SPI metodą odpytywania (pollingu). Funkcja ma 2 parametry:

  • handle – wartość typu spi_device_handle_t, odnosząca się do urządzenia podrzędnego i uzyskana po wcześniejszym wywołaniu funkcji spi_bus_add_device(),
  • trans_desc – wskaźnik do zmiennej typu spi_transaction_t zawierającej parametry danych do przesłania. Listing 3 to przykład wpisu do struktury spi_transaction_t, ustawianej przed wysłaniem do urządzenia podrzędnego 2 bajtów danych.
uint8_t data[2] = { 0x01, 0x02 };
spi_transaction_t t = {
.tx_buffer = data,
.length = 2 * 8
};

Listing 3.

W przypadku programów wielowątkowych procedura spi_device_polling_transmit() nie jest bezpieczna i może prowadzić do zakłócenia transmisji SPI. Korzystając z trybu odpytywania, lepiej w takim przypadku posłużyć się procedurami spi_device_polling_start() i spi_device_polling_end().

Przykład programu transmisji SPI do urządzenia podrzędnego

W katalogach przykładów firmowych dostępnych w ramach środowiska IDF:

esp-idf/esp-idf-v4.4/examples/peripherals/spi_master
esp-idf/esp-idf-v4.4/examples/peripherals/spi_slave

znaleźć można przykłady oprogramowania korzystającego z bibliotek sterowników SPI pracujących zarówno w trybie nadrzędnym, jak i podrzędnym. Jest tam m.in. przykład komunikacji pomiędzy pamięcią EEPROM a modułem ESP32 czy też sterowania wyświetlaczem graficznym LCD wyposażonym w interfejs SPI. Nie zabrakło także klasycznego przykładu komunikacji dwóch modułów ESP32 poprzez magistralę SPI. Niestety, są to jednak przykłady rozbudowane i wymagające sporej uwagi do zrozumienia ich działania.

Dlatego też jako przykład transmisji z modułu ESP32 do urządzenia podrzędnego wybrałem kod znaleziony w Internecie [1]. Jest to proste oprogramowanie do komunikacji z układem MAX7219, użytym do sterowania matrycą diod LED 8×8. Listing 4 zawiera pliki nagłówkowe wszystkich niezbędnych bibliotek użytych w omawianym przykładzie. Oprócz tego zdefiniowane są porty GPIO używane do współpracy z magistralą SPI (CS_PIN to port sterowania linią wyboru urządzenia podrzędnego, odpowiadający w ramach przyjętej wcześniej nomenklatury skrótowi SS). Zdefiniowano także spi2, czyli HSPI jako używany w programie sterownik sprzętowy SPI.

#include <stdio.h>
#include „esp_log.h”
#include „freertos/FreeRTOS.h”
#include „freertos/task.h”
#include „driver/spi_master.h”

#define CLK_PIN 33
#define MOSI_PIN 32
#define CS_PIN 25

#define DECODE_MODE_REG 0x09
#define INTENSITY_REG 0x0A
#define SCAN_LIMIT_REG 0x0B
#define SHUTDOWN_REG 0x0C
#define DISPLAY_TEST_REG 0x0F

spi_device_handle_t spi2;

Listing 4.

Listing 5 zawiera procedurę inicjalizacji sterownika w trybie master. Zwróćmy uwagę, że linia magistrali MISO nie jest inicjalizowana, ponieważ żadne dane nie będą odczytywane z urządzenia podrzędnego, czyli układu MAX7219. Szybkość zegara magistrali SPI jest ustawiona na „bezpieczną” wartość 1 MHz, dzięki czemu całość będzie działała poprawnie nawet przy połączeniach o przeciętnej jakości pod względem integralności sygnałów (np. na płytce stykowej).

static void spi_init() {
esp_err_t ret;

spi_bus_config_t buscfg={
.miso_io_num = -1,
.mosi_io_num = MOSI_PIN,
.sclk_io_num = CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};

ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret);

spi_device_interface_config_t devcfg={
.clock_speed_hz = 1000000, // 1 MHz
.mode = 0, //SPI mode 0
.spics_io_num = CS_PIN,
.queue_size = 1,
.flags = SPI_DEVICE_HALFDUPLEX,
.pre_cb = NULL,
.post_cb = NULL,
};

ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &spi2));
};

Listing 5.

Listing 6 zawiera procedury związane z obsługą układu MAX7219. Za pośrednictwem funkcji write_reg wysyłane są do rejestrów MAX7219 dane sterujące. Do tego celu używamy procedury transmisji do urządzenia podrzędnego spi_device_polling_transmit, przesyłającej magistralą SPI bajt adresu rejestru i bajt danych. Pozostałe funkcje zawarte na listingu 6 związane są już stricte ze sterowaniem MAX7219.

static void write_reg(uint8_t reg, uint8_t value) {
uint8_t tx_data[2] = { reg, value };

spi_transaction_t t = {
.tx_buffer = tx_data,
.length = 2 * 8
};

ESP_ERROR_CHECK(spi_device_polling_transmit(spi2, &t));
}

static void set_row(uint8_t row_index) {
write_reg(row_index + 1, 0xFF);
}

static void set_col(uint8_t col_index) {
for (int i = 0; i < 8; i++) {
write_reg(i + 1, 0x01 << col_index);
}
}

static void clear(void) {
for (int i = 0; i < 8; i++) {
write_reg(i + 1, 0x00);
}
}

static void max7219_init() {
write_reg(DISPLAY_TEST_REG, 0);
write_reg(SCAN_LIMIT_REG, 7);
write_reg(DECODE_MODE_REG, 0);
write_reg(SHUTDOWN_REG, 1);
clear();
}

Listing 6.
void app_main(void)
{
spi_init();
max7219_init();

while (1) {
for (int i = 0; i < 8; i++) {
clear();
set_row(i);
vTaskDelay(1000/portTICK_PERIOD_MS);
}

for (int i = 0; i < 8; i++) {
clear();
set_col(i);
vTaskDelay(1000/portTICK_PERIOD_MS);
}
}
}

Listing 7.

Ostatni listing 7 zawiera główną procedurę main, odpowiedzialną za inicjalizację sterownika SPI i układu MAX7219, a następnie – w nieskończonej pętli – odświeżającą stan matrycy LED.

Ryszard Szymaniak, EP

[1] https://embeddedexplorer.com/esp32-spi-master/

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

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik luty 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio styczeń - luty 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje styczeń - luty 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna luty 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich luty 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów