tiny rtos for 8051 part4

Posted by: 邱小新 at 下午4:40 in , ,
原始碼下載

這次修改的內容,主要是加入 code banking 功能。

  1. 這次主要是參考 Keil C51 的 RTX-51 code banking 設定,利用 ?B_CURRENTBANK 及呼叫 ?B_RESTORE_BANK 來切換。

  2. L51_BANK.A51 除了主要的設定外,需要把 ?B_RTX 也設成 1,才能讓 ?B_RESTORE_BANK 函數可以使用。

  3. 在初始化過程中,也要先切換第一個 task 的 code bank。

tiny rtos for 8051 part3

Posted by: 邱小新 at 下午3:49 in , ,
原始碼下載

在 tiny rtos for 8051 part2 裏,本來想把已經測試好的程式碼加入 code banking 的相容性,但經過幾天幾夜的測試,不管怎麼做都會出錯,真是莫名奇妙。後來就倒回去看看是否之前程式碼是否也有問題,結果真的是如此。part1 的程式碼執行無誤,part2 的程式碼執行有時會出錯。經過反覆檢查,發現把 interrupt 的程式碼全改成 assembly 就會出錯,即使照著 Keil C51 編出來的 assembly code 寫進去也是一樣有問題,只能猜想是否在 link 過程中,又被動了一些手腳,造成程式莫名奇妙的出錯吧。

這次主要修改的內容

  1. 原本 rtos_isr 函數維持加入部份 assembly code,不要全部改成 assembly code。

  2. ?C_IBP 儲存方式改成放入堆疊裏,以利精簡程式碼,減少 interrupt 執行時間。

  3. 原本的 void (* const task_func[MAX_TASKS])(void) 改成 unsigned int code task_func[MAX_TASKS],將函數指標改成一般變數放在 rom 裏,如此可以節省記憶體用量,也精簡程式碼。

  4. 為了避免 LCALL rtos_start 變成 LJMP rtos_start 問題,直接改變 SP 的初始化過程,藉由修改 STARTUP.A51 來增加 STACK 位址的讀取。

  5. 不可以將 STARTUP.A51 中的 IBPSTACK 設成 1,原來使用 reentrant 函數都需要將 IBPSTACK 設成 1,但是不知什麼原因,在用 IspWriter 燒錄完後的自動重啟執行都會出現怪怪的執行結果。

tiny rtos for 8051 part2

Posted by: 邱小新 at 下午2:08 in , ,

經過反覆測試,發現將 interrupt 全改寫成 assembly 會造成不明原因輸出錯誤,這個原始碼不要用喔,後面有解決方法。

原始碼下載

在這次的實驗裏要解決 reentrant 函數的問題,其實也很簡單,就是把 reentrant 的 stack 變數依照每個 task 儲存起來,執行該 task 時,再回復其值就好了。程式碼如下,都是用 c 語法做說明,實際程式碼很多都改成 assemble 語法,主要是因為 Keil C51 的 reentrant stack 是存在 ?C_IBP,而 C 語法不允許變數有 ? 符號,所以就改成用 asseble 來寫。

void rtos_isr(void) interrupt 1 { // save current stack rtos_stack[rtos_task_id] = SP; rtos_stack_IBP[rtos_task_id] = ?C_IBP; // load next stack if (++rtos_task_id >= MAX_TASKS) rtos_task_id = 0; SP = rtos_stack[rtos_task_id]; ?C_IBP = rtos_stack_IBP[rtos_task_id]; } void rtos_start(void) { xdata unsigned char i, size; xdata unsigned char idata *sp; // variable init rtos_task_id = 0; // stack init size = (256 - SP + 2) / MAX_TASKS; sp = (unsigned char idata *)SP; for (i=0; i<MAX_TASKS; i++) { *sp-- = ((unsigned int)(task_func[i])) / 256; *sp-- = ((unsigned int)(task_func[i])) % 256; rtos_stack[i] = sp + 15; sp += size; rtos_stack_IBP[i] = sp; sp += 2; } ?C_IBP = rtos_stack_IBP[0]; // timer0 init TMOD = (TMOD & 0xF0) | 0x01; TH0 = 0; TL0 = 0; TF0 = LOW; ET0 = HIGH; TR0 = HIGH; }

接下來二個 test tasks 就全改成 reentrant,如此就不會有 data overlaying 的問題,而且 Code Optimization Level 也可以調到 7:Extended Index Access Optimizing。執行結果就會看到數字跟英文字母相互出現。

void task1(void) reentrant { unsigned char i=0x30; while(1) { if (++i>=0x3a) i=0x31; putchar(i); delay_1ms(100); } } void task2(void) reentrant { unsigned char j=0x60; while(1) { if (++j>=0x6a) j=0x61; putchar(j); delay_1ms(100); } }

寫到這裏也許有人會覺得奇怪,為什麼 Code Optimization Level 沒有調到預設值 8:Reuse of Common Entry Code?主要是因為在 rtos_start 裏,我利用 LCALL 的特性把 stack 裏的返回位址改成 task1 的位址,而 level 8 會把 main 函數裏的 LCALL rtos_start 改成 LJMP rtos_start,造成程式無法執行。當然只要一個小修改就可以修正掉這個 bug 了。

tiny rtos for 8051 part1

Posted by: 邱小新 at 下午3:50 in , ,
原始碼下載

最近有空去研究了一下陳明計的 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。很簡單吧,就如下面程式碼所列,並不會很難。

#define MAX_TASKS 2 unsigned char rtos_task_id; unsigned char rtos_stack[MAX_TASKS]; void (* const task_func[MAX_TASKS])(void)={ task1, task2 }; void rtos_isr(void) interrupt 1 { // save current stack rtos_stack[rtos_task_id] = SP; // load next stack if (++rtos_task_id >= MAX_TASKS) rtos_task_id = 0; SP = rtos_stack[rtos_task_id]; } void rtos_start(void) { xdata unsigned char i, size; xdata unsigned char idata *sp; rtos_task_id = 0; size = (256 - SP + 2) / MAX_TASKS; sp = (unsigned char idata *)SP; for (i=0; i<MAX_TASKS; i++) { *sp-- = ((unsigned int)(task_func[i])) / 256; *sp-- = ((unsigned int)(task_func[i])) % 256; rtos_stack[i] = sp + 15; sp += size + 2; } TMOD = (TMOD & 0xF0) | 0x01; TH0 = 0; TL0 = 0; TF0 = LOW; ET0 = HIGH; TR0 = HIGH; }

寫了二個測試函數,主要就是從 rs232 印出 1~9 及 a~i,如下所示。

void task1(void) { xdata unsigned char i=0x30; while(1) { if (++i>=0x3a) i=0x31; putchar(i); delay_1ms(50); } } void task2(void) { xdata unsigned char j=0x60; while(1) { if (++j>=0x6a) j=0x61; putchar(j); delay_1ms(50); } }

但是結果卻不如預期,跑出來的東西都是亂碼,傻眼。最後,經過了二天的努力,總算把結果正確無誤的弄出來了。主要有下列幾個地方要修改。

  1. 把 C51 的 Code Optimization Level 降成 1:Dead code elimination。因為 Data overlaying 的關係,造成 task1 的 i 變數跟 task2 的 j 變數共用同樣的位址,所以讓印出來的值都在 1~9 跳動。

  2. 原本 data overlaying 問題想要利用 reentrant 指令來建立可重入函數去解決,但最後發現仍然是無效。因為 keil 把變數都放在一個 ?C_IBP 的位址,仍然會造成共用的問題,這個問題真是難解啊,以後如果要寫複雜的 multi-task 函數有一定的難度,必須克服可重入函數及 data overlaying 造成的問題。

  3. 在 rtos_isr 函數裏加入 push 及 pop 指令,把 R0~R7,ACC,B,PSW,DPH,DPL 都丟到 stack 裏去。因為 keil 都會利用這些暫存器做一些運算,在進入 interrupt 時,也會主動 push 一些在 interrupt 會用到的暫存器到 stack 裏,但其它沒用到的暫存器就沒有主動 push,造成切換到其它 task 再切回原來 task 時,那些暫存器裏的值都被變更了,而造成運算錯誤。

  4. 另外在加入 assembly code 時,需把 Generate Assembler SRC File 及 Assemble SRC File 都打勾才可以,缺一不可。

Keil C51 Data Overlaying

Posted by: 邱小新 at 下午2:41 in

一般的編譯器將函數中的區域變數動態配置在 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 的警告了。