Dispositivos de programación basados ​​en el módulo ESP32

Un microcontrolador es un circuito integrado capaz de ejecutar programas. En la actualidad, existen muchos modelos de este tipo en el mercado de una variedad de fabricantes. Los precios de estos dispositivos continúan cayendo. Los chips de un solo chip se utilizan ampliamente en una amplia variedad de campos: desde instrumentos de medición hasta entretenimiento y todo tipo de electrodomésticos. A diferencia de las computadoras personales, un microcontrolador combina las funciones de un procesador y dispositivos periféricos en un cristal, contiene RAM y memoria de solo lectura para almacenar código y datos, pero tiene significativamente menos recursos informáticos. ESP32 es un microcontrolador desarrollado por Espressif Systems. ESP32 es un sistema en chip con controladores integrados de Wi-Fi y Bluetooth. La serie ESP32 utiliza núcleoTensilica Xtensa LX6 . Las placas con ESP32 tienen una buena potencia informática, los periféricos desarrollados son muy populares debido a sus bajos precios en el rango de $ 7 - $ 14: Aliexpress , Amazon .



Este artículo no pretende ser una guía exhaustiva, sino más bien una colección de fuentes de material y recomendaciones. En el artículo, quiero abordar los problemas que tuve que enfrentar al elegir las herramientas de software para el desarrollo de proyectos, así como algunos casos de aplicación práctica de los módulos ESP32. En el siguiente artículo, quiero mostrar un ejemplo ilustrativo del uso del ESP32 como controlador de control para una pequeña plataforma móvil de dos ruedas. Por lo tanto, aquí consideraremos detalles como:



  • Elegir un entorno de desarrollo;
  • Configurar el entorno de trabajo, compilar y cargar el proyecto ESP-IDF;
  • Procesamiento de señales de entrada / salida GPIO;
  • Modulación de ancho de pulso mediante módulo MCPWM;
  • Contador de hardware PCNT;
  • Conexión WI-Fi y MQTT.


Descripción general del módulo ESP32-WROOM-32E



Según la hoja de datos, el módulo contiene:



MCU



  • ESP32-D0WD-V3 integrado, microprocesador Xtensa LX6 de doble núcleo de 32 bits, hasta 240 MHz
  • ROM de 448 KB para funciones básicas y de arranque
  • 520 KB SRAM para datos e instrucciones
  • SRAM de 16 KB en RTC


Wifi



  • 802.11b / g / n
  • Tasa de bits: 802.11n hasta 150 Mbps
  • Agregación A-MPDU y A-MSDU
  • Soporte de intervalo de guarda de 0,4 µs
  • Rango de frecuencia central del canal operativo: 2412 ~ 2484 MHz


Bluetooth



  • Especificación Bluetooth V4.2 BR / EDR y Bluetooth LE
  • Transmisor de clase 1, clase 2 y clase 3
  • AFH
  • CVSD y SBC


Hardware



  • Interfaces: SD card, UART, SPI, SDIO, I 2 C, LED PWM, Motor PWM, I 2 S, IR, pulse counter, GPIO, capacitive touch sensor, ADC, DAC
  • 40 MHz crystal oscillator
  • 4 MB SPI flash
  • Operating voltage/Power supply: 3.0 ~ 3.6 V
  • Operating temperature range: –40 ~ 85 °C
  • Dimensions: See Table 1


Certification



  • Bluetooth certification: BQB
  • RF certification: FCC/CE-RED/SRRC
  • Green certification: REACH/RoHS


imagen

Diagrama de bloques funcionales



Se pueden encontrar más detalles sobre las características del microcontrolador en Wikipedia .



El módulo se basa en el microcircuito ESP32-D0WD-V3 *. El chip integrado está diseñado teniendo en cuenta la escalabilidad y la adaptabilidad. La unidad central de procesamiento contiene dos núcleos que se pueden controlar individualmente y la velocidad del reloj de la CPU se puede ajustar de 80 MHz a 240 MHz. El chip también tiene un coprocesador de baja potencia que se puede usar en lugar de la CPU para ahorrar energía cuando se realizan tareas que no requieren mucha potencia informática, como monitorear el estado de los pines. ESP32 integra un amplio conjunto de periféricos que van desde sensores táctiles capacitivos, sensores Hall, interfaz de tarjeta SD, Ethernet, SPI de alta velocidad, UART, I²S e I²C.



La documentación técnica se presenta en el recurso oficial .



La información sobre el pinout del módulo ESP-WROOM-32 se puede encontrar fácilmente en los espacios abiertos de la red, como aquí



Elegir un entorno de desarrollo



IDE de Arduino



Los microcontroladores de la familia AVR, y luego la plataforma Arduino, aparecieron mucho antes que el ESP32. Una de las características clave del Arduino es su barrera de entrada relativamente baja, lo que permite a casi cualquier persona crear algo rápida y fácilmente. La plataforma ha hecho una contribución importante a la comunidad de hardware de código abierto y ha permitido que se unan un gran número de radioaficionados. El IDE de Arduino se puede descargar gratis desde fuera del sitio . A pesar de las limitaciones obvias en comparación con un entorno de desarrollo profesional, el IDE de Arduino cubre el 90% de lo que se requiere para proyectos de hobby. La red también tiene una cantidad suficiente de artículos sobre la instalación y configuración del IDE de Arduino para la programación de módulos ESP32, por ejemplo: núcleo Arduino para el ESP32 , habr.com, Voltiq.ru y randomnerdtutorials.com .



Al programar el ESP32 en el entorno Arduino, debe tener en cuenta el pinout como se indica en la página arduino-esp32 .



imagen

Configuración de pines del módulo ESP32



La principal ventaja de este enfoque de desarrollo es la entrada rápida y la facilidad de creación de proyectos utilizando los mismos principios que para Arduino. Y también el uso de muchas bibliotecas, como para Arduino. Otra característica interesante es la capacidad de combinar bibliotecas Arduino y principios de diseño con el marco ESP-IDF original.



PlatformIO



Como se indica en el recurso oficial : “IDE multiplataforma PlatformIO y depurador unificado · Analizador de código estático y prueba de unidad remota. Sistema de compilación multiplataforma y multiplataforma · Explorador de archivos de firmware e inspección de memoria »En otras palabras, PlatformIO es un ecosistema para el desarrollo de dispositivos integrados que admite múltiples plataformas, incluidas Arduino y ESP32. El IDE es Visual Studio Code o Atom. La instalación y configuración es bastante simple: después de instalar el editor de código, seleccione PlatformIO de la lista de complementos e instálelo. Nuevamente, hay muchos materiales sobre este tema en la red, comenzando por la fuente oficial aquí y aquí , y continuando con artículos con ilustraciones detalladas aquí y aquí....



En comparación con Arduino IDE, PlatformIO tiene todas las cualidades de un entorno de desarrollo moderno: organización de proyectos, compatibilidad con complementos, finalización de código y mucho más.



Una característica del desarrollo en PlatformIO es una estructura de proyecto unificada para todas las plataformas



project_dir
├── lib
│   └── README
├── platformio.ini
└── src
    └── main.cpp


Cada proyecto de PlatformIO contiene un archivo de configuración llamado platformio.ini en la raíz del proyecto. platformio.ini tiene secciones (cada una indicada por un [título]) y pares clave / valor dentro de las secciones. Líneas que comienzan con un punto y punto y coma ";" se ignoran y se pueden utilizar para comentarios. Los parámetros de varios valores se pueden especificar de dos formas:



  1. separando el valor con "," (coma + espacio);
  2. formato de varias líneas, donde cada nueva línea comienza con al menos dos espacios.


La siguiente característica de desarrollo para ESP32 es la capacidad de elegir un marco: Arduino o ESP-IDF. Al elegir Arduino como marco, obtenemos los beneficios de desarrollo descritos anteriormente.



imagen



PlatformIO incluye herramientas convenientes para crear, descargar y depurar proyectos



imagen



Marco de desarrollo de Espressif IoT



Para ESP32, Espressif ha desarrollado un marco llamado IoT Development Framework conocido como “ESP-IDF”. Se puede encontrar en Github . El proyecto contiene muy buena documentación y se suministra con ejemplos que puede tomar como base. La configuración y configuración del entorno está bien documentada en la sección Introducción . Hay varias opciones para instalar y trabajar con el marco.



Clonando un proyecto del repositorio e instalando manualmente las utilidades.



Clonando un proyecto de Github



mkdir -p ~/esp
cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git


Para Windows, la instalación de las utilidades de desarrollo es posible usando el instalador o usando scripts para la línea de comando:



cd %userprofile%\esp\esp-idf
install.bat


Para PowerShell



cd ~/esp/esp-idf
./install.ps1


Para Linux y macOS



cd ~/esp/esp-idf
./install.sh


El siguiente paso es configurar variables de entorno . Si las herramientas de desarrollo se instalaron en Windows usando el instalador, se agrega un acceso directo a la consola de comandos al menú y al escritorio, después de lo cual puede abrir el shell de comandos y trabajar con proyectos. Alternativamente, para ejecutar un shell de comandos de Windows:



%userprofile%\esp\esp-idf\export.bat


o Windows PowerShell:



.$HOME/esp/esp-idf/export.ps1


Linux y macOS:



. $HOME/esp/esp-idf/export.sh


Debe prestar atención al espacio entre el punto y la ruta del script.



Más adelante en la guía, se recomienda agregar un alias al script para configurar las variables de entorno en el perfil de usuario si está trabajando en Linux o macOS. Para hacer esto, copie y pegue el siguiente comando en su perfil de shell (.profile, .bashrc, .zprofile, etc.):



alias get_idf='. $HOME/esp/esp-idf/export.sh'


Al llamar al comando get_idf en la consola, se exportan las variables de entorno necesarias. En mi caso, también fue necesario registrar un alias para iniciar el entorno virtual de Python



alias esp_va=’source $HOME/.espressif/python_env/idf4.2_py2.7_env/bin/activate’


y agréguelo al siguiente alias



alias get_idf='esp_ve && . $HOME/esp/esp-idf/export.sh'


Para crear un nuevo proyecto desde cero, puede clonar las fuentes de github.com o copiar desde el directorio con ejemplos esp-idf / examples / get-started / hello_world /.



La información sobre la estructura del proyecto, compilación, carga, utilidades de configuración, etc. se encuentra aquí .



El proyecto es un directorio con la siguiente estructura:



- myProject/
             - CMakeLists.txt
             - sdkconfig
             - components/ - component1/ - CMakeLists.txt
                                         - Kconfig
                                         - src1.c
                           - component2/ - CMakeLists.txt
                                         - Kconfig
                                         - src1.c
                                         - include/ - component2.h
             - main/       - CMakeLists.txt
                           - src1.c
                           - src2.c

             - build/


La configuración del proyecto está contenida en el archivo sdkconfig en el directorio raíz. Para cambiar la configuración, debe llamar al comando idf.py menuconfig (o posiblemente idf.py.exe menuconfig en Windows).



Por lo general, se crean dos aplicaciones en un proyecto: "aplicación de proyecto" (el archivo ejecutable principal, es decir, su firmware personalizado) y "aplicación de cargador de arranque" (programa del cargador de arranque del proyecto).

Los "componentes" son piezas modulares de código autónomo que se compilan en bibliotecas estáticas (archivos .a) y se vinculan a la aplicación. Algunos de estos son proporcionados por el propio ESP-IDF, otros pueden obtenerse de otras fuentes.



La utilidad de línea de comandos idf.py proporciona una interfaz para administrar fácilmente las compilaciones de proyectos. Su ubicación en Windows es% userprofile% \. Espressif \ tools \ idf-exe \ 1.0.1 \ idf.py.exe. Ella controla los siguientes instrumentos:



  • CMake: configura el proyecto para construir
  • Constructor de proyectos de consola: Ninja o GNU Make)
  • esptool.py - para flashear módulos.


Cada proyecto tiene un archivo CMakeLists.txt de nivel superior que contiene la configuración de compilación para todo el proyecto. La configuración de archivo mínima incluye las siguientes líneas requeridas:



cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(myProject)


Un proyecto ESP-IDF se puede considerar como una colección de componentes en los que el directorio principal es el componente principal que ejecuta el código. Por lo tanto, este directorio también contiene el archivo CMakeLists.txt. Muy a menudo, su estructura es similar:



idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")


Donde se indica que el archivo fuente main.c debe estar registrado para el componente, y los archivos de encabezado están contenidos en el directorio actual. Si es necesario, puede cambiar el nombre del directorio principal configurando EXTRA_COMPONENT_DIRS en el proyecto CMakeLists.txt. Puede encontrar más detalles aquí .



Además, el directorio contiene el archivo main.c original (el nombre puede ser cualquiera) con un punto de entrada: la función void app_main (void).



Los componentes personalizados se crearán en el directorio de componentes. El proceso se describe con más detalle en la sección Requisitos de los componentes.



La conexión del módulo ESP32 a una computadora en la mayoría de los casos se realiza mediante un cable USB como las placas Arduino debido al cargador de arranque existente. El proceso se describe con más detalle aquí.... Lo único que se requiere es la presencia de un controlador de conversión de USB a UART en el sistema, que se puede descargar desde la fuente proporcionada. Después de instalar el controlador, debe determinar el número de puerto COM en el sistema para cargar el firmware compilado en el módulo.



Configurando el proyecto.



La configuración predeterminada está bien en la mayoría de los casos. Pero para llamar a la interfaz del menú de la consola, debe ir al directorio del proyecto y escribir en la línea de comando:



idf.py menuconfig




imagen

Menú con opciones de configuración



Después de llamar a este comando, se creará el archivo sdkconfig si no estaba previamente o si fue reconfigurado. En tutoriales anteriores, encontrará comandos make menuconfig obsoletos.



Es posible agregar configuraciones personalizadas al archivo sdkconfig manualmente, por ejemplo:



#
# WiFi Settings   
#
CONFIG_ESP_HOST_NAME=" "
CONFIG_ESP_WIFI_SSID="  "
CONFIG_ESP_WIFI_PASSWORD=""


Pero el método preferido es usar un archivo de configuración adicional Kconfig.projbuild, que debe estar ubicado en el directorio con el componente. El contenido del archivo puede ser el siguiente:



# put here your custom config value
menu "Example Configuration"
config ESP_WIFI_SSID
    string "Keenetic"
    default "myssid"
    help
    SSID (network name) for the example to connect to.

config ESP_WIFI_PASSWORD
    string "password"
    default "mypassword"
    help
    WiFi password (WPA or WPA2) for the example to use.
endmenu


Después de llamar al comando idf.py menuconfig, se agrega automáticamente una sección adicional en el archivo sdkconfig. Llamar al comando idf.py menuconfig también es posible en el proyecto PlatformIO, sin embargo, debe tener en cuenta el hecho de que la estructura del proyecto PlatformIO es diferente de la clásica ESP-IDF, lo que puede hacer que el archivo sdkconfig se vuelva a generar y ajustar la configuración personalizada. Aquí, las opciones anteriores son posibles: editar el archivo a mano, renombrar temporalmente el directorio src en main o configurar el archivo CMakeLists.txt



Compilar y cargar el proyecto.

Para construir un proyecto, debe escribir el comando



idf.py build


Este comando compilará la aplicación y todos los componentes de ESP-IDF, y luego generará el cargador, la tabla de particiones y los binarios de la aplicación.



$ idf.py build
Running cmake in directory /path/to/hello_world/build
Executing "cmake -G Ninja --warn-uninitialized /path/to/hello_world"...
Warn about uninitialized values.
-- Found Git: /usr/bin/git (found version "2.17.0")
-- Building empty aws_iot component due to configuration
-- Component names: ...
-- Component paths: ...

... (more lines of build system output)

[527/527] Generating hello-world.bin
esptool.py v2.3.1

Project build complete. To flash, run this command:
../../../components/esptool_py/esptool/esptool.py -p (PORT) -b 921600 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x10000 build/hello-world.bin  build 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin
or run 'idf.py -p PORT flash'


Debe tenerse en cuenta que el proceso de compilación inicial de incluso un proyecto simple lleva tiempo, por lo que, a diferencia del marco de Arduino, se compilan muchos módulos ESP-IDF adicionales. La modificación adicional de las fuentes solo conduce a la compilación de los mismos archivos. Una excepción es el cambio de configuración.



Para descargar los binarios compilados (bootloader.bin, partition-table.bin y hello-world.bin) en la placa ESP32, ejecute el comando:



idf.py -p PORT [-b BAUD] flash


donde reemplazamos PORT con lo que necesitamos (COM1, / dev / ttyUSB1), y opcionalmente podemos cambiar la velocidad de descarga especificando los valores requeridos para BAUD.



Para rastrear el programa cargado, puede usar cualquier utilidad de monitoreo de puerto com, como HTerm , CoolTerm , o use la utilidad de monitoreo IDF Monitor , para iniciarlo, ingrese el comando:



idf.py -p PORT monitor


Complemento ESP-IDF Eclipse



La documentación para instalar y configurar el complemento se encuentra aquí



imagen



Presets para su uso:



  • Java 11 y superior; (aunque funciona en java 8, posiblemente debido a estos fallos);
  • Python 3.5 y superior;
  • Eclipse 2020-06 CDT;
  • Git;
  • ESP-IDF 4.0 y superior;


El complemento está bastante bien integrado en el entorno de desarrollo, automatiza la mayor parte de la funcionalidad. Pero, desafortunadamente, no sin una mosca en el ungüento. En las versiones de Eclipse posteriores a 2019-09, todavía hay un error con la indexación de archivos de origen en proyectos ESP-IDF en Windows.Además



imagen



, hay otras fallas cuando el proyecto simplemente no se compila por razones desconocidas. Solo ayuda cerrar el proyecto y reiniciar Eclipse.



Extensión de código ESP-IDF Visual Studio



Y por último, en mi opinión la opción más interesante es el plugin oficial para Visual Studio Code.

Al igual que PlatformIO, se puede instalar fácilmente desde la sección de extensiones. La instalación y configuración del marco ESP-IDF en esta extensión se presenta como un menú de incorporación, que también se describe en la descripción. La descarga e instalación de todos los componentes se produce automáticamente en el proceso de pasar por las etapas del menú. Se pueden citar todas las capturas de pantalla del proceso, pero son intuitivas y requieren poca o ninguna explicación. A favor de PlatformIO, se puede destacar un conjunto de herramientas más conveniente para construir, descargar y monitorear un proyecto. Por el contrario, el complemento ESP-IDF se controla mediante un menú de comandos que se puede invocar con la tecla F1 o una combinación de teclas descritas en el manual.



imagen

Configuración inicial del complemento



La ventaja de usar el complemento es que se respeta la estructura clásica del proyecto, no hay necesidad de chamanizar de alguna manera con la configuración (en PlatformIO, surge esta necesidad). Hay un matiz, si queremos abrir un proyecto creado previamente en código de Visual Studio con el complemento ESP-IDF, entonces solo necesitamos copiar el directorio .vscode a la raíz del proyecto, que se puede obtener generando al menos una vez un proyecto de plantilla usando ESP- Complemento IDF.



imagen

Menú de comando



FreeRTOS



Según wikipedia, FreeRTOS es un sistema operativo multitarea en tiempo real (RTOS) para sistemas integrados. FreeRTOS proporciona multitarea al compartir el tiempo de CPU entre todos los subprocesos o, en la terminología del sistema operativo, tareas. En mi opinión, el manual FreeRTOS más completo e inteligible en ruso está aquí . En el idioma original, los manuales se pueden estudiar de la fuente oficial . Solo daré una imagen del estado de las tareas.



imagen



FreeRTOS se ha adaptado a una amplia variedad de plataformas de hardware, incluidos los procesadores Xtensa utilizados en ESP32. Se pueden encontrar más detalles en la documentación.



GPIO



GPIO o entrada / salida universal es la capacidad de controlar discretamente un pin con una señal "1" o "0".



Como su nombre lo indica, estos pines tienen dos modos de funcionamiento: entrada o salida. En el primer caso, leemos el valor, en el segundo, lo escribimos. Otro factor importante cuando se trata de GPIO es el nivel de voltaje. El ESP32 es un dispositivo de 3,3 V. Por lo tanto, debe tener cuidado al trabajar con otros dispositivos que tengan un voltaje de 5 V o superior. También es importante comprender que la corriente máxima que se puede aplicar al pin GPIO es de 12 mA. Para utilizar las funciones GPIO proporcionadas por ESP-IDF, necesitamos conectar el encabezado driver / gpio.h. Luego puede llamar a gpio_pad_select_gpio () para especificar la función de este pin. Hay 34 GPIO diferentes disponibles en el ESP32. Están designados como:



  • GPIO_NUM_0 - GPIO_NUM_19
  • GPIO_NUM_21 - GPIO_NUM_23
  • GPIO_NUM_25 - GPIO_NUM_27
  • GPIO_NUM_32 - GPIO_NUM_39


La siguiente numeración no está incluida en el número de pines 20, 24, 28, 29, 30 y 31. La

tabla de pines se puede encontrar aquí .



Tenga en cuenta que los pines GPIO_NUM_34 - GPIO_NUM_39 - usan solo el modo de entrada. No se pueden utilizar para la salida de señal. Además, los pines 6, 7, 8, 9, 10 y 11 se utilizan para interactuar con una tarjeta flash externa a través de SPI, no se recomienda usarlos para otros fines, pero si realmente lo desea, puede hacerlo. El tipo de datos gpio_num_t es una enumeración con valores correspondientes a números de pin. Se recomienda utilizar estos valores en lugar de números. La dirección del pin se establece mediante la función gpio_set_direction (). Por ejemplo, para establecer un pin como salida:



gpio_set_direction(GPIO_NUM_17, GPIO_MODE_OUTPUT);


Para establecer un pin como entrada:



gpio_set_direction(GPIO_NUM_17, GPIO_MODE_INPUT);


Si hemos configurado GPIO como salida, podemos establecer su valor en 1 o 0 llamando a gpio_set_level ().



El siguiente ejemplo cambia los GPIO una vez por segundo:




gpio_pad_select_gpio(GPIO_NUM_17);
gpio_set_direction(GPIO_NUM_17, GPIO_MODE_OUTPUT);
while(1) {
    printf("Off\n");
    gpio_set_level(GPIO_NUM_17, 0);
    vTaskDelay(1000 / portTICK_RATE_MS);
    printf("On\n");
    gpio_set_level(GPIO_NUM_17, 1);
    vTaskDelay(1000 / portTICK_RATE_MS);
}


Como alternativa a configurar todos los atributos de los pines individuales, podemos configurar las propiedades de uno o más contactos llamando a la función gpio_config (). Toma una estructura gpio_config_t como entrada y establece la dirección, pull up, pull down e interrupción para todos los pines representados en la máscara de bits.

Por ejemplo:




gpio_config_t gpioConfig;
gpioConfig.pin_bit_mask = (1 << 16) | (1 << 17);
gpioConfig.mode = GPIO_MODE_OUTPUT;
gpioConfig.pull_up_en = GPIO_PULLUP_DISABLE;
gpioConfig.pull_down_en = GPIO_PULLDOWN_ENABLE;
gpioConfig.intr_type = GPIO_INTR_DISABLE;
gpio_config(&gpioConfig);


Configuración de pull up y pull down



Generalmente se lee que el pin de entrada GPIO es alto o bajo. Esto significa que está conectado a una fuente de alimentación o a tierra. Sin embargo, si el pin no está conectado a nada, entonces está en un estado "flotante". A menudo es necesario establecer el nivel inicial de un pin no conectado en alto o bajo. En este caso, se realiza un pull-up por hardware (conexión mediante resistencias) o software de la salida, respectivamente, a + V - pull up o a 0 - pull down. En el SDK de ESP32, podemos definir un GPIO como un pull up o pull down usando la función gpio_set_pull_mode (). Esta función toma como entrada el número de pin que queremos configurar y el modo pull-up asociado a ese pin.

Por ejemplo:



gpio_set_pull_mode (21, GPIO_PULLUP_ONLY);


Manejo de interrupciones GPIO



Para detectar un cambio en la señal de entrada en un pin, podemos sondear periódicamente su estado, pero esta no es la mejor solución por varias razones. Primero, debemos recorrer la verificación, desperdiciando tiempo de CPU. En segundo lugar, en el momento del sondeo, es posible que el estado del pin ya no sea relevante debido al retraso y puede omitir las señales de entrada. La solución a estos problemas es la interrupción. Una interrupción es como un timbre. Sin llamar, tendremos que comprobar periódicamente si hay alguien en la puerta. En el código fuente, podemos definir una función de devolución de llamada de interrupción que será llamada cuando el pin cambie el valor de su señal. También podemos determinar qué está causando que se llame al controlador configurando los siguientes parámetros:



  • Desactivar: no causa interrupciones cuando cambia la señal;
  • PosEdge: llame al controlador de interrupciones cuando cambie de menor a mayor;
  • NegEdge: llame a un manejador de interrupciones cuando cambie de alto a bajo;
  • AnyEdge: invoca el controlador de interrupciones cuando se cambia de menor a mayor o al cambiar de mayor a menor;


imagen



Se puede marcar un manejador de interrupciones para que se cargue en la RAM durante la compilación. Por defecto, el código generado está en la memoria flash. Si lo marca como IRAM_ATTR de antemano, estará listo para su ejecución inmediata desde la RAM.



void IRAM_ATTR my_gpio_isr_handle(void *arg) {...}


imagen



Aquellos que han trabajado con microcontroladores saben que el procesamiento de las señales de entrada de los botones va acompañado de un rebote de contacto. Lo cual se puede interpretar como una serie de transiciones y, por lo tanto, una serie de eventos de manejador de interrupciones. Para hacer esto, debemos agregar el manejo de rebote de contactos al código. Para hacer esto, necesitamos leer el evento original, esperar hasta que las vibraciones disminuyan y luego volver a muestrear el estado de entrada.



imagen



El siguiente ejemplo demuestra el manejo de interrupciones de señales de entrada. Le recomiendo que se familiarice con la gestión de colas en FreeRTOS para comprender mejor el código si aún no lo está. El ejemplo muestra dos tareas:



  • test1_task, que se desbloquea cuando ocurre un evento de interrupción cuando la señal se activa en el pin 25 y el mensaje "Registrado un clic" se muestra en la consola una vez;
  • test2_task se consulta periódicamente, y cuando se activa la señal en el pin 26, el mensaje "¡GPIO 26 es alto!" se envía a la consola cada 100 ms.


El ejemplo también tiene un temporizador de software configurado xTimer, es opcional para este caso, más bien como un ejemplo de retardo asincrónico.



El anti-rebote se realiza mediante la función timeval_durationBeforeNow , que comprueba si la prensa dura más de 100ms. Hay otros patrones de software anti-rebote, pero el significado es casi el mismo. ESP-IDF también incluye un ejemplo de cómo funciona GPIO.



Procesamiento de señal de entrada

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/queue.h"
#include "c_timeutils.h"
#include "freertos/timers.h"

static char tag[] = "test_intr";
static QueueHandle_t q1;
TimerHandle_t xTimer;
#define TEST_GPIO (25)

static void handler(void *args) {
    gpio_num_t gpio;
    gpio = TEST_GPIO;
    xQueueSendToBackFromISR(q1, &gpio, NULL);
}

void test1_task(void *ignore) {
    struct timeval lastPress;
    ESP_LOGD(tag, ">> test1_task");
    gpio_num_t gpio;
    q1 = xQueueCreate(10, sizeof(gpio_num_t));
    gpio_config_t gpioConfig;
    gpioConfig.pin_bit_mask = GPIO_SEL_25;
    gpioConfig.mode = GPIO_MODE_INPUT;
    gpioConfig.pull_up_en = GPIO_PULLUP_DISABLE;
    gpioConfig.pull_down_en = GPIO_PULLDOWN_ENABLE;
    gpioConfig.intr_type = GPIO_INTR_POSEDGE;
    gpio_config(&gpioConfig);
    gpio_install_isr_service(0);
    gpio_isr_handler_add(TEST_GPIO, handler, NULL);
    while(1) {
        //ESP_LOGD(tag, "Waiting on queue");
        BaseType_t rc = xQueueReceive(q1, &gpio, portMAX_DELAY);
        //ESP_LOGD(tag, "Woke from queue wait: %d", rc);
        struct timeval now;
        gettimeofday(&now, NULL);
        if (timeval_durationBeforeNow(&lastPress) > 100) {
            if(gpio_get_level(GPIO_NUM_25)) {
                ESP_LOGD(tag, "Registered a click");
                if( xTimerStart( xTimer, 0 ) != pdPASS ) {
                    // The timer could not be set into the Active state.
                }
            }
        }
        lastPress = now;
    }
    vTaskDelete(NULL);
}

void test2_task(void *ignore) {
    gpio_set_direction(GPIO_NUM_26, GPIO_MODE_INPUT);
    gpio_set_pull_mode(GPIO_NUM_26, GPIO_PULLDOWN_ONLY);
    while(true) {
        if(gpio_get_level(GPIO_NUM_26)) {
            ESP_LOGD(tag, "GPIO 26 is high!");
            if( xTimerStart( xTimer, 0 ) != pdPASS ) {
                    // The timer could not be set into the Active state.
                }
        }
        vTaskDelay(100/portTICK_PERIOD_MS);
    }
}

void vTimerCallback( TimerHandle_t pxTimer ) {
    ESP_LOGD(tag, "The timer has expired!");
}

void app_main(void)
{
    xTaskCreate(test1_task, "test_task1", 5000, NULL, 8, NULL);
    xTaskCreate(test2_task, "test_task2", 5000, NULL, 8, NULL);

    xTimer = xTimerCreate("Timer",       // Just a text name, not used by the kernel.
                            2000/portTICK_PERIOD_MS,   // The timer period in ticks.
                            pdFALSE,        // The timers will auto-reload themselves when they expire.
                            ( void * ) 1,  // Assign each timer a unique id equal to its array index.
                            vTimerCallback // Each timer calls the same callback when it expires.
                        );
}




PCNT (contador de pulsos)



El módulo PCNT (Contador de pulsos) está diseñado para contar el número de flancos ascendentes y / o descendentes de la señal de entrada. Cada bloque del módulo tiene un registro firmado de 16 bits y dos canales que se pueden configurar para aumentar o disminuir el valor del contador. Cada canal tiene una señal de entrada que captura el cambio en la señal, así como una entrada de control que puede usarse para habilitar o deshabilitar el conteo. Las entradas tienen filtros adicionales que se pueden usar para eliminar picos de señal no deseados.



El contador PCNT tiene ocho unidades independientes, numeradas del 0 al 7. En la API, se especifican mediante pcnt_unit_t. Cada módulo tiene dos canales independientes, numerados 0 y 1, indicados por pcnt_channel_t.



La configuración se proporciona por separado para cada canal de dispositivo mediante pcnt_config_t y cubre:



  • Número de unidad y número de canal al que pertenece esta configuración;
  • Números GPIO de entrada de pulso y entrada de puerta;
  • Dos pares de parámetros, pcnt_ctrl_mode_t y pcnt_count_mode_t, para definir cómo reacciona el contador según el estado de la señal de control y cómo se cuentan los flancos ascendente / descendente.
  • Dos valores límite (mínimo / máximo) que se utilizan para establecer puntos de vigilancia y activar interrupciones cuando el contador de pulsos alcanza un cierto límite.


La configuración de un canal específico se realiza llamando a la función pcnt_unit_config () con la estructura de configuración pcnt_config_t anterior como parámetro de entrada.

Para deshabilitar una entrada de pulso o control en la configuración, debe especificar PCNT_PIN_NOT_USED en lugar del número GPIO.



Después de configurar con pcnt_unit_config (), el contador comienza a ejecutarse inmediatamente. El valor del contador acumulado se puede verificar llamando a pcnt_get_counter_value ().



Las siguientes funciones le permiten controlar cómo funciona el contador: pcnt_counter_pause (), pcnt_counter_resume () y pcnt_counter_clear ()



También es posible cambiar dinámicamente los modos de contador previamente establecidos utilizando pcnt_unit_config () llamando a pcnt_set_mode ().



Si lo desea, el pin de entrada de pulso y el pin de entrada de control se pueden cambiar sobre la marcha usando pcnt_set_pin ().



El módulo PCNT tiene filtros en cada una de las entradas de pulso y control, lo que agrega la capacidad de ignorar picos cortos en las señales. La longitud de los pulsos ignorados se proporciona en ciclos de reloj APB_CLK llamando a pcnt_set_filter_value (). La configuración actual del filtro se puede verificar con pcnt_get_filter_value (). El ciclo APB_CLK opera a 80 MHz.



El filtro se inicia / pausa llamando a pcnt_filter_enable () / pcnt_filter_disable ().

Los siguientes eventos, definidos en pcnt_evt_type_t, pueden desencadenar una interrupción. El evento ocurre cuando el contador de pulsos alcanza ciertos valores:



  • : counter_l_lim counter_h_lim, pcnt_config_t;
  • 0 1, pcnt_set_event_value ().
  • = 0


Para registrarse, habilitar o deshabilitar la interrupción de los eventos anteriores, debe llamar a pcnt_isr_register (), pcnt_intr_enable () y pcnt_intr_disable (). Para habilitar o deshabilitar eventos cuando se alcanzan los umbrales, también deberá llamar a pcnt_event_enable () y pcnt_event_disable ().



Para verificar qué umbrales están configurados actualmente, use la función pcnt_get_event_value (). Aquí



se presenta un ejemplo de ESP-IDF . Usé un contador PCNT para calcular la velocidad de la rueda. Para hacer esto, es necesario contar el número de pulsos por revolución y luego reiniciar el contador.







Código de muestra

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;


esp_err_t init_speed_sensor(speed_sensor_params_t* params) {
      /* Prepare configuration for the PCNT unit */
    pcnt_config_t pcnt_config;
    // Set PCNT input signal and control GPIOs
    pcnt_config.pulse_gpio_num = params->pin;
    pcnt_config.ctrl_gpio_num = params->ctrl_pin;
    pcnt_config.channel = params->channel;
    pcnt_config.unit = params->unit;
    // What to do on the positive / negative edge of pulse input?
    pcnt_config.pos_mode = PCNT_COUNT_INC;   // Count up on the positive edge
    pcnt_config.neg_mode = PCNT_COUNT_DIS;   // Keep the counter value on the negative edge
    pcnt_config.lctrl_mode = PCNT_MODE_REVERSE; // Reverse counting direction if low
    pcnt_config.hctrl_mode = PCNT_MODE_KEEP;    // Keep the primary counter mode if high
    pcnt_config.counter_h_lim = INT16_MAX;
    pcnt_config.counter_l_lim = - INT16_MAX;

     /* Initialize PCNT unit */
    esp_err_t err = pcnt_unit_config(&pcnt_config);

    /* Configure and enable the input filter */
    pcnt_set_filter_value(params->unit, 100);
    pcnt_filter_enable(params->unit);

    /* Initialize PCNT's counter */
    pcnt_counter_pause(params->unit);
    pcnt_counter_clear(params->unit);

    /* Everything is set up, now go to counting */
    pcnt_counter_resume(params->unit);
    return err;
}

int32_t calculateRpm(speed_sensor_params_t* params) {
    pcnt_get_counter_value(params->unit, &(params->count));
    int32_t rpm = 60*(1000/params->delay)*params->count/PULSE_PER_TURN;
    pcnt_counter_clear(params->unit);
    return rpm;
}




Modulación de ancho de pulso (PWM) usando el módulo MCPWM



La información sobre el módulo se presenta aquí

Hay muchos artículos en la red sobre el tema PWM , especialmente si busca en relación con Arduino.

Wikipedia ofrece una definición breve y concisa: modulación de ancho de pulso (PWM), el proceso de controlar la energía pulsando el dispositivo de encendido y apagado. El principio del control PWM es cambiar el ancho del pulso a una amplitud y frecuencia constantes de la señal.



imagen



La frecuencia PWM de Arduino es 488.28 Hz, la resolución es de 8 bits (0 ... 255) y es posible usar seis pines de hardware 3, 5, 6, 9, 10, 11. Sin embargo, usando la configuración de registro del microcontrolador AVR, puede lograr otros valores Frecuencia PWM.



El microcontrolador ESP32 tiene en su arsenal un módulo MCPWM separado, o más bien dos módulos, cada uno de los cuales tiene tres pares de pines PWM



imagen



Además, en la documentación, las salidas de un bloque separado están marcadas como PWMxA / PWMxB.

A continuación se presenta un diagrama de bloques más detallado del bloque MCPWM. Cada par A / B se puede sincronizar con cualquiera de los tres temporizadores: temporizador 0, 1 y 2. El mismo temporizador se puede utilizar para sincronizar más de un par de salidas PWM. Cada unidad también puede recopilar datos de entrada como señales de sincronización, detectar alarmas como sobrecorriente o sobretensión del motor y recibir retroalimentación de señales de captura como la posición del rotor.



imagen



El alcance de la configuración depende del tipo de motor, en particular de cuántas salidas y entradas se requieren y cuál será la secuencia de señales para controlar el motor.



En nuestro caso, describimos una configuración simple para impulsar un motor de CC con escobillas que usa solo algunos de los recursos MCPWM disponibles. A continuación se muestra un circuito de ejemplo. Incluye un puente en H para cambiar la polarización del voltaje suministrado al motor (M) y proporcionar suficiente corriente para impulsarlo.



imagen



La configuración incluye los siguientes pasos:



  • Selección del bloque MPWn que se utilizará para impulsar el motor. Hay dos módulos disponibles en la placa ESP32 de los enumerados en mcpwm_unit_t.
  • Inicializa dos GPIO como salidas en el módulo seleccionado llamando a mcpwm_gpio_init (). Las dos señales de salida se utilizan generalmente para impulsar el motor hacia la derecha o hacia la izquierda. Todos los parámetros de señal disponibles se enumeran en mcpwm_io_signals_t. Para establecer más de un pin a la vez, use la función mcpwm_set_pin () junto con mcpwm_pin_config_t.
  • Selección de temporizador. Hay tres temporizadores disponibles en el dispositivo. Los temporizadores se enumeran en mcpwm_timer_t.
  • Establecer la frecuencia del temporizador y el bootstrap en la estructura mcpwm_config_t.
  • Llamar a mcpwm_init () con los parámetros anteriores.


Los métodos de control de PWM son los siguientes:



  • mcpwm_set_signal_high () mcpwm_set_signal_low (). . A B .
  • — , mcpwm_start () mcpwm_stop (). .
  • , mcpwm_set_duty () . mcpwm_set_duty_in_us (), . mcpwm_get_duty (). , mcpwm_set_duty_type (). A B mcpwm_generator_t. . mcpwm_init (), , mcpwm_duty_type_t.


Un ejemplo de un código para un motor con escobillas está aquí



En mi proyecto, prácticamente usé el código del ejemplo, corrigiéndolo ligeramente y agregando un segundo control de motor. Para el control independiente de los canales PWM, cada uno de ellos debe configurarse con un temporizador independiente, por ejemplo, MCPWM_TIMER_0 y CPWM_TIMER_1:



Código de muestra

void mcpwm_example_gpio_initialize(void)
{
    mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GPIO_PWM0A_OUT);
    mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0B, GPIO_PWM0B_OUT);
    mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GPIO_PWM1A_OUT);
    mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1B, GPIO_PWM1B_OUT);
    //mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_SYNC_0, GPIO_SYNC0_IN);

    mcpwm_config_t pwm_config;
    pwm_config.frequency = 1000;    //frequency = 500Hz,
    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
    mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_1, &pwm_config);    //Configure PWM0A & PWM0B with above settings
          // deadtime (see clock source changes in mcpwm.c file)
    mcpwm_deadtime_enable(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_BYPASS_FED, 80, 80);   // 1us deadtime
    mcpwm_deadtime_enable(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_BYPASS_FED, 80, 80);  
}




Conectarse a Wi-Fi y trabajar con MQTT



El tema del protocolo Wi-FI es bastante extenso. Se necesitará una serie de artículos separados para describir el protocolo. En la guía oficial, consulte la sección de controladores de Wi-Fi . Aquí encontrará una descripción de la API del software . Los ejemplos de código se pueden ver aquí.



Las bibliotecas Wi-Fi brindan soporte para configurar y monitorear las funciones de red Wi-Fi ESP32. Están disponibles las siguientes configuraciones:



  • ( STA Wi-Fi). ESP32 .
  • AP ( Soft-AP ). ESP32.
  • AP-STA (ESP32 , ).
  • (WPA, WPA2, WEP . .)
  • ( ).
  • Wi-Fi IEEE802.11.


MQTT



Puede familiarizarse con el tema aquí o aquí . El manual de ESP-IDF con ejemplos está aquí .



Para configurar MQTT en código, primero debe conectarse a una red Wi-Fi. Luego, establezca una conexión con el corredor. El mensaje se procesa en una devolución de llamada, cuyo parámetro es el evento esp_mqtt_event_handle_t. Si el tipo de evento es MQTT_EVENT_DATA, se pueden analizar el tema y los datos. Puede personalizar diferentes comportamientos como resultado de una conexión, desconexión y suscripciones de temas exitosas.



Ejemplo de conexión Wi-Fi:

tcpip_adapter_init();
    wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &ip_event_handler, NULL));
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
    ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
    wifi_config_t sta_config = {
        .sta = {
            .ssid = CONFIG_ESP_WIFI_SSID,
            .password = CONFIG_ESP_WIFI_PASSWORD,
            .bssid_set = false
        }
    };
    ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &sta_config) );
    ESP_LOGI(TAG, "start the WIFI SSID:[%s] password:[%s]", CONFIG_ESP_WIFI_SSID, "******");
    ESP_ERROR_CHECK( esp_wifi_start() );
    ESP_LOGI(TAG, "Waiting for wifi");
    xEventGroupWaitBits(wifi_event_group, BIT0, false, true, portMAX_DELAY);

    //MQTT init
    mqtt_event_group = xEventGroupCreate();
    mqtt_app_start(mqtt_event_group);




Conexión con el corredor MQTT

void mqtt_app_start(EventGroupHandle_t event_group)
{
    mqtt_event_group = event_group;
    const esp_mqtt_client_config_t mqtt_cfg = {
        .uri = "mqtt://mqtt.eclipse.org:1883",    //mqtt://mqtt.eclipse.org:1883
        .event_handle =  mqtt_event_handler,
        .keepalive = 10,
        .lwt_topic = "esp32/status/activ",
        .lwt_msg = "0",
        .lwt_retain = 1,
    };

    ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
    client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_start(client);






Controlador MQTT

esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event)
{
    esp_mqtt_client_handle_t client = event->client;
    int msg_id;
    command_t command;
    // your_context_t *context = event.context;
    switch (event->event_id) {
        case MQTT_EVENT_CONNECTED:
             xEventGroupSetBits(mqtt_event_group, BIT1);
            ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
            msg_id = esp_mqtt_client_subscribe(client, "esp32/car/#", 0);
            msg_id = esp_mqtt_client_subscribe(client, "esp32/camera/#", 0);
            ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
            break;

        case MQTT_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
            break;

        case MQTT_EVENT_SUBSCRIBED:
            ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
            msg_id = esp_mqtt_client_publish(client, "esp32/status/activ", "1", 0, 0, 1);
            ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
            break;

        case MQTT_EVENT_UNSUBSCRIBED:
            ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
            break;

        case MQTT_EVENT_PUBLISHED:
            ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
            break;

        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;

        case MQTT_EVENT_ERROR:
            ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
            break;

        default:
            break;
    }
    return ESP_OK;
}




Con esto concluye mi historia sobre el uso del módulo ESP32. El artículo consideró ejemplos sobre ESP-IDF, como un marco que aprovecha al máximo los recursos del módulo. La programación utilizando otras plataformas como javaScript, MicroPython, Lua se puede encontrar en los recursos relacionados. En el próximo artículo, como ya se mencionó, daré un ejemplo práctico del uso de un microcontrolador y también compararé el enfoque de software de Arduino y ESP-IDF.



All Articles