摘要:对于单片机程序来说,大家都不陌生,但是真正使用架构,考虑架构的恐怕并不多,随着程序开发的不断增多,架构是非常必要的。 应用程序的架构大致有三种: 1、简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。 2、时间片轮询法,此方法是介于顺序执行与操作系统之间的一种方法。 3、操作系统,此法应该是应用程序编写的最高境界。一、程序框架设计 1、前后台顺序执行法 这是初学者们常用的程序框架设计方案,不用考虑太多东西,代码简单,或者对系统的整体实时性和并发性要求不高;初始化后通过while(1){}或for(;;){}循环不断调用自己编写完成的函数,也基本不考虑每个函数执行所需要的时间,大部分情况下函数中或多或少都存在毫秒级别的延时等待。优点:对于初学者来说,这是最容易也是最直观的程序架构,逻辑简单明了,适用于逻辑简单,复杂度比较低的软件开发。缺点:实时性低,由于每个函数或多或少存在毫秒级别的延时,即使是1ms,也会造成其他函数间隔执行时间的不同,虽然可通过定时器中断的方式,但是前提是中断执行函数花的时间必须短。当程序逻辑复杂度提升时,会导致后来维护人员的大脑混乱,很难理清楚该程序的运行状态。 以下是在校期间做的寝室防盗系统的部分代码(当时也存在部分BUG,没有解决。现在再看,其实很多问题,而且比较严重,比如中断服务函数内竟然有3000ms延时,这太可怕了,还有串口发送等等;由于实时性要求不算太高,因此主函数中的毫秒级别延时对系统运行没有多大影响,当然除BUG外;若是后期需要维护,那就是一个大工程,还不如推翻重写):intmain(void){u8u8delayinit();NVICPriorityGroupConfig(NVICPriorityGroup2);I2cinit();uart2Init(9600);uartinit(9600);串口初始化为115200TIM3IntInit(4999,7199);ds1302init();while(DHT11Init())DHT11初始化{led20;}a1602init();Ds1302Init();EXTIXInit();GPIOXInit();lcd12864INIT();LcdInit();beepinit();REDInit();led11;beep0;while(1){for(a0;a11;a){num〔a3〕At24c02Read(a2)208;delayus(10);}for(a0;a6;a){shuru〔a〕At24c02Read(a13)208;delayus(10);}delayms(10);REDScan();Ds1302ReadTime();读取ds1302的日期时间shiAt24c02Read(0);读取闹钟保存的数据delayms(10);fenAt24c02Read(1);读取闹钟保存的数据usart2scan();蓝牙数据扫描usart2bian();蓝牙处理数据usart2gai();naoscan();k;if(k20){if(k1)LcdWriteCom(0x01);清屏LcdDisplay();显示日期时间}if(RED0)REDScan();if(k20k30){if(k20)LcdWriteCom(0x01);清屏Lcddisplay();显示温湿度LcdWriteCom(0x806);DHT11ReadData(temperature,humidity);读取温湿度值THLcdWriteData(0temperature10);LcdWriteData(0temperature10);LcdWriteCom(0x800X406);LcdWriteData(0humidity10);LcdWriteData(0humidity10);}if(k30)k0;lcd12864();显示防盗闹钟状态}}定时器3中断服务程序voidTIM3IRQHandler(void)TIM3中断{if(TIMGetITStatus(TIM3,TIMITUpdate)!RESET)检查TIM3更新中断发生与否{TIMClearITPendingBit(TIM3,TIMITUpdate);清除TIMx更新中断标志if(key11FENfen0SHIshi0)时间一到闹钟响起{f1;}if(key10FENfen!0SHIshi!0)else{f0;}if(USARTRXBUF〔0〕RUSARTRXBUF〔1〕IUSARTRXBUF〔2〕NUSARTRXBUF〔3〕G){key01;for(i0;i17;i){USARTSendData(USART1,num〔i〕);向串口1发送数据while(USARTGetFlagStatus(USART1,USARTFLAGTC)!SET);等待发送结束USARTRXSTA0;}delayms(3000);for(i0;i3;i){USARTSendData(USART1,num1〔i〕);向串口1发送数据while(USARTGetFlagStatus(USART1,USARTFLAGTC)!SET);等待发送结束USARTRXSTA0;}}}}2、时间片论法 介于前后台顺序执行法和操作系统之间的一种程序架构设计方案。该设计方案需能帮助嵌入式软件开发者更上一层楼,在嵌入式软件开发过程中,若遇到以下几点,那么该设计方案可以说是最优选择,适用于程序较复杂的嵌入式系统;目前的需求设计需要完全没有必要上操作系统。任务函数无需时刻执行,存在间隔时间(比如按键,一般情况下,都需要软件防抖,初学者的做法通常是延时10ms左右再去判断,但10ms极大浪费了CPU的资源,在这段时间内CPU完全可以处理很多其他事情)实时性有一定的要求。 该设计方案需要使用一个定时器,一般情况下定时1ms即可(定时时间可随意定,但中断过于频繁效率就低,中断太长,实时性差),因此需要考虑到每个任务函数的执行时间,建议不能超过1ms(能通过程序优化缩短执行时间则最好优化,如果不能优化的,则必须保证该任务的执行周期必须远大于任务所执行的耗时时间),同时要求主循环或任务函数中不能存在毫秒级别的延时。 如何确定每个函数的任务周期呢?根据任务的耗时和效果决定、如按键扫描任务周期为10ms(为了提高响应),指示灯控制任务周期为100ms(通常情况下最高100ms的闪烁频率正好,特殊需求除外),LCDOLED显示周期为100ms(通过这种通过SPIIIC等接口的方式耗时大约在110ms,甚至更长,所以任务周期必须远大于耗时,同时为了满足人眼所能接受的刷屏效果,也不能太长,100ms的任务周期比较合适)等。 以下介绍两种不同的实现方案,分别针对无函数指针概念的朋友和想进一步学习的朋友。 1、无函数指针的设计方式brief主函数。paramNone。returnNone。intmain(void){SystemInit();while(1){if(TIM1msFlag)1ms{CANCommTask();CAN发送接收通信任务TIM1msFlag0;}if(TIM10msFlag)10ms{KEYScanTask();按键扫描处理任务TIM10msFlag0;}if(TIM20msFlag)20ms{LOGICHandleTask();逻辑处理任务TIM20msFlag0;}if(TIM100msFlag)100ms{LEDCtrlTask();指示灯控制任务TIM100msFlag0;}if(TIM500msFlag)500ms{TIM500msFlag0;}if(TIM1secFlag)1s{WDogTask();喂狗任务TIM1secFlag0;}}}brief定时器3中断服务函数。paramNone。returnNone。voidTIM3IRQHandler(void){if(TIMGetITStatus(TIM3,TIMITUpdate)SET)溢出中断{sg1msTsg1msTic10?TIM1msFlag1:0;sg1msTic100?TIM10msFlag1:0;sg1msTic200?TIM20msFlag1:0;sg1msTic1000?TIM100msFlag1:0;sg1msTic5000?TIM500msFlag1:0;sg1msTic10000?(TIM1secFlag1,sg1msTic0):0;}TIMClearITPendingBit(TIM3,TIMITUpdate);清除中断标志位} 2、含函数指针的设计方式brief任务函数相关信息结构体定义。typedefstruct{uint8mrunF!程序运行标记:0不运行,1运行uint16!计时器uint16mitvT!任务运行间隔时间void(mpTaskHook)(void);!要运行的任务函数}TASKInfoTdefineTASKSMAX5定义任务数目任务函数相关信息staticTASKInfoTypesgtTaskInfo〔TASKSMAX〕{{0,1,1,CANCommTask},CAN通信任务{0,10,10,KEYScanTask},按键扫描任务{0,20,20,LOGICHandleTask},逻辑处理任务{0,100,100,LEDCtrlTask},指示灯控制任务{0,1000,1000,WDogTask},喂狗任务};brief任务函数运行标志处理。note该函数由1ms定时器中断调用paramNone。returnNone。voidTASKRemarks(void){uint8i;for(i0;iTASKSMAX;i){if(sgtTaskInfo〔i〕。mtimer){sgtTaskInfo〔i〕。if(0sgtTaskInfo〔i〕。mtimer){sgtTaskInfo〔i〕。mtimersgtTaskInfo〔i〕。mitvTsgtTaskInfo〔i〕。mrunFlag1;}}}}brief任务函数运行处理。note该函数由主循环调用paramNone。returnNone。voidTASKProcess(void){uint8i;for(i0;iTASKSMAX;i){if(sgtTaskInfo〔i〕。mrunFlag){sgtTaskInfo〔i〕。mpTaskHook();运行任务sgtTaskInfo〔i〕。mrunFlag0;标志清0}}}brief主函数。paramNone。returnNone。intmain(void){SystemInit();while(1){TASKProcess();}}brief定时器3中断服务函数。paramNone。returnNone。voidTIM3IRQHandler(void){if(TIMGetITStatus(TIM3,TIMITUpdate)SET)溢出中断{TASKRemarks();}TIMClearITPendingBit(TIM3,TIMITUpdate);清除中断标志位}三、操作系统 嵌入式操作系统EOS(EmbeddedOperatingSystem)是一种用途广泛的系统软件,过去它主要应用于工业控制和国防系统领域,而对于单片机来说,比较常用的有UCOS、FreeRTOS、RTThreadNano和RTX等多种抢占式操作系统(其他如Linux等操作系统不适用于单片机) 操作系统和时间片论法,在任务执行方面来说,操作系统对每个任务的耗时没有过多的要求,需要通过设置每个任务的优先级,在高优先级的任务就绪时,会抢占低优先级的任务;操作系统相对复杂,因此这里没有详细介绍了。 关于如何选择合适的操作系统(uCOS、FreeRTOS、RTThread、RTX等RTOS的对比之特点:uCOS:网上资料丰富,非常适合学习,但是在产品上使用则需要收费。FreeRTOS:使用免费,因此很多产品都在用。RTThread:国产物联网操作系统,有着十分丰富的组件,也免费,资料:RTThread文档中心。RTX:为ARM和CortexM设备设计的免版税,确定性的实时操作系统。 借网上一张对比图: 四、总结 从上述的对比中可以看出,时间片轮询法的优势还是比较大的,它既有前后台顺序执行法的优点,也有操作系统的优点。结构清晰,简单,非常容易理解,所以这种是比较常用的单片机设计框架。