rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验

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 函数声明

c 复制代码
431 void __iomem *ioremap(resource_size_t res_cookie, size_t size);

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

示例代码 4.1-2 ioremap 函数实现

c 复制代码
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 寄存器对应的虚拟地址,

使用如下代码即可:

c 复制代码
#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 函数原型

c 复制代码
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 读操作函数

c 复制代码
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 写操作函数

c 复制代码
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 硬件原理图分析

图中R113是限流电阻,避免NPN三极管基极电流过大烧毁三极管,取值计算方法如下:

三极管进入饱和态:基极电流

假设 LED 电流

Ic ≈ 5 ~ 10mA,三极管 β ≈ 100。 则基极电流

𝐼𝑏≈0.1𝑚𝐴

假设 GPIO 输出高电平 3.3V,V_BE ≈ 0.7V:

实际使用中,通常会放大电流裕量,10kΩ 是比较常见的保守值,确保三极管能充分导通。

R115 的作用(下拉电阻):

作用:R115 是下拉电阻,用于在 GPIO 输出为高阻态(Hi-Z)或系统上电初始化期间,防止三极管误导通。

保证 Q4 基极电位默认为低电平,避免 LED 误亮。提高系统上电稳定性。

数值选择:

数值需远大于 R113,不能分流太多基极电流,一般选用几十 kΩ 到几百 kΩ。

51kΩ 是一个常见的折中选择。

三极管(以NPN型为例)具有以下三个工作状态:

  1. 截止区(Cutoff Region)

    条件:基极-发射极电压

    𝑉𝐵𝐸<0.7𝑉(未导通)

    特征:基极电流

    𝐼𝐵≈0,集电极电流 𝐼𝐶≈0

    作用:三极管完全关闭,相当于开关断开

  2. 放大区(Active Region)

    条件:

    𝑉𝐵𝐸≈0.7𝑉,且 𝑉𝐶𝐸>𝑉𝐵𝐸

特征:三极管工作在线性放大状态,

𝐼𝐶≈𝛽⋅𝐼𝐵

作用:主要用于模拟信号放大,不是开关工作区

  1. 饱和区(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 这个寄存器,寄存器描述如下图所示:

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,寄存器结构如下图所示:

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 如下表所

示:

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

GPIO_SWPORT_DDR_L 寄存器地址也是 base+offset,其中 GPIO0~GPIO4 的基地址如下

表所示:

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

同样的,GPIO1_A3 对应 bit3,如果要输出低电平,那么 bit3 置 0,如果要输出高电平,

bit3 置 1。bit19 也要置 1,允许写 bit3。

4.4 实验程序编写

4.4.1 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/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!\r\n");
        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!\r\n");
        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");


4.4.2 编写测试 APP

编写测试 APP,led 驱动加载成功以后手动创建/dev/led 节点,应用程序(APP)通过操作

/dev/led 文件来完成对 LED 设备的控制。向/dev/led 文件写 0 表示关闭 LED 灯,写 1 表示打开

LED 灯。新建 ledApp.c 文件,在里面输入如下内容

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!\r\n");
        return -1;
    }
    filename = argv[1];

    fd = open(filename,O_RDWR);
    if(fd<0)
    {
        printf("file %s open failed!\r\n", argv[1]);
        return -1;  
    }
    databuf[0] = atoi(argv[2]);
    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;
}
powershell 复制代码
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
相关推荐
中国lanwp12 分钟前
Pingora vs. Nginx vs. 其他主流代理服务器性能对比
运维·nginx
YUELEI11815 分钟前
Centos9安装docker
运维·docker·容器
andrew_121927 分钟前
docker底层原理简述
linux·docker·容器
Lilith的AI学习日记29 分钟前
n8n 中文系列教程_05.如何在本机部署/安装 n8n(详细图文教程)
运维·windows·macos·ai编程·n8n
庸子32 分钟前
Docker镜像与容器概念解析
运维·docker·容器
liuyunluoxiao1 小时前
文件【Linux操作系统】
linux
Zz_waiting.1 小时前
网络原理 - 6
运维·服务器·网络·tcp
长流小哥2 小时前
Linux网络编程 从集线器到交换机的网络通信全流程——基于Packet Tracer的深度实验
linux·c语言·网络
CheungChunChiu2 小时前
Qt 容器类使用指南
linux·开发语言·c++·qt·容器
riveting2 小时前
明远智睿2351开发板:性价比之选,赋能智能硬件创新
大数据·linux·图像处理·人工智能·智能硬件