2012年6月24日
PICマイコンでテレビにカラー映像を出力する実験に成功しました。主な部品は16ビットPICとバッファーのためのIC1個、DAコンバーターのための抵抗5本、システムクロック用の水晶発信子、そしてPICを動かすための最低限の抵抗やコンデンサだけです。これほど単純な回路で、テレビにカラー映像が出力できるとは驚きました。PICに内蔵のビデオメモリで160×100ドット、8色のカラーグラフィックシステムが実現します。
PICによるビデオ信号出力と言えば、これまで白黒映像での出力が一般的でした。私も16ビットのPIC24FJ64GA002でテレビに直接接続できるゲームを作ったりしましたが、オリジナルがカラーのゲームだったので、非常に不満でなんとかカラー化できないかと考えていた頃に、トランジスタ技術の2012年2月号に、PC用の液晶モニターにアナログRGBインターフェイスでカラー出力する記事が掲載されました。テレビで一般的なカラーコンポジット信号に比べ、アナログRGB信号は、信号線の数が多い分、信号そのものは単純でPICでも信号生成しやすい仕様になっています。
しかし、我が家のテレビも含め、アナログRGBに対応したテレビはそれほど一般的ではないようです。やはり現状のテレビでカラーゲームを楽しむためには、以前からあるカラーコンポジット入力端子(黄色のいわゆるビデオ入力端子)につないでやるのが、一番適用範囲が広いようです。
カラーコンポジット信号の生成について調べているうちに、nekosan氏のCPLDを用いた「ネコ8」にたどり着きました。カラーコンポジット信号に関する詳しい解説もされており、大いに勉強させていただきました。それで、よくよく検討してみると、16ビットの高速PICであれば、直接信号生成できそうな気がしてきました。
いきなりですが、利用方法から紹介します。というのも、今回のテーマであるカラーコンポジットビデオ信号出力というのは、テレビのビデオ入力端子につなぐだけの単純なものですが、歴史的な経緯もあり中身はかなり複雑で、理解するにはそれなりの忍耐が必要です。今回製作したプログラムは独立した割り込みで動作しているので、初期設定さえ行えば、あとは特に専門的な知識を必要とせず、ビデオメモリ領域に書き込みを行うだけで特に意識しなくてもカラー出力を得ることができるようになっています。
使用した回路は大変シンプルです。PICは40MIPS対応で8Kバイト以上のRAM搭載という条件で、入手性のよさからdsPIC33FJ256GP506を使用しました。秋月電子でチップと変換基板を購入して、写真のようにブレッドボード上で実験しています。書き込みはPICKit2でICSP機能を使いました。また、dsPIC33Fシリーズの出力電流の定格がわずか4mAしかないので、74HC04をバッファー代わりに使っています。オシレーターは必ず3.579545MHzの水晶を利用してください。セラミック発信子ではカラー信号に必要な精度が出せません。
プログラムはCのソースになっていますが、信号生成のタイミングが重要なため、中身の大半はインラインアセンブラーを使って書いています。
ソースプログラムのダウンロード |
main関数の最初にクロック速度の定義をしています。3.58MHzの24倍の約86MHz、実行速度でいうと約43MIPSで動作させています。本来は40MIPSまでの定格なのでややオーバークロックとなりますが、許容範囲のようです。続いてI/Oポートの定義です。今回の実験回路では、PORTBの下位8ビットを使用しています。実際には下位5ビットしか使っていませんが、残る3ビットもダミーデータが出力されるため、他の用途に使用することはできません。 続くinit_composite関数で本システムの初期化を行い、利用を開始します。実際には割り込みの設定とビデオメモリのクリアを行います。割り込みには、TIMER2と出力コンペア1、2、3を使っています。
unsigned intで定義された3000ワードのグローバル配列変数「VRAM」の書き込み内容で、画像を表現します。図のように青、赤、緑の3枚のプレーンがあり、それぞれ配列の0番目、1000番目、2000番目から開始されます。各プレーンは横方向に10ワード(160ビット)続いた後に次の行となり、縦方向は100行分あります。
1ワード内では最上位ビットが左端のドット、最下位ビットが右端のドットを表し、1にするとオン、0でオフとなります。例えば、青のプレーンの最初に0x8000、赤のプレーンに0x0000、緑のプレーンに0x8000とすると、一番左上のドットが水色で表示されます。
本システムでは常時割り込みが発生して、映像信号や同期信号の生成と出力を行っています。そのため、ユーザプログラムがCPUを利用できるのは、映像信号や同期信号などを生成していない時間帯、つまりテレビ画面の外側(上下および左右)の処理のタイミングに限られます。とはいえ、平均すると16MIPS相当程度の空きがあるので、PIC24Fシリーズの最高速度と同等程度の実行速度は得られます。
静止画を表示するだけであれば気にする必要はありませんが、アクションゲームなどのプログラムの場合、タイマー割り込みなどでタイミングを図る必要があります。今回はTIMER2割り込みを使用しており、また、カラー信号生成の途中で他の割り込みが発生すると画像が乱れるため、極力他の割り込みは使わないほうがよいと思います。その代わり、以下の方法でタイミングを図ることができます。
本システムでは、映像出力状態に関して2つのグローバル変数を提供しています。1つはdrawingで、システムが映像区間内(0行目左端から99行目右端)の処理中は-1、それ以外で0となっています。もう1つはdrawcountで、こちらは映像区間終了時に毎回1増加するカウンターとなっています。drawcountは必ず約60分の1秒ごとにカウントアップします。これらを利用することで、タイマーを使わなくてもタイミングを図った動作を得ることが出来ます。
私のお勧めはdrawcountを利用する方法で、まず最初にdrawcountを0にクリアし、1になるのを待ちます。drawcountが1になったら、すぐにまたクリアします。この段階で映像表示が終了しているので、画像の書き換え処理を最初に行います。その後、キャラクターの動きなどゲームの処理を行い、またdrawcountが1になるのを待つ、という繰り返しになります。もし60分の1秒だと速い場合は、drawcountのチェック数を変更してください。
画像の書き換え処理はなるべくdrawcountが1増えてからすぐに行い、次の映像表示区間に入る前に終わらせるべきです。そうしないと、書き換え途中の画像が表示されてしまうことになります。
なお、私は今のところ遭遇していませんが、16ビットPICでは1命令の実行に3クロックを要する場合があり、その命令実行中に割り込みが発生すると、通常より割り込み処理に1クロックの遅延が発生し、色がずれたりしてしまいます。これを避けるため、以下の2点を守ってください。
1.long整数や浮動小数点を使用しない
ダブルワードのMOV命令は前後の関係で3クロックを要する場合があります。
2.PSV読み出しを避けるため、下記のビルドオプションを設定する
MPLAB IDEの「Project」−「Build Options」−「Project」の「MPLAB C30」タブで、「Categories」のプルダウンにて「Memory Model」の設定が必要です。
「Location of Constants」は必ず「Const in data space」を選択してください。カラー信号の定数がcode spaceにあると、PSV読み出しとなりRAMからの読み出しより1クロック余分にかかるため、正しいタイミングで信号出力することができません。また「Data Model」は「Large data model」を選択しないとコンパイル時にエラーが出ることがあります。
最適化については、大半がアセンブラーで書かれているため、あまり設定の影響はありません。ただし、場合によっては割り込み発生時の初期処理内容の違いにより、タイミングにずれが発生することがあるかもしれません。正しい色が表示されない場合、割り込み発生タイミングの調整を行ってください。調整方法は、シミュレータのストップウォッチとブレイクポインタ機能を使って、水平同期の立ち下がり信号出力でいったんカウンタをクリアし、次に、水平同期の立ち上がり、カラーバースト信号の開始、映像信号の開始のタイミングをそれぞれ計測します。詳しくはソースコードにそれぞれどこで何クロックとなるべきか記載していますので、参照してください。そして、ずれている場合は、Timer2割り込み処理内で設定している、OC1R、OC3R、OC2Rに代入している値をずれたクロック数だけ変更してください。
以下は、いよいよカラーコンポジット映像信号生成に関する解説となります。特に読まなくても利用は出来ますが、仕組みを知りたい方はご覧ください。といっても、必要最小限の記述にとどめて、本当に詳しい解説は他に譲ります。
そもそもNTSCのカラーコンポジット信号は、同期信号とカラーバースト信号、そして映像信号から成り立っています。さらに映像信号は輝度信号と2種類の色差信号を合成して出来ています。この合成信号はカラーサブキャリアと呼ばれる3.579545MHz(以後3.58MHz)の波で変調されており、3.58MHzの何倍かのタイミングで信号出力してやれば、正しいカラー信号をテレビに認識させてやることができます。nekosan氏のネコ8では3.58MHzの4倍のタイミングで色差信号を出力しています。
PICで実現する場合は、PICの動作クロック自体を3.58MHzの整数倍にして、数クロックごとにタイミングよく信号出力すれば、同じことができそうです。
16ビットPICといえば、もっともポピュラーなPIC24Fシリーズが最大16MIPSで、3.58MHzの倍数となると4倍した14.32MIPSで動作させることになりそうです。しかし、これではネコ8と同じタイミングで出力しようとすると、1命令サイクルごとにひたすら信号データ出力する必要があり、信号データを生成する時間がありません。そこでもう少し性能のよい、PIC24HシリーズやdsPIC33Fシリーズで考えてみます。これらは最大40MIPSまで対応しており、3.58MHzの倍数となると11倍の39.38MIPSまでとなりますが、11倍では中途半端で扱いにくいため、少しオーバークロックした12倍の42.96MIPSで動作させ、3命令サイクルごとに1回信号出力すれば、ネコ8同様のことが実現できそうです。また少しサボって、4命令サイクルごとに1回の出力でも、1波長3分割(つまり120度単位)となり、これくらいあればテレビ側で正しく波形を認識できると思われます。オーバークロックするところが気になりますが、約7%のオーバーであり、許容範囲内だと信じることにしました。あとは3または4命令ごとにカラー信号をリアルタイムに生成して出力できるかです。
カラーコンポジットビデオ信号は、白黒テレビとの互換性を重視した歴史的な背景から、大変複雑で巧妙にできており、専門的な部分は他に譲ります。nekosan氏のページは大変参考になりました。また、トランジスタ技術の2011年7月号の別冊付録「アナログ&デジタル ビデオ規格辞典」も役に立ちました。
まず同期信号はモノクロとほぼ同じです。1フィールドの先頭から10ライン程度垂直同期期間をとります。実際のテレビ放送ではインターレース方式を採用しているため、より複雑な形式になりますが、今回はインターレースしませんので、単純な同期信号となっています。なお1フィールドは262ラインです。
垂直同期期間のあとは数行の空白行の後、映像期間となります。空白行といっても、映像期間同様に、水平同期信号とカラーバースト信号と呼ばれる信号を出力する必要があります。
映像期間では、まず4.7μsの水平同期信号の後、少し間を空けて、カラーバースト信号を出力します。カラーコンポジット信号では、3.58MHzのカラーサブキャリアで変調する際、振幅と位相のずれで色を表現します。その位相の基準を決めるのが、カラーバースト信号です。カラーバースト信号は位相が180°ずれた周波数3.58MHz、輝度Y=0で振幅0.2の正弦波を9回繰り返す決まりになっています。この間にテレビ側では位相のタイミングを記憶します。なお、カラーバースト信号は画面全体で継続して位相が合っている必要があります。もしカラーバースト信号の位相が間違っていると正しく色を認識できなくなるため、一定以上のずれがあった場合はカラーキラーと呼ばれる機能が働き、画面全体が白黒画像となってしまいます。
カラーバースト信号の後、少し空けていよいよ映像信号が開始となります。ある点のR、G、Bそれぞれの明るさを0〜1としたとき、映像信号値Nは以下で表されます。
ここでYは輝度信号、B-Y、R-Yは色差信号と呼ばれます。また、fscは3.58MHzのカラーサブキャリア周波数で、(2π・fsc・t)はカラーバースト信号を基準とした位相です。R・G・B全て0の黒の場合N=0となり、R・G・B全て1の白の場合N=1で、どちらも一定です。それ以外の色は、輝度と90°位相がずれた2つの正弦波を足し合わせた波となります。8色カラーについて計算してプロットすると下図のようになります。破線はカラーバースト信号です。
この波形をきれいに表現できればベストですが、実際にはあまり厳密にしなくてもテレビ側で波形を認識してくれます。例えば1波長を4分割で信号出力する場合は、(2π・fsc・t)の部分を0°、90°、180°、270°で計算した値を、正確なタイミングで出力すればよいことになります。実際には各色ごとに定数で4個のデータを持ったテーブルを用意し、そこから順次読み出して出力します。
なお、実際の電気信号は電圧で表現されるため、N=0の時を286mV、N=1の時を1.0VとなるようにDA変換します。同期信号の下がっているところが0Vです。(本来はN=0が0Vで、同期信号は-286mVが正しいようですが、グランドをずらすのが面倒なので、多くの場合正の電圧だけで製作していて、動作上も問題ないようです。)
ところで、水平1ラインが63.5μs=2730命令サイクルとなっていて、計算が得意な方は疑問に感じたかもしれません。2730÷12=227.5であり、12で割り切れません。これは何を意味するかというと、同じ水平位置では1行ごとにカラーサブキャリアの位相が180°ずれているということです。これはおそらく画面上で色のずれが発生した場合に、行ごとにうまく打ち消し合って、よく見せるための仕様だと思いますが、プログラムで実現するには、少し面倒です。そこで今回は思い切って、1行を2724命令サイクルとしました。水平1ラインの期間が規格より短いですが、水平同期信号により補正されることを期待します。
まずは解像度を考えます。プログラムを単純にするため、カラーサブキャリア1波長分を1ドットとします。そうすると、テレビの画面横幅内に180ドット程度が収まるようです。そこで横180ドットとしてもよかったのですが、私はかつてPC-8001というパソコン黎明期の頃の名機を所有していて、プログラムを自作しては楽しんでいました。このPCのグラフィックの解像度は横160ドット、縦100ドットで、色数は制限付きですが8色でした。これで決まりです。横160ドットで縦は100ドットです。実際には1ドットをテレビの2ラインで表現します。縦横のアスペクト比はやや縦長(1:1.16らしいです)となってしまいますが、これはPICの動作周波数を3.58MHzの倍数にした場合、避けられないことなのであきらめます。色数はやはり8色としました。これで、PC-8001相当のグラフィックが実現できそうです。
160×100ドット、8色となると、ビデオメモリは最低でも6000バイト必要となります。その他のワーク領域も考えると、8Kバイト以上のRAMを搭載したPICが必要となります。私はいつも秋月電子でPICを調達していますので、秋月電子で手に入るPICで条件に満たすものを探したところ、64ピンTQFPパッケージでRAMを16Kバイト搭載のdsPIC33FJ256GP506になりました。本当なら28ピンのDIPタイプのもので作りたかったのですが、該当するものがありません。PIC24HJ12GP202が価格も手ごろで扱いやすいのですが、残念ながらRAMが2Kバイトしかありません。これでは160×30ドットくらいしか扱えません。秋月電子では今のところ取り扱いがないですが、28ピンDIPのdsPIC33FJ64GP802なども16KバイトのRAM搭載なので、使えると思います。
垂直同期、水平同期信号などのモノクロ信号と同様の部分は、後閑哲也氏のプログラムを参考にしました(PIC24F製作例の「NTSCビデオ出力のオシロスコープ」等)。全てのタイミングの基準となる水平同期パルスの立ち下がりをTimer2割り込みで発生させた後、水平同期の立ち上げを出力コンペア1、カラーバースト信号の開始を出力コンペア3、映像信号の開始を出力コンペア2のそれぞれの割り込みでタイミングを生成しています。なお出力コンペア3がないPICの場合、出力コンペア1の割り込み関数内で、水平同期立ち上げ後、NOP等ダミー命令を何回か実行させてタイミングを調整した後、カラーバースト信号出力を行うことで実現できます。
そして、肝心の映像信号データの生成です。プログラム全体はC言語で作成しますが、信号出力のタイミングが正確でなければならないため、割り込みルーチン内はインラインアセンブラーで記述します。
ビデオメモリの形式は、あえて昔ながらのRGBプレーン方式とします。1ドットを1バイトで表すような形式のほうがカラー信号生成のための処理数が少なくて済むのですが、利用する側からすると、RGBプレーン方式のほうが過去の資産を流用しやすいのではないかと思います。というか、私自身が過去に作成したゲームがこの形式だったりするので、ここは譲れないのです。それに、8色カラーでメモリを無駄なく利用するには、この形がベストです。ドットの並びは、16ビット単位で左端を最上位ビットとする形式にしました。これも過去の資産継承のためです。具体的な信号生成の手順は以下のようになります。
1.ビデオメモリからR、G、Bの各ビットを読み出しカラー番号に変換
2.カラー番号に対応した、信号データテーブルのアドレスを計算
3.テーブルから読み出したデータを正確なタイミングで、DAコンバーターのポートに1波長分出力
これを1ライン160ドット分ひたすら続けます。1波長4分割で出力し続けるためには、先述の通り、3命令クロックごとに1回の出力が必要です。1波長は12命令クロックなので、言い換えると、出力命令を除くと8命令クロックしか1ドットの信号データ生成にかける時間がありません。上記の手順1の部分でシフト命令を6回行う必要があり、少しでも信号生成に時間を掛けられるよう、今回は1波長3分割での出力とすることにしました。これだと、12命令クロック中、9クロックが信号生成に掛けられます。この場合、プログラムは以下のように書くことができます。
1ドット目も2ドット目も、DAコンバーターへの出力以外に9命令で信号生成しています。しかし、このままではドット間に隙間ができてしまいますので、実際のプログラムでは、タイミングを取るためのNOP(No Operation)命令の代わりに、次のドットの信号生成部分を入れ込みます。そのため、プログラムは大変読みにくくなってしまっていますが、これでちょうど隙間なくカラー信号出力が可能となります。3ドット目以降も2ドット目と同じように続けます。そして、17ドット目からまた1ドット目と同じことを繰り返します。ただし、ループカウンタをチェックしたり、ジャンプしたりする時間はないので、160ドット目まで、ひたすらこのまま羅列していきます。12命令×160ドットで実に1920行にもなりますが、PICのプログラム領域は大きいので、大したことはありません。
最初に書いたように、今回の実験に使用した回路は非常にシンプルです。DAコンバーターはネコ8同様の5ビットにしました。RB0〜RB4を使用しています。PICの出力にインバーターの74HC04をつないでいるのは、バッファーとしての役割です。PIC24HシリーズやdsPIC33Fシリーズは1ポート当たり4mAしか流すことができません。これがPIC24Fシリーズだと25mAまで流せるので十分だったのですが、仕方なく手元にあったインバーターをバッファー代わりにしました。そのため、副作用でカラー信号テーブルを含め、全ての信号をビット反転しておく必要があり、少し分かりにくくなってしまいました。なお、プログラムの都合上、RB5〜RB7にも常時データが出力されていますので、何もつないではいけません。
3.58MHzのクロックは内部設定で24倍しています。クロック源には必ず水晶発信子を使ってください。私は最初ケチって、セラミック発信子を使ってしまいましたが、精度が足らず、カラーキラーに引っ掛かり全く色が出ませんでした。
デモプログラムを作成し、我が家の液晶テレビで表示させた写真を掲載しました。カラーサブキャリア120度ごとの出力で、うまく色が表示されるか心配しましたが、写真の通り、8色を十分判別可能な表示とすることができました。
画像データ表示部分ではCPUパワーを全て使っているため、どの程度CPUの空き時間があるかが気になりましたが、シミュレータで確認したところ、1映像フィールドあたり40%弱の空き時間があることがわかりました。これは平均すると約16MIPS相当となり、PIC24Fの最高速度と同等程度の余力がまだ残っていることになります。割り込みを多用したことも効果的だったと思います。これだけ余力があれば、アクションゲームの実行などにも十分耐えられるものと思います。実際、私が過去に作成したシューティングゲームをカラー化してみましたが、問題なくスムーズに動作しました。YouTubeにアップしたので再生してみてください。
今回の実験を参考に、PICでのカラー映像出力に対応した応用作品にチャレンジしてはいかがでしょうか。