Lekcja 14- Górne i dolne połówki

Wstęp

W tej lekcji zapoznamy się z koncepcją górnych i dolnych połówek(ang. top and bottom halves). Pojęcie połówek ma związek z obsługą przerwań. Używanie połówek oznacza, że obsługa danego zdarzenia zostaje podzielona na dwie części. Pierwszą część będzie stanowić stanowić procedura obsługi przerwania znana nam z lekcji 6. Taka procedura powinna się wykonywać możliwie szybko, dlatego powinno się w niej wykonać czynności, które muszą być wykonane natychmiast po wystąpieniu danego zdarzenia takie jak np. pobranie nowootrzymanych danych przez jakiś moduł komunikacyjny. Następnie przerwanie powinno zakolejkować do wykonania kolejną procedurę, która wykona ewentualne przetwarzanie otrzymanych danych. Tę drugą procedurę, która może się wykonać później nazywamy właśnie dolną połówką.

Implementacja

W Linuksie koncepcje połówek można zaprogramować na kilka sposób, najczęściej prezentuje się użycie taskletów oraz workqueue. Różnica pomiędzy tymi mechanizmami jest bardzo techniczna, tasklet jest wykonywany jako przerwanie, natomiast workqueue jest wykonywane jako zwykły proces. Jakie ma to znaczenie dla nas? Przede wszystkim taskletu nie możemy uśpić, a workqueue możemy.

W tej lekcji zaimplementujemy zarówno użycie taskletu i workqueue. Będziemy bazować na kodzie z lekcji 6. Dla przypomnienia napisaliśmy tam moduł, który w reakcji na naciśnięcie podłączonego przycisku zapalał diodę. Teraz dodatkowo w przerwaniu zakolejkujemy tasklet lub workqueue, które to będą logować coś do logu systemowego.

Tasklet

Kod niewiele się różni od kodu z lekcji 6 i wygląda następująco:

#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>

#define LED 48		//BBB
#define BUTTON 49
//#define LED 23			//RPi4
//#define BUTTON 24

static int led_state = 0;
static int di;

void tasklet_fun(unsigned long arg)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_TASKLET(tasklet, tasklet_fun, 1);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	tasklet_schedule(&tasklet);
	return IRQ_HANDLED;
}

static int __init helloworld_init(void)
{
	int ret;

	if(!gpio_is_valid(LED)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", LED);
		ret = -ENODEV;
		goto err1;
	}
	gpio_request(LED, "sysfs");
	gpio_direction_output(LED, led_state);
	gpio_export(LED, false);

	if(!gpio_is_valid(BUTTON)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", BUTTON);
		ret = -ENODEV;
		goto err2;
	}
	gpio_request(BUTTON, "sysfs");
	gpio_direction_input(BUTTON);
	gpio_set_debounce(BUTTON, 200);
	gpio_export(BUTTON, false);

	if((ret = request_irq(gpio_to_irq(BUTTON), irq_handler, IRQF_TRIGGER_RISING, "interrupt_test", &di))) {
		pr_err("Cannot register IRQ, ret: %i\n", ret);
		goto err3;
	}

	pr_info("Init succeeded\n");
	return 0;

err3:
	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);
err2:
	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
err1:
	return ret;
}

static void __exit helloworld_exit(void)
{
	free_irq(gpio_to_irq(BUTTON), &di);

	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);

	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
	pr_info("Exit succeeded\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");

Większość kodu powinna być dla Ciebie zrozumiała, nas interesują następujące linijki:

void tasklet_fun(unsigned long arg)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_TASKLET(tasklet, tasklet_fun, 1);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	tasklet_schedule(&tasklet);
	return IRQ_HANDLED;
}

Idąc od góry, definiujemy funkcję dla naszego taskletu. Jest ona bardzo prosta, wypisuje komunikat do logu systemowego.

Następnie deklarujemy tasklet za pomocą makra DECALRE_TASKLET. Przyjmuje ono 3 parametry- sam tasklet, funkcję taskletu oraz domyślną wartość argumentu przekazywanego do funkcji taskletu. Argument ten może być później zmieniony poprzez zmianę wartości pola data w naszym tasklecie.

Ostatnim interesującym nas elementem jest wywołanie funkcji tasklet_schedule. Wywołanie tej funkcji kolejkuje nasz tasklet do wykonania w przyszłości.

Workqueue

Podobnie jak w przypadku taskletu i w przypadku workqueue kod niewiele różni się od kodu z lekcji 6:

#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/workqueue.h>

#define LED 48		//BBB
#define BUTTON 49
//#define LED 23			//RPi4
//#define BUTTON 24

static int led_state = 0;
static int di;

void workqueue_fun(struct work_struct *work)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_WORK(workqueue, workqueue_fun);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	schedule_work(&workqueue);
	return IRQ_HANDLED;
}

static int __init helloworld_init(void)
{
	int ret;

	if(!gpio_is_valid(LED)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", LED);
		ret = -ENODEV;
		goto err1;
	}
	gpio_request(LED, "sysfs");
	gpio_direction_output(LED, led_state);
	gpio_export(LED, false);

	if(!gpio_is_valid(BUTTON)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", BUTTON);
		ret = -ENODEV;
		goto err2;
	}
	gpio_request(BUTTON, "sysfs");
	gpio_direction_input(BUTTON);
	gpio_set_debounce(BUTTON, 200);
	gpio_export(BUTTON, false);

	if((ret = request_irq(gpio_to_irq(BUTTON), irq_handler, IRQF_TRIGGER_RISING, "interrupt_test", &di))) {
		pr_err("Cannot register IRQ, ret: %i\n", ret);
		goto err3;
	}

	pr_info("Init succeeded\n");
	return 0;

err3:
	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);
err2:
	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
err1:
	return ret;
}

static void __exit helloworld_exit(void)
{
	free_irq(gpio_to_irq(BUTTON), &di);

	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);

	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
	pr_info("Exit succeeded\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");

W tym przypadku interesują nas następujące linijki:

void workqueue_fun(struct work_struct *work)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_WORK(workqueue, workqueue_fun);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	schedule_work(&workqueue);
	return IRQ_HANDLED;
}

Zaczynamy od funkcji dla naszego workqueue, która jest wypisuje tylko komunikat do logu systemowego.

Następnie deklarujemy nasze workqueue za pomocą makra DECLARE_WORK, które przyjmuje dwa parametry- workqueue i przypisaną do niej funkcje.

Ostatnim elementem układanki jest wywołanie funkcji schedule_work.Wywołanie tej funkcji kolejkuje nasze workqueue do wykonania w przyszłości.

Kompilacja

No to jeszcze szybki makefile:

obj-m += 14_tasklet.o 14_workqueue.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

Przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp. Testy zacznijmy od taskletu. Załaduj moduł na swojej płytce:

sudo insmod 14_tasklet.ko

Wciśnij przycisk podłączony do twojej płytki. Dioda powinna zmienić swój stan. Sprawdź czy odpowiedni komunikat pojawił się w logu systemowym:

dmesg

Aby przetestować workqueue odładuj moduł z taskletem:

sudo rmmod 14_tasklet

I wykonaj analogiczny test z workqueue:

sudo insmod 14_workqueue.ko
# wciśnij przycisk
dmesg
Ten wpis został opublikowany w kategorii Kurs pisania sterowników. Dodaj zakładkę do bezpośredniego odnośnika.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *