From b47bf600032ff46efe2aa03342576a093b4e50c8 Mon Sep 17 00:00:00 2001 From: Mayank Tyagi Date: Fri, 12 Jun 2026 17:59:52 +0530 Subject: [PATCH] fix(nimble): Optimize Nimble throughput example for 1M/2M/S2/S8 --- .../gatt/blecent_throughput/main/main.c | 316 ++++++++++++++---- .../blecent_throughput/sdkconfig.defaults | 31 +- .../sdkconfig.defaults.esp32c6 | 5 + .../gatt/bleprph_throughput/main/gatt_svr.c | 32 +- .../gatt/bleprph_throughput/main/main.c | 112 ++++--- .../bleprph_throughput/sdkconfig.defaults | 28 +- .../sdkconfig.defaults.esp32c6 | 5 + 7 files changed, 408 insertions(+), 121 deletions(-) create mode 100644 examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults.esp32c6 create mode 100644 examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults.esp32c6 diff --git a/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/main.c b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/main.c index f1d2a14a8b8..43a4ff8cb70 100644 --- a/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/main.c +++ b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/main/main.c @@ -49,13 +49,13 @@ #define WRITE_THROUGHPUT 2 #define NOTIFY_THROUGHPUT 3 -#define READ_THROUGHPUT_PAYLOAD 510 /* MTU(512) - ATT read rsp header(1) - 1 (avoid Read Blob) */ -#define WRITE_THROUGHPUT_PAYLOAD 509 /* MTU(512) - ATT write cmd header(3) */ +#define READ_THROUGHPUT_PAYLOAD 497 /* 502 bytes ACL -> 2x 251 LL packets exactly (497 + 1 Read Rsp + 4 L2CAP) */ +#define WRITE_THROUGHPUT_PAYLOAD 495 /* 502 bytes ACL -> 2x 251 LL packets exactly (495 + 3 Write Cmd + 4 L2CAP) */ #define LL_PACKET_TIME 2120 #define LL_PACKET_LENGTH 251 static const char *tag = "blecent_throughput"; static int blecent_gap_event(struct ble_gap_event *event, void *arg); -static SemaphoreHandle_t xSemaphore; + static int mbuf_len_total; static int failure_count; static TaskHandle_t throughput_task_handle = NULL; @@ -66,28 +66,102 @@ static int mtu_def = 512; static ble_addr_t conn_addr; static uint16_t handle; #define PHY_1M 0 -#if CONFIG_EXAMPLE_EXTENDED_ADV -static int current_phy_updated; #define PHY_2M 1 #define PHY_CODED_S2 2 #define PHY_CODED_S8 3 + +#if CONFIG_EXAMPLE_EXTENDED_ADV +static int current_phy_updated; #endif +/* State for callback-chained read throughput test */ +static volatile bool read_test_active = false; +static uint16_t read_val_handle_g; +static uint16_t read_conn_handle_g; +static int read_count_g; + +/* ============================================================================== + * Connection Parameter Tuning + * ============================================================================== + * itvl_min / itvl_max: Connection interval (time between connection events). + * Units are in 1.25ms. (e.g., 24 * 1.25ms = 30ms). + * latency: Slave latency (number of events the peripheral can skip). + * supervision_timeout: Time before link is considered lost. Units: 10ms. + * min_ce_len / max_ce_len: Connection Event length. max_ce_len = 0xFFFF tells + * the controller to use the ENTIRE connection interval + * for transmitting data, instead of cutting it short. + * ============================================================================== + * + * Write/Notify-optimized for 1M/2M: 30-50ms interval, full CE length. */ static struct ble_gap_upd_params conn_params = { - /** Minimum value for connection interval in 1.25ms units */ .itvl_min = CONFIG_EXAMPLE_CONN_ITVL_MIN, - /** Maximum value for connection interval in 1.25ms units */ .itvl_max = CONFIG_EXAMPLE_CONN_ITVL_MAX, - /** Connection latency */ .latency = CONFIG_EXAMPLE_CONN_LATENCY, - /** Supervision timeout in 10ms units */ .supervision_timeout = CONFIG_EXAMPLE_CONN_TIMEOUT, - /** Minimum length of connection event in 0.625ms units */ - .min_ce_len = CONFIG_EXAMPLE_CONN_CE_LEN_MIN, - /** Maximum length of connection event in 0.625ms units */ - .max_ce_len = CONFIG_EXAMPLE_CONN_CE_LEN_MAX, + .min_ce_len = 0, + .max_ce_len = 0xFFFF, }; +/* Write/Notify-optimized for Coded S2 */ +static struct ble_gap_upd_params conn_params_coded_s2 = { + .itvl_min = 24, /* 30 ms */ + .itvl_max = 32, /* 40 ms */ + .latency = 0, + .supervision_timeout = CONFIG_EXAMPLE_CONN_TIMEOUT, + .min_ce_len = 0, + .max_ce_len = 0xFFFF, +}; + +/* Write/Notify-optimized for Coded S8 */ +static struct ble_gap_upd_params conn_params_coded_s8 = { + .itvl_min = 60, /* 75 ms */ + .itvl_max = 80, /* 100 ms */ + .latency = 0, + .supervision_timeout = CONFIG_EXAMPLE_CONN_TIMEOUT, + .min_ce_len = 0, + .max_ce_len = 0xFFFF, +}; + +/* Read-optimized for 1M/2M: short interval for fast round-trips */ +static struct ble_gap_upd_params conn_params_read = { + .itvl_min = 6, /* 7.5 ms */ + .itvl_max = 8, /* 10 ms */ + .latency = 0, + .supervision_timeout = CONFIG_EXAMPLE_CONN_TIMEOUT, + .min_ce_len = 0, + .max_ce_len = 0xFFFF, +}; + +/* Read-optimized for Coded S2 */ +static struct ble_gap_upd_params conn_params_coded_s2_read = { + .itvl_min = 12, /* 15 ms */ + .itvl_max = 16, /* 20 ms */ + .latency = 0, + .supervision_timeout = CONFIG_EXAMPLE_CONN_TIMEOUT, + .min_ce_len = 0, + .max_ce_len = 0xFFFF, +}; + +/* Read-optimized for Coded S8 */ +static struct ble_gap_upd_params conn_params_coded_s8_read = { + .itvl_min = 36, /* 45 ms */ + .itvl_max = 40, /* 50 ms */ + .latency = 0, + .supervision_timeout = CONFIG_EXAMPLE_CONN_TIMEOUT, + .min_ce_len = 0, + .max_ce_len = 0xFFFF, +}; + +static void switch_conn_params(uint16_t conn_handle, const struct ble_gap_upd_params *params) { + int rc = ble_gap_update_params(conn_handle, params); + if (rc != 0) { + ESP_LOGE(tag, "Failed to update connection parameters: %d", rc); + } else { + ESP_LOGI(tag, "Requested connection parameter update"); + vTaskDelay(500 / portTICK_PERIOD_MS); + } +} + void ble_store_config_init(void); #if CONFIG_EXAMPLE_EXTENDED_ADV @@ -137,22 +211,20 @@ static int blecent_write(uint16_t conn_handle, uint16_t val_handle, start_time = esp_timer_get_time(); while (write_time < test_time * 1000) { - /* Wait till the previous write is complete. For first time Semaphore - * is already available */ - label: - rc = ble_gattc_write_no_rsp_flat(conn_handle, val_handle, &value, sizeof value); + rc = ble_gattc_write_no_rsp_flat(conn_handle, val_handle, &value, sizeof value); - if(rc == BLE_HS_ENOMEM) { - vTaskDelay(2); /* Wait for buffers to free up and try again */ - goto label; - } - else if (rc != 0) { - ESP_LOGE(tag, "Error: Failed to write characteristic; rc=%d",rc); - goto err; + if (rc == BLE_HS_ENOMEM) { + vTaskDelay(2); /* Wait for buffers to free up and try again */ + end_time = esp_timer_get_time(); + write_time = (end_time - start_time) / 1000; + continue; + } else if (rc != 0) { + ESP_LOGE(tag, "Error: Failed to write characteristic; rc=%d", rc); + goto err; } end_time = esp_timer_get_time(); - write_time = (end_time - start_time) / 1000 ; + write_time = (end_time - start_time) / 1000; write_count += 1; } @@ -171,6 +243,11 @@ err: return ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM); } +/** + * Read callback that chains the next read immediately from within the + * NimBLE host context. This eliminates the FreeRTOS task-switch delay + * that previously caused each read to waste an extra connection event. + */ static int blecent_repeat_read(uint16_t conn_handle, const struct ble_gatt_error *error, @@ -178,55 +255,96 @@ blecent_repeat_read(uint16_t conn_handle, void *arg) { if (error->status == 0) { - xSemaphoreGive(xSemaphore); ESP_LOGD(tag, " attr_handle=%d value=", attr->handle); mbuf_len_total += OS_MBUF_PKTLEN(attr->om); + read_count_g++; } else { - ESP_LOGE(tag, " Read failed, callback error code = %d", error->status ); - xSemaphoreGive(xSemaphore); + ESP_LOGE(tag, " Read failed, callback error code = %d", error->status); + /* On error, stop chaining and wake up main task */ + read_test_active = false; + if (throughput_task_handle) { + xTaskNotifyGive(throughput_task_handle); + } + return 0; } - return error->status; + + /* Chain next read immediately (zero task-switch overhead) */ + if (read_test_active) { + int rc = ble_gattc_read(read_conn_handle_g, read_val_handle_g, + blecent_repeat_read, arg); + if (rc != 0) { + ESP_LOGE(tag, "Failed to chain read; rc=%d", rc); + read_test_active = false; + if (throughput_task_handle) { + xTaskNotifyGive(throughput_task_handle); + } + } + } else { + /* Test time expired — wake up main task to print results */ + if (throughput_task_handle) { + xTaskNotifyGive(throughput_task_handle); + } + } + + return 0; } static int blecent_read(uint16_t conn_handle, uint16_t val_handle, - ble_gatt_attr_fn *cb, struct peer *peer, int test_time) + struct peer *peer, int test_time) { - int rc, read_count = 0; - int64_t start_time, end_time, read_time = 0; - /* Keep track of number of bytes read from char */ + int rc; + int64_t start_time; + + /* Reset counters */ mbuf_len_total = 0; + read_count_g = 0; + read_conn_handle_g = conn_handle; + read_val_handle_g = val_handle; + read_test_active = true; + + /* Drain any stale notifications from a prior test that timed out */ + ulTaskNotifyTake(pdTRUE, 0); + start_time = esp_timer_get_time(); ESP_LOGD(tag, " Throughput read started :val_handle=%d test_time=%d", val_handle, test_time); - while (read_time < (test_time * 1000)) { - /* Wait till the previous read is complete. For first time use Semaphore - * is already available */ - xSemaphoreTake(xSemaphore, portMAX_DELAY); - - rc = ble_gattc_read(peer->conn_handle, val_handle, - blecent_repeat_read, (void *) &peer); - if (rc != 0) { - ESP_LOGE(tag, "Error: Failed to read characteristic; rc=%d", - rc); - goto err; - } - - end_time = esp_timer_get_time(); - read_time = (end_time - start_time) / 1000 ; - read_count += 1; + /* Kick off the first read — the callback will chain all subsequent reads */ + rc = ble_gattc_read(conn_handle, val_handle, + blecent_repeat_read, (void *) peer); + if (rc != 0) { + ESP_LOGE(tag, "Error: Failed to start read chain; rc=%d", rc); + read_test_active = false; + goto err; } - /* Application data throughput */ + /* Sleep for the test duration while the callback chain runs autonomously. + * If the chain encounters an error (e.g. disconnect), it will notify us early. */ + uint32_t notified = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(test_time * 1000)); + + if (notified == 0) { + /* Normal timeout, signal the chain to stop after the current in-flight read completes */ + read_test_active = false; + /* Wait for the last callback to notify us (up to 5s safety timeout) */ + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(5000)); + } else { + /* Woken early by an error in the callback chain */ + read_test_active = false; + } + + int64_t end_time = esp_timer_get_time(); + int actual_secs = (int)((end_time - start_time) / 1000000); + if (actual_secs == 0) actual_secs = test_time; + + /* Application data throughput */ printf("\n****************************************************************\n"); ESP_LOGI(tag, "Application Read throughput = %d bps, Read op counter = %d", - (mbuf_len_total * 8) / (test_time), read_count); + (mbuf_len_total * 8) / actual_secs, read_count_g); printf("\n****************************************************************\n"); return 0; err: - xSemaphoreGive(xSemaphore); /* Terminate the connection. */ vTaskDelay(100 / portTICK_PERIOD_MS); return ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM); @@ -334,7 +452,7 @@ static void throughput_task(void *arg) printf(" | Phy mode: Enter value in 0 for 1M, 1 for 2M ,2 for Coded S2, |\n"); printf(" | 3 for Coded S8. |\n"); printf(" | |\n"); - printf(" | e.g. throughput read 600 3 |\n"); + printf(" | e.g. throughput read 60 3 |\n"); printf(" | |\n"); printf(" | ** Enter 'throughput read 60 0' for reading char for 60 seconds on 1M Phy |\n"); printf(" | OR 'throughput write 60 1' for writing to char for 60 seconds on 2M Phy |\n"); @@ -365,12 +483,46 @@ static void throughput_task(void *arg) vTaskDelay(100 / portTICK_PERIOD_MS); } } + + /* Send a 1-byte control message to the peripheral's read/write char + * to tell it which PHY coding we are testing. This ensures the peripheral + * perfectly mirrors our PHY preference (1M, 2M, S2, or S8). */ + if (test_data[2] >= 0 && test_data[2] <= 3) { + const struct peer_chr *cmd_chr = peer_chr_find_uuid(peer, + THRPT_UUID_DECLARE(THRPT_SVC), + THRPT_UUID_DECLARE(THRPT_CHR_READ_WRITE)); + if (cmd_chr != NULL) { + uint8_t phy_cmd = test_data[2]; + rc = ble_gattc_write_no_rsp_flat(conn_handle, cmd_chr->chr.val_handle, &phy_cmd, 1); + if (rc != 0) { + ESP_LOGW(tag, "Failed to send PHY cmd to peripheral; rc=%d", rc); + } else { + vTaskDelay(200 / portTICK_PERIOD_MS); /* Give peripheral time to apply PHY */ + } + } + } + } +#else + /* Extended advertising is disabled, only 1M PHY is available */ + if (test_data[2] != PHY_1M) { + ESP_LOGW(tag, "Extended advertising disabled; forcing PHY to 1M (ignoring user selection %d)", test_data[2]); + test_data[2] = PHY_1M; } #endif switch (test_data[0]) { case READ_THROUGHPUT: + /* PHY-aware read connection params: short interval for fast + * round-trips on 1M/2M, longer for coded PHY */ + if (test_data[2] == PHY_CODED_S2) { + switch_conn_params(conn_handle, &conn_params_coded_s2_read); + } else if (test_data[2] == PHY_CODED_S8) { + switch_conn_params(conn_handle, &conn_params_coded_s8_read); + } else { + switch_conn_params(conn_handle, &conn_params_read); + } + /* Read the characteristic supporting long read support * `THRPT_LONG_CHR_READ_WRITE` (0x000b) */ chr = peer_chr_find_uuid(peer, @@ -379,12 +531,12 @@ static void throughput_task(void *arg) if (chr == NULL) { ESP_LOGE(tag, "Peer does not support " "LONG_READ (0x000b) characteristic "); - break; + goto read_cleanup; } if (test_data[1] > 0) { rc = blecent_read(conn_handle, chr->chr.val_handle, - blecent_repeat_read, (void *) peer, test_data[1]); + (void *) peer, test_data[1]); if (rc != 0) { ESP_LOGE(tag, "Error while reading from GATTS; rc = %d", rc); /* Delete task on critical error (connection lost or fatal error) */ @@ -398,9 +550,28 @@ static void throughput_task(void *arg) } else { ESP_LOGE(tag, "Please enter non-zero value for test time in seconds!!"); } + +read_cleanup: + /* Restore write-optimized params after the read test */ + ESP_LOGI(tag, "Restoring WRITE-optimized conn params"); + if (test_data[2] == PHY_CODED_S2) { + switch_conn_params(conn_handle, &conn_params_coded_s2); + } else if (test_data[2] == PHY_CODED_S8) { + switch_conn_params(conn_handle, &conn_params_coded_s8); + } else { + switch_conn_params(conn_handle, &conn_params); + } break; case WRITE_THROUGHPUT: + if (test_data[2] == PHY_CODED_S2) { + switch_conn_params(conn_handle, &conn_params_coded_s2); + } else if (test_data[2] == PHY_CODED_S8) { + switch_conn_params(conn_handle, &conn_params_coded_s8); + } else { + switch_conn_params(conn_handle, &conn_params); + } + chr = peer_chr_find_uuid(peer, THRPT_UUID_DECLARE(THRPT_SVC), THRPT_UUID_DECLARE(THRPT_CHR_READ_WRITE)); @@ -428,6 +599,14 @@ static void throughput_task(void *arg) break; case NOTIFY_THROUGHPUT: + if (test_data[2] == PHY_CODED_S2) { + switch_conn_params(conn_handle, &conn_params_coded_s2); + } else if (test_data[2] == PHY_CODED_S8) { + switch_conn_params(conn_handle, &conn_params_coded_s8); + } else { + switch_conn_params(conn_handle, &conn_params); + } + chr = peer_chr_find_uuid(peer, THRPT_UUID_DECLARE(THRPT_SVC), THRPT_UUID_DECLARE(THRPT_CHR_NOTIFY)); @@ -463,6 +642,20 @@ static void throughput_task(void *arg) test_data[1]); } vTaskDelay(test_data[1]*1000 / portTICK_PERIOD_MS); + + /* Unsubscribe so the next notify test triggers a fresh + * BLE_GAP_EVENT_SUBSCRIBE on the peripheral (cur_notify 0→1) */ + { + uint8_t unsub_value[2] = {0, 0}; + rc = ble_gattc_write_flat(conn_handle, dsc->dsc.handle, + unsub_value, sizeof unsub_value, + NULL, NULL); + if (rc != 0) { + ESP_LOGW(tag, "Unsubscribe failed; rc=%d (non-fatal)", rc); + } else { + ESP_LOGI(tag, "Unsubscribed from notifications"); + } + } break; default: @@ -656,7 +849,7 @@ blecent_should_connect(const struct ble_gap_disc_desc *disc) } } - ESP_LOGI(tag, "connect; fields.num_uuids128 =%d", fields.num_uuids128); + ESP_LOGD(tag, "connect; fields.num_uuids128 =%d", fields.num_uuids128); for (i = 0; i < fields.num_uuids128; i++) { if ((memcmp(&fields.uuids128[i], THRPT_UUID_DECLARE(THRPT_SVC), sizeof(ble_uuid128_t))) == 0 ) { @@ -759,7 +952,7 @@ blecent_gap_event(struct ble_gap_event *event, void *arg) switch (event->type) { case BLE_GAP_EVENT_DISC: - ESP_LOGI(tag, "Event DISC "); + ESP_LOGD(tag, "Event DISC "); rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data); if (rc != 0) { @@ -880,6 +1073,11 @@ blecent_gap_event(struct ble_gap_event *event, void *arg) /* Attribute data is contained in event->notify_rx.attr_data. */ return 0; + case BLE_GAP_EVENT_CONN_UPDATE: + ESP_LOGI(tag, "connection updated; status=%d", + event->conn_update.status); + return 0; + case BLE_GAP_EVENT_MTU: ESP_LOGI(tag, "mtu update event; conn_handle = %d cid = %d mtu = %d", event->mtu.conn_handle, @@ -890,7 +1088,6 @@ blecent_gap_event(struct ble_gap_event *event, void *arg) #if CONFIG_EXAMPLE_EXTENDED_ADV case BLE_GAP_EVENT_EXT_DISC: /* An advertisement report was received during GAP discovery. */ - blecent_connect_if_interesting(&event->ext_disc); return 0; @@ -977,12 +1174,9 @@ blecent_on_sync(void) void blecent_host_task(void *param) { ESP_LOGI(tag, "BLE Host Task Started"); - xSemaphore = xSemaphoreCreateBinary(); - xSemaphoreGive(xSemaphore); /* This function will return only when nimble_port_stop() is executed */ nimble_port_run(); - vSemaphoreDelete(xSemaphore); nimble_port_freertos_deinit(); } diff --git a/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults index 0f40a673239..e65b4a19cb7 100644 --- a/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults +++ b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults @@ -2,14 +2,35 @@ # in this example and some misc buffer sizes are increased # -# BT config +# BT config (universal across all ESP32 variants) # CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y + +# === BT 5.0 / 2M PHY support === +# Ignored automatically on classic ESP32 which lacks BT 5.0 +CONFIG_BT_NIMBLE_EXT_ADV=n +CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=y +CONFIG_EXAMPLE_EXTENDED_ADV=n + +# === ATT / GATT === CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512 -CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=20 -CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255 + +# === Memory Pool Tuning === +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=50 -CONFIG_BT_NIMBLE_LOG_LEVEL=4 -CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=592 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=40 + +# Increase controller-to-host buffers +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=40 +CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255 + +# === Connection Parameters (write/notify optimal; code switches dynamically for reads) === +CONFIG_EXAMPLE_CONN_ITVL_MIN=24 +CONFIG_EXAMPLE_CONN_ITVL_MAX=40 + +# Disable NimBLE INFO logging for throughput tests +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_LOG_LEVEL=2 diff --git a/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults.esp32c6 b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults.esp32c6 new file mode 100644 index 00000000000..a12d9353d92 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/sdkconfig.defaults.esp32c6 @@ -0,0 +1,5 @@ +# === High-Performance Tuning === +# This config is only applied when building for the ESP32-C6 target +# It is necessary for the C6 to reach 750+ kbps and 1.3+ Mbps throughput. +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatt_svr.c b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatt_svr.c index 0f952f2f67c..b5535b81e05 100644 --- a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatt_svr.c +++ b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/gatt_svr.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -33,8 +33,8 @@ #define THRPT_CHR_NOTIFY 0x000a #define THRPT_LONG_CHR_READ_WRITE 0x000b -#define READ_THROUGHPUT_PAYLOAD 510 /* MTU(512) - ATT read rsp header(1) - 1 (avoid Read Blob) */ -#define WRITE_THROUGHPUT_PAYLOAD 509 /* MTU(512) - ATT write cmd header(3) */ +#define READ_THROUGHPUT_PAYLOAD 497 /* 502 bytes ACL -> 2x 251 LL packets exactly (497 + 1 Read Rsp + 4 L2CAP) */ +#define WRITE_THROUGHPUT_PAYLOAD 495 /* 502 bytes ACL -> 2x 251 LL packets exactly (495 + 3 Write Cmd + 4 L2CAP) */ static const char *tag = "bleprph_throughput"; @@ -142,6 +142,32 @@ gatt_svr_read_write_long_test(uint16_t conn_handle, uint16_t attr_handle, case THRPT_CHR_READ_WRITE: if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + /* If the central sends exactly 1 byte, it is a PHY control command */ + if (om_len == 1) { + uint8_t requested_phy; + os_mbuf_copydata(ctxt->om, 0, 1, &requested_phy); + rc = 0; + if (requested_phy == 0) { /* 0 = PHY_1M */ + rc = ble_gap_set_prefered_le_phy(conn_handle, BLE_HCI_LE_PHY_1M_PREF_MASK, BLE_HCI_LE_PHY_1M_PREF_MASK, 0); + ESP_LOGI(tag, "Central requested 1M PHY via GATT command"); + } else if (requested_phy == 1) { /* 1 = PHY_2M */ + rc = ble_gap_set_prefered_le_phy(conn_handle, BLE_HCI_LE_PHY_2M_PREF_MASK, BLE_HCI_LE_PHY_2M_PREF_MASK, 0); + ESP_LOGI(tag, "Central requested 2M PHY via GATT command"); + } else if (requested_phy == 2) { /* 2 = PHY_CODED_S2 */ + rc = ble_gap_set_prefered_le_phy(conn_handle, BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x01); + ESP_LOGI(tag, "Central requested S2 PHY via GATT command"); + } else if (requested_phy == 3) { /* 3 = PHY_CODED_S8 */ + rc = ble_gap_set_prefered_le_phy(conn_handle, BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x02); + ESP_LOGI(tag, "Central requested S8 PHY via GATT command"); + } + + if (rc != 0) { + ESP_LOGE(tag, "Failed to set preferred LE PHY; rc=%d", rc); + } + return 0; + } + rc = gatt_svr_chr_write(conn_handle, attr_handle, ctxt->om, 0, sizeof gatt_svr_thrpt_static_short_val, diff --git a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/main.c b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/main.c index 05ab145ee75..579f98f4686 100644 --- a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/main.c +++ b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/main/main.c @@ -31,16 +31,16 @@ static uint8_t s_current_phy; static const char *device_name = "nimble_prph"; -#define NOTIFY_THROUGHPUT_PAYLOAD 509 /* MTU(512) - ATT notify header(3) */ -#define MIN_REQUIRED_MBUF 2 /* Assuming payload of 500Bytes and each mbuf can take 292Bytes. */ -#define NOTIFY_PIPELINE_DEPTH 15 /* Number of notifications to keep in flight for throughput */ +#define NOTIFY_THROUGHPUT_PAYLOAD 495 /* Exactly aligns with 2 LL packets */ +#define MIN_REQUIRED_MBUF 10 /* Reserve mbufs for incoming ATT packets */ +#define NOTIFY_PIPELINE_DEPTH 8 /* Number of notifications to keep in flight */ #define PREFERRED_MTU_VALUE 512 #define LL_PACKET_TIME 2120 #define LL_PACKET_LENGTH 251 #define MTU_DEF 512 static const char *tag = "bleprph_throughput"; -static SemaphoreHandle_t notify_sem; +static TaskHandle_t notify_task_handle = NULL; static bool notify_state; static int notify_test_time = 60; static uint16_t conn_handle; @@ -261,12 +261,13 @@ notify_task(void *arg) break; } - while (notify_time < (notify_test_time * 1000)) { - /* We are anyway using counting semaphore for sending - * notifications. So hopefully not much waiting period will be - * introduced before sending a new notification. Revisit this - * counter if need to do away with semaphore waiting. XXX */ - xSemaphoreTake(notify_sem, portMAX_DELAY); + while (notify_state && notify_time < (notify_test_time * 1000)) { + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + + /* Stop immediately if central unsubscribed */ + if (!notify_state) { + break; + } if (dummy == 200) { dummy = 0; @@ -285,17 +286,22 @@ notify_task(void *arg) rc = ble_gatts_notify_custom(conn_handle, notify_handle, om); if (rc != 0) { - ESP_LOGE(tag, "Error while sending notification; rc = %d", rc); + if (rc == BLE_HS_ENOMEM) { + ESP_LOGD(tag, "Notification queue full (expected backpressure)"); + } else { + ESP_LOGE(tag, "Error while sending notification; rc = %d", rc); + } notify_count -= 1; - xSemaphoreGive(notify_sem); /* Yield to let mbufs free up */ vTaskDelay(1); } } else { - xSemaphoreGive(notify_sem); - notify_count -= 1; - /* Yield briefly to let mbufs free up */ - vTaskDelay(1); + if (notify_task_handle) { + xTaskNotifyGive(notify_task_handle); + } + notify_count -= 1; + /* Yield briefly to let mbufs free up */ + vTaskDelay(1); } end_time = esp_timer_get_time(); @@ -303,13 +309,20 @@ notify_task(void *arg) notify_count += 1; } - printf("\n*********************************\n"); - ESP_LOGI(tag, "Notify throughput = %d bps, count = %d", - (notify_count * NOTIFY_THROUGHPUT_PAYLOAD * 8) / notify_test_time, notify_count); - printf("\n*********************************\n"); - ESP_LOGI(tag, " Notification test complete for stipulated time of %d sec", notify_test_time); + /* Use actual elapsed time for accurate throughput calculation */ + { + int actual_secs = (int)(notify_time / 1000); + if (actual_secs > 0 && notify_count > 0) { + printf("\n*********************************\n"); + ESP_LOGI(tag, "Notify throughput = %d bps, count = %d", + (int)(((uint64_t)notify_count * NOTIFY_THROUGHPUT_PAYLOAD * 8) / actual_secs), notify_count); + printf("\n*********************************\n"); + ESP_LOGI(tag, " Notification test complete (%d sec elapsed)", actual_secs); + } + } notify_test_time = 0; notify_count = 0; + notify_time = 0; break; } @@ -356,6 +369,12 @@ gatts_gap_event(struct ble_gap_event *event, void *arg) case BLE_GAP_EVENT_DISCONNECT: ESP_LOGI(tag, "disconnect; reason = %d", event->disconnect.reason); + /* Stop notification task loop cleanly */ + notify_state = false; + if (notify_task_handle) { + xTaskNotifyGive(notify_task_handle); + } + /* Connection terminated; resume advertising */ #if CONFIG_EXAMPLE_EXTENDED_ADV ble_gap_ext_adv_stop(0); @@ -376,11 +395,6 @@ gatts_gap_event(struct ble_gap_event *event, void *arg) case BLE_GAP_EVENT_ADV_COMPLETE: ESP_LOGI(tag, "adv complete "); -#if CONFIG_EXAMPLE_EXTENDED_ADV - ext_bleprph_advertise(); -#else - gatts_advertise(); -#endif break; case BLE_GAP_EVENT_SUBSCRIBE: @@ -389,17 +403,22 @@ gatts_gap_event(struct ble_gap_event *event, void *arg) event->subscribe.cur_notify, event->subscribe.attr_handle); if (event->subscribe.attr_handle == notify_handle) { notify_state = event->subscribe.cur_notify; - if (arg != NULL) { - ESP_LOGI(tag, "notify test time = %d", *(int *)arg); - notify_test_time = *((int *)arg); - } if (notify_state) { + /* Always reset test time on new subscription. + * The central controls the actual duration by unsubscribing. + * Use a large default so the peripheral never stops on its own. */ + notify_test_time = 3600; + ESP_LOGI(tag, "Notifications enabled, test time = %d sec", notify_test_time); /* Prime the notification pipeline to allow multiple in-flight * notifications. This enables the controller to fill connection * events with back-to-back PDUs for maximum throughput. */ for (int i = 0; i < NOTIFY_PIPELINE_DEPTH; i++) { - xSemaphoreGive(notify_sem); + if (notify_task_handle) { + xTaskNotifyGive(notify_task_handle); + } } + } else { + ESP_LOGI(tag, "Notifications disabled"); } } else if (event->subscribe.attr_handle != notify_handle) { notify_state = event->subscribe.cur_notify; @@ -408,16 +427,20 @@ gatts_gap_event(struct ble_gap_event *event, void *arg) case BLE_GAP_EVENT_NOTIFY_TX: ESP_LOGD(tag, "BLE_GAP_EVENT_NOTIFY_TX success !!"); - if ((event->notify_tx.status == 0) || - (event->notify_tx.status == BLE_HS_EDONE)) { - /* Send new notification i.e. give Semaphore. By definition, - * sending new notifications should not be based on successful - * notifications sent, but let us adopt this method to avoid too - * many `BLE_HS_ENOMEM` errors because of continuous transfer of - * notifications.XXX */ - xSemaphoreGive(notify_sem); - } else { - ESP_LOGE(tag, "BLE_GAP_EVENT_NOTIFY_TX notify tx status = %d", event->notify_tx.status); + + /* Always return the pipeline token to prevent the task from blocking permanently, + * even if the notification failed (e.g., due to disconnect or buffer full). */ + if (notify_task_handle) { + xTaskNotifyGive(notify_task_handle); + } + + if ((event->notify_tx.status != 0) && + (event->notify_tx.status != BLE_HS_EDONE)) { + if (event->notify_tx.status == BLE_HS_ENOMEM) { + ESP_LOGD(tag, "BLE_GAP_EVENT_NOTIFY_TX flow control (ENOMEM)"); + } else { + ESP_LOGE(tag, "BLE_GAP_EVENT_NOTIFY_TX notify tx status = %d", event->notify_tx.status); + } } break; @@ -477,13 +500,8 @@ gatts_on_reset(int reason) void gatts_host_task(void *param) { ESP_LOGI(tag, "BLE Host Task Started"); - /* Create a counting semaphore for Notification. Can be used to track - * successful notification txmission. Optimistically take some big number - * for counting Semaphore */ - notify_sem = xSemaphoreCreateCounting(100, 0); /* This function will return only when nimble_port_stop() is executed */ nimble_port_run(); - vSemaphoreDelete(notify_sem); nimble_port_freertos_deinit(); } @@ -512,7 +530,7 @@ void app_main(void) ble_hs_cfg.store_status_cb = ble_store_util_status_rr; /* Initialize Notify Task */ - BaseType_t task_rc = xTaskCreate(notify_task, "notify_task", 4096, NULL, 10, NULL); + BaseType_t task_rc = xTaskCreate(notify_task, "notify_task", 4096, NULL, 10, ¬ify_task_handle); if (task_rc != pdPASS) { ESP_LOGE(tag, "Failed to create notify_task (rc=%d)", task_rc); return ; diff --git a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults index 79575fc23a5..2b58c85d2ff 100644 --- a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults +++ b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults @@ -2,14 +2,32 @@ # in this example and some example specific misc sizes are increased. # -# BT config +# BT config (universal across all ESP32 variants) # CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y + +# === BT 5.0 / 2M PHY support === +# Ignored automatically on classic ESP32 which lacks BT 5.0 +CONFIG_BT_NIMBLE_EXT_ADV=n +CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=y +CONFIG_EXAMPLE_EXTENDED_ADV=n + +# === ATT / GATT === CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512 -CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=20 + +# === Memory Pool Tuning === +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=80 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=592 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=80 + +# Increase controller-to-host buffers +CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=40 +CONFIG_BT_NIMBLE_TRANSPORT_ACL_SIZE=255 CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255 -CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=50 -CONFIG_BT_NIMBLE_LOG_LEVEL=4 -CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y + +# Disable NimBLE INFO logging for throughput tests +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_LOG_LEVEL=2 diff --git a/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults.esp32c6 b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults.esp32c6 new file mode 100644 index 00000000000..a12d9353d92 --- /dev/null +++ b/examples/bluetooth/nimble/throughput_app/gatt/bleprph_throughput/sdkconfig.defaults.esp32c6 @@ -0,0 +1,5 @@ +# === High-Performance Tuning === +# This config is only applied when building for the ESP32-C6 target +# It is necessary for the C6 to reach 750+ kbps and 1.3+ Mbps throughput. +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_FREERTOS_HZ=1000