요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
========================
Linux and the Devicetree
========================
The Linux usage model for device tree data
:Author: Grant Likely <[email protected]>
This article describes how Linux uses the device tree. An overview of
the device tree data format can be found on the device tree usage page
at devicetree.org\ [1]_.
.. [1] https://www.devicetree.org/specifications/
The "Open Firmware Device Tree", or simply Devicetree (DT), is a data
structure and language for describing hardware. More specifically, it
is a description of hardware that is readable by an operating system
so that the operating system doesn't need to hard code details of the
machine.
Structurally, the DT is a tree, or acyclic graph with named nodes, and
nodes may have an arbitrary number of named properties encapsulating
arbitrary data. A mechanism also exists to create arbitrary
links from one node to another outside of the natural tree structure.
Conceptually, a common set of usage conventions, called 'bindings',
is defined for how data should appear in the tree to describe typical
hardware characteristics including data busses, interrupt lines, GPIO
connections, and peripheral devices.
As much as possible, hardware is described using existing bindings to
maximize use of existing support code, but since property and node
names are simply text strings, it is easy to extend existing bindings
or create new ones by defining new nodes and properties. Be wary,
however, of creating a new binding without first doing some homework
about what already exists. There are currently two different,
incompatible, bindings for i2c busses that came about because the new
binding was created without first investigating how i2c devices were
already being enumerated in existing systems.
1. History
----------
The DT was originally created by Open Firmware as part of the
communication method for passing data from Open Firmware to a client
program (like to an operating system). An operating system used the
Device Tree to discover the topology of the hardware at runtime, and
thereby supported a majority of available hardware without hard coded
information (assuming drivers were available for all devices).
Since Open Firmware is commonly used on PowerPC and SPARC platforms,
the Linux support for those architectures has for a long time used the
Device Tree.
In 2005, when PowerPC Linux began a major cleanup and to merge 32-bit
and 64-bit support, the decision was made to require DT support on all
powerpc platforms, regardless of whether or not they used Open
Firmware. To do this, a DT representation called the Flattened Device
Tree (FDT) was created which could be passed to the kernel as a binary
blob without requiring a real Open Firmware implementation. U-Boot,
kexec, and other bootloaders were modified to support both passing a
Device Tree Binary (dtb) and to modify a dtb at boot time. DT was
also added to the PowerPC boot wrapper (``arch/powerpc/boot/*``) so that
a dtb could be wrapped up with the kernel image to support booting
existing non-DT aware firmware.
Some time later, FDT infrastructure was generalized to be usable by
all architectures. At the time of this writing, 6 mainlined
architectures (arm, microblaze, mips, powerpc, sparc, and x86) and 1
out of mainline (nios) have some level of DT support.
2. Data Model
-------------
If you haven't already read the Device Tree Usage\ [1]_ page,
then go read it now. It's okay, I'll wait....
2.1 High Level View
-------------------
The most important thing to understand is that the DT is simply a data
structure that describes the hardware. There is nothing magical about
it, and it doesn't magically make all hardware configuration problems
go away. What it does do is provide a language for decoupling the
hardware configuration from the board and device driver support in the
Linux kernel (or any other operating system for that matter). Using
it allows board and device support to become data driven; to make
setup decisions based on data passed into the kernel instead of on
per-machine hard coded selections.
Ideally, data driven platform setup should result in less code
duplication and make it easier to support a wide range of hardware
with a single kernel image.
Linux uses DT data for three major purposes:
1) platform identification,
2) runtime configuration, and
3) device population.
2.2 Platform Identification
---------------------------
First and foremost, the kernel will use data in the DT to identify the
specific machine. In a perfect world, the specific platform shouldn't
matter to the kernel because all platform details would be described
perfectly by the device tree in a consistent and reliable manner.
Hardware is not perfect though, and so the kernel must identify the
machine during early boot so that it has the opportunity to run
machine-specific fixups.
In the majority of cases, the machine identity is irrelevant, and the
kernel will instead select setup code based on the machine's core
CPU or SoC. On ARM for example, setup_arch() in
arch/arm/kernel/setup.c will call setup_machine_fdt() in
arch/arm/kernel/devtree.c which searches through the machine_desc
table and selects the machine_desc which best matches the device tree
data. It determines the best match by looking at the 'compatible'
property in the root device tree node, and comparing it with the
dt_compat list in struct machine_desc (which is defined in
arch/arm/include/asm/mach/arch.h if you're curious).
The 'compatible' property contains a sorted list of strings starting
with the exact name of the machine, followed by an optional list of
boards it is compatible with sorted from most compatible to least. For
example, the root compatible properties for the TI BeagleBoard and its
successor, the BeagleBoard xM board might look like, respectively::
compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
Where "ti,omap3-beagleboard-xm" specifies the exact model, it also
claims that it is compatible with the OMAP 3450 SoC, and the omap3 family
of SoCs in general. You'll notice that the list is sorted from most
specific (exact board) to least specific (SoC family).
Astute readers might point out that the Beagle xM could also claim
compatibility with the original Beagle board. However, one should be
cautioned about doing so at the board level since there is typically a
high level of change from one board to another, even within the same
product line, and it is hard to nail down exactly what is meant when one
board claims to be compatible with another. For the top level, it is
better to err on the side of caution and not claim one board is
compatible with another. The notable exception would be when one
board is a carrier for another, such as a CPU module attached to a
carrier board.
One more note on compatible values. Any string used in a compatible
property must be documented as to what it indicates. Add
documentation for compatible strings in Documentation/devicetree/bindings.
Again on ARM, for each machine_desc, the kernel looks to see if
any of the dt_compat list entries appear in the compatible property.
If one does, then that machine_desc is a candidate for driving the
machine. After searching the entire table of machine_descs,
setup_machine_fdt() returns the 'most compatible' machine_desc based
on which entry in the compatible property each machine_desc matches
against. If no matching machine_desc is found, then it returns NULL.
The reasoning behind this scheme is the observation that in the majority
of cases, a single machine_desc can support a large number of boards
if they all use the same SoC, or same family of SoCs. However,
invariably there will be some exceptions where a specific board will
require special setup code that is not useful in the generic case.
Special cases could be handled by explicitly checking for the
troublesome board(s) in generic setup code, but doing so very quickly
becomes ugly and/or unmaintainable if it is more than just a couple of
cases.
Instead, the compatible list allows a generic machine_desc to provide
support for a wide common set of boards by specifying "less
compatible" values in the dt_compat list. In the example above,
generic board support can claim compatibility with "ti,omap3" or
"ti,omap3450". If a bug was discovered on the original beagleboard
that required special workaround code during early boot, then a new
machine_desc could be added which implements the workarounds and only
matches on "ti,omap3-beagleboard".
PowerPC uses a slightly different scheme where it calls the .probe()
hook from each machine_desc, and the first one returning TRUE is used.
However, this approach does not take into account the priority of the
compatible list, and probably should be avoided for new architecture
support.
2.3 Runtime configuration
-------------------------
In most cases, a DT will be the sole method of communicating data from
firmware to the kernel, so also gets used to pass in runtime and
configuration data like the kernel parameters string and the location
of an initrd image.
Most of this data is contained in the /chosen node, and when booting
Linux it will look something like this::
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
The bootargs property contains the kernel arguments, and the initrd-*
properties define the address and size of an initrd blob. Note that
initrd-end is the first address after the initrd image, so this doesn't
match the usual semantic of struct resource. The chosen node may also
optionally contain an arbitrary number of additional properties for
platform-specific configuration data.
During early boot, the architecture setup code calls of_scan_flat_dt()
several times with different helper callbacks to parse device tree
data before paging is set up. The of_scan_flat_dt() code scans through
the device tree and uses the helpers to extract information required
during early boot. Typically the early_init_dt_scan_chosen() helper
is used to parse the chosen node including kernel parameters,
early_init_dt_scan_root() to initialize the DT address space model,
and early_init_dt_scan_memory() to determine the size and
location of usable RAM.
On ARM, the function setup_machine_fdt() is responsible for early
scanning of the device tree after selecting the correct machine_desc
that supports the board.
2.4 Device population
---------------------
After the board has been identified, and after the early configuration data
has been parsed, then kernel initialization can proceed in the normal
way. At some point in this process, unflatten_device_tree() is called
to convert the data into a more efficient runtime representation.
This is also when machine-specific setup hooks will get called, like
the machine_desc .init_early(), .init_irq() and .init_machine() hooks
on ARM. The remainder of this section uses examples from the ARM
implementation, but all architectures will do pretty much the same
thing when using a DT.
As can be guessed by the names, .init_early() is used for any machine-
specific setup that needs to be executed early in the boot process,
and .init_irq() is used to set up interrupt handling. Using a DT
doesn't materially change the behaviour of either of these functions.
If a DT is provided, then both .init_early() and .init_irq() are able
to call any of the DT query functions (of_* in include/linux/of*.h) to
get additional data about the platform.
The most interesting hook in the DT context is .init_machine() which
is primarily responsible for populating the Linux device model with
data about the platform. Historically this has been implemented on
embedded platforms by defining a set of static clock structures,
platform_devices, and other data in the board support .c file, and
registering it en-masse in .init_machine(). When DT is used, then
instead of hard coding static devices for each platform, the list of
devices can be obtained by parsing the DT, and allocating device
structures dynamically.
The simplest case is when .init_machine() is only responsible for
registering a block of platform_devices. A platform_device is a concept
used by Linux for memory or I/O mapped devices which cannot be detected
by hardware, and for 'composite' or 'virtual' devices (more on those
later). While there is no 'platform device' terminology for the DT,
platform devices roughly correspond to device nodes at the root of the
tree and children of simple memory mapped bus nodes.
About now is a good time to lay out an example. Here is part of the
device tree for the NVIDIA Tegra board::
/{
compatible = "nvidia,harmony", "nvidia,tegra20";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
chosen { };
aliases { };
memory {
device_type = "memory";
reg = <0x00000000 0x40000000>;
};
soc {
compatible = "nvidia,tegra20-soc", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
intc: interrupt-controller@50041000 {
compatible = "nvidia,tegra20-gic";
interrupt-controller;
#interrupt-cells = <1>;
reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
};
serial@70006300 {
compatible = "nvidia,tegra20-uart";
reg = <0x70006300 0x100>;
interrupts = <122>;
};
i2s1: i2s@70002800 {
compatible = "nvidia,tegra20-i2s";
reg = <0x70002800 0x100>;
interrupts = <77>;
codec = <&wm8903>;
};
i2c@7000c000 {
compatible = "nvidia,tegra20-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x7000c000 0x100>;
interrupts = <70>;
wm8903: codec@1a {
compatible = "wlf,wm8903";
reg = <0x1a>;
interrupts = <347>;
};
};
};
sound {
compatible = "nvidia,harmony-sound";
i2s-controller = <&i2s1>;
i2s-codec = <&wm8903>;
};
};
At .init_machine() time, Tegra board support code will need to look at
this DT and decide which nodes to create platform_devices for.
However, looking at the tree, it is not immediately obvious what kind
of device each node represents, or even if a node represents a device
at all. The /chosen, /aliases, and /memory nodes are informational
nodes that don't describe devices (although arguably memory could be
considered a device). The children of the /soc node are memory mapped
devices, but the codec@1a is an i2c device, and the sound node
represents not a device, but rather how other devices are connected
together to create the audio subsystem. I know what each device is
because I'm familiar with the board design, but how does the kernel
know what to do with each node?
The trick is that the kernel starts at the root of the tree and looks
for nodes that have a 'compatible' property. First, it is generally
assumed that any node with a 'compatible' property represents a device
of some kind, and second, it can be assumed that any node at the root
of the tree is either directly attached to the processor bus, or is a
miscellaneous system device that cannot be described any other way.
For each of these nodes, Linux allocates and registers a
platform_device, which in turn may get bound to a platform_driver.
Why is using a platform_device for these nodes a safe assumption?
Well, for the way that Linux models devices, just about all bus_types
assume that its devices are children of a bus controller. For
example, each i2c_client is a child of an i2c_master. Each spi_device
is a child of an SPI bus. Similarly for USB, PCI, MDIO, etc. The
same hierarchy is also found in the DT, where I2C device nodes only
ever appear as children of an I2C bus node. Ditto for SPI, MDIO, USB,
etc. The only devices which do not require a specific type of parent
device are platform_devices (and amba_devices, but more on that
later), which will happily live at the base of the Linux /sys/devices
tree. Therefore, if a DT node is at the root of the tree, then it
really probably is best registered as a platform_device.
Linux board support code calls of_platform_populate(NULL, NULL, NULL, NULL)
to kick off discovery of devices at the root of the tree. The
parameters are all NULL because when starting from the root of the
tree, there is no need to provide a starting node (the first NULL), a
parent struct device (the last NULL), and we're not using a match
table (yet). For a board that only needs to register devices,
.init_machine() can be completely empty except for the
of_platform_populate() call.
In the Tegra example, this accounts for the /soc and /sound nodes, but
what about the children of the SoC node? Shouldn't they be registered
as platform devices too? For Linux DT support, the generic behaviour
is for child devices to be registered by the parent's device driver at
driver .probe() time. So, an i2c bus device driver will register a
i2c_client for each child node, an SPI bus driver will register
its spi_device children, and similarly for other bus_types.
According to that model, a driver could be written that binds to the
SoC node and simply registers platform_devices for each of its
children. The board support code would allocate and register an SoC
device, a (theoretical) SoC device driver could bind to the SoC device,
and register platform_devices for /soc/interrupt-controller, /soc/serial,
/soc/i2s, and /soc/i2c in its .probe() hook. Easy, right?
Actually, it turns out that registering children of some
platform_devices as more platform_devices is a common pattern, and the
device tree support code reflects that and makes the above example
simpler. The second argument to of_platform_populate() is an
of_device_id table, and any node that matches an entry in that table
will also get its child nodes registered. In the Tegra case, the code
can look something like this::
static void __init harmony_init_machine(void)
{
/* ... */
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
}
"simple-bus" is defined in the Devicetree Specification as a property
meaning a simple memory mapped bus, so the of_platform_populate() code
could be written to just assume simple-bus compatible nodes will
always be traversed. However, we pass it in as an argument so that
board support code can always override the default behaviour.
[Need to add discussion of adding i2c/spi/etc child devices]
Appendix A: AMBA devices
------------------------
ARM Primecells are a certain kind of device attached to the ARM AMBA
bus which include some support for hardware detection and power
management. In Linux, struct amba_device and the amba_bus_type is
used to represent Primecell devices. However, the fiddly bit is that
not all devices on an AMBA bus are Primecells, and for Linux it is
typical for both amba_device and platform_device instances to be
siblings of the same bus segment.
When using the DT, this creates problems for of_platform_populate()
because it must decide whether to register each node as either a
platform_device or an amba_device. This unfortunately complicates the
device creation model a little bit, but the solution turns out not to
be too invasive. If a node is compatible with "arm,primecell", then
of_platform_populate() will register it as an amba_device instead of a
platform_device.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Linux와 Devicetree
1-16Grant Likely가 작성한 이 글은 Linux가 Device Tree 데이터를 사용하는 방식을 설명합니다. Device Tree 데이터 형식의 개요는 `https://www.devicetree.org/specifications/`의 Device Tree 사용 페이지에서 볼 수 있으며, 문서에는 `GPL-2.0` 라이선스가 적용됩니다.
하드웨어 기술 언어와 binding
17-42Open Firmware Device Tree, 줄여서 Devicetree(DT)는 하드웨어를 설명하는 자료 구조이자 언어입니다. 운영체제가 읽을 수 있는 하드웨어 설명을 제공하므로 운영체제가 시스템의 세부 사항을 hard code할 필요가 없습니다.
구조적으로 DT는 이름이 있는 노드로 이루어진 트리 또는 비순환 그래프입니다. 각 노드는 임의 데이터를 담는 이름 있는 속성을 원하는 수만큼 가질 수 있고, 자연스러운 트리 구조 밖에서 한 노드와 다른 노드를 임의로 연결하는 메커니즘도 제공합니다.
개념적으로는 데이터 버스, 인터럽트 선, GPIO 연결, 주변 장치 같은 일반적인 하드웨어 특성을 설명할 때 데이터가 트리에 나타나는 방식을 공통 사용 규약인 binding으로 정의합니다.
기존 지원 코드를 최대한 활용하려면 가능한 한 기존 binding으로 하드웨어를 설명해야 합니다. 속성과 노드 이름이 단순한 문자열이므로 새 노드와 속성을 정의해 binding을 확장하거나 만들기는 쉽지만, 기존 방식을 조사하지 않고 새 binding을 만들어서는 안 됩니다.
실제로 기존 시스템이 I2C 장치를 열거하는 방법을 먼저 조사하지 않은 채 새 binding을 만든 탓에 현재 서로 호환되지 않는 I2C 버스 binding이 두 개 존재합니다.
Open Firmware에서 범용 FDT로
43-72DT는 원래 Open Firmware가 자신과 운영체제 같은 client program 사이에 데이터를 전달하는 통신 방식의 일부로 만들었습니다. 운영체제는 runtime에 Device Tree에서 하드웨어 topology를 발견하여, 필요한 드라이버가 있다는 전제 아래 hard-code된 정보 없이 사용 가능한 하드웨어 대부분을 지원할 수 있었습니다.
Open Firmware는 PowerPC와 SPARC 플랫폼에서 널리 사용되었기 때문에 Linux의 두 아키텍처 지원도 오래전부터 Device Tree를 사용했습니다.
2005년에 PowerPC Linux가 대대적인 정리를 시작하고 32비트와 64비트 지원을 통합하면서, Open Firmware 사용 여부와 관계없이 모든 powerpc 플랫폼에 DT 지원을 요구하기로 했습니다.
이를 위해 실제 Open Firmware 구현 없이도 binary blob으로 커널에 전달할 수 있는 Flattened Device Tree(FDT) 표현을 만들었습니다. U-Boot, kexec와 다른 bootloader는 Device Tree Binary(dtb)를 전달하고 부팅 시 dtb를 수정하도록 바뀌었습니다.
DT는 PowerPC boot wrapper인 `arch/powerpc/boot/*`에도 추가되어, dtb를 커널 이미지와 함께 감싸 기존의 DT 비인식 firmware에서도 부팅할 수 있게 했습니다.
그 뒤 FDT 기반 구조는 모든 아키텍처에서 쓸 수 있도록 일반화되었습니다. 이 글을 쓸 당시 mainline의 6개 아키텍처인 arm, microblaze, mips, powerpc, sparc, x86과 mainline 밖의 nios가 일정 수준의 DT 지원을 제공했습니다.
데이터 중심 플랫폼 구성의 세 목적
73-99아직 Device Tree Usage[1] 페이지를 읽지 않았다면 먼저 읽어야 합니다. 가장 중요한 점은 DT가 하드웨어를 설명하는 단순한 자료 구조라는 사실입니다. DT가 모든 하드웨어 구성 문제를 자동으로 해결하지는 않습니다.
DT가 제공하는 것은 하드웨어 구성을 Linux 커널 또는 다른 운영체제의 보드·장치 드라이버 지원과 분리하는 언어입니다. 시스템별 선택을 hard code하는 대신 커널에 전달된 데이터를 바탕으로 설정을 결정하므로 보드와 장치 지원이 데이터 중심이 됩니다.
이상적인 데이터 중심 플랫폼 설정은 코드 중복을 줄이고 하나의 커널 이미지로 다양한 하드웨어를 더 쉽게 지원하게 합니다.
Linux는 DT 데이터를 세 가지 주요 목적으로 사용합니다.
- 1. platform identification
- 2. runtime configuration
- 3. device population
root compatible을 이용한 플랫폼 식별
100-149커널은 무엇보다 먼저 DT 데이터로 특정 시스템을 식별합니다. 모든 플랫폼 세부 사항이 일관되고 신뢰할 수 있게 Device Tree에 기술되는 이상적인 환경이라면 커널에 특정 플랫폼이 중요하지 않겠지만, 실제 하드웨어에는 예외가 있으므로 커널은 early boot에 시스템을 식별하여 machine-specific fixup을 실행할 기회를 가져야 합니다.
대부분은 시스템의 정확한 정체보다 핵심 CPU나 SoC가 중요합니다. ARM의 `arch/arm/kernel/setup.c`에 있는 `setup_arch()`는 `arch/arm/kernel/devtree.c`의 `setup_machine_fdt()`를 호출합니다. 이 함수는 `machine_desc` 테이블을 검색하여 Device Tree 데이터와 가장 잘 맞는 `machine_desc`를 선택합니다.
일치는 root Device Tree 노드의 `compatible` 속성과 `struct machine_desc`의 `dt_compat` 목록을 비교해 결정합니다. `struct machine_desc`는 `arch/arm/include/asm/mach/arch.h`에 정의되어 있습니다.
`compatible` 속성은 정확한 시스템 이름으로 시작하고, 선택적으로 호환되는 보드를 호환성이 높은 순서에서 낮은 순서로 나열한 정렬된 문자열 목록입니다. TI BeagleBoard와 후속 BeagleBoard xM의 root 값은 각각 다음과 같습니다.
compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
`ti,omap3-beagleboard-xm`은 정확한 모델을 지정하면서 OMAP 3450 SoC 및 일반적인 omap3 SoC 제품군과도 호환됨을 선언합니다. 목록은 가장 구체적인 정확한 보드에서 가장 덜 구체적인 SoC 제품군 순으로 정렬됩니다.
Beagle xM이 원래 Beagle 보드와 호환된다고 선언할 수도 있지만, 같은 제품군 안에서도 보드 간 변경 폭이 크고 보드 간 호환의 정확한 뜻을 정하기 어렵기 때문에 최상위 보드 수준에서는 보수적으로 다른 보드와의 호환을 선언하지 않는 편이 좋습니다.
한 보드가 다른 보드의 carrier 역할을 하는 경우, 예를 들어 CPU module이 carrier board에 연결되는 경우는 주목할 만한 예외입니다. `compatible` 속성에 쓰는 모든 문자열은 의미를 문서화해야 하며, 문서는 `Documentation/devicetree/bindings`에 추가합니다.
machine_desc 선택과 특수 보드 예외
150-182ARM 커널은 각 `machine_desc`의 `dt_compat` 항목이 `compatible` 속성에 나타나는지 검사합니다. 일치하면 해당 `machine_desc`가 시스템을 구동할 후보가 됩니다. 전체 테이블을 검색한 뒤 `setup_machine_fdt()`는 각 `machine_desc`가 `compatible`의 어느 항목과 일치했는지를 기준으로 가장 호환되는 항목을 반환하며, 일치 항목이 없으면 `NULL`을 반환합니다.
이 구조는 같은 SoC 또는 SoC 제품군을 쓰는 많은 보드를 하나의 `machine_desc`가 지원할 수 있다는 관찰에 기반합니다. 그러나 특정 보드에만 early boot 특수 설정이 필요한 예외는 항상 생깁니다. 일반 설정 코드가 문제 보드를 직접 검사하도록 만들면 예외가 몇 개만 넘어도 코드가 금방 복잡하고 유지하기 어려워집니다.
대신 `compatible` 목록을 사용하면 일반 `machine_desc`가 `dt_compat`에 덜 구체적인 값을 지정하여 폭넓은 보드를 지원할 수 있습니다. 위 예제의 일반 보드 지원은 `ti,omap3` 또는 `ti,omap3450`과의 호환을 선언할 수 있습니다.
원래 BeagleBoard에서 early boot workaround가 필요한 버그가 발견되면, workaround를 구현하고 `ti,omap3-beagleboard`에만 일치하는 새 `machine_desc`를 추가할 수 있습니다.
PowerPC는 각 `machine_desc`의 `.probe()` hook을 호출하고 처음으로 `TRUE`를 반환한 항목을 사용하는 조금 다른 방식을 씁니다. 이 방식은 `compatible` 목록의 우선순위를 고려하지 않으므로 새 아키텍처 지원에는 피하는 편이 좋습니다.
runtime 구성과 early flat tree scan
183-219대부분의 경우 DT는 firmware가 커널에 데이터를 전달하는 유일한 수단이므로 kernel parameter 문자열과 initrd 이미지 위치 같은 runtime 및 구성 데이터도 전달합니다.
이 데이터 대부분은 `/chosen` 노드에 들어가며 Linux 부팅 시 다음과 같은 형태입니다.
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
`bootargs` 속성은 커널 인자를 담고 `initrd-*` 속성은 initrd blob의 주소와 크기를 정의합니다. `initrd-end`는 initrd 이미지 바로 다음의 첫 주소이므로 일반적인 `struct resource` 의미와 다릅니다. `/chosen`은 플랫폼별 구성 데이터를 위한 속성을 원하는 수만큼 추가로 포함할 수도 있습니다.
early boot 동안 아키텍처 설정 코드는 paging을 구성하기 전에 서로 다른 helper callback을 사용하여 `of_scan_flat_dt()`를 여러 번 호출합니다. 이 함수는 Device Tree를 훑고 helper를 통해 early boot에 필요한 정보를 추출합니다.
일반적으로 `early_init_dt_scan_chosen()`은 커널 매개변수를 포함한 `/chosen`을 분석하고, `early_init_dt_scan_root()`는 DT address space model을 초기화하며, `early_init_dt_scan_memory()`는 사용 가능한 RAM의 크기와 위치를 결정합니다.
ARM에서는 보드를 지원하는 올바른 `machine_desc`를 선택한 뒤 `setup_machine_fdt()`가 Device Tree의 early scan을 담당합니다.
unflatten과 machine 초기화 hook
220-249보드를 식별하고 early 구성 데이터를 분석한 뒤에는 일반적인 커널 초기화를 진행할 수 있습니다. 이 과정의 어느 시점에 `unflatten_device_tree()`를 호출하여 데이터를 더 효율적인 runtime 표현으로 변환합니다.
이때 ARM의 `machine_desc`에 있는 `.init_early()`, `.init_irq()`, `.init_machine()` 같은 시스템별 설정 hook도 호출합니다. 이 절은 ARM 구현을 예제로 사용하지만 DT를 사용하는 모든 아키텍처의 동작은 거의 같습니다.
이름이 나타내듯 `.init_early()`는 부팅 초기에 필요한 시스템별 설정을, `.init_irq()`는 인터럽트 처리를 설정합니다. DT 사용은 두 함수의 동작을 실질적으로 바꾸지 않습니다. DT가 제공되면 두 hook 모두 `include/linux/of*.h`의 `of_*` query 함수를 호출해 플랫폼 추가 데이터를 가져올 수 있습니다.
DT 맥락에서 가장 중요한 hook은 플랫폼 정보로 Linux device model을 채우는 `.init_machine()`입니다. 과거 embedded 플랫폼은 board support `.c` 파일에 정적 clock 구조, `platform_device`와 기타 데이터를 정의하고 `.init_machine()`에서 한꺼번에 등록했습니다.
DT를 사용하면 플랫폼마다 정적 장치를 hard code하는 대신 DT를 분석하여 장치 목록을 얻고 device structure를 동적으로 할당할 수 있습니다.
platform_device에 대응하는 DT 노드
250-257가장 단순한 경우 `.init_machine()`은 `platform_device` 묶음을 등록하기만 합니다. Linux의 `platform_device`는 하드웨어가 검출할 수 없는 memory-mapped 또는 I/O-mapped 장치와 composite 또는 virtual 장치를 표현하는 개념입니다.
DT에는 platform device라는 용어가 없지만, 대략 트리 root의 장치 노드와 단순한 memory-mapped bus 노드의 자식이 platform device에 해당합니다.
NVIDIA Tegra DT와 노드 유형 구분
258-334다음 NVIDIA Tegra 보드의 Device Tree 일부에는 root의 호환성·주소 형식·메모리, `simple-bus` SoC, interrupt controller, UART, I2S, I2C codec와 이들을 연결하는 sound 노드가 들어 있습니다.
/{
compatible = "nvidia,harmony", "nvidia,tegra20";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
chosen { };
aliases { };
memory {
device_type = "memory";
reg = <0x00000000 0x40000000>;
};
soc {
compatible = "nvidia,tegra20-soc", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
intc: interrupt-controller@50041000 {
compatible = "nvidia,tegra20-gic";
interrupt-controller;
#interrupt-cells = <1>;
reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
};
serial@70006300 {
compatible = "nvidia,tegra20-uart";
reg = <0x70006300 0x100>;
interrupts = <122>;
};
i2s1: i2s@70002800 {
compatible = "nvidia,tegra20-i2s";
reg = <0x70002800 0x100>;
interrupts = <77>;
codec = <&wm8903>;
};
i2c@7000c000 {
compatible = "nvidia,tegra20-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x7000c000 0x100>;
interrupts = <70>;
wm8903: codec@1a {
compatible = "wlf,wm8903";
reg = <0x1a>;
interrupts = <347>;
};
};
};
sound {
compatible = "nvidia,harmony-sound";
i2s-controller = <&i2s1>;
i2s-codec = <&wm8903>;
};
};
`.init_machine()` 시점에 Tegra board support 코드는 이 DT에서 어느 노드에 `platform_device`를 만들지 결정해야 합니다. 하지만 트리만 보면 각 노드가 어떤 장치인지, 실제로 장치를 나타내는지조차 곧바로 명확하지 않습니다.
`/chosen`, `/aliases`, `/memory`는 장치를 설명하지 않는 정보 노드입니다. `/soc`의 자식은 memory-mapped 장치지만 `codec@1a`는 I2C 장치입니다. `sound` 노드는 장치가 아니라 다른 장치가 audio subsystem을 만들도록 연결되는 방식을 나타냅니다.
보드 설계를 아는 사람은 각 장치를 구분할 수 있지만, 커널에는 각 노드를 처리하는 일반 규칙이 필요합니다.
root 장치 등록과 bus driver의 자식 생성
335-380커널은 트리 root에서 시작하여 `compatible` 속성이 있는 노드를 찾습니다. 일반적으로 `compatible`이 있는 노드는 어떤 형태로든 장치를 나타낸다고 가정하며, root의 노드는 processor bus에 직접 연결되었거나 다른 방식으로 설명할 수 없는 기타 시스템 장치라고 가정합니다.
Linux는 이런 각 노드에 `platform_device`를 할당하고 등록하며, 이 장치는 이후 `platform_driver`에 bind될 수 있습니다.
Linux device model의 거의 모든 `bus_type`은 장치가 bus controller의 자식이라고 가정합니다. 각 `i2c_client`는 `i2c_master`의 자식이고, 각 `spi_device`는 SPI 버스의 자식입니다. USB, PCI, MDIO도 마찬가지입니다. DT에서도 I2C 장치 노드는 항상 I2C 버스 노드의 자식이며 SPI, MDIO, USB도 같은 계층을 따릅니다.
특정 유형의 parent device가 필요 없는 장치는 `platform_device`와 뒤에서 설명할 `amba_device`뿐이며, 이들은 Linux `/sys/devices` 트리의 root에 놓일 수 있습니다. 따라서 DT root의 노드는 `platform_device`로 등록하는 것이 가장 타당합니다.
Linux board support 코드는 root 장치 검색을 시작하기 위해 `of_platform_populate(NULL, NULL, NULL, NULL)`을 호출합니다. 첫 번째 `NULL`은 시작 노드를 root로 사용한다는 뜻이고 마지막 `NULL`은 parent `struct device`가 없다는 뜻이며, 아직 match table도 사용하지 않으므로 모든 인자가 `NULL`입니다.
장치 등록만 필요한 보드라면 `.init_machine()`은 `of_platform_populate()` 호출 외에는 완전히 비어 있어도 됩니다.
Tegra 예제에서는 `/soc`와 `/sound`가 이렇게 등록됩니다. SoC 노드의 자식은 일반적으로 parent device driver가 `.probe()`할 때 등록합니다. I2C bus driver는 각 자식 노드에 `i2c_client`를, SPI bus driver는 `spi_device` 자식을 등록하며 다른 `bus_type`도 같은 방식입니다.
이 모델대로라면 SoC 노드에 bind하는 드라이버가 각 자식의 `platform_device`를 등록할 수 있습니다. board support 코드는 SoC 장치를 등록하고, 이론적인 SoC driver는 `.probe()`에서 `/soc/interrupt-controller`, `/soc/serial`, `/soc/i2s`, `/soc/i2c`의 `platform_device`를 만들 수 있습니다.
of_device_id 재귀와 simple-bus
381-402일부 `platform_device`의 자식을 다시 `platform_device`로 등록하는 방식은 흔합니다. Device Tree 지원 코드는 이를 반영하여 위 예제를 더 단순하게 만듭니다.
`of_platform_populate()`의 두 번째 인자는 `of_device_id` 테이블입니다. 이 테이블의 항목과 일치하는 노드는 그 자식 노드도 등록합니다. Tegra 코드는 다음처럼 작성할 수 있습니다.
static void __init harmony_init_machine(void)
{
/* ... */
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
}
Devicetree Specification은 `simple-bus`를 단순한 memory-mapped bus를 뜻하는 속성으로 정의합니다. `of_platform_populate()`가 `simple-bus` 호환 노드를 항상 순회한다고 가정할 수도 있지만, board support 코드가 기본 동작을 언제든 바꿀 수 있도록 match table을 인자로 전달합니다.
원문에는 I2C, SPI 등의 자식 장치 추가에 관한 논의를 보충해야 한다는 메모가 남아 있습니다.
부록 A: ARM Primecell과 AMBA 장치
403-420ARM Primecell은 ARM AMBA 버스에 연결되는 특정 유형의 장치로, 하드웨어 검출과 전원 관리를 일부 지원합니다. Linux에서는 `struct amba_device`와 `amba_bus_type`으로 Primecell 장치를 표현합니다.
문제는 AMBA 버스의 모든 장치가 Primecell은 아니며, Linux에서는 같은 bus segment에서 `amba_device`와 `platform_device` 인스턴스가 sibling인 경우가 일반적이라는 점입니다.
DT를 사용할 때 `of_platform_populate()`는 각 노드를 `platform_device` 또는 `amba_device` 중 무엇으로 등록할지 결정해야 합니다. 이 때문에 장치 생성 모델이 조금 복잡해지지만 해결책은 크지 않습니다.
노드가 `arm,primecell`과 호환되면 `of_platform_populate()`는 그 노드를 `platform_device` 대신 `amba_device`로 등록합니다.
요약과 해설
usage-model.rst:1-420Linux가 DT를 플랫폼 식별, runtime 구성, device population에 사용하고 root·simple-bus·AMBA 노드를 장치 모델에 등록하는 방식을 설명합니다. 영어 원문 전체와 한국어 전문 번역을 함께 제공하며 함수명, symbol, source path, DTS·C 코드와 원문 줄 좌표를 보존합니다.