tiny rtos for 8051 using SDCC part 4

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

原始碼下載

前言

由於 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。

SDCC Bankswitching (code banking) part 2

Posted by: 邱小新 at 下午3:08 in ,
範例下載,使用 ip210s,uart output "3210+dcba"。

前言

最近在玩 IP210S 的晶片,該顆 8051 使用外掛的 flash(SST 39VF040),有 512KB 之大,總共有 19 條 address line。之前寫的 SDCC Bankswitching 太簡略了,所以又提筆補充了一下。

  1. ip210s 對於 512K flash 的 address line 是放在 P1.7(18),P3.4(17),P3.5(16)。所以只要對該三個 GPIO 做設定,就可以讓 ip210s 定址到 512KB。
  2. 每個 bank 的起始位址設定指令為 "-Wl-b BANK1=0x18000",BANK1 為 #pragma codeseg 的名稱,0x18000 為該 bank 的起始位址。以 IP210S 為例,512/64=8,所以必須切成 8 banks。"-Wl-b BANK0=0x8000","-Wl-b BANK1=0x18000","-Wl-b BANK2=0x28000","-Wl-b BANK3=0x38000","-Wl-b BANK4=0x48000","-Wl-b BANK5=0x58000","-Wl-b BANK6=0x68000","-Wl-b BANK7=0x78000"。
  3. #pragma codeseg 的使用只對在它宣告下面的程式碼有用,以上則無效,因為內定值是不同的,所以才需用 #pragma codeseg 來變更。
  4. 在寫程式時,通常伴隨著一些存放在 code 的變數,如 code char str[] = "hello test\n\r";,一般而言,它們存放的區域也是放在 common 區,如果也把它們放在 bank 裏,則需宣告 #pragma constseg,如此 SDCC 才會把 code 變數也放到 bank 裏。
  5. SDCC 編譯出來的 hex 檔,包含了 common 及所有的 bank 程式,經由 hex2bin 的轉換後,只有 bank0 包含 common 程式碼,其它 bank 都沒有,只能利用 ultraedit 自己剪貼搞定。
  6. 使用 SDCC Bankswitching 需要在 main.c 宣告一個變數 unsigned char PSBANK=0;,並要先初始化為 0,這個 PSBANK 主要是存放目前的 bank 值。

__sdcc_banked_call

  1. 一般函數呼叫都是用 lcall _test1,但是在呼叫 banked 函數時,則是會先呼叫 __sdcc_banked_call,並把 banked 函數位址放到 r0~r2 當參數傳遞。
  2. 當要呼叫位於 bank? 區段的函數時,會把讓函數的位址放到 r0~r2,由於在 bank? 區段的函數位址包含了最高的 16~18 條位址線,所以會把 r2 當做 bank 的選擇,實際上 r2 只是代表 address line 的 16~23 位址線而已。由此可知,SDCC 的 Bankswitching 可以達到 224,也就 16M bytes。
  3. 一開始需先把目前的 bank 值(PSBANK)推入堆疊,以便離開時還原 bank 值。
  4. 由於函數可能利用 acc 來傳遞參數,所以必須先把 acc 值保存,xch a,r0 就是把 acc 保存在 r0。
  5. 接著把呼叫函數的位址推入堆疊,以便等一下 ret 時跳到該函數位址。
  6. 接著就是判斷 r2 值來切換位址線,我只會最笨的方法,貽笑大方了。
  7. 然後還原 acc 值,並用 ret 返回呼叫函數。
;../source/test2.c:19: test1(); mov r0,#_test1 mov r1,#(_test1 >> 8) mov r2,#(_test1 >> 16) lcall __sdcc_banked_call __sdcc_banked_call:: push _PSBANK ;save return bank xch a,r0 ;save Acc in r0, do not assume any register bank push acc ;push LSB address mov a,r1 push acc ;push MSB address ; set new bank mov a,r2 ;get new bank mov _PSBANK,acc cjne a,#0x07,call_bank6 setb _P1_7 setb _P3_4 setb _P3_5 sjmp call_end call_bank6: ;..... 略 ...... call_bank0: clr _P1_7 clr _P3_4 clr _P3_5 call_end: xch a,r0 ;restore Acc ret ;make the call

__sdcc_banked_ret

  1. 一般函數結束時都是直接執行 ret 回到上一層,但是在 banked 函數結束時,則是會呼叫 __sdcc_banked_ret,並經由 __sdcc_banked_ret 返回上一層。
  2. 一開始需先把備份的 bank 值(PSBANK)從堆疊取出,以便等會還原 address line。
  3. 由於函數可能利用 acc 來傳遞返回值,所以必須先把 acc 值保存,xch a,r0 就是把 acc 保存在 r0。
  4. 接著就是判斷 PSBANK 值來切換位址線,我只會最笨的方法,貽笑大方了。
  5. 然後還原 acc 值,並用 ret 返回上一層函數。
__sdcc_banked_ret:: pop _PSBANK ;restore bank xch a,r0 ;save Acc in r0, do not assume any register bank mov a,_PSBANK cjne a,#0x07,ret_bank6 setb _P1_7 setb _P3_4 setb _P3_5 sjmp ret_end ret_bank6: ;...... 略 ....... ret_bank0: clr _P1_7 clr _P3_4 clr _P3_5 ret_end: xch a,r0 ;restore Acc ret ;return to caller

tiny rtos for 8051 using SDCC part 3

Posted by: 邱小新 at 上午11:46 in , ,

原始碼下載

前言

在前面一個 tiny rtos 裏只是簡單平均分配時間到每一個 task 裏,但這有一個缺點,就是當某一個 task 需要暫停一下時,無法把時間交給其它的 task,這個時間無形間就被浪費掉了。所以在這一次新的 task 裏,加入了 rtos_delay_10ms 函數,讓 task 可以暫停一段時間,並把時間讓出來給別的 task。

  1. 把 timer0 的時間都切成 10ms,每一個 task 的運行時間都變成 10ms。
  2. 多了一個 rtos_tick 變數做 delay 的計數,以 10ms 為單位。

rtos_start

  1. 原本儲放 stack 的變數多了一個,用來放置 rtos_idle 的 stack。
  2. 第一個執行的 task 改成 rtos_idle,而非 task1。
  3. 由於 rtos_idle 沒做什麼事,所以只留一份必要的 stack 空間給它,其它的都分給其它 task。
  4. 不可以在此函數裏呼叫任何函數,因為呼叫函數會變更 stack 裏的內容(有 push 動作),這樣會把 task1 的 stack 破壞,造成進入 task1 時位置被變更,而跑不到 task1。
  5. 如果按照之前的方式,直接跳到 task1 執行,而不是從 rtos_idle 執行,就可以在此函數內呼叫其它函數,因為 stack 被破壞並不會影嚮 task1 的執行。
#define PUSH_SIZE 14 void rtos_start(void) { unsigned char i, size; __idata unsigned char *sp; // init stack size = (255 - SP - PUSH_SIZE - 2) >> 2; sp = (__idata unsigned char *)SP; for (i=0; i<MAX_TASKS; i++) { rtos_tick[i] = 0; *++sp = (unsigned int)task_func[i] & 0xff; *++sp = (unsigned int)task_func[i] >> 8; rtos_stack[i] = (unsigned char)sp + PUSH_SIZE; sp = sp + size - 2; } rtos_stack[MAX_TASKS] = (unsigned char)sp; // variable init rtos_task_id = MAX_TASKS; SP = rtos_stack[MAX_TASKS]; // timer0 init TMOD = (TMOD & 0xF0) | 0x01; TH0 = 0; TL0 = 0; TF0 = LOW; ET0 = HIGH; TR0 = HIGH; // jump to idle task rtos_idle(); }

rtos_idle

  1. rtos_idle 裏其實不沒做什麼事,就是一直進入 idle 模式以利省電。
  2. rtos_idle 裏不可以呼叫任何函數,因為保留給它的 stack 空間只夠做 task 切換而已,容不下其它的 push 指令。
static void rtos_idle(void) { while (1) { PCON |= 1; } }

rtos_nexttask

  1. rtos_nexttask 新增了計數功能,用來計算每一個 task delay 時間,當 rtos_tick==0 時,該 task 才擁有執行時間。
  2. 如果所有 task 都處於閒置狀態,就進入 idle task 等待其它 task 被喚醒。
  3. rtos_nexttask 裏不可以呼叫任何函數,因為如果遇到從 rots_idle 進入的 stack,它的 stack 空間只夠做 task 切換而已,容不下其它的 push 指令。
void rtos_nexttask(void) __naked { unsigned char i; _asm clr ea _endasm; // save current stack rtos_stack[rtos_task_id] = SP; // check tick for (i=0; i<MAX_TASKS; i++) { if (rtos_tick[i] > 0) rtos_tick[i]--; } // load next stack for (i=0; i<MAX_TASKS; i++) { if (++rtos_task_id >= MAX_TASKS) rtos_task_id = 0; if (rtos_tick[rtos_task_id] == 0) break; } if (i == MAX_TASKS) rtos_task_id = MAX_TASKS; SP = rtos_stack[rtos_task_id]; // restore register _asm pop _bp pop ar7 pop ar6 pop ar5 pop ar4 pop ar3 pop ar2 pop ar1 pop ar0 pop psw pop dpl pop dph pop b pop acc setb ea ret _endasm; }

執行結果

ABCDEFGHIJKLMNOPabcdefghijklmnopqrstuvwxyzQRSTUVWXYZ0123456789(0)SP = '1A' (1)SP = '53' (2)SP = '8C' (3)SP = 'C5' (4)SP = 'FE' abcdefghijklmnopABCDEFGHIJKLMNOPQRSTUVWXYZqrstuvwxyz0123456789(0)SP = '1A' (1)SP = '53' (2)SP = '8C' (3)SP = 'C5' (4)SP = 'FE' ABCDEFGHIJKLMNOPabcdefghijklmnopqrstuvwxyzQRSTUVWXYZ0123456789(0)SP = '1A' (1)SP = '53' (2)SP = '8C' (3)SP = 'C5' (4)SP = 'FE'

extern unsigned char *rtos_stack 跟 extern unsigned char rtos_stack[] 的差異

Posted by: 邱小新 at 上午11:37 in

extern __xdata unsigned char *rtos_stack;

;../source/test.c:66: printf_tiny("(%d)SP = '%x'\n\r", i, rtos_stack[i]); mov a,r2 add a,_rtos_stack mov dpl,a clr a addc a,(_rtos_stack + 1) mov dph,a movx a,@dptr

extern __xdata unsigned char rtos_stack[];

;../source/test.c:66: printf_tiny("(%d)SP = '%x'\n\r", i, rtos_stack[i]); mov a,r2 add a,#_rtos_stack mov dpl,a clr a addc a,#(_rtos_stack >> 8) mov dph,a movx a,@dptr

原來的變數為 __xdata unsigned char rtos_stack[3],很明顯的在 SDCC 裏 (char *) 不能等同於 (char []),這點要特別注意。

tiny rtos for 8051 using SDCC part 2

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

原始碼下載

前言

使用 SDCC 來寫 rtos 比起用 keil-c 來得方便許多,因為 SDCC 有一個 naked 修飾元。naked function 會省略前置的 push 動作及最後的 pop 及 ret 指令,這些動作都需要使用者自己加入。在 sdccman.pdf 內的說明指出 naked function 主要是用於 interrupt function 來省略一堆無意義的 push/pop 動作,剛好被我用來增加 push/pop 動作(完全相反的立意)。

rtos_start

  1. 其實我寫的 rtos 很簡單,也就是把 stack 分成四份,配給四個 task,讓每個 task 的 stack 各自獨立不互相干擾。
  2. 再利用 timer 0 的中斷處理來切換 task。
  3. rtos_task_id 是記錄目前正在執行的 task 是那一個。
  4. rtos_stack 是記錄每一個 task 的 stack 位置。
  5. 由於在切換 task 時,會 pop 14 個 register 及 task 的執行位置。所以一開始需要把每個 task 的起始位置 push 到堆疊里,並把 stack 起始位置加上 14,在做切換 task 時,才會順利執行相應的函數。
  6. 由於第一個 task 就是主程序,所以不用做 push 動作,因為在第一次切換 task 時,會自動 push,因此才會有 rtos_stack[0] = SP; 的指令。
  7. call task1() 及 call rtos_start() 理論上會浪費 stack 空間才對,但因為被 SDCC 最佳化的關係,二個指令都變成 ljmp task1 及 ljmp rtos_start,並沒有浪費到 stack 空間,請安心使用。
#define PUSH_SIZE 14 void rtos_start(void) { unsigned char i, size; __idata unsigned char *sp; // variable init rtos_task_id = 0; // init stack size = (255 - SP) >> 2; // size = (256 - SP) / MAX_TASKS; sp = (__idata unsigned char *)SP; for (i=0; i<MAX_TASKS; i++) { *++sp = (unsigned int)task_func[i] & 0xff; *++sp = (unsigned int)task_func[i] >> 8; rtos_stack[i] = (unsigned char)sp + PUSH_SIZE; sp = sp + size - 2; } rtos_stack[0] = SP; // timer0 init TMOD = (TMOD & 0xF0) | 0x01; TH0 = 0; TL0 = 0; TF0 = LOW; ET0 = HIGH; TR0 = HIGH; // jump to task 1 task1(); }

rtos_nexttask

  1. rtos_nexttask 的動作很簡單,就是保存現在所有的 register (r0~r7, acc, b, dph, dpl, psw, bp)及目前的 sp 位置,然後變更到下一個 task 的 sp,再還原所有的 register,最後 return 到新 task 的位置(在 rtos_start 時已經記錄到 堆疊里)。
  2. bp 這個東東應該不算 register,這個主要是被用在 reentrant 函數,一個很特殊的全域變數,忘了保存它還會造成程式錯亂喔。
void rtos_nexttask(void) __naked { // 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]; // restore register _asm pop _bp pop ar7 pop ar6 pop ar5 pop ar4 pop ar3 pop ar2 pop ar1 pop ar0 pop psw pop dpl pop dph pop b pop acc ret _endasm; } void rtos_isr(void) __interrupt (1) __naked { // store register _asm push acc push b push dph push dpl push psw push ar0 push ar1 push ar2 push ar3 push ar4 push ar5 push ar6 push ar7 push _bp _endasm; // call rtos_nexttask *(__idata unsigned char *)++SP = (unsigned int)&rtos_nexttask & 0xff; *(__idata unsigned char *)++SP = (unsigned int)&rtos_nexttask >> 8; _asm reti _endasm; }

執行結果

567890123456789012345678901234567890123456SP = 1D rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgSP = 5A QRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGSP = 97 [\]^_`[\]^_`[\]^_`[\]^_`[\]^_`[\]^_`[\]^_`SP = D4 789012345678901234567890123456789012345678SP = 1D hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwSP = 5A HIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWSP = 97 [\]^_`[\]^_`[\]^_`[\]^_`[\]^_`[\]^_`[\]^_`[SP = D4

tiny rtos for 8051 using SDCC part 1

Posted by: 邱小新 at 下午3:49 in ,

注意事項

  1. 由於是多工的系統,所以對於 SDCC 內建的 library 的使用要求就是要有 --stack-auto 的選項。而內建的 library 只有 small-stack-auto,所以要在 sdcc-eclipse 的 SDCC Linker Command 選項加入 sdcc --model-small --stack-auto 才會選用 small-stack-auto library。
  2. 至於 SDCC Compiler Command 要不要加 --stack-auto 都可以,如果沒有加入此參數,則需自己判斷是否為可重入函數,在函數宣告後面加入 __reentrant 關鍵字。
  3. 為了避免 task 之間的 local variable 因為 overlay 的關係互相干擾,所以要在 sdcc-eclipse 的 SDCC Compiler Command 選項加入 --nooverlay 參數。
  4. PUSH 指令是先將 SP 加一,再將直接定址資料放入堆疊空間裏。相反的,POP/RET 指令是先把堆疊空間資料取出放到直接定址空間,再將 SP 減一。
  5. 在 tiny rots 內不要使用 printf,儘可能使用 printf_tiny 代替。因為 printf 會用到 bit variable,造成 stack 空間減少。
  6. 確定會被不同 bank 呼叫的函數才需要加 __banked,否則不用浪費堆疊空間去記錄相同的 PSBANK 值。
  7. 一般的初始函數可以放在 bank0,而且也不用加 __banked,這樣可以把 common area 縮小。
  8. common area 越小,相對的每個 bank 可用的空間越大。
  9. __reentrant 函數的建立要視函數內的變數是否被轉換成 idata 變數,如果區域變數都是使用 register,那麼就不需要加 __reentrant,加了還可以能浪費到 stack 空間。
  10. 要減少函數用到 idata 變數,可以試著關掉編譯器的最佳化功能,比如 #pragma nogcse 等。
  11. ...

reentrant function with _bp

Posted by: 邱小新 at 下午4:52 in

由於 8051 單晶片內部的堆疊空間有限,所以無法像 windows/unix 那樣使用堆疊。一般在 C 語言裏,呼叫函數時會將函數參數及函數使用的局部變數放入堆疊。因為 8051 堆疊空間有限,所以無法使用此方式,而是為每個函數的局部變數及參數設定一個空間來存放。比如下面的範例就是如此,參數 a 及 b 被分配到二個全域變數 _test_a_1_1 及 _test_PARM_2。

541 ;------------------------------------------------------------ 542 ;Allocation info for local variables in function 'test' 543 ;------------------------------------------------------------ 544 ;b Allocated with name '_test_PARM_2' 545 ;a Allocated with name '_test_a_1_1' 546 ;------------------------------------------------------------ 547 ; ../source/main.c:60: int test(int a, int b) 548 ; ----------------------------------------- 549 ; function test 550 ; ----------------------------------------- 551 _test: 552 push ar2 553 push ar3 554 push ar4 555 push ar5 556 mov r2,dph 557 mov a,dpl 558 mov dptr,#_test_a_1_1 559 movx @dptr,a 560 inc dptr 561 mov a,r2 562 movx @dptr,a 563 ; ../source/main.c:62: return a + b; 564 mov dptr,#_test_PARM_2 565 movx a,@dptr 566 mov r2,a 567 inc dptr 568 movx a,@dptr 569 mov r3,a 570 mov dptr,#_test_a_1_1 571 movx a,@dptr 572 mov r4,a 573 inc dptr 574 movx a,@dptr 575 mov r5,a 576 mov a,r2 577 add a,r4 578 mov r2,a 579 mov a,r3 580 addc a,r5 581 mov r3,a 582 mov dpl,r2 583 mov dph,r3 584 pop ar5 585 pop ar4 586 pop ar3 587 pop ar2 588 ret

呼叫方式也是直接把傳遞值直接存入全域變數 _test_PARM_2 裏。

607 ; ../source/main.c:74: test(1, 2); 608 mov dptr,#_test_PARM_2 609 mov a,#0x02 610 movx @dptr,a 611 clr a 612 inc dptr 613 movx @dptr,a 614 mov dptr,#0x0001 615 lcall _test

SDCC 把局部變數被放到全域變數裏,相當於被宣告成靜態變數一樣,只是差在沒宣告 static 而已。由於此種原因,造成此類函數變成不可當成遞迴函數來使用,也就是不能自我呼叫函數。而且在 interrupt function 裏不可以呼叫此類函數,因為當中斷發生在此類函數時,interrupt function 又呼叫一次原來函數,會造成局部變數被修改,而造成不可預期的錯誤。所以需要在函數宣告後加上 __reentrant,讓編譯器按照一般 C 語言方式把局部變數都放到堆疊裏。下面的函數就是如此,把局部變數放到堆疊裏(sp-4)。

536 ;------------------------------------------------------------ 537 ;Allocation info for local variables in function 'test' 538 ;------------------------------------------------------------ 539 ;b Allocated to stack - offset -4 540 ;a Allocated to registers r2 r3 541 ;------------------------------------------------------------ 542 ; ../source/main.c:60: int test(int a, int b) __reentrant 543 ; ----------------------------------------- 544 ; function test 545 ; ----------------------------------------- 546 _test: 547 push ar2 548 push ar3 549 push ar0 550 push ar1 551 push _bp 552 mov _bp,sp 553 mov r2,dpl 554 mov r3,dph 555 ; ../source/main.c:62: return a + b; 556 mov a,_bp 557 add a,#0xf8 558 mov r0,a 559 mov a,@r0 560 add a,r2 561 mov r2,a 562 inc r0 563 mov a,@r0 564 addc a,r3 565 mov r3,a 566 mov dpl,r2 567 mov dph,r3 568 pop _bp 569 pop ar1 570 pop ar0 571 pop ar3 572 pop ar2 573 ret

呼叫時,直接把傳遞值放入堆疊裏,而且在呼叫結束也會還原堆疊指針。

592 ; ../source/main.c:74: test(1, 2); 593 mov a,#0x02 594 push acc 595 clr a 596 push acc 597 mov dptr,#0x0001 598 lcall _test 599 dec sp 600 dec sp

各位有沒有在 reentrant function 裏發現一個不名的變數 _bp,用來做堆疊指標的運算,以取得參數的值。而這個 _bp 變數是宣告在 SDCC\lib\src\_bp.c --> __data unsigned char bp;。而且這個變數還被 printf_tiny 及 printf_fast 所使用,當然這二個函數有宣告成 __reentrant 函數,使用 _bp 變數也只是變相利用而已啦。