OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 273|回复: 2

移植 Linux 内核的"自动初始化"机制到单片机

[复制链接]

1

主题

16

帖子

0

精华

初级会员

Rank: 2

积分
58
金钱
58
注册时间
2023-11-10
在线时间
19 小时
发表于 2026-1-16 23:48:56 | 显示全部楼层 |阅读模式
1金钱
搜索 告别冗长的 main 函数:移植 Linux 内核的"自动初始化"机制到单片机  最后卡在分散加载文件上来.有研究的不?原文告别冗长的 main 函数:移植 Linux 内核的"自动初始化"机制到单片机[color=rgba(0, 0, 0, 0.9)][color=rgba(0, 0, 0, 0.3)]原创 [color=var(--weui-FG-2)]一枚嵌入式码农 [color=var(--weui-LINK)][url=]一枚嵌入式码农[/url]
2026年1月11日 07:31 广东
[color=rgba(0, 0, 0, 0.9)]
[size=1em]用链接脚本玩出花活,让模块自己注册自己

0. 引言:你的 main 函数是不是也长这样?
打开你的项目,翻到 main.c,是不是这种画风:
int main(void) {
    HAL_Init();
    SystemClock_Config();

    // 下面是无尽的初始化...

    GPIO_Init();
    Uart_Init();
    I2C_Init();
    SPI_Init();
    Screen_Init();
    Wifi_Init();
    Sensor_Init();
    Motor_Init();
    LED_Init();
    Key_Init();
    // ...此处省略20行


    while
(1) {
        // 业务逻辑

    }
}
别笑,咱们都写过。
这种写法有两个致命问题:
问题一:耦合度爆表。 每次新增一个功能模块,比如加个温湿度传感器,你不仅要写 dht11.c,还得跑到 main.c 里加一行 DHT11_Init()。多人协作的时候,main.c 就成了兵家必争之地,天天冲突。
问题二:容易遗漏。 头文件引用了,驱动也写好了,结果忘了在 main 里调 Init 函数。程序跑飞了查半天,最后发现是初始化没调——相信不少人都踩过这个坑。
Linux 内核从来不这么干。它用一个叫 module_init 的宏,让每个模块"自己注册自己"。今天我们就把这套机制搬到单片机上。

1. 核心原理:链接器不只是用来生成 hex 的
很多嵌入式工程师只盯着编译器(Compiler),对链接器(Linker)知之甚少。顶多知道它能生成 hex 文件,用来烧录。
但链接器能玩的花活远不止这些。
"段"是个什么玩意儿
你写的代码,编译完之后并不是乱糟糟堆在一起的。它们被分门别类地放在不同的"段"(Section)里:
  • • 代码放在 .text 段
  • • 已初始化的全局变量放在 .data 段
  • • 未初始化的全局变量放在 .bss 段
这些是编译器自动安排的。但重点来了:你完全可以自己发明一个段,比如叫 .my_init_call,然后把东西塞进去。
自动初始化的核心思路
既然能自定义段,我们就可以这么玩:
  • 1. 定义一个自定义段:比如叫 .auto_init
  • 2. 把函数指针塞进去:每个模块的 Init 函数,通过宏把它的函数指针强行塞到这个段里
  • 3. 它们会在内存中连续排列:就像一个数组,只不过这个"数组"是链接器帮你组装的
  • 4. 启动时遍历执行:只要知道这个段的起始地址和结束地址,一个 for 循环就能把所有 Init 函数都调一遍
原理就这么简单。难点在于:怎么让编译器和链接器配合你完成这件事。

2. 手把手移植实战(GCC / Keil 双适配)
下面开始上代码。我会以 GCC 为主讲解,Keil 用户也别急,原理完全一样,我会标注区别。
第一步:定义函数指针类型
先给初始化函数定个统一的接口:
typedef int (*init_fn_t)(void);
所有要注册的初始化函数,都得长这样:返回 int,参数为空。
第二步:实现"注册宏"
这是整个机制的灵魂:
// GCC 写法
#define INIT_EXPORT(fn) \
    static init_fn_t __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]
\
    __attribute__((used, section(".auto_init"))) = fn
看着有点绕,拆开来说:
  • • __attribute__((section(".auto_init"))):告诉编译器,把这个变量放到 .auto_init 段里,而不是默认的 .data 段
  • • __attribute__((used)):告诉编译器"这个变量我用了,别给我优化掉"。因为这个变量没有被显式引用,编译器可能会认为它是死代码,直接删掉
  • • __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]:用函数名拼出一个变量名,避免多个模块冲突
Keil AC6 的写法几乎一样。如果用 AC5(armcc),语法略有不同:
// Keil AC5 写法
#define INIT_EXPORT(fn) \
    static init_fn_t __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]
\
    __attribute__((used, section("auto_init"))) = fn
注意 AC5 的段名不需要加点号前缀。
第三步:修改链接脚本
光有宏还不够,链接器得知道这个段放在哪,以及它的边界在哪。
GCC(.ld 文件):
在你的链接脚本里找到 .text 段附近,加上这么一段:
.auto_init : {
    . = ALIGN(4);
    __init_start = .;
    KEEP(*(.auto_init))
    __init_end = .;
} > FLASH
解释一下:
  • • __init_start 和 __init_end:这是两个符号,标记这个段的起始和结束地址
  • • KEEP(...):告诉链接器"这些东西不能删",防止被当成未引用代码优化掉
  • • . = ALIGN(4):4 字节对齐,保证函数指针读取正确
Keil:
好消息是,Keil 比较智能。对于 AC6,它会自动生成 Image$$auto_init$$Base 和 Image$$auto_init$$Limit 这样的符号。你只需要在代码里这样声明:
extern init_fn_t Image$$auto_init$$Base[];
extern
init_fn_t Image$$auto_init$$Limit[];
#define __init_start Image$$auto_init$$Base

#define __init_end   Image$$auto_init$$Limit
不用手动改散列文件,省事不少。
第四步:实现自动遍历器
最后一步,写一个函数来遍历执行所有注册的初始化函数:
extern init_fn_t __init_start[];
extern
init_fn_t __init_end[];

void
auto_init(void)
{
    init_fn_t
*fn;

    for
(fn = __init_start; fn < __init_end; fn++) {
        if
(*fn) {
            (*fn)();
        }
    }
}
现在你的 main() 就可以瘦身成这样了:
int main(void)
{
    HAL_Init();
    SystemClock_Config();

    auto_init();  // 一行搞定所有模块初始化

    while
(1) {
        // 业务逻辑

    }
}使用示例
在任意一个模块文件底部,比如 uart.c:
static int uart_init(void)
{
    // 串口初始化代码...

    return
0;
}
INIT_EXPORT(uart_init);
完事。不用去动 main 函数,不用到处加 extern。这个模块只要参与编译,它的初始化函数就会被自动调用。
调试小技巧: 如果程序卡死在初始化阶段,不知道是哪个模块出了问题,可以在 auto_init() 循环里加个打印:
printf("Calling init at: %p\n", *fn);
(*fn)();
通过地址配合 .map 文件,很容易定位是谁的锅。

3. 进阶玩法:给初始化分个等级
基础版本搞定了,但细心的你可能会发现一个问题:
如果 Wifi_Init() 依赖 SPI_Init() 呢?
自动初始化的执行顺序,通常取决于链接顺序(哪个 .o 文件先链接)。这个顺序是不稳定的,换个编译器版本可能就变了。
Linux 内核怎么解决的?它定义了一堆不同等级的初始化宏:early_initcall、subsys_initcall、device_initcall……等级越低,越早执行。
我们也可以抄这个思路。
利用段名的字母排序
链接器在排列段的时候,会按照段名的 ASCII 码顺序来排。也就是说,.init_0 会排在 .init_1 前面,.init_1 会排在 .init_2 前面。
利用这个特性,我们可以定义一组分级宏:
// 第 0 级:板级初始化(时钟、GPIO 基础配置)
#define INIT_BOARD_EXPORT(fn) \
    static init_fn_t __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]
\
    __attribute__((used, section(".init_0"))) = fn

// 第 1 级:驱动初始化(SPI、I2C、UART 等)

#define INIT_DRIVER_EXPORT(fn) \
    static init_fn_t __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]
\
    __attribute__((used, section(".init_1"))) = fn

// 第 2 级:设备初始化(传感器、屏幕等,依赖驱动)

#define INIT_DEVICE_EXPORT(fn) \
    static init_fn_t __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]
\
    __attribute__((used, section(".init_2"))) = fn

// 第 3 级:应用初始化(业务逻辑,依赖设备)

#define INIT_APP_EXPORT(fn) \
    static init_fn_t __init_#[color=rgb(87, 107, 149) !important][url=]#fn[/url]
\
    __attribute__((used, section(".init_3"))) = fn
链接脚本里也要相应改一下:
.auto_init : {
    . = ALIGN(4);
    __init_start = .;
    KEEP(*(.init_0))
    KEEP(*(.init_1))
    KEEP(*(.init_2))
    KEEP(*(.init_3))
    __init_end = .;
} > FLASH
这样一来,执行顺序就有保障了:板级 → 驱动 → 设备 → 应用,依赖关系自然满足。
使用的时候也很直观:
// spi.c
INIT_DRIVER_EXPORT(spi_init);

// wifi.c(依赖 SPI)

INIT_DEVICE_EXPORT(wifi_init);
不用操心谁先谁后,等级定好了,顺序就定了。

4. 总结与价值
回头看看,我们干了什么?
之前的 main 函数: 二三十行初始化代码,每加一个模块就得改一次,多人协作天天冲突。
现在的 main 函数:
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    auto_init();
    while
(1) { /* ... */ }
}
三行搞定,清爽无比。
这就是软件架构里常说的**"开闭原则"**——对扩展开放,对修改关闭。新加一个模块?写好驱动,文件末尾加一行 INIT_EXPORT,完事。不用碰 main 函数,不用改任何现有代码。
这种写法在 RT-Thread、FreeRTOS 等嵌入式操作系统里非常常见。掌握了它,你就摸到了构建大型嵌入式系统的门槛。
说到底,MCU 开发和 Linux 开发并没有那么割裂。很多优秀的架构思想是通用的,只不过实现方式有所不同。多翻翻 Linux 内核代码,收获的不只是技术细节,更是解决问题的思路。





回复

使用道具 举报

0

主题

459

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4211
金钱
4211
注册时间
2016-3-19
在线时间
970 小时
发表于 2026-1-19 17:00:57 | 显示全部楼层
学习了 谢谢分享
Nothing is impossible
回复

使用道具 举报

17

主题

588

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4472
金钱
4472
注册时间
2013-6-27
在线时间
565 小时
发表于 2026-1-21 22:13:08 | 显示全部楼层
cola系统也有这部分,还有简易设备注册管理,os两个模块.
让我们的思维驾驭在电的速度之上!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-1-29 06:09

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表