Raspberry Pi PicoのVGA信号生成ライブラリを使ってみた


今回はPicoからVGA信号を出力する方法について解説したいと思います。
Picoではpico-extrasというライブラリーを使って、PIOを制御してVGA信号を生成できるようになっています。このライブラリは現在はまだ正規のSDKの中に含める段階ではないという位置づけのようですが、すでに豊富な機能が用意されています。

ライブラリの名前は「scanvideo」です。このライブラリでは、scanlineという言葉が頻繁に出てきます。scanlineとは日本語で「走査線」のことです。多くの映像信号は、画面左から右へ、上から下へ走査線上を電子ビームが動いて映像を描画して行きますが、scanvideoが行う処理は、この走査線にカラー情報を載せていくようなイメージです。

ライブラリを使う上で、 VGA 信号の詳細を知っている必要はありませんが、知っていた方が理解しやすくなると思われますので、簡単に解説しておきます。

代表的なVGA信号は、640×480ピクセルの画像を毎秒約60回出力します。色情報はRGBの3色を別々の信号線で、アナログで与えます。水平同期パルスは走査線を描く電子ビームをスクリーン左端へ移動させ、垂直同期パルスは上端へ移動させます。

電子ビームを左端や上端まで移動させるのは、ある程度時間がかかりますので、その間は色情報は出力しません。この、ピクセルを描画しない時間帯を「ブランキング」と言います。ブランキング期間は描画処理が必要ないので、プロセッサーはそれ以外の処理を行うことができます。

水平ブランキングではカラー情報を黒レベルに落とし、電子ビームが右端から左端まで復帰するのに必要な時間を確保します。 この時間の長さは160ピクセル描画する時間に相当します。 したがって、走査線一本の描画あたり800ピクセル分の時間が必要です。

同様に垂直ブランキングでは走査線45本分の時間が必要です。従って、VGA 信号の一画面には800×525ピクセルを描画するだけの時間が必要です。これを毎秒60回行いますので、VGAの色信号は毎秒

800×525×60 = 25,200,000

ピクセルを描画するだけの速度が必要です。これをピクセルクロックレートと言います。

実際には、VGA信号のピクセルクロックは端数無しの25MHzで、逆に画面のリフレッシュレートが

25,000,000 / (800×525) = 59.524 (Hz)

となっています。

VGA信号の詳細については、こちらのスライドが分かりやすいと思います。

***

それでは、本題のscanvideoライブラリ(以下、scanvideo)の話に移ります。

このライブラリの解説は現在のところ、GitHubのREADMEページしかないようです。

raspberrypi/pico-extras
Contribute to raspberrypi/pico-extras development by creating an account on GitHub.

以下の内容は、上記のREADMEページ及び実際にコードを読んだり動かしたりした結果に基づいていますが、ライブラリも更新中ですし、かならずしも正確な内容ではないかもしれないことを予めお断りしておきます。

scanvideoでは、CPUは走査線を1本描画する都度、走査線1本分の色データをPIOを使用した描画エンジンへ渡します。描画エンジンはDMAでデータを読み取りますので、CPU側のタイミングは正確である必要はありません。

現在では、すべてのピクセルの色情報をメモリに保存しておくビットマップディスプレイが一般的ですが、scanvideoではそのようなメモリはありません。そもそもVGA解像度で1ピクセル16ビットが必要なら、画面全体では614,400バイトのメモリが必要になりますが、マイコンではRAM容量がこれよりずっと少ないのが一般的です。(ただしPicoの場合、2MBあるフラッシュメモリに格納しておくことは可能です。)

scanvideoでは、走査線1本分のデータを繰り返し処理することで画面を描きます。走査線1本分の色データは、RLE(Run Length Encoding)で表現します。ランレングスというのは「連続する回数」のことで、同じ色のピクセルが続く場合に、そのデータを「色と個数」で表現します。圧縮方式としては非常に単純かつ軽量な方式で、ファクシミリなどもRLEで画像情報を転送しています。

scanvideoは走査線1本分の色データを以下のようにトークンの配列で表現します。トークン1つが16ビットです。末尾には終了マークが入ります。また、データ全体は32ビットの整数倍でなければなりません。そのため、トークンの数が奇数か偶数かで以下の2つのパターンがあります。(図では32ビット境界を赤で太線で表しています。)

トークンはコマンドか、コマンドに伴うパラメータです。パラメータの個数はコマンドによって決まっており、データ構造としては以下のようなパターンがあります。

利用できるコマンドには、以下のようなものがあります。RAW1P_SKIP_ALIGNは無視されるトークンを1つ含んでおり、数合わせに使うことができます。例えば、色データをforループで生成するときに、1回分のループの中で生成するデータを32ビット境界に合わせるために使えます。


色情報は、RGBが5:5:5ビットで、マクロPICO_SCANVIDEO_PIXEL_FROM_RGB8(r, g, b)で24ビット情報から変換できます。実際の16ビットデータは、5-5-1-5ビットの構成(1ビット部分は常に0)になっているようです。RGB555とRGB565のどちらでも使えるようにしてあるのかもしれません。

なお、scanvideoライブラリを使う上では以下のような注意点があります。

(1)走査線の最後のピクセル(END_OF_SCANLINEの直前のピクセル)の色は、必ず0(黒)でなければなりません。(画面に表示される必要はありません)

(2)走査線1本分の色データは、現在180×2トークンです。この値はPICO_SCANVIDEO_MAX_SCANLINE_BUFFER_WORDSで指定することができます。

(3)表示に間に合うように走査線の色データを準備できなかったときは、画面の左半分が青、右半分が黒で表示されます。

***

それでは、実際のAPIを見ていきましょう。

プログラムの基本的な構造は、走査線に合わせたデータを用意する無限ループで、その処理内容は以下のようになります。

while (true) {
    scanvideo_scanline_buffer_t *buffer = scanvideo_begin_scanline_generation(true);
    // buffer->dataに走査線1本文の色データを書き込む
    // buffer->data_usedに書き込んだデータのワード(32bit)数を書き込む
    // buffer->statusにSCANLINE_OKを書き込む
    scanvideo_end_scanline_generation(buffer);
}

scanvideo_begin_scanline_generation()を呼ぶごとに、次に描画すべき走査線の情報を格納した構造体が渡されます。引数trueはブロッキングあり(次に描画する走査線の処理を開始可能な状態になるまで、呼び出し元へ戻ってこない)を表します。

そして、上で述べたようなトークン列および個数(トークンの個数/2)を書き込み、statusを更新します。
scanvideo_end_scanline_generation()を呼ぶとデータが描画エンジンへ渡されます。

これだけだと、静止画が表示されるだけですが、以下のようにフレーム番号をチェックすることで画面を更新することができます。

static uint32_t last_frame_num = 0;
while (true) {
    scanvideo_scanline_buffer_t *buffer = scanvideo_begin_scanline_generation(true);
    uint32_t frame_num = scanvideo_frame_number(scanline_buffer->scanline_id);
    if (frame_num != last_frame_num) {
        last_frame_num = frame_num;
	// 画面更新に必要な処理を行う
    }
    // buffer->dataに走査線1本文の色データを書き込む。個数はbuffer->data_max個まで
    // buffer->data_usedに書き込んだデータのワード(32bit)数を書き込む
    // buffer->statusにSCANLINE_OKを書き込む
    scanvideo_end_scanline_generation(buffer);
}

***

それでは、具体的なサンプルを紹介します。このサンプルは、以下のような40×30のマス目を表示するものです。

内部のデータではマス目1つが1バイトでそのマスの色情報を表し、色情報はインデックスカラー形式です。長さ216(6x6x6)のカラーテーブルがあり、マス目の値はそのテーブル内でのインデックスを表します。

このマス目一つを、16×16ピクセルで描画します。16×16のうち、右端と下端は黒です。描画は走査線単位で行いますので、カラーの走査線を15回描いた後、黒一色の走査線を1回描くことになります。

黒だけの走査線は、上で述べたCOMPOSABLE_COLOR_RUNを使って描画できます。そのコードは以下のようになります。bufは上のコードでのbuffer->data、buf_lengthはbuffer->data_maxが渡されます。

int32_t single_black_line(uint32_t *buf, size_t buf_length) {
    assert(buf_length >= 2);

    buf[0] = COMPOSABLE_COLOR_RUN     | 0;
    buf[1] = VGA_MODE.width - MIN_RUN | (COMPOSABLE_EOL_ALIGN << 16);
    return 2;
}

下位16ビットが先に処理されるので、コード上も下位16ビットに入るデータを先に書いています。MIN_RUNは3、VGA_MODE.widthは640です。

ピクセルのカラーが付く走査線を描くコードは以下のようになります。DISPBUF_Wは40、PIXEL_Wは16です。dataはこれから書こうとする行の先頭のマス目を指すポインタで、配列colorは16ビットの色情報が216個入ったカラーテーブルです。

int32_t single_scanline(uint32_t *buf, size_t buf_length, uint8_t *data) {
    assert(buf_length >= 1 + 3 * DISPBUF_W);

    for(int i = 0; i < DISPBUF_W; i++) {
	buf[0] = COMPOSABLE_COLOR_RUN  | (color[*data++] << 16);
	buf[1] = PIXEL_W - 1 - MIN_RUN | (COMPOSABLE_RAW_1P_SKIP_ALIGN << 16);
	buf[2] = 0;                  //| -- the last token is ignored --
	buf += 3;
    }
    buf[0] = COMPOSABLE_EOL_SKIP_ALIGN | 0;
    return 3 * DISPBUF_W + 1;
}

同じ色で15ピクセル描いた後、黒1ピクセルを描くので、COMPOSABLE_COLOR_RUN のあと COMPOSABLE_RAW_1Pを使いますが、これだと合計5トークンで偶数個にならないので、COMPOSABLE_RAW_1P_SKIP_ALIGNで1つ余計にトークンを入れています。これを40回繰り返したあと、末尾に終了マークを入れています。

コードは以下にアップロードしてあります。

boochow/pico_test_projects
Some projects to test Raspberry Pi Pico unique functionalities, such as interpolators or scanvideo library. - boochow/pico_test_projects

このコードは、デュアルコアで画面を乱さずに書き換えたり、走査線のデータを作成するのに割り込み要求を使ったりということはしていませんが、画面更新の際により複雑な処理が必要になる場合は、そういった処理が必要になります。

そのためのAPIは既に用意されています。サンプルとしては、pico-playgroundの中にあるscanvideo_minimalは排他制御や同期を行っています。

***

ちょっと長くなってしまいましたが、scanvideoライブラリを使うのは実際にはそれほど難しくありません。上のサンプルも、全体で100行程度のものです。

感想としては、PIOは確かに便利で、同じことをCPUだけでやろうとしたら正確なタイミングを取るために苦労するところです。ずっと以前、FPGAでVGA信号を生成したことがありますが、このscanvideoライブラリを使えばVerilogやVHDLよりも簡単にVGA信号を生成できます。もちろん、ユーザプログラムがタイミングを考慮しなくて済むようにscanvideoライブラリが頑張っているわけですが。

面白いライブラリですので、もう少し使い込んでみたいと思います。

コメント