9.1 Character device driver development

1. Introduction to character device driver

  Character device is the most basic type of device driver among Linux drivers. A character device is a device that reads and writes data byte by byte according to the byte stream. Data is read and written in order. For example, our most common lights, buttons, IIC, SPI, LCD, etc. are all character devices, and the drivers of these devices are called character device drivers. ˆ   

  Linux application calls to the driver:

  After the driver is successfully loaded, a corresponding file will be generated in the "/dev" directory. The application can implement the corresponding operation by performing corresponding operations on the file named "/dev/xxx" (xxx is the specific driver file name). Operation of hardware. For example, there is a driver file called /dev/led, which is the driver file for LED lights. The application uses the open function to open the file /dev/led, and after completion, uses the close function to close the file /dev/led. open and close are functions to turn on and off the LED driver. If you want to light up or turn off the LED, then use the write function to operate, that is, write data to the driver. This data is the control parameter of whether to turn off or turn on the LED.​ 

  Applications run in user space, and Linux drivers are part of the kernel, so the drivers run in kernel space. When we want to operate the kernel in user space, such as using the open function to open the /dev/led driver, because user space cannot directly operate the kernel, we must use a method called "system call" to realize the operation from the user. The space is "trapped" into the kernel space so that operations on the underlying driver can be realized. These functions such as open, close, write and read are provided by the C library. In Linux systems, system calls are part of the C library. The process when calling the open function is as follows:

  The functions used by the application have corresponding functions in the specific driver. For example, if the open function is called in the application, there must also be a function named open 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. The content is 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 *); // 向设备发送数据
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, bool spin);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*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 位的应用程序调用将会使用此函数
    int (*mmap) (struct file *, struct vm_area_struct *);    // mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
    unsigned long mmap_supported_flags;
    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); // fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
    int (*fasync) (int, struct file *, int);
    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);
    loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags);
    int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

2. Character device driver development steps

1. Loading and unloading of driver modules

  There are two ways to run Linux drivers. The first is to compile the driver into the Linux kernel, so that the driver will automatically run when the Linux kernel starts. The second method is to compile the driver into a module (the module extension under Linux is .ko), and use the "modprobe" or "insmod" command to load the driver module after the Linux kernel is started. The difference between these two commands is that modprobe will automatically parse and load the module and its dependencies, while insmod will provide basic manual loading functions, suitable for specific scenarios. When debugging a driver, we generally choose to compile it into a module. In this way, after modifying the driver, we only need to compile the driver code, and there is no need to compile the entire Linux code. And during debugging, you only need to load or unload the driver module, without restarting the entire system. In short, the biggest advantage of compiling the driver into a module is to facilitate development. When the driver development is completed and there are no problems, the driver can be compiled into the Linux kernel. Of course, it does not need to be compiled into the Linux kernel.

  The module has two operations: loading and unloading. We need to register these two operation functions when writing the driver. The loading and unloading registration functions of the module are as follows: 

module_init(xxx_init); // 注册模块加载函数
module_exit(xxx_exit); // 注册模块卸载函数

  The module_init function is used to register a module loading function with the Linux kernel. The parameter xxx_init is the specific function that needs to be registered. When the "modprobe" command is used to load the driver, the xxx_init function will be called. The module_exit function is used to register a module uninstall function with the Linux kernel. The parameter xxx_exit is the specific function that needs to be registered. When the "rmmod" command is used to uninstall a specific driver, the xxx_exit function will be called. The character device driver module loading and unloading template is as follows: 

/* 驱动入口函数 */
static int __init xxx_init(void)    // 定义了一个静态的、返回整数类型的 xxx_init 函数,并使用 __init 宏进行修饰;__init:一个宏,用于将该函数标记为内核初始化函数。这意味着该函数只在模块加载时执行,并且在加载完成后会被自动从内存中删除,以释放资源
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)    // 定义了一个静态的、无返回值的 xxx_exit 函数,并使用 __exit 宏进行修饰;__exit:一个宏,用于将该函数标记为内核退出函数。这意味着该函数只在模块卸载时执行,在卸载完成后会被自动从内存中删除
{
    /* 出口函数具体内容 */
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);    // 函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用
module_exit(xxx_exit);    // 调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用

  The extension will be .ko after the driver is compiled. For example, load the driver module drv.ko:

/* 加载驱动推荐使用 modprobe,因为它提供模块的依赖性分析、错误检查、错误报告等功能 */
modprobe -a drv    // 加载 drv.ko 驱动模块

/* 卸载驱动推荐使用rmmod,因为 modprobe 卸载驱动模块所依赖的其他模块,但有时候我们也用到了这些模块 */
rmrod drv.ko       // 卸载 drv.ko 驱动模块

2. Character device registration and deregistration

  For the character device driver, the character device needs to be registered after the driver module is loaded successfully. Similarly, the character device also needs to be unregistered when the driver module is uninstalled. The registration and deregistration function prototypes of character devices are as follows: 

static inline int register_chrdev(unsigned int major,
                                  const char *name,
                                  const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major,
                                     const char *name)

/*
static:这里的 static 是静态函数,作用域为当前文件,只能被当前文件的其他函数调用
inline:是建议编译器对函数进行内联展开。内联展开是指在函数调用的地方直接将函数的代码插入,而不是通过函数调用来执行代码。这样可以减少函数调用的开销

register_chrdev 函数用于注册字符设备,此函数一共有三个参数:
major:主设备号,Linux 下每一个设备都有一个设备号,设备号分为住设备号和次设备号
name:设备名字,指向一串字符串
*fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量

unregister_chrdev 函数用户注销字符设备,此函数有两个参数:
major:要注销的设备的主设备号
name:要注销的设备的设备名字
*/

  Generally, the registration of character devices is performed in the entry function xxx_init of the driver module, and the unregistration of character devices is performed in the exit function xxx_exit of the driver module:

static struct file_operations test_fops;    // 定义一个 file_operations 结构体变量 test_fops,test_fops 就是设备操作函数集合,我们现在没有初始化成员变量,所以这个函数集合是空的

/* 驱动入口函数 */
static int __init xxx_init(void) 
{
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);    // 主设备号为 200,设备名字为“chrtest”,设备操作函数集合"test_fops",选择没有被使用的主设备号,输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号
    if (retvalue < 0) {
        /* 字符设备注册失败,自行处理 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void) 
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");    // 调用函数 unregister_chrdev 注销主设备号为 200 的这个设备
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

3. Implement specific operating functions of the device

① Able to open and close chrtest

  The opening and closing of equipment is the most basic requirement, and almost all equipment must provide opening and closing functions. Therefore, we need to implement the open and release functions in file_operations.​ 

② Read and write chrtest

  Suppose the device chrtest controls a buffer (memory), and the application needs to read and write the buffer of chrtest through the two functions read and write. The code is as follows:

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp) 
{
    /* 用户实现具体功能 */
    return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)    // __user:标记用户空间指针
{
    /* 用户实现具体功能 */
    return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 
{
    /* 用户实现具体功能 */
    return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

// 加入 test_fops 这个结构体变量的初始化操作
static struct file_operations test_fops = 
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void) 
{
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if (retvalue < 0) {
        /* 字符设备注册失败,自行处理 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void) 
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

  Four functions were initially written: chrtest_open, chrtest_read, chrtest_write and chrtest_release. These four functions are the open, read, write and release operation functions of the chrtest device.​ 

4. Add LICENSE and author information

  Finally, you need to add LICENSE information and author information to the driver. LICENSE must be added, otherwise an error will be reported during compilation. Author information may or may not be added. Adding LICENSE and author information uses the following two functions: 

MODULE_LICENSE() // 添加模块 LICENSE 信息
MODULE_AUTHOR() // 添加模块作者信息

  The complete character device driver module is roughly as follows: 

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp) 
{
    /* 用户实现具体功能 */
    return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)    // __user:标记用户空间指针
{
    /* 用户实现具体功能 */
    return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 
{
    /* 用户实现具体功能 */
    return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

// 加入 test_fops 这个结构体变量的初始化操作
static struct file_operations test_fops = 
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void) 
{
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if (retvalue < 0) 
    {
        /* 字符设备注册失败,自行处理 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void) 
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

/* 添加license信息和作者信息*/
MODULE_LICENSE("GPL");          // 添加 LICENSE 信息
MODULE_AUTHOR("luoxuesong");    // 添加作者信息

3. Linux device number

1. Device number composition

  The device number consists of two parts: a major device number and a minor device number. The major device number represents a specific driver, and the minor device number represents each device that uses this driver. Linux provides a data type named dev_t to represent the device number. dev_t is defined in the file include/linux/types.h and is defined as follows: 

typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;

  It can be seen that dev_t is of type __u32, and __u32 is defined in the file include/uapi/asm-generic/int-ll64.h and is defined as follows: 

typedef unsigned int __u32;

  -dev_t is actually an unsigned int type, which is a 32-bit data type. This 32-bit data constitutes two parts: the major device number and the minor device number. The high 12 bits are the primary device number and the low 20 bits are the minor device number. Therefore, the major device number in the Linux system ranges from 0 to 4095 and cannot exceed this range. Several operation functions (essentially macros) on device numbers are provided in the file include/linux/kdev_t.h:

#define MINORBITS 20    // 次设备号位数,一共20位
#define MINORMASK ((1U << MINORBITS) - 1)    // 次设备号掩码

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))    // 从 dev_t 获取主设备号,将 dev_t 右移20位
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))    // 从 dev_t 获取次设备号,取 dev_t 低20位
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))    // 将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号

2. Device number allocation

① Static allocation of device numbers

  When registering a character device, you need to specify a device number for the device. The device number can be specified by the driver developer himself, such as the previous 200. Use cat/proc/devices to view the device numbers to see which ones have been used.

② Dynamically assign device number

  Static allocation of device numbers is prone to conflicts. It is recommended to use dynamic allocation of device numbers. Apply for a device number before registering a character device. The system will automatically give you an unused device number to avoid conflicts.

  Device number application function is as follows:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

/*
dev:保存申请到的设备号
baseminor:次设备号起始地址,这个函数可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始
count:要申请设备号的个数
name:设备名字
*/

  Device number release function is as follows: 

void unregister_chrdev_region(dev_t from, unsigned count);

/*
from:要释放的设备
count:从from开始,需要释放的设备号数量
*/

4. chrdevbase character device driver development experiment

  chrdevbase is a virtual device. The chrdevbase device has two buffers, a read buffer and a write buffer, both of which are 100 bytes in size. In the application, you can write data to the write buffer of the chrdevbase device and read data from the read buffer.​ 

1. Programming

  The application calls the open function to open the chrdevbase device. After opening, you can use the write function to write data (no more than 100 bytes) into the write buffer writebuf of chrdevbase, or you can use the read function to read the data in the read buffer readbuf. After the operation is completed, the application uses the close function to close the chrdevbase device (open, read, write, close).

① Create VScode project

  Create a 1_chrdevbase folder to store all files for this experiment.

  Create a new VScode project and create a new chrdevbase.c file.

② Add header file path

  Because we are writing a Linux driver, we will use functions in the Linux source code. We need to add the header file path in the Linux source code to VSCode. Press SHIFT+CTRL+P in VScode and enter C/C++: Edit configurations(JSON) .

③Write a program

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

#define CHRDEVBASE_MAJOR 200              /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase"      /* 设备名 */

static char readbuf[100];                 /* 读缓冲区 */
static char writebuf[100];                /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};

/*
 * @description : 打开设备
 * @param – inode : 传递给驱动的 inode
 * @param - filp : 设备文件, file 结构体有个叫做 private_data 的成员变量,一般在 open 的时候将 private_data 指向设备结构体。
 * @return : 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    // printk("chrdevbase open!\r\b");  // printf运行在用户态, printk 运行在内核态。
    return 0;
}

/*
 * @description : 从设备读取数据
 * @param - filp : 要打开的设备文件(文件描述符)
 * @param - buf : 返回给用户空间的数据缓冲区
 * @param - cnt : 要读取的数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)   // ssize_t:函數返回的字节数或错误码       // // 应用程序调用 read 函数从设备中读取数据的时候此函数会执行
{
    int retvalue = 0;

    /* 向用户空间发送数据 */                                // memcpy() 将根据指定的大小复制 kerneldata 中的内容,并将其粘贴到 readbuf 中,其中有sizeof(kerneldata)个字节
    memcpy(readbuf, kerneldata, sizeof(kerneldata));     // kerneldata 是里面保存着用户空间要读取的数据
    retvalue = copy_to_user(buf, readbuf, cnt);          // 因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
    if (retvalue == 0) {
        printk("kernel senddata ok!\r\n");
    } else {
        printk("kernel senddata failed!\r\n");
    }

    // printk("chrdevbase read!\r\n");
    return 0;
}

/*
 * @description : 向设备写数据
 * @param - filp : 设备文件,表示打开的文件描述符
 * @param - buf : 要写给设备写入的数据
 * @param - cnt : 要写入的数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue = 0;

     /* 接收用户空间传递给内核的数据并且打印出来 */         // 参数 buf 就是应用程序写入设备的数据
    retvalue = copy_from_user(writebuf, buf, cnt);   // 因为用户空间内存不能直接访问内核空间的内存,所以需要借助函数 copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中
    if (retvalue == 0) {
        printk("kernel recevdata:%s\r\n", writebuf);
    } else {
        printk("kernel recevdata failed!\r\n");
    }

    // printk("chrdevbase write!\r\n");
    return 0;
}

/*
 * @description : 关闭/释放设备
 * @param - filp : 要关闭的设备文件(文件描述符)
 * @return : 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode,struct file *filp)    // 应用程序调用 close 关闭设备文件的时候此函数会执行
{
    // printk("chrdevbase release!\r\n");
    return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};

/*
 * @description : 驱动入口函数
 * @param : 无
 * @return : 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
    if (retvalue < 0) {
        printk("chrdevbase driver register failed\r\n");
    }
    return 0;
}

/*
 * @description : 驱动出口函数
 * @param : 无
 * @return : 0 成功;其他 失败
 */
static void __exit chrdevbase_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\r\n");
}

/*
 * 将上面两个函数指定为入口和出口函数
 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

/*
 * LICENSAE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");    
MODULE_INFO(intree, "Y");   // 如果不加就会有“loading out-of-treemodule taints kernel.”这个警告     

  Where printf runs in user mode, printk runs in kernel mode, and printk can be classified according to log levels. There are eight levels in total, in the file include/linux/kern_levels.h:

#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用 KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息

  Among them, 0 has the highest priority and 7 has the lowest priority. for example:

printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");

  ˆAdd KERN_ENMERG in front of a specific message to set the message to KERN_ENMERG several times. If you do not explicitly set the message level when using printk, the default level MESSAGE_LOGLEVEL_DEFAULT will be used, and there is a macro CONSOLE_LOGLEVEL_DEFAULT in include/linux/printk.h with this definition:

#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT

  MESSAGE_LOGLEVEL_DEFAULT and CONFIG_CONSOLE_LOGLEVEL_DEFAULT are configured through the kernel graphical interface:

-> Kernel hacking
    -> printk and dmesg options
        -> (7) Default console loglevel (1-15) // 设置默认终端消息级别
        -> (4) Default message log level (1-7) // 设置默认消息级别

  - CONSOLE_LOGLEVEL_DEFAULT controls which levels of messages can be displayed on the console. This macro defaults to 7, which means that only messages with a priority higher than 7 can be displayed on the console. The default message level is 4. The level of 4 is higher than 7. The information output by using printk directly can be displayed on the console.​ 

2. Write APP

① Basic functions of C library file operation

  Writing a test APP is writing a Linux application, which requires the use of some functions related to file operations in the C library, such as open, read, write and close.​ 

1. open function
/*
 * @param - pathname : 要打开的设备或者文件名
 * @param -  flags : 文件打开模式,以下三种模式必选其一:                   
                     O_RDONLY 只读模式
                     O_WRONLY 只写模式
                     O_RDWR 读写模式
 * @return : 如果文件打开成功的话返回文件的文件描述符
 */
int open(const char *pathname, int flags);

2. read function
/*
 * @param - fd : 要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符
 * @param - buf : 数据读取到此 buf 中
 * @param - count : 读取的数据长度,即字节数
 * @return : 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败
 */
ssize_t read(int fd, void *buf, size_t count);

3. write function
/*
 * @param - fd : 要写操作的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符
 * @param - buf : 要写入的数据
 * @param - count : 写入的数据长度,即字节数
 * @return : 写入成功的话返回读取到的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败
 */
ssize_t write(int fd, const void *buf, size_t count);

4. close function
/*
 * @param - fd : 要关闭的文件描述符
 * @return : 0 表示关闭成功,负值表示关闭失败
 */
int close(int fd);

②Write test APP program 

  The test APP runs in user space. The test APP performs read or write operations on the chrdevbase device by entering corresponding instructions. Create a new chrdevbaseApp.c file in the 1_chrdevbase directory and enter the following content in this file: 

#include "stdio.h"          // 提供了输入输出函数的声明,如 printf() 和 scanf()
#include "unistd.h"         // 提供了对操作系统的访问和操作,如进程控制、文件操作等
#include "sys/types.h"      // 定义了一些基本的数据类型,如文件描述符类型 int、进程 ID pid_t 等
#include "sys/stat.h"       // 包含了文件状态的获取和设置函数,如 stat() 和 chmod()
#include "fcntl.h"          // 定义了一些文件控制相关的常量和函数,如文件打开方式的设置和文件锁定操作
#include "stdlib.h"         // 提供了一些通用的函数和宏定义,如内存分配函数 malloc() 和随机数生成函数 rand()
#include "string.h"         // 提供了一些字符串处理函数的声明,如字符串复制函数 strcpy() 和字符串比较函数 strcmp()


static char usrdata[] = {"usr data!"};  // 要向 chrdevbase 设备写入的数据

/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

/*
 比如输入命令:./chrdevbaseApp /dev/chrdevbase 1
 这个命令有3个参数,“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,这三个参数分别对应 argv[0]、 argv[1]和 argv[2]。
 第一个参数是运行 chrdevbaseAPP 软件,第二个参数表示测试 APP 要打开/dev/chrdevbase这个设备,第三个参数就是要执行的操作, 1表示从chrdevbase中读取数据, 2 表示向 chrdevbase 写数据。
 */
    if(argc != 3)       // 判断参数是否有 3 个,不足3个表示测试 APP 用法错误
    {      
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);        //调用C库中的open打开设备文件/dev/chrdevbase
    if(fd < 0){
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    if(atoi(argv[2]) == 1)      // atoi:将字符串格式的数字转换成真实的数字
    { 
        /* 从驱动文件读取数据 */
        retvalue = read(fd, readbuf, 50);
        if(retvalue < 0){
            printf("read file %s failed!\r\n", filename);
        }else{
            /* 读取成功,打印出读取成功的数据 */
            printf("read data:%s\r\n",readbuf);
        }
    }

    if(atoi(argv[2]) == 2){
        /* 向设备驱动写数据 */
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);
        if(retvalue < 0){
            printf("write file %s failed!\r\n", filename);
        }
    }

    /* 关闭设备 */
    retvalue = close(fd);
    if(retvalue < 0){
        printf("Can't close file %s\r\n", filename);
        return -1;
    }

    return 0;
}

3. Compile driver and test APP 

① Compile driver 

  First compile the driver, which is the chrdevbase.c file. We need to compile it into a .ko module and create a Makefile:

KERNELDIR = /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31	# Linux内核源码路径
CURRENT_PATH = $(shell pwd)		# 获取当前所处路径
obj-m := chardevbase.o		# 将 chrdevbase.c 编译为 chrdevbase.ko 模块

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules	 
clean: 
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

#  modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件

  After successful compilation, a file called chrdevbaes.ko will be generated. This file is the driver module of the chrdevbase device.​ 

② Compile and test APP 

  Because the test APP is to be run on the ARM development board, it needs to be compiled using arm-linux-gnueabihf-gcc:

arm-none-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

4. Run the test 

① Load driver module

  Linux systems choose to boot from the network through TFTP and use NFS to mount the network root file system.

// uboot 中 bootcmd 环境变量的值为: 
setevn bootcmd tftp c2000000 uImage;tftp c4000000 stm32mp157d-atk.dtb;bootm c2000000 - c4000000

// bootargs 环境变量的值为:
setenv bootargs aconsole=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.1.100:/home/alientek/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.106:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off

  Check whether there is a directory "/lib/modules/5.4.31" in the root file system of the development board. This directory needs to be created by yourself. Now you need to copy chrdevbase.ko and chrdevbaseAPP to the rootfs/lib/modules/5.4.31 directory:

cd /linux/atk-mpl/Drivers/1_chrdevbase
sudo cp chrdevbase.ko chrdevbaseApp /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f

  The moules.xx file exists because the command: depmod is entered, which can automatically generate the modules.dep file. Enter the command to load the driver file:

modprobe chrdevbase

lsmod    // 这个命令可以查看当前系统中存在的模块

  Enter cat/proc/devices to see the chrdevbase device with device number 200.​ 

② Create device node file

  Successful loading of the driver requires the creation of a corresponding device node file in the /dev directory. The application completes the operation of the specific device by operating this device node file. Enter the following command to create the device node file /dev/chrdevbase: 

mknod /dev/chrdevbase c 200 0

  Among them, "mknod" is the command to create a node, "/dev/chrdevbase" is the node file to be created, "c" indicates that this is a character device, "200" is the major device number of the device, and "0" is the minor device number of the device . After the creation is completed, the file /dev/chrdevbase will exist. You can use the "ls /dev/chrdevbase -l" command to view it.

  If chrdevbaseAPP wants to read and write the chrdevbase device, just read and write to /dev/chrdevbase directly. This file equivalent to /dev/chrdevbase is the implementation of the chrdevbase device in user space.​ 

③ chrdevbase device operation test 

  Use the chrdevbaseApp software to operate the chrdevbase device to see if the reading and writing are normal. First perform the reading operation and enter the following command: 

cd /
cd /lib/modules/5.4.31
./chrdevbaseApp /dev/chrdevbase 1

  ˜ Kernel data! is output, because chrdevbaseAPP uses the read function to read data from the chrdevbase device, so the chrdevbase_read function will be executed. The chrdevbase_read function sends "kerneldata!" data to chrdevbaseAPP. After chrdevbaseAPP receives it, it prints it out. "read data:kernel data!" is the received data printed by chrdevbaseAPP. Continue testing write operations.

/chrdevbaseApp /dev/chrdevbase 2

  "kernel recevdata: usr data!" is output, which is output by the chrdevbase_write function in the driver. chrdevbaseAPP uses the write function to write data "usr data!" to the chrdevbase device. The chrdevbase_write function prints it out after receiving it.​ 

④ Uninstall the driver module 

  If you no longer use a device you can uninstall it:

rmmod chrdevbase

# 之后可以用lsmod查看模块是否存在

Guess you like

Origin blog.csdn.net/qq_45475497/article/details/134963949