你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

单片机裸机程序框架是怎样炼成的?

[复制链接]
gaosmile 发布时间:2020-8-26 23:07
前言

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

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

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

答案一:轮询系统

代码结构如:

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

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 ,项目仓库地址:

http://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, 1000, 1000); //1s loop
    timer_start(&timer1);
   
    timer_init(&timer2, timer2_callback, 50, 0); //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 = 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 ) g_Timer1-- ;
  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-1, 36000-1); // 定时周期1s
LedInit();                  // Led初始化
TemperatureInit();          // 温度传感器初始化
LcdInit();                  // LCD初始化
}


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

int main(void)
{
//-----------------------------------------------------------------------------------------------
// 上电初始化函数
SysInit();

//-----------------------------------------------------------------------------------------------
// 主程序
while (1)
{
  //-----------------------------------------------------------------------------------------------
  // 定时任务
  Task_Led();
  Task_GetTemperature();
  Task_SendToLcd();
}
}

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

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

微信图片_20200826230432.png

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

2、实践(代入法)

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

微信图片_20200826230436.png
微信图片_20200826230440.png
微信图片_20200826230443.png
微信图片_20200826230447.png
微信图片_20200826230450.png
微信图片_20200826230454.png
微信图片_20200826230457.png
微信图片_20200826230501.png
微信图片_20200826230503.png


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

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

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

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

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

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

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


收藏 评论1 发布时间:2020-8-26 23:07

举报

1个回答
jeffhe1 回答时间:2020-8-27 09:02:01
假如你已經使用 RTOS (答案2) , 那一切不就更有效率 ,  也比自己用 第3方法 timer管理 , 更好一 些

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版