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.