PicoのMicroPythonにscanvideoのVGA出力機能を追加してみる(WIP)


Raspberry Pi PicoのMicroPythonからscanvideoライブラリを利用できないかと思い、ここしばらく試行錯誤していました。
完成はしていないのですが、休暇も今日で終わりなので現状をメモとして残しておきます。

MicroPythonはCでモジュールを追加できますので、scanvideoを使うことも基本的には可能なはずです。
Cで書いたモジュールを追加するには、
(1)MicroPythonのport/rp2を直接編集する
(2)Exnternal C moduleとして実装する
(3)ネイティブコードの.mpyファイルを作る
という方法があります。ただし(3)はPicoを含めCortex-M0をサポートしていません(無理やり動かしている人は居るようですが)。

まず(2)をちょっと試しましたが、pico-extrasライブラリを外部Cモジュールから使おうとすると、慣れないcmakeの知識が必要になり、面倒くさくなって(1)の方法を取ることにしました。この場合は、MicroPythonのCMakeLists.txtがすでにpico-sdkを使えるように設定しているので、それを真似てpico-extrasを使えるようにできます。

次に、scanvideoライブラリ自体も一部修正が必要でした。scanvideoは自分が使うバッファをcallocで確保しています。しかし、MicroPythonはヒープをMicroPython自身が管理するので、Cモジュールはpico-sdkのメモリ管理機能を使ってはいけません。これは、とりあえずバッファをstatic変数として確保することで回避しました。

最後に、scanvideoはPIOを使用しますので、MicroPythonのPIOクラスとぶつかります。これ自体は予想していましたが、どうもそれに加えてscanvideoをリンクした時点で(APIを呼ばなくても)MicroPythonが動作しなくなってしまうようで(原因は不明)、やむなくソースコードからPIOクラス自体を外しました。

これでどうやらscanvideoライブラリを使うCモジュールが作れるようになりました。scanvideoを使うプログラムはこれまでにも作っていますので、あとは淡々と実装・・・と思っていたのですが、まだ何か他にも地雷があるようで、しばらく動かしているとscanvideoが動作しなくなったりMicroPythonごと落ちたりします。挙動からしてメモリ関連ではないかと思いますが、まだ原因を絞れていません。

ちなみに現状の実装は以下のような設計にしています。

scanvideo(mode)
scanvideoシングルトンオブジェクトを返す。modeはクラス変数VGA_MODE_640X480_60またはVGA_MODE_320X240_60を指定する(現在は前者のみサポート)。なお、この関数をコールした時点ではscanvideoの初期化は行わない。
init()
scanvideoライブラリを初期化する。
start()
タイマ割り込みによるscanlineデータ生成関数の呼び出しを開始する。
stop()
タイマ割り込みによるscanlineデータ生成関数の呼び出しを停止する。
test_pattern()
scanlineデータ生成関数として画面に水平線をカラーで描く関数を指定する。パラメータは無し。(この関数と同じ描画をするCのコードはここにあります)
fill(color)
scanlineデータ生成関数として、画面全体を1色で塗りつぶす関数を指定する。colorは16ビット整数。
pixel_from_rgb(r, g, b)
RGB値に対応する16ビット整数を返すクラスメソッド。
bitmap(buffer, fgcolor=0xffdf, bgcolor=0)
scanlineデータ生成関数として、ビットマップを表示する関数を指定する。bufferはビットマップデータで、38400バイト(640/8 * 480)のbytearray形式。fgcolorとbgcolorは、それぞれ前景色と背景色を指定する16ビット整数。
scanline_generation()
scanlineデータ生成関数を実行する。スキャンラインバッファが満杯になったら終了する。タイマ割り込みではなくMicroPythonのwhileループなどでVGA信号を生成することができる。
render_loop()
scanlineデータ生成関数を無限ループで実行する。この関数を呼ぶと無限ループになるので、Core1でのみ実行すべき。

scanvideoで画像を生成するには、scanlineデータを常に生成しつづける必要がありますが、その方法として

(1)タイマ割り込みを使う:start()、stop()
(2)whileループを使う:scanline_generation()
(3)メソッド自体が無限ループを内包する:render_loop()

という3つの方法を用意しています。穏便なのは(1)で、高速なのは(3)です。(2)は実験用ですね。
しかしいずれの方法を用いても、現状、安定性は低い(画像が乱れるレベルではなく、ハングアップする)です。

以下は画面をランダムな色で塗りつぶす例です。scanvideoの描画処理をCore1側で行わせるために、_threadクラスを使用しています。Core0側のfill(color)は、色の値の設定を行っているだけで、実際の描画はCore1側が行ないます。


from rp2extra import scanvideo
sc = scanvideo(scanvideo.VGA_MODE_640X480_60)
sc.test_pattern()

def start_video(sv):
    sv.init()
    sv.start()

import _thread
_thread.start_new_thread(start_video, (sc,))

from urandom import randint
sc.fill(sc.pixel_from_rgb(255,128,64))
for i in range(0, 100000):
    sc.fill(randint(0,65536))

実行するとこんな感じの画面になります。

bitmap()を使うとPicoのMicroPythonにVRAMを持たせることができ、さらにframebufモジュールとFBConsoleを使ってそのVRAMにREPLを表示させることも可能です(すぐクラッシュしてしまいますが・・・)。

動画サンプルをいくつか、Twitterにも上げました。

コメント

  1. 暮G より:

    http://vdmgr.g2.xrea.com/rpipico_vga.html
    こちらのサイトの”フラッシュメモリ速度調査”によるとキャッシュの状態で実行速度が大きく低下するようです。
    よくわかりませんが、タイミングが厳しい処理はこの影響を受けると思います。

  2. boochow より:

    ありがとうございます。リンク先のページ、色々調べられていて面白いですね。