我们会创建两个任务,并让这两个任务不断地切换,任务的主体都是让一个变量按照一定的频率翻转,通过 KEIL 的软件仿真功能,在逻辑分析仪中观察变量的波形变化,最终的波形图具体见任务轮流切换波形图。
其实,图任务轮流切换波形图 的波形图的效果,并不是真正的多任务系统中任务切换的效果图,这个效果其实可以完全由裸机代码来实现,具体见代码清单: 任务-1。
1
4 uint32_t flag1;
5 uint32_t flag2;
6
7
8
9 void delay( uint32_t count )
10 {
11 for (; count!=0; count--);
12 }
13
14 int main(void)
15 {
16
17 for (;;)
18 {
19 flag1 = 1;
20 delay( 100 );
21 flag1 = 0;
22 delay( 100 );
23
24 flag2 = 1;
25 delay( 100 );
26 flag2 = 0;
27 delay( 100 );
28 }
29 }
在多任务系统中,两个任务不断切换的效果图应该像多任务系统任务切换波形图 所示那样,即两个变量的波形是完全一样的,就好像 CPU 在同时干两件事一样,这才是多任务的意义。虽然两者的波形图一样,但是,代码的实现方式是完全不一样的,原来的顺序执行变成了任务的主动切换,这是根本区别。这章只是开始,我们先掌握好任务是如何切换,在后面章节中,我们会陆续的完善功能代码,加入系统调度,实现真正的多任务。
目标:必须要学会创建任务,并重点掌握任务是如何切换的。
6.2 什么是任务
在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。任务的大概形式具体见代码清单: 任务-2。
1 void task_entry (void *parg)
2 {
3
4 for (;;)
5 {
6
7 }
8 }
6.3 创建任务
6.3.1 定义任务栈
我们先回想下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生。那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址发哪里。如果只是单纯的裸机编程,它们放哪里我们不用管,但是如果要写一个 RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的。在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小由启动文件里面的代码配置,具体见代码清单: 任务-3 ,最后由 C 库函数 _main 进行初始化。它们在 RAM 空间里面的大概分布具体见。
1 Stack_Size EQU 0x00000400
2
3 AREA STACK, NOINIT, READWRITE, ALIGN=3
4 Stack_Mem SPACE Stack_Size
5 __initial_sp
打开上节课我们自己组建的工程文件。
把这个cpu.h复制 粘贴到我们自己的工程文件User下。
把这个os_type.h也复制到对应目录下。
把cpu.h和os_type.h添加进来。
最终把所有东西都加进来。
在app.c中写下这样一段
#include "os.h"
#include "ARMCM4.h"
#define TASK1_STK_SIZE 20
#define TASK2_STK_SIZE 20
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
这里我们报了警告:…/User/app.c(11): warning: unused variable ‘Task2Stk’ [-Wunused-variable]
意思是未使用的变量。我们不用去管这个警告。
我们这里报了13个警告
我们一个一个来解决
终于我们找到了原因。是TM得用5的编译器。可惜可惜!
但是,在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组。这些一个个的任务栈也是存在于 RAM 中,能够使用的最大的栈也是由代码清单: 任务-3 中的 Stack_Size 决定。只是多任务系统中任务的栈就是在统一的一个栈空间里面分配好一个个独立的房间,每个任务只能使用各自的房间,而裸机系统中需要使用栈的时候则可以天马行空,随便在栈里面找个空闲的空间使用,大概的区别具体见。
本章我们要实现两个变量按照一定的频率轮流的翻转,需要两个任务来实现,那么就需要定义两个任务栈,具体见代码清单: 任务-4。在多任务系统中,有多少个任务就需要定义多少个任务栈。
1 #define TASK1_STK_SIZE 128 (1)
2 #define TASK2_STK_SIZE 128
3 4
static CPU_STK Task1Stk[TASK1_STK_SIZE];(2)
5 static CPU_STK Task2Stk[TASK2_STK_SIZE];
• 代码清单: 任务-4(1)任务栈的大小由宏定义控制,在 μC/OS-III 中,空闲任务的栈最小应该大于 128,那么我们这里的任务的栈也暂且配置为 128。
• 代码清单: 任务-4(2)任务栈其实就是一个预先定义好的全局数据,数据类型为 CPU_STK。在 μC/OS-III 中,凡是涉及数据类型的地方, μC/OS-II 都会将标准的 C 数据类型用 typedef重新取一个类型名,命名方式则采用见名之义的方式命名且统统大写。凡是与 CPU 类型相关的数据类型则统一在 cpu.h 中定义,与 OS 相关的数据类型则在 os_type.h 定义。 CPU_STK
就是与 CPU 相关的数据类型,则在 cpu.h 中定义,具体见代码清单: 任务-5。 cpu.h 首次使用则需要自行在 μC-CPU 文件夹中新建并添加到工程的 μC/CPU 这个组中。 代码清单: 任务-5中除了 CPU_STK 外,其他数据类型重定义是本章后面内容需要使用到,这里统一贴出来,后面将不再赘述。
1 #ifndef CPU_H
2 #define CPU_H
3
4 typedef unsigned short CPU_INT16U;
5 typedef unsigned int CPU_INT32U;
6 typedef unsigned char CPU_INT08U;
7
8 typedef CPU_INT32U CPU_ADDR;
9
10
11 typedef CPU_INT32U CPU_STK;
12 typedef CPU_ADDR CPU_STK_SIZE;
13
14 typedef volatile CPU_INT32U CPU_REG32;
15
16 #endif
6.3.3 定义任务控制块 TCB
在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块 TCB(Task ControlBlock),这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个 TCB 来实现。 TCB 是一个新的数据类型,在 os.h(os.h 第一次使用需要自行在文件夹 μC/OSIIISource 中新建并添加到工程的 μC/OS-III Source 组)这个头文件中声明,有关 TCB 具体的声明
见代码清单: 任务-7 ,使用它可以为每个任务都定义一个 TCB 实体。
任务控制块的定义在os.h中。