声明
本博客所记录的关于正点原子i.MX6ULL开发板的学习笔记,(内容参照正点原子I.MX6U嵌入式linux驱动开发指南,可在正点原子官方获取正点原子Linux开发板 — 正点原子资料下载中心 1.0.0 文档),旨在如实记录我在学校学习该开发板过程中所遭遇的各类问题以及详细的解决办法。其初衷纯粹是为了个人知识梳理、学习总结以及日后回顾查阅方便,同时也期望能为同样在学习这款开发板的同学或爱好者提供一些解决问题的思路和参考。我尽力保证内容的准确性和可靠性,但由于个人知识水平和实践经验有限,若存在错误或不严谨之处,恳请各位读者批评指正。
责任声明:虽然我力求提供有效的问题解决办法,但由于开发板使用环境、硬件差异、软件版本等多种因素的影响,我的笔记内容不一定适用于所有情况。对于因参考本笔记而导致的任何直接或间接损失,我不承担任何法律责任。使用本笔记内容的读者应自行承担相关风险,并在必要时寻求专业技术支持。
1.自动分配和释放设备号
要使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一般都是一个;参数 name 是设备名字。
注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是register_chrdev_region 函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
设备号分配示例代码如下:
/* 1、创建设备号 */
int major;
int minor;
if (major) { /* 如果定义了设备号,就是用MKDEV构建完整的设备号,驱动次设备号一般选择 0 */
devid = MKDEV(major, 0);
register_chrdev_region(devid, 申请个数, 设备名);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, 申请个数, 设备名); /* 申请设备号 */
major = MAJOR(devid); /* 获取分配号的主设备号 */
minor = MINOR(devid); /* 获取分配号的次设备号 */
}
printk("major=%d,minor=%d\r\n", major, minor);
如果要注销设备号的话,使用如下代码即可:
unregister_chrdev_region(devid, 申请个数);
2. 自动创建设备节点
2.1 mdev 机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。使用 busybox 构建根文件系统的时候, busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理,在/etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
2.2 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,首先要创建一个 class 类, class 是个结构体,定义在文件include/linux/device.h 里面。 class_create 是类创建函数, class_create 是个宏定义,内容如下:
#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,
struct lock_class_key *key)
将宏 class_create 展开以后内容如下:
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
参数 cls 就是要删除的类。
2.3 创建设备
还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备, device_create 函数原型如下:
struct device *device_create(
struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...
);
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:
void device_destroy(struct class *class, dev_t devt)
参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */
/* 驱动入口函数 */
static int __init led_init(void)
{
/* 动态申请设备号 */
if (alloc_chrdev_region(&devid, 0, 1, "xxx") < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return -EBUSY;
}
/* 创建类 */
class = class_create(THIS_MODULE, "xxx");
if (IS_ERR(class)) {
unregister_chrdev_region(devid, 1);
return PTR_ERR(class);
}
/* 创建设备 */
device = device_create(class, NULL, devid, NULL, "xxx");
if (IS_ERR(device)) {
class_destroy(class);
unregister_chrdev_region(devid, 1);
return PTR_ERR(device);
}
return 0;
}
/* 驱动出口函数 */
static void __exit led_exit(void)
{
/* 删除设备 */
device_destroy(class, devid);
/* 删除类 */
class_destroy(class);
/* 释放设备号 */
unregister_chrdev_region(devid, 1);
}
2.4 设置私有数据
在编写驱动的时候你可以将这些属性全部写成变量的形式,然后将一个设备的所有属性信息将其做成一个结构体。编写open函数的时候将设备结构体作为私有数据添加到设备文件中。
/* 设备结构体 */
struct test_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev 结构体 */
struct class *class; /* 设备类 */
struct device *device; /* 设备节点 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct test_dev testdev; /* 全局设备实例 */
/* open 函数实现 */
static int test_open(struct inode *inode, struct file *filp)
{
/* 将设备结构体设置为文件私有数据 */
filp->private_data = &testdev;
return 0;
}
3.新的字符设备注册方法
3.1 字符设备结构
在 Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中的定义如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
在 cdev 中有两个重要的成员变量: ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
3.2 cdev_init 函数
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化, cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。使用 cdev_init 函数初始化 cdev 变量的示例代码如下:
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
3.3 cdev_add 函数
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
cdev_add(&testcdev, devid, 1);
3.4 cdev_del 函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备, cdev_del函数原型如下:
void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。如果要删除字符设备
cdev_del(&testcdev); /* 删除 cdev */