Начало здесь: https://forum.amperka.ru/threads/esp32c3-насыпаем-bluetooth-zephyr-esp-idf.23549/ Есть много способов, как поставить родную для ESP32 среду разработки. Можно с эклипсом, можно без, можно в Linux, MacOS, Windows. Я же, как приверженец минимализма, буду ставить только то, что мне нужно в среде GNU Linux Debian без графической оболочки (иксов). В качестве IDE буду использовать VSCode из под виндовоза с установленным расширением Remote - SSH. Сперва выполняем классические команды: Код (Bash): sudo apt update sudo apt upgrade Затем ставим необходимые пакеты: Код (Bash): sudo apt install python3-pip git cmake ninja-build Не отходя от кассы делаем следующий трюк: Код (Bash): mkdir ~/.local/bin ln -sf /usr/bin/python3 ~/.local/bin/python source ~/.profile Это нужно для того, чтобы python вызывался из командной среды командой python, а не python3. Создаём папку (у меня это esp_idf, но может быть абсолютно любая), куда будем ставить ESP-IDF, переходим в неё и клонируем проект: Код (Bash): cd esp_idf git clone https://github.com/espressif/esp-idf.git --recursive Теперь ставим тулзы. Тулзы по умолчанию будут установлены в папку ~/.espressif. Меня это никак не устраивает, поэтому меняем куда нам нужно с помощью переменной окружения: Код (Bash): export IDF_TOOLS_PATH=/home/andrey/esp_idf/espressif По умолчанию они ставятся для всей линейки ESP32. Я же хочу только для своей платы: Код (Bash): ./install.sh esp32c3 После установки нам необходимо подготовить нашу командную оболочку для работы с ESP-IDF: Код (Bash): . ./export.sh Но проблема здесь в том, что при следующем сеансе работы нам необходимо вызвать этот скрипт заново. Чтобы каждый раз не перемещаться по файловой системе, на хабре я подсмотрел замечательный скрипт: Код (Bash): ESP_PATH="$HOME/esp_idf" IDF_TOOLS_PATH="$HOME/esp_idf/espressif" if [ -d "$ESP_PATH/esp-idf" ] ; then IDF_PATH="$ESP_PATH/esp-idf" export IDF_PATH if [ -f "$IDF_PATH/export.sh" ] ; then alias idfexp="\. $IDF_PATH/export.sh" fi fi if [ -f "$ESP_PATH/esp-adf" ] ; then ADF_PATH="$ESP_PATH/esp-adf" export ADF_PATH fi if [ -f "$ESP_PATH/esp-mdf" ] ; then MDF_PATH="$ESP_PATH/esp-mdf" export MDF_PATH fi if [ -d "$IDF_TOOLS_PATH" ] ; then export IDF_TOOLS_PATH fi который назвал .idfrc и положил в корень папки esp_idf. Вызов скрипта прописал в .bashrc: Код (Bash): if [ -f "$HOME/esp_idf/.idfrc" ]; then \. "$HOME/esp_idf/.idfrc" # This load Espressif environment fi Теперь перед началом работы достаточно вызвать Код (Bash): idfexp из любой рабочей папки и у меня будет готова командная оболочка для работы с ESP-IDF.
Как и прежде, мы будем поджигать RGB светодиод WS2812 по bluetooth из смартфона. Создаём пустой проект: Код (Bash): idf.py create-project BLExample И начинаем редактировать BLExample.c. Сперва настроим SPI. Код (C++): #include"driver/spi_master.h" #define MOSI_IO_NUM 8 #define SCLK_IO_NUM 9 #define T0 0b11000000 #define T1 0b11110000 #define WS2812_FILL_BUFFER (COLOR) \ for (uint8_t mask =0x80; mask; mask >>=1) \ { \ if (COLOR & mask) \ { \ *ptr++=T1; \ } \ else \ { \ *ptr++=T0; \ } \ } uint8_t ws2812_buffer[40]; uint8_t led_R=0; uint8_t led_G=0; uint8_t led_B=0; void SPI_Init(void) { spi_device_interface_config_t devcfg = { .clock_speed_hz = 6600000, .mode = 0, .queue_size = 7, }; spi_bus_config_t buscfg = { .mosi_io_num = MOSI_IO_NUM, .sclk_io_num = SCLK_IO_NUM, .max_transfer_sz = 40, }; spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_DISABLED); spi_bus_add_device(SPI2_HOST, &devcfg, &spi_handle); } voidapp_main(void) { SPI_Init(); } Клок SPI вытащил для диагностики логическим анализатором. Для работы WS2812 он не нужен. Частота тактирования SPI 6.6 МГц, режим работы здесь не сильно важен, поэтому 0. Максимальное количество байт, пересылаемых в рамках одной транзакции 40. С DMA не работаем, поэтому выключен. SPI, как и в прошлый раз, у нас выбран SPI2. Наш проект уже должен собраться, поэтому можем вызвать: Код (Bash): idf.py menuconfig Спойлер: Замечание Если терминал пишет, что команда idf.py не найдена, то необходимо вызвать алиас из скрипта Код (Bash): idfexp Но сейчас проект будет собран для SoC по умолчанию, а это ESP32. Либо не будет собран, если не установлен тулчейн (как в моём случае). Поэтому прежде всего необходимо указать целевой SoC для сборки проекта: Код (Bash): idf.py set-target esp32c3 После чего вызываем menuconfig и переходим в пункт выбора стека bluetooth: Как видим, нам доступно для выбора два стека: Bluedroid и NimBLE - BLE Only. Несмотря на то, что NimBLE хуже документирован, на мой первый взгляд он не так тонко позволяет настроить стек, как bluedroid, я сейчас выберу именно его. Затем, конечно же, поработаем с bluedroid и сравним. Здесь также, как и в Zephyr, конфигурацию проекта можно настроить путём записи необходимых опций в файл, чтобы каждый раз при пересборке не искать эти настройки в menuconfig. Для этого создадим файл sdkconfig.defaults: Код (Text): CONFIG_IDF_TARGET="esp32c3" CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE="4MB" # # BT config # CONFIG_BT_ENABLED=y CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n CONFIG_BTDM_CTRL_MODE_BTDM=n CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="LED BLE" В нём я сразу описываю наш SoC, чтобы каждый раз не вводить set-target, указываю количество памяти на борту, далее указываю настройки для стека bluetooth. Если имя опции не известно, то в menuconfig можно перейти по справке [?] или нажать [A]. Для переноса опции в файл sdkconfig.defaults необходимо добавить префикс CONFIG_
Документация на NimBLE лежит здесь https://mynewt.apache.org/latest/network/index.html Портированный NimBLE на плюсы (под ардуино) есть здесь: https://h2zero.github.io/esp-nimble-cpp/index.html Как мы уже знаем, для обнаружения периферийного устройства ему необходимо транслировать в эфир рекламу. За это отвечает профиль GAP. Возвращаемся в BLExample.c: Код (C++): void ble_app_advertise(void) { struct ble_hs_adv_fields fields; const char *device_name; memset(&fields, 0, sizeof(fields)); fields.flags = BLE_HS_ADV_F_BREDR_UNSUP | BLE_HS_ADV_F_DISC_GEN; fields.tx_pwr_lvl_is_present = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; device_name = ble_svc_gap_device_name(); fields.name = (uint8_t *)device_name; fields.name_len = strlen(device_name); fields.name_is_complete = 1; ble_gap_adv_set_fields(&fields); struct ble_gap_adv_params adv_params; memset(&adv_params, 0, sizeof(adv_params)); adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; adv_params.itvl_min = 2900; adv_params.itvl_max = 3000; ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL); } Здесь мы говорим: мы не поддерживаем режим BT Classic (BR/EDR), мы обыкновенное BLE устройство; в пакет рекламы мы включаем значение излучаемой мощности, также включаем имя устройства. В параметрах рекламы мы указываем, что к нам может подключиться любое устройство, мы ведём широковещательную рекламу без ограничений. Далее указываем интервал излучения пакетов рекламы. Указанные числа необходимо помножить на 0.625мс, чтобы получить реальное значение. В функцию ble_gap_adv_start мы передаём обратный вызов ble_gap_event - это обработчик событий. Давайте опишем его: Код (C++): static int ble_gap_event(struct ble_gap_event *event, void *arg) { switch (event->type) { case BLE_GAP_EVENT_CONNECT: ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK" : "FAILED!"); if (event->connect.status != 0) { ble_app_advertise(); } break; case BLE_GAP_EVENT_DISCONNECT: ESP_LOGI("GAP", "BLE GAP EVENT DISCONNECT %d", event->disconnect.reason); ble_app_advertise(); break; default: break; } return 0; } Для начала нам хватит и двух событий: событие подключения и события отключения. В событии подключения мы просто выводим отладочную информацию, а в событии отключения мы снова запускаем рекламу, т.к. наш стек при подключении перестаёт себя рекламировать. Создадим ещё одну вспомогательную функцию, которую вызовем впоследствии из main функции: Код (C++): void ble_app_on_sync(void) { ble_hs_id_infer_auto(0, &ble_addr_type); ble_app_advertise(); } В первой строчке мы создаём MAC адрес нашего устройства (у нас он будет статическим), после чего запускаем рекламу. Добавим в app_main инициализацию хоста стека NimBLE Код (C++): void app_main(void) { SPI_Init(); nimble_port_init(); } Запустить в работу bluetooth мы может только тогда, когда ност синхронизирован с контроллером: Код (C++): void app_main(void) { SPI_Init(); nimble_port_init(); ble_hs_cfg.sync_cb=ble_app_on_sync; } Теперь вызовем не библиотечную функцию, которую подготовили создатели ESP-IDF. Код (C++): ble_svc_gap_init(); Эта функция создаст обязательный первичный сервис. Теперь осталось это всё запустить на выполнение: Код (C++): void host_task(void *param) { nimble_port_run(); } Функцию вызываем в отдельной задаче freertos: Код (C++): void app_main(void) { SPI_Init(); nimble_port_init(); ble_hs_cfg.sync_cb = ble_app_on_sync; ble_svc_gap_init(); nimble_port_freertos_init(host_task); } Осталось добавить заголовочные файлы: Код (C++): #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" #include "host/ble_hs.h" #include "services/gap/ble_svc_gap.h" Собрать, прошить и открыть в терминале: Код (Bash): idf.py flash monitor команда flash самостоятельно вызовет build, если будут обнаружены изменения в проекте.
Спойлер: BLExample.c Код (C++): #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" #include "host/ble_hs.h" #include "services/gap/ble_svc_gap.h" #include "driver/spi_master.h" #define MOSI_IO_NUM 8 #define SCLK_IO_NUM 9 #define T0 0b11000000 #define T1 0b11110000 #define WS2812_FILL_BUFFER(COLOR) \ for (uint8_t mask = 0x80; mask; mask >>= 1) \ { \ if (COLOR & mask) \ { \ *ptr++ = T1; \ } \ else \ { \ *ptr++ = T0; \ } \ } uint8_t ws2812_buffer[40]; uint8_t led_R = 0; uint8_t led_G = 0; uint8_t led_B = 0; spi_device_handle_t spi_handle; uint8_t ble_addr_type; void ble_app_advertise(void); void SPI_Init(void) { spi_device_interface_config_t devcfg = { .clock_speed_hz = 6600000, .mode = 0, .queue_size = 7, }; spi_bus_config_t buscfg = { .mosi_io_num = MOSI_IO_NUM, .sclk_io_num = SCLK_IO_NUM, .max_transfer_sz = 40, }; spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_DISABLED); spi_bus_add_device(SPI2_HOST, &devcfg, &spi_handle); } static int ble_gap_event(struct ble_gap_event *event, void *arg) { struct ble_gap_conn_desc desc; int rc; switch (event->type) { case BLE_GAP_EVENT_CONNECT: ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK" : "FAILED!"); if (event->connect.status != 0) { ble_app_advertise(); } break; case BLE_GAP_EVENT_DISCONNECT: ESP_LOGI("GAP", "BLE GAP EVENT DISCONNECT %d", event->disconnect.reason); ble_app_advertise(); break; default: break; } return 0; } void ble_app_advertise(void) { struct ble_hs_adv_fields fields; const char *device_name; memset(&fields, 0, sizeof(fields)); fields.flags = BLE_HS_ADV_F_BREDR_UNSUP | BLE_HS_ADV_F_DISC_GEN; fields.tx_pwr_lvl_is_present = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; device_name = ble_svc_gap_device_name(); fields.name = (uint8_t *)device_name; fields.name_len = strlen(device_name); fields.name_is_complete = 1; ble_gap_adv_set_fields(&fields); struct ble_gap_adv_params adv_params; memset(&adv_params, 0, sizeof(adv_params)); adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; adv_params.itvl_min = 2900; adv_params.itvl_max = 3000; ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL); } void ble_app_on_sync(void) { ble_hs_id_infer_auto(0, &ble_addr_type); ble_app_advertise(); } void host_task(void *param) { nimble_port_run(); } void app_main(void) { SPI_Init(); nimble_port_init(); ble_hs_cfg.sync_cb = ble_app_on_sync; ble_svc_gap_init(); nimble_port_freertos_init(host_task); } После подключения в терминале видим следующее: Код (Bash): E (348) phy_init: esp_phy_load_cal_data_from_nvs: NVS has not been initialized. Call nvs_flash_init before starting WiFi/BT. W (358) phy_init: failed to load RF calibration data (0x1101), falling back to full calibration I (398) BLE_INIT: Bluetooth MAC: dc:54:75:9a:6b:62 I (408) NimBLE: GAP procedure initiated: stop advertising. I (408) NimBLE: Failed to restore IRKs from store; status=8 I (408) NimBLE: GAP procedure initiated: advertise; I (418) NimBLE: disc_mode=2 I (418) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=2900 adv_itvl_max=3000 I (428) NimBLE: I (428) main_task: Returned from app_main() Видим Error и Warning - мы это в дальнейшём исправим. Также видим минимальный и максимальный периоды извещения рекламы. Их надо умножить на 0.625мс, чтобы получить действительный период в мс. Теперь берём смартфон и запускаем nRF Connect и радостно наблюдаем наше устройство: Здесь мы можем наблюдать всю информацию, которую наше устройство передаёт вместе с рекламой. 9дБм - это мощность излучения передатчика по умолчанию при рассылке рекламы. При желании её можно изменить на любую другую. Там же видим период рассылки рекламы - он примерно совпадает с тем, что мы выставили в параметрах. Если нажмём на MORE и перейдём во вкладку flags & services, то увидим флаги, которые выставили: Можем подключится, но ничего, кроме обязательного сервиса мы не увидим: Оно и понятно - никаких характеристик мы не добавляли. Терминал же нам выдаст следующее: Код (Bash): I (217978) GAP: BLE GAP EVENT CONNECT OK Т.е. подключение прошло успешно. Теперь, если обратно вернёмся в сканер, то там уже не будет нашего устройства. Его не будет и на любом другом устройстве - оно больше не вещает и к подключению не доступно. Нам необходимо разорвать соединение, тогда оно снова вернётся к рекламе и будет доступно для подключения.
Определение характеристик в сервисе в ESP-IDF осуществляется с помощью заполнения полей структуры Код (C++): static const struct ble_gatt_svc_def gatt_svcs[] = { {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = BLE_UUID16_DECLARE(0x0180), .characteristics = (struct ble_gatt_chr_def[]){ {.uuid = BLE_UUID16_DECLARE(0xFEF4), .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, .access_cb = ledR_cb}, {.uuid = BLE_UUID16_DECLARE(0xDEAD), .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, .access_cb = ledG_cb}, {.uuid = BLE_UUID16_DECLARE(0xFD01), .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, .access_cb = ledB_cb}, {0}}}, {0}}; Мы определили первичный сервис, все его три характеристики, которые доступны как для чтения, так и для записи, их uuid (в данном случае я пишу только изменяемую часть. Остальное вставит ESP-IDF). Также указали имя функций, которые будут вызваны при попытке обращения к значениям атрибута. Теперь типовая функция-обработчик: Код (C++): static int ledB_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { spi_transaction_t spi_trans; switch (ctxt->op) { case BLE_GATT_ACCESS_OP_READ_CHR: os_mbuf_append(ctxt->om, &led_B, sizeof led_B); break; case BLE_GATT_ACCESS_OP_WRITE_CHR: led_B = *ctxt->om->om_data; memset(&spi_trans, 0, sizeof(spi_trans)); spi_trans.length = 40 * 8; uint8_t *ptr = &ws2812_buffer[8]; WS2812_FILL_BUFFER(led_G); WS2812_FILL_BUFFER(led_R); WS2812_FILL_BUFFER(led_B); spi_trans.tx_buffer = ws2812_buffer; spi_device_transmit(spi_handle, &spi_trans); break; default: break; } return 0; } В отличии от Zephyr, здесь у нас одна функция, а тип запроса мы читаем из поля op принятого контекста. Для остальных двух цветов всё аналогично. В app-main добавим инициализацию контроллера флеш памяти: Код (C++): nvs_flash_init(); а также зарегистрируем наши характеристики: Код (C++): ble_gatts_count_cfg(gatt_svcs); ble_gatts_add_svcs(gatt_svcs); Весь наш файл с теперь такой: Спойлер: BLExample.c Код (C++): /* */ #include <stdio.h> #include "nvs_flash.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" #include "host/ble_hs.h" #include "services/gap/ble_svc_gap.h" #include "driver/spi_master.h" #define MOSI_IO_NUM 8 #define SCLK_IO_NUM 9 #define T0 0b11000000 #define T1 0b11110000 #define WS2812_FILL_BUFFER(COLOR) \ for (uint8_t mask = 0x80; mask; mask >>= 1) \ { \ if (COLOR & mask) \ { \ *ptr++ = T1; \ } \ else \ { \ *ptr++ = T0; \ } \ } uint8_t ws2812_buffer[40]; uint8_t led_R = 0; uint8_t led_G = 0; uint8_t led_B = 0; spi_device_handle_t spi_handle; uint8_t ble_addr_type; void ble_app_advertise(void); void SPI_Init(void) { spi_device_interface_config_t devcfg = { .clock_speed_hz = 6600000, .mode = 0, .queue_size = 7, }; spi_bus_config_t buscfg = { .mosi_io_num = MOSI_IO_NUM, .sclk_io_num = SCLK_IO_NUM, .max_transfer_sz = 40, }; spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_DISABLED); spi_bus_add_device(SPI2_HOST, &devcfg, &spi_handle); } static int ledR_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { spi_transaction_t spi_trans; switch (ctxt->op) { case BLE_GATT_ACCESS_OP_READ_CHR: os_mbuf_append(ctxt->om, "Value of ledR", strlen("Value of ledR")); break; case BLE_GATT_ACCESS_OP_WRITE_CHR: led_R = *ctxt->om->om_data; memset(&spi_trans, 0, sizeof(spi_trans)); spi_trans.length = 40 * 8; uint8_t *ptr = &ws2812_buffer[8]; WS2812_FILL_BUFFER(led_G); WS2812_FILL_BUFFER(led_R); WS2812_FILL_BUFFER(led_B); spi_trans.tx_buffer = ws2812_buffer; spi_device_transmit(spi_handle, &spi_trans); break; default: break; } return 0; } static int ledG_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { spi_transaction_t spi_trans; switch (ctxt->op) { case BLE_GATT_ACCESS_OP_READ_CHR: os_mbuf_append(ctxt->om, "Value of ledG", strlen("Value of ledG")); break; case BLE_GATT_ACCESS_OP_WRITE_CHR: led_G = *ctxt->om->om_data; memset(&spi_trans, 0, sizeof(spi_trans)); spi_trans.length = 40 * 8; uint8_t *ptr = &ws2812_buffer[8]; WS2812_FILL_BUFFER(led_G); WS2812_FILL_BUFFER(led_R); WS2812_FILL_BUFFER(led_B); spi_trans.tx_buffer = ws2812_buffer; spi_device_transmit(spi_handle, &spi_trans); break; default: break; } return 0; } static int ledB_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { spi_transaction_t spi_trans; switch (ctxt->op) { case BLE_GATT_ACCESS_OP_READ_CHR: os_mbuf_append(ctxt->om, &led_B, sizeof led_B); break; case BLE_GATT_ACCESS_OP_WRITE_CHR: led_B = *ctxt->om->om_data; memset(&spi_trans, 0, sizeof(spi_trans)); spi_trans.length = 40 * 8; uint8_t *ptr = &ws2812_buffer[8]; WS2812_FILL_BUFFER(led_G); WS2812_FILL_BUFFER(led_R); WS2812_FILL_BUFFER(led_B); spi_trans.tx_buffer = ws2812_buffer; spi_device_transmit(spi_handle, &spi_trans); break; default: break; } return 0; } static const struct ble_gatt_svc_def gatt_svcs[] = { {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = BLE_UUID16_DECLARE(0x0180), .characteristics = (struct ble_gatt_chr_def[]){ {.uuid = BLE_UUID16_DECLARE(0xFEF4), .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, .access_cb = ledR_cb}, {.uuid = BLE_UUID16_DECLARE(0xDEAD), .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, .access_cb = ledG_cb}, {.uuid = BLE_UUID16_DECLARE(0xFD01), .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, .access_cb = ledB_cb}, {0}}}, {0}}; static int ble_gap_event(struct ble_gap_event *event, void *arg) { struct ble_gap_conn_desc desc; int rc; switch (event->type) { case BLE_GAP_EVENT_CONNECT: ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK" : "FAILED!"); if (event->connect.status != 0) { ble_app_advertise(); } break; case BLE_GAP_EVENT_DISCONNECT: ESP_LOGI("GAP", "BLE GAP EVENT DISCONNECT %d", event->disconnect.reason); ble_app_advertise(); break; default: break; } return 0; } void ble_app_advertise(void) { struct ble_hs_adv_fields fields; const char *device_name; memset(&fields, 0, sizeof(fields)); fields.flags = BLE_HS_ADV_F_BREDR_UNSUP | BLE_HS_ADV_F_DISC_GEN; fields.tx_pwr_lvl_is_present = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; device_name = ble_svc_gap_device_name(); fields.name = (uint8_t *)device_name; fields.name_len = strlen(device_name); fields.name_is_complete = 1; ble_gap_adv_set_fields(&fields); struct ble_gap_adv_params adv_params; memset(&adv_params, 0, sizeof(adv_params)); adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; adv_params.itvl_min = 2900; adv_params.itvl_max = 3000; ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL); } void ble_app_on_sync(void) { ble_hs_id_infer_auto(0, &ble_addr_type); ble_app_advertise(); } void host_task(void *param) { nimble_port_run(); } void app_main(void) { SPI_Init(); nvs_flash_init(); nimble_port_init(); ble_hs_cfg.sync_cb = ble_app_on_sync; ble_svc_gap_init(); ble_gatts_count_cfg(gatt_svcs); ble_gatts_add_svcs(gatt_svcs); nimble_port_freertos_init(host_task); } Собираем-прошиваем и в терминале убеждаемся, что больше нет ни ошибок, ни предупреждений. Снова подключаемся nRF Connect и уже нам доступны наши харакетристики: Теперь, если мы что-то запишем (в HEX формате), то наш светодиод загорится соответствующим цветом
Теперь давайте умерим аппетит ESP32C3. В ESP-IDF практически уже всё есть - требуется только настройка. Сперва необходимо добавить опции в sdkconfig.defaults: Разрешаем modem sleep Код (Bash): CONFIG_BT_CTRL_MODEM_SLEEP=y CONFIG_BT_CTRL_MODEM_SLEEP_MODE_1=y Выбираем источник тактирования для радиочасти в момент, когда весь SoC находится в режиме light sleep. У нас на выбор есть несколько осцилляторов. Первая часть - это RC осцилляторы, но их выбор крайне не рекомендуется из-за низкой стабильности частоты (высокого ppm). Вторая часть - это стабилизированные кварцевым резонатором осцилляторы. У ESP32C3 доступно два таких источника - это главный осциллятор на 40 МГц и часовой. Но часового кварца на моей плате нет, поэтому я выбираю кварц на 40МГц: Код (Bash): # Bluetooth low power clock CONFIG_BT_CTRL_LPCLK_SEL_MAIN_XTAL=y # Power up main XTAL during light sleep CONFIG_BT_CTRL_MAIN_XTAL_PU_DURING_LIGHT_SLEEP=y Разрешаем выключать PHY в момент light sleep: Код (Bash): # Enable power down of MAC and baseband in light sleep mode CONFIG_ESP_PHY_MAC_BB_PD=y Разрешаем работу power management Код (Bash): # Enable support for power management CONFIG_PM_ENABLE=y Этот блок будет масштабировать тактовую частоту процессора В заключение настроим free rtos Код (Bash): # Enable tickless idle mode CONFIG_FREERTOS_USE_TICKLESS_IDLE=y # Set the tick rate at which FreeRTOS does pre-emptive context switching. CONFIG_FREERTOS_HZ=1000 # Minimum number of ticks to enter sleep mode for CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3 Теперь перемещаемся в наш код и конфигурируем power management - задаём диапазон частот, а также разрешаем light sleep. Принудительно в light sleep не уходим. Решение об этом принимает стек NimBLE. Код (C++): void config_pm(void) { #if CONFIG_PM_ENABLE esp_pm_config_t pm_config = { .max_freq_mhz = 160, .min_freq_mhz = 10, #if CONFIG_FREERTOS_USE_TICKLESS_IDLE .light_sleep_enable = true }; #endif ESP_ERROR_CHECK(esp_pm_configure(&pm_config)); #endif } Добавляем вызов этой функции в самое начало app_main. Не забываем про заголовочный файл. Код (C++): #include"esp_pm.h" void app_main(void) { SPI_Init(); config_pm(); /////// } Чтобы применились новые опции, указанные в sdkconfig.defaults, необходимо удалить файл sdkconfig. Собираем-прошиваем-подключаем: в выводе мы видим наши настроенные частоты, разрешение light sleep, а также то, что в режиме сна bluetooth тактируется от осциллятора на 40 МГц (main XTAL). Попробуем на ощупь температуру SoC....а он абсолютно холодный! При этом мы видим наше устройство в nRF Connect, можем подключиться к нему, поуправлять светодиодом. Но какой же ток?
5 миллиампер. Это, правда, в режиме рекламы. В момент трансляции пакета рекламы будут импульсы в районе 35 мА. Впрочем, если вычесть потребление самой платы, а это около 2.6 мА, то полученный результат стыкуется с данными, которые предоставляет сам Espressif. Из этой таблицы видно, что при тактировании от часового кварца, ток в режиме рекламы так и вообще на порядок ниже. Если говорить об автономной работе от аккумулятора с габаритами 18650, то его полного заряда хватит на год. Что очень хорошо. Но на моей плате нет часового кварца, поэтому будем довольствоваться на 40МГц. Давайте подключимся и посмотрим на ток. А малыш-то подрос. Оно и не удивительно - в момент соединения происходит постоянный обмен между центральным и периферийным устройствами. Давайте внесём изменения в код - создадим функцию, которая будет печатать параметры связи после установления соединения: Код (C++): static void ble_print_conn_desc(struct ble_gap_conn_desc *desc) { MODLOG_DFLT(INFO, "conn_itvl=%d conn_latency=%d supervision_timeout=%d \n", desc->conn_itvl, desc->conn_latency, desc->supervision_timeout); } Вызывать её будем в обработчике события установившегося соединения: Код (C++): case BLE_GAP_EVENT_CONNECT: ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK" : "FAILED!"); if (event->connect.status == 0) { ble_gap_conn_find(event->connect.conn_handle, &desc); ble_print_conn_desc(&desc); } else { ble_app_advertise(); } break; Прошиваемся и смотрим: Код (Bash): I (489584) GAP: BLE GAP EVENT CONNECT OK I (489585) NimBLE: conn_itvl=36 conn_latency=0 supervision_timeout=500 conn_itvl - интервал соединения - интервал времени, в течение которого периферийное устройство спит. Данное число необходимо умножить на 0.625 мс, чтобы получить действительное время. Latency - число, которое отображает сколько пакетов можно потерять/пропустить периферийному устройству для экономии энергии. Supervision_timeout - если в течение данного времени не будет получено ни одного пакета, соединение будет считаться утраченным. Данное число необходимо умножить на 10 мс, чтобы получить действительное время.
Можем ли мы изменить параметры соединения? В нашем случае вопрос не к протоколу BLE, а к тому ПО, которое мы используем на смартфоне. В частности, nRF Connect позволяет это сделать, но только в виде трёх пресетов. Выбираем пресет LOW POWER и следим за током: Как можем видеть, он заметно снизился. Я измеряю в режиме TrueRMS с открытым входом, чтобы получить интегрированное по времени значение. Если измерять в режиме постоянного тока, то мы получим всё те же 5 мА с периодическим всплесками, что никак не позволит оценить картину в общем. Возникает закономерный вопрос: мы изменяем параметры, но в терминале ничего не происходит. Возможно ли отследить событие изменения параметров соединения? Да, стек NimBLE позволяет это сделать. Необходимо перехватить два события: BLE_GAP_EVENT_CONN_UPDATE_REQ - запрос на изменение параметров соединения и BLE_GAP_EVENT_CONN_UPDATE - изменение параметров соединения обработано. Допишем в функции ble_gap_event: Код (C++): case BLE_GAP_EVENT_CONN_UPDATE_REQ: ESP_LOGI("GAP", "connection request update"); ble_print_conn_params(event->conn_update_req.self_params); break; case BLE_GAP_EVENT_CONN_UPDATE: ESP_LOGI("GAP", "connection updated; status=%d ", event->conn_update.status); rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc); assert(rc == 0); ble_print_conn_desc(&desc); break; И запишем функцию, которая печатает принятые периферийным устройством параметры (self_params) соединения: Код (C++): static void ble_print_conn_params(const struct ble_gap_upd_params *params) { MODLOG_DFLT(INFO, "Minimum value for connection interval=%d Maximum value for connection interval=%d " "Supervision timeout=%d Minimum length of connection event=%d Maximum length of connection event=%d\n", params->itvl_min, params->itvl_max, params->supervision_timeout, params->min_ce_len, params->max_ce_len); } Смотрим в монитор: Код (Bash): I (40609) GAP: BLE GAP EVENT CONNECT OK I (40609) NimBLE: conn_itvl=36 conn_latency=0 supervision_timeout=500 I (41025) GAP: connection request update I (41025) NimBLE: Minimum value for connection interval=6 Maximum value for connection interval=6 Supervision timeout=500 Minimum length of connection event=0 Maximum length of connection event=0 I (41497) GAP: connection updated; status=0 I (41498) NimBLE: conn_itvl=6 conn_latency=0 supervision_timeout=500 I (41550) GAP: connection request update I (41550) NimBLE: Minimum value for connection interval=36 Maximum value for connection interval=36 Supervision timeout=500 Minimum length of connection event=0 Maximum length of connection event=0 I (41651) GAP: connection updated; status=0 I (41652) NimBLE: conn_itvl=36 conn_latency=0 supervision_timeout=500 Теперь посылаем запрос со смартфона на изменение параметров соединения: Код (Bash): I (253805) GAP: connection request update I (253805) NimBLE: Minimum value for connection interval=80 Maximum value for connection interval=100 Supervision timeout=500 Minimum length of connection event=0 Maximum length of connection event=0 I (254368) GAP: connection updated; status=0 I (254369) NimBLE: conn_itvl=100 conn_latency=2 supervision_timeout=500 Запрос принят, как и параметры соединения. А может ли периферийное устройство самостоятельно устанавливать параметры соединения? Конечно, может. Для этого необходимо заполнить поля следующей структуры: Код (C++): static struct ble_gap_upd_params conn_params = { .itvl_min = 120, .itvl_max = 150, .latency = 3, .supervision_timeout = 1500, // .min_ce_len = 0, // .max_ce_len = 0, }; Увлекаться интервалами соединения не стоит - от этого зависит скорость обмена информацией и, как следствие, скорость реакции периферийного устройства на запросы снаружи. supervision_timeout должен быть не меньше, чем: (1 + latency) * интервал_соединения * 2. Последние два поля (длина интервала события) зарезервированы на будущее и должны быть равны нулю. Для уведомления стека о параметрах соединения, необходимо их передать в функции ble_gap_update_params(). Я это сделаю сразу после установления соединения: Код (C++): case BLE_GAP_EVENT_CONNECT: ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK" : "FAILED!"); if (event->connect.status != 0) { ble_app_advertise(); } else { rc = ble_gap_update_params(event->connect.conn_handle, &conn_params); if (rc != 0) { ESP_LOGE("GAP", "Failed to update params; rc = %d", rc); } ble_gap_conn_find(event->connect.conn_handle, &desc); ble_print_conn_desc(&desc); } break;
Вывод в терминале: Спойлер: вывод Код (Bash): I (30569) GAP: BLE GAP EVENT CONNECT OK I (30569) NimBLE: GAP procedure initiated: I (30570) NimBLE: connection parameter update; conn_handle=1 itvl_min=120 itvl_max=150 latency=3 supervision_timeout=1500 min_ce_len=0 max_ce_len=0 I (30581) NimBLE: I (30586) NimBLE: conn_itvl=36 conn_latency=0 supervision_timeout=500 I (30783) GAP: connection request update I (30784) NimBLE: Minimum value for connection interval=120 Maximum value for connection interval=150 Supervision timeout=1500 Minimum length of connection event=0 Maximum length of connection event=0 I (31293) GAP: connection updated; status=0 I (31293) NimBLE: conn_itvl=150 conn_latency=3 supervision_timeout=1500 I (32235) GAP: connection request update I (32236) NimBLE: Minimum value for connection interval=6 Maximum value for connection interval=6 Supervision timeout=500 Minimum length of connection event=0 Maximum length of connection event=0 I (36965) GAP: connection updated; status=0 I (36966) NimBLE: conn_itvl=6 conn_latency=0 supervision_timeout=500 I (37041) GAP: connection request update I (37041) NimBLE: Minimum value for connection interval=120 Maximum value for connection interval=150 Supervision timeout=1500 Minimum length of connection event=0 Maximum length of connection event=0 I (37163) GAP: connection updated; status=0 I (37163) NimBLE: conn_itvl=150 conn_latency=3 supervision_timeout=1500 Мы запросили изменения, nRF Connect их подтвердил и применил. Теперь, если мы попробуем прочитать атрибуты, а затем выбирем пресет HIGH и снова прочитаем, то разница в скорости реакции заметна невооруженным взглядом. И всё каазлось бы замечательно, но на языке вертится вопрос: так это шо, любая обезьяна с гранатаой со смартфоном сможет дёрнуть чеку подключиться к моим шторам и управлять ими!?