目录
- 前言
- 一、模块化编程
- 1.模块化驱动代码框架
- 2.printk详解
- 3.应用操作
- 二、多模块编程
- 三、多文件编程
- 四、函数传参
前言
没多少东西,就是最基础的一些Linux驱动编写操作。
一、模块化编程
驱动加载到内核中的两种方法:
1.静态编译:就是将模块化的驱动代码就直接集成到了内核镜像里,缺点就是你每次都需要重新编译内核,烧写内核。这种方法的优点是启动时即有驱动支持还稳定,但不便于动态管理或升级驱动程序。
2.动态加载(模块化):将驱动程序通过Makefile编译成内核模块,可以在运行时通过insmod命令加载,使用rmmod命令卸载。这种方法使得驱动程序的管理更加灵活,允许在不重启系统的情况下加载或更新驱动程序。
在Linux系统中,驱动程序的模块化编程是一种设计和实现设备驱动程序的方法,能够提供更大的灵活性和可维护性。模块化编程允许驱动程序以模块的形式被加载或卸载,而无需重启系统。这种方式使得系统资源的管理更加高效,并简化了驱动程序的开发和调试过程。
这里提到的模块化,跟C语言的.c.h模块化不同,这个里的模块化是指可以在内核运行时被动态地加载和卸载。优点:极大的缩小了内核的镜像大小。如同电脑装windows系统时,官方只给一个比较小的纯净版系统,后续装好后,根据自己的需求再去下载安装其他驱动,这个过程就是模块化的应用。
模块化指令:
insmod:用于将模块插入内核。
rmmod:用于从内核中卸载模块。
lsmod:列出当前加载的模块。
modinfo:显示模块的信息。
1.模块化驱动代码框架
#include <linux/kernel.h>
#include <linux/module.h>
static int __init test_init(void)
{
printk("test加载函数运行!\n");
return 0;
}
static void __exit test_exit(void)
{
printk("test卸载函数运行!\n");
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
理解
static int __init test_init(void)
:入口函数。类似于main,当该模块被加载时,入口函数里的代码便会运行。
static void __exit test_exit(void)
:出口函数。当该模块被卸载时,会执行该函数里的代码。
声明驱动模型出/入口函数
module_init(test_init);
module_exit(test_exit);
头文件
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kernel.h>
:包含了内核级的函数和宏,主要用于内核日志记录和调试(printk)。
#include <linux/module.h>
:定义了内核模块的基本功能和宏,用于模块的加载、卸载及描述。
开源许可
MODULE_LICENSE("GPL");
就是一个开源声明。指定了内核模块使用的许可证类型为 GNU 通用公共许可证(GPL)。这是一个自由软件许可证,允许用户自由使用、修改和分发代码,只要遵守相同的许可证条款。
如果你不声明模块的许可证,内核在加载模块时会输出警告,提示模块没有许可证信息。
2.printk详解
printk函数是 Linux 内核中的一个调试工具,用于在内核空间打印日志信息。类似于C语言中的printf函数,但是用于内核空间的打印,因此它有一些特定的用法和限制。
1.printk打印的东西有消息等级,Linux 内核共提供了八种不同的消息级别,分为级别 0~7。只有当消息等级大于控制台等级时(值越低等级越高),printk输出的消息才会被打印到终端上。
2.printk不能使用浮点功能,不能有 %f,%lf,其余功能和printf一样。
使用命令:cat /proc/sys/kernel/printk
查看内核打印等级:
1.控制台日志级别:内核消息的最低级别,决定了哪些日志信息会显示在控制台上。比如,设置为 4 时,KERN_WARNING 及以上级别的信息会显示。
2.默认日志级别:在内核启动时的默认日志级别。如果内核日志级别未被设置,使用此值。
3.内核消息日志级别:日志写入内核日志缓冲区的级别。此级别决定了日志信息在内核日志缓冲区中的可见性。
4.串行端口日志级别:如果系统使用串行端口来记录日志,这个值决定了日志信息的级别。
消息等级宏(在 include/linux/kern_levels.h
文件中):
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
设置printk的消息等级:printk(KERN_ERR "test111\n");
在输出内容前添加消息等级的宏定义。若不设置消息等级,则为默认的消息等级。
修改内核的消息等级:echo 0 4 1 7 >/proc/sys/kernel/printk
,修改控制台等级或默认消息等级。
3.应用操作
当驱动程序的.c编写好后,我们直接用makefile生成.ko文件方便后续加载到内核中。
Makefile文件
obj-m += test.o #最终生成模块的名字就是 test.ko
KDIR:=/home/zht/RK3588S/kernel # 内核源码的路径
CROSS_COMPILE_FLAG=/home/zht/RK3588S/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#交叉编译器路径
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
#调用内核层 Makefile 编译目标为 modules->模块 文件在当前路径
# 架构 ARCH=arm64
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.markers *.order app *.mod
解释:
这段 Makefile 就是指定了内核源代码目录、交叉编译器路径,并通过调用内核 Makefile 编译当前目录下的模块文件,生成一个适用于 ARM 64 位架构的内核模块 (test.ko)。
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
:调用内核的 Makefile 来编译模块。各个参数的含义如下:
-C $(KDIR)
:告诉 make 在 $(KDIR)
目录下执行 Makefile,也就是内核源代码的路径。
M=$(PWD)
:指定当前目录 ($(PWD))
作为模块编译的目录。
modules
:指定要编译的目标是内核模块。
ARCH=arm64
:指定目标架构为 ARM 64 位架构(即 AArch64)。
CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
:使用指定的交叉编译器来编译模块。
生成.ko文件后直接用adb push推到硬件中,然后就能使用命令执行加载、卸载等操作。
二、多模块编程
多模块编程就是同过加载多个模块A、B、C等,通过声明和调用,实现B模块调用A模块的内容。
例:
texta.c
#include <linux/kernel.h>
#include <linux/module.h>
void hello_text0(void)
{
printk("hello_来自texta\n");
}
static int __init texta_init(void)
{
printk("texta加载函数运行!\n");
return 0;
}
static void __exit texta_exit(void)
{
printk("texta卸载函数运行!\n");
}
module_init(texta_init);
module_exit(texta_exit);
EXPORT_SYMBOL(hello_text0);//模块内的函数如果要提供给其他模块使用,必须使用EXPORT_SYMBOL()导出
MODULE_LICENSE("GPL");
textb.c
#include <linux/kernel.h>
#include <linux/module.h>
extern void hello_text0(void);
static int __init textb_init(void)
{
printk("textb加载函数运行!\n");
hello_text0();
return 0;
}
static void __exit textb_exit(void)
{
printk("textb卸载函数运行!\n");
}
module_init(textb_init);
module_exit(textb_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m += texta.o #这要生成两个.ko
obj-m += textb.o
KDIR:=/home/zht/RK3588S/kernel #他就是你现在rk3588s里内核的路径
CROSS_COMPILE_FLAG=/home/zht/RK3588S/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#这是你的交叉编译器路径 --- 这里你也要替换成你自己的交叉编译工具的路径
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
#调用内核层 Makefile 编译目标为 modules->模块 文件在当前路径
# 架构 ARCH=arm64
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.markers *.order app *.mod
解析:
就是多了个EXPORT_SYMBOL
,EXPORT_SYMBOL是一个重要的机制,用于在 Linux 内核模块之间共享代码。使用该宏后,其他模块再使用extern声明外部函数,就能够实现跨模块使用该函数。
这里就是在texta中使用EXPORT_SYMBOL宏传入hello_text0,之后在textb用extern外部声明,最后就实现了外部模块函数的调用。
结果:
注意:由于是b调用a,所以加载时,要先加载a,再加载b。卸载时,要先卸载b,再卸a。
三、多文件编程
多文件编译就是将一个模块的函数,在外边单独写一个.c文件,入口和出口函数还是只有一个,最后编译结果也是生成一个.ko文件。
示例:
texta.c
#include <linux/kernel.h>
#include <linux/module.h>
extern void waibu_hello(void);
static int __init texta_init(void)
{
printk("texta加载函数运行!\n");
waibu_hello();
return 0;
}
static void __exit texta_exit(void)
{
printk("texta卸载函数运行!\n");
}
module_init(texta_init);
module_exit(texta_exit);
MODULE_LICENSE("GPL");
textb.c
#include <linux/kernel.h>
void waibu_hello(void)
{
printk("hello_来自textb\n");
}
Makefile
obj-m := textab.o
textab-objs = texta.o textb.o//因为是多文件编译,所以这里也要修改
KDIR:=/home/zht/RK3588S/kernel #他就是你现在rk3588s里内核的路径
CROSS_COMPILE_FLAG=/home/zht/RK3588S/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#这是你的交叉编译器路径 --- 这里你也要替换成你自己的交叉编译工具的路径
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
#调用内核层 Makefile 编译目标为 modules->模块 文件在当前路径
# 架构 ARCH=arm64
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.markers *.order app *.mod
结果:
四、函数传参
函数传参有点像c语言的主函数传参,大致一样。主要就是用了两个宏module_param
和module_param_array
。
- module_param
语法:module_param(name, type, permissions);
作用: 定义一个模块参数,其中 name 是参数名,type 是参数的数据类型,permissions 是权限设置。
int a = 0;
module_param(a, int, S_IRUGO | S_IWUSR);
a: 参数名,模块的用户可以通过此名称来设置参数值。
int: 数据类型,表示参数是整数类型。
S_IRUGO | S_IWUSR
: 权限设置,S_IRUGO 表示所有用户都可以读取,S_IWUSR 表示只有当前用户可以写入。
- module_param_array
语法:module_param_array(name, type, num, permissions);
作用: 定义一个数组类型的模块参数,其中 name 是参数名,type 是数组元素的数据类型,num 是一个指针,用于存储数组元素的个数,permissions 是权限设置。
int arr[10] = {0};
int num = 0;
module_param_array(arr, int, &num, S_IRUGO | S_IWUSR);
arr: 参数名,表示数组参数。
int: 数组元素的数据类型,这里是整数类型。
&num: 用于存储数组元素的个数的指针。内核会将实际元素数量写入这个变量。
S_IRUGO | S_IWUSR: 权限设置,所有用户可读,当前用户可写。
应用示例:
send_data.c
#include <linux/kernel.h>
#include <linux/module.h>
int num =0;
static int a=1024;
static char *p = "zht";
static int arr[10]={0,1,2,3,4};
static int __init text_init(void)
{
int i;
printk("text加载函数运行!\n");
printk("a=%d\n",a);
printk("p=%s\n",p);
printk("num=%d\n",num);
for(i=0;i<10;i++)
printk("arr[%d]=%d\n",i,arr[i]);
return 0;
}
static void __exit text_exit(void)
{
printk("text卸载函数运行!\n");
}
module_param(a,int,S_IRUGO|S_IWUSR);
module_param(p,charp,S_IRUGO|S_IWUSR);
module_param_array(arr,int,&num,S_IRUGO|S_IWUSR);
module_init(text_init);
module_exit(text_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m += send_data.o
KDIR:=/home/zht/RK3588S/kernel #他就是你现在rk3588s里内核的路径
CROSS_COMPILE_FLAG=/home/zht/RK3588S/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#这是你的交叉编译器路径 --- 这里你也要替换成你自己的交叉编译工具的路径
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE_FLAG)
#调用内核层 Makefile 编译目标为 modules->模块 文件在当前路径
# 架构 ARCH=arm64
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.markers *.order app *.mod
不传参时:
传参时: