电子产业一站式赋能平台

PCB联盟网

搜索
查看: 30|回复: 0
收起左侧

这几个嵌入式软件架构,能胜任90%的项目了。

[复制链接]

302

主题

302

帖子

2497

积分

三级会员

Rank: 3Rank: 3

积分
2497
发表于 4 天前 | 显示全部楼层 |阅读模式
关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞

文 | 无际(微信: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为例):
  • #include "FreeRTOS.h"#include "task.h"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!
    ");            }            else if (temperature 10.0)            {                currentTemperatureState = STATE_LOW_TEMP_ALARM;                // 启动加热器                GPIO_SetBits(HEATER_PORT, HEATER_PIN);                printf("Low Temperature Alarm!
    ");            }            break;        case STATE_HIGH_TEMP_ALARM:            if (temperature 30.0)            {                currentTemperatureState = STATE_NORMAL;                // 关闭风扇                GPIO_ResetBits(FAN_PORT, FAN_PIN);                printf("Normal Temperature.
    ");            }            break;        case STATE_LOW_TEMP_ALARM:            if (temperature >= 15.0)            {                currentTemperatureState = STATE_NORMAL;                // 关闭加热器                GPIO_ResetBits(HEATER_PORT, HEATER_PIN);                printf("Normal Temperature.
    ");            }            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
    ", 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,就是把轮询架构加了一层封装,让它们在管理任务时更加灵活方便,以轮询作为主框架,其余有状态机、表驱动之类的架构配合,具体可以看我前面几篇文章。

    0tpkoozwvja64029034935.png

    0tpkoozwvja64029034935.png

             
    关于这个轮询架构我也在2018年录了个全面的教程,目前开源,无际粉丝可找我安排。

    mwixgdxh52r64029035035.png

    mwixgdxh52r64029035035.png

             
    ?实战避坑:
    在设计混合架构之前,先分析系统需求,明确各个模块的功能和性能要求。
    确定核心架构,例如使用RTOS作为主框架,然后根据需要添加其他架构模块。
    定义清晰的接口规范,确保各个模块之间的通信和数据交换顺畅。
             
    6. 选择架构:没有最好的,只有最合适的
             
    选择哪种架构模式,并没有绝对的答案,关键是要根据具体的项目需求和资源情况进行选择。
             
    ?如果任务简单,实时性要求不高,资源有限,那么轮询架构是一个不错的选择。
    ?如果需要快速响应外部事件,或者有严格的实时性要求,那么中断架构是必不可少的。
    ?如果系统逻辑复杂,有明显的状态概念,那么状态机架构可以帮助你更好地管理代码。
    ?如果系统功能复杂,需要多个任务并行执行,那么RTOS架构可以提高系统效率和响应速度。
    ?如果以上几种架构都不能满足你的需求,那么可以考虑采用混合架构,将多种架构模式结合起来使用。
             
    7. 总结
    嵌入式软件架构是单片机开发的“灵魂”,它决定了你的代码是“豆腐渣工程”还是“艺术品”。
             
    掌握这些常见的架构模式,你就能更好地组织你的代码,构建更清晰、更稳定、更易维护的嵌入式系统。
             
    希望这篇文章能帮助你摆脱“代码搬运工”的身份,起到抛砖引玉的作用,助你成为一名真正的嵌入式软件架构师!好的架构不仅能提高开发效率,还能让你在开发复杂项目时游刃有余。

    end

    xgkonqdxcya64029035135.jpg

    xgkonqdxcya64029035135.jpg


    下面是更多无际原创的个人成长经历、行业经验、技术干货。
    1.电子工程师是怎样的成长之路?10年5000字总结
    2.如何快速看懂别人的代码和思维
    3.单片机开发项目全局变量太多怎么管理?
    4.C语言开发单片机为什么大多数都采用全局变量的形式
    5.单片机怎么实现模块化编程?实用程度让人发指!
    6.c语言回调函数的使用及实际作用详解

    7.手把手教你c语言队列实现代码,通俗易懂超详细!

    8.c语言指针用法详解,通俗易懂超详细!
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表