라즈베리파이 Device Tree Overlay 원리 — fragment/fixup을 dtc로 직접 확인하기

라즈베리파이에 센서나 디스플레이를 하나 새로 붙일 때마다 base device tree(.dtb) 전체를 다시 컴파일해서 부트 파티션에 올리는 건 번거롭고, 보드마다 다른 base tree를 관리해야 하는 상황과도 잘 안 맞는다. Device Tree Overlay는 base tree를 건드리지 않고 필요한 조각(fragment)만 따로 컴파일해뒀다가, 부팅 시점에 base tree 위에 얹는 방식이다. config.txtdtoverlay=my-led 한 줄만 추가하면 끝나는 것처럼 보이지만, 그 뒤에서는 컴파일러가 “아직 존재하지 않는 노드에 대한 참조”를 나중에 채우도록 표시해두는 메커니즘이 돌아간다. 이 글에서는 오버레이가 실제로 어떻게 컴파일되고 base tree에 병합되는지 dtcfdtoverlay로 직접 확인한다.

fragment/target 구조

GPIO 17번에 LED를 하나 붙이는 오버레이를 예로 든다. fragment@0은 기존 &gpio 노드 안에 핀 설정을 추가하고, fragment@1은 트리 루트(/)에 새 gpio-leds 디바이스를 추가한다.

/dts-v1/;
/plugin/;

/ {
	compatible = "brcm,bcm2711";

	fragment@0 {
		target = <&gpio>;
		__overlay__ {
			my_pins: my_pins {
				brcm,pins = <17>;
				brcm,function = <1>; /* output */
				brcm,pull = <0>;     /* none */
			};
		};
	};

	fragment@1 {
		target-path = "/";
		__overlay__ {
			my_led: my_led {
				compatible = "gpio-leds";
				pinctrl-names = "default";
				pinctrl-0 = <&my_pins>;

				led0 {
					label = "my-led0";
					gpios = <&gpio 17 0>;
					default-state = "off";
				};
			};
		};
	};
};

target = <&gpio>는 base tree에 이미 있는 gpio 노드 에 얹으라는 뜻이고, target-path = "/"는 지정한 경로에 새 노드를 추가하라는 뜻이다. 문제는 이 오버레이 파일 자체는 base tree를 모르는 채로 독립적으로 컴파일된다는 점이다 — &gpio가 실제로 어떤 노드를 가리키는지는 컴파일 시점엔 알 수 없다.

__symbols__ / __fixups__ — 나중에 연결되는 원리

-@ 옵션을 주고 컴파일한 뒤 다시 디컴파일해보면 이 문제를 어떻게 처리하는지 그대로 드러난다.

$ dtc -@ -I dts -O dtb -o my-led.dtbo my-led-overlay.dts
$ dtc -I dtb -O dts my-led.dtbo

/ {
	fragment@0 {
		target = <0xffffffff>;
		__overlay__ {
			my_pins {
				brcm,pins = <0x11>;
				phandle = <0x01>;
				...
			};
		};
	};
	fragment@1 {
		...
		my_led {
			pinctrl-0 = <0x01>;
			...
			led0 {
				gpios = <0xffffffff 0x11 0x00>;
			};
		};
	};
	__symbols__ {
		my_pins = "/fragment@0/__overlay__/my_pins";
		my_led = "/fragment@1/__overlay__/my_led";
	};
	__fixups__ {
		gpio = "/fragment@0:target:0\0/fragment@1/__overlay__/my_led/led0:gpios:0";
	};
	__local_fixups__ {
		fragment@1 { __overlay__ { my_led { pinctrl-0 = <0x00>; }; }; };
	};
};

&gpio가 있던 자리는 전부 자리표시자 0xffffffff로 채워져 있다. 대신 __fixups__에 “gpio라는 심볼이 실제로 뭘 가리키는지 알게 되면, fragment@0target 필드와 led0gpios 필드 0번째 값을 그걸로 채워라”는 지시가 남는다. 반대로 오버레이 내부에서만 쓰이는 참조(pinctrl-0 = <&my_pins>)는 컴파일 시점에 이미 알 수 있으니 __local_fixups__에서 바로 해결된다. my_pins/my_led 자체도 __symbols__에 등록되어, 다른 오버레이가 이 오버레이의 노드를 다시 참조하는 것도 가능하다.

fdtoverlay로 실제 병합해보기

이 fixup이 실제로 채워지는 걸 보려면 gpio라는 심볼을 가진 base tree가 있어야 한다. 처음에 base tree를 그냥 dtc -I dts -O dtb로만 컴파일했다가 fdtoverlayFDT_ERR_NOTFOUND로 실패했다 — base tree도 -@로 컴파일해야 __symbols__가 생기고, fdtoverlay가 그 심볼 테이블을 보고 gpio가 어디 있는지 찾을 수 있다.

$ dtc -@ -I dts -O dtb -o base.dtb base.dts   # -@ 필수
$ fdtoverlay -i base.dtb -o merged.dtb my-led.dtbo
$ dtc -I dtb -O dts merged.dtb

/ {
	my_led {
		phandle = <0x9b>;
		pinctrl-0 = <0x9a>;
		compatible = "gpio-leds";
		led0 {
			gpios = <0x99 0x11 0x00>;
			label = "my-led0";
		};
	};

	gpio@7e200000 {
		phandle = <0x99>;
		my_pins {
			phandle = <0x9a>;
			brcm,pins = <0x11>;
		};
	};
};

병합된 결과를 보면 두 가지가 정확히 맞아떨어졌다. 첫째, my_pins 노드가 정말로 gpio@7e200000 노드 안에 들어갔다(target = <&gpio>가 의도한 그대로). 둘째, led0gpios0x99gpio@7e200000의 실제 phandle(0x99)과 정확히 일치한다 — 컴파일 시점의 자리표시자 0xffffffff가 병합 시점에 진짜 값으로 채워졌다는 뜻이다.

라즈베리파이에 실제 적용하기

라즈베리파이 OS에서는 이 병합을 부트로더가 부팅 시점에 대신 해준다. .dtbo 파일을 /boot/firmware/overlays/에 넣고 config.txt에 한 줄 추가하면 된다.

# /boot/firmware/config.txt
dtoverlay=my-led

# 오버레이가 파라미터를 지원한다면
dtoverlay=my-led,gpio_pin=27

부팅 후에는 dtoverlay -l로 현재 적용된 오버레이 목록을, dtoverlay -h <name>으로 특정 오버레이가 어떤 파라미터를 받는지 확인할 수 있다(둘 다 라즈베리파이 OS 전용 헬퍼 스크립트라 이 부분은 실행 환경이 아니라 공식 문서 기준 설명이다).

주의사항

  • base tree에 __symbols__가 없으면(-@ 없이 컴파일했다면) fdtoverlayFDT_ERR_NOTFOUND로 실패한다. 실제로 겪은 실수이니, base tree를 직접 준비해 테스트할 때는 -@를 빼먹지 않았는지부터 확인한다.
  • 오버레이 .dtbo를 base tree 없이 단독으로 디컴파일하면 Could not get phandle node for (cell 1) 같은 경고가 뜬다. 아직 fixup이 채워지지 않은 자리표시자(0xffffffff)를 해석하려다 나는 경고라 무해하다 — 실제 문제인지는 fdtoverlay로 병합해본 뒤 판단해야 한다.
  • 오버레이 문법 오류나 존재하지 않는 target은 부트로더 단계에서 조용히 무시되거나 부팅 실패로 이어질 수 있다. 실제 보드에 올리기 전에 dtc로 문법 검증(컴파일이 에러 없이 되는지)부터 하는 게 안전하다.

마무리

Device Tree Overlay는 결국 “지금 당장은 모르는 참조를 일단 자리표시자로 남겨두고, 실제로 합쳐지는 시점에 심볼 테이블을 보고 채워 넣는” 지연 바인딩 메커니즘이다. config.txt에 한 줄 추가하는 것으로 끝나 보이지만, 그 이면에는 __fixups__/__symbols__라는 명시적인 프로토콜이 있고, fdtoverlay로 그 병합 과정을 로컬에서 미리 재현해볼 수 있다는 걸 알아두면 실제 보드에 붙이기 전 문제를 미리 잡는 데 도움이 된다.

참고

답글 남기기