Linux淘金记(一):module_init——初始化就该这么写

没头绪时,Linux源码必有解决方案。 ——伊曼努尔·康德

一、shi山般的初始化

写裸机时,初始化程序简单粗暴这么写:

#include "gpio.h"
#include "uart.h"
#include "led.h"
#include "spi.h"
...
int main(void)
{
  gpio_init();
  uart_init();
  led_init();
  spi_init();
  ...
}

优雅点这么写:

#include "gpio.h"
#include "uart.h"
#include "led.h"
#include "spi.h"
...
void bsp_init(void)
{
  gpio_init();
  uart_init();
  led_init();
  spi_init();
  ...
}

int main(void)
{
  bsp_init();
  ...
}

真的优雅么?bsp_init就是一个fen坑!
问题一:
所有init函数调用都耦合到bsp_init()里,意味着每加一个驱动程序就得在bsp_init追加这个调用。一千个设备就得扩到上千行,增删改查恶心得要吐,还容易丢三落四,制造bug。自己的shi还好,别人的呢?
问题二:
对嵌入式项目,不同的定制方案需要不同的bsp_init,你需要在为每个项目重写一遍bsp_init,无聊又浪费精力。
问题三:
有没有这样的初始化办法,每个工程的main函数都调用一个相同的bsp_init,每个驱动的c文件无需将自己的xxx_init函数添加到bsp_init,bsp_init会自动调用它们?

对于问题三,像是做梦。但是回头想想,咱们能想到的问题,写内核的神级前辈们就没想到吗?来看看Linux内核的方案。

二、 释放函数名的本质

有这么个公式:

xxx_init == 函数名 == 指针 == 数据

那么c文件中的xxx_init函数就是数据,数据要放到内存中,内存又可寻址。
诶?灵感来了,要把猫赶到一个笼子里去。把所有bsp_init要调用的函数名字放到一个已知地址的内存段中,然后bsp_init去遍历这个内存段,并通过每个函数指针调用这个函数原型。当然这个灵感不是笔者想的,是写内核的神人想的。

Linux内核是这么做的:

第一步:把猫赶到一个笼子里,利用module_init

Linux几乎每个驱动模块的结尾都要使用module_init这个宏:

#define module_init(x)	          __initcall(x);
#define __initcall(fn)            device_initcall(fn)
#define device_initcall(fn)		  __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec) \
	static initcall_t __initcall_##fn##id __used \
		__attribute__((__section__(#__sec ".init"))) = fn;
#endif

展开:

#define module_init(x)  static initcall_t __initcall_##x##6 __used \
		__attribute__((__section__(".initcall6.init"))) = x;

例如module_init(led_init)就能展开为:

static initcall_t __initcall_led_initinit6 __used \
__attribute__((__section__(".initcall6.init"))) = led_init;

定义一个静态函数指针__initcall_led_initinit6指向初始化函数led_init,并且利用__attribute__把__initcall_led_initinit6链接到".initcall6.init"这个内存数据段里。简单来说就是把函数名丢到名为.initcall6.init的内存段里。如果每个驱动文件都这么做的话会出现这样:
在这里插入图片描述

第二步:遍历猫笼子,do_initcalls()

内核启动时会调用do_initcalls(),原型位于init/main.c:

static void __init do_initcalls(void)
{
	int level;

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

循环调用do_initcall_level,原型(保留核心代码):

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,
};
/*...*/
static void __init do_initcall_level(int level)
{
	initcall_entry_t *fn;
	/* ... */
	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(initcall_from_entry(fn));
}

最后do_one_initcall(保留核心代码):


int __init_or_module do_one_initcall(initcall_t fn)
{
    /* ... */
	ret = fn();
	return ret;
	/* ... */
}

遍历过程如图:
在这里插入图片描述
这样驱动文件中的xxx_init无需追加到do_initcalls中,只需在本文件使用module_init(xxx_init),然后在main函数调用do_initcalls。本质上这种做法是把在bsp_init函数中追加函数的工作交给了链接器。

三、在keil中实现module_init及万能初始化函数

第一步、修改链接脚本

1.找到MDK的链接脚本xxx.sct(xxx为工程名)的路径。注意这个文件需要编译工程后生成。
在这里插入图片描述
2.拷贝这个文件并重命名为module.sct。笔者这里把它放到了工程根目录下以免被误删。
在这里插入图片描述
3.链接器中选择module.sct,并打开编辑。
在这里插入图片描述
打开后如下:
在这里插入图片描述
4.追加自定义数据段_initcall6_init,这里模仿内核添加8个数据段:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00100000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00100000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00020000  {  ; RW data
   .ANY (+RW +ZI)
  }
  _initcall0_init +0 {
   .ANY (_initcall0_init)
  }
  _initcall1_init +0 {
   .ANY (_initcall1_init)
  }
  _initcall2_init +0 {
   .ANY (_initcall2_init)
  }
  _initcall3_init +0 {
   .ANY (_initcall3_init)
  }
  _initcall4_init +0 {
   .ANY (_initcall4_init)
  }
  _initcall5_init +0 {
   .ANY (_initcall5_init)
  }
  _initcall6_init +0 {
   .ANY (_initcall6_init)
  }
  _initcall7_init +0 {
   .ANY (_initcall7_init)
  }
}

完成!

第二步、实现module_init

创建一个init.h文件,module_init的宏基本照抄Linux内核:

#ifndef __INIT_H
#define __INIT_H

typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);

#define __init
#define __exit
#define __used  __attribute__((__used__))

#define __define_initcall(fn, id) __used \
	static volatile initcall_t __initcall_##fn##id \
	__attribute__((__section__("_initcall" #id "_init"))) = fn

#define pure_initcall(fn)		__define_initcall(fn, 0)
#define core_initcall(fn)		__define_initcall(fn, 1)
#define postcore_initcall(fn)   __define_initcall(fn, 2)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define late_initcall(fn)		__define_initcall(fn, 7)

#define __initcall(fn) device_initcall(fn)

#define __exitcall(fn) \
	static volatile exitcall_t __exitcall_##fn__exit_call = fn

#define module_init(x)	__initcall(x);
#define module_exit(x)	__exitcall(x);

void __init do_initcalls(void);

#endif  /* __INIT_H */

第三步、实现do_initcalls()

仿照内核,写init.c:

#include "init.h"

#if defined ( __CC_ARM   )
extern unsigned int Image$$ER_IROM1$$Base;
extern unsigned int Image$$ER_IROM1$$Limit;
extern unsigned int Image$$ER_IROM1$$Length;   

extern unsigned int Image$$RW_IRAM1$$Base;
extern unsigned int Image$$RW_IRAM1$$Limit;
extern unsigned int Image$$RW_IRAM1$$Length;     

extern unsigned int Image$$RW_IRAM1$$ZI$$Base;
extern unsigned int Image$$RW_IRAM1$$ZI$$Limit;
extern unsigned int Image$$RW_IRAM1$$ZI$$Length;

extern unsigned int Image$$_initcall0_init$$Base;
extern unsigned int Image$$_initcall0_init$$Limit;
extern unsigned int Image$$_initcall0_init$$Length;	

extern unsigned int Image$$_initcall1_init$$Base;
extern unsigned int Image$$_initcall1_init$$Limit;
extern unsigned int Image$$_initcall1_init$$Length;

extern unsigned int Image$$_initcall2_init$$Base;
extern unsigned int Image$$_initcall2_init$$Limit;
extern unsigned int Image$$_initcall2_init$$Length;

extern unsigned int Image$$_initcall3_init$$Base;
extern unsigned int Image$$_initcall3_init$$Limit;
extern unsigned int Image$$_initcall3_init$$Length;

extern unsigned int Image$$_initcall4_init$$Base;
extern unsigned int Image$$_initcall4_init$$Limit;
extern unsigned int Image$$_initcall4_init$$Length;

extern unsigned int Image$$_initcall5_init$$Base;
extern unsigned int Image$$_initcall5_init$$Limit;
extern unsigned int Image$$_initcall5_init$$Length;

extern unsigned int Image$$_initcall6_init$$Base;
extern unsigned int Image$$_initcall6_init$$Limit;
extern unsigned int Image$$_initcall6_init$$Length;

extern unsigned int Image$$_initcall7_init$$Base;
extern unsigned int Image$$_initcall7_init$$Limit;
extern unsigned int Image$$_initcall7_init$$Length;
	
#define __initcall0_start (initcall_t *)&Image$$_initcall0_init$$Base
#define __initcall1_start (initcall_t *)&Image$$_initcall1_init$$Base
#define __initcall2_start (initcall_t *)&Image$$_initcall2_init$$Base
#define __initcall3_start (initcall_t *)&Image$$_initcall3_init$$Base
#define __initcall4_start (initcall_t *)&Image$$_initcall4_init$$Base
#define __initcall5_start (initcall_t *)&Image$$_initcall5_init$$Base
#define __initcall6_start (initcall_t *)&Image$$_initcall6_init$$Base
#define __initcall7_start (initcall_t *)&Image$$_initcall7_init$$Base
#define __initcall_end    (initcall_t *)&Image$$_initcall7_init$$Limit

#endif  /* #if defined ( __CC_ARM   ) */

#define ARRAY_SIZE(a)  (sizeof(a)/sizeof(a[0]))
static initcall_t *initcall_levels[] = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

static int do_one_initcall(initcall_t fn)
{
  int ret;
  ret = fn();
  return ret;
}

static void do_initcall_level(int level)
{
	initcall_t *fn;
  for (fn = initcall_levels[level]; fn < initcall_levels[level + 1]; fn++)
    do_one_initcall(*fn);
}

void __init do_initcalls(void)
{
	int level;

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

注意:这里MDK使用自己的armcc编译工具链,获取数据段地址的语法于gcc不同,MDK内置了全局变量表示内存段的信息:

extern unsigned int Image$$name$$Base;    //这个变量位于name段的起始
extern unsigned int Image$$name$$Limit;   //位于name段的结束地址加1,也是下一段的开始
extern unsigned int Image$$name$$Length;  //name段长度

其他函数提取核心代码。

四、在STM32测试一下

main函数无脑调用do_initcalls:

#include "stm32f4xx.h"
#include "stdio.h"
#include "console.h"
#include "init.h"

int main(void)
{
	console_init();
	
	printf("start test!\r\n");
	
	do_initcalls();
	
  while(1) {
		
	}
}

添加两个测试模块文件module1.c和module2.c,利用module_init注册初始化函数:

#include "stdio.h"
#include "init.h"

int module1_init(void)
{
	printf("module1 init!\r\n");
	return 0;
}

module_init(module1_init);

#include "stdio.h"
#include "init.h"

int module2_init(void)
{
	printf("module2 init!\r\n");
	return 0;
}

module_init(module2_init);

编译,运行,完美!
在这里插入图片描述

五、优雅!

现在我们无需在main.c里include一堆头文件,然后bsp_init里加一堆init函数,只需要在main函数里调用do_initcalls,然后在每个需要初始化的.c文件内用module_init宏注册init函数。你甚至不用给这个.c文件做个.h文件,而且删除某个.c文件完全不会报错!最大的好处是我们可以在不同的项目上复用这个do_initcalls,只需更改链接脚本就可以。这就是松耦合的优雅。当然这块金子的价值远不止于此,期待日后的开发。

参考:
https://blog.csdn.net/weixin_39094034/article/details/123549216

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要实现这个功能,你可以使用淘金池串口屏的按钮事件和图片切换功能。具体步骤如下: 1. 首先,你需要在淘金池串口屏上设置一个按钮事件,当按钮被按下时触发该事件。 2. 然后,你需要在代码中编相应的处理函数,处理按钮事件。在处理函数中,你需要判断当前显示的图片,并根据需要切换到另一个图片。 3. 切换图片的具体操作,可以使用淘金池串口屏提供的图片切换功能。你可以使用 `pic()` 函数来切换图片,该函数接受两个参数,第一个参数是图片的编号,第二个参数是图片显示的时间(以毫秒为单位)。 4. 最后,为了保持另一个图片不动,你需要在切换到另一个图片时,将该图片的显示时间设置为一个非常大的值,比如 9999999。这样就可以让该图片一直保持不动了。 示例代码如下: ``` #include <TJC.h> void setup() { Serial.begin(9600); TJC.begin(); // 设置按钮事件 TJC.setButtonHandler(1, buttonHandler); // 显示第一张图片 TJC.pic(1, 5000); } void loop() { // 主循环 } void buttonHandler(int button) { // 处理按钮事件 int currentPic = TJC.currentPic(); // 获取当前显示的图片编号 if (currentPic == 1) { // 切换到第二张图片,并设置显示时间为 9999999 毫秒 TJC.pic(2, 9999999); } else { // 切换到第一张图片,并设置显示时间为 5000 毫秒 TJC.pic(1, 5000); } } ``` 这段代码会在淘金池串口屏上显示两张图片,当按钮被按下时,会切换到另一个图片并保持不动。注意,这只是一个示例代码,具体实现方式可能会因为硬件和软件环境的不同而有所不同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值