当前位置:首页 > 嵌入式 > 嵌入式微处理器
[导读]前言 前不久,我有位做测试的朋友转去做开发的工作,面试遇到了一个问题,他没明白,打电话问了我。题目大概就是: 在单片机裸机开发时,单片机要处理多个任务,此时你的程序框架是怎样的呢? 这其实是个经典面试问题,我以前面试也被问过。 答案一:轮询系统


前言

前不久,我有位做测试的朋友转去做开发的工作,面试遇到了一个问题,他没明白,打电话问了我。题目大概就是:

单片机裸机开发时,单片机要处理多个任务,此时你的程序框架是怎样的呢?

这其实是个经典面试问题,我以前面试也被问过。

答案一:轮询系统

代码结构如:

左右滑动查看全部代码>>>

int main(void)
{
 init_something();
 
 while(1)
 {
  do_something1();
        do_something2();
        do_something3();
 }
}

这种结构大概是我们初学单片机的时候的代码结构。在没有外部事件驱动时,可以较好使用。

只答出了这种情况,印象分估计会比较低,多半凉凉。

答案二:前后台系统

代码结构如(该代码来自 《RT-Thread内核实现与应用开发实践指南》 ):

左右滑动查看全部代码>>>

int flag1 = 0;
int flag2 = 0;
int flag3 = 0;

int main(void)
{
 /* 硬件相关初始化 */
 HardWareInit();

 /* 无限循环 */
 for (;;) {
   if (flag1) {
     /* 处理事情 1 */
     DoSomething1();
   }

   if (flag2) {
     /* 处理事情 2 */
     DoSomethingg2();
   }

   if (flag3) {
     /* 处理事情 3 */
     DoSomethingg3();
   }
 }
}

void ISR1(void)
{
 /* 置位标志位 */
 flag1 = 1;
 /* 如果事件处理时间很短,则在中断里面处理
 如果事件处理时间比较长,在回到后台处理 */

 DoSomething1();
}

void ISR2(void)
{
 /* 置位标志位 */
 flag2 = 2;

 /* 如果事件处理时间很短,则在中断里面处理
 如果事件处理时间比较长,在回到后台处理 */

 DoSomething2();
}

void ISR3(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"

struct Timer timer1;
struct Timer timer2;

void timer1_callback()
{
    printf("timer1 timeout!\r\n");
}

void timer2_callback()
{
    printf("timer2 timeout!\r\n");
}

int main()
{
    timer_init(&timer1, timer1_callback, 10001000); //1s loop
    timer_start(&timer1);
    
    timer_init(&timer2, timer2_callback, 500); //50ms delay
    timer_start(&timer2);
    
    while(1) {
        
        timer_loop();
    }
}

void HAL_SYSTICK_Callback(void)
{
    timer_ticks(); //1ms ticks
}

3、MultiTimer 的移植、剖析

想要对MultiTimer 进行深入学习可阅读项目源码及如下这篇文章:

第6期 | MultiTimer,一款可无限扩展的软件定时器

自己动手,丰衣足食

1、代码模板

准备一个定时器,可以是系统滴答定时器,也可以是TIM定时器,使用这个定时器拓展出多个软件定时器。

比如我们系统中有三个任务:LED翻转、温度采集、温度显示。此时我们可以使用一个硬件定时器拓展出3个软件定时器,定义如下宏定义:

左右滑动查看全部代码>>>

#define  MAX_TIMER            3            // 最大定时器个数
EXT volatile unsigned long    g_Timer1[MAX_TIMER]; 
#define  LedTimer             g_Timer1[0]  // LED翻转定时器
#define  GetTemperatureTimer  g_Timer1[1]  // 温度采集定时器
#define  SendToLcdTimer       g_Timer1[2]  // 温度显示定时器

#define  TIMER1_SEC        (1)              // 秒
#define  TIMER1_MIN        (TIMER1_SEC*60)  // 分


在定时器初始化的时候也顺便给三个软件定时器进行初始化操作:

左右滑动查看全部代码>>>

/********************************************************************************************************
** 函数: TIM1_Init, 通用定时器1初始化
**------------------------------------------------------------------------------------------------------
** 参数: arr:自动重装值 psc:时钟预分频数
** 说明: 定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft
** 返回: void 
********************************************************************************************************/

void TIM1_Init(uint16_t arr, uint16_t psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
 NVIC_InitTypeDef NVIC_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(int i = 0; i < MAX_TIMER; i++)
 {
  g_Timer1[i] = 0;   
 }
}


在定时器中断中对这些软件定时器进行定时值做递减操作:

左右滑动查看全部代码>>>

/********************************************************************************************************
** 函数: TIM1_IRQHandler,  定时器1中断服务程序
**------------------------------------------------------------------------------------------------------
** 参数: 无
** 返回: 无 
********************************************************************************************************/

void TIM1_UP_IRQHandler(void)   //TIM1中断
{
 uint8 i;
 
 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则该任务会被触发执行,比如:

左右滑动查看全部代码>>>

void Task_Led(void)
{
 //----------------------------------------------------------------
 // 等待定时时间
 if(LedTimer) return;
 LedTimer = 1 * TIMER1_SEC;
 //----------------------------------------------------------------
 // LED任务主体
 LedToggle();
}

void Task_GetTemperature(void)
{
 //----------------------------------------------------------------
 // 等待定时时间
 if(LedTimer) return;
 LedTimer = 2 * TIMER1_SEC;
 //----------------------------------------------------------------
 // 温度采集任务主体
 GetTemperature();
}

void Task_SendToLcd(void)
{
 //----------------------------------------------------------------
 // 等待定时时间
 if(LedTimer) return;
 LedTimer = 2 * TIMER1_SEC;
 //----------------------------------------------------------------
 // 温度显示任务主体
 LcdDisplay();
}


如此一来,每过1、2、4秒则分别触发LED翻转任务、温度采集任务、温度显示任务。

这里配置的最小定时单位为1秒,当然根据实际需要进行配置(定时器初始化),定时器初始化可以放在系统统一初始化函数里:

左右滑动查看全部代码>>>

/********************************************************************************************************
** 函数: SysInit, 系统上电初始化
**------------------------------------------------------------------------------------------------------
** 参数: 
** 说明: 
** 返回: 
********************************************************************************************************/

void SysInit(void)
{
 CpuInit();                  // 配置系统信息函数
 SysTickInit();              // 系统滴答定时器初始化函数
 UsartInit(115200);          // 串口初始化函数,波特率115200
 TIM1_Init(2000-136000-1); // 定时周期1s
 LedInit();                  // Led初始化
 TemperatureInit();          // 温度传感器初始化
 LcdInit();                  // LCD初始化
}


此时我们的main函数就可以设计为:

int main(void)
{
 //----------------------------------------------------------------------------------------------- 
 // 上电初始化函数
 SysInit(); 
 
 //----------------------------------------------------------------------------------------------- 
 // 主程序
 while (1)
 {
  //----------------------------------------------------------------------------------------------- 
  // 定时任务
  Task_Led();
  Task_GetTemperature(); 
  Task_SendToLcd();
 }
}

主函数主要是进行系统上电的一些初始化操作,接着是调用各定时任务函数。

本demo使用定时器1来扩展出3个软件定时器,如果TIM资源不够用,可以换用系统滴答定时器来做。如:

其中,时间基数可以根据实际需要进行调整。

2、实践(代入法)

套用以上模板,分享我的一个实例:


需要思考及注意的问题是给每个任务的定时值设置多大合适?这也是一些朋友有疑问的,这只能是自己对自己的任务做考虑,具体情况具体分析,给经验值、调试调整。

就如同常常有人问定义多大的数组合适?在使用RTOS时每个线程的线程栈大小设置多大合适、优先级设置为多少合适?这些都是需要我们自己进行思考的。

有模板/轮子套用是好事,但有些问题不能单单依靠模板,否则有可能把自己给套进去。

以上是以STM32为例的,其它单片机也是可以用这样子的思想的,包括51单片机。

面对文首提到的面试问题,若是可以提到使用软件定时器来处理,进一步能清楚地表达出来,再进一步能写出一些伪代码,那这场面试多半是稳了。

不仅仅是为了面试,本文的方法是很经典的,小编曾经接触的产品项目中就有用到,很实用,值得学习掌握。方法掌握多了,实际应用的时候想用屠龙刀还是倚天剑根据实际情况选择使用即可。

以上就是本次的分享,如有错误,欢迎指出,谢谢。


-END-


本文授权转载嵌入式大杂烩,作者:ZhengNL




推荐阅读



【01】怎么学习单片机外围器件?
【02】漫画版:如何学习单片机?
【03】单片机:3种时钟电路方案对比,你常用哪一种?
【04】单片机编程技术学习攻略
【05】国产超低价单片机五宗罪!“扶不起”的原因就是它们?


免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

嵌入式ARM

扫描二维码,关注更多精彩内容

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭