Платформа с web-камерой на ESP32

    Идея собрать мобильную платформу с web-камерой на борту появилась практически спонтанно. Мне хотелось иметь в арсенале скромной домашней автоматизации что-то вроде IP-камеры. И тут вопрос не столь в цене или качестве, сколь в своеобразном творческом эксперименте. Материалом для вдохновения были различные статьи DIY и проекты вроде этого.

    image

    Собранная конструкция выглядит так:



    Комплектующие


    В качестве базы выступает мобильная двухпалубная робо-платформа Car Chassis 2WD Mini Kit

    image

    Размеры платформы: 135 мм х 135 мм х 80 мм

    Приводом являются два стандартных мотор-колеса с редуктором и двигателем постоянного тока с растровыми дисками для датчиков скорости:

    • номинальный ток: 250 мА макс. при напряжении 3,6 В
    • крутящий момент 800 г/см (при напряжении 6В)
    • напряжение питания: 6 — 8 В
    • скорость вращения без нагрузки: 170 об/мин (при напряжении 3,6 В)
    • передаточное число редуктора: 1: 48
    • оси выходят с двух сторон
    • диаметр осей: 5 мм
    • размеры: 64x20x20 мм
    • вес: 26 г


    В качестве драйвера электродвигателя выбран модуль MX1508.
    Почитать про модуль можно здесь.

    image

    Технические параметры:

    • Напряжение питания: 2 — 10 В
    • Рабочий ток на один канал: 1.5 А (пиковый ток 2.5 А, не более 10 секунд)
    • Входной сигнал логика: 5 В
    • Габариты: 24,7 х 21 х 0,5 мм


    Для горизонтального и вертикального перемещения IP-камеры выбраны популярные серводвижки SG90 2кг.

    image

    На сайте производителя представлена следующая спецификация:

    • Weight: 9g
    • Dimension: 23×12.2x29mm
    • Stall torque: 1.8kg/cm(4.8v)
    • Gear type: POM gear set
    • Operating speed: 0.1sec/60degree(4.8v)
    • Operating voltage: 4.8v
    • Temperature range: 0℃_ 55℃
    • Dead band width: 1us
    • Power Supply: Through External Adapter
    • servo wire length: 25 cm
    • Servo Plug: JR (Fits JR and Futaba)


    Для web-камеры был выбран держатель FPV Bracket Kit.

    image

    Описание держателя в интернет-магазине: “FPV позволит ориентировать FPV-камеру в 3-х плоскостях. Простое подключение и управление позволит быстро собрать и подключить платформу к контроллеру или полетному контроллеру. Используется вместе с сервоприводами EMAX 9g ES08A Mini Servo или сервами SG90 (с некоторыми доработками).”
    “С некоторыми доработками” — следует учитывать, набор пришлось дорабатывать напильником в прямом смысле слова. Но для DIY за $3 вполне ничего. Некоторые жаловались, что даже доработка не помогла и сервы не подошли по размерам, в моем же случае все нормально. Используется два движка SG90 для горизонтального и вертикального перемещения камеры. Вариант спроектировать и распечатать на 3D-принтере тоже рассматривался, но остановился пока на этом держателе.

    IP-камера на базе ESP32 CAM

    image

    Согласно описанию: “The I2S subsystem in the ESP32 also provides a high speed bus connected directly to RAM for Direct Memory Access. Putting it simply, you can configure the ESP32 I2S subsystem to send or receive parallel data under hardware control.”
    То есть можно настроить интерфейс I2S ESP32 для отправки или получения параллельных данных под аппаратным управлением, что и реализовано для подключения камеры. Разработкой данной платы занимается компания Seeed Studio, здесь представлена цена $9.90, но в наших радиомагазинах продают за $8, видимо не только Seeed Studio их может производить.

    Технические данные:

    • The smallest 802.11b/g/n Wi-Fi BT SoC Module
    • Low power 32-bit CPU,can also serve the application processor
    • Up to 160MHz clock speed,Summary computing power up to 600 DMIPS
    • Built-in 520 KB SRAM, external 4MPSRAM
    • Supports UART/SPI/I2C/PWM/ADC/DAC
    • Support OV2640 and OV7670 cameras,Built-in Flash lamp.
    • Support image WiFI upload
    • Support TF card
    • Supports multiple sleep modes.
    • Embedded Lwip and FreeRTOS
    • Supports STA/AP/STA+AP operation mode
    • Support Smart Config/AirKiss technology
    • Support for serial port local and remote firmware upgrades (FOTA)


    Источник питания


    Управление платформы от автономного питания длительное время без подзарядки не предусматривалось. Поэтому в качестве источника был выбран модуль питания 2А на 18650 с USB-выходом с одним слотом.

    image

    Характеристики:
    • Тип аккумулятора: 18650 Li-Ion (без защиты)
    • Напряжение зарядного устройства: от 5В до 8В
    • Выходные напряжения:
    • 3В — непосредственно с аккумулятора через защитное устройство
    • 5В — через повышающий преобразователь.
    • Максимальный выходной ток:
    • Выход 3В — 1А
    • Выход 5В — 2А
    • Максимальный ток зарядки: 1А
    • Тип входного разъёма: micro-USB
    • Тип выходного разъёма: USB-A
    • Потребителей 5В — от перегрузки и короткого замыкания
    • Габаритные размеры:
    • Печатная плата: 29,5 х 99,5 х 19 мм
    • Всего устройства: 30 х 116 х 20 мм


    В качестве основного контроллера выбран ESP-WROOM-32

    image

    Ранее я описывал характеристики ESP32 более детально. Здесь приведу базовые характеристики модуля:
    • 32-битный двуядерный микропроцессор Xtensa LX6 до 240 МГц
    • Флеш-память: 4 МБ
    • Беспроводная связь Wi-Fi 802.11b/g/n до 150 Мб/c, Bluetooth 4.2 BR/EDR/BLE
    • Поддержка STA/AP/STA+AP режимов, встроенный стек TCP/IP
    • GPIO 32 (UART, SPI, I2C, I2S интерфейсы, ШИМ, SD контроллеры, емкостные сенсорные, АЦП, ЦАП и не только
    • Питание: через microUSB разъем (CP2102 преобразователь) или выводы
    • Шаг выводов: 2.54 мм (можно вставить в макетную плату)
    • Размер платы: 5.2 х 2.8 см


    В качестве датчиков скорости применены два оптических энкодеров “Noname” для подсчета импульсов вращения растровых дисков мотор-колеса.

    image

    Характеристики:
    • Напряжение питания: 3,3В — 5В
    • Ширина паза датчика: 6 мм;
    • Тип выхода: аналоговый и цифровой
    • Индикатор: cостояние выхода


    Для измерения расстояния применен популярный ультразвуковой дальномер HC-SR04.

    image

    Характеристики:
    • Напряжение питания: 5 В
    • Потребление в режиме тишины: 2 мА
    • Потребление при работе: 15 мА
    • Диапазон расстояний: 2–400 см
    • Эффективный угол наблюдения: 15
    • Рабочий угол наблюдения: 30°


    Программные реализации


    Первым шагом стало знакомство и прошивка модуля ESP32 CAM.
    Описание работы с модулем представлены на Харбе, здесь, здесь, а также на других ресурсах.
    В основном статьи описывают простой процесс прошивки с помощью Arduino IDE. В большинстве случает этого достаточно и меня по-началу такой вариант тоже устраивал.

    image

    В радиомагазинах модули ESP32-CAM продаются с камерой OV2640, поэтому в скетче необходимо сделать небольшое изменение:

    // Select camera model
    //#define CAMERA_MODEL_WROVER_KIT
    //#define CAMERA_MODEL_ESP_EYE
    //#define CAMERA_MODEL_M5STACK_PSRAM
    //#define CAMERA_MODEL_M5STACK_WIDE
    #define CAMERA_MODEL_AI_THINKER
    


    А также указать SSID и пароль к точке доступа Wi-Fi.

    const char* ssid = "REPLACE_WITH_YOUR_SSID";
    const char* password = "REPLACE_WITH_YOUR_PASSWORD";
    


    Одним из условий работы web-камеры в моем случае является возможность передавать видеопоток через прокси-сервер Keenetic. Я использую домашний роутер Keenetik Viva. Сервис KeenDNS предоставляет доменное имя домашнему web-ресурсу. Но к моему удивлению первая попытка завершилась неудачей. При попытке удаленного доступа через интернет я получил ошибку «Header fields are too long for server to interpret». С подобной проблемой я столкнулся не первый. Решением вопроса является изменение конфигурации CONFIG_HTTPD_MAX_REQ_HDR_LEN, например:

    #define CONFIG_HTTPD_MAX_REQ_HDR_LEN 2048
    


    В случае с Arduino IDE ESP32 модули уже скомпилированы и представлены в виде статических библиотек, которые располагаются в Windows по пути — %userprofile%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\tools\sdk\
    Просто изменение параметра в заголовке ничего не даст.
    То есть для изменения конфигурации нам необходимо перекомпилировать библиотеки ESP-IDF.
    Решением стало клонирование проекта github.com/espressif/esp-who. В каталоге с примерами находим проект camera_web_server, делаем изменение параметра максимальной длины заголовка, ну а также не забываем указать настройки подключения к Wi-Fi.

    image

    Для того, чтобы проект скомпилировался, пришлось установить еще один чек-бокс — Support array ‘rtc_gpio_desc’ for ESP32.

    image

    После успешной компиляции и загрузки проекта переходим по соответствующему IP адресу в браузере и попадаем на страницу с интерфейсом нашей web-камеры.

    image

    Интерфейс похож на Arduino-примеры, но добавлен некоторый функционал.

    Я внес небольшие изменения в исходный файл app_httpd.c для управления сигналом вывода GPIO_NUM_2 с web-интерфейса. Хотя в описании модуля говорится об использовании пинов для нужд SD-карты, но я ее не использую, поэтому могу задействовать данные пины.

    void app_httpd_main()
    {
    	gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
    
    static esp_err_t cmd_handler(httpd_req_t *req)
    {
    .......
    //строчка 736
    else if(!strcmp(variable, "gpio2")) {
        		if (val == 0)
                    gpio_set_level(GPIO_NUM_2, 0);
                else
                    gpio_set_level(GPIO_NUM_2, 1);
        	}
    


    Для дистанционного управления я наверстал незамысловатую панель на Node-Red, которая крутится на Raspberry pi.

    image

    Изображение видеопотока удалось встроить в ноду template:

    <iframe 
        src="http://192.168.1.61"
        width="300" height="300">
    </iframe>
    


    Тут важен один момент: необходимо встраивать именно http, в случае https будут проблемы с Content-Security-Policy. Если же и в этом случае будут возникать проблемы, то можно попробовать добавить заголовки:

    <script>
        var meta = document.createElement('meta');
        meta.httpEquiv = "Content-Security-Policy";
        meta.content = "default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';";
    document.getElementsByTagName('head')[0].appendChild(meta);
    </script>
    


    Для управления пином GPIO_NUM_2 модуля ESP32-CAM после внесения изменений в прошивку следует выполнять следующий http GET запрос:

    http://192.168.1.61/control?var=gpio2&val=1 //или 0
    


    На интерфейсе панели — это переключатель wakeup, в рабочем потоке все выглядит так

    image

    где функция request:

    var newMsg = {}
    var i = msg.payload ? 1 : 0;
    newMsg.query = "control?var=gpio2&val=" + i
    node.send(newMsg)
    


    Настройки ноды http request:

    image

    Остальные параметры и статусы передаются по MQTT.

    Подключение к Wi-Fi и MQTT


    Я приведу примеры, используя Arduino фреймворк, так с ним я также экспериментировал. Но в итоге рабочее приложение у меня на ESP-IDF.

    Подключение заголовка #include <WiFi.h>

    Функция подключения к Wi-Fi для Arduino фреймворка
    void setup_wifi()
    {
      Serial.println("Starting connecting WiFi.");
      delay(1000);
      for (int8_t i = 0; i < 3; i++)
      {
        WiFi.begin(ssid, password);
        uint32_t start = millis();
        while (WiFi.status() != WL_CONNECTED && ((millis() - start) < 4000))
        {
          Serial.print(".");
          delay(500);
        }
        if (WiFi.status() == WL_CONNECTED)
        {
          Serial.println("WiFi connected");
          Serial.println("IP address: ");
          Serial.println(WiFi.localIP());
          return;
        }
        else
        {
          Serial.println("Connecting Failed");
          //WiFi.reconnect(); // this reconnects the AP so the user can stay on the page
        }
      }
    }
    



    В функции присутствует цикл на три итерации, так как с первой попытки часто не подключается, а затем бесконечно ждет статуса WL_CONNECTED. Может как-то по-другому еще можно решить, но так точно работает.

    Подключение к MQTT для Arduino-фреймворка выполняется с помощью библиотеки github.com/knolleary/pubsubclient.git.

    Для использования библиотеки необходимо подключить заголовок #include <PubSubClient.h>

    Функция подключения к MQTT
    bool setup_mqtt()
    {
      if (WiFi.status() == WL_CONNECTED)
      {
        if (!client.connected())
        {
          client.setServer(mqtt_server, 1883);
          client.setCallback(callback);
        }
        Serial.print("Connecting to MQTT server ");
        Serial.print(mqtt_server);
        Serial.println("...");
        String clientId = "ESP32_car_client";
        if (client.connect(clientId.c_str()))
        {
          Serial.println("Connected to MQTT server ");
          //subscribing to topics
          client.subscribe("esp32/car/#");
          client.subscribe("esp32/camera/#");
          return true;
        }
        else
        {
          Serial.println("Could not connect to MQTT server");
          return false;
        }
      }
      return false;
    }
    



    Вначале мы проверяем, что подключены к Wi-Fi, затем подключаемся к брокеру client.setServer(mqtt_server, 1883);

    И устанавливаем коллбек функцию client.setCallback(callback);

    MQTT коллбек функция
    void callback(char *topic, byte *payload, unsigned int length)
    {
      Serial.println("Message arrived ");
      memset(payload_buf, 0, 10);
      for (int i = 0; i < length; i++)
      {
        payload_buf[i] = (char)payload[i];
      }
    
      command_t mqtt_command = {
          .topic = topic,
          .message = payload_buf};
      xQueueSend(messageQueue, (void *)&mqtt_command, 0);
    }
    



    В случае успешного подключения подписываемся на топики:

    client.subscribe("esp32/car/#");
    client.subscribe("esp32/camera/#");
    


    Были замечены случаи “отваливания” MQTT соединения, поэтому была добавлена проверка в периодическую задачу опроса.

    Периодическая задача опроса
    void pollingTask(void *parameter)
    {
      int32_t start = 0;
    
      while (true) {
        if (!client.connected()) {
          long now = millis();
          if (now - start > 5000) {
            start = now;
            // Attempt to reconnect
            if (setup_mqtt()) {
              start = 0;
            }
          }
        }
        else {
          client.loop();
          int val = digitalRead(WAKEUP_PIN);
          if (val == LOW) {
            Serial.println("Going to sleep now");
            esp_deep_sleep_start();
          }
        }
        vTaskDelay(100 / portTICK_PERIOD_MS);
      }
      vTaskDelete(NULL);
    }
    



    Пример подключения к Wi-FI и MQTT c помощью ESP-IDF был описан в предыдущей статье.

    В случае использования ESP-IDF сбоев при подключении к Wi-Fi и MQTT не наблюдалось. Один нюанс при обработке данных из MQTT топика в функции esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event): при типе события MQTT_EVENT_DATA следует учитывать параметры event->topic_len и event->data_len и брать имя топика и данные именно соответствующей длины, в противном случае мы получим мусор. Для этого мы можем создать буферные массивы или выделить память динамически (затем освободить ее), и скопировать данные, например так:

    strncpy(topic, event->topic, event->topic_len);
    strncpy(data, event->data, event→data_len);
    


    Отправка данных в топик производится с помощью функции esp_mqtt_client_publish

    esp_mqtt_client_publish(client, topics[i], topic_buff[i], 0,0,0);
    


    Обработка данных ультразвукового датчика HC-SR04


    HC-SR04 – доступный и популярный датчик для проектирования устройств с микроконтроллерами. Как обычно — в сети куча материала на эту тему: здесь и здесь. Описание также можно посмотреть здесь, а краткий даташит — здесь.
    Если коротко, то для начала измерения расстояния необходимо подать высокий сигнал длительностью 10 μs на пин Trig. Это инициирует передачу сенсором 8 циклов ультразвукового импульса с частотой 40 кГц и ожидания отраженного ультразвукового импульса. Когда датчик обнаруживает ультразвуковой сигнал от приемника, он устанавливает для вывода Echo высокий уровень и задержку на период (ширину), пропорциональный расстоянию. Чтобы вычислить расстояние необходимо вычислить формулу:

    distance = duration * 340 м/с = duration * 0.034 м/мкс, где

    340 м/с — скорость распространения звука в воздухе.

    image

    В Arduino-фреймворке функция pulseIn позволяет узнать длительность импульса в μs
    Для ESP-IDF есть проект ESP-IDF Components library, в котором также есть компонент ultrasonic для HC-SR04.

    Пример кода
    esp_err_t ultrasonic_measure_cm(const ultrasonic_sensor_t *dev, uint32_t max_distance, uint32_t *distance)
    {
        CHECK_ARG(dev && distance);
    
        PORT_ENTER_CRITICAL;
    
        // Ping: Low for 2..4 us, then high 10 us
        CHECK(gpio_set_level(dev->trigger_pin, 0));
        ets_delay_us(TRIGGER_LOW_DELAY);
        CHECK(gpio_set_level(dev->trigger_pin, 1));
        ets_delay_us(TRIGGER_HIGH_DELAY);
        CHECK(gpio_set_level(dev->trigger_pin, 0));
    
        // Previous ping isn't ended
        if (gpio_get_level(dev->echo_pin))
            RETURN_CRITICAL(ESP_ERR_ULTRASONIC_PING);
    
        // Wait for echo
        int64_t start = esp_timer_get_time();
        while (!gpio_get_level(dev->echo_pin))
        {
            if (timeout_expired(start, PING_TIMEOUT))
                RETURN_CRITICAL(ESP_ERR_ULTRASONIC_PING_TIMEOUT);
        }
    
        // got echo, measuring
        int64_t echo_start = esp_timer_get_time();
        int64_t time = echo_start;
        int64_t meas_timeout = echo_start + max_distance * ROUNDTRIP;
        while (gpio_get_level(dev->echo_pin))
        {
            time = esp_timer_get_time();
            if (timeout_expired(echo_start, meas_timeout))
                RETURN_CRITICAL(ESP_ERR_ULTRASONIC_ECHO_TIMEOUT);
        }
        PORT_EXIT_CRITICAL;
    
        *distance = (time - echo_start) / ROUNDTRIP;
    
        return ESP_OK;
    }
    



    В комментариях присутствует объяснение к алгоритму. Измерение длительности импульса происходит в цикле while пока уровень сигнала высокий на Echo пине (после // got echo, measuring) после чего расстояние измеряется:

    *distance = (time - echo_start) / ROUNDTRIP
    

    Коэффициент для получения расстояния в сантиметрах ROUNDTRIP = 58.

    В Arduino-фреймворке это выглядит еще проще:

    Пример кода
    #include "ultrasonic.h"
    
    portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
    #define PORT_ENTER_CRITICAL portENTER_CRITICAL(&mux)
    #define PORT_EXIT_CRITICAL portEXIT_CRITICAL(&mux)
    
    Ultrasonic::Ultrasonic() {
      pinMode(GPIO_NUM_33, OUTPUT);
      pinMode(GPIO_NUM_26, INPUT);
    }
    
    uint32_t Ultrasonic::calculateDistance() {
        PORT_ENTER_CRITICAL;
        digitalWrite(GPIO_NUM_33, LOW);
        delayMicroseconds(2);
        digitalWrite(GPIO_NUM_33, HIGH);
        delayMicroseconds(10);
        digitalWrite(GPIO_NUM_33, LOW);
        duration = pulseIn(GPIO_NUM_26, HIGH);
        PORT_EXIT_CRITICAL;
        distance = duration / 58;
        return distance;
    }
    
    uint32_t Ultrasonic::getDistance() {
        return distance;
    }
    



    Была попытка использования библиотеки ultrasonic ESP-IDF для Arduino проекта ESP32, но работает это дело до первого сбоя датчика. Почему так — точно выяснить не удалось. Сбой датчика — это периодический просчет в импульсах и выдача ложных показаний, в вычисленных цифрах он выглядит как расстояние более 20000 см. На форумах пишут, что это из-за некачественного датчика (китайская копия).

    Измерение скорости с помощью оптических датчиков


    Оптический модуль считывания импульсов создан на основе компаратора LM393 и щелевого датчика. Предназначен для использования с растровыми дисками, которые одеваются на вал редуктора или электродвигателя.

    Как обычно на эту тему уже есть статьи: digitrode.ru, mirrobo.ru, и arduino-kit.ru.

    Схема датчика:

    image

    В Arduino-фреймворке мы вычисляем скорость следующим образом.
    Определяем переменную (структуру) счетчика, например:
    typedef struct {
      int encoder_pin = ENCODER_PIN; // pulse output from the module
      unsigned int rpm = 0; // rpm reading
      volatile byte pulses = 0; // number of pulses
      unsigned long timeold = 0;
      unsigned int pulsesperturn = 20;
    } pulse_t;
    


    Затем в функции setup мы должны зарегистрировать входной пин и прерывание на него:

    pinMode(pulse_struct.encoder_pin, INPUT);
    attachInterrupt(pulse_struct.encoder_pin, counter, FALLING);
    


    Далее вычисляется количество оборотов в минуту:

    pulse_struct.rpm = 
            (60 * 1000 / pulse_struct.pulsesperturn )/ 
            (1000)* pulse_struct.pulses;
    


    Пример кода
    void pulseTask(void *parameters) {
      sensor_data_t data;
      data.sensor = OPTICAL_SENSOR;
      portBASE_TYPE xStatus;
    
       while (true) {
          //Don't process interrupts during calculations
          detachInterrupt(0);
          pulse_struct.rpm = 
            (60 * 1000 / pulse_struct.pulsesperturn )/ 
            (1000)* pulse_struct.pulses;
          pulse_struct.pulses = 0;
          data.value = pulse_struct.rpm;
          //Restart the interrupt processing
          attachInterrupt(0, counter, FALLING);
          Serial.print("optical: ");
          Serial.println(data.value);
         //Sending data to sensors queue
        xStatus = xQueueSend(sensorQueue, (void *)&data, 0);
        if( xStatus != pdPASS ) {
         printf("Could not send optical to the queue.\r\n");
        }
        taskYIELD();
        vTaskDelay(1000 / portTICK_PERIOD_MS);
       }
    }
    



    В ESP-IDF для этих целей можно использовать аппаратный счетчик PCNT, который был описан в предыдущей статье.

    Пример кода обрабатываемой задачи
    typedef struct {
          uint16_t delay; //delay im ms
          int pin;
          int ctrl_pin;
          pcnt_channel_t channel;
          pcnt_unit_t unit;
          int16_t count;
    } speed_sensor_params_t;
    
    void pulseTask(void *parameters) {
      sensor_data_t data_1;
      sensor_data_t data_2;
      data_1.sensor = OPTICAL_SENSOR_1;
      data_2.sensor = OPTICAL_SENSOR_2;
      portBASE_TYPE xStatus;
    
      speed_sensor_params_t params_1 = {
          .delay = 100,
          .pin = ENCODER_1_PIN,
          .ctrl_pin = GPIO_NUM_0,
          .channel = PCNT_CHANNEL_0,
          .unit = PCNT_UNIT_0,
          .count = 0,
      };
        ESP_ERROR_CHECK(init_speed_sensor(&params_1));
    
        speed_sensor_params_t params_2 = {
          .delay = 100,
          .pin = ENCODER_2_PIN,
          .ctrl_pin = GPIO_NUM_1,
          .channel = PCNT_CHANNEL_0,
          .unit = PCNT_UNIT_1,
          .count = 0,
      };
        ESP_ERROR_CHECK(init_speed_sensor(&params_2));
    
        while(true) {
            data_1.value = calculateRpm(&params_1);
            data_2.value = calculateRpm(&params_2);
            sensor_array[OPTICAL_SENSOR_1] = data_1.value;
            sensor_array[OPTICAL_SENSOR_2] = data_2.value;
            printf("speed 1 = %d\n", data_1.value);
            printf("speed 2 = %d\n", data_2.value);
            xStatus = xQueueSend(sensorQueue, (void *)&data_1, 0);
            xStatus = xQueueSend(sensorQueue, (void *)&data_2, 0);
            if( xStatus != pdPASS ) {
            printf("Could not send optical to the queue.\r\n");
            }
            vTaskDelay(100 / portTICK_PERIOD_MS);
    }
    }
    



    ШИМ управление


    Про управление сервоприводами на Arduino можно почитать на developer.alexanderklimov, wiki.amperka.ru.
    Как сказано в источнике выше: “Сервопривод — это механизм с электромотором, который может поворачиваться в заданный угол и удерживать текущее положение.” Практически мы имеем дело с широтно-импульсной модуляцией, где от ширины импульса сигнала зависит угол поворота привода.

    image

    Для ESP32 на Arduino-фреймворке можно использовать библиотеку ESP32Servo.

    Для этого мы подключаем заголовок:

    #include <ESP32Servo.h>


    Создаем объект:

    Servo servo_horisontal;


    Указываем выходной пин:

     servo_horisontal.attach(SERVO_CAM_HOR_PIN);


    После этого можем записывать необходимое значение величины поворота:

    servo_horisontal.write(value);


    ШИМ управление для других типов устройств на Arduino-фреймворке производится с помощью библиотеки esp32-hal-ledc.h.
    Микроконтроллеры ESP32 не поддерживают стандартную функцию Arduino analogWrite() для ШИМ. Вместо их предусмотрены функции:
    ledcSetup(channel, freq, resolution_bits) — указываются канал, частота и разрешение;
    ledcAttachPin(GPIO, channel) — указываются порт и канал;
    ledcWrite(channel, dutycycle) — указываются канал и коэффициент заполнения ШИМ-сигнала.
    Примеры можно увидеть по ссылке.
    Как видно из названия, изначально функции проектировались для управления светодиодными модулями, но их также используют и для других целей.

    В ESP-IDF фреймворке управление серво-приводом осуществляется так же, как и управление коллекторным с использованием модуля MCPWM, как описано в предыдущей статье. Пример MCPWM servo motor control можно посмотреть здесь.

    Пример кода
    static uint32_t servo_per_degree_init(uint32_t degree_of_rotation)
    {
        uint32_t cal_pulsewidth = 0;
        cal_pulsewidth = (SERVO_MIN_PULSEWIDTH + (((SERVO_MAX_PULSEWIDTH -          SERVO_MIN_PULSEWIDTH) * (degree_of_rotation)) / (SERVO_MAX_DEGREE)));
        return cal_pulsewidth;
    }
    
    void mcpwm_example_servo_control(void *arg)
    {
        uint32_t angle, count;
        //1. mcpwm gpio initialization
        mcpwm_example_gpio_initialize();
    
        //2. initial mcpwm configuration
        printf("Configuring Initial Parameters of mcpwm......\n");
        mcpwm_config_t pwm_config;
        pwm_config.frequency = 50;    //frequency = 50Hz, i.e. for every servo motor time period should be 20ms
        pwm_config.cmpr_a = 0;    //duty cycle of PWMxA = 0
        pwm_config.cmpr_b = 0;    //duty cycle of PWMxb = 0
        pwm_config.counter_mode = MCPWM_UP_COUNTER;
        pwm_config.duty_mode = MCPWM_DUTY_MODE_0;
        mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);    //Configure PWM0A & PWM0B with above settings
        while (1) {
            for (count = 0; count < SERVO_MAX_DEGREE; count++) {
                printf("Angle of rotation: %d\n", count);
                angle = servo_per_degree_init(count);
                printf("pulse width: %dus\n", angle);
                mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, angle);
                vTaskDelay(10);     //Add delay, since it takes time for servo to rotate, generally 100ms/60degree rotation at 5V
            }
        }
    }
    



    То есть нам необходимо инициализировать модуль с помощью функции mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);
    А затем задавать значение угла
    mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, angle);

    C примерами использования модуля MCPWM для разных типов привода можно ознакомиться на github.
    Пример управления коллекторным двигателем также был представлен в предыдущей статье.

    Хочется отметить, что подобная платформа представляет собой дифференциально-управляемую неголономную систему. Двигатели имеют разброс в характеристиках, поэтому приходится задавать программное смещение для одного их них для обеспечения равномерной скорости. Ознакомиться с теорией можно на сайте robotosha.ru. Для оптимального управления мотор-редукторами применен PID-алгоритм с обратной связью в виде оптических датчиков. Описание алгоритма представлено здесь и здесь.
    Описание уравнений движения, а также алгоритмов управления выходит за рамки данной статьи. Дифференциальная кинематика еще не реализована в коде.

    Sleep Modes


    Согласно документации, а также описанию в статье , ESP32 может переключаться между различными режимами питания:
    • Active mode
    • Modem Sleep mode
    • Light Sleep mode
    • Deep Sleep mode
    • Hibernation mode


    В таблице приведены различия потребления тока в разных режимах.

    image

    Я задействовал режим Deep Sleep mode в случае отсутствия высокого сигнала на пине GPIO_NUM_13:

    gpio_set_direction(WAKEUP_PIN, GPIO_MODE_INPUT);
    esp_sleep_enable_ext0_wakeup(WAKEUP_PIN,1); //1 = High, 0 = Low
    


    В случае отсутствия внешнего воздействия я подтянул вход 10к резистором к 3.3 В, хотя можно и программно. А в задаче периодического опроса проверяю состояние сигнала входа:

    if(!gpio_get_level(WAKEUP_PIN)) {
             printf("Going to sleep now\n");
            esp_deep_sleep_start();
        }
    


    На этом буду завершать описание. Выше был показан практический пример использования модулей ESP32 с различной периферией. Также затронуты некоторые вопросы программной реализации и сравнение подходов ESP-IDF и Arduino.
    EPAM
    Компания для карьерного и профессионального роста

    Комментарии 8

      +1
      Было бы интересно, если бы камера учувствовала в ориентации платформы в пространстве. В полетных контроллерах современных дронов эта функция вполне пашет.
        0
        Идея неплохая. Надо изучить эту фичу. Не обязательно к этой платформе. Тема технического зрения весьма актуальна.
          0

          Как-то попытался добавить серво в аналогичный вариант камеры, только код в ESP-IDF, тоже внес задатчик в Web интерфейс, но при старте потока вся система падает.

            0
            У меня тоже последняя версия кода — на ESP-IDF. Я данные передаю по MQTT, и затем работаю со строками:
            case MQTT_EVENT_DATA:
            ESP_LOGI(TAG, «MQTT_EVENT_DATA»);
            printf(«TOPIC=%.*s\r\n», event->topic_len, event->topic);
            printf(«DATA=%.*s\r\n», event->data_len, event->data);
            memset(topic, 0, strlen(topic));
            memset(data, 0, strlen(data));
            strncpy(topic, event->topic, event->topic_len);
            strncpy(data, event->data, event->data_len);
            command_t command = {
            .topic = topic,
            .message = data,
            };
            parseCommand(&command);
            break;
              0
              А зачем две платы esp32?
                0
                Так проще в реализации. ESP32-CAM имеет мало свободных GPIO, а также необходимо оптимизировать прошивку. Хотя варианты могут быть разные. Во-вторых, у меня уже были в наличии эти модули. В-третьих, идёт полное разделение фукционала, платформа и камера могут независимо функционировать. Недостатки я также понимаю: два модуля вместо одного; больше потребление энергии при одновременной работе модулей, дополнительное подключение к Wi-Fi.
                  0
                  Если интересно, я тоже похожий проект сделал, правда без сервоприводов зато на одной плате.
                  Реализиация так же на espidf. Исходники тоже там

                  kirillyatsenko.medium.com/esp32-wi-fi-rc-car-with-the-video-camera-9e23d4309399
                    0
                    Круто! Все лаконично. Как я понял управление драйвером L298N производится дискретно подачей высоких или низких уровней на пины 12, 13, 14, 15. Я тоже с этим драйвером пробовал, мощная штука, но пока остановился на MX1508, одной батарейки хватает. Насчет питания плат, если в работе только камера, то центральный контроллер перевожу в режим сна, а потом бужу внешним сигналом с ESP32CAM.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое