赞
踩
1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
上一章我们详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的chrdevbase设备为例带领大家完成了第一个字符设备驱动的开发。本章我们就开始编写第一个真正的Linux字符设备驱动。在正点原子ATK-DLRK3568开发板上有一个LED灯,本章我们就来学习一下如何编写Linux下的LED灯驱动。
6.1 Linux下LED灯驱动原理
Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的LED灯驱动最终也是对RK3568的IO口进行配置,与裸机实验不同的是,在Linux下编写驱动要符合Linux的驱动框架。开发板上的LED连接到RK3568的GPIO0_C0这个引脚上,因此本章实验的重点就是编写Linux下RK3568引脚控制驱动。
6.1.1 地址映射
在编写驱动之前,我们需要先简单了解一下MMU这个神器,MMU全称叫做Memory Manage Unit,也就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能如下:
①、完成虚拟空间到物理空间的映射。
②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于32位的处理器来说,虚拟地址范围是232=4GB(64位的处理器则是264=18.45 x 10^18 GB,即从 0 到 2^64-1 的范围。这个地址范围比 32 位处理器的地址范围要大得多,可以支持更大的内存空间,提高了计算机的性能)。例如我们的开发板上有1GB的DDR3,这1GB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间,如图6.1.2所示:
图6.1.2 内存映射
物理内存只有1GB,虚拟内存有4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究,因为MMU是很复杂的一个东西。
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。比如RK3568的GPIO0_C0引脚的IO复用寄存器PMU_GRF_GPIO0C_IOMUX_L物理地址为0xFDC20010。如果没有开启MMU的话直接向0xFDC20010)这个寄存器地址写入数据就可以配置GPIO0_C0的引脚的复用功能。现在开启了MMU,并且设置了内存映射,因此就不能直接向0xFDC20010这个地址写入数据了。我们必须得到0xFDC20010这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。
1、ioremap函数
ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h文件中,定义如下:
示例代码6.1.1.1 ioremap函数声明
431 void __iomem *ioremap(resource_size_t res_cookie, size_t size);
函数的实现是在arch/arm/mm/ioremap.c文件中,实现如下:
示例代码6.1.1.2 ioremap函数实现
376 void __iomem *ioremap(resource_size_t res_cookie, size_t size)
377 {
378 return arch_ioremap_caller(res_cookie, size, MT_DEVICE,
379 __builtin_return_address(0));
380 }
381 EXPORT_SYMBOL(ioremap);
ioremap有两个参数:res_cookie和size,真正起作用的是函数arch_ioremap_caller。ioremap函数有两个参数和一个返回值,这些参数和返回值的含义如下:
res_cookie:要映射的物理起始地址。
size:要映射的内存空间大小。
返回值:__iomem类型的指针,指向映射后的虚拟空间首地址。
假如我们要获取RK3568的PMU_GRF_GPIO0C_IOMUX_L寄存器对应的虚拟地址,使用如下代码即可:
#define PMU_GRF_GPIO0C_IOMUX_L (0xFDC20010)
static void __iomem* PMU_GRF_GPIO0C_IOMUX_L_PI;
PMU_GRF_GPIO0C_IOMUX_L_PI = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4);
宏PMU_GRF_GPIO0C_IOMUX_L是寄存器物理地址,PMU_GRF_GPIO0C_IOMUX_L_PI是映射后的虚拟地址。对于RK3568来说一个寄存器是4字节(32位),因此映射的内存长度为4。映射完成以后直接对PMU_GRF_GPIO0C_IOMUX_L_PI进行读写操作即可。
2、iounmap函数
卸载驱动的时候需要使用iounmap函数释放掉ioremap函数所做的映射,iounmap函数原型如下:
示例代码6.1.1.3 iounmap函数原型
460 void iounmap (volatile void __iomem *addr)
iounmap只有一个参数addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉PMU_GRF_GPIO0C_IOMUX_L_PI寄存器的地址映射,使用如下代码即可:
iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);
6.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、读操作函数
读操作函数有如下几个:
示例代码6.1.2.1 读操作函数
1 u8 readb(const volatile void __iomem *addr)
2 u16 readw(const volatile void __iomem *addr)
3 u32 readl(const volatile void __iomem *addr)
readb、readw和readl这三个函数分别对应8bit、16bit和32bit读操作,参数addr就是要读取写内存地址,返回值就是读取到的数据。
2、写操作函数
写操作函数有如下几个:
示例代码6.1.2.2 写操作函数
1 void writeb(u8 value, volatile void __iomem *addr)
2 void writew(u16 value, volatile void __iomem *addr)
3 void writel(u32 value, volatile void __iomem *addr)
writeb、writew和writel这三个函数分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr是要写入的地址。
6.2 硬件原理图分析
我们先进行LED的硬件原理分析,打开开发板底板原理图,底板原理图和核心板原理图都放到了开发板光盘中,路径为:开发板光盘02、开发板原理图01、底板原理图ATK_DLRK3568 VX.X(底板原理图).pdf。开发板上有一个LED,原理如图6.2.1所示:
图6.2.1 LED原理图
从图 6.2.1 可以看出,LED 接到了GPIO0_C0(WORKING_LEDN_H)上,当GPIO0_C0输出高电平(1)的时候Q1这个三极管就能导通,LED (DS1)这个绿色的发光二极管就会点亮。当GPIO0_C0输出低电平(0)的时候Q1这个三极管就会关闭,发光二极管 LED (DS1)不会导通,因此 LED 也就不会点亮。所以 LED 的亮灭取决于GPIO0_C0的输出电平,输出 1就亮,输出 0就灭。
6.3、RK3568 GPIO驱动原理讲解
我们以GPIO0_C0为例,讲一下如何驱动RK3568的某一个IO,应该做那些工作,操作哪些寄存器等。这里我们就要用到RK3568的参考手册,这个我们已经放到开发板资料里面了,路径:开发板光盘 03、核心板资料核心板板载芯片资料Rockchip RK3568 TRM Part1 V1.1-20210301.pdf(RK3568参考手册1).pdf和Rockchip RK3568 TRM Part2 V1.1-20210301(RK3568参考手册2).pdf。
6.3.1 引脚复用设置
RK3568的一个引脚一般用多个功能,也就是引脚复用,比如GPIO0_C0这个IO就可以用作:GPIO,PWM1_M0,GPU_AVS和UART0_RX这四个功能,所以我们首先要设置好当前引脚用作什么功能,这里我们要使用GPIO0_C0的GPIO功能。
打开《Rockchip RK3568 TRM Part1 V1.1-20210301(RK3568参考手册1).pdf》这份文档,找到PMU_GRF_GPIO0C_IOMUX_L这个寄存器,寄存器描述如图6.3.1.1所示:
图6.3.1.1 PMU_GRF_GPIO0C_IOMUX_L寄存器描述
从图6.3.1.1可以看出PMU_GRF_GPIO0C_IOMUX_L寄存器地址为:base+offset,其中base就是PMU_GRF外设的基地址,为0xFDC20000,offset为0x0010,所以PMU_GRF_GPIO0C_IOMUX_L寄存器地址为0xFDC20000+0x0010=0xFDC20010。
PMU_GRF_GPIO0C_IOMUX_L寄存器分为2部分:
①、bit31:16:低16位写使能位,这16个bit控制着寄存器的低16位写使能。比如bit16就对应着bit0的写使能,如要要写bit0,那么bit16要置1,也就是允许对bit0进行写操作。
②、bit15:0:功能设置位。
可以看出,PMU_GRF_GPIO0C_IOMUX_L寄存器用于设置GPIO0_C0~C3这4个IO的复用功能,其中bit2:0用于设置GPIO0_C0的复用功能,有四个可选功能:
0:GPIO0_C0
1:PWM1_M0
2:GPU_AVS
3:UART0_RX
我们要将GPIO0_C0设置为GPIO,所以PMU_GRF_GPIO0C_IOMUX_L的bit2:0这三位设置000。另外bit18:16要设置为111,允许写bit2:0。
6.3.2 引脚驱动能力设置
RK3568的IO引脚可以设置不同的驱动能力,GPIO0_C0的驱动能力设置寄存器为PMU_GRF_GPIO0C_DS_0,寄存器结构如图6.3.2.1所示:
图6.3.2.1 PMU_GRF_GPIO0C_DS_0寄存器
PMU_GRF_GPIO0C_DS_0寄存器地址为:base+offset=0xFDC20000+0X0090=0xFDC20090。
PMU_GRF_GPIO0C_DS_0寄存器也分为2部分:
①、bit31:16:低16位写使能位,这16个bit控制着寄存器的低16位写使能。比如bit16就对应着bit15:0的写使能,如要要写bit15:0,那么bit16要置1,也就是允许对bit15:0进行写操作。
②、bit15:0:功能设置位。
可以看出,PMU_GRF_GPIO0C_DS_0寄存器用于设置GPIO0_C0~C1这2个IO的驱动能力,其中bit5:0用于设置GPIO0_C0的驱动能力,一共有6级。
这里我们将GPIO0_C0的驱动能力设置为5级,所以GRF_GPIO3D_DS_H的bit5:0这六位设置111111。另外bit21:16要设置为111111,允许写bit5:0。
6.3.3 GPIO输入输出设置
GPIO是双向的,也就是既可以做输入,也可以做输出。本章我们使用GPIO0_C0来控制LED灯的亮灭,因此要设置为输出。GPIO_SWPORT_DDR_L和GPIO_SWPORT_DDR_H这两个寄存器用于设置GPIO的输入输出功能。RK3568一共有GPIO0、GPIO1、GPIO2、GPIO3和GPIO4这五组GPIO。其中GPIO03这四组每组都有A0A7、B0B7、C0C7和D0~D7这32个GPIO。每个GPIO需要一个bit来设置其输入输出功能,一组GPIO就需要32bit,GPIO_SWPORT_DDR_L和GPIO_SWPORT_DDR_H这两个寄存器就是用来设置这一组GPIO所有引脚的输入输出功能的。其中GPIO_SWPORT_DDR_L设置的是低16bit,GPIO_SWPORT_DDR_H设置的是高16bit。一组GPIO里面这32给引脚对应的bit如表6.3.3.1所示:
GPIO组 GPIOX_A0~A7 GPIOX_B0~B7 GPIOX_C0~C7 GPIOX_D0~D7
对应的bit bit0~bit7 bit8~bit15 bit16~bit23 bit24~bit31
表6.3.3.1 引脚对应的bit
GPIO0_C0很明显要用到GPIO_SWPORT_DDR_H寄存器,寄存器描述如图6.3.3.1所示:
图6.3.3.1 GPIO_SWPORT_DDR_H寄存器
GPIO_SWPORT_DDR_H寄存器地址也是base+offset,其中GPIO0~GPIO4的基地址如表6.3.3.2所示:
GPIO组 基地址
GPIO0 0xFDD60000
GPIO1 0xFE740000
GPIO2 0xFE750000
GPIO3 0xFE760000
GPIO4 0xFE770000
表6.3.3.2 GPIO基地址
所以GPIO0_C0对应的GPIO_SWPORT_DDR_H基地址就是0xFDD60000+0X000C=0X FDD6000C。
GPIO_SWPORT_DDR_H寄存器也分为2部分:
①、bit31:16:低16位写使能位,这16个bit控制着寄存器的低16位写使能。比如bit16就对应着bit0的写使能,如要要写bit0,那么bit16要置1,也就是允许对bit0进行写操作。
③、bit15:0:功能设置位。
这里我们将GPIO0_C0设置为输出,所以GPIO_SWPORT_DDR_H的bit0要置1,另外bit16要设置为1,允许写bit16。
6.3.4 GPIO引脚高低电平设置
GPIO配置好以后就可以控制引脚输出高低电平了,需要用到GPIO_SWPORT_DR_L和GPIO_SWPORT_DR_H这两个寄存器,这两个原理和上面讲的GPIO_SWPORT_DDR_L和GPIO_SWPORT_DDR_H一样,这里就不再赘述了。
GPIO0_C0需要用到GPIO_SWAPORT_DR_H寄存器,寄存器描述如图6.3.4.1所示:
图6.3.4.1 GPIO_SWPORT_DR_H寄存器
同样的,GPIO0_C0对应bit0,如果要输出低电平,那么bit0置0,如果要输出高电平,bit0置1。bit16也要置1,允许写bit0。
关于RK3568的GPIO配置原理就讲到这里。
6.4 实验程序编写
本实验对应的例程路径为:开发板光盘 06、Linux驱动例程源码 02_led。
本章实验编写Linux下的LED灯驱动,可以通过应用程序对开发板上的LED0进行开关操作。
6.4.1 LED灯驱动程序编写
新建名为“02_led”文件夹,然后在02_led文件夹里面创建VSCode工程,工作区命名为“led”。工程创建好以后新建led.c文件,此文件就是led的驱动文件,在led.c里面输入如下内容:
示例代码6.4.1.1 led.c驱动文件代码
1 #include <linux/types.h> 2 #include <linux/kernel.h> 3 #include <linux/delay.h> 4 #include <linux/ide.h> 5 #include <linux/init.h> 6 #include <linux/module.h> 7 #include <linux/errno.h> 8 #include <linux/gpio.h> 9 //#include <asm/mach/map.h> 10 #include <asm/uaccess.h> 11 #include <asm/io.h> 12 /*************************************************************** 13 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 14 文件名 : led.c 15 作者 : 正点原子 16 版本 : V1.0 17 描述 : LED驱动文件。 18 其他 : 无 19 论坛 : www.openedv.com 20 日志 : 初版V1.0 2022/12/02 正点原子团队创建 21 ***************************************************************/ 22 #define LED_MAJOR 200 /* 主设备号 */ 23 #define LED_NAME "led" /* 设备名字 */ 24 25 #define LEDOFF 0 /* 关灯 */ 26 #define LEDON 1 /* 开灯 */ 27 28 #define PMU_GRF_BASE (0xFDC20000) 29 #define PMU_GRF_GPIO0C_IOMUX_L (PMU_GRF_BASE + 0x0010) 30 #define PMU_GRF_GPIO0C_DS_0 (PMU_GRF_BASE + 0X0090) 31 32 #define GPIO0_BASE (0xFDD60000) 33 #define GPIO0_SWPORT_DR_H (GPIO0_BASE + 0X0004) 34 #define GPIO0_SWPORT_DDR_H (GPIO0_BASE + 0X000C) 35 36 /* 映射后的寄存器虚拟地址指针 */ 37 static void __iomem *PMU_GRF_GPIO0C_IOMUX_L_PI; 38 static void __iomem *PMU_GRF_GPIO0C_DS_0_PI; 39 static void __iomem *GPIO0_SWPORT_DR_H_PI; 40 static void __iomem *GPIO0_SWPORT_DDR_H_PI; 41 42 /* 43 * @description : LED打开/关闭 44 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED 45 * @return : 无 46 */ 47 void led_switch(u8 sta) 48 { 49 u32 val = 0; 50 if(sta == LEDON) { 51 val = readl(GPIO0_SWPORT_DR_H_PI); 52 val &= ~(0X1 << 0); /* bit0 清零*/ 53 val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0, 54 bit0,高电平*/ 55 writel(val, GPIO0_SWPORT_DR_H_PI); 56 }else if(sta == LEDOFF) { 57 val = readl(GPIO0_SWPORT_DR_H_PI); 58 val &= ~(0X1 << 0); /* bit0 清零*/ 59 val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0, 60 bit0,低电平 */ 61 writel(val, GPIO0_SWPORT_DR_H_PI); 62 } 63 } 64 65 /* 66 * @description : 物理地址映射 67 * @return : 无 68 */ 69 void led_remap(void) 70 { 71 PMU_GRF_GPIO0C_IOMUX_L_PI = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4); 72 PMU_GRF_GPIO0C_DS_0_PI = ioremap(PMU_GRF_GPIO0C_DS_0, 4); 73 GPIO0_SWPORT_DR_H_PI = ioremap(GPIO0_SWPORT_DR_H, 4); 74 GPIO0_SWPORT_DDR_H_PI = ioremap(GPIO0_SWPORT_DDR_H, 4); 75 } 76 77 /* 78 * @description : 取消映射 79 * @return : 无 80 */ 81 void led_unmap(void) 82 { 83 /* 取消映射 */ 84 iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI); 85 iounmap(PMU_GRF_GPIO0C_DS_0_PI); 86 iounmap(GPIO0_SWPORT_DR_H_PI); 87 iounmap(GPIO0_SWPORT_DDR_H_PI); 88 } 89 90 /* 91 * @description : 打开设备 92 * @param - inode : 传递给驱动的inode 93 * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 94 * 一般在open的时候将private_data指向设备结构体。 95 * @return : 0 成功;其他 失败 96 */ 97 static int led_open(struct inode *inode, struct file *filp) 98 { 99 return 0; 100 } 101 102 /* 103 * @description : 从设备读取数据 104 * @param - filp : 要打开的设备文件(文件描述符) 105 * @param - buf : 返回给用户空间的数据缓冲区 106 * @param - cnt : 要读取的数据长度 107 * @param - offt : 相对于文件首地址的偏移 108 * @return : 读取的字节数,如果为负值,表示读取失败 109 */ 110 static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) 111 { 112 return 0; 113 } 114 115 /* 116 * @description : 向设备写数据 117 * @param - filp : 设备文件,表示打开的文件描述符 118 * @param - buf : 要写给设备写入的数据 119 * @param - cnt : 要写入的数据长度 120 * @param - offt : 相对于文件首地址的偏移 121 * @return : 写入的字节数,如果为负值,表示写入失败 122 */ 123 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 124 { 125 int retvalue; 126 unsigned char databuf[1]; 127 unsigned char ledstat; 128 129 retvalue = copy_from_user(databuf, buf, cnt); 130 if(retvalue < 0) { 131 printk("kernel write failed!\r\n"); 132 return -EFAULT; 133 } 134 135 ledstat = databuf[0]; /* 获取状态值 */ 136 137 if(ledstat == LEDON) { 138 led_switch(LEDON); /* 打开LED灯 */ 139 } else if(ledstat == LEDOFF) { 140 led_switch(LEDOFF); /* 关闭LED灯 */ 141 } 142 return 0; 143 } 144 145 /* 146 * @description : 关闭/释放设备 147 * @param - filp : 要关闭的设备文件(文件描述符) 148 * @return : 0 成功;其他 失败 149 */ 150 static int led_release(struct inode *inode, struct file *filp) 151 { 152 return 0; 153 } 154 155 /* 设备操作函数 */ 156 static struct file_operations led_fops = { 157 .owner = THIS_MODULE, 158 .open = led_open, 159 .read = led_read, 160 .write = led_write, 161 .release = led_release, 162 }; 163 164 /* 165 * @description : 驱动出口函数 166 * @param : 无 167 * @return : 无 168 */ 169 static int __init led_init(void) 170 { 171 int retvalue = 0; 172 u32 val = 0; 173 174 /* 初始化LED */ 175 /* 1、寄存器地址映射 */ 176 led_remap(); 177 178 /* 2、设置GPIO0_C0为GPIO功能。*/ 179 val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI); 180 val &= ~(0X7 << 0); /* bit2:0,清零 */ 181 val |= ((0X7 << 16) | (0X0 << 0)); /* bit18:16 置1,允许写bit2:0, 182 bit2:0:0,用作GPIO0_C0 */ 183 writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI); 184 185 /* 3、设置GPIO0_C0驱动能力为level5 */ 186 val = readl(PMU_GRF_GPIO0C_DS_0_PI); 187 val &= ~(0X3F << 0); /* bit5:0清零*/ 188 val |= ((0X3F << 16) | (0X3F << 0)); /* bit21:16 置1,允许写bit5:0, 189 bit5:0:0,用作GPIO0_C0 */ 190 writel(val, PMU_GRF_GPIO0C_DS_0_PI); 191 192 /* 4、设置GPIO0_C0为输出 */ 193 val = readl(GPIO0_SWPORT_DDR_H_PI); 194 val &= ~(0X1 << 0); /* bit0 清零*/ 195 val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0, 196 bit0,高电平 */ 197 writel(val, GPIO0_SWPORT_DDR_H_PI); 198 199 /* 5、设置GPIO0_C0为低电平,关闭LED灯。*/ 200 val = readl(GPIO0_SWPORT_DR_H_PI); 201 val &= ~(0X1 << 0); /* bit0 清零*/ 202 val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0, 203 bit0,低电平 */ 204 writel(val, GPIO0_SWPORT_DR_H_PI); 205 206 /* 6、注册字符设备驱动 */ 207 retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops); 208 if(retvalue < 0) { 209 printk("register chrdev failed!\r\n"); 210 goto fail_map; 211 } 212 return 0; 213 214 fail_map: 215 led_unmap(); 216 return -EIO; 217 } 218 219 /* 220 * @description : 驱动出口函数 221 * @param : 无 222 * @return : 无 223 */ 224 static void __exit led_exit(void) 225 { 226 /* 取消映射 */ 227 led_unmap(); 228 229 /* 注销字符设备驱动 */ 230 unregister_chrdev(LED_MAJOR, LED_NAME); 231 } 232 233 module_init(led_init); 234 module_exit(led_exit); 235 MODULE_LICENSE("GPL"); 236 MODULE_AUTHOR("ALIENTEK"); 237 MODULE_INFO(intree, "Y");
第22~26行,定义了一些宏,包括主设备号、设备名字、LED开/关宏。
第28~34行,本实验要用到的寄存器宏定义。
第37~40行,经过内存映射以后的寄存器地址指针。
第47~63行,led_switch函数,用于控制开发板上的LED灯亮灭,当参数sta为LEDON(0)的时候打开LED灯,sta为LEDOFF(1)的时候关闭LED灯。
第69~75行,led_remap函数,通过ioremap函数获取物理寄存器地址映射后的虚拟地址。
第81~88行,led_unmap函数,取消所有物理寄存器映射,回收对应的资源。当程序出错退出或者卸载驱动模块的时候需要调用此函数,用来取消此前所做的寄存器映射。
第97~100行,led_open函数,为空函数,可以自行在此函数中添加相关内容,一般在此函数中将设备结构体作为参数filp的私有数据(filp->private_data),后面实验会讲解如何添加私有数据。
第110~113行,led_read函数,为空函数,如果想在应用程序中读取LED的状态,那么就可以在此函数中添加相应的代码。
第123~143行,led_write函数,实现对LED灯的开关操作,当应用程序调用write函数向led设备写数据的时候此函数就会执行。首先通过函数copy_from_user获取应用程序发送过来的操作信息(打开还是关闭LED),最后根据应用程序的操作信息来打开或关闭LED灯。
第150~153行,led_release函数,为空函数,可以自行在此函数中添加相关内容,一般关闭设备的时候会释放掉led_open函数中添加的私有数据。
第156~162行,设备文件操作结构体led_fops的定义和初始化。
第169~217行,驱动入口函数led_init,此函数实现了LED的初始化工作,。比如设置GPIO0_D4的复用功能、设置驱动能力等级、配置输出功能、设置默认电平等。最后,最重要的一步!使用register_chrdev函数注册led这个字符设备。
第214~216 行,如果前面注册字符设备失败,就要回收以前注册成功的资源。
第224~231行,驱动出口函数led_exit,首先使用函数iounmap取消内存映射,最后使用函数unregister_chrdev注销led这个字符设备。
第233~234行,使用module_init和module_exit这两个函数指定led设备驱动加载和卸载函数。
第235~236行,添加LICENSE和作者信息。
第237行,告诉内核这个驱动也是intree模块驱动。
6.4.2 编写测试APP
编写测试APP,led驱动加载成功以后手动创建/dev/led节点,应用程序(APP)通过操作/dev/led文件来完成对LED设备的控制。向/dev/led文件写0表示关闭LED灯,写1表示打开LED灯。新建ledApp.c文件,在里面输入如下内容:
示例代码6.4.2.1 ledApp.c文件代码
1 #include "stdio.h" 2 #include "unistd.h" 3 #include "sys/types.h" 4 #include "sys/stat.h" 5 #include "fcntl.h" 6 #include "stdlib.h" 7 #include "string.h" 8 /*************************************************************** 9 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 10 文件名 : ledApp.c 11 作者 : 正点原子 12 版本 : V1.0 13 描述 : led测试APP。 14 其他 : 无 15 使用方法 :./ledtest /dev/led 0 关闭LED 16 ./ledtest /dev/led 1 打开LED 17 论坛 : www.openedv.com 18 日志 : 初版V1.0 2022/12/02 正点原子团队创建 19 ***************************************************************/ 20 21 #define LEDOFF 0 22 #define LEDON 1 23 24 /* 25 * @description : main主程序 26 * @param - argc : argv数组元素个数 27 * @param - argv : 具体参数 28 * @return : 0 成功;其他 失败 29 */ 30 int main(int argc, char *argv[]) 31 { 32 int fd, retvalue; 33 char *filename; 34 unsigned char databuf[1]; 35 36 if(argc != 3){ 37 printf("Error Usage!\r\n"); 38 return -1; 39 } 40 41 filename = argv[1]; 42 43 /* 打开led驱动 */ 44 fd = open(filename, O_RDWR); 45 if(fd < 0){ 46 printf("file %s open failed!\r\n", argv[1]); 47 return -1; 48 } 49 50 databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */ 51 52 /* 向/dev/led文件写入数据 */ 53 retvalue = write(fd, databuf, sizeof(databuf)); 54 if(retvalue < 0){ 55 printf("LED Control Failed!\r\n"); 56 close(fd); 57 return -1; 58 } 59 60 retvalue = close(fd); /* 关闭文件 */ 61 if(retvalue < 0){ 62 printf("file %s close failed!\r\n", argv[1]); 63 return -1; 64 } 65 return 0; 66 }
ledApp.c的内容还是很简单的,就是对led的驱动文件进行最基本的打开、关闭、写操作等。
6.5 运行测试
6.5.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第五章实验基本一样,只是将obj-m变量的值改为led.o,Makefile内容如下所示:
示例代码6.5.1.1 Makefile文件
1 KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
......
4 obj-m := led.o
......
11 clean:
12 $(MAKE) -C
(
K
E
R
N
E
L
D
I
R
)
M
=
(KERNELDIR) M=
(KERNELDIR)M=(CURRENT_PATH) clean
第4行,设置obj-m变量的值为led.o。
输入如下命令编译出驱动模块文件:
make ARCH=arm64 //ARCH=arm64必须指定,否则编译会失败
编译成功以后就会生成一个名为“led.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试ledApp.c这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp这个应用程序。
6.5.2 运行测试
1、关闭心跳灯
正点原子出厂系统将LED这个绿色的LED灯设置成了系统心跳灯,大家应该能看到这个板子上这个红色的LED灯一闪一闪的,提示系统正在运行。很明显,这个会干扰我们本章实验结果,需要先临时关闭系统心跳灯功能,在开发板中输入如下命令:
echo none > /sys/class/leds/work/trigger
上述命令就是临时关闭LED的心跳灯功能,开发板重启以后LED又会重新作为心跳灯。要想永久关闭LED0的心跳灯功能,需要修改设备树,这个我们后面会讲怎么将一个LED灯用作心跳灯。
2、加载并测试驱动
在Ubuntu中将上一小节编译出来的led.ko和ledApp这两个文件通过adb命令发送到开发板的/lib/modules/4.19.232目录下,命令如下:
adb push led.ko ledApp /lib/modules/4.19.232
发送成功以后进入到目录lib/modules/4.19.232中,输入如下命令加载led.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe led //加载驱动
驱动加载成功以后创建“/dev/led”设备节点,命令如下:
mknod /dev/led c 200 0
驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/led 1 //打开LED灯
输入上述命令以后观察开发板上的绿色LED灯,也就是LED0是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:
./ledApp /dev/led 0 //关闭LED灯
输入上述命令以后观察开发板上的绿色LED灯是否熄灭,如果熄灭的话说明我们编写的LED驱动工作完全正常!至此,我们成功编写了第一个真正的Linux驱动设备程序。
如果要卸载驱动的话输入如下命令即可:
rmmod led
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。