ゲームボーイでマルチタスク
〜 タスクスイッチャの実装 〜
2026-01-26 作成 福島
TOP > gbc > mtask
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob | Shell | GBC ]

0. 前置き

0-1. マルチタスクとは
「マルチタスク」という言葉を聞いたことがあると思います。
忍者が使う分身の術は、
  1. 敵の左側で停止 → 敵の右側へ素早く移動
  2. 敵の右側で停止 → 敵の左側へ素早く移動
ということを一人で繰り返して、あたかも挟み撃ちにしたかのように見せかけますが、(修練を積んだ優秀な忍者なら必ずできる。…らしい)
これと同じことを実施するのが「マルチタスク」です。

コンピュータでは一つしかない CPU で、時間を区切って複数のタスクを処理します。
マルチコアやスレッド、果ては仮想 CPU など、コンピュータには様々な CPU の形態がありますが、基本はすべて同じです。

最近では気取った働き方を「マルチタスク」と呼んだりしますが、元々はコンピュータ用語です。
コンピュータと同じく、人間が一人しかいないのに複数の仕事を一度に処理しますが、実際にそんなことは出来ません。
できたとしても、非常に質の悪い結果を出してしまうか、一つの仕事にかかる労力が軽いものしか扱えません。

コンピュータの「マルチタスク」は、特定のタイミングごとに CPU の中身をすべて入れ替えてタスクを処理していきます。
特定のタイミングとは、おもに割り込み信号によって引き起こされます。
タイマ割り込み、キー入力割り込み、画像信号割り込み、通信終了割り込みなど、
コンピュータに搭載されている機器や仕組みにより、様々な種類の割り込みがあります。
0-2. ジョブ
「タスク」に似た言葉として「ジョブ」があります。
ジョブは、人間が必要とする一連の流れを指して呼びます。

例えば「歩く」は右足と左足を交互に出すことですが、これは「ジョブ」に分類されます。
これに対し「右足を出す」「左足を出す」がそれぞれ「タスク」に該当します。

コンピュータは連続したタスクを処理することしかできません。
人間が終了の条件を決定し、タスクがその条件に達したときにジョブが終了します。
0-3. タスクスイッチャ
「タスクスイッチャ」とは、複数存在するタスクを次々と切り替えるプログラムのことです。
本稿ではゲームボーイの CPU を利用し、マルチタスクの基本となるタスクスイッチャをタイマ割り込みで実装します。
(説明用にプログラムを単純化するため、タスク数を 2 に限定しています。この数を増やしてみるのも面白いと思います)

ノイマン型の CPU なら何を使ってもタスクスイッチャを作ることができます。
ゲームボーイの CPU 「sm83(LR35902)」はレジスタが少ないため、学習用のタスクスイッチャを作りやすくなっています。
(本来の目的であるゲーム制作には不必要な論理思考が要求されるのですが…)

また、sm83 は i8080Z80 を合わせたような CPU なので、組み込みシステムへの応用もできます。
ただし、アクションゲームのように高速性を第一に要求されるような処理にマルチタスクは向きません。
0-4. エミュレータとデバッガで動作確認
このプログラムは実機でも動作しますが、このままでは実機での確認ができません。
動作確認はエミュレータとデバッガで実施してください。

画面表示は VBlank 中に行う必要があるため、表示処理を組み込むとプログラムが複雑になりタスクスイッチャが分かりにくくなります。
タスクスイッチャの原理が理解出来たら、前述の「ゲームボーイで文字列表示」を組み込んで実機での動作確認を実施してください。


1. メモリマップ

ROM

割り込みベクタ
割り込みが引き起こされたときの命令を記述する場所。アドレスは変更できない。

VBlank 不使用 Stat 不使用
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
0040  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Timer → タスクスイッチャへ Serial 不使用
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
0050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Joypad 不使用
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
0060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00


プログラムエントリ
ゲームボーイではプログラムエントリのアドレスは $0100 と決まっている。アドレスは変更できない。
続いてすぐ後にカートリッジヘッダ (固定値) が入る。

エントリ カートリッジヘッダ …
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
0100  00 C3 50 01 00 00 00 00 00 00 00 00 00 00 00 00


プログラム本体

メインプログラム …
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
0150  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00


RAM

タスク管理用

タス
クID
ダミ
ー0
ダミ
ー1
SP0 SP1 不使用
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
C000  00 0000 0000 0000 00 00 00 00 00 00 00 00 00

スタック(タスク 0 用)
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
C010  00000000 00000000 00000000 00000000
C020  00000000 00000000 00000000 00000000
C030  00000000 00000000 00000000 00000000
C040  00000000 0000HL
0000
DE
0000
BC
0000
AF
0000
ret
0000

スタック(タスク 1 用)
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
C050  00000000 00000000 00000000 00000000
C060  00000000 00000000 00000000 00000000
C070  00000000 00000000 00000000 00000000
C080  00000000 0000HL
0000
DE
0000
BC
0000
AF
0000
ret
0000


プログラム初期化用

スタック(初期化時) IE
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
FFF0  00000000 00000000 00000000 00000000


2. プロジェクトディレクトリの作成

VS Code ではディレクトリを開くことにより、プロジェクトを切り替える。

2-1. ディレクトリを作成する。
ファイルエクスプローラーでディレクトリを作成する。(プロジェクトディレクトリとする)

C:\Users\who\vsc-gba-mtask\
2-2. ハードウェア定数定義を設置する。
GitHub から hardware.inc をダウンロードし、作成したディレクトリに設置する。
(ダウンロード方法がわかりにくい)

直接ダウンロードするのはこちらから。
2-3. プロジェクトディレクトリとしてディレクトリを開く。
VS Code: メニュー > ファイル > フォルダーを開く (Ctrl+K Ctrl+O)
(上記プロジェクトディレクトリ : C:\Users\who\vsc-gba-mtask\)
このフォルダー内のファイルの作成者を信頼しますか?


「信頼します」ボタンをクリックする。
→ このディレクトリの中にあとで .vscode\ が作られる。


3. アセンブラの設定

上記 2 の操作のあとに実行する。

3-1. ひな形からビルド情報を記述する。(アセンブラ)
VS Code: メニュー > ターミナル > タスクの構成...
検索: テンプレートから tasks.json を生成 ← 選択
Others 任意の外部コマンドを実行する例 ← 選択
生成された .vscode\tasks.json にビルド情報を記述する。
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Assemble a_mtask",
            "type": "shell",
            "command": "cmd",
            "args": [
                "/C",
                // アセンブルを指示
                "rgbasm -o a_mtask.obj a_mtask.asm",
                // 実行ファイルを生成 (*.gb, *.sym を出力)
                "&& rgblink -n a_mtask.sym -o a_mtask.gb a_mtask.obj",
                // シグネチャとパディングの書き込み (ROM+MBC5+RAM: -m 0x1A)
                "&& rgbfix -v -p 0xFF -C -m 0x1A a_mtask.gb",
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": []
        }
    ]
}

変更が完了したら、ファイルを閉じる。
VS Code: メニュー > ファイル > エディターを閉じる Ctrl+F4 →
3-2. ひな形からデバッグの構成を記述する。
エミュレータ起動 / 接続の設定 (VS Code の「デバッグの実行」を選択すると呼ばれる)

VS Code: メニュー > 実行 > 構成の追加...
検索: Emulicious Debugger ← 選択
生成された .vscode\launch.json にデバッグ情報を記述する。
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "emulicious-debugger",
            "request": "launch",
            "name": "Launch in Emulicious",
            "program": "${workspaceFolder}/a_mtask.gb",
            "port": 58870,
            "stopOnEntry": true
        }
    ]
}

変更が完了したら、ファイルを閉じる。
VS Code: メニュー > ファイル > エディターを閉じる Ctrl+F4 →


4. アセンブラのプログラムを作成

プログラムを作成する。

VS Code: メニュー > ファイル > 新しいファイル Ctrl+Alt+Win+N
a_mtask.asm
プロジェクトディレクトリの直下に作成する。(.vscode\ ではない)
a_mtask.asm (ソースコードデバッグはメインの *.gb と *.asm の名称を合わせる必要がある)
INCLUDE "hardware.inc"


; 割り込みベクタ 5 種類 SECTION "Vector", ROM0[$0000] ds $0040 - @ ; 63bytes ここにプログラムを格納しない(できるけどしない) SECTION "VBlank", ROM0[$0040] reti ds $0048 - @ ; 7Bytes SECTION "Stat", ROM0[$0048] reti ds $0050 - @ ; 7Bytes SECTION "Timer", ROM0[$0050] jp task_switcher ; -> Timer 処理へ ds $0058 - @ ; 5Bytes SECTION "Serial", ROM0[$0058] reti ds $0060 - @ ; 7Bytes SECTION "Joypad", ROM0[$0060] reti ds $0100 - @ ; 160bytes ここにプログラムを格納しない(できるけどしない)
; --- カートリッジのエントリ (本体タイトル表示の後に有効化) --- SECTION "Entry", ROM0[$0100] ; 自作プログラムは $0100 から記述する nop jp Start ; --- カートリッジヘッダ (0104h-014Fh) rgbfix がここを埋める --- ds $0150 - @ ; --- メインプログラム --- Start: di ; 割り込み禁止 ld sp, $FFFF ; 初期化時だけ HRAM を使用 (PUSH はプリデクリメント) ; タイマオーバーフロー時の再初期化値を設定 xor a ld [rTMA], a ; [FF06] <- $00 ; タイマスタート ld a, TAC_START | TAC_4KHZ ld [rTAC], a ; [FF07] <- TimerStart | 256 M-cycle ; タイマ割り込みを許可 ld a, IE_TIMER ld [rIE], a ; [FFFF] <- EnableINT ; タスクテーブルに Task0 (当該シーケンス) を登録 ld bc, $0000 ; 最初の戻り先は必ず上書きされるためテキトーで良い ld hl, Task0_StackEnd ld de, Task0_SP call task_Init ; タスクテーブルに Task1 を登録 ld bc, Task1_Entry ld hl, Task1_StackEnd ld de, Task1_SP call task_Init ; SP を Task0 用に変更 ld sp, Task0_StackEnd ei ; 割り込み許可
; タスク 0 Task0_Entry: ld a, [Task0_CNT] inc a ld [Task0_CNT], a halt ; 省電力対策 jr Task0_Entry ; タスク 1 Task1_Entry: ld a, [Task1_CNT] inc a ld [Task1_CNT], a halt ; 省電力対策 jr Task1_Entry
; --- タスクテーブルの登録 --- task_Init: dec hl ; Task1_StackEnd に戻り先アドレス (PC) を積む ld a, b ; HIGH (アドレスの PUSH だから H->L の順: Low,High のフェッチでジャンプ) ld [hld], a ld a, c ; LOW ld [hld], a ; 初期レジスタ (AF,BC,DE,HL) のダミーを積む ld b, 8 xor a .clear_regs: ld [hld], a dec b jr nz, .clear_regs ; 偽造したスタックを保存 inc hl ; POP はポストデクリメントなので +1 しておく ld a, l ld [de], a inc de ld a, h ld [de], a ret
; --- タスクスイッチャ --- task_switcher: ; 現タスクのレジスタを保存 push af push bc push de push hl ; HL <- 現在の SP ld hl, $0000 add hl, sp ; 現在 SP の保存処理へ飛ぶ ld a, [CurrentTaskID] and a jr nz, .save_task1 .save_task0: ; 現在の SP を保存 (Task0) ld a, l ld [Task0_SP], a ld a, h ld [Task0_SP + 1], a jr .switch_logic .save_task1: ; 現在の SP を保存 (Task1) ld a, l ld [Task1_SP], a ld a, h ld [Task1_SP + 1], a .switch_logic: ; TaskID を反転 (0->1, 1->0) ld a, [CurrentTaskID] xor 1 ld [CurrentTaskID], a ; 次のタスクの復帰へ飛ぶ and a jr nz, .load_task1 .load_task0: ; 次のタスクの SP をセット (Task0) ld a, [Task0_SP] ld l, a ld a, [Task0_SP + 1] ld h, a jr .do_switch .load_task1: ; 次のタスクの SP をセット (Task1) ld a, [Task1_SP] ld l, a ld a, [Task1_SP + 1] ld h, a .do_switch: ; 次のタスクのレジスタを復帰 ld sp, hl pop hl pop de pop bc pop af reti ; SP の切り替えによって、別のタスクへ戻る
; --- RAM の定義 --- SECTION "Task_Control", WRAM0[$C000] ; WRAM バンク 0 として $C000 に配置 CurrentTaskID: db ; 現在のタスク番号 (0 or 1) Task0_CNT: db ; ダミーカウンタ Task1_CNT: db ; ダミーカウンタ Task0_SP: dw ; タスク 0 の保存用 SP Task1_SP: dw ; タスク 1 の保存用 SP SECTION "Task_Stacks", WRAM0[$C010] ; WRAM バンク 0 として $C010 に配置 Task0_Stack: ds 64 ; タスク 0 用スタック Task0_StackEnd: Task1_Stack: ds 64 ; タスク 1 用スタック Task1_StackEnd:

記述したら、ファイルを閉じる。
VS Code: メニュー > ファイル > エディターを閉じる Ctrl+F4 →


5. アセンブルとデバッグ

5-1. ソースをアセンブルする。
VS Code: メニュー > ターミナル > ビルドタスクの実行... Ctrl+Shift+B
→ 上記 3-1 で定義した tasks.json が実行される。
5-2. デバッグの開始
VS Code: メニュー > 実行 > デバッグの開始 F5
→ 上記 3-2 で定義した launch.json が実行される。

Emulicious が起動し、VS Code の画面にはアセンブラのソース画面が表示される。
このとき、(上記 launch.json の  "stopOnEntry": true  により)  $0100  nop  で実行が停止する。
5-3. デバッグの続行
上記 5-2 でプログラムが停止しているので、実行を再開する。

VS Code: メニュー > 実行 > 続行 F5
5-4. 実行の確認
Emulicious: メニュー > Tools > Debugger F1
 Memory タブ - RAM タブ

タスク管理用メモリが頻繁に変更されていることを確認する。
タス
クID
ダミ
ー0
ダミ
ー1
SP0 SP1 不使用
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
C000  00 0000 0000 0000 00 00 00 00 00 00 00 00 00