-
Notifications
You must be signed in to change notification settings - Fork 0
led module
wiki# LED Driver als character device
Door de opleiding Elektrotechniek van de Hogeschool Rotterdam is een Arch Linux image ontwikkeld die gebruikt kan worden op een DE1-SoC bord van Terasic. Hoe je deze Linux distributie kunt starten op het DE1-SoC bord vind je hier.
Op deze wiki-pagina wordt uitgelegd hoe je een character device (apparaat) kunt implementeren als een Linux kernel module waardoor je de leds kunt aansturen door naar het bestand /dev/leds te schrijven.
Linux deelt het (virtuele) geheugen op in kernel-space en user space. Zie figuur 1.
Figuur 1: User space en kernel space [1].
Kernel space is het geheugengebied waar de kernel van het besturingssysteem draait en die heeft volledige toegang tot de hardware en systeembronnen. Het is een beveiligde omgeving die niet direct toegankelijk is voor applicatieprogramma's.
User space is het geheugengebied waar de applicatieprogramma's draaien. Deze programma's kunnen gebruik maken van verschillende system calls die de kernel aanbiedt om indirect met hardware te communiceren. Zo'n user space programma kan wel nog steeds root rechten nodig hebben om bepaalde acties uit te kunnen voeren. Verdere informatie over de scheiding tussen user space en kernel space kun je indien gewenst op wikipedia vinden.
De code in de kernel is uit te breiden met kernel modules.
Een kernel module draait in kernel space, heeft direct toegang tot de hardware en kan geïnstalleerd worden in de kernel door een superuser (root) met behulp van het insmod commando.
Met het rmmod commando kan een kernel module weer uit de kernel verwijderd worden.
Extra uitleg over kernel modules met een eenvoudig voorbeeld vind je op de wiki-pagina hello kernel module.
Om vanuit user space programma's gebruikt te kunnen maken van onze kernel module zonder superuser rechten, kunnen we onze module een character device aan laten maken.
Door onze driver een character device aan te laten maken wordt er onder de /dev map een virtuele file aangemaakt waar een user space programma of terminal naar kan schrijven en lezen.
De kernel module ziet deze schrijf- en leesacties en kan hierop reageren.
In de kernel module led_module.c wordt het character device /dev/leds gekoppeld aan de leds van je systeem.
Met als gevolg dat je leds vanuit user space aan kunt sturen door te schrijven naar de 'file' /dev/leds.
In Linux zijn er verschillende soorten devices (apparaten), zoals character devices, block devices en network devices. Een character device is een type apparaat dat data byte voor byte verwerkt. Bijvoorbeeld seriële poorten, toetsenborden en muizen zijn character devices. Een block device is een type apparaat dat data in blokken verwerkt. Bijvoorbeeld harde schijven en USB-memorysticks zijn block devices. Een network device is een type apparaat dat netwerkcommunicatie mogelijk maakt. Voorbeelden hiervan zijn netwerkkaarten en draadloze adapters.
Je kunt alle character en block devices die beschikbaar zijn op een Linux systeem vinden in de map /dev. Het volgende commando produceert een lijst met, onder andere, alle beschikbare character en block devices:
$ ls -l /dev
total 0
...
brw-rw---- 1 root disk 179, 0 Oct 9 2024 mmcblk0
brw-rw---- 1 root disk 179, 1 Oct 9 2024 mmcblk0p1
brw-rw---- 1 root disk 179, 2 Oct 9 2024 mmcblk0p2
brw-rw---- 1 root disk 179, 3 Oct 9 2024 mmcblk0p3
...
crw-rw-rw- 1 root tty 5, 0 Oct 9 2024 tty
...De eerste kolom van de output geeft het type device aan: een c voor character devices en een b voor block devices. Daarna volgt de permissies, eigenaar, groep, major en minor nummers datum van aanmaak en de naam van het device.
Permissies, major en minor nummers worden later op deze wiki-pagina uitgelegd.
Een lijst met beschikbare netwerk devices kun je vinden met het commando:
$ ls -l /sys/class/net
total 0
lrwxrwxrwx 1 root root 0 Oct 9 2024 end0 -> '../../devices/platform/sopc@0/ff702000.ethernet/net/end0'/
...De module led_module.c maakt een character device /dev/leds aan.
Je kunt deze kernel module als volgt compileren en installeren:
$ cd ~/CSC10_Development/led_driver
$ make
$ sudo insmod led_module.koMet het commando ls -l /dev/leds kun je zien dat het character device is aangemaakt.
Je kunt nu een karakter naar het device schrijven met het commando echo en de ASCII-code van dit karakter zal op de leds verschijnen.
# Zet de waarde 15 (0x0F ) op de leds:
$ echo -n -e "\x0F" >/dev/ledsDe optie -e van het echo-commando zorgt ervoor dat de escape-sequentie \x0F wordt
geïnterpreteerd en -n zorgt ervoor dat er geen nieuwe regelkarakter wordt toegevoegd.
De leds LEDR3 t/m LEDR0 zullen nu branden.
Vanuit de kernel module worden kernel messages gestuurd om te later zien welke operaties uitgevoerd worden. Je kunt de laatste 5 kernel messages bekijken met het commando dmesg:
$ dmesg -H | tail -5
[Nov22 20:27] MYLEDDEV: Create character device
[ +19.546603] MYLEDDEV: Device open
[ +0.000080] MYLEDDEV: Device write
[ +0.000010] MYLEDDEV: Data written 0x0F
[ +0.000023] MYLEDDEV: Device closeOmdat je de leds nu aan kunt sturen door simpelweg naar een bestand te schrijven
(zonder superuser rechten), kun je dit ook vanuit een C-programma doen.
Het C-programma genaamd leds.c dat laat zien hoe je dit kunt doen:
# include <stdio.h>
# include <unistd.h>
int main () {
FILE * fp = fopen ("/dev/leds", "w");
if (fp != NULL) {
int i;
for (i = 0; i < 17; i++) {
printf("Send 0x%02X to the leds\n", i);
fputc(i, fp);
fflush(fp);
sleep(1);
}
fclose(fp);
} else {
printf("Error: can not open /dev/leds\n");
}
return 0;
}De functie fopen opent het character device /dev/leds als een bestand voor schrijven ("w"). Met de functie fputc wordt een karakter naar het device geschreven.
De functie fflush zorgt ervoor dat de buffer wordt geleegd, zodat de waarde direct naar de leds wordt gestuurd.
Met de functie fclose wordt het bestand weer gesloten.
Al de bovenstaande functies zijn gedeclareerd in stdio.h.
Na het versturen van een karakter wordt steeds 1 seconde gewacht door de functie sleep aan te roepen, die gedeclareerd is in unistd.h.
Dit programma kun je als volgt compileren en uitvoeren:
$ gcc leds.c -o leds
$ ./leds
Send 0x00 to the leds
Send 0x01 to the leds
Send 0x02 to the leds
Send 0x03 to the leds
Send 0x04 to the leds
...Het C-programma genaamd leds_try_read.c probeert te lezen van het character device /dev/leds.
#include <stdio.h>
#include <unistd.h>
int main() {
FILE *fp = fopen("/dev/leds", "r");
if (fp != NULL) {
char buffer;
fread(&buffer, sizeof(char), 1, fp);
printf("Read character code 0x%02X", (int)buffer);
}
else {
printf("Error: can not open /dev/leds\n");
perror("Error message");
}
return 0;
}Dit programma kun je als volgt compileren en uitvoeren:
$ gcc leds_try_read.c -o leds_try_read
$ ./leds_try_read
Error: can not open /dev/leds
Error message: Permission deniedJe ziet dat het character device /dev/leds niet geopend kan worden om te lezen ("r").
Hieronder wordt de code in led_module.c eerst globaal en daarna stap voor stap toegelicht.
We beginnen onderaan in het bestand.
De macro module_platform_driver registreert de platform driver genaamd MYLEDDEV bij de Linux-kernel.
De struct mijn_module_driver bevat pointers naar functies die worden aangeroepen
als de driver wordt geladen (init_handler) en verwijderd (clean_handler).
Deze functies worden aangeroepen door de kernel wanneer een device wordt gevonden in
de devicetree die overeenkomt met de driver (op basis van het compatible veld).
In de functie init_handler wordt het character device /dev/leds aangemaakt en
wordt het adres van de PIO-module opgeslagen in de globale pointer pio_ptr.
In de functie clean_handler wordt het character device weer verwijderd.
De functies my_dev_open, my_dev_release en my_dev_write worden aangeroepen als een user space programma het character device opent, sluit, of beschrijft.
Deze koppelingen worden gemaakt in de struct file_operations my_dev_fops.
Er zijn nog meer koppelingen mogelijk, zoals lezen (.read), maar deze zijn in dit character device niet geïmplementeerd.
Alle globale variabelen en functies zijn static gedefinieerd zodat ze alleen binnen dit bronbestand zichtbaar zijn.
// virtual address of leds PIO
static void __iomem *pio_ptr;De globale pointer pio_ptr slaat het virtuele adres op van de PIO-module die de leds aanstuurt.
Deze pointer wordt geïnitialiseerd in de functie init_handler en gebruikt in de functie my_dev_write om de leds aan te sturen.
// device number
static dev_t dev = 0;
// device class structure
static struct class *my_dev_class = NULL;
// char device structure
static struct cdev my_dev_cdev;Deze globale variabelen worden gevuld in de functie init_handler als het character device aangemaakt wordt en in de functie clean_handler gebruikt om het character device weer te verwijderen.
// initialize file_operations
static const struct file_operations my_dev_fops = {
.open = my_dev_open,
.release = my_dev_release,
.write = my_dev_write
};
// describe which device we want to bind to this kernel module
// this must match an entry in the device tree
static const struct of_device_id mijn_module_id[] = {
{.compatible = "leds"},
{}
};
// platform driver structure linking handlers to events
static struct platform_driver mijn_module_driver = {
.driver = {
.name = DEVNAME,
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(mijn_module_id),
},
.probe = init_handler,
// See https://elixir.bootlin.com/linux/v6.6.22/source/include/linux/platform_device.h#L236 for an explanation why we use .remove_new in stead of .remove
.remove_new = clean_handler
};Deze globale variabelen worden gebruikt tijdens het installeren van de kernel module en bij het registreren van de platform driver en het aanmaken van het character device.
Een platform driver is bedoeld om hardware te ondersteunen die rechtstreeks is aangesloten op de adresbus van de processor. Zoals vaak gebeurt bij embedded systemen, zoals het DE1-SoC bord, waar verschillende hardware componenten (zoals PIO-modules, timers, UARTs, etc.) direct verbonden zijn met de processor zonder tussenkomst van een buscontroller. Dit in tegenstelling tot systemen die gebruik maken van een busarchitectuur, zoals PCI of USB, waar apparaten zijn aangesloten op een gedeelde bus en communiceren via gestandaardiseerde protocollen.
Deze platformdrivers zijn specifiek voor een bepaald platform en communiceren rechtstreeks met de hardware via registers en geheugenadressen die zijn gedefinieerd in de devicetree van het platform.
De struct mijn_module_driver bevat pointers naar functies die worden aangeroepen
als de driver wordt geladen (init_handler) en verwijderd (clean_handler).
Deze functies worden aangeroepen door de kernel wanneer een device wordt gevonden in
de devicetree die overeenkomt met de driver (op basis van het compatible veld dat gedefinieerd is in de struct of_device_id mijn_module_id).
Zie hier voor de uitleg waarom .remove_new in plaats van .remove gebruikt is.
// register this platform driver
module_platform_driver(mijn_module_driver);De macro module_platform_driver registreert de platform driver die gespecificeerd is in de globale variabele mijn_module_driver bij de Linux-kernel.
Deze macro genereert de boilerplate code die nodig is om een module te initialiseren en te beëindigen en vervangt dus de module_init en module_exit macro's.
Zie eventueel: hello-kernel-module.
Je kunt de platform driver terugvinden in het virtuele bestandssysteem onder /sys/bus/platform/drivers/MYLEDDEV:
$ ls -l /sys/bus/platform/drivers/MYLEDDEV
total 0
--w------- 1 root root 4096 Nov 27 20:36 bind
lrwxrwxrwx 1 root root 0 Nov 27 20:36 ff200010.gpio -> '../../../../devices/platform/sopc@0/ff200000.bridge/ff200010.gpio'/
lrwxrwxrwx 1 root root 0 Nov 27 20:36 module -> ../../../../module/led_module/
--w------- 1 root root 4096 Nov 27 20:36 uevent
--w------- 1 root root 4096 Nov 27 20:36 unbindDe link ff200010.gpio verwijst naar het device dat is gevonden in de devicetree.
$ ls -l /sys/devices/platform/sopc@0/ff200000.bridge/ff200010.gpio
total 0
lrwxrwxrwx 1 root root 0 Nov 27 20:37 driver -> ../../../../../bus/platform/drivers/MYLEDDEV/
-rw-r--r-- 1 root root 4096 Nov 27 20:37 driver_override
-r--r--r-- 1 root root 4096 Nov 27 20:37 modalias
lrwxrwxrwx 1 root root 0 Nov 27 20:37 of_node -> '../../../../../firmware/devicetree/base/sopc@0/bridge@0xff200000/gpio@0x100000010'/
drwxr-xr-x 2 root root 0 Nov 27 20:37 power/
lrwxrwxrwx 1 root root 0 Nov 27 20:37 subsystem -> ../../../../../bus/platform/
-rw-r--r-- 1 root root 4096 Nov 27 20:37 ueventDe link of_node verwijst naar de node in de devicetree die overeenkomt met dit device.
$ ls -l /sys/firmware/devicetree/base/sopc@0/bridge@0xff200000/gpio@0x100000010
total 0
-r--r--r-- 1 root root 4 Nov 27 20:38 '#gpio-cells'
-r--r--r-- 1 root root 4 Nov 27 20:38 altr,gpio-bank-width
-r--r--r-- 1 root root 4 Nov 27 20:38 clocks
-r--r--r-- 1 root root 5 Nov 27 20:38 compatible
-r--r--r-- 1 root root 0 Nov 27 20:38 gpio-controller
-r--r--r-- 1 root root 5 Nov 27 20:38 name
-r--r--r-- 1 root root 12 Nov 27 20:38 reg
-r--r--r-- 1 root root 4 Nov 27 20:38 resetvalueDe inhoud van de virtuele file compatible in deze map komt uiteraard overeen met de in mijn_module_id gedefinieerde waarde.
$ cat /sys/firmware/devicetree/base/sopc@0/bridge@0xff200000/gpio@0x100000010/compatible
ledsDe link module in /sys/bus/platform/drivers/MYLEDDEV verwijst naar de kernel module die deze driver implementeert.
$ ls -l /sys/module/led_module
total 0
-r--r--r-- 1 root root 4096 Nov 27 22:10 coresize
drwxr-xr-x 2 root root 0 Nov 27 22:10 drivers/
drwxr-xr-x 2 root root 0 Nov 27 22:10 holders/
-r--r--r-- 1 root root 4096 Nov 27 22:10 initsize
-r--r--r-- 1 root root 4096 Nov 27 22:10 initstate
drwxr-xr-x 2 root root 0 Nov 27 22:10 notes/
-r--r--r-- 1 root root 4096 Nov 27 22:10 refcnt
drwxr-xr-x 2 root root 0 Nov 27 22:10 sections/
-r--r--r-- 1 root root 4096 Nov 27 22:10 taint
--w------- 1 root root 4096 Nov 27 22:10 ueventDe inhoud van de virtuele file coresize in deze map geeft de grootte van de kernel module aan (in bytes).
$ cat /sys/module/led_module/coresize
12288Deze informatie had je ook kunnen vinden met het commando cat /proc/modules:
$ cat /proc/modules
led_module 12288 0 - Live 0x00000000 (O)
...MODULE_LICENSE("GPL");
MODULE_AUTHOR("Daniël Versluis, Harry Broeders");
MODULE_DESCRIPTION("Character device driver for LEDR7 downto LEDR0 on DE1-SoC board");Deze macro's bevatten metadata over de kernel module, zoals de licentie, auteur en een korte beschrijving. De matadata van de module kan worden bekeken met het commando:
$ modinfo led_module.ko
filename: /home/student/CSC10_Development/led_driver/led_module.ko
description: Character device driver for LEDR7 downto LEDR0 on DE1-SoC board
author: Daniël Versluis, Harry Broeders
license: GPL
depends:
name: led_module
vermagic: 6.6.22-1-lts-CSC10-csc10 SMP mod_unload ARMv7 p2v8 static int init_handler(struct platform_device *pdev)
{
pr_info(DEVNAME ": Create character device\n");
// allocate a range of char device numbers
// in this case only one minor number
// the major number will be assigned dynamically
// and can be found in /proc/devices
int err = alloc_chrdev_region(&dev, 0, 1, "leds");
if (err < 0) {
pr_err(DEVNAME ": ERROR: no major device number available\n");
return err;
}
// log the assigned major device number
pr_info(DEVNAME ": Major device number is %d\n", MAJOR(dev));
// create device class
// class leds is a reserved class name and can not be used. Therefore we use my_leds
my_dev_class = class_create("my_leds");
if (IS_ERR(my_dev_class)) {
pr_err(DEVNAME ": ERROR: class can not be created for device\n");
err = PTR_ERR(my_dev_class);
goto cleanup_chrdev_region;
}
// my_dev_uevent is called when a device is added to this class
my_dev_class->dev_uevent = my_dev_uevent;
// init new device
cdev_init(&my_dev_cdev, &my_dev_fops);
my_dev_cdev.owner = THIS_MODULE;
// register device in the kernel
err = cdev_add(&my_dev_cdev, dev, 1);
if (err < 0) {
pr_err(DEVNAME ": ERROR: device number can not be added for device\n");
goto cleanup_class;
}
// create device node /dev/leds
struct device *my_dev = device_create(my_dev_class, NULL, dev, NULL, "leds");
if (IS_ERR(my_dev)) {
pr_err(DEVNAME ": ERROR: device can not be created for driver\n");
err = PTR_ERR(my_dev);
goto cleanup_cdev;
}
// map the PIO device registers into virtual memory
pio_ptr = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(pio_ptr)) {
pr_err(DEVNAME ": ERROR: no base address found for PIO device\n");
err = PTR_ERR(pio_ptr);
goto cleanup_device;
}
return 0;
// cleanup on errors
cleanup_device:
device_destroy(my_dev_class, dev);
cleanup_cdev:
cdev_del(&my_dev_cdev);
cleanup_class:
class_destroy(my_dev_class);
cleanup_chrdev_region:
unregister_chrdev_region(dev, 1);
return err;
}De functie init_handler wordt aangeroepen als de kernel module wordt geladen en een device wordt gevonden in de devicetree die overeenkomt met de driver (op basis van het compatible veld).
De functie krijgt een pointer naar de struct platform_device mee die informatie bevat over het device dat gekoppeld is aan deze driver.
In deze functie wordt eerst een range van char device nummers toegewezen aan het device met behulp van de functie alloc_chrdev_region.
Elk device in Linux wordt geïdentificeerd door een uniek apparaatnummer, bestaande uit een major en minor nummer.
Het major nummer identificeert het type apparaat (device) of de driver die het apparaat beheert, terwijl het minor nummer een specifiek apparaat binnen dat type aanduidt.
In dit geval wordt er alleen het minor nummer 0 aangevraagd.
Aan de functie alloc_chrdev_region wordt daarom als tweede argument het eerste minornummer (0) en als derde argument het aantal minor nummers (1) doorgegeven.
Als eerste argument wordt een pointer naar een variabele van het type dev_t doorgegeven waarin het toegewezen apparaatnummer wordt opgeslagen.
Het major nummer wordt dynamisch toegewezen door de kernel.
Dit major nummer kan uit het apparaatnummer worden opgehaald met de macro MAJOR.
Als vierde argument wordt de gewenste naam van het device (apparaat) opgegeven, die zichtbaar is in /proc/devices.
Je kunt, nadat de kernel module geinstalleerd is, alle toegewezen apparaatnummers en -namen bekijken in het bestand /proc/devices met het commando:
$ sudo insmod led_module.ko
$ cat /proc/devices
Character devices:
1 mem
...
247 leds
...
Block devices:
1 ramdisk
...Vervolgens wordt er een device class aangemaakt met behulp van de functie class_create.
Door een device class aan te maken, wordt automatisch een device node in de /dev map aangemaakt.
Als argument van de functie class_create wordt de gewenste naam van de class doorgegeven.
De gewenste naam van de class is hier my_leds omdat leds een al gereserveerde naam is en dus niet gebruikt kan worden.
Je kunt alle device classes bekijken in de map /sys/class met het commando:
$ ls -l /sys/class
total 0
...
drwxr-xr-x 2 root root 0 Oct 9 2024 leds/
...
drwxr-xr-x 2 root root 0 Nov 23 15:12 my_leds/
...De return waarde van de functie class_create is een pointer naar de aangemaakte class.
Deze wordt opgeslagen in de globale variabele my_dev_class een pointer naar het type struct class.
In het veld dev_uevent van deze struct wordt een pointer naar de functie my_dev_uevent opgeslagen.
Deze functie wordt aangeroepen als een device aan de class wordt toegevoegd.
Hierdoor kunnen we de permissies van de device node in de map /dev aanpassen zodat alle gebruikers (lees- en schrijf-rechten) toegang hebben tot het character device.
Vervolgens wordt de struct cdev geïnitialiseerd met behulp van de functie cdev_init en wordt de koppeling gemaakt tussen het character device en de functies die de verschillende operaties op het device afhandelen (openen, sluiten, schrijven) door het adres van de globale variabel my_dev_fops door te geven.
Deze struct is van het type file_operations en bevat pointers naar de functies die de verschillende operaties op het device afhandelen (openen, sluiten, schrijven).
De globale variabele my_dev_fops is als volgt gedefinieerd:
// initialize file_operations
static const struct file_operations my_dev_fops = {
.open = my_dev_open,
.release = my_dev_release,
.write = my_dev_write
};Vervolgens wordt het veld owner van de struct my_dev_cdev ingesteld op THIS_MODULE.
Dit zorgt ervoor dat de kernel weet welke module verantwoordelijk is voor dit character device.
THIS_MODULE is een macro die verwijst naar het adres van een struct module die informatie bevat van de huidige kernel module.
Vervolgens wordt het character device toegevoegd aan het systeem met behulp van de functie cdev_add.
Als eerste argument wordt een pointer naar de struct my_dev_cdev doorgegeven.
Als tweede argument wordt het apparaatnummer dev doorgegeven.
Als derde argument wordt het aantal minor nummers (1) doorgegeven.
Daarna wordt het character device /dev/leds aangemaakt met behulp van de functie device_create.
Als eerste argument wordt de pointer naar de device class (my_dev_class) doorgegeven.
Als tweede argument wordt NULL doorgegeven omdat er geen parent device is.
Als derde argument wordt het apparaatnummer dev doorgegeven.
Als vierde argument wordt NULL doorgegeven omdat er geen extra data aan het device gekoppeld hoeft te worden.
Als vijfde argument wordt de naam van het device node ("leds") doorgegeven
In de map /sys/class/my_leds is de device node leds te vinden die door onze kernel module is aangemaakt:
$ ls -l /sys/class/my_leds
total 0
lrwxrwxrwx 1 root root 0 Nov 23 23:06 leds -> ../../devices/virtual/my_leds/leds/Je ziet dat leds verwijst naar de map /sys/devices/virtual/my_leds/leds/ waar meer informatie over het device te vinden is.
$ ls -l /sys/devices/virtual/my_leds/leds/
total 0
-r--r--r-- 1 root root 4096 Nov 23 23:08 dev
drwxr-xr-x 2 root root 0 Nov 23 23:08 power/
lrwxrwxrwx 1 root root 0 Nov 23 23:08 subsystem -> ../../../../class/my_leds/
-rw-r--r-- 1 root root 4096 Nov 23 23:08 ueventHet bestand dev bevat het apparaatnummer van het device in de vorm major:minor.
$ cat /sys/devices/virtual/my_leds/leds/dev
247:0Het bestand uevent bevat informatie die gebruikt wordt door udev om de device node in de map /dev aan te maken.
$ cat /sys/devices/virtual/my_leds/leds/uevent
MAJOR=247
MINOR=0
DEVNAME=leds
DEVMODE=0666De link subsystem verwijst naar de device class my_leds.
De map power bevat informatie over het energiebeheer van het device.
$ ls -l /sys/devices/virtual/my_leds/leds/power
total 0
-rw-r--r-- 1 root root 4096 Nov 23 23:13 autosuspend_delay_ms
-rw-r--r-- 1 root root 4096 Nov 23 23:13 control
-r--r--r-- 1 root root 4096 Nov 23 23:13 runtime_active_time
-r--r--r-- 1 root root 4096 Nov 23 23:13 runtime_status
-r--r--r-- 1 root root 4096 Nov 23 23:13 runtime_suspended_timeMeer uitleg over device power management vind op https://docs.kernel.org/6.6/driver-api/pm/devices.html.
Tot slot wordt het fysieke adres van de PIO-module gemapt naar een virtueel adres in de kernel space met behulp van de functie devm_platform_ioremap_resource.
Als eerste argument wordt de pointer pdev die wijst naar de struct platform_device doorgegeven.
Deze pointer is door de kernel meegegeven aan de functie init_handler en bevat informatie over het device dat gekoppeld is aan deze driver.
Als tweede argument wordt 0 doorgegeven om aan te geven dat we het eerste resource (de PIO-module) willen mappen.
Het geretourneerde virtuele adres wordt opgeslagen in de globale pointer pio_ptr.
Het virtuele adres kan later in de functie my_dev_write gebruikt worden om de leds aan te sturen met behulp van de functie writel.
Het character device /dev/leds is toegankelijk voor alle gebruikers omdat in de functie my_dev_uevent de permissies van deze device node zijn aangepast naar 0666 (de octale code 666) (lees- en schrijf-rechten voor alle gebruikers).
Meer uitleg over file permissies in Linux vind je op https://linuxize.com/post/understanding-linux-file-permissions/.
Als er een fout optreedt tijdens het aanmaken van het character device, worden de eerder aangemaakte resources weer opgeruimd in de cleanup-sectie aan het einde van de functie.
Waarschijnlijk is je vast wel eens verteld dat je goto-statements moet vermijden in je code, zie eventueel ook het beroemde artikel van Edgar Dijkstra uit 1968: Go To Statement Considered Harmful.
Het goto-statment komt hier echter goed van pas om de cleanup-code overzichtelijk te houden.
Dit is een uitzondering op de regel dat goto vermeden moet worden.
De resources worden verwijderd in de omgekeerde volgorde van aanmaak om te voorkomen dat er naar niet meer bestaande resources verwezen wordt.
static void clean_handler(struct platform_device *pdev)
{
pr_info(DEVNAME ": Destroy character device\n");
device_destroy(my_dev_class, dev);
cdev_del(&my_dev_cdev);
class_destroy(my_dev_class);
unregister_chrdev_region(dev, 1);
}De functie clean_handler wordt aangeroepen als de kernel module wordt verwijderd.
De functie krijgt een pointer naar de struct platform_device mee die informatie bevat over het device dat gekoppeld is aan deze driver.
In deze functie wordt het character device /dev/leds verwijderd met behulp van de functie device_destroy.
Vervolgens wordt het character device verwijderd uit het systeem met behulp van de functie cdev_del.
Daarna wordt de device class verwijderd met behulp van de functie class_destroy.
Tot slot wordt het toegewezen apparaatnummer vrijgegeven met behulp van de functie unregister_chrdev_region.
De resources worden verwijderd in de omgekeerde volgorde van aanmaak om te voorkomen dat er naar niet meer bestaande resources verwezen wordt.
// callback function called when device is added to class
// used to set the permissions of the device node to 0666 (read and write for all users)
static int my_dev_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
int ret = add_uevent_var(env, "DEVMODE=%#o", 0666);
if (ret < 0) {
pr_err(DEVNAME ": ERROR: add_uevent_var failed\n");
return ret;
}
return 0;
}Deze functie wordt aangeroepen als een device aan de class wordt toegevoegd.
Hierdoor kunnen we de permissies van het character device leds in de map /dev aanpassen zodat alle gebruikers (lees- en schrijf-rechten) toegang hebben tot het character device.
Dit wordt gedaan door de functie add_uevent_var aan te roepen om een extra variabele toe te voegen aan de uevent (/sys/devices/virtual/my_leds/leds/uevent) die gebruikt wordt door udev om de device node in de map /dev aan te maken.
In dit geval wordt de variabele DEVMODE toegevoegd met als waarde 0666 (de octale code 666) (lees- en schrijf-rechten voor alle gebruikers).
Meer uitleg over file permissies in Linux vind je op https://linuxize.com/post/understanding-linux-file-permissions/.
De extra variabele in de uevent kan worden bekeken in de map /sys/devices/virtual/my_leds/leds/uevent:
$ cat /sys/devices/virtual/my_leds/leds/uevent
MAJOR=247
MINOR=0
DEVNAME=leds
DEVMODE=0666De rechten van de device node /dev/leds kunnen ook bekeken worden met het commando:
$ ls -l /dev/leds
crw-rw-rw- 1 root root 247, 0 Nov 23 23:03 /dev/ledsJe ziet dat alle gebruikers lees- en schrijf-rechten hebben (rw-rw-rw-).
In dit geval hebben we de device node (/dev/leds) aangemaakt in de kernel module.
Maar je kunt ook een device node handmatig aanmaken met het commando mknod:
$ sudo mknod -m 0666 alt_leds c 247 0We hebben nu een character device node genaamd alt_leds aangemaakt in het huidige directory.
Met de optie -m 0666 worden de permissies van de device node ingesteld op lees- en schrijf-rechten voor alle gebruikers.
Met het argument c wordt aangegeven dat het een character device is.
De volgende twee argumenten (247 en 0) zijn respectievelijk het major en minor nummer van het device.
Deze eigenschappen van de device node alt_leds kunnen bekeken worden met het commando:
$ ls -l alt_leds
crw-rw-rw- 1 root root 247, 0 Nov 25 19:20 alt_ledsJe kunt de leds nu ook aansturen door naar de lokale device node alt_leds te schrijven.
$ echo -n -e '\xF0' > alt_ledsDe vier leds LEDR7 t/m LEDR4 zullen nu branden.
De lokale device node alt_leds kan weer verwijderd worden met het commando rm:
$ rm alt_ledsstatic int my_dev_open(struct inode *inode, struct file *file)
{
pr_info(DEVNAME ": Device open\n");
if ((file->f_flags & O_WRONLY) != O_WRONLY) {
pr_err(DEVNAME ": ERROR: Device opened not in write only mode\n");
return -EACCES;
}
return 0;
}Deze functie wordt aangeroepen als een user space programma het character device opent.
De functie krijgt twee parameters mee: een pointer naar de inode van het device (zie eventueel https://man7.org/linux/man-pages/man7/inode.7.html) en een pointer naar de struct file die het geopende bestand representeert.
Deze struct bevat onder andere de bestandsmodus waarin het device is geopend (lees-, schrijf- of lees/schrijf-modus).
Deze modus is terug te vinden in het veld f_flags van de struct file.
De mogelijke modi zijn gedefinieerd in fcntl.h
De functie controleert of het device in schrijf-only modus (O_WRONLY) is geopend.
Als dat niet het geval is, wordt er een foutcode -EACCES teruggegeven.
static int my_dev_release(struct inode *inode, struct file *file)
{
pr_info(DEVNAME ": Device close\n");
return 0;
}Deze functie wordt aangeroepen als een user space programma het character device sluit.
In dit geval wordt er alleen een kernel message gelogd en wordt er 0 teruggegeven om aan te geven dat het sluiten succesvol is verlopen.
static ssize_t my_dev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
char buffer[128];
size_t maxdatalen = sizeof buffer;
pr_info(DEVNAME ": Device write\n");
if (count < maxdatalen) {
maxdatalen = count;
}
unsigned long ret = copy_from_user(buffer, buf, maxdatalen);
if (ret) {
pr_err(DEVNAME ": ERROR: copy_from_user failed\n");
return ret;
}
size_t i;
for (i = 0; i < maxdatalen; i++) {
int value = (int)buffer[i];
pr_info(DEVNAME ": Data written 0x%02X\n", value);
writel(value, pio_ptr);
}
return maxdatalen;
}Deze functie wordt aangeroepen als een user space programma data naar het character device schrijft.
De functie krijgt vier parameters mee: een pointer naar de struct file die het geopende bestand representeert, een pointer naar de buffer met de data die geschreven moet worden, de grootte van de data en een pointer naar de huidige offset in het bestand.
Omdat we de data die geschreven wordt meteen doorsturen naar de leds, wordt de offset niet gebruikt.
Daarom moet de data eerst gekopieerd worden naar een buffer in de kernel space. Als er meerdere bytes worden geschreven, worden deze één voor één naar de leds gestuurd. Dit zal echter niet zichtbaar zijn omdat de waarde op de leds steeds meteen overschreven wordt door de volgende byte. Maar het laat wel zien hoe je meerdere bytes kunt verwerken en als je een logic analyser aansluit op de leds, kun je de verschillende waarden wel zien.
Eerst wordt de maximale grootte van de data die verwerkt kan worden bepaald door de grootte van de lokale buffer (buffer) te vergelijken met de grootte van de data die geschreven moet worden (count).
De grootte van het lokale buffer is hier 128 bytes gekozen zonder specifieke reden.
De data die geschreven moet worden, waar de parameter const char __user *buf naar wijst, bevindt zich in de user space.
Dit wordt aangegeven door het gebruik van het attribuut __user.
Deze data kan niet direct door de kernel gelezen worden omdat user space en kernel space gescheiden zijn.
De data kan van de user space buffer (buf) naar de kernel space buffer (buffer) gekopieerd worden met behulp van de functie copy_from_user.
Vervolgens worden de gekopieerde bytes één voor één verwerkt en naar de leds geschreven met behulp van de functie writel.
De functie retourneert het aantal verwerkte bytes (maxdatalen).
Als dit aantal kleiner is dan het aantal bytes dat geschreven moest worden (count), dan wordt de functie door de kernel automatisch nogmaals aangeroepen met de rest van de data.
[1] T. Gayet, Linux Kernel Programming: Developing kernel architecture and device drivers for character, block, USB, and network interfaces. BPB Publications, 2025.
[2] P. J. Salzman, M. Burian, O. Pomerantz, B. Mottram, and J. Huang, The Linux Kernel Module Programming Guide, 2025. Online
[3] R. Giometti, Linux Device Drivers Development Cookbook. Packt Publishing, 2019.
[4] J. Corbet, A. Rubini, and G. Kroah-Hartman, Linux Device Drivers, 3rd Edition. O'Reilly Media, 2005. Online