이번에는 uart에 관해서 분석해 보겠다.

uart는 범용 비동기 송수신기( Universal Asynchronous Receiver / Transmitter)의 약자로 두 장치 간에 '직렬' 데이터를 교환하기 위한 프로토콜이다.

 

필자가 생각하는 비동기와 동기의 차이는

동기는 clock을 통해 신호의 처음과 끝을 알리며 clock이 on(high)일때만 데이터 전송이 가능하고 clock이 off(low)일때는 데이터가 전송되지 않는다.

비동기는 동기에서 사용된 clock신호가 없다. uart에서는 start bit와 stop bit를 데이터비트 전후로 입력하여 데이터 전송을 구분한다.

 

  • 시작 비트 : 통신의 시작을 의미하며 한 비트 시간 길이 만큼 유지한다. 지금부터 정해진 약속에 따라 통신을 시작한다.
  • 데이터 비트 : 5~8비트의 데이터 전송을 한다. 몇 비트를 사용할 것인지는 해당 레지스터 설정에 따라 결정된다.
  • 패리티 비트 : 오류 검증을 하기 위한 패리티 값을 생성하여 송신하고 수신쪽에 오류 판단한다. 사용안함, 짝수, 홀수 패리티 등의 세가지 옵션으로 해당 레지스터 설정에 따라 선택할 수 있다. '사용안함'을 선택하면 이 비트가 제거된다.
  • 끝 비트 : 통신 종료를 알린다. 세가지의 정해진 비트 만큼 유지해야 한다. 1, 1.5, 2비트로 해당 레지스터 설정에 따라 결정된다.

출처: https://ko.wikipedia.org/wiki/UART

 

출처에 따르면 8비트~12비트 정도의 프레임 단위로 데이터를 전송한다.

또한 uart에는 보율(baudrate) 라는게 필요하다. 이는 간단하게 1초에 얼마나 많은 비트를 보낼 수 있는가 이다.

예를 들어 송신측에서 1초동안 이진수 10 이라는 데이터를 보냈는데 수신측에서 1초동안 1비트만 받을 수 있다고 하면 1이라는 데이터밖에 읽지 못하기 때문인 것 같다.

 

tera term

 

결과부터 보면

11byte를 썻다.

라고 되어있고 아래 uart_async_rxtxtasks_main.c 파일을 보면 "hello world"의 11byte를 말하는 것 같다.

 

이제 분석을 시작해 본다.

 

/* UART asynchronous example, that uses separate RX and TX tasks

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "driver/uart.h"
#include "string.h"
#include "driver/gpio.h"

static const int RX_BUF_SIZE = 1024;

#define TXD_PIN (GPIO_NUM_4)
#define RXD_PIN (GPIO_NUM_5)

void init(void) {
    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,
    };
    // We won't use a buffer for sending data.
    uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
    uart_param_config(UART_NUM_1, &uart_config);
    uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

int sendData(const char* logName, const char* data)
{
    const int len = strlen(data);
    const int txBytes = uart_write_bytes(UART_NUM_1, data, len);
    ESP_LOGI(logName, "Wrote %d bytes", txBytes);
    return txBytes;
}

static void tx_task(void *arg)
{
    static const char *TX_TASK_TAG = "TX_TASK";
    esp_log_level_set(TX_TASK_TAG, ESP_LOG_INFO);
    while (1) {
        sendData(TX_TASK_TAG, "Hello world");
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}

static void rx_task(void *arg)
{
    static const char *RX_TASK_TAG = "RX_TASK";
    esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO);
    uint8_t* data = (uint8_t*) malloc(RX_BUF_SIZE+1);
    while (1) {
        const int rxBytes = uart_read_bytes(UART_NUM_1, data, RX_BUF_SIZE, 1000 / portTICK_RATE_MS);
        if (rxBytes > 0) {
            data[rxBytes] = 0;
            ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, data);
            ESP_LOG_BUFFER_HEXDUMP(RX_TASK_TAG, data, rxBytes, ESP_LOG_INFO);
        }
    }
    free(data);
}

void app_main(void)
{
    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);
}

 

일단 RX는 수신측 핀, TX는 송신측 핀이다.

여기서 사용되는 uart통신은 2개의 반이중방식의 선으로 이루어진걸 알 수있다.

*전이중 방식의 선 하나로 되어있는 경우도 있다.(이렇게 하면 선을 더 아낄텐데...)

 

헤더파일 아래 부분에 수신측 버퍼크기를 정의하고 그 아래에 송,수신측의 gpio 번호를 정한다.

 

// Valid UART port number
#define UART_NUM_0             (0) /*!< UART port 0 */
#define UART_NUM_1             (1) /*!< UART port 1 */
#if SOC_UART_NUM > 2
#define UART_NUM_2             (2) /*!< UART port 2 */
#endif
#define UART_NUM_MAX           (SOC_UART_NUM) /*!< UART port max */

pin map

 

위 핀맵을 보면 GPIO1,3,16,17가 사용이 가능하며 다른 블로그 참조에 의하면 9,10번또한 사용이 가능하다.

 

serial0

UART 0 TX  ->  GPIO1

UART 0 RX  ->  GPIO3

 

serial1

UART 1 TX (SD2) ->  GPIO10

UART 1 RX (SD3) ->  GPIO9

 

serial2

UART 2 TX  ->  GPIO17

UART 2 RX  ->  GPIO16

 

하지만 9,10번은 spi flash로 사용중이며 1,3번은 usb가 사용중이라고 하여 사용시 16,17번 핀을 사용하여 통신을 하면 될 것이다.

uart_async_rxtxtasks_main.c이 코드에서 gpio4,5를 사용한 이유는 잘 모르겠다.

 

참조: https://blog.naver.com/PostView.naver?blogId=heennavi1004&logNo=222316035038

 

app_main()의 시작 init()함수를 보자.

 

/**
 * @brief UART configuration parameters for uart_param_config function
 */
typedef struct {
    int baud_rate;                      /*!< UART baud rate*/
    uart_word_length_t data_bits;       /*!< UART byte size*/
    uart_parity_t parity;               /*!< UART parity mode*/
    uart_stop_bits_t stop_bits;         /*!< UART stop bits*/
    uart_hw_flowcontrol_t flow_ctrl;    /*!< UART HW flow control mode (cts/rts)*/
    uint8_t rx_flow_ctrl_thresh;        /*!< UART HW RTS threshold*/
    union {
        uart_sclk_t source_clk;         /*!< UART source clock selection */
        bool use_ref_tick  __attribute__((deprecated)); /*!< Deprecated method to select ref tick clock source, set source_clk field instead */
    };
} uart_config_t;

baud_rate, data_bits, parity, stop_bits까지는 위에서 말했듯 사용할 uart통신에 필요한 보율, 데이터 비트, 패티리 비트, 스탑 비트 를 정의한다. 이 프로그램에서는 시작 비트는 없다.(왜인지는 잘 모르겠다.)

그리고 이는 115200, 8bit, 없음, 1bit 이다. 시작 비트나 패리티 비트는 통신에 필수적으로 필요한 것들은 아닌가....?

 

/**
 * @brief UART hardware flow control modes
 */
typedef enum {
    UART_HW_FLOWCTRL_DISABLE = 0x0,   /*!< disable hardware flow control*/
    UART_HW_FLOWCTRL_RTS     = 0x1,   /*!< enable RX hardware flow control (rts)*/
    UART_HW_FLOWCTRL_CTS     = 0x2,   /*!< enable TX hardware flow control (cts)*/
    UART_HW_FLOWCTRL_CTS_RTS = 0x3,   /*!< enable hardware flow control*/
    UART_HW_FLOWCTRL_MAX     = 0x4,
} uart_hw_flowcontrol_t;

 

flow_ctrl은 흐름제어를 위한 비트인 것 같다. rts는 ready to send, cts는 clear to send 라고 하여 각각 송신준비 완료 및 수신준비 완료를 알려준다. 이또한 이 프로그램에서는 DISABLE로 설정되어 사용하지 않는 듯 하다.

 

/**
 * @brief UART source clock
 */
typedef enum {
    UART_SCLK_APB = 0x0,            /*!< UART source clock from APB*/
#if SOC_UART_SUPPORT_RTC_CLK
    UART_SCLK_RTC = 0x1,            /*!< UART source clock from RTC*/
#endif
#if SOC_UART_SUPPORT_XTAL_CLK
    UART_SCLK_XTAL = 0x2,           /*!< UART source clock from XTAL*/
#endif
#if SOC_UART_SUPPORT_REF_TICK
    UART_SCLK_REF_TICK = 0x3,       /*!< UART source clock from REF_TICK*/
#endif
} uart_sclk_t;

 

source_clk은 apb(Advanced Peripheral Bus)라는 프로토콜의 클락을 이용한다.

apb는 비교적 느린 속도의 주변장치를 제어한다. 전력소모를 줄이기 위해 간단한 인터페이스를 갖는다.

 

rtc는 real time clock이라고 하여 보드 내에서 배터리를 이용한 시간을 사용하는데 이는 실재 시간과 같다고 한다.

xtal은 크리스탈이라는 이름으로 불리며 xx.xxxxhz와 같이 정밀한 주파수가 필요할때 사용한다고 한다.

 

아래 참조에 apb,ahb 등에 관해 잘 정리 해놓은 포스팅이 있다. 참고하길 바란다.

 

참조: https://boradol0902.tistory.com/10

 

 

 

uart_driver_install()
 
uart.c

 

uart_driver_install의 함수 원형 내부이다.

line 1552~1554를 보면 uart_num이 정해진 uart개수인 3보다 작아야한다.

rx_buffer_size가 soc_uart_fifo_len(128로 정의됨)보다 커야하고

tx_buffer_size가 soc_uart_fifo_len보다 크거나 0이어야 한다.

다면 에러를 출력할 것이다.

 

uart.c

 

인터럽트 관련 플래그 설정을 마친 후에

 

#endif

    if (p_uart_obj[uart_num] == NULL) {
        p_uart_obj[uart_num] = uart_alloc_driver_obj(event_queue_size, tx_buffer_size, rx_buffer_size);
        if (p_uart_obj[uart_num] == NULL) {
            ESP_LOGE(UART_TAG, "UART driver malloc error");
            return ESP_FAIL;
        }
        p_uart_obj[uart_num]->uart_num = uart_num;
        p_uart_obj[uart_num]->uart_mode = UART_MODE_UART;
        p_uart_obj[uart_num]->coll_det_flg = false;
        p_uart_obj[uart_num]->rx_always_timeout_flg = false;
        p_uart_obj[uart_num]->event_queue_size = event_queue_size;
        p_uart_obj[uart_num]->tx_ptr = NULL;
        p_uart_obj[uart_num]->tx_head = NULL;
        p_uart_obj[uart_num]->tx_len_tot = 0;
        p_uart_obj[uart_num]->tx_brk_flg = 0;
        p_uart_obj[uart_num]->tx_brk_len = 0;
        p_uart_obj[uart_num]->tx_waiting_brk = 0;
        p_uart_obj[uart_num]->rx_buffered_len = 0;
        p_uart_obj[uart_num]->rx_buffer_full_flg = false;
        p_uart_obj[uart_num]->tx_waiting_fifo = false;
        p_uart_obj[uart_num]->rx_ptr = NULL;
        p_uart_obj[uart_num]->rx_cur_remain = 0;
        p_uart_obj[uart_num]->rx_int_usr_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT;
        p_uart_obj[uart_num]->rx_head_ptr = NULL;
        p_uart_obj[uart_num]->tx_buf_size = tx_buffer_size;
        p_uart_obj[uart_num]->uart_select_notif_callback = NULL;
        xSemaphoreGive(p_uart_obj[uart_num]->tx_fifo_sem);
        uart_pattern_queue_reset(uart_num, UART_PATTERN_DET_QLEN_DEFAULT);
        if (uart_queue) {
            *uart_queue = p_uart_obj[uart_num]->event_queue;
            ESP_LOGI(UART_TAG, "queue free spaces: %d", uxQueueSpacesAvailable(p_uart_obj[uart_num]->event_queue));
        }
    } else {
        ESP_LOGE(UART_TAG, "UART driver already installed");
        return ESP_FAIL;
    }

    uart_intr_config_t uart_intr = {
        .intr_enable_mask = UART_INTR_CONFIG_FLAG,
        .rxfifo_full_thresh = UART_FULL_THRESH_DEFAULT,
        .rx_timeout_thresh = UART_TOUT_THRESH_DEFAULT,
        .txfifo_empty_intr_thresh = UART_EMPTY_THRESH_DEFAULT,
    };
    uart_module_enable(uart_num);
    uart_hal_disable_intr_mask(&(uart_context[uart_num].hal), UART_LL_INTR_MASK);
    uart_hal_clr_intsts_mask(&(uart_context[uart_num].hal), UART_LL_INTR_MASK);
    r = uart_isr_register(uart_num, uart_rx_intr_handler_default, p_uart_obj[uart_num], intr_alloc_flags, &p_uart_obj[uart_num]->intr_handle);
    if (r != ESP_OK) {
        goto err;
    }
    r = uart_intr_config(uart_num, &uart_intr);
    if (r != ESP_OK) {
        goto err;
    }
    return r;

err:
    uart_driver_delete(uart_num);
    return r;

 

메인에서 받아온 인자들로 하여금 uart의 번호, rx,tx의 버퍼 사이즈, 세마포, 큐 사이즈 등의 설정을 한다.

 

uart_driver_install() 함수가 uart통신에 관한 칩 내에 만들어지는 객체라면 

 

esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config)
{
    ESP_RETURN_ON_FALSE((uart_num < UART_NUM_MAX), ESP_FAIL, UART_TAG, "uart_num error");
    ESP_RETURN_ON_FALSE((uart_config), ESP_FAIL, UART_TAG, "param null");
    ESP_RETURN_ON_FALSE((uart_config->rx_flow_ctrl_thresh < SOC_UART_FIFO_LEN), ESP_FAIL, UART_TAG, "rx flow thresh error");
    ESP_RETURN_ON_FALSE((uart_config->flow_ctrl < UART_HW_FLOWCTRL_MAX), ESP_FAIL, UART_TAG, "hw_flowctrl mode error");
    ESP_RETURN_ON_FALSE((uart_config->data_bits < UART_DATA_BITS_MAX), ESP_FAIL, UART_TAG, "data bit error");
    uart_module_enable(uart_num);
#if SOC_UART_SUPPORT_RTC_CLK
    if (uart_config->source_clk == UART_SCLK_RTC) {
        rtc_clk_enable(uart_num);
    }
#endif
    UART_ENTER_CRITICAL(&(uart_context[uart_num].spinlock));
    uart_hal_init(&(uart_context[uart_num].hal), uart_num);
    uart_hal_set_sclk(&(uart_context[uart_num].hal), uart_config->source_clk);
    uart_hal_set_baudrate(&(uart_context[uart_num].hal), uart_config->baud_rate);
    uart_hal_set_parity(&(uart_context[uart_num].hal), uart_config->parity);
    uart_hal_set_data_bit_num(&(uart_context[uart_num].hal), uart_config->data_bits);
    uart_hal_set_stop_bits(&(uart_context[uart_num].hal), uart_config->stop_bits);
    uart_hal_set_tx_idle_num(&(uart_context[uart_num].hal), UART_TX_IDLE_NUM_DEFAULT);
    uart_hal_set_hw_flow_ctrl(&(uart_context[uart_num].hal), uart_config->flow_ctrl, uart_config->rx_flow_ctrl_thresh);
    UART_EXIT_CRITICAL(&(uart_context[uart_num].spinlock));
    uart_hal_rxfifo_rst(&(uart_context[uart_num].hal));
    uart_hal_txfifo_rst(&(uart_context[uart_num].hal));
    return ESP_OK;
}

 

uart_param_config() 함수는 

첫번째 인자에 들어가는 uart_num은 3보다 작아야한다.

드라이버와 같이 ESP_RETURN_ON_FALSE 함수를 통해 받아온 uart_config의 멤버들이 적절한지 확인하는 과정도 있다.

uart_config_t 라는 구조체를 통해 받아온 설정을 hal계층에서 레지스터에 비트를 설정한다.

 

 

 

여기서 나오는 hal과 드라이버에 대해서 찾아보았다.

둘 다 하드웨어와 상호작용하는 것인데 무엇이 크게 다른지 잘 모르겠다.

차차 알아가야겠다...

 

//internal signal can be output to multiple GPIO pads
//only one GPIO pad can connect with input signal
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num)
{
    ESP_RETURN_ON_FALSE((uart_num >= 0), ESP_FAIL, UART_TAG, "uart_num error");
    ESP_RETURN_ON_FALSE((uart_num < UART_NUM_MAX), ESP_FAIL, UART_TAG, "uart_num error");
    ESP_RETURN_ON_FALSE((tx_io_num < 0 || (GPIO_IS_VALID_OUTPUT_GPIO(tx_io_num))), ESP_FAIL, UART_TAG, "tx_io_num error");
    ESP_RETURN_ON_FALSE((rx_io_num < 0 || (GPIO_IS_VALID_GPIO(rx_io_num))), ESP_FAIL, UART_TAG, "rx_io_num error");
    ESP_RETURN_ON_FALSE((rts_io_num < 0 || (GPIO_IS_VALID_OUTPUT_GPIO(rts_io_num))), ESP_FAIL, UART_TAG, "rts_io_num error");
    ESP_RETURN_ON_FALSE((cts_io_num < 0 || (GPIO_IS_VALID_GPIO(cts_io_num))), ESP_FAIL, UART_TAG, "cts_io_num error");

    /* In the following statements, if the io_num is negative, no need to configure anything. */
    if (tx_io_num >= 0 && !uart_try_set_iomux_pin(uart_num, tx_io_num, SOC_UART_TX_PIN_IDX)) {
        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[tx_io_num], PIN_FUNC_GPIO);
        gpio_set_level(tx_io_num, 1);
        esp_rom_gpio_connect_out_signal(tx_io_num, UART_PERIPH_SIGNAL(uart_num, SOC_UART_TX_PIN_IDX), 0, 0);
    }

    if (rx_io_num >= 0 && !uart_try_set_iomux_pin(uart_num, rx_io_num, SOC_UART_RX_PIN_IDX)) {
        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[rx_io_num], PIN_FUNC_GPIO);
        gpio_set_pull_mode(rx_io_num, GPIO_PULLUP_ONLY);
        gpio_set_direction(rx_io_num, GPIO_MODE_INPUT);
        esp_rom_gpio_connect_in_signal(rx_io_num, UART_PERIPH_SIGNAL(uart_num, SOC_UART_RX_PIN_IDX), 0);
    }

    if (rts_io_num >= 0 && !uart_try_set_iomux_pin(uart_num, rts_io_num, SOC_UART_RTS_PIN_IDX)) {
        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[rts_io_num], PIN_FUNC_GPIO);
        gpio_set_direction(rts_io_num, GPIO_MODE_OUTPUT);
        esp_rom_gpio_connect_out_signal(rts_io_num, UART_PERIPH_SIGNAL(uart_num, SOC_UART_RTS_PIN_IDX), 0, 0);
    }

    if (cts_io_num >= 0  && !uart_try_set_iomux_pin(uart_num, cts_io_num, SOC_UART_CTS_PIN_IDX)) {
        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[cts_io_num], PIN_FUNC_GPIO);
        gpio_set_pull_mode(cts_io_num, GPIO_PULLUP_ONLY);
        gpio_set_direction(cts_io_num, GPIO_MODE_INPUT);
        esp_rom_gpio_connect_in_signal(cts_io_num, UART_PERIPH_SIGNAL(uart_num, SOC_UART_CTS_PIN_IDX), 0);
    }

    return ESP_OK;
}

 

uart_pin_set() 함수는 

마찬가지로 uart번호, tx,rx의 핀 등을 확인한다.

uart_async_rxtxtasks_main.c에서 rts_io_num, cts_io_num는 -1로 설정되어 아래 두 if문은 실행되지 않는다.

 

일단 app_main의 xTaskCreate()로 넘어가자

xTaskCreate(rx_task, "uart_rx_task", 1024*2, NULL, configMAX_PRIORITIES, NULL);
 
이러한 함수가 있다고 하면 rx_task라는 함수를 실행하되 로그에서 구분되기 쉬운  "uart_rx_task"라는 별칭을 갖고,
1024*2바이트 크기의 스택을 갖는 함수를 동적으로 실행하라 라는뜻이다.
static void rx_task(void *arg)
{
    static const char *RX_TASK_TAG = "RX_TASK";
    esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO);
    uint8_t* data = (uint8_t*) malloc(RX_BUF_SIZE+1);
    while (1) {
        const int rxBytes = uart_read_bytes(UART_NUM_1, data, RX_BUF_SIZE, 1000 / portTICK_RATE_MS);
        if (rxBytes > 0) {
            data[rxBytes] = 0;
            ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, data);
            ESP_LOG_BUFFER_HEXDUMP(RX_TASK_TAG, data, rxBytes, ESP_LOG_INFO);
        }
    }
    free(data);
}

 

여기서 이 전 단계에서 만들었던 UART_NUM_1을 인수로 갖는 uart_read_bytes함수를 이용해 메세지를 무한정 읽다가 일정 크기의 메세지를 읽고 그것을 출력한 후에 특정 로그를 남긴다.

 

int sendData(const char* logName, const char* data)
{
    const int len = strlen(data);
    const int txBytes = uart_write_bytes(UART_NUM_1, data, len);
    ESP_LOGI(logName, "Wrote %d bytes", txBytes);
    return txBytes;
}

static void tx_task(void *arg)
{
    static const char *TX_TASK_TAG = "TX_TASK";
    esp_log_level_set(TX_TASK_TAG, ESP_LOG_INFO);
    while (1) {
        sendData(TX_TASK_TAG, "Hello world");
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}

 

이 함수에서는 rx_task의 절반의 주기로 데이터를 전송한다. 

 

결론:

이 프로그램에서 데이터를 송수신하는 방법은 어렵지 않지만 uart를 초기화 하고 설정하고 하는 등의 초기 설정이 매우 복잡한것을 알았다...

+ Recent posts