Raspberry Pi PicoでNTSCのコンポジット映像信号を出力する(まずモノクロから)

Raspberry Pi PicoでNTSCの映像信号出力に挑戦。一度NTSC映像信号生成ってのを試してみたかったんですよねー。

PicoにはPIO(programmable I/O)なる小規模なCPU的なハードが載ってて、メインのCPUから独立してちょっとした処理を行うことができます、しかもCPUと同じクロックスピードで。命令も全て1クロック。
これ使えば専用のIC使ったりしないでリアルタイムにNTSC信号作れちゃうのでは!?というのが今回のチャレンジです。

 

最終目標は320x240くらいの解像度のカラー映像の表示です、が、まずはモノクロ映像に挑戦します。信号がカラーよりシンプル*1で難易度が低い。

 

 

コンポジット出力結果はこんな感じ。

この映像はPicoからコンポジットで出力したのをビデオキャプチャ(GV-USB)のコンポジット入力からキャプチャしたものです。

出力映像の確認はビデオキャプチャとブラウン管テレビと液晶テレビで行っています、3つとも無事映ってます。

 

開発環境

開発環境はWindows 11とVS Code、PlatformIOでearlephilhower版Arduinoコア(Raspberry Pi Pico/RP2040)を使用してます。

 

仕様

結果的に以下のような仕様になりました。

・使用する電子部品はRaspberry Pi Picoと抵抗11個(回路組むのにブレッドボードやジャンパ線を使ったりはする)

NTSC 480i(インターレース

フレームバッファ642x480、5bitグレースケール

・表示処理にはCPUを使わない(割り込み処理でCPUを使ったりもしない)

当初320x240のノンインターレースにするつもりでしたが、モノクロだとフレームバッファをそのままPIOにDMA転送して表示することが可能なので、試しに解像度を高くしてみました。

Picoのメモリ(SRAM)は264KBなので、640x480x8bitだと300KBになりメモリに入らない。ということで解像度を高くする際8bitから5bitに減らしました、1ワード(4byte・32bit)に5bitを6ピクセル入れる。

これで200KBちょっと。

横640ピクセルだと6で割り切れないので横は642ピクセルに。
縦か横の解像度を半分にすれば8bitで表示することも可能ですが(途中まではそうしていた)、今回は解像度重視で。

フリッカー処理(インターレース表示のフリッカー、いわゆるちらつきを軽減)をしてないというかする余裕が無いので、ブラウン管に映した時くっきりした横線などがちらつきます。液晶テレビとかならいい感じにプログレッシブ化してくれますが。

 

NTSC信号について

NTSCの映像信号のタイミングについてはこちらのサイトを参考にしました、いろんな方が参考にしてるであろうページ。

elm-chan.org


フレームレートは、カラーテレビの59.94fpsでも白黒テレビの60fpsでもなく、Picoの設定できるクロック(今回は129MHz)の都合で60fpsちょっとになっています、若干60fpsより多い。まぁファミコンも約60.1fpsなのに映ってるので大丈夫かなーと。

Picoが思ったより細かくクロック設定できないので、カラー映像を試す時カラーサブキャリア周波数がどれだけ規格からずれててもちゃんと映るか今から不安…。

黒レベルはNTSC-Jの0IREではなくNTSC標準の7.5IREにしています。

電圧はだいたい-40IREを0.0V、100IREを1.0Vとしています。別に-0.286V~0.714Vじゃなくてもいいらしい。

 

回路図

電気回路の知識はもう中学の理科の情報すら忘れかけてるレベルなので、ネットで調べながらあまりよくわかってないことも多い状態で組んでます。拙い点はご容赦を…。

回路は以下の、PicoでPAL出力をしてるサイトを参考にしました(プログラムの方は見てないですが)。

Composite Video on the Raspberry Pi Pico - L Break Into Program, 0:1

そして組んだ回路を回路図で表すとこう。

ピン配置が違うだけで前述のサイトの回路とほぼ同じです、普通の5bitラダー抵抗回路。
実際に組んでる回路はカラー用に8bitです(回路図では今回のモノクロで使ってない3bit分を省略)、R11の抵抗が110Ωではなく150ΩなのもカラーでIRE100超え(電圧1.0V超え)するのを前提にしてるためです。ピン配置がGP0ではなくGP3から始まる5bitなのも、実際はGP0から始まる8bitだからという理由。
R1~R5の抵抗も440Ωというのは持ってないので実際は470Ωの抵抗を使っています。そのためグラデーションを表示すると5bit(0~31)の15より16の方が少し暗いという逆転現象が。
220Ωと470Ωの組み合わせより100Ωと200Ωにした方がいいんだろうか、電流高くなってPicoに負担かかったりしないかな。1ピン最大10mA、全ピン合計20mAくらいに収まれば大丈夫か?または1kΩと2kΩの組み合わせとか。コンポジットのインピーダンス75Ωからだいぶ離れて電流低くなるけど画質に影響したりしないだろうか。

 

実際の回路は↓こんな感じですが、組んだ回路は後でバラす予定なので抵抗の足もケチって切らずにそのまま使ってます。

足同士が触れる危険性があるわ経路が無駄に長くなるわで良いことはないんですけどね。

手前にあるタクトスイッチはPicoのリセット用で、コンポジット出力とは関係ありません。

 

プログラム

ソース置いときます。他人が読めるように書くのは途中で諦めました。ソースコードしか入ってないのでプロジェクトは自分で作成する必要があります。

NTSCtest_monochrome.zip - Google ドライブ


PIOのステートマシンを1つ、DMAチャンネルを3つ使用してます、IRQは未使用。

PIO

PIOでは、同期信号部分向けの「ピンに5bit出力してnクロック待つ」を繰り返す処理と、フレームバッファ表示部分向けの「ピンに5bit出力する」をピクセルクロックごとに繰り返す処理という2つの処理があり、1つのステートマシン内で切り替えて実行してます。そのため最初にどちらの処理を行うかの分岐があります。

1.分岐:osrから8bitをステートマシンのpc(プログラムカウンタ)にoutして「2.同期信号処理」「3.フレームバッファ表示」のどちらかに飛ぶ

2.同期信号処理:「ピンに5bit出力」して「27bitクロック待機」(計32bit)、を繰り返す。繰り返す回数は最初に12bitで受け取る。その回数繰り返し終わったら「1.分岐」に戻る

3.フレームバッファ表示:「ピンに5bit出力」して「ピクセルクロック分待機」(6ピクセル繰り返し計30bitに達したらautopull)、を繰り返す。繰り返す回数(横解像度のピクセル数)とピクセルクロック数は最初に各12bitで受け取る。その回数繰り返し終わったら「1.分岐」に戻る

PIOに送る1.のpc設定と2.の同期信号のデータを予め作っておいて、DMAでフレームバッファと交互にPIOに送信します。予め作った「2.に分岐する1.」「2.」「3.に分岐する1.」のセットをDMAで送った後、DMAの読み込みアドレスと転送する長さをフレームバッファに変更して「3.」を送信する、という流れです。
命令数は全部で16。1つのPIOあたりの最大命令数32のうち半分も使ってしまってます。信号出力タイミングがズレないように、CPUから独立してるPIO側でなるべく処理してもらうようにした結果。プログラム側でピクセルクロックを変更できるようにしていますが、これを固定にすればもうちょっと短くできそう。

5つのピン(GP3~GP7)に出力するたびにPIOのプログラム側ではsidesetでGP8ピンをHIGHにしてますが、これはなんとなくDACのクロックをイメージして試しに付けてみただけで、特に使用してません。

DMA

DMAは、PIOにデータ(前述の同期信号情報とフレームバッファ)を送るPIO向け転送チャンネルと、そのPIO向け転送チャンネルのTRANS_COUNTレジスタ(転送回数)を更新するチャンネルと、PIO向け転送チャンネルのREAD_ADDRレジスタ(読み込みアドレス)を更新するチャンネルの3つを順番に使用しています。

DMAチャンネルの転送を開始するトリガーは3種類ありますが(RP2040 Datasheetの”2.5.2. Starting Channels”を参照)、「chain trigger(転送終了したら別のチャンネルの転送を開始する)」と「channel trigger register(チャンネルの設定レジスタに書き込むと転送開始)」の2種類を使用しています、流れはこんな感じ。

❷と❹がchain trigger、❺がchannel trigger register(ここではREAD_ADDR_TRIG)への書き込みです。

DMAの設定レジスタ更新ならCPUで行えば2つもDMAチャンネルを消費しないのですが(PIOにデータを送るチャンネル1つだけで済む)、CPUでやると水平同期(1秒間に15734回)1回につき2回(前述の同期信号処理とフレームバッファ処理の切り替え)、1秒間に約3万回という結構な頻度で割り込みが発生し、CPUリソースを消費してしまいます。

その上信号出力をPIOのステートマシン1つだけで全て処理する都合上フレームバッファ処理から同期信号処理に切り替える時のデータ受信待機時間の余裕がなく、割り込みでCPUでレジスタ更新する方法だと、メモリアクセスが渋滞(フレームバッファの書き込みと読み込みが被る)してもたついた時にPIOへのデータ送信が間に合わずPIOがストールして映像が数ライン横に乱れることがありました。BUS_PRIORITYレジスタSRAMへのアクセスの優先順位をDMA>CPUにしてみても変わらなかった。ステートマシンを同期信号出力とフレームバッファ表示で別々の2つに分ければCPUでも安定するのでしょうけれど。

 

CPUを使わずDMAだけで転送範囲を変更する手法は、RP2040 Datasheetの"2.5.6. Example Use Cases"内の"2.5.6.2. DMA Control Blocks"を参考にしています。このサンプルではDMA設定レジスタに書き込むアドレスをchannel_config_set_ringを使ってループさせることでTRANS_COUNTとREAD_ADDRのレジスタ更新を2つではなく1つのDMAチャンネルで1度に済ませています。
自分のNTSC信号出力プログラムではすでにchannel_config_set_ringを読み込みアドレスの方に使ってしまっているため書き込みアドレスには使えず、2つチャンネルを使用しています。
このサンプルのようにNUllトリガー(転送開始のトリガーとなる設定レジスタに書き込むのが0だと転送が開始されない。この時割り込みを発生させることも可能)を使えば読み込みアドレスにchannel_config_set_ringを使わず垂直同期(1秒間に60回)ごとの割り込みで読み込みアドレスをCPUでリセットしDMAチャンネルの消費を2つではなく1つにできますが、今回は完全にCPUがフリーになるやり方を試したかったので割り込みなしの方法を採っています。カラー表示ではCPUの片方のコアをがっつり使用する方向で検討中。

PIOへデータを送るDMAチャンネルは、ステートマシンのデータ消費ペースに合わせるためTX FIFOが空になった時のみ転送を進行するDREQ設定にしています。

 

TRANS_COUNTとREAD_ADDRのレジスタに書き込む、読み込みアドレスと転送回数のデータのセット(前出の表で言うと、❸と❺で書き込むデータ)も予め1024個分用意しておきます。個数を2の累乗にすることでchannel_config_set_ringが使えるようになり、1024個転送し終わった後も自動的に転送位置が1からに戻ります。
1024という数は、525ライン(奇数フィールド263ライン+偶数フィールド262ライン)✕2(同期信号情報データの範囲とフレームバッファの範囲の2種類)から来ています。525✕2だと1024を超えてしまうので、垂直同期信号部分である1ライン目から20ライン目(偶数フィールドでは264ライン目から283ライン目)を1つにまとめています。すると今度は数が1024には足りなくなるので、262ライン目の範囲内で待機するだけの穴埋めデータも置いています。

READ_ADDRとTRANS_COUNTでそれぞれ32bit(4byte)、それを1024個用意するので8192byteとそこそこメモリを消費します。これも垂直同期ごとに割り込み関数で設定し直すなら2の累乗にしなくてよくなり3608byteに、水平同期ごとに割り込み関数で設定し直すならそもそも事前準備しなくてよくなるのですが。

 

最後に

作ってみてわかった点は、

・5bitで-40IRE~121IREまでをカバーしたため実際の階調の精度が4bit程度しかない

カラー用の8bitのラダー回路を使い回すために、5bitに変更した後も同期信号のIRE-40とモノクロ映像の7.5IRE~100IREを分けずに混ぜて使ってます。そのため-40IRE~121IREの上下161IREの範囲内で映像信号で使えるのは7.5IRE~100IREの上下92.5IRE、57%ほどです。5bit・32段階の内18段階ほどしか輝度の階調が出ません、ほとんど4bit相当。
しかもこれだとフレームバッファに直接輝度の値を入れられず(0を入れると7.5IREの黒ではなく-40IREの同期信号になってしまう)、輝度からIREに変換する必要が出て手間と処理時間が増えます。

モノクロ表示をちゃんとやるなら、同期信号1ピンとモノクロ映像信号5ピンに回路を分けるべきでしょうね。それを回路側で足す。それならフレームバッファのフォーマットは最初から4bitにした方が扱いやすいか。


・Picoのクロック設定は細かさに限界がある

詳細はRP2040 Datasheetの”2.18.2. Calculating PLL parameters”を参照。

例えば125MHzの次に125.1MHzに設定しようとしても125.1MHzぴったりには設定できず、125.142857MHzになります、その次は125.333333MHz。

カラー映像出力のためになるべくNTSCのカラーサブキャリア周波数の規格に忠実な周波数で信号を出力したいのですが、画面の左端と右端で色相に差がでたりしないか心配です。

 

モノクロの今回はCPU側をフリーにできたり解像度も低下させず実現できましたが、本題のカラー映像信号はいろいろと速度が足りなくなることが予想されます、320x240を想定してますが横320でもなかなか厳しそう。
それでもラダー抵抗回路オンリー縛りでなんとか試行錯誤してみたいところです。いやコンデンサも使うかも…。

 

*1:というか白黒テレビに後付けでカラーにする信号を乗っけてるので、白黒がシンプルというよりカラーで複雑化という感じ?