Coroutine in FPPA

 

0. 話說從頭

前陣子,一位知名台灣大牛對本人說:「Godspeed,也許你可以替我們公司
寫篇文章,FPPA 可以把一顆 CPU 模擬成兩顆喔!」,「喔,好厲害喔(擺明
是敷衍XD)」,過了沒多久,站長也把這件事忘的一乾二淨。

直到看到 Jserv 大大的 blog 寫了一篇討論 coroutine 的文章,才驚覺也許
可以把這個技巧用到 FPPA 上,那豈不是原本 8 顆 CPU 的 FPPA 可以變成
8*2 = 16 顆甚至是 32 顆,64顆...哈哈哈(其他同事:「這個死阿宅又起笑了!」)

回到正題,於是小弟花了一點時間實驗,在這裡把成果跟大家分享一下(到目前
為止好像都是這種看不到實際硬體的文章比較多?站長你很混喔!?)

1. coroutine 是什麼?

按照 wiki 上面的解釋:

In computer science,
coroutines are program components that generalize subroutines to allow
multiple entry points and suspending and resuming of execution at certain
locations. Coroutines are more generic and flexible than subroutines, but are
less widely used in practice. Coroutines originated as an assembly-language
technique, but are supported in some high-level languages, Simula and Modula-2
being two early examples. Coroutines are well-suited for implementing more
familiar program components such as cooperative tasks, iterators, infinite lists,
and pipes.

注意到這段:

"...allow
multiple entry points and suspending and resuming of execution at certain
locations."

也就是說允許副程式具有多個進入點,並且允許暫停執行與從某些確定的位址恢復執行。
(聽起來是不是很像 multithreading?)

再看到這句:

"Coroutines originated as an assembly-language technique", but are supported in
some high-level languages, Simula and Modula-2(書上常出現的兩種語言,但是從沒聽過
有誰用過!?)

也就是說,要是組合語言寫不出來,高階語言也沒什麼指望了~
(開玩笑的,但是用組語的卻不用像 C 用一堆稀奇古怪的 macro,還比較好寫,而且 FPPA
到目前為止也只有 ASM 可用)

還有這句:

Coroutines are well-suited for implementing more
familiar program components such as cooperative tasks

cooperative task 就是以前 win 3.1 時代那種鳥到不行的多工,還記不記得 Windows SDK
的書上不停強調,一定要呼叫某 API 釋放執行權,不然整個視窗系統就會死當??

現在各位知道 coroutine 是什麼東西了吧? :)

2. coroutine 的用途

不用 coroutine 某些程式就寫不出來嗎?答案當然是 No,基本上程式本來就沒有
一定的規範,尤其是組合語言這種東西,要寫成跟狗啃的一樣很容易,一個 call
都不用全部 goto 過來過去也不是不行(有時也是不得已,誰叫 8bit MCU 廠商只提
供少不啦機的 SRAM),但是假如你希望程式的結構能夠優雅一點,不要三個月後咒罵
三個月前的自己的話,coroutine 說不定可以幫上你的忙。

舉個例子來說,前陣子站長的同事 Johnny 兄用 FPPA 作了一個超音波 demo kit,就挺
適合使用 coroutine,而不需要真的用兩顆 CPU 去做 Tx/Rx,當然這只是站長對超音波
片面的瞭解,哪位超音波達人看了覺得不妥請指正不要客氣 :)

超音波距離偵測的過程是先用超音波發射器產生一段 40KHz 左右的脈衝(burst),
射到物體反彈回來後,測量接收器的 pulse 寬度判斷距離物體的距離:

由圖中我們可以看到,當發射一段脈衝之後,碰到障礙物之後彈回,Rx sensor
會把彈回的信號轉成 pulse,測量 pulse 的寬度就可以知道距離障礙物有多遠
(就筆者知道的皮毛,pulse 越短代表距離越進)。重點是各位可以發現測量
程式其實是不斷在 Tx/Rx 中切換,各位會不會覺得這很像切換兩個 task 呢?

就筆者的看法,coroutine 特別適合這種同時兼具生產者與消費者的應用。
(wiki上也是以此作為範例)當然用兩顆 CPU 一顆當 Tx 一顆當 Rx 也不是不行,
但是話說回來在多核心 MCU 中 CPU 本身也是一項資源,假如能省下一顆 CPU
豈不是能做更多應用嗎?(多核心的缺點就是害得工程師功能加不完,無法
準時下班,生活品質變差...= =)

3. 實做 coroutine

由於是用組合語言實做,所以我們可以輕易的控制 Program Counter,又加上
FPPA 提供了兩個好用的指令 - pushw/popw,先稍微解說一下這兩個指令,
從外觀就可以看出來,這兩個指令存取資料的寬度是 16bits,也就是 word
(要稍微澄清一下,word 不一定是指 16bits,像是在 ARM 中指的是 32bits,
不過這裡指的是 16bits 就是了),也就是說可以用來控制 PC,像是下面這樣:

ok,那要如何用這兩個指令來實做 coroutine 呢?這就需要一點巧思了,
首先回想一下傳統的 RTOS,每個 task 必定有 TCB(Task Context Block)
這一塊資料結構,TCB 中一定會存放 PC,在這裡假設有兩個 task,所以
我們需要兩個變數備份 PC:

此外還有其他暫存器也要備份起來,但是這裡為了簡化就免了(其實 FPPA 每個
CPU 也只有 PC, ACC, FLAG, SP 這4個暫存器而已),另外 RTOS 中每個 task
有其獨立的 stack,但是 coroutine 是在同一個 stack 堆疊上運作,所以
這也可以免了(其實這也是 coroutine 與 multithreading 不同之處)

好,現在來看關鍵的部份,也就是如何進行 task switch,回想一下什麼時候
PC 會跟 push/pop 扯上關係呢?我想大概每個人都會猜到就是 call function
的時候,當副程式結束時呼叫 ret 指令會自動呼叫 popw pc#N,於是我們可以
在這裡動點手腳,把 stack 裡的資料偷偷換掉,換成欲切換過去的 task 位址,
原來在 stack 中的 PC 值則備份到前面定義的變數裡,於是我們可以設計兩個
副程式一個稱為 Task0Sleep、另一個稱為 Task1Sleep:

有了這兩個副程式,task0/1 就可以很方便的切換到對方,而切換回來時
從 call Task0/1Sleep 的下一個指令開始執行,如此一來可以改善原本的
設計流程免除一堆旗標與 goto 的使用。最後我們做個總結,看圖就一目了然:

註:
上圖範例程式一開始將 label Fpp5Task1 的位址存到 Task1PC,但是又把 Task1PC - 1,這是因為
ret 指令會取出 return address + 1 再放到 PC 的緣故。

4. 心得

說實話,這招剛發明時站長也不知道可以拿來幹嘛,不過心理總覺得終究
有一天會派上用場,一直到最近好像才派上一點用場,所以假如各位有什麼好的
idea 也請不吝分享,謝謝!

5. 參考資料

Jserv's blog: 使用 coroutine 實做 user-level thread

Wiki: coroutine