2. 嵌入式软件架构到底在解决什么?
本质:
前期增加一点设计成本
⬇️
后期降低大量维护成本
🔴 第一道坎:复杂度失控
⭐ 问题本质
很多人觉得:
-
单片机代码就几千行,应该不会太复杂
实际上:
⚠️ 嵌入式复杂度取决于外设和接口数量,而不是代码量。
一个设备可能同时包含:
-
UART
-
SPI
-
I2C
-
CAN
-
Flash
-
OTA
-
按键
-
LED
-
传感器
每增加一个模块:
📈 复杂度 +1
模块之间开始交互后:
📈📈 复杂度指数增长
✅ 架构解决方案
通过:
-
模块化
-
分层设计
-
接口隔离
实现:
✅ 独立理解
✅ 独立修改
✅ 独立测试
❌ 不做架构
所有代码堆一起:
while(1)
{
wifi_connect();
temp = ds18b20_read();
oled_show(temp);
oled_show_time();
key_scan();
if(key_pressed)
{
wifi_reconnect();
}
}
如果现在需求变了:
DS18B20 ↓ 换成DHT11
你可能要改:
main.c oled.c wifi.c config.c
到处找。
✅ 模块化
把功能拆开:
wifi.c oled.c sensor.c key.c
主函数:
while(1)
{
Sensor_Task();
Display_Task();
Wifi_Task();
Key_Task();
}
此时:
独立理解
想看温度采集:
直接看sensor.c
不用管OLED。
独立修改
DS18B20换DHT11:
只改:
sensor.c sensor.h
其它模块不用动。
独立测试
直接测试:
Sensor_Task();
看温度是否正确。
✅ 分层设计
进一步升级:
Application ↓ Driver ↓ Hardware
例如:
天气显示 ↓ OLED驱动 ↓ SPI ↓ STM32
业务层:
DisplayTemp(25);
根本不知道:
SPI怎么发 GPIO怎么控制
如果以后:
OLED ↓ TFT屏
只改驱动层:
oled.c
业务层不动。
✅ 接口隔离
例如:
先定义接口:
float Sensor_ReadTemp(void);
业务层:
temp = Sensor_ReadTemp();
DS18B20实现:
float Sensor_ReadTemp(void)
{
return DS18B20_Read();
}
DHT11实现:
float Sensor_ReadTemp(void)
{
return DHT11_Read();
}
业务层代码:
temp = Sensor_ReadTemp();
一行都不用改。
🔵 第二道坎:硬件变化
⭐ 问题本质
嵌入式开发中:
-
MCU缺货
-
芯片升级
-
供应商替换
-
产品迭代
都是常态。
✅ 架构解决方案 HAL硬件抽象层
核心思想:
🟢 业务代码不碰寄存器,只调用接口。
例如:
uart_send(data);
而不是:
USART1->DR = data;
⭐分层骨架
app ↓ service ↓ driver ↓ HAL接口层 ↓ 芯片寄存器
更换芯片:
✅ 只修改HAL
✅ 业务代码保持不变
抽象不是免费的
代价:
-
ROM增加
-
RAM增加
-
调用开销增加
高速路径:
-
ADC采样
-
PWM控制
-
电机控制
允许适度访问底层。
举个最简单的例子
直接操作寄存器:
USART1->DR = data;
CPU执行:
写寄存器 ↓ 结束
可能就几条汇编指令。
加HAL
uart_send(data);
内部:
void uart_send(uint8_t data)
{
USART1->DR = data;
}
CPU执行:
调用函数 ↓ 保存现场 ↓ 压栈 ↓ 跳转 ↓ 执行代码 ↓ 返回
比直接写:
USART1->DR = data;
多了额外开销。
为什么ROM增加
以前:
USART1->DR = data;
1行代码。
现在:
uart_send() uart_recv() uart_init() uart_deinit()
多了很多函数。
这些函数最终都会编译进:
Flash(ROM)
为什么调用开销增加
假设电机控制:
控制周期:50us
如果你每次:
Motor_Control() (app)
↓
Driver_PWM_Set() (driver)
↓
HAL_PWM_Set() (hal)
↓
Register_Write() (寄存器)
经过:
3层调用
每层都压栈、出栈。
累计:
几微秒
都有可能。
🎯 本质
🟢 HAL解决的是可移植性问题。
🟡 第三道坎:实时性与可维护性的矛盾
⭐ 问题本质
嵌入式必须满足实时性:
-
电机控制
-
通信协议
-
传感器采样
-
按键扫描
都有严格时间要求。
✅ 架构解决方案
找到平衡点:
实时性 ↔ 可维护性
三种主流架构

① 前后台架构
结构:
中断 ↓ 主循环
特点:
-
简单
-
资源占用小
适用:
-
家电
-
玩具
-
仪表
② 时间触发架构(软件定时器+工作队列)
结构:
1ms任务 10ms任务 100ms任务
特点:
✅ 时序可预测
适用:
-
汽车电子
-
医疗设备
③ RTOS架构
优点:
✅ 灵活
缺点:
-
死锁
-
优先级反转
-
栈溢出
适用:
-
中大型项目
🟣 第四道坎:状态爆炸
⭐ 问题本质
例如充电器:
-
待机
-
检测
-
慢充
-
快充
-
满电
-
过温
-
过压
-
故障恢复
状态越来越多。
❌ 不做状态管理
假设有个电池管理系统(BMS),一开始状态很简单:
STATE_IDLE 待机 STATE_CHARGE 充电 STATE_FULL 满电
代码:
if(state == STATE_IDLE)
{
if(charger_insert)
{
state = STATE_CHARGE;
}
}
if(state == STATE_CHARGE)
{
Charge_Run();
if(battery_full)
{
state = STATE_FULL;
}
}
if(state == STATE_FULL)
{
Charge_Stop();
}
完全没问题。
第一次需求变更
老板说:
增加温度保护
温度超过60℃:
停止充电 进入保护状态
你加:
if(state == STATE_CHARGE)
{
if(temp > 60)
{
state = STATE_PROTECT;
}
}
以及:
if(state == STATE_PROTECT)
{
Charge_Stop();
}
还能接受。
第二次需求变更
增加:
过压保护
于是:
if(state == STATE_CHARGE)
{
if(temp > 60)
{
state = STATE_PROTECT;
}
if(voltage > 4.25)
{
state = STATE_PROTECT;
}
}
第三次需求变更
增加:
过流保护
继续:
if(state == STATE_CHARGE)
{
if(temp > 60)
state = STATE_PROTECT;
if(voltage > 4.25)
state = STATE_PROTECT;
if(current > 20)
state = STATE_PROTECT;
}
第四次需求变更
增加:
故障恢复
要求:
温度恢复正常 ↓ 自动恢复充电
于是:
if(state == STATE_PROTECT)
{
if(temp < 50)
{
state = STATE_CHARGE;
}
}
第五次需求变更(增加状态)
增加:
涓流充电
老板说:
电压太低 先涓流 再快充
状态变成:
IDLE ↓ TRICKLE ↓ FAST_CHARGE ↓ FULL
这时候灾难来了。
原来:
if(state == STATE_CHARGE)
出现了几十处。
你得思考:
涓流充电算不算充电?
这里:
if(state == STATE_CHARGE)
{
Led_Blink();
}
要不要改?
这里:
if(state == STATE_CHARGE)
{
Save_Charge_Time();
}
要不要改?
这里:
if(state == STATE_CHARGE)
{
Can_Send_Status();
}
要不要改?
这里:
if(state == STATE_CHARGE)
{
Fan_Control();
}
要不要改?
你必须全工程搜索:
STATE_CHARGE
可能搜出来:
87处
如果漏掉一个:
比如:
if(state == STATE_CHARGE)
{
Can_Send_Status();
}
没改。
结果:
进入涓流充电 ↓ CAN状态不上报 ↓ 上位机显示异常
Bug来了。
为什么会全崩
因为:
状态逻辑
散落在:
charge.c can.c led.c fan.c alarm.c storage.c
每个文件都有:
if(state == xxx)
新增一个状态:
TRICKLE
意味着:
所有文件 所有if 全部检查
这就是:
If-Else地狱
✅ 架构解决方案
FSM有限状态机
状态统一管理:
switch(state)
{
case IDLE:
...
break;
case TRICKLE:
...
break;
case FAST_CHARGE:
...
break;
case FULL:
...
break;
}
状态转移统一写:
IDLE ↓ 插入充电器 TRICKLE TRICKLE ↓ 电压达到3.0V FAST_CHARGE FAST_CHARGE ↓ 满电 FULL
以后新增:
PRE_CHARGE
只需要:
增加一个状态 增加几条转移规则
而不是:
全工程搜 STATE_CHARGE 87处逐个分析
常见小架构
事件驱动+消息队列
按键模块 ↓ 发送事件 ↓ 业务模块处理
实现解耦。
Ring Buffer
⭐ 嵌入式里的问题
例如串口接收:
MCU:
串口每1ms收到1字节
但是主循环:
10ms才处理一次数据
如果直接处理:
void USART_IRQHandler(void)
{
data = USART1->DR;
}
那么:
1ms 来一个字节
而你:
10ms 才读一次
结果:
前面9个字节丢失
⭐ 解决办法:环形缓冲区
中断负责:
收到数据 ↓ 放入缓冲区
主循环负责:
从缓冲区取数据 ↓ 慢慢处理
结构:
UART ↓ 中断 ↓ RingBuffer ↓ 主循环
这样:
接收快 处理慢 也不会丢数据
参数中心化
先看最原始的写法:
#define TEMP_LIMIT 80 #define SPEED_LIMIT 3000 #define CURRENT_LIMIT 20
代码里到处用:
if(temp > TEMP_LIMIT) if(speed > SPEED_LIMIT) if(current > CURRENT_LIMIT)
项目小的时候没问题。
问题1:参数到处都是
比如客户突然说:
温度保护从80℃改成75℃
你就得:
全项目搜索 TEMP_LIMIT
或者更离谱:
if(temp > 80)
这种魔数直接写死。
最后:
改一个参数 找半天代码
问题2:不同客户需求不同
客户A:
过温保护 80℃
客户B:
过温保护 90℃
客户C:
过温保护 70℃
如果全靠:
#define TEMP_LIMIT 80
那意味着:
每个客户一套代码
维护直接爆炸。
单独搞一个参数模块:
typedef struct
{
uint16_t temp_limit;
uint16_t speed_limit;
uint16_t current_limit;
}sys_param_t;
sys_param_t g_param;
初始化:
g_param.temp_limit = 80; g_param.speed_limit = 3000; g_param.current_limit = 20;
业务代码:
if(temp > g_param.temp_limit)
{
...
}
🎯 本质
🟢 FSM解决的是状态复杂度问题。
🔥 最终总结
嵌入式软件架构本质解决四件事:
| 问题 | 解决方案 |
|---|---|
| 🔴 复杂度失控 | 模块化+分层+接口隔离 |
| 🔵 硬件变化 | HAL抽象层 |
| 🟡 实时性矛盾 | 前后台 / 时间触发 / RTOS |
| 🟣 状态爆炸 | FSM状态机 |
常见小架构:事件驱动+消息队列,RingBuffer,参数中心化
更多推荐

所有评论(0)