Skip to content

On creating Image sensor drivers in Zephyr

On creating Image sensor drivers in Zephyr

On creating Image sensor drivers in Zephyr

Embedded Vision · Zephyr RTOS

Camera Sensor Driver Guide (Zephyr)

A practical walkthrough for bringing up a camera sensor driver in Zephyr, from decoding the datasheet to integrating with the video subsystem and debugging the usual hardware-software traps.

Understand the Sensor First

Before writing any driver code, get comfortable with the sensor datasheet and the vendor's default register configuration, if one exists. Those documents usually look hostile at first: dense register maps, timing diagrams, PLL trees, startup sequences, and a lot of assumptions hidden between the lines.

Your first real job is not coding. It is building a mental model of how the sensor talks, what clocks it expects, which pixel formats it can emit, and what register sequence turns it from a silent chip into a streaming image source.

Control path

I2C / SCCB

This is the configuration channel. You use it to program exposure, enable test patterns, set PLL values, read the chip ID, and switch streaming on or off.

Data path

MIPI CSI-2 or DVP

This is the pixel firehose. The sensor pushes image data over a high-speed serial link like CSI-2, or over a parallel DVP bus with clock and sync signals.

Key parameters to extract

  • Sensor ID registers: the read-only identity values that confirm you are talking to the expected device.
  • Initialization sequence: the ordered list of register writes needed to make the sensor functional.
  • Supported resolutions: the actual frame sizes the silicon and vendor tuning support.
  • Pixel formats: RAW10, RAW12, YUV, RGB, or monochrome variants, each with different bandwidth and host-side expectations.
  • Clocking requirements: external master clock, internal PLL multiplication, dividers, and the resulting pixel rate.

Important: a register dump is not random glue code. It is usually generated or validated by the sensor vendor for a very specific mode, clock, and frame rate. Change values casually and the image can vanish, roll, or corrupt in ways that look like software bugs.

{0x0103, 0x01}, // software reset
{0x0300, 0x04}, // PLL pre-divider
{0x0302, 0x64}, // PLL multiplier
...

Zephyr Driver Architecture Overview

In Zephyr, camera drivers generally live under drivers/video/. The driver acts as a translator between the sensor's register-level behavior and Zephyr's higher-level video API.

The application should not need to know which I2C register enables streaming or which PLL register defines the pixel clock. The driver absorbs those details and exposes consistent operations like setting format, enumerating capabilities, and starting the stream.

Device configuration struct

Compile-time, read-only hardware information pulled from device tree: bus handle, I2C address, GPIOs, clocks, and lane-related wiring facts.

Runtime data struct

Mutable per-instance state such as current format, frame rate, exposure, streaming state, and cached controls.

Initialization function

The bring-up path that toggles reset, checks identity, applies the initial register list, and leaves the device ready.

Driver API

The function hooks that let the Zephyr video subsystem control the sensor without exposing raw register details.

Create Driver Skeleton

Set up the file layout before adding logic. This gives you the build-system hooks and Kconfig entry points needed to keep the driver aligned with the rest of Zephyr.

drivers/video/my_sensor.c
drivers/video/Kconfig.my_sensor
drivers/video/Kconfig
drivers/video/CMakeLists.txt
  • Add a symbol such as CONFIG_MY_SENSOR=y in Kconfig.my_sensor.
  • Include that Kconfig file from the main video driver Kconfig.
  • Register the source in the subsystem CMakeLists.txt.

Device Tree Binding

Device tree is where you describe how the sensor is physically connected. This lets the same driver work across multiple boards, as long as the compatible string and binding schema match.

&i2c1 {
    camera@36 {
        compatible = "vendor,my-sensor";
        reg = <0x36>;
        reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
        clocks = <&clk 0>;
    };
};

Then define the binding YAML under dts/bindings/video/vendor,my-sensor.yaml so Zephyr can validate the node properties and generate the correct access helpers.

compatible: "vendor,my-sensor"

properties:
  reg:
    required: true
    description: I2C slave address of the sensor

  reset-gpios:
    type: phandle-array
    description: GPIO connected to sensor reset pin

child-binding:
  child-binding:
    include: video-interfaces.yaml

Driver Data Structures

Good bring-up gets easier when the driver state is clearly partitioned into immutable hardware description and mutable runtime state. That separation matters even more when the same driver can instantiate more than one sensor.

struct my_sensor_config {
    struct i2c_dt_spec i2c;
    struct gpio_dt_spec reset_gpio;
};

struct my_sensor_ctrls {
    struct video_ctrl exposure;
};

struct my_sensor_data {
    struct my_sensor_ctrls ctrls;
    bool streaming;
};

Rule of thumb: anything that comes from the board description belongs in config. Anything that changes while the driver runs belongs in data.

I2C Helpers

Zephyr includes camera-oriented register helpers through the Camera Control Interface abstraction in zephyr/drivers/video/video_common.h. They save you from rewriting the same endian and address-width handling every time.

  • video_read_cci_reg reads a sensor register.
  • video_write_cci_reg writes one register.
  • video_write_cci_multiregsN writes a table of sensor registers, where N indicates address width.

These helpers are worth using early because bring-up bugs are already plentiful without adding homemade I2C packing mistakes.

Apply Register Configuration

Turn the vendor's init file into a clean C array and keep it readable. That table is the center of gravity for the whole driver, because every supported mode will eventually map back to a known-good register recipe.

static const struct video_reg16 init_regs[] = {
    {0x0103, 0x01},
    {0x0300, 0x04},
    {0x0302, 0x64},
    {0x0304, 0x03},
    {0x0306, 0x00},
    ...
};

Once mode support grows, split those arrays by resolution, frame rate, or output format rather than letting one giant table become impossible to audit.

Initialization Sequence

The init function is where hardware assumptions become visible. Reset timing, sensor wake-up time, chip ID reads, and the initial register burst all need to happen in the right order.

#define MY_SENSOR_CCI_ID MY_SENSOR_REG16(0x130)
#define MY_SENSOR_ID 0xDEAD

static int my_sensor_init(const struct device *dev)
{
    const struct my_sensor_config *cfg = dev->config;

    if (gpio_is_ready_dt(&cfg->reset_gpio)) {
        gpio_pin_configure_dt(&cfg->reset_gpio, GPIO_OUTPUT_ACTIVE);
        k_sleep(K_MSEC(10));
        gpio_pin_set_dt(&cfg->reset_gpio, 0);
        k_sleep(K_MSEC(10));
    }

    uint16_t id;
    int ret = video_read_cci_reg(&cfg->i2c, MY_SENSOR_CCI_ID, &id);
    if (ret < 0) {
        printk("Failed to read sensor ID %d\n", ret);
        return ret;
    }

    if (id != MY_SENSOR_ID) {
        return -ENODEV;
    }

    return video_write_cci_multiregs16(&cfg->i2c,
                                       init_regs,
                                       ARRAY_SIZE(init_regs));
}

Why this matters: if the sensor ID is wrong, stop immediately. It is better to fail cleanly than continue with a register list meant for a different device or an unresponsive bus.

Start / Stop Streaming

Many sensors expose a simple streaming register, often around 0x0100, that flips the output engine on or off. The actual implementation is small, but it is where the sensor transitions from configuration mode into live video output.

#define MY_SENSOR_CCI_STREAM_ON  0x0100

static int my_sensor_set_stream(const struct device *dev,
                                bool on,
                                enum video_buf_type type)
{
    const struct my_sensor_config *cfg = dev->config;

    if (type != VIDEO_BUF_TYPE_OUTPUT) {
        return -EINVAL;
    }

    uint8_t val = on ? 0x01 : 0x00;
    return video_write_cci_reg(&cfg->i2c, MY_SENSOR_CCI_STREAM_ON, val);
}

In practice, the hard part is not writing this register. It is making sure the downstream CSI receiver, clocks, and negotiated format are already correct before the first frame arrives.

Enumerate Frame Rates and Formats

A usable camera driver must advertise what the sensor can really do. Formats, resolutions, and frame intervals are not arbitrary; they must match concrete register sets and validated timing.

Supported modes

static const struct video_format_cap my_sensor_fmts[] = {
    {
        .pixelformat = VIDEO_PIX_FMT_Y10P,
        .width_min = 1280,
        .width_max = 1280,
        .height_min = 800,
        .height_max = 800,
        .width_step = 1,
        .height_step = 1,
    },
    {
        .pixelformat = VIDEO_PIX_FMT_Y10P,
        .width_min = 640,
        .width_max = 640,
        .height_min = 480,
        .height_max = 480,
        .width_step = 1,
        .height_step = 1,
    },
};

Frame intervals

static int my_sensor_enum_frmival(const struct device *dev,
                                 struct video_frmival_enum *fie)
{
    if (fie->index >= ARRAY_SIZE(my_sensor_fmts)) {
        return -EINVAL;
    }

    const struct my_sensor_fmts mode = my_sensor_fmts[fie->index];

    fie->width = mode->width;
    fie->height = mode->height;
    fie->pixel_format = mode->pixelformat;
    fie->type = VIDEO_FRMIVAL_TYPE_DISCRETE;
    fie->discrete.numerator = 1;
    fie->discrete.denominator = mode->fps;

    return 0;
}

Format negotiation

static int my_sensor_set_format(const struct device *dev,
                               enum video_buf_type type,
                               struct video_format *fmt)
{
    if (type != VIDEO_BUF_TYPE_OUTPUT) {
        return -EINVAL;
    }

    for (int i = 0; i < ARRAY_SIZE(my_sensor_fmts); i++) {
        if (fmt->width == my_sensor_fmts[i].width &&
            fmt->height == my_sensor_fmts[i].height &&
            fmt->pixelformat == my_sensor_fmts[i].pixelformat) {
            return 0;
        }
    }

    return -EINVAL;
}

Once you support multiple modes, tie each one to a register configuration that is known to produce the advertised output. Declaring a mode in software without a corresponding, validated register table is where many drivers drift into fiction.

Hook into Zephyr Device Model

Zephyr macros generate the boilerplate needed to create driver instances from device tree. That means you can support one or several sensors using the same driver code.

#define MY_SENSOR_INIT(inst)                                \
    static struct my_sensor_data data_##inst;               \
    static const struct my_sensor_config cfg_##inst = {     \
        .i2c = I2C_DT_SPEC_INST_GET(inst),                  \
        .reset_gpio = GPIO_DT_SPEC_INST_GET(inst, reset_gpios), \
    };                                                      \
    DEVICE_DT_INST_DEFINE(inst,                             \
                          my_sensor_init,                   \
                          NULL,                             \
                          &data_##inst,                     \
                          &cfg_##inst,                      \
                          POST_KERNEL,                      \
                          CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \
                          NULL);

DT_INST_FOREACH_STATUS_OKAY(MY_SENSOR_INIT)

This pattern keeps the sensor declaration close to the device tree description and avoids writing repetitive instantiation code by hand.

Integrate with Video Subsystem

To present the sensor as a Zephyr video device, define the video_driver_api structure and attach your callbacks. This is the bridge between the rest of the system and the sensor-specific logic you wrote.

static struct video_driver_api my_sensor_api = {
    .set_format = my_sensor_set_format,
    .set_stream = my_sensor_set_stream,
    .set_ctrl = my_sensor_set_ctrl,
    .get_format = my_sensor_get_format,
    .get_caps = my_sensor_get_caps,
    .set_frmival = my_sensor_set_frmival,
    .get_frmival = my_sensor_get_frmival,
    .enum_frmival = my_sensor_enum_frmival
};

After that point, applications can query capabilities and configure the camera without ever knowing the register-level details behind the scenes.

Debugging Tips

Camera bring-up failures often look mysterious, but they usually collapse into a handful of recurring categories. A disciplined debug order saves a lot of time.

No ACK on I2C

Start with the obvious: wrong I2C address, held reset line, missing power rail, or the sensor clock never started.

Sensor ID mismatch

Readbacks like 0xFFFF or 0x0000 often point to endianness issues, reset timing problems, or a dead bus.

No image data

If register writes succeed but no frames arrive, verify MCLK, PLL programming, stream enable, and receiver-side expectations.

Corrupted image

Garbled or rolling output usually means the receiver and sensor disagree about format, lane count, sync, or pixel packing.

Useful tools

  • Logic analyzer with I2C decode: the fastest way to confirm whether register transactions are actually reaching the device.
  • Oscilloscope: essential for checking MCLK, reset pulse behavior, and signal integrity assumptions.
  • Zephyr logging: add focused logs around register writes, stream transitions, and error returns.
LOG_MODULE_REGISTER(my_sensor, LOG_LEVEL_DBG);
LOG_DBG("Writing register 0x%04x = 0x%02x", reg, val);
LOG_ERR("I2C transaction failed: %d", err);

The Key Insights

Most working camera drivers rest on three pillars. If one of them is even slightly wrong, the resulting failure can look deceptively unrelated.

Pillar 1

Correct hardware configuration

Clock frequency, reset polarity, voltage rails, pin mapping, lane wiring, and signal integrity all have to be right before software gets a fair chance.

Pillar 2

Accurate register initialization

The register list needs to match the sensor mode, expected input clock, output format, and target frame rate.

Pillar 3

Proper Zephyr integration

The driver has to expose real capabilities, translate control requests correctly, and fit the video subsystem model cleanly.

First checks when nothing works: confirm the MCLK frequency, confirm the PLL assumptions in the register sequence, and confirm that the receiver actually supports the sensor's output format. Those three checks eliminate a huge fraction of early bring-up failures.

Join Our Developer Community

Connect with experts, get technical support, and accelerate your vision AI development with our active community.

Discord Community

Collaborate with innovators on Discord