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 등록] 흐름을 충실히 따릅니다.

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

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

  1. skyer9 Post author

    부팅 시에만 작동하지 않고 **부팅 후에 꼽으면 정상 작동**하는 이유는 **부팅 초기(초기화 단계)에는 udev 규칙이 실행되는 시점과 USB 동글의 드라이버가 로드되는 시점이 엇갈리기 때문**입니다.
    특히 시스템이 켜질 때 동글이 이미 꽂혀 있으면, 커널이 동글을 드라이버(DISK)로 인식한 뒤 usb_modeswitch가 개입하기 전에 이미 마운트되거나 충돌이 나서 모드 전환이 씹히는 경우가 많습니다.
    이를 해결하기 위한 2가지 방법을 안내해 드립니다.
    ## 방법 1: usb_modeswitch 자체 설정 파일 수정 (가장 깔끔한 방법)
    udev 규칙을 거치지 않고, 시스템에 내장된 usb_modeswitch 데몬이 부팅 시 자동으로 해당 동글의 모드를 강제로 전환하도록 설정하는 방법입니다.
    1. 동글의 기본 정보(Vendor ID, Product ID)로 modeswitch 설정을 추가하거나 기존 설정을 확인합니다.
    2. 터미널에 아래 명령어를 입력하여 새로운 규칙 파일을 하나 더 만들어 줍니다.
    “`bash
    sudo nano /etc/usb_modeswitch.d/0bda:1a2b

    “`
    3. 파일 안에 아래 내용을 그대로 붙여넣고 저장합니다 (Ctrl+O -> Enter -> Ctrl+X):
    “`text
    # Realtek 0bda:1a2b ZeroCD Switch
    TargetVendor=0bda
    TargetProductList=”c811,c820,c821″
    MessageContent=”5553424312345678000000000000061b000000020000000000000000000000″

    “`
    *(※ MessageContent는 일반적인 Realtek 제로CD 해제 명령 패킷입니다.)*
    ## 방법 2: 부팅 완료 후 스크립트 실행 (가장 확실하고 쉬운 우회법)
    부팅 시에는 인식이 안 되므로, **부팅이 완전히 끝난 직후(로그인 직전/직후)에 자동으로 위 명령어를 한 번 더 실행**해 주는 방식입니다.
    1. rc.local 또는 systemd 서비스를 이용할 수 있지만, 가장 간편하게 사용자 프로필(~/.profile 또는 /etc/rc.local)에 등록할 수 있습니다.
    2. /etc/rc.local 파일을 열어봅니다 (없으면 생성):
    “`bash
    sudo nano /etc/rc.local

    “`
    3. 파일 상단에 #!/bin/sh가 있는지 확인하고, exit 0 바로 위에 아래 명령어를 추가합니다.
    “`bash
    sleep 3
    /usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b
    exit 0

    “`
    *(부팅 직후 바로 실행되면 장치가 준비되지 않았을 수 있으므로 sleep 3으로 3초 정도 대기 후 모드 전환을 강제 실행하는 원리입니다.)*
    4. rc.local 파일에 실행 권한을 줍니다:
    “`bash
    sudo chmod +x /etc/rc.local

    “`
    이렇게 설정하신 후 재부팅을 해보시면, 부팅 직후 자동으로 동글 모드가 전환되어 정상 작동할 것입니다.

답글 남기기