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 函数声明
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 硬件原理图分析
图中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型为例)具有以下三个工作状态:
-
截止区(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 这个寄存器,寄存器描述如下图所示:
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 灯驱动程序编写
#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 文件,在里面输入如下内容
#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;
}
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