BLE i nRF5340: Twoja własna charakterystyka
February 17, 2024

W tym artykule pokażemy jak można szybko wystartować z implementacją podstawowej komunikacji BLE, wykorzystując procesor nRF5340 firmy Nordic Semiconductor, na płytce deweloperskiej nRF5340-DK.

Przygotowanie projektu

Nasze przygodę zaczynamy od zestawienia środowiska z nRF Connect SDK i Visual Studio Code, korzystając z rozszerzenia od Nordic. Instrukcja uruchomienia dostępna jest w oficjalnym przewodniku startowym (Get Started). Projekt rozpoczniemy od klasycznego "mrugania diodą" przy użyciu przykładu Blinky. Możemy go pobrać bezpośrednio ze strony lub utworzyć klikając Create a new application w menu startowym rozszerzenia nRF Connect i wybierając Blinky Sample z listy.

Menu startowe w rozszerzeniu nRF Connect dla VS Code

Projekt Blinky idealnie nadaje się na początek naszej przygody, nie tylko dlatego, że zawiera kluczowe elementy potrzebne do startu, ale również umożliwia sprawdzenie, czy nasze środowisko programistyczne jest odpowiednio skonfigurowane i czy nasz hardware działa bez zarzutu.
Aby ruszyć z budową naszej aplikacji, musimy jeszcze ustawić konfigurację, określając, dla której płytki rozwijamy kod. Robimy to, wybierając opcję Create new build configuration z menu Applications i zaznaczając nrf5340dk_nrf5340_cpuapp na liście dostępnych płyt w sekcji Board. Resztę ustawień pozostawiamy bez zmian i finalizujemy proces, klikając Build Configuration.

Fragment menu do tworzenia nowej konfiguracji dla aplikacji w VS Code

Od tego momentu otwiera się przed nami droga do swobodnego edytowania kodu, kompilowania aplikacji oraz jej wgrywania bezpośrednio do mikrokontrolera. Wszystko to dzięki debuggerowi na płytce, który łączy się z naszym środowiskiem za pośrednictwem interfejsu USB.

Widok gotowego VSC

Wybierając opcję Flash z menu Actions wgrywamy aplikację na płytkę, a następnie otwieramy terminal VCOM1, w którym wyświetlane będą wszystkie logi wysyłane funkcją printk. Jeśli wszystko przebiegło zgodnie z naszymi oczekiwaniami, jedna z czterech dostępnych na płycie nRF5340-DK diod LED zacznie mrugać, co sekundę przełączając się między stanami.

Uruchomienie podstawowej komunikacji

Teraz wprowadzimy kilka zmian w kodzie, które pozwolą nam uruchomić rozgłaszanie domyślnych pakietów "rozgłoszeniowych" BLE. Zaczynamy od dodania poniższych linii do pliku prj.conf:

CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="nRF5340 DK test"

Opcje te staną się częścią konfiguracji Kconfig, na którym oparte jest budowanie systemu Zephyr.

  • CONFIG_BT odblokowuje wsparcie dla komunikacji Bluetooth,
  • CONFIG_BT_PERIPHERAL konfiguruje sprzęt do pracy w trybie urządzenia peryferyjnego (Peripheral Device),
  • CONFIG_BT_DEVICE_NAME pozwala ustawić nazwę, która widoczna będzie podczas skanowania, wykoywanego przez urządzenia centralne (Central Device).

Przechodzimy teraz do main.c, gdzie w pierwszej kolejności dodajemy poniższe pliki nagłówkowe:

#include <zephyr/bluetooth/uuid.h> #include <zephyr/bluetooth/gatt.h>

Następnie, nad funkcją int main(void), dodajemy dwie nowe struktury:

static const struct bt_data adv_data[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)) } static void connected(struct bt_conn *conn, uint8_t err) { if (err) { printk("Connection failed (err 0x%02x)\n", err); } else { printk("Connected\n"); } } static void disconnected(struct bt_conn *conn, uint8_t reason) { printk("Disconnected (reason 0x%02x)\n", reason); } BT_CONN_CB_DEFINE(conn_callbacks) = { .connected = connected, .disconnected = disconnected };

Pierwsza z nich, czyli tablica adv_data, zawiera konfigurację pakietów rozgłoszeniowych (advertising). Z kolei conn_callbacks przechowuje wskaźniki na funkcje wywoływanie w momencie nawiązania połączenia oraz tuż po jego zerwaniu.

Na koniec, wewnątrz funkcji int main(void), nad pętlą while dodajemy:

ret = bt_enable(NULL); if (ret) { printk("Bluetooth init failed (err %d)\n", ret); return 0; } ret = bt_le_adv_start(BT_LE_ADV_CONN_NAME, adv_data, ARRAY_SIZE(adv_data), NULL, 0); if (ret) { printk("Advertising failed to start (err %d)\n", ret); } else { printk("Advertising successfully started\n"); }

Funkcja bt_enable inicjalizuje ustawienia i uruchamia komunikację Bluetooth, a bt_le_adv_start rozpoczyna rozsyłanie pakietów rozgłoszeniowych, zgodnie ze wskazanymi parametrami.

Test połączenia w aplikacji mobilnej

Po wgraniu aplikacji, nasz procesor powinien rozgłaszać swoją obecność dla innych urządzeń w pobliżu. Najłatwiej możemy to zweryfikować korzystając z telefonu wyposażonego w BLE i aplikacji nRF Connect.

Android vs iOS

Aplikacja dla systemów iOS wygląda zupełnie inaczej niż dla Androida, ale funkcjonalność jest dość zbliżona. W zakładce Scanner uruchamiamy skanowanie i po chwili powinniśmy na liście zobaczyć nasze urządzenie, z wcześniej zdefiniowaną nazwą. Po kliknięciu na przycisk Connect nawiązane zostanie połączenie i powinniśmy zobaczyć w aplikacji atrybuty Generic.

Android vs iOS

Dodajemy własną charakterystykę

W tej części dodamy własną charakterystykę, która pozwoli nam sterować drugą diodą dostępną na naszej płytce.
Ponownie zaczynamy od pliku prj.conf, gdzie dodajemy opcję, która umożliwia konfigurowanie urządzenia jako klienta GATT (Generic Attribute Profile):

CONFIG_BT_GATT_CLIENT=y

Wracając do main.c, w pierwszej kolejności dodajemy dodatkowe pliku nagłówkowe:

#include <zephyr/bluetooth/uuid.h> #include <zephyr/bluetooth/gatt.h>

Następnie duplikujemy kod odpowiedzialny za konfigurację diody LED0 i wykorzystujemy go do konfiguracji diody led1:

#define LED0_NODE DT_ALIAS(led0) #define LED1_NODE DT_ALIAS(led1) ... static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); ... if (!gpio_is_ready_dt(&led)) { return 0; } ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); if (ret < 0) { return 0; } if (!gpio_is_ready_dt(&led1)) { return 0; } ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT_INACTIVE); if (ret < 0) { return 0; }

Sterowaine diodą będzie wymagało dwóch funkcji z charakterystycznymi nagłówkami, led_read oraz led_write:

static uint8_t led1_state = 0; static ssize_t led_read( struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { printk("Led read: %d", led1_state); return bt_gatt_attr_read(conn, attr, buf, len, offset, &led1_state, sizeof(led1_state)); } static ssize_t led_write( struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { led1_state = *(uint8_t*)buf; gpio_pin_set_dt(&led1, led1_state); printk("Led write: %d", led1_state); return len; }

W tym momencie powinniśmy zdefiniować uniwersalny niepowtarzalny numer identyfikacyjny (UUID) dla naszej charakterystyki oraz dla serwisu, do którego będzie ona przypisana. Na potrzeby testu możemy wpisać dowolny ciąg liczb, jednak na przyszłość polecamy korzystanie ze specjalnego generatora. UUID wpisujemy do naszego kodu z użyciem makr kodujących:

#define BT_UUID_CUSTOM_SERVICE_VAL \ BT_UUID_128_ENCODE(0x11111111, 0x2222, 0x3333, 0x4444, 0x123400000000) #define BT_UUID_LED_CHRC_VAL \ BT_UUID_128_ENCODE(0x11111111, 0x2222, 0x3333, 0x4444, 0x123400000001)

Pora na najważniejszą część prezentowanego kodu, czyli definicję naszej własnej charakterystyki. Tutaj również z pomocą przychodzą nam specjalne makra, dzięki którym w prosty sposób w jednym miejscu definiujemy wszystkie potrzebne parametry:

BT_GATT_SERVICE_DEFINE(custom_service, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(BT_UUID_CUSTOM_SERVICE_VAL)), BT_GATT_CHARACTERISTIC( BT_UUID_DECLARE_128(BT_UUID_LED_CHRC_VAL), BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, led_read, led_write, NULL) );

Ostatnim krokiem jest dodanie do wcześniej zdefiniowanej tablicy adv_data dodatkowego elementu, który zagwarantuje widoczność naszego własnego serwisu (wraz z charakterystyką) dla innych urządzeń:

static const struct bt_data adv_data[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_CUSTOM_SERVICE_VAL) };

Test działania charakterystyki

Podobnie jak we wcześniejszym teście, łączymy się z przez aplikację na telefonie, jednak tym razem powinniśmy w widoku Client zobaczyć dodatkowy serwis i naszą charakterystykę:

Android vs iOS

Nie pozostaje nam nic innego jak sprawdzić sterowanie diodą. Klikając strzałkę w dół, znajdująca się po prawej stronie charakterystyki powinniśmy otrzymać w polu Value stan diody. Natomiast klikając na strzałkę w górę, mamy możliwość wysłania wartości unsigned int, gdzie wartość 1 powinna natychmiast załączyć diodę.

Podsumowanie

W artykule pokazaliśmy jak szybko można zacząć pracę z komunikacją BLE, jeśli zdecydujemy się skorzystać z platformy opartej na procesorze nRF5340 i narzędziach zawartych w nRF Connect SDK.

Referencje

#YourOwnCharacteristicInBLE #GoodByte