デジタル大辞泉は 68000 アーキテクチャ推しなのか
最近webで辞書を使う場合が多いので、Android端末にいれてお試しでいろいろ調べてみた。気になった言葉はレジスタの説明の3番目。
http://dic.yahoo.co.jp/detail?p=%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF&stype=0&dtype=0
アキュムレーター、アドレスレジスター、プログラムカウンターなど用途に応じた専用のレジスターや、複数の用途に使われる汎用レジスターがある。
さらにアキュームレーターを引いてみると2番目がアレ。
四則演算の結果やデータを一時的に記憶するもの。データレジスター。
この辞書にはハードウェアレベルのレジスタの説明は書いてないので、ソフトウェアで使うレジスタという解釈で考えてみる。
データレジスタは強引かもしれないが、アドレスレジスタという言葉はいくつかの CPU でアセンブラを書いてみて 68000 系以外にみたことがないような... なんとかレジスタを indirect でアクセスするとか、なんとかポインタを使うとかそういうのが一般的な気がする。
辞書の説明文なのでかなり昔に書かれた可能性を加味しても気になった。昔、新聞の記事で「intel の MPU」という言葉もあったんだが、そういうのがまだ残ってるのかしら。
file system driver その3
SPI はやめて SD card mode で読み込むところまで実装できた。かなり詰まったが、いまのところ全て改善できて databus 4bit にして正常に読み取れるし、SDHC の初期化も通るようになった。FAT32 のドライバはまだ書いてないので初期化だけ。
google 先生が教えてくれる結果は大半が SPI なので、参考にならないことがおおかったり、 SDHC のことがかいてなくてちょっと面倒だった。
CMD3 の返答が変
CMD2 発行後に CMD3 でカードの認証を行うんだが、カードによって無返答の場合があって、2,3度読み直すと認証が通るという点。
それ以外にも CMD0 のあとの CMD55 (ACMD41 の prefix 命令)などもどうも反応が怪しかった。
これは、 4.4 Clock Control に書いてあるように、command -> response などの流れの末尾に 8 bit 空のクロックを送るべきというところを見落としていたため。これを修正したら全ての SDSC カードは使えるようになった。
SHDC の初期化が通らない
この時点で "SD Specifications Part 1 Physical Layer Simplified Specification Version 4.10" を読み始めたら、古い流出資料以外にやるべきことがちゃんと書いてあった。この資料は公開されているものだからここに書いても問題ないはず。
まず CMD0 の後に CMD8 を発行して、使用電圧範囲を設定しないといけない。いけないが、有効な設定項目は1つだけで別の目的が資料に書いてあるので、読んでおくこと。
次に ACMD41 の argument を正しく設定する。設定しないと ACMD41 の返答がいつまでも初期化が終わらなかったり、 CMD55 の反応が返ってこなくなる。これらはたぶん嫌がらせ。やりかたは書いてあるものの自分の英語解読能力が悪いのか、はたまた嫌がらせが入っているのかはわからんが、 bit23:0 を 0 以外にしろと書いてあるが、どうも bit23:16 を 0 以外、 bit15:0 は 16'h0000 らしい。
PIO へのアクセスの仕方が悪かった
SPI では返答のバリエーションも少なかったり、コマンドもわりと単純だったので PIO を呼び出す方法を簡略化してたんだが、このノリで native mode を実装していたのが間違いで、さらに資料をちゃんと読んでないことも停滞の原因となった。
PIO で 1bit ずつ clock を上げ下げするのは基礎的な確認が目的だったので、 PIO を呼び出す関数もハードから理論値で clock を制御するためのレジスタに取り替える体で作り直した。
data read が SPI と native で勝手がかなり違う
SPI の場合は response と data は DO から出てくるのだが、native の場合は cmd から response, dat から data が出てくる。
仕様書には rensonse と同時に data が出てくると書いてあって、ソフトでやる場合はかなり迷惑。ただ、実験に使ったカードではそういうことはなかった。(response と同時に data を取れるように穴を開けまくったのに、動作確認が出来ないというのは信頼性に欠けて逆に困る)
利点もちゃんとあって、 read 途中に STOP_TRANSMISSION を発行できて、途中で card からの data の出力も打ち切れる。 SPI の場合は data の出力をやめる場合は block の区切りまで空読みする必要があったので無駄な時間も発生するし、ちょっとした対応を用意しないといけないのと比較すると楽だ。SPI ではこれらを考慮して blocksize は適切な調整を行う必要があるのだが、native では blocksize は最大値を設定して好きなときにとめたほうがよさそうと感じた。
file system driver その2
fragment 関連のテストがかなり雑な状態で、メモリーカード(SDC)を SPI でつなぐところまでできた。
MBR が入っている
実機で動かして気づいたんだが、つないだメモリーカードの先頭セクタは fat のパラメータではなくてパーティションテーブルが入っていた。イメージファイルではそれが含まれていない状態だったのでテストが出来ていなかった。
パーティションテーブルのイメージは linux の fdisk コマンド作ることが出来る。0x200 byte を 0 fill したファイルを $ /sbin/fdisk imagefile とすれば動く(root権限不要)。パーティションテーブルがついた状態のイメージを mkfs でフォーマットすることはどうやらできないみたい。パーティションテーブルとフォーマット済みの fat イメージと連結することでそういうイメージを作ることが出来る。 (おそらく /dev/sda がパーティションテーブル付きのデバイスで、これを fdisk で操作するものの、各パーティションは /dev/sda1 などで操作して mkfs をするからと思われる)
実デバイスなら得られる物理ヘッド数、セクタ数などをイメージファイルの場合は自分で計算して矛盾無く設定することが求められる。面倒くさいので、メモリーカードの先頭8M を dd でイメージつくって、ルートディレクトリのエントリが読めることを確認して終わった。
その後対応済みのソースでメモリーカードを直接見に行ったらファイルもとれたのでよしとする。
native mode
SPI はとりあえず確認のためだけで、本質は native mode だったりする。MMC だと stream 転送というのができるらしいが、 SDC ではない。残念。たまに検索されている CRC7 のソースを書いたのでみんな使うといいさ。
=begin CRC7 shift register input --v-------v | +---+ | +---+ @>|0:2|>@>|3:6|----> output | +---+ | +---+ | @ is xor^-------^-------/ output = old[6]; new[0] = input ^ output; new[1:2] = old[0:1]; new[3] = old[2] ^ input ^ output; new[4:6] = old[3:5]; =end def shift(crc, d) carry = crc >> 6 carry &= 1 crc <<= 1 crc &= 0x7f crc |= d ^ carry #bit0 crc ^= (d ^ carry) << 3 #bit3 return crc end def calc40_shift(v) crc = 0 40.times{ d = v >> 39 crc = shift(crc, d) v <<= 1 v &= 0xff_ffff_ffff } crc <<= 1 #MMC command packet crc7 field to bit7:1 crc |= 1 #bit 0 is stop bit (1) printf("%02x ", crc) return crc end
多項式の説明は実は理解できてないし、C で書いたようなソースはいろいろ処理をはしょっていて意味がわからない。結局はシフトレジスタの図をみてハードウェア都合で実装するんだが、シフトレジスタの図がすごくみづらくて時間をかけてしまった。
あとは、シフトレジスタというのは msb, lsb の順番はわりとどうでもよいので実装するときにそれが逆になってたりする。ソフトでの値の表記から MSB を順番に入れていったが 無駄な bitshift 多いので、 bit reverse して LSB からいれたいなーと思った。やらんけど。
file system driver の作成
メモリカードからの読み込みを目的として、 FAT からファイルを読み込みできるソースコードを書き始めている。書き込みはたぶんやらないと思う。
設計方針は下記である。
- OS なしで動くようにする
- C99 で実装するが、必要があれば C89 とかでもビルドできるといいな
- disk, memory card などのデバイス依存部分は分離する
- RAM は 0x200 byte のバッファを持ち、0x80 byte 程度の作業用 RAM を持つ
- FAT の cluaster chain、つまり fragment の処理を効率化する
- 乗除算をなくす
最近よく使わせていただいたソースで気になる部分がいくつかあったので、それへのオマージュというか、学習ということもある。
stdint.h の使用
名前の設定方法はいくつかあるが、 C99 標準規格ということでこれに準ずる。stdint.h がない古い処理系はそれに準じた名前を勝手に typedef すればいい。
byte order の対応
FAT の場合は 8bit の data の配列を持ち、場合によっては 16bit, 32bit の値を持つ。汎用性を持たせるために、16bit, 32bit のデータは inline 関数で持つようにする。(Ruby からとって unpack としたが、言葉が的確かちょっと自信がない)
static inline uint32_t unpack_le32(const uint8_t *buf, int offset) { uint32_t v; buf += offset; v = buf[0]; v |= buf[1] << 8; v |= buf[2] << 16; v |= buf[3] << 24; return v; }
inline 関数は static の属性を与え、汎用性が高い場合はヘッダファイルに分離して複数の c ファイルから共有する。
inline をサポートしない古い処理系ではマクロで対応する。私は1行に複雑な処理を書きたくないので v という中間変数を設けて簡潔な記述を心がけているが、マクロではそういうことがやりづらい。
unpack 系がボトルネックとなるのであれば、使用する CPU 依存の専用の処理を書いてもいいだろう。
重要なのはソース中で共通となるインターフェースを持つことだ。
disk から filesystem parameter を取得する
ここからは ChaN 氏の文書に準拠して記載していく。
http://elm-chan.org/docs/fat.html
disk の先頭から 0x200 byte を取得し、BS とか BPB の prefix がつくパラメータを取得していく。文書によっては struct の形で cast してしまうやり方を書いているが、これはバイトオーダーや memory alignment が違うと使えないし、公式のパラメータの名称がわかりにくい事もあるので行わない。
これらの設定値は大きく下記の3つにわけられるが、0x200 byte のデータではぐちゃぐちゃになっているので注意したい。
- disk 個別
- FAT (のchain table)
- ルートディレクトリ
BPB_BytsPerSec → disk 個別
disk では sector size をデータの固まりの最小単位として管理する。これがフォーマットやメディアの種類によって可変なために設定したと思われる。
BPB_SecPerClus → FAT
filesystem では cluster をデータの管理単位と定義する。大きいファイルは複数の cluster を持ち、どのクラスタを使うかは FAT とよばれる chain table に記載される。
このパラメータは前述の sector を複数つなげて利用することが出来る数をここに記載する。
BPB_BytsPerSec, BPB_SecPerClus にシフト単位も持たせる
- file の offset から disk の物理アドレスの算出 (fseek とか)
- filesystem の user data 容量計算
こういった処理には対応する chain table を見つけ出す必要があり、sector size と cluster size の乗除算が発生する。チープな CPU を使用する場合、乗除算の使用は避けたい。要するに某CPU向けにコンパイルするときに umodsi3 とか mulsi3 が見つからないというリンクエラーは見たくないし、リンクしても時間がかかる気がする。
この2つのパラメータを定義を見てみると BPB_BytsPerSec は 0x200, 0x400, 0x800, 0x1000 の4通り、BPB_SecPerClue は 2 ** n (n は 1 から 8) となっていてそれ以外はエラーとして弾いてよいと解釈できる。よってシフト演算に置き換えやすい仕様となっている。
前置きが長くなったが datasize とは別にシフト回数も取り込むようにする。
static bool filesystem_init(struct fat_filesystem *const t, const uint8_t *const buf) { int errorcount = 0; t->fattype = FATTYPE_ERROR; if(buf[0x1fe] != 0x55 || buf[0x1ff] != 0xaa){ DEBUGPUTS("signature is not found"); errorcount += 1; } t->disk.sector_size = unpack_le16(buf, 11); //BPB_BytesPerSec switch(t->disk.sector_size){ case 0x200: t->disk.sector_sizebit = 9; break; case 0x400: t->disk.sector_sizebit = 10; break; case 0x800: t->disk.sector_sizebit = 11; break; case 0x1000: t->disk.sector_sizebit = 12; break; default: t->disk.sector_sizebit = -1; DEBUGPUTS("disk.sector_size is illigal"); errorcount += 1; break; } t->fat.cluster_par_sector = unpack_byte(buf, 13); //BPB_SecPerClus #define CPS(bit) case (1 << (bit)): t->fat.cluster_par_sectorbit = bit; break switch(t->fat.cluster_par_sector){ CPS(0); CPS(1); CPS(2); CPS(3); CPS(4); CPS(5); CPS(6); CPS(7); default: t->fat.cluster_par_sectorbit = -1; DEBUGPUTS("fat.cluster_par_sector is illigal"); errorcount += 1; break; } #undef CPS ---- 中略 ---- return errorcount == 0 ? true : false;
disk.sector_size のほうは4度なのでコピペして switch を作ったが、 fat.cluster_par_sector のほうは8度もコピペするのは質が落ちるのでマクロを使った。こういうマクロの使い方は賛否両論あると思うので、マクロの有効範囲をできるだけ狭めることが大切だと思う。
FAT12 の chain を取得する関数
話がかなり飛ぶが、 FAT12 の cluster 番号の取得は 12bit 単位となり、 2.5 byte 単位で参照することとなる。ChanN 氏の説明では byte 単位のソースコードで説明しているが、変数と変数の除算が発生するし、わかりにくいと私は考えている。
ここで、bit 単位で考えることにする。
/* bitcount is fixed 小数点 bit 31:19 must be 0 18:3 byteoffset 2:0 bitoffset (1:0 is 0) */ assert(cluster_num_in != 0); bitcount = cluster_num_in * 12; sn = bitcount >> 3; sn >>= t->filesystem->disk.sector_sizebit; sn += t->filesystem->fat.startsector; so = bitcount >> 3; so &= t->filesystem->disk.sector_size - 1; if(buffer_sector_cacheup(t, sn, so) == false){ return false; }
まずクラスタ番号から sector number (sn) と sector byte offset (so) を算出する。 bitcount の算出に乗算を使うが、変数*定数であればコンパイラがシフト演算と加算に置き換えてくれることが期待できる。12 の場合は (1 + 2) << 2 となり、シフト演算のほうが低コストであるはずだ。
bitcount を3回右シフトすれば byteoffset になる。 byteoffset から sectorsize を割れば、 sector number がでてくる。普通にやると byteoffset / sectorsize なので変数/変数の除算となるが、シフト回数にすれば大幅なコストダウンと思われる (ただし、CPU によってはシフト回数が変数だと効率が若干落ちる可能性がある, たしか ARM がそうだった)。
so のほうは sector_size が 2 の累乗値であることを利用して除算(剰余)を and 演算に置き換える。
switch(bitcount & 0x7){ case 0: ret = buf[0]; ret |= (buf[1] & 0xf) << 8; break; case 4: ret = buf[0] >> 4; ret |= buf[1] << 4; break; default: DEBUGPUTS("fat12 cluster bit offset is illigal"); return false; }
記載は中略したが、 buf には対応するセクタからセクタ境界の問題も処理した 2byte が入っている。これを当初の bitcount の byte 未満の bit 部分で分岐させれば 12bit の cluster が取得できる。
変な処理をするときはコメントをいれたり、わかりやすい変数名をつけて >> 3 や & 7 のようなマジックナンバーの補足を行うことが大切である。広範囲に影響する定数であればマクロなどで名前をつけることも大切と私は考えている。
MAME デバッガ続き
デバッガ標準装備となったバージョン以降の最新ビルドかつ Windows の SDL port を見つけたのですが、デバッガは動かないようになってました。うーん、ソースでは Windows も考慮した記述になってるので残念。
仕方ないので Win32API のほうとデバッガコアのヘッダファイルを見てみました。拡張子が .c とか、古めの driver 系は変更がないので、 C で書いてあるなと思うわけですが、こういったコアの部分は C++ で手の込んだコードになっています。デバッガコアの方は4つぐらいのクラスを多重継承してある記述や friend という、素人が使うと泥沼にはまる要素があって私が使うには大変なオーラが漂ってきます。
一方 osd/windows/debugwin.c のほうは Windows 依存のウインドウの作成、位置の管理、テキストボックスのフォームの設置などがメインです。こっちも C++ ですが、使い切れてない出来です(もとは C だったのを徐々に移行中かもしれません)。デバッガコアとの通信は、カーソルの移動なり、デバッグ情報の文字列の取得、デバッグコマンドの送信にだけときっちり切り分けられていました。カーソルの選択などもデバッガコアがすべて管理しているので、OS 依存 (osd, OS depend) だけきっかりやってます。
当たり前かもしれませんが、コマンド入力フォーム以外の表示部分は、文字列を画像化して文字の色などを塗っています。ここが合理化というか、手抜きなんでしょうが、文字列配列を1行ずつコアから得て、bitmap に描画してます。逆アセンブルコードは1行ずつで、CPUレジスタダンプも1行ずつ...ということになります。
旧デバッガだと CPU ごとにレジスタのレイアウトが違うみたいで Z80 は横長に2行(上段:表レジスタ,下段:裏レジスタ)、68000 は縦長に2列(左列:Dレジスタ、右列:Aレジスタ)となっていた余波を見た気がします。
自分好みの入力系統にして、無駄な空白をなくして、レジスタも好きに並べられるとなるとちょっとおもしろそう。とここまでで満足。だってデバッガコアが CPU や RAM にどうつながってるか調べたり実装するのは大変そうですから。
気は済んだので、 M72 を再開したいと思います。
スプライトのY座標系統
2次元なコンピュータグラフィックの場合、縦座標は上から下に行くのが普通ですが、ほとんどのスプライト属性は逆方向の下から上へ行きます。なんでそうなるのか自分はわからなかったのですが、落ち着いて考えるとわかりました。
- sprite atributte position Y と display scanline counter を加算する (ここでは両方とも8bitとする)
- 加算値の bit7:0 が 0 であったら(たぶんcarry は無視)で sprite hit となる
- sprite の height が 16bit で zoom がなければ bit3:0 がそのまま offset となる
とても合理的ですね。これに気づかず、vram から scratch ram に取り込むときに xor で反転して、hit = display == position とか offset = display - position を使うから無駄な回路を作ってたり、不必要に難しくなってました。
これと似た方式として display scanline counter 更新時に scratch ram の position Y を全て increment する方式も考えたのですが、描画途中に attribute を書き換えるようなテクニックなり、CPU との同期を取るのに不都合だったのでなしにしました。
linebuffer + zoom down ありの場合は、ちょっと特殊で全ビットを 0 比較するまではいいのですが、次の offset が += 1 ではない場合がありますので下位ビットで offset y の算出ができません。これはやはり、offset y 専用のバッファが必要です。
framebuffer の場合は ... 縦座標を上から下に置いた方がいいんじゃないのかな? zoom up sprite を使う回路を作らないと気が回らないです。
久しぶりに M72
これより優先度の高い開発プロジェクトがある気がしますが、他がつまったり、機材が出せないなどの理由で久しぶりにこれをやってみました。
外部 CPU
PLD 内部は 64MHz で動き、外部の CPU は PLL も外付けして 8MHz だったのですが、非同期になり setup / hold time が守られないので動かない、と開発中断中に気づきました。
このために外部の PLL は停止し、 PLD の 64MHz を 8 分周したクロックを出力・供給することにしました。分周は回路は 3bit のカウンタ用意し、単純にインクリメントします。 bit2 は 8MHz となり、 bit2:0 の分周値で外の CPU のタイミングをとれるようにします。
この方法は以前は、10MHz より低い clock の供給として内部のCPUでやっていたのですが、動作が非常に安定しないので clock は PLL からでる clock を入力し、cke (clock enable) へ分周タイミングをいれることにしました。
外部の場合はそういうことが出来ないわけでこれでいいのかなと思いつつやっているのですが、いまのところは改善の兆し無し... 結構ソースを書いたのにそれに見合う結果を得られませんでした。なえる。
tilemap の書き込みミス
tilemap は VRAM の中でも中規模の容量を求められるため、PLD 外部の RAM としました。外部の RAM は SDRAM となるわけで、わりと複雑なコントローラがいります。
原因を探ってみたところ、CPU - video でのバスの切り換えが不適切でメモリアクセスが正しくできていませんでした。 CPU からの write 途中で、 video 側からのアクセスがあると、コントローラが write sequece を実行するものの、バスが video に切り替わっている上に read ではなく write が行われるというものでした。
SRAM とかならすぐに切り換えてもなんとかなってるんで、その感じで作ったらだめだったという。というか、いままで作っていたやつでもこういう事が発生してもいいのになぜ起きなかったのか。外部CPUだからいけないのか、アーキテクチャの問題なのか、ということを考えると自分がてきとーに作ってたことに気づきました。
まだまだ修行が足りないです。別の問題をいうとデバッグ用のシミュレーション条件を作る根気も無くなったみたいで、参ります。