• 嵌入式软件开发常用的3种架构


    摘要:对于单片机程序来说,大家都不陌生,但是真正使用架构,考虑架构的恐怕并不多,随着程序开发的不断增多,架构是非常必要的。

    应用程序的架构大致有三种:

    • 1、简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。
    • 2、时间片轮询法,此方法是介于顺序执行与操作系统之间的一种方法。
    • 3、操作系统,此法应该是应用程序编写的最高境界。

    一、程序框架设计

    1、前后台顺序执行法

    这是初学者们常用的程序框架设计方案,不用考虑太多东西,代码简单,或者对系统的整体实时性和并发性要求不高;初始化后通过while(1){}for(;;){}`循环不断调用自己编写完成的函数,也基本不考虑每个函数执行所需要的时间,大部分情况下函数中或多或少都存在毫秒级别的延时等待。

    • 优点:对于初学者来说,这是最容易也是最直观的程序架构,逻辑简单明了,适用于逻辑简单,复杂度比较低的软件开发。
    • 缺点:实时性低,由于每个函数或多或少存在毫秒级别的延时,即使是1ms,也会造成其他函数间隔执行时间的不同,虽然可通过定时器中断的方式,但是前提是中断执行函数花的时间必须短。当程序逻辑复杂度提升时,会导致后来维护人员的大脑混乱,很难理清楚该程序的运行状态。

    以下是在校期间做的寝室防盗系统的部分代码(当时也存在部分BUG,没有解决。现在再看,其实很多问题,而且比较严重,比如中断服务函数内竟然有3000ms延时,这太可怕了,还有串口发送等等;由于实时性要求不算太高,因此主函数中的毫秒级别延时对系统运行没有多大影响,当然除BUG外;若是后期需要维护,那就是一个大工程,还不如推翻重写):

    1. int main(void)
    2. {
    3. u8 temperature;
    4. u8 humidity;
    5. int a;
    6. delay_init();
    7. NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    8. I2c_init();
    9. uart2_Init(9600);
    10. uart_init(9600);//串口初始化为115200
    11. TIM3_Int_Init(4999,7199);
    12. ds1302_init();
    13. while(DHT11_Init())//DHT11初始化
    14. {
    15. led2=0;
    16. }
    17. a1602_init();
    18. Ds1302Init();
    19. EXTIX_Init();
    20. GPIOX_Init();
    21. lcd12864_INIT();
    22. LcdInit();
    23. beep_init();
    24. RED_Init();
    25. led1=1;
    26. beep=0;
    27. while(1)
    28. {
    29. for(a=0;a<11;a++)
    30. {
    31. num[a+3]=At24c02Read(a+2)-208;
    32. delay_us(10);
    33. }
    34. for(a=0;a<6;a++)
    35. {
    36. shuru[a]=At24c02Read(a+13)-208;
    37. delay_us(10);
    38. }
    39. delay_ms(10);
    40. RED_Scan();
    41. Ds1302ReadTime(); //读取ds1302的日期时间
    42. shi=At24c02Read(0); //读取闹钟保存的数据
    43. delay_ms(10);
    44. fen=At24c02Read(1); //读取闹钟保存的数据
    45. usart2_scan(); //蓝牙数据扫描
    46. usart2_bian(); //蓝牙处理数据
    47. usart2_gai();
    48. nao_scan();
    49. k++;
    50. if(k<20)
    51. {
    52. if(k==1)
    53. LcdWriteCom(0x01);//清屏
    54. LcdDisplay(); //显示日期时间
    55. }
    56. if(RED==0)
    57. RED_Scan();
    58. if(k>=20&&k<30)
    59. {
    60. if(k==20)
    61. LcdWriteCom(0x01); //清屏
    62. Lcddisplay(); //显示温湿度
    63. LcdWriteCom(0x80+6);
    64. DHT11_Read_Data(&temperature,&humidity); //读取温湿度值
    65. Temp=temperature;Humi=humidity;
    66. LcdWriteData('0'+temperature/10);
    67. LcdWriteData('0'+temperature%10);
    68. LcdWriteCom(0x80+0X40+6);
    69. LcdWriteData('0'+humidity/10);
    70. LcdWriteData('0'+humidity%10);
    71. }
    72. if(k==30)
    73. k=0;
    74. lcd12864(); //显示防盗闹钟状态
    75. }
    76. }
    77. //定时器3中断服务程序
    78. void TIM3_IRQHandler(void)//TIM3中断
    79. {
    80. int i;
    81. if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否
    82. {
    83. TIM_ClearITPendingBit(TIM3, TIM_IT_Update);//清除TIMx更新中断标志
    84. if(key1==1&&FEN-fen==0&&SHI-shi==0)//时间一到闹钟响起
    85. {
    86. f=1;
    87. }
    88. if(key1==0||FEN-fen!=0||SHI-shi!=0)
    89. else
    90. {
    91. f=0;
    92. }
    93. if(USART_RX_BUF[0]=='R'&&USART_RX_BUF[1]=='I'&&USART_RX_BUF[2]=='N'&&USART_RX_BUF[3]=='G')
    94. {
    95. key0=1;
    96. for(i=0;i<17;i++)
    97. {
    98. USART_SendData(USART1, num[i]);//向串口1发送数据
    99. while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
    100. USART_RX_STA=0;
    101. }
    102. delay_ms(3000);
    103. for(i=0;i<3;i++)
    104. {
    105. USART_SendData(USART1, num1[i]);//向串口1发送数据
    106. while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
    107. USART_RX_STA=0;
    108. }
    109. }
    110. }
    111. }

    二、时间片论法

    介于前后台顺序执行法操作系统之间的一种程序架构设计方案。该设计方案需能帮助嵌入式软件开发者更上一层楼,在嵌入式软件开发过程中,若遇到以下几点,那么该设计方案可以说是最优选择,适用于程序较复杂的嵌入式系统;

    • 目前的需求设计需要完全没有必要上操作系统。
    • 任务函数无需时刻执行,存在间隔时间(比如按键,一般情况下,都需要软件防抖,初学者的做法通常是延时10ms左右再去判断,但10ms极大浪费了CPU的资源,在这段时间内CPU完全可以处理很多其他事情)
    • 实时性有一定的要求。

    该设计方案需要使用一个定时器,一般情况下定时1ms即可(定时时间可随意定,但中断过于频繁效率就低,中断太长,实时性差),因此需要考虑到每个任务函数的执行时间,建议不能超过1ms(能通过程序优化缩短执行时间则最好优化,如果不能优化的,则必须保证该任务的执行周期必须远大于任务所执行的耗时时间),同时要求主循环或任务函数中不能存在毫秒级别的延时。

    “如何确定每个函数的任务周期呢?根据任务的耗时和效果决定、如按键扫描任务周期为 10ms(为了提高响应),指示灯控制任务周期为 100ms(通常情况下最高100ms的闪烁频率正好,特殊需求除外),LCD/OLED 显示周期为 100ms(通过这种通过SPI/IIC等接口的方式耗时大约在 1~10ms,甚至更长,所以任务周期必须远大于耗时,同时为了满足人眼所能接受的刷屏效果,也不能太长,100ms 的任务周期比较合适)等。

    以下介绍两种不同的实现方案,分别针对无函数指针概念的朋友和想进一步学习的朋友。

    1、无函数指针的设计方式

    1. /**
    2. * @brief 主函数.
    3. * @param None.
    4. * @return None.
    5. */
    6. int main(void)
    7. {
    8. System_Init();
    9. while (1)
    10. {
    11. if (TIM_1msFlag)// 1ms
    12. {
    13. CAN_CommTask(); // CAN发送/接收通信任务
    14. TIM_1msFlag = 0;
    15. }
    16. if (TIM_10msFlag) // 10ms
    17. {
    18. KEY_ScanTask(); // 按键扫描处理任务
    19. TIM_10msFlag = 0;
    20. }
    21. if (TIM_20msFlag) // 20ms
    22. {
    23. LOGIC_HandleTask();// 逻辑处理任务
    24. TIM_20msFlag = 0;
    25. }
    26. if (TIM_100msFlag) // 100ms
    27. {
    28. LED_CtrlTask(); // 指示灯控制任务
    29. TIM_100msFlag = 0;
    30. }
    31. if (TIM_500msFlag)// 500ms
    32. {
    33. TIM_500msFlag = 0;
    34. }
    35. if (TIM_1secFlag) // 1s
    36. {
    37. WDog_Task(); // 喂狗任务
    38. TIM_1secFlag = 0;
    39. }
    40. }
    41. }
    42. /**
    43. * @brief 定时器3中断服务函数.
    44. * @param None.
    45. * @return None.
    46. */
    47. void TIM3_IRQHandler(void)
    48. {
    49. if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) //溢出中断
    50. {
    51. sg_1msTic++;
    52. sg_1msTic % 1 == 0 ? TIM_1msFlag = 1 : 0;
    53. sg_1msTic % 10 == 0 ? TIM_10msFlag = 1 : 0;
    54. sg_1msTic % 20 == 0 ? TIM_20msFlag = 1 : 0;
    55. sg_1msTic % 100 == 0 ? TIM_100msFlag = 1 : 0;
    56. sg_1msTic % 500 == 0 ? TIM_500msFlag = 1 : 0;
    57. sg_1msTic % 1000 == 0 ? (TIM_1secFlag = 1, sg_1msTic = 0) : 0;
    58. }
    59. TIM_ClearITPendingBit(TIM3,TIM_IT_Update); // 清除中断标志位
    60. }

    2、含函数指针的设计方式

    1. /**
    2. * @brief 任务函数相关信息结构体定义.
    3. */
    4. typedef struct{
    5. uint8 m_runFlag; /*!< 程序运行标记:0-不运行,1运行 */
    6. uint16 m_timer; /*!< 计时器 */
    7. uint16 m_itvTime; /*!< 任务运行间隔时间 */
    8. void (*m_pTaskHook)(void); /*!< 要运行的任务函数 */
    9. } TASK_InfoType;
    10. #define TASKS_MAX 5 // 定义任务数目
    11. /** 任务函数相关信息 */
    12. static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {
    13. {0, 1, 1, CAN_CommTask}, // CAN通信任务
    14. {0, 10, 10, KEY_ScanTask}, // 按键扫描任务
    15. {0, 20, 20, LOGIC_HandleTask}, // 逻辑处理任务
    16. {0, 100, 100, LED_CtrlTask}, // 指示灯控制任务
    17. {0, 1000, 1000, WDog_Task}, // 喂狗任务
    18. };
    19. /**
    20. * @brief 任务函数运行标志处理.
    21. * @note 该函数由1ms定时器中断调用
    22. * @param None.
    23. * @return None.
    24. */
    25. void TASK_Remarks(void)
    26. {
    27. uint8 i;
    28. for (i = 0; i < TASKS_MAX; i++)
    29. {
    30. if (sg_tTaskInfo[i].m_timer)
    31. {
    32. sg_tTaskInfo[i].m_timer--;
    33. if (0 == sg_tTaskInfo[i].m_timer)
    34. {
    35. sg_tTaskInfo[i].m_timer = sg_tTaskInfo[i].m_itvTime;
    36. sg_tTaskInfo[i].m_runFlag = 1;
    37. }
    38. }
    39. }
    40. }
    41. /**
    42. * @brief 任务函数运行处理.
    43. * @note 该函数由主循环调用
    44. * @param None.
    45. * @return None.
    46. */
    47. void TASK_Process(void)
    48. {
    49. uint8 i;
    50. for (i = 0; i < TASKS_MAX; i++)
    51. {
    52. if (sg_tTaskInfo[i].m_runFlag)
    53. {
    54. sg_tTaskInfo[i].m_pTaskHook(); // 运行任务
    55. sg_tTaskInfo[i].m_runFlag = 0; // 标志清0
    56. }
    57. }
    58. }
    59. /**
    60. * @brief 主函数.
    61. * @param None.
    62. * @return None.
    63. */
    64. int main(void)
    65. {
    66. System_Init();
    67. while (1)
    68. {
    69. TASK_Process();
    70. }
    71. }
    72. /**
    73. * @brief 定时器3中断服务函数.
    74. * @param None.
    75. * @return None.
    76. */
    77. void TIM3_IRQHandler(void)
    78. {
    79. if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) //溢出中断
    80. {
    81. TASK_Remarks();
    82. }
    83. TIM_ClearITPendingBit(TIM3,TIM_IT_Update);// 清除中断标志位
    84. }

    三、操作系统

    嵌入式操作系统EOS(Embedded OperatingSystem)是一种用途广泛的系统软件,过去它主要应用于工业控制和国防系统领域,而对于单片机来说,比较常用的有UCOS、FreeRTOS、RT-Thread Nano和RTX 等多种抢占式操作系统(其他如Linux等操作系统不适用于单片机)

    操作系统和“时间片论法”,在任务执行方面来说,操作系统对每个任务的耗时没有过多的要求,需要通过设置每个任务的优先级,在高优先级的任务就绪时,会抢占低优先级的任务;操作系统相对复杂,因此这里没有详细介绍了。

    关于如何选择合适的操作系统(uCOSFreeRTOSRTThreadRTX等RTOS的对比之特点:

    • uCOS:网上资料丰富,非常适合学习,但是在产品上使用则需要收费。
    • FreeRTOS:使用免费,因此很多产品都在用。
    • RT-Thread:国产物联网操作系统,有着十分丰富的组件,也免费,资料:RT-Thread文档中心。
    • RTX:为ARM和Cortex-M设备设计的免版税,确定性的实时操作系统。

    借网上一张对比图:

    四、总结

    从上述的对比中可以看出,时间片轮询法的优势还是比较大的,它既有前后台顺序执行法的优点,也有操作系统的优点。结构清晰,简单,非常容易理解,所以这种是比较常用的单片机设计框架。

    好文推荐:

    2022年嵌入式开发想进互联网大厂,你技术过硬吗?

    从事十年嵌入式转内核开发(23K到45K),给兄弟们的一些建议

    腾讯首发Linux内核源码《嵌入式开发进阶笔记》差距差的不止一点点哦

     

  • 相关阅读:
    HDFS学习笔记(五):Yarn架构原理
    发布 jar 包到 maven 中央仓库
    037-JTree控件使用讲解
    【零基础学Java】第十八篇 包装类1(Integer,String)
    NeRF-SLAM 学习笔记
    服务器数据恢复- Ext4文件系统分区挂载报错的数据恢复案例
    python之PyQt按钮右键菜单功能的实现代码
    算法竞赛进阶指南:噩梦(Python)
    前端开发技术栈(工具篇):2023深入了解webpack的安装和使用以及核心概念和启动流程(详细) 63.3k stars
    Thread的常用方法
  • 原文地址:https://blog.csdn.net/m0_50662680/article/details/127710824