ESP32_IDF學習1【基本內容】


學校老師留了個作業,讓用剩下一半的寒假學學ESP32,做藍牙透傳+STA&AP模式下工作的http服務器,但是不准用Arduino

當場就傻了:ESP32我剛剛好就會一手Arduino;樂鑫那套ESPIDF太難啃,之前點了個燈就去快樂stm32了;micropython......刷完固件發現藍牙支持跟【數據刪除】一樣,還不如用c寫——一咬牙一跺腳,回頭肝ESPIDF吧

總體思路:資源少,跟着官方走准沒錯,硬啃就完事了

這個系列筆記可以供只接觸過單片機開發(STM32、51基礎)和硬件相關知識但沒有接觸過網絡相關知識的同學翻閱學習

項目文件夾構建

ESP-IDF項目由各種“組件”構成,你需要什么功能就要往里扔進去什么組件

如果你的代碼里用了一堆WiFi的庫函數,但是沒把WiFi組件加入進去,你是沒辦法用WiFi功能的

項目保存在項目文件夾下,它的根目錄如下所示:

├── CMakeLists.txt				Cmake使用的文件
├── other_documents				其他文件
├── main						存儲主程序
│   ├── CMakeLists.txt			
│   ├── component.mk           組件的make file
│   └── main.c
└── Makefile                   由傳統的GNU make程序使用的Makefile

需要注意的是:ESP-IDF並不是項目文件夾的一部分,它更像是一個自助編譯器,項目文件夾通過idf.py esptools等工具和${IDF_PATH}與ESP-IDF目錄建立聯系;同樣,esp的開發工具鏈也獨立於項目存在,通過${PATH}對項目進行作用

項目建立前,esp-idf會通過idf.py menuconfig配置出Makefile,這些配置保存在sdkconfig中。sdkconfig會被保存在項目文件夾的根目錄

CMakeLists.txt通過idf_component_register將項目文件夾下面的組件進行注冊,如下所示

idf_component_register(SRCS "foo.c" "bar.c"
                       INCLUDE_DIRS "include"
                       REQUIRES mbedtls)

SRCS給出了源文件清單,能支持的源文件后綴名為.c .cpp .cc .S

INCLUDE_DIRS給出了組件中文件的搜索路徑

REQUIRES不是必須的,它聲明了其他需要加入的組件

通過這個txt文檔,esp-idf就能知道你往里扔了什么組件,然后在編譯的時候就會把這些組件編譯鏈接進去(可以理解成操作系統的靜態鏈接)

當編譯完成后,文件根目錄下會多出build文件夾和sdkconfig文件,build文件夾用來存放編譯過程中的文件和生成的文件,sdkconfig文件是在menuconfig的過程中產生的,如果曾經多次重新設置過menuconfig,還會發現多出了以.old結尾的config文件

另外組件也可以自制,詳細內容參考官方教程;而idf.py 的底層是用Cmake、make工具實現的,所以也可以直接用這些工具進行編譯(不過應該沒人這么干)

CMake與component組件

【摘自官方文檔】一個ESP-IDF項目可以看作是多個不同組件(component)的集合,組件是模塊化且獨立的代碼,會被編譯成靜態庫並鏈接到應用程序。ESP-IDF自帶一些組件,也可以去找開源項目已有的組件來用

ESP-IDF的組件其實是對CMake的封裝,如果使用純CMake風格的構建方式也可行(說到底還是交叉編譯的那套流程,只是樂鑫針對ESP32進行了優化),如下所示

cmake_minimum_required(VERSION 3.5)
project(my_custom_app C)

# 源文件 main.c 包含有 app_main() 函數的定義
add_executable(${CMAKE_PROJECT_NAME}.elf main.c)

# 提供 idf_import_components 及 idf_link_components 函數
include($ENV{IDF_PATH}/tools/cmake/idf_functions.cmake)

# 為 idf_import_components 做一些配置
# 使能創建構件(不是每個項目都必須)
set(IDF_BUILD_ARTIFACTS ON)
set(IDF_PROJECT_EXECUTABLE ${CMAKE_PROJECT_NAME}.elf)
set(IDF_BUILD_ARTIFACTS_DIR ${CMAKE_BINARY_DIR})

# idf_import_components 封裝了 add_subdirectory(),為組件創建庫目標,然后使用給定的變量接收“返回”的庫目標。
# 在本例中,返回的庫目標被保存在“component”變量中。
idf_import_components(components $ENV{IDF_PATH} esp-idf)

# idf_link_components 封裝了 target_link_libraries(),將被 idf_import_components 處理過的組件鏈接到目標
idf_link_components(${CMAKE_PROJECT_NAME}.elf "${components}")

示例項目的目錄樹結構可能如下所示:

- myProject/ #主目錄
             - CMakeLists.txt #全局CMAke文檔,用於配置項目CMake
             - sdkconfig #項目配置文件,可用menuconfig生成
             - components/ - component1/ - CMakeLists.txt #組件的CMake文檔,用於配置組件CMake
                                         - Kconfig #用於定義menuconfig時展示的組件配置選項
                                         - src1.c
                           - component2/ - CMakeLists.txt
                                         - Kconfig
                                         - src1.c
                                         - include/ - component2.h
             - main/ #可以將main目錄看作特殊的偽組件       - src1.c
                           - src2.c
             - build/ #用於存放輸出文件

main 目錄是一個特殊的“偽組件”,包含項目本身的源代碼。main 是默認名稱,CMake 變量 COMPONENT_DIRS 默認包含此組件,但您可以修改此變量。或者,您也可以在頂層 CMakeLists.txt 中設置 EXTRA_COMPONENT_DIRS 變量以查找其他指定位置處的組件。如果項目中源文件較多,建議將其歸於組件中,而不是全部放在 main 中。

全局CMake編寫

全局CMake文檔應該至少包含如下三個部分:

cmake_minimum_required(VERSION 3.5) #必須放在第一行,設置構建該項目所需CMake的最小版本號
include($ENV{IDF_PATH}/tools/cmake/project.cmake) #用於導入CMake的其余功能來完成配置項目、檢索組件等任務
project(myProject) #指定項目名稱並創建項目,改名成蕙作為最終輸出的bin文件或elf文件的名字

每個 CMakeLists 文件只能定義一個項目

還可以包含以下可選部分

COMPONENT_DIRS #組件搜索目錄,默認為${IDF_PATH}/components、${PROJECT_PATH}/components和EXTRA_COMPONENT_DIRS
COMPONENTS #要構建進項目中的組件名稱列表,默認為COMPONENT_DIRS目錄下檢索到的所有組件
EXTRA_COMPONENT_DIRS #用於搜索組件的其它可選目錄列表,可以是絕對路徑也可以是相對路徑
COMPONENT_REQUIRES_COMMON #每個組件都需要的通用組件列表,這些通用組件會自動添加到每個組件的COMPONENT_PRIV_REQUIRES列表和項目的COMPONENTS列表中

使用set命令來設置以上變量,如下所示

set(COMPONENTS "COMPONENTx")

注意:set命令需要放在include之前,cmake_minimum_required之后

特別地,可以重命名main組件,分為兩種情況

  1. main組件處於正常位置${PROJECT_PATH}/main,則會被自動添加到構建系統中,其他組件自動成為main的依賴項,方便處理依賴關系

  2. main組件被重命名為xxx,需要在全局CMake設定中設置EXTRA_COMPONENT_DIRS=${PROJECT_PATH}/xxx,並在組件CMake目錄中設置COMPONENT_REQUIRES或COMPONENT_PRIV_REQUIRES以指定依賴項

組件CMake編寫

每個項目都包含一個或多個組件,這些組件可以是 ESP-IDF 的一部分,可以是項目自身組件目錄的一部分,也可以從自定義組件目錄添加

組件是COMPONENT_DIRS列表中包含CMakeLists.txt文件的任何目錄

ESP-IDF會搜索COMPONENT_DIRS中的目錄列表來查找項目的組件此列表中的目錄可以是組件自身(即包含CMakeLists.txt文件的目錄),也可以是子目錄為組件的頂級目錄;搜索順序:【ESP-IDF內部組件】-【項目組件】-【EXTRA_COMPONENT_DIRS】中的組件,如果這些目錄中的兩個或者多個包含具有相同名字的組件,則使用搜索到的最后一個位置的組件,允許將組件復制到項目目錄中再修改以覆蓋ESP-IDF組件

最小的組件CMakeLists如下

set(COMPONENT_SRCS "foo.c" "k.c") #用空格分隔的源文件列表
set(COMPONENT_ADD_INCLUDEDIRS "include") #用空格分隔的目錄列表,里面的路徑會被添加到所有需要該組件的組件(包括 main 組件)全局 include 搜索路徑中
register_component() #構建生成與組件同名的庫,並最終被鏈接到應用程序中

有以下預設變量,不建議修改

COMPONENT_PATH #組件目錄,是包含CMakeLists.txt文件的絕對路徑,注意路徑中不能包含空格
COMPONENT_NAME #組件名,等同於組件目錄名
COMPONENT_TARGET #庫目標名,由CMake在內部自動創建

有以下項目級別的變量,不建議修改,但可以在組件CMake文檔中使用

PROJECT_NAME #項目名,在全局CMake文檔中設置
PROJECT_PATH #項目目錄(包含項目 CMakeLists 文件)的絕對路徑,與CMAKE_SOURCE_DIR相同
COMPONENTS #此次構建中包含的所有組件的名稱
CONFIG_* #項目配置中的每個值在cmake中都對應一個以CONFIG_開頭的變量
IDF_VER #ESP-IDF的git版本號,由git describe命令生成
IDF_TARGET #項目的硬件目標名稱,一般是ESP32
PROJECT_VER #項目版本號

COMPONENT_ADD_INCLUDEDIRS #相對於組件目錄的相對路徑,會被添加到所有需要該組件的其他組件的全局include搜索路徑中
COMPONENT_REQUIRES #用空格分隔的組件列表,列出了當前組件依賴的其他組件

【摘自官網】如果一個組件僅需要額外組件的頭文件來編譯其源文件(而不是全局引入它們的頭文件),則這些被依賴的組件需要在 COMPONENT_PRIV_REQUIRES 中指出

有以下可選的組件特定變量,用於控制某組件的行為

COMPONENT_SRCS #要編譯進當前組件的源文件的路徑,推薦使用此方法向構建系統中添加源文件

COMPONENT_PRIV_INCLUDEDIRS #相對於組件目錄的相對路徑,僅會被添加到該組件的include搜索路徑中
COMPONENT_PRIV_REQUIRES #以空格分隔的組件列表,用於編譯或鏈接當前組件的源文件
COMPONENT_SRCDIRS #相對於組件目錄的源文件目錄路徑,用於搜索源文件,匹配成功的源文件會替代COMPONENT_SRCS中指定的源文件
COMPONENT_SRCEXCLUDE #需要從組件中 剔除 的源文件路徑
COMPONENT_ADD_LDFRAGMENTS #組件使用的鏈接片段文件的路徑,用於自動生成鏈接器腳本文件

組件配置文件Kconfig

每個組件都可以包含一個Kconfig文件,和CMakeLists.txt放在同一目錄下

Kconfig文件中包含要添加到該組件配置菜單中的一些配置設置信息,運行menuconfig時,可以在Component Settings菜單欄下找到這些設置

有手就行的入門

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "esp_log.h"
//固定需要include的頭文件
//用於freertos支持和輸出調試信息

以下內容頭文件包含部分會省略這些

一般程序的入口是app_main()函數

void app_main(void)

點燈

#include "driver/gpio.h"

#define BLINK_GPIO CONFIG_BLINK_GPIO
/*Kconfig.projbuild文件內容如下

menu "Example Configuration"

    config BLINK_GPIO
        int "Blink GPIO number"
        range 0 34
        default 5
        help
            GPIO number (IOxx) to blink on and off.
            Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.
            GPIOs 35-39 are input-only so cannot be used as outputs.

endmenu
這個文件的內容是在c預編譯器之前進行的替換,會把CONFIG_BLINK_GPIO變成default的值(5)
*/

void app_main(void)
{
    gpio_pad_select_gpio(BLINK_GPIO);//選擇的引腳
    gpio_set_direction(BLINK_GPIO,GPIO_MODE_OUTPUT);//設置輸入輸出方向
    while(1)
    {
		printf("Turning off the LED\n");//串口打印信息
        gpio_set_level(BLINK_GPIO, 0);//GPIO寄存器清零
        vTaskDelay(1000 / portTICK_PERIOD_MS);//vTaskDelay()用於任務中的延時,下面會提到這其實是將任務轉入阻塞態

		printf("Turning on the LED\n");
        gpio_set_level(BLINK_GPIO, 1);//GPIO寄存器置位
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

UART

官方給出的配置步驟為:

  1. 設置uart_config_t配置結構體
  2. 通過ESP_ERROR_CHECK(uart_param_config(uart_num, &uart_config));應用設置
  3. 設置引腳
  4. 安裝驅動,設置buffer和事件處理函數等
  5. 配置FSM並運行UART
#include "string.h"
#include "esp_system.h"
#include "driver/uart.h"
#include "driver/gpio.h"
//include uart庫和gpio庫來實現相應功能

void UART_init(void)//uart初始化函數
{
    const uart_config_t uart_config =
    {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_APB,
    };//配置uart設置
    ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_config));//應用設置
    //設置uart引腳
    ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    //使用buffer的情況,使用freertos提供的設備驅動
    ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2,0,0, NULL, 0));
}

int UART_send_data(const char* TAG, const char* data)//發送數據函數
{
    const int length = uart_write_bytes(UART_NUM_1, data, strlen(data));
    ESP_LOGI(TAG, "Wrote %d bytes", length);
    return length;
}

int UART_read_data(const char* TAG, const char* buffer)//收取數據函數
{
	int length = 0;
	ESP_ERROR_CHECK(uart_get_buffered_data_len(UART_NUM_1, (size_t*)&length));
	length = uart_read_bytes(UART_NUM_1,buffer,length,100);
    ESP_LOGI(TAG, "Read %d bytes", length);
    return length;
}

void app_main(void)
{
    UART_init();//初始化
    
    //分別配置發送和接收串口信息的任務
    xTaskCreate(rx_task, "uart_rx_task", 1024*2, NULL, configMAX_PRIORITIES, NULL);
    xTaskCreate(tx_task, "uart_tx_task", 1024*2, NULL, configMAX_PRIORITIES-1, NULL);
}

console控制台

ESP提供了一個console用於串口調試,可以實現類似shell的操作

在固件中加入console相關組件后燒錄,在串口中打出help就可以查看相關幫助

void register_system(void)//系統相關指令
{
    register_free();
    register_heap();
    register_version();
    register_restart();
    register_deep_sleep();
    register_light_sleep();
#if WITH_TASKS_INFO
    register_tasks();
#endif
}

組件中兩個目錄 :cmd_nvs用於指令的識別;cmd_system用於系統指令的實現(這部分功能需要與RTOS配合才行)

NVS FLASH

NVS即Non-volatile storage非易失性存儲

它相當於把ESP32的關鍵數據以鍵值格式存儲在FLASH里,NVS通過spi_flash_{read|write|erase}三個API進行操作,NVS使用主flash的一部分。管理方式類似數據庫的表,在NVS里面可以存儲很多個不同的表,每個表下面有不同的鍵值,每個鍵值可以存儲8位、16位、32位等等不同的數據類型,但不能是浮點數

  1. 使用接口函數nvs_flash_init();進行初始化,如果失敗可以使用nvs_flash_erase();先擦除再初始化
  2. 應用程序可以使用nvs_open();選用NVS表中的分區或通過nvs_open_from_part()指定其名稱后使用其他分區

注意:NVS分區被截斷時,其內容應該被擦除

讀寫操作

nvs_get_i8(my_handle,//表的句柄
           "nvs_i8",//鍵值
           &nvs_i8);//對應變量的指針
//使用這個API來讀取8位數據,同理還有i16、u32等版本的API可用

nvs_set_i8(my_handle,//表的句柄
           "nvs_i8",//鍵值
           nvs_i8);//對應的變量
//使用這個API來寫入8位數據,同理還有i16、u32等版本的API可用

表操作

nvs_open("List",//表名
         NVS_READWRITE,//讀寫模式,可選讀寫模式或只讀模式
         &my_handle);//表的句柄
//打開表

nvs_commit(my_handle);//提交表
nvs_close(my_handle);//關閉表

NVS初始化示例程序

官方給出的示例程序中一般以以下形式初始化NVS

//Initialize NVS
esp_err_t ret = nvs_flash_init();//初始化
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)//如果初始化未成功
{
	ESP_ERROR_CHECK(nvs_flash_erase());//擦除NVS並查錯
	ret = nvs_flash_init();//再次初始化
}
ESP_ERROR_CHECK(ret);//查錯

ESPIDF提供的常用庫函數

ESP_LOG打印系統日志到串口

#include "esp_err.h"
//用於打印錯誤信息
  • ESP_LOGE - 錯誤日志 (最高優先級)
  • ESP_LOGW - 警告日志
  • ESP_LOGI - 信息級別的日志
  • ESP_LOGD - 用於調試的日志
  • ESP_LOGV - 僅僅用於提示的日志{最低優先級)

這些日志可以在menuconfig設置中打開或關閉,也可以在代碼中手動設置關閉

RTOS操作

  1. vTaskDelay將任務置為阻塞狀態,期間CPU繼續運行其它任務

持續時間由參數xTicksToDelay指定,單位是系統節拍時鍾周期

void vTaskDelay(portTickTypexTicksToDelay)

常量portTickTypexTicksToDelay用來輔助計算真實時間,此值是系統節拍時鍾中斷的周期,單位是ms

在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskDelay 必須設置成1,此函數才能有效

  1. xTaskCreate創建新的任務並添加到任務隊列

注意:所有任務應當為死循環且永遠不會返回,即嵌套在while(1)內

xTaskCreate(pdTASK_CODE pvTaskCode,//指向任務的入口函數
            const portCHAR * const pcName,//任務名
            unsigned portSHORT usStackDepth,//任務堆棧大小
            void *pvParameters,//任務參數指針
            unsigned portBASE_TYPE uxPriority,//任務優先級
            xTaskHandle *pvCreatedTask)//任務句柄,用於引用創建的任務

注意,任務優先級0為最低,數字越大優先級越高

  1. FreeRTOS的神奇之處

一句話概論:RTOS就是彳亍,FreeRTOS可以實現任務之間的時間片輪轉調度,兩個任務可以你執行一會我執行一會,高優先級任務還能搶占低優先級任務,讓它馬上爪巴,高優先級任務先運行

  • FreeRTOS的底層實現還沒看明白,過一陣子再學,反正效果和RTThread差不多,先把作業肝完再說 =)

這種神奇的操作靠的就是上面兩個API

需要注意的是:所有任務應當為死循環且永遠不會返回(兩次強調)

不過如果實在不想寫死循環,可以在任務末尾加上

vTaskDelete();//用於刪除執行結束的任務

不過只執行一次的任務大多是在初始化階段完成的,用的時候盡量小心些

  1. 事件event

事件是一種實現任務間通信的機制,主要用於實現多任務間的同步,但事件通信只能是事件類型的通信,無數據傳輸。事件可以實現一對多和多對多的傳輸:一個任務可以等待多個事件的發生:可以是任意一個事件發生時喚醒任務進行事件處理;也可以是幾個事件都發生后才喚醒任務進行事件處理

#include "esp_event.h"
//include 這個文件才能使用event

事件使用事件循環來管理,事件循環分別為默認事件循環和自定義事件循環

默認事件循環不需要傳入事件循環句柄;但自定義循環需要

esp_event_loop_create(const esp_event_loop_args_t *event_loop_args,//事件循環參數
                      esp_event_loop_handle_t *event_loop)//事件循環句柄
//用於創建一個事件循環   
esp_event_loop_delete(esp_event_loop_handle_t event_loop)//刪除事件循環

事件需要注冊到事件循環

/* 注冊事件到事件循環 */
esp_event_handler_instance_register(esp_event_base_t event_base,//事件基本ID
                                    int32_t event_id,//事件ID
                                    esp_event_handler_t event_handler,//事件回調函數指針(句柄)
                                    void *event_handler_arg,//事件回調函數參數
                                    esp_event_handler_instance_t *instance)
//如果事件回調函數在事件刪除之前還沒有被注冊,需要在這里注冊來進行調用
    
esp_event_handler_instance_register_with(esp_event_loop_handle_t event_loop,//事件循環句柄
                                         esp_event_base_t event_base,//事件基本ID
                                         int32_t event_id,//事件ID
                                         esp_event_handler_t event_handler,//事件回調函數指針(句柄)
                                         void *event_handler_arg,//事件回調函數參數
                                         esp_event_handler_instance_t *instance)
//如果事件回調函數在事件刪除之前還沒有被注冊,需要在這里注冊來進行調用
    
esp_event_handler_register(esp_event_base_t event_base,//事件基本ID
                           int32_t event_id,//事件ID
                           esp_event_handler_t event_handler,//事件句柄
                           void *event_handler_arg)//事件參數
    
esp_event_handler_register_with(esp_event_loop_handle_t event_loop,//事件循環句柄
                                esp_event_base_t event_base,//事件基本ID
                                int32_t event_id,//事件ID
                                esp_event_handler_t event_handler,//事件回調函數指針(句柄)
                                void *event_handler_arg)//事件回調函數的參數
    
/* 取消注冊 */
esp_event_handler_unregister(esp_event_base_t event_base,
                             int32_t event_id,
                             esp_event_handler_t event_handler)
esp_event_handler_unregister_with(esp_event_loop_handle_t event_loop,
                                  esp_event_base_t event_base,
                                  int32_t event_id,
                                  esp_event_handler_t event_handler)

默認事件循環default event loop是系統的基礎事件循環,用於傳遞系統事件(如WiFi等),但是也可以注冊用戶事件,一般的藍牙+WiFi用這一個循環就足夠了

esp_event_loop_create_default(void)//創建默認事件循環
esp_event_loop_delete_default(void)//刪除默認事件循環
esp_event_loop_run(esp_event_loop_handle_t event_loop,//事件循環句柄
                   TickType_t ticks_to_run)//運行時間

默認事件和自定義事件之間可以進行發送操作

esp_event_post(esp_event_base_t event_base, int32_t event_id, void *event_data, size_t event_data_size, TickType_t ticks_to_wait)

使用宏

ESP_EVENT_DECLARE_BASE()
ESP_EVENT_DEFINE_BASE()

來聲明和定義事件,同時事件的ID應該用enum枚舉變量來指出,如下所示

/* 頭文件 */
// Declarations for event source 1: periodic timer
#define TIMER_EXPIRIES_COUNT// number of times the periodic timer expires before being stopped
#define TIMER_PERIOD                1000000  // period of the timer event source in microseconds

extern esp_timer_handle_t g_timer;           // the periodic timer object

// Declare an event base
ESP_EVENT_DECLARE_BASE(TIMER_EVENTS);        // declaration of the timer events family

enum {// declaration of the specific events under the timer event family
    TIMER_EVENT_STARTED,                     // raised when the timer is first started
    TIMER_EVENT_EXPIRY,                      // raised when a period of the timer has elapsed
    TIMER_EVENT_STOPPED                      // raised when the timer has been stopped
};

// Declarations for event source 2: task
#define TASK_ITERATIONS_COUNT        5       // number of times the task iterates
#define TASK_ITERATIONS_UNREGISTER   3       // count at which the task event handler is unregistered
#define TASK_PERIOD                  500     // period of the task loop in milliseconds

ESP_EVENT_DECLARE_BASE(TASK_EVENTS);         // declaration of the task events family

enum {
    TASK_ITERATION_EVENT,                    // raised during an iteration of the loop within the task
};
/* 頭文件 */

/* 源文件 */
ESP_EVENT_DEFINE_BASE(TIMER_EVENTS);
/* 源文件 */

/* 枚舉定義的事件名應該放在頭文件,宏函數應該放在源文件 */

可使用API esp_event_loop_create_default()來創建事件

esp_event_loop_create_default()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM