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
相关文章:
rk3588 驱动开发(二)第四章嵌入式 Linux LED 驱动开发实验
4.1 Linux 下 LED 灯驱动原理 Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动 最终也是对 RK3588 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux 的驱动框架。开发板上的 LED 连接…...
第49讲:AI驱动的农业碳汇估算与生态价值评估 —— 打造更“绿”的智慧农业未来
目录 🌍 一、农业碳汇:我们为什么要关心它? 🤖 二、AI是如何介入农业碳汇评估的? 🛠 三、案例实战:AI估算区域农田碳汇储量 📍 场景设定: 📊 数据来源: 🔁 处理流程: 📈 四、生态价值评估:从碳储量到生态效益 🧭 五、平台与工具推荐 💬 六、…...
springmvc入门案例
目录 前言 springmvc概述 springmvc入门案例(使用配置类替代原本的web.xml) 第一步、创建一个web工程 第二步、引入相应的依赖(servlet-api、spring-webmvc、) 第三步、编写 SpringMVC配置类,并开启包扫描功能 第四步、编写…...
Node.js学习
概述 Node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境,允许在服务器端运行 JavaScript 代码。它采用事件驱动和非阻塞 I/O 模型,适合构建高性能、可扩展的网络应用,尤其擅长处理实时应用和大规模数据密集型场景 背景 JavaScri…...
SQL注入漏洞中会使用到的函数
目录 一、信息获取函数 1. 通用函数 2. 元数据查询(INFORMATION_SCHEMA) 二、字符串操作函数 1. 字符串连接 2. 字符串截取 3. 编码/解码 三、报错注入专用函数 1. MySQL 2. SQL Server 3. PostgreSQL 四、时间盲注函数 1. 通用延迟 2. 计…...
MIT IDSS深度解析:跨学科融合与系统科学实践
麻省理工学院的IDSS(Institute for Data, Systems, and Society, IDSS)是一个致力于通过先进分析方法推动教育与研究的前沿机构。它将工程学、信息科学和数据科学的方法与社会科学的分析方法相结合,以应对复杂的社会挑战。 MIT IDSS 建立在统计学、计算机科学和特定应用领域…...
重塑智慧出行新生态,德赛西威全新战略愿景发布
4月22日,上海车展开幕前夕,德赛西威以“智新境,向远大”为主题,正式对外发布全新发展战略及使命、愿景;同时,代表未来AI出行趋势的智慧出行解决方案Smart Solution 3.0重磅亮相。 一、把握变革节点 创领产…...
全面解析 classification_report:评估分类模型性能的利器
解读 classification_report 的使用:评估分类模型性能的关键工具 在机器学习中,分类任务是最常见的应用场景之一。无论是垃圾邮件过滤、图像识别还是情感分析,分类模型的性能评估都是至关重要的一步。而 classification_report 是 Scikit-le…...
Qt案例 使用QFtpServerLib开源库实现Qt软件搭建FTP服务器,使用QFTP模块访问FTP服务器
本以为搭建和访问FTP服务器的功能已经是被淘汰的技术了,只会在学习新技术的时候才会了解学习学习,WinFrom版本,和windows Api版本访问FTP服务器的功能示例也都写过。没想到这次会在项目中再次遇到, 这里记录下使用Qt开源库QFtpSer…...
图像后处理记录
图像后处理记录 ocr后处理记录 opencv裁剪 编译命令 cmake -S . -B build-x64 -DBUILD_LIST"core,imgproc,imgcodecs,highgui" -DBUILD_SHARED_LIBSOFF -DBUILD_opencv_appsOFF -DBUILD_opencv_jsOFF -DBUILD_ANDROID_PROJECTSOFF -DBUILD_ANDROID_EXAMPLESOFF -…...
解决element中的el-anchor链接被作为路由跳转导致页面404
解决element中的el-anchor链接被作为路由跳转导致页面404 问题: 在使用elementPlus时,el-anchor-link中的href被识别为路由进行跳转,导致不能正常跳转到锚点,且页面显示404。 解决:自定义方法解决 <!--添加hand…...
Mapreduce中maven打包
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序(例如:jar包),并发运行在…...
C++初阶——string的使用(下)
C初阶——string的使用(下) 一、string类对象的容量操作 对于string的容量操作,我们可以通过顺序表来理解,顺序表是通过动态数组来实现的,在数据结构专栏的第一篇就是顺序表的详细讲解,链接如下ÿ…...
AIGC vs 人类创作者:是竞争还是协作?
AIGC vs 人类创作者:是竞争还是协作? 随着人工智能技术的飞速发展,特别是生成式AI(AIGC, AI-Generated Content)的崛起,越来越多的领域开始出现AI的身影。从文本创作、图像生成到音乐制作,AIGC…...
Stable Baselines3 结合 gym 训练 CartPole 倒立摆
视频讲解: Stable Baselines3 结合 gym 训练 CartPole 倒立摆 今天介绍下stable_baselines3和gym,可以方便实现DL的实现,应用在机械臂catch、reach等场景 测试代码仓库:https://github.com/LitchiCheng/DRL-learning.git https:…...
ctfshow web8
前言 学习内容:简单的盲注脚本的书写 web8 这个题目题目手动注入很麻烦 主要是他过滤了 union 空格和 过滤了union的解决方法 1、使用盲注(报错注入和盲注) 2、使用时间盲注 3、堆叠注入 盲注脚本的书写 首先他是有注入点的 然后熟悉requests包的使用 …...
Linux程序地址空间
目录 研究背景 程序地址空间回顾 来段代码感受一下 进程地址空间 Linux2.6内核进程调度队列 一个CPU拥有一个runqueue 优先级 活跃队列(只出不进) 过期队列(只进不出) active指针和expired指针 总结 研究背景 Linux内核版本&#…...
破茧成蝶:阿里云应用服务器让传统 J2EE 应用无缝升级 AI 原生时代
丝滑升级拥抱大模型:详解AI时代的应用智能化升级路径 破茧成蝶:阿里云应用服务器让传统 J2EE 应用无缝升级AI原生时代 ——十年代码无需重写,三步开启智能化跃迁 作者:孤弋、孚阳 序幕:一场跨越 20 年的技术对话 在杭…...
游戏引擎学习第240天:将渲染器移至第三层
这节又枯燥又无聊简直了 回顾并为今天的内容做铺垫 昨天我们说到,想对渲染器和平台层的集成方式做一些修改。我们之前简单讲了一下修改的目的:我们希望游戏本身不再直接调用 OpenGL 的渲染代码,而是只生成一组渲染指令缓冲区,然…...
2025.04.23华为机考第三题-300分
📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 03. 时空旅行者的最优路径 问题描述 A先生是一名时空旅行者,他可以在不同的时空点之间穿梭。每次从一个时空点跳跃到另一个时空点需要消耗一个时间单位。在每个时空点,都有一些特…...
Kafka 保证多分区的全局顺序性的设计方案和具体实现
Kafka 本身无法直接保证多分区的全局顺序性,因为分区设计旨在并行处理以提升吞吐量。 要实现多分区的顺序性,可尝试通过以下方法在系统层面或业务逻辑上解决: 一、方案设计 单一分区路由(还是将消息发送到同一分区)&a…...
数据结构初阶:二叉树(四)
概述:本篇博客主要介绍链式结构二叉树的实现。 目录 1.实现链式结构二叉树 1.1 二叉树的头文件(tree.h) 1.2 创建二叉树 1.3 前中后序遍历 1.3.1 遍历规则 1.3.1.1 前序遍历代码实现 1.3.1.2 中序遍历代码实现 1.3.1.3 后序遍历代…...
华为开发岗暑期实习笔试(2025年4月16日)
刷题小记: 第一题怀疑测试样例不完整,贪心法不应该能够解决该题。第二题使用0-1BFS解决单源最短路径的问题,往往搭配双端队列实现。第三题是运用动态规划解决最大不重叠子区间个数的问题,难点在于满足3重判断规则,所需…...
第一篇:Django简介
第一篇:Django简介 文章目录 第一篇:Django简介一、纯手写一个简易版的web框架1、软件开发架构2、HTTP协议3、简易的socket服务端4、wsgiref模块5、动静态网页6、后端获取当前时间展示到html页面上7、字典数据传给html文件8、数据从数据库中获取的展示到…...
2025年渗透测试面试题总结-拷打题库13(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 2025年渗透测试面试题总结-拷打题库13 一、GitHub等三方敏感信息泄漏防御 二、业务逻辑漏洞技术规避 …...
(09)Vue脚手架的使用(Vite、vue-cli、create-vue)
本系列教程目录:Vue3Element Plus全套学习笔记-目录大纲 文章目录 第3章 Vue脚手架3.1 vite3.3.1 Vite使用1)创建Vite项目2)Vite项目打包 3.1.2 组件化开发3.1.4 Vite工程运行原理1)分析main.js2)自定义根组件 3.2 vue…...
Unity 将Excel表格中的数据导入到Mysql数据表中
1.Mysql数据表users如下: 2.即将导入的Excel表格如下: 3.代码如下: using System; using System.Data; using System.IO; using Excel; using MySql.Data.MySqlClient; using UnityEngine; using UnityEditor;public class ImportExcel {// …...
【QT】信号与槽中多个按钮(pushbutton)共用一个槽函数的两种实现方式
两种方法的对比 方法1:sender() 优点:代码简洁,无需额外参数 缺点:依赖运行时类型转换,安全性较低 适用场景:简单场景,少量按钮 方法2:Lambda (推荐) 优点:安全直观&…...
Python----深度学习(神经网络的过拟合解决方案)
一、正则化 1.1、正则化 正则化是一种用于控制模型复杂度的技术。它通过在损失函数中添加额外的项(正则 化项)来降低模型的复杂度,以防止过拟合。 在机器学习中,模型的目标是在训练数据上获得较好的拟合效果。然而,过…...
【金仓数据库征文】从 HTAP 到 AI 加速,KingbaseES 的未来之路
国产数据库早已实现 “可替代”,但要真正与国际头部厂商掰手腕,必须在 HTAP(Hybrid‑Transaction/Analytical Processing)与 AI 加速 两条技术赛道上实现跨越。KingbaseES 自 V8R3 调整为多进程架构后,历经 V8R6、KSOn…...
创建第一个Spring Boot项目
什么是Spring Boot 随着Spring的快速发展,项目中的XML文件越来越多,繁琐的配置以及,整合第三方框架的配置问题,导致大大增加了开发和部署的效率,使开发者无法专心于业务的开发。Spring Boot就相当于使Spring框架的脚手…...
Java—— 正则表达式 练习
需求: 请编写正则表达式验证用户输入的手机号码是否满足要求。 请编写正则表达式验证用户输入的邮箱号是否满足要求。 请编写正则表达式验证用户输入的电话号码是否满足要求。 验证手机号码 13112345678 13712345667 13945679027 139456790271 验证座机电话号码 02…...
Linux[指令与权限]
Linux指令与权限 Linux环境中,打包文件有多种 tar (打包/解包) 指令 tar -czvf 文件要打包到的位置 文件(打包并压缩到) tar -xzvf 文件(在当前目录下解压) tar选项 -c创建压缩文件 -z使用gzip属性压缩 -v展现压缩过程 -f后面使用新建文档名 -x不要新建,解压 -C 文件…...
MySQL数据库精研之旅第十期:打造高效联合查询的实战宝典
专栏:MySQL数据库成长记 个人主页:手握风云 目录 一、简介 1.1. 为什么要使用联合查询 1.2. 多表联合查询时的计算 1.3. 示例 二、内连接 2.1. 语法 2.2. 示例 三、外连接 4.1. 语法 4.2. 示例 一、简介 1.1. 为什么要使用联合查询 一次查询需…...
【Redis】集合类型Set 常用命令详解
1. sadd - 添加 语法:sadd key value > sadd testset A 1 > sadd testset B 1 > sadd testset C 1 > sadd testset C # set的值不能重复 0 > smembers set1 # 查询指定set的所有值,乱序 1) "B" 2) "A" 3) "C&qu…...
React 5 种组件提取思路与实践
在开发时,经常遇到一些高度重复但略有差异的 UI 模式,此时我们当然会把组件提取出去,但是组件提取的方式有很多,怎么根据不同场景选取合适的方式呢?尤其时在复杂的业务场景中,组件提取的思路影响着着代码的可维护性、可读性以及扩展性。本文将以一个[详情]组件为例,探讨…...
第十五届蓝桥杯 2024 C/C++组 合法密码
目录 题目: 题目描述: 题目链接: 思路: substr函数: 思路详解: 代码: 代码详解; 题目: 题目描述: 题目链接: P10906 [蓝桥杯 2024 国 B] 合法密码 -…...
云原生时代的双轮驱动
在当今数字化浪潮汹涌澎湃的时代,企业 IT 主管、CIO、CTO 们肩负着引领企业乘风破浪、实现数字化转型的重任。而主数据平台与数据中台,宛如企业数字化征程中的双引擎,为企业发展注入强劲动力。 一、主数据与数据中台:企业数据世界…...
GD32F407单片机开发入门(六)定时器TIMER详解及实战含源码
文章目录 一.概要二.通用定时器内部结构1.时基单元2.时钟源3.输入捕获4.输出比较 三.通用定时器内部特色四.TIME定时器1ms中断例程五.工程源代码下载六.小结 一.概要 定时器就是计数器,应用在我们生活的方方面面,比如有闹钟、计时器等。在GD32F407VET6定…...
时序数据库 TDengine 助力石油石化业务, 平滑接替 Oracle 数据库
小T导读:胜软科技在石油石化行业中选择使用 TDengine 处理时序数据,不仅显著降低了运维数据库的成本,也大幅减少了存储空间的占用,实现了从原有的 40 多套 Oracle 数据库向仅 9 套 TDengine集群的精简替换。在迁移过程中ÿ…...
【问题解决】本机navicat连接云服务器mysql
一般情况下,当你使用navicat等工具连接云服务器会因为mysql的安全机制,导致无法连接root用户,但是在测试环境中,不考虑安全性的前提条件下,可以通过修改MySQL的配置文件来连接云服务器mysql的root用户。 选择数据库&am…...
STM32F407 的通用定时器与串口配置深度解析
在 STM32F407 芯片的开发过程中,通用定时器和串口的配置与使用是极为关键的技能点。本文将结合提供的代码示例,深入剖析这两个模块的配置流程、工作原理以及实际应用,助力开发者更好地掌握相关技术。 一、通用定时器 (一&#x…...
深入探究Linux项目自动化构建工具:make与Makefile
目录 引言 一、make与Makefile概述 1.1 背景 1.2 理解 二、make工作原理 2.1 查找Makefile 2.2 确定目标文件 2.3 处理文件依赖 三、Makefile实例分析 3.1 简单C程序示例 3.2 项目清理机制 四、结合行缓冲区概念的有趣现象 五、结语 引言 在Linux软件开发的世界里…...
【Hive入门】Hive基础操作与SQL语法:DDL操作全面指南
目录 1 Hive DDL操作概述 2 数据库操作全流程 2.1 创建数据库 2.2 查看数据库 2.3 使用数据库 2.4 修改数据库 2.5 删除数据库 3 表操作全流程 3.1 创建表 3.2 查看表信息 3.3 修改表 3.4 删除表 4 分区与分桶操作 4.1 分区操作流程 4.2 分桶操作 5 最佳实践与…...
STM32F103 “BluePill” 上的 DMA 原理与实践
摘要:本文深入浅出地介绍什么是 DMA(直接存储器访问),它的核心原理、硬件架构,以及在 STM32F103(BluePill)上常见的几种使用场景(ADC、UART、内存拷贝等)。通过对比 CPU 轮询、中断、DMA 三种方式的数据搬运效率,结合寄存器级和 HAL 库示例代码,并附带性能测试与优化…...
软考软件设计师30天备考指南
文章目录 一、考情分析(一)综合知识(二)案例分析 二、30天学习规划(一)第1 - 5天:基础夯实(二)第6 - 10天:核心知识突破(三)第11 - 15…...
比较:AWS VPC peering与 AWS Transit Gateway
简述: VPC 对等连接和 Transit Gateway 用于连接多个 VPC。VPC 对等连接提供全网状架构,而 Transit Gateway 提供中心辐射型架构。Transit Gateway 提供大规模 VPC 连接,并简化了 VPC 间通信管理,相比 VPC 对等连接,支持大量 VPC 的 VPC 间通信管理。 VPC 对等连接 AWS V…...
【AI大模型】MCP:AI应用的“超级扩展坞”
一、什么是MCP MCP(Model Context Protocol,模型上下文协议)是一种新兴的开放协议,于2024年11月由Anthropic公司(Claude的开发者)开源。它的核心目标是建立一个类似USB-C的标准化协议,统一AI模…...
线程封装
目录 makefile Thread.hpp main.cc 以面向对象的方式造轮子 #ifndef _THREAD_HPP__ // 如果没有定义过 _THREAD_HPP__ #define _THREAD_HPP__ // 则定义 _THREAD_HPP__// 这里是头文件的实际内容(类、函数声明等)#endif // 结束条件…...
【Java后端】MyBatis 与 MyBatis-Plus 如何防止 SQL 注入?从原理到实战
在日常开发中,SQL 注入是一种常见但危害巨大的安全漏洞。如果你正在使用 MyBatis 或 MyBatis-Plus 进行数据库操作,这篇文章将带你系统了解:这两个框架是如何防止 SQL 注入的,我们又该如何写出安全的代码。 什么是 SQL 注入&#…...