文章目录
- 一、字符设备驱动高级
- 1.1 注册字符设备驱动新接口
- 1.1.1 新接口与旧接口
- 1.1.2 cdev介绍
- 1.1.3 设备号
- 1.1.4 编程实践
- 1.1.5 alloc_chrdev_region自动分配设备号
- 1.1.6 中途出错的倒影式错误处理方法
- 二、字符设备驱动注册代码分析
- 2.1 旧接口register_chrdev
- 2.2 新接口register_chrdev_region & alloc_chrdev_region
- 2.3 注销
- 三、自动创建字符设备驱动的设备文件
- 3.1 解决方案:udev(嵌入式中用的是mdev)
- 3.2 内核驱动设备类相关函数
- 四、设备类相关代码分析
- 4.1 sysfs文件系统
- 五、静态映射表建立过程分析
- 5.1 建立映射表的三个关键部分
- 六、动态映射结构体方式操作寄存器
- 七、内核提供的读写寄存器接口
一、字符设备驱动高级
1.1 注册字符设备驱动新接口
1.1.1 新接口与旧接口
- 旧接口:register_chrdev
- 新接口:register_chrdev_region(注册设备号)/alloc_chrdev_region(分配设备号) + cdev
1.1.2 cdev介绍
(1) 结构体
struct cdev {
struct kobject kobj; // 内嵌的内核对象,每个 cdev 都是一个 kobject
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头,用来将已经向内核注册的所有字符设备形成链表
dev_t dev; // 字符设备的设备号,由主设备号和次设备号构成
unsigned int count; // 隶属于同一主设备号的次设备号的个数.
};
(2) 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;
(3) 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
(4) 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;
1.1.3 设备号
(1) 设备号 = 主设备号 + 次设备号
(2) dev_t类型
(3) MKDEV(MAJOR, MINOR);
说明: 获取设备在设备表中的位置。
MAJOR 主设备号
MINOR 次设备号
1.1.4 编程实践
(1) 使用register_chrdev_region + cdev_init + cdev_add进行字符设备驱动注册
全局变量:
#define MYMAJOR 200
#define MYCNT 1
#define MYNAME "testchar"
static dev_t mydev;
static struct cdev test_cdev;
注册驱动:
// 新的接口注册字符设备驱动需要2步
// 第1步:静态注册/分配主次设备号
int retval;
mydev = MKDEV(MYMAJOR, 0);
retval = register_chrdev_region(mydev, MYCNT, MYNAME);
if (retval)
{
printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
return -EINVAL;
}
printk(KERN_INFO "register_chrdev_region success\n");
// 第2步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops);
retval = cdev_add(&test_cdev, mydev, MYCNT);
if (retval)
{
printk(KERN_ERR "Unable to cdev_add\n");
return -EINVAL;
}
printk(KERN_INFO "cdev_add success\n");
注销驱动:
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
cdev_del(&test_cdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);
1.1.5 alloc_chrdev_region自动分配设备号
(1) register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的
。
(2) 更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。
(3) 自动分配的设备号,必须去知道它的主次设备号,否则后面没法去mknod创建他对应的设备文件。
(4) 使用MAJOR宏和MINOR宏从dev_t得到major和minor
(5) 反过来使用MKDEV宏从major和minor得到dev_t。
(6) 使用这些宏的代码具有可移植性
#define MYCNT 1
#define MYNAME "testchar"
static dev_t mydev;
static struct cdev test_cdev;
//自动分配主次设备号
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
if (retval < 0)
{
printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
goto flag1;
}
printk(KERN_INFO "alloc_chrdev_region success\n");
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev)); //获取我们的主次设备号,用于创建设备文件
1.1.6 中途出错的倒影式错误处理方法
(1) 内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后,后面的步骤就没有进行下去的必要性了。所以就有了倒影式处理错误的方法。
// 第1步:分配主次设备号
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
if (retval < 0)
{
printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
goto flag1;
}
// 第2步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops);
retval = cdev_add(&test_cdev, mydev, MYCNT);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
// 第3步:使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
goto flag3;
// 如果第3步才出错跳转到这里来
flag3:
release_mem_region(GPJ0CON_PA, 4);
// 如果第2步才出错跳转到这里来
flag2:
cdev_del(&test_cdev);
// 如果第1步才出错跳转到这里来
flag1:
// 在这里把第1步做成功的东西给注销掉
unregister_chrdev_region(mydev, MYCNT);
注:使用cdev_alloc,cdev_init的替代(重点) |
二、字符设备驱动注册代码分析
2.1 旧接口register_chrdev
register_chrdev注册函数
=》 __register_chrdev -- 内核级函数
==》 __register_chrdev_region -- 内核级函数
==》cdev_alloc -- 让内核为这个结构体分配内存的。
===》cdev_add -- 向内核里面添加一个驱动,注册驱动。
函数 __register_chrdev_region() 主要执行以下步骤:
- 分配一个新的 char_device_struct 结构,并用 0 填充。
- 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
- 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
- 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
- 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。
2.2 新接口register_chrdev_region & alloc_chrdev_region
register_chrdev_region== 动态分配主次设备号
=》__register_chrdev_region
register_chrdev_region() 函数用于分配指定的设备编号
范围。如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。
alloc_chrdev_region==让内核自动给我们分配设备号
=》__register_chrdev_region
alloc_chrdev_region() 函数用于动态申请设备编号范围,通过指针参数返回实际获得的起始设备编号。
2.3 注销
注销和注册分配字符设备编号范围类似,内核提供了两个注销字符设备编号范围的函数,分别是 unregister_chrdev_region() 和 unregister_chrdev() 。它们都调用__unregister_chrdev_region函数,这个就不分析了。
三、自动创建字符设备驱动的设备文件
(1) 整体流程回顾
(2) 使用mknod创建设备文件的缺点
(3) 能否自动生成和删除设备文件
3.1 解决方案:udev(嵌入式中用的是mdev)
(1) 什么是udev?应用层的一个应用程序
(2) 内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3) 应用层启用udev,内核驱动中使用相应接口
(4) 驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
3.2 内核驱动设备类相关函数
- class_create/class_destroy
- device_create/device_destroy
#include <linux/device.h> //相关的函数包含在这个头文件里面
static dev_t mydev;
static struct class *test_class;
// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
// 给udev,让udev自动创建和删除设备文件
test_class = class_create(THIS_MODULE, "aston_class");
if (IS_ERR(test_class))
return -EINVAL;
// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
// 所以我们这里要的文件名是/dev/test111
device_create(test_class, NULL, mydev, NULL, "test111");
//在注销设备驱动之前
device_destroy(test_class, mydev);
class_destroy(test_class);
四、设备类相关代码分析
4.1 sysfs文件系统
因为udev需要sysfs文件系统的支持(sysfs文件系统只在linux-2.6内核以上才有),所以它存在于Linux-2.6版本之后的内核。udev借助于netlink协议在内核驱动和应用层之间传递信息。当内核中的驱动完成注册和注销时,信息会被传送给应用层的udev,udev便会自动地完成设备文件的创建和删除。
内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类, 内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应 device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
2、class_creat & device_createt
(1) class_creat 树形调用的主要的函数
class_create();
__class_create();
__class_register();
kset_register();
kobject_uevent();
(2) device_createt 树形调用的主要的函数
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...) ------------------- 参数”...”就是可变参数, 更多的时候表示了次设备号。
{
.........
}
例如:此图中 tty 就表示主设备号, 后面的14… 15等表示次设备号
device_createt();
device_create_vargs();
kobject_set_name_vargs();
device_register();
device_add();
kobject_add(); //真正的把设备添加进去了
device_create_file();//下面这些函数都是操作 sysfs 的函数
device_create_sys_dev_entry();
devtmpfs_create_node();
device_add_class_symlinks();
device_add_attrs();
device_pm_add();
kobject_uevent();
device_create_file() 函数创建的就是 dev 目录(sysfs函数实现的),
int device_create_file(struct device *dev,
const struct device_attribute *attr)
{
int error = 0;
if (dev) {
....
error = sysfs_create_file(&dev->kobj, &attr->attr);
}
return error;
}
EXPORT_SYMBOL_GPL(device_create_file);
参数 uevent_attr
static struct device_attribute uevent_attr =
__ATTR(uevent, S_IRUGO | S_IWUSR, show_uevent, store_uevent); -------- 给出的属性, 参数中 show_uevent, store_uevent 表示读和存储。
所以在执行cat dev 读取 dev 这个文件时, 内核调用的就是 show_uevent 函数。
五、静态映射表建立过程分析
5.1 建立映射表的三个关键部分
(1) 主映射表:映射表具体物理地址和虚拟地址的值相关的宏定义
(2) 映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。
在kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数
smdkc110_map_io();
s5p_init_io();
iotable_init(); // 引出io 描述符的概念
经过分析,真正的内核移植时给定的静态映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射
。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系
,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。
注:该部分重点记录分析方法。
// 由上图可以看出,内存管理最小的表是4K。
(3) 开机时调用映射表建立函数
问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?
函数调用层级:
start_kernel();
setup_arch();
paging_init();
devicemaps_init();
if (mdesc->map_io)
mdesc->map_io();
六、动态映射结构体方式操作寄存器
知识回顾:之前的动态映射,每个寄存器地址是单独映射的,要进行多次。
(1) 仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。
实验代码:
typedef struct GPJ0REG
{
volatile unsigned int gpj0con;
volatile unsigned int gpj0dat;
} gpj0_reg_t;
#define GPJ0_REGBASE 0xe0200240 //物理地址
gpj0_reg_t *pGPJ0REG;
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
return -EINVAL;
pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 映射之后用指向结构体的指针来进行操作
// 指针使用->结构体内元素的方式来操作各个寄存器
pGPJ0REG->gpj0con = 0x11111111;
pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5)); // 亮
// 解除映射
iounmap(pGPJ0REG);
release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
七、内核提供的读写寄存器接口
- 内核提供的寄存器读写接口(在不同的架构下,可移植性高)
(1) writel和readl,在3.字符设备驱动上篇已经提过了。
(2) iowrite32和ioread32 - 代码实践
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
#define GPJ0CON_PA 0xe0200240
#define GPJ0DAT_PA 0xe0200244
#define S5P_GPJ0REG(x) (x)
#define S5P_GPJ0CON S5P_GPJ0REG(0)
#define S5P_GPJ0DAT S5P_GPJ0REG(4)
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
static void __iomem *baseaddr; // 寄存器的虚拟地址的基地址
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
return -EINVAL;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
return -EINVAL;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
/************/ 原始的用解引用指针的方法 /***********/
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
/***********/ 使用内部读写接口的方法 /***********/
测试1:用2次ioremap得到的动态映射虚拟地址来操作,测试成功
writel(0x11111111, pGPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT);
测试2:用静态映射的虚拟地址来操作,测试成功
writel(0x11111111, GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
测试3:用1次ioremap映射多个寄存器得到虚拟地址,测试成功
if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
return -EINVAL;
baseaddr = ioremap(GPJ0CON_PA, 8);
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
- 操作寄存器的大致流程
注: 本文参考朱有鹏老师网上学员的学习记录博客,根据自己的理解,进行学习汇总,如有侵权,联系本人及时删除。