工程师实战:单片机裸机程序框架是怎样炼成的?

前言

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

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

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

答案一:轮询系统

代码结构如:

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

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, 1000, 1000); //1s looptimer_start(&timer1);timer_init(&timer2, timer2_callback, 50, 0); //50ms delaytimer_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);          // 串口初始化函数,波特率115200TIM1_Init(2000-1, 36000-1); // 定时周期1sLedInit();                  // 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-

推荐阅读:

    专辑|Linux文章汇总

    专辑|程序人生

    专辑|C语言

嵌入式Linux

微信扫描二维码,关注我的公众号 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/467374.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c 语言指针教学视频,[C语言] 猎豹网校 C和指针视频教程

资源介绍21days_Cpp_二进制和十六进制.mp4xa0xa080x86_内联汇编.mp4xa0xa0ABG2C_for_循环.mp4xa0xa0ABG2C_关系运算符.mp4xa0xa0ABG2C_字符串.mp4xa0xa0ABG2C_循环.mp4xa0xa0ABG2C_数学运算.mp4xa0xa0ABG2C_更高级的运算符.mp4xa0xa0ABG2C_测试多个值.mp4xa0xa0ABG2C_终止循环.…

此处为本人自勉自留地!

本人自97年从师范学院毕业&#xff0c;整整8年的青春献给了自己所事业!前一段&#xff0c;遇到一些人和事&#xff0c;对我有些触动!找了一块自留地&#xff0c;写些东西进行自勉!也许我将有一个全新的开始&#xff0c;也许还是走以前的老路!希望是自己的&#xff0c;结果也是自…

Oracle shared server模式连接ORA-12519

设置了shared server连接&#xff0c;dispatcher进程和shared server进程都没有问题listener.ora文件配置如下&#xff1a;LSNR2 (DESCRIPTION (ADDRESS_LIST (ADDRESS(PROTOCOLtcp)(HOSTedbjr2p1.example.com)(PORT1526)) ))SID_LIST_LSNR2 (SID_LIST (SID_…

图解http

今天是周五&#xff0c;按照正常的情况&#xff0c;今天下班后将会经历一周中最愉快的时光&#xff0c;愉快的时间总是会有那么一些短暂&#xff0c;所以我就送给大家一个资料 「图解http」这是一个微信好友发给我的&#xff0c;他一直给我发送了很多很多消息&#xff0c;比如什…

17种正则表达式

"^\d$"  //非负整数&#xff08;正整数 0&#xff09;"^[0-9]*[1-9][0-9]*$"  //正整数"^((-\d)|(0))$"  //非正整数&#xff08;负整数 0&#xff09;"^-[0-9]*[1-9][0-9]*$"  //负整数"^-?\d$"    //整数&…

为什么执行自己的程序要在前面加./

前言在Linux中&#xff0c;我们执行内置命令时&#xff0c;直接输入命令名称即可&#xff0c;如&#xff1a;$ mv a b #将a重命名为b而在执行自己写好的程序时&#xff0c;却要带上./&#xff0c;例如&#xff1a;$ hello hello: command not found $ ./hello hello world这是为…

break lab c语言,C语言实验lab10.doc

C语言实验lab10C程序设计实验报告学院&#xff1a;国际商学院班级&#xff1a;14电商专业&#xff1a;电子商务姓名&#xff1a;熊靓男日期&#xff1a;15.5.25学号&#xff1a;1420070049实验目的复习一维数组掌握二维数组参数的传递掌握排序算法实验内容消灭怪物在阳光明媚月…

C语言怎么计算数据类型范围?

之前在网上看到的一个讨论&#xff0c;是谁决定了数据类型的范围&#xff1f;比如说&#xff0c;怎么确定 char 就是 -128~127 &#xff0c;而不是 -127~128 呢&#xff1f;说下规定signed 的取值范围是 -(2N-1) to 2N-1 - 1unsigned 的取值范围是 0 to (2N-1) (2N-1 - 1)只要…

双十一为何规则复杂,套路多多

为啥不直接打5折?为了让你把“穷人”俩字写到自己脸上啊。 双十一快到了&#xff0c;今年我又一次有了不太想参加的感觉。作为一个阅读理解不太灵光的人&#xff0c;去年的活动我就整得不太明白——优惠券都是十块十块的&#xff0c;也不知道该咋用;还有预付款、整点秒杀之类的…

出去转了一圈

以前一直不知道市场啊&#xff0c;销售啊什么的到底是怎么回事&#xff0c;今天下午跟着公司的市场总监出去转了一圈&#xff0c;算是有了点感谢认识。现在&#xff0c;伴着柔柔的jazz&#xff0c;写点东西。今下午的风很大&#xff0c;温度也很底&#xff0c;应该说是我来北京…

c语言程序设计顺序结构题目,C语言编程 顺序结构编程练习题目

C语言顺序结构编程练习题目C语言编程 顺序结构题目题目1计算摄氏温度 输入一个华氏温度&#xff0c;要求输出摄氏温度。公式为&#xff1a;C5/9(F-32)&#xff0c;输出要有文字说明&#xff0c;取2位小数。式中&#xff1a;C表示摄氏温度&#xff0c;F表示华氏温度题目2 一辆汽…

团建是什么鬼?

2012年我入职TCL&#xff0c;在TCL我体会到了团队建设的文化&#xff0c;也正是经历了那段职场&#xff0c;我明白了很多「可意味不可言传」。那时候我们经常五十成群出去吃饭、喝酒、唱歌&#xff1b;上学那会我很不喜欢团队活动&#xff0c;特别是室内的活动&#xff0c;总感…

Alpha 答辩总结

【Alpha展示评审表格】 小组序号小组名称格式&#xff08;20%&#xff09;内容&#xff08;20%&#xff09;PPT&#xff08;20%&#xff09;演讲&#xff08;20%&#xff09;答辩&#xff08;20%&#xff09;总分1天机组1515151516762PMS1617171616824“像我这么能打的还有五个…

一粒沙子变成芯片的全过程

推荐阅读&#xff1a;专辑|Linux文章汇总专辑|程序人生专辑|C语言嵌入式Linux微信扫描二维码&#xff0c;关注我的公众号

使用指针星号转移c语言,C中的指针:何时使用号和星号?

你有指针和值&#xff1a;int* p; // variable p is pointer to integer typeint i; // integer value将指针转换为带*的值&#xff1a;int i2 *p; // integer i2 is assigned with integer value that pointer p is pointing to您可以将值转换为带&amp ;:的指针int* p2 …

使用oledb读写excel出现“操作必须使用一个可更新的查询”的解决办法

使用oledb读写excel出现“操作必须使用一个可更新的查询”的解决办法 转自&#xff1a;http://www.cnblogs.com/Richinger/archive/2008/09/28/1301170.html 前两天使用oledb连接excel的办法为单位某部门从一个excel的多个sheet中作连选抽出需要的数据&#xff0c;程序非常简单…

要学会拒绝

Img「聊天截图」这是今晚上跟一个朋友聊天的截图&#xff0c;之后、突然想到这个话题&#xff0c;今年因为疫情&#xff0c;很多人抱怨工作不好找&#xff0c;我这个朋友&#xff0c;也是在疫情之下&#xff0c;可能是干得不开心&#xff0c;也可能还是干得不开心&#xff0c;可…

二叉树小球下落问题c语言,#C++初学记录(树和二叉树)

二叉树的编号 例题 6-6 小球下落问题 有一棵二叉树&#xff0c;最大深度为D&#xff0c;且所有叶子深度都相同。所有节点从上到下&#xff0c;从左到右编号为1,2,3,4&#xff0c;....&#xff0c;2^D-1。在节点1处放置小球&#xff0c;他会往下落。每个节点上都有一个开关&…

unique离散化用法

用法类似lower_bound,sort&#xff0c;不过下标从1开始的话和lower_bound减去的东西不一样 用来离散化很好用 pos就是在原数组a中的rank了&#xff0c;根据cnt建线段树啥的。。。 sort(a21, a21n);cnt unique(a21, a21n) - (a21);FOR(n) {int pos lower_bound(a21, a21cnt, a…

linux设备模型之kset/kobj/ktype分析

1. 概述今天来聊一下Linux设备模型的基石&#xff1a;kset/kobject/ktype。sysfs文件系统提供了一种用户与内核数据结构进行交互的方式&#xff0c;可以通过mount -t sysfs sysfs /sys来进行挂载&#xff1b;Linux设备模型中&#xff0c;设备、驱动、总线组织成拓扑结构&#x…