搜索
bottom↓
回复: 1

【正点原子FPGA连载】第二十三章新字符设备驱动实验--摘自【正点原子】领航者 ZYNQ 之linux驱动开发指南

[复制链接]

出0入234汤圆

发表于 2020-9-7 11:15:13 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2020-9-7 11:16 编辑

1)实验平台:正点原子领航者ZYNQ开发板
2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.html
4)对正点原子FPGA感兴趣的同学可以加群讨论:876744900 点击加入:
QQ群头像.png                            
5)关注正点原子公众号,获取最新资料

100846rel79a9p4uelap24.jpg

100846f1ce1fg14zbg0va4.png

第二十三章新字符设备驱动实验



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



23.1新字符设备驱动原理
23.1.1分配和释放设备号
使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~1048575(2^20-1)这个区间的次设备号就全部都被LED一个设备分走了。这样太浪费次设备号了!一个LED设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是要使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。这个就是我们在32.3.2小节讲解的设备号的分配,如果没有指定设备号的话就使用如下函数来申请设备号:
  1. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
复制代码

如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
  1. int register_chrdev_region(dev_t from, unsigned count, const char *name)
复制代码

参数from是要申请的起始设备号,也就是给定的设备号;参数count是要申请的数量,一般都是一个;参数name是设备名字。
注销字符设备之后要释放掉设备号,不管是通过alloc_chrdev_region函数还是register_chrdev_region函数申请的设备号,统一使用如下释放函数:
  1. void unregister_chrdev_region(dev_t from, unsigned count)
复制代码

新字符设备驱动下,设备号分配示例代码如下:
示例代码23.1.1.1 新字符设备驱动下设备号分配
  1. 1  int major;                        /* 主设备号 */
  2. 2  int minor;                        /* 次设备号 */
  3. 3  dev_t devid;                /* 设备号 */
  4. 4  
  5. 5  if (major) {                                                /* 定义了主设备号         */
  6. 6      devid = MKDEV(major, 0);                /* 大部分驱动次设备号都选择0        */
  7. 7              register_chrdev_region(devid, 1, "test");
  8. 8  } else {                                                        /* 没有定义设备号 */
  9. 9      alloc_chrdev_region(&devid, 0, 1, "test");        /* 申请设备号 */
  10. 10     major = MAJOR(devid);                /* 获取分配号的主设备号 */
  11. 11     minor = MINOR(devid);                /* 获取分配号的次设备号 */
  12. 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.1.2 cdev结构体
  1. 1 unregister_chrdev_region(devid, 1);                /* 注销设备号 */
复制代码

注销设备号的代码很简单。
23.1.2新的字符设备注册方法
1、字符设备结构
在Linux中使用cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h文件中的定义如下:
示例代码23.1.2.1 cdev结构体
  1. 1 struct cdev {
  2. 2     struct kobject  kobj;
  3. 3             struct module  *owner;
  4. 4             const struct  file_operations *ops;
  5. 5             struct list_head  list;
  6. 6             dev_t  dev;
  7. 7             unsigned int  count;
  8. 8 };
复制代码

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

2、cdev_init函数
定义好cdev变量以后就要使用cdev_init函数对其进行初始化,cdev_init函数原型如下:
  1. void cdev_init(struct cdev *cdev, const struct file_operations *fops)
复制代码

参数cdev就是要初始化的cdev结构体变量,参数fops就是字符设备文件操作函数集合。使用cdev_init函数初始化cdev变量的示例代码如下:
示例代码23.1.2.2 cdev_init函数使用示例代码
  1. 1  struct cdev testcdev;
  2. 2  
  3. 3  /* 设备操作函数 */
  4. 4  static struct file_operations test_fops = {
  5. 5      .owner = THIS_MODULE,
  6. 6               /* 其他具体的初始项 */
  7. 7  };
  8. 8  
  9. 9  testcdev.owner = THIS_MODULE;
  10. 10 cdev_init(&testcdev, &test_fops); /* 初始化cdev结构体变量 */
  11. 3、cdev_add函数
复制代码

cdev_add函数用于向Linux系统添加字符设备(cdev结构体变量),首先使用cdev_init函数完成对cdev结构体变量的初始化,然后使用cdev_add函数向Linux系统添加这个字符设备。cdev_add函数原型如下:
  1. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
复制代码

参数p指向要添加的字符设备(cdev结构体变量),参数dev就是设备所使用的设备号,参数count是要添加的设备数量。完善示例代码23.1.4,加入cdev_add函数,内容如下所示:
示例代码23.1.2.3 cdev_add函数使用示例
  1. 1  struct cdev testcdev;
  2. 2  
  3. 3  /* 设备操作函数 */
  4. 4  static struct file_operations test_fops = {
  5. 5      .owner = THIS_MODULE,
  6. 6              /* 其他具体的初始项 */
  7. 7  };
  8. 8  
  9. 9  testcdev.owner = THIS_MODULE;
  10. 10 cdev_init(&testcdev, &test_fops);      /* 初始化cdev结构体变量 */
  11. 11 cdev_add(&testcdev, devid, 1);          /* 添加字符设备 */
复制代码

示例代码23.1.4就是新的注册字符设备代码段,Linux内核中大量的字符设备驱动都是采用这种方法向Linux内核添加字符设备。如果在加上示例代码23.1.1中分配设备号的程序,那么就它们一起实现的就是函数register_chrdev的功能。
3、cdev_del函数
卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备,cdev_del函数原型如下:
  1. void cdev_del(struct cdev *p)
复制代码

参数p就是要删除的字符设备。如果要删除字符设备,参考如下代码:
示例代码23.1.2.4 cdev_del函数使用示例
  1. 1 cdev_del(&testcdev);        /*  删除cdev */
复制代码

cdev_del和unregister_chrdev_region这两个函数合起来的功能相当于unregister_chrdev函数。
23.2自动创建设备节点
在前面的Linux驱动实验中,当我们使用modprobe加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。
23.2.1mdev机制
udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除,比如使用modprobe命令成功加载驱动模块以后就自动在/dev目录下创建对应的设备节点文件,使用rmmod命令卸载驱动模块以后就删除掉/dev目录下的设备节点文件。使用busybox构建根文件系统的时候(我们在petalinux中编译得到的根文件系统其实就是busybox构建出来的),busybox会创建一个udev的简化版本—mdev,所以在嵌入式Linux中我们使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理。
关于udev或mdev更加详细的工作原理我们不去讨论,本章我们重点来学习设备文件节点的自动创建与删除。
23.2.2创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个class类(class的概念这里暂时不去细说,大家可以先简答地理解为设备类即可,就是某个设备属于某个类,后面我们会详细的讲),class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义,内容如下:
示例代码23.2.2.1 class_create函数
  1. 1 #define class_create(owner, name)     \
  2. 2 ({                                        \
  3. 3   static struct lock_class_key __key; \
  4. 4   __class_create(owner, name, &__key);    \
  5. 5 })
  6. 6
  7. 7 struct class *__class_create(struct module *owner, const char *name,
  8. 8                                   struct lock_class_key *key)
复制代码

根据上述代码,将宏class_create展开以后内容如下:
  1. struct class *class_create (struct module *owner, const char *name)
复制代码

class_create一共有两个参数,参数owner一般为THIS_MODULE,参数name是类名字。返回值是个指向结构体class的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为class_destroy,函数原型如下:
  1. void class_destroy(struct class *cls);
复制代码

参数cls就是要删除的类。
23.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,函数原型如下:
  1. void device_destroy(struct class *class, dev_t devt)
复制代码

参数classs是要删除的设备所处的类,参数devt是要删除的设备号。
23.2.4参考示例
在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:
示例代码23.2.4.1 创建/删除类/设备参考代码
  1. 1  struct class *class;                /* 类 */      
  2. 2  struct device *device;        /* 设备 */
  3. 3  dev_t devid;                                /* 设备号 */         
  4. 4  
  5. 5  /* 驱动入口函数 */
  6. 6  static int __init xxx_init(void)
  7. 7  {
  8. 8      /* 创建类         */
  9. 9      class = class_create(THIS_MODULE, "xxx");
  10. 10     /* 创建设备 */
  11. 11     device = device_create(class, NULL, devid, NULL, "xxx");
  12. 12     return 0;
  13. 13 }
  14. 14
  15. 15 /* 驱动出口函数 */
  16. 16 static void __exit led_exit(void)
  17. 17 {
  18. 18      /* 删除设备 */
  19. 19      device_destroy(newchrled.class, newchrled.devid);
  20. 20      /* 删除类   */
  21. 21      class_destroy(newchrled.class);
  22. 22 }
  23. 23
  24. 24 module_init(led_init);
  25. 25 module_exit(led_exit);
复制代码

23.3设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示:
示例代码23.3.1 变量形式的设备属性
  1. dev_t devid;                                /* 设备号 */
  2. struct cdev cdev;                        /* cdev */
  3. struct class *class;                /* 类 */
  4. struct device *device;        /* 设备 */
  5. int major;                                        /* 主设备号 */
  6. int minor;                                        /* 次设备号 */
复制代码

这样写肯定没有问题,但是这样写不专业!对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示:
示例代码23.3.2 设备结构体作为私有数据
  1. /* 设备结构体 */
  2. 1  struct test_dev{
  3. 2      dev_t devid;                                /* 设备号    */
  4. 3              struct cdev cdev;                        /* cdev     */
  5. 4              struct class *class;                /* 类      */
  6. 5              struct device *device;        /* 设备    */
  7. 6              int major;                                        /* 主设备号   */
  8. 7              int minor;                                        /* 次设备号   */
  9. 8  };
  10. 9  
  11. 10 struct test_dev testdev;
  12. 11  
  13. 12 /* open函数 */
  14. 13 static int test_open(struct inode *inode, struct file *filp)
  15. 14 {
  16. 15     filp->private_data = &testdev; /* 设置私有数据 */
  17. 16     return 0;
  18. 17 }
复制代码

在open函数里面设置好私有数据以后,在write、read、close等函数中直接读取private_data即可得到设备结构体。
23.4硬件原理图分析
本章实验硬件原理图参考33.3小节即可!
23.5实验程序编写
本实验对应的例程路径为:ZYNQ开发板光盘资料(A盘)\4_SourceCode\ZYNQ_7010\3_Embedded_Linux\Linux驱动例程\3_newchrled。
本章实验在上一章实验的基础上完成,重点是使用了新的字符设备驱动、设置了文件私有数据、添加了自动创建设备节点相关内容。
23.5.1LED灯驱动程序编写
在drivers目录下新建名为“3_newchrled”的文件夹,在3_newchrled目录下新建newchrled.c文件,里面输入如下内容:
示例代码23.5.1.1 newchrled.c
  1. 1 /***************************************************************
  2.   2  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  3.   3  文件名    : newchrled.c
  4.   4  作者      : 邓涛
  5.   5  版本      : V1.0
  6.   6  描述      : ZYNQ LED驱动文件。
  7.   7  其他      : 无
  8.   8  论坛      : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  9.   9  日志      : 初版V1.0 2019/1/30 邓涛创建
  10. 10  ***************************************************************/
  11. 11
  12. 12 #include <linux/types.h>
  13. 13 #include <linux/kernel.h>
  14. 14 #include <linux/delay.h>
  15. 15 #include <linux/ide.h>
  16. 16 #include <linux/init.h>
  17. 17 #include <linux/module.h>
  18. 18 #include <linux/errno.h>
  19. 19 #include <linux/gpio.h>
  20. 20 #include <asm/mach/map.h>
  21. 21 #include <asm/uaccess.h>
  22. 22 #include <asm/io.h>
  23. 23 #include <linux/cdev.h>
  24. 24
  25. 25 #define NEWCHRLED_CNT       1                 /* 设备号个数 */
  26. 26 #define NEWCHRLED_NAME      "newchrled"     /* 名字 */
  27. 27
  28. 28 /*
  29. 29  * GPIO相关寄存器地址定义
  30. 30  */
  31. 31 #define ZYNQ_GPIO_REG_BASE            0xE000A000
  32. 32 #define DATA_OFFSET                    0x00000040
  33. 33 #define DIRM_OFFSET                    0x00000204
  34. 34 #define OUTEN_OFFSET                   0x00000208
  35. 35 #define INTDIS_OFFSET                  0x00000214
  36. 36 #define APER_CLK_CTRL                  0xF800012C
  37. 37
  38. 38 /* 映射后的寄存器虚拟地址指针 */
  39. 39 static void __iomem *data_addr;
  40. 40 static void __iomem *dirm_addr;
  41. 41 static void __iomem *outen_addr;
  42. 42 static void __iomem *intdis_addr;
  43. 43 static void __iomem *aper_clk_ctrl_addr;
  44. 44
  45. 45 /* newchrled设备结构体 */
  46. 46 struct newchrled_dev {
  47. 47     dev_t devid;             /* 设备号 */
  48. 48     struct cdev cdev;       /* cdev */
  49. 49     struct class *class;    /* 类 */
  50. 50     struct device *device;  /* 设备 */
  51. 51     int major;                /* 主设备号 */
  52. 52     int minor;                /* 次设备号 */
  53. 53 };
  54. 54
  55. 55 static struct newchrled_dev newchrled;  /* led设备 */
  56. 56
  57. 57 /*
  58. 58  * @description         : 打开设备
  59. 59  * @param – inode       : 传递给驱动的inode
  60. 60  * @param - filp        : 设备文件,file结构体有个叫做private_data的成员变量
  61. 61  *                          一般在open的时候将private_data指向设备结构体。
  62. 62  * @return               : 0 成功;其他 失败
  63. 63  */
  64. 64 static int led_open(struct inode *inode, struct file *filp)
  65. 65 {
  66. 66     filp->private_data = &newchrled;    /* 设置私有数据 */
  67. 67     return 0;
  68. 68 }
  69. 69
  70. 70 /*
  71. 71  * @description         : 从设备读取数据
  72. 72  * @param - filp        : 要打开的设备文件(文件描述符)
  73. 73  * @param - buf         : 返回给用户空间的数据缓冲区
  74. 74  * @param - cnt         : 要读取的数据长度
  75. 75  * @param - offt        : 相对于文件首地址的偏移
  76. 76  * @return               : 读取的字节数,如果为负值,表示读取失败
  77. 77  */
  78. 78 static ssize_t led_read(struct file *filp, char __user *buf,
  79. 79             size_t cnt, loff_t *offt)
  80. 80 {
  81. 81     return 0;
  82. 82 }
  83. 83
  84. 84 /*
  85. 85  * @description         : 向设备写数据
  86. 86  * @param - filp        : 设备文件,表示打开的文件描述符
  87. 87  * @param - buf         : 要写给设备写入的数据
  88. 88  * @param - cnt         : 要写入的数据长度
  89. 89  * @param - offt        : 相对于文件首地址的偏移
  90. 90  * @return               : 写入的字节数,如果为负值,表示写入失败
  91. 91  */
  92. 92 static ssize_t led_write(struct file *filp, const char __user *buf,
  93. 93             size_t cnt, loff_t *offt)
  94. 94 {
  95. 95     int ret;
  96. 96     int val;
  97. 97     char kern_buf[1];
  98. 98
  99. 99     ret = copy_from_user(kern_buf, buf, cnt);  // 得到应用层传递过来的数据
  100. 100     if(0 > ret) {
  101. 101         printk(KERN_ERR "kernel write failed!\r\n");
  102. 102         return -EFAULT;
  103. 103     }
  104. 104
  105. 105     val = readl(data_addr);
  106. 106     if (0 == kern_buf[0])
  107. 107         val &= ~(0x1U << 7);    // 如果传递过来的数据是0则关闭led
  108. 108     else if (1 == kern_buf[0])
  109. 109         val |= (0x1U << 7);     // 如果传递过来的数据是1则点亮led
  110. 110
  111. 111     writel(val, data_addr);
  112. 112     return 0;
  113. 113 }
  114. 114
  115. 115 /*
  116. 116  * @description         : 关闭/释放设备
  117. 117  * @param – filp        : 要关闭的设备文件(文件描述符)
  118. 118  * @return               : 0 成功;其他 失败
  119. 119  */
  120. 120 static int led_release(struct inode *inode, struct file *filp)
  121. 121 {
  122. 122     return 0;
  123. 123 }
  124. 124
  125. 125 static inline void led_ioremap(void)
  126. 126 {
  127. 127     data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
  128. 128     dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
  129. 129     outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
  130. 130     intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
  131. 131     aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
  132. 132 }
  133. 133
  134. 134 static inline void led_iounmap(void)
  135. 135 {
  136. 136     iounmap(data_addr);
  137. 137     iounmap(dirm_addr);
  138. 138     iounmap(outen_addr);
  139. 139     iounmap(intdis_addr);
  140. 140     iounmap(aper_clk_ctrl_addr);
  141. 141 }
  142. 142
  143. 143 /* 设备操作函数 */
  144. 144 static struct file_operations newchrled_fops = {
  145. 145     .owner   = THIS_MODULE,
  146. 146     .open    = led_open,
  147. 147     .read    = led_read,
  148. 148     .write   = led_write,
  149. 149     .release = led_release,
  150. 150 };
  151. 151
  152. 152 static int __init led_init(void)
  153. 153 {
  154. 154     u32 val;
  155. 155     int ret;
  156. 156
  157. 157     /* 1.寄存器地址映射 */
  158. 158     led_ioremap();
  159. 159
  160. 160     /* 2.使能GPIO时钟 */
  161. 161     val = readl(aper_clk_ctrl_addr);
  162. 162     val |= (0x1U << 22);
  163. 163     writel(val, aper_clk_ctrl_addr);
  164. 164
  165. 165     /* 3.关闭中断功能 */
  166. 166     val |= (0x1U << 7);
  167. 167     writel(val, intdis_addr);
  168. 168
  169. 169     /* 4.设置GPIO为输出功能 */
  170. 170     val = readl(dirm_addr);
  171. 171     val |= (0x1U << 7);
  172. 172     writel(val, dirm_addr);
  173. 173
  174. 174     /* 5.使能GPIO输出功能 */
  175. 175     val = readl(outen_addr);
  176. 176     val |= (0x1U << 7);
  177. 177     writel(val, outen_addr);
  178. 178
  179. 179     /* 6.默认关闭LED */
  180. 180     val = readl(data_addr);
  181. 181     val &= ~(0x1U << 7);
  182. 182     writel(val, data_addr);
  183. 183
  184. 184     /* 7.注册字符设备驱动 */
  185. 185      /* 创建设备号 */
  186. 186     if (newchrled.major) {
  187. 187         newchrled.devid = MKDEV(newchrled.major, 0);
  188. 188         ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
  189. 189         if (ret)
  190. 190             goto out1;
  191. 191     } else {
  192. 192         ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);
  193. 193         if (ret)
  194. 194             goto out1;
  195. 195
  196. 196         newchrled.major = MAJOR(newchrled.devid);
  197. 197         newchrled.minor = MINOR(newchrled.devid);
  198. 198     }
  199. 199
  200. 200     printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);
  201. 201
  202. 202      /* 初始化cdev */
  203. 203     newchrled.cdev.owner = THIS_MODULE;
  204. 204     cdev_init(&newchrled.cdev, &newchrled_fops);
  205. 205
  206. 206      /* 添加一个cdev */
  207. 207     ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
  208. 208     if (ret)
  209. 209         goto out2;
  210. 210
  211. 211      /* 创建类 */
  212. 212     newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
  213. 213     if (IS_ERR(newchrled.class)) {
  214. 214         ret = PTR_ERR(newchrled.class);
  215. 215         goto out3;
  216. 216     }
  217. 217
  218. 218      /* 创建设备 */
  219. 219     newchrled.device = device_create(newchrled.class, NULL,
  220. 220                 newchrled.devid, NULL, NEWCHRLED_NAME);
  221. 221     if (IS_ERR(newchrled.device)) {
  222. 222         ret = PTR_ERR(newchrled.device);
  223. 223         goto out4;
  224. 224     }
  225. 225
  226. 226     return 0;
  227. 227
  228. 228 out4:
  229. 229     class_destroy(newchrled.class);
  230. 230
  231. 231 out3:
  232. 232     cdev_del(&newchrled.cdev);
  233. 233
  234. 234 out2:
  235. 235     unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
  236. 236
  237. 237 out1:
  238. 238     led_iounmap();
  239. 239
  240. 240     return ret;
  241. 241 }
  242. 242
  243. 243 static void __exit led_exit(void)
  244. 244 {
  245. 245     /* 注销设备 */
  246. 246     device_destroy(newchrled.class, newchrled.devid);
  247. 247
  248. 248     /* 注销类 */
  249. 249     class_destroy(newchrled.class);
  250. 250
  251. 251     /* 删除cdev */
  252. 252     cdev_del(&newchrled.cdev);
  253. 253
  254. 254     /* 注销设备号 */
  255. 255     unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
  256. 256
  257. 257     /* 取消地址映射 */
  258. 258     led_iounmap();
  259. 259 }
  260. 260
  261. 261 /* 驱动模块入口和出口函数注册 */
  262. 262 module_init(led_init);
  263. 263 module_exit(led_exit);
  264. 264
  265. 265 MODULE_AUTHOR("DengTao <<a href="mailto:773904075@qq.com">773904075@qq.com</a>>");
  266. 266 MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
  267. 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,而且他这个顺序还是倒退式的,大家需要去好好理解下,以后自己开发驱动也需要养成这样的习惯。
23.5.2编写测试APP
本章直接使用上一章的测试APP,将上一章的ledApp.c文件复制到本章实验目录下(3_newchrled)即可。
23.6运行测试
23.6.1编译驱动程序和测试APP
1、编译驱动程序
将上一章使用的Makefile文件拷贝到本实验的目录下,然后打开这个Makefile文件,将obj-m变量的值改为newchrled.o,最终Makefile内容如下所示:
        示例代码23.6.1.1 Makefile文件内容
  1. 1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
  2.   2
  3.   3 obj-m := newchrled.o
  4.   4
  5.   5 all:
  6.   6         make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules
  7.   7
  8.   8 clean:
  9.   9         make -C $(KERN_DIR) M=`pwd` clean
复制代码

第3行,设置obj-m变量的值为newchrled.o。
执行make命令编译出驱动模块文件:
make
编译成功以后就会生成一个名为“newchrled.ko”的驱动模块文件,如下所示:
阿莫论坛发帖领航者专用117003.png

图 34.6.1 编译过程

2、编译测试APP
我们直接用上一章使用的测试程序ledApp.c,直接使用上一章编译好的ledApp可执行文件,不用再重新编译了。
23.6.2运行测试
将上一小节编译出来的newchrled.ko和ledApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录下,重启开发板,进入到/lib/modules/4.14.0-xilinx目录中,输入如下命令加载newchrled.ko驱动模块:
  1. Depmod                                        //第一次加载驱动的时候需要运行此命令
  2. modprobe newchrled.ko                //加载驱动
复制代码

驱动加载成功以后会输出申请到的主设备号和次设备号,如下图所示:
阿莫论坛发帖领航者专用117367.png

图 34.6.2 加载驱动模块

从上图可以看出,申请到的主设备号为244,次设备号为0。驱动加载成功以后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看/dev/newchrdev这个设备节点文件是否存在:
ls /dev/newchrled -l
结果如下图所示:
阿莫论坛发帖领航者专用117564.png

图 34.6.3 /dev/newchrled设备节点

从图中可以看出,/dev/newchrled这个设备文件存在,而且主设备号为244,此设备号为0,说明设备节点文件创建成功。
驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
  1. ./ledApp /dev/newchrled 1                //打开LED灯
复制代码

输入上述命令以后查看底板上的PS_LED0灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:
  1. ./ledApp /dev/newchrled 0                //关闭LED灯
复制代码

输入上述命令以后查看底板上的PS_LED0灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
  1. rmmod newchrled.ko
复制代码

阿莫论坛20周年了!感谢大家的支持与爱护!!

如果想吃一顿饺子,就得从冰箱里取出肉,剁馅儿,倒面粉、揉面、醒面,擀成皮儿,下锅……
一整个繁琐流程,就是为了出锅时那一嘴滚烫流油的热饺子。

如果这个过程,禁不住饿,零食下肚了,饺子出锅时也就不香了……《非诚勿扰3》

出0入224汤圆

发表于 2020-9-7 11:23:14 | 显示全部楼层
很详细的资料,后面还有几章,大概什么时候更新完?
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-3-28 17:16

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表