嵌入式Linux驱动开发(一)——字符设备驱动框架入门

提到了关于Linux的设备驱动,那么在Linux中I/O设备可以分为两类:块设备和字符设备。这两种设备并没有什么硬件上的区别,主要是基于不同的功能进行了分类,而他们之间的区别也主要是在是否能够随机访问并操作硬件上的数据。

字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,调制解调器是典型的字符设备。
块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512Byte)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
两种设备本身并没用严格的区分,主要是字符设备和块设备驱动程序提供的访问接口(file I/O API)是不一样的。本文主要就数据接口、访问接口和设备注册方法对两种设备进行比较。
那么,首先,认识一下字符设备的驱动框架。

对于上层的应用开发人员来说,没有必要了解具体的硬件是如何组织在一起并工作的。比如,在Linux中,一切设备皆文件,那么应用程序开发者,如果需要在屏幕上打印一串文字,虽然表面看起来只是使用printf函数就实现了,其实,他也是使用了int fprintf(FILE fp, const char format[, argument,…])封装后的结果,而实际上,fprintf函数操作的还是一个FILE,这个FILE对应的就是标准输出文件,也就是我们的屏幕了。

那么最简单的字符设备驱动程序的框架是如何呢?
嵌入式Linux驱动开发(一)——字符设备驱动框架入门_第1张图片

                                          应用程序和底层调用的结构

正如上图所显示的那样,用户空间的应用开发者,只需要通过C库来和内核空间打交道;而内核空间通过系统调用和VFS(virtual file system),来调用各类硬件设备的驱动。如果,有过单片机的经验,那么一定知道,操作硬件简单来说就是操作对应地址的寄存器中的内容。而硬件驱动实际就是和这些寄存器打交道的。通过操作对应硬件的寄存器来直接的控制硬件设备。

那么,对于上面这幅图可以看出,驱动程序实际也是内核的一部分,当然可以把代码直接放到内核中一起编译出来。但是对于很多开发板来说,内核来说早已经编译完成运行在开发板上,那么是不是必须要重新编译并烧写整个内核呢?
换到我们使用pc来说,显然不是这样,如果我们购买了一个键盘,为了键盘还需要重新安装对应的操作系统,那么未免也太不方便,并且我们的使用经验也并非如此。
而在之前谈到的内核编译过程中,可以将一些模块编译为module的方式编译,在运行时加载该模块即可,而不用每次都需要完整的对内核进行编译。
因此,对于驱动程序的开发来说,这一点就显得很重要,也是我们日常工作最常用的一种方式。

那么我们先回顾一下,在应用层我们一般是如何来操作一个设备文件的?我们通常会使用一些类似于read、open、write等函数。(可以参见我之前写的文章:Linux文件编程)。那么在使用这些函数的时候,会包含一些头文件,例如:sys/types.h、sys/stat.h以及fcntl.h等这些头文件,实际他们就是C库的部分,用户程序这时候只需要关心的是C库到底如何使用,而C库背后实际完成的是调用一些系统调用,类似于sys_open、sys_read等函数来对内核空间进行调用的。

在这里毕竟不是为了分析框架的具体实现原理,以后有机会慢慢展开,在此主要为了讨论,如何快速使用这些框架来写出字符设备的驱动程序。

其实编写字符驱动的步骤并不复杂,我们首先将框架建立起来,建立框架的大致我认为可以分为以下两部(其中的细节问题后续展开):

编写驱动的入口和出口函数,此函数会在驱动模块加载和卸载时调用
编写具体的read、write、open等函数,在用户程序使用对应的函数时,该函数可以被调用。(非必须)
我们先看看一个简单的驱动程序的框架:

#include      //定义了module_init
#include    //最基本的头文件,其中定义了MODULE_LICENSE这一类宏
#include        // file_operations结构体定义在该头文件中

static const char* dev_name = "first_driver";  //  定义设备名
static unsigned int major = 55;               //定义设备号

//定义了open函数
static int first_drv_open (struct inode *inode, struct file *file)
{
        printk("open\n");
        return 0;
}

//定义了write函数
static ssize_t first_drv_write (struct file *file, const char __user *buf, size_t size, loff_t * ppos)
{
        printk("write\n");
        return 0;
}

//在file_operations中注册open和write函数
static struct file_operations first_drv_fo =
{
        .owner  =  THIS_MODULE,

        //将对应的函数关联在file_operations的结构体中
        .open   =  first_drv_open,      
        .write  =  first_drv_write,
};

//init驱动的入口函数
static int first_drv_init(void)
{      
        //注册设备,实际是将file_operations结构体放到内核的制定数组中,以便管理
        //在register_chrdev中制定major作为主设备号
        register_chrdev(major, dev_name , &first_drv_fo);
        printk("init\n");
        if(dev_id < 0) 
            printk("error\n");
        return 0;
}

//驱动的出口函数
static void first_drv_exit(void)
{
        printk("exit\n");
        unregister_chrdev(major, dev_name);  //卸载设备,实际是将file_operations结构体从内核维护的相关数组中以主设备号作为索引删除
}

//内核将通过这个宏,来直到这个驱动的入口和出口函数
module_init(first_drv_init);  
module_exit(first_drv_exit);

MODULE_AUTHOR("Ethan Lee <[email protected]>");
MODULE_LICENSE("GPL");  //指定协议

以上的代码基本是关于字符型设备驱动的框架结构了。可以看到以上的代码其实就是一个简单的驱动程序框架了,其实如果没有first_drv_open和first_dev_write两个函数也是可以的,在硬件上可以正确的安装该驱动,在安装驱动的时候会调用注册在module_init中的函数,在卸载程序时会调用module_exit中所注册的函数。但是file_operations结构体依然还是需要定义的,但实际的驱动程序需要操作实际的硬件,一般都会有open、read、write这类函数。但在此仅仅是为了说明驱动的最小框架而已。

那么驱动程序写完了,我们来使用测试程序调用一下,以下是测试程序

#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
    int fd;      //声明设备描述符
    int val = 1;  //随便定义变量传入到
    fd = open("/dev/xxx",  O_RDWR);  //根据设备描述符打开设备
    if(fd < 0)          //打开失败
            printf("can't open\n");  
    write(fd, &val, 4);  //根据文件描述符调用write


    return 0;
}

Makefile

#驱动程序实际属于内核的一部分,那么在编译的时候就需要使用已经编译好的内核,来编译驱动程序了,这点尤为重要。
KERN_DIR=/code/LinuxDev/Lab/KernelOfLinux/linux-2.6.22.6        #内核目录

all:
        make -C $(KERN_DIR) M=`pwd` modules     #M=`pwd`表示,生成的目标放在pwd命令的目录下                                                             # -C代表使用目录中的Makefile来进行编译

clean:
        make -C $(KERN_DIR) M=`pwd` modules clean
        rm -f modules.order

obj-m += first.o #加载到module的编译链中,内核会编译生成出来ko文件,作为一个模块

嵌入式Linux驱动开发(一)——字符设备驱动框架入门_第2张图片
嵌入式Linux驱动开发(一)——字符设备驱动框架入门_第3张图片
完成了测试程序和驱动程序的编译,那么接下来就是将写好的驱动程序安装在开发板上
在开发板上使用lsmod命令查看已安装的模块

PS:我的开发板使用的是NFS系统,这个NFS系统是linux服务器所提供的,所以在Linux服务器上编译完成后就直接切换在了开发板上操作,如果你的开发板使用的不是NFS系统,那么,还需要把编译出来的测试程序的可执行文件和.ko模块文件拷贝到开发板的文件系统中,才能执行后续的操作。

作者:故事狗
链接:https://www.jianshu.com/p/716ed9cdb8f3
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(嵌入式Linux驱动开发(一)——字符设备驱动框架入门)