4.1 Linux 下 LED 灯驱动原理
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动
 最终也是对 RK3588 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合
 Linux 的驱动框架。开发板上的 LED 连接到 RK3588 的 GPIO1_A3 这个引脚上,因此本章实验
 的重点就是编写 Linux 下 RK3588 引脚控制驱动。
4.1.1 地址映射
在编写驱动之前,我们需要先简单了解一下 MMU 这个神器,MMU 全称叫做 Memory
 Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在
 Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下:
 ①、完成虚拟空间到物理空间的映射。
 ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
 我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了
 解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于 64
 位的处理器来说,虚拟地址范围是 2^64=16EB(1EB=1024PB=1024*1024TB)。
 虚拟地址: 就是针对系统调用按照系统位数虚拟的地址
 物理地址: 就是实际的地址,包括SOC上和DDR上的地址
 系统要访问物理地址需要通过虚拟地址映射到物理地址进行访问。
 Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚
 拟地址。RK3588 的 GPIO1_A3 引脚的 IO 复用寄存器 BUS_IOC_GPIO1A_IOMUX_SEL_L 物
 理地址为 0xFD5F8020。如果没有开启 MMU 的话直接向 0xFD5F8020)这个寄存器地址写入数
 据就可以配置 GPIO1_A3 的引脚的复用功能。现在开启了 MMU,并且设置了内存映射,因此
 就不能直接向 0xFD5F8020 这个地址写入数据了。我们必须得到 0xFD5F8020 这个物理地址在
 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到
 两个函数:ioremap 和 iounmap。
 1、ioremap 函数
 ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间,定义在
 arch/arm/include/asm/io.h 文件中,定义如下:
 示例代码 4.1-1 ioremap 函数声明
431 void __iomem *ioremap(resource_size_t res_cookie, size_t size);
函数的实现是在 arch/arm/mm/ioremap.c 文件中,实现如下:
 示例代码 4.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 类型的指针,指向映射后的虚拟空间首地址。
 假如我们要获取 RK3588 的 BUS_IOC_GPIO1A_IOMUX_SEL_L 寄存器对应的虚拟地址,
 使用如下代码即可:
#define BUS_IOC_GPIO1A_IOMUX_SEL_L (0xFD5F8020)
static void __iomem* BUS_IOC_GPIO1A_IOMUX_SEL_L_PI;
BUS_IOC_GPIO1A_IOMUX_SEL_L_PI = ioremap(BUS_IOC_GPIO1A_IOMUX_SEL_L, 4);
对于 RK3588 来说一个寄存器是4 字节(32 位),因此映射的内存长度为 4。映射完成以后直接对BUS_IOC_GPIO1A_IOMUX_SEL_L_PI 进行读写操作即可。
2、iounmap 函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原
 型如下:
 示例代码 4.1-3 iounmap 函数原型
460 void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现
 在要取消掉 BUS_IOC_GPIO1A_IOMUX_SEL_L_PI 寄存器的地址映射,使用如下代码即可:
 iounmap(BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);
4.1.2I/O 内存访问函数
使用 ioremap 函数将寄存器
 的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不
 建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
 1、读操作函数
 读操作函数有如下几个:
 示例代码 4.1-4 读操作函数
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、写操作函数
 写操作函数有如下几个:
 示例代码 4.1-5 写操作函数
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 是要写入的地址。
4.2 硬件原理图分析
![图片[1] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/2eb2f109b22148e2bfb4acd839845809.png)
 图中R113是限流电阻,避免NPN三极管基极电流过大烧毁三极管,取值计算方法如下:
 三极管进入饱和态:基极电流
 
假设 LED 电流
 Ic ≈ 5 ~ 10mA,三极管 β ≈ 100。 则基极电流
 𝐼𝑏≈0.1𝑚𝐴
 假设 GPIO 输出高电平 3.3V,V_BE ≈ 0.7V:
 ![图片[2] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/3065a386de7e4b9b8d80b8f7622cf87d.png)
 实际使用中,通常会放大电流裕量,10kΩ 是比较常见的保守值,确保三极管能充分导通。
 R115 的作用(下拉电阻):
 作用:R115 是下拉电阻,用于在 GPIO 输出为高阻态(Hi-Z)或系统上电初始化期间,防止三极管误导通。
 保证 Q4 基极电位默认为低电平,避免 LED 误亮。提高系统上电稳定性。
 数值选择:
 数值需远大于 R113,不能分流太多基极电流,一般选用几十 kΩ 到几百 kΩ。
 51kΩ 是一个常见的折中选择。
三极管(以NPN型为例)具有以下三个工作状态:
截止区(Cutoff Region)
 条件:基极-发射极电压
 𝑉𝐵𝐸<0.7𝑉(未导通)
 特征:基极电流
 𝐼𝐵≈0,集电极电流 𝐼𝐶≈0
 作用:三极管完全关闭,相当于开关断开
放大区(Active Region)
 条件:
 𝑉𝐵𝐸≈0.7𝑉,且 𝑉𝐶𝐸>𝑉𝐵𝐸
特征:三极管工作在线性放大状态,
 𝐼𝐶≈𝛽⋅𝐼𝐵
 作用:主要用于模拟信号放大,不是开关工作区
          饱和区(Saturation Region)
 条件:基极电流足够大,使得
 𝑉𝐶𝐸≈0.2𝑉
特征:三极管“完全导通”,
 𝐼𝐶不再严格依赖
 𝐼𝐵
作用:相当于导通的开关,集电极与发射极接近短路
4.3 RK3588 GPIO 驱动原理讲解
4.3.1 引脚复用设置
RK3588 的一个引脚一般用多个功能,也就是引脚复用,比如 GPIO1_A3 这个 IO 就可以
 用作:GPIO,HDMI_TX1_SDA_M2、SPI4_CS0_M2、I2C4_SCL_M3、UART6_CTSN_M1、
 PWM1_M2 这六个功能,所以我们首先要设置好当前引脚用作什么功能,这里我们要使用
 GPIO1_A3 的 GPIO 功能。
 打开《Rockchip RK3588 TRM V1.0-Part1-20220309(RK3588 参考手册 1).pdf》这份文
 档,找到 BUS_IOC_GPIO1A_IOMUX_SEL_L 这个寄存器,寄存器描述如下图所示:
 ![图片[3] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/7851c6d838be47aea9ccb4f61e42fa89.png)
 BUS_IOC_GPIO1A_IOMUX_SEL_L 寄存器地址为:base+offset,其中 base 就是 PMU_GRF 外设的基地址,为 0xFD5F8000,offset 为 0x0020,所以BUS_IOC_GPIO1A_IOMUX_SEL_L 寄存器地址为 0xFD5F8000 + 0x0020 = 0xFD5F8020。
 BUS_IOC_GPIO1A_IOMUX_SEL_L 寄存器分为 2 部分:
 ① 、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16
 就对应着 bit0 的写使能,如要要写 bit0,那么 bit16 要置 1,也就是允许对 bit0 进行写
 操作。
 ② 、bit15:0:功能设置位。
 可以看出,BUS_IOC_GPIO1A_IOMUX_SEL_L 寄存器用于设置 GPIO1_A0~A3 这 4 个 IO
 的复用功能,其中 bit15:12 用于设置 GPIO1_A3 的复用功能,有六个可选功能:
 0:GPIO0_C0
 5:HDMI_TX1_SDA_M2
 8:SPI4_CS0_M2
 9:I2C4_SCL_M3
 a:UART6_CTSN_M1
 b:PWM1_M2
 我们要将 GPIO1_A3 设置为 GPIO,所以 BUS_IOC_GPIO1A_IOMUX_SEL_L 的 bit15:12
 这四位设置 0000。另外 bit31:28 要设置为 1111,允许写 bit15:12
4.3.2引脚驱动能力设置
RK3588 的 IO 引脚可以设置不同的驱动能力,GPIO1_A3 的驱动能力设置寄存器为
 VCCIO1_4_IOC_GPIO1A_DS_L,寄存器结构如下图所示:
 ![图片[4] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/89b317b86e044b9f8bffd98f4ed2ead5.png)
 VCCIO1_4_IOC_GPIO1A_DS_L 寄存器地址为:
 base+offset=0xFD5F9000 + 0x0020 = 0xFD5F9020。
 VCCIO1_4_IOC_GPIO1A_DS_L 寄存器也分为 2 部分:
 ① 、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16
 就对应着 bit15:0 的写使能,如要要写 bit15:0,那么 bit16 要置 1,也就是允许对 bit15:0
 进行写操作。
 ② 、bit15:0:功能设置位。
 可以看出,VCCIO1_4_IOC_GPIO1A_DS_L 寄存器用于设置 GPIO1_A0~A3 这 4 个 IO 的
 驱动能力,其中 bit14:12 用于设置 GPIO1_A3 的驱动能力,一共有 6 级。
 这里我们将 GPIO1_A3 的驱动能力设置为 40ohm,所以 VCCIO1_4_IOC_GPIO1A_DS_L
 的 bit14:12 这三位设置 110。另外 bit30:28 要设置为 111,允许写 bit14:12。
 这的阻值指的是输出电阻,值越小,驱动越强
 不同的引脚功能需要配置不同的驱动能力。
4.3.3GPIO 输入输出设置
GPIO 是双向的,也就是既可以做输入,也可以做输出。本章我们使用 GPIO1_A1 来控制
 LED 灯的亮灭,因此要设置为输出。GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 这
 两个寄存器用于设置 GPIO 的输入输出功能。RK3588 一共有 GPIO0、GPIO1、GPIO2、
 GPIO3 和 GPIO4 这五组 GPIO。其中 GPIO0~3 这四组每组都有 A0A7、B0B7、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 如下表所
 示:
 ![图片[5] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/5f2a41bbbdae474187adebf62ac99678.png)
 GPIO1_A3 很明显要用到 GPIO_SWPORT_DDR_L 寄存器,寄存器描述如下图所示:
 ![图片[6] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/45f4911b18a44d068ddbc7fcf33f49aa.png)
 GPIO_SWPORT_DDR_L 寄存器地址也是 base+offset,其中 GPIO0~GPIO4 的基地址如下
 表所示:
 ![图片[7] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/1aac2aa7589b49b88902d7170f0f299a.png)
 所以 GPIO1_A3 对应的 GPIO_SWPORT_DDR_L 基地址就是
 0xFEC20000+0X0008=0xFEC20008。
 GPIO_SWPORT_DDR_L 寄存器也分为 2 部分:
 ①、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16
 就对应着 bit0 的写使能,如要要写 bit0,那么 bit16 要置 1,也就是允许对 bit0 进行写操作。
 ③ 、bit15:0:功能设置位。
 这里我们将 GPIO1_A3 设置为输出,所以 GPIO_SWPORT_DDR_L 的 bit3 要置 1,另外
 bit19 要设置为 1,允许写 bit19。
4.3.4 GPIO 引脚高低电平设置
GPIO 配置好以后就可以控制引脚输出高低电平了,需要用到 GPIO_SWPORT_DR_L 和
 GPIO_SWPORT_DR_H 这两个寄存器,这两个原理和上面讲的 GPIO_SWPORT_DDR_L 和
 GPIO_SWPORT_DDR_H 一样,这里就不再赘述了。
 GPIO1_A1 需要用到 GPIO_SWAPORT_DR_L 寄存器,寄存器描述如下图所示:
 ![图片[8] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/adb59d51cf874f2ab3682bb48c7e9673.png)
 同样的,GPIO1_A3 对应 bit3,如果要输出低电平,那么 bit3 置 0,如果要输出高电平,
 bit3 置 1。bit19 也要置 1,允许写 bit3。
4.4 实验程序编写
4.4.1 LED 灯驱动程序编写
#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/uaccess.h>
#include <asm/io.h>
/* 主设备号 设备名称 */
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* PMU_GRF_BASE 外设的基地址,为 0xFD5F8000 */
#define PMU_GRF_BASE (0xFD5F8000)
#define BUS_IOC_BASE (0xFD5F8000)
/* IO功能复用寄存器 */
#define BUS_IOC_GPIO1A_IOMUX_SEL_L (BUS_IOC_BASE + 0x0020)
/* 驱动能力设置寄存器基地址 */
#define VCCIO1_4_BASE (0xFD5F9000)
/* GPIO1_A3 的驱动能力设置寄存器地址 */
#define VCCIO1_4_IOC_GPIO1A_DS_L (VCCIO1_4_BASE + 0x0020)
/* GPIO1组的基地址 */
#define GPIO1_BASE (0xFEC20000)
/* 输出输入模式寄存器 */
#define GPIO_SWPORT_DR_L (GPIO1_BASE + 0X0000)
/* 设置输出高低电平寄存器*/
#define GPIO_SWPORT_DDR_L (GPIO1_BASE + 0X0008)
static void __iomem *BUS_IOC_GPIO1A_IOMUX_SEL_L_PI;
static void __iomem *VCCIO1_4_IOC_GPIO1A_DS_L_PI;
static void __iomem *GPIO_SWPORT_DR_L_PI;
static void __iomem *GPIO_SWPORT_DDR_L_PI;
/* 
static
表示这个指针的作用域限制在当前文件中(即文件内全局变量)。
void *
指针类型为 void,可以转换为任意数据类型,表示一个未定具体类型的地址。
__iomem
是一个内核宏,表示指针指向 I/O 内存(而非普通的 RAM)。
目的是让 sparse(Linux 的静态分析工具)在编译阶段检测可能的非法操作。
 */
static void __iomem *GPIO_SWPORT_DDR_L_PI;
void led_switch(u8 sta)
{
            
    u32 val = 0;
    if(sta == LEDON) {
            
        val = readl(GPIO_SWPORT_DR_L_PI);
        val &= ~(0X8 << 0); /* bit3 清零*/
        val |= ((0X8 << 16) | (0X8 << 0)); /* bit19 置 1,允许写 bit3 bit3,高电平*/
        writel(val, GPIO_SWPORT_DR_L_PI);    
    }
    else if(sta == LEDOFF) {
            
        val = readl(GPIO_SWPORT_DR_L_PI);
        val &= ~(0X8 << 0); /* bit3 清零*/
        val |= (0X8 << 16); /* bit19 置 1,允许写 bit3,bit3,低电平*/
        writel(val, GPIO_SWPORT_DR_L_PI);
    }
}
/*
* @description : 物理地址映射  将物理地址映射到虚拟地址上
* @return : 无
*/
void led_remap(void)
{
            
    /* GPIO的复用功能寄存器 */
    BUS_IOC_GPIO1A_IOMUX_SEL_L_PI = ioremap(BUS_IOC_GPIO1A_IOMUX_SEL_L,4);
    /* GPIO输出能力设置寄存器 */
    VCCIO1_4_IOC_GPIO1A_DS_L_PI = ioremap( VCCIO1_4_IOC_GPIO1A_DS_L ,4);
    /* 输出输入模式寄存器 */  
    GPIO_SWPORT_DR_L_PI = ioremap(GPIO_SWPORT_DR_L, 4);
     /* 输出电平寄存器 */  
    GPIO_SWPORT_DDR_L_PI = ioremap(GPIO_SWPORT_DDR_L, 4);
}
void led_unmap(void)
{
            
    /* 取消映射 */
    iounmap(BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);
    iounmap(VCCIO1_4_IOC_GPIO1A_DS_L_PI);
    iounmap(GPIO_SWPORT_DR_L_PI);
    iounmap(GPIO_SWPORT_DDR_L_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!
");
        return -EFAULT;
    }
    ledstat = databuf[0]; /* 获取状态值 */
    if(ledstat == LEDON) {
            
        led_switch(LEDON); /* 打开 LED 灯 */
    }
    else if(ledstat == LEDOFF) {
            
        led_switch(LEDOFF);
    }
    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,
    
};
/* 入口函数实现 */
static int __init led_init(void)
{
            
    int retvalue = 0;
    u32 val = 0;
    /* 初始化LED */
    /* 1、映射寄存器地址 */
    led_remap();
    /* 2、设置GPIO1_A3的功能 */
    val = readl(BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);
    val &= ~(0XF000 << 0); /* bit15:12,清零 */
    val |= ((0XF000 << 16) | (0X0 << 0)); /* bit31:28 置 1,bit15:12:0,用作 GPIO1_A3 */
    writel(val, BUS_IOC_GPIO1A_IOMUX_SEL_L_PI);
    /* 3、设置 GPIO0_C0 驱动能力为 40ohm */
    val = readl(VCCIO1_4_IOC_GPIO1A_DS_L_PI);
    val &= ~(0X7000 << 0); /* bit14:12 清零*/
    val |= ((0X7000 << 16) | (0X6000 << 0));/* bit30:28 置 1,允许写 bit14:12,bit14:12:110, 用作 GPIO1_A3 */
    writel(val, VCCIO1_4_IOC_GPIO1A_DS_L_PI);
    /* 4、设置 GPIO1_A3 为输出 */
    val = readl(GPIO_SWPORT_DDR_L_PI);
    val &= ~(0X8 << 0); /* bit3 清零*/
    val |= ((0X8 << 16) | (0X8 << 0)); /* bit19 置 1,允许写 bit3,bit3,高电平 */
    writel(val, GPIO_SWPORT_DDR_L_PI);
    /* 5、设置 GPIO1_A3 为低电平,关闭 LED 灯。*/
    val = readl(GPIO_SWPORT_DR_L_PI);
    val &= ~(0X8 << 0); /* bit3 清零*/
    val |= (0X8 << 16); /* bit19 置 1,允许写 bit3,bit3,低电平 */
    writel(val, GPIO_SWPORT_DR_L_PI);
    /* 6、注册字符设备驱动 */
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if(retvalue < 0) {
            
        printk("register chrdev failed!
");
        goto fail_map;
    }
    return 0;
fail_map:
    led_unmap();
    return -EIO;
 }
/* 出口函数实现 */
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("LK");
/* 骗过内核该模块属于设备树 */
MODULE_INFO(intree, "Y");
![图片[9] - rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验 - 宋马](https://pic.songma.com/blogimg/20250502/028fc41fb2664efbb8b041870a4438be.png)
 
4.4.2 编写测试 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"
#define LEDOFF 0
#define LEDON 1
int main (int argc,char **argv)
{
            
    int fd,retvalue;
    char *filename;
    unsigned char databuf[1];
    if(argc!=3)
    {
            
        printf("Error Usage!
");
        return -1;
    }
    filename = argv[1];
    fd = open(filename,O_RDWR);
    if(fd<0)
    {
            
        printf("file %s open failed!
", argv[1]);
        return -1;  
    }
    databuf[0] = atoi(argv[2]);
    retvalue = write(fd,databuf,sizeof(databuf));
    if(retvalue < 0)
    {
            
        printf("LED Control Failed!
");
        close(fd);
        return -1;
    }
    retvalue = close(fd); /* 关闭文件 */
    if(retvalue < 0){
            
        printf("file %s close failed!
", argv[1]);
        return -1;
    }
    return 0;
}
KERNELDIR := /home/lk/rk3588_linux_sdk/kernel
CURRENT_PATH := $(shell pwd)
# obj-m 是内核模块编译规则中的一个特殊变量。
# obj-m 定义了要生成的模块目标文件(即 .ko 文件)。
# obj-m 表示编译时将 chrdevbase.o 作为模块(module)对象,最终会生成 chrdevbase.ko。
# chrdevbase.o
# chrdevbase.o 是将 chrdevbase.c 文件编译为目标文件(.o 文件)的名称。
# 生成的目标文件会自动链接成内核模块 chrdevbase.ko。
obj-m := led.o
# make 会首先检查 kernel_modules 目标。
# 如果 kernel_modules 目标没有生成或需要更新,make 会执行 kernel_modules 的命令。
# 执行完 kernel_modules 后,build 目标就算完成了。
build : kernel_modules
# kernel_modules
# 定义一个名为 kernel_modules 的目标。
# 当执行 make kernel_modules 时,会触发后面的命令。
# $(MAKE)
# $(MAKE) 是一个特殊的变量,表示 make 命令本身。
# 使用 $(MAKE) 而不是直接调用 make 可以在嵌套调用时保持参数一致性。
# -C $(KERNELDIR)
# -C 选项表示切换到 $(KERNELDIR) 目录下执行命令。
# $(KERNELDIR) 是一个变量,通常指定为 Linux 内核源码的构建目录。
# 在内核源码目录中调用 make 会使用内核的构建系统。
# M=$(CURRENT_PATH)
# M= 选项告诉内核构建系统,当前模块的源代码位于 $(CURRENT_PATH) 目录下。
# modules
# modules 是内核构建系统的一个目标,表示要构建模块(.ko 文件)。
# 当传入 modules 目标时,内核会根据 obj-m 定义的模块进行编译。
# 总结  使用make buil 就会检查kernel_modules是否存在或者更新 ,kernel_modules会执行$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
#也就是 make 内核路径 当前文件路径 生成modules即obj-m 对应的 chrdevbase.o生成chrdevbase.ko文件
kernel_modules :
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
                    















暂无评论内容