STM32WBAでBLEスループットを最大化する方法【100KBを最速で送る実装ガイド】

bluetooth

こんな悩みはありませんか?

「ATT MTUやDLEを設定したのに、思ったより速くならない…」

BLEでまとまったデータを転送しようとすると、こういう壁にぶつかります。
原因のほとんどは、パラメータではなくソフトウェア側の処理がボトルネックになっているからです。

この記事を読めば、STM32WBAを使って100KBのデータを約0.8秒で転送できます。
デフォルト設定の約40秒から、50倍以上の高速化です。

必要なのは次の4ステップだけです。

  1. ATT MTUを拡張する
  2. DLEを有効にする
  3. 2M PHYに切り替える
  4. TXバッファをパイプライン充填する(ここが核心)

順を追って解説していきます。


BLEのスループット構造を理解する

Connection Interval と送信タイミング

BLEの通信は Connection Interval(CI)という周期で動いています。

|<-------- CI (例: 20ms) -------->|<-------- CI -------->|
|  Connection Event  |  sleep...  | Connection Event |...
| pkt pkt pkt pkt.. |            | pkt pkt ...      |

ここで重要なのが Connection Event の存在です。
1つのCIの中で、BLEスタックは複数のパケットを連続して送信できます。

問題は、CIの開始時点でTXバッファに積まれているパケットしか送れないという点です。

ソフトがボトルネックになるケース

CI開始 → pkt1送信完了 → アプリがpkt2をバッファに積む
       → 間に合わず  → 次のCIまで待機 → 帯域が半分以下に

パラメータをどれだけ最適化しても、アプリ側の積み込みが遅ければ
Connection Event の送信スロットは空のまま流れていきます。

スループットの本質:CIよりも速くTXバッファを埋め続けること

この構造を頭に入れた上で、4つのステップを進めていきます。


ステップ1:ATT MTU を拡張する

1パケットに詰めるデータ量を増やすことで、転送効率が大幅に上がります。

デフォルトでは1パケットに20バイトしか入りませんが、MTU拡張により最大509バイトまで増やせます。

デフォルト最大
ATT MTU23 bytes512 bytes
実データ20 bytes509 bytes

CentralからExchange MTU Requestを送るか、STM32WBAのPeripheral実装では
接続直後に相手からのMTU Exchangeを受け入れる処理を入れます。

// MTU Exchange 完了イベントで更新値を保存
case ACI_ATT_EXCHANGE_MTU_RESP_VSEVT_CODE: {
    aci_att_exchange_mtu_resp_event_rp0 *pr = (void*)p_meta_evt->data;
    currentMtu = pr->Server_RX_MTU;
    break;
}

MTU拡張だけでは効果は限定的です。次のDLE設定とセットで使うことで効果が出ます。


ステップ2:DLE(Data Length Extension)を有効にする

Link Layerレベルでパケットサイズを拡張し、無線区間の効率を上げます。

MTU拡張とは独立した手順です。どちらか片方では効果が半減します。

デフォルト最大
LL PDU payload27 bytes251 bytes

DLEを有効にすることで、1回の無線送信でより多くのデータを運べるようになります。

// 接続後にDLE更新を要求
hci_le_set_data_length(connHandle, 251, 2120);

MTU拡張とDLEを両方設定して初めて、パケット効率が最大化されます。


ステップ3:PHYを2M PHYに切り替える

転送速度を 1 Mbps → 2 Mbps に上げ、帯域を単純に2倍にします。

// 接続後に2M PHYへ切り替え
hci_le_set_phy(connHandle, 0x00, 0x02, 0x02, 0x0000);

アドバタイジングは常に 1M PHY のため、接続確立後に実行してください。
切り替え完了は HCI_LE_PHY_UPDATE_COMPLETE_SUBEVT_CODE で確認できます。

ここまでの3ステップで、スループットはデフォルト比で約15倍になります。
ただし、次のステップを省くと帯域の半分以上を無駄にしてしまいます。


ステップ4:TXバッファをパイプライン充填する(最重要)

ここが他の解説記事にはない、スループットを決定づけるポイントです。

なぜパイプライン充填が必要なのか

aci_gatt_update_char_value_ext() を1回ずつ呼ぶ実装では、
Connection Event の開始時点でバッファが空になりがちです。
スタックのTXバッファが満杯になるまで連続して積み込み続けることで、
CIの空きを最小化できます。

実装の考え方

aci_gatt_update_char_value_ext()BLE_STATUS_INSUFFICIENT_RESOURCES が返るまで連続呼び出しします。
スタックのTXバッファが満杯になった時点で一時停止し、
ACI_GATT_TX_POOL_AVAILABLE_VSEVT_CODE イベントで即座に再開します。

[アプリ]                         [BLEスタック]          [RF]
  │                                   │                   │
  ├──pkt1──────────────────────────→ │                   │
  ├──pkt2──────────────────────────→ │                   │
  ├──pkt3──────────────────────────→ │   CI開始          │
  ├──pkt4──(INSUFFICIENT_RESOURCES)  ├──pkt1──────────→ │
  │   待機                            ├──pkt2──────────→ │
  │                                   ├──pkt3──────────→ │
  │ ←──TX_POOL_AVAILABLE─────────── │                   │
  ├──pkt4──────────────────────────→ │                   │
  ├──pkt5──────────────────────────→ │                   │
        ...

実装例(STM32WBA)

static bool s_waitingTxPool = false;
static uint32_t s_bytesSent  = 0;
static uint32_t s_totalSize  = 0;
static uint8_t *s_pData      = NULL;

void BLE_StartTransfer(uint8_t *pData, uint32_t size) {
    s_pData      = pData;
    s_totalSize  = size;
    s_bytesSent  = 0;
    s_waitingTxPool = false;
    BLE_FillPipeline();
}

static void BLE_FillPipeline(void) {
    while (s_bytesSent < s_totalSize) {
        uint16_t chunk = MIN(s_totalSize - s_bytesSent, currentMtu - 3);
        tBleStatus ret = aci_gatt_update_char_value_ext(
            connHandle, svcHandle, charHandle,
            1,              // Notification
            s_totalSize,
            s_bytesSent,
            chunk,
            &s_pData[s_bytesSent]
        );
        if (ret == BLE_STATUS_INSUFFICIENT_RESOURCES) {
            s_waitingTxPool = true;
            return;
        }
        s_bytesSent += chunk;
    }
    // 完了
}

// BLEイベントコールバック内(タスクコンテキストで呼ばれる)
case ACI_GATT_TX_POOL_AVAILABLE_VSEVT_CODE: {
    if (s_waitingTxPool) {
        s_waitingTxPool = false;
        BLE_FillPipeline();
    }
    break;
}

なぜ aci_gatt_update_char_value_ext() を使うのか

STM32WBAのBLEスタックでは aci_gatt_update_char_value_ext() がオフセット指定に対応しており、
自前でフラグメント処理を書かずにまとまったデータを分割送信できます。

このパイプライン充填により、ステップ1〜3だけでは得られなかった残りの帯域を使い切れます。


改善効果の目安

構成実効スループット100KB転送時間
デフォルト(MTU23, 1M PHY, pipeline無し)~20 kbps~40秒
MTU+DLE+2M PHY(pipeline無し)~300 kbps~2.7秒
MTU+DLE+2M PHY + パイプライン充填~1,000 kbps~0.8秒

※ CI設定・環境・対向デバイスにより変動


STM32WBA 固有の注意点

  • TX pool サイズ: CFG_BLE_NUM_GATT_ATTRIBUTES 等のリンク設定に依存。小さいとすぐ INSUFFICIENT_RESOURCES になる。プロジェクトの要件に応じて増やすこと
  • Connection Event 最大長: LL_MAX_EVENT_LENGTH パラメータで上限が決まる。デフォルトが短い場合は1CI内に詰め込めるパケット数が制限される
  • 2M PHY切り替えタイミング: 切り替え完了前に大量送信を始めると1M PHYで送られてしまう。HCI_LE_PHY_UPDATE_COMPLETE_SUBEVT_CODE を待ってから開始する

まとめ

この記事で解説した4ステップをおさらいします。

  1. ATT MTUを拡張する(1パケットのデータ量を増やす)
  2. DLEを有効にする(無線区間のパケット効率を上げる)
  3. 2M PHYに切り替える(接続確立後に実行)
  4. TXバッファをパイプライン充填する(BLE_STATUS_INSUFFICIENT_RESOURCES まで詰め込む)

1〜3はパラメータ設定、4がソフトウェア実装の核心です。

この4ステップを実装するだけで、100KBの転送時間は40秒→0.8秒まで短縮できます。
BLEでのファームウェア配信やセンサーログ収集など、まとまったデータを扱う場面で
すぐに使える知識なので、ぜひ自分のプロジェクトに取り入れてみてください。


今後の動向:HDT(High Data Throughput)

Bluetooth SIG では、現在の最高速度である 2M PHY(2 Mbps)をさらに上回る 高スループット規格の策定が進んでいます。
これが HDT(High Data Throughput)と呼ばれる取り組みで、Core Specification ver 7.0 への採用が期待されています。

現時点では仕様の詳細は確定していませんが、実現すれば PHY レート自体が底上げされ、
本記事で紹介したパイプライン充填の恩恵もさらに大きくなります。

BLEでの大容量転送がより現実的な選択肢になっていく方向性として、動向を追っておく価値があります。

コメント

タイトルとURLをコピーしました