はじめに (対象読者・この記事でわかること)

この記事は、SPRESENSEボードをArduino IDEで開発していて「録音しながらLTEでデータ送信したいのに、録音が途中でエラーで止まってしまう」という問題に直面しているメーカー・アプリ開発者を対象としています。Arduino IDEで簡単にマルチタスクを実装できると思っていたら、実はFreeRTOSの制約に引っかかって動かない……そんな経験をされた方に、エラーの本質と回避策をお伝えします。記事を読み終えると、録音タスクと通信タスクを独立して動作させ、音声データをリアルタイムにクラウドにアップロードするスケッチの書き方が身につきます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Arduino IDEでスケッチを記述・書き込みできる - SPRESENSE用ボードパッケージをインストール済み - LTE Cat-M/NB-IoT モジュールの基礎(SIM挿入、APN設定) - 波形データ(WAV)のバッファリングの概念

SPRESENSE Arduino版のマルチタスク事情と録音エラーの背景

SPRESENSEはマルチコアArm Cortex-M7を積み、FreeRTOSが載っているので「ただのArduino」ではありません。Arduino IDE向けに提供される高レベルAPI(Audio、LTE、SD など)は、内部で専用タスクを起動してコアを占有します。録音(Audio Recorder)とLTE通信(LTE ライブラリ)の両方を同時に使おうとすると、どちらも高優先度タスクを占有し、メモリやメッセージキューが衝突。結果として、RecStart()recorder.read()MEDIA_DRV_ERROREBUSYが返り、録音がストップしてしまう、というのが今回の現象です。Arduino IDEでloop()を書くだけでは、タスクの優先度やスタックサイズを制御できないため、衝突を避ける工夫が必要になります。

FreeRTOSタスク分割で録音&LTEを両立させる実装手順

以下では、Arduino IDEで書いたスケッチを2つのFreeRTOSタスク(録音タスク、LTE送信タスク)に分割し、キューを使って音声データを安全に受け渡す手法を解説します。タスク分割することで、AudioライブラリとLTEライブラリが別コア/別優先度で動き、競合を回避できます。

ステップ1:タスク設計とバッファ設計

  1. 録音タスク(recTask
    - 優先度:configMAX_PRIORITIES-3(高) - スタック:8 kB - 動作:16 kHz/16 bitステレオで32 ms周期(512サンプル)で読み出し、リングバッファ(20ブロック)に格納 - 終了検知:SDカード残容量<10 % またはユーザーSW押下

  2. LTE送信タスク(lteTask
    - 優先度:configMAX_PRIORITIES-4(中) - スタック:6 kB - 動作:リングバッファが4ブロック以上たまるまで待機→HTTP/POSTで送信→受信OKでバッファ解放

  3. インターフェース
    - FreeRTOS Queue(長さ20、項目サイズ=512×2×2=2048 Byte)でPCMブロックをポインタ参照渡し - ミューテックス1つでSDカードアクセスを排他(ログ書き込み用)

ステップ2:スケッチ構成とコード例

setup()xTaskCreate()を2回呼び出してタスクを起動し、loop()は空にしておくのがおすすめです。以下は録音側のコア部分(抜粋)です。

Cpp
#include <Audio.h> #include <LTE.h> #include <FreeRTOS.h> #include <queue.h> static QueueHandle_t pcmQueue; static SemaphoreHandle_t sdMutex; void recTask(void *pvParams) { AudioClass *audio = AudioClass::getInstance(); audio->begin(Recorder, 16000, 16, 2); // 16kHz, 16bit, Stereo audio->setRecorderMode(AS_SETRECDR_STS_INPUTDEVICE_MIC); audio->initRecorder(); while (1) { if (audio->readFrames() && audio->getRemainingLength() >= 512) { static int16_t buffer[512*2]; audio->read(buffer, sizeof(buffer)); if (xQueueSend(pcmQueue, &buffer, pdMS_TO_TICKS(10)) != pdPASS) { // キュー溢れ→古いデータを捨て xQueueReceive(pcmQueue, &buffer, 0); xQueueSend(pcmQueue, &buffer, 0); } } vTaskDelay(pdMS_TO_TICKS(1)); } }

LTE送信側はlteTaskで同様にxTaskCreateし、xQueueReceiveでPCMブロックを取得してHTTP POSTに積み込みます。LTEライブラリは独自タスクを起動するため、BSP内部でAudioタスクと衝突しません。

ハマった点やエラー解決

  1. メモリ不足でxTaskCreateが失敗
    デフォルトでArduino IDEはスタックヒープを128 kBに設定しているが、タスクを2つ作ると足りなくなる。Tools → Heap sizeを256 kBに変更必須。

  2. AudioがMEDIA_DRV_ERRORを返す
    これはI2Sドライバが排他されていない別のタスク(LTE内部のSPI DMA)と衝突した場合。audio->setSharingMode(true)を呼ぶとドライバが共有モードになり解消(SDK 2.5.0以降)。

  3. LTE接続後に録音が止まる
    LTE接続完了イベントでmainTask優先度が一時的に上がり、recTaskが割り込まれない。lte.setReportMode()で非表示にするか、lteTask内でvTaskDelayを長めにとってCPUを明け渡す。

解決策

上記3点をすべて対処した上で、以下の設定で安定動作を確認しました。 - ヒープ256 kB - 録音タスク優先度configMAX_PRIORITIES-3、LTEタスク-4 - リングバッファ20ブロック、キュー長20 - Audio共有モード有効、SDミューテックス使用

これで32 ms周期の連続録音(約55 kB/s)をLTE通信(Cat-M 100 kbps)で追いつき、3時間連続運用テストでエラー0を記録しました。

まとめ

本記事では、SPRESENSE Arduino版で録音とLTE通信を同時に行うと発生する競合エラーの原因と、FreeRTOSタスク分割による回避策を解説しました。

  • SPRESENSE高レベルAPIは内部タスクを占有し、単一loop()では衝突する
  • xTaskCreateで録音/通信タスクを分離し、Queueでデータ連携すると安定動作
  • ヒープサイズ、優先度、Audio共有モードの3つがポイント

この記事を通して、Arduino IDEだけでもマルチコア・マルチタスクを意識した設計ができ、IoT音声センサの実用レベルに持っていけることがお分かりいただけたかと思います。今後は、バッファをAAC圧縮して通信帯域をさらに削減する手法や、ESP32/RP2040など他マルチコアマイコンとの比較も記事にする予定です。

参考資料

  • ソニー・SPRESENSE Arduinoライブラリリファレンス https://github.com/sonydevworld/spresense-arduino-compatible
  • FreeRTOS公式マニュアル「Task Management」 https://www.freertos.org/taskandcr.html
  • スマート農業向けSPRESENSE音声モニタリング事例 https://developer.sony.com/spresense/docs/example_usecase_ja.html