XC8攻略記1

はじめに

一般的な話としてコーディング(プログラムの作成)を行う場合は、「わかりやすいこと」「簡潔であること」が望ましいとされています。それは、主に次の理由からです。

  • 不具合:不具合を作りこみにくい。また不具合が発生してもデバッグが行いやすい。
  • 保守性:他の人が見てもわかりやすいコードであればあるほど、自分自身がメンテナンスがしやすい。コードの作成者であっても、半年も経てばすっかり忘れてしまうのが人間である。
  • 生産性:他のプログラムを作成するときに流用しやすい。

しかし、これはWindowsやLinux、HTML5+Javascriptなどで動作するような大規模なアプリケーションを作る場合の話であり、マイコン、特にPICのプログラムを作る場合は違ってくると思います。少々わかりにくいコードになっても、プログラムサイズを小さくすることはとても重要です。

なぜならばPIC(特に8ピンPIC)に格納できるプログラム領域が小さいからです。プログラムサイズが大きすぎてサイズオーバーしてしまっては意味がありません。また、プログラムサイズが小さいほど、プログラムの実行も早くなりやすいです。

また、メモリ(SRAM)のサイズも小さいです。わかりやすいコーディングをすることで、メモリサイズが足りなくなることもあります。

そして、PICのコンパイラXC8の最適化はお世辞にも宜しくありません。PRO版は高性能なのかもしれませんが、とても高額なため私はフリー版しか使ったことがありません。フリー版は最適化に制限がかかっていて、生成されるプログラムは大きくなりがちです。(実際PRO版はプログラムサイズを小さくできることをセールスポイントにしています。)

こうなるとCのソースコードの書き方を工夫していくしかありません。Cソースの段階でプログラムサイズを意識しておくことは重要です。XC8がどういうコンパイルをするのかを知りつつ、最適なCソースの書き方を探っていく…XC8を攻略するのです。その記録として、この記事を書いていこうと思っています。

まずは具体的な実際例として次のコードを見てください。

#define SRCLK RC0 // シフトレジスタクロック(connect 595-11Pin) 
#define SER RC1   // シフトレジスタ入力(connect 595-14Pin)
#define RCLK RC2  // ストレージレジスタクロック(connect 595-12Pin)
// クロック信号は通常は0にしておく。立ち上がりエッジが必要なときのみ1にする。
/*
 * 74hc595にdataを送信する
 */
void SendDataTo595(uint8_t data)
{
    for (uint8_t i = 0; i < 8; i++)
    {
        if (data & (0x01 << i))
        {
            SER = 1;
        }
        else
        {
            SER = 0;
        }
        SRCLK = 1;
        // NOPを入れる必要があるかもしれません。
        SRCLK = 0;
    }
    RCLK = 1;
        // NOPを入れる必要があるかもしれません。
    RCLK = 0;
}

上記は、74hc595(8ビットシフトレジスタIC)を制御する関数です。74hc595は小ピンのマイコンの出力ピンを手軽に増やすことができるためによく使われるICであり、LEDの点滅(ナイトライダー)や7セグメントLEDの点灯に使う例をネットで検索するとすぐみつかるでしょう。

この関数は14ピンPICで使う事をイメージしていて、PICのRC0,RC1,RC2端子で引き数dataの8bitを74hc595に送信する処理を行っています。この例では、0bit目、1bit目→7bit目の順で送信していますが逆にしたい場合は、最初のif文を下のようにすればよいです。

        if (data & (0x80 >> i))

0bit目と7bit目のどちらを先に送信するかについては、この後の話には関係はありません。

この関数はこのままでも動作します。しかし処理時間の面でいくつかの改善点があります。74hc595への送信時間がそれほど気にならないならこのままでもよいです。しかし、「複数桁の7セグメントLEDのダイナミック点灯を制御する」とか「8×8マトリクスLEDを制御する」などで74hc595を使う場合なら、送信処理は短い方が有利です。処理時間が短いということは1秒間に再出力できる回数が増える…つまりFPSを増やすことができますので、表示のちらつきを抑えるために効果的です。

【注意点1】ビットシフト操作に気を付けよう

上記コードの問題点を説明しましょう。そのためにコードをコンパイルして生成されるアセンブルリストを見てみましょう。コンパイル時には-O2オプションはつけています。(つまりXC8フリー版としては最大に最適化しています。)

116:           void SendDataTo595(uint8_t data)
0039  00F4     MOVWF data
117:           {
118:               for (uint8_t i = 0; i < 8; i++)
003A  01F5     CLRF i
119:               {
120:                   if (data & (0x01 << i))
003B  3001     MOVLW 0x1
003C  00F3     MOVWF __pcstackCOMMON
003D  0A75     INCF i, W
003E  2840     GOTO 0x40
003F  35F3     LSLF __pcstackCOMMON, F
0040  0B89     DECFSZ WREG, F
0041  283F     GOTO 0x3F
0042  0873     MOVF __pcstackCOMMON, W
0043  0574     ANDWF data, W
0044  1903     BTFSC STATUS, 0x2
0045  2849     GOTO 0x49
121:                   {
122:                       SER = 1;
0046  0020     MOVLB 0x0
0047  148E     BSF PORTC, 0x1
123:                   }
0048  284B     GOTO 0x4B
124:                   else
125:                   {
126:                       SER = 0;
0049  0020     MOVLB 0x0
004A  108E     BCF PORTC, 0x1
127:                   }
128:                   SRCLK = 1;
004B  140E     BSF PORTC, 0x0
129:                   // NOPを入れる必要があるかもしれません。
130:                   SRCLK = 0;
004C  100E     BCF PORTC, 0x0
131:               }
004D  3001     MOVLW 0x1
004E  00F3     MOVWF __pcstackCOMMON
004F  0873     MOVF __pcstackCOMMON, W
0050  07F5     ADDWF i, F
0051  3008     MOVLW 0x8
0052  0275     SUBWF i, W
0053  1C03     BTFSS STATUS, 0x0
0054  283B     GOTO 0x3B
132:               RCLK = 1;
0055  150E     BSF PORTC, 0x2
133:                   // NOPを入れる必要があるかもしれません。
134:               RCLK = 0;
0056  110E     BCF PORTC, 0x2
135:           }
0057  0008     RETURN

最初のif文("120: if (data & (0x01 << i))"の行)の下のアセンブルリストが問題点の1つです。

ここでは、C言語のソースコードの"(0x01<<i)"を計算するために、アセンブルリストの8~14行目(003B~0041の部分)が生成されています。

  1. 003Bと003Cで0x01を__pcstackCOMMONにセットする。
  2. 003D~0041で__pcstackCOMMONをi回左シフトする。

という処理をforループの繰り返しで毎回行っているのです。

例えば、i=3のときに(0x01<<i)=0x08を計算するために3回の左シフトしています。そして、その結果を捨ててしまい、i=4のときはまた再計算(4回の左シフト)をしているのです。この処理は時間の無駄です。ここを改善したほうが良いでしょう。

改善したコードは下の通りです。

void SendDataTo595(uint8_t data)
{
    uint8_t bitmask = 0x01;
    for (uint8_t i = 0; i < 8; i++)
    {
        if (data & bitmask)
        {
            SER = 1;
        }
        else
        {
            SER = 0;
        }
        SRCLK = 1;
        // NOPを入れる必要があるかもしれません。
        bitmask <<= 1;
        SRCLK = 0;
    }
    RCLK = 1;
    // NOPを入れる必要があるかもしれません。
    RCLK = 0;
}

シフト操作を減らすためにbitmaskという変数を追加しました。そして(data & (1<<i))ではなく(data & bitmask)としています。

これにより、左シフトはforループの中では1回だけ行うようになりました。bitmask<<=1; を行うのはif文の後ならどこでもよかったのですが、「NOPを入れる必要があるかもしれません」というコメントのところで行うようにしました。bitmaskの計算処理がNOP代わりになり、余計なNOPを入れずに済みます。

「NOPを入れる必要があるかもしれません」についての補足説明。

74hc595はクロックの立ち上がりで内部の信号が変化したり、出力が変化したりします。SRCLKやRCLKを 0→1→0と変化することでそのクロックを加えていますが、信号の変化が早すぎる(1になっている時間が短すぎる)と74hc595がクロックの立ち上がりを認識できなくなるかもしれません。その場合は1になっている時間を延ばすためにNOPを入れる必要があります。

このコードのアセンブルリストは下のようになりました。

116:           void SendDataTo595(uint8_t data)
0039  00F1     MOVWF data
117:           {
118:               uint8_t bitmask = 0x01;
003A  01F2     CLRF bitmask
003B  0AF2     INCF bitmask, F
119:               for (uint8_t i = 0; i < 8; i++)
003C  01F3     CLRF i
120:               {
121:                   if (data & bitmask)
003D  0871     MOVF data, W
003E  0572     ANDWF bitmask, W
003F  1903     BTFSC STATUS, 0x2
0040  2844     GOTO 0x44
122:                   {
123:                       SER = 1;
0041  0020     MOVLB 0x0
0042  148E     BSF PORTC, 0x1
124:                   }
0043  2846     GOTO 0x46
125:                   else
126:                   {
127:                       SER = 0;
0044  0020     MOVLB 0x0
0045  108E     BCF PORTC, 0x1
128:                   }
129:                   SRCLK = 1;
0046  140E     BSF PORTC, 0x0
130:                   // NOPを入れる必要があるかもしれません。
131:                   bitmask <<= 1;
0047  1003     BCF STATUS, 0x0
0048  0DF2     RLF bitmask, F
132:                   SRCLK = 0;
0049  100E     BCF PORTC, 0x0
133:               }
004A  3001     MOVLW 0x1
004B  00F0     MOVWF __pcstackCOMMON
004C  0870     MOVF __pcstackCOMMON, W
004D  07F3     ADDWF i, F
004E  3008     MOVLW 0x8
004F  0273     SUBWF i, W
0050  1C03     BTFSS STATUS, 0x0
0051  283D     GOTO 0x3D
134:               RCLK = 1;
0052  150E     BSF PORTC, 0x2
135:               // NOPを入れる必要があるかもしれません。
136:               RCLK = 0;
0053  110E     BCF PORTC, 0x2
137:           }
0054  0008     RETURN

問題点としていた「(1<<i)を計算のための繰り返し処理」が無くなったので処理時間が改善されるはずです。また、プログラムサイズも減っています。

【注意点2】ループカウンタを見直そう

bitmaskに着目してみます。この変数は最初は0x01ですが、ループの中で左シフトされるので、

   初期値:0x01  // 00000001(2bit表現)
シフト1回目:0x02  // 00000010
(中略)
シフト6回目:0x40  // 01000000
シフト7回目:0x80  // 10000000
シフト8回目:0x00  // 00000000

と変化します。つまりbitmaskを使えば、ループカウンタの変数iを使わなくても8回ループしたことをチェックできます。

そこで下のようにコードを変更することができます。

void SendDataTo595(uint8_t data)
{
    uint8_t bitmask = 0x01;
    do
    {
        if (data & bitmask)
        {
            SER = 1;
        }
        else
        {
            SER = 0;
        }
        SRCLK = 1;
        // NOPを入れる必要があるかもしれません。
        bitmask <<= 1;
        SRCLK = 0;
    } while (bitmask);
    RCLK = 1;
    // NOPを入れる必要があるかもしれません。
    RCLK = 0;
}

8回をカウントするための変数iが必要なくなりましたので、for文ではなくdo while形式になっています。

上のコードに対するアセンブルリストは下のようになりました。

116:           void SendDataTo595(uint8_t data)
0039  00F5     MOVWF __pcstackCOMMON
117:           {
118:               uint8_t bitmask = 0x01;
003A  01F6     CLRF bitmask
003B  0AF6     INCF bitmask, F
119:               do
120:               {
121:                   if (data & bitmask)
003C  0875     MOVF __pcstackCOMMON, W
003D  0576     ANDWF bitmask, W
003E  1903     BTFSC STATUS, 0x2
003F  2843     GOTO 0x43
122:                   {
123:                       SER = 1;
0040  0020     MOVLB 0x0
0041  148E     BSF PORTC, 0x1
124:                   }
0042  2845     GOTO 0x45
125:                   else
126:                   {
127:                       SER = 0;
0043  0020     MOVLB 0x0
0044  108E     BCF PORTC, 0x1
128:                   }
129:                   SRCLK = 1;
0045  140E     BSF PORTC, 0x0
130:                   // NOPを入れる必要があるかもしれません。
131:                   bitmask <<= 1;
0046  1003     BCF STATUS, 0x0
0047  0DF6     RLF bitmask, F
132:                   SRCLK = 0;
0048  100E     BCF PORTC, 0x0
133:               } while (bitmask);
0049  0876     MOVF bitmask, W
004A  1D03     BTFSS STATUS, 0x2
004B  283C     GOTO 0x3C
134:               RCLK = 1;
004C  150E     BSF PORTC, 0x2
135:               // NOPを入れる必要があるかもしれません。
136:               RCLK = 0;
004D  110E     BCF PORTC, 0x2
137:           }
004E  0008     RETURN

変更前のコードは、for文の i < 8と i++ の部分のアセンブルリストがちょっと冗長になっていましたが、それがなくなったことでプログラムサイズがさらに小さくなりました。

C言語に慣れている人は、do whileを使うのではなく、for文を使って、

for ( uint8_t bitmask = 0x01 ; bitmask ; bitmask <<= 1)

のようにしたいと思うかもしれません。この辺は好みの問題だと思います。

ただしdo whileの方がアセンブルリストをイメージしやすいので、こちらの方がおすすめです。forループのカウンタを増やす処理は終了条件をチェックする部分はアセンブルリストになると分かりにくいところに生成されるので、最適化を検討しにくいと思います。(今までのケースの i<8やi++に対するアセンブルリストのある場所を探してみてください。そして今回のwhile(bitmask); のアセンブルリストのある場所と比較してみてください。)

【注意点3】ビットチェックの方法を検討しよう

ちょっと別のアプローチでコードを書いてみます。今までのコードでは関数の引数dataは不変にしていましたが、これを変えてしまっても呼び出し元には影響がありません。そこで、dataをシフトさせるコードにします。具体的には下のようなコードです。

void SendDataTo595(uint8_t data)
{
    uint8_t i = 8;
    do
    {
        if (data & 0x01)
        {
            SER = 1;
        }
        else
        {
            SER = 0;
        }
        SRCLK = 1;
        // NOPを入れる必要があるかもしれません。
        data >>= 1;
        SRCLK = 0;
    } while (--i);
    RCLK = 1;
    // NOPを入れる必要があるかもしれません。
    RCLK = 0;
}

dataを右シフトさせて0x01とANDを取りつつ信号を送信しています。このため変数bitmaskが不要になりましたが、8回シフトをしたことをチェックするために変数iが復活しました。

ビットチェック(特定ビットが0か1かをチェックする)処理を行う場合、PICは(data&bitmask)のような変数でビットチェックするコードよりも(data & 0x01)のような定数でビットチェックするコードを得意としています。それは「変数のnビット目が1なら…」というプログラム命令があるためです。そのため、こういったコードにすることを検討することは大事です。

そして実は上のコードは今回紹介したコード中では、プログラムサイズが最小になります。といっても、1つ前と比べると1つか減っていませんが…。アセンブルリストは載せないので興味ある方は自分で試してみてください。

測定結果

ここまでコードとアセンブルリストだけを表示してきましたが、最後に実行時間(サイクル数)を示しておきます。

MPLABのStopwatch機能で各関数のサイクル数を計測
使用したXC8のバージョン:v2.41

なお、サイクル数はdataの値によっても多少変化しますので、すべてdata=0のときの計測結果です。

コードStopwatchで計測したサイクル数
最初のコード320
改善1
1<<iの処理を無くした
162
改善2
iを使わないようにした
121
改善3
dataを左シフトするようにした
113

コメント