RK3568驱动指南|驱动基础进阶篇-进阶1 编译进内核的驱动系统是如何运行的?

这篇具有很好参考价值的文章主要介绍了RK3568驱动指南|驱动基础进阶篇-进阶1 编译进内核的驱动系统是如何运行的?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】824412014(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(驱动基础进阶篇_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


驱动基础-进阶篇

进阶1 编译进内核的驱动系统是如何运行的?

在经过前面章节的学习后,相信大家已经对驱动有了一些自己的认识和理解,从本章开始将对一些驱动相关的进阶知识进行讲解。本章要研究的内容为编译进内核的驱动系统是如何运行的?

在驱动程序中,module_init 宏定义了驱动的入口函数,在模块加载时被内核自动调用,该宏定义在内核源码目录下的“include/linux/module.h”文件中,具体内容如下所示:

#ifndef MODULE
#define module_init(x)  __initcall(x);
#define module_exit(x)  __exitcall(x);
#else /* MODULE */
 ..........
#define module_init(initfn)                 \
    static inline initcall_t __maybe_unused __inittest(void)        \
    { return initfn; }                  \
    int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
..........
#endif

module_init的具体内容由MODULE宏定义来决定,该宏定义在内核源码的顶层Makefile中,具体为KBUILD_CFLAGS_KERNEL和KBUILD_CFLAGS_MODULE两个宏,如下所示:

图 1-1

由于本章节探究的是编译进内核的驱动,所以要看KBUILD_CFLAGS_KERNEL宏定义,该宏为空,那module_init 的宏定义具体内容如下所示:

注意:因为静态编译的驱动无法卸载,所以module_exit在编译进内核的驱动中并不会被执行!所以这里只是分析module_init。

然后继续向下查找__initcall的定义路径,该宏定义在内核源码目录下的“include/linux/init.h”文件中,具体内容如下所示:

#define __initcall(fn) device_initcall(fn)

接下来会发现该宏定义仍会套很多层宏定义,这些宏都在内核源码目录下的“include/linux/init.h”文件中,具体后续嵌套内容如下所示:

1 

#define device_initcall(fn) __define_initcall(fn, 6)

1 

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

1

2

3 

#define ___define_initcall(fn, id, __sec) \

static initcall_t __initcall_##fn##id __used \

__attribute__((__section__(#__sec ".init"))) = fn;

由于嵌套关系较为复杂,这里以module_init(helloworld)为例绘制了调用关系,具体内容如下所示:

 RK3568驱动指南|驱动基础进阶篇-进阶1 编译进内核的驱动系统是如何运行的?,RK3568驱动开发指南,RK3568驱动开发指南 | 驱动基础-进阶篇,linux,驱动开发

图137- 2

注意:##代表强制连接,#表示对这个变量替换后,用双引号引起来。

而宏定义展开到最后的initcall_t是一个函数指针,它的原型如下所示:

typedef int (*initcall_t)(void);

所以,当使用module_init(helloworld)宏定义模块的入口函数后,会创建一个 __initcall_hello_world6函数指针变量,并将其初始化为hello_world函数,这个__initcall_hello_world6函数指针变量的目的是将模块的入口函数放置在内核的初始化调用链中,以便在系统引导期间自动执行。

在编译过程中,这个函数指针会被放置在.initcall6.init段中。这个段是内核初始化调用链的一部分,用于在系统引导期间按顺序调用所有位于该段中的函数。通过将模块的入口函数放置在.initcall6.init段中,可以确保在系统引导期间自动调用该函数,从而初始化模块并注册模块的功能。

而在内核源码中除了module_init,还有其他的宏定义接口用来完成初始化模块并注册模块的功能,他们的原型都是define_initcall,只是相应的优先级不同,而优先级的不同就导致了系统启动时驱动模块的加载先后顺序不一样,module_init的优先级是6,其他的宏定义在include/linux/init.h 文件中,具体内容如下所示:

#define pure_initcall(fn)		__define_initcall(fn, 0)

#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)

而在include/asm-generic/vmlinux.lds.h 链接脚本(linker script)中定义初始化调用函数的布局和顺序,具体内容如下所示:

#define INIT_CALLS_LEVEL(level)						\
		__initcall##level##_start = .;				\
		KEEP(*(.initcall##level##.init))			\
		__initcall##level##s_start = .;				\
		KEEP(*(.initcall##level##s.init))			\

#define INIT_CALLS							\
		__initcall_start = .;					\
		KEEP(*(.initcallearly.init))				\
		INIT_CALLS_LEVEL(0)					\
		INIT_CALLS_LEVEL(1)					\
		INIT_CALLS_LEVEL(2)					\
		INIT_CALLS_LEVEL(3)					\
		INIT_CALLS_LEVEL(4)					\
		INIT_CALLS_LEVEL(5)					\
		INIT_CALLS_LEVEL(rootfs)				\
		INIT_CALLS_LEVEL(6)					\
		INIT_CALLS_LEVEL(7)					\
		__initcall_end = .;

INIT_CALLS_LEVEL(level) 宏用于定义特定优先级(level)的初始化调用函数的布局。它会创建以下两个符号:

__initcall[level]_start:表示该优先级初始化调用函数段的起始位置。

__initcall[level]s_start:表示该优先级初始化调用函数段的起始位置(用于静态初始化)。

接着,INIT_CALLS宏用于定义整个初始化调用函数的布局。它按照一定的顺序将不同优先级的初始化调用函数放置在链接器脚本的相应位置。具体的步骤如下:

1.定义 __initcall_start符号,表示初始化调用函数段的起始位置。

2.使用KEEP命令保留所有.initcallearly.init段中的内容。这个段包含了一些早期的初始化调用函数,它们会在其他优先级之前被调用。

3.依次调用 INIT_CALLS_LEVEL 宏,传入不同的优先级参数,将相应优先级的初始化调用函数放置在链接器脚本中的正确位置。

4.定义 __initcall_end 符号,表示初始化调用函数段的结束位置。 

链接器在链接过程中会根据这些符号的位置信息,将初始化调用函数按照优先级顺序放置在对应的段中。这样,当系统启动时,初始化调用函数将按照定义的顺序被调用,实现系统的初始化和功能注册。

展开之后的INIT_CALLS宏内容如下所示:

__initcall_start = .;
KEEP(*(.initcallearly.init))
__initcall0_start = .;
KEEP(*(.initcall0.init))
__initcall0s_start = .;
KEEP(*(.initcall0s.init))
.....................
__initcall7_start = .;
KEEP(*(.initcall7.init))
__initcall7s_start = .;
KEEP(*(.initcall7s.init))
__initcall_end = .;

_initcall0_start等以_start结尾的相关变量记录了.initcall0.init等段的首地址,这些变量在 init/main.c中通过extern关键字进行引用,并将这些首地址放置在数组initcall_levels中,具体内容如下所示:

extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];

static initcall_entry_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

在1-10行声明了一系列__initcall0_start相关变量,在第12行定义了一个名为initcall_levels 的静态指针数组。该数组用于存储不同优先级的初始化调用函数段的起始地址。数组的元素对应不同的优先级,按照顺序存储了对应优先级的起始地址。该数组最终会在do_one_initcall函数中执行,由于调用关系较为复杂,所以这里直接绘制出了相应的调用关系图,具体内容如下所示:

RK3568驱动指南|驱动基础进阶篇-进阶1 编译进内核的驱动系统是如何运行的?,RK3568驱动开发指南,RK3568驱动开发指南 | 驱动基础-进阶篇,linux,驱动开发

图1- 3

首先来看do_initcalls函数,该函数定义在内核源码的init/main.c目录下,具体内容如下所示:

static void __init do_initcalls(void)
{
	int level;

#ifdef CONFIG_INITCALL_ASYNC
	initcall_init_workers();
#endif

	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);

#ifdef CONFIG_INITCALL_ASYNC
	initcall_free_works();
#endif
}

在第9行,循环遍历了initcall_levels数组,其中ARRAY_SIZE(initcall_levels)表示 initcall_levels数组的大小,do_initcalls函数的循环将执行7次do_initcall_level。在每次循环中,do_initcall_level函数被调用,并传递当前迭代的level值作为参数,数字越小,优先级越高,带s段的优先级要小于不带 "s" 段的优先级,然后我们继续来看do_initcall_level函数,该函数的具体内容如下所示:

static void __init do_initcall_level(int level)
{
	initcall_entry_t *fn;

	// 备份命令行参数并解析参数
	strcpy(initcall_command_line, saved_command_line);
	parse_args(initcall_level_names[level],
		   initcall_command_line, __start___param,
		   __stop___param - __start___param,
		   level, level,
		   NULL, &repair_env_string);

	// 跟踪当前初始化级别
	trace_initcall_level(initcall_level_names[level]);

#ifdef CONFIG_INITCALL_ASYNC
	// 如果启用了异步初始化调用并且有工作线程
	if (initcall_nr_workers)
		if (do_initcall_level_threaded(level) == 0)
			return;
#endif

	// 遍历当前级别的初始化调用函数数组,并执行每个初始化调用函数
	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(initcall_from_entry(fn));
}

在该函数中最重要的内容为24、25行的for循环,关于for循环内容的具体解释如下所示:

(1)fn 是一个指向initcall_entry_t类型的指针,用于迭代遍历当前级别的初始化调用函数数组。

(2)initcall_levels[level] 表示当前级别的初始化调用函数数组的起始地址。

(3)initcall_levels[level+1] 表示下一个级别的初始化调用函数数组的起始地址。由于数组是连续存储的,因此通过比较fn和initcall_levels[level+1]的值,可以确定循环的终止条件。

(4)do_one_initcall是一个函数,用于执行单个初始化调用函数。它接受一个函数指针作为参数,并调用该函数。

(5)initcall_from_entry是一个宏,用于从函数指针fn中获取实际的初始化调用函数。

因此,循环的作用是遍历当前级别的初始化调用函数数组,并依次将每个函数指针传递给 do_one_initcall 函数执行初始化调用。通过这个循环,可以按照预定义的顺序执行每个初始化调用函数,完成系统的初始化过程。do_one_initcall函数的具体内容如下所示:

int __init_or_module do_one_initcall(initcall_t fn)
{
	// 保存当前的抢占计数
	int count = preempt_count();
	// 用于存储警告消息的缓冲区
	char msgbuf[64];
	// 初始化返回值
	int ret;

	// 检查初始化调用函数是否在黑名单中
	if (initcall_blacklisted(fn))
		return -EPERM;

	// 追踪初始化调用函数的开始
	do_trace_initcall_start(fn);
	// 调用初始化调用函数并获取返回值
	ret = fn();
	// 追踪初始化调用函数的结束,并传递返回值
	do_trace_initcall_finish(fn, ret);

	// 初始化消息缓冲区
	msgbuf[0] = 0;

	// 检查抢占计数是否发生变化
	if (preempt_count() != count) {
		// 如果抢占计数发生变化,将警告信息写入消息缓冲区
		sprintf(msgbuf, "preemption imbalance ");
		// 恢复抢占计数到原始值
		preempt_count_set(count);
	}
	// 检查中断是否被禁用
	if (irqs_disabled()) {
		// 如果中断被禁用,将警告信息写入消息缓冲区
		strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
		// 启用中断
		local_irq_enable();
	}

	// 如果消息缓冲区中有警告信息,则输出警告信息
	WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);

	// 增加潜在熵
	add_latent_entropy();
	// 返回初始化调用函数的返回值
	return ret;
}

该函数的作用是执行单个初始化调用函数并处理相关逻辑,至此一系列的调用关系就解释完成了。

最后对本章节内容进行一下简单的总结,在使用module_init(hello_world)时,hello_world()函数指针会被放置在.initcall6.init段处。内核启动时,会执行do_initcall()函数,该函数根据指针数组initcall_levels[6]找到_initcall6_start,在include/asm-generic/vmlinux.lds.h文件中可以查到_initcall6_start对应.initcall6.init段的起始地址。然后,依次取出该段中的函数指针,并执行这些函数。

至此,关于编译进内核的驱动系统是如何运行的这一问题就讲解完成了,最后布置一个课程作业,利用本章节学习到的知识来让驱动可以更快的被加载,会在下一章中对该作业进行讲解。文章来源地址https://www.toymoban.com/news/detail-790128.html

到了这里,关于RK3568驱动指南|驱动基础进阶篇-进阶1 编译进内核的驱动系统是如何运行的?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第22章 自旋锁死锁实验(iTOP-RK3568开发板驱动开发指南 )

    瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网

    2024年02月09日
    浏览(35)
  • 第19章 并发与竞争实验(iTOP-RK3568开发板驱动开发指南 )

    瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网

    2024年02月09日
    浏览(34)
  • 安卓RK3399编译驱动MPU6050,实现内核层与HAL层驱动

    今天我们一起学习一下如何实现对一款有驱动代码的传感器适配安卓系统 开发板:某AR眼镜公司的开发板RK3399 1. 什么是设备树(.dts) DTS即Device Tree Source 设备树源码, Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。 其主要目的是定义MCU各个引脚的接线功能,通过

    2024年02月04日
    浏览(32)
  • RK3568平台开发系列讲解(驱动基础篇)自动创建设备节点

    🚀返回专栏总目录 沉淀、分享、成长,让自己和他人都能有所收获!😄 📢自动创建设备节点分为两个步骤: 步骤一:使用 class_create 函数创建一个类。 步骤二:使用 device_create 函数在我们创建的类下面创建一个设备。 Linux 驱动实验中,当我们通过 insmod 命令加载模块后,

    2023年04月12日
    浏览(44)
  • RK3568平台开发系列讲解(驱动基础篇)V4L2 用户空间 API 说明

    🚀返回专栏总目录 沉淀、分享、成长,让自己和他人都能有所收获!😄 📢设备驱动的主要目的是控制和利用底层硬件,同时向用户展示功能。 这些用户可以是在用户空间或其他内核驱动中运行的应用。 本篇我们将学习如何利用内核公开的 V4L2 设备功能。 我们将从描述和

    2023年04月25日
    浏览(33)
  • RK3568 安卓源码编译

    项目模块化/组件化之后各模块也作为独立的 Git 仓库从主项目里剥离了出去,各模块各自管理自己的版本。Android源码引用了很多开源项目,每一个子项目都是一个Git仓库,每个Git仓库都有很多分支版本,为了方便统一管理各个子项目的Git仓库,需要一个上层工具批量进行处理

    2024年02月11日
    浏览(29)
  • RK3568的CAN驱动适配

    目录 背景: 1.内核驱动模块配置 2.设备树配置 3.功能测试 4.bug修复         某个项目上使用RK3568的芯片,需要用到4路CAN接口进行通信,经过方案评审后决定使用RK3568自带的3路CAN外加一路spi转的CAN实现功能,在这个平台上进行CAN驱动的适配和测试。 图一 应用原理框图 1

    2024年02月07日
    浏览(28)
  • rk3568驱动开发之mipi屏

    屏是嵌入式驱动开发中常见的设备,一般的带屏项目中最开始要调试的,简单记录一下自己在项目开发中的经验过程。所用平台是rockchip的rk3568,android11。 硬件原理图主要看接的是哪个mipi接口,屏的电源控制io,背光控制io,这些需要在设备树中配置的要仔细核对。 PS:以上

    2024年02月12日
    浏览(28)
  • RK系列(RK3568) 收音机tef6686芯片驱动,i2c驱动

    SOC:RK3568 模块:tef6686 系统:Android12 1.首先目前tef6686只有单片机才有驱动,Linux要集成只需要控制模块内部的i2c地址的顺序 从github下载tef6686 Andruino的代码 https://github.com/tehniq3/TEF6686 解压进入TEF6686-masterTEF6686_1602i2c_v6beta 这时候你可以发现TEF6686_1602i2c_v5.ino 和其他C++文件 .ino 里的

    2024年02月11日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包