《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》 第十九章 新字符设备驱动实验​

时间:2023-01-19 12:55:21

新字符设备驱动实验​

经过前两章实验的实战操作,我们已经掌握了Linux字符设备驱动开发的基本步骤,字符设备驱动开发重点是使用register_chrdev函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev函数注销字符设备,驱动模块加载成功以后还需要手动使用mknod命令创建设备节点。register_chrdevunregister_chrdev这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。本节我们就来学习一下如何编写新字符设备驱动,并且在驱动模块加载的时候自动创建设备节点文件。



新字符设备驱动原理

分配和释放设备号

使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:

①、需要我们事先确定好哪些主设备号没有使用。

②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~1048575(2^20-1)这个区间的次设备号就全部都被LED一个设备分走了。这样太浪费次设备号了!一个LED设备肯定只能有一个主设备号,一个次设备号。

解决这两个问题最好的方法就是要使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。这个就是我们在17.3.2小节讲解的设备号的分配,如果没有指定设备号的话就使用如下函数来申请设备号:

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)

新字符设备驱动下,设备号分配示例代码如下:

示例代码19.1.1.1 新字符设备驱动下设备号分配

1 int major;   /* 主设备号 */​
2 int minor; /* 次设备号 */​
3 dev_t devid; /* 设备号 */​
4 ​
5 if (major) { /* 定义了主设备号 */​
6 devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择0 */​
7 register_chrdev_region(devid, 1, "test");​
8 } else { /* 没有定义设备号 */​
9 alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */​
10 major = MAJOR(devid); /* 获取分配号的主设备号 */​
11 minor = MINOR(devid); /* 获取分配号的次设备号 */​
12 }

第1~3行,定义了主/次设备号变量major和minor,以及设备号变量devid。

第5行,判断主设备号major是否有效,在Linux驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择0,这算是Linux驱动开发中约定俗成的一种规定了。

第6行,如果major有效的话就使用MKDEV来构建设备号,次设备号选择0。

第7行,使用register_chrdev_region函数来注册设备号。

第9~11行,如果major无效,那就表示没有给定设备号。此时就要使用alloc_chrdev_region函数来申请设备号。设备号申请成功以后使用MAJOR和MINOR来提取出主设备号和次设备号,当然了,第10和11行提取主设备号和次设备号的代码可以不要。

如果要注销设备号的话,使用如下代码即可:

示例代码19.1.1.2 cdev结构体

1 unregister_chrdev_region(devid, 1);  /* 注销设备号 */

注销设备号的代码很简单。

新的字符设备注册方法

1、字符设备结构

在Linux中使用cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h文件中的定义如下:

示例代码19.1.2.1 cdev结构体

1 struct cdev {​
2 struct kobject kobj;​
3 struct module *owner;​
4 const struct file_operations *ops;​
5 struct list_head list;​
6 dev_t dev;​
7 unsigned int count;​
8 };

在cdev中有两个重要的成员变量:ops和dev,这两个就是字符设备文件操作函数集合file_operations以及设备号dev_t。编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就表示一个字符设备,如下所示:

struct cdev test_cdev;

2cdev_init函数

定义好cdev变量以后就要使用cdev_init函数对其进行初始化,cdev_init函数原型如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

参数cdev就是要初始化的cdev结构体变量,参数fops就是字符设备文件操作函数集合。使用cdev_init函数初始化cdev变量的示例代码如下:

示例代码19.1.2.2 cdev_init函数使用示例代码

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

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是要添加的设备数量。完善示例代码19.1.2.3,加入cdev_add函数,内容如下所示:

示例代码19.1.2.3 cdev_add函数使用示例

1 struct cdev testcdev;​
2 ​
3 /* 设备操作函数 */​
4 static struct file_operations test_fops = {​
5 .owner = THIS_MODULE,​
6 /* 其他具体的初始项 */​
7 };​
8 ​
9 testcdev.owner = THIS_MODULE;​
10 cdev_init(&testcdev, &test_fops); /* 初始化cdev结构体变量 */​
11 cdev_add(&testcdev, devid, 1); /* 添加字符设备 */

示例代码19.1.2.3就是新的注册字符设备代码段,Linux内核中大量的字符设备驱动都是采用这种方法向Linux内核添加字符设备。如果在加上示例代码19.1.1.1中分配设备号的程序,那么它们一起实现的就是函数register_chrdev的功能。

3、cdev_del函数

卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备,cdev_del函数原型如下:

void cdev_del(struct cdev *p)

参数p就是要删除的字符设备。如果要删除字符设备,参考如下代码:

示例代码19.1.2.4 cdev_del函数使用示例

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

cdev_delunregister_chrdev_region这两个函数合起来的功能相当于unregister_chrdev函数。

自动创建设备节点

在前面的Linux驱动实验中,当我们使用modprobe加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。

mdev机制

udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除,比如使用modprobe命令成功加载驱动模块以后就自动在/dev目录下创建对应的设备节点文件,使用rmmod命令卸载驱动模块以后就删除掉/dev目录下的设备节点文件。使用busybox构建根文件系统的时候(我们在petalinux中编译得到的根文件系统其实就是busybox构建出来的),busybox会创建一个udev的简化版本—mdev,所以在嵌入式Linux中我们使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理。

关于udev或mdev更加详细的工作原理我们不去讨论,本章我们重点来学习设备文件节点的自动创建与删除。

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个class类(class的概念这里暂时不去细说,大家可以先简答地理解为设备类即可,就是某个设备属于某个类,后面我们会详细的讲),class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义,内容如下:

示例代码19.2.2.1 class_create函数

1 #define class_create(owner, name) \​
2 ({ \​
3 static struct lock_class_key __key; \​
4 __class_create(owner, name, &__key); \​
5 })​
6 ​
7 struct class *__class_create(struct module *owner, const char *name,​
8 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就是要删除的类。

创建设备

上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用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)

参数classs是要删除的设备所处的类,参数devt是要删除的设备号。

参考示例

在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:

示例代码19.2.4.1 创建/删除类/设备参考代码

1 struct class *class;  /* 类 */ ​
2 struct device *device; /* 设备 */​
3 dev_t devid; /* 设备号 */ ​
4 ​
5 /* 驱动入口函数 */ ​
6 static int __init xxx_init(void)​
7 {​
8 /* 创建类 */​
9 class = class_create(THIS_MODULE, "xxx");​
10 /* 创建设备 */​
11 device = device_create(class, NULL, devid, NULL, "xxx");​
12 return 0;​
13 }​
14 ​
15 /* 驱动出口函数 */​
16 static void __exit led_exit(void)​
17 {​
18 /* 删除设备 */​
19 device_destroy(newchrled.class, newchrled.devid);​
20 /* 删除类 */​
21 class_destroy(newchrled.class);​
22 }​
23 ​
24 module_init(led_init);​
25 module_exit(led_exit);

设置文件私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示:

示例代码19.3.1 变量形式的设备属性

dev_t devid;    /* 设备号 */​
struct cdev cdev; /* cdev */​
struct class *class; /* 类 */​
struct device *device; /* 设备 */​
int major; /* 主设备号 */​
int minor; /* 次设备号 */

这样写肯定没有问题,但是这样写不专业!对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示:

示例代码23.3.2 设备结构体作为私有数据

/* 设备结构体 */​
1 struct test_dev{​
2 dev_t devid; /* 设备号 */​
3 struct cdev cdev; /* cdev */​
4 struct class *class; /* 类 */​
5 struct device *device; /* 设备 */​
6 int major; /* 主设备号 */​
7 int minor; /* 次设备号 */​
8 };​
9 ​
10 struct test_dev testdev;​
11 ​
12 /* open函数 */​
13 static int test_open(struct inode *inode, struct file *filp)​
14 {​
15 filp->private_data = &testdev; /* 设置私有数据 */​
16 return 0;​
17 }

在open函数里面设置好私有数据以后,在write、read、close等函数中直接读取private_data即可得到设备结构体。

硬件原理图分析

本章实验硬件原理图参考18.3小节即可!

实验程序编写

本章实验在上一章实验的基础上完成,重点是使用了新的字符设备驱动、设置了文件私有数据、添加了自动创建设备节点相关内容。

LED灯驱动程序编写

在drivers目录下新建名为“3_newchrled”的文件夹,在3_newchrled目录下新建newchrled.c文件,里面输入如下内容:

示例代码19.5.1.1 newchrled.c

1 /***************************************************************​
2 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.​
3文件名​
4作者 : 邓涛​
5版本​
6描述 : ZYNQ LED驱动文件。​
7其他 : 无​
8论坛​
9日志 : 初版V1.0 2019/1/30 邓涛创建​
10 ***************************************************************/​
11 ​
12 #include <linux/types.h>​
13 #include <linux/kernel.h>​
14 #include <linux/delay.h>​
15 #include <linux/ide.h>​
16 #include <linux/init.h>​
17 #include <linux/module.h>​
18 #include <linux/errno.h>​
19 #include <linux/gpio.h>​
20 /*#include <asm/mach/map.h>*/​
21 #include <asm/uaccess.h>​
22 #include <asm/io.h>​
23 #include <linux/cdev.h>​
24 ​
25 #define NEWCHRLED_CNT 1 /* 设备号个数 */​
26 #define NEWCHRLED_NAME "newchrled" /* 名字 */​
27 ​
28 /* ​
29相关寄存器地址定义​
30 */​
31 #define ZYNQ_GPIO_REG_BASE 0xFF0A0000​
32 #define DATA_OFFSET 0x00000044​
33 #define DIRM_OFFSET 0x00000244​
34 #define OUTEN_OFFSET 0x00000248​
35 #define INTDIS_OFFSET 0x00000254​
36 #define APER_CLK_CTRL 0xFF5E00AC​
37 ​
38 /* 映射后的寄存器虚拟地址指针 */​
39 static void __iomem *data_addr;​
40 static void __iomem *dirm_addr;​
41 static void __iomem *outen_addr;​
42 static void __iomem *intdis_addr;​
43 static void __iomem *aper_clk_ctrl_addr;​
44 ​
45 /* newchrled设备结构体 */​
46 struct newchrled_dev {​
47 dev_t devid; /* 设备号 */​
48 struct cdev cdev; /* cdev */​
49 struct class *class; /* 类 */​
50 struct device *device; /* 设备 */​
51 int major; /* 主设备号 */​
52 int minor; /* 次设备号 */​
53 };​
54 ​
55 static struct newchrled_dev newchrled; /* led设备 */​
56 ​
57 /*​
58打开设备​
59传递给驱动的inode​
60设备文件,file结构体有个叫做private_data的成员变量​
61一般在open的时候将private_data指向设备结构体。​
62成功;其他 失败​
63 */​
64 static int led_open(struct inode *inode, struct file *filp)​
65 {​
66 filp->private_data = &newchrled; /* 设置私有数据 */​
67 return 0;​
68 }​
69 ​
70 /*​
71从设备读取数据 ​
72 要打开的设备文件(文件描述符)​
73返回给用户空间的数据缓冲区​
74要读取的数据长度​
75相对于文件首地址的偏移​
76读取的字节数,如果为负值,表示读取失败​
77 */​
78 static ssize_t led_read(struct file *filp, char __user *buf,​
79 size_t cnt, loff_t *offt)​
80 {​
81 return 0;​
82 }​
83 ​
84 /*​
85向设备写数据 ​
86设备文件,表示打开的文件描述符​
87要写给设备写入的数据​
88要写入的数据长度​
89相对于文件首地址的偏移​
90写入的字节数,如果为负值,表示写入失败​
91 */​
92 static ssize_t led_write(struct file *filp, const char __user *buf,​
93 size_t cnt, loff_t *offt)​
94 {​
95 int ret;​
96 int val;​
97 char kern_buf[1];​
98 ​
99 ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据​
100 if(0 > ret) {​
101 printk(KERN_ERR "kernel write failed!\r\n");​
102 return -EFAULT;​
103 }​
104 ​
105 val = readl(data_addr);​
106 if (0 == kern_buf[0])​
107 val &= ~(0x1U << 12); // 如果传递过来的数据是0则关闭led​
108 else if (1 == kern_buf[0])​
109 val |= (0x1U << 12); // 如果传递过来的数据是1则点亮led​
110 ​
111 writel(val, data_addr);​
112 return 0;​
113 }​
114 ​
115 /*​
116关闭/释放设备​
117 要关闭的设备文件(文件描述符)​
118成功;其他 失败​
119 */​
120 static int led_release(struct inode *inode, struct file *filp)​
121 {​
122 return 0;​
123 }​
124 ​
125 static inline void led_ioremap(void)​
126 {​
127 data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);​
128 dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);​
129 outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);​
130 intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);​
131 aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);​
132 }​
133 ​
134 static inline void led_iounmap(void)​
135 {​
136 iounmap(data_addr);​
137 iounmap(dirm_addr);​
138 iounmap(outen_addr);​
139 iounmap(intdis_addr);​
140 iounmap(aper_clk_ctrl_addr);​
141 }​
142 ​
143 /* 设备操作函数 */​
144 static struct file_operations newchrled_fops = {​
145 .owner = THIS_MODULE,​
146 .open = led_open,​
147 .read = led_read,​
148 .write = led_write,​
149 .release = led_release,​
150 };​
151 ​
152 static int __init led_init(void)​
153 {​
154 u32 val;​
155 int ret;​
156 ​
157 /* 1.寄存器地址映射 */​
158 led_ioremap();​
159 ​
160 /* 2.使能GPIO时钟 */​
161 val = readl(aper_clk_ctrl_addr);​
162 val |= (0x1U << 24);​
163 writel(val, aper_clk_ctrl_addr);​
164 ​
165 /* 3.关闭中断功能 */​
166 val |= (0x1U << 12);​
167 writel(val, intdis_addr);​
168 ​
169 /* 4.设置GPIO为输出功能 */​
170 val = readl(dirm_addr);​
171 val |= (0x1U << 12);​
172 writel(val, dirm_addr);​
173 ​
174 /* 5.使能GPIO输出功能 */​
175 val = readl(outen_addr);​
176 val |= (0x1U << 12);​
177 writel(val, outen_addr);​
178 ​
179 /* 6.默认关闭LED */​
180 val = readl(data_addr);​
181 val &= ~(0x1U << 12);​
182 writel(val, data_addr);​
183 ​
184 /* 7.注册字符设备驱动 */​
185 /* 创建设备号 */​
186 if (newchrled.major) {​
187 newchrled.devid = MKDEV(newchrled.major, 0);​
188 ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);​
189 if (ret)​
190 goto out1;​
191 } else {​
192 ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);​
193 if (ret)​
194 goto out1;​
195 ​
196 newchrled.major = MAJOR(newchrled.devid);​
197 newchrled.minor = MINOR(newchrled.devid);​
198 }​
199 ​
200 printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);​
201 ​
202 /* 初始化cdev */​
203 newchrled.cdev.owner = THIS_MODULE;​
204 cdev_init(&newchrled.cdev, &newchrled_fops);​
205 ​
206 /* 添加一个cdev */​
207 ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);​
208 if (ret)​
209 goto out2;​
210 ​
211 /* 创建类 */​
212 newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);​
213 if (IS_ERR(newchrled.class)) {​
214 ret = PTR_ERR(newchrled.class);​
215 goto out3;​
216 }​
217 ​
218 /* 创建设备 */​
219 newchrled.device = device_create(newchrled.class, NULL,​
220 newchrled.devid, NULL, NEWCHRLED_NAME);​
221 if (IS_ERR(newchrled.device)) {​
222 ret = PTR_ERR(newchrled.device);​
223 goto out4;​
224 }​
225 ​
226 return 0;​
227 ​
228 out4:​
229 class_destroy(newchrled.class);​
230 ​
231 out3:​
232 cdev_del(&newchrled.cdev);​
233 ​
234 out2:​
235 unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);​
236 ​
237 out1:​
238 led_iounmap();​
239 ​
240 return ret;​
241 }​
242 ​
243 static void __exit led_exit(void)​
244 {​
245 /* 注销设备 */​
246 device_destroy(newchrled.class, newchrled.devid);​
247 ​
248 /* 注销类 */​
249 class_destroy(newchrled.class);​
250 ​
251 /* 删除cdev */​
252 cdev_del(&newchrled.cdev);​
253 ​
254 /* 注销设备号 */​
255 unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);​
256 ​
257 /* 取消地址映射 */​
258 led_iounmap();​
259 }​
260 ​
261 /* 驱动模块入口和出口函数注册 */​
262 module_init(led_init);​
263 module_exit(led_exit);​
264 ​
265 MODULE_AUTHOR("DengTao <773904075@qq.com>");​
266 MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");​
267 MODULE_LICENSE("GPL");

第25行,宏NEWCHRLED_CNT表示设备数量,在申请设备号或者向Linux内核添加字符设备的时候需要设置设备数量,一般我们一个驱动一个设备,所以这个宏为1。

第26行,宏NEWCHRLED_NAME表示设备名字,本实验的设备名为“newchrdev”,为了方便管理,所有使用到设备名字的地方统一使用此宏,当驱动加载成功以后就生成/dev/newchrled这个设备文件。

46~53行,创建设备结构体newchrled_dev

第55行,定义一个设备结构体变量newchrdev,此变量表示led设备。

64~68行,在led_open函数中设置文件的私有数据private_data指向newchrdev。

第152~241行,根据前面讲解的方法在驱动入口函数led_init中申请设备号、添加字符设备、创建类和设备。186~198行代码去申请设备号,如果提供了设备号则申请静态设备号,如果我们提供的设备号为0则采用动态申请设备号的方法,第200行使用printk在终端上显示出申请到的主设备号和次设备号。

第241~245行,根据前面讲解的方法,在驱动出口函数led_exit中注销字符新设备、删除类和设备。

总体来说newchrled.c文件中的内容不复杂,LED灯驱动部分的程序和上一章一样。重点就是使用了新的字符设备驱动方法。

驱动中的倒退式处理方法

细心的同学会发现在上面的代码当中用到了C语言中goto语句,不知道大家对这个goto熟悉不熟悉,goto顾名思义其实即使跳转的意思,例如goto out1,就是跳转到out地址所在的地方,那么goto语句在linux内核当中用的特别多,因为它非常符合linux下这种开发环境,因为内核中一个函数可能包含了很多个操作,这些操作每一步都有可能出错,如果出错之后那么后面的步骤就没有进行下去的必要性了,但是你不能直接退出,你得把你前面做过的操作给“复原”了。

例如在上面的代码当中,如果在调用device_create函数注册设备的时候失败了,没有成功,那么你就得把前面做的工作给“复原”,你创建了class类,那你就得调用class_destroy删除这个类,你添加了cdev,那你就得删除cdev,而且他这个顺序还是倒退式的,大家需要去好好理解下,以后自己开发驱动也需要养成这样的习惯。

编写测试APP

本章直接使用上一章的测试APP,将上一章的ledApp.c文件复制到本章实验目录下(3_newchrled)即可。

运行测试

编译驱动程序和测试APP

1、编译驱动程序

将上一章使用的Makefile文件拷贝到本实验的目录下,然后打开这个Makefile文件,将obj-m变量的值改为newchrled.o,最终Makefile内容如下所示:

示例代码23.6.1.1 Makefile文件内容

1 KERN_DIR := /home/shang/git.d/linux-xlnx​
2 ​
3 obj-m := newchrled.o​
4 ​
5 all:​
6 make -C $(KERN_DIR) M=`pwd` modules​
7 ​
8 clean:​
9 make -C $(KERN_DIR) M=`pwd` clean

3行,设置obj-m变量的值为newchrled.o

执行make命令编译出驱动模块文件:

make

编译成功以后就会生成一个名为“newchrled.ko”的驱动模块文件,如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》 第十九章 新字符设备驱动实验​


19.6.1 编译过程

2、编译测试APP

我们直接用上一章使用的测试程序ledApp.c,直接使用上一章编译好的ledApp可执行文件,不用再重新编译了。

运行测试

将上一小节编译出来的newchrled.ko和ledApp这两个文件拷贝到开发板根文件系统/lib/modules/4.19.0目录下,重启开发板,进入到/lib/modules/4.19.0目录中,输入如下命令加载newchrled.ko驱动模块:

depmod     //第一次加载驱动的时候需要运行此命令​
modprobe newchrled.ko //加载驱动

驱动加载成功以后会输出申请到的主设备号和次设备号,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》 第十九章 新字符设备驱动实验​


19.6.2 加载驱动模块

从上图可以看出,申请到的主设备号为244,次设备号为0。驱动加载成功以后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看/dev/newchrdev这个设备节点文件是否存在:

ls /dev/newchrled -l

结果如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》 第十九章 新字符设备驱动实验​


19.6.3 /dev/newchrled设备节点

从图中可以看出,/dev/newchrled这个设备文件存在,而且主设备号为244,此设备号为0,说明设备节点文件创建成功。

驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:

./ledApp /dev/newchrled 1  //打开LED灯

输入上述命令以后查看底板上的PS_LED0灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:

./ledApp /dev/newchrled 0  //关闭LED灯

输入上述命令以后查看底板上的PS_LED0灯是否熄灭。如果要卸载驱动的话输入如下命令即可:

rmmod newchrled.ko