有没有用单片机能做什么好玩做的比较小的好玩的东西

再出个调度器,极小资源单片机值得一用
本帖最后由 dr2001 于
20:28 编辑
takashiki 发表于
可以的,按照388楼的第二条,WaitX是可以继续优化的,优化的程度与CPU相关。51可以优化到4个字节,AVR可 ...
直接往DPTR写数据啊……这分明是在挑衅Keil C51的常用寄存器追踪能力么。
建议此事慎重,不知道Keil C51对保护DPTR是怎么定义的,如果手册里没有明确写明的话,有潜在风险。
這個是PIC使用前後比較
本帖子中包含更多资源
才可以下载或查看,没有帐号?
本帖最后由 smset 于
21:36 编辑
topdog 发表于
這個是PIC使用前後比較
谢谢,看来rom也少了10%以上。看来400楼的优化是普遍有效的。
#define UpdateTimers() for(i=MAXTASKS;i&0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}
这句好像并不能减小代码,请将:
#define MAXTASKS 5再试试。
我在winavr下测试,这个方法比原来会增加2个字节rom
zsmbj 发表于
#define UpdateTimers() for(i=MAXTASKS;i&0 ;i--){if((timers!=0)&&(timers!=255)) time ...
keil下 ,会少2字节rom,一直会少2字节。
另外,400楼的CallSub 应该是:
#define CallSub(SubTaskName) do { _lc=__LINE__+((__LINE__%256)==0); return 0; case __LINE__+((__LINE__%256)==0):&&currdt=SubTaskName(); if(currdt!=255)} while(0);
已经更新。
本帖最后由 smset 于
22:09 编辑
另外,void型任务函数的版本我再次评估了一下,rom效率也相当高。而且:WaitX增量rom要低1个字节,这个很有吸引力。
我都有点纠结了,究竟409楼与400楼两个版本,哪种方式综合指标更优,请各位帮忙对比分析。
#include &stc89c51.h&
/****小小调度器开始**********************************************/
#define MAXTASKS 2
static unsigned char timers[MAXTASKS];
#define _SS& &sta switch(lc){default:
#define _EE& &;}; lc=0;
#define WaitX(tickets)&&do {lc=__LINE__+(__LINE__%256==0); currdt=} while(0); case __LINE__+(__LINE__%256==0):
#define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0){&&currdt=255; TaskName(); timers[TaskID]= }}&&while(0);
#define CallSub(SubTaskName) do { lc=__LINE__+(__LINE__%256==0); currdt=0; case __LINE__+(__LINE__%256==0):&&SubTaskName();&&if (currdt!=255)}&&while(0);
#define SEM unsigned int
//初始化信号量
#define InitSem(sem) sem=0;
//等待信号量
#define WaitSem(sem) do{ sem=1; WaitX(0); if (sem&0)} while(0);
//等待信号量或定时器溢出, 定时器tickets 最大为0xFFFE
#define WaitSemX(sem,tickets)&&do { sem=tickets+1; WaitX(0); if(sem&1){ sem--;} } while(0);
//发送信号量
#define SendSem(sem)&&do {sem=0;} while(0);
#define UpdateTimers() for(i=MAXTASKS;i&0;i--){if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}
/*****小小调度器结束*******************************************************/
sbit LED1 = P2^1;
sbit LED2 = P2^2;
void InitT0()
& && &&&TMOD = 0x21;
& && &&&IE |= 0x82;&&// 12t
& && &&&TL0=0X
& && &&&TH0=0XDB;//22M---b7;
& && &&&TR0 = 1;
void INTT0(void) interrupt 1 using 1
& & UpdateTimers();
& & TL0=0X& & //10ms 重装
& & TH0=0XDB;//b7;& &
void&&task1(){
&&while(1){
& &WaitX(50);
& &LED1=!LED1;& &
void&&task2(){
&&while(1){
& &WaitX(100);
& &LED2=!LED2;& &
void main()
& && &&&InitT0();
& && &&&while(1){
& && && &&&RunTask(task1,0);
& && && &&&RunTask(task2,1);
看了一会儿,没看懂。。。求解
恩,倒要仔细看看!
再顶& && && && && && &
这个要顶!一直裸奔,又没有机会上ARM芯片,这个可是个好的实践机会。
楼主加油啊!
dr2001 发表于
直接往DPTR写数据啊……这分明是在挑衅Keil C51的常用寄存器追踪能力么。
建议此事慎重,不知道Keil C51 ...
鉴于Keil C51的弱智,他对各个寄存器的使用非常保守,DPTR它保护的严严实实的。中断中如果用到DPTR它是会放入堆栈的,其他几个与DPTR相关的指令(如MOV、MOVC、MOVX、JMP)都不能跨越阻塞过程,所以其实是安全的。
不对DPTR赋值,那就返回一个unsigned short,分别是lc和延时各一字节,这时会使用R6、R7传递。
ProtoThread其实就是一个大的函数,主函数和这些函数中间是存在依赖关系的,编译器会保证各个变量、寄存器、资源之间不存在冲突。那些保护现场的就不一定了,堆栈设小了很容易覆盖。
本帖最后由 zsmbj 于
08:10 编辑
smset 发表于
另外,void型任务函数的版本我再次评估了一下,rom效率也相当高。而且:WaitX增量rom要低1个字节,这个很有 ...
经过测试avr和stm8,带返回的编译后代码明显小,void的编译后代码大不少。还是原来的好。这个不好。
因为定义了,所以他们都必须经过内存进传递数据,导致rom变大速度变慢。
而带返回值的版本,由于没有变量,他们都经过寄存器进行传递数据,这样rom变小,而且速度快。
本帖最后由 dr2001 于
08:25 编辑
takashiki 发表于
鉴于Keil C51的弱智,他对各个寄存器的使用非常保守,DPTR它保护的严严实实的。中断中如果用到DPTR它是会 ...
因为我记得C51的手册里是提到了DPTR相关的保护的东西的,只不过手里没有C51所以没办法了。-_!
如果C51包裹的很严实,那确实可以直接利用DPTR。
潜在的问题是Caller和Callee之间的问题,涉及ABI/寄存器保护约定;函数内部肯定不会有事儿,这个是编译器必然要保证的东西。
完全的针对51+Keil C... ...
本帖最后由 dr2001 于
08:35 编辑
smset 发表于
另外,void型任务函数的版本我再次评估了一下,rom效率也相当高。而且:WaitX增量rom要低1个字节,这个很有 ...
如果要评估,建议评估每个宏对应的反汇编后的指令代码量。
RunTask,_SS/_EE,带来的代码增量是和任务数量正相关的,不随间断点数量而变化;而WaitX的总代码量,是和间断点数量正相关的,一方面是WaitX宏本身插入的字节数,另一方面是Switch跳转操作到达对应Case需要的字节数。
在特定的任务下进行比较,得到的结果意义不一定充分。
要求总代码字节数少,是整体代码的优化:如果任务数量多,每个任务中间断点的数量少,可能采取一套方案;如果任务数量没两个,每个任务里超多状态,可能就是另外一套方案了。
也许有统一的最优方案,但是在具体平台下详细评估汇编指令之前,不是太好说。
smset 发表于
另外,void型任务函数的版本我再次评估了一下,rom效率也相当高。而且:WaitX增量rom要低1个字节,这个很有 ...
PIC貌似相差不是很大,WAITX組合語言看VOID型更精練
本帖子中包含更多资源
才可以下载或查看,没有帐号?
smset 发表于
另外,void型任务函数的版本我再次评估了一下,rom效率也相当高。而且:WaitX增量rom要低1个字节,这个很有 ...
你这个lc=__LINE__+(__LINE__%256==0)毫无意义,跟lc=__LINE__效果其实是一样的。
我知道你的目的是想避免标号重复,但这个同样避免不了,还是手动回车或者插入#line来的实在。
比如我257行调用了WaitX,513行又调用了WaitX,标号仍然会重复。这个宏仅仅排除了case 0的重复。
有几个问题请教一下高手:
将上面的宏定义带人到任务一中后为一下程序:
unsigned char task1(){
& & & & static unsigned char _
& & & & switch(_lc)
& & & & & & & & case 0:
& & & & & & & & & & & & while(1){
& & & & & & & & & & & & do {_lc=__LINE__; return 50 ;}
& & & & & & & & & & & & while(0);
& & & & & & & & & & & & case __LINE__:
& & & & & & & & & & & & LED1=!LED1;& &
& & & & & & & & & & & & }
& & & & & & & & ;
& & & & };
& & & & _lc=0;
& & & & return 255;
问题:当运行任务一时_cl =0时,程序直接return 50退出,运行到任务二时,_cl 就不等于0了,运行到最后将_CL=0,且返回255,那什么时候才能运行LED灯反转呢?
#define _SS static unsigned char _ switch(_lc){default:
改成这个定义后,iar编译器会出警告,如下:
本帖子中包含更多资源
才可以下载或查看,没有帐号?
randy~@~ 发表于
有几个问题请教一下高手:
将上面的宏定义带人到任务一中后为一下程序:
unsigned char task1(){
初始_lc=0运行case0: 首先给_lc赋值__LINE__,然后返回50,
等到延迟时间到后,再运行次函数_lc=__LINE__,所以运行到case __LINE__,就执行了LED反转。
randy~@~ 发表于
有几个问题请教一下高手:
将上面的宏定义带人到任务一中后为一下程序:
unsigned char task1(){
你错了吧,错的离谱,嘿嘿。WaitX必须是同一行,不能分行写的哦,因为__LINE__预定义宏的依赖。你分开尝试调试一下,一定是没有达到预计的效果。
或者你直接将__LINE__替换成行号,把case缩进对齐一下,这样你就理解了。
本帖最后由 smset 于
09:55 编辑
takashiki 发表于
你这个lc=__LINE__+(__LINE__%256==0)毫无意义,跟lc=__LINE__效果其实是一样的。
我知道你的目的是想避 ...
这个有重要意义的。只要任务内的WaitX所获行号不等于0,就达到效果。
因为400楼优化的思想是: switch 本来有个默认的default判断, 我想让 case 0 变为默认值, 也就是 让0 投入 default的怀抱。
既然 0 是任务的开始, (由于_lc是static静态的,初值自动是0, 然后在函数最后_lc=0,也是让任务下次被调用时,可以从头开始): 那么其他任何WaitX获取的行都必须避开0这个值.& &(至于257 和 513 重复,那个不管哈,自己加回车解决)
这样修改的另一个好处是: WaitX造成 重复case的概率也比以往的版本更低: 因为以往的版本都固定有case 0: 不管任务函数内部代码行数多还是少,一旦某个WaitX行落在256行的整数倍时,必然会造成重复case.
而400楼版本, 会自动在WaitX行号时避开0值, 至于避开后,如果再跟自己的另一个WaitX再读相撞,那这个概率要低很多的,
而且: 必须是一个任务函数内部的行数必须超过256行,才会发生这种257与513再次相撞的可能。如果任务函数内部代码少于256行,则不可能相撞。 内部代码行大于256行的任务函数实际上应该是很少的。
有一个可能相撞的情况: 256行和257行, 256行WaitX,自动取为257行,但257行恰好也是一个WaitX,它又不自动取为258,所以它们不幸地相撞了, 也就是连续两行都是WaitX,这种可能是有的。
这个很容易解决,加回车,或者不要连续两行都写WaitX.
本帖最后由 smset 于
10:00 编辑
zsmbj 发表于
#define _SS static unsigned char _ switch(_lc){default:
改成这个定义后,iar编译器会出警告,如下 ...
我试了其他c编译器下不告警的啊,IAR下会告警,能否测试下,能否正常运行?
按道理,switch语句中的default位置可以随意放的, IAR会因为default放前面就告警吗?
smset 发表于
这个有重要意义的。只要任务内的WaitX所获行号不等于0,就达到效果。
因为400楼优化的思想是: switch&&...
你说的是对的,你前面用的default,少了一次判断。
本帖最后由 randy~@~ 于
10:17 编辑
takashiki 发表于
你错了吧,错的离谱,嘿嘿。WaitX必须是同一行,不能分行写的哦,因为__LINE__预定义宏的依赖。你分开尝 ...
觉得自己有点愚钝,还是不明白。程序最终的结果我是知道。但是就没理解程序细节。还望多多指定。谢谢!
unsigned char task1(){
& & & & static unsigned char _
& & & & switch(_lc)
& & & & & & & & case 0:
& & & & & & & & & & & & while(1){
& & & & & & & & & & & & do {_lc=__LINE__; return 50 ;}
& & & & & & & & & & & & while(0);
& & & & & & & & & & & & case __LINE__:
& & & & & & & & & & & & LED1=!LED1;& &
& & & & & & & & & & & & }
& & & & & & & & ;
& & & & };
& & & & _lc=0;
& & & & return 255;
在上面的程序中,case __LINE__:是在case 0:的程序中,而运行case 0:时都返回50啊。如果不进入case 0:又怎能到case __LINE__:呢?不知哪里理解错误
修改原因:增加说明。
smset 发表于
我试了其他c编译器下不告警的啊,IAR下会告警,能否测试下,能否正常运行?
按道理,switch语句中的defa ...
下载在板子上跑程序是对的,winavr不警告,iar会警告,只是提示永远不能到达这个状态。
程序优化后这个_EE是没有代码的。就警告了,如果取消while(1)这个地方,那_EE是有代码的,则不会警告。
smset 发表于
这个有重要意义的。只要任务内的WaitX所获行号不等于0,就达到效果。
因为400楼优化的思想是: switch&&...
对这种存在bug的情况,是否修改lc为16bit类型?
为了少一个字节,存在有这样的风险....
flor 发表于
对这种存在bug的情况,是否修改lc为16bit类型?
为了少一个字节,存在有这样的风险.... ...
按400楼的版本,这个重复概率已经非常小了。
即使重复,编译器会报警,通不过编译,你肯定知道去改的(加个空行就解决),这个并不算bug。
所以无需为了这个去修改lc类型。
randy~@~ 发表于
觉得自己有点愚钝,还是不明白。程序最终的结果我是知道。但是就没理解程序细节。还望多多指定。谢谢!
我所指出的是你对宏替换的概念不清楚。
宏替换只是简单地在编译期原原本本的直接替换,中间不插入任何其他逻辑。
就你的程序来说,WaitX替换成:do {_lc=__LINE__; return 50 ;} while(0); case __LINE__:复制代码,千万注意,中间没有回车,不会换行的。这样,前面和后面的__LINE__值是一致的,是同一个,换行了后面的__LINE__和前面的__LINE__就不是同一个了,执行的时候就找不到标号而执行default。
__LINE__是C/C++预定义宏,它是根据行号改变的常数。
程序流程是这样的,我们假设WaitX位于line行,那么,我们将其分解:
第一次执行:unsigned char task1(){
& && &&&static unsigned char _
& && &&&switch(_lc)
& && &&&{
& && && && && & case 0:& && && && && && && && && && && && && && && && && && &&&//第一次执行到这里,_lc = 0
& && && && && && && && &while(1){
& && && && && && && && &do {_lc= return 50 ;}& && && && && && && &&&//第一次执行到这里后,_lc已经是line了,下面太监了,延时是50个Tick复制代码50个Tick过去了,通过定时器中断减掉的,主函数会第二次调用这个任务:unsigned char task1(){
& && &&&static unsigned char _
& && &&&switch(_lc)
& && &&&{
switch(_lc)
& && &&&{
& && && && && & //case 0:& && && && && && && && && && && && && && && && && && &//执行不到了吧
& && && && && && && && &while(1){& && && && && && && && && && && && && && && &//while还是会起作用的,这个变态,很变态
& && && && && & //& && &&&do {_lc= return 50 ;}
& && && && && & //& && &&&while(0);
& && && && && &case line:& && && && && && && && && && && && && && && && && &&&//因为上一次_lc已经是line了,所以直接跳到这里
& && && && && && && && &LED1=!LED1;& && && && && && && && && && && && &&&//执行
& && && && && && && && &}& && && && && && && && && && && && && && && && && && &//变态的while还是会循环的,于是下面的代码通通太监,无法执行到复制代码下面的代码是太监了,可是这次任务还没有阉割呢,while的阴影依然笼罩在task1的头上,硬着头皮执行这个while吧,成了这样:while(1){& && && && && && && && && && && && && && && && && && && &&&//变态的while起作用了
& && &&&do {_lc= return 50 ;}& && && && && && && && && && && && &&&//到这里又return了,下面的代码太监了,_lc又被赋值为line了,历史又要重演了
& && &&&while(0); 复制代码就这样执行下去,直到天荒地老,海枯石烂,设备断电,程序跑飞……
randy~@~ 发表于
觉得自己有点愚钝,还是不明白。程序最终的结果我是知道。但是就没理解程序细节。还望多多指定。谢谢!
你为什么不严格按宏定义来展开呢?
unsigned char task1(){
& && &&&static unsigned char _&&switch(_lc)& &{&&case 0:
& && && && && && && && &while(1){
& && && && && && && && &do {_lc=__LINE__; return 50 ;}& &while(0);& & case __LINE__:
& && && && && && && && &LED1=!LED1;& &
& && && && && && && && &}
& && && && && & ;
& && &&&};
& && &&&_lc=0;& && && &return 255;
本帖最后由 smset 于
10:43 编辑
takashiki 发表于
我所指出的是你对宏替换的概念不清楚。
宏替换只是简单地在编译期原原本本的直接替换,中间不插入任何其 ...
&&就这样执行下去,直到天荒地老,海枯石烂,设备断电,程序跑飞……
啊,程序跑飞? 看门狗伺候!
randy~@~ 发表于
觉得自己有点愚钝,还是不明白。程序最终的结果我是知道。但是就没理解程序细节。还望多多指定。谢谢!
ProtoThread多任务核心原理解读:#define _SS static unsigned char _ switch(_lc){default:
#define _EE ;}; _lc=0; return 255;
#define WaitX(tickets)&&do {_lc=__LINE__+((__LINE__%256)==0);} while(0); case __LINE__+((__LINE__%256)==0):
#define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) timers[TaskID]=TaskName(); }&&while(0);复制代码1. 首先ProtoThread实现的多任务是“协作式”多任务,也即多个任务间通过“友好合作”的方法共享CPU资源,当一个任务不需要CPU时,主动让出CPU。
2. ProtoThread任务调用WaitX主动让出CPU时,作了两件事:记录断点(代码当前行记录到_lc变量里),让出CPU(return实现)
3. 任务的恢复运行,当一个任务等待的条件发生时,恢复运行,这时应该要恢复到原来的断点处,这通过_SS宏里的switch(_lc)和WaitX宏里的case __LINE__+((__LINE__%256)==0): 实现。
& & 所以,恢复运行的本质是通过switch找到原来有WaitX留下来的断点。
4. 至于RunTask宏是加了定时服务,当任务定时时间到后,去运行任务函数,也即充当了调度器的角色。
smset 发表于
按400楼的版本,这个重复概率已经非常小了。
即使重复,编译器会报警,通不过编译,你肯定知道去改的( ...
我觉的还是把lc改成2bytes稳妥,程序稍微大点(也可能某个地方注释多点)就有error,这不友好.
另外lz在更新或增加功能时,不要总局限几个字节的优化,可以放多经历在通用性(比如单为某些编译器优化的特性就不要加了吧),稳定性,和多增加些功能,像signal处理(前面有哪个大侠给了个例子就很不错,可以考虑加进去)就非常有用.
flor 发表于
我觉的还是把lc改成2bytes稳妥,程序稍微大点(也可能某个地方注释多点)就有error,这不友好.
另外lz在更新 ...
一步一步来吧, 基础的东西先优化好再说。
330楼版本开始,就有信号量了。
flor 发表于
我觉的还是把lc改成2bytes稳妥,程序稍微大点(也可能某个地方注释多点)就有error,这不友好.
另外lz在更新 ...
建议弄清楚原理,自行根据应用需求修改完善再加以使用。
ProtoThread这种Stackless的场景容易出Bug。
#include &stdio.h&
int main(void)
{
& && &&&printf(&Hello World!&);
& && &&&return 0;
}
复制代码
原来这样发代码的,了解了。
本帖最后由 smset 于
12:57 编辑
对,用任何调度器或者RTOS操作系统,最好搞清其原理。
小小调度器任务函数的写法主要注意的,主要有三点:
1) 任务函数内部变量,建议都用静态局部变量来定义。
2) 任务函数内不能用switch语句。
3) 任务函数内,不能用return语句。 因为return已经被赋予任务延时的特定意义。(这是返回型任务函数版本的一个强制要求)
这三点,并不会明显造成写程序的不方便。
---------------------------
从裸奔到使用OS操作系统或调度系统的代价主要有:
硬件资源代价(对RAM和ROM的消耗),学习代价(学会其原理,并掌握其用法),移植代价(往不同cpu上移植的工作量),效率代价(使用调度系统后带来的额外cpu负担),商业代价(版权费用),稳定性代价(是否引入潜在不稳定因素,或者增大bug跟踪调试工作量)。
从这几方面来讲,应用小小调度器的代价,都是非常小的。
1) 硬件资源代价: 这个不用再说了,前面的优化版本已经说明问题。
2) 学习代价: 小小调度器总共只有十多行代码,如果我们做一个简单的解释说明,理解起来其实是很快的。我相信学习时间比其他调度系统要短。
3) 移植代价: 几乎没有什么移植工作量,对于各种cpu,几乎是通吃。
4) 效率代价: 我们一直在努力优化,相信调度效率已经不低了。比如任务切换时间,应该是可以做到uS级别,甚至亚uS级别。
5) 商业代价: 小小本调度器为免费使用,无需支付任何费用。
6) 稳定性代价:小小调度器本质上仅仅是几个宏而已,未涉及任何对内部寄存器或堆栈的操作,避免了引入不稳定风险因素,所有操作都在可预见,可把控的前提下进行。
takashiki 发表于
我所指出的是你对宏替换的概念不清楚。
宏替换只是简单地在编译期原原本本的直接替换,中间不插入任何其 ...
非常感谢各位抽出时间为我解答。谢谢!
但是有一点我始终没能转过这个弯:
在宏定义中WaitX(tickets)&&do {_lc=__LINE__;} while(0); case __LINE__: ,按我的理解是要执行case __LINE__: 就必须先执行do {_lc=__LINE__;} while(0);,为何第一次赋值给_cl后就不执行这条指令了?而且他们都是同一个while(1){}内?
randy~@~ 发表于
非常感谢各位抽出时间为我解答。谢谢!
但是有一点我始终没能转过这个弯:
在宏定义中WaitX(tickets)&&do ...
没有规定说c语言,写在同一行的代码,一定要一起执行的啊。
smset 发表于
没有规定说c语言,写在同一行的代码,一定要一起执行的啊。
您能解释一下为什么不执行d{}while(0)吗?谢谢!
randy~@~ 发表于
非常感谢各位抽出时间为我解答。谢谢!
但是有一点我始终没能转过这个弯:
在宏定义中WaitX(tickets)&&do ...
case后面的是标号,switch只查找标号,找到了就直接跳转过去,而不论这个标号在什么地方,即使是在while中,for中,if...else...中。
就好比您(switch)牵着您夫人的手(default)和她一群闺蜜(按位置一堆的case)出去逛街了,这时,您领导发话了,你去给我出去办点事吧(return),我下次站到队伍的第XXX位(保存状态:_lc=XXX)。你再回来时,直接找第XXX位就是,不论她是不是躲在别人后面。你不能因为她躲在别人背后你就先去牵别人的手,对吧。
楼上这样解释可能他更迷糊了。
实际上看一下这个代码楼主就应该理解了:
uchar taskA()
{
&&static unsigned char _
&&switch(_lc){
&&case 0:
& & while(1){
& && &...
& && &WaitX(10);
& & }
&&}
}
复制代码展开宏WaitX(10);
uchar taskA()
{
&&static unsigned char _
&&switch(_lc){
&&case 0:
& & while(1){
& && &...
& && &// 假设下面是地87行
& && &do{ _lc=__LINE__; return 10;}while(0);case __LINE__:
& & }
&&}
}
复制代码;
展开__LINE__,
uchar taskA()
{
&&static unsigned char _
&&switch(_lc){
&&case 0:
& & while(1){
& && &...
& && &// 假设下面是地87行
& && &do{ _lc=87; return 10;}while(0);case 87:
& & }
&&}
}
复制代码稍微重新排版一下:
uchar taskA()
{
&&static unsigned char _
&&switch(_lc){
&&case 0:
& & while(1){
& && &...
& && &// 假设下面是地87行
& && &_lc=87;& && &&&// 保存位置为87
& && &return 10;
& & case 87:& && && &// 下一次switch(_lc)时,从case 87处开始运行。
& & }
&&}
}复制代码
takashiki 发表于
case后面的是标号,switch只查找标号,找到了就直接跳转过去,而不论这个标号在什么地方,即使是在while ...
够生动!比喻到位!谢谢!
ifree64 发表于
楼上这样解释可能他更迷糊了。
实际上看一下这个代码楼主就应该理解了:展开宏WaitX(10);;
够详细!谢谢!
看样子我是没明白switch()的使用,汗颜啊!
对,再结合timer延时查询调度的功能,就明白为什么能实现多任务了。
首先,小小调度器是C语言通用的,只要支持C语言的编译器都可以用。 然后又是基于延时查询的方式来调度的,调度器不断查询每个任务的延时,对延时时间到的任务,再继续往下执行。
所以,如果要取一个名字的话应该是:
通用 C 延时 查询 调度器,翻译成英文是 General&&C&&Delay&&Querying&&Scheduler
顶!这个贴子我现在每天都看看,学习一下有没有新思想。佩服楼主。
这个一定要顶
再一次抛砖引玉,打算引入一个高优先级任务抢占机制,邀请大家共同验证和探讨:
#include &stc89c51.h&
/****小小调度器开始**********************************************/
#define MAXTASKS 2
volatile unsigned char timers[MAXTASKS];
#define _SS static unsigned char _ switch(_lc){default:
#define _EE ;}; _lc=0; return 255;
#define WaitX(tickets)&&do {_lc=__LINE__+((__LINE__%256)==0);} while(0); case __LINE__+((__LINE__%256)==0):
#define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) timers[TaskID]=TaskName(); }&&while(0);
#define CallSub(SubTaskName) do { _lc=__LINE__+((__LINE__%256)==0); return 0; case __LINE__+((__LINE__%256)==0):&&currdt=SubTaskName(); if(currdt!=255)} while(0);
#define UpdateTimers() for(i=MAXTASKS;i&0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}
#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 char task1(){
_SS
&&while(1){
& &WaitX(50);
& &LED1=!LED1;& &
&&}
_EE
}
unsigned char&&task2(){
_SS
&&while(1){
& &WaitX(100);
& &LED2=!LED2;& &
&&}
_EE
}
void InitT0()
{
& && &&&TMOD = 0x21;
& && &&&IE |= 0x82;&&// 12t
& && &&&TL0=0X
& && &&&TH0=0XDB;//22M---b7;
& && &&&TR0 = 1;
}
void INTT0(void) interrupt 1 using 1
{
& & UpdateTimers();
& & RunTask(task1,0);//任务1具有高的运行权限
& & TL0=0X& & //10ms 重装
& & TH0=0XDB;//b7;& &
void main()
{
& && &&&InitT0();
& && &&&while(1){
//& && && &&&RunTask(task1,0);
& && && &&&RunTask(task2,1);//任务2具有低的运行权限
& & }
}
复制代码
经过测试, CallSub(SubTaskName) 这个调用子函数,并不需要在开始调用一个WaitX(0),如下即可:
#define CallSub(SubTaskName) do { currdt=SubTaskName(); if(currdt!=255)} while(0);
经过测试,运行没有任何问题。
本帖最后由 smset 于
08:47 编辑
zsmbj 发表于
经过测试, CallSub(SubTaskName) 这个调用子函数,并不需要在开始调用一个WaitX(0),如下即可:
#define C ...
不能去掉的,呵呵,否则前面一段代码会重复执行,运行逻辑上已经不对了
smset 发表于
不能去掉的,呵呵,否则前面一段代码会重复执行,运行逻辑上已经不对了 ...
不会啊,我看如下代码,在任务0里调用一个子任务。unsigned char subtask(void)
{
& & & & _SS
& & & & while(1)
& & & & {
& & & & & & & & led1_on();
& & & & & & & & WaitX(30);
& & & & & & & & led1_off();
& & & & & & & & WaitX(40);
& & & & }
& & & & _EE
}
unsigned char task0(void)
{
& & & & _SS
& & & & while(1)
& & & & {
& & & & & & & & led1_on();
& & & & & & & & WaitX(30);
& & & & & & & & led1_off();
& & & & & & & &
& & & & & & & & CallSub(subtask);
& & & & }
& & & & _EE
}
复制代码编译后的汇编如下:unsigned char task0(void)
{
& & & & _SS
&&f8:& & & & 80 91 61 00 & & & & lds& & & & r24, 0x0061
&&fc:& & & & 88 23& && & & & & & and& & & & r24, r24
&&fe:& & & & 19 f0& && & & & & & breq& & & & .+6& && && & & & ; 0x106 &task0+0xe&
100:& & & & 83 36& && & & & & & cpi& & & & r24, 0x63& & & & ; 99
102:& & & & 71 f4& && & & & & & brne& & & & .+28& &&&& & & & ; 0x120 &task0+0x28&
104:& & & & 06 c0& && & & & & & rjmp& & & & .+12& &&&& & & & ; 0x112 &task0+0x1a&
& & & & while(1)
& & & & {
& & & & & & & & led1_on();
106:& & & & c3 98& && & & & & & cbi& & & & 0x18, 3& & & & ; 24
& & & & & & & & WaitX(30);
108:& & & & 83 e6& && & & & & & ldi& & & & r24, 0x63& & & & ; 99
10a:& & & & 80 93 61 00 & & & & sts& & & & 0x0061, r24
10e:& & & & 8e e1& && & & & & & ldi& & & & r24, 0x1E& & & & ; 30
110:& & & & 08 95& && & & & & & ret
& & & & & & & & led1_off();
112:& & & & c3 9a& && & & & & & sbi& & & & 0x18, 3& & & & ; 24
& & & & & & & &
& & & & & & & & CallSub(subtask);
114:& & & & d9 df& && & & & & & rcall& & & & .-78& &&&& & & & ; 0xc8 &subtask&
116:& & & & 80 93 66 00 & & & & sts& & & & 0x0066, r24
11a:& & & & 8f 3f& && & & & & & cpi& & & & r24, 0xFF& & & & ; 255
11c:& & & & 21 f4& && & & & & & brne& & & & .+8& && && & & & ; 0x126 &task0+0x2e&
11e:& & & & f3 cf& && & & & & & rjmp& & & & .-26& &&&& & & & ; 0x106 &task0+0xe&
& & & & }
& & & & _EE
120:& & & & 10 92 61 00 & & & & sts& & & & 0x0061, r1
124:& & & & 8f ef& && & & & & & ldi& & & & r24, 0xFF& & & & ; 255
}
126:& & & & 08 95& && & & & & & ret复制代码可以看到在执行了led1_off();后,立即调用了callsub()函数,这个逻辑也的对的啊。
如果增加了waitx(0),那么在执行led1_off();后,先返回timer0,等待下一个循环再调用callsub,感觉没必要啊。
RE: 再出个调度器,极小资源单片机值得一用
zsmbj 发表于
不会啊,我看如下代码,在任务0里调用一个子任务。编译后的汇编如下:可以看到在执行了led1_off();后,立 ...
你这样理解是不对的。
假设CallSub的子任务再调用wait停在某处时,当恢复子任务运行的条件成立时,必须直接回到callsub的子任务的断点处,也就是你代码里的ledoff()是不应该执行的。
ProtoThread实现协作多任务的原理是,“中断时”在整个任务的调用链沿路“铺设(记录)”断点位置,通过插入case __LINE__实现;然后再通过return层层退出。
“恢复”运行时,通过switch层层沿着原来铺好的路进入中断点。
所以ProtoThread任务切换有开销没有?绝对有,任务的换出需要层层return,任务的换入需要层层switch。如果不需要保存寄存器的话,下面的任务切换方式也许性能更高:
void task_switch()
& && &task_stack_top[taskid++] = SP;
& && &if(taskid & MAX_TASKS) taskid = 0;
& && &SP = task_stack_top[taskid];
这种方法比较遗憾的是,需要大量的堆栈开销。
如果结合调度器的运行过程看,CallSub前面的代码会执行两次。
smset 发表于
如果结合调度器的运行过程看,CallSub前面的代码会执行两次。
呵呵,才看到。看来还是不能省略。
& & & &&&while(1)
& && && &RunTask(task5,6); //task5
& && && &RunTask(task6,5); //task6
这样还是先task5,再task6
smset 发表于
再一次抛砖引玉,打算引入一个高优先级任务抢占机制,邀请大家共同验证和探讨: ...
你这个方案我没有实际验证,但是估计是具有很大的局限的。因为无法解决重入问题,会引起冲突。
比如中断中的任务调用了某个子函数,主函数调用的某个任务也调用了这个子函数,该子函数的重入问题将无法解决,只能重新再写一遍。
本帖最后由 smset 于
11:47 编辑
takashiki 发表于
你这个方案我没有实际验证,但是估计是具有很大的局限的。因为无法解决重入问题,会引起冲突。
比如中断 ...
子任务,确实不可重入,即便是没有在中断中的抢占任务也是这样。& &(或许,应该把_lc独立出去,和timer合成一个TaskPCB结构,也能实现可重入。)
而对于一般的函数,如果函数本身是可重入的,我觉得好像也没有问题啊。
如果函数本身就是不可重入函数,那即使不采用抢断机制,也不能重入:如果两个普通任务都调用这个函数,也会有问题。
对于不可重入函数,也可以进行代码与数据分离的方式,将之变为可重入。
欢迎一起继续探讨。
smset 发表于
子任务,确实不可重入,即便是没有在中断中的抢占任务也是这样。& &(或许,应该把_lc独立出去,和timer ...
另外,中断可以重入吗,这个我还不清楚,从来没有这么做过。
假如中断调用的任务需要占用长时间,比如占用了10个Tick后才能释放CPU,那么在占用这么长时间里来了十个定时器中断,这些中断能够响应吗?印象中,51是无法响应的,会被吃掉。我自己也不确定,因为我从来不这样写程序,中断中从不运行占用时间太长的过程,全部转成异步了。
本帖最后由 smset 于
12:12 编辑
中断调用的任务, 不存在占用长时间的情况.
因为任务函数一样是随时switch ,case, return的,任务函数内的WaitX(10),并不占用中断内部的10个Tick时间。
正因为每个任务函数每次实际被执行都是瞬间完成的,所以放在中断里,也是可以的。
当然,高级任务本身的代码片断效率要高。只要每次执行任务函数不超过1个tick,(现在是10mS),就不会对低优先级任务产生明显影响。
而相反,高优先级任务不受低优先级任务的限制,即便低优先级任务用Delayms函数延时个10秒,高优先级任务一样可以按时运行。
smset 发表于
中断调用的任务, 不存在占用长时间的情况.
因为任务函数一样是随时switch ,case, return的,任务函数内的W ...
WaitX(10)当然不占用10个Tick,但是如果WaitX和WaitX之间的逻辑本身就需要10个Tick,那很显然就会出现我说的那种情况。为了解决这个问题,只能在这些过程中间插入一堆的WaitX(0),那么一旦WaitX了,那么就需要等到下次Tick才能继续执行,这与该任务要求高优先级是背离的,根本就是不可取的。
smset 发表于
中断调用的任务, 不存在占用长时间的情况.
因为任务函数一样是随时switch ,case, return的,任务函数内的W ...
不能随意的return哦,一旦return,就表示已经主动释放了CPU,该给别人了。
基于优先级的抢占式调度能够直接在中断时从别的任务中强制抢占CPU执行权,协作式的无法强行抢占,只能老老实实排队,别人让出来了,自己才有可能得到执行的机会。
考虑如下问题:
如果低优先级的某个任务运行过程中需要耗时3个Tick(不是WaitX),高优先级的任务需要每个Tick都执行,那么低优先级的任务执行时,高优先级的任务根本就没有获得CPU的机会。解决方案当然是低优先级任务中插入WaitX(0)主动释放CPU,因为主任务是反复调度的,释放了CPU在下次while中会补偿回来,因此不会出现什么问题。可是高优先级任务由于由中断调用,中断不是一直while的,一个Tick过去了,那么在下一个Tick到来之前,它是得不到任何执行的机会的,这点和在主任务中不同。
终上,我认为,在中断中调度协作式内核任务,看上去很美,很像真正地OS,但是很可能是镜花水月。
本帖最后由 smset 于
13:18 编辑
如果一个任务的两个WaitX之间的代码,一次运行的CPU占用就需高达10个Tick的时间(100ms) (100ms我们可以计算对应对少次指令执行周期,一般的代码片断,除非内嵌大数量循环,相信很难达到这么长时间的占用量)。
同时,那它就不应该安排成为高优先级任务的,只能是当低级任务的命了,呵呵。
高优先级任务,本身的执行效率当然也是要求高的。自己效率地下,还要占到高级任务的位置,那就乱套了啊。
就好比 把拖拉机,安排到高速公路上去跑, 那交通状况肯定拥堵不堪啊。
另外,高优先级任务当能能抢断低优先级的啊,
因为高优先级是在定时器中断里面执行。
即使低优先级霸占cpu不放,也一样被中断,然后去执行高优先级任务。
另外,并不是说高优先级任务,不能进行延时等待,高优先级任务也可以延时等待个十秒八秒(我是指它本身逻辑上就是要主动等待),这并不影响它成为高优先级任务。
只是高优先级任务可以精确按时执行而已,不受低优先级任务占用CPU的限制。
smset 发表于
如果一个任务的两个WaitX之间的代码,一次运行的CPU占用就需高达10个Tick的时间(100ms) (100ms我们可以 ...
10个Tick是我打的比方。事实上,只要高优先级任务中存在一条执行时间间隔超过1个Tick的事务,就一定会出问题,因为会阻塞下次Tick的到来,使得定时不准,心律不齐,该送医院了。
本帖最后由 smset 于
13:41 编辑
takashiki 发表于
10个Tick是我打的比方。事实上,只要高优先级任务中存在一条执行时间间隔超过1个Tick的事务,就一定会出 ...
是的, 高优先级任务的一个要求就是: 单次执行时间小于1个tick (10mS)。 这就是对高优先级任务的效率要求。
实际上,在现在的单片机指令周期速度下,这并算不是一个太高的要求: 单片机可以轻易达到1uS的指令周期,甚至0.1uS也不算稀奇,10ms意味 1万或10万个指令周期,这可以干很多很多事情了哦。
就好比道法规定:行使速度小于60码的汽车,不得上高速公路一样,其实大多车都能轻易达到要求。
当然,肯定也有些是达到不60码速度的汽车, 但我们并不能因为有这种慢速度的车,就不修高速公路嘛,只要不让慢车上高速公路就可以了嘛,呵呵。
本帖最后由 dr2001 于
13:51 编辑
smset 发表于
再一次抛砖引玉,打算引入一个高优先级任务抢占机制,邀请大家共同验证和探讨: ...
这里的”抢占优先级“的实现方法,原理上和Gorgon_Meducer进行过的讨论有相似的地方。
讨论的地址是:
当然,我对Gorgon_Meducer的方案也存有疑虑:那个主要是前台调度到一个冗长任务导致的长等待之类的问题。
简单且不严格的说,452#的方案:
具有高优先级的任务不一定享有更多的CPU处理时间。这点与“优先级”的一般认知有悖。
如果高优先级任务因为等待某种信号/条件而使用了Yield语义的代码,则导致中断返回;无论该信号发生与否,下次被调度一定是在Tick发生之后。会出现类似优先级反转的现象。当然,都用中断处理可以缓解/解决这个问题。但是452楼代码没有体现。
但是,如果这里的“任务”没有使用Yield语义的话,就没必要使用RunTask之类的了。
smset 发表于
是的, 高优先级任务的一个要求就是: 单次执行时间小于1个tick (10mS)。 这就是对高优先级任务的效率 ...
你这不是高速公路,你这是专车专道,全线管制,前面还有警察开道的那种。因为只允许同时存在一个高优先级任务,所以不可能出现塞车、超车的情况。
另外,作为一个内核,考虑的是通用性,而不是特定到某个频率下的某个CPU。从你一开始你就在要求通用性,那么外围环境同样也是通用的,而不能是用了你这个调度器,CPU主频就得跑多快,定时间隔就得多长。
操作系统(无论是轮询式的还是抢占式的)都可以抢占CPU,你这个协作式的中断同样可以抢占,但是中间存在的缺陷却是巨大的,大到可能你都没有考虑到。因为实际情况可能是千变万化的,而你可能仅仅局限在翻转个灯的事情上了。
回到你的比喻。就算你的高优先级任务是专车专道,警察开道,没人能够跟你抢道,但是凡事还有万一,天有不测风云,人有旦夕祸福,高速路突然垮了,你怎么办?高优先级任务某个过程占用了0.8个Tick,中间来了一个奇怪的中断,处理它的时间消耗了0.3个Tick,于是逢山开路遇河搭桥,可是时钟已经不准了。
可能你又要说,调度任务的定时器中断优先级必须最高!嗯,我没话说了,实际情况下很可能不是你想象的那么简单。
另外,奉劝一句,CPU不要迷信高主频,真正地程序员会用尽量低的频率完成尽量多的事务。要不然的话,老妖啥时发明个1GHz的51,把什么AVR、PIC、CortexM、……之类的单片机全部干趴下
本帖最后由 smset 于
15:00 编辑
其实1uS和0.1uS仅仅对应1M或10M的主频,根本算不上多高的主频啊,大多数单片机应用都是达到这个速度了的。不能因为少数应用,cpu运行频率非常低, 就全面否定这种机制。
很多管理体系和防御机制,都有不能满足的特定例外情况,但是不能因为某种万一的例外,就全面否定这种机制的存在价值。
如比如飞机存在空难的可能性,但全世界还是没有把飞机取消啊。
高优先级任务机制,我觉得还是值得存在的,因为对于多数实际应用中,确实存在需要精确运行高级任务,不被低级慢速任务拖累的实际应用需求。
另外,你提到的最大问题,是高优先级任务在遇到特殊的情况下,可能导致该次时钟不准,导致某些任务不能及时得到运行,
其实,如果没有高优先级任务机制,让所有任务都在同一优先级来协作,更容易出现任务不能及时得到运行的情况。正因为这个原因,才使我们考虑高优先级任务机制的啊。
那么我们是否可以这样理解: 在某些特殊情况下,高优先级任务会导致低优先级任务的不准时运行。可能变得和没有使用高优先级机制的总体效果差不多。这就是最差情况。
只是需要大家把这种情况讨论清晰:使用高优先级任务,需要注意什么基本条件, 我觉得搞清楚了,用起来也应该是没有问题的。
本帖最后由 smset 于
14:40 编辑
dr2001 发表于
这里的”抢占优先级“的实现方法,原理上和Gorgon_Meducer进行过的讨论有相似的地方。
讨论的地址是:htt ...
高优先级任务并不一定获得更多的cpu时间,但是能 精确的,及时的&&获得CPU控制权。
表面上看,似乎mian里面的一般任务获得了更多的cpu,但是实际上,由于调度器是基于时间timers[x]==0来决定进入任务的,
在中断未发生期间,timers未有改变,实际上调度器基本是在空转(也有例外),一般任务照样得不到cpu。
----------------------------------------------------------------
可能从概念上讲,中断里面执行“任务”,确实直观上感觉有问题,任务,是一个过程啊,有等待之类的啊,怎么能在中断里执行呢,嗯,一定有问题!!!!不行!!!!!!!。
我们换个说法:中断里面执行状态机行不?& &嗯!当然啊,可以的哦:我们很多裸奔的单片机的中断里面,不都在用状态机来处理一些数据吗?
其实,两者真的有区别吗?只是说法不同而已。
因为从本质来看,小小调度器里的任务函数就是经典状态机啊,只是在语法上变成“线程”而已,^_^。
玩调度器...........
请问一下 LZ&&你的调度器里面 每个任务的 执行时间不应该超过多少
本帖最后由 zsmbj 于
14:59 编辑
对小小调度器进行了一下修改,借鉴了1楼的方法,函数不在main里直接使用,而是使用了函数指针的方法。高优先级的任务列在前面。低优先级的任务列在后面
在主程序里循环查询任务时间执行时间,当一个任务等待时间到,则执行任务。当执行完成该任务后,重新循环查询任务,保证高优先级的任务优先执行。
经过我的测试当任务比较多的时候(我测试4个任务),编译的rom会比原来调度器的少。不过ram是会增加,每个任务增加2个bytes ram。
请测试。/*****小小调度器开始*******************************************************/
#ifndef ARRAYSIZE
#define ARRAYSIZE(a) (sizeof(a) / sizeof((a)[0]))
#endif
typedef struct{
& & & & unsigned char (*fTask)(void);
}sT
//高优先级任务在前
static sTask mTaskTab[] = {
& & & &&&{task0}
& & & & ,{task1}
& & & & ,{task2}
& & & & ,{task3}
};
#define MAXTASKS ARRAYSIZE(mTaskTab)
static unsigned char timers[MAXTASKS];
#define _SS static unsigned char _ switch(_lc){case 0:
#define _EE ;}; _lc=0; return 255;
#define WaitX(tickets)&&do {_lc=__LINE__;} while(0); case __LINE__:
#define CallSub(SubTaskName) do { WaitX(0); currdt=SubTaskName(); if(currdt!=255)} while(0);
#define UpdateTimers() for(i=0;i&MAXTASKS;i++) {if((timers[i]!=0)&&(timers[i]!=255)) timers[i]--;}
#define RunTasks()& &&& for(i=0;i&MAXTASKS;i++) {if (timers[i]==0) {timers[i]=mTaskTab[i].fTask();} }
/*****小小调度器结束*******************************************************/复制代码main里的while(1)需要做一下修改:& & while(1)
& & {
& & & & & & & & RunTasks();
& &&&//& &RunTask(task0,0);
& &&&//& &RunTask(task1,1);
& &&&//& &RunTask(task2,2);
& &&&//& &RunTask(task3,3);
& & }复制代码
zsmbj 发表于
对小小调度器进行了一下修改,借鉴了1楼的方法,函数不在main里直接使用,而是使用了函数指针的方法。高优 ...
对,Rom会减少,因为RunTasks用一个循环,代替了多个RunTask代码。不过Ram会增加。
另外,这种优先级机制不能保证高级任务及时运行,一旦落入慢速任务之手,高级任务就只能等慢速任务完成了。
楼上的代码得到了一种类似优先级的效果。
RunTasks里每次都是从0开始搜索任务,所以在mTaskTab数组里排在前面的任务具有较高的优先级。
但又不是真正意义的优先级,因为”高优先级“代码不能抢占排在后面的低优先级代码。
标记一下,学习学习。
本帖最后由 zsmbj 于
16:10 编辑
ifree64 发表于
楼上的代码得到了一种类似优先级的效果。
RunTasks里每次都是从0开始搜索任务,所以在mTaskTab数组里排在前 ...
这个是肯定的,因为目前的调度器是协作式的。如果要高优先级可以实时抢占,那必须是抢占式的了。那入栈,出栈,消耗的ram可就大了去了。
这个还是比顺序执行要好一些。打一个比喻:
公司只有一个卫生间,现在有3个人在排队上厕所。他们分别是:经理2,程序员3,前台4。
现在厕所里边程序员在用,所以在程序员没出来前,大家只能等待,不过经理明显优先级高,下一个就是他了。
可是没等程序员出来前,老总1也来等待了,那老总的优先级比经理更高。当程序员出来后,老总会第一个进去用厕所。
等老总出来后,经理才能用。经理用完后,程序员由于不需要上厕所,最后一个前台可以用。
不过老的调度器由于没有优先级机制。那么当程序员上完厕所后,下一个就是前台,然后循环到老总,经理。
本帖最后由 smset 于
16:39 编辑
对,不用函数指针, 也可以达到这种调度效果:
只需要修改下宏:
& &#define RunTask(TaskName,TaskID)&&{ if (timers[TaskID]==0) {timers[TaskID]=TaskName(); } }&&
本帖最后由 ifree64 于
16:49 编辑
花了点时间写了这样一种调度方法,调度思想与里的类似。
请大家点评下。#include &reg51.h&
#define FCPU& & & & & & & & & & & & UL
#define& & & & HZ& & & & & & & & & & & & 1000
/* --------- Begin of Simple OS -----------------*/
#define& & & & MAX_TASKS& & & & & & & & 4
#define MAX_TASK_DEP & & & & 12
unsigned char idata os_task_stack[MAX_TASKS+1][MAX_TASK_DEP];
unsigned char idata os_task_sp[MAX_TASKS+1];
unsigned char idata os_task_timers[MAX_TASKS+1];
unsigned char os_task_
void os_sched()
{
& & & & os_task_sp[os_task_id] = SP;
& & & & for(os_task_id = 0; os_task_id & MAX_TASKS+1; ++os_task_id)
& & & & {
& & & & & & & & if(os_task_timers[os_task_id] == 0)
& & & & & & & & {
& & & & & & & & & & & & SP = os_task_sp[os_task_id];
& & & & & & & & & & & &
& & & & & & & & }& & & & & & & &
& & & & }
}
void os_task_load(unsigned int fn, unsigned char tid)
{
& & & & os_task_sp[tid] = os_task_stack[tid] + 1;
& & & & os_task_stack[tid][0] = (unsigned int)fn & 0
& & & & os_task_stack[tid][1] = (unsigned int)fn && 8;
void os_timer_tick()
{
& & & & unsigned char i = MAX_TASKS;
& & & & for(; i & 0; --i)
& & & & {
& & & & & & & & if(os_task_timers[i-1] && os_task_timers[i-1] != 255){
& & & & & & & & & & & & --os_task_timers[i-1];
& & & & & & & & }
& & & & }
}
void os_wait(unsigned char tick)
{
& & & & os_task_timers[os_task_id] =
& & & & os_sched();
}
void os_idle()
{
& & & & while(1)
& & & & {
& & & & & & & & PCON |= 0x01;
& & & & & & & & os_sched();
& & & & }
}
void os_start(tid)
{& & & &
& & & & os_task_stack[MAX_TASKS][0] = (unsigned int)os_idle & 0
& & & & os_task_stack[MAX_TASKS][1] = (unsigned int)os_idle && 8;
& & & & os_task_sp[MAX_TASKS] = os_task_stack[MAX_TASKS] + 1;
& & & & os_task_id =
& & & & SP = os_task_sp[tid];
& & & && & & & & & & & // return to task tid.
}
/* --------- End of Simple OS -------------*/
sbit LED1 = P1^0;
sbit LED2 = P1^7;
void timer0_init()
{
& && &&&TMOD = 0x21;
& && &&&IE |= 0x82;&&// 12t
& && &&&TL0 = (65536-FCPU/12/HZ);
& && &&&TH0 = (65536-FCPU/12/HZ)&&8;
& && &&&TR0 = 1;
}
void timer_isr(void) interrupt 1 using 1
{
& & & & TL0 = (65536-FCPU/12/HZ);
& && &&&TH0 = (65536-FCPU/12/HZ)&&8;&&
& & & & os_timer_tick();
}
void task1()
{
& & while(1)
& & & & {
& & & & & & & & LED1 = !LED1;
& & & & & & & & os_wait(100);& && &&&
& & & & }
void task2()
{
& & while(1)
& & & & {
& & & & & & LED2 = !LED2;
& && &&&os_wait(250);
& & }
unsigned char code zixingma[] = {
& & & & 0x3f, 0x06,& & & &&&0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f
};
unsigned char code weiduanma[] = {
& & & & 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f
};
unsigned char buffer[6];
void task3()
{
& & & & static unsigned char pos = 0;
& & & & while(1)
& & & & {
& & & &&&& & & & for(pos = 0; pos & 6; pos++)
& & & & & & & & {
& & & & & & & & & & & & P2 = 0
& & & & & & & &&&& & & & P0 = ~zixingma[buffer[pos]];
& & & & & & & & & & & & P2 = weiduanma[pos];
& & & & & & & & & & & & os_wait(2);
& & & & & & & & }& & & &
& & & & }
}
void task4()
{
& & & & static unsigned char hour, min, sec,& & & &
& & & & while(1)
& & & & {
& & & & & & & & for(i = 0; i & 10; i++)
& & & & & & & & & & & & os_wait(100);
& & & & & & & & if(++sec &= 60)
& & & & & & & & {& & & &
& & & & & & & & & & & & sec = 0;
& & & & & & & & & & & & if(++min &= 60){
& & & & & & & & & & & & & & & & min = 0;
& & & & & & & & & & & & & & & & if(++hour &= 24)
& & & & & & & & & & & & & & & & & & & & hour = 0;
& & & & & & & & & & & & }
& & & & & & & & }
& & & & & & & & buffer[0] = sec%10;
& & & & & & & & buffer[1] = sec/10;
& & & & & & & & buffer[2] = min%10;
& & & & & & & & buffer[3] = min/10;
& & & & & & & & buffer[4] = hour%10;
& & & & & & & & buffer[5] = hour/10;
& & & & }
}
void main()
{
& & & & timer0_init();
& & & & os_task_load(task1, 0);
& & & & os_task_load(task2, 1);
& & & & os_task_load(task3, 2);
& & & & os_task_load(task4, 3);
& & & & os_start(0);
}
复制代码简单解释下为什么没有在任务切换时保存寄存器。
C51规定调用函数时,被调用函数可以任意使用寄存器,意味着调用函数时,已经保存了所有需要保存的参数。但由于C51不是在堆栈里分配局部变量和传递参数,所以每一个任务的变量使用了静态变量来避免重入问题。
本帖最后由 smset 于
16:58 编辑
中断里能否执行状态机:
可以参考这个文章,写得还不错:
这个更狠,把所有任务全部放在中断里了。
我们不用这么“极端”,全部任务放中断里面,只不过,放任务进中断里,并不是那么可怕。
本帖最后由 zsmbj 于
17:06 编辑
smset 发表于
对,不用函数指针, 也可以达到这种调度效果:
只需要修改下宏:
果然如此,不过发现增加了continue后,代码更大了。
我475楼的写法,每增加一个任务,增加2个字节的开销,不过rom会小不少。
smset 发表于
其实1uS和0.1uS仅仅对应1M或10M的主频,根本算不上多高的主频啊,大多数单片机应用都是达到这个速度了的。 ...另外,你提到的最大问题,是高优先级任务在遇到特殊的情况下,可能导致该次时钟不准,导致某些任务不能及时得到运行,
你没有理解我的意思。我的意思是你的定时器本身就已经不准了,造成所有任务的执行时刻均不可预测,而且定时器的误差是累积的,很可能高优先级任务首先出错,低优先级任务由于容错能力较强而可以忽略。
在这种情况下,定时器中断一定要补偿的,或者采用可以自动重装机制的定时器。你看过Keil RTX51Tiny的源代码没有?它那个T0 ISR一开始就对TH0、TL0进行了补偿,仅仅是为了避免几个字节的执行时间所带来的误差。
另外,对于RAM、ROM等资源都特别受限的MCU,将目标放在节省1、2个字节的ROM的CPU,主频往往也是受限的。不是你所说的1us、0.1us没有压力哦,标准的51的0.1us是什么概念你知道吗?是120MHz。STC的1T你信吗?反正我不信。
在中断中如果需要执行耗时的操作,我不认为是好事。但这样的需求真不见得就没有,优先级是由具体情况所决定的,不是以占用时间的多寡来决定的。对于在中断中直接调度而不是释放给主函数我还是持保留意见。
另外发现 :static unsigned char timers[MAXTASKS];定义为static后,winavr优化会出错。取消static后才正常。
写成unsigned char timers[MAXTASKS] ={0};这样也可以吧。
zsmbj 发表于
另外发现 :static unsigned char timers[MAXTASKS];定义为static后,winavr优化会出错。取消static后才正 ...
按照C语言规范,应该写成volatile型的,因为任务和中断同时对它进行了写操作。
takashiki 发表于
你没有理解我的意思。我的意思是你的定时器本身就已经不准了,造成所有任务的执行时刻均不可预测,而且 ...
我也同意,尽量在中断里做最少的事情,然后在主程序里做事情。把任务放在中断里感觉更不靠谱了。
像avr在进入一个中断时会关闭所有中断的。那如果timer里做的事情太多了,岂不是耽误了其他中断。
smset 发表于
中断里能否执行状态机:
可以参考这个文章,写得还不错:
这个文章我看了,但是和你这个具有明显的区别:
他这个在一开始执行定时器中断时,就已经使用了RETI跳出了中断,后续的任务实质上是在主函数中执行的。他这么做是让编译器完成了寄存器的保护操作,而不是像平时我们写OS时手动保护寄存器。你这个一旦定时器被阻塞,那后续的缺陷无法弥补。
本帖最后由 smset 于
18:40 编辑
那个reti只是为了开中断,让同级中断可以响应。
后续任务还是在中断里面执行的啊。 文章里面写得很明白的啊。
呃,换句话说,不管实质上任务在主函数还是在中断里运行,
如果他这种方式可以保证程序按时运行,
对于我们是否有一定的借鉴意义?
smset 发表于
那个reti只是为了开中断,让同级中断可以响应。
后续任务还是在中断里面执行的啊。 文章里面写得很明白的 ...
他这个方法恰恰解决了你的方案所存在的缺陷:中断不可重入。他这个是可以重入的,因为一旦进入定时器中断,定时器中断就会被阻塞,于是他就CALL RETI了,效果是中断处理实质上已经退出了。不可否认的是,RETI就是退出中断。至于前面的CALL,那是换了一个手法,执行的是RETI后面的指令,实质上和main中执行没有任何区别。
也就是说,他通过汇编偷天换日,把形势上位于中断的程序的执行权交给了main。
这样的程序完全利用了编译器的弱智。如果编译器超强的话,那么它在识别reti之后,后面的代码是应该优化掉的,但是显然Keil C51在这方面无能为力。
本帖最后由 ifree64 于
20:53 编辑
takashiki 发表于
他这个方法恰恰解决了你的方案所存在的缺陷:中断不可重入。他这个是可以重入的,因为一旦进入定时器中断 ...
void timer0_isr(void) interrupt 1
& && &EnableInterrupt();
& && &// other code...
EnableInterrupt:
& && && && & ACALL _RETI
_RETI:& &&&RETI
这里并不是退出中断,而是执行RETI命令,但不退出中断。
1、中断里调用EnableInterrupt函数,堆栈里存放返回地址,返回地址是other code这部分代码;
2、进入到EnableInterrupt,ACALL _RETI,堆栈里存放返回地址,返回地址是_RETI这个标签的代码;
3、执行RETI,使能中断,并返回到_RETI这个标签;
4、再一次执行RETI,返回到EnableInterrupt这个调用的后面,仍然没有退出中断。
如果在ISR里面直接插入RETI指令,那就是退出中断了。其实作者有点多余,不需要ACALL _RETI这个指令。
ifree64 发表于
void timer0_isr(void) interrupt 1
& && &EnableInterrupt();
不不不,你理解错了。这句不能去掉的,去掉了中断就无法重入了。
RETI的效果相当于ret + 释放中断标志。执行了这个汇编之后,程序依然会继续执行,宏观表现是就是在中断中执行的。但是它实实在在的释放了中断标识,释放了中断标志就意味着退出中断了,中断处理器硬件认为这次中断已经完成,剩下的代码名义上是由中断调用的,实际上和主函数调用没有区别。
事实上,代码在主函数中和中断中执行本质上是没有区别的。
如果你曾经使用过ARM,你可能会赞同我这个说法。ARM中使用了几个标志位表示当前的状态,比如SVC态,用户态,异常态等等。用它这种方案偷梁换柱之后,中断一旦退出,那么状态是会发生改变的,比如会转到用户态,虽然它依然是在名义上中断中执行的,但是已经无法执行中断中可以使用的某些特权指令了,只不过是打入了中断内部的间谍而已。
takashiki 发表于
不不不,你理解错了。这句不能去掉的,去掉了中断就无法重入了。
RETI的效果相当于ret + 释放中断标志。 ...
我说的是可以去掉EnableInterrupt里面的ACALL _RETI这条指令,因为加上它起到的效果仅仅是把RETI执行了两遍,EnableInterrupt里面仅仅放一条RETI就够了。
ifree64 发表于
花了点时间写了这样一种调度方法,调度思想与这个帖子里的类似。
请大家点评下。简单解释下为什么没有在任 ...
嗯,我以前也调试过这个帖子的代码。这种调度思想也是很好的,能否说说你的改进之处。
只是这个调度器的局限性在于,它是针对51单片机设计的。其他单片机用不了。
本帖最后由 smset 于
11:58 编辑
综合这几天的讨论,先更新一版,仅供评测用:
#include &stc89c51.h&
/****小小调度器开始**********************************************/
#define MAXTASKS 3
volatile unsigned char timers[MAXTASKS];
#define _SS static unsigned char _ switch(_lc){default:
#define _EE ;}; _lc=0; return 255;
#define WaitX(tickets)&&do {_lc=__LINE__+((__LINE__%256)==0);} while(0); case __LINE__+((__LINE__%256)==0):
#define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) timers[TaskID]=TaskName(); }&&while(0);
#define RunTaskA(TaskName,TaskID) { if (timers[TaskID]==0) {timers[TaskID]=TaskName();} }& &//前面的任务优先保证执行
#define CallSub(SubTaskName) do { _lc=__LINE__+((__LINE__%256)==0); return 0; case __LINE__+((__LINE__%256)==0):&&currdt=SubTaskName(); if(currdt!=255)} while(0);
#define UpdateTimers() { for(i=MAXTASKS;i&0 ;i--){if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}}
#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;
sbit LED0 = P2^5;
unsigned char task0(){
_SS
&&while(1){
& &WaitX(50);
& &LED0=!LED0;& &
&&}
_EE
}
unsigned char&&task1(){
_SS
&&while(1){
& &WaitX(100);
& &LED1=!LED1;& &
&&}
_EE
}
unsigned char&&task2(){
_SS
&&while(1){
& &WaitX(100);
& &LED2=!LED2;& &
&&}
_EE
}
void InitT0()
{
& && &&&TMOD = 0x21;
& && &&&IE |= 0x82;&&// 12t
& && &&&TL0=0X
& && &&&TH0=0XDB;
& && &&&TR0 = 1;
}
void INTT0(void) interrupt 1 using 1
{
& & TL0=0X& & //10ms 重装
& & TH0=0XDB;//b7;& &
& & UpdateTimers();
& & RunTask(task0,0);//任务0具有精确按时获得执行的权限,要求:task0每次执行消耗时间&0.5个 ticket
}
void main()
{
& && &&&InitT0();
& && &&&while(1){
//& && && &&&RunTask(task0,0);
& && && && && & RunTaskA(task1,1);//任务1具有比任务2高的运行权限& & & & & & & && &
& && && &&&RunTaskA(task2,2);//任务2具有低的运行权限& & & & & & & && &
& && &}
}
复制代码主要改动有: 1) 增加了RunTaskA宏,让前面的任务有优先执行的权限, 这个思路是zsmbj提出的。
& && && && & 2) 修改了INTT0中断,定时器重装的位置放到到最前面,这个是得到takashik回帖i的启发,定时器重装确实需要放在前面。
& && && && & 3) 仍然保留了中断执行高级任务的机制, 但是明确了高级任务效率要求。
对于第三点,争议可能是最大的。
我经过综合考虑,还是保留了这个机制,至于是否使用这种机制,取决于编程人员自己的需要和喜好。
至于中断中启用高级任务机制,这个需要编程人员具备一定的判断力,就是判断任务函数单次执行的消耗时间,
我认为这个是一个基本功,不算难度高的技能,很多其他的实时调度器中,也都要求判断任务执行所消耗时间。
如果一个编程者,对于任务函数执行的大致时间都不能估算,即便不采用高级任务机制,或者直接采用其他高级的实时RTOS系统,我相信一样也做不好实时性应用。
smset 发表于
综合这几天的讨论,先更新一版,仅供评测用:主要改动有: 1) 增加了RunTaskA宏,让前面的任务有优先执行 ...
CallSub有改进,变量定义在内部,解决了多次调用的问题,而且汇编进一步优化,在avr下不再多占用一个内存了。
#define CallSub(SubTaskName) do { _lc=__LINE__+((__LINE__%256)==0); return 0; case __LINE__+((__LINE__%256)==0):&&currdt=SubTaskName(); if(currdt!=255)} while(0);
我把上面的改成下面写法,编译出的代码是一样的:
#define CallSub(SubTaskName) do { WaitX(0); currdt=SubTaskName(); if(currdt!=255)} while(0);
zsmbj 发表于
CallSub有改进,变量定义在内部,解决了多次调用的问题,而且汇编进一步优化,在avr下不再多占用一个内存 ...
对,两种写法是等效的, 你的语法更精炼。
ifree64 发表于
花了点时间写了这样一种调度方法,调度思想与这个帖子里的类似。
请大家点评下。简单解释下为什么没有在任 ...
可否讲解一下这个OS 的特点。有些地方没看明白。
smset 发表于
嗯,我以前也调试过这个帖子的代码。这种调度思想也是很好的,能否说说你的改进之处。
只是这个调度器的 ...
对于他的贴子中的代码,在调度思想上本质是一样的,所以没有本质的改进只是相对于原帖加入了timer的服务。
另我正在尝试给这种调度器加入抢占的机制,我也写了些测试代码测试代码能正常稳定工作,但还需要仔细推敲下。
对于这种写法的可移植性我也在思考中,我想如果编译器保证在调用函数时,被调用函数可以随意使用寄存器,这个调度思想就可以移植,也就是
说调度器不需要进行寄存器的保护,所有需要保护的寄存器由编译器在编译函数时已经做了必要的保护。
本帖最后由 ifree64 于
15:30 编辑
zsmbj 发表于
可否讲解一下这个OS 的特点。有些地方没看明白。
我写这个OS的目的是想做个比较,到底传统的调度方式和基于状态机的调度方式在性能上的差异在哪里,有没有可能传统的调度方式和基于状态机的方式在理论上开销是一致的?
因为不同写法都是做相同的事:保存断点,切换任务,恢复运行。
理解我这种写法,核心在于理解调度是如何工作的,其实想明白后也是很简单的。简单说明如下,比如有两个任务,TaskA,TaskB
void TaskA()
& && &&&while(1){
& && && && && & LED1 = !LED1;
& && && && && & OSSched();
& && && && && & OtherCode1();
void TaskB()
& && &&&while(1){
& && && && && & LED2 = !LED2;
& && && && && & OSSched();
& && && && && & OtherCode2();
虽然我们在实践中不会在任务函数中去掉用OSSched,而是调用OSWait;但调用OSWait本质上还是调用OSSched来完成任务切换,所以为了理解方便,直接写作调用
1、系统启动,在main中的OSStart函数执行完毕后,将执行到OSStart函数的最后一条语句,RET
& &&&RET语句的具体动作时,将堆栈顶部的返回地址弹出到PC,并执行这个地址处指令;由于OSStart函数里
& &&&已经将堆栈顶部地址设置为了TaskA的地址,所以RET实际上返回到了TaskA任务;
2、TaskA开始执行,执行到调用OSSched这条语句,它的汇编代码将是一条CALL指令,CALL指令做了两件事,
& &&&第一,将返回地址送到TaskA的堆栈;也就是OtherCode1这条语句的地址进入堆栈,第二,跳转到OSSched这个函数出去执行;
& &&&按照“正常”情况,OSSched返回时,将返回到TaskA中,并继续执行OtherCode1
3、但是,OSSched函数,查找了下一个就绪任务,并且将当前堆栈切换到了TaskB的堆栈,当OSSched返回时,RET指令
& &&&将把堆栈B的返回地址送到PC,而我们事先将TaskB的地址送到了TaskB的堆栈;于是OSSched的返回并没有返回到TaskA,
& &&&而是返回到了TaskB这个函数去执行。于是任务就切换了,由TaskA切换到了TaskB;
3.1 TaskA切换到了TaskB,那么TaskA停在哪里了呢?从TaskA的角度来看,它是调用了一个函数,因此它在调用OSSched这个位置被打断执行了;
& &&&如何回到TaskA呢?
4、我们继续看TaskB的运行;TaskB运行到调用OSSched这个点处,TaskB也被打断了,进入OSSched,这次假设OSSched发现TaskA处于就绪状态;
& &&&OSSched重新将堆栈切换为TaskA的堆栈;由于先前第2步中A调用OSSched时,堆栈中存放的返回地址是在OtherCode1处,因此RET返回时,返回到了
& &&&TaskA先前“换出”时的位置,恢复运行OtherCode1.
5、同理,当下一次TaskA调用OSSched时,又将恢复到先前TaskB的“断点”处运行。这在实质上仍然是合作式调度,A不需要CPU时,调用OSSched换出到其他任务;
& &&&其他任务不需要CPU时,也调用OSSched换出。
6、如果TaskA、TaskB换出后,处于阻塞状态,OSSched就查找不到就绪的任务,这时OSSched调度系统任务OSIdle运行。OSIdle执行休眠指令。
7、如何唤醒?中断将单片机唤醒后,系统代码流工作与OSIdle里,OSIdle调用OSSched,这时只要有其他就绪任务,其他就绪任务就得到运行权了。
8、如何增加抢占功能?如果直接在中断处理里面调用OSSched就可以实现抢占。但中断抢占存在现场保护问题,这里如何处理才不出错还需要仔细琢磨一下。
ifree64 发表于
我写这个OS的目的是想做个比较,到底传统的调度方式和基于状态机的调度方式在性能上的差异在哪里,有没有 ...
很好啊,如果有任务切换时间的对比数据就更好了。
smset 发表于
很好啊,如果有任务切换时间的对比数据就更好了。
由于跑灯这样的任务太简单了,可能不能说明问题,我们能不能用一些实践上比较有代表性的问题来做比较。
在wait比较少时,ProtoThread的方案可能非常有优势,但当任务具备一定的复杂度后也许ProtoThread的开销就上来了。
本帖最后由 smset 于
17:09 编辑
呵呵,感谢版主置酷。
我检查了下以往产品中的代码,大部分任务函数中的WaitX没有超过10个。
我们可以按5个,10个,15个,20个WaitX的情况,分别做对比测试。
还有就是调用子任务嵌套情况下的对比。 嵌套1层,嵌套2层,嵌套3层。 我估计在任务嵌套层数加多的情况下,protothread会处逐渐处于劣势,
好在嵌套很多层子任务的情况不多见,我的应用中最多是嵌套2层。
阿莫电子论坛, 原"中国电子开发网"

我要回帖

更多关于 单片机好玩吗 的文章

 

随机推荐