前段时间接到一个新任务,打开工程时,我还保持着微笑;双击main.c后,笑容逐渐凝固,一个文件,3000行代码,所有功能像一锅杂烩挤在一起。同事在旁边幽幽地说:“这就是我们要维护两年的代码,上一个人就要离职了。”

那一刻我明白了什么是“屎山”。

为什么嵌入式项目容易“烂尾”?

1. 速度优先思维
项目初期,老板总是说:“先做出来,能跑起来就行。”于是架构设计成了奢侈品,每个人都专注于“让灯先闪起来”。

2. 资源有限恐惧症
“MCU就32KB RAM,还敢搞分层?函数调用都嫌开销大!”这种思维让工程师不敢增加任何抽象层。

3. 硬件思维惯性
很多嵌入式开发者出身硬件,擅长寄存器操作,但对软件工程的理解停留在“能用就行”的阶段。

4. 人员流动性大
每个离职的工程师都在代码里留下自己的“特色”,后来者不敢大改,只能在原有基础上“打补丁”。

但问题在于:项目的生命周期不止于“跑起来”。当需要添加新功能、更换硬件平台、修复复杂Bug时,混乱的架构会让你加班到怀疑人生。

嵌入式分层架构:从“能用”到“好用”

经过多个项目的洗礼,我总结出经典的嵌入式软件四层架构模型。

核心原则:上层依赖下层,下层永远不知道上层的存在
就像公司组织结构:高层可以指挥中层,但中层绝不能越级指挥高层。

第一层:硬件抽象层(HAL)

职责:封装对MCU寄存器的操作,屏蔽硬件差异
价值:实现可移植性的第一道防线

去年我们项目需要从STM32F103迁移到ESP32。如果没有HAL层,几乎要重写所有代码;有了HAL层,只需重写硬件相关部分,上层业务逻辑几乎不变。

// 标准化的HAL接口
void hal_gpio_set_pin_mode(uint8_t port, uint8_t pin, uint8_t mode);
void hal_gpio_write_pin(uint8_t port, uint8_t pin, uint8_t level);
uint8_t hal_gpio_read_pin(uint8_t port, uint8_t pin);

技巧:ST、Nordic等厂商的官方库(如STM32 HAL库)已经是HAL层,可以在其基础上做二次封装,统一接口风格。

第二层:驱动层

职责:驱动具体的外部设备
关键:驱动层不关心硬件连接方式,只关心如何与设备通信

我在一个项目中同时使用了AHT20和SHT30两款温湿度传感器,但它们的I2C地址和通信协议不同。驱动层的价值就在于统一接口

// 统一的传感器接口
bool sensor_init(void);
bool sensor_read_temperature_humidity(float* temp, float* humi);

上层应用调用sensor_read_temperature_humidity()时,根本不需要知道底层是哪个传感器型号。

第三层:中间件/服务层

职责:提供通用的、与硬件无关的功能模块
示例:RTOS、TCP/IP协议栈、文件系统、日志系统

// 日志系统示例
#define LOG_INFO(format, ...) printf("[INFO] " format "\n", ##__VA_ARGS__)
#define LOG_ERROR(format, ...) printf("[ERROR] " format "\n", ##__VA_ARGS__)

void logger_init(void);  // 可绑定到UART、RTT或文件

这个日志系统可以被所有上层应用使用,而应用层不需要关心日志最终输出到哪里。

第四层:应用层

职责:实现产品的具体业务逻辑
目标:应用层代码应该像产品需求文档一样易读

// 清晰的应用层代码
void main_task(void* arg) {
    sensor_init();
    logger_init();
    
    while(1) {
        float temperature, humidity;
        if(sensor_read_temperature_humidity(&temperature, &humidity)) {
            LOG_INFO("温度:%.1f℃,湿度:%.1f%%", temperature, humidity);
            
            if(temperature > 30.0f) {
                // 温度过高,开启风扇
                fan_turn_on();
            }
        }
        hal_delay_ms(5000);
    }
}

理想状态:应用层只关心“做什么”,不关心“怎么做”。

接口设计:层与层的优雅“握手”

分层架构的核心在于接口设计。我借鉴了Linux内核的file_operations思想:

// 定义设备操作接口
typedefstruct {
    int (*init)(void);
    int (*read)(uint8_t *data, size_t len);
    int (*write)(constuint8_t *data, size_t len);
    int (*ioctl)(uint32_t cmd, void *arg);
} device_ops_t;

// A传感器实现
constdevice_ops_t sensor_a_ops = {
    .init = sensor_a_init,
    .read = sensor_a_read,
    .write = NULL,
    .ioctl = NULL,
};

// B传感器实现  
constdevice_ops_t sensor_b_ops = {
    .init = sensor_b_init,
    .read = sensor_b_read,
    .write = NULL,
    .ioctl = sensor_b_ioctl,
};

// 应用层只与抽象接口交互
void process_sensor(const device_ops_t* sensor) {
    uint8_t buffer[32];
    sensor->init();
    sensor->read(buffer, sizeof(buffer));
}

当需要新增传感器C时,只需实现对应的device_ops_t,应用层代码完全不用修改。这就是面向接口编程的魅力。

实战:重构3000行main.c

重构前(典型“屎山”):

// main.c - 3000行的“地狱”
void main(void) {
    // GPIO配置(直接操作寄存器)
    RCC->APB2ENR |= (1 << 2);
    GPIOA->CRL &= 0xFFFFFF00;
    GPIOA->CRL |= 0x00000033;
    // ... 还有100行类似的寄存器操作
    
    while(1) {
        // I2C通信(位操作)
        // 发送传感器地址
        // 读取原始数据
        // 手动解析数据
        uint32_t raw_data = ...;
        float temp = (float)raw_data * 200 / 1048576 - 50;
        
        // 软件延时
        for(volatileint i=0; i<1000000; i++);
    }
}

重构后(清晰分层):

project/
├── hal/           # 硬件抽象层
│   ├── hal_gpio.c
│   ├── hal_gpio.h
│   ├── hal_i2c.c
│   └── hal_i2c.h
├── drivers/       # 驱动层
│   ├── aht20.c
│   └── aht20.h
└── app/          # 应用层
    └── main.c
// app/main.c - 重构后
void main(void) {
    aht20_init();  // 初始化传感器
    
    while(1) {
        float temp, humi;
        if(aht20_read_temperature_humidity(&temp, &humi)) {
            printf("温度:%.1f,湿度:%.1f\n", temp, humi);
        }
        hal_delay_ms(2000);  // 使用HAL延时
    }
}

重构成效

  • 代码行数:main.c从3000行缩减到50行

  • 可读性:从“天书”变为“说明文档”

  • 可维护性:模块化,可独立测试和替换

  • 可移植性:更换MCU时,只需修改HAL层

避免这些“反模式”

在实施分层架构时,要警惕这些破坏原则的做法:

❌ 下层依赖上层
驱动层调用应用层函数,造成循环依赖。正确做法:使用回调函数,由应用层注册处理函数。

❌ 应用层直接操作寄存器
main.c里出现GPIOA->ODR = 0x01;,所有分层努力白费。正确做法:永远通过HAL接口操作硬件。

❌ 全局变量满天飞
几十个全局变量在各个模块间随意读写,数据流向混乱。正确做法:模块私有数据用static修饰,模块间通过接口通信。

❌ 过度设计
一个闪烁LED的项目,硬要上RTOS+事件总线+微服务架构。正确做法:架构为项目服务,小项目可以简化分层。

从现在开始,告别“屎山”

分层架构不是银弹,但它是复杂度和可维护性的最佳平衡点。它将一个庞大混乱的问题,分解为一系列小而清晰的子问题。

我给你的3个立即行动建议

  1. 建立目录结构
    在你的下一个新项目中,立即创建app/drivers/hal/middleware/目录,哪怕目前只有一个文件。

  2. 封装第一个HAL函数
    下次要点亮LED时,别直接操作寄存器,先写hal_led_init()hal_led_toggle()

  3. 代码审查时多问一句
    “这行代码应该属于哪个层次?”这个问题会改变你的编码思维。

那个3000行的项目,同事花了一个月时间重构,重构过程很痛苦,优秀的嵌入式工程师,不是写出能跑起来的代码,而是写出能轻松维护、轻松扩展、轻松交接的代码,这话真的没错。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“星球”加入知识星球,有问必答。
点击“阅读原文”查看知识星球详情,欢迎点分享、收藏、点赞、在看。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐