3. 系统架构设计:嵌入式系统的骨架

好,咱们进入第三个环节——系统架构设计。说实话,很多新手工程师一上来就画框图、选芯片,结果做到一半发现架构撑不住需求,推倒重来。我早期就吃过这个亏,所以现在养成了一个习惯:先想清楚架构,再动手。

嵌入式系统的架构,说白了就是你的软件和硬件怎么搭伙过日子。它决定了系统的实时性、可靠性、可扩展性,甚至决定了你后期调试时会不会想骂人。

3.1 嵌入式系统典型架构

我这些年做过的项目,架构基本就三种:前后台系统、RTOS、多核架构。每种都有它的脾气。

3.1.1 前后台系统(裸机)

这是最经典的架构。一个主循环(后台)处理任务,中断(前台)响应紧急事件。代码结构简单,资源开销小。

// 典型的前后台架构伪代码
void main(void) {
    System_Init();
    while(1) {
        Task_ReadSensor();   // 后台轮询
        Task_ProcessData();
        Task_UpdateDisplay();
    }
}

void TIMER_IRQHandler(void) {  // 前台中断
    static uint32_t tick = 0;
    tick++;
    if(tick % 100 == 0) {
        g_Flag_1s = 1;  // 设置标志位
    }
}

我的经验:前后台架构适合逻辑简单、任务少的产品。比如一个温湿度采集器,每秒读一次数据,显示一下,足够了。但如果你有多个实时性要求不同的任务,比如既要读传感器,又要处理按键,还要刷屏幕,前后台就容易出问题——一个任务卡住了,整个系统就僵了。

避坑指南:我曾经在一个智能家居项目里用了前后台架构,结果按键消抖和LED闪烁用了同一个定时器中断,调试了三天才发现优先级冲突。所以,中断里别干太多活,尽量只设标志位。

3.1.2 RTOS(实时操作系统)

当任务多了,前后台就扛不住了。这时候RTOS就派上用场了。它把任务拆成独立的线程,由调度器统一管理。

// FreeRTOS 任务创建示例
void vTask_Sensor(void *pvParameters) {
    while(1) {
        Read_Temperature();
        vTaskDelay(pdMS_TO_TICKS(100));  // 100ms 周期
    }
}

void vTask_Display(void *pvParameters) {
    while(1) {
        Update_LCD();
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

int main(void) {
    xTaskCreate(vTask_Sensor, "Sensor", 128, NULL, 1, NULL);
    xTaskCreate(vTask_Display, "Display", 128, NULL, 2, NULL);
    vTaskStartScheduler();
}

RTOS的好处很明显:每个任务各司其职,互不干扰。你想想看,一个任务在等I2C通信,另一个任务还能继续刷屏幕,用户体验就好多了。

关键点:RTOS不是万能的。任务切换有开销,栈空间要算好。我见过有人给每个任务分配了1KB栈,结果系统内存直接爆了。嗯,这里要注意,RTOS的栈大小要根据任务的实际调用深度来算,别拍脑袋。

3.1.3 多核架构

这几年多核MCU越来越多了,比如双核Cortex-M4+M0。一个核跑实时控制,另一个核跑通信协议栈。说白了,就是让专业的人干专业的事。

// 双核通信示例(Mailbox机制)
// Core 0 (M4) - 控制任务
void Core0_Main(void) {
    while(1) {
        float speed = Read_MotorSpeed();
        // 通过Mailbox发送给Core1
        MAILBOX_Send(MAILBOX_ID_0, &speed, sizeof(speed));
    }
}

// Core 1 (M0) - 通信任务
void Core1_Main(void) {
    while(1) {
        float speed;
        MAILBOX_Receive(MAILBOX_ID_0, &speed, sizeof(speed));
        UART_Send(&speed, sizeof(speed));  // 转发到上位机
    }
}

个人习惯:多核架构最大的坑是核间通信。我建议用Mailbox或者共享内存+信号量的方式,别用全局变量裸奔,否则数据一致性会让你崩溃。

3.2 硬件选型原则

架构定好了,接下来就是选硬件。我见过太多人先选MCU再定需求,结果不是性能不够就是成本超标。正确的姿势是:需求驱动选型。

3.2.1 MCU选型

考量维度 我的建议 避坑点
主频 根据最坏情况下的计算量估算,留30%余量 别只看标称频率,要看实际能跑多快
Flash/RAM 代码大小+20%,RAM按最大堆栈深度算 我曾经选了个64KB Flash的芯片,结果代码编译出来62KB,差点装不下
外设接口 列出所有需要的UART/I2C/SPI,数一下够不够 有些芯片的I2C和SPI是复用的,注意别冲突
功耗 电池供电选Cortex-M0+,工控选M4/M7 休眠电流和唤醒时间要一起看

3.2.2 传感器选型

传感器选型,我总结了三步走:

  1. 精度够用就行:别追求0.1%的精度,如果系统只需要±1℃,那选±0.5℃的传感器就浪费了。
  2. 接口要匹配:模拟输出需要ADC,数字输出要看I2C/SPI时序能不能满足。
  3. 环境适应性:工业场景选工业级温度范围,消费电子选商业级就行。

我曾经踩过的坑:在一个户外项目中选了消费级的温湿度传感器,结果夏天高温直接罢工。后来换了工业级的,贵了3块钱,但再也没出过问题。

3.2.3 通信模块选型

通信方式 适用场景 注意事项
UART 短距离、点对点 波特率别太高,线长有限制
I2C 板内传感器通信 总线电容有限制,设备多了要加缓冲
SPI 高速数据传输(如LCD、SD卡) 注意信号完整性,走线要短
无线(BLE/WiFi) 物联网产品 功耗和传输距离要权衡

3.3 模块划分与接口定义

架构和硬件都定了,接下来就是模块划分。我习惯用「高内聚、低耦合」的原则来切模块。

举个例子,一个智能温控器,我会这样划分:

// 模块接口定义示例
// sensor.h - 传感器模块
#ifndef __SENSOR_H__
#define __SENSOR_H__

typedef struct {
    float temperature;
    float humidity;
} SensorData_t;

// 初始化传感器
void Sensor_Init(void);

// 读取传感器数据(阻塞,等待I2C完成)
SensorData_t Sensor_Read(void);

#endif

// display.h - 显示模块
#ifndef __DISPLAY_H__
#define __DISPLAY_H__

void Display_Init(void);
void Display_ShowTemperature(float temp);
void Display_ShowHumidity(float humi);

#endif

// control.h - 控制逻辑模块
#ifndef __CONTROL_H__
#define __CONTROL_H__

void Control_Init(float targetTemp);
void Control_Run(SensorData_t currentData);
float Control_GetOutput(void);

#endif

模块划分的原则:

  • 每个模块只做一件事(单一职责)
  • 模块之间通过接口通信,别直接访问全局变量
  • 接口参数尽量用结构体,方便扩展

接口定义好了,模块之间就像拼积木一样。你想想看,如果后期要换传感器,只需要改sensor.c,其他模块完全不用动。这就是模块化的好处。

我的习惯:接口定义时,我会加一个版本号宏。比如 #define SENSOR_API_VERSION 0x0100。这样如果接口变了,编译时就能发现不兼容,省得运行时出问题。

好了,系统架构设计这块就聊到这儿。下一章咱们聊聊详细设计,包括状态机、数据结构这些实战内容。记住一句话:架构设计花的时间,会在后期调试阶段加倍还给你。