Linux驱动——字符设备驱动基本框架和相关内容详细说明

在Linux系统中通常把驱动分成三类:
字符设备驱动、块设备驱动、网络设备驱动,关于三种设备设备驱动的相关定义如下:
字符设备:只能一个字节一个字节的读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序进行。字符设备是面向流的设备,常见的字符设备如鼠标、键盘、串口、控制台、LED等。

块设备:是指可以从设备的任意位置读取一定长度的数据设备。块设备如硬盘、磁盘、U盘和SD卡等存储设备。

网络设备:网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。

本篇博文主要阐述字符设备驱动模型,字符设备驱动结构:
1.确定主设备号;
2.构造file_operation结构体。
3.注册设备register_chrdev
4.入口函数
5.出口函数

首先字符设备驱动模型的总体代码框架如下:

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>


static int major = 0;
static int kernel_buf = 77;
static struct class *hello_class;


static int hello_open (struct inode *node, struct file *file)
{
        printk("this is open\n");
        return 0;
}

static ssize_t hello_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
        printk("this is write\n");
        copy_from_user(&kernel_buf,buf,4);
        printk("from user %d\n",kernel_buf);
        return size;
}

static ssize_t hello_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
        printk("this is read\n");
        copy_to_user(buf,&kernel_buf,4);
        printk("to user %d",kernel_buf);

        return size;
}

static struct file_operations hello_op = {
        .owner = THIS_MODULE,
        .open = hello_open,
        .read = hello_read,
        .write = hello_write,
};

static int hello_init(void)
{
        printk("this is init\n");
        major = register_chrdev(0,"hello1",&hello_op);
        hello_class = class_create(THIS_MODULE,"hello_class");
        device_create(hello_class,NULL,MKDEV(major,0),NULL,"hello1");

        return 0;
}


static void hello_exit(void)
{
        printk("this is exit\n");
        device_destroy(hello_class,MKDEV(major,0));
        class_destroy(hello_class);
        unregister_chrdev(major,"hello");

}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

接下来对字符设备驱动框架进行详细描述:
对于字符设备驱动来说首先需要找到其入口函数,在Linux中将module_init()定义为宏,用于设置某个函数作为真正入口函数
对于上述代码框架而言hello_init是真正的驱动入口函数。也可以不使用module_init去定义驱动入口函数,修改static void hello_init(void)函数改为static void init_module(void).既然有入口函数就有出口函数,与定义驱动入口函数一样定义驱动出口函数使用的是module_exit()这个宏,也可以直接将驱动真正出口函数改为static void exit_module(void)。MODULE_LICENSE(“GPL”)作为协议通常默认添加。
驱动入口函数如下:

static int hello_init(void)
{
        printk("this is init\n");
        major = register_chrdev(0,"hello1",&hello_op);
        hello_class = class_create(THIS_MODULE,"hello_class");
        device_create(hello_class,NULL,MKDEV(major,0),NULL,"hello1");

        return 0;
}

在驱动入口函数中主设备号可以利用register_chrdrvde函数的返回值进行赋值,register_chrdrv是字符驱动注册函数,第一个参数为主设备号,使用0就可以让其自动生成主设备号,第二个参数为驱动名,第三个函数为结构体file_operation。
file_operation结构体
file_operation定义如下:
在这里插入图片描述
在file_operation中定义了应用层有可能使用到的所有的系统调用函数,在应用层通过调用系统调用函数会触发一个异常(swi异常),会进入到驱动层。通常在驱动中定义open 、read和write是三个系统调用,以open为例描述应用层到底层再到硬件过程,在应用层使用open函数以相应的打开方式去打开对应的底层驱动文件,这是就触发了swi异常进入到内核态,在内核态调用sys_open函数,通过判断是普通文件还是设备文件,如果是普通文件则可以使用可读、可写或者可读可写的方式打开。如果是设备文件就根据主设备号找到对应的驱动调动驱动文件中file_operation中定义的结构体指针指向的open函数,如果使用到了硬件通常是在open函数中对硬件寄存器进行初始化而不是在入口函数中进行,因为如果放在入口函数中,只要insmod装载驱动就会对硬件进行操作,这样就会导致应用层程序还没运行寄存器就已经被初始化。对硬件寄存器的操作其实是对物理地址使用ioremap映射成虚拟地址,在Linux系统中是对虚拟地址进行操作的。通常驱动程序中file_operation结构体如下:

static struct file_operations hello_op = {
        .owner = THIS_MODULE,
        .open = hello_open,
        .read = hello_read,
        .write = hello_write,
};

在结构体外对这些结构体中的函数指针进行初始化:

static int hello_open (struct inode *node, struct file *file)
{
        printk("this is open\n");
        return 0;
}

static ssize_t hello_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
        printk("this is write\n");
        copy_from_user(&kernel_buf,buf,4);
        printk("from user %d\n",kernel_buf);
        return size;
}

static ssize_t hello_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
        printk("this is read\n");
        copy_to_user(buf,&kernel_buf,4);
        printk("to user %d",kernel_buf);

        return size;
}

调用完register_chrdev之后系统就完成对驱动程序的注册,不需要class_create和device_create就可以完成驱动注册,对程序进行编译并insmod装载驱动之后可以在/proc/dev中查看驱动程序和主设备号。
在这里插入图片描述
前面的数字代表主设备号,此时系统中已经注册了该驱动。

创建类和设备节点:
创建设备节点可以通过手动设置和在驱动程序中自动设置;
手动设置:当程序编译完成之后装载驱动通过cat /proc/devices查看驱动的主设备号,根据主设备号使用mknod创建设备节点
在这里插入图片描述
自动创建:class_create和device_create
调用class_create去创建该设备的类,可以进入到/sys/class中查看是否生成对应的类。
在这里插入图片描述
之后再调用device_create创建对应的设备节点,查看设备节点有多种方式:
1.同样通过/sys/dev/char查看到主设备号和次设备号,以此判断是否创建成功。
在这里插入图片描述
2.通过/dev/char查看设备名
在这里插入图片描述
驱动出口函数
完成了驱动入口函数完成之后,需要对出口函数进行初始化:
驱动出口函数的操作顺序顺序和入口操作顺序相反,首先是销毁设备节点,然后再销毁类,最后卸载驱动程序。

static void hello_exit(void)
{
        printk("this is exit\n");
        device_destroy(hello_class,MKDEV(major,0));
        class_destroy(hello_class);
        unregister_chrdev(major,"hello");

}

如果要实现应用层和驱动层的信息交互,需要借用API:copy_from_user和copy_to_user去获取和传递信息。

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

用法可见下面带有硬件的驱动程序。

在操纵硬件的驱动程序中,和传统的单片机配置寄存器不同点在于,在入口函数中需要对寄存器的物理地址进行映射,使用ioremap将寄存器的物理地址映射为虚拟地址,因为Linux系统程序运行是在虚拟地址上运行,所以需要将物理地址映射成虚拟地址。另外对于寄存器的初始化通常在file_operation中的open中进行。在write和read中根据情况使用copy_from_user和copy_to_user,获得相关信息之后配置寄存器实现硬件的相关操作。

带有硬件操作的驱动程序:

#include <linux/fs.h>            //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>        //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linuxpes.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件


static struct class *pin4_class;
static struct device *pin4_class_dev;

static dev_t devno;                //设备号
static int major =231;                     //主设备号
static int minor =0;                       //次设备号
static char *module_name="pin4";   //模块名

volatile int *GPFSEL0 = NULL;
volatile int *GPSET0 = NULL;
volatile int *GPCLR0 = NULL;


//driver_open函数
static int driver_open(struct inode *inode,struct file *file)
{
    printk("driver_open\n");  //内核的打印函数和printf类似

    *GPFSEL0 &= ~(0x6 << 12);
    *GPFSEL0 |= (0x1 << 12);

    return 0;
}

//driver_write函数
static ssize_t driver_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    int userCmd;

    printk("driver_write\n");

    copy_from_user(&userCmd,buf,count);

    printk("get value\n");

    if(userCmd == 0){
         printk("set 0\n");
         *GPSET0 |= 0x1<<4;
    }else if(userCmd == 1){
          printk("set 1\n");
          *GPCLR0|= 0x1<<4;
    }else{
      printk("undo\n");
    }


    return 0;
}

static struct file_operations driver_fops = {

    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};

int __init driver_init(void)
{

    int ret;
    devno = MKDEV(major,minor);  //创建设备号
    ret   = register_chrdev(major, module_name,&driver_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

    driver_class=class_create(THIS_MODULE,"driver");
    driver_class_dev =device_create(driver_class,NULL,devno,NULL,module_name);  //创建设备文件

    GPFSEL0 = (volatile int *)0x3f200000;
    GPSET0  = (volatile int *)0x3f20001C;
    GPCLR0  = (volatile int *)0x3f200028;

    return 0;
}

void __exit driver_exit(void)
{
     iounmap(GPFSEL0);
     iounmap(GPSET0);
     iounmap(GPCLR0);

    device_destroy(driver_class,devno);
    class_destroy(driver_class);
    unregister_chrdev(major, module_name);  //卸载驱动

}

module_init(driver_init);  //入口
module_exit(driver_exit);
MODULE_LICENSE("GPL");

注意在应用层的程序调试方式可以使用printf打印相关信息,但是在驱动程序中是使用printk打印信息,并且命令窗口中不会直接显示驱动程序中printk打印的相关信息,需要再命令窗口使用dmesg来查看驱动程序的prink打印的相关信息。

learned from:韦东山

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值