1. Introduction to Device Tree
  2. Compilation of the device tree
  3. Basic syntax of device tree
    1. Root node
    2. Child Node
    3. reg property
    4. #address-cellsand#size-cellsProperties
      1. #address-cells
      2. #size-cells
    5. model property
    6. status property
    7. The compatible property
    8. aliases node
    9. chosen node
    10. device_type node
    11. Custom Properties
  4. Case Analysis
    1. Case Analysis: Interrupts
      1. The ‘interrupts’ Property
      2. interrupt-controller property
      3. interrupt-parent property
      4. #interrupt-cellsattribute
      5. Comparison of other SoC device trees
    2. Case analysis: clock
      1. Clock provider
        1. Clock provider property
          1. clock-cells
          2. clock-frequency
      2. Clock consumer
        1. Clock consumer properties
          1. clock
          2. clock-names
          3. assigned-clocksandassigned-clock-rates
          4. clock-indices
          5. assigned-clock-parents
    3. Case Study: CPU
      1. Processor Topology Relationships
        1. cpu-map, socket, cluster nodes
        2. core and thread nodes
    4. Case Study: GPIO
      1. gpio-controller property
      2. #gpio-cellsproperty
      3. gpio-ranges property
      4. GPIO description attributes and gpio-cells
      5. Other attributes
      6. Example
      7. Comparison with other SoCs
    5. Case analysis: pinctrl
      1. Introduction to pinmux
      2. Using pinctrl to Set Multiplexing Relationships
        1. Client
        2. Server
      3. pinctrl Example Writing
  5. DTB File Format Parsing
    1. Header
    2. Memory Reserved Block
    3. Structure Block
      1. Token Types
      2. Tree Structure
      3. End of Structure Block
    4. Strings Block
  6. dtb expanded into device_node
    1. Source code analysis of the DTB parsing process
      1. init/main.c
      2. arch/arm64/kernel/setup.c
      3. setup_machine_fdt(__fdt_pointer)
      4. unflatten_device_tree()
        1. __unflatten_device_tree
        2. unflatten_dt_nodes()
        3. populate_node
  7. device_node to platform_device
    1. Conversion rules
    2. Example
    3. Source Code Analysis
      1. of_platform_default_populate_init()
      2. of_platform_populate()
      3. of_platform_bus_create()
      4. of_platform_device_create_pdata()
      5. of_device_alloc()
  8. platform under the device tree_device and platform_driver matching
    1. of_match_table
    2. Example
  9. of operations
    1. get device tree node
      1. of_find_by_name()
      2. of_find_node_by_path()
      3. of_get_parent()
      4. of_get_next_child()
      5. for_each_child_of_node()
      6. of_find_compatible_node()
      7. of_find_matching_node_and_match()
      8. Example
    2. Get device tree property
      1. of_find_property()
      2. of_property_count_elems_of_size()
      3. of_property_read_u32_index()
      4. of_property_read_u64_index()
      5. of_property_read_variable_u32_array()
      6. of_property_read_string()
      7. of_property_read_bool()
      8. Example
    3. ranges attribute
      1. platform_get_Prerequisite for resource to obtain device tree resources
      2. Introduction to the ranges property
      3. Device Classification
        1. Memory-Mapped Devices
        2. Non-Memory-Mapped Devices
    4. The concept of named resources
    5. Accessing the reg property
    6. Obtaining interrupt resources
      1. irq_of_parse_and_map()
      2. irqd_get_trigger_type()
      3. irq_get_irq_data()
      4. gpio_to_irq()
      5. of_irq_get()
      6. platform_get_irq()
      7. Example
  10. Reference Document: dts-bindings
  11. Device Tree Overlay
    1. Device Tree Overlay Syntax
    2. Porting Device Tree Driver Overlays
    3. Load the device tree overlay
    4. Changeset
    5. Introduction to the Virtual File System ConfigFS
    6. ConfigFS Core Data Structures
      1. configfs_subsystem
      2. config_group
      3. config_item
      4. config_item_type
        1. configfs_item_operations
          1. release()
        2. configfs_group_operations
          1. make_item()
          2. make_group()
          3. drop_item()
        3. struct configfs_attribute
      5. Key data structure relationship diagram
    7. ConfigFS API
      1. Core API
        1. config_group_init()
        2. config_group_init_type_name()
        3. config_item_init_type_name()
        4. configfs_register_subsystem()
        5. configfs_unregister_subsystem()
        6. configfs_register_group()
        7. configfs_unregister_group()
        8. config_item_put()
      2. Attribute-related
        1. CONFIGFS_ATTR_RO()
        2. CONFIGFS_ATTR_WO()
        3. CONFIGFS_ATTR()
    8. Example
      1. Register configFS subsystem
      2. Register group container
      3. Create item in user space
      4. Improve drop and release
      5. Register attribute
      6. Implement multi-level directories
    9. dtbocfg driver analysis
    10. References
Cover image for Linux Device Tree

Linux Device Tree


Timeline

Timeline

2025-11-30

init

2025-12-01

add devicetree

2025-12-03

add devicetree plugin

This article introduces the concept and role of the Linux Device Tree, discusses in detail basic concepts such as DTS, DTSI, DTB, and DTC, the storage location and compilation method of device tree source code, and summarizes basic syntax rules of the device tree including root node, child nodes, and property definitions.

Linux Driver Notes

DirectoryLinks
1. Linux Driver Framework
2. Linux Driver Loading Logic
3. Character Device Basics
4. Concurrency and Race Conditions
5. Advanced Character Device Progression
6. Interrupts
7. Platform Bus
8. Device Tree
9. Device Model
10. Hot Plug
11. pinctrl Subsystem
12. GPIO Subsystem
13. Input Subsystem
14. Single Bus
15. I2C
16. SPI
17. UART
18. PWM
19. RTC
20. Watchdog
21. CAN
22. Network Device
23. ADC
24. IIO
25. USB
26. LCD

Introduction to Device Tree

Background

Device Tree is a hardware description mechanism used to describe the characteristics, connection relationships, and configuration information of hardware devices in embedded systems and operating systems. It provides a platform-independent way to describe hardware, reducing the coupling between the kernel and hardware, and improving system portability and maintainability.

In the platform bus model, we useplatform_devicestructures to describe hardware devices, which is a traditional way of describing platform bus devices.

Eachplatform_devicestructure represents a specific hardware device, and by registering it on the platform bus, the kernel can communicate and interact with the device. This structure contains information such as the device name, resources (e.g., memory addresses, interrupt numbers), and device driver.

However, over time, the ARM portion of the Linux kernel contained a large amount of platform-specific configuration code, which was often messy and repetitive, leading to maintenance difficulties and increased workload. The introduction of the device tree brought revolutionary changes to the Linux kernel on the ARM architecture. It provides a unified way to describe hardware, making support for different chips and board levels simpler and more flexible. Additionally, the device tree offers visualization and readability of hardware configurations, making it easier for developers to understand and debug hardware.

DTS(Device Tree Source)DTS is the source file of the device tree, using a text-like syntax to describe the structure, properties, and connections of hardware devices. DTS files have the .dts extension and are typically written by developers. They are human-readable forms used to describe the hierarchical structure and attribute information of the device tree.

DTSI(Device Tree Source Include)DTSI files are include files for device tree source files. They extend the functionality of DTS files and are used to define reusable device tree fragments. DTSI files have the .dtsi extension and can be included and shared across multiple DTS files. Using DTSI improves the reusability and maintainability of the device tree (similar to the role of header files in the C language).

DTB(Device Tree Blob)DTB is the binary representation of the device tree. DTB files are binary files compiled from DTS or DTSI files, with the .dtb extension. DTB files contain the structure, properties, and connection information of the device tree and are loaded and parsed by the operating system. At runtime, the operating system uses DTB files to dynamically identify and manage hardware devices.

DTC(Device Tree Compiler)DTC is the device tree compiler. It is a command-line tool used to compile DTS and DTSI files into DTB files. DTC converts the text-format device tree source code into a binary device tree representation so that the operating system can load and parse it. DTC is an important tool in device tree development.

Location of device tree in source code

Device tree source files under the ARM64 architecture are typically stored in thearch/arm64/boot/dts/directory and its subdirectories. This directory is also the root directory for device tree source files and contains subdirectories for different ARM64 platforms and devices.

Within the ARM64 subdirectories, they are also organized and categorized by hardware platform, device type, or manufacturer. The naming of these subdirectories may be related to specific chip manufacturers (such as Qualcomm, NVIDIA, Samsung). Since the SoC we use is Rockchip’s rk3568, the matching device tree directory isarch/arm64/boot/dts/rockchip. Each subdirectory may contain multiple device tree files to describe different hardware configurations and device types.

Compilation of the device tree

In the Linux kernel source code, the source code and related tools of DTC (Device Tree Compiler) are usually stored in thescripts/dtc/directory

Device tree compilation

1
dtc -I dts -O dtb -o output.dtb input.dts
  • -Iindicates the input file type
  • -Oindicates the output file type
  • -oindicates the compiled output file name

Device tree decompilation

1
dtc -I dtb -O dts -o output.dts input.dtb

Compilation in Linux

1
2
3
4
# Compile all arm64 DTB files
ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- make dtbs
# Compile a single DTB file
ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- make imx6dl-sabrelite.dtb

For debugging, exposing the DT to user space may be useful. Use theCONFIG_PROC_DEVICETREEconfiguration variable to achieve this. Then, you can browse the/proc/devicetreeDT in

Basic syntax of device tree

Root node

1
2
3
4
5
6
7
8
/dts-v1/; // Device Tree Version Information

/ {
// Root Node Start
/*
Comments can be added here,Describe the properties and configuration of the root node
*/
}

Child Node

1
2
3
4
[label:] node-name@[unit-address] {
[properties definitions]
[child nodes]
};
  • Node Label(Optional): A node label is an optional identifier used to reference the node in the device tree. Labels allow other nodes to directly reference this node, establishing reference relationships within the device tree.

  • Node Name: The node name is a string that uniquely identifies the node’s location in the device tree. The node name is typically the name of the hardware device but must be unique within the device tree.

  • Unit Address(Optional): The unit address is used to identify an instance of a device. It can be an integer, a hexadecimal value, or a string, depending on the device’s requirements. The purpose of the unit address is to distinguish different instances of the same type of device.

  • Property Definitions: Property definitions are a set of key-value pairs used to describe the configuration and characteristics of a device. Properties can be defined based on the device’s requirements, such as register addresses, interrupt numbers, clock frequencies, etc.

  • Child Nodes: Child nodes are sub-items of the current node, used to further describe sub-components or configurations of the hardware device. Child nodes can contain their own property definitions and deeper child nodes, forming the hierarchical structure of the device tree.

reg property

The reg property is used tospecify the register address and size of a device in the device tree, providing the register mapping relationship with the physical device in the device tree.

The reg property can havesingle-value formatandlist-value formatthese two common formats.

  • The single-value format is as follows:
1
reg = <address size>;

This format is suitable for describing a single register. Here, address is the starting register address of the device, which can be an integer or hexadecimal value (u32). size indicates the size of the register, i.e., the number of bytes it occupies.

Example:

1
2
3
4
5
my_device {
compatible = "vendor,device";
reg = <0x1000 0x4>;
// Definition of other properties and child nodes
}
  • List-value format
1
reg = <address1 size1 address2 size2 ...>;

When a device has multiple register regions, the list-value format of the reg property can be used to describe the address and size of each register region. In this way, the positions and sizes of multiple registers can be specified to describe the complete register mapping of the device.

Example:

1
2
3
4
5
my_device {
compatible = "vendor,device";
reg = <0x1000 0x8 0x2000 0x4>;
// Definition of Other Properties and Child Nodes
};

Each device has at least one node in the DT. Some properties are common to many device types, especially those on buses known to the kernel (SPI, I2C, platform, MDIO, etc.). These properties are reg,#address-cellsand#size-cellswhose purpose is device addressing on their respective bus.

Every addressable device has a reg property.

That is, the primary addressing property is reg, a generic property whose meaning depends on the bus the device is on.

#address-cellsand#size-cellsProperties

#address-cellsand#size-cellsproperties are used to specify, in the device tree to be set up in the previous subsection,address cellsandaddress sizethe number of bits. They provide the metadata needed for device tree parsing to correctly interpret the address and size information of devices.

size-cellandaddress-cellprefix of#(sharp) can be translated as length.

Addressable devices inherit from their parent node’s#size-celland#address-cell, the parent node represents the bus controller. Specifying that a device has#size-celland#address-celldoes not affect the device itself, but affects its child devices. In other words, when interpreting a given node’sregproperty, the parent node’s#address-cellsand#size-cellsvalues must be known. The parent node can freely define the addressing scheme applicable to its child nodes (children).

#address-cells

The property is a special property located at the root node of the device tree, which specifies the number of bits in an address cell within the device tree. An address cell is a single unit used to represent a device address in the device tree. It is typically an integer, which can be a decimal or hexadecimal value. Its value tells the software parsing the device tree how many bits to use to represent one address cell when interpreting device addresses.

By default,#address-cellsthe value of is 2, meaning two cells are used to represent a device address. This means the device’s address will consist of two integers (each using a specified number of bits).

For example, for a device usingtwo 32 bit integerFor devices representing addresses (64-bit), the #address-cells property can be set to <2> in the root node of the device tree.

#size-cells

#size-cells The property is also a special property located in the root node of the device tree, which specifies the number of bits for size cells in the device tree. A size cell is a single unit used to represent the size of a device in the device tree. It is typically an integer, which can be a decimal or hexadecimal value.

example

1
2
3
4
5
6
7
8
9
node1 {
#address-cells = <1>;
#size-cells = <1>;

node1-child {
reg = <0x02200000 0x4000>;
// definitions of other properties and child nodes
};
};
  • Address part: 0x02200000 is interpreted as one address cell, with the address being 0x02200000.
  • Size part: 0x4000 is interpreted as one size cell, with the size being 0x4000.

model property

In the device tree,the model property is used to describe the model or name of the device. It is typically an attribute of a device node, used to provide identification information about the device. The model property is optional but is frequently used in practice.

1
2
3
4
5
my_device {
compatible = "vendor,device";
model = "My Device XYZ";
// definitions of other properties and child nodes
}

The model property is often used to identify and distinguish different devices, especially when the compatible property of device nodes is the same or similar. By using different model property values, the type of device in use can be determined more accurately.

status property

In the device tree, the status propertyis used to describe the state of a device or node. It is one of the common properties in the device tree, used to indicate the availability or operational status of a device or node.

The values of the status property can be the following:

  • okay"okay”: Indicates that the device or node is working normally and available.

  • disabled"disabled”: Indicates that the device or node is disabled and unavailable.

  • reserved"reserved”: Indicates that the device or node is reserved and temporarily unavailable.

  • fail"fail”: Indicates that the device or node has failed initialization or operation and is unavailable.

Example:

1
2
3
4
5
my_device {
compatible = "vendor,device";
status = "okay";
// Definitions of other properties and child nodes
}

The compatible property

In the device tree, the compatible property is used to describe the compatibility information of a device. It is one of the important properties in the device tree,used to identify the matching relationship between a device node and a driver.

The value of the compatible property is a string or a list of strings, used to specify the rules for compatibility between a device node and the corresponding driver or device descriptor. Typically, the value of the compatible property is defined by the device manufacturer and used in the device tree.

The following are some examples of common compatible property values:

  • Single string value: for example"vendor,device", used to specify that a device node is compatible with a specific device from a specific manufacturer.
  • String list: for example["vendor,device1", "vendor,device2"], used to specify that a device node is compatible with multiple devices, typically when the device node has multiple variants or configurations.
  • Wildcard matching: for example"vendor,*", used to specify that a device node is compatible with all devices from a specific manufacturer, regardless of the specific device identifier.

By using the compatible property, the device tree can provide matching information between devices and drivers. When the device tree is parsed by the operating system or device management software, the appropriate driver is selected based on the compatible property value of the device node to initialize and configure the device.

aliases node

The aliases node is a special node used todefine device aliases. This nodeis located at the root of the device tree, and has a node path/aliases

1
2
3
4
5
6
aliases {
mmc0 = &sdmmc0;
mmc1 = &sdmmc1;
mmc2 = &sdhci;
serial0 = "/simple@fe000000/seria1@11c500";
};

In the definition of aliases,the & symbol is used to reference nodes in the device tree. The purpose of aliases is to provide more readable names, making the device tree easier to understand and maintain. By using aliases, associations between device nodes can be simplified, and repeated input of device node paths can be reduced.

**Aliases defined in the aliases node are only visible within the device tree and cannot be referenced outside the device tree.**They are mainly used for internal organization and referencing within the device tree to improve readability and maintainability.

chosen node

The chosen node is a special node in the device tree, used topass and store information related to system boot and configuration. It has a path/chosen

The chosen node typically contains the following child nodes and properties:

  • bootargs: used tostore the command-line parameters passed when booting the kernel. It can contain information such as kernel parameters, device tree parameters, etc. During the boot process, the operating system or bootloader can read this property to obtain startup parameters.
  • stdout-path: used toSpecifies the device path for standard outputDuring the boot process, the operating system can use this property to determine which device to send console output to, such as a serial port or display.
  • firmware-name: Used to specify the name of the system firmware. It can be used to identify the type and version of the bootloader or firmware being used.
  • linux,initrd-start and linux,initrd-end: These properties are used to specify the start and end addresses of the Linux kernel initial RAM disk (initrd). This information is used by the bootloader during the boot process to load the initrd into memory for the kernel.
  • Other custom properties: The chosen node can also contain other custom properties for storing information specific to system boot and configuration. The specific meaning and usage of these properties depend on the device tree’s usage and context.

Example

1
2
3
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

By using the chosen node, relevant information during the system boot process can be conveniently passed to the operating system or bootloader. This allows various components of system boot and configuration to share and access this information, enabling a more flexible and configurable system boot flow.

device_type node

In the device tree,the device_type node is a node used to describe the device type. Ittypically exists as a property of a device node

The value of the device_type property is a string used to identify the type of the device.

The existence of the device_type node helps the operating system or other software identify and handle devices. It provides basic classification information about the device, enabling drivers, device tree parsers, or other system components to perform corresponding operations based on the device type.

Common device types include but are not limited to:

  • cpu: Represents the central processing unit.
  • memory: Represents a memory device.
  • display: Represents a display device, such as an LCD screen.
  • serial: Represents a serial communication device, such as a serial port.
  • ethernet: Represents an Ethernet device.
  • usb: Represents a Universal Serial Bus device.
  • i2c: Represents a device that communicates using the I2C (Inter-Integrated Circuit) bus.
  • spi: Represents a device that communicates using the SPI (Serial Peripheral Interface) bus.
  • gpio: Represents a general-purpose input/output device.
  • pwm: Represents a pulse-width modulation device.

These are just some examples of common device types. In practice, device types can be customized and extended based on specific hardware and device tree usage.

Custom Properties

Custom properties in the device tree are attributes added by users based on specific needs. These properties can be used to provide additional information, configuration parameters, or metadata to meet the specific requirements of a device or system.

For example, you can define a custom pin number attribute ‘pinnum’ in the device tree.

1
2
3
4
my_device {
compatible = "my_device";
pinnum = <0 1 2 3 4>;
};

Definitions of some data types used in the device tree:

  • Text strings are represented with double quotes. Commas can be used to create a list of strings.
  • Cells are 32-bit unsigned integers separated by angle brackets.
  • Boolean data is simply an empty property. Its value is true or false depending on whether the property exists.

Case Analysis

Case Analysis: Interrupts

arch/arm64/boot/dts/rk3568.dtsi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pinctrl: pinctrl {
compatible = "rockchip,rk3568-pinctrl";
rockchip,grf = <&grf>;
rockchip,pmu = <&pmugrf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;

gpio0: gpio0@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;

gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};

};

arch/arm64/boot/dts/rockchip/topeet-screen-lcds.dts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
&i2c1 {
status = "okay";
ft5x061:ft5x06@38 {
status = "disabled";
compatible = "edt,edt-ft5306";
reg = <0x38>;
touch-gpio = <&gpio3 RK_PA5 IRQ_TYPE_EDGE_RISING>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
reset-gpio = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <800>;
touchscreen-size-y = <1280>;

};
};

The ‘interrupts’ Property

The ‘interrupts’ property is used to specify interrupt-related information for a device. It describesthe type of interrupt controller, the interrupt number, and the interrupt trigger type.

1
2
3
4
5
6
7
8
9
10
11
12
13
gpio0: gpio0@fdd60000 {
...
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
interrupt-controller;
...
};

ft5x061:ft5x06@38 {
...
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
...
};
  1. Interrupt Controller Type

The first parameter of the ‘interrupts’ property specifies the type of interrupt controller.

Common types include**GIC (Generic Interrupt Controller)、IRQ (Basic Interrupt Handling)**etc. For example, in the given code snippet, GIC_SPI indicates that the interrupt controller type is GIC SPI interrupt.
The interrupt controller is responsible for managing interrupt signals in the system. It can be a dedicated interrupt controller in hardware or an interrupt controller within the processor.

  1. Interrupt number

The second parameter of the interrupts property specifies the interrupt number used by the device.
The interrupt number is a unique identifier used to distinguish different interrupt signal sources. The system uses the interrupt number to identify the interrupt source and perform corresponding interrupt handling.

The interrupt number can be an integer value, a macro definition, or a symbolic reference. In the given code snippet, 33 indicates that the interrupt number used by the device is 33.

  1. Interrupt trigger type

The third parameter of the interrupts property specifies the interrupt trigger type, i.e., the triggering condition of the interrupt signal. Common trigger types include edge-triggered and level-triggered.
Level-triggered means the interrupt signal triggers when maintaining a specific level state, which can be high-level triggered or low-level triggered.

In the given code snippet, IRQ_TYPE_LEVEL_HIGH indicates that the interrupt trigger type is high-level triggered. The macro definitions for trigger types are in the kernel source codeinclude/dt-bindings/interrupt-controller/irq.hdirectory

1
2
3
4
5
6
#define IRQ_TYPE_NONE 0 // No interrupt trigger type
#define IRQ_TYPE_EDGE_RISING 1 // Rising edge triggered
#define IRQ_TYPE_EDGE_FALLING 2 // Falling edge triggered
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)// Both edges triggered
#define IRQ_TYPE_LEVEL_HIGH 4 // High-level trigger
#define IRQ_TYPE_LEVEL_LOW 8 // Low-level trigger

interrupt-controller property

interrupt-controllerThis property is used toidentify that the device described by the current node is an interrupt controller

An interrupt controller is ahardware or software module, responsible for managing and distributing interrupt signals. It receives interrupt requests from various devices and distributes interrupts to the corresponding processors or devices based on priority and configuration rules.

interrupt-controllerThis property itself has no specific value; it only needs to appear in the node’s property list.

interrupt-parent property

interrupt-parentThis property is used in the device tree to establish an association between an interrupt signal source and an interrupt controller. Itspecifies the interrupt controller node to which the interrupt signal source belongs, ensuring correct interrupt handling and distribution

interrupt-parentThe property value is a reference that points to the path or label of the interrupt controller node.

You can use a path to reference the interrupt controller node, such as/interrupt-controller-node, or use a label to reference the interrupt controller node, such as&interrupt-controller-label

1
2
3
4
5
6
ft5x061:ft5x06@38 {
...
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
...
};

Not present in gpio0interrupt-parent, but because there is a default in the root DTSinterrupt-parent = GIC, GPIO0 does not need to be explicitly written.

#interrupt-cellsattribute

#interrupt-cellsThe attribute is used todescribe the number of interrupt number units for each interrupt signal source in the interrupt controller

An interrupt number unit refers to a fixed-size unit used to represent the interrupt number and other related information. By specifying the number of interrupt number units, the operating system can correctly parse and process interrupt information and associate it with the interrupt controller and interrupt signal source.

#interrupt-cellsThe value of the attribute is an integer representing the number of interrupt number units. Typically, this value is a positive integer, such as 1, 2, or 3, depending on the requirements of the interrupt controller and device.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gpio0: gpio0@fdd60000 {
...
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
...
interrupt-controller;
#interrupt-cells = <2>;
...
};

ft5x061:ft5x06@38 {
...
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
...
};

In gpio0, the interrupt controller is gic, and in the gic node#interrupt-cellsthe attribute is set to 2, which is why the interrupts attribute in the gpio0 node has two values;

while the interrupt controller of ft5x061 is gpio3, and in the gpio3 node#interrupt-cellsthe attribute is set to 2, so the interrupts attribute of the ft5x06 node has only two values.

Comparison of other SoC device trees

NXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gpio1: gpio@0209c000 {
compatible = "fsl,inx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;

edt-ft5x06@38 {
compatible = "edt,edt-ft5306", "edt,edt-ft5x06", "edt,edt-ft5406";
pinctrl-names = "default";
pinctrl-0 = <&ts_int_pin &ts_reset_pin>;
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <9 0>;
reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
irq-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
status = "disabled";
};
}

Samsung

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gpio_c: gpioc {
compatible = "gpio-controller";
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};

ft5x06: ft5x06038 {
compatible = "edt,edt-ft5406";
reg = <0x38>;
pinctrl-names = "default";
#if defined(RGB_1024x600) || defined(RGB_800x480)
pinctrl-0 = <&tsc2007_irq>;
interrupt-parent = <&gpio_c>;
interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
#endif
#if defined(LvDs_800×1280) || defined(LvDS_1024x768)
pinctrl-0 = <&gt911_irq>;
interrupt-parent = <&gpio_b>;
interrupts = <29 IRQ_TYPE_EDGE_FALLING>;
#endif
reset-gpios = <&gpio_e 30 0>;
}

Case analysis: clock

Clock is used todescribe the clock sources, clock-related configurations, and connection relationships in hardware devices and systems.

Clocks play a crucial role in computer systems, used to synchronize and time the operations of various hardware devices.

Clocks can be divided into two main roles:clock providerandclock consumer

Clock provider

  • Definition: A clock provider isa hardware or software module responsible for generating and providing clock signals. It can be a clock controller, PLL, clock generator, etc.
  • Device tree node: A clock provider is represented in the device treein the form of a clock node

Clock provider property

clock-cells

This property is used to specify the number of bits for the clock ID. It is an integer value representing the number of bits for the clock ID.

Typically, whenclock-cellsis 0, it indicates one clock; when it is 1, it indicates multiple clocks.

1
2
3
4
5
6
7
8
9
10
11
12
osc24m: osc24m {
compatible = "clock";
clock-frequency = <24000000>;
clock-output-names = "osc24m";
#clock-cells = <0>;
};

// Multiple clocks
clock: clock {
#clock-cells = <1>;
clock-output-names = "clock1", "clock2";
};
clock-frequency

is a property in the device tree used to specify the clock frequency. It describes the frequency of the clock signal provided by a clock node, using Hertz (Hz) as the unit.

For a clock producer node,clock-frequencyproperty indicates the frequency of the clock signal generated by that node. It is used to describe the output frequency of hardware or software modules that generate clock signals, such as clock controllers, crystal oscillators, and PLLs.

1
2
3
4
5
6
osc24m: osc24m {
compatible = "clock";
clock-frequency = <24000000>;
clock-output-names = "osc24m";
#clock-cells = <0>;
};

Clock consumer

A clock consumer isa hardware device or module that depends on a clock signal. They obtain clock signals by referencing the clock source provided by a clock producer node.

Clock consumer properties

clock

This property is used to specify the clock sources required by a clock consumer node. It is an integer array, where each element is a clock number representing a clock source needed by the clock consumer.

clock-names

An optional property used to specify the names of the clock sources required by a clock consumer node. It is a string array, corresponding one-to-one with the clocks array, to provide descriptive names for the clock sources.

An example of a clock consumer is as follows:

1
2
3
4
clock: clock {
clocks = <&cru CLK_VOP>;
clock-names = "clk_vop";
};

The clocks property specifies the clock sources used by this node, referencingcruin the nodeCLK_VOPclock source.clock-namesThe attribute specifies the name of the clock source, here it isclk_vop

assigned-clocksandassigned-clock-rates

are attributes in the device tree used to describe multiple clocks, typically used together.

assigned-clocksThe attribute is used toidentify the clock source used by the clock consumer node

It is an integer array, where each element corresponds to a clock number. The clock number refers to the index of the clock source provided by the clock producer node (such as a clock controller).

By using theassigned-clocksattribute in the clock consumer node, the required clock source for that node can be specified.

assigned-clock-ratesThe attribute is used tospecify the clock frequency for each clock source

It is an integer array, where each element corresponds to the frequency of a clock source. The clock frequency is expressed in Hz (Hertz).

assigned-clock-ratesThe number and order of elements in the attribute should correspond toassigned-clocksthe clock numbers in the attribute.

1
2
3
4
5
cru: clock-controller@fdd20000 {
#clock-cells = <1>;
assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKVDEC_PRE>;
assigned-clock-rates = <32768>, <300000000>;
};
clock-indices

clock-indicesThe attribute is used tospecify the index value of the clock source used by the clock consumer node

It is an integer array, where each element corresponds to the index of a clock source.

The clock index refers to the number of the clock source provided by the clock producer node (such as a clock controller). By using theclock-indicesattribute in the clock consumer node, the required clock source for that node can be explicitly specified and matched in a specific order.

1
2
3
4
5
6
7
8
9
10
11
scpi_dvfs: clocks-0 {
#clock-cells = <1>;
clock-indices = <0>, <1>, <2>;
clock-output-names = "atlclk", "aplclk", "gpuclk";
};

scpi_clk: clocks-1 {
#clock-cells = <1>;
clock-indices = <3>;
clock-output-names = "pxlclk";
};

In the first node,atlclk,aplclk,gpuclkthe indices of the three clock sources are set to 0, 1, and 2 respectively; in the second node,pxlclkthe index value of the clock source is set to 3.

assigned-clock-parents

Used tospecify the parent clock source of the clock source used by the clock consumer node

It is an array of clock source references, where each element corresponds to a reference to a parent clock source.

In the clock hierarchy,some clock sources may be parent clock sources of other clock sources, meaning they provide clock signals as input to other clock sources.

By using theassigned-clock-parentsattribute in the clock consumer node, the required parent clock source for that node can be explicitly specified and matched in a specific order.

1
2
3
4
5
clock: clock {
assigned-clocks = <&clkcon 0>, <&pll 2>;
assigned-clock-parents = <&pll 2>;
assigned-clock-rates = <115200>, <9600>;
};

assigned-clocksThe attribute specifies the clock source used by this node, referencing two clock source nodes:clkcon 0andpll 2

assigned-clock-parentsThe attribute specifies the parent clock source for these clock sources, referencing thepll 2clock source node.

assigned-clock-ratesThe attribute specifies the clock frequency for each clock source, which are 115200 and 9600 respectively.

Case Study: CPU

The device tree’scpus nodeis an important node used to describe the processors in the system. It is thetop-level node of the processor topology, containing all processor-related information.

Node Structure

The cpus node is a container node that contains child nodes for each processor in the system.

The name of each child node is typicallycpu@X, where X is the index number of the processor. Each child node contains processor-related attributes, such as clock frequency, cache size, etc.

Processor Properties

cpu@XThe attributes in the child node may include the following information:

  • device_type: Indicates the device type as a processor (e.g., “cpu”).
  • reg: Specifies the address range of the processor, typically a physical address or register address.
  • compatible: Specifies the compatibility information of the processor, used to match the corresponding device driver.
  • clock-frequency: Specifies the clock frequency of the processor.
  • cache-size: Specifies the cache size of the processor.

Processor Topology Relationships

In addition to the basic attributes of the processor,the cpus node can also contain other nodes used to describe processor topology relationships, to provide more detailed processor topology information. These nodes can help the operating system and software understand the connection relationships, organizational structure, and characteristics among processors.

  • cpu-map: Describes the mapping relationship of processors, typically used in multi-core processor systems.
  • socket: Describes the physical sockets or chipset in a multi-processor system.
  • cluster: Describes a processor cluster, which is a logical group formed by organizing multiple processors together.
  • core: Describes a processor core, which is an independent execution unit within a physical processor.
  • thread: Describes a processor thread, which is a thread within a physical processor core. (Multiple threads exist only if hyper-threading is enabled; otherwise, one core has only one thread.)

The nesting relationship of these nodes can form a hierarchical structure under the cpus node, reflecting the processor’s topology.

Single-core CPU:

1
2
3
4
5
6
7
8
9
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
// Other attributes...
};
}

Multi-core CPU:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};

cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};

cpu2: cpu@2 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};

cpu3: cpu@3 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};
}

The cpus node is a container node that contains the cpu0 child node. This node uses the#address-cellsand#size-cellsattributes to specify the number of units for address and size.

cpu-map, socket, cluster nodes

  • cpu-mapnode isone of the nodes in the device tree used to describe the mapping relationship of processors with big.LITTLE architecture

    • Its parent node must be thecpusnode, whilechild nodes can be one or moreclusterandsocketnodes.
    • Throughcpu-mapnodes, the connections and organizational structure between different cores and clusters can be defined.
  • socketnodes are used to describe the mapping relationship between processor sockets (socket).

    • Eachsocketchild node represents a processor socket, which canusecpu-map-maskattribute to specify the core used by that socket.
    • By specifying the appropriatesocketfor eachcpu-map-mask, you can define the cores used in different sockets. This allows the operating system and software to understand the core allocation across different sockets.
  • clusterThe node is used to describe the mapping relationship between cores (cluster).

    • Eachclusterchild node represents a core cluster, which canuse thecpu-map-maskattribute to specify the cores used by that cluster.
    • By specifying the appropriateclusterfor eachcpu-map-maskchild node, you can define the cores used in each cluster. This allows the operating system and software to understand the core allocation across different clusters

A specific example of a big.LITTLE architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
cpus {
#address-cells = <2>;
#size-cells = <0>;

cpu-map {
cluster0 {
core0 {
cpu = <&cpu_l0>;
};

core1 {
cpu = <&cpu_l1>;
};
core2 {
cpu = <&cpu_l2>;
};
core3 {
cpu = <&cpu_l3>;
};
};

cluster1 {
core0 {
cpu = <&cpu_b0>;
};
core1 {
cpu = <&cpu_b1>;
};
};
};

cpu_l0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};

cpu_l1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};

cpu_l2: cpu@2 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};

cpu_l3: cpu@3 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};

cpu_b0: cpu@100 {
device_type = "cpu";
compatible = "arm,cortex-a72", "arm,armv8";
};

cpu_b1: cpu@101 {
device_type = "cpu";
compatible = "arm,cortex-a72", "arm,armv8";
};
};

This device tree describes a system with multiple CPU cores, including four Cortex-A53 cores and two Cortex-A72 cores.

core and thread nodes

coreandthreadNodes are typically used to describe the configuration of processor cores and threads.

coreNodes are used to describe processor cores. A processor usually consists of multiple cores, each of which can independently execute instructions and tasks.

threadNodes are used to describe processor threads. A thread is the basic execution unit running on a processor core, and each core can support multiple threads. (Unless hyper-threading is enabled, a core has only one thread.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
cpus {
#address-cells = <2>;
cpu-map {
socket0 {
cluster0 {
core0 {
thread0 {
cpu = <&CPU0>;
};
thread1 {
cpu = <&CPU1>;
};
};

core1 {
thread0 {
cpu = <&CPU2>;
};
thread1 {
cpu = <&CPU3>;
};
};
};

cluster1 {
core0 {
thread0 {
cpu = <&CPU4>;
};
thread1 {
cpu = <&CPU5>;
};
};
core1 {
thread0 {
cpu = <&CPU6>;
};
thread1 {
cpu = <&CPU7>;
};
};
};
};
socket1 {
cluster0 {
core0 {
thread0 {
cpu = <&CPU8>;
};
thread1 {
cpu = <&CPU9>;
};
};
core1 {
thread0 {
cpu = <&CPU10>;
};
thread1 {
cpu = <&CPU11>;
};
};
};
cluster1 {
core0 {
thread0 {
cpu = <&CPU12>;
};
thread1 {
cpu = <&CPU13>;
};
};
core1 {
thread0 {
cpu = <&CPU14>;
};
thread1 {
cpu = <&CPU15>;
};
};
};
};
};
};

Case Study: GPIO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPI00>, <&pmucru DBCLK_GPI00>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller;
#interrupt-cells = <2>;
};

ft5x06: ft5x06@38 {
status = "disabled";
compatible = "edt,edt-ft5306";
reg = <0x38>;
touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <800>;
touchscreen-size-y = <1280>;
touch_type = <1>;
};

gpio-controller property

gpio-controllerThis property is used toidentify a device node as a GPIO controller

A GPIO controller is a hardware module or driver responsible for managing and controlling GPIO pins

gpio-controllerThis property typically appears as an attribute of a device node, located in the device node’s property list.

When a device node is identified as a GPIO controller, it usually defines a set of GPIO pins and provides related GPIO control and configuration functions. Other device nodes can use this GPIO controller to control and manage their GPIO pins.

#gpio-cellsproperty

#gpio-cellsThis property is used tospecify the encoding method of GPIO pin descriptors. GPIO pin descriptors are a set of values used to identify and configure GPIO pins, such as pin numbers, pin attributes, etc.

#gpio-cellsThe property value of this attribute is an integer, indicating the encoding used forGPIO pin descriptorThe number of cells. Usually this value is 2.

1
2
3
4
5
ft5x06: ft5x06@38 {
.....
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
.....
};

gpio-ranges property

Background

EachGPIO inside the controllerassigns to each of its pins alocal number, for example, internal GPIO controller numbers: 0, 1, 2, 3, … 31

Howeverin the entire system or in other hardware modules, these GPIOs may have their ownglobal numberexternal numbers: 100, 101, 102, 103, … 131

That is,The internal and external numbers of the controller may be inconsistent.

To facilitate other devices using GPIO, a mapping table is needed to map the controller’s internal numbers to the system’s external numbers. This is thegpio-rangesfunction.

gpio-rangesThe attribute is a device tree property used for**describing GPIO range mapping.**It is typically used to describe GPIO controllers with a large number of GPIO pins, simplifying the encoding and access of GPIO pins.
gpio-rangesThe attribute is a list containing a series of integer values, each corresponding to a GPIO controller in the device tree. Each integer value in the list provides the following information in a specific order:

  • The starting value of the external pin number
  • The starting value of the GPIO controller’s internal local number
  • The size of the pin range (number of pins)

Example:

1
2
3
4
5
ft5x06: ft5x06@38 {
.....
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
.....
};
  • <&pinctrl>Indicates a reference to the pin controller node named pinctrl
  • 0 0 32Indicates
    • External pins start from 0
    • The controller’s local number starts from 0
    • A total of 32 pins are mapped.

GPIO description attributes and gpio-cells

1
2
3
4
5
ft5x06: ft5x06@38 {
.....
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
.....
};

The number of GPIO pin description attributes is determined by#gpio-cellsbecause in the gpio0 node,#gpio-cellsthe attribute is set to 2, so the number of GPIO pin description attributes in the device tree above is also 2.

Among them,RK_PB6is defined in the kernel source directory underinclude/dt-bindings/pinctrl/rockchip.hthe header file, which defines macro definitions for RK pin names and GPIO numbers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __DT_BINDINGS_ROCKCHIP_PINCTRL_H__
#define __DT_BINDINGS_ROCKCHIP_PINCTRL_H__

#define RK_PA0 0
#define RK_PA1 1
#define RK_PA2 2
#define RK_PA3 3
#define RK_PA4 4
#define RK_PA5 5
#define RK_PA6 6
#define RK_PA7 7
#define RK_PB0 8
#define RK_PB1 9
#define RK_PB2 10
#define RK_PB3 11
#define RK_PB4 12
#define RK_PB5 13
#define RK_PB6 14

GPIO_ACTIVE_LOWis defined in the source directory underinclude/dt-bindings/gpio/gpio.hindicating it is set to low level. Similarly,GPIO_ACTIVE_HIGHindicates setting this GPIO to high level. However, this is only a description of the device; the actual configuration must match the driver.

Other attributes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gpio-controller@00000000 {
compatible = "foo";
reg = <0x00000000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
ngpios = <18>;
gpio-reserved-ranges = <0 4>, <12 2>;
gpio-line-names = "MMC-CD", "MMC-WP",
"voD eth", "RST eth", "LED R",
"LED G", "LED B", "col A",
"col B", "col C", "col D",
"NMI button", "Row A", "Row B",
"Row C", "Row D", "poweroff",
"reset";
}
  • ngpios attribute

Specifies thenumber of GPIO pins supported by the GPIO controller. It indicates the total number of GPIO pins available on the device.

In this example,ngpiosthe value is 18, meaning the GPIO controller supports 18 GPIO pins.

  • The gpio-reserved-ranges property

**defines the reserved GPIO ranges.**Each range is represented by two integer values enclosed in angle brackets.

A reserved GPIO range means these GPIO pins are unavailable or have been reserved by other devices or functions.

In this example, there are two reserved ranges: <0 4> and <12 2>. <0 4> indicates that 4 consecutive pins starting from pin 0 are reserved, while <12 2> indicates that 2 consecutive pins starting from pin 12 are reserved.

  • The gpio-line-names property

**defines the names of the GPIO pins,**separated by commas. Each name corresponds to a GPIO pin. These names are used to identify and recognize the function or connected device of each GPIO pin.

In this example,gpio-line-namesthe property lists the names of multiple GPIO pins, such as “MMC-CD”, “MMC-WP”, “voD eth”, and so on. Through these names, the function or purpose of each GPIO pin can be clearly understood.

Example

Topeet RK3568 LED Schematic
Topeet RK3568 LED Schematic

From the above schematic, the pin net label of the LED can be obtained asWorking_LEDEN_H_GPIO0_B7, and the corresponding pin isGPIO0_B7

Then, check the kernel source directory for thedrivers/leds/leds-gpio.cfile, which is the LED driver file, and find the part related to the compatible match value, as shown below:

1
2
3
4
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", },
{},
};

It can be seen that the compatible match value is gpio-leds.

Finally, in the kernel source directory, theinclude/dt-bindings/pinctrl/rockchip.hheader file defines macros for RK pin names and GPIO numbers, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Header providing constants for Rockchip pinctrl bindings.
*
* Copyright (c) 2013 MundoReader S.L.
* Author: Heiko Stuebner <heiko@sntech.de>
*/

#ifndef __DT_BINDINGS_ROCKCHIP_PINCTRL_H__
#define __DT_BINDINGS_ROCKCHIP_PINCTRL_H__

#define RK_PA0 0
#define RK_PA1 1
#define RK_PA2 2
#define RK_PA3 3
#define RK_PA4 4
#define RK_PA5 5
#define RK_PA6 6
#define RK_PA7 7
#define RK_PB0 8
#define RK_PB1 9
#define RK_PB2 10
#define RK_PB3 11
#define RK_PB4 12
#define RK_PB5 13
#define RK_PB6 14
#define RK_PB7 15
#define RK_PC0 16
#define RK_PC1 17
#define RK_PC2 18
#define RK_PC3 19
#define RK_PC4 20
#define RK_PC5 21
#define RK_PC6 22
#define RK_PC7 23
#define RK_PD0 24
#define RK_PD1 25
#define RK_PD2 26
#define RK_PD3 27
#define RK_PD4 28
#define RK_PD5 29
#define RK_PD6 30
#define RK_PD7 31

#define RK_FUNC_GPIO 0

#endif

include/dt-bindings/gpio/gpio.hThe file defines macros for pin polarity settings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* SPDX-License-Identifier: GPL-2.0 */
/*
* This header provides constants for most GPIO bindings.
*
* Most GPIO bindings include a flags cell as part of the GPIO specifier.
* In most cases, the format of the flags cell uses the standard values
* defined in this header.
*/

#ifndef _DT_BINDINGS_GPIO_GPIO_H
#define _DT_BINDINGS_GPIO_GPIO_H

/* Bit 0 express polarity */
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1

/* Bit 1 express single-endedness */
#define GPIO_PUSH_PULL 0
#define GPIO_SINGLE_ENDED 2

/* Bit 2 express Open drain or open source */
#define GPIO_LINE_OPEN_SOURCE 0
#define GPIO_LINE_OPEN_DRAIN 4

/*
* Open Drain/Collector is the combination of single-ended open drain interface.
* Open Source/Emitter is the combination of single-ended open source interface.
*/
#define GPIO_OPEN_DRAIN (GPIO_SINGLE_ENDED | GPIO_LINE_OPEN_DRAIN)
#define GPIO_OPEN_SOURCE (GPIO_SINGLE_ENDED | GPIO_LINE_OPEN_SOURCE)

/* Bit 3 express GPIO suspend/resume and reset persistence */
#define GPIO_PERSISTENT 0
#define GPIO_TRANSITORY 8

/* Bit 4 express pull up */
#define GPIO_PULL_UP 16

/* Bit 5 express pull down */
#define GPIO_PULL_DOWN 32

#endif

Therefore, the device tree is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
/dts-v1/;

#include "dt-bindings/pinctrl/rockchip.h"
#include "dt-bindings/gpio/gpio.h"

/{
model = "This is my devicetree!";

led: led@1 {
compatible = "gpio-leds";
gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>
};
};

&gpio0is a reference to the pin controller,RK_PB7is the pin number or identifier,GPIO_ACTIVE_HIGHindicates that the active level of this GPIO pin is high level.

Comparison with other SoCs

NXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gpio1: gpio@0209c000 {
compatible = "fsl,inx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;

edt-ft5x06@38 {
compatible = "edt,edt-ft5306", "edt,edt-ft5x06", "edt,edt-ft5406";
pinctrl-names = "default";
pinctrl-0 = <&ts_int_pin &ts_reset_pin>;
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <9 0>;
reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
irq-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
status = "disabled";
};
};

samsung

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gpio_c: gpioc {
compatible = "gpio-controller";
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};

ft5x06: ft5x06038 {
compatible = "edt,edt-ft5406";
reg = <0x38>;
pinctrl-names = "default";
#if defined(RGB_1024x600) || defined(RGB_800x480)
pinctrl-0 = <&tsc2007_irq>;
interrupt-parent = <&gpio_c>;
interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
#endif
#if defined(LvDs_800×1280) || defined(LvDS_1024x768)
pinctrl-0 = <&gt911_irq>;
interrupt-parent = <&gpio_b>;
interrupts = <29 IRQ_TYPE_EDGE_FALLING>;
#endif
reset-gpios = <&gpio_e 30 0>;
};

Case analysis: pinctrl

Introduction to pinmux

**Pinmux (pin multiplexing) refers to the process of configuring and managing pin functions in a system.**In many modern integrated circuits,**a single pin can have multiple functions,**such as GPIO, UART, SPI, or I2C. By using pin multiplexing, it is possible to switch between these different functions.

Pin multiplexing is implemented through both hardware and software methods.

  • At the hardware level,

chip designs provide multiple function options for each pin. These functions are typically defined by the chip manufacturer in the chip specification documentation. By programming registers or switches, a specific function can be selected to connect to the pin. This hardware-level configuration is usually managed by thePin ControllerorPin Mux Controller.

Pin definitions in the chip manual
Pin definitions in the chip manual

  • At the software level,

the operating system or device drivers need to understand and configure pin functions. They use Device Tree or Device Tree Bindings to describe and configure pin functions. In the Device Tree, the multiplexing function of a pin can be specified, connecting it to a specific hardware interface or function. The operating system or device driver parses the Device Tree during startup and initializes and sets the pins according to the configuration.

From the above diagram, it can be seenUART4_RX_M1The corresponding pins can be multiplexed into the following 6 functionsLCDC_D16VOP_BT1120_D7GMAC1_RXD0_M0UART4_RX_M1PWM8_M0GPIO3_B1_d, and the corresponding BGA pin label is AG1

InBGA (Ball Grid Array) package, pin labels are identifiers used to uniquely identify each pin. These labels are typically defined by the chip manufacturer and provided in the chip’s specification documents or datasheets.

The pin labels of a BGA chip usually consist of a combination of letters and numbers. They are used to mark the pads on the bottom of the chip package. Each pin label corresponds to a function or signal inside the chip, so as to correctly connect to the target position on the printed circuit board (PCB). The pin label diagram of the RK3568 is shown below:

RK3568 Pin Label Diagram
RK3568 Pin Label Diagram

It can be seen that the vertical direction has 28 letter-type labels from A to AH, and the horizontal direction has 28 number-type labels from 1 to 28. Rockchip has also added a multiplexing function diagram based on BGA positions in the corresponding 3568 datasheet, part of which is shown in the figure below.

Multiplexing Function Diagram Based on BGA Positions
Multiplexing Function Diagram Based on BGA Positions

Among them, the black boxes represent reserved pins, the other colored boxes are generally power and ground, and the white boxes represent pins with specific multiplexing functions.

Using pinctrl to Set Multiplexing Relationships

pinctrl (pin control) is used to describe and configure the pin functions and connection methods on hardware devices. It is part of the device tree, used to pass pin configuration information to the operating system and device drivers during the boot process, so as to correctly initialize and control the pins.

In the device tree, pinctrl (pin control) uses the concepts of client and server to describe the relationships and configurations of pin control.

Client

1
2
3
4
node {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
}

In the above example,pinctrl-namesThe attribute defines a state name: default.

pinctrl-0The attribute specifies the pin configuration corresponding to the first state, default.

<&pinctrl_hog_1>is a pin descriptor that references a pin controller node namedpinctrl_hog_1. This means that in the default state, the device’s pin configuration will use the configuration defined in thepinctrl_hog_1node.

1
2
3
4
5
node {
pinctrl-names = "default", "wake up";
pinctrl-0 = <&pinctrl_hog_1>;
pinctrl-1 = <&pinctrl_hog_2>;
}

In the example,pinctrl-namesthe attribute defines two state names: default and wake up.

pinctrl-0The attribute specifies the pin configuration corresponding to the first state, default, referencing thepinctrl_hog_1node.

pinctrl-1The attribute specifies the pin configuration corresponding to the second state, wake up, referencing thepinctrl_hog_2node.

This means the device can be in one of two different states, each using a different pin configuration.

1
2
3
4
node {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>;
}

In this example,pinctrl-namesthe attribute still defines a state name: default.

pinctrl-0The attribute specifies the pin configuration corresponding to the first state, default, but unlike the previous example, it references two pin descriptors:pinctrl_hog_1andpinctrl_hog_2

This means that in the default state, the device’s pin configuration will usepinctrl_hog_1andpinctrl_hog_2the configurations defined in the two nodes.

This method cancombine the configurations of multiple pin controllers togetherto meet the pin requirements in a specific state.

Server

The server is the part of the device tree that defines pin configurations. It contains pin groups and pin descriptors, providing pin configuration options for the client.

The server defines the pinctrl node in the device tree, which contains the definitions of pin groups and pin descriptors.

Here, Rockchip’s RK3568 is taken as an example to explain the pinctrl server. To facilitate users in setting pin multiplexing relationships through pinctrl, the Rockchip BSP engineers wrote the configurations containing all multiplexing relationships in the kernel directory’sarch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsidevice tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
* Copyright (c) 2020 Rockchip Electronics Co., Ltd.
*/

#include <dt-bindings/pinctrl/rockchip.h>
#include "rockchip-pinconf.dtsi"

/*
* This file is auto generated by pin2dts tool, please keep these code
* by adding changes at end of this file.
*/
&pinctrl {
acodec {
/omit-if-no-ref/
acodec_pins: acodec-pins {
rockchip,pins =
/* acodec_adc_sync */
<1 RK_PB1 5 &pcfg_pull_none>,
/* acodec_adcclk */
<1 RK_PA1 5 &pcfg_pull_none>,
/* acodec_adcdata */
<1 RK_PA0 5 &pcfg_pull_none>,
/* acodec_dac_datal */
<1 RK_PA7 5 &pcfg_pull_none>,
/* acodec_dac_datar */
<1 RK_PB0 5 &pcfg_pull_none>,
/* acodec_dacclk */
<1 RK_PA3 5 &pcfg_pull_none>,
/* acodec_dacsync */
<1 RK_PA5 5 &pcfg_pull_none>;
};
};

In the pinctrl node, it is the multiplexing function of each node. Then we take the pin multiplexing of uart4 as an example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
uart4 {
/omit-if-no-ref/
uart4m0_xfer: uart4m0-xfer {
rockchip,pins =
/* uart4_rxm0 */
<1 RK_PA4 2 &pcfg_pull_up>,
/* uart4_txm0 */
<1 RK_PA6 2 &pcfg_pull_up>;
};

/omit-if-no-ref/
uart4m0_ctsn: uart4m0-ctsn {
rockchip,pins =
/* uart4m0_ctsn */
<1 RK_PA7 2 &pcfg_pull_none>;
};

/omit-if-no-ref/
uart4m0_rtsn: uart4m0-rtsn {
rockchip,pins =
/* uart4m0_rtsn */
<1 RK_PA5 2 &pcfg_pull_none>;
};

/omit-if-no-ref/
uart4m1_xfer: uart4m1-xfer {
rockchip,pins =
/* uart4_rxm1 */
<3 RK_PB1 4 &pcfg_pull_up>,
/* uart4_txm1 */
<3 RK_PB2 4 &pcfg_pull_up>;
};
};

Among them,<3 RK_PB1 4 &pcfg_pull_up>and<3 RK_PB2 4 &pcfg_pull_up>respectively indicate setting the PB1 pin of GPIO3 to function 4, and setting the PB2 pin of GPIO3 to function 4 as well, with the electrical properties set to pull-up. By looking up the schematic, the BGA package positions of the two pins are AG1 and AF2 respectively.

Function 4
Function 4

It can be seen that Function 4 corresponds to the transmit and receive ends of UART 4, and the configuration of the pinctrl server corresponds one-to-one with the pin multiplexing functions in the datasheet.

If you want to setRK_PB1andRK_PB2to GPIO function, how should it be configured? From the figure above, it can be seen that GPIO corresponds to Function 0, so the following pinctrl content can be used to setRK_PB1andRK_PB2to GPIO function (in fact, if no function multiplexing is performed on this pin, it will be set to GPIO function by default):

  • <3 RK_PB1 0 &pcfg_pull_up>
  • <3 RK_PB2 0 &pcfg_pull_up>

Finally, let’s look at the client’s reference to the uart4 server. The specific content is in the kernel source directoryarch/arm64/boot/dts/rockchip/topeet-rk3568-linux.dts

1
2
3
4
5
&uart4{
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart4m1_xfer>;
}

By referencing the server’s pin descriptors in the client, the device tree can associate the pin configurations of the client and server.

pinctrl Example Writing

In the SDK source directory, thedevice/rockchip/rk356x/BoardConfig-rk3568-evb1-ddr4-v10.mkdefault configuration file can reveal that the compiled device tree isrk3568-evb1-ddr4-v10-linux.dts, and the inclusion relationships between device trees are listed as follows:

Inclusion Relationships Between Device Trees
Inclusion Relationships Between Device Trees

The above device tree is for the 4.x version of the kernel

The LED has been properly configured in the device tree:

arch/arm64/boot/dts/rockchip/topeet-rk3568-linux.dts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//LED
#if LED_PWM
leds { compatible = "pwm-leds";
work {
pwms = <&pwm0 0 500000 0>;
linux,default-trigger = "heartbeat";
default-state = "on";
};
};
#else
leds {
compatible = "gpio-leds";
work {
gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "on";
};
};
#endif

Pinctrl is not configured here, so why can the LED still work normally? As mentioned above, in the RK3568, when the GPIO0_B7 pin is not multiplexed for any function, it defaults to GPIO functionality. Therefore, even without pinctrl, the LED can function normally.

We can write our own LED node:

1
2
3
4
5
6
my_led: led {
compatible = "topeet,led";
gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&rk_led_gpio>;
}

Server

1
2
3
4
5
rk_led{
rk_led_gpio:rk-led-gpio {
rockchip,pins = <0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
};
};

DTB File Format Parsing

The Device Tree Blob (DTB) format is a flat binary encoding of device tree data. It is used to exchange device tree data between software programs. For example, when booting an operating system, firmware passes the DTB to the OS kernel.

The DTB format encodes device tree data in a single, linear, pointer-free data structure.

It consists ofa small headerand three variable-sized parts:memory reservation blockstructure blockandstrings block. These should appear in the flattened device tree in that order. Therefore, the device tree structure as a whole, when loaded into a memory address, will resemble the following diagram

DTB
DTB

Take the following device tree file as an example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/dts-v1/;

/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;

chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};

aliases {
led1 = "/gpio@22020101";
};

node1 {
#address-cells = <1>;
#size-cells = <1>;
gpio@22020102 {
reg = <0x20220102 0x40>;
};
};

node2 {
node1-child {
pinnum = <01234>;
};
};

gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

After compiling into dtb, open it with Binary Viewer:

Binary Viewer
Binary Viewer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct fdt_header {
uint32_t magic; // Magic number of the device tree header

uint32_t totalsize; // Total size of the device tree file

uint32_t off_dt_struct; // Offset of the device tree structure (node data) relative to the beginning of the file

uint32_t off_dt_strings; // Offset of the device tree strings table relative to the beginning of the file

uint32_t off_mem_rsvmap; // Offset of the memory reservation map relative to the beginning of the file

uint32_t version; // Device tree version number

uint32_t last_comp_version; // Last compatible version number

uint32_t boot_cpuid_phys; // Physical ID of the boot CPU

uint32_t size_dt_strings; // Size of the device tree strings table

uint32_t size_dt_struct; // Size of the device tree structure (node data)
};
Core function classificationFieldKey Description
File IdentifiermagicFixed magic number of DTB (0xd00dfeed, big-endian), used to verify file validity
Size and Offset PositioningtotalsizeTotal size of DTB (including all blocks and gaps), determines file read range
off_dt_structOffset of the structure block (stores hardware nodes/properties), core entry point for parsing hardware description
off_dt_stringsOffset of the strings block (stores property names), used with indices in the structure block to retrieve property names
off_mem_rsvmapOffset of the memory reservation block (marks non-allocatable memory regions), avoids kernel memory conflicts
size_dt_stringsLength of the strings block, used to read the complete set of property names
size_dt_structLength of the structure block, used to read the complete hardware description data
Version CompatibilityversionFormat version of the current DTB, determines parsing logic
last_comp_versionMinimum compatible version for backward compatibility, ensures compatibility across kernel versions
Hardware Associationboot_cpuid_physThe physical ID of the boot CPU, corresponding to the CPU node in the device treeregattribute, used for core identification in multi-core systems

Memory Reserved Block

A Memory Reserved Block is a list of protected and reserved physical memory regions for client programs.

These reserved regionsshould not be used for general memory allocation, but rather to protect important data structures from being overwritten by client programs

The purpose of the memory reserved block is to ensure that specific memory regions are not modified or used during the execution of client programs. Since no memory reserved block is set in the example device tree, the corresponding regions are all 0.
  • Reserved Region List: The memory reserved block is a list consisting of a set of 64-bit big-endian integer pairs. Each pair of integers corresponds to a reserved memory region, containing the physical address and the size of the region (in bytes). These reserved regions should not overlap with each other.

Each reserved region in the memory reserved block is represented by a 64-bit big-endian integer pair. Each pair is represented by the following C structure

1
2
3
4
struct fdt_reserve_entry {
uint64_t address;
uint64_t size;
};

The first integer represents the physical address of the reserved region, and the second integer represents the size of the reserved region (in bytes). Each integer is represented in 64-bit form, even on 32-bit architectures. On 32-bit CPUs, the high 32 bits of the integer are ignored.

  • Purpose of Reserved Regions: Client programs should not access the reserved regions in the memory reserved block unless other information provided by the bootloader explicitly indicates that access is allowed. The bootloader may use specific methods to indicate that client programs can access parts of the reserved memory. The bootloader may describe the specific purposes of the reserved memory in documentation, optional extensions, or platform-specific documentation.

The memory reserved block provides the device tree with the ability to protect and reserve physical memory regions. It ensures that specific memory regions are not modified or used during the execution of client programs. This ensures that the bootloader and other critical components can access specific parts of the reserved memory when needed, and protects critical data structures from accidental modification.

Structure Block

The structure block is the part of the device tree that describes the structure and content of the device tree itself. Itconsists of a sequence of tokens with data, organized in a linear tree structure

Token Types

The tokens in the structure block are divided into five types, each used for a different purpose.

  • FDT_BEGIN_NODE (0x00000001)

FDT_BEGIN_NODETokenIndicates the start of a node. It is followed by the node’s unit name as additional data. The node name is stored as a null-terminated string and may include a unit address. After the node name, zero bytes may be needed for alignment, followed by the next token, which can be any token exceptFDT_END.

  • FDT_END_NODE (0x00000002)

FDT_END_NODETokenIndicates the end of a node. This token has no additional data, and is immediately followed by the next token, which can be any token exceptFDT_PROP.

  • FDT_PROP (0x00000003)

FDT_PROPTokenIndicates the start of a property in the device tree. It is followed by additional data describing the property, which first consists of the length and name of the property, represented as the following C structure

1
2
3
4
struct {
uint32_t len;
uint32_t nameoff;
}

The length indicates the byte length of the property value, and the name offset points to the location in the strings block where the property name is stored.
After this structure, the property value is given as a byte string. The property value may need to be padded with zero bytes for alignment, followed by the next token, which can be any token exceptFDT_ENDany token other than

  • FDT_NOP (0x00000004)

FDT_NOPThe token can be ignored by programs parsing the device tree. This token has no additional data, and is immediately followed by the next token, which can be any valid token. UsingFDT_NOPtoken can override a property or node definition in the tree, thereby removing it from the tree without needing to move other parts of the device tree blob.

Tree Structure

The structure of the device tree is represented as a linear tree. Each node begins with aFDT_BEGIN_NODEtoken and ends with aFDT_END_NODEtoken.

The properties and child nodes of a node are represented beforeFDT_END_NODE, so the child node’sFDT_BEGIN_NODEandFDT_END_NODEtokens are nested within the parent node’s tokens.

End of Structure Block

The structure block ends with a singleFDT_ENDmarker. This marker has no additional data; it is located at the end of the structure block and is the last marker in the structure block.
FDT_ENDThe bytes after the marker should be at the starting offset of the structure block, which equals the value of thesize_dt_structfield in the device tree blob header.

Strings Block

The strings block is used to store all property names used in the device tree. It consists of a series of null-terminated strings that are simply concatenated together within the strings block.

  • String Concatenation

Strings in the strings blockare concatenated with a null character (\0) as the terminator. This means each string ends with a null character, and the next string immediately follows the end of the previous one. This concatenation method forms all strings in the strings block into a continuous sequence of characters.

  • Offset Reference

In the structure block,property names reference the corresponding strings in the strings block via offsets. An offset is an unsigned integer value that indicates the position of a string within the strings block. By using offset references, the device tree saves space and becomes more flexible when property names change, as only the offsets need to be updated without modifying the property references in the structure block.

  • Alignment Constraints

**The strings block has no alignment constraints, meaning it can appear at any offset within the device tree blob.**This makes the position of the strings block flexible within the device tree blob, allowing adjustments as needed without affecting the parsing and processing of the device tree.

The strings block is the part of the device tree used to store property names. It is composed of concatenated strings and is referenced by offsets in the structure block. The flexible position of the strings block makes the device tree representation more compact and extensible.

dtb expanded into device_node

dtb expansion flowchart
dtb expansion flowchart

  • U-Boot loading

U-Boot (Universal Bootloader) is a commonly used open-source boot loader for booting embedded systems. During system startup, U-Boot loads theboot.imgbinary files of the kernel and device tree into specific addresses of system memory.

  • Kernel initialization

After U-Boot loads the binary files of the kernel and device tree into specific addresses of system memory, control is transferred to the kernel. During kernel initialization, the device tree binary file is parsed and expanded into data structures recognizable by the kernel, enabling the kernel to correctly initialize and manage hardware resources.

  • Device tree expansion

Device tree expansion refers to the process of parsing the device tree binary file into device nodes (struct device_node) within the kernel. The kernel reads the content of the device tree binary file and, based on the device tree’s description, constructs device tree data structures such as device nodes, interrupt controllers, registers, clocks, etc. These device tree data structures are used at runtime to manage and configure hardware resources.

struct device_nodeDefined as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// include/linux/of.h
typedef u32 phandle;
typedef u32 ihandle;

struct property {
char *name; // Property name
int length;// Length of attribute value (in bytes)
void *value;// Pointer to attribute value
struct property *next;// Pointer to next attribute node
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;// Attribute flags
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;// Unique identifier of attribute
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;// Kernel object binary attribute
#endif
};

#if defined(CONFIG_SPARC)
struct of_irq_controller;
#endif


struct device_node {
const char *name; //Name of device node
phandle phandle; // Handle of device node
const char *full_name;// Full name of device node
struct fwnode_handle fwnode;// Firmware node handle of device node

struct property *properties;// Attribute list of device node
struct property *deadprops; /* removed properties */ // Deleted attribute list
struct device_node *parent; // Pointer to parent device node
struct device_node *child; // Pointer to child device node
struct device_node *sibling; // Pointer to sibling device node
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj; // Kernel object (for sysfs)
#endif
unsigned long _flags; // Device node flags
void *data; // Data pointer associated with the device node
#if defined(CONFIG_SPARC)
unsigned int unique_id;// Unique identifier of the device node
struct of_irq_controller *irq_trans;// Interrupt controller of the device node
#endif
};

Source code analysis of the DTB parsing process

init/main.c

In the kernelstart_kernel()Called insetup_arch(&command_line);Perform architecture-specific initialization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// init/main.c
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
char *command_line;
char *after_dashes;

set_task_stack_end_magic(&init_task);// Set the magic number of the task stack
smp_setup_processor_id();// Set the processor ID
debug_objects_early_init();// Initialize the debug object

cgroup_init_early();// Initialize cgroup (control group)

local_irq_disable();// Disable local interrupts
early_boot_irqs_disabled = true;// Mark interrupts as disabled during early boot

/*
* Interrupts are still disabled。Perform necessary settings,Then enable them。
*/
boot_cpu_init();// Initialize the boot CPU
page_address_init();// Set page address
pr_notice("%s", linux_banner);// Print Linux kernel version information
early_security_init();
setup_arch(&command_line);// Architecture-specific initialization
setup_boot_config(command_line);
setup_command_line(command_line);// Set command line parameters
setup_nr_cpu_ids();// Set number of CPUs
setup_per_cpu_areas();// Set per-CPU area
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
boot_cpu_hotplug_init();// Initialize hot-pluggable boot CPU

build_all_zonelists(NULL);// Build list of all memory zones
page_alloc_init();// Initialize page allocator
...
}

arch/arm64/kernel/setup.c

While insetup_arch()the call at line 21 below insetup_machine_fdt(__fdt_pointer);Set up the machine’s FDT (Flattened Device Tree)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// arch/arm64/kernel/setup.c
void __init __no_sanitize_address setup_arch(char **cmdline_p)
{
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = (unsigned long) _end;

*cmdline_p = boot_command_line;

/*
* If know now we are going to need KPTI then use non-global
* mappings from the start, avoiding the cost of rewriting
* everything later.
*/
arm64_use_ng_mappings = kaslr_requires_kpti();

early_fixmap_init();// Initialize early fixmap
early_ioremap_init();// Initialize early ioremap

setup_machine_fdt(__fdt_pointer);// Set up the machine's FDT (Flattened Device Tree)

/*
* Initialise the static keys early as they may be enabled by the
* cpufeature code and early parameters.
*/
jump_label_init();// Initialize static keys, which may be enabled early by cpufeature code and early parameters
parse_early_param();
/*
* Unmask asynchronous aborts and fiq after bringing up possible
* earlycon. (Report possible System Errors once we can report this
* occurred).
*/
// Unmask asynchronous interrupts and FIQ after starting the possible early console (so we can immediately report system errors that occur)
local_daif_restore(DAIF_PROCCTX_NOIRQ);

/*
* TTBR0 is only used for the identity mapping at this stage. Make it
* point to zero page to avoid speculatively fetching new entries.
*/
cpu_uninstall_idmap();// At this stage, TTBR0 is only used for identity mapping. Point it to the zero page to avoid speculative new entries being fetched.

xen_early_init();// Early initialization for Xen platform
efi_init();// Initialization for EFI platform

if (!efi_enabled(EFI_BOOT) && ((u64)_text % MIN_KIMG_ALIGN) != 0)
pr_warn(FW_BUG "Kernel image misaligned at boot, please fix your bootloader!");

arm64_memblock_init();// Initialization of ARM64 memory blocks

paging_init();// Paging initialization

acpi_table_upgrade();// Upgrade of ACPI tables

/* Parse the ACPI tables for possible boot-time configuration */
acpi_boot_table_init();// Parse ACPI tables for possible boot-time configuration

if (acpi_disabled)
unflatten_device_tree();// Unflatten device tree

bootmem_init();// Initialization of boot memory

...
}

setup_machine_fdt(__fdt_pointer)

__fdt_pointeris the address where the dtb binary file is loaded into memory, which is passed by the bootloader to the kernel via the x0 register during startup

arch/arm64/kernel/head.S

1
2
3
4
5
6
7
8
9
// arch/arm64/kernel/head.S
SYM_CODE_START_LOCAL(preserve_boot_args)
mov x21, x0 // x21=FDT
...

SYM_FUNC_START_LOCAL(__primary_switched)
...
str_l x21, __fdt_pointer, x5 // Save FDT pointer
...

arch/arm64/kernel/setup.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// arch/arm64/kernel/setup.c
// Initialize and set up the machine's device tree
static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
int size;
// Map the physical address of the device tree to the kernel virtual address space
void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
const char *name;

if (dt_virt)// If the mapping is successful
memblock_reserve(dt_phys, size);// Reserve the memory region occupied by the device tree

if (!dt_virt || !early_init_dt_scan(dt_virt)) {// If the device tree mapping fails or the device tree parsing fails
pr_crit("\n"
"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
"\nPlease check your bootloader.",
&dt_phys, dt_virt);

while (true)// Infinite loop, waiting for system crash
cpu_relax();
}

/* Early fixups are done, map the FDT as read-only now */
fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);// Map the device tree in read-only mode

name = of_flat_dt_get_machine_name();// Get the machine name from the device tree
if (!name)
return;

pr_info("Machine model: %s\n", name);// Output machine model information
dump_stack_set_arch_desc("%s (DT)", name);// Set the architecture description for stack dump to the machine model
}

Line 13 aboveearly_init_dt_scanThe function validates the compatibility and integrity of the device tree. It may check for consistency markers, version information, and the presence of required nodes and attributes in the device tree. If validation fails, the function returns false. The function is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
// drivers/of/fdt.c
bool __init early_init_dt_scan(void *params)
{
bool status;

status = early_init_dt_verify(params);// Validate the compatibility and integrity of the device tree
if (!status)
return false;

early_init_dt_scan_nodes();// Scan device tree nodes
return true;
}

early_init_dt_scanFirst calledearly_init_dt_verifyValidate the device tree, verifying its compatibility and integrity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// drivers/of/fdt.c
bool __init early_init_dt_verify(void *params)
{
if (!params)// Verify whether the passed parameters are null
return false;

/* check device tree validity */
// Check the validity of the device tree header
// If the device tree header is invalid, return false
if (fdt_check_header(params))
return false;

/* Setup flat device-tree pointer */
initial_boot_params = params;// Set the pointer to the device tree as the passed parameter
// Calculate the CRC32 checksum of the device tree
// And save the result in the global variable of_fdt_crc32
of_fdt_crc32 = crc32_be(~0, initial_boot_params,
fdt_totalsize(initial_boot_params));
return true;
}

Finallyearly_init_dt_scanCalledearly_init_dt_scan_nodesScan the device tree nodes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// drivers/of/fdt.c
void __init early_init_dt_scan_nodes(void)
{
int rc = 0;

/* Retrieve various information from the /chosen node */
rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);/* Retrieve various information from the /chosen node */
if (!rc)
pr_warn("No chosen node found, continuing without\n");

/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);/* Initialize the {size,address}-cells information */

/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);/* Set memory information, call early_init_dt_add_memory_arch function */
}

functionearly_init_dt_scan_nodesis declared as__init, whichindicates that it is called during the kernel initialization phase,and is no longer needed after initialization is complete. The purpose of this function is to scan device tree nodes in the early stage and perform some initialization operations.

The function mainly callsof_scan_flat_dtfunction, which is used to scan the flat device tree. The flat device tree is adata structure that represents the device tree in a compact form,It does not use a tree structure,but uses a linear structure,to save memory space

unflatten_device_tree()

This function is used to parse the device tree, converting the compact device tree data structure into a tree-structured device tree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// drivers/of/fdt.c
void __init unflatten_device_tree(void)
{
/* parse the device tree */
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);

/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
/* Obtain pointers to the "/chosen" and "/aliases" nodes for global use */
of_alias_scan(early_init_dt_alloc_memory_arch);

/* Run unit tests for the device tree */
unittest_unflatten_overlay_base();
}

This function is mainly used to parse the device tree and store the parsed device tree in a global variableof_root.

The function first calls the__unflatten_device_treefunction to perform the device tree parsing operation. The parsed device tree will be stored using theof_rootpointer.

Next, the function calls theof_alias_scanfunction. This function is used to scan the/chosenand/aliasesnodes in the device tree and allocate memory for them. This way, other parts of the code can access these nodes through global variables.

Finally, the function calls theunittest_unflatten_overlay_basefunction to run unit tests for the device tree.

__unflatten_device_tree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//drivers/of/fdt.c
/**
* __unflatten_device_tree - create tree of device_nodes from flat blob
*
* unflattens a device-tree, creating the
* tree of struct device_node. It also fills the "name" and "type"
* pointers of the nodes so the normal device-tree walking functions
* can be used.
* @blob: The blob to expand
* @dad: Parent device node
* @mynodes: The device_node tree created by the call
* @dt_alloc: An allocator that provides a virtual address to memory
* for the resulting tree
* @detached: if true set OF_DETACHED on @mynodes
*
* Returns NULL on failure or the memory chunk containing the unflattened
* device tree on success.
*/
void *__unflatten_device_tree(const void *blob,
struct device_node *dad,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align),
bool detached)
{
int size;
void *mem;

pr_debug(" -> unflatten_device_tree()\n");

if (!blob) {
pr_debug("No device tree pointer\n");
return NULL;
}

pr_debug("Unflattening device tree:\n");
pr_debug("magic: %08x\n", fdt_magic(blob));
pr_debug("size: %08x\n", fdt_totalsize(blob));
pr_debug("version: %08x\n", fdt_version(blob));

if (fdt_check_header(blob)) {
pr_err("Invalid device tree blob header\n");
return NULL;
}

/* First pass, scan for size */
size = unflatten_dt_nodes(blob, NULL, dad, NULL);/* First pass scan to calculate size */
if (size < 0)
return NULL;

size = ALIGN(size, 4);
pr_debug(" size is %d, allocating...\n", size);

/* Allocate memory for the expanded device tree */
mem = dt_alloc(size + 4, __alignof__(struct device_node));/* Allocate memory for the expanded device tree */
if (!mem)
return NULL;

memset(mem, 0, size);

*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

pr_debug(" unflattening %p...\n", mem);

/* Second pass, do actual unflattening */
unflatten_dt_nodes(blob, mem, dad, mynodes);/* Second scan, actually unflatten the device tree */
if (be32_to_cpup(mem + size) != 0xdeadbeef)
pr_warn("End of tree marker overwritten: %08x\n",
be32_to_cpup(mem + size));

if (detached && mynodes) {
of_node_set_flag(*mynodes, OF_DETACHED);
pr_debug("unflattened tree is detached\n");
}

pr_debug(" <- unflatten_device_tree()\n");
return mem;
}

The focus of this function is on two scans of the device tree. The purpose of the first scan is to calculate the memory size required for unflattening the device tree.

Line 46:size = unflatten_dt_nodes(blob, NULL, dad, NULL);The function recursively traverses the device tree data block and calculates the memory size required for unflattening the device tree. It takes four parameters:

  • blob (pointer to the device tree data block)
  • start (starting address of the current node, initially NULL)
  • dad (parent node pointer, andunflatten_device_treeNULL is passed in the function)
  • mynodes (pointer to an array for storing node pointers, initially NULL).

After the first scan is completed,unflatten_dt_nodesthe function willreturn the memory size required for unflattening the device tree,then perform alignment on the size,and allocate memory for the unflattened device tree

Line 65: called againunflatten_dt_nodes(blob, mem, dad, mynodes);The function performs a second scan. Through this process,the second scan expands the nodes in the device tree data block into real device nodes,,and fills in the node names,、types, attributes, and other information.This completes the device tree expansion process.

unflatten_dt_nodes()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// drivers/of/fdt.c
/**
* unflatten_dt_nodes - Alloc and populate a device_node from the flat tree
* @blob: The parent device tree blob
* @mem: Memory chunk to use for allocating device nodes and properties
* @dad: Parent struct device_node
* @nodepp: The device_node tree created by the call
*
* It returns the size of unflattened device tree or error code
*/
static int unflatten_dt_nodes(const void *blob,
void *mem,
struct device_node *dad,
struct device_node **nodepp)
{
struct device_node *root;// Root node
int offset = 0, depth = 0, initial_depth = 0;// Offset, depth, and initial depth
#define FDT_MAX_DEPTH 64 // Maximum depth
struct device_node *nps[FDT_MAX_DEPTH];// Device node array
void *base = mem;// Base address, used to calculate offsets
bool dryrun = !base;// Whether it is just a simulation run without actual processing

if (nodepp)
*nodepp = NULL;// If the pointer is not null, set it to a null pointer

/*
* We're unflattening device sub-tree if @dad is valid. There are
* possibly multiple nodes in the first level of depth. We need
* set @depth to 1 to make fdt_next_node() happy as it bails
* immediately when negative @depth is found. Otherwise, the device
* nodes except the first one won't be unflattened successfully.
*/
/*
* If @dad valid,it indicates that the device subtree is being expanded。
* There may be multiple nodes at the first depth level。
* Set @depth to 1,so that fdt_next_node() works properly。
* When a negative @depth is found,the function will exit immediately。
* Otherwise,device nodes other than the first node will not be successfully expanded。
*/
if (dad)
depth = initial_depth = 1;

root = dad;// The root node is @dad
nps[depth] = dad;// Put the root node into the device node array

for (offset = 0;
offset >= 0 && depth >= initial_depth;
offset = fdt_next_node(blob, offset, &depth)) {
if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH - 1))
continue;
// If CONFIG is not enabled_OF_KOBJ and the node is unavailable, skip this node
if (!IS_ENABLED(CONFIG_OF_KOBJ) &&
!of_fdt_device_is_available(blob, offset))
continue;
// Fill in node information and add child nodes to the device node array
if (!populate_node(blob, offset, &mem, nps[depth],
&nps[depth+1], dryrun))
return mem - base;

if (!dryrun && nodepp && !*nodepp)
*nodepp = nps[depth+1];// Assign the child node pointer to @nodepp
if (!dryrun && !root)
root = nps[depth+1];// If the root node is null, set the child node as the root node
}

if (offset < 0 && offset != -FDT_ERR_NOTFOUND) {
pr_err("Error %d processing FDT\n", offset);
return -EINVAL;
}

/*
* Reverse the child list. Some drivers assumes node order matches .dts
* node order
*/
if (!dryrun)// Reverse the child node list. Some drivers assume the node order matches that in the .dts file
reverse_nodes(root);

return mem - base;// Return the number of bytes processed
}

fdt_next_node()The function is used to traverse the nodes of the device tree.

Start with offset 0, and execute the loop as long as the offset is greater than or equal to 0 and the depth is greater than or equal to the initial depth.

Each iteration of the loop processes a device tree node. In each iteration, first check if the depth exceeds the maximum depthFDT_MAX_DEPTHIf it does, skip the node.

If not enabledCONFIG_OF_KOBJand the node is unavailable (determined by theof_fdt_device_is_available()function), skip the node.

Then call thepopulate_node()function to populate node information and add child nodes to the device node arraynps.populate_node()The function definition is as follows

populate_node

This function parses the attributes of a device node and allocates memory as needed to store the attribute values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// drivers/of/fdt.c
static bool populate_node(const void *blob,
int offset,
void **mem,
struct device_node *dad,
struct device_node **pnp,
bool dryrun)
{
struct device_node *np;// Device node pointer
const char *pathp;// Node path string pointer
unsigned int l, allocl;// Path string length and allocated memory size

pathp = fdt_get_name(blob, offset, &l);// Get node path and length
if (!pathp) {
*pnp = NULL;
return false;
}

allocl = ++l;// Allocate memory size as path length plus one to store the node path string

np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
__alignof__(struct device_node));// Allocate device node memory
if (!dryrun) {
char *fn;
of_node_init(np);// Initialize device node
np->full_name = fn = ((char *)np) + sizeof(*np);// Set the full path name of the device node

memcpy(fn, pathp, l);// Copy the node path string into the full path name of the device node

if (dad != NULL) {
np->parent = dad;// Set the parent node of the device node
np->sibling = dad->child;// Set the sibling node of the device node
dad->child = np;// Add the device node as a child of the parent node
}
}

populate_properties(blob, offset, mem, np, pathp, dryrun);// Fill in the attribute information of the device node
if (!dryrun) {
np->name = of_get_property(np, "name", NULL);// Get the name attribute of the device node
if (!np->name)
np->name = "<NULL>";
}

*pnp = np;// Assign the device node pointer to *pnp
return true;
}

Inpopulate_nodefunction, first callunflatten_dt_allocfunction to allocate memory for the device node.

The allocated memory size issizeof(struct device_node) + alloclbytes, and use__alignof__(struct device_node)alignment, then callpopulate_propertiesfunction to fill in the attribute information of the device node.

Herenp->name = of_get_property(np, "name", NULL);get the name attribute of the device node and store it instruct device_node’snameattribute

device_node to platform_device

In the platform bus model, the device part is described usingplatform_devicestructure to describe hardware resources, so the kernel will ultimately convert the kernel-recognizeddevice_nodetree conversionplatform_device, but

not all device_node will be converted to platform_device

only those that meet the requirements will be converted toplatform_device, converted toplatform_devicenodes can be viewed under/sys/bus/platform/devices.

Conversion rules

The rules are as follows:

  • Traverse the child nodes under the root node that contain the compatible attribute, and for each child node, create a correspondingplatform_device
  • Traverse the nodes under the root node whose compatible attribute issimple-bussimple-mfdorisanodes and their child nodes. If their child nodes contain a compatible attribute value, a correspondingplatform_device
  • Check whether the node’s compatible attribute containsarmorprimecell. If so, do not convert this node toplatform_device, but recognize it as an AMBA device.

For Rule 2:

Some nodes, although themselves “buses” or “containers”, do not directly correspond to hardware devices but are used to organize child devices. The compatible values of these nodes are typically:

  • simple-bus: Generic simple bus (e.g., ordinary memory-mapped buses other than AMBA APB/AHB)
  • simple-mfd: Multi-Function Device container
  • isa: ISA bus (legacy PC)

For such nodes, they themselves usually do not createplatform_device, but will recursively traverse all their child nodes, and as long as a child node has a compatible property, createplatform_device

Purpose: To support the “bus nesting” structure in the device tree. For example, peripherals inside an SoC are attached under a simple-bus node.

For Rule 3:

If a node’s compatible string containsarm,xxxorprimecell(e.g.,arm,pl011arm,pl081), indicating it is an ARM PrimeCell peripheral, belonging to AMBA bus devices (APB/AHB). Such devices:

  • are not registered asplatform_device, but are handled by the AMBA bus subsystem (amba_bus_type) specifically
  • using theamba_devicestructure, rather than theplatform_device

Reason: ARM PrimeCell devices have standard register layouts (e.g., CID/PID), and the AMBA subsystem automatically detects and verifies them, which is more secure and efficient than genericplatform_device.

Example

Example 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};

aliases {
led1 = "/gpio@22020101";
};

node1 {
#address-cells = <1>;
#size-cells = <1>;
gpio@22020102 {
reg = <0x20220102 0x40>;
};
};

node2 {
node1-child {
pinnum = <01234>;
};
};

gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
}

In the device tree above, there are a total ofchosencpu1: cpu@1aliasesnode1node2gpio@22020101these six nodes. Among them, the first five nodes do not have a compatible property, so they will not be converted toplatform_device, while the lastgpio@22020101node meets rule one, being under the root node and having a compatible property, so it will ultimately be converted toplatform_device

Example 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/dts-v1/;

/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};

aliases {
led1 = "/gpio@22020101";
};

node1 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
gpio@22020102 {
reg = <0x20220102 0x40>;
};
};

node2 {
node1-child {
pinnum = <01234>;
};
};

gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

Here, in thenode1node, a compatible attribute is added, but the value of this compatible attribute issimple-bus, we need to continue looking at its child nodes. The child nodegpio@22020102does not have a compatible attribute value, so thenode1node will not be converted.

Example 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/dts-v1/;

/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};

aliases {
led1 = "/gpio@22020101";
};

node1 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";

gpio@22020102 {
compatible = "gpio";
reg = <0x20220102 0x40>;
};
};

node2 {
node1-child {
pinnum = <01234>;
};
};

gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

Here, in the child nodegpio@22020102of node1, a compatible attribute is added. The compatible attribute value of node1 issimple-bus, and then we need to continue looking at its child nodes. The child nodegpio@22020102has a compatible attribute value of gpio, so thegpio@22020102node will be converted toplatform_device

Example 4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

cpul: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;

amba {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges;

dmac_peri: dma-controller@ff250000 {
compatible = "arm,p1330", "arm,primecell";
reg = <0x0 0xff250000 0x0 0x4000>;
interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>;
arm,pl330-broken-no-flushp;
arm,p1330-periph-burst;
clocks = <&cru ACLK DMAC_PERI>;
clock-names = "apb_pclk";
};

dmac_bus: dma-controller@ff600000 {
compatible = "arm,p1330", "arm,primecell";
reg = <0x0 0xff600000 0x0 0x4000>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>;
arm,pl330-broken-no-flushp;
arm,pl330-periph-burst;
clocks = <&cru ACLK_DMAC_BUS>;
clock-names = "apb_pclk";
};
};
};
};

The compatible value of the amba node issimple-bus, and it will not be converted toplatform_device, but instead serves as a parent node for organizing other devices, so we need to examine its child nodes.

dmac_peri: dma-controller@ff250000Node: The compatible property of this node containsarm,p1330andarm,primecell, according to rule 3, this node will not be converted toplatform_device, but is recognized as an AMBA device.

dmac_bus: dma-controller@ff600000Node: The compatible property of this node containsarm,p1330andarm,primecell, according to rule 3, this node will not be converted toplatform_device, but is recognized as an AMBA device.

Source Code Analysis

of_platform_default_populate_init()

First, let’s look atof_platform_default_populate_init, which usesarch_initcall_sync(of_platform_default_populate_init);to register this function for invocation during the boot phase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// drivers/of/platform.c
static int __init of_platform_default_populate_init(void)
{
struct device_node *node;
// Pause device link supplier synchronization state
device_links_supplier_sync_state_pause();

// If the device tree has not been populated yet, return an error code
if (!of_have_populated_dt())
return -ENODEV;

/*
* Handle certain compatibles explicitly, since we don't want to create
* platform_devices for every node in /reserved-memory with a
* "compatible",
*/
/*
* Explicitly handle certaincompatibles,because we do not want to/reserved-memory each node in“compatible”create a node forplatform_device。
*/
for_each_matching_node(node, reserved_mem_matches)
of_platform_device_create(node, NULL, NULL);
// find the node "/firmware"
node = of_find_node_by_path("/firmware");
if (node) {
// use this node to populate device tree platform devices
of_platform_populate(node, NULL, NULL, NULL);
of_node_put(node);
}

/* Populate everything else. */
of_platform_default_populate(NULL, NULL, NULL);// populate other devices

return 0;
}

arch_initcall_sync(of_platform_default_populate_init);

arch_initcall_syncis a function in the Linux kernel used to execute architecture-specific initialization functions during kernel initialization. It belongs to the kernel’s initialization call mechanism, ensuring that architecture-specific initialization functions are called at the appropriate time during system startup.

During the initialization of the Linux kernel, various subsystems and architectures register their own initialization functions. These initialization functions are responsible for completing initialization tasks specific to a subsystem or architecture, such as initializing hardware devices, registering interrupt handlers, setting up memory mappings, etc. And arch_initcall_syncfunction is used to call initialization functions related to the current architecture.

When the kernel starts, it callsrest_init()function to start the initialization process. During initialization,arch_initcall_syncfunction is called to ensure that all initialization functions related to the current architecture are executed in the correct order. This ensures that architecture-specific initialization tasks are completed correctly during startup.

And of_platform_default_populate_initfunction is to automatically parse the device tree during kernel initialization,**and create corresponding devices based on the device nodes in the device treeplatform_devicestructure. It will traverse the device nodes in the device tree and create a correspondingplatform_devicestructure, then register it into the kernel, enabling device drivers to recognize and operate these devices.

of_platform_default_populate_initThe function ultimately callsof_platform_default_populatepopulate other devices

1
2
3
4
5
6
7
8
// drivers/of/platform.c
int of_platform_default_populate(struct device_node *root,
const struct of_dev_auxdata *lookup,
struct device *parent)
{
return of_platform_populate(root, of_default_bus_match_table, lookup,
parent);
}

The purpose of this function is to callof_platform_populatefunction to populate the platform devices in the device tree, and uses the default device match tableof_default_bus_match_table, the content of the device match table is as follows:

1
2
3
4
5
6
7
8
9
10
// drivers/of/platform.c
const struct of_device_id of_default_bus_match_table[] = {
{ .compatible = "simple-bus", },
{ .compatible = "simple-mfd", },
{ .compatible = "isa", },
#ifdef CONFIG_ARM_AMBA
{ .compatible = "arm,amba-bus", },
#endif /* CONFIG_ARM_AMBA */
{} /* Empty terminated list */
};

The above device match table corresponds to rule 2:

Traverse nodes under the root node that contain compatible attribute as simple-bus、simple-mfd or isa nodes and their child nodes。If their child nodes contain compatible attribute value

The function will automatically match the corresponding device driver based on the attributes of the device tree node and populate the kernel’s platform device list.

of_platform_populate()

of_platform_populateThe function is defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// drivers/of/platform.c
/**
* of_platform_populate() - Populate platform_devices from device tree data
* @root: parent of the first level to probe or NULL for the root of the tree
* @matches: match table, NULL to use the default
* @lookup: auxdata table for matching id and platform_data with device nodes
* @parent: parent to hook devices from, NULL for toplevel
*
* Similar to of_platform_bus_probe(), this function walks the device tree
* and creates devices from nodes. It differs in that it follows the modern
* convention of requiring all device nodes to have a 'compatible' property,
* and it is suitable for creating devices which are children of the root
* node (of_platform_bus_probe will only create children of the root which
* are selected by the @matches argument).
*
* New board support should be using this function instead of
* of_platform_bus_probe().
*
* Returns 0 on success, < 0 on failure.
*/
int of_platform_populate(struct device_node *root,
const struct of_device_id *matches,
const struct of_dev_auxdata *lookup,
struct device *parent)
{
struct device_node *child;
int rc = 0;
// If root is not null, increment the reference count of the root node; otherwise, find the root node in the device tree by path.
root = root ? of_node_get(root) : of_find_node_by_path("/");
if (!root)
return -EINVAL;

pr_debug("%s()\n", __func__);
pr_debug(" starting at: %pOF\n", root);
// Suspend device link supplier sync state.
device_links_supplier_sync_state_pause();
// Traverse all child nodes of the root node.
for_each_child_of_node(root, child) {
// Create platform devices and add them to the device tree bus.
rc = of_platform_bus_create(child, matches, lookup, parent, true);
if (rc) {
of_node_put(child);
break;
}
}
// Resume device link supplier sync state.
device_links_supplier_sync_state_resume();

// Set the OF of the root node._POPULATED_BUS flag.
of_node_set_flag(root, OF_POPULATED_BUS);

// Release the reference count of the root node.
of_node_put(root);
return rc;
}
EXPORT_SYMBOL_GPL(of_platform_populate);

of_platform_populateBy callingrc = of_platform_bus_create(child, matches, lookup, parent, true);Createplatform_device

of_platform_bus_create()

of_platform_bus_createDefined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// drivers/of/platform.c
/**
* of_platform_bus_create() - Create a device for a node and its children.
* @bus: device node of the bus to instantiate
* @matches: match table for bus nodes
* @lookup: auxdata table for matching id and platform_data with device nodes
* @parent: parent for new device, or NULL for top level.
* @strict: require compatible property
*
* Creates a platform_device for the provided device_node, and optionally
* recursively create devices for all the child nodes.
*/
static int of_platform_bus_create(struct device_node *bus,
const struct of_device_id *matches,
const struct of_dev_auxdata *lookup,
struct device *parent, bool strict)
{
const struct of_dev_auxdata *auxdata;
struct device_node *child;
struct platform_device *dev;
const char *bus_id = NULL;
void *platform_data = NULL;
int rc = 0;

/* Make sure it has a compatible property */
/* Ensure the device node has a compatible property. */
if (strict && (!of_get_property(bus, "compatible", NULL))) {
pr_debug("%s() - skipping %pOF, no compatible prop\n",
__func__, bus);
return 0;
}

/* Skip nodes for which we don't want to create devices */
/* Skip nodes for which devices are not to be created. */
if (unlikely(of_match_node(of_skipped_node_table, bus))) {
pr_debug("%s() - skipping %pOF node\n", __func__, bus);
return 0;
}

if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
pr_debug("%s() - skipping %pOF, already populated\n",
__func__, bus);
return 0;
}

auxdata = of_dev_lookup(lookup, bus);
if (auxdata) {
bus_id = auxdata->name;
platform_data = auxdata->platform_data;
}

if (of_device_is_compatible(bus, "arm,primecell")) {
/*
* Don't return an error here to keep compatibility with older
* device tree files.
*/
/*
* Do not return an error here to maintain compatibility with old device tree files.。
*/
of_amba_device_create(bus, bus_id, platform_data, parent);
return 0;
}

dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
if (!dev || !of_match_node(matches, bus))
return 0;

for_each_child_of_node(bus, child) {
pr_debug(" create child: %pOF\n", child);
rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
if (rc) {
of_node_put(child);
break;
}
}
of_node_set_flag(bus, OF_POPULATED_BUS);
return rc;
}

of_platform_bus_createBy callingof_match_node(of_skipped_node_table, bus)match the nodes to be skipped,of_skipped_node_tabledefined as follows

1
2
3
4
static const struct of_device_id of_skipped_node_table[] = {
{ .compatible = "operating-points-v2", },
{} /* Empty terminated list */
};

operating-points-v2

This is a Device Tree Binding used to describe the Operating Performance Points (OPP) of a CPU or device, such as different frequency/voltage combinations. It is not a hardware device, but a data table (describing performance states) that typically appears as a child node under CPU, GPU, or SoC device nodes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpu@0 {
compatible = "arm,cortex-a53";
...
operating-points-v2 {
compatible = "operating-points-v2";
opp00 {
opp-hz = /bits/ 64 <1000000000>;
opp-microvolt = <900000>;
};
opp01 {
opp-hz = /bits/ 64 <1500000000>;
opp-microvolt = <1000000>;
};
};
};

Subsequentlyof_platform_bus_createcall theof_platform_device_create_pdatafunction to create a platform device and assign it to the variable dev. Then, check whether the device node bus matches the given match tablematchesmatches. If the platform device creation fails or the device node does not match, return 0.

Finallyof_platform_bus_createusefor_each_child_of_node(bus, child), traverse each child node child of the device node bus, and recursively call theof_platform_bus_createfunction to create platform devices for the child nodes.

of_platform_device_create_pdata()

of_platform_device_create_pdatadefined as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// drivers/of/platform.c

/**
* of_platform_device_create_pdata - Alloc, initialize and register an of_device
* @np: pointer to node to create device for
* @bus_id: name to assign device
* @platform_data: pointer to populate platform_data pointer with
* @parent: Linux device model parent device.
*
* Returns pointer to created platform device, or NULL if a device was not
* registered. Unavailable devices will not get registered.
*/
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np,
const char *bus_id,
void *platform_data,
struct device *parent)
{
struct platform_device *dev;
/* check whether the device node is available or already populated */
if (!of_device_is_available(np) ||
of_node_test_and_set_flag(np, OF_POPULATED))
return NULL;
/* allocate a platform device structure */
dev = of_device_alloc(np, bus_id, parent);
if (!dev)
goto err_clear_flag;
/* Set some properties of the platform device */
dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
if (!dev->dev.dma_mask)
dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
dev->dev.bus = &platform_bus_type;
dev->dev.platform_data = platform_data;
of_msi_configure(&dev->dev, dev->dev.of_node);

/* Add the platform device to the device model */
if (of_device_add(dev) != 0) {
platform_device_put(dev);
goto err_clear_flag;
}

return dev;

err_clear_flag:
of_node_clear_flag(np, OF_POPULATED);/* Clear the filled flag of the device node */
return NULL;
}

of_platform_device_create_pdataFunction callof_device_allocAllocate a platform device structure, and pass the device node pointer, device identifier, and parent device pointer to it. If allocation fails, jump toerr_clear_flaglabel for error handling.

Lines 29 to 34of_platform_device_create_pdataSet some properties of the platform device:

  • It sets thecoherent_dma_maskproperty to a 32-bit DMA bitmask
  • Check if thedma_maskproperty is NULL. Ifdma_maskis NULL, point it tocoherent_dma_mask
  • Set the bus type of the platform device toplatform_bus_type, and store the platform data pointer inplatform_datain the attribute.
  • Callof_msi_configureandof_reserved_mem_device_init_by_idxto configure the device’s MSI and reserved memory information

of_device_allocThe function is defined as follows

of_device_alloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* of_device_alloc - Allocate and initialize an of_device
* @np: device node to assign to device
* @bus_id: Name to assign to the device. May be null to use default name.
* @parent: Parent device.
*/
struct platform_device *of_device_alloc(struct device_node *np,
const char *bus_id,
struct device *parent)
{
struct platform_device *dev;
int rc, i, num_reg = 0, num_irq;
struct resource *res, temp_res;

dev = platform_device_alloc("", PLATFORM_DEVID_NONE);
if (!dev)
return NULL;

/* count the io and irq resources */
while (of_address_to_resource(np, num_reg, &temp_res) == 0)
num_reg++;
num_irq = of_irq_count(np);

/* Populate the resource table */
if (num_irq || num_reg) {
res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);
if (!res) {
platform_device_put(dev);
return NULL;
}

dev->num_resources = num_reg + num_irq;
dev->resource = res;
for (i = 0; i < num_reg; i++, res++) {
rc = of_address_to_resource(np, i, res);
WARN_ON(rc);
}
if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
pr_debug("not all legacy IRQ resources mapped for %pOFn\n",
np);
}

dev->dev.of_node = of_node_get(np);
dev->dev.fwnode = &np->fwnode;
dev->dev.parent = parent ? : &platform_bus;

if (bus_id)
dev_set_name(&dev->dev, "%s", bus_id);
else
of_device_make_bus_id(&dev->dev);

return dev;
}
EXPORT_SYMBOL(of_device_alloc);

It can be seen that it callsplatform_device_alloc("", PLATFORM_DEVID_NONE)createdplatform_device’snameis empty

platform under the device tree_device and platform_driver matching

of_match_table

In the platform bus model, only when one of the following three conditions is met will it correctly match and load the probe initialization function:

  1. platform_driver.driver.of_match_tablematches the value in the compatible property of the device tree
  2. platform_driver.id_tablematches the value in the compatible property of the device tree
  3. platform_driver.driver.nameandplatform_device.namematch

For the priority of matching, from the followingplatform_matchfunction, it can be seen:

  • Device tree matching is used first, i.e.,platform_driver.driver.of_match_table
  • then ACPI matching
  • thenplatform_driverinid_tablematching.
  • Finally, fall back toreturn (strcmp(pdev->name, drv->name) == 0);, i.e., matchingplatform_device.nameandplatform_driver.driver.name, and this name is a legacy field, which after device tree parsing is converted fromstruct device_nodetostruct platform_devicewhen converting thisnameis set to empty (of_device_alloc), meaning the one generated through the device treestruct platform_devicewill not use this field.

However,platform_driver.driver.namemust be set, otherwise a kernel panic will occur, becausestrcmp(pdev->name, drv->name)this line of code usesstrcmp, anddrv->namean error will be reported when it is NULL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// drivers/base/platform.c
/**
* platform_match - bind platform device to platform driver.
* @dev: device.
* @drv: driver.
*
* Platform device IDs are assumed to be encoded like this:
* "<name><instance>", where <name> is a short description of the type of
* device, like "pci" or "floppy", and <instance> is the enumerated
* instance of the device, like '0' or '42'. Driver IDs are simply
* "<name>". So, extract the <name> from the platform_device structure,
* and compare it against the name of the driver. Return whether they match
* or not.
*/
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);

/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);

/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;

/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;

/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;

/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}

Andof_driver_match_devicethe function definition is as follows:

1
2
3
4
5
6
7
8
9
10
11
// include/linux/of_device.h
/**
* of_driver_match_device - Tell if a driver's of_match_table matches a device.
* @drv: the device_driver structure to test
* @dev: the device structure to match against
*/
static inline int of_driver_match_device(struct device *dev,
const struct device_driver *drv)
{
return of_match_device(drv->of_match_table, dev) != NULL;
}

of_driver_match_devicecalls theof_match_devicefunction, with the first parameter passed asdrv->of_match_table, and this function is defined as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// drivers/of/device.c
/**
* of_match_device - Tell if a struct device matches an of_device_id list
* @matches: array of of device match structures to search in
* @dev: the of device structure to match against
*
* Used by a driver to check whether an platform_device present in the
* system is in its list of supported devices.
*/
const struct of_device_id *of_match_device(const struct of_device_id *matches,
const struct device *dev)
{
if ((!matches) || (!dev->of_node))
return NULL;
return of_match_node(matches, dev->of_node);
}
EXPORT_SYMBOL(of_match_device);

of_match_devicewhich in turn calls theof_match_nodefunction,of_match_nodeUsing a spin lock to__of_match_nodelock,__of_match_nodeis the actual matching function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// drivers/of/base.c
**
* of_match_node - Tell if a device_node has a matching of_match structure
* @matches: array of of device match structures to search in
* @node: the of device structure to match against
*
* Low level utility function used by device matching.
*/
const struct of_device_id *of_match_node(const struct of_device_id *matches,
const struct device_node *node)
{
const struct of_device_id *match;
unsigned long flags;

raw_spin_lock_irqsave(&devtree_lock, flags);
match = __of_match_node(matches, node);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return match;
}
EXPORT_SYMBOL(of_match_node);

static
const struct of_device_id *__of_match_node(const struct of_device_id *matches,
const struct device_node *node)
{
const struct of_device_id *best_match = NULL;
int score, best_score = 0;

if (!matches)
return NULL;

for (; matches->name[0] || matches->type[0] || matches->compatible[0]; matches++) {
score = __of_device_is_compatible(node, matches->compatible,
matches->type, matches->name);
if (score > best_score) {
best_match = matches;
best_score = score;
}
}

return best_match;
}

__of_match_nodecall__of_device_is_compatiblematch, with the parameters passed in order:

  • struct device_node *node
  • struct of_device_id *matches’scompatibletypename
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* __of_device_is_compatible() - Check if the node matches given constraints
* @device: pointer to node
* @compat: required compatible string, NULL or "" for any match
* @type: required device_type value, NULL or "" for any match
* @name: required node name, NULL or "" for any match
*
* Checks if the given @compat, @type and @name strings match the
* properties of the given @device. A constraints can be skipped by
* passing NULL or an empty string as the constraint.
*
* Returns 0 for no match, and a positive integer on match. The return
* value is a relative score with larger values indicating better
* matches. The score is weighted for the most specific compatible value
* to get the highest score. Matching type is next, followed by matching
* name. Practically speaking, this results in the following priority
* order for matches:
*
* 1. specific compatible && type && name
* 2. specific compatible && type
* 3. specific compatible && name
* 4. specific compatible
* 5. general compatible && type && name
* 6. general compatible && type
* 7. general compatible && name
* 8. general compatible
* 9. type && name
* 10. type
* 11. name
*/
static int __of_device_is_compatible(const struct device_node *device,
const char *compat, const char *type, const char *name)
{
struct property *prop;
const char *cp;
int index = 0, score = 0;

/* Compatible match has highest priority */
if (compat && compat[0]) {
prop = __of_find_property(device, "compatible", NULL);
for (cp = of_prop_next_string(prop, NULL); cp;
cp = of_prop_next_string(prop, cp), index++) {
if (of_compat_cmp(cp, compat, strlen(compat)) == 0) {
score = INT_MAX/2 - (index << 2);
break;
}
}
if (!score)
return 0;
}

/* Matching type is better than matching name */
if (type && type[0]) {
if (!__of_node_is_type(device, type))
return 0;
score += 2;
}

/* Matching name is a bit better than not */
if (name && name[0]) {
if (!of_node_name_eq(device, name))
return 0;
score++;
}

return score;
}

It can be seen thatof_device_id’scompatibleattribute has the highest priority, followed bytypeattribute, and finallynameattribute. The score is calculated based on these three attributesscore, the higher the score, the higher the match degree.

Andplatform_driverstruct nested within thedriverstruct’sof_match_tableThe attribute is a pointer toconst struct of_device_ida structure, used to describe the matching rules between device tree nodes and drivers.

1
2
3
4
5
6
7
8
9
10
// include/linux/mod_devicetable.h
/*
* Struct used for matching a device
*/
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};

struct of_device_idIt is usually defined as an array in the driver.,Used to describe the matching rules between device tree nodes and drivers.And:

The last element of the array must be an empty structure.,To mark the end of the array.

Example:

1
2
3
4
5
static const struct of_device_id my_driver_match[] = {
{ .compatible = "vendor,device-1" },
{ .compatible = "vendor,device-2" },
{ },
};

Example

Device Tree LevelDevice Tree NameDevice Tree Name
Top-Level Device Treerk3568-evb1-ddr4-v10-linux.dtsrk3568-evb1-ddr4-v10-linux.dts
Second-Level Device Treerk3568-evb1-ddr4-v10.dtsirk3568-linux.dtsi
Third-Level Device Treerk3568.dtsi
rk3568-evb.dtsi
topeet_screen_choose.dtsi
topeet_rk3568_lcds.dtsi

rk3568-evb1-ddr4-v10-linux.dtsis the top-level device tree

Add:

rk3568-evb1-ddr4-v10-linux.dts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
* Copyright (c) 2020 Rockchip Electronics Co., Ltd.
*
*/

#include "rk3568-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"

#include <dt-bindings/display/rockchip_vop.h>
/{
topeet{
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";

myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};
};
};

&vp0 {
cursor-win-id = <ROCKCHIP_VOP2_CLUSTER0>;
};

&vp1 {
cursor-win-id = <ROCKCHIP_VOP2_CLUSTER1>;
};


&uart7 {
status ="okay";
pinctrl-name = "default";
pinctrl-0 = <&uart7m1_xfer>;
};
&uart4 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart4m1_xfer>;
};
&uart9 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart9m1_xfer>;
};
&can1 {
status = "okay";
compatible = "rockchip,canfd-1.0";
assigned-clocks = <&cru CLK_CAN1>;
assigned-clock-rates = <150000000>; //If can bitrate lower than 3M,the clock-rates should set 100M,else set 200M.
pinctrl-names = "default";
pinctrl-0 = <&can1m1_pins>;
};

driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
// platform device initialization function
static int my_platform_probe(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_probe: Probing platform device\n");

// add device-specific operations
// ...

return 0;
}

// platform device removal function
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");

// clean up device-specific operations
// ...

return 0;
}


const struct of_device_id of_match_table_id[] = {
{.compatible="my devicetree"},
};

// define platform driver structure
static struct platform_driver my_platform_driver = {
.probe = my_platform_probe,
.remove = my_platform_remove,
.driver = {
.name = "my_platform_device",
.owner = THIS_MODULE,
.of_match_table = of_match_table_id,
},
};

// module initialization function
static int __init my_platform_driver_init(void)
{
int ret;

// register platform driver
ret = platform_driver_register(&my_platform_driver);
if (ret) {
printk(KERN_ERR "Failed to register platform driver\n");
return ret;
}

printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

return 0;
}

// module exit function
static void __exit my_platform_driver_exit(void)
{
// unregister platform driver
platform_driver_unregister(&my_platform_driver);

printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629");

of operations

get device tree node

of_find_by_name()

of_find_node_by_nameis a function in the Linux kernel used to find a device tree node by its name

ItemDescription
Function Definitionstruct device_node *of_find_node_by_name(struct device_node *from, const char *name);
Header File#include <linux/of.h>
Parameter fromStarting search node:
NULL: Start searching from the device tree root node
Non-NULL: Continue searching for nodes with the same name after this node
Parameter nameThe name of the device tree node to search for (node name, not compatible)
FunctionSearch for a node with a matching name in the device tree and return the correspondingdevice_nodestructure pointer
Return ValueFound: Returns the matching node’sstruct device_node *; Not found: returnNULL

of_find_node_by_path()

ItemDescription
Function Definitionstruct device_node *of_find_node_by_path(const char *path);
Header File#include <linux/of.h>
Parameter pathAbsolute path string of the device tree node, for example:/soc/gpio@ff720000/topeet/myLed
FunctionAccording to the device tree node’sabsolute pathfind the corresponding node, return the matchingstruct device_nodestructure pointer
Return ValueSuccess: returns a pointer tostruct device_nodepointer;
Failure: returnsNULL

of_get_parent()

itemDescription
Function Definitionstruct device_node *of_get_parent(const struct device_node *node);
Header File#include <linux/of.h>
Parameter nodeDevice tree node pointer of the node whose parent is to be obtained
FunctionGet the parent node of the specified node and return its correspondingdevice_nodestructure pointer
Return ValueSuccess: returns the parent node’sstruct device_nodepointer;
Failure or no parent node: returnsNULL

of_get_next_child()

itemDescription
Function Definitionstruct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);
Header File#include <linux/of.h>
Parameter nodePointer to the current device tree node, used to specify the parent node whose children are to be traversed
Parameter prevPointer to the previous child node; if it isNULL, returns the first child node
FunctionTraverse the child nodes of a device tree node; return all child nodes of the specified node one by one
Return ValueSuccess: returns a pointer to the next child nodestruct device_nodepointer;
No more child nodes: returnsNULL

for_each_child_of_node()

NULLDescription
Macro Definitionfor_each_child_of_node(parent, child)
Header File#include <linux/of.h>
Parameter parentParent device tree node pointer (struct device_node *
Parameter childChild node pointer variable (struct device_node *), used for iteration
FunctionIterate through all child nodes of the specified parent node
Return ValueNone (macro, used in for loop)

Example:

1
2
3
4
5
6
7
8
9
10
struct device_node *np = pdev->dev.of_node;
struct device_node *sub_np;

for_each_child_of_node(np, sub_np) {
/* sub_np will point to each sub-node in turn */
[...]
int size;
of_property_read_u32(client->dev.of_node,"size", &size);
[...]
}

of_find_compatible_node()

ProjectDescription
Function Definitionstruct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible);
Header File#include <linux/of.h>
Parameter fromSpecifies the node to start searching from; if it isNULL, the search starts from the root node of the device tree
Parameter typeThe device type string to match, can be used to match the node’sdevice_typeproperty; usually can be set toNULL
Parameter compatibleThecompatibleproperty string to match in the device tree
FunctionFinds the first node in the device tree that matches the specified compatible string; the return value can be used to continue searching for the next matching node
Return ValueSuccess: returns the matching nodestruct device_node*
Failure or non-existence: returnsNULL

of_find_matching_node_and_match()

ItemDescription
Function Definitionstruct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match);
Header File#include <linux/of.h>
Parameter fromSpecifies the node from which to start searching:
PassNULLindicates searching from the root node of the device tree;
Passing the previously returned node allows continuing to search for the next matching node
Parameter matchesPoints to a of_device_id[]match table, which contains entries for matching device tree nodescompatibleortypecondition
parameter matchoutput parameter, used to return the of_device_identry pointer; can be NULL
functionin the device tree, according tomatchesmatch table to find nodes that meet the conditions, and can return the corresponding match
return valuesuccess: returns the matchedstruct device_node *
failure: returnsNULL

example:

1
2
3
4
5
6
7
8
9
10
11
#include <linux/of.h>
static const struct of_device_id my_match_table[] = {
{ .compatible = "vendor,device" },
{ /* sentinel */ }
};

const struct of_device_id *match;
struct device_node *np;

// start searching for matching nodes from the root node
np = of_find_matching_node_and_match(NULL, my_match_table, &match)

first, define aof_device_idmatch tablemy_match_table, which contains a compatibility string ofvendor,devicematching items. Then, we useof_find_matching_node_and_matchfunction to find the matching node starting from the root node.

Example

Device Tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/{
test_device{
#address-cells =<1>;
#size-cells = <1>;
compatible = "simple-bus";

myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};

};
};

Driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/of.h>
static const struct of_device_id mynode_of_match[] = { { .compatible = "my devicetree" }, {} };

static int my_platform_driver_probe(struct platform_device *pdev)
{
struct device_node *mydev_node;
pr_info("my_platform_driver_probe: Probing platform device\n");

// Find device tree node by node name
mydev_node = of_find_node_by_name(NULL, "myLed");
pr_info("[of_find_node_by_name]: device node is %s\n", mydev_node->name);

// Find device tree node by node path
mydev_node = of_find_node_by_path("/test_device/myLed");
pr_info("[of_find_node_by_path]: device node is %s\n", mydev_node->name);

// Get parent node
mydev_node = of_get_parent(mydev_node);
pr_info("[of_find_node_by_path]: device node is %s\n", mydev_node->name);

// Get child node
mydev_node = of_get_next_child(mydev_node, NULL);
pr_info("[of_get_next_child]: device node is %s\n", mydev_node->name);

// Find node using compatible
mydev_node = of_find_compatible_node(NULL, NULL, "my devicetree");
pr_info("[of_find_compatible_node]: device node is %s\n", mydev_node->name);

// Using of_device_Find matching node using id match table
mydev_node = of_find_matching_node_and_match(NULL, mynode_of_match, NULL);
pr_info("[of_find_matching_node_and_match]: device node is %s\n", mydev_node->name);

return 0;
}

static int my_platform_driver_remove(struct platform_device *pdev)
{
return 0;
}

static const struct of_device_id match_table[] = { { .compatible = "my devicetree" }, {} };

static struct platform_driver my_platform_driver ={
.driver = {
.owner = THIS_MODULE,
.name = "my_platform_driver",
.of_match_table = match_table,
},
.probe = my_platform_driver_probe,
.remove = my_platform_driver_remove,

};

module_platform_driver(my_platform_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test sample for of api");

Test:

1
2
3
4
5
6
7
8
root@topeet:/root# insmod of_api_test.ko
[ 1230.073568] my_platform_driver_probe: Probing platform device
[ 1230.074042] [of_find_node_by_name]: device node is myLed
[ 1230.074156] [of_find_node_by_path]: device node is myLed
[ 1230.074163] [of_find_node_by_path]: device node is test_device
[ 1230.074170] [of_get_next_child]: device node is myLed
[ 1230.074461] [of_find_compatible_node]: device node is myLed
[ 1230.074717] [of_find_matching_node_and_match]: device node is myLed

Get device tree property

of_find_property()

ProjectDescription
Function Definitionstruct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
Header File#include <linux/of.h>
Parameter npDevice tree node to search for the property (struct device_nodepointer)
Parameter nameThe property name string to search for, e.g.,compatibleregstatus
Parameter lenpPointer tointpointer of type, used to return the byte length of the property value;
If the length is not needed, NULL can be passed
FunctionUnder the specified nodenpsearch for the property namednameattributes, and can return the length of the attribute value
Return valueSuccess: returns a pointer to the attribute structurestruct property *
Failure (attribute not found or invalid parameter): returns NULL

of_property_count_elems_of_size()

ItemDescription
Function definitionint of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size);
Header file#include <linux/of.h>
Parameter npDevice tree node pointer (struct device_node *), indicating the node from which to read the attribute
Parameter propnameAttribute name string, e.g.,reggpiosetc.
Parameter elem_sizeSize of a single element (in bytes), for example:sizeof(u32)→ Number of 32-bit integers in the computed attribute
FunctionCount the number of elements in the specified attribute (divided by the given element size)
Return ValueSuccess: returns the number of elements in the attribute
Attribute does not exist or has no content: returns0
Other errors: returnsnegative error code(for example,-EINVAL

of_property_read_u32_index()

ItemDescription
Function Definitionint of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value);
Header File#include <linux/of.h>
Parameter npDevice tree node pointer (struct device_node *), indicating the node whose property is to be read
Parameter propnameProperty name string, e.g.,reggpiosetc.
Parameter indexIndex of the property element (starting from 0), specifying which element to read
Parameter out_valuePointer tou32a variable of type, used to store the read value
FunctionRetrieve a 32-bit unsigned integer (u32) at the specified index from the given property
Return valueOn success: returns0out_valuestores the read value
Failure: returns a negative error code (attribute does not exist or read failure)

of_property_read_u64_index()

ItemDescription
Function Definitionstatic inline int of_property_read_u64_index(const struct device_node *np, const char *propname, u32 index, u64 *out_value);
Header File#include <linux/of.h>
Parameter npDevice tree node pointer (struct device_node *), indicating the node whose attribute is to be read
Parameter propnameAttribute name string, e.g.,reggpios, etc.
Parameter indexIndex of the attribute element (starting from 0), specifying which element to read
Parameter out_valuePointer tou64Pointer to a type variable, used to store the read value
FunctionGet a 64-bit unsigned integer (u64) at the specified index from the given property
Return valueSuccess: returns0out_valueSave the read value
Failure: returns a negative error code, e.g., property does not exist or read failed

of_property_read_variable_u32_array()

ItemDescription
Function definitionint of_property_read_variable_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t SZ_min, size_t SZ_max);
Header file#include <linux/of.h>
Parameter npDevice tree node pointer (struct device_node *), indicating the node whose property is to be read
Parameter propnameAttribute name string, e.g.,reggpiosetc.
Parameter out_valuesPointer tou32a pointer to an array of type, used to store the read values
Parameter SZ_minSpecifies the minimum number of elements in the array
Parameter SZ_maxSpecifies the maximum number of elements in the array
FunctionReads a variable-lengthu32array from the specified attribute and stores it intoout_values
Return valueSuccess: Returns the actual number of array elements read
Failure: Returns a negative error code, e.g., attribute does not exist or read failed
  • Read a variable-length u8 array from the specified attribute
1
2
int of_property_read_variable_u8_array(const struct device_node *np, const char *propname, u8 *out_values,
size_t SZ_min, size_t SZ_max)
  • Read a variable-length u16 array from the specified attribute
1
2
int of_property_read_variable_u16_array(const struct device_node *np, const char *propname, u16
*out_values, size_t SZ_min, size_t SZ_max)
  • Read a variable-length u64 array from the specified attribute
1
2
int of_property_read_variable_u64_array(const struct device_node *np, const char *propname, u64
*out_values, size_t SZ_min, size_t SZ_max)

of_property_read_string()

ItemDescription
Function Definitionstatic inline int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string);
Header File#include <linux/of.h>
Parameter npDevice tree node pointer (struct device_node *), indicating the node from which to read the attribute
Parameter propnameAttribute name string, e.g.,compatiblestatusetc.
Parameter out_stringPointer to a string pointer, used to store the read string
FunctionRead a string value from the specified attribute
Return ValueSuccess: returns 0
Failure: returns a negative error code, e.g., attribute does not exist or read failed

of_property_read_bool()

ItemDescription
Function Definitionstatic inline bool of_property_read_bool(const struct device_node *np, const char *propname);
Header File#include <linux/of.h>
Parameter npDevice tree node pointer (struct device_node *), indicating the node from which to read the attribute
Parameter propnameAttribute name string, e.g.,gpio-active-lowenableetc.
FunctionDetermine whether a specified node has a boolean type attribute
Return valueIf the attribute exists: returntrue
If it does not exist: returnfalse

Example

Device tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/{
test_device{
#address-cells =<1>;
#size-cells = <1>;
compatible = "simple-bus";

myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};

};
};

Driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/of.h>

static int my_platform_driver_probe(struct platform_device *pdev)
{
struct device_node *mydev_node;
int i, num;
u32 out_value_u32;
u64 out_value_u64;
u32 out_value_u32_array[2];
const char *value_compatible;
struct property *my_prop;

pr_info("my_platform_driver_probe: Probing platform device\n");

// Find a device tree node by name
mydev_node = of_find_node_by_name(NULL, "myLed");
pr_info("[of_find_node_by_name]: device node is %s\n", mydev_node->name);

// Find the compatible attribute
my_prop = of_find_property(mydev_node, "compatible", NULL);
pr_info("[of_find_property]: property name is %s\n", my_prop->name);

// Get the number of elements in the reg attribute
num = of_property_count_elems_of_size(mydev_node, "reg", sizeof(u32));
pr_info("[of_property_count_elems_of_size]: reg elem size is %d\n", num);

// Read the value of the reg attribute as u32
for (i = 0; i < num; i++) {
of_property_read_u32_index(mydev_node, "reg", i, &out_value_u32);
pr_info("[of_property_read_u32_index]: reg u32 value: 0x%X\n", out_value_u32);
}

// Read the value of the reg attribute as u64
of_property_read_u64_index(mydev_node, "reg", 0, &out_value_u64);
pr_info("[of_property_read_u64_index]: reg u64 value: 0x%llX\n", out_value_u64);

// Read the reg attribute as an array
of_property_read_variable_u32_array(mydev_node, "reg", out_value_u32_array, 1, 2);
pr_info("[of_property_read_variable_u32_array]: array[0] is 0x%X\n",
out_value_u32_array[0]);
pr_info("[of_property_read_variable_u32_array]: array[1] is 0x%X\n",
out_value_u32_array[1]);

// Read the string value of the compatible attribute
of_property_read_string(mydev_node, "compatible", &value_compatible);
pr_info("[of_property_read_string]: compatible string is %s\n", value_compatible);

return 0;
}

static int my_platform_driver_remove(struct platform_device *pdev)
{
return 0;
}

static const struct of_device_id of_match_table[] = { { .compatible = "my devicetree" }, {} };

static struct platform_driver my_platform_driver={
.driver={
.owner = THIS_MODULE,
.name = "my_platform_driver",
.of_match_table = of_match_table,
},
.probe = my_platform_driver_probe,
.remove = my_platform_driver_remove,
};

module_platform_driver(my_platform_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("this is a test sample for devicetree api: of_property");

Test:

1
2
3
4
5
6
7
8
9
10
11
root@topeet:/root# insmod of_api_property_test.ko
[ 1173.158496] my_platform_driver_probe: Probing platform device
[ 1173.158912] [of_find_node_by_name]: device node is myLed
[ 1173.158922] [of_find_property]: property name is compatible
[ 1173.158929] [of_property_count_elems_of_size]: reg elem size is 2
[ 1173.158935] [of_property_read_u32_index]: reg u32 value: 0xFDD60000
[ 1173.158941] [of_property_read_u32_index]: reg u32 value: 0x4
[ 1173.158947] [of_property_read_u64_index]: reg u64 value: 0xFDD6000000000004
[ 1173.158954] [of_property_read_variable_u32_array]: array[0] is 0xFDD60000
[ 1173.158958] [of_property_read_variable_u32_array]: array[1] is 0x4
[ 1173.158964] [of_property_read_string]: compatible string is my devicetree

ranges attribute

platform_get_Prerequisite for resource to obtain device tree resources

Since the device tree is converted into platform devices during system startup, we can directly use in the driver theplatform_get_resourcefunction to directly obtainplatform_deviceresources

Example:

Device tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/{
test_device{
#address-cells =<1>;
#size-cells = <1>;
compatible = "simple-bus";

myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};

};
};

Driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>

// Platform device initialization function
struct resource *myresources;

static int my_platform_probe(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_probe: Probing platform device\n");
// Obtain platform device resources
myresources = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (myresources == NULL) {
// If obtaining resources fails, print the value of value_compatible
printk("platform_get_resource is error\n");
}
printk("reg valus is %llx\n", myresources->start);
return 0;
}

// Platform device removal function
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");
// Clean up device-specific operations
// ...
return 0;
}

const struct of_device_id of_match_table_id[] = {
{ .compatible = "my devicetree" },
};

// Define platform driver structure
static struct platform_driver my_platform_driver = {
.probe = my_platform_probe,
.remove = my_platform_remove,
.driver = {
.name = "my_platform_device",
.owner = THIS_MODULE,
.of_match_table = of_match_table_id,
},
};

// Module initialization function
static int __init my_platform_driver_init(void)
{
int ret;
// Register platform driver
ret = platform_driver_register(&my_platform_driver);
if (ret) {
printk(KERN_ERR "Failed to register platform driver\n");
return ret;
}
printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");
return 0;
}

// Module exit function
static void __exit my_platform_driver_exit(void)
{
// Unregister platform driver
platform_driver_unregister(&my_platform_driver);
printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

This method will fail to load. The reason isplatform_get_resourceReturn NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// drivers/base/platform.c
/**
* platform_get_resource - get a resource for a device
* @dev: platform device
* @type: resource type
* @num: resource index
*
* Return: a pointer to the resource or NULL on failure.
*/
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num)
{
u32 i;

for (i = 0; i < dev->num_resources; i++) {
struct resource *r = &dev->resource[i];

if (type == resource_type(r) && num-- == 0)
return r;
}
return NULL;
}
EXPORT_SYMBOL_GPL(platform_get_resource);

There are two possibilities for returning NULL: one is that the above for loop is not entered and NULL is returned directly; the other is that the for loop is entered, but the type matching is incorrect, and NULL is returned after breaking out of the for loop.

The type here must match, so we need to find out why the for loop was not entered. There is only one possibility, which isdev->num_resources 为 0

Let’s look atof_platform_device_create_pdatathis function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// drivers/of/platform.c
/**
* of_platform_device_create_pdata - Alloc, initialize and register an of_device
* @np: pointer to node to create device for
* @bus_id: name to assign device
* @platform_data: pointer to populate platform_data pointer with
* @parent: Linux device model parent device.
*
* Returns pointer to created platform device, or NULL if a device was not
* registered. Unavailable devices will not get registered.
*/
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np,
const char *bus_id,
void *platform_data,
struct device *parent)
{
struct platform_device *dev;

/* Check if the device node is available or already populated */
if (!of_device_is_available(np) ||
of_node_test_and_set_flag(np, OF_POPULATED))
return NULL;

/* Allocate platform device structure */
dev = of_device_alloc(np, bus_id, parent);
if (!dev)
goto err_clear_flag;

/* Set some properties of the platform device */
dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
if (!dev->dev.dma_mask)
dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
dev->dev.bus = &platform_bus_type;
dev->dev.platform_data = platform_data;
of_msi_configure(&dev->dev, dev->dev.of_node);

/* Add the platform device to the device model */
if (of_device_add(dev) != 0) {
platform_device_put(dev);
goto err_clear_flag;
}

return dev;

err_clear_flag:
/* Clear the populated flag of the device node */
of_node_clear_flag(np, OF_POPULATED);
return NULL;
}

function callof_device_allocIt is this function that allocates a platform device structure and passes the device node pointer, device identifier, and parent device pointer to it.resource.num

of_device_allocThe function is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// drivers/of/platform.c
/**
* of_device_alloc - Allocate and initialize an of_device
* @np: device node to assign to device
* @bus_id: Name to assign to the device. May be null to use default name.
* @parent: Parent device.
*/
struct platform_device *of_device_alloc(struct device_node *np,
const char *bus_id,
struct device *parent)
{
struct platform_device *dev;
int rc, i, num_reg = 0, num_irq;
struct resource *res, temp_res;

dev = platform_device_alloc("", PLATFORM_DEVID_NONE);
if (!dev)
return NULL;

/* count the io and irq resources */
while (of_address_to_resource(np, num_reg, &temp_res) == 0)
num_reg++;
num_irq = of_irq_count(np);

/* Populate the resource table */
if (num_irq || num_reg) {
res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);
if (!res) {
platform_device_put(dev);
return NULL;
}

dev->num_resources = num_reg + num_irq;
dev->resource = res;
for (i = 0; i < num_reg; i++, res++) {
rc = of_address_to_resource(np, i, res);
WARN_ON(rc);
}
if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
pr_debug("not all legacy IRQ resources mapped for %pOFn\n",
np);
}

dev->dev.of_node = of_node_get(np);
dev->dev.fwnode = &np->fwnode;
dev->dev.parent = parent ? : &platform_bus;

if (bus_id)
dev_set_name(&dev->dev, "%s", bus_id);
else
of_device_make_bus_id(&dev->dev);

return dev;
}
EXPORT_SYMBOL(of_device_alloc);

At line 32dev->num_resources = num_reg + num_irq;That is, the number of reg and the number of irq. Since no interrupt-related attributes were added in the device tree,num_irqis 0, andnum_regis obtained by line 20 through this functionof_address_to_resource(np, num_reg, &temp_res) == 0obtained by loop countingnum_rg

1
2
3
/* count the io and irq resources */
while (of_address_to_resource(np, num_reg, &temp_res) == 0)
num_reg++;

of_address_to_resourceThe function definition is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// drivers/of/address.c
/**
* of_address_to_resource - Translate device tree address and return as resource
*
* Note that if your address is a PIO address, the conversion will fail if
* the physical address can't be internally converted to an IO token with
* pci_address_to_pio(), that is because it's either called too early or it
* can't be matched to any host bridge IO space
*/
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r)
{
const __be32 *addrp;
u64 size;
unsigned int flags;
const char *name = NULL;

addrp = of_get_address(dev, index, &size, &flags);
if (addrp == NULL)
return -EINVAL;

/* Get optional "reg-names" property to add a name to a resource */
of_property_read_string_index(dev, "reg-names", index, &name);

return __of_address_to_resource(dev, addrp, size, flags, name, r);
}
EXPORT_SYMBOL_GPL(of_address_to_resource);

Line 18 retrieves the address, size, and type of the reg attribute. Since the reg attribute already exists in the device tree, it will return correctly here.

Line 23 reads thereg-namesattribute. Since this attribute is not defined in the device tree, the function will have no effect.

The ultimately decisive function is the returned__of_address_to_resourcefunction. Jumping to its definition is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// drivers/of/address.c
static int __of_address_to_resource(struct device_node *dev,
const __be32 *addrp, u64 size, unsigned int flags,
const char *name, struct resource *r)
{
u64 taddr;

if (flags & IORESOURCE_MEM)
taddr = of_translate_address(dev, addrp);
else if (flags & IORESOURCE_IO)
taddr = of_translate_ioport(dev, addrp, size);
else
return -EINVAL;

if (taddr == OF_BAD_ADDR)
return -EINVAL;
memset(r, 0, sizeof(struct resource));

r->start = taddr;
r->end = taddr + size - 1;
r->flags = flags;
r->name = name ? name : dev->full_name;

return 0;
}

The flags of the reg attribute areIORESOURCE_MEM, so it will execute line 9’sof_translate_addressfunction, jump to this function, whose definition is as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// drivers/of/address.c
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
{
struct device_node *host;
u64 ret;

ret = __of_translate_address(dev, of_get_parent,
in_addr, "ranges", &host);
if (host) {
of_node_put(host);
return OF_BAD_ADDR;
}

return ret;
}
EXPORT_SYMBOL(of_translate_address);

The key point of this function is in line 7. The above function is actually__of_translate_addressa wrapper of the function, where the third parameter passed inranges is the focus we need to pay attention to. Continue to jump to the definition of this function, the specific content is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// drivers/of/address.c
/*
* Translate an address from the device-tree into a CPU physical address,
* this walks up the tree and applies the various bus mappings on the
* way.
*
* Note: We consider that crossing any level with #size-cells == 0 to mean
* that translation is impossible (that is we are not dealing with a value
* that can be mapped to a cpu physical address). This is not really specified
* that way, but this is traditionally the way IBM at least do things
*
* Whenever the translation fails, the *host pointer will be set to the
* device that had registered logical PIO mapping, and the return code is
* relative to that node.
*/
static u64 __of_translate_address(struct device_node *dev,
struct device_node *(*get_parent)(const struct device_node *),
const __be32 *in_addr, const char *rprop,
struct device_node **host)
{
struct device_node *parent = NULL;
struct of_bus *bus, *pbus;
__be32 addr[OF_MAX_ADDR_CELLS];
int na, ns, pna, pns;
u64 result = OF_BAD_ADDR;

pr_debug("** translation for device %pOF **\n", dev);

/* Increase refcount at current level */
of_node_get(dev);

*host = NULL;
/* Get parent & match bus type */
parent = get_parent(dev);
if (parent == NULL)
goto bail;
bus = of_match_bus(parent);

/* Count address cells & copy address locally */
bus->count_cells(dev, &na, &ns);
if (!OF_CHECK_COUNTS(na, ns)) {
pr_debug("Bad cell count for %pOF\n", dev);
goto bail;
}
memcpy(addr, in_addr, na * 4);

pr_debug("bus is %s (na=%d, ns=%d) on %pOF\n",
bus->name, na, ns, parent);
of_dump_addr("translating address:", addr, na);

/* Translate */
for (;;) {
struct logic_pio_hwaddr *iorange;

/* Switch to parent bus */
of_node_put(dev);
dev = parent;
parent = get_parent(dev);

/* If root, we have finished */
if (parent == NULL) {
pr_debug("reached root node\n");
result = of_read_number(addr, na);
break;
}

/*
* For indirectIO device which has no ranges property, get
* the address from reg directly.
*/
iorange = find_io_range_by_fwnode(&dev->fwnode);
if (iorange && (iorange->flags != LOGIC_PIO_CPU_MMIO)) {
result = of_read_number(addr + 1, na - 1);
pr_debug("indirectIO matched(%pOF) 0x%llx\n",
dev, result);
*host = of_node_get(dev);
break;
}

/* Get new parent bus and counts */
pbus = of_match_bus(parent);
pbus->count_cells(dev, &pna, &pns);
if (!OF_CHECK_COUNTS(pna, pns)) {
pr_err("Bad cell count for %pOF\n", dev);
break;
}

pr_debug("parent bus is %s (na=%d, ns=%d) on %pOF\n",
pbus->name, pna, pns, parent);

/* Apply bus translation */
if (of_translate_one(dev, bus, pbus, addr, na, ns, pna, rprop))
break;

/* Complete the move up one level */
na = pna;
ns = pns;
bus = pbus;

of_dump_addr("one level translation:", addr, na);
}
bail:
of_node_put(parent);
of_node_put(dev);

return result;
}

Lines 34 to 37, get the parent node and the matching bus type

Line 40, getaddress-cellandsize-cellsand store them in int variables na and ns respectively

Line 52 is a for loop, in which line 92 uses theof_translate_onefunction for conversion, whererpropparameter indicates the resource attribute to be converted, and its value is the passed-in string"ranges", then we continue to jump to this function, the specific content of which is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// drivers/of/address.c
static int of_translate_one(struct device_node *parent, struct of_bus *bus,
struct of_bus *pbus, __be32 *addr,
int na, int ns, int pna, const char *rprop)
{
const __be32 *ranges;
unsigned int rlen;
int rone;
u64 offset = OF_BAD_ADDR;

/*
* Normally, an absence of a "ranges" property means we are
* crossing a non-translatable boundary, and thus the addresses
* below the current cannot be converted to CPU physical ones.
* Unfortunately, while this is very clear in the spec, it's not
* what Apple understood, and they do have things like /uni-n or
* /ht nodes with no "ranges" property and a lot of perfectly
* useable mapped devices below them. Thus we treat the absence of
* "ranges" as equivalent to an empty "ranges" property which means
* a 1:1 translation at that level. It's up to the caller not to try
* to translate addresses that aren't supposed to be translated in
* the first place. --BenH.
*
* As far as we know, this damage only exists on Apple machines, so
* This code is only enabled on powerpc. --gcl
*
* This quirk also applies for 'dma-ranges' which frequently exist in
* child nodes without 'dma-ranges' in the parent nodes. --RobH
*/
ranges = of_get_property(parent, rprop, &rlen);
if (ranges == NULL && !of_empty_ranges_quirk(parent) &&
strcmp(rprop, "dma-ranges")) {
pr_debug("no ranges; cannot translate\n");
return 1;
}
if (ranges == NULL || rlen == 0) {
offset = of_read_number(addr, na);
memset(addr, 0, pna * 4);
pr_debug("empty ranges; 1:1 translation\n");
goto finish;
}

pr_debug("walking ranges...\n");

/* Now walk through the ranges */
rlen /= 4;
rone = na + pna + ns;
for (; rlen >= rone; rlen -= rone, ranges += rone) {
offset = bus->map(addr, ranges, na, ns, pna);
if (offset != OF_BAD_ADDR)
break;
}
if (offset == OF_BAD_ADDR) {
pr_debug("not found !\n");
return 1;
}
memcpy(addr, ranges + na, 4 * pna);

finish:
of_dump_addr("parent translation for:", addr, pna);
pr_debug("with offset: %llx\n", (unsigned long long)offset);

/* Translate it into parent bus space */
return pbus->translate(addr, offset, pna);
}

In line 30 of this function, use theof_get_propertyfunction to get"ranges"attribute, but since this attribute is not present in the device tree node we added, the ranges value here is NULL, the condition on line 34 holds true, and thus it returns 1.

Next, based on this return value, continue analyzing the parent function:

of_translate_oneAfter the function returns 1, the parent level’s_of_translate_addressreturn value isOF_BAD_ADDR;

The next higher level’sof_translate_addressreturn value is alsoOF_BAD_ADDR;

Continue searching upward__of_address_to_resourcefunction will return-EINVAL;

of_address_to_resourcereturns-EINVAL, sonum_regis 0;

At this point, regarding why theplatform_get_resourcefunction fails to obtain resources, the issue is found, simply because the parent node in its device tree does not have this attribute namedranges, so just add therangesattribute.

Introduction to the ranges property

rangesThe property is aproperty used to describe address mapping relationships between devices.It is used in the Device Tree todescribe how the child device’s address space maps to the parent device’s address space. The Device Tree is a hardware description language used to describe hardware components and their interconnections in embedded systems.

Each device node in the Device Tree can have arangesproperty, which contains address mapping information.

The common format is as follows:

1
ranges = <child-bus-address parent-bus-address length>;

or

1
ranges;
  • child-bus-address

The starting address of the child device’s address space.

It specifies the location of the child device within the parent device’s address space. The specific word length is determined by therangesnode’s#address-cellsproperty.

  • parent-bus-address

The starting address of the parent device’s address space.

It specifies the address range in the parent device used for mapping the child device. The specific word length is determined by therangesparent node’s#address-cellsattribute.

  • length

The size of the mapping. It specifies the length of the child device’s address space within the parent device’s address space. The specific word length is determined by therangesparent node’s#size-cellsattribute.

When therangesattribute’s value is empty, it indicates that the child device’s address space and the parent device’s address space have an identical mapping, i.e., a 1:1 mapping. This is typically used to describe memory regions where the child device and parent device share the same address range.

When therangesattribute’s value is not empty, the child device’s address space is mapped to the parent device’s address space according to the specified mapping rules. The specific mapping rules depend on the device tree structure and the device’s specific requirements.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
....

external-bus {
#address-cells = <2>;
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000
1 0 0x10160000 0x10000
2 0 0x30000000 0x30000000>;

// Chipselect 1, Ethernet
// Chipselect 2, i2c controller
// Chipselect 3, NOR Flash
.......
};
......
};

In theexternal-busnode, the#address-cellsattribute value of 2 indicates thatchild-bus-addressis represented by two values, namely 0 and 0, and the parent node’s#address-cellsattribute value and#size-cellsattribute value is 1, indicatingparent-bus-addressandlengthare both represented by 1, that is0x10100000and0x10000, thisrangesvalue indicates mapping the child address space (0x0-0xFFFF) to the parent address space0x10100000 - 0x1010FFFF, the example here is with parameterrangesattribute mapping, without parameter therangesattribute is a 1:1 mapping, which is simpler, so no example is given here.

In embedded systems, different devices may be connected to the same bus or bus controller, and they need to be correctly mapped in the physical address space for data exchange and communication.

For example, a device may be connected to the main processor or other devices via a bus, and the physical address ranges of these devices may differ. The ranges attribute is used to describe this address mapping relationship.

Device Classification

Memory-Mapped Devices

Memory-mapped devices refer todevices that can be directly accessed via memory addresses. A portion of the physical address space of such devices is mapped into the system’s memory address space, allowing the CPU to communicate with and control the device by reading and writing memory addresses.

  • Characteristics
    • Direct Access: Memory-mapped devices can be directly accessed by the CPU, similar to accessing data in memory. This direct access method provides high-speed data transfer and low-latency device operations.
    • Memory Mapping: The device’s registers, buffers, and other resources are mapped into the system’s memory address space, using memory read and write operations to communicate with the device.
    • Read and Write Operations: The CPU can exchange data and perform control operations with the device by reading and writing to the mapped memory addresses.

An example of a device tree for a memory-mapped device is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/dts-v1/;

/ {
#address-cells = <1>;
#size-cells = <1>;
ranges;

serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000>;
};

gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x10>;
};

spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000>;
};
};

Non-Memory-Mapped Devices

Non-memory-mapped devices refer to devices that cannot be directly accessed via memory addresses. Such devices may communicate with the CPU through other means, such as I/O ports, dedicated buses, or specific communication protocols.

  • Characteristics
    • Non-Memory Access: Non-memory-mapped devices cannot be accessed directly through memory addresses like memory-mapped devices. They may use separate I/O ports or dedicated buses for communication.
    • Specific Interface: Devices typically use specific interfaces and protocols to communicate with and be controlled by the CPU, such as SPI, I2C, UART, etc.
    • Driver: Non-memory-mapped devices usually require specific device drivers to achieve communication and control with the CPU.

In the device tree, an example of a device tree for a non-memory-mapped device is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
....
external-bus {
#address-cells = <2>;
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000
1 0 0x10160000 0x10000
2 0 0x30000000 0x30000000>;

// Chipselect 1, Ethernet
// Chipselect 2, i2c controller
// Chipselect 3, NOR Flash

ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};

i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <0x58>;
};
};
};
};

Mapping Address Calculation

Next, using theethernet@0node in the device tree of the non-memory-mapped device listed above as an example, calculate the mapping address of the network card device.

First, find theethernet@0node where it is located, and check its reg property. In the given device tree fragment,ethernet@0the reg property of<0 0 0x1000>is . In the root node,#address-cellsthe value of is 1, indicating that the address consists of one cell.

Next, according torangesattribute for address mapping calculation. In theexternal-busnode’srangesattribute, there are three mapping entries:

  • The first mapping entry is0 0 0x10100000 0x10000, indicating the external bus address range is0x10100000to0x1010FFFF. The first value of this mapping entry is 0, indicating it is associated with theexternal-busnode’s first child node (ethernet@0,0).
  • The second mapping entry:1 0 0x10160000 0x10000, indicating the external bus address range is0x10160000to0x1016FFFF. The first value of this mapping entry is 1, indicating it is associated with theexternal-busnode’s second child node (i2c@1,0).
  • The third mapping entry:2 0 0x30000000 0x30000000, indicating that the address range of the external bus is0x30000000to0x5FFFFFFF. The first value of this mapping entry is 2, indicating that it is associated withexternal-busthe third child node of the node.

Sinceethernet@0is associated withexternal-busthe first child node of , and its reg property is<0 0 0x1000>, we can perform the following calculation:

ethernet@0The physical start address of = the start value of the external bus address = 0x10100000

ethernet@0The physical end address of = the start value of the external bus address + (ethernet@0the second value of the reg property of - 1) =$$ 0x10100000 + 0xFFF = 0x10100FFF $$

Therefore,ethernet@0the physical address range of is0x10100000 - 0x10100FFF

The concept of named resources

When a driver expects a resource list of a certain type, since the person writing the device tree for the development board is usually not the same person writing the driver, there is no guarantee that the list is ordered in the way the driver expects. For example, a driver might expect its device node to have 2 IRQ lines, one for Tx events at index 0 and another for Rx at index 1.

If this order is not satisfied, the driver may exhibit abnormal behavior. To avoid such mismatches, the concept of named resources (clock, irq, dma, reg, etc.) was introduced. It consists of defining resource lists and naming them, so that regardless of the index, a given name will always match the resource.

The corresponding properties for named resources are as follows.

  • reg-names: List of memory regions in the reg property.
  • clock-names: Named clocks in the clocks property.
  • interrupt-names: Specifies a name for each interrupt in the interrupts property.
  • dma-names: Used for the dma property.

For example

1
2
3
4
5
6
7
8
9
10
11
fake_device {
compatible = "packt,fake-device";
reg = <0x4a064000 0x800>, <0x4a0648000x200>, <0x4a064c00 0x200>;
reg-names = "config", "ohci", "ehci";
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>, <0 67 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "ohci", "ehci";
clocks = <&clks IMX6QDL_CLK_UART_IPG>, <&clks IMX6QDL_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
};

The code for extracting each named resource in the driver is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct resource *res1, *res2;
res1 = platform_get_resource_byname(pdev,IORESOURCE_MEM, "ohci");
res2 = platform_get_resource_byname(pdev,IORESOURCE_MEM, "config");

struct dma_chan *dma_chan_rx, *dma_chan_tx;
dma_chan_rx = dma_request_slave_channel(&pdev->dev, "rx");
dma_chan_tx = dma_request_slave_channel(&pdev->dev, "tx");

int txirq, rxirq;
txirq = platform_get_irq_byname(pdev, "ohci");
rxirq = platform_get_irq_byname(pdev, "ehci");

structclk *clck_per, *clk_ipg;
clk_ipg = devm_clk_get(&pdev->dev, "ipg");
clk_ipg = devm_clk_get(&pdev->dev, "pre");

Accessing the reg property

Here, the driver will take the memory region and map it into the virtual address space.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct resource *res;
void __iomem *base;

res = platform_get_resource(pdev,IORESOURCE_MEM, 0);
/*
* Here, this function is equivalent to usingrequest_mem_region(res->start,resource_size(res), pdev->name)
* and ioremap(iores->start, resource_size(iores)requesting and mapping a memory region
*
*/
base = devm_ioremap_resource(&pdev->dev, res);

if (IS_ERR(base))
return PTR_ERR(base);

Obtaining interrupt resources

irq_of_parse_and_map()

ItemDescription
Function Definitionunsigned int irq_of_parse_and_map(struct device_node *dev, int index);
Header File#include <linux/of_irq.h>
Parameter devDevice tree node pointer (struct device_node *), indicating the device node whose interrupt number is to be parsed
Parameter indexIndex number, indicating which interrupt number to retrieve from the device node’sinterruptsproperty
FunctionParses and maps the corresponding interrupt number from the device node’sinterruptsproperty
Return ValueSuccess: Returns an unsigned integer representing the parsed and mapped interrupt number
Failure: Typically returns 0 or an invalid interrupt number (specific error handling depends on platform implementation)

irqd_get_trigger_type()

ItemDescription
Function Definitionu32 irqd_get_trigger_type(struct irq_data *d);
Header File#include <linux/irq.h>
Parameter dPointer to interrupt data structure (struct irq_data *), indicating the interrupt for which to obtain the trigger type
FunctionRetrieve the trigger type of the corresponding interrupt from the interrupt data structure
Return ValueSuccess: Returns an unsigned 32-bit integer representing the interrupt trigger type (e.g., level-triggered or edge-triggered). It will not fail because it always returns a valid trigger type definition macro.

irq_get_irq_data()

ItemDescription
Function Definitionstruct irq_data *irq_get_irq_data(unsigned int irq);
Header file#include <linux/irq.h>
Parameter irqInterrupt number, indicating the interrupt number for which to obtain the interrupt data structure
FunctionObtain the corresponding interrupt data structure based on the interrupt number
Return valueSuccess: returns a pointer tostruct irq_datapointer;
Failure: returns NULL

gpio_to_irq()

ItemDescription
Function definitionint gpio_to_irq(unsigned int gpio);
Header file#include <linux/gpio.h>
Parameter gpioGPIO number, indicating the GPIO for which to obtain the interrupt number
FunctionGet the corresponding interrupt number based on the GPIO number
Return ValueSuccess: Returns the corresponding interrupt number (integer);
Failure: Returns a negative error code

of_irq_get()

ItemDescription
Function Definitionint of_irq_get(struct device_node *dev, int index);
Header File#include <linux/of_irq.h>
Parameter devDevice node, indicating the device node from which to obtain the interrupt number
Parameter indexIndex number, indicating from theinterruptsattribute to obtain which interrupt number
Functionfrom the device nodeinterruptsobtain the corresponding interrupt number from the attribute
Return valueSuccess: returns the corresponding interrupt number (integer);
Failure: returns a negative error code

platform_get_irq()

ItemDescription
Function definitionint platform_get_irq(struct platform_device *dev, unsigned int num);
Header file#include <linux/platform_device.h>
Parameter devPlatform device, indicating the platform device from which to obtain the interrupt number
Parameter numIndex number, indicating which interrupt number to obtain from the device
FunctionObtain the corresponding interrupt number based on the platform device and index number
Return ValueSuccess: Returns the corresponding interrupt number (integer);
Failure: Returns a negative error code

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
* Copyright (c) 2020 Rockchip Electronics Co., Ltd.
*
*/

#include "rk3568-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"

#include <dt-bindings/display/rockchip_vop.h>
/{
test_device{
#address-cells =<1>;
#size-cells = <1>;
ranges;
compatible = "simple-bus";

myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};

myirq{
compatible = "my irq";
interrupt-parent=<&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
};

};
};

Driver Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>

static int my_platform_driver_probe(struct platform_device *pdev)
{
int irq;
struct irq_data *my_irq_data;
struct device_node *mydev_node;
u32 trigger_type;
pr_info("my_platform_probe: Probing platform device\n");

// Get Device Node
mydev_node = pdev->dev.of_node;

// Parse and Map Interrupt
irq = irq_of_parse_and_map(mydev_node, 0);
pr_info("[irq_of_parse_and_map]: irq is %d\n", irq);

// Get Interrupt Data Structure
my_irq_data = irq_get_irq_data(irq);
// Get Interrupt Trigger Type
trigger_type = irqd_get_trigger_type(my_irq_data);
pr_info("[irqd_get_trigger_type]: trigger_type is %d\n", trigger_type);

// Convert GPIO to Interrupt Number
irq = gpio_to_irq(101);
pr_info("[gpio_to_irq]: irq is %d\n", irq);

// Get Interrupt Number from Device Node
irq = of_irq_get(mydev_node, 0);
pr_info("[of_irq_get]: irq is %d\n", irq);

// Get Interrupt Number of Platform Device
irq = platform_get_irq(pdev, 0);
pr_info("[platform_get_irq]: irq is %d\n", irq);

return 0;
}

static int my_platform_driver_remove(struct platform_device *pdev)
{
return 0;
}

static const struct of_device_id of_match_table[] = { { .compatible = "my irq" }, {} };

static struct platform_driver my_platform_drv = {
.driver = {
.owner = THIS_MODULE,
.name = "my platform driver",
.of_match_table = of_match_table,
},
.probe = my_platform_driver_probe,
.remove = my_platform_driver_remove,
};

module_platform_driver(my_platform_drv);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test sample for platform irq");

Test:

1
2
3
4
5
6
7
8
9
root@topeet:/root# insmod of_api_irq_test.ko
[ 7138.032958] of_api_irq_test: loading out-of-tree module taints kernel.
[ 7138.033958] my_platform_probe: Probing platform device
[ 7138.033994] [irq_of_parse_and_map]: irq is 94
[ 7138.034002] [irqd_get_trigger_type]: trigger_type is 8
[ 7138.034013] [gpio_to_irq]: irq is 94
[ 7138.034025] [of_irq_get]: irq is 94
[ 7138.034037] [platform_get_irq]: irq is 94

Reference Document: dts-bindings

Documentation/devicetree/bindingsThe directory is an important directory in the Linux kernel source code, used to store the bindings documentation of the Device Tree. The Device Tree is a data structure that describes hardware platforms and device configurations, specifying device attributes, register configurations, interrupt information, etc., in a portable and hardware-independent manner.

The documents in the bindings directory provide detailed descriptions and usage examples of various devices and drivers in the device tree. These documents are very important for developers because they provide the properties and conventions needed to describe hardware and configure drivers in the device tree.

Documentation/devicetree/bindingsAn overview of some common subdirectories of the directory and their contents:

  • armContains bindings documents for devices and drivers related to the ARM architecture.
  • clockContains bindings documents for clock devices and clock controllers.
  • dmaContains bindings documents for Direct Memory Access (DMA) controllers and devices.
  • gpioContains bindings documents for General Purpose Input/Output (GPIO) controllers and devices.
  • i2cContains bindings documents for I2C buses and devices.
  • interrupt-controllerContains bindings documents for interrupt controllers.
  • mediaContains bindings documents for multimedia devices and drivers.
  • mfdContains bindings documents for Multi-Function Device (MFD) subsystems and devices.
  • networkingContains bindings documents for network devices and drivers.
  • powerContains bindings documents for power management subsystems and devices.
  • spiContains bindings documents for SPI buses and devices.
  • usbContains bindings documents for USB controllers and devices.
  • videoContains bindings documents for video devices and drivers.

Documents in each subdirectory are usually saved with the extension.txtor.yamland written in text or YAML format. These documents provide detailed explanations of properties in the device tree, including property syntax, optional values, and usage examples. They also describe device tree conventions and best practices to help developers correctly configure and describe hardware devices and drivers.

The YAML format follows thejson-schemaDevicetree bindings written in, reference:

Device Tree Overlay

After Linux 4.4, Dynamic DeviceTree was introduced. Device Tree Overlay is an extension mechanism for the Device Tree. The Device Tree is a data structure used to describe hardware devices, widely applied in embedded systems, especially those based on the Linux kernel.

Device Tree Overlayallows dynamic modification of the device tree content at runtime to add, modify, or delete device nodes and properties. It provides a flexible way to configure and manage hardware devices without recompiling the entire device tree. By using device tree overlays, developers can make configuration changes to hardware without rebooting the system.

Device Tree Overlay (Dynamic DeviceTree) is typically defined in a text format called Device Tree Source (DTS). DTS files describe the structure and properties of the device tree, including device nodes, register addresses, interrupt information, etc. Device tree overlays can achieve dynamic modification of the device tree by loading and parsing device tree files and merging them into the existing device tree.

Application Scenarios

Using device tree overlays, common configuration changes can be achieved, such as adding external devices, disabling unnecessary devices, modifying device properties, etc. This is very useful for embedded system development and debugging, especially when dealing with multiple hardware configurations or frequent hardware configuration changes.

Device Tree Overlay Syntax

overlay.dts

  • Header Declaration
1
2
/dts-v1/;
/plugin/;
  • The overlay node name is used to define the device nodes and their properties to be added, modified, or deleted. It uses the same syntax as the device tree source file, but with specific modifiers before the node name to indicate the overlay operation.

For example, the following node:

1
2
3
4
5
6
7
8
//arch/arm64/boot/dts/rockchip/topeet-rk3568-linux.dts
//485 Enable Pin
rk_485_ctl: rk-485-ctl {
compatible = "topeet,rs485_ctl";
gpios = <&gpio0 RK_PC6 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&rk_485_gpio>;
};

If in the device tree overlay, to add aoverlay_nodenode:

There are several ways to express it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/dts-v1/;
/plugin/;

// Method 1
&{/rk-485-ctl}{
overlay_node{
status = "okay";
};
};

// Method 2
&rk_485_ctl{
overlay_node{
status = "okay";
};
};

// Method 3

/{
fragment@0{
target-path="/rk-485-ctl";
__overlay__{
overlay_node{
status = "okay";
};
};
};
fragment@1{
target=<&rk_485_ctl>;
__overlay__{
overlay_node{
status = "okay";
};
};
};
};

The compilation of device tree overlays is the same as that of device trees.

1
dtc -I dts -O dtb overlay.dts -o overlay.dtbo

Porting Device Tree Driver Overlays

We will explore how to port device tree overlays to the iTOP-RK3568 development board. Porting device tree overlays mainly includes the following steps.

  • Configure the kernel to support mounting the configfs virtual file system.
  • Configure the kernel to support device tree overlays
  • Port device tree overlay drivers

Configure the kernel to support mounting the configfs virtual file system

1
2
3
4
5
$ make ARCH=arm64 menuconfig
# The path is as follows
File System->
Pseudo filesystems ->
-*- UserSpace-driven configuration filesystem

After boot, check if the kernel has mounted the virtual file system; if not, run the following command to mount it

1
$ mount -t configfs none /sys/kernel/config

Configure the kernel to support device tree overlays

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ make ARCH=arm64 menuconfig
# The path is as follows
Device Driver ->
-*- Device Tree and Open Firmware support ->
--- Device Tree and Open Firmware support
[*] Enable dtc generation of symbols for overlays support # Generate symbols for the base device tree for overlay references
[ ] Device Tree runtime unit tests # Runtime testing of the kernel device tree subsystem
[*] Device Tree overlays # Support runtime dynamic loading of device tree overlays

File System ->
<*> Overlay filesystem support # Enable OverlayFS core support.
[*] Overlayfs: turn on redirect directory feature by default # Redirect directory is an optimization feature of OverlayFS.
[*] Overlayfs: follow redirects even if redirects are turned off # Even if the redirect feature is disabled at mount time (redirect_dir=off), the kernel will still attempt to recognize and follow existing redirect metadata. Used for compatibility with existing redirects.
[*] Overlayfs: turn on inodes index feature by default # The inode index feature is used to solve the 'stale inode' problem. It ensures inode consistency and supports hardlinks.
[*] Overlayfs: auto enable inode number mapping # Automatically assign globally unique and persistent inode numbers to files in the upper layer (via an indexing mechanism). Depends on the inode index feature above. Without this, the inode number of the same file may change each time OverlayFS is mounted.
[*] Overlayfs: turn on metadata only copy up feature by default # Metadata-only copy up is a performance optimization. It does not copy the entire file when modifying attributes.

Save configuration:

1
2
cp .config arch/arm64/configs/rockchip_linux_defconfig
../build.sh kernel

Port the device tree overlay driver

There is already a pre-written one on GitHubdevice tree overlay driver, we just need to compile this driver as a module or into the kernel

Compile:

1
2
$ git clone https://github.com/ikwzm/dtbocfg.git
$ make KERNEL_SRC=/home/zhaohang/repository/linux/rk3568_linux_5.10/kernel ARCH=arm64 CROSS_COMPILE=/home/zhaohang/repository/linux/rk3568_linux_5.10/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-

Compile and generatedtbocfg.koCopy it to the development board.

Load the device tree overlay

Load the device tree overlay (first, you need the device tree overlay driver, check if configfs is mounted viacat proc/filesystemscheck if configfs is mounted successfully)

Enter the system directory/sys/kernel/config/device-tree/overlays/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ insmod dtbocfg.ko
[ 430.997017] dtbocfg: loading out-of-tree module taints kernel.
[ 430.997630] dtbocfg: 0.1.1
[ 430.997675] dtbocfg: OK

$ cd /sys/kernel/config/device-tree/overlays/
# Create a kernel object
$ mkdir test
$ cd test
$ ls
dtbo status

$ cat /root/overlay.dtb > dtbo
# Enable dtbo
$ echo 1 > status
# At this point, we can use the following command to see the loaded nodes
$ ls /proc/device-tree/rk-485-ctl/overlay_node/

# If we want to remove the nodes modified by dtbo, simply delete the created kernel object test
$ cd /sys/kernel/config/device-tree/overlays/
$ rmdir test

Multiple can be created; if multiple modify the same property, the last overlay’s value is used

1
2
3
4
5
6
7
8
9
10
$ cd /sys/kernel/config/device-tree/overlays/

# Create a kernel object
$ mkdir test1

# Overlay with another device tree
...

# When deleted, it reverts to the device tree after test was loaded
$ rmdir test1

Changeset

Nodes in the device tree overlay (dtbo) must also be converted todevice_node, somedevice_nodemust also be converted toplatform_device. However, before performing the conversion,of_overlay_fdt_applythe function first creates achangeset. Then modifications are made based on this changeset.

The purpose of creating a changeset is to facilitate modification and restoration of the device tree.A device tree is a static data structure,Once compiled and loaded into the kernel,,it is difficult to modify directly.To address this issue, the device tree overlay feature introduces the concept of a changeset.

A changeset is a data structure that describes changes to the device tree, recording modification operations such as adding, deleting, or modifying nodes. By creating a changeset, we can dynamically modify the device tree at runtime without altering the original device tree source files.

  • By creating a changeset, we can conveniently define the modification operations needed without directly manipulating the underlying structure of the device tree. This provides a high-level abstraction, allowing us to describe device tree changes in a more concise and readable manner.
  • Additionally, changesets can be saved, transferred, and applied to other device trees, facilitating device tree configuration and customization across different systems or environments.
  • Furthermore, changesets can also be used for device tree restoration. In some cases, we may need to undo modifications to the device tree at runtime and revert to its original state. By applying a reverse changeset, we can restore the device tree to its state before modification, achieving restoration of changes.

Therefore, creating a changeset provides a convenient, controllable, and reversible way to modify the device tree.

Introduction to the Virtual File System ConfigFS

In the Linux kernel, there are several commonly used virtual file systems. The virtual file system provides a kernel abstraction layer, allowing applications to operate on different types of files and devices through a unified file access interface. It simplifies application development and maintenance, offers higher portability and flexibility, and provides functionality for managing file systems and accessing underlying hardware.

  • procfs

The virtual file system,**provides an interface to access the runtime state of the system kernel,**representing processes, devices, drivers, and other system information in the kernel as files and directories. By reading and writing files in procfs, information about the system state can be obtained and modified.

  • sysfs

A virtual file system,**used to represent devices, drivers, and other kernel objects in the system.**It provides a unified interface to access and configure the properties and states of these objects through files and directories. Sysfs is commonly used by device drivers and system management tools to view and control the system’s hardware and kernel objects.

  • configfs

A virtual file system,**used for dynamically configuring and managing kernel objects.**It provides an interface to access kernel objects in the form of files and directories, allowing users to add, modify, and delete kernel objects at runtime without recompiling the kernel or rebooting the system. ConfigFS is often used to configure and manage devices, drivers, and subsystems.

These virtual file systems have some functional differences:

  • procfsis mainly used to access and manage process information, providing information about processes, kernel parameters, and system status.
  • sysfsis mainly used to represent and configure devices, drivers, and other kernel objects in the system, providing a unified interface to access and control the properties and states of these objects.
  • configfsis mainly used for dynamically configuring and managing kernel objects, providing an interface to access kernel objects in the form of files and directories, allowing addition, modification, and deletion of kernel objects at runtime.

Linux startup handling of the device tree
Linux startup handling of the device tree

To achieve the above functions, user space needs to interact with the kernel, that is, to loaddtbointo memory.sysfsThe role of the virtual file system is to export kernel data and attributes to user space in the form of files. Once exported to user space, reading these files means reading device files, and writing to these files means controlling the device.

configfsThe English explanation of the role isUserspace-driven kernel object configurationwhich translates to user-space configuration of kernel objects.

Soconfigfsandsysfsare exactly opposite,sysfsexports kernel objects to user space,configfsconfigures kernel objects from user space without recompiling the kernel or modifying kernel code. Soconfigfsis more suitable for the device tree overlay technology.

ConfigFS Core Data Structures

The core data structures of ConfigFS mainly include the following parts:

  • configfs_subsystem

configfs_subsystemis a top-level data structure representing the entire ConfigFS subsystem. It contains a pointer to the root config item group, as well as other attributes and status information of ConfigFS.

  • config_group

config_groupis a special type of config item representing a config item group. It can contain a set of related config items, forming a hierarchical structure.config_groupstructure contains a pointer to the parent config item and a linked list pointing to child config items.

  • config_item

This is the most basic data structure in ConfigFS, used to represent a config item. Each config item is a kernel object, which can be a device, driver, subsystem, etc.config_itemstructure contains information such as the type, name, attributes, and status of the config item, as well as pointers to the parent and child config items.

The relationships among these data structures can form a tree structure, whereconfigfs_subsystemis the root node,config_grouprepresents a configuration item group,config_itemrepresents a single configuration item. Child configuration items are linked together via a linked list, forming a parent-child relationship.

configfs_subsystem

1
2
3
4
struct configfs_subsystem {
struct config_group su_group;
struct mutex su_mutex;
};

struct configfs_subsystemThe structure containsstruct config_groupstructure,struct config_groupThe structure is as follows:

config_group

1
2
3
4
5
6
7
struct config_group {
struct config_item cg_item;
struct list_head cg_children;
struct configfs_subsystem *cg_subsys;
struct list_head default_groups;
struct list_head group_entry;
};

struct config_groupThe structure containsstruct config_itemstructure,struct config_itemThe structure is as follows:

config_item

1
2
3
4
5
6
7
8
9
10
struct config_item {
char *ci_name;
char ci_namebuf[CONFIGFS_ITEM_NAME_LEN]; //The name of the directory
struct kref ci_kref;
struct list_head ci_entry;
struct config_item *ci_parent;
struct config_group *ci_group;
const struct config_item_type *ci_type; //Attribute files and attribute operations under the directory
struct dentry *ci_dentry;
};

struct config_itemThe structure containsstruct config_item_typestructure,struct config_item_typeThe structure is as follows

config_item_type

1
2
3
4
5
6
7
struct config_item_type {
struct module *ct_owner;
struct configfs_item_operations *ct_item_ops; //Operation methods of the item (directory)
struct configfs_group_operations *ct_group_ops; //Operation methods of group (container)
struct configfs_attribute **ct_attrs; //Operation methods of attribute files
struct configfs_bin_attribute **ct_bin_attrs; //Operation methods of bin attribute files
};

struct config_item_typeContains many important data structures:

  • struct configfs_item_operations *ct_item_opsOperation methods of item (directory)
  • struct configfs_group_operations *ct_goup_opsOperation methods of group (container)
  • struct configfs_attribute **ct_attrsOperation methods of attribute files
  • struct configfs_bin_attribute **ct_bin_attrsOperation methods of bin attribute files

Eachconfig_item(orconfig_group) must be bound to aconfig_item_type
This structure determines what kind of “operable object” the item appears as in sysfs.

configfs_item_operations

1
2
3
4
5
6
7
8
9
10
11
struct configfs_item_operations {

// Called when the item's reference count reaches zero
void (*release)(struct config_item *);

// Whether to allow creating a symbolic link between src and target (`ln -s`)
int (*allow_link)(struct config_item *src, struct config_item *target);

// Called when the link is deleted
void (*drop_link)(struct config_item *src, struct config_item *target);
};
release()
ProjectDescription
Prototypevoid (*release)(struct config_item *);
TriggerReference count is 0
Responsibilitykfree / Resource release
Required✅ Required

configfs_group_operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct configfs_group_operations {

//Method to create an item, called when using the mkdir command under a group
struct config_item *(*make_item)(struct config_group *group, const char *name);

//Method to create a group, called when the user executes`mkdir <name>`and wants to create a child group
struct config_group *(*make_group)(struct config_group *group, const char *name);

// (Optional) Called when the item is 'committed' after configuration (rarely used)
int (*commit_item)(struct config_item *item);

// Notification before an item is removed (can be used to clean up resources)
void (*disconnect_notify)(struct config_group *group, struct config_item *item);

// When the item is`rmdir`Called after deletion (note: the item has been removed from the directory tree at this point), typically used to disassociate from a group
void (*drop_item)(struct config_group *group, struct config_item *item);
};
make_item()
ItemDescription
Prototypestruct config_item *(*make_item)(struct config_group *, const char *name);
TriggerUser mkdir
FunctionDynamically create item
RequiredIf item creation is allowed
make_group()
ItemDescription
Prototypestruct config_group *(*make_group)(struct config_group *, const char *name);
Triggermkdir
FunctionCreate sub-group (multi-level directory)
drop_item()
ProjectDescription
Prototypevoid (*drop_item)(struct config_group *, struct config_item *);
Triggerrmdir
Responsibilityconfig_item_put()
Must✅ Yes

struct configfs_attribute

1
2
3
4
5
6
7
8
9
10
struct configfs_attribute {
const char *ca_name; // Property file name
struct module *ca_owner;// Module to which the property file belongs
umode_t ca_mode; // Property file access permissions

// Called when reading properties, returns the number of bytes; specific functionality needs to be implemented by yourself.
ssize_t (*show)(struct config_item *, char *);
// Called when writing properties; specific functionality needs to be implemented by yourself.
ssize_t (*store)(struct config_item *, const char *, size_t);
};

Key data structure relationship diagram

Key data structure relationship diagram
Key data structure relationship diagram

ConfigFS API

Core API

config_group_init()

ItemDescription
Function Definitionvoid config_group_init(struct config_group *group);
Header File#include <linux/configfs.h>
Parameter groupconfig_group to be initialized
FunctionInitialize the basic structure of a group (without name/type)
Typical ScenarioInitialize the su_group of a subsystem
Return ValueNone

config_group_init_type_name()

ItemDescription
Function Definitionvoid config_group_init_type_name(struct config_group *group, const char *name, const struct config_item_type *type);
Header File#include <linux/configfs.h>
Parameter groupThe group to be initialized
Parameter nameThe directory name displayed by the group in configfs
Parameter typeCorresponding config_item_type
FunctionInitialize group + set name + bind type
Typical ScenarioCreate a visible directory node
Return ValueNone

config_item_init_type_name()

ProjectDescription
Function Definitionvoid config_item_init_type_name(struct config_item *item, const char *name, const struct config_item_type *type);
Header File#include <linux/configfs.h>
Parameter itemconfig_item object
Parameter nameitem name
Parameter typeitem type
FunctionInitialize an item and bind its type
Typical ScenarioUsed in make_item
Return ValueNone

configfs_register_subsystem()

ItemDescription
Function Definitionint configfs_register_subsystem(struct configfs_subsystem *subsys);
Header File#include <linux/configfs.h>
Parameter subsysThe configfs subsystem to register
FunctionUnder/sys/kernel/config/Register the subsystem directory
EffectCreate/sys/kernel/config/<name>
Return ValueSuccess: 0 Failure: Negative error code

configfs_unregister_subsystem()

ItemDescription
Function Definitionvoid configfs_unregister_subsystem(struct configfs_subsystem *subsys);
FunctionUnregister the entire configfs subsystem
EffectDelete the subsystem directory and all sub-items
Return ValueNone

configfs_register_group()

ItemDescription
Function Definitionint configfs_register_group(struct config_group *parent, struct config_group *group);
Parameter parentParent group
Parameter groupSubgroup to register
FunctionRegister a group under the parent directory
Features✅ Static group (users cannot rmdir)
Return valueSuccess: 0

configfs_unregister_group()

ItemDescription
Function definitionvoid configfs_unregister_group(struct config_group *group);
FunctionUnregister group
NoteOnly applies to groups registered via register_group

config_item_put()

ItemDescription
Function definitionvoid config_item_put(struct config_item *item);
FunctionDecrease reference count
TriggerReference count is 0 → call release
Common locationIn drop_item callback
Return valueNone

Can utilizeCONFIGFS_ATTRRelated macro

CONFIGFS_ATTR_RO()

ItemDescription
Macro definitionCONFIGFS_ATTR_RO(prefix, name)
GenerateRead-only property
Required functionprefix_name_show()
Generated variableprefixattr_name

CONFIGFS_ATTR_WO()

ItemDescription
Macro definitionCONFIGFS_ATTR_WO(prefix, name)
GenerateWrite-only property
Required functionprefix_name_store()

CONFIGFS_ATTR()

ItemDescription
Macro definitionCONFIGFS_ATTR(prefix, name)
GenerateRead-write property

Example

Register configFS subsystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <linux/module.h>
#include <linux/init.h>
#include <linux/configfs.h>

static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = NULL,
.ct_group_ops = NULL,
.ct_attrs = NULL,
// .ct_bin_attrs = NULL,
};

static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};

static int __init myconfigfs_init(void)
{
// Initialize config_group
config_group_init(&myconfigfs_subsystem.su_group);
// Register subsystem
configfs_register_subsystem(&myconfigfs_subsystem);

return 0;
}

static void __exit myconfigfs_exit(void)
{
configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfigfs_init);
module_exit(myconfigfs_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test sample for configfs ");

After loading, you can see/sys/kernel/configthe registered subsystem myconfigfs in the directory

1
2
$ ls /sys/kernel/config
device-tree myconfigfs

Register group container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <linux/module.h>
#include <linux/init.h>
#include <linux/configfs.h>

// Create mygroup under myconfigfs
static struct config_group mygroup;

// mygroup_config_item_type, used to describe the configuration item type of mygroup
static const struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = NULL,
.ct_group_ops = NULL,
.ct_attrs = NULL,
};

// myconfig_item_type, the structure used to describe the configuration item type
static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = NULL,
};

static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};

static int __init myconfigfs_group_init(void)
{
// Initialize configuration group
config_group_init(&myconfigfs_subsystem.su_group);
// Register subsystem
configfs_register_subsystem(&myconfigfs_subsystem);

// Initialize configuration group "mygroup"
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// Configure group "mygroup" in the subsystem
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);

return 0;
}

static void __exit myconfigfs_group_exit(void)
{
// Unregister subsystem
configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfigfs_group_init);
module_exit(myconfigfs_group_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a sample for configfs: register group");

Test

1
2
3
4
$ ls /sys/kernel/config/
device-tree myconfigfs
$ ls /sys/kernel/config/myconfigfs/
mygroup

Create item in user space

We have successfully created/sys/kernel/config/under the directorymyconfigfssubsystem, and createdmygroupcontainer under this subsystem, butmygroupcannot use mkdir to create item under the container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <linux/init.h>
#include <linux/module.h>
#include <linux/configfs.h>
#include <linux/slab.h>

static struct config_group mygroup;

struct myitem {
struct config_item item;
};

void myitem_release(struct config_item *item)
{
struct myitem *mitem = container_of(item, struct myitem, item);
kfree(mitem);
pr_info("%s\n", __func__);
}
struct configfs_item_operations myitem_ops = {
.release = myitem_release,
};

static struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &myitem_ops,
};

struct config_item *rootgroup_make_item(struct config_group *group, const char *name)
{
struct myitem *my_config_item;
pr_info("%s\n", __func__);
my_config_item = kzalloc(sizeof(*my_config_item), GFP_KERNEL);
config_item_init_type_name(&my_config_item->item, name, &mygroup_config_item_type);
return &my_config_item->item;
}

struct configfs_group_operations rootgroup_ops = {
.make_item = rootgroup_make_item,
};

static struct config_item_type rootgroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &rootgroup_ops,
};

static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &rootgroup_config_item_type,
},
},
};

static int __init myconfigfs_test_init(void)
{
// Initialize configuration group
config_group_init(&myconfigfs_subsystem.su_group);
// Register subsystem
configfs_register_subsystem(&myconfigfs_subsystem);

// Initialize configuration group mygroup
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// Mount mygroup to myconfigfs_subsystem.su_Under group
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);

return 0;
}

static void __exit myconfigfs_test_exit(void)
{
// Unregister myconfigfs_subsystem
configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfigfs_test_init);
module_exit(myconfigfs_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("this is a test sample for configfs");

Test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ $ insmod configfs_make_item_test.ko
[ 12.066995] configfs_make_item_test: loading out-of-tree module taints kernel.
~ $ ls /sys/kernel/config/
device-tree myconfigfs
~ $ ls /sys/kernel/config/myconfigfs/
mygroup
~ $ cd /sys/kernel/config/myconfigfs/
/sys/kernel/config/myconfigfs $ ls
mygroup
/sys/kernel/config/myconfigfs $ mkdir test
[ 43.220027] rootgroup_make_item
/sys/kernel/config/myconfigfs $ ls test
/sys/kernel/config/myconfigfs $ rmdir test
[ 55.003042] myitem_release

Improve drop and release

releaseanddrop_itemare two different member fields used for different purposes:

  • releaseThe member field is defined in thestruct config_item_typestructure as a callback function pointer. It points to a function that the kernel calls when a configfs config item is released or deleted, to perform corresponding resource cleanup operations. It is typically used to release resources associated with the config item, such as freeing dynamically allocated memory, closing open file descriptors, etc.
  • drop_itemis defined in thestruct configfs_group_operationsstructure as a callback function pointer. It points to a function that the kernel calls when a configfs config group is deleted, to handle operations related to the config group. This function is typically used to clean up the state of the config group, release associated resources, and perform other necessary cleanup operations.drop_itemThe function is called when a config group is deleted, not when a single config item is deleted.

releaseThe member field is used for releasing config items, while thedrop_itemmember field is used for deleting config groups. They perform different tasks in different contexts, but both are related to resource release and cleanup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/configfs.h>

struct myitem {
struct config_item conf_item;
};

void child_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
kfree(myitem);
pr_info("%s\n", __func__);
}

static struct configfs_item_operations child_item_ops = {
.release = child_release,
};

static struct config_item_type child_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &child_item_ops,
};

struct config_item *root_make_item(struct config_group *group, const char *name)
{
struct myitem *myitem;

myitem = kzalloc(sizeof(*myitem), GFP_KERNEL);

config_item_init_type_name(&myitem->conf_item, name, &child_config_item_type);

pr_info("%s\n", __func__);
return &myitem->conf_item;
}

// When a config group (created under the root directory) is deleted in configfs, the kernel calls this function to handle operations related to the config group.
void root_drop_item(struct config_group *group, struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
config_item_put(&myitem->conf_item);
pr_info("%s\n", __func__);
}

static struct configfs_group_operations root_configfs_group_ops = {
.make_item = root_make_item,
.drop_item = root_drop_item,
};

static const struct config_item_type root_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &root_configfs_group_ops,
};

static struct configfs_subsystem test_configfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &root_config_item_type,
},
},
};
static struct config_group child_group1;

static int __init configfs_test_init(void)
{
config_group_init(&test_configfs_subsystem.su_group);
configfs_register_subsystem(&test_configfs_subsystem);

// Initialize the config group child_group1
config_group_init_type_name(&child_group1, "child_group1", &child_config_item_type);
configfs_register_group(&test_configfs_subsystem.su_group, &child_group1);
return 0;
}

static void __exit configfs_test_exit(void)
{
configfs_unregister_subsystem(&test_configfs_subsystem);
}

module_init(configfs_test_init);
module_exit(configfs_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test description for configfs");

Test:

1
2
3
4
5
6
7
8
9
10
11
12
$ insmod configfs_release_and_drop_test.ko
[ 11.641528] configfs_release_and_drop_test: loading out-of-tree module taints kernel.
$ cd /sys/kernel/config/myconfigfs/
/sys/kernel/config/myconfigfs $ ls
child_group1
/sys/kernel/config/myconfigfs $ mkdir test
[ 26.684113] root_make_item
/sys/kernel/config/myconfigfs $ rmdir test
[ 30.148208] root_drop_item
[ 30.148397] child_release
/sys/kernel/config/myconfigfs $ rmdir child_group1/
rmdir: 'child_group1/': Operation not permitted

child_group1is a subgroup statically registered viaconfigfs_register_groupand is “fixed” by default, meaning it cannot be deleted from user space.

The functionCall timingResponsibilityIs it mandatory
root_drop_itemCalled by the parent group when rmdir startsDisassociate item from group, decrease reference count✅ Mandatory (otherwise release cannot be triggered)
child_releaseAutomatically called when reference count reaches zeroRelease item’s own memory and resources✅ Mandatory (otherwise memory leak)

Register attribute

We successfully created the item, but no attributes or operation items were created under the item.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/configfs.h>

struct myitem {
struct config_item conf_item;
int size;
void *addr;
};

void child_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
kfree(myitem);
pr_info("%s\n", __func__);
}

static struct configfs_item_operations child_item_ops = {
.release = child_release,
};

ssize_t myread_show(struct config_item *item, char *page)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
memcpy(page, myitem->addr, myitem->size);
pr_info("%s\n", __func__);
return myitem->size;
}
ssize_t mywrite_store(struct config_item *item, const char *page, size_t size)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
myitem->addr = kmemdup(page, size, GFP_KERNEL);
myitem->size = size;
pr_info("%s\n", __func__);
return myitem->size;
}

// Create read-only configuration item myread
CONFIGFS_ATTR_RO(my, read);
// Create write-only configuration item mywrite
CONFIGFS_ATTR_WO(my, write);

static struct configfs_attribute *my_attrs[] = {
&myattr_read,
&myattr_write,
NULL,
};

static struct config_item_type child_config_item_type = { .ct_owner = THIS_MODULE,
.ct_item_ops = &child_item_ops,
.ct_attrs = my_attrs };

struct config_item *root_make_item(struct config_group *group, const char *name)
{
struct myitem *myitem;

myitem = kzalloc(sizeof(*myitem), GFP_KERNEL);

config_item_init_type_name(&myitem->conf_item, name, &child_config_item_type);

pr_info("%s\n", __func__);
return &myitem->conf_item;
}

// When a configuration group in configfs is deleted (a group created under the root directory), the kernel calls this function to handle operations related to the configuration group
void root_drop_item(struct config_group *group, struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
config_item_put(&myitem->conf_item);
pr_info("%s\n", __func__);
}

static struct configfs_group_operations root_configfs_group_ops = {
.make_item = root_make_item,
.drop_item = root_drop_item,
};

static const struct config_item_type root_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &root_configfs_group_ops,
};

static struct configfs_subsystem test_configfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &root_config_item_type,
},
},
};
static struct config_group child_group1;

static int __init configfs_test_init(void)
{
config_group_init(&test_configfs_subsystem.su_group);
configfs_register_subsystem(&test_configfs_subsystem);

// Initialize configuration group child_group1
config_group_init_type_name(&child_group1, "child_group1", &child_config_item_type);
configfs_register_group(&test_configfs_subsystem.su_group, &child_group1);
return 0;
}

static void __exit configfs_test_exit(void)
{
configfs_unregister_subsystem(&test_configfs_subsystem);
}

module_init(configfs_test_init);
module_exit(configfs_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test description for configfs");

Test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ insmod configfs_attribute_test.ko
[ 37.565258] configfs_attribute_test: loading out-of-tree module taints kernel.
$ cd /sys/kernel/config/
/sys/kernel/config $ ls
device-tree myconfigfs
/sys/kernel/config $ cd myconfigfs/
/sys/kernel/config/myconfigfs $ ls
child_group1
/sys/kernel/config/myconfigfs $ cd child_group1/
/sys/kernel/config/myconfigfs/child_group1 $ ls
read write
/sys/kernel/config/myconfigfs/child_group1 $ echo 1 > write
[ 71.704345] mywrite_store
/sys/kernel/config/myconfigfs/child_group1 $ cat read
[ 75.550669] myread_show
1
/sys/kernel/config/myconfigfs/child_group1 $ cat read
[ 78.938556] myread_show
1

Implement multi-level directories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/configfs.h>

struct myitem {
struct config_item conf_item;
int size;
void *addr;
};
struct mygroup {
struct config_group conf_group;
};

void child_item_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
kfree(myitem);
pr_info("%s\n", __func__);
}

static struct configfs_item_operations child_item_ops = {
.release = child_item_release,
};

// static struct configfs_group_operations child_group_ops = {

// };

ssize_t myread_show(struct config_item *item, char *page)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
if (myitem->size > 0 && myitem->addr != NULL)
memcpy(page, myitem->addr, myitem->size);
pr_info("%s\n", __func__);
return myitem->size;
}
ssize_t mywrite_store(struct config_item *item, const char *page, size_t size)
{
struct myitem *myitem = container_of(item, struct myitem, conf_item);
myitem->addr = kmemdup(page, size, GFP_KERNEL);
myitem->size = size;
pr_info("%s\n", __func__);
return myitem->size;
}

CONFIGFS_ATTR_RO(my, read);
CONFIGFS_ATTR_WO(my, write);

struct configfs_attribute *child_item_attribute[] = {
&myattr_read,
&myattr_write,
NULL,
};

static struct config_item_type child_item_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &child_item_ops,
.ct_attrs = child_item_attribute,
};

// Secondary folder
static struct config_item_type child_group_config_item_type = {
.ct_owner = THIS_MODULE,
// .ct_group_ops = &child_group_ops,
.ct_group_ops = NULL,
};

// root folder

struct config_item *root_make_item(struct config_group *group, const char *name)
{
struct myitem *myitem;

myitem = kzalloc(sizeof(*myitem), GFP_KERNEL);
config_item_init_type_name(&myitem->conf_item, name, &child_item_config_item_type);
pr_info("%s\n", __func__);
return &myitem->conf_item;
}

struct config_group *root_make_group(struct config_group *group, const char *name)
{
struct mygroup *mygroup;

mygroup = kzalloc(sizeof(*mygroup), GFP_KERNEL);
config_group_init_type_name(&mygroup->conf_group, name, &child_group_config_item_type);
pr_info("%s\n", __func__);
return &mygroup->conf_group;
}

void root_drop_item(struct config_group *group, struct config_item *item)
{
config_item_put(item);
pr_info("%s\n", __func__);
}

static struct configfs_group_operations root_group_ops = {
.make_item = root_make_item,
.make_group = root_make_group,
.drop_item = root_drop_item,
};

static struct config_item_type root_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &root_group_ops,
};

static struct configfs_subsystem configfs_test_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &root_config_item_type,
},
},
};

static struct config_group mygroup;

static int __init configfs_test_init(void)
{
config_group_init(&configfs_test_subsystem.su_group);
configfs_register_subsystem(&configfs_test_subsystem);

config_group_init_type_name(&mygroup, "mygroup", &child_group_config_item_type);
configfs_register_group(&configfs_test_subsystem.su_group, &mygroup);

return 0;
}

static void __exit configfs_test_exit(void)
{
configfs_unregister_group(&mygroup);
}

module_init(configfs_test_init);
module_exit(configfs_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("even629<asqwgo@outlook.com>");
MODULE_DESCRIPTION("This is a test sample for configfs");

It should be noted that:
When the user executesmkdir /sys/kernel/config/myconfigfs/xxx, ConfigFS first calls.make_item(if defined), and on failure attempts to call.make_group(if defined)

dtbocfg driver analysis

Next, let’s analyze the device tree overlay driverdtbocfg.koCode:

1
2
3
4
5
6
7
8
9
static struct configfs_subsystem dtbocfg_root_subsys = {
.su_group = {
.cg_item = {
.ci_namebuf = "device-tree",
.ci_type = &dtbocfg_root_type,
},
},
.su_mutex = __MUTEX_INITIALIZER(dtbocfg_root_subsys.su_mutex),
};

This code defines a structure instance nameddtbocfg_root_subsysofconfigfs_subsystemstructure instance,representing ConfigFS a subsystem within

First,dtbocfg_root_subsys.su_groupis aconfig_groupstruct, which represents the root configuration item group of the subsystem. Here, the struct’scg_itemfield represents the basic configuration item of the root configuration item group.

.ci_namebuf = "device-tree": The name of the configuration item is set todevice-tree, indicating that the name of the configuration item isdevice-tree

.ci_type = &dtbocfg_root_type: The type of the configuration item is set todtbocfg_root_type, which is a custom configuration item type.
Next,.su_mutexfield is a mutex lock used to protect the subsystem’s operations. Here, the__MUTEX_INITIALIZERmacro is used to initialize the mutex lock.

Summary: The above code creates a subsystem nameddevice-tree, whose root configuration item group is empty. More configuration items and configuration item groups can be added under this subsystem for dynamically configuring and managing device tree-related kernel objects.

1
2
$ ls /sys/kernel/config/
devicetree usb_gadget

Driver code entry part:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int __init dtbocfg_module_init(void)
{
int retval = 0;
pr_info("%s\n", __func__);

// Initialize the configfs group
config_group_init(&dtbocfg_root_subsys.su_group);
config_group_init_type_name(&dtbocfg_overlay_group, "overlays", &dtbocfg_overlays_type);

// Register subsystem
retval = configfs_register_subsystem(&dtbocfg_root_subsys);
if (retval != 0) {
pr_err("%s: couldn't register subsys\n", __func__);
goto register_subsystem_failed;
}

// Register group
retval = configfs_register_group(&dtbocfg_root_subsys.su_group, &dtbocfg_overlay_group);
if (retval != 0) {
pr_err("%s: couldn't register group\n", __func__);
goto register_group_failed;
}

pr_info("%s: OK\n", __func__);
return 0;

register_group_failed:
configfs_unregister_subsystem(&dtbocfg_root_subsys);
register_subsystem_failed:
return retval;
}

This code is an initialization functiondtbocfg_module_init(), used to initialize and register the ConfigFS subsystem and configuration item groups.

  1. First, through theconfig_group_init()function, initializedtbocfg_root_subsys.su_group, which is the root configuration item group of the subsystem.
  2. Using theconfig_group_init_type_name()function, initializedtbocfg_overlay_group, representing a configuration item group namedoverlays, and specify the type of the configuration item group asdtbocfg_overlays_type, which is a custom configuration item type.
  3. Call theconfigfs_register_subsystem()function to registerdtbocfg_root_subsyssubsystem. If registration fails, an error message will be printed, and it will jump toregister_subsystem_failedError handling at the tag.
  4. Callconfigfs_register_group()function registereddtbocfg_overlay_groupconfiguration item group and added it todtbocfg_root_subsys.su_groupunder.

The purpose of this code is to initialize and register a ConfigFS subsystem nameddevice-treeand create a configuration item group namedoverlaysunder it.

That is, under the Linux system, indevice-treesubsystem, createdoverlayscontainer

1
2
$ ls /sys/kernel/config/device-tree/
overlays

References

kernel/Documentation/filesystems/configfsdirectory underconfigfs.txt

kernel/samples/configfsdirectory underconfigfs_sample.c