第一个驱动之字符设备驱动(一)

时间:2023-01-04 11:36:01

1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

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

  每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

主设备号和次设备号(二者一起为设备号):
  一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

驱动程序原理图:

第一个驱动之字符设备驱动(一)

那么对于刚接触驱动的我们来说如何快速编写一个驱动程序呢?

最好也是最快的方法是参考内核源代码中的demo。例如现在,我想编写我们的第一个字符驱动程序,那么我们可以看看别人是怎么实现的,在内核driver目录下找到led的驱动程序,参考别人是如何实现。还有就是厂家的参考demo。这是我们最快的学习方式。和STM32学习固件库函数一样的道理。

先写出两个函数模型,打开(open)和写(write)函数:

static int first_drv_open(struct inode *inode, struct file *file)
{
    
    return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    return 0;
}

然后是要告诉内核有这两个函数,怎样告诉内核呢?通过定义下面这样结构:

第一个驱动之字符设备驱动(一)

/*
 * NOTE:
 * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
 * can be called without the big kernel lock held in all filesystems.
 */
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);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*dir_notify)(struct file *filp, unsigned long arg);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
}

然后通过一个函数告诉内核:

第一个驱动之字符设备驱动(一)

 

 那么谁来调用上面这个函数呢?调用上面这个函数的函数就叫做驱动入口函数(这里是first_drv_init):

第一个驱动之字符设备驱动(一)

 

入口函数需要区分是哪个驱动,所以需要修饰一下,怎么修饰呢?就是调用一个函数:

 完整的myled.c函数如下:

第一个驱动之字符设备驱动(一)第一个驱动之字符设备驱动(一)
#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/init.h>

#include <linux/delay.h>

#include <asm/uaccess.h>

#include <asm/irq.h>

#include <asm/io.h>

#include <asm/arch/regs-gpio.h>

#include <asm/hardware.h>



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

{

    printk("first_drv_open...\r\n");

    return 0;

}



static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

{

    printk("first_drv_write...\r\n");

    return 0;

}



/* 这个结构是字符设备驱动程序的核心

 * 当应用程序操作设备文件时所调用的open、read、write等函数,

 * 最终会调用这个结构中指定的对应函数

 */

static struct file_operations first_drv_fops = {

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

    .open   =   first_drv_open,            

    .write    =    first_drv_write,       

};

int fisrt_drv_init(void)

{

    register_chrdev(111, "first_drv", &first_drv_fops);

    return 0;

}



void fisrt_drv_exit(void)

{

    unregister_chrdev(111, "first_drv");

}



module_init(fisrt_drv_init);

module_exit(fisrt_drv_init);

MODULE_AUTHOR("http://www.100ask.net");

MODULE_VERSION("0.1.0");

MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");

MODULE_LICENSE("GPL");
View Code

Makefile如下:

  1 KERN_DIR =/home/book/Documents/linux-2.6.22.6
  2 PWD       := $(shell pwd)
  3 all:
  4         make -C $(KERN_DIR) M=$(PWD) modules 
  5 
  6 clean:
  7         make -C $(KERN_DIR) M=$(PWD) modules clean
  8         rm -rf modules.order
  9 
 10 obj-m   += myled.o

上面的Makefile经过了一次更改,之前韦老师的Makefile如下:

第一个驱动之字符设备驱动(一)

关键在于使用韦老师的`pwd`这个方式,我在ubuntu 16.04上make会失败,查询网上资料,改成$(PWD)之后,终于make成功了。特别注意一点,在make驱动函数之前,需要先构建内核树,其实就是保证在make驱动函数之前,先make一下内核。还有一点需要注意,想要加载驱动,在第一次make内核之后,把此次生成的uImage下载进入flash,然后才可以看到驱动被加载。还有就是,在使用不更改uboot参数的网络文件系统,即通过手动mount的方式,这种情况下insmod驱动的.ko文件,会比较耗时,甚至容易出现失败或者长时间卡死状态,所以建议选用set uboot参数的方式,这样insmod的时候可以快速响应:

第一个驱动之字符设备驱动(一)

现在写个main函数测试这个驱动:

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdio.h>



int main(int argc, char **argv)

{

    int fd;

    int val=1;

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

    if(fd<0)

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

    write(fd,&val,4);

    return 0;

}

在nfs共享目录下编译一下这个源文件:

第一个驱动之字符设备驱动(一)

生成可执行文件之后,在开发板上运行:

第一个驱动之字符设备驱动(一)

首先执行的时候,显示不能打开,因为我们还没有创建这样的设备,使用mknod创建设备节点之后,可以看到应用程序的open和write会触发我们驱动函数的open和write,证明我们的入门测试成功了。创建设备采用/dev/xxx是为了展示这个设备的名字,其实无关紧要,但是最好能有意义。

当然,这里只是我们第一个测试程序,存在不足,我们在驱动函数中是写死了主设备号为111,而且还需要手动创建节点,在之后的随笔中,将对其进行改进。

(现在我是使用的经过uboot更改了参数的nfs网络文件系统,这样的方式insmod更快)