X-CUBE-AIのApplicationTemplateの使い方をわかりやすく解説します。
目次
1.ApplicationTemplateとは
ApplicationTemplateとは、X-CUBE-AIに含まれる3つのアプリケーションの内の1つで、正確にはA.I.アプリケーションを作るためのテンプレートです。
X-CUBE-AIのアプリケーションに含まれる3つのアプリケーションについては、下記の記事を参考にしてください。
関連:X-CUBE-AI/Applicationの3つのアプリケーションの違いをわかりやすく解説
初期の頃(ver.3.4.0)のApplicationTemplateは、中身が入っていなくて使い物になりませんでした。そのため、ほぼ自前でプログラムを作る必要がありました。
ところが、2020年2月現在の最新バージョンVer.5.0.0では、このApplicationTemplateがちゃんとテンプレートとして使えるようになっています。
このテンプレートを使うと、ちょっと追加のコードを書くだけで、簡単にA.I.アプリケーションを作れるようになります。
2.ゴールの確認
では本記事のゴールを確認しましょう。
以前、ApplicationTemplateを使わずに推論プログラムを作ったことがあります。これはXORを推論してみるアプリケーションでした。
今回は、ApplicationTemplateを使って同じアプリケーション作り、ApplicationTemplateの使い方を学習します。
関連:X-CUBE-AIを使った推論プログラムの作り方をわかりやすく解説
3.前提条件
- STM32Cube.IDE ver.1.10
- NUCLEO-F476RG
4.ApplicationTemplateの使い方
4-1.プロジェクトの作成
まずはプロジェクトを作成します。とくに説明は不要と思います。
4-2.X-CUBE-AIの追加
プロジェクトにX-CUBE-AIのソフトウェアを追加します。
- Device Configuration Tool→Additional Softwareで追加ソフトウェアの設定画面を開きます
- STMicroelectronics X-CUBE-AI→Artificial_Intelligence_Application→Application→ApplicationTemplateを選択
- STMicroelectronics X-CUBE-AI→Artificial_Intelligence_Application→Coreにチェックを入れる
- 右下の『OK』をクリック

4-3.推論モデルを追加
推論モデルをプロジェクトに追加します。前回使ったXORモデルをそのまま使用します。
- 左メニューのAdditional Software→STMicroelectronics.X-CUBE-AI…をクリック
- 『Add network』ボタンをクリックし、XORモデルのファイルを選択(ここではxor.h5を選択)

4-4.ソースコードの自動生成
自動生成ボタンをクリックするか、Device Configuration Toolを保存(Ctrl+S)すると、ApplicationTemplateを使ったソースコードが自動生成されます。

どういうプログラムの構造になっているか図示します。

一番上のmainは、そのままmain関数です。推論プログラムを呼び出すところまでApplicationTemplateが生成してくれているので、ほとんどいじる必要はありません。今回はprintfの初期化だけ追加します。
真ん中の『app_x-cube-ai』が推論プログラムのロジック部分で、ここを適時改造します。
xor、xor_dataは、X-CUBE-AIが生成したXORを推論するライブラリです。ライブラリなのでこのまま利用します。
4-5.main.cの改造
main関数を見てみましょう。

『MX_X_CUBE_AI_Init()』と『MX_X_CUBE_AI_Process()』が、推論プログラムです。
『MX_X_CUBE_AI_Init()』は名前の通り、初期化のための関数です。これはこのまま使用します。
『MX_X_CUBE_AI_Process()』が、推論を実際に実行している関数です。この中身をお好みで改造します。
printfを使いたいので、printfの各種準備を行います。printfについては下記の記事を参考にしてください。
関連:【便利】STM32CubeIDEでprintf【UART編】
4-6.app_x-cube-ai.cの改造
app_x-cube-ai.cの『MX_X_CUBE_AI_Process()』を見ていきます。
はじめに変数の初期化を行っています。
/* USER CODE BEGIN 1 */ int nb_run = 20;
nb_runは、推論を何回実行するかを宣言しています。実行したい回数+1を指定するようになっているので、このままだと19回実行することになります。
今回は1回実行すればよいと思いますので、20を2に変えておきます。
/* USER CODE BEGIN 1 */
// int nb_run = 20;
int nb_run = 2;
また、XORの入力として、{0,0}、{0,1}、{1,0}、{1,1}、の4パターンを使うので、配列を作っておきます。
// input to xor
static ai_float inputs[4][2] = {
{0.0f, 0.0f},
{0.0f, 1.0f},
{1.0f, 0.0f},
{1.0f, 1.0f}
};
この配列を使ってXORに入力するので、デフォルトで生成されている入力の生成部分はコメントアウトします。
/* ---------------------------------------- */
/* Data generation and Pre-Process */
/* ---------------------------------------- */
/* - fill the input buffer with random data */
// for (ai_size i=0; i < AI_XOR_IN_1_SIZE; i++ ) {
//
// /* Generate random data in the range [-1, 1] */
// ai_float val = 2.0f * (ai_float)rand() / (ai_float)RAND_MAX - 1.0f;
//
// /* Convert the data if necessary */
// if (type_ == AI_BUFFER_FMT_TYPE_FLOAT) {
// ((ai_float *)in_data)[i] = val;
// } else { /* AI_BUFFER_FMT_TYPE_Q */
// /* Scale the values in the range [-2^M, 2^M] */
// val *= max_;
// /* Convert float to Qmn format */
// const ai_i32 tmp_ = AI_ROUND(val * scale_, ai_i32);
// in_data[i] = AI_CLAMP(tmp_, -128, 127, ai_i8);
// }
// }
変わりに、下記のコードを入れます。実行の度に{0,0}→{0,1}→{1,0}→{1,1}→・・・と入力が変わっていくようにしました。
static uint8_t i = 0;
((ai_float *)in_data)[0] = inputs[i][0];
((ai_float *)in_data)[1] = inputs[i][1];
i++;
if (i > 3) {
i = 0;
}
最後に推論実行部分を見てみます。
/* Perform the inference */
res = aiRun(in_data, out_data);
if (res) {
// ...
return;
}
aiRun()は、成功すると0が返り、失敗すると0以外が返る仕様です。失敗したらif文の中に入るので、ここで失敗時の処理を入れてください、という意図でしょう。
今回は下記のようにしました。
/* Perform the inference */
res = aiRun(in_data, out_data);
if (res) {
// ...
printf("input[%1.3f, %1.3f] -> failed aiRun. cause=%d\n", (float)in_data[0], (float)in_data[1], res);
return;
}
printf("input[%1.3f, %1.3f] -> output[%1.3f]\r\n", ((float*)in_data)[0], ((float*)in_data)[1], ((float*)out_data)[0]);
MX_CUBE_AI_Process()の全文は下記のようになりました。
void MX_X_CUBE_AI_Process(void)
{
/* USER CODE BEGIN 1 */
// int nb_run = 20;
int nb_run = 2;
int res;
// input to xor
static ai_float inputs[4][2] = {
{0.0f, 0.0f},
{0.0f, 1.0f},
{1.0f, 0.0f},
{1.0f, 1.0f}
};
/* Example of definition of the buffers to store the tensor input/output */
/* type is dependent of the expected format */
AI_ALIGNED(4)
static ai_i8 in_data[AI_XOR_IN_1_SIZE_BYTES];
AI_ALIGNED(4)
static ai_i8 out_data[AI_XOR_OUT_1_SIZE_BYTES];
/* Retrieve format/type of the first input tensor - index 0 */
const ai_buffer_format fmt_ = AI_BUFFER_FORMAT(&ai_input[0]);
const uint32_t type_ = AI_BUFFER_FMT_GET_TYPE(fmt_);
/* Prepare parameters for float to Qmn conversion */
const ai_i16 N_ = AI_BUFFER_FMT_GET_FBITS(fmt_);
const ai_float scale_ = (0x1U << N_);
const ai_i16 M_ = AI_BUFFER_FMT_GET_BITS(fmt_)
- AI_BUFFER_FMT_GET_SIGN(fmt_) - N_;
const ai_float max_ = (ai_float)(0x1U << M_);
/* Perform nb_rub inferences (batch = 1) */
while (--nb_run) {
/* ---------------------------------------- */
/* Data generation and Pre-Process */
/* ---------------------------------------- */
/* - fill the input buffer with random data */
// for (ai_size i=0; i < AI_XOR_IN_1_SIZE; i++ ) {
//
// /* Generate random data in the range [-1, 1] */
// ai_float val = 2.0f * (ai_float)rand() / (ai_float)RAND_MAX - 1.0f;
//
// /* Convert the data if necessary */
// if (type_ == AI_BUFFER_FMT_TYPE_FLOAT) {
// ((ai_float *)in_data)[i] = val;
// } else { /* AI_BUFFER_FMT_TYPE_Q */
// /* Scale the values in the range [-2^M, 2^M] */
// val *= max_;
// /* Convert float to Qmn format */
// const ai_i32 tmp_ = AI_ROUND(val * scale_, ai_i32);
// in_data[i] = AI_CLAMP(tmp_, -128, 127, ai_i8);
// }
// }
static uint8_t i = 0;
((ai_float *)in_data)[0] = inputs[i][0];
((ai_float *)in_data)[1] = inputs[i][1];
i++;
if (i > 3) {
i = 0;
}
/* Perform the inference */
res = aiRun(in_data, out_data);
if (res) {
// ...
printf("input[%1.3f, %1.3f] -> failed aiRun. cause=%d\n", (float)in_data[0], (float)in_data[1], res);
return;
}
printf("input[%1.3f, %1.3f] -> output[%1.3f]\r\n", ((float*)in_data)[0], ((float*)in_data)[1], ((float*)out_data)[0]);
/* Post-Process - process the output buffer */
// ...
}
/* USER CODE END 1 */
}
4-7.実行してみる
実行してみます。

動きました!
5.A.I.アプリケーションを本格的に作る場合のプログラムの構造
以上が、最小のA.I.アプリケーションを作成する方法です。
ここで少し、本格的にアプリケーションを作る時のことも考えておきましょう。
基本的なA.I.アプリケーションのアーキテクチャは下記の図のようなイメージになります。

基本となる動作は下記の順になります。
- コントローラーが入力モジュールからデータを取得する。
入力データは各種センサーになることが多いでしょう。 - 取得したデータを整形しニューラルネットワークに入力し、推論を実行させます。
- ニューラルネットワークの結果に基づき、制御対象モジュールに対し制御を実行します。
制御対象モジュールは例えばモーターなどが考えられます。
今回作成したアプリケーションは、ニューラルネットワークとコントローラーだけのものでした。
ニューラルネットワークが『XOR』と『XOR_data』です。
コントローラーが『app_x-cube-ai』です。
もう1度図示すると、下記の構造になっています。

※XORは汎用的にxxxと表記
本格的なアプリケーションだと、これに入力モジュールと制御対象モジュールが追加されるので、次のような構造になります。

この章の1つ目の図が理解できていれば、この構造図もスっと頭に入ると思います。モヤモヤが残る方は、2つの図を見比べて、どことどこが対応しているのか見てみてください。
この構造に関しては、X-CUBE-AIのマニュアルにはここまで書いてありませんが、自動生成されたソースコードの書き方からして、こういう意図なのでしょう。
1点注意事項があり、入力モジュールと制御対象モジュールをapp_x-cube-aiから利用するのにincludeが必要になりますが、includeを書く場所が決まっています。
app_x-cube-ai.h
/* USER CODE BEGIN includes */ /* USER CODE END includes */
STM32CubeIDEによる自動生成されたコード全てに言えることですが、ユーザーコードは『USER CODE BEGIN xxx』から『USER CODE END xxx』の間に入れておく必要があります。そうしないと、再度自動生成した時に、せっかく書いたコードが上書きされて消えてしまうからです。
app_x-cube-ai.hも同様で、includeを書く場所が決まっているので、そこだけ注意しましょう。
6.おわりに
ApplicationTemplateを使ってXORを推論するアプリケーションを作りました。
ApplicationTemplateがソースコードのほとんどを生成してくれるため、コーディングの量を最小限にすることができます。とりあえず動かしたい時にはとても便利なツールだと思います。