当前位置: 首页 > news >正文

RK3568 Linux驱动学习——Linux LED驱动开发

前言

上一章详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的 chrdevbase 设备为例完成了第一个字符设备驱动的开发。本章就开始编写第一个真正的 Linux 字符设备驱动。在正点原子 ATK-DLRK3568 开发板上有一个 LED 灯,本章就来学习一下如何编写 Linux 下的 LED 灯驱动。

Linux 下 LED 驱动原理

Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动最终也是对 RK3568 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux的驱动框架。开发板上的 LED 连接到 RK3568 的 GPIO0_C0 这个引脚上,因此本章实验的重点就是编写 Linux 下 RK3568 引脚控制驱动。

地址映射

在编写驱动之前,需要先简单了解一下 MMU,MMU 全称叫做 Memory
Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下:

  1. 完成虚拟空间到物理空间的映射。
  2. 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

重点来看一下第1点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了
解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB(64 位的处理器则是 2^64=18.45 x 10^18 GB,即从 0 到 2^64-1 的范围。这个地址范围比 32 位处理器的地址范围要大得多,可以支持更大的内存空间,提高了计算机的性能)。例如开发板上有 1GB 的 DDR3,这 1GB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间,如下图所示:

内存映射

物理内存只有 1GB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上,虚拟地址范围比物理地址范围大的问题处理器自会处理。

Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。比如 RK3568 的 GPIO0_C0 引脚的 IO 复用寄存器 PMU_GRF_GPIO0C_IOMUX_L 物理地址为 0xFDC20010。如果没有开启 MMU 的话直接向 0xFDC20010)这个寄存器地址写入数据就可以配置 GPIO0_C0 的引脚的复用功能。现在开启了 MMU,并且设置了内存映射,因此
就不能直接向 0xFDC20010 这个地址写入数据了。必须得到 0xFDC20010 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap

  • ioremap 函数

ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间,定义在 arch/arm/include/asm/io.h 文件中,定义如下:

void __iomem *ioremap(resource_size_t res_cookie, size_t size);

函数的实现是在 arch/arm/mm/ioremap.c 文件中,实现如下:

void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{return arch_ioremap_caller(res_cookie, size, MT_DEVICE,__builtin_return_address(0));
}
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 进行读写操作即可。

  • iounmap 函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:

void iounmap(volatile void __iomem *iomem_cookie);

iounmap 只有一个参数 iomem_cookie,此参数就是要取消映射的虚拟地址空间首地址。假如现在要取消掉 PMU_GRF_GPIO0C_IOMUX_L_PI 寄存器的地址映射,使用如下代码即可:

iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);

I/O 内存访问函数

这里说的 I/O 是输入/输出的意思,涉及到两个概念:I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作

  • 读操作函数

读操作函数有如下几个:

u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

  • 写操作函数

写操作函数有如下几个:

void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要写入的数值,addr 是要写入的地址。

硬件原理图分析

先进行 LED 的硬件原理分析,打开开发板底板原理图,可以看到开发板有一个 LED,如下图所示:
LED 原理图

从上图可以看出,LED 接到了 GPIO0_C0(WORKING_LEDN_H)上,当 GPIO0_C0 输出高电平(1)的时候 Q1 这个三极管就能导通,LED (DS1)这个绿色的发光二极管就会点亮。当 GPIO0_C0 输出低电平(0)的时候 Q1 这个三极管就会关闭,发光二极管 LED (DS1)不会导通,因此 LED 也就不会点亮。所以 LED 的亮灭取决于 GPIO0_C0 的输出电平,输出 1 就亮,输出 0 就灭

RK3568 GPIO 驱动原理

以 GPIO0_C0 为例,讲一下如何驱动 RK3568 的某一个 IO,应该做那些工作,操作哪些寄存器等。这里就要用到 RK3568 的参考手册。

引脚复用设置

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 这个寄存器,寄存器描述如下图所示:

PMU_GRF_GPIO0C_IOMUX_L  寄存器描述

从上图可以看出 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

引脚驱动能力设置

RK3568 的 IO 引脚可以设置不同的驱动能力,GPIO0_C0 的驱动能力设置寄存器为 PMU_GRF_GPIO0C_DS_0,寄存器结构如下图所示:

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

GPIO 输入输出设置

GPIO 是双向的,也就是既可以做输入,也可以做输出。本章使用 GPIO0_C0 来控制 LED 灯的亮灭,因此要设置为输出。

GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 这两个寄存器用于设置 GPIO 的输入输出功能。RK3568 一共有 GPIO0、GPIO1、GPIO2、GPIO3 和 GPIO4 这五组 GPIO。其中 GPIO0-3 这四组每组都有 A0-A7、B0-B7、C0-C7 和 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 如下图所示:

引脚对应 bit

GPIO0_C0 很明显要用到 GPIO_SWPORT_DDR_H 寄存器,寄存器描述如下图所示:

GPIO_SWPORT_DDR_H  寄存器

GPIO_SWPORT_DDR_H 寄存器地址也是 base+offset,其中 GPIO0~GPIO4 的基地址如下图所示:

GPIO 基地址

所以 GPIO0_C0 对应的 GPIO_SWPORT_DDR_H 基地址就是 0xFDD60000+0X000C=0XFDD6000C

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

GPIO 引脚高低电平设置

GPIO 配置好以后就可以控制引脚输出高低电平了,需要用到 GPIO_SWPORT_DR_L 和 GPIO_SWPORT_DR_H 这两个寄存器,这两个原理和上面讲的 GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 一样,这里就不再赘述了。

GPIO0_C0 需要用到 GPIO_SWAPORT_DR_H 寄存器,寄存器描述如下图所示:
GPIO_SWAPORT_DR_H 寄存器

同样的,GPIO0_C0 对应 bit0,如果要输出低电平,那么 bit0 置 0,如果要输出高电平,bit0 置 1。bit16 也要置 1,允许写 bit0。

实验程序编写

本章实验编写 Linux 下的 LED 灯驱动,可以通过应用程序对开发板上的 LED0 进行开关操作。

LED 灯驱动程序编写

新建名为 “02_led” 文件夹,然后在 02_led 文件夹里面创建 VSCode 工程,工作区命名为 “led”。工程创建好以后新建 led.c 文件,此文件就是 led 的驱动文件,在 led.c 里面输入如下内容:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
//#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: led.c
作者	  	: 正点原子
版本	   	: V1.0
描述	   	: LED驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2022/12/02 正点原子团队创建
***************************************************************/
#define LED_MAJOR		200		/* 主设备号 */
#define LED_NAME		"led" 	/* 设备名字 */#define LEDOFF 	0				/* 关灯 */
#define LEDON 	1				/* 开灯 */#define PMU_GRF_BASE						(0xFDC20000)
#define PMU_GRF_GPIO0C_IOMUX_L				(PMU_GRF_BASE + 0x0010)
#define PMU_GRF_GPIO0C_DS_0					(PMU_GRF_BASE + 0X0090)#define GPIO0_BASE						(0xFDD60000)
#define GPIO0_SWPORT_DR_H				(GPIO0_BASE + 0X0004)
#define GPIO0_SWPORT_DDR_H				(GPIO0_BASE + 0X000C)/* 映射后的寄存器虚拟地址指针 */
static void __iomem *PMU_GRF_GPIO0C_IOMUX_L_PI;
static void __iomem *PMU_GRF_GPIO0C_DS_0_PI;
static void __iomem *GPIO0_SWPORT_DR_H_PI;
static void __iomem *GPIO0_SWPORT_DDR_H_PI;/** @description		: LED打开/关闭* @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED* @return 			: 无*/
void led_switch(u8 sta)
{u32 val = 0;if(sta == LEDON) {val = readl(GPIO0_SWPORT_DR_H_PI);val &= ~(0X1 << 0); /* bit0 清零*/val |= ((0X1 << 16) | (0X1 << 0));	/* bit16 置1,允许写bit0,bit0,高电平*/writel(val, GPIO0_SWPORT_DR_H_PI);}else if(sta == LEDOFF) { val = readl(GPIO0_SWPORT_DR_H_PI);val &= ~(0X1 << 0); /* bit0 清零*/val |= ((0X1 << 16) | (0X0 << 0));	/* bit16 置1,允许写bit0,bit0,低电平	*/writel(val, GPIO0_SWPORT_DR_H_PI);}	
}/** @description		: 物理地址映射* @return 			: 无*/
void led_remap(void)
{PMU_GRF_GPIO0C_IOMUX_L_PI = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4);PMU_GRF_GPIO0C_DS_0_PI = ioremap(PMU_GRF_GPIO0C_DS_0, 4);GPIO0_SWPORT_DR_H_PI = ioremap(GPIO0_SWPORT_DR_H, 4);GPIO0_SWPORT_DDR_H_PI = ioremap(GPIO0_SWPORT_DDR_H, 4);
}/** @description		: 取消映射* @return 			: 无*/
void led_unmap(void)
{/* 取消映射 */iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);iounmap(PMU_GRF_GPIO0C_DS_0_PI);iounmap(GPIO0_SWPORT_DR_H_PI);iounmap(GPIO0_SWPORT_DDR_H_PI);
}/** @description		: 打开设备* @param - inode 	: 传递给驱动的inode* @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量* 					  一般在open的时候将private_data指向设备结构体。* @return 			: 0 成功;其他 失败*/
static int led_open(struct inode *inode, struct file *filp)
{return 0;
}/** @description		: 从设备读取数据 * @param - filp 	: 要打开的设备文件(文件描述符)* @param - buf 	: 返回给用户空间的数据缓冲区* @param - cnt 	: 要读取的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 读取的字节数,如果为负值,表示读取失败*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description		: 向设备写数据 * @param - filp 	: 设备文件,表示打开的文件描述符* @param - buf 	: 要写给设备写入的数据* @param - cnt 	: 要写入的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 写入的字节数,如果为负值,表示写入失败*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;unsigned char databuf[1];unsigned char ledstat;retvalue = copy_from_user(databuf, buf, cnt);if(retvalue < 0) {printk("kernel write failed!\r\n");return -EFAULT;}ledstat = databuf[0];		/* 获取状态值 */if(ledstat == LEDON) {	led_switch(LEDON);		/* 打开LED灯 */} else if(ledstat == LEDOFF) {led_switch(LEDOFF);		/* 关闭LED灯 */}return 0;
}/** @description		: 关闭/释放设备* @param - filp 	: 要关闭的设备文件(文件描述符)* @return 			: 0 成功;其他 失败*/
static int led_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数 */
static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release = 	led_release,
};/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static int __init led_init(void)
{int retvalue = 0;u32 val = 0;/* 初始化LED *//* 1、寄存器地址映射 */led_remap();/* 2、设置GPIO0_C0为GPIO功能。*/val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI);val &= ~(0X7 << 0);	/* bit2:0,清零 */val |= ((0X7 << 16) | (0X0 << 0));	/* bit18:16 置1,允许写bit2:0,bit2:0:0,用作GPIO0_C0	*/writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI);/* 3、设置GPIO0_C0驱动能力为level5 */val = readl(PMU_GRF_GPIO0C_DS_0_PI);val &= ~(0X3F << 0);	/* bit5:0清零*/val |= ((0X3F << 16) | (0X3F << 0));	/* bit21:16 置1,允许写bit5:0,bit5:0:0,用作GPIO0_C0	*/writel(val, PMU_GRF_GPIO0C_DS_0_PI);/* 4、设置GPIO0_C0为输出 */val = readl(GPIO0_SWPORT_DDR_H_PI);val &= ~(0X1 << 0); /* bit0 清零*/val |= ((0X1 << 16) | (0X1 << 0));	/* bit16 置1,允许写bit0,bit0,高电平	*/writel(val, GPIO0_SWPORT_DDR_H_PI);/* 5、设置GPIO0_C0为低电平,关闭LED灯。*/val = readl(GPIO0_SWPORT_DR_H_PI);val &= ~(0X1 << 0); /* bit0 清零*/val |= ((0X1 << 16) | (0X0 << 0));	/* bit16 置1,允许写bit0,bit0,低电平	*/writel(val, GPIO0_SWPORT_DR_H_PI);/* 6、注册字符设备驱动 */retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);if(retvalue < 0) {printk("register chrdev failed!\r\n");goto fail_map;}return 0;fail_map:led_unmap();return -EIO;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit led_exit(void)
{/* 取消映射 */led_unmap();/* 注销字符设备驱动 */unregister_chrdev(LED_MAJOR, LED_NAME);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
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 模块驱动。

编写测试 APP

编写测试 APP,led 驱动加载成功以后手动创建/dev/led 节点,应用程序(APP)通过操作 /dev/led 文件来完成对 LED 设备的控制。向 /dev/led 文件写 0 表示关闭 LED 灯,写 1 表示打开 LED 灯。新建 ledApp.c 文件,在里面输入如下内容:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: ledApp.c
作者	  	: 正点原子
版本	   	: V1.0
描述	   	: chrdevbase驱测试APP。
其他	   	: 无
使用方法	 :./ledtest /dev/led  0 关闭LED./ledtest /dev/led  1 打开LED		
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2022/12/02 正点原子团队创建
***************************************************************/#define LEDOFF 	0
#define LEDON 	1/** @description		: main主程序* @param - argc 	: argv数组元素个数* @param - argv 	: 具体参数* @return 			: 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd, retvalue;char *filename;unsigned char databuf[1];if(argc != 3){printf("Error Usage!\r\n");return -1;}filename = argv[1];/* 打开led驱动 */fd = open(filename, O_RDWR);if(fd < 0){printf("file %s open failed!\r\n", argv[1]);return -1;}databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 *//* 向/dev/led文件写入数据 */retvalue = write(fd, databuf, sizeof(databuf));if(retvalue < 0){printf("LED Control Failed!\r\n");close(fd);return -1;}retvalue = close(fd); /* 关闭文件 */if(retvalue < 0){printf("file %s close failed!\r\n", argv[1]);return -1;}return 0;
}

ledApp.c 的内容还是很简单的,就是对 led 的驱动文件进行最基本的打开、关闭、写操作等。

运行测试

编译驱动程序和测试 APP

  • 编译驱动程序

编写 Makefile 文件,本章实验的 Makefile 文件和第五章实验基本一样,只是将 obj-m 变量 的值改为 led.o,Makefile 内容如下所示:

KERNELDIR := /home/xhj/rk3568_linux_sdk/kernel
CURRENT_PATH := $(shell pwd)
obj-m := led.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第 4 行,设置 obj-m 变量的值为 led.o。

输入如下命令编译出驱动模块文件:

make ARCH=arm64 //ARCH=arm64 必须指定,否则编译会失败

编译成功以后就会生成一个名为 “led.ko” 的驱动模块文件。

  • 编译测试 APP

输入如下命令编译测试 ledApp.c 这个测试程序:

/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp

编译成功以后就会生成 ledApp 这个应用程序。

运行测试

  • 关闭心跳灯

正点原子出厂系统将 LED 这个绿色的 LED 灯设置成了系统心跳灯,应该能看到这个板子上这个红色的 LED 灯一闪一闪的,提示系统正在运行。很明显,这个会干扰本章实验结果,需要先临时关闭系统心跳灯功能,在开发板中输入如下命令:

echo none > /sys/class/leds/work/trigger

上述命令就是临时关闭 LED 的心跳灯功能,开发板重启以后 LED 又会重新作为心跳灯。要想永久关闭 LED0 的心跳灯功能,需要修改设备树,这个后面会讲怎么将一个 LED 灯用作心跳灯。

  • 加载并测试驱动

在 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
http://www.lryc.cn/news/618918.html

相关文章:

  • Linux NAPI 实现机制深度解析
  • 【Oracle APEX开发小技巧16】交互式网格操作内容根据是否启用进行隐藏/展示
  • 2025年渗透测试面试题总结-16(题目+回答)
  • 力扣(LeetCode) ——移除链表元素(C语言)
  • 飞算AI:企业智能化转型的新引擎
  • 【电子硬件】EMI中无源晶振的优势
  • SpringBoot项目部署
  • string 类运算符重载
  • Win10系统Ruby+Devkit3.4.5-1安装
  • qt界面优化--api绘图
  • SpringBoot项目限制带参数接口配置使用数量实现
  • php+apache+nginx 更换域名
  • 力扣.870优势洗牌解决方法: 下标排序​编辑力扣.942增减字符串匹配最长回文子序列牛客.背包问题(最大体积)力扣.45跳跃游戏II 另一种思考
  • 牛客疑难题(6)
  • Transformer的编码器与解码器模块深度解析及python实现完整案例
  • 树:数据结构中的层次架构
  • 前端基础知识NodeJS系列 - 06( Node 中的 Stream 的理解?应用场景?)
  • 【154页PPT】某大型再生资源集团管控企业数字化转型SAP解决方案(附下载方式)
  • 【从零开始java学习|第三篇】变量与数据类型的关联
  • 扣子空间深度解析
  • Apache 服务器基础配置与虚拟主机部署
  • CentOS 7.9 升级 GLibc 2.34
  • (C++)继承全解析及运用
  • Java 大视界 -- Java 大数据在智能教育学习效果评估指标体系构建与精准评估中的应用(394)
  • 教程 | 用Parasoft SOAtest实现高效CI回归测试
  • Day02——Docker
  • 一体化步进伺服电机在无人机舱门应用中的应用案例
  • 书籍数组中未出现的最小正整数(8)0812
  • 小白挑战一周上架元服务——ArkUI04
  • Ubuntu与Rocky系统安装Java全指南