Skip to content

Latest commit

 

History

History
532 lines (434 loc) · 29.2 KB

relm_sdk_j.md

File metadata and controls

532 lines (434 loc) · 29.2 KB

ReLM開発環境

開発言語

  • Python
    • アプリケーションをPython上のDSLで記述して、ReLMアーキテクチャ用のバイナリコードを生成します。
    • 開発中は回路を固定してソフトウェアのみを更新しますので、煩わしい論理合成が不要になります。
      • Intel(ALTERA)環境では、USB BlasterをDLL呼び出しで使用します。
    • なるべく新しいバージョンが望ましいですが、:= 演算子を使用しますのでPython 3.8以降になります。
  • Verilog HDL
    • FPGA用の論理合成に使用します。
    • デバイス接続用のカスタムロジックや、カスタム拡張命令の記述にも使用します。

開発ツール

  • Quartus Prime
    • Intel(ALTERA) FPGA用の論理合成に必要です。
    • Pythonからの通信のため、付属のUSB Blasterのドライバーもインストールする必要があります。
    • FPGAの規模によりますが、基本的にLite Editionで問題ありません。
  • Visual Studio Code(推奨)
    • プラグインでPythonとVerilog HDLどちらも対応可能です。
    • Pythonコードの起動が比較的簡単です。
    • GitHub Copilotが利用可能です。
      • ReLM関連のコード例が増えれば予測精度が向上するかもしれません。

ターゲットFPGAボード

  • Terasic DE0-CV Board

    • FPGA: Cyclone V 5CEBA4F23C7N
    • 8コア構成
      • 16コア構成も可能だが余裕が無い
    • PS/2 port
    • VGA output
    • 残念ながら生産中止なので、次期ターゲットに切り替え予定
  • Terasic Cyclone V GX Starter Kit

    • FPGA: Cyclone V GX 5CGXFC5C6F27C7N
    • 16コア構成(予定)
    • HDMI output
    • Audio input & output
    • Arduino header
      • USB Host Shield経由でUSBデバイス対応予定
    • Micro SD Card
      • AIアプリケーションに使用予定
    • 次期メインターゲット予定

ファイル構成

  • relm.v
    ReLMアーキテクチャ基本回路実装
  • relm_custom_none.v
    カスタム命令無し
  • relm_custom_div.v
    カスタム命令(整数除算)
  • relm_custom_div_fp.v
    カスタム命令(整数除算および浮動小数点演算)
  • relm.py
    Python DSL基本ライブラリ
  • relm_jtag.py
    USB Blaster通信関連
  • de0cv (folder)
    DE0-CVターゲット関連
    • relm_de0cv.py
      DE0-CVデバイス対応(ローダー生成)
    • relm_font.py
      コンソール出力関連
    • relm_de0cv.v
      DE0-CVトップモジュール
    • relm_de0cv.sdc
      クロック周波数指定
    • loader (folder)
      開発時ローダー関連
      • relm_de0cv.qpf
        Quartus Primeプロジェクト(ローダー構成)
      • relm_de0cv.qsf
        Quartus Primeプロジェクト設定
      • relm_test_led.py
        LEDテスト(ローダー実行)
      • relm_test_ps2.py
        PS/2通信テスト(ローダー実行)
      • relm_test_vga.py
        VGA表示テスト(ローダー実行)
    • bubble (folder)
      Playable PoC ゲームアプリ関連
      • relm_bubble.py
        Playable PoC サンプルゲームアプリ 『Bubble Estate』
      • relm_bubble_release.py
        スタンドアロン構成リリース用メモリイメージ生成
      • relm_de0cv.qpf
        Quartus Primeプロジェクト(スタンドアロン構成)
      • relm_de0cv.qsf
        Quartus Primeプロジェクト設定

ローダー構成準備

DE0-CVターゲットを例に開発作業手順を説明していきます。

de0cvフォルダ内の relm_de0cv.py を実行すると、de0cv/loaderフォルダ内にコードデータ出力code??.txtとdata??.txtが作成されます。

コードデータ出力と同じフォルダにあるQuartus Primeのプロジェクトファイル relm_de0cv.qpf を開いて論理合成すると、プログラムローダーを実装したReLM環境が生成されます。(必要な設定は既にrelm_de0cv.qsf内に保存)

生成されたReLM環境はloader/output_filesフォルダに以下のFPGAコンフィグレーション用ファイルとして保存されます。

  • relm_de0cv.sof
    Quartus PrimeでFPGAをコンフィグレーションするためのSOFファイル
  • relm_de0cv.pof
    Quartus Primeでボード上のROMに回路を書き込んで電源投入時にコンフィグレーションするためのPOFファイル
  • relm_de0cv.svf
    ReLM環境で直接コンフィグレーションするためのSVFファイル

SOFファイルは最も標準的なコンフィグレーション用ファイルですが、毎回Quartus Primeを使用する必要がありますのでReLM環境上での開発作業では若干不便です。

ReLM環境で最も使いやすいのはSVFファイルによるコンフィグレーションで、ボード上のROMに全く別の回路が入っていたとしても、自動的にリセットを掛けてReLMローダー環境を読み込んでの実行が可能なので、とても便利です。(ボード上のROMの内容は変更しない)

SVFファイルによる起動ではコンフィグレーションに数秒ほど待たされるので、作業が佳境に入ってくると若干効率を上げたくなるかもしれません。

そういった場合、POFファイルを使ってボード上のROMを書き換えると、電源投入ですぐにローダー環境が立ち上がるので起動時の待ち時間が短縮されます。(POFファイルによるROMの書き換えに関しては、FPGAボードの説明書を参照)

POFファイルはローダー環境以外にも、ReLM環境で開発したアプリケーションをホストPC無しでも起動できるようにする、スタンドアロン構成のリリース(ROMへの書き込み)にも利用されます。

ReLMの起動シーケンス

コードデータ出力時のデバッグ用ダンプ出力を利用して、電源投入時の起動シーケンスについて説明します。

DE0-CVターゲットは8コア構成なので、最初の8ワードは全8スレッドのエントリーポイントになります。

0000:   JUMP    0000:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0001:   JUMP    0001:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0002:   JUMP    0002:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0003:   JUMP    0003:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0004:   JUMP    0004:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0005:   JUMP    0005:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0006:   JUMP    0006:                   202:    with ReLMLoader(__file__, "..", "loader", release_loader=True):
0007:   JUMP    0008:                   126:    Loader[

0番地から6番地までは同じ番地へのジャンプ命令となりますので、最初の7スレッドは休止状態となります。

7番地は8番地へのジャンプとなり、8番地からはプログラムローダーのコードが続きますので、スレッドの1つを使用してプログラムローダーを実行していることがわかります。

0008:   IN      JTAG                    128:    Out("PUTOP", In("JTAG")),
0009:   BLOAD   PUTOP                   128:    Out("PUTOP", In("JTAG")),
000A:   OPB     OUT                     128:    Out("PUTOP", In("JTAG")),
000B:   OPB     LOAD                    129:    If(RegB & 0xC00000 != 0)[
000C:   AND     00C00000                129:    If(RegB & 0xC00000 != 0)[
000D:   JEQ     0013:                   129:    If(RegB & 0xC00000 != 0)[
000E:   AND     00800000                130:    (Acc & 0x800000).opb("JEQ"),
000F:   OPB     JEQ                     130:    (Acc & 0x800000).opb("JEQ"),
0010:   LOAD    00000000                131:    (+operand).opb("PUT"),
0011:   OPB     PUT                     131:    (+operand).opb("PUT"),
0012:   JUMP    0008:                   132:    Continue(),
0013:   LOAD    00000000                134:    operand(operand << 16 | RegB),
0014:   MUL     00010000                134:    operand(operand << 16 | RegB),
0015:   OPB     OR                      134:    operand(operand << 16 | RegB),
0016:   PUT     0010:                   131:    ->      (+operand).opb("PUT"),
0017:   PUT     0013:                   134:    ->      operand(operand << 16 | RegB),
0018:   JUMP    0008:                       DoLoop

左側のアセンブリコードに対し、右側はソースコードのどの行に対応するかを示しています。

プログラムローダー部分のソースコード(relm_de0cv.py)は以下になりますが、まず"JTAG"からデータを入力して"PUTOP"に出力していることがわかります。

operand = Int()
Loader[
    Do()[
        Out("PUTOP", In("JTAG")),
        If(RegB & 0xC00000 != 0)[
            (Acc & 0x800000).opb("JEQ"),
            (+operand).opb("PUT"),
            Continue(),
        ],
        operand(operand << 16 | RegB),
    ],
]
del operand

この"PUTOP"というのはプログラムローダーが使用する命令コード(OpCode)書き換え用デバイスのポート番号で、開発終了後のリリース時には削除される機能です。

アセンブリコードの可読性を上げるため、I/Oポート番号や命令コードは文字列で指定して、メモリに書き込む際に実際の数値のコードに変換されます。

RegBやAccはReLMアーキテクチャのレジスタに直接アクセスするためのintrinsicですが、これはプログラムローダーのコードを極力コンパクトにするための最適化で、通常のアプリケーションレベルでは必ずしも使う必要はありません。

先程作成したコードデータ出力は、このプログラムローダーだけが起動している状態のメモリーイメージになります。

FPGA上のReLM環境でプログラムローダーが起動していると、任意の番地の命令コード(OpCode)とオペランド(Operand)を書き換えられますので、ホストPCからJTAG経由でローダー直後の番地(0x19)からプログラムコードを書き込み、最後にスレッドエントリー(0番地~7番地)のオペランドを書き換えるとスレッドを起動することができます。

このプログラムローダー環境を利用することで、煩わしい論理合成をせずにアプリケーションソフトウェアの開発作業を進めることが可能になります。

loaderフォルダ内に動作確認用のサンプルコードがありますので、これらのコードを使ってPythonコードによるアプリケーション記述について説明します。

動作確認: LED点灯およびボタン入力(relm_test_led.py)

FPGAボードを接続した状態で、まずrelm_test_led.pyを実行してみてください。

コンソール出力を表示
0000:   JUMP    0019:                   9:      Thread[
0001:   JUMP    0001:                   8:      with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
0002:   JUMP    0002:                   8:      with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
0003:   JUMP    0003:                   8:      with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
0004:   JUMP    0004:                   8:      with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
0005:   JUMP    0005:                   8:      with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
0006:   JUMP    0006:                   8:      with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
0007:   JUMP    0008:                   6:      from relm_de0cv import *
0008:   IN      JTAG                    6:      from relm_de0cv import *
0009:   BLOAD   PUTOP                   6:      from relm_de0cv import *
000A:   OPB     OUT                     6:      from relm_de0cv import *
000B:   OPB     LOAD                    6:      from relm_de0cv import *
000C:   AND     00C00000                6:      from relm_de0cv import *
000D:   JEQ     0013:                   6:      from relm_de0cv import *
000E:   AND     00800000                6:      from relm_de0cv import *
000F:   OPB     JEQ                     6:      from relm_de0cv import *
0010:   LOAD    00000000                6:      from relm_de0cv import *
0011:   OPB     PUT                     6:      from relm_de0cv import *
0012:   JUMP    0008:                   6:      from relm_de0cv import *
0013:   LOAD    00000000                6:      from relm_de0cv import *
0014:   MUL     00010000                6:      from relm_de0cv import *
0015:   OPB     OR                      6:      from relm_de0cv import *
0016:   PUT     0010:                   6:      ->      from relm_de0cv import *
0017:   PUT     0013:                   6:      ->      from relm_de0cv import *
0018:   JUMP    0008:                       DoLoop
0019:   LOAD    LED1                    10:     LED(
001A:   OUT     7DDB4B4B                10:     LED(
001B:   LOAD    LED0                    10:     LED(
001C:   OUT     EF030000                10:     LED(
001D:   IN      KEY                     19:     key := Int(In("KEY")),
001E:   PUT     0020:                   20:     ->      Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
001F:   PUT     0022:                   20:     ->      Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0020:   LOAD    00000000                20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0021:   AND     0000001F                20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0022:   BLOAD   00000000                20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0023:   SAR     00000020                20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0024:   OPB     XOR                     20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0025:   MUL     00000002                20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0026:   ADD     00000001                20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0027:   BLOAD   LED0                    20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0028:   OPB     OUT                     20:     Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
0029:   JUMP    001D:                       DoLoop
SVF configuration: loader/output_files/relm_de0cv.svf
IDCODE: 2B050DD
..................................................
.
Loading instructions...
IDCODE: 2B050DD
42 / 65536 instructions (0.1 % used)

起動に成功するとPCのコンソールに上のダンプ出力が表示され、FPGAボードではLEDに HELLO_ と表示されてボタンを押したりスイッチを変更したりすると、LEDが連動して変化するようになります。

残念ながらローダー部分のソースコードはダンプ出力に表示されなくなりますが、0x19番地からユーザーコードが読み込まれ、0番地から起動されていることが読み取れます。

Pythonのソースコードは以下になります。

with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
    Thread[
        LED(
            hex5=0b0111110,  # H
            hex4=0b1101101,  # E
            hex3=0b0100101,  # L
            hex2=0b0100101,  # L
            hex1=0b1110111,  # O
            hex0=0b0000001,  # _
        ),
        Do()[
            key := Int(In("KEY")),
            Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
        ],
    ]

ReLMLoaderのloaderオプションで実行構成を選択できますが、ここではSVFファイルの相対パスを指定しています。

loaderにSVFファイルを指定すると、ボードのリセットとによるローダー環境のコンフィグレーションを実行した上で、さらにこのローダー環境を利用したプログラムのロードと起動を行います。

なお、SVFファイル指定の相対パスはrelm_de0cv.pyがあるフォルダde0cvが基準となります。(アプリケーションコードのフォルダではない)

loader=Trueと指定するかSVFファイルを削除(警告メッセージが出る)すると、FPGAボード上のROMにローダー環境が既に保存されている場合、SVFファイルによるローダー環境のコンフィギュレーションをせずにROM上のローダー環境を利用してプログラムをロードするようになります。

load=Falseとすると、プログラムのロードを行わずダンプ出力のみを表示します。

このオプションではFPGAボードを使用しないので、プログラムがまだ未完成でダンプ出力によるチェックのみをしたい場合等、ボード未接続でも問題なく利用可能です。

ReLMのアプリケーションコードは、基本的にReLMLoaderのwithブロック内に記述します。

FPGAに送信されるコードは Thread[ ] または Define[ ] ブロックに記述しますが、Thread[ ] ブロック毎にスレッドが1つ起動します。

後から参照される配列データや関数オブジェクトの定義は Define[ ] ブロック内に記述します。

通常のPython文法内でドメイン固有言語を構築する都合上、コードブロックは配列の要素の様にカンマ区切りの式の列として記述されます。

かなり見慣れない表記になりますので、コード整形ツールの導入をお勧めいたします。(私はVisual Studio Code環境でBlack Formatterを使用)

この表記の都合上、全ての実行文は式としても評価できる必要があります。

Pythonの通常の代入文 [変数名 = 式] は式としては評価できないので、変数定義の場合は:= 演算子[変数名 := 式] 定義をしない既存変数への代入は [変数名(式)] の様に括弧つき表記になります。

上の例では以下のPythonコードが key という符号付き整数型(Int)変数の定義と初期化の処理になります。

            key := Int(In("KEY")),

このアセンブリコード出力は以下の様に、I/Oポートからのボタン状態の取得と、PUT命令2つに展開されます。

001D:   IN      KEY                     19:     key := Int(In("KEY")),
001E:   PUT     0020:                   20:     ->      Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),
001F:   PUT     0022:                   20:     ->      Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),

ここでPUT命令が2つあるのは key の参照が2箇所あるためで、Int型変数の実体は、実は全ての参照箇所のオペランドとなります。

            Out("LED0", ((key & 0b11111) ^ (key >> 5)) * 2 + 1),

プログラムローダーのコードにも出てきましたが、Do()[ ブロック ] は無限ループになります。

Do()[ ブロック ].While( 条件式 ) で脱出条件を付けると通常のdo-while文になります。

動作確認: PS/2キーボード入力(relm_test_ps2.py)

念のため、FPGAボードの電源を切った状態でPS/2ポートにキーボードを接続してください。

FPGAボードの電源を入れて、relm_test_ps2.pyを実行してください。

起動に成功するとLEDに PS2PS2 と表示され、キーボード上のLEDが点灯し、キーボードを押すとキーコードに応じてLEDが点滅するようになります。

Pythonコードは以下の様になります。

コードを表示
with ReLMLoader(loader="loader/output_files/relm_de0cv.svf"):
    Define[
        # sendPS2 := SendPS2(),
        sendPS2 := Function(data := Int())[
            IO("PS2", 0x80000001),
            Acc(1 - (-100 * 1000) // (20 * ReLM.ncpu)),  # 100us
            Do()[...].While(Acc - 1 != 0),
            IO("PS2", 0x40000000),
            data & 0xFF,
            IO("PS2", 0x0, load="BLOAD"),
            IO("PS2", RegB | 0x40000000),
            IO("PS2", RegB),
            IO("PS2", (RegB >> 1) | 0x40000000),
            IO("PS2", RegB >> 1),
            IO("PS2", (RegB >> 2) | 0x40000000),
            IO("PS2", RegB >> 2),
            IO("PS2", (RegB >> 3) | 0x40000000),
            IO("PS2", RegB >> 3),
            IO("PS2", (RegB >> 4) | 0x40000000),
            IO("PS2", RegB >> 4),
            IO("PS2", (RegB >> 5) | 0x40000000),
            IO("PS2", RegB >> 5),
            IO("PS2", (RegB >> 6) | 0x40000000),
            IO("PS2", RegB >> 6),
            IO("PS2", (RegB >> 7) | 0x40000000),
            IO("PS2", RegB >> 7),
            RegB((RegB >> 1) ^ RegB),
            RegB((RegB >> 2) ^ RegB),
            RegB((RegB >> 4) ^ RegB),
            IO("PS2", RegB ^ 0x40000001),
            IO("PS2", RegB ^ 0x1),
            IO("PS2", 0x40000001),
            IO("PS2", 0x1),
            IO("PS2", 0x40000001),
        ],
        # recvPS2 := RecvPS2(),
        recvPS2 := Function()[
            Do()[IO("PS2", 0x40000001),].While(IO("PS2", 0x1) != 0),
            IO("PS2", 0x40000001),
            IO("PS2", 0x1),
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 1) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 2) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 3) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 4) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 5) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 6) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            (IO("PS2", 0x1) << 7) | RegB,
            IO("PS2", 0x40000001, load="BLOAD"),
            IO("PS2", 0x1),
            IO("PS2", 0x40000001),
            IO("PS2", 0x1),
            IO("PS2", 0x40000001),
        ].Return(RegB),
    ]
    Thread[
        LED(
            hex5=0b1111100,  # P
            hex4=0b1101011,  # S
            hex3=0b1011101,  # 2
            hex2=0b1111100,  # P
            hex1=0b1101011,  # S
            hex0=0b1011101,  # 2
        ),
        sendPS2(0xED),  # Keyboard LED command
        sendPS2(0x7),  # Keyboard LED command
        # sendPS2(0xF4),  # Mouse Enable
        Do()[Out("LED0", recvPS2() * 2 + 1),],
    ]

Thread[ ] ブロックの前に Define[ ] ブロックで関数オブジェクト(Function)を2つ定義しています。

このように、スレッドを起動せずに後で参照される関数や配列といったオブジェクトを定義する場合、Define[ ] ブロックを使用します。

PS/2ポートにデータを送出するsendPS2関数定義の冒頭は以下になります。

        sendPS2 := Function(data := Int())[
            IO("PS2", 0x80000001),
            Acc(1 - (-100 * 1000) // (20 * ReLM.ncpu)),  # 100us
            Do()[...].While(Acc - 1 != 0),

関数定義では、パラメータにInt型変数の定義を入れて、続く関数本体でパラメータを参照するコードを記述します。

ここではPS/2の送信開始時にクロック信号をLに落として100マイクロ秒待機する処理ですが、待ち時間用のループ数計算は通常のPython式で計算して、結果をアキュムレータ(Acc)に代入しています。

続くDo-Whileループの本体にEllipsis(...)が入っていますが、これはPythonの文法上ブロック内を空にできないので、ダミーのオブジェクトで見た目の違和感が少ないものを置いています。

実はブロック内でStatement型(relm.pyで定義)以外のオブジェクトを置いてもコード生成の際は無視されますので、Noneや0といった数値を置いても同じ効果になります。(数値だけを置いても何も起こらないので、アキュムレータに代入したい場合 Acc(数値) とする)

以下は関数呼び出し側のコードですが、

        sendPS2(0xED),  # Keyboard LED command

この部分のアセンブリコード出力は以下の様になり、パラメータ変数への代入と、戻り先番地(以下では0xB4)をアキュムレータに入れて関数のエントリーポイントにジャンプするコードに展開されます。

00BC:   LOAD    000000ED                78:     sendPS2(0xED),  # Keyboard LED command
00BD:   PUT     0021:                   16:     ->      data & 0xFF,
00BE:   LOAD    000000B4                78:     sendPS2(0xED),  # Keyboard LED command
00BF:   JUMP    0019:                   78:     sendPS2(0xED),  # Keyboard LED command

関数の戻り先番地は回転待ちのペナルティを抑えるために最適化されますので、必ずしも連続したコード配置にはなりません。

recvPS2関数は本体の末尾で結果を返していますので、

        recvPS2 := Function()[
        ...
        ].Return(RegB),

式の中で呼び出して結果を参照することができます。

        Do()[Out("LED0", recvPS2() * 2 + 1),],

動作確認: VGAディスプレイ出力(relm_test_vga.py)

relm_test_vga.pyはVGAで画面に文字を出力する比較的複雑なサンプルです。

relm_test_vga.jpg

最初のスレッドはVRAMの実装です。

    Thread[
        Acc("VGA"),
        Do()[vram := Array(*([0xFEDCBA98] * 80 * 480))],
    ]

VRAMとして配列(Array)オブジェクトを定義していますが、その実体は連続したPUSH命令です。

アキュムレータにVGA出力用FIFOのポート番号を入れてPUSH命令のループを回し続けることで、映像信号を生成することができるようになります。

DE0-CVターゲットのコードメモリは65536ワードなので、VRAMがこれに収まるようにピクセル当たり4ビット(16色)のインデックスカラーを採用しています。

それでもVGAで 640 x 480 ピクセルの場合、80 x 480 = 38400 ワードが必要になりますので、コードメモリの半分以上をVRAMが占めることになります。

Arrayオブジェクトはこのように巨大になる可能性がありますので、デバッグ用ダンプ出力は先頭10要素までで抑止されます。

次にコンソールオブジェクトとサービススレッドの起動です。

    console = Console(vram, 80, FIFO.Alloc(), FIFO.Alloc())
    Thread[console.Service()]

FIFO.Alloc() はFIFOの割り当てで、フォントデータの通信用とメインスレッドとの通信用の2本割り当てます。

サービススレッドの実装部分はrelm_font.pyにあります。

    def Service(self) -> Block:
        return Block[
            pos := Int(),
            color_fg := Int(),
            color_bg := Int(),
            self.fifo_font.Lock(),
            self.fifo_print.Lock(),
            Do()[
                If(RegB(self.fifo_print.Pop(), 0x80000000).opb("AND") == 0)[
                    text := Int(RegB),
                    While(text != 0)[
                        self.PutChar(pos, text & 0x7F, color_fg, color_bg),
                        pos(pos + 1),
                        text(text >> 8),
                    ],
                    Continue(),
                ],
                If(RegB & 0x40000000 == 0)[pos(RegB & 0x3FFFFFFF), Continue()],
                bg := Int(RegB & 0xF),
                color_bg(Acc * 0x11111111),
                color_fg(((RegB & 0xF0) >> 4) - bg),
            ],
        ]

必要に応じてコードを展開したい場合、このようにBlockオブジェクトやFunctionオブジェクトを返すPython関数を用意して、ThreadブロックやDefineブロックの中で呼び出します。

以下はメインスレッドでのカラーパレット設定と文字列出力処理の一部になります。

        Out("VGAPAL", *[i * 0x1111 for i in range(16)]),
        console.Print(" !\"#$%&'()*+,-./0123456789:;<=>?", pos=0, color=0xF0),

文字列出力時のVRAMへのフォント書き込みと、FIFO出力のためのVRAM読み出しは、それぞれ別々のスレッドで実行されますが、意図的に排他処理をしなくても並列処理が可能です。