本帖最后由 正点原子运营 于 2024-2-28 14:15 编辑
第十八章 嵌入式Linux LED驱动开发实验
1)实验平台:正点原子 DFZU2EG_4EV MPSoC开发板
2) 章节摘自【正点原子】DFZU2EG_4EV MPSoC开发板之嵌入式Linux 驱动开发指南 V1.0
6)Linux技术交流QQ群:887820935
上一章我们详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的chrdevbase设备为例带领大家完成了第一个字符设备驱动的开发。本章我们就开始编写第一个真正的Linux字符设备驱动。在MPSoC开发板上有4个LED灯,在《DFZU2EG_4EVMPSoC之嵌入式Vitis开发指南》中已经编写过LED灯的裸机驱动,本章我们就来学习一下如何编写Linux下的LED灯驱动程序,本章我们只驱动底板上的PS_LED1灯。
1.1 Linux下LED灯驱动原理Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的LED灯驱动最终也是对ZYNQ MPSoC的IO口进行配置,与裸机实验不同的是,在Linux下编写驱动要符合Linux的驱动框架。DFZU2EG_4EV MPSoC开发板上的PS_LED1连接到ZYNQ MPSoC的MIO38这个引脚上,因此本章实验的重点就是编写Linux下ZYNQ MPSoC引脚控制驱动。关于ZYNQ MPSoC的GPIO详细讲解请参考《DFZU2EG_4EV MPSoC之嵌入式开发指南》第二章。 1.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的虚拟空间,如下图所示: 物理内存只有512MB,虚拟内存有4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究,因为MMU是很复杂的一个东西,后续有时间的话正点原子Linux团队会专门做MMU专题教程。 Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。比如ZYNQ MPSoC的GPIO模块寄存器基地址为0xFF0A0000。如果没有开启MMU的话直接读写0xFF0A0000这个地址是没有问题的,现在开启了MMU,并且设置了内存映射,因此就不能直接向0xFF0A0000这个地址写入数据了。我们必须得到0xFF0A0000这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。 1、ioremap函数 ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm64/include/asm/io.h文件中,定义如下: - 示例代码22.1.1.1 ioremap函数
- 1 #define ioremap(cookie,size)__arm_ioremap((cookie), (size),MT_DEVICE)
- 2
- 3 void__iomem * __arm_ioremap(phys_addr_tphys_addr, size_t size, unsigned intmtype)
- 4 {
- 5 returnarch_ioremap_caller(phys_addr, size,mtype, __builtin_return_address(0));
- 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 MPSoC的APER_CLK_CTRL寄存器对应的虚拟地址,使用如下代码即可: - #defineAPER_CLK_CTRL 0xFF5E00AC
- staticvoid __iomem *aper_clk_ctrl_addr;
- aper_clk_ctrl_addr= ioremap(APER_CLK_CTRL, 4);
复制代码宏定义APER_CLK_CTRL是寄存器物理地址,aper_clk_ctrl_addr是该物理地址映射后的虚拟地址。对于ZYNQ MPSoC来说一个寄存器是4字节(32位)的,因此映射的内存长度为4。映射完成以后直接对aper_clk_ctrl_addr进行读写操作即可。 2、iounmap函数 卸载驱动的时候需要使用iounmap函数释放掉ioremap函数所做的映射,iounmap函数原型如下: - 示例代码22.1.1.2 iounmap函数原型
- voidiounmap (volatile void__iomem *addr)
复制代码iounmap只有一个参数addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉APER_CLK_CTRL寄存器的地址映射,使用如下代码即可: - iounmap(aper_clk_ctrl_addr);
复制代码 1.1.2 I/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 读操作函数
- 1u8 readb(const volatile void__iomem *addr)
- 2 u16readw(const volatile void__iomem *addr)
- 3 u32readl(const volatile void__iomem *addr)
复制代码 readb、readw和readl这三个函数分别对应8bit、16bit和32bit读操作,参数addr就是要读取写内存地址,返回值就是读取到的数据。 2、写操作函数 写操作函数有如下几个: - 示例代码22.1.2.2 写操作函数
- 1 voidwriteb(u8 value, volatile void__iomem *addr)
- 2 voidwritew(u16 value, volatile void__iomem *addr)
- 3 voidwritel(u32 value, volatile void__iomem *addr)
复制代码 writeb、writew和writel这三个函数分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr是要写入的地址。 1.2 ZYNQ MPSoCGPIO相关寄存器讲解在《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》文档的第二章中给大家详细的介绍过ZYNQ MPSoC的GPIO模块,如果大家不明白的可以看看该文档;因为本小节我们将直接对GPIO相关的寄存器进行操作,所以在此之前有必要向大家简单地说明下ZYNQ MPSoC GPIO相关的寄存器控制。在Xlinx官网搜索ug1087(zynq-ultrascale-registers)文档,找到GPIO Module小节,如下所示: 在这里可以看到ZYNQ MPSoC GPIO寄存器的基地址是0xFF0A0000,接下来重点给大家介绍DATA寄存器、DIRM寄存器、OUTEN寄存器以及INTDIS寄存器。 1.2.1 DATA寄存器 从上面的描述信息就可以知道,DATA寄存器就是控制管脚输出高低电平的,所以在输出模式下,就可以通过对寄存器的相应bit位写0或1来控制某个GPIO输出电平为高还是低。这里面有6个,前面3个(DATA0、DATA1、DATA2)控制的是ZYNQ MPSoC的Bank0、Bank1和Bank2三组的GPIO,而DATA_3、DATA_4、DATA_5则是控制EMIO中的GPIO;因为本实验以PS_LED1为例,它连接的是MIO38,所以可以通过控制第二个DATA寄存器(DATA1寄存器,地址偏移量为0x44)的bit12位来控制该管脚,例如下面的代码可以将MIO38管脚拉低: - val= readl(data_addr); // 读取data寄存器数据
- val&= ~(0x1U << 12); // 将bit12位数据清零
- writel(val,data_addr); // 将数据写入寄存器
复制代码 1.2.2 DIRM寄存器DIRM是Direction mode的缩写,所以从名字可以知道该寄存器是控制GPIO的输入、输出方向的: 同样DIRM寄存器也有6个,因为我们控制的是MIO38管脚,所以只需要对第二个DIRM寄存器进行控制即可,它对应的寄存器地址偏移量为0x244。 从寄存器的描述信息可以知道,向寄存器相应的bit位写入0则表示将该GPIO作为输入模式,写入1则表示将该GPIO作为输出模式,是吧,非常的简单,没有任何花里胡哨的东西! 1.2.3 OUTEN寄存器看这个名字就知道这个寄存器是干啥的,输出使能嘛;也就是啥呢,你上面将GPIO设置为输出模式后还不能真正的作为输出使用,你还得使能它才行。 从上面的描述信息可以知道,将bit位置1就是使能输出功能,置0则禁止输出功能;同样这个寄存器也有6个,对于MIO38我们只需要控制第二个OUTEN寄存器即可,地址偏移量为0x248。 1.2.4 INTDIS寄存器INTDIS就是Interrupt Disable的缩写,所以该寄存器是用来禁止GPIO中断功能的: 同样写入1表示禁止中断,但是写入0并不是使能中断功能,它的中断使能和禁止是使用两个不同的寄存器来控制的,这里大家要清楚。 同样该寄存器也有6个,对于MIO38,我们只需要控制第二个寄存器即可(INT_DIS_1),它的地址偏移量为0x254。 1.2.5 GPIO时钟我们需要把GPIO模块的时钟给打开,GPIO才能正常工作,那么如何使能GPIO时钟呢?我们可以通过控制LPD_LSBUS_CTRL寄存器。打开数据手册ug1087搜索LPD_LSBUS_CTRL寄存器: 图 18.2.9 CRL_APBModule部分寄存器 我们可以看到CRL_APB Module部分寄存器的基地址是0xFF5E0000,往下看可以找到我们这里说的LPD_LSBUS_CTRL寄存器,如下所示: 图 18.2.10 LPD_LSBUS_CTRL寄存器 该寄存器的地址偏移量为0xAC,加上前面的基地址,所以可以知道该寄存器地址的绝对偏移量为0xFF5E00AC。该寄存器的描述如下所示: 图 18.2.11 LPD_LSBUS_CTRL描述信息 该寄存器用于控制ZYNQ MPSoC APB接口和GPIO 逻辑的时钟,从图中可以知道该寄存器的bit24位GPIO时钟控制位,向该位写入0禁止GPIO时钟,写入1则使能GPIO时钟,所以目标就很明确了! 上面已经向大家介绍了本章需要使用到的所有寄存器了,那么在驱动源码中就不给大家再去一一讲解了!细心的同学可能会发现,为什么没有关于管脚电气特性相关的初始化呢?例如什么上下拉、什么IO速率等之类的,那么这些问题留给大家去想一想,我们会在后面的章节给大家解答! 1.3 硬件原理图分析本章我们以DFZU2EG_4EV MPSoC开发板上的PS_LED1为例,打开我们提供给大家的开发板的底板原理图,LED部分原理图如下所示: 从图 18.3.1中可以知道,当PS_LED1输出为高电平的时候点亮LED,相反低电平的时候LED灭。从图 18.3.2中知道PS_LED1与ZYNQ MPSoC的MIO38引脚相连。所以接下来我们需要做的就是对MIO38引脚进行控制输出高电平点亮LED,输出低电平则LED灭! 1.4 实验程序编写本实验对应的例程路径为:开发板光盘资料(A盘)\4_SourceCode\3_Embedded_Linux\Linux驱动例程\2_led。 本章实验编写Linux下的LED灯驱动,可以通过应用程序对开发板上的LED灯进行开关操作。 1.4.1 LED灯驱动程序编写在drivers目录下新建名为“2_led”的文件夹,如下所示: 进入到2_led目录下,新建一个名为led.c的驱动源文件,输入如下内容: - 示例代码22.4.1.1 led.c驱动示例代码
- 1 /***************************************************************
- 2 Copyright ©ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
- 3 文件名 : led.c
- 4 作者 : 邓涛
- 5 版本 : V1.0
- 6 描述 : ZYNQ LED驱动文件。
- 7 其他 : 无
- 8 论坛 : www.openedv.com
- 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
- 24 #defineLED_MAJOR 200 /* 主设备号 */
- 25 #defineLED_NAME "led" /* 设备名字 */
- 26
- 27 /*
- 28 * GPIO相关寄存器地址定义
- 29 */
- 30 #defineZYNQ_GPIO_REG_BASE 0xFF0A0000
- 31 #defineDATA_OFFSET 0x00000044
- 32 #defineDIRM_OFFSET 0x00000244
- 33 #defineOUTEN_OFFSET 0x00000248
- 34 #defineINTDIS_OFFSET 0x00000254
- 35 #defineAPER_CLK_CTRL 0xFF5E00AC
- 36
- 37 /* 映射后的寄存器虚拟地址指针 */
- 38 static void__iomem *data_addr;
- 39 static void__iomem *dirm_addr;
- 40 static void__iomem *outen_addr;
- 41 static void__iomem *intdis_addr;
- 42 static void__iomem *aper_clk_ctrl_addr;
- 43
- 44
- 45 /*
- 46 * @description : 打开设备
- 47 * @param – inode : 传递给驱动的inode
- 48 * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
- 49 * 一般在open的时候将private_data指向设备结构体。
- 50 * @return : 0 成功;其他 失败
- 51 */
- 52 static intled_open(struct inode *inode, struct file*filp)
- 53 {
- 54 return 0;
- 55 }
- 56
- 57 /*
- 58 * @description : 从设备读取数据
- 59 * @param - filp : 要打开的设备文件(文件描述符)
- 60 * @param - buf : 返回给用户空间的数据缓冲区
- 61 * @param - cnt : 要读取的数据长度
- 62 * @param - offt : 相对于文件首地址的偏移
- 63 * @return : 读取的字节数,如果为负值,表示读取失败
- 64 */
- 65 staticssize_t led_read(struct file *filp, char__user *buf,
- 66 size_t cnt,loff_t *offt)
- 67 {
- 68 return 0;
- 69 }
- 70
- 71 /*
- 72 * @description : 向设备写数据
- 73 * @param - filp : 设备文件,表示打开的文件描述符
- 74 * @param - buf : 要写给设备写入的数据
- 75 * @param - cnt : 要写入的数据长度
- 76 * @param - offt : 相对于文件首地址的偏移
- 77 * @return : 写入的字节数,如果为负值,表示写入失败
- 78 */
- 79 staticssize_t led_write(struct file *filp, const char__user *buf,
- 80 size_t cnt,loff_t *offt)
- 81 {
- 82 int ret;
- 83 int val;
- 84 char kern_buf[1];
- 85
- 86 ret =copy_from_user(kern_buf, buf, cnt);// 得到应用层传递过来的数据
- 87 if(0 > ret) {
- 88 printk(KERN_ERR"kernel write failed!\r\n");
- 89 return -EFAULT;
- 90 }
- 91
- 92 val =readl(data_addr);
- 93 if (0 ==kern_buf[0])
- 94 val &= ~(0x1U << 12); // 如果传递过来的数据是0则关闭led
- 95 else if (1 ==kern_buf[0])
- 96 val |= (0x1U << 12); // 如果传递过来的数据是1则点亮led
- 97
- 98 writel(val,data_addr);
- 99 return 0;
- 100 }
- 101
- 102 /*
- 103 * @description : 关闭/释放设备
- 104 * @param – filp : 要关闭的设备文件(文件描述符)
- 105 * @return : 0 成功;其他 失败
- 106 */
- 107 static intled_release(struct inode *inode, struct file*filp)
- 108 {
- 109 return 0;
- 110 }
- 111
- 112 /* 设备操作函数 */
- 113 static structfile_operations led_fops = {
- 114 .owner = THIS_MODULE,
- 115 .open =led_open,
- 116 .read =led_read,
- 117 .write = led_write,
- 118 .release= led_release,
- 119 };
- 120
- 121 static int__init led_init(void)
- 122 {
- 123 u32 val;
- 124 int ret;
- 125
- 126 /* 1.寄存器地址映射 */
- 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 /* 2.使能GPIO时钟 */
- 134 val =readl(aper_clk_ctrl_addr);
- 135 val |= (0x1U << 24);
- 136 writel(val,aper_clk_ctrl_addr);
- 137
- 138 /* 3.关闭中断功能 */
- 139 val |= (0x1U << 12);
- 140 writel(val,intdis_addr);
- 141
- 142 /* 4.设置GPIO为输出功能 */
- 143 val =readl(dirm_addr);
- 144 val |= (0x1U << 12);
- 145 writel(val,dirm_addr);
- 146
- 147 /* 5.使能GPIO输出功能 */
- 148 val =readl(outen_addr);
- 149 val |= (0x1U << 12);
- 150 writel(val,outen_addr);
- 151
- 152 /* 6.默认关闭LED */
- 153 val =readl(data_addr);
- 154 val &= ~(0x1U << 12);
- 155 writel(val,data_addr);
- 156
- 157 /* 7.注册字符设备驱动 */
- 158 ret =register_chrdev(LED_MAJOR,LED_NAME, &led_fops);
- 159 if(0 > ret){
- 160 printk(KERN_ERR"Register LED driver failed!\r\n");
- 161 return ret;
- 162 }
- 163
- 164 return 0;
- 165 }
- 166
- 167 static void__exit led_exit(void)
- 168 {
- 169 /* 1.卸载设备 */
- 170 unregister_chrdev(LED_MAJOR,LED_NAME);
- 171
- 172 /* 2.取消内存映射 */
- 173 iounmap(data_addr);
- 174 iounmap(dirm_addr);
- 175 iounmap(outen_addr);
- 176 iounmap(intdis_addr);
- 177 iounmap(aper_clk_ctrl_addr);
- 178 }
- 179
- 180 /* 驱动模块入口和出口函数注册 */
- 181 module_init(led_init);
- 182 module_exit(led_exit);
- 183
- 184 MODULE_AUTHOR("DengTao<773904075@qq.com>");
- 185MODULE_DESCRIPTION("AlientekZYNQ GPIO LED Driver");
- 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、作者信息以及模块描述信息。 1.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 /***************************************************************
- 2 Copyright ©ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
- 3 文件名 : led-app.c
- 4 作者 : 邓涛
- 5 版本 : V1.0
- 6 描述 : LED驱测试APP。
- 7 其他 : 无
- 8 使用方法 : ./ledApp /dev/led 0 关闭LED
- 9 ./ledApp /dev/led 1打开LED
- 10 论坛 : www.openedv.com
- 11 日志 : 初版V1.0 2019/1/30 邓涛创建
- 12 ***************************************************************/
- 13
- 14 #include <stdio.h>
- 15 #include <unistd.h>
- 16 #include <sys/types.h>
- 17 #include <sys/stat.h>
- 18 #include <fcntl.h>
- 19 #include <stdlib.h>
- 20 #include <string.h>
- 21
- 22 /*
- 23 * @description : main主程序
- 24 * @param - argc : argv数组元素个数
- 25 * @param - argv : 具体参数
- 26 * @return : 0 成功;其他 失败
- 27 */
- 28 int main(int argc, char *argv[])
- 29 {
- 30 int fd, ret;
- 31 unsigned char buf[1];
- 32
- 33 if(3 != argc) {
- 34 printf("Usage:\n"
- 35 "\t./ledApp/dev/led 1 @ close LED\n"
- 36 "\t./ledApp /dev/led0 @ open LED\n"
- 37 );
- 38 return -1;
- 39 }
- 40
- 41 /* 打开设备 */
- 42 fd = open(argv[1],O_RDWR);
- 43 if(0 > fd) {
- 44 printf("file%s open failed!\r\n", argv[1]);
- 45 return -1;
- 46 }
- 47
- 48 /* 将字符串转换为int型数据 */
- 49 buf[0] = atoi(argv[2]);
- 50
- 51 /* 向驱动写入数据 */
- 52 ret =write(fd, buf, sizeof(buf));
- 53 if(0 > ret){
- 54 printf("LEDControl Failed!\r\n");
- 55 close(fd);
- 56 return -1;
- 57 }
- 58
- 59 /* 关闭设备 */
- 60 close(fd);
- 61 return 0;
- 62 }
复制代码 ledApp.c的内容还是很简单的,就是对led的驱动文件进行最基本的打开、关闭、写操作等,具体代码就不给大家进行讲解了!
1.5 运行测试
1.5.1 编译驱动程序和测试APP1、编译驱动程序 首先我们还是得需要一个Makefile文件,直接将17.4.3小节使用的Makefile文件拷贝到本实验目录下(2_led目录下),然后打开Makefile文件,将obj-m变量的值改为led.o,Makefile内容如下所示: - 示例代码22.5.1.1 Makefile文件内容示例
- 1 KERN_DIR := /home/shang/git.d/linux-xlnx
- 2
- 3 obj-m := led.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变量的值为led.o。 修改完成之后保存保存退出即可,那么此时我们的2_led目录下就已经有3个文件了,如下所示: 输入如下命令编译驱动模块文件: 编译成功以后就会生成一个名为“led.ko”的驱动模块文件,如下所示: 2、编译测试APP 输入如下命令编译测试ledApp.c这个测试程序: 编译成功以后就会生成ledApp这个应用程序。 1.5.2 运行测试将上一小节编译出来的led.ko和ledApp这两个文件拷贝到开发板根文件系统的/lib/modules/4.19.0目录下,重启开发板,进入到目录/lib/modules/4.19.0目录,输入如下命令加载led.ko驱动模块: - depmod //第一次加载驱动的时候需要运行此命令
- modprobe led.ko //加载驱动
复制代码驱动加载成功以后创建“/dev/led”设备节点,命令如下: 驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令点亮底板上的PS_LED0小灯: - ./ledApp /dev/led 1 //点亮LED灯
复制代码输入上述命令以后查看底板上的PS_LED0小灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯: - ./ledApp /dev/led 0 //关闭LED灯
复制代码输入上述命令以后查看底板上的PS_LED0小灯是否熄灭,如果熄灭的话说明我们编写的LED驱动工作完全正常!至此,我们成功编写了第一个真正的Linux驱动设备程序。 如果要卸载驱动的话输入如下命令即可: |