試作の検討

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

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

  • 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()) と書くのが面倒

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

pukiwiki のファイルアップロードを複数でもできるようにする

最近 pukiwiki を設置し、いままでの文書を移行中です. そこでファイルのアップロードが1個ずつでかったるいので書き換えました.

コードは php のサイト https://www.php.net/manual/ja/features.file-upload.multiple.php からぱくってきました.

$ diff pukiwiki-1.5.2_utf8/plugin/attach.inc.php ./attach.inc.php
78a79,94
> 
> function reArrayFiles(&$file_post)
> {
>     $file_ary = array();
>     $file_count = count($file_post['name']);
>     $file_keys = array_keys($file_post);
> 
>     for ($i=0; $i<$file_count; $i++) {
>         foreach ($file_keys as $key) {
>             $file_ary[$i][$key] = $file_post[$key][$i];
>         }
>     }
> 
>     return $file_ary;
> }
> 
112c128,138
< 		return attach_upload($_FILES['attach_file'], $refer, $pass);
---
> 		//return attach_upload($_FILES['attach_file'], $refer, $pass);
> 		$str = "";
> 		$t = 0;
> 		foreach(reArrayFiles($_FILES['attach_file']) as $file){
> 			$t = attach_upload($file, $refer, $pass);
> 			$str .= $file['name'] . ':'. $t['msg'] . " ";
> 			if($t['result'] == FALSE){
> 				break;
> 			}
> 		}
> 		return array('result' => $t['result'], 'msg' => $str);
417c443
<   <label for="_p_attach_file">{$_attach_messages['msg_file']}:</label> <input type="file" name="attach_file" id="_p_attach_file" />
---
>   <label for="_p_attach_file">{$_attach_messages['msg_file']}:</label> <input type="file" name="attach_file[]" id="_p_attach_file" multiple/>

わたしは PHP の書式をほとんど知らないので $t = 0; が必要がわかりません. ruby だと t = nil などと宣言する必要があります. ruby ではそれがないと foreach{} 内部だけの変数になってしまい、 return で t を使うとエラーになってしまいます.

今回の対応で複数のアップロードをすると 'msg' が長くなりまして、それが大きい文字サイズで結果が表示されてしまうので編集者はちょっと見苦しくなります.

しかしこれぐらい公式で対応してほしいですね.

memory base driver for 6280 3/3

同名の記事 1/3 の "メモリデータの受信" を高速化できました. 2/3 の "書き込みルーチン" はいろいろ試しましたが劇的な効果は得られなかったので前回の物が最適のようです.

コードは read_optimize が 1,2,4,8 で異なるマクロを使ってます.

read_optimize	equ	8 ;7.0 sec
;read_optimize	equ	1 ;7.6 sec
	cly
	ldx	#%10
read_byte_next:
(ここにシフトレジスタからの読み込みコードが入る)
	sta	(<mb128_destsrc),y
	iny
	bne	read_byte_next
	inc	<mb128_destsrc+1
	inc	<mb128_length+0
	bne	read_byte_next
	inc	<mb128_length+1
	bne	read_byte_next
	rts

read_optimize = 1,2,4

	lda	#1<<7
read_bit_next
 rept read_optimize
	stz	joypad
	stx	joypad
	nop
	nop
	lsr	joypad
	ror	a
 endm
	bcc	read_bit_next
  • 最初の lda #1<<7 は前回同様ループカウンタの初期化です.
  • 2度の nop はシフトレジスタの立ち上がりから 4 CSH clocks が必要なので挿入します. 3 CSH clocks も試しましたがダメでした.

今回はいろいろ考えて lsr joypad としました. lsr mem 命令の場合は下記の3つの処理をします.

  • joypad を read.
  • read した data をシフト. temp = {1'b0, temp[7:1]}, C = temp[0]; とする.
  • (いらない) シフトしたデータを write する.

3 番目の不要な write は動作不安定の原因になると思って前回は避けていたのですが読み込み処理としては, clock が 1->0 または 1->1 になることは次の処理として問題ないです. data はずっと don't care です.

lda joypad; から ror a の間に txa などを挟む必要がなくなりましてとてもすっきりました.

read_optimize == 8

i	eval	0
 rept read_optimize
	stz	joypad
	stx	joypad
  if i == 0
	nop
  else
	ror	a
  endif
	nop
	lsr	joypad
  if i == 7
	ror	a
  endif
i	eval	i + 1
 endm
  • この命令はループがなくマクロで繰り返すので lda #1<<7 が不要になりました.
  • 2度 nop を挟む場所に可能なら ror a をいれることで暇な時間を有効活用できます.

アセンブラの最適化はパズルとしてはおもしろい

これを組むのに時間かけすぎです. 業務としてはよくないです.

memory base driver for 6280 2/3

write driver は RAM の data をまるまるコピーではなく通信経路からの fifo のコピーとしたかったのですが、既存の機能を使い回すということで前者でてっとりばやく実装することにしました.

書き込みルーチン

先日の serial_send を使ったら処理時間がえらいおそかったので早い物をつくりました。

write_start:
	cly
write_byte_next:
	lda	(<mb128_destsrc),y
write_shift:
temp	eval	0
 rept 8
 if temp <> 7
	tax
 endif
	and	#1
	sta	joypad
	ora	#1<<1
	sta	joypad
 if temp <> 7
	txa
	lsr	a
 endif
temp	eval	temp+1
 endm
	iny
	bne	write_byte_next
write_pointer_update:
	inc	<mb128_destsrc+1
	inc	<mb128_length+0
	bne	write_byte_next
	inc	<mb128_length+1
	bne	write_byte_next
	rts

今回は RAM を使わず a,x,y で回すことにしましたので bit 単位のループをなくして rept でコードを繰り返してます. tax と txa がまどろっこしいので削りたいのですが削れるのは最後だけみたいです.
あとコード量が増えるので bne が届くのがギリギリになってきました.
1bit 単位でループを回すなら下記でしょうか. 2bit 単位でもそんなに悪くないかも.

write_byte_next:
	lda	(<mb128_destsrc),y
	phy
	ldy	#8/2
write_shift:
 rept 2
	tax
	and	#1
	sta	joypad
	ora	#1<<1
	sta	joypad
	txa
	lsr	a
 endm
	dey
	bne	write_shift
	ply
	iny
	bne	write_byte_next

さらなる高速化の失敗

送信したい data が 8'h00 である場合は %00 と %10 を交互に送るだけであること, address 0x1fff000-0x1fff3ff が mirror であることを利用して tai 命令を使ってみました.

write_byte_next:
	lda	(<mb128_destsrc),y
	bne	write_shift
	tai	data00,joypad,2*8
	iny
	bne	write_byte_next
	bra	write_pointer_update
write_shift:
	(snip)
data00:
	byt	%00,%10
dataff:
	byt	%01,%11

結果は memory base 側へデータが正しくかけてませんでした. *1 csl; tai; csh とすれば正しく転送できますが、手計算では先述のループより遅いみたいです.
8bit 単位の転送ではオーバーヘッドが大きいのでたくさん 0 で埋める場合には使えそうです. mirror 領域が 0x400 byte なので1つの tai 命令での最大転送量は 0x400 / 8 / 2 で 0x40 bytes なのに注意してください.

data 0xff の転送もできそうなんですが inc a; beq がやりづらくその関わりに cmp #$ff; beq を書くのがまどろこっしくて実用化は難しそうでした.

*1:hold 時間がだめなのでしょうか?

memory base driver for 6280 1/3

自前の資料を基に作りました. 対象はネイティブの IO port 経由です. 別の組み込み MCU を使う場合は設定条件をよくみて SPI 機能を使うとよいでしょう.

こういうシフトレジスタの処理だけならアセンブラで書くのはとても楽しいです.

送信ルーチン

serial_send:
send_bit_next:
	lsr	<$02
	ror	<$01
	ror	<$00
	cla
	rol	a
	sta	joypad
	ora	#1<<1
 rept 0
	nop
 endm
	sta	joypad
	dex
	bne	send_bit_next
	rts

解析したサブルーチンは最大 8 bits まで送れる作りでしたが、zero page を使って最大 24 bits送れるようにしました. また Carry flag を活用して分岐命令を減らしています.

rept 0 について. 送信に関しては nowait で送ってかまわないようです.

初期化ルーチン+簡単な受信

	ldx	#9
	lda	#$A8
	sta	<$00
	stz	<$01
	bsr	serial_send
	lda	joypad
	sta	<$00
	and	#$0f
	php
	ldx	#1
	stx	<$00
	bsr	serial_send
	plp
	bne	init_ng
	lda	joypad
	and	#$0f
	cmp	#1<<2
	bne	init_ng
	sec
	rts

既存のルーチンは 8bits 送信, 1 bit 送信+受信, 1bit 送信+受信でしたが、 9bits 送信+受信, 1bit 送信+受信にわけました. 送信後, IO port から受信するときは CLOCK 立ち上がりから出力まで遅いみたいです. この場合は dex; bne; rts と十分に時間が経過しているので待ち時間の挿入はいりません.

先日の解析でプログラムを読み間違えたので、間違いに従い 10bits の初期化コードを3度送ってしまいました. この場合はエラーがでるにもかかわらず、実際には初期化が成功していて input bit 0 の出力が memory data となるので上キーとIボタンが送信されなくなりました.

メモリデータの受信

	ldx	#1<<1
	cly
read_byte_next:
	lda	#1<<7
	sta	<$00
read_bit_next:
	stz	joypad
 rept 0
	nop
 endm
	stx	joypad
 rept 2 ;do not remove
	nop
 endm
	lda	joypad
	lsr	a
	ror	<$00
	bcc	read_bit_next
	lda	<$00
	sta	(<mb128_destsrc),y
;dest or src += 1; length -= val; length == 0
	bsr	rw_ptr_length_update
	bne	read_byte_next
	stz	joypad
	rts
  • zero page $00 を 8bits シフトレジスタにします.
  • bit7 = 1, bit6:0 = 7'h00 にすることでカウンタも兼ねます.
  • stz joypad (clock=0); stx joypad (clock=1)のあとの nop は必要でそのあと lda joypad です.
  • lda joypad; lsr a で取り込んだデータを Carry にいれ、シフトレジスタにいれます. lsr joypad だと予期しない書き込み処理も入るので lda 命令を使います.

最近の MCU でやる場合

  • 最近のシフトレジスタバイスは MSB->LSB がほとんどで LSB->MSB なのに注意が必要です.
  • メモリベースからのデータの更新条件はクロックの立ち下がりではなく立ち上がりのために立ち上がり直後に読み込むとデータが安定しません.
  • どこまで早いクロックでいけるのかを調べると楽しいと思います.

KOCD2001 MB128 driver

こちらのが前回よりプログラムがきれいです. ただしやってることはほとんど同じでした.

ec1b8:
	jsr	lc08e ;send 10x3+3 bits, check detection bits
	bcc	lc1c0
	lda	#$FF
	rts
lc1c0:
;operation = read
	ldx	#$01
	ldy	#$01
	jsr	lc070
;send address
	ldx	<$8d
	ldy	#$08
	jsr	lc070
	ldx	<$8e
	ldy	#$02
	jsr	lc070
;send length
	lda	<$8b
	pha
	tax
	lda	<$8c
	pha
	sta	<$8b
	stz	<$8c
	txa
;(snip)

;---- initialize ----
lc08e:
	stz	$1000
	ldy	#$03
lc093:
	phy
	ldx	#$A8
	ldy	#$08
	bsr	lc070
	clx
	ldy	#$01
	bsr	lc070
	nop
	lda	$1000
	and	#$0F
	pha
	ldx	#$01
	ldy	#$01
	bsr	lc070
	nop
	lda	$1000
	and	#$0F
	plx
	ply
	cpx	#$00
	bne	lc0bd
	cmp	#$04
	clc
	beq	lc0c7
lc0bd:
	dey
	bne	lc093
	clx
	ldy	#$03
	jsr	lc070
	sec
lc0c7:
	rts

;---- send 1 bit into MB128 ----
lc070:
	txa
	and	#$01
	sta	$1000
	nop
	nop
	nop
	ora	#$02
	sta	$1000
	pha
	txa
	lsr	a
	tax
	pla
	and	#$FD
	dey
	bne	lc070
	nop
	nop
	sta	$1000
	rts

ADCD3001 A.III memory base 128 driver

解析しました.
(2020年1月28日更新: コードの読み間違いが原因のactivation * 3 とその後の不明の 3 bits を削除)

;ADCD3001 A.III memory base 128 driver
;bits |send data   |meaning
;10   |0001_0101_01|activation
;1    |p           |set operation read or write
;10   |aaaa_aaaa_aa|memory address bit 16:7; address bit6:0 = 7'h00
;20   |l *20       |memory data bit length
;lengt|x *length   |(read) memory read data
;lengt|d *length   |(write) memory write data
;(legend)  0:low, 1:high, x:don't care, p:operation, a:address, l:length, d:write data
;---- main routine ----
;initialize -> send read command -> check memory signature
ed7ff:
	jsr	ld789 ;rts only (?)
	jsr	ld7b8 ;get activation code (A=0:ng, A=1:ok)
	cmp	#$00
	bne	ld81d
	lda	#$01
	jsr	ld885 ;send R/W(=A.1, 1:read, 0:write), address (10 bits), length (20 bits)
ld80e:
	jsr	ld863 ;read data
	lda	$36e5
	sta	($21)
	jsr	ld6f9
	bcc	ld80e
	cla
	clc
ld81d:
	jsr	ld78a
	jsr	ld901
	rts

;address 0x1ff000 assignments for memory base 128
;w0 serial write data
;w1 serial clock
;r3 always 0
;r2 memory base detection serial data
;r1 always 0
;r0 memory data

;I'm not sure how to reset serial clock count???
ld7b8:
	clx
ld7b9:
;   old -> new
;sd 0001_0101_01
;sr xxxx_xxxx_01
;sd:serial data (when serial clock sets 0->1, mb128 latch 0x1ff000.w0)
;sr:r2 = detection data, bit3, 1 and 0 must be 0
	lda	#$A8 ;serial data lsb->msb
	ldy	#$08 ;bitcount
	jsr	ld8d4
	cla
	jsr	ld8b7 ;send 1bit (A.0)
	lda	$1000 ;get activation reply
	and	#$0F
	tay
	lda	#$01
	jsr	ld8b7
	lda	$1000
	and	#$0F
	cmp	#$04
	bne	ld7e1
	tya
	cmp	#$00
	bne	ld7e1
;activation ok
	clc
	cla
	bra	ld7ec
ld7e1: ;retry max 3 times
	inx
	cpx	#$03
	bne	ld7b9
;error!
	jsr	ld7ed
	sec
	lda	#$01
ld7ec:
	rts
ld7ed:
;sd 000
	cla
	ldy	#$03
	jsr	ld8d4
	jsr	ld901 ;wait some clocks
	lda	#$01
	sta	$1000 
	jsr	ld901
	rts

ld885:
	jsr	ld730 ;tai $D91D,$36E1,$0004; rts
	jsr	ld8b7 ;send 1 bit; data=A.0
	jsr	ld738
	lda	$36d9
	ldy	#$08
	jsr	ld8d4 ;send 8bits
	lda	$36da
	ldy	#$02
	jsr	ld8d4 ;send 2bits
	lda	$36dd
	ldy	#$08
	jsr	ld8d4 ;send 8bits
	lda	$36de
	ldy	#$08
	jsr	ld8d4 ;send 8bits
	lda	$36df
	ldy	#$04
	jsr	ld8d4 ;send 4bits
	rts