Arduino® UNO Qは、高性能なLinux側だけでなく、超省電力で動作するマイコン側(STM32U585)も魅力です。
今回は、マイコン側でリアルタイムOS「Zephyr RTOS」を動かし、SPIカメラ(Arducam Mega)とPIRセンサーを用いた、省電力なエッジAI(デバイス識別)の実行例を紹介します。
Arduino® UNO Q & Zephyr RTOS でエッジAI!【SPIカメラ & FOMOによるデバイス識別編】
Arduino® UNO Qは、高性能なLinux側だけでなく、超省電力で動作するマイコン側(STM32U585)も魅力です。
今回は、マイコン側でリアルタイムOS「Zephyr RTOS」を動かし、SPIカメラ(Arducam Mega)とPIRセンサーを用いた、省電力なエッジAI(デバイス識別)の実行例を紹介します。



プロジェクト・GitHubのリンク先
コンテストに提出したプロジェクトのリンク先は下記です
GiHubのリンク先は下記です

STM32U585でZephyr RTOSを動かすメリット
Arduino® UNO Qは、強力なマイコンであるSTM32U585(Cortex-M33、2MB Flash、768KB SRAM)を搭載しています。
Zephyr RTOSを採用することで、以下の恩恵を受けられます。
* デバイス木(Device Tree)によるすっきりした物理ピン管理
* 省電力管理(Power Management)が容易
PIRセンサー信号でスリープから復帰し、実行後に再びアイドルに戻る処理が簡単に書けます。
* Edge Impulse C++ SDKとの高い親和性
TinyMLモデルをRTOSのタスクとして安全に実行可能です。
SPI接続カメラのデバイスツリー設定(Device Tree Overlay)
SPIカメラをSTM32U585に認識させるため、デバイスツリーオーバーレイ(app.overlay)を作成します。
SPI2ポートの物理ピンを設定し、CSピンにD10(PB9)を割り当てます。
デバイスツリーのソースコードは、下記GitHubで公開しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* app.overlay */ &arduino_spi { status = "okay"; /* ICSPコネクタの物理ピン(SPI2)へオーバーライド */ pinctrl-0 = <&spi2_sck_pd1 &spi2_miso_pc2 &spi2_mosi_pc3>; pinctrl-names = "default"; cs-gpios = <&arduino_header ARDUINO_HEADER_R3_D10 GPIO_ACTIVE_LOW>; arducam_mega: arducam_mega@0 { compatible = "arducam,mega"; reg = <0>; spi-max-frequency = <4000000>; /* 安定動作のため最初は4MHzに設定 */ status = "okay"; }; }; |
Edge Impulse C++ SDKのZephyrへの組み込み
ディレクトリ構成
Edge Impulse Studioで学習したFOMOのC++ライブラリを、Zephyrのアプリケーション構成内に配置します。
|
1 2 3 4 5 6 7 8 9 |
src/spi-cam/object-detect/ ├── CMakeLists.txt # ビルド定義 ├── prj.conf # RTOS動作設定(スタックサイズ等) ├── app.overlay # デバイスツリー ├── src/ │ └── main.cpp # メインのC++ロジック ├── edge-impulse-sdk/ # Edge ImpulseコアSDK ├── model-parameters/ # モデルの定義・ラベル └── tflite-model/ # TFLite C++ソース |
CMakeLists.txtの設定
静的メモリ割り当てフラグを設定し、モデルソースコードをリンクします。
CMakeLists.txtのソースコードは、下記GitHubで公開しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# CMakeLists.txt # 動的メモリ確保(heap)を避けるため、静的メモリ確保を有効化 zephyr_compile_definitions(EI_CLASSIFIER_ALLOCATION_STATIC) # インクルードパスの追加 target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/model-parameters ${CMAKE_CURRENT_SOURCE_DIR}/tflite-model ) # モデルソースコードの指定 target_sources(app PRIVATE tflite-model/tflite_learn_1061869_5_compiled.cpp ) # Zephyr用のSDKサブディレクトリを追加 add_subdirectory(edge-impulse-sdk/cmake/zephyr) |
メモリ(SRAM 768KB)の制約とAverage Poolingリサイズ
STM32U585のSRAMは768KBとマイコンとしては大容量ですが、320x320ピクセルのRGB565画像は1フレームで約200KBを消費します。
ダブルバッファやTFLiteのワークメモリを合わせると、メモリが枯渇します。
そこで、キャプチャしたフレームをAverage Pooling(平均化プーリング)アルゴリズムにより、RAM上でノイズを抑えつつ 96x96 ピクセルに縮小して入力します。
これによりSRAMを大幅に節約し、RTOS上で安全に推論を実行可能です。


実際のデモ動画は下記です。
PIR人感センサーの割り込みと省電力ライフサイクル
センサーが検知した時だけ動作させるため、PIRセンサーとカメラの制御を連動させます。

PIR割り込みのC++実装
D7(PB2)ピンをエッジトリガーの割り込みとして設定します。
検知するとセマフォ pir_sem が解放され、ループが起動します。
PIRセンサー割り込みのコードの詳細は、下記GitHubで公開しています。

|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#define PIR_PIN 2 static const struct device *const gpiob_dev = DEVICE_DT_GET(DT_NODELABEL(gpiob)); static struct gpio_callback pir_cb_data; K_SEM_DEFINE(pir_sem, 0, 1); void pir_sensor_handler(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { k_sem_give(&pir_sem); } // (初期化部) gpio_pin_configure(gpiob_dev, PIR_PIN, GPIO_INPUT | GPIO_ACTIVE_LOW | GPIO_PULL_UP); gpio_pin_interrupt_configure(gpiob_dev, PIR_PIN, GPIO_INT_EDGE_TO_ACTIVE); gpio_init_callback(&pir_cb_data, pir_sensor_handler, BIT(PIR_PIN)); gpio_add_callback(gpiob_dev, &pir_cb_data); |
Edge Impulse 推論ループ
検知されたらカメラを起動し、フレームを取得して推論します。
識別結果(pico, xiao, nrf54l15, fpc)をオンボードのRGB LEDで表示し、再びカメラを省電力モードにして停止します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
while (1) { // 1. PIRセンサー検知までスリープ状態で待機 k_sem_take(&pir_sem, K_FOREVER); // 2. カメラストリームを動的に開始 video_stream_start(camera_dev, VIDEO_BUF_TYPE_OUTPUT); // 3. 1フレーム取得 struct video_buffer *captured_buf; video_dequeue(camera_dev, &captured_buf, K_FOREVER); // 4. 96x96へのリサイズと推論 signal_t signal; signal.total_length = EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; signal.get_data = &get_camera_data; ei_impulse_result_t result = { 0 }; run_classifier(&signal, &result, false); // 5. 結果をRGB LEDに反映 uint8_t r_val = 0, g_val = 0, b_val = 0; for (size_t ix = 0; ix < result.bounding_boxes_count; ix++) { auto bb = result.bounding_boxes[ix]; if (bb.value >= 0.5f) { if (strcmp(bb.label, "pico") == 0) b_val = 1; else if (strcmp(bb.label, "xiao") == 0) g_val = 1; else if (strcmp(bb.label, "nrf54l15") == 0) r_val = 1; else if (strcmp(bb.label, "fpc") == 0) { r_val = 1; g_val = 1; } break; } } gpio_pin_set_dt(&led_red, r_val); gpio_pin_set_dt(&led_green, g_val); gpio_pin_set(gpioh_dev, LED3_BLUE_PIN, b_val); // 6. 次の検知までカメラを停止して電力をセーブ video_stream_stop(camera_dev, VIDEO_BUF_TYPE_OUTPUT); k_sleep(K_MSEC(3000)); } |
まとめ
Zephyr RTOSの割り込み機能を活かし、無駄な電力を徹底的に排除した、イベント駆動型のデバイス識別ユニットが完成しました。
次回は、マイコン側での各種設定や、Linux側でのMIPIカメラ制御、画質調整および検証結果を紹介します!


コメント