開発日記

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 の負担が減って本当に早くなる. 今回のソフトの改善も回路変更前提で作った.

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

開発日記

DMA descriptor

用途によって分かれる descriptor 群をあらかじめ3通り用意しておいて、DMAC->BASEADDR だけを書き換えようという案. これを作るのに手間取った. シミュレータ上はそれとなくできたが、MCU で動かすと下記の問題が判明.

  • DMAC->BASEADDR は DMAC->CTRL.DMAENABLE が 0 のときのみ更新可能.
  • ソフトが生成した descriptor は DMAC によって更新されてしまう (なんのために writeback があるの?)
  • そもそもボトルネックが descriptor の再生成かちゃんと調べてない.

descriptor の更新は想定していなくて、DMAC が更新しない前提で処理の軽減化を狙っていたので困った. どういう値に更新しているか調べる. そしてどの処理の処理時間が長いか調べる必要がある(かなり面倒くさい).

開発日記

USB 受信

  • 前回の通りで実装に成功. PCKSIZE を調整することで受信処理の効率化と安定化を確認.
  • 受信側の汎用バッファはなくなったが、送信側の汎用バッファは0x200bytesも確保しているので RAM の 1/8 を浪費していることが判明. そのうち減らす.
  • 0x80000 + 0x40000 bytes の ROM dump は前回は約3秒(実際には2.9 秒 だった)から 2.3 秒と早くなった. ROM dump ではMCUの受信は重要ではなかったので意外.
  • 一方 programming は処理時間が変わらず. programming は fifo で待ち時間を許容できる仕組みなので仕方なし.

DMA descriptor

  • 前回の通りで DMA descriptor の初期化処理を減らすべく、複数の descriptor を用意しておいて随時切り替える方式に変更.
  • 昨日と本日の実装では... かなり難しくて嫌になってくる. polling 間隔の効率化で、次回設計分の基板にむけさらに複雑になるであろう descriptor 管理システムのことを考えると気が重い.
  • 現状複雑になっているのは DMA controller から SPI module へ 16 bit address を increment pattern で出す部分.
    • increment pattern は RAM 上 (DMA の source は RAM が必須で ROM におけない) に 0x100 bytes を確保し、address 下位 8 bits が 0xff -> 0x00 になる時点で descriptor を address上位下位で更新するので 2 個消費する.
    • これを主に使うのは dump で、DMA により 0x478 bytes の RAM buffer に書き込み、address += 0x478; して dma descriptor を再構成して再度 DMA を動かすを繰り返す.
    • adrdress = 0x8000, length = 0x478 で descriptor は 5 x2 個いる.
    • その次は address = 0x8478, length = 0x478 で descriptor は 6 x2 個いる.
    • descriptor の更新は増えることを想定してなかったので1度の DMA での read cycle length を 0x100 byte 単位にして descriptor が増えることを予防するしかない.
  • programming fifo の容量確保よりも、消費量が多くなるであろう DMA descriptor の確保のほうが重要かもしれない.

その他

  • 当分先の拡張性に関して外形デザインについてアイディアが思いつく. 難点は太いケーブルがいる.

開発日記

flash programming に成功. sector erase 0x10000 bytes, back switch 8 times 込みで 0x10000 bytes の転送で 16 秒かかっている. おそいし、USBの受信が安定していない.

USB 受信

  • 64 bytes を超えるPCからの送信はMCU 側の処理が多くなると、USB 受信パケット(64 bytes単位)を喪失してしまう.
  • USB プロトコルの NAK をうまく利用する方法はどうもなさそう.
  • 根本的な解決は受信前に連続受信バイト数を UsbDeviceDescBank.PCKSIZE.MULTI_PACK_SIZE を設定する.
    • DMA で書かれるので MCU からの転送処理(memcpy())を省ける
    • PCKSIZE.SIZE の倍数でないと DMA で RAM を破壊される可能性あり
  • PCKSIZE.SIZE は 8,16,32,64 から選べるので少ない転送量の場合は小さめの値にしておくことで倍数制限とうまくつきあえるはず

DMA descriptor

  • program cycle (write) と polling cycle (read)のたびに DMA descriptor を最初から作り直しているが時間がかかる(25us程度)ので descriptor の再利用機能を実装して DMA の初期化時間を減らす.
  • descriptor とは別に event contoller の初期化は毎回必要でそれも気になる.

ハードウェア変更

1度の DMA で REGION と RW を変更させると flash の programming や polling が効率化できる.

  • 74595 を追加, 4個目, 出力 CPU RW, BOTH RW. REGION x2
  • 新しい 74595 の LATCH 許可用に OR か AND gate が必要, DMA 制御でそれを変えるか従来どおりにするか選択する.
  • DMA 途中に memory strobe の時間を変更できないので、 CPU と PPU で別の脚を設けて有効時間は決めておく
74595.RCLK = DW_SS || 74595.control (AND, OR どちらでもよい)
PPU.A13# = CARTRIDGE_A13 == 1 && REGION_PPU
74165.PL# = ~(CPU_MS || PPU_MS)
CPU_ROMSEL# = REGION_CPU && CPU_MS && CARIDGE_A15 == 1
PPU_RD# = REGION_PPU && PPU_MS && BOTH_RW
PPU_WR# = REGION_PPU && PPU_MS && ~BOTH_RW

開発日記

近況

諸事情で休止期間が入っていますが、MCU からのファミカセ操作の続きです.

今月にはいってからブートローダの作成、タイムアウトの実装、ROM dump が実装できました. ROM dump は DMA を利用するのでかなり早いです. 0x80000+0x40000 bytes の dump が 3 秒で終わります.

現在は flash programming の実装中です. flash programming は kazzo にある問題を解決するために仕様が複雑になり、MCU 用のコードを書いても細かい分析が実質不可能になっています. 対策として制御用コードを機種依存コードを排除して PC でもコンパイルできようにしてテスト環境を作ってシミュレータと称してデバッグを6月中にやっていました.

昨日ようやく flash programming の MCU 側での動作確認が始まりました. これに到達する前にシミュレータでは動くのに MCU ではなぜか止まるという現象が2件あり、原因分析にめぼしのありそうな場所をコメントいれたりして非効率な作業を行うことになりました. 奇数アドレスで uint16_t * の演算を行う address alingment error であったり*1 DMA の writeback レジスタの初期化を間違えていて、シミュレータでは起きるはずのないメモリ破壊が起きていました.

Erase Polling の波形分析

現在は erase の write cycle を出して、 polling の read cycle を出して polling が完了してしまったところで止まっています. シミュレータでは起きないので雑に作っているテスト環境が悪いのと根本的な設計が悪いので直します.


その波形をみていると無駄な時間がかなりあることに気づきました.
これが polling の read cycle で、 ΔX から 52.23 us あります. 詳しく調査していませんが, 次の read cycle 生成ための DMA controller の初期化を最初からやり直してるのでその時間に無駄がありそうです. DMA なんで ROM dump のように転送バイト数が多い場合は 53 us は相対的に短いのですが、2 byte 単位だと長いです.

これは Erase なので 53 us はいいのですが、 Programming Cycle としては間隔が長いです. デバイス別に AM29F040B は typ 7us, max 300us. SST39SF040 は max 20 us (typ 記載なし)です. max 300 us は 53 us より短いので polling が必要ですが、max 20 us なら polling せずに書いていっても大丈夫な気がします.

flash は programming の空き時間があるので疑似並列処理で空き時間を有効にしているけど、実際は programming に空き時間はないので erase の空き時間だけ切り替えたほうが早い. そのような改善案がたくさん思い浮かびました. (SST39SF040 Sector-Erase max 25 ms, Chip-Erase max 100ms. AM29F040B Sector Erase typ 1 sec, max 8 sec, Chip Erase typ 8 src, max 64 sec)


次にさっきの波形を拡大したメモリアクセスの波形です. これは私から新しい発見はないです. -51.14 us から -50.64 us の間の部分は DMA を動かしているものの CPU が動いてるとこういう隙間の時間が発生しているのでソフトウェアである以上仕方ないと解釈しています. 100%無駄なく完璧にやりたいなら CPLD を使うしかないかなと思います.

同じaddress生成しているならSPIだけ動かさずに memory strobe だけ動かせば時間短縮もできる気がしますが、read 周りのシフトレジスタと共有しているのでそれはできないです. 他の改善案は CPU と PPU の region 切り替えは GPIO でやっているのでそれを DMA で制御するようにシフトレジスタを追加すると polling の時間短縮ができそうな気がします.

*1:Cortex-m0 では起きるが 同 m3 では起きないらしい

開発日記

近況

先週ぐらいから MCU のソースに手を入れて専用ソフトで通信するプロトコルの策定とそれの C ソースの実装をしています. memory read, memory write に関してはそれでできています.

flash memory programming になると、programming の空き時間があるので2つのバスで並列に動かす仕様にしてます. 通信プロトコルなりコマンド詳細は頭の中ではできてるのですが、複雑になっているので準拠した C のソースを MCU で動かしてもデバッグが困難になっていきます.
リモートデバッガでもステップ実行などはできるのですがそんなに早くないとかブレークポイントの制限がつきまとっていたり、CPU は待ってくれても周辺デバイスが待ってくれないのでうまいこと調べられないことがあります.

今回作った C のソースは MCUレジスタはいじらないものの大量のクラスもどき変数があるという状態なので、シミュレータを作ることにしました. このソースは PC 用にコンパイルできますので、入出力は別途作ってリンクします. リンクの部分など本当に C で書く必要がある所以外は mruby で作って試行錯誤している状態です.

もう何度かやってるのですがグルー言語を組み込んだデバッグはリンクが通るまでは煩雑ですが、動き出すと C 特有のルール(static とか配列サイズを超えないようにするとか struct の宣言とか typedef struct{}name だとポインタだけ宣言がやりづらいとかなど多数)から開放されてやりやすくなります. 作り込む C のソースはローカルの gdb で好きなだけデバッグ情報も取れます.

本題

今日は MCU から PC に USB でデータを送る部分のソースを見直しました. MCU の仕様上そのデータのポインタのアドレスは alignment 4 bytes を守る必要になり下記のコードを書きました.

	uintptr_t addr = (uintptr_t) t->data_buffer.top;
	uintptr_t mask = 0b11;
	if(addr & mask){
		addr &= ~mask;
		addr += 4;
		t->data_buffer.top = (uint8_t *) addr;
	}
	t->data_buffer.length = (t->input.top + BUFFER_SIZE) - t->data_buffer.top;

uintptr_t は標準ライブラリの inttypes.h (か stdint.h) で定義されている CPU の address を整数にする場合の型です. 当初は uint32_t で書いていたのですが、 MCU 用のコンパイラでは通り、 PC 用のコンパイラでは警告が出ました(-Werror でエラー扱い). 理由を考えると MCU のアドレスバスは 32 bit, PC のアドレスバスは 64 bit なので正しく計算できなくて当然です.
そういうときに inttypes.h 検索したらそれ用の型があって助かりました. 他目的だと uintptr_t はどういう目的で必要があるのでしょうか??

あとは.. 2種類ぐらいのコンパイラで同じソースをコンパイルすると Warning 出ずにたまたま動いていた所がでてきて、結果として信頼性があがる気がします. (気がするだけ)

C のソースの変なかきかた

C++ のほうがテンプレート活用で変なかきかたできるんでしょうけど、私には C++ は難しくて使えません.

タグを活用する

タグは struct/enum/union につけられる名前で、変数や関数名と名前空間が別です. とはいえ struct だけが利用される傾向にあり、いつのまにか typedef を利用して struct を書かなくてもいいようになっているのがよくある例です. ここでは typedef は使いません.

タグ名と変数名を同じにする

struct header{
	int xx;
	....
}header;

先述の通り名前空間が別なのでエラーになりません. typedef ではエラーになります.

struct の宣言の前に変数の属性をつける

static const struct header{
	int xx;
	....
}header[] = {
	{...},
	{...}
};

struct の中身宣言のあとに変数名だけをつけるので,最初に属性をつけるらしいです.

struct の中身宣言と一緒に関数を実装する

struct header{
	int xx;
	....
}*func(void){
	static struct header hoge;
	return &hoge;
}

変数が宣言できるなら関数も実装できます. 意外なことに struct の隠蔽化でわりと実用的です. 関数の中身ですが、 struct の変数属性に static をつけ忘れると Warning がでますし、実際にそのまま動かすと使い物になりません.

これの派生として enum でも似たようなことができます.

enum hoge{
	HOGE_HOGE, ....
}hoge(void){
	return HOGE_HOGE;
}

enum の場合はヘッダに中身を書く場合もあるので enum の中身の宣言とその戻り値を持つ関数の宣言なんかに使えてしまいます.

関数プロトタイプを複数宣言する

static enum hoge{
	HOGE_HOGE, ....
}hoge_add(int), hoge_sub(int), hoge_shift(int);

個人的には static 関数のプロトタイプは書かないようにソースの上のほうに書いておくのですが、関数ポインタで切り替える場合はプロトタイプ宣言は必要です. そのような場合に戻り値の型が同じ関数は , で区切って複数宣言可能です.

可能ですが引数は都度書くことになるので、関数ポインタの型を typedef して複数宣言するほうが便利です.

その他関係ないこと

最近は私は集中力が続かないので C を書くこともこれぐらいが限界になっております...

約5年半ぶりにファミコンの技術情報サイトを更新しました

2016年の更新は大したものではないので実質7年半ぶりでしょうか. ドラクエ4の記事は書いてみたらおもしろくなかったのでそのうち消すと思います.

今回は RAM の不定値とか hotswap (電源を入れたままカセットを交換する行為) について説明してます. RAM の不定値なんてエミュレーションするものではないでしょうし、 FCEUX の default は BR6216 特性(といってもかなり簡単な法則性なのですが)になっているのでそこそこ信憑性があります.
https://seesaawiki.jp/famicomcartridge/d/RAM%20%a4%ce%c9%d4%c4%ea%c3%cd%a4%ce%cd%f8%cd%d1

レビューしてもらいながら思い出したのは Z ガンダムホットスクランブルの MONITOR でも任意のアドレスと値をかけます. ただし MONITOR を有効にするのが手間なので RTA への分野の実用はなさそうです.

seesaawiki がいつまで使えるかわかりませんし、デザインのカスタマイズの限界を感じているので3年以内を目標に自分で管理しているサーバーの pukiwiki に移動しようかと思います.

VRAM A10 と VRAM CS#

dumper/programmer においてあまり重要ではない扱いになっている VRAM A10 と VRAM CS# 端子について最近の考察をお伝えします.

カートリッジ自動検出

既存の dumper soft ではカートリッジ自動検出は ROM data の hash (checksum) を算出してやっていますが、この方式は既存でないソフトにめっぽう弱く、個人的に嫌な思い出があるので絶対に私は実装しません. ほかの方法は模索していたのですがようやく思いつきました.

VRAM A10 の制御がレジスタ経由の場合(初期ハードのようなはんだづけ方式は不可)、はこのレジスタを操作すると自動検出ができるかもしれません.

例えば MMC3 の場合は CPU address $A000 (有効ビット A14:13, A0) で制御、 MMC1 の場合は CPU adress $8000 (有効ビット A14:13)で制御してるので反応がある方がハード検出できます. 基板によって接続 address bit だけを変更している VRC (2,4,6,7)もたぶん検出できそうです.
ただ、VRAM A10 をレジスタ制御アドレス基準なので誤動作もたくさんあるので大まかにハード製造メーカーを手動で指定して詳細は自動でということになりそうです.

(MMC3 系統の亜種チップと亜種基板を調べていて、その判別を自動でやろうと思っていたときに通常の MMC1 と MMC3 を判別するほうが簡単なことに気づきました)

アフターバーナー以外で nametable に ROM を割り振る方式

VRAM CS# をレジスタ制御できるのは SUNSOFT-4 と Namco の 163 と MMC5 ぐらいだと思われます. その中でもこの機能を使うソフトは少ないです.

最近マインドシーカーは互換機で動かないということを聞きました. これはタイトル画面で nametable を ROM にしているのが原因だと思われて、伝統的な互換機では VRAM CS# をちゃんと配線してない可能性は十分にあります. (Linuxの上でエミュレータを動かしてるだけのレトロフリークは話は別でちゃんと動くと思います)

いまでもマインドシーカーの苦行を楽しみたいのは個人的には謎ですが、タイトル画面以外はnametable が RAM なので最初だけが問題だとは思います.

特別な入力端子とする

現在設計中の dumper ではエッジコネクタから MCU への programming をしたいです. それを考えたところ 5V 入力が必須の入出力端子は実装が難しいのですが、出力端子はバススイッチを経由させればおそらく安全に MCU への programming 端子につなぐことができそうです.

VRAM CS# と XOR ゲート

現状 MCU とエッジコネクタのバスはシフトレジスタ経由で読んでいるのですが、そこで溢れてしまったのが VRAM A10 と VRAM CS# の2つです. VRAM A10 のほうは74165の DS 端子(べつの74165を連結する場合にいれるためにある) にねじ込んでいるのですが、 VRAM CS# はそれにも溢れて抵抗経由で MCU の GPIO につないでいます.

一応、その2入力をXOR gate を介して、74165.DS につなぐ方法も思いついてはいるのですが、入力の仕様が未知の場合の解析に弱いことと、他につかうレベルシフト兼反転ゲートが4つで空きがないという状態のため採用できていません.

あと2つぐらい入力端子があふれるのであれば IC を追加してシフトレジスタの入力を切り替えるとか74165を追加ということもできるのですが切替端子にGPIOを使ってたりICを増やすのも絶妙な状態でパズルがとけません.

2022年4月9日付補足: 74165のつもりで(74)161と書いていた部分を訂正しました.

mruby のビルドの手順

https://na6ko.hatenadiary.jp/entry/2021/06/10/213745 の続きです.

  • 目的: require が必要なのでコンパイルが必要.
  • 環境: msys2-mingw64 でのビルド.

mattn/mruby-require

fts.h が不要. msys2-mingw64 でビルドできる.

git clone https://github.com/mruby/mruby
cd mruby
cp build_config/default.rb build_config/myconfig.rb
(編集後)
rake -v MRUBY_CONFIG=myconfig

myconfig は conf.gembox 'default' を使用する.
mrbgems/default.gembox から不要そうな機能を削除してみたり設定を変えてみたが、コンパイル時にエラーがでたりバイナリができても mirb で未使用変数をタイプするだけで segmentation fault が出たりするので諦めた. これは mattn/mruby-require と無関係である. (比較した)

このためコンパイラも clang ではなく標準設定 の gcc のままにする.

iij/mruby-require

fts.h が必要なこと、 msys2-msys が必要なことがわかっている. そのためのビルド方法は不明.
fts.h をなしで無理やりコンパイルした場合はエラーメッセージがでてしまう.