Chapter 27A: DT bindings YAML and dt_binding_check

DMA: Direct Memory Access. Hardware moves data to or from memory without the CPU copying each byte. MCU bridge: Think of DMA like the MCU DMA controller you used for UART or SPI, but with cache coherency, scatter-gather descriptors, and kernel ownership rules added.

What: the mainline kernel’s machine-checkable description of every Device Tree binding, a JSON-Schema document (in YAML form) that says exactly which properties a node should have, of what type, with what constraints. Plus the make dt_binding_check / make dtbs_check targets that validate your DTS against them. MCU bridge: Think of Device Tree like a board-level hardware description table that replaces hard-coded #define LED_PORT GPIOA decisions. Unlike an MCU header, the kernel parses it at boot and matches it to drivers. Device Tree: a data file that describes board hardware to the Linux kernel instead of hard-coding it in C.

Why: Since kernel v4.18 (mid-2018), every new binding must ship a YAML schema, and existing bindings are being migrated. Without a schema, your patch will not be accepted upstream. Without a schema check on your CI, your binding can silently drift between board variants and you’ll only discover it when something breaks.

Focus: the schema as a contract. A binding YAML is the source of truth for what a node should look like. The DTS files are checked against it. Drivers are documented by it. Read one binding YAML carefully and the rest follow the same pattern.

27A.1 Why bindings need schemas

For the first ~15 years of Device Tree’s life, “bindings” lived as free-form .txt files in Documentation/devicetree/bindings/. A typical pre-2018 binding looked like:

* Freescale i.MX UART

Required properties:
- compatible: should be "fsl,<soc>-uart"
- reg: address and length of the register region
- interrupts: should contain the UART interrupt

Optional properties:
- fsl,dte-mode: indicates the UART is in DTE mode

Two problems:

  1. Not machine-checkable. A DTS file with a typo (compatible = "fsl,imx6ull-art";, missing the u) or with a missing property silently passes compilation. Kernel just doesn’t probe the device at runtime, and you go hunting.

  2. Inconsistent. Two binding files written by two people, even for closely-related hardware, would describe things in subtly different ways. Some used reg for one purpose, some another. No way to enforce style.

Since 2018, every new binding ships as a YAML file containing a JSON-Schema. Schemas are validated automatically. DTS files are linted against them. CI rejects bindings that fail the lint. The .txt bindings are being migrated, with a deadline that keeps slipping but is real: any new binding without YAML is rejected.

27A.2 Anatomy of a binding YAML

Open Documentation/devicetree/bindings/serial/fsl-imx-uart.yaml (the i.MX UART binding). Lightly elided:

# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/serial/fsl-imx-uart.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: Freescale i.MX Universal Asynchronous Receiver/Transmitter (UART)

maintainers:
  - Fabio Estevam <festevam@gmail.com>

allOf:
  - $ref: serial.yaml#

properties:
  compatible:
    oneOf:
      - enum:
          - fsl,imx1-uart
          - fsl,imx21-uart
      - items:
          - enum:
              - fsl,imx25-uart
              - fsl,imx27-uart
              - fsl,imx31-uart
              - fsl,imx35-uart
              - fsl,imx50-uart
              - fsl,imx51-uart
              - fsl,imx53-uart
              - fsl,imx6q-uart
          - const: fsl,imx21-uart
      - items:
          - enum:
              - fsl,imx6sl-uart
              - fsl,imx6sll-uart
              - fsl,imx6ul-uart
          - const: fsl,imx6q-uart
          - const: fsl,imx21-uart

  reg:
    maxItems: 1

  interrupts:
    maxItems: 1

  clocks:
    items:
      - description: IPG clock for the UART
      - description: Per-module clock for the UART

  clock-names:
    items:
      - const: ipg
      - const: per

  dmas:
    items:
      - description: DMA channel for RX
      - description: DMA channel for TX

  dma-names:
    items:
      - const: rx
      - const: tx

  fsl,uart-has-rtscts:
    type: boolean
    description: |
      Indicates the UART has RTS and CTS lines, that are mostly required to do
      hardware flow control. Deprecated, use uart-has-rtscts instead.

  uart-has-rtscts: true

  fsl,dte-mode:
    type: boolean
    description: |
      Indicate the uart works in DTE mode. The uart works in DCE mode by default.

required:
  - compatible
  - reg
  - interrupts
  - clocks
  - clock-names

unevaluatedProperties: false

examples:
  - |
    #include <dt-bindings/clock/imx6sx-clock.h>
    aliases {
        serial0 = &uart1;
    };

    uart1: serial@2020000 {
        compatible = "fsl,imx6sx-uart", "fsl,imx21-uart";
        reg = <0x02020000 0x4000>;
        interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&clks IMX6SX_CLK_UART_IPG>,
                 <&clks IMX6SX_CLK_UART_SERIAL>;
        clock-names = "ipg", "per";
        uart-has-rtscts;
        fsl,dte-mode;
    };

Section-by-section:

  • $id: globally unique URL for this schema. Used by $ref to pull schemas in from other files.

  • title: human-readable. Shows up in generated docs.

  • maintainers: who signs off on changes.

  • allOf: [$ref: serial.yaml#]: inherits constraints from a parent schema. Every UART binding includes the generic-serial schema, which adds e.g. current-speed and rs485-* properties.

  • properties: the meat. Each property gets a sub-schema saying what kind of value it accepts.

    • compatible.oneOf: three legal forms: just fsl,imx1-uart or fsl,imx21-uart. A SoC string followed by fsl,imx21-uart as fallback. Or a three-string list ending in fsl,imx21-uart. Anything else is rejected.

    • reg.maxItems: 1: exactly one address-range, no more.

    • clocks.items: exactly two entries. The schema even documents what each represents.

    • clock-names.items: must be the literal strings “ipg” and “per”, in that order.

    • uart-has-rtscts: true: equivalent to “this is a boolean flag” (the value true after a name is the YAML shorthand for “this is a valid property with no additional constraints”).

  • required: properties that MUST be present.

  • unevaluatedProperties: false: no other properties are allowed. This catches typos. If you wrote clock-name (singular), the schema rejects it as an unknown property.

  • examples: a working DT fragment that must validate against the schema. Doubly useful: it documents usage and it’s lint-checked as part of dtbs_check.

This one file replaces ~30 lines of English prose, and it can be checked by tools.

27A.3 Running the checks

Two make targets:

$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dt_binding_check
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dtbs_check

What each does:

  • dt_binding_check validates every binding YAML file against the meta-schema. Catches errors in the bindings themselves: malformed YAML, references to non-existent parent schemas, missing required meta-fields. Run this when you write or edit a binding.

  • dtbs_check compiles every DTS for the current arch, then validates each compiled DTB against the matching binding schema. Catches errors in the DT source: wrong property types, missing required properties, extra unknown properties. Run this when you edit a DTS.

Prerequisites:

$ pip install --user --upgrade dtschema yamllint

dtschema is the validator that does the actual matching. yamllint catches YAML formatting issues.

A typical dtbs_check run on the i.MX tree produces ~100 warnings as of v6.6, many existing DTS files have minor issues that haven’t been cleaned up. New code should add zero new warnings. The upstream maintainers will require that.

27A.4 Writing your first binding

Consider a GPIO-driven LED as a small schema case study. The fictional “my-pa-led” binding below illustrates constraints, while the existing gpio-leds binding is the appropriate interface for a normal GPIO LED.

MCU bridge: Think of Linux GPIO like the same pin set/reset block you used on STM32, but accessed through a kernel subsystem that owns numbering, direction, interrupts, and user-space exposure. GPIO: General-Purpose Input/Output, a pin controlled as a digital input, output, or interrupt source.

Documentation/devicetree/bindings/leds/myorg,pa-led.yaml:

# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/leds/myorg,pa-led.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: My-Org Point Atom LED

maintainers:
  - Your Name <you@example.org>

description: |
  A single GPIO-driven LED on the Point Atom MINI / ALPHA boards.
  Active-low (cathode connected to the GPIO; anode through resistor to 3V3).

properties:
  compatible:
    const: myorg,pa-led

  gpios:
    maxItems: 1
    description: GPIO connected to the LED's cathode

  default-state:
    enum: [on, off, keep]
    default: off

required:
  - compatible
  - gpios

additionalProperties: false

examples:
  - |
    #include <dt-bindings/gpio/gpio.h>

    led0 {
        compatible = "myorg,pa-led";
        gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
        default-state = "off";
    };

The corresponding scoped binding check is:

$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dt_binding_check \
    DT_SCHEMA_FILES=leds/myorg,pa-led.yaml

If the schema is well-formed, the example inside it compiles cleanly and the binding-check passes. If you have a typo, you’ll get a precise error pointing at the line.

Then, to add the matching DT node to a board:

/ {
    led0 {
        compatible = "myorg,pa-led";
        gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
        default-state = "off";
    };
};

The make ARCH=arm dtbs_check target validates the board node against the corresponding schema.

27A.5 Common schema patterns

A short library of patterns you’ll re-use:

Property whose value is one of a fixed set

my-mode:
  enum: [polled, irq, dma]
  default: polled

Property whose value is a number in a range

my-freq-hz:
  $ref: /schemas/types.yaml#/definitions/uint32
  minimum: 1000000
  maximum: 50000000

Property whose value is a string matching a pattern

label:
  pattern: '^[a-z0-9-]+$'

Property whose value depends on compatible

allOf:
  - if:
      properties:
        compatible:
          contains:
            const: myorg,pa-board-rev-b
    then:
      required:
        - my-rev-b-only-property

27A.6 Inheriting from base schemas

A binding rarely starts from scratch. Almost every binding allOf:-includes one or more parents:

  • serial.yaml for UARTs

  • i2c-controller.yaml for I²C buses

  • spi-controller.yaml for SPI buses

  • leds.yaml for LED-class devices

  • hwmon.yaml for hardware monitoring

These parents define standard properties common to the class (e.g., current-speed for serial, clock-frequency for I²C buses). Inheriting from them means your binding picks up that vocabulary automatically and is validated consistently with everyone else’s.

27A.7 Key Notes

  • A binding is the contract between hardware description and driver interpretation. YAML expresses that contract using JSON Schema constraints rather than replacing it with implementation details.

  • dt_binding_check checks the binding and its examples. dtbs_check checks compiled board descriptions against applicable bindings, so the two targets address different error sources.

  • Required properties, permitted values and ordered resource names make assumptions explicit. Unknown-property rejection can expose a typo that ordinary DTS compilation accepts.

  • Shared schemas carry class-wide vocabulary. Closing a composed schema with unevaluatedProperties accounts for inherited properties rather than treating them all as unknown.

  • A schema example illustrates a node’s interface, not a new driver. The fictional LED binding here does not replace the maintained gpio-leds implementation.

  • Validation can reject malformed descriptions without detecting every wrong address, pin connection or voltage assumption. Schema success is not physical-board qualification.

27A.8 Pitfalls

  • Schema-validation passes but DT still doesn’t work at runtime. The schema only catches syntactic errors, wrong types, wrong arity, missing required props. Semantic errors (a reg value pointing at the wrong physical address) pass schema but fail at boot. The schema catches some bugs but not all. You still need to test on real hardware.

  • Forgetting to install dtschema. Symptom: dt_binding_check reports zero warnings on a tree that clearly has issues. Means the validator silently isn’t running. Verify: pip show dtschema.

  • Schema example doesn’t validate. Common when you write a binding without testing the example. Run dt_binding_check against your own binding first. Only then propose it upstream.

  • additionalProperties: false too strict. If you forget that the inherited base schema allows certain extra properties, the strict-mode rejection of “unknown” properties will reject legal usage. Use unevaluatedProperties: false (which respects allOf inheritance) instead.

  • maxItems vs items. maxItems: 1 means up to one entry of any type. items: [- description: foo] means exactly one entry, described as foo. These are different, use the second form when you want the description to appear in the docs.

27A.9 Going deeper

  • Documentation/devicetree/writing-schema.rst: the canonical tutorial.

  • Documentation/devicetree/bindings/example-schema.yaml: a fully-annotated example schema. Read it after this chapter.

  • dtschema source: github.com/devicetree-org/dt-schema. Read for ground truth on what’s actually validated.

  • JSON Schema spec at json-schema.org/specification. DT bindings use JSON Schema vocabulary. This is the underlying spec.

  • grep -r "unevaluatedProperties" Documentation/devicetree/bindings/: read a few real-world bindings end-to-end before writing your own.

Next chapter: Chapter 27B: Device Tree for a custom i.MX6ULL board. With DTS grammar and bindings understood, we now turn a board schematic into a Linux board .dts.