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 传感器选型
传感器选型,我总结了三步走:
- 精度够用就行:别追求0.1%的精度,如果系统只需要±1℃,那选±0.5℃的传感器就浪费了。
- 接口要匹配:模拟输出需要ADC,数字输出要看I2C/SPI时序能不能满足。
- 环境适应性:工业场景选工业级温度范围,消费电子选商业级就行。
我曾经踩过的坑:在一个户外项目中选了消费级的温湿度传感器,结果夏天高温直接罢工。后来换了工业级的,贵了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。这样如果接口变了,编译时就能发现不兼容,省得运行时出问题。
好了,系统架构设计这块就聊到这儿。下一章咱们聊聊详细设计,包括状态机、数据结构这些实战内容。记住一句话:架构设计花的时间,会在后期调试阶段加倍还给你。