MAME から無劣化動画を作る (かなり面倒くさい)

MAME の AVI 作成機能ではなくて MNG 作成機能から lossless の動画を作りました. 10年前に試したところ AVI 作成機能は圧縮一切なしの超巨大ファイルを作るし、 MNG は対応しているソフトが当時も今も皆無です.

MAME で入力を記録する

mame [gamename] -record [recordname]

満足のいく内容が取れるまでゲームを遊びます. [] で囲ったところは任意です.

MAME で入力を再現させて、それを MNG file と wav file でだす

mame [gamename] -playback [recordname] -mngwrite [mngfilename] -wavwrite [wavfilename]

mng ファイルは、表示フレームをすべて png 画像として圧縮してそれを順番にいれているものです. 圧縮するので再現中はゲームのスピードがかなり遅くなるので気長に待ちます.

出力された mng と wav のファイルは間違って消さないように readonly 属性を付けておいたほうがいいでしょう. コマンドラインだと履歴巡って再実行でファイル上書きでやりなおしの事故がよくあります.

MNG file を連番の PNG file として大量に出力する

対応しているソフトがないので分離します. やること自体は単純で IHDR と IEND の間の chunk を切り出して png のヘッダをつけてファイルに出します.

問題は1秒間で60個、1分間で3,600個、10分間で36,000個のファイルになるので2時間も録画したものを分離すると、ディスクの圧迫は当然としてファイルシステムやファイル管理に異常が出るらしくファイルの出力が何故か遅くなります.

この対策として 10,000 個ずつ png ファイルを出して、それを1つの mp4 ファイルに統合を繰り返し、複数の mp4 ファイルを1つに統合する、という流れであればマシになりました.

分離ソフトは自作です.先述のように1時間30分ほどの1つの mng ファイルを一度に分離したらファイル生成が妙に遅くなって出力に4時間かかりました. 10,000単位なら1分程度で終わります. 1分でも遅いのですがボトルネックはファイル出力にあるので、自分の力では改善できそうにありませんでした.

連番の PNG file 群を1つの mp4 ファイルに統合する

rm -f [09999.png]
ffmpeg [options] -r [fps] -i [%06d.png] -c:v libx264 -preset ultrafast -crf 0 -f mp4 [v%04d.mp4]
  • [09999.png] は分割生成で結合前に最終フレームのファイルを1つ消します. かなり重要で、結合の部分で説明します.
  • [options] は -y とか -loglevel とか好きなのを入れます
  • [fps] は MNG file の MHDR chunk 内部の Ticks_per_second から取得します. これは整数なのが気になりますがそれでうまくいきました. http://www.libpng.org/pub/mng/spec/mng-1.0-20010209-pdg.html#mng-MHDR
  • [%06d.png] は入力ファイル, [v%04d.mp4] は出力ファイル

これで 1 frame 欠けてますが、可逆圧縮の H264 でエンコードできます. MAME が出す AVI ファイルに比べるとかなり小さく、MNG ファイルより小さくなりました.
png への分離と H264 へのエンコードを繰り返します.

連番の映像ファイルを1つに統合する

ffmpeg [options] -f concat -safe 0 -i [list] -c copy video.mp4

このコマンドは web 上のffmpeg のマニュアルや野良情報と同じです.

この結合機能は入力ファイルごとになぜか 1 frame ずつ増えてしまい、音ズレの原因となっていました. 16個も分離した場合には後半の音のズレがひどいので使い物にならないので、入力側から 1 frame 削除しておくのが妥協案だと思います. (この原因解明に1日かかりました)

結合前に音をいれておくと音ズレは予防できたのですが、今度は結合フレームで音が切れてしまう問題があり、その点でもこれが一番マシだと思います.

統合した映像ファイルに音声を追加する

野良情報と同じなので省略します.

一連の処理をスクリプトで管理する

mng から png 分離プログラムとスクリプトはまだ未完成なのでできたら公開するつもりです.

かなり変数をとってくる必要があるので bat や bash では無理でした.
あとは中間ファイルの削除もスクリプトにいれる必要があります.

ようやく編集する

この面倒な作業を終えるとようやく編集できますのでアスペクト比とかfpsの変更、ステージごとの分割ということができます.

libFLAC 続き

malloc, calloc, realloc, fclose が残っていましたので除去しました.
一応PCで動いているみたいですが ROM と RAM の消費量は下記となりました.

0x3870 .text                    libflac.a(stream_decoder.o)
0x1158 .text                    libflac.a(bitreader.o)
0x0b24 .text                    libflac.a(format.o)
0x0a4c .text                    libflac.a(fixed.o)
0x09a4 .text.qsort              libg.a(lib_a-qsort.o)
0x0960 .text                    libflac.a(lpc.o)
0x0578 .text.__ieee754_log      libm.a(lib_a-e_log.o)
0x1100 .rodata                  libflac.a(crc.o)
0x063c .rodata.log.str1.4       libm.a(lib_a-w_log.o)
0x2be8 .bss                     libflac.a(stream_decoder.o)
0x102c .bss                     libflac.a(bitreader.o)
0x0428 .data.impure_data        libg.a(lib_a-impure.o)
0x0200 .bss                     libflac.a(format.o)

allocate_output_(), private_->output, private_->residual

output と residual は int32_t の配列で下記の RAM を必要とします.

sizeof(int32_t) * channels * blocksize * 2
  • sizeof(int32_t) は最終の decode された data が 16bits であっても decode の過程で上位 16bits は利用しているので int16_t に変更はできませんでした.
  • channels は CDDA であれば 2 固定です.
  • blocksize は encode 時のパラメータで変更可能です.
  • 最後の *2 は output と residual それぞれで2つです.

前回では根拠なく blocksize を 2352 にしましたが、これですと 0x9300 bytes も必要になりますので 2352 / 4 に変更しました. 588 を超える値で encode された flac data は利用できない制限ができてしまいました.

output は絶対にいるにしろ residual を 1 channel だけ使うようにできればもっと RAM の確保に余裕が持てそうです.

libFLAC/lpc.c

libFLAC/stream_decoder.c の read_subframe_lpc_() で decoder->private_->local_lpc_restore_signal(_16bit|_64bit)? がありますが x86_64 と i686 の CPU 向けの最適化で利用しているみたいなので関連する関数ポインタは使わずに、 FLAC__lpc_restore_signal() のみを利用します. FLAC__lpc_restore_signal_64bits() は CDDA ではいらないみたいなのでなくしました.

FLAC__lpc_restore_signal() は redisual と output を利用している部分なので residual が減らせるか調査した方がいいと思います.

metadata各種

METADATA_BLOCK_STREAMINFO は固定長、必須データですので問題ありません.
他の metadata は可変長データが含まれており、そのたびに malloc 系で RAM を確保します. METADATA_BLOCK_SEEKTABLE を除き、 可変長 metadata は音楽データの decode 前に動的確保され、 callback 関数の呼び出しの後に動的解放を行います.
先述の private_->output, private_->residual は排他の関係だと思うのでこの静的確保 RAM を必要量に応じて動的配分するということにしました.

METADATA_BLOCK_SEEKTABLE はずっと確保されているのですが、なくても動くと書かれているので利用しないようにソースを調整しました. 動かしてみて seek の時間が遅すぎるなら調整が必要です. 標準の encoder では単純に 10 秒間隔でいれているのでデータ量をこの用途ではもう少し減らした方がいいです.

残りは METADATA_BLOCK_PICTURE ですが、これは非常に難しいです. 問題を列挙します.

  • private_->output で確保した領域は画像を入れる場所としては小さい
  • png/jpeg などを使う場合は別途画像ライブラリで展開するため RAM が足りなすぎる
  • 別途の画像ライブラリはいまのところ何も考えていない
  • もし VRAM 用のベタデータを割り振るにしろ必ず RAM に展開する仕組みなのでやはり RAM が足りない
  • picture は UI でほしい

画像データの最小転送量は decode するなら block size だけ展開し、decode した data は VRAM 用の stream に流し込む仕組みでないと.. 難しいですね.

各種 metadata は利用できないように設定できますので今のところ画像は触れないということにしています.

FLAC__format_entropy_coding_method_partitioned_rice_contents_ensure_size (長い)

safe_realloc_() を呼ぶ場所がありました. debug print をいれたら 0x100 * 2 bytes が必要で、呼ばれるのは1度だけでしたので、それだけを確保しました. それが適切なのかよくわかりません.

その他

libFLAC 側でファイルを管理してもらう FLAC__stream_decoder_init_FILE() と関連する関数や decoder->private_ の変数はすべてなくしました. ここに fclose が残っていてこれが malloc/free を利用しているようでした.

decoder->private_->last_frame.subframe は(今のところ)重要ではないのに消費量が多いのでなくして header と foot だけにしました.

今のところの評価

libflac に必要な RAM は 0x3e14 bytes なので、 MCU の SAMD20 の最低 1/4 を占めることになります. mruby/C も最低 1/4 なのでこの2つが最終的に2/3を使うのであればギリギリ大丈夫なのでは... というところです.

libFLAC の自作 PC 向けソフトへの利用、および MCU 向けの調査

説明の順番をすっとばしますが、 FLAC の一連のソースコードやドキュメントはとても丁寧です. いままでみた有名なプロジェクトのソースコードの中でもきれいで感動しました.

configure (automake) からのビルド

なにもよまずにやってみましたが、 memset_chk() がリンクできないというエラーがでてしまいました. make の途中では gcc のコマンドがでないようになっていてにコンパイル/リンクのオプションが不明な上、automake で生成された Makefile も私が解読できなかったのであきらめました.

automake の場合はこちらもとても丁寧な自動テストが入っているのでそれもつかうほうが品質のためにはよいです.

Makefile を作る

作ってから同じ目的の Makefile.lite というのがあることをしりましたのでここは飛ばしていいと思います.

とりあえず自分がいつも使うオプションでコンパイルを始めてとまったら分析してオプションをつけてやり直しという形にしました. src/libFLAC++, src/utils, テストスクリプトは使わなかったのでわかりません.

my.mk

ifeq ($(MSYSTEM_CARCH), i686)
	CC = gcc
else
	CC = clang
endif
CFLAGS = -O2 -Wall -Werror -I$(TOPDIR)/include
CFLAGS += -DHAVE_FSEEKO -DHAVE_LROUND -DPACKAGE_VERSION=\"1.3.3\"
CFLAGS += -DFLAC__HAS_OGG=0
CFLAGS += -D__STDC_FORMAT_MACROS -DFLAC__NO_ASM -DHAVE_STDINT_H

.PHONY: all clean

これは src/libFLAC, src/flac, src/metaflac で共用の設定で各ディレクトリの Makefile から include します. 必要なソースファイルは Makefile.am か Makefile.lite をみたほうがいいのでここでは記載を省略します.

この設定だけで(いままでみた著名なオープンソースプロジェクトと比較して)簡単に flac, metaflac, libflac.a がビルドできました. 試してみたところ flac, metaflac は私の目的では手を入れる必要がないとわかったので公式に配布されている物を利用した方がいいです.

SDL との連携

sound_flac.c, 大半の callback 関数は examples/c/decoder と libflac にのってたものをそのまま利用してます.

#include <inttypes.h>
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <FLAC/stream_decoder.h>
#define DP printf("%s:%d\n", __FUNCTION__, __LINE__); fflush(stdout)

struct sound_flac{
	FILE *flac_file;
	FLAC__StreamDecoder *flac_decoder;
	struct {
		uint8_t *buf;
		int len, pos;
	}src, dest;
};
//static 関数の prototype は省略
#define SRC_BUF_SIZE (2352*4*16)
struct sound_flac *flac_init(void)
{
	static struct sound_flac t;
	static uint8_t src_buf[SRC_BUF_SIZE];
	memset(&t, 0, sizeof(t));
	t.flac_file = NULL;
	t.flac_decoder = FLAC__stream_decoder_new();
	t.src.buf = src_buf;
	assert(t.flac_decoder != NULL);
	FLAC__StreamDecoderInitStatus r = 
	FLAC__stream_decoder_init_stream(t.flac_decoder, 
		flac_read_cb, flac_seek_cb,
		flac_tell_cb, flac_length_cb, flac_eof_cb,
		flac_write_cb, flac_metadata_cb, flac_error_cb, 
		&t
	);
	if(r != FLAC__STREAM_DECODER_INIT_STATUS_OK){
		printf("%s ERROR: initializing decoder: %s\n", __FUNCTION__, FLAC__StreamDecoderInitStatusString[r]);
		return NULL;
	}
	return &t;
}
void flac_file_load(struct sound_flac *t, const char *filename)
{
	t->flac_file = fopen(filename, "rb");
	assert(t->flac_file != NULL);
	FLAC__stream_decoder_process_until_end_of_metadata(t->flac_decoder);
}
void flac_delete(struct sound_flac *t)
{
	FLAC__stream_decoder_delete(t->flac_decoder);
	if(t->flac_file != NULL){
		fclose(t->flac_file);
	}
	t->flac_decoder = NULL;
	t->flac_file = NULL;
}
static FLAC__StreamDecoderReadStatus 
flac_read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data)
{
//サンプルと同じなので省略
}

static FLAC__StreamDecoderSeekStatus 
flac_seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, void *client_data)
{
//サンプルと同じなので省略
}
static FLAC__StreamDecoderTellStatus 
flac_tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, void *client_data)
{
//サンプルと同じなので省略
}
static FLAC__StreamDecoderLengthStatus 
flac_length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, void *client_data)
{
//サンプルと同じなので省略
}
static FLAC__bool 
flac_eof_cb(const FLAC__StreamDecoder *decoder, void *client_data)
{
//サンプルと同じなので省略
}
static void flac_metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data)
{
//サンプルとほぼ同じなので省略
}

static void flac_error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data)
{
//サンプルと同じなので省略
}
void flac_seek(struct sound_flac *t, uint64_t pos)
{
//サンプルと同じなので省略
}
void flac_decode(struct sound_flac *t, uint8_t *buf, int len)
{
	if(t->src.len != 0){
		int l = t->src.len;
		if(l > len){
			l = len;
		}
		memcpy(buf, t->src.buf + t->src.pos, l);
		buf += l;
		t->src.pos += l;
		t->src.len -= l;
		len -= l;
	}
	//printf("dest.len:%d, src.len:%d\n", t->dest.len, t->src.len); fflush(stdout);
	if(len == 0){
		return;
	}
	t->dest.buf = buf;
	t->dest.len = len;
	assert(len % 4 == 0);
	while(t->dest.len != 0){
		FLAC__bool r = FLAC__stream_decoder_process_single(t->flac_decoder);
		assert(r == true);
	}
}
static FLAC__StreamDecoderWriteStatus 
flac_write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data)
{
	assert(frame->header.channels == 2);
	assert(buffer[0] != NULL);
	assert(buffer[1] != NULL);

	if(frame->header.number.sample_number == 0) {
	}
	/* write decoded PCM samples */
	struct sound_flac *t = client_data;
	if(t->dest.buf == NULL){
		return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
	}
	//printf("dest.len %d\n", t->dest.len); fflush(stdout);
	size_t i = 0;
	assert(t->dest.buf != NULL);
	assert(t->dest.len % 4 == 0);
	for(; t->dest.len != 0 && i < frame->header.blocksize; i++) {
		int16_t v = buffer[0][i];
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		v >>= 8;
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		
		v = buffer[1][i];
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		v >>= 8;
		*(t->dest.buf) = v & 0xff;
		t->dest.buf += 1;
		t->dest.len -= 4;
	}
	uint8_t *src = t->src.buf;
	t->src.pos = 0;
	for(; i < frame->header.blocksize; i++) {
		int16_t v = buffer[0][i];
		*src++ = v & 0xff;
		v >>= 8;
		*src++ = v & 0xff;

		v = buffer[1][i];
		*src++ = v & 0xff;
		v >>= 8;
		*src++ = v & 0xff;
		t->src.len += 4;
		assert(t->src.len <= SRC_BUF_SIZE);
	}
	//printf("dest.len:%d, src.len:%d\n", t->dest.len, t->src.len); fflush(stdout);

	return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}

sound_sdl.c

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <SDL_audio.h>
#include "sound_flac.h"

static void sdl_stream_cb(void *user, Uint8 *stream, int len);
struct sound_flac;
struct sound_sdl{
	SDL_AudioDeviceID stream;
	struct sound_flac *sf;
} *sound_sdl_init(struct sound_flac *sf)
{
	static struct sound_sdl t;
	SDL_AudioSpec w, h;
	w.freq = 44100;
	w.format = AUDIO_S16LSB;
	w.channels = 2;
	w.samples = 2352;
	w.callback = sdl_stream_cb;
	w.userdata = &t;
	t.stream = SDL_OpenAudioDevice(NULL, 0, &w, &h, 0);
	t.sf = sf;
	if(t.stream != 0){
		puts(SDL_GetError());
	}
	assert(t.stream != 0);
	assert(w.format == h.format);
	SDL_PauseAudioDevice(t.stream, 0);
	return &t;
}
void sound_sdl_delete(struct sound_sdl *t)
{
	SDL_CloseAudioDevice(t->stream);
}
static void sdl_stream_cb(void *user, Uint8 *stream, int len)
{
	struct sound_sdl *t = user;
	flac_decode(t->sf, stream, len);
}

#ifdef MAIN
#include <SDL.h>
#undef main
int main(int c, const char **v)
{
	if(c < 2){
		printf("%s [flacfile]\n", v[0]);
		return 1;
	}
	SDL_Init(SDL_INIT_AUDIO);
	struct sound_flac *sf = flac_init();
	flac_file_load(sf, v[1]);
	flac_seek(sf, 142812264 / 2 + 142812264 / 4);
	struct sound_sdl *sd = sound_sdl_init(sf);
	SDL_Delay(100*1000);
	sound_sdl_delete(sd);
	flac_delete(sf);
	SDL_Quit();
	return 0;
}
#endif

ファイルの読み込み手順に困りましたがこれだけで flac ファイルの音が再生できました. 絵や操作性はまったくありませんが目的としてはこれで十分です. あと定数の2352はなんとなくつけてるので別の値でもかまいません. (2352 samples であるので、2532 * 4 bytes -> 4 cd frames です)

libFLAC の軽量化

ここからは arm の mcu に ROM として入れるために libflac で使う関数は sound_flac.c で使っている物だけで encoder はいらないということにします.

今回の目的で libFLAC で本当に必要なソースファイルは下記だけでした.

libFLAC_sources = \
	bitmath.c bitreader.c crc.c fixed.c \
	lpc.c memory.c format.c stream_decoder.c

cpu.c, md5.c はそのままだといりますので #ifdef/#ifndef で使わないようにしました.
cpu.c は intel/AMD 向けの i686 か x64 かというものなので省略, md5.c は正当性チェックのようでテスト環境でエラーを出せばいいので省略. crc.c は演算に時間がかからなそうだと思って入れました.

次に printf 系/ malloc 系/ free を排除できるかやってみます. 自分が作った flac_init() をみればわかるんですが struct メンバの隠蔽化のために malloc 系をつかっている箇所がありましたのでこれを static 化します.

bitreader.c

FLAC__BitReader *FLAC__bitreader_new(void)
{
#ifndef SHRINK
	FLAC__BitReader *br = calloc(1, sizeof(FLAC__BitReader));
	/* calloc() implies:
		memset(br, 0, sizeof(FLAC__BitReader));
		br->buffer = 0;
		br->capacity = 0;
		br->words = br->bytes = 0;
		br->consumed_words = br->consumed_bits = 0;
		br->read_callback = 0;
		br->client_data = 0;
	*/
	return br;
#else
	static FLAC__BitReader br;
	memset(&br, 0, sizeof(br));
	return &br;
#endif
}

stream_decoder.c

FLAC_API FLAC__StreamDecoder *FLAC__stream_decoder_new(void)
{
	FLAC__StreamDecoder *decoder;
	uint32_t i;

	FLAC__ASSERT(sizeof(int) >= 4); /* we want to die right away if this is not true */
#ifndef SHRINK
	decoder = calloc(1, sizeof(FLAC__StreamDecoder));
	if(decoder == 0) {
		return 0;
	}

	decoder->protected_ = calloc(1, sizeof(FLAC__StreamDecoderProtected));
	if(decoder->protected_ == 0) {
		free(decoder);
		return 0;
	}

	decoder->private_ = calloc(1, sizeof(FLAC__StreamDecoderPrivate));
	if(decoder->private_ == 0) {
		free(decoder->protected_);
		free(decoder);
		return 0;
	}

	decoder->private_->input = FLAC__bitreader_new();
	if(decoder->private_->input == 0) {
		free(decoder->private_);
		free(decoder->protected_);
		free(decoder);
		return 0;
	}
#else
#define STATIC_CALLOC(TYPE,NAME) static TYPE NAME; memset(&NAME, 0, sizeof(NAME))
	STATIC_CALLOC(FLAC__StreamDecoder, dd);
	decoder = &dd;
	STATIC_CALLOC(FLAC__StreamDecoderProtected, dp);
	decoder->protected_ = &dp;
	STATIC_CALLOC(FLAC__StreamDecoderPrivate, dv);
	decoder->private_ = &dv;
	decoder->private_->input = FLAC__bitreader_new();
#ifdef WIN32
	printf("dd %u, dp %u, dv %u dv.rice %u\n", sizeof(dd), sizeof(dp), sizeof(dv), sizeof(dv.partitioned_rice_contents));
	printf("dv.frame %u dv.frame.header %u dv.frame.subframes %u dv.frame.footer %u \n", sizeof(dv.frame), sizeof(dv.frame.header), sizeof(dv.frame.subframes), sizeof(dv.frame.footer));
	fflush(stdout);
#endif
#endif

これでは malloc を使わないので複数の struct が管理できませんが、複数を使うことはないと判断しております. これの利点は free() がいらなくなります.
ほかの細かい malloc/calloc/free は簡単に判断できなさそうなので後でやることにしました.

arm 向けの ROM としてリンクする

必要な関数だけを書いてビルドします. libflac を作る過程で標準ライブラリのヘッダが間違っているとか、関数の引数と呼び出しで int * と int32_t * が一致しないとかありましたが全て浅い問題ですぐに直せます.

diff --git a/flac-1.3.3/src/libFLAC/include/private/memory.h b/flac-1.3.3/src/libFLAC/include/private/memory.h
index a6d3faf..2298a81 100644
--- a/flac-1.3.3/src/libFLAC/include/private/memory.h
+++ b/flac-1.3.3/src/libFLAC/include/private/memory.h
@@ -37,7 +37,8 @@
 #include <config.h>
 #endif

-#include <stdlib.h> /* for size_t */
+//#include <stdlib.h> /* for size_t */
+#include <stddef.h>

main.c

static void flac_call(void)
{
	FLAC__stream_decoder_new();
	FLAC__stream_decoder_init_stream(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
	FLAC__stream_decoder_process_until_end_of_metadata(NULL);
	FLAC__stream_decoder_delete(NULL);
	FLAC__stream_decoder_seek_absolute(NULL, 0);
	FLAC__stream_decoder_process_single(NULL);
}
int main(void)
{
	//関係のないものは略
	flac_call();
	while(1) __WFI();
}

いまのところ動かせませんのでリンカが出す map file をみて .text (ROM), .rodata(arm では ROM), .data(RAM), .bss(RAM) の消費量をみます. .map ファイルを整形した物がこれです.

0x43f4 .text                    libflac.a(stream_decoder.o)
0x356c .text                    libflac.a(lpc.o)
0x1158 .text                    libflac.a(bitreader.o)
0x0bc0 .text                    libflac.a(format.o)
0x0a4c .text                    libflac.a(fixed.o)
0x09a4 .text.qsort              libg.a(lib_a-qsort.o)
0x07d4 .text._malloc_r          libg.a(lib_a-mallocr.o)
0x1100 .rodata                  libflac.a(crc.o)
0x063c .rodata.log.str1.4       libm.a(lib_a-w_log.o)
0x802c .bss                     libflac.a(bitreader.o)
0x1838 .bss                     libflac.a(stream_decoder.o)
0x0428 .data.impure_data        libg.a(lib_a-impure.o)
0x0408 .data.__malloc_av_       libg.a(lib_a-mallocr.o)
0x0028 _malloc_current_mallinfo libg.a(lib_a-mallocr.o)

libFLAC/bitreader.o

これは丁寧なコメントがあります. また malloc 排除の過程で定数からマクロに変えました.

/*
 * This should be at least twice as large as the largest number of words
 * required to represent any 'number' (in any encoding) you are going to
 * read.  With FLAC this is on the order of maybe a few hundred bits.
 * If the buffer is smaller than that, the decoder won't be able to read
 * in a whole number that is in a variable length encoding (e.g. Rice).
 * But to be practical it should be at least 1K bytes.
 *
 * Increase this number to decrease the number of read callbacks, at the
 * expense of using more memory.  Or decrease for the reverse effect,
 * keeping in mind the limit from the first paragraph.  The optimal size
 * also depends on the CPU cache size and other factors; some twiddling
 * may be necessary to squeeze out the best performance.
 */
#ifndef SHRINK
static const uint32_t FLAC__BITREADER_DEFAULT_CAPACITY = 
	65536u / FLAC__BITS_PER_WORD; /* in words */
#else
#define FLAC__BITREADER_DEFAULT_CAPACITY (0x2000)
#endif

FLAC__bool FLAC__bitreader_init(FLAC__BitReader *br, FLAC__BitReaderReadCallback rcb, void *cd)
{
	FLAC__ASSERT(0 != br);

	br->words = br->bytes = 0;
	br->consumed_words = br->consumed_bits = 0;
	br->capacity = FLAC__BITREADER_DEFAULT_CAPACITY;
#ifdef SHRINK
	static brword buf[FLAC__BITREADER_DEFAULT_CAPACITY];
	br->buffer = buf;
#else
	br->buffer = malloc(sizeof(brword) * br->capacity);
#endif
	if(br->buffer == 0)
		return false;
	br->read_callback = rcb;
	br->client_data = cd;

	return true;
}

0x2000 を 0x0400 に変えると 0x1000 bytes -> 4K bytes の消費になります.
この値をかえても PC で動かしてもいまのところ差はわかりません.

libFLAC/stream_decoder.c

libflac.a からの .text は当然ですが 0x1838 .bss は使いすぎなのでこれを調査しました. 原因はすでに stream_decoder.c に追加した printf の部分で FLAC__StreamDecoderPrivate\.(last)?frame\.subframes が原因でした.

libFLAC のライブラリを利用する側の include/FLAC/format.h に記述がありました.

(中略)
#ifndef SHRINK
#define FLAC__MAX_CHANNELS (8u)
#else
#define FLAC__MAX_CHANNELS (2u) //追記分
#endif
(中略)
typedef struct {
	FLAC__FrameHeader header;
	FLAC__Subframe subframes[FLAC__MAX_CHANNELS];
	FLAC__FrameFooter footer;
} FLAC__Frame;
(以下略)

FLAC__MAX_CHANNELS は私の目的では 2 つ必要で6個は未使用ですので削ります.*1それと lastframe は seek に必要と書いてあるのですが、削ってしまいました. こうすると消費量は大体 2/16 になります.

flac 側もsubframesの定義はポインタに変えてもらって動的確保していただくと静的化の記述も楽で助かります...

対応後

0x43b4 .text                    libflac.a(stream_decoder.o)
0x356c .text                    libflac.a(lpc.o)
0x1158 .text                    libflac.a(bitreader.o)
0x0bc0 .text                    libflac.a(format.o)
0x0a4c .text                    libflac.a(fixed.o)
0x09a4 .text.qsort              libg.a(lib_a-qsort.o)
0x07d4 .text._malloc_r          libg.a(lib_a-mallocr.o)
0x1100 .rodata                  libflac.a(crc.o)
0x063c .rodata.log.str1.4       libm.a(lib_a-w_log.o)
0x102c .bss                     libflac.a(bitreader.o)
0x0738 .bss                     libflac.a(stream_decoder.o)
0x0428 .data.impure_data        libg.a(lib_a-impure.o)
0x0408 .data.__malloc_av_       libg.a(lib_a-mallocr.o)
0x0028 _malloc_current_mallinfo libg.a(lib_a-mallocr.o)

ここまでやりますとメモリ確保はいけそうな気がしてきました. 動かすまではわからないのが処理速度で、これでいけると楽しくなる気がします.

何で flac を使おうかと決めたのは可逆圧縮は最優先ではなくて metadata として cuesheet, picture, tag が1つのファイルで管理できるのが魅力的だなと思ってます.

*1:ただし、これ前提で encoder を使うと標準規格からそれていきそうで危険だと思います

試作の検討

これは趣味として試作をするものであるので売り物を作る訳ではありません.

前作の性能の限界とその対策

  • MCU block が FPGA の内部ロジック使用量の半分を超えている
  • FPGA のパッケージが大きい,単価も少し高い
  • ソフトウェア部がPC用,6502用,MCU用の3つに分かれており管理の負担が多い

MCU の選定

以前からSTM32F シリーズの選定をしていましたが、最近の mruby 系などのスクリプト管理をできることやメモリ容量や価格などを考えて旧 Atmel の SAMD20 / 21 series を使うことにしました.

SAMD20 は SAMD21 から USB 機能がない分単価が50円ほどやすく200円ぐらいで手に入るようです. またSAMD21 の評価ボードは豊富にあります. デバッグ機能として SAMD21 を利用するのは便利だと思いました.

mruby/c とメモリ容量の関係ですが、前回の記述で動かしてみたところ動的に管理する RAM 領域が 0x4000 bytes は必要だとわかりました. また先述の3つに分かれているソフトウェア部を統合しますと 0x40000 bytes の ROM に .text や .rodata を割り当てられるというのは魅力です.

mruby/c のドキュメント量の少なさは作っている過程で非常に嫌な気持ちになっていたのですが、サンプル用の映像を出してみたら C で全部書くこととどっちが嫌かと考えた場合に C のほうが嫌ということになってしまいました.

FPGA 周辺の IO 端子の数

以前から ICE40UP を利用するか考えていたので、IO 端子の数を減らすことを考えています. 全部のバスを(レベルシフタ経由で)直接つなぐと IO の数を食ってしまっていて、基板設計でも負担が大きかったです.

今のところは CPU のバスは 16 bits 用意し、 read or write strobe が来たところで入力元を切り替える方針にしています. video のバスはパラレル->シリアルのシフトレジスタを利用してますが、data 1 本では時間が足りないので 2 本にしています.

それ以外にも multitap 内蔵のためのコネクタ用のポート、MCU との通信、mmc、 serial SRAM あたりはすべて SPI バスを利用するためにシフトレジスタを多用することになっています. analog 映像出力もシフトレジスタを使いたいのですが時間が足りないようなので data 6 本を使うパラレルのレジスタを使うかもしれません.

その他更新頻度が低いとか(FPGAの精度と比較して)厳密なタイミングを求めないIO端子は MCU にもやってもらうことにしました.

analog 映像出力は SPI x4, digital 映像出力なしでの今のところの必要な IO 端子の数は 69 本です. 前回の設計では IO 端子が 130 本ぐらいあったので半分近くは減ってるでしょうか. とりあえずこれであれば小さめの QFP144 に収まりそうです.

mruby/c をリンクする

mruby/cの呼び出し

サンプルのコードをテキトーに張りました.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "mrubyc.h"

int hal_write(int fd, const void *buf, int nbytes)
{
	return 1;
}
int hal_flush(int fd)
{
	return 1;
}


static const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
bin[] = {
0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x36,0x7a,0xa8,0x00,0x00,0x00,0x77,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0x59,0x30,0x30,
0x30,0x32,0x00,0x00,0x00,0xae,0x00,0x01,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x1f,
0x21,0x00,0x14,0x10,0x01,0x4f,0x02,0x00,0x2e,0x01,0x00,0x01,0x10,0x01,0x07,0x02,
0x2e,0x01,0x01,0x01,0x11,0x01,0x22,0x01,0x00,0x03,0x0f,0x01,0x37,0x01,0x67,0x00,
0x00,0x00,0x01,0x00,0x00,0x06,0x73,0x61,0x6d,0x70,0x6c,0x65,0x00,0x00,0x00,0x02,
0x00,0x04,0x70,0x75,0x74,0x73,0x00,0x00,0x05,0x73,0x6c,0x65,0x65,0x70,0x00,0x45,
0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};

#define MEMORY_SIZE (0x400)
static uint8_t memory_pool[MEMORY_SIZE];

void mbrc_test(void)
{
	mrbc_init(memory_pool, MEMORY_SIZE);
	if(mrbc_create_task(bin, 0) != NULL){
		mrbc_run();
	}
}

mrubyc.h の作成

src/mrubyc.h は存在しますが、ライブラリ内部処理の全部のヘッダを読むみたいでした. それはユーザー側が手を出す必要のない定義が大量に含まれていたり、前回の src/hal の問題がありまして厄介でした.

API の記述はアーカイブになかったので不明で、上記の呼び出し側で使う関数が記述されていた src/rrt0.h を編集し、 include/mrubyc.h を別途作成しました.

#ifndef MRBC_INCLUDE_MRUBYC_H_
#define MRBC_INCLUDE_MRUBYC_H_

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>

struct RTcb;
struct RMutex;
#define MRBC_MUTEX_INITIALIZER { 0 }

void mrbc_tick(void);
void mrbc_init(uint8_t *ptr, unsigned int size);
void mrbc_init_tcb(struct RTcb *tcb);
struct RTcb *mrbc_create_task(const uint8_t *vm_code, struct RTcb *tcb);
int mrbc_start_task(struct RTcb *tcb);
int mrbc_run(void);
void mrbc_sleep_ms(struct RTcb *tcb, uint32_t ms);
void mrbc_relinquish(struct RTcb *tcb);
void mrbc_change_priority(struct RTcb *tcb, int priority);
void mrbc_suspend_task(struct RTcb *tcb);
void mrbc_resume_task(struct RTcb *tcb);
struct RMutex *mrbc_mutex_init(struct RMutex *mutex);
int mrbc_mutex_lock(struct RMutex *mutex, struct RTcb *tcb);
int mrbc_mutex_unlock(struct RMutex *mutex, struct RTcb *tcb);
int mrbc_mutex_trylock(struct RMutex *mutex, struct RTcb *tcb);

#ifdef __cplusplus
}
#endif
#endif // ifndef MRBC_INCLUDE_MRUBYC_H_

struct RTcb; struct RMutex; の中身はここでは未定義とし、ポインタだけ使うようにしました. 必要になるのでしたら別途編集したほうがいいです.

このような隠蔽化されたヘッダがなくやユーザーが必要な API が記述されてないのは、実用するかの評価ではかなり悪いです.

リンク

STM32F070RB での RAM 使用量は下記でした. libmrubyc.a は -Os のビルドでやってます. ライブラリだけで .text (instruction ROM) を 88% も食ってたらユーザー側のソフトをいれる余裕がなさそうです.

  • .text: 0x1c374 / 0x20000 bytes, 88%
  • .data: 0x1de8 / 0x4000 bytes, 47%

リンカが出したメモリ配置データを成形し、使用容量順に並べたのが下記です.

0x3468 .text                    libmrubyc.a(vm.o)
0x277c .text._svfprintf_r       libc.a(lib_a-svfprintf.o)
0x1dd4 .text                    libmrubyc.a(c_string.o)
0x1a30 .text                    libmrubyc.a(class.o)
0x16b4 .text._dtoa_r            libc.a(lib_a-dtoa.o)
0x1684 .text._strtod_l          libc.a(lib_a-strtod.o)
0x1528 .text                    libmrubyc.a(c_array.o)
0x1458 .text._vfiprintf_r       libc.a(lib_a-vfiprintf.o)
0x0c08 .text                    libmrubyc.a(c_hash.o)
0x0bc0 .text                    libmrubyc.a(rrt0.o)
0x09a4 .text.qsort              libc.a(lib_a-qsort.o)
0x08dc .text                    libmrubyc.a(console.o)
0x0858 .text                    libmrubyc.a(alloc.o)
0x0838 .text.__gethex           libc.a(lib_a-gdtoa-gethex.o)
0x07f0 .text                    libmrubyc.a(c_numeric.o)
0x07d4 .text._malloc_r          libc.a(lib_a-mallocr.o)
0x04dc .text._realloc_r         libc.a(lib_a-reallocr.o)
0x04cc .text.__sfvwrite_r       libc.a(lib_a-fvwrite.o)
0x04b4 .text                    libmrubyc.a(c_range.o)
0x04ac .text                    libmrubyc.a(keyvalue.o)
0x049c .text                    libgcc.a(_arm_muldivdf3.o)
0x0458 .text                    libmrubyc.a(symbol.o)
0x0454 .text                    libmrubyc.a(load.o)
0x0424 .text                    libmrubyc.a(value.o)
0x0424 .text                    libgcc.a(_arm_addsubdf3.o)
0x02e0 .text._free_r            libc.a(lib_a-freer.o)
0x0260 .text.__hexnan           libc.a(lib_a-gdtoa-hexnan.o)
0x0250 .text.__sflush_r         libc.a(lib_a-fflush.o)
0x0224 .text                    libc.a(lib_a-strcmp.o)
(以下略)

libmrubyc.a は後にして libc.a からprintf 系と dtoa, strtod あたりを削っていけば text の削減ができそうです. 軽く見た感じ printf は mrubyc でも軽量化した物を用意してるはずですから.

軽量化

ソースを見たところ float 関連で snprintf(), assert で printf() を呼んでいるのが問題の原因でした. float は src/vm_config.h で切り離せますし、assert は自作して軽量化した printf() を呼べばいいです.
今回は assert のための文字列領域も削ったのでコンパイルオプションで -DNDEBUG をつけました. .text 使用順は下記となりました.

0x2e98 .text                    libmrubyc.a(vm.o)
0x1ce4 .text                    libmrubyc.a(c_string.o)
0x18fc .text                    libmrubyc.a(class.o)
0x1504 .text                    libmrubyc.a(c_array.o)
0x0bd0 .text                    libmrubyc.a(c_hash.o)
0x0a28 .text                    libmrubyc.a(rrt0.o)
0x09a4 .text.qsort              libc.a(lib_a-qsort.o)
0x07a4 .text                    libmrubyc.a(console.o)

軽量化の後の使用領域は下記となり、メモリ資源の観点からは実用できそうです.

  • .text: 0x08980/0x20000 bytes, 27%
  • .data: 0x1808/0x4000 bytes, 37%

その他 RAM 使用量をみていたら .impure_data が 0x400 bytes 程度消費していました. 逆アセンブルしてみたところ atol() から呼ばれる strtol() がそれをなぜか使用しているみたいでした. 私が使用している arm gcc toolchain 限定でしょうけど、試しに atol() の呼び出しを消してみたら .impure_data も消えました. atol() は自作した方がいいかもしれません.

mruby/c をコンパイルする

設計を考えていて MCU を決める事が最優先事項なので、mruby/c を導入してみました. 2週間ぐらい前に試したらリンクさえ通らなかったのですが、手順をやり直しました.

目的

MCU 側のクロスコンパイラで mruby/c をリンク、簡単な動作をみて実用に値するかが目的です. 実用できないと判断した場合は MCUスクリプトシステムを選定し直します.

スクリプトシステムを使うのは MCU 経由で FPGA で自作する予定の簡単な映像機能にたいして、画面描画制御とユーザーインタフェース管理をやりたいためです. C やアセンブラで作るには向いてない用途で、便利な選択肢があるならそれを使って開発時間を節約すべきだと考えております.

ダウンロード

https://github.com/mrubyc/mrubyc/releases/tag/release2.0

今回は release したものを使いました. 前回は git で最新版を clone して意味がわからなくなってしまって諦めてしまいました.

とりあえずコンパイル

src/Makefile の CFLAGS に -DMRBC_NO_TIMER を追加します. その後 msys2 で cd mrubyc-release2.0; make であっさり通りました. これは msys2 のパソコン上でのビルドで MCU のためのクロスコンパイルの記述は別途必要です.

シミュレーション用とターゲット用で分ける

mruby 系は組み込み向けのため、調整して組み込めという要素が強いみたいですので主に Makefile を手直しします.

シミュレーション用は msys2, ターゲット用は STM32F series を想定していきます. いきなりターゲット用にコードを書いても制限が多い MCU 上では問題の切り分けに手間が多いので、ある程度はシミュレーション用として PC で作っておく方が便利なためです.

変更分

いか diff の結果を切り貼りしてコメントをつけていきます.

diff -ru /e/mrubyc-release2.0/Makefile ././Makefile
--- /e/mrubyc-release2.0/Makefile	2019-06-19 19:14:00.000000000 +0900
+++ ././Makefile	2020-03-16 09:44:51.850266900 +0900
@@ -11,16 +11,16 @@
 
 
 mrubyc_lib:
-	cd mrblib ; $(MAKE) all
-	cd src ; $(MAKE) all
+	$(MAKE) -C mrblib all
+	$(MAKE) -C src all
 
 mrubyc_bin:
-	cd sample_c ; $(MAKE) all
+	$(MAKE) -C sample_c all
 
 clean:
-	cd mrblib ; $(MAKE) clean
-	cd src ; $(MAKE) clean
-	cd sample_c ; $(MAKE) clean
+	$(MAKE) -C mrblib clean
+	$(MAKE) -C src clean
+	$(MAKE) -C sample_c clean
 
 package: clean
 	@LANG=C ;\

ここは直さなくてもよかったんですが Makefile 作った人が make の -C を知らなかった気がします.自分も最近まで知りませんでした.

--- /e/mrubyc-release2.0/sample_c/Makefile	2019-06-19 19:14:00.000000000 +0900
+++ ././sample_c/Makefile	2020-03-16 10:05:38.938544900 +0900
@@ -7,10 +7,12 @@
 #  This file is distributed under BSD 3-Clause License.
 #
 
+CC = clang
 TARGETS = mrubyc mrubyc_sample mrubyc_concurrent mrubyc_myclass
-CFLAGS += -g -I ../src -Wall -Wpointer-arith
+CFLAGS = -I../src -I../src/hal_posix -Werror -Wall -Wpointer-arith
+CFLAGS += -O2
 LDFLAGS +=
-LIBMRUBYC = ../src/libmrubyc.a
+LIBMRUBYC = ../obj_msys/libmrubyc.a
 
 all: $(TARGETS)
 
@@ -27,4 +29,4 @@
 	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ main_myclass.c $(LIBMRUBYC)
 
 clean:
-	@rm -rf $(TARGETS) *.o *.dSYM *~
+	@rm -rf $(TARGETS) *.o *.dSYM

sample_c は libmrubyc をリンクして C の実行ファイルを作る目的のディレクトリです. -g は gdb はいまのところはいらないので削除, -O../src/hal_posix は src/hal の分離で追加. -O2 は好みです.
rm *~ は emacs とかのバックアップファイルだと思いますが私は使わないので消しました.

diff -ru /e/mrubyc-release2.0/src/alloc.c ././src/alloc.c
--- /e/mrubyc-release2.0/src/alloc.c	2019-06-19 19:14:00.000000000 +0900
+++ ././src/alloc.c	2020-03-16 09:57:28.267718100 +0900
@@ -19,7 +19,7 @@
 #include <assert.h>
 #include "vm.h"
 #include "alloc.h"
-#include "hal/hal.h"
+#include "hal.h"
 
 
 // Layer 1st(f) and 2nd(s) model
diff -ru /e/mrubyc-release2.0/src/class.c ././src/class.c
--- /e/mrubyc-release2.0/src/class.c	2019-06-19 19:14:00.000000000 +0900
+++ ././src/class.c	2020-03-16 06:54:07.594869800 +0900
@@ -274,6 +274,7 @@
   // error.
   // raise TypeError.
   assert( !"TypeError" );
+  return 0;
 }
 
 
diff -ru /e/mrubyc-release2.0/src/console.h ././src/console.h
--- /e/mrubyc-release2.0/src/console.h	2019-06-19 19:14:00.000000000 +0900
+++ ././src/console.h	2020-03-16 09:59:04.210404700 +0900
@@ -17,7 +17,7 @@
 #include <stdint.h>
 #include <stdarg.h>
 #include <string.h>
-#include "hal/hal.h"
+#include "hal.h"
 
 #ifdef __cplusplus
 extern "C" {

src 上の hal (hardware abstract layer)はファイルコピーで作るみたいですが、2つの環境向けに1つのファイルでやる場合は邪魔でしたのでディレクトリ指定を消しました.

return 0; は -Werror -Wall で停まる対処です. assert で停まるならいいんでしょうが、 warning は出さない方がいいので追加しました.

diff -ru /e/mrubyc-release2.0/src/hal_posix/hal.h ././src/hal_posix/hal.h
--- /e/mrubyc-release2.0/src/hal_posix/hal.h	2019-06-19 19:14:00.000000000 +0900
+++ ././src/hal_posix/hal.h	2020-03-16 09:55:03.289404300 +0900
@@ -21,7 +21,7 @@
 /***** Feature test switches ************************************************/
 /***** System headers *******************************************************/
 #include <unistd.h>
-
+ int fsync(int fd); 
 
 /***** Local headers ********************************************************/
 /***** Constant values ******************************************************/

私の msys2 が悪いのか fsync() の定義が unistd.h にないみたいでして、その場しのぎに定義をはっつけときました. あとで悪影響があるかもしれませんが、リンクは通りました.

diff -ru /e/mrubyc-release2.0/src/vm_config.h ././src/vm_config.h
--- /e/mrubyc-release2.0/src/vm_config.h	2019-06-19 19:14:00.000000000 +0900
+++ ././src/vm_config.h	2020-03-16 09:37:43.281886700 +0900
@@ -75,9 +75,5 @@
 // #define MRBC_REQUIRE_32BIT_ALIGNMENT
 
 // Debug code.
-#if !defined(MRBC_DEBUG)
-#define MRBC_DEBUG
-#endif
-
 
 #endif

MRBC_DEBUG をつけると arm-xx-gcc で pointer の cast で文句をいってました. MRBC_DEBUG を強制する記述は害なので消しました.

追加分

src/mrubyc.mak

#
# mruby/c  src/Makefile
#
# Copyright (C) 2015-2019 Kyushu Institute of Technology.
# Copyright (C) 2015-2019 Shimane IT Open-Innovation Center.
#
#  This file is distributed under BSD 3-Clause License.
#

.PHONY: all clean
CFLAGS = $(_CFLAGS) -Wall -Werror -Wpointer-arith -std=c99
CFLAGS += #-pedantic -pedantic-errors
CFLAGS += -I$(HALDIR) -DMRBC_NO_TIMER

COMMON_SRCS = alloc.c class.c console.c global.c keyvalue.c load.c rrt0.c static.c symbol.c value.c vm.c $(HAL_C)
RUBY_LIB_SRCS = c_array.c c_hash.c c_numeric.c c_math.c c_range.c c_string.c mrblib.c

TARGET = $(OBJDIR)/libmrubyc.a
OBJS = $(addprefix $(OBJDIR)/, $(COMMON_SRCS:.c=.o) $(RUBY_LIB_SRCS:.c=.o))

all: $(OBJDIR)/$(HALDIR) $(TARGET)
$(OBJDIR)/$(HALDIR):
	mkdir -p $@
$(TARGET): $(OBJS)
	$(AR) $(ARFLAGS) $@ $?
clean:
	@rm -Rf $(TARGET) $(OBJS)

$(OBJDIR)/vm.o:
	$(CC) $(CFLAGS) -Wno-unused-variable $(CPPFLAGS) -c -o $@ $<
$(OBJDIR)/%.o: %.c
	$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
include mrubyc.d

オブジェクト出力ディレクトリとhalディレクトリの分離. 依存関係ファイルは長いので分離. -DMRBC_NO_TIMER はとりあえずでコンパイルを通すのに必要でした.

Makefile

ARMGCC_PREFIX = /d/dev/arm-gcc/bin/arm-none-eabi-
all:
	$(MAKE) -f mrubyc.mak CC=clang _CFLAGS="-O2 -DMRBC_DEBUG" OBJDIR=../obj_msys HALDIR=hal_posix HAL_C=hal_posix/hal.c
	$(MAKE) -f mrubyc.mak CC=$(ARMGCC_PREFIX)gcc.exe AR=$(ARMGCC_PREFIX)ar.exe _CFLAGS=-Os OBJDIR=../obj_arm HALDIR=hal_arm
clean:
	$(MAKE) -f mrubyc.mak OBJDIR=../obj_msys HAL_C=hal_posix/hal.c clean
	$(MAKE) -f mrubyc.mak OBJDIR=../obj_arm clean

こういうライブラリで MCU 用に CFLAGS に -g と -DMRBC_DEBUG をつけるのはよく考える必要がありますし、ユーザーレイヤのデバッグはシミュレーション上でやるのでメモリの節約の観点から MCU 向けには消しました.

mrubyc.d はファイル依存関係を抜き出したものです. xxx.o: を $(OBJDIR)/xxx.o にしただけなので省略します.

Squirrel を久しぶりに使う

今の開発には大変疲弊しており気分転換に別の事をやることにしました. 割と重要なのが自作パッチ当てツールです.これは15年ぐらい前にテキトーに作ったものを使っていて、気に入らない部分がたくさんあるけど致命的な理由がなくなんとかなっていたのですが、今回作り直すことにしました.

Squirrel は10年ぐらい前にあった Lua ブームのなか、わたしは anago で採用しましてかなり強力で満足してます. また私が最後に C で組み込む側のプログラムを使ったのが5年前らしいです. 公式の website は 2016年3月以降は更新がないみたいでちょっと寂しく、別の組み込み型スクリプト言語も探している途中です. 将来の開発のためにも mruby 系が気になるのですが調査に時間がかかりそうなので手っ取り早く Squirrel でやりたいことを作りました.

Squirrel Standard Libraries ではテキストファイルが簡単に読み込めない

今回の目的はアセンブラで生成される Motorola S record を読み込んで ROM にパッチを当てるというものです. S record の読み込みはどの言語で作ってもいいのですが、今回は Squirrel で書くことにしました.

それで件名のテキストファイルの話になります. リファレンスを読んでいたのですが、標準ライブラリは正規表現はあるのにテキストファイル読み込みがみあたらず、回りくどくない代替の方法もなぜかありませんでした. 仕方ないので標準ライブラリをいじって fgets() をいれました....

diff -r /e/squirrel3/include/sqstdio.h ./squirrel3/include/sqstdio.h
14a15,17
>     virtual SQInteger Gets(SQChar *str, SQInteger size){
> 		return -1;
> 	}

diff -r /e/squirrel3/sqstdlib/sqstdio.cpp ./squirrel3/sqstdlib/sqstdio.cpp
41a42,47
> SQInteger sqstd_gets(SQChar *str, SQInteger size, SQFILE file)
> {
> 	char *r = fgets(str, size, (FILE *)file);
>     return r == NULL ? -1 : 1;
> }
> 
87a94,96
>     SQInteger Gets(SQChar *str, SQInteger size){
> 		return sqstd_gets(str, size,_handle);
> 	}
diff -r /e/squirrel3/sqstdlib/sqstdstream.cpp ./squirrel3/sqstdlib/sqstdstream.cpp
201c201,208
< 
---
> SQInteger _stream_gets(HSQUIRRELVM v)
> {
>     SETUP_STREAM(v);
>     char str[0x200];
>     self->Gets(str, sizeof(str));
>     sq_pushstring(v, str, -1);
>     return 1;
> }
243a251
>     _DECL_STREAM_FUNC(gets,1,_SC("xn")),

str[0x200] の文字列の長さにつきましては、私がその場しのぎに入れてることをご留意ください.

S record を読むスクリプトを作る

gets 導入後は正規表現でつまづきましたが手っ取り早く作れましたし、実行速度も速いのでいいと思います.

function mot_analyse(str)
{
	local ex = regexp(@"^S([01235789])([0-9A-Fa-f]{8,80})\n?");
	local t = {result = false, error = "", address = 0, data = []};
	if(ex.match(str) == false){
		t.error = "invalid string";
		return t;
	}
	local r = ex.capture(str);
	local mot_type = str.slice(r[1].begin, r[1].end).tointeger();
	local ar = [];
	for(local i = r[2].begin; i < r[2].end; i+=2){
		ar.push(str.slice(i, i+2).tointeger(0x10));
	}
	local sum = 0;
	foreach(s in ar){
		sum += s;
	}
	if((sum & 0xff) != 0xff){
		t.error = "checksum error";
		return t;
	}
	local data_count = ar[0];
	local address_count = 0;
	local pt = 1;
	switch(mot_type){
	case 0: case 5:
	case 1: case 9: address_count = 2; break;
	case 2: case 8: address_count = 3; break;
	case 3: case 7: address_count = 4; break;
	default: t.error = "unknown type"; return t;
	}
	for(local i = 0; i < address_count; i++){
		t.address = (t.address << 8) | ar[pt];
		pt += 1;
	}
	data_count -= address_count;
	data_count -= 1; //checksum
	t.data = ar.slice(pt, pt+data_count);
	t.result = true;
	return t;
}

正規表現は末尾の $ がなくても文字列の最後までチェックしているみたいでした. 仕方ないので \n? をいれてマッチしています. それと () の文字列を取り出す capture が他の言語とちょっと違ったので理解するのに時間がかかりました.

他の言語は文字列自体を返してくれるので r で文字列が得られることを期待していたのですが、r.begin, r[].end を利用し slice で文字列の最初から最後までを取り出せということでした....

Squirrel の将来性は...?

自分だけが開発に使い、パソコンで利用し、他人が作った C ソースやライブラリを組み込んで、スクリプトでテストしまくるという用途では最高です. わたしの将来の用途は安価なワンチップマイコンでの UI の作成に使いたいのですが C++ を使ってる時点で OS なしの低機能な環境ではたぶんだめかなと思いました. いくらか重複しますが気になる点を列挙いたします.

  • website の更新が止まっている
  • インターネットを調べても使っている人が少ないのか参考になる情報が5年前と対して変わってない
  • fgets 相当が標準になく、テキストファイル読み込みがやりづらい
  • 配列,文字列の切り出しが [begin...end] 形式で [begin, length] 形式がない
  • printf がなく print(format()) と書くのが面倒

なかなか難しいところです.