最弱のPICでTV出力2012年11月26日 01:38
最弱のPICマイコン、PIC10F200でTV出力をしてみた。
PIC10F200はPICの中で最弱なだけでなく、今普通に店で買えるマイコンの中でも最弱ではないかと思う。
TV出力は以前も16F628Aや10F222でやったことがあった。
それらと今回の10f200の性能を比べるとこんな感じ。
色々違いはあるが、今回問題になるのは処理速度とFlashROMのみである。
16F628AはFlashが2Kワードと(10F2xxに比べれば)潤沢であったのでXORWFとNOPをベタ書きして1サイクル1ドットを出力する力技を使っていたのだが、10F222ではそうはいかないので1ドットあたりデータの読み取りと書き換えの2サイクルを使う方式をとった。
(なお後で知ったが、16F628Aの同期シリアル出力を流用して1ドット1サイクルでメモリから出力する技がある)
ベタ書きバージョン
実際にはNOP2つをGOTO $+1にするなど多少の圧縮は効くので52×52くらいいけたりする。
メモリ読み込みバージョン
解像度は半分になるが、メモリを1ワード8ビットとして使えるので効率的。
今回はFlashが少ないので当然メモリ読み込みバージョンを使ったのだが、10F222は16F628Aの2倍速で動くので都合16F628Aと同じ解像度が出せるのだが、10F200では解像度は半分になってしまった。
なおどちらの場合も、白と黒の切り替わるところで出力をXORで切り替えるので、画像データの持ち方が面倒になる。
最初は読み取るときに変換しようとしていたのだが、その分のコード(6ワード)が惜しかったのでXORした状態のデータを持つようにした。
そのかわりマクロを使ったのでソースでは一応見やすくなっている。
(ちなみにXORで出力を切り替えるのは、出力先のインピーダンス(抵抗)が十分に高い場合でないと正常に働かないらしいが、まあなんか動いてるので大丈夫だったようだ)
コードの流れは次のようになっている。
・画像の1行(走査線12ライン相当)ごとに、
ROMの画像データテーブルを読み込み、メモリに書きだしておく
・走査線1ラインごとに、
メモリを読んでI/Oピンに出力×24回
水平同期信号を出力
・1画面ごとに、
垂直同期信号を出力する
画像データのポインタをリセットする
・15画面ごとに
アニメーションのため画像データのポインタを1画面分ずらす
ROMから直接出力せずにメモリを経由する理由だが、ベースラインのPICはROMからの読み出しに時間がかかるためである。
メモリからワーキングレジスタにデータを読むには
ROMからの読み出しは
10F222では水平帰線期間を使ってROMからの読み出しをしていたのだが、速度が半分の10F200ではかなり無理があったので、1ラインを捨てて読み出しに専念している。画像の1pxごとに1ラインの黒線が入っているのがこれだ。
また、コード短縮に効果があったのが、同期信号の簡略化だ。
同期信号は前回は律儀に出していたのだが、ファミコンなど結構いい加減な同期信号を出していてそれでも大丈夫という情報を得たので、今回は垂直同期と水平同期のみにしてみた。これは難なく成功した。これによりコードサイズの大幅な削減ができた。今回もっともきつかった制限がコードサイズなので、これが無かったら1枚絵になっていただろう。
最後に回路とソースを公開する。
・回路
2本の抵抗で4値を出す一種の抵抗ラダーにPICの0番ピンと2番ピンをつないでいる。
ちなみになんで0と1でないかというと、この石の2番ピンはプルアップ抵抗がついておらず入力に使いづらい仕様のため優先的に出力に使いたいからである。
同軸ケーブルの右はTV内部を示す。映像信号の終端抵抗は75Ωのはず(というか実際測ったらそうだった)。電源電圧3Vの時にこれと分圧して1Vp-pが出るように抵抗値は定めたつもり。ただ、実はこれで出力したところやけに暗かったので何か間違っているかもしれない。
コンデンサはちょうど手元にあった470uFを使っているが、値は適当でよい。
・ソース
HTMLに書くにあたってタブを全角スペース2つに、半角スペース2つを全角スペース1つに置換しているので直さないと動かない。
PIC10F200はPICの中で最弱なだけでなく、今普通に店で買えるマイコンの中でも最弱ではないかと思う。
TV出力は以前も16F628Aや10F222でやったことがあった。
16F628A、「イ」 | 16F628A、猫耳 |
10F222、「イ」 | 10F222、猫耳 |
それらと今回の10f200の性能を比べるとこんな感じ。
16F628A | 10F222 | 10F200 | |
I/Oピン数 | 16 | 4 | 4 |
処理速度 | 1MIPS | 2MIPS | 1MIPS |
FlashROM | 2kワード | 512ワード | 256ワード |
メモリ | 224バイト | 23バイト | 16バイト |
周辺機能 | 色々 | ADCとタイマ | タイマのみ |
16F628AはFlashが2Kワードと(10F2xxに比べれば)潤沢であったのでXORWFとNOPをベタ書きして1サイクル1ドットを出力する力技を使っていたのだが、10F222ではそうはいかないので1ドットあたりデータの読み取りと書き換えの2サイクルを使う方式をとった。
(なお後で知ったが、16F628Aの同期シリアル出力を流用して1ドット1サイクルでメモリから出力する技がある)
ベタ書きバージョン
BCF PORTA,1 ;まず黒にしておくこれを表示する画像のドット数だけ並べる。例えば50×50ドットなら2500ワード。一方16F628AのFlashは2048ワード。
MOVLW 0x01 ;XOR用のマスクを読み込む
XORLW PORTA ;ここで白になる
XORLW PORTA ;ここで黒になる
NOP ;ここで黒のまま
NOP ;ここで黒のまま
XORLW PORTA ;ここで白になる
NOP ;ここで白のまま
実際にはNOP2つをGOTO $+1にするなど多少の圧縮は効くので52×52くらいいけたりする。
メモリ読み込みバージョン
BCF GPIO,1 ;こことこれを1ライン分用意しておき、必要に応じてデータを書き換える。
MOVLW 0x01 ;ここは同じ
btfsc DATA0, 0 ;メモリを見る
xorlw GPIO ;メモリが1ならばここで色が変わる
btfsc DATA0, 1 ;以下同様
xorlw GPIO
btfsc DATA0, 2
xorlw GPIO
解像度は半分になるが、メモリを1ワード8ビットとして使えるので効率的。
今回はFlashが少ないので当然メモリ読み込みバージョンを使ったのだが、10F222は16F628Aの2倍速で動くので都合16F628Aと同じ解像度が出せるのだが、10F200では解像度は半分になってしまった。
なおどちらの場合も、白と黒の切り替わるところで出力をXORで切り替えるので、画像データの持ち方が面倒になる。
最初は読み取るときに変換しようとしていたのだが、その分のコード(6ワード)が惜しかったのでXORした状態のデータを持つようにした。
そのかわりマクロを使ったのでソースでは一応見やすくなっている。
(ちなみにXORで出力を切り替えるのは、出力先のインピーダンス(抵抗)が十分に高い場合でないと正常に働かないらしいが、まあなんか動いてるので大丈夫だったようだ)
コードの流れは次のようになっている。
・画像の1行(走査線12ライン相当)ごとに、
ROMの画像データテーブルを読み込み、メモリに書きだしておく
・走査線1ラインごとに、
メモリを読んでI/Oピンに出力×24回
水平同期信号を出力
・1画面ごとに、
垂直同期信号を出力する
画像データのポインタをリセットする
・15画面ごとに
アニメーションのため画像データのポインタを1画面分ずらす
ROMから直接出力せずにメモリを経由する理由だが、ベースラインのPICはROMからの読み出しに時間がかかるためである。
メモリからワーキングレジスタにデータを読むには
movf MEM, Wの1命令1サイクルで済むのだが、
ROMからの読み出しは
(Wレジスタに対象のアドレスが入っているとして)という3命令6サイクルが必要になる。
call READ_TABLE ;サブルーチンコール
READ_TABLE: ↓サブルーチンの場所
movwf PCL ;対象のアドレスへジャンプ
;↓PCLに書き込まれたアドレス
retlw 0xNN ;定数値を返すリターン命令
10F222では水平帰線期間を使ってROMからの読み出しをしていたのだが、速度が半分の10F200ではかなり無理があったので、1ラインを捨てて読み出しに専念している。画像の1pxごとに1ラインの黒線が入っているのがこれだ。
また、コード短縮に効果があったのが、同期信号の簡略化だ。
同期信号は前回は律儀に出していたのだが、ファミコンなど結構いい加減な同期信号を出していてそれでも大丈夫という情報を得たので、今回は垂直同期と水平同期のみにしてみた。これは難なく成功した。これによりコードサイズの大幅な削減ができた。今回もっともきつかった制限がコードサイズなので、これが無かったら1枚絵になっていただろう。
最後に回路とソースを公開する。
・回路
2本の抵抗で4値を出す一種の抵抗ラダーにPICの0番ピンと2番ピンをつないでいる。
ちなみになんで0と1でないかというと、この石の2番ピンはプルアップ抵抗がついておらず入力に使いづらい仕様のため優先的に出力に使いたいからである。
同軸ケーブルの右はTV内部を示す。映像信号の終端抵抗は75Ωのはず(というか実際測ったらそうだった)。電源電圧3Vの時にこれと分圧して1Vp-pが出るように抵抗値は定めたつもり。ただ、実はこれで出力したところやけに暗かったので何か間違っているかもしれない。
コンデンサはちょうど手元にあった470uFを使っているが、値は適当でよい。
・ソース
HTMLに書くにあたってタブを全角スペース2つに、半角スペース2つを全角スペース1つに置換しているので直さないと動かない。
list p=10F200
#include p10f200.inc
radix dec ;数値のデフォルトが16進だが、10進に変えておくのが好み
; N/C+-v-+(GP3) ;ここに図を書いておくとブレッドボードに組むとき便利
; Vdd| |Vss
; GP2| |N/C
; GP1+---+GP0
__CONFIG _MCLRE_OFF & _CP_OFF & _WDT_OFF ;MCLRオフ、コードプロテクトオフ、WDTオフ。WDT以外はどうでもいい。
cblock 0x10 ;メモリの宣言
linebuf1 ;ラインバッファ×3バイト
linebuf2
linebuf3
linecount ;画像1行ごとの走査線ライン数のカウンタ
imgcount ;画像データ読み出し用のポインタ
wcount ;ウェイト用のカウンタ
blankloopcount ;垂直同期中の走査線ライン数カウンタ
framecount ;アニメーション用にフレーム数(正しくはフィールド数)をカウント
endc
#define lineperrow 11 ;絵の1行あたりの走査線数-1
waitx macro cycle ;指定サイクル待つ; 2ワード
movlw ((cycle)-5)/3
call wait3w4 - (((cycle)-5)%3)
endm
imgdat macro dat1, dat2, dat3 ;画像データをXORされた形式で保存するためのマクロ
retlw low(dat1 ^ ((dat1<<1) | (dat2>>7)))
retlw low(dat2 ^ ((dat2<<1) | (dat3>>7)))
retlw low(dat3 ^ (dat3<<1))
endm
init ;初期化
org 0x00
movwf OSCCAL ;内蔵発振器を較正。
;NTSC信号のタイミングはそれなりにシビアなので、較正しないとTVに映らない。
;とはいえ、63.5usを64usで代用できる程度にはルーズ。
movlw b'10011000' ;以下3つのレジスタの初期化値を兼ねる値
option ;x00xxxxx ;
; ^/GPWU: Don'tCare
; ^/GPPU=0: PullUp ON ←スイッチ入力も視野に入れていたので
; ^T0CS=0: Fosc/4,GP2=I/O ←結局2ピンしか使ってないので要らないっちゃ要らない
; ^T0SE Don'tCare
; ^PSA Don'tCare
; ^^^PS2:0 Don'tCare
movwf imgcount ; 83<=x<=BC or C3<=x<=FC でかつ下6bitが3で割れる数
;これであれば、最初の1フレームは中途半端になるが2フレーム目からは正常。
;この条件を満たさないと、画像の切り替え判定にかからず正常に動作しない。
tris GPIO ;xxxxx000: 全部出力
;linecount: 最初の1回のみライン数が増えるだけなので、初期化不要
;framecount: 最初の1枚の画像のみ長く表示されるだけなので、初期化不要
;blankloopcount: 最初の1フレームのみ同期信号の長さが異常になるだけなので、初期化不要
main ;メインルーチン
;ラインごとのタイムテーブル。0~63サイクル、1サイクル1us。
;電圧値: 0:同期信号、低:黒、高:白
;0
;1: 0 : 水平同期信号始
;6: 低 : 水平同期信号終
;13: 低/高 : dot0始
;15: 低/高 : dot0終、dot1始
;...
;59: 低/高 : dot23始
;61: 低 :dot23終
;63
;line 24
drawloop ;描画ループ。11ライン分繰り返す
bsf GPIO,0 ;6 ;電圧低:水平同期終
call wait4 ;7-10 ←この数字は1ライン内のサイクル数カウント
movlw 2 ;11
btfsc linebuf1,0
xorwf GPIO,F ;13
btfsc linebuf1,1
xorwf GPIO,F
btfsc linebuf1,2
xorwf GPIO,F
btfsc linebuf1,3
xorwf GPIO,F
btfsc linebuf1,4
xorwf GPIO,F
btfsc linebuf1,5
xorwf GPIO,F
btfsc linebuf1,6
xorwf GPIO,F
btfsc linebuf1,7
xorwf GPIO,F
btfsc linebuf2,0
xorwf GPIO,F
btfsc linebuf2,1
xorwf GPIO,F
btfsc linebuf2,2
xorwf GPIO,F
btfsc linebuf2,3
xorwf GPIO,F
btfsc linebuf2,4
xorwf GPIO,F
btfsc linebuf2,5
xorwf GPIO,F
btfsc linebuf2,6
xorwf GPIO,F
btfsc linebuf2,7
xorwf GPIO,F
btfsc linebuf3,0
xorwf GPIO,F
btfsc linebuf3,1
xorwf GPIO,F
btfsc linebuf3,2
xorwf GPIO,F
btfsc linebuf3,3
xorwf GPIO,F
btfsc linebuf3,4
xorwf GPIO,F
btfsc linebuf3,5
xorwf GPIO,F
btfsc linebuf3,6
xorwf GPIO,F
btfsc linebuf3,7
xorwf GPIO,F
nop ;60
bcf GPIO,1 ;61 ;電圧低
goto $+1 ;62,63
nop ;0
bcf GPIO,0 ;1 ;電圧0:水平同期始
nop ;2
decfsz linecount,F ;3
goto drawloop ;4,5
endofrow ;1行の終わり。次の行のために画像をラインバッファに読み込む。
bcf STATUS,C ;5
bsf GPIO,0 ;6
call readgraphic
movwf linebuf1 ;14
; rlf linebuf1,W ;画像をそのまま保存していたときはここでXORをとっていた。
; xorwf linebuf1,F ;17
call readgraphic
movwf linebuf2
; rlf linebuf2,W
; xorwf linebuf2,F ;28
call readgraphic
movwf linebuf3
; rlf linebuf3,W
; xorwf linebuf3,F ;39
movlw lineperrow
movwf linecount ;41
;~62
waitx 62-41+6 ;62-41 ;サイクル数カウントがXORの分がそのままなので、+6で調整。
movlw 0x3F ;63 ;
andwf imgcount,W ;0 ;画像ポインタをチェックするためにWに。
bcf GPIO,0 ;1 ;ここで電圧:0にして水平同期始
btfsc STATUS,Z ;2 ;閑話休題、画像ポインタの下6bitが0なら、
goto vblank ;3,4 ;画面の下端なので垂直同期へ。
goto drawloop ;4,5 ;さもなくばループ。
;line 263
vblank
bsf blankloopcount,2 ;5 ;空の走査線のカウンタ。(0であることを前提に)4を代入。
blankloop1 ;line 1-4
bsf GPIO,0 ;6 ;電圧低:水平同期終
waitx 64-6
bcf GPIO,0 ;1=64 ;電圧0:垂直同期始
movlw 15 ;2 ;下で使う値。ちょうど空いていたのでここでWに置いておく。
decfsz blankloopcount,F ;3 ;垂直同期前の空の走査線をカウント。
goto blankloop1 ;4,5 ;4ライン数えたら垂直同期へ。
;垂直同期信号。3ライン間L、切込みパルス省略。
;アニメーション
decf framecount,F ;フレームカウンタを減らし、
btfsc STATUS,Z ;0なら、フレームカウンタを15にリセット
movwf framecount ;10 ;上でWに置いた値をここで使う。
movlw 0x40 ;
btfsc STATUS,Z ;上で0ならここも0
xorwf imgcount,F ;0なら画像カウンタの6bit目を反転
;
movlw 60
addwf imgcount,F ;12 ;画像カウンタを画像1枚分戻す
;~64=0
;line 5
;~64=0
;line 6
;~64=0
;line 7
;~2
waitx 64+64+64+2-12+1 ;3ライン分のウェイト
bsf blankloopcount, 4 ;空の走査線のカウンタ。(0であることを前提に)16を代入。
blankloop2 ;line 7~24
nop ;5
bsf GPIO,0 ;6 ;電圧低:垂直/水平同期終
;~64
waitx 64-6
bcf GPIO,0 ;1 ;電圧0:水平同期始
decfsz blankloopcount,F ;2 ;垂直同期前の空の走査線をカウント。
goto blankloop2 ;3,4 ;16ライン数えたら最初に戻る
goto drawloop ;4,5 ;line 24
;;メインルーチンここまで;;
wait3w6 ;下に同じく
nop
wait3w5 ;↓に加えてもう1サイクルのウェイト
nop
wait3w4 ;1<=W<=255の時、(3*W+4)サイクルのウェイト。W=0なら256扱い。
movwf wcount
decfsz wcount,F
goto $-1
wait4 ;ここにcallすると、callとretlwで計4サイクルのウェイトになる
retlw 0
org 0x80
graphicdata1 ;3byte × 20行 = 60byte
;画像データ。
;下6bitが000000であることを描画の終了判定に使っていることと、
;0xFF番地は発振器の補正値に使われていることにより、
;下から上の順に記録している。
;また、出力の後にラインバッファの更新をしているため1行ずれて、最初が最上段で次から最下段~最上段の下の順。
;逆にしてバッファ更新後に出力とすればよかったのかな。もう遅いけど。
imgdat 0xC0, 0x9F, 0xFF ;最上段
imgdat 0x20, 0x10, 0x7C ;最下段~
imgdat 0x24, 0x0B, 0x71
imgdat 0x2E, 0x44, 0xC8
imgdat 0x0F, 0xE8, 0x30
imgdat 0x0E, 0x50, 0x4F
imgdat 0x0E, 0x00, 0x3F
imgdat 0x6C, 0xA0, 0x3F
imgdat 0xAD, 0x60, 0x3F
imgdat 0x40, 0x41, 0xFF
imgdat 0x9F, 0xBF, 0xFF
imgdat 0x38, 0xC1, 0xFF
imgdat 0x5A, 0x83, 0xFF
imgdat 0x7D, 0xC3, 0xFF
imgdat 0x27, 0x03, 0xFF
imgdat 0x25, 0x07, 0xFF
imgdat 0x1C, 0x87, 0xFF
imgdat 0x08, 0x07, 0xFF
imgdat 0x00, 0x0F, 0xFF
imgdat 0x00, 0x0F, 0xFF ;~最上段の下
;ちなみに画像をそのまま保存していたときはこう
; dt 0xC0, 0x9F, 0xF2 ;最上段
; dt 0x20, 0x10, 0x7F, 0x24, 0x08, 0x7F, 0x2E, 0x44, 0x7F, 0x0F, 0xE4, 0x7F ;最下段~
; dt 0x0E, 0x58, 0xFF, 0x0E, 0x20, 0xFF, 0x6C, 0xBC, 0xFF, 0xAD, 0x43, 0xFF
; dt 0x40, 0x60, 0x7F, 0x9F, 0xA0, 0x7F, 0x38, 0xC0, 0x7F, 0x5A, 0x80, 0x7F
; dt 0x7D, 0xD0, 0xBF, 0x27, 0x09, 0x5F, 0x25, 0x07, 0xAF, 0x1C, 0x87, 0xD7
; dt 0x08, 0x07, 0xE9, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0 ;~最上段の下
org 0xBD
readgraphic ;画像データ読み取りコード。ちょうど2枚の画像データの隙間に収まるサイズのコードだったのでここに入れた。
decf imgcount,F
movf imgcount,W
movwf PCL
org 0xC0
graphicdata2 ;画像2枚目
imgdat 0xC0, 0x9F, 0xF2 ;最上段
imgdat 0x20, 0x10, 0x7F ;最下段~
imgdat 0x24, 0x08, 0x7F
imgdat 0x2E, 0x44, 0x7F
imgdat 0x0F, 0xE4, 0x7F
imgdat 0x0E, 0x58, 0xFF
imgdat 0x0E, 0x20, 0xFF
imgdat 0x6C, 0xBC, 0xFF
imgdat 0xAD, 0x43, 0xFF
imgdat 0x40, 0x60, 0x7F
imgdat 0x9F, 0xA0, 0x7F
imgdat 0x38, 0xC0, 0x7F
imgdat 0x5A, 0x80, 0x7F
imgdat 0x7D, 0xD0, 0xBF
imgdat 0x27, 0x09, 0x5F
imgdat 0x25, 0x07, 0xAF
imgdat 0x1C, 0x87, 0xD7
imgdat 0x08, 0x07, 0xE9
imgdat 0x00, 0x0F, 0xF0
imgdat 0x00, 0x0F, 0xF0 ;~最上段の下
;3ワードの隙間にちょうど入るコードだったのでここに入れたが、上の4サイクルウェイトとあわせて使うコード。
;4サイクルウェイトに加えてgotoで2サイクルづつウェイトを増やしている。
;あれでもよく見たらここ使われてない…。コードの最適化のせいで中途半端なウェイトを使う場所が無くなったようだ。
wait10 ;call wait10 1,2
goto $+1 ;3,4
wait8
goto $+1 ;5,6
wait6
goto wait4 ;7,8
end
Post time : 2012年11月26日 01:38│Comments(0)