Getting started with Linux drivers

1. Driver introduction
The Linux driver is essentially a software program. The upper layer software can communicate with the computer hardware through the interface provided by the driver without knowing the hardware characteristics.

The system call is the interface between the kernel and the application, and the driver is the interface between the kernel and the hardware. It shields the hardware details for the application program, so for the application program, the hardware device is just a device file, and the application program can operate the hardware device like an ordinary file.

The Linux driver is only a part of the kernel, managing the system’s device controller and corresponding devices. Driver, the English name is “Device Driver”, the full name is “Device Driver”, which is a special program that enables the computer to communicate with the device. It is equivalent to the interface of the hardware. Only through this interface can the operating system control the work of the hardware device. It mainly completes the following functions:

1. Initialize and release the device.

2. Transfer data to hard disk and read data from hardware.

3. Detect and deal with device errors.

2. Driver classification
The hardware of a computer system consists of a CPU, memory, and peripherals. The targets of the driver are both memory and peripherals. Linux divides peripherals and memory into three basic categories: block device drivers, character device drivers, and network device drivers.

2.1, character device driver
Character devices are those devices that must be accessed in serial order, and the I/O operations of character devices are not cached. Character device operations are byte-based, but operations can only be performed one byte at a time. Typical such as LCD, serial port, LED, buzzer, touch screen and so on.

2.2, block device driver
Block devices are defined relative to character devices, can be accessed in any order, and are operated in units of blocks. The reading and writing of the block device driver is supported by a cache, and the block device must be able to be accessed randomly. The block size of the device is defined when the device itself is designed, and the software cannot change it. The block size of different devices can be different. Common block devices are storage devices, such as: hard disk, NandFlash, iNand, SD and so on.

2.3, network device driver
The network device driver is a driver model specially designed for the network card, and it is designed for receiving and sending data packets, and it should not be used for the nodes of the file system. That is, it does not correspond to the device files in the /dev directory, and the application program finally uses the socket word to complete the interface with the network device.

In addition to network devices, character devices and block devices are mapped to files and directories in the Linux file system, and character devices can be accessed through the file system system call interface open(), write(), read(), close(), etc. and block devices. A block device is more complicated than a character device. A disk/Flash file system will first be established on it, such as FAT, EXT3, TAFFS, TFFS, etc. FAT, EXT3, TAFFS, and TFF standardize the organization of files and directories on storage media.

3. Driver compilation and loading
Linux device drivers are part of the kernel, and a module of the Linux kernel can be compiled and loaded in two ways.

3.1. Compilation method
Internal compilation: put the driver source code in the kernel source code directory for compilation.

External compilation: Compile the driver source code outside the kernel source code directory.

3.2. Loading method
Static loading: compiled into uImage, loaded directly when the system starts.

Dynamic loading: Compile the .ko file and dynamically load the driver module.

3.3. Compiler
Architectures such as x86 can use gcc, and arm embedded devices need to use related cross-compilation tool chains.

The following are examples of kernel modules:
#include <linux/module.h> //所有模块都需要的头文件
#include <linux/init.h> // init&exit相关宏

static int __init hello_init (void)
{
printk(“Hello module init\n”);
return 0;
}

static void __exit hello_exit (void)
{
printk(“Hello module exit\n”);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“LYB”);
MODULE_DESCRIPTION(“test for linux driver”);

Analyzing the above program, it is found that a Linux kernel module needs to include module initialization and module unloading functions, the former runs when insmod, and the latter runs when rmmod. The initialization and unloading functions must be defined before the macros module_init and module_exit are used, otherwise compilation errors will occur.

The initialization and unloading functions must be defined before the macros module_init and module_exit are used, otherwise compilation errors will occur. In the program:

MODULE_LICENSE (“GPL”) is used to declare the license of the module.

MODULE_AUTHOR: Describe the author information..

MODULE_DESCRIPTION: Description of this driver.

If you want to directly compile it into the Linux kernel, you need to copy the source code file into the corresponding path of the Linux kernel source code, and modify the Makefile.

The task of the module initialization function is to prepare for calling the module’s functions later, as if the module is saying, “Here I am, this is what I can do”.

The module’s exit function ( hello_exit in the example) is called when the module is unloaded. It seems to tell the kernel, “I’m not there anymore, don’t ask me to do anything”.

This approach to programming is similar to event-driven programming, but while not all applications are event-driven, every kernel module is. Another major difference, between event-driven applications and kernel code, is the exit function: a terminated application can be lazy in freeing resources, or not do cleanup at all, but a module’s exit function must be careful to restore every something established by the initialization function, otherwise something remains until the system is rebooted.

Write a Makerfile to compile:
KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/ #内核源码目录/usr/src/linux-headers-$(shell uname -r)/
PWD := $(shell pwd)

obj-m := driverTest.ko

all:
make -C $(KERN_DIR) M=$(PWD) modules

clean:
make -C $(KERN_DIR) M=$(PWD) clean

3.4. Driver loading, unloading and debugging

insmod ./hello.ko // 加载驱动

lsmod // 查看已加载的驱动
lsmod | grep hello // 使用grep检索过滤

demsg // 查看内核打印信息
demsg | grep hello // 使用grep过滤信息

rmmod hello // 卸载驱动

4. Module/driver loading process
Insmod 1.ko ->load file in memory->sys_init_module(1.ko, file size, parameter address)->load_module(hello.ko, file size, parameter address).

Module loading source code analysis: module_init source code analysis_Coder personal blog blog-CSDN blog.

5. Module/driver uninstall process
Module uninstall process:

1. rmmod module name, call exit

2. Use name to find the mod structure of the module to be uninstalled.

3. Verify the relationship between the modules (depended by other modules), and the modules with the relationship cannot be uninstalled.

4. free_module.

Module uninstallation source code analysis: module_exit source code analysis_Coder personal blog’s blog-CSDN blog.

6. The process of application layer control equipment
The call to the driver by the Linux application is shown in the figure below:

In Linux, everything is a file. After the driver is successfully loaded, a corresponding file will be generated in the “/dev” directory. The application program uses this file named “/dev/xxx” (xxx is the specific driver file name) Carry out the corresponding operation to realize the operation of the hardware. For example, now there is a driver file named /dev/led, which is the driver file of led. The application program uses the open function to open the file /dev/led, and uses the close function to close the file /dev/led after use. open and close are the functions to turn on and off the led driver. If you want to turn on or turn off the led, then use the write function to operate, that is, to write data to the driver. This data is the control parameter to turn off or turn on the led. If you want to get the state of the led light, use the read function to read the corresponding state from the driver.

Applications run in user space, while Linux drivers are part of the kernel, so drivers run in kernel space. When we want to operate the kernel in the user space, such as using the open function to open the /dev/led driver, because the user space cannot directly operate the kernel, we must use a method called “system call” to implement from the user The space is trapped in the kernel space, so that the operation of the underlying driver can be realized. Functions such as open, close, write, and read are provided by the C library. In the Linux system, system calls are part of the C library. When we call the open function, the process is shown in the following figure:

We don’t need to worry about the C library and how to fall into the kernel space through system calls. We focus on the application and the specific driver. The functions used by the application have corresponding functions in the specific driver. For example, if the function open is called in the application program, then there must also be a function named open in the driver program. Each system call has a corresponding driver function in the driver. There is a structure called file_operations in the Linux kernel file include/linux/fs.h. This structure is a collection of Linux kernel driver operation functions. As follows:
struct file_operations {
struct module *owner; //owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
loff_t (*llseek) (struct file *, loff_t, int);//llseek 函数用于修改文件当前的读写位置。
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//read 函数用于读取设备文件。
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//write 函数用于向设备文件写入(发送)数据。
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);//poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);//compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
int (*mmap) (struct file *, struct vm_area_struct *);//mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
int (*open) (struct inode *, struct file *);//open 函数用于打开设备文件。
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);//release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
int (*fsync) (struct file *, loff_t, loff_t, int datasync);//fsync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
int (*fasync) (int, struct file *, int);//fasync 函数与 fsync 函数的功能类似,只是fasync 是异步刷新待处理的数据。
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
} __randomize_layout;

7. Driver development process
The general development process of the driver is as follows:

1. Check the schematic diagram and data sheet to understand the operation method of the equipment.

2. Find a similar driver in the kernel and develop it as a template, sometimes starting from scratch.

3. Realize the initialization of the driver, such as registering the driver with the kernel.

4. Design the operations to be realized: such as open, close, read, write and other functions.

5. It is not necessary for every device driver to implement interrupt service.

6. Compile the driver to the kernel, or dynamically load it as a module.

7. Test drive.

Eight, LED device driver
led_drv.c file content:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>

#include <asm/io.h>
#include <asm/uaccess.h>

#define GPJ0CON_PHY_ADDR 0xE0200240
#define GPJ0DAT_PHY_ADDR 0xE0200244

static dev_t dev_id;
static struct cdev *led_dev;
static struct class *led_class;

static volatile unsigned int *gpj0_con = NULL;
static volatile unsigned int *gpj0_dat = NULL;

int led_open(struct inode *inode, struct file *file)
{
unsigned int cfg;

/* 将GPIO设置为输出模式 */
cfg = readl(gpj0_con);
writel(cfg | (1<<12), gpj0_con);
/* 熄灭led */
cfg = readl(gpj0_dat);
writel(cfg | (1<<3), gpj0_dat);

return 0;
}

ssize_t led_write(struct file *file, const char __user *data, size_t size, loff_t *loff)
{
int val, ret;
unsigned int cfg;

/* 从用户空间拷贝数据 */
ret = copy_from_user(&val, data, sizeof(val));
cfg = readl(gpj0_dat);
if (val == 0) //熄灭
{
writel(cfg | (1<<3), gpj0_dat);
} else if (val == 1) //点亮
{
writel(cfg & ~(1<<3), gpj0_dat);
} else {
return -1;
}

return 0;
}

static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};

static __init int led_init(void)
{
/* 申请设备号 */
alloc_chrdev_region(&dev_id, 1, 1, “led”);

/* 分配字符设备 */
led_dev = cdev_alloc();

/* 设置字符设备 */
cdev_init(led_dev, &led_fops);

/* 注册字符设备 */
cdev_add(led_dev, dev_id, 1);

/* 创建设备节点 */
led_class = class_create(THIS_MODULE, “led”); //创建类
device_create(led_class, NULL, dev_id, NULL, “led”); //创建设备节点

/* 映射物理地址 */
gpj0_con = (volatile unsigned int *)ioremap(GPJ0CON_PHY_ADDR, 8);
gpj0_dat = gpj0_con+1;

return 0;
}

static __exit void led_exit(void)
{
/* 注销设备节点 */
device_destroy(led_class, dev_id);
class_destroy(led_class);

/* 注销字符设备 */
cdev_del(led_dev);
kfree(led_dev);

/* 注销注册的设备号 */
unregister_chrdev_region(dev_id, 1);

/* 注销映射的地址 */
iounmap(gpj0_con);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE(“GPL”);

Makefile content:

KERN_DIR = /work/linux/kernel

all:
make -C $(KERN_DIR) M=`pwd` modules

clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order

obj-m += led_drv.ko

Modify your kernel source tree, execute make, generate led_drv.ko, load the module through insmod led_drv.ko, and the device node of /dev/led will be generated at this time.

led_test.c file content:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define LED_DEV “/dev/led”

int main(int argc, char* argv[])
{
int val;
int fd;

if(argc != 2)
{
printf(“Usage: %s <on|off>\n”, argv[0]);
return -1;
}

fd = open(LED_DEV, O_RDWR);
if(fd < 0)
{
printf(“failed to open %s\n”, LED_DEV);
return -1;
}

if(!strcmp(argv[1], “on”))
val = 1;
else if(!strcmp(argv[1], “off”))
val = 0;
else
{
printf(“Usage: %s <on|off>\n”, argv[0]);
return -1;
}

write(fd, &val, sizeof(val));

close(fd);

return 0;
}

compile arm-linux-gcc -o led_test led_test.c

Execute led_test on, the led is turned on.

Execute led_test off, the led is turned off.