单片机程序流程图和程序清单:单片机软件开发
单片机程序流程图和程序清单:单片机软件开发一。介绍任务。进入正题!!!一。首先简单介绍一下“基于时间片的分时多任务”。顾名思义“时间片”就是把单片机执行的函数或任务,按划定的时间片来执行。比如一个工程有按键,有触摸屏通讯,有LED运行指示。可以把他们三个分别当成3个任务,并且每个任务的运行间隔时间也可以自定义。比如按键扫描一般 10ms执行一次就行了。通讯 20ms执行一次。运行指示可以 250ms执行一次。这样就在一个主循环里有了3个任务,而且每个任务执行的时间间隔都不一样,可以充分利用CPU硬件资源,让CPU的资源尽量少浪费在无用的循环里。那么这个时间是依据什么来的呢?我们一般是用一个定时器,每1ms中断一次,也就是产生一个任务节拍。(下文提到的所有任务节拍都是1ms)每次中断都有一个或多个全局变量加1,根据这个全局变量的值我们就可以知道任务延时情况。/*==================================更改
单片机软件开发,实时系统和分时多任务,看完你就懂了
///插播一条:我自己在今年年初录制了一套还比较系统的入门单片机教程,想要的同学找我拿就行了免費的,私信我就可以哦~点我头像黑色字体加我地球呺也能领取哦。最近比较闲,带做毕设,带学生参加省级或以上比赛///
说明:
作者本人虽然岁数不小,但却是初入此行的新人,此篇文章只适合正在学习的准程序员,或初入行业的新人。当然如果有大牛路过,欢迎指点。
单片机软件开发,如果不用实时系统的话,那么“基于时间片的分时多任务”一定是会用到的。“时间片的分时多任务”看似简单,其实还是有几个方面需要注意的,以下我就根据自己的了解和实际应用经验来讲解一下相关代码。虽然我没有把完整的代码贴出来,但以下的代码块都是从实际的应用中摘出来的。意思就是贴出的代码块都是经过实际验证的。
一。首先简单介绍一下“基于时间片的分时多任务”。顾名思义“时间片”就是把单片机执行的函数或任务,按划定的时间片来执行。比如一个工程有按键,有触摸屏通讯,有LED运行指示。可以把他们三个分别当成3个任务,并且每个任务的运行间隔时间也可以自定义。比如按键扫描一般 10ms执行一次就行了。通讯 20ms执行一次。运行指示可以 250ms执行一次。这样就在一个主循环里有了3个任务,而且每个任务执行的时间间隔都不一样,可以充分利用CPU硬件资源,让CPU的资源尽量少浪费在无用的循环里。
那么这个时间是依据什么来的呢?我们一般是用一个定时器,每1ms中断一次,也就是产生一个任务节拍。(下文提到的所有任务节拍都是1ms)每次中断都有一个或多个全局变量加1,根据这个全局变量的值我们就可以知道任务延时情况。
/*==================================更改分割线=======================================之前这篇笔记写得有些杂乱,我自己都看不下去。在这五一小长假的最后一天我就收拾起自己的懒散,重新认真编辑一次,其中的代码部分也是优化过了的。任务除了基本的分时运行功能外,还可以在运行中暂停或恢复某个任务,以及统计某个任务的运行时间,以ms为单位。*/
进入正题!!!
一。介绍任务。
//代码文件 task.h#define TASK_MAX 10u //最大任务数量#define TASK_BEAT_MS 1u //任务节拍时间(ms)#define TASK_BATE_MAX (u32)40050 //最大任务节拍//任务函数类型typedefvoid(*TASK)(void*p_arg);
//任务结构体typedefstruct{
s8id;//任务IDu8RunTime;//任务运行时间统计(1统计0不统计)TASKTaskAddr;//任务函数地址void*p_arg;//任务参数vu32TaskBeat;//任务节拍u32TaskDelay;//延迟节拍}TaskStruct;
externvu16TaskBeat_ms;//系统定时,毫秒(用作任务片延时)externTaskStructtask[TASK_MAX];//任务结构体数组
以上代码块就是把任务相关的一些变量以及函数指针放在一个结构体里,之后对任务的运行以及管理只需要传递结构体指针就行了。
二。任务管理
再来看看任务管理函数,以及统计任务运行时间函数。
//代码文件 task.c/*任务函数说明:任务管理函数参数: *p_task 任务结构体指针返回值:无*/voidTask(TaskStruct*p_task){
if(p_task==NULL)//指针错误return;
if(p_task->TaskAddr==NULL)//函数指针为空return;
if(p_task->id<0)//此任务暂停return;
if(p_task->TaskBeat>p_task->TaskDelay)//任务节拍大于上次节拍{
p_task->TaskBeat=1u;
if(p_task->RunTime==0)//不统计任务执行时间p_task->TaskAddr(p_task->p_arg);//任务函数else//统计任务执行时间{
TaskRunTime(p_task->id 1);//执行任务前节拍p_task->TaskAddr(p_task->p_arg);//任务函数TaskRunTime(p_task->id 2);//执行任务后节拍}
}}
/*统计任务运行时间说明:把统计的任务时间通过串口输出参数: task_id 任务ID(不可重复) mark 用于标记任务运行前后时间 1任务运行前 2任务运行后返回值:无*/staticvoidTaskRunTime(s8task_id u32mark){
staticu32RunBeat[3];//记录任务节拍
if(mark==1)
{
RunBeat[0]=TaskBeat_ms;//任务运行前任务节拍return;
}
elseif(mark==2)
{
RunBeat[1]=TaskBeat_ms;//任务运行后任务节拍if(RunBeat[1]>=RunBeat[0])
RunBeat[2]=RunBeat[1]-RunBeat[0];
else
RunBeat[2]=(TASK_BATE_MAX-RunBeat[1]) RunBeat[0];
//发送任务运行时间printf("id:d tim[u] bea:%u\r\n" task_id RunBeat[2] TaskBeat_ms);
}}
以上的任务管理函数,前 2个 if判断是排错,第 3个 if是一个任务暂停功能,如果把某个任务的 id变量赋值为负数,此任务就不会执行。第 4个 if判断才是真的得任务运行判断。具体的这些参数下面再介绍。任务统计函数其实就是在任务运行前和运行后都统计一次系统节拍,并在第二次统计完之后算出此任务的运行时间,然后通过串口输出。
三。定时器中断
定时器中断函数,用作任务切换。定时器配置为 1ms中断一次。
//代码文件 timer.c/*普通定时器 TIM16计时模式,中断服务函数说明:用作任务时钟片计时*/voidTIM1_UP_TIM16_IRQHandler(void){
vu32i;
if(TIM_GetITStatus(TIM16 TIM_IT_Update)!=RESET)//定時器 TIM16中断{
TIM_ClearITPendingBit(TIM16 TIM_IT_Update);//清除 TIM16中断标志
TaskBeat_ms ;//所有任务节拍(用作统计任务运行时间)for(i=0;i<TASK_MAX;i )
{
task[i].TaskBeat ;//单个任务节拍}
for(i=0;i<MAX_PORTS;i )
{
USARTx_Time_ms[i] ;//串口通信计时if(USARTx_Time_ms[i]>=200)
{
USARTx_Time_ms[i]=100;
}
}
}}
以上代码块,定时器中断函数里的第 2个 for循环和任务函数无关,是串口通信使用的,大家可以忽略掉。
到此为止,我们就把“单片机基于时间片的多任务”主要部分介绍完毕。下面我们要开始结合上面的各个代码块,来介绍 main.c文件里的任务结构体数组初始化和主循环里的任务运行说明。
四。 main.c
//代码文件 main.c/*任务结构体初始化任务结构体数据说明任务ID,任务时间统计,任务函数,任务参数,任务节拍,延时节拍*/TaskStructtask[TASK_MAX]={
{0 0 KEY_Scan NULL 0 4} //按键扫描及执行{1 0 HMI_Receive NULL 3 29} //接收HMI报文{2 0 Status NULL 0 20} //设备状态{3 0 DataDispose NULL 19 20} //数据处理{4 0 Angle_RxTx NULL 39 100} //角度传感器{5 0}
{6 0}
{7 0}
{8 0 Debug NULL 0 100} //Debug调试{9 0 Runing NULL 0 250}//运行指示};
/*Debug调试,具体运行时间视输出数据而定。实际开发完后请注释掉此任务。说明:用来调试程序,可以输出变量,查看其具体变化参数: *p_arg没用返回值:无*/voidDebug(void*p_arg){
printf("\r\n");}
/*主函数说明:参数:无返回值:无*/intmain(void){
Hard_Init();//硬件初始化Hard_Set();//外设设置
printf("编译日期:%s.\r\n" __DATE__);
printf("编译时间:%s.\r\n" __TIME__);
while(1)
{
Task(&task[0]);//按键扫描Task(&task[1]);//接收HMI报文Task(&task[2]);//设备状态Task(&task[3]);//数据处理Task(&task[4]);//角度传感器// Task(&task[8]); //Debug调试Task(&task[9]);//运行指示}}
/*运行指示,运行时间小于1ms*/voidRuning(void*p_arg){
staticu8run_num=0;
IWDG_Feed();//喂狗,独立看门狗LED_RUN=!LED_RUN;//LED运行指示run_num ;
if(run_num>1)
{
run_num=0;
CPU_GetTemp();//获取CPU温度}}
五。细说
现在我们要开始仔细讲解以上代码的变量和一些逻辑判断的具体说明以及作用。
1:先来看上面的任务结构体数组定义,这个要结合最上面代码块的任务结构体声明来看。
因为我们在前面 task.h头文件,通过宏 TASK_MAX把最大任务定义为 10,所以在这个结构体数组我们初始化了10个任务结构体变量。
1.1:任务结构体里的第1个成员是任务 id,这个任务 id我们手动初始化,从0开始往后累加,不可以重复。假如任务2在系统刚上电时不需要运行,我们可以把他的 id初始化为-2,等到他可以运行的时候再赋值为2。
1.2:结构体第2个成员变量是任务运行时间统计:0不统计,1统计。时间单位为ms。我们结合第二段代码,任务管理函数来看,如果这个参数为0就直接运行任务函数,非0就会在任务运行前、后各统计1次任务节拍。在第2次统计完节拍之后算出任务运行时占用的节拍数,并且通过串口输出。(1节拍,就是1ms)这个功能一般是在前期调试的时候使用,清楚自己的任务运行时间,在实际发布的时候这个变量都初始化为0,不然任务实际运行时间才一两ms,可是通过串口输出统计结果就要十几二十毫秒,会拖慢其他任务的实时性。
任务运行时间统计代码块:
//代码文件 task.c
if(p_task->RunTime==0)//不统计任务执行时间p_task->TaskAddr(p_task->p_arg);//任务函数else//统计任务执行时间{
TaskRunTime(p_task->id 1);//执行任务前节拍p_task->TaskAddr(p_task->p_arg);//任务函数TaskRunTime(p_task->id 2);//执行任务后节拍}
1.3:第3个成员就是函数指针,最终任务管理函数通过这个函数指针,来调用相应的任务函数。所有的任务函数都是这种类型。结合前面结构体声明来看,这是一个 void类型函数也就是没有返回值,还带一个 void指针参数,关于这个指针其实一般是用不到的,可以忽略掉他,如果有警告的话在任务函数里可以 p_arg = p_arg消除警告。
//代码文件 task.h//任务函数类型typedefvoid(*TASK)(void*p_arg);
1.4:第4个成员是任务函数的参数,可以看到我们所有的结构体变量都把这个参数初始化为一个 NULL指针。
1.5:第5个成员变量是任务节拍 TaskBeat。每个任务都有一个任务节拍变量,在定时器中断里以每ms加1的方式一直累加。如下代码段所示。
//代码文件 timer.c
TaskBeat_ms ;//所有任务节拍(用作统计任务运行时间)for(i=0;i<TASK_MAX;i )
{
task[i].TaskBeat ;//单个任务节拍}
1.6:终于到最后一个成员变量了,他就是任务延时节拍。就是说这个任务间隔多少个任务节拍运行一次。也就是我们这个任务管理函数最基本的功能。因为我们的目的就让每个任务函数以他合理的频率来分时运行。看 main.c文件我们把每个任务的延时节拍都初始化为不同的值。最小 4ms,最大 250ms。实际延时节拍都是根据实际项目需求来定的。
在任务管理函数里,我们每次都判断任务的运行节拍是否大于延时节拍。如果条件为真,说明这个任务的延时时间完了,他可以运行。进入 if语句后,第一步我们把当前任务的运行节拍复位,赋值为1。目的是让他运行完此次任务后开始新一轮的延时。第二步执行任务函数指针,也就是执行具体的任务。
//代码文件 task.c
if(p_task->TaskBeat>p_task->TaskDelay)//任务节拍大于上次节拍{
p_task->TaskBeat=1u;
if(p_task->RunTime==0)//不统计任务执行时间p_task->TaskAddr(p_task->p_arg);//任务函数else//统计任务执行时间{
TaskRunTime(p_task->id 1);//执行任务前节拍p_task->TaskAddr(p_task->p_arg);//任务函数TaskRunTime(p_task->id 2);//执行任务后节拍}
}
2:进入 main函数里的主循环,如下所示。我们看到主循环里是一直调用任务管理函数 Task()每个 Task的参数都不同。其实就是任务结构体数组里的不同结构体变量指针。根据实际参数,最终就可以调用不同的任务函数。
其实我们完全可以在主循环里只调用一次 Task()然后在 Task里遍历结构体数组里的结构体变量,来判断哪个任务可以运行,这样代码更加简洁。但是如果这样做的话, Task会把整个结构体数组都遍历一遍,有些不需要运行的任务就需要我们手动修改结构体数组初始化值,而且我们不能很直观的看出哪个任务可以运行,哪个任务不可以运行。有些我们只在调试时使用的任务,比如 Debug(void *p_arg)任务。在调试时我们用他输出我们要观察的一些变量,但在实际发布时就不需要他了。我们只要在主循环里注释掉调用他的 Task(&task[8]);就行了。而且在主循环里我们可以很直观的看出那些任务可以运行,那些任务不能运行。
//代码文件 main.c
while(1)
{
Task(&task[0]);//按键扫描Task(&task[1]);//接收HMI报文Task(&task[2]);//设备状态Task(&task[3]);//数据处理Task(&task[4]);//角度传感器// Task(&task[8]); //Debug调试Task(&task[9]);//运行指示}
基于时间片的分时任务到此我们就介绍完了,希望能对大家有所帮助。
最后附上一些免费的入门单片机教程 私信我就行