1 前言

该内容是工作一年来通过上网或其他方式不断搜索、实践、总结出来的嵌入式软件开发经验(本文仅适用于单片机的裸机开发),希望能帮到正在学习这方面的朋友,如有不好的地方,请多多见谅;在嵌入式软件开发过程中,程序架构的搭建尤为重要,下面介绍三种常用的程序框架设计方案:

  • 前后台顺序执行法
  • 时间片论法
  • 操作系统

2 程序框架设计

2.1 前后台顺序执行法

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

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

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

 int main(void){    u8 temperature;         u8 humidity;   int a;delay_init();NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
//   I2c_init(); uart2_Init(9600);uart_init(9600);   //串口初始化为115200  TIM3_Int_Init(4999,7199);ds1302_init();while(DHT11_Init()) //DHT11初始化  {led2=0;}a1602_init();
//   Ds1302Init();EXTIX_Init();         GPIOX_Init();lcd12864_INIT();LcdInit();beep_init();RED_Init();led1=1;beep=0;while(1){for(a=0;a<11;a++){num[a+3]=At24c02Read(a+2)-208;delay_us(10);                   }for(a=0;a<6;a++){shuru[a]=At24c02Read(a+13)-208;delay_us(10);               }       delay_ms(10);  RED_Scan();Ds1302ReadTime();                 //读取ds1302的日期时间shi=At24c02Read(0);              //读取闹钟保存的数据delay_ms(10);fen=At24c02Read(1);             //读取闹钟保存的数据         usart2_scan();                      //蓝牙数据扫描usart2_bian();                      //蓝牙处理数据usart2_gai();nao_scan();k++;if(k<20){if(k==1)LcdWriteCom(0x01);  //清屏LcdDisplay();                           //显示日期时间}if(RED==0)RED_Scan();if(k>=20&&k<30){if(k==20)LcdWriteCom(0x01);  //清屏Lcddisplay();                          //显示温湿度LcdWriteCom(0x80+6); DHT11_Read_Data(&temperature,&humidity);    //读取温湿度值    Temp=temperature;Humi=humidity;LcdWriteData('0'+temperature/10);LcdWriteData('0'+temperature%10);LcdWriteCom(0x80+0X40+6);  LcdWriteData('0'+humidity/10);LcdWriteData('0'+humidity%10);}if(k==30)k=0;lcd12864();                               //显示防盗闹钟状态}
}//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{int i;if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否{TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 if(key1==1&&FEN-fen==0&&SHI-shi==0)               //时间一到闹钟响起{f=1;                     }
//      if(key1==0||FEN-fen!=0||SHI-shi!=0)else{f=0;}   if(USART_RX_BUF[0]=='R'&&USART_RX_BUF[1]=='I'&&USART_RX_BUF[2]=='N'&&USART_RX_BUF[3]=='G'){key0=1;for(i=0;i<17;i++){USART_SendData(USART1, num[i]);//向串口1发送数据while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束USART_RX_STA=0;   }   delay_ms(3000);for(i=0;i<3;i++){USART_SendData(USART1, num1[i]);//向串口1发送数据while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束USART_RX_STA=0;  }           }}
}

2.2 时间片论法

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

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

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

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

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

2.2.1 无函数指针的设计方式

/*** @brief      主函数.* @param      None.* @return     None.*/
int main(void)
{System_Init();while (1){if (TIM_1msFlag)                            // 1ms{CAN_CommTask();                         // CAN发送/接收通信任务TIM_1msFlag = 0;}if (TIM_10msFlag)                           // 10ms{KEY_ScanTask();                         // 按键扫描处理任务TIM_10msFlag = 0;}if (TIM_20msFlag)                           // 20ms{LOGIC_HandleTask();                     // 逻辑处理任务TIM_20msFlag = 0;}if (TIM_100msFlag)                          // 100ms{ LED_CtrlTask();                         // 指示灯控制任务TIM_100msFlag = 0;}    if (TIM_500msFlag)                          // 500ms{TIM_500msFlag = 0;}    if (TIM_1secFlag)                           // 1s{WDog_Task();                            // 喂狗任务TIM_1secFlag = 0;}  }
}/*** @brief      定时器3中断服务函数.* @param      None.* @return     None.*/
void TIM3_IRQHandler(void)
{if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) // 溢出中断{sg_1msTic++;sg_1msTic % 1 == 0 ? TIM_1msFlag = 1 : 0;sg_1msTic % 10 == 0 ? TIM_10msFlag = 1 : 0;sg_1msTic % 20 == 0 ? TIM_20msFlag = 1 : 0;sg_1msTic % 100 == 0 ? TIM_100msFlag = 1 : 0;sg_1msTic % 500 == 0 ? TIM_500msFlag = 1 : 0;sg_1msTic % 1000 == 0 ? (TIM_1secFlag  = 1, sg_1msTic = 0) : 0;}TIM_ClearITPendingBit(TIM3,TIM_IT_Update);  // 清除中断标志位
}

2.2.2 含函数指针的设计方式

/*** @brief 任务函数相关信息结构体定义.*/
typedef struct{uint8 m_runFlag;                 /*!< 程序运行标记:0-不运行,1运行 */uint16 m_timer;                  /*!< 计时器 */uint16 m_itvTime;                /*!< 任务运行间隔时间 */void (*m_pTaskHook)(void);       /*!< 要运行的任务函数 */
} TASK_InfoType; #define TASKS_MAX     5              // 定义任务数目/** 任务函数相关信息 */
static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {{0, 1, 1, CAN_CommTask},         // CAN通信任务{0, 10, 10, KEY_ScanTask},       // 按键扫描任务{0, 20, 20, LOGIC_HandleTask},   // 逻辑处理任务{0, 100, 100, LED_CtrlTask},     // 指示灯控制任务{0, 1000, 1000, WDog_Task},      // 喂狗任务
};/*** @brief      任务函数运行标志处理.* @note       该函数由1ms定时器中断调用* @param      None.* @return     None.*/
void TASK_Remarks(void)
{uint8 i;for (i = 0; i < TASKS_MAX; i++){if (sg_tTaskInfo[i].m_timer){sg_tTaskInfo[i].m_timer--;if (0 == sg_tTaskInfo[i].m_timer){sg_tTaskInfo[i].m_timer = sg_tTaskInfo[i].m_itvTime;sg_tTaskInfo[i].m_runFlag = 1;}}}
}/*** @brief      任务函数运行处理.* @note       该函数由主循环调用* @param      None.* @return     None.*/
void TASK_Process(void)
{uint8 i;for (i = 0; i < TASKS_MAX; i++){if (sg_tTaskInfo[i].m_runFlag){sg_tTaskInfo[i].m_pTaskHook();         // 运行任务sg_tTaskInfo[i].m_runFlag = 0;         // 标志清0}}
}/*** @brief      主函数.* @param      None.* @return     None.*/
int main(void)
{System_Init();while (1){TASK_Process();}
}/*** @brief      定时器3中断服务函数.* @param      None.* @return     None.*/
void TIM3_IRQHandler(void)
{if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) // 溢出中断{TASK_Remarks();}TIM_ClearITPendingBit(TIM3,TIM_IT_Update);     // 清除中断标志位
}

2.3 操作系统

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

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

关于如何选择合适的操作系统([RTOS]uCOS、FreeRTOS、RTThread、RTX等RTOS的对比之特点):

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

借网上一张对比图:


总结

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

下篇:嵌入式软件开发之程序分层(二)

嵌入式软件开发之程序架构(一)相关推荐

  1. 嵌入式软件开发之程序编程规范(五)

    1.前言 作为一个软件开发人员,应养成良好的编程习惯,随着编码越来越多,内容也会变得越来越多,规范化自己的编程有助于为了在程序代码量很大的时候,便于自己阅读,也便于别人阅读(团队合作),特别是作为一个 ...

  2. PX4二次开发——PX4程序架构

    PX4程序架构 一.从RCS启动脚本可以看出哪些东西 ​ 启动脚本是一个神奇的东西,它能够识别出你对应的飞机类型,加载对应的混控器,选择对应的姿态.位置估计程序以及控制程序,初始化你需要的驱动程序.下 ...

  3. 嵌入式软件开发之常用软件(六)

    1.前言 作为一个软件开发人员,除了程序能力素养方面外,还应该熟练地使用各种常用的辅助工具,所谓"工欲善其事,必先利其器":通过这些工具能提升自己的专业素养,且在工作中选择合适的工 ...

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

    摘要:对于单片机程序来说,大家都不陌生,但是真正使用架构,考虑架构的恐怕并不多,随着程序开发的不断增多,架构是非常必要的. 应用程序的架构大致有三种: 1.简单的前后台顺序执行程序,这类写法是大多数人 ...

  5. 三种嵌入式开发程序架构

    三种必须掌握的嵌入式开发程序架构 前言 reference 前言 在嵌入式软件开发,包括单片机开发中,软件架构对于开发人员是一个必须认真考虑的问题.软件架构对于系统整体的稳定性和可靠性是非常重要的,一 ...

  6. 程序员新手 0年份等级 指导(一) 开发人员IT架构总览

    程序员新手 0年份等级 指导(一) 开发人员IT架构总览 程序员新手 0年份等级 指导(一) 开发人员相关IT架构总览之职能分解 开发人员IT架构总览 一.职能分解 软件项目的主要组成大体上按照一个项 ...

  7. Android开发入门一之Android应用程序架构详解

    Android应用程序架构详解如下: src/ java源代码存放目录 gen/自动生成目录 gen 目录中存放所有由Android开发工具自动生成的文件.目录中最重要的就是R.java文件.这个文件 ...

  8. 嵌入式软件开发常用的三种架构

    1.前后台顺序执行法 这是初学者常用的程序设计方案,不用考虑太多东西,代码简单,对系统是整体实时性和并发性要求不高.初始化后通过while(1){}或for(;;){}循环不断调用自己编写完成的函数, ...

  9. 嵌入式软件开发工程师谈软件架构的设计

    嵌入式软件开发工程师谈软件架构的设计 注:此处嵌入式特指基于linux平台,单片机和其他rtos不在讨论范围 笔者从事嵌入式软件开发有6,7个年头,bsp,驱动,应用软件,android hall,f ...

  10. 找文心一言问了几个嵌入式软件开发的问题

    百度终于通知我可以试用文心一言了,试验了一番,总体满意,记录几个文心一言回答的问题. 我是一个嵌入式软件开发者,你能帮我做什么工作以提高我的开发效率? 作为一个嵌入式软件开发者,以下是一些可以提高您的 ...

最新文章

  1. 数据结构-二叉树面试 常考
  2. 框架:springboot组合spring、springmvc、mybatis的一个小demo
  3. dotNET Core 3.X 使用 Web API
  4. 记一次MySQL手工注入
  5. Basic Theory of Physically-Based Rendering
  6. WSFC CLUSDB
  7. *第六周*数据结构实践项目一【建立顺序栈算法库】
  8. matlab各种分布计算公式,SPC常见项目的计算公式
  9. 淘宝APP用户行为数据分析 by 一只废鹅
  10. soul—产品体验报告
  11. 和计算机相关的英文名字女孩,简单好听寓意好的英文名字女孩
  12. 英文博客群建如何保证收录?
  13. armbian清理_N1盒子armbian/ubuntu/linux系统修改日志输出到内存
  14. ps磨皮插件专用智能磨皮插件 AI算法美颜 提高P图效率
  15. 这七种职业男人让多少日本女性着迷
  16. 大卫·希尔伯特:数学界的无冕之王
  17. Xero 系列文章分享-Xero Vs MYOB
  18. 把Linux系统装在U盘上
  19. 理解梁友栋-Barsky裁剪算法
  20. 【DRV8323】电机驱动芯片寄存器配置指南,通过STM32F407的SPI通信配置

热门文章

  1. java微信聊天机器人源码_三步轻松打造微信聊天机器人(附源码)
  2. iFIX组态软件在某电厂辅控系统的应用
  3. 2017计算机二级c语言题库,2017年计算机C语言二级考试题库
  4. 软件测试基础知识+面试
  5. netty权威指南 微云_《Netty权威指南》(二)NIO 入门
  6. Paraview使用指南
  7. Unity url编码转换
  8. [转载]使用 Abbot 框架自动化测试 Eclipse 插件的用户界面,第 1 部分
  9. Javascript分页:模拟后台返回数据方法
  10. 任玉刚【Android开发艺术探索】读后笔记二