TOPPERS/ASP開発時に初めにNT-Shellとxprintfを組み込んだら幸せになった話

#はじめに
TOPPERS/ASP　GR-PEACHで装置のテストを行う治具をつくることになりました。

- [TOPPERS/ASP　GR-PEACH](https://github.com/ncesnagoya/asp-gr_peach_gcc-mbed/tree/v1.0.0-AllinOne "TOPPERS/ASP　×　GR-PEACH")

TOPPERS/ASPのsample1アプリ確認後にライブラリ(**NT-Shell・xprintf**)を組込みました。
このライブラリは個人的に組込みソフトウェア開発で非常に便利・汎用的で有用だと感じました。
なので**NT-Shell・xprintf**をTOPPERS/ASPに組み込んだ事例を情報共有したいと考えました。
今回はTOPPERS/ASP GR-PEACHの事例ですが、TOPPERSの他のシリーズ、他のプロセッサ・マイコンのターゲットでも同様に適用できると思います。

#お礼
ライブラリを公開してくださっている作者のお二人に尊敬と感謝を。

- NT-Shell @shintamainjp さん
- xprintf ChaN　さん


#ソースコード
今回のソースコードは次にアップロードしました。

https://github.com/grace2riku/asp-gr_peach_ntshell_sample

#NT-Shell
NT-ShellはNatural Tiny Shellの略称で組込みシステム向けのシェルです。
使用したバージョンは最新版の【0.3.1】です。

- [NT-Shell](https://cubeatsystems.com/ntshell/index.html "NT-Shell")

既にWeb上にNT-Shellの解説記事が多数アップされています。
私の記事は何番煎じだ、という気がしますが、それでも素晴らしいものは素晴らしいので何度でも紹介したいです。
組込みソフトウェア開発を縁の下で支える素晴らしいソフトウェア・インフラだな、と個人的に感じています。
TOPPERSはデフォルトでシェルが実装されていません。
それはそれで何も問題はないと考えますが、開発を進めるうえでちょっとしたコマンドを実装したい、ということがあると思います。
そういったときにNT-Shellのようなシェルがあると重宝します。
今回私がNT-Shellを組込みシステム向けのシェルとして良いと思ったのは次の点です。

- 容易にTOPPERSに組み込むことができる。
- NT-Shellの構造がシンプル、美しい。
- NT-Shellが高機能。具体的には次の点。
    - 編集機能がかゆいところに手が届く(バックスペース、Deleteに対応)
    - 入力履歴機能がある。

NT-Shellの編集機能については次を参照ください。

- [NT-Shellの編集機能](https://cubeatsystems.com/ntshell/index.html "NT-Shellの編集機能")

シェルの操作性はデバッグ効率に直結すると思うので使いやすいものを選択したいですよね。
今回はNT-ShellをGR-PEACHに組込みました。
PCのシリアルコンソールからNT-Shellを経由し、UARTでGR-PEACHにデバッグコマンドを送受信します。


#xprintf
xprintfは組み込み用printfモジュールです。
使用したバージョンは2021/3/10時点の最新版です。
ちょっとした文字列処理をしたいときに重宝します。
軽量・コンパクトなモジュールなので標準入出力ライブラリをインクルードするのはサイズ的にちょっとやりたくないないなぁ・・・という場合に良いと思います。

- [xprintf](http://elm-chan.org/fsw/strf/xprintf_j.html "xprintf")


今回は次のケースでxprintfを使いました。

 - NT-Shellが受信した文字列をデバッグ用のコマンドか解析する
 - メモリリードのデバッグコマンドで指定アドレスをダンプ表示する


#環境構築
今回は次の環境で開発を行いました。

## ビルド環境の選択
ビルド環境は次のGitHubのリンク【IDE】に記載されている**TrueSTUDIO**を使うことにしました。

- [TOPPERS/ASP GR-PEACHビルド環境](https://github.com/ncesnagoya/asp-gr_peach_gcc-mbed　"TOPPERS/ASP GR-PEACHビルド環境")

こちらはv9.0.0より新しいTrueSTUDIOのバージョンは使わないこと、と記載があります。
しかしTrueSTUDIOの古いバージョンは入手不可能なため私は現状入手できた最新版【v9.3.0】を使用しました。
リンクのREADMEに記載があるようにSTM32向けのツールと明記されているので大丈夫かなぁ・・・と思いました。
結果、TrueSTUDIO　ｖ９．３．0でビルドしたバイナリがGR-PEACHでも動いたので、とりあえず大丈夫そうです。
また、TrueSTUDIOはSTマイクロにユーザ登録しないとダウンロードできませんでした。
次のリンクからTrueSTUDIOのダウンロード・ユーザ登録ができます。

- [TrueSTUDIOのダウンロードリンク](https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-ides/truestudio.html TrueSTUDIOのダウンロードリンク")

今回はWindows版のTrueSTUDIOを使いました。


## TrueSTUDIOプロジェクトの設定
TrueSTUDIOをインストール完了したら起動します。

![Select a directory as workspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/a3449a28-cb60-e618-d264-21cdcdfdb4d3.png)

ワークスペースのパスは次に設定します。

- examples¥truestudio

TrueSTUDIOが起動します。

![a atollic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/e4ea2dbc-bcd7-1a06-1521-8844bd7f31af.png)

TOPPERS/ASP・GR-PEACHのパッケージをダウンロードし、任意のフォルダに解凍します。

- [TOPPERS/ASP　GR-PEACH](https://github.com/ncesnagoya/asp-gr_peach_gcc-mbed/tree/v1.0.0-AllinOne "TOPPERS/ASP　×　GR-PEACH")

今回開発したGitHubのソースコード【examples】はTOPPERS/ASP　GR-PEACHのパッケージの【examples】のフォルダに上書きコピーしておきます。


ファイル -> Open Project from File System...を選択します。
![open_prj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/9d62ca12-222f-7e73-027c-01b5df185ebf.png)

インポート元のパスに次のパスを指定し終了ボタンを押下します。

- examples¥truestudio¥ntshell_sample

![prj_import.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/8b1a3299-b1c6-f9fa-a2c5-d57789269f4f.png)

Nt-Shellプロジェクトがインポートされます。

![import....png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/af000ad5-3fc2-19cb-420b-89b6d5d3d26f.png)

プロジェクト・エクスプローラのntshell_sampleで右クリックし、【プロジェクトのビルド】を選択しビルドを開始します。

![build.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/46c5f91b-85d6-6d72-ebe5-385855d5e1ef.png)

ビルドが開始されます。

![build....png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/c45c9c98-bccc-1acd-3d7f-d721c63b4d49.png)

ビルドが正常終了すると次のフォルダ以下にバイナリファイル【ntshell_sample.bin】が作成されます。

![create_bin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/ff20737f-2ce4-b428-fad8-e23c03c83f06.png)

バイナリファイルができたらGR-PEACHをUSBケーブルでPCに接続します。
GR-PEACHがUSBメモリとして認識されるので作成されたバイナリをUSBメモリのドライブにドラッグ&ドロップします。
GR-PEACHがUSBメモリ認識解除され、再び認識されたらシリアルターミナルを起動します。
シリアルを選択しOKボタンを押下します。

![teraterm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/e0071701-4fcb-8845-b0a2-43f32b78b1b2.png)

設定 -> シリアルポートを選択し、スピードを【115200】に設定しOKボタンを押下します。

![teraterm_baud.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/c3eb6fe5-eeac-6ecc-0cf6-51caf69326e4.png)

GR-PEACHのリセットボタン押下でシリアルターミナルにTOPPERS/ASPのバナーが表示されます。

![Copyrisht (C) 2000-2003 by Embedded and Real-Time Systems Laboratory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/b1cad07a-4c67-4990-5258-9f84bbb399cf.png)

シリアルターミナルで【>】押下するとコマンド入力待ちの【>】が表示され、キー入力が可能になります。

![2021. 01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/a5a5eda0-61ea-bce8-b363-0a8c23f40014.png)
￼￼

#実装
今回はデバッグ用のコマンドとして次を実装しました。

- コマンドのヘルプ表示
- システム、バージョン表示
- LED点灯
- LED消灯
- メモリリード

NT-Shell, xprintfの実装について説明します。

## NT-Shell
NT-Shellのソースコードは次になります。

### NT-Shellライブラリ本体
NT-Shellライブラリは次のフォルダです。

- examples\ntshell_sample\ntshell-v0.3.1\src\lib

このフォルダに格納されているソースコードをNT-Shellタスク、デバッグコマンドから呼び出します。


### NT-Shellタスク
メインタスクから起動されます。
NT-Shellの初期化をおこないます。

#### 初期化
```c:task_ntshell.c
	ntshell_t ntshell;
	
	ntshell_init(
		&ntshell,
		func_read,
		func_write,
		func_callback,
		(void *)&ntshell);
	ntshell_set_prompt(&ntshell, ">");
	ntshell_execute(&ntshell);
```

リード関数、ライト関数、文字列受信時のコールバック関数をntshell_initに指定します。
ntshell_set_prompt関数にはキャレット文字(今回は">")を指定します。
ntshell_execute関数実行でNT-Shellが実行され、呼び出し元にはリターンしないようになっています。

#### リード、ライト、コールバック関数の設定
リード関数(func_read)にシリアルポートからの文字列受信関数(serial_rea_dat)を指定します。

```c:task_ntshell.c
static int func_read(char *buf, int cnt, void *extobj)
{
	UNUSED_VARIABLE(extobj);
    return serial_rea_dat(TASK_NT_SHELL_PORTID, buf, cnt);
}
```

ライト関数(func_write)にシリアルポートへの文字列送信関数(serial_wri_dat)を指定します。

```c:task_ntshell.c
static int func_write(const char *buf, int cnt, void *extobj)
{
	UNUSED_VARIABLE(extobj);
    return serial_wri_dat(TASK_NT_SHELL_PORTID, buf, cnt);
}
```

serial_rea_dat、serial_wri_datは自作関数ではなく、TOPPERSのサービスコールです。
他のプロセッサ・マイコンターゲットもこのサービスコールでシリアルデータの送信・受信ができるはずです。

コールバック関数は文字列受信完了で呼び出されます。

```c:task_ntshell.c
static int func_callback(const char *text, void *extobj)
{
	usrcmd_execute(text);
	return 0;
}
```

受信した文字列はusrcmd_execute関数内部で解析し、デバッグコマンドを実行します。


### デバッグコマンド
受信した文字列解析、デバッグコマンドはusrcmd.cに定義してあります。

#### 受信文字列のパース
usrcmd_execute関数でntopt_parse関数を実行し、コマンド引数の文字列に分解します。

```c:usrcmd.c
int usrcmd_execute(const char *text)
{
    return ntopt_parse(text, usrcmd_ntopt_callback, 0);
}
```


#### コマンドの検索
usrcmd_ntopt_callback関数ではcmdlist配列と受信したコマンド文字列を比較し、一致したらコマンドを実行します。

```c:usrcmd.c
static int usrcmd_ntopt_callback(int argc, char **argv, void *extobj)
{
    if (argc == 0) {
        return 0;
    }
    const cmd_table_t *p = &cmdlist[0];
    for (int i = 0; i < sizeof(cmdlist) / sizeof(cmdlist[0]); i++) {
        if (ntlibc_strcmp((const char *)argv[0], p->cmd) == 0) {
            return p->func(argc, argv);
        }
        p++;
    }
	syslog_0(LOG_NOTICE, "Unknown command found.");

	return 0;
}
```

次のcmdlistはコマンド文字列、コマンド説明文字列、コマンド関数をメンバに持つcmd_table_t構造体の配列です。

```c:usrcmd.c　cmdlist配列
typedef struct {
    char *cmd;
    char *desc;
    USRCMDFUNC func;
} cmd_table_t;

static const cmd_table_t cmdlist[] = {
    { "help", "This is a description text string for help command.", usrcmd_help },
    { "info", "This is a description text string for info command.", usrcmd_info },
    { "ledon", "This is a LED turn on command.", usrcmd_ledon },
    { "ledoff", "This is a LED turn off command.", usrcmd_ledoff },
    { "mr", mr_cmd_example, usrcmd_memory_read },
};
```

#### helpコマンド
helpコマンドは各コマンドのヘルプを表示するコマンドです。
ｓｙｓlog_３関数はTOPPERSのログ出力関数です。

```c:usrcmd.c　usrcmd_help
static int usrcmd_help(int argc, char **argv)
{
    const cmd_table_t *p = &cmdlist[0];
    for (int i = 0; i < sizeof(cmdlist) / sizeof(cmdlist[0]); i++) {
		syslog_3(LOG_NOTICE, "%s%s%s", p->cmd, "\t:", p->desc);
    	p++;
    }
    return 0;
}
```

#### infoコマンド
infoコマンドはシステム情報を表示するコマンドです。
今回はターゲット名称、バージョンを表示しています。

```c:usrcmd.c　usrcmd_info
static int usrcmd_info(int argc, char **argv)
{
    if (argc != 2) {
		syslog_0(LOG_NOTICE, "info sys");
		syslog_0(LOG_NOTICE, "info ver");

    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "sys") == 0) {
		syslog_0(LOG_NOTICE, INFO_SYSTEM);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "ver") == 0) {
		syslog_0(LOG_NOTICE, INFO_VERSION);
    	return 0;
    }
	syslog_0(LOG_NOTICE,  "Unknown sub command found");
	return -1;
}
```

#### ledonコマンド
ledonコマンドはGR-PEACHのLEDを点灯するコマンドです。

```c:usrcmd.c　usrcmd_ledon
static int usrcmd_ledon(int argc, char **argv)
{
    if (argc != 2) {
		syslog_0(LOG_NOTICE, "ledon red or green or blue or user");
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "red") == 0) {
		set_led(LED1_RED_INDEX, true);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "green") == 0) {
		set_led(LED2_GREEN_INDEX, true);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "blue") == 0) {
		set_led(LED3_BLUE_INDEX, true);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "user") == 0) {
		set_led(LED4_USER_INDEX, true);
    	return 0;
    }
	syslog_0(LOG_NOTICE,  "Unknown sub command found");
	return -1;
}
```

#### ledoffコマンド
ledonコマンドはGR-PEACHのLEDを消灯するコマンドです。

```c:usrcmd.c　usrcmd_ledoff
static int usrcmd_ledoff(int argc, char **argv)
{
    if (argc != 2) {
		syslog_0(LOG_NOTICE, "ledoff red or green or blue or user");
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "red") == 0) {
		set_led(LED1_RED_INDEX, false);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "green") == 0) {
		set_led(LED2_GREEN_INDEX, false);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "blue") == 0) {
		set_led(LED3_BLUE_INDEX, false);
    	return 0;
    }
    if (ntlibc_strcmp(argv[1], "user") == 0) {
		set_led(LED4_USER_INDEX, false);
    	return 0;
    }
	syslog_0(LOG_NOTICE,  "Unknown sub command found");
	return -1;
}
```

#### mrコマンド
mrコマンドはメモリリードコマンドです。
１、２、４バイトのメモリリードに対応しています。
また、指定アドレスから指定サイズのメモリリード・ダンプ表示に対応しています。

```c:usrcmd.c　usrcmd_memory_read
static int usrcmd_memory_read(int argc, char **argv){
	long address;
	long count = 1;
	int read_size;
	char *ptr;

	if (argc < 3) {
		syslog_0(LOG_NOTICE, "mrb <[b|h|w]> <addr> [count]");
    	return 0;
    }
	
    if (ntlibc_strcmp(argv[1], "b") == 0) {
		read_size = 1;
    }
    else if (ntlibc_strcmp(argv[1], "h") == 0) {
		read_size = 2;
    }
    else if (ntlibc_strcmp(argv[1], "w") == 0) {
		read_size = 4;
    } else {
		syslog_0(LOG_NOTICE, "memory read size is [b|h|w].");
    	return 0;
    }
	
	if (!xatoi(&argv[2], &address)) {
		syslog_0(LOG_NOTICE, "address error");
		return 0;
	}

	if (argc == 4) {
		if (!xatoi(&argv[3], &count)) {
			syslog_0(LOG_NOTICE, "count error");
			return 0;
		}
    }

	for (ptr = (char*)address; count >= 16 / read_size; ptr += 16, count -= 16 / read_size)
		put_dump(ptr, (ulong_t)ptr, 16 / read_size, read_size);
	if (count) put_dump((char*)ptr, (uint_t)ptr, count, read_size);
	
	return 0;
}
```


## xprintf
xprintfはコマンド解析、メモリリードコマンドのダンプ表示時に使いました。

### コマンド解析
コマンド解析はメモリリードコマンドで使用しています。
xatoi関数で文字列→数字の変換をおこなっています。

```c:usrcmd.c　usrcmd_memory_read
static int usrcmd_memory_read(int argc, char **argv){
	
	// 略
	
	if (!xatoi(&argv[2], &address)) {
		syslog_0(LOG_NOTICE, "address error");
		return 0;
	}

	if (argc == 4) {
		if (!xatoi(&argv[3], &count)) {
			syslog_0(LOG_NOTICE, "count error");
			return 0;
		}
	}

	// 略

}
```

### メモリダンプ表示
メモリリードコマンドのダンプ表示はput_dump関数でおこなっています。
ダンプ表示を行う場合は初期設定が必要です。
xdev_out関数でput_dump関数内部でダンプ表示出力する関数を登録します。
target_fput_log関数はTOPPERSの文字出力関数です。

```c:task_ntshell.c　
void task_ntshell(intptr_t exinf)
{

	// 略

	/* xprintfシリアル送信関数登録 */
	xdev_out(target_fput_log);
	
	// 略
	
}
```

put_dump関数を呼び出し指定アドレスからダンプ表示をおこないます。

```c:usrcmd.c　
static int usrcmd_memory_read(int argc, char **argv){

	// 略

	for (ptr = (char*)address; count >= 16 / read_size; ptr += 16, count -= 16 / read_size)
		put_dump(ptr, (ulong_t)ptr, 16 / read_size, read_size);
	if (count) put_dump((char*)ptr, (uint_t)ptr, count, read_size);
	
	return 0;
}
```

#動作確認
シリアルターミナルからコマンド文字列を入力し、コマンドの実行結果を確認します。

## コマンドのヘルプ表示
```text:help
>help   :This is a description text string for help command.
info    :This is a description text string for info command.
ledon   :This is a LED turn on command.
ledoff  :This is a LED turn off command.
mr      :mr <[b|h|w]> <addr> [count]
1byte * 16count read example) >mr b 0x1801e35d 16
1801E35D  54 4F 50 50 45 52 53 2F 41 53 50 20 4B 65 72 6E  TOPPERS/ASP Kern

2byte * 8count read example) >mr h 0x1801e35d 8
1801E35D  4F54 5050 5245 2F53 5341 2050 654B 6E72

4byte * 4count read example) >mr w 0x1801e35d 4
1801E35D  50504F54 2F535245 20505341 6E72654B

1byte read example) >mr b 0x1801e35d
1801E35D  54  T

2byte read example) >mr h 0x1801e35d
1801E35D  4F54

4byte read example) >mr w 0x1801e35d
1801E35D  50504F54
```

## システム、バージョン表示
```text:info
>info
>info sys
info ver

>info sys
>GR-PEACH

>info ver
>0.1.0
```

## LED点灯
```text:ledon
>ledon
>ledon red or green or blue or user
>ledon red
```

## LED消灯
```text:ledoff
>ledoff
>ledoff red or green or blue or user
>ledoff red
```

## メモリリード
メモリリードコマンドの１番目の引数にはリードサイズを指定します。
bは１byte、hは２byte、wは4byteの指定になります。
３番目の引数countはメモリリードする回数をしています。countの指定なしの場合は１回のリードをおこないます。
１番目の引数が１byteで、３番目の引数が１６の場合は指定アドレスから１byteを１６回メモリリードします。
1byteのメモリリードの場合は右横にASCIIコードを表示してくれます。
ダンプ表示のロジックはxprintfのput_dump関数にお任せしています。

【mr b 0x1801d674 16】コマンド実行結果ではTOPPERS起動時のバナー表示文字列を読み出しています。
次のファイルから【banner】シンボルのアドレスを検索し、１byte✕１６カウントのメモリリードをおこなうとTOPPERSのバナー表示文字列をメモリリードコマンドで確認できます。

 - examples\truestudio\ntshell_sample\ntshell_sample.syms


```text:mr
>mr
>mrb <[b|h|w]> <addr> [count]

>mr b 0x1801d674 16
1801D674  0A 54 4F 50 50 45 52 53 2F 41 53 50 20 4B 65 72  .TOPPERS/ASP Ker
>mr h 0x1801d674 8
1801D674  540A 504F 4550 5352 412F 5053 4B20 7265
>mr w 0x1801d674 4
1801D674  504F540A 53524550 5053412F 72654B20
>mr b 0x1801d674
1801D674  0A  .
>mr h 0x1801d674
1801D674  540A
>mr w 0x1801d674
1801D674  504F540A
```

#まとめ
TOPPERSでNT-Shell、xprintfを使うことで簡単にデバッグコマンドを組み込むことができました。
今回はメモリリードコマンドだけを実装しましたがメモリライトコマンドを実装すればプロセッサ・マイコンのレジスタを直接リード・ライトすることも可能だと思うので、低レベルのデバッグにも使えそうです。
また、意図しない不具合が発生したときにメモリリードして状況を確認する、など保守用途で使っても良いな、と思いました。


#ライセンス
今回使用したソフトウェアのライセンスは次のとおりです。

| 項目 | ライセンス |
|:-----------|:------------|
|TOPPERS/ASP       |[TOPPERS ライセンス](https://toppers.jp/license.html)|
|NT-Shell       |[MIT](https://cubeatsystems.com/ntshell/license.html)|
|xprintf       |[ダウンロードしたソースコードを参照](http://elm-chan.org/fsw/strf/xprintf_j.html)|