// SPDX-License-Identifier: GPL-2.0 /* * ARM Mali-C55 ISP Driver - Image signal processor * * Copyright (C) 2025 Ideas on Board Oy */ #include #include #include #include #include #include #include #include #include #include #include #include #include "mali-c55-common.h" #include "mali-c55-registers.h" static const struct mali_c55_isp_format_info mali_c55_isp_fmts[] = { { .code = MEDIA_BUS_FMT_SRGGB20_1X20, .shifted_code = MEDIA_BUS_FMT_SRGGB16_1X16, .order = MALI_C55_BAYER_ORDER_RGGB, .bypass = false, }, { .code = MEDIA_BUS_FMT_SGRBG20_1X20, .shifted_code = MEDIA_BUS_FMT_SGRBG16_1X16, .order = MALI_C55_BAYER_ORDER_GRBG, .bypass = false, }, { .code = MEDIA_BUS_FMT_SGBRG20_1X20, .shifted_code = MEDIA_BUS_FMT_SGBRG16_1X16, .order = MALI_C55_BAYER_ORDER_GBRG, .bypass = false, }, { .code = MEDIA_BUS_FMT_SBGGR20_1X20, .shifted_code = MEDIA_BUS_FMT_SBGGR16_1X16, .order = MALI_C55_BAYER_ORDER_BGGR, .bypass = false, }, { .code = MEDIA_BUS_FMT_RGB202020_1X60, .shifted_code = 0, /* Not relevant for this format */ .order = 0, /* Not relevant for this format */ .bypass = true, } /* * TODO: Support MEDIA_BUS_FMT_YUV20_1X60 here. This is so that we can * also support YUV input from a sensor passed-through to the output. At * present we have no mechanism to test that though so it may have to * wait a while... */ }; const struct mali_c55_isp_format_info * mali_c55_isp_get_mbus_config_by_index(u32 index) { if (index < ARRAY_SIZE(mali_c55_isp_fmts)) return &mali_c55_isp_fmts[index]; return NULL; } const struct mali_c55_isp_format_info * mali_c55_isp_get_mbus_config_by_code(u32 code) { unsigned int i; for (i = 0; i < ARRAY_SIZE(mali_c55_isp_fmts); i++) { if (mali_c55_isp_fmts[i].code == code) return &mali_c55_isp_fmts[i]; } return NULL; } const struct mali_c55_isp_format_info * mali_c55_isp_get_mbus_config_by_shifted_code(u32 code) { unsigned int i; for (i = 0; i < ARRAY_SIZE(mali_c55_isp_fmts); i++) { if (mali_c55_isp_fmts[i].shifted_code == code) return &mali_c55_isp_fmts[i]; } return NULL; } static void mali_c55_isp_stop(struct mali_c55 *mali_c55) { u32 val; mali_c55_write(mali_c55, MALI_C55_REG_INPUT_MODE_REQUEST, MALI_C55_INPUT_SAFE_STOP); readl_poll_timeout(mali_c55->base + MALI_C55_REG_MODE_STATUS, val, !val, 10 * USEC_PER_MSEC, 250 * USEC_PER_MSEC); } static int mali_c55_isp_start(struct mali_c55 *mali_c55, const struct v4l2_subdev_state *state) { struct mali_c55_context *ctx = mali_c55_get_active_context(mali_c55); const struct mali_c55_isp_format_info *cfg; const struct v4l2_mbus_framefmt *format; const struct v4l2_rect *crop; u32 val; int ret; mali_c55_update_bits(mali_c55, MALI_C55_REG_MCU_CONFIG, MALI_C55_REG_MCU_CONFIG_WRITE_MASK, MALI_C55_REG_MCU_CONFIG_WRITE_PING); /* Apply input windowing */ crop = v4l2_subdev_state_get_crop(state, MALI_C55_ISP_PAD_SINK_VIDEO); format = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SINK_VIDEO); cfg = mali_c55_isp_get_mbus_config_by_code(format->code); mali_c55_write(mali_c55, MALI_C55_REG_HC_START, MALI_C55_HC_START(crop->left)); mali_c55_write(mali_c55, MALI_C55_REG_HC_SIZE, MALI_C55_HC_SIZE(crop->width)); mali_c55_write(mali_c55, MALI_C55_REG_VC_START_SIZE, MALI_C55_VC_START(crop->top) | MALI_C55_VC_SIZE(crop->height)); mali_c55_ctx_update_bits(mali_c55, MALI_C55_REG_BASE_ADDR, MALI_C55_REG_ACTIVE_WIDTH_MASK, format->width); mali_c55_ctx_update_bits(mali_c55, MALI_C55_REG_BASE_ADDR, MALI_C55_REG_ACTIVE_HEIGHT_MASK, format->height << 16); mali_c55_ctx_update_bits(mali_c55, MALI_C55_REG_BAYER_ORDER, MALI_C55_BAYER_ORDER_MASK, cfg->order); mali_c55_ctx_update_bits(mali_c55, MALI_C55_REG_INPUT_WIDTH, MALI_C55_INPUT_WIDTH_MASK, MALI_C55_INPUT_WIDTH_20BIT); mali_c55_ctx_update_bits(mali_c55, MALI_C55_REG_ISP_RAW_BYPASS, MALI_C55_ISP_RAW_BYPASS_BYPASS_MASK, cfg->bypass ? MALI_C55_ISP_RAW_BYPASS_BYPASS_MASK : 0x00); mali_c55_params_write_config(mali_c55); ret = mali_c55_config_write(ctx, MALI_C55_CONFIG_PING, true); if (ret) { dev_err(mali_c55->dev, "failed to write ISP config\n"); return ret; } mali_c55_write(mali_c55, MALI_C55_REG_INPUT_MODE_REQUEST, MALI_C55_INPUT_SAFE_START); ret = readl_poll_timeout(mali_c55->base + MALI_C55_REG_MODE_STATUS, val, val, 10 * USEC_PER_MSEC, 250 * USEC_PER_MSEC); if (ret) { mali_c55_isp_stop(mali_c55); dev_err(mali_c55->dev, "timeout starting ISP\n"); return ret; } return 0; } static int mali_c55_isp_enum_mbus_code(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, struct v4l2_subdev_mbus_code_enum *code) { /* * Only the internal RGB processed format is allowed on the regular * processing source pad. */ if (code->pad == MALI_C55_ISP_PAD_SOURCE_VIDEO) { if (code->index) return -EINVAL; code->code = MEDIA_BUS_FMT_RGB121212_1X36; return 0; } /* On the sink and bypass pads all the supported formats are allowed. */ if (code->index >= ARRAY_SIZE(mali_c55_isp_fmts)) return -EINVAL; code->code = mali_c55_isp_fmts[code->index].code; return 0; } static int mali_c55_isp_enum_frame_size(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, struct v4l2_subdev_frame_size_enum *fse) { const struct mali_c55_isp_format_info *cfg; if (fse->index > 0) return -EINVAL; /* * Only the internal RGB processed format is allowed on the regular * processing source pad. * * On the sink and bypass pads all the supported formats are allowed. */ if (fse->pad == MALI_C55_ISP_PAD_SOURCE_VIDEO) { if (fse->code != MEDIA_BUS_FMT_RGB121212_1X36) return -EINVAL; } else { cfg = mali_c55_isp_get_mbus_config_by_code(fse->code); if (!cfg) return -EINVAL; } fse->min_width = MALI_C55_MIN_WIDTH; fse->min_height = MALI_C55_MIN_HEIGHT; fse->max_width = MALI_C55_MAX_WIDTH; fse->max_height = MALI_C55_MAX_HEIGHT; return 0; } static int mali_c55_isp_set_fmt(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, struct v4l2_subdev_format *format) { struct v4l2_mbus_framefmt *fmt = &format->format; struct v4l2_mbus_framefmt *src_fmt, *sink_fmt; const struct mali_c55_isp_format_info *cfg; struct v4l2_rect *crop; /* * Disallow set_fmt on the source pads; format is fixed and the sizes * are the result of applying the sink crop rectangle to the sink * format. */ if (format->pad != MALI_C55_ISP_PAD_SINK_VIDEO) return v4l2_subdev_get_fmt(sd, state, format); sink_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SINK_VIDEO); cfg = mali_c55_isp_get_mbus_config_by_code(fmt->code); sink_fmt->code = cfg ? fmt->code : MEDIA_BUS_FMT_SRGGB20_1X20; /* * Clamp sizes in the accepted limits and clamp the crop rectangle in * the new sizes. */ sink_fmt->width = clamp(fmt->width, MALI_C55_MIN_WIDTH, MALI_C55_MAX_WIDTH); sink_fmt->height = clamp(fmt->height, MALI_C55_MIN_HEIGHT, MALI_C55_MAX_HEIGHT); *fmt = *sink_fmt; crop = v4l2_subdev_state_get_crop(state, MALI_C55_ISP_PAD_SINK_VIDEO); crop->left = 0; crop->top = 0; crop->width = sink_fmt->width; crop->height = sink_fmt->height; /* * Propagate format to source pads. On the 'regular' output pad use * the internal RGB processed format, while on the bypass pad simply * replicate the ISP sink format. The sizes on both pads are the same as * the ISP sink crop rectangle. The "field" and "colorspace" fields are * set in .init_state() and fixed for both source pads, as is the "code" * field for the processed data source pad. */ src_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SOURCE_VIDEO); src_fmt->width = crop->width; src_fmt->height = crop->height; src_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SOURCE_BYPASS); src_fmt->code = sink_fmt->code; src_fmt->width = crop->width; src_fmt->height = crop->height; return 0; } static int mali_c55_isp_get_selection(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, struct v4l2_subdev_selection *sel) { if (sel->pad != MALI_C55_ISP_PAD_SINK_VIDEO || sel->target != V4L2_SEL_TGT_CROP) return -EINVAL; sel->r = *v4l2_subdev_state_get_crop(state, MALI_C55_ISP_PAD_SINK_VIDEO); return 0; } static int mali_c55_isp_set_selection(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, struct v4l2_subdev_selection *sel) { struct v4l2_mbus_framefmt *src_fmt; const struct v4l2_mbus_framefmt *fmt; struct v4l2_rect *crop; if (sel->pad != MALI_C55_ISP_PAD_SINK_VIDEO || sel->target != V4L2_SEL_TGT_CROP) return -EINVAL; fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SINK_VIDEO); sel->r.left = clamp_t(unsigned int, sel->r.left, 0, fmt->width); sel->r.top = clamp_t(unsigned int, sel->r.top, 0, fmt->height); sel->r.width = clamp_t(unsigned int, sel->r.width, MALI_C55_MIN_WIDTH, fmt->width - sel->r.left); sel->r.height = clamp_t(unsigned int, sel->r.height, MALI_C55_MIN_HEIGHT, fmt->height - sel->r.top); crop = v4l2_subdev_state_get_crop(state, MALI_C55_ISP_PAD_SINK_VIDEO); *crop = sel->r; /* * Propagate the crop rectangle sizes to the source pad format. The crop * isn't propagated to the bypass source pad, because the bypassed data * cannot be cropped. */ src_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SOURCE_VIDEO); src_fmt->width = crop->width; src_fmt->height = crop->height; return 0; } static int mali_c55_isp_enable_streams(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, u32 pad, u64 streams_mask) { struct mali_c55_isp *isp = container_of(sd, struct mali_c55_isp, sd); struct mali_c55 *mali_c55 = isp->mali_c55; struct v4l2_subdev *src_sd; struct media_pad *sink_pad; int ret; /* * We have two source pads, both of which have only a single stream. The * core v4l2 code already validated those parameters so we can just get * on with starting the ISP. */ sink_pad = &isp->pads[MALI_C55_ISP_PAD_SINK_VIDEO]; isp->remote_src = media_pad_remote_pad_unique(sink_pad); src_sd = media_entity_to_v4l2_subdev(isp->remote_src->entity); isp->frame_sequence = 0; ret = mali_c55_isp_start(mali_c55, state); if (ret) { dev_err(mali_c55->dev, "Failed to start ISP\n"); isp->remote_src = NULL; return ret; } /* * We only support a single input stream, so we can just enable the 1st * entry in the streams mask. */ ret = v4l2_subdev_enable_streams(src_sd, isp->remote_src->index, BIT(0)); if (ret) { dev_err(mali_c55->dev, "Failed to start ISP source\n"); mali_c55_isp_stop(mali_c55); return ret; } return 0; } static int mali_c55_isp_disable_streams(struct v4l2_subdev *sd, struct v4l2_subdev_state *state, u32 pad, u64 streams_mask) { struct mali_c55_isp *isp = container_of(sd, struct mali_c55_isp, sd); struct mali_c55 *mali_c55 = isp->mali_c55; struct v4l2_subdev *src_sd; if (isp->remote_src) { src_sd = media_entity_to_v4l2_subdev(isp->remote_src->entity); v4l2_subdev_disable_streams(src_sd, isp->remote_src->index, BIT(0)); } isp->remote_src = NULL; mali_c55_isp_stop(mali_c55); return 0; } static const struct v4l2_subdev_pad_ops mali_c55_isp_pad_ops = { .enum_mbus_code = mali_c55_isp_enum_mbus_code, .enum_frame_size = mali_c55_isp_enum_frame_size, .get_fmt = v4l2_subdev_get_fmt, .set_fmt = mali_c55_isp_set_fmt, .get_selection = mali_c55_isp_get_selection, .set_selection = mali_c55_isp_set_selection, .link_validate = v4l2_subdev_link_validate_default, .enable_streams = mali_c55_isp_enable_streams, .disable_streams = mali_c55_isp_disable_streams, }; void mali_c55_isp_queue_event_sof(struct mali_c55 *mali_c55) { struct v4l2_event event = { .type = V4L2_EVENT_FRAME_SYNC, }; event.u.frame_sync.frame_sequence = mali_c55->isp.frame_sequence; v4l2_event_queue(mali_c55->isp.sd.devnode, &event); } static int mali_c55_isp_subscribe_event(struct v4l2_subdev *sd, struct v4l2_fh *fh, struct v4l2_event_subscription *sub) { switch (sub->type) { case V4L2_EVENT_FRAME_SYNC: return v4l2_event_subscribe(fh, sub, 0, NULL); case V4L2_EVENT_CTRL: return v4l2_ctrl_subscribe_event(fh, sub); default: return -EINVAL; } } static const struct v4l2_subdev_core_ops mali_c55_isp_core_ops = { .subscribe_event = mali_c55_isp_subscribe_event, .unsubscribe_event = v4l2_event_subdev_unsubscribe, }; static const struct v4l2_subdev_ops mali_c55_isp_ops = { .pad = &mali_c55_isp_pad_ops, .core = &mali_c55_isp_core_ops, }; static int mali_c55_isp_init_state(struct v4l2_subdev *sd, struct v4l2_subdev_state *state) { struct v4l2_mbus_framefmt *sink_fmt, *src_fmt; struct v4l2_rect *in_crop; sink_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SINK_VIDEO); src_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SOURCE_VIDEO); in_crop = v4l2_subdev_state_get_crop(state, MALI_C55_ISP_PAD_SINK_VIDEO); sink_fmt->width = MALI_C55_DEFAULT_WIDTH; sink_fmt->height = MALI_C55_DEFAULT_HEIGHT; sink_fmt->field = V4L2_FIELD_NONE; sink_fmt->code = MEDIA_BUS_FMT_SRGGB20_1X20; sink_fmt->colorspace = V4L2_COLORSPACE_RAW; sink_fmt->xfer_func = V4L2_MAP_XFER_FUNC_DEFAULT(sink_fmt->colorspace); sink_fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(sink_fmt->colorspace); sink_fmt->quantization = V4L2_MAP_QUANTIZATION_DEFAULT(false, sink_fmt->colorspace, sink_fmt->ycbcr_enc); *v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SOURCE_BYPASS) = *sink_fmt; src_fmt->width = MALI_C55_DEFAULT_WIDTH; src_fmt->height = MALI_C55_DEFAULT_HEIGHT; src_fmt->field = V4L2_FIELD_NONE; src_fmt->code = MEDIA_BUS_FMT_RGB121212_1X36; src_fmt->colorspace = V4L2_COLORSPACE_SRGB; src_fmt->xfer_func = V4L2_MAP_XFER_FUNC_DEFAULT(sink_fmt->colorspace); src_fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(sink_fmt->colorspace); src_fmt->quantization = V4L2_MAP_QUANTIZATION_DEFAULT(false, sink_fmt->colorspace, sink_fmt->ycbcr_enc); in_crop->top = 0; in_crop->left = 0; in_crop->width = MALI_C55_DEFAULT_WIDTH; in_crop->height = MALI_C55_DEFAULT_HEIGHT; src_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SOURCE_STATS); sink_fmt = v4l2_subdev_state_get_format(state, MALI_C55_ISP_PAD_SINK_PARAMS); src_fmt->width = 0; src_fmt->height = 0; src_fmt->field = V4L2_FIELD_NONE; src_fmt->code = MEDIA_BUS_FMT_METADATA_FIXED; sink_fmt->width = 0; sink_fmt->height = 0; sink_fmt->field = V4L2_FIELD_NONE; sink_fmt->code = MEDIA_BUS_FMT_METADATA_FIXED; return 0; } static const struct v4l2_subdev_internal_ops mali_c55_isp_internal_ops = { .init_state = mali_c55_isp_init_state, }; static int mali_c55_subdev_link_validate(struct media_link *link) { /* * Skip validation for the parameters sink pad, as the source is not * a subdevice. */ if (link->sink->index == MALI_C55_ISP_PAD_SINK_PARAMS) return 0; return v4l2_subdev_link_validate(link); } static const struct media_entity_operations mali_c55_isp_media_ops = { .link_validate = mali_c55_subdev_link_validate, }; static int mali_c55_isp_s_ctrl(struct v4l2_ctrl *ctrl) { /* * .s_ctrl() is a mandatory operation, but the driver has only a single * read only control. If we got here, something went badly wrong. */ return -EINVAL; } static const struct v4l2_ctrl_ops mali_c55_isp_ctrl_ops = { .s_ctrl = mali_c55_isp_s_ctrl, }; /* NOT const because the default needs to be filled in at runtime */ static struct v4l2_ctrl_config mali_c55_isp_v4l2_custom_ctrls[] = { { .ops = &mali_c55_isp_ctrl_ops, .id = V4L2_CID_MALI_C55_CAPABILITIES, .name = "Mali-C55 ISP Capabilities", .type = V4L2_CTRL_TYPE_BITMASK, .min = 0, .max = MALI_C55_GPS_PONG_FITTED | MALI_C55_GPS_WDR_FITTED | MALI_C55_GPS_COMPRESSION_FITTED | MALI_C55_GPS_TEMPER_FITTED | MALI_C55_GPS_SINTER_LITE_FITTED | MALI_C55_GPS_SINTER_FITTED | MALI_C55_GPS_IRIDIX_LTM_FITTED | MALI_C55_GPS_IRIDIX_GTM_FITTED | MALI_C55_GPS_CNR_FITTED | MALI_C55_GPS_FRSCALER_FITTED | MALI_C55_GPS_DS_PIPE_FITTED, .def = 0, }, }; static int mali_c55_isp_init_controls(struct mali_c55 *mali_c55) { struct v4l2_ctrl_handler *handler = &mali_c55->isp.handler; struct v4l2_ctrl *capabilities; int ret; ret = v4l2_ctrl_handler_init(handler, 1); if (ret) return ret; mali_c55_isp_v4l2_custom_ctrls[0].def = mali_c55->capabilities; capabilities = v4l2_ctrl_new_custom(handler, &mali_c55_isp_v4l2_custom_ctrls[0], NULL); if (capabilities) capabilities->flags |= V4L2_CTRL_FLAG_READ_ONLY; if (handler->error) { dev_err(mali_c55->dev, "failed to register capabilities control\n"); ret = handler->error; v4l2_ctrl_handler_free(handler); return ret; } mali_c55->isp.sd.ctrl_handler = handler; return 0; } int mali_c55_register_isp(struct mali_c55 *mali_c55) { struct mali_c55_isp *isp = &mali_c55->isp; struct v4l2_subdev *sd = &isp->sd; int ret; isp->mali_c55 = mali_c55; v4l2_subdev_init(sd, &mali_c55_isp_ops); sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS; sd->entity.ops = &mali_c55_isp_media_ops; sd->entity.function = MEDIA_ENT_F_PROC_VIDEO_ISP; sd->internal_ops = &mali_c55_isp_internal_ops; strscpy(sd->name, MALI_C55_DRIVER_NAME " isp", sizeof(sd->name)); isp->pads[MALI_C55_ISP_PAD_SINK_VIDEO].flags = MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_MUST_CONNECT; isp->pads[MALI_C55_ISP_PAD_SOURCE_VIDEO].flags = MEDIA_PAD_FL_SOURCE; isp->pads[MALI_C55_ISP_PAD_SOURCE_BYPASS].flags = MEDIA_PAD_FL_SOURCE; isp->pads[MALI_C55_ISP_PAD_SOURCE_STATS].flags = MEDIA_PAD_FL_SOURCE; isp->pads[MALI_C55_ISP_PAD_SINK_PARAMS].flags = MEDIA_PAD_FL_SINK; ret = media_entity_pads_init(&sd->entity, MALI_C55_ISP_NUM_PADS, isp->pads); if (ret) return ret; ret = mali_c55_isp_init_controls(mali_c55); if (ret) goto err_cleanup_media_entity; ret = v4l2_subdev_init_finalize(sd); if (ret) goto err_free_ctrl_handler; ret = v4l2_device_register_subdev(&mali_c55->v4l2_dev, sd); if (ret) goto err_cleanup_subdev; mutex_init(&isp->capture_lock); return 0; err_cleanup_subdev: v4l2_subdev_cleanup(sd); err_free_ctrl_handler: v4l2_ctrl_handler_free(&isp->handler); err_cleanup_media_entity: media_entity_cleanup(&sd->entity); isp->mali_c55 = NULL; return ret; } void mali_c55_unregister_isp(struct mali_c55 *mali_c55) { struct mali_c55_isp *isp = &mali_c55->isp; if (!isp->mali_c55) return; mutex_destroy(&isp->capture_lock); v4l2_device_unregister_subdev(&isp->sd); v4l2_subdev_cleanup(&isp->sd); media_entity_cleanup(&isp->sd.entity); }