├── README.md ├── main.c └── note.txt /README.md: -------------------------------------------------------------------------------- 1 | # xxddq 2 | 小小调度器16位版本 3 | 小小调度器 V1.1 设计原理 (讨论稿) 4 | By smset 5 | 前言: 6 | 小小调度器是一款基于 C 语言的,协作式多任务编程框架。它基于状态机原理实现,所有任务 7 | 均采用公共堆栈,具有简单小巧,易于移植的特点,非常适合于资源紧张的单片机编程使用。 8 | 小小调度器的多任务并行机制和传统的状态机的并行机制原理是想通的。 9 | 10 | 11 | 主要的区别在于:小小调度器利用了 C 语言的__LINE__宏,这个__LINE__宏,代表了源文件中 12 | 代码的行号,通过将代码行号保存到静态变量的方式,来记录程序运行的位置信息,从而使得 13 | 原来需要人工实现的状态值设计、状态变量赋值以及状态跳转的系列编程工作,大部分均由记 14 | 录行号的宏自动实现了,使得开发者可以节省很多底层的状态设计和处理的编程工作。 15 | 基于此核心原理,小小调度器设计了一个框架和宏定义,以极低的 CPU 资源代价,模拟了一个 16 | 和真实 RTOS 系统相似的多任务编程环境,使得编程者可以用更自然、更优雅、更易于理解的方 17 | 式,来编写多任务并行代码,把时间和精力,更多的放在应用业务层逻辑的代码实现上。 18 | 19 | 20 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /****小小调度器开始**********************************************/ 4 | #define MAXTASKS 3 5 | volatile unsigned short timers[MAXTASKS]; 6 | #define _SS static unsigned char _lc=0; switch(_lc){default: 7 | #define _EE ;}; _lc=0; return 65535; 8 | 9 | #define WaitX(tickets) do { _lc=(__LINE__%255)+1; return (tickets) ;case (__LINE__%255)+1:;} while(0); 10 | #define WaitUntil(A) do { while(!(A)) WaitX(1);} while(0); 11 | #define WaitUtilT(A,TimeOut) do { static unsigned short _count=(TimeOut); do { WaitX(1); _count--; } while((!(A))&&(_count>0));} while(0); 12 | 13 | #define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) { unsigned short d=TaskName(); while(timers[TaskID]!=d) timers[TaskID]=d;} } while(0); 14 | #define RunTaskA(TaskName,TaskID) do { if (timers[TaskID]==0) {unsigned short d=TaskName(); while(timers[TaskID]!=d) timers[TaskID]=d; continue;} }while(0); //前面的任务优先保证执行 15 | 16 | #define CallSub(SubTaskName) do {unsigned short currdt; _lc=(__LINE__%255)+1; return 0; case (__LINE__%255)+1: currdt=SubTaskName(); if(currdt!=65535) return currdt;} while(0); 17 | #define InitTasks() do {unsigned char i; for(i=MAXTASKS;i>0 ;i--) timers[i-1]=0; } while(0); 18 | #define UpdateTimers() do{unsigned char i; for(i=MAXTASKS;i>0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=65535)) timers[i-1]--;}} while(0); 19 | 20 | #define SEM unsigned int 21 | //初始化信号量 22 | #define InitSem(sem) do{sem=0;}while(0); 23 | //等待信号量 24 | #define WaitSem(sem) do{ sem=1; WaitX(0); if (sem>0) return 1;} while(0); 25 | //发送信号量 26 | #define SendSem(sem) do {sem=0;} while(0); 27 | 28 | /*****小小调度器结束*******************************************************/ 29 | 30 | 31 | sbit LED1 = P2^1; 32 | sbit LED2 = P2^2; 33 | 34 | sbit LED0 = P2^5; 35 | 36 | unsigned short task0(){ 37 | _SS 38 | while(1){ 39 | WaitX(50); 40 | LED0=!LED0; 41 | } 42 | _EE 43 | } 44 | 45 | unsigned short task1(){ 46 | _SS 47 | while(1){ 48 | WaitX(100); 49 | LED1=!LED1; 50 | } 51 | _EE 52 | } 53 | 54 | unsigned short task2(){ 55 | _SS 56 | while(1){ 57 | WaitX(100); 58 | LED2=!LED2; 59 | } 60 | _EE 61 | } 62 | 63 | void InitT0() 64 | { 65 | TMOD = 0x21; 66 | IE |= 0x82; // 12t 67 | TL0=0Xff; 68 | TH0=0XDB; 69 | TR0 = 1; 70 | } 71 | 72 | void INTT0(void) interrupt 1 using 1 73 | { 74 | TL0=0Xff; //10ms 重装 75 | TH0=0XDB;//b7; 76 | 77 | UpdateTimers(); 78 | } 79 | 80 | 81 | 82 | 83 | void main() 84 | { 85 | InitT0(); 86 | InitTasks(); //初始化任务,实际上是给timers清零 87 | while(1){ 88 | // RunTask(task0,0); 89 | RunTaskA(task1,1);//任务1具有比任务2高的运行权限 90 | RunTaskA(task2,2);//任务2具有低的运行权限 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /note.txt: -------------------------------------------------------------------------------- 1 | # xxddq 2 | 小小调度器16位版本 3 | 小小调度器 V1.1 设计原理 (讨论稿) 4 | By smset 5 | 前言: 6 | 小小调度器是一款基于 C 语言的,协作式多任务编程框架。它基于状态机原理实现,所有任务 7 | 均采用公共堆栈,具有简单小巧,易于移植的特点,非常适合于资源紧张的单片机编程使用。 8 | 小小调度器的多任务并行机制和传统的状态机的并行机制原理是想通的。 9 | 10 | 11 | 主要的区别在于:小小调度器利用了 C 语言的__LINE__宏,这个__LINE__宏,代表了源文件中 12 | 代码的行号,通过将代码行号保存到静态变量的方式,来记录程序运行的位置信息,从而使得 13 | 原来需要人工实现的状态值设计、状态变量赋值以及状态跳转的系列编程工作,大部分均由记 14 | 录行号的宏自动实现了,使得开发者可以节省很多底层的状态设计和处理的编程工作。 15 | 基于此核心原理,小小调度器设计了一个框架和宏定义,以极低的 CPU 资源代价,模拟了一个 16 | 和真实 RTOS 系统相似的多任务编程环境,使得编程者可以用更自然、更优雅、更易于理解的方 17 | 式,来编写多任务并行代码,把时间和精力,更多的放在应用业务层逻辑的代码实现上。 18 | 19 | 并行编程的基本原理: 20 | 当一个 CPU 需要并行执行几个任务函数时,就意味着这几个任务函数在宏观上看上去是并行 21 | 运行的。 22 | 当然在微观上,单核 CPU,只能一个时刻执行一个函数的代码,所以实现并行编程的关键在于, 23 | 每个时刻 CPU 都只花很少的时间,运行某个任务函数的一小段代码,然后 CPU 以同样的方式, 24 | 又花很少的时间去执行下一个任务的一小段代码。 这样,从宏观上看起来,多个任务函数就是 25 | 在并行执行了。 26 | 因此 CPU 实际上是在不断的进入一个任务函数的内部运行一瞬间,并不断从任务函数内部跳 27 | 至另一个任务的内部运行一瞬间,同时,CPU 下次进入任务函数时,必须从上次跳出的位置接 28 | 着运行。 29 | 因此,这里必然的涉及到几方面的问题: 30 | 1) 使用什么机制,能让 CPU 从一个任务函数的某个点,切换至另一个任务函数的某个点。并 31 | 且再次进入一个任务时,还能从原来跳走的地方接着往下执行。 32 | 这个问题的关键 :涉及到代码跳转机制以及代码执行位置的记录。 33 | 在 C 语言中,代码的跳转有多种实现机制,代码跳转本质上其实是控制代码快的运行逻辑。 34 | 跳转有两种主要的实现机制:一个是纯粹利用 C 语言本身提供的语法来实现, 另一个是利 35 | 用底层操作 CPU 内部寄存器来实现跳转。 36 | 具体来讲, C 语言本身提供的语法里,有以下几种在函数内部跳转的语法: 37 | 比如 goto 配合标签,可以实现一个函数内部代码的跳转, 38 | 用 if else 实际上在逻辑上也能控制 cpu,用 switch case 也可以实现代码的跳转。 39 | C 语言本身还提供 setjmp, longjmp 机制,实现函数间全局跳转机制。 40 | 事实上,基于 goto, if else, switch case,这三种机制,是大多数传统状态机编程所使用的 41 | 代码块跳转控制语法。 42 | 而 setjmp/longjmp 则是相对少用的语法。 这几种语法均不涉及到 CPU 寄存器的操作,因 43 | 此不会因为 CPU 的不同而导致跳转实现的代码有所区别。 44 | 另一种机制是利用修改 CPU 内部代码指针寄存器来实现代码跳转,这种机制是大多数 RTOS 45 | 操作系统使用的,由于设计到底层寄存器的操作,因此针对不同的 CPU,跳转实现的代码实际 46 | 上是各不相同的。 47 | 这里,我们也不想讨论得过于深入,总之就是一种是不涉及到寄存器操作的代码切换方式, 48 | 一种是涉及到寄存器操作的代码切换方式,小小调度器选择的是不涉及到寄存器操作的代码切 49 | 换方式。 50 | 具体来说,小小调度器 V1.1 , 采用的是 switch case 跳转语法,该语法只能在函数内部 51 | 进行跳转,而不能在任务之间之间进行跳转。采用 switch case 跳转语法,是总体来说这种跳 52 | 转语法更有优势(在后面的描述中会提到)。 53 | 在调度过程中,任务函数是需要先进入函数的开头,从函数开头跳转至函数中间的某个位置, 54 | 执行一个瞬间,然后退出任务函数。然后下一个任务也是一样的: 先进入函数的开头,从函数 55 | 开头跳转至函数中间的某个位置,执行一个瞬间,然后退出任务函数。 所有任务依次轮转,从 56 | 而实现并行效果。 57 | 这种架构,仔细想像,似乎感觉和传统的状态机编程也没有区别:就是在任务函数开头处, 58 | 根据状态值,进行代码的散转。 59 | 对的,其实框架是相同的,由于框架相同,使得几乎所有状态机可以实现的过程逻辑,都能 60 | 由小小调度器代替实现。并且 CPU 的资源消耗和状态机实现的代码几乎没有区别。 61 | 主要的区别在于行号的记录,以及一些语法宏,使得代码变得更简单了。 62 | 63 | 64 | 65 | 66 | 我们以一个任务为例: 67 | unsigned short task0(){ 68 | _SS 69 | while(1){ 70 | WaitX(50); 71 | LED0=!LED0; 72 | } 73 | _EE 74 | } 75 | 这个任务的作用是: 每 500 毫秒,让 LED1 亮灭翻转一次。 76 | 我们先把宏替代了: 77 | unsigned short task0(){ 78 | static unsigned char _lc=0; switch(_lc){default: 79 | while(1){ 80 | do { _lc=(__LINE__%255)+1; return 50 ; case (__LINE__%255)+1:;} while(0); 81 | LED0=!LED0; 82 | } 83 | ;}; _lc=0; return 65535; 84 | } 85 | 这里__LINE__实际上是一个行号,在一个文件内,行号从 1 开始,一直递增。 86 | (使用__LINE__行号来记录代码运行位置,在已知信息中,最早是由 PT Thread 采用的,PT 87 | Thread 是一种多任务机制,有兴趣的可以自行去网上搜索相关资料) 88 | 比如 WaitX(50);这个代码行在第 100 行,那么实际上任务函数内就是: 89 | unsigned short task0(){ 90 | static unsigned char _lc=0; switch(_lc){default: 91 | while(1){ 92 | do { _lc=(100%255)+1; return 50 ; case (100%255)+1:;} while(0); 93 | LED0=!LED0; 94 | } 95 | ;}; _lc=0; return 65535; 96 | } 97 | 从这个 task0 任务函数,我们可以看到,一开头,就申明了 static unsigned char _lc=0; 98 | 这个静态变量。初值为 0, 这个_lc 变量,就是任务函数用来记录运行位置的变量。 99 | 这里采用了 static unsigned char _lc 申明, 其中 static 保证了它作为一个静态变量, 100 | 在任务函数退出后,变量的值不会丢失。 101 | 另外,就是它是一个 unsigned char 变量,之所以采用 unsigned char,是只给它分配了一 102 | 个字节,也就是意味着_lc 只有 0-255 的取值范围。(只分配一个字节,是因为小小调度器 V1.1 103 | 版本是针对 CPU 资源极致优化的导向。) 104 | 在_lc 后,就是一个 switch(_lc){default: 告诉大家,其实这里用 switch(_lc){case 0: 也 105 | 是一样的。 106 | 任务函数开头的第一个 case,必须占用一个常值,因为,当 task0 任务函数整个过程被执 107 | 行完毕后,当需要重新进入 task0 这个任务流程时,task0 任务函数还能从头运行。因此在 task0 108 | 任务函数的结尾处,_lc 必须赋值为一个常值,并且这个常值和 task0 任务函数内第一个 case 109 | 的值是相同的,只有这样才能保证 task0 任务函数流程被执行完毕后,在以后重启任务流程时, 110 | 代码又从第一个 case 处开始执行。 111 | 反之,如果没有这个机制,那么当 task0 任务函数被执行完毕后,_lc 就永远的记录的是 112 | task0 任务函数内最后一个 case 的位置,再也没有办法从头进入 task0 任务流程执行了。 113 | (注意,这里说的任务函数执行完毕,是指宏观上一个任务的总体时序流程运行完成,而不是 114 | 指任务单次执行时的进入和退出)。 115 | 既然需要用到一个常数,那么我们自然的选择 0 这个常数,因此,就有了函数尾部_EE 的 116 | 宏定义里面,_lc=0;将_lc 赋值为 0 的操作。(当然,你说,我选 255 这个常量,可以吗?其实, 117 | 也是可以的,只是反正得选一个,这里我就做个主,选 0 好了) 118 | 接下来,就带来了额外的麻烦事儿:由于_lc 是 unsigned char ,取值为 0-255,那么,当 119 | 代码行号为 256 时,会怎样? 这个时候_lc=256;实际上导致_lc 的值变为 0,那么这个时候就 120 | 有问题了:这会导致任务函数此时会变成和任务结束时_lc 取值一样,都是 0 了!也即是说任 121 | 务函数结束时,和任务行号取值为 256 的整数倍时相同了。 122 | 我们当然是不希望这个发生的,所以需要进行补救: 让_lc 在任务函数内取值不可能为 0, 123 | 于是就有了_lc=(__LINE__%255)+1 的取值方式,这个方式就是让_lc 取值变为 1-255,从而避开 124 | 了 0 值。 125 | 至于开头为什么不直接用 case 0,而用 default 呢,还是因为是小小调度器 V1.1 版本是 126 | 针对 CPU 资源极致优化的导向,因为采用 default 会更省 CPU 资源,因为会让编译器省一次判 127 | 断,其实逻辑是完全一样的。 128 | 好,再回头看代码的具体执行流程: 129 | 当 task0 函数第一次运行时,_lc 初始是零,所以程序会直接跳到 default 处,因为函数 130 | 内部其他代码行的_lc 取值已经不可能是 0 了,而 case 0 也没有显式的出现过,所以 0 是没有 131 | 被定义的情况,恰好就归于 default 处理。 132 | (这里顺便提下,为何不用 goto 跳转呢? 因为标准 C 语言里,goto 只能跳转到一个标签 133 | 处,而这个标签又不能是一个数据值,所以 goto 就被否了。) 134 | 进入 default 后,就进入 while(1)循环体,并且执行: 135 | do { _lc=(100%255)+1; return 50 ; case (100%255)+1:;} while(0); 136 | 这里,_lc 被赋值为 101 了,并且函数返回 50,其实就是记忆了代码运行到这一行的位置,并 137 | 带着一个返回值 50,函数就地返回。 138 | 至于这个 50,就是告诉调度器,延时 500 毫秒后,再次进入 task0 任务函数执行的意思。这个 139 | 是由调度框架来保证的,后面会描述到。 140 | 当 500 毫秒过去后,会再次进入 task0 任务函数,这次,_lc 是 101,那么 switch 会去找 case 141 | 101 的代码,恰好我们在 WaitX(50)的语句展开后,发现了后面有个 case (100%255)+1: 142 | 正是 case 101 啊,因此代码直接会跳到 case 101 处往下执行。 143 | 因此,我们就可以看出,task0 第二次进入后,是从第一次的退出点接着往下执行的。之所以 144 | 前面提到_lc 是静态变量,就是需要在第一次进入时保存的行号,在第二次进入时没有消失。 145 | 这里,顺便提下,其实 C 语言的 case 处,应该是一个值,而不是表达式,但这里 case (100%255)+1: 146 | 明明就是一个表达式,因此,我们是利用了编译器的宏计算功能,其实只能说大多数编译器都 147 | 支持宏计算,但不是所有的都支持。 有少数编译器会在这里报错。(后续的版本,实际上解决 148 | 了这个宏计算兼容性的问题了)。 149 | task0 在等待 500 毫秒,进入 case 101 行后,就会执行 LED0!=LED0;导致 LED0 变化一次,然 150 | 后又会因为 while(1)循环,再次 do { _lc=(100%255)+1; return 50 ; case (100%255)+1:;} 151 | while(0); 又把_lc 赋值为 101,返回 50。 152 | 再等 500 毫秒,又进入 case 101 处执行,LED0 又变化一次。 153 | 周而复往,LED0 就会一直每过 500 毫秒变量一次亮灭状态。 154 | Task0 说完了,task1 也是没啥区别的,就是 led 管脚不同,就不用单独再说 task1 了。 155 | 顺便提下,我们看到 task0 里有 while(1),按常规的编程来看,当一个函数有 while(1)是个死 156 | 循环, 157 | 从语法简单来看, 158 | while(1){ 159 | WaitX(50); 160 | LED0=!LED0; 161 | } 162 | 是不会退出这个 task0 函数的,但是其实上 WaitX(50);并不是普通的延时函数,而是一个宏, 163 | 这个宏里面包含了 return,使得代码在 while(1)里面实际上是退出去了的。 164 | 我们可以把这里的 return 理解为任务主动释放 CPU。 165 | 因此一个任务如果在 while(1)里面,没有用到 WaitX 的话,很可能就是一个霸占 CPU 的不自觉 166 | 的任务。 正是需要每个任务都能主动释放 CPU,所以小小调度器是协作式多任务调度器。而不 167 | 是抢占式多任务调度器。 168 | 2) 使用什么机制,让 CPU 再次回到任务时,原来的运行环境没发生错乱,比如变量和寄存器 169 | 的值,都保持上次跳走时的一样的值,不会因为任务切换操作给破坏了,或者被别的任务给破 170 | 坏了。 171 | 前面我们提过,当 CPU 从 task0 函数的某处退出,再次进入 task0 时,是需要从上次记忆的位 172 | 置执行,那么首先运行的位置是需要记录,这个前面提供_lc 变量是静态局部变量,及时中途 173 | 从函数退出,这个变量也是不会发生改变的。 174 | 除此之外,任务函数还有可能用到其他变量,比如一些临时用于计算或判断的中间变量,这些 175 | 变量也需要和_lc 变量一样,不会因为任务中途退出而消失。 176 | 我们知道,在 C 语言里全局变量,和静态局部变量,是满足这种条件的。因为他们分配在静态 177 | 存储区,在程序整个运行期间都不释放。 178 | 而普通的局部变量则不同,因为局部变量声明为函数内部的变量,其存储空间位于栈中。当进 179 | 入函数时,会对根据局部变量需求,在栈上申请一段内存空间,供局部变量使用。当局部变量 180 | 生命周期结束后,在栈上释放。会在函数退出后,发生变化。 181 | 因此,在任务里,如果需要使用额外的变量,应首先考虑使用静态局部变量,这样可以保证任 182 | 务每次进入时,这些变量的值没被释放,还是保持和上次退出时一样。 183 | (如果确保需要用到的变量,其使用过程中任务不会释放 CPU,也可以使用局部变量) 184 | 顺便提一下,很多 RTOS 系统,都采用了任务独立堆栈的方式,来实现局部变量的保护。也就是 185 | 说每个任务的局部变量都分配在任务独自的堆栈空间里,当进行任务切换时,连同堆栈指针一 186 | 起进行切换。这种方式的好处就是可以使用局部变量了,但是带来了程序的复杂行,因为切换 187 | 堆栈指针,也都是需要对 CPU 底层寄存器进行操作的。 188 | 另外每个任务采用独立堆栈时,开发这必须估算每个任务分派多大的堆栈空间,这需要一定的 189 | 经验,因为堆栈分派少了,会发生堆栈溢出,导致程序异常,堆栈分配多了,又导致内存浪费。 190 | 小小调度器采用任务共用公共堆栈的方式,无需估算堆栈大小,就简化的多了。 191 | 3) 使用什么机制,让每次进入任务函数时,能确保只执行一瞬间,然后就切换到下一个任务 192 | 去,而不是长期霸占 CPU。 193 | 如果要使得每个任务都只执行一小段,就让出 CPU 运行权,只有两种方式,一种是任务主动释 194 | 放 CPU,一种是任务不主动释放,由中断主动打断 CPU 的运行,强制将 CPU 交给另一个任务。 195 | 第一种方式就称为协作式多任务,第二种方式就称为抢占式多任务。 196 | 小小调度器 V1.1 版本,实际上是采用了两种机制,一种是协作式多任务,一种是抢占式多任务。 197 | 因此,属于混合式任务调度模式。 198 | 我们可以从代码中看到,共有三个任务,task0,task1,task2; 199 | 其中,task1 和 task2 在 main 函数里循环运行。 而在定时器中断运行着 task0 任务。 200 | void INTT0(void) interrupt 1 using 1 201 | { 202 | TL0=0Xff; //10ms 重装 203 | TH0=0XDB;//b7; 204 | UpdateTimers(); 205 | RunTask(task0,0);//任务具有精确按时获得执行的权限,要求:task0 每次执行消耗时间 206 | <0.5 个 ticket 207 | } 208 | 因此,对于 task1 和 task2 来说,它们两个任务是平级的,需要协作释放 CPU,如果 task1 不 209 | 主动释放 CPU,task2 就无法运行,反之如果 task2 不主动释放 CPU,task1 也无法运行。 210 | 但是 task0 则具有更高的优先级,即便 task1 或 task2 不主动释放 CPU, task0 任务仍然在中断 211 | 里可以定时得到执行。 212 | 实际上,大部分情况下,使用协作式多任务已经能够满足一般项目的需要。并且纯协作式多任 213 | 务有一个好处,就是不需要考虑变量的互斥机制。 214 | 当引入抢占式多任务后,就需要认真考虑哪些变量有可能被中断里面的任务给修改。 215 | 调度框架描述: 216 | 最后一个部分,是小小调度器的延时框架。 217 | 小小调度器 V1.1 有一个名为 timers 的数组: volatile unsigned short timers[MAXTASKS]; 218 | 这里的 MAXTASKS 就是顶级任务的个数,最大为 255 个。 219 | 我们知道每个任务函数,在开头只有一个简单的 switch 跳转,这个 switch 就像 goto 一样, 220 | 只负责跳转到相应行。也就是说只要进入了函数,任务就会往后面的代码执行。 221 | 那么等待延迟的任务,就需要任务函数以外的机制来实现,而不是让任务函数自己去判断。 222 | 也就是说当一个任务内部使用 WaitX 进行 500 毫秒,那么就应该调度器框架保证在 500 毫秒后 223 | 才再次进入任务函数。 224 | 因此我们可以看 RunTask 宏: 225 | #define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) 226 | timers[TaskID]=TaskName(); } while(0); 227 | 这里就是有一个判断:只有 timers[TaskID] 等于 0 时 ,才进入任务函数。然后,当任务函数 228 | 返回延时值后,又写入 timers[TaskID]。 229 | 也就是说,如果 task0 任务使用 WaitX(50);等待 500 毫秒,那么 timers[0]就会变成 50,然 230 | 后调度器必须保证在 500 毫秒时,准确的让 timers[0]又变为 0,好让 RunTask 再次能够进入 231 | task0 任务函数。 232 | 因此,我们设计了一个 10 毫秒中断,在这个 10 毫秒中断里,每次中断让 timers[0]的值减一。 233 | 当 500 毫秒过后,刚好就使得 timers[0]的值变为 0. 234 | void InitT0() 235 | { 236 | TMOD = 0x21; 237 | IE |= 0x82; // 12t 238 | TL0=0Xff; 239 | TH0=0XDB; 240 | TR0 = 1; 241 | } 242 | void INTT0(void) interrupt 1 using 1 243 | { 244 | TL0=0Xff; //10ms 重装 245 | TH0=0XDB;//b7; 246 | UpdateTimers(); 247 | RunTask(task0,0);//任务具有精确按时获得执行的权限,要求:task0 每次执行消耗时间 248 | <0.5 个 ticket 249 | } 250 | 展开其中的 UpdateTimers()宏: 251 | do{unsigned char i; for(i=MAXTASKS;i>0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=65535)) 252 | timers[i-1]--;}} while(0); 253 | 就看得出来,当每个任务的 timer 变量不为 0,或者不为 65535 时,timer 变量值都在减少。 254 | 当任务的 timer 变量不为 0 时,变量值减少,这个很容易理解,因为 0 已经是最小值了。 255 | 但是还有一个条件:当任务的 timer 变量为 65535 时,也不发生变化。这不就会导致这个任务永 256 | 远不会再进入了吗?因为一直是 65535。 257 | 让我们再次看一下任务结尾出的_EE 宏,就能明白了: 258 | #define _EE ;}; _lc=0; return 65535; 259 | 当任务函数执行到最后时,返回 65535,那么这个任务再也不会被执行了。这个就恰好体现了任 260 | 务的 timer 变量为 65535 的意义了:任务对用的 timers[TASKID]值为 65535 意味着任务终止。 261 | 那么,当一个任务作为子任务,这一次流程执行完毕,下一次又被父任务调用,该如何处理呢? 262 | 我们来看调用子任务的宏 CallSub: 263 | #define CallSub(SubTaskName) do {unsigned char currdt; _lc=(__LINE__%255)+1; return 264 | 0; case (__LINE__%255)+1: currdt=SubTaskName(); if(currdt!=255) return currdt;} 265 | while(0); 266 | 这个宏,只是记录了父任务的当前行号,然后返回 0; 267 | 意味着几乎无需等待,调度框架立即会再次进入父任务。执行 currdt=SubTaskName(); 268 | if(currdt!=65535) return currdt; 269 | 这意味着,当子任务结束返回 65536 时,这个 65535 值并不写入父任务的 timers 数组对应的变量, 270 | 而是直接执行父任务后面的代码:也就等于是父任务已经完成这次子任务的调用,往后执行后 271 | 续代码。当子任务返回的不是 65535 的值时,我们可以看到父任务会返回这个值:return currdt; 272 | 这会导致父任务在等待一段时间后才会再次进入。 273 | 这可以解释为:因为父任务在子任务未结束时,不能去执行子任务后的代码,因此把子任务需 274 | 要等待的时间,转由让父任务来进行等待。这样就可以让子任务共用父任务的时间延迟变量, 275 | 子任务无需独立的时间延迟变量,从而节省了 RAM 空间。 276 | 使用说明事项: 277 | 这里把在论坛上发布小小调度器早期版本时,码的一段文字拷贝过来: 278 | 小小调度器任务函数的写法主要注意的,主要有三点: 279 | 1) 任务函数内部变量,建议都用静态局部变量来定义。 280 | 2) 任务函数内不能用 switch 语句。 281 | 3) 任务函数内,不能用 return 语句。 因为 return 已经被赋予任务延时的特定意义。(这是 282 | 返回型任务函数版本的一个强制要求) 283 | 这三点,并不会明显造成写程序的不方便。 284 | --------------------------- 285 | 从裸奔到使用 OS 操作系统或调度系统的代价主要有: 286 | 硬件资源代价(对 RAM 和 ROM 的消耗),学习代价(学会其原理,并掌握其用法),移植代价 287 | (往不同 cpu 上移植的工作量),效率代价(使用调度系统后带来的额外 cpu 负担),商业代 288 | 价(版权费用),稳定性代价(是否引入潜在不稳定因素,或者增大 bug 跟踪调试工作量)。 289 | 从这几方面来讲,应用小小调度器的代价,都是非常小的。 290 | 1) 硬件资源代价: 前面的优化版本已经说明问题。keil 下,本例程 ram 消耗 : 22 字节,rom 291 | 消耗 126 字节. 292 | 2) 学习代价: 小小调度器总共只有十多行代码,如果我们做一个简单的解释说明,理解起来 293 | 其实是很快的。我相信学习时间比其他调度系统要短。 294 | 3) 移植代价: 几乎没有什么移植工作量,对于各种 cpu,几乎是通吃。 295 | 4) 效率代价: 我们一直在努力优化,相信调度效率已经不低了。比如任务切换时间,应该是 296 | 可以做到 uS 级别,甚至亚 uS 级别。 297 | 5) 商业代价: 小小调度器为免费使用,无需支付任何费用。 298 | 6) 稳定性代价:小小调度器本质上仅仅是几个宏而已,未涉及任何对内部寄存器或堆栈的操 299 | 作,避免了引入不稳定风险因素,所有操作都在可预见,可把控的前提下进行。 300 | 除了以上三点需要注意的外,还容易出的错误有: 301 | 1) WaitX 里面的值>=65535 了,因为 v1.1 版本的值最大只能是 65534。 302 | 2) While,for 循环里,忘了写 WaitX,导致程序死循环。 303 | 3) 任务函数重入,也就是说一个任务函数同时被几个顶级任务调用。这在 V1.1 里是不允许的。 304 | 4) 妄图在普通函数里使用 WaitX。这个是无法通过编译的。 305 | 5) 在 RunTaskA 调度的任务里使用 waitx(0),导致后续任务无法运行。 306 | 附录: 小小调度器 V1.1.c 307 | 308 | 309 | #include 310 | 311 | /****小小调度器开始**********************************************/ 312 | #define MAXTASKS 3 313 | volatile unsigned short timers[MAXTASKS]; 314 | #define _SS static unsigned char _lc=0; switch(_lc){default: 315 | #define _EE ;}; _lc=0; return 65535; 316 | 317 | #define WaitX(tickets) do { _lc=(__LINE__%255)+1; return (tickets) ;case (__LINE__%255)+1:;} while(0); 318 | #define WaitUntil(A) do { while(!(A)) WaitX(1);} while(0); 319 | #define WaitUtilT(A,TimeOut) do { static unsigned short _count=(TimeOut); do { WaitX(1); _count--; } while((!(A))&&(_count>0));} while(0); 320 | 321 | #define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) { unsigned short d=TaskName(); while(timers[TaskID]!=d) timers[TaskID]=d;} } while(0); 322 | #define RunTaskA(TaskName,TaskID) do { if (timers[TaskID]==0) {unsigned short d=TaskName(); while(timers[TaskID]!=d) timers[TaskID]=d; continue;} }while(0); //前面的任务优先保证执行 323 | 324 | #define CallSub(SubTaskName) do {unsigned short currdt; _lc=(__LINE__%255)+1; return 0; case (__LINE__%255)+1: currdt=SubTaskName(); if(currdt!=65535) return currdt;} while(0); 325 | #define InitTasks() do {unsigned char i; for(i=MAXTASKS;i>0 ;i--) timers[i-1]=0; } while(0); 326 | #define UpdateTimers() do{unsigned char i; for(i=MAXTASKS;i>0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=65535)) timers[i-1]--;}} while(0); 327 | 328 | #define SEM unsigned int 329 | //初始化信号量 330 | #define InitSem(sem) do{sem=0;}while(0); 331 | //等待信号量 332 | #define WaitSem(sem) do{ sem=1; WaitX(0); if (sem>0) return 1;} while(0); 333 | //发送信号量 334 | #define SendSem(sem) do {sem=0;} while(0); 335 | 336 | /*****小小调度器结束*******************************************************/ 337 | 338 | 339 | sbit LED1 = P2^1; 340 | sbit LED2 = P2^2; 341 | 342 | sbit LED0 = P2^5; 343 | 344 | unsigned short task0(){ 345 | _SS 346 | while(1){ 347 | WaitX(50); 348 | LED0=!LED0; 349 | } 350 | _EE 351 | } 352 | 353 | unsigned short task1(){ 354 | _SS 355 | while(1){ 356 | WaitX(100); 357 | LED1=!LED1; 358 | } 359 | _EE 360 | } 361 | 362 | unsigned short task2(){ 363 | _SS 364 | while(1){ 365 | WaitX(100); 366 | LED2=!LED2; 367 | } 368 | _EE 369 | } 370 | 371 | void InitT0() 372 | { 373 | TMOD = 0x21; 374 | IE |= 0x82; // 12t 375 | TL0=0Xff; 376 | TH0=0XDB; 377 | TR0 = 1; 378 | } 379 | 380 | void INTT0(void) interrupt 1 using 1 381 | { 382 | TL0=0Xff; //10ms 重装 383 | TH0=0XDB;//b7; 384 | 385 | UpdateTimers(); 386 | } 387 | 388 | 389 | 390 | 391 | void main() 392 | { 393 | InitT0(); 394 | InitTasks(); //初始化任务,实际上是给timers清零 395 | while(1){ 396 | // RunTask(task0,0); 397 | RunTaskA(task1,1);//任务1具有比任务2高的运行权限 398 | RunTaskA(task2,2);//任务2具有低的运行权限 399 | } 400 | } 401 | 402 | QQ 群:371719283(小小调度器和单片机) 403 | --------------------------------------------------------------------------------