[PATCH v14 13/22] media: i2c: add Maxim GMSL2/3 serializer and deserializer framework
Dumitru Ceclan via B4 Relay <[email protected]> Tue, 04 Aug 2026 11:49:56 +0300
| Newsgroups | org.kernel.vger.linux-gpio,dev.linux.lists.linux-staging,org.kernel.feeds.b4-sent,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel,org.kernel.vger.linux-media |
|---|---|
| Message-ID | <[email protected]> |
From: Cosmin Tanislav <[email protected]> These drivers are meant to be used as a common framework for Maxim GMSL2/3 serializers and deserializers. This framework enables support for the following new features across all the chips: * Full Streams API support * .get_frame_desc() * .get_mbus_config() * I2C ATR * automatic GMSL link version negotiation * automatic stream id selection * automatic VC remapping * automatic pixel mode / tunnel mode selection * automatic double mode selection / data padding * logging of internal state and chip status registers via .log_status() * PHY modes * serializer pinctrl * TPG Signed-off-by: Cosmin Tanislav <[email protected]> --- MAINTAINERS | 1 + drivers/media/i2c/Kconfig | 2 + drivers/media/i2c/Makefile | 1 + drivers/media/i2c/maxim-serdes/Kconfig | 17 ++ drivers/media/i2c/maxim-serdes/Makefile | 3 + drivers/media/i2c/maxim-serdes/max_serdes.c | 413 ++++++++++++++++++++++++++++ drivers/media/i2c/maxim-serdes/max_serdes.h | 181 ++++++++++++ 7 files changed, 618 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index f9bdc133b52d..75ee49bc612b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15768,6 +15768,7 @@ M: Cosmin Tanislav <[email protected]> L: [email protected] S: Maintained F: Documentation/devicetree/bindings/media/i2c/maxim,max9296a.yaml +F: drivers/media/i2c/maxim-serdes/ MAXIM MAX11205 DRIVER M: Ramona Bolboaca <[email protected]> diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig index a3ab48607dcf..42cb53c5395d 100644 --- a/drivers/media/i2c/Kconfig +++ b/drivers/media/i2c/Kconfig @@ -1786,6 +1786,8 @@ config VIDEO_MAX96717 To compile this driver as a module, choose M here: the module will be called max96717. +source "drivers/media/i2c/maxim-serdes/Kconfig" + endmenu endif # VIDEO_DEV diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile index 90b276a7417a..9af890f8c298 100644 --- a/drivers/media/i2c/Makefile +++ b/drivers/media/i2c/Makefile @@ -72,6 +72,7 @@ obj-$(CONFIG_VIDEO_MAX9271_LIB) += max9271.o obj-$(CONFIG_VIDEO_MAX9286) += max9286.o obj-$(CONFIG_VIDEO_MAX96714) += max96714.o obj-$(CONFIG_VIDEO_MAX96717) += max96717.o +obj-$(CONFIG_VIDEO_MAXIM_SERDES) += maxim-serdes/ obj-$(CONFIG_VIDEO_ML86V7667) += ml86v7667.o obj-$(CONFIG_VIDEO_MSP3400) += msp3400.o obj-$(CONFIG_VIDEO_MT9M001) += mt9m001.o diff --git a/drivers/media/i2c/maxim-serdes/Kconfig b/drivers/media/i2c/maxim-serdes/Kconfig new file mode 100644 index 000000000000..f5a4ca80a263 --- /dev/null +++ b/drivers/media/i2c/maxim-serdes/Kconfig @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0 + +config VIDEO_MAXIM_SERDES + tristate "Maxim GMSL2/3 Serializer and Deserializer support" + depends on VIDEO_DEV + depends on I2C + select I2C_ATR + select I2C_MUX + select MEDIA_CONTROLLER + select V4L2_FWNODE + select VIDEO_V4L2_SUBDEV_API + help + This driver supports the Maxim GMSL2/3 common Serializer and + Deserializer framework. + + To compile this driver as a module, choose M here: the module + will be called max_serdes. diff --git a/drivers/media/i2c/maxim-serdes/Makefile b/drivers/media/i2c/maxim-serdes/Makefile new file mode 100644 index 000000000000..630fbb486bab --- /dev/null +++ b/drivers/media/i2c/maxim-serdes/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0 +max-serdes-objs := max_serdes.o +obj-$(CONFIG_VIDEO_MAXIM_SERDES) += max-serdes.o diff --git a/drivers/media/i2c/maxim-serdes/max_serdes.c b/drivers/media/i2c/maxim-serdes/max_serdes.c new file mode 100644 index 000000000000..7d013c4739d1 --- /dev/null +++ b/drivers/media/i2c/maxim-serdes/max_serdes.c @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2025 Analog Devices Inc. + */ + +#include <linux/export.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/stringify.h> + +#include <media/mipi-csi2.h> + +#include <video/videomode.h> + +#include <uapi/linux/media-bus-format.h> + +#include "max_serdes.h" + +const char * const max_serdes_tpg_patterns[] = { + [MAX_SERDES_TPG_PATTERN_GRADIENT] = "Gradient", + [MAX_SERDES_TPG_PATTERN_CHECKERBOARD] = "Checkerboard", +}; + +static const char * const max_gmsl_versions[] = { + [MAX_SERDES_GMSL_2_3GBPS] = "GMSL2 3Gbps", + [MAX_SERDES_GMSL_2_6GBPS] = "GMSL2 6Gbps", + [MAX_SERDES_GMSL_3_12GBPS] = "GMSL3 12Gbps", +}; + +const char *max_serdes_gmsl_version_str(enum max_serdes_gmsl_version version) +{ + if (version > MAX_SERDES_GMSL_3_12GBPS) + return NULL; + + return max_gmsl_versions[version]; +} + +static const char * const max_gmsl_mode[] = { + [MAX_SERDES_GMSL_PIXEL_MODE] = "pixel", + [MAX_SERDES_GMSL_TUNNEL_MODE] = "tunnel", +}; + +const char *max_serdes_gmsl_mode_str(enum max_serdes_gmsl_mode mode) +{ + if (mode > MAX_SERDES_GMSL_TUNNEL_MODE) + return NULL; + + return max_gmsl_mode[mode]; +} + +static const struct max_serdes_mipi_format max_serdes_mipi_formats[] = { + { MIPI_CSI2_DT_EMBEDDED_8B, 8 }, + { MIPI_CSI2_DT_YUV422_8B, 16 }, + { MIPI_CSI2_DT_YUV422_10B, 20 }, + { MIPI_CSI2_DT_RGB565, 16 }, + { MIPI_CSI2_DT_RGB666, 18 }, + { MIPI_CSI2_DT_RGB888, 24 }, + { MIPI_CSI2_DT_RAW8, 8 }, + { MIPI_CSI2_DT_RAW10, 10 }, + { MIPI_CSI2_DT_RAW12, 12 }, + { MIPI_CSI2_DT_RAW14, 14 }, + { MIPI_CSI2_DT_RAW16, 16 }, +}; + +const struct max_serdes_mipi_format *max_serdes_mipi_format_by_dt(u8 dt) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(max_serdes_mipi_formats); i++) + if (max_serdes_mipi_formats[i].dt == dt) + return &max_serdes_mipi_formats[i]; + + return NULL; +} + +int max_serdes_get_fd_stream_entry(struct v4l2_subdev *sd, u32 pad, u32 stream, + struct v4l2_mbus_frame_desc_entry *entry) +{ + struct v4l2_mbus_frame_desc fd; + unsigned int i; + int ret; + + ret = v4l2_subdev_call(sd, pad, get_frame_desc, pad, &fd); + if (ret) + return ret; + + if (fd.type != V4L2_MBUS_FRAME_DESC_TYPE_CSI2) + return -EOPNOTSUPP; + + for (i = 0; i < fd.num_entries; i++) { + if (fd.entry[i].stream == stream) { + *entry = fd.entry[i]; + return 0; + } + } + + return -ENOENT; +} + +int max_serdes_get_fd_bpp(struct v4l2_mbus_frame_desc_entry *entry, + unsigned int *bpp) +{ + const struct max_serdes_mipi_format *format; + + format = max_serdes_mipi_format_by_dt(entry->bus.csi2.dt); + if (!format) + return -ENOENT; + + *bpp = format->bpp; + + return 0; +} + +int max_serdes_process_bpps(struct device *dev, u32 bpps, + u32 allowed_double_bpps, unsigned int *doubled_bpp) +{ + unsigned int min_bpp; + unsigned int max_bpp; + bool doubled = false; + + if (!bpps) + return 0; + + *doubled_bpp = 0; + + /* + * Hardware can double bpps 8, 10, 12, and it can pad bpps < 16 + * to another bpp <= 16: + * Hardware can only stream a single constant bpp up to 24. + * + * From these features and limitations, the following rules + * can be deduced: + * + * A bpp of 8 can always be doubled if present. + * A bpp of 10 can be doubled only if there are no other bpps or the + * only other bpp is 20. + * A bpp of 12 can be doubled only if there are no other bpps or the + * only other bpp is 24. + * Bpps <= 16 cannot coexist with bpps > 16. + * Bpps <= 16 need to be padded to the biggest bpp. + */ + + min_bpp = __ffs(bpps); + max_bpp = __fls(bpps); + + if (min_bpp == 8) { + doubled = true; + } else if (min_bpp == 10 || min_bpp == 12) { + u32 bpp_or_double = BIT(min_bpp) | BIT(min_bpp * 2); + u32 other_bpps = bpps & ~bpp_or_double; + + if (!other_bpps) + doubled = true; + } + + if (doubled && (allowed_double_bpps & BIT(min_bpp))) { + *doubled_bpp = min_bpp; + bpps &= ~BIT(min_bpp); + bpps |= BIT(min_bpp * 2); + } + + min_bpp = __ffs(bpps); + max_bpp = __fls(bpps); + + if (max_bpp > 24) { + dev_err(dev, "Cannot stream bpps > 24\n"); + return -EINVAL; + } + + if (min_bpp <= 16 && max_bpp > 16) { + dev_err(dev, "Cannot stream bpps <= 16 with bpps > 16\n"); + return -EINVAL; + } + + if (max_bpp > 16 && min_bpp != max_bpp) { + dev_err(dev, "Cannot stream multiple bpps when one is > 16\n"); + return -EINVAL; + } + + return 0; +} + +int max_serdes_xlate_enable_disable_streams(struct max_serdes_source *sources, + u32 source_sink_pad_offset, + const struct v4l2_subdev_state *state, + u32 pad, u64 updated_streams_mask, + u32 sink_pad_start, u32 num_sink_pads, + bool enable) +{ + u32 failed_sink_pad; + int ret; + u32 i; + + for (i = sink_pad_start; i < sink_pad_start + num_sink_pads; i++) { + u64 matched_streams_mask = updated_streams_mask; + u64 updated_sink_streams_mask; + struct max_serdes_source *source; + + updated_sink_streams_mask = + v4l2_subdev_state_xlate_streams(state, pad, i, + &matched_streams_mask); + if (!updated_sink_streams_mask) + continue; + + source = &sources[i + source_sink_pad_offset]; + if (!source->sd) + continue; + + if (enable) + ret = v4l2_subdev_enable_streams(source->sd, source->pad, + updated_sink_streams_mask); + else + ret = v4l2_subdev_disable_streams(source->sd, source->pad, + updated_sink_streams_mask); + if (ret) { + failed_sink_pad = i; + goto err; + } + } + + return 0; + +err: + for (i = sink_pad_start; i < failed_sink_pad; i++) { + u64 matched_streams_mask = updated_streams_mask; + u64 updated_sink_streams_mask; + struct max_serdes_source *source; + + updated_sink_streams_mask = + v4l2_subdev_state_xlate_streams(state, pad, i, + &matched_streams_mask); + if (!updated_sink_streams_mask) + continue; + + source = &sources[i + source_sink_pad_offset]; + if (!source->sd) + continue; + + if (!enable) + v4l2_subdev_enable_streams(source->sd, source->pad, + updated_sink_streams_mask); + else + v4l2_subdev_disable_streams(source->sd, source->pad, + updated_sink_streams_mask); + } + + return ret; +} + +int max_serdes_get_streams_masks(struct device *dev, + const struct v4l2_subdev_state *state, + u32 pad, u64 updated_streams_mask, + u32 num_pads, u64 *old_streams_masks, + u64 **new_streams_masks, bool enable) +{ + u64 *streams_masks; + unsigned int i; + + streams_masks = devm_kcalloc(dev, num_pads, sizeof(*streams_masks), GFP_KERNEL); + if (!streams_masks) + return -ENOMEM; + + for (i = 0; i < num_pads; i++) { + u64 matched_streams_mask = updated_streams_mask; + u64 updated_sink_streams_mask; + + streams_masks[i] = old_streams_masks[i]; + updated_sink_streams_mask = + v4l2_subdev_state_xlate_streams(state, pad, i, + &matched_streams_mask); + if (!updated_sink_streams_mask) + continue; + + if (enable) + streams_masks[i] |= updated_sink_streams_mask; + else + streams_masks[i] &= ~updated_sink_streams_mask; + } + + if (enable) + streams_masks[pad] |= updated_streams_mask; + else + streams_masks[pad] &= ~updated_streams_mask; + + *new_streams_masks = streams_masks; + + return 0; +} + +static const struct videomode max_serdes_tpg_pixel_videomodes[] = { + { + .pixelclock = 25000000, + .hactive = 640, + .hfront_porch = 10, + .hsync_len = 96, + .hback_porch = 40, + .vactive = 480, + .vfront_porch = 2, + .vsync_len = 24, + .vback_porch = 24, + }, + { + .pixelclock = 75000000, + .hactive = 1920, + .hfront_porch = 88, + .hsync_len = 44, + .hback_porch = 148, + .vactive = 1080, + .vfront_porch = 4, + .vsync_len = 16, + .vback_porch = 36, + }, + { + .pixelclock = 150000000, + .hactive = 1920, + .hfront_porch = 88, + .hsync_len = 44, + .hback_porch = 148, + .vactive = 1080, + .vfront_porch = 4, + .vsync_len = 16, + .vback_porch = 36, + }, +}; + +static void max_serdes_get_vm_timings(const struct videomode *vm, + struct max_serdes_tpg_timings *timings) +{ + u32 hact = vm->hactive; + u32 hfp = vm->hfront_porch; + u32 hsync = vm->hsync_len; + u32 hbp = vm->hback_porch; + u32 htot = hact + hfp + hbp + hsync; + + u32 vact = vm->vactive; + u32 vfp = vm->vfront_porch; + u32 vsync = vm->vsync_len; + u32 vbp = vm->vback_porch; + u32 vtot = vact + vfp + vbp + vsync; + + *timings = (struct max_serdes_tpg_timings) { + .gen_vs = true, + .gen_hs = true, + .gen_de = true, + .vs_inv = true, + .vs_dly = 0, + .vs_high = vsync * htot, + .vs_low = (vact + vfp + vbp) * htot, + .v2h = 0, + .hs_high = hsync, + .hs_low = hact + hfp + hbp, + .hs_cnt = vact + vfp + vbp + vsync, + .v2d = htot * (vsync + vbp) + (hsync + hbp), + .de_high = hact, + .de_low = hfp + hsync + hbp, + .de_cnt = vact, + .clock = vm->pixelclock, + .fps = DIV_ROUND_CLOSEST(vm->pixelclock, vtot * htot), + }; +} + +int max_serdes_get_tpg_timings(const struct max_serdes_tpg_entry *entry, + struct max_serdes_tpg_timings *timings) +{ + u32 fps; + + if (!entry) + return 0; + + fps = DIV_ROUND_CLOSEST(1 * entry->interval.denominator, + entry->interval.numerator); + + for (unsigned int i = 0; i < ARRAY_SIZE(max_serdes_tpg_pixel_videomodes); i++) { + struct max_serdes_tpg_timings vm_timings; + const struct videomode *vm; + + vm = &max_serdes_tpg_pixel_videomodes[i]; + + max_serdes_get_vm_timings(vm, &vm_timings); + + if (vm->hactive == entry->width && + vm->vactive == entry->height && + vm_timings.fps == fps) { + *timings = vm_timings; + return 0; + } + } + + return -EINVAL; +} +EXPORT_SYMBOL_NS_GPL(max_serdes_get_tpg_timings, "MAX_SERDES"); + +int max_serdes_validate_tpg_routing(struct v4l2_subdev_krouting *routing) +{ + const struct v4l2_subdev_route *route; + + if (routing->num_routes != 1) + return -EINVAL; + + route = &routing->routes[0]; + + if (!(route->flags & V4L2_SUBDEV_ROUTE_FL_ACTIVE)) + return -EINVAL; + + if (route->sink_stream != MAX_SERDES_TPG_STREAM) + return -EINVAL; + + return 0; +} + +MODULE_DESCRIPTION("Maxim GMSL2 Serializer/Deserializer Driver"); +MODULE_AUTHOR("Cosmin Tanislav <[email protected]>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/media/i2c/maxim-serdes/max_serdes.h b/drivers/media/i2c/maxim-serdes/max_serdes.h new file mode 100644 index 000000000000..1679b38fc8ae --- /dev/null +++ b/drivers/media/i2c/maxim-serdes/max_serdes.h @@ -0,0 +1,181 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2025 Analog Devices Inc. + */ + +#ifndef MAX_SERDES_H +#define MAX_SERDES_H + +#include <linux/bitfield.h> +#include <linux/types.h> + +#include <media/mipi-csi2.h> +#include <media/v4l2-subdev.h> + +#define REG_SEQUENCE_2(reg, val) \ + { (reg), ((val) >> 8) & 0xff }, \ + { (reg) + 1, ((val) >> 0) & 0xff } + +#define REG_SEQUENCE_3(reg, val) \ + { (reg), ((val) >> 16) & 0xff }, \ + { (reg) + 1, ((val) >> 8) & 0xff }, \ + { (reg) + 2, ((val) >> 0) & 0xff } + +#define REG_SEQUENCE_3_LE(reg, val) \ + { (reg), ((val) >> 0) & 0xff }, \ + { (reg) + 1, ((val) >> 8) & 0xff }, \ + { (reg) + 2, ((val) >> 16) & 0xff } + +#define MAX_SERDES_PHYS_MAX 4 +#define MAX_SERDES_STREAMS_NUM 4 +#define MAX_SERDES_VC_ID_NUM 4 +#define MAX_SERDES_TPG_STREAM 0 + +#define MAX_SERDES_GRAD_INCR 4 +#define MAX_SERDES_CHECKER_COLOR_A 0x00ccfe +#define MAX_SERDES_CHECKER_COLOR_B 0xa76a00 +#define MAX_SERDES_CHECKER_SIZE 60 + +extern const char * const max_serdes_tpg_patterns[]; + +enum max_serdes_gmsl_version { + MAX_SERDES_GMSL_MIN, + MAX_SERDES_GMSL_2_3GBPS = MAX_SERDES_GMSL_MIN, + MAX_SERDES_GMSL_2_6GBPS, + MAX_SERDES_GMSL_3_12GBPS, + MAX_SERDES_GMSL_MAX = MAX_SERDES_GMSL_3_12GBPS, +}; + +enum max_serdes_gmsl_mode { + MAX_SERDES_GMSL_PIXEL_MODE, + MAX_SERDES_GMSL_TUNNEL_MODE, +}; + +enum max_serdes_tpg_pattern { + MAX_SERDES_TPG_PATTERN_MIN, + MAX_SERDES_TPG_PATTERN_CHECKERBOARD = MAX_SERDES_TPG_PATTERN_MIN, + MAX_SERDES_TPG_PATTERN_GRADIENT, + MAX_SERDES_TPG_PATTERN_MAX = MAX_SERDES_TPG_PATTERN_GRADIENT, +}; + +struct max_serdes_phys_config { + unsigned int lanes[MAX_SERDES_PHYS_MAX]; + unsigned int clock_lane[MAX_SERDES_PHYS_MAX]; +}; + +struct max_serdes_phys_configs { + const struct max_serdes_phys_config *configs; + unsigned int num_configs; +}; + +struct max_serdes_i2c_xlate { + u8 src; + u8 dst; + bool en; +}; + +struct max_serdes_mipi_format { + u8 dt; + u8 bpp; +}; + +struct max_serdes_vc_remap { + u8 src; + u8 dst; +}; + +struct max_serdes_source { + struct v4l2_subdev *sd; + u16 pad; + struct fwnode_handle *ep_fwnode; + + unsigned int index; +}; + +struct max_serdes_asc { + struct v4l2_async_connection base; + struct max_serdes_source *source; +}; + +struct max_serdes_tpg_entry { + u32 width; + u32 height; + struct v4l2_fract interval; + u32 code; + u8 dt; + u8 bpp; +}; + +#define MAX_TPG_ENTRY_640X480P60_RGB888 \ + { 640, 480, { 1, 60 }, MEDIA_BUS_FMT_RGB888_1X24, MIPI_CSI2_DT_RGB888, 24 } + +#define MAX_TPG_ENTRY_1920X1080P30_RGB888 \ + { 1920, 1080, { 1, 30 }, MEDIA_BUS_FMT_RGB888_1X24, MIPI_CSI2_DT_RGB888, 24 } + +#define MAX_TPG_ENTRY_1920X1080P60_RGB888 \ + { 1920, 1080, { 1, 60 }, MEDIA_BUS_FMT_RGB888_1X24, MIPI_CSI2_DT_RGB888, 24 } + +struct max_serdes_tpg_entries { + const struct max_serdes_tpg_entry *entries; + unsigned int num_entries; +}; + +struct max_serdes_tpg_timings { + bool gen_vs; + bool gen_hs; + bool gen_de; + bool vs_inv; + bool hs_inv; + bool de_inv; + u32 vs_dly; + u32 vs_high; + u32 vs_low; + u32 v2h; + u32 hs_high; + u32 hs_low; + u32 hs_cnt; + u32 v2d; + u32 de_high; + u32 de_low; + u32 de_cnt; + u32 clock; + u32 fps; +}; + +static inline struct max_serdes_asc *asc_to_max(struct v4l2_async_connection *asc) +{ + return container_of(asc, struct max_serdes_asc, base); +} + +const char *max_serdes_gmsl_version_str(enum max_serdes_gmsl_version version); +const char *max_serdes_gmsl_mode_str(enum max_serdes_gmsl_mode mode); + +const struct max_serdes_mipi_format *max_serdes_mipi_format_by_dt(u8 dt); + +int max_serdes_get_fd_stream_entry(struct v4l2_subdev *sd, u32 pad, u32 stream, + struct v4l2_mbus_frame_desc_entry *entry); + +int max_serdes_get_fd_bpp(struct v4l2_mbus_frame_desc_entry *entry, + unsigned int *bpp); +int max_serdes_process_bpps(struct device *dev, u32 bpps, + u32 allowed_double_bpps, unsigned int *doubled_bpp); + +int max_serdes_xlate_enable_disable_streams(struct max_serdes_source *sources, + u32 source_sink_pad_offset, + const struct v4l2_subdev_state *state, + u32 pad, u64 updated_streams_mask, + u32 sink_pad_start, u32 num_sink_pads, + bool enable); + +int max_serdes_get_streams_masks(struct device *dev, + const struct v4l2_subdev_state *state, + u32 pad, u64 updated_streams_mask, + u32 num_pads, u64 *old_streams_masks, + u64 **new_streams_masks, bool enable); + +int max_serdes_get_tpg_timings(const struct max_serdes_tpg_entry *entry, + struct max_serdes_tpg_timings *timings); + +int max_serdes_validate_tpg_routing(struct v4l2_subdev_krouting *routing); + +#endif // MAX_SERDES_H -- 2.53.0