チュートリアルの最初の方で少し触れたVBlankとHBlankについて再度解説していこうと思います。HBlank期間を利用することで特殊な画面効果が可能です。
GBAの液晶画面は1秒間に約60回更新されます。1回の画面更新では、上から横1ラインずつ更新していき、画面を更新する縦160ライン分の期間と、直接描画は行わない縦68ライン分がありました。

The drawing time for each dot is 4 CPU cycles. Visible 240 dots, 57.221 us, 960 cycles - 78% of h-time H-Blanking 68 dots, 16.212 us, 272 cycles - 22% of h-time Total 308 dots, 73.433 us, 1232 cycles - ca. 13.620 kHz
Visible (*) 160 lines, 11.749 ms, 197120 cycles - 70% of v-time V-Blanking 68 lines, 4.994 ms, 83776 cycles - 30% of v-time Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz
画面を更新する横1ラインを描画する期間が960サイクル、描画を行わない期間が272サイクル(HBlank)あります。960+272=1232サイクル。縦は160とVBlank68で160+68=228ライン。1フレームあたりのサイクル数は1232×228=280896サイクルです。GBAのCPUは16.78MHz=16780000サイクルなので、1678000/280896=59.737... hzと、ほぼ計算が合うことになります。ようするに1秒間のCPUサイクル=約60フレームと考えることができます。
各ドットの色は、BGやスプライト、拡大縮小回転、モザイクなどの特殊効果を考慮して決定、描画されます。1ラインの描画が終わって次のラインを描画するまでのブランク期間(HBlank)に、横方向のスクロールレジスタ値を変更します。これをラスタースクロールと呼びます。HBlankを判断する方法としてHBlank割り込みを使い、現在どのラインを描画中なのかREG_VCOUNT(0~227)を取得します。
4000006h - VCOUNT - Vertical Counter (Read only) Indicates the currently drawn scanline, values in range from 160..227 indicate 'hidden' scanlines within VBlank area. Bit Expl. 0-7 Current Scanline (LY) (0..227) (R) 8 Not used (0) / NDS: MSB of Current Scanline (LY.Bit8) (0..262) (R) 9-15 Not Used (0)
準備としては、割り込みできるようIrqInit関数内のREG_IE、REG_DISPSTATにHBlankフラグを登録をします。IrqHandler関数は実際の割り込み中の処理です。
#include "irq.arm.h"
#include "math.h"
//---------------------------------------------------------------------------
// main.c
extern vs32 shakeAngle; // 横揺れの1周期内の位置
extern vs32 shakeCycle; // 横揺れの周期
extern vs32 shakeCx; // 横振れの幅
//---------------------------------------------------------------------------
void IrqInit(void)
{
REG_IME = 0;
INT_VECTOR = (u32)IrqHandler;
REG_IE = IRQ_VBLANK | IRQ_HBLANK;
REG_DISPSTAT = LCDC_VBL | LCDC_HBL;
REG_IME = 1;
}
//---------------------------------------------------------------------------
IWRAM_CODE void IrqHandler(void)
{
REG_IME = 0;
u16 flag = REG_IF;
if(flag & IRQ_HBLANK)
{
if(REG_VCOUNT < 160)
{
// 横方向を揺らす
REG_BG1HOFS = MathSin(shakeAngle * shakeCycle + REG_VCOUNT) * shakeCx / 256;
}
else if(REG_VCOUNT == 227)
{
// 次のフレームのVCOUNT 0用
REG_BG1HOFS = MathSin(shakeAngle * shakeCycle + 0) * shakeCx / 256;
}
}
if(flag & IRQ_VBLANK)
{
REG_IRQ_WAITFLAGS |= IRQ_VBLANK;
}
REG_IF = flag;
REG_IME = 1;
}
REG_BG1HOFS = MathSin(shakeAngle * shakeCycle + REG_VCOUNT) * shakeCx / 256;
横ラインの左右のスクロール値を決定している部分です。MathSin(shakeAngle * shakeCycle + REG_VCOUNT)の部分は、現在のフレーム数を元にsin値を取得します。サインテーブルは360で1周期となっているので、angleにshakeCycleをかけて360で1周期になるようにします。縦の1ラインごとにスクロール値を変更する必要がある(これをしないとラスタースクロールにならない)ので、REG_VCOUNTで現在のライン番号を取得してラインごとにスクロール値を変更しています。残りの* shakeCx / 256は、ゆれ幅の調整部分です。MathSin()で取得する値は-256~256の範囲なので、-shakeCx ~shakeCx の範囲になるように調節します。
#include "lib/gba.h"
#include "irq.arm.h"
#include "bg.h"
#include "key.h"
#include "math.h"
//---------------------------------------------------------------------------
vs32 shakeAngle; // 横揺れの1周期内の位置
vs32 shakeCycle; // 横揺れの周期
vs32 shakeCx; // 横振れの幅
//---------------------------------------------------------------------------
int main(void)
{
REG_WSCNT = 0x4317;
shakeAngle = 0;
shakeCycle = 4;
shakeCx = 32;
IrqInit();
KeyInit();
BgInit();
u16 cnt, trg;
for(;;)
{
VBlankIntrWait();
BgAsciiDrawPrintf(1, 1, "cycle = [%3d]", shakeCycle);
BgAsciiDrawPrintf(1, 2, "cx = [%3d]", shakeCx);
KeyExec();
cnt = KeyGetCnt();
trg = KeyGetTrg();
if(trg & KEY_UP) { shakeAngle=0; shakeCycle--; }
if(trg & KEY_DOWN) { shakeAngle=0; shakeCycle++; }
if(cnt & KEY_LEFT) { shakeAngle=0; shakeCx--; }
if(cnt & KEY_RIGHT) { shakeAngle=0; shakeCx++; }
shakeCycle = MathClamp(shakeCycle, 1, 32);
shakeCx = MathClamp(shakeCx, 1, 64);
if(shakeAngle == Div(360, shakeCycle))
{
shakeAngle = 0;
}
shakeAngle++;
}
}
ボタン操作できるので動作を見つつ確認して頂ければ幸いです。
