字符设备驱动模型
字符设备驱动注册的步骤
自己创建
struct file_operations结构体fp在入口函数,申请设备号,把fp占着该设备号对应的槽
在入口函数,创建类
struct class在入口函数,在类下创建设备节点
在出口函数,倒着来,
删除设备节点
删除类
释放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部分工作调用
__register_chrdev_region完成设备号申请完成了字符设备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部分工作调用
__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地址映射

完成虚拟空间到物理空间的映射。
内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
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的才会被显示出来
驱动模块常用命令
insmod xxx.ko:装载驱动
cat /proc/device:查看
lsmod:查看内核中已经加载的驱动模块
rmod xxx:卸载驱动
dmesg:驱动里面printk的打印信息在加载驱动的时候不一定会显示,可以使用该命令查看
代码示例
老版本参见【01.led_drv】工程
前面介绍了一个简单的led驱动框架,下面就按照该框架进行程序填充:
自己创建
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, };
在入口函数,申请设备号,把fp占着该设备号对应的槽
在入口函数,创建类
struct class在入口函数,在类下创建设备节点
/** * @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; }
在出口函数,倒着来,
删除设备节点
删除类
释放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驱动框架,下面就按照该框架进行程序填充:
自己创建
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, };
在入口函数,申请设备号,把fp占着该设备号对应的槽
在入口函数,创建类
struct class在入口函数,在类下创建设备节点
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; }
在出口函数,倒着来,
删除设备节点
删除类
释放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__); }
实验步骤
工程编译完成后会生成两个文件
led_app
xym_led.ko
把两个文件拷贝到nfs文件系统运行
insmod xym_led.ko :加载驱动模块
lsmod:查看内核中已经加载的驱动模块
./led_app on 或者./led_app off 打开或者关闭led
rmod xym_led:卸载驱动模块
lsmod:查看内核中已经加载的驱动模块