這次修改的內容,主要是加入 code banking 功能。
- 這次主要是參考 Keil C51 的 RTX-51 code banking 設定,利用 ?B_CURRENTBANK 及呼叫 ?B_RESTORE_BANK 來切換。
- L51_BANK.A51 除了主要的設定外,需要把 ?B_RTX 也設成 1,才能讓 ?B_RESTORE_BANK 函數可以使用。
- 在初始化過程中,也要先切換第一個 task 的 code bank。
在 tiny rtos for 8051 part2 裏,本來想把已經測試好的程式碼加入 code banking 的相容性,但經過幾天幾夜的測試,不管怎麼做都會出錯,真是莫名奇妙。後來就倒回去看看是否之前程式碼是否也有問題,結果真的是如此。part1 的程式碼執行無誤,part2 的程式碼執行有時會出錯。經過反覆檢查,發現把 interrupt 的程式碼全改成 assembly 就會出錯,即使照著 Keil C51 編出來的 assembly code 寫進去也是一樣有問題,只能猜想是否在 link 過程中,又被動了一些手腳,造成程式莫名奇妙的出錯吧。
經過反覆測試,發現將 interrupt 全改寫成 assembly 會造成不明原因輸出錯誤,這個原始碼不要用喔,後面有解決方法。
原始碼下載在這次的實驗裏要解決 reentrant 函數的問題,其實也很簡單,就是把 reentrant 的 stack 變數依照每個 task 儲存起來,執行該 task 時,再回復其值就好了。程式碼如下,都是用 c 語法做說明,實際程式碼很多都改成 assemble 語法,主要是因為 Keil C51 的 reentrant stack 是存在 ?C_IBP,而 C 語法不允許變數有 ? 符號,所以就改成用 asseble 來寫。
接下來二個 test tasks 就全改成 reentrant,如此就不會有 data overlaying 的問題,而且 Code Optimization Level 也可以調到 7:Extended Index Access Optimizing。執行結果就會看到數字跟英文字母相互出現。
寫到這裏也許有人會覺得奇怪,為什麼 Code Optimization Level 沒有調到預設值 8:Reuse of Common Entry Code?主要是因為在 rtos_start 裏,我利用 LCALL 的特性把 stack 裏的返回位址改成 task1 的位址,而 level 8 會把 main 函數裏的 LCALL rtos_start 改成 LJMP rtos_start,造成程式無法執行。當然只要一個小修改就可以修正掉這個 bug 了。
最近有空去研究了一下陳明計的 small rtos for 8051,看了老半天,還是看不懂他在寫什麼;尤其是他在 stack 的處理方式,實在是有看沒有懂。所以自己就自己來寫了一套 tiny rtos for 8051。沒有 semaphore,也沒有 task priority,什麼都沒有,只有簡單的時間分配,也就是所有工作時間都一致,current task 工作固定時間後,再換 next task 工作固定時間,如此循環工作。
工作原理也很簡單,如果有 n 個 tasks 就把 stack 分成 n 等份,讓每個 task 保有自己的 stack,不要互相干擾就可以。再利用 timer interrupt 來切換 task。很簡單吧,就如下面程式碼所列,並不會很難。
寫了二個測試函數,主要就是從 rs232 印出 1~9 及 a~i,如下所示。
但是結果卻不如預期,跑出來的東西都是亂碼,傻眼。最後,經過了二天的努力,總算把結果正確無誤的弄出來了。主要有下列幾個地方要修改。
一般的編譯器將函數中的區域變數動態配置在 stack,等函數結束空間就釋放出來。因為 8051 的內部記憶體很少,只有區區 128 或 256 bytes,而且 stack 也是共用這塊記憶體。為了節省 stack 空間,所以區域變數基本上是靜態配置在固定位址, 也就是變成全域變數。如此就又造成浪費記憶體的情況,為了解決這個問題,所以 8051 的編譯器基本上都採用所謂的 data overlaying 技術來克服區域變數浪費空間的問題。
所謂 data overlaying 是指沒有呼叫關係的函數,它們的區域變數區可以重疊在一起(共用一塊記憶體)。Keil C51 會分析程式中函數間呼叫的關係,產生一個呼叫樹。它就根據這個呼叫樹來決定那些函數的區域變數區可以 overlaying 在一起。一種情況是是編譯器發現某一個函數(不是 main)沒有被別的函數呼叫,這會造成編譯器的困惑。一個正常的程式,除了 main 之外,除非是垃圾程式碼(沒用處但沒有刪除),否則所有的函數應該是至少會被一個其它函數呼叫的。編譯器在安全至上的原則下,會認定它的分析無法正確的辨識這個函數呼叫關係,所以對這個函數的區域變數就會獨立配置,不會重疊配置。這樣有沒有問題?邏輯上當然不會有問題,但沒 overlaying 就是會浪費記憶體,而且也會一直產生 *** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS 的警告。
什麼情況下,函數的呼叫樹讓編譯器無法分析?真實應用是有一些情況會發生這樣的問題。最常見的就是使用函數指標來呼叫函數。因為呼叫是執行時期動態變動的,這就可以難倒編譯器了。在這種情況這些被呼叫的指標函數就會獨立配置它們的區域奱數。如果你要這些函數也能正確的使用 overlaying 的好處,那麼你就必需手動分析那些函數的呼叫樹,然後告訴編譯器就可以了。這樣你也就不會在編譯時產 UNCALLED SEGMENT 的警告了。