9.1 字符设备驱动开发

news2024/11/18 22:27:41

一、字符设备驱动简介

  字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。   

  Linux 应用程序对驱动程序的调用:

  驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。 

  应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。 open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。调用 open 函数的时候流程如下:

  应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。 

  在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示: 

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;

二、字符设备驱动开发步骤

1. 驱动模块的加载和卸载

  Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“modprobe”或者“insmod”命令加载驱动模块。这两个命令的区别是 modprobe 会自动解析和加载模块及其依赖项,insmod 会提供基础的手动加载功能,适用于特定场景。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进 Linux 内核中,当然也可以不编译进Linux 内核中。

  模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下: 

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

  module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“modprobe”命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。字符设备驱动模块加载和卸载模板如下所示: 

/* 驱动入口函数 */
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函数就会被调用

  驱动编译完成以后扩展名为.ko。比如加载 drv.ko这个驱动模块:

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

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

2. 字符设备注册与注销

  对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示: 

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:要注销的设备的设备名字
*/

  一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行:

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. 实现设备的具体操作函数

① 能对 chrtest 进行打开和关闭

  设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。 

② 对 chrtest 进行读写操作

  假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作,代码如下:

/* 打开设备 */
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);

  一开始编写了四个函数: chrtest_open、 chrtest_read、chrtest_write 和 chrtest_release。这四个函数就是 chrtest 设备的 open、 read、 write 和 release 操作函数。 

4. 添加 LICENSE 和作者信息

  最后需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数: 

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

  完整字符设备驱动模块大致内容如下: 

/* 打开设备 */
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");    // 添加作者信息

三、Linux 设备号

1. 设备号组成

  设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,定义如下: 

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

  可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里面,定义如下: 

typedef unsigned int __u32;

  dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,不能超过这个范围。在文件 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. 设备号分配

① 静态分配设备号

  注册字符设备的时候需要给设备指定一个设备号,设备号可以是驱动开发者自己去指定,比如之前的 200 。用 cat/proc/devices 查看设备号,看哪些被用过了。

② 动态分配设备号

  静态分配设备号容易出现冲突问题,推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用过的设备号,避免冲突。

  设备号 申请 函数如下:

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

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

  设备号 释放 函数如下: 

void unregister_chrdev_region(dev_t from, unsigned count);

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

四、chrdevbase 字符设备驱动开发实验

  chrdevbase 是一个虚拟设备。chrdevbase 设备有两个缓冲区,一个读缓冲区,一个写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。 

1. 程序编写

  应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备(打开,读取,写入,关闭)。

① 创建 VScode 工程

  创建 1_chrdevbase 文件夹存放这次实验所有文件。

  新建 VScode 工程,新建 chrdevbase.c 文件。

② 添加头文件路径

  因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux源码中的头文件路径。 在 VScode 按下 SHIFT+CTRL+P,输入 C/C++: Edit configurations(JSON)  。

③ 编写程序

#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.”这个警告     

  其中 printf运行在用户态, printk 运行在内核态,并且 printk 可以根据日志级别进行分类,一共有八个级别,在文件的  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" /* 调试信息

  其中 0 的优先级最高,7 的优先级最低。比如:

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

  在具体的消息前面加上 KERN_ENMERG 可以将这条消息几倍设置为 KERN_ENMERG 。如果在使用 printk 的时候不显示设置消息级别,则会采用默认级别 MESSAGE_LOGLEVEL_DEFAULT,并且在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT 有这样的定义:

#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT

  MESSAGE_LOGLEVEL_DEFAULT 和 CONFIG_CONSOLE_LOGLEVEL_DEFAULT 是通过内核图形化界面配置的:

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

  CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。 默认消息级别为 4, 4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。 

2. 编写APP

① C 库文件操作基本函数

  编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read、 write 和 close 这四个函数。 

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

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

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

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

② 编写测试 APP 程序 

  测试 APP 运行在用户空间。测试 APP 通过输入相应的指令来对 chrdevbase 设备执行读或者写操作。在1_chrdevbase 目录中新建 chrdevbaseApp.c 文件,在此文件中输入如下内容: 

#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. 编译驱动程序和测试 APP 

① 编译驱动程序 

  首先编译驱动程序,也就是 chrdevbase.c 这个文件,我们需要将其编译为.ko 模块,创建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 文件

  编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块。 

②  编译测试 APP 

  因为测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 来编译:

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

4. 运行测试 

① 加载驱动模块

  Linux 系统选择通过 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统。

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

  检查开发板根文件系统中有没有“/lib/modules/5.4.31”这个目录,这个目录需要自己创建。现在需要将  chrdevbase.ko 和 chrdevbaseAPP 复制到 rootfs/lib/modules/5.4.31 目录中 :

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

  有moules.xx文件是因为输入了命令:depmod ,它可以自动生成 modules.dep 文件。输入命令加载驱动文件:

modprobe chrdevbase

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

  输入 cat/proc/devices 就可以看到设备号为 200 的chrdevbase 设备。 

② 创建设备节点文件

  驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件: 

mknod /dev/chrdevbase c 200 0

  其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“ 200”是设备的主设备号,“ 0”是设备的次设备号。创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看。

  如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。 

③ chrdevbase 设备操作测试 

  使用 chrdevbaseApp 软件操作 chrdevbase 这个设备,看看读写是否正常,首先进行读操作,输入如下命令: 

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

  输出了 kernel data! ,因为 chrdevbaseAPP 使用 read 函数从 chrdevbase 设备读取数据,因此 chrdevbase_read 函数就会执行。 chrdevbase_read 函数向 chrdevbaseAPP 发送“kerneldata!”数据, chrdevbaseAPP 接收到以后就打印出来,“read data:kernel data!”就是 chrdevbaseAPP打印出来的接收到的数据。继续测试写操作。

/chrdevbaseApp /dev/chrdevbase 2

  输出了“kernel recevdata:usr data!”,这个是驱动程序中的 chrdevbase_write 函数输出的。chrdevbaseAPP 使用 write 函数向 chrdevbase 设备写入数据“usr data!”。 chrdevbase_write 函数接收到以后将其打印出来。 

④ 卸载驱动模块 

  如果不再使用某个设备可以将其卸载:

rmmod chrdevbase

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1308955.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c语言二维数组之字符数组

介绍&#xff1a; 本篇介绍 c语言二维数组之字符数组 其实他和前两种数组 c语言二维数组之整型数组 c语言二维数组之浮点型数组 差不多 就有两处不同 所以 本篇文章重点讲那两点不同 c语言二维数组之字符数组&#xff1a; 第一点不同&#xff1a; 第一个不同点就是 它存储的…

小航助学2023年9月电子学会Scratch四级真题(含题库答题软件账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09; 单选题3.00分 删除编辑附件图文 答案:A 第1题角色为一个紫色圆圈&#xff0c;运行程序后&#xff0c;舞台上的图案是&#xff1f;&#xff08; &#xff09; A…

imazing是什么软件

imazing是什么软件 iMazing 是世界上最值得信赖的软件,可以将您的信息、音乐、文件和数据从 iPhone 或 iPad 传输到您的 Mac 或 PC。 获得备份、数据提取、媒体和文件传输的强大能力,以及更多 iMazing 功能。 iMazing是一款第三方的苹果iOS设备管理软件。 iMazing- 2 Mac-安装…

CPython(将Python编译为so)

环境 先配一下环境&#xff0c;我使用的是python3.8.5 pip install Cython 编译过程 我们准备一个要编译的文件 test.py def xor(input_string): output_string "" for char in input_string: output_string chr(ord(char) ^ 0x66) return output_string …

对于双显卡电脑,如何分辨现在用的是独立显卡还是集成显卡?

一、问题描述 台式电脑本身自带了集成显卡&#xff0c;然后又购买了一块NVIDIA的独立显卡。 现在&#xff0c;就有疑问了&#xff0c;如何判断你的显示器连接的是独立显卡还是集成显卡呢&#xff1f; 二、NVIDIA双显卡机型 1、在桌面右下角&#xff0c;选择NVIDIA图标&…

【字符串】ABC324E

退役啦&#xff0c;接下来的博客全是图一乐啦 E - Joint Two Strings 题意 思路 统计两个指针的方案数一定是枚举一个&#xff0c;统计另一个 然后因为拼起来之后要包含 t 这个字符串&#xff0c;隐隐约约会感觉到和前缀后缀子序列有关 考虑预处理每个 s[i] 的最长公共前…

C语言-WIN32API介绍

Windows API 从第一个32位的Windows开始就出现了&#xff0c;就叫做Win32API.它是一个纯C的函数库&#xff0c;就和C标准库一样&#xff0c;使你可以写Windows应用程序过去很多Windows程序是用这个方式做出来的 main()? main()成为C语言的入口函数其实和C语言本身无关&…

boost1.55 安装使用教程 windows

第一步 &#xff1a;首先在boost官网上下载库压缩包 添加链接描述 选择自己需要的版本进行下载 解压后执行booststrap.bat 用来生成创建b2.exe 和bjam.exe 拓展&#xff1a;.\b2 --help 了解一下有哪些参数可以配置 默认b2.exe编译后&#xff0c;链接到项目如果出现如下错误…

Docker Compose入门:打造多容器应用的完美舞台

Docker Compose 是一个强大的工具&#xff0c;它允许开发者通过简单的 YAML 文件定义和管理多容器的应用。本文将深入讨论 Docker Compose 的基本概念、常用命令以及高级应用场景&#xff0c;并通过更为丰富和实际的示例代码&#xff0c;助您轻松掌握如何通过 Docker Compose 打…

docker-compose Nginx Proxy Manager

Nginx Proxy Manager前言 Nginx Proxy Manager(以下简称 NPM)就是一个 Nginx 的代理管理器,它最大的特点是简单方便。 美观且安全的管理界面,基于表格轻松创建转发域、重定向、流和 404 主机,而无需了解 Nginx使用 Lets Encrypt 免费 SSL 或提供您自己的自定义 SSL 证书主…

详细说说vuex

Vuex 是什么 Vuex有几个属性及作用注意事项vuex 使用举例Vuex3和Vuex4有哪些区别 创建 Store 的方式在组件中使用 Store辅助函数的用法响应式的改进Vuex4 支持多例模式 Vuex 是什么 Vuex是一个专门为Vue.js应用设计的状态管理构架&#xff0c;它统一管理和维护各个Vue组件的可…

【从零开始学习JVM | 第八篇】学习垃圾回收算法 和 垃圾回收器

前言&#xff1a; 现代编程语言通常采用垃圾回收机制来自动管理内存。垃圾回收机制是一种自动化的内存管理技术&#xff0c;可以在程序运行时自动识别和回收不再使用的内存&#xff0c;从而减少内存泄漏和其他内存相关问题的发生。 本文将介绍垃圾回收算法和垃圾回收器的相关…

【算法与数据结构】37、LeetCode解数独

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;本题也是一道困难题&#xff0c;难点在于如何构建数独棋盘&#xff0c;如何检查棋盘的合法性&#xff…

智能优化算法应用:基于树种算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于树种算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于树种算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.树种算法4.实验参数设定5.算法结果6.参考文献7.MA…

前端体系:前端应用

目录 前端体系基础 html&#xff08;超文本标记语言&#xff09; css&#xff08;层叠样式单&#xff09; javascript&#xff08;&#xff09; 一、前端体系概述 二、前端框架 React Vue Angular 三、前端库和工具 lodash Redux Webpack 四、模块化和组件化 ES…

点云几何 之 判断某一点是否在三角形的边上(3)

点云几何 之 判断某一点是否在三角形的边上&#xff08;3&#xff09; 一、算法介绍二、算法实现1.代码2.结果 总结 一、算法介绍 判断某一点是否在三角形的边上 之前已经介绍了点在三角形的内外判断方法&#xff0c;这里增加点恰好在三角形边上的判断方法 &#xff08;本质上…

js基础:函数、对象、WebAPIs-DOM

一、函数和对象 1、函数概述 &#x1f916;chatgpt&#xff1a;什么是函数&#xff1f;为什么要有函数&#xff1f; 函数是一种可重复使用的代码块&#xff0c;它们可以接受输入&#xff08;参数&#xff09;、执行特定的任务&#xff0c;并返回结果。 JavaScript中函数是非常…

springboot整合日志框架log4j2

springboot整合日志框架log4j2 前言&#xff1a;springboot提供了默认的日志框架logback&#xff0c;结合slf4j门面&#xff0c;基于简单配置即可实现日志输出记录。但是实际开发中很多项目会使用log4j2&#xff0c;log4j2是log4j的升级版本&#xff0c;性能和安全性上比log4j…

论文阅读《DPS-Net: Deep Polarimetric Stereo Depth Estimation》

论文地址&#xff1a;https://openaccess.thecvf.com/content/ICCV2023/html/Tian_DPS-Net_Deep_Polarimetric_Stereo_Depth_Estimation_ICCV_2023_paper.html 概述 立体匹配模型难以处理无纹理场景的匹配&#xff0c;现有的方法通常假设物体表面是光滑的&#xff0c;或者光照是…

Linux | 多线程

前言 本文主要介绍多线程基础知识&#xff0c;以及使用多线程技术进行并发编程&#xff1b;最后会介绍生产者消费者模型&#xff1b; 一、线程基本认识 1、什么是线程 如果你是科班出生&#xff0c;你肯定听过线程相关概念&#xff1b;但是你可能没有真正搞懂什么是线程&#…