一、摘要
- 上一章我们主要将了我们的定时器配置和使用,产生了一个5ms一次的中断,那么这一章我们将展示这个定时器的主要作用,我们以它来产生的5ms作为一个时基,然后我们产生多个系统时钟,从而实现分时任务处理;
二、系统任务分时处理介绍
- 为什么要进行任务分时呢?
在我们之前的程序中我们经常会用到软件延时,如:HAL_Delay(20);代表延时20ms,我们进入这个函数内部看,如下图1所示,我们发现他在函数内部while循环中一直等20ms,那么在执行这个软件延时函数的时候,单片机其实是在这个函数里空跑20ms,会降低效率;所以我们尝试换一种写法,在不进行死延时的情况下完成不同时间周期的任务;
- 分时任务最终效果
如下图2所示,我们在While(1)循环中会持续产生不同的时钟,并且每个任务处理函数中的任务在它的固定周期执行,如Task_5ms_Processing(); 5ms执行一次;这样就避免不同任务的延时对其他任务的影响;
二、程序设计
- 可能在其他地方也可以见到类似的做法,我也是学习过来的,我觉得在我们的程序中采用这种方法应该比较合适,所以在新程序中尝试以下这种做法;
- 添加文件路径(以下是我习惯上新建文件的方式)
按如下图3、图4、图5步骤添加文件;
- 在工程文件中添加:
- 在System文件中添加:
- 在Time_task中添加Inc和Src用来存放相关的.c和.h文件:
- 新建.c和.h文件并且导入工程
- 如下图6所示新建C文件,在名字后面加.c即可:
- 如下图7所示,类似的新建H文件,保存时在名字后面加.h即可:
- 如下图8所示,在工程新建一个文件夹来存放c文件和h文件:
- 如下图9所示在新建的工程文件夹中添加C文件
注:其中文件夹和文件都可以拖动换位置
- 类似的如下图10所示新建的工程文件夹中添加H文件
- 头文件路径添加
我们在引入新的头文件的时候,工程要将头文件的路径包含进来,不然在编译的时候会报错,如图11所示,所以我们按照图12中的步骤将头文件路径添加进来;以后在新添加头文件时候有这个错误就知道,大概率是忘记添加新的头文件路径了;
到这里为止,新的C文件和H文件我们已经添加完成,下面介绍我们的程序内容部分
- 程序设计-time_task.h
- 如下图13所示,在time_task.h文件中首先添加头文件以及预处理指令
预处理指令在多个C文件同时引用时候判断是否已经定义,若没有才会引用,确保只会被定义一次;
main.h里面引用了大部分的公用头文件,引用main.h就可以进行后面内容的编写了
Keil编译器每个文件都需要多一行空行,不然会报错(如果好奇原因,可以直接搜索c语言最后一行报错)
#ifndef __TIME_TASK_H
#define __TIME_TASK_H
#include "main.h"
#include <stdbool.h>
#endif
-
后面的部分我们会涉及到结构体和共用体的简单应用,如果你刚开始学习,对结构体还不是很熟悉,那么我们这一次刚好稍微熟悉一下;这里的简单介绍可能不足以有深刻的理解,所以可以通过下面的传送门去详细学习之后再看会变得简单很多;
如下图14所示,我们先定义一个位域结构体(Timer_Bit),结构体里面八个变量总共占用一个字节的长度,每个变量的冒号后面表示占几位,我们这里(bit0 ~ bit7)分别表示这个字节的(0 ~ 7)位,方便我们后面对某个单字节变量位操作;
传送门:C位域介绍1以及C位域介绍2;
然后我们又声明定义了一个共用体(Time_Char_Field),共用体里面不同类型的变量公用同一片内存,也就是说改变其中一个变量,其他的也会改变,我们这里定义了一个八位变量byte,以及用上面的位域结构体定义了一个结构体变量field,也是八位,这样我们对共用体定义的八位变量既可以通过byte对整体操作(主要为了后面可以将标志位整体清零)又可以通过field.bitx对单独某一位操作;
传送门:C共用体
/*声明定义一种结构体类型,将连续的8位定义为8个变量,用于单独使用一个字节的某一位*/
typedef struct
{
uint8_t bit0: 1;//定义变量bit0占用一位,冒号后面代表这个变量占用长度
uint8_t bit1: 1;
uint8_t bit2: 1;
uint8_t bit3: 1;
uint8_t bit4: 1;
uint8_t bit5: 1;
uint8_t bit6: 1;
uint8_t bit7: 1;
} Timer_Bit;
/*声明定义一个共用体,其中的不同类型变量共用同一段内存*/
typedef union
{
unsigned char byte; //定义一个8位变量,用于一次性修改这8位的数据
Timer_Bit field;//定义一个Timer_Bit类型结构体变量,将一个字节分为一位一个变量
} Time_Char_Field;
- 如下图15所示,定义一个关于我们的系统时间管理的结构体,结构体成员介绍也在注释里:
- Tick_Base_Flag,定时器中断时置1,也就是我们上一章中配置的5ms置位一次;
- Tick_Base_Count,记录总共走过多长时间;
- SysTimeStatus,是后面每个分时任务是否执行的判断标志;
传送门:C结构体
/*声明且定义一种结构体类型,包含一个计数值,和一个8位的共用体,并且可以使用别名Timer_Struct替代*/
typedef struct
{
uint8_t Tick_Base_Flag; //最小时基标志位,定时器中断中置位
uint32_t Tick_Base_Count;//定时器中断的次数计数值
Time_Char_Field SysTimeStatus; //定义共用体SysTimeStatus,保存
} Timer_Struct;
- 如下图16所示使用宏定义,将上面结构体变量中SysTimeStatus的每一位都用一个简短易懂的名字来替换,方便我们后面使用;从前面的名字就可以看出其含义,如bSysTime5Ms表示到达5ms了,则把这个变量置1,保存这个状态;
/*宏定义,将后面一长串的结构体调用替换为前面简短易懂的别名*/
/*每一位保存系统时基变量,作为任务执行的系统时基判断依据*/
#define bSysTime5Ms gTimer_Base.SysTimeStatus.field.bit0
#define bSysTime10Ms gTimer_Base.SysTimeStatus.field.bit1
#define bSysTime50Ms gTimer_Base.SysTimeStatus.field.bit2
#define bSysTime100Ms gTimer_Base.SysTimeStatus.field.bit3
#define bSysTime200Ms gTimer_Base.SysTimeStatus.field.bit4
#define bSysTime300Ms gTimer_Base.SysTimeStatus.field.bit5
#define bSysTime500Ms gTimer_Base.SysTimeStatus.field.bit6
#define bSysTime1S gTimer_Base.SysTimeStatus.field.bit7
- 我们最后还需要一个枚举变量,如下图17用来表示变量的置位与复位,TIMER_RESET表示0,TIMER_SET表示1,相比于直接使用0和1,这样会让我们代码更易读;
传送门:C枚举(enum)
/*声明定义一个枚举类型,相当于给0和1起俩个与定时相关的名字,用于上面时基变量的赋值与判断*/
typedef enum
{
TIMER_RESET = 0U,//TIMER_RESET代表0,U代表无符号类型
TIMER_SET = 1,//这里 = 1 可以不写,枚举里面变量不写会跟着上一个变量加一
} TimerStatus;
- 这里将我们后面time_task.c文件中定义的变量以及函数声明,其他引用time_task.h的文件就可以访问这些变量以及函数;
/*声明可以被外部调用的函数*/
void Tick_Base_Get(void);
bool SysTick_Generate(void);
void Task_5ms_Processing(void);
void Task_10ms_Processing(void);
void Task_50ms_Processing(void);
void Task_100ms_Processing(void);
void Task_200ms_Processing(void);
void Task_300ms_Processing(void);
void Task_500ms_Processing(void);
void Task_1s_Processing(void);
void Function_5ms(void);
void Function_10ms(void);
void Function_50ms(void);
void Function_100ms(void);
void Function_200ms(void);
void Function_300ms(void);
void Function_500ms(void);
void Function_1s(void);
到目前为止我们整个分时任务系统的对象就建立好了,下面我们开始介绍C文件里面函数的封装和应用
- 程序设计-time_task.c文件
- 如下图19我们需要引入一些必要的头文件,然后我们又用这个结构体类型定义了一个结构体变量,gTimer_Base,它就是之后我们实际访问的对象;
#include "time_task.h"
Timer_Struct gTimer_Base = {0, 0, 0}; //定义一个Timer_Struct类型的结构体变量
-
那么按顺序我们先介绍第一个函数,如下图所示,这个函数的主要功能是在被调用时让时基标志位累加,所以我们会在提供时基的地方调用它,
-
如下图20所示,这个函数在被调用的时候时基标志位会被累加,每加一次用来表示刚才经过了一个最小时基;在我们的程序中,我们把这个函数在上节课讲述的5ms中断回调函数中调用,也就是说每过5ms就会累加一次;
/*产生我们需要的最小时基的地方运行该函数*/
void Tick_Base_Get(void)
{
gTimer_Base.Tick_Base_Flag ++;//
}
-
如下图21所示,SysTick_Generate()这个函数,主要为了通过判断结构体变量gTimer_Base中的俩个成员变量:最小时基标志位gTimer_Base.Tick_Base_Flag及其时间累加值gTimer_Base.Tick_Base_Count,得到每个时间段的标志位:bSysTime5Ms等;
- 我们这个函数是bool类型,所以我们这个函数会有true(正确)或者false(错误)这样的返回值;
- 那么我们分别判断如果gTimer_Base.Tick_Base_Flag大于1,则返回false,如果gTimer_Base.Tick_Base_Flag等于1,则最下面返回了true;原因:我们上面说到Tick_Base_Get()函数在定时器中断函数中被调用,也就是说每5ms调用一次,则gTimer_Base.Tick_Base_Flag ++;每5ms执行一次,下图21中的函数会在while(1)循环中一直调用,所以当他判断标志位为一的时候就会清零进而执行后面的任务;但是如果我们后面任务执行时间太长超过了5ms,那么gTimer_Base.Tick_Base_Flag就会超过1,此时就说明我们的最小时基大小设置太小,则返回错误(false)。
/*产生不同的时基*/
bool SysTick_Generate(void)
{
if(gTimer_Base.Tick_Base_Flag > 1) //判断有没有到达新的时基周期
{
return false;
}
if(gTimer_Base.Tick_Base_Flag == 1)
{
gTimer_Base.Tick_Base_Flag = 0;//标志位置零
gTimer_Base.Tick_Base_Count += 5;//每次进入中断代表增加5ms
bSysTime5Ms = TIMER_SET; //5ms临时变量置位
if (0 == (gTimer_Base.Tick_Base_Count % 10)){ //判断到达10ms,时基计数值为10的倍数
bSysTime10Ms = TIMER_SET; //10ms临时变量置位
}
if (0 == (gTimer_Base.Tick_Base_Count % 50)){//判断到达50ms,时基计数值为50的倍数
bSysTime50Ms = TIMER_SET; //50ms时基临时变量置位
}
if (0 == (gTimer_Base.Tick_Base_Count % 100)){//判断到达100ms,时基计数值为100的倍数
bSysTime100Ms = TIMER_SET; //100ms时基临时变量置位
}
if (0 == (gTimer_Base.Tick_Base_Count % 200)){//判断到达200ms,时基计数值为200的倍数
bSysTime200Ms = TIMER_SET; //200ms时基临时变量置位
}
if (0 == (gTimer_Base.Tick_Base_Count % 300)){//判断到达300ms,时基计数值为300的倍数
bSysTime300Ms = TIMER_SET; //300ms时基临时变量置位
}
if (0 == (gTimer_Base.Tick_Base_Count % 500)){//判断到达500ms,时基计数值为500的倍数
bSysTime500Ms = TIMER_SET; //500ms时基临时变量置位
}
if (0 == gTimer_Base.Tick_Base_Count % 1000){//判断到达1s,时基计数值为1000的倍数
gTimer_Base.Tick_Base_Count = 0; //时基计数值清零
bSysTime1S = TIMER_SET; //1s时基临时变量置位
}
}
return true;
}
- 紧接着我们介绍下一组函数,如下图22所示,在这里的函数中我们通过判断上述各个时基标志位来执行对应周期的功能函数,这些功能函数后面介绍;
/*------------------------------------
任务处理
每个时间的任务处理
------------------------------------*/
void Task_5ms_Processing(void)
{
if(bSysTime5Ms == TIMER_SET) //判断系统时基标志位
{
Function_5ms(); //执行我们5ms周期的函数内容
bSysTime5Ms = 0; //将对应的系统时基标志位清零
}
}
void Task_10ms_Processing(void)
{
if(bSysTime10Ms == TIMER_SET)
{
Function_10ms();
bSysTime10Ms = 0;
}
}
void Task_50ms_Processing(void)
{
if(bSysTime50Ms == TIMER_SET)
{
Function_50ms();
bSysTime50Ms = 0;
}
}
void Task_100ms_Processing(void)
{
if(bSysTime100Ms == TIMER_SET)
{
Function_100ms();
bSysTime100Ms = 0;
}
}
void Task_200ms_Processing(void)
{
if(bSysTime200Ms == TIMER_SET)
{
Function_200ms();
bSysTime200Ms = 0;
}
}
void Task_300ms_Processing(void)
{
if(bSysTime300Ms == TIMER_SET)
{
Function_300ms();
bSysTime300Ms = 0;
}
}
void Task_500ms_Processing(void)
{
if(bSysTime500Ms == TIMER_SET)
{
Function_500ms();
bSysTime500Ms = 0;
}
}
void Task_1s_Processing(void)
{
if(bSysTime1S == TIMER_SET)
{
Function_1s();
bSysTime1S = 0;
}
}
- 下面介绍上面任务函数调用的功能函数部分,如图23,这里我们应用了弱定义来定义这些函数,弱定义的作用是,允许其他地方重新定义该函数,如果其他地方重定义了,则重定义的函数会被调用,如果没有重定义,则调用我们这里弱定义的函数;
因为我们这里只是给其他地方留出接口,本次实验我们会在main.c里面重定义这些函数,所以我们这里的弱定义函数里面没有内容。
/*------------------------------------
弱定义函数,用户可重定义
------------------------------------*/
__weak void Function_5ms(void){}
__weak void Function_10ms(void){}
__weak void Function_50ms(void){}
__weak void Function_100ms(void){}
__weak void Function_200ms(void){}
__weak void Function_300ms(void){}
__weak void Function_500ms(void){}
__weak void Function_1s(void){}
time_task.c我们就介绍完了,下面我们介绍他在我们工程中的应用
- 程序设计-main.c文件
- 如下图24所示定时器中断回调函数中调用最小时基获取函数,从而每5ms将时基标志位置位,为后面time_task.c里的函数做时基判断依据;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
Tick_Base_Get();
}
- 如下图25,紧接着我们继续写入我们需要的时间周期对应的功能函数,也就是将我们前面提到的弱定义功能函数重新定义,并写入我们要在对应周期执行的内容,这样我们重定义的功能函数就会被各个周期的任务处理函数(如:Task_5ms_Processing())调用;
这里我们暂时只使用了5ms和50ms周期的功能函数,其他周期的也是一样的,可以看到每5ms采集一次ADC,前面介绍我们总共需要采集10次,所以50ms的时候我们将采集的数据打印出来;
void Function_5ms(void)
{
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)ADC_Value,50);//启动DMA搬运ADC
}
void Function_50ms(void)
{
ADC1_Value_average();
printf("%d,%d,%d,%d,%d\r\n",left_x,left_y,right_x,right_y,battery);
}
- 最后,如下图26所示main.c中main函数的while(1)中调用应用函数,分别是各个任务时基产生的函数,以及5ms,50ms执行一次的任务函数(其他周期的函数调用模式一样);这样所有任务就会按照自己的时间周期运行,没有了那么多的延时等待;
SysTick_Generate();//获得各个的系统时基
Task_5ms_Processing();
Task_50ms_Processing();
OK,这部程序我们就介绍完了,最后我们编译烧录后观察结果
- 观察现象,如图27,ADC采集数据成功打印出来
四、 结语
- 这一章的发布我比较纠结,我不擅长讲这个,我会担心不能把这一部分讲清楚,所以如果有没有写清楚,或者啰嗦的地方,欢迎指出,我看到会想办法改进,谢谢各位。
- 这部分内容呢是我们整体程序的主要框架,后面我们的其他内容也会采用这种形式将各种功能添加到这个框架中来,希望对你有所帮助。