前言
由於 IP210S 使用的 flash 有 512KB,所以一定會用到 code banking 的功能,所以這次把 code banking 功能加入 tiny rots 裏。主要新增的部份就是在切換 task 時,會把目前 bank 值 push 到堆疊裏,然後把下一個 task 的 bank 值 pop 出來,並還原 bank 設定。
- IP210S 一開機時會把 A16~A18 設成 0,所以相對應的 port 值在一開始初始化時,不要任意變動。因為只要一變動,address 就會被切換到新的 bank 區。
- 比如我喜歡在初始化時把所有 port 值設成 0xff,所以在初始化過後,bank 會被切換到 bank7。因此在最後一塊必須要有 common code,不然會當機的。
- 由於 demo board 上的 A16~A18 剛好位在二個不同的 port 上,當我從 bank0 切換到 bank7 時,會先設定 A18=1,此時 bank0 會馬上切到 bank4,如果 bank4 剛好沒有 common code,那就馬上當機了,同理設定 A16/A17 值也是如此,所以最好每一個 bank 都要有 common code,不然就是好好注意 A16~A18 設定時的 bank 變化。
- 在 rtos_start 裏記得要多一行指令 *((__idata unsigned char *)rtos_stack[i]) = i;,目的是要把每一個 task 的 bank 值先設定好,免得跑錯 bank 區域了。
- 每一個 task 的起始函數不需要加 __banked,因為那些函數是沒有結束的一刻,所以加不加都無所謂。因為加入 __banked 是要讓編譯器把 call 及 ret 換成 __sdcc_banked_call 及 __sdcc_banked_ret,既然都沒有結束,也不會呼叫到 ret。
前言
在前面一個 tiny rtos 裏只是簡單平均分配時間到每一個 task 裏,但這有一個缺點,就是當某一個 task 需要暫停一下時,無法把時間交給其它的 task,這個時間無形間就被浪費掉了。所以在這一次新的 task 裏,加入了 rtos_delay_10ms 函數,讓 task 可以暫停一段時間,並把時間讓出來給別的 task。
- 把 timer0 的時間都切成 10ms,每一個 task 的運行時間都變成 10ms。
- 多了一個 rtos_tick 變數做 delay 的計數,以 10ms 為單位。
rtos_start
- 原本儲放 stack 的變數多了一個,用來放置 rtos_idle 的 stack。
- 第一個執行的 task 改成 rtos_idle,而非 task1。
- 由於 rtos_idle 沒做什麼事,所以只留一份必要的 stack 空間給它,其它的都分給其它 task。
- 不可以在此函數裏呼叫任何函數,因為呼叫函數會變更 stack 裏的內容(有 push 動作),這樣會把 task1 的 stack 破壞,造成進入 task1 時位置被變更,而跑不到 task1。
- 如果按照之前的方式,直接跳到 task1 執行,而不是從 rtos_idle 執行,就可以在此函數內呼叫其它函數,因為 stack 被破壞並不會影嚮 task1 的執行。
rtos_idle
- rtos_idle 裏其實不沒做什麼事,就是一直進入 idle 模式以利省電。
- rtos_idle 裏不可以呼叫任何函數,因為保留給它的 stack 空間只夠做 task 切換而已,容不下其它的 push 指令。
rtos_nexttask
- rtos_nexttask 新增了計數功能,用來計算每一個 task delay 時間,當 rtos_tick==0 時,該 task 才擁有執行時間。
- 如果所有 task 都處於閒置狀態,就進入 idle task 等待其它 task 被喚醒。
- rtos_nexttask 裏不可以呼叫任何函數,因為如果遇到從 rots_idle 進入的 stack,它的 stack 空間只夠做 task 切換而已,容不下其它的 push 指令。
執行結果
前言
使用 SDCC 來寫 rtos 比起用 keil-c 來得方便許多,因為 SDCC 有一個 naked 修飾元。naked function 會省略前置的 push 動作及最後的 pop 及 ret 指令,這些動作都需要使用者自己加入。在 sdccman.pdf 內的說明指出 naked function 主要是用於 interrupt function 來省略一堆無意義的 push/pop 動作,剛好被我用來增加 push/pop 動作(完全相反的立意)。
rtos_start
- 其實我寫的 rtos 很簡單,也就是把 stack 分成四份,配給四個 task,讓每個 task 的 stack 各自獨立不互相干擾。
- 再利用 timer 0 的中斷處理來切換 task。
- rtos_task_id 是記錄目前正在執行的 task 是那一個。
- rtos_stack 是記錄每一個 task 的 stack 位置。
- 由於在切換 task 時,會 pop 14 個 register 及 task 的執行位置。所以一開始需要把每個 task 的起始位置 push 到堆疊里,並把 stack 起始位置加上 14,在做切換 task 時,才會順利執行相應的函數。
- 由於第一個 task 就是主程序,所以不用做 push 動作,因為在第一次切換 task 時,會自動 push,因此才會有 rtos_stack[0] = SP; 的指令。
- call task1() 及 call rtos_start() 理論上會浪費 stack 空間才對,但因為被 SDCC 最佳化的關係,二個指令都變成 ljmp task1 及 ljmp rtos_start,並沒有浪費到 stack 空間,請安心使用。
rtos_nexttask
- rtos_nexttask 的動作很簡單,就是保存現在所有的 register (r0~r7, acc, b, dph, dpl, psw, bp)及目前的 sp 位置,然後變更到下一個 task 的 sp,再還原所有的 register,最後 return 到新 task 的位置(在 rtos_start 時已經記錄到 堆疊里)。
- bp 這個東東應該不算 register,這個主要是被用在 reentrant 函數,一個很特殊的全域變數,忘了保存它還會造成程式錯亂喔。
執行結果
注意事項
- 由於是多工的系統,所以對於 SDCC 內建的 library 的使用要求就是要有 --stack-auto 的選項。而內建的 library 只有 small-stack-auto,所以要在 sdcc-eclipse 的 SDCC Linker Command 選項加入 sdcc --model-small --stack-auto 才會選用 small-stack-auto library。
- 至於 SDCC Compiler Command 要不要加 --stack-auto 都可以,如果沒有加入此參數,則需自己判斷是否為可重入函數,在函數宣告後面加入 __reentrant 關鍵字。
- 為了避免 task 之間的 local variable 因為 overlay 的關係互相干擾,所以要在 sdcc-eclipse 的 SDCC Compiler Command 選項加入 --nooverlay 參數。
- PUSH 指令是先將 SP 加一,再將直接定址資料放入堆疊空間裏。相反的,POP/RET 指令是先把堆疊空間資料取出放到直接定址空間,再將 SP 減一。
- 在 tiny rots 內不要使用 printf,儘可能使用 printf_tiny 代替。因為 printf 會用到 bit variable,造成 stack 空間減少。
- 確定會被不同 bank 呼叫的函數才需要加 __banked,否則不用浪費堆疊空間去記錄相同的 PSBANK 值。
- 一般的初始函數可以放在 bank0,而且也不用加 __banked,這樣可以把 common area 縮小。
- common area 越小,相對的每個 bank 可用的空間越大。
- __reentrant 函數的建立要視函數內的變數是否被轉換成 idata 變數,如果區域變數都是使用 register,那麼就不需要加 __reentrant,加了還可以能浪費到 stack 空間。
- 要減少函數用到 idata 變數,可以試著關掉編譯器的最佳化功能,比如 #pragma nogcse 等。
- ...
到這裏應該算是一個總結了,後面有什麼功能,再慢慢來想吧。這次主要是加入了 rtos delay 函數,讓 delay 更為精準,也讓空出來的時間,給其它 task 去執行,並且加入了 rtos_idle 函數,利用 cpu idle 功能達到省電功能,當然如果你有用到 PWM 時,記得把這個功能換掉,免得 PWM 不動了。
主要加入的功能
- rtos_idle 函數主要是在所有 task 都在 wait 時要執行的函數。由於 rtos_idle 沒有用到任何的堆疊空間,所以在計算每一個 task 的堆疊空間時,把 idle task 的空間直接定義成 17 bytes,就造成 size = (256 - SP - 17) / MAX_TASKS;,而非 size = (256 - SP) / (MAX_TASKS + 1);。
- rtos_nexttask 函數主要是用來做切換任務的工作,並根據 rtos_tick 決定是否要執行。
- rtos_isr 函數中加入了計時功能(以 10ms 為一個單位),並把 nexttask 功能分離出來,並加上 RETI 把後面的 POP 擋掉。
- rtos_scheduling 函數主要是用在非中斷環境下執行 nexttask 功能。
- rtos_delay_10ms 函數主要是用來做時間延時用的,每 10ms 為一單位。
程式執行結果為 0.5 秒顯示數字,1 秒顯示字母,也就是看到 1a23b45c67d89e12f34g56h78i9 如此循環顯示的字串。
這次修改的內容,主要是加入 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 過程中,又被動了一些手腳,造成程式莫名奇妙的出錯吧。
這次主要修改的內容
- 原本 rtos_isr 函數維持加入部份 assembly code,不要全部改成 assembly code。
- ?C_IBP 儲存方式改成放入堆疊裏,以利精簡程式碼,減少 interrupt 執行時間。
- 原本的 void (* const task_func[MAX_TASKS])(void) 改成 unsigned int code task_func[MAX_TASKS],將函數指標改成一般變數放在 rom 裏,如此可以節省記憶體用量,也精簡程式碼。
- 為了避免 LCALL rtos_start 變成 LJMP rtos_start 問題,直接改變 SP 的初始化過程,藉由修改 STARTUP.A51 來增加 STACK 位址的讀取。
- 不可以將 STARTUP.A51 中的 IBPSTACK 設成 1,原來使用 reentrant 函數都需要將 IBPSTACK 設成 1,但是不知什麼原因,在用 IspWriter 燒錄完後的自動重啟執行都會出現怪怪的執行結果。
經過反覆測試,發現將 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,如下所示。
但是結果卻不如預期,跑出來的東西都是亂碼,傻眼。最後,經過了二天的努力,總算把結果正確無誤的弄出來了。主要有下列幾個地方要修改。
- 把 C51 的 Code Optimization Level 降成 1:Dead code elimination。因為 Data overlaying 的關係,造成 task1 的 i 變數跟 task2 的 j 變數共用同樣的位址,所以讓印出來的值都在 1~9 跳動。
- 原本 data overlaying 問題想要利用 reentrant 指令來建立可重入函數去解決,但最後發現仍然是無效。因為 keil 把變數都放在一個 ?C_IBP 的位址,仍然會造成共用的問題,這個問題真是難解啊,以後如果要寫複雜的 multi-task 函數有一定的難度,必須克服可重入函數及 data overlaying 造成的問題。
- 在 rtos_isr 函數裏加入 push 及 pop 指令,把 R0~R7,ACC,B,PSW,DPH,DPL 都丟到 stack 裏去。因為 keil 都會利用這些暫存器做一些運算,在進入 interrupt 時,也會主動 push 一些在 interrupt 會用到的暫存器到 stack 裏,但其它沒用到的暫存器就沒有主動 push,造成切換到其它 task 再切回原來 task 時,那些暫存器裏的值都被變更了,而造成運算錯誤。
- 另外在加入 assembly code 時,需把 Generate Assembler SRC File 及 Assemble SRC File 都打勾才可以,缺一不可。
- 利用 timer 中斷來取得。
- 利用 while loop 來取得,但是要算出 while loop 的數值,通常使用示波器來輔助求得。
計算過程
- 首先 delay_1ms 中的 i 值先給 100,燒進去執行。
- 取得 TH0, TL0,利用 timer 公式 (TH0*256+TL0)*12/Fosc,計算出 100 次要花多少 t 時間。
- 再利用公式 t/i0 = 1ms/i1,i1=1ms*i0/t,取得下一次要填入的 i1 值。
- 再次重覆計算,直到 i 值不變或 t>1ms 為止。
範例:W79L632A@40MHz
- TH0=2, TL0=75, t=176.1 us, i1=567.8591 取 568。
- TH0=12, TL0=168, t=972 us, i1=584.3621 取 584。
- TH0=13, TL0=3, t=999.3 us, i1=584.4090 取 585。
- TH0=13, TL0=8, t=1000.8 us, i 值設定成 585。
- 同樣的硬體,同樣的 source code,使用 keil C51,計算出來的值卻是 711,由此可見 keil C51 跑得比較快??
- 最近又從別人的 code 發現,其實在 i=CNT 前加入 nop 指令可以用來增加準確度到 1us 喔,當然 nop 數量的多寡要經由測試才能得知,有興趣的人自己試看看吧。
測試誤差
Keil-C 怪怪的問題
最近玩 mstar 的晶片,改用 Keil C 來測這個程式,結果發現一個怪異現象,就是每次 printf 出來的 TH0 都是 16bit 的數值,而 TL0 卻總是 0,time,cnt 算出的值也不對,真是莫明奇怪。後來發現把 TH0, TL0 放入 int 變數就正常了,如果放入 char 變數也是不對。由此證明 keil 在 char 變數的四則運算有點問題喔。下面這支程式的 baudrate 是 38400 使用 40MHz 的震盪器。主要功能就是你輸入什麼,就顯示什麼。依此下列程式再加上 SDCC 裏的 printf 就可以用來做 debug 輸出功能了,短小精幹好用。
PS: 由於沒有使用到 ES 中斷,所以可以很放心的在中斷函數中使用 printf 來輸出訊息。
PS: 因為我不會用 8051 的模擬器,所以都用 printf 在做 debug,有人說這樣不方便,其實用久習慣就好了。不管黑貓白貓,可以抓到老鼠就是好貓。
原始碼下載
全域變數
- char g_cmd_line[SZ_CMD_LINE+1];
記錄指令輸入的暫存字串,SZ_CMD_LINE 不可大於 128。 - char g_position;
記錄游標位置。 - char g_ansi_digit;
記錄 ANSI escape code 的數字部位。 - bit g_ansi_mode;
記錄是否進入 ANSI escape sequence 解析模式。 - char (*fp_parse_ansi)(char escape);
記錄使用 ANSI 解析的函數。
console init
- 清除營幕所有東西,並回到左上角的位置,最後顯示提示字元。
console routine process
- 讀取輸入字元。
- 進入 ANSI escape code parse 或解析輸入字元。
- '\r': 輸入完成,解析輸入字串。
- 0x07: beep char,輸出錯誤嗚叫聲。
- '\t': 忽略定位字元。
- '\b', 0x7f: 刪除字元。
- '\033': 進入 ANSI escape sequence mode。
- default: 記錄字元並顯示在營幕上。
console parse
- 取得 command id,並依 command id 執行相關程式。
parse command
- 取得 command id,並回傳下一個參數字串指標。
next string
- 取得參數字串並補上 \0 字元,並回傳下一個參數字串指標。
clear space
- 清除空白字元,並回傳參數字串指標。
parse ansi code
- 解析 ANSI escape code 第一個字元。
parse bracket
- 解析 ANSI escape code 第二個字元。
parse bracket
- 解析 ANSI escape code 第二個字元,這一個函數是針對 windows 的超級終端機所做的,標準 ANSI escape code 好像沒有。
parse bracket
- 解析 ANSI escape code 數字。
