異なるアーキテクチャのマイコンボード間で、同一のC言語プログラムから高解像度カメラを動かしてみました。
対象ボードは、Raspberry Pi Pico 2 W、Nordic nRF54L15-DK、そしてArduino® UNO Qです。
今回は、リアルタイムOS「Zephyr RTOS」と「Arducam Mega 3MP」を組み合わせ、最大3MP(2048x1536)のJPEG静止画をPCへ安定転送する「マルチボード統合キャプチャシステム」の構築記録を紹介します。
マイコンの内蔵SRAM制約を打破する「1面持ち(Single-Buffer)」設計や、Zephyr上流ドライバのバグ解明など、実践的なノウハウも詳しく解説します。
Zephyr RTOS & Arducam Megaで高解像度撮影!【Pico 2W / nRF54L15 / UNO Q 統合編】
異なるアーキテクチャのマイコンボード間で、同一のC言語プログラムから高解像度カメラを動かしてみました。
対象ボードは、Raspberry Pi Pico 2 W、Nordic nRF54L15-DK、そしてArduino® UNO Qです。



今回は、リアルタイムOS「Zephyr RTOS」と「Arducam Mega 3MP」を組み合わせ、最大3MP(2048x1536)のJPEG静止画をPCへ安定転送する「マルチボード統合キャプチャシステム」の構築記録を紹介します。



マイコンの内蔵SRAM制約を打破する「1面持ち(Single-Buffer)」設計や、Zephyr上流ドライバのバグ解明など、実践的なノウハウも詳しく解説します。
プロジェクト・GitHubのリンク先
GitHubのリンク先は下記です

実際のデモ動画は下記です
Nordic nRF54L15-DK(UXGA 1600x1200 キャプチャ実演)
Raspberry Pi Pico 2 W(QXGA 2048x1536 3MP キャプチャ実演)
Arduino® UNO Q(UXGA 1600x1200 キャプチャ実演)
マルチボード構成とピンアサイン
今回のプロジェクトでは、ARM Cortex-M33を搭載した3種類の最新マイコンボードを使用しています。
それぞれアーキテクチャや周辺回路が異なりますが、Arducam Mega 3MP(SPI接続カメラ)を接続し、同一のZephyrファームウェアで駆動します。
ハードウェア接続一覧表
各ボードとカメラの主要な接続ピン配置は以下の通りです。
| 信号名 | カメラピン | Raspberry Pi Pico 2 W | Nordic nRF54L15-DK (P1) | Arduino® UNO Q |
|---|---|---|---|---|
| 電源 (VCC) | Pin 1 (VCC) | 3V3 (Pin 36) | Pin 1 (VDDIO / 3.3V) | 3.3V / 5V |
| グランド (GND) | Pin 2 (GND) | GND (Pin 23) | Pin 2 (GND) | GND |
| SPI MOSI | Pin 3 (MOSI) | GP19 (Pin 25) | Pin 6 (P1.06 / SPI22) | D11 (PB15 / SPI2) |
| SPI MISO | Pin 4 (MISO) | GP16 (Pin 21) | Pin 7 (P1.07 / SPI22) | D12 (PB14 / SPI2) |
| チップセレクト (CS) | Pin 6 (CS) | GP17 (Pin 22) | Pin 10 (P1.10 / GPIO) | D10 (PB9 / GPIO) |
| SPI クロック (SCK) | Pin 5 (SCK) | GP18 (Pin 24) | Pin 11 (P1.11 / SPI22) | D13 (PB13 / SPI2) |
| PC通信方式 | - | 内蔵USB CDC ACM | オンボード J-Link VCOM1 | USART1シリアル (D0/D1) |
デバイスツリーによるボード別設定の分離
Zephyr RTOSの強力な利点は、ハードウェア依存のピン割り当てやバス設定を「デバイスツリー(Device Tree Overlay)」に分離できる点です。
C言語のアプリケーションロジック(src/main.c)は100%共通のまま、ターゲットボードごとの差異を設定ファイル側で完全に吸収できます。
プロジェクト構成
ボードごとの設定は boards/ フォルダ下に配置しています。
|
1 2 3 4 5 6 7 |
boards/ ├── rpi_pico2_rp2350a_m33_w.overlay # Pico 2 W SPI0 & USB CDC ACM設定 ├── rpi_pico2_rp2350a_m33_w.conf # Pico 2 W用Kconfig設定 ├── nrf54l15dk_nrf54l15_cpuapp.overlay # nRF54L15 SPI22ピン競合解消設定 ├── nrf54l15dk_nrf54l15_cpuapp.conf # nRF54L15用Kconfig設定 ├── arduino_uno_q.overlay # Arduino UNO Q SPI2 & Arducam設定 └── arduino_uno_q.conf # Arduino UNO Q用Kconfig設定 |
ピン競合の解消例(Nordic nRF54L15-DK)
Nordic nRF54L15-DKでは、デバッグログ用のUART20(P1.06/P1.07のRTS/CTS)とSPI22(MOSI/MISO)で物理ピンが重複します。
そこで、オーバーレイ側でUART20のフロー制御ピンを除外し、SPI通信専用としてピンを再定義しています。
|
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 |
/* boards/nrf54l15dk_nrf54l15_cpuapp.overlay */ / { chosen { zephyr,camera = &arducam_mega0; }; }; /* UART20からRTS(P1.06)・CTS(P1.07)を除外 */ &pinctrl { uart20_default: uart20_default { group1 { psels = <NRF_PSEL(UART_TX, 1, 4)>; }; group2 { psels = <NRF_PSEL(UART_RX, 1, 5)>; bias-pull-up; }; }; }; /* 拡張ポートのSPI22をカメラ用に有効化 */ &nordic_expansion_spi { status = "okay"; cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; arducam_mega0: arducam_mega@0 { compatible = "arducam,mega"; reg = <0>; status = "okay"; spi-max-frequency = <8000000>; }; }; |
メモリ不足を克服する「1面持ち」静的バッファ設計
マイコンで高解像度画像を扱う場合、最大の制約となるのが内蔵SRAMの容量です。
特にNordic nRF54L15-DKは内蔵SRAMが256KBとコンパクトであり、メモリの確保方法に工夫が必要でした。
Zephyrのヒープ割り当て(k_heap)の壁
Zephyrのビデオサブシステムでは、通常 video_buffer_aligned_alloc() を使ってカーネルヒープ(k_heap)から動的にバッファを確保します。
しかし、ヒープ管理にはヘッダー領域やアライメントパディングなどのメタデータが必要です。
CONFIG_VIDEO_BUFFER_POOL_HEAP_SIZE=131072(128KB)と設定しても、メタデータ分の容量が差し引かれるため、128KBぴったりの巨大な単一バッファを確保しようとすると、確実にメモリ不足エラー(-ENOMEM)が発生します。
video_import_buffer()による解決
そこで本システムでは、C言語の .bss 領域に静的配列としてフレームバッファを定義しました。
そして、Zephyrの公式外部バッファインポートAPIである video_import_buffer() を使用してビデオサブシステムへ登録しています。
|
1 2 3 4 5 6 7 8 9 10 11 |
/* .bss領域に静的フレームバッファを定義 */ static uint8_t s_single_frame_buf[MAX_JPEG_BUF_SIZE] __aligned(4); /* Zephyrのビデオバッファとしてインポート(1面持ち) */ struct video_buffer *vbuf = video_import_buffer(s_single_frame_buf, sizeof(s_single_frame_buf)); if (vbuf == NULL) { /* インポート失敗時はフォールバック */ vbuf = video_buffer_aligned_alloc(MAX_JPEG_BUF_SIZE, 4, K_NO_WAIT); } vbuf->type = VIDEO_BUF_TYPE_OUTPUT; video_enqueue(camera_dev, vbuf); |
この工夫により、ヒープの断片化や管理オーバーヘッドを完全に排除し、コンパイル時にリンカレベルでメモリ配置を確定させることができました。
ボード別の解像度適応とメモリフットプリント
各マイコンのSRAM容量に合わせて、撮影解像度と静的バッファサイズを最適化しています。
| 対象ボード | 内蔵SRAM | 撮影解像度 | 静的バッファ | SRAM使用率 | 空きメモリ |
|---|---|---|---|---|---|
| Raspberry Pi Pico 2 W | 520 KB | 2048 x 1536 (3MP QXGA) | 256 KB | 56.68% | ~225 KB (43.3%) |
| Nordic nRF54L15-DK | 256 KB | 1600 x 1200 (1.92MP UXGA) | 160 KB | 68.61% | ~76 KB (31.4%) |
| Arduino® UNO Q | 768 KB | 1600 x 1200 (1.92MP UXGA) | 160 KB | 22.80% | ~588 KB (77.2%) |
Pico 2 Wではカメラセンサの最大性能である3MP(2048x1536)をフル活用し、nRF54L15-DKとUNO Qではセンサ本来の4:3比率を維持したUXGA(1600x1200)で安定撮影を行えます。
直面した技術的トラブルと解決ノウハウ
開発過程でいくつかの技術的課題に直面し、それぞれ原因の特定と対策を行いました。
Zephyr上流ドライバの解像度マッピングバグの修正
当初、画像を取得すると斜めに激しく歪む現象(バーバーポール模様)が発生しました。
原因を調査したところ、Zephyrの公式Arducamドライバ(video_arducam_mega.c)にバグがあることが判明しました。
ドライバ内で解像度レジスタを設定する際、配列のインデックス番号をそのままカメラのレジスタへ書き込んでいました。
例えば96x96を指定した際、レジスタには 0x0A を書き込む必要がありますが、誤って 0x00(QQVGA: 160x120)が書き込まれていました。
その結果、センサは幅160ピクセルで撮影しているのに、ホスト側が幅96ピクセルとして処理するため、1行ごとに64ピクセル(128バイト)のズレが生じていました。
解像度の縦横サイズから正しいレジスタ値を返すマッピング関数を実装することで、歪みのない綺麗な画像が得られるようになりました。
USB CDC ACMバッファ溢れを防ぐ4KBブロックACK
Pico 2 WのネイティブUSB経由で大容量JPEGデータを連続送信すると、途中でデータ転送が止まる問題が発生しました。
USBの送信FIFOが溢れてデータ同期が崩れることが原因でした。
そこで、マイコンとPC間の通信に「4,096バイトごとのブロックACKハンドシェイク」を導入しました。
マイコン側は64バイト送信ごとにCPUを解放(k_yield())しつつ、4KB送信するたびにPC側からの受信完了合図('K')を待機します。
|
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 |
/* 4KBごとのブロックハンドシェイク送信処理 */ static void send_payload_blocks(const struct device *dev, const uint8_t *data, size_t len) { const size_t block_size = 4096; size_t offset = 0; while (offset < len) { size_t to_send = len - offset; if (to_send > block_size) { to_send = block_size; } /* 64バイトごとにCPUを解放してFIFO溢れを防止 */ for (size_t i = 0; i < to_send; i++) { uart_poll_out(dev, data[offset + i]); if ((i & 0x3F) == 0x3F) { k_yield(); } } offset += to_send; /* PC側からのブロック受信ACK ('K') を待機 */ unsigned char ack = 0; int timeout_ms = 0; while (1) { if (uart_poll_in(dev, &ack) == 0) { if (ack == 'K' || ack == 'k') { break; } } k_sleep(K_MSEC(1)); timeout_ms++; if (timeout_ms > 4000) break; } } } |
このハンドシェイク制御により、数百KBのJPEGデータも途切れることなく100%確実に転送できるようになりました。
ハードウェアFIFOのゼロパディング対策
Arducam Megaは画像データを内部FIFOに蓄積する際、512バイト境界に合わせるため末尾に余分なゼロバイト(0x00)を付加します。
そのため、単純にデータの末尾がJPEGの終端マーカー(EOI: 0xFF 0xD9)であるかを判定すると、正常な画像でもエラーと判定されてしまいます。
マイコン側ではパケット送信前に末尾のゼロパディングをトリムし、PC側のPythonスクリプトでも後方検索(rfind)を行うことで、完全なJPEGデータとして保存できるようにしました。
ビルドと書き込み手順(West)
本プロジェクトには、各ボード向けのビルドと書き込みを自動化するバッチファイル(build.bat)を用意しています。
ソースコードはGitHubで公開しています。
Raspberry Pi Pico 2 W のビルド手順
PowerShellで仮想環境を起動し、ビルドを実行します。
|
1 2 |
cd zephyr-spicam/unified_capture_spicam .\build.bat pico |
生成された build\pico\zephyr\zephyr.uf2 を、BOOTSELモードで接続したPico 2 Wのドライブへドラッグ&ドロップして書き込みます。
Nordic nRF54L15-DK のビルドと書き込み手順
nRF54L15-DKは、内蔵のSEGGER J-Link経由でコマンドラインから一発で書き込めます。
|
1 2 3 4 5 |
# ビルドの実行 .\build.bat nrf # ボードへの書き込み(west flash / nrfutilRunner) .\build.bat flash-nrf |
Arduino® UNO Q のビルド手順
Arduino® UNO QのSTM32マイコン向けファームウェアをビルドします。
|
1 |
.\build.bat uno |
生成されたバイナリ(zephyr.bin)を、UNO Qのブートローダー経由で書き込みます。
Pythonツールによる自動画像取得
PC側のキャプチャツールとして、Pythonスクリプト(capture_image.py)を作成しました。
接続されたボードのCOMポートを自動判別し、撮影トリガーの送信から画像の受信、ファイル保存、ビューワー起動までを自動で行います。
キャプチャの実行コマンド
コマンドプロンプトまたはPowerShellで以下を実行します。
|
1 |
python capture_image.py |
スクリプトを実行すると、自動的にデバイスを検出して撮影を開始します。
|
1 2 3 4 5 6 7 |
Auto-detected device on COM33: Nordic nRF54L15-DK (M33 App Core / VCOM1) Connecting to COM33 at 115200 baud... Sending capture trigger command 'C'... Received valid image header: 1600x1200 JPEG, size=94182 bytes Progress: 94182 / 94182 bytes (100.0%) Saved captured image to 'captured.jpg' Opening image viewer... |
撮影された写真は captured.jpg として保存され、PCの既定の画像ビューワーで自動表示されます。
まとめ
Cortex-M33を搭載した3種類の最新マイコンボード(Pico 2 W / nRF54L15 / UNO Q)を用いて、Zephyr RTOS上で同一ソースコードによるSPIカメラの画像取得システムを構築しました。
* マルチボード統合: デバイスツリーオーバーレイを活用し、メインCコードを100%共通化。
* メモリ最適化: video_import_buffer() による静的バッファ登録で、ヒープ破綻を完全に防止。
* 高解像度撮影: Pico 2 Wで最大3MP(2048x1536)、nRF54L15とUNO QでUXGA(1600x1200)を実現。
* 安定通信: 4KBブロックACKハンドシェイクにより、USB CDC ACMおよびVCOMでのデータ欠損を解消。
マイコンの内蔵リソースを限界まで活かしたエッジカメラ開発のリファレンスとして、非常に実用的な構成となりました。
ソースコードはGitHubで公開していますので、ぜひお手持ちのボードでお試しください!


コメント