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 とすれば想定通り動くはずです.

ICE40 の spi programmer と jtag debugger その1

UPduino を昨年少し触っていてある程度使い勝手がわかったのですべて自分で設計した基板を動かしはじめました. いまのところは programming の部分で停滞しているのでその先の動作にはまだ至りません.

FT232H と ADbus

UPduino では FT232H を利用していましたので、秋月電子の AE-FT232HL を利用します. FT232H からは AD7:0 を ICE40 につなぎます.

ADbus と SPI flash

programmer は CRESET_B を制御し, ICE40 を停止し SPI で flash memory に書き込むことができます. 配線は下記となります.

AD
0 - SCK
1 - SDI
2 - SDO
3 - (未使用)
4 - SS
5 - (未使用)
6 - CDONE
7 - CRESET-B

ICE40 は restart 後に回路データを取りに行くために SPI で flash memory に読み込みます. SPI のピン番号は ICE40 から指定されているのでユーザーが自由に決められません. ユーザーの回路データを読み込んだあとは SPI のピンは自由に IO として利用できますが、flash memory の誤動作を防ぐために SS (CS) は完全に自由には利用できません.

ADbus と JTAG

ICE40 では内部ロジックアナライザ(reveral)を利用し JTAG にて通信ができます. ADbus の配線は下記となります.

AD
0 - TCK
1 - TDI
2 - TDO
3 - TMS
4 から 7 - 未使用

これらはユーザーの回路データを読み込んだあとに有効になります. JTAG のピンはユーザーが自由に決められます. 他種ではあまりみられない気がします.
内部ロジックアナライザは任意のもので、他の計測手段があり限界までピン数を使いたいとなればなしにできます.

SPI と JTAG の同居 (簡易版)

ここからが問題となります. この似たような2種類のピンで重複する xCK, xDI, xDO を同じピン番号に割り振ると1つの FT232H で両方利用できます. できますが、ユーザー回路で SPI flash memory が全く使えなくなります.

ADbus から SPI と JTAG の分離/結合

現在停滞している部分です. 1つの FT232H で自動に切り替える回路を模索中です. これらの回路は汎用ロジックを組み合わせるのが現状マシな状態です. GAL などの簡易PLDは開発環境が古すぎて動作が安定しないとか、供給側がまともなサポートをしておらずたぶん10年前のほうが便利でいまは逆に技術が退化している気がしています.

話がそれてしまいました. 3つのモードの切り替えは今の所下記を考えています.

  • CDONE == 1 なら JTAG, ユーザー回路での flash 制御
  • CDONE == 0 で CRESET_B の立ち上がり検出
    • AD4 == 1 なら ICE から startup の flash 読み込み
    • AD4 == 0 なら FT232H から flash の書き込みほか各種制御

FT232H から SPI bus を触る場合は下記とします.

  • AD0 -> SCK, AD1 -> SDI, AD4 -> SS は tristate buffer で出力を制御
  • AD2 <- SDO は selector で入力を選択

FT232H から JTAG を触る場合は下記とします.

  • AD0 -> TCK, AD1 -> TDI は共有だが制御ゲートなしでいいと思う(まだ未確認)
  • AD2 <- TDO は selector で入力を選択
  • AD3 -> TMS は共有はないので直結

立ち上がり検出は register (74HC74)を注文中でそれが届き次第ブレッドボードに差し込んで確認しますが、 tristate buffer (74HC244) と selector (74HC157) は配線済みでそれら経由での SPI bus の制御はできています.
ただ 74HC series を Vcc = 3.3V で動作させてるので伝搬遅延が無視できなくなり TCK Divider Setting >= 2 で動作します.

このような状態で標準的なプログラマがないので自身がプログラマを開発しているのが現状です. 専用基板には 74HC series は使いませんけど、本来の回路の開発にできないのは楽しくありません. なぜだが知りませんが Radiant Programmer は PC から操作をして 10 秒から 15 秒の待機時間があるのも楽しくありません.

SAMD21 で各種ソフトを動かしてみたが性能不足で評価終了

前回は外部デバイスに対してでしたが内部の話です.

Wavpack Tiny Decoder (CPUの性能不足)

tiny decoder は Makefile を調整して ARM 用にしたぐらいですが、リンクも問題なく、動かすことができました.

ソースツリーに含まれている ARM 用のアセンブラソースは使えませんでした. これ ARM7TDMI 用で Cortex M0 用ではないし、自分は ARM のアセンブラが読めないのですすめられないです.

内部のタイマーを利用しながら 75 compact disc frames (=1秒間) を decode したところ 2.9 秒もかかりました. 1 秒間のデータを展開するのに 3 秒間かかるのでは使えません. 実は Cortex M0+ 48MHz ではだめかなと予想しててその通りでした.

一応なんですが hybrid mode で作ったデータでは 0.75 秒間程度の計測値がでてますので、ファイルシステムなしとか他に重い割り込み処理を入れないとか条件付きでは一応使えるという値です. アセンブラのソースをいれても4倍以上の処理速度がでるとは思えません.

LZ4 (decode できてない)

PC 側で encode するのはとても簡単だったのですが、decode のまともな C のコードを用意できてません.

LZ4 のサイトに多種の decoder への言及がありますが、ARM Cortex assembly decoder はリンクはできたけどちゃんと動きませんでした. C decoder (tiny) はどうも 80386 依存のコードになっていて実用的ではないです. LZ4 の公式の decoder の C ソースはちょっとみましたが、OS がある前提でかなり safety に作られてて MCU 用には各種手続きを削る手間があり、やってません.

LZF (実用可能)

こちらは無難な C の関数で fall-through の Warning を止めれば、コンパイル、リンク、Decode すべてできました.

計測方法は手抜きで 0x800 bytes (1/75秒)の data を decode して 75 倍した時間で 0.094 秒でした.

考察

CPU の性能不足なので SAMD21 での評価はこれで終了とし、SAMD51 など Cortex M4 あたりで評価をやり直す必要があります.

LZ4 は top page がぱっとみ素晴らしいんですが、 decode できないとか、アセンブラじゃ PC 上のテスト用環境でも動かせないとかで悪い方に予想外でした. msys で pacman からとってこれるとか PC 向けのパッケージ管理はよかったです.

LZF は LZ4 の性能比較対象にもありますけど、こっちのほうがいいかなと今のところは思います.

そもそも data sector を圧縮する意義はあるのかという話もあります.

SAMD21 のドライバを書いてみる

ASF (メーカー提供のライブラリ)なしでやってます.

I2S (断念)

master clock を入力もしくは内部の GCLK を利用し SCK (BCLK), FS (LRCK) を出すのはそんなに難しくありません. そこから data を出すのにつまってしまい、適切な解決方法が不明で断念となりました.

この確認はとりあえずソフトから I2S->DATA[x].reg = xx; とするのですが I2S->CLKCTRL[x] への I2S_CLKCTRL_SLOTSIZE_32 でのみ SD (DAC.DAT) が反応することまではわかりました. SLOTSIZE は 16 で使いたかったのですが...

よくわからないのは I2S->DATA[x].reg へ書き込む際の型も 16bit 単位でかけるのかもよくわからなく SLOTSIZE_32 で 32bit 単位で I2S->DATA[x].reg をかけば動くというものでした.

ここからうまく動けば DMA controller に RAM から I2S->DATA[x].reg を定期的に書いてもらうつもりでしたがうまくいかなそうだし、MCU から直接 I2S を出すのはここだけで試して今考えている今後の設計ではつかわないつもりでしたのでここでやめました.

WavPack の tiny decoder の出す decoded data が int32_t で bit15:0 のみが有効な配列のため I2S->DATA[x].reg に書く型は 16 bit で address increment を 4 にするつもりでやってたので無理そうです.

SERCOM.SPI.Master (使えた)

関数を抜粋します.

#define DECLARE_PORT(name) volatile SercomSpi *name = &SERCOM4->SPI
static inline void spi_write(uint8_t data)
{
	DECLARE_PORT(s);
	s->DATA.bit.DATA = data;
	while(s->INTFLAG.bit.TXC == 0);
}

この関数の前に s->CTRLB.bit.RXEN = 0; が必要です. この RXEN は書き換えはいつでもできて同期もいりません. 逆にいうとほかの設定向けレジスタは s->CTRLB.bit.ENABLE = 0; にした上で各種同期がいります.

TX の DMA (polling, 完了割り込みなし)です.

static void dma_write(const uint8_t *src, int src_increment, int length)
{
	uint16_t flag = DMAC_BTCTRL_STEPSIZE_X1 | DMAC_BTCTRL_BEATSIZE_BYTE;
	if(src_increment){
		flag |= DMAC_BTCTRL_SRCINC;
	}
	DECLARE_PORT(s);
	dma_descripter_set(DMACH_SDC_SPI_TX, src, (void *) &s->DATA, length, flag);
	dma_channel_enable_polling(DMACH_SDC_SPI_TX, SERCOM4_DMAC_ID_TX);
	while(s->INTFLAG.bit.TXC == 0);
}

DMACH_xxx は私が定義した DMA channel number のための enum です.

src_increment の切り替えは結構大切です. 意図的に CLOCK を動かして初期化する場面、memset のように一定データで埋めたい場面、RX のための CLOCK を動かす場面など用途が結構あります.
また dma controller における s->DATA の書き込みは SERCOM への同期が含まれていないので DMA controller としては最後のデータを書いた時点で完了となります. よって s->INTFLAG.bit.TXC の同期が必要です.

static inline uint8_t spi_read(void)
{
	DECLARE_PORT(s);
	s->DATA.bit.DATA = 0xff;
	while(s->INTFLAG.bit.RXC == 0);
	return s->DATA.bit.DATA;
}

この関数の前に s->CTRLB.bit.RXEN = 1 が必要です. CLOCK の動作は s->DATA への書き込みによって発生します.

RX の DMA (polling) で、これが曲者でした.

static void dma_read(uint8_t *dest, int length)
{
	const uint16_t flag = DMAC_BTCTRL_STEPSIZE_X1 | DMAC_BTCTRL_BEATSIZE_BYTE | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_STEPSEL;
	DECLARE_PORT(s);
	dma_descripter_set(DMACH_SDC_SPI_RX, (const void *) &s->DATA, dest, length, flag);
	switch_rx();
	dma_channel_enable(DMACH_SDC_SPI_RX, SERCOM4_DMAC_ID_RX);
	
	const uint8_t ff = 0xff;
	dma_write(&ff, 0, length);
	dma_channel_disable(DMACH_SDC_SPI_RX);
}

SERCOMx_DMAC_ID_RX だけの DMA をいれても CLOCK が動かないので、TX も並列動作させます. TX の書き込みは increment なしで ff というローカル変数使ってますけど、割り込みでこの関数を抜ける場合(ffが無効になる場合)はいつでも無効にならない static 変数を利用する必要があります.
DMA channel number は TX < RX として, 転送量も同じにして、 RX を polling なしで動かしてから、 TX を polling します. また dma_write() のように同期がいります. (RXC の同期はいらない気がして抜きました)

dma_write() は 4 byte とか 8 byte でも使ってもいいんですが、 dma_read() は動作の波形をみると初期化に時間がかかるので 10 byte 以上に使ったほうがいいです. さらに転送量が多くなると当然割り込み待ちにもなるんですがまだそこまで試す段階にはなってません.

dma_read は forum の書き込みを参考に対処させてもらいました. DMA controller の使い方はよくわかってなかったのですが、channel 並列動作の場合は、channel を切り替えて順番にやることもここで理解しました.

*1

*1:dma_descripter_set(), dma_channel_enable_polling() は初期化をまとめたもので割愛します.

CD image を可逆/非可逆圧縮して保存する

前回 libFLAC を組み込めるか検討しましたが大変そうなのでやめました. そのあと... とても長い間やる気をなくしてしまいました.

WavPack を利用する

別途同目的の規格を探していたところ、 WavPack のダウンロードページに https://www.wavpack.com/downloads.html Tiny Decoder (Tiny Encoder もある)があるのを見つけたので利用してみました.

Tiny Decoder は10年以上更新されていないソースでしたが、現在のコンパイラ(x64 の clang)でも特に問題なく通り、最新バージョンの encoder で生成したファイルも問題なく decode できました. またドキュメントには当時の MCU で利用していることが限られた性能で十分に動いているし、 malloc を使っていないことを書いてあるのでこれを深く試すことにしました.

Tiny Decoder のソースを触る

これは本当に再生だけで seek 機能がないので、wavpack 規格を知ることも兼ねてをソースに手をいれました. 弱いと感じたのは順番にデータを読むので時刻とファイルオフセットを1度すべて読んでキャッシュを作る必要がある点でした.

ほかのソースも作りましたがこれが一番時間がかかりました.(3日間)

wavpack ですべての PCE の CD をエンコードする

data track も無関係にエンコードしたところ、平均圧縮率(decoded file size / original file size, 小さいほど優秀) は 61% でした. audio data だけでみるとおおよそ 1/2 ぐらいで、 flac とも同じに見えます.

平均ですので、最悪値もあり圧縮率 101% もありました. これは 1 disc で data track の割合が 100% のソフトがあったからで、それ以外にも data track が多いソフトは圧縮率 70% もあります.

PC 上の .wv file を再生すると当然ながら data track を audio track して再生したことになり不快な音がでてしまいます.

dll を利用して、専用 wavpack encoder を作ってみる

WavPack5FileFormat.pdf 内 2.0 Block Header に下記の記述があります.

typedef struct { 
    char ckID [4];          // "wvpk"
    uint32_t ckSize;        // size of entire block (minus 8)
    uint16_t version;       // 0x402 to 0x410 are valid for decode
    uchar block_index_u8;   // upper 8 bits of 40-bit block_index
    uchar total_samples_u8; // upper 8 bits of 40-bit total_samples
    uint32_t total_samples; // lower 32 bits of total samples for
                            // entire file, but this is only valid
                            // if block_index == 0 and a value of -1
                            // indicates an unknown length
    uint32_t block_index;   // lower 32 bit index of the first sample
                            // in the block relative to file start,
                            // normally this is zero in first block
    uint32_t block_samples; // number of samples in this block, 0 =
                            // non-audio block
    uint32_t flags;         // various flags for id and decoding
    uint32_t crc;           // crc for actual decoded data 
} WavpackHeader;

block_samples = 0 とした場合は non-audio block にできるとあるので、実際に生成されたファイルのヘッダを分析し、手を加えることにしました. block_samples = 0 として、こちらで data sector (中身だけ, 0x800 bytes) をいれて、audio data は 0 だけの無音をいれました.

重要なソースは下記です. ソース全体は https://pastebin.com/T3Hj2HMc でみてください.

	const uint8_t compactdisc_data_header[] = {
		0, 0xff, 0xff, 0xff, 0xff, 0xff, 
		0xff, 0xff, 0xff, 0xff, 0xff, 0
	};
	WavpackHeader h = {"wvpk", 0, 0x410, 0, 0, src_sector_num * config.block_samples, 0, 0, 0x14801821, 0};
	assert(sizeof(h) == 0x20);

	(中略)
		int32_t *audio_src = b;
		if(memcmp(compactdisc_data_header, fb, sizeof(compactdisc_data_header)) == 0){
			uint8_t compressed_data[0x800 * 4];
			uint32_t compressed_size = (*packer)(fb + 0x10, 0x800, compressed_data, 0x800);
			if(compressed_size == 0){
				compressed_size = 0x800;
				memcpy(compressed_data, fb+10, 0x800);
			}
			h.ckSize = 0x20 - 8 + compressed_size;
			h.crc = crc32(0, fb + 0x10, 0x800);
			fwrite(&h, 1, 0x20, d);
			fwrite(compressed_data, 1, compressed_size, d);

			audio_src = audio_silent;
		}
		r = WavpackPackSamples(w, audio_src, config.block_samples);
		assert(r == TRUE);
		r = WavpackFlushSamples(w);
	(以下略)

いまは .img のファイルだけみてるので data/audio 振り分けはヘッダの有無というとても雑なものですが、実験ということで細かい部分は後回しにします.

*packer に関しては data 用の可逆圧縮ルーチンで非力な MCU でも利用できる lz4 か lzf を選んでいれてみました. ただこれでも圧縮できない場合は、もとのデータをそのままいれることにしました.

エンコードしたデータは PC 用の player (foobar2000, MPC-HC)で問題なく再生できます. ソース全体見ればわかりますがこちらが作成したのは1つのCファイルでこれも1日間で作れました.

非可逆オーディオもいれられる

data track は可逆圧縮(または無圧縮)は必須ですが、オーディオは非可逆でもなんとかなる気がしたので、 wavpack の hybrid mode を利用してみました. これだと audio data が 1/4 程度になります.
音質に関しては詳しくないのですが、違和感はありませんでした.

これも同様に PC 用の player でも再生できますし、 tiny decoder でも問題なく decode できました.

ソフト別で圧縮率を比較する

全部を見たい方は http://dev.upergrafx.com/download/cdimage_fraction_ratio.csv .

0.82	0.18	0.61	0.578	0.577	0.27	0.51	(average)
audio	data	wv/img	lz4/img	lzf/img	lossy/img	chd/img	name
0.00	1.00	1.01	0.84	0.84	0.85	0.61	jccd3012_sherlock_holmes_no_tantei_kouza
0.00	1.00	1.00	0.86	0.85	0.86	0.61	jccd1004_sherlock_holmes_no_tantei_kouza
0.02	0.98	0.95	0.66	0.63	0.66	0.42	nipr1002_de-ja
0.12	0.88	0.89	0.73	0.72	0.69	0.54	kmcd4007_tokimeki_memorial
(中略)
0.97	0.03	0.60	0.61	0.61	0.21	0.57	nxcd3019_downtown_nekketsu_monogatari
0.97	0.03	0.60	0.60	0.60	0.21	0.56	napr1025_forgotten_worlds
0.93	0.07	0.60	0.61	0.61	0.23	0.56	nxcd2010_double_dragon2the_revenge
0.97	0.03	0.60	0.62	0.62	0.21	0.57	fccd4001_shin_nihon_pro_wrestling_kounin
(中略)
0.90	0.11	0.43	0.39	0.39	0.24	0.35	hcd4064_deden_no_den
0.86	0.15	0.41	0.40	0.40	0.19	0.35	tjcd1015_cosmic_fantasy2bouken_shounen_b
0.93	0.08	0.40	0.39	0.39	0.22	0.34	nscd0002_sol_bianca
0.96	0.04	0.37	0.38	0.38	0.20	0.33	nxcd2011_wizardry5heart_of_the_maelstrom

値の説明

  • audio, data: 1 disc の audio/data の割合. 2つを足すと1になる.
  • wv/img: 全部audioとして encode した .wv file size / original file size
  • lz4/img: audio は可逆 wavpack, data は lz4 + 無音で encode した圧縮率
  • lzf/img: 上記から lz4 を lzf にかえたもの
  • lossy/img: lz4/img から audio は hybrid (非可逆) wavpack にかえたもの
  • chd/img: mame の chdman で encode した chd file size / original file size
  • 平均圧縮率は lz4:0.578, lz4:0.577 でほんのわずか lzf が優秀

考察

  • data sector 可逆圧縮は意外と効果が少ない. 0x800 bytes 単位では小さいのでオーバーヘッドが大きいと思われる.
  • data track が多い場合は可逆圧縮がそれなりに効いている, audio track が多い場合は data 圧縮が効果が少ない
  • 平均をみると chd にわずかに多い程度でしょぼい MCU でもそんなに悪くない
  • hybrid にするなら今回の data sector + 無音挿入は結構よさそう.

付加データ

f:id:na6ko:20210211161140p:plain
.wv ファイルに cuesheet とジャケット画像をいれられるので、ぱっとみが非常によくできます. 画像は web 上から勝手にとってきました.

ちょっと気になるのは cuesheet や tag の文字コードが混沌としていて、palyer 側は UTF-16LE で非ASCII文字がでる可能性が高いです. 規格上は UTF-8 と書いてるんですが大丈夫なのでしょうか. また古参ユーザが無意識に Windows-31J をねじ込んできそうで混乱はありそうです.