模块初始化与清理:写好“入场”与“退场”

写嵌入式程序久了,你会发现一个规律:大部分崩溃,都发生在启动和关闭的时候

为什么?因为启动时资源还没就位,关闭时资源正在被撕扯。我早年做的一个车载项目,就因为在关机时没有按顺序释放 DMA 缓冲区,导致下一次开机直接硬 fault。从那以后,我对模块的 init 和 deinit 就格外上心。

今天我们就聊聊,怎么给每个模块写好“入场”和“退场”的规矩。

1. 模块的生命周期:从出生到销毁

一个模块,说白了就是一组数据和操作这些数据的函数。它的生命周期通常分四步:

  1. 初始化(init):分配资源、设置默认值、注册中断或回调。
  2. 运行(run):正常提供服务。
  3. 暂停/休眠(suspend/sleep):低功耗场景下临时关闭,但不释放资源。
  4. 清理(deinit):释放资源、注销中断、归还内存。

你想想看,如果 init 和 deinit 没写好,模块就像个不关水龙头的人——要么打不开,要么关不上。

核心原则:init 和 deinit 必须成对出现,且调用顺序要严格对称。谁先 init,谁后 deinit。

2. init 函数:把“地基”打牢

我个人习惯,每个模块只暴露一个 xxx_init() 函数。它内部做三件事:

  • 参数校验:传入的指针、句柄、配置参数是否合法。
  • 资源分配:malloc、信号量创建、队列创建、定时器注册。
  • 状态置位:把模块内部的状态机切到“就绪”状态。

来看一个简单的 UART 驱动模块:

// uart_driver.h
typedef struct {
    uint32_t baudrate;
    uint8_t  data_bits;
    uint8_t  stop_bits;
    uint8_t  parity;
} uart_config_t;

typedef struct uart_module uart_module_t;

// 初始化:返回句柄,失败返回 NULL
uart_module_t* uart_init(uart_config_t *cfg);

// 清理:归还所有资源
void uart_deinit(uart_module_t *handle);

实现里我会加一个“防重复初始化”的检查:

uart_module_t* uart_init(uart_config_t *cfg) {
    if (cfg == NULL) return NULL;

    // 检查硬件是否已被占用
    if (is_uart_busy(cfg->port)) {
        // 我曾经遇到过两个模块抢同一个外设,结果一个发一个收全乱套
        return NULL;
    }

    uart_module_t *mod = (uart_module_t*)malloc(sizeof(uart_module_t));
    if (mod == NULL) return NULL;

    // 配置硬件寄存器
    hal_uart_set_baudrate(cfg->port, cfg->baudrate);
    hal_uart_set_format(cfg->port, cfg->data_bits, cfg->stop_bits, cfg->parity);

    // 创建接收队列
    mod->rx_queue = osMessageQueueNew(64, sizeof(uint8_t), NULL);
    if (mod->rx_queue == NULL) {
        free(mod);
        return NULL;
    }

    mod->state = MODULE_READY;
    return mod;
}

小技巧:init 函数里如果中途失败,一定要把已经分配的资源全部释放掉,再返回 NULL。这叫“原子性”——要么全成功,要么像没发生过。

3. deinit 函数:优雅地“退场”

deinit 比 init 更容易写崩。为什么?因为 deinit 执行时,可能有别的任务还在用这个模块的资源。

我建议 deinit 遵循以下步骤:

  1. 通知所有使用者:模块即将关闭,请停止访问。
  2. 等待正在进行的操作完成:比如等待 DMA 传输结束、等待队列清空。
  3. 释放资源:按 init 的逆序释放。
  4. 句柄置空:防止野指针。
void uart_deinit(uart_module_t *handle) {
    if (handle == NULL) return;
    if (handle->state == MODULE_UNINIT) return;  // 防止重复释放

    // 1. 标记模块为“正在关闭”
    handle->state = MODULE_CLOSING;

    // 2. 等待接收队列清空(超时 100ms)
    uint8_t dummy;
    while (osMessageQueueGet(handle->rx_queue, &dummy, NULL, 10) == osOK) {
        // 丢弃残留数据
    }

    // 3. 关闭硬件中断
    hal_uart_disable_irq(handle->port);

    // 4. 释放队列
    osMessageQueueDelete(handle->rx_queue);

    // 5. 释放内存
    free(handle);

    // 注意:调用方需要把外部指针置 NULL
}

警告:不要在中断服务函数里调用 deinit!我曾经见过有人在 ISR 里释放信号量,结果死锁了整个系统。deinit 必须放在任务级代码中执行。

4. 模块生命周期管理:谁负责调用?

在一个多文件项目中,模块的 init/deinit 不能乱来。我常用的策略有两种:

策略 适用场景 优点 缺点
集中式管理 小型项目、裸机程序 调用顺序一目了然 耦合度高,新增模块要改主文件
分散式注册 大型项目、RTOS 环境 模块自管理,扩展性好 调试时调用链不直观

我个人偏爱“集中式管理”,尤其是在 bootloader 或安全关键系统中。我会在 main.c 里写一个 system_init() 函数,按依赖顺序调用各个模块的 init:

// system_init.c
void system_init(void) {
    // 先底层,后上层
    bsp_clock_init();       // 时钟系统
    bsp_gpio_init();        // GPIO
    bsp_uart_init();        // 调试串口
    bsp_i2c_init();         // I2C 总线
    sensor_init();          // 传感器模块(依赖 I2C)
    display_init();         // 显示模块(依赖传感器数据)
    app_task_init();        // 应用任务
}

关闭时,顺序反过来:

void system_deinit(void) {
    app_task_deinit();
    display_deinit();
    sensor_deinit();
    bsp_i2c_deinit();
    bsp_uart_deinit();
    bsp_gpio_deinit();
    bsp_clock_deinit();
}

避坑指南:我曾经在一个项目中,把 deinit 写成了“能关就关,关不掉拉倒”。结果系统休眠时,一个传感器没关干净,漏电把电池耗光了。从那以后,我要求每个 deinit 必须返回成功/失败状态,主循环里检查所有模块是否都成功关闭。

5. 资源释放的“三板斧”

嵌入式系统里,资源就那么几种。我总结了一个检查清单:

  • 内存:malloc 了就要 free,别忘了结构体内部的指针。
  • 硬件外设:关闭时钟、关闭中断、GPIO 恢复默认状态。
  • RTOS 对象:信号量、消息队列、互斥锁、任务——创建了就要删除。
  • 文件句柄:如果用了文件系统,记得 close。
  • 网络连接:socket、TCP 连接要主动关闭。

嗯,这里要注意:释放的顺序和分配的顺序相反。比如你 init 时先 malloc 了 A,再 malloc 了 B,那么 deinit 时先 free B,再 free A。否则 A 释放后,B 里可能还存着指向 A 的指针,变成野指针。

6. 用 SVG 理清思路

下面这张图,画的是一个典型模块从 init 到 deinit 的完整流程:

模块生命周期流程图 1. 参数校验 2. 资源分配 3. 状态置位 运行中(RUN) 1. 通知停止 2. 等待完成 3. 释放资源 4. 句柄置空 模块销毁 初始化阶段 运行阶段 清理阶段

7. 写在最后

模块的 init 和 deinit,说白了就是“有借有还”。你借了内存、借了外设、借了中断,就得在 deinit 里一一还回去。

我见过太多项目,只写 init 不写 deinit,觉得“反正系统一直跑着,不用关”。结果产品要做低功耗休眠时,发现根本关不干净,只能加班重构。嗯,别让自己陷入这种窘境。

从今天开始,每个新模块都先写好 deinit 再写 init——你会发现,代码的健壮性会上一个台阶。


公众号:蓝海资料掘金营,微信deep3321