1. 课程导论:为什么需要软件架构?

各位同学好,我是这门课的主讲。在嵌入式这行摸爬滚打了十几年,我见过太多项目从意气风发走向一地鸡毛。今天咱们开篇第一讲,不急着上代码,先聊聊一个根本问题:嵌入式系统到底需不需要软件架构?

我的答案是:不仅需要,而且比纯软件领域更需要。为什么?你想想看,一个跑在 Linux 服务器上的程序崩了,最多重启一下。但一个嵌入式设备——比如心脏起搏器、汽车 ECU、工业控制器——如果软件架构混乱导致崩溃,那可能就是人命关天的大事。

核心观点: 嵌入式软件架构不是“锦上添花”,而是“生死攸关”。它决定了系统的可维护性、可移植性、实时性和可靠性。

1.1 嵌入式系统的特殊性

嵌入式系统不是“小一号的 PC”。它有自己独特的脾气。我刚开始做嵌入式时,把 PC 上的那套开发习惯直接搬过来,结果被现实狠狠教育了一顿。

具体来说,嵌入式系统有三大特殊性:

特性 描述 对架构的影响
资源受限 RAM 可能只有几 KB~几百 KB,Flash 几 MB~几十 MB,CPU 主频几十 MHz~几百 MHz 架构必须轻量,不能引入过多抽象层,避免内存碎片和栈溢出
实时性 任务必须在规定时间内完成,否则系统失效(硬实时 vs 软实时) 架构必须支持优先级调度、中断响应、时间确定性
可靠性 设备可能连续运行数年不重启,环境恶劣(高温、振动、电磁干扰) 架构必须包含看门狗、冗余、错误恢复、状态机等机制

说白了,嵌入式架构师的工作就是在“资源有限”和“要求无限”之间找平衡。我曾经在一个只有 32KB RAM 的项目上做 TCP/IP 协议栈,那感觉就像在针尖上跳舞——每一字节都得精打细算。

1.2 没有架构的后果

有人可能会说:“我写个单片机程序,就几个功能,要什么架构?”嗯,我年轻时也这么想。

结果呢?项目做到一半,需求变了。客户说:“老王,加个 OTA 升级功能吧。”我一看代码,所有功能都揉在一个 main.c 里,全局变量满天飞,中断服务函数里调了延时函数……改一个地方崩三个地方。最后只能重写。

这就是典型的“架构债务”。没有架构的代码,前期跑得飞快,后期寸步难行。

避坑指南: 我曾经接手过一个项目,前任工程师把所有业务逻辑都写在中断里。理由是“这样响应快”。结果系统偶发死锁,查了两个月才找到原因——中断嵌套导致栈溢出。记住:中断里只做最必要的事,架构上必须把业务逻辑和硬件驱动解耦。

1.3 课程目标与学习方法

这门课的目标很明确:让你掌握嵌入式软件架构设计的核心方法,尤其是接口定义和内聚设计。

具体来说,学完这门课,你应该能:

  • 理解分层架构、模块化、接口隔离等原则在嵌入式场景下的落地方式
  • 掌握如何定义清晰、稳定的模块接口(API、回调、消息、共享内存)
  • 学会用状态机、事件驱动、任务调度等模式组织代码
  • 具备评估和重构现有嵌入式软件架构的能力

学习方法上,我建议你:

  1. 动手实践——每章都有代码示例,别只看,要自己敲一遍
  2. 带着问题学——想想你当前项目中的痛点,看课程能不能解决
  3. 多问为什么——架构设计没有标准答案,只有 trade-off
学习建议: 准备一个笔记本,记录每章学到的“架构模式”和“避坑经验”。我自己的笔记本已经记了三大本,每次做新项目都会翻一翻。

1.4 本章知识体系

下面这张图概括了本章的核心脉络,也是整门课的知识地图:

嵌入式软件架构知识体系(第1章) 为什么需要架构? 嵌入式系统特殊性 资源受限 实时性要求 高可靠性需求 没有架构的后果 代码耦合、难以维护 需求变更成本高 调试困难、Bug 频发 课程目标与方法 掌握接口定义方法 学会内聚设计原则 动手实践 + 案例驱动 核心:接口定义 + 内聚设计 = 可维护的嵌入式架构

1.5 一个简单的例子

为了让你直观感受“有架构”和“没架构”的区别,我举个 LED 闪烁的例子。

没有架构的写法:

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

void delay(volatile uint32_t count) {
    while(count--);
}

int main(void) {
    RCC->APB2ENR |= (1 << 4);  // 使能 GPIOC 时钟
    GPIOC->CRH = 0x33333333;    // 配置为推挽输出
    
    while(1) {
        GPIOC->ODR ^= (1 << 13);  // 翻转 PC13
        delay(500000);
    }
}

这段代码能跑,但问题很多:硬件寄存器直接操作、延时用空循环、没有模块划分。如果明天要改成 PWM 控制亮度,或者换一个 MCU,基本得重写。

有架构的写法:

// led.h
#ifndef LED_H
#define LED_H

#include <stdint.h>

void led_init(void);
void led_on(void);
void led_off(void);
void led_toggle(void);

#endif

// led.c
#include "led.h"
#include "stm32f1xx.h"

void led_init(void) {
    RCC->APB2ENR |= (1 << 4);
    GPIOC->CRH = 0x33333333;
}

void led_on(void) {
    GPIOC->ODR &= ~(1 << 13);
}

void led_off(void) {
    GPIOC->ODR |= (1 << 13);
}

void led_toggle(void) {
    GPIOC->ODR ^= (1 << 13);
}

// main.c
#include "led.h"
#include "timer.h"

int main(void) {
    led_init();
    timer_init(500);  // 500ms 定时
    
    while(1) {
        // 主循环做其他事情
    }
}

// timer.c 中定时回调
void timer_callback(void) {
    led_toggle();
}

看到了吗?通过定义清晰的接口(led.h),我们把硬件操作封装起来。上层代码只关心“开灯”“关灯”“翻转”,不关心具体怎么操作寄存器。这就是接口隔离和模块化的雏形。

关键区别: 没有架构的代码是“面向寄存器编程”,有架构的代码是“面向接口编程”。后者让你能轻松应对需求变化、硬件迁移和团队协作。