Linux 驱动程序开发

Linux 驱动程序开发

设备驱动概述

设备驱动在操作系统中的位置

  • 设备驱动程序是 内核 代码的一部分 , 驱动程序的地址空间是内核的地址空间
  • 驱动程序的代码直接对设备硬件 直接对设备硬件(实际是设 实际是设备的各种寄存器 备的各种寄存器) 进行控制( 读写操作)
  • 应用程序通过操作系统的系统调用执行相应的驱动程序函数 ,应 中断则直接执行相应 的中断程序代码
  • 设备驱动程序的file_operations结构体的 结构体的地址被注册到内核中的设备链表中

设备驱动的主要功能

  • 对设备进行初始化 , 启动或停止设备的运行 。
  • 把数据从内核传送到硬件和从硬件读取数据 。
  • 读取应用程序传送给设备文件的数据和回送应用程序请求的数据 。
  • 检测和处理设备出现的错误等

驱动程序与应用程序的区别

  • 驱动程序没有main函数 ,通过使用宏 module_init (初始化函数名), 将初始化函数加入内核全局初始化函数列表中
  • 驱动程序中有一个宏moudule_exit(退出处 处理函数名)注册退出处理函数 注册退出处理函数 。它在驱动退出时被调用 。
  • 驱动程序没有内存保护机制
  • 应用程序可以和GLIBC 库连接 库连接 , 因此可以包含标准的头文件,如:<stdio.h> <stdlib.h>。
  • 在驱动程序中是不能使用标准C 库的 库的 ,因 因 此不能调用所有的 此不能调用所有的C 库函数 库函数,如:输出打印函数只能使用内核的printk函数;包含的头文件只能是内核的头文件,如:<linux/module.h >。

设备分类

  • Linux有三种基本设备: 字符设备 、 块设备、 、 网络设备 。
  • 字符设备:字符( char ) 设备是一种按字节来访问的设备 , 可以当作一个字节流来存取 。文本控制台( /dev/console )和串口( /dev/ttyS0 ) 是字符设备的例子
  • 块设备 :块设备也通过位于 /dev 目录的文件系统结点来存取。Linux允许块设备传送任意数目的字节
  • 网络设备:任何网络事务都通过一个接口来进行 , 一个能够与其他主机交换数据的设备。一个网络接口负责发送和接收数据报文,在内核网络子系统的驱动下,不必知道单个事务是如何映射到实际的被发送的报文的。

Linux设备号

  • 设备号是一个数字 , 是设备的标志 , 由主设备号和次设备号组成

    • 主设备号表明某一类设备,主设备号相同的设备使用相同的驱动程序
    • 次设备号用来标识具体设备的实例。
    • 系统中块设备IDE硬盘的主设备号是 3,多个 IDE硬盘及其各个分区的次设备号1、2、……
  • 从dev_t ( unsigned int 32位整数)中分解主次设备号

    • 主设备号 MAJOR(dev_t dev),高12位
    • 次设备号 MINOR(dev_t dev),低20位
  • Linux 系统下有关主设备号的分配原则,可参看documentation/device.txt

  • Linux的设备驱动程序通常在 “/dev”下面存在一个对应的逻辑设备节点

    image-20210110201810622

创建设备文件

  • 可使用mknod手工创建 手工创建 mknod filename type major minnor
    • filename:设备文件名
    • type:设备文件类型
    • major:主设备号
    • minor:次设备号
  • 例 mknod s3c241sel c 231 0

IO控制方式

  • 驱动程序的IO控制方式有:查询方式 、 中断方式 、 直接访问内存(DMA) 方式 、 通道方式
  • 查询方式占用CPU,中断处理存在延迟
  • 利用DMA进行数据传输的同时, 处理器仍然可以继续执行指令
  • 追求高效的大中型机器采用具有独立处理器的通道来实现IO传输控制。

设备驱动程序接口

Linux设备驱动的加载方式

  • 在LINUX下加载驱动程序可以采用动态 和 静态 两种方式 。
  • 静态加载 就是把驱动程序直接编译到内核里 ,在执行make menuconfig 命令时选择
    • 驱动编译进内核后,系统启动后可以直接调用
    • Linux最基础的内核模块,如CPU、PCI总线、 TCP/IP协议、VFS等则直接编译在内核文件中
  • 动态加载利用了 利用了LINUX的模块化特性,可在系统启动后用 insmod 命令把驱动程序(.o 文件)添加上去,在不需要的时候用rmmod命令来卸载
    • 要把一般文件(设备文件通常在/dev目录)和内核模块链接在一起需要两个数据:主设备号和从设备号(两者都在模块定义时声明)。
    • 主设备号用于内核把文件和它的驱动链接在一起。从设备号用于设备内部使用。

相关命令

  • lsmod命令:查看当前加载到内核中的所有驱动模块 同时提供其它一些信息 , 比如其它模块是否在使用另一个模块
  • modinfo命令:用来查看模块信息 ,如modinfo nfs
  • rmmod命令:删除模块
  • insmod命令:是插入模块的命令 , 但是它不会自动解决依存关系 , 所以一般加载内核模块时使用的命令为modprobe
  • modprobe命令:智能插入模块 , 它可以根据模块间依存关系 , 以及/etc/modules.conf 文件中的内容智能插入模块

设备驱动程序接口

  • 系统调用对设备的操作过程 ,比如open ,read ,write ,ioctl、 release 等操作

  • 设备驱动程序所提供的这组入口点 , 分别是 file_operations 数据结构、 inode 数据结构和 file 数据结构 。

  • 通常所说的设备驱动程序接口是指结构 file_operations{} ,它定义在 它定义在 include/linux/fs.h 中

    image-20210110203249747

常用操作

  • *lseek, 移动文件指针的位置,只能用于可以随机存取的设备。
  • *read, 进行读操作
  • *write, 进行写操作,与read类似
  • *readdir, 取得下一个目录入口点,只有与文件系统相关的设备程序才使用。
  • *ioctl, 进行读、写以外的其他操作,参数cmd 为自定义的命令 。
  • *mmap, 用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。
  • *open, 打开设备准备进行I/O操作。返回0表示打开成功,返回负数表示失败。如果驱动程序 没有提供open入口,则只要/dev/driver文件存在就认为打开成功。
  • *release, 即close操作。

设备驱动开发流程

  • 1、编写驱动模块
  • 2、编写Makefile文件
  • 3、编译驱动模块
  • 4、加载驱动模块
  • 5、在 /dev 创建设备文件(如果是驱动模块)

1、编写驱动模块

  • 应用程序一般有一个 main 函数,从头到尾执行 一个任务;驱动程序却不同,它没有 main函数

  • 通 过 使 用 宏 ( module_init(初始化函数名) ),将初始化函数加入内核全局初始化函数列表中

  • 驱 动 程 序 中 有 一 个 宏 ( moudule_exit(退出处理函数名) ) 。它在驱动退出时被调用。

    image-20210110203734325

驱动程序框架

  • 头文件:大多的 Linux驱动程序需要包含下面三个头文件:

    • #include <linux/init.h>:定义了驱动的初始化和退出相关的函数
    • #include <linux/module.h>:定义了经常用到的函数原型及宏定义
    • #include <linux/kernel.h>:定义了内核模块相关的函数、变量及宏
  • 初始化

    • 驱动程序是通过 module_init宏来声明初始化函数的:

      image-20210110204641080
  • 卸载:如果驱动程序编译成模块(动态加 载)模式,那么它需要一个清理函数。当移除一个内核模块时这个函数被调用执行清理工作。

    • 驱动程序是通过 module_exit宏来声明清理函数的:
      image-20210110204857172

2、编写驱动模块的Makefile文件

  • 针对以上源码写一个 Makefile文件用来编译它, Makefile和 hello. c文件保存在同一个目 录下。

  • Makefile文件的内容可以简单编写如下内容:

    image-20210110203815410

3、编译驱动模块

  • 先进入 linux内核所在的目录,并编译出 hello. o 文件

  • 然后创建模块,运行 MODPOST生成临时的 hello. mod. c文件,而后根据此文件编译出 hello. mod. o

  • 连接 hello. o和 hello. mod. o文件得到模块目标文件 hello. ko

    image-20210110204147161
  • 编译后可能结果

    image-20210110204222696

4、加载驱动模块

  • 使用 insmod来加载该驱动模块,并且使用相关命令来验证或查看该驱动模块运行的信息

    image-20210110204448061

5、由于上面示例的模块创建不是驱动模块,只是创建了一个模块,所以即使在 /dev 创建了设备文件也无法和模块绑定。

  • 设备模块声明示例(假设有一个内存驱动)

    /* Necessary includes for device drivers */
    #include <linux/init.h>
    #include <linux/config.h>
    #include <linux/module.h>
    #include <linux/kernel.h> /* printk() */
    #include <linux/slab.h> /* kmalloc() */
    #include <linux/fs.h> /* everything... */
    #include <linux/errno.h> /* error codes */
    #include <linux/types.h> /* size_t */
    #include <linux/proc_fs.h>
    #include <linux/fcntl.h> /* O_ACCMODE */
    #include <asm/system.h> /* cli(), *_flags */
    #include <asm/uaccess.h> /* copy_from/to_user */
    
    MODULE_LICENSE("Dual BSD/GPL");
    
    /* Declaration of memory.c functions */
    int memory_open(struct inode *inode, struct file *filp);
    int memory_release(struct inode *inode, struct file *filp);
    ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);
    ssize_t memory_write(struct file *filp, char *buf,size_t count, loff_t *f_pos);
    
    void memory_exit(void);
    int memory_init(void);
    
    /* Structure that declares the usual file */
    /* access functions */
    /*
     * 给 file_operations 定义的操作赋值(函数指针)
     * 即操作对应设备文件时会执行的操作
     */
    struct file_operations memory_fops = {
        read: memory_read,
        write: memory_write,
        open: memory_open,
        release: memory_release
    };
    
    /* Declaration of the init and exit functions */
    /**
     * 模块初始化操作
     */
    module_init(memory_init);
    module_exit(memory_exit);
    
    /* Global variables of the driver */
    /* Major number */
    /**
     * 指定对应的设备号
     */
    int memory_major = 60;
    
    /* Buffer to store data */
    /**
     * 将用于存储该驱动的数据
     */
    char *memory_buffer;
    
  • “memory”驱动:绑定设备文件

    • 在安装模块时,调用 register_chrdev 函数用于 在内核空间,把驱动和/dev下设备文件链接在一起
    • 它由三个参数:主设备号,模块、名称和一个 file_operations结构的指针。
    int memory_init(void) {
        int result;
        /* Registering device */
        /**
         * 注册驱动
         */
        result = register_chrdev(memory_major, "memory",&memory_fops);
        if (result < 0) {
            printk("<1>memory: cannot obtain major number %d\n", memory_major);
            return result;
        }
        
        /* Allocating memory for the buffer */
        /**
         * 为该驱动程序的缓冲区分配内存。
         */
        memory_buffer = kmalloc(1, GFP_KERNEL);
        if (!memory_buffer) {
            result = -ENOMEM;
            goto fail;
        }
        
        memset(memory_buffer, 0, 1);
        printk("<1>Inserting memory module\n");
        return 0;
        
        /**
         * 如果注册主设备号或者分配内存失败,模块将退出。
         */
        fail:
            memory_exit();
        	return result;
    }
    
  • “memory”驱动:卸载驱动

    • 在通过 memory_exit函数卸载模块时,需要调用 unregsiter_chrdev函数,其将释放驱动之前向内核申请的主设备号。
    void memory_exit(void) {
    /* Freeing the major number */
        unregister_chrdev(memory_major, "memory");
        /* Freeing buffer memory */
        if (memory_buffer) {
            // 为了完全的卸载该驱动,缓冲区也需要通过该函数进行释放。
        	kfree(memory_buffer);
        }
        printk("<1>Removing memory module\n");
    }
    
  • “memory”驱动:创建设备文件

    • 需要创建一个文件(该设备文件用于和设备驱动操作),mknod /dev/memory c 60 0
    • 其中,c说明创建的是字符设备,60是主设备号,0是从设备号。
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值