itop-3568开发板驱动学习笔记(1)驱动基础

《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记

内核源码结构

下图为 rk3568 所用 linux 内核的目录:

在这里插入图片描述

内核目录简介:

目录名 介绍
arch 存放体系结构相关代码,每个架构的 CPU,该目录下都会有一个对应的子目录,比如 arch/arm、arch\x86
block 块设备的通用函数
crypto 存放加密、压缩、CRC 校验等算法的代码
Documentation 存放内核说明文档
drivers 存放内核驱动源码,每一个子目录对应一类驱动,如 drivers/char、drivers/usb 等
firmware 存放处理器相关固件
fs 存放 Linux 支持的文件系统的代码,每个子目录对应一种文件系统,如 fs/ext2、fs/nfs 等
include 存放内核头文件
init 内核初始化代码,但不是系统的引导代码,而是内核引导后要运行的代码
ipc 存放进程间通信代码
kernel 存放内核核心代码
lib 存放库文件代码
mm 存放内存管理代码
net 存放网络相关代码
samples 存放一些内核编程范例
scripts 存放用于配置、编译内核的脚本文件
security 存放系统安全性相关代码
sound 存放音频设备的驱动
tools 存放一些常用工具,如性能剖析、功能测试等
usr 用于生成 initramfs 的代码
virt 提供虚拟技术的支持

编译内核源码

进入 rk3568 linux SDK 目录,使用 “./build.sh kernel” 可以进行内核编译:

在这里插入图片描述
build.sh kernel 实际运行的内容如下:

在这里插入图片描述

helloworld 驱动实验

下面是一个简单的 linux 驱动,虽然它看起来非常简单,但却包含了驱动的所有要素。

在这里插入图片描述
linux 驱动的框架主要包括:

  1. 模块加载函数:当驱动被加载时,内核会执行该函数,完成初始化工作。
  2. 模块卸载函数:当驱动被卸载时,内核会执行该函数。
  3. 模块许可证声明:内核的许可证包括 “GPL” 和 “GPL v2”,如果不声明模块许可,加载驱动时会报“kernel tained” 的警告。
  4. 模块参数(可选):加载模块时可以传参进驱动
  5. 模块符号导出(可选):内核模块可以导出符号,其他模块可以调用这些导出的符号。
  6. 模块作者信息(可选)

编译内核模块

配置交叉编译器

驱动编写完成,还需要使用编译器将其编译成内核模块,在 X86 Ubuntu 平台,我们可以直接使用 gcc 来编译代码,但如果要在 X86 平台的电脑上编译 ARM 开发板的 Linux 驱动,就必须用到交叉编译器。

rk3568 SDK 所使用的交叉编译器位于 SDK/prebuilts/gcc/linux-x86/aarch64/ 目录下:

在这里插入图片描述
为了方便后面的调用,我们需要将交叉编译器的 bin 目录加到全局环境变量中(不然每次使用都得加上交叉编译器的绝对路径),打开 ~/.bashrc,在最后一行添加一行代码:

在这里插入图片描述

保存 ~/.bashrc 后,需要重启电脑或者使用 source ~/.bashrc 来使其生效(source 只能在当前终端生效),然后,我们就能直接输入 aarch64-linux-gnu-xx 来运行交叉编译工具了:

在这里插入图片描述

编写 Makefile

Makefile 文件用来编译 Linux 驱动,内容如下:

在这里插入图片描述

编译驱动模块

编写完 Makefile 文件后,在终端输入 make 或者 make all,就能完成内核驱动的编译。后缀为 .ko 的文件即为我们所需要的驱动模块。

在这里插入图片描述

加载和卸载驱动模块

上面的 Makefile 中使用了 arm 平台的编译器来编译 helloworld.c,所以生成的驱动文件也只能放到 arm 开发板上运行,将驱动拷贝到 rk3568 开发板。

我们使用 insmod helloworld.ko 来加载内核模块:

在这里插入图片描述
如果要卸载 helloworld.ko,可以使用 rmmod helloworld.ko

在这里插入图片描述

我们可以使用 lsmod 命令来查看系统已经载入的内核模块。

在这里插入图片描述

驱动传参

在安装驱动时,可以通过传参的方式改变驱动的功能(类似于带参数的 main() 函数),这样可以使驱动模块更加灵活。

内核驱动可支持的传参类型包括 C 语言常用的数据类型,module_param 可以传基本类型(整型、字符型、布尔型等等),module_param_array 可以传数组,module_param_string 可以传字符串。它们的定义如下:

#define module_param(name, type, perm) \
        module_param_named(name, name, type, perm)
#define module_param_array(name, type, nump, perm) \
        module_param_array_named(name, name, type, nump, perm)
#define module_param_string(name, string, len, perm) \
		static const struct kparam_string __param_string_##name \
 				= {
      
       len, string }; \
  		__module_param_call(MODULE_PARAM_PREFIX, name, \
						&param_ops_string, \
						.str = &__param_string_##name, perm, -1, 0);\
	 	__MODULE_PARM_TYPE(name, "string")

module_param(name, type, perm)

参数介绍:

  • name 驱动程序中的变量名称,也是用户传入参数的名称。
  • type 模块参数数据类型(byte, short, ushort, int, uint, long, ulong, charp, bool, invbool)。
  • perm 模块参数的访问权限,和 linux 文件访问权限相同。
#define S_IRUSR 00400 /*文件所有者可读*/
#define S_IWUSR 00200 /*文件所有者可写*/
#define S_IXUSR 00100 /*文件所有者可执行*/
#define S_IRGRP 00040 /*与文件所有者同组的用户可读*/
#define S_IWGRP 00020 /*与文件所有者同组的用户可写*/
#define S_IXGRP 00010 /*与文件所有者同组的用户可执行*/
#define S_IROTH 00004 /*与文件所有者不同组的用户可读*/
#define S_IWOTH 00002 /*与文件所有者不同组的用户可写*/
#define S_IXOTH 00001 /*与文件所有者不同组的用户可可执行*/

#define S_IRWXUGO	(S_IRWXU|S_IRWXG|S_IRWXO)	/* 所有用户可读、写、执行 */
#define S_IALLUGO	(S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)/* 所有用户可读、写、执行*/
#define S_IRUGO		(S_IRUSR|S_IRGRP|S_IROTH)	/* 所有用户可读 */
#define S_IWUGO		(S_IWUSR|S_IWGRP|S_IWOTH)	/* 所有用户可写 */
#define S_IXUGO		(S_IXUSR|S_IXGRP|S_IXOTH)	/* 所有用户可执行 */

如果直接填 0644,表示文件所有者可读写,同组用户和其他用户可读。

module_param_array(name, type, nump, perm)

module_param() 相比,module_param_array() 多了一个 nump 参数,

  • nump 指向一个整数,表示有多少个参数存在 name 数组中。

module_param_string(name, string, len, perm)

module_param_string 专门用来传字符串,所以不需要 type 参数,

  • string 参数为驱动内部的字符串名
  • len 为字符串长度。

该宏函数的 name 参数和之前两个函数的 name 参数有所区别,这里的 name 只作为用户传入参数的名称。

实验程序

该驱动的功能:在驱动加载时可以传入 number(整型)、name(字符指针)、array(数组)和 string(字符串)这 4 个参数,驱动加载后,内核会打印这些传入的参数。

#include <linux/module.h>
#include <linux/kernel.h>

static int number;
static char *name;
static int array[5];
static char str[10];
static int len;

module_param(number, int, 0644);
module_param(name, charp, 0644);
module_param_array(array, int, &len, 0644);
module_param_string(string, str, sizeof(str), 0644);

static int __init module_param_init(void)   //驱动入口函数
{
    
    
	int i = 0;
	printk("number: %d\n", number);
	printk("name: %s\n", name);
	for(i = 0; i < len; i++)
	{
    
    
		printk("array[%d]: %d\n", i, array[i]);
	}
	printk("str: %s\n", str);
	return 0;
}

static void __exit module_param_exit(void)  //驱动出口函数
{
    
    
	printk("module_param exit exit\n");
}

module_init(module_param_init);  //注册入口函数
module_exit(module_param_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");        //同意GPL协议
MODULE_AUTHOR("xiaohui");        //作者信息

下面是该驱动的 Makefile 文件,这次我没有将其编译为 arm 开发板上的驱动,而是直接编译成了 x86 平台的驱动模块。

在这里插入图片描述
驱动编译完成后,使用下面的命令进行驱动安装:

sudo insmod module_param.ko number=100 name="xiaohui" array=1,2,3,4,5 string="xiaohui"

查看内核打印,可以看到我们传入的四种参数都被成功打印:

在这里插入图片描述

符号导出

一些复杂的驱动模块需要分层进行设计,这时需要用到内核模块的符号导出功能。这里的符号指的是全局变量和函数,在函数被加载时,这些符号被记录在公共内核符号表内。

符号导出所使用的宏为 EXPORT_SYMBOL(sym)EXPORT_SYMBOL_GPL(sym)。他们使用方法相同,只是后者只能被 GPL 许可的模块使用。sym 为要导出的符号(函数或变量名)。

实验程序

本实验总共要写两个驱动程序,一个用来导出模块符号,另一个用来调用被导出的符号。第一个驱动文件是 export_symbol.c,该驱动定义了一个整型变量 tmp 和一个函数 func,并将他们导出。

export_symbol.c:

#include <linux/module.h>
#include <linux/kernel.h>

int tmp = 10;
EXPORT_SYMBOL(tmp); //导出参数tmp

int func(int x, int y)
{
    
    
	return x + y;
}
EXPORT_SYMBOL(func); //导出函数func()

static int __init export_symbol_init(void)   //驱动入口函数
{
    
    
	printk("export_symbol init\n");
	return 0;
}

static void __exit export_symbol_exit(void)  //驱动出口函数
{
    
    
	printk("export_symbol exit\n");
}

module_init(export_symbol_init);  //注册入口函数
module_exit(export_symbol_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");        //同意GPL协议
MODULE_AUTHOR("xiaohui");        //作者信息


第二个驱动文件 test.c 里直接引用上一个驱动里的两个“符号“,并在驱动被加载时将导入的”符号“打印出来。

test.c:

#include <linux/module.h>
#include <linux/kernel.h>

extern int tmp;  //外部导入的变量
extern int func(int x, int y); //外部导入的函数

static int __init test_init(void)   //驱动入口函数
{
    
    
	int ret = 0;
	printk("tmp = %d\n", tmp);  //打印tmp
	ret = func(1, 2);           //运行func函数 
	printk("1 + 2 = %d\n", ret);//打印func运行结果
	return 0;
}

static void __exit test_exit(void)  //驱动出口函数
{
    
    
	printk("test exit\n");
}

module_init(test_init);   //注册入口函数
module_exit(test_exit);   //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息

下面是该实验的 Makefile 文件,同样选择在 X86 平台测试。

在这里插入图片描述

驱动编译完成后,开始加载驱动。如果直接加载 test.ko,会提示“Unknown symbol in module”同时内核层会打印错误提示,并且更加详细。

在这里插入图片描述

要想加载 test.ko ,必须先安装 export_symbol.ko,两个驱动依次加载后,test.ko 成功打印 export_symbol.ko 中定义的变量和函数(函数运行结果)
在这里插入图片描述

除了加载驱动时需要按照 export_symbol.ko --> test.ko 顺序,卸载驱动也有先后顺序,只是刚好和安装驱动时的顺序相反。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43772810/article/details/128868127