3.5【Linux】字符设备驱动程序

本文深入解析字符设备驱动的实现原理,从系统调用到硬件操作,包括设备注册、文件操作结构体、硬件映射及控制,以及中断和poll机制的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

应用层的open,read,write实际上是执行swi val这么一条指令,这条指令类似于一条中断,在C库下有一个系统调用的接口,根据val的值调用sys_open,sys_read,sys_write等,当我们在应用层open(“dev/led”,ORDWR)时,sys_open会根据打开的不同文件找到不同的驱动程序。

       那么如何写出一个字符设备驱动程序呢?首先要实现例如led_open,led_write等设备操作函数,将这些函数挂在file_operation结构体中,然后在驱动的入口函数和出口函数注册注销这个结构体。

       那么app是如何调用我们定的file_operation结构体,实现例如点亮LED这样一个功能呢?

       根据主设备号。在我们注册这个结构体的时候要指定或者由系统指定一个主设备号major。根据这么主设备号进行查找。那么多的major是如何管理的?虚拟文件系统VFS,里面存在着已经注册的major。所以,我们可以在注册函数里major参数传入0,系统将会自己去查询分配主设备号返回。

       那么open(“dev/led”,ORDWR)里“dev/led”是如何而来的?两种方式:一种是手动创建,一种是自动创建。

       自动创建需要系统信息创建设备节点,用代码实现即在注册结构体之后class_create创建类,然后在用这个类创建一个设备class_device_create,这样我们在insmod时驱动就可以用这个的设备name在应用层open了。

       那么我们如何操作硬件。在单片机中程序可以直接使用物理地址进行操作,但在Linux里,需要ioremap(mac,size)映射成虚拟地址才能进行操作

       怎么来总结字符设备呢?这个过程像极了校园爱情。

        第一:上课期间不管要表示爱意还是想下课操场见都只能通过传纸条 => open,read,write本质是执行swi val指令

        第二:纸条上要写收件人的名字,不然可能就没了,而这个名字是人家对方的父母取的 => open(“dev/led”,ORDWR),而“dev/led”是在驱动创建的时候由class_device_create设置的

        第三:在班级里唯有座号不会重复,每个人的座号都贴在黑板上方便老师叫号 => 注册结构体的时候要分配一个主设备号major,major在VFS虚拟文件系统里放着

 

二、驱动框架搭建

 

①写出 led_open、led_read

②怎么告诉内核

     --->定义一个file_operations结构,填充它

Linux下的file_operations结构体(linux-2.6.22.6\include\linux)

struct file_operations {

    struct module *owner;

    loff_t (*llseek) (struct file *, loff_t, int);

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

    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

    int (*readdir) (struct file *, void *, filldir_t);

      省略

};

---------------------------------------------------------------------------------------------------------------------------------------------------

填充完成的结构体

static struct file_operations sencod_drv_fops = {

    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */

    .open    =  sixth_drv_open,     

    .read     =    sixth_drv_read,       

    .release =  sixth_drv_close,

    .poll    =  sixth_drv_poll,

    .fasync     =  sixth_drv_fasync,

};

        --->把这个结构告诉内核

            --->使用register_chrdev注册:register_chrdev(主设备号, 名字,上面填充完成的结构体);  注册驱动程序

register_chrdev(0, "sixth_drv", &sencod_drv_fops);

                --->谁来调用register_chrdev,即驱动的入口

static int sixth_drv_init(void)

{

    register_chrdev(0, "sixth_drv", &sencod_drv_fops);

    return 0;

}

                    --->入口函数如何被识别

module_init(sixth_drv_init);//module_init是一个结构体,结构体指向定义的入口函数,内核在启动的时候会调用这个结构体。

                        --->应用程序如何找到register_chrdev并对应到相应的file_operations:依据设备号和主设备名称

            --->谁来注销register_chrdev,即驱动的出口

static void sixth_drv_exit(void)

{

    unregister_chrdev(major, "sixth_drv");

}

                --->出口函数如何被注销

module_exit(sixth_drv_exit);

 

        --->实现sencod_drv_fops结构体中定义的方法类

static int sixth_drv_open(struct inode *inode, struct file *file)

{

    return 0;

}

ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

    return 1;

}

    .

    .

    .

    省略

 

使用Makefile

KERN_DIR = /work/system/linux-2.6.22.6    //内核的目录,程序依赖于linux-2.6.22.6内核开发,内核需要编译好



all:

    make -C $(KERN_DIR) M=`pwd` modules   // make -C $(KERN_DIR):使用make -C $转到KERN_DIR里面进行编译;M=`pwd`当前目录是什么;modules:目标



clean:

    make -C $(KERN_DIR) M=`pwd` modules clean

    rm -rf modules.order



obj-m    += buttons.o

 

 

 

测试代码

int main(int argc,char *argv){

    int fd;

    int val = 1;

    fd  =  open("/dev/xxx",O_PDWR);

    if(fd<0){

        printf("can't open!\n");

        return -1;

    }

    write(fd,&val,4);

    return 0;

}

 register_chrdev(0, "sixth_drv", &sencod_drv_fops); 中如果第一个参数填写0,则系统自动分配设备号

 

一、驱动:1、自动分配主设备号

                 2、手工指定


二、应用   open("/dev/xxx",O_PDWR);

    "/dev/xxx"怎么来的

    a、手工创建 mknod  /dev/xxx 

    b、自动创建  udev,mdev  ----->根据系统信息创建设备节点


                ----->如何创建设备节点

static struct class *sixthdrv_class;

static struct class_device    *sixthdrv_class_dev;

static int sixth_drv_init(void)

{

    major = register_chrdev(0, "sixth_drv", &sencod_drv_fops);



    sixthdrv_class = class_create(THIS_MODULE, "sixth_drv");//先创建类



    /* 为了让mdev根据这些信息来创建设备节点 */

    sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */



    return 0;

}

                ---->删除设备节点

static void sixth_drv_exit(void)

{

    unregister_chrdev(major, "sixth_drv");

    class_device_unregister(sixthdrv_class_dev);

    class_destroy(sixthdrv_class);

    return 0;

}

 

二、在框架内写LED驱动

    --->配置硬件

       --->映射引脚:如何映射?-->定义全局变量,在入口函数映射

volatile unsigned long *gpfcon;

volatile unsigned long *gpfdat;

static int sixth_drv_init(void)

{

    ....省略上面的代码

    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);//ioremap(地址, 长度);

    gpfdat = gpfcon + 1;//+1等于+4,以unsigned long为单位的



    return 0;

}

       

     --->取消映射

static void sixth_drv_exit(void)

{

        ...省略上面的代码

    iounmap(gpfcon);

    return 0;

}

    --->在sixth_drv_open中配置引脚

static int sixth_drv_open(struct inode *inode, struct file *file){

    /*配置GPF4,5,6为输出*/

    *gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));

    *gpfcon |= ((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));

    return 0;

}

static ssize_t sixth_drv_write(struct file *file, char __user *buf, size_tcount, loff_t *ppos){

        int val;

    copy_from_user(&val,buf,count);


        if(val==1){

            //点灯

            *gpfdat |= (1<<4)|(1<<5)|(1<<6);

        }else{

            //灭灯

      *gpfdat &= ~(1<<4)|(1<<5)|(1<<6);

        }

    return 0;

}

 

三、按键查询的方式控制LED

    --->框架搭建

        --->实现file_operations结构体,实现结构体需要用到的方法

        --->在入口函数注册file_operations结构体,和注销入口函数

static int sixth_drv_init(void)

{

    register_chrdev(0, "sixth_drv", &sencod_drv_fops);

    return 0;

}

static void sixth_drv_exit(void)

{

    unregister_chrdev(major, "sixth_drv");

}

     --->出口函数卸载

module_exit(sixth_drv_exit);

     --->给内核提供更多信息:udev机制,可以自动创建设备节点>>>>>创建一个class,在class上创建一个设备节点

static struct class *sixthdrv_class;

static struct class_device    *sixthdrv_class_dev;

static int sixth_drv_init(void)

{

    major = register_chrdev(0, "sixth_drv", &sencod_drv_fops);

    sixthdrv_class = class_create(THIS_MODULE, "sixth_drv");//先创建类


    /* 为了让mdev根据这些信息来创建设备节点 */

    sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */

    return 0;

}

 

    --->卸载设备节点

static void sixth_drv_exit(void)

{

    unregister_chrdev(major, "sixth_drv");

    class_device_unregister(sixthdrv_class_dev);

    class_destroy(sixthdrv_class);

    return 0;

}

 

   --->硬件配置:.open配置引脚,.read 返回引脚状态

 

        --->引脚映射


 

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;


static int sixth_drv_init(void)

{

       .....省略上面的file_operations结构体注册步骤.....

    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);

    gpfdat = gpfcon + 1;



    gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);

    gpgdat = gpgcon + 1;


    return 0;

}

      --->在出口函数注销掉这些映射的引脚

static void sixth_drv_exit(void)

{

    ....省略....

    iounmap(gpfcon);

    iounmap(gpgcon);

    return 0;

}

 

      ---->在open函数中配置引脚

static int sixth_drv_open(struct inode *inode, struct file *file)

{
         /*配置GPF0,2为输出*/
    *gpfcon &= ~((0x3<<(0*2)) | (0x3<<2*2)));

    /*配置GPF3,11为输出*/
    *gpfcon &= ~((0x3<<(3*2)) | (0x3<<(11*2)));
   
    return 0;
}

 

    --->在read函数中读取引脚电平

static int sixth_drv_read(struct file *file, char __user *buf, size_tcount, loff_t *ppos){

    unsigned char key_vals[4];

    int regval;

    if(size!=sizeof(key_vals)){

        return -EINVAL;

    }
    
    /*读GPF0,2*/

    regval = *gpfdat;

    key_vals[0] = (regval & (1<<0))?1:0;
    key_vals[1] = (regval & (1<<2))?1:0;


    /*读GPF3,11*/
    regval = *gpgdat;

    key_vals[2] = (regval&(1<<3))?1:0;
    key_vals[3] = (regval&(1<<4))?1:0;


    copy_to_user(buf,key_vals,sizeof(key_vals));

    return sizeofn(key_vals);

}

 

    --->实现测试程序

int main(int argc,char *argv){

    int fd;

    unsigned char key_vals[4];

    int cnt = 0;

    fd  =  open("/dev/xxx",O_PDWR);

    while(1){

        read(fd,key_vals,sizeof(key_vals));

        if(!key_vals[0]||!key_vals[1]||!key_vals[2]||!key_vals[3]){

            printf("%04d key pressed :%d %d %d %d\n",cnt++,key_vals[0],key_vals[1],key_vals[2],key_vals[3]);

        }

    }

    return 0;

}

 

 

 

四、中断方式控制LED

    --->框架

    --->内核分析

        --->1、分辨是哪一个中断---2、调用处理函数---3、清中断:都是由asm_do_IRQ函数实现的:在\arch\arm\kernel中的irq.c实现

asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

{

    struct pt_regs *old_regs = set_irq_regs(regs);

    struct irq_desc *desc = irq_desc + irq;

    if (irq >= NR_IRQS)

        desc = &bad_irq_desc;

    irq_enter();

    desc_handle_irq(irq, desc);//三项功能都是在这里实现的

    irq_finish(irq);

    irq_exit();

    set_irq_regs(old_regs);

}

 

//desc_handle_irq内定义的方法,文件在\include\asm-arm\mach的irq.h中

static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)

{

    desc->handle_irq(irq, desc);

}

 

    --->通过中断配置引脚,request_irq能够自动配置引脚为输入输出。

static int sixth_drv_open(struct inode *inode, struct file *file)

{

    /* 配置GPF0,2为输入引脚 */

    /* 配置GPG3,11为输入引脚 */

    request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", 1);

                //中断号      处理函数        触发方式     取名 设备号   

    request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", 1);

    request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", 1);

    request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", 1);    

    return 0;

}

 

    --->释放中断

int sixth_drv_close(struct inode *inode, struct file *file)

{

    //atomic_inc(&canopen);

    free_irq(IRQ_EINT0,1);

    free_irq(IRQ_EINT2,1);

    free_irq(IRQ_EINT11,1);

    free_irq(IRQ_EINT19,1);

    up(&button_lock);

    return 0;

}

 

        --->定义buttons_irq处理函数

static irqreturn_t buttons_irq(int irq, void *dev_id)

{

    printk("irq = %d\n");

    return IRQ_HANDLED;

}

 

 

五、poll机制(查询机制)——可以休眠

 

        在file_operations结构体中加.poll

static struct file_operations sencod_drv_fops = {

    .....省略.....

    .poll    =  sixth_drv_poll,

    .....省略.....

};

 

       在.read中会调用sys_read,那么poll也会调用sys_read,接下来分析一下sys_read

            

            所有的系统调用,基于都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。

 

应用层:app  ------>.poll

内核:kernel  ------> sys_poll:(fs/select.c)

            ----->do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, s64 *timeout)

                                        ----->poll_initwait()

                                             -----> init_poll_funcptr(poll_table *pt, poll_queue_proc qproc):pt->qproc = qproc;

                                         ------>do_poll(nfds, head, &table, timeout);

                                            for(;;){

 

                                            }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林零七

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值