1、架构思维启蒙:为什么需要分层?从面包板到工业级代码的进化之路。

大家好,我是你们的嵌入式架构课讲师。

今天咱们聊点实在的。很多刚入行的朋友问我:“为什么我的代码跑着跑着就崩了?”或者“明明功能都实现了,老板却说我代码写得像坨屎?”

嗯,这背后其实就一个问题——你还没建立起架构思维

说白了,写MCU代码和盖房子是一个道理。你见过谁家盖楼直接往地上堆砖头的?没有吧。但很多嵌入式工程师,就是这么干的。

1.1 面包板时代的“野蛮生长”

我记得刚入行那会儿,带我的老工程师扔给我一块STM32开发板,说:“把LED点亮,然后做个呼吸灯效果。”

我当时怎么写的?

void main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    while(1)
    {
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
        HAL_Delay(500);
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
        HAL_Delay(500);
    }
}

你看,所有代码都堆在main函数里。功能简单的时候,这没问题。但项目一复杂,就完蛋了。

我在一个项目中遇到过这种情况:一个同事写了3000行的main函数,里面塞了按键扫描、LCD显示、传感器读取、电机控制……

后来要加个新功能,他找了半天不知道从哪里下手。改了一行代码,结果LED不亮了。为什么?因为全局变量被意外修改了。

⚠️ 我曾经踩过的坑: 所有代码堆在main函数里,看似“高效”,实则是在给自己埋雷。项目规模超过500行,这种写法基本就是灾难。

1.2 为什么需要分层?

你想想看,一个复杂的嵌入式系统,通常包含哪些东西?

  • 硬件层:GPIO、定时器、UART、SPI、I2C……
  • 驱动层:操作这些外设的代码
  • 中间件层:RTOS、文件系统、协议栈
  • 应用层:业务逻辑、用户交互

如果不分层,这些代码会混在一起。就像把衣服、袜子、鞋子全塞进一个洗衣机里洗——最后拿出来,什么颜色都串了。

分层的核心目的就三个:

  1. 解耦:硬件变了,应用代码不用改。我换了个MCU,驱动层重写,上层逻辑直接复用。
  2. 可读性:别人看你的代码,一眼就知道哪部分是干什么的。
  3. 可维护性:修bug、加功能,只改对应层,不会牵一发动全身。
💡 我的个人习惯: 拿到一个新项目,先花30分钟画个分层图。哪层做什么,接口怎么定义,想清楚了再写代码。这30分钟,能省下后面3天的调试时间。

1.3 从“面包板”到“工业级”的进化

咱们来看看,一个典型的工业级嵌入式软件,是怎么分层的。

层级 职责 典型文件
应用层 业务逻辑、状态机、用户交互 app_main.c, app_task.c
中间件层 RTOS、文件系统、通信协议 FreeRTOS, lwIP, FatFS
硬件抽象层(HAL) 封装MCU外设操作,提供统一接口 hal_gpio.c, hal_uart.c
驱动层 直接操作寄存器,最底层代码 stm32f4xx_gpio.c

你看,每一层都有明确的边界。上层调用下层,下层为上层服务。但注意——上层不能越级调用下层

举个例子:应用层想读一个传感器数据。正确的做法是:

  1. 应用层调用中间件层的传感器驱动接口
  2. 传感器驱动调用HAL层的I2C接口
  3. HAL层调用驱动层的寄存器操作

如果应用层直接去操作I2C寄存器,那分层就形同虚设了。

🔧 避坑指南: 我曾经在一个项目中,看到有人为了“省事”,在应用层直接调用了HAL_Delay()。后来要移植到另一个MCU上,发现HAL_Delay的实现完全不同,所有延时逻辑都得重写。这就是越级调用的代价。

1.4 一个简单的分层示例

咱们用LED闪烁这个例子,看看分层怎么写。

驱动层(driver_led.c):

#include "stm32f4xx.h"

void LED_Init(void)
{
    RCC->AHB1ENR |= (1 << 3);  // 使能GPIOD时钟
    GPIOD->MODER |= (1 << 24); // 设置PD12为输出
}

void LED_On(void)
{
    GPIOD->BSRR = (1 << 12);
}

void LED_Off(void)
{
    GPIOD->BSRR = (1 << 28);
}

硬件抽象层(hal_led.c):

#include "hal_led.h"
#include "driver_led.h"

void HAL_LED_Init(void)
{
    LED_Init();
}

void HAL_LED_SetState(uint8_t state)
{
    if(state)
        LED_On();
    else
        LED_Off();
}

应用层(app_main.c):

#include "hal_led.h"

void App_Task(void)
{
    HAL_LED_Init();

    while(1)
    {
        HAL_LED_SetState(1);
        DelayMs(500);
        HAL_LED_SetState(0);
        DelayMs(500);
    }
}

你看,应用层根本不知道底层用的是哪个MCU,也不知道LED接在哪个GPIO上。它只知道调用HAL_LED_SetState()

如果哪天要换MCU,只需要重写驱动层和HAL层,应用层代码一行都不用改。

🎯 核心思想: 分层不是为了让代码变复杂,而是为了让复杂的事情变简单。每一层只关心自己的事,不越界、不越级。

1.5 什么时候可以不分层?

说实话,也不是所有项目都需要严格分层。

  • 一次性原型验证:功能验证完就扔了,不分层也没关系
  • 代码量小于500行:一个人维护,逻辑简单,不分层也能跑
  • 硬件永远不会变:比如一个固定型号的遥控器,量产10年不改

但话说回来,养成好习惯比什么都重要。我见过太多人,一开始图省事不分层,结果项目越做越大,最后代码根本没法维护,只能推倒重来。

嗯,这一章咱们先聊到这儿。下一章,我会带大家手把手搭建一个真正的分层架构,从文件结构到接口定义,一步步来。

记住:架构思维,是区分“码农”和“工程师”的分水岭


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