ESP32 MCU learning notes - 01 - gpio&ledc&uart

CSDN big bug, why can't my "no need" format be used? It's too uncomfortable. All typesetting "disorderly", "orderly" and "to do" cannot be used. The layout below may be a little ugly. If you feel inconvenient to watch, you can find a backup on github.

ESP32 MCU learning notes - 01 - gpio&ledc&uart

0. Prepare relevant materials

1. Tutorial notes: ESP32 development guide directory (produced in a small step of open source) , this is a tutorial released by "jiayouchuang technology" on CSDN. I also referred to it when preparing the compilation environment in the early stage, although it didn't play any role. However, the content of writing esp32 routines in the later chapters is very perfect, so it is still recommended. Thank the author for his summary.
2. Official guidelines: ESP-IDF Programming Guide , is an official guide, which is very complete. In fact, just look at this. However, because the content is a little jumping, it is still a little difficult for me as a Xiaobai to fully understand it. It is better to match it with the tutorial notes. In "API reference", it is the explanation of routines.
3. Technical Manual: ESP32 H/W hardware reference , it's not enough to just look at the software program, but also think and understand it together with the hardware. Also in the official guide, the classification belongs to "ESP32 H/W hardware reference". The first "ESP32 technical reference manual (PDF)" is the data manual of the chip, which describes various hardware resources, register configuration, etc.
4. Official routine: github: esp-idf/examples/ , you can also view it in the ESP IDF folder or in the vscode plug-in. Here you can release the connection in the git warehouse. It's very convenient to view directly on the web/ Peripherals are mainly peripherals to learn. To learn MCU, start with peripherals. I can use it. It's easy to say anything else.

  • Now that you have prepared these four materials and the compiled environment, you can start learning esp32. Directly on the code routine, quick start. I used vscode to write + cmd compilation. The plug-in of vscode has not been done well, and I found that cmd is a little easier to use. If there is an error in compilation, you can also check the error content and the number of error lines, and check the error, and the running speed is very fast.

matters needing attention:

  1. I found that the IO naming format of the esp32 chip is IOx, while the one on the development board is Dx, which corresponds one by one. But when I read the tutorial of esp8266, I found that the Dx of esp8266 module does not correspond to IOx one by one?! Pay attention when you use it later.

1, Get started / Hello_ world&key&led

Official routine: github:esp-idf/examples/get-started/
Official Guide: it is not specifically listed. Part of it should be the content whether the test board can be downloaded or compiled normally, so it is not mentioned separately.
Tutorial notes: Chapter 8 ESP32 drive LED lamp

  • Under the example file get started, there are two basic IO control and printout files, combined with the boss's notes: ESP32 development 2 add to. c.h and modify CMakeLists to customize your own project . First write a project by yourself to realize serial port printing, light led lights and read keys.

  • It is generally thought that the MCU is similar to GPIO. A quick initialization function can be used to configure the io port gpio_pad_select_gpio, and then set the input / output direction gpio_set_direction, high and low level gpio_set_level, or read level gpio_get_level is OK. Serial port printing is print, because there is no local library to view directly, so I don't know where to link. Initialization and other work seem to have been done in advance.

/* Quick initialization */
/* Set the GPIO as a push/pull output */
gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
/* Set level */
gpio_set_level(BLINK_GPIO, 1);
/* Read level */
esp_err_t flag_i = gpio_get_level(18);
  • Simple api functions can also jump to the header file GPIO H check, and the reading level function is found in this way. The initial exploration was successful. At the same time, we launched our own library under the engineering components and began to cultivate habits. That's the case in the initial stage.

2, peripherals/gpio

Official routine: github:esp-idf/examples/peripherals/gpio/.
Official guidelines: GPIO & RTC GPIO , this chapter of the official guide has only a handful of English introductions, and then all API introductions.
Tutorial notes: Chapter 9 GPIO input key operation of ESP32 , RTC is not mentioned in the tutorial, nor are tasks and queues involved.

  • In terms of io configuration, the traditional structure GPIO is used_ config_ T + initialize gpio_config combination, you can open the declaration to view more enumeration / macro definition settings. At the same time, you can also select multiple pins at the same time. Remember to use shift conversion instead of directly losing decimal. Also, common changes
#define GPIO_OUTPUT_IO_0    5
#define GPIO_OUTPUT_IO_1    19

    gpio_config_t io_conf;
    //disable interrupt
    io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
    //set as output mode
    io_conf.mode = GPIO_MODE_OUTPUT;
    //bit mask of the pins that you want to set,e.g.GPIO18/19
    io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
    //disable pull-down mode turns off the pull-down mode
    io_conf.pull_down_en = 0;
    //disable pull-up mode disables the pull-up mode
    io_conf.pull_up_en = 0;
    //configure GPIO with the given settings

    //Change gpio intrupt type for one pin
    gpio_set_intr_type(GPIO_INPUT_IO_0, GPIO_INTR_ANYEDGE);
  • It should be noted that there is no need to enable the sub clock under the bus. esp has only one series of pins. GPIOx is written in the manual. Unlike other microcontrollers, there are so many pins and prefixes.

  • Moreover, I didn't expect that the first peripheral routine peripherals/gpio was so complex, using threads and semaphores? Compared with the RT thread system just learned recently, it seems to be similar to the FreeRTOS system in esp. (hey, hey, the role of previous learning has been reflected so quickly)

static xQueueHandle gpio_evt_queue = NULL;

static void IRAM_ATTR gpio_isr_handler(void* arg)
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);

static void gpio_task_example(void* arg)
    uint32_t io_num;
    for(;;) {
        if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));

    //create a queue to handle gpio event from isr
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    //Start gpio task start gpio task
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    //install gpio isr service
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(GPIO_INPUT_IO_1, gpio_isr_handler, (void*) GPIO_INPUT_IO_1);

    //remove isr handler for gpio number.  Delete ISR handler for GPIO number.
    //hook isr handler for specific gpio pin again
    gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
  1. Combined with the RT thread knowledge learned before, we know that ISR function means interrupt function. Through the routine, we can know that GPIO needs to be installed before using the interrupt function_ install_ isr_ Service. If it can be deleted after being linked, it can also be linked again after being deleted.
  2. There is also the xQueueCreate queue creation API in the previous step, which creates a queue with a length of 10 and writes the corresponding function GPIO to execute when the queue receives a signal_ task_ For example, this signal is sent from the interrupt function. Use xTaskCreate to initialize the task (function). It's very friendly to see this parameter list, which is similar to RTT system. The first blind guess is the function name of the task, and the second is the name of the task. It is generally called thread in RTT system, and thread is also used in English. In freertos, task is used, which means task in Chinese. This is similar, and the problem is not big. The third should refer to 2048 running space, and the fourth refers to the parameters of the task function, which is empty. Is the fifth priority or time slice? The sixth is the operation mode setting?
  3. For the task task, it should be noted that the parameter set by the function is NULL, and then the parameter is obtained by obtaining the queue information in the function. According to the comment "create a queue to handle gpio events in isr", it should be for a kind of programming thinking - the program does not execute in interrupts, so additional task tasks are set up.
  4. More information about the task can be found in the official manual. Unexpectedly, it is all in English. I think esp is made in China? Search the api documentation for xTaskCreate.

3, peripherals/ledc/

Official routine: github:esp-idf/examples/peripherals/ledc/.
Official guidelines: LED PWM controller , this official guide has a very detailed introduction, and I also found that the original browser can also use the shortcut key "ALT + ← / →" to jump back. Combined with the guide to view the api definition, it is more convenient than ide in some length.
Tutorial notes: Chapter 10 ESP32 Development Guide - PWM full color LED display.
Data book: ESP32 technical reference manual , in Chapter 14 of PDF: LED PWM controller (LEDC).

  • First look at the official guide. According to the "function Overview", the first step is timer configuration; Step 2: channel configuration; The third step is to change the PWM signal. Then corresponding to the routine code and the introduction of each step.
    ledc_timer_config_t ledc_timer = {              // Specify the value directly while creating, and remember the usage of the structure
        .duty_resolution = LEDC_TIMER_13_BIT,       // Resolution of PWM duty
        .freq_hz = 5000,                            // Frequency of PWM signal
        .speed_mode = LEDC_HIGH_SPEED_MODE,         // timer mode
        .timer_num = LEDC_TIMER_0,                  // Timer index timer index
        .clk_cfg = LEDC_AUTO_CLK,                   // Auto select the source clock
    // Set configuration of timer0 for high speed channels

     *   Note: if different channels use one timer, Note: if different channels use a timer,
     *         then frequency and bit_num of these channels will be the same Then the frequency and resolution of these channels will be the same
    ledc_channel_config_t ledc_channel = {
            .channel    = LEDC_CHANNEL_0,           // controller's channel number
            .duty       = 0,                        // output duty cycle, set initially to 0 output duty cycle, value range [0, (2**duty_resolution)]
            .gpio_num   = 5,                        // GPIO number where LED is connected to
            .speed_mode = LEDC_HIGH_SPEED_MODE,     // speed mode, either high or low
            .hpoint     = 0,                        // LEDC channel hpoint value, the max value is 0xfffff
            .timer_sel  = LEDC_TIMER_0              // timer servicing selected channel (0-3)
    ledc_set_duty(                                  //Use software to change PWM duty cycle
        ledc_channel.speed_mode,                    // Same below, 
        4000);                                      // Note that there is no function setting of transition time when using the software
        ledc_channel.speed_mode,                    // Same below;
    ledc_fade_func_install(0);                      // Initialize fade service.  Initialize the gradient service.
    ledc_set_fade_with_time(                        // Use hardware to change PWM duty cycle
        ledc_channel.speed_mode,                    // Select high and low speed,                       // Select the number of channels
        4000,                                       // Set duty cycle
        3000);                                      // Set transition time
    ledc_fade_start(                                // Make the new configuration effective
        ledc_channel.speed_mode,                    // ditto, 
        LEDC_FADE_NO_WAIT);                         // Whether to block until fading done.  Whether to block until fade out.
  • The above is the main code of the three-step process, which is still struct+config. Points needing attention:

    1. The duty cycle of software modification has no transition time and belongs to direct modification. LEDC needs to be called before the hardware modifies the duty cycle_ fade_ func_ install.
    1. The duty cycle value range is related to the resolution, and then the resolution is related to the frequency.
    1. ledc_ channel_ config_ In the T structure, the number of hpoint channels is unknown. This parameter is not written in the tutorial notes, so I don't need to care about its function for the time being. One of the intr routines is missing_ Type interrupt enable parameter.
    1. The modified duty cycle of hardware is in LEDC_ fade_ The mode can be selected in the start effective function, for example: LEDC_FADE_NO_WAIT.
  • Combined with hardware knowledge to understand the configuration, you can ledc_timer_bit_t resolution (1-20), ledc_mode_t timing mode (high or low), ledc_timer_tLEDC timer (0-3), ledc_channel_tLEDC channel (0-7).

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (IMG heeb0mnw-1620429634623) (. / img_list / 20210430195240. PNG)]

  • More operations: change PWM frequency, more ways to control PWM, use interrupt, and the difference between high and low speed modes. You can check the manual when you use them. Note that different frequency resolution and duty cycle can be substituted into different frequency resolution formulas. Compare the frequency, duty cycle and total duty cycle set when playing smart car. At that time, the library should have packed the calculation, and because it did not exceed the range, it did not care about the parameter of resolution.

4, peripherals/uart/

Official routine: github:esp-idf/examples/peripherals/uart/ , the official routine gives many uart routines, and the official guide is all in English, so I'll start with the tutorial notes and learn how to use the basic functions first.
Official guidelines: UART , another article in English, woo woo.
Tutorial notes: Chapter 11 two UART experiments of ESP32 , the mapping pin written in the note is different from my board. You should also pay attention to it when using it and check the corresponding manual. Avoid doubting yourself.
Data book: ESP32 technical reference manual , in Chapter 13 of PDF: UART controller (UART).
Module Manual: ESP32­WROOM­32 Among them, 2.2 pin definitions record the pin definitions. You can see which pins uart0 and uart2 lead out from the development board refer to.

  • First of all, I want to find out which serial port is used when downloading the program of esp32 module. According to the previous thinking, I jump to calling the main function app_main, found esp32/cpu_start.c. There are two cpu core calls in it_ start_ cpu0 / 1 function. Where call_start_cpu0 is similar to the RT tread system learned before, including frequency initialization, vector loading, special external ram and flash initialization, and then a bunch of initialization that I didn't understand. Then call_ start_ The cpu1 function is much simpler. There are only some basic initializations, including uart0 initialization. The jump can only turn to the header file and can't see the specific content. However, the comment says that uart0 is initialized, which seems to be a dead function. There is main at the end of the file_ Task task function, which calls the main function app_main.

  • The routine content in the tutorial notes is to initialize uart1 and uart2, create two task functions respectively, receive the information of another serial port and send it back, and the main function sends it once again as the starting condition. Summary: Step 1: initialization; Step 2: just read or receive. Note that after config is configured, there are two more steps: IO mapping and installation enabling. Then you can read and write normally.

Because uart1 is the SPI flash integrated on the connection module, I want to change it to uart0, but the serial port is the printing function of the system. And I don't know why there is no expected experimental effect... It's OK to try the official routine. I should have made a mistake... Or I can't connect uart0 and uart2 in series? Change and learn on the basis of official routines, and the tutorial notes may be expired or wrong. The next day, I wrote it again. The configuration function is OK again... The task function hasn't changed yet. Maybe I made a mistake before. I compared the configuration functions. It seems that the same is true. However, uart0 printing still has some problems. Should it be the problem of receiving? “L�&�+�This is a test string. - 4;This is a test string. - 4”???

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,
    .rx_flow_ctrl_thresh = 122,
// Configure UART parameters
uart_param_config(uart_num, &uart_config);

// Set UART pins(TX: IO17 (UART2 default), RX: IO16 (UART2 default), RTS: IO18, CTS: IO19)
uart_set_pin(uart_num, tx_io, rx_io, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

// Setup UART buffered IO with event queue
const int uart_buffer_size = (512 * 2);
// QueueHandle_t uart_queue;
// Install UART driver using an event queue here
uart_driver_install(uart_num, uart_buffer_size, \
                        uart_buffer_size, 10, NULL, 0);

//Serial port 0 data sending test. The parameters are: serial port number, data pointer and data size
uart_write_bytes(UART_NUM_0, "uart0 test OK ", strlen("uart0 test OK "));
//Allocate memory for serial port reception
uint8_t* data = (uint8_t*) malloc(1024+1);     
//Get the data received by serial port 1 and return the number of bytes received. If it is 0, it is timeout. The last parameter is timeout / wait time
const int rxBytes = uart_read_bytes(UART_NUM_0, data, 1024, 10 / portTICK_RATE_MS);

1) UART Overview

  • Next introduce Official Guide In depth understanding of the use of esp32/uart.

  • The guide summarizes the six steps of using UART, three of which are the necessary configuration stages: 1 Set communication parameters - set baud rate, data bit, stop bit, etc.; 2. Set the communication pin - assign the connection pin to the device.; 3. Driver installation - allocate ESP32 resources for UART driver.. The fourth is the operation phase: run UART communication - send / receive data. The fifth and sixth are optional: use interrupts - interrupts that trigger specific communication events and delete drivers - release allocated resources if UART communication is no longer required. In addition, it is worth noting that each function will have UART_ port_ The T parameter is used to specify the serial port number. The selection range is (0, 1, 2).

    1. Step 1: set the communication parameters. The guide puts forward two schemes. One is all configurations in one step. That is the example of the above code. Use struct+config in one step. The second is step-by-step, because ESP IDF provides many APIs set separately, and each corresponding one has a get read API to call. As shown in the figure below:

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-1XMbzPK2-1620429634628)(./img_list/20210501072415.png)]

    1. Step 2: set the communication pin and call uart_ set_ The pin function can fill in the corresponding function. This function is a little like pin initialization or pin multiplexing, because the uart module may support the multiplexing of multiple pins, so we need to choose which pin to use. At the same time, it also has a default configuration. If you don't want to modify the pin specified for yourself (for example, you don't care about it if you can't use the pin), you can fill UART_PIN_NO_CHANGE as a replacement. The corresponding operation also appears in the above code. The selected parameters of RTS pin and CTS pin remain the default, while the RX and TX pins remain the default in the guide. In addition, it should be noted that ESP is also used in the guide_ ERROR_ Check macro is defined to nest calling functions, which should play the role of checking parameters.

Episode, I thought the default pins of uart2 were io17 and io16, so UART was used_ PIN_ NO_ Change instead, that's what the official guide says. Unexpectedly, I couldn't receive the sent information. Then I thought it was the wrong configuration, so I changed the serial port number to uart0. By using the default pin, the content can be sent again, so I ruled out the problem in the configuration process. In addition, test whether the hardware is connected. tx2 and rx2 measured by multimeter are also connected to io17 and io16 of the module. Finally, I tried to specify io17 and io16 instead of using the default parameters, and the result was successful. So I come to the conclusion that the default uart2 pins of esp32 are not io17 and io16, but this contradicts the instructions in the guide and manual

    1. Step 3: install the driver. Once the communication pin is set, call uart_driver_install() installs the driver and specifies the following parameters: the size of Tx ring buffer, the size of Rx ring buffer, event queue handle and size, and flag to allocate interrupts. This function will allocate the required internal resources for the UART driver. After completing this step, you can connect the external UART device and check the communication. (the translation of the official guide is very clear)
// Setup UART buffered IO with event queue
const int uart_buffer_size = (1024 * 2);
// This step is not quite understandable. Let it go first and see it later.
QueueHandle_t uart_queue;       
// Install UART driver using an event queue here
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_2, uart_buffer_size, \
                                        uart_buffer_size, 10, &uart_queue, 0));

Personal understanding UART_ driver_ UART in install_ Queue parameter, in the official guide API description UART event queue handle (output parameter). If successful, a new queue handle will be written here to provide access to UART events. If set to null, the driver will not use the event queue. Combined with the previous study of gpio, we also used the QueueHandle queue handle, and then look at the previous gpio routine, which uses static xQueueHandle gpio_evt_queue = NULL; The UART routine uses the static QueueHandle_t uart0_queue;, You can find that xQueueHandle is a QueueHandle by jumping_ A packaged macro definition (alias) of T. I don't know why so many duplicate names should be defined... Combined with the application scenario, it is "natural / natural" to understand: if this parameter is configured, When the serial port receives or sends information (later, combined with routine practice, it is known that each state change of the serial port will trigger once, not just receiving and sending), a queue signal will be generated, and then obtained by xQueueReceive function. This can replace the interrupt function (?). (after learning rtos operating system) I prefer to run with thread + queue rather than interrupt.

2) peripherals/uart/uart_events/

After testing, it is found that the function of this routine is to receive serial port data and end with a specific number of consecutive frame endings. If the received data does not contain the end of the frame, it will be output normally. If it is included, it will be truncated directly, and the remaining data will be read for the second time. (at the beginning, I thought it was the frame head, but I didn't understand the test law)

  • The experimental phenomenon is quite interesting because I'm curious about how the serial port judges that the received data contains a specific frame tail. If you want to monitor, you should have received it once, but according to the routine, in uart_driver_install when installing the UART driver, the specified uart0_queue queue, in UART_ event_ In the task task, (these three sentences may be a little windy) is to monitor the contents of the queue first, and the data in the queue is a structure (clever usage, more and more feel the appearance of object usage). The type attribute in the structure already indicates what data is received.

  • If the data contains the end of the frame, UART will be returned_ PATTERN_ Det flag. If it is ordinary data, UART will be returned_ Data flag. Read the contents of the serial port only after judging the flag. Moreover, if the frame tail is included, only the frame tail will be read, the remaining data will be retained until the next time, and the queue return can be triggered. FreeRTOS system is really wonderful!

  • To summarize the procedure flow:

    1. The first stage: 1) configure UART driver parameters; 2) Install UART driver and queue; 3) Set UART pin;
    1. The second stage: 1) set uart mode detection function; 2) Set the maximum length of the detection flag (it seems that it is not the length of the data, but the length of the end of the frame. It should be set in the previous step, so this step is a protective measure?);
    1. The third stage: 1) create a task to process the queue content; 2) Write the content of the task, read the queue, and check the event Type, print the corresponding content as required.
    1. Extra operation: ESP is also used in this routine_ log_ level_ Set log settings and ESP_ According to the experimental phenomenon, logi log printing feels similar to printf printing. The difference is that the printed content will be wrapped with prefixes and suffixes. If it is received by the terminal, these prefixes and suffixes represent the displayed color. If it is received by other copper laying serial port assistants, unknown characters will be displayed. This should not be necessary, just look
    /* Configure parameters of an UART driver, Configure UART driver parameters,
     * communication pins and install the driver Communication pin and installation driver */
    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_param_config(EX_UART_NUM, &uart_config);
    //Install UART driver, and get the queue.  Install the UART driver and get the queue.
    uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart0_queue, 0);

    //Set UART log level sets the UART log level
    esp_log_level_set(TAG, ESP_LOG_INFO);
    //Set UART pins (using UART0 default pins ie no changes.)  Set UART pin (use UART0 default pin, i.e. no change)

    //Set uart pattern detect function.  Set UART mode detection function.
    uart_enable_pattern_det_baud_intr(EX_UART_NUM, '+', PATTERN_CHR_NUM, 9, 0, 0);
    //Reset the pattern queue length to record at most 20 pattern positions.  Reset the pattern queue length to record up to 20 pattern positions.
    uart_pattern_queue_reset(EX_UART_NUM, 20);

    //Create a task to handler UART event from ISR
    xTaskCreate(uart_event_task, "uart_event_task", 2048, NULL, 12, NULL);
static void uart_event_task(void *pvParameters)
    /* ellipsis */
    if(xQueueReceive(uart0_queue, (void * )&event, (portTickType)portMAX_DELAY)) 
        { /* Make content according to different signs */}
    /* ellipsis */
  • There are several trigger event types in the routine that haven't tested the phenomenon, so I'm not sure when to use them. When you encounter them later, look back. They are: UART_FIFO_OVF,UART_BUFFER_FULL,UART_BREAK,UART_PARITY_ERR,UART_FRAME_ERR. It seems that this form is a bit like typ error detection in python.

Additional test: tested, UART_ read_ The rxBytes return value of the bytes read function represents the length of the received data. In the tutorial, if you want to send the original data, you need to manually add an end character at the end, which is 0. Curious why not \ 0? Or other values to ensure that rxBytes is used to specify the end. It should also be for fear that the value in the initialization memory is unknown, so it is necessary to manually assign 0 just in case.

  • Took a look at UART_ async_ Rxtasks and UART_ The introduction of the experiment in the select routine feels similar. In order to catch up with the progress and space, I'll skip it temporarily. I'll use it later and come back.

Keywords: Single-Chip Microcomputer ESP32

Added by itshim on Thu, 17 Feb 2022 16:41:00 +0200