Kurs Nordic nRF z BT (3). Przyciski i wielowątkowość

Kurs Nordic nRF z BT (3). Przyciski i wielowątkowość

W ostatniej części kursu pokazaliśmy, jak środowisko nRF Connect wspiera użytkownika w analizowaniu działania aplikacji. Tym razem skoncentrujemy się na systemie Zephyr, zintegrowanym z zainstalowanym SDK. Pokażemy, jak utworzyć własny wątek i przypisać mu konkretne zadania do wykonania.

Przyciski

Do tej pory w naszym kursie omówiliśmy interfejsy wyjściowe, takie jak klasyczny LED oraz rozbudowany moduł logowania. Zanim przejdziemy do bardziej zaawansowanych zagadnień, uruchomimy prosty interfejs wejściowy. Będzie to zestaw 4 przycisków zamontowanych na płytce nRF5340 DK, z których każdy jest podłączony do osobnego pinu.

Zaczynamy od utworzenia nowego, pustego projektu i sprawdzenia konfiguracji przycisków w devicetree (listing 1). Konfiguracja składa się z listy przycisków, umieszczonych w bloku kompatybilnym ze wskazaniem „gpio-keys”. Każdy przycisk ma swoje pole gpios, w którym określamy pin oraz dodatkowe parametry.

buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 23 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 1";
zephyr,code = <INPUT_KEY_0>;
};
button1: button_1 {
gpios = <&gpio0 24 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 2";
zephyr,code = <INPUT_KEY_1>;
};
button2: button_2 {
gpios = <&gpio0 8 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 3";
zephyr,code = <INPUT_KEY_2>;
};
button3: button_3 {
gpios = <&gpio0 9 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 4";
zephyr,code = <INPUT_KEY_3>;
};
};

Listing 1. Konfiguracja przycisków w devicetree

Następnie wypełniamy plik main.c zgodnie z listingiem 2. Kod do obsługi przycisków korzysta ze znanego nam już modułu gpio. Nowością jest sposób konfiguracji przerwań oraz funkcja callback, która wykonuje się przy każdym naciśnięciu przycisku.

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG);

static const struct gpio_dt_spec buttons[] = {
GPIO_DT_SPEC_GET(DT_NODELABEL(button0), gpios),
GPIO_DT_SPEC_GET(DT_NODELABEL(button1), gpios),
GPIO_DT_SPEC_GET(DT_NODELABEL(button2), gpios),
GPIO_DT_SPEC_GET(DT_NODELABEL(button3), gpios)
};

static struct gpio_callback cb_data;

static void button_callback(const struct device *dev,
struct gpio_callback *cb, uint32_t pins)
{
LOG_INF("Pin mask 0x%08x", pins);
}

int main(void) {
gpio_init_callback(&cb_data, button_callback,
BIT(buttons[0].pin) | BIT(buttons[1].pin) |
BIT(buttons[2].pin) | BIT(buttons[3].pin));

for (int i = 0; i < ARRAY_SIZE(buttons); ++i) {
gpio_pin_configure_dt(&buttons[i], GPIO_INPUT);
gpio_pin_interrupt_configure_dt(&buttons[i],
GPIO_INT_EDGE_FALLING);
gpio_add_callback(buttons[i].port, &cb_data);
}

int counter = 0;
while (1) {
LOG_INF("Tick %d", counter++);
k_msleep(1000);
}
return 0;
}
Listing 2. Plik main.c

Po skompilowaniu i wgraniu aplikacji na płytkę możemy zaobserwować w konsoli logów, że każde naciśnięcie przycisku generuje log informacyjny z maską bitową identyfikującą przycisk.

Warto wspomnieć, ile pracy wykonuje za nas Zephyr. Dzięki odpowiedniej konfiguracji opadające zbocze na dowolnym z pinów przypisanych do przycisków wywołuje przerwanie, które – obsługiwane przez Zephyra – uruchamia naszą funkcję button_callback, podając pin wywołujący przerwanie jako parametr pins. Nie musimy konfigurować żadnych rejestrów przerwań. Pamiętajmy jednak, że funkcja button_callback jest wywoływana bezpośrednio z przerwania, więc nie powinna zajmować zbyt wiele czasu. W naszym przypadku zlecamy tylko wysłanie loga wątkowi logowania poprzez makro LOG_INF i kończymy pracę w kontekście przerwania.

Podczas debugowania programu proponujemy ustawić breakpoint w funkcji button_callback. Po zatrzymaniu programu zwróć uwagę na stos wywołań (call stack).

Nowy wątek

Uznaliśmy, że jednym z tematów obowiązkowych do omówienia w naszym kursie są wątki (task, thread). W poprzednim artykule, opisując moduł logowania, wspomnieliśmy o osobnym wątku, który Zephyr automatycznie tworzy do delegowania obsługi wysyłanych wiadomości (poprzez LOG_INF, LOG_ERR itp.). Teraz pora utworzyć nasz własny wątek. Jego zadaniem będzie obsługa komend do sterowania diodą LED oraz przełączanie go w odpowiednim czasie.

Chcielibyśmy włączać i wyłączać naszego LED-a, a także ustawiać go w tryb migania z określoną częstotliwością. Przygotujmy więc interfejs, którego działanie sprawdzimy za pomocą wcześniej przygotowanych przycisków. Przyda nam się on również w późniejszej części kursu. Plik led_control.h (listing 3) opisuje nasz interfejs.

#ifndef LED_CONTROL_H
#define LED_CONTROL_H

typedef struct {
enum {
LED_STATE, // param: 0 (off) or 1 (on)
LED_BLINK // param: time [ms] for every toggle
} command;
unsigned int param;
} led_msg;

void led_send(led_msg msg);

#endif

Listing 3. Plik led_control.h

Plik led_control.c (listing 4) zawiera implementację obsługi diody LED. Nowy wątek tworzymy statycznie za pomocą makra K_THREAD_DEFINE. Warto wspomnieć, że Zephyr przewiduje również opcję tworzenia wątków w trakcie działania programu, funkcją k_thread_create (więcej szczegółów w dokumentacji [1]).

#include "led_control.h"
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(led_control, LOG_LEVEL_DBG);

static const struct gpio_dt_spec led =
GPIO_DT_SPEC_GET(DT_NODELABEL(led0), gpios);

K_MSGQ_DEFINE(led_queue, sizeof(led_msg), 10, 1);

void led_send(led_msg msg) {
k_msgq_put(&led_queue, &msg, K_NO_WAIT);
}

static void led_worker(void *a, void *b, void *c) {
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);

led_msg msg;
k_timeout_t timeout = K_FOREVER;

while (1) {
if (k_msgq_get(&led_queue, &msg, timeout) == 0) {
if (msg.command == LED_STATE) {
LOG_INF("LED_STATE %d received", msg.param);
gpio_pin_set_dt(&led, msg.param);
timeout = K_FOREVER;
} else if (msg.command == LED_BLINK) {
LOG_INF("LED_BLINK %dms received", msg.param);
timeout = K_MSEC(msg.param);
}
} else {
gpio_pin_toggle_dt(&led);
}
}
}

K_THREAD_DEFINE(led_thread, 500, led_worker, NULL, NULL, NULL,
K_HIGHEST_THREAD_PRIO, 0, 0);

Listing 4. Plik led_control.c

A oto, jak działa nasza kontrola LED-a w dużym skrócie i uproszczeniu. Zephyr będzie wywoływał led_worker niezależnie od main czy innych wątków. Można powiedzieć, że wątki działają równolegle, choć technicznie nie jest to możliwe, gdyż ich kod wykonuje jeden procesor. Wątki nie są „świadome” siebie nawzajem, zatem z punktu widzenia naszego led_worker jest on jedyną funkcją w systemie. Tak też należy go interpretować.

Funkcja led_worker zostanie wywołana przez Zephyra i na początku włączy LED-a. Następnie, w niekończącej się pętli, będzie oczekiwać na wiadomość podaną poprzez kolejkę led_queue w czasie określonym w zmiennej timeout. Jeśli wiadomość przyjdzie przed wyznaczonym czasem, k_msgq_get zwróci „0” i rozpocznie się dekodowanie odebranej wiadomości. Timeout jest inny niż „wieczność” tylko w trybie migania, i po jego upływie po prostu zmienia stan diody.

Wiadomości do kolejki mogą być wysyłane z dowolnego wątku, a także mogą mieć dowolną strukturę. Są bardzo popularnym, jeśli nie najpopularniejszym, sposobem sterowania wątkami i komunikacji między nimi w systemie Zephyr.

Skoro mamy już przygotowany wątek do obsługi diody LED, pozostaje nam podłączyć sterowanie nią do przycisków w funkcji button_callback (listing 5). Oczywiście należy pamiętać o dołączeniu nagłówka led_control.h.

static void button_callback(const struct device *dev,
struct gpio_callback *cb, uint32_t pins)
{
LOG_INF("Pin mask 0x%08x", pins);

if (pins & BIT(buttons[0].pin)) {
led_send((led_msg){ LED_STATE, 0 });
} else if (pins & BIT(buttons[1].pin)) {
led_send((led_msg){ LED_STATE, 1 });
} else if (pins & BIT(buttons[2].pin)) {
led_send((led_msg){ LED_BLINK, 500 });
} else if (pins & BIT(buttons[3].pin)) {
led_send((led_msg){ LED_BLINK, 100 });
}
}

Listing 5. Plik button_callback.c

Dołączmy również plik źródłowy led_control.c do naszego projektu w CMakeLists.txt (listing 6). Po skompilowaniu i wgraniu projektu możemy kontrolować diodę LED za pomocą czterech przycisków znajdujących się na płytce.

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

project(thread_test)

target_sources(app PRIVATE
src/main.c
src/led_control.c
)

Listing 6. Plik CMakelist.txt

Shell

Istnieje jeszcze jedna kwestia, warta dodania do arsenału programisty przy pracy z Zephyrem, a mianowicie shell. To wspaniałe narzędzie do testowania i konfigurowania naszych projektów. Najlepiej zobaczyć go w akcji.

Włączamy shell z obsługą UART w Kconfig poprzez dodanie do prj.conf dwóch ustawień, jak w listingu 7.

CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
Listing 7. Ustawienia w prj.conf

Mówimy Zephyrowi, który UART ma być skojarzony z shellem, uzupełniając wpis w led_pcb.overlay (listing 8).

  chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};

Listing 8. Wpis w led_pcb.overlay

Nie przejmujmy się na razie tym, że zarówno logi, jak i shell używają tego samego portu. Dodajemy nasze własne komendy powłoki Zephyra na końcu pliku led_control.c (jak w listingu 9).

#ifdef CONFIG_SHELL

#include <zephyr/shell/shell.h>
#include <stdlib.h>

static int cmd_led_on(const struct shell *shell, size_t argc, char **argv) {
led_send((led_msg){ LED_STATE, 1 });
shell_print(shell, "LED on");
return 0;
}

static int cmd_led_off(const struct shell *shell, size_t argc, char **argv) {
led_send((led_msg){ LED_STATE, 0 });
shell_print(shell, "LED off");
return 0;
}

static int cmd_led_blink(const struct shell *shell, size_t argc, char **argv) {
if (argc != 2) {
shell_print(shell, "led blink <time in ms>");
return -EINVAL;
}

uint32_t period = strtoul(argv[1], NULL, 10);
led_send((led_msg){ LED_BLINK, period });
shell_print(shell, "LED blinking period %d ms:",period);
return 0;
}

SHELL_STATIC_SUBCMD_SET_CREATE(led_menu,
SHELL_CMD(on, NULL, "LED on", cmd_led_on),
SHELL_CMD(off, NULL, "LED off", cmd_led_off),
SHELL_CMD(blink, NULL, "LED blink with period [ms]", cmd_led_blink),
SHELL_SUBCMD_SET_END
);

SHELL_CMD_REGISTER(led, &led_menu, "LED", NULL);
#endif

Listing 9. Komendy shella w led_control.c

Będą one wywoływać nasze funkcje sterujące LED-em. Budujemy i testujemy nasz interfejs do LED-a poprzez komendy wpisywane w tym samym terminalu, w którym oglądaliśmy (i nadal oglądamy) logi (rysunek 1).

Rysunek 1. Logi działającej aplikacji

Wiele modułów Zephyra ma wbudowane komendy shellowe. Wystarczy je włączyć. Tak w ramach ćwiczeń pobawmy się shellem modułu gpio. Dodajemy ostatni już wpis do prj.conf: CONFIG_GPIO_SHELL=y – włączy on komendy shella wkompilowane w moduł gpio. Po wgraniu projektu będziemy mogli sterować naszym LED-em bezpośrednio, sterując pinem w module gpio Zephyra niejako poza „wiedzą” naszej aplikacji.

Komenda wyłączająca LED to: „gpio set gpio@842500 28 0”. Dane dotyczące portu i pinu możemy uzyskać, analizując konfigurację led0 w pliku led_pcb.dts.

Podsumowanie

W tej części kursu nauczyliśmy się konfigurować i obsługiwać przyciski, tworzyć wątki oraz zarządzać nimi, a także korzystać z narzędzia shell. Dzięki tym umiejętnościom jesteśmy gotowi do dalszej pracy z systemem Zephyr i realizacji bardziej zaawansowanych zagadnień związanych z Bluetooth.

Krzysztof Kierys
Paweł Jachimowski

Odnośniki w tekście:

  1. https://docs.zephyrproject.org/latest/kernel/services/threads/index.html
Artykuł ukazał się w
Elektronika Praktyczna
sierpień 2024
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik listopad 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio listopad - grudzień 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje październik 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna listopad 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich listopad 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów