2013/11/13

Cコンパイラの罠2

●機能レジスタのアドレスをポインタに
次にポインタにTRISAなどの機能レジスタのアドレスを代入してみました。

char* p = &PORTA;
*p |= (0x01 << 1);
のコンパイル結果は次の通りです。

movlw    high(12)
movwf    (main@p+1)
movlw    low(12)   
movwf    (main@p)

movf    (main@p),w     
movwf    fsr1l     
movf    (main@p+1),w   
movwf    fsr1h   
bsf    indf1+(1/8),(1)&7

(9サイクル)

これはTRISAの番地12のhighである0とlowである0x0cを取得してmain@pラベルの位置に書き込みます。
次にそのメモリの位置からFSR1L, FSR1Hレジスタにそれぞれ書き込み、INDF1レジスタから値を受け取ります。
(1/8)の意味するところは分かりません。bsf INDF1, 1によってビットの1番目をセットしています。
コンパイラのデータシートに書いてあるとおり、ビットのセット/クリアは次のように書けばビット命令でコンパイルしてくれます。

var |= (0x01 << N);
var &= ~(0x01 << N);

*pの指すバンクは事前に分かるわけですから、人間がアセンブリを書くとすれば次のように最適化されるでしょう。

movlb   0x00 ; バンクを0に
movlw    0x0c ; バンクの中のアドレス0x0cを
movwf    (main@p) ; main@pラベルの位置に書き込み

movlb   0x00 ; バンクを0に
bsf    (main@p), 1 ; main@pラベルのメモリにあるデータの1bit目をset

(5サイクル)

これはフリー版ですから仕方がないのですけどね。コンパイラ作りたくなってきますね。。。





●onewireライブラリのコンパイル結果を見る
初期化時に次の様なコードを書いています。

onewire = Onewire_initialize(0);
port.groupe = 0;
port.no = 2;
Onewire_setPort(onewire, port);

着目するのはOnewire_setPort(onewire, port);であり、これのコンパイル結果は次の通りです。

movf    (main@port),w
movwf    (??_main+0)+0
movf    (??_main+0)+0,w
movwf    (?_Onewire_setPort)
movf    (main@onewire),w
fcall    _Onewire_setPort

PICにはCALL命令とRETURN命令があり、
CALLで関数(というか任意のアドレス)の先頭にジャンプし、直前のアドレス+1をスタックに詰みます。
次にRETURNが現れたらスタックのアドレスを戻して元に戻るという仕組みです。
これらの命令にはそれぞれ2サイクルかかり、合計4サイクル必要です。
fcallはおそらくアセンブラがCALLを呼び出すようにしてくれると思います。

初めの
movf main@port, w
では、main関数のport変数を作業用(W)レジスタに格納しています。
次にmovwfで??_mainラベルにWレジスタの値を格納します。つまりmain@portを??_mainに移したわけです。
その後movfで再びWレジスタに??_mainの値を格納し、?_Onewire_setPortに戻します。
その後main@onewireの値をWレジスタに格納し、fcallで関数へジャンプします。

つまり、引数を渡すためにportをWに読み込んで、Wから一時変数に読み込んで、一時変数から一時引数に読み込んで、onewireをWに読み込む処理をするわけです。
これが引数一つならWレジスタに格納して渡すだけです。

一見余分に見える処理は
movwf    (??_main+0)+0
movf    (??_main+0)+0,w
ですが、

movf    (main@port),w
movwf    (?_Onewire_setPort)
movf    (main@onewire),w
fcall    _Onewire_setPort

(?_Onewire_setPort)や(??_main+0)は一体何なのでしょう? その前にsetPortを確認してみます。

void Onewire_setPort(Onewire* self, Onewire_Port port) {
    if(port.groupe == 0) {
        self->tris = &TRISA;
        self->lat = &LATA;
        self->port = &PORTA;
    }
    else {
        self->tris = &TRISB;
        self->lat = &LATB;
        self->port = &PORTB;
    }
    self->no = port.no;
}

ポート設定でA/Bを切り替えられるように、というか任意のポートから出力できるようにこのように書きました。
以下はコンパイル結果です。

_Onewire_setPort:   
movwf    (Onewire_setPort@self)
movf    (Onewire_setPort@port),w
andlw    (1<<4)-1
iorlw    0
skipz
goto    u11
goto    u10
u11:
goto    l624
u10:
l622:   
incf    (Onewire_setPort@self),w
movwf    fsr1l
clrf fsr1h
movlw    low(140)
movwi    [0]fsr1   
movf    (Onewire_setPort@self),w
movf    (Onewire_setPort@self),w
addlw    03h
movwf    fsr1l
clrf fsr1h   
movlw    low(268)
movwi    [0]fsr1
movf    (Onewire_setPort@self),w
addlw    05h
movwf    fsr1l
clrf fsr1h   
movlw    low(12)
movwi    [0]fsr1
goto    l112
l110:   
l624:   
incf    (Onewire_setPort@self),w
movwf    fsr1l
clrf fsr1h   
movlw    low(141)
movwi    [0]fsr1
movf    (Onewire_setPort@self),w
addlw    03h
movwf    fsr1l
clrf fsr1h   
movlw    low(269)
movwi    [0]fsr1
movf    (Onewire_setPort@self),w
addlw    05h
movwf    fsr1l
clrf fsr1h
movlw    low(13)
movwi    [0]fsr1
goto    l112
return
(終わり)

movwf    (Onewire_setPort@self)
movf    (Onewire_setPort@port),w
で、Wレジスタからselfへ格納し、次にOnewire_setPort@portラベルの中身をWレジスタへ格納しています。
これはOnewire_setPort@portにどこかで書き込んだと言うことです。

以下単調に処理が進みます。
andlw    (1<<4)-1
では0x01を4回左シフトして1引くことで0x0fを得ます。これはアセンブラが処理する計算で、PIC自体は計算しません。即ち、PIC内には、
andlw 0x0fが書き込まれ、実行されます。
andlwは定数値(0x0f)とWレジスタの値をANDしてWレジスタに格納する命令です。

iorlw    0
では0とWレジスタをORしてWレジスタに格納する命令です。0とORしても変わらないので一見要らない命令のように考えられます。

skipz
はゼロフラグが立ったらスキップするマクロです。
skipz    macro
btfss    3,2
endm
と定義されています。skipzはマクロの名前で、endmまでがマクロです。BTFSSはPICの命令で、アドレス3のレジスタの中身の2bit目がsetされていたら次の命令をスキップするというものです。

PIC16F1827ではバンクの切り替えなしでアクセスできるCOMMONレジスタがあり、その番地は各バンクの0x00から0x0Bまでです。
3が指すレジスタはSTATUSレジスタで、データシートによると次のようなレジスタです。

bit 7-5 Unimplemented: Read as ‘0’
bit 4 TO: Time-out bit
1 = After power-up, CLRWDT instruction or SLEEP instruction
0 = A WDT time-out occurred
bit 3 PD: Power-down bit
1 = After power-up or by the CLRWDT instruction
0 = By execution of the SLEEP instruction
bit 2 Z: Zero bit
1 = The result of an arithmetic or logic operation is zero
0 = The result of an arithmetic or logic operation is not zero
bit 1 DC: Digit Carry/Digit Borrow bit (ADDWF, ADDLW, SUBLW, SUBWF instructions)(1)
1 = A carry-out from the 4th low-order bit of the result occurred
0 = No carry-out from the 4th low-order bit of the result
bit 0 C: Carry/Borrow bit(1) (ADDWF, ADDLW, SUBLW, SUBWF instructions)(1)
1 = A carry-out from the Most Significant bit of the result occurred
0 = No carry-out from the Most Significant bit of the result occurred

2bit目はゼロフラグで、計算結果が0なら1になり、計算結果が0でなければ0という一般的なCPUにあるものです。
直前のIORLWは要らない命令でした。なぜならばANDLWで演算した結果が0ならゼロフラグが立つからです。
なお、スキップには2サイクル必要です。スキップしなければ1サイクルで済みます。

従って、
skipz
goto    u11
goto    u10
u11:
goto    l624
u10:
のコードは、
結果が0ならu10の処理を、結果が1ならu11の処理をするコードとなっています。u11はすぐにl624にジャンプします。
何度も言うようにgotoでジャンプするには2サイクル必要です。4MHzで動作するPICなら2μsかかります。これが2+2+2 = 6μsとなれば大きいものですね。

これらの処理は
if(port.groupe == 0) {
    // u10
}
else {
    // u11
}
に対応します。

続いて次の命令に進みます。
l622:   
incf    (Onewire_setPort@self),w
これはOnewire_setPort@selfの(中身の)値をインクリメント(+1)してWレジスタに書き込む処理です。wを省略すると自信に書き込みます。

movwf    fsr1l
clrf fsr1h
これはFSR1Lレジスタに書き込み、FSR1Hレジスタに0を書き込んだものです。

movlw    low(140)
ここでは140=0x8Cのアドレスの下位8bitを取得し、Wレジスタに書き込んでいます。

movwi    [0]fsr1
MOVWI命令は新しい命令で、FSR1レジスタの指すメモリに書き込む命令です。(今までなかったんだ……)
[0]はオフセットで、メモリの位置を-32から+31まで動かせます。

結局、Onewire_setPort@self + 1のアドレスに、0x008C = TRISAのアドレスを書き込んだことになります。

同様の処理をします。
movf    (Onewire_setPort@self),w
addlw    03h
movwf    fsr1l
clrf fsr1h
movlw    low(268)
movwi    [0]fsr1
Onewire_setPort@selfから読み出して、03h(hは16進数の意味)加えて(つまり、初めは+1, 次が+3だからポインタの格納のためにしっかり2byte確保していることが分かります。)
MOVWF, CLRFでアドレスを設定し、MOVLWでアドレスを取得し、NIVWIで書き込むというものです。ここで問題が発生。268 > 255だから、TRISAを示さなくなってしまいます。
fsr1hを1にしなければならないはずですが……。

これはおかしい。もう一度ポインタの動作を確認しなければ……

追記 :

●検証
間に何か挟んでみた。

char* p = &TRISA;
LATA0 = 0;
*p = 5;

代入部分 :
movlw high(140)
movwf (main@p+1)
movlw low(140)
movwf (main@p)

構造体に入れてみた。

struct {
    char* samp;
    char x;
} test;
test.samp = &LATA;

*test.samp = 0x01;

movlw high(268)
movwf (main@test+1)
movlw low(268)
movwf (main@test)

代入部分 :
movlw high(268)
movwf (main@test+1)
movlw low(268)
movwf (main@test)

movlw low(140)
movwi [0]fsr1
movlw high(140)
movwi [1]fsr1

構造体のポインタに入れてif文の中で実行してみた。
struct _A {
    char* lat;
    char* tris;
    char x;
} test;
struct _A* p = &test;

p->x = 0;

if(test.x == 0) {
    p->tris = &TRISA;
}

代入部分 :
movlw low(140)
movwi [0]fsr1
movlw high(140)
movwi [1]fsr1

プログラムをそのままコピーして持ってきた。

Onewire test;
Onewire* self = &test;

if(port.groupe == 0) {
    self->tris = &TRISA;
    self->lat = &LATA;
    self->port = &PORTA;
}
else {
    self->tris = &TRISB;
    self->lat = &LATB;
    self->port = &PORTB;
}
self->no = port.no;

代入の一部 :
movlw low(268)
movwi [0]fsr1
movlw high(268)
movwi [1]fsr1

できてるし。どうやらselfを引数で受け取った後で中身に代入すると発症してしまうようだ。
試行錯誤の上、次のようにしたところうまくhighで取得してくれた。

void Onewire_setPort(Onewire* self_, Onewire_Port port) {
    Onewire o = *self_;
    Onewire* self = &o;

    if(port.groupe == 0) {
        self->tris = &TRISA;
        self->lat = &LATA;
        self->port = &PORTA;
    }
    else {
        self->tris = &TRISB;
        self->lat = &LATB;
        self->port = &PORTB;
    }
    self->no = port.no;
}

最初の結果 :
movlb 0
movwf (Onewire_setPort@self_)
l802:
movf (Onewire_setPort@self_),w
movwf fsr1l
clrf fsr1h
movlw low(Onewire_setPort@o)
movwf fsr0l
movlw high(Onewire_setPort@o)
movwf fsr0h
movlw 8
movwf btemp+1
0033 u250:
moviw fsr1++
movwi fsr0++
decfsz btemp+1,f
goto u250
l804:
movlw (Onewire_setPort@o)&0ffh
movwf (??_Onewire_setPort+0)+0
movf (??_Onewire_setPort+0)+0,w
movwf (Onewire_setPort@self)

結局症状は、main関数内でOnewire構造体のポインタを宣言し、Onewire_initializeによりポインタを受け取り代入した後にOnewire_setPort関数など呼び出して内部で設定すると上記のようなhighを呼び出さないコードが生成されるようだ。
仕方がないのでmain関数内でOnewireの実体を宣言し、各関数にポインタを渡すことで解決した。

(が、これではカプセル化もあったもんじゃない)

0 件のコメント:

コメントを投稿