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.
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.
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=yinKconfig.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_regreads a sensor register. -
video_write_cci_regwrites one register. -
video_write_cci_multiregsNwrites a table of sensor registers, whereNindicates 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.
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.
Accurate register initialization
The register list needs to match the sensor mode, expected input clock, output format, and target frame rate.
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.