支持使用keil编译的32位芯片使用Linux自注册机制,用于将所有初始化函数进行模块化处理

说明:
本文沿用项目中的“Linux 自注册”叫法。严格来说,当前实现不是 Linux 内核原版
initcall,而是一套借鉴 Linux/RT-Thread 思路、适用于裸机/FreeRTOS 固件的静态
初始化注册机制。它通过“编译期生成描述项 + 链接脚本集中收集 + 启动时扫描调用”
来代替在 main() 中逐个手工调用初始化函数。

传统写法通常把所有初始化函数直接写在 main() 中,例如:

GPIO_IO_Init();
UART_Init();
RTC_Init();
......

当模块增多时,这种方式会产生以下问题:

  1. main() 与所有模块强耦合;
  2. 新增或删除模块时必须同步修改 main();
  3. 初始化先后关系分散,不易按层次管理;
  4. 容易漏掉初始化调用。

当前项目把初始化过程分为 BOARD、DRIVER、DEVICE、APP 四级。模块只需在自己的
C 文件中声明注册宏,编译器和链接器就会把注册信息放入统一的 INITCALL 区域;
main() 只调用一次 do_initcalls(),即可按级别执行全部已注册函数。

这是一种静态自注册机制:注册关系在编译、链接阶段已经确定,不是运行期间动态
加载模块,也不是运行时向链表添加节点。

/.c文件实现/

点击查看代码
#include "initcall.h"

extern const initcall_t Image$$INITCALL_REGION$$Base;
extern const initcall_t Image$$INITCALL_REGION$$Limit;

void do_initcalls(void)
{
    const initcall_t *p;
    uint32_t level;

    for (level = 10; level <= 40; level += 10)
    {
        for (p = &Image$$INITCALL_REGION$$Base;
             p < &Image$$INITCALL_REGION$$Limit;
             p++)
        {
            if (p->level == level)
            {
                if (p->fn)
                {
                    p->fn();
                }
            }
        }
    }
}

/.h文件实现/

点击查看代码
/*
BOARD
│
├── clock
├── gpio
└── heap
     ↓

DRIVER
│
├── uart
├── spi
└── i2c
     ↓

DEVICE
│
├── w25qxx
├── sht30
└── lcd
     ↓

APP
│
├── shell
├── filesystem
├── modbus
└── mqtt
*/
#ifndef __INITCALL_H__
#define __INITCALL_H__

#include <stdint.h>

typedef void (*init_fn_t)(void);

typedef struct
{
    const char *name;
    uint32_t level;
    init_fn_t fn;
} initcall_t;

/* init levels */

#define INIT_LEVEL_BOARD     10
#define INIT_LEVEL_DRIVER    20
#define INIT_LEVEL_DEVICE    30
#define INIT_LEVEL_APP       40

/* export macro */

#define INIT_EXPORT(fn, lvl)                    \
    const initcall_t __init_##fn               \
    __attribute__((used))                      \
    __attribute__((section("INITCALL"))) =     \
    {                                          \
        #fn, lvl, fn                           \
    }

/* wrappers */

#define BOARD_INIT(fn) \
    INIT_EXPORT(fn, INIT_LEVEL_BOARD)

#define DRIVER_INIT(fn) \
    INIT_EXPORT(fn, INIT_LEVEL_DRIVER)

#define DEVICE_INIT(fn) \
    INIT_EXPORT(fn, INIT_LEVEL_DEVICE)

#define APP_INIT(fn) \
    INIT_EXPORT(fn, INIT_LEVEL_APP)

void do_initcalls(void);

#endif

/.sct文件实现/

点击查看代码
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00030000  {    ; boot reserved region, APP starts at 0x08030000
  ER_IROM1 0x08000000 0x00025000  {  ; main RO, INITCALL_REGION uses the remaining boot space
   *.o (RESET, +First)
   *(InRoot$$Sections)

   .ANY (+RO)
;   .ANY (+XO)
  }
  
	INITCALL_REGION +0 ALIGN 4
	{
	  *(INITCALL)
	}
	 

;	APP starts at 0x08030000. Keep INITCALL_REGION before that address.
;	If INITCALL_REGION is fixed manually, place it in the reserved boot gap.

  RW_IRAM1 0x20000000 0x00038000  {  ; RW data
   .ANY (+RW +ZI)
  }
}


/使用方式/
板层级别初始化,优先级第一 init level first
BOARD_INIT(GPIO_IO_Init);

/注册项的数据结构/
每个注册项包含三个字段:

  1. name
    初始化函数名称字符串。宏通过 #fn 自动把函数名转换为字符串。
    当前 do_initcalls() 没有使用该字段打印日志,但它可用于后续调试、统计或故障定位。

  2. level
    初始化级别,决定函数在哪一轮被调用。

  3. fn
    初始化函数地址,函数原型必须与 void fn(void) 兼容。

在当前 32 位 Cortex-M4/ARMCC 5 环境中,字符串指针、uint32_t 和函数指针均为
32 位,因此一个 initcall_t 通常占 12 字节。扫描代码并未把 12 写死,而是通过
initcall_t 指针自增,实际步长由 sizeof(initcall_t) 决定。

  1. BOARD(10)
    板级基础资源,例如基础时钟、引脚、最早期调试串口等。

  2. DRIVER(20)
    芯片外设驱动,例如 UART、SPI、I2C、RTC 等。

  3. DEVICE(30)
    建立在驱动之上的具体设备或中间件设备层,例如外部 Flash、传感器、FlashDB
    存储设备等。

  4. APP(40)
    上层组件和应用资源,例如文件系统、协议、队列、应用框架、内存钩子等。

级别之间的顺序由 do_initcalls() 明确保证:10 一定先于 20,20 一定先于 30,
30 一定先于 40。

使用教程:
1.可以直接使用我提供的.c和.h文件模板,需要了解如何自定义初始化优先级.
2.需要了解如何自定义.sct文件和sct文件内容意义,sct文件用于引导keil,主要内容就是需要规划程序放置的区域和函数存在的区域,比如我现在规划了LR_IROM1 0x08000000 0x00030000,意思是我现在的程序空间规划了0x00030000这个大小,ER_IROM1 0x08000000 0x00025000的话是我规划0x00025000用于存储程序,写在ER_IROM1后面的部分有0x5000的剩余空间是0x00025000后面,所以INITCALL_REGION放在了,原理是*(INITCALL) 是整个机制的关键:它把各个目标文件中名为 INITCALL 的输入节全部收集INITCALL_REGION,使多个分散模块的 initcall_t 描述项在 Flash 中形成一段连续表。
3.使用方式只需要在你的初始化函数写完后,在该函数下面把函数名填进去就行,例如;BOARD_INIT(GPIO_IO_Init);
4.keil需要设置取消自动.sct文件,选择并改用自己的sct文件.如下图:
image

5.在main函数中运行do_initcalls();函数会自动寻找注册过的函数进行运行.

posted @ 2026-08-05 14:50  BUG_KING  阅读(8)  评论(0)    收藏  举报