0%

【驱动开发】Linux 驱动开发入门(三) 字符驱动框架

字符设备驱动是 Linux 驱动开发中最重要,也是最简单的设备驱动程序,学习字符设备驱动的编写是入门驱动开发的最简单方式。字符设备驱动并不是天马行空,肆意妄为的,恰恰相反,字符设备驱动程序的编写甚至有些地方非常套路化,这主要归功于内核开发者们的努力,让驱动开发只需要调用接口即可。这篇文章,主要关注字符设备驱动中的”套路”,形成一套字符设备驱动的万能框架。

老的方法

模块的入口和出口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit)

注册字符设备驱动

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。 字符设备的注册和注销函数原型:

1
2
3
4
5
6
static inline int register_chrdev(unsigned int major, 
const char *name,
const struct file_operations *fops)

static inline void unregister_chrdev(unsigned int major,
const char *name)

这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。 一般字符设备的注册驱动模块的入口函数 xxx_init 中进行,字符设备的注销驱动模块的出口函数 xxx_exit 中进行。

内存映射

  • 内存映射 在Linux中不能直接访问寄存器,要想要操作寄存器需要完成物理地址到虚拟空间的映射。
1
2
3
4
5
6
7
8
9
10
11
12
#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));
}

返回值: __iomem 是编辑器标记,指向映射后的虚拟空间首地址。 建立映射:**映射的虚拟地址 = ioremap(IO内存起始地址,映射长度)**; 一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。

  • 解除映射
1
void iounmap (volatile void __iomem *addr)

应用层和内核层传递数据

应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助下面的这两个函数

1
2
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

to:目标地址 from:源地址 n:将要拷贝数据的字节数 返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数

字符设备最基本框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#define CHRDEVBASE_MAJOR 200			//手动设置主设备号
#define CHRDEVBASE_NAME "chrdevbase" //设备名称
//内核缓存区
static char readbuf[100]; //读数据缓存
static char writebuf[100]; //写数据缓存
static char kerneldata[] = {"kernel data!"}; //测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址
static void __iomem *GPIO_TEST; // __iomem 类型的指针,指向映射后的虚拟空间首地址
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
return 0;
}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt)
{
int retvalue = 0;
unsigned char databuf[1];
// 读取硬件寄存器
#if 0
//读取寄存器状态
databuf[0] = readl(GPIO_TEST);
retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else
//测试数据拷贝到读数据缓存中
memcpy(readbuf , kerneldata , sizeof(kerneldata));
//内核中数据(读缓存)拷贝到用户空间
retvalue = copy_to_user(buf , readbuf , cnt);
#endif

if(retvalue == 0) printk("kernel senddate ok!\n");
else printk("kernel senddate failed!\n");
return 0;
}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt)
{
int retvalue = 0;
//写硬件寄存器
#if 0
writel(buf[0],GPIO_TEST);
//写内核缓存
#else
//用户数据拷贝到内核空间(写缓存)
retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
else printk("kernel recevdate failed!");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
int retvalue = 0;
//寄存器物理映射,物理地址映射到虚拟地址指针
GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
//注册字符设备驱动
retvalue = register_chrdev(CHRDEVBASE_MAJOR, //主设备号
CHRDEVBASE_NAME, //设备名称
&chrdevbase_fops); //设备操作函数集合

if(retvalue < 0) printk("chrdevbase driver register failed\n");
printk("chrdevbase_init()\r\n");
return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
//解除寄存器映射
iounmap(GPIO_TEST);
//注消字符设备驱动
unregister_chrdev(CHRDEVBASE_MAJOR , //主设备号
CHRDEVBASE_NAME); //设备名称
printk("chrdevbase_exit()\r\n");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPL");//GPL模块许可证
MODULE_AUTHOR("huashansun");//作者信息

创建驱动节点文件

加载驱动模块后,需手动创建驱动节点文件

1
mknod /dev/chrdevbase c 200 0
  • 其中“mknod”是创建节点命令,
  • “/dev/chrdevbase”是要创建的节点文件,
  • “c”表示这是个字符设备,
  • “200”是设备的主设备号,
  • “0”是设备的次设备号。
  • 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看

新的方法

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

在 Linux 下通过 udev(用户空间程序)实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。

设备文件系统

设备文件系统有devfs,mdev,udev这三种

  1. devfs, 一个基于内核的动态设备文件系统
  • devfs 缺点(过时原因)
    • 不确定的设备映射
    • 没有足够的主/辅设备号
    • /dev目录下文件太多
    • 内核内存使用
  1. udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
  • udev 和 devfs 的区别
    • 采用 devfs,当一个并不存在的 /dev 节点被打开的时候,devfs 能自动加载对应的驱动
    • udev 的 Linux 应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
    • 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
  1. mdev,是 udev 的简化版本,是 busybox 中所带的程序, 适合用在嵌入式系统

申请设备号

上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号

  • 动态申请设备号

    1
    2
    3
    4
    int alloc_chrdev_region(dev_t *dev, 
    unsigned baseminor,
    unsigned count,
    const char *name)

    dev:保存申请到的设备号。 baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0 count: 要申请的设备号数量。 name:设备名字。

  • 静态申请设备号

    1
    int register_chrdev_region(dev_t from, unsigned count, const char *name);

    from - 要申请的起始设备号
    count - 设备号个数
    name - 设备号在内核中的名称 返回0申请成功,否则失败

  • 释放设备号

    1
    void unregister_chrdev_region(dev_t from, unsigned count)

    from:要释放的设备号。 count: 表示从 from 开始,要释放的设备号数量。

  • 申请设备号模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //创建设备号 
    if (newchrled.major) //定义了设备号就静态申请
    {
    newchrled.devid = MKDEV(newchrled.major, 0);
    register_chrdev_region(newchrled.devid,
    NEWCHRLED_CNT,
    NEWCHRLED_NAME);
    }
    else //没有定义设备号就动态申请
    {

    alloc_chrdev_region(&newchrled.devid,
    0,
    NEWCHRLED_CNT,
    NEWCHRLED_NAME);//申请设备号
    newchrled.major = MAJOR(newchrled.devid); //获取分配号的主设备号
    newchrled.minor = MINOR(newchrled.devid); // 获取分配号的次设备号
    }

注册字符设备

在 Linux 中使用 cdev 结构体表示一个字符设备

1
2
3
4
5
6
7
8
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结构体变量

    1
    2
    void cdev_init(struct cdev *cdev, 
    const struct file_operations *fops);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct cdev testcdev;
    //设备操作函数
    static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    //其他具体的初始项
    };
    testcdev.owner = THIS_MODULE;
    //初始化 cdev 结构体变量
    cdev_init(&testcdev, &test_fops);
  • 将设备添加到内核

    cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备

    将cdev添加到内核同时绑定设备号。 其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev 函数完成者两步操作

    1
    int cdev_add(struct cdev *p, dev_t dev, unsigned count)

    p - 要添加的cdev结构
    dev - 绑定的起始设备号
    count - 设备号个数

    1
    cdev_add(&testcdev, devid, 1); //添加字符设备
  • 将设备从内核注销 卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

    1
    void cdev_del(struct cdev *p);

    p - 要添加的cdev结构

    1
    cdev_del(&testcdev); //删除 cdev

自动创建设备节点

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。

  • 创建一个class类

    1
    struct class *class_create(struct module *owner, const char *name);
    • class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
    • 设备类名对应 /sys/class 目录的子目录名。
    • 返回值是个指向结构体 class 的指针,也就是创建的类。
  • 删除一个class类

    1
    void class_destroy(struct class *cls); // cls要删除的类
  • 创建设备 还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。 成功会在 /dev 目录下生成设备文件。

    1
    2
    3
    4
    5
    struct device *device_create(struct class *class,
    struct device *parent,
    dev_t devt,
    void *drvdata,
    const char *fmt, ...)

    *class——设备类指针, *parent——父设备指针, devt——设备号, *drvdata——额外数据, *fmt——设备文件名

  • 删除设备 卸载驱动的时候需要删除掉创建的设备

    1
    void device_destroy(struct class *class, dev_t devt);

    class——设备所处的类 devt——设备号

文件私有数据

  • 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),
  • 一个设备的所有属性信息将其做成一个结构体,
  • 编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
  • 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchrled; /* led设备 */

/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
return 0;
}

新字符设备驱动程序框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#define NEWCHR_CNT 1
#define NEWCHR_NAME "NEWCHR"
//内核缓存区
static char readbuf[100]; //读数据缓存
static char writebuf[100]; //写数据缓存
static char kerneldata[] = {"kernel data!"}; //测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址
static void __iomem *GPIO_TEST; // __iomem 类型的指针,指向映射后的虚拟空间首地址

/* newchr设备结构体 */
struct newchr_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};

struct newchrled_dev newchr; /* newchr设备 */

//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchr; /* 设置私有数据 */
return 0;
}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt)
{
int retvalue = 0;
unsigned char databuf[1];
//读取私有数据
struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
// 读取硬件寄存器
#if 0
//读取寄存器状态
databuf[0] = readl(GPIO_TEST);
retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else
//测试数据拷贝到读数据缓存中
memcpy(readbuf , kerneldata , sizeof(kerneldata));
//内核中数据(读缓存)拷贝到用户空间
retvalue = copy_to_user(buf , readbuf , cnt);
#endif

if(retvalue == 0) printk("kernel senddate ok!\n");
else printk("kernel senddate failed!\n");
return 0;
}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt)
{
int retvalue = 0;
//读取私有数据
struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
//写硬件寄存器
#if 0
writel(buf[0],GPIO_TEST);
//写内核缓存
#else
//用户数据拷贝到内核空间(写缓存)
retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
else printk("kernel recevdate failed!");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
int retvalue = 0;
//寄存器物理映射,物理地址映射到虚拟地址指针
GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);

//申请设备号
if(newchr.major) //静态申请
{
newchr.devid = MKDEV(newchr.major , 0);
register_chrdev_region(newchr.devid, NEWCHR_CNT,NEWCHR_NAME);
}else //动态申请
{
alloc_chrdev_region(&newchr.devid , 0 , NEWCHR_CNT , NEWCHR_NAME);
newchr.major = MAJOR(newchr.devid);
newchr.minor = MINOR(newchr.devid);
}
printk("newche major=%d,minor=%d\r\n",newchr.major , newchr.minor);

//字符串设备初始化、注册添加到内核
newchr.cdev.owner = THIS_MODULE;
cdev_init(&newchr.cdev , &newchr_fops);
cdev_add(&newchr.cdev , newchr.devid ,NEWCHR_LED_CNT);
//创建设备类
newchr.class = class_create(THIS_MODULE , NEWCHR_NAME);
if(IS_ERR(newchr.class))
{
return PTR_ERR(newchr.class);
}
//创建类的实例化设备 ,dev下面创建文件
newchr.device = device_create(newchr.class , NULL , newchr.devid ,NULL ,NEWCHR_NAME);
if(IS_ERR(newchr.device))
{
return PTR_ERR(newchr.device);
}
return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
//解除寄存器映射
iounmap(GPIO_TEST);
//删除cdev字符串设备
cdev_del(&newchr.cdev);
//释放设备号
unregister_chrdev_region(newchr.devid , NEWCHR_CNT);
//具体设备注销
device_destroy(newchr.class, newchr.devid);
//类注销
class_destroy(newchr.class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPL");//GPL模块许可证
MODULE_AUTHOR("huashan sun");//作者信息