앞서 문자열을 출력하는 것은 해보았으니.. 이번에는 터미널을 이용하여 시리얼 포트에서 부터 데이터를 받아오는 예제를 작성해 보도록 하자. 

     

    --- contents ---

    01. [ESP32] VS Code 개발환경 구성 : https://makeutil.tistory.com/303

    02. [ESP32] 첫 프로젝트 생성하기 : https://makeutil.tistory.com/304

    03. [ESP32] 멀티 테스크 예제 (2 Task) : https://makeutil.tistory.com/305

    04. [ESP32] Task간 데이터 공유 (Queue, Mutex) : https://makeutil.tistory.com/306

    05. [ESP32] 개발보드 별 형상 및 I/O (ESP32/ESP32-S3) : https://makeutil.tistory.com/307

    06. [ESP32] UART 통신 예제 #1 : https://makeutil.tistory.com/309 

    07. [ESP32] UART 통신 예제 #2 : 현재 글

    08. [ESP32] GPIO LED 켜기 : https://makeutil.tistory.com/311

    09. [ESP32] Timer와 PWM을 이용한 LED 점멸 : https://makeutil.tistory.com/312

     

    A1. [ESP32] 오류 - Fatal Error : No such file or directory : https://makeutil.tistory.com/308

    ------------------

     

    1. 터미널에서 데이터 입력 받기

    1.1. 소스작성

      앞서 출력 소스를 수정하여 다음과 같이 만들고 빌드를 진행한다. 

    #include <stdio.h>
    #include "esp_log.h"
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/uart.h"
    #include "string.h"
    
    static const char *TAG = "UART2";
    
    #define UART_PORT_NUM      UART_NUM_2   // UART 2번 포트
    
    #define BUF_SIZE           1024         // 버퍼 크기
    
    void app_main(void)
    {
        char data[128];
    
        // UART 설정: 115200 baud rate, 8 data bits, no parity, 1 stop bit
        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
        };
    
        memset(data,0,sizeof(data));
    
        // UART 드라이버 초기화
        ESP_ERROR_CHECK(uart_driver_install(UART_PORT_NUM, BUF_SIZE, BUF_SIZE, 0, NULL, 0));
        ESP_ERROR_CHECK(uart_param_config(UART_PORT_NUM, &uart_config));
        ESP_ERROR_CHECK(uart_set_pin(UART_PORT_NUM, 19, 20, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));    // RX, TX 핀 설정 : UART2
        
        int len; 
    
        while (1) 
        {
            len = uart_read_bytes(UART_PORT_NUM, data, sizeof(data) - 1, pdMS_TO_TICKS(20));
            if (len > 0) {
                data[len] = '\0';
                ESP_LOGI(TAG, "Data : %s", data);
            }  
    
            vTaskDelay(pdMS_TO_TICKS(100)); // 100ms 딜레이 추가
        }
    }

     

     

     

    1.2. 설명

      특별히 설명할 것은 없다. 아래의 100ms 딜레이는 CPU의 부하를 줄이기 위해서 추가되었다. 기타 설정은 동일하고, uart_read_bytes를 통해 키보드로부터 데이터를 입력 받는다. 그리고 버퍼인 data에 넣은 이 후 출력한다.

    부가적으로 LOGI를 쓰기 위해서 esp_log.h를 추가하였고, LOGI() 형식에 따라 static const * 변수를 추가하였다.

     

     

    1.3. 테스트 결과 

      결과는 아래의 그림과 같다. 

     

      터미널을 실행하여 ESP32와 연결된 USB to TTL 을 연결한다. 아래의 그림에서 알수 있듯, 해당 시리얼 포트는 COM3이기 때문에 테라텀이나 Putty 같은 터미널 소프트웨어를 실행하여 연결하고, 터미널에서 키보드를 누르면, 해당 키보드의 데이터가 ESP32로 들어와 LOGI에 의해서 출력되는 것을 알 수 있다. 

     

     

     

    2. 비동기 입출력

      이렇게 만들어놓고보니, 이제 입력과 출력을 동시에 하고 싶어졌다. 그래서 select()를 써서 코딩을 해봤는데. 안되는것 같다. 그래서 그냥 task를 하나 더 만들어서 처리하도록 수정해봤다. 

     

    2.1. 소스코드

      출력용테스크를 만들어서 입력과 출력을 분리하였다. 

     

    // 입력 처리용 task
    void uart_input_task(void *arg) 
    {
        char data[128];
    
        while (1) {
            int len = uart_read_bytes(UART_PORT_NUM, (uint8_t *)data, sizeof(data) - 1, pdMS_TO_TICKS(100));
            if (len > 0) {
                data[len] = '\0';   
                ESP_LOGI(TAG, "Received: %s", data);
            }
        }
    }
    
    // 출력 처리용 task
    void uart_output_task(void *arg) 
    {
        while (1) {
            uart_write_bytes(UART_PORT_NUM, "Hello World\r\n", strlen("Hello World\r\n"));
            vTaskDelay(pdMS_TO_TICKS(1000)); // 1초 대기
        }
    }
    
    void app_main(void) 
    {
        uart_init(); // UART 설정은 여기에다 밀어넣었다. 
        
        // while문의 내용을 input task로..
        xTaskCreate(uart_input_task, "uart_input_task", 2048, NULL, 10, NULL);
        
        // 출력용 task를 output task로..
        xTaskCreate(uart_output_task, "uart_output_task", 2048, NULL, 5, NULL);
    }

     

     

    2.2. 결과

       결과는 아래와 같다. 입력도 되고, 출력도 된다. 하나의 task에서 처리할 수 있는 방법은 좀더 알아봐야될것 같다. 시작한지 몇일되지 않아서 아직은 좀 낮설다.

     

     

    3. 인터럽트로 입력받기

      근데 조금 맘에 걸리는게 있다. 키보드 입력시마다 인터럽트가 걸리면 한바이트씩 출력이 되는데, 인터럽트 방식이 아니라서 조금 없어보인다. 그래서 인터럽트를 이용해서 입력이 발생할 때마다 한바이트씩 처리되도록 만들어보자.

     

    3.1. 소스코드

      우리가 간단한 프로그램을 만들때 굳이 이벤트를 사용하지 않는다. 하지만 폴링을 이용하다보면, 이전의 예제처럼 랜덤한길이의 데이터가 발생된다. 그래도 상관없다면 모르겟지만, 데이터가 한바이트씩 들어와야되는 상황이라면 앞서 이용한 폴링방식으로는 처리가 어려울 수 있다. 특히 비동기 통신에서는 더더욱 그러하다. 그래서 시리얼에서 데이터를 읽어오는 함수인 uart_input_task를 uart_event_task로 이름을 바꾸고, Queue를 이용하여 처리하는 방식으로 변경했다.

    #include <stdio.h>
    #include <string.h>
    #include "esp_log.h"
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "freertos/queue.h"
    #include "driver/uart.h"
    
    static const char *TAG = "UART2";
    
    #define UART_PORT_NUM      UART_NUM_2
    #define BUF_SIZE           1024
    #define RX_PIN             19
    #define TX_PIN             20
    
    static QueueHandle_t uart_queue; // UART 인터럽트를 처리할 Queue 핸들이다.
    
    // UART 설정 및 초기화 함수
    void uart_init(void) {
        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
        };
    
        // 큐 할당: 이벤트 큐 사용 (인터럽트 기반)
        ESP_ERROR_CHECK(uart_driver_install(UART_PORT_NUM, BUF_SIZE, BUF_SIZE, 20, &uart_queue, 0));
        ESP_ERROR_CHECK(uart_param_config(UART_PORT_NUM, &uart_config));
        ESP_ERROR_CHECK(uart_set_pin(UART_PORT_NUM, RX_PIN, TX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    }
    
    // 인터럽트 기반 UART 수신 처리
    void uart_event_task(void *arg) {
        uart_event_t event;
        uint8_t data[BUF_SIZE];
    
        while (1) {
            // 기존은 별도의 태스크를 만들어서 타이머를 이용하여 폴링(반복)하면서 처리하였다면
            // 이번에는 xQueueReceive를 이용하여 인터럽트를 처리한다.  이벤트가 발생되면 queue에 이벤트가 저장되고 
            if (xQueueReceive(uart_queue, (void *)&event, portMAX_DELAY)) { 
                if (event.type == UART_DATA && event.size > 0) {   // 들어온 이벤트가 있으면
                    int len = uart_read_bytes(UART_PORT_NUM, data, event.size, portMAX_DELAY); // 데이터를 읽는다.
                    if (len > 0) {
                        data[len] = '\0';  
                        ESP_LOGI(TAG, "Received: %s", data);
                    }
                }
            }
        }
    }
    
    // 출력 태스크는 수정없이 이전의 예제와 같이 그대로 유지했다.
    void uart_output_task(void *arg) {
        while (1) {
            uart_write_bytes(UART_PORT_NUM, "Hello World\r\n", strlen("Hello World\r\n"));
            vTaskDelay(pdMS_TO_TICKS(1000));
        }
    }
    
    void app_main(void) {
        uart_init();
        // Queue를 쓰다보면 스택 공간이 모자랄 수 있어서 4K를 할당했다.
        xTaskCreate(uart_event_task, "uart_event_task", 4096, NULL, 10, NULL);
        xTaskCreate(uart_output_task, "uart_output_task", 2048, NULL, 5, NULL);
    }

     

     

    3.2. 실행결과

      실행결과를 보도록 하자. UART에서 입력이 발생되면 이벤트가 발생되면서 한 바이트씩 VSCode에 출력됨을 확인할 수 있다. 그리고 터미널에는 헬로월드가 출력되고 말이다. 사실 MCU를 이용하고 시리얼 인터페에스를 여러개를 사용할 때, 이렇게 한바이트씩 읽는 장치를 여러개를 일겅야된다면 동기와 관련된 이런저런 문제가 발생될 수 있다. 

     

      그런 코딩을 해야하는때가 오면 그때 고민하도록 하겠다. 

     

      앞 예제에서 UART와 관련된 몇 가지 예제를 진행했다. 그 중에서 입출력을 분리하기 위해서 2개의 Task를 이용했다. 우리가 일반적으로 리눅스를 이용하여 프로그래밍을 할 때에는 select를 이용하여 하나의 프로세스에서 입력과 출력을 분리하여 사용하였다. 이는 POSIX등 지원여부와 관련이 있을 수 있지만 말이다. 어쨋든, 프로그래밍 하다보면 여러개의 task를 생성하기 힘든 상황이 분명 올 수 있다. 

     

      필자의 지인에게 이런저런 이야기를 하다가 물어봤는데, FreeRTOS에서는 LWIP를 불러서 내부의 .select()를 사용하는 방법도있고, Queue, 세마포어등을 이용하여 구현도 가능하다고 한다. 아니면 FreeRTOS 말고 제퍼를 사용하면 된다고 한다... 아직 필요한 상황은 아니라서... 필요하면 다시 물어보고 하던지 해야겠다.

    반응형
    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기