正点原子 发表于 2022-1-19 15:21:55

《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第二十三章 新字符设备

1)实验平台:正点原子领航者V2 ZYNQ开发板
2)章节摘自【正点原子】《领航者ZYNQ之嵌入式Linux开发指南_V2.0》
3)购买链接:https://detail.tmall.com/item.htm?id=609032204975
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-329957-1-1.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流QQ群:90562473







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

       1.1新字符设备驱动原理
1.1.1分配和释放设备号
       使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
       ①、需要我们事先确定好哪些主设备号没有使用。
       ②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~1048575(2^20-1)这个区间的次设备号就全部都被LED一个设备分走了。这样太浪费次设备号了!一个LED设备肯定只能有一个主设备号,一个次设备号。
       解决这两个问题最好的方法就是要使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。这个就是我们在21.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)       新字符设备驱动下,设备号分配示例代码如下:
示例代码 23.1.1 新字符设备驱动下设备号分配
1int major;                        /* 主设备号 */
2int minor;                        /* 次设备号 */
3dev_t devid;                /* 设备号 */
4
5if (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行提取主设备号和次设备号的代码可以不要。
       如果要注销设备号的话,使用如下代码即可:
示例代码 23.1.2 注销设备号
1 unregister_chrdev_region(devid, 1);                /* 注销设备号 */       注销设备号的代码很简单。
1.1.2新的字符设备注册方法
       1、字符设备结构
       在Linux中使用cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h文件中的定义如下:
示例代码 23.1.3 cdev结构体
1 struct cdev {
2   struct kobjectkobj;
3   struct module*owner;
4   const structfile_operations *ops;
5   struct list_headlist;
6   dev_tdev;
7   unsigned intcount;
8 };       在cdev中有两个重要的成员变量:ops和dev,这两个就是字符设备文件操作函数集合file_operations以及设备号dev_t。编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;       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变量的示例代码如下:
示例代码 23.1.4 cdev_init函数使用示例代码
1struct cdev testcdev;
2
3/* 设备操作函数 */
4static struct file_operations test_fops = {
5      .owner = THIS_MODULE,
6      /* 其他具体的初始项 */
7};
8
9testcdev.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是要添加的设备数量。完善示例代码 23.1.4,加入cdev_add函数,内容如下所示:
示例代码 23.1.5 cdev_add函数使用示例
1struct cdev testcdev;
2
3/* 设备操作函数 */
4static struct file_operations test_fops = {
5      .owner = THIS_MODULE,
6      /* 其他具体的初始项 */
7};
8
9testcdev.owner = THIS_MODULE;
10 cdev_init(&testcdev, &test_fops);                /* 初始化cdev结构体变量 */
11 cdev_add(&testcdev, devid, 1);                /* 添加字符设备 */       示例代码 23.1.5就是新的注册字符设备代码段,Linux内核中大量的字符设备驱动都是采用这种方法向Linux内核添加字符设备。如果在加上示例代码 23.1.1中分配设备号的程序,那么就它们一起实现的就是函数register_chrdev的功能。
       3、cdev_del函数
       卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备,cdev_del函数原型如下:
void cdev_del(struct cdev *p)       参数p就是要删除的字符设备。如果要删除字符设备,参考如下代码:
示例代码 23.1.6 cdev_del函数使用示例
1 cdev_del(&testcdev);      /* 删除cdev */       cdev_del和unregister_chrdev_region这两个函数合起来的功能相当于unregister_chrdev函数。
       1.2自动创建设备节点
       在前面的Linux驱动实验中,当我们使用modprobe加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。
1.2.1mdev机制
       udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除,比如使用modprobe命令成功加载驱动模块以后就自动在/dev目录下创建对应的设备节点文件,使用rmmod命令卸载驱动模块以后就删除掉/dev目录下的设备节点文件。使用busybox构建根文件系统的时候(我们在petalinux中编译得到的根文件系统其实就是busybox构建出来的),busybox会创建一个udev的简化版本—mdev,所以在嵌入式Linux中我们使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理。
关于udev或mdev更加详细的工作原理我们不去讨论,本章我们重点来学习设备文件节点的自动创建与删除。
1.2.2创建和删除类
       自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个class类(class的概念这里暂时不去细说,大家可以先简答地理解为设备类即可,就是某个设备属于某个类,后面我们会详细的讲),class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义,内容如下:
示例代码 23.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就是要删除的类。
1.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)       参数classs是要删除的设备所处的类,参数devt是要删除的设备号。
1.2.4参考示例
       在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:
示例代码 23.2.2 创建/删除类/设备参考代码
1struct class *class;                /* 类 */
2struct device *device;      /* 设备 */
3dev_t devid;                        /* 设备号 */
4
5/* 驱动入口函数 */
6static 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);       1.3设置文件私有数据
       每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示:
示例代码 23.3.1 变量形式的设备属性
dev_t devid;                        /* 设备号 */
struct cdev cdev;                /* cdev */
struct class *class;                /* 类 */
struct device *device;      /* 设备 */
int major;                              /* 主设备号 */
int minor;                        /* 次设备号 */       这样写肯定没有问题,但是这样写不专业!对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示:
示例代码 23.3.2 设备结构体作为私有数据
/* 设备结构体 */
1struct 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即可得到设备结构体。
       1.4硬件原理图分析
       本章实验硬件原理图参考22.3小节即可。
       1.5实验程序编写
       本实验对应的例程路径为:ZYNQ开发板光盘资料(A盘)\4_SourceCode\3_Embedded_Linux\Linux驱动例程\3_newchrled。
       本章实验在上一章实验的基础上完成,重点是使用了新的字符设备驱动、设置了文件私有数据、添加了自动创建设备节点相关内容。
1.5.1LED灯驱动程序编写
       在drivers目录下新建名为“3_newchrled”的文件夹,在3_newchrled目录下新建newchrled.c文件,里面输入如下内容:
示例代码 23.5.1 newchrled.c文件内容
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名    : newchrled.c
4作者      : 邓涛
5版本      : V1.0
6描述      : ZYNQ LED驱动文件。
7其他      : 无
8论坛      : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
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* GPIO相关寄存器地址定义
30*/
31 #define ZYNQ_GPIO_REG_BASE                0xE000A000
32 #define DATA_OFFSET                                        0x00000040
33 #define DIRM_OFFSET                                        0x00000204
34 #define OUTEN_OFFSET                              0x00000208
35 #define INTDIS_OFFSET                              0x00000214
36 #define APER_CLK_CTRL                              0xF800012C
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* @description                        : 打开设备
59* @param – inode                        : 传递给驱动的inode
60* @param – filp                        : 设备文件,file结构体有个叫做private_data的成员变量
61*                                                一般在open的时候将private_data指向设备结构体。
62* @return                              : 0 成功;其他 失败
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* @description                        : 从设备读取数据
72* @param – filp                        : 要打开的设备文件(文件描述符)
73* @param – buf                        : 返回给用户空间的数据缓冲区
74* @param – cnt                        : 要读取的数据长度
75* @param – offt                        : 相对于文件首地址的偏移
76* @return               : 读取的字节数,如果为负值,表示读取失败
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* @description                        : 向设备写数据
86* @param – filp                        : 设备文件,表示打开的文件描述符
87* @param – buf                        : 要写给设备写入的数据
88* @param – cnt                        : 要写入的数据长度
89* @param – offt                        : 相对于文件首地址的偏移
90* @return               : 写入的字节数,如果为负值,表示写入失败
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;
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)
107         val &= ~(0x1U << 7);    // 如果传递过来的数据是0则关闭led
108   else if (1 == kern_buf)
109         val |= (0x1U << 7);   // 如果传递过来的数据是1则点亮led
110
111   writel(val, data_addr);
112   return 0;
113 }
114
115 /*
116* @description                        : 关闭/释放设备
117* @param – filp                        : 要关闭的设备文件(文件描述符)
118* @return               : 0 成功;其他 失败
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 << 22);
163   writel(val, aper_clk_ctrl_addr);
164
165   /* 3.关闭中断功能 */
166   val |= (0x1U << 7);
167   writel(val, intdis_addr);
168
169   /* 4.设置GPIO为输出功能 */
170   val = readl(dirm_addr);
171   val |= (0x1U << 7);
172   writel(val, dirm_addr);
173
174   /* 5.使能GPIO输出功能 */
175   val = readl(outen_addr);
176   val |= (0x1U << 7);
177   writel(val, outen_addr);
178
179   /* 6.默认关闭LED */
180   val = readl(data_addr);
181   val &= ~(0x1U << 7);
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 <<a href="mailto:773904075@qq.com">773904075@qq.com</a>>");
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,而且他这个顺序还是倒退式的,大家需要去好好理解下,以后自己开发驱动也需要养成这样的习惯。
1.5.2编写测试APP
       本章直接使用上一章的测试APP,将上一章的ledApp.c文件复制到本章实验目录下(3_newchrled)即可。
       1.6运行测试
1.6.1编译驱动程序和测试APP
       1、编译驱动程序
       将上一章使用的Makefile文件拷贝到本实验的目录下,然后打开这个Makefile文件,将obj-m变量的值改为newchrled.o,最终Makefile内容如下所示:
示例代码 23.6.1 Makefile文件内容
1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
2
3 obj-m := newchrled.o
4
5 all:
6         make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -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”的驱动模块文件,如下所示:

图 23.6.1 编译过程
       2、编译测试APP
       我们直接用上一章使用的测试程序ledApp.c,直接使用上一章编译好的ledApp可执行文件,不用再重新编译了。
1.6.2运行测试
       将上一小节编译出来的newchrled.ko和ledApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录下,重启开发板,进入到/lib/modules/4.14.0-xilinx目录中,输入如下命令加载newchrled.ko驱动模块:
Depmod                                        //第一次加载驱动的时候需要运行此命令
modprobe newchrled.ko                //加载驱动       驱动加载成功以后会输出申请到的主设备号和次设备号,如下图所示:


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


图 23.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

页: [1]
查看完整版本: 《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第二十三章 新字符设备