Zephyr RTOS – 유선 인터넷(Ethernet) 디바이스 드라이버 작성 가이드

By | 2026년 7월 22일
Table of Contents

Zephyr RTOS – 유선 인터넷(Ethernet) 디바이스 드라이버 작성 가이드

Zephyr RTOS는 임베디드 시스템의 다양한 하드웨어를 추상화하기 위해 강력한 드라이버 모델(Driver Model)을 제공합니다.

본 글에서는 Zephyr 메인라인에 수록된 유선 인터넷 컨트롤러 드라이버 Microchip ENC28J60(drivers/ethernet/eth_enc28j60.c) 사례를 바탕으로, 새로운 네트워크 드라이버를 직접 설계하고 구현하는 전체 프로세스를 단계별로 설명합니다.


1. Zephyr 드라이버 아키텍처 개요

Zephyr 드라이버는 크게 3가지 파일로 구성됩니다.

┌────────────────────────┐
│   Device Tree Binding  │ (.yaml)  : 하드웨어 속성(핀, 클록, IRQ 등) 정의
└───────────┬────────────┘
            ▼
┌────────────────────────┐
│     Kconfig System     │ (Kconfig): 빌드 옵션 및 기능 Enable/Disable 제어
└───────────┬────────────┘
            ▼
┌────────────────────────┐
│  Driver Implementation │ (.c)     : 실제 C 언어 기반 드라이버 로직 구현
└────────────────────────┘

네트워크 드라이버의 경우, Zephyr의 L2 Network Interface (Network Stack)와 연결되어 상위 IP 스택(L3)과의 데이터 송수신을 담당합니다.


2. 1단계: Device Tree Binding 작성

하드웨어 연결 정보(SPI 버스, CS 핀, INT 핀 등)를 상형문자처럼 정의하기 위해 DT Binding YAML 파일을 작성합니다.

  • 경로 예시: dts/bindings/net/microchip,enc28j60.yaml
# microchip,enc28j60.yaml
description: Microchip ENC28J60 Standalone Ethernet Controller with SPI Interface

compatible: "microchip,enc28j60"

include: [ethernet-controller.yaml, spi-device.yaml]

properties:
  int-gpios:
    type: phandle-array
    required: true
    description: Interrupt pin specifier

  local-mac-address:
    type: uint8-array
    description: Default MAC address setting

Tip: compatible 문자열은 DTS 파일에서 드라이버 매칭을 위해 사용하는 고유 키 값입니다.


3. 2단계: Kconfig 구성

드라이버를 빌드 시스템에 포함시키고, 스레드 우선순위 및 버퍼 크기 등을 제어하기 위한 Kconfig 옵션을 정의합니다.

  • 경로 예시: drivers/ethernet/Kconfig.enc28j60
config ETH_ENC28J60
    bool "Microchip ENC28J60 Ethernet controller driver"
    default y
    depends on DT_HAS_MICROCHIP_ENC28J60_ENABLED
    select SPI
    help
      Enable Microchip ENC28J60 Ethernet driver.

if ETH_ENC28J60

config ETH_ENC28J60_RX_THREAD_STACK_SIZE
    int "RX thread stack size"
    default 1024
    help
      Stack size for the Ethernet interrupt deferred processing thread.

config ETH_ENC28J60_RX_THREAD_PRIO
    int "RX thread priority"
    default 2
    help
      Priority for the Ethernet RX thread.

endif # ETH_ENC28J60

4. 3단계: 드라이버 소스 코드 구현

실제 드라이버 동작 로직을 작성하는 단계입니다. Ethernet 드라이버는 Zephyr의 net_ifethernet_api 인터페이스를 반드시 구현해야 합니다.

  • 경로 예시: drivers/ethernet/eth_enc28j60.c

① 설정 및 런타임 데이터 구조체 정의

드라이버의 ROM(Config) 영역과 RAM(Context Data) 영역을 구분하여 정의합니다.

#define DT_DRV_COMPAT microchip_enc28j60

#include <zephyr/kernel.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/ethernet.h>

/* ROM 영역: DTS로부터 읽어온 불변 정보 */
struct eth_enc28j60_config {
    struct spi_dt_spec spi;
    struct gpio_dt_spec interrupt;
};

/* RAM 영역: 런타임 상태 및 버퍼 */
struct eth_enc28j60_runtime {
    struct net_if *iface;
    uint8_t mac_address[6];
    struct k_thread thread;
    K_KERNEL_STACK_MEMBER(thread_stack, CONFIG_ETH_ENC28J60_RX_THREAD_STACK_SIZE);
    struct k_sem lock;
    struct gpio_callback gpio_cb;
};

② Ethernet API 함수 작성

상위 네트워킹 스택에서 패킷을 보낼 때 호출할 함수(send)와 초기화 함수(iface_api)를 등록합니다.

/* 데이터 패킷 전송 로직 */
static int eth_enc28j60_send(const struct device *dev, struct net_pkt *pkt)
{
    struct eth_enc28j60_runtime *context = dev->data;

    k_sem_take(&context->lock, K_FOREVER);

    /* 1. net_pkt에서 이더넷 프레임 데이터 추출 */
    /* 2. SPI 통신을 이용해 하드웨어 TX 버퍼로 패킷 전송 명령 */
    /* 3. 하드웨어 전송 시작 비트 활성화 */

    k_sem_give(&context->lock);

    return 0;
}

/* 네트워크 인터페이스 초기화 콜백 */
static void eth_enc28j60_iface_init(struct net_if *iface)
{
    const struct device *dev = net_if_get_device(iface);
    struct eth_enc28j60_runtime *context = dev->data;

    /* Interface 초기화 및 MAC 주소 설정 */
    net_if_set_link_addr(iface, context->mac_address, sizeof(context->mac_address),
                 NET_LINK_ETHERNET);

    context->iface = iface;
    ethernet_init(iface);
}

/* Ethernet API 구조체 바인딩 */
static const struct ethernet_api eth_enc28j60_api = {
    .iface_api.init = eth_enc28j60_iface_init,
    .send = eth_enc28j60_send,
};

③ 인터럽트 및 지연 처리 스레드 (ISR & Deferred Thread)

네트워크 패킷 수신은 시간 민감도가 높지만, ISR 내에서 직접 SPI 통신을 수행하면 blocking이 발생하므로 지연 처리 스레드(Deferred Work/Thread)로 처리하는 것이 Zephyr의 권장 패턴입니다.

static void eth_enc28j60_rx_thread(void *arg1, void *arg2, void *arg3)
{
    const struct device *dev = arg1;
    struct eth_enc28j60_runtime *context = dev->data;

    while (1) {
        /* ISR 신호를 대기 */
        k_sem_take(&context->lock, K_FOREVER);

        /* SPI를 통해 하드웨어 수신 버퍼 읽기 */
        /* net_pkt_rx_alloc()을 통해 패킷 할당 후 net_recv_data()로 상위 스택 전달 */
    }
}

/* GPIO 인터럽트 콜백 (ISR 맥락) */
static void eth_enc28j60_gpio_callback(const struct device *port,
                       struct gpio_callback *cb,
                       uint32_t pins)
{
    struct eth_enc28j60_runtime *context =
        CONTAINER_OF(cb, struct eth_enc28j60_runtime, gpio_cb);

    /* 지연 스레드를 깨우기 위한 세마포어 신호 */
    k_sem_give(&context->lock);
}

④ 디바이스 인스턴스화 매크로 (Multi-Instance support)

Zephyr의 매크로 시스템을 활용하여 보드 파일에 정의된 여러 개의 인스턴스를 자동으로 생성합니다.

static int eth_enc28j60_init(const struct device *dev)
{
    const struct eth_enc28j60_config *config = dev->config;
    struct eth_enc28j60_runtime *context = dev->data;

    /* 1. SPI 버스 상태 확인 */
    if (!spi_is_ready_dt(&config->spi)) {
        return -ENODEV;
    }

    /* 2. GPIO 인터럽트 핀 설정 */
    gpio_pin_configure_dt(&config->interrupt, GPIO_INPUT);
    gpio_init_callback(&context->gpio_cb, eth_enc28j60_gpio_callback, BIT(config->interrupt.pin));
    gpio_add_callback(config->interrupt.port, &context->gpio_cb);

    /* 3. RX 처리 전용 스레드 생성 */
    k_thread_create(&context->thread, context->thread_stack,
            CONFIG_ETH_ENC28J60_RX_THREAD_STACK_SIZE,
            eth_enc28j60_rx_thread, (void *)dev, NULL, NULL,
            K_PRIO_COOP(CONFIG_ETH_ENC28J60_RX_THREAD_PRIO),
            0, K_NO_WAIT);

    return 0;
}

/* 매크로를 이용한 인스턴스 자동 등록 */
#define ETH_ENC28J60_INIT(inst)                                                \
    static const struct eth_enc28j60_config eth_enc28j60_config_##inst = { \
        .spi = SPI_DT_SPEC_INST_GET(inst, SPI_WORD_SET(8), 0),         \
        .interrupt = GPIO_DT_SPEC_INST_GET(inst, int_gpios),          \
    };                                                                     \
                                                                               \
    static struct eth_enc28j60_runtime eth_enc28j60_runtime_##inst;        \
                                                                               \
    ETH_NET_DEVICE_DT_INST_DEFINE(inst,                                    \
                      eth_enc28j60_init,                       \
                      NULL,                                    \
                      &eth_enc28j60_runtime_##inst,            \
                      &eth_enc28j60_config_##inst,             \
                      CONFIG_ETH_INIT_PRIORITY,                \
                      &eth_enc28j60_api,                       \
                      NET_ETH_MTU);

/* DTS에 정의된 모든 active node에 대해 위 매크로 실행 */
DT_INST_FOREACH_STATUS_OKAY(ETH_ENC28J60_INIT)

5. 핵심 체크포인트 & Best Practices

  1. ETH_NET_DEVICE_DT_INST_DEFINE 매크로 활용
    일반 디바이스 드라이버는 DEVICE_DT_INST_DEFINE을 사용하지만, 이더넷 디바이스는 L2 인터페이스 수록 및 net_if 데이터 바인딩을 자동화해 주는 네트워크 전용 초기화 매크로인 ETH_NET_DEVICE_DT_INST_DEFINE을 사용해야 합니다.
  2. ISR 최소화 & 스레드 활용
    SPI/I2C 버스를 사용하는 외부 네트워크 컨트롤러는 통신 시 블로킹(Blocking) 동작이 포함되므로 인터럽트 핸들러(ISR) 내부에서 직렬 통신을 수행해서는 안 됩니다. 반드시 세마포어나 워크큐(k_work)를 통해 스레드로 넘겨 처리해야 합니다.
  3. Memory Alignment & Ring Buffer
    고속 네트워크 통신의 경우, DMA 접근을 위해 송수신 버퍼의 메모리 정렬(Memory Alignment) 규칙을 반드시 준수해야 성능 병목이 발생하지 않습니다.

💡 요약

Zephyr의 네트워크 드라이버 작성 과정은 [DTS 정의 → Kconfig 스위치 구성 → ethernet_api 구현 및 ETH_NET_DEVICE_DT_INST_DEFINE 등록] 흐름을 충실히 따릅니다.

메인라인 드라이버 코드를 커스텀 드라이버 개발의 스타터 킷으로 참고하면 표준에 맞는 안정적인 드라이버를 신속하게 구현할 수 있습니다.

답글 남기기