字符设备驱动基础

1. linux 设备驱动分类

​ 设备驱动程序在Linux内核中有着十分重要的角色,设备驱动程序就好像一个个“黑匣子”,内部实现了对硬件设备的实际操作,而对外提供了操作硬件的接口,用户可以通过一系列标准化的调用来使用这些接口,从而操作硬件的工作。

Linux系统将设备驱动程序主要分为了三大类:

  • 字符设备驱动

  • 块设备驱动

  • 网络设备驱动

2. 各类型设备介绍

2.1 字符设备

​ 字符设备是能够像字节流(比如文件)一样访问的设备。应用程序和驱动程序之间进行数据读写的时候,数据是以“字节”为单位。数据交互的时候,是按照固定的顺序传输的;数据是实时传输的,是没有缓存的。字符设备是没有文件系统的。

  • 绝大部分设备驱动是字符设备:LED、BEEP、按键、键盘、触摸屏、摄像头、液晶屏、声卡、IIC、SPI、…

  • 字符设备通过 /dev 下的字符设备文件来访问。

  • 应用程序需要借助系统IO函数来操作字符设备。

    open("/dev/led_drv", O_RDWR)   
    read()   
    write()  
    ioctl()   
    mmap()
    close()
    

2.2 块设备

​ 块设备通常是按照块为单位来访问数据,应用程序和驱动程序之间进行数据读写的时候,数据是以“块”为单位,1block=1024KB。块设备是有缓存的,块设备是有文件系统的**。**

  • 大容量的存储设备一般都是块设备:nand flash、eMMC、SD、U盘、硬盘、…

  • 查看系统已有的块设备

    [root@GEC6818 /]# cat /proc/partitions
    
  • 块设备也是通过/dev目录下的文件系统节点来访问,块设备和字符设备区别仅仅在于内核内部管理数据的方式,也就是内核和驱动程序的接口不同。

  • 应用程序 往往借助标准IO函数来操作块设备。

    fopen("/dev/led_drv", O_RDWR)
    fread()/fwrite()
    fclose()     
    

    当然也是可以通过系统IO函数来操作块设备的,因为标准IO内部就是调用系统IO访问文件的。

2.3 网络设备

  • 网络设备驱动不同于字符设备和块设备,不在/dev下以文件节点代表,而是通过单独的网络接口来代表。

  • 内核和网络驱动程序间的通信完全不同于内核和字符设备驱动以及块设备前驱动程序之间的通信,内核调用一套与数据包传输相关的函数。

  • 有线网卡、无线网卡、虚拟网卡… 都属于网络设备网络设备是没有设备文件的。

  • 应用程序是要通过 socket来访问网络设备的

3. 字符设备开发

Linux系统中的驱动程序往往是以模块的方式加载进系统中,所以驱动程序编写的第一步是要先进行一个模块的初始化,以及对模块的信息进行描述;

3.1 字符设备开发流程

 字符设备驱动程序的开发流程:
 1. 定义一个描述字符设备的结构体变量;
	struct cdev cxxxdev;
 2. 创建针对该字符设备cdev的文件操作集 file_operations;
    struct  file_operations  xxx_ops ={.};

//主设备相同时以次设备号区分的局部注册------------------------------------------------
 3. 创建设备号;并注册设备号
     3.1 静态注册  dev_t device =MKDEV(major,minor); register_chrdev_region(device,minor,Count,"device name");
     3.2 动态申请  alloc_chrdev_region(&device,minorStart,minorCount,"device name");
 4. 初始化字符设备cdev  cdev_init(&cdev,&fops)
 5. 添加字符设备到内核
     cdev_add(&cdev,&device,Count)
 -------------------------------------------------------------------------------
     
 //单独以主设备号作为区分------------------------------------(常用)-----------------------
 major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);  /* /dev/gpio_desc */
 -------------------------------------------------------------------------------
     
 // 以下的步骤主要作用是在内核加载该驱动模块的时候自动生成设备文件;    
 6. 创建class   
    class_create(THIS_MODULE,"led_cls") 
 7. 创建device 
    struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
        
        
 8. 申请物理内存区资源(不是必须的)
    struct resource * request_mem_region(resource_size_t start, resource_size_t n,
				   const char *name, int flags)
 9. 映射物理内存得到到虚拟内存	
    ioremap
        
//请注意: 1-5步骤后字符设备驱动已基本完成,只是在系统中没有产生设备文件,我们可以在系统中用 mknod手动增加设备文件
      mknod  DEVNAME {b | c}  MAJOR  MINOR
     # mknod  /dev/xyz c 245 0
//或者继续6-7自动生成设备文件

相关结构体的说明

struct cdev {
	struct kobject kobj;            //内核管理驱动使用的kobj
	struct module *owner;       //cdev是属于哪个module,一般写成THIS_MODULE
                                                   
	const struct file_operations *ops;  //cdev的文件操作集
	struct list_head list;                //内核管理cdev的链表
	dev_t dev;                               //设备号
	unsigned int count;               //次设备的数量
};
struct file_operations { 
  struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULE
   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);//仅用于读取目录,对于设备文件,该字段为NULL   
   unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入   
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令   
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl  
  long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替   
  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); //通知设备FASYNC标志发生变化   
  int (*lock) (struct file *, int, struct file_lock *);   
  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 (*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);   
  int (*setlease)(struct file *, long, struct file_lock **);   
};  

3.2 主次设备的具体注册和释放函数接口

驱动开发中根据设备资源情况

设备号

  Linux中设备的唯一标识。每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。

Linux内核将设备号定义为一个32bits的无符号整型值。

typedef __u32   __kernel_dev_t;
typedef __kernel_dev_t  dev_t;

同时我们可以利用 :
MKDEV(major,minor)来产生一个表示设备的号。

3.2.1 主设备号作为驱动类型的区分(次设备号相同)

设备注册
  • 申请设备号

    /* 函数定义 */
    static inline int register_chrdev(unsigned int major, const char *name,
    				  const struct file_operations *fops)
    {
    	return __register_chrdev(major, 0, 256, name, fops);
    }
    
  • 创建设备节点 device, class的处理

    // 创建class和device的目的是在安装的驱动的时候,可以自动生成设备文件,在卸载驱动的时候,可以自动的删除设备文件。
    struct class *class_create(struct module *owner, const char *name)
                               
    参数说明:
        owner-->创建的class属于哪个module,一般为THIS_MODULE
        name --->自定义的class的名字
    返回值:
    	创建成功的class结构的指针。
    	
    //创建class和device的目的是在安装的驱动的时候,可以自动生成设备文件,在卸载驱动的时候,可以自动的删除设备文件。
    struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
    参数说明:
        class-->创建后的device所属的class,
        parent--->创建后的device的父设备,一般设为NULL
        devt --->设备号
        drvdata  --->字符设备驱动的data,一般为NULL
        fmt --->设备文件的名字
    返回值: 	
        创建成功的device结构的指针。
    
    
卸载设备
  • device, class的回收

    //device回收
    void device_destroy(struct class *class, dev_t devt)
    
    参数说明:
        class-->创建的device所属的class结构体指针,
        devt --->设备号
        
    //class回收
    void class_destroy(struct class *cls);
    
    参数说明:
    	cls-->创建的class结构体指令
    
  • 注销设备号

    /* 函数定义 */
    static inline void unregister_chrdev(unsigned int major, const char *name)
    {
    	__unregister_chrdev(major, 0, 256, name);
    }
    
示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/printk.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/uaccess.h>
static struct class *hello_class;

static int major;

static int hello_init(void)
{
    major = register_chrdev(0, "100ask_hello", &hello_drv);

	hello_class = class_create(THIS_MODULE, "hello_class");
	if (IS_ERR(hello_class)) {
		printk("failed to allocate class\n");
		return PTR_ERR(hello_class);
	}

    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");  /* /dev/hello */

   return 0;
}


/* 4. exit function */
static void hello_exit(void)
{
    device_destroy(hello_class, MKDEV(major, 0));

    class_destroy(hello_class);

    unregister_chrdev(major, "100ask_hello");
}

3.3.3 次设备号作为设备的区分(主设备号相同)

设备注册
  • 静态申请设备号

    int register_chrdev_region(dev_t from, unsigned count, const char *name)
        
    参数说明:
    	from --->注册的设备号;如果一次注册多个设备号,from就是注册设备号的开始值
    	count --->次设备的数量
    	name ---->设备名称,但不是设备文件的名字    #cat /proc/devices
        
    返回值:
    	成功返回0,失败返回负数错误码
    
  • 动态申请设备号 (常用)

    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
    			const char *name)
    参数说明:
    	dev[OUT]--->系统分配的设备号
    	baseminor--->次设备号的开始值
        count ---> 次设备的数量
        name ----> 设备名称,但不是设备文件的名字     #cat /proc/devices
    
    返回值:
        成功返回0,失败返回负数错误码
    
  • 初始化字符设备

    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    
    参数说明:
        cdev---> 需要初始化的字符设备 cdev 结构
        fops--->与cdev 设备关联的文件操作集
    
  • 将字符设备加入内核

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    
    参数说明:
        p--> 字符设备 cdev 结构
        dev---> 系统分配号的设备号。
        count --->     次设备的数量
    
    返回值:
    	失败返回负数错误码
    
  • 创建设备节点 (创建class / 创建device)

    // 创建class和device的目的是在安装的驱动的时候,可以自动生成设备文件,在卸载驱动的时候,可以自动的删除设备文件。
    struct class *class_create(struct module *owner, const char *name)
                               
    参数说明:
        owner-->创建的class属于哪个module,一般为THIS_MODULE
        name --->自定义的class的名字
    返回值:
    	创建成功的class结构的指针。
    	
    struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
    参数说明:
        class-->创建后的device所属的class,
        parent--->创建后的device的父设备,一般设为NULL
        devt --->设备号
        drvdata  --->字符设备驱动的data,一般为NULL
        fmt --->设备文件的名字
    返回值: 	
        创建成功的device结构的指针。
    
卸载设备
  • device, class的回收

    //device回收
    void device_destroy(struct class *class, dev_t devt)
    
    参数说明:
        class-->创建的device所属的class结构体指针,
        devt --->设备号
    //class回收
    void class_destroy(struct class *cls);
    
    参数说明:
    	cls-->创建的class结构体指令
    
  • 从内核移除字符设备

    void cdev_del(struct cdev *p)
        
    参数说明:
       p--> 字符设备 cdev 结构
    
    函数功能:
    	从系统中移除一个字符设备,并释放相关资源
    
  • 注销设备号

    当字符设备模块被卸载或执行异常的情况下,申请到的设备号需要释放,以便下次申请。

    void unregister_chrdev_region(dev_t from, unsigned count)
        
    参数说明:
        from--->注册的设备号;如果一次注册多个设备号,from就是注册
        设备号的开始值
        count ---> 次设备的数量
    
示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/printk.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/uaccess.h>

static struct class *hello_class;
static struct cdev hello_cdev;
static dev_t dev;


/* 3. entry function */
static int hello_init(void)
{
    int ret;

    // register_chrdev
/*    //静态注册
	dev = MKDEV(0,1);    
	
	if(register_chrdev_region(dev, 1, "led_device")<0)
	{
        printk(KERN_WARNING "register chrdev failed!\n");
		return -EAGAIN;
	}
*/
	//动态申请
	ret = alloc_chrdev_region(&dev, 0, 2, "hello");
	if (ret < 0) {
		printk(KERN_ERR "alloc_chrdev_region() failed for hello\n");
		return -EINVAL;
	}

    cdev_init(&hello_cdev, &hello_drv);

    ret = cdev_add(&hello_cdev, dev, 2);
	if (ret)
    {
        unregister_chrdev_region(dev, 2); //注销设备号
		printk(KERN_ERR "cdev_add() failed for hello\n");
		return -EINVAL;
    }
		
	hello_class = class_create(THIS_MODULE, "hello_class");
	if (IS_ERR(hello_class)) {
        
        cdev_del(&hello_cdev);
        unregister_chrdev_region(dev, 2);
        
		printk("failed to allocate class\n");
		return PTR_ERR(hello_class);
	}

    struct device * hello_device = device_create(hello_class, NULL, dev, NULL, "hello");  /* /dev/hello */
    if (IS_ERR(hello_device)) {
        
        class_destroy(hello_class);
    	cdev_del(&hello_cdev);
        unregister_chrdev_region(dev, 2);
       
		printk("failed to allocate class\n");
		return PTR_ERR(hello_class);
	}
   return 0;
}


/* 4. exit function */
static void hello_exit(void)
{
    device_destroy(hello_class, dev);

    class_destroy(hello_class);

    //unregister_chrdev(major, "100ask_hello");
    cdev_del(&hello_cdev);
    unregister_chrdev_region(dev, 2);
}

3.3 标准驱动程序

上述驱动程序存在一个问题,在后面某出设置若出现问题,则直接返回不会释放之前申请的资源,下面给出一个标准化的驱动测试程序

/*
动态申请设备号
动态创建设备文件

*/
#include <linux/fs.h> //申请设备号  register_chrdev
#include <linux/init.h> //模块
#include <linux/module.h>//模块  
#include <linux/device.h> //创建设备文件 class_create  devcie_create
#include <linux/slab.h> //kmalloc



//定义hello设备结构体
struct rk3399_hello 
{
	unsigned int major;
	struct class *hello_cls;
	struct device *hello_dev;
};


//定义hello_device设备对象
struct rk3399_hello  *hello_device;

static int hello_open (struct inode *inode, struct file *filp)
{
	printk("<kernel> call %s @ %d\n", __func__, __LINE__);
	//调用函数名  行号
	return 0;

}
static int hello_close (struct inode *inode, struct file *filp)
{
	printk("<kernel> call %s @ %d\n", __func__, __LINE__);
	return 0;
}
ssize_t hello_read (struct file *filp, char __user *buf, size_t size, loff_t *flag)
{
	printk("<kernel> call %s @ %d\n", __func__, __LINE__);
	return 0;
}
ssize_t hello_write (struct file *filp, const char __user *buf, size_t size, loff_t *flag)
{
	printk("<kernel> call %s @ %d\n", __func__, __LINE__);
	return 0;
}

//定义文件操作接口
static const struct file_operations fops = {
	.owner = THIS_MODULE, //避免卸载模块
	.open = hello_open, // 对应  应用程序的 open
	.read = hello_read,
	.write = hello_write,
	//unlocked_ioctl
	.release = hello_close,
};


//入口函数  ,static文件内可见, __init 优化一次性, insmod调用
static int __init hello_init(void)
{
	int  ret;
	//创建hello设备对象
	hello_device  =  kmalloc(sizeof(struct rk3399_hello),GFP_KERNEL);
	if (!hello_device) {
		printk("register_chrdev fail\n");
		return -ENOMEM;

	}
		
    //申请设备号
	hello_device->major = register_chrdev(0, "hello_drv", &fops);
	if (hello_device->major < 0) {
		printk("register_chrdev fail\n");
		ret= -EBUSY;
		goto err_regist;
	}

	//创建设备类
	hello_device->hello_cls = class_create(THIS_MODULE, "hello_class");
	
	if (IS_ERR(hello_device->hello_cls)) {											
		printk("class_create fail\n");			
		ret = PTR_ERR(hello_device->hello_cls);
		goto err_class;
	}											

	//创建设备文件
	hello_device->hello_dev = device_create(hello_device->hello_cls, NULL, MKDEV(hello_device->major, 5), NULL, "hello"); //  /dev/hello
	if (IS_ERR(hello_device->hello_dev)) {									
		printk("device_create fail\n");								
		ret=PTR_ERR(hello_device->hello_dev);	
		goto err_dev;
	}									
	
	printk("<kernel> major=%d\n", hello_device->major);
	printk("<kernel> call %s @ %d\n", __func__, __LINE__);
	return 0;
    
//出错处理 顺序 先申请后释放 后申请先释放
err_dev:
	class_destroy(hello_device->hello_cls);
err_class:
	unregister_chrdev(hello_device->major, "hello_drv");
err_regist:
	kfree(hello_device);

	return ret;
}

//出口函数,static文件内可见, __exit 优化一次性, rmmod调用
static void __exit hello_exit(void)
{
	device_destroy(hello_device->hello_cls, MKDEV(hello_device->major, 5));
	class_destroy(hello_device->hello_cls);
	unregister_chrdev(hello_device->major, "hello_drv");
	kfree(hello_device);
	printk("<kernel> call %s @ %d\n", __func__, __LINE__);
}

module_init(hello_init); //告诉内核我这个模块程序入口函数
module_exit(hello_exit);//告诉内核我这个模块程序出口函数

MODULE_LICENSE("GPL"); //开源许可

4. 驱动程序访问IO物理地址

驱动程序访IO物理地址的主要步骤和方法:

a) 将物理地址区作为一个系统资源,申请该资源访问权

b) 将该物理内存区做内存的动态映射,得到虚拟地址

c) 访问虚拟地址

但请注意:

作为资源是有限的,也就是说一旦一个物理内存区已经被申请了,在该资源未被释放前,就不能再次申请。

4.1 申请/释放 物理资源

// 物理资源的申请
struct resource *  request_mem_region(resource_size_t start, resource_size_t n,const char *name)

参数说明:
    start  --->物理内存区的开始地址,
    n --->物理内存区的大小
    name --->自定义的物理内存区的名字
返回值:成功返回表示资源的结构体指针。失败返回NULL// 物理资源的释放:
void release_mem_region(resource_size_t start, resource_size_t n)

4.2 IO内存映射/解映射

// IO 内存动态映射,得到虚拟内存首地址
void __iomem *ioremap(phys_addr_t offset, unsigned long size)

参数说明:
    offset--->要映射的物理内存区开始地址,
    size --->物理内存区的大小
返回值:
    映射后虚拟地址内存区的首地址

// IO 内存解映射:
void iounmap(void __iomem * addr)

4.3 虚拟地址的访问

1.可以像裸机中对物理地址访问一样去访问虚拟内存

  例子: 
   gpio_base_va = ioremap(phys_addr_t offset, 
                           unsigned long size)*(unsigned int *)gpiocout_va |= ((1<<7)|(1<<8)|(1<<12)|(1<<17));

2.使用内核提供的函数

   u32 readl(const volatile void __iomem *addr)
   void writel(u32 b, volatile void __iomem *addr)

4.4 用户/内核空间数据交互

我们知道运行在用户模式下的进程是无法获取内核模式下的数据,反之也一样,应用程序往往是运行在用户模式下的,驱动程序是运行在内核空间的,这就需要解决用户和内核空间下的数据交互问题。

 #include <linux/uaccess.h>
// 1.从用户空间获取数据
unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
// 2  将数据拷贝给用户
unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Engineer-Jaylen_Sun

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

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

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

打赏作者

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

抵扣说明:

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

余额充值