• Linux字符设备驱动开发一


    应用程序调用文件系统的API(open、close、read、write) -> 文件系统根据访问的设备类型,调用对应设备的驱动API -> 驱动对硬件进行操作。
    请添加图片描述

    0 驱动介绍

    驱动类型:

    1. 字符设备驱动:以一个字节一个字节读写的硬件对应的驱动。比如串口、watchdog、rtc、鼠标、触摸屏等。字符设备必须以串行顺序依次进行访问。
    2. 块设备驱动:以一个扇区一个扇区(512kb、4096kb)读写的硬件对应的驱动。比如硬盘。块设备可以按任意顺序进行访问。
    3. 网络设备驱动:走内核里的协议栈内存。比如网卡驱动、can驱动。网络设备面向数据包的接收和发送而设计。

    网络设备外,字符设备与块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open()、write()、read()、close()等即可访问字符设备和块设备。

    驱动的动态加载和静态加载区别:

    1. 编译方式不同:静态加载–obj-y,动态加载–obj-m
    2. 存在位置不同:静态加载,则直接编译到linux内核镜像中uimag;动态加载,则是独立的ko,驱动代码不在内核镜像中,而是独立存在于文件系统中
    3. 加载时机不同:静态加载,内核启动时就加载;动态加载,在系统启动的时候首先uboot的启动,然后linux内核启动,最后文件系统启动。静态加载,随linux内核一起加载启动,动态加载则需要等文件系统启动后手动insmod加载。

    动态加载优势:

    1. 实现热插拔机制:硬件要实现热插拔,则驱动必须使用动态加载。设备插入时,加载驱动;设备移除时,卸载驱动。
    2. 驱动调试:调试更方便。单独编译驱动,通过insmod、rmmod加载和卸载进行单独调试。
    3. 开机优化:有的驱动加载非常耗时,影响设备启动速度。动态加载,可以等用户的图形界面起来后,再加载驱动,让用户感觉系统启动速度加快了。

    1 字符设备驱动

    字符设备必须以串行顺序依次进行访问,常见的字符设备:串口、watchdog、rtc、鼠标、触摸屏等。字符设备的读写以字节为单位。

    1.1 字符设备相关概念和结构体

    了解字符设备相关概念和结构体,为之后阅读字符设备驱动代码打下坚实基础。

    1 设备号dev_t:类似一个人的身份证号。内核中通过dev_t来描述设备号,其实质是unsigned int 32位整数,其中高12位为主设备号,低20位为次设备号。

    • 为每一个外设设置一个独一无二的ID,让内核能够区分每一个设备。
    • 高12区分不同类别的设备
    • 低20位,区分同一类别的不同设备

    linux如何为每一个设备生成设备号呢?
    可以是驱动自行构造一个设备号,然后调用函数register_chrdev_region向内核注册。如果注册成功,该函数返回0;注册失败,返回一个负数,就不能用这个设备号。

    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    
    • 1

    2 设备描述cdev、file_operations:
    一个linux设备,需要通过cdev结构体来描述设备信息,通过file_operations来描述如何操作设备。最后要通过cdev_add函数来注册设备,让linux内核知道这个设备。

    /*设备信息的描述*/
    struct cdev {
        struct kobject kobj; /**/
        struct module *owner;
        const struct file_operations *ops; /*一组函数集*/
        struct list_head list; /*链表*/
        dev_t dev; /*设备号*/
        unsigned int count; /*已支持的设备*/
    }
    
    /*设备行为的描述:驱动设备开发,就要实现这些函数*/
    struct file_operations {
    	struct module *owner;
    	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        int (*open) (struct inode *, struct file *);
        int (*release) (struct inode *, struct file *);
        ...
    };
    
    /*相关注册函数*/
    int cdev_add(struct cdev *p, dev_t dev, unsigned count);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.2 实现简单的字符设备模块

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define OK (0)
    #define ERROR (-1)
    #define DEV_NUM (10)
    
    /*
     创建设备ID:MKDEV
     注册设备ID:
     给设备行为结构体的成员函数赋值
     初始化设备:建立设备行为结构体file_operations和设备信息结构体cdev的关系;
     添加设备:向内核注册设备,使内核知道设备,建立设备ID和设备信息cdev的关系;
     加载设备:insmod
     创建设备文件:建立设备文件与设备ID的关系:mknod /dev/my_chr_dev c 232 0
     测试设备行为结构体的成员函数功能
    */
    
    dev_t g_dev_num; /*设备号*/
    struct cdev *g_cdev; /*设备信息*/
    struct file_operations *g_fops; /*设备行为信息*/
    const int g_major = 232; /*主设备号*/
    const int g_minor = 0; /*次设备号*/
    unsigned g_dev_count = 3;
    const char g_dev_name[] = "my_char_dev"; /*设备名称,内核管理用的,随便写*/
    
    ssize_t my_dev_read(struct file *my_file, char __user *my_user, size_t my_size, loff_t *my_loff)
    {
        printk(KERN_EMERG "read my dev success\n");
        return OK;
    }
    
    ssize_t my_dev_write(struct file *my_file, const char __user *my_user, size_t my_size, loff_t *my_loff)
    {
        printk(KERN_EMERG "write my dev success\n");
        return OK;
    }
    
    int my_dev_open(struct inode *my_inode, struct file *my_file)
    {
        printk(KERN_EMERG "open my dev success\n");
        return OK;
    }
    
    int my_dev_release(struct inode *my_inode, struct file *my_file)
    {
        printk(KERN_EMERG "release my dev success\n");
        return OK;
    }
    
    static int __init chr_init(void)
    {
        g_dev_num = MKDEV(g_major, g_minor); /*生成设备号*/
        printk("dev id:%d\n", g_dev_num);
    
        /*注册设备号*/
        if (register_chrdev_region(g_dev_num, g_dev_count, g_dev_name) != 0) {
            printk(KERN_EMERG "register dev id failed\n");
            return ERROR;
        }
    
        /*内核函数:自动生成设备函数, 避免自定义设备号冲突,导致注册失败
        if (alloc_chrdev_region(&g_dev_num, g_major, g_dev_count, g_dev_name) != OK) {
            printk(KERN_EMERG "register dev id failed\n");
            return ERROR;
        }
        */
    
       /*为设备信息结构体cdev和设备行为结构体file_operations申请内存;kzalloc相比kmalloc,会将申请的内存地址置为0,*/
        g_cdev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
        g_fops = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
    
        /*为设备行为结构体成员函数赋值*/
        g_fops->read = my_dev_read;
        g_fops->write = my_dev_write;
        g_fops->open = my_dev_open;
        g_fops->release = my_dev_release;
        g_fops->owner = THIS_MODULE; /*一般就赋值为THIS_MODULE宏*/
    
        cdev_init(g_cdev, g_fops); /*初始化设备:建立cdev和file_operations的联系*/
        cdev_add(g_cdev, g_dev_num, g_dev_count); /*向内核注册设备:建立cdev和设备号的联系*/
        printk(KERN_EMERG "add my_chr_dev success\n");
        return OK;
    }
    
    static void __exit chr_exit(void)
    {
        cdev_del(g_cdev); /*删除设备*/
        unregister_chrdev_region(g_dev_num, g_dev_count); /*注销设备号*/
        printk(KERN_EMERG "unregister my_chr_dev success\n");
    }
    
    module_init(chr_init);
    module_exit(chr_exit);
    MODULE_LICENSE("GPL");
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    make编译模块后,insmod my_chr_dev加载字符设备驱动。
    makefile文件解读:

    • ifneq,首先判断是否定义了内核宏,第一次执行由于没有定义内核宏,先执行else分支,然后执行all。
    • make -C,切换到内核目录内,-M指定编译的模块。此时进入-M指定的模块内,再次执行makefile文件。
    • 此时,内核宏定义了,执行if分支,编译内核模块。
    ifneq ($(KERNELRELEASE),)
      obj-m := my_chr_dev.o
    else
      KERDIR ?="/lib/modules/$(shell uname -r)"/build
      PWD := $(shell pwd)
    
    all:
    	$(MAKE) -C $(KERDIR) M=$(PWD)
    clean:
    	rm -rf *.o *.ko *mod.c 9.module.o *.order
    endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1.3 创建字符设备

    mknod /dev/my_chr_dev c 232 0232 0为主、次设备号。

    • mknod,实际上就是建立设备文件my_chr_dev与设备号232 0之间的联系。

    驱动测试程序:

    #include 
    #include  /*O_RDWR*/
    #include  /*read open write close usleep*/
    #define DATA_NUM (10)
    
    int main()
    {
        int fd;
        int wd, rd;
        char my_buffer[DATA_NUM] = "12345";
        fd = open("/dev/my_chr_dev", O_RDWR);
        printf("fd=%d\n", fd);
        if (fd == -1){
            printf("open failed\n");
            return -1;
        }
        wd = write(fd, my_buffer, DATA_NUM);
        rd = read(fd, my_buffer, DATA_NUM);
        printf("wd=%d, rd=%d\n", wd, rd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    该程序用于测试驱动程序的open、write、read、release。编译执行该驱动程序后,执行dmesg,可以查看内核调用驱动程序对应的open、write、read、release函数时日志信息。

    1.4 总结

    开发字符设备驱动主要有7步:

    1. 创建设备号MKDEV、注册设备号register_chrdev_region;
    2. 编写设备行为结构体的成员函数:成员函数实现硬件的open/close/read/write等操作。
    3. 建立设备信息结构体cdev与设备行为结构体file_operations之间的联系:cdev_init
    4. 向内核注册设备,建立设备信息结构体与设备号之间的联系:cdev_add
    5. make编译、insmod加载设备驱动。
    6. 创建设备文件,建立设备文件与设备号之间的联系:mknod /dev/chrDev c 主设备号 次设备号
    7. 编写测试脚本,测试驱动程序的open/close/read/write功能。
  • 相关阅读:
    《MySQL实战45讲》——学习笔记04-05 “深入浅出索引、最左前缀原则、索引下推优化“
    学习使用doxygen生成代码的文档
    大厂面试题-JVM中的三色标记法是什么?
    PHP相关漏洞
    SQL中的单行注释,多行注释
    线程和线程池
    WuThreat ITDR 可以快速构建多场景的身份认证与威胁检测能力
    JavaScript数组常用的方法整理
    python3.10 安装问题解决
    09/10的一周
  • 原文地址:https://blog.csdn.net/luofeng_/article/details/136666957