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から映像を出せました!
自力で生成する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は試してみよう。
参考ページ
- http://tinyvga.com/vga-timing
- http://diode.matrix.jp/VGA/index.htm
- http://www.net.c.dendai.ac.jp/~anada/
大変勉強になりました!
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 のセットは忘れないでしょう。
[残課題]
次はCUIツールを改良します。現状はプロセス内の独自プロンプトから連続的にコマンド入力する形式ですが、git のように1コマンド1プロセスとした方が便利だしメンテも楽そうです。
SRAMの初期状態
前々々回の記事で電源投入直後のSRAMの中身が全部ゼロだと思ってCPUの動作確認したことを書きましたが、実際に確かめてみました。
結果は…
どう見ても不定状態です。本当にありがとうございました。全体俯瞰した時にパターンが見えるのは、シリコン層のレイアウトによるものでしょうか。M1の周波数で1MHzピッタリ出たのは、偶然HALTが実行された時だったのかも(NOPがリピートされるらしい)。
ついでにRAMを0xFFでクリアし、前回のFizzBuzzプログラムを転送→実行した直後でもダンプ取ってみました。 0000Hから77バイトのプログラムがあり、C000Hから100バイトのFizzBuzz結果があり、F000Hから上方向にスタックデータが…って
あれ、ちょっと違うぞ……? 全く身に覚えのない位置に謎データがあります。
確認のためもう一度同じ手順でダンプ。
最後のCANDIVからのリターンアドレス(0036H)とFIZBUZからのリターンアドレス(000FH)が積まれているので、これが正しいはず。
よく見ると謎結果のダンプは、プログラムの先頭がBB 00 F0
になっています。
LD SP,F000H
に対応するマシン語は31 00 F0
なので、書き込みに何かバグがあるかもです……。
ちょっとBB 00 F0...
がどう実行されるか追いかけてみます。
BB CP E 00 NOP F0 RET P 3E 00 LD A,0 ;この行以降は正常
レジスタは基本、電源投入直後は不定のよう。CP E
でPが0にセットされるなら、SPが不定なままLD A,0
以降は元のFizzBuzzが実行されます。
P=1だとRET P
で…どこに戻るんだ。SP不定ならどこかからFFFFHをPCにPOPする可能性が高いはず。FFFFHのデータもFFHだからRST 38H
でFIZBUZルーチンの中途半端な位置に飛んで……って、FFFFHにはなぜかBBHがダンプされてました。……これはちょっとお手上げですね。
スタックらしき領域に36 00 0F 00
以外の値があるので一旦暴走してからFizzBuzzに戻ってるようには見えます。
この先Z80の動作がわけのわからないことになったら、今回の現象を念頭に置こう……。
Z80アセンブラでFizzBuzz
レジスタ内容をダンプするシステムコール云々などと言ってましたが、もうちょっと複雑なアセンブラの動作確認がどうしてもしたくなって、BUSRQしてRAMを見にいく方法をさくっと試してみました。
今回のプログラムは遂にLチカを卒業して、FizzBuzzをやってみます。RAMに1からカウントアップする数字を書いていって、
としつつ100(0x64)まで数えます。
STACK: equ 0xf000 org 0x0000 ld sp,STACK ; Turn off LED ld a,0 out 0,a ; Do FizzBuzz ld b,100 ld hl,0xc000 call FIZBUZ ; Turn on LED ld a,1 out 0,a halt ; FizzBuzz counter ; [parameters] ; B -- a number upto which you want to play FizzBuzz ; HL -- starting address to record the result ; [result] ; The counted numbers are recorded in RAM from HL. FIZBUZ: ld a,0 _LOOP: inc a ; Is A divisible by 15? ld d,0x0f call CANDIV jp nz,_DIV3 ld c,0xfb ; Let's say FIZZBUZZ! jp _DUMP ; Is A divisible by 3? _DIV3: ld d,0x03 call CANDIV jp nz,_DIV5 ld c,0xff ; Let's say FIZZ! jp _DUMP ; Is A divisible by 5? _DIV5: ld d,0x05 call CANDIV jp nz,_NOP ld c,0xbb ; Let's say BUZZ! jp _DUMP _NOP: ld c,a ; Say it as it is. _DUMP: ld (hl),c inc hl djnz _LOOP ret ; CANDIV tests if A is divisible by D. ; [parameters] ; A -- dividend ; D -- divisor ; [result] ; F -- Z == 1 if divisible CANDIV: ld e,a _SUB: sub d jr z,_EXIT jp nc,_SUB _EXIT: ld a,e ret
どうってことない処理ですが、CANDIV(割り切れるか?)の実装はちょっと時間かかりました。 はじめは % (mod演算)を実装しようとしてたんですね。 すると余りを出すのに、ボローが出たら除数を足すか、計算過程を1回分キャッシュするかというわちゃっとした解法しかすぐに浮かばなくて悩みました。 でもよく考えたら「割り切れるかどうか」だけ分かればいいので、Aがゼロになるかボローが出るかを見るだけでいいですよね。 これならまぁ、正解に近い感じがします。
さて、上記コードをアセンブルして実行すると0xc000番地からFizzBuzzの結果が書き込まれるはずです。 前回までは「アセンブルした機械語をArduinoファームウェアのソースにuint8の配列として埋め込む」という原始的な方法でプログラミングしていましたが、 今回はCUIソフトも作ったので、転送→実行→停止→確認がPC側から全部行えるようになりました。
以下、その操作ログです。
$ ./loader.rb requesting connection.. connected! MAX_BLOCK_SIZE: 96 erasing RAM data...done in 0.10248114200021519 sec. cmd?> wfile fizzbuzz.rom programming the romfile...done in 0.012214564000259998 sec. reading back the romfile to verify...done in 0.020541404000141483 sec. SUCCEEDED!! dumped the readback image to "_fizzbuzz.rom" cmd?> reset let the board...GO! cmd?> busrq requesting bus control...accepted! cmd?> dump c000 100 01 02 ff 04 bb ff 07 08 ff bb 0b ff 0d 0e fb 10 11 ff 13 bb ff 16 17 ff bb 1a ff 1c 1d fb 1f 20 ff 22 bb ff 25 26 ff bb 29 ff 2b 2c fb 2e 2f ff 31 bb ff 34 35 ff bb 38 ff 3a 3b fb 3d 3e ff 40 bb ff 43 44 ff bb 47 ff 49 4a fb 4c 4d ff 4f bb ff 52 53 ff bb 56 ff 58 59 fb 5b 5c ff 5e bb ff 61 62 ff bb cmd?> exit $
GOOD!
ブートロード成功
システムとしてはまだ完成してませんが、とりあえずArduinoからZ80のブートロード(と言っていいのか)が上手くいきました。
- BUSRQでバス権を取る
- RAMにプログラムを転送
- Z80をリセット(RESETアクティブ中にBUSRQを落とす)
動かしたプログラムはやっぱりLチカです。
org 0x0000 ld sp,0xf000 BLINK: ld a,0x01 out 0,a call DELAY ld a,0x00 out 0,a call DELAY jp BLINK DELAY: push af push bc ld a,0x00 _LOOP1: ld b,0x00 _LOOP2: djnz _LOOP2 inc a jp nz,_LOOP1 pop bc pop af ret
PIOついてないですが、74HC74の余った回路にIORQ&WRをトリガとしてD0をラッチさせるようにして、out命令(アドレスはなんでもいい)によるLチカを実現してます。
動作が正常かどうかの確認は例によってLチカの周波数を測定する方法で。前回、前々回と同じように計算していくと……
- DELAY = 29 + (7 + (13 * 255 + 8) + 22) * 256 + 30 = 858171クロック
- BLINK = 80 + DELAY * 2 = 1716422クロック
- Lチカ周波数 = 1 / (BLINK * 250ns) = 約2.33Hz
そして実測値は、
バッチリ。
……とはいえ、そろそろこの方法から脱却したいですね。今回はArduinoが動作中のZ80ボードに直接繋がっているので、BUSRQでZ80を止めてからRAMを見に行けば複雑な動作の確認も可能ではあります。ですが、ここはもうちょっと踏み込んで、Z80側に「レジスタ内容をRAMにダンプした上でArduinoにバス権を明け渡す」ようなシステムコールを実装する予定です。デバッグするのにすごく役立ちそう。