前言
前不久,我有位做测试的朋友转去做开发的工作,面试遇到了一个问题,他没明白,打电话问了我。题目大概就是:
在单片机裸机开发时,单片机要处理多个任务,此时你的程序框架是怎样的呢?
这其实是个经典面试问题,我以前面试也被问过。
答案一:轮询系统
代码结构如:
左右滑动查看全部代码>>>
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!rn"); } void timer2_callback() { printf("timer2 timeout!rn"); } 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[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-1, 36000-1); // 定时周期1s LedInit(); // Led初始化 TemperatureInit(); // 温度传感器初始化 LcdInit(); // LCD初始化 }
上一篇:单片机指令中如何区分是字节传送和位传送
下一篇:单片机I/O控制方式(UART中断和DMA中断的区别)
推荐阅读最新更新时间:2024-11-12 13:21
设计资源 培训 开发板 精华推荐
- 使用 Analog Devices 的 LTC3602IFE 的参考设计
- STEVAL-ISA186V1,基于 A6985F5V、38V、0.5A 同步降压开关稳压器的评估板
- DC2190A-B,基于 LTC3892-1 2 相降压控制器的演示板,16V = VIN = 55V,VOUT = 12V @ 30A
- 使用 Analog Devices 的 AD5611AKS 的参考设计
- 使用 2 速时钟的 7.5us 模数转换器
- 【锁相环】ADF4350频率合成器
- 四合一智能电子锁
- LTC1603 具有停机功能的高速、16 位、250ksps 采样 A/D 转换器的典型应用
- IEEE 802.3af兼容的单端口、供电设备应用
- LTC4367CMS8 过压电源控制器的典型应用