AVRでNokia5110を使ってみる (初ネギ)の続きです。
ネギ振りに使った画像(56×48ドット×6)です。チラチラするドットは修正しました。
髪の毛が黒くつぶれているので、腕がどこにあるのか分かりません。変換元の画像はInterfaceのダウンロードページからダウンロードしたものです。
プログラムを色々変えて、ネギ振りの速度を測ってみました。
測定は1秒間のタイマー割り込みで何回振るかを数えています。左下に秒間振り回数を表示しています。速すぎてネギが見えていませんが、実物は残像が見えます。
(1) 元々のコード 秒間100振り
lcd_data( )を使って1バイトずつ転送しています。呼び出すときは、
PROGMEM byte h1[56*6] = {0x00,...,};
みたいなデータを作って、
lcd_image_p(h1, 28, 0, 56, 6);
のように呼び出します。(28, 0)に大きさ56×6の画像を表示します。pgm_read_byteはコードの格納されているフラッシュからデータを読み出す関数です。この関数を使うときはavr/pgmspace.hをインクルードしておく必要があります。
全画面が84×48なので、はちゅね画像のサイズ56×48は2/3のサイズになります。また一振りが6枚の画像を使ったアニメーションです。
(2) インライン展開+先読み 秒間123振り
lcd_locateとlcd_dataをインライン展開しました。SCEの制御はまとめて最初と最後だけにしています。SPIの送信完了待ちの間に次のデータを読み込むようにしました。loop_until_bit_is_set( )というマクロはsfr_defs.hで定義されています。avr/io.hを読み込むと自動的に読み込まれるので明示的にインクルードする必要はありません。I/Oポートの特定のビットが1になるまで待ちます。sfr_defs.hで定義されているマクロは他にbit_is_set、bit_is_clear、loop_until_bit_is_clearがあります。
(3) ループをアンロールする 秒間153振り
xのforループの内側だけ示します。他は(2)と一緒です。ループを展開して4つずつ送信しています。横方向が56ドットなので8個ずつまでいけます。
(4) ウェイトをnopにしてみる 秒間210振り
xのforループの内側だけ示します。他は(2)と一緒です。loop_until_bit_is_set( )による待ち合わせを止めてみました。nopの個数は試行錯誤した結果6個がちょうどでした。ただし、関数から抜ける前にloop_until_bit_is_set( )を入れる必要がありました。今回は内部発振クロックの8MHzが動作クロックで、4MHz通信を行っています。コンパイラのはくオブジェクトが変わったりするとnopの個数が変わります。環境や条件によって動作しなくなるので、通常はやってはいけないコードです。
loop_until_bit_is_set( )による送信完了判定自体が数クロックかかっているので、実際にSPIEが1になってから、数クロック後に送信が完了したことが分かります。この分の無駄が結構あったみたいです。
最初のコードから2倍以上速くなりました。もっとちゃんとオブジェクトを見たり、出力波形を観測したりすればよかったのですが、試行錯誤が楽しかったので手を抜いています。書き換える必要のある最小限のデータだけ送るようにすれば、もっと速くなるはずです。全画面転送だと、画面サイズが1.5倍、画面枚数が1/6なので計算上は秒間840枚(=210*6/1.5)はいきそうです(実際は座標設定とかも減るのでもっと速い?)。約1.2ミリ秒で一画面の転送が終わることになります。画面全体でも504バイト(=84×48÷8)なので、SRAM上に画面全体のコピーを持っていて必要なときだけ全画面転送するという使い方も可能です。
ネギ振りに使った画像(56×48ドット×6)です。チラチラするドットは修正しました。
髪の毛が黒くつぶれているので、腕がどこにあるのか分かりません。変換元の画像はInterfaceのダウンロードページからダウンロードしたものです。
プログラムを色々変えて、ネギ振りの速度を測ってみました。
測定は1秒間のタイマー割り込みで何回振るかを数えています。左下に秒間振り回数を表示しています。速すぎてネギが見えていませんが、実物は残像が見えます。
(1) 元々のコード 秒間100振り
// (x0, y0)から大きさ(dx, dy)のPROGMEMの絵pを描く void lcd_image_p(byte *p, byte x0, byte y0, byte dx, byte dy) { byte x, y; for(y = 0; y < dy; y++){ lcd_locate(x0, y0 + y); for(x = 0; x < dx; x++){ lcd_data(pgm_read_byte(p++)); } } }
lcd_data( )を使って1バイトずつ転送しています。呼び出すときは、
PROGMEM byte h1[56*6] = {0x00,...,};
みたいなデータを作って、
lcd_image_p(h1, 28, 0, 56, 6);
のように呼び出します。(28, 0)に大きさ56×6の画像を表示します。pgm_read_byteはコードの格納されているフラッシュからデータを読み出す関数です。この関数を使うときはavr/pgmspace.hをインクルードしておく必要があります。
全画面が84×48なので、はちゅね画像のサイズ56×48は2/3のサイズになります。また一振りが6枚の画像を使ったアニメーションです。
(2) インライン展開+先読み 秒間123振り
// (x0, y0)から大きさ(dx, dy)のPROGMEMの絵pを描く void lcd_image_p(byte *p, byte x0, byte y0, byte dx, byte dy) { byte x, y, c; c = pgm_read_byte(p++); // 1バイト先読み PORTB &= ~(_BV(SCE)); // SCE=0 for(y = 0; y < dy; y++){ PORTB &= ~(_BV(DC)); // DC=0 (コマンド) SPDR = SETX | x0; // x座標送信 loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち SPDR = SETY | (y0 + y); // y座標送信 loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち PORTB |= _BV(DC); // DC=1 (データ) for(x = 0; x < dx; x++){ SPDR = c; // データ送信 c = pgm_read_byte(p++); // 次のデータを読み込む loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち } } PORTB |= _BV(SCE); // SCE=1 }
lcd_locateとlcd_dataをインライン展開しました。SCEの制御はまとめて最初と最後だけにしています。SPIの送信完了待ちの間に次のデータを読み込むようにしました。loop_until_bit_is_set( )というマクロはsfr_defs.hで定義されています。avr/io.hを読み込むと自動的に読み込まれるので明示的にインクルードする必要はありません。I/Oポートの特定のビットが1になるまで待ちます。sfr_defs.hで定義されているマクロは他にbit_is_set、bit_is_clear、loop_until_bit_is_clearがあります。
(3) ループをアンロールする 秒間153振り
for(x = 0; x < dx / 4; x++){ SPDR = c; // データ送信(1) c = pgm_read_byte(p++); // 次のデータを読む loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち SPDR = c; // データ送信(2) c = pgm_read_byte(p++); // 次のデータを読む loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち SPDR = c; // データ送信(3) c = pgm_read_byte(p++); // 次のデータを読む loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち SPDR = c; // データ送信(4) c = pgm_read_byte(p++); // 次のデータを読む loop_until_bit_is_set(SPSR, SPIE); // 送信完了待ち }
xのforループの内側だけ示します。他は(2)と一緒です。ループを展開して4つずつ送信しています。横方向が56ドットなので8個ずつまでいけます。
(4) ウェイトをnopにしてみる 秒間210振り
for(x = 0; x < dx; x++){ SPDR = c; // データ送信 c = pgm_read_byte(p++); // 次のデータを読む asm("nop"); // (1) asm("nop"); // (2) asm("nop"); // (3) asm("nop"); // (4) asm("nop"); // (5) asm("nop"); // (6) }
xのforループの内側だけ示します。他は(2)と一緒です。loop_until_bit_is_set( )による待ち合わせを止めてみました。nopの個数は試行錯誤した結果6個がちょうどでした。ただし、関数から抜ける前にloop_until_bit_is_set( )を入れる必要がありました。今回は内部発振クロックの8MHzが動作クロックで、4MHz通信を行っています。コンパイラのはくオブジェクトが変わったりするとnopの個数が変わります。環境や条件によって動作しなくなるので、通常はやってはいけないコードです。
loop_until_bit_is_set( )による送信完了判定自体が数クロックかかっているので、実際にSPIEが1になってから、数クロック後に送信が完了したことが分かります。この分の無駄が結構あったみたいです。
最初のコードから2倍以上速くなりました。もっとちゃんとオブジェクトを見たり、出力波形を観測したりすればよかったのですが、試行錯誤が楽しかったので手を抜いています。書き換える必要のある最小限のデータだけ送るようにすれば、もっと速くなるはずです。全画面転送だと、画面サイズが1.5倍、画面枚数が1/6なので計算上は秒間840枚(=210*6/1.5)はいきそうです(実際は座標設定とかも減るのでもっと速い?)。約1.2ミリ秒で一画面の転送が終わることになります。画面全体でも504バイト(=84×48÷8)なので、SRAM上に画面全体のコピーを持っていて必要なときだけ全画面転送するという使い方も可能です。
※コメント投稿者のブログIDはブログ作成者のみに通知されます