基于ESP32模块的编程设备

微控制器是能够执行程序的集成电路。今天,市场上有许多制造商提供了许多这样的模型。这些设备的价格继续下降。单芯片芯片广泛应用于各个领域:从测量仪器到娱乐和各种家用电器。与个人计算机不同,微控制器在一个晶体中结合了处理器和外围设备的功能,包含RAM和用于存储代码和数据的只读存储器,但其计算资源却大大减少。 ESP32是由Espressif Systems开发的微控制器。 ESP32是集成了Wi-Fi和蓝牙控制器的片上系统。 ESP32系列使用核心Tensilica Xtensa LX6搭载ESP32的主板具有良好的计算能力,发达的外围设备因其价格在7至14美元之间的低价而非常受欢迎:Aliexpress亚马逊



本文无意作为详尽的指南,而只是材料和建议来源的集合。在本文中,我想谈谈在选择用于项目开发的软件工具时遇到的问题,以及一些ESP32模块的实际应用案例。在下一篇文章中,我想展示一个使用ESP32作为小型两轮移动平台的控制控制器的说明性示例。因此,这里我们将考虑以下详细信息:



  • 选择开发环境;
  • 搭建工作环境,编译加载ESP-IDF项目;
  • 处理输入/输出信号GPIO;
  • 使用MCPWM模块进行脉冲宽度调制;
  • PCNT硬件计数器;
  • WI-Fi和MQTT连接。


ESP32-WROOM-32E模组概述



根据数据表,该模块包含:



MCU



  • 嵌入式ESP32-D0WD-V3,Xtensa双核32位LX6微处理器,最高240 MHz
  • 448 KB ROM,用于引导和核心功能
  • 520 KB SRAM,用于数据和指令
  • RTC中的16 KB SRAM


无线上网



  • 802.11b / g / n
  • 比特率:802.11n最高150 Mbps
  • A-MPDU和A-MSDU聚合
  • 0.4 µs保护间隔支持
  • 工作通道中心频率范围:2412〜2484 MHz


蓝牙



  • 蓝牙V4.2 BR / EDR和蓝牙LE规范
  • 1级,2级和3级发射机
  • 美国空军
  • CVSD和SBC


硬件



  • 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


图片

功能框图



有关微控制器功能的更多详细信息,请参见Wikipedia



该模块基于ESP32-D0WD-V3 *电路。嵌入式芯片在设计时考虑了可伸缩性和适应性。中央处理单元包含两个可以单独控制的内核,CPU时钟速度可在80 MHz至240 MHz之间调节。该芯片还具有一个低功耗协处理器,可以在执行不需要大量计算能力的任务(例如监视引脚状态)时代替CPU来节省功耗。 ESP32集成了丰富的外围设备,包括电容式触摸传感器,霍尔传感器,SD卡接口,以太网,高速SPI,UART,I²S和I²C。



技术文档在官方资源中提供



关于ESP-WROOM-32模块的引脚分配的信息可以在网络的开放空间很容易被发现,因为这里



选择开发环境



Arduino IDE



AVR系列的微控制器以及Arduino平台早在ESP32之前就已出现。 Arduino的主要功能之一是相对较低的进入门槛,几乎任何人都可以快速轻松地创建东西。该平台为开源硬件社区做出了重要贡献,并允许大量无线电爱好者加入。可以从场外免费下载Arduino IDE 。尽管与专业开发环境相比存在明显的限制,但Arduino IDE可以满足爱好项目所需的90%。该网络上有足够的关于Arduino IDE的安装和配置的文章,用于对ESP32模块进行编程,例如:用于ESP32的Arduino内核habr.comvoltiq.rurandomnerdtutorials.com



在Arduino环境中对ESP32进行编程时,您需要考虑arduino-esp32页面上指示的引脚排列



图片

ESP32模块引脚排列



此开发方法的主要优点是使用与Arduino相同的原理可以快速入门并易于创建项目。并使用许多库,例如Arduino。另一个不错的功能是能够将Arduino库和设计原理与原始ESP-IDF框架相结合。



PlatformIO



如官方资源中所述:“跨平台PlatformIO IDE和统一调试器·静态代码分析器和远程单元测试。多平台和多体系结构构建系统·固件文件资源管理器和内存检查»换句话说,PlatformIO是一个用于嵌入式设备开发的生态系统,支持包括Arduino和ESP32在内的多个平台。 IDE是Visual Studio Code或Atom。安装和配置非常简单-安装代码编辑器后,从插件列表中选择PlatformIO并安装。同样,也有对这个话题有很多的材料在网络上,从官方消息开始在这里这里,并用详细的插图文章继续在这里这里。...



与Arduino IDE相比,PlatformIO具有现代开发环境的所有特质:项目组织,插件支持,代码完成等等。



PlatformIO开发的一个特点是所有平台的统一项目结构



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


每个PlatformIO项目在项目的根目录中都包含一个名为platformio.ini的配置文件platformio.ini具有节(每个节均由[title]表示)和节内的键/值对。以点分号“;”开头的行 被忽略,可以用于注释。可以通过两种方式指定多个值参数:



  1. 用“,”(逗号+空格)分隔值;
  2. 多行格式,其中每个新行至少以两个空格开头。


ESP32的下一个开发功能是选择框架的能力:Arduino或ESP-IDF。通过选择Arduino作为框架,我们可以获得前面描述的开发优势。



图片



PlatformIO包括用于构建,下载和调试项目的便捷工具



图片



乐鑫物联网开发框架



乐鑫为ESP32开发了一个称为IoT Development Framework的框架,称为“ ESP-IDF”。可以在Github上找到该项目包含非常好的文档,并提供了一些示例,您可以以此为基础。在“入门”部分中详细介绍了如何设置和设置环境有几种安装和使用框架的选项。



从存储库克隆项目并手动安装实用程序。



从Github克隆项目



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


对于Windows,可以使用安装程序或命令行脚本来安装开发实用程序



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


对于PowerShell



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


对于Linux和macOS



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


下一步是设置环境变量如果开发工具是使用安装程序在Windows上安装的,则命令控制台的快捷方式将添加到菜单和桌面,然后您可以打开命令外壳并使用项目。或者,运行Windows命令外壳程序:



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


或Windows PowerShell:



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


Linux和macOS:



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


您应注意句点与脚本路径之间的间隔,



此外,在指南中,建议您在脚本中添加别名,以便在Linux或macOS上工作时在用户配置文件中设置环境变量。为此,请将以下命令复制并粘贴到您的外壳配置文件(.profile,.bashrc,.zprofile等)中:



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


通过在控制台中调用get_idf命令,将导出所需的环境变量。就我而言,还必须注册一个别名以启动python虚拟环境



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


并将其添加到下一个别名



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


从头开始创建新项目,您可以从github.com克隆源代码,也可以使用示例esp-idf / examples / get-started / hello_world /从目录中复制源代码。



有关项目结构,编译,加载,配置实用程序等的信息位于此处



该项目是具有以下结构的目录:



- 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/


项目配置包含在根目录的sdkconfig文件中。要更改设置,您需要调用idf.py menuconfig命令(或在Windows上可能调用idf.py.exe menuconfig)。



通常在一个项目中创建两个应用程序-“项目应用程序”(主要的可执行文件,即您的自定义固件)和“ bootloader应用程序”(项目的Bootloader程序)。

“组件”是模块化的独立代码,它们被编译成静态库(.a文件)并链接到应用程序。其中一些由ESP-IDF本身提供,其他一些则可以从其他来源获得。



idf.py命令行实用程序提供了一个界面,可轻松管理项目构建。它在Windows上的位置是%userprofile%\。Espressif \ tools \ idf-exe \ 1.0.1 \ idf.py.exe。她控制以下乐器:



  • CMake-配置要构建的项目
  • 控制台项目生成器:Ninja或GNU Make)
  • esptool.py-用于刷新模块。


每个项目都有一个顶级CMakeLists.txt文件,其中包含整个项目的构建设置。最小文件配置包括以下必需行:



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


ESP-IDF项目可以看作是组件的集合,其中主目录是运行代码的主要组件。因此,此目录还包含CMakeLists.txt文件。最常见的是,其结构类似:



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


指出必须为该组件注册源文件main.c,并且头文件包含在当前目录中。如有必要,可以通过在CMakeLists.txt项目中设置EXTRA_COMPONENT_DIRS来重命名主目录。可以在此处找到更多详细信息



此外,该目录还包含带有入口点的原始main.c(名称可以为任意)文件-void app_main(void)函数。



自定义组件将在components目录中创建。该过程在“组件要求”部分中进行了详细描述



由于现有的引导加载程序,大多数情况下使用Arduino板之类的USB电缆将ESP32模块连接至计算机。该过程在这里更详细地描述... 唯一需要的是系统中存在USB到UART转换器驱动程序,可以从给定的源下载该驱动程序。安装驱动程序后,您需要确定系统中的COM端口号,以将编译后的固件加载到模块中。



配置项目。



在大多数情况下,默认设置都可以。但是要调用控制台菜单界面,您需要转到项目目录并在命令行中键入:



idf.py menuconfig




图片

带有配置设置的菜单



调用此命令后,如果之前未配置sdkconfig文件或重新配置了sdkconfig文件,则会创建该文件。在以前的教程中,您将看到过时的make menuconfig命令。



可以手动将自定义设置添加到sdkconfig文件,例如:



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


但是首选方法是使用其他配置文件Kconfig.projbuild,该文件必须位于组件的目录中。该文件的内容可以如下:



# 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


调用idf.py menuconfig命令后,将在sdkconfig文件中自动添加一个附加部分。在PlatformIO项目中也可以调用idf.py menuconfig命令,但是,您需要考虑到PlatformIO项目的结构与经典ESP-IDF不同的事实,因此可以重新生成sdkconfig文件并调整自定义设置。在这里,可以使用上述选项-手动编辑文件,临时重命名main中的src目录或设置CMakeLists.txt文件



编译并加载项目。

要构建一个项目,您需要输入命令



idf.py build


该命令将编译应用程序和所有ESP-IDF组件,然后生成加载程序,分区表和应用程序二进制文件。



$ 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'


应该记住,即使是简单项目的初始编译过程也要花费时间,因此,与Arduino框架不同,它编译了许多其他的ESP-IDF模块。进一步修改源代码只会导致编译相同的文件。例外是配置更改。



要将已编译的二进制文件(bootloader.bin,partition-table.bin和hello-world.bin)下载到ESP32板,请运行以下命令:



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


用我们需要的端口(COM1,/ dev / ttyUSB1)替换PORT的地方,我们可以有选择地通过为BAUD指定必要的值来更改下载速度。



要跟踪加载的程序,可以使用任何com端口监视实用程序,例如HTermCoolTerm,或使用IDF Monitor监视实用程序来启动它,输入命令:



idf.py -p PORT monitor


ESP-IDF Eclipse插件



用于安装和配置插件的文档位于此处的



图片



预设使用:



  • Java 11及以上;(尽管它可以在java 8上运行,可能是由于此故障);
  • Python 3.5及更高版本;
  • 蚀2020-06 CDT;
  • Git;
  • ESP-IDF 4.0及更高版本;


该插件已很好地集成到开发环境中,可以自动实现大部分功能。但是,不幸的是,美中不足。在2019-09以后的Eclipse版本中,Windows上的ESP-IDF项目仍然存在一个索引源文件错误



图片



仅关闭项目并重新启动Eclipse会有帮助。



ESP-IDF Visual Studio代码扩展



最后,我认为最有趣的选择是Visual Studio Code的官方插件

与PlatformIO一样,可以从扩展部分轻松安装它。此扩展中的ESP-IDF框架的安装和配置以入门菜单的形式提供,说明中也对此进行了描述。在完成菜单阶段的过程中,将自动下载和安装所有组件。可以引用该过程的所有屏幕截图,但是它们是直观的,几乎不需要解释。为了支持PlatformIO,可以注意到用于构建,下载和监视项目的更方便的工具包。相反,ESP-IDF插件是通过命令菜单控制的,可以使用F1键或手册中描述的键组合来调用该命令菜单。



图片

初始插件设置



使用该插件的优点是尊重经典项目结构,无需以某种方式进行萨满化(在PlatformIO中,这是必要的)。有一个细微差别,如果我们想使用ESP-IDF插件在Visual Studio代码中打开一个先前创建的项目,那么我们只需要将.vscode目录复制到项目的根目录即可,这可以通过使用ESP-至少生成一次模板项目来获得。 IDF插件。



图片

命令菜单



FreeRTOS



根据维基百科,FreeRTOS是用于嵌入式系统的实时多任务操作系统(RTOS)。FreeRTOS通过所有线程或使用OS术语的任务共享CPU时间来提供多任务处理。我认为,这里是俄语最完整,最易懂的FreeRTOS手册可以使用原始语言从官方来源学习手册我只会给出任务状态的图片。



图片



FreeRTOS已移植到各种硬件平台,包括ESP32中使用的Xtensa处理器。可以在文档中找到更多详细信息



通用输入输出



GPIO或通用输入/输出是用“ 1”或“ 0”信号离散控制引脚的能力。



顾名思义,此类引脚具有两种工作模式-输入或输出。在第一种情况下,我们读取该值,在第二种情况下,我们将其记录下来。处理GPIO时的另一个重要因素是电压电平。 ESP32是一个3.3V的设备,因此,当您使用其他电压为5V或更高的设备时,请务必小心。同样重要的是要了解,可施加到GPIO引脚的最大电流为12 mA。要使用ESP-IDF提供的GPIO功能,我们需要连接驱动程序/gpio.h标头。然后,您可以调用gpio_pad_select_gpio()来指定此引脚的功能。 ESP32上有34个可用的GPIO。它们被指定为:



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


以下编号不包括在

引脚20、24、28、29、30和31的数量中。可在此处找到引脚分配表



请注意,引脚GPIO_NUM_34-GPIO_NUM_39-仅使用输入模式。它们不能用于信号输出。另外,引脚6、7、8、9、10和11用于通过SPI与外部闪存卡进行交互,不建议将它们用于其他目的,但是如果您愿意,可以这样做。gpio_num_t数据类型是一个枚举,其值与引脚号相对应。建议使用这些值而不是数字。使用gpio_set_direction()函数设置引脚方向。例如,要将引脚设置为输出:



gpio_set_direction(GPIO_NUM_17, GPIO_MODE_OUTPUT);


要将引脚设置为输入:



gpio_set_direction(GPIO_NUM_17, GPIO_MODE_INPUT);


如果已将GPIO配置为输出,则可以通过调用gpio_set_level()将其值设置为1或0。



以下示例每秒切换一次GPIO:




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);
}


作为设置单个引脚的所有属性的替代方法,我们可以通过调用gpio_config()函数来设置一个或多个触点的属性。它以gpio_config_t结构作为输入,并为位掩码中表示的所有引脚设置方向,上拉,下拉和中断设置。

例如:




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);


上拉和下拉设置



通常读取GPIO输入引脚为高电平或低电平。这意味着它已连接到电源或接地。但是,如果该引脚未连接任何东西,则处于“浮动”状态。通常需要将未连接的引脚的初始电平设置为高电平或低电平。在这种情况下,将输出的硬件(使用电阻器连接)或软件上拉分别为+ V-上拉或0-下拉。在ESP32 SDK中,我们可以使用gpio_set_pull_mode()函数将GPIO定义为上拉或下拉。此功能将要设置的引脚号以及与该引脚关联的上拉模式作为输入。

例如:



gpio_set_pull_mode (21, GPIO_PULLUP_ONLY);


GPIO中断处理



为了检测引脚上输入信号的变化,我们可以定期轮询其状态,但是由于多种原因,这不是最佳解决方案。首先,我们必须循环进行检查,浪费CPU时间。其次,在轮询时,由于延迟,引脚的状态可能不再重要,您可以跳过输入信号。这些问题的解决方案是中断。打扰就像是门铃。如果不响铃,我们将不得不定期检查以查看是否有人在门口。在源代码中,我们可以定义一个中断回调函数,当引脚更改其信号值时将调用该函数。我们还可以通过设置以下参数来确定导致处理程序被调用的原因:



  • 禁用-当信号改变时不触发中断;
  • PosEdge-从低变高时调用中断处理程序;
  • NegEdge-从高变低时调用中断处理程序;
  • AnyEdge-从低变高或从高变低时调用中断处理程序;


图片



可以将中断处理程序标记为在编译时加载到RAM中。默认情况下,生成的代码在闪存中。如果事先将其标记为IRAM_ATTR,它将准备从RAM立即执行。



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


图片



那些使用微控制器的人都知道,按钮输入信号的处理伴随着触点的反弹。这可以解释为一系列转换,因此可以解释为一系列中断处理程序事件。为此,我们必须在代码中添加联系人退回处理。为此,我们需要读取原始事件,等到振动消退后,再对输入状态进行采样。



图片



以下示例演示了输入信号的中断处理。我强烈建议您熟悉FreeRTOS中的队列管理,以进一步了解代码(如果您还不熟悉的话)。该示例显示了两个任务:



  • test1_task,当在引脚25上激活信号并在控制台上显示一次消息“注册点击”时,在发生中断事件时将被解锁;
  • 定期轮询test2_task,并激活引脚26上的信号时,每100毫秒将消息“ GPIO 26为高!”输出到控制台。


该示例还设置了一个软件计时器xTimer,在这种情况下它是可选的,而不是作为异步延迟的示例。



使用timeval_durationBeforeNow函数执行防跳动,该函数检查按下是否持续100ms以上。还有其他防反弹软件模式,但是含义大致相同。ESP-IDF还包括一个GPIO工作方式的示例



输入信号处理

#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(脉冲计数器)



PCNT(脉冲计数器) 模块设计用于计算输入信号的上升沿和/或下降沿的数量。模块的每个块都有一个16位带符号寄存器和两个通道,可以将其配置为增加或减少计数器值。每个通道都有一个捕获信号变化的输入信号,以及一个可用于启用或禁用计数的控制输入。输入具有附加滤波器,可用于消除不需要的信号尖峰。



PCNT计数器具有八个独立的单元,编号从0到7。在API中,使用pcnt_unit_t指定它们。每个模块都有两个独立的通道,编号为0和1,由pcnt_channel_t表示。



使用pcnt_config_t为每个设备通道分别提供配置,包括:



  • 此配置所属的单元号和通道号;
  • 脉冲输入和门输入GPIO编号;
  • 两对参数pcnt_ctrl_mode_t和pcnt_count_mode_t用于定义计数器如何根据控制信号的状态做出反应以及如何对上升沿/下降沿进行计数。
  • 两个极限值(最小/最大),用于设置观察点并在脉冲计数器达到特定极限时触发中断。


然后,通过使用上述pcnt_config_t配置结构作为输入参数调用pcnt_unit_config()函数来完成特定通道的配置。

要在配置中禁用脉冲或控制输入,您需要指定PCNT_PIN_NOT_USED而不是GPIO号。



使用pcnt_unit_config()配置后,计数器立即开始运行。可以通过调用pcnt_get_counter_value()来检查计数器的累积值。



下列功能允许您控制计数器操作:pcnt_counter_pause(),pcnt_counter_resume()和pcnt_counter_clear()



也可以通过调用pcnt_set_mode()使用pcnt_unit_config()动态更改先前设置的计数器模式。



如果需要,可以使用pcnt_set_pin()快速更改脉冲输入引脚和控制输入引脚。



PCNT模块在每个脉冲和控制输入上都有滤波器,从而增加了忽略信号中短尖峰的能力。通过调用pcnt_set_filter_value(),可以在APB_CLK时钟周期中提供被忽略脉冲的长度。可以使用pcnt_get_filter_value()检查当前的过滤器设置。 APB_CLK周期工作在80 MHz。



通过调用pcnt_filter_enable()/ pcnt_filter_disable()启动/暂停过滤器。

pcnt_evt_type_t中定义的以下事件可以触发中断。该事件在脉冲计数器达到某些值时发生:



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


要注册,启用或禁用上述事件的中断,必须调用pcnt_isr_register(),pcnt_intr_enable()和pcnt_intr_disable()。要在达到阈值时启用或禁用事件,您还需要调用pcnt_event_enable()和pcnt_event_disable()。



要检查当前设置的阈值,请使用pcnt_get_event_value()函数。这里



提供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;


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;
}




使用MCPWM模块的脉宽调制(PWM)



这里 提供有关模块的信息。关于PWM

主题网上有很多文章,特别是如果您搜索有关Arduino的文章。 维基百科给出了一个简短的定义-脉宽调制(PWM)-通过打开和关闭设备脉冲来控制功率的过程。 PWM控制的原理是在信号的恒定振幅和频率下改变脉冲宽度。 Arduino的PWM频率为488.28 Hz。分辨率为8位(0 ... 255),可以使用六个硬件引脚3、5、6、9、10、11。但是,使用AVR微控制器的寄存器设置,您可以实现其他值PWM频率。





图片







ESP32微控制器在其武器库中有一个单独的MCPWM模块,或者说是两个模块,每个模块都有三对PWM引脚,



图片



此外,在文档中,单独模块的输出标记为PWMxA / PWMxB。

下面给出了MCPWM模块的更详细的框图。每个A / B对可以与以下三个定时器中的任何一个同步:定时器0、1和2。同一定时器可用于同步多于一对的PWM输出。每个单元还可以收集输入数据(例如同步信号),检测警报(例如过电流或电动机过电压)以及接收带有捕获信号(例如转子位置)的反馈。



图片



配置的范围取决于电动机的类型,特别是需要多少个输出和输入以及用于控制电动机的信号序列是什么。



在我们的案例中,我们将描述一种用于驱动有刷直流电动机的简单配置,该配置仅使用一些可用的MCPWM资源。电路示例如下所示。它包括一个H桥,用于切换提供给电动机(M)的电压的极化并提供足够的电流来驱动它。



图片



配置包括以下步骤:



  • 选择用于驱动电动机的MPWn块。ESP32板上的mcpwm_unit_t中列出了两个模块。
  • 通过调用mcpwm_gpio_init()初始化两个GPIO作为所选模块上的输出。这两个输出信号通常用于向右或向左驱动电动机。所有可用的信号参数在mcpwm_io_signals_t中列出。要一次设置多个引脚,请同时使用mcpwm_set_pin()函数和mcpwm_pin_config_t。
  • 计时器选择。设备上有三个计时器。计时器在mcpwm_timer_t中列出。
  • 在mcpwm_config_t结构中设置计时器频率和引导程序。
  • 使用上述参数调用mcpwm_init()。


PWM控制方法如下:



  • 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.


这里 是一个有刷电机



的代码示例,在我的项目中,我实际上使用了该示例中的代码,对其进行了稍微的校正并添加了第二个电机控件。为了独立控制PWM通道,每个通道必须配置一个单独的计时器,例如MCPWM_TIMER_0和CPWM_TIMER_1:



样例代码

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);  
}




连接到WI-Fi并使用MQTT



Wi-FI协议的主题非常广泛。需要一系列单独的文章来描述该协议。在官方指南中,请参阅“ Wi-Fi驱动程序”部分在此处对软件API进行了描述可以在此处查看代码示例



Wi-Fi库为配置和监视ESP32 Wi-Fi网络功能提供支持。提供以下配置:



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


MQTT



您可以在这里这里熟悉该主题此处提供带示例的ESP-IDF手册



要以代码设置MQTT,首先需要连接到Wi-Fi网络。然后建立与代理的连接。该消息在回调中处理,该回调的参数为esp_mqtt_event_handle_t事件。如果事件类型为MQTT_EVENT_DATA,则可以分析主题和数据。成功连接,断开连接和主题订阅后,您可以自定义不同的行为。



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);




连接到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);






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;
}




到此结束我有关使用ESP32模块的故事。本文考虑了ESP-IDF上的示例,这是一个充分利用该模块资源的框架。在相关资源上可以找到使用其他平台(例如javaScript,MicroPython,Lua)进行编程的信息。如前所述,在下一篇文章中,我将给出一个使用微控制器的实际示例,并比较Arduino和ESP-IDF的软件方法。



All Articles