ESP32_IDF learning 1 [basic content]

The school teacher left an assignment to use the remaining half of the winter vacation to learn ESP32 and make an http server working in Bluetooth transparent + sta & AP mode, but Arduino is not allowed

I was stupid on the spot: ESP32 I just got ready to do Arduino; Le Xin's ESPIDF is too hard to chew. I lit a light and went to be happy stm32; Micro Python... After brushing the firmware, I found that Bluetooth support is the same as [data deletion]. It's better to write in c - bite your teeth and stamp your feet, and go back to ESPIDF

General idea: there are few resources. It's right to follow the official. It's over if you bite hard

This series of notes can be read and learned by students who have only been exposed to MCU development (STM32, 51 basis) and hardware related knowledge, but have not been exposed to network related knowledge

Project folder build

The ESP-IDF project consists of various "components". You have to throw in any component you need

If you use a bunch of WiFi library functions in your code, but you don't add WiFi components, you can't use WiFi functions

The project is saved in the project folder, and its root directory is as follows:

├── CMakeLists.txt				Cmake Files used
├── other_documents				Other documents
├── main						Store main program
│   ├── CMakeLists.txt			
│   ├── component.mk           Component make file
│   └── main.c
└── Makefile                   By traditional GNU make Program used Makefile

It should be noted that ESP-IDF is not a part of the project folder. It is more like a self-service compiler. The project folder is connected to the ESP-IDF directory through tools such as idf.py esptools and ${IDF_PATH}; Similarly, the development tool chain of ESP also exists independently of the project and acts on the project through ${PATH}

Before the project is created, esp IDF will configure Makefile through idf.py menuconfig, and these configurations are saved in sdkconfig. Sdkconfig is saved in the root directory of the project folder

CMakeLists.txt via idf_component_register registers the components under the project folder, as shown below

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

SRCS gives the list of source files, and the suffixes of source files that can be supported are. C. CPP. CC. S

INCLUDE_DIRS gives the search path for files in the component

Requirements is not required. It declares other components that need to be added

Through this txt document, esp IDF can know what components you throw into it, and then compile and link these components when compiling (which can be understood as the static link of the operating system)

After the compilation is completed, there will be more build folders and sdkconfig files in the file root directory. The build folder is used to store the files and generated files during the compilation process. The sdkconfig file is generated in the process of menuconfig. If menuconfig has been reset many times, there will be more config files ending in. old

In addition, components can also be self-made. For details, please refer to the official tutorial; The bottom layer of idf.py is implemented with Cmake and make tools, so you can also compile directly with these tools (but no one should do so)

CMake and component components

[extract from official document] an ESP-IDF project can be regarded as a collection of multiple different components. Components are modular and independent code, which will be compiled into static libraries and linked to applications. ESP-IDF comes with some components. You can also find the existing components of open source projects

The components of ESP-IDF are actually the encapsulation of CMake. It is also feasible to use the pure CMake style construction method (in the final analysis, it is the cross compilation process, but Lexin has optimized ESP32), as shown below

cmake_minimum_required(VERSION 3.5)
project(my_custom_app C)

# The source file main.c contains app_ Definition of main() function
add_executable(${CMAKE_PROJECT_NAME}.elf main.c)

# Provide idf_import_components and idf_link_components function
include($ENV{IDF_PATH}/tools/cmake/idf_functions.cmake)

# Is idf_import_components do some configuration
# Enable component creation (not required for every project)
set(IDF_BUILD_ARTIFACTS ON)
set(IDF_PROJECT_EXECUTABLE ${CMAKE_PROJECT_NAME}.elf)
set(IDF_BUILD_ARTIFACTS_DIR ${CMAKE_BINARY_DIR})

# idf_import_components encapsulates add_subdirectory(), create a library target for the component, and then receive the "returned" library target with the given variable.
# In this case, the returned library target is saved in the "component" variable.
idf_import_components(components $ENV{IDF_PATH} esp-idf)

# idf_ link_ The components encapsulate the target_link_libraries() will be used by IDF_ import_ The components processed by components are linked to the target
idf_link_components(${CMAKE_PROJECT_NAME}.elf "${components}")

The directory tree structure of the sample project may be as follows:

- myProject/ #home directory
             - CMakeLists.txt #Global cmake document for configuring project cmake
             - sdkconfig #The project configuration file can be generated by menuconfig
             - components/ - component1/ - CMakeLists.txt #The CMake document of the component, which is used to configure the CMake of the component
                                         - Kconfig #Used to define the component configuration options displayed when menuconfig is defined
                                         - src1.c
                           - component2/ - CMakeLists.txt
                                         - Kconfig
                                         - src1.c
                                         - include/ - component2.h
             - main/ #You can think of the main directory as a special pseudo component - src1.c
                           - src2.c
             - build/ #Used to store output files

The main directory is a special "pseudo component" that contains the source code of the project itself. Main is the default name, CMake variable COMPONENT_DIRS includes this component by default, but you can modify this variable. Alternatively, you can set extra in the top-level CMakeLists.txt_ COMPONENT_DIRS variable to find components at other specified locations. If there are many source files in the project, it is recommended to attribute them to components rather than all in main.

Write global CMake

The global CMake document should contain at least the following three parts:

cmake_minimum_required(VERSION 3.5) #It must be placed on the first line to set the minimum version number of CMake required to build the project
include($ENV{IDF_PATH}/tools/cmake/project.cmake) #It is used to import other functions of CMake to complete tasks such as configuration items and retrieval components
project(myProject) #Specify the project name and create the project, and change the name to Cheng Hui as the name of the final output bin file or elf file

Only one project can be defined per CMakeLists file

You can also include the following optional sections

COMPONENT_DIRS #The component search directory defaults to ${IDF_PATH}/components, ${PROJECT_PATH}/components, and EXTRA_COMPONENT_DIRS
COMPONENTS #A list of component names to be built into the project. The default is component_ All components retrieved under dirs directory
EXTRA_COMPONENT_DIRS #Other optional directory lists for searching components, which can be absolute paths or relative paths
COMPONENT_REQUIRES_COMMON #List of common COMPONENTS required by each component. These common COMPONENTS will be automatically added to the component of each component_ PRIV_ The requirements list and the COMPONENTS list of the project

Use the set command to set the above variables, as shown below

set(COMPONENTS "COMPONENTx")

Note: the set command needs to be placed before include, cmake_ minimum_ After required

In particular, you can rename the main component in two cases

  1. If the maincomponent is in the normal position ${PROJECT_PATH}/main, it will be automatically added to the build system, and other components will automatically become the dependencies of main to facilitate the processing of dependencies

  2. The main component is renamed to XXX, and extra needs to be set in the global CMake setting_ COMPONENT_ Dirs = ${project_path} / xxx, and set component in the component CMake directory_ Requirements or component_ PRIV_ Requirements to specify dependencies

Component CMake writing

Each project contains one or more components, which can be part of ESP-IDF, part of the project's own component directory, or added from the custom component directory

Component is component_ Any directory in the dirs list that contains the CMakeLists.txt file

ESP-IDF will search for component_ Use the directory list in dirs to find the components of the project. The directory in this list can be the component itself (that is, the directory containing CMakeLists.txt file), or the sub directory can be the top-level directory of the component; Search order: components in [ESP-IDF internal components] - [project components] - [EXTRA_COMPONENT_DIRS]. If two or more of these directories contain components with the same name, the components found in the last location will be used. Components can be copied to the project directory and modified to overwrite ESP-IDF components

The smallest component CMakeLists is as follows

set(COMPONENT_SRCS "foo.c" "k.c") #Space delimited list of source files
set(COMPONENT_ADD_INCLUDEDIRS "include") #The directory list separated by spaces will be added to the global include search path of all components (including main components) that need this component
register_component() #The build generates a library with the same name as the component and is eventually linked to the application

There are the following preset variables, which are not recommended to be modified

COMPONENT_PATH #The component directory is the absolute path containing the CMakeLists.txt file. Note that the path cannot contain spaces
COMPONENT_NAME #Component name, equivalent to component directory name
COMPONENT_TARGET #The library item name is automatically created by CMake internally

There are the following project level variables, which are not recommended to be modified, but can be used in the component CMake document

PROJECT_NAME #Project name, set in the global CMake document
PROJECT_PATH #The absolute path of the project directory (including the project CMakeLists file), which is the same as cmake_ SOURCE_ Same as dir
COMPONENTS #The names of all components included in this build
CONFIG_* #Each value in the project configuration corresponds to one in cmake with CONFIG_ Variable at the beginning
IDF_VER #Git version number of ESP-IDF, generated by git describe command
IDF_TARGET #The hardware target name of the project, generally ESP32
PROJECT_VER #Project version number

COMPONENT_ADD_INCLUDEDIRS #The relative path relative to the component directory is added to the global include search path of all other components that need the component
COMPONENT_REQUIRES #The component list separated by spaces lists other components that the current component depends on

[excerpt from official website] if a component only needs the header files of additional components to compile its source files (instead of importing their header files globally), these dependent components need to be in component_ PRIV_ Requirements indicates that

The following optional component specific variables are available to control the behavior of a component

COMPONENT_SRCS #The path of the source file to compile into the current component. This method is recommended to add the source file to the build system

COMPONENT_PRIV_INCLUDEDIRS #The relative path relative to the component directory is only added to the include search path of the component
COMPONENT_PRIV_REQUIRES #A space delimited list of components used to compile or link the source files of the current component
COMPONENT_SRCDIRS #The directory path of the source file relative to the component directory is used to search for the source file. The source file that matches successfully will replace component_ Source file specified in SRCs
COMPONENT_SRCEXCLUDE #The source file path that needs to be removed from the component
COMPONENT_ADD_LDFRAGMENTS #The path of the link fragment file used by the component to automatically generate the linker script file

Component configuration file Kconfig

Each component can contain a Kconfig file in the same directory as CMakeLists.txt

The Kconfig file contains some configuration settings to be added to the component configuration menu. When running menuconfig, you can find these settings under the Component Settings menu bar

Just get started with your hands

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "esp_log.h"
//Fix the header file that needs to be include d
//Used for freertos support and output debugging information

These are omitted from the following header file

The entry of a general program is app_main() function

void app_main(void)

Light up

#include "driver/gpio.h"

#define BLINK_GPIO CONFIG_BLINK_GPIO
/*Kconfig.projbuild The contents of the document are as follows

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
 The contents of this file are replaced before the c precompiler, which will replace CONFIG_BLINK_GPIO becomes the value of default (5)
*/

void app_main(void)
{
    gpio_pad_select_gpio(BLINK_GPIO);//Selected pin
    gpio_set_direction(BLINK_GPIO,GPIO_MODE_OUTPUT);//Set input / output direction
    while(1)
    {
		printf("Turning off the LED\n");//Serial port printing information
        gpio_set_level(BLINK_GPIO, 0);//GPIO register reset
        vTaskDelay(1000 / portTICK_PERIOD_MS);//Vtask delay() is used to delay the task. As will be mentioned below, this is actually turning the task into a blocked state

		printf("Turning on the LED\n");
        gpio_set_level(BLINK_GPIO, 1);//GPIO register set
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

UART

The configuration steps officially given are:

  1. Set UART_ config_ Tconfiguration structure
  2. Via ESP_ ERROR_ CHECK(uart_param_config(uart_num, &uart_config)); Apply settings
  3. Set pin
  4. Install drivers, set buffer and event handling functions, etc
  5. Configure FSM and run UART
#include "string.h"
#include "esp_system.h"
#include "driver/uart.h"
#include "driver/gpio.h"
//include uart library and gpio library to realize the corresponding functions

void UART_init(void)//uart initialization function
{
    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,
    };//Configure uart settings
    ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_config));//Apply settings
    //Set uart pin
    ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    //When using buffer, use the device driver provided by 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)//Send data function
{
    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)//Receive data function
{
	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();//initialization
    
    //Configure the tasks of sending and receiving serial port information respectively
    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 console

ESP provides a console for serial port debugging, which can realize shell like operation

After adding console related components to the firmware, burn them, and type help in the serial port to view the related help

void register_system(void)//System related instructions
{
    register_free();
    register_heap();
    register_version();
    register_restart();
    register_deep_sleep();
    register_light_sleep();
#if WITH_TASKS_INFO
    register_tasks();
#endif
}

Two directories in the component: cmd_nvs is used for instruction identification; cmd_system is used to implement system instructions (these functions need to be coordinated with RTOS)

NVS FLASH

NVS is Non-volatile storage

It is equivalent to storing the key data of ESP32 in flash in key value format, and NVS through spi_flash_{read|write|erase} three API s operate. NVS uses part of the main flash. The management method is similar to that of database tables. Many different tables can be stored in NVS. There are different key values under each table. Each key value can store 8-bit, 16 bit, 32-bit and other different data types, but it cannot be a floating point number

  1. Use interface function nvs_flash_init(); Initialize. If it fails, NVS can be used_ flash_ erase(); Erase before initialization
  2. Applications can use nvs_open(); Select partition in NVS table or through nvs_open_from_part() uses a different partition after specifying its name

Note: when the NVS partition is truncated, its contents should be erased

Read / write operation

nvs_get_i8(my_handle,//Handle to table
           "nvs_i8",//Key value
           &nvs_i8);//Pointer to the corresponding variable
//Use this API to read 8-bit data. Similarly, i16, u32 and other APIs are available

nvs_set_i8(my_handle,//Handle to table
           "nvs_i8",//Key value
           nvs_i8);//Corresponding variable
//Use this API to write 8-bit data. Similarly, i16, u32 and other APIs are available

Table operation

nvs_open("List",//Table name
         NVS_READWRITE,//Read / write mode, optional read / write mode or read-only mode
         &my_handle);//Handle to table
//Open table

nvs_commit(my_handle);//Submission Form
nvs_close(my_handle);//Close table

NVS initialization sample program

In the official sample program, NVS is generally initialized in the following form

//Initialize NVS
esp_err_t ret = nvs_flash_init();//initialization
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)//If initialization is not successful
{
	ESP_ERROR_CHECK(nvs_flash_erase());//Erase NVS and check for errors
	ret = nvs_flash_init();//Initialize again
}
ESP_ERROR_CHECK(ret);//Error checking

Common library functions provided by ESPIDF

ESP_LOG print system log to serial port

#include "esp_err.h"
//Used to print error messages
  • ESP_ Log - error log (highest priority)
  • ESP_LOGW - warning log
  • ESP_LOGI - information level log
  • ESP_LOGD - log for debugging
  • ESP_LOGV - log for prompt only {lowest priority)

These logs can be turned on or off in the menuconfig setting, or they can be turned off manually in code

RTOS operation

  1. Vtask delay sets the task to the blocking state, during which the CPU continues to run other tasks

The duration is specified by the parameter xTicksToDelay, and the unit is the system beat clock cycle

void vTaskDelay(portTickTypexTicksToDelay)

The constant portTickTypexTicksToDelay is used to assist in calculating the real time. This value is the cycle of the system beat clock interrupt, and the unit is ms

In the file FreeRTOSConfig.h, the macro include_ Vtask delay must be set to 1 for this function to be valid

  1. xTaskCreate creates a new task and adds it to the task queue

Note: all tasks should be loop free and never return, i.e. nested within while(1)

xTaskCreate(pdTASK_CODE pvTaskCode,//Entry function to the task
            const portCHAR * const pcName,//Task name
            unsigned portSHORT usStackDepth,//Task stack size
            void *pvParameters,//Task parameter pointer
            unsigned portBASE_TYPE uxPriority,//Task priority
            xTaskHandle *pvCreatedTask)//Task handle, used to reference the created task

Note that task priority 0 is the lowest. The higher the number, the higher the priority

  1. The magic of FreeRTOS

Introduction to one sentence: RTOS is speed. FreeRTOS can realize the time slice rotation scheduling between tasks. You can execute two tasks for a while and I can execute them for a while. High priority tasks can also seize low priority tasks and let them run immediately. High priority tasks run first

  • The underlying implementation of FreeRTOS hasn't been understood yet. Learn it after a while. Anyway, the effect is similar to RTThread. Finish the operation first =)

This magical operation depends on the above two API s

It should be noted that all tasks should be closed loop and never return (emphasized twice)

However, if you really don't want to write an endless loop, you can add it at the end of the task

vTaskDelete();//Used to delete tasks that end execution

However, most tasks that are executed only once are completed in the initialization phase. Try to be careful when using them

  1. event

Event is a mechanism to realize inter task communication, which is mainly used to realize the synchronization between multiple tasks, but event communication can only be event type communication without data transmission. Events can realize one to many and many to many transmission: a task can wait for the occurrence of multiple events: it can wake up the task for event processing when any event occurs; You can also wake up the task for event processing after several events have occurred

#include "esp_event.h"
//include this file to use event

Events are managed using event loops, which are the default event loop and the custom event loop

The default event loop does not need to pass in the event loop handle; But custom loops require

esp_event_loop_create(const esp_event_loop_args_t *event_loop_args,//Event loop parameters
                      esp_event_loop_handle_t *event_loop)//Event loop handle
//Used to create an event loop   
esp_event_loop_delete(esp_event_loop_handle_t event_loop)//Delete event loop

Events need to be registered to the event loop

/* Register event to event loop */
esp_event_handler_instance_register(esp_event_base_t event_base,//Event base ID
                                    int32_t event_id,//Event ID
                                    esp_event_handler_t event_handler,//Event callback function pointer (handle)
                                    void *event_handler_arg,//Event callback function parameters
                                    esp_event_handler_instance_t *instance)
//If the event callback function has not been registered before the event is deleted, it needs to be registered here to call
    
esp_event_handler_instance_register_with(esp_event_loop_handle_t event_loop,//Event loop handle
                                         esp_event_base_t event_base,//Event base ID
                                         int32_t event_id,//Event ID
                                         esp_event_handler_t event_handler,//Event callback function pointer (handle)
                                         void *event_handler_arg,//Event callback function parameters
                                         esp_event_handler_instance_t *instance)
//If the event callback function has not been registered before the event is deleted, it needs to be registered here to call
    
esp_event_handler_register(esp_event_base_t event_base,//Event base ID
                           int32_t event_id,//Event ID
                           esp_event_handler_t event_handler,//Event Handler 
                           void *event_handler_arg)//Event parameters
    
esp_event_handler_register_with(esp_event_loop_handle_t event_loop,//Event loop handle
                                esp_event_base_t event_base,//Event base ID
                                int32_t event_id,//Event ID
                                esp_event_handler_t event_handler,//Event callback function pointer (handle)
                                void *event_handler_arg)//Parameters of the event callback function
    
/* Unregister */
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 is the basic event loop of the system, which is used to transfer system events (such as WiFi), but user events can also be registered. This loop is sufficient for general Bluetooth + WiFi

esp_event_loop_create_default(void)//Create default event loop
esp_event_loop_delete_default(void)//Delete default event loop
esp_event_loop_run(esp_event_loop_handle_t event_loop,//Event loop handle
                   TickType_t ticks_to_run)//Running time

You can send between default events and custom events

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)

Using macros

ESP_EVENT_DECLARE_BASE()
ESP_EVENT_DEFINE_BASE()

To declare and define events, and the event ID should be indicated by enum enumeration variables, as shown below

/* Header file */
// 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
};
/* Header file */

/* source file */
ESP_EVENT_DEFINE_BASE(TIMER_EVENTS);
/* source file */

/* The event name defined by enumeration should be placed in the header file, and the macro function should be placed in the source file */

API ESP can be used_ event_ loop_ create_ Default() to create an event

esp_event_loop_create_default()

Added by xcandiottix on Thu, 11 Nov 2021 09:33:35 +0200