Kernel Headers
To compile the driver, you need Linux kernel headers. They are required to compile the code that interfaces with the kernel [Rpika].
$ sudo apt update $ sudo apt full-upgrade $ sudo apt install raspberrypi-kernel-headers
If it installs in another folder with different version numbers, you can link as
$ sudo ln -s /lib/modules/x.xx.xx-new/build/ /lib/modules/x.x.old/build
or
$ apt search raspberrypi-kernel-headers $ sudo apt-get install raspberrypi-kernel-headers=1.20210303-1
to install specific version.
For 64-bit Debian, the following commanads can be used. [Der15A].
$ sudo apt-get update $ apt-cache search linux-headers-$(uname -r) $ sudo apt-get install linux-headers-$(uname -r) $ cd /usr/src/linux-headers-$(uname -r) $ ls
Testing the Headers
To quickly test compiling a driver, a folder is created and the code file simdrv.c is created in that folder [Kal17].
$ cd ~ $ mkdir simdrv $ cd simdrv $ nano simdrv.c
When the nano editor is opened, put the following code and press 'ctrl+o' and enter to save it. Then 'ctrl+x' to exit to the command prompt.
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); static int __init simdrv_init(void){ printk(KERN_INFO "SIMDRV: init."); return 0; } static void __exit simdrv_exit(void){ printk(KERN_INFO "SIMDRV: exit."); } module_init(simdrv_init); module_exit(simdrv_exit);
Thereafter, create Makefile to build the module. The content of the make file is as follows. Note that it must be a tab character in front of make in line 4 and line 6.
obj-m+=simdrv.o all: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
The files are available at the following link.
https://github.com/yan9a/rpi/tree/master/sys/simdrv
Compile the module using 'make', list, load the module using 'insmod', check the information about the module using 'lsmod' and 'modinfo', and unload using 'rmmod' as follows.
$ ls $ make $ ls $ sudo insmod simdrv.ko $ lsmod $ modinfo simdrv.ko $ sudo rmmod simdrv.ko
Kernel building for Raspberry Pi
The headers have to match the the kernel version. If you are using a newly released kernel, the repo might not be updated because it can take several weeks. In that case, you can clone the kernel and build it [Rpikb].
$ sudo apt install git bc bison flex libssl-dev make $ git clone --depth=1 https://github.com/raspberrypi/linux
Use --branch option to download a different branch.
$ git clone --depth=1 --branch <branch> https://github.com/raspberrypi/linux
After cloning, go to the linux folder and configure the kernel depending on Pi version.
For Pi 1, Pi Zero, Pi Zero W, and Compute Module,
$ cd linux $ KERNEL=kernel $ make bcmrpi_defconfig
For Pi 2, Pi 3, Pi 3+, and Compute Module 3,
$ cd linux $ KERNEL=kernel7 $ make bcm2709_defconfig
For Pi 4,
$ cd linux $ KERNEL=kernel7l $ make bcm2711_defconfig
For Pi 4 64 bit version,
$ cd linux $ KERNEL=kernel8 $ make bcm2711_defconfig
Thereafter, build and install the kernel which can take a long time.
$ make -j4 zImage modules dtbs $ sudo make modules_install $ sudo cp arch/arm/boot/dts/*.dtb /boot/ $ sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/ $ sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/ $ sudo cp arch/arm/boot/zImage /boot/$KERNEL.img
You can use 'uname -r' command to see the updated kernel version after rebooting the machine.
Driver
After setting up the required kernel headers, and testing the simple driver, let us explore more about the module code by discussing an example cedevdrv.c which can be found at
https://github.com/yan9a/rpi/blob/master/sys/cedevdrv/cedevdrv.c
We need to include a few headers in the module C code.
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h>
We include 'init.h' for mark up function macros, 'module.h' for loading the module into the kernel, and 'kernel.h' for kernel types, macros, and functions.
#include <linux/device.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/mutex.h> #include <linux/slab.h>
Similarly, the kernel driver model, file system support, access to user function, mutex, slab for memory allocation are included. Thereafter, the device name is defined as "cedevdrv", so that it will appear as /dev/cedevdrv in the device directory. The device class name is also defined.
#define DEVICE_NAME "cedevdrv" #define CLASS_NAME "cedev"
The following macros are used to define the module information.
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Cool Emerald"); MODULE_DESCRIPTION("A Linux device driver"); MODULE_VERSION("1.0");
When we load the module into the kernel using 'insmod' command, we can pass additional information to the device driver by using arguments. As an example, we define an argument 'cedrvarg' as a character pointer.
static char *cedrvarg = "cedriver"; module_param(cedrvarg, charp, S_IRUGO); MODULE_PARM_DESC(cedrvarg,"To use with printk");
As global variables are kernel wide, we use static keyword to restrict its scope to within the module. The arguments for module_param macro are name, type, and permission. In this case, the type is charp for character pointer. The details about the permission can be found at the following link.
https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html
As kernel code cannot use user space libraries, we cannot use functions such as printf. Instead, we can call printk to print in /var/log/kern.log. We should define log level KERN_INFO, KERN_ALERT, KERN_CRIT, KERN_ERR, KERN_WARNING, etc. when using it.
Linux Devices
On Linux systems, the devices are classified into three types: network devices, block devices, and character devices [LDD3]. Network devices are represented as network interfaces. They can be seen when using 'ifconfig' command from userspace. Storage devices such as hard disks are block devices and appear in /dev directory. Devices that transfer data such as serial port, or sound are character devices. They also appear in /dev directory. The command
$ ls -l /dev
lists devices in dev directory as shown in the following figure.
As seen in the first column in Figure 3, character devices are identified by 'c' and block devices are identified by 'b'. The associated major and minor numbers for the device drivers are also listed in column 5 and 6. You can manually create a file entry and associate your device driver as follow.
$ sudo mknod /dev/test c 66 1
To avoid conflict in major number, it is better to automatically assign an unused major number to use. We declare a global variable 'majorNumber' to store the automatically assigned device number, a device class structure pointer, and device structure pointer.
static int majorNumber; static struct class* cedevdrvClass = NULL; static struct device* cedevdrvDevice = NULL;
Then, cedevdrv_init() and cedevdrv_exit() functions are defined which are executed when the module is loaded and unloaded respectively.
When loading, we register the character device using 0, to get a dynamically assigned major number. The DEVICE_NAME is defined as "cedevdrv" as mentioned before. The structure 'fops' is discussed in the following section. Then, create the device class using CLASS_NAME, and create the device using device class, majorNumber, and DEVICE_NAME.
static int __init cedevdrv_init(void) { printk(KERN_INFO "CEDRV: init %s\n",cedrvarg); // memory allocation mes = kmalloc(sizeof(char)*256,GFP_KERNEL); // register char device majorNumber = register_chrdev(0,DEVICE_NAME, &fops); if(majorNumber<0) { printk(KERN_ALERT "CEDRV: registering chrdev failed\n"); return majorNumber; } // create device class cedevdrvClass = class_create(THIS_MODULE,CLASS_NAME); if(IS_ERR(cedevdrvClass)){ unregister_chrdev(majorNumber,DEVICE_NAME); printk(KERN_ALERT "CEDRV: creating device class failed\n"); return PTR_ERR(cedevdrvClass); } // create device cedevdrvDevice = device_create(cedevdrvClass,NULL,MKDEV(majorNumber,0),NULL,DEVICE_NAME); if(IS_ERR(cedevdrvDevice)){ class_destroy(cedevdrvClass); unregister_chrdev(majorNumber,DEVICE_NAME); printk(KERN_ALERT "CEDRV: creating device failed\n"); return PTR_ERR(cedevdrvDevice); } printk(KERN_INFO "CEDRV: device %s created\n",cedrvarg); return 0; }
When unloading, we destroy device, unregister and destroy device class, and unregister the character device.
static void __exit cedevdrv_exit(void) { device_destroy(cedevdrvClass,MKDEV(majorNumber,0)); class_unregister(cedevdrvClass); class_destroy(cedevdrvClass); unregister_chrdev(majorNumber,DEVICE_NAME); // free memory kfree(mes); printk(KERN_INFO "CEDRV: %s exited\n",cedrvarg); }
Then use module_init and module_exit macros from 'linux/init.h' to identify them.
module_init(cedevdrv_init); module_exit(cedevdrv_exit);
A device driver needs a structure with the specific functions [Mosi15]. The 'file_operations' structure holds pointers to these functions and it is defined in '/linux/fs.h'. We need to provide implementation for functions to read, write, open and close the device. And these functions needs to be associated with callback functions in file_operations structure. We can declare the function prototypes as follows.
static int device_open(struct inode *inodep, struct file *filep); static int device_release(struct inode *inodep, struct file *filep); static ssize_t device_write(struct file *filep, const char *buffer, size_t length, loff_t * offset); static ssize_t device_read(struct file *filep, char *buffer, size_t length, loff_t * offset);
These functions will be registered to the kernel as defined in the structure file_operations when the driver is loaded.
static struct file_operations fops = { .owner = THIS_MODULE, .open = device_open, .release = device_release, .read = device_read, .write = device_write };
We print device open and close messages as kernel information.
static int device_open(struct inode *inodep, struct file *filep) { if(!mutex_trylock(&cedevmutex)){ printk(KERN_ALERT "CEDRV: device is busy\n"); return -EBUSY; } printk(KERN_INFO "CEDRV: device opened\n"); return 0; } static int device_release(struct inode *inodep, struct file *filep) { mutex_unlock(&cedevmutex); printk(KERN_INFO "CEDRV: device closed\n"); return 0; }
The characters being written to the device are stored in a global character array variable called 'mes'.
static char mes*; static size_t meslen = 0;
We then copy the characters from buffer to mes.
static ssize_t device_write(struct file *filep, const char *buffer, size_t length, loff_t * offset) { size_t bytesToCopy = length >= 255 ? 255: length; size_t bytesNotCopied = 0; bytesNotCopied = copy_from_user(mes, buffer, bytesToCopy); meslen = bytesToCopy - bytesNotCopied; printk(KERN_INFO "CEDRV: received %zu bytes\n",meslen); if(bytesNotCopied){ printk(KERN_INFO "CEDRV: Failed to receive %zu characters",bytesNotCopied); return -EFAULT; } return bytesToCopy; }
When the device is being read from user space, we send back the requested number of characters from the message.
static ssize_t device_read(struct file *filep, char *buffer, size_t length, loff_t * offset) { size_t sent_count=0; size_t bytesToCopy = length >= meslen ? meslen : length; size_t bytesNotCopied = 0; if(!bytesToCopy) return 0; bytesNotCopied = copy_to_user(buffer,mes,bytesToCopy); sent_count = bytesToCopy - bytesNotCopied; if(sent_count){ printk(KERN_INFO "CEDRV: sent %zu bytes\n",meslen); } if(bytesNotCopied){ printk(KERN_INFO "CEDRV: error in sending %d bytes\n",bytesNotCopied); return -EFAULT; } meslen=0;//reset len return bytesToCopy; }
If you want to prevent the device from being used by multiple processes at the same time, you can use a mutex lock. For that, you need to include 'linux/mutex.h' and declare a mutex as follows.
#include <linux/mutex.h> static DEFINE_MUTEX(cedevmutex);
The mutex is initialized and destroyed when loading and unloading respectively. Add
mutex_init(&cedevmutex);
near the end of cedevdrv_init() and
mutex_destroy(&cedevmutex);
at the beginning of cedevdrv_exit().
When the device is opened, we try to lock the mutex first,
if(!mutex_trylock(&cedevmutex)){ printk(KERN_ALERT "CEDRV: device is busy\n"); return -EBUSY; }
and release the mutex when releasing the device.
mutex_unlock(&cedevmutex);
The complete source file is at
https://github.com/yan9a/rpi/blob/master/sys/cedevdrv/cedevdrv.c
The Makefile for cedevdrv.c is similar to the one in the previous section and it is shown below.
obj-m += cedevdrv.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Thereafter, you can build and load the driver. When you are loading the driver, you can set its input argument. The command 'modinfo' can be used to check the module also.
$ make $ ls -l *.ko $ modinfo cedevdrv.ko $ sudo insmod cedevdrv.ko cedrvarg=CoolDriver $ ls -l /dev/cedevdrv* $ modinfo cedevdrv.ko
You can unload the driver using 'rmmod'. The kern.log can be checked also.
$ sudo rmmod cedevdrv.ko $ tail -f /var/log/kern.log $ dmesg | grep cedevdrv
The output of building the module and loading the driver on an Ubuntu desktop machine is illustrated in the following Figure.
Using the Device
A simple program cedevtest.cpp which is in the same source directory as the driver source file is used to test the device. It demonstrates opening, writing, reading back and closing the device.
https://github.com/yan9a/rpi/blob/master/sys/cedevdrv/cedevtest.cpp
We can build the program and run it using sudo as follows.
$ g++ cedevtest.cpp -o cedevtest $ sudo ./cedevtest $ tail -f /var/log/kern.log
The output of running the driver testing program and checking kern.log is shown in the following figure.
Device Access Rules
To avoid the requirement of using sudo to use the device, we can define udev rules [Der15B]. For that, we can identify the sysfs entry, KERNEL and SUBSYSTEM values as follows.
$ sudo find /sys -name "cedevdrv" $ udevadm info -a -p /sys/class/cedev/cedevdrv
Thereafter, we can add a new rule file in /etc/udev/rules.d directory. A rule file name starts with a priority number and we use '99-cedevdrv.rules' for the device. Open nano editor to create the file.
$ sudo nano /etc/udev/rules.d/99-cedevdrv.rules
Add the rule as follow and press ctrl+o and ctrl+x to save and exit.
KERNEL=="cedevdrv", SUBSYSTEM=="cedev", MODE="0666"
After reloading the driver using 'insmod' command, we can see the premission of the device is updated by listing it in the /dev directory. Now, the test program can be executed without using sudo.
$ ls -l /dev/cedevdrv* # crw-rw-rw- 1 root root 241, 0 Jan 23 15:45 /dev/cedevdrv
Digital IO
We will discuss an example driver ceiodev.c. To use gpio and interrupt, we need to include
#include <linux/gpio.h> #include <linux/interrupt.h>
as additional header files. And define module information as usual.
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Cool Emerald"); MODULE_DESCRIPTION("GPIO driver for RPi"); MODULE_VERSION("1.0");
Thereafter, we can define GPIO pins, a variable to store interrupt number, and function prototype for interrupt handler. [Der15C].
static unsigned int pinOut = 17; // GPIO 17 which is pin 11 on the header static unsigned int pinIn = 27; // GPIO 27 which is pin 13 on the header static unsigned int irqno; static irq_handler_t pininput_handler(unsigned int irq, void *dev_id, struct pt_regs *regs);
When the driver is loaded, we check the validity of the pins, request, set direction, and export the pins. Then set up the interrupt as follows.
static int __init ceiodev_init(void) { int irqreqresult = 0; printk(KERN_INFO "CEIO: init\n"); if(!gpio_is_valid(pinOut)){ printk(KERN_INFO "CEIO: invalid pinOut\n"); return -ENODEV; } gpio_request(pinOut,"sysfs"); gpio_direction_output(pinOut,1); // set dir out and set it 1 (high) gpio_export(pinOut,false); // export to appear as /sys/class/gpio/gpio17 // false to disable changing the direction if(!gpio_is_valid(pinIn)){ printk(KERN_INFO "CEIO: invalid pinIn\n"); return -ENODEV; } gpio_request(pinIn,"sysfs"); gpio_direction_input(pinIn); // set dir input gpio_export(pinIn,false); // export to appear as /sys/class/gpio/gpio27 irqno = gpio_to_irq(pinIn); printk(KERN_INFO "CEIO: irq no: %d\n",irqno); irqreqresult = request_irq(irqno,(irq_handler_t)pininput_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"ceio_pin_handler",NULL); // ceio_pin_handler will appear as owner identity in /proc/interrupts printk(KERN_INFO "CEIO: irq result: %d\n",irqreqresult); return 0; }
Again, unexport the pins, and free the interrupt when the device is unloaded.
static void __exit ceiodev_exit(void) { gpio_set_value(pinOut,0); gpio_unexport(pinOut); gpio_free(pinOut); free_irq(irqno,NULL); gpio_unexport(pinIn); gpio_free(pinIn); printk(KERN_INFO "CEIO: exit\n"); }
Finally, the interrupt handler implementation is added where the pin input is read and the pin output is set according to the input.
static irq_handler_t pininput_handler(unsigned int irq, void *dev_id, struct pt_regs *regs) { int s = gpio_get_value(pinIn); gpio_set_value(pinOut,s); printk(KERN_INFO "CEIO: pin input: %d\n",s); return (irq_handler_t)IRQ_HANDLED; }
To test the driver, we can connect an LED series with 1 kΩ resistor at pin 11 (gpio17). The LED should immediately light up when the driver is loaded. To test the input pin 13 (gpio27), we can use another IO pin, for example, pin 7 (gpio4) to drive the input. For that, we can connect a jumper wire from pin 7 to pin 13.
We can use bash commands to toggle pin 7 as follows.
$ cd /sys/class/gpio $ ls # export gpiochip0 gpiochip100 gpiochip504 unexport $ echo 4 > export $ ls # export gpio4 gpiochip0 gpiochip100 gpiochip504 unexport $ cd gpio4 $ echo out > direction $ echo 1 > value $ echo 0 > value $ # ... to unexport after using the pin $ cd .. $ echo 4 > unexport
After building and loading the driver, we can see that gpio17 and gpio27 also appear in /sys/class/gpio folder as the driver exported them.
$ cd /sys/class/gpio $ ls # export gpio17 gpio27 gpio4 gpiochip0 gpiochip100 gpiochip504 unexport
As you toggle the input pin, you will see that the output LED following the input. You can see the messages from the driver in kern.log as shown in the following figure.
Using sysfs
We can map our driver in sysfs to interface with user space. Example driver can be seen at
https://github.com/yan9a/rpi/blob/master/sys/ceiofs/ceiofs.c
To appear the driver as /sys/ceiofs in the file system, we can use kobject for sysfs bindings. And linux headers for threading, using udelay, and time are added also.
#include <linux/kobject.h> #include <linux/kthread.h> #include <linux/delay.h> #include <linux/time.h>
For the purpose of producing pulses when 1 is written to the sysfs file called 'pulsing', we declare a bool variable 'pulseEn', and a variable for spinlock, and implement the callback function, 'pulsing_store'. Similarly, to return the status of pulsing when reading the file, we implement 'pulsing_show'.
static bool pulseEn = 0; static spinlock_t tightLoop; static ssize_t pulsing_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { unsigned int b; sscanf(buf,"%u",&b); pulseEn = b; return count; } static ssize_t pulsing_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { unsigned int b = pulseEn; return sprintf(buf,"%u\n",b); }
Use the following macro to define the name and access level of the file.
static struct kobj_attribute pulsing_attr = __ATTR(pulsing,0664,pulsing_show,pulsing_store);
Then, the name of the files to appear under the directory '/sys/ceiofs/pulser' are declared in the attribute array.
static struct attribute *pulser_attrs[] = { &pulsing_attr.attr, NULL, };
Thereafter, we can define the name and its attributes for the attribute group which is exposed in sysfs. A pointer to the kobject and a pointer to the thread task are declared also.
static struct attribute_group pulser_group = { .name = "pulser", .attrs = pulser_attrs, }; static struct kobject *ceiofs_kobj; static struct task_struct *task;
The function for the main thread loop is implemented as follows.
static int pulses_task(void *arg) { unsigned long flags; ktime_t t0, tn; uint64_t intervalns; uint64_t pulsewidthns = 50000; // pulse width tick interval in ns int i=0; printk(KERN_INFO "CEIO: Thread has started.\n"); while(!kthread_should_stop()){ // enter high priority pulses loop spin_lock_irqsave(&tightLoop, flags); //udelay(50); t0 = ktime_get(); i = 0; while(pulseEn){ tn = ktime_get(); intervalns = ktime_to_ns(ktime_sub(tn, t0)); if( intervalns >= pulsewidthns ) { t0 = tn; gpio_set_value(pinOut,i%2); i++; } if(i>=200) { pulseEn = 0; gpio_set_value(pinOut,0); } } spin_unlock_irqrestore(&tightLoop, flags); // exit high priority mag pulses loop msleep(50); } printk(KERN_INFO "CEIO: Thread has stopped.\n"); return 0; }
In the init function, we setup kobject, setup io pin, and run the kthread task.
static int __init ceiofs_init(void) { int result = 0; printk(KERN_INFO "CEIO: init.\n"); ceiofs_kobj = kobject_create_and_add("ceiofs",kernel_kobj->parent); if(!ceiofs_kobj){ printk(KERN_ALERT "CEIO: error in creating kobject\n"); return -ENOMEM; } result = sysfs_create_group(ceiofs_kobj,&pulser_group); if(result){ printk(KERN_ALERT "CEIO: error in creating sysfs group\n"); kobject_put(ceiofs_kobj); return result; } gpio_request(pinOut,"sysfs"); gpio_direction_output(pinOut,false); gpio_export(pinOut,false); spin_lock_init(&tightLoop); task = kthread_run(pulses_task,NULL,"Pulsing_thread"); kthread_bind(task, 2); if(IS_ERR(task)){ printk(KERN_ALERT "CEIO: error in running thread\n"); return PTR_ERR(task); } return result; }
We then stop the task, clean up the kobject, and free the io pin in the exit function and identify as usual.
static void __exit ceiofs_exit(void) { kthread_stop(task); kobject_put(ceiofs_kobj); gpio_set_value(pinOut,0); gpio_unexport(pinOut); gpio_free(pinOut); printk(KERN_INFO "CEIO: exit.\n"); } module_init(ceiofs_init); module_exit(ceiofs_exit);
After making and inserting the driver module as usual, we can go to /sys/ceiofs/pulser directory where you can produce pulses by writing 1 to 'pulsing' file using echo command and use cat command to read the attribute as follows.
$ make $ sudo insmod ceiofs.ko $ cd /sys/ceiofs/pulser $ ls $ sudo sh -c "echo 1 > pulsing" $ cat pulsing $ sudo rmmod ceiofs.ko $ cd -
Inserting LKM at Startup
To automatically load a module at startup, copy the driver into system driver directory, and add the driver configuration file in modules-load.d directory as follows.
$ sudo cp ceiofs.ko /lib/modules/$(uname -r)/kernel/drivers/ceiofs.ko $ sudo depmod $ sudo sh -c "echo ceiofs > /etc/modules-load.d/ceiofs.conf"
References
[Rpika] RaspberryPi.org. Kernel Headers.url: https://www.raspberrypi.org/documentation/linux/kernel/headers.md.
[Mosi15] Mosi\_62. Simple I/O device driver for RaspberryPi. 2015 September 26.
url: https://www.codeproject.com/Articles/1032794/Simple-I-O-device-driver-for-RaspberryPi.
[Der15A] Derek Molloy. Writing a Linux Kernel Module — Part 1: Introduction. 2015 April 14.
url: http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/.
[Kal17] Michal Kalbarczyk. The Beginner’s Guide to Linux Kernel Module, Raspberry Pi and LED Matrix. 2017 September 10.
url: https://puddleofcode.com/story/the-beginners-guide-to-linux-kernel-module-raspbery-pi-and-led-matrix.
[Rpikb] RaspberryPi.org. Kernel building.
url: https://www.raspberrypi.org/documentation/linux/kernel/building.md.
[Der15B] Derek Molloy. Writing a Linux Kernel Module — Part 2: A Character Device. 2015 April 18.
url: http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/.
[Der15C] Derek Molloy. Writing a Linux Kernel Module — Part 3: Buttons and LEDs. 2015 April 26.
url: http://derekmolloy.ie/kernel-gpio-programming-buttons-and-leds/.
[LDD3] Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman. Linux Device Drivers, Third Edition. O'Reilly. 2005.
url: https://lwn.net/Kernel/LDD3/.
No comments:
Post a Comment
Comments are moderated and don't be surprised if your comment does not appear promptly.