Swapping Nibbles in Keil C51

今天寫程式的時候,有個地方需要 swap 某個 byte 的兩個 nibbles。其實 8051 原本就有一個 instruction 專門做這件事。若是直接寫組語的話,只要 3 行就搞定了:

    MOV     A,tmp       ; 先將原本的值搬進 accumulator
    SWAP    A           ; swap nibbles
    MOV     tmp,A       ; 先將原本的值搬進 accumulator

問題我是用 C 在寫 code 的,所以我就開始研究是不是有辦法讓 Keil C 幫我產生這樣的 code。

首先,我們用標準的 C 的作法來做:

    tmp = (tmp >> 4) | (tmp << 4);

Keil C 產生出來的組語程式如下:

    MOV     A,tmp
    SWAP    A
    ANL     A,#0F0H
    MOV     R7,A
    MOV     A,tmp
    SWAP    A
    ANL     A,#0FH
    ORL     A,R7
    MOV     tmp,A

這樣翻出來的組語程式雖然看起來很長,不過我們還是可以看出 Keil C 厲害的地方:雖然我們寫的程式碼是「向左/向右 shift 4 個 bits」,但翻出來的並沒有「連續 shift 4 次」這樣的過程(8051 的 shift 一次只能 shift 1 個 bit,所以要 shift 多個 bits 就得用迴圈來跑),而是很聰明地利用 SWAP,分別取得 high/low nibble,再重新組合成 完成的 byte。

這樣的結果讓我蠻興奮的,因為這表示 Keil C 會去看 bit operation 的位數,適時地使用處理 nibble 的指令。也就是說,我有機會找到一種寫法,讓 Keil C 翻出直接 SWAP 的組語程式。

但接下來我就卡住了,因為我找不到更好的寫法去表達 “swap nibble” 這樣的想法。

後來,我在 Keil 的文件中找到一系列 intrinsic routines。很不幸地,裡面還是沒有 swap;但卻_crol_()_cror_() 這樣的 rotate 函式。於是我試著這樣寫:

    tmp = _crol_(tmp,4);

結果翻出來的組語如下:

    MOV     R7,tmp
    MOV     R0,#04H
    MOV     A,R7
    INC     R0
    SJMP    ?C0081
?C0080:
    RL      A
?C0081:
    DJNZ    R0,?C0080
    MOV     tmp,A

結果令人失望:變得更糟了!沒有直接用 SWAP 就算了,還利用迴圈的方式來處理。看樣子這個方法也行不通。

剩下一個方法:寫 inline assembly。但 Keil C 的 inline assembly 實在很難用:只要用了這個功能,該 .C 檔就不能直接編譯成 .OBJ 檔,而必須先產生出組語程式,再另外去組譯這個檔案。很麻煩。站在專案維護的角度來看,代價太高了,所以我放棄這樣的做法。

到頭來,我還是採用一開始的那種做法,畢竟在可維護性、執行速度、code size 等各方面,都算是平衡的做法。只是不能用到 8051 提供的功能,心中還是有些遺憾….

PS: 在 這個討論串中,也有人提到用 intrisic routine 的做法,而且說 Keil C 會幫忙做最佳化;不過我實驗的結果並不是這樣。