smset 发表于 2012-12-21 21:26:14

我是因为考虑通用性才没使用这种Goto跳转法。

ifree64 发表于 2012-12-21 21:26:41

smset 发表于 2012-12-21 21:23 static/image/common/back.gif
哦,Gcc没试过,我觉得Keil下优化还可以。不过Gcc支持Goto变量标号,那个效率高 ...

goto 变量标号是个什么原理?

我是手写的代码直接switch(c){case 0: case 1: case 2:}这样一路下去的,全是用的比较跳转。sdcc都知道用跳转表来做优化,我再看看IAR编译得怎么样。

dr2001 发表于 2012-12-21 21:39:09

ifree64 发表于 2012-12-21 20:43 static/image/common/back.gif
测试中发现avr-gcc没有对switch代码使用跳转表优化(即便case从0开始,依次递增也是如此),一大堆的cpi ...

GCC还是认真用Label As Values吧。

AVR的两个完全独立的地址空间GCC不一定特别适应,这货还是主要针对统一编址的地址空间比较适应一些。

dr2001 发表于 2012-12-21 21:41:01

ifree64 发表于 2012-12-21 21:26 static/image/common/back.gif
goto 变量标号是个什么原理?

我是手写的代码直接switch(c){case 0: case 1: case 2:}这样一路下去的, ...

Label as Values就是:
一个功能是把Label的地址提取出来保存在一个void *变量里;
另一个是goto一个void*变量的值,作为目标地址。

BaoMy 发表于 2012-12-21 21:42:51

支持,好东西。

ifree64 发表于 2012-12-21 21:51:28

IAR设置最高优化级别,也没使用跳转表来实现switch。
可能AVR的体系结构使用跳转表的效率不高,它没有类似51那样的jmp @a+dptr这样的指令;只有一条IJMP利用Z来间接跳转。

ifree64 发表于 2012-12-21 22:01:23

dr2001 发表于 2012-12-21 21:41 static/image/common/back.gif
Label as Values就是:
一个功能是把Label的地址提取出来保存在一个void *变量里;
另一个是goto一个void ...

谢谢!

另请教下:是否avr的体系结构不太能高效实现查找表技术。

ifree64 发表于 2012-12-21 23:01:40

dr2001 发表于 2012-12-21 21:39 static/image/common/back.gif
GCC还是认真用Label As Values吧。

AVR的两个完全独立的地址空间GCC不一定特别适应,这货还是主要针对统 ...

AVR上,Lable as value可能只是看上去很美而已,感觉效率不一定有多高。
switch
void receive_uart(unsigned char rbyte)
{
    static unsigned char state = 0;
    static unsigned char counter = 0;
    static unsigned char buffer;
    static unsigned char data_xor;
    static unsigned char length = 0;

    switch(state)
    {
    case 0:
      if(rbyte == 0xAA)
      {
            data_xor = rbyte;
            state = 1;
      }
      break;
    case 1:
      if(rbyte > 20 || rbyte < 1)
      {
            state = 0;
      }else{
            state = 2;
            data_xor ^= rbyte;
            length = rbyte;
      }
      break;
    case 2:
      buffer = rbyte;
      data_xor ^= rbyte;
      if(length == counter){
            counter = 0;
            state = 3;
      }
      break;
    case 3:
      if(data_xor == rbyte)
      {
            parse_data(buffer, length);
      }
      state = 0;
      break;
    default:
      state = 0;
      break;
    }
}
编译结果
.global        receive_uart
        .type        receive_uart, @function
receive_uart:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
        lds r25,state.1400
        cpi r25,lo8(1)
        breq .L19
        brlo .L18
        cpi r25,lo8(2)
        breq .L20
        cpi r25,lo8(3)
        brne .L27
        rjmp .L21
.L18:
        cpi r24,lo8(-86)
        brne .L16
        sts data_xor.1403,r24
        ldi r24,lo8(1)
        rjmp .L28
.L19:
        mov r25,r24
        subi r25,lo8(-(-1))
        cpi r25,lo8(20)
        brlo .L24
        rjmp .L27
.L24:
        ldi r25,lo8(2)
        sts state.1400,r25
        lds r25,data_xor.1403
        eor r25,r24
        sts data_xor.1403,r25
        sts length.1404,r24
        ret
.L20:
        lds r18,counter.1401
        mov r30,r18
        ldi r31,0
        subi r30,lo8(-(buffer.1402))
        sbci r31,hi8(-(buffer.1402))
        st Z,r24
        subi r18,lo8(-(1))
        sts counter.1401,r18
        lds r25,data_xor.1403
        eor r25,r24
        sts data_xor.1403,r25
        lds r24,length.1404
        cpse r24,r18
        rjmp .L16
        sts counter.1401,__zero_reg__
        ldi r24,lo8(3)
.L28:
        sts state.1400,r24
        ret
.L21:
        lds r25,data_xor.1403
        cpse r25,r24
        rjmp .L27
        lds r22,length.1404
        ldi r24,lo8(buffer.1402)
        ldi r25,hi8(buffer.1402)
        rcall parse_data
.L27:
        sts state.1400,__zero_reg__
.L16:
        ret
        .size        receive_uart, .-receive_uart
改用Lable as value
void lable_as_value(unsigned char c)
{
    static void* next;
    static unsigned char xor;
    static unsigned char length;
    static unsigned char count;
    static unsigned char buffer;
    if(next) goto *next;

R_Start:
    if(c == 0xAA)
    {
      next = &&R_Length;
      xor = 0xAA;
      return;
    }
R_Length:
    if(c > 20 || c < 1)
    {
      next = &&R_Start;
    }else{
      next = &&R_Data;
      length = c;
      xor ^= c;
    }
    return;
R_Data:
    buffer = c;
    xor ^= c;
    if(length == count){
      count = 0;
      next = &&R_Check;
    }
    return;
R_Check:
    if(xor == c){
      parse_data(buffer, length);
    }
    next = &&R_Start;
    return;
}
编译结果
.global        lable_as_value
        .type        lable_as_value, @function
lable_as_value:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
        lds r30,next.1410
        lds r31,next.1410+1
        sbiw r30,0
        breq .L7
        ijmp
.L7:
        cpi r24,lo8(-86)
        brne .L8
        ldi r18,lo8(gs(.L8))
        ldi r19,hi8(gs(.L8))
        sts next.1410+1,r19
        sts next.1410,r18
        sts xor.1411,r24
        ret
.L8:
        mov r25,r24
        subi r25,lo8(-(-1))
        cpi r25,lo8(20)
        brlo .L10
        rjmp .L13
.L10:
        ldi r18,lo8(gs(.L11))
        ldi r19,hi8(gs(.L11))
        sts next.1410+1,r19
        sts next.1410,r18
        sts length.1412,r24
        lds r25,xor.1411
        eor r25,r24
        sts xor.1411,r25
        ret
.L11:
        lds r18,count.1413
        mov r30,r18
        ldi r31,0
        subi r30,lo8(-(buffer.1414))
        sbci r31,hi8(-(buffer.1414))
        st Z,r24
        subi r18,lo8(-(1))
        sts count.1413,r18
        lds r25,xor.1411
        eor r25,r24
        sts xor.1411,r25
        lds r24,length.1412
        cpse r24,r18
        rjmp .L6
        sts count.1413,__zero_reg__
        ldi r24,lo8(gs(.L12))
        ldi r25,hi8(gs(.L12))
        rjmp .L17
.L12:
        lds r25,xor.1411
        cpse r25,r24
        rjmp .L13
        lds r22,length.1412
        ldi r23,0
        ldi r24,lo8(buffer.1414)
        ldi r25,hi8(buffer.1414)
        rcall parse_data
.L13:
        ldi r24,lo8(gs(.L7))
        ldi r25,hi8(gs(.L7))
.L17:
        sts next.1410+1,r25
        sts next.1410,r24
.L6:
        ret
跳转确实变快了,但为了保存这个指针,要用那么多指令。

dr2001 发表于 2012-12-22 10:17:39

本帖最后由 dr2001 于 2012-12-22 10:39 编辑

ifree64 发表于 2012-12-21 22:01 static/image/common/back.gif
谢谢!

另请教下:是否avr的体系结构不太能高效实现查找表技术。

简单查看了一下AVR的指令集,AVR上使用跳转表的门槛会比较高:
先要把表地址放到Z里边;用两个LPM从Flash获得目的地址到别的Reg;用两个MOV一类的把地址再运回Z里;用IJMP完成跳转。
当swith的数量不足够大时,这么干不一定是划算的。

avr-libc的帮助里说AVR-GCC是有JumpTable优化的。
估计是触发这个优化的条件比较高,而且对应的优化做得不足够好吧。毕竟AVR的代码应该是主要由ATmel贡献的,他们要不是疯狂优化就没办法了。

Label As Values的直接实现,
goto方面,如果有JMP 这样的指令最爽了;没有的话,有LDR REG, ;JMP REG这样的也行。
保存地址方面,有MOV , #IMM这样的优先,其次就是看MOV REG,#IMM和MOV , REG这样的指令了。

要说总体效率最高的方案,应该还是switch/case的跳转表优化;Label As Values散转是快了,也不是不用加载东西,但是保存间断点消耗的东西也潜在多一些。

Tliang 发表于 2012-12-22 10:55:45

顶                        

smset 发表于 2012-12-22 11:06:55

本帖最后由 smset 于 2012-12-22 11:16 编辑

dr2001 发表于 2012-12-22 10:17 static/image/common/back.gif
简单查看了一下AVR的指令集,AVR上使用跳转表的门槛会比较高:
先要把表地址放到Z里边;用两个LPM从Flash ...


很有启发,如果暂不考虑通用性的情况下:

能否在WaitX时把pc指针(可能要加个1或者几),保存在_lc里, 然后在函数入口:把_lc压入堆栈, 然后return, 这样能否自动跳转到WaitX的下一语句处?
如果行的话,也可以考虑这个变种, 因为获取pc和操作sp,大部分单片机都支持。差异并不大。

这样return仅仅是返回到函数自己内部的某行,起到跳转的作用,goto和switch都不用了。也许开拓了switch和goto之外的一种全新跳转方式,而且效率极高。

仅仅是个想法而已哈。也许是异想天开。

dr2001 发表于 2012-12-22 12:20:25

smset 发表于 2012-12-22 11:06 static/image/common/back.gif
很有启发,如果暂不考虑通用性的情况下:

能否在WaitX时把pc指针(可能要加个1或者几),保存在_lc里,...

主要问题是插入代码的长度不好控制。导致后来一系列的东西都不好实现。
但是,对于Label as Values的实现来说,有可能简化很多平台上的代码。

这个是一个可能的思路,需要研究研究,有可能实现倒是。

smset 发表于 2012-12-22 13:09:28

dr2001 发表于 2012-12-22 12:20
主要问题是插入代码的长度不好控制。导致后来一系列的东西都不好实现。
但是,对于Label as Values的实现 ...

任务入口调用一个Myjmp函数,该函数只作一个事:把lc替换栈成里的返回地址,是否可以?求验证

ifree64 发表于 2012-12-22 14:00:51

本帖最后由 ifree64 于 2012-12-22 15:45 编辑

smset 发表于 2012-12-22 11:06 static/image/common/back.gif
很有启发,如果暂不考虑通用性的情况下:

能否在WaitX时把pc指针(可能要加个1或者几),保存在_lc里,...

这个想法很好啊,这样做能否实现?

voidsave_return(unsigned int idata* plc)
{
       // 栈顶是返回地址,这个返回地址+1(RET指令的长度)是下次重新运行的恢复地址
       *plc = (unsigned int)(*SP)<<8 + *(SP-1);
}

#define WaitX(tickets)do {timers = tickets; save_return(&_lc); return;} while(0);

由于不好实现返回值,任务里不返回值了。


经过验证,上面的方法不行。
通过WaitX插入的return语句将直接导致后面的代码被编译器优化掉。也就是return后面的代码因为“无法到达”编译器把它优化掉了。

smset 发表于 2012-12-22 16:06:26

不应该啊,原来的WaitX也有return

QXKJ 发表于 2012-12-22 16:20:35

越来越牛叉了,只是刚开始连C语言都没看明白.用法很特殊,一般书上很少介绍LINE

dr2001 发表于 2012-12-22 18:35:13

ifree64 发表于 2012-12-22 14:00 static/image/common/back.gif
这个想法很好啊,这样做能否实现?

voidsave_return(unsigned int idata* plc)


这么干肯定要被优化掉啊。。。
switch case的一个作用就是让case有希望被执行,这样才能保证不被优化的啊。。。

minier 发表于 2012-12-22 21:31:08

这个调度器很优秀,毋庸置疑!不过其尽管借用了photothreath的思想,但是主题还是时间触发的时间片调度器!应用范围受到时间片大小的限制!

smset 发表于 2012-12-22 23:10:02

本帖最后由 smset 于 2012-12-23 01:08 编辑

minier 发表于 2012-12-22 21:31
这个调度器很优秀,毋庸置疑!不过其尽管借用了photothreath的思想,但是主题还是时间触发的时间片调度器! ...

是的,目前延时触发机制基于10ms中断,任务内无法进行小于10ms的精细延时,这个是一个问题,下一版将对延时机制进行重造,将支持0.1ms级别非阻塞精确延时。

stm8s 发表于 2012-12-23 18:53:52

我在AVR上试用了一下,问一下楼主,几个版本,现在那一楼的版本最完善可靠?330楼还是?

zsmbj 发表于 2012-12-23 19:13:15

stm8s 发表于 2012-12-23 18:53 static/image/common/back.gif
我在AVR上试用了一下,问一下楼主,几个版本,现在那一楼的版本最完善可靠?330楼还是? ...

目前感觉还是493楼最好,不过中断调用不太喜欢。

smset 发表于 2012-12-23 20:55:54

本帖最后由 smset 于 2012-12-23 21:12 编辑

精细延时评测版:仅供功能示意,实现2个发光器件的PWM亮度渐变, 尚未优化。

简单说明:
采用定时器值作为延时依据,取消定时器中断。
由于直接与TH0进行比较,22 M 12T模式下,实际延时分辨率为138us.
WaitX内的值不再是ticket,而是us

#include <stc89c51.h>
/****小小调度器开始**********************************************/
#define MAXTASKS 3
unsigned char timers;
#define _SS static unsigned char _lc; switch(_lc){default:
#define _EE ;}; _lc=0; return 0;
#define WaitX(tickets)do {_lc=__LINE__+((__LINE__%256)==0); return (unsigned char)TH0+(((unsigned int)tickets)/138);} while(0); case __LINE__+((__LINE__%256)==0):
#define RunTask(TaskName,TaskID) do { if ((timers>0)&&(timers<=(unsigned char)TH0)) timers=(unsigned char)TaskName(); }while(0);
#define CallSub(SubTaskName) do {unsigned char currdt; _lc=__LINE__+((__LINE__%256)==0); return 1; case __LINE__+((__LINE__%256)==0):currdt=SubTaskName(); if(currdt) return currdt;} while(0);
/*****小小调度器结束*******************************************************/

sbit LED1 = P2^4;

sbit KEY = P3^2;
sfr IAP_CONTR = 0xC7;

//WaitX最大延时值:20148,--20.148ms
static unsigned int i;
unsigned intpwm;

unsigned char task1(){
_SS
while(1){
    LED1=0;
        WaitX(pwm);
    LED1=1;
           WaitX(2000-pwm);//总共延时2000us,500Hz pwm
}
_EE
}

unsigned char task2(){
_SS
while(1){
    P1=0xFF;
        WaitX(pwm);
    P1=0x0;
           WaitX(2000-pwm);//总共延时2000us,500Hz pwm
}
_EE
}

unsigned chartask3(){
static unsigned intj;
_SS
KEY=1;
while(1){
   //LED1 1秒渐亮
   for(j=0;j<2000;j++){   
      WaitX(500);
      pwm++;
   }
   //LED1 1秒渐灭
   for(j=0;j<2000;j++){   
      WaitX(500);
      pwm--;
   }
}
_EE
}


void main()
{
      unsigned char i;
      //-------初始化定时器---12T mode-------
      TMOD = 1;

      TH0=1;//22.0592M,TH0的1步对应138us
      TR0 = 1;
      //------------------------------
      timers=1;timers=1;timers=1;

      while(1){
                  if(KEY==0){ IAP_CONTR = 0x60; }

         RunTask(task1,0);
         RunTask(task2,1);
         RunTask(task3,2);

         if (TH0<144) continue; //运行20ms
         //
         TH0=1; //at 22.1184M,TH0的1步对应138us
         for(i=0;i<MAXTASKS;i++)        {
                     if(timers>143) timers=timers-143;
                   }
      }
}

yiming988 发表于 2012-12-24 10:11:19

这个是干货,很实用!

wxlcj 发表于 2012-12-24 13:30:51

本帖最后由 wxlcj 于 2012-12-24 13:36 编辑

530楼的不太好,因为每个系统设计人员一定都算好了时间基准了,现在这个最新的,反而退回去,每个任务还要重算时间。还有wait如果要移植,还要改,不太适合我这种笨人。
以前的好,建议还是按以前的思路设计,在main中不要有太多和硬件相关的东西。

wxlcj 发表于 2012-12-24 14:06:23

再问一个问题,如果想得到更长的延时,可不可以修改volatile unsigned char timers;
数据类型?

smset 发表于 2012-12-24 14:36:57

wxlcj 发表于 2012-12-24 14:06 static/image/common/back.gif
再问一个问题,如果想得到更长的延时,可不可以修改volatile unsigned char timers;
数据类型? ...

可以改的。自己按照这个方法改就是了。

至于530版本,等需要精细延时的时候,就会觉得很有用了,如果你的应用不需要精细延时,那么就用前面的版本即可。

530版本仅仅是用于功能测试,语法还未优化,也还未给出最简化的应用版本。

zsmbj 发表于 2012-12-24 15:17:57

smset 发表于 2012-12-24 14:36 static/image/common/back.gif
可以改的。自己按照这个方法改就是了。

至于530版本,等需要精细延时的时候,就会觉得很有用了,如果你 ...

直接改成int应该不行的。只有char型的没有问题。

举例如下:

以avr为例:如果是一个int类型的,RunTask(task0,0); 汇编如下:

1b8:        80 91 60 00         lds        r24, 0x0060
1bc:        90 91 61 00         lds        r25, 0x0061
1c0:        89 2b               or        r24, r25
1c2:        29 f4               brne        .+10           ; 0x1ce <main+0x30>
1c4:        79 df               rcall        .-270            ; 0xb8 <task0>
1c6:        90 93 61 00         sts        0x0061, r25
1ca:        80 93 60 00         sts        0x0060, r24

timer是16位的,保存在0x0060/61内存里。0x61放高位,0x60放低位。比如我们要延迟0x0110个tick,那么如果刚好汇编写入了0x61寄存器里的0x01后,产生了timer中断,那么中断里认为这个数据不是0,进行了-1操作,内存里变成了0x00ff,中断完成返回,这时我们再将0x60寄存器写入了0x10,结果内存里的数据变成了。0x0010,产生了一个错误。

应该需要对写入部分进行中断保护。



zsmbj 发表于 2012-12-24 16:16:01

smset 发表于 2012-12-24 14:36 static/image/common/back.gif
可以改的。自己按照这个方法改就是了。

至于530版本,等需要精细延时的时候,就会觉得很有用了,如果你 ...

你530的一个延迟最少是138us吧,没有看出低于138us怎么工作?

smset 发表于 2012-12-24 17:04:43

本帖最后由 smset 于 2012-12-24 17:07 编辑

如要低于138us,就找更快的频率,简单的就是设置工作在1T,AUXR=0x80, 那就是12us了。

dr2001 发表于 2012-12-24 17:19:17

zsmbj 发表于 2012-12-24 16:16 static/image/common/back.gif
你530的一个延迟最少是138us吧,没有看出低于138us怎么工作?

在不支持抢占的协作调度器下,小延时需要所有参与调度的任务,调度器调度算法的共同优化才能保证;否则有个长任务就会超时。

换句话说,协作调度下,“硬”延时比较难实现。不像抢占调度器的同优先级下的轮询调度模式,更容易保证一点。

smset 发表于 2012-12-24 17:37:27

dr2001 发表于 2012-12-24 17:19 static/image/common/back.gif
在不支持抢占的协作调度器下,小延时需要所有参与调度的任务,调度器调度算法的共同优化才能保证;否则有 ...

是的。 在1M指令执行频率下,太小的延迟,比如几个微秒以内的精确延时,实际运行已无发保证。

所以这个版本是定位在支持100us(=0.1ms)级别延迟精度上, 新版本是提供了一个新框架,从框架上,已经具备小于1ms的精细延时的能力了。

与以前的版本最少10ms精度相比,延时分辨率提升了近100倍。

至于如何避免某个任务函数消耗太长(不超过100us),以达到最终准确延时目的,包括所有任务的优化配合,是需要编程者去留意的。

我们是提供一个极省资源的通用多任务框架,尽量简化编程者的工作,而不是提供一个傻瓜化的框架。

wangyj173 发表于 2012-12-24 22:15:44

确实是个简单调度器!

smset 发表于 2012-12-24 23:32:36

本帖最后由 smset 于 2012-12-24 23:42 编辑

优化无止境,接493版本再接再励,其实main函数也是一个任务,不用很可惜的:

#include <stc89c51.h>
/****小小调度器开始**********************************************/
#define MAXTASKS 2
volatile unsigned char timers;
#define _SS static unsigned char _lc; switch(_lc){default:
#define _EE ;}; _lc=0; return 255;
#define WaitX(tickets)do {_lc=__LINE__+((__LINE__%256)==0); return tickets ;} while(0); case __LINE__+((__LINE__%256)==0):

#define RunTask(TaskName,TaskID) do { if (timers==0) timers=TaskName(); }while(0);
#define RunTaskA(TaskName,TaskID) { if (timers==0) {timers=TaskName(); continue;} }   //前面的任务优先保证执行

#define CallSub(SubTaskName) do {unsigned char currdt; _lc=__LINE__+((__LINE__%256)==0); return 0; case __LINE__+((__LINE__%256)==0):currdt=SubTaskName(); if(currdt!=255) return currdt;} while(0);
#define UpdateTimers() {unsigned char i; for(i=MAXTASKS;i>0 ;i--){if((timers!=0)&&(timers!=255)) timers--;}}

#define SEM unsigned int
//初始化信号量
#define InitSem(sem) sem=0;
//等待信号量
#define WaitSem(sem) do{ sem=1; WaitX(0); if (sem>0) return 1;} while(0);
//等待信号量或定时器溢出, 定时器tickets 最大为0xFFFE
#define WaitSemX(sem,tickets)do { sem=tickets+1; WaitX(0); if(sem>1){ sem--;return 1;} } while(0);
//发送信号量
#define SendSem(sem)do {sem=0;} while(0);

/*****小小调度器结束*******************************************************/


sbit LED1 = P2^1;
sbit LED2 = P2^2;


unsigned chartask2(){
_SS
while(1){
   WaitX(100);
   LED2=!LED2;   
}
_EE
}

void InitT0()
{
      TMOD = 0x21;
      IE |= 0x82;// 12t
      TL0=0Xff;
      TH0=0XDB;
      TR0 = 1;
}

void INTT0(void) interrupt 1 using 1
{
    TL0=0Xff;    //10ms 重装
    TH0=0XDB;//b7;   

    UpdateTimers();
}

#define DelayX(tickets)timers=tickets; \
while(timers){\
RunTask(task2,1);\
}


void main()
{
               InitT0();
                while(1){                     
                    DelayX(100);
                  LED1=!LED1;
                }
}

其实DelayX本质是阻塞式延时,因为是while 里面一直等,但实际上又没阻塞其他任务。
{:lol:} 感觉好像又变为前后台了?但又不是。不管是什么,反正两个LED还是在闪,而Rom又降了10%以上。

tonyone 发表于 2012-12-25 21:44:10

凑个热闹,mark下

huilai 发表于 2012-12-25 22:01:27

调度器,看不懂~,mark

freeboyxd 发表于 2012-12-25 23:38:57

请教一下楼主,在多语句宏定义中为什么有些用了do{}while(0)语句,有些又没有用呢?没用的会不会在某些编译环境下产生错误,或是有相关的风险而不利于移植啊?

dr2001 发表于 2012-12-26 07:47:12

freeboyxd 发表于 2012-12-25 23:38 static/image/common/back.gif
请教一下楼主,在多语句宏定义中为什么有些用了do{}while(0)语句,有些又没有用呢?没用的会不会在某些编译 ...

如果宏包含的内容是一段完整的代码,那么最好用do {} while(0),这也是常见用法。
如果宏里内容是有特殊语义的,比如switch(){ // }这样的,就不能保护。同时,这样的宏的使用是必须加以特别注意的。

丅輩孑_变壊 发表于 2012-12-26 08:20:19

MARK......

smset 发表于 2012-12-26 12:45:34

本帖最后由 smset 于 2012-12-26 13:10 编辑

ifree64 发表于 2012-12-22 14:00 static/image/common/back.gif
这个想法很好啊,这样做能否实现?

voidsave_return(unsigned int idata* plc)

。。

这么干肯定要被优化掉啊。。。
经过验证,上面的方法不行。
通过WaitX插入的return语句将直接导致后面的代码被编译器优化掉。也就是return后面的代码因为“无法到达”编译器把它优化掉了

---------------------------------------------------------------------------

自己加个判断就可以避免被优化掉:

#define WaitX(tickets)do {timers = tickets; save_return(&_lc); if (_lc) return;} while(0);


新跳转方案我已经实现并验证了,我是在有返回的任务函数版本上实现的,可以保存当前执行位置,并在下次跳转到上次保存的地方,和goto label效果类似,但无需编译器做特殊的goto Label 支持。

至于运行效率嘛,和Switch(有优化)相比,并不算高,当任务内断点数量较少时,不如switch,可能当任务内断点数量较大时,才可能体现出优势。

至于如你所说,GCC编译器无法用Switch优化的话, 用这种新的跳转方案,应该是很有优势的。


wangyj173 发表于 2012-12-27 08:44:43

经典啊,有用,

weiyix 发表于 2012-12-27 09:37:58

多谢lz分享!

Syth 发表于 2012-12-27 16:09:12

学习了很多,谢谢

freeboyxd 发表于 2012-12-28 23:01:42

楼主,如果将定时器初始化函数改成宏,还可以节约4bytes ROM
void InitT0()
{
      TMOD = 0x21;
      IE |= 0x82;// 12t
      TL0=0Xff;
      TH0=0XDB;
      TR0 = 1;
}

#define InitT0() {\
    TMOD = 0x21;\
    IE|= 0x82;\
    TL0= 0x60;\
    TH0= 0xF0;\
    TR0= 1;}

wxlcj 发表于 2012-12-29 15:07:34

再提个建议,能不能把_SS和_EE改个名字,比如ThreadStart(),ThreadEnd()这样表面看起来是个函数,更好理解一点。

smset 发表于 2012-12-29 15:40:09

wxlcj 发表于 2012-12-29 15:07 static/image/common/back.gif
再提个建议,能不能把_SS和_EE改个名字,比如ThreadStart(),ThreadEnd()这样表面看起来是个函数,更好理解 ...

也可以的,C语言就是很灵活哈。

holts2 发表于 2012-12-29 18:36:51

smset 发表于 2012-12-29 15:40 static/image/common/back.gif
也可以的,C语言就是很灵活哈。

不改,它本来就不是个涵数, 就这样挺好

wxlcj 发表于 2012-12-29 21:02:28

holts2 发表于 2012-12-29 18:36 static/image/common/back.gif
不改,它本来就不是个涵数, 就这样挺好

我的意思是不让新接触的人陌生,Protothread也是用了很多#define,但它就是很好的包装了一下,这样使程序看起来舒服。

feverkim 发表于 2012-12-29 22:23:41

收藏起来,慢慢研究

lzh7735 发表于 2013-1-1 14:01:53

牛人,学习了

freeboyxd 发表于 2013-1-1 19:09:07

小小调度器学习心得,如有不妥还望指正:

任务函数中不可随意另外使用return();否则会造成程序运行出错。
如确实要提前返回则应在返回语句前增加_LC=0;语句以确保下次调度时任务函数可从头执行。

cq-317 发表于 2013-1-2 10:15:54

不错啊,参考一下~~

freeboyxd 发表于 2013-1-3 21:06:06

TO: smset

541楼 的任务2
unsigned chartask2(){
_SS
while(1){
   WaitX(100);
   LED2=!LED2;   
}
_EE
}
改写成如下这样,还可以节约10字节ROM
unsigned chartask2(){
_SS
while(1){
   LED2=!LED2;
   WaitX(100);
}
_EE
}
只是使用有一个小小的局限性,部分任务是完全可以这样写的

Tomas_Yung 发表于 2013-1-3 21:12:50

mark
/////

xingh8009 发表于 2013-1-3 21:41:11

收藏一下,谢谢楼主

eddia2012 发表于 2013-1-4 19:42:56

期持新年楼主给出最好的一个版本!

freeboyxd 发表于 2013-1-4 22:16:10

没有最好,只有更好!

PPMZZ 发表于 2013-1-4 22:24:04

通用小小调度器记号。

propor123 发表于 2013-1-4 23:05:13

好,学习一下。

happy_andy 发表于 2013-1-5 09:41:20

新思路,新观点,好的程序员就要勇于突破自我!

oszp1688com 发表于 2013-1-5 14:59:10

看了半天 硬是没看懂   希望给多点注释看懂的朋友也出来指点下 谢谢

freeboyxd 发表于 2013-1-5 21:09:53

慢慢看,多看几遍,会看懂的!另外还是得多动手

Gorgon_Meducer 发表于 2013-1-6 11:24:42

感觉简单事情弄复杂了……执着于时间调度害人不浅……
按道理应该没有所谓大任务和小任务的差别……
protoThread说白了就是利用switch的label列表特性建立了一个规则:
遇到需要长时间等待或者打断当前顺序流的任务就break,等待下一次label重定位。在这种前提下
实际上任务的粒度完全可以通过增加break的数量来降低一个任务的优先级——或者说,break的
数量约少,一个任务占用处理器的时间越长,看起来就是优先级越高了。

我还是那句话,研究如何调度是本末倒置的。应该研究如何以事件驱动或者说数据流的角度设计
多任务,让他们能有效地协作从而提高处理器效率;而不是先研究如何去协调多任务,尝试获得
所谓的效率——这没什么好研究的,操作系统已经提供了方案,就是用信号量啊,临界区阿之类
的工具——简单说:
1、你找一种实现多任务的方式:要么操作系统,要么裸机上跑状态机。protoThread是一种傻瓜
   状态机写法。
2、用操作系统提供的概念来解决多任务协调问题。

所以说,上面的1、2两条没什么好研究的。这些都是工具,而如何使用工具才是关键啊!或者说
如何面对一个应用需求设计多任务(Task Design),这些多任务自然会用到1和2提供的便利。
一个糟糕的多任务设计不会因为有一个优秀的多任务协调机制或者调度方法就会变得优秀;一个
优秀的多任务设计不需要专门的操作系统和现成的协调机制也能跑得很欢畅;一个优秀的多任务
协调机制最多让把糟糕的任务隔离起来从而不影响其它任务——所以你看到操作系统一堆一堆的
研究独立地址空间,优先级,时间片——其实就是为了把任务都隔离起来,这是最笨的方法——
因为从操作系统设计者的角度来说,他们没法左右操作系统用户的行为——也就是说操作系统设计
者没法限制用户如何设计它们的任务,所以只能做自己能做的事情——把任务隔离起来,你用你
自己的,别影响别人——问题是,我留多少处理器时间给你呢?事情就是这么复杂起来的。

我们做嵌入式设计,大的系统不说,MCU,整个系统任务设计是我们自己做的,我们自己即是
操作系统设计者也是任务设计者,那么就不要做这么傻的事情了。我们应该先考虑如何设计多任务
获得最高的效率,再根据多任务的协调要求去安排(裁减、定制)下面所需的操作系统结构及其
服务程序(信号量、临界区啊……之类的)

看到很多人研究调度器,我很感兴趣,也很开心。但是我发现大部分人感兴趣的原因是似乎想通过
研究操作系统来解决任务设计的问题,这个就不好笑了……完全是南辕北辙。打个比方,这就像我们
试图通过学习Photoshop的使用技巧来尝试做出好的平面设计一样——Photoshop只是工具,你熟练
使用工具只是第一步,再熟练也不能替代平面设计的核心——也就是设计思想、理念、构图设计、
色彩运用……类比回来,任务设计是我们真正想搞明白的,但你通过研究操作系统,是无法达到这个
目标的。

最后说一点,学任务设计之前,你最好已经掌握了基本的工具——比如操作系统的一些基本概念,如果
工具都还没有掌握,就去思考任务设计的问题,可能有点空中楼阁了……

smset 发表于 2013-1-6 13:52:27

本帖最后由 smset 于 2013-1-6 14:17 编辑

感谢傻孩子版主的参与,写这个帖子的目的,一直都是希望抛砖引玉,等待更好更多的想法出现。

我觉得呢,写这个小小调度器的目的,还是把复杂的事情简单化,图难于易,这是人类的天性,呵呵。

至于最后真的是把事情搞复杂了,还是搞简单了,这个可能从不同的角度得到不同的结果。

打个比方吧:

就如同高等数学一样,绝大多数公式,都可以从几个基本公式一步步推导出来,只要理解了其原理就好办,

对于喜欢数学的同学,并不用死记硬背,还能从推导过程感受数学的乐趣,这比硬记一堆公式简单多了嘛。

但是,对于另外一些同学来说,俺对数学不感冒,不觉得推导公式有啥乐趣,我就是想完成老师布置的作业而已,有现成的公式不记,自己去推导,不是没事找抽么?

所以,究竟是理解数学原理更简单,还是死背几个数学公式更简单呢?可能真的要因人而已了。

还是回到主题吧,您说的很具体:“实现多任务的方式:要么操作系统,要么裸机上跑状态机。protoThread是一种傻瓜状态机写法”。

核心需求确实是: 我要实现多任务。我只是附加了一个条件是: 用最小的代价,实现多任务。

那么解决方案是:

1)操作系统, 这个可能我就要拿附加条件来说事了,操作系统代价不小,一个是cpu资源可能不允许,另一个理由是我很懒,并不想投入太多精力,或者换句话说,我仅仅是需要实现多任务机制而已,其他的什么文件系统,内存管理,网络通信协议,GUI等等,我都用不着,也装不下,难道非要上操作系统?

2)状态机,状态机确实是一个很好的东西,而且傻孩子版主在这方面有很深的造诣,这是我非常佩服的。所以我签名的开头是: 真正的程序员用状态机,聪明的程序员用调度器。我只是觉得状态机让我必须以机器的风格来思考问题,当写处理逻辑很复杂的任务时,觉得我的大脑快变成机器了,然而我是人,希望用更符合人类的思维方式来写程序,但是一直没有找到好的办法。直到后来,我遇见了----3)(是指protothread,不是小三哈),用一句话表达当时的感受: 相见恨晚!为什么不让我在十年前就遇见你?

3)protothread, protothread真是一个天才的发明,它借用了状态机的躯体,竟滋生出多线程的灵魂! 其实它是很叛逆的:出生于状态机家庭,却从思想上背叛了状态机,甚至要革状态机的命。 它恰好满足了我的需要:用更符合人类的思维自然方式来写多任务功能的程序。所以从一接触,我就爱上了它。然而,随使用的增多,我逐渐发现了一些不完善的地方,由于其延时机制的实现方面并不完善,导致代码体积过大,调度效率也较低。我决定改变一下,因此有了---4)

4)小小调度器,如帖子所说,小小调度器吸取了protothread的精华,然而重造了一个延时实现机制,同时增加了一些宏,使得任务函数语法更加简洁,同时大幅提高了代码效率(这是最主要的一点)。我相信,至少从代码的编译和运行效率来讲,这个版本优于protothread,更易于推广和使用。

当然,小小调度器本身,只是一个调度器而已,所以不要用操作系统的眼光来要求它。

Gorgon_Meducer 发表于 2013-1-6 16:28:35

本帖最后由 Gorgon_Meducer 于 2013-1-6 16:56 编辑

smset 发表于 2013-1-6 13:52 static/image/common/back.gif
感谢傻孩子版主的参与,写这个帖子的目的,一直都是希望抛砖引玉,等待更好更多的想法出现。

我觉得呢,写 ...

有两点是要纠正的:
1、用状态机并不是要从机器的角度来理解程序,而是要从应用逻辑的角度来理解和描述程序,这也是人的自然思维。
2、并不因为你选择调度器就背离了它操作系统的本质,只要你写多任务代码,操作系统就存在,只不过像一个滑动变阻器:
    最左边是人脑,最右边是完整的操作系统代码。先说从右到左,这是逐渐将操作系统的概念从代码转变为逻辑概念放到
    人脑中——也就是你要在写代码的时候有对应的概念,而不是依赖代码提供的便利;最左边就是我们大家熟悉的裸机了。
    从左到右的过程正好是操作系统发展的过程,人们首先有了操作系统的概念,在资源允许的情况下逐步将一些逻辑通过
    代码的形式固化下来,从而简化后面的使用……

当我们使用调度器的时候,实际上意味着相当一部分操作系统逻辑必须积存在开发人员的大脑中并以此指导多任务的编写;
这并不是说你就可以摆脱操作系统的束缚——从而觉得一身轻松——或者觉得自己可以不遵守操作系统的规约了。

你认识中的操作系统恐怕都是Linux级别的吧……ucos也是操作系统阿……本质上操作系统就是一个调度器加一堆信号量而已,
至于你看到的恐龙级别的操作系统,那是又额外加入很多其他东西的华丽版本……

把调度器不看成操作系统,是你对问题看得太乐观了。该有的从来都一个不少。

我回这个帖子的目的不是要打击你研究调度器的积极性,相反我希望越来越多的人研究这个,毕竟这是学习工具的一个必经
之路,绝对不可以说已经有现成的,所以就阻止后来自己重新走一遍这个必经之路。我是发现很多人学习这部分知识的最终
目的并不是调度或者操作系统本身,而是想解决 任务设计 中遇到的问题,所以我只是提醒下大家,在已经掌握工具的情况下
更应该关注任务设计的内容,而不是工具本身。

关于简单问题弄复杂,我想更清楚地解释下:
首先,我注意到讨论中花费了很多时间在时间片调度上。同时讨论了10ms粒度的问题。这就是一个典型的尝试以操作系统(
调度算法)本身来解决任务设计的例子——显然本末倒置了。本质上,时间片和时间触发是两个概念,然而这里我发现很多
情况下大家是混淆来用的。

时间片:每个任务分配固定的处理器时间,这是在抢占系统中很常见的策略
时间触发:以时间为事件的触发源,比如触发event, mutex, semphore... 时间触发可以广泛的应用在抢占和非抢占系统中

前面贴子讨论的问题中,有一个10ms粒度的问题……显然是在一个非抢占系统中尝试去实现时间片的概念,同时以为自己
使用的是时间触发。单纯的时间触发并不需要专门在定时器里面进行任务调度,前后台模式下,最佳的处理方式是,所有
任务都在前台执行,并且即便用protothread结构,也可以通过加入额外break来降低任务优先级(任务粒度),从而保证
高优先级的程序获得足够的处理器资源,同时保证整个系统已最优的状态运行——这就不存在所谓10ms粒度的问题。

至于需要用到时间触发的场合就是在定时器中断里面设置对应的信号量就可以了。

所以,扪心自问,前面的讨论中,哪些是混淆了时间片和时间触发的?哪些调度器结构是不需要的?希望有所帮助。


回头又看了你的回帖,突然想到,你会喜欢protoThread,并且有相见恨晚的感觉,其实并不是你感慨于它的效率有多高,
而是他让你能以非状态机的形式写状态机效果的代码——其实你是对状态机有恐惧的,这个恐惧就写在你的回复里面,你
基本上就等于在说用状态机写代码简直就是“匪夷所思”的(非人类的……)。

其实……也许后面的话,我说已经没有什么说服力了——因为你认为我已经很擅长状态机开发了——所以当然不觉得难。即
便如此,我还是想说,你需要正视这个恐惧,并且认真去学习状态机思维——并不是要你去用机器思维,而是要你去用流程
图的模式去思维——流程图会画吧?然后你认真学学如何把流程图翻译成状态机——我有专门的帖子——学懂了,好好的
做几个应用,你就会明白,其实状态机是最傻瓜的……至于ProtoThread,我们爱它只是因为它轻量级,并且效率还是不错
的。

你一定要克服对状态机的恐惧,从翻译流程图到状态机开始,坚持一个星期就可以了……这只是一个窗户纸那么薄的东西……

dr2001 发表于 2013-1-6 17:10:41

之前的代码主要实现的是“任务休眠/挂起不少于指定时间”的语义。涉及的是
1、任务状态转换,运行->"挂起"态;
2、基于系统时基(Tick)的事件触发行为,当然该行为基于调度器轮询实现,开销较大,其导致任务状态"挂起"->运行态。

我并不认为smset的代码中涉及到了时间片的概念。
因此,对于代码中所涉及的语义,在Tick消耗可以接受的前提下,讨论Tick的粒度是有意义的。

了解工具,改造工具,应用工具。重要的是应用场景需要这样的工具使得任务能被方便快速的实现。

Gorgon_Meducer 发表于 2013-1-6 17:35:52

dr2001 发表于 2013-1-6 17:10 static/image/common/back.gif
之前的代码主要实现的是“任务休眠/挂起不少于指定时间”的语义。涉及的是
1、任务状态转换,运行->"挂起" ...

信息是不对称的,从你的角度出发,这个帖子讨论的内容是工具本身,并且是恰当的,我同意。但是楼主实际上
是把调度器与操作系统割裂来看的,这是我希望他能注意到的。

至于这个帖子本身,我觉得非常赞!还需要持续关注!持续顶!从趋势上看,楼主开始向 小小调度器 加入优先级了,
这是非常有趣的——因为协作式调度器的优先级其实是任务本身用脚投票的——谁占用的处理器时间多,谁的优先级
就大,而protoThread的特征就是“死不放手”——除非遇到需要等待状况——这当然和普通的跑RTOS的习惯一致,但
普通RTOS有抢占式的时间片,所以死不放手无所谓——在合作式调度环境下protoThread写出来的代码,人人都死
不放手……这个就……楼主试图通过一个专用的定时器任务来保证某些特权任务,我觉得思路是没有问题的,但是否可
以换个角度——搞清楚脚投票的本质,直接把优先级低的任务的粒度变小(多加点yield)就直接实现了优先级的问题
岂不是多了一种手段?

smset 发表于 2013-1-6 18:30:47

本帖最后由 smset 于 2013-1-6 18:48 编辑

Gorgon_Meducer 发表于 2013-1-6 16:28 static/image/common/back.gif
有两点是要纠正的:
1、用状态机并不是要从机器的角度来理解程序,而是要从应用逻辑的角度来理解和描述程 ...

嗯,您说的很有道理。

可能本帖中有部分回帖认为: 小小调度器是 把时间片调度和protothread结合起来的一个产物。这个可能导致了傻孩子版主的误解,以为我们一直在讨论时间片方面的东西。

其实这些回帖的说法是片面的, 小小调度器和时间片设计模式,没有半毛钱的关系,至少从设计思想上是这样的,和时间片并不相干,用10ms,和用1ms,其实没啥区别。

也许有些和时间片调度代码有相似的感觉,我可以说,那纯属巧合,因为一个不好意思的理由: 我还没仔细拜读过时间片设计模式这本书的内容,只是大概看过其简介。

至于后来,设计到高优先级任务机制时,才真正讨论到一些10ms粒度的概念,那个也仅是从应用经验的角度出发,取一个比较现实的例子而已。

另外,在任务里多加点yield,确实是降低任务优先级的一个基本方法,其实就是让任务更具协作性,主动放出CPU,哪怕WaitX(0),也会让其他任务获得执行。
-----------------------------------------------------------------------

其实,另外一点,对于状态机,我也并不是真正的恐惧,我也相信,在经过众多先行者的不断努力(包括傻孩子版主的贡献)下,状态机设计工具和指导理论日渐完善的今天,其实不用花很大的力气,经过一些学习,也能顺利掌握其设计方法,并形成一种流畅的设计方法。   

既然说到这里了,顺便往旁边拐个弯: 其实PLC的功能顺序图设计方法,也是典型的状态机设计方法,是纯粹的事件驱动机制,有成熟的设计理论,而且纳入了国际自动化编程标准,我认为其有值得借鉴的地方, 我甚至专门学习plc编程的书籍来掌握这种状态机设计理念,并一度把PLC的功能顺序图编程法的思想纳入到单片机的状态机写法中,因为这样有个好处, plc的代码和单片机的代码从编程思想上兼容了。会plc编程的几乎可以无缝过度到单片机编程,同样会单片机编程的几乎可以无缝过度到plc编程。。。。,嗯,扯远了。有兴趣以后可以在这方面交流吧。

而让我暂停状态机这条路,最终走向小小调度器的,是CPU资源,因为我遇到了一个实际的问题:我做的项目上,必须用的cpu(无法改换),仅仅只有512字节RAM, 8K字节ROM,而且我希望它干更多的事情。

在这个情况下,我必须进行抉择,所以在写出小小调度器的一个基本的理由是: 对节省CPU资源的极致追求。。。。

Gorgon_Meducer 发表于 2013-1-6 21:57:10

本帖最后由 Gorgon_Meducer 于 2013-1-6 22:08 编辑

smset 发表于 2013-1-6 18:30 static/image/common/back.gif
嗯,您说的很有道理。

可能本帖中有部分回帖认为: 小小调度器是 把时间片调度和protothread结合起来的 ...

如果想节省资源,沿着你的路走下去就可以了,因为实际上目前的小小调度器就是裸机,巧妙的借用了宏来固化很多逻辑。
哈哈哈,不如知道为啥,一说到宏固化逻辑,我就想到之前很多人说用宏以后不好调试{:lol:} 我支持你~!

白天的时候突然看到这个帖子,楼太多了,的确没办法一楼一楼的看,所以囫囵吞枣,看到很多人在说时间片,就产生了
误解,不过我的初衷还是引导大家更多关注多任务的设计上,因为工具本身已经得到了500多楼的关注,很火爆哈。

如果可能,我希望你能开一个帖子,大家一起来讨论讨论多任务的设计。你知道我最近在筹备这方面的资料,看到大家的
讨论也能给我很多有用的信息,比如大家关心什么,疑问是什么……甚至很多时候能给我提供非常典型的例子。

不管怎么说,看到你如此耐心,我还是要谢谢你~

BTW,感觉你说话太谨慎了~都是做技术的,用敬语距离就远了,虽然我言语中经常流露出自负的语气,那不过是我自我娱
乐而已,牛人多了去了,我就是经常出来搬一些书本上已经有的东西混个脸熟而以~我最喜欢交流,也希望你放开一些,我
并不一定比你厉害,直率的交换自己的想法才叫交流~

dr2001 发表于 2013-1-6 22:19:02

本帖最后由 dr2001 于 2013-1-6 22:32 编辑

Gorgon_Meducer 发表于 2013-1-6 17:35 static/image/common/back.gif
信息是不对称的,从你的角度出发,这个帖子讨论的内容是工具本身,并且是恰当的,我同意。但是楼主实际上 ...

顺路跑题一下,对这个讨论的内容,我目前的完整看法是这样的:

借用Revisit Coroutine这个Paper中的定义:如果函数是多入口多出口的,那么可以称之为Coroutine。
其主要的语义有Resume/Call和Yield两个(实际上这是非对称Coroutine,对称Coroutine可以用非对称的处理,见最后的说明)。
语义:调用:从函数开始或上次Yield的地方继续运行;间断返回/完成退出:交还控制权到调用者。

如果是Stackful的Coroutine,即跨越Yield自动变量能保留的情形,在C语言和MCU环境下,我认为就是RTOS中Thread的概念。
因为该情况下,每个Coroutine都需要保存自己独立的调用栈。

如果不想消耗那么多的内存在调用栈分配上,毕竟需要预留调用栈资源;如果动态分配栈,C处理器来会很麻烦,还有内存碎片问题,无MMU的话。
那么就只能使用Stackless的Coroutine。而ProtoThread就是Stackless的C实现。
此时,Yield Point以及内部自动变量,要么static,要么保存在堆上。不依赖于栈。

状态机方法可以表达ProtoThread,所以二者的等价性就不讨论了。

从这个层面上,其实ProtoThread和一般RTOS的Thread是可以类比的:
都可以从CoRoutine的层面上来认识,实现上会受到Stackful和Stackless的实现的制约。

这样来看,Stackful的很多东西实际上是可以在Stackless实现上借鉴的:
1 Stackless调度可以引入抢占优先级的概念:即调度器始终优先运行高优先级任务。(对应RTOS的抢占调度。)
2 Stackless调度可以引入轮转调度的概念:当优先级相同的时候,大家采用轮转方案。
3 自然,在轮转调度中,一种用于限制同优先级的某个任务过多占用处理器资源的基于时间片的限制机制也可以被引入:不是去限制用的多的人,而是多补偿用的少的人,从而达到某种调度公平。这个有点借鉴Linux CFS调度器的想法。
到这里,实际上RTOS Thread(Stackful实现)调度器核心的几个大部分基本上可以类比了。

然后,RTOS的Thread抢占发生且只能发生于外部事件或者Thread Yield的情形。对应Stackless的实现,Yield是直接返回调度器;和Stackful实现等价。
问题在于需要解决外部事件导致的调度:高优先级任务的抢占;同优先级任务的轮转。

特别注意到,我们使用的是Stackless的实现,意味着Yield之后不需要额外的Stack保存状态信息。
4 因此,我们完全可以在事件中断结束后中发起一轮调度,并且继续使用原有的堆栈运行。等到新的Stackless Coroutine Yield之后,我们还可以回退到原有的现场继续运行。这是Stackless带来的最大好处。

4-1 这样的话,由于Yield事件直接带来的抢占操作直接由调度器解决;外部事件带来的抢占操作,通过中断退出后由调度器构建的新现场解决。

4-2 唯一剩下的就是同优先级轮转,除了上边提到的时间片补偿策略以外,可以类比Linux的nice值,构建一个第二级的抢占优先级,从而使用一种有限的抢占策略达成另外的一种公平性。

这样的话,只要限制抢占的级数,就能限制住对堆栈的消耗量。而且只需要一个堆栈。

如果基于中断嵌套,在中断中直接调度器而不退出中断,构建现场那么操作;那实质上是基于中断控制器这个更高级的“抢占调度器”进行的更高级的调度行为。

对于对称CoRoutine,实际上就是同优先级的调度的一个处理技巧,可以通过group或者类似mutex的机制搞定;不碍大事。

这样做,实质上是一个Stackless 伪RTOS的框架。
它基于ProtoThread这样的Stackless实现;最大的好处是只需要一个堆栈(实质上是把本来存在堆栈上的东西放在全局存储区或者堆上)。

对于重入之类的问题,如果能够较好的解决内存碎片问题(基于Block分配么……),从堆走就不是问题了。

Gorgon_Meducer 发表于 2013-1-6 22:32:22

本帖最后由 Gorgon_Meducer 于 2013-1-6 23:15 编辑

dr2001 发表于 2013-1-6 22:19 static/image/common/back.gif
顺路跑题一下,对这个讨论的内容,我目前的完整看法是这样的:

借用Revisit Coroutine这个Paper中的定义 ...

不知为啥……感觉你总是能用理论论证一堆显而易见的事情……当然啦,我是羡慕嫉妒恨。
结论是妥妥的。这个公用栈也是我非常喜欢的东西,所以我宁可构造复杂的基于函数指针
的调度方式,也不去构造抢占式的RTOS。因为,一旦涉及到RTOS,你就要给每一个任务
都分配一个独立的栈,无论怎么分配,都是有浪费的,但是公用栈就可以很自然的解决这
个矛盾。当然,因为裸机下(有调度器也一样),存在任务的自动变量会失效的问题,
因此任务中需要使用堆分配的变量,或者在使用状态机的情况下,在状态机调用中需要人
为构造栈结构来处理任务的变量,这种人为处理栈的方式好处有以下几点:
1、大小和规模都是确定的(deterministic)
2、允许我们通过栈帧的方法让不同的任务共享整个栈池,从而不用考虑为每一个任务独立
    分配专用栈的问题。(share stack)
3、能实现可充入的任务

从这个角度来说,这个stackless RTOS真是好东西~

关于共享栈池,我其实研究过一个动态优先级算法,用来处理类似下面的问题:

1、考虑存在A,B,C,D若干个任务,这些任务都要消耗某一个资源S,这些资源通过semaphore
    进行维护。
2、当任务执行的时候,任务会消耗一定数量的Sx,当任务执行完成后,会释放自己消耗的资源Sx
3、考虑一个极端的情况,假设任务A消耗的资源S是最多的,并且等于资源池中资源的数量,其它
    任务消耗的资源都小于A,则为了让这些任务都得到执行,需要在semaphore中引入一个特殊的
    算法——动态优先级算法。

该算法描述如下:
    当一个任务通过semaphore申请资源时,该任务需要提供两个信息:所需的最大资源数以及优先
级。semaphore将该任务压入一个根据优先级进行排序的队列中
    当有任务释放资源或者排队队列为空时,semaphore从队列的头部开始向后依次执行如下操作:
    a. 检查排队者所需的资源数量是否可以满足,如果能满足,则分配资源,并将排队者删除;如果
       排队者所需的资源无法满足,则将其优先级加一
    b. 向后搜索下一个元素,重复a,直到没有元素为止

通过这个算法,不仅可以解决共享栈的问题——有效的防止 “饿死” 现象的发生,同时还能让所有
申请资源的人——无论优先级如何,都得到照顾。同时,值得注意的是,死锁现象也是可以避免的。
   
如果把处理器时间也看成资源,那么实际上这个算法对于处理任务优先级以及同优先级的轮训也是
有效的。显然,高优先级的任务会首先被执行,同优先级的任务会遵循FIFO规则,低优先级的任
务会逐步提升自己的优先级从而获得执行机会。
这个算法的缺点是,每一次调度都要搜索整个链表,显然在调度开销上有点大。

最后关于碎片问题,我觉得从MCU的级别上来说,直接用Frame分配策略就可以了,代码小,维护
简单,效率高,而且可以分级别建立堆:对小对象,提供小Frame或者干脆提供malloc算法;对于
大对象,则可以把尺寸相近的放在一起,建立一个Frame堆——实际上你哪怕给所有要动态分配的
资源都建立自己的堆,也不算浪费——因为你以为MCU能有多复杂的应用,实际上需要动态分配
的资源并不多,有时候偷懒,一大一小两个Frame堆就Over了,再复杂,大中小三个堆也就妥妥的。
碎片啥的都是浮云阿……

smset 发表于 2013-1-7 10:37:35

本帖最后由 smset 于 2013-1-7 11:20 编辑

现在讨论更加深入了,这是我更期待的。

说真的,看了dr2001和傻孩子的帖子,我觉得要学习的东西还很多,你们的编程知识确实比我深厚(不是我在谦虚哈)。

论坛里比我编程功力深的数不胜数,其实只要仔细查看我写出的例子代码,会发现我是一个对于单片机编程很随意和初级的人,而且毫无良好的编程习惯(这是我的最大缺点)。

我之所以写这个小小调度器,可能仅仅是机缘巧合,然后我恰好对此有兴趣,把较多时间聚集到在这一个点上而已。
-------------------------------------------------------

继续回到讨论内容吧,确实堆栈的处理,是RTOS最关键的地方,大部分RTOS都是要求任务拥有自己的堆栈,带来的好处是更完善的运行环境保存和恢复,但是代价是RAM消耗以及为了维

护这些堆栈带来的代码消耗。

另外,dr2001回复的: “完全可以在事件中断结束后中发起一轮调度,并且继续使用原有的堆栈运行。 ”

这个对于原生的protothread,这是没有问题的,因为它的时间判断机制是在任务函数内再次进行了时间比较。

但对于小小调度器来说,是不行的, 因为对于小小调度器来说,函数内已经没有时间判断环节了,只要进入函数,就意味前面的等待时间已经到了,直接是执行后续的代码。


从节省资源的角度来看,无堆栈是有好处的,带来的代价是动态局部变量无法保存,所以只好不用,另外对于任务内临时使用大数组的情况,用动态内存分配方法,其实是使用公共堆,在这种环境下是一个较好的补偿措施,

在任务内仅仅保存一个静态指针就可以了,这样既节省了RAM的消耗,又不存在数据内容丢失的问题。我以前用protothread的时候,就是这么干的。

--------------------------------------------------------------------------------------------

另外,对于stackless的实现方式,并非只有protothread一条路可走, 这个以前的帖子上讨论过:

在任务需要Wait的时候,可以调用一个save函数,这个函数通过SP指针把调用的位置取出来,保存在任务函数的lc变量里,然后通过return退出任务函数,从而让出CPU,同时也释放了栈空间。

而在任务函入口,则设计调用一个jump()函数,跳转到lc处。

这个jump()函数的实现很简单,就是把lc的内容(就是WaitX保存的上次运行位置)直接赋给SP指向的地址,然后return; 这样就实现了一个函数内部的跳转,和goto的效果差不多,支持跳转到变量指定的位置上,还无需编译器的特定支持。

其他的RTOS一般是跳转到另一个任务函数的某个执行点----但是带来的就是独立堆栈的保护, 而这个方式实现的函数内部跳转,则避开了这个问题,因为毕竟是函数内部的跳转,情况就简单多了,任务还是公用一个堆栈。

这样做,可能感觉更接近“正统”的调度内核,还是那句话,毕竟protothread太叛逆了,很多人在展开protothread宏后,被其怪异的语法吓得目瞪口呆。

而通过save(),jump()来实现,可能大家理解起来反而更容易,这个版本我还在测试,完善后我会贴出例子代码。

也许这样,还可以把哪个"伪RTOS", 的伪字去掉吧,而我其实无所谓称之为什么,内心还是觉得伪的更妙,就感觉能用豆腐做出猴脑的味道,对资源的要求更小,却达到相同的效果,岂不更妙。

dr2001 发表于 2013-1-7 11:30:19

本帖最后由 dr2001 于 2013-1-7 11:33 编辑

呃,暂时不要把问题局限在51以及现在所写的代码上,可以看全面一些。然后根据实际情况选择一部分需要的功能剪裁。
先弄明白工具可以有什么功能;然后是对于解决现在问题,什么样的工具最趁手。这样容易得到想要的东西。

我说的内容并不专门针对目前的代码。你所引用的内容主要是说为什么可以单堆栈重入并且如何通用的实现之。
原理和目标清晰了,具体代码实现总是可以有的,而且绑定到具体环境,通常会有更优的方案。

针对特定的体系结构,编译器/ABI,比如51,Keil MDK,自然可以实现出具有强依赖性,更为精妙的代码,优化而已。

powerlabor001 发表于 2013-1-7 12:27:18

我也mark一把。

eddia2012 发表于 2013-1-7 17:11:32

"通过save(),jump()来实现,可能大家理解起来反而更容易,这个版本我还在测试,完善后我会贴出例子代码。"

//--------------------------------------------
期待。。。

freeboyxd 发表于 2013-1-7 22:47:42

共同期待。。。

smset 发表于 2013-1-8 00:01:14

本帖最后由 smset 于 2013-1-8 00:16 编辑

#include "STC89C51.h"
/****小小调度器开始**********************************************/
#define MAXTASKS 4
unsigned char currid;
volatile unsigned char timers;
#define _SS   static unsigned short lc; glc=&lc; if(lc) jump();
#define _EE   lc=0;
#define WaitX(delaytime) { save(delaytime); if(yieldflag) return;}      //强行加上 if(yieldflag)是为了防止编译器把后面的代码优化掉

#define RunTask(taskname,taskid) {currid=taskid; if (timers==0){timers=255; taskname();}}
#define CallSub(x)   WaitX(0); x(); if (timers!=255) return;

unsigned short GSP;
unsigned short *glc;
unsigned char yieldflag=1;

void save(unsigned char delaytime){
//save是用于保存运行位置
//在9级优化时,SP与调用前一样了。所以不能用到keil第9级优化。
GSP=SP;
*glc=(*(unsigned char *)(GSP))*256+*(unsigned char *)(GSP-1)+4;//+4是为了跳过if(yieldflag) return;
timers=delaytime;
}

void jump(){
//jump实现函数内部跳转
GSP=SP;
*(unsigned char *)GSP=(*glc)>>8;          
*(unsigned char *)(GSP-1)=(*glc);
}

/*****小小调度器结束*******************************************************/
sbit LED1 = P2^1;
sbit LED2 = P2^2;

sfr WDT_CONTR = 0xC1;

void InitT0()
{
        TMOD = 0x21;
        IE |= 0x82;// 12t
        TL0=0Xff;
        TH0=0XDB;//22M---b7;
        TR0 = 1;
}


void INTT0(void) interrupt 1 using 1
{
    unsigned char i;
        TL0=0Xff;    //10ms 重装
        TH0=0XDB;//b7;

    for (i=0;i<MAXTASKS;i++){
   if ((timers!=0)&&(timers!=255)) {
           timers--;
       }
    }       
}


voidtask1(){
_SS
while(1){
   WaitX(50);
   LED1=!LED1;   
}
_EE
}

voidtask2(){
_SS
while(1){
   WaitX(50);
   P1=!P1;   
}
_EE
}

//清除看门狗
void clr_wdt()
{
WDT_CONTR =0x3C;
}

void main()
{
        InitT0();
   
        while(1){
         clr_wdt();
           RunTask(task1,1);
           RunTask(task2,2);
    }
}

smset 发表于 2013-1-8 00:07:42

其余任务函数写法和主函数统统不变。

这个原理很简单,只是要考虑编译器优化代码的情况,所以附加了一些变量来辅助,包括GSP的操作,yieldflag的操作等。

让我花最多时间的是,在keil最高优化级别下,调用一个函数,竟然在函数内的SP指针和调用前是一样的。

这个我还没找到好的解决方案。

XIE2099 发表于 2013-1-8 01:17:38

记号一下,下去后做个实验

dr2001 发表于 2013-1-8 08:20:26

smset 发表于 2013-1-8 00:07 static/image/common/back.gif
其余任务函数写法和主函数统统不变。

这个原理很简单,只是要考虑编译器优化代码的情况,所以附加了一些变 ...

莫非为了省栈,返回地址放Rn,用Jmp调用了?

Keil C51很多行为和通常的想象的都不一样,直接和编译器的代码生成器互动风险挺大的。
最讨厌的就是C51的动态变量实际上是根据调用树静态分配的。。。

smset 发表于 2013-1-8 21:13:51

可能是的,我打算看汇编代码贴上来看看,不过keil突然看不成汇编代码了,提示MegawinOCD.DLL找不到,重装了都不行,不知为何。

再贴一个版本:
/****小小调度器开始**********************************************/
#define MAXTASKS 4
unsigned char currid;
volatile unsigned char timers;
#define _SS   static unsigned short lc; glc=&lc; if(lc) jump();
#define _EE   lc=0;
//#define WaitX(delaytime) { save(delaytime); if(yieldflag) return;}
#define WaitX(delaytime) { save(delaytime); }

#define RunTask(taskname,taskid) {currid=taskid; if (timers==0){timers=255; savetaskentry();taskname();}}
#define CallSub(x)   WaitX(0); x(); if (timers!=255) return;

unsigned short GTaskEntry,GTaskSP;
unsigned short GSP;
unsigned short *glc;
unsigned char yieldflag=1;

void savetaskentry(){
//保存任务函数调用位置
GTaskSP=SP;
GTaskEntry=(*(unsigned char *)(GTaskSP))*256+*(unsigned char *)(GTaskSP-1)+3; //+3是跳过任务函数本身,以便执行下一个任务。
}

void save(unsigned char delaytime){
//save是用于保存运行位置
//在9级优化时,SP与调用前一样了。所以不能用到keil第9级优化。
GSP=SP;
*glc=(*(unsigned char *)(GSP))*256+*(unsigned char *)(GSP-1);
timers=delaytime;

//保存后,直接退出到顶级任务函数调用处
(*(unsigned char *)(GTaskSP))=GTaskEntry>>8;
(*(unsigned char *)(GTaskSP-1))=GTaskEntry;
SP=GTaskSP;
}

void jump(){
//jump实现函数内部跳转
GSP=SP;
*(unsigned char *)GSP=(*glc)>>8;          
*(unsigned char *)(GSP-1)=(*glc);
}

/*****小小调度器结束*******************************************************/

这个版本,在save保存执行位置后,直接回到顶级任务调用出,执行下一个任务,相当于是一步到位彻底地释放栈,并执行下一个任务函数。

至此,任务位置的保存和恢复,已经彻底的摆脱了编译器宏计算的参与,是真正动态的保存恢复运行地址信息,任务内退出时,运行位置的保存,已不再依赖于状态机。

同时,这引入了一个新的可能性,既然save是一个函数,可以由任务函数主动调用来,保存执行位置,并返回到顶级任务位置,

那么:同样可以在定时中断函数里面,执行同样的动作: 让任务保存运行位置到lc,同时彻底释放栈,返回到顶级任务位置,和save达到一样的效果。区别是这个动作是主动发生的,而只是不像save函数那样必须由函数来调用。 当然中断里面肯定会增加一些判断机制,判断某任务不及时释放CPU才这么干。

这样就实现了主动抢断:即使某任务函数打死不放手, 不释放CPU,定时中断到来时,会强行在中断里面,保存任务的运行位置,并强行退出任务。 然后在下次任务调用时,同样可以通过jump跳到被打断的地方继续运行。

也就是:对霸占cpu不放的任务强行进行切换。

takashiki 发表于 2013-1-8 21:30:20

smset 发表于 2013-1-8 21:13 static/image/common/back.gif
可能是的,我打算看汇编代码贴上来看看,不过keil突然看不成汇编代码了,提示MegawinOCD.DLL找不到,重装了 ...

这样就实现了主动抢断:即使某任务函数打死不放手, 不释放CPU,定时中断到来时,会强行在中断里面,保存任务的运行位置,并强行退出任务。 然后在下次任务调用时,同样可以通过jump跳到被打断的地方继续运行。

也就是:对霸占cpu不放的任务强行进行切换。

强行进行切换是可能的,只是下次在执行时99%的可能会出错。因为没有保存现场,任务切换出去之后,寄存器会被其他任务破坏,再次切换回来就挂了。如果将寄存器保存、恢复,就基本上是一个完善的操作系统了,而不仅仅只是这个调度器了。

smset 发表于 2013-1-8 21:44:50

本帖最后由 smset 于 2013-1-8 21:51 编辑

嗯,我会做些测试来证实到底抢断会不会有问题。

但是,有两点是可能很有利的,因为这种方案的任务切换和继续执行机制和以往的多任务实现方案是很不同的:

1)中断返回时,强行返回的是顶级任务处,然后下一个顶级任务实际上是完整的从顶级任务的开始来执行,因此该任务的所有栈内容会自动重构一次,而非直接从外部跳进任务函数的断点处执行。

2)即使再一次调用到被打断的任务,也一样是完整的从顶级任务的入口进入,也会自动把栈内容重构一次。

由于所有的顶级任务和子任务,都保存了跳转位置在lc里,因此是刚好可以自动重构处全部的栈内容的。

也就是说:虽然抢断是临时“意外”发生了,但是抢断后,所有的任务函数都是统一从入口进去,在进入之后再来跳转到函数内部。

如果任务函数不使用动态变量(我一直推荐在这种场合下不用),是否就不存在寄存器保护和恢复问题呢。

takashiki 发表于 2013-1-8 21:58:38

smset 发表于 2013-1-8 21:44 static/image/common/back.gif
嗯,我会做些测试来证实到底抢断会不会有问题。

但是,有两点是可能有利的,因为有的机制和一般的多任务实 ...

寄存器 != 动态变量,寄存器是根据调用约定由编译器自由分配的, 尤其是SFR。单以51来说,R0~R7(虽然鉴于Keil的RZ,R1一般几乎不会用到)、ACC、B、DPTR均没有保护,很有可能会错乱。而函数返回地址已经保护了。函数从头执行根本无法恢复这几个寄存器值。

另外对于你589楼所说的可能是的,我打算看汇编代码贴上来看看,不过keil突然看不成汇编代码了,提示MegawinOCD.DLL找不到,重装了都不行,不知为何。
很可能是因为你使用了硬件仿真的原因,改成软件仿真应该就可以了。这样:

smset 发表于 2013-1-8 22:19:04

本帖最后由 smset 于 2013-1-8 22:35 编辑

嗯,这块我以前确实没怎么深入接触过了,看来无知者无畏啊,哈哈。

那么589楼的版本,是在save里面直接返回顶级任务处,是否存在类似的问题呢?

我用实际硬件测试过589的版本,可以正常使用。

那个编译器的问题确实是你说的原因,感谢!我又可以看汇编代码了。

再次纠结一次,寄存器!=动态变量

但是如果从顶级任务入口进去,任何被编译器安排来干事的寄存器,应该都没有使用其残留的值的机会吧?

如果真使用了残留的寄存器值,那589楼也应该会出错啊。

takashiki 发表于 2013-1-8 22:58:10

smset 发表于 2013-1-8 22:19 static/image/common/back.gif
嗯,这块我以前确实没怎么深入接触过了,看来无知者无畏啊,哈哈。

那么589楼的版本,是在save里面直接返 ...

我测试了你的589楼的代码,很遗憾的结果。

1、采用8级优化,最大AJUMP/ACALL,调用了第一个任务,程序在save()中返回时直接跑飞……
2、不采用最大AJUMP/ACALL优化,人为地设置一堆复杂的逻辑,结果就在某个任务(可能就是你说的顶级任务调用吧)中打转,再也出不来了……

takashiki 发表于 2013-1-9 09:08:48

takashiki 发表于 2013-1-8 22:58 static/image/common/back.gif
我测试了你的589楼的代码,很遗憾的结果。

1、采用8级优化,最大AJUMP/ACALL,调用了第一个任务,程序在 ...

猜测:589楼的程序实际上已经利用到了堆栈(直接访问SP),但是没有分配足够的堆栈(因为目前的调度器是stackless的,编译器不知道该分配多少),于是就可能会在其他的任务或中断中破坏。

解决方案有两个:
(1)使用stackful,为每个任务分配足够的堆栈,但这样的实现将与设计初衷背离。
(2)采用汇编,直接将断点PC值保存到变量,不访问堆栈,也就是完全实现labels as values方案。但这样的话,移植很麻烦。

smset 发表于 2013-1-9 10:30:26

最大AJUMP/ACALL优化项,我的keil里面没看到呢? 难道我们用的不同的编译器?

我觉得应该不是堆栈分配的问题,因为调度器的任务无需私有堆栈。

另外,能否给出你的人为一堆复杂的逻辑的具体代码?我测试测试呢。

takashiki 发表于 2013-1-9 11:16:16

本帖最后由 takashiki 于 2013-1-9 11:21 编辑

smset 发表于 2013-1-9 10:30 static/image/common/back.gif
最大AJUMP/ACALL优化项,我的keil里面没看到呢? 难道我们用的不同的编译器?

我觉得应该不是堆栈分配的问 ...

我一直说,Keil的优化水平很RZ,因为我用的是它的最强有力的优化级别:LX51+AX51+11级优化+最大AJUMP/ACALL+寄存器全局着色,结果依然是非常的不尽人意~~~

看图:首先使用LX51+AX51进行链接。
         使用了AX51,汇编中才会识别长整型和浮点型的四则运算宏,否则长整型会被当做短整型处理,以前被坑过,于是每次都开这个选项……
         使用了LX51,才会出现10级优化和11级优化以及最大AJUMP/ACALL,虽然节省的ROM和效率提升不算很高,但也至少离我的苛求进了一步。



程序代码我Kill掉了,觉得没有什么作用了。我调试时分配了三个任务,调试发现调用时三个任务的堆栈都只相隔两个字节(保存的断点位置),于是非常脆弱。
我稍微写一个测试任务,未经测试,而且不推荐这么写。void Task2(){
_SS
       _push_(B);
       _push_(B);
       WaitX(50);
       _pop_(B);
       _pop_(B);
_EE
}基于我测试的代码,临近的某个任务(Task1或Task3)堆栈就被干掉了~~~~当然函数中一般不会手动PUSH/POP,但是中断处理中编译器自己就会这么干,这里是模拟操作。


修改原因:A51不认识长整型,认识短整型。AX51认识长整型,前面写错了,改过来。

jiwm 发表于 2013-1-9 11:23:07

大神都出来了,很好很强大

smset 发表于 2013-1-9 11:28:49

本帖最后由 smset 于 2013-1-9 11:36 编辑

takashiki 发表于 2013-1-9 11:16 static/image/common/back.gif
我一直说,Keil的优化水平很RZ,因为我用的是它的最强有力的优化级别:LX51+AX51+11级优化+最大AJUMP/A ...

我在第9级优化时,就发现会出问题,save依赖于SP指针,所以这块操作是敏感的。

这会要求用户代码里面不要进行汇编级操作。 另外,如果要实现抢断式,那么在中断函数里面,还要做特定处理。

另外,我参考了一些坛友的资料,应该是如果任务都用静态局部变量,则无需考虑寄存器覆盖的问题,只需要把堆栈的那些事搞好即可。





smset 发表于 2013-1-9 14:09:40

本帖最后由 smset 于 2013-1-9 14:13 编辑

void Task2(){
_SS
       _push_(B);
       _push_(B);
       WaitX(50);
       _pop_(B);
       _pop_(B);
_EE
}
{:lol:} 俺想明白了,这个代码恰好制造了一个“不平衡”的栈操作。
因为第二次进入task2前,SP指针被指向起始位置,然后在task2里,直接跳转到pop处,就这样,栈多pop出东西了。

实际的代码里面,应该不大会涉及到这种非平衡的栈操作的。

然后,在中断函数里,并不会调用waitx函数,由于中断函数不是任务函数,所以内部的代码是连续完整运行的。不存在不平衡的栈操作。

xtxtt 发表于 2013-1-10 21:11:52

收藏!{:3_52:}

foxpro2005 发表于 2013-1-13 00:00:32

嗯,这篇帖子太好了,我现在正在学习程序架构....

jetimchen 发表于 2013-1-17 14:46:27

过来顶顶,好好学习

leicai05 发表于 2013-1-17 15:22:40

调度器学习

steaven2000 发表于 2013-1-19 12:22:49

所有的任务都是定时执行吗?

edkaifa 发表于 2013-1-23 17:17:58

谢谢分享 回头试试能不能用

actshuishan 发表于 2013-1-23 21:48:53

收藏学习了

guwu454 发表于 2013-1-29 23:37:28

对于实时性要求高的项目能用这个调度器吗?

waking 发表于 2013-2-2 13:11:45

经典的调度器!!
页: 1 2 3 4 5 [6] 7 8 9 10 11 12 13
查看完整版本: 再出个调度器,极小资源单片机值得一用