Lekcja 09- Sterowniki platformowe

Wstęp

Urządzenia platformowe(ang. Platform devices) to krótko mówiąc urządzenia, które nie posiadają mechanizmu automatycznego wykrycia czyli popularnie mówiąc nie mają hot-pluga. Takimi urządzeniami na pewno nie będą żadne urządzenia używające USB. Przykładowymi urządzeniami platformowymi będą wszelkie urządzenia komunikujące się po interfejsach I2C czy SPI.

Jak łatwo się domyślić sterowniki platformowe to po prostu sterowniki służące do obsługi urządzeń platformowych.

Implementacja

Dotychczas przygotowywaliśmy sterowniki, które nie były elastyczne, jeśli napisaliśmy obsługę jednej diody LED to tylko jedna dioda mogła być obsługiwana. W tej lekcji będzie nieco inaczej, za pomocą jednego sterownika obsłużymy dwie diody, a w ogólności mogłoby być ich znacznie więcej. W tej lekcji będziemy bazować na przykładzie z lekcji poprzedniej gdzie mogliśmy

Co do podłączenia diod, wybierz dwa piny GPIO do których podłączysz swoje diody LED, oczywiście pamiętaj aby użyć rezystorów podczas podłączania diod.

Implementacja naszego sterownika wygląda następująco:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>

#define MOD_NAME "myled"
#define LED 48

static ssize_t led_state_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	int state = gpio_get_value(pdev->id);
	return sprintf(buf, "%i", state);
}

static ssize_t led_state_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	int state;
	if(sscanf(buf, "%i", &state) < 1) {
		return -EINVAL;
	}

	if(state < 0) {
		return -EINVAL;
	}

	gpio_set_value(pdev->id, state);
	return count;
}

static DEVICE_ATTR(led_state, 0644, led_state_show, led_state_store);

static int my_led_probe(struct platform_device *pdev)
{
	if(device_create_file(&pdev->dev, &dev_attr_led_state) != 0)	{
		pr_err("%s: cannot create sysfs entry!!!\n", MOD_NAME);
		goto err1;
	}

	if (!gpio_is_valid(pdev->id)){
		pr_err("Invalid GPIO pin!!1\n");
		goto err2;
	}
	gpio_request(pdev->id, "gpioLED");
	gpio_direction_output(pdev->id, 1);
	gpio_export(pdev->id, false);
	pr_info("my_led pin: %i\n", pdev->id);
	return 0;

err2:
	device_remove_file(&pdev->dev, &dev_attr_led_state);
err1:
	return -1;
}

static int my_led_remove(struct platform_device *pdev)
{
	gpio_set_value(pdev->id, 0);
	gpio_unexport(pdev->id);
	gpio_free(pdev->id);
	device_remove_file(&pdev->dev, &dev_attr_led_state);
	return 0;
}

static struct platform_driver mypdrv = {
	.probe = my_led_probe,
	.remove = my_led_remove,
	.driver = {
		.name     = "my_led",
		.owner    = THIS_MODULE,
	},
};

module_platform_driver(mypdrv);

MODULE_LICENSE("GPL");

Jak widać różni się on nieco od naszych poprzednich sterowników, pierwsze co się wrzuca w oczy to brak funkcji init oraz exit, ale za to doszły nam funkcje probe i remove.

Niektórzy pewnie teraz poczuli się oszukani ponieważ na samym początku mówiłem, że funkcje init oraz exit są implementowane zawsze, a tutaj ich brakuje. Otóż te funkcje są w tym pliku tylko, że są ukryte w makrze module_platform_driver, które to rozwija się do domyślnych funkcji init oraz exit dla sterownika platformowego.

Można w powyższym kodzie zauważyć mocne podobieństwo funkcji probe oraz remove do funkcji init oraz exit z poprzedniej lekcji. Przede wszystkim funkcje init i probe oraz exit i remove nie są ze sobą tożsame. Funkcja probe zostaje wywołana gdy dane urządzenie zostanie wykryte w systemie, a remove gdy to samo urządzenie zostanie z niego usunięte. Funkcje init oraz exit jak już wiecie służą odpowiednio do inicjalizacji modułu w systemie oraz do usunięcia modułu z systemu.

Podobieństwo w tych funkcji z funkcjami init i exit z poprzednich lekcji wynika z faktu, że w poprzednich lekcjach dokonywaliśmy inicjalizacji urządzeń podczas ładowania sterownika, w tym przypadku tak nie robimy.

Omówienie funkcji show oraz store pominiemy, są one identyczne jak w poprzednim przykładzie.

Funkcja probe jest wywoływana kiedy dane urządzenie zostanie wykryte. W przypadku sterowników platformowych urządzenie zostanie wykryte gdy zostanie dodane odpowiednie urządzenie w systemie lub gdy umieścimy odpowiedni wpis w device-tree(o tym porozmawiamy w kolejnej lekcji).

Funkcja probe przyjmuje jeden parametr- wskaźnik do struktury platform_device. Struktura platform_device reprezentuje urządzenie platformowe w Linuksie. W naszym przykładzie implementacja funkcji probe jest bardzo prosta. Najpierw tworzymy atrybut, który umożliwia sprawdzenie i zmianę stanu diody LED. Dlaczego nie tworzymy samego urządzenia poprzez alokacje numerów major, minor itd.? Ponieważ to już ogarnia za nas system i struktura device wchodzi w skład struktury platform_device. Dlatego możemy od razu pracować na urządzeniu jak ma to miejsce tutaj.

W dalszej części funkcja wykonuje operacje niezbędne do pracy z pinem GPIO. W tym przypadku założyłem, że numer identyfikacyjny urządzenia platformowego(pole id struktury platform_device) jest numerem pinu do którego podłączono diodę. Dlatego we wszystkich funkcjach do obsługi GPIO używamy pdev->id jako numeru pinu.

Na koniec funkcja powinna zwrócić wartość 0 jako informację o pomyślnym jej wykonaniu. Jeśli wystąpił jakiś błąd podczasy wykonywania funkcji to powinien zostać zwrócony odpowiedni kod błędu.

Funkcja remove jest komplementarna względem funkcji probe tj. robi to samo tylko, że na odwrót. Występuje tutaj ten sam schemat jak w przypadku exit i init.

Poniżej funkcji remove znajduje się struktura platform_driver. Reprezentuje ona sterownik platformowy w systemie. Ustawiamy w niej funkcje probe i remove. Musimy jeszcze zdefiniować pola zagnieżdżonej struktury driver. Pole name służy do powiązania urządzenia ze sterownikiem czyli jeśli chcemy aby sterownik był użyty do obsługi danego urządzenia to musi ono zostać zarejestrowane z odpowiednią nazwą. Pole owner określa, który moduł jest właścicielem sterownika. Póki co nie musimy się tym polem przejmować, wystarczy, że ustawimy je na bieżący moduł.

No ok, mamy nasz sterownik, ale teraz jawi się pytanie jak dodać takie urządzenie platformowe w systemie? Póki co w tym celu wykorzystamy kolejne moduły. Przykładowy moduł dodający takie urządzenie do systemu wygląda następująco:

#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/types.h>

static struct platform_device *pdev;

static int __init platform_my_led_add(void)
{
	int inst_id = TWÓJ PIN;
	pdev = platform_device_alloc("my_led", inst_id);
	platform_device_add(pdev);
	pr_info("my_led device added\n");
	return 0;
}

static void __exit platform_my_led_put(void)
{
	pr_info("my_led device removed\n");
	// poniższa linia nic to nie da, raz dodanego urządzenia nie można usunąć jeśli nie wystąpił błąd
	// platform_device_put(pdev);
}

module_init(platform_my_led_add);
module_exit(platform_my_led_put);
MODULE_LICENSE("GPL");

Jak widać kod jest bardzo prosty, składa się zaledwie z dwóch krótkich funkcji init oraz exit. Urządzenie platformowe tworzymy za pomocą funkcji platform_device_alloc, która przyjmuje dwa parametry- nazwę urządzenia oraz jego identyfikator, który w naszym przypadku odpowiada pinowi GPIO do którego jest podłączona dioda. Funkcją platform_device_add dodajemy nasze urządzenie do systemu.

W funkcji exit wyświetlamy informację o odładowaniu z systemu, co prawda powinniśmy wykonać operacje odwrotne do operacji, które zostały wykonane w funkcji init czyli w tym przypadku usunięcie urządzenia platformowego jednak nic to tutaj nie da.

Utwórz dwie wersje tego modułu, po jednym dla każdej diody, jedyne co musisz zmienić to wartość zmiennej inst_id.

To jest wszystko co potrzeba. Przygotuj Makefile, który tym razem będzie kompilować trzy pliki:

obj-m += 09_platform_driver.o 09_platform_led48.o 09_platform_led49.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

I 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. Tym razem testowanie będzie nieco bardziej złożone bo będziemy ładować aż trzy moduły. Najpierw musimy załadować nasz sterownik:

sudo insmod 09_platform_driver.ko

I teraz nic nie powinno się stać z podłączonymi diodami. Teraz utworzymy pierwsze urządzenie poprzez załadowanie odpowiedniego modułu:

sudo insmod 09_platform_led48.ko

Pierwsza dioda powinna się zapalić. Jej stan możemy zmienić poprzez przejście do jej katalogu:

cd /sys/devices/platform/my_led.48
ls

Tak jak w poprzednim przykładzie powinien być w tym katalogu dostępny plik led_state, zapisując do niego 0 lub 1 będziesz wyłączał i włączał diodę.

Analogicznie postąp z drugim modułem rejestrującym urządzenie w systemie.

Niby fajnie, mamy jeden sterownik, który umożliwia obsługę kilku urządzeń, ale trochę tak głupio pisać dla każdego urządzenia oddzielny moduł, który będzie je rejestrować. Rozwiązaniem tego problemu zajmiemy się w kolejnej lekcji.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 08- sysfs

Wstęp

W tej lekcji zapoznamy się z implementacją kolejnej metody komunikacji z urządzeniem. W lekcji 03 zapoznaliśmy się z urządzeniami znakowymi i tworzyliśmy plik urządzenia w katalogu /dev, który reprezentował to urządzenie. Teraz utworzymy atrybut dla tego urządzenia. Mówiąc obrazowo, za atrybut urządzenia możemy uznać plik dostępny gdzieś w katalogu /sys za pomocą którego możemy wpływać na działanie urządzenia lub odczytać jakiś jego parametr np. atrybuty mogą reprezentować zawartość jakiegoś rejestru danego urządzenia.

Implementacja

W tej lekcji wykorzystamy naszą wiedzę o urządzeniach znakowych oraz(jakże by inaczej) o GPIO. Tym razem utworzymy sterownik za pomocą, którego będziemy mogli zmieniać stan diody poprzez zapis wartości 0 lub 1 do odpowiedniego pliku. Oczywiście będziemy mogli ten stan również odczytać. Diodę podłącz jak w poprzednich lekcjach. Implementacja wygląda następująco:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/gpio.h>

#define MOD_NAME "myled"
#define LED 48

static struct class *cls;
static struct device *dev;
static dev_t first;
static unsigned int count = 1;

static ssize_t led_state_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	int state = gpio_get_value(LED);
	return sprintf(buf, "%i", state);
}

static ssize_t led_state_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int state;
	if(sscanf(buf, "%i", &state) < 1) {
		return -EINVAL;
	}

	if(state < 0) {
		return -EINVAL;
	}

	gpio_set_value(LED, state);
	return count;
}

static DEVICE_ATTR(led_state, 0644, led_state_show, led_state_store);

static int __init my_init(void)
{
	if(alloc_chrdev_region(&first, 0, count, MOD_NAME) < 0) {
		pr_err("%s: Failed to allocate character device region\n", MOD_NAME);
		goto err1;
	}

	cls = class_create(THIS_MODULE, "myled");
	if(cls == NULL) {
		pr_err("%s: Cannot create class\n", MOD_NAME);
		goto err2;
	}

	dev = device_create(cls, NULL, first, "%s", "myled0");
	if(dev == NULL) {
		pr_err("%s: Cannot create device\n", MOD_NAME);
		goto err3;
	}

	if(device_create_file(dev, &dev_attr_led_state) != 0)	{
		pr_err("%s: cannot create sysfs entry!!!\n", MOD_NAME);
		goto err4;
	}

	if (!gpio_is_valid(LED)){
		pr_err("Invalid GPIO pin!!1\n");
		goto err5;
	}
	gpio_request(LED, "gpioLED");
	gpio_direction_output(LED, 1);
	gpio_export(LED, false);

	pr_info("%s: Loading succeeded, major=%i, minor=%i\n", MOD_NAME, MAJOR(first), MINOR(first));
	return 0;

err5:
	device_remove_file(dev, &dev_attr_led_state);
err4:
	device_destroy(cls, first);
err3:
	class_destroy(cls);
err2:
	unregister_chrdev_region(first, count);
err1:
	return -1;
}

static void __exit my_exit(void)
{
	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
	device_remove_file(dev, &dev_attr_led_state);
	device_destroy(cls, first);
	class_destroy(cls);
	unregister_chrdev_region(first, count);
	pr_info("%s: Unloading succeeded\n", MOD_NAME);
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");

Pierwsze co się pewnie wrzuciło Wam w oczy to dwie dodatkowe funkcje- funkcja led_state_show oraz led_state_store. Funkcje show i store służą odpowiednio do odczytu atrybutu oraz do zapisu atrybutu. Oczywiście nie trzeba implementować obu funkcji gdyż np. możemy implementować obsługę urządzenia, które nie obsługuje modyfikacji danej cechy, w takim wypadku zaimplementowalibyśmy tylko funkcję show.

Patrząc na te funkcje pewnie też zauważyłeś, że nie używamy funkcji copy_to_user i copy_from_user do pracy z przekazywanymi buforami, funkcje te nie wymagają użycia tych funkcji. Szukałem odpowiedzi na to pytanie i nie znalazłem go, jedyne co znalazłem to wpis na stockoverflow(zobacz ten wątek), że bufor jest bezpośrednim odniesieniem do bufora z user-space’a i jedyne co trzeba zrobić to sprawdzić czy dane, które się w nim znajdują są poprawne(w przypadku funkcji store oczywiście). Jedyne co nam pozostaje to zaakceptować ten fakt i nie dyskutować z tym…

Przejdźmy do samej implementacji, zaczniemy od funkcji show. Jak zostało wcześniej wspomniane zostanie ona wywołana podczas odczytu danych z danego atrybutu. U nas jest ona bardzo prosta, sprawdzamy stan piu GPIO za pomocą funkcji gpio_get_value i zapisujemy otrzymaną wartość do bufora za pomocą funkcji sprintf. Funkcja show zwraca ilość zapisanych bajtów czyli akurat tę samą wartość, którą zwraca sprintf.

Dalej przechodzimy do funkcji store, która to zostanie wywołana podczas modyfikacji danego atrybutu. W tym przypadku funkcja jest nieco bardziej złożona ponieważ musimy sprawdzić czy przekazane dane są poprawne. W tym przypadku próbujemy zapisać dane przekazane w buforze do zmiennej o nazwie state, która ma typ int, jeśli się ta operacja się nie powiedzie to zwracamy błąd. Następnie sprawdzamy czy jej wartość jest mniejsza niż 0, jeśli jest to zwracamy błąd. Gdy już jesteśmy pewni, że przekazane dane są poprawne ustawiamy stan pinu GPIO, do kótego podłączyliśmy diodę. Na końcu zwracamy ilość odczytanych danych czyli w naszym przypadku możemy zwrócić parametr count funkcji.

Poniżej tych dwóch funkcji mamy taką dziwną linijkę:

static DEVICE_ATTR(led_state, 0644, led_state_show, led_state_store);

DEVICE_ATTR jest makrem, które służy do prostego zdefiniowania struktury atrybuty dlatego też możemy przed nim umieścić słowo kluczowe static. Jak widać posiada ono 4 parametry. Pierwszy to nazwa atrybutu pod jaką będzie on widoczny w systemie plików, nie podajemy go w cudzysłowie. Kolejny parametr to uprawnienia dostępu, w tym przypadku użytkownik root posiada prawo do odczytu i zapisu, wszyscy pozostali będą mogli tylko odczytywać plik. Dwa ostatnie parametry makra to odpowiednio funkcja show oraz store. Struktura utworzona przez to makro będzie się nazywać dev_attr_led_state czyli w ogólności nazwa to dev_attr_pierwszyparametr

Funkcja init powinna być w większości zrozumiała. Najpierw tworzymy klasę myled i urządzenie znakowe myled0. Dodatkowo tworzymy atrybut tworzonemu urządzeniu za pomocą funkcji device_create_file, która przyjmuje dwa parametry- urządzenie dla którego tworzymy atrybut oraz strukturę opisującą ten atrybut. W dalszej części funkcji wykonuje wszystkie niezbędne ustawienia pinu GPIO do którego jest podłączona dioda LED.

Funkcja exit również powinna być zrozumiała, jedyną nowością w niej jest wywołanie funkcji device_remove_file, która to usuwa utworzony atrybut urządzenia.

To jest wszystko co potrzeba. Przygotuj Makefile:

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

I 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 zbudowany sterownik na swoją płytkę np. za pomocą scp, a następnie załaduj sterownik:

sudo insmod 08_sysfs.ko

Jeśli instalacja modułu się powiodła to przejdź do katalogu urządzenia i wyświetl jego zawartość:

cd /sys/class/myled/myled0
ls

Powinieneś zobaczyć plik led_state. Ponieważ do pliku led_state pełen dostęp na tylko użytkownik root to zaloguj się na tego użytkownika:

sudo su

Teraz możesz zmieniać i odczytywać stan diody za pomocą pliku led_state:

echo 1 > led_state
cat led_state
echo 0 > led_state
cat led_state

Jeśli dioda zapalała się i gasła oraz odczyt odpowiadał zapisowi to znaczy, że wszystko zostało dobrze zaimplementowane.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 07- Parametry modułów

Wstęp

Czasami chcemy aby nasz moduł był bardziej uniwersalny i mógł obsługiwać dane peryferium, ale podłączone do różnych magistrali i pinów. My w tym celu wykorzystamy parametry przekazywane do modułów, będziemy mogli przekazać naszemu sterownikowi do którego pinu GPIO jest podłączona nasza dioda.

Implementacja

Ponownie będziemy bazować na kodzie z lekcji czwartej. Co do podłączenia diod, podłącz je do któregokolwiek wolnego pinu GPIO. Implementacja wygląda następująco:

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

static int pin = 48;
module_param(pin, int, 0);

static int __init led_init(void)
{
	if (!gpio_is_valid(pin)){
		pr_err("Invalid GPIO pin!!1\n");
		return -ENODEV;
	}
	gpio_request(pin, "gpioLED");
	gpio_direction_output(pin, 1);
	gpio_export(pin, false);	
	return 0;
}
static void __exit led_exit(void)
{
	gpio_set_value(pin, 0);
	gpio_unexport(pin);
	gpio_free(pin);
}

module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("Adam Olek");
MODULE_LICENSE("GPL");

Nie będę się rozwodzić tutaj nad ustawieniami GPIO bo to już dobrze znamy, nas tutaj najbardziej interesują te dwie linijki:

static int pin = 48;
module_param(pin, int, 0);

Pierwsza z nich to zmienna, która będzie służyła do przechowywania parametru, w tym przypadku domyślnie wynosi 48. Druga z nich to makro, które służy do zdefiniowania parametru modułu. Przyjmuje ono trzy parametry- nazwę parametru(naszą zmienną), typ tego parametru(u nas jest to int, oczywiście mogą być użyte inne typy) oraz uprawnienia do odpowiadającego pliku w sysfs(w naszym przypadku taki plik nie istnieje więc takie uprawnienia nie mają żadnego sensu). I to wszystko na temat implementacji.

Przygotuj Makefile:

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

I 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 zbudowany sterownik na swoją płytkę np. za pomocą scp. Tym razem ładowanie modułu będzie wyglądać nieco inaczej ponieważ będziemy podawać parametr:

sudo insmod 07_params.ko pin=49

Teraz pomimo, że w kodzie do zmiennej pin została przypisane wartość 48 zostanie ona nadpisana przez wartość 49. Przekazywany numer pinu musisz oczywiście dopasować do swojej konfiguracji.

Usunięcie modułu z systemu wygląda tak samo:

sudo rmmod 07_params
Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 06- Przerwania

Wstęp

Przerwanie to zdarzenie zewnętrzne odbierane przez system, które musi zostać obsłużone przez system. Takim zdarzeniem może być np. wciśnięcie guzika czy odebranie danych przez kartę sieciową. Obsługa przerwania jest wykonywana poprzez odpowiednio zdefiniowaną funkcję. Dzięki użyciu przerwań nie musimy sprawdzać okresowo stanu danej rzeczy(np. guzika) tylko spokojnie oczekujemy na sygnał.

Implementacja

Nie ma chyba sensu się póki co głębiej rozwodzić na przerwaniami, przejdziemy od razu do implementacji.

Na początek zacznijmy od tego co zaimplementujemy, napiszemy sterownik, który będzie zmieniał stan diody w reakcji na naciśnięcie przycisku tact-switch. Będziemy bazować na kodzie z lekcji czwartej.

Zanim zaczniesz implementację podłącz wymagane elementy do swojej płytki. W przypadku BBB dioda niech będzie podłączona do GPIO48, a przycisk do GPIO49, w przypadku RPi4 użyj pinów GPIO23 dla diody oraz GPIO24 dla przycisku. Oczywiście pamiętaj aby użyć rezystora podczas podłączania diody. Druga nóżka przycisku powinna być podłączona do VCC3V3 poprzez rezystor.

Cały kod sterownika wygląda tak:

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

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

static int led_state = 0;
static int di;

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	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 v2");

Jak widać kod nam się nieco wydłużył względem lekcji czwartej, wynika to przede wszystkim z faktu potrzeby zainicjalizowania dodatkowego pinu w funkcji init oraz jego uwolnienia w funkcji exit. Nad samą konfiguracją pinu nie będę się może zbytnio rozwodzić, zwróć uwagę, że w przypadku inicjalizacji pinu do którego jest podłączony przycisk ustawiamy go jako wejście(funkcja gpio_direction_input). Drugą rzeczą na którą musisz zwrócić uwagę to ustawienie debounce’a(nie wiem czy mamy jakieś polskie słowo na to) wynoszący 200 mikrosekund, chodzi o to aby system nie reagował na drgania styków przycisku.

A teraz najważniejsze, samo przerwanie. W funkcji init musimy zarejestrować nasze przerwanie funkcją request_irq. Przyjmuje ona następujące parametry- numer przerwania do obsługi, procedurę obsługi przerwania, flagi, nazwę urządzenia generującego przerwanie oraz „ciasteczko” przekazywane do procedury obsługi.

Każde przerwanie posiada swój numer w systemie, w przypadku GPIO nie musimy znać numeru przerwania generowanego przez używany pin GPIO, możemy do tego wykorzystać funkcję gpio_to_irq co też robimy.

Drugi parametr jest oczywisty, jest to funkcja, która zostanie wywołana po wystąpieniu tego przerwania.

Trzeci parametr to flagi informujące o właściwościach przerwania np. czy przerwanie może być współdzielone pomiędzy kilkoma procedurami obsługi. W naszym przypadku chcemy aby przerwanie było generowane tylko na zbocze narastające czyli tylko po wciśnięciu, nie chcemy aby przerwanie było obsługiwane podczas puszczania przycisku. Dlatego też przekazujemy flagę IRQF_TRIGGER_RISING.

Czwarty parametr to nazwa urządzenia, które generuje przerwanie, w zasadzie możesz tam wpisać cokolwiek.

Ostatni parametr jest nieco tajemniczy, jakieś ciasteczko, prawie jak w przypadku stron internetowych. Ten parametr służy do identyfikacji procedur obsługi przerwań w przypadku gdy chcemy je usunąć, a istnieje wiele procedur do obsługi danego przerwania, w zasadzie u nas by mogło ono wynosić NULL.

Jeszcze krótko o funkcji exit, w niej dochodzi linijka free_irq, funkcja ta służy do usunięcia danej procedury obsługi przerwań z systemu.

Teraz przejdźmy do samej procedury obsługi przerwania, wygląda ona następująco:

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

Musi ona posiadać dokładnie taką deklaracje tj. musi zwracać typ irqreturn_t, jest to po prostu enum, który może przybierać następujące wartości:

  • IRQ_NONE- przerwanie nieobsłużone
  • IRQ_HANDLED- przerwanie obsłużone
  • IRQ_WAKE_THREAD- przerwanie ma zostać obsłużone w wątku przerwania

Nas będą w najbliższej przyszłości interesować dwie pierwsze wartości.

Musi także przybierać takie dwa parametry- numer przerwania oraz nasze ciasteczko.

W samej funkcji dzieje się niewiele- zmieniamy stan diody poprzez zrobienie operacji XOR, a następnie zapisujemy nowy stan na wyjście pinu do którego jest podłączona dioda. Na koniec zwracamy wartość IRQ_HANDLED.

Makefile wygląda standardowo:

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

Przebuduj moduł komendą odpowiednią 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

Jak już podłączyłeś diodę i przycisk tak jak to zostało opisane w poprzedniej sekcji prześlij sterownik na swoją płytkę np. za pomocą scp i załaduj go:

sudo insmod 06_interrupt.ko

Teraz możesz zmieniać stan diody poprzez wciskanie przycisku.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 05- Timery

Wstęp

W tej lekcji poznamy metody pracy z linuksowymi timerami czyli funkcjonalnością, która umożliwia nam odwleczenie wykonania danej funkcji w czasie. Aby zilustrować to zagadnienie zmodyfikujemy kod naszego sterownika z poprzedniej lekcji. Tym razem dioda będzie migać, a nie tylko świecić ciągłym światłem.

Jiffies

Jiffies to nie nazwa żadnego zespołu z ubiegłego wieku, jest to zmienna przechowująca czas, który upłynął od uruchomienia systemu. Zmienna ta jest aktualizowana z częstotliwością, która jest określona poprzez opcję HZ w kernelu, opcja ta jest ustawiania podczas kompilacji kernela. Typowo wynosi ona 1000 dla architektur x86/x86_64 lub 250 dla ARM/ARM64. Liczba oznacza jak często zmienna jiffies jest aktualizowana w ciągu sekundy czyli jest to dosłownie częstotliwość, zapewne stąd nazwa tej opcji- HZ. Możesz sprawdzić ile wynosi u Ciebie ta opcja w ustawieniach kernela, wystarczy, że wejdziesz do swoich źródeł kernela, uruchomisz menuconfig i wyszukasz HZ. Zarówno dla BBB jaki i RPi4 domyślnie powinna ona wynosić 250.

Traktuj jiffiesy jako podstawową jednostkę czasu w kernelu, jednostki takie jak np. milisekundy uzyskujemy poprzez przeliczenie ilości jiffiesów na daną jednostkę. Na szczęście istnieją do tego gotowe funkcje i nie musimy przeliczać wszystkiego ręcznie.

Implementacja

Jak zostało wspomniane we wstępie, aby zobrazować działanie timerów zmodyfikujemy sterownik z poprzedniej lekcji, dodamy do niego taki właśnie timer, który będzie sobie migał diodą. Taki sterownik wygląda następująco:

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

#define LED // 48 dla BBB lub 4 dla RPi4, wybierz odpowiedni pin
#define TIMEOUT 1000

static int led_state = 0;
static struct timer_list led_timer;

void led_timer_callback(struct timer_list *data)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	mod_timer(&led_timer, jiffies + msecs_to_jiffies(TIMEOUT));
}

static int __init led_init(void)
{
	if(!gpio_is_valid(LED)) {
		pr_err("Invalid GPIO pin!!1\n");
		return -ENODEV;
	}
	gpio_request(LED, "sysfs");
	gpio_direction_output(LED, led_state);
	gpio_export(LED, false);
	timer_setup(&led_timer, led_timer_callback, 0);
	mod_timer(&led_timer, jiffies + msecs_to_jiffies(TIMEOUT));
	return 0;
}
static void __exit led_exit(void)
{
	del_timer(&led_timer);
	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

Jak widać niewiele kodu doszło względem kodu z poprzedniej lekcji. Pominiemy omówienie kodu związanego z GPIO, skupmy się tylko na ustawieniu timera. W funkcji init inicjalizujemy nasz timer za pomocą funkcji timer_setup. Jej parametrami są nasz timer, w naszym przypadku struktura timer_list jest zmienną globalną dla tego modułu. Drugim parametrem jest funkcja, która ma być wywołana przez timer, w naszym przypadku jest to funkcja led_timer_callback. Zwróć uwagę, że nie możesz tak przekazać jakiejkolwiek funkcji, musi ona zwracać typ void(czyli w zasadzie nic nie zwracać) oraz musi przyjmować jeden parametr w postaci wskaźnika na strukturę timer_list. Trzeci parametr timer_setup to flagi, które mogą wpływać na specyficzne działanie timerów, ich opis znajdziesz w źródłach kernela w pliku include/linux/timer.h.

Gdy już zainicjalizujemy timer to go ustawiamy za pomocą funkcji mod_timer, która przyjmuje dwa parametry. Pierwszy to oczywiście nasz timer, a drugi to czas w jakim ma się wykonać. Zwróć uwagę, że ustawiamy czas od „teraz” dlatego dodajemy jakąś wartość do obecnej wartości zmiennej jiffies. My chcemy odwlec wykonanie się naszej funkcji o 1 sekundę. Nie możemy jednak przekazać jak człowiek wartości 1 sekunda, musimy tę jedną sekundę przeliczyć na jiffiesy dlatego też używamy funkcji msecs_to_jiffies, która przelicza ilość milisekund na ilość jiffiesów. My chcemy odwlec wykonanie o jedną sekundę więc musimy przekazać wartość 1000 milisekund. I to tyle co musimy zrobić w funkcji init.

Teraz może zajmijmy się funkcją led_timer_callback czyli funkcją, którą chcemy wywoływać okresowo. Jej kod jest bardzo prosty- najpierw zmieniamy aktualny stan diody, a potem ten stan zapisujemy na pin GPIO. Ważna jest ostatnia linijka funkcji. Jeśli chcemy aby funkcja wywoływała się okresowo musimy timer ustawiać na końcu tej funkcji w innym przypadku funkcja zostanie wywołana tylko raz.

W funkcji exit dochodzi nam jedna linijka- wywołanie funkcji del_timer, która to usuwa nasz timer z systemu.

Makefile wygląda standardowo:

obj-m += 05_timer.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ł komendą odpowiednią 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

Przede wszystkim dioda musi być podłączona do płytki tak samo jak w poprzedniej lekcji. Gdy już wszystko jest podłączone prześlij nasz nowy sterownik na swoją płytkę np. za pomocą scp i załaduj moduł:

sudo insmod 05_timer.ko

Dioda powinna zacząć migać z częstotliwością 1hz czyli co jedną sekundę. Teraz odładuj moduł:

sudo rmmod 05_timer

Dioda powinna przestać migać.

Inne funkcje związane z czasem

W kernelu możesz się jeszcze spotkać z funkcjami, które służą do wprowadzenia opóźnienia pomiędzy kolejnymi operacjami, są to tzw. sleepy. Funkcje tego typu to nsleep, usleep i msleep. Funkcje te służą odpowiednio do wprowadzenia opóźnia wyrażonego w nanosekundach, mikrosekundach i milisekundach. Oczywiście dokładność tych funkcji może być różna na różnych systemach, będzie to zależało jak dokładny pomiar czasu oferuje dane urządzenie.

Dodatkowo jeśli dokładność czasowa standardowego kernelowego timera to za mało to można się przyjrzeć zegarom o wysokiej rozdzielczości lepiej znanych jako high resolution timers lub w skrócie hrtimers. Ich użycie jest zbliżone do użycia zwykłego kernelowego timer, w zasadzie to sposób użycia jest analogiczny, główną różnicą z punktu widzenia użytkownika API są inne nazwy funkcji.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 04- Obsługa GPIO

Wstęp

W tej krótkiej lekcji zapoznamy się z podstawami obsługi GPIO w kernelu Linuksa. Obsługa tego interfejsu jest bardzo prosta tak samo jak on sam, ale umożliwia on zaprezentowanie wielu aspektów kernela na żywo, a nie „na sucho” jakimiś dziwnymi przykładami. GPIO będzie nam towarzyszyło w kilku kolejnych lekcjach w których będą prezentowane takie koncepcje jak timery, przerwania, sysfs, sterowniki platformowe(platform drivers) czy device-tree. Zatem zabierzmy się do roboty.

Czym jest GPIO?

Mam nadzieję, że nikomu nie trzeba przedstawiać czym jest GPIO, ale dla formalności dwa zdania czym to mniej więcej jest. GPIO(General Purpose Input/Output) to interfejs, który umożliwia nam wysłanie jednego bitu informacji bądź odczyt jednego bitu informacji, mówiąc po ludzku, są to te piny na twojej płytce, które nie mają przypisanej żadnej konkretnej roli jak np. UART i umożliwiają ustawienie na swoim wyjściu stanu wysokiego lub niskiego bądź umożliwiają odczyt czy dochodzi do nich stan wysokiego lub niskiego napięcia.

Implementacja

Nasz sterownik będzie bardzo prosty, jego działanie będzie polegało tylko i wyłącznie na zapaleniu podłączonej diody podczas jego ładowania i wyłączenia jej podczas jego usunięcia z systemu.

W przypadku RPi4 podłączałem diodę do pinu GPIO4, a w przypadku BBB do pinu GPIO48, oczywiście możesz użyć innych pinów. Podczas podłączania diody pamiętaj o użyciu rezystora.

Kod wygląda następująco:

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

#define LED // 48 dla BBB lub 4 dla RPi4, wybierz odpowiedni pin

static int __init led_init(void)
{
    if (!gpio_is_valid(LED)){
        pr_err("Invalid GPIO pin!!1\n");
        return -ENODEV;
    }
    gpio_request(LED, "gpioLED");
    gpio_direction_output(LED, 1);
    gpio_set_value(LED, 1);
    gpio_export(LED, false);	
    return 0;
}
static void __exit led_exit(void)
{
    gpio_set_value(LED, 0);
    gpio_unexport(LED);
    gpio_free(LED);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

Jak widać kod jest bardzo prosty, składa się tylko z funkcji init oraz exit. Ale po kolei, co się dzieje w tym kodzie.

Najpierw sprawdzamy czy nasz podany pin jest poprawny za pomocą funkcji gpio_is_valid, jeśli zwróci ona błąd zwrócimy informację o braku takiego urządzenia w systemie.

Następnie prosimy o dostęp do tego pinu za pomocą gpio_request, pierwszy parametr to numer naszego GPIO, a drugi to po prostu etykieta, którą mu nadajemy, może być ona jakakolwiek.

W kolejnej linijce ustawiamy pin GPIO jako wyjście ponieważ chcemy ustawiać jego stan gdyż chcemy zapalać diodę. Gdybyśmy chcieli odczytywać stan guzika byśmy ustawili GPIO jako wejście. Aby ustawić pin GPIO jako wyjście używamy funkcji gpio_direction_output, jako parametry podajemy numer pinu oraz jaki stan ma mieć- 1 dla stanu wysokiego(włączony) lub 0 dla stanu niskiego(wyłączony).

Kolejna linijka jest dla celów demonstracyjnych i nie ma ona wpływu na działanie modułu. Funkcja gpio_set_value służy do ustawienia stanu na wyjściu, my tutaj ustawiamy stan wysoki, który to już został ustawiony wcześniej przez funkcję gpio_direction_output.

Funkcja gpio_export służy do utworzenia odpowiednich katalogów i plików w wirtualnym systemie plików(katalog /sys) dzięki, którym użytkownik mógłby komunikować się z naszą diodą. My nie chcemy aby system utworzył takie pliki więc przekazujemy wartość false jako drugi parametr.

Na końcu zwracamy wartość 0 jako informację, że wszystko się powiodło.

Jak wiemy z poprzedniej lekcji funkcja exit robi to samo co init tylko, że na odwrót. W tym przypadku ustawiamy stan pinu na 0 czyli stan niski oraz zwracamy diodę do systemu za pomocą funkcji gpio_unexport i gpio_free.

Makfile wygląda jak poprzednio:

obj-m += 04_gpio.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ł komendą odpowiednią dla swojego systemu:

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

Testowanie sterownika

Aby przetestować sterownik musisz mieć wcześniej podłączoną diodę jak to zostało opisane na początku poprzedniej sekcji.

Prześlij plik na swoją płytkę np. za pomocą scp i załaduj moduł:

sudo insmod 04_gpio.ko

Dioda powinna się zapalić. Teraz usuń moduł:

sudo rmmod 04_gpio

Dioda powinna zgasnąć.

I to tyle w tej lekcji, dzięki temu prostemu interfejsowi poznamy całkiem złożone funkcjonalności kernela.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 03- Urządzenia znakowe

Wstęp

W tej lekcji utworzymy pierwszy sterownik wirtualnego urządzenia znakowego. Czemu wirtualnego? Wirtualnego ponieważ ten sterownik nie będzie obsługiwać żadnego fizycznego urządzenia, stworzy on natomiast odpowiedni plik w katalogu /dev do którego będzie można pisać i czytać. Można to porównać do urządzenia typu /dev/zero, /dev/null czy /dev/random.

Czym jest urządzenie znakowe?

Urządzenie znakowe(ang. character device, chardev) to urządzenie z którego możemy czytać znaki lub zapisywać znaki. Jakie znaki? Normalne, odczytujemy z/zapisujemy do niego po prostu ciąg bajtów, który może być interpretowany np. jako tekst jak to ma miejsce np. w przypadku UARTa.

Jak obsługujemy takie urządzenia? Zgodnie z filozofią wszystko jest plikiem to takie urządzenia obsługujemy tak samo jak zwykłe pliki czyli używamy POSIXowych funkcji takich jak open, close, read, write czy lseek. W zależności od potrzeb różne funkcje mogą być zaimplementowane, nie jest wymagana implementacja wszystkich funkcji jeśli dana opcja nie jest potrzebna, np. może być urządzenie do którego zapisujemy jakieś dane(czyli implementujemy funkcje write), ale z jakiś powodów nie chcemy aby była możliwość odczytu z niego(wtedy nie implementujemy funkcji read).

Major i Minor numbers

Numer major i minor to numery służące odpowiednio do identyfikacji sterownika i urządzenia w systemie. Każde urządzenie posiadające ten sam numer major używa tego samego sterownika, numer minor jest używany przez sterownik do określenia z którym urządzeniem ma się komunikować.

W modułach do przechowywania tych numerów używa się zmiennej dev_t. Na chwilę pisania tego kursu pierwsze 12 bitów służy do przechowywania numeru major, a pozostałe 20 jest zarezerwowane dla minora. Niech też Cię nie kusi aby robić jakieś operacje bitowe aby odczytać te numery, istnieją do tego odpowiednie makra. Dla swojej wygody i łatwiejszej przenoszalności używaj ich.

Numery te mogą być nadawane zarówno statycznie jak i dynamicznie. Statyczne nadawanie tych numerów grozi tym, że będziemy próbowali użyć numeru, który jest już w użyciu co spowoduje błąd. W tym przykładzie zostanie jedynie zademonstrowane dynamiczne przydzielenie numerów major i minor.

Szkielet sterownika

Skoro już wiemy co będziemy kodować to zakodujmy to. Na początek zaprezentuje szkielet takiego modułu, funkcje będą puste. Omówieniem istotnych elementów kodu oraz implementacją poszczególnych funkcji zajmiemy się w odpowiednich sekcjach. Szkielet takiego sterownika prezentuje się następująco:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/cdev.h>

#define MOD_NAME "mychardev"
#define KBUF_SIZE (size_t)(10 * PAGE_SIZE)

static char *kbuf;
static dev_t first;
static unsigned int count = 1;
static struct cdev *my_cdev;
static struct class *c;

static int my_open(struct inode *i, struct file *f)
{
}

static int my_release(struct inode *i, struct file *f)
{
}

static ssize_t my_read(struct file *f, char __user *buf, size_t lbuf, loff_t *ppos)
{
}

static ssize_t my_write(struct file *f, const char __user *buf, size_t lbuf, loff_t *ppos)
{
}

static const struct file_operations my_fops = {
	.owner = THIS_MODULE,
	.read = my_read,
	.write = my_write,
	.open = my_open,
	.release = my_release
};

static int __init my_init(void)
{
}

static void __exit my_exit(void)
{
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");

Kod nie wygląda jakoś strasznie, zaledwie pięć zmiennych globalnych, sześć funkcji i jakaś struktura.

Struktura file_operations

Zaczniemy trochę od środka kodu bo od struktury file_operations. Struktura ta przechowuje funkcje, które będą wywoływane podczas odpowiedniego wywołania systemowego. Niemalże wszystkie pola mają takie same nazwy jak w standardzie POSIX oprócz funkcji close. W strukturze file_operations pole release odpowiada funkcji close. Struktura ta ma znacznie więcej pól, nie będą one nam póki co potrzebne więc aby zachować prostotę przykładu ograniczmy się do tych czterech podstawowych wywołań: open, release(close), read oraz write.

Funkcja init

Tę funkcję już poznałeś w poprzedniej lekcji, w tym przypadku będzie ona bardziej rozbudowana ponieważ będzie robić teraz nieco więcej niż tylko logowanie, że moduł został załadowany. Tutaj dokona ona inicjalizacji naszego urządzenia.

Na początek zobaczymy jak wygląda cała funkcja, a następnie zajmiemy się jej poszczególnymi fragmentami:

static int __init my_init(void)
{
	if(alloc_chrdev_region(&first, 0, count, MOD_NAME) < 0) {
		pr_err("%s: Failed to allocate character device region\n", MOD_NAME);
		goto err1;
	}

	if(!(my_cdev = cdev_alloc())) {
		pr_err("%s: cdev_aloc() failed\n", MOD_NAME);
		goto err2;
	}

	kbuf = kmalloc(KBUF_SIZE, GFP_KERNEL);
	if(kbuf == NULL) {
		pr_err("%s: Cannot allocate memory\n", MOD_NAME);
		goto err3;
	}
	cdev_init(my_cdev, &my_fops);
	if(cdev_add(my_cdev, first, count) < 0) {
		pr_err("%s: Cannot add character device\n", MOD_NAME);
		goto err4;
	}

	c = class_create(THIS_MODULE, "my_cls");
	device_create(c, NULL, first, "%s", "mycdrv");

	pr_info("%s: Loading succeeded, major=%i, minor=%i\n", MOD_NAME, MAJOR(first), MINOR(first));
	return 0;

err4:
	kfree(kbuf);
err3:
	cdev_del(my_cdev);
err2:
	unregister_chrdev_region(first, count);
err1:
	return -1;
}

Nie jest ona jakoś wielka, ale zacznijmy od początku.

Zaczyna się ona od wywołania funkcji alloc_chrdev_region(), służy ona do przydzielenia wolnych numerów major i minor. Numery te są zapisywane do pierwszego parametru, w naszym przypadku jest to zmienna first. Drugi parametr określa od jakiego numery minor mają zostać przydzielone, my chcemy zacząć od 0. Trzeci parametr określa ile numerów minor ma zostać przydzielonych(ile urządzeń będzie obsługiwanych przez ten sterownik). Ostatni parametr to nazwa sterownika lub urządzenia powiązanego z przydzielonymi numerami. Jeśli operacja nie powiedzie się zostanie zalogowana odpowiednia informacja i zostanie wykonany skok do odpowiedniego miejsca, który zwróci błąd.

Następnie wywołujemy cdev_alloc(). Funkcja ta alokuje w pamięci strukturę cdev, która to służy do reprezentacji naszego urządzenia w kodzie. Jeśli alokacja się nie powiedzie logujemy odpowiednią informację i skaczemy do odpowiedniego miejsca w kodzie, które cofnie poprzednie operacje i zwróci błąd.

Następnie alokujemy pamięć za pomocą funkcji kmalloc(). kmalloc() to taki kernelowy malloc(). Funkcja ta pobiera dwa parametry- ilość bajtów do zaalokowania oraz flagę, która określa sposób alokacji pamięci. W naszym przykładzie alokujemy standardową pamięć z kernelowej przestrzeni adresowej oraz dopuszczamy możliwość uśpienia funkcji. Podobnie jak w przypadku malloca jest zwracany wskaźnik na voida. Jeśli alokacja nie powiedzie się to zostanie zwrócony NULL, w takim przypadku logujemy co trzeba i skaczemy do odpowiedniego miejsca.

Po alokacji pamięci wykonujemy inicjalizacje urządzenia i przekazujemy mu naszą strukturę file_operations. Dzięki tej funkcji nasze urządzenie będzie mogło zostać zarejestrowane w systemie. Zgodnie z dokumentacją funkcja ta nie może się nie powieść dlatego nie sprawdzamy czy wystąpił błąd.

Zainicjowaliśmy urządzenie więc teraz możemy je dodać do systemu za pomocą funkcji cdev_add(). Oprócz struktury reprezentującej urządzenie przekazujemy również wcześniej uzyskany numery major, minor oraz zakres numerów minor. Oczywiście jeśli funkcja się nie powiedzie logujemy odpowiednią informację oraz skaczemy do odpowiedniego miejsca w kodzie.

Dodaliśmy urządzenie w systemie, ale nie został utworzony żaden plik w systemie plik z nim powiązany. Aby uniknąć dodawania tego plik ręcznie za pomocą komendy mknod my stworzymy odpowiedni plik w kodzie.

Najpierw musimy utworzyć klasę do którego będzie należeć nasze urządzenie używamy do tego funkcję class_create(). Jeśli funkcja się nie powiedzie to zapewne wiecie co się stanie.

Jak mamy utworzoną klasę urządzeń to możemy teraz utworzyć urządzenie, służy do tego funkcja device_create(), utworzy ona nam urządzenie o nazwie mycdrv w katalogu /dev.

Na koniec logujemy, że wszystko się udało i zwracamy wartość 0 czyli kod informujący system, że wszystko się powiodło.

Poniżej return 0 znajduje się obsługa błędów do której skakaliśmy.

Oj, nie uważałeś na wykładach, czemu używasz goto?

goto, zapomniane słowo kluczowe języka C, a jak ktoś już je zna to je demonizuje je tak samo mocno jak programiści Javy demonizują singletony. A są to zwykłe narzędzia, które mają swoje zastosowanie. Ale wracając do rzeczy, niechęć do słowa goto prawdopodobnie w dużej mierze wywołuje artykuł z 1968 „Goto considered harmful” autorstwa Dijkstry. Zwróćmy jednak uwagę na kontekst historyczny, Dijkstra pisał ten artykuł w czasach gdy wynalazkiem były takie funkcjonalności języka jak wyrażenia warunkowe i pętle, a po lasach biegały jeszcze ostatnie dinozaury. Wtedy rzeczywiście programiści mogli nagminnie używać goto. Natomiast dzisiaj ludzie niepotrzebnie gadają takie rzeczy, śmiem twierdzić, że nie rozumieją nawet co mówią, są oni pewnego rodzaju fanatykami programistycznymi tak samo jak te osoby co narzekają na singletony. Obecnie goto ma jedno bardzo dobre zastosowanie, obsługa błędów podczas inicjalizacji sprzętu, sterowników itd. Poza Linuksem takie podejście jest stosowane również w Androidzie i przypuszczam, że w innych systemach również. Gdybyśmy nie użyli goto w funkcji init jej kod by się wydłużył i wyglądał by tak:

static int __init my_init(void)
{
	if(alloc_chrdev_region(&first, 0, count, MOD_NAME) < 0) {
		pr_err("%s: Failed to allocate character device region\n", MOD_NAME);
		return -1;
	}

	if(!(my_cdev = cdev_alloc())) {
		pr_err("%s: cdev_aloc() failed\n", MOD_NAME);
		unregister_chrdev_region(first, count);
		return -1;
	}

	kbuf = kmalloc(KBUF_SIZE, GFP_KERNEL);
	if(kbuf == NULL) {
		pr_err("%s: Cannot allocate memory\n", MOD_NAME);
		cdev_del(my_cdev);
		unregister_chrdev_region(first, count);
		return -1;
	}
	cdev_init(my_cdev, &my_fops);
	if(cdev_add(my_cdev, first, count) < 0) {
		pr_err("%s: Cannot add character device\n", MOD_NAME);
		kfree(kbuf);
		cdev_del(my_cdev);
		unregister_chrdev_region(first, count);
		return -1;
	}

	c = class_create(THIS_MODULE, "my_cls");
	if(c == NULL) {
		pr_err("%s: Cannot create class\n", MOD_NAME);
		kfree(kbuf);
		cdev_del(my_cdev);
		unregister_chrdev_region(first, count);
		return -1;
	}
	if(device_create(c, NULL, first, "%s", "mycdrv") == NULL) {
		pr_err("%s: Cannot create device\n", MOD_NAME);
		class_destroy(c);
		kfree(kbuf);
		cdev_del(my_cdev);
		unregister_chrdev_region(first, count);
		return -1;
	}

	pr_info("%s: Loading succeeded, major=%i, minor=%i\n", MOD_NAME, MAJOR(first), MINOR(first));
	return 0;
}

Jak widać każdy kolejny warunek sprawdzający czy operacja się powiodła puchnie, a tak to mamy wszystko elegancko ogarnięte na końcu funkcji.

Tak więc młody adepcie kernela, nie bój się goto!

Funkcja exit

Funkcja exit() jest dosyć prosta, robi ona to samo co funkcja init tylko, że na odwrót, wygląda ona następująco:

static void __exit my_exit(void)
{
	device_destroy(c, first);
	class_destroy(c);
	kfree(kbuf);
	cdev_del(my_cdev);
	unregister_chrdev_region(first, count);
	pr_info("%s: Unloading succeeded\n", MOD_NAME);
}

Jak widać wykonuje ona operacje odwrotne niż init np. jeśli coś był rejestrowane to teraz jest odrejestrowywane. Dodatkowo zwróć uwagę, że wszystko odbywa się jakby na odwrót np. w funkcji init najpierw rejestrowaliśmy numery major i minor, w funkcji exit zwracamy je systemowi na końcu.

Funkcja open

Funkcja open() czyli funkcja służąca do otwarcia pliku powiązanego z naszym urządzeniem. W naszym przypadku nie będzie ona robiła nic specjalnego, będzie ona jedynie zliczać ilość otwarć pliku i będzie tę informacje logować do systemu. Jej wykonanie zawsze się powiedzie. Jej implementacja wygląda następując:

static int my_open(struct inode *i, struct file *f)
{
	static int counter = 0;
	counter++;
	pr_info("%s: Opened, counter: %i\n", MOD_NAME, counter);
	return 0;
}

Zwróć uwagę na jej parametry. Struktura inode służy do określenia pliku w pamięci, wskazuje ona gdzie on fizycznie się znajduje i czym on faktycznie jest.

Natomiast struktura file nie opisuje gdzie ten plik jest(choć jednym z jej pól jest wskaźnik do struktury inode), służy ona do przechowywania stanu obecnych operacji na pliku takich jak np. aktualna pozycja w pliku.

Funkcja release

Funkcja release() czyli funkcja odpowiedzialna za zamknięcie pliku powiązanego z naszym urządzeniem, odpowiednik POSIXowej funkcji close(). W naszym przypadku również nie robi ona nic nadzwyczajnego, loguje ona informacje o zamknięciu pliku i zwraca 0. Jej implementacja wygląda następująco:

static int my_release(struct inode *i, struct file *f)
{
	pr_info("%s: Closed\n", MOD_NAME);
	return 0;
}

Funkcja read

Funkcja read() czy funkcja służąca do odczytu danych z naszego urządzenia. W naszym przypadku dane będą odczytywane bufora kbuf, który zaalokowaliśmy w funkcji init. Funkcja ta wygląda następująco:

static ssize_t my_read(struct file *f, char __user *buf, size_t lbuf, loff_t *ppos)
{
	int nbytes, maxbytes, bytes_to_read;
	maxbytes = KBUF_SIZE - *ppos;
	bytes_to_read = maxbytes > lbuf ? lbuf : maxbytes;
	nbytes = bytes_to_read - copy_to_user(buf, kbuf + *ppos, bytes_to_read);
	*ppos += nbytes;
	return nbytes;
}

Najpierw sprawdzamy ile bajtów możemy odczytać z naszego bufora. Gdy już wiemy ile maksymalnie bajtów może zostać odczytanych sprawdzamy czy wartość ta jest większa niż ilość bajtów do odczytu zażądana przez użytkownika(parametr lbuf), jeśli jest większa to będziemy odczytywać tyle bajtów ile chce użytkownik.

Gdy już wiemy ile bajtów należy odczytać to wywołujemy funkcje copy_to_user(), służy ona do skopiowania danych z kernel space’a do user space’a(przypominam, że te obie przestrzenie są oddzielone od siebie w pamięci). Funkcja ta zwraca ilość bajtów, które mogły się nie skopiować, idealnie powinna ona zwrócić wartość 0 co oznacza, że wszystkie bajty zostały skopiowane. Zwróconą wartość zwracamy od zmiennej bytes_to_read aby wiedzieć ile bajtów zostało rzeczywiście odczytanych.

Wskaźnik pozycji przesuwamy o ilość odczytanych bajtów. Poprawna zmiana pozycji w pliku jest bardzo istotna, jeśli byśmy tego nie zrobili to przy kolejnych odczytach byśmy ciągle czytali dane z początku pliku, może to się objawiać np. wpadaniem programów takich jak cat w nieskończoną pętle.

Zgodnie z konwencją funkcja kończy się zwróceniem ilości odczytanych bajtów, zwrócenie wartości 0 oznacza dotarcie do końca pliku.

Funkcja write

Funkcja write() czyli funkcja służąca do zapisu danych w naszym buforze. Jej implementacja jest właściwie identyczna jak funkcji read() z tą drobną różnicą, że zamiast funkcji copy_to_user wywołujemy funkcję copy_from_user czyli funkcję kopiującą dane z user space’a do kernel space’a. Reszta kodu jest identyczna, implementacja wygląda następująco:

static ssize_t my_write(struct file *f, const char __user *buf, size_t lbuf, loff_t *ppos)
{
	int nbytes, maxbytes, bytes_to_write;
	maxbytes = KBUF_SIZE - *ppos;
	bytes_to_write = maxbytes > lbuf ? lbuf : maxbytes;
	nbytes = bytes_to_write - copy_from_user(kbuf + *ppos, buf, bytes_to_write);
	*ppos += nbytes;
	return nbytes;
}

Kompilacja

Makefile poza nazwą pliku nie różni się niczym od Makefile’a z poprzedniej lekcji:

obj-m += 02_chardev.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(shell pwd) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

A budujemy wszystko za pomocą komendy make:

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

Testowanie modułu

Uruchom swoją płytkę, prześlij moduł na nią i go załaduj:

sudo insmod 02_chardev.ko

Jeśli operacja się powiodła to możesz zapisywać do i odczytywać dane ze swojego własnego urządzenia:

echo „Eluwina mój sterowniku” > /dev/mycdrv
cat /dev/mycdrv

Komenda cat powinna wyświetlić napis, który zapisałeś komendą echo.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 02- Pierwszy moduł

Wstęp

W tej lekcji w końcu zaimplementujemy pierwszy prosty moduł kernelowy. Nie będzie on robił nic szczególnego, jego jedyną funkcją będzie wypisanie informacji w logu systemowym, że został on załadowany lub usunięty z systemu.

Jak programować w kernelu?

Pierwszą różnicą, która wrzuci Ci się w oczy patrząc na kod kernelowych modułów to brak funkcji main. Do implementacji modułów należy podejść jak do implementacji programu z użyciem jakiegoś frameworka. W przypadku modułów linuksowych musimy implementować funkcje, które są wywoływane w odpowiednim momencie np. podczas załadowania lub usunięcia modułu, odczytu danych lub wykrycia urządzenia.

Drugą różnicą jest brak dostępu do biblioteki standardowej, kod kernela dostarcza odpowiednie funkcje np. do logowania czy alokacji pamięci, ale nie są to te same funkcje co w przypadku biblioteki standardowej języka C.

Po trzecie musisz zwrócić uwagę na dostępy do pamięci. Gdy piszesz standardowy program to system operacyjny czuwa nad tym abyś nie napsuł czegoś poprzez dostęp do pamięci do której nie powinieneś mieć dostępu. W przypadku implementacji takich operacji w kernelu nikt Cię nie powstrzyma ponieważ system operacyjny nad tobą wtedy nie czuwa ponieważ implementujesz właśnie część systemu operacyjnego.

Po czwarte, odradza się używanie typu float w kernelu, wynika to z faktu, że tryb FPU jest wyłączony domyślnie w kernelu. Oczywiście użycie floata nie spowoduje wybuchu twojego komputera, ale może to negatywnie wpłynąć na obliczenia przeprowadzane przez programy użytkownika.

Kernel space i user space

Oba powyższe terminy odnoszą się do przestrzeni adresowych. Jak łatwo się domyślić kernel space to przestrzeń adresowa kernela, a user space to przestrzeń adresowa użytkownika. Przez taki podział dane dostępne w kernelu nie są łatwo dostępne przez programy i odwrotnie. Wiedza ta zostanie użyta w praktyce w kolejnych lekcjach.

Implementacja

Teraz przyszła pora na to co tygryski lubią najbardziej czyli na implementacje. Pierwszy kod jest bardzo krótki zatem zostanie on zaprezentowany poniżej w całości, a następnie omówiony:

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

static int __init hello_init(void) {
	pr_info("Hello world!\n");
	return 0;
}
static void __exit hello_exit(void) {
	pr_info("End of the world\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("Adam Olek");
MODULE_LICENSE("GPL");

Jak widać kod jest bardzo krótki, składa się z kilku nagłówków, dwóch funkcji oraz kilku makr.

hello_init to funkcja, która zostanie wywołana podczas ładowania modułu, hello_exit natomiast zostanie wywołane gdy moduł zostanie usunięty z systemu.
Obie funkcje wywołują pr_info. W gruncie rzeczy pr_info nie jest funkcją tylko jest makrem opakowującym funkcję printk, która to jest kernelowym odpowiednikiem funkcji printf. Użytkowo główna różnica pomiędzy printk oraz printf polega na tym, że używając printk oprócz danych do wyświetlenia przekazujemy również priorytet loga. Dla prostoty użycia zostały zdefiniowane makra takie jak pr_info, pr_err czy pr_debug, które elegancko opakowują funkcję printk i nie musimy przekazywać priorytetu loga.

Dodatkowo w oczy mogły wrzucić się atrybutu funkcji __init oraz __exit. W gruncie rzeczy nie mają one wpływu na moduły, które są ładowane podczas pracy systemu, mają one wpływ na kod, który jest wkompilowany w kernel. __init w przypadku kodu wkompilowanego w kernel informuje, żeby dana funkcja została usunięta z pamięci po jej wykonaniu ponieważ nie będzie ona później wykorzystywana i nie ma sensu aby zajmowała miejsce. Atrybut __exit informuje kernel żeby w przypadku kodu wkompilowanego w kernel nie był on używany i nie był brany do wynikowego pliku binarnego ponieważ nie zostanie on nigdy wykonany.

Następnie mamy dwa makra, module_init i module_exit, służą one do określenia funkcji init oraz exit.

Następnie mamy makro, które określa autora kodu- MODULE_AUTHOR.

Makro MODULE_LICENSE określa licencję pod jaką dany moduł został napisany. Na pozór to może się wydawać, że to makro ma jedynie funkcje informacyjną nie ma żadnego wpływu na działanie kodu. Nic bardziej mylnego, jeśli użyjemy licencji, która nie jest kompatybilna z licencją GPL nie będziemy mogli używać funkcji kernela, które zostały stworzone pod tą właśnie licencją.

Kompilacja

Aby skompilować moduł musimy utworzyć plik Makefile o następującej zawartości:

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

Parametrem -C musimy wskazać kod Linuksa, dla którego kompilujemy moduł.

Teraz możemy przebudować moduł za pomocą komendy make, musimy jednak wskazać docelową architekturę i używany zestaw kompilatorów. Dla naszych płytek komendy te wyglądają następująco:

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

Zostanie wygenerowanych kilka plików, nas najbardziej interesuje plik o rozszerzeniu ko czyli nasz moduł.

Testowanie modułu

Teraz przetestujemy nasz moduł. Uruchom swoją płytkę i prześlij na nią zbudowany plik 02_hello.ko(chyba, że nazwałeś plik inaczej) np. za pomocą komendy scp:

scp user@hostip:~/ścieżka/do/02_hello.ko .

Teraz możesz załadować swój moduł:

sudo insmod 02_hello.ko

Jeśli na konsoli nie pojawiła żadna informacja o błędzie to wszystko zadziałało. Jeśli chcesz zobaczyć log z funkcji init użyj komendy dmesg. Możesz też dodatkowo sprawdzić listę załadowanych modułów za pomocą komendy lsmod.

Aby sprawdzić czy usunięci modułu działa wykonaj komendę:

sudo rmmod 02_hello

Ponownie użyj komendy dmesg aby sprawdzić log z funkcji exit.

Im system jest dłużej uruchomiony tym więcej logów znajduje się w logu systemowym, aby wypisać kilka ostatnich logów użyj komendy tail. Np. aby wypisać 10 ostatnich logów użyjemy komendy:

dmesg | tail -n 10
Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 01- Przygotowanie środowiska

Wstęp

Jak zostało wspomniane we wprowadzeniu posiadam płytki BeagleBone Black oraz Raspberry Pi 4. Nie będziemy przygotowywać własnego systemu dla tych płytek bo jesteśmy leniwi. Użyjemy Debiana dla BBB oraz Raspberry Pi OS dla RPi4. Dwie kolejne sekcje prezentują jak przygotować sobie środowisko dla obu płytek więc jeśli jesteś zainteresowany jedną z nich przejdź do odpowiedniej sekcji.

Kompilatory

Aby budować moduły na inną architekturę sprzętową niż twój komputer potrzebujesz odpowiednich kompilatorów. Możesz je pobrać np. z repozytoriów swojego systemu. W przypadku dystrybucji bazujących na Debianie możesz zainstalować następujące pakiety:

  • gcc-arm-linux-gnueabihf w przypadku użycia BBB
  • gcc-aarch64-linux-gnu w przypadku użycia RPi4

BeagleBone Black

Jak zostało wspomniane we wstępie będziemy używać Debiana. Zatem pobierz obraz systemu z tej strony:

wget https://debian.beagleboard.org/images/bone-debian-10.3-iot-armhf-2020-04-06-4gb.img.xz

Jak widać po powyższym linku ja używam wersji 10.3 dla IoT zbudowanej 6 kwietnia 2020. Zainstaluj pobrany obraz na karcie uSD używanej przez BBB. W przypadku systemu Linux Mint wystarczy dwukrotnie kliknąć na pobrany obraz, uruchomi to program odzyskiwanie dysku. Być może w innych dystrybucjach będziesz musiał ręcznie otworzyć ten plik w programie „Zapisywaniu obrazów dysków”. Instalacja obrazu potrwa kilka minut. Zanim uruchomisz instalację systemu upewnij się, że jako miejsce do zapisu wybrałeś swoją kartę pamięci.

Aby zbudować moduły na właśnie zainstalowany system musimy mieć źródła kernela na swoim komputerze, ale nie może to być byle jaki kernel, chcemy taki sam kernel jaki jest zainstalowany na naszej płytce. Aby sprawdzić jaki kernel jest używany podłącz się do BBB za pomocą UARTa i wykonaj komendę:

uname -a
Linux beaglebone 4.19.94-ti-r42 #1buster SMP PREEMPT Tue Mar 31 19:38:29 UTC 2020 armv7l GNU/Linux

W powyższym przykładzie widać, że mój system ma wersję 4.19.94-ti-r42. Pobierzmy zatem taką wersję z repozytorium beagleboard/linux:

wget https://github.com/beagleboard/linux/archive/refs/tags/4.19.94-ti-r42.zip

Przy pobraniu skorzystałem z odpowiedniego tagu. Gdy paczka zostanie pobrana rozpakuj ją. Po pobraniu źródeł należy przebudować kod. Do zbudowania kernela użyjemy konfiguracji bb.org_defconfig:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bb.org_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4

Nie potrzebujemy nic zmieniać w konfiguracji. Kompilacja potrwa kilka minut. W następnej lekcji wykorzystamy przebudowane źródła do kompilacji pierwszego modułu.

Raspberry Pi 4

Jak zostało wspomniane we wstępie będziemy używać Raspberry Pi OS. Pobierz zatem obraz z tej strony:

wget https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-05-28/2021-05-07-raspios-buster-armhf-lite.zip

Ja używam wersji Lite ponieważ nie będzie nam potrzebny interfejs graficzny.

Aby zainstalować system rozpakuj pobraną paczkę. W przypadku Linuksa Mint do instalacji można wykorzystać program „Zapisywanie obrazów dysków”. Instalacja systemu potrwa kilka minut. Zanim zatwierdzisz instalację upewnij się, że jako miejsce zapisu wybrałeś swoją kartę pamięci.

Po instalacji zmienimy jeszcze nieco ustawienia. Otwórz plik config.txt znajdujący się na partycji boot i dodaj do niego następujące linijki:

enable_uart=1
arm_64bit=1

Dzięki temu aktywowaliśmy UART oraz włączyliśmy tryb 64-bitowy. Jak już mamy tyle bitów to wykorzystujmy wszystkie :). Trzeba jeszcze zmodyfikować plik cmdline.txt do następującej postaci:

console=ttyS0,115200 root=PARTUUID=923ca0ae-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Jedyne co się zmienia to parametr console, reszta pozostaje bez zmian. PARTUUID może się oczywiście różnić w twoim przypadku. Teraz możesz uruchomić system, pierwsze uruchomienie potrwa chwilę.

Teraz potrzebujemy mieć kernel na naszym komputerze, ale nie byle jaki kernel, musimy mieć taki sam kernel jaki jest zainstalowany w systemie na RPi4. Z RPi4 jest trochę lipa, mi osobiście nie udało się zbudować takiego samego kernela jaki jest używany przes RPi OS, pomimo że użyłem dokładnie tej konfiguracji, użyłem tej samej rewizji i tej samej wersji kompilatora, nie wiem co poszło nie tak… Dlatego aby uniknąć problemów z modułami zbudujemy własny kernel, którym zastąpimy kernel na RPi4.

Zacznijmy od pobrania kodu źródłowego:

git clone --depth 1 https://github.com/raspberrypi/linux.git

Teraz potrzebujemy konfiguracji. Użyjemy tej samej konfiguracji kernela, którą możemy zdobyć na naszym RPi OS. Podłącz się do RPi4 za pomocą UARTa i wykonaj następującą komendę na nim:

modprobe configs

Ta komenda spowoduje pojawienie się pliku /proc/config.gz, plik ten zawiera konfiguracje z którą został zbudowany używany kernel. Prześlij ten na swój komputer np. za pomocą scp i rozpakuj go. Tak otrzymaną konfigurację umieść w katalogu arch/arm64/config wewnątrz pobranego repozytorium, możesz ją nazwać np. rpios_defconfig. Teraz możemy przebudować kernel:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8 rpios_defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8 -j4

Kompilacja chwilę potrwa. Po kompilacji podłącz kartę pamięci do swojego komputera. Zapisz zbudowany kernel na partycji boot:

cp arch/arm64/boot/Image /media/user/boot/kernel8.img

Oczywiście jeśli chcesz możesz zachować oryginalny kernel na karcie np. poprzez zmianę jego nazwy.

Aby system działał w pełni prawidłowo trzeba jeszcze zainstalować zbudowane moduły na karcie SD na partycji rootfs:

sudo make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8 INSTALL_MOD_PATH=/media/user/rootfs/ modules_install

System jest gotowy, możesz go uruchomić, wszystko powinno na nim działać tak jak poprzednio. W następnej lekcji wykorzystamy przebudowane źródła do kompilacji pierwszego modułu.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 00- Wprowadzenie

Wstęp

W tym kursie zostanie omówione w jaki sposób implementować moduły kernela Linux czyli przede wszystkim zapoznamy się z jego API oraz pisaniem sterowników- czyli tym co zapewne interesuje najwięcej osób, które tu trafiło.

Aby kurs był możliwie zwięzły i obrazowy koncepcje związane z kernelem będą w miarę możliwości prezentowane z wykorzystaniem rzeczywistego sprzętu, będę starał się unikać prezentowania abstrakcyjnych przykładów.

Co jest potrzebne do kursu?

Do kursu będzie potrzebny komputer z zainstalowanym Linuksem, który będzie używany do kompilacji przygotowywanych modułów. Będzie również potrzebna płytka typu Raspberry Pi aby przetestować moduły Ja posiadam Raspberry Pi 4 oraz BeagleBone Black więc te płytki będą używane przeze mnie. Poza tym będą potrzebne takie elementy elektroniczne jak płytka stykowa, diody LED, przyciski tact-switch, kabelki goldpin, zegar RTC DS3232 [uzupełnić co jeszcze potrzeba].

Co trzeba umieć?

Warto posiadać wiedzę z Kursu budowania Linuksa, zachęcam do jego przerobienia jeśli nie posiadasz wiedzy w nim zawartej przed przerobieniem tego kursu.

Po drugie musisz znać język C, nie musisz być starym wyjadaczem, ale wskaźniki musisz ogarniać.

Po trzecie obsługę Linuksa też powinieneś mieć ogarniętą. Aby utrzymać kurs zwięzły będę pomijać instruktaż przesyłania plików pomiędzy komputerem, a twoją płytką i inne proste operacje tego typu.

Po czwarte powinieneś umieć podłączać elementy elektroniczne do RPi4 i BBB. Zakładam również, że potrafisz połączyć się ze swoją płytką za pomocą interfejsu UART, jeśli nie wiesz jak tego dokonać zapraszam do Kursu budowania Linuksa.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz