说明:本期博客踩了很多坑,全文字字珠玑,建议开发时全文背诵
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-8export 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结尾的文件,打开解包选项:
如果你的固件没问题的话,可以点击三个点,选中整合好的镜像,点击解包即可:
解包后会在软件目录下看到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
文件,你可以单独放在一个文件夹内。
然后可以去github下载Linux_Pack_Firmware工具,利用打包工具把替换内核后的的.img
文件打包为一整个img文件,注意在这个过程中内核的大小发生了改变,需要修改对印的parameter.txt,在Linux_Pack_Firmware的根目录下,简单进行文本文档修改即可完成可以使用的img文件打包,这时再利用瑞芯微开发工具就可以烧录镜像了。当然此镜像也可以烧录U盘或SD卡,仅需要确保其他存储介质为空即可。
最终就会打包成一个完整的镜像文件,到此环境搭建就算完成了。
最可能出问题的地方 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
编译 我所使用的源码包含了build.sh,可以直接选择需要编译的对象和文件系统,非常方便,简单按几下就可以无脑编译了
我直接选择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> 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可以保证优先级最高,但是会影响其他模块的输出。
优先级数值 宏定义 0 KERN_EMERG
1 KERN_ALERT
2 KERN_CRIT
3 KERN_ERR
4 KERN_WARNING
5 KERN_NOTICE
6 KERN_INFO
7 KERN_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 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/kernelall: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modulesclean: $(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,对应在串口终端也会输出。
如果都看不到的话,可以去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> MODULE_LICENSE("GPL" ); MODULE_AUTHOR("Minloha" ); MODULE_DESCRIPTION("Module param" ); MODULE_VERSION("1.0" ); static char *str = "default" ; module_param(str, charp, S_IRUGO); static int array [10 ] = {0 };static int num = 0 ; module_param_array(array , int , &num, S_IRUGO); 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/kernelall: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modulesclean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
最终编译运行得到输出:
1 sudo insmod param.ko str="test" array=2,3,4,5,6
当然此消息可以在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> #include <asm/fpsimd.h> MODULE_LICENSE("GPL" ); MODULE_AUTHOR("Minloha" ); MODULE_DESCRIPTION("Signal Module" ); MODULE_VERSION("1.0" );static void enable_fpu (void ) { preempt_disable(); kernel_neon_begin(); }static void disable_fpu (void ) { kernel_neon_end(); 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; 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(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> 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 ); 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; 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); } printk(KERN_INFO "----- Standard C Operations (%d iterations) -----\n" , iterations); 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); 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); 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); 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); 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); printk(KERN_INFO "----- Kernel Operations (%d iterations) -----\n" , iterations); 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); 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); 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); 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); 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+simdall: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modulesclean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
最终我们进行make后就可以得到calc.ko和signal.ko。
放在rk3588平台上,我们需要先把符号来源加载进系统内,我们的符号均生命在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的要求,但是唯一的缺点就是功耗太高了,也就是发热严重,当然也与它八个核心有关,总体很不错的。