GPIO kernelio modulis

Šiandien anksti ryte pabudęs supratau, kad jums, mieli skaitytojai, gyvenime kažko trūksta. Pamąsčius paaiškėjo, kad trūkti gali tik vieno - mirksinčio LED. Aš nebūčiau aš, jeigu neatskubėčiau jums į pagalbą. Šiandien parašysiu kaip pamirksinti LEDą iš kernelio modulio.

Teorija

Tai nebepirma mūsų pažintis su kernelio draiveriais: jau išmokome parašyti paprastą labas pasauli ir SPI modulius. Šį kartą išmoksime naudotis GPIO interfeisu ir taimeriais. Tiesą sakant GPIO interfeisui jau yra sukurta nemažai kernelio tvarkyklių, tad naujos rašymas naudingas tik mokymosi tikslams. Jeigu tenori vien tik pamirksinti LEDą, siūlyčiau pasidomėti gpio-leds moduliu, kuris sukurs /sys/class/leds/ledo-pavadinimas/ ‘sysfs’ interfeisą, kuriame bus galima nurodyti kaip mirksėti: pagal CPU naudojimą, sistemos apkrovimą, HDD naudojimą, CPAS LOCK būseną ar pan.

GPIO interfeisas

Yra du GPIO interfeisai - pasenęs (gpio_) ir naujas (gpiod_). Taip pat yra ir pagalbinės funkcijos perėjimui iš senojo interfeiso į naująjį.

Senasis interfeisas

Šio API naudoti nerekomenduojama, nes jis pasenęs. Taip pat kyla problemų turint ne vieną GPIO kontrolerį, o ypač juos pajungiant sistemai veikiant (hotplug). Kadangi pagal seną interfeisą GPIO numeruojami nuo 0 iki kiek yra per visus kontrolerius, gali būti sudėtinga atsirinkti kuris numeriukas kurį GPIO junginėja.

Antrą vertus naujasis interfeisas gan panašus į senąjį, tad ieškant informacijos galima užmesti akį ir į jį. Visai geras senojo aprašymas: http://derekmolloy.ie/kernel-gpio-programming-buttons-and-leds/ .

Naujasis interfeisas

Naujasis interfeisas geras tuo, kad nebenaudojami GPIO numeriai, o moduliui reikalingi GPIO aprašomi iš karto device-tree. Arba jeigu kas nors pasistengė į device-tree surašyti visus GPIO pavadinimus, užteks ir pavadinimo.

Dar vienas pliusas - naudojant metodus prasidedančius devm_ nebereikės sukti galvos apie resursų atlaisvinimą - jie bus automatiškai atlaisvinti atjungus modulį arba jam nulūžus.

Apie kitus privalumus išgirsite pažiūrėję youtube prezentaciją ar pasiskaitinėję skaidres.

Pereinamasis interfeisas

Pereinamasis interfeisas naująjį papildo dviem funkcijom, susiejančiomis gpio_desc objektą su senojo interfeiso GPIO numeriuku. Jis naudotinas tik perrašinėjant senas tvarkykles.
Papildomos funkcijos:

  • int desc_to_gpio(const struct gpio_desc *desc)
  • struct gpio_desc *gpio_to_desc(unsigned gpio)

Taimerių interfeisas

Linuxe yra visokiausių taimerių, bet mes naudosime high-resolution timerį, nes jis man labiausiai patiko. Labai trumpas ir gražus pavyzdys: https://stackoverflow.com/a/20303613

Įrenginio tvarkyklė

Iki šiol mes rašime paprastus kernelio modulius, kurie nebuvo tikros įrenginių tvarkyklės. Šį kartą rašysime tvarkyklę (draiverį) LEDui. Visa laimė, kad gerieji linux žmonės jau parašė pagalbines funkcijas, kurias naudojant dėl nieko nereikės sukti galvos: bus pasirūpinta device-tree paieška, atminties atlaisvinimu tiek atjungiant modulį, tiek jam nulūžus ir visom kitom smulkmenomis.
Tiesiog vietoj module_init() ir module_exit() reikės naudoti module_platform_driver.

container_of

container_of yra macro, kuri gali iš struktūros laukelio pasiimti pačia struktūrą. Labai naudinga macro naudojantis callback‘ais ir norint perduoti daugiau duomenų, nei įmanoma paduoti per parametrus. Detaliau su ja susipažinsime praktiniame pavyzdyje.

Praktika

Praktika susidės iš dviejų dalių: device-tree ir tvarkyklės kodo. Panagrinėkime kiekvieną atskirai:

device-tree

Per device-tree mes nurodysime kurį GPIO kontaktą (pin) naudoti. Tam į pagrindinę sekciją įdedame tokią atšaką:

labukas.clink
1
2
3
4
ledo_lempute {
compatible = "mirksiukas";
led-gpios = <&pio 8 11 GPIO_ACTIVE_HIGH>;
};

Pavadinimas ‘ledo_lempute’ visiškai nesvarbus. Svarbu tik, kad nesikirstų su jau panaudotais pavadinimais. Toliau įrašome ‘compatible’, pagal kurį mūsų tvarkyklė ir susiras šią atšaką. ‘led-gpios’ nurodome kurį GPIO naudosime. Šiam pavyzdžiui pasirinkau PI11 išvadą. P kontroleris šiame device-tree aprašyras kaip pio, o raidė I skaičiuojant nuo 0 yra aštunta, todėl ir rašome <&pio 8 11 >. GPIO_ACTIVE_HIGH reiškia, kad jeigu kode išvestį nurodysime kaip 1, tai ir fiziniame kontakte bus aukštas signalas. Jeigu būtume nurodę atvirkščiai - GPIO_ACTIVE_LOW - nurodžius vienetą LEDas užgestų.

Tvarkyklė

Tvarkyklę aprašiau komentaruose pačiame kode.

mirksiukas.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of_platform.h>
#include <linux/gpio/consumer.h>
#include <linux/hrtimer.h>
#include <linux/sched.h>

/*
* Šį kartą nenaudosime glabalių kintamųjų.
* Viską sukrausime į struktūrą ir tampysimės ją per funkcijas.
*/
struct mirksiukas_data {
struct gpio_desc *ledas;
struct hrtimer htimer;
ktime_t period;
u8 is_on;
};

/*
* Ši funkcija bus iškviečiama kas sekundę. Čia įjungiamas arba išjungiamas LED.
*/
static enum hrtimer_restart on_pulse(struct hrtimer * timer)
{
//Tai magiškoji container_of funkcija apie kurią buvo rašyta aukščiau.
//Pasinaudosime ja gauti savo duomenų struktūrą.
struct mirksiukas_data *data = container_of(timer, struct mirksiukas_data, htimer);

//Pamirksiname LEDą:
data->is_on = !data->is_on;
gpiod_set_value(data->ledas, data->is_on);

//Nustatome laikmatį kitam mirktelėjimui
hrtimer_forward_now(&data->htimer, data->period);
return HRTIMER_RESTART;
}

/*
* Inicializavimo funkcija. Iškviačiama kai insmod'inamas modulis.
*/
static int mirksiukas_probe(struct platform_device *pdev)
{
//Naudosime devm_* funkciją, tad nebereikės sukti galvos dėl atminties atlaisvinimo
struct mirksiukas_data *data = devm_kzalloc(&pdev->dev,
sizeof(struct mirksiukas_data), GFP_KERNEL);

if(!data)
return -ENOMEM;

//Mūsų duomenų struktūros dar prireiks atjungiant modulį (mirksiukas_remove),
//todėl prijungiame ją prie pdev.
platform_set_drvdata(pdev, data);

//Pasiimame GPIO iš device-tree, pagal led-gpios įrašą. Iš karto nustatome į 0.
data->ledas = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->ledas)) {
dev_err(&pdev->dev, "Nepavyko gauti GPIO. GPIO užimtas?\n");
return PTR_ERR(data->ledas);
}

//Inicializuojame taimerį. mirksinsime kas sekundę
data->period = ktime_set(1, 0); //sekundės, nanosekundės
hrtimer_init (&data->htimer, CLOCK_REALTIME, HRTIMER_MODE_REL);
data->htimer.function = on_pulse;
hrtimer_start(&data->htimer, data->period, HRTIMER_MODE_REL);

return 0;
}

static int mirksiukas_remove(struct platform_device *pdev)
{
struct mirksiukas_data *data = platform_get_drvdata(pdev);

//palaukime kol baigs veikti on_pulse (jeigu ji veikia) ir sustabdkime taimerį.
hrtimer_cancel(&data->htimer);

//Prieš išeinant būtų gražu išjungti šviesą.
gpiod_set_value(data->ledas, 0);

return 0;
}

//Laukas 'compatible' bus naudojamas surandant device-tree atšaką
static const struct of_device_id match[] = {
{ .compatible = "mirksiukas", },
{},
};

/*
* Vietoje module_init ir module_exit naudosime module_platform_driver.
* Šiai funkcijai reikia paduoti 'struct platform_driver' struktūrą,
* kurioje aprašoma pati tvarkyklė: inicializavimo/atjungimo funkcijos,
* tvarkyklės pavadinimas ir device-tree paieškos kriterijus.
*/
static struct platform_driver mirksiukas_driver = {
.probe = mirksiukas_probe,
.remove = mirksiukas_remove,
.driver = {
.name = "mano-mirksiukas",
.of_match_table = match,
},
};

module_platform_driver(mirksiukas_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Piktas Zuikis");
MODULE_DESCRIPTION("Linux kernel driver for blinking a LED via GPIO.");

Bandymai

Norėdami paleisti mūsų tvarkyklę iš pradžių sukompiliuojame naująjį device-tree: įmetame jį prie kitų kernelio device-tree į arch/arm/boot/dts/ ir iš pagrindinio kernelio katalogo paleidžiame:

1
make sun7i-a20-pcduino3b.dtb

Tada sukompiliuotą failą arch/arm/boot/dts/sun7i-a20-pcduino3b.dtb perkeliame į kompiuteriuką ir nustatome, kad įjungiant būtų naudojamas mūsiškis. Paprasčiausia tiesiog pakeisti senąjį ‘*.dtb’ failą naujuoju.

Perkrovus kompiuteriuką perkeliame ir sukompiliuojame savo tvarkyklę, prijungiame LEDą ir aktyvuojame sukompiliuotą modulį:

1
insmod mirksiukas.ko

Ir štai rezultatas:

Kaip visada, kodą rasite github’e.