《【北京迅为】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 驱动的框架主要包括:
- 模块加载函数:当驱动被加载时,内核会执行该函数,完成初始化工作。
- 模块卸载函数:当驱动被卸载时,内核会执行该函数。
- 模块许可证声明:内核的许可证包括 “GPL” 和 “GPL v2”,如果不声明模块许可,加载驱动时会报“kernel tained” 的警告。
- 模块参数(可选):加载模块时可以传参进驱动
- 模块符号导出(可选):内核模块可以导出符号,其他模块可以调用这些导出的符号。
- 模块作者信息(可选)
编译内核模块
配置交叉编译器
驱动编写完成,还需要使用编译器将其编译成内核模块,在 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, \
¶m_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 顺序,卸载驱动也有先后顺序,只是刚好和安装驱动时的顺序相反。