RK3588系列内核开发——你好世界

说明:本期博客踩了很多坑,全文字字珠玑,建议开发时全文背诵

RK3588启动说明

在第一部分需要介绍一下RK3588的结构,这款芯片并不存在片内的flash空间。说到这时可能就会好奇rk3588的系统启动流程了。其实答案很简单,在rk3588的硅晶圆设计时就预留一个一定区域利用掩膜技术固化了一段代码,它是硬件程序并不可升级,是刻录在硅片上的,因此此固件是实打实的固件(无法被任何软件修改),他的名字官方叫做BootROM。因此rk3588无惧任何存储损坏或软件错误,在任何外部存储故障或软件错误的情况下都能确保最基本的引导能力。

在上电复位阶段,BootROM会首先完成芯片内部的基础硬件初始化,包括时钟树配置、电源域管理和存储控制器复位等操作。这一过程完全由硬件逻辑电路实现,不涉及任何软件执行。初始化完成后,BootROM会根据BOOTMODE[1:0]引脚的电气电平状态选择启动介质。这两个引脚的电平组合构成一个2位编码,每个编码值对应特定的存储接口:

  • 00b:SPI NOR Flash接口
  • 01b:eMMC Boot分区
  • 10b:SD/MMC卡接口
  • 11b:保留配置(通常用于特殊启动模式)

选定的存储接口控制器会被自动初始化,随后BootROM会从该介质的固定物理地址读取前16KB数据。这个读取操作遵循严格的时序要求,在SPI Flash上使用1-bit模式以保证最大兼容性,而在eMMC设备上则访问预定义的Boot分区。读取到的数据需要包含有效的Rockchip头部标识(如”RK33”的UTF-8或者”RK3588”等)和正确的CRC校验值,否则BootROM会尝试切换至下一个备选启动介质。

通过验证的引导程序(通常是SPL或MiniLoaderAll.bin)会被加载到芯片内部有限的SRAM空间中。此时BootROM会将执行权限完全移交给这个二级加载程序,自身进入休眠状态。需要特别指出的是,BootROM在此过程中并不区分加载的程序类型,它仅作为简单的代码加载器工作,所有的功能逻辑都由新加载的程序实现。

二级加载程序的首要任务是初始化外部DRAM内存系统。这一过程包含三个关键步骤:首先是DDR控制器的时钟配置和PHY层训练,这需要根据具体的PCB布局调整时序参数;其次是内存颗粒的容量和通道识别,需要正确配置LPDDR4/5的Bank地址映射;最后是可选的ECC功能初始化。完成DRAM初始化后,系统获得足够的内存空间来加载更复杂的引导程序。当必要的初始化完成后就会利用分区表中uboot的所处位置进行快速跳跃,此时进入主BootLoader。

当进入U-Boot后就会初始化更多的硬件(所有的硬件层外设,比如GPIO、PCIe、USB以及以太网等),这个时候U-Boot可以加载系统内核和设备树到内存中,并给内核传递响应的启动参数,这时的U-Boot完全位于外部的Flash中。

最后进入内核阶段,U-Boot通过bootm命令启动Linux内核,此时的内核需要先在内存中解压,与此同时设备树数据会被传递到特定内存地址供内核解析,而cmdline参数则决定了系统的初始配置(可以在系统启动后利用cat /proc/cmdline查看启动参数,包括cmdline或者串口终端等信息)。内核启动后会依次初始化进程调度、内存管理和设备驱动,最终挂载根文件系统并启动用户空间的init进程。

环境搭建

一般的厂家都会自己修改一部分源码用于适配其他外设,比如屏幕、相机等,因此需要根据你的开发板进行内核源码拷贝,注意我们在内核开发的环境需要在自己的电脑而非嵌入式平台,原因很简单,算力与架构。在Linux内核编译过程中,工具目录scripts会选用gcc进行编译(最好也是gcc编译),同时交叉编译的过程会使用到很多并不支持arm架构的编译器,因此需要在amd或者叫x86_64平台下完成,这里我尝试过物理机ubuntu22、虚拟机ubuntu20以及wsl2的ubuntu20,最终总结出不同平台下的问题。

首先给出瑞芯微官方linux内核的地址:

https://github.com/rockchip-linux/kernel.git

当然此内核并不一定适合你的系统(大概率缺失设备驱动,有可能出现外设不好使或者设备树不对的情况,需要自行修改,过于麻烦),你可以编译当作linux-headers使用,但如果编译镜像的话还是选用官方的资料。

物理机

当你在物理机部署源码环境时,并不需要考虑太多,只需要注意aarch64-none-linux-gnu的链接工具aarch64-none-linux-gnu-ld是否存在即可,我在使用apt源下载时并不包含ld工具,建议通过tar包进行安装。

虚拟机

当你在虚拟机部署源码环境时需要注意如下内容:

  • 硬盘:一定要200G,不然随随便便就超过160G了
  • 字符:需要加两个指令,不然会在编译时弹出警告,打断编译。
1
2
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8

WSL

当你在子系统编译时只需要注意内存就好,和物理机一样,编译的大小还是很大的。


当你顺利的将代码放入环境后你就可以开始接下来的开发了(注意开发并不在rk3588平台下,我们仅把编译好的内容拷贝过去运行,切记不可打包deb包,因为scripts是基于gcc编译的,并不支持arm架构)

下面是我的项目结构:

1
2
3
minloha@minloha:/mnt/g/rk3588-linux$ ls
Makefile app buildroot device external output rkbin rockdev tools ubuntu
README.md build.sh debian docs kernel prebuilts rkflash.sh script_run_flag u-boot

注意我使用的代码为topeet厂家提供的源码,内部包含了包括uboot、rootfs以及kernel并进行了非常好的整合,当然如果没有这么详细的话仅内核目录也是可以的,自然还有其他方法进行取巧。

取巧编译法

打开瑞芯微的烧录工具,你的开发板的厂家一定会提供一个镜像,比如叫update.img或者其他任意以.img结尾的文件,打开解包选项:

1

如果你的固件没问题的话,可以点击三个点,选中整合好的镜像,点击解包即可:

2

解包后会在软件目录下看到Output文件夹,在Android/Image下就会出现包括boot、misc、rootfs等分区镜像,而在Android下则会有MiniLoaderAll.bin、parameter.txt这两个至关重要的文件。MiniLoaderAll.bin就是我们的SPL程序,无需管他,parameter指定了内存的分区,关键区域的格式为:

1
2
3
4
5
6
7
CMDLINE: mtdparts=:
0x00002000@0x00004000(uboot),
0x00002000@0x00006000(misc),
0x00020000@0x00008000(boot),
0x00040000@0x00028000(recovery),
0x00010000@0x00068000(backup),
-@0x00078000(rootfs:grow)

@符号前为分区的大小,@符号后为区域的头地址。这个我们在后面会用到,但不是这个文件。我们可以把解包出来的镜像文件全部拷贝出来,利用官方提供的源码编译出对应的镜像文件进行替换即可,此时我们仅需要所有的镜像即.img文件,你可以单独放在一个文件夹内。

3

然后可以去github下载Linux_Pack_Firmware工具,利用打包工具把替换内核后的的.img文件打包为一整个img文件,注意在这个过程中内核的大小发生了改变,需要修改对印的parameter.txt,在Linux_Pack_Firmware的根目录下,简单进行文本文档修改即可完成可以使用的img文件打包,这时再利用瑞芯微开发工具就可以烧录镜像了。当然此镜像也可以烧录U盘或SD卡,仅需要确保其他存储介质为空即可。

4

最终就会打包成一个完整的镜像文件,到此环境搭建就算完成了。

最可能出问题的地方

  • 1、首先是内核的menuconfig,这个必须要选择板子对应的config文件,如果没有则选用rk3588_linux.config或者rockchip_linux_defconfig,指令如下:
1
2
3
4
5
6
// 默认的config文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
// 手动指定rk3588配置文件(需要确保此config存在在根目录)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rk3588_linux.config
// 官方源码的配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip_linux_defconfig
  • 2、编译指令务必指定架构和编译链,不然你在自己的机器编译完成后无法做成可以使用的头文件,或者编译成功后架构不对,最好参考下面的指令格式:

    1
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- 编译选项(modules、defconfig)
  • 3、无法找到ld文件,或者出现如下报错:

    1
    2
    3
    /bin/sh: 1: gcc: not found
    make: aarch64-linux-gnu-gcc: Command not found
    Error: unrecognized emulation mode: aarch64linux

    只需要安装gcc和gcc-arm即可,Rockchip 官方推荐使用gcc-arm可以去developer.arm.com下载

编译

我所使用的源码包含了build.sh,可以直接选择需要编译的对象和文件系统,非常方便,简单按几下就可以无脑编译了

5

我直接选择all,如果出现不能编译的我直接使用官方镜像解包的内容替换即可(除了kernel),最终我得到了自己的内核的镜像。

模块开发

当上面的内容完全没问题后,就可以进入下一阶段的开发即字符设备,首先我们需要知道内核的地位要高于标准库,因此很多标准库的功能我们都无法使用,同时我们只能使用MakeFile进行交叉编译,因此我们还需要顺便学一下Makefile。

你好世界

首先实现第一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <linux/init.h>     // 包含模块初始化和清理函数的宏
#include <linux/module.h> // 包含内核模块相关的函数和宏
#include <linux/kernel.h> // 包含printk等内核打印函数

MODULE_LICENSE("GPL"); // 模块许可证
MODULE_AUTHOR("Minloha"); // 模块作者
MODULE_DESCRIPTION("A simple module"); // 模块描述
MODULE_VERSION("1.0"); // 模块版本

static int __init hello_init(void)
{
printk(KERN_EMERG "[Hello]: Hello, World!\n");
return 0;
}

static void __exit hello_exit(void)
{
printk(KERN_EMERG "[Hello]: Goodbye, World!\n");
}

module_init(hello_init); // 指定模块加载时调用的函数
module_exit(hello_exit); // 指定模块卸载时调用的函数

注意写的过程使用的并非是printf而是printk,他的第一个参数为输出等级,它包含了很多种情况,你可以通过下面的指令查看输出等级:

1
2
topeet@topeet:~/ker_test$ cat /proc/sys/kernel/printk
4 4 1 7

四个数字分别为控制台日志级别、默认消息日志级别、最低允许控制台日志级别、默认控制台日志级别。具体级别可以对照下面的内容,我使用KERN_EMERG可以保证优先级最高,但是会影响其他模块的输出。

优先级数值宏定义
0KERN_EMERG
1KERN_ALERT
2KERN_CRIT
3KERN_ERR
4KERN_WARNING
5KERN_NOTICE
6KERN_INFO
7KERN_DEBUG

代码中一定要有的内容为:

1
2
3
MODULE_LICENSE("协议"); 
module_init(初始化函数);
module_exit(退出函数);

因为Linux内核为开源代码,因此你写的内容必须要遵守对应的协议,协议包括:

  • 标准协议
协议字符串描述
"GPL"标准GNU通用公共许可证(GPL v2或更高),最常用
"GPL v2"明确指定GPL版本2(Linux内核自身采用此协议)
"GPL and additional rights"GPL加额外权限(需具体说明)
"Dual BSD/GPL"模块可选择遵循BSD或GPL协议(常见于兼容性驱动)
"Dual MIT/GPL"模块可选择遵循MIT或GPL协议
"Dual MPL/GPL"模块可选择遵循Mozilla公共许可证或GPL协议

  • 宽松协议
协议字符串描述
"BSD"经典BSD许可证(无广告条款)
"MIT"MIT许可证(极简宽松协议)
"Apache"Apache许可证 2.0版本
"MPL"Mozilla公共许可证(MPL 2.0)
  • 私有协议
协议字符串描述
"Proprietary"专有协议(不推荐,可能导致模块无法加载或内核警告⚠️)
"(Proprietary)"等效于"Proprietary"

在初始化和退出函数出写入自己的函数名即可。然后我们实现一个Makefile用于交叉编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 文件名.o
obj-m := hello.o

# 工具链和架构配置
ARCH := arm64
CROSS_COMPILE := /mnt/g/rk3588-linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-

# 内核源码路径(绝对路径)
KERNEL_DIR := /mnt/g/rk3588-linux/kernel

all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

首先obj-m为你要编译的对象,比如你的c文件名为hello.c,那么o文件就是hello.o,如果是main.c那o文件就是main.o,很好懂。

架构选择arm64,工具链自行填写,可以直接写aarch64-none-linux-gnu-(需要确保有aarch64-none-linux-gnu-ld)

内核源码用于提供linux头文件。

最后就是all和clean,注意下面的两个指令用tab进行缩进而不是空格,不然make会报错,最终我们得到一整个makefile,文件结构如下:

1
2
3
4
minloha@minloha:~/kernel/helloworld$ tree
.
├── Makefile
└── hello.c

我们每次都可以先clean后编译,以避免添加文件不生效的情况。使用make clean && make -j13就可以,自己修改核心数

1
2
3
4
5
6
7
8
9
10
11
minloha@minloha:~/kernel/helloworld$ make clean && make -j13
make -C /mnt/g/rk3588-linux/kernel M=/home/minloha/kernel/helloworld clean
make[1]: Entering directory '/mnt/g/rk3588-linux/kernel'
make[1]: Leaving directory '/mnt/g/rk3588-linux/kernel'
make -C /mnt/g/rk3588-linux/kernel M=/home/minloha/kernel/helloworld ARCH=arm64 CROSS_COMPILE=/mnt/g/rk3588-linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu- modules
make[1]: Entering directory '/mnt/g/rk3588-linux/kernel'
CC [M] /home/minloha/kernel/helloworld/hello.o
MODPOST /home/minloha/kernel/helloworld/Module.symvers
CC [M] /home/minloha/kernel/helloworld/hello.mod.o
LD [M] /home/minloha/kernel/helloworld/hello.ko
make[1]: Leaving directory '/mnt/g/rk3588-linux/kernel'

最终我们就会得到对应的.ko文件,我们把ko文件复制到rk3588上即可进行运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
topeet@topeet:~/ker_test$ ls
hello.ko
topeet@topeet:~/ker_test$ sudo insmod hello.ko

Message from syslogd@topeet at Jul 16 21:49:48 ...
kernel:[36377.139169] [Hello]: Hello, World!
topeet@topeet:~/ker_test$ lsmod
Module Size Used by
hello 16384 0
8723du 1568768 0
rtk_btusb 65536 0
topeet@topeet:~/ker_test$ sudo rmmod hello

Message from syslogd@topeet at Jul 16 21:49:59 ...
kernel:[36387.789052] [Hello]: Goodbye, World!
topeet@topeet:~/ker_test$

我们可以看到对应的输出都有了,因为我是用KERN_EMERG,对应在串口终端也会输出。

6

如果都看不到的话,可以去dmesg查看,这是查看最近5条的指令:dmesg | tail -n 5

1
2
3
4
5
6
topeet@topeet:~/ker_test$ dmesg | tail -n 5
[35706.819295] [Hello]: Goodbye, World!
[36377.139169] [Hello]: Hello, World!
[36387.789052] [Hello]: Goodbye, World!
[36432.550470] [Hello]: Hello, World!
[36434.242593] [Hello]: Goodbye, World!

模块参数

这一小节我们介绍模块带参数运行的写法,因为我们的内核初始化部分均为函数指针,无法进行额外参数传递,因此内核提供了宏用于声明多种类型参数的输入,下面为一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <linux/init.h>     // 包含模块初始化和清理函数的宏
#include <linux/module.h> // 包含内核模块相关的函数和宏
#include <linux/kernel.h> // 包含printk等内核打印函数

MODULE_LICENSE("GPL"); // 模块许可证
MODULE_AUTHOR("Minloha"); // 模块作者
MODULE_DESCRIPTION("Module param"); // 模块描述
MODULE_VERSION("1.0"); // 模块版本

// 用于参数的字符串
static char *str = "default";
// 参数名(变量名), 参数类型, 访问权限
module_param(str, charp, S_IRUGO); // char point类型, 参数名str, 变量名str

// 数组本体
static int array[10] = {0};
static int num = 0;
// 数组名(变量名), 类型, 长度, 访问权限
module_param_array(array, int, &num, S_IRUGO); // 定义整, 参数名array, 变量名param_array


static int __init param_init(void) {
printk(KERN_ALERT "Array Name: %s, Array length: %d\n", str, num);
for (int i = 0; i < num; i++) {
printk(KERN_ALERT "\tarray[%d] = %d\n", i, array[i]);
}
return 0;
}

static void __exit param_exit(void) {
printk(KERN_ALERT "[Param]: Goodbye!\n");
}

module_init(param_init); // 指定模块加载时调用的函数
module_exit(param_exit); // 指定模块卸载时调用的函数

我们可以用三种宏用于声明参数包括:

1
2
3
module_param(name,type,perm);
module_param_array(name,type,len,perm);
module_param_string(name,type,len,perm);

其中:

  • name: 模块的参数名,也是代码的变量名
  • type:模块参数的数据类型
  • len:模块参数长度的指针(整形)
  • perm:模块的权限

其中type支持(注意没有浮点数):

1
2
3
4
5
6
7
8
9
bool:布尔型
inbool:布尔反值
charp:字符指针(相当于char*,不超过1024字节的字符串)
short:短整型
ushort:无符号短整型
int:整型
uint:无符号整型
long:长整型
ulong:无符号长整型。

perm支持:

1
2
3
4
5
6
7
8
9
#defineS_IRUSR00400		/*文件所有者可读*/
#defineS_IWUSR00200 /*文件所有者可写*/
#defineS_IXUSR00100 /*文件所有者可执行*/
#defineS_IRGRP00040 /*与文件所有者同组的用户可读*/
#defineS_IWGRP00020 /*与文件所有者同组的用户可写*/
#defineS_IXGRP00010 /*与文件所有者同组的用户可执行*/
#defineS_IROTH00004 /*与文件所有者不同组的用户可读*/
#defineS_IWOTH00002 /*与文件所有者不同组的用户可写*/
#defineS_IXOTH00001 /*与文件所有者不同组的用户可可执行*

为了避免我代码中较新的语法结构对编译造成影响,我采用指定C语言版本进行忽略一些奇怪的报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
obj-m := param.o

# 工具链和架构配置
ARCH := arm64
CROSS_COMPILE := /mnt/g/rk3588-linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
ccflags-y := -std=gnu11

# 内核源码路径(绝对路径)
KERNEL_DIR := /mnt/g/rk3588-linux/kernel

all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

最终编译运行得到输出:

1
sudo insmod param.ko str="test" array=2,3,4,5,6

7

当然此消息可以在dmesg查看,但是我可以连接USB串口终端,自然可以免去一条指令的时间。

符号导出

这一部分作为第一节最难的部分,我们是在RK3588上进行模块开发,自然要利用其中的一些设备进行调试,从简单开始我们可以利用FPU进行一些快速浮点运算等功能,而为了低耦合我们可以把必要的函数封装为一个单独的模块,那么其他的模块需要调用这些封装好的功能。因此我们需要导出模块的符号。符号的定义可以是变量或函数名(函数指针)。

那么我们可以写一个signal.c用于声明FPU的相关操作,FPU的调用需要用到一些汇编指令,汇编写法如下:

1
2
3
4
ldr s1, [x1]
ldr s2, [x2]
fadd s0, s1, s2 ; // 这部分是关键的指令, s0 = s1 + s2
str s0, [x0]

可以用到的指令很多,如下所示:

1
2
3
4
5
fadd d0, d0, d1    // d0 = d0 + d1 (双精度加法)
fsub d0, d0, d1 // d0 = d0 - d1
fmul d0, d0, d1 // d0 = d0 * d1
fdiv d0, d0, d1 // d0 = d0 / d1
fsqrt d0, d1 // d0 = sqrt(d1)

我们就利用C语言的asm的魔术字进行编写,而我们需要进行时间对比,因此所需内联方法以避免多余的地址跳跃造成时间过长。同时FPU在默认状态时不会开启,因此我们需要先启用FPU,启用方法务必放在init内,因为切换需要的时间大约需要50ns,因此需要考虑这一小部分时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <linux/init.h>
#include <linux/module.h>
#include <asm/neon.h> // NEON SIMD 支持
#include <asm/fpsimd.h> // ARMv8 FPU/SIMD 支持


MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");
MODULE_DESCRIPTION("Signal Module");
MODULE_VERSION("1.0");

// 启用FPU
static void enable_fpu(void)
{
preempt_disable(); // 禁用抢占(必须)
kernel_neon_begin(); // 启用 NEON/FPU
}

// 关闭FPU
static void disable_fpu(void)
{
kernel_neon_end(); // 退出 NEON/FPU
preempt_enable(); // 重新启用抢占
}


static inline float arm_fadd(float a, float b) {
float result;
asm volatile ("fadd %s0, %s1, %s2" : "=w"(result) : "w"(a), "w"(b));
return result;
}

static inline float arm_fsub(float a, float b) {
float result;
asm volatile ("fsub %s0, %s1, %s2" : "=w"(result) : "w"(a), "w"(b));
return result;
}

static inline float arm_fmul(float a, float b) {
float result;
asm volatile ("fmul %s0, %s1, %s2" : "=w"(result) : "w"(a), "w"(b));
return result;
}

static inline float arm_fdiv(float a, float b) {
float result;
if (unlikely(b == 0.0f)) {
pr_err("Division by zero!\n");
return 0.0f;
}
asm volatile ("fdiv %s0, %s1, %s2" : "=w"(result) : "w"(a), "w"(b));
return result;
}


float kernel_add(float a, float b)
{
float result;
result = arm_fadd(a, b);
return result;
}

float kernel_sub(float a, float b)
{
float result;
result = arm_fsub(a, b);
return result;
}

float kernel_mul(float a, float b)
{
float result;
result = arm_fmul(a, b);
return result;
}

float kernel_div(float a, float b) {
float result; // 声明放在最前面

if (b == 0.0f) {
printk(KERN_ERR "Division by zero!\n");
return 0.0f;
}

result = arm_fdiv(a, b); // 赋值在声明之后
return result;
}

// 平方根实现
float kernel_sqrt(float x) {
float result;
// 这里用到了unlikely宏去进行近似判断,可以避免多次判断
if (unlikely(x < 0.0f)) {
printk(KERN_ERR "FPU Error: Square root of negative number\n");
return 0.0f;
}

asm volatile(
"fsqrt %s0, %s1"
: "=w"(result)
: "w"(x)
);

return result;
}

// 这里就是符号导出宏, 使用EXPORT_SYMBOL(你的函数名或变量名)即可导出作为内核函数使用
EXPORT_SYMBOL(kernel_add);
EXPORT_SYMBOL(kernel_sub);
EXPORT_SYMBOL(kernel_mul);
EXPORT_SYMBOL(kernel_div);
EXPORT_SYMBOL(kernel_sqrt);

static int __init signal_init(void) {
enable_fpu();
printk(KERN_ALERT "[Signal]: Signal Run!\n");
return 0;
}

static void __exit signal_exit(void) {
disable_fpu();
printk(KERN_ALERT "[Signal]: Bye Bye!\n");
}

module_init(signal_init);
module_exit(signal_exit);

导出符号后,这个模块中的函数就可以作为全局变量所使用,因此我们再新建一个模块就可以通过extern复用到此函数。我们再新建一个calc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#include <linux/init.h>
#include <linux/module.h>
#include <linux/ktime.h>

// 使用signal.ko的功能
extern float kernel_add(float a, float b);
extern float kernel_sub(float a, float b);
extern float kernel_mul(float a, float b);
extern float kernel_div(float a, float b);
extern float kernel_sqrt(float a);

static int a = 6;
static int b = 3;

// 两个浮点参数
module_param(a, int, S_IRUGO);
module_param(b, int, S_IRUGO);

float f_a = 0.0f;
float f_b = 0.0f;

#define safe_abs(x) ((x) < 0 ? -(x) : (x))

static void print_float(float value) {
int int_part = (int)value;
int frac_part = (int)(safe_abs(value - int_part) * 1000000); // 保留6位小数
printk(KERN_CONT "%d.%05d", int_part, frac_part);
}

#define SQRT_ITERATIONS 3
// 牛顿法迭代
static float fast_sqrt(float x) {
float guess = x;
if (x <= 0) return 0;
for (int i = 0; i < SQRT_ITERATIONS; i++) {
guess = 0.5f * (guess + x / guess); // 牛顿迭代公式
}
return guess;
}

static void measure_op(const char *op_name, float result, s64 time_ns) {
printk(KERN_INFO "[Calc]: %-8s took %5lld ns, result=", op_name, time_ns);
print_float(result);
printk(KERN_CONT "\n");
}

static int __init calc_init(void) {
float f_a, f_b, std_add, std_sub, std_mul, std_div, std_sqrt;
float kernel_sum, kernel_diff, kernel_prod, kernel_quot, kernel_sqrt_val;
ktime_t start, end;
s64 time_ns;
int i;
const int warmup = 100;
const int iterations = 10000;

f_a = (float)a;
f_b = (float)b;

/* Warmup phase */
for (i = 0; i < warmup; i++) {
std_add = f_a + f_b;
std_sub = f_a - f_b;
std_mul = f_a * f_b;
std_div = f_a / f_b;
std_sqrt = fast_sqrt(f_a);
}

/* Standard C Operations */
printk(KERN_INFO "----- Standard C Operations (%d iterations) -----\n", iterations);

/* Addition */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
std_add = f_a + f_b;
asm volatile("" : "+m" (std_add) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("std_add", std_add, time_ns);

/* Subtraction */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
std_sub = f_a - f_b;
asm volatile("" : "+m" (std_sub) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("std_sub", std_sub, time_ns);

/* Multiplication */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
std_mul = f_a * f_b;
asm volatile("" : "+m" (std_mul) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("std_mul", std_mul, time_ns);

/* Division */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
std_div = f_a / f_b;
asm volatile("" : "+m" (std_div) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("std_div", std_div, time_ns);

/* Square Root */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
std_sqrt = fast_sqrt(f_a);
asm volatile("" : "+m" (std_sqrt) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("std_sqrt", std_sqrt, time_ns);

/* Kernel Operations */
printk(KERN_INFO "----- Kernel Operations (%d iterations) -----\n", iterations);

/* Addition */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
kernel_sum = kernel_add(f_a, f_b);
asm volatile("" : "+m" (kernel_sum) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("k_add", kernel_sum, time_ns);

/* Subtraction */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
kernel_diff = kernel_sub(f_a, f_b);
asm volatile("" : "+m" (kernel_diff) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("k_sub", kernel_diff, time_ns);

/* Multiplication */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
kernel_prod = kernel_mul(f_a, f_b);
asm volatile("" : "+m" (kernel_prod) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("k_mul", kernel_prod, time_ns);

/* Division */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
kernel_quot = kernel_div(f_a, f_b);
asm volatile("" : "+m" (kernel_quot) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("k_div", kernel_quot, time_ns);

/* Square Root */
start = ktime_get_ns();
for (i = 0; i < iterations; i++) {
kernel_sqrt_val = kernel_sqrt(f_a);
asm volatile("" : "+m" (kernel_sqrt_val) : : "memory");
}
end = ktime_get_ns();
time_ns = (end - start) / iterations;
measure_op("k_sqrt", kernel_sqrt_val, time_ns);

printk(KERN_ALERT "[Calc]: Benchmark completed\n");
return 0;
}

static void __exit calc_exit(void)
{
printk(KERN_ALERT "[Calc]: Bye Bye!\n");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");
MODULE_DESCRIPTION("Calculation Module");
MODULE_VERSION("1.0");

module_init(calc_init);
module_exit(calc_exit);

统计时间可以用内核自带的ktime进行,ktime可以实现内核级时间统计,精度非常高,写法很简单。这里需要注意,linux内核内并没有float和double的处理方法(因为压根没想做上层开发)因此我们这里需要取巧,把浮点转化为int进行输出处理。

最后我们给出Makefile,我们需要同时编译出两个.o文件,因此我们不能再对obj-m进行:=,而是+=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
obj-m := calc.o
# 注意是 +=
obj-m += signal.o

# 工具链和架构配置
ARCH := arm64
CROSS_COMPILE := /mnt/g/rk3588-linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
ccflags-y := -std=gnu11

# 内核源码路径(绝对路径)
KERNEL_DIR := /mnt/g/rk3588-linux/kernel

KBUILD_CFLAGS := $(filter-out -mgeneral-regs-only,$(KBUILD_CFLAGS))
KBUILD_CFLAGS += -march=armv8-a+fp+simd


all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

最终我们进行make后就可以得到calc.ko和signal.ko。

放在rk3588平台上,我们需要先把符号来源加载进系统内,我们的符号均生命在signal.ko中,所以我们要先:

1
sudo insmod signal.ko

然后再加载calc.ko:

1
sudo insmod calc.ko a=6 b=3

这时候我们使用指令lsmod可以发现,模块存在了依赖关系:

1
2
3
Module      Size  Used by
calc 16384 0
signal 16384 1 calc

我们再查看dmesg,查看我们的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[  508.880187] ----- Standard C Operations (10000 iterations) -----
[ 508.880236] [Calc]: std_add took 1 ns, result=9.00000
[ 508.880257] [Calc]: std_sub took 1 ns, result=3.00000
[ 508.880273] [Calc]: std_mul took 1 ns, result=18.00000
[ 508.880289] [Calc]: std_div took 1 ns, result=2.00000
[ 508.880306] [Calc]: std_sqrt took 1 ns, result=2.454256
[ 508.880310] ----- Kernel Operations (10000 iterations) -----
[ 508.880359] [Calc]: k_add took 4 ns, result=9.00000
[ 508.880409] [Calc]: k_sub took 4 ns, result=3.00000
[ 508.880460] [Calc]: k_mul took 4 ns, result=18.00000
[ 508.880568] [Calc]: k_div took 10 ns, result=2.00000
[ 508.880670] [Calc]: k_sqrt took 9 ns, result=2.449489
[ 508.880674] [Calc]: Benchmark completed

分析可以发现,直接运行的速度更快,但是利用FPU后对一些计算的精度更高。

总结

本期介绍了Linux内核模块开发的方法,并介绍了rk3588的启动流程,瑞芯微这款芯片做的非常好,可以说满足了很多边缘Linux的要求,但是唯一的缺点就是功耗太高了,也就是发热严重,当然也与它八个核心有关,总体很不错的。


RK3588系列内核开发——你好世界
https://blog.minloha.cn/posts/0007431008aea72025070742.html
作者
Minloha
发布于
2025年7月7日
更新于
2025年7月17日
许可协议