0. 你的main函数是不是也长这样?

打开你的STM32项目,翻到main.c,是不是这种画风:

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. 模块自己注册自己的魔法

核心思想很简单:每个模块在编译时主动上报自己的初始化函数,系统启动时自动收集并执行

实现这个黑科技只需要两个文件:auto_init.hauto_init.c

核心头文件(auto_init.h)

#ifndef __AUTO_INIT_H__
#define __AUTO_INIT_H__

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>
#include <stddef.h>

/* 编译器检测 */
#if defined(__CC_ARM) || (defined(__ARMCC_VERSION) && __ARMCC_VERSION >= 6000000)
    /* ARM Compiler (Keil MDK) */
    #define USING_ARM_COMPILER  1
#elif defined(__GNUC__)
    /* GCC Compiler */
    #define USING_GCC_COMPILER  1
#elif defined(__ICCARM__)
    /* IAR Compiler */
    #define USING_IAR_COMPILER  1
#else
    #error "Unsupported compiler"
#endif

/* 属性定义 */
#if USING_ARM_COMPILER || USING_GCC_COMPILER
    #define AUTO_INIT_ATTR_USED        __attribute__((used))
    #define AUTO_INIT_ATTR_SECTION(n)  __attribute__((section(n)))
    #define AUTO_INIT_ATTR_ALIGNED     __attribute__((aligned(4)))
#elif USING_IAR_COMPILER
    #define AUTO_INIT_ATTR_USED        __root
    #define AUTO_INIT_ATTR_SECTION(n)  @ n
    #define AUTO_INIT_ATTR_ALIGNED
#endif

/* 初始化等级 */
#define LEVEL_BOARD    0
#define LEVEL_CORE     1
#define LEVEL_DRIVER   2
#define LEVEL_DEVICE   3
#define LEVEL_APP      4
#define LEVEL_USER     5

/* 默认优先级 */
#define PRIO_NORMAL    100
#define PRIO_EARLY     50
#define PRIO_LATE      150

/* 初始化条目结构体 */
typedef struct {
    void (*func)(void);    /* 初始化函数指针 */
    uint8_t level;         /* 初始化等级 0-255 */
    uint8_t prio;          /* 优先级 0-255 */
    uint16_t reserved;     /* 保留,用于对齐 */
} init_entry_t;

/* 主注册宏 */
#define AUTO_INIT(func, level, prio) \
    static init_entry_t _auto_init_##level##_##prio##_##func \
    AUTO_INIT_ATTR_USED \
    AUTO_INIT_ATTR_ALIGNED \
    AUTO_INIT_ATTR_SECTION("AUTO_INIT_TABLE") \
    = {(func), (level), (prio), 0}

/* 便捷宏 - 带优先级参数 */
#define INIT_BOARD(func, prio)      AUTO_INIT(func, LEVEL_BOARD, prio)
#define INIT_CORE(func, prio)       AUTO_INIT(func, LEVEL_CORE, prio)
#define INIT_DRIVER(func, prio)     AUTO_INIT(func, LEVEL_DRIVER, prio)
#define INIT_DEVICE(func, prio)     AUTO_INIT(func, LEVEL_DEVICE, prio)
#define INIT_APP(func, prio)        AUTO_INIT(func, LEVEL_APP, prio)
#define INIT_USER(func, prio)       AUTO_INIT(func, LEVEL_USER, prio)

/* 自定义初始化 */
#define INIT_CUSTOM(func, level, prio) AUTO_INIT(func, level, prio)

/*----------------------------------------------------------------------------
 * 编译器适配的段边界宏
 *----------------------------------------------------------------------------*/

#if USING_ARM_COMPILER
    /* Keil MDK */
    extern const init_entry_t AUTO_INIT_TABLE$$Base[];
    extern const init_entry_t AUTO_INIT_TABLE$$Limit[];
    
    #define AUTO_INIT_TABLE_START()    AUTO_INIT_TABLE$$Base
    #define AUTO_INIT_TABLE_END()      AUTO_INIT_TABLE$$Limit
    
#elif USING_GCC_COMPILER
    /* GCC */
    extern const init_entry_t __start_AUTO_INIT_TABLE[];
    extern const init_entry_t __stop_AUTO_INIT_TABLE[];
    
    #define AUTO_INIT_TABLE_START()    __start_AUTO_INIT_TABLE
    #define AUTO_INIT_TABLE_END()      __stop_AUTO_INIT_TABLE
    
#elif USING_IAR_COMPILER
    /* IAR */
    #pragma section="AUTO_INIT_TABLE"
    
    #define AUTO_INIT_TABLE_START()    __section_begin("AUTO_INIT_TABLE")
    #define AUTO_INIT_TABLE_END()      __section_end("AUTO_INIT_TABLE")
    
#endif

/* API函数声明 */
void auto_init(void);
size_t auto_init_get_count(void);

#ifdef __cplusplus
}
#endif

#endif /* __AUTO_INIT_H__ */

核心文件(auto_init.c)

#include "auto_init.h"

/* 简单的冒泡排序实现 */
void bubble_sort(init_entry_t *array, int size)
{
    int i, j;
    volatile int swapped;  /* volatile防止循环优化 */
    
    for (i = 0; i < size - 1; i++) {
        swapped = 0;
        for (j = 0; j < size - i - 1; j++) {
            /* 使用volatile指针确保从内存读取最新值 */
            volatile init_entry_t *a = &array[j];
            volatile init_entry_t *b = &array[j + 1];
            
            if (a->level > b->level || (a->level == b->level && a->prio > b->prio)) {
                
                /* 强制内存交换 */
                init_entry_t temp;
                
                /* 方法1:通过volatile指针访问 */
                temp = *a;
                *a = *b;
                *b = temp;
                swapped = 1;
            }
        }
        if (!swapped) break;
    }
}


/* 获取初始化函数数量 */
size_t auto_init_get_count(void)
{
    const init_entry_t *start = AUTO_INIT_TABLE_START();
    const init_entry_t *end = AUTO_INIT_TABLE_END();
    
    if (!start || !end || start >= end) {
        return 0;
    }
    
    return (size_t)(end - start);
}

/* 执行自动初始化 */
void auto_init(void)
{
    /* 获取段边界 */
    const init_entry_t *start = AUTO_INIT_TABLE_START();
    const init_entry_t *end = AUTO_INIT_TABLE_END();
    
    /* 检查有效性 */
    if (!start || !end || start >= end) {
        return;
    }
    
    /* 计算元素个数 */
    size_t count = (size_t)(end - start);
    
    if (count == 0) {
        return;  /* 没有初始化函数 */
    }
    
    /* 创建临时指针(注意:这会修改原始数据) */
    init_entry_t *temp_array = (init_entry_t *)start;
    
    /* 先排序 */
    bubble_sort(temp_array, count);
    
    /* 按顺序执行初始化函数 */
    for (size_t i = 0; i < count; i++) {
        if (temp_array[i].func != NULL) {
            temp_array[i].func();
        }
    }
}

2. 使用示例:爽到飞起

以前的做法(main.c变成大杂烩)

c

// main.c
int main(void) {
    // ...系统初始化
    
    // 手动调用每个初始化函数
    GPIO_Init();
    SPI_Init();
    LCD_Init();
    Touch_Init();
    Sensor_Init();
    Wifi_Init();
    // ...无穷无尽
    
    while(1);
}

现在的做法(模块自己管自己)

c

// spi.c - SPI驱动
static void spi_init(void) {
    // SPI初始化代码
}
INIT_DRIVER(spi_init, PRIO_NORMAL);  // 自动注册!

// lcd.c - LCD屏幕驱动
static void lcd_init(void) {
    // LCD初始化代码
}
INIT_DEVICE(lcd_init, PRIO_NORMAL);  // 自动注册!

// wifi.c - WiFi模块(依赖SPI)
static void wifi_init(void) {
    // WiFi初始化代码
}
INIT_DEVICE(wifi_init, PRIO_LATE);  // 晚点执行,确保SPI已就绪

main.c瘦身成功!

c

// main.c
#include "auto_init.h"

int main(void) {
    HAL_Init();
    SystemClock_Config();
    
    auto_init();  // 一行搞定所有初始化!
    
    while(1) {
        // 业务逻辑
    }
}

3. 解决依赖问题:智能排序

如果wifi_init依赖spi_init怎么办?我们的系统支持智能排序

  1. 按等级排序:LEVEL_BOARD → LEVEL_DRIVER → LEVEL_DEVICE → LEVEL_APP

  2. 同等级按优先级排序:PRIO_EARLY → PRIO_NORMAL → PRIO_LATE

所以:

  • spi_init注册为LEVEL_DRIVER

  • wifi_init注册为LEVEL_DEVICE

  • 系统会自动先执行spi_init,再执行wifi_init

如果都在同一等级,可以用优先级控制:

c

INIT_DRIVER(spi_init, PRIO_EARLY);     // 先执行
INIT_DRIVER(i2c_init, PRIO_NORMAL);    // 接着执行
INIT_DRIVER(uart_init, PRIO_LATE);     // 最后执行

4. 支持主流编译器(开箱即用)

这个方案已经适配了三大主流编译器:

编译器 支持状态 说明
GCC (STM32CubeIDE) ✅ 完美支持 无需任何配置
Keil MDK (AC5/AC6) ✅ 完美支持 无需任何配置
IAR Embedded Workbench ✅ 完美支持 无需任何配置

真正做到了开箱即用,不用改链接脚本,不用搞复杂的编译器配置。

5. 高级特性:按需初始化

5.1 获取初始化函数数量

c

size_t count = auto_init_get_count();
printf("共有%zu个模块自动注册\n", count);

5.2 自定义初始化等级

c

// 创建自定义等级
#define LEVEL_MY_SPECIAL  10

// 注册特殊初始化函数
INIT_CUSTOM(my_special_init, LEVEL_MY_SPECIAL, PRIO_NORMAL);

5.3 条件编译支持

c

#ifdef USING_WIFI
static void wifi_init(void) {
    // WiFi初始化代码
}
INIT_DEVICE(wifi_init, PRIO_NORMAL);
#endif

#ifdef USING_BLUETOOTH  
static void bt_init(void) {
    // 蓝牙初始化代码
}
INIT_DEVICE(bt_init, PRIO_NORMAL);
#endif

6. 实战:完整项目改造

改造前项目结构

text

project/
├── main.c        # 300行,包含所有初始化
├── gpio.c
├── uart.c
├── spi.c
├── lcd.c
├── wifi.c
└── sensor.c

每次新增模块都要:

  1. 编写驱动文件

  2. 在main.c添加头文件

  3. 在main()中添加初始化调用

  4. 担心调用顺序问题

改造后项目结构

text

project/
├── main.c        # 30行,极度清爽
├── auto_init.h   # 自动初始化框架
├── auto_init.c
├── gpio.c        # 自带注册:INIT_BOARD(gpio_init, PRIO_EARLY)
├── uart.c        # 自带注册:INIT_DRIVER(uart_init, PRIO_NORMAL)
├── spi.c         # 自带注册:INIT_DRIVER(spi_init, PRIO_EARLY)
├── lcd.c         # 自带注册:INIT_DEVICE(lcd_init, PRIO_NORMAL)
├── wifi.c        # 自带注册:INIT_DEVICE(wifi_init, PRIO_LATE)
└── sensor.c      # 自带注册:INIT_DEVICE(sensor_init, PRIO_NORMAL)

新增模块只需要一步:在模块文件末尾加一行INIT_xxx宏!

7. 性能与内存分析

内存占用

  • 每个初始化条目:8字节(函数指针 + 等级 + 优先级)

  • 10个模块:80字节

  • 50个模块:400字节

对于现代MCU(几十KB到几百KB RAM)来说,完全可以接受。

执行时间

  • 排序开销:O(n²)(使用冒泡排序,n通常很小)

  • 对于20个模块,排序时间可忽略不计

  • 主要时间还是各模块自身的初始化时间

8. 调试技巧

8.1 排查初始化问题

如果系统卡在启动阶段:

c

// 在auto_init.c中添加调试信息
for (size_t i = 0; i < count; i++) {
    printf("[Init] Level:%d, Prio:%d, Func:%p\n", 
           temp_array[i].level, 
           temp_array[i].prio,
           temp_array[i].func);
    temp_array[i].func();
}

8.2 验证执行顺序

c

// 在每个初始化函数中添加日志
static void spi_init(void) {
    printf("[SPI] Initializing...\n");
    // 初始化代码
}

9. 对比传统方法的优势

特性 传统方法 自动注册方法
新增模块 修改main.c 无需修改main.c
依赖管理 人工保证顺序 自动按等级排序
多人协作 容易冲突 互不干扰
代码复用 需要手动集成 模块自带注册信息
维护成本 高(集中式) 低(分布式)

10. 开始使用

10.1 快速集成

  1. 下载auto_init.hauto_init.c

  2. 添加到你的项目

  3. 在main.c中调用auto_init()

  4. 开始改造现有模块

10.2 现有项目迁移步骤

  1. 先将auto_init框架加入项目

  2. 选择一个简单模块(如LED驱动)进行试验

  3. LED_Init()调用从main.c移到led.c

  4. 在led.c末尾添加INIT_DRIVER(led_init, PRIO_NORMAL)

  5. 编译测试,确保正常工作

  6. 逐步迁移其他模块

  7. 最后清理main.c中的初始化代码

11. 适用场景

  • ✅ 中大型嵌入式项目(模块数量 > 5)

  • ✅ 团队协作开发

  • ✅ 产品线多个型号共用代码

  • ✅ 需要灵活配置功能(通过宏定义开启/关闭模块)

  • ✅ 希望提高代码复用性

  • ❌ 极简项目(只有2-3个模块)

  • ❌ 对启动时间极度敏感的应用

  • ❌ 内存极其紧张的MCU(< 4KB RAM)

12. 开源地址

完整代码已经开源,包含示例项目:

  • GitHub仓库:embedded-auto-init

  • 支持:GCC/Keil/IAR三平台

  • 文档:详细API说明和示例

13. 总结

从"集中式管理"到"分布式自治",这不仅是代码组织方式的改变,更是架构思维的升级。

自动注册机制带来的好处:

  1. 解耦:模块之间独立,main函数保持稳定

  2. 可维护:每个模块的初始化信息就在模块内部

  3. 可扩展:新增模块不用修改现有代码

  4. 可配置:通过宏定义轻松开启/关闭功能

  5. 可移植:模块可以无缝移植到其他项目

记住好的架构原则:开闭原则(对扩展开放,对修改关闭)。新增功能时,应该是添加新代码,而不是修改老代码。

告别臃肿的main函数,让你的嵌入式项目拥有Linux内核般的优雅架构!


最后留个思考题:如果把这种自动注册思想应用到其他方面,比如:

  • 命令解析器自动注册命令

  • 协议栈自动注册协议处理器

  • 中间件自动注册服务

能不能实现?如何实现?欢迎在评论区讨论!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐