【正点原子Linux连载】第六章 嵌入式Linux LED驱动开发实验 摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南

1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban

第六章 嵌入式Linux LED驱动开发实验

上一章我们详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的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

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值