2009年1月20日火曜日

TinyでPONG!(ソフトウェア2)

映像出力の仕方を具体的に説明します。

映像データを出力ポートに滞りなく書きこまねばならないのですが、どのくらいの速度でなければならないのでしょう。水平方向192ドットを、水平映像期間48μs=384クロックで出力するので、1ドット当たり2クロックにすればよいのです。

この速度はプログラムをいくら効率よく書いてもループ(分岐)を使った時点で達成はできません。分岐命令は、後方分岐(ループ)の場合それだけで2クロックを消費してしまうからです。そこで、ループを「展開する」ことにしました。ループを展開するというのは同じ命令をループの回数だけずらっと並べて書くということで、当然プログラムの容量は増大します。

ループを展開するとはということです。簡単ですね。

ループの展開をすれば1ドット出力に2クロックを使うことができます。しかし2クロックしかありませんから、これとてもいくつかの工夫をしなくてはなりません。
  1. 汎用レジスタにあらかじめ映像データを用意しておく。
  2. 1つめのクロックでポート出力を完了させ、
  3. 次の1クロックで汎用レジスタ内の映像データを1ビット(左)シフトする。
直前の周期であらかじめ1行分の映像データを汎用レジスタ(群)に仕込んでおくことで上記のアイディアが実現できます。これは汎用レジスタが32本と多いAVRだからこそできる技で、そのうちの24本をこの目的のためにつかいます。24本×8ビット=192ドットですから。

また、出力命令のOUTは1クロックで完了するのですが、8ビットいっぺんに出力されます。8ビットマイコンなんで当たり前のことですね。その中の1ビットだけ出力したい(変化させたい)という場合、通常はポートを8ビットで読んで→狙ったビットをクリアし→狙ったビット以外の部分を0でマスクしたデータと論理和し→ポートに書き戻すとか、ビットクリア/ビットセット命令を分岐で使い分けたりと結構面倒なもので、とても1クロックで済むような話ではありません。

そこで、出力ポートの他のビットを無視すなわち未使用にすることにしました。そうすると面倒くさい1ビット書き換え処理が全く不要になり、単に8ビットいっぺんに出力するだけで済んでしまうのです。以前「映像出力がポートの最上位ビットであることと、同じポートの他のビットに出力が割り当てられていないことがとても重要」と書いたのはこのためだったのです。

これらを図で示すと下記のようになります。

プログラムの中でこの処理をやっているのはファイル"draw.asm"の以下の部分です。ループの展開はまだるっこしく読みにくいのでマクロを使っています。

8ビット分のループ展開(マクロ定義)

.macro vout8px
out PORTB, @0 ; +1=1 output bit7
lsl @0 ; +1=2
out PORTB, @0 ; +1=3 output bit6
lsl @0 ; +1=4
out PORTB, @0 ; +1=5 output bit5
lsl @0 ; +1=6
out PORTB, @0 ; +1=7 output bit4
lsl @0 ; +1=8
out PORTB, @0 ; +1=9 output bit3
lsl @0 ; +1=10
out PORTB, @0 ; +1=11 output bit2
lsl @0 ; +1=12
out PORTB, @0 ; +1=13 output bit1
lsl @0 ; +1=14
out PORTB, @0 ; +1=15 output bit0
clr @0 ; +1=16 clear this segment for next timing
.endmacro

24レジスタ分のループ展開

vout8px r0 ; 16*24=384cycles(visible period)
vout8px r1
vout8px r2
vout8px r3
vout8px r4
vout8px r5
vout8px r6
vout8px r7
vout8px r8
vout8px r9
vout8px r10
vout8px r11
vout8px r12
vout8px r13
vout8px r14
vout8px r15
vout8px r16
vout8px r17
vout8px r18
vout8px r19
vout8px r20
vout8px r21
vout8px r22
vout8px r23
out PORTB, r23 ; 0+1=1 set to blank level

最後のOUT命令は描画が終わったら背景の黒レベルにもどす処理です。これをやらないと右端のドットが1だった場合、白い線が画面の右端までびよ~んと尾を引いてしまいます。

2009年1月19日月曜日

TinyでPONG!(ソフトウェア1)

さあ、ここからが本番、ソフトウェアの説明に入ります。早速ソースコードをアップロードしておきました。


トップレベルのエントリーは"pong.asm"です。拡張子を見ただけで分かると思いますが、8MHzという低速のCPUでビデオ信号のタイミングにシビアに合わせるためアセンブラを使っています。AVRのような簡易なマイコンはパイプライニングなどの高級な高速化技術を一切使っていないため、命令表の消費クロック数の値がそのままタイミングカウントとして簡単に計算できますので、アセンブラで記述すればタイミングを正確に合わせることができます。

時代はデジタルハイビジョンに向かっているのに今更ながらアナログTV信号(NTSC方式)を扱いますが、アナログ放送の停波まで公式にはまだ2年ちょっとありますし、全世帯からアナログTVがなくなるのはまだまだ時間がかかると思います。逆にデジタルTVへの切り替えで不要になるアナログTV(壊れても泣かなくて済む!)を実験などに存分に使えるようになる絶好の機会が到来すると前向きに考えましょう。

NTSC信号を使ってTVに絵を出すためにどんな信号をどのようなタイミングで出さなければならないかは、こちらなどのサイトに素晴らしい説明がされていますので参照していただくことにして、本プロジェクトではそのタイミングをどのようにして作り出しているのかを説明します。なお、本プロジェクトで扱う映像信号は白黒で、カラーではありません。また、グレースケールはなく2値の白黒ーーすなわち1画素=1ビットです。

信号出力のタイミングは唯一、16ビットタイマーのTimer1からの割り込みによります。Timer1は原クロック8MHzでカウントアップし、水平同期信号の周期(63.555μs≒64μs)に達したところでゼロクリアして自動的に循環するモードに設定。このカウンタの2つある比較レジスタ一致の1つOCR1Bでの一致により発生する割り込みで映像信号出力を開始します。同時に比較レジスタOCR1Aの値によるPWM動作を設定し、OC1Aピン(=PB1ピン)から水平同期信号を自動的に出力しています。


これをこのように作ります。無理やりテキストで書いて、見にくくてすみません。のこぎり状の波形はTimer1カウンタの内容です。


水平方向にいくつカウントするかは64μs×8MHz=512カウントですのでTimer1は0から511まで変化します。カウンタがOCR1Bレジスタと一致したところで事前に準備してあった水平1行分の映像データをタイミングをきちんと計算されたプログラムで1画素=1ビットずつ出力します(図中の「映像送」)。1行出力した直後には次の行のデータを準備しておきます。図中「備」と書かれた部分がそれに相当します。準備してから次の割り込みが来るまでの間は、たった数クロック程度の余裕しかありませんが、スリープ(図中の「Z」)して待ちます。

割り込みハンドラ(ソースtim1oc1b.asm)は垂直方向のカウント、垂直同期の生成、パドル=簡易A/Dコンバータの状態遷移の管理、音声出力など軽い処理をするのみです。

割り込みハンドラから戻ってくるとメインプログラム(main.asm)のスリープ命令が解除されて、映像送出(draw.asm)と次の行の準備(同)を行います。

OC1A出力はOCR1Aレジスタでの一致で"L"を出力、カウントトップすなわちICR1レジスタとの一致で"H"を自動的に出力するような高速PWMモードの設定にしてあります。これが同期信号出力になります。

水平方向の表示期間が終わり、垂直ブランキング期間に入ったかどうかはメインプログラムの冒頭で判断しています。本プログラムでは水平20行分がその期間となります。その中の3行分の期間は水平同期信号の極性反転を行います。これが垂直同期信号となります。極性反転は前述のPWMの条件を反転するだけで実現しています(つまり、OCR1A一致で"H"、トップで"L"になるように)。

垂直ブランキング期間では、映像出力が必要なくなるのでこの時とばかりにゲーム本来の動作を実行します(game.asm)。表示期間とブランキング期間合わせて全部で256行あるうちたった20行がゲーム進行に使われますので、本来の7%のパフォーマンスしか得られません。8MHzで公称8MIPSのAVRでも約0.6MIPSとなります。それでもこの程度のゲームであれば全く問題なくスムーズに動作しています。

2009年1月7日水曜日

TinyでPONG!(ハードウェア)

長らくのお待たせでした。PONG復活です。今回は回路図の公開とハードウェアの解説をしましょう。

例によって秋葉原の某所(ほとんど秋月ですが…)で入手しやすくできるだけ安価な部品ばかり集めています。基板を除けば千円でお釣りがくるくらいです。

Tiny2313は約8MHzのRC内蔵発振器を持っていますが、わざわざセラロックを用いているのは周波数の安定性が重要になるからです。もちろん水晶発振子でもかまいません。ためしに内蔵発振器を使って画面出力をしてみたところ、絵がゆらゆら、ばらばらでとても見られたものではありませんでした。温度係数もやたら大きくて、温めたり冷やしたりすると揺れ具合も大きく変化します。

映像出力はPB7、同期信号出力OC1A(=PB3)で、これらはダイオードとRを使って混ぜてコンポジット出力としています。実は映像出力がポートの最上位ビットであることと、同じポートの他のビットに出力が割り当てられていないことがとても重要なのです。(PB3が出力として使っているように見えますが、これはあくまでもOC1A出力として使っているので影響ありません。)

音声出力はPD6から出しています。出力コンデンサを省略していますが、心配ならば100uF程度のアルミ電解を直列につなぐと良いでしょう。

パドル入力用の簡易ADCにはコンデンサーへの充放電を制御する出力PD4、PD5と放電時間を計測するための入力PD0、PD1を使います。PD4またはPD5をHにしてダイオードを経由して急速に充電したのちLに切り替えます。放電はボリュームの抵抗を通って徐々に行われます。コンデンサの端子電圧がVILまで下がったことをPD0またはPD1でセンスし、それまでの時間をボリュームの抵抗値=パドルの位置として求めるのです。

ありがたいことにTiny2313は旧製品の90S2313と違って3Vで動作しますので、電源は単三乾電池2本でOKです。この回路では電源スイッチはありませんが、ソフトウェアスイッチ(サーブスイッチSW2を長押し)でパワーダウンモードに入ります。この時の消費電流は極めて低くて1μA未満です。

(2009.2.12 回路図のTYNY2313のシンボルマークを修正。)

2009年1月6日火曜日

90S2323でスリープタイマー付きベッドライト(2)

あけましておめでとうございます。
うだうだしている間に時間がたってしまい…年が明けてしまいました。
この先どうなる事やら…ま、気を取り直して更新してまいりましょう。

本件のベッドライトは自宅で毎日便利に使っております。自分としては満足です。

操作はいたって簡単で、白いボタンで点灯、もう一度白いボタンで30分スリープタイマー起動です。消したくなったら黒いボタンを押すだけ。たったそれだけのものなのですが、これで本を読みながら眠ってしまっても大丈夫。照度は十分すぎるくらいにとれていて、逆にまぶしすぎるくらいです。白色LEDの性能上少し青白いので赤とか黄色のLEDキャップをいくつかはめ込むとちょうどいいくらいの感じになります。

写真ギャラリー:
秋月の100円ユニバーサル基板(72x47mm)に組みました。左側から出ている4本の赤いリード線はポリウレタン線で、LED出力となっています。LEDヘッドを支えるアームをケースに固定している部分が見えます。アームは嫁が手芸用に持っていたダイソーの直径3mmのアルミニウム線を使っています。ちょうどポリウレタン線と同じ色の塗装がかかっていてうまい具合です。

ケースはタカチのプラケースSW-95Bだったと思います。白ボタンの上に開いている穴はパイロットランプ用。タイマー動作中は1Hzで点滅します。アームは、軸のアルミ線とLED出力のポリウレタン線にシリコンチューブをかぶせて両端を熱収縮チューブでギュッとしめてあります。


LEDヘッド部を正面から見たところです。グリーンレジストのかかったユニバーサル基板の切れ端にLEDをマウントしています。黒い台座は…





実はヒートシンクを2個連結したものだったのです。分厚い熱伝導シートを基板との間に挟みねじでしっかりと密着固定しています。LEDが低消費電力だと言ってもかなり発熱しますし、その熱がLEDの寿命を縮めますので放熱してあげることは良いことです。このヒートシンクも秋月の1個50円のものです。


ソースと回路図を以下に載せておきました。