μC/OSIII学习day11
介绍
内存管理,中断管理,CPU利用率及栈检测统计
# 内存管理
# 内存管理的基本概念
在计算系统中,变量,中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算.通常存储空间可以分为两种:内部存储空间和外部存储空间.内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的RAM(随机存储器),或计算机的内存;而外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为计算机的硬盘.在这一章中我们主要讨论内部存储空间(RAM)的管理——内存管理.
在嵌入式系统设计中,内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率.静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反.
μC/OS的内存管理是采用内存池的方式进行管理,也就是创建一个内存池,静态划分一大块连续空间作为内存管理的空间,里面划分为很多个内存块,我们在使用的时候就从这个内存池中获取一个内存块,使用完毕的时候用户可以将其放回内存池中,这样子就不会导致内存碎片的产生.
μC/OS内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一,主要包括内存池的创建,分配以及释放.
在计算机中我们可以用C语言标准库中的
malloc()
和free()
这两个函数动态的分配内存和释放内存.但是,在嵌入式实时操作系统中,调用malloc()
和free()
却是危险的,原因有以下几点:- 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的RAM不足.
- 它们的实现可能非常的复杂,占据了相当大的一块代码空间.
- 他们几乎都不是安全的.
- 它们并不是确定的,每次调用这些函数执行的时间可能都不一样.
- 它们有可能产生碎片.
- 这两个函数会使得链接器配置得复杂.
- 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为debug的灾难.
在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制.所有的内存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所有的系统栈的管理,都由用户自己管理.
同时,在嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间必须是确定的.一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面,而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定.
在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存,而在分配中随着内存不断被分配和释放,整个系统内存区域会产生越来越多的碎片,因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已经无法分配到合适的内存了,导致系统瘫痪.其实系统中实际是还有内存的,但是因为小块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避免这种情况的出现.所以μC/OS提供的内存分配算法是只允许用户分配固定大小的内存块,当使用完成就将其放回内存池中,这样的分配效率极高,时间复杂度是O(1),也就是一个固定的时间常数,并不会因为系统内存的多少而增加遍历内存块列表的时间,并且还不会导致内存碎片的出现,但是这样的内存分配机制会导致内存利用率的下降以及申请内存大小的限制.
# 内存管理的运作机制
内存池(Memory Pool)是一种用于分配大量大小相同的内存对象的技术,它可以极大加快内存分配/释放的速度.
在系统编译的时候,编译器就静态划分了一个大数组作为系统的内存池,然后在初始化的时候将其分成大小相等的多个内存块,内存块直接通过链表连接起来(此链表也称为空闲内存块列表).每次分配的时候,从空闲内存块列表中取出表头上第一个内存块,提供给申请者.物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个大小相同的空闲内存块组成.我们必须先创建内存池才能去使用内存池里面的内存块,在创建的时候,我们必须定义一个内存池控制块,然后进行相关初始化,内存控制块的参数包括内存池名称,内存池起始地址,内存块大小,内存块数量等信息,在以后需要从内存池取出内存块或者释放内存块的时候,我们只需根据内存控制块的信息就能很轻易做到 .内存池一旦创建完成,其内部的内存块大小将不能再做调整.
内存控制块数据结构
struct os_mem { OS_OBJ_TYPE Type; //内核对象类型 void *AddrPtr; //内存池的起始地址 CPU_CHAR *NamePtr; //内存池名称 void *FreeListPtr; //空闲内存块列表 OS_MEM_SIZE BlkSize; //内存块大小 OS_MEM_QTY NbrMax; //内存池中内存块的总数量 OS_MEM_QTY NbrFree; //空闲内存块数量 #if OS_CFG_DBG_EN > 0u OS_MEM *DbgPrevPtr; OS_MEM *DbgNextPtr; #endif };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15静态内存示意图
PS:内存池中的内存块是通过单链表连接起来的,类似于消息池,内存池在创建的时候内存块地址是连续的,但是经过多次申请以及释放后,空闲内存块列表的内存块在地址上不一定是连续的.
# 内存管理的应用场景
在使用内存分配前,必须明白在做什么,这样做与其他的方法有什么不同,特别是会产生哪些负面影响,在产品面前,应当选择哪种分配策略.
内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使用大小不等的内存块的场景中使用,当用户需要分配内存时,可以通过操作系统的内存申请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用(heap_1.c的内存管理除外).
例如我们需要定义一个flfloat型数组:flfloatArr[];但是,在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?在很多的情况下,你并不能确定要使用多大的数组,可能为了避免发生错误你就需要把数组定义得足够大.即使你知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少,你又必须重新去修改程序,扩大数组的存储范围.这种分配固定大小的内存分配方法称之为静态内存分配.这种内存分配的方法存在比较严重的缺陷,在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果.
μC/OS将系统静态分配的大数组作为内存池,然后进行内存池的初始化,然后分配固定大小的内存块.
PS:μC/OS也不能很好解决这种问题,因为内存块的大小是固定的,无法解决这种弹性很大的内存需求,只能按照最大的内存块进行分配.但是μC/OS的内存分配能解决内存利用率的问题,在不需要使用内存的时候,将内存释放到内存池中,让其他任务能正常使用该内存块.
# 内存管理函数
函数名称 | 函数作用 |
---|---|
OSMemCreate() | 创建一个内存池 |
OSMemGet() | 申请固定大小的内存块 |
OSMemPut() | 进行内存的释放管理 |
内存池创建函数OSMemCreate()
在使用内存池的时候首先要创建一个内存池,需要用户静态分配一个数组空间作为系统的内存池,且用户还需定义一个内存控制块.创建内存池后,任务才可以通过系统的内存申请、释放函数从内存池中申请或释放内存.
参数 含义 *p_mem 内存池控制块 *p_name 命名内存池 *p_addr 内存池首地址 n_blks 内存块数目 blk_size 内存块大小 *p_err 指向错误类型的指针 错误类型 含义 OS_ERR_ILLEGAL_CREATE_RUN_TIME 正在尝试创建内存分区 OS_ERR_MEM_CREATE_ISR 从ISR调用了此函数 OS_ERR_MEM_INVALID_BLKS 用户指定的块数无效 OS_ERR_MEM_INVALID_P_ADDR 如果为内存指定了无效地址(分区的存储或者块在指针边界) OS_ERR_MEM_INVALID_SIZE 用户指定的块大小无效 OS_ERR_NONE 无错误 void OSMemCreate(OS_MEM *p_mem, //内存池控制块 CPU_CHAR *p_name, //命名内存池 void *p_addr, //内存池首地址 OS_MEM_QTY n_blks, //内存块数目 OS_MEM_SIZE blk_size, //内存块大小(单位:字节) OS_ERR *p_err){ #if OS_CFG_ARG_CHK_EN > 0u CPU_DATA align_msk; #endif OS_MEM_QTY i; OS_MEM_QTY loops; CPU_INT08U *p_blk; void **p_link; //二级指针,存放指针的指针 CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏 #ifdef OS_SAFETY_CRITICAL//如果启用了安全检测 if (p_err == (OS_ERR *)0) //如果错误类型实参为空 { OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数 return; //返回,停止执行 } #endif #ifdef OS_SAFETY_CRITICAL_IEC61508//如果启用了安全关键 if (OSSafetyCriticalStartFlag == DEF_TRUE) { *p_err = OS_ERR_ILLEGAL_CREATE_RUN_TIME;//错误类型为“非法创建内核对象” return; //返回,停止执行 } #endif #if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u//如果启用了中断中非法调用检测 if (OSIntNestingCtr > (OS_NESTING_CTR)0) //如果该函数是在中断中被调用 { *p_err = OS_ERR_MEM_CREATE_ISR; //错误类型为“在中断中创建对象” return; //返回,停止执行 } #endif #if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测 if (p_addr == (void *)0) //如果 p_addr 为空 { *p_err = OS_ERR_MEM_INVALID_P_ADDR; //错误类型为“内存池地址非法” return; //返回,停止执行 } if (n_blks < (OS_MEM_QTY)2) //如果内存池的内存块数目少于 2 { *p_err = OS_ERR_MEM_INVALID_BLKS; //错误类型为“内存块数目非法” return; //返回,停止执行 } if (blk_size <sizeof(void *)) //如果内存块空间小于指针的 { *p_err = OS_ERR_MEM_INVALID_SIZE; //错误类型为“内存空间非法” return; //返回,停止执行 } align_msk = sizeof(void *) - 1u; //开始检查内存地址是否对齐 if (align_msk > 0u) { if (((CPU_ADDR)p_addr & align_msk) != 0u) //如果首地址没对齐 { *p_err = OS_ERR_MEM_INVALID_P_ADDR; //错误类型为“内存池地址非法” return; //返回,停止执行 } if ((blk_size & align_msk) != 0u) //如果内存块地址没对齐 { *p_err = OS_ERR_MEM_INVALID_SIZE; //错误类型为“内存块大小非法” return; //返回,停止执行 } } #endif /* 将空闲内存块串联成一个单向链表 */ p_link = (void **)p_addr; //内存池首地址转为二级指针 p_blk = (CPU_INT08U *)p_addr; //首个内存块地址 loops = n_blks - 1u; for (i = 0u; i < loops; i++) //将内存块逐个串成单向链表 { p_blk += blk_size; //下一内存块地址 *p_link = (void *)p_blk;//在当前内存块保存下一个内存块地址 p_link = (void **)(void *)p_blk;//下一个内存块的地址转为二级指针 } *p_link = (void *)0; //最后一个内存块指向空 OS_CRITICAL_ENTER(); //进入临界段 p_mem->Type = OS_OBJ_TYPE_MEM; //设置对象的类型 p_mem->NamePtr = p_name; //保存内存池的命名 p_mem->AddrPtr = p_addr; //存储内存池的首地址 p_mem->FreeListPtr = p_addr; //初始化空闲内存块池的首地址 p_mem->NbrFree = n_blks; //存储空闲内存块的数目 p_mem->NbrMax = n_blks; //存储内存块的总数目 p_mem->BlkSize = blk_size; //存储内存块的空间大小 #if OS_CFG_DBG_EN > 0u//如果启用了调试代码和变量 OS_MemDbgListAdd(p_mem); //将内存管理对象插入内存管理双向调试列表 #endif OSMemQty++; //内存管理对象数目加 1 OS_CRITICAL_EXIT_NO_SCHED(); //退出临界段(无调度) *p_err = OS_ERR_NONE; //错误类型为“无错误” }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101内存池创建函数的使用实例
OS_MEM mem; //声明内存管理对象 uint8_t ucArray [ 3 ] [ 20 ]; //声明内存池大小 OS_ERR err; /* 创建内存管理对象 mem */ OSMemCreate ((OS_MEM *)&mem, //指向内存管理对象 (CPU_CHAR *)"Mem For Test", //命名内存管理对象 (void *)ucArray, //内存池的首地址 (OS_MEM_QTY )3, //内存池中内存块数目 (OS_MEM_SIZE )20, //内存块的字节数目 (OS_ERR *)&err);
1
2
3
4
5
6
7
8
9
10
11
12内存申请函数OSMemGet()
这个函数用于申请固定大小的内存块,从指定的内存池中分配一个内存块给用户使用,该内存块的大小在内存池初始化的时候就已经决定的.如果内存池中有可用的内存块,则从内存池的空闲内存块列表上取下一个内存块并且返回对应的内存地址;如果内存池中已经没有可用内存块,则返回0与对应的错误代码
OS_ERR_MEM_NO_FREE_BLKS
.参数 含义 *p_mem 内存管理对象 *p_err 指向错误类型的指针 错误类型 含义 OS_ERR_MEM_INVALID_P_MEM 传递了"p_mem"的NULL指针 OS_ERR_MEM_NO_FREE_BLKS 没有更多的可用内存块要分配给调用方 OS_ERR_OBJ_TYPE "p_mem"未指向内存分区 OS_ERR_NONE 无错误 void *OSMemGet (OS_MEM *p_mem, //内存管理对象 OS_ERR *p_err){ void *p_blk; CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏 #ifdef OS_SAFETY_CRITICAL//如果启用了安全检测 if (p_err == (OS_ERR *)0) //如果错误类型实参为空 { OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数 return ((void *)0); //返回 0(有错误),停止执行 } #endif #if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测 if (p_mem == (OS_MEM *)0) //如果 p_mem 为空 { *p_err = OS_ERR_MEM_INVALID_P_MEM; //错误类型为“内存池非法” return ((void *)0); //返回 0(有错误),停止执行 } #endif CPU_CRITICAL_ENTER(); //关中断 if (p_mem->NbrFree == (OS_MEM_QTY)0) //如果没有空闲的内存块 { CPU_CRITICAL_EXIT(); //开中断 *p_err = OS_ERR_MEM_NO_FREE_BLKS; //错误类型为“没有空闲内存块” return ((void *)0); //返回 0(有错误),停止执行 } p_blk = p_mem->FreeListPtr; //如果还有空闲内存块,就获取它 p_mem->FreeListPtr = *(void **)p_blk; //调整空闲内存块指针 p_mem->NbrFree--; //空闲内存块数目减 1 CPU_CRITICAL_EXIT(); //开中断 *p_err = OS_ERR_NONE; //错误类型为“无错误” return (p_blk); //返回获取到的内存块 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36内存释放函数OSMemGet()
嵌入式系统的内存对我们来说是十分珍贵的,任何内存块使用完后都必须被释放,否则会造成内存泄漏,导致系统发生致命错误.μC/OS提供了
OSMemPut()
函数进行内存的释放管理,使用该函数接口时,根据指定的内存控制块对象,将内存块插入内存池的空闲内存块列表中,然后增加该内存池的可用内存块数目.参数 含义 *p_mem 内存管理对象 *p_blk 要退回的内存块 *p_err 指向错位类型的指针 错误类型 含义 void OSMemPut(OS_MEM *p_mem, //内存管理对象 void *p_blk, //要退回的内存块 OS_ERR *p_err){ CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏 #ifdef OS_SAFETY_CRITICAL//如果启用了安全检测 if (p_err == (OS_ERR *)0) //如果错误类型实参为空 { OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数 return; //返回,停止执行 } #endif #if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测 if (p_mem == (OS_MEM *)0) //如果 p_mem 为空 { *p_err = OS_ERR_MEM_INVALID_P_MEM; //错误类型为“内存池非法” return; //返回,停止执行 } if (p_blk == (void *)0) //如果内存块为空 { *p_err = OS_ERR_MEM_INVALID_P_BLK; //错误类型为" 内存块非法" return; //返回,停止执行 } #endif CPU_CRITICAL_ENTER(); //关中断 if (p_mem->NbrFree >= p_mem->NbrMax) //如果内存池已满 { CPU_CRITICAL_EXIT(); //开中断 *p_err = OS_ERR_MEM_FULL; //错误类型为“内存池已满” return; //返回,停止执行 } *(void **)p_blk = p_mem->FreeListPtr; //把内存块插入空闲内存块链表 p_mem->FreeListPtr = p_blk; //内存块退回到链表的最前端 p_mem->NbrFree++; //空闲内存块数目加 1 CPU_CRITICAL_EXIT(); //开中断 *p_err = OS_ERR_NONE; //错误类型为“无错误” }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40内存释放函数使用实例
OS_MEM mem; //声明内存管理对象 OS_ERR err; /* 释放内存块 */ OSMemPut ((OS_MEM *)&mem, //指向内存管理对象 (void *)pMsg, //内存块的首地址 (OS_ERR *)&err);
1
2
3
4
5
6
7
8PS:我们想要使用内存管理相关的函数时,需要将os_cfg.h中的
OS_CFG_MEM_EN
宏定义配置为1;OSMemCreate()
只能在任务级被调用,但是OSMemGet()
和OSMemPut()
可以在中断中被调用.
# 内存管理实验
本次的实验例程采用消息队列进行发送与接收消息,只不过存放消息的地方是在内存块中,在获取完消息的时候,就进行释放内存块,反复使用内存块.
声明内存管理对象和内存分区,定义任务空间栈的大小以及任务栈数组,任务控制块和优先级.
定义任务函数
任务启动函数编写
结果现象
# 中断管理
# 异常与中断的基本概念
异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时进行处理,轻则系统出错,重则会导致系统毁灭性瘫痪.所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环,对于实时系统更是如此.
异常是指任何打断处理器正常执行,并且迫使处理器进入一个由有特权的特殊指令执行的事件.异常通常可以分成两类:
- 同步异常和异步异常.由内部事件(像处理器指令运行产生的事件)引起的异常称为同步异常,例如造成被零除的算术运算引发一个异常,又如在某些处理器体系结构中,对于确定的数据尺寸必须从内存的偶数地址进行读和写操作.从一个奇数内存地址的读或写操作将引起存储器存取一个错误事件并引起一个异常(称为校准异常).
- 异步异常主要是指由于外部异常源产生的异常,是一个由外部硬件装置产生的事件引起的异步异常.同步异常不同于异步异常的地方是事件的来源,同步异常事件是由于执行某些指令而从处理器内部产生的,而异步异常事件的来源是外部硬件装置.例如按下设备某个按钮产生的事件.同步异常与异步异常的区别还在于,同步异常触发后,系统必须立刻进行处理而不能够依然执行原有的程序指令步骤;而异步异常则可以延缓处理甚至是忽略,例如按键中断异常,虽然中断异常触发了,但是系统可以忽略它继续运行(同样也忽略了相应的按键事件).
中断,中断属于异步异常.所谓中断是指中央处理器CPU正在处理某件事的时候,外部发生了某一事件,请求CPU迅速处理,CPU暂时中断当前的工作,转入处理所发生的事件,处理完后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断.
中断能打断任务的运行,无论该任务具有什么样的优先级,因此中断一般用于处理比较紧急的事件,而且只做简单处理,例如标记该事件,在使用μC/OS系统时,一般建议使用信号量,消息或事件标志组等标志中断的发生,将这些内核对象发布给处理任务,处理任务再做具体处理.
通过中断机制,在外设不需要CPU介入时,CPU可以执行其他任务,而当外设需要CPU时通过产生中断信号使CPU立即停止当前任务转而来响应中断请求.这样可以使CPU避免把大量时间耗费在等待,查询外设状态的操作上,因此将大大提高系统实时性以及执行效率.
μC/OS源码中有许多处临界段的地方,临界段虽然保护了关键代码的执行不被打断,但也会影响系统的实时,任何使用了操作系统的中断响应都不会比裸机快.比如,某个时候有一个任务在运行中,并且该任务部分程序将中断屏蔽掉,也就是进入临界段中,这个时候如果有一个紧急的中断事件被触发,这个中断就会被挂起,不能得到及时响应,必须等到中断开启才可以得到响应,如果屏蔽中断时间超过了紧急中断能够容忍的限度,危害是可想而知的.操作系统的中断在某些时候会产生必要的中断延迟,因此调用中断屏蔽函数进入临界段的时候,也需快进快出.
μC/OS的中断管理支持:
开/关中断.
恢复中断.
中断启用.
中断屏蔽.
中断嵌套.
中断延迟发布.
# 中断的介绍
- 与中断相关的硬件可以划分为三类:
- 外设:当外设需要请求CPU时,产生一个中断信号,该信号连接至中断控制器.
- 中断控制器:中断控制器是CPU众多外设中的一个,它一方面接收其他外设中断信号的输入,另一方面,它会发出中断信号给CPU.可以通过对中断控制器编程实现对中断源的优先级,触发方式,打开和关闭源等设置操作.在Cortex-M系列控制器中常用的中断控制器是NVIC(内嵌向量中断控制器Nested Vectored Interrupt Controller).
- CPU:CPU会响应中断源的请求,中断当前正在执行的任务,转而执行中断处理程序.NVIC最多支持240个中断,每个中断最多256个优先级.
# 中断相关的名词解释
中断号:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号.
中断请求:"紧急事件"需向CPU提出申请,要求CPU暂停当前执行的任务,转而处理该"紧急事件",这一申请过程称为中断请求.
中断优先级:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级.
中断处理程序:当外设产生中断请求后,CPU暂停当前的任务,转而响应中断申请,即执行中断处理程序.
中断触发:中断源发出并送给CPU控制信号,将中断触发器置"1",表明该中断源产生了中断,要求CPU去响应该中断,CPU暂停当前任务,执行相应的中断处理程序.
中断触发类型:外部中断申请通过一个物理信号发送到NVIC,可以是电平触发或边沿触发.
中断向量:中断服务程序的入口地址.
中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储.
临界段:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断.为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断.
# 中断的运作机制
当中断产生时,处理机将按如下的顺序执行:
- 保存当前处理机状态信息
- 载入异常或中断处理函数到PC寄存器
- 把控制权转交给处理函数并开始执行
- 当处理函数执行完成时,恢复处理器状态信息
- 从异常或中断中返回到前一个程序执行点
中断使得CPU可以在事件发生时才给予处理,而不必让CPU连续不断地查询是否有相应的事件发生.通过两条特殊指令:关中断和开中断可以让处理器不响应或响应中断,在关闭中断期间,通常处理器会把新产生的中断挂起,当中断打开时立刻进行响应,所以会有适当的延时响应中断,故用户在进入临界区的时候应快进快出.
中断发生的环境有两种情况:在任务的上下文中,在中断服务函数处理上下文中.
在任务的上下文中
任务在工作的时候,如果此时发生了一个中断,无论中断的优先级是多大,都会打断当前
任务的执行,从而转到对应的中断服务函数中执行.
中断发生在任务上下文:①,③:在任务运行的时候发生了中断,那么中断会打断任务的运
行,那么操作系统将先保存当前任务的上下文环境,转而去处理中断服务函数.
中断发生在任务上下文:②,④:当且仅当中断服务函数处理完的时候才恢复任务的上下文
环境,继续运行任务.
在中断服务函数处理上下文中
在执行中断服务例程的过程中,如果有更高优先级别的中断源触发中断,由于当前处于中断处理上下文环境中,根据不同的处理器构架可能有不同的处理方式,比如新的中断等待挂起直到当前中断处理离开后再行响应;或新的高优先级中断打断当前中断处理过程,而去直接响应这个更高优先级的新中断源.后面这种情况,称之为中断嵌套.在硬实时环境中,前一种情况是不允许发生的,不能使响应中断的时间尽量的短.而在软件处理(软实时环境)上,μC/OS允许中断嵌套,即在一个中断服务例程期间,处理器可以响应另外一个优先级更高的中断,过程如上图"中断嵌套发生"所示.
中断嵌套发生①:当中断1的服务函数在处理的时候发生了中断2,由于中断2的优先级比中断1更高,所以发生了中断嵌套,那么操作系统将先保存当前中断服务函数的上下文环境,并且转向处理中断2,当且仅当中断2执行完的时候图中断嵌套发生②,才能继续执行中断1.
# 中断延迟的概念
即使操作系统的响应很快了,但对于中断的处理仍然存在着中断延迟响应的问题,我们称之为中断延迟 (Interrupt Latency).
中断延迟是指从硬件中断发生到开始执行中断处理程序第一条指令之间的这段时间.也就是:系统接收到中断信号到操作系统作出响应,并完成换到转入中断服务程序的时间.也可以简单地理解为:(外部)硬件(设备)发生中断,到系统执行中断服务子程序(ISR)的第一条指令的时间.
中断的处理过程是:外界硬件发生了中断后,CPU到中断处理器读取中断向量,并且查找中断向量表,找到对应的中断服务子程序(ISR)的首地址,然后跳转到对应的ISR去做相应处理.这部分时间,我称之为:识别中断时间.
在允许中断嵌套的实时操作系统中,中断也是基于优先级的,允许高优先级中断抢断正在处理的低优先级中断,所以,如果当前正在处理更高优先级的中断,即使此时有低优先级的中断,也系统不会立刻响应,而是等到高优先级的中断处理完之后,才会响应.而即使在不支持中断嵌套,即中断是没有优先级的,中断是不允许被中断的,所以,如果当前系统正在处理一个中断,而此时另一个中断到来了,系统也是不会立即响应的,而只是等处理完当前的中断之后,才会处理后来的中断.此部分时间,称为:等待中断打开时间.
在操作系统中,很多时候我们会主动进入临界段,系统不允许当前状态被中断打断,故而在临界区发生的中断会被挂起,直到退出临界段时候打开中断.此部分时间,称为:关闭中断时间.
中断延迟可以定义为,从中断开始的时刻到中断服务例程开始执行的时刻之间的时间段.
中断延迟 = 识别中断时间 + [等待中断打开时间] + [关闭中断时间].
PS:注意:"[ ]"的时间是不一定都存在的,此处为最大可能的中断延迟时间.此外,中断恢复时间定义为:执行完ISR 中最后一句代码后到恢复到任务级代码的这段时间.任务延迟时间定义为:中断发生到恢复到任务级代码的这段时间.
# 中断的应用场景
中断在嵌入式处理器中应用非常之多,没有中断的系统不是一个好系统,因为有中断,才能启动或者停止某件事情,从而转去做另一间事情.我们可以举一个日常生活中的例子来说明,假如你正在给朋友写信,电话铃响了,这时你放下手中的笔去接电话,通话完毕再继续写信.这个例子就表现了中断及其处理的过程:电话铃声使你暂时中止当前的工作,而去处理更为急需处理的事情,当把急需处理的事情处理完毕之后,再回过头来继续原来的事情.在这个例子中,电话铃声就可以称为"中断请求",而你暂停写信去接电话就叫作"中断响应",那么接电话的过程就是"中断处理".由此我们可以看出,在计算机执行程序的过程中,由于出现某个特殊情况(或称为"特殊事件"),使得系统暂时中止现行程序,而转去执行处理这一特殊事件的程序,处理完毕之后再回到原来程序的中断点继续向下执行.
没有中断的系统不是好系统,举一个例子.假设有一个朋友来拜访你,但是由于不知何时到达,你只能在门口等待,于是什么事情也干不了;但如果在门口装一个门铃,你就不必在门口等待而可以在家里去做其他的工作,朋友来了按门铃通知你,这时你才中断手中的工作去开门,这就避免了不必要的等待.CPU也是一样,如果时间都浪费在查询的事情上,那这个CPU啥也干不了.在嵌入式系统中合理利用中断,能更好利用CPU的资源.
# 中断管理讲解
ARM Cortex-M系列内核的中断是由硬件管理的,而μC/OS是软件,它并不接管由硬件管理的相关中断(接管简单来说就是,所有的中断都由RTOS的软件管理,硬件来了中断时,由软件决定是否响应,可以挂起中断,延迟响应或者不响应),只支持简单的开关中断等,所以μC/OS中的中断使用其实跟裸机差不多的,需要我们自己配置中断,并且启用中断,编写中断服务函数,在中断服务函数中使用内核IPC通信机制,一般建议使用信号量,消息或事件标志组等标志事件的发生,将事件发布给处理任务,等退出中断后再由相关处理任务具体处理中断,当然μC/OS为能让系统更快退出中断,它支持中断延迟发布,将中断级的发布变成任务级.
ARM Cortex-M NVIC支持中断嵌套功能:当一个中断触发并且系统进行响应时,处理器硬件会将当前运行的部分上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR,R0,R1,R2,R3以及R12寄存器.当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样的会打断当前运行的中断服务例程,然后把老的中断服务例程上下文的PSR,R0,R1,R2,R3和R12寄存器自动保存到中断栈中.这些部分上下文寄存器保存到中断栈的行为完全是硬件行为,这一点是与其他ARM处理器最大的区别(以往都需要依赖于软件保存上下文).
在 ARM Cortex-M系列处理器上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理.而在ARM7,ARM9中,一般是先跳转进入IRQ入口,然后再由软件进行判断是哪个中断源触发,获得了相对应的中断服务例程入口地址后,再进行后续的中断处理.ARM7,ARM9的好处在于,所有中断它们都有统一的入口地址,便于OS的统一管理.而ARM Cortex-M系列处理器则恰恰相反,每个中断服务例程必须排列在一起放在统一的地址上(这个地址必须要设置到NVIC的中断向量偏移寄存器中).中断向量表一般由一个数组定义(或在起始代码中给出).在STM32上,默认起始代码给出.
;中断向量表(部分) __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler DCD BusFault_Handler ; Bus Fault Handler DCD UsageFault_Handler ; Usage Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD DebugMon_Handler ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler ; External Interrupts DCD WWDG_IRQHandler ; Window Watchdog DCD PVD_IRQHandler ; PVD through EXTI Line detect DCD TAMPER_IRQHandler ; Tamper DCD RTC_IRQHandler ; RTC DCD FLASH_IRQHandler ; Flash DCD RCC_IRQHandler ; RCC DCD EXTI0_IRQHandler ; EXTI Line 0 DCD EXTI1_IRQHandler ; EXTI Line 1 DCD EXTI2_IRQHandler ; EXTI Line 2 DCD EXTI3_IRQHandler ; EXTI Line 3 DCD EXTI4_IRQHandler ; EXTI Line 4 DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1 DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2 DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3 DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4 DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5 DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6 DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7 ………
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40μC/OS在Cortex-M系列处理器上也遵循与裸机中断一致的方法,当用户需要使用自定义的中断服务例程时,只需要定义相同名称的函数覆盖弱化符号即可.所以,μC/OS在Cortex-M系列处理器的中断控制其实与裸机没什么差别,不过在进入中断与退出中断的时候需要调用一下
OSIntEnter()
函数与OSIntExit()
函数,方便中断嵌套管理.
# 中断延迟发布
# 中断延迟发布的概念
μC/OS有两种方法处理来自于中断的事件,直接发布(或者称为释放)和延迟发布.通过os_cfg.h中的
OS_CFG_ISR_POST_DEFERRED_EN
来选择,当设置为0时,μC/OS使用直接发布的方法.当设置为1时,使用延迟发布方法,用户可以根据自己设计系统的应用选择其中一种方法即可.启用中断延时发布,可以将中断级发布转换成任务级发布,而且在进入临界段时也可以使用锁调度器代替关中断,这就大大减小了关中断时间,有利于提高系统的实时性(能实时响应中断而不受中断屏蔽导致响应延迟).在前面提到的
OSTimeTick()
,OSSemPost()
,OSQPost()
,OSFlagPost()
,OSTaskSemPost()
,OSTaskQPost()
,OSTaskSuspend()
和OSTaskResume()
等这些函数,如果没有使用中断延迟发布,那么调用这些函数意味着进入一段很长的临界段,也就要关中断很长时间.在启用中断延时发布后,如果在中断中调用这些函数,系统就会将这些post提交函数必要的信息保存到中断延迟提交的变量中去,为了配合中断延迟,μC/OS还将创建了优先级最高(优先级为0)的任务:中断发布函数OS_IntQTask
,退出中断后根据之前保存的参数,在任务中再次进行post相关操作.这个过程其实就是把中断中的临界段放到任务中来实现,这个时候进入临界段就可以用锁住调度器的方式代替了关中断,因此大大减少了关中断的时间,系统将post操作延迟了,中断延迟就是这么来的.进入临界段的方式可以是关中断或者锁住调度器,系统中有些变量不可能在中断中被访问,所以只要保证其他任务不要使用这些变量即可,这个时候就可以用锁调度启动的方式,用锁住调度代替关中断,大大减少了关中断的时间,也能达到进入临界段的目的.中断延迟就是利用这种思想,让本该在中断中完成的事情切换到任务中完成,而且进入临界段的方式是锁定调度器,这样子中断就不会被屏蔽,系统能随时响应中断,并且,整个中断延迟发布的过程是不影响post的效果,因为μC/OS已经设定中断发布任务的优先级为最高,在退出中断后会马上进行post操作,这与在中断中直接进行post操作的时间基本一致.
PS:操作系统内核相关函数一般为了保证其操作的完整性,一般都会进入或长或短的临界段,所以在中断的要尽量少调用内核函数,部分μC/OS提供的函数是不允许在中断中调用的.
在直接发布方式中,μC/OS访问临界段时是采用关中断方式.然而,在延迟提交方式中,μC/OS访问临界段时是采用锁调度器方式.在延迟提交方式中,访问中断队列时μC/OS仍需要关中断进入临界段,但是这段关中断时间是非常短的且是固定的.
中断延迟发布与直接发布的区别
中断延时发布
①:进入中断,在中断中需要发布一个内核对象(如消息队列,信号量等),但是使用了中断延迟发布,在中断中值执行
OS_IntQPost()
函数,在这个函数中,采用关中断方式进入临界段,因此在这个时间段是不能响应中断的.②:已经将内核对象发布到中断消息队列,那么将唤醒
OS_IntQTask
任务,因为该任务是最高优先级任务,所以能立即被唤醒,然后转到OS_IntQTask
任务中发布内核对象,在该任务中,调用OS_IntQRePost()
函数进行发布内核对象,进入临界段的方式采用锁调度器方式,那么在这个阶段,中断是可以被响应的.③:系统正常运行,任务按优先级进行切换.
中断直接发布
①,②:而采用中断直接发布的情况是在中断中直接屏蔽中断以进入临界段,这段时间中,都不会响应中断,直到发布完成,系统任务正常运行才开启中断.
③:系统正常运行,任务按照优先级正常切换.从两个图中可以看出,采用中断延迟发布的效果更好,将本该在中断中的处理转变成为在任务中处理,系统关中断的时间大大降低,使得系统能很好地响应外部中断,如果在应用中关中断时间是关键性的,应用中有非常频繁的中断源,且应用不能接受直接发布方式那样较长的关中断时间,推荐使用中断延迟发布方式.
# 中断队列控制块
如果启用中断延迟发布,在中断中调用内核对象发布(释放)函数,系统会将发布的内容存放在中断队列中控制块中.
中断队列信息块.
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u
struct os_int_q
{
OS_OBJ_TYPE Type; //用于发布的内核对象类型
OS_INT_Q *NextPtr; //指向下一个中断队列控制块
void *ObjPtr; //指向内核对象变量指针
void *MsgPtr; //如果发布的是任务消息或者是内核对象消息,指向发布消息的指针
OS_MSG_SIZE MsgSize; //如果发布的是任务消息或者是内核对象消息,记录发布的消息的字节大小
OS_FLAGS Flags; //如果发布的是事件标志,该成员变量记录要设置事件的标志位
OS_OPT Opt; //记录发布内核对象时的选项
CPU_TS TS; //记录时间戳
};
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
# 中断延时发布相关函数
函数名称 | 函数作用 |
---|---|
OS_IntQTaskInit() | 中断延迟发布任务初始化 |
OS_IntQPost() | |
OS_IntQTask() |
中断延迟发布任务初始化OS_IntQTaskInit()
在系统初始化的时候,如果我们启用了中断延迟发布,那么系统会根据我们自定义配置中断延迟发布任务的宏定义
OS_CFG_INT_Q_SIZE
与OS_CFG_INT_Q_TASK_STK_SIZE
进行相关初始化,这两个宏定义在os_cfg_app.h文件中.void OS_IntQTaskInit (OS_ERR *p_err) { OS_INT_Q *p_int_q; OS_INT_Q *p_int_q_next; OS_OBJ_QTY i; #ifdef OS_SAFETY_CRITICAL if (p_err == (OS_ERR *)0) { OS_SAFETY_CRITICAL_EXCEPTION(); return; } #endif /* 清空延迟提交过程中溢出的计数值 */ OSIntQOvfCtr = (OS_QTY)0u; //延迟发布信息队列的基地址必须不为空指针 if (OSCfg_IntQBasePtr == (OS_INT_Q *)0) { *p_err = OS_ERR_INT_Q; return; } //延迟发布队列成员必须不小于 2 个 if (OSCfg_IntQSize < (OS_OBJ_QTY)2u) { *p_err = OS_ERR_INT_Q_SIZE; return; } //初始化延迟发布任务每次运行的最长时间记录变量 OSIntQTaskTimeMax = (CPU_TS)0; //将定义的数据连接成一个单向链表 p_int_q = OSCfg_IntQBasePtr; p_int_q_next = p_int_q; p_int_q_next++; for (i = 0u; i < OSCfg_IntQSize; i++) { //每个信息块都进行初始化 p_int_q->Type = OS_OBJ_TYPE_NONE; p_int_q->ObjPtr = (void *)0; p_int_q->MsgPtr = (void *)0; p_int_q->MsgSize = (OS_MSG_SIZE)0u; p_int_q->Flags = (OS_FLAGS )0u; p_int_q->Opt = (OS_OPT )0u; p_int_q->NextPtr = p_int_q_next; p_int_q++; p_int_q_next++; } //将单向链表的首尾相连组成一个“圈 p_int_q--; p_int_q_next = OSCfg_IntQBasePtr; p_int_q->NextPtr = p_int_q_next; //队列出口和入口都指向第一个 OSIntQInPtr = p_int_q_next; OSIntQOutPtr = p_int_q_next; //清空延迟发布队列中需要进行发布的内核对象个数 OSIntQNbrEntries = (OS_OBJ_QTY)0u; //清空延迟发布队列中历史发布的内核对象最大个数 OSIntQNbrEntriesMax = (OS_OBJ_QTY)0u; if (OSCfg_IntQTaskStkBasePtr == (CPU_STK *)0) { *p_err = OS_ERR_INT_Q_STK_INVALID; return; } if (OSCfg_IntQTaskStkSize < OSCfg_StkSizeMin) { *p_err = OS_ERR_INT_Q_STK_SIZE_INVALID; return; } //创建延迟发布任务 OSTaskCreate((OS_TCB *)&OSIntQTaskTCB, (CPU_CHAR *)((void *)"μC/OS-III ISR Queue Task"), (OS_TASK_PTR )OS_IntQTask, (void *)0, (OS_PRIO )0u, //优先级最高 (CPU_STK *)OSCfg_IntQTaskStkBasePtr, (CPU_STK_SIZE)OSCfg_IntQTaskStkLimit, (CPU_STK_SIZE)OSCfg_IntQTaskStkSize, (OS_MSG_QTY )0u, (OS_TICK )0u, (void *)0, (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), (OS_ERR *)p_err); } #endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94中断延迟发布过程OS_IntQPost()
如果启用了中断延迟发布,并且发送消息的函数是在中断中被调用,此时就不该立即发送消息,而是将消息的发送放在指定发布任务中,此时系统就将消息发布到租单消息队列中,等待到中断发布任务唤醒再发送消息.
void OS_IntQPost (OS_OBJ_TYPE type, //内核对象类型 void *p_obj, //被发布的内核对象 void *p_void, //消息队列或任务消息 OS_MSG_SIZE msg_size, //消息的数目 OS_FLAGS flags, //事件 OS_OPT opt, //发布内核对象时的选项 CPU_TS ts, //发布内核对象时的时间戳 OS_ERR *p_err){ CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏 #ifdef OS_SAFETY_CRITICAL //如果启用(默认禁用)了安全检测 if (p_err == (OS_ERR *)0) { //如果错误类型实参为空 OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数 return; //返回,不继续执行 } #endif CPU_CRITICAL_ENTER(); //关中断 if (OSIntQNbrEntries < OSCfg_IntQSize) { //如果中断队列未占满 OSIntQNbrEntries++; //更新中断队列的最大使用数目的历史记录 if (OSIntQNbrEntriesMax < OSIntQNbrEntries) { OSIntQNbrEntriesMax = OSIntQNbrEntries; } /* 将要重新提交的内核对象的信息放入到中断队列入口的信息记录块 */ OSIntQInPtr->Type = type; /* 保存要发布的对象类型 */ OSIntQInPtr->ObjPtr = p_obj; /* 保存指向要发布的对象的指针 */ OSIntQInPtr->MsgPtr = p_void;/* 将信息保存到消息块的中 */ OSIntQInPtr->MsgSize = msg_size; /* 保存信息的大小 */ OSIntQInPtr->Flags = flags; /* 如果发布到事件标记组,则保存标志 */ OSIntQInPtr->Opt = opt; /* 保存选项 */ OSIntQInPtr->TS = ts; /* 保存时间戳信息 */ OSIntQInPtr = OSIntQInPtr->NextPtr; //指向下一个中断队列入口 /* 让中断队列管理任务 OSIntQTask 就绪 */ OSRdyList[0].NbrEntries = (OS_OBJ_QTY)1; //更新就绪列表上的优先级 0 的任务数为 1 个 //就绪列表的头尾指针都指向 OSIntQTask 任务 OSRdyList[0].HeadPtr = &OSIntQTaskTCB; OSRdyList[0].TailPtr = &OSIntQTaskTCB; OS_PrioInsert(0u); //在优先级列表中增加优先级 0 if (OSPrioCur != 0) { //如果当前运行的不是 OSIntQTask 任务 OSPrioSaved = OSPrioCur; //保存当前任务的优先级 } *p_err = OS_ERR_NONE; //返回错误类型为“无错误” } else { //如果中断队列已占满 OSIntQOvfCtr++; //中断队列溢出数目加 1 *p_err = OS_ERR_INT_Q_FULL;//返回错误类型为“中断队列已满” } CPU_CRITICAL_EXIT(); //开中断 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51中断延迟发布任务OS_IntQTask()
在中断中将消息放入中断队列,那么接下来又怎么样进行发布内核对象呢?μC/OS在中断中只是将要提交的内核对象的信息都暂时保存起来,然后就绪优先级最高的中断延迟发布任务,接着继续执行中断,在退出所有中断嵌套后,第一个执行的任务就是延迟发布任务.
void OS_IntQTask (void *p_arg) { CPU_BOOLEAN done; CPU_TS ts_start; CPU_TS ts_end; CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏 p_arg = p_arg; while (DEF_ON) //进入死循环 { done = DEF_FALSE; while (done == DEF_FALSE) { CPU_CRITICAL_ENTER(); //关中断 if (OSIntQNbrEntries == (OS_OBJ_QTY)0u) { //如果中断队列里的内核对象发布完毕 //从就绪列表移除中断队列管理任务 OS_IntQTask OSRdyList[0].NbrEntries = (OS_OBJ_QTY)0u; OSRdyList[0].HeadPtr = (OS_TCB *)0; OSRdyList[0].TailPtr = (OS_TCB *)0; OS_PrioRemove(0u); //从优先级表格移除优先级 0 CPU_CRITICAL_EXIT(); //开中断 OSSched(); //任务调度 done = DEF_TRUE; //退出循环 } else {//如果中断队列里还有内核对象 CPU_CRITICAL_EXIT(); //开中断 ts_start = OS_TS_GET(); //获取时间戳 OS_IntQRePost(); //发布中断队列里的内核对象 ts_end = OS_TS_GET() - ts_start; //计算该次发布时间 if (OSIntQTaskTimeMax < ts_end) //更新中断队列发布内核对象的最大时间的历史记录 { OSIntQTaskTimeMax = ts_end; } CPU_CRITICAL_ENTER(); //关中断 OSIntQOutPtr = OSIntQOutPtr->NextPtr;//处理下一个 OSIntQNbrEntries--; //中断队列的成员数目减 1 CPU_CRITICAL_EXIT(); //开中断 } } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 中断管理实验
中断管理实验是在μC/OS中创建了两个任务分别获取信号量与消息队列,定义WKUP按键的触发方式为中断触发,其触发的中断服务函数则跟裸机一样,在中断触发的时候通过消息队列将消息传递给任务,任务接收到消息就将信息通过串口调试助手显示出来。而且中断管理实验也实现了一个串口的DMA传输+空闲中断功能,当串口接收完不定长的数据之后产生一个空闲中断,在中断中将信号量传递给任务,任务在收到信号量的时候将串口的数据读取出来并且在串口调试助手中回显.
开启延时中断
定义任务空间栈的大小以及任务栈数组,任务控制块和优先级.
定义任务函数
中断服务函数编写
任务启动函数编写
结果现象
# CPU利用率及栈检测统计
# CPU利用率的基本概念及作用
- CPU利用率其实就是系统运行的程序占用的CPU资源,表示机器在某段时间程序运行的情况,如果这段时间中,程序一直在占用CPU的使用权,那么可以认为CPU的利用率是100%.CPU的利用率越高,说明机器在这个时间上运行了很多程序,反之较少.利用率的高低与CPU性能强弱有直接关系,就像一段一模一样的程序,如果使用运算速度很慢的CPU,它可能要运行1000ms,而使用很运算速度很快的CPU可能只需要10ms,那么在1000ms这段时间中,前者的CPU利用率就是100%,而后者的CPU利用率只有1%,因为1000ms内前者都在使用CPU做运算,而后者只使用10ms的时间做运算,剩下的时间CPU可以做其他事情.
- μC/OS是多任务操作系统,对CPU都是分时使用的:比如A任务占用10ms,然后B任务占用30ms,然后空闲60ms,再又是A任务占10ms,B任务占30ms,空闲60ms;如果在一段时间内都是如此,那么这段时间内的利用率为40%,因为整个系统中只有40%的时间是CPU处理数据的时间.
- 一个系统设计的好坏,可以使用CPU利用率来衡量,一个好的系统必然是能完美响应急需的处理,并且系统的资源不会过于浪费(性价比高).举个例子,假设一个系统的CPU利用率经常在90%~100%徘徊,那么系统就很少有空闲的时候,这时候突然有一些事情急需CPU的处理,但是此时CPU都很可能被其他任务在占用了,那么这个紧急事件就有可能无法被相应,即启用被相应,那么占用CPU的任务又处于等待状态,这种系统就是不够完美的,因为资源处理得太过于紧迫;反过来,假如CPU的利用率在1%以下,那么我们就可以认为这种产品的资源过于浪费,搞一个那么好的CPU去干着没啥意义的活(大部分时间处于空闲状态),作为产品的设计,既不能让资源过于浪费,也不能让资源过于紧迫,这种设计才是完美的,在需要的时候能及时处理完突发事件,而且资源也不会过剩,性价比更高.
- μC/OS提供的CPU利用率统计是一个可选功能,只有将
OS_CFG_STAT_TASK_EN
宏定义启用后用户才能使用CPU利用率统计相关函数,该宏定义位于os_cfg.h文件中.
# CPU利用率统计初始化
CPU利用率统计的原理很简单,系统中必须存在空闲任务,当且仅当CPU空闲的时候才会去执行空闲任务,那么我们就可以让CPU在空闲任务中一直做加法运算,假设某段时间T中CPU一直都在空闲任务中做加法运算(变量自加),那么这段时间算出来的值就是CPU空闲时候的最大值,我们假设为100,那么当系统中有其他任务的时候,CPU就不可能一直处于空闲任务做运算了,那么同样的一段时间T里,空闲任务算出来的值变成了80,那么是不是可以说明空闲任务只占用了系统的80%的资源,剩下的20%被其他任务占用了,这是显而易见的,同样的,利用这个原理,我们就能知道CPU的利用率大约是多少了(这种计算不会很精确),假设CPU在T时间内空闲任务中运算的最大值为OSStatTaskCtrMax(100),而有其他任务参与时候 T 时间内空闲任务运算的值为80(OSStatTaskCtr),那么CPU的利用率CPUUsage的公式应该为:
CPUUsage(%)= 100*(1-OSStatTaskCtr / OSStatTaskCtrMax).
假设有一次空闲任务运算的值为100(OSStatTaskCtr),说明没有其他任务参与,那么CPU的利用率就是0%,如果 OSStatTaskCtr的值为0,那么表示这段时间里CPU都没在空闲任务中运算,那么CPU的利用率自然就是100%.
PS:一般情况下时间T由
OS_CFG_STAT_TASK_RATE_HZ
宏定义决定,是我们自己在os_cfg_app.h文件中定义的,我们的例程定义为10,该宏定义决定了统计任务的执行频率,即决定了更新一次CPU利用率的时间为1/OS_CFG_STAT_TASK_RATE_HZ,单位是秒.此外,统计任务的时钟节拍与软件定时器任务的时钟节拍一样,都是由系统时钟节拍分频得到的,如果统计任务运行的频率设定不是时钟节拍整数倍,那么统计任务实际运行的频率跟设定的就会有误差,这点跟定时器是一样的.在统计CPU利用率之前必须先调用
OSStatTaskCPUUsageInit()
函数进行相关初始化,这个函数的目的就是为了计算只有空闲任务时CPU在某段时间内的运算最大值,也就是
OSStatTaskCtrMax
.
# 栈溢出检测概念及作用
如果处理器有MMU或者MPU,检测栈是否溢出是非常简单的,MMU和MPU是处理器上特殊的硬件设施,可以检测非法访问,如果任务企图访问未被允许的内存空间的话,就会产生警告,但是我们使用的STM32是没有MMU和MPU的,但是可以使用软件模拟栈检测,但是软件的模拟比较难以实现,但是 μC/OS 为我们提供了栈使用情况统计的功能,直接使用即可,如果需要使用栈溢出检测的功能,就需要用户自己在
App_OS_TaskSwHook()
钩子函数中自定义实现(我们不实现该功能),需要使用μC/OS为我们提供的栈检测功能,想要使用该功能就需要在os_cfg_app.h文件中将OS_CFG_STAT_TASK_STK_CHK_EN
宏定义配置为1.某些处理器中有一些栈溢出检测相关的寄存器,当CPU的栈指针小于(或大于,取决于栈的生长方向)设置于这个寄存器的值时,就会产生一个异常(中断),异常处理程序就需要确保未允许访问空间代码的安全(可能会发送警告给用户,或者其他处理).任务控制块中的成员变量
StkLimitPtr
就是为这种目的而设置的.每个任务的栈必须分配足够大的内存空间供任务使用,在大多数情况下,StkLimitPtr
指针的值可以设置接近于栈顶(&TaskStk[0],假定栈是从高地址往低地址生长的,事实上STM32的栈生长方向就是向下生长,从高地址向低地址生长),StkLimitPtr
的值在创建任务的时候由用户指定.PS:此处的栈检测是对于带有MPU的处理器.
# 栈溢出检测过程
中断管理实验是在μC/OS中创建了两个任务分别获取信号量与消息队列,并且定义了两个按键KEY1与KEY2的触发方式为中断触发,其触发的中断服务函数则跟裸机一样,在中断触发的时候通过消息队列将消息传递给任务,任务接收到消息就将信息通过串口调试助手显示出来.而且中断管理实验也实现了一个串口的DMA传输+空闲中断功能,当串口接收完不定长的数据之后产生一个空闲中断,在中断中将信号量传递给任务,任务在收到信号量的时候将串口的数据读取出来并且在串口调试助手中回显.
μC/OS 是怎么检测任务使用了多少栈的呢?以STM32的栈生长方向为例子(高地址向低地址生长),在任务初始化的时候先将任务所有的栈都置 0,使用后的栈不为 0,在检测的时候只需从栈的低地址开始对为 0 的栈空间进行计数统计,然后通过计算就可以得出任务的栈使用了多少,这样子用户就可以根据实际情况进行调整任务栈的大小,这些信息同样也会在统计任务每隔1/OSCfg_StatTaskRate_Hz 秒就进行更新.
# 统计任务OS_StatTask()
μC/OS提供了统计任务的函数, 该函数为系统内部函数(任务),在启用宏定义OS_CFG_STAT_TASK_EN
后,系统会自动创建一个统计任务OS_StatTask()
,它会在任务中计算整个系统的CPU利用率,各个任务的CPU利用率和各个任务的栈使用信息.
void OS_StatTask (void *p_arg) //统计任务函数
{
#if OS_CFG_DBG_EN > 0u
#if OS_CFG_TASK_PROFILE_EN > 0u
OS_CPU_USAGE usage;
OS_CYCLES cycles_total;
OS_CYCLES cycles_div;
OS_CYCLES cycles_mult;
OS_CYCLES cycles_max;
#endif
OS_TCB *p_tcb;
#endif
OS_TICK ctr_max;
OS_TICK ctr_mult;
OS_TICK ctr_div;
OS_ERR err;
OS_TICK dly;
CPU_TS ts_start;
CPU_TS ts_end;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏
p_arg = p_arg;
//没意义,仅为预防编译器警告
while (OSStatTaskRdy != DEF_TRUE) //如果统计任务没被允许运行
{
OSTimeDly(2u * OSCfg_StatTaskRate_Hz, //一直延时
OS_OPT_TIME_DLY,
&err);
}
OSStatReset(&err); //如果统计任务已被就绪,复位统计,继续执行
/* 根据设置的宏计算统计任务的执行节拍数 */
dly = (OS_TICK)0;
if (OSCfg_TickRate_Hz > OSCfg_StatTaskRate_Hz)
{
dly = (OS_TICK)(OSCfg_TickRate_Hz / OSCfg_StatTaskRate_Hz);
}
if (dly == (OS_TICK)0)
{
dly = (OS_TICK)(OSCfg_TickRate_Hz / (OS_RATE_HZ)10);
}
while (DEF_ON) //进入任务体
{
ts_start = OS_TS_GET(); //获取时间戳
#ifdef CPU_CFG_INT_DIS_MEAS_EN//如果要测量关中断时间
OSIntDisTimeMax = CPU_IntDisMeasMaxGet(); //获取最大的关中断时间
#endif
CPU_CRITICAL_ENTER(); //关中断
OSStatTaskCtrRun = OSStatTaskCtr; //获取上一次空闲任务的计数值
OSStatTaskCtr = (OS_TICK)0; //进行下一次空闲任务计数清零
CPU_CRITICAL_EXIT(); //开中断
/* 计算 CPU 利用率 */
if (OSStatTaskCtrMax > OSStatTaskCtrRun)
//如果空闲计数值小于最大空闲计数值
{
if (OSStatTaskCtrMax < 400000u)
//这些分类是为了避免计算 CPU 利用率过程中
{
ctr_mult = 10000u; //产生溢出,就是避免相乘时超出 32 位寄存器.
ctr_div = 1u;
}
else if (OSStatTaskCtrMax < 4000000u)
{
ctr_mult = 1000u;
ctr_div = 10u;
}
else if (OSStatTaskCtrMax < 40000000u)
{
ctr_mult = 100u;
ctr_div = 100u;
}
else if (OSStatTaskCtrMax < 400000000u)
{
ctr_mult = 10u;
ctr_div = 1000u;
}
else
{
ctr_mult = 1u;
ctr_div = 10000u;
}
ctr_max = OSStatTaskCtrMax / ctr_div;
OSStatTaskCPUUsage = (OS_CPU_USAGE)((OS_TICK)10000u -
ctr_mult * OSStatTaskCtrRun / ctr_max);
if (OSStatTaskCPUUsageMax < OSStatTaskCPUUsage)
//更新 CPU 利用率的最大历史记录
{
OSStatTaskCPUUsageMax = OSStatTaskCPUUsage;
}
} else {//如果空闲计数值大于或等于最大空闲计数值
OSStatTaskCPUUsage = (OS_CPU_USAGE)10000u; //那么 CPU 利用率为 0
}
OSStatTaskHook(); //用户自定义的钩子函数
/* 下面计算各个任务的 CPU 利用率,原理跟计算整体 CPU 利用率相似 */
#if OS_CFG_DBG_EN > 0u//如果启用了调试代码和变量
#if OS_CFG_TASK_PROFILE_EN > 0u
//如果启用了允许统计任务信息
cycles_total = (OS_CYCLES)0;
CPU_CRITICAL_ENTER(); //关中断
p_tcb = OSTaskDbgListPtr; //获取任务双向调试列表的首个任务
CPU_CRITICAL_EXIT(); //开中断
while (p_tcb != (OS_TCB *)0) //如果该任务非空
{
OS_CRITICAL_ENTER(); //进入临界段
p_tcb->CyclesTotalPrev = p_tcb->CyclesTotal; //保存任务的运行周期
p_tcb->CyclesTotal = (OS_CYCLES)0;
//复位运行周期,为下次运行做准备
OS_CRITICAL_EXIT(); //退出临界段
cycles_total+=p_tcb->CyclesTotalPrev;//所有任务运行周期的总和
CPU_CRITICAL_ENTER(); //关中断
p_tcb = p_tcb->DbgNextPtr;
//获取列表的下一个任务,进行下一次循环
CPU_CRITICAL_EXIT(); //开中断
}
#endif
/* 使用算法计算各个任务的 CPU 利用率和任务栈用量 */
#if OS_CFG_TASK_PROFILE_EN > 0u
//如果启用了任务的统计功能
if (cycles_total > (OS_CYCLES)0u) //如果有任务占用过 CPU
{
if (cycles_total < 400000u)
//这些分类是为了避免计算 CPU 利用率过程中
{
cycles_mult = 10000u; //产生溢出,就是避免相乘时超出 32 位寄存器.
cycles_div = 1u;
}
else if (cycles_total < 4000000u)
{
cycles_mult = 1000u;
cycles_div = 10u;
}
else if (cycles_total < 40000000u)
{
cycles_mult = 100u;
cycles_div = 100u;
}
else if (cycles_total < 400000000u)
{
cycles_mult = 10u;
cycles_div = 1000u;
}
else
{
cycles_mult = 1u;
cycles_div = 10000u;
}
cycles_max = cycles_total / cycles_div;
}
else//如果没有任务占用过 CPU
{
cycles_mult = 0u;
cycles_max = 1u;
}
#endif
CPU_CRITICAL_ENTER(); //关中断
p_tcb = OSTaskDbgListPtr;
//获取任务双向调试列表的首个任务
CPU_CRITICAL_EXIT(); //开中断
while (p_tcb != (OS_TCB *)0) //如果该任务非空
{
#if OS_CFG_TASK_PROFILE_EN > 0u
//如果启用了任务控制块的简况变量
usage = (OS_CPU_USAGE)(cycles_mult * //计算任务的 CPU 利用率
p_tcb->CyclesTotalPrev / cycles_max);
if (usage > 10000u) //任务的 CPU 利用率为 100%
{
usage = 10000u;
}
p_tcb->CPUUsage = usage; //保存任务的 CPU 利用率
if (p_tcb->CPUUsageMax < usage) //更新任务的最大 CPU 利用率的历史记录
{
p_tcb->CPUUsageMax = usage;
}
#endif
/* 栈检测 */
#if OS_CFG_STAT_TASK_STK_CHK_EN > 0u//如果启用了任务栈检测
OSTaskStkChk(p_tcb, //计算被激活任务的栈用量
&p_tcb->StkFree,
&p_tcb->StkUsed,
&err);
#endif
CPU_CRITICAL_ENTER(); //关中断
p_tcb = p_tcb->DbgNextPtr;
//获取列表的下一个任务,进行下一次循环
CPU_CRITICAL_EXIT(); //开中断
}
#endif
if (OSStatResetFlag == DEF_TRUE) //如果需要复位统计
{
OSStatResetFlag = DEF_FALSE;
OSStatReset(&err); //复位统计
}
ts_end = OS_TS_GET() - ts_start; //计算统计任务的执行时间
if (OSStatTaskTimeMax < ts_end)//更新统计任务的最大执行时间的历史记录
{
OSStatTaskTimeMax = ts_end;
}
OSTimeDly(dly,//按照先前计算的执行节拍数延时
OS_OPT_TIME_DLY,
&err);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 栈检测OSTaskStkChk()
μC/OS提供OSTaskStkChk()
函数用来进行栈检测, 在使用之前必须将宏定义OS_CFG_STAT_TASK_STK_CHK_EN
配置为1,对于需要进行任务栈检测的任务,在其被OSTaskCreate()
函数创建时,选项参数opt
还需包含OS_OPT_TASK_STK_CHK
.统计任 务会以我们设定的运行频率不断更新栈使用的情况并且保存到任务控制块的StkFree
和StkUsed
成员变量中,这两个变量分别表示任务栈的剩余空间与已使用空间大小,单位为任务栈大小的单位(在STM32中采用4字节).
#if OS_CFG_STAT_TASK_STK_CHK_EN > 0u//如果启用了任务栈检测
void OSTaskStkChk (OS_TCB *p_tcb, //目标任务控制块的指针
CPU_STK_SIZE *p_free, //返回空闲栈大小
CPU_STK_SIZE *p_used, //返回已用栈大小
OS_ERR *p_err){
CPU_STK_SIZE free_stk;
CPU_STK *p_stk;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏
#ifdef OS_SAFETY_CRITICAL//如果启用了安全检测
if (p_err == (OS_ERR *)0) //如果 p_err 为空
{
OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
return; //返回,停止执行
}
#endif
#if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u//如果启用了中断中非法调用检测
if (OSIntNestingCtr > (OS_NESTING_CTR)0) //如果该函数是在中断中被调用
{
*p_err = OS_ERR_TASK_STK_CHK_ISR; //错误类型为“在中断中检测栈”
return; //返回,停止执行
}
#endif
#if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测
if (p_free == (CPU_STK_SIZE*)0) //如果 p_free 为空
{
*p_err = OS_ERR_PTR_INVALID; //错误类型为“指针非法”
return; //返回,停止执行
}
if (p_used == (CPU_STK_SIZE*)0) //如果 p_used 为空
{
*p_err = OS_ERR_PTR_INVALID; //错误类型为“指针非法”
return; //返回,停止执行
}
#endif
CPU_CRITICAL_ENTER(); //关中断
if (p_tcb == (OS_TCB *)0) //如果 p_tcb 为空
{
p_tcb = OSTCBCurPtr;//目标任务为当前运行任务(自身)
}
if (p_tcb->StkPtr == (CPU_STK*)0) //如果目标任务的栈为空
{
CPU_CRITICAL_EXIT(); //开中断
*p_free = (CPU_STK_SIZE)0; //清零 p_free
*p_used = (CPU_STK_SIZE)0; //清零 p_used
*p_err = OS_ERR_TASK_NOT_EXIST; //错误类型为“任务不存在
return; //返回,停止执行
}
/* 如果目标任务的栈非空 */
if ((p_tcb->Opt & OS_OPT_TASK_STK_CHK) == (OS_OPT)0) //如果目标任务没选择检测栈
{
CPU_CRITICAL_EXIT(); //开中断
*p_free = (CPU_STK_SIZE)0; //清零 p_free
*p_used = (CPU_STK_SIZE)0; //清零 p_used
*p_err = OS_ERR_TASK_OPT;
//错误类型为“任务选项有误”
return; //返回,停止执行
}
CPU_CRITICAL_EXIT();//如果任务选择了检测栈,开中断
/* 开始计算目标任务的栈的空闲数目和已用数目 */
free_stk = 0u; //初始化计算栈工作
#if CPU_CFG_STK_GROWTH == CPU_STK_GROWTH_HI_TO_LO
//如果 CPU 的栈是从高向低增长
p_stk = p_tcb->StkBasePtr;
//从目标任务栈最低地址开始计算
while (*p_stk == (CPU_STK)0) //计算值为 0 的栈数目
{
p_stk++;
free_stk++;
}
#else
//如果 CPU 的栈是从低向高增长
p_stk = p_tcb->StkBasePtr + p_tcb->StkSize - 1u;
//从目标任务栈最高地址开始计算
while (*p_stk == (CPU_STK)0) //计算值为 0 的栈数目
{
free_stk++;
p_stk--;
}
#endif
*p_free = free_stk;
//返回目标任务栈的空闲数目
*p_used = (p_tcb->StkSize - free_stk);
//返回目标任务栈的已用数目
*p_err = OS_ERR_NONE; //错误类型为“无错误”
}
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 任务栈大小的确定
- 任务栈的大小取决于该任务的需求,设定栈大小时,我们就需要考虑:所有可能被栈调用的函数及其函数的嵌套层数,相关局部变量的大小,中断服务程序所需要的空间,另外,栈还需存入CPU寄存器,如果处理器有浮点数单元FPU寄存器的话还需存入FPU寄存器.
- 嵌入式系统的潜规则,避免写递归函数,这样子可以人为计算出一个任务需要的栈空间大小,逐级嵌套所有可能被调用的函数,计数被调用函数中所有的参数,计算上下文切换时的CPU寄存器空间,计算切换到中断时所需的CPU寄存器空间(假如CPU没有独立的栈用于处理中断),计算处理中断服务函数(ISR)所需的栈空间,将这些值相加即可得到任务最小的需求空间,但是我们不可能计算出精确的栈空间,我们通常会将这个值再乘以1.5到2.0以确保任务的安全运行.这个计算的值是假定在任务所有的执行路线都是已知的情况下的,但这在真正的应用中并不太可能,比如说,如果调用printf()函数或者其他的函数,这些函数所需要的空间是很难测得或者说就是不可能知道的,在这种情况下,我们这种人为计算任务栈大小的方法就变得不太可能了,那么我们可以在刚开始创建任务的时候给任务设置一个较大的栈空间,并监测该任务运行时栈空间的实际使用量,运行一段时间后得到任务的最大栈使用情况(或者叫任务栈最坏结果),然后用该值乘1.5到2.0作为栈空间大小就差不多可以作为任务栈的空间大小,这样子得到的值就会比较精确一点,在调试阶段可以这样子进行测试,发现崩溃就增大任务的栈空间,直到任务能正常稳定运行为止.
# CPU利用率及栈检测统计实验
CPU利用率及栈检测统计实验是在μC/OS中创建了四个任务,其中三个任务是普通任务,另一个任务用于获取CPU利用率与任务相关信息并通过串口打印出来.
启用宏定义
OS_CFG_STAT_TASK_EN
.声明内存管理对象和内存分区,定义任务空间栈的大小以及任务栈数组,任务控制块和优先级.
定义任务函数
任务启动函数编写
结果现象