字符设备驱动模型

字符设备驱动注册的步骤

  1. 自己创建struct file_operations结构体fp

  2. 在入口函数,申请设备号,把fp占着该设备号对应的槽

  3. 在入口函数,创建类struct class

  4. 在入口函数,在类下创建设备节点

  5. 在出口函数,倒着来,

    • 删除设备节点

    • 删除类

    • 释放fp所占的槽

基本概念

设备号

设备号组成

Linux提供dev_t的数据类型表示设备号:高12位主设备号(大小:0-4095),低20位位次设备号

// include/linux/types.h
    
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t; // 所以可以看出dev_t就是一个uint32_t 的数据

设备号常用函数

//include/linux/kdev_t.h

#define MINORBITS 20 
#define MINORMASK ((1U << MINORBITS) - 1) 

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 从设备号中得到主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))  // 从设备号中得到次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))        // 把主设备号和次设备号组装成dev_t类型的linux设备号

字符设备

linux用cdev表示一个字符设备

struct cdev { 
	struct kobject kobj; 
	struct module *owner; 
	const struct file_operations *ops;   // 操作函数集合
	struct list_head list; 
	dev_t dev;                           // 设备id
	unsigned int count; 
};

入口函数中定义字符设备步骤如下

struct cdev testcdev;            	// 定义字符设备结构体 
testcdev.owner = THIS_MODULE;     
cdev_init(&testcdev, &test_fops);	// 字符设备结构体 初始化
cdev_add(&testcdev, devid, 1);   	// 把字符设备添加到linux内核中

出口函数删除字符设备

cdev_del(&testcdev);

函数原型介绍

/**
*
*@cdev:字符设备
*@fops:file_operations结构体指针
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

/**
*用于向 Linux系统添加字符设备 (cdev结构体变量 )
*@p:指向要往系统添加的字符设备
*@count:要添加的设备数量
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

/**
*驱动卸载的时候要从linux内核中卸载字符设备
*@p:指向要往系统添加的字符设备
*/
void cdev_del(struct cdev *p)

在 2.4 的内核我们使用 major = register_chrdev(0, "xym_led", &led_drv) 来进行字符设备注册,在注册过程中分配了设备号,这种方式每一个主设备号只能存放一种设备,它们使用相同的 file_operation 结构体,也就是说内核最多支持 256 个字符设备驱动程序。

在 2.6 的内核之后,新增了一个 register_chrdev_region 函数,它支持将同一个主设备号下的次设备号进行分段,每一段供给一个字符设备驱动程序使用,使得资源利用率大大提升,同时,2.6 的内核以后保留了原有register_chrdev 方法。

  • 老版本

    内核在入口函数都是使用register_chrdev函数来确定主设备号:

    major = register_chrdev(0, "xym_led", &led_drv); // 第一个参数是0的话表示自动申请设备号
    

    函数调用关系图:

    register_chrdev
    	-》__register_chrdev(major, 0, 256, name, fops)
    		-》__register_chrdev_region(major, baseminor, count, name)
    		-》cdev = cdev_alloc();
    		-》cdev_add
    

    从上面的调用关系可以看出,register_chrdev完成2部分工作

    1. 调用__register_chrdev_region完成设备号申请

    2. 完成了字符设备cdev的创建和添加到内核工作。

    上面用到的函数原型如下

    
    /**
    * 设备号申请函数
    *@major :主设备号,如果为0,那么系统默认会自动获取主设备号返回
    *@name:设备名字
    *@fops: 驱动的 file_operations函数集合
    */
    //include/linux/fs.h
    static inline int register_chrdev(unsigned int major, const char *name,
    				  const struct file_operations *fops)
    {
        /*
        * 这个地方就是我们说的为什么使用register_chrdev函数获取设备号的时候次设备号一下子就没了,
        * 这里从0开始,申请了256个次设备号,就是一个主设备号major号下的256个次设备号(0-255)全用完了
        */
    	return __register_chrdev(major, 0, 256, name, fops); 
    
    }
    
    int __register_chrdev(unsigned int major, unsigned int baseminor,
    		      unsigned int count, const char *name,
    		      const struct file_operations *fops)
    {
    	struct char_device_struct *cd;
    	struct cdev *cdev;
    
    	cd = __register_chrdev_region(major, baseminor, count, name);// 申请设备号
    	cdev = cdev_alloc();                                         // 创建cdev
    	cdev->owner = fops->owner;
    	cdev->ops = fops;
    	kobject_set_name(&cdev->kobj, "%s", name);
    
    	err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);     // 增加cdev到内核
    
    	cd->cdev = cdev;
    
    	return major ? 0 : cd->major;
    //   ………………… 
    
    }
    
    /**
    * 设备号释放函数
    *@major :主设备号,如果为0,那么系统默认会自动获取主设备号返回
    *@name:设备名字
    */
    static inline void unregister_chrdev(unsigned int major, const char *name)
    
  • 新版本

    在入口函数中使用register_chrdev_region或者alloc_chrdev_region函数获取主设备号:

       if(major){
    	    devid = MKDEV(major, 0); 
    		register_chrdev_region(devid, 1, "xym_led");  /* 设备号事先确定的情况 */
    			-》__register_chrdev_region(MAJOR(n), MINOR(n),next - n, name);
    
    	}else{
    		alloc_chrdev_region(&devid, 0, 1, "xym_led"); /* 设备号事先不确定,申请设备号 */ 
    			 -》__register_chrdev_region(0, baseminor, count, name);
    
    		major = MAJOR(devid); 					      /* 获取主设备号 */ 
    		minor = MINOR(devid); 					       /* 获取次设备号 */
    	}
       /* 这里 需要手动创建字符设备,并添加到内核 */
    	struct cdev cdev;
        cdev.owner = THIS_MODULE;
    	cdev_init(&cdev, &led_drv);
        cdev_add(&cdev, devid, 1);
    

    从上面的调用关系可以看出,register_chrdev_region或者alloc_chrdev_region完成1部分工作

    1. 调用__register_chrdev_region完成设备号申请,和老版本一样;

    上面用到的函数原型如下

    /**
    * 需要在驱动的入口函数申请设备号  如果设备号已经确定,那么使用 register_chrdev_region 函数 申请设备id
    *@from		:要从哪个设备id开始申请设备号
    *@count		:要申请的设备号数量。
    *@name		:设备名字
    */
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    
    /**
    * 需要在驱动的入口函数申请设备号  如果主设备号没有确定 使用alloc_chrdev_region函数申请设备id
    *@dev		:保存申请到的设备号
    *@baseminor	:次设备号起始地址,可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor为起始地址地址开始递增。一般从0,也就是说次设备号从0开始。
    *@count		:要申请的设备号数量。
    *@name		:设备名字
    */
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
    
    
    /**
    * 设备号释放函数,在出口函数中调用,字符设备释放函数,无论是register_chrdev_region或者alloc_chrdev_region注册的字符设备,都用该函数进行释放
    *@from :要释放的设备号。
    *@count:表示从 from开始,要释放的设备号数量
    */
    void unregister_chrdev_region(dev_t from, unsigned count)
    

总结:从调用关系可以看出无论是新版本还是老版本,获取设备号的方式,最总都是 __register_chrdev_region函数实现,只是老版本register_chrdev不仅仅会分配设备号,还自动创建了并向内核添加了字符设备cdev,而新版本需要用户手动完成 cdev的创建和添加到内核。

注意:使用cat /proc/devices 命令查看当前系统已经使用的了设备号

设备节点

命令行手动创建设备节点

/dev/chrdevbase :设备节点名字
c 	:代表字符设备驱动
200	: 主设备号
0	: 次设备号

mknod /dev/chrdevbase c 200 0

自动创建设备节点

在驱动入口函数中先创建类,然后在类下创建设备

/* 创建类,类名 xym_led_class*/
led_class = class_create(THIS_MODULE, "xym_led_class");    
/* 类下创建设备,那么 /dev/xym_led 即是显示的设备节点*/
device_create(led_class, NULL, devid, NULL, "xym_led");     

在出口函数就要设备类和设备节点

  device_destroy(led_class, devid);  // 摧毁类下的设备
  class_destroy(led_class);          // 摧毁类

函数原型

创建类和删除类

自动创建设备节点相关代码。首先要创建一个 class类, class是个结构体,定义在文件 include/linux/device.h里面。 class_create是类创建函数, class_create是个宏定义,内容如下:

/**
*
*@owner:THIS_MODULE
*@name:类名
*/

#define class_create(owner, name) \
({ \ 
	static struct lock_class_key __key; \
    __class_create(owner, name, &__key); \
})


struct class *__class_create(struct module *owner, const char *name, 8 struct lock_class_key *key)

/**
*
*@cls:指向要卸载的类指针
*/
void class_destroy(struct class *cls);
创建设备和删除设备
/**
*
*@class:指向要在哪个类下面创建设备的类指针
*@parent:父亲,一般为NULL
*@devt  : 设备id
*@drvdata:设备可能会使用的一些数据
*@fmt :可变参数,设备名字,该名字会在/dev/目录下显示
*/
/*
 * 建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。
 * 使用 device_create函数在类下面创建设备, device_create函 数原型如下:
*/

struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)

在设备卸载的时候,即出口函数中调用摧毁函数,参数 classs是要删除的设备所处的类,参数 devt是要删除的设备号。

/**
*
*@class:指向要删除的设备所处的类
*@devt  : 设备id
*/
void device_destroy(struct class *class, dev_t devt)

mmu地址映射

../../_images/image-20200601095304812.png

  • 完成虚拟空间到物理空间的映射。

  • 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

ioremap函数

Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。当我们想访问物理地址的时候就必须做相应的转换ioremap函数。

ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h文件中,定义如下:

/**
*@phys_addr	:要映射给的物理起始地址。
*@*@size		:要映射的内存空间大小。
mtype ioremap的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED和 *@MT_DEVICE_WC ioremap函数选择 MT_DEVICE。
*返回值		:  __iomem类型的指针,指向映射后的虚拟空间首地址。
*/
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE) 
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype) 
{ 
	return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0)); 
}

例如我们要访问GPIO1_IO03的寄存器,可以这样定义

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)            // 查找手册确定要操作的寄存器物理地址
static void __iomem* SW_MUX_GPIO1_IO03; 
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);// 通过ioremap把物理地址转换为虚拟地址 SW_MUX_GPIO1_IO03 // 这样就可以用SW_MUX_GPIO1_IO03访问了

iounmap函数

卸载驱动的时候需要使用 iounmap函数释放掉 ioremap函数所做的映射, iounmap函数原型如下:

函数原型 void iounmap (volatile void __iomem *addr)
iounmap(SW_MUX_GPIO1_IO03);

内核空间的内存操作函数

使用 ioremap函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

读操作函数
u8 readb(const volatile void __iomem *addr) 
u16 readw(const volatile void __iomem *addr) 
u32 readl(const volatile void __iomem *addr)

readb、 readw和 readl这三个函数分别对应 8bit、 16bit和 32bit读操作,参数 addr就是要读取写内存地址,返回值就是读取到的数据。

写操作函数

写操作函数有如下几个:

void writeb(u8 value, volatile void __iomem *addr) 
void writew(u16 value, volatile void __iomem *addr) 
void writel(u32 value, volatile void __iomem *addr)

writeb、 writew和 writel这三个函数分别对应 8bit、 16bit和 32bit写操作,参数 value是要写入的数值, addr是要写入的地址。

printf 和printk

printf运行在用户空间,printk运行在内核空间,可以根据日志级别对消息进行分类,一共有 8个消息级别,这 8个消息级别定义在文件 include/linux/kern_levels.h里面,定 义如下:

#define KERN_SOH "\001" 
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */ 
#define KERN_ALERT KERN_SOH "1" /* 必 须立即采取行动 */ 
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误 */ 
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR报告硬件错误 */ 
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */ 
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */ 
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */ 
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */ 一共定义了 8个级别,其中 0的优先级最高, 7的优先级最低。

如果要设置消息级别,参考如下示例:

printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");

不显式的设置消息级别,那么 printk将会采用默认级别

MESSAGE_LOGLEVEL_DEFAULT默认为 4。
在 include/linux/printk.h中有个宏 
#define CONSOLE_LOGLEVEL_DEFAULT 7 

消息基本高于7的才会被显示出来

驱动模块常用命令

  1. insmod xxx.ko:装载驱动

  2. cat /proc/device:查看

  3. lsmod:查看内核中已经加载的驱动模块

  4. rmod xxx:卸载驱动

  5. dmesg:驱动里面printk的打印信息在加载驱动的时候不一定会显示,可以使用该命令查看

代码示例

老版本参见【01.led_drv】工程

前面介绍了一个简单的led驱动框架,下面就按照该框架进行程序填充:

  1. 自己创建struct file_operations结构体fp

    /* 
     * 1. 定义自己的file_operations结构体                                     
     */
    static struct file_operations led_file_op = {
    	.owner	 = THIS_MODULE,
    	.open    = led_drv_open,
    	.read    = led_drv_read,
    	.write   = led_drv_write,
    	.release = led_drv_close,
    };
    
  2. 在入口函数,申请设备号,把fp占着该设备号对应的槽

  3. 在入口函数,创建类struct class

  4. 在入口函数,在类下创建设备节点

    /**
     * @description: led_init - 入口函数
     */
    static int __init led_init(void)
    {
    	/*
    	 *  2: 自动获取主设备号
    	 */
    
    	g_led_dev.major = register_chrdev(0, "xym_led", &led_file_op);        /* 注册字符设备驱动 */
    
        /*
    	 *  3:  创建类
    	 */
    
        g_led_dev.class = class_create(THIS_MODULE, "xym_led_class");
    	if (IS_ERR(g_led_dev.class)) {
    		unregister_chrdev(g_led_dev.major, "xym_led");
    		goto led_init_error;
    	}
    
        /* 
    	 *  4.在类下创建设备节点设备 /dev/xym_led
    	 */
    
    	g_led_dev.devid = MKDEV(g_led_dev.major,0);
    	device_create(g_led_dev.class, NULL, g_led_dev.devid, NULL, "xym_led"); /* /dev/xym_led */
    
    	printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__);
    
    	return 0;
    
    led_init_error:
    
    	printk("%s %s line %d:init error !!!\n", __FILE__, __FUNCTION__, __LINE__);
    	return  -1;
    }
    
  5. 在出口函数,倒着来,

    • 删除设备节点

    • 删除类

    • 释放fp所占的槽

    static void __exit led_exit(void)
    {
        /* 
    	 *  5.倒着来 释放资源
    	 */
    
        hw_led_reinit();
    	device_destroy(g_led_dev.class, g_led_dev.devid); /* 删除设备节点               */
    	class_destroy(g_led_dev.class);                   /* 删除类                 */
    	unregister_chrdev(g_led_dev.major, "xym_led");    /* 删除字符设备槽              */
    	printk("%s %s line %d: rmmod ! \n", __FILE__, __FUNCTION__, __LINE__);
    
    }
    

新版本参见【02.led_driver_new】工程

前面介绍了一个简单的led驱动框架,下面就按照该框架进行程序填充:

  1. 自己创建struct file_operations结构体fp

    /* 
     * 1. 定义自己的file_operations结构体                                     
     */
    
    static struct file_operations led_drv = {
    	.owner	 = THIS_MODULE,
    	.open    = led_drv_open,
    	.read    = led_drv_read,
    	.write   = led_drv_write,
    	.release = led_drv_close,
    };
    
  2. 在入口函数,申请设备号,把fp占着该设备号对应的槽

  3. 在入口函数,创建类struct class

  4. 在入口函数,在类下创建设备节点

    static int __init led_init(void)
    {
    	/*
    	 *  2: 自动获取主设备号
    	 */
    
    	if(g_led_dev.major){
    		g_led_dev.devid = MKDEV(g_led_dev.major, 0); 
    		register_chrdev_region(g_led_dev.devid, 1, "xym_led");
    	}else{
    		alloc_chrdev_region(&g_led_dev.devid, 0, 1, "xym_led");    /* 申请设备号 */ 
    		g_led_dev.major = MAJOR(g_led_dev.devid); 					 /* 获取主设备号 */ 
    		g_led_dev.minor = MINOR(g_led_dev.devid); 					 /* 获取次设备号 */
    	}
    	printk("newcheled major=%d,minor=%d\r\n",g_led_dev.major, g_led_dev.minor);
    
    	/* 
    	 * 2.1:初始化cdev 
         */ 
    	g_led_dev.cdev.owner = THIS_MODULE;
    	cdev_init(&g_led_dev.cdev, &led_drv);
       /* 
        * 2.3:添加cdev 到 内核
        */ 
        cdev_add(&g_led_dev.cdev, g_led_dev.devid, 1);
    
        /*
    	 *  3:  创建类
    	 */
    
     	g_led_dev.class = class_create(THIS_MODULE, "xym_led_class");
    
        /* 
    	 *  4.在类下创建设备节点设备 /dev/xym_led
    	 */
    	device_create(g_led_dev.class, NULL, g_led_dev.devid, NULL, "xym_led"); /* /dev/xym_led */
    
    	printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__);
    
    	return 0;
    
    led_init_error:
    
    	printk("%s %s line %d:init error !!!\n", __FILE__, __FUNCTION__, __LINE__);
    	return  -1;
    }
    
  5. 在出口函数,倒着来,

    • 删除设备节点

    • 删除类

    • 释放fp所占的槽

    static void __exit led_exit(void)
    {
        /* 
    	 *  5.倒着来 释放资源
    	 */
    
        hw_led_reinit();
    	cdev_del(&g_led_dev.cdev);
    	unregister_chrdev_region(g_led_dev.devid, 1); /* 卸载掉1个 */
    
    	device_destroy(g_led_dev.class, g_led_dev.devid); 
    	class_destroy(g_led_dev.class);
    
    	printk("%s %s line %d: rmmod ! \n", __FILE__, __FUNCTION__, __LINE__);
    
    }
    

实验步骤

  1. 工程编译完成后会生成两个文件

    • led_app

    • xym_led.ko

  2. 把两个文件拷贝到nfs文件系统运行

    • insmod xym_led.ko :加载驱动模块

    • lsmod:查看内核中已经加载的驱动模块

    • ./led_app on 或者./led_app off 打开或者关闭led

    • rmod xym_led:卸载驱动模块

    • lsmod:查看内核中已经加载的驱动模块