drivers: video: common: introduce CCI utilities

Add a library for the Camera Common Interface, part of the MIPI CSI
protocol standard defining methods to configure a camera device over I2C,
such as which size for the register address/data.

Signed-off-by: Josuah Demangeon <me@josuah.net>
This commit is contained in:
Josuah Demangeon 2025-03-31 20:02:56 +00:00 committed by Benjamin Cabé
commit c8ff2b89d9
3 changed files with 442 additions and 2 deletions

View file

@ -50,6 +50,14 @@ config VIDEO_BUFFER_SMH_ATTRIBUTE
1: SMH_REG_ATTR_NON_CACHEABLE
2: SMH_REG_ATTR_EXTERNAL
config VIDEO_I2C_RETRY_NUM
int "Number of retries after a failed I2C communication"
default 0
help
If set to 0, only a single write attempt will be done with no retry.
The default is to not retry. Board configuration files or user project can then
use the number of retries that matches their situation.
source "drivers/video/Kconfig.esp32_dvp"
source "drivers/video/Kconfig.mcux_csi"

View file

@ -1,14 +1,23 @@
/*
* Copyright (c) 2019, Linaro Limited
* Copyright (c) 2024, tinyVision.ai Inc.
* Copyright (c) 2024-2025, tinyVision.ai Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/video.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include "video_common.h"
LOG_MODULE_REGISTER(video_common, CONFIG_VIDEO_LOG_LEVEL);
#if defined(CONFIG_VIDEO_BUFFER_USE_SHARED_MULTI_HEAP)
#include <zephyr/multi_heap/shared_multi_heap.h>
@ -166,3 +175,193 @@ void video_closest_frmival(const struct device *dev, struct video_frmival_enum *
}
}
}
static int video_read_reg_retry(const struct i2c_dt_spec *i2c, uint8_t *buf_w, size_t size_w,
uint8_t *buf_r, size_t size_r)
{
int ret;
for (int i = 0;; i++) {
ret = i2c_write_read_dt(i2c, buf_w, size_w, buf_r, size_r);
if (ret == 0) {
break;
}
if (i == CONFIG_VIDEO_I2C_RETRY_NUM) {
LOG_HEXDUMP_ERR(buf_w, size_w, "failed to write-read to I2C register");
return ret;
}
k_sleep(K_MSEC(1));
}
return 0;
}
int video_read_cci_reg(const struct i2c_dt_spec *i2c, uint32_t reg_addr, uint32_t *reg_data)
{
size_t addr_size = FIELD_GET(VIDEO_REG_ADDR_SIZE_MASK, reg_addr);
size_t data_size = FIELD_GET(VIDEO_REG_DATA_SIZE_MASK, reg_addr);
bool big_endian = FIELD_GET(VIDEO_REG_ENDIANNESS_MASK, reg_addr);
uint16_t addr = FIELD_GET(VIDEO_REG_ADDR_MASK, reg_addr);
uint8_t buf_w[sizeof(uint16_t)] = {0};
uint8_t *data_ptr;
int ret;
__ASSERT(addr_size > 0, "The address must have a address size flag");
__ASSERT(data_size > 0, "The address must have a data size flag");
*reg_data = 0;
if (big_endian) {
/* Casting between data sizes in big-endian requires re-aligning */
data_ptr = (uint8_t *)reg_data + sizeof(*reg_data) - data_size;
} else {
/* Casting between data sizes in little-endian is a no-op */
data_ptr = (uint8_t *)reg_data;
}
for (int i = 0; i < data_size; i++) {
if (addr_size == 1) {
buf_w[0] = addr + i;
} else {
sys_put_be16(addr + i, &buf_w[0]);
}
ret = video_read_reg_retry(i2c, buf_w, addr_size, &data_ptr[i], 1);
if (ret < 0) {
LOG_ERR("Failed to read from register 0x%x", addr + i);
return ret;
}
LOG_HEXDUMP_DBG(buf_w, addr_size, "Data written to the I2C device...");
LOG_HEXDUMP_DBG(&data_ptr[i], 1, "... data read back from the I2C device");
}
*reg_data = big_endian ? sys_be32_to_cpu(*reg_data) : sys_le32_to_cpu(*reg_data);
return 0;
}
static int video_write_reg_retry(const struct i2c_dt_spec *i2c, uint8_t *buf_w, size_t size)
{
int ret;
for (int i = 0;; i++) {
ret = i2c_write_dt(i2c, buf_w, size);
if (ret == 0) {
break;
}
if (i == CONFIG_VIDEO_I2C_RETRY_NUM) {
LOG_HEXDUMP_ERR(buf_w, size, "failed to write to I2C register");
return ret;
}
k_sleep(K_MSEC(1));
}
return 0;
}
int video_write_cci_reg(const struct i2c_dt_spec *i2c, uint32_t reg_addr, uint32_t reg_data)
{
size_t addr_size = FIELD_GET(VIDEO_REG_ADDR_SIZE_MASK, reg_addr);
size_t data_size = FIELD_GET(VIDEO_REG_DATA_SIZE_MASK, reg_addr);
bool big_endian = FIELD_GET(VIDEO_REG_ENDIANNESS_MASK, reg_addr);
uint16_t addr = FIELD_GET(VIDEO_REG_ADDR_MASK, reg_addr);
uint8_t buf_w[sizeof(uint16_t) + sizeof(uint32_t)] = {0};
uint8_t *data_ptr;
int ret;
__ASSERT(addr_size > 0, "The address must have a address size flag");
__ASSERT(data_size > 0, "The address must have a data size flag");
if (big_endian) {
/* Casting between data sizes in big-endian requires re-aligning */
reg_data = sys_cpu_to_be32(reg_data);
data_ptr = (uint8_t *)&reg_data + sizeof(reg_data) - data_size;
} else {
/* Casting between data sizes in little-endian is a no-op */
reg_data = sys_cpu_to_le32(reg_data);
data_ptr = (uint8_t *)&reg_data;
}
for (int i = 0; i < data_size; i++) {
/* The address is always big-endian as per CCI standard */
if (addr_size == 1) {
buf_w[0] = addr + i;
} else {
sys_put_be16(addr + i, &buf_w[0]);
}
buf_w[addr_size] = data_ptr[i];
LOG_HEXDUMP_DBG(buf_w, addr_size + 1, "Data written to the I2C device");
ret = video_write_reg_retry(i2c, buf_w, addr_size + 1);
if (ret < 0) {
LOG_ERR("Failed to write to register 0x%x", addr + i);
return ret;
}
}
return 0;
}
int video_modify_cci_reg(const struct i2c_dt_spec *i2c, uint32_t reg_addr, uint32_t field_mask,
uint32_t field_value)
{
uint32_t reg;
int ret;
ret = video_read_cci_reg(i2c, reg_addr, &reg);
if (ret < 0) {
return ret;
}
return video_write_cci_reg(i2c, reg_addr, (reg & ~field_mask) | field_value);
}
int video_write_cci_multiregs(const struct i2c_dt_spec *i2c, const struct video_reg *regs,
size_t num_regs)
{
int ret;
for (int i = 0; i < num_regs; i++) {
ret = video_write_cci_reg(i2c, regs[i].addr, regs[i].data);
if (ret < 0) {
return ret;
}
}
return 0;
}
int video_write_cci_multiregs8(const struct i2c_dt_spec *i2c, const struct video_reg8 *regs,
size_t num_regs)
{
int ret;
for (int i = 0; i < num_regs; i++) {
ret = video_write_cci_reg(i2c, regs[i].addr | VIDEO_REG_ADDR8_DATA8, regs[i].data);
if (ret < 0) {
return ret;
}
}
return 0;
}
int video_write_cci_multiregs16(const struct i2c_dt_spec *i2c, const struct video_reg16 *regs,
size_t num_regs)
{
int ret;
for (int i = 0; i < num_regs; i++) {
ret = video_write_cci_reg(i2c, regs[i].addr | VIDEO_REG_ADDR16_DATA8, regs[i].data);
if (ret < 0) {
return ret;
}
}
return 0;
}

View file

@ -0,0 +1,233 @@
/*
* Copyright (c) 2025 tinyVision.ai Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_DRIVERS_VIDEO_COMMON_H_
#define ZEPHYR_DRIVERS_VIDEO_COMMON_H_
#include <stddef.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/types.h>
/**
* @brief Register for building tables supporting 8/16 bit address and 8/16/24/32 bit value sizes.
*
* A flag in the address indicates the size of the register address, and the size and endianness of
* the value.
*
* If willing to save space on large register tables, a more compact version @ref video_reg8 and
* @ref video_reg16 can be used, however only supporting 8-bit values .
*
* The data field is in CPU-native endianness, and the library functions will perform the
* endianness swap as needed while transmitting data.
*/
struct video_reg {
/** Address of the register, and other flags if used with the @ref video_cci API. */
uint32_t addr;
/** Value to write to this address */
uint32_t data;
};
/** Register for building tables supporting 8-bit addresses and 8-bit value sizes */
struct video_reg8 {
/** Address of the register */
uint8_t addr;
/** Value to write to this address */
uint8_t data;
};
/** Register for building tables supporting 16-bit addresses and 8-bit value sizes */
struct video_reg16 {
/** Address of the register */
uint16_t addr;
/** Value to write to this address */
uint8_t data;
};
/**
* @defgroup video_cci Video Camera Control Interface (CCI) handling
*
* The Camera Control Interface (CCI) is an I2C communication scheme part of MIPI-CSI.
* It defines how register addresses and register values are packed into I2C messages.
*
* After the I2C device address, I2C messages payload contain:
*
* 1. The 8-bit or 16-bit of the address in big-endian, written to the device.
* 2. The 8-bit of the register data either read or written.
*
* To write to registers larger than 8-bit, multiple read/writes messages are issued.
* Endianness and segmentation of larger registers are defined on a per-sensor basis.
*
* @{
*/
/** @cond INTERNAL_HIDDEN */
#define VIDEO_REG_ENDIANNESS_MASK (uint32_t)(GENMASK(24, 24))
#define VIDEO_REG_ADDR_SIZE_MASK (uint32_t)(GENMASK(23, 20))
#define VIDEO_REG_DATA_SIZE_MASK (uint32_t)(GENMASK(19, 16))
#define VIDEO_REG_ADDR_MASK (uint32_t)(GENMASK(15, 0))
#define VIDEO_REG(addr_size, data_size, endianness) \
(FIELD_PREP(VIDEO_REG_ADDR_SIZE_MASK, (addr_size)) | \
FIELD_PREP(VIDEO_REG_DATA_SIZE_MASK, (data_size)) | \
FIELD_PREP(VIDEO_REG_ENDIANNESS_MASK, (endianness)))
/** @endcond */
/**
* @defgroup video_cci_reg_flags Flags describing a Video CCI register
*
* For a given drivers, register sizes are not expected to change, so macros can be used when
* defining registers:
*
* @code{.c}
* #define SENSORNAME_REG8(addr) ((uint32_t)(addr) | VIDEO_REG_ADDR16_DATA8)
* #define SENSORNAME_REG16(addr) ((uint32_t)(addr) | VIDEO_REG_ADDR16_DATA16_LE)
* #define SENSORNAME_REG24(addr) ((uint32_t)(addr) | VIDEO_REG_ADDR16_DATA24_LE)
* @endcode
*
* Then the macros can be used directly in register definitions:
*
* @code{.c}
* #define SENSORNAME_REG_EXPOSURE_LEVEL SENSORNAME_REG16(0x3060)
* #define SENSORNAME_REG_AN_GAIN_LEVEL SENSORNAME_REG8(0x3062)
* ...
* video_write_cci_reg(&cfg->i2c, SENSORNAME_REG_EXPOSURE_LEVEL, 3000);
* video_write_cci_reg(&cfg->i2c, SENSORNAME_REG_AN_GAIN_LEVEL, 220);
* @endcode
*
* Or used directly inline:
* @code{.c}
* video_write_cci_reg(&cfg->i2c, SENSORNAME_REG16(0x3060), 3000);
* video_write_cci_reg(&cfg->i2c, SENSORNAME_REG8(0x3062), 220);
* @endcode
*
* As well as in register tables:
*
* @code{.c}
* struct video_reg init_regs[] = {
* {SENSORNAME_REG_EXPOSURE_LEVEL, 2000},
* {SENSORNAME_REG_AN_GAIN_LEVEL, 180},
* ...
* {0},
* };
* @endcode
*
* @{
*/
/** Flag a register as 8-bit address size, 8-bit data size */
#define VIDEO_REG_ADDR8_DATA8 VIDEO_REG(1, 1, false)
/** Flag a register as 8-bit address size, 16-bit data size, little-endian */
#define VIDEO_REG_ADDR8_DATA16_LE VIDEO_REG(1, 2, false)
/** Flag a register as 8-bit address size, 16-bit data size, big-endian */
#define VIDEO_REG_ADDR8_DATA16_BE VIDEO_REG(1, 2, true)
/** Flag a register as 8-bit address size, 24-bit data size, little-endian */
#define VIDEO_REG_ADDR8_DATA24_LE VIDEO_REG(1, 3, false)
/** Flag a register as 8-bit address size, 24-bit data size, big-endian */
#define VIDEO_REG_ADDR8_DATA24_BE VIDEO_REG(1, 3, true)
/** Flag a register as 8-bit address size, 32-bit data size, little-endian */
#define VIDEO_REG_ADDR8_DATA32_LE VIDEO_REG(1, 4, false)
/** Flag a register as 8-bit address size, 32-bit data size, big-endian */
#define VIDEO_REG_ADDR8_DATA32_BE VIDEO_REG(1, 4, true)
/** Flag a register as 16-bit address size, 8-bit data size */
#define VIDEO_REG_ADDR16_DATA8 VIDEO_REG(2, 1, false)
/** Flag a register as 16-bit address size, 16-bit data size, little-endian */
#define VIDEO_REG_ADDR16_DATA16_LE VIDEO_REG(2, 2, false)
/** Flag a register as 16-bit address size, 16-bit data size, big-endian */
#define VIDEO_REG_ADDR16_DATA16_BE VIDEO_REG(2, 2, true)
/** Flag a register as 16-bit address size, 24-bit data size, little-endian */
#define VIDEO_REG_ADDR16_DATA24_LE VIDEO_REG(2, 3, false)
/** Flag a register as 16-bit address size, 24-bit data size, big-endian */
#define VIDEO_REG_ADDR16_DATA24_BE VIDEO_REG(2, 3, true)
/** Flag a register as 16-bit address size, 32-bit data size, little-endian */
#define VIDEO_REG_ADDR16_DATA32_LE VIDEO_REG(2, 4, false)
/** Flag a register as 16-bit address size, 32-bit data size, big-endian */
#define VIDEO_REG_ADDR16_DATA32_BE VIDEO_REG(2, 4, true)
/** @} */
/**
* @brief Write a Camera Control Interface register value with the specified address and size.
*
* The size of the register address and register data passed as flags in the high bits of
* @p reg_addr in the unused bits of the address.
*
* @brief i2c Reference to the video device on an I2C bus.
* @brief reg_addr Address of the register to fill with @p reg_data along with size information.
* @brief reg_data Value to write at this address, the size to write is encoded in the address.
*/
int video_write_cci_reg(const struct i2c_dt_spec *i2c, uint32_t reg_addr, uint32_t reg_data);
/**
* @brief Perform a read-modify-write operation on a register given an address, mask and value.
*
* The size of the register address and register data passed as flags in the high bits of
* @p reg_addr in the unused bits of the address.
*
* @brief i2c Reference to the video device on an I2C bus.
* @brief reg_addr Address of the register to fill with @p reg_data along with size information.
* @brief field_mask Mask of the field to insert into the existing value.
* @brief field_value Value to write at this address, the size to write is encoded in the address.
*/
int video_modify_cci_reg(const struct i2c_dt_spec *i2c, uint32_t reg_addr, uint32_t field_mask,
uint32_t field_value);
/**
* @brief Read a Camera Control Interace register value from the specified address and size.
*
* The size of the register address and register data passed as flags in the high bits of
* @p reg_addr in the unused bits of the address.
*
* @brief i2c Reference to the video device on an I2C bus.
* @brief reg_addr Address of the register to fill with @p reg_data along with size information.
* @brief reg_data Value to write at this address, the size to write is encoded in the address.
* This is a 32-bit integer pointer even when reading 8-bit or 16 bits value.
*/
int video_read_cci_reg(const struct i2c_dt_spec *i2c, uint32_t reg_addr, uint32_t *reg_data);
/**
* @brief Write a complete table of registers to a device one by one.
*
* The address present in the registers need to be encoding the size information using the macros
* such as @ref VIDEO_REG_ADDR16_DATA8. The last element must be empty (@c {0}) to mark the end of
* the table.
*
* @brief i2c Reference to the video device on an I2C bus.
* @brief regs Array of address/value pairs to write to the device sequentially.
* @brief num_regs Number of registers entries in the table.
*/
int video_write_cci_multiregs(const struct i2c_dt_spec *i2c, const struct video_reg *regs,
size_t num_regs);
/**
* @brief Write a complete table of registers to a device one by one.
*
* The registers address are 8-bit wide and values are 8-bit wide.
*
* @brief i2c Reference to the video device on an I2C bus.
* @brief regs Array of address/value pairs to write to the device sequentially.
* @brief num_regs Number of registers entries in the table.
*/
int video_write_cci_multiregs8(const struct i2c_dt_spec *i2c, const struct video_reg8 *regs,
size_t num_regs);
/**
* @brief Write a complete table of registers to a device one by one.
*
* The registers address are 16-bit wide and values are 8-bit wide.
*
* @brief i2c Reference to the video device on an I2C bus.
* @brief regs Array of address/value pairs to write to the device sequentially.
* @brief num_regs Number of registers entries in the table.
*/
int video_write_cci_multiregs16(const struct i2c_dt_spec *i2c, const struct video_reg16 *regs,
size_t num_regs);
/** @} */
#endif /* ZEPHYR_DRIVERS_VIDEO_COMMON_H_ */