搜索
bottom↓
回复: 0

【正点原子FPGA连载】第二十二章嵌入式Linux LED驱动开发实验--摘自【正点原子】领航者 ZYNQ 之linux驱动开发指南

[复制链接]

出0入234汤圆

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

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 LED驱动开发实验



上一章我们详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的chrdevbase设备为例带领大家完成了第一个字符设备驱动的开发。本章我们就开始编写第一个真正的Linux字符设备驱动。在领航者开发板上有6个LED灯,在《领航者ZYNQ之嵌入式开发指南》中已经编写过LED灯的裸机驱动,本章我们就来学习一下如何编写Linux下的LED灯驱动程序,本章我们只驱动底板上的PS_LED0灯。



22.1Linux下LED灯驱动原理
Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的LED灯驱动最终也是对ZYNQ的IO口进行配置,与裸机实验不同的是,在Linux下编写驱动要符合Linux的驱动框架。领航者开发板上的PS_LED0连接到ZYNQ的MIO7这个引脚上,因此本章实验的重点就是编写Linux下ZYNQ引脚控制驱动。关于ZYNQ的GPIO详细讲解请参考《领航者ZYNQ之嵌入式开发指南》第二章。
22.1.1地址映射
在编写驱动之前,我们需要先简单了解一下MMU这个神器,MMU全称叫做Memory Manage Unit,也就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能如下:
①完成虚拟空间到物理空间的映射。
②内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于32位的处理器来说,虚拟地址范围是2^32=4GB,我们的7010核心板搭配的是512MB的DDR3(7020是1GB),这512MB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间,如下图所示:
阿莫论坛发帖领航者专用1811.png

图 33.1.1 内存映射

物理内存只有512MB,虚拟内存有4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究,因为MMU是很复杂的一个东西,后续有时间的话正点原子Linux团队会专门做MMU专题教程。
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。比如ZYNQ的GPIO模块寄存器基地址为0xE000A000。如果没有开启MMU的话直接读写0xE000A000这个地址是没有问题的,现在开启了MMU,并且设置了内存映射,因此就不能直接向0xE000A000这个地址写入数据了。我们必须得到0xE000A000这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。
1、ioremap函数
ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h文件中,定义如下:
示例代码22.1.1.1 ioremap函数
  1. 1 #define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
  2. 2
  3. 3 void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
  4. 4 {
  5. 5    return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
  6. 6 }
复制代码

        ioremap是个宏,有两个参数:cookie和size,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:
phys_addr:要映射给的物理起始地址。
size:要映射的内存空间大小。
mtype:ioremap的类型,可以选择MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED和MT_DEVICE_WC,ioremap函数选择MT_DEVICE。
返回值:__iomem类型的指针,指向映射后的虚拟空间首地址。
假如我们要获取ZYNQ的APER_CLK_CTRL寄存器对应的虚拟地址,使用如下代码即可:
  1. #define APER_CLK_CTRL       0xF800012C
  2. static void __iomem *aper_clk_ctrl_addr;
  3. aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
复制代码

宏定义APER_CLK_CTRL是寄存器物理地址,aper_clk_ctrl_addr是该物理地址映射后的虚拟地址。对于ZYNQ来说一个寄存器是4字节(32位)的,因此映射的内存长度为4。映射完成以后直接对aper_clk_ctrl_addr进行读写操作即可。
2、iounmap函数
卸载驱动的时候需要使用iounmap函数释放掉ioremap函数所做的映射,iounmap函数原型如下:
示例代码22.1.1.2 iounmap函数原型
  1. void iounmap (volatile void __iomem *addr)
复制代码

iounmap只有一个参数addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉APER_CLK_CTRL寄存器的地址映射,使用如下代码即可:
  1. iounmap(aper_clk_ctrl_addr);
复制代码

22.1.2I/O内存访问函数
这里说的I/O是输入/输出的意思,并不是我们学习单片机的时候讲的GPIO引脚。这里涉及到两个概念:I/O端口和I/O内存。当外部寄存器或内存映射到IO空间时,称为I/O端口。当外部寄存器或内存映射到内存空间时,称为I/O内存。但是对于ARM来说没有I/O空间这个概念,因此ARM体系下只有I/O内存(可以直接理解为内存)。使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
1、读操作函数
读操作函数有如下几个:
示例代码22.1.2.1 读操作函数
  1. 1 u8  readb(const volatile void __iomem *addr)
  2. 2 u16 readw(const volatile void __iomem *addr)
  3. 3 u32 readl(const volatile void __iomem *addr)
复制代码

        readb、readw和readl这三个函数分别对应8bit、16bit和32bit读操作,参数addr就是要读取写内存地址,返回值就是读取到的数据。
2、写操作函数
写操作函数有如下几个:
示例代码22.1.2.2 写操作函数
  1. 1 void writeb(u8 value,  volatile void __iomem *addr)
  2. 2 void writew(u16 value, volatile void __iomem *addr)
  3. 3 void writel(u32 value, volatile void __iomem *addr)
复制代码

        writeb、writew和writel这三个函数分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr是要写入的地址。
22.2ZYNQ GPIO相关寄存器讲解
在《领航者ZYNQ之嵌入式开发指南》文档的第二章中给大家详细的介绍过ZYNQ的GPIO模块,如果大家不明白的可以看看该文档;因为本小节我们将直接对GPIO相关的寄存器进行操作,所以在此之前有必要向大家简单地说明下ZYNQ GPIO相关的寄存器控制。在我们提供给大家的资料包中有一份文档,路径为:领航者ZYNQ开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\Xilinx\User Guide\ug585-Zynq-7000-TRM.pdf;找到Appx. B: Register Details章中的General Purpose I/O (gpio)小节,也就是文档的1346页,如下所示:
阿莫论坛发帖领航者专用13529.png

图 33.2.1 gpio寄存器

在这里可以看到ZYNQ GPIO寄存器的基地址是0xE000A000,接下来重点给大家介绍DATA寄存器、DIRM寄存器、OUTEN寄存器以及INTDIS寄存器。
22.2.1DATA寄存器
阿莫论坛发帖领航者专用13681.png

图 33.2.2 DATA寄存器

从上面的描述信息就可以知道,DATA寄存器就是控制管脚输出高低电平的,所以在输出模式下,就可以通过对寄存器的相应bit位写0或1来控制某个GPIO输出电平为高还是低。这里面有4个,前面2个(XGPIOPS_DATA、DATA1)控制的是ZYNQ的Bank0和Bank1两组的GPIO,而DATA_2和DATA_3则是控制EMIO中的GPIO;因为本实验以PS_LED0为例,它连接的是MIO7,所以可以通过控制第一个DATA寄存器(寄存器地址偏移量为0x40)的bit7位来控制该管脚,例如下面的代码可以将MIO7管脚拉低:
val = readl(data_addr);                // 读取data寄存器数据
val &= ~(0x1U << 7);                // 将bit7位数据清零
writel(val, data_addr);                // 将数据写入寄存器
22.2.2DIRM寄存器
DIRM是Direction mode的缩写,所以从名字可以知道该寄存器是控制GPIO的输入、输出方向的:
阿莫论坛发帖领航者专用14181.png

图 33.2.3 DIRM寄存器

同样DIRM寄存器也有4个,因为我们控制的是MIO7管脚,所以只需要对第一个DIRM寄存器进行控制即可,它对应的寄存器地址偏移量为0x204。
阿莫论坛发帖领航者专用14315.png

图 33.2.4 DIRM寄存器描述

从寄存器的描述信息可以知道,向寄存器相应的bit位写入0则表示将该GPIO作为输入模式,写入1则表示将该GPIO作为输出模式,是吧,非常的简单,没有任何花里胡哨的东西!
22.2.3OUTEN寄存器
看这个名字就知道这个寄存器是干啥的,输出使能嘛;也就是啥呢,你上面将GPIO设置为输出模式后还不能真正的作为输出使用,你还得使能它才行。
阿莫论坛发帖领航者专用14542.png

图 33.2.5 OUTEN寄存器

阿莫论坛发帖领航者专用14605.png

图 33.2.6 OUTEN寄存器描述

从上面的描述信息可以知道,将bit位置1就是使能输出功能,置0则禁止输出功能;同样这个寄存器也有4个,对于MIO7我们只需要控制第一个即可,地址偏移量为0x208。
22.2.4INTDIS寄存器
INTDIS就是Interrupt Disable的缩写,所以该寄存器是用来禁止GPIO中断功能的:
阿莫论坛发帖领航者专用14814.png

图 33.2.7 INTDIS寄存器

同样写入1表示禁止中断,但是写入0并不是使能中断功能,它的中断使能和禁止是使用两个不同的寄存器来控制的,这里大家要清楚。
阿莫论坛发帖领航者专用14939.png

图 33.2.8 INTDIS寄存器描述

同样该寄存器也有4个,对于MIO7,我们只需要控制第一个即可,它的地址偏移量为0x214。
22.2.5GPIO时钟
我们需要把GPIO模块的时钟给打开,GPIO才能正常工作,那么如何使能GPIO时钟呢?我们可以通过控制APER_CLK_CTRL寄存器。APER_CLK_CTRL寄存器属于系统级别的控制寄存器(system level control register),打开收据手册的Apx. B: Register Details章中的System Level Control Register小节中找到该寄存器:
阿莫论坛发帖领航者专用15260.png

图 33.2.9 SLRC部分寄存器

我们可以看到SLRC部分寄存器的基地址是0xF8000000,往下看可以找到我们这里说的APER_CLK_CTRL寄存器,如下所示:
阿莫论坛发帖领航者专用15391.png

图 33.2.10 APER_CLK_CTRL寄存器

该寄存器的地址偏移量为0x12C,加上前面的基地址,所以可以知道该寄存器地址的绝对偏移量为0xF800012C。点击蓝色字体跳转到该寄存器的描述页面:
阿莫论坛发帖领航者专用15539.png

图 33.2.11 APER_CLK_CTRL描述信息

该寄存器用于控制ZYNQ AMBA外设时钟,从图中可以知道该寄存器的bit22位GPIO时钟控制位,向该位写入0禁止GPIO时钟,写入1则使能GPIO时钟,所以目标就很明确了!
上面已经向大家介绍了本章需要使用到的所有寄存器了,那么在驱动源码中就不给大家再去一一讲解了!细心的同学可能会发现,为什么没有关于管脚电气特性相关的初始化呢?例如什么上下拉、什么IO速率等之类的,那么这些问题留给大家去想一想,我们会在后面的章节给大家解答!
22.3硬件原理图分析
本章我们以领航者底板上的PS_LED0为例,打开我们提供给大家的领航者开发板的底板原理图,LED部分原理图如下所示:
阿莫论坛发帖领航者专用15896.png

图 33.3.1 LED原理图

阿莫论坛发帖领航者专用15957.png

图 33.3.2 PS-LED0绑定的管脚

从图 33.3.1中可以知道,当PS_LED0输出为高电平的时候点亮LED,相反低电平的时候LED灭。从图 33.3.2中知道PS_LED0与ZYNQ的MIO7引脚相连。所以接下来我们需要做的就是对MIO7引脚进行控制输出高电平点亮LED,输出低电平则LED灭!
22.4实验程序编写
本实验对应的例程路径为:ZYNQ开发板光盘资料(A盘)\4_SourceCode\ZYNQ_7010\3_Embedded_Linux\Linux驱动例程\2_led。
本章实验编写Linux下的LED灯驱动,可以通过应用程序对领航者开发板上的LED灯进行开关操作。
22.4.1LED灯驱动程序编写
在drivers目录下新建名为“2_led”的文件夹,如下所示:
阿莫论坛发帖领航者专用16389.png

图 33.4.1 2_led文件夹

进入到2_led目录下,新建一个名为led.c的驱动源文件,输入如下内容:
示例代码22.4.1.1 led.c驱动示例代码
  1. 1 /***************************************************************
  2.   2  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  3.   3  文件名    : led.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
  24. 24 #define LED_MAJOR         200       /* 主设备号 */
  25. 25 #define LED_NAME          "led"     /* 设备名字 */
  26. 26
  27. 27 /*
  28. 28  * GPIO相关寄存器地址定义
  29. 29  */
  30. 30 #define ZYNQ_GPIO_REG_BASE        0xE000A000
  31. 31 #define DATA_OFFSET               0x00000040
  32. 32 #define DIRM_OFFSET               0x00000204
  33. 33 #define OUTEN_OFFSET              0x00000208
  34. 34 #define INTDIS_OFFSET             0x00000214
  35. 35 #define APER_CLK_CTRL             0xF800012C
  36. 36
  37. 37 /* 映射后的寄存器虚拟地址指针 */
  38. 38 static void __iomem *data_addr;
  39. 39 static void __iomem *dirm_addr;
  40. 40 static void __iomem *outen_addr;
  41. 41 static void __iomem *intdis_addr;
  42. 42 static void __iomem *aper_clk_ctrl_addr;
  43. 43
  44. 44
  45. 45 /*
  46. 46  * @description         : 打开设备
  47. 47  * @param – inode       : 传递给驱动的inode
  48. 48  * @param - filp        : 设备文件,file结构体有个叫做private_data的成员变量
  49. 49  *                        一般在open的时候将private_data指向设备结构体。
  50. 50  * @return              : 0 成功;其他 失败
  51. 51  */
  52. 52 static int led_open(struct inode *inode, struct file *filp)
  53. 53 {
  54. 54     return 0;
  55. 55 }
  56. 56
  57. 57 /*
  58. 58  * @description         : 从设备读取数据
  59. 59  * @param - filp        : 要打开的设备文件(文件描述符)
  60. 60  * @param - buf         : 返回给用户空间的数据缓冲区
  61. 61  * @param - cnt         : 要读取的数据长度
  62. 62  * @param - offt        : 相对于文件首地址的偏移
  63. 63  * @return              : 读取的字节数,如果为负值,表示读取失败
  64. 64  */
  65. 65 static ssize_t led_read(struct file *filp, char __user *buf,
  66. 66                         size_t cnt, loff_t *offt)
  67. 67 {
  68. 68     return 0;
  69. 69 }
  70. 70
  71. 71 /*
  72. 72  * @description         : 向设备写数据
  73. 73  * @param - filp        : 设备文件,表示打开的文件描述符
  74. 74  * @param - buf         : 要写给设备写入的数据
  75. 75  * @param - cnt         : 要写入的数据长度
  76. 76  * @param - offt        : 相对于文件首地址的偏移
  77. 77  * @return              : 写入的字节数,如果为负值,表示写入失败
  78. 78  */
  79. 79 static ssize_t led_write(struct file *filp, const char __user *buf,
  80. 80                         size_t cnt, loff_t *offt)
  81. 81 {
  82. 82     int ret;
  83. 83     int val;
  84. 84     char kern_buf[1];
  85. 85
  86. 86     ret = copy_from_user(kern_buf, buf, cnt);// 得到应用层传递过来的数据
  87. 87     if(0 > ret) {
  88. 88         printk(KERN_ERR "kernel write failed!\r\n");
  89. 89         return -EFAULT;
  90. 90     }
  91. 91
  92. 92     val = readl(data_addr);
  93. 93     if (0 == kern_buf[0])
  94. 94         val &= ~(0x1U << 7);    // 如果传递过来的数据是0则关闭led
  95. 95     else if (1 == kern_buf[0])
  96. 96         val |= (0x1U << 7);     // 如果传递过来的数据是1则点亮led
  97. 97
  98. 98     writel(val, data_addr);
  99. 99     return 0;
  100. 100 }
  101. 101
  102. 102 /*
  103. 103  * @description         : 关闭/释放设备
  104. 104  * @param – filp        : 要关闭的设备文件(文件描述符)
  105. 105  * @return              : 0 成功;其他 失败
  106. 106  */
  107. 107 static int led_release(struct inode *inode, struct file *filp)
  108. 108 {
  109. 109     return 0;
  110. 110 }
  111. 111
  112. 112 /* 设备操作函数 */
  113. 113 static struct file_operations led_fops = {
  114. 114     .owner   = THIS_MODULE,
  115. 115     .open    = led_open,
  116. 116     .read    = led_read,
  117. 117     .write   = led_write,
  118. 118     .release = led_release,
  119. 119 };
  120. 120
  121. 121 static int __init led_init(void)
  122. 122 {
  123. 123     u32 val;
  124. 124     int ret;
  125. 125
  126. 126     /* 1.寄存器地址映射 */
  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     /* 2.使能GPIO时钟 */
  134. 134     val = readl(aper_clk_ctrl_addr);
  135. 135     val |= (0x1U << 22);
  136. 136     writel(val, aper_clk_ctrl_addr);
  137. 137
  138. 138     /* 3.关闭中断功能 */
  139. 139     val |= (0x1U << 7);
  140. 140     writel(val, intdis_addr);
  141. 141
  142. 142     /* 4.设置GPIO为输出功能 */
  143. 143     val = readl(dirm_addr);
  144. 144     val |= (0x1U << 7);
  145. 145     writel(val, dirm_addr);
  146. 146
  147. 147     /* 5.使能GPIO输出功能 */
  148. 148     val = readl(outen_addr);
  149. 149     val |= (0x1U << 7);
  150. 150     writel(val, outen_addr);
  151. 151
  152. 152     /* 6.默认关闭LED */
  153. 153     val = readl(data_addr);
  154. 154     val &= ~(0x1U << 7);
  155. 155     writel(val, data_addr);
  156. 156
  157. 157     /* 7.注册字符设备驱动 */
  158. 158     ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
  159. 159     if(0 > ret){
  160. 160         printk(KERN_ERR "Register LED driver failed!\r\n");
  161. 161         return ret;
  162. 162     }
  163. 163
  164. 164     return 0;
  165. 165 }
  166. 166
  167. 167 static void __exit led_exit(void)
  168. 168 {
  169. 169     /* 1.卸载设备 */
  170. 170     unregister_chrdev(LED_MAJOR, LED_NAME);
  171. 171
  172. 172     /* 2.取消内存映射 */
  173. 173     iounmap(data_addr);
  174. 174     iounmap(dirm_addr);
  175. 175     iounmap(outen_addr);
  176. 176     iounmap(intdis_addr);
  177. 177     iounmap(aper_clk_ctrl_addr);
  178. 178 }
  179. 179
  180. 180 /* 驱动模块入口和出口函数注册 */
  181. 181 module_init(led_init);
  182. 182 module_exit(led_exit);
  183. 183
  184. 184 MODULE_AUTHOR("DengTao <<a href="mailto:773904075@qq.com">773904075@qq.com</a>>");
  185. 185 MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
  186. 186 MODULE_LICENSE("GPL");
复制代码

        第24~25行,定义了两个宏,设备名字和设备的主设备号。
第30~35行,本实验要用到的寄存器宏定义。
第38~42行,经过内存映射以后的寄存器地址指针。
第52~55行,led_open函数,为空函数,可以自行在此函数中添加相关内容,一般在此函数中将设备结构体作为参数filp的私有数据(filp->private_data)。
第65~69行,led_read函数,为空函数,如果想在应用程序中读取LED的状态,那么就可以在此函数中添加相应的代码,比如读取MIO 的DATA寄存器的值,然后返回给应用程序。
第79~100行,led_write函数,实现对LED灯的开关操作,当应用程序调用write函数向led设备写数据的时候此函数就会执行。首先通过函数copy_from_user获取应用程序发送过来的操作信息(打开还是关闭LED),最后根据应用程序的操作信息来控制寄存器打开或关闭LED灯。
第107~110行,led_release函数,为空函数,可以自行在此函数中添加相关内容,一般关闭设备的时候会释放掉led_open函数中添加的私有数据。
第113~119行,设备文件操作结构体led_fops的定义和初始化。
第121~165行,驱动入口函数led_init,此函数实现了LED的初始化工作,127~131行通过ioremap函数获取物理寄存器地址映射后的虚拟地址,得到寄存器对应的虚拟地址以后就可以完成相关初始化工作了。比如使能GPIO时钟、关闭MIO7的中断功能、配置并使能MIO7的输出功能等。最后,最重要的一步!使用register_chrdev函数注册led这个字符设备。
第167~178行,驱动出口函数led_exit,首先使用函数unregister_chrdev注销led这个字符设备,然后调用iounmap函数取消内存映射,因为设备已经被卸载,也就意味用不到了,必须要取消映射;需要注意的是这两顺序不要反了,不能在设备没有卸载的情况下,你就把人家的内存映射给取消了,这是不合理的!
第181~182行,使用module_init和module_exit这两个函数指定led设备驱动加载和卸载函数。
第184~186行,添加模块LICENSE、作者信息以及模块描述信息。
22.4.2编写测试APP
编写测试APP,led驱动加载成功以后手动创建/dev/led节点,应用APP通过操作/dev/led文件来完成对LED设备的控制。向/dev/led文件写0表示关闭LED灯,写1表示打开LED灯。在我们的drivers目录下新建一个名为ledApp.c测试文件,在里面输入如下内容:
示例代码22.4.2.1 ledApp.c示例代码
  1. 1 /***************************************************************
  2.   2  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  3.   3  文件名                 : led-app.c
  4.   4  作者                   : 邓涛
  5.   5  版本                   : V1.0
  6.   6  描述                   : LED驱测试APP。
  7.   7  其他                   : 无
  8.   8  使用方法               : ./ledApp /dev/led 0 关闭LED
  9.   9                            ./ledApp /dev/led 1 打开LED      
  10. 10  论坛                   : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  11. 11  日志                   : 初版V1.0 2019/1/30 邓涛创建
  12. 12  ***************************************************************/
  13. 13
  14. 14 #include <stdio.h>
  15. 15 #include <unistd.h>
  16. 16 #include <sys/types.h>
  17. 17 #include <sys/stat.h>
  18. 18 #include <fcntl.h>
  19. 19 #include <stdlib.h>
  20. 20 #include <string.h>
  21. 21
  22. 22 /*
  23. 23  * @description         : main主程序
  24. 24  * @param - argc        : argv数组元素个数
  25. 25  * @param - argv        : 具体参数
  26. 26  * @return              : 0 成功;其他 失败
  27. 27  */
  28. 28 int main(int argc, char *argv[])
  29. 29 {
  30. 30     int fd, ret;
  31. 31     unsigned char buf[1];
  32. 32
  33. 33     if(3 != argc) {
  34. 34         printf("Usage:\n"
  35. 35                "\t./ledApp /dev/led 1          @ close LED\n"
  36. 36                "\t./ledApp /dev/led 0          @ open LED\n"
  37. 37                );
  38. 38         return -1;
  39. 39     }
  40. 40
  41. 41     /* 打开设备 */
  42. 42     fd = open(argv[1], O_RDWR);
  43. 43     if(0 > fd) {
  44. 44         printf("file %s open failed!\r\n", argv[1]);
  45. 45         return -1;
  46. 46     }
  47. 47
  48. 48     /* 将字符串转换为int型数据 */
  49. 49     buf[0] = atoi(argv[2]);
  50. 50
  51. 51     /* 向驱动写入数据 */
  52. 52     ret = write(fd, buf, sizeof(buf));
  53. 53     if(0 > ret){
  54. 54         printf("LED Control Failed!\r\n");
  55. 55         close(fd);
  56. 56         return -1;
  57. 57     }
  58. 58
  59. 59     /* 关闭设备 */
  60. 60     close(fd);
  61. 61     return 0;
  62. 62 }
复制代码

        ledApp.c的内容还是很简单的,就是对led的驱动文件进行最基本的打开、关闭、写操作等,具体代码就不给大家进行讲解了!
22.5运行测试
22.5.1编译驱动程序和测试APP
1、编译驱动程序
首先我们还是得需要一个Makefile文件,直接将31.8使用的Makefile文件拷贝到本实验目录下(2_led目录下),然后打开Makefile文件,将obj-m变量的值改为led.o,Makefile内容如下所示:
示例代码22.5.1.1 Makefile文件内容示例
  1. 1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
  2.   2
  3.   3 obj-m := led.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变量的值为led.o。
修改完成之后保存保存退出即可,那么此时我们的2_led目录下就已经有3个文件了,如下所示:
阿莫论坛发帖领航者专用115354.png

图 33.5.1 2_led目录下的文件

输入如下命令编译驱动模块文件:
make
编译成功以后就会生成一个名为“led.ko”的驱动模块文件,如下所示:
阿莫论坛发帖领航者专用115477.png

图 33.5.2 编译led驱动

2、编译测试APP
输入如下命令编译测试ledApp.c这个测试程序:
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp这个应用程序。
22.5.2运行测试
将上一小节编译出来的led.ko和ledApp这两个文件拷贝到开发板根文件系统的/lib/modules/4.14.0-xilinx目录下,重启开发板,进入到目录/lib/modules/4.14.0-xilinx目录,输入如下命令加载led.ko驱动模块:
  1. depmod                                //第一次加载驱动的时候需要运行此命令
  2. modprobe led.ko                //加载驱动
复制代码

驱动加载成功以后创建“/dev/led”设备节点,命令如下:
  1. mknod /dev/led c 200 0
复制代码

驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令点亮底板上的PS_LED0小灯:
  1. ./ledApp /dev/led 1                //点亮LED灯
复制代码

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

输入上述命令以后查看底板上的PS_LED0小灯是否熄灭,如果熄灭的话说明我们编写的LED驱动工作完全正常!至此,我们成功编写了第一个真正的Linux驱动设备程序。
如果要卸载驱动的话输入如下命令即可:
  1. rmmod led.ko
复制代码

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

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2024-4-25 14:42

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

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