Lekcja 10- Device-tree

Wstęp

W tej lekcji zapoznamy się ze sposobem użycia device-tree z poziomu modułu kernelowego. Jeśli nie masz najmniejszego pojęcia czym jest device-tree to odsyłam do lekcji 6 z kursu budowania Linuksa.

Device-tree

Zanim użyjemy device-tree w sterowniku to najpierw musimy dodać odpowiednie wpisy w nim, które będą używane przez nasz moduł. W tej lekcji napiszemy sterownik do obsługi diody LED(no bo do czego by innego można by napisać sterownik), pin do którego jest podłączona dioda będzie zdefiniowany w device-tree. Poniżej jest zademonstrowane jak dodać odpowiedni wpis zarówno dla BBB jak i RPi. Kod modułu będzie identyczny dla obu platform. To, że kod będzie identyczny dla obu platform jest w tym przypadku zasługą device-tree.

Nasz wpis w device-tree będzie się składał z trzech własności- z comptible’a(powinieneś wiedzieć co to pole oznacza), z pinu oraz z początkowego stanu diody(włączona lub wyłączona).

BeagleBone Black

Aby zmodyfikować device-tree przejdź do katalogu z kernelem dla BBB. Otwórz plik arch/arm/boot/dts/am335x-boneblack-uboot-univ.dts. Jest to plik na podstawie, którego jest generowane docelowe device-tree dla BBB. Dodaj dwa wpisy pod węzłem root(czyli węzeł /). My dodamy dwie diody LED, kod po naszej modyfikacji powinien wyglądać następująco:

/ {
     model = "TI AM335x BeagleBone Black";
     compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
 
     chosen {
         base_dtb = "am335x-boneblack-uboot-univ.dts";                                                                                                                                                                                     
         base_dtb_timestamp = __TIMESTAMP__;
     };
 
     myled0 {
          compatible = "my_led";
          pin = <48>;
          state = <0>;
     };
     myled1 {
          compatible = "my_led";
          pin = <49>;
          state = <1>;
     };
 };

Zapisz zmiany i przebuduj kernel za pomocą komendy:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

Po kompilacji podmień plik /boot/dtb/am335x-boneblack-uboot-univ.dtb na karcie pamięci nowym zmodyfikowanym device-tree, które znajduje się pod ścieżką arch/arm/boot/dts/am335x-boneblack-uboot-univ.dts.

Diody podłącz oczywiście do pinów, które zdefiniowałeś w device-tree i jak zwykle pamiętaj o użyciu rezystorów podczas podłączania.

Raspberry Pi 4

Aby zmodyfikować device-tree przejdź do katalogu z kernelem dla RPi. Otwórz plik arch/arm/boot/dts/bcm2711-rpi-4-b.dts. Wprawne oko zauważyło już pewnie, że w ścieżce odnosimy się do architektury ARM, a nie ARM64. Nie jest to przypadek. Dla RPi devicetree dla architektury ARM i ARM64 są identyczne. Plik arch/arm64/boot/dts/bcm2711-rpi-4-b.dts zawiera tylko jedną linijkę- import pliku arch/arm/boot/dts/bcm2711-rpi-4-b.dts.

W podanym pliku na samym jego końcu dodaj dwa wpisy pod węzłem root(czyli węzeł /):

/ {
	__overrides__ {
		act_led_gpio = <&act_led>,"gpios:4";
		act_led_activelow = <&act_led>,"gpios:8";
		act_led_trigger = <&act_led>,"linux,default-trigger";

		pwr_led_gpio = <&pwr_led>,"gpios:4";
		pwr_led_activelow = <&pwr_led>,"gpios:8";
		pwr_led_trigger = <&pwr_led>,"linux,default-trigger";

		eth_led0 = <&phy1>,"led-modes:0";
		eth_led1 = <&phy1>,"led-modes:4";

		sd_poll_once = <&emmc2>, "non-removable?";
		spi_dma4 = <&spi0>, "dmas:0=", <&dma40>,
			   <&spi0>, "dmas:8=", <&dma40>;
	};

	myled0 {
		compatible = "my_led";
		pin = <23>;
		state = <0>;
	};
	myled1 {
		compatible = "my_led";
		pin = <24>;
		state = <1>;
	};
};

Zapisz zmiany i przebuduj kernel za pomocą komendy:

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

Po kompilacji podmień plik bcm2711-rpi-4-b.dtb na partycji boot na karcie pamięci używanej przez RPi nowym zmodyfikowanym device-tree, które znajduje się pod ścieżką arch/arm64/boot/dts/ bcm2711-rpi-4-b.dtb.

Diody podłącz oczywiście do pinów, które zdefiniowałeś w device-tree i jak zwykle pamiętaj o użyciu rezystorów podczas podłączania.

Implementacja

Podobnie jak w poprzedniej lekcji implementowany moduł będzie sterownikiem platformowym. Kod 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>
#include <linux/of.h>

#define MOD_NAME "myled"

struct led_data {
	u32 pin;
	u32 state;
};

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);
	struct led_data *data = platform_get_drvdata(pdev);
	int state = gpio_get_value(data->pin);
	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);
	struct led_data *data = platform_get_drvdata(pdev);
	int state;
	if(sscanf(buf, "%i", &state) < 1) {
		return -EINVAL;
	}

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

	gpio_set_value(data->pin, state);
	return count;
}

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

static int my_led_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	struct led_data *data;

	data = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);
	if(data == NULL) {
		pr_err("%s: cannot allocate memory!!!\n", MOD_NAME);
		return -ENOMEM;
	}

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

	of_property_read_u32(np, "pin", &data->pin);
	of_property_read_u32(np, "state", &data->state);
	if (!gpio_is_valid(data->pin)){
		pr_err("Invalid GPIO pin!!!\n");
		goto err2;
	}

	gpio_request(data->pin, "gpioLED");
	gpio_direction_output(data->pin, data->state);
	gpio_export(data->pin, false);

	platform_set_drvdata(pdev, data);
	pr_info("my_led pin: %i\n", data->pin);
	return 0;

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

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

static const struct of_device_id myled_match[] = {
	{ .compatible = "my_led", },
	{ },
};
MODULE_DEVICE_TABLE(of, myled_match);

static struct platform_driver mypdrv = {
	.probe = my_led_probe,
	.remove = my_led_remove,
	.driver = {
		.name     = "my_led",
		.of_match_table = of_match_ptr(myled_match),
	},
};

module_platform_driver(mypdrv);

MODULE_LICENSE("GPL");

Jak widać kod nie różni się jakoś bardzo od kodu z lekcji 9. Zupełnie nowym elementem, który dochodzi w tym kodzie jest struktura myled_match o typie of_device_id:

static const struct of_device_id myled_match[] = {
	{ .compatible = "my_led", },
	{ },
};

Struktura ta zawiera listę urządzeń, która ma być obsługiwana przez dany sterownik. Struktura ta jest przypisywana do pola of_match_table w strukturze driver wewnątrz struktury platform_driver.

Funkcja probe różni się nieznacznie od jej odpowiednika z poprzedniej lekcji. Po pierwsze, na samym początku alokujemy pamięć dla struktury data typu led_data(struktura ta jest zdefiniowana na samym początku kodu):

data = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);

Z poprzednich lekcji zapewne pamiętasz, że do alokacji pamięci w kernelu mamy takie funkcje jak kmalloc i kzalloc(ta funkcja oprócz alokacji pamięci czyści zaalokowany obszar pamięci). Funkcja devm_kzalloc też oczywiście alokuje pamięć, ale dodatkowo przypisuje ten obszar pamięci do danego urządzenia. Takie podejście powoduje, że nie musimy ręcznie zwalniać tak zaalokowanej pamięci, zostanie ona zwolniona w momencie usunięcia tego urządzenia z systemu.

Kolejną różnicą są wywołania funkcji of_property_read_u32:

of_property_read_u32(np, "pin", &data->pin);
of_property_read_u32(np, "state", &data->state);

Funkcje te odczytują dane właściwości z węzła w device-tree(oczywiście dla każdego typu danych występuje analogiczna funkcja). W tym przypadku odczytane wartości lądują od razu w strukturze data. Odczytane dane są następnie użyte do skonfigurowania pinów GPIO.

Na końcu funkcji przypisujemy strukturę data jako dane dla tego urządzenia platformowego za pomocą funkcji platform_set_drvdata. Dzięki temu będziemy mogli sprawdzać do jakiego pinu jest podłączona dioda w innych funkcjach.

Funkcja remove też się nieco różni od swojego odpowiednika z poprzedniej lekcji, ale w tym przypadku właściwie jedyną różnicą jest to, że najpierw odczytujemy dane urządzenia za pomocą funkcji platform_get_drvdata i na podstawie odczytanych danych zwracamy pin GPIO do systemu.

W funkcjach show i store również używamy funkcji platform_get_drvdata do odczytu danych urządzenia. Tylko w tym przypadku musimy nieco pokombinować ponieważ do funkcji show oraz store nie jest przekazywany bezpośrednio struktura platform_device tylko jej pole- struktura device. Aby otrzymać interesującą strukturę platform_device używamy makra container_of, które to zwraca nam strukturę, w której znajduje się nasza zmienna, w tym przypadku jest to struktura device.

I to chyba tyle, mam nadzieje, że wszystko jest jasne, szczególnie jeśli chodzi o ostatni akapit i strukturę container_of, gdyż może się to wydawać trochę zagmatwane na początku.

Przygotuj Makefile:

obj-m += 10_devicetree.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.

Testowanie tego modułu będzie zbliżone do testu z poprzedniej lekcji. Załaduj moduł do systemu:

sudo insmod 10_devicetree.ko

Jeśli poprawnie wprowadziłeś zmiany do devicetree oraz moduł nie posiada żadnych błędów to w sysfs powinny powstać odpowiednie pliki:

# Pierwsza dioda
cd /sys/devices/platform/myled0
# Druga dioda
cd /sys/devices/platform/myled1

W obu katalogach powinien być plik led_state. Zapisuj do tych plików 0 lub 1 aby zmieniać stany diod.

Dodatkowo, jeśli wykonasz komendę ls w tych katalogach zobaczysz, że znajduje się tam katalog of_node, katalog ten jest reprezentacją wpisu w devicetree w sysfs. Wykonaj komendę ls na tym katalogu:

ls of_node
compatible  name  pin  state

Jak wszystkie właściwości wpisu myled w devicetree są odwzorowane w postaci plików.

Dla formalności, katalog of_node jest dowiązaniem symbolicznym do /sys/firmware/devicetree/base/myledX gdzie X to 0 lub 1. Jest tak ponieważ w katalogu /sys/firmware/devicetree/base znajduje się odwzorowanie całego devicetree.

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 *