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プロセスとした方が便利だしメンテも楽そうです。

SRAMの初期状態

前々々回の記事で電源投入直後のSRAMの中身が全部ゼロだと思ってCPUの動作確認したことを書きましたが、実際に確かめてみました。

  1. Z80ボードをON
  2. Arduinoをアタッチ(即BUSRQ)
  3. PCでRAM全領域を吸い出してdump
  4. バイナリエディタで内容確認

結果は…

f:id:marlesan:20170512001555p:plain:w400

どう見ても不定状態です。本当にありがとうございました。全体俯瞰した時にパターンが見えるのは、シリコン層のレイアウトによるものでしょうか。M1の周波数で1MHzピッタリ出たのは、偶然HALTが実行された時だったのかも(NOPがリピートされるらしい)。

ついでにRAMを0xFFでクリアし、前回のFizzBuzzプログラムを転送→実行した直後でもダンプ取ってみました。 0000Hから77バイトのプログラムがあり、C000Hから100バイトのFizzBuzz結果があり、F000Hから上方向にスタックデータが…って

f:id:marlesan:20170512002141p:plain:w400

あれ、ちょっと違うぞ……? 全く身に覚えのない位置に謎データがあります。

確認のためもう一度同じ手順でダンプ。

f:id:marlesan:20170512002630p:plain:w400

最後の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からカウントアップする数字を書いていって、

  • 3の倍数の時は 0xff (Fizz)
  • 5の倍数の時は 0xbb (Buzz)
  • 3の倍数かつ5の倍数の時は 0xfb (FizzBuzz)

としつつ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のブートロード(と言っていいのか)が上手くいきました。

  1. BUSRQでバス権を取る
  2. RAMにプログラムを転送
  3. 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

そして実測値は、

f:id:marlesan:20170507050038j:plain:w400

バッチリ。
……とはいえ、そろそろこの方法から脱却したいですね。今回はArduinoが動作中のZ80ボードに直接繋がっているので、BUSRQでZ80を止めてからRAMを見に行けば複雑な動作の確認も可能ではあります。ですが、ここはもうちょっと踏み込んで、Z80側に「レジスタ内容をRAMにダンプした上でArduinoにバス権を明け渡す」ようなシステムコールを実装する予定です。デバッグするのにすごく役立ちそう。