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_if 및 ethernet_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, \
ð_enc28j60_runtime_##inst, \
ð_enc28j60_config_##inst, \
CONFIG_ETH_INIT_PRIORITY, \
ð_enc28j60_api, \
NET_ETH_MTU);
/* DTS에 정의된 모든 active node에 대해 위 매크로 실행 */
DT_INST_FOREACH_STATUS_OKAY(ETH_ENC28J60_INIT)
5. 핵심 체크포인트 & Best Practices
ETH_NET_DEVICE_DT_INST_DEFINE매크로 활용
일반 디바이스 드라이버는DEVICE_DT_INST_DEFINE을 사용하지만, 이더넷 디바이스는 L2 인터페이스 수록 및net_if데이터 바인딩을 자동화해 주는 네트워크 전용 초기화 매크로인ETH_NET_DEVICE_DT_INST_DEFINE을 사용해야 합니다.- ISR 최소화 & 스레드 활용
SPI/I2C 버스를 사용하는 외부 네트워크 컨트롤러는 통신 시 블로킹(Blocking) 동작이 포함되므로 인터럽트 핸들러(ISR) 내부에서 직렬 통신을 수행해서는 안 됩니다. 반드시 세마포어나 워크큐(k_work)를 통해 스레드로 넘겨 처리해야 합니다. - Memory Alignment & Ring Buffer
고속 네트워크 통신의 경우, DMA 접근을 위해 송수신 버퍼의 메모리 정렬(Memory Alignment) 규칙을 반드시 준수해야 성능 병목이 발생하지 않습니다.
💡 요약
Zephyr의 네트워크 드라이버 작성 과정은 [DTS 정의 → Kconfig 스위치 구성 → ethernet_api 구현 및 ETH_NET_DEVICE_DT_INST_DEFINE 등록] 흐름을 충실히 따릅니다.
메인라인 드라이버 코드를 커스텀 드라이버 개발의 스타터 킷으로 참고하면 표준에 맞는 안정적인 드라이버를 신속하게 구현할 수 있습니다.