Skip to content

Lesson 3: Hardware Interfaces & Device Tree Primer

Óbuda University — Linux in Embedded Systems

"How does Linux discover hardware without hard-coding?"


Today's Map

  • Block 1 (45 min): Device Tree: why DT exists, DTS syntax/content, matching flow, overlays, common failures. Bus protocols: I2C, SPI, and high-speed interfaces (MIPI D-PHY, CSI, DSI, HDMI).
  • Block 2 (45 min): Hands-on: enable I2C via overlay, connect MCP9808, verify with i2cdetect, read temperature through sysfs.

Block 1 — Device Tree

"The hardware manifest Linux cannot live without"


Why Device Tree Exists

On a microcontroller you hard-code pin numbers, bus addresses, and clock speeds directly into firmware. One board, one binary — it works.

Linux runs on thousands of boards. Hard-coding every board's wiring into every driver would be unmaintainable.

Device Tree separates what hardware exists from how to use it.

  • Driver contains the logic (how to talk to a sensor)
  • Device Tree contains the wiring (which bus, which address, which pins) and configuration information for the driver

A single MCP9808 driver works on every board that has I2C — as long as the Device Tree correctly describes the connection.


Three Things Device Tree Provides

1. Hardware description outside driver source The kernel binary stays generic. Board-specific details live in a separate data file.

2. Separation between board wiring and driver logic Changing a sensor's I2C address does not require recompiling the kernel.

3. Reusable drivers across multiple products The same MCP9808 driver works on a Raspberry Pi, a BeagleBone, and a custom industrial board.

Change the wiring? Edit the .dts file. The driver stays untouched.


What Device Tree Describes

center

Device Tree hierarchy: the root node contains SoC nodes, bus controllers, and peripheral devices — a complete hardware manifest.


DTS Content in Detail

Unlike USB or PCIe, I2C and SPI devices cannot identify themselves. The kernel needs a manifest.

Device Tree describes:

Category Examples
Buses I2C, SPI, UART controllers — which exist, how configured
Devices What is connected to each bus, at which address
Pins & IRQs GPIO assignments, interrupt lines, memory regions

Think of it as a bill of materials that tells the kernel: "here is every bus, every device, and every wire on this board."


Annotated DTS Overlay — MCP9808

/dts-v1/;                           // DTS format version
/plugin/;                           // This is an overlay, not a base DT

/ {
    compatible = "brcm,bcm2835";    // Target: any Raspberry Pi

    fragment@0 {
        target = <&i2c1>;           // Attach to I2C bus 1 (GPIO 2/3)

        __overlay__ {
            #address-cells = <1>;   // I2C uses 7-bit addresses
            #size-cells = <0>;      // I2C devices have no memory-mapped region

            mcp9808@18 {                    // Node name @ I2C address
                compatible = "mcp9808";     // Must match driver's ID table
                reg = <0x18>;               // I2C address (from datasheet)
            };
        };
    };
};

Try It Now: Read a Device Tree (5 min)

Explore the live device tree on your Raspberry Pi — find how the kernel sees hardware:

# Navigate the live device tree (it's a filesystem!)
ls /proc/device-tree/
ls /proc/device-tree/soc/

# Find I2C bus nodes
ls /proc/device-tree/soc/i2c@*/

# List devices on I2C bus 1 (if any overlays are loaded)
ls /proc/device-tree/soc/i2c@7e804000/

# Read a compatible string of any device you find
# (e.g., after loading the MCP9808 overlay from the tutorial)
cat /proc/device-tree/soc/i2c@7e804000/mcp9808@18/compatible

The MCP9808 node only appears after you load its Device Tree overlay. If you haven't done the Enable I2C + MCP9808 Driver tutorials yet, explore what's already there — look at the I2C bus node itself and any built-in devices.

Tutorial: MCP9808 Driver — Device Tree Overlay Theory: Section 2: DTS Content


Key Fields Explained

Field Value Purpose
target <&i2c1> Which bus to add the device to
compatible "mcp9808" Must match the driver code exactly
reg <0x18> I2C address — verify with i2cdetect -y 1

The compatible string is the contract between Device Tree and driver.

If it does not match, the kernel will never try to bind the driver. No error. No log. Just silence.


Device Tree Binding

center

How the kernel matches a Device Tree node to a driver via the compatible string — the binding contract between hardware description and driver code.


The Matching Flow

How does a Device Tree node find its driver?

+---------------------------+
| Kernel parses Device Tree |
+------------+--------------+
             v
+---------------------------+
| Creates bus nodes         |
+------------+--------------+
             v
+---------------------------+
| Creates child device nodes|
+------------+--------------+
             v
   +---------+-----------+
   | Driver with         |
   | matching compatible |----No----> Device stays UNBOUND
   | string?             |            (silent -- no error)
   +---------+-----------+
             v Yes
+---------------------------+
|  Call driver probe()      |
+------------+--------------+
             |
   +---------+----------+
   | probe() succeeds?  |----No----> Error in dmesg
   +---------+----------+
             v Yes
+---------------------------+
|  Device node in /dev      |
|  or sysfs                 |
+---------------------------+

The compatible String Is the Key

The compatible string must match exactly between:

  • The Device Tree node's compatible property
  • The driver's of_match_table[] or i2c_device_id[]
// In driver code:
static const struct of_device_id mcp9808_of_match[] = {
    { .compatible = "mcp9808" },    // <-- must match DTS
    { }
};

One typo — "mcp9880" instead of "mcp9808" — and the device silently never appears.


Live: Find the I2C Node

On any Raspberry Pi with a Device Tree, the live tree is at /proc/device-tree/.

# List all I2C bus nodes
ls /proc/device-tree/soc/i2c@*/

# Find devices on I2C bus 1
ls /proc/device-tree/soc/i2c@7e804000/

# Read the compatible string of a device
cat /proc/device-tree/soc/i2c@7e804000/mcp9808@18/compatible

If your device node does not appear here, the overlay was not applied.


Overlays — Patching the Base Tree

The base Device Tree describes the SoC as it ships from the manufacturer. But you plug in custom sensors, enable disabled interfaces, change pins.

Overlays patch the base tree at boot time.

+---------------------+     +----------------+
|  Base Device Tree   |     |  Your Overlay  |
|  (SoC + board)      | + + |  (MCP9808 on   |
|                     |     |   I2C bus 1)   |
+----------+----------+     +-------+--------+
           |                        |
           +----------+-------------+
                      |
                      v
           +----------+----------+
           |  Merged Device Tree |
           |  (passed to kernel) |
           +---------------------+

On Raspberry Pi: add dtoverlay=mcp9808 to /boot/firmware/config.txt.


Why Overlays Matter

  • No kernel rebuild needed when adding devices
  • Per-deployment customization — one unit with MCP9808, another with BMI160
  • Safe iteration during development — change wiring without touching drivers

The bootloader merges overlays with the base tree before handing it to the kernel.

You can ship a standard base image and customize hardware configuration per deployment.


Common Failure Modes

Failure Symptom Fix
Wrong I2C address probe failed with error -6 (ENXIO) Check reg matches i2cdetect
Missing/wrong compatible No dmesg output at all Verify exact string match
Pinmux conflict pin already claimed Check config.txt for conflicts
Overlay not applied Node missing from /proc/device-tree Verify config.txt entry

What Errors Look Like in Practice

Wrong I2C address:

[   12.345] mcp9808 1-0019: probe failed with error -6
Error -6 = ENXIO (no device at that address). The reg value does not match the hardware.

Missing compatible string:

(no dmesg output at all for your device)
The kernel never tried. No driver matched.

Pinmux conflict:

[    1.234] pinctrl-bcm2835 fe200000.gpio: pin already claimed
Another peripheral is using the same GPIO pins. Check /boot/firmware/config.txt.


Debugging Checklist

# 1. Check dmesg for probe messages
dmesg | grep -i -E "i2c|spi|probe|dt"

# 2. Verify device-tree node exists
ls /proc/device-tree/soc/i2c@7e804000/

# 3. Verify device appears on bus scan
i2cdetect -y 1

# 4. Confirm driver module is loaded
lsmod | grep mcp9808

Work through these in order. The first one that fails tells you where the problem is.


Live: Modify a Property

Scenario: the MCP9808 is at address 0x19 instead of 0x18 (A0 pin pulled high).

Change two things in the overlay:

mcp9808@19 {                    // Node name updated
    compatible = "mcp9808";     // Driver match -- unchanged
    reg = <0x19>;               // New I2C address
};

Recompile: dtc -@ -I dts -O dtb -o mcp9808.dtbo mcp9808-overlay.dts

Reboot. Verify with i2cdetect -y 1 and dmesg | grep mcp9808.


Mini Exercise — Fill in the Blanks

An accelerometer (LIS3DH) is connected to I2C bus 1 at address 0x19:

__overlay__ {
    #address-cells = <1>;
    #size-cells = <0>;

    ________@____ {
        compatible = "________";
        reg = <____>;
    };
};

Then: list two reasons why probe() might still fail even with a correct Device Tree entry.


I2C Bus: What Happens on the Wire

When i2cdetect shows 0x18, this is the actual bus transaction:

  SDA ─┐  ┌─────────┬───┬────────────┬───────────┬─────┬─┐
       └──┤ 0011000 │ R │    ACK     │ DATA BYTE │ ACK │S│
          │ (0x18)  │ /W│  (slave)   │  (8 bits) │     │T│
  SCL ────┼─┬─┬─┬─┬─┼─┬─┼─┬──────────┼─┬─┬─┬─┬─┬─┼─┬───┼─┤
          │ │ │ │ │ │ │ │ │          │ │ │ │ │ │ │ │   │ │
       START  7 address bits  ACK     8 data bits   ACK STOP

Protocol sequence: 1. START — master pulls SDA low while SCL is high 2. Address — 7-bit device address + R/W bit (0 = write, 1 = read) 3. ACK — slave pulls SDA low to acknowledge 4. Data — one or more bytes, each followed by ACK 5. STOP — master releases SDA while SCL is high

If no device responds at the address → NACKi2cdetect shows --.


I2C: Clock Stretching and Multi-Master

Clock stretching: The slave holds SCL low when it is not ready. The master must wait. This is why I2C has no guaranteed throughput — a slow slave can hold up the entire bus.

Multi-master arbitration: If two masters start simultaneously, the one that sends a 0 while the other sends 1 wins (wired-AND). The loser detects the conflict and backs off.

Why clock-frequency in device tree matters:

Setting Speed Use Case
100 kHz (Standard) 12.5 KB/s Legacy sensors, long wires
400 kHz (Fast) 50 KB/s Most modern sensors (MCP9808, BME280)
1 MHz (Fast-mode Plus) 125 KB/s High-throughput devices (displays, EEPROMs)
&i2c1 {
    clock-frequency = <400000>;  /* 400 kHz — match your sensor's spec */
};

Wrong frequency → unreliable reads, corrupted data, or bus hangs.


SPI Bus: Four Wires, Four Modes

  Master                          Slave
  ┌──────────┐                   ┌──────────┐
  │     MOSI ├──────────────────►│ MOSI     │  (Master Out, Slave In)
  │     MISO │◄──────────────────┤ MISO     │  (Master In, Slave Out)
  │     SCLK ├──────────────────►│ SCLK     │  (Clock)
  │       CS ├──────────────────►│ CS       │  (Chip Select, active low)
  └──────────┘                   └──────────┘

Four SPI modes from clock polarity (CPOL) and phase (CPHA):

Mode CPOL CPHA Idle Clock Sample Edge
0 0 0 Low Rising
1 0 1 Low Falling
2 1 0 High Falling
3 1 1 High Rising

Wrong SPI mode = garbage data. The device tree spi-cpol and spi-cpha properties configure this. Check your sensor's datasheet — it specifies which mode to use.


SPI vs I2C: When to Use Which

I2C SPI
Wires 2 (SDA, SCL) shared bus 4+ (MOSI, MISO, SCLK, CS per device)
Speed 100-400 kHz typical 1-50 MHz typical
Addressing In-band (7-bit address) Hardware (one CS line per device)
Duplex Half (one wire for data) Full (separate MOSI and MISO)
Multi-device Easy (shared bus, addresses) Expensive (one CS pin per device)
Typical sensors Temperature, humidity, EEPROM IMU, ADC, display, flash

Decision rule: - Few slow sensors (temperature, humidity) → I2C — simpler wiring - Fast data streams (IMU at 1 kHz, ADC, display) → SPI — higher throughput - Many devices, limited pins → I2C — shared bus saves GPIOs

The MCP9808 (temperature) uses I2C. The BMI160 (IMU) uses SPI. Both choices follow this logic.


Beyond I2C and SPI

High-speed interfaces for cameras and displays


The Speed Gap

I2C and SPI are fine for sensors. But cameras and displays need orders of magnitude more bandwidth:

Interface Max Speed Typical Use
I2C 400 kbit/s Temperature sensor (MCP9808)
SPI 50 Mbit/s IMU (BMI160), small TFT display
MIPI CSI-2 (2-lane) 3 Gbit/s Camera module
MIPI DSI (2-lane) 3 Gbit/s 7" touchscreen panel
HDMI 2.0 18 Gbit/s External monitor, 4K display

I2C/SPI use single-ended signals (voltage relative to ground). CSI/DSI/HDMI use differential pairs (two wires, opposite voltages — noise cancels).


Differential Signaling: Why It Enables Gigabit Speeds

  Single-ended (SPI):          Differential (MIPI D-PHY):

  Signal ──────┐               Dp ───────┐
               │ 3.3V          (+ side)  │ +100 mV
  GND ─────────┘               Dn ───────┘ -100 mV
                                Receiver: Dp - Dn = 200 mV

  Noise hits signal & ground   Noise hits BOTH wires equally:
  → signal corrupted           (Dp+N) - (Dn+N) = Dp - Dn
                               → noise cancels out
Single-ended (SPI) Differential (MIPI D-PHY)
Voltage swing 3.3 V 200 mV
Noise immunity Moderate Excellent (common-mode rejection)
Max speed ~50 MHz ~2.5 GHz per lane
Wires per signal 1 + ground 2 (differential pair)
EMI Higher Lower (fields cancel)
PCB rules Relaxed Matched traces, controlled impedance

The small 200 mV swing is why MIPI uses less power and runs faster than SPI.


MIPI D-PHY: The Physical Layer for CSI and DSI

Both camera (CSI-2) and display (DSI) use the same electrical interface: MIPI D-PHY.

  Camera uses CSI-2 over D-PHY:   Sensor ──CSI-2──► SoC
  Display uses DSI over D-PHY:    SoC ──DSI──► Panel

D-PHY has two operating modes:

Mode Speed Voltage Use
Low-Power (LP) ~10 Mbit/s 0 / 1.2 V (single-ended) Commands, wake-up
High-Speed (HS) up to 2.5 Gbit/s ±100 mV (differential) Pixel data

A dedicated clock lane (differential pair) provides timing. Data is sampled on both clock edges (DDR):

  500 MHz clock → 1 Gbit/s per data lane
  2 lanes → 2 Gbit/s total

CSI-2: Camera to SoC

The Pi's camera connector carries 2 CSI data lanes + 1 clock lane (6 wires in the flat cable):

  ┌────────────┐     2-lane CSI-2         ┌──────────────┐
  │ IMX219     │ ────────────────────────►│ BCM2711 SoC  │
  │ sensor     │ (D-PHY, ~700 Mbit/s/lane)│              │
  │            │◄───────── I2C ──────────►│  ISP ──► DMA │
  │            │  (config: exposure,      │ (debayer,    │
  │            │   gain, resolution)      │  white bal.) │
  └────────────┘                          └──────────────┘

Bandwidth example — 1920×1080 RAW10 @ 30 FPS:

  1920 × 1080 × 10 bits × 30 FPS = 622 Mbit/s
  + ~15% overhead (headers, sync) ≈ 715 Mbit/s
  Per lane (2 lanes): 358 Mbit/s ◄── well within D-PHY limits

The raw Bayer data goes through the ISP (Image Signal Processor) inside the SoC — demosaicing, white balance, exposure — before reaching your application.


DSI: SoC to Display

The Pi's display connector also carries 2 DSI data lanes + 1 clock lane:

  ┌──────────────┐   2-lane DSI    ┌───────────┐     ┌─────────┐
  │ BCM2711 SoC  │ ───────────────►│ TC358762  │────►│ 800×480 │
  │  Display     │  (D-PHY)        │ bridge    │ DPI │ LCD     │
  │  Controller  │                 │ (DSI→DPI) │     │ panel   │
  └──────────────┘                 └───────────┘     └─────────┘

Bandwidth example — 800×480 RGB888 @ 60 FPS:

  800 × 480 × 24 bits × 60 FPS = 553 Mbit/s
  + ~20% overhead ≈ 664 Mbit/s
  Per lane: 332 Mbit/s ◄── plenty of margin

DSI panels are GPU-driven — the display controller scans pixels out of the DRM buffer automatically. No CPU involvement per frame (unlike SPI displays).


HDMI: Highest Bandwidth

HDMI uses TMDS (Transition-Minimized Differential Signaling) — 3 data channels + 1 clock:

  SoC ──► TMDS Ch0 (Blue + sync)  ──► Monitor
       ──► TMDS Ch1 (Green)        ──►
       ──► TMDS Ch2 (Red)          ──►
       ──► TMDS Clock              ──►
       ◄──► DDC (I2C) ── reads EDID (display capabilities)

1080p @ 60 FPS bandwidth:

  Pixel clock: 148.5 MHz (includes blanking)
  Per TMDS channel: 148.5 × 10 bits (8b/10b encoding) = 1.485 Gbit/s
  Total: 3 × 1.485 = 4.46 Gbit/s
  HDMI 2.0 max: 18 Gbit/s → OK

The monitor reports its supported resolutions via EDID — read over I2C (DDC channel) at cable plug-in. You can inspect it: cat /sys/class/drm/card1-HDMI-A-1/edid | edid-decode


Interface Summary: Full Picture

I2C SPI DSI HDMI CSI-2
Speed 400 kbit/s 50 Mbit/s 3 Gbit/s 18 Gbit/s 3 Gbit/s
Signaling Single-ended Single-ended Differential Differential Differential
Direction Bidirectional Full-duplex SoC → Panel SoC → Monitor Sensor → SoC
GPU driven N/A No (CPU) Yes Yes N/A (input)
Wires 2 4+ 6 19 6
Typical use Temp sensor IMU, small TFT Touchscreen Monitor Camera

From I2C to HDMI, the principle is the same: faster data needs more sophisticated signaling. Differential pairs, encoding schemes, and dedicated clock lanes are the price of gigabit speeds.

Theory: Camera and Display Interfaces


Pin Multiplexing: One Pin, Many Functions

A single physical pin on an SoC can serve multiple roles:

  Physical Pin 2 (GPIO 2)
       ├── GPIO input/output
       ├── I2C1_SDA
       ├── UART0_TX
       └── SPI0_MOSI

Only one function at a time. The pinctrl subsystem resolves which function a pin serves.

Device tree assigns pin groups:

&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;  /* Claims GPIO 2 and 3 for I2C */
};

"Pin already claimed" error: Another peripheral's device tree node has already taken this pin. Fix: check /boot/firmware/config.txt for conflicting overlays or disable the conflicting interface.


The Clock Tree

Every peripheral needs a clock signal. The SoC generates clocks from a PLL chain:

  Crystal        PLL          Bus Clock      Peripheral Clocks
  (24 MHz)  ──► (x50)  ──►  /4 = 300 MHz    ──►  I2C: /750 = 400 kHz
            1200 MHz                        ──►  SPI: /12 = 25 MHz
                                            ──►  UART: /3125 = 96 kHz

Device tree describes clock connections:

sensor@18 {
    clocks = <&clk_i2c1>;         /* Which clock drives this device */
    clock-names = "i2c";          /* Name the driver uses to request it */
};

"Driver probed but reads zeros" — the peripheral clock might be gated (disabled to save power). The clock framework enables it when the driver calls clk_prepare_enable().

Clock gating is invisible from user space. If i2cdetect shows 0x18 but reads return 0x00, suspect the sensor's internal clock or the bus clock speed.


Block 1 Summary

  • Device Tree separates hardware description from driver logic
  • The compatible string is the binding contract between DTS and driver
  • Overlays patch the base tree without rebuilding the kernel
  • Most "device not found" bugs are DTS issues, not driver bugs
  • Debug path: dmesg --> /proc/device-tree --> i2cdetect --> lsmod
  • Low-speed (I2C, SPI) = single-ended, sensor data
  • High-speed (CSI, DSI, HDMI) = differential pairs, cameras and displays