開発日記

数日前にシミュレータ上でのテストは通った. 性能とメモリ使用量は課題はあるものの MCU 用にコンパイルして調整することになった.

久しぶりとなる MCU 用のコンパイルでは意外なことに 0x1000 bytes だけの RAM は余裕があり、0x6000 bytes の ROM の使用率が 90% になってしまっていた.
ROM の使用量はひとまずとして MCU 特有の DMA がちゃんと動くかを1日調整. 同じ descriptor と memory cycle を使う場合はやらなくてもいい初期化が結構あったので簡略化. ここで結構バグが見つかり、初期化を省いた分、起動が早くなった.

大体は動いているようなので ROM の使用量の削減に入る. text mode と呼んでいる serial terminal からコマンドを打つモードであまり使っていない機能を ifdef で分離したり削除した. その後、elf file を逆アセンブルして分析. C 標準ライブラリの主に string.h の関数がかなり食っているのでそこを調整.

string.h の関数とは文字列処理は別にして、memcpy(), memset(), memcmp() の3つがものすごく重要. これらは #include <string.h> がなくても暗黙的に呼ばれることもある. また引数の src や length が定数の場合は関数を呼ばずにインライン展開されることもある.

実際に逆アセンブルしてみるとインライン展開はほとんどなく関数の呼び出しが多い. 今回は下記のソースで memset() を暗黙的に呼び出していた.

(0 で埋めた配列の宣言)
	int content_length[CT_NUM] = {0, 0, 0, 0};
	int content_num[CT_NUM] = {0, 0, 0, 0};
(そこそこのサイズがある struct の宣言)
	if(1){
		const struct tcx_parameters t = {
			.evctrl = 
				TC_EVCTRL_TCEI | TC_EVCTRL_EVACT_RETRIGGER |
				TC_EVCTRL_MCEO1,
			
			.per = 40,
			.cc = {10, wait_for_next, 0, 0},
			.ctrlc = TC_CTRLC_INVEN0,
			.ctrlb = TC_CTRLBSET_CMD_RETRIGGER | TC_CTRLBSET_ONESHOT,
			.ctrla = TC_CTRLA_RUNSTDBY |
				TC_CTRLA_PRESCALER_DIV1 |
				TC_CTRLA_WAVEGEN_NPWM
		};
		tc_8bitmode_init(3, &t);
		tc_8bitmode_init(4, &t);
	}

配列の宣言は初期化をせず、宣言後に memset() を明示して呼び出せば暗黙は消せる. struct のほうは... 0 で埋めている変数が少ないのになぜ memset() を呼び出すのかARMのアセンブラは読めないことも手伝い理解できない. static const にすれば .rodata に入るのだがどっちが使用率が低いのか判断にも困る.

MCU用のソースコードブートローダとアプリケーションで2層になっている. ブートローダ側に mem なんとかはリンクされているので、アプリケーションからブートローダ側のリンクされた関数を利用することで ROM 使用量を減らす.
当初の方針として3つの関数は prefix に my_ をつけて my_memset() として別の関数にするつもりだったが、この2つの暗黙的な呼び出しにうまく対応できない.

そこで my_memset() の名前を memset() にして libc.a の memset() をリンクしないようにしたら、これは想定通りに動いた. weak 属性が付いてるのかもしれないが調べていない.

その他、標準ライブラリをリンクしている部分を見つけ出して削っていったわけだが、標準の名の通り引数の制限がかなり緩くその条件でも規格通りに動くようになっているので命令のコードがかなり長いのが気になった. strtoul() *1の中で <ctype.h> の isspace() を呼んでいるが ' ' を認識すればいいだけで容量を食うので削った. あとは newlib にほとんどインライン展開が効いてないので想定とは違っていた.

それらの削減作業により標準ライブラリは string.h の主要3関数は別のビルドに動的にリンクして、strlen() のみ残した. strlen() も長いコードになるのが気になるが、使用率が 75% にまで減ったのでそこまでにした.

他に言えることは機能を削ったりするのはわりと頭を使わなくて済むので途中でいやにならなくて助かる.

*1:newlib からソースを取ってきて別名関数として手を入れたもの

開発日記

programming の設計はだいたい完成したと2,3日前に思った. その後少し足りない機能を追加して性能を改善した. 昨夜から今夜にかけてはテストをしやすくするためにソースを整理したときに問題が発生した.

カートリッジがうまくささってないときのエラー判断がない...

いままでの正常系でもバグが大量にあったりコマンド設計が不適切な部分があまりにも多くて気づいていなかった... それもソースの整理がちゃんとできてなくてカートリッジ側がちゃんと動作していない状態が偶然初めて作られた.
2,3日前には明日あさってでシミュレータは終わり、MCU での動作確認に入れると思ったが完成は程遠い.

開発日記

前回の DMA descriptor の構成を基礎に programming の task 切り替えを実装途中. ソースコードは大して長くないのに考えることが多すぎて非常に疲れる. 2022年8月のコードはごっそり削除. ソフトのコードでここまで密度が高いのはあまりなくて verilog かいなと思った.

programming task は bank register 書き込み後に flash programming の task を開始するという流れにして指定バイト数書き込み後に task は停まる. 指定バイト数はバンクの領域と同じにするはずだし、自動でバンク切り替えするには MCU の中の RAM が大量に要りそうとか難しそうでいまのところ実装していない. bank register 書き込みのコマンドに Erase をいれることも可能.

そんな流れで片側の領域のみだがシミュレータでちゃんと動いていることを確認している. 次は並列programmingを実装する.

このtaskの設計のために仕様書を書いてプログラムを書いて仕様書を訂正という作業がかなり長い中、fifo の制御で data 0xff が 64 byte 単位で連続する場合は飛ばせるようにするというのが、2022年夏に書いてあってそれだけは実装していなかった.

これをちゃんとやるには fifo に無圧縮のデータをいれるより Runlength で符号化したデータをいれたほうがいいということに気づいて簡単に実験するコードを作った(下記の rle_decode). Runlength の仕様は byte 0 に同じデータを繰り返す回数かバラバラのデータが繰り返す回数を 1 から 128 で記載するという基本的な方式. あまり凝ったことをしても fifo として data 0xff を飛ばすという目的に離れてしまう. その目的に離れないレベルで繰り返しdataを2byte単位にしてみたり小手先で変えてみたものの、最初の単純な仕様がデータ削減に一番いいらしいことがわかった.

とはいえ Runlength 符号の対応はなくてもいいので MCU で安定して動いてから実装することになる予定.

def rle_decode(src, limit)
	dest = []
	mask = limit - 1
	while src.size != 0
		d = src.shift
		l = d & mask
		if l == 0
			l = limit
		end
		if (d & limit) != 0
			dest.concat Array.new(l, src.shift)
			next
		end
		dest.concat src.slice!(0, l)
	end
	dest
end

開発日記

今回の並列 programming は下記の順番(上から下)で bus cycle を DMA で一度に連続で動かす.

通常           |page programming
r a     d   l c|l    c
c $5555 $aa 1 4|   1 $103
c $2aaa $55 1 /|   1    /
c $5555 $90 1 /|   1    /
c  d+0   ww 1 /|$100    /
p $5555 $aa 1 4|   1 $103
p $2aaa $55 1 /|   1    /
p $5555 $90 1 /|   1    /
p  d+0   ww 1 /|$100    /
c  d+0   rr 1 2|   1    2
c  d+0   rr 1 /|   1    /
p  d+0   rr 1 2|   1    2
p  d+0   rr 1 /|   1    /
  • r: memory region
  • a: flash memory address, d+n は programming destination address + offset
  • d: memory data, ww は programming destination data, rr は read data
  • l: bus cycle 回数, 1を超える場合は a における d+0 の 0 が l 回 increment される.
  • c: bus control 回数. 詳しくは後述.

通常であれば address と data はすべて個別に記載する. control は変わる部分では何回繰り返すと定義するので数字が書いてある. 変わらない部分は / にしてある. c は 4///,4/// とあり、 DMA の参照元を 1 byte 書き換えることによって write cycle を送るか何もしないかを早く変更できる.

page programming device の場合は a=d+0 の行の l が $100 (=256) となっているので、 a=d+0 から a=d+0xff まで順番に変更する. これは ROM dump でよく使うのでその仕組みを再利用する. 今回の問題は c の $103/// という中途半端な値である.

長さの定義は ROM dump ではきりのいい値を使うことが大半なので 16 未満は 2 の n 乗の値ということにして、転送プロトコルとして本来 2 byte いる領域を 1 byte に縮めている. この規定では 0x103 は1つで表現できないので、指数と整数の選択をやめて、指数と整数の加算に変更した. 8 bit のうち 4 bit は2のn乗の指数で 1 から 0x8000 まで. 残り 4 bit は定数で 0 から 15 まで. これにより 1 byte で 0x103 を表現することができる.

表現はできるのだがここまでケチる意義はあるのかは programming が成功してから考え直す. それとこの方式は 0 が表現できない (2 の 0 乗は 1 であるため)ことと 1 から 32 までは表現方法が何通りもあるということが中途半端である.

開発日記

2022-09-19 の続きです. 4個目の 74595 を追加する回路は10月の時点で完成*1、その後3か月間諸事情により停止、思い出すのに2週間というところで新しいハードウェアの対応が必要最小限でできたのが先週半ばです.

今回の対応で 4 spi cycles = 1 memory cycle ができるようになりました. これで memory cycle の種別を切り替えるときに CPU を呼び出す必要がなくなり、 flash programming の効率化が期待できます. 従来どおり memory dump は 2 spi cycles = 1 memory cycle でできるので、種別を切り替える必要がない場合はこのほうが早くできます.

前回のハード設計でできなかった VRAM A10 と VRAM CS# の取得もできるようになりました. これは MCU の接続端子の削減のため 74240 → 外部CPU+PPUデータバス → シフトレジスタを通っています. これで nametable 制御レジスタのあるハードは自動検出する仕組みが作れそうです.

別の問題は 4 spi cycles を実現するのに DMA channel が最低4個、 read data の取得が必要だと channel 数が 5 個になります. 複雑な memory cycle の組み合わせを素直に組むと DMA descriptor が 5 x (種別数+1) 必要になります. DMA descriptor は 1 個 16 bytes で RAM 容量がしょぼい MCU にとっては結構大きな領域になってしまいます.

効率よく DMA descritpor を利用するとなると channel 別に線形にデータを並べることがいいです. 下記の順の cycle が必要だとします.

種別      addr   data-write
CPU-WRITE 0xaaaa 0x55
CPU-WRITE 0xf555 0xaa
CPU-WRITE 0xaaaa 0xa0
CPU-WRITE 0x8000 0x01
PPU-WRITE 0x02aa 0x55
PPU-WRITE 0x0555 0xaa
PPU-WRITE 0x02aa 0xa0
PPU-WRITE 0x0000 0x23
CPU-READ  0x8000
CPU-READ  0x8000
PPU-READ  0x0000
PPU-READ  0x0000

この場合、memory cycle 種別で 1 group のデータとすると種別の切り替えで descriptor が必要になり 5x4 descriptors (=0x140 bytes)も消費します. 一方、address だけ, data-write だけで線形に並べるとこの場合だと 5 descriptors ですみます. read cycle に data-write の data は dummy を4 byte として並べておいたほうが結果としてよいという話にもなります. 実際の運用では速度優先、容量優先などを組み合わせる必要がありますか基本的な考えはこの通りです.

そんな感じでデータの並べ替えが必要になりました. 最初は CPU 側でやろうとしたのですが C で書くのと RAM 管理が面倒になったので PC 側(ruby)でやることにしました. 面倒な処理は PC でやってもらうほうが楽です. また転送のための仕様変更もあり、仕様を決めるのに2日間、仕様に準拠したコードを書いて実際にシミュレータで動かしてみるのに2日間、大したコードの量は書いてないのにとても疲れてしまいました.

9月時点であった CPU での種別切り替えのコードはすべて削除するのですがどこを残していいのか、とりあえずで動かしたいけどリンクを通すにはどうすればいいのかそういうことを考えてるだけでも大変です.

*1:三菱?と書いた 74240 は三菱ではなく松下のようです.

解析記事を追加

12月下旬から開発作業を再開しているのですがスランプのため以前調査した麻雀(HVC-MJ)の裏技を記事にしました. 意外だったのは Disk の麻雀(FMC-MJA)がどのエミュレータでもちゃんと動くことでした.
seesaawiki.jp

開発日記 (UNROM 互換設計)

リセットボタンを押さずリセット相当の命令(jmp ($FFFC))を実行させる回路を設計途中. これはおそらくレジスタが5個いるので先述の SLG46826 で組んでみることにした. リセットについては後日記載する予定. (もう1個書きたいネタがあるが私の余裕が足りてない)

UNROM にそこそこ互換の拡張カートリッジ

リセット相当回路のおまけに UNROM もどきを作ることにした. UNROM はソフト数が多いし、何よりキャラクタ側はRAM確定とか簡単な汎用ロジックICで回路が組みやすい.

UNROM には過去に私が提示した方法では SST39SF040 を利用できないという問題がある. 当時に私にとってこの問題は小さかったものの、今安定供給しているのが SST39SF040 だけだからいつの間にか問題は大きくなっていた.

今回の配線では下記でやってみようと思う. (今の段階で実験は一切してないので注意)

A[17:15] = 74161.Q[2:0] | {3{CPU_A[14]}};
A[14:0] = CPU_A[14:0];
CE# = CPU_ROMSEL#;
WE# = CPU_RW;
OE# = CPU_RW NAND BUSMASTER;

ここでの肝は本物の UNROM では ROM に接続しない CPU_A14 を ROM に接続し、バンクレジスタの側の入力は 1 bit ずらしている点. これにより command address A14 が確保できない問題は一応解決するはず.

ここで互換性を損なう問題と拡張性が同時に存在する. バンクレジスタの値は 0 から 7 が書けるが、 page 7 については CPU address $8000-$BFFF (可変バンク) と CPU address $C000-$FFFF (固定バンク)の内容が一致しない. 本当の UNROM では 8 通りあるが、この構成では 9 通りある.

素直にプログラムを組む場合は固定バンクの内容を可変バンクに置いて利用するということはほとんどないはずで、おそらく、たぶん、きっと昔のソフトも無難に動くと予想される. 新しくソフトを作る場合は ROM の容量が 1/8 増えるので去年自分が作って容量不足で完成を断念した FF3 の hack では簡単に ROM 容量をひねり出すことが可能になると思う.

Flash 側の A18 については対応ソフトが少ないことと74161からのレジスタを別の目的に利用するので省略.

OE# についてはリセット相当回路とデータバス出力が重ならないようにいれているが、それがないなら CPU_RW の反転でいいと思う. *1

半固定バンクを作る

MMC1 やそれ以降にある CPU address $8000-$BFFF と同 $C000-$FFFF をどちらか固定と可変のバンクに入れ替えるという回路も考えた. 74157 か 74257 を利用する.

74157.A = 74161.Q[3:0];
74157.B = {4{CPU_A[14]}};
74157.S = CPU_A14 ^ 74161.Q[x];

74157.Y を FLASH の上位アドレスに接続する. 先述の A17:15 を繋ぐ方法を利用すると、16 ページ利用できる. OR gate 4 つ (7432) はいらなくなるが XOR gate 1 つ (7486) が必要になる.

この回路自体を考えるのは面白かったが gate を使い切れなく中途半端にあまるとか 74161 のレジスタ 4 bit では足りなくなるとか74161 のレジスタの power on reset 回路が必要など部品が増えるなり、互換性が低くなるなど実用性が低そうだったのでボツにした.

*1:以前 Read Cycle の hold time が足りないような指摘を受けたので改めて考え直したが、その問題はないと思った

開発日記

PLD を試した

最初は SLG46620 で内部回路を組んでみた. この場合は特に問題なく組めたのだが、 Programming の手段が専用の Programmer が必要で、基板につけたMCUから programming できないことが判明. その programmer も入手できないので詰んだ.

つぎによく使われるというか I2C で回路データを転送できる SLG46826 で内部回路を組んだ. 前デバイスと比べるとレジスタがそんなに多くなかったり LUT と兼用するために 6x2 のレジスタの確保は無理で、 5x2 のレジスタでギリギリ組めた.

判明した問題

SLG46620 は単価が安いが programmer が入手不可, 部品実装後に回路を設定できる手段もなく、 programmer が手に入っても量産時に programming の人件費がかかって安くない可能性がある.

SLG46826 は上記の問題はなく 74595, 74139, 7400 は1つにまとめられる. IO の脚の数が少なく、もしIOがもう1本あれば反転ゲートがいれられる. 結果として 74240 を減らすことができない. 74165と周辺側も実装はできそうだが脚が足りないので 74165 以外にうまくまとまらない.

考察

期待した割に機能面でもうちょっとというところで本当に採用するか悩んでいる. 本来の機能としてはロジックだけではなくアナログ部分の統合が強力なので私の用途ではちょっと違うので性能が足りていない.

開発日記

回路の改訂案

前回の回路までは address bus と data bus をシフトレジスタの 74595 で制御していたのですが、PHI2, ROMSEL#, RD#, WR# といった memory strobe の生成は MCU の GPIO (ソフトウェアで操作)とタイマ出力を 74139 と 7404 で生成していました.

今回の回路は memory strobe の生成元に 74595 を追加してその他汎用ロジックで生成することになりました. これにより AND や OR が欲しくなったので、 7404 を 7400 に変更、NAND から AND や OR を作るために4つの反転ゲートが必要になりました. また VRAM CS# と VRAM A10 も DMA で取れるようにするために 2つの tristate buffer が必要になりました. そこでの折衷案は 74240 か 74368 という、定番からやや外れる型番です.

74368 がピンの合理性からは最適解なのですが、これは AHCT シリーズがないので 74240 になりそうです. ほかは 74139 が最適解かわからないのですが、 3 入力デコーダは複数の定番2入力ゲートで手作業で組むのが大変なので妥協しています.

今回ブレッドボードで組んでロジックの確認のために 74HC240 を通販で買いました. 三菱?の印字で30年間ぐらい売れずに残ってたICかもしれません.

ロジックの確認はわりと入念にシミュレーションしたので、回路図は問題ありませんでした. ただし手作業でやる分人為的配線ミスでテストがやや遅くなりました.

改訂案の本題

今回の構成は結果として汎用ロジックICが2個増えてしまい、合計は10個になってしまいました. 今回は動作時間を理想的にしたいので仕方ないのですが、kazzo の頃の汎用ロジック1個とは違い複雑です.

前回の基板の大きさや穴の位置を変えることなく載せることができるはずですが、前から気になっていた GreenPak なる PLD を導入して1つのPLDで汎用ロジック(74139,7404,74595,7474)がまとまってくれると量産のときにやすくなりそうな気がします.

ちょっと気になるのがこの手の PLD ではゲートは自由度が高いもののレジスタを利用するとロジック資源がすぐになくなってしまうのです. 今回は 74595 相当のレジスタがほしい(その出力はすべて中間配線)ので 16 個もレジスタが入るのか、 PLD の開発ソフトを触ってみたいと思います.

開発日記

DMA descriptor

前回の下記の記述について.
ソフトが生成した descriptor は DMAC によって更新されてしまう

これは DMA の最後のサイクルに 2 度か 3 度同じ出力をするために、同じ descriptor を流用したところで想定とは違う動作をしていたために DMAC によって更新すると解釈した. 実際は DMAC が descritpor を更新するのは writeback だけで DMAC->BASEADDR は更新しない. 唯一の問題は descriptor の流用がダメ. 流用を修正したソースコードの定数管理が不適切で、シミュレータの assert を抜ける形で定義領域を超えた操作のため別の変数を操作していた. よって descriptor が勝手に更新されているように見えた.

自身の人為的ミスに気づかず原因調査のために時間を浪費してしまった. 今後同じようなミスをしないようにしたい.

progamming

この問題を修正したところちゃんと動き、2022-08-20 と同じ条件の programming の経過時間が 16.0 秒から 12.3 秒に改善した. polling 間隔は前回の 54 us から 30 us に改善した.

descriptor の再生成を省略すると 24 us 早くなる. descriptor は再利用できることがわかっているので同じ操作をするだけなら DMA channel の初期化なんてものは必要なく再度起動するだけで早くなる. などソフトの細かい最適化の幅がまだある.

programming の並列動作については現状の回路に問題がありデバッグをあきらめてしまった. REGION の切り替えに DMA を利用できない GPIO を利用しているのでこれも 74595 を追加して DMA で操作できるようにすると MCU の負担が減って本当に早くなる. 今回のソフトの改善も回路変更前提で作った.

作業の終わりが見えない.