はじめに
一般的な話としてコーディング(プログラムの作成)を行う場合は、「わかりやすいこと」「簡潔であること」が望ましいとされています。それは、主に次の理由からです。
- 不具合:不具合を作りこみにくい。また不具合が発生してもデバッグが行いやすい。
- 保守性:他の人が見てもわかりやすいコードであればあるほど、自分自身がメンテナンスがしやすい。コードの作成者であっても、半年も経てばすっかり忘れてしまうのが人間である。
- 生産性:他のプログラムを作成するときに流用しやすい。
しかし、これは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の部分)が生成されています。
- 003Bと003Cで0x01を__pcstackCOMMONにセットする。
- 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 |
コメント