做开发1-3年的时候,一些简单的项目,功能基本都能实现,项目复杂度一上来,我的代码就变得乱七八糟了,改一行就崩一片,修个小bug像是拆弹,剪错一根线,整个程序原地爆炸。
后来接触了不少项目,我发现一个规律:那些代码写得牛的人,都有个共同点,很多功能明明很简单,代码却写得弯弯绕绕的,这不是一个全局变量就能搞定吗?为啥搞这么麻烦?
后面独立开发比较复杂项目时,才搞懂他们的精髓所在,像搭积木一样,整齐有序,扩展性和移植性又好,这背后靠的是软件架构。
我开始钻研这个东西,慢慢从"救命,代码炸了"的小白,变成了能淡定应对复杂项目的"老司机。
这些都是架构的功劳,没有架构,代码就是一盘散沙;有了架构,开发效率也能蹭蹭往上走,换一个项目大多数代码Ctrl C+V,然后稍微改改就搞定了。
好了,废话少说,进入正题。
我要介绍的三种嵌入式软件架构,分别是:分层架构、状态机架构、事件驱动架构。
每一种我都会掰开揉碎讲清楚,从原理、优点、到适用场景,还会带点例子,保你一看就懂,拿回去就能在项目里试试水。
一、分层架构
分层架构就像给代码分了"楼层":底层管硬件,中层管逻辑,上层管应用。
每层只跟旁边的层打交道,互不越界。
1.分层架构的优点
1.1 清晰到飞起:每层干啥一目了然,改代码不用满世界找。
1.2 扩展性强:加个新功能?直接在上层插一块就行,下层不动。
1.3 复用性高:底层写好了,别的项目也能拿来用。
2.适用场景
适合那种功能多、跟硬件耦合深的复杂项目,比如我们无际单片机项目6这种物联网网关、工业控制器等等。
3.实战举例
还是拿我们无际单片机的项目6(WiFi+4G+Lora防盗报警主机)为例:
我们一共把程序分为3层:硬件层、中间层、应用层。
硬件层(HAL):单片机驱动外围器件的程序,比如控制LED、Flash、液晶屏、温湿度、电池电量检测、语言输出、触摸等等。
中间层:主要是把采集到的信号转换成具体的值,比如把ADC转换成电量,还有就是一些协议数据的解析,比如lora、4G、WiFi。
应用层:具体的产品业务逻辑,比如菜单、探测器配对、防盗报警逻辑等等。
这样分层的好处是啥?假如换了传感器,你只要改HAL层,其他层照用不误,省时省力。
二、状态机架构
状态机就是把系统分成几个状态,比如待机、运行、停止,然后定义好每个状态下能干啥、收到啥信号会跳到哪。就像玩游戏的流程图,走哪步都清清楚楚。
这个架构问的人最多,我来重点讲解下。
1.状态机架构优点
1.1 逻辑不乱:再复杂的流程也能理顺,画张图就明白。
1.2 调试省心:状态一目了然,问题出在哪秒定位。
1.3 稳定如山:不会因为漏了个条件就崩。
2.适用场景
适合有状态切换的系统,比如洗衣机控制、协议解析、按键处理。
3.实战举例
还是拿无际单片机的项目报警主机举例,展示如何使用状态机实现四种防盗报警模式:离家布防、在家布防、撤防和报警中的示例。
我将使用C语言实现一个简单的状态机,说明这四种模式如何定义、切换,并通过代码展示其工作原理。
1.1 防盗报警模式的状态机设计
状态机由状态、事件和状态转移规则组成,以下是设计的具体步骤:
(1) 定义四种状态
我们首先定义系统的四种状态:
撤防(Disarmed):系统未激活,不检测入侵。
离家布防(Arm Away):家中无人,所有区域的入侵都会触发报警。
在家布防(Arm Home):家中有人,仅特定区域的入侵触发报警。
报警中(Alarming):检测到入侵,系统发出警报。
用C语言的枚举类型定义如下:
typedef enum
{
STATE_DISARMED, // 撤防
STATE_ARM_AWAY, // 离家布防
STATE_ARM_HOME, // 在家布防
STATE_ALARMING // 报警中
} SecurityState;
(2) 定义触发事件
状态的切换由事件驱动。以下是常见的事件:
启动离家布防(EVENT_ARM_AWAY)
启动在家布防(EVENT_ARM_HOME)
撤防(EVENT_DISARM)
检测到入侵(EVENT_INTRUSION)
报警超时(EVENT_TIMEOUT)
用C语言定义如下:
typedef enum {
EVENT_ARM_AWAY, // 启动离家布防
EVENT_ARM_HOME, // 启动在家布防
EVENT_DISARM, // 撤防
EVENT_INTRUSION, // 检测到入侵
EVENT_TIMEOUT // 报警超时
} SecurityEvent;
(3) 定义系统结构
为了记录当前状态和相关信息(如报警计时器),我们定义一个结构体:
typedef struct {
SecurityState currentState; // 当前状态
int alarmTimer; // 报警计时器(秒)
} SecuritySystem;
2.2 状态处理逻辑
每个状态有对应的处理函数,根据接收到的事件决定是否切换状态或执行特定操作。
(1) 撤防状态
行为:系统未激活,不响应入侵。
可切换到:离家布防、在家布防。
void stateDisarmed(SecuritySystem* system, SecurityEvent event) {
switch (event) {
case EVENT_ARM_AWAY:
system->currentState = STATE_ARM_AWAY;
printf("切换到离家布防\n");
break;
case EVENT_ARM_HOME:
system->currentState = STATE_ARM_HOME;
printf("切换到在家布防\n");
break;
default:
printf("系统处于撤防状态\n");
break;
}
}
(2) 离家布防状态
行为:检测所有区域的入侵,若触发则进入报警状态。
可切换到:撤防、报警中。
void stateArmAway(SecuritySystem* system, SecurityEvent event) {
switch (event) {
case EVENT_DISARM:
system->currentState = STATE_DISARMED;
printf("切换到撤防\n");
break;
case EVENT_INTRUSION:
system->currentState = STATE_ALARMING;
system->alarmTimer = 30; // 报警持续30秒
printf("检测到入侵!切换到报警中\n");
break;
default:
printf("系统处于离家布防状态\n");
break;
}
}
(3) 在家布防状态
行为:仅特定区域(如门窗)触发入侵报警,其他区域(如客厅)忽略。
可切换到:撤防。
void stateArmAway(SecuritySystem* system, SecurityEvent event) {
switch (event) {
case EVENT_DISARM:
system->currentState = STATE_DISARMED;
printf("切换到撤防\n");
break;
case EVENT_INTRUSION:
system->currentState = STATE_ALARMING;
system->alarmTimer = 30; // 报警持续30秒
printf("检测到入侵!切换到报警中\n");
break;
default:
printf("系统处于离家布防状态\n");
break;
}
}
(4) 报警中状态
行为:发出警报,等待超时或手动撤防。
可切换到:撤防。
void stateAlarming(SecuritySystem* system, SecurityEvent event) {
if (event == EVENT_TIMEOUT) {
system->currentState = STATE_DISARMED;
printf("报警超时,切换到撤防\n");
} else if (event == EVENT_DISARM) {
system->currentState = STATE_DISARMED;
printf("报警中手动撤防\n");
} else {
printf("系统处于报警中\n");
}
}
3.状态机运行函数
以下函数根据当前状态调用对应的状态处理函数:
void runSecurityStateMachine(SecuritySystem* system, SecurityEvent event) {
switch (system->currentState) {
case STATE_DISARMED:
stateDisarmed(system, event);
break;
case STATE_ARM_AWAY:
stateArmAway(system, event);
break;
case STATE_ARM_HOME:
stateArmHome(system, event);
break;
case STATE_ALARMING:
stateAlarming(system, event);
break;
}
}
4.示例代码与运行
以下是完整的C语言代码,模拟状态切换:
#include <stdio.h>
int main() {
SecuritySystem system = {STATE_DISARMED, 0}; // 初始为撤防状态
// 模拟事件序列
SecurityEvent events[] = {
EVENT_ARM_AWAY, // 启动离家布防
EVENT_INTRUSION, // 检测到入侵
EVENT_TIMEOUT, // 报警超时
EVENT_ARM_HOME, // 启动在家布防
EVENT_DISARM // 撤防
};
for (int i = 0; i < sizeof(events)/sizeof(events[0]); ++i) {
printf("事件: %d\n", events[i]);
runSecurityStateMachine(&system, events[i]);
}
return 0;
}
输出示例:
事件: 0
切换到离家布防
事件: 1
检测到入侵!切换到报警中
事件: 2
报警超时,切换到撤防
事件: 3
切换到在家布防
事件: 4
切换到撤防
5.总结
这个防盗报警系统的状态机包含四种模式:
离家布防:全面监控,入侵即报警。
在家布防:部分监控,灵活应对。
撤防:关闭监控,无报警。
报警中:触发警报,自动或手动结束。
通过状态机设计,系统逻辑清晰、易于扩展,非常适合管理防盗报警模式的复杂状态切换。
三、事件驱动架构
事件驱动就是有事做事,没事睡觉。系统等着事件(按键、传感器、定时器等)触发,一旦有动静,立马跑去处理,像个反应超快的客服。
在嵌入式系统中,这种架构通常通过事件循环、回调函数或消息队列实现。在单片机开发中,中断机制是最常见的事件驱动实现方式。
1.事件驱动架构的优点
快如闪电:实时性强,绝不拖泥带水。
省电省资源:没事时可以睡大觉,功耗低到感人,如果研究过TI蓝牙协议栈就知道,他们用的就是事件驱动架构。
模块化强:每个事件独立,互不干扰。
2.适用场景
适合实时性要求高、事件多的系统,比如RTOS应用、智能家居、GUI界面。
3.实战举例
按键按下:中断触发,灯开关。
定时器到点:检查光线传感器,自动调亮度。
串口收到命令:执行远程控制。
下面以代码示例模型,来直观感受下以事件驱动架构的实现。
设计思路
事件循环:主程序通过一个无限循环持续检查并处理事件队列中的事件。
回调函数:为每种事件类型定义一个处理函数,当事件发生时调用对应的函数。
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
// 定义事件类型
typedef enum {
EVENT_KEY_PRESS, // 按键按下
EVENT_TIMER_TIMEOUT, // 定时器到点
EVENT_SERIAL_COMMAND, // 串口命令
EVENT_MAX // 事件类型总数
} EventType;
// 事件结构体,包含类型和回调函数指针
typedef struct {
EventType type;
void (*callback)(void); // 指向回调函数
} Event;
// 事件队列(环形缓冲区)
#define EVENT_QUEUE_SIZE 10
Event eventQueue[EVENT_QUEUE_SIZE];
uint8_t eventQueueHead = 0; // 队列头
uint8_t eventQueueTail = 0; // 队列尾
// 全局回调函数表
static Event callbacks[EVENT_MAX];
// 注册回调函数
void registerCallback(EventType type, void (*callback)(void)) {
callbacks[type].type = type;
callbacks[type].callback = callback;
}
// 事件入队
void enqueueEvent(EventType type) {
// 检查队列是否已满
if ((eventQueueHead + 1) % EVENT_QUEUE_SIZE == eventQueueTail) {
printf("Event queue full\n");
return;
}
eventQueue[eventQueueHead].type = type;
eventQueue[eventQueueHead].callback = callbacks[type].callback;
eventQueueHead = (eventQueueHead + 1) % EVENT_QUEUE_SIZE;
}
// 处理事件队列
void processEvents(void) {
while (eventQueueTail != eventQueueHead) {
Event event = eventQueue[eventQueueTail];
if (event.callback != NULL) {
event.callback(); // 调用回调函数
}
eventQueueTail = (eventQueueTail + 1) % EVENT_QUEUE_SIZE;
}
}
// 示例回调函数
void keyPressHandler(void) {
printf("Key pressed: toggle light\n");
}
void timerTimeoutHandler(void) {
printf("Timer timeout: adjust brightness\n");
}
void serialCommandHandler(void) {
printf("Serial command received: remote control\n");
}
// 中断服务函数(模拟硬件触发)
void EXTI0_IRQHandler(void) {
// 按键中断
enqueueEvent(EVENT_KEY_PRESS);
}
void TIM2_IRQHandler(void) {
// 定时器中断
enqueueEvent(EVENT_TIMER_TIMEOUT);
}
void USART1_IRQHandler(void) {
// 串口中断
enqueueEvent(EVENT_SERIAL_COMMAND);
}
// 主函数
int main(void) {
// 注册回调函数
registerCallback(EVENT_KEY_PRESS, keyPressHandler);
registerCallback(EVENT_TIMER_TIMEOUT, timerTimeoutHandler);
registerCallback(EVENT_SERIAL_COMMAND, serialCommandHandler);
// 模拟事件触发
EXTI0_IRQHandler(); // 模拟按键按下
TIM2_IRQHandler(); // 模拟定时器到点
USART1_IRQHandler(); // 模拟串口命令
// 事件循环
while (1) {
processEvents(); // 处理事件队列
// 可在此添加其他任务
}
return 0;
}
事件类型和结构体:使用枚举EventType定义事件类型,结构体Event包含事件类型和回调函数指针。
事件队列:使用环形缓冲区实现队列,eventQueue存储待处理事件,enqueueEvent将事件加入队列,processEvents从队列中取出并处理事件。
回调函数:通过registerCallback为每种事件类型绑定一个处理函数(如keyPressHandler),事件发生时,调用对应的回调函数执行具体逻辑。
事件循环:main函数中的while (1)循环不断调用processEvents,确保事件得到及时处理。
中断服务:中断函数(如EXTI0_IRQHandler)模拟硬件触发,将事件加入队列。
代码运行输出:
Key pressed: toggle light
Timer timeout: adjust brightness
Serial command received: remote control
四、架构怎么选?
简单项目:事件驱动就够了,轻快省事。
复杂系统:分层架构稳如泰山。
状态多变:状态机一招搞定。
实时性强:事件驱动冲冲冲。
其实细心又有经验的朋友应该发现了,这几个架构并不是独立的,组合使用才是无敌的存在。
比如我们无际单片机的项目6,就是分层架构+状态机+事件驱动架构的混合。
3个架构组合去做一个复杂的项目,几乎能满足稳定性,扩展性,移植性,和低功耗的要求,符合成年人全要的原则。
总结一下:软件架构是嵌入式开发的灵魂,这篇文章从我的经历出发,聊了三种嵌入式软件架构的痛点和价值,希望你看完能有所启发,在单片机开发的路上越走越顺,越写越牛!
最近很多粉丝问我单片机怎么学,我根据自己从业十年经验,累积耗时一个月,精心整理一份「单
片机最佳学习路径+单片机入门到高级教程+工具包」,全部无偿分享给铁粉!!!
除此以外,再含泪分享我压箱底的22个热门开源项目,包含源码+原理图+PCB+说明文档,让你迅速进阶成高手!
教程资料包和详细的学习路径可以看我下面这篇文章的开头。