linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程
linux基础篇(二)——静态和动态链接
ARM裸机篇(一)——i.MX6ULL介绍
ARM裸机篇(二)——i.MX6ULL启动过程
ARM裸机篇(三)——i.MX6ULL第一个裸机程序
ARM裸机篇(四)——重定位和地址无关码
ARM裸机篇(五)——异常和中断
linux系统移植篇(一)—— linux系统组成
linux系统移植篇(二)—— Uboot使用介绍
linux系统移植篇(三)—— Linux 内核使用介绍
linux系统移植篇(四)—— 根文件系统使用介绍
linux驱动开发篇(一)—— Linux 内核模块介绍
linux驱动开发篇(二)—— 字符设备驱动框架
linux驱动开发篇(三)—— 总线设备驱动模型
linux驱动开发篇(四)—— platform平台设备驱动
linux驱动开发篇(五)—— linux设备驱动面向对象的编程思想
linux驱动开发篇(六)—— 设备树的引入
Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations),在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的 cdev,当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。
在linux内核中,使用cdev结构体描述一个字符设备,cdev结构体定义如下:
struct cdev {
struct kobject kobj;/*内嵌的内核对象,通过它将设备统一加入到“Linux 设备驱动模型”中*/
struct module *owner;/*字符设备驱动程序所在的内核模块对象的指针*/
const struct file_operations *ops;/*文件操作结构体*/
struct list_head list;
dev_t dev;/*设备号*/
unsigned int count;
};
Linux内核提供了一组函数用来操作cdev结构体, 声明在文件
void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
void cd_forget(struct inode *);
(1)cdev_init函数用来初始化cdev结构体的成员,并建立cdev和file_operations之间的连接:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;/*将传入的文件操作结构体指针赋值给cdev的ops*/
}
(2)cdev_alloc函数用来动态申请一个cdev内存:
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
(3)cdev_add函数用来向系统中添加一个cdev,完成字符设备的注册:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
(4)cdev_del函数用于从系统中删除一个cdev,完成字符设备的注销:
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
file_operations 结构体中成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行linux的open()、write()、read()、close()等系统调用时最终被内核调用,如图所示linux的调用关系:
驱动程序必须提供一些必要的函数,来与open、read、write、close这些函数相对应,file_operations结构体中定义了Linux内核驱动操作函数的集合,在Linux内核文件include/linux/fs.h中定义,代码如下:
struct file_operations {
struct module *owner;
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(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 *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
在 linux 中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。
cdev 结构体的dev成员被内核用来记录设备号,dev_t其实就是一个u32类型,在include/linux/types.h文件中,定义如下:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
主设备号占据高12位、范围是0-4095,次设备号占据低20位。
在文件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))
使用MAJOR和MINOR宏可以得到主设备号和次设备号。
有一些设备号已经被linux内核开发者给分配了,如果系统中已经使用了的设备号,可以使用下面的命令查看,那么我们就不能强行使用了,否则会造成冲突。
cat /proc/devices
静态分配设备号简单粗暴,但是很容易造成冲突。
Linux社区推荐使用动态分配设备号,在调用cdev_add()函数注册字符设备之前先申请一个设备号,系统会自动分配一个没有使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。
(1)申请设备号API
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
(2)释放设备号API
void unregister_chrdev_region(dev_t from, unsigned count);
应用程序要实现对设备的操作,还需要一个在 /dev 目录下的设备节点,设备节点是 Linux 内核对设备的抽象,一个设备节点就是一个文件。Linux 中设备节点是通过“mknod”命令来创建的。应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。
加载驱动到内核后,可以通过mknod命令,根据设备号手动创建一个设备节点:
mknod <设备目录> <设备类型> <主设备号> <次设备号>
比如:
mknod /dev/hello_drv c 200 0
这样就会在/dev目录下创建一个名为hello_drv的设备节点。
Linux内核提供了自动创建设备节点的机制,具体使用如下,这些API在include/linux/device.h文件中声明。
(1)创建一个设备类
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
该函数会在/sys/class目录下创建一个该类。
(2)创建一个设备并将其注册到文件系统:
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...);
该函数会在/dev目录下创建该设备节点。
(3)删除设备
void device_destroy(struct class *cls, dev_t devt);
(4)删除类
extern void class_destroy(struct class *cls);
实际上,在 Linux 上写驱动程序,都是做一些“填空题”。因为 Linux 给我们提供了一个基本的框架,我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。
字符设备驱动框架架构:
我们创建一个字符设备的时候,首先要的到一个设备号,分配设备号的途径有静态分配和动态分配;拿到设备的唯一 ID,我们需要实现 file_operation 并保存到 cdev 中,实现 cdev 的初始化;然后我们需要将我们所做的工作告诉内核,使用 cdev_add() 注册 cdev;最后我们还需要创建设备节点,以便我们后面调用 file_operation 接口。
注销设备时我们需释放内核中的 cdev,归还申请的设备号,删除创建的设备节点。
以open函数为例,当用户在C语言程序中调用open函数时,调用关系链如下图所示:
用户空间使用 open() 系统调用函数打开一个字符设备时 (int fd = open(“dev/xxx” , O_RDWR)) 大致有以下过程:
结合前面所有的知识点,首先,字符设备驱动程序是以内核模块的形式存在的,因此,使用内核模块的程序框架是毫无疑问的。紧接着,我们要向系统注册一个新的字符设备,需要这几样东西:字符设备结构体 cdev,设备编号 devno,以及最最最重要的操作方式结构体 file_operations。
#include
#include
#include
#include
#include
#define DEV_NAME "EmbedCharDev"
#define DEV_CNT (1)
#define BUFF_SIZE 128
//定义字符设备的设备号
static dev_t devno;
//定义字符设备结构体chr_dev
static struct cdev chr_dev;
//数据缓冲区
static char vbuf[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
static struct file_operations chr_dev_fops =
{
.owner = THIS_MODULE,
.open = chr_dev_open,
.release = chr_dev_release,
.write = chr_dev_write,
.read = chr_dev_read,
};
static int chr_dev_open(struct inode *inode, struct file *filp)
{
printk("\nopen\n");
return 0;
}
static int chr_dev_release(struct inode *inode, struct file *filp)
{
printk("\nrelease\n");
return 0;
}
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
if(p > BUFF_SIZE)
return 0;
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_from_user(vbuf, buf, tmp);
*ppos += tmp;
return tmp;
}
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
static int i = 0;
i++;
if(p >= BUFF_SIZE)
return 0;
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}
static int __init chrdev_init(void)
{
int ret = 0;
printk("chrdev init\n");
//第一步
//采用动态分配的方式,获取设备编号,次设备号为0,
//设备名称为EmbedCharDev,可通过命令cat /proc/devices查看
//DEV_CNT为1,当前只申请一个设备编号
ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
if(ret < 0){
printk("fail to alloc devno\n");
goto alloc_err;
}
//第二步
//关联字符设备结构体cdev与文件操作结构体file_operations
cdev_init(&chr_dev, &chr_dev_fops);
//第三步
//添加设备至cdev_map散列表中
ret = cdev_add(&chr_dev, devno, DEV_CNT);
if(ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
return 0;
add_err:
//添加设备失败时,需要注销设备号
unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
return ret;
}
module_init(chrdev_init);
static void __exit chrdev_exit(void)
{
printk("chrdev exit\n");
unregister_chrdev_region(devno, DEV_CNT);
cdev_del(&chr_dev);
}
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
#include
#include
#include
#include
char *wbuf = "Hello World\n";
char rbuf[128];
int main(void)
{
printf("EmbedCharDev test\n");
//打开文件
int fd = open("/dev/chrdev", O_RDWR);
//写入数据
write(fd, wbuf, strlen(wbuf));
//写入完毕,关闭文件
close(fd);
//打开文件
fd = open("/dev/chrdev", O_RDWR);
//读取文件内容
read(fd, rbuf, 128);
//打印读取的内容
printf("The content : %s", rbuf);
//读取完毕,关闭文件
close(fd);
return 0;
}
makefile 文件:
KERNEL_DIR=../../ebf_linux_kernel
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
obj-m := chrdev.o
out = chrdev_test
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE)gcc -o $(out) main.c
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm $(out)
编译成功后,实验目录下会生成两个名为” chrdev.ko”驱动模块文件和” chrdev_test”测试程序。
加载之后查看系统当前使用的设备号:
以看到我们注册的字符设备 EmbedCharDev 的主设备号为 244。
mknod /dev/chrdev c 244 0
当我们不需要该内核模块的时候,我们可以执行以下命令:
sudo rmmod chrdev.ko
sudo rm /dev/chrdev
编写驱动的套路大致流程可以总结如下: