LPH9157-2 kernelio modulis

Jau parašėme programėlę rodyti vaizdams per SIEMENS C75 mobiliojo telefono ekranėlį LPH9157-2, tačiau ar norime čia ir sustoti? Tikrai ne! Mes norime, kad šis ekranėlis mums tarnautų kaip pilnavertis monitorius. Juk mažam kompiuteriui reikia ir mažo ekraniuko.

Teorija

Ko gi mums trūksta, kad galėtume paversti šią mūsų svajonę realybe? Ogi reikia parašyti Linux kernelio tvarkyklę. Beveik taip pat kaip ir anksčiau parašyta programa, bet vietoj vaizdų nuskaitymo iš failo, grafinius duomenis paimtų iš framebuferio. Kaip mums tai padaryti? Googlas mums pasakys! Pradedame knistis ir štai ką randame:

Būtent paskutiniuoju punktu ir turėtų vadovautis naują ekranėlio tvarkyklę kuriantys programuotojai. Mes imsime taip ir padarysime. Kaip pavyzdį naudosime oficialią ekranėlio mi0283qt tvarkyklę.

Kodas

Pradžių pradžia

Kaip ir visuose kernelio moduliuose viskas kas svarbiausia yra apačioje, tad pradedame nuo tvarkyklės inicializavimo struktūrų:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//device-tree paieškos kriterijai
static const struct of_device_id lph9157_of_match[] = {
{ .compatible = "lph9157" },
{},
};
MODULE_DEVICE_TABLE(of, lph9157_of_match);

static struct spi_driver lph9157_spi_driver = {
.driver = {
.name = "lph9157",
.owner = THIS_MODULE,
.of_match_table = lph9157_of_match,
},
.probe = lph9157_probe, //paleidžiama inicializavimo metu
.remove = lph9157_remove, //paleidžiama pašalinant modulį
.shutdown = lph9157_shutdown, //paleidžiama išjungiant OS
};
module_spi_driver(lph9157_spi_driver);

Kaip matome – nieko įmantraus, tiesiog užregistruojame device-tree taisykles ir nurodome probe, remove ir shutdown funkcijas.

Funkcija probe paleidžiama kai aptinkamas ekranėlis. Kadangi SPI neturi įrenginių aptikimo protokolo, mūsų ekranėlis bus visada automatiškai aptiktas vos tik užkrovus modulį (žinoma jeigu device-tree tinkamai aprašytas).

Funkcija Probe

Kaip pasiimami GPIO galite prisiminti ankstesniame straipsnyje apie GPIO kernelio modulį.

Tikriausiai svarbiausia vieta yra mipi_dbi_spi_init funkcijos iškvietimas. Į ją paduodame struktūrose aprašytą tvarkyklės informaciją ir piešimo funkcijas.

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
//Surašome funkcijas, kurios bus naudojamos piešimui. Mums pasisekė, nes jų 
//implementuoti nereikės - naudosime jau kažkieno parašytas
static const struct drm_simple_display_pipe_funcs lph9157_pipe_funcs = {
.enable = mipi_dbi_pipe_enable,
.disable = mipi_dbi_pipe_disable,
.update = tinydrm_display_pipe_update,
.prepare_fb = tinydrm_display_pipe_prepare_fb,
};

//Nurodome ekranėlio plotį ir aukštį pikseliais ir milimetrais
static const struct drm_display_mode lph9157_mode = {
TINYDRM_MODE(132, 176, 37, 27),
};

//Čia aprašome bendrą tvarkyklės informaciją. Kas ką reiškia galite pasiskaitinėti čia:
//https://www.kernel.org/doc/html/v4.11/gpu/drm-internals.html#c.drm_driver
DEFINE_DRM_GEM_CMA_FOPS(lph9157_fops);

static struct drm_driver lph9157_driver = {
.driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME | DRIVER_ATOMIC,
.fops = &lph9157_fops,
TINYDRM_GEM_DRIVER_OPS,
.lastclose = tinydrm_lastclose,
.debugfs_init = mipi_dbi_debugfs_init,
.name = "lph9157",
.desc = "Ekranėlio LPH9157-2 video draiveris.",
.date = "20171011",
.major = 1,
.minor = 0,
};

//Pagalbinė struktūra, kurioje saugosime kintamuosius, kad nepasimestų.
struct modulio_apjungti_duomenys
{
struct mipi_dbi *mipi;
struct gpio_desc *dc;
struct gpio_desc *power;
};

static int lph9157_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
struct tinydrm_device *tdev;
struct modulio_apjungti_duomenys *data;

//Išskiriame atminties (kerneliška malloc versija su automatiniu atlaisvinimu)
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
data->mipi = devm_kzalloc(dev, sizeof(*data->mipi), GFP_KERNEL);

//Pasiimame GPIO visai kaip GPIO kernelio modulio pavyzdyje
data->power = devm_gpiod_get_optional(dev, "power", GPIOD_OUT_HIGH);
data->mipi->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
data->dc = devm_gpiod_get_optional(dev, "dc", GPIOD_OUT_LOW);

//Inicializuoja spi ir mipi struktūras
mipi_dbi_spi_init(spi, data->mipi, data->dc, &lph9157_pipe_funcs, &lph9157_driver,
&lph9157_mode, 0);

//Pridedame 'hakų'
mipi_command_base = data->mipi->command;
data->mipi->command = mipi_command_override;

//Laikas išsiųsti ekranėliui inicializacijos komandas
lph9157_init(data->mipi);

//Nepameskime mūsų apjungtų duomenų, jų mums prireiks modulio atjungimo metu!
spi_set_drvdata(spi, data);

//Užregistruojame savo tvarkyklę - sukurs /dev/fb1
tdev = &data->mipi->tinydrm;
devm_tinydrm_register(tdev);

printk(KERN_INFO "Initialized %s:%s @%uMHz on minor %d\n",
tdev->drm->driver->name, dev_name(dev),
spi->max_speed_hz / 1000000,
tdev->drm->primary->index);

return 0;
}

Kadangi kažkoks geras žmogus piešimu jau pasirūpino, mums nieko daryti nereikės, yra tik viena problemėlė – mūsų ekranėliui reikia nestandartinių piešimo lango nustatymo koordinačių. Reikia koordinačių po vieną baitą, o standartinės funkcijos siunčia po du, todėl perimsime kernelio duomenis ir juos pakoreguosime taip, kaip mums reikia.
Tam ir skirtas tas “hakas” – mipi_command_override. Čia toks savotiškas senoviškas C stiliaus override:

1
2
3
4
5
6
7
8
9
10
11
12
static int mipi_command_override(struct mipi_dbi *mipi, u8 cmd, u8 *data, size_t len)
{
if(len == 4 && (cmd == MIPI_DCS_SET_COLUMN_ADDRESS || cmd == MIPI_DCS_SET_PAGE_ADDRESS))
{
//Suglaudžiame baitukus drauge ir pameluojam, kad jie tik du :)
data[0] = data[1];
data[1] = data[3];
len = 2;
}

return (*originaCommand)(mipi, cmd, data, len); //Iškviečiame standartinę funkciją
}

Funkcija lph9157_init

Dabar imsimės ekranėlio inicializacijai skirtos funkcijos. Visai kaip ir paprastoje userspace programėlėje (funckija initializeDevice), nusiųsim reset komandą, inicializuosim atmintį ir nustatysime spalvų paletę. Modulyje, skirtingai nei programoje kintamųjų pavadinimai standartizuoti, bet esu tikras be vargo atsirinksite.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int lph9157_init(struct mipi_dbi *mipi)
{
struct tinydrm_device *tdev = &mipi->tinydrm;
struct device *dev = tdev->drm->dev;

mipi_dbi_hw_reset(mipi);
mipi_dbi_command(mipi, MIPI_DCS_SOFT_RESET);

msleep(20);

mipi_dbi_command(mipi, MIPI_DCS_SET_ADDRESS_MODE, 0b00000000);
mipi_dbi_command(mipi, MIPI_DCS_EXIT_SLEEP_MODE);
mipi_dbi_command(mipi, MIPI_DCS_SET_PIXEL_FORMAT, 0x05); //16 bitų paletė
mipi_dbi_command(mipi, MIPI_DCS_SET_DISPLAY_ON);

msleep(20);

return 0;
}

Funckijos lph9157_shutdown ir lph9157_remove

Liko tik apsivalymo funkcijos, kurių viena iškviečiama rmmod metu, o kita išjungiant kompiuteriuką. Šiaip tai nėra būtina, bet mes vien dėl mandagumo nunulinsime GPIO, kad ekranėlis iki galo išsijungtų. Tada nekils problemų dar kartą kviečiant insmod, nes mūsų prietaisėliui nepatinka būti inicializuotam du kartus be išjungimo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void lph9157_poweroff(struct modulio_apjungti_duomenys *data)
{
gpiod_set_value(data->power, 0);
gpiod_set_value(data->dc, 0);
gpiod_set_value(data->mipi->reset, 0);
}

//iškviečiama išjungiant/perkraunant linuxus
static void lph9157_shutdown(struct spi_device *spi)
{
struct modulio_apjungti_duomenys *data = spi_get_drvdata(spi);
lph9157_poweroff(data);
tinydrm_shutdown(&data->mipi->tinydrm);
}

//iškviečiama, kai pašalinamas modulis (rmmod)
static int lph9157_remove(struct spi_device *spi)
{
lph9157_poweroff(spi_get_drvdata(spi));
return 0;
}

device-tree

Tam, kad kernelis aptiktų ekranėlį ir žinotų kuriuos GPIO naudoti reikės pamodifikuoti device-tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&spi0 {
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins_a>,
<&spi0_cs0_pins_a>;
status = "okay";
lph9157@0{
compatible = "lph9157";
reg = <0>;
spi-max-frequency = <12000000>;
dc-gpios = <&pio 7 10 GPIO_ACTIVE_HIGH>;
reset-gpios = <&pio 7 5 GPIO_ACTIVE_HIGH>;
power-gpios = <&pio 7 9 GPIO_ACTIVE_HIGH>;
};
};

Štai ir viskas! modulis parašytas! Kodą kaip visada rasite githube, liko tik testavimas.

Testavimas

Kodas kompiliuojasi paprastai – visai kaip ir kiti moduliai, tiesiog parašome make ir viskas baigta.
Paskui imamės insmod:

1
2
3
4
5
6
7
8
9
10
11
12
make

#Iš pradžių mums reikės mipi_dbi modulio, nes jame yra mūsų naudojamos standartinės funkcijos.
#Mano kernelyje mipi_dbi sukompiliuotas kaip modulis, todėl reikia jį modprobinti.
#Jeigu mipi_dbi būtų įkompiliuotas į kernelį, to daryti mums nereikėtų:
modprobe mipi_dbi

#Įjungiame mūsų ekranėlio modulį:
insmod lph9157-2.ko

#Patikriname ar viskas pavyko:
dmesg | tail

Kaip matome viskas puiku: dmesg parodė, kad užregistruotas naujas įrenginys, o ir pačiam ekranėlyje pasirodė kažkoks triukšmas:

ekranėlis po inicializavimo

Laikas konsolei! Linux konsolę į ekranėlį iš pagrindinio monitoriaus perkelsime pasinaudoję komanda con2fbmap:

1
2
3
con2fbmap 1 1
#Taip pat sumažiname ir šriftą:
setfont /usr/share/consolefonts/Lat7-VGA8.psf.gz -C /dev/tty0

Mums pavyko:

konsolė ekranėlyje

Bet gražiau atrodo paversta:

1
echo 1 > /sys/class/graphics/fbcon/rotate

paversta konsolė ekranėlyje

Panašu, kad tvarkyklė veikia! Mūsų pačių parašyta, krauju ir prakaitu aplaistyta, bet veikia! Veikia!