Raspberry Piベアメタルプログラミングで画面表示をやってみた

baremetal_fb.jpg

ラズパイでのベアメタルプログラミング、再開しました。
これまで、LチカUARTUSBキーボードUSBキーボード(その2)と試してきましたが、今回は画面への描画です。
画面出力を制御するGPUから、フレームバッファのアドレスを取得し、フレームバッファに値を書き込むことで画面にピクセルを表示させます。

ベアメタルでのフレームバッファの使い方も、以前紹介したBaking Piというチュートリアルに解説されていますので、この通りやってみました。

Computer Laboratory – Raspberry Pi: Lesson 6 Screen01

今回のコードも以下のGitHubにアップロードしてあります。

bare_matal_rpi_zero/video at master · boochow/bare_matal_rpi_zero

予備知識として、グラフィックスを実際に処理するGPUと、メインのCPUとの関係を知っておく必要があります。
GPUは、独自のソフトウェアが動作する独立したCPUを持っています。これはPCのグラフィックスカード等とは別のシステムで、「VideoCore」と呼ばれています。

CPUとVideoCoreとの連携は、共有メモリを介して行います。
BCM2835 ARM PeripheralsのP5に、メモリマップが掲載されています。

rpi-memory-map.png

図の左側がVideoCore、中央がCPUのメモリマップです。右はLinuxでのメモリマップですが、今回は関係ありません。
VideoCoreとCPUでは、VC/ARM MMUを介して、SDRAMおよびI/O Peripheralsの2つのメモリ領域が共有されています。

注意が必要なのは、VideoCoreのメモリマップでは同じもの(SDRAMおよびI/O Peripherals)が4回登場している点です。
これについては後で述べます。

SDRAMにはフレームバッファが置かれ、VideoCoreとCPUとの通信はI/O Peripheralsを介して行います。
この通信は「Mailbox」という仕組みを使います。
簡単にいうと「送信用Mailbox」と「受信用Mailbox」と「ステータスレジスタ」があり、

送信:ステータスレジスタをチェックし、送信可なら送信用Mailboxに値を書き込む
受信:ステータスレジスタをチェックし、受信可なら受信用Mailboxから値を読み出す

というものです。
Mailboxは0番と1番の2つがあり、前者がVideoCore→CPU、後者がCPU→VideoCoreのメッセージに使われます(前述のマニュアルのP.109)。
SDRAMを共有していますので、Mailboxに書き込む値はポインタでもよく、大きなデータは送受信用バッファを指すポインタを送信し、そのバッファから受信データを読み取ることも可能です。
また、Mailboxには「チャネル」があり、Mailboxの最下位4ビットはチャネル番号を表すことになっています。
従って、実際にやり取りできる情報は28ビットということになります。

このMailboxの仕組みを実装すると、以下のような感じになります。


2018/7/7追記:Write時のステータスレジスタのアドレスがRead用のものになっているというよくある誤りをこのコードも行っていたので、修正しました)
画面出力を行うのに必要なのは、「VCへ画面の設定(解像度等)を送信し、VCが確保したフレームバッファのアドレスを受信する」という処理です。
アドレスを受け取れれば、あとはフレームバッファへ書き込めばGPUが画面へ反映してくれます。

Mailboxには0番~9番のチャネルがありますが、今回使うのは1番のチャネルだけです。
このチャネルは、フレームバッファに関する情報交換を行います。
以下のページにマニュアル(?)があります。

Mailbox framebuffer interface · raspberrypi/firmware Wiki

フレームバッファのアドレスを取得するには、フレームバッファを表す構造体を用意し、その構造体へのポインタをVCへ渡します。
VCは、必要なメモリを確保し、そのメモリへのポインタを構造体のメンバ変数に書き込みます。
上のマニュアルにもあるとおり、構造体は以下のような構成になっています。
(変数名は、マニュアルとはちょっと変えています)

初期化処理では、フレームバッファのアドレス(fb_info->buf_addr)に0を書き込んでから、チャネル1にmailbox_writeし、mailbox_readしてチャネル1への応答をチェックします。
これをfb_info->buf_addrにアドレスが返るまで繰り返します。
VideoCoreの処理はCPUとは並列に行われていますので、実際に結果が得られるまでにはタイムラグがあり、結果が得られるまでループすることが必要です。

上記のコードでは、フレームバッファ構造体のアドレスに0x40000000を加えています。
これが、最初にメモリマップのところで「注意が必要」と書いた点です。

VideoCoreのメモリ空間では、SDRAMとI/O Peripheralsが4回出てきますが、アドレスによってキャッシュとメモリの値の一貫性(キャッシュコヒーレンシ)が異なります。
最初のメモリマップの図をよく見ると、以下のようになっている(らしい)ことが分かります。

・00000000~ 物理メモリにキャッシュの内容が未反映の場合がある
・40000000~ 物理メモリとL2キャッシュの値は常に一致している
・80000000~ L2キャッシュされているメモリ空間のみ
・C0000000~ キャッシュは利用されない(DMA用)

ARM側では、メモリ空間は00000000~3FFFFFFFまでの1GBです。従ってポインタは常に00000000~のエリアを指していますが、これをVideoCoreに渡すと、VideoCoreが書き込んだ応答はVideoCore側のL2キャッシュに入り、ARM側から読み取れません。
そのため、VideoCoreに渡すポインタは40000000~のエリアを指すようにして、VideoCoreからの応答が物理メモリに反映されるようにします。

VideoCoreが確保するフレームバッファは、上で示した構造体の変数で表すと、論理的には「幅w」×「高さh」で1ピクセルあたりのビット長がbppになります。
また、横1ラインで消費するバイト数はrow_bytesになります。
y座標をフレームバッファ上のアドレスに変換する際には、y * row_bytesを使えばいいことになります。

この論理的なフレームバッファは、HDMIの解像度とは異なって構いません。
HDMIの解像度は「幅display_w」×「高さdisplay_h」であり、フレームバッファはこの解像度に拡大されて表示されます。

例えば

fb_info_t fb_info = {1920, 1080, 480, 270, 0, 16, 0, 0, 0, 0};

というパラメータを渡すと、480×270ピクセル、16 bits per pixelのフレームバッファを1920×1080に拡大して表示することになります。
最初に載せた画面写真は、以前紹介した3.5インチのLCDへこの設定で出力したものです。

コメント