U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
#한 줄 요약
**“Driver Model은 uclass(인터페이스)·driver(구현)·udevice(인스턴스)의 삼각 구조입니다.” — DT의
compatible프로퍼티가 driver와 device를 자동 매칭합니다. 보드 코드는 DT만 잘 쓰면 대부분의 init이 끝납니다.
2014년 이전의 U-Boot은 각 driver가 고유 API를 가졌습니다. MMC는 mmc_*, GPIO는 gpio_*, I2C는 i2c_*. 공통점이 없었고 모든 driver init이 보드 코드의 work였습니다. Linux의 device model을 본떠 *Driver Model(DM)*이 도입되었고, 지금은 거의 모든 driver가 DM 위에서 동작합니다.
#왜 Driver Model인가
legacy 시절의 U-Boot 보드 코드는 모든 device를 명시적으로 init했습니다.
/* legacy 방식 (구식) */
int board_mmc_init(struct bd_info *bis){ /* MMC controller 1번 init */ init_clk_usdhc(0); fsl_esdhc_initialize(bis, &usdhc_cfg[0]);
/* MMC controller 2번 init */ init_clk_usdhc(1); fsl_esdhc_initialize(bis, &usdhc_cfg[1]);
return 0;}
int board_eth_init(struct bd_info *bis){ /* ENET init */ setup_fec(); return cpu_eth_init(bis);}
int board_i2c_init(void){ /* I2C 1번 init */ i2c_init(I2C_SPEED, 0); /* I2C 2번 init */ i2c_init(I2C_SPEED, 0); return 0;}보드 추가 시 모든 init 코드를 수동으로 짜야 했습니다. 같은 SoC라도 어떤 페리페럴을 쓰는지에 따라 코드가 다 달랐습니다.
DM 도입 후에는 DT에 device를 정의하기만 하면 됩니다.
/* DT */&usdhc1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_usdhc1>; bus-width = <8>; status = "okay";};U-Boot이 부트 시점에 DT를 스캔해서 해당 driver를 찾아 probe합니다. 보드 코드는 touch 안 합니다.
#삼각 구조 — uclass / driver / udevice
#uclass
uclass는 기능 카테고리입니다. “MMC controller”라는 추상 인터페이스가 UCLASS_MMC입니다. uclass는 operation 시그니처를 정의합니다.
enum uclass_id { UCLASS_INVALID = 0, UCLASS_ROOT, UCLASS_CLK, UCLASS_CPU, UCLASS_GPIO, UCLASS_I2C, UCLASS_MMC, UCLASS_PINCTRL, UCLASS_PMIC, UCLASS_REGULATOR, UCLASS_SERIAL, UCLASS_ETH, UCLASS_USB, UCLASS_BLK, UCLASS_RTC, UCLASS_SPI, ...};각 uclass는 operation struct를 가집니다. MMC의 경우:
struct dm_mmc_ops { int (*send_cmd)(struct udevice *dev, struct mmc_cmd *cmd, struct mmc_data *data); int (*set_ios)(struct udevice *dev); int (*get_cd)(struct udevice *dev); int (*get_wp)(struct udevice *dev); ...};driver들이 이 시그니처를 구현합니다.
#driver
driver는 특정 hardware의 구체 코드입니다. U_BOOT_DRIVER 매크로로 정의합니다.
/* drivers/mmc/fsl_esdhc_imx.c (간략화) */
static int fsl_esdhc_probe(struct udevice *dev){ /* clock enable, pinmux 설정, etc */ return 0;}
static const struct dm_mmc_ops fsl_esdhc_ops = { .send_cmd = fsl_esdhc_send_cmd, .set_ios = fsl_esdhc_set_ios, .get_cd = fsl_esdhc_get_cd,};
static const struct udevice_id fsl_esdhc_ids[] = { { .compatible = "fsl,imx8mp-usdhc" }, { .compatible = "fsl,imx8mm-usdhc" }, { .compatible = "fsl,imx7d-usdhc" }, { /* sentinel */ }};
U_BOOT_DRIVER(fsl_esdhc) = { .name = "fsl-esdhc-mmc", .id = UCLASS_MMC, .of_match = fsl_esdhc_ids, .ops = &fsl_esdhc_ops, .probe = fsl_esdhc_probe, .priv_auto = sizeof(struct fsl_esdhc_priv), .platdata_auto = sizeof(struct fsl_esdhc_plat), .flags = DM_FLAG_PRE_RELOC,};.id = UCLASS_MMC이 어느 uclass에 속하는지 표시합니다. .of_match = fsl_esdhc_ids가 DT의 compatible과 매칭할 문자열 배열입니다.
#udevice
udevice는 driver의 인스턴스입니다. DT에서 해당 driver의 compatible과 매치되는 노드 하나가 udevice 하나가 됩니다.
/* DT */soc { usdhc1: mmc@30b40000 { compatible = "fsl,imx8mp-usdhc"; reg = <0x30b40000 0x10000>; ... };
usdhc2: mmc@30b50000 { compatible = "fsl,imx8mp-usdhc"; reg = <0x30b50000 0x10000>; ... };};이 DT에서 udevice 두 개가 생깁니다.
udevice 1: name="mmc@30b40000", driver=fsl-esdhc-mmc, uclass=UCLASS_MMCudevice 2: name="mmc@30b50000", driver=fsl-esdhc-mmc, uclass=UCLASS_MMC같은 driver가 두 인스턴스를 가지는 것입니다.
#binding — DT가 driver를 찾는 법
U-Boot이 DT를 어떻게 driver로 변환하는지가 binding입니다.
- U-Boot이 DT를 scan.
- 각 노드의
compatible프로퍼티 확인. - 모든 driver의
of_match배열을 검색. - 매치되면 udevice 생성 + driver와 binding.
- 부모 노드 우선 (root → soc → mmc@xxx 순).
int lists_bind_fdt(struct udevice *parent, ofnode node, ...){ struct driver *entry; int ret;
for (entry = driver_list_start; entry != driver_list_end; entry++) { if (!entry->of_match) continue;
for (id = entry->of_match; id->compatible; id++) { if (ofnode_device_is_compatible(node, id->compatible)) { /* 매치! udevice 생성 */ ret = device_bind(parent, entry, ...); if (ret == 0) return 0; } } } return -ENODEV;}매치되는 driver를 찾지 못한 노드는 device가 생성되지 않습니다. 동작도 안 합니다. 이것이 가장 흔한 디버깅 포인트입니다.
#bind vs probe
DM은 bind와 probe를 분리합니다.
bind
- device 인스턴스 생성
- parent-child 관계 설정
- driver_data 포인터만 연결
- 빠름, 메모리만 차지
probe
- 실제 hardware 초기화
- clock enable, pinmux, 레지스터 설정
- 첫 사용 시점까지 lazy
probe는 해당 device가 처음 사용될 때까지 연기됩니다. 부트 시간을 줄이기 위함입니다. MMC controller가 부트에 안 쓰이면 probe 안 됩니다.
int uclass_get_device(enum uclass_id id, int index, struct udevice **devp){ struct udevice *dev = ...; /* lookup */
if (dev->flags & DM_FLAG_ACTIVATED) return 0; /* 이미 probe됨 */
return device_probe(dev); /* lazy probe */}mmc list 같은 명령이 호출되는 시점에 그제서야 MMC가 probe됩니다.
#DM_FLAG — driver 동작 제어
U_BOOT_DRIVER의 .flags 필드가 driver의 동작 방식을 조정합니다.
| flag | 의미 |
|---|---|
DM_FLAG_PRE_RELOC | relocation 전에도 사용 가능 |
DM_FLAG_ACTIVATED | probe 완료 상태 |
DM_FLAG_NAME_ALLOCED | name이 동적 할당됨 |
DM_FLAG_REMOVE_WITH_PD_ON | power domain 켜진 상태로 remove |
DM_FLAG_OS_PREPARE | OS 인계 전 호출 |
DM_FLAG_PROBE_AFTER_BIND | bind 직후 즉시 probe |
DM_FLAG_DEFAULT_PD_CTRL_OFF | power domain 자동 제어 끔 |
DM_FLAG_PRE_RELOC이 가장 중요합니다. 이 flag 없는 driver는 board_init_r 이후에만 동작합니다. UART, console driver는 반드시 PRE_RELOC.
U_BOOT_DRIVER(serial_imx) = { .name = "serial_imx", .id = UCLASS_SERIAL, .of_match = serial_imx_ids, .probe = imx_serial_probe, .flags = DM_FLAG_PRE_RELOC, /* console은 pre-reloc */};#dm tree — 런타임 검증
U-Boot 명령 인터프리터에서 dm tree가 전체 device tree를 출력합니다.
=> dm tree Class Index Probed Driver Name----------------------------------------------------------- root 0 [ + ] root_driver root_driver simple_bus 0 [ + ] generic_simple_bus |-- soc@0 clk 0 [ + ] imx8mp_clk | |-- clock-controller@30380000 pinctrl 0 [ + ] imx8mp_pinctrl | |-- iomuxc@30330000 serial 0 [ + ] serial_imx | |-- serial@30890000 mmc 0 [ ] fsl-esdhc-mmc | |-- mmc@30b40000 mmc 1 [ ] fsl-esdhc-mmc | |-- mmc@30b50000 ethernet 0 [ + ] eqos_imx | |-- ethernet@30bf0000 i2c 0 [ + ] imx-i2c | |-- i2c@30a20000 pmic 0 [ + ] pca9450 | | `-- pmic@25 regulator 0 [ + ] pca9450_regulator | | `-- regulators cpu 0 [ + ] imx8_cpu |-- cpu@0 cpu 1 [ + ] imx8_cpu |-- cpu@1 cpu 2 [ + ] imx8_cpu |-- cpu@2 cpu 3 [ + ] imx8_cpu |-- cpu@3[ + ]이 probed, [ ]이 bind만 됨, probe 전. probed가 *모두 +*가 아닐 수도 있습니다. lazy probe이기 때문입니다.
probe 실패는 DT의 incomplete 또는 parent driver bug가 원인입니다. 가장 자주 보이는 패턴:
=> dm tree mmc 0 [ ] fsl-esdhc-mmc | |-- mmc@30b40000=> mmc listCard did not respond to voltage select! : -110=> dm tree mmc 0 [ ] fsl-esdhc-mmc | |-- mmc@30b40000probe가 시도되었지만 실패했고, 다음에 다시 probe됩니다. 원인 대부분은 clock 안 잡힘 또는 regulator 안 켜짐.
#dm uclass — uclass별 목록
=> dm uclassuclass 0: root0 * root_driver @ ff8e5b80, seq -1uclass 14: clk0 clock-controller@30380000 @ ff8e6b40, seq 0...uclass 47: mmc0 * mmc@30b40000 @ ff8eac80, seq 01 * mmc@30b50000 @ ff8eba80, seq 1uclass 52: serial0 * serial@30890000 @ ff8e7c80, seq 0특정 uclass의 모든 device를 한 번에 봅니다. driver 개발 시 빠른 진단 도구입니다.
#SPL에서의 DM
SPL도 DM을 전부 또는 부분적으로 쓸 수 있습니다. CONFIG_SPL_DM=y 옵션입니다.
CONFIG_SPL_DM=yCONFIG_SPL_DM_MMC=yCONFIG_SPL_DM_GPIO=yCONFIG_SPL_DM_PMIC=yCONFIG_SPL_OF_CONTROL=yCONFIG_SPL_OF_PLATDATA=y ← 옵션: DT 대신 platdata 사용 (크기 절약)CONFIG_SPL_OF_PLATDATA는 SPL에서 DT 파싱을 건너뛰고 미리 생성된 C struct를 사용하는 옵션입니다. SPL 크기를 줄이는 고급 옵션입니다. 빌드 시 dtoc 도구가 DT를 C 코드로 변환합니다.
$ make spl/u-boot-spl DTOC C spl/dts/dt-plat.c DTOC H spl/dts/dt-structs-gen.h생성된 dt-plat.c는 다음 같이 static 데이터로 device들을 정의합니다.
/* spl/dts/dt-plat.c (생성 코드) */
struct dtd_fsl_imx8mp_usdhc dtv_mmc_30b40000 = { .reg = {0x30b40000, 0x10000}, .bus_width = 0x8, .pinctrl_0 = 0x40, ...};
U_BOOT_DRVINFO(mmc_30b40000) = { .name = "fsl_esdhc_imx", .plat = &dtv_mmc_30b40000,};DT 파싱 코드가 빠지므로 SPL 크기가 수 KB 감소합니다.
#새 driver 한 개 — 최소 예시
가상의 가속도 센서 driver를 DM 위에서 짜는 예시입니다.
#include <common.h>#include <dm.h>#include <i2c.h>#include <sensor.h>
struct acme_accel_priv { struct udevice *i2c_dev; u8 chip_addr;};
static int acme_accel_read(struct udevice *dev, int *xyz){ struct acme_accel_priv *priv = dev_get_priv(dev); u8 buf[6]; int ret;
ret = dm_i2c_read(priv->i2c_dev, 0x32, buf, 6); if (ret) return ret;
xyz[0] = (buf[1] << 8) | buf[0]; xyz[1] = (buf[3] << 8) | buf[2]; xyz[2] = (buf[5] << 8) | buf[4];
return 0;}
static int acme_accel_probe(struct udevice *dev){ struct acme_accel_priv *priv = dev_get_priv(dev);
/* parent가 i2c bus */ priv->i2c_dev = dev->parent; priv->chip_addr = dev_read_addr(dev);
/* sensor reset */ return dm_i2c_reg_write(priv->i2c_dev, 0x2D, 0x08);}
static const struct sensor_ops acme_accel_ops = { .read = acme_accel_read,};
static const struct udevice_id acme_accel_ids[] = { { .compatible = "acme,xl345" }, { /* sentinel */ }};
U_BOOT_DRIVER(acme_xl345) = { .name = "acme-xl345", .id = UCLASS_SENSOR, .of_match = acme_accel_ids, .ops = &acme_accel_ops, .probe = acme_accel_probe, .priv_auto = sizeof(struct acme_accel_priv),};DT:
&i2c1 { accelerometer@53 { compatible = "acme,xl345"; reg = <0x53>; };};빌드 후 부트:
=> dm tree i2c 0 [ + ] imx-i2c |-- i2c@30a20000 sensor 0 [ ] acme-xl345 | `-- accelerometer@53자동으로 device가 생겼습니다. 보드 코드에 한 줄도 안 썼습니다.
#자주 하는 실수
#of_match의 sentinel 누락
static const struct udevice_id ids[] = { { .compatible = "vendor,my-driver" }, /* sentinel 누락! */};배열의 끝을 표시하는 { /* sentinel */ }를 빼면 out-of-bounds로 메모리를 읽어 random match가 일어날 수 있습니다. 항상 끝에 빈 entry를 둡니다.
#DM_FLAG_PRE_RELOC 없이 board_init_f에서 사용
UART, console, GPIO는 pre-reloc 가능해야 합니다. 이 flag 없으면 board_init_f 단계에서 device가 안 보입니다.
.flags = DM_FLAG_PRE_RELOC,console에 아무것도 안 나오는데 device 자체는 제대로 정의된 경우, PRE_RELOC 누락이 자주 범인입니다.
#priv_auto 크기 mismatch
priv_auto = sizeof(struct acme_accel_priv)가 실제 priv struct 크기와 다르면 메모리 corruption. typedef alias로 큰 struct를 정의했는데 크기를 작게 잡으면 probe 후 주변 메모리가 깨집니다.
#dev->parent를 잘못 가정
I2C device의 parent는 I2C bus이지만, SPI device의 parent는 SPI bus입니다. driver별로 parent 사용 패턴이 다릅니다. uclass 문서를 확인.
#probe가 DT 노드를 다시 읽음
dev_read_* 함수로 DT 노드의 프로퍼티를 probe 시점에 읽습니다. priv_auto에 캐시해 두지 않으면 매 호출마다 DT 파싱이 일어나 느려집니다.
#dm tree에서 device가 안 보임
=> dm tree(my device not shown)원인 후보:
- DT의 compatible string 오타
- driver의 of_match 배열에 누락
- driver 자체가 빌드에 포함 안 됨 (Kconfig 누락)
- 부모 노드가 status = “disabled”
fdt print /soc/i2c@xxx/my-device로 DT를 확인하고, 빌드 산출물에 .o가 있는지 확인합니다.
#select 누락
defconfig에서 CONFIG_MY_DRIVER=y만 켜고 CONFIG_DM=y가 안 켜져 있는 경우. DM은 대부분의 Kconfig에서 default y이지만 명시적으로 확인합니다.
CONFIG_DM=yCONFIG_DM_I2C=yCONFIG_DM_SENSOR=yCONFIG_MY_DRIVER=y#정리
- Driver Model은 *uclass(인터페이스) · driver(구현) · udevice(인스턴스)*의 삼각 구조입니다.
- DT의 compatible 프로퍼티가 driver의 of_match와 매칭되어 udevice가 자동 생성됩니다.
- bind(인스턴스 생성)와 probe(실제 init)가 분리되어 있고, probe는 lazy입니다.
DM_FLAG_PRE_RELOC이 relocation 이전에도 사용 가능한 driver임을 표시합니다. UART, console에 필수.dm tree로 전체 device 트리와 probe 상태를 봅니다.[ + ]이 probed,[ ]이 bind만 됨.dm uclass로 특정 uclass의 모든 device를 봅니다.- SPL은
CONFIG_SPL_DM=y로 DM 사용. 크기 절약을 위해CONFIG_SPL_OF_PLATDATA로 DT 대신 C struct를 쓸 수 있습니다. - 새 driver는
U_BOOT_DRIVER매크로로 정의합니다. uclass id, of_match, probe, ops, priv_auto가 핵심 필드.
#다음 편
Ch 8: 보드 초기화 — board_init_f와 board_init_r에서는 U-Boot의 부트 흐름을 봅니다. relocation 전과 후의 환경 차이, init_sequence_f/init_sequence_r 배열, 보드가 hook할 수 있는 지점.
#관련 항목
Bootloader Internals · 7 of 37
- 1ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석
- 2Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
- 3U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적
- 4ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
- 5U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅
- 6Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적
- 7U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
- 8U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
- 9DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업
- 10임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교
- 11임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석
- 12U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘
- 13U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
- 14Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석
- 15FIT image 구조 분석 — multi-image·hash·configuration 추적
- 16U-Boot Verified Boot — RSA 서명과 public key 임베딩 흐름
- 17임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계
- 18U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리
- 19Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
- 20임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교
- 21새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
- 22부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
- 23SoC BootROM·eFuse·OTP — 부팅의 0단계 분석
- 24SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
- 25ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름
- 26DDR Training과 PHY Calibration — 보드별 파라미터 튜닝
- 27임베디드 Chain of Trust — 다단계 서명 검증의 전체 흐름
- 28임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교
- 29U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
- 30부트로더 CI 구축 — build matrix와 자동 부팅 테스트
- 31TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적
- 32PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
- 33ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
- 34U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
- 35EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정
- 36부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식
- 37UEFI Secure Boot 인증서 만료 — 2011→2023 CA 롤오버와 PQC 대비
관련 글
U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
U-Boot PCIe 열거 과정 — Root Complex 초기화·Config Space scan·BAR sizing·resource 할당, CXL DVSEC 인식까지.
U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
보드별 다른 부트 스크립트를 표준화 — U-Boot Distro Boot, extlinux.conf, boot.scr의 차이와 선택.
SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
SPL과 TPL의 정확한 역할, SRAM 안에 들어가는 코드 구조, DDR이 없는 환경에서 어떻게 동작하는가.