【Linux驱动篇】字符设备驱动开发

文章目录

  • 【Linux驱动篇】字符设备驱动开发
    • [1 驱动简介](#1 驱动简介)
      • [1.1 驱动分类](#1.1 驱动分类)
      • [1.2 驱动模块加载和卸载](#1.2 驱动模块加载和卸载)
        • [1.2.1 模块加载和卸载注册函数](#1.2.1 模块加载和卸载注册函数)
        • [1.2.2 模块加载](#1.2.2 模块加载)
        • [1.2.3 模块卸载](#1.2.3 模块卸载)
      • [1.3 Linux设备号](#1.3 Linux设备号)
        • [1.3.1 设备号组成](#1.3.1 设备号组成)
        • [1.3.2 设备号分配](#1.3.2 设备号分配)
      • [1.4 printk函数](#1.4 printk函数)
      • [1.5 内核空间数据和用户空间数据交互](#1.5 内核空间数据和用户空间数据交互)
    • [2 字符设备驱动简介](#2 字符设备驱动简介)
      • [2.1 注册与注销](#2.1 注册与注销)
      • [2.2 实现设备的具体操作函数](#2.2 实现设备的具体操作函数)
    • [3 chrdevbase字符设备驱动开发实验](#3 chrdevbase字符设备驱动开发实验)
      • [3.1 程序编写](#3.1 程序编写)
        • [3.1.1 配置VSCode工程](#3.1.1 配置VSCode工程)
          • [3.1.1.1 新建工程](#3.1.1.1 新建工程)
          • [3.1.1.2 添加头文件路径](#3.1.1.2 添加头文件路径)
        • [3.1.2 编写实验程序](#3.1.2 编写实验程序)
        • [3.1.3 编写测试APP](#3.1.3 编写测试APP)
          • [3.1.3.1 C库文件操作基本函数](#3.1.3.1 C库文件操作基本函数)
          • [3.1.3.2 编写测试APP程序](#3.1.3.2 编写测试APP程序)
      • [3.2 编译驱动程序和测试APP](#3.2 编译驱动程序和测试APP)
        • [3.2.1 编译驱动程序](#3.2.1 编译驱动程序)
        • [3.2.2 编译测试APP](#3.2.2 编译测试APP)
    • [4 运行测试](#4 运行测试)
      • [4.1 加载驱动模块](#4.1 加载驱动模块)
      • [4.2 创建设备节点](#4.2 创建设备节点)
      • [4.3 chrdevbase设备操作测试](#4.3 chrdevbase设备操作测试)
      • [4.4 卸载驱动模块](#4.4 卸载驱动模块)

【Linux驱动篇】字符设备驱动开发

1 驱动简介

1.1 驱动分类

Linux 中的三大类驱动:

  • 字符设备驱动

    从最简单的点灯到 I2C、SPI、音频等都属于字符设备驱动的类型

  • 块设备驱动

    存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。

  • 网络设备驱动。

    不管是有线的还是无线的,都属于网络设备驱动的范畴

一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

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

驱动加载后会在"/dev"目录下生成一个对应的文件,对其操作即可对硬件操作。Linux 驱动属于内核的一部分,因此驱动运行于内核空间,用户必须要通过系统调用才可以进入到内核。如open函数的操作流程如下所示:

每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合:

c 复制代码
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t 
                     *);
    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 (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct 
                          *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned
                            long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned
                          long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*mremap)(struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    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
};

其中:

  • owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。

  • llseek 函数用于修改文件当前的读写位置

  • read 函数用于读取设备文件

  • write 函数用于向设备文件写入(发送)数据

  • poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写

  • unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应

  • compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。

  • mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了.

  • open 函数用于打开设备文件

  • release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应

  • fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

  • aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。

1.2 驱动模块加载和卸载

Linux 驱动有两种运行方式:

  • 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
  • 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用"insmod"命令加载驱动模块。
1.2.1 模块加载和卸载注册函数

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

sh 复制代码
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

其中:

  • module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用"insmod"命令加载驱动的时候,xxx_init 这个函数就会被调用。
  • module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

模版如下:

c 复制代码
/* 驱动入口函数 */
static int __init xxx_init(void)
{

}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
}
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

其中:

  • xxx_init是驱动入口函数,使用"__init"修饰
  • xxx_exit是驱动出口函数,使用"__exit"修饰
1.2.2 模块加载

驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:

  • insmod

    最简单的模块加载命令,用于加载指定的.ko模块,格式如下:

    sh 复制代码
    insmod drv.ko #drv.ko是模块的相对或者绝对路径

    注:insmod不能解决模块依赖关系。如:drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块,否则会报错。

  • modprobe

    提供了模块的依赖分析性、错误检查和错误报告的功能,将所有依赖关系的模块都加载到内核当中。推荐使用 modprobe 命令来加载驱动。格式如下:

    sh 复制代码
    modprobe drv.ko # drv.ko需要存放到/lib/modules/<kernel-version>目录下,否则查找会失败

    注:modprobe 命令默认会去/lib/modules/<kernel-version>目录中查找模块

1.2.3 模块卸载

有两种命令可以卸载模块:

  • rmmod

    格式如下:

    sh 复制代码
    rmmod drv.ko

    推荐使用 rmmod 命令。

  • modprobe -r

    格式如下:

    sh 复制代码
    modprobe -r drv.ko

    使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。

1.3 Linux设备号

1.3.1 设备号组成

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux中使用dev_t的数据类型来表示设备号,定义在include/linux/types.h内,如下:

c 复制代码
typedef __u32 __kernel_dev_t;
...
typedef __kernel_dev_t dev_t;

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

c 复制代码
typedef unsigned int __u32;

综上所述,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。所以Linux的主设备号的范围为: 0~4095。

在include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:

sh 复制代码
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

其中:

  • 宏 MINORBITS 表示次设备号位数,一共是 20 位。
  • 宏 MINORMASK 表示次设备号掩码。
  • 宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
  • 宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
  • 宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
1.3.2 设备号分配

主要有两种方式进行设备号的分配:

  • 静态分配设备号

    有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt,但是并不代表我们就不能使用了,具体要看系统内是否使用了这些设备,使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号,其余的未使用设备号都可以分配。

  • 动态分配设备号

    Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突,卸载驱动的时候释放掉这个设备号即可。

    设备号的申请函数如下:

    c 复制代码
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

    其中:

    • dev: 保存申请到的设备号
    • baseminor: 次设备号的起始地址,一般为0
    • count:要申请的设备号数量。
    • name: 设备名字

    注销字符设备之后要释放掉设备号,设备号释放函数如下:

    sh 复制代码
    void unregister_chrdev_region(dev_t from, unsigned count)

    其中:

    • from: 要释放的设备号
    • count: 从from开始,要释放的数量。

1.4 printk函数

Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用printk 这个函数。

printk 可以根据日志级别对消息进行分类,一共有 8 个消息级,定义在文件 include/linux/kern_levels.h 里面,定义如下:

sh 复制代码
#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" /* 调试信息 */

一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低。如果要设置消息级别,参考如下示例:

sh 复制代码
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");

上述代码就是设置"gsmi: Log Shutdown Reason\n"这行消息的级别为 KERN_EMERG。在具体的消息前面加上 KERN_EMERG 就可以将这条消息的级别设置为 KERN_EMERG。如果使用 printk 的 时 候 不 显 式 的 设 置 消 息 级 别 , 那 么 printk 将 会 采 用 默 认 级 别MESSAGE_LOGLEVEL_DEFAULT,MESSAGE_LOGLEVEL_DEFAULT 默认为 4。在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:

sh 复制代码
#define CONSOLE_LOGLEVEL_DEFAULT 7

CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。

1.5 内核空间数据和用户空间数据交互

为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。copy_to_user 函数原型如下:

c 复制代码
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

其中:

  • 参数 to 表示目的
  • 参数 from 表示源
  • 参数 n 表示要复制的数据长度
  • 如果复制成功,返回值为 0,如果复制失败则返回负数。

copy_from_user 将用户空间的数据复制到内核空间中,函数原型如下:

c 复制代码
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

其中:

  • 参数 to 表示目的
  • 参数 from 表示源
  • 参数 n 表示要复制的数据长度
  • 如果复制成功,返回值为 0,如果复制失败则返回负数。

2 字符设备驱动简介

字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。

2.1 注册与注销

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

c 复制代码
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)

其中:

  • register_chrdev 函数用于注册字符设备,其参数含义如下下:

    • major: 主设备号,Linux每个设备下都有一个设备号。
    • name: 设备名字,指向一个字符串。
    • fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
  • unregister_chrdev 函数用户注销字符设备,其参数含义如下:

    • major: 注销设备的主设备号
    • name: 设备的设备名

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

c 复制代码
static struct file_operations test_fops;

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 注册字符设备驱动 */
	int retvalue = 0;
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0)
    {
        /* 处理设备注册失败的逻辑 */
    }
}

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

module_init(xxx_init);
module_exit(xxx_exit);

其中:

  • test_fops 就是设备的操作函数集合,只是此时我们还没有初始化 test_fops 中的 open、release 等这些成员变量,所以这个操作函数集合还是空的。

  • 调用函数 register_chrdev 注册字符设备,主设备号为 200,设备名字为"chrtest"。

    注:需要选择在系统中没有使用的主设备号,可以通过"cat /proc/devices"查看已经被使用掉的设备号

  • 调用函数 unregister_chrdev 注销主设备号为 200 的这个设备。

2.2 实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,需要对其进行初始化。在初始化前我们需要确定要对设备进行哪些操作,才知道实现哪些操作函数,假设对chrtest有如下要求:

  • 对其进行打开和关闭操作

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

  • 对其进行读写操作

    假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。

实现的代码示例如下所示:

c 复制代码
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)
{
    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;
}

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release
};

static int __init chrtest_init(void)
{
    int retvalue = 0;
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0)
    {
    }
}

static void __exit chrtest_exit(void)
{
    unregister_chrdev(200, "chrtest");
}

module_init(xxx_init);
module_exit(xxx_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("pzs");

其中:

  • chrtest_open、chrtest_read、chrtest_write和 chrtest_release。这四个函数就是 chrtest 设备的 open、read、write 和 release 操作函数。
  • 初始化 test_fops 的 open、read、write 和 release 这四个成员变量。
  • MODULE_LICENSE:添加LICENSE信息,否则编译会报错
  • MODULE_AUTHOR:添加作者信息。可选添加

3 chrdevbase字符设备驱动开发实验

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

3.1 程序编写

3.1.1 配置VSCode工程
3.1.1.1 新建工程

新建工程,内容如下:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/1_chrdevbase$ ls
chrdevbase.c
3.1.1.2 添加头文件路径

因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux源码中的头文件路径。打开 VSCode,按下"Crtl+Shift+P"打开 VSCode 的控制台,然后输入"C/C++: Edit configurations(JSON) ",打开 C/C++编辑配置文件.

打开以后会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件,我们修改后的内容如下:

sh 复制代码
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/",
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c17",
            "cppStandard": "c++98",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

其中:

  • 主要是往includePath加入Linux源码下的include、arch/arm/include 和 arch/arm/include/generated 这三个目录的路径。

    注意:这里使用了绝对路径

3.1.2 编写实验程序

在chrdevbase.c里面输入如下内容:

c 复制代码
#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 CHADEVBASE_MAJOR 200 /* major number */
#define CHRDEVBASE_NAME "chrdevbase" /* driver name */

static char readbuf[100]; /* read buffer */
static char writebuf[100]; /* write buffer */
static char kerneldata[] = { "kernel data!"}; /* kernel data */


/**
 * @description: chrdevbase_open - open function
 * @param - inode: inode of device file
 * @param - filp: device file
 * @return: 0 on success, -1 on failure
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    printk("chrdevbase open!\r\n");
    return 0;
}

/**
 * @description: chrdevbase_read - read function
 * @param - filp: device file
 * @param - buf: user buffer
 * @param - cnt: count of bytes to read
 * @param - offt: offset of file
 * @return: count of bytes read
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue = 0;
    printk("chrdevbase read!\r\n");
    /* copy kernel data to user buffer */
    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if(retvalue == 0){
        printk("kernel senddata:%s\r\n", readbuf);
    } else {
        printk("kernel senddata failed!\r\n");
    }

    return 0;
}

/**
 * @description: chrdevbase_write - write function
 * @param - filp: device file
 * @param - buf: user buffer
 * @param - cnt: count of bytes to write
 * @param - offt: offset of file
 * @return: count of bytes written
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue = 0;

    printk("chrdevbase write!\r\n");
    /* copy user data to kernel buffer */
    retvalue = copy_from_user(writebuf, buf, cnt);
    if(retvalue == 0){
        printk("kernel recevdata:%s\r\n", writebuf);
    } else {
        printk("kernel recevdata failed!\r\n");
    }

    return 0;
}

/**
 * @description: chrdevbase_release - release function
 * @param - inode: inode of device file
 * @param - filp: device file
 * @return: 0 on success, -1 on failure
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
    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: chrdevbase_init - initialization function
 * @return: 0 on success, -1 on failure
 */
static int __init chrdevbase_init(void)
{
    int retvalue = 0;
    retvalue = register_chrdev(CHADEVBASE_MAJOR, CHRDEVBASE_NAME, 
        &chrdevbase_fops);
    if(retvalue < 0)
    {
        printk("chrdevbase register failed!\r\n");
        return retvalue;
    }

    printk("chrdevbase register success!\r\n");
    return 0;
}

/**
 * @description: chrdevbase_exit - exit function
 * @brief: unregister chrdev
 */
static void __exit chrdevbase_exit(void)
{
    unregister_chrdev(CHADEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase unregister success!\r\n");
}

/* module init and exit */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

/* module information */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("pzs");
3.1.3 编写测试APP
3.1.3.1 C库文件操作基本函数

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

3.1.3.2 编写测试APP程序

在1_chrdevbase 目录中新建 chrdevbaseApp.c 文件,在此文件中输入如下内容:

c 复制代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char usrdata[] = {"usr data!"};

int main(int argc, char *argv[]) {
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];
    if(argc != 3) {
        printf("usage: %s <filename>\n", argv[0]);
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd < 0) {
        printf("open %s failed!\n", filename);
        return -1;
    }

    /* read data from device */
    if(atoi(argv[2]) == 1){ 
        retvalue = read(fd, readbuf, sizeof(readbuf));
        if(retvalue >= 0) {
            printf("read data:%s\n", readbuf);
        } else {
            printf("read file %s failed!\n", filename);
            return -1;
        }
    }

    /* write data to device */
    if(atoi(argv[2]) == 2){ 
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, sizeof(writebuf));
        if(retvalue >= 0) {
            printf("write data:%s\n", writebuf);
        } else {
            printf("write file %s failed!\n", filename);
            return -1;
        }
    }

    /* close device file */
    retvalue = close(fd);
    if(retvalue < 0) {
        printf("close %s failed!\n", filename);
        return -1;
    }

    return 0;
}

3.2 编译驱动程序和测试APP

3.2.1 编译驱动程序

在1_chrdevbase 目录中新建 Makefile 文件,在此文件中输入如下内容:

sh 复制代码
KERNELDIR := /home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga

CURRENT_PATH := $(shell pwd)

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

obj-m := chrdevbase.o

build: kernel_modules

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

clean:
	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

其中:

  • KERNELDIR 表示开发板所使用的 Linux 内核源码目录

  • CURRENT_PATH 表示当前路径

  • obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。

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

Makefile 编写好以后输入"make"命令编译驱动模块:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/1_chrdevbase$ make 
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C /home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/home/pzs/linux/drivers/1_chrdevbase  modules
make[1]: Entering directory '/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
  CC [M]  /home/pzs/linux/drivers/1_chrdevbase/chrdevbase.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/pzs/linux/drivers/1_chrdevbase/chrdevbase.mod.o
  LD [M]  /home/pzs/linux/drivers/1_chrdevbase/chrdevbase.ko
make[1]: Leaving directory '/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
3.2.2 编译测试APP

测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 来编译,输入如下命令:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/1_chrdevbase$ arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

编译完成以后会生成一个叫做 chrdevbaseApp 的可执行程序,输入如下命令查看chrdevbaseAPP 这个程序的文件信息:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/1_chrdevbase$ file ./chrdevbaseApp
./chrdevbaseApp: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.31, BuildID[sha1]=c0f63d765eee26d47c76b674144d46c310e6d1f8, with debug_info, not stripped

chrdevbaseAPP 这个可执行文件是 32 位 LSB 格式,ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下运行。

4 运行测试

4.1 加载驱动模块

为了方便测试,Linux 系统选择通过 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统,确保 uboot 中环境变量的值如下所示:

sh 复制代码
=> setenv ipaddr 192.168.137.3
=> setenv ethaddr b8:ae:1d:01:00:00
=> setenv gatewayip 192.168.137.1
=> setenv netmask 255.255.255.0
=> setenv serverip 192.168.137.2
==> setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.137.2:/home/pzs/linux/nfs/ubuntu_rootfs,proto=tcp rw ip=192.168.137.3:192.168.137.2:192.168.137.1:255.255.255.0::eth0:off'
==> setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
==> saveenv
==> boot

设置好以后启动 Linux 系统,检查开发板根文件系统中有没有"/lib/modules/4.1.15"这个目录,如果没有的话自行创建。

注:4.1.15是Linux的版本号。

因为是通过 NFS 将 Ubuntu 中的 rootfs目录挂载为根文件系统的,所以将对应的驱动程序和测试APP复制到根目录下:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/1_chrdevbase$ sudo mkdir -p /home/pzs/linux/nfs/ubuntu_rootfs/lib/modules/4.1.15
pzs@pzs-jammy:~/linux/drivers/1_chrdevbase$ sudo cp chrdevbase.ko chrdevbaseApp /home/pzs/linux/nfs/ubuntu_rootfs/lib/modules/4.1.15/ -f

输入如下命令加载 chrdevbase.ko 驱动文件:

sh 复制代码
root@alientek_imx6ul:/home/pzs# touch /lib/modules/4.1.15/modules.order
root@alientek_imx6ul:/home/pzs# touch /lib/modules/4.1.15/modules.builtin
root@alientek_imx6ul:/home/pzs# depmod -a
root@alientek_imx6ul:/home/pzs# modprobe chrdevbase

注:如果modprobe 提示无法打开"modules.dep"这个文件,那么需要自动生成一下,使用depmod这个命令。输入"depmod"命令以后会自动生成 modules.alias、modules.symbols 和 modules.dep 这三个文件

之后可以通过dmesg查看驱动加载的情况:

sh 复制代码
root@alientek_imx6ul:/home/pzs# dmesg | tail -n 1
[  534.079779] chrdevbase register success!

可以看到"chrdevbase register success!"这一行,说明驱动加载成功了。

输入"lsmod"命令即可查看当前系统中存在的模块:

sh 复制代码
root@alientek_imx6ul:/home/pzs# lsmod
Module                  Size  Used by
chrdevbase              2367  0

可以看到chrdevbase已经存在于系统当中了。

令查看当前系统中有没有 chrdevbase 这个设备:

sh 复制代码
root@alientek_imx6ul:/home/pzs# cat /proc/devices
Character devices:
....
200 chrdevbase
...

当前系统存在 chrdevbase 这个设备,主设备号为 200,跟我们设置的主设备号一致。

4.2 创建设备节点

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

sh 复制代码
root@alientek_imx6ul:/home/pzs# mknod /dev/chrdevbase c 200 0

其中:

  • 中"mknod"是创建节点命令
  • "/dev/chrdevbase"是要创建的节点文件
  • "c"表示这是个字符设备
  • "200"是设备的主设备号
  • "0"是设备的次设备号。

可以使用"ls /dev/chrdevbase -l"命令查看:

sh 复制代码
root@alientek_imx6ul:/home/pzs# ls /dev/chrdevbase -l
crw-r--r-- 1 root root 200, 0 Dec 17 17:55 /dev/chrdevbase

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

4.3 chrdevbase设备操作测试

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

sh 复制代码
root@alientek_imx6ul:/home/pzs# cd /lib/modules/4.1.15/
root@alientek_imx6ul:/lib/modules/4.1.15# ./chrdevbaseApp /dev/chrdevbase 1
read data:kernel data!

查看dmesg的数据:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# dmesg | tail
....
[  733.433083] chrdevbase open!
[  733.433186] chrdevbase read!
[  733.433220] kernel senddata:kernel data!
[  733.439939] chrdevbase release!

接下来测试对 chrdevbase 设备的写操作,输入如下命令:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# ./chrdevbaseApp /dev/chrdevbase 2
write data:usr data!

查看dmesg的数据:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# dmesg | tail
....
[  922.223722] chrdevbase open!
[  922.223789] chrdevbase write!
[  922.223809] kernel recevdata:usr data!
[  922.224563] chrdevbase release!

4.4 卸载驱动模块

如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉 chrdevbase 这个设备:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# rmmod chrdevbase.ko

卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# lsmod
Module                  Size  Used by

此时系统已经没有任何模块了,chrdevbase 这个模块也不存在了,说明模块卸载成功。

至此,chrdevbase 这个设备的整个驱动就验证完成了,驱动工作正常。

相关推荐
wxh_无香花自开2 小时前
Linux 笔记:rpm命令
linux·运维·笔记·rpm
乐迪信息2 小时前
乐迪信息:AI摄像机识别煤矿出入井车辆数量异常检测
大数据·运维·人工智能·物联网·安全
张童瑶2 小时前
Linux SSH配置密钥文件及免密登录
linux·运维·ssh
tianyuanwo2 小时前
Linux密码加密技术深度解析:从盐值到加密算法的完整指南
linux·密码加密
翼龙云_cloud2 小时前
腾讯云渠道商:腾讯云快照和镜像备份区别在哪?
运维·服务器·云计算·腾讯云
YMatrix 官方技术社区2 小时前
YMatrix 高可用详解:3 种镜像策略在节点宕机时表现有何不同?
运维·数据库·数据仓库·ai·数据库开发·数据库架构·ymatrix
vortex52 小时前
Bash 替换机制(三):变量替换
linux·开发语言·bash
Elastic 中国社区官方博客2 小时前
在 Kibana 中可视化你的 Bosch Smart Home 数据
大数据·运维·elasticsearch·搜索引擎·信息可视化·全文检索·kibana
松涛和鸣2 小时前
34、 Linux IPC进程间通信:无名管道(Pipe) 和有名管道(FIFO)
linux·服务器·c语言·网络·数据结构·数据库