mruby - mpsse 経由で SPI Flash/ I2C EEPROM programmer を作るその2

mruby でエラーの行番号を出す方法を下記を参考にいれました. API ドキュメントは読んでないので本当にいいのか保証がありません.
https://tyfkda.github.io/blog/2013/09/21/mruby-backtrace.html

ソースコードは整備ができましたので公開いたします. プロジェクト名は ezp201x という怪しい programmer をもらったけどソフトを使いたくなかったのでケースだけを流用しようと思ったからです.
gitlab.com

ruby script を書いて感じたのはやはり Squirrel より便利なのでこちらを乗り換えようと思います. もう 10 年以上放置している anago もこれで作り直して、libusb を触るところだけ C, 基本的な処理はコンパイル済みの mruby, カートリッジハード個別のドライバは rb ファイル直接とすると 楽しくプログラムをかけそうです.

anago の場合はユーザー数がかなり多いので GUI をつけろと要求が来るのは面倒なんですよね... 自分は GUI 制作に向いてない*1し、wxWidgets 使うにしろ C++ を書くのが大変に苦痛(言語として難しすぎる)なので気軽にかければ本当にそれがいいのですけど.

これは個人的な信仰で、 C++ は一応書けるけど難しく手間が多いので書きたくない、 Python はインデントが慣れないので*2やろうと思ったことさえない、結果として Ruby がいいということです.

anago を作り直すことに GUI で協力してくれる人がいればコメントなりで連絡ください.

*1:twitter で upergrafx control panel が使いにくいと言われて気にしてます

*2:C では {} を絶対に省略しない人です

mruby - mpsse 経由で SPI Flash/ I2C EEPROM programmer を作るその1

ICE40 専用ではなく汎用の 8 pin memory programmer を作ることにしました.

24 系 I2C EEPROM と 25 系 SPI flash のピン配置

1つの socket で対応ということで下記にしました.

socket         SPI flash    I2C EEPROM
1 AD4 8 +3.3V |1 CS# 8 Vcc |1 A0  8 Vcc
2 DO  7 AD5   |2 DO  7 HOLD|2 A1  7 WP
3 PU  6 AD0   |3 WP  6 CLK |3 A2  6 SCL
4 GND 5 AD1+PU|4 GND 5 DI  |4 GND 5 SDA

74157
AD2 = mode == 0 ? I2C.SDA : SPI.DO

AD
0 SPI.CLK, I2C.SCL
1 SPI.DI, I2C.SDA(input)
2 SPI.DO, I2C.SDA(output)
3 未使用
4 SPI.CS
5 mode (0:I2C, 1:SDA)
* PU はプルアップ

I2C では SDA を AD1 と AD2 につなぐようになる回路図が多いですが SPI と共用する場合は multiplexer を利用します. I2C では WP = 0 で書き込み可能ですが、SPI の WP は逆ですのでここも注意が必要です.

93 系の EEPROM は配線が全く違うので共用は不可能としてソケットのあまりに別途 8 pin の場所を用意して, CS# を個別出力にして切り替えようと思います. Vcc ラインは P-MOS FET で電源を切り替えるようにするつもりです.

mpsse の driver

前回の ICE40 の programmer のソースに書き足して対応しました. I2C は野良ソースコードと FTDI の技術文書を見て作りました. 野良ソースコードは SDA の出力が wired or ではなく high と low を出すようでしたが、こちらの実装では hiz と low の2通りとしました.

mpsse driver とグルー言語 mruby をつなぐ

  • 毎度ながら Squirrel でもいいのですが新規開拓として mruby にしました.
  • mruby の取得は msys2 なら pacman -S mruby でできます.
  • mruby のリンクは -lmruby -lws2_32 でできました.

一番の問題は C の関数と mruby の API で、公式がまともなドキュメントを出していないのでインターネット上の野良情報なり自分でヘッダやソースを読むことが求められます. 一応いままでこの手のグルー言語なりなんらかのライブラリの API を見ましたがここまで雑なのはとても不満です.

公式のドキュメント: http://mruby.org/docs/api/header_list.html

2013 年 3 月に書かれた下記の野良情報が便利というのは 8 年間放置している証拠だと思います.
https://tyfkda.github.io/blog/2013/03/11/mruby-api.html

話がそれました. 私の実装は ruby から呼び出された method の引数を配列とみなし、 uint8_t * の配列を作って mpsse の driver と渡すものです. mruby の配列長を得る方法は信じられないことにどこにも書いてませんでした.

自分が書いたソースは下記です.

struct array{
	mrb_value mrb_array;
	mrb_int size;
	uint8_t *data;
};

static int array_into_uint8_t(mrb_state *m, struct array *t)
{
	t->size = ARY_LEN(RARRAY(t->mrb_array));
	if(t->size == 0){
		t->data = NULL;
		return 1;
	}
	t->data = malloc(t->size);
	for(mrb_int i = 0; i < t->size; i++){
		mrb_int v = mrb_fixnum(mrb_ary_ref(m, t->mrb_array, i));
		if(v < 0 || v >= 0x100){
			free(t->data);
			t->data = NULL;
			return 0;
		}
		t->data[i] = v;
	}
	return 1;
}

<mruby/array.h> にあったそれっぽい ARY_LEN(RARRAY()) を組み合わせたらたまたま動きました. この2つは C のマクロでキャストしていて型保証もないのでうさんくさいのですが動きました. だから、もしこれをみた mruby の関係者はまともな解説を目に付く場所に書いてほしいです.

あとは慣れなのでしょうが C->mruby, mruby->C での API の名称も解説が不十分かつ関数名が推測しづらいのはいい印象ではありません.

mruby の組み込み後

今の所、 mruby->C の method は引数1個、型は整数か上記のuint8_tにいれるような配列だけです. これだけわかればあとは登録した C の関数を rubyスクリプトで組むことになります.

ここに達すると C で考えるべき型やタグやマクロや固定配列長やstaticとか面倒なことを考えなくてよくなり急にスピードアップを感じられます. (たぶん string.h にある memcpy, memcmp, memset がいらなくなるのが大きい) そして先程まで不満だった mruby の API ドキュメントの雑さも忘れられてしまいます. *1

こうなると Squirrel の言語との比較が多くなりますが、書き慣れている ruby script のほうが簡単にやれることが多いのでさらに楽になれます. 例としては下記です.

  • g++, clang++ でリンクしなくて良い
  • &=, <<=, |= 演算子が使える
  • Array.push だけなく Array << が使える. shift が使えるのもうれしい.
  • hash がある
  • File の扱いが楽で blob とかがない

今のところでの不満はエラーメッセージが標準ででてこないので自前でAPIからとってくることです.

もうちょっと mruby での調べ事なり雑なエラーチェックをまともにして満足できたならグルー言語として mruby を使うのは結構いい気がしています.

*1:だからといってそれでやってみようとしたのに諦めた無数の無名の挑戦者がいるのは忘れないでほしいです

アーケード用ストリートファイターIIの日本語文字列

https://dev.upergrafx.com/sf2message/
気になったので ROM image から抜き出しました. いくつか仕様をかきます.

  • 位置、色、改行の制御は抜きました.
  • 改行=文字列の終端です.
  • ----xxxx----, #x はわたしが独自にいれました.
  • 濁音/半濁音記号とかな文字は分離しているのはそのままの表記です.
  • 文字化けしていたら文字コードUTF-8 にしてください.
  • 斜めの矢印は Unicode の絵文字です.

Street Figher II - World Warrior

https://dev.upergrafx.com/sf2message/sf2ww_message.txt

  • ---- advice ---- コンティニュー画面の文字列で, #x はキャラ別です.
    • キャラ別に12個 (10個?) の文字列がありますが重複する文字列は省略しました.
    • #4 はなにもないのは #0 と全部同じで省略されたからです.
    • 解析が不十分で [xxxx] などの制御記号をうまく処理できませんでした.
  • ---- message ---- は試合後のセリフとエンディングが一緒になっています

Street Figher II' - Champion Edition

https://dev.upergrafx.com/sf2message/sf2ce_message.txt

  • ---- advice ---- は同じです.
  • タ"メー[8891], [890a]ヒ"エト大統領 などの表記はそこに文字データがありませんでした. おそらく未使用文字列で重要ではないことだとおもいます.
  • フ"レスレっト, ナっシュはゲーム画面でもその表記です.
  • 追加キャラのエンディングの文字列は別に増えました.

Street Figher II' Turbo - Hyper Fighting

  • すべての文字列が前バージョンと同じなので省略します.
  • バルログのバックステップはコマンドが変わったので、該当文字列は未使用になったはずです(未確認)

Super Street Fighter II - The New Challengers

https://dev.upergrafx.com/sf2message/supersf2_message.txt

  • 濁音/半濁音文字が登録されました.
  • 変換元は通常版でトーナメント版ではありません.
  • ---- advice ---- はキャラ別のデータ管理が別になったのでこちらでは表記していません.
  • すべてのキャラで試合後のセリフとエンディングが別管理になりました. キャラ別の分類用データは見つけていません.

Super Street Fighter IIX - Grand Master Challenge

https://dev.upergrafx.com/sf2message/x_message.txt
https://dev.upergrafx.com/sf2message/super_x_messege_diff.txt (差分)
大半が前作と同じなので差分をみたほうが便利だと思われます.

開発後記

Super SF2 で大量の漢字が登録されたので Google Drive -> 同 Docs で OCR にかけてみましたが、中国語にない漢字(売、与、写など多数)の識別がほとんどできていなくて、日本語認識モードがほしいと思いました.

Hyper SF2 や他の機種の解析はやりません.

ICE40 の spi programmer と jtag debugger その4

d2xx と libftdi1 の共有

d2xx のほう.

static int x_ft_write(void *tt, uint8_t *data, int length)
{
	struct d2xx *t = tt;
	DWORD rc;
	FT_STATUS s = FT_Write(t->h, data, length, &rc);
	assert(s == FT_OK);
	return rc;
}

static int x_ft_read(void *tt, uint8_t *data, int length)
{
	struct d2xx *t = tt;
	DWORD rc;
	FT_STATUS s = FT_Read(t->h, data, length, &rc);
	assert(s == FT_OK);
	return rc;
}

static void x_ft_close(void *tt)
{
	struct d2xx *t = tt;
	FT_STATUS s = FT_SetBitMode(t->h, 0, FT_BITMODE_RESET);
	assert(s == FT_OK);
	FT_Close(t->h);
}

static const struct mpsse_driver d2xx_driver = {
	x_ft_write, x_ft_read, x_ft_close
};

libftdi1 のほう.

static int mou_write(void *tt, uint8_t *data, int length)
{
	struct ftdi1 *t = tt;
	return ftdi_write_data(&t->mpsse_ftdic, data, length);
}

static int mou_read(void *tt, uint8_t *data, int length)
{
	struct ftdi1 *t = tt;
	return ftdi_read_data(&t->mpsse_ftdic, data, length);
}

static void mou_close(void *tt)
{
	struct ftdi1 *t = tt;
	if(t->mpsse_ftdic_open){
		if(t->mpsse_ftdic_latency_set){
			ftdi_set_latency_timer(&t->mpsse_ftdic, t->mpsse_ftdi_latency);
		}
		ftdi_disable_bitbang(&t->mpsse_ftdic);
		ftdi_usb_close(&t->mpsse_ftdic);
	}
	ftdi_deinit(&t->mpsse_ftdic);
}

static const struct mpsse_driver ftdi1_driver = {
	mou_write, mou_read, mou_close
};

こんな感じにクラスもどきを作って, 切り替える.

これの対応自体は大したことがないが C 故に void * を使うのでエラーチェックが雑で原因究明に困るとか、グローバル変数をローカル変数にかえる(必須ではないがやりたくなる)のが手間. void * は &data と書くべきところを data としていたので動かない系統のミスが多すぎるし予防できない.

flash_wait() の修正

verbose の都合なのか status register を待ち時間をいれて 2 度確認する仕様になってたので書き直した.

static void flash_wait(struct mpsse *t, const useconds_t first, const useconds_t every)
{
	if(verbose){
		fprintf(stderr, "waiting..");
	}

	useconds_t waitus = first;
	uint8_t data[2];
	do{
		usleep(waitus);
		waitus = every;
		data[0] = FC_RSR1;
		data[1] = 0;
		flash_xfer(t, data, sizeof(data));
	}while(data[1] & 1);

	if(verbose){
		fprintf(stderr, "R\n");
		fflush(stderr);
	}
}

戻り値 void, 引数 void の関数とか while(1) がやたらと多いソースはもしかするとダメなんじゃと予感がするがほぼ的中する.

flash_read() の分離

他に flash_read_status とかがあったので prefix を flash_read_data に改名. flash_read() のなかで [CS=L, コマンド発行, data 取得, CS=H ]の流れを毎回やっていたので flash_read_data_start() で [CS=L, コマンド発行], flash_read_data_continue() で [data 取得] に分離して、ループの内部での無駄な手続きを削除.

妙に長い待機時間を削減

ループの外とはいえ 200ms 待つ処理がコピペされてたので技術文書と確認. iceprog も C_RESET_B = 0 の期間にすべて転送していているので、規定時間というのが場当たり的対応になっている.
それでも 200 ms は明らかに長いのでこちらも場当たり的に削減.

根本的には時間が明言されている C_RESET_B = 0 をして SS = 0 で C_RESET_B = 1 にして slave mode に入ることが求められる. これの実装はソフトとハードも後回し.

総括

これらの対応で前回 6.9 秒かかった処理が 4.2 秒にまで縮んだ. これでとりあえず満足したので本来の目的に戻る.

ICE40 の spi programmer と jtag debugger その3

Radiant Programmer の処理速度がとても遅い理由

実際に動くまでに約10秒かかる理由

FTDI 提供の libmpsse を自分でプログラムを書いてみたら、libmpsee の関数を呼ぶだけで3秒もかかった. それらを3つぐらい呼んでいることが原因かもしれない.

FT_SetLatencyTimer() の値を小さくするとそれが軽減できる傾向があるようだ.

program に約16秒かかる

flash memory address:0 を program するまえに chip erase をしているらしい. 他のセクタにいれていたデータもまるまる消えていることが判明. 利用している flash memory (4M bytes) の chip erase time は typ で 10 秒なので時間がかかるのは当然. 0x20000 bytes にも満たない data に chip erase (全領域初期化) は気が狂ってる.

address 0 ではないデータは sector erase かけてるのかも気になっている.

とにかく Radiant Programmer の品質はひどいもので捨てることにした.

opensource 開発環境についてる iceprog を利用してみたが遅かった

https://github.com/YosysHQ/icestorm/tree/master/iceprog

簡単なソースなのでとりあえずコンパイルした -> FT232H を認識しない -> zadig で FTDI ドライバから WinUSB 系ドライバに書き換え -> 動いたがおそい.

これは初期化は遅くないのだが、0x20000 bytes の data の program と verify で 36 秒もかかったので原因を調べた. 1 番の原因は flash_wait() にあった.

static void flash_wait()
{
	if (verbose)
		fprintf(stderr, "waiting..");

	int count = 0;
	while (1)
	{
		uint8_t data[2] = { FC_RSR1 };

		flash_chip_select();
		mpsse_xfer_spi(data, 2);
		flash_chip_deselect();

		if ((data[1] & 0x01) == 0) {
			if (count < 2) {
				count++;
				if (verbose) {
					fprintf(stderr, "r");
					fflush(stderr);
				}
			} else {
				if (verbose) {
					fprintf(stderr, "R");
					fflush(stderr);
				}
				break;
			}
		} else {
			if (verbose) {
				fprintf(stderr, ".");
				fflush(stderr);
			}
			count = 0;
		}

		usleep(1000);
	}

	if (verbose)
		fprintf(stderr, "\n");

}

どの flash command に対しても usleep(1000); なのに対して使っている flash memory の page programming time は typ で 700 us である. chip erase の場合は 1000 us 単位で polling するのは無駄なのは目に見えている.

直したのがこれ.

static void flash_wait(const useconds_t first, const useconds_t every)
{
	if(verbose){
		fprintf(stderr, "waiting..");
	}

	useconds_t waitus = first;
	int count = 0;
	while(1){
		uint8_t data[2] = {
			FC_RSR1
		};

		flash_chip_select();
		mpsse_xfer_spi(data, 2);
		flash_chip_deselect();

		if((data[1] & 0x01) == 0){
			if(count < 2){
				count++;
				if(verbose){
					fprintf(stderr, "r");
					fflush(stderr);
				}
			}else{
				if(verbose){
					fprintf(stderr, "R");
					fflush(stderr);
				}
				break;
			}
		}else{
			if(verbose){
				fprintf(stderr, ".");
				fflush(stderr);
			}
			count = 0;
		}

		usleep(waitus);
		waitus = every;
	}

	if(verbose){
		fprintf(stderr, "\n");
	}
}

試しに usleep(700) にしたら programming はとても早くなった. そこで待ち時間を引数に変え,1度目は typ の値, 2 度目以降は小刻みに polling するほうがいい.

実際の運用例はこれ. もともとはクッソ長い main() の中にあったが気持ち悪かった(個人の意見です)ので分離.

static void flash_program_main(const struct options *t, FILE *f, const int file_size)
{
	if(t->disable_protect){
		flash_write_enable();
		flash_disable_protection();
	}
	if(!t->dont_erase && t->bulk_erase){
		flash_write_enable();
		flash_bulk_erase();
		//M25P40: typ 4.5 s, max 10 s
		//SST25PF040: typ 0.25 s, max 2 s
		//W25Q32JV: typ typ 10 s, max 50 s
		flash_wait(250 * 1000, 1000 * 1000);
	}
	if(t->dont_erase){
		return;
	}

	fprintf(stderr, "file size: %d\n", file_size);

	int block_size = t->erase_block_size << 10;
	int block_mask = block_size - 1;
	void (*eraser)(int) = NULL;
	useconds_t first, every;
/*
W25Q32JV      |SST25PF040 |M25P40    |
kb, typ [ms], max [ms]|
 4,  45,  400| 4,  40, 150|64, 600, 3000
32, 120, 1600|64,  80, 250
64, 150, 2000|
*/
	switch(t->erase_block_size){
	case 4:
		first = 40;
		every = 5;
		eraser = flash_4kB_sector_erase;
		break;
	case 32:
		first = 100;
		every = 10;
		eraser = flash_32kB_sector_erase;
		break;
	case 64:
		first = 80;
		every = 50;
		eraser = flash_64kB_sector_erase;
		break;
	}
	first *= 1000;
	every *= 1000;
	fprintf(stderr, "programming..\n");
	fflush(stderr);

	uint8_t allff[0x100];
	memset(allff, 0xff, sizeof(allff));
	
	for(int file_offset = 0, rc; file_offset < file_size; file_offset += rc){
		const int flash_address = t->rw_offset + file_offset;
		fprintf(stderr, "\r0x%05x", flash_address);
		fflush(stderr);

		if(!t->bulk_erase && (flash_address & ~block_mask) == flash_address){
			if(verbose){
				fprintf(stderr, "Status after block erase:\n");
				flash_read_status();
			}
			flash_write_enable();
			(*eraser)(flash_address);
			flash_wait(first, every);
		}
		
		uint8_t buffer[0x100];
		int page_size = 0x100 - (flash_address) % 0x100;
		rc = fread(buffer, 1, page_size, f);
		assert(page_size == rc);
		assert(rc > 0);

		if(memcmp(allff, buffer, page_size) == 0){
			continue;
		}
		flash_write_enable();
		flash_program(flash_address, buffer, rc);

		//M25P40: typ 0.8 ms, max 10 ms
		//SST25PF040: typ 4 ms, max 5 ms
		//W25Q32JV: typ 0.7 ms, max 3 ms
		flash_wait(500, 100);
	}

	/* seek to the beginning for second pass */
	fseek(f, 0, SEEK_SET);
}

flash_wait() の引数にはコマンド別に手元にある 3 デバイスのdatasheetをみて無難な時間をいれたがコマンドラインの引数にするなり、デバイスIDから引いてくるということも将来的にはしたほうがいい.

もとの処理は erase (複数) -> program だったが、 program のループで該当アドレスが入るときだけ erase をかける処理に変更した. 入力ファイル末尾に合わせて自動で小さい erase をすることも考えていたが、 flash device によって erase command が2種類や1種類だったのでそれはなくした.

さらに page size の data の中で全部が 0xff で埋まっていたら program の命令を送る必要はないので飛ばす処理を追加. これでかなりの時間節約ができる.

verify/read も何故か 256 bytes*1 単位で取りに行ってたので 0x200 bytes にしたら処理時間が短くなった, さらに 0x800 bytes 単位にしたらそれはできなかったので 0x400 bytes 単位にした.

これらの対応で約36秒の処理時間が約6秒になった.

iceprog を libftdi1 から D2XX ライブラリに変更する

Radiant Programmer は捨てられるが、 Radiant 内の Riveal Analyzer は FTDI のドライバがいる. iceprog の libftdi1 関数を D2XX 関数に書き換えてみて、ドライバの変更なしに動かせるか試した.

libftdi1 が D2XX を参考に作っているようなので似た関数名に書き換えて引数をあわせたらそれで動いた.

例. この関数は iceprog.c から読んでなかったので関数属性に static をつけて mpsse.h の宣言からは消した.

static void mpsse_send_data(uint8_t *data, int length)
{
	//int rc = ftdi_write_data(&mpsse_ftdic, data, length);
	DWORD rc;
	FT_STATUS s = FT_Write(mpsse_ftdic, data, length, &rc);
	assert(s == FT_OK);
	if(rc != length){
		fprintf(stderr, "Write error (%s, rc=%d, expected %d).\n", __FUNCTION__, rc, 1);
		mpsse_error(2);
	}
}

後日どちらのライブラリからでも使えるように変更する. device open などの初期化は勝手がかなり違うので、これはよく考えて作る必要がある.

あと.... ほかの terminal ならいいかもしれないが msys2 は printf してもすぐにでないので時間のかかる処理の手前に fflush(stdout); をいれる必要がありこれが結構面倒くさい.

1つ言えるのは Altera の programmer はわりと優秀だったのでこういうことに時間を取られることがなかったので、 ICE が敬遠される理由がわかってきた.

*1:256 とか 4096 とか直書きするのは意味のある数値に見えないので 0x100 としてほしい

mantisbt のID が 7 桁で多すぎる

core/bug_api.php: 2180

function bug_format_id( $p_bug_id ) {
	#$t_padding = config_get( 'display_bug_padding' );
	$t_padding = 3;
	$t_string = sprintf( '%0' . (int)$t_padding . 'd', $p_bug_id );
	return event_signal( 'EVENT_DISPLAY_BUG_ID', $t_string, array( $p_bug_id ) );
}

設定は3にしているはずなのになぜか読まれてない. (int) ってかいてあるから t_padding は3(数値)ではなく'3'(数字) の気がするが自分は php の言語仕様を知らないし、php も文句いえばいいのに.

ICE40 の spi programmer と jtag debugger その2

昨日書いた回路で SS が双方向で共有信号でないので tristate buffer は経由せず AD4 = SS 直結としました. その回路を組んだところちゃんと動きませんでした.

理由は Radiant Programmer が出す波形が CRESET_B を L レベルのまま FT232H から SPI を操作していました. Lattice の Technical Note も守ってないし、起動前の謎の待ち時間、ちゃんと回路を組んだ時間の浪費などでうんざりしています.

CRESET_B = L のままなら ICE40 側は tristate を維持するようですので、動作としては納得しましたが、Lattice のソフト担当者への信用は減りました.

とりあえずは SLAVE = ~(CRESET_B == 0 && C_DONE == 0) = CRESET_B | C_DONE とすれば想定通り動くはずです.