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 등록] 흐름을 충실히 따릅니다.
메인라인 드라이버 코드를 커스텀 드라이버 개발의 스타터 킷으로 참고하면 표준에 맞는 안정적인 드라이버를 신속하게 구현할 수 있습니다.
부팅 시에만 작동하지 않고 **부팅 후에 꼽으면 정상 작동**하는 이유는 **부팅 초기(초기화 단계)에는 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
“`
이렇게 설정하신 후 재부팅을 해보시면, 부팅 직후 자동으로 동글 모드가 전환되어 정상 작동할 것입니다.
# Realtek USB Wi-Fi Disk Mode Switch
ATTRS{idVendor}==”0bda”, ATTRS{idProduct}==”1a2b”, RUN+=”/usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b”