| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * NXP i.MX8MQ SoC series MIPI-CSI2 receiver driver |
| * |
| * Copyright (C) 2021 Purism SPC |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/cleanup.h> |
| #include <linux/clk.h> |
| #include <linux/clk-provider.h> |
| #include <linux/delay.h> |
| #include <linux/errno.h> |
| #include <linux/interconnect.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/kernel.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/regmap.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/reset.h> |
| #include <linux/spinlock.h> |
| |
| #include <media/v4l2-common.h> |
| #include <media/v4l2-device.h> |
| #include <media/v4l2-fwnode.h> |
| #include <media/v4l2-mc.h> |
| #include <media/v4l2-subdev.h> |
| |
| #define MIPI_CSI2_DRIVER_NAME "imx8mq-mipi-csi2" |
| #define MIPI_CSI2_SUBDEV_NAME MIPI_CSI2_DRIVER_NAME |
| |
| #define MIPI_CSI2_PAD_SINK 0 |
| #define MIPI_CSI2_PAD_SOURCE 1 |
| #define MIPI_CSI2_PADS_NUM 2 |
| |
| #define MIPI_CSI2_DEF_PIX_WIDTH 640 |
| #define MIPI_CSI2_DEF_PIX_HEIGHT 480 |
| |
| /* Register map definition */ |
| |
| /* i.MX8MQ CSI-2 controller CSR */ |
| #define CSI2RX_CFG_NUM_LANES 0x100 |
| #define CSI2RX_CFG_DISABLE_DATA_LANES 0x104 |
| #define CSI2RX_BIT_ERR 0x108 |
| #define CSI2RX_IRQ_STATUS 0x10c |
| #define CSI2RX_IRQ_MASK 0x110 |
| #define CSI2RX_IRQ_MASK_ALL 0x1ff |
| #define CSI2RX_IRQ_MASK_ULPS_STATUS_CHANGE 0x8 |
| #define CSI2RX_ULPS_STATUS 0x114 |
| #define CSI2RX_PPI_ERRSOT_HS 0x118 |
| #define CSI2RX_PPI_ERRSOTSYNC_HS 0x11c |
| #define CSI2RX_PPI_ERRESC 0x120 |
| #define CSI2RX_PPI_ERRSYNCESC 0x124 |
| #define CSI2RX_PPI_ERRCONTROL 0x128 |
| #define CSI2RX_CFG_DISABLE_PAYLOAD_0 0x12c |
| #define CSI2RX_CFG_VID_VC_IGNORE 0x180 |
| #define CSI2RX_CFG_VID_VC 0x184 |
| #define CSI2RX_CFG_VID_P_FIFO_SEND_LEVEL 0x188 |
| #define CSI2RX_CFG_DISABLE_PAYLOAD_1 0x130 |
| |
| struct csi_state; |
| |
| enum { |
| ST_POWERED = 1, |
| ST_STREAMING = 2, |
| ST_SUSPENDED = 4, |
| }; |
| |
| struct imx8mq_plat_data { |
| int (*enable)(struct csi_state *state, u32 hs_settle); |
| void (*disable)(struct csi_state *state); |
| bool use_reg_csr; |
| }; |
| |
| /* |
| * The send level configures the number of entries that must accumulate in |
| * the Pixel FIFO before the data will be transferred to the video output. |
| * The exact value needed for this configuration is dependent on the rate at |
| * which the sensor transfers data to the CSI-2 Controller and the user |
| * video clock. |
| * |
| * The calculation is the classical rate-in rate-out type of problem: If the |
| * video bandwidth is 10% faster than the incoming mipi data and the video |
| * line length is 500 pixels, then the fifo should be allowed to fill |
| * 10% of the line length or 50 pixels. If the gap data is ok, then the level |
| * can be set to 16 and ignored. |
| */ |
| #define CSI2RX_SEND_LEVEL 64 |
| |
| struct csi_state { |
| struct device *dev; |
| const struct imx8mq_plat_data *pdata; |
| void __iomem *regs; |
| struct clk_bulk_data *clks; |
| struct clk *esc_clk; |
| u32 num_clks; |
| struct reset_control *rst; |
| struct regulator *mipi_phy_regulator; |
| |
| struct v4l2_subdev sd; |
| struct media_pad pads[MIPI_CSI2_PADS_NUM]; |
| struct v4l2_async_notifier notifier; |
| struct v4l2_subdev *src_sd; |
| |
| struct v4l2_mbus_config_mipi_csi2 bus; |
| |
| struct mutex lock; /* Protect state */ |
| u32 state; |
| |
| struct regmap *phy_gpr; |
| u8 phy_gpr_reg; |
| |
| struct icc_path *icc_path; |
| s32 icc_path_bw; |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * Format helpers |
| */ |
| |
| struct csi2_pix_format { |
| u32 code; |
| u8 width; |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * i.MX8MQ GPR |
| */ |
| |
| #define GPR_CSI2_1_RX_ENABLE BIT(13) |
| #define GPR_CSI2_1_VID_INTFC_ENB BIT(12) |
| #define GPR_CSI2_1_HSEL BIT(10) |
| #define GPR_CSI2_1_CONT_CLK_MODE BIT(8) |
| #define GPR_CSI2_1_S_PRG_RXHS_SETTLE(x) (((x) & 0x3f) << 2) |
| |
| static int imx8mq_gpr_enable(struct csi_state *state, u32 hs_settle) |
| { |
| regmap_update_bits(state->phy_gpr, |
| state->phy_gpr_reg, |
| 0x3fff, |
| GPR_CSI2_1_RX_ENABLE | |
| GPR_CSI2_1_VID_INTFC_ENB | |
| GPR_CSI2_1_HSEL | |
| GPR_CSI2_1_CONT_CLK_MODE | |
| GPR_CSI2_1_S_PRG_RXHS_SETTLE(hs_settle)); |
| |
| return 0; |
| } |
| |
| static const struct imx8mq_plat_data imx8mq_data = { |
| .enable = imx8mq_gpr_enable, |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * i.MX8QXP |
| */ |
| |
| #define CSI2SS_PL_CLK_INTERVAL_US 100 |
| #define CSI2SS_PL_CLK_TIMEOUT_US 100000 |
| |
| #define CSI2SS_PLM_CTRL 0x0 |
| #define CSI2SS_PLM_CTRL_ENABLE_PL BIT(0) |
| #define CSI2SS_PLM_CTRL_VSYNC_OVERRIDE BIT(9) |
| #define CSI2SS_PLM_CTRL_HSYNC_OVERRIDE BIT(10) |
| #define CSI2SS_PLM_CTRL_VALID_OVERRIDE BIT(11) |
| #define CSI2SS_PLM_CTRL_POLARITY_HIGH BIT(12) |
| #define CSI2SS_PLM_CTRL_PL_CLK_RUN BIT(31) |
| |
| #define CSI2SS_PHY_CTRL 0x4 |
| #define CSI2SS_PHY_CTRL_RX_ENABLE BIT(0) |
| #define CSI2SS_PHY_CTRL_AUTO_PD_EN BIT(1) |
| #define CSI2SS_PHY_CTRL_DDRCLK_EN BIT(2) |
| #define CSI2SS_PHY_CTRL_CONT_CLK_MODE BIT(3) |
| #define CSI2SS_PHY_CTRL_RX_HS_SETTLE_MASK GENMASK(9, 4) |
| #define CSI2SS_PHY_CTRL_RTERM_SEL BIT(21) |
| #define CSI2SS_PHY_CTRL_PD BIT(22) |
| |
| #define CSI2SS_DATA_TYPE_DISABLE_BF 0x38 |
| #define CSI2SS_DATA_TYPE_DISABLE_BF_MASK GENMASK(23, 0) |
| |
| #define CSI2SS_CTRL_CLK_RESET 0x44 |
| #define CSI2SS_CTRL_CLK_RESET_EN BIT(0) |
| |
| static int imx8qxp_gpr_enable(struct csi_state *state, u32 hs_settle) |
| { |
| int ret; |
| u32 val; |
| |
| /* Clear format */ |
| regmap_clear_bits(state->phy_gpr, CSI2SS_DATA_TYPE_DISABLE_BF, |
| CSI2SS_DATA_TYPE_DISABLE_BF_MASK); |
| |
| regmap_write(state->phy_gpr, CSI2SS_PLM_CTRL, 0x0); |
| |
| regmap_write(state->phy_gpr, CSI2SS_PHY_CTRL, |
| FIELD_PREP(CSI2SS_PHY_CTRL_RX_HS_SETTLE_MASK, hs_settle) | |
| CSI2SS_PHY_CTRL_RX_ENABLE | CSI2SS_PHY_CTRL_DDRCLK_EN | |
| CSI2SS_PHY_CTRL_CONT_CLK_MODE | CSI2SS_PHY_CTRL_PD | |
| CSI2SS_PHY_CTRL_RTERM_SEL | CSI2SS_PHY_CTRL_AUTO_PD_EN); |
| |
| ret = regmap_read_poll_timeout(state->phy_gpr, CSI2SS_PLM_CTRL, |
| val, !(val & CSI2SS_PLM_CTRL_PL_CLK_RUN), |
| CSI2SS_PL_CLK_INTERVAL_US, |
| CSI2SS_PL_CLK_TIMEOUT_US); |
| |
| if (ret) { |
| dev_err(state->dev, "Timeout waiting for Pixel-Link clock\n"); |
| return ret; |
| } |
| |
| /* Enable Pixel link Master */ |
| regmap_set_bits(state->phy_gpr, CSI2SS_PLM_CTRL, |
| CSI2SS_PLM_CTRL_ENABLE_PL | CSI2SS_PLM_CTRL_VALID_OVERRIDE); |
| |
| /* PHY Enable */ |
| regmap_clear_bits(state->phy_gpr, CSI2SS_PHY_CTRL, |
| CSI2SS_PHY_CTRL_PD | CSI2SS_PLM_CTRL_POLARITY_HIGH); |
| |
| /* Release Reset */ |
| regmap_set_bits(state->phy_gpr, CSI2SS_CTRL_CLK_RESET, CSI2SS_CTRL_CLK_RESET_EN); |
| |
| return ret; |
| } |
| |
| static void imx8qxp_gpr_disable(struct csi_state *state) |
| { |
| /* Disable Pixel Link */ |
| regmap_write(state->phy_gpr, CSI2SS_PLM_CTRL, 0x0); |
| |
| /* Disable PHY */ |
| regmap_write(state->phy_gpr, CSI2SS_PHY_CTRL, 0x0); |
| |
| regmap_clear_bits(state->phy_gpr, CSI2SS_CTRL_CLK_RESET, |
| CSI2SS_CTRL_CLK_RESET_EN); |
| }; |
| |
| static const struct imx8mq_plat_data imx8qxp_data = { |
| .enable = imx8qxp_gpr_enable, |
| .disable = imx8qxp_gpr_disable, |
| .use_reg_csr = true, |
| }; |
| |
| static const struct csi2_pix_format imx8mq_mipi_csi_formats[] = { |
| /* RAW (Bayer and greyscale) formats. */ |
| { |
| .code = MEDIA_BUS_FMT_SBGGR8_1X8, |
| .width = 8, |
| }, { |
| .code = MEDIA_BUS_FMT_SGBRG8_1X8, |
| .width = 8, |
| }, { |
| .code = MEDIA_BUS_FMT_SGRBG8_1X8, |
| .width = 8, |
| }, { |
| .code = MEDIA_BUS_FMT_SRGGB8_1X8, |
| .width = 8, |
| }, { |
| .code = MEDIA_BUS_FMT_Y8_1X8, |
| .width = 8, |
| }, { |
| .code = MEDIA_BUS_FMT_SBGGR10_1X10, |
| .width = 10, |
| }, { |
| .code = MEDIA_BUS_FMT_SGBRG10_1X10, |
| .width = 10, |
| }, { |
| .code = MEDIA_BUS_FMT_SGRBG10_1X10, |
| .width = 10, |
| }, { |
| .code = MEDIA_BUS_FMT_SRGGB10_1X10, |
| .width = 10, |
| }, { |
| .code = MEDIA_BUS_FMT_Y10_1X10, |
| .width = 10, |
| }, { |
| .code = MEDIA_BUS_FMT_SBGGR12_1X12, |
| .width = 12, |
| }, { |
| .code = MEDIA_BUS_FMT_SGBRG12_1X12, |
| .width = 12, |
| }, { |
| .code = MEDIA_BUS_FMT_SGRBG12_1X12, |
| .width = 12, |
| }, { |
| .code = MEDIA_BUS_FMT_SRGGB12_1X12, |
| .width = 12, |
| }, { |
| .code = MEDIA_BUS_FMT_Y12_1X12, |
| .width = 12, |
| }, { |
| .code = MEDIA_BUS_FMT_SBGGR14_1X14, |
| .width = 14, |
| }, { |
| .code = MEDIA_BUS_FMT_SGBRG14_1X14, |
| .width = 14, |
| }, { |
| .code = MEDIA_BUS_FMT_SGRBG14_1X14, |
| .width = 14, |
| }, { |
| .code = MEDIA_BUS_FMT_SRGGB14_1X14, |
| .width = 14, |
| }, |
| /* YUV formats */ |
| { |
| .code = MEDIA_BUS_FMT_YUYV8_1X16, |
| .width = 16, |
| }, { |
| .code = MEDIA_BUS_FMT_UYVY8_1X16, |
| .width = 16, |
| } |
| }; |
| |
| static const struct csi2_pix_format *find_csi2_format(u32 code) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < ARRAY_SIZE(imx8mq_mipi_csi_formats); i++) |
| if (code == imx8mq_mipi_csi_formats[i].code) |
| return &imx8mq_mipi_csi_formats[i]; |
| return NULL; |
| } |
| |
| /* ----------------------------------------------------------------------------- |
| * Hardware configuration |
| */ |
| |
| static inline void imx8mq_mipi_csi_write(struct csi_state *state, u32 reg, u32 val) |
| { |
| writel(val, state->regs + reg); |
| } |
| |
| static int imx8mq_mipi_csi_sw_reset(struct csi_state *state) |
| { |
| int ret; |
| |
| ret = reset_control_assert(state->rst); |
| if (ret < 0) { |
| dev_err(state->dev, "Failed to assert resets: %d\n", ret); |
| return ret; |
| } |
| |
| /* Explicitly release reset to make sure reset bits are cleared. */ |
| return reset_control_deassert(state->rst); |
| } |
| |
| static void imx8mq_mipi_csi_set_params(struct csi_state *state) |
| { |
| int lanes = state->bus.num_data_lanes; |
| |
| imx8mq_mipi_csi_write(state, CSI2RX_CFG_NUM_LANES, lanes - 1); |
| imx8mq_mipi_csi_write(state, CSI2RX_CFG_DISABLE_DATA_LANES, |
| (0xf << lanes) & 0xf); |
| imx8mq_mipi_csi_write(state, CSI2RX_IRQ_MASK, CSI2RX_IRQ_MASK_ALL); |
| /* |
| * 0x180 bit 0 controls the Virtual Channel behaviour: when set the |
| * interface ignores the Virtual Channel (VC) field in received packets; |
| * when cleared it causes the interface to only accept packets whose VC |
| * matches the value to which VC is set at offset 0x184. |
| */ |
| imx8mq_mipi_csi_write(state, CSI2RX_CFG_VID_VC_IGNORE, 1); |
| imx8mq_mipi_csi_write(state, CSI2RX_CFG_VID_P_FIFO_SEND_LEVEL, |
| CSI2RX_SEND_LEVEL); |
| } |
| |
| static struct clk *imx8mq_mipi_csi_find_esc_clk(struct csi_state *state) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < state->num_clks; i++) { |
| if (!strcmp(state->clks[i].id, "esc")) |
| return state->clks[i].clk; |
| } |
| |
| return ERR_PTR(-ENODEV); |
| } |
| |
| static int imx8mq_mipi_csi_calc_hs_settle(struct csi_state *state, |
| struct v4l2_subdev_state *sd_state, |
| u32 *hs_settle) |
| { |
| struct media_pad *src_pad; |
| s64 link_freq; |
| u32 lane_rate; |
| unsigned long esc_clk_rate; |
| u32 min_ths_settle, max_ths_settle, ths_settle_ns, esc_clk_period_ns; |
| const struct v4l2_mbus_framefmt *fmt; |
| const struct csi2_pix_format *csi2_fmt; |
| |
| src_pad = media_entity_remote_source_pad_unique(&sd_state->sd->entity); |
| if (IS_ERR(src_pad)) { |
| dev_err(state->dev, "can't get source pad of %s (%pe)\n", |
| sd_state->sd->name, src_pad); |
| return PTR_ERR(src_pad); |
| } |
| |
| /* Calculate the line rate from the pixel rate. */ |
| |
| fmt = v4l2_subdev_state_get_format(sd_state, MIPI_CSI2_PAD_SINK); |
| csi2_fmt = find_csi2_format(fmt->code); |
| |
| link_freq = v4l2_get_link_freq(src_pad, csi2_fmt->width, |
| state->bus.num_data_lanes * 2); |
| if (link_freq < 0) { |
| dev_err(state->dev, "Unable to obtain link frequency: %d\n", |
| (int)link_freq); |
| return link_freq; |
| } |
| |
| lane_rate = link_freq * 2; |
| if (lane_rate < 80000000 || lane_rate > 1500000000) { |
| dev_dbg(state->dev, "Out-of-bound lane rate %u\n", lane_rate); |
| return -EINVAL; |
| } |
| |
| /* |
| * The D-PHY specification requires Ths-settle to be in the range |
| * 85ns + 6*UI to 140ns + 10*UI, with the unit interval UI being half |
| * the clock period. |
| * |
| * The Ths-settle value is expressed in the hardware as a multiple of |
| * the Esc clock period: |
| * |
| * Ths-settle = (PRG_RXHS_SETTLE + 1) * Tperiod of RxClkInEsc |
| * |
| * Due to the one cycle inaccuracy introduced by rounding, the |
| * documentation recommends picking a value away from the boundaries. |
| * Let's pick the average. |
| */ |
| esc_clk_rate = clk_get_rate(state->esc_clk); |
| if (!esc_clk_rate) { |
| dev_err(state->dev, "Could not get esc clock rate.\n"); |
| return -EINVAL; |
| } |
| |
| dev_dbg(state->dev, "esc clk rate: %lu\n", esc_clk_rate); |
| esc_clk_period_ns = 1000000000 / esc_clk_rate; |
| |
| min_ths_settle = 85 + 6 * 1000000 / (lane_rate / 1000); |
| max_ths_settle = 140 + 10 * 1000000 / (lane_rate / 1000); |
| ths_settle_ns = (min_ths_settle + max_ths_settle) / 2; |
| |
| *hs_settle = ths_settle_ns / esc_clk_period_ns - 1; |
| |
| dev_dbg(state->dev, "lane rate %u Ths_settle %u hs_settle %u\n", |
| lane_rate, ths_settle_ns, *hs_settle); |
| |
| return 0; |
| } |
| |
| static int imx8mq_mipi_csi_start_stream(struct csi_state *state, |
| struct v4l2_subdev_state *sd_state) |
| { |
| int ret; |
| u32 hs_settle = 0; |
| |
| ret = imx8mq_mipi_csi_sw_reset(state); |
| if (ret) |
| return ret; |
| |
| imx8mq_mipi_csi_set_params(state); |
| ret = imx8mq_mipi_csi_calc_hs_settle(state, sd_state, &hs_settle); |
| if (ret) |
| return ret; |
| |
| ret = state->pdata->enable(state, hs_settle); |
| if (ret) |
| return ret; |
| |
| return 0; |
| } |
| |
| static void imx8mq_mipi_csi_stop_stream(struct csi_state *state) |
| { |
| imx8mq_mipi_csi_write(state, CSI2RX_CFG_DISABLE_DATA_LANES, 0xf); |
| |
| if (state->pdata->disable) |
| state->pdata->disable(state); |
| } |
| |
| /* ----------------------------------------------------------------------------- |
| * V4L2 subdev operations |
| */ |
| |
| static struct csi_state *mipi_sd_to_csi2_state(struct v4l2_subdev *sdev) |
| { |
| return container_of(sdev, struct csi_state, sd); |
| } |
| |
| static int imx8mq_mipi_csi_s_stream(struct v4l2_subdev *sd, int enable) |
| { |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| struct v4l2_subdev_state *sd_state; |
| int ret = 0; |
| |
| if (enable) { |
| ret = pm_runtime_resume_and_get(state->dev); |
| if (ret < 0) |
| return ret; |
| } |
| |
| mutex_lock(&state->lock); |
| |
| if (enable) { |
| if (state->state & ST_SUSPENDED) { |
| ret = -EBUSY; |
| goto unlock; |
| } |
| |
| sd_state = v4l2_subdev_lock_and_get_active_state(sd); |
| ret = imx8mq_mipi_csi_start_stream(state, sd_state); |
| v4l2_subdev_unlock_state(sd_state); |
| |
| if (ret < 0) |
| goto unlock; |
| |
| ret = v4l2_subdev_call(state->src_sd, video, s_stream, 1); |
| if (ret < 0) |
| goto unlock; |
| |
| state->state |= ST_STREAMING; |
| } else { |
| v4l2_subdev_call(state->src_sd, video, s_stream, 0); |
| imx8mq_mipi_csi_stop_stream(state); |
| state->state &= ~ST_STREAMING; |
| } |
| |
| unlock: |
| mutex_unlock(&state->lock); |
| |
| if (!enable || ret < 0) |
| pm_runtime_put(state->dev); |
| |
| return ret; |
| } |
| |
| static int imx8mq_mipi_csi_init_state(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *sd_state) |
| { |
| struct v4l2_mbus_framefmt *fmt_sink; |
| struct v4l2_mbus_framefmt *fmt_source; |
| |
| fmt_sink = v4l2_subdev_state_get_format(sd_state, MIPI_CSI2_PAD_SINK); |
| fmt_source = v4l2_subdev_state_get_format(sd_state, |
| MIPI_CSI2_PAD_SOURCE); |
| |
| fmt_sink->code = MEDIA_BUS_FMT_SGBRG10_1X10; |
| fmt_sink->width = MIPI_CSI2_DEF_PIX_WIDTH; |
| fmt_sink->height = MIPI_CSI2_DEF_PIX_HEIGHT; |
| fmt_sink->field = V4L2_FIELD_NONE; |
| |
| fmt_sink->colorspace = V4L2_COLORSPACE_RAW; |
| fmt_sink->xfer_func = V4L2_MAP_XFER_FUNC_DEFAULT(fmt_sink->colorspace); |
| fmt_sink->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(fmt_sink->colorspace); |
| fmt_sink->quantization = |
| V4L2_MAP_QUANTIZATION_DEFAULT(false, fmt_sink->colorspace, |
| fmt_sink->ycbcr_enc); |
| |
| *fmt_source = *fmt_sink; |
| |
| return 0; |
| } |
| |
| static int imx8mq_mipi_csi_enum_mbus_code(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *sd_state, |
| struct v4l2_subdev_mbus_code_enum *code) |
| { |
| /* |
| * We can't transcode in any way, the source format is identical |
| * to the sink format. |
| */ |
| if (code->pad == MIPI_CSI2_PAD_SOURCE) { |
| struct v4l2_mbus_framefmt *fmt; |
| |
| if (code->index > 0) |
| return -EINVAL; |
| |
| fmt = v4l2_subdev_state_get_format(sd_state, code->pad); |
| code->code = fmt->code; |
| return 0; |
| } |
| |
| if (code->pad != MIPI_CSI2_PAD_SINK) |
| return -EINVAL; |
| |
| if (code->index >= ARRAY_SIZE(imx8mq_mipi_csi_formats)) |
| return -EINVAL; |
| |
| code->code = imx8mq_mipi_csi_formats[code->index].code; |
| |
| return 0; |
| } |
| |
| static int imx8mq_mipi_csi_set_fmt(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *sd_state, |
| struct v4l2_subdev_format *sdformat) |
| { |
| const struct csi2_pix_format *csi2_fmt; |
| struct v4l2_mbus_framefmt *fmt; |
| |
| /* |
| * The device can't transcode in any way, the source format can't be |
| * modified. |
| */ |
| if (sdformat->pad == MIPI_CSI2_PAD_SOURCE) |
| return v4l2_subdev_get_fmt(sd, sd_state, sdformat); |
| |
| if (sdformat->pad != MIPI_CSI2_PAD_SINK) |
| return -EINVAL; |
| |
| csi2_fmt = find_csi2_format(sdformat->format.code); |
| if (!csi2_fmt) |
| csi2_fmt = &imx8mq_mipi_csi_formats[0]; |
| |
| fmt = v4l2_subdev_state_get_format(sd_state, sdformat->pad); |
| |
| fmt->code = csi2_fmt->code; |
| fmt->width = sdformat->format.width; |
| fmt->height = sdformat->format.height; |
| |
| sdformat->format = *fmt; |
| |
| /* Propagate the format from sink to source. */ |
| fmt = v4l2_subdev_state_get_format(sd_state, MIPI_CSI2_PAD_SOURCE); |
| *fmt = sdformat->format; |
| |
| return 0; |
| } |
| |
| static const struct v4l2_subdev_video_ops imx8mq_mipi_csi_video_ops = { |
| .s_stream = imx8mq_mipi_csi_s_stream, |
| }; |
| |
| static const struct v4l2_subdev_pad_ops imx8mq_mipi_csi_pad_ops = { |
| .enum_mbus_code = imx8mq_mipi_csi_enum_mbus_code, |
| .get_fmt = v4l2_subdev_get_fmt, |
| .set_fmt = imx8mq_mipi_csi_set_fmt, |
| }; |
| |
| static const struct v4l2_subdev_ops imx8mq_mipi_csi_subdev_ops = { |
| .video = &imx8mq_mipi_csi_video_ops, |
| .pad = &imx8mq_mipi_csi_pad_ops, |
| }; |
| |
| static const struct v4l2_subdev_internal_ops imx8mq_mipi_csi_internal_ops = { |
| .init_state = imx8mq_mipi_csi_init_state, |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * Media entity operations |
| */ |
| |
| static const struct media_entity_operations imx8mq_mipi_csi_entity_ops = { |
| .link_validate = v4l2_subdev_link_validate, |
| .get_fwnode_pad = v4l2_subdev_get_fwnode_pad_1_to_1, |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * Async subdev notifier |
| */ |
| |
| static struct csi_state * |
| mipi_notifier_to_csi2_state(struct v4l2_async_notifier *n) |
| { |
| return container_of(n, struct csi_state, notifier); |
| } |
| |
| static int imx8mq_mipi_csi_notify_bound(struct v4l2_async_notifier *notifier, |
| struct v4l2_subdev *sd, |
| struct v4l2_async_connection *asd) |
| { |
| struct csi_state *state = mipi_notifier_to_csi2_state(notifier); |
| struct media_pad *sink = &state->sd.entity.pads[MIPI_CSI2_PAD_SINK]; |
| |
| state->src_sd = sd; |
| |
| return v4l2_create_fwnode_links_to_pad(sd, sink, MEDIA_LNK_FL_ENABLED | |
| MEDIA_LNK_FL_IMMUTABLE); |
| } |
| |
| static const struct v4l2_async_notifier_operations imx8mq_mipi_csi_notify_ops = { |
| .bound = imx8mq_mipi_csi_notify_bound, |
| }; |
| |
| static int imx8mq_mipi_csi_async_register(struct csi_state *state) |
| { |
| struct v4l2_fwnode_endpoint vep = { |
| .bus_type = V4L2_MBUS_CSI2_DPHY, |
| }; |
| struct v4l2_async_connection *asd; |
| unsigned int i; |
| int ret; |
| |
| v4l2_async_subdev_nf_init(&state->notifier, &state->sd); |
| |
| struct fwnode_handle *ep __free(fwnode_handle) = |
| fwnode_graph_get_endpoint_by_id(dev_fwnode(state->dev), 0, 0, |
| FWNODE_GRAPH_ENDPOINT_NEXT); |
| if (!ep) |
| return dev_err_probe(state->dev, -ENOTCONN, |
| "failed to get local endpoint fwnode\n"); |
| |
| ret = v4l2_fwnode_endpoint_parse(ep, &vep); |
| if (ret) |
| return dev_err_probe(state->dev, ret, |
| "failed to parse endpoint\n"); |
| |
| for (i = 0; i < vep.bus.mipi_csi2.num_data_lanes; ++i) { |
| if (vep.bus.mipi_csi2.data_lanes[i] != i + 1) |
| return dev_err_probe(state->dev, -EINVAL, |
| "data lanes reordering is not supported"); |
| } |
| |
| state->bus = vep.bus.mipi_csi2; |
| |
| dev_dbg(state->dev, "data lanes: %d flags: 0x%08x\n", |
| state->bus.num_data_lanes, |
| state->bus.flags); |
| |
| asd = v4l2_async_nf_add_fwnode_remote(&state->notifier, ep, |
| struct v4l2_async_connection); |
| if (IS_ERR(asd)) |
| return dev_err_probe(state->dev, PTR_ERR(asd), |
| "failed to add fwnode to notifier\n"); |
| |
| state->notifier.ops = &imx8mq_mipi_csi_notify_ops; |
| |
| ret = v4l2_async_nf_register(&state->notifier); |
| if (ret) |
| return dev_err_probe(state->dev, ret, |
| "failed to register notifier\n"); |
| |
| ret = v4l2_async_register_subdev(&state->sd); |
| if (ret) |
| return dev_err_probe(state->dev, ret, |
| "failed to register subdev\n"); |
| |
| return 0; |
| } |
| |
| /* ----------------------------------------------------------------------------- |
| * Suspend/resume |
| */ |
| |
| static void imx8mq_mipi_csi_pm_suspend(struct device *dev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| |
| mutex_lock(&state->lock); |
| |
| if (state->state & ST_POWERED) { |
| imx8mq_mipi_csi_stop_stream(state); |
| clk_bulk_disable_unprepare(state->num_clks, state->clks); |
| state->state &= ~ST_POWERED; |
| } |
| |
| mutex_unlock(&state->lock); |
| } |
| |
| static int imx8mq_mipi_csi_pm_resume(struct device *dev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| struct v4l2_subdev_state *sd_state; |
| int ret = 0; |
| |
| mutex_lock(&state->lock); |
| |
| if (!(state->state & ST_POWERED)) { |
| state->state |= ST_POWERED; |
| ret = clk_bulk_prepare_enable(state->num_clks, state->clks); |
| } |
| if (state->state & ST_STREAMING) { |
| sd_state = v4l2_subdev_lock_and_get_active_state(sd); |
| ret = imx8mq_mipi_csi_start_stream(state, sd_state); |
| v4l2_subdev_unlock_state(sd_state); |
| if (ret) |
| goto unlock; |
| } |
| |
| state->state &= ~ST_SUSPENDED; |
| |
| unlock: |
| mutex_unlock(&state->lock); |
| |
| return ret ? -EAGAIN : 0; |
| } |
| |
| static int imx8mq_mipi_csi_suspend(struct device *dev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| |
| imx8mq_mipi_csi_pm_suspend(dev); |
| |
| state->state |= ST_SUSPENDED; |
| |
| return 0; |
| } |
| |
| static int imx8mq_mipi_csi_resume(struct device *dev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| |
| if (!(state->state & ST_SUSPENDED)) |
| return 0; |
| |
| return imx8mq_mipi_csi_pm_resume(dev); |
| } |
| |
| static int imx8mq_mipi_csi_runtime_suspend(struct device *dev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| int ret; |
| |
| imx8mq_mipi_csi_pm_suspend(dev); |
| |
| ret = icc_set_bw(state->icc_path, 0, 0); |
| if (ret) |
| dev_err(dev, "icc_set_bw failed with %d\n", ret); |
| |
| return ret; |
| } |
| |
| static int imx8mq_mipi_csi_runtime_resume(struct device *dev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| int ret; |
| |
| ret = icc_set_bw(state->icc_path, 0, state->icc_path_bw); |
| if (ret) { |
| dev_err(dev, "icc_set_bw failed with %d\n", ret); |
| return ret; |
| } |
| |
| return imx8mq_mipi_csi_pm_resume(dev); |
| } |
| |
| static const struct dev_pm_ops imx8mq_mipi_csi_pm_ops = { |
| RUNTIME_PM_OPS(imx8mq_mipi_csi_runtime_suspend, |
| imx8mq_mipi_csi_runtime_resume, NULL) |
| SYSTEM_SLEEP_PM_OPS(imx8mq_mipi_csi_suspend, imx8mq_mipi_csi_resume) |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * Probe/remove & platform driver |
| */ |
| |
| static int imx8mq_mipi_csi_subdev_init(struct csi_state *state) |
| { |
| struct v4l2_subdev *sd = &state->sd; |
| int ret; |
| |
| v4l2_subdev_init(sd, &imx8mq_mipi_csi_subdev_ops); |
| sd->internal_ops = &imx8mq_mipi_csi_internal_ops; |
| sd->owner = THIS_MODULE; |
| snprintf(sd->name, sizeof(sd->name), "%s %s", |
| MIPI_CSI2_SUBDEV_NAME, dev_name(state->dev)); |
| |
| sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE; |
| |
| sd->entity.function = MEDIA_ENT_F_VID_IF_BRIDGE; |
| sd->entity.ops = &imx8mq_mipi_csi_entity_ops; |
| |
| sd->dev = state->dev; |
| |
| state->pads[MIPI_CSI2_PAD_SINK].flags = MEDIA_PAD_FL_SINK |
| | MEDIA_PAD_FL_MUST_CONNECT; |
| state->pads[MIPI_CSI2_PAD_SOURCE].flags = MEDIA_PAD_FL_SOURCE |
| | MEDIA_PAD_FL_MUST_CONNECT; |
| ret = media_entity_pads_init(&sd->entity, MIPI_CSI2_PADS_NUM, |
| state->pads); |
| if (ret) |
| return ret; |
| |
| ret = v4l2_subdev_init_finalize(sd); |
| if (ret) { |
| media_entity_cleanup(&sd->entity); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static void imx8mq_mipi_csi_release_icc(struct platform_device *pdev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(&pdev->dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| |
| icc_put(state->icc_path); |
| } |
| |
| static int imx8mq_mipi_csi_init_icc(struct platform_device *pdev) |
| { |
| struct v4l2_subdev *sd = dev_get_drvdata(&pdev->dev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| |
| /* Optional interconnect request */ |
| state->icc_path = of_icc_get(&pdev->dev, "dram"); |
| if (IS_ERR_OR_NULL(state->icc_path)) |
| return PTR_ERR_OR_ZERO(state->icc_path); |
| |
| state->icc_path_bw = MBps_to_icc(700); |
| |
| return 0; |
| } |
| |
| static int imx8mq_mipi_csi_parse_dt(struct csi_state *state) |
| { |
| struct device *dev = state->dev; |
| struct device_node *np = state->dev->of_node; |
| struct device_node *node; |
| phandle ph; |
| u32 out_val[2]; |
| int ret = 0; |
| |
| state->rst = devm_reset_control_array_get_exclusive(dev); |
| if (IS_ERR(state->rst)) |
| return dev_err_probe(dev, PTR_ERR(state->rst), |
| "Failed to get reset\n"); |
| |
| if (state->pdata->use_reg_csr) { |
| const struct regmap_config regmap_config = { |
| .reg_bits = 32, |
| .val_bits = 32, |
| .reg_stride = 4, |
| }; |
| void __iomem *base; |
| |
| base = devm_platform_ioremap_resource(to_platform_device(dev), 1); |
| if (IS_ERR(base)) |
| return dev_err_probe(dev, PTR_ERR(base), "Missing CSR register\n"); |
| |
| state->phy_gpr = devm_regmap_init_mmio(dev, base, ®map_config); |
| if (IS_ERR(state->phy_gpr)) |
| return dev_err_probe(dev, PTR_ERR(state->phy_gpr), |
| "Failed to init CSI MMIO regmap\n"); |
| return 0; |
| } |
| |
| ret = of_property_read_u32_array(np, "fsl,mipi-phy-gpr", out_val, |
| ARRAY_SIZE(out_val)); |
| if (ret) |
| return dev_err_probe(dev, ret, "property %s not found\n", |
| "fsl,mipi-phy-gpr"); |
| |
| ph = *out_val; |
| |
| node = of_find_node_by_phandle(ph); |
| if (!node) |
| return dev_err_probe(dev, -ENODEV, |
| "Error finding node by phandle\n"); |
| |
| state->phy_gpr = syscon_node_to_regmap(node); |
| of_node_put(node); |
| if (IS_ERR(state->phy_gpr)) |
| return dev_err_probe(dev, PTR_ERR(state->phy_gpr), |
| "failed to get gpr regmap\n"); |
| |
| state->phy_gpr_reg = out_val[1]; |
| dev_dbg(dev, "phy gpr register set to 0x%x\n", state->phy_gpr_reg); |
| |
| return ret; |
| } |
| |
| static int imx8mq_mipi_csi_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct csi_state *state; |
| int ret; |
| |
| state = devm_kzalloc(dev, sizeof(*state), GFP_KERNEL); |
| if (!state) |
| return -ENOMEM; |
| |
| state->dev = dev; |
| |
| state->pdata = of_device_get_match_data(dev); |
| |
| ret = imx8mq_mipi_csi_parse_dt(state); |
| if (ret < 0) |
| return ret; |
| |
| /* Acquire resources. */ |
| state->regs = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(state->regs)) |
| return PTR_ERR(state->regs); |
| |
| ret = devm_clk_bulk_get_all(dev, &state->clks); |
| if (ret < 0) |
| return dev_err_probe(dev, ret, "Failed to get clocks\n"); |
| |
| state->num_clks = ret; |
| |
| state->esc_clk = imx8mq_mipi_csi_find_esc_clk(state); |
| if (IS_ERR(state->esc_clk)) |
| return dev_err_probe(dev, PTR_ERR(state->esc_clk), |
| "Couldn't find esc clock\n"); |
| |
| platform_set_drvdata(pdev, &state->sd); |
| |
| mutex_init(&state->lock); |
| |
| ret = imx8mq_mipi_csi_subdev_init(state); |
| if (ret < 0) |
| goto mutex; |
| |
| ret = imx8mq_mipi_csi_init_icc(pdev); |
| if (ret) |
| goto mutex; |
| |
| /* Enable runtime PM. */ |
| pm_runtime_enable(dev); |
| if (!pm_runtime_enabled(dev)) { |
| ret = imx8mq_mipi_csi_runtime_resume(dev); |
| if (ret < 0) |
| goto icc; |
| } |
| |
| ret = imx8mq_mipi_csi_async_register(state); |
| if (ret < 0) |
| goto cleanup; |
| |
| return 0; |
| |
| cleanup: |
| pm_runtime_disable(&pdev->dev); |
| imx8mq_mipi_csi_runtime_suspend(&pdev->dev); |
| |
| media_entity_cleanup(&state->sd.entity); |
| v4l2_subdev_cleanup(&state->sd); |
| v4l2_async_nf_unregister(&state->notifier); |
| v4l2_async_nf_cleanup(&state->notifier); |
| v4l2_async_unregister_subdev(&state->sd); |
| icc: |
| imx8mq_mipi_csi_release_icc(pdev); |
| mutex: |
| mutex_destroy(&state->lock); |
| |
| return ret; |
| } |
| |
| static void imx8mq_mipi_csi_remove(struct platform_device *pdev) |
| { |
| struct v4l2_subdev *sd = platform_get_drvdata(pdev); |
| struct csi_state *state = mipi_sd_to_csi2_state(sd); |
| |
| v4l2_async_nf_unregister(&state->notifier); |
| v4l2_async_nf_cleanup(&state->notifier); |
| v4l2_async_unregister_subdev(&state->sd); |
| |
| pm_runtime_disable(&pdev->dev); |
| imx8mq_mipi_csi_runtime_suspend(&pdev->dev); |
| media_entity_cleanup(&state->sd.entity); |
| v4l2_subdev_cleanup(&state->sd); |
| mutex_destroy(&state->lock); |
| pm_runtime_set_suspended(&pdev->dev); |
| imx8mq_mipi_csi_release_icc(pdev); |
| } |
| |
| static const struct of_device_id imx8mq_mipi_csi_of_match[] = { |
| { .compatible = "fsl,imx8mq-mipi-csi2", .data = &imx8mq_data }, |
| { .compatible = "fsl,imx8qxp-mipi-csi2", .data = &imx8qxp_data }, |
| { .compatible = "fsl,imx8ulp-mipi-csi2", .data = &imx8qxp_data }, |
| { /* sentinel */ }, |
| }; |
| MODULE_DEVICE_TABLE(of, imx8mq_mipi_csi_of_match); |
| |
| static struct platform_driver imx8mq_mipi_csi_driver = { |
| .probe = imx8mq_mipi_csi_probe, |
| .remove = imx8mq_mipi_csi_remove, |
| .driver = { |
| .of_match_table = imx8mq_mipi_csi_of_match, |
| .name = MIPI_CSI2_DRIVER_NAME, |
| .pm = pm_ptr(&imx8mq_mipi_csi_pm_ops), |
| }, |
| }; |
| |
| module_platform_driver(imx8mq_mipi_csi_driver); |
| |
| MODULE_DESCRIPTION("i.MX8MQ MIPI CSI-2 receiver driver"); |
| MODULE_AUTHOR("Martin Kepplinger <martin.kepplinger@puri.sm>"); |
| MODULE_LICENSE("GPL v2"); |