mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
fix(nimble): Restore throughput after switching from LE Coded PHY
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
|
||||
# L2CAP COC Throughput Central Example
|
||||
|
||||
`l2cap_coc_cent` demonstrates the central (initiator) side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It passively scans for a peripheral advertising UUID 0x1812, establishes a GAP connection, enables Data Length Extension (DLE), then opens an L2CAP COC channel over PSM 0x1002 and continuously sends SDUs to measure TX throughput.
|
||||
`l2cap_coc_cent` demonstrates the central (initiator) side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It passively scans for a peripheral advertising UUID 0x1812, establishes a GAP connection, enables Data Length Extension (DLE), then opens an L2CAP COC channel over PSM 0x0080 and continuously sends SDUs to measure TX throughput.
|
||||
|
||||
The central automatically cycles through all enabled PHYs (1M, 2M, Coded S2, Coded S8) in sequence, printing a throughput summary box after each test interval. It must be used together with the `l2cap_coc_prph` example which acts as the receiving side.
|
||||
The central automatically cycles through all enabled PHYs (1M, 2M, Coded S2, Coded S8) in sequence, printing a throughput summary box after each test interval. Only the PHY is changed during the test; the connection parameters selected during connection setup are preserved. It must be used together with the `l2cap_coc_prph` example which acts as the receiving side.
|
||||
|
||||
It uses ESP32's Bluetooth controller and NimBLE stack based BLE host.
|
||||
It uses the ESP Bluetooth controller with the NimBLE-based BLE host.
|
||||
|
||||
## How to Use Example
|
||||
|
||||
@@ -36,10 +36,10 @@ In the `L2CAP COC Throughput Configuration` menu:
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `EXAMPLE_L2CAP_COC_MTU` | 2048 | L2CAP CoC SDU MTU size in bytes (central receive buffer). Data flows cent → prph in this test, so throughput is governed by the peripheral's MTU. This value only limits how much the peripheral can send back and does not affect TX throughput. |
|
||||
| `EXAMPLE_EXTENDED_ADV` | y (BLE 5.0 chips) | Enable extended scanning to find peripherals using extended advertising. Required for Coded PHY testing on ESP32-C6/H2. |
|
||||
| `EXAMPLE_TEST_PHY_1M` | n | Enable throughput test on 1M PHY. |
|
||||
| `EXAMPLE_TEST_PHY_2M` | y | Enable throughput test on 2M PHY (BLE 5.0 chips only). |
|
||||
| `EXAMPLE_L2CAP_COC_MTU` | 2048 | Central L2CAP CoC SDU MTU size in bytes. The TX SDU size is limited by the smaller MTU negotiated between central and peripheral. |
|
||||
| `EXAMPLE_EXTENDED_ADV` | y (BLE 5.0 chips) | Enable extended scanning to find peripherals using extended advertising. |
|
||||
| `EXAMPLE_TEST_PHY_1M` | y on ESP32, n on BLE 5.0 chips | Enable throughput test on 1M PHY. |
|
||||
| `EXAMPLE_TEST_PHY_2M` | y on BLE 5.0 chips | Enable throughput test on 2M PHY (BLE 5.0 chips only). |
|
||||
| `EXAMPLE_TEST_PHY_CODED_S2` | n | Enable throughput test on Coded PHY S2 (500 kbps, BLE 5.0 chips only). |
|
||||
| `EXAMPLE_TEST_PHY_CODED_S8` | n | Enable throughput test on Coded PHY S8 (125 kbps, BLE 5.0 chips only). |
|
||||
| `EXAMPLE_TEST_DURATION_1M` | 8 | Test duration in seconds for 1M PHY. |
|
||||
@@ -68,6 +68,7 @@ I (xxx) l2cap_coc_cent: L2CAP COC connected, chan=0xxxxxxxxx
|
||||
I (xxx) l2cap_coc_cent: L2CAP COC Throughput — TX side (central sends to peripheral)
|
||||
I (xxx) l2cap_coc_cent: Number of enabled PHYs: x
|
||||
I (xxx) l2cap_coc_cent: PHY updated: tx=2 rx=2 status=0
|
||||
I (xxx) l2cap_coc_cent: 2M: preserving CI=xx
|
||||
I (xxx) l2cap_coc_cent: [2M PHY] Sending for 8 s
|
||||
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
|
||||
I (xxx) l2cap_coc_cent: | PHY : 2M |
|
||||
@@ -77,6 +78,7 @@ I (xxx) l2cap_coc_cent: | Time : 8 s |
|
||||
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
|
||||
I (xxx) l2cap_coc_cent: Cycle complete. Looping back to first PHY...
|
||||
I (xxx) l2cap_coc_cent: PHY updated: tx=2 rx=2 status=0
|
||||
I (xxx) l2cap_coc_cent: 2M: preserving CI=xx
|
||||
I (xxx) l2cap_coc_cent: [2M PHY] Sending for 8 s
|
||||
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
|
||||
I (xxx) l2cap_coc_cent: | PHY : 2M |
|
||||
@@ -87,7 +89,7 @@ I (xxx) l2cap_coc_cent: +-------------------------------------------------+
|
||||
I (xxx) l2cap_coc_cent: Cycle complete. Looping back to first PHY...
|
||||
```
|
||||
|
||||
> **Note:** The above output was captured on ESP32-H2 with only 2M PHY enabled. With additional PHYs enabled (1M, Coded S2, Coded S8), the central cycles through each in sequence before looping back.
|
||||
> **Note:** The above output shows only 2M PHY enabled. With additional PHYs enabled (1M, Coded S2, Coded S8), the central cycles through each in sequence before looping back. A small app-level tag is placed in each SDU so the peripheral can split summaries for Coded S2 and Coded S8, which are both reported as Coded PHY by GAP.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -30,38 +30,37 @@ static const char *TAG = "l2cap_coc_cent";
|
||||
#define LL_PACKET_LENGTH 251
|
||||
#define LL_PACKET_TIME 2120
|
||||
#define L2CAP_COC_UUID 0x1812
|
||||
/* App-level tag placed at the start of each CoC SDU.
|
||||
* GAP reports both Coded S2 and Coded S8 as BLE_HCI_LE_PHY_CODED, so the
|
||||
* receiver cannot distinguish those test segments from PHY events alone. */
|
||||
#define COC_SEGMENT_TAG_0 'L'
|
||||
#define COC_SEGMENT_TAG_1 '2'
|
||||
#define COC_SEGMENT_TAG_2 'C'
|
||||
#define COC_SEGMENT_TAG_3 'P'
|
||||
|
||||
/* EventGroup bits */
|
||||
#define PHY_UPDATED_BIT (1 << 0)
|
||||
#define COC_CONNECTED_BIT (1 << 1)
|
||||
#define CONN_UPDATED_BIT (1 << 2)
|
||||
#define TX_UNSTALLED_BIT (1 << 3)
|
||||
#define TX_UNSTALLED_BIT (1 << 2)
|
||||
|
||||
/* Timeout / interval constants */
|
||||
#define CONN_PARAM_UPDATE_TIMEOUT_MS 5000
|
||||
#define PREDRAIN_TIMEOUT_MS 20000
|
||||
#define POSTDRAIN_TIMEOUT_MS 5000
|
||||
#define TX_YIELD_INTERVAL 50
|
||||
#define LINK_SETTLE_TIME_MS 100
|
||||
|
||||
static EventGroupHandle_t coc_event_group;
|
||||
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
static struct ble_l2cap_chan *coc_chan = NULL;
|
||||
static bool ci_is_slow = false;
|
||||
static bool l2cap_connecting = false; /* guards double L2CAP connect */
|
||||
static volatile bool chan_stalled = false;
|
||||
static volatile int phy_update_status = BLE_HS_EUNKNOWN;
|
||||
static volatile uint8_t active_tx_phy = 0;
|
||||
static volatile uint8_t active_rx_phy = 0;
|
||||
static uint32_t *cent_seg_tx_done = NULL; /* points to segment SDU counter for async TX_UNSTALLED */
|
||||
static uint32_t *cent_seg_tx_drop = NULL; /* counts SDUs dropped (TX_UNSTALLED status != 0) */
|
||||
static uint16_t cent_tx_sdu_len = L2CAP_COC_MTU; /* min(local, peer) after COC connect */
|
||||
|
||||
static const struct ble_gap_upd_params conn_params = {
|
||||
.itvl_min = 6,
|
||||
.itvl_max = 6,
|
||||
.latency = 0,
|
||||
.supervision_timeout = 2000,
|
||||
.min_ce_len = 12,
|
||||
.max_ce_len = 24,
|
||||
};
|
||||
|
||||
void ble_store_config_init(void);
|
||||
|
||||
static os_membuf_t sdu_coc_mem[OS_MEMPOOL_SIZE(COC_BUF_COUNT, SDU_BLOCK_SIZE)];
|
||||
@@ -74,25 +73,25 @@ typedef struct {
|
||||
uint8_t phy_opts; /* 0=none, 1=S2, 2=S8 */
|
||||
int duration_s;
|
||||
const char *name;
|
||||
bool is_coded_s8;
|
||||
uint8_t segment_id; /* app-level RX summary boundary */
|
||||
} phy_entry_t;
|
||||
|
||||
static const phy_entry_t phy_list[] = {
|
||||
#if CONFIG_EXAMPLE_TEST_PHY_1M
|
||||
{ BLE_HCI_LE_PHY_1M_PREF_MASK, BLE_HCI_LE_PHY_1M_PREF_MASK, 0,
|
||||
CONFIG_EXAMPLE_TEST_DURATION_1M, "1M", false },
|
||||
CONFIG_EXAMPLE_TEST_DURATION_1M, "1M", 1 },
|
||||
#endif
|
||||
#if CONFIG_EXAMPLE_TEST_PHY_2M
|
||||
{ BLE_HCI_LE_PHY_2M_PREF_MASK, BLE_HCI_LE_PHY_2M_PREF_MASK, 0,
|
||||
CONFIG_EXAMPLE_TEST_DURATION_2M, "2M", false },
|
||||
CONFIG_EXAMPLE_TEST_DURATION_2M, "2M", 2 },
|
||||
#endif
|
||||
#if CONFIG_EXAMPLE_TEST_PHY_CODED_S2
|
||||
{ BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x01,
|
||||
CONFIG_EXAMPLE_TEST_DURATION_CODED_S2, "Coded S2", false },
|
||||
CONFIG_EXAMPLE_TEST_DURATION_CODED_S2, "Coded S2", 3 },
|
||||
#endif
|
||||
#if CONFIG_EXAMPLE_TEST_PHY_CODED_S8
|
||||
{ BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x02,
|
||||
CONFIG_EXAMPLE_TEST_DURATION_CODED_S8, "Coded S8", true },
|
||||
CONFIG_EXAMPLE_TEST_DURATION_CODED_S8, "Coded S8", 4 },
|
||||
#endif
|
||||
};
|
||||
#define PHY_LIST_LEN ((int)(sizeof(phy_list) / sizeof(phy_list[0])))
|
||||
@@ -100,6 +99,21 @@ static const phy_entry_t phy_list[] = {
|
||||
static int cent_gap_event(struct ble_gap_event *event, void *arg);
|
||||
static int cent_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg);
|
||||
|
||||
static bool coc_lost(void)
|
||||
{
|
||||
return conn_handle == BLE_HS_CONN_HANDLE_NONE || coc_chan == NULL;
|
||||
}
|
||||
|
||||
static void cent_set_segment_id(uint8_t *data, uint8_t segment_id)
|
||||
{
|
||||
/* Keep this in the normal payload; no extra control packet is sent. */
|
||||
data[0] = COC_SEGMENT_TAG_0;
|
||||
data[1] = COC_SEGMENT_TAG_1;
|
||||
data[2] = COC_SEGMENT_TAG_2;
|
||||
data[3] = COC_SEGMENT_TAG_3;
|
||||
data[4] = segment_id;
|
||||
}
|
||||
|
||||
static void cent_l2cap_coc_mem_init(void)
|
||||
{
|
||||
int rc;
|
||||
@@ -285,7 +299,7 @@ static int cent_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
|
||||
cent_seg_tx_drop = NULL;
|
||||
l2cap_connecting = false;
|
||||
xEventGroupClearBits(coc_event_group, COC_CONNECTED_BIT);
|
||||
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | CONN_UPDATED_BIT | PHY_UPDATED_BIT);
|
||||
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | PHY_UPDATED_BIT);
|
||||
return 0;
|
||||
|
||||
case BLE_L2CAP_EVENT_COC_TX_UNSTALLED:
|
||||
@@ -358,7 +372,18 @@ static void cent_send_task(void *arg)
|
||||
|
||||
for (int i = 0; i < PHY_LIST_LEN && !lost_connection; i++) {
|
||||
const phy_entry_t *phy = &phy_list[i];
|
||||
const uint8_t expected_phy =
|
||||
phy->tx_phys == BLE_HCI_LE_PHY_CODED_PREF_MASK ?
|
||||
BLE_GAP_LE_PHY_CODED : phy->tx_phys;
|
||||
|
||||
if (coc_lost()) {
|
||||
lost_connection = true;
|
||||
break;
|
||||
}
|
||||
|
||||
phy_update_status = BLE_HS_EUNKNOWN;
|
||||
active_tx_phy = 0;
|
||||
active_rx_phy = 0;
|
||||
xEventGroupClearBits(coc_event_group, PHY_UPDATED_BIT);
|
||||
#if CONFIG_SOC_BLE_50_SUPPORTED
|
||||
rc = ble_gap_set_prefered_le_phy(conn_handle,
|
||||
@@ -366,11 +391,19 @@ static void cent_send_task(void *arg)
|
||||
phy->rx_phys,
|
||||
phy->phy_opts);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "PHY switch to %s failed; rc=%d — continuing anyway",
|
||||
ESP_LOGE(TAG, "PHY switch to %s failed; rc=%d; skipping measurement",
|
||||
phy->name, rc);
|
||||
xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT);
|
||||
if (coc_lost()) {
|
||||
lost_connection = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
phy_update_status = 0;
|
||||
/* No PHY-update event on non-BLE50; treat preferred PHY as applied (GAP id). */
|
||||
active_tx_phy = expected_phy;
|
||||
active_rx_phy = expected_phy;
|
||||
xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT);
|
||||
#endif
|
||||
|
||||
@@ -378,62 +411,34 @@ static void cent_send_task(void *arg)
|
||||
pdTRUE, pdTRUE,
|
||||
pdMS_TO_TICKS(5000));
|
||||
if (!(bits & PHY_UPDATED_BIT)) {
|
||||
ESP_LOGW(TAG, "PHY update timeout for %s; continuing anyway", phy->name);
|
||||
ESP_LOGE(TAG, "PHY update timeout for %s; skipping measurement", phy->name);
|
||||
if (coc_lost()) {
|
||||
lost_connection = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phy->is_coded_s8) {
|
||||
/* CI=40ms, CE=32.5-40ms: fits 2 Coded S8 K-frames per CI and prevents credit starvation */
|
||||
struct ble_gap_upd_params s8_params = {
|
||||
.itvl_min = 32,
|
||||
.itvl_max = 32,
|
||||
.latency = 0,
|
||||
.supervision_timeout = 2000,
|
||||
.min_ce_len = 52,
|
||||
.max_ce_len = 64,
|
||||
};
|
||||
xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT);
|
||||
rc = ble_gap_update_params(conn_handle, &s8_params);
|
||||
if (rc == 0) {
|
||||
ESP_LOGI(TAG, "Coded S8: updating CI");
|
||||
ci_is_slow = true;
|
||||
xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE,
|
||||
pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "S8 CI update failed (rc=%d)", rc);
|
||||
}
|
||||
} else if (phy->tx_phys == BLE_HCI_LE_PHY_CODED_PREF_MASK) {
|
||||
/* CI=20ms, CE=10-20ms: fits 2 Coded S2 K-frames per CI */
|
||||
struct ble_gap_upd_params s2_params = {
|
||||
.itvl_min = 16,
|
||||
.itvl_max = 16,
|
||||
.latency = 0,
|
||||
.supervision_timeout = 2000,
|
||||
.min_ce_len = 16,
|
||||
.max_ce_len = 32,
|
||||
};
|
||||
xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT);
|
||||
rc = ble_gap_update_params(conn_handle, &s2_params);
|
||||
if (rc == 0) {
|
||||
ESP_LOGI(TAG, "Coded S2: updating CI");
|
||||
ci_is_slow = true;
|
||||
xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE,
|
||||
pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "S2 CI update failed (rc=%d)", rc);
|
||||
}
|
||||
} else if (ci_is_slow) {
|
||||
ci_is_slow = false;
|
||||
xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT);
|
||||
rc = ble_gap_update_params(conn_handle, &conn_params);
|
||||
if (rc == 0) {
|
||||
ESP_LOGI(TAG, "%s: restoring CI to 6 ms", phy->name);
|
||||
xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE,
|
||||
pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "CI restore failed (rc=%d)", rc);
|
||||
/* Measure only after the requested PHY is actually active. */
|
||||
if (phy_update_status != 0 || active_tx_phy != expected_phy ||
|
||||
active_rx_phy != expected_phy) {
|
||||
ESP_LOGE(TAG, "%s: PHY not applied (status=%d tx=%u rx=%u); skipping measurement",
|
||||
phy->name, phy_update_status, active_tx_phy, active_rx_phy);
|
||||
if (coc_lost()) {
|
||||
lost_connection = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
struct ble_gap_conn_desc desc;
|
||||
if (ble_gap_conn_find(conn_handle, &desc) == 0) {
|
||||
/* Do not update CI here; repeated CI changes caused post-coded throughput drops. */
|
||||
ESP_LOGI(TAG, "%s: preserving CI=%u",phy->name, desc.conn_itvl);
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(LINK_SETTLE_TIME_MS));
|
||||
|
||||
if (chan_stalled) {
|
||||
ESP_LOGI(TAG, "Pre-drain: waiting for unstall");
|
||||
wait_unstall(PREDRAIN_TIMEOUT_MS);
|
||||
@@ -454,6 +459,8 @@ static void cent_send_task(void *arg)
|
||||
cent_seg_tx_done = &segment_sdus;
|
||||
cent_seg_tx_drop = &segment_drops;
|
||||
|
||||
cent_set_segment_id(value, phy->segment_id);
|
||||
|
||||
xEventGroupClearBits(coc_event_group, TX_UNSTALLED_BIT); /* clear stale signal from previous segment */
|
||||
|
||||
ESP_LOGI(TAG, "[%s PHY] Sending for %d s", phy->name, phy->duration_s);
|
||||
@@ -619,14 +626,16 @@ static int cent_gap_event(struct ble_gap_event *event, void *arg)
|
||||
conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
coc_chan = NULL;
|
||||
chan_stalled = false;
|
||||
ci_is_slow = false;
|
||||
l2cap_connecting = false;
|
||||
xEventGroupClearBits(coc_event_group, COC_CONNECTED_BIT);
|
||||
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | CONN_UPDATED_BIT | PHY_UPDATED_BIT);
|
||||
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | PHY_UPDATED_BIT);
|
||||
cent_scan();
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:
|
||||
phy_update_status = event->phy_updated.status;
|
||||
active_tx_phy = event->phy_updated.tx_phy;
|
||||
active_rx_phy = event->phy_updated.rx_phy;
|
||||
ESP_LOGI(TAG, "PHY updated: tx=%d rx=%d status=%d",
|
||||
event->phy_updated.tx_phy,
|
||||
event->phy_updated.rx_phy,
|
||||
@@ -643,7 +652,6 @@ static int cent_gap_event(struct ble_gap_event *event, void *arg)
|
||||
ESP_LOGW(TAG, "Connection parameter update failed (status=%d)",
|
||||
event->conn_update.status);
|
||||
}
|
||||
xEventGroupSetBits(coc_event_group, CONN_UPDATED_BIT);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DATA_LEN_CHG:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512
|
||||
CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255
|
||||
CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=270
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL=4
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y
|
||||
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=400
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
# L2CAP COC Throughput Peripheral Example
|
||||
|
||||
`l2cap_coc_prph` demonstrates the peripheral side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It advertises with UUID 0x1812, accepts an incoming GAP connection from `l2cap_coc_cent`, registers an L2CAP COC server on PSM 0x1002, and measures RX throughput as the central sends SDUs.
|
||||
`l2cap_coc_prph` demonstrates the peripheral side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It advertises with UUID 0x1812, accepts an incoming GAP connection from `l2cap_coc_cent`, registers an L2CAP COC server on PSM 0x0080, and measures RX throughput as the central sends SDUs.
|
||||
|
||||
The peripheral tracks throughput per PHY — each time the central switches PHY, the peripheral prints a per-PHY throughput summary box and resets its counters. A background stats task also prints a live per-second RX rate while data is flowing. It must be used together with the `l2cap_coc_cent` example which acts as the sending side.
|
||||
The peripheral tracks RX throughput while the central sends data. A background stats task prints a live per-second RX rate while data is flowing. When more than one PHY is enabled on the central, the peripheral also prints a summary box at each test-segment boundary. It must be used together with the `l2cap_coc_cent` example which acts as the sending side.
|
||||
|
||||
It uses ESP32's Bluetooth controller and NimBLE stack based BLE host.
|
||||
It uses the ESP Bluetooth controller with the NimBLE-based BLE host.
|
||||
|
||||
## How to Use Example
|
||||
|
||||
@@ -37,9 +37,9 @@ In the `L2CAP COC Throughput Configuration` menu:
|
||||
| Option | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `EXAMPLE_L2CAP_COC_MTU` | `16384` | Peripheral L2CAP CoC SDU MTU size in bytes. |
|
||||
| `EXAMPLE_EXTENDED_ADV` | `y` (BLE 5.0 chips) | Enable extended advertising for BLE 5.0 capable devices. Required for Coded PHY testing on ESP32-C6 and ESP32-H2. |
|
||||
| `EXAMPLE_EXTENDED_ADV` | `y` (BLE 5.0 chips) | Enable extended advertising for BLE 5.0 capable devices. |
|
||||
|
||||
> **Note:** Throughput in the central → peripheral direction is primarily determined by the peripheral MTU. With the default configuration (`MTU=16384`, `MPS=247`), NimBLE grants approximately 67 initial credits to the central sender, allowing multiple packets to remain in flight and maximizing link throughput.
|
||||
> **Note:** Throughput in the central → peripheral direction is affected by the MTU negotiated between the central and peripheral. With the default peripheral MTU, enough initial credits are granted to keep multiple packets in flight and maintain high link utilization.
|
||||
|
||||
### Build and Flash
|
||||
|
||||
@@ -70,7 +70,7 @@ I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
|
||||
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
|
||||
```
|
||||
|
||||
> **Note:** The peripheral prints one RX line per second. The central controls PHY selection and test duration; the peripheral tracks and displays throughput continuously as long as data is flowing.
|
||||
> **Note:** The peripheral prints one RX line per second. Summary boxes are printed when the next test segment starts, so they appear when two or more PHYs are enabled on the central. If only one PHY is enabled, the peripheral continues printing live RX throughput, while the central prints the per-interval summary.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ static const char *TAG = "l2cap_coc_prph";
|
||||
#define LL_PACKET_LENGTH 251
|
||||
#define LL_PACKET_TIME 2120
|
||||
#define L2CAP_COC_UUID 0x1812
|
||||
/* App-level tag placed at the start of each CoC SDU.
|
||||
* GAP reports both Coded S2 and Coded S8 as BLE_HCI_LE_PHY_CODED, so the
|
||||
* receiver cannot distinguish those test segments from PHY events alone. */
|
||||
#define COC_SEGMENT_TAG_0 'L'
|
||||
#define COC_SEGMENT_TAG_1 '2'
|
||||
#define COC_SEGMENT_TAG_2 'C'
|
||||
#define COC_SEGMENT_TAG_3 'P'
|
||||
#define COC_SEGMENT_TAG_LEN 5
|
||||
|
||||
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
static struct ble_l2cap_chan *coc_chan = NULL;
|
||||
@@ -39,6 +47,9 @@ static uint32_t rx_packets = 0;
|
||||
static volatile bool coc_active = false;
|
||||
static const char *phy_name = "1M";
|
||||
static uint8_t current_phy = BLE_HCI_LE_PHY_1M;
|
||||
static uint8_t current_segment_id = 0;
|
||||
/* Bumps when RX counters reset so the 1s live logger skips partial windows. */
|
||||
static volatile uint32_t rx_stats_gen = 0;
|
||||
|
||||
void ble_store_config_init(void);
|
||||
|
||||
@@ -57,6 +68,17 @@ static const char *prph_phy_str(uint8_t phy)
|
||||
}
|
||||
}
|
||||
|
||||
static const char *prph_segment_str(uint8_t segment_id)
|
||||
{
|
||||
switch (segment_id) {
|
||||
case 1: return "1M";
|
||||
case 2: return "2M";
|
||||
case 3: return "Coded S2";
|
||||
case 4: return "Coded S8";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void prph_report_phy(int64_t end_time, int64_t start_time,
|
||||
uint32_t bytes, uint32_t packets,
|
||||
const char *phy_name)
|
||||
@@ -76,6 +98,34 @@ static void prph_report_phy(int64_t end_time, int64_t start_time,
|
||||
ESP_LOGI(TAG, "+-------------------------------------------------+");
|
||||
}
|
||||
|
||||
static bool prph_get_segment_id(struct os_mbuf *om, uint8_t *segment_id)
|
||||
{
|
||||
uint8_t header[COC_SEGMENT_TAG_LEN];
|
||||
|
||||
/* Segment tag is optional for compatibility with older centrals. */
|
||||
if (OS_MBUF_PKTLEN(om) < COC_SEGMENT_TAG_LEN ||
|
||||
os_mbuf_copydata(om, 0, COC_SEGMENT_TAG_LEN, header) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != COC_SEGMENT_TAG_0 || header[1] != COC_SEGMENT_TAG_1 ||
|
||||
header[2] != COC_SEGMENT_TAG_2 || header[3] != COC_SEGMENT_TAG_3 ||
|
||||
header[4] < 1 || header[4] > 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*segment_id = header[4];
|
||||
return true;
|
||||
}
|
||||
|
||||
static void prph_reset_rx_stats(int64_t start_time)
|
||||
{
|
||||
rx_bytes = 0;
|
||||
rx_packets = 0;
|
||||
phy_start_time = start_time;
|
||||
rx_stats_gen++;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
static uint8_t ext_adv_pattern[] = {
|
||||
0x02, BLE_HS_ADV_TYPE_FLAGS, 0x06,
|
||||
@@ -216,6 +266,8 @@ static int prph_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
|
||||
rx_bytes = 0;
|
||||
rx_packets = 0;
|
||||
coc_active = true;
|
||||
current_segment_id = 0;
|
||||
rx_stats_gen++;
|
||||
phy_name = prph_phy_str(current_phy);
|
||||
return 0;
|
||||
|
||||
@@ -223,6 +275,8 @@ static int prph_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
|
||||
coc_active = false;
|
||||
coc_chan = NULL;
|
||||
current_phy = BLE_HCI_LE_PHY_1M;
|
||||
current_segment_id = 0;
|
||||
rx_stats_gen++;
|
||||
{
|
||||
int64_t end = esp_timer_get_time();
|
||||
int64_t st = phy_start_time;
|
||||
@@ -249,7 +303,21 @@ static int prph_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
|
||||
|
||||
case BLE_L2CAP_EVENT_COC_DATA_RECEIVED:
|
||||
if (event->receive.sdu_rx) {
|
||||
if (rx_packets == 0) {
|
||||
uint8_t segment_id;
|
||||
|
||||
if (prph_get_segment_id(event->receive.sdu_rx, &segment_id) &&
|
||||
segment_id != current_segment_id) {
|
||||
int64_t now = esp_timer_get_time();
|
||||
/* First SDU of a new segment closes the previous segment. */
|
||||
if (current_segment_id != 0) {
|
||||
prph_report_phy(now, phy_start_time, rx_bytes, rx_packets,
|
||||
phy_name);
|
||||
}
|
||||
current_segment_id = segment_id;
|
||||
phy_name = prph_segment_str(current_segment_id);
|
||||
prph_reset_rx_stats(now);
|
||||
} else if (rx_packets == 0) {
|
||||
/* First SDU with no segment tag (or before any segment change). */
|
||||
phy_start_time = esp_timer_get_time();
|
||||
}
|
||||
rx_bytes += OS_MBUF_PKTLEN(event->receive.sdu_rx);
|
||||
@@ -270,6 +338,7 @@ static void prph_stats_task(void *arg)
|
||||
{
|
||||
uint32_t prev_bytes = 0;
|
||||
int64_t prev_time = 0;
|
||||
uint32_t seen_rx_gen = 0;
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
@@ -277,11 +346,21 @@ static void prph_stats_task(void *arg)
|
||||
if (!coc_active) {
|
||||
prev_bytes = 0;
|
||||
prev_time = 0;
|
||||
seen_rx_gen = rx_stats_gen;
|
||||
continue;
|
||||
}
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
uint32_t bytes = rx_bytes;
|
||||
uint32_t rx_gen = rx_stats_gen;
|
||||
|
||||
/* RX counters were reset; restart the live 1s window. */
|
||||
if (rx_gen != seen_rx_gen) {
|
||||
prev_bytes = bytes;
|
||||
prev_time = now;
|
||||
seen_rx_gen = rx_gen;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prev_time > 0) {
|
||||
if (bytes < prev_bytes) {
|
||||
@@ -320,6 +399,8 @@ static int prph_gap_event(struct ble_gap_event *event, void *arg)
|
||||
coc_chan = NULL;
|
||||
coc_active = false;
|
||||
current_phy = BLE_HCI_LE_PHY_1M;
|
||||
current_segment_id = 0;
|
||||
rx_stats_gen++;
|
||||
phy_name = "1M";
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
ble_gap_ext_adv_stop(0);
|
||||
@@ -333,40 +414,16 @@ static int prph_gap_event(struct ble_gap_event *event, void *arg)
|
||||
event->phy_updated.rx_phy,
|
||||
event->phy_updated.status);
|
||||
if (event->phy_updated.status == 0) {
|
||||
if (coc_active) {
|
||||
int64_t end = esp_timer_get_time();
|
||||
int64_t st = phy_start_time;
|
||||
uint32_t by = rx_bytes;
|
||||
uint32_t pk = rx_packets;
|
||||
prph_report_phy(end, st, by, pk, phy_name);
|
||||
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
|
||||
}
|
||||
current_phy = event->phy_updated.rx_phy;
|
||||
phy_name = prph_phy_str(event->phy_updated.rx_phy);
|
||||
rx_stats_gen++;
|
||||
if (current_segment_id == 0) {
|
||||
phy_name = prph_phy_str(event->phy_updated.rx_phy);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
ESP_LOGI(TAG, "Conn params updated; status=%d", event->conn_update.status);
|
||||
if (event->conn_update.status == 0 && coc_active &&
|
||||
current_phy == BLE_HCI_LE_PHY_CODED) {
|
||||
struct ble_gap_conn_desc desc;
|
||||
if (ble_gap_conn_find(conn_handle, &desc) == 0) {
|
||||
if (desc.conn_itvl != 16 && desc.conn_itvl != 32) {
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(phy_name, "Coded") != 0) {
|
||||
int64_t end = esp_timer_get_time();
|
||||
uint32_t by = rx_bytes;
|
||||
uint32_t pk = rx_packets;
|
||||
prph_report_phy(end, phy_start_time, by, pk, phy_name);
|
||||
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
|
||||
}
|
||||
phy_name = (desc.conn_itvl >= 32) ? "Coded S8" : "Coded S2";
|
||||
ESP_LOGI(TAG, "Coding scheme updated to %s (CI=%u × 1.25ms)",
|
||||
phy_name, desc.conn_itvl);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
|
||||
Reference in New Issue
Block a user