SDCC reentrant library

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

在 mutli-task 或是 interrupt 中想要使用長整數乘除法是不可以的。因為長整數乘除法會用到額外的全域變數 _mulint_PARM_2,如果同時有二個 task 同時用到長整數乘除法時,就會造成錯亂。

這時候就需要加入 --stack-auto 參數,讓所有編譯都搞成 reentrant 函數,這樣就可以避免使用全域變數的問題。

雖然使用 --stack-auto 可以呼叫 reentrant libray,但是也把使用者的函數全都編譯成 reentrant function 了。這樣雖然沒有什麼大問題,但是在 tiny rtos 因為 stack 空間不大,使用太多 reentrant function 有可能會讓 stack 爆掉。所以只在 SDCC Linker 地方加入 --stack-auto 即可,不要在 SDCC Compiler 加。

奇怪的是卻出現了,在 SDCC Linker 時會有錯誤訊息 ?ASlink-Warning-Undefined Global '__mulint_PARM_2' referenced by module 'ip210s_net';原來在 SDCC Linker 加上 --stack-auto 是不夠的,此時需在 SDCC Compiler 加入 --int-long-reent 才能正確無誤使用長整數乘除法。

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 變數也只是變相利用而已啦。

當指標遇上結構

Posted by: 邱小新 at 上午10:50 in ,

請注意下列二行程式碼有啥不同?

1. (__xdata unsigned char *)(icmp_data_rx+sizeof(struct icmp_hdr)) 2. ((__xdata unsigned char *)icmp_data_rx)+sizeof(struct icmp_hdr) 3. (__xdata unsigned char *)icmp_data_rx+sizeof(struct icmp_hdr)

沒錯,就是括弧位置不同,但是請看下列編譯出來的組合碼。

../source/icmp.c:54: (__xdata unsigned char *)(icmp_data_rx+sizeof(struct icmp_hdr)), mov dptr,#_icmp_data_rx movx a,@dptr mov r4,a inc dptr movx a,@dptr mov r5,a mov a,#0x40 ; 不同處 add a,r4 mov _memcpy_PARM_2,a clr a addc a,r5 ../source/icmp.c:54: ((__xdata unsigned char *)icmp_data_rx)+sizeof(struct icmp_hdr), mov dptr,#_icmp_data_rx movx a,@dptr mov r4,a inc dptr movx a,@dptr mov r5,a mov a,#0x08 ; 不同處 add a,r4 mov _memcpy_PARM_2,a clr a addc a,r5

注意到那不同處的地方了吧,二者算出來的值卻相差十萬八千里,其實在指標使用時,這是一個很容易犯錯的地方。當你原本只是要取一個結構結尾的指標,很自然的會寫出 1 的程式碼,但那是錯誤的。因為指標加法是根據指標的變數型態去相加,比如一個 short 指標加一,是指向下一個 short 位置,也就是指數加二。而在此案例上,第一種寫法是指向第8個結構的位置,也就 8x8=64=0x40 的位置,實在是大錯特錯。正確寫法為第2,3個寫法。

二的補數 (電腦負數表示法)

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

二的補數法(2's complement)是電腦中為了儲存及運算有正負號的整數而使用的一種方法,要用二進位表達一個負整數的方法很多,但是二的補數表示法使得負整數與正整數的運算(加、減、及乘法)完全一致,不需要額外的硬體來處理。

範例

以 4 位元二的補數數字來說。例如:1 + (-2) = -1,0001(1) + 1110(-2) = 1111(-1)。你可以發現二的補數的加法只要使用標準的二進位加法就可以了。

2 進位無號整數有號整數
111115-1
111014-2
110113-3
110012-4
101111-5
101010-6
10019-7
10008-8
011177
011066
010155
010044
001133
001022
000111
000000

十進制轉成二進制

以 -3 為例。

  1. 11: 先把數字部分轉換成二進制。
  2. 0011: 依照 bit 數,補齊空缺。
  3. 1100: 使用1的補數,將1變0,0變1。
  4. 1101: 再把1的補數加上1,形成2的補數。

二進制轉成十進制

以 1101 為例。

  1. 1100: 2的補數減1,還原成1的補數。
  2. 0011: 1變0,0變1。
  3. 3: 二進制轉成十進制。
  4. -3: 數字前加上負號。

while (i--) or while (--i)

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

while (i--)

mov r2,#0xC8 mov ar3, r2 dec r2 mov ar6, r2 mov a, r3

while (--i)

mov r2,#0xC8 dec r2 mov a, r2

結論

這樣結果很明顯吧,i-- 比 --i 多用了一個 r6 register,而且也多了二個指令,下次記得寫 while (--i)。

運算子優先順序

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

最近在寫一段程式碼,剛好有去注意它編譯出來的assembly code,發現怎麼沒有預期出來的結果。結果發現卻是運算子優先順序的問題。切記位元運算的優先權為 shift > and > xor > or,之前一直以為三者是一樣大,原來卻不是如此。希望以前寫的產品不要因為如此而出問題啊,阿彌陀佛,善哉善哉。

原始錯誤程式碼 tmp = *(volatile unsigned char __xdata *)(0x8001) | 0x10 & ~0x08; mov dptr,#0x8001 movx a,@dptr mov r2,a orl ar2,#0x10 修改正確程式碼 tmp = (*(volatile unsigned char __xdata *)(0x8001) | 0x10) & ~0x08; mov dptr,#0x8001 movx a,@dptr mov r2,a mov a,#0x10 orl a,r2 anl a,#0xF7

運算子優先順序

優先
順序
評估順序運算子說明

15

由左至右

., [], ()

欄位存取、陣列索引、函式呼叫和運算式群組

14

由右至左

++, --, -, ~, !, delete, new, typeof, void

一元運算子、傳回資料型別、物件建立、未定義的值

13

由左至右

*, /, %

乘法、除法、modulo 除法

12

由左至右

+, -

加法和字串串連、減法

11

由左至右

<<, >>, >>>

位元移位

10

由左至右

<, <=, >, >=, instanceof

小於、小於或等於、大於、大於或等於、instanceof

9

由左至右

==, !=, ===, !==

等號比較、不等比較、嚴格等號比較和嚴格不等比較

8

由左至右

&

位元 AND

7

由左至右

^

位元 XOR

6

由左至右

|

位元 OR

5

由左至右

&&

邏輯 AND

4

由左至右

||

邏輯 OR

3

由右至左

?:

條件式

2

由右至左

=, OP=

指派、複合指派

1

由左至右

, (逗號)

多重評估

IP210S external flash (code banking)

Posted by: 邱小新 at 下午3:22 in

最近又接了一份工作,是搞 IP210S 的案子,上頭想在 LCD 裏內建網路,可以讓使用者透過網路去控制 LCD 功能,還有能從網路更新軔體;好像是要仿造 NEC 的軟體功能。

拿到 IP210S 的 source code,看了一下,主要的函數全被封裝在 library 裏了,真不知為啥要簽 NDA,都被包起來了,有意義嗎?

今天主要是來記錄 code banking 的用法,IP210S 內含一顆 8052 的 mcu,ROM 是採用外部 flash,SST 39VF040 512KB。原本 IP210S 只有 16 條的 address line,也就是只能使用 64KB,剛好也是 8052 最大的定址位址。

那 512KB 要怎麼使用?當然就是切成八塊 64KB,使用 Keil-C 的 code banking 功能,硬體也很簡單,拉三條 GPIO 到 39VF040 的 A16~A18 即可。而 L51_BANK.A51 改成使用 mode 4,自己寫 bank switch code 即可。bank 0 就三個 GPIO 設成 0,bank 1 就 A16 那個設成 1,以此類推,夠簡單吧。

之前只使用過 winbond W79E632 的 code banking,而且還是 internal ROM,使用 mode 1,一直搞不清楚 mode 4 會用在那里。現在有了範例,就更清楚為啥要分那麼多種模式了。學 8051 還真是沒啥捷徑,真是要多看多學,才能累積經驗值。

S35390A driver for 8051 part 2

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

● 警報中斷 alarm interrupt

  1. alarm 的設置很簡單,只要先設置好警報時間,再設置中斷模式即可。
  2. 中斷模式有很多種,主要使用 alarm interrupt 模式,即 INT1AE=1,INT1ME=0,INT1FE=0。
  3. 當警報觸發時,INT1 pin 會從 HIGH 變成 LOW,直到設置 INT1AE=0,INT1 pin 才又恢復成 HIGH。
  4. 當警報觸發時,register status1 中的 INT1 會變成 1,只要讀取 status1 後自動變成 0,即下次再讀取 INT1 的值為 0。
  5. 當警報觸發時,而且 INT1AE=1,每隔一秒就會重置 status1(INT1=1)。也就是讀取完 status1 後,再次讀取 INT1 的值為 0,但是在下一秒再次讀取 status1,則 INT1 又會變成 1,每隔一秒就再次變成 1,除非當 INT1AE=0,則此動作就取消。
  6. 手動設定連接 INT1 pin 的 port 是沒有作用的,也就是手動設定 INT1 並不會觸發中斷。
  7. S35390A 的 alarm interrupt 剛好符合 8051 的 external interrupt,可以直接用 external interrupt 來偵測。如果沒有多餘的腳位,則可改成讀取 status1 來判斷 interrupt。
  8. 如果一開始就把 INT1 pin 拉 LOW,就會造成當 alarm interrupt 發生後要取消回 HIGH 時,不會自動回復成 HIGH,而是一直呈現在 LOW 的狀態下。

void s35390a_write_alarm1(unsigned char xdata *value) { unsigned char i=10, j; while (--i) { // set alarm time i2c_start(); if(i2c_write(ADDR_INT1 | I2C_WRITE)) continue; for (j=0; j<RTC_ALARM_LENGTH; j++) { if(i2c_write(value[j])) break; } if (j<RTC_ALARM_LENGTH) continue; i2c_stop(); // start alarm j = s35390a_read_status_2(); j = j & INT1_MASK | INT1AE; s35390a_write_status_2(j); break; } }

● free SRAM register

  1. S35390A 可使用的 SRAM 只有二個 bytes,一個是完整的 Free register,一個是分散在各地的 Scratch bits。
  2. Scratch bits 中的 SC0/SC1 存在 status1,所以任意時間都可以用。而 SC2~SC7 存在 INTx register,只有當 INTxME=0 and INTxFE=1,才可以使用。

bin2bcd bcd2bin

Posted by: 邱小新 at 下午4:49 in
int bin2bcd (int x) { return (x%10) | ((x/10) << 4); } int bcd2bin (int x) { return (x >> 4) * 10 + (x & 0x0f); }

S35390A driver for 8051 part 1

Posted by: 邱小新 at 下午1:34 in ,
原始碼下載

● 簡介

S35390A 是由精工電子有限公司開發製造,Seiko Instruments Inc. 是一家日本公司,台灣也有子公司,網址為 http://www.sii.com.tw/

datasheet 下載點: 簡體中文版英文版日文版

● 電源 Power Supply

  1. 一般來說 S35390A 的電源是由鈕扣電池(CR2032 3V)所提供,一開始我還以為是由主板上的電源所提供,害我一直誤解為啥 Power-on 就要重置時間。原來是說當電池沒電或換了新電池時,才會有 power-on detection。
  2. BLD 為電壓降低偵測旗標,S35390A 一秒才會做一次電壓降低偵測,所以只要電池拔掉再裝回去的時間在一秒內都不會引起 BLD。
  3. POC 為 Power-on Detection,只要把電池拔掉約五秒,再次裝上電池就會觸發POC。
  4. 當 POC or BLD 被設置,都需要做一次 chip reset。

● 協定 protocol

  1. S35390A 是使用 I2C protocol,在 1.5V 有 100kHz 速度,在 3.0V 有 400kHz 速度,算是快速的 I2C chip。
  2. 在早期的 datasheet 裏跟現在的 datasheet(Rev.2.5_00) 對於經由 command 所取的 data 描述大不同。主要是早期接收 bit 時,是從 LSB 到 MSB,剛好跟 command 的傳送方式相反,造成一些問題。而新的 datasheet 就把 data 的描述顛倒過來,使得 command 跟 data 的 bit order 剛好一致,但本質是不變的。
  3. 參數設定 command 對於 bit order 不同是沒有差異的,主要是時間 command 對於 bit order 是很敏感的。由於 bit order 接收是 little endian 而非普遍的 big endian,所以對於 I2C protocol 必須重新設計,不能使用舊的方法。
  4. 如果是按照新的 datasheet 來說,是不用重新設計 protocol,但是由於接收下來的時間卻 little endian,所以必須另外寫一個轉換函數把 little endian 轉換成 big endian。
  5. 執行 command(Real-time data 1 access),照原始 I2C protocol 來說,讀取最後一個 byte 回傳 ack=1,讀取中間的 bytes 一律回傳 ack=0。當然 S35390A datasheet 也是如此寫,沒有錯誤。但是實際上在 coding 的過程中卻發現完全相反,讀取中間的 bytes 必須回傳 ack=1,才能得到正確值;如果回傳 ack=0,傳回值卻都是 0。真是奇也怪哉,莫名奇妙,害我玩了好多天才找到問題所在。
  6. 在設定時間時,必須一次寫入完整 7 bytes,如果漏了一個 byte 就執行 i2c_stop, S35390A 則不會存入這次寫入的時間。

● slave address

S35390A 的 slave address 為 7-bit addressing 變化型,也就是把前 4 bits 固定,後 3 bits 來當做 command。因此在 I2C bus 上的 chip,前 4 bits 不能為 0110,也就是不能為 0x6? 的 slave address,否則會跟 S35390A 相衝突。

● chip init

按照 datasheet 所陳述的初始化流程圖,有下列需要注意的事項。

  1. 當電源接上時(也就是裝上電池),會設置 POC=1;在此時需要設置 RESET=1,讓 chip reset。
  2. 當電源拔掉時(也就是突然接觸不良),會設置 BLD=1;在此時需要設置 RESET=1,讓 chip reset。
  3. 當有不明原因讓 chip 進入 test mode 時,會設置 TEST=1;在此時需要設置 RESET=1,讓 chip reset。
  4. chip reset 完之後,必需再次檢查上述三項條件。
void s35390a_init(void) { unsigned char tmp; while (1) { // check status 1 tmp = s35390a_read_status_1(); if ((tmp & (POC | BLD)) || ((tmp & H24) == 0)) { s35390a_reset(); continue; } // check status 2 tmp = s35390a_read_status_2(); if (tmp & TEST) { s35390a_reset(); continue; } break; } // read datetime s35390a_read_data1(rtc_data); }

to be continue...

winbond reset status

Posted by: 邱小新 at 上午10:08 in ,

記錄一下系統開機或是被 reset 時的一些旗標狀態。

旗標狀態

resetWTRFPOREWTWDCON判斷
power-on reset01001000000bPOR=1
external resetxxx0x0x0xx0bEWT=1*
POR=0#
watchdog reset1xx0x0x01x0bWTRF=1

註解

  1. x: 保留原值,不會變更。
  2. *: 系統如果有開啟 watchdog 功能,在發生 external reset 時,EWT 一定是 1。
  3. #: 一開機時 POR=1,此時把 POR 設成 0,下次發生 external reset 時,POR 一定是 0,以此來區分。適合沒有開啟 watchdog 功能的系統。

判斷順序

  1. 開啟 watchdog: WTRF=1(watchdog) -> EWT=1(external) -> POR=1(power-on)。
  2. 關閉 watchdog: POR=1(power-on) -> POR=0(external),watchdog 不用判斷。

reset 開機狀態

  1. Program Counter 強制跳回 0000h,也就是從頭開始執行。
  2. SFR 都會返回 reset value,datasheet 有一張表。
  3. stack point 會變成 07h。
  4. Vdd 小於 2V 時,RAM 資料會遺失;反之則會保存。

好用的 STATUS register

Posted by: 邱小新 at 上午10:47 in

以往在寫 uart 的 putchar 函數時,都是用下列方式來寫。在初始化時還要先把 TI 設成 1。但是如此會造成 ES 中斷一直發生。只適合沒有開啟 ES 中斷程序來用。

void putchar(char value) { while (!TI); TI = 0; SBUF = value; }

若是改用下列方式來寫,初始化時不用把 TI 設成 1,ES 中斷就不會一直發生。但是如此會被 block,等到傳送完才繼續其它工作,對效能來說不是很好。適合需要開啟 ES 中斷程序來用。

void putchar(char value) { SBUF = value; while (!TI); TI = 0; }

W77E352 有一個 register 叫 STATUS(0xC5),它的 SPTA0 位元為 uart 傳送忙碌指標,當 TI 為 1 時,自動清成 0。在 W79E632 也有 STATUS register,但是卻沒有 SPTA0 位元,不知是否可用?

void putchar(char value) { while (STATUS & SPTA0); SBUF = value; } void uart_isr(void) interrupt (4) { if (TI) TI = 0; }

ir remote control 加入 repeat/long key

Posted by: 邱小新 at 下午1:47 in

ir remote control NEC protocol 裏只是提到如何處理一個按鍵的處理。在實際應用上,常常會有遇到按著不放時,要重覆處理這個按鍵,或是按住某個按鍵多久才處理。現在我們就來討論這二個應用方法。

下面主要是用來做一個 buffer,免得 mcu 處理來不急,ir_put 都放在 interrupt 內使用,並沒有給一般常式使用;而 ir_get 只有 ir_process 會用到,但是 counter 跟 ir_put 共用到,所以加上中斷排除,免得發生同時修改的慘況。

#define IR_BUFFER_MAX 4 #define IR_BUFFER_MASK 3 static xdata unsigned char ir_buffer[IR_BUFFER_MAX]; static data unsigned char get_index, put_index, counter; static unsigned char ir_debound, ir_key, ir_repeat_key, ir_long_key; static unsigned char code ir_repeat[] = { IR_UP, IR_DOWN, IR_LEFT, IR_RIGHT }; #define IR_REPEAT_MAX (sizeof(ir_repeat)/sizeof(unsigned char)) static void ir_put(unsigned char value) { if (counter < IR_BUFFER_MAX) { ir_buffer[put_index] = value; counter++; put_index++; put_index &= IR_BUFFER_MASK; } } static unsigned char ir_get(void) { unsigned char value; value = ir_buffer[get_index]; ET0 = LOW; counter --; ET0 = HIGH; get_index++; get_index &= IR_BUFFER_MASK; return value; }

下面是在 interrupt 內會用到 ir_put 的片斷,只有二個地方;一個是輸入 repeat 訊號,一個是輸入按鍵訊號。

if (IS_IR_REPEAT(pulse)) { if (ir_pos == 32) ir_put(IR_REPEAT); return; } if(ir_pos >= 32) { ir_mode = 0; stop_timer(); if ((ir_code[0] | ir_code[1]) != 0xef) return; if ((ir_code[2] | ir_code[3]) != 0xff) return; if (ir_code[0] == 0x40) ir_put(ir_code[2]); }

下面是放在 main loop 裏的函數,ir_function 用來處理按鍵事件,而 ir_process 是用來計算處理 normal,repeat,long 按鍵的函數。

void ir_function(unsigned char key) { switch (key) { default: printf_tiny("ir recv: %x,%d,%d,%d\n\r", ir_key, get_index, put_index, counter); } } void ir_process(void) { unsigned char key; if (counter == 0) { return; } key = ir_get(); switch (key) { case IR_REPEAT: if (ir_debound == 0) { ir_debound++; if (ir_key == IR_LONG_KEY) { ir_long_key = IR_LONG_KEY; break; } for (key=0; key<IR_REPEAT_MAX; key++) { if (ir_key == ir_repeat[key]) break; } if (key < IR_REPEAT_MAX) ir_repeat_key = ir_key; } else if (ir_repeat_key != IR_NONE) { if (ir_debound > IR_REPEAT_COUNT) ir_function(ir_repeat_key); else ir_debound++; } else if (ir_long_key != IR_NONE) { if (ir_debound < IR_LONG_COUNT) { ir_debound++; } else if (ir_debound == IR_LONG_COUNT) { ir_function(ir_long_key); ir_debound++; } } break; default: ir_debound = 0; ir_long_key = IR_NONE; ir_repeat_key = IR_NONE; ir_key = key; if (key != IR_LONG_KEY)&#12288;ir_function(key); } }

I2C protocol 時間計算

Posted by: 邱小新 at 上午10:23 in

I2C protocol 原理及應用有提供了一個範例程式碼,裏面的 i2c_wait 在當時是用試誤法來測出需要幾個 nop 指令。最近正好在做新的案子,使用不同的 crystal,就想用時脈來計算出真正需要多少個 nop 指令。

以下推導過程,使用 22.1184MHz 的振盪器,I²C匯流排速度為標準模式(100 Kbit/s)。

  1. 1 clock = 1/22.1184 us
  2. 1 machine cycle = 12 clock = 12/22.1184 us
  3. 1 nop = 1 machine cycle = 12/22.1184 us = 542.534722 ns
  4. 100 kbit/s = 1bit per 1/100000s = 10 us
  5. 1 bit cycle = 10/0.542 = 18.432 machine cycle
  6. 2 * i2c_wait = 1 bit cycle ==> i2c_wait = 9.2 machine cycle = 9 nop

實際上使用的 nop 有 8 個,可以正常 read/write 256 bytes eeprom。但是由於還會呼叫 lcall,ret 等指令,理論上應該少更多才對。不知是 eeprom 不穩,無法達到 100Kbit/s,還是其它原因?

以下推導過程,使用 40MHz 的振盪器,I²C匯流排速度為標準模式(100 Kbit/s)。

  1. 1 clock = 1/40 us
  2. 1 machine cycle = 12 clock = 12/40 us
  3. 1 nop = 1 machine cycle = 12/40 us = 0.3 us
  4. 100 kbit/s = 1bit per 1/100000s = 10 us
  5. 1 bit cycle = 10/0.3 = 33.33 machine cycle
  6. 2 * i2c_wait = 1 bit cycle ==> i2c_wait = 16.67 machine cycle = 16 nop

實際上使用的 nop 有 10 個,只有測試讀寫 1 byte。由於還會呼叫 lcall,ret 等指令,所以少了 6 個 nop 是可以理解的。但是實際上測試需要做讀寫大量的資料,不然像上例的 eeprom,單獨讀寫一個沒問題,大量讀寫時,卻出現很多錯誤。

W77E352 interrupt

Posted by: 邱小新 at 下午1:51 in

W77E352 可以算是 winbond 系列中中斷最多的一個,而像之前我使用的 W79E632 的中斷似乎也是跟 W77E352 系出同門。比如 watchdog timer 的中斷號碼都是 12,而 W79E632 的中斷就少了中間的 7~11,現在終於在 W77E352 看到影子了。但是奇怪的是 6 號中斷不知是那一個?


priority /
interrupt number
sourceflagaddr
0external interrupt 0IE00003h
1timer 0 overflowTF0000bh
2external interrupt 1IE10013h
3timer 1 overflowTF1001bh
4serial portRI+TI0023h
5timer 2 overflowTF2+EXF2002bh
6????0033h
7serial port 1RI_1+TI_1003bh
8external interrupt 2IE20043h
9external interrupt 3IE3004bh
10external interrupt 4IE40053h
11external interrupt 5IE5005bh
12watchdog timerWDIF0063h

  1. INT0,INT1 根據標準的 8051,是可以設定成 edge trigger 或 level trigger,但是由 winbond 提供的 INT2~INT5 卻只能是 edge trigger,而且還各有不同的 edge。
  2. INT2 is raising edge triggered.
  3. INT3 is falling edge triggered.
  4. INT4 is raising edge triggered.
  5. INT5 is falling edge triggered.
  6. INT2~INT5 需手動清除中斷旗標,但可以經由設定 T2MOD 的 HCx,變成自動清除。
  7. INT2~INT5 如果沒有清除中斷旗標,會造成中斷一直重覆,切記。由於 INT0~INT1 會自動清除,所以沒有這個問題。

時間單位

Posted by: 邱小新 at 上午10:56 in
  1. second: 1s = 1000ms
  2. millisecond: 1ms = 1000us
  3. microsecond: 1us = 1000ns
  4. nanosecond: 1ns = 1000ps
  5. picosecond: 1ps = 1000fs
  6. femtosecond: 1fs = 1000as
  7. attosecond: 1as = 10-18 s

SPI (Serial Peripheral Interface)

Posted by: 邱小新 at 上午9:35 in

SPI,Serial Peripheral Interface,顧名思義就是串列周邊介面。是 Motorola 首先在其 MC68HCXX 系列處理器上定義的。SPI 主要應用在轉換器(ADC and DAC),記憶體(EEPROM and FLASH),即時時鐘(RTC),感測器(溫度、壓力)等。SPI 是一種高速串列傳輸匯流排,全雙工通訊協定,主僕控制架構。