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 としてほしい