Raspberry Pi PicoでNTSCのコンポジット映像信号を出力する(カラー出力編)

Raspberry Pi PicoでNTSCの映像信号出力に挑戦。前回はモノクロ出力でした。

u-mid.hateblo.jp

今回はカラー出力なんですが、バグというか何故かNTSC出力の処理が一瞬停止して横にノイズが走ってしまうという問題(何らかの割り込み?メモリアクセス待ち?)が起き、その原因の調べ方がわからず詰まっている間にゼルダのティアキンが出てそっちに没頭してしまっていたため、Picoの方がほったらかしになってました。
問題の調査の仕方自体がわからなくて原因不明のままなのですが、デュアルコアCPUの2コア目の方を使えば問題なくNTSCカラー出力できる状態なので、とりあえずここで一段落とします。

プログラムはゼルダが出た15ヶ月前のままで、今見返すと色々と忘れてしまっています。なので今回の記事は実際のプログラム内容と違うことを間違って書いてる箇所があるかもしれません。

 

 

 

カラー出力結果はこんな感じです。

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

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

 

開発環境

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

 

仕様

カラー版では以下のような仕様になりました。

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

NTSC 240pまたは480i

・8bitインデックスカラー。RGB888(実際の表示精度はHSV557くらい)から256色表示可

・PIOのSMを3つ、DMAを5ch使用してフレームバッファ読み込み・パレット読み込み・信号出力を行う。CPUの処理は水平同期のタイミング毎に割り込みでPIOとDMAを再設定するだけ

フレームバッファは1ピクセル8bitなので、640x480とかはSRAM容量が足りなくてできません(解像度は640x480表示できてもフレームバッファ640x240を上下2回繰り返す形になる)。640x240や320x480で使用します。

640x240の4:3表示時なるべくピクセルアスペクト比が1:1になるように表示する設定が可能です。

使用するSM数やDMAチャンネル数は当初はこれより少ない数で画面表示できていたのですが、なんらかの理由で処理が間に合わないということが一度でも起こるとPIOとDMAのリレーが止まり信号出力が停止してしまうので、PIOの信号出力(GPIO制御)用SMをフレームバッファ映像信号出力とフレームバッファ領域外信号出力の2つに分けDMAの使用チャンネルを増やして対処しています。

表示精度がHSV557くらいな理由。
-40IRE~135IREを8bitで賄うため、グレースケール(7.5IRE~100IRE)に8bit分使えるわけではありません(256段階の内使えるのはおよそ135段階)。彩度の分解能も同様。
処理速度の都合で正弦波ではなく矩形波を使うため、色相はpicoの動作クロックに依存します。157.5MHzだとカラーサブキャリア周波数の44倍なので使える色相も44段階。

 

 

回路図

今回の回路は以下の、Picoでモノクロ8bitNTSC出力をしてるサイトを参考にしました。

GitHub - obstruse/pico-composite8: Raspberry Pi Pico NTSC 8-bit Composite Video output using Resistor R2R DAC, interlaced at 640x480, with slideshow on second core.

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

(回路図の右上の部分が間違っていたので修正しました)

先程リンクしたページではRが180Ω、2Rが320ΩとR2Rラダー抵抗回路ながら2RがRの2倍ではないですが、これはそのままR2RだとPicoのGPIO内部抵抗(40Ωくらい)の影響で誤った出力になるためだそうです。

なお今回は持ち合わせの抵抗の都合で120Ωと200Ωになっています。

 

カラーサブキャリアを正弦波ではなく矩形波で出力するせいかビデオキャプチャで見てみた場合だと全体的にうっすらノイズがあったのですが、この回路図の右上のここにコンデンサを入れるとノイズが減りました。

(容量値の単位が間違っていたので修正しました)
コンデンサなしでもブラウン管テレビと液晶テレビではノイズは目立たなかったです。映像入力側のノイズ低減処理能力次第らしい。

 

プログラム

ソース置いときます。去年の5月の状態のままです(余計なコメントアウト箇所とかを削除した程度)、本当は後で変数名とかちゃんと直すつもりだったんですけど…結局そのまま。
ソースコードしか入ってないのでプロジェクトは自分で作成する必要があります。

NTSCtest_color.zip - Google ドライブ


以下プログラムの中身についての解説。

 

Picoの動作クロックをカラーサブキャリア周波数に合わせる

NTSCで色をつけるには、カラーサブキャリア周波数(長いんで以下サブキャリアと略します)という3,579,545Hzの決められた周波数の正弦波を出力する必要があります(前述の通り当プログラムでは正弦波だと処理が重いので矩形波を出力してますが)。
少しだけなら周波数がズレても色はつきますが色相もズレたりするので、ちゃんとした色を出すためになるべく正確な周波数の矩形波を出力したいところです。

Picoの動作クロックをサブキャリアの倍数にすることで、サブキャリアと全く同じ周波数の矩形波を出力できます。が、Picoの動作クロックは小数点以下の0.01MHzみたいな細かい周波数までは正確に設定できるわけではありません。

最初はPicoの動作クロックを129MHzにしていました、これはサブキャリアの36倍の128.863636..MHzに設定したかったけど実際に設定可能な一番近いクロックが129MHzだったためです。Picoのスペック上の動作クロックの「標準125MHz、最大133MHz」にある程度近い100~140MHzの範囲且つサブキャリアの4の倍数(当初は4の倍数だと処理が楽だった)で探した結果36倍になりました。
でも矩形波の周波数が128.863636..MHz÷36ではなく129MHz÷36だと、サブキャリアの3,579,545HzHzより3,788Hz高くズレてる周波数になってしまい、画面の左の方と右の方で色相が変わってしまいました。

これじゃダメだということで、ではサブキャリアの何倍ならズレが小さくなるのか、それを調べたのが以下の表です。

pico_ntsc_clock - Google スプレッドシート

そこで見つかったのがサブキャリアの44倍の157.5MHz。なんとズレが小さいどころかPicoもぴったり157.5MHzに設定可能!

157.5MHzというと133MHzの1.18倍くらいのオーバークロックですが、幸いPicoは200MHzでも300MHzでも動くほどオーバークロック耐性が高く(ただしFLASHメモリは270MHzあたりで同期が取れなくなってくる)、157.5MHzなら余裕なのでこれで決定となりました。

ついでにPALだとズレが小さいのは何倍なのかも確認してます。

pico_pal_clock - Google スプレッドシート

一番ズレが小さいのは-121Hzズレてる29倍の128.5714286MHz。その次に+280Hzズレてる59倍の261.6MHz、は流石にクロックが高すぎる。その次の-285Hzのズレなら30倍133MHz・36倍159.6MHz・45倍199.5MHz等結構ありました。

 

フレームバッファとパレット

フレームバッファのメモリ形式はごく普通です。1ピクセル8bitなので、1ピクセルに0~255のうちのどれか1つのパレット番号が入っています。

一方パレット。「◯番のパレットがどんな色か」というパレットカラー情報を指定する時はRGB888(RGBが各8bit)ですが、パレットカラー情報のメモリに格納するのはRGB値そのままではなくGPIOから8bitで出力する1ピクセル分の信号の形についての情報になっています。つまりパレットカラー情報と言っても指定されたRGBカラー値をメモリに格納するわけではなく、指定されたRGB値を信号情報に変換した上でメモリに格納します(パレットカラー情報と言いつつ正確にはパレット信号情報)。

普通のパレットカラーのメモリ構成(RGB888)はこう。

このプログラムのメモリ構成はこう(メモリの中身はイメージ、位相については後述)。

この図の矩形波の信号の絵が入ってる所の実際の信号情報は「SMのプログラムカウンタ初期化・GPIOの8bit出力を96に設定・34クロック待機・GPIOの8bit出力を192に設定・10クロック待機」のような形式になってます。これをPIOの信号出力担当SMに送信するとその情報に従ったGPIO制御と待機を実行します。1つの信号情報の容量は64bit(8バイト)です、uint32_t(32bit)1つに「SMのプログラムカウンタ・GPIOの8bit出力を◯◯に設定・◯◯クロック待機」という情報を入れて、この情報を2つ入れられるように64bitにしてあります。

1ピクセルの横の長さがサブキャリア1周期と同じ長さなら処理も簡単なのですが、それだと横解像度が160~180程度にしかできません。なのでサブキャリア1周期の間に2ピクセル(横解像度320時)や4ピクセル(横解像度640時)入れる必要があります。
サブキャリア1周期の間に4ピクセル表示するというような周期だと、1つのパレットカラーでもサブキャリアの位相4箇所に合わせて4種類の信号が必要になります。

そのため先程のメモリ構成図のように、サブキャリア1周期に4ピクセル入れるならその4種類の信号情報を全て持つという形で実装しています。

更に、サブキャリア2周期に7ピクセル入れる、というような指定ができるようにして、ピクセルアスペクト比を少し調整することが可能になっています。2周期内で7ピクセルという設定だと4:3表示の時のピクセルアスペクト比が1:1に近い表示になります。


ちなみにインターレース時に奇数フィールドと偶数フィールドで位相が反転するので、その2パターン分も別々にパレットカラー情報内に用意するようにしています。
つまりパレットカラー情報の数はパレット数(256)だけではなく、

パレット数(256) ✕ サブキャリアn周期内のピクセル数(PIOのメモリ生成処理の都合で2のn乗。1周期に4ピクセル入れるなら4、2周期に7ピクセル入れるなら7ではなく2のn乗なため8) ✕ フィールド別位相(240pだと1、480iだと2)

と設定次第で数が掛け算で増えます。

 

PIOとDMA

GPIOを使った8bitの信号出力はPIOが行い、PIOへのデータ転送はDMAで行います。なるべくCPUリソースを使わない。

PIOのSMとDMAのそれぞれの役割を箇条書きすると(名前はプログラム内での変数名)、

sm_sync:フレームバッファ領域外の信号(同期信号など)の情報を受け取り、GPIOを制御し信号を出力する

dma_sync:メモリからsm_syncに同期信号情報を転送する

sm_addr:パレットカラー情報(前述の通り、RGB値ではなくそのRGB値の色を表示するための信号情報が入っている)配列の先頭メモリアドレスとフレームバッファからDMA転送されてきたパレット番号と現在の位相位置などを組み合わせて、次にsm_colorに渡す1つのパレットカラー情報のメモリアドレスを生成する

dma_pal:フレームバッファピクセル内容(ピクセルのパレット番号)をsm_addrに転送する

dma_paladdr:sm_addrで生成したパレットカラー情報メモリアドレスの値を読み込み、dma_colorの読み込みアドレス設定に書き込む

dma_color:dma_paladdrから指定されたメモリアドレスにあるパレットカラー情報(中身は信号情報)をsm_colorに転送する

sm_color:dma_colorから転送されてきた信号情報を、GPIOを制御して出力する(つまりフレームバッファの映像の信号を出力)

dma_raddr:dma_syncの読み込みアドレス設定更新

これらをPIOのirqやらdreqやらDMAのtriggerやらchainやらを使ってCPU処理なしに転送処理をリレーさせます。構成考えてる時はまるでパズル。
CPUは水平同期のタイミング毎に割り込みでPIOとDMAを再設定するのに使用しています。CPU使用率は1%も行かないかと。いや計ってみないとわからないですが。

sm_addrの役割について。
パレットカラー情報(入ってるのは信号情報)内の信号情報をsm_colorにdma_colorでDMA転送するのですが、そのパレットカラー情報内の具体的にどこの信号情報を送るのか?のメモリアドレスを生成するのがsm_addrです。

生成するメモリアドレス(32bit)はこんな感じです。

①パレット情報の先頭アドレスの上位部分:パレットカラー情報の先頭アドレスそのまま

②フィールドが奇数が偶数か:インターレースの場合のみ1bit存在

③位相の位置:サブキャリアn周期内のピクセル数次第で増減。この図はサブキャリア1周期の間に4ピクセル入れる場合(4=2bit)。7ピクセル入れる場合は3bit。

④パレット番号:フレームバッファの1ピクセルの色。256色(256=8bit)

⑤信号情報1つ分のバイト長:長さ64bitなので8バイト=3bit

この内、①と②は固定(水平同期時にCPUから書き換え)、③は1ピクセルごとにsm_addr内でデクリメント(インクリメントではない)、④はフレームバッファからdma_palでDMA転送、⑤は0固定です。これらをSM内で組み合わせて出力します。

「③位相の位置」の値(位相の位置のインデックス)がインクリメントではなくデクリメントなのは、PIOには四則演算命令が無く、回数ループ用にJMP命令にレジスタ(残りループ回数を記録)をデクリメントする機能があるからという理由です。
そのためインデックスが逆に進んでもいいように、先に貼ったカラーパレット情報メモリ構成図では位相1~位相4と順番に配置する表記をしてましたが実際のメモリには位相4~位相1のような逆の並びで配置されています。