読者です 読者をやめる 読者になる 読者になる

HD6445+XGAディスプレイ

HD6445の使い方を勉強中です。

グラフィックボード(CRTC)その1
こちらのページを大いに参考にさせてもらってます。もちろんデータシートも。

で、気付いたことメモ。

  • 同期信号のタイミング指定がキャラ単位なので、先日PSoC5でうまくいったタイミングと同じにできない。HD6445が出す信号をPSoC5でエミュレートして検証する必要あり。
    • 特に垂直はかなりズレそう。ただしトータルは R4 VERTICAL TOTAL ADJUST で調整できるはず。
  • ラスター(1文字あたりの縦px数)は1文字8*8pxなので8。ではなく、32を設定する。で、ラスタアドレスは下位2bitを無視してキャラROMに繋ぐ?
    • 256*192pxを縦横4倍してXGA(1024*768px)としてディスプレイに出力するための方策。
    • データシートのINTERNAL BLOCK DIAGRAMによるとラスターカウンタは2段あって、2段目がスムーススクロールのコントロールに使われてるよう。つまり、上記のような拡大出力にも対応していて、XGAレベルでの4px移動をスムーススクロールとして扱ってくれるということなら、すごく考えられたICだ。その機能を使うかは別として…。

追記

  • スムーススクロールは行ごとの開始ラスターアドレスがR29に設定した値になる機能のよう。ラスター上限を超えると次の行のデータが出てくるような図があるので、そのタイミングでVRAMのアドレスが次の行+ラスターアドレスが0にリセット、となるのでしょう。つまりR29を+4ずつ足していけば4倍拡大出力時のスムーススクロールも実現可能、のはず。
  • それとは別に、縦2倍ならCRTCで対応可能(RASTER INERPOLATION)。同じラスタのタイミングを2回ずつ出力してくれる。
  • SKEW。詳しい説明がないので常識的な概念なんだろうか…たぶん、可視範囲でアクティブになる信号DISPTMGのディレイをかけられる(キャラクタ単位)。先日考慮した「走査に先行して出力データを作っておかないといけない」というのはこれで対応できそう。走査の方を遅れさせるわけですね。この場合、同期信号も遅れてくれた方がいいと思うけど、その辺はどうなってるんだろう。
  • 分割スクリーン機能あり。どういう仕組みだろう?と思ったけど、各スクリーンごとにVRAM上の開始アドレスを設定できるらしい。なるほど!

キャラクタディスプレイ考

1文字8*8pxを256*192のスクリーンに敷き詰めると32*24文字。1文字(の1ラスタ)のデータを用意するのに使える時間は16MHz/8の逆数で500ns。85nsのSRAMで3メモリサイクルはいける?

  1. VRAMから文字コード読み込み
  2. キャラクタROMからシフトレジスタへ8bitのラスタを読み込み
  3. 色などの指定を読み込めそう

色指定するならDACの経路をスイッチして、そこへラスタデータをピクセルクロック(16MHz)で押し出す。てな感じでしょうか。 走査が始まるまでに押し出し準備完了している必要があるので、データ読み込みは走査タイミングより先行しないとですね。 そう考えると水平同期や垂直同期のタイミングって色々工夫ができそうな期間に思えます。でもまぁ、とりあえずシンプルに。

次の問題はVRAMからデータを読み込む方法です。これはDMA(グラフィック回路が直接RAMにアクセス)になると思います。 この時VRAMがメモリ空間にあるならCPUを止めないとなんですが、4MHzで動作してるところに2MHzのタイミングでバス権取ったらかなり遅くなりそう。 というか都度BUSRQする方式ではCPUの状態次第で走査タイミングに間に合わない場合も出てくるのでは?

  • VRAMをIO空間に置く
  • VRAMをメモリ空間に置くが、メイン空間と別バンクにする

単純に思いつく別解は以上で、どちらもCPUからのアクセスが明示的になるので、それ以外の期間はCPUバスから切り離しておいてグラフィック回路が自由にアクセスできるようにする方法です。 今度はCPUからVRAMに書き込むタイミングが難しくなりますね。IO方式だとさらにCPUからVRAMへのアクセスが遅くなるし、使える命令も限られてきそうです。 どうするのがいいかなー。

サイクルスチールというキーワードが気になったので調べてみます。

追記

85nsのSRAMで3メモリサイクルはいける?

むりかも。バスにデータが出てくるまでに16MHzの1クロックじゃ足りないので2クロック待って、3クロック目で出力先がホールド、なので1メモリサイクル=3クロック。65ns*3クロック*3サイクル=585ns。oh…

  • A案:色を諦める。もともとオプションだからこれでいいけども。
  • B案:色情報だけ1文字4bitとすれば2文字分取ってこれる計算になる。ただ2文字処理ごとに3サイクル目を入れて結果をラッチするという回路がめちゃめんどくさいはず。
  • C案:データ待ち130ns・ホールドに65nsどっちも無駄があるので、ベースクロックを32MHzとかもっと細分化して、RAMアクセスはそっちで動かす。現実的。

追追記

いや、VRAMとキャラROMにアクセスするためのアドレスはHD6445が生成するから、そのタイミングも合わせて考えないと何ができるか確定しない…。

Z80システムのグラフィックボード

解像度256*192でスペック下げたつもりでしたが、8bitCPUのゲーム機と考えたら限界近いサイズな気がしてきました。 ハードで相当工夫しないとMSXとかファミコンみたいにならないですね。当たり前か。

目下の問題は、VRAMから映像信号作るのに、使用予定のSRAMのアクセス速度が足りないことです。 ピクセルクロック16MHzだと1pxの信号は65nsで作らないといけないと思うんですが、手持ちのSRAMは85ns。 1px処理するのに、リアルタイムに読み込めるデータって1Byteもない計算に……。

なんだか知識経験ゼロ故に根本的に勘違いしてる感あるので、まずシンプルなところから始めたいと思います。 白黒のキャラクタディスプレイドライバ製作を次の目標としましょう。 HD6445を使ってみる予定。

PSoCでVGA出力!

PSoCやばいよ。
ものの数時間でカラーバー出せちゃったよ。

これで1000円ぽっちなんて…。


というわけで、何ができるかも良く分からないまま、単に安いからという理由で買っていた PSoC 5LP Prototyping Kit を使ってVGA出力に再挑戦しました。 なんとなく、回路図を書くようなビジュアルプログラミングでマイコンに食わせるバイナリを吐ける感じのフレームワークだと思ってましたが、とんでもない勘違いでしたね。 ペリフェラルを自由に設計してワンチップマイコンそのものを作るような仕組みだと、とりあえず素人目に理解しました。 しかもアナログ回路もいけるという。 完全に手に余る代物です。

PSoCを始めましょう(Lチカまで) - Qiita
こちらの記事で基本操作を一通り勉強させてもらって、あとは勘でアタックです。

先日の実験でビデオ信号のタイミングはばっちり把握できたので、改めて目標地点を定めます。

希望要件としては「モニタ全画面を使う」「正方形のピクセルの2つ。 つまりモニタのフルスペック1024x768から半々にしていった解像度のどれかになります。 そこに「素人が作るZ80システムでどうにかできる規模感」という制限事項を勘案するとずばり、256x192px ということになるんじゃないでしょうか。

で、再びこちらのページXGAのタイミングを確認させてもらうと……。

水平方向のタイミングが全部4で割り切れるので、ピクセルクロックの周波数(Pixel freq.)も1/4としてよさそうです。 上記資料では65MHzとあるところキリが悪いので64MHzと考えると1/4で16MHz。 この丸めの結果、垂直周波数が59Hzくらいになりますがモニタのスペックとしては問題なし。

16MHzのクロックを基準に、水平方向を数えるカウンタ、垂直方向を数えるカウンタを動かして同期信号と色信号を作ります。 色に関しては可視範囲内でのみアクティブになるOE信号のようなものが出せれば今回はOK。

以上を念頭に入れてPSoC Creatorコンポーネントを色々見ていった結果、


こんな回路になりました。 デジタル回路のセオリーのようなものは全く知らないので、アホなことやってる可能性が高いですが…。 数を数えられるコンポーネントはConter、PWMなどもありましたが、1個のカウンターで複数回の値比較をしようと思ったらBasicCounter+DigitalComparatorに落ち着きました。

回路部分にはカウンターだけ置いて、カウント値から信号を生成するのはソフトウェアで書くという方法もあるみたいです。 でも、タイミング重要なのでデジタル回路だけで実装した方がよいんじゃないかなと。

pixel = line = 0
while true do
  hsync = pixel < 34
  h_visible = 74 <= pixel && pixel < 330

  vsync = line < 6
  v_visible = 35 <= line && line < 803

  visible = h_visible && v_visible;

  pixel += 1
  pixel %= 336
  line += 1 if pixel == 0
  line %= 806
end

処理内容をプログラム脳で考えると上記のような感じです。 垂直方向は4pxずつというわけにはいかないのでXGAの806ラインを全部数える必要があります。 また、同期信号はアクティブLOWなので、回路では不等号が逆になっています。


こちらはクロック設定。 16MHzを直接作れなかったため、48MHzから3分周しています。 はじめ左上のXTALに16MHzを設定してましたが、external、つまり自分で繋いだ外部水晶のパラメータを入力するものだと気付かずにドハマリしました。


ピンアサイン。ここも罠があって、P0あたりにアサインしたらアナログ回路と繋がってる?ピンだったらしくVSYNCが鈍ってしまいました。 デフォルトで役割があるらしきピンを避けると自由にできるのは10個もないですね……というのはおかしいので、 いらない機能をdisableできるとか、デジタル/アナログごとにアサインしていいピンがあるとか、ちょっと勉強が必要かと思います。


書いたコードは初期化のみ。得も言われぬ気持ちよさがあります。


ヒャッハー! やってやったぜ!

ここではVISIBLE信号を0.7Vくらいに降圧して赤に繋いでいます。

でもぉ、見たくないですか? カラーバー……。

こうして……


こう!

可視範囲でピクセルをカウントして、カウント値の適当なビットをRGBとして出力すれば8色のバーが出せますね。 使用するビットとRGBへのアサイン先を変えれば、色が変化する周期や順番を変えられます。

楽しすぎるぅ……。

ちなみに、カウンタのバスからビット毎のワイヤを引き出す方法はこちらです


というわけで、ビデオ出力の目途は立った感じです。 VRAMの設計などまだ課題はありますけどね。256*192とはいってもフルカラー・フルグラフィックなんてのは難しいでしょうし。

それから、今回出力した映像はちょっとノイズが酷いです。


ピクセル境界がギザギザのゆれゆれで、これも解消の要有り。 単にクロック精度の問題であってくれると非常に助かります。今回は水晶使ってないので。

追記

16MHzの外部水晶を使ったらピシっとした映像になりました!

追追記

回路ちょっと修正。 LineCounterのインクリメントが想定より約1クロック早くなってしまってました。 BasicCounterはクロックの立ち上がりでカウント動作するので、前掲の回路だと水平カウンタがラインの右端に達した瞬間、垂直カウンタが1増えます(今回の回路では問題にならないですが)。 ならばLineCounterのclockの前にnot入れればいいのかというと、そもそも前段カウンタのキャリーを後段のクロックに入れる、いわゆるリップルカウンタが良くないみたいですね。 後段カウンタの変化が遅延してしまいます。


前段カウンタのキャリーを次段のclockではなくenableに入れてclockを共通とすれば、前段リセットと後段インクリメントが同時になってベター。

上記の気付きは ディジタル回路テイクオフ指南 という本によるものです。 教科書的でなく、この世界の常識・非常識を切り分けるセンスが語り口から滲み出てくる感じで大変勉強になります。 まぁ、リップルカウンタが非同期式なんていうのは学生レベルの常識かもしれませんが……。

VGA信号解析

DVI-D→VGA変換ケーブルでPCから映像を出せました!

f:id:marlesan:20170518002837j:plain
自力で生成するVGA信号の目標が垂直同期60Hzあたりなので、PCからの出力を60Hzに設定して……


こんな形で信号線をブレッドボードに引き出し、AnalogDiscovery2のオシロで波形を観測してみます。


まずは水平同期パルスまわりの拡大。 画面には RGB(255, 0, 0) を全体に表示するようにしているので、可視範囲では赤の信号がが最大レベルの0.7Vで出力されています。 可視範囲を抜けて水平同期が立ち下がるまでがフロントポーチで0.4usほど。 パルスが約2us続いて、非アクティブに立ち上がってから次の可視範囲が始まるまでがバックポーチで約2.5us。

XGA Signal 1024 x 768 @ 60 Hz timing
上記サイトの値と合致していますね。


水平同期の周期は約20.7us。Vertical refresh 48.363095238095 kHz やモニタに表示されていた 48.3 kHz の逆数に合致します。


垂直同期信号の全景はこんな感じ(タイミングを見やすくするよう上下にずらしています)。 水平同期6ライン分、アクティブが続いています。 水平同期が立ち下がるタイミングでフリップするのはこうして測ってみないと分からなかったかも。

これだと垂直同期のフロントポーチ、バックポーチが測れないので、垂直同期と赤レベルで比べてみます。


お、なんとなく水平同期のタイミングもわかるのでこのまま数えられそうですね。 フロントポーチが3ライン、バックポーチが29ライン分あります。 ここも前掲のサイトと合致してます。なるほど。

さて、以上の計測でなにがわかったかというと……

  • モニタはちゃんと映る
  • 先日のVGA信号生成実験で参照していたデータも合ってる

よってArduinoで愚直に書くのでは信号のタイミングを合わせるのは難しいという結論が裏付けされたのかな?と思います。 NOPでタイミング微調整したりとか、内蔵タイマを厳密に使って同期信号を作るとか、そもそもコンパイラでは遅すぎる処理なんかもアセンブラで書くとか、カリッカリのチューニングが必要なようです。

んーどうしましょう。 カジュアルに実験できると思ってのArduinoだったので、アセンブラのレベルまでテクるようならもうハード設計した方が良い気がしてきました。 タイミングはばっちり把握できたことですし。

あ、なんとなく買ってみたPSoCは試してみよう。


参考ページ

大変勉強になりました!

VGA出力実験失敗

割り込みでVGA出力の実験。

ArduinoVGA信号生成するのは、少なくともアセンブラ使わないと辛いっぽい。というところで今日は挫折しました……。

GitHub - smaffer/vgax: VGA library for Arduino UNO

これ使ってもモニタは NO SIGNAL だったので、まずは今回のために買ったVGAモニタがちゃんと外部入力の画を出せるのか検証するのと、出せるならその信号を解析するのが次のアクションです。

とはいっても、おお…VGA出力できるデバイスなんて手元に全くないぞ……。

https://www.amazon.co.jp/gp/product/B00B2HP8AW
これでいけるかな?

BREAK実験

PCからZ80プログラムのデバッグができるよう、BREAKサブルーチンを作ってみました。

call BREAKを実行するとレジスタの内容をRAMにダンプしてHALTします。 PCからはBUSRQでRAMを覗くなり書き換えるなりして、気が済んだらNMIでZ80の実行を再開させます。

ORG_PG: equ     0x0800
STACK:  equ     0xf000
REGDMP: equ     0xc000

        org     0x0000
        ld      sp,STACK
        ; Turn off LED
        ld      a,0
        out     0,a
        ; Jump to the entry point
        jp      ORG_PG

        ; NMI: Continue from the next instruction of "call BREAK"
        org     0x0066
        retn

BREAK:  ; Save A, BC, DE, HL, IX, IY
        ld      (REGDMP),a
        ld      (REGDMP+1),a ; @TODO save flag register
        ld      (REGDMP+2),bc
        ld      (REGDMP+4),de
        ld      (REGDMP+6),hl
        ld      (REGDMP+8),ix
        ld      (REGDMP+10),iy
        ; Save SP, PC
        ld      (REGDMP+12),sp
        pop     hl
        push    hl
        ld      (REGDMP+14),hl
        ld      hl,(REGDMP+6)
        ; It's time for Arduino to control the bus
        halt
        ; Returns here from NMI
        ret

        ; Main program
        org     ORG_PG
        ld      a,0xAA
        ld      bc,0x1234
        ld      de,0x5678
        ld      hl,0x9ABC
        call    BREAK
        ; Turn on LED
        ld      a,1
        out     0,a
        halt

BREAK後にREGDMPのデータを覗くと…

cmd?> dump c000 16
AA AA 34 12 78 56 BC 9A AF BF 00 00 FE EF 0E 08

とりあえず動作する模様。080EH がld a,1のアドレスです。 はじめNMIがPCを退避することを分かってなかったので、SPの位置が想定とズレてしまいプログラム暴走しまくりでした。 これだけ見事に間違えれば NMI → RETN のセットは忘れないでしょう。

[残課題]

  • レジスタの書き出しにPUSHを使う
    • Fレジスタの保存のため。ついでにBREAKルーチンのサイズ圧縮にもなる。
  • BREAK呼び出し方法の策定
    • MSXシステムコールは、ファンクション番号をレジスタに入れてcall 0005Hする形です。この明確な利点がわかれば、似たような方式を採用したい。今は「ラベルで直接callした方が楽じゃん」て思ってます…。

次はCUIツールを改良します。現状はプロセス内の独自プロンプトから連続的にコマンド入力する形式ですが、git のように1コマンド1プロセスとした方が便利だしメンテも楽そうです。