Add a new implementation of a test pattern generator, with the same architecture as real drivers: split receiver core and I2C-controlled sub-device, with changes of video format in "zephyr,emul-imager" leads to different data produced by "zephyr,emul-rx". Signed-off-by: Josuah Demangeon <me@josuah.net>
497 lines
15 KiB
C
497 lines
15 KiB
C
/*
|
|
* Copyright (c) 2024 tinyVision.ai Inc.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT zephyr_video_emul_imager
|
|
|
|
#include <string.h>
|
|
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/sys/util.h>
|
|
#include <zephyr/sys/byteorder.h>
|
|
#include <zephyr/drivers/video.h>
|
|
#include <zephyr/drivers/video-controls.h>
|
|
#include <zephyr/drivers/i2c.h>
|
|
#include <zephyr/logging/log.h>
|
|
|
|
LOG_MODULE_REGISTER(video_emul_imager, CONFIG_VIDEO_LOG_LEVEL);
|
|
|
|
#define EMUL_IMAGER_REG_SENSOR_ID 0x0000
|
|
#define EMUL_IMAGER_SENSOR_ID 0x99
|
|
#define EMUL_IMAGER_REG_CTRL 0x0001
|
|
#define EMUL_IMAGER_REG_INIT1 0x0002
|
|
#define EMUL_IMAGER_REG_INIT2 0x0003
|
|
#define EMUL_IMAGER_REG_TIMING1 0x0004
|
|
#define EMUL_IMAGER_REG_TIMING2 0x0005
|
|
#define EMUL_IMAGER_REG_TIMING3 0x0006
|
|
#define EMUL_IMAGER_REG_EXPOSURE 0x0007
|
|
#define EMUL_IMAGER_REG_GAIN 0x0008
|
|
#define EMUL_IMAGER_REG_PATTERN 0x0009
|
|
#define EMUL_IMAGER_PATTERN_OFF 0x00
|
|
#define EMUL_IMAGER_PATTERN_BARS1 0x01
|
|
#define EMUL_IMAGER_PATTERN_BARS2 0x02
|
|
|
|
/* Emulated register bank */
|
|
uint8_t emul_imager_fake_regs[10];
|
|
|
|
enum emul_imager_fmt_id {
|
|
RGB565_64x20,
|
|
YUYV_64x20,
|
|
};
|
|
|
|
struct emul_imager_reg {
|
|
uint16_t addr;
|
|
uint8_t value;
|
|
};
|
|
|
|
struct emul_imager_mode {
|
|
uint8_t fps;
|
|
/* List of registers lists to configure the various properties of the sensor.
|
|
* This permits to deduplicate the list of registers in case some lare sections
|
|
* are repeated across modes, such as the resolution for different FPS.
|
|
*/
|
|
const struct emul_imager_reg *regs[2];
|
|
/* More fields can be added according to the needs of the sensor driver */
|
|
};
|
|
|
|
struct emul_imager_config {
|
|
struct i2c_dt_spec i2c;
|
|
};
|
|
|
|
struct emul_imager_data {
|
|
/* First field is a framebuffer for I/O emulation purpose */
|
|
uint8_t framebuffer[CONFIG_VIDEO_EMUL_IMAGER_FRAMEBUFFER_SIZE];
|
|
/* Other fields are shared with real hardware drivers */
|
|
const struct emul_imager_mode *mode;
|
|
enum emul_imager_fmt_id fmt_id;
|
|
struct video_format fmt;
|
|
};
|
|
|
|
/* Initial parameters of the sensors common to all modes. */
|
|
static const struct emul_imager_reg emul_imager_init_regs[] = {
|
|
{EMUL_IMAGER_REG_CTRL, 0x00},
|
|
/* Example comment about REG_INIT1 */
|
|
{EMUL_IMAGER_REG_INIT1, 0x10},
|
|
{EMUL_IMAGER_REG_INIT2, 0x00},
|
|
{0},
|
|
};
|
|
|
|
/* List of registers aggregated together in "modes" that can be applied
|
|
* to set the timing parameters and other mode-dependent configuration.
|
|
*/
|
|
|
|
static const struct emul_imager_reg emul_imager_rgb565_64x20[] = {
|
|
{EMUL_IMAGER_REG_TIMING1, 0x64},
|
|
{EMUL_IMAGER_REG_TIMING2, 0x20},
|
|
{0},
|
|
};
|
|
static const struct emul_imager_reg emul_imager_rgb565_64x20_15fps[] = {
|
|
{EMUL_IMAGER_REG_TIMING3, 15},
|
|
{0},
|
|
};
|
|
static const struct emul_imager_reg emul_imager_rgb565_64x20_30fps[] = {
|
|
{EMUL_IMAGER_REG_TIMING3, 30},
|
|
{0},
|
|
};
|
|
static const struct emul_imager_reg emul_imager_rgb565_64x20_60fps[] = {
|
|
{EMUL_IMAGER_REG_TIMING3, 60},
|
|
{0},
|
|
};
|
|
struct emul_imager_mode emul_imager_rgb565_64x20_modes[] = {
|
|
{.fps = 15, .regs = {emul_imager_rgb565_64x20, emul_imager_rgb565_64x20_15fps}},
|
|
{.fps = 30, .regs = {emul_imager_rgb565_64x20, emul_imager_rgb565_64x20_30fps}},
|
|
{.fps = 60, .regs = {emul_imager_rgb565_64x20, emul_imager_rgb565_64x20_60fps}},
|
|
{0},
|
|
};
|
|
|
|
static const struct emul_imager_reg emul_imager_yuyv_64x20[] = {
|
|
{EMUL_IMAGER_REG_TIMING1, 0x64},
|
|
{EMUL_IMAGER_REG_TIMING2, 0x20},
|
|
{0},
|
|
};
|
|
static const struct emul_imager_reg emul_imager_yuyv_64x20_15fps[] = {
|
|
{EMUL_IMAGER_REG_TIMING3, 15},
|
|
{0},
|
|
};
|
|
static const struct emul_imager_reg emul_imager_yuyv_64x20_30fps[] = {
|
|
{EMUL_IMAGER_REG_TIMING3, 30},
|
|
{0},
|
|
};
|
|
struct emul_imager_mode emul_imager_yuyv_64x20_modes[] = {
|
|
{.fps = 15, .regs = {emul_imager_yuyv_64x20, emul_imager_yuyv_64x20_15fps}},
|
|
{.fps = 30, .regs = {emul_imager_yuyv_64x20, emul_imager_yuyv_64x20_30fps}},
|
|
{0},
|
|
};
|
|
|
|
/* Summary of all the modes of all the frame formats, with the format ID as
|
|
* index, matching fmts[].
|
|
*/
|
|
static const struct emul_imager_mode *emul_imager_modes[] = {
|
|
[RGB565_64x20] = emul_imager_rgb565_64x20_modes,
|
|
[YUYV_64x20] = emul_imager_yuyv_64x20_modes,
|
|
};
|
|
|
|
/* Video device capabilities where the supported resolutions and pixel formats are listed.
|
|
* The format ID is used as index to fetch the matching mode from the list above.
|
|
*/
|
|
#define EMUL_IMAGER_VIDEO_FORMAT_CAP(width, height, format) \
|
|
{ \
|
|
.pixelformat = (format), \
|
|
.width_min = (width), \
|
|
.width_max = (width), \
|
|
.height_min = (height), \
|
|
.height_max = (height), \
|
|
.width_step = 0, \
|
|
.height_step = 0, \
|
|
}
|
|
static const struct video_format_cap fmts[] = {
|
|
[RGB565_64x20] = EMUL_IMAGER_VIDEO_FORMAT_CAP(64, 20, VIDEO_PIX_FMT_RGB565),
|
|
[YUYV_64x20] = EMUL_IMAGER_VIDEO_FORMAT_CAP(64, 20, VIDEO_PIX_FMT_YUYV),
|
|
{0},
|
|
};
|
|
|
|
/* Emulated I2C register interface, to replace with actual I2C calls for real hardware */
|
|
static int emul_imager_read_reg(const struct device *const dev, uint8_t reg_addr, uint8_t *value)
|
|
{
|
|
LOG_DBG("%s placeholder for I2C read from 0x%02x", dev->name, reg_addr);
|
|
switch (reg_addr) {
|
|
case EMUL_IMAGER_REG_SENSOR_ID:
|
|
*value = EMUL_IMAGER_SENSOR_ID;
|
|
break;
|
|
default:
|
|
*value = emul_imager_fake_regs[reg_addr];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Helper to read a full integer directly from a register */
|
|
static int emul_imager_read_int(const struct device *const dev, uint8_t reg_addr, int *value)
|
|
{
|
|
uint8_t val8;
|
|
int ret;
|
|
|
|
ret = emul_imager_read_reg(dev, reg_addr, &val8);
|
|
*value = val8;
|
|
return ret;
|
|
}
|
|
|
|
/* Some sensors will need reg8 or reg16 variants. */
|
|
static int emul_imager_write_reg(const struct device *const dev, uint8_t reg_addr, uint8_t value)
|
|
{
|
|
LOG_DBG("%s placeholder for I2C write 0x%08x to 0x%02x", dev->name, value, reg_addr);
|
|
emul_imager_fake_regs[reg_addr] = value;
|
|
return 0;
|
|
}
|
|
|
|
static int emul_imager_write_multi(const struct device *const dev,
|
|
const struct emul_imager_reg *regs)
|
|
{
|
|
int ret;
|
|
|
|
for (int i = 0; regs[i].addr != 0; i++) {
|
|
ret = emul_imager_write_reg(dev, regs[i].addr, regs[i].value);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int emul_imager_set_ctrl(const struct device *dev, unsigned int cid, void *value)
|
|
{
|
|
switch (cid) {
|
|
case VIDEO_CID_EXPOSURE:
|
|
return emul_imager_write_reg(dev, EMUL_IMAGER_REG_EXPOSURE, (int)value);
|
|
case VIDEO_CID_GAIN:
|
|
return emul_imager_write_reg(dev, EMUL_IMAGER_REG_GAIN, (int)value);
|
|
case VIDEO_CID_TEST_PATTERN:
|
|
return emul_imager_write_reg(dev, EMUL_IMAGER_REG_PATTERN, (int)value);
|
|
default:
|
|
return -ENOTSUP;
|
|
}
|
|
}
|
|
|
|
static int emul_imager_get_ctrl(const struct device *dev, unsigned int cid, void *value)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
|
|
switch (cid) {
|
|
case VIDEO_CID_EXPOSURE:
|
|
return emul_imager_read_int(dev, EMUL_IMAGER_REG_EXPOSURE, value);
|
|
case VIDEO_CID_GAIN:
|
|
return emul_imager_read_int(dev, EMUL_IMAGER_REG_GAIN, value);
|
|
case VIDEO_CID_TEST_PATTERN:
|
|
return emul_imager_read_int(dev, EMUL_IMAGER_REG_PATTERN, value);
|
|
case VIDEO_CID_PIXEL_RATE:
|
|
*(int64_t *)value = (int64_t)data->fmt.width * data->fmt.pitch * data->mode->fps;
|
|
return 0;
|
|
default:
|
|
return -ENOTSUP;
|
|
}
|
|
}
|
|
|
|
/* Customize this function according to your "struct emul_imager_mode". */
|
|
static int emul_imager_set_mode(const struct device *dev, const struct emul_imager_mode *mode)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
int ret;
|
|
|
|
if (data->mode == mode) {
|
|
return 0;
|
|
}
|
|
|
|
LOG_DBG("Applying mode %p at %d FPS", mode, mode->fps);
|
|
|
|
/* Apply all the configuration registers for that mode */
|
|
for (int i = 0; i < 2; i++) {
|
|
ret = emul_imager_write_multi(dev, mode->regs[i]);
|
|
if (ret < 0) {
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
data->mode = mode;
|
|
return 0;
|
|
err:
|
|
LOG_ERR("Could not apply %s mode %p (%u FPS)", dev->name, mode, mode->fps);
|
|
return ret;
|
|
}
|
|
|
|
static int emul_imager_set_frmival(const struct device *dev, enum video_endpoint_id ep,
|
|
struct video_frmival *frmival)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
struct video_frmival_enum fie = {.format = &data->fmt, .discrete = *frmival};
|
|
|
|
if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
video_closest_frmival(dev, ep, &fie);
|
|
LOG_DBG("Applying frame interval number %u", fie.index);
|
|
return emul_imager_set_mode(dev, &emul_imager_modes[data->fmt_id][fie.index]);
|
|
}
|
|
|
|
static int emul_imager_get_frmival(const struct device *dev, enum video_endpoint_id ep,
|
|
struct video_frmival *frmival)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
|
|
if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
frmival->numerator = 1;
|
|
frmival->denominator = data->mode->fps;
|
|
return 0;
|
|
}
|
|
|
|
static int emul_imager_enum_frmival(const struct device *dev, enum video_endpoint_id ep,
|
|
struct video_frmival_enum *fie)
|
|
{
|
|
const struct emul_imager_mode *mode;
|
|
size_t fmt_id;
|
|
int ret;
|
|
|
|
if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = video_format_caps_index(fmts, fie->format, &fmt_id);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
mode = &emul_imager_modes[fmt_id][fie->index];
|
|
|
|
fie->type = VIDEO_FRMIVAL_TYPE_DISCRETE;
|
|
fie->discrete.numerator = 1;
|
|
fie->discrete.denominator = mode->fps;
|
|
fie->index++;
|
|
|
|
return mode->fps == 0;
|
|
}
|
|
|
|
/* White, Yellow, Cyan, Green, Magenta, Red, Blue, Black */
|
|
static const uint16_t pattern_8bars_yuv[8][3] = {
|
|
{0xFF, 0x7F, 0x7F}, {0xFF, 0x00, 0xFF}, {0xFF, 0xFF, 0x00}, {0x7F, 0x00, 0x00},
|
|
{0x00, 0xFF, 0xFF}, {0x00, 0x00, 0xFF}, {0x00, 0xFF, 0x00}, {0x00, 0x7F, 0x7F}};
|
|
static const uint16_t pattern_8bars_rgb[8][3] = {
|
|
{0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0x00}, {0x00, 0xFF, 0xFF}, {0x00, 0xFF, 0x00},
|
|
{0xFF, 0x00, 0xFF}, {0xFF, 0x00, 0x00}, {0x00, 0x00, 0xFF}, {0x00, 0x00, 0x00}};
|
|
static void emul_imager_fill_framebuffer(const struct device *const dev, struct video_format *fmt)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
uint16_t *fb16 = (uint16_t *)data->framebuffer;
|
|
uint16_t r, g, b, y, uv;
|
|
|
|
/* Fill the first row of the emulated framebuffer */
|
|
switch (fmt->pixelformat) {
|
|
case VIDEO_PIX_FMT_YUYV:
|
|
for (size_t i = 0; i < fmt->width; i++) {
|
|
y = pattern_8bars_yuv[i * 8 / fmt->width][0];
|
|
uv = pattern_8bars_yuv[i * 8 / fmt->width][1 + i % 2];
|
|
fb16[i] = sys_cpu_to_be16(y << 8 | uv << 0);
|
|
}
|
|
break;
|
|
case VIDEO_PIX_FMT_RGB565:
|
|
for (size_t i = 0; i < fmt->width; i++) {
|
|
r = pattern_8bars_rgb[i * 8 / fmt->width][0] >> (8 - 5);
|
|
g = pattern_8bars_rgb[i * 8 / fmt->width][1] >> (8 - 6);
|
|
b = pattern_8bars_rgb[i * 8 / fmt->width][2] >> (8 - 5);
|
|
fb16[i] = sys_cpu_to_le16((r << 11) | (g << 6) | (b << 0));
|
|
}
|
|
break;
|
|
default:
|
|
LOG_WRN("Unsupported pixel format %x, supported: %x, %x", fmt->pixelformat,
|
|
VIDEO_PIX_FMT_YUYV, VIDEO_PIX_FMT_RGB565);
|
|
memset(fb16, 0, fmt->pitch);
|
|
}
|
|
|
|
/* Duplicate the first row over the whole frame */
|
|
for (size_t i = 1; i < fmt->height; i++) {
|
|
memcpy(data->framebuffer + fmt->pitch * i, data->framebuffer, fmt->pitch);
|
|
}
|
|
}
|
|
|
|
static int emul_imager_set_fmt(const struct device *const dev, enum video_endpoint_id ep,
|
|
struct video_format *fmt)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
size_t fmt_id;
|
|
int ret;
|
|
|
|
if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (fmt->pitch * fmt->height > CONFIG_VIDEO_EMUL_IMAGER_FRAMEBUFFER_SIZE) {
|
|
LOG_ERR("%s has %u bytes of memory, unable to support %x %ux%u (%u bytes)",
|
|
dev->name, CONFIG_VIDEO_EMUL_IMAGER_FRAMEBUFFER_SIZE, fmt->pixelformat,
|
|
fmt->width, fmt->height, fmt->pitch * fmt->height);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (memcmp(&data->fmt, fmt, sizeof(data->fmt)) == 0) {
|
|
return 0;
|
|
}
|
|
|
|
ret = video_format_caps_index(fmts, fmt, &fmt_id);
|
|
if (ret < 0) {
|
|
LOG_ERR("Format %x %ux%u not found for %s", fmt->pixelformat, fmt->width,
|
|
fmt->height, dev->name);
|
|
return ret;
|
|
}
|
|
|
|
ret = emul_imager_set_mode(dev, &emul_imager_modes[fmt_id][0]);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* Change the image pattern on the framebuffer */
|
|
emul_imager_fill_framebuffer(dev, fmt);
|
|
|
|
data->fmt_id = fmt_id;
|
|
data->fmt = *fmt;
|
|
return 0;
|
|
}
|
|
|
|
static int emul_imager_get_fmt(const struct device *dev, enum video_endpoint_id ep,
|
|
struct video_format *fmt)
|
|
{
|
|
struct emul_imager_data *data = dev->data;
|
|
|
|
if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
*fmt = data->fmt;
|
|
return 0;
|
|
}
|
|
|
|
static int emul_imager_get_caps(const struct device *dev, enum video_endpoint_id ep,
|
|
struct video_caps *caps)
|
|
{
|
|
if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
caps->format_caps = fmts;
|
|
return 0;
|
|
}
|
|
|
|
static int emul_imager_stream_start(const struct device *dev)
|
|
{
|
|
return emul_imager_write_reg(dev, EMUL_IMAGER_REG_CTRL, 1);
|
|
}
|
|
|
|
static int emul_imager_stream_stop(const struct device *dev)
|
|
{
|
|
return emul_imager_write_reg(dev, EMUL_IMAGER_REG_CTRL, 0);
|
|
}
|
|
|
|
static DEVICE_API(video, emul_imager_driver_api) = {
|
|
.set_ctrl = emul_imager_set_ctrl,
|
|
.get_ctrl = emul_imager_get_ctrl,
|
|
.set_frmival = emul_imager_set_frmival,
|
|
.get_frmival = emul_imager_get_frmival,
|
|
.enum_frmival = emul_imager_enum_frmival,
|
|
.set_format = emul_imager_set_fmt,
|
|
.get_format = emul_imager_get_fmt,
|
|
.get_caps = emul_imager_get_caps,
|
|
.stream_start = emul_imager_stream_start,
|
|
.stream_stop = emul_imager_stream_stop,
|
|
};
|
|
|
|
int emul_imager_init(const struct device *dev)
|
|
{
|
|
struct video_format fmt;
|
|
uint8_t sensor_id;
|
|
int ret;
|
|
|
|
if (/* !i2c_is_ready_dt(&cfg->i2c) */ false) {
|
|
/* LOG_ERR("Bus %s is not ready", cfg->i2c.bus->name); */
|
|
return -ENODEV;
|
|
}
|
|
|
|
ret = emul_imager_read_reg(dev, EMUL_IMAGER_REG_SENSOR_ID, &sensor_id);
|
|
if (ret < 0 || sensor_id != EMUL_IMAGER_SENSOR_ID) {
|
|
LOG_ERR("Failed to get %s correct sensor ID (0x%x", dev->name, sensor_id);
|
|
return ret;
|
|
}
|
|
|
|
ret = emul_imager_write_multi(dev, emul_imager_init_regs);
|
|
if (ret < 0) {
|
|
LOG_ERR("Could not set %s initial registers", dev->name);
|
|
return ret;
|
|
}
|
|
|
|
fmt.pixelformat = fmts[0].pixelformat;
|
|
fmt.width = fmts[0].width_min;
|
|
fmt.height = fmts[0].height_min;
|
|
fmt.pitch = fmt.width * 2;
|
|
|
|
ret = emul_imager_set_fmt(dev, VIDEO_EP_OUT, &fmt);
|
|
if (ret < 0) {
|
|
LOG_ERR("Failed to set %s to default format %x %ux%u", dev->name, fmt.pixelformat,
|
|
fmt.width, fmt.height);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define EMUL_IMAGER_DEFINE(inst) \
|
|
static struct emul_imager_data emul_imager_data_##inst; \
|
|
\
|
|
static const struct emul_imager_config emul_imager_cfg_##inst = { \
|
|
.i2c = /* I2C_DT_SPEC_INST_GET(inst) */ {0}, \
|
|
}; \
|
|
\
|
|
DEVICE_DT_INST_DEFINE(inst, &emul_imager_init, NULL, &emul_imager_data_##inst, \
|
|
&emul_imager_cfg_##inst, POST_KERNEL, CONFIG_VIDEO_INIT_PRIORITY, \
|
|
&emul_imager_driver_api);
|
|
|
|
DT_INST_FOREACH_STATUS_OKAY(EMUL_IMAGER_DEFINE)
|