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.