Natural Tiny Shell(NT-Shell)をArduinoに移植してみた

ntshell.jpg

前回から、Arduboyの画面にPCからデータを送って絵を表示させようと考えていますが、これには当然Arduboy側にもソフトが必要です。
以前から使ってみようと思っていた「NT-Shell」を、この機会にArduboyで動かしてみることにしました。

今回はArduboyに移植しましたが、処理はシリアルポート経由の通信とデータ処理がメインです。
従って、今回の内容は通常のArduinoでもそのまま使えると思います。(なので、記事タイトルもArduboyではなくArduinoとしました。)
Arduboy用のコードは、例によってGitHubにアップロードしてあります。

boochow/abshell: a comman shell for arduboy

NT-Shellは10KB ROM/1KB RAMといったマイコンクラスのシステムで動作するシェルです。
VT100コンパチブルですので、コマンドのパラメータを打ち間違えた際にカーソルを移動してそこだけ修正する、といったことができます。

Natural Tiny Shell (NT-Shell)

使い方は、こちらのページにあるサンプルを見れば分かります。
また、上記のリンク先からダウンロードできるファイルの中にもサンプルが入っており、これを見ればコマンドの追加方法なども分かります。

以下、簡単に今回の移植のポイントを書いておきます。
まず、上記のサンプルではmain関数が以下のようになっています。

nt main(void)
{
  ntshell_t ntshell;
  chip_init();
  uart_init();
  ntshell_init(
    &ntshell,
    func_read,
    func_write,
    func_callback,
    (void *)&ntshell);
  ntshell_set_prompt(&ntshell, "BlueTank>");
  ntshell_execute(&ntshell);
  return 0;
}

ntshell_initでコールバック関数func_read、func_write、func_callbackの3つを渡しています。
そしてntshell_executeでシェルを実行しています。

まず、Arduinoにおけるコールバック関数ですが、func_readとfunc_writeは単にシリアルポートの読み書きができれば良いので、以下のようにしました。

static int func_read(char *buf, int cnt, void *extobj) {
  if (Serial.available())
    return Serial.readBytes(buf, cnt);
  else
    return 0;
}

static int func_write(const char *buf, int cnt, void *extobj) {
  return Serial.write(buf, cnt);
}

何の芸も無いですね・・・。

func_callbackは、コマンド文字列へのポインタを受け取って処理する関数ですが、これはサンプル(lpc_monitor)に入っていた関数をそのまま使わせていただきました。

static int func_callback(const char *text, void *extobj) {
  return usrcmd_execute(text);
}

usrcmd_executeはtextで与えられた文字列を解析してコマンド名と引数の配列(Cでお馴染みのargc, **argv)を取得し、各コマンドを実行する関数へジャンプします。
Arduino用に、メッセージ用文字列をプログラム領域へ保存するように手を加えたものが、冒頭のGitHubリポジトリのusrcmd_arduino.cppに入っています。

基本的な移植は以上なのですが、このままではArduinoでは動作しません。

まず一つ目の問題は、ntshell_executeが無限ループを内包していることです。
一方、Arduinoはloop()を繰り返し呼び出す前提になっており、特にArduboyはこの構造に強く依存しています。

そこで、ntshell_execute自体は使用せず、その中身をloop()内に移植することにしました。
中身といっても、以下の最後の3行がwhile(1){}の内側に入っていたのを、whileループを取り去っただけです。この関数はntshell_arduino.cの中にあります。

そして、スケッチファイルのloop()から、このntshell_execute_arduinoを呼び出します。

void ntshell_execute_arduino(ntshell_t *p)
{
  if (p->initcode != INITCODE) {
    return;
  }

  unsigned char ch;
  SERIAL_READ(p, (char *)&ch, sizeof(ch));
  vtrecv_execute(&(p->vtrecv), &ch, sizeof(ch));
}

次の問題はSRAMの不足です。
Arduboyは2.5KBのSRAMのうち、1KBをディスプレイバッファとして使っています。
そのため、1KB以上は余裕があるはずなのですが、実際にはコンパイルしたらRAM容量不足になってしまいました。

対策として、まずヒストリ機能はあきらめました。
この機能により、コマンドを途中まで入力してtabを押すと、過去の履歴によって残りの部分が補完されます。
ヒストリ機能を使うと、ヒストリ回数×1行あたりの最大文字数 分のRAMが必要になります。
この設定はlib/core/ntconf.hに書かれています。
元の設定にあった、1行64文字はそのままとして、8回分のヒストリ機能は1回に削減しました。
せっかくある機能ですから、直前のコマンドくらいは呼び出したいということです。

そして、最大のメモリ食いは状態遷移テーブルでした。
これはlib/core/vtrecv.cで実装されています。
ただ、静的なテーブルですので、RAM上に置く必要はありません。
以下のようにして、テーブルをプログラム領域へ移しました。
テーブルの読み出し部分も、プログラム領域から読むように書き換えました。

#ifdef __AVR__
static const state_table_t table[] PROGMEM =  {
#else
static const state_table_t table[] =  {
#endif


#ifdef __AVR__
state_table_t ts;
ts.state = pgm_read_word_near(tp);
ts.code_start = pgm_read_byte_near((char *)tp + 2);
ts.code_end = pgm_read_byte_near((char *)tp + 3);
ts.state_change = pgm_read_byte_near((char *)tp + 4);
if (ts.state == state) {
  if ((ts.code_start <= ch) && (ch <= ts.code_end)) {
    return ts.state_change;
  }
}
tp++;
#else
if (tp->state == state) {
  if ((tp->code_start <= ch) && (ch <= tp->code_end)) {
    return tp->state_change;
  }
}
tp++;
#endif

これで大体動くようになったので、あとは淡々とArduboyのコマンドを実装していきます。
線、矩形、円、角丸矩形、三角形、などなど一通り実装しました。
機械的に実装したので、中身は綺麗ではありません。
これらはusrcmd_arduboy.cppにあります。

とりあえず、これでターミナルソフトから

print hello world

とか

rect 20 10 50 50

とか入力すればArduboyの画面に表示できるようになりました。

ビットマップ関係だけは、仕様をちょっと考えたいので、まだ実装していません。

コメント