Esp32c2. насыпаем bluetooth. esp-idf

Тема в разделе "ESP8266, ESP32", создана пользователем parovoZZ, 23 июн 2023.

Метки:
  1. parovoZZ

    parovoZZ Гуру

    Начало здесь:
    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.
     
    8bitai и ИгорьК нравится это.
  2. parovoZZ

    parovoZZ Гуру

    Как и прежде, мы будем поджигать 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:
    upload_2023-6-23_12-50-8.png

    Как видим, нам доступно для выбора два стека: 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_
     
    8bitai, ИгорьК и DetSimen нравится это.
  3. parovoZZ

    parovoZZ Гуру

    Документация на 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, если будут обнаружены изменения в проекте.
     
    8bitai и ИгорьК нравится это.
  4. parovoZZ

    parovoZZ Гуру

    Код (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 и радостно наблюдаем наше устройство:

    upload_2023-6-23_16-9-28.png
    Здесь мы можем наблюдать всю информацию, которую наше устройство передаёт вместе с рекламой. 9дБм - это мощность излучения передатчика по умолчанию при рассылке рекламы. При желании её можно изменить на любую другую. Там же видим период рассылки рекламы - он примерно совпадает с тем, что мы выставили в параметрах.
    Если нажмём на MORE и перейдём во вкладку flags & services, то увидим флаги, которые выставили:
    upload_2023-6-23_16-13-21.png
    Можем подключится, но ничего, кроме обязательного сервиса мы не увидим:
    upload_2023-6-23_16-11-39.png
    Оно и понятно - никаких характеристик мы не добавляли.

    Терминал же нам выдаст следующее:
    Код (Bash):
    I (217978) GAP: BLE GAP EVENT CONNECT OK
    Т.е. подключение прошло успешно. Теперь, если обратно вернёмся в сканер, то там уже не будет нашего устройства. Его не будет и на любом другом устройстве - оно больше не вещает и к подключению не доступно. Нам необходимо разорвать соединение, тогда оно снова вернётся к рекламе и будет доступно для подключения.
     
    8bitai и ИгорьК нравится это.
  5. parovoZZ

    parovoZZ Гуру

    Определение характеристик в сервисе в 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);
    Весь наш файл с теперь такой:
    Код (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 и уже нам доступны наши харакетристики:
    upload_2023-6-23_17-22-50.png

    Теперь, если мы что-то запишем (в HEX формате), то наш светодиод загорится соответствующим цветом
    upload_2023-6-23_17-32-11.png
     
    8bitai и ИгорьК нравится это.
  6. parovoZZ

    parovoZZ Гуру

    Теперь давайте умерим аппетит 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.
    Собираем-прошиваем-подключаем:
    upload_2023-6-24_0-8-11.png
    в выводе мы видим наши настроенные частоты, разрешение light sleep, а также то, что в режиме сна bluetooth тактируется от осциллятора на 40 МГц (main XTAL).
    Попробуем на ощупь температуру SoC....а он абсолютно холодный!
    При этом мы видим наше устройство в nRF Connect, можем подключиться к нему, поуправлять светодиодом. Но какой же ток?
     
    8bitai и ИгорьК нравится это.
  7. ИгорьК

    ИгорьК Гуру

    Таки С2 или С3?
     
  8. parovoZZ

    parovoZZ Гуру

    upload_2023-6-26_18-45-40.png
    5 миллиампер. Это, правда, в режиме рекламы. В момент трансляции пакета рекламы будут импульсы в районе 35 мА.
    upload_2023-6-26_18-49-13.png
    Впрочем, если вычесть потребление самой платы, а это около 2.6 мА, то полученный результат стыкуется с данными, которые предоставляет сам Espressif.
    upload_2023-6-26_18-53-1.png
    Из этой таблицы видно, что при тактировании от часового кварца, ток в режиме рекламы так и вообще на порядок ниже. Если говорить об автономной работе от аккумулятора с габаритами 18650, то его полного заряда хватит на год. Что очень хорошо. Но на моей плате нет часового кварца, поэтому будем довольствоваться на 40МГц.
    Давайте подключимся и посмотрим на ток.
    upload_2023-6-26_19-17-33.png
    А малыш-то подрос. Оно и не удивительно - в момент соединения происходит постоянный обмен между центральным и периферийным устройствами. Давайте внесём изменения в код - создадим функцию, которая будет печатать параметры связи после установления соединения:
    Код (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 мс, чтобы получить действительное время.
     
    8bitai нравится это.
  9. parovoZZ

    parovoZZ Гуру

    Можем ли мы изменить параметры соединения? В нашем случае вопрос не к протоколу BLE, а к тому ПО, которое мы используем на смартфоне. В частности, nRF Connect позволяет это сделать, но только в виде трёх пресетов.
    upload_2023-6-26_19-57-38.png
    Выбираем пресет LOW POWER и следим за током:
    upload_2023-6-26_19-58-48.png

    Как можем видеть, он заметно снизился. Я измеряю в режиме 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;
     
    8bitai нравится это.
  10. parovoZZ

    parovoZZ Гуру

    Вывод в терминале:
    Код (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 и снова прочитаем, то разница в скорости реакции заметна невооруженным взглядом.

    И всё каазлось бы замечательно, но на языке вертится вопрос: так это шо, любая обезьяна с гранатаой со смартфоном сможет дёрнуть чеку подключиться к моим шторам и управлять ими!?
     

    Вложения:

    • BLExample.c
      Размер файла:
      8,4 КБ
      Просмотров:
      106
  11. parovoZZ

    parovoZZ Гуру

    без разницы. С2, судя по даташиту, младшая сестра С3.