레슨5. Serial communication (UART)

모코코개발자·2025년 1월 8일
post-thumbnail

UART(Universal Asynchronous Receiver/Transmitter)는 널리 사용되는 시리얼 통신 프로토콜입니다.

UART(Universal Asynchronous Receiver/Transmitter)는 피어 투 피어(Peer-to-Peer, P2P) 하드웨어 통신 프로토콜입니다.

이번 레슨에서는 UART 드라이버를 인터럽트 기반 방식으로 사용하는 방법을 배웁니다.

이 레슨의 실습 섹션에서는 UART를 통해 명령어를 전송하여 보드의 LED를 제어합니다.

목표

  • 비동기 모드(Interrupt-driven)로 UART를 통해 데이터를 송수신하는 방법을 학습합니다.

  • UART API를 사용하여 UART 주변 장치를 설정하는 방법을 학습합니다.

  • UART 드라이버 API를 사용하여 UART 기능을 실습하고 검토합니다.

용어 정리

TX (Transmit) : 데이터를 전송하는 핀
RX (Receive) : 데이터를 수신하는 핀

RTS (Request to Send) : 데이터를 송신할 준비가 되었음을 나타냄
CTS (Clear to Send) : 데이터를 전송해도 되는지 확인

개념 정리

UART는 간단하고 널리 사용되는 직렬 통신 프로토콜로, 다음과 같은 주요 특징을 가집니다:

시작/데이터/종료 비트를 통해 데이터를 직렬로 전송.
TX와 RX 핀의 크로스 커플링으로 데이터 송수신.
선택적으로 패리티 비트와 하드웨어 흐름 제어 사용 가능.

Zephyr에서 UART 활용

Zephyr에서 UART 주변 장치를 사용하는 방법은 세 가지가 있습니다:

  1. Polling
  • 가장 기본적인 방식
  • uart_poll_in() (읽기)는 비차단(non-blocking) 함수로, 데이터가 없으면 -1을 반환
  • uart_poll_out() (쓰기)는 차단(blocking) 함수로, 데이터가 전송될 때까지 대기
  1. Interrupt-driven API
  • UART 드라이버 ISR이 데이터를 관리하며, 애플리케이션은 다른 작업을 계속 수행 가능.
  • 커널의 데이터 전달 기능(ex FIFO)을 사용해 UART 드라이버와 애플리케이션 간 통신
  1. Asynchronous API
  • 가장 효율적인 방식
  • EasyDMA를 사용하여 데이터를 백그라운드에서 읽고 쓸 수 있음
  • 수신 시간 초과 설정 및 데이터 수신 양에 따라 인터럽트 트리거 가능
  • 대부분의 사용 사례를 지원

UART 드라이버 활성화

드라이버 활성화

prf.conf 파일에 다음을 추가

CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y (비동기 API 사용을 위해 반드시 필요)

헤더 파일 포함

UART 드라이버를 사용하기 위해 다음 헤더 파일을 포함

#include <zephyr/drivers/uart.h>

UART 디바이스 가져오기

DEVICE_DT_GET() 매크로를 사용하여 UART 디바이스 구조체를 가져옴

const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart0));
if (!device_is_ready(uart)) {
    return;
}

UART 설정

설정 변경

UART 설정(보드레이트, 패리티 비트 등)은 정적(빌드 타임) 또는 동적(런타임)으로 변경 가능.

동적 설정을 위해 uart_config 구조체를 생성

const struct uart_config uart_cfg = {
    .baudrate = 115200,
    .parity = UART_CFG_PARITY_NONE,
    .stop_bits = UART_CFG_STOP_BITS_1,
    .data_bits = UART_CFG_DATA_BITS_8,
    .flow_ctrl = UART_CFG_FLOW_CTRL_NONE
};

설정을 적용하려면 uart_configure() 함수 호출

int err = uart_configure(uart, &uart_cfg);
if (err == -ENOSYS) {
    return -ENOSYS;
}

콜백 함수 정의

UART 이벤트를 처리하기 위한 콜백 함수를 정의:

static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data) {
    switch (evt->type) {
    case UART_TX_DONE:
        // 데이터 전송 완료 처리
        break;
    case UART_RX_RDY:
        // 데이터 수신 처리
        break;
    default:
        break;
    }
}

콜백 함수 등록

uart_callback_set() 함수를 사용해 콜백 함수 등록

int err = uart_callback_set(uart, uart_cb, NULL);
if (err) {
    return err;
}

데이터 수신

수신 버퍼 선언

UART로 들어오는 데이터를 저장할 버퍼 선언

static uint8_t rx_buf[10] = {0}; // 10바이트 크기의 수신 버퍼

데이터 수신 시작

uart_rx_enable()를 호출해 수신 시작

uart_rx_enable(uart, rx_buf, sizeof(rx_buf), 100);

데이터 접근

콜백 함수에서 UART_RX_RDY 이벤트를 통해 데이터 접근

evt->data.rx.len        // 수신된 데이터 길이
evt->data.rx.offset     // 버퍼 내 데이터가 저장된 위치
evt->rx.buf[offset]     // 수신된 실제 데이터

연속 수신

수신 버퍼가 가득 차면 기본적으로 수신이 멈춤
UART_RX_DISABLED 이벤트에서 uart_rx_enable()을 다시 호출해 연속 수신 활성화

case UART_RX_DISABLED:
    uart_rx_enable(dev, rx_buf, sizeof(rx_buf), 100);
    break;

데이터 전송

전송 버퍼 선언

전송할 데이터를 저장할 버퍼 선언

static uint8_t tx_buf[] = {"nRF Connect SDK Fundamentals Course \n\r"};

데이터 전송

uart_tx() 함수 호출

int err = uart_tx(uart, tx_buf, sizeof(tx_buf), SYS_FOREVER_US);
if (err) {
    return err;
}

전송 완료 이벤트 처리

UART_TX_DONE 이벤트를 통해 전송 완료 후 작업 수행

case UART_TX_DONE:
    // 필요한 작업 수행
    break;

전체 코드

prj.conf

#
# Copyright (c) 2016 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#

CONFIG_GPIO=y
# STEP 2 - Enable the serial driver in asynchronous mode
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y

CMake

#
# Copyright (c) 2016 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrf_connect_sdk_fundamentals)

target_sources(app PRIVATE src/main.c)

main.c

/*
 * Intel Corporation의 저작권(c) 2016
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/* UART를 통해 LED 제어. 키보드에서 1-3을 누르면 개발 키트의 LED 1-3이 전환됩니다. */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>
/* STEP 3 - main.c에 UART 드라이버 헤더 파일 포함 */
#include <zephyr/drivers/uart.h>

/* 1000밀리초 = 1초 */
#define SLEEP_TIME_MS 1000

/* STEP 10.1.1 - 수신 버퍼 크기 정의 */
#define RECEIVE_BUFF_SIZE 10

/* STEP 10.2 - 수신 타임아웃 기간 정의 */
#define RECEIVE_TIMEOUT 100

/* STEP 5.1 - gpio_dt_spec를 사용하여 LED의 디바이스 포인터 가져오기 */
/* nRF7002dk에는 LED가 2개만 있으므로, 사용하는 DK에 따라 컴파일 시간 조건을 반영합니다. */
#if defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP) || defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP_NS)
static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
#else
static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
static const struct gpio_dt_spec led2 = GPIO_DT_SPEC_GET(DT_ALIAS(led2), gpios);
#endif

/* STEP 4.1 - UART 하드웨어의 디바이스 포인터 가져오기 */
#if defined(CONFIG_BOARD_NRF54L15DK_NRF54L15_CPUAPP) || defined(CONFIG_BOARD_NRF54L15DK_NRF54L15_CPUAPP_NS)
const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart20));
#else
const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart0));
#endif

/* STEP 9.1 - 전송 버퍼 정의, UART를 통해 전송될 데이터를 저장할 버퍼 */
static uint8_t tx_buf[] = {
#if defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP) || defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP_NS)
	"nRF Connect SDK 기본 과정\r\n"
	"개발 키트의 LED 1-2를 전환하려면 키보드에서 1-2를 누르세요.\r\n"};
#elif defined(CONFIG_BOARD_NRF54L15DK_NRF54L15_CPUAPP) || defined(CONFIG_BOARD_NRF54L15DK_NRF54L15_CPUAPP_NS)
	"nRF Connect SDK 기본 과정\r\n"
	"개발 키트의 LED 0-2를 전환하려면 키보드에서 0-2를 누르세요.\r\n"};
#else
	"nRF Connect SDK 기본 과정\r\n"
	"개발 키트의 LED 1-3을 전환하려면 키보드에서 1-3을 누르세요.\r\n"};
#endif

/* STEP 10.1.2 - 수신 버퍼 정의 */
static uint8_t rx_buf[RECEIVE_BUFF_SIZE] = {0};

/* STEP 7 - UART 콜백 함수 정의 */
static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
	switch (evt->type) {

	case UART_RX_RDY:
    #if defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP) || defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP_NS)
		if ((evt->data.rx.len) == 1) {

			if (evt->data.rx.buf[evt->data.rx.offset] == '1') {
				gpio_pin_toggle_dt(&led0);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '2') {
				gpio_pin_toggle_dt(&led1);
			}
		}
    #elif defined(CONFIG_BOARD_NRF54L15DK_NRF54L15_CPUAPP) || defined(CONFIG_BOARD_NRF54L15DK_NRF54L15_CPUAPP_NS)
		if ((evt->data.rx.len) == 1) {
			if (evt->data.rx.buf[evt->data.rx.offset] == '0') {
				gpio_pin_toggle_dt(&led0);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '1') {
				gpio_pin_toggle_dt(&led1);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '2') {
				gpio_pin_toggle_dt(&led2);
			}
        }
    #else
		if ((evt->data.rx.len) == 1) {

			if (evt->data.rx.buf[evt->data.rx.offset] == '1') {
				gpio_pin_toggle_dt(&led0);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '2') {
				gpio_pin_toggle_dt(&led1);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '3') {
				gpio_pin_toggle_dt(&led2);
			}
		}
#endif
		break;
	case UART_RX_DISABLED:
		uart_rx_enable(dev, rx_buf, sizeof rx_buf, RECEIVE_TIMEOUT);
		break;

	default:
		break;
	}
}

int main(void)
{
	int ret;

	/* STEP 4.2 - UART 디바이스가 준비되었는지 확인 */
	if (!device_is_ready(uart)) {
		printk("UART 디바이스가 준비되지 않았습니다.\n");
		return 1;
	}
	/* STEP 5.2 - LED 디바이스가 준비되었는지 확인 */
	if (!device_is_ready(led0.port)) {
		printk("GPIO 디바이스가 준비되지 않았습니다.\n");
		return 1;
	}
/* STEP 6 - LED GPIO 구성 */
#if defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP) ||                                              \
	defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP_NS)
	ret = gpio_pin_configure_dt(&led0, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1;
	}
	ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1;
	}
#else
	ret = gpio_pin_configure_dt(&led0, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1;
	}
	ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1;
	}
	ret = gpio_pin_configure_dt(&led2, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1;
	}
#endif

	/* STEP 8 - UART 콜백 함수 등록 */
	ret = uart_callback_set(uart, uart_cb, NULL);
	if (ret) {
		return 1;
	}
	/* STEP 9.2 - uart_tx()를 호출하여 UART를 통해 데이터 전송 */
	ret = uart_tx(uart, tx_buf, sizeof(tx_buf), SYS_FOREVER_MS);
	if (ret) {
		return 1;
	}
	/* STEP 10.3  - uart_rx_enable()을 호출하여 데이터 수신 시작 */
	ret = uart_rx_enable(uart, rx_buf, sizeof rx_buf, RECEIVE_TIMEOUT);
	if (ret) {
		return 1;
	}
	while (1) {
		k_msleep(SLEEP_TIME_MS);
	}
}
profile
모코코개발자

0개의 댓글