Skip to content

led module

Harry Broeders edited this page Nov 22, 2025 · 12 revisions

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 kunt implementeren als een Linux kernel module waardoor je de leds kunt aansturen door naar het bestand /dev/leds te schrijven.

Linux deelt programma's op tussen 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 user space programma's.

User space programma's draaien niet in de kernel maar maken gebruiken van verschillende interface-lagen 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.

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 genaamd /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

De module led_module.c maakt een character device genaamd /dev/leds aan. Je kunt deze kernel module als volgt compileren en installeren:

$ cd ~/CSC10_Development/led_driver
$ make
$ sudo insmod led_module.ko

Met 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/leds

De 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. 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 close

Omdat 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. 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 denied

Je 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.

Globale uitleg van de code in led_module.c

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_operationsmy_dev_fops. Er zijn nog meer koppelingen mogelijk, zoals lezen (.read), maar deze zijn in deze character device niet geïmplementeerd.

Globale variabelen en structuren

#define DEVNAME "MYLEDDEV"

static volatile unsigned int *PIO_ptr;

static dev_t dev = 0;
static int dev_major = 0;

static struct class *my_dev_class = NULL;

static struct cdev my_dev_cdev;

static const struct file_operations my_dev_fops = {
	.open = my_dev_open,
	.release = my_dev_release,
	.write = my_dev_write
};

static const struct of_device_id mijn_module_id[] ={
	{.compatible = "leds"},
	{}
};

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,
	.remove_new = clean_handler
};

Zie hier voor een uitleg waarom .remove_new in plaats van .remove gebruikt is.

Functie init_handler

static int init_handler(struct platform_device *pdev)
{
	printk(KERN_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
	int err = alloc_chrdev_region(&dev, 0, 1, "leds");
	if (err < 0) {
		printk(KERN_ALERT DEVNAME ": ERROR: no major device number available\n");
		return err;
	}
	// extract the assigned major device number
	dev_major = 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)) {
		printk(KERN_ALERT 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;

	// create device in /sys/devices/virtual/my_leds/leds
	err = cdev_add(&my_dev_cdev, MKDEV(dev_major, 0), 1);
	if (err < 0) {
		printk(KERN_ALERT 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, MKDEV(dev_major, 0), NULL, "leds");
	if (IS_ERR(my_dev)) {
		printk(KERN_ALERT 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
	void *mem_ptr = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(mem_ptr)) {
		printk(KERN_ALERT DEVNAME ": ERROR: no base address found for PIO device\n");
		err = PTR_ERR(mem_ptr);
		goto cleanup_device;
	}
	PIO_ptr = mem_ptr;
	return 0;
	// cleanup on errors
cleanup_device:	
	device_destroy(my_dev_class, MKDEV(dev_major, 0));
cleanup_cdev:
	cdev_del(&my_dev_cdev);
cleanup_class:
	class_destroy(my_dev_class);
cleanup_chrdev_region:
	unregister_chrdev_region(dev, 1);
	return err;
}

Functie clean_handler

static void clean_handler(struct platform_device *pdev)
{
	printk(KERN_INFO DEVNAME ": Destroy character device\n");

	device_destroy(my_dev_class, MKDEV(dev_major, 0));
	cdev_del(&my_dev_cdev);
	class_destroy(my_dev_class);
	unregister_chrdev_region(dev, 1);
}

Functie my_dev_uevent

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) {
		printk(KERN_ALERT DEVNAME ": ERROR: add_uevent_var failed\n");
		return ret;
	}
	return 0;
}

Functie my_dev_open

static int my_dev_open(struct inode *inode, struct file *file) 
{
	printk(KERN_INFO DEVNAME ": Device open\n");
	if ((file->f_flags & O_WRONLY) != O_WRONLY) {
		printk(KERN_ALERT DEVNAME ": ERROR: Device opened not in write only mode\n");
		return -EACCES;
	} 
	return 0;
}

Functie my_dev_release

static int my_dev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO DEVNAME ": Device close\n");
	return 0;
}

Functie my_dev_write

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;
	printk(KERN_INFO DEVNAME ": Device write\n");

	if (count < maxdatalen) {
		maxdatalen = count;
	}

	unsigned long ret = copy_from_user(buffer, buf, maxdatalen);
	if (ret) {
		printk(KERN_ALERT DEVNAME ": ERROR: copy_from_user failed\n");
		return ret;
	}

	size_t i;
	for (i = 0; i < maxdatalen; i++) {
		int value = (int)buffer[i];
		printk(KERN_INFO DEVNAME ": Data written 0x%02X\n", value);
		*PIO_ptr = value;
	}

	return maxdatalen;
}

Referenties

[1] T. Gayet, Linux Kernel Programming: Developing kernel architecture and device drivers for character, block, USB, and network interfaces. BPB Publications, 2025.

Clone this wiki locally