精华内容
下载资源
问答
  • Linux设备驱动程序教程将为您提供有关如何为Linux操作系统编写设备驱动程序的所有必要信息。 本文包含一个易于遵循的实用Linux驱动程序开发示例。 我们将讨论以下内容: 内核日志系统 如何使用角色设备 如何使用...

    翻译来自:
    https://www.apriorit.com/dev-blog/195-simple-driver-for-linux-os
    代码下载

    此Linux设备驱动程序教程将为您提供有关如何为Linux操作系统编写设备驱动程序的所有必要信息。 本文包含一个易于遵循的实用Linux驱动程序开发示例。 我们将讨论以下内容:

    • 内核日志系统
    • 如何使用角色设备
    • 如何使用内核中的用户级内存

    我们将使用Linux内核版本2.6.32。 我们可以使用更新的版本,但是它们的API可能已被修改,因此可能与我们的示例和构建系统中使用的API不同。 学习本教程后,您将熟悉为Linux操作系统编写设备驱动程序或内核模块的过程。

    概述

    Linux有一个单片内核。 因此,为Linux编写设备驱动程序需要与内核进行组合编译。 另一种方法是将驱动程序实现为内核模块,在这种情况下,您无需重新编译内核即可添加其他驱动程序。 我们将关注第二个选项:内核模块。

    在其基础上,模块是专门设计的目标文件。 使用模块时,Linux会将它们加载到其内核空间,从而将它们链接到内核。 Linux内核是使用C编程语言和Assembler开发的。 C实现了内核的主要部分,而Assembler实现了依赖于体系结构的部分。 不幸的是,这些是我们用于编写Linux设备驱动程序的唯一两种语言。 我们不能使用用于Microsoft Windows操作系统内核的C ++,因为Linux内核源代码的某些部分 - 特定的头文件 - 可能包含来自C ++的关键字(例如, delete或new ),而在Assembler中我们可能会遇到诸如’ : : ‘词汇。

    我们在内核上下文中运行模块代码。 这需要开发人员非常专注,因为它需要承担额外的责任:如果开发人员在实现用户级应用程序时出错,在大多数情况下这不会导致用户应用程序之外的问题; 但是如果开发人员在实现内核模块时出错,后果将是系统级别的问题。 幸运的是,Linux内核有一个很好的功能,可以抵御模块代码中的错误。 当内核遇到非严重错误(例如,空指针解除引用)时,您将看到oops消息(Linux操作期间无意义的故障称为oops ),之后将卸载故障模块,允许内核和其他模块像往常一样工作。 此外,您还可以在内核日志中找到准确描述此错误的记录。 但请注意,不建议在oops消息之后继续工作,因为这样做可能会导致不稳定和内核恐慌。

    内核及其模块本质上代表一个程序模块 - 因此请记住,单个程序模块使用单个全局命名空间。 为了最小化它,您必须观察模块导出的内容:导出的全局字符必须唯一命名(常用的解决方法是简单地使用将字符导出为前缀的模块的名称)并且必须剪切到最低限度。

    装载和卸载模块

    要创建一个简单的示例模块,我们不需要做太多工作。 这里有一些代码可以证明这一点:

    #include <linux/init.h>
    #include <linux/module.h>
    
    static int my_init(void)
    {
                           return  0;
    }
    
    static void my_exit(void)
    {
                           return;
    }
    
    module_init(my_init);
    module_exit(my_exit);

    这个模块唯一做的两件事就是加载和卸载自己。 要加载Linux驱动程序,我们调用my_init函数,并卸载它,我们调用my_exit函数。 module_init和module_exit宏通知内核有关驱动程序的加载和卸载。 my_init和my_exit函数必须具有相同的签名,这些签名必须完全如下:

    int init(void);
    void exit(void);

    如果模块需要某个内核版本并且必须包含有关版本的信息,我们需要链接linux / module.h头文件。 尝试加载为另一个内核版本构建的模块将导致Linux操作系统禁止其加载。 出现这种情况的原因是:内核API的更新经常被释放,当您调用其签名已更改的模块函数时,会对整个堆栈造成损害。 module_init和module_exit宏在linux / init.h头文件中声明。

    注册角色设备

    上面的示例模块非常简单; 现在我们将开始处理更复杂的事情。 然而,这个简短的Linux内核驱动程序教程的目的之一是展示如何使用登录内核以及如何与设备文件交互。 这些工具可能很简单,但它们可以为任何驱动程序派上用场,并且在某种程度上,它们使内核模式开发过程更加丰富。

    首先,这里有一些有关设备文件的有用信息。 通常,您可以在/ dev文件夹中找到设备文件。 它们促进了用户和内核代码之间的交互。 如果内核必须接收任何内容,您只需将其写入设备文件即可将其传递给提供此文件的模块; 从设备文件中读取的任何内容都来自提供此文件的模块。 我们可以将设备文件分为两组:字符文件和块文件。 字符文件是非缓冲的,而块文件是缓冲的。 正如其名称所暗示的那样,字符文件允许您逐个字符地读取和写入数据,而块文件允许您只写入整个数据块。 我们将讨论块文件超出本文的范围,并将直接获得字符文件。

    Linux系统有一种通过主设备号识别设备文件的方法, 主设备号识别服务设备文件或一组设备的模块,以及次要设备号 ,用于识别主设备号指定的一组设备中的特定设备。 在驱动程序代码中,我们可以将这些数字定义为常量,也可以动态分配它们。 如果已经使用了定义为常量的数字,系统将返回错误。 当动态分配一个数字时,该函数保留该数字以禁止其他任何数字使用它。

    指定设备的名称

    下面引用的函数用于注册字符设备:

    int register_chrdev (unsigned int   major,
                         const char *   name,
                         const struct   fops);
                         file_operations *

    在这里,我们指定要注册它的设备的名称和主要编号,之后将链接设备和file_operations结构。 如果我们为主参数指定零,该函数将自己分配一个主设备号(即它返回的值)。 如果返回的值为零,则表示成功,而负数表示错误。 两个设备编号均在0-255范围内指定。

    我们将设备名称作为name参数的字符串值传递(如果模块注册单个设备,则此字符串也可以传递模块的名称)。 然后,我们使用此字符串来标识/ sys / devices文件中的设备。 读取,写入和保存等设备文件操作由存储在file_operations结构中的函数指针处理。 这些函数由模块实现,并且指向标识该模块的module结构的指针也存储在file_operations结构中。 在这里你可以看到2.6.32内核版本结构:

    
    struct file_operations {
           struct module *owner;
           loff_t (*llseek) (struct file *, loff_t, int);
           ssize_t (*read) (struct file *, char *, size_t, loff_t *);
           ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
           int (*readdir) (struct file *, void *, filldir_t);
           unsigned int (*poll) (struct file *, struct poll_table_struct *);
           int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
           int (*mmap) (struct file *, struct vm_area_struct *);
           int (*open) (struct inode *, struct file *);
           int (*flush) (struct file *);
           int (*release) (struct inode *, struct file *);
           int (*fsync) (struct file *, struct dentry *, int datasync);
           int (*fasync) (int, struct file *, int);
           int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,
              loff_t *);
        ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,
              loff_t *);
        };

    file_operations结构

    如果file_operations结构包含一些不需要的函数,您仍然可以使用该文件而不实现它们。 指向未实现函数的指针可以简单地设置为零。 之后,系统将负责该功能的实现并使其正常运行。 在我们的例子中,我们将实现read函数。

    由于我们要确保只使用我们的Linux驱动程序操作单一类型的设备,因此我们的file_operations结构将是全局和静态的。 相应地,在它创建之后,我们需要静态填充它。 在这里你可以看到这是如何完成的:

    
    static struct file_operations simple_driver_fops = 
    {
        .owner   = THIS_MODULE,
        .read    = device_file_read,
    };

    THIS_MODULE宏的声明包含在linux / module.h头文件中。 我们将宏转换为指向所需模块的模块结构的指针。 稍后,我们将用原型编写函数体,但是现在我们只有指向它的指针,即device_file_read 。

    ssize_t device_file_read (struct file *, char *, size_t, loff_t *);

    file_operations结构允许我们编写几个函数来执行和撤销设备文件的注册。

    static int device_file_major_number = 0;
    static const char device_name[] = "Simple-driver";
    static int register_device(void)
    {
            int result = 0;
            printk( KERN_NOTICE "Simple-driver: register_device() is called." );
            result = register_chrdev( 0, device_name, &simple_driver_fops );
            if( result < 0 )
            {
                printk( KERN_WARNING "Simple-driver:  can\'t register character device with errorcode = %i", result );
                return result;
            }
            device_file_major_number = result;
            printk( KERN_NOTICE "Simple-driver: registered character device with major number = %i and minor numbers 0...255"
                 , device_file_major_number );
            return 0;
    }

    device_file_major _number是一个包含主设备号的全局变量。 当驱动程序的生命周期到期时,此全局变量将撤消设备文件的注册。

    printk Fucntion

    我们已经列出并提到了几乎所有的功能,最后一个是printk功能。 这个函数的声明包含在linux / kernel.h文件中,它的任务很简单:记录内核消息。 毫无疑问,请注意KERN_NOTICE和KERN_WARNING前缀,这些前缀出现在printk的所有列出的格式字符串中。 您可能已经猜到, NOTICE和WARNING表示消息的优先级。 级别从最无关紧要的KERN_DEBUG到关键的KERN_EMERG ,提醒内核不稳定。 这是printk函数和printf库函数之间的唯一区别。

    printk函数形成一个字符串,我们将其写入循环缓冲区, klog守护程序将其读取并将其发送到系统日志。 printk函数的实现允许从内核中的任何地方调用它。 最糟糕的情况是循环缓冲区溢出,这意味着最旧的消息不会记录在日志中。

    下一步是编写一个函数来恢复设备文件的注册。 如果成功注册了设备文件,则device_file_major_number的值将不为零。 这允许我们使用nregister_chrdev function撤销文件的注册,我们在linux / fs.h文件中声明了该nregister_chrdev function 。 主设备号是此函数的第一个参数,后跟包含设备名称的字符串。 register_chrdev和unresister_chrdev函数以类似的方式起作用。

    要注册设备,我们使用以下代码:

    
    void unregister_device(void)
    {
        printk( KERN_NOTICE "Simple-driver: unregister_device() is called" );
        if(device_file_major_number != 0)
        {
            unregister_chrdev(device_file_major_number, device_name);
        }
    }

    使用在用户模式下分配的内存

    我们要编写的函数将从设备中读取字符。 此函数的签名必须适合file_operations结构中的签名:

    ssize_t (*read) (struct file *, char *, size_t, loff_t *);

    让我们看看第一个参数,即指向file结构的指针。 此file结构允许我们获取有关我们正在使用的文件的必要信息,有关此当前文件的私有数据的详细信息,等等。 已读取的数据使用第二个参数(即缓冲区)分配给用户空间。 读取的字节数在第三个参数中定义,我们从第四个参数中定义的某个偏移量开始读取字节。 执行该函数后,必须返回已成功读取的字节数,之后必须刷新偏移量。

    用户在用户模式地址空间中分配特殊缓冲区。 而read函数必须执行的另一个操作是将信息复制到此缓冲区。 来自该空间的指针指向的地址和内核地址空间中的地址可以具有不同的值。 这就是我们不能简单地取消引用指针的原因。 使用这些指针时,我们有一组特定的宏和函数,我们在asm / uaccess.h文件中声明。 在我们的例子中,最合适的函数是copy_to_user() 。 它的名字不言而喻:它只是将特定数据从内核缓冲区复制到用户空间中分配的缓冲区。 此外,它还验证指针是否有效以及缓冲区大小是否足够大。 因此,可以相对容易地处理驱动器中的错误。 这是copy_to_user原型的代码:

    long copy_to_user( void __user *to, const void * from, unsigned long n );

    首先,该函数必须接收三个指针作为参数:指向缓冲区的指针,指向数据源的指针,以及指向复制的字节数的指针。 正如我们所提到的,错误返回的值不是零,并且在成功执行的情况下,该值将为零。 该函数包含_user宏,其任务是执行文档处理。 它有另一个有用的应用程序,它允许我们分析代码是否正确使用地址空间中的指针; 这是使用稀疏分析器完成的,稀疏分析器执行静态代码分析。 确保始终将用户地址空间指针标记为_user 。

    本教程仅包含没有实际设备的Linux驱动程序编程示例。 如果在读取设备文件后不需要返回除文本字符串以外的任何内容,那么这就足够了。

    这是实现read功能的代码:

    static const char    g_s_Hello_World_string[] = "Hello world from kernel mode!\n\0";
    static const ssize_t g_s_Hello_World_size = sizeof(g_s_Hello_World_string);
    static ssize_t device_file_read(
                            struct file *file_ptr
                           , char __user *user_buffer
                           , size_t count
                           , loff_t *position)
    {
        printk( KERN_NOTICE "Simple-driver: Device file is read at offset = %i, read bytes count = %u"
                    , (int)*position
                    , (unsigned int)count );
        /* If position is behind the end of a file we have nothing to read */
        if( *position >= g_s_Hello_World_size )
            return 0;
        /* If a user tries to read more than we have, read only as many bytes as we have */
        if( *position + count > g_s_Hello_World_size )
            count = g_s_Hello_World_size - *position;
        if( copy_to_user(user_buffer, g_s_Hello_World_string + *position, count) != 0 )
            return -EFAULT;    
        /* Move reading position */
        *position += count;
        return count;
    }

    构建内核模块的系统

    在我们为驱动程序编写代码之后,是时候构建它并查看它是否像我们期望的那样工作。 在早期的内核版本(例如2.4)中,构建模块需要来自开发人员的更多动作:编译环境需要单独准备,编译本身需要GCC编译器。 只有在那之后,开发人员才会收到一个* .o文件 - 一个可以加载到内核的模块。 幸运的是,这些时间早已过去,现在这个过程要简单得多。 今天,大部分工作都是由makefile完成的:它启动内核构建系统,并为内核提供有关构建模块所需组件的信息。 从单个源文件构建的模块需要makefile中的单个字符串。 创建此文件后,您只需要启动内核构建系统:

    obj-m := source_file_name.o

    如您所见,这里我们已将源文件名分配给模块,该文件将是* .ko文件。

    相应地,如果有多个源文件,则只需要两个字符串

    obj-m := module_name.o 
    module_name-objs := source_1.o source_2.o … source_n.o

    make命令初始化内核构建系统:

    要构建模块:

    make –C KERNEL_MODULE_BUILD_SYSTEM_FOLDER M=`pwd` modules

    要清理构建文件夹:

    make –C KERNEL_MODULES_BUILD_SYSTEM_FOLDER M=`pwd` clean

    模块构建系统通常位于 /lib/modules/uname -r/build中。 现在是时候准备模块构建系统了。 要构建第一个模块,请从构建系统所在的文件夹中执行以下命令:

    make modules_prepare

    最后,我们将我们学到的所有内容组合到一个makefile中:

    TARGET_MODULE:=simple-module
    # If we are running by kernel building system
    ifneq ($(KERNELRELEASE),)
        $(TARGET_MODULE)-objs := main.o device_file.o
        obj-m := $(TARGET_MODULE).o
    # If we running without kernel build system
    else
        BUILDSYSTEM_DIR:=/lib/modules/$(shell uname -r)/build
        PWD:=$(shell pwd)
    all : 
    # run kernel build system to make module
        $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) modules
    clean:
    # run kernel build system to cleanup in current directory
        $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) clean
    load:
        insmod ./$(TARGET_MODULE).ko
    unload:
        rmmod ./$(TARGET_MODULE).ko
    endif

    load目标加载构建模块, unload目标将其从内核中删除。

    在我们的教程中,我们使用了main.c和device_file.c中的代码来编译驱动程序。 生成的驱动程序名为simple-module.ko。

    加载和使用模块

    从源文件文件夹执行以下命令允许我们加载构建的模块:

    sudo make load

    执行此命令后,驱动程序的名称将添加到/proc/modules文件中,而模块注册的设备将添加到/proc/devices文件中。 添加的记录如下所示:

    Character devices: 1 mem 4 tty 4 ttyS … 239 Simple-driver …

    cat /proc/devices | grep  Simple-driver
    $ 239 Simple-driver

    前三个记录包含添加的设备的名称以及与之关联的主设备号。 次编号范围(0-255)允许在/ dev虚拟文件系统中创建设备文件。

    sudo mknod /dev/simple-driver c 239 0

    在我们创建设备文件之后,我们需要执行最终验证以确保我们所做的工作按预期工作。 要验证,我们可以使用cat命令显示内容:

    cat /dev/simple-driver
    $ Hello world from kernel mode!

    参考资料

    1. Linux设备驱动程序, Jonathan Corbet , Alessandro Rubini和Greg Kroah-Hartman的第3版: http://lwn.net/Kernel/LDD3/
    2. Peter Jay Salzman和Ori Pomeranz撰写的Linux内核模块编程指南: http://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
    3. Linux Cross Reference http://lxr.free-electrons.com/ident

    代码下载

    Demo代码下载

    尾文

    device_file.c有一点编译问题,这样修正。

    copy_to_user -> raw_copy_to_user

    查看内核日志

    dmesg

    玩~

    展开全文
  • Linux是Unix操作系统的种变种,在Linux编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别。在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是...
    Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别。在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便。

    以下的一些文字主要来源于khg,johnsonm的Write linux device driver,Brennan's Guide to Inline Assembly,The Linux A-Z,还有清华BBS上的有关device driver的一些资料。

    一、Linux device driver 的概念

      系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:
      1。对设备初始化和释放。
      2。把数据从内核传送到硬件和从硬件读取数据。
      3。读取应用程序传送给设备文件的数据和回送应用程序请求的数据。
      4。检测和处理设备出现的错误。

      在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
      已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。
      最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。
      读/写时,它首先察看缓冲区的内容,如果缓冲区的数据
      如何编写Linux操作系统下的设备驱动程序

      二、实例剖析

    我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。
        #define  __NO_VERSION__
        #include  <linux/modules.h>
        #include  <linux/version.h>
          char kernel_version [] = UTS_RELEASE; 
      这一段定义了一些版本信息,虽然用处不是很大,但也必不可少。Johnsonm说所有的驱动程序的开头都要包含<linux/config.h>,一般来讲最好使用。
      由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:
        struct file_operations
        {
        int (*seek) (struct inode * ,struct file *, off_t ,int);
        int (*read) (struct inode * ,struct file *, char ,int);
        int (*write) (struct inode * ,struct file *, off_t ,int);
        int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
        int (*select) (struct inode * ,struct file *, int ,select_table *);
        int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);
        int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
        int (*open) (struct inode * ,struct file *);
        int (*release) (struct inode * ,struct file *);
        int (*fsync) (struct inode * ,struct file *);
        int (*fasync) (struct inode * ,struct file *,int);
        int (*check_media_change) (struct inode * ,struct file *);
        int (*revalidate) (dev_t dev);
        } 
      这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
      下面就开始写子程序。
        #include <linux/types.h>
        #include <linux/fs.h>
        #include <linux/mm.h>
        #include <linux/errno.h>
        #include <asm/segment.h>
        unsigned int test_major = 0;
        static int read_test(struct inode *node,struct file *file,char *buf,int count)
        {
        int left;
        if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )
        return -EFAULT;
        for(left = count ; left > 0 ; left--)
        {
        __put_user(1,buf,1);
        buf++;
        }
        return count;
        } 
    这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。

        static int write_tibet(struct inode *inode,struct file *file,const char *buf,int count)
        {
        return count;
        }
        static int open_tibet(struct inode *inode,struct file *file )
        {
        MOD_INC_USE_COUNT;
        return 0;
        }
        static void release_tibet(struct inode *inode,struct file *file )
        {
        MOD_DEC_USE_COUNT;
        } 
      这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。
        struct file_operations test_fops = {
        NULL,
        read_test,
        write_test,
        NULL, /* test_readdir */
        NULL,
        NULL, /* test_ioctl */
        NULL, /* test_mmap */
        open_test,
        release_test,
        NULL, /* test_fsync */
        NULL, /* test_fasync */
        /* nothing more, fill with NULLs */
        };  
      设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。
        int init_module(void)
        {
        int result;
        result = register_chrdev(0, "test", &test_fops);
        if (result < 0) {
        printk(KERN_INFO "test: can't get major number\n");
        return result;
        }
        if (test_major == 0) test_major = result; /* dynamic */
        return 0;
        } 
      在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。
      如果登记成功,返回设备的主设备号,不成功,返回一个负值。
        void cleanup_module(void)
        {
        unregister_chrdev(test_major,"test");
        }  
      在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。
      一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。
      下面编译 :
          $ gcc -O2 -DMODULE -D__KERNEL__ -c test.c 
      得到文件test.o就是一个设备驱动程序。
      如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后
      ld  -r  file1.o  file2.o  -o  modulename。 
      驱动程序已经编译好了,现在把它安装到系统中去。
          $ insmod  –f  test.o 
    如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。要卸载的话,运行 :
        $ rmmod test 

      下一步要创建设备文件。

    mknod /dev/test c major minor 
      c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。
      用shell命令
      $ cat /proc/devices 
      就可以获得主设备号,可以把上面的命令行加入你的shell script中去。
      minor是从设备号,设置成0就可以了。
      我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。
        #include <stdio.h>
        #include <sys/types.h>
        #include <sys/stat.h>
        #include <fcntl.h>
        main()
        {
        int testdev;
        int i;
        char buf[10];
        testdev = open("/dev/test",O_RDWR);
        if ( testdev == -1 )
        {
        printf("Cann't open file \n");
        exit(0);
        }
        read(testdev,buf,10);
        for (i = 0; i < 10;i++)
        printf("%d\n",buf[i]);
        close(testdev);
        } 
      编译运行,看看是不是打印出全1 ?
      以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。请看下节,实际情况的处理。
    如何编写Linux操作系统下的设备驱动程序

    三、设备驱动程序中的一些具体问题

      1。 I/O Port。
      和硬件打交道离不开I/O Port,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。
      有两个重要的kernel函数可以保证驱动程序做到这一点。
      1)check_region(int io_port, int off_set)
      这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。
      参数1:io端口的基地址,
      参数2:io端口占用的范围。
      返回值:0 没有占用, 非0,已经被占用。
      2)request_region(int io_port, int off_set,char *devname)
      如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统登记,以防止被其他程序占用。登记后,在/proc/ioports文件中可以看到你登记的io口。
      参数1:io端口的基地址。
      参数2:io端口占用的范围。
      参数3:使用这段io地址的设备名。
      在对I/O口登记后,就可以放心地用inb(), outb()之类的函来访问了。
            在一些pci设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内存。经常性的,我们要获得一块内存的物理地址。

      2。内存操作
      在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages。 请注意,kmalloc等函数返回的是物理地址!
      注意,kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。
      内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。
      另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块程序需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。
      这可以通过牺牲一些系统内存的方法来解决。
      
      3。中断处理
      同处理I/O端口一样,要使用一个中断,必须先向系统登记。
        int request_irq(unsigned int irq ,void(*handle)(int,void *,struct pt_regs *),
        unsigned int long flags, const char *device); 
           irq: 是要申请的中断。
           handle:中断处理函数指针。
           flags:SA_INTERRUPT 请求一个快速中断,0 正常中断。
           device:设备名。

      如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。

      4。一些常见的问题。
      对硬件操作,有时时序很重要。但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你的程序进行优化,这样时序会发生错误。如果用汇编写呢,gcc同样会对汇编代码进行优化,除非用volatile关键字修饰。最保险的办法是禁止优化。这当然只能对一部分你自己编写的代码。如果对所有的代码都不优化,你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性,而这些扩展特性必须在加了优化选项之后才能体现出来。
    展开全文
  • 如何编写Linux设备驱动程序

    千次阅读 2012-11-14 17:11:14
    设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:  1. 对设备初始化...

    一、Linux device driver 的概念   

           系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:
      1. 对设备初始化和释放。
      2. 把数据从内核传送到硬件和从硬件读取数据。
      3. 读取应用程序传送给设备文件的数据和回送应用程序请求的数据。
      4. 检测和处理设备出现的错误。

      在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
      已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。
      最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。
      读/写时,它首先察看缓冲区的内容,如果缓冲区的数据
      如何编写Linux操作系统下的设备驱动程序

    二、实例剖析

        我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。

    #define  __NO_VERSION__ 
    #include  <linux/modules.h> 
    #include  <linux/version.h> 
      char kernel_version [] = UTS_RELEASE; 
    


            这一段定义了一些版本信息,虽然用处不是很大,但也必不可少。Johnsonm说所有的驱动程序的开头都要包含<linux/config.h>,一般来讲最好使用。
      由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:

    struct file_operations 
    { 
    int (*seek) (struct inode * ,struct file *, off_t ,int); 
    int (*read) (struct inode * ,struct file *, char ,int); 
    int (*write) (struct inode * ,struct file *, off_t ,int); 
    int (*readdir) (struct inode * ,struct file *, struct dirent * ,int); 
    int (*select) (struct inode * ,struct file *, int ,select_table *); 
    int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long); 
    int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *); 
    int (*open) (struct inode * ,struct file *); 
    int (*release) (struct inode * ,struct file *); 
    int (*fsync) (struct inode * ,struct file *); 
    int (*fasync) (struct inode * ,struct file *,int); 
    int (*check_media_change) (struct inode * ,struct file *); 
    int (*revalidate) (dev_t dev); 
    } 


      这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
      下面就开始写子程序。

    #include <linux/types.h> 
    #include <linux/fs.h> 
    #include <linux/mm.h> 
    #include <linux/errno.h> 
    #include <asm/segment.h> 
    unsigned int test_major = 0; 
    static int read_test(struct inode *node,struct file *file,char *buf,int count)
    { 
    int left; 
    if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT ) 
    return -EFAULT; 
    for(left = count ; left > 0 ; left--) 
    { 
    __put_user(1,buf,1); 
    buf++; 
    } 
    return count; 
    } 


            这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。

    static int write_tibet(struct inode *inode,struct file *file,const char *buf,int count) 
    { 
    return count; 
    } 
    
    static int open_tibet(struct inode *inode,struct file *file ) 
    {
    MOD_INC_USE_COUNT; 
    return 0; 
    } 
    
    static void release_tibet(struct inode *inode,struct file *file ) 
    { 
    MOD_DEC_USE_COUNT; 
    } 


          这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。

    struct file_operations test_fops = { 
    NULL, 
    read_test, 
    write_test, 
    NULL, /* test_readdir */ 
    NULL, 
    NULL, /* test_ioctl */ 
    NULL, /* test_mmap */ 
    open_test, 
    release_test, 
    NULL, /* test_fsync */ 
    NULL, /* test_fasync */ 
    /* nothing more, fill with NULLs */ 
    };  

         设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。

    int init_module(void) 
    { 
    int result; 
    result = register_chrdev(0, "test", &test_fops); 
    if (result < 0) { 
    printk(KERN_INFO "test: can't get major number\n"); 
    return result; 
    } 
    if (test_major == 0) test_major = result; /* dynamic */ 
    return 0; 
    } 


           在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。
      如果登记成功,返回设备的主设备号,不成功,返回一个负值。

    void cleanup_module(void) 
    { 
    unregister_chrdev(test_major,"test"); 
    }  


           在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。
      一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。
      下面编译 :

      $ gcc -O2 -DMODULE -D__KERNEL__ -c test.c 


      得到文件test.o就是一个设备驱动程序。
      如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后

      ld  -r  file1.o  file2.o  -o  modulename。 


         驱动程序已经编译好了,现在把它安装到系统中去。

      $ insmod  –f  test.o 


        如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。要卸载的话,运行 :

    $ rmmod test 


        下一步要创建设备文件。

      mknod /dev/test c major minor 

           c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。
      用shell命令

      $ cat /proc/devices 


           就可以获得主设备号,可以把上面的命令行加入你的shell script中去。
      minor是从设备号,设置成0就可以了。
      我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。

    #include <stdio.h> 
    #include <sys/types.h> 
    #include <sys/stat.h> 
    #include <fcntl.h> 
    main() 
    { 
    int testdev; 
    int i; 
    char buf[10]; 
    testdev = open("/dev/test",O_RDWR); 
    if ( testdev == -1 ) 
    { 
    printf("Cann't open file \n"); 
    exit(0); 
    } 
    read(testdev,buf,10); 
    for (i = 0; i < 10;i++) 
    printf("%d\n",buf[i]); 
    close(testdev); 
    } 


           编译运行,看看是不是打印出全1 ?
      以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。请看下节,实际情况的处理。


           如何编写Linux操作系统下的设备驱动程序


    三、设备驱动程序中的一些具体问题  

     

     1.  I/O Port

      和硬件打交道离不开I/O Port,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。
      有两个重要的kernel函数可以保证驱动程序做到这一点。
      1)check_region(int io_port, int off_set)
      这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。
      参数1:io端口的基地址,
      参数2:io端口占用的范围。
      返回值:0 没有占用, 非0,已经被占用。
      2)request_region(int io_port, int off_set,char *devname)
      如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统登记,以防止被其他程序占用。登记后,在/proc/ioports文件中可以看到你登记的io口。
      参数1:io端口的基地址。
      参数2:io端口占用的范围。
      参数3:使用这段io地址的设备名。
      在对I/O口登记后,就可以放心地用inb(), outb()之类的函来访问了。
    在一些pci设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内存。经常性的,我们要获得一块内存的物理地址。

    2. 内存操作

       在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages。 请注意,kmalloc等函数返回的是物理地址!
      注意,kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。
      内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。
      另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块程序需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。
      这可以通过牺牲一些系统内存的方法来解决。
      
    3. 中断处理
      同处理I/O端口一样,要使用一个中断,必须先向系统登记。

    int request_irq(unsigned int irq ,void(*handle)(int,void *,struct pt_regs *),
    unsigned int long flags, const char *device); 


    irq: 是要申请的中断。
    handle:中断处理函数指针。
    flags:SA_INTERRUPT 请求一个快速中断,0 正常中断。
    device:设备名。

      如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。

    4.一些常见的问题
      对硬件操作,有时时序很重要。但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你的程序进行优化,这样时序会发生错误。如果用汇编写呢,gcc同样会对汇编代码进行优化,除非用volatile关键字修饰。最保险的办法是禁止优化。这当然只能对一部分你自己编写的代码。如果对所有的代码都不优化,你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性,而这些扩展特性必须在加了优化选项之后才能体现出来。

     转自:http://bbs.chinaunix.net/thread-2047152-1-1.html

     

     

    展开全文
  • Linux设备驱动程序编写

    千次阅读 2009-07-10 11:16:00
    设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件, 应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能: 1 对设备初始化和释放...

    一   Linux   device   driver   的概念  
    系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,   应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:  
    1   对设备初始化和释放。  
    2   把数据从内核传送到硬件和从硬件读取数据。  
    3   读取应用程序传送给设备文件的数据和回送应用程序请求的数据。  
    4   检测和处理设备出现的错误。  
    Linux操作系统下有两类主要的设备文件类型,一种是字符设备,另一种是块设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。  
    已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。  
    最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。  
    二   实例剖析  
    我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。不过我的kernel是2。0。34,在低版本的kernel上可能会出现问题,我还没测试过。  
    #define   __NO_VERSION__
    #include  
    #include  
    char   kernel_version   []   =   UTS_RELEASE;
    这一段定义了一些版本信息,虽然用处不是很大,但也必不可少。Johnsonm说所有的驱动程序的开头都要包含,但我看倒是未必。由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如   open,read,write,close。。。。,   注意,不是fopen,   fread。,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:  
    struct   file_operations   {
    int   (*seek)   (struct   inode   *   ,struct   file   *,   off_t   ,int);
    int   (*read)   (struct   inode   *   ,struct   file   *,   char   ,int);
    int   (*write)   (struct  inode   *   ,struct   file   *,   off_t   ,int);
    int   (*readdir) (struct  inode   *  ,struct file   *,   struct  dirent  *  ,int);
    int   (*select) (struct  inode  *  ,struct   file   *,   int   ,select_table   *);
    int   (*ioctl)  (struct  inode *  ,struct file *, unsined   int   ,unsigned   long
    int   (*mmap)   (struct   inode * ,struct file *, struct   vm_area_struct   *);
    int   (*open)   (struct   inode   *   ,struct   file   *);
    int   (*release)   (struct   inode   *   ,struct   file   *);
    int   (*fsync)   (struct   inode   *   ,struct   file   *);
    int   (*fasync)   (struct   inode   *   ,struct   file   *,int);
    int   (*check_media_change)   (struct   inode   *   ,struct   file   *);
    int   (*revalidate)   (dev_t   dev);
    }
    这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。相当简单,不是吗?  
    下面就开始写子程序。  
    #include  
    #include  
    #include  
    #include  
    #include  
    unsigned   int   test_major   =   0;
    static int read_test(struct  inode  *node,struct file *file, char  *buf,int count)
    {
      int   left;

    if   (verify_area(VERIFY_WRITE,buf,count)   ==   -EFAULT   )
      return   -EFAULT;
    for(left   =   count   ;   left   >   0   ;   left--)
    {
      __put_user(1,buf,1);
      buf++;
    }
    return   count;
    }
    这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf   是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考。在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。  
    static int write_test(struct inode *inode,struct file *file,const char *buf,int count)
    {
      return   count;
    }
    static int open_test(struct inode  *inode,struct file   *file   )
    {
      MOD_INC_USE_COUNT;
      return   0;
    }  

    static void  release_test(struct  inode *inode,struct file  *file   )
    {
      MOD_DEC_USE_COUNT;
    }
    这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。  
    struct   file_operations   test_fops   =   {
    NULL,
    read_test,
    write_test,
    NULL,   /*   test_readdir   */
    NULL,
    NULL,   /*   test_ioctl   */
    NULL,   /*   test_mmap   */
    open_test,
    release_test,   NULL,   /*   test_fsync   */
    NULL,   /*   test_fasync   */
    /*   nothing   more,   fill   with   NULLs   */
    };
    设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。  
    int   init_module(void)
    {
      int   result;

    result   =   register_chrdev(0,   "test",   &test_fops);
    if   (result   <   0)   {
      printk(KERN_INFO   "test:   can't   get   major   number   ");
      return   result;
    }
    if   (test_major   ==   0)   test_major   =   result;   /* dynamic   */
      return   0;
    }
    在用insmod命令将编译好的模块调入内存时,init_module   函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。如果登记成功,返回设备的主设备号,不成功,返回一个负值。  
    void   cleanup_module(void)
    {
      unregister_chrdev(test_major,   "test");
    }
    在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。  
    一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。下面编译  
    $   gcc   -O2   -DMODULE   -D__KERNEL__   -c   test.c
    得到文件test.o就是一个设备驱动程序。如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后:  
    ld   -r   file1.o   file2.o   -o   modulename。
    驱动程序已经编译好了,现在把它安装到系统中去。  
    $   insmod   -f   test
    如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号,要卸载的话,运行  
    $   rmmod   test
    下一步要创建设备文件。  
    mknod   /dev/test   c   major   minor
    c   是指字符设备,major是主设备号,就是在/proc/devices里看到的。用shell命令  
    $   cat   /proc/devices   ¦   awk   "/$2=="test"   {print   /$1}"
    就可以获得主设备号,可以把上面的命令行加入你的shell   script中去。minor是从设备号,设置成0就可以了。我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。  
    #include  
    #include  
    #include  
    #include  
    main()
    {
    int   testdev;
    int   i;
    char   buf[10];

    testdev   =   open("/dev/test",O_RDWR);
    if   (   testdev   ==   -1   )
    {
      printf("Cann't   open   file   ");
      exit(0);
    }
    read(testdev,buf,10);
    for   (i   =   0;   i   <   10;i++)
      printf("%d   ",buf[i]);
      close(testdev);
    }
    编译运行,看看是不是打印出全1   ?  
    以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O   port等问题。这些才是真正的难点。请看下节,实际情况的处理。
    三   设备驱动程序中的一些具体问题  
    1   I/O   Port  
    和硬件打交道离不开I/O   Port,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可以对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。有两个重要的kernel函数可以保证驱动程序做到这一点。  
    1)check_region(int   io_port,   int   off_set)  
    这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。  
    参数1:io端口的基地址,  
    参数2:io端口占用的范围。  
    返回值:0   没有占用,   非0,已经被占用。  
    2)request_region(int   io_port,   int   off_set,char   *devname)  
    如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统登记,以防止被其他程序占用。登记后,在/proc/ioports文件中可以看到你登记的io口。  
    参数1:io端口的基地址。  
    参数2:io端口占用的范围。  
    参数3:使用这段io地址的设备名。  
    在对I/O口登记后,就可以放心地用inb(),   outb()之类的函来访问了。在一些pci设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内存。经常性的,我们要获得一块内存的物理地址。在dos环境下,(之所以不说是dos操作系统是因为我认为DOS根本就不是一个操作系统,它实在是太简单,太不安全了)只要用段:偏移就可以了。在window95中,95ddk提供了一个vmm   调用   _MapLinearToPhys,用以把线性地址转化为物理地址。但在Linux中是怎样做的呢?  
    2   内存操作  
    在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages。   请注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!关于kmalloc返回的是物理地址这一点本人有点不太明白:既然从线性地址到物理地址的转换是由386cpu硬件完成的,那样汇编指令的操作数应该是线性地址,驱动程序同样也不能直接使用物理地址而是线性地址。但是事实上kmalloc返回的确实是物理地址,而且也可以直接通过它访问实际的RAM,我想这样可以由两种解释,一种是在核心态禁止分页,但是这好像不太现实;另一种是linux的页目录和页表项设计得正好使得物理地址等同于线性地址。我的想法不知对不对,还请高手指教。  
    言归正传,要注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。kmalloc用法参见khg。  
    内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。这可以通过牺牲一些系统内存的方法来解决。具体做法是:比如说你的机器由32M的内存,在lilo。conf的启动参数中加上mem=30M,这样linux就认为你的机器只有30M的内存,剩下的2M内存在vremap之后就可以为DMA所用了。  
    请记住,用vremap映射后的内存,不用时应用unremap释放,否则会浪费页表。  
    3   中断处理  
    同处理I/O端口一样,要使用一个中断,必须先向系统登记。  
    int   request_irq(unsigned   int   irq   ,
    void(*handle)(int,void   *,struct   pt_regs   *),
    unsigned   int   long   flags,
    const   char   *device);
    irq:   是要申请的中断。  
    handle:中断处理函数指针。  
    flags:SA_INTERRUPT   请求一个快速中断,0   正常中断。  
    device:设备名。  
    如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。  
    4一些常见的问题  
    对硬件操作,有时时序很重要。但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你的程序进行优化,这样时序就错掉了。如果用汇编写呢,gcc同样会对汇编代码进行优化,除非你用volatile关键字修饰。最保险的办法是禁止优化。这当然只能对一部分你自己编写的代码。如果对所有的代码  
    都不优化,你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性,而这些扩展特性必须在加了优化选项之后才能体现出来。  
    关于kernel的调试工具,还没有发现有合适的。一直都在printk打印调试信息,倒也还凑合。关于设备驱动程序还有很多内容,如等待/唤醒机制,块设备的编写等。

    本文来自ChinaUnix博客,如果查看原文请点: http://blog.chinaunix.net/u/26185/showart_475608.html
     ----------------------------------------------
    嵌入式精品课程一览:
    最新嵌入式就业实训班请参考:http://www.top-e.org/page/pxjy/index.php
    嵌入式Linux暑期实训班:http://www.top-e.org/page/sqb/index.php
    嵌入式Linux驱动、内核四天班:http://www.top-e.org/page/scb/index.php
    展开全文
  • linux内核编译以及字符设备驱动程序编写 关键词: 内核编译; globalmem字符设备驱动编写; linux; linux设备驱动编写; 字符设备驱动程序主要内容 虚拟内存设备globalmem驱动实现,编写程序,然后将生成的...
  • 学习一个新东西最好的方式就是去实践它。在实践的过程中会不断遇到问题、...在学习驱动程序的时候我们也可以写一个类似的小程序,它的源码如下: //file name module_ts.c #include #include static int __init m
  •  从一个角度看,设备驱动程序的作用在于提供机制,而不是策略。在编写驱动程序时,程序员应该特别注意下面这个基本概念:编写访问硬件的内核代码时,不要给用户强加任何特定策略。因为不同的用户有不同的需求,驱动...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 67,943
精华内容 27,177
关键字:

编写一个linux的设备驱动程序

linux 订阅