MachiKania

MachiKania type Pで、BASICコードからCで書かれたコードを呼び出す

2023年3月26日

MachiKania type P の ver 1.2 では、BASIC コードから C で書かれたコードを呼び出すための機能追加がされた。この記事では、どのようにすれば C で書かれたコードを呼び出すことができるのかについて述べる。

方法は大きく分けて二通りあって、一つは RAM 上に HEX アプリケーションを読み込んでそこに含まれる機能を呼び出す方法である。RAM 上にアプリケーションを配置して HEX ファイルを作成する方法は、別の記事で紹介した。この記事では、作成したHEXファイル中にある C で関数をどのようにして呼び出すかについて述べる。この手法を用いて HELLO_CW アプリケーションREGEXP クラスを作成したので、それを例に挙げながら説明する。

もう一つは、C でビルドしたオブジェクトから、必要な機能だけをピックアップして、BASIC の EXEC ステートメントに変換して埋め込む方法である。簡単な C のコードを実行させる場合には、この方法が簡便でサイズもコンパクトにできるが、これについては、別の記事で取り上げることにする。

この記事では HEX アプリケーションを読み込んでそこに含まれる機能を呼び出す方法について述べたい。複雑な C のコードを呼び出すのに向いた方法である。

BASIC からHEX アプリケーションを呼び出す例(hello_cwアプリケーション)

もっとも簡単な例として、2つの整数値の和を返す関数をCで次のように記述して、この関数を呼び出す方法について説明する。
int add_values(int a, int b){
	return a+b;
}

この例を説明するために、GitHubレポジトリーを作成したので、参照していただきたい。Git CloneしてCommit Logを見ていただければ、説明が分かりやすいと思う。
2023-03-17-log.png
また、この記事では、1から8までの手順ごとに該当レポジトリーへのリンクを付けたので、そちらも見ていただきたい。

1.まずは、Hello, World! から
まず、Cのプロジェクトを一つ作成する必要がある。何でも良いのだが、ここではpico-examplesからhello_usb.cを選んで、若干変更した物から始める事とする。これは、ビルドした後にPi Picoにインストールすれば、USB シリアル接続を通して、コンソール上に「Hello, World!」と表示し続けるプログラムだ。
#include <stdio.h>
#include "pico/stdlib.h"

int main() {
	stdio_init_all();
	while (true) {
		printf("Hello, world!\n");
		sleep_ms(1000);
	}
	return 0;
}
このプロジェクトをcmake/makeでビルドすると、hello_cw.uf2に加えてhello_cw.hexが作成されることが分かる。このようにして作成されたhexファイルを用いることになる。

2.RAM上のアプリケーションに変更する
このHello, World!アプリケーションは、hello_cw.uf2をRPI-RP2ドライブにコピーすると、フラッシュ上にプログラムが構築され、電源を切ったりリセットボタンを押したりしても、同じプログラムが実行される。しかし、今回必要なCプログラムは、フラッシュ上に構築されるものではなく、RAM上に実行コードが構築されるタイプの物である。このための変更を行う。この方法については、別に記事を書いてあるので、詳細はそちらを参照していただきたい。この変更を施すと、hello_cw.uf2をRPI-RP2ドライブにコピーした直後は目的のプログラムが実行されるが、リセットボタンを押すと、元の(フラッシュにインストールされていた)プログラムに戻ることが分かる。

3.目的のコードを、プロジェクト内に記述する
Cアプリケーションを記述してテストする環境が整ったので、ここで目的のコードを書いてみる。上で述べたadd_values()関数を追加して、テスト結果を表示するようなものにした。
#include <stdio.h>
#include "pico/stdlib.h"

int add_values(int a, int b){
	return a+b;
}

int main() {
	int a=0,b=0;
	stdio_init_all();
	while (true) {
		printf("%d + %d = %d\n", a, b, add_values(a,b));
		a+=1;
		b+=2;
		sleep_ms(1000);
	}
	return 0;
}

4.RAM格納アドレスを変更する
今のままでは、作成されたCアプリケーションが、0x20000000から配置され、これはMachiKania BASICの実行コード配置位置と重なってしまう。そこで、これらが重ならないように、配置されるRAMアドレスを修正しなければならない。0x20000000から0x2002FFFFまでがMachiKania BASICのコード格納領域なので、始めの方のアドレスをBASICコード用に少し取っておいて、その少し後ぐらいに格納すればよい。ここでは、0x20010000からの64K bytesを使う事とする。「memmap_no_flash.ld」の26行目を、次のように書き換える。
RAM(rwx) : ORIGIN =  0x20010000, LENGTH = 64k

5.MachiKaniaライブラリーを取り込む
ここで、アプリケーションに、MachiKania BASICと連携するためのライブラリーを取り込む。
2023-03-17-include.png
「machikania.c」と「machikania.h」を追加した。これらのファイルはhello_cwプロジェクトに特化したものではなく、共通の物なので、他のケースでもここからコピーして使えるはずである。また、「hello_usb.c」を編集して、以下のような内容にした。
#include <stdio.h>
#include "pico/stdlib.h"
#include "machikania.h"

int add_values(int a, int b){
	return a+b;
}

int main() {
	int a=0,b=0;
	stdio_init_all();
	machikania_init();
	while (true) {
		printf("%d + %d = %d\n", a, b, add_values(a,b));
		a+=1;
		b+=2;
		sleep_ms(1000);
	}
	return 0;
}
「#include "machikania.h"」と、「machikania_init();」の2行を加えてある。また、「CMakeFiles.txt」は、「machikania.c」をビルドの対象に加えるようにするだけでなく、12行目に以下の行を追加している。
add_definitions(-O0)
この設定項目の追加の意味については、次の段落で述べる。

この記事で述べる方法で、MachiKaniaからHEXファイル内のコードを呼び出そうとする場合、以下の点に注意が必要だ。

a.呼び出そうとするC関数が、必ずHEXオブジェクトに含まれるようにする
b.呼び出そうとするC関数の引数は、4つまで
c.呼び出そうとするC関数の引数の取り扱いに関して、ビルドの際の最適化で標準のARMレジスター使用(R0, R1, R2, R3を使用する)から変更されないようにする


a.の対策のためには、必要な関数がすべて、「main()」関数、もしくはそこから呼び出される関数の中で、最低一度は呼び出される必要がある。でないと、リンクの際に不必要な関数コードとして、削除されてしまう。

また、Cコンパイラー(gcc)が行う最適化のために、関数がインライン展開されてしまう事もあり、そうなると外部から呼び出すことができなくなってしまう。これを防ぐ方法にはさまざまあり、1:目的の関数を「main.c」とは別のファイル内に記述する、2:「__declspec(noinline)」アトリビュートを使う、3:最適化を行わない、などがある。

b.は ARM gcc コンパイラーの規約に関係するもので、これを遵守しないと(引数を5つ以上にしてしまうと)、正常に動作しない。

c.の対策は色々と複雑で、同一のCファイル内で目的の関数を呼び出している場合に変なレジスター使用が起こるようだ。最適化の一つなので、当然ながら最適化を行わないようにすることで回避することができる。

以上の事から、トラブルを防ぐにはgccの最適化を停止することが有効である。CMakeLists.txtに「add_definitions(-O0)」の一行を加えたのは、そのためである。特殊な作例で、実行速度が最大でないといけないようなケースにはこの方法は使えないが、そうでなければ、最適化を停止して使用するのが無難だろう。

6.PHPスクリプトを配置する
作成されたHEXファイルを呼び出すBASICプログラムを自動作成するためのスクリプト「c_convert2.php」を用意した。このスクリプトは、PHPで記述されているので、PHPがない場合はインストールが必要である。このPHPスクリプトは汎用であるが、冒頭の記述(13-26行目辺り)を若干編集して使用する。今回の例では、次のようになっている。
class configclass{
	// File names
	public $dis_file='./build/hello_cw.dis';
	public $map_file='./build/hello_cw.elf.map';
	public $hex_file='./build/hello_cw.hex';
	//public $debug_file='./machikap/machikap.bas';
	//public $debug_hex='./machikap/hello_cw.hex';
	
	// Functions to be exported
	public $functions=array(
		'machikania_init',
		'add_values',
	);
};
$dis_file, $map_file, $hex_fileでは、ビルドの際に作成された「*.dis」「*.map」「*.hex」の3つのファイルの位置を指定する。$debug_file, $debug_hexはオプションで、PC-Connect機能を利用してデバッグを行っている際の、BASICファイルとHEXファイルの位置を指定する。必要が無い場合は、「//」でコメントアウトしておく。また、$functionsには、BASICから呼び出したいCの関数名を列挙しておく。

7.不必要なUSB機能を削除する
Pi PicoのUSBシリアル機能は、アプリケーション作成時やデバッグ時には便利であるが、完成したHEXファイルには必要が無い機能である。USBシリアル機能のためには大きなプログラム領域が必要なので、これを削除することで、HEXファイル及び占有RAM領域を小さくすることができる。USBシリアル機能を削除するためには、CMakeLists.txtの26行目を、次のようにすればよい。
pico_enable_stdio_usb(hello_cw 0)

8.Cプログラムのビルドと、PHPスクリプトの実行
ここまで準備できれば、Cプログラムをビルドした後に、PHPスクリプトを実行する。「result.txt」と「log.txt」の二つのファイルが出来ているはずだ。ここでの例のように進めると、「result.txt」の内容は次のようになる。
useclass CLDHEX
usevar C_CODE
gosub INIT_C
end

label INIT_C
  var A,V
  REM Load the main code
  A=$20010000
  C_CODE=new(CLDHEX,"HELLO_CW.HEX",A,65536)
  REM data_cpy_table
  REM Link functions
  V=(A+1168-DATAADDRESS(C_MACHIKANIA_INIT)-12)>>1
  poke16 DATAADDRESS(C_MACHIKANIA_INIT)+8,$f000+(V>>11)
  poke16 DATAADDRESS(C_MACHIKANIA_INIT)+10,$f800+(V and $7ff)
  V=(A+1068-DATAADDRESS(C_ADD_VALUES)-12)>>1
  poke16 DATAADDRESS(C_ADD_VALUES)+8,$f000+(V>>11)
  poke16 DATAADDRESS(C_ADD_VALUES)+10,$f800+(V and $7ff)
  REM Initialize C global variables
  gosub C_MACHIKANIA_INIT
return

label C_MACHIKANIA_INIT
  exec $68f0,$6931,$6972,$69b3,$f000,$f800,$bd00
label C_ADD_VALUES
  exec $68f0,$6931,$6972,$69b3,$f000,$f800,$bd00
このBASICプログラムは、カレントディレクトリーにあるHEXファイル(HELLO_CW.HEX)を取り込み、Cプログラム中の「machikania_init();」を呼び出すだけの、必要最小限の構成になっている。目的のコードを、3行目の「gosub INIT_C」と4行目の「end」の間に書けばよい。例えば、次のように変更して、BASICプログラムを完成させてみよう。
useclass CLDHEX
usevar C_CODE
gosub INIT_C

for i=1 to 10
  print a,b,a+b,gosub(C_ADD_VALUES,a,b)
  a=a+1
  b=b+2
next

end

label INIT_C
  var A,V
  REM Load the main code
  A=$20010000
  C_CODE=new(CLDHEX,"HELLO_CW.HEX",A,65536)
  REM data_cpy_table
  REM Link functions
  V=(A+1168-DATAADDRESS(C_MACHIKANIA_INIT)-12)>>1
  poke16 DATAADDRESS(C_MACHIKANIA_INIT)+8,$f000+(V>>11)
  poke16 DATAADDRESS(C_MACHIKANIA_INIT)+10,$f800+(V and $7ff)
  V=(A+1068-DATAADDRESS(C_ADD_VALUES)-12)>>1
  poke16 DATAADDRESS(C_ADD_VALUES)+8,$f000+(V>>11)
  poke16 DATAADDRESS(C_ADD_VALUES)+10,$f800+(V and $7ff)
  REM Initialize C global variables
  gosub C_MACHIKANIA_INIT
return

label C_MACHIKANIA_INIT
  exec $68f0,$6931,$6972,$69b3,$f000,$f800,$bd00
label C_ADD_VALUES
  exec $68f0,$6931,$6972,$69b3,$f000,$f800,$bd00
あとは、このBASICプログラムとHELLO_CW.HEXをMMC/SDカードにコピーすれば、実行することができる。実行時のスナップショットは、以下の通り。
2023-03-18-20230318_012739.png

以上が、簡単なCコードをHEXに変換して、MachiKania BASICプログラムから呼び出すための手順である。

応用編:REGEXPクラスでの使用例

HEX アプリケーションを利用する方法について、応用例について取り上げる。また、ここでは動作のメカニズムに関しても少し説明する。REGEXP クラスを見ていただきたい。上で述べたHELLO_CWプログラムはCのコードを実行するための最小限の構成であるが、REGEXPクラスでは、次のように発展させてある。

・RAM配置アドレスの異なる複数のHEXファイルを作成し、メモリーの空きを見つけて適切なHEXプログラムをロードする
・複数の*.cファイルにまたがる、少し規模の大きいプログラムである
・mallocを使用している
・RAM占有領域を、必要最小限の大きさ(32 kb)に制限している


このクラスの構造がどうなっているか、まず説明しよう。REGEXP ディレクトリーには、以下のファイル・ディレクトリーが存在する。

2023-01-30-dir.png

help.txtは説明書きなので、ここでは取り上げない。「regexp.bas」以外に「hex」ディレクトリーと「c」ディレクトリーがある。「hex」ディレクトリーに含まれるファイルは、以下の通り。

2023-01-30-hexdir.png

ここには、7つの HEX ファイルがあり、それぞれでRAM上の配置アドレスが異なる。「c」ディレクトリーに含まれるファイルは、以下の通り。

2023-01-30-cdir.png

ここには、「c_convert2.php」という PHP スクリプトと、C のソースコードを含む「src」ディレクトリーがある。

まずは、regexp.bas の中身から見てみよう。ソースコードは、こちらで閲覧していただきたい。なお、ここではc_convert2.phpで自動作成されるコードについてのみ説明し、REGEXPクラスが扱う正規表現エンジンに関するコードの説明は割愛する。

まず冒頭の6行目:
useclass CLDHEX
CDLHEXクラスを読み込んでいる。このクラスは、HEX ファイルを読み込んで、RAM に構築する役割がある。なお、ここで作成した HEX ファイルは、前の記事で説明したように作成されたもので、アプリケーションはフラッシュ上でなく、RAM上に構築される。上記「HEX」ディレクトリーにあるものがそれで、REGEXPクラスでは7つある中から適応するもの(なるだけ、格納アドレスが若い物)を選んで読み込むようにしてある。

次に見ていただきたいのが、111行目の
label INIT_C
以降のコードである。ここは、REGEXP クラスの実行時の初めに呼ばれる部分で、C で書かれたコードを BASIC から呼び出すための前準備をしている。まず、
A=gosub(INIT_C_LDHEX)
でHEXファイルをロードして、その後に複数の「POKE16」ステートメントを実行している。これらは、「C_PRECOMP」「C_REGCOMP」等のサブルーチン呼び出しで、HEXファイル中のどのアドレスを呼び出すかを、設定するための物である。

136行目の「label C_MACHIKANIA_INIT」以下のコードを見ていただきたい。EXEC ステートメントが並んでいる。これらの機械語は、次のアセンブリーコードである。
68f0          ldr    r0, [r6, #12]
6931          ldr    r1, [r6, #16]
6972          ldr    r2, [r6, #20]
69b3          ldr    r3, [r6, #24]
f000 f800     bl    <xxxx>
bd00          pop    {pc}
「[r6, #12]」等は、MachiKania BASIC の「ARGS(1)」等に相当するコードで、サブルーチン呼び出し時の引数を、R0 などの ARM レジスターに代入している。ここから分かるように、関数の引数は4つまでに限定される。「bl」 インストラクションは、上で述べた「POKE16」ステートメントにより、HEXファイル中の該当する関数へジャンプするアセンブリーに置き換えられる。これらの仕様により、BASICコードからHEXファイル中の該当する関数を呼び出す事が可能になる。最後の「pop {pc}」は、MachiKania BASIC の「RETURN」ステートメントに相当する。

HEXファイルの作成方法と、HEX 中の C 関数を呼び出すBASICコードの作成方法

HEXファイルは、「c/src」ディレクトリー内のソースコードをビルドすると、作成できる。一回のビルドで、7つのHEXファイルが同時に作成できる構成である。対応するリンカースクリプトは、「c/src/lds」ディレクトリーにある7つで、0x20002000, 0x20004000, 0x20008000, 0x2000c000, 0x20010000, 0x20018000, 0x20020000 の RAM アドレスに、32k バイトの長さでビルドオブジェクトを配置するタイプの物だ(memmap_no_flash.ld を元に作成; 領域の長さはビルドオブジェクトが収まるぎりぎりの長さ+α)。ビルドすると、*.hex だけでなく、*.elf, *.uf2, *.dis, *.map などのファイルも同じディレクトリーに作成される。私は、すべて「build」という名のディレクトリーに作成されるようにした。

次に、BASIC コードを作成する。これは、上で説明した C コードのビルドが終わった後に、「c_convert2.php」スクリプトをPHPで実行することにより行う。このスクリプトを使用する前に、以下の箇所を編集する。
class configclass{
	// File names
	public $dis_file='./build/regexp02.dis';
	public $map_file='./build/regexp02.elf.map';
	public $hex_file='./build/regexp02.hex';
	public $debug_file='./machikap/regexp.bas';
	public $debug_hex='./machikap/regexp02.hex';
	
	// Functions to be exported
	public $functions=array(
		'machikania_init',
		'precomp',
		'regcomp',
		'regexec',
		'regsub',
	);
};
「$dis_file」「$map_file」「$hex_file」には、3つのビルドオブジェクトファイルの格納位置を指定する。「$debug_file」と「$debug_hex」はオプションで、指定しなくても良いが、このスクリプトを実行後にテストを行いたい場合、書き換える BASIC ファイルと HEX ファイルを指定する(PC-connect 機能を用いてデバッグを行う事を想定;指定しない場合は、これらの2つの行を「//」でコメントアウトする)。「$functions」には、エクスポートしたい C コードの関数を指定する。REGEXPの例では、5つの関数が指定されている。

「c_convert2.php」を実行すると、「result.txt」が作成される。この BASIC ファイルには、HEX 中の C 関数を呼び出すための必要最小限のコードが含まれるため、目的のアプリケーションを作成するために編集して用いる。

C ソースコードの書き方

ここでの C のソースコードを書く際は、いくらか工夫が必要である。

1.上でも述べたが、フラッシュ領域ではなく RAM 領域にコードを配置する(リンカースクリプトは memmap_no_flash.ld を元に作成)。
2.必要のないライブラリーなどは、なるだけ取り込まれないようにする。
3.gcc による最適化は、行わない。

「c/src/CMakeLists.txt」の次の記述を見ていただきたい。
set(PICO_NO_FLASH 1)
set(ENABLE_USB_UART 0)
add_definitions(-O0)
上で述べた3つの事項に該当する部分の記述である。「PICO_NO_FLASH 1」は、フラッシュを用いずに RAM 領域にコードを構築するのに必要な設定。「ENABLE_USB_UART 0」は、USBを用いたシリアル通信機能を含めないための指定である(CMakeListsSub.txtの中で「pico_enable_stdio_usb(${ENV_TARGET} ${ENABLE_USB_UART})」として使用されている)。この通信機能は、C のコードをデバッグするときには必要であるが、最終的な HEX ファイルに含める必要はない。最後の「-O0」は、gcc によるコンパイルの際に最適化を行わないようにする指定だ。最適化を行うと、時として関数の引数の扱いを変えたりすることによる誤動作の可能性があるので、最適化をオフにしておくことをお勧めする。

BASIC から呼び出しを行いたい関数は、なるだけmain() 関数とは別の C ファイルに記述するようにする(CMakeLists.txtで「add_definitions(-O0)」を指定した場合は、同じCファイルに記述可能)。また、main() 関数内では、これらの関数を呼び出すコードを記述しておく。これにより、最終的なビルドオブジェクトに、必要とする関数のコードが必ず含まれるようにする。

C から MachiKania の機能を呼び出すためのコードは、「machikania.c」に用意した。使用の際は、「machikania.h」をインクルードして用いる。なお、ここには「malloc()」「calloc()」「free()」「exit()」の4つの関数も含まれているので、これらを用いたい場合も、「machikania.c」と「machikania.h」が必要だ。

HEX ファイルは、一つでも良い

REGEXP では、他のクラスファイルなどとのコードの配置を考えて、複数の HEX ファイルを準備してフィットする物を用いるという、複雑な構成になっている。多くのケースでは、そのような複雑な構成を取る必要はないだろう。HEX ファイルは、一つだけでも良い。リンカースクリプトとしては、「c/src/lds」ディレクトリーの中から一つだけ選んで用いればよい。多くのケースで、「memmap_no_flash_02.ld」であろう。メインの BASIC ファイルや、読み込むクラスファイルのサイズが大きければ、より下位のアドレスを用いる ld ファイルを用いればよい。

どんどん C コードを取り込もう

この記事と、HELLO_CW アプリケーション・REGEXP クラスの例を参考にすれば、C でのソースコードが公表されている様々な機能を、MachiKania に取り込むことができる。それにより、MachiKania の応用範囲がさらに広くなることを、期待している。

コメント

コメントはありません

コメント送信