0. 前置き
0-1. マルチタスクとは
「マルチタスク」という言葉を聞いたことがあると思います。0-2. ジョブ
忍者が使う分身の術は、
ということを一人で繰り返して、あたかも挟み撃ちにしたかのように見せかけますが、(修練を積んだ優秀な忍者なら必ずできる。…らしい)
- 敵の左側で停止 → 敵の右側へ素早く移動
- 敵の右側で停止 → 敵の左側へ素早く移動
これと同じことを実施するのが「マルチタスク」です。
コンピュータでは一つしかない CPU で、時間を区切って複数のタスクを処理します。
マルチコアやスレッド、果ては仮想 CPU など、コンピュータには様々な CPU の形態がありますが、基本はすべて同じです。
最近では気取った働き方を「マルチタスク」と呼んだりしますが、元々はコンピュータ用語です。
コンピュータと同じく、人間が一人しかいないのに複数の仕事を一度に処理しますが、実際にそんなことは出来ません。
できたとしても、非常に質の悪い結果を出してしまうか、一つの仕事にかかる労力が軽いものしか扱えません。
コンピュータの「マルチタスク」は、特定のタイミングごとに CPU の中身をすべて入れ替えてタスクを処理していきます。
特定のタイミングとは、おもに割り込み信号によって引き起こされます。
タイマ割り込み、キー入力割り込み、画像信号割り込み、通信終了割り込みなど、
コンピュータに搭載されている機器や仕組みにより、様々な種類の割り込みがあります。
「タスク」に似た言葉として「ジョブ」があります。0-3. タスクスイッチャ
ジョブは、人間が必要とする一連の流れを指して呼びます。
例えば「歩く」は右足と左足を交互に出すことですが、これは「ジョブ」に分類されます。
これに対し「右足を出す」「左足を出す」がそれぞれ「タスク」に該当します。
コンピュータは連続したタスクを処理することしかできません。
人間が終了の条件を決定し、タスクがその条件に達したときにジョブが終了します。
「タスクスイッチャ」とは、複数存在するタスクを次々と切り替えるプログラムのことです。0-4. エミュレータとデバッガで動作確認
本稿ではゲームボーイの CPU を利用し、マルチタスクの基本となるタスクスイッチャをタイマ割り込みで実装します。
(説明用にプログラムを単純化するため、タスク数を 2 に限定しています。この数を増やしてみるのも面白いと思います)
ノイマン型の CPU なら何を使ってもタスクスイッチャを作ることができます。
ゲームボーイの CPU 「sm83(LR35902)」はレジスタが少ないため、学習用のタスクスイッチャを作りやすくなっています。
(本来の目的であるゲーム制作には不必要な論理思考が要求されるのですが…)
また、sm83 は i8080 と Z80 を合わせたような CPU なので、組み込みシステムへの応用もできます。
ただし、アクションゲームのように高速性を第一に要求されるような処理にマルチタスクは向きません。
このプログラムは実機でも動作しますが、このままでは実機での確認ができません。
動作確認はエミュレータとデバッガで実施してください。
画面表示は VBlank 中に行う必要があるため、表示処理を組み込むとプログラムが複雑になりタスクスイッチャが分かりにくくなります。
タスクスイッチャの原理が理解出来たら、前述の「ゲームボーイで文字列表示」を組み込んで実機での動作確認を実施してください。
1. メモリマップ
ROM
割り込みベクタ
割り込みが引き起こされたときの命令を記述する場所。アドレスは変更できない。
VBlank 不使用 Stat 不使用 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0040 00000000000000000000000000000000
Timer → タスクスイッチャへ Serial 不使用 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0050 00 00 00 00000000000000000000000000
Joypad 不使用 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0060 00000000000000000000000000000000
プログラムエントリ
ゲームボーイではプログラムエントリのアドレスは $0100 と決まっている。アドレスは変更できない。
続いてすぐ後にカートリッジヘッダ (固定値) が入る。
エントリ カートリッジヘッダ … +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0100 00 C3 50 01 000000000000000000000000
プログラム本体
メインプログラム … +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ダミ
ー1SP0 SP1 不使用 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F C000 00 00 00 0000 0000 000000000000000000
スタック(タスク 0 用) +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F C010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C040 00 00 00 00 00 00 HL
0000DE
0000BC
0000AF
0000ret
0000
スタック(タスク 1 用) +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F C050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C080 00 00 00 00 00 00 HL
0000DE
0000BC
0000AF
0000ret
0000
プログラム初期化用
スタック(初期化時) IE +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F FFF0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
2. プロジェクトディレクトリの作成
VS Code ではディレクトリを開くことにより、プロジェクトを切り替える。
2-1. ディレクトリを作成する。
ファイルエクスプローラーでディレクトリを作成する。(プロジェクトディレクトリとする)2-2. ハードウェア定数定義を設置する。
C:\Users\who\vsc-gba-mtask\
GitHub から hardware.inc をダウンロードし、作成したディレクトリに設置する。2-3. プロジェクトディレクトリとしてディレクトリを開く。
(ダウンロード方法がわかりにくい)
直接ダウンロードするのはこちらから。
VS Code: メニュー > ファイル > フォルダーを開く (Ctrl+K Ctrl+O)
(上記プロジェクトディレクトリ : C:\Users\who\vsc-gba-mtask\)
→ このディレクトリの中にあとで .vscode\ が作られる。「信頼します」ボタンをクリックする。
このフォルダー内のファイルの作成者を信頼しますか?
3. アセンブラの設定
上記 2 の操作のあとに実行する。
3-1. ひな形からビルド情報を記述する。(アセンブラ)VS Code: メニュー > ターミナル > タスクの構成...3-2. ひな形からデバッグの構成を記述する。
生成された .vscode\tasks.json にビルド情報を記述する。
検索: テンプレートから tasks.json を生成 ← 選択 Others 任意の外部コマンドを実行する例 ← 選択
{ // 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 →
エミュレータ起動 / 接続の設定 (VS Code の「デバッグの実行」を選択すると呼ばれる)
VS Code: メニュー > 実行 > 構成の追加...
生成された .vscode\launch.json にデバッグ情報を記述する。
検索: Emulicious Debugger ← 選択
{ // 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 (ソースコードデバッグはメインの *.gb と *.asm の名称を合わせる必要がある)プロジェクトディレクトリの直下に作成する。(.vscode\ ではない)
a_mtask.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+B5-2. デバッグの開始
→ 上記 3-1 で定義した tasks.json が実行される。
VS Code: メニュー > 実行 > デバッグの開始 F55-3. デバッグの続行
→ 上記 3-2 で定義した launch.json が実行される。
Emulicious が起動し、VS Code の画面にはアセンブラのソース画面が表示される。
このとき、(上記 launch.json の "stopOnEntry": true により) $0100 の nop で実行が停止する。
上記 5-2 でプログラムが停止しているので、実行を再開する。5-4. 実行の確認
VS Code: メニュー > 実行 > 続行 F5
Emulicious: メニュー > Tools > Debugger F1
Memory タブ - RAM タブ
タスク管理用メモリが頻繁に変更されていることを確認する。
タス
クIDダミ
ー0ダミ
ー1SP0 SP1 不使用 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F C000 00 00 00 0000 0000 000000000000000000