all
Stage 08

I2C / SPI / UART Drivers

Write Linux kernel drivers for I2C, SPI, and UART buses — i2c_driver and i2c_client, spi_driver and spi_device, regmap abstraction, the tty layer, and writing client drivers for real ICs.

10 min read
65623 chars

I2C Driver Model {:.gc-basic}

Basic

I2C (Inter-Integrated Circuit) is a two-wire (SDA + SCL) serial bus supporting multiple devices addressed by a 7-bit (or 10-bit) address. In the Linux kernel:

  • i2c_adapter — the bus controller driver (already provided for your SoC)
  • i2c_client — represents one device on the bus
  • i2c_driver — your client driver, matched against i2c_client entries
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>

#define TMP102_REG_TEMP  0x00
#define TMP102_REG_CONF  0x01

struct tmp102_priv {
    struct i2c_client *client;
    s16                last_temp_raw;
};

/* Read a 16-bit big-endian register via SMBus */
static int tmp102_read_temp(struct tmp102_priv *priv)
{
    s32 ret = i2c_smbus_read_word_swapped(priv->client, TMP102_REG_TEMP);
    if (ret < 0)
        return ret;
    priv->last_temp_raw = (s16)(ret >> 4);  /* 12-bit value, MSB aligned */
    return 0;
}

/* Raw multi-message I2C transfer (write then read) */
static int tmp102_read_reg16(struct i2c_client *client, u8 reg, u16 *val)
{
    struct i2c_msg msgs[2];
    u8 buf[2];
    int ret;

    msgs[0].addr  = client->addr;
    msgs[0].flags = 0;             /* write */
    msgs[0].len   = 1;
    msgs[0].buf   = &reg;

    msgs[1].addr  = client->addr;
    msgs[1].flags = I2C_M_RD;     /* read */
    msgs[1].len   = 2;
    msgs[1].buf   = buf;

    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2)
        return ret < 0 ? ret : -EIO;

    *val = (buf[0] << 8) | buf[1];
    return 0;
}

static int tmp102_probe(struct i2c_client *client,
                         const struct i2c_device_id *id)
{
    struct tmp102_priv *priv;

    if (!i2c_check_functionality(client->adapter,
                                  I2C_FUNC_SMBUS_WORD_DATA))
        return -EOPNOTSUPP;

    priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->client = client;
    i2c_set_clientdata(client, priv);

    dev_info(&client->dev, "TMP102 probed at address 0x%02x\n",
             client->addr);
    return 0;
}

static void tmp102_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "TMP102 removed\n");
}

static const struct i2c_device_id tmp102_id[] = {
    { "tmp102", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, tmp102_id);

static const struct of_device_id tmp102_of_match[] = {
    { .compatible = "ti,tmp102" },
    { }
};
MODULE_DEVICE_TABLE(of, tmp102_of_match);

static struct i2c_driver tmp102_driver = {
    .driver = {
        .name           = "tmp102",
        .of_match_table = tmp102_of_match,
    },
    .probe    = tmp102_probe,
    .remove   = tmp102_remove,
    .id_table = tmp102_id,
};
module_i2c_driver(tmp102_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TMP102 I2C temperature sensor driver");

DTS binding for an I2C device

&i2c1 {
    clock-frequency = <400000>;  /* 400 kHz Fast-mode */

    tmp102@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;            /* 7-bit I2C address */
        interrupt-parent = <&gpio1>;
        interrupts = <3 IRQ_TYPE_LEVEL_LOW>;
    };
};

SPI Driver Model {:.gc-basic}

Basic

SPI (Serial Peripheral Interface) is a four-wire full-duplex bus (MOSI, MISO, SCLK, CS). Transfers are always full-duplex — the master sends and receives simultaneously.

#include <linux/module.h>
#include <linux/spi/spi.h>

#define MCP3204_CHANNELS  4

struct mcp3204_priv {
    struct spi_device *spi;
    u8                 tx_buf[3];
    u8                 rx_buf[3];
};

/* Read one ADC channel: start bit + single-ended + channel select */
static int mcp3204_read_channel(struct mcp3204_priv *priv, u8 channel)
{
    struct spi_transfer t = {
        .tx_buf = priv->tx_buf,
        .rx_buf = priv->rx_buf,
        .len    = 3,
    };
    struct spi_message m;
    int ret;

    priv->tx_buf[0] = 0x06 | ((channel >> 2) & 0x01);  /* start + D2 */
    priv->tx_buf[1] = (channel & 0x03) << 6;             /* D1 D0 */
    priv->tx_buf[2] = 0x00;

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);

    ret = spi_sync(priv->spi, &m);
    if (ret)
        return ret;

    /* 12-bit result in rx_buf[1..2] */
    return ((priv->rx_buf[1] & 0x0F) << 8) | priv->rx_buf[2];
}

static int mcp3204_probe(struct spi_device *spi)
{
    struct mcp3204_priv *priv;
    int ret;

    priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->spi = spi;
    spi_set_drvdata(spi, priv);

    /* Verify SPI settings negotiated by DT */
    dev_info(&spi->dev, "SPI mode %d, %d Hz, CS%d\n",
             spi->mode, spi->max_speed_hz, spi->chip_select);

    ret = mcp3204_read_channel(priv, 0);
    dev_info(&spi->dev, "CH0 = %d counts\n", ret);
    return 0;
}

static const struct of_device_id mcp3204_of_match[] = {
    { .compatible = "microchip,mcp3204" },
    { }
};
MODULE_DEVICE_TABLE(of, mcp3204_of_match);

static const struct spi_device_id mcp3204_id[] = {
    { "mcp3204", 0 }, { }
};
MODULE_DEVICE_TABLE(spi, mcp3204_id);

static struct spi_driver mcp3204_driver = {
    .driver   = { .name = "mcp3204", .of_match_table = mcp3204_of_match },
    .probe    = mcp3204_probe,
    .id_table = mcp3204_id,
};
module_spi_driver(mcp3204_driver);
MODULE_LICENSE("GPL");

DTS binding for an SPI device

&spi0 {
    mcp3204@0 {
        compatible = "microchip,mcp3204";
        reg = <0>;                      /* chip select index */
        spi-max-frequency = <1000000>;  /* 1 MHz */
        spi-cpol;                       /* CPOL=1 */
        spi-cpha;                       /* CPHA=1 (SPI mode 3) */
    };
};

SPI mode matrix

Mode  CPOL  CPHA  Clock idle  Data sampled
  0     0     0   Low         Rising edge
  1     0     1   Low         Falling edge
  2     1     0   High        Falling edge
  3     1     1   High        Rising edge

regmap Abstraction {:.gc-mid}

Intermediate

regmap provides a unified register access API that works across I2C, SPI, MMIO, and other buses. It adds register caching, range checks, and reduces duplicated bus-specific read/write boilerplate.

#include <linux/regmap.h>

/* Register map configuration */
static const struct regmap_config mydev_regmap_config = {
    .reg_bits       = 8,    /* 8-bit register addresses */
    .val_bits       = 8,    /* 8-bit register values */
    .max_register   = 0x7F,

    /* Caching — REGCACHE_RBTREE stores written values, avoids re-reading */
    .cache_type     = REGCACHE_RBTREE,

    /* List read-only registers so cache doesn't serve stale writes */
    .writeable_reg  = mydev_writeable_reg,
    .readable_reg   = mydev_readable_reg,
    .volatile_reg   = mydev_volatile_reg,  /* don't cache — hardware changes these */
};

static bool mydev_volatile_reg(struct device *dev, unsigned int reg)
{
    switch (reg) {
    case REG_STATUS:
    case REG_IRQ_FLAGS:
        return true;   /* always read from hardware */
    default:
        return false;
    }
}

/* Probe: initialise regmap for I2C */
static int mydev_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct mydev_priv *priv;
    unsigned int val;
    int ret;

    priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->regmap = devm_regmap_init_i2c(client, &mydev_regmap_config);
    if (IS_ERR(priv->regmap))
        return PTR_ERR(priv->regmap);

    /* Read chip ID register */
    ret = regmap_read(priv->regmap, REG_CHIP_ID, &val);
    if (ret)
        return ret;
    if (val != EXPECTED_CHIP_ID) {
        dev_err(&client->dev, "unexpected chip ID 0x%02x\n", val);
        return -ENODEV;
    }

    /* Write a configuration register */
    ret = regmap_write(priv->regmap, REG_CONFIG, 0x42);
    if (ret)
        return ret;

    /* Read-modify-write: set bits 3:2, clear bit 0 */
    ret = regmap_update_bits(priv->regmap, REG_CONFIG,
                              BIT(3) | BIT(2) | BIT(0),  /* mask */
                              BIT(3) | BIT(2));           /* value */
    return ret;
}

regmap_field for bit-level register access

/* Define a field: bits [5:4] of REG_CONFIG */
static const struct reg_field MYDEV_RATE_FIELD =
    REG_FIELD(REG_CONFIG, 4, 5);

/* In probe */
priv->rate_field = devm_regmap_field_alloc(&client->dev,
                                            priv->regmap,
                                            MYDEV_RATE_FIELD);

/* Read/write the field (shifted and masked automatically) */
unsigned int rate;
regmap_field_read(priv->rate_field, &rate);
regmap_field_write(priv->rate_field, 2);

For SPI, replace devm_regmap_init_i2c with devm_regmap_init_spi(spi, &cfg). For MMIO, use devm_regmap_init_mmio(dev, base, &cfg).


UART and tty Layer {:.gc-mid}

Intermediate

UART drivers in Linux use the serial core (uart_driver, uart_port, uart_ops). The serial core sits between the tty layer (which userspace interacts with) and the hardware-specific driver.

#include <linux/serial_core.h>
#include <linux/serial.h>
#include <linux/tty.h>

#define MYUART_NR_PORTS  2

static struct uart_driver myuart_driver = {
    .owner        = THIS_MODULE,
    .driver_name  = "myuart",
    .dev_name     = "ttyMY",       /* creates /dev/ttyMY0, /dev/ttyMY1 */
    .major        = 0,             /* auto-assign */
    .minor        = 0,
    .nr           = MYUART_NR_PORTS,
};

/* uart_ops — hardware-specific callbacks */
static unsigned int myuart_tx_empty(struct uart_port *port)
{
    return (readl(port->membase + UART_STATUS) & TX_EMPTY) ?
           TIOCSER_TEMT : 0;
}

static void myuart_start_tx(struct uart_port *port)
{
    /* Enable TX interrupt */
    u32 ier = readl(port->membase + UART_IER);
    writel(ier | IER_TX, port->membase + UART_IER);
}

static void myuart_stop_tx(struct uart_port *port)
{
    u32 ier = readl(port->membase + UART_IER);
    writel(ier & ~IER_TX, port->membase + UART_IER);
}

static void myuart_set_termios(struct uart_port *port,
                                struct ktermios *new,
                                struct ktermios *old)
{
    unsigned int baud = uart_get_baud_rate(port, new, old, 0, 4000000);
    u32 div = port->uartclk / (16 * baud);
    /* Program divisor registers */
    writel(div, port->membase + UART_BAUD_DIV);
}

static const struct uart_ops myuart_ops = {
    .tx_empty     = myuart_tx_empty,
    .start_tx     = myuart_start_tx,
    .stop_tx      = myuart_stop_tx,
    .stop_rx      = myuart_stop_rx,
    .startup      = myuart_startup,
    .shutdown     = myuart_shutdown,
    .set_termios  = myuart_set_termios,
    .type         = myuart_type,
    .request_port = myuart_request_port,
    .release_port = myuart_release_port,
    .config_port  = myuart_config_port,
    .verify_port  = myuart_verify_port,
};

static int __init myuart_init(void)
{
    return uart_register_driver(&myuart_driver);
}
static void __exit myuart_exit(void)
{
    uart_unregister_driver(&myuart_driver);
}
module_init(myuart_init);
module_exit(myuart_exit);

The 8250 UART (drivers/tty/serial/8250/) is the most widely used UART driver. Most x86 COM ports, and many embedded UARTs compatible with the 16550 register layout, use it.


Advanced: I2C Mux and Bus Recovery {:.gc-adv}

Advanced

I2C Multiplexers

An I2C mux chip (e.g., PCA9548) switches connectivity between a single upstream I2C adapter and one of several downstream buses. The kernel models each downstream segment as a separate i2c_adapter.

&i2c1 {
    i2c-mux@70 {
        compatible = "nxp,pca9548";
        reg = <0x70>;
        #address-cells = <1>;
        #size-cells = <0>;

        i2c@0 {
            reg = <0>;
            /* downstream devices on channel 0 */
            sensor@48 { compatible = "ti,tmp102"; reg = <0x48>; };
        };
        i2c@1 {
            reg = <1>;
            /* downstream devices on channel 1 */
        };
    };
};

I2C Bus Recovery

If SCL or SDA is stuck low (a device holding the bus), the kernel can attempt recovery by bit-banging clock pulses.

#include <linux/i2c.h>

static void myuart_get_scl(struct i2c_adapter *adap)
{
    /* Read SCL GPIO */
}
static void myuart_set_scl(struct i2c_adapter *adap, int val)
{
    /* Drive SCL GPIO high/low */
}

static struct i2c_bus_recovery_info mybus_recovery = {
    .recover_bus    = i2c_generic_scl_recovery,
    .get_scl        = myuart_get_scl,
    .set_scl        = myuart_set_scl,
};

/* Assign during adapter registration */
adap->bus_recovery_info = &mybus_recovery;

SMBUS vs I2C differences

Feature SMBus I2C
Voltage levels 3.3V fixed 1.8V–5V
Max speed 100 kHz 400 kHz (fast), 3.4 MHz (HS)
Packet Error Check (PEC) Yes Optional
Timeout Mandatory (25–35 ms) Optional
Repeated START Supported Supported
Protocol Layered commands Raw byte sequences

Interview Q&A {:.gc-iq}

Interview Q&A

Q1: What are the key trade-offs between I2C and SPI for a sensor interface?

I2C uses only 2 wires (SDA+SCL) and supports multi-master and many devices on one bus, but is limited to ~400 kHz (fast mode) or ~1 MHz (fast-mode plus). SPI uses 4 wires minimum (MOSI, MISO, SCLK, plus one CS per device) but runs at tens of MHz, is full-duplex, and has simpler hardware. Choose I2C for slow sensors where wire count matters; choose SPI for high-speed ADCs, displays, or anything needing bulk data throughput.

Q2: Why is regmap useful in a driver that supports both I2C and SPI?

Without regmap, you’d write separate read_reg/write_reg functions and use runtime conditionals based on bus type. With regmap, you call devm_regmap_init_i2c or devm_regmap_init_spi in probe and then the rest of the driver calls regmap_read/regmap_write identically regardless of bus. You also get free register caching, range validation, and debugfs register dumps.

Q3: What do CPOL and CPHA mean in SPI?

CPOL (Clock Polarity) sets the idle state of the clock: 0 = idle low, 1 = idle high. CPHA (Clock Phase) sets when data is sampled: 0 = on the first (leading) edge, 1 = on the second (trailing) edge. The four combinations form SPI modes 0–3. The sensor datasheet specifies which mode it requires; the DTS spi-cpol / spi-cpha properties configure the controller accordingly.

Q4: What is I2C clock stretching and why does it matter for drivers?

Clock stretching allows an I2C slave to hold SCL low after the master releases it, effectively pausing the transfer while the slave prepares its response. Most hardware I2C controllers handle this transparently. However, some low-cost bit-banging controllers or I2C master implementations have a timeout for how long SCL can be held low. If a slow sensor stretches beyond that timeout, the transfer aborts. The driver may need to increase the controller timeout or use a device that supports clock stretching within the controller’s limits.

Q5: How does the Device Tree specify an I2C device address?

The reg property in the DT node of an I2C device contains the 7-bit address (e.g., reg = <0x48>). The I2C core reads this property and stores it in i2c_client.addr. There is no bus scan — the address is a static fact of the hardware design encoded in the DTS.

Q6: What is the difference between the tty layer and the serial core?

The tty layer (tty_driver, tty_operations) is the generic interface for character-based terminal devices — it handles line disciplines (UART, PPP, Bluetooth), buffering, and the POSIX terminal API (tcsetattr, signals, etc.). The serial core (uart_driver, uart_port) is a specialisation of the tty layer specifically for hardware UARTs. It provides the common UART features (baud rate, flow control, modem lines) so that individual UART hardware drivers only need to implement the register-level uart_ops callbacks.


References {:.gc-ref}

References

Resource Link
kernel.org: I2C and SMBus subsystem https://www.kernel.org/doc/html/latest/i2c/index.html
kernel.org: SPI subsystem https://www.kernel.org/doc/html/latest/spi/index.html
kernel.org: regmap API https://www.kernel.org/doc/html/latest/driver-api/regmap.html
kernel.org: serial driver documentation https://www.kernel.org/doc/html/latest/driver-api/serial/index.html
I2C bus recovery documentation https://www.kernel.org/doc/html/latest/i2c/i2c-protocol.html
Writing an I2C client driver https://www.kernel.org/doc/html/latest/i2c/writing-clients.html
SPI summary documentation https://www.kernel.org/doc/html/latest/spi/spi-summary.html