关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约4501字,阅读大约需要 15 分钟
我还记得当年刚踏入嵌入式开发领域的时候,对软件架构完全没有概念。写代码想到哪写到哪,最后拼凑成一个能跑的程序。
但随着项目越来越复杂,代码也越来越臃肿,维护起来简直就是一场噩梦。改动一个小功能,都要提心吊胆,生怕把其他地方搞崩了。
工作大概4年左右,有幸接接手了大牛做的项目维护,感觉他的功底深不可测,能把复杂的系统拆解成一个个清晰的模块,而且封装得也挺复杂,当时看还挺复杂,挺不解,写这么复杂干吊?看起来头疼。
后面自己独立做复杂点的项目,才发现他的架构是真香,因此吸收了很多他的思路和架构。
如果你也想提升自己的代码质量,那么这篇文章就是为你准备的!我会用最通俗易懂的语言,带你了解嵌入式软件开发中常见的几种架构模式,让你摆脱“面向过程编程”的原始状态,掌握“面向架构编程”的高级技巧。
无论你是初入茅庐的新手,还是想进阶提升的老鸟,这篇文章都能帮你打造更清晰、更稳定、更易维护的嵌入式系统。
好了,废话不多说,我们直接进入主题。
1. 轮询(Polling)架构:简单直接,但容易阻塞
轮询架构是最简单、最基础的架构模式,就像一个不知疲倦的“巡逻员”,不停地循环检查各个任务的状态,哪个任务需要执行就执行哪个。
•基本原理:
轮询架构的核心是一个无限循环(while(1)),在循环中按照一定的顺序依次执行各个任务模块。每个任务模块通常是一个函数,执行完成后立即返回,继续执行下一个任务。
•代码示例:
void task1()
{
// 任务1的代码
}
void task2()
{
// 任务2的代码
}
int main()
{
while (1)
{
task1();
task2();
// ... 其他任务
}
return 0;
}
•适用场景:
任务比较简单,实时性要求不高。
系统资源非常有限,无法支持更复杂的架构。
初学者学习和理解嵌入式编程的入门架构。
•优缺点:
优点: 简单易懂,易于实现,代码量少。
缺点: 响应速度慢,所有任务共享CPU时间,容易出现“任务饥饿”现象(某个任务一直得不到执行);如果某个任务执行时间过长,会阻塞其他任务的执行。
•实战避坑
尽量让每个任务的执行时间短,避免长时间占用CPU。
可以调整任务的执行顺序,将优先级较高的任务放在前面执行,以提高响应速度。
轮询架构适合简单的控制逻辑和数据采集,但不适合复杂的实时系统。
2. 中断(Interrupt)架构:快速响应,但需要谨慎管理
中断架构是一种基于事件驱动的简易架构模式,当外部事件发生时,会触发中断,CPU暂停当前任务,立即执行中断服务程序(ISR)。
•基本原理:
中断架构将任务分为“前台任务”和“后台任务”。后台任务在主循环中执行,前台任务(即中断服务程序)在中断发生时执行。
•代码示例:
// 中断服务程序
void EXTI0_IRQHandler()
{
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 处理中断事件
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
int main()
{
// 初始化中断
// ...
while (1)
{
// 后台任务
}
return 0;
}
•适用场景:
需要快速响应外部事件,如按键按下、传感器数据变化等。
系统有严格的实时性要求。
外部设备需要实时监控,例如串口接收数据。
•优缺点:
优点: 响应速度快,能及时处理紧急事件;CPU利用率高,可以在后台执行其他任务。
缺点: 中断服务程序应该尽可能短,避免长时间占用CPU;中断嵌套管理复杂,容易导致中断优先级反转等问题。
•实战避坑:
中断服务程序要“短小精悍”,只做最必要的工作,其他耗时操作交给后台任务处理。
合理设置中断优先级,避免高优先级的中断被低优先级的中断阻塞。
避免在中断服务程序中使用阻塞函数,如delay(),printf()等。
3. 状态机(State Machine)架构:逻辑清晰,但容易“状态爆炸”
状态机架构是一种将系统划分为多个状态,并根据输入事件在不同状态之间进行转换的架构模式。
•基本原理:
状态机将系统抽象成有限个状态,每个状态下有特定的行为和转移条件。通过状态转移图或表来管理状态间的转换。
•代码示例:
typedef enum
{
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR
} State_t;
State_t currentState = STATE_IDLE;
void stateMachine()
{
switch (currentState)
{
case STATE_IDLE:
if (startCondition)
{
currentState = STATE_RUNNING;
}
break;
case STATE_RUNNING:
if (errorCondition)
{
currentState = STATE_ERROR;
}
break;
case STATE_ERROR:
// 处理错误
break;
}
}
int main()
{
while (1)
{
stateMachine();
// ... 其他任务
}
return 0;
}
•适用场景:
系统有明显的“状态”概念,如通信协议、控制系统、用户界面、不同的模式等。
逻辑比较复杂,需要清晰的状态管理。
需要明确的流程控制,例如按键检测,电梯运行等。
•优缺点:
优点: 逻辑清晰,易于维护和扩展;能有效管理复杂的状态转移。
缺点: 状态过多时,代码量会急剧增加,维护成本高;状态转移逻辑错误容易导致“死循环”等问题。
•实战避坑:
在编写代码之前,先画好状态转移图,明确每个状态的行为和转移条件。
尽量减少状态的数量,避免“状态爆炸”。
可以使用状态机框架或工具来简化开发,例如UML状态机图。
4. 实时操作系统(RTOS)架构:多任务并行,但需要更多资源
实时操作系统(RTOS)是一种专门用于嵌入式系统的操作系统,它提供了任务调度、任务间通信、资源管理等功能,让开发者可以像编写PC程序一样编写嵌入式软件。
•基本原理:
RTOS提供任务调度器,可以将系统划分为多个独立的任务(线程),每个任务都有自己的优先级和栈空间。RTOS负责调度这些任务,让它们并行执行。
•代码示例(以FreeRTOS为例):
void task1(void *pvParameters)
{
while (1)
{
// 任务1的代码
vTaskDelay(100 / portTICK_PERIOD_MS); // 延时100ms
}
}
void task2(void *pvParameters)
{
while (1)
{
// 任务2的代码
vTaskDelay(200 / portTICK_PERIOD_MS); // 延时200ms
}
}
int main() {
xTaskCreate(task1, "Task1", 128, NULL, 1, NULL);
xTaskCreate(task2, "Task2", 128, NULL, 2, NULL);
vTaskStartScheduler();
return 0;
}
•适用场景:
系统功能复杂,需要多个任务并行执行。
对实时性和响应速度有较高要求。
需要复杂的任务管理和资源管理。
•优缺点:
优点: 任务管理灵活,易于实现复杂功能;提高系统响应速度;可以有效利用CPU资源。
缺点: 资源消耗较大,不适合资源受限的单片机;学习成本高;需要考虑任务同步和互斥等问题。
•实战避坑:
根据项目和应用场景选择合适的RTOS,如FreeRTOS、uC/OS等。
合理分配任务优先级和栈空间,避免任务冲突和栈溢出。
使用信号量、互斥锁等机制进行任务间通信和资源保护。
避免在中断服务程序中调用RTOS API。
5. 混合架构:灵活应对,但需要精心设计
在实际开发中,单一的架构模式往往难以满足所有需求,因此经常会采用混合架构,将多种架构模式结合起来使用。例如,“轮询 + 中断”、“状态机 + RTOS”等。
这次我们以一个简单的温度监控系统为例,该系统需要:
•实时监控温度传感器: 需要中断来快速响应温度变化。
•根据温度进行状态切换: 需要状态机来管理不同的工作模式(例如:正常、高温报警、低温报警)。
•定期进行数据记录: 需要轮询来定期保存温度数据到存储器。
// 1. 定义状态
typedef enum
{
STATE_NORMAL,
STATE_HIGH_TEMP_ALARM,
STATE_LOW_TEMP_ALARM
} TemperatureState_t;
TemperatureState_t currentTemperatureState = STATE_NORMAL;
// 2. 定义全局变量
volatile float temperature = 25.0; // 当前温度,volatile 确保中断和主循环都能访问
bool newDataAvailable = false; // 新数据标志
// 3. 温度传感器中断服务程序 (假设通过ADC读取)
void ADC_IRQHandler()
{
if (ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET)
{
// 读取ADC值
uint16_t adcValue = ADC_GetConversionValue(ADC1);
// 转换成温度值 (简化计算)
temperature = (float)adcValue * 0.1; // 假设每0.1代表1摄氏度
newDataAvailable = true; // 设置新数据标志
// 清除中断标志位
ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
}
}
// 4. 状态机函数
void temperatureStateMachine()
{
switch (currentTemperatureState)
{
case STATE_NORMAL:
if (temperature > 35.0)
{
currentTemperatureState = STATE_HIGH_TEMP_ALARM;
// 启动风扇
GPIO_SetBits(FAN_PORT, FAN_PIN);
printf("High Temperature Alarm!\n");
}
else if (temperature < 10.0)
{
currentTemperatureState = STATE_LOW_TEMP_ALARM;
// 启动加热器
GPIO_SetBits(HEATER_PORT, HEATER_PIN);
printf("Low Temperature Alarm!\n");
}
break;
case STATE_HIGH_TEMP_ALARM:
if (temperature <= 30.0)
{
currentTemperatureState = STATE_NORMAL;
// 关闭风扇
GPIO_ResetBits(FAN_PORT, FAN_PIN);
printf("Normal Temperature.\n");
}
break;
case STATE_LOW_TEMP_ALARM:
if (temperature >= 15.0)
{
currentTemperatureState = STATE_NORMAL;
// 关闭加热器
GPIO_ResetBits(HEATER_PORT, HEATER_PIN);
printf("Normal Temperature.\n");
}
break;
}
}
// 5. 数据记录任务 (轮询方式)
void dataLoggingTask()
{
static uint32_t lastLogTime = 0;
uint32_t currentTime = HAL_GetTick(); // 获取当前时间 (需要HAL库支持)
if (currentTime - lastLogTime >= 5000) // 每5秒记录一次
{
lastLogTime = currentTime;
// 将温度数据记录到存储器 (这里简化成打印)
printf("Logging: Temperature = %.2f, State = %d\n", temperature, currentTemperatureState);
// 实际应用中,需要写入Flash或者SD卡
}
}
// 6. 主函数
int main()
{
// 初始化 ADC,GPIO,中断
// ... (初始化代码略)
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init();
// 启动ADC转换并使能中断
HAL_ADC_Start_IT(&hadc1);
// 主循环
while (1)
{
// 1. 状态机处理
temperatureStateMachine();
// 2. 数据记录 (轮询)
dataLoggingTask();
// 3. 其他任务 (可以添加更多任务)
// ...
// 4. 降低功耗 (可选)
// HAL_PWR_EnterSleepMode(PWR_LOWPOWERREGULATOR_ON, ADC_IRQn);
}
}
代码说明:
温度传感器中断: 使用ADC读取温度,并在每次读取后设置newDataAvailable标志。volatile关键字确保主循环能及时读取到最新的温度数据。
状态机:temperatureStateMachine() 函数根据温度值,切换系统状态,并控制风扇或加热器的开启/关闭。
数据记录任务:dataLoggingTask() 函数每隔5秒记录一次温度数据和系统状态。
混合架构的优势:
•实时响应: 中断确保系统能及时响应温度变化。
•状态管理: 状态机负责管理系统的工作模式,并根据温度进行切换。
•数据记录: 轮询确保数据能够被定期记录。
这个例子虽然简单,但已经展示了混合架构的基本思想:将不同的架构模式结合起来,充分发挥各自的优势,以满足复杂的系统需求。
•适用场景:
系统既有实时性要求,又有复杂的逻辑。
资源有限,需要在性能和复杂度之间进行平衡。
各个模块的功能特性不同,需要采用不同的架构模式。
•优缺点:
优点: 灵活性高,可以针对具体问题选择最佳解决方案。
缺点: 架构设计难度较大,需要经验丰富的工程师;架构间的接口和通信需要精心设计,避免出现“缝合怪”现象。
我们从事了10年,就喜欢用自己设计的架构,采用了多种混合架构,缝合了不同框架的优势,相对RTOS更精简更节省资源,很多51单片机也能用。
比如我们无际单片机的项目3和项目6,就是把轮询架构加了一层封装,让它们在管理任务时更加灵活方便,以轮询作为主框架,其余有状态机、表驱动之类的架构配合,具体可以看我前面几篇文章。
关于这个轮询架构我也在2018年录了个全面的教程,目前开源,无际粉丝可找我安排。
•实战避坑:
在设计混合架构之前,先分析系统需求,明确各个模块的功能和性能要求。
确定核心架构,例如使用RTOS作为主框架,然后根据需要添加其他架构模块。
定义清晰的接口规范,确保各个模块之间的通信和数据交换顺畅。
6. 选择架构:没有最好的,只有最合适的
选择哪种架构模式,并没有绝对的答案,关键是要根据具体的项目需求和资源情况进行选择。
•如果任务简单,实时性要求不高,资源有限,那么轮询架构是一个不错的选择。
•如果需要快速响应外部事件,或者有严格的实时性要求,那么中断架构是必不可少的。
•如果系统逻辑复杂,有明显的状态概念,那么状态机架构可以帮助你更好地管理代码。
•如果系统功能复杂,需要多个任务并行执行,那么RTOS架构可以提高系统效率和响应速度。
•如果以上几种架构都不能满足你的需求,那么可以考虑采用混合架构,将多种架构模式结合起来使用。
7. 总结
嵌入式软件架构是单片机开发的“灵魂”,它决定了你的代码是“豆腐渣工程”还是“艺术品”。
掌握这些常见的架构模式,你就能更好地组织你的代码,构建更清晰、更稳定、更易维护的嵌入式系统。
希望这篇文章能帮助你摆脱“代码搬运工”的身份,起到抛砖引玉的作用,助你成为一名真正的嵌入式软件架构师!好的架构不仅能提高开发效率,还能让你在开发复杂项目时游刃有余。
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细!