朱工

早年从事单片机、实时控制系统产品设计及编程。目前耄耋之年开始学习AI技术。

实现协程多任务的无标号单步跳转方法

0
阅读(4511)

实现协程多任务的无标号单步跳转方法

-- 微控制器中基于协程的实时协作多任务方法 (3)

  Protothreads的switch方法编译后的目标代码是通过多步比较才找到case的入口。而Protothreads的addrlabels方法的高效就在于它是用“goto *lc”一步跳转到后续点LC(Local Continuation)。但是,主流的C编译器不支持这样的goto。

  正如当初达夫在引入后来被叫做“Duff’s Device”的Switch-Case机制时,被人称作诡异(trick)方法。正是Simon Tatham借用这种诡计才把Coroutine引入C编程,而Adam Dunkels受到Simon的启迪而创建了Protothreads。既然主流的C编译器也接受诡异的方法,而民众化的GNU GCC做了“Labels as Values”扩展,那么我们何不采取一些办法让主流的C编译器(包括8051的C编译器)也接受Protothreads的addrlabels方法,让协程重入时一步跳转到后续点LC。

  后面,就是我要表述的用C和汇编混合编程实现的“无标号单步跳转方法”。

  GOTO不用标号,似乎讲不通。其实一点就明。C编译器为了提高代码执行效率,允许与汇编语言混合编程,互相留有接口。这就是契机。

  任何CPU都有调用子程序的指令——不妨叫CALL吧。CALL指令为了执行子程序后返回,都要CPU先记住后面的指令地址,才跳到调用的子程序。所以,我们只要在后续点LC之前,用一条混合编程CALL指令,LC就已经在握了,而不必在LC写上标号,再去取标号。所以,不管编译器允许不允许取标号地址,CPU已经取得LC的地址,我们只有把它存到变量lc中去就行了。

  原理已定。下面就是具体的实施。因为不同的CPU有不同的CALL指令,而且不同的C编译器有不同的汇编语言书写格式,所以文档可能因CPU和编译器有所不同,但大同小异。

  程序代码部分见附件: 用C和汇编混合编程实现协程多任务方法(ARM).pdf 

  首先说明一下:

  在这里,我们不再使用Protothread及缩写PT,而使用协程Coroutine这个词及缩写CR。因为Protothread有其特定的内涵。这里的扩展,已超出其内涵;拓展未必受欢迎。所以使用“协程”比较不会有争议。

  并且,我们不使用抢占式多任务常用的阻塞(block)一词,而使用YIELD(让出)一词,因为它更能体现协程多任务方法的协作思想。

  对于本方法,我们还有如下几点要说。

一.关键点

  因程序文档中不见标号,初读起来有点难度,所以先把关键点拿出来说明一下。

ARM Cortex-M 的Thumb指令:

C语句:

EXTERN void SUBROUTINE(void) ;

SUBROUTINE() ;

//<- LC is here (后续点LC就在这里)

DoSomething …

汇编语句:

_ SUBROUTINE:

; lr 中就是LC的地址,把它保存到lc中就行了。

;(Thumb指令是LC的地址+1,返回跳转时BIT0要置1。所以不减不增,就保存地址+1)

[有关8051指令部分,我们另外给出]。

二.way_out()的作用

  我们把CR_START宏展开如下:

retcode_temp=CR_LEAVESETTING;

way_out();

//quit_addr: <- way_out()的作用就是取得这个地址。

if (retcode_temp != CR_LEAVESETTING) { return retcode_temp; }

if (lc != 0) { resume_lc(lc); }

  way_out()的作用就是为了取得quit_addr这个出口地址,YIELD让出时,跳到这里,统一出口路径。

  这里,我们顺便讲一下:多入多出与单入单出。

  从协程本身的定义来讲,有多个出入口。在用汇编语言实现协程多任务方法时,编程者自己可以决定是否可以从协程中间某处跳出。而C语言的函数,从规范性来讲,是单入单出的。所以,用C来实现协程多任务方法时,必须将多入多出纳入单入单出的规范。因为调用C函数时,编译器经常会在入口处对寄存器和栈指针做一些处理,而在出口处恢复原状。

  CR_START宏就是将多入多出汇集为单入单出。单入,与Protothreads没有什么区别。单出,就是为汇编函数提供与C语言接口的统一出口。本来统一出口与CR_END宏放在一起比较直观。但多任务程序中,每一任务往往是一个while(1)或for( ; ; )的无限循环。循环后的CR_END宏会被聪明的编译器优化掉,而仅仅只起一个语法作用。因此只好把它放在入口的必经路上。就是说,还没进去,就留好了退路。在START中安置退路,不怕被人耻笑?哈哈,人人会用WINDOWS,大家为了关机,不是也要先点击START吗。听说当年WINDOWS刚出时,通用汽车公司的老总就笑话盖茨:哪有汽车停车要先打火的道理!

三.为什么不用C语言内嵌汇编

  原则上,本方法也可以用C语言内嵌汇编的办法来实现。但是,有几个因素是要考虑的。首先,KEIL编译器不支持ARM Thumb指令内嵌汇编;而本方法主要是针对Cortex-M0、-M3等使用Thumb指令的小型MCU。另外,现在的编译器越来越聪明,有时在优化时要省掉CALL指令,直接跳到子程序。而用CALL指令取得后一指令地址,恰恰是“无标号”方法的核心。这样的优化,将使方法失灵。所以,我们趁着目前编译器的“手还不够长”,无法伸到与C语言文件独立的另一个汇编语言文件中去做优化,而保持用CALL指令去调用汇编子程序,这就能达到我们的目的。

四.可移植性

  本方法将会受到一下指责:可移植性会受到影响。我认为,工程师的职责是在产品的设计运用中找到解决问题的高效的方法。如果有人喜欢低效的完美,那就自当别论。当然,我也喜欢两全其美的完美。

===

fy_zhu

2013-02-18 SV_CA