前言
前不久,我有位做测试的朋友转去做开发的工作,面试遇到了一个问题,他没明白,打电话问了我。题目大概就是:
在单片机裸机开发时,单片机要处理多个任务,此时你的程序框架是怎样的呢?
这其实是个经典面试问题,我以前面试也被问过。
答案一:轮询系统
代码结构如:
左右滑动查看全部代码>>>
intmain(void)
{ init_something(); while(1) { do_something1(); do_something2(); do_something3(); } }
这种结构大概是我们初学单片机的时候的代码结构。在没有外部事件驱动时,可以较好使用。
只答出了这种情况,印象分估计会比较低,多半凉凉。
答案二:前后台系统
代码结构如(该代码来自 《RT-Thread内核实现与应用开发实践指南》 ):
左右滑动查看全部代码>>>
intflag1=0;
intflag2=0; intflag3=0; intmain(void) { /*硬件相关初始化*/ HardWareInit(); /*无限循环*/ for(;;){ if(flag1){ /*处理事情1*/ DoSomething1(); } if(flag2){ /*处理事情2*/ DoSomethingg2(); } if(flag3){ /*处理事情3*/ DoSomethingg3(); } } } voidISR1(void) { /*置位标志位*/ flag1=1; /*如果事件处理时间很短,则在中断里面处理 如果事件处理时间比较长,在回到后台处理*/ DoSomething1(); } voidISR2(void) { /*置位标志位*/ flag2=2; /*如果事件处理时间很短,则在中断里面处理 如果事件处理时间比较长,在回到后台处理*/ DoSomething2(); } voidISR3(void) { /*置位标志位*/ flag3=1; /*如果事件处理时间很短,则在中断里面处理 如果事件处理时间比较长,在回到后台处理*/ DoSomething3(); }
此处,中断称为前台,main中的while循环称为后台。相比于循环系统,这种方式相对可以提高外部事件的实时响应能力。
可以回答出这种情况,印象分大概一半以上,会再细问。
答案三:升级版前后台系统(软件定时器法)
以前,学C语言时,常常听到有人说:指针是C语言的灵魂,没学会指针就是没学会C语言。。
后来,学单片机时,又听到有人说:中断和定时器是单片机的灵魂,没掌握中断与定时器就没学会单片机。。
大佬们都那么说了,那就拿定时器来搞点事情。定时器浑身都是宝,本篇笔记我们来介绍使用定时器(系统滴答定时器或者其它定时器)来做的裸机框架。软件定时器法也有另一种说法:时间片轮询法。
可以回答出这种情况,这场面试多半稳了。
下面以STM32单片机为例看看这种方法的使用。
站在巨人的肩膀上
开源项目—— MultiTimer ,项目仓库地址:
https://github.com/0x1abin/MultiTimer
1、MultiTimer 简介
MultiTimer 是一个软件定时器扩展模块,可无限扩展你所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。
2、MultiTimer 的demo
左右滑动查看全部代码>>>
#include"multi_timer.h"
structTimertimer1; structTimertimer2; voidtimer1_callback() { printf("timer1timeout! "); } voidtimer2_callback() { printf("timer2timeout! "); } intmain() { timer_init(&timer1,timer1_callback,1000,1000);//1sloop timer_start(&timer1); timer_init(&timer2,timer2_callback,50,0);//50msdelay timer_start(&timer2); while(1){ timer_loop(); } } voidHAL_SYSTICK_Callback(void) { timer_ticks();//1msticks }
3、MultiTimer 的移植、剖析
想要对MultiTimer 进行深入学习可阅读项目源码及如下这篇文章:
第6期 | MultiTimer,一款可无限扩展的软件定时器
自己动手,丰衣足食
1、代码模板
准备一个定时器,可以是系统滴答定时器,也可以是TIM定时器,使用这个定时器拓展出多个软件定时器。
比如我们系统中有三个任务:LED翻转、温度采集、温度显示。此时我们可以使用一个硬件定时器拓展出3个软件定时器,定义如下宏定义:
左右滑动查看全部代码>>>
#defineMAX_TIMER3//最大定时器个数
EXTvolatileunsignedlongg_Timer1[MAX_TIMER]; #defineLedTimerg_Timer1[0]//LED翻转定时器 #defineGetTemperatureTimerg_Timer1[1]//温度采集定时器 #defineSendToLcdTimerg_Timer1[2]//温度显示定时器 #defineTIMER1_SEC(1)//秒 #defineTIMER1_MIN(TIMER1_SEC*60)//分
在定时器初始化的时候也顺便给三个软件定时器进行初始化操作:
左右滑动查看全部代码>>>
/********************************************************************************************************
**函数:TIM1_Init,通用定时器1初始化 **------------------------------------------------------------------------------------------------------ **参数: arr:自动重装值 psc:时钟预分频数 **说明:定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft **返回:void ********************************************************************************************************/ voidTIM1_Init(uint16_tarr,uint16_tpsc) { TIM_TimeBaseInitTypeDefTIM_TimeBaseStructure; NVIC_InitTypeDefNVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE); /*定时器TIM1初始化*/ TIM_TimeBaseStructure.TIM_Period=arr; TIM_TimeBaseStructure.TIM_Prescaler=psc; TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_RepetitionCounter=0; TIM_TimeBaseInit(TIM1,&TIM_TimeBaseStructure); TIM_ClearFlag(TIM1,TIM_FLAG_Update); /*中断使能*/ TIM_ITConfig(TIM1,TIM_IT_Update,ENABLE); /*中断优先级NVIC设置*/ NVIC_InitStructure.NVIC_IRQChannel=TIM1_UP_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; NVIC_InitStructure.NVIC_IRQChannelSubPriority=0; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM1,ENABLE); //全局定时器初始化 for(inti=0;i< MAX_TIMER; i++) { g_Timer1[i] = 0; } }
在定时器中断中对这些软件定时器进行定时值做递减操作:
左右滑动查看全部代码>>>
/********************************************************************************************************
**函数:TIM1_IRQHandler,定时器1中断服务程序 **------------------------------------------------------------------------------------------------------ **参数:无 **返回:无 ********************************************************************************************************/ voidTIM1_UP_IRQHandler(void)//TIM1中断 { uint8i; if(TIM_GetITStatus(TIM1,TIM_IT_Update)!=RESET)//检查TIM1更新中断发生与否 { //------------------------------------------------------------------------------- //各种定时间器计时 for(i=0;i< MAX_TIMER; i++) // 定时时间递减 if( g_Timer1[i] ) g_Timer1[i]-- ; TIM_ClearITPendingBit(TIM1, TIM_IT_Update); //清除TIMx更新中断标志 } }
我们在各个定时任务中给这些软件定时器赋予定时值,这些定时值递减到0则该任务会被触发执行,比如:
左右滑动查看全部代码>>>
voidTask_Led(void)
{ //---------------------------------------------------------------- //等待定时时间 if(LedTimer)return; LedTimer=1*TIMER1_SEC; //---------------------------------------------------------------- //LED任务主体 LedToggle(); } voidTask_GetTemperature(void) { //---------------------------------------------------------------- //等待定时时间 if(LedTimer)return; LedTimer=2*TIMER1_SEC; //---------------------------------------------------------------- //温度采集任务主体 GetTemperature(); } voidTask_SendToLcd(void) { //---------------------------------------------------------------- //等待定时时间 if(LedTimer)return; LedTimer=2*TIMER1_SEC; //---------------------------------------------------------------- //温度显示任务主体 LcdDisplay(); }
如此一来,每过1、2、4秒则分别触发LED翻转任务、温度采集任务、温度显示任务。
这里配置的最小定时单位为1秒,当然根据实际需要进行配置(定时器初始化),定时器初始化可以放在系统统一初始化函数里:
左右滑动查看全部代码>>>
/********************************************************************************************************
**函数:SysInit,系统上电初始化 **------------------------------------------------------------------------------------------------------ **参数: **说明: **返回: ********************************************************************************************************/ voidSysInit(void) { CpuInit();//配置系统信息函数 SysTickInit();//系统滴答定时器初始化函数 UsartInit(115200);//串口初始化函数,波特率115200 TIM1_Init(2000-1,36000-1);//定时周期1s LedInit();//Led初始化 TemperatureInit();//温度传感器初始化 LcdInit();//LCD初始化 }
此时我们的main函数就可以设计为:
intmain(void)
{ //----------------------------------------------------------------------------------------------- //上电初始化函数 SysInit(); //----------------------------------------------------------------------------------------------- //主程序 while(1) { //----------------------------------------------------------------------------------------------- //定时任务 Task_Led(); Task_GetTemperature(); Task_SendToLcd(); } }
主函数主要是进行系统上电的一些初始化操作,接着是调用各定时任务函数。
本demo使用定时器1来扩展出3个软件定时器,如果TIM资源不够用,可以换用系统滴答定时器来做。如:
其中,时间基数可以根据实际需要进行调整。
2、实践(代入法)
套用以上模板,分享我的一个实例:
需要思考及注意的问题是给每个任务的定时值设置多大合适?这也是一些朋友有疑问的,这只能是自己对自己的任务做考虑,具体情况具体分析,给经验值、调试调整。
就如同常常有人问定义多大的数组合适?在使用RTOS时每个线程的线程栈大小设置多大合适、优先级设置为多少合适?这些都是需要我们自己进行思考的。
有模板/轮子套用是好事,但有些问题不能单单依靠模板,否则有可能把自己给套进去。
以上是以STM32为例的,其它单片机也是可以用这样子的思想的,包括51单片机。
面对文首提到的面试问题,若是可以提到使用软件定时器来处理,进一步能清楚地表达出来,再进一步能写出一些伪代码,那这场面试多半是稳了。
不仅仅是为了面试,本文的方法是很经典的,小编曾经接触的产品项目中就有用到,很实用,值得学习掌握。方法掌握多了,实际应用的时候想用屠龙刀还是倚天剑根据实际情况选择使用即可。
以上就是本次的分享,如有错误,欢迎指出,谢谢。
-
单片机
+关注
关注
6030文章
44482浏览量
631749 -
C语言
+关注
关注
180文章
7591浏览量
135770 -
定时器
+关注
关注
23文章
3229浏览量
114295
原文标题:工程师实战:单片机裸机程序框架是怎样炼成的?
文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论