* [PATCH v6 0/2] media: i2c: add support for ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge
@ 2026-07-22 11:18 ` Hermes Wu
0 siblings, 0 replies; 8+ messages in thread
From: Hermes Wu via B4 Relay @ 2026-07-22 11:18 UTC (permalink / raw)
To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: linux-media, devicetree, linux-kernel, Hermes Wu
This series adds support for the ITE IT6625/IT6626 HDMI to MIPI CSI-2
bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an HDMI 2.1
input, converting it to a D-PHY (or C/D-PHY on IT6626) MIPI CSI-2
output. The bridge is configured over I2C, exposes an HDMI CEC
adapter, and supports EDID read/write, DV timings detection and
configuration, and HPD control via the standard V4L2 subdevice pad
and video ops.
Patch 1 documents the devicetree binding for the bridge, including
its two selectable CSI-2 output graph ports.
Patch 2 adds the V4L2 subdevice driver itself.
Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw>
---
Changes in v6:
- Patch 2 (driver): fix two correctness issues found by automated
review of v5 -- an ignored v4l2_phys_addr_validate() return let a
malformed CEC physical address reach cec_s_phys_addr() and get
broadcast on the CEC bus, and it6625_parse_endpoint() pre-set the
fwnode bus_type to D-PHY, which made the parser reject every IT6626
C-PHY device tree outright. One other v5 review finding (a
concurrent-ioctl-vs-remove() race) was checked and is not being
acted on -- see the patch's own changelog for the reasoning.
- Patch 1 (dt-bindings): unchanged -- the v5 review concern was
checked and dismissed; see the patch's own changelog for the
reasoning.
- Link to v5: https://lore.kernel.org/r/20260722-upstream-it6625-it6626-v5-0-387e7584f2c9@ite.com.tw
Changes in v5:
- Patch 2 (driver): fix further correctness issues found by automated
review of v4 -- two debugfs read handlers could leak uninitialized
kernel stack memory to userspace on an I2C read failure, TRY-format
state could go stale or leak active hardware state into what should
be an isolated buffer, a concurrent S_FMT ioctl could interleave
with hardware programming because the lock was released too early,
S_EDID's blocks==0 case didn't actually disable the EDID per spec
and a failed EDID write left HPD disabled permanently, the mipi_reg
debugfs write was unlocked, and the HDMI info control returned a
hardcoded string instead of real telemetry; also wire up the
previously-unused reset-gpios line during probe. See the patch's
own changelog for the full list, including three other v4 review
findings that were checked (one against the hardware author's own
knowledge of the chip) and are not being acted on -- see the
patch's own changelog for the reasoning.
- Patch 1 (dt-bindings): unchanged -- both v4 review concerns for this
patch were self-dismissed by the automated review itself with sound
reasoning (the endpoint/patternProperties gap matches an established
single-endpoint convention already used elsewhere in-tree, and
dropping port@2 doesn't weaken validation since graph.yaml's own
generic port pattern still covers it).
- Link to v4: https://lore.kernel.org/r/20260722-upstream-it6625-it6626-v4-0-c03362eb55ca@ite.com.tw
Changes in v4:
- Patch 2 (driver): fix further correctness issues found by automated
review of v3 -- a CEC RX interrupt left unacknowledged on an invalid
message length could storm the IRQ line, set_fmt returned -EINVAL
for an unrecognized format code instead of falling back to a
default, get_fmt's active-path reads of the format state were
unlocked against a concurrent set_fmt, g_edid/s_edid didn't follow
the VIDIOC_G/S_EDID spec for over-capacity requests, and the two
mipi_reg debugfs handlers were missing locking/offset handling; also
mark the HDMI info control volatile so it is actually refreshed. See
the patch's own changelog for the full list. Separately (not a
review finding), drop a redundant FW-start register trigger from
probe -- the firmware starts automatically on power-on.
- Patch 1 (dt-bindings): drop the unused port@2 (HDMI input) graph
port entirely rather than the review's suggested fix of adding it to
required, and require bus-type on port@0/port@1 endpoints for
"ite,it6626" specifically, since only that variant's D-PHY default
can silently mismatch actual board wiring. Both found by the
automated review of v3, which also flagged one other dt-bindings
issue that was checked and is not being acted on -- see the patch's
own changelog for the reasoning.
- Link to v3: https://lore.kernel.org/r/20260722-upstream-it6625-it6626-v3-0-947e50f01764@ite.com.tw
Changes in v3:
- Patch 2 (driver): fix further correctness issues found by automated
review of v2 -- two banked-register reads still missing a lock, a
CEC RX length TOCTOU and a dropped CEC TX interrupt on invalid RX
length, it6625_s_edid() treating a successful write as a failure,
missing V4L2_SUBDEV_FORMAT_TRY support, an unlocked active-format
commit, and detected timings lumping the whole blanking interval
into hsync/vsync instead of reading the real porch registers. See
the patch's own changelog for the full list, including four other
v2 review findings that were checked and are not being acted on
(with reasoning).
- Patch 1 (dt-bindings): unchanged.
- Link to v2: https://lore.kernel.org/r/20260721-upstream-it6625-it6626-v2-0-7c4f3564ca38@ite.com.tw
Changes in v2:
- Patch 2 (driver): fix several correctness issues found by automated
review of v1 -- premature V4L2 async subdev registration/teardown
ordering, a missing-lock race on banked register reads, a
status-poll interval bug, a dropped CEC interrupt, an unacked CEC
transmit, set_fmt mutating active state on TRY, EDID blocks=0
handling, and a couple of uninitialized-stack hardening fixes. See
the patch's own changelog for the full list.
- Patch 1 (dt-bindings): restrict port@0/port@1 bus-type to D-PHY (4)
only for "ite,it6625", since that chip doesn't support C-PHY (also
found by the automated review of v1).
- Link to v1: https://lore.kernel.org/r/20260720-upstream-it6625-it6626-v1-0-475912f74459@ite.com.tw
---
Hermes Wu (2):
dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding
media: i2c: add driver for ITE IT6625/IT6626
.../devicetree/bindings/media/i2c/ite,it6625.yaml | 169 ++
MAINTAINERS | 7 +
drivers/media/i2c/Kconfig | 18 +
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/it6625.c | 2216 ++++++++++++++++++++
5 files changed, 2411 insertions(+)
---
base-commit: 8dac27bfa2f994ecb11f01a63641527d17d48fc1
change-id: 20260716-upstream-it6625-it6626-6874ab8a5305
Best regards,
--
Hermes Wu <Hermes.wu@ite.com.tw>
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH v6 0/2] media: i2c: add support for ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge @ 2026-07-22 11:18 ` Hermes Wu 0 siblings, 0 replies; 8+ messages in thread From: Hermes Wu @ 2026-07-22 11:18 UTC (permalink / raw) To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski, Conor Dooley Cc: linux-media, devicetree, linux-kernel, Hermes Wu This series adds support for the ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an HDMI 2.1 input, converting it to a D-PHY (or C/D-PHY on IT6626) MIPI CSI-2 output. The bridge is configured over I2C, exposes an HDMI CEC adapter, and supports EDID read/write, DV timings detection and configuration, and HPD control via the standard V4L2 subdevice pad and video ops. Patch 1 documents the devicetree binding for the bridge, including its two selectable CSI-2 output graph ports. Patch 2 adds the V4L2 subdevice driver itself. Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw> --- Changes in v6: - Patch 2 (driver): fix two correctness issues found by automated review of v5 -- an ignored v4l2_phys_addr_validate() return let a malformed CEC physical address reach cec_s_phys_addr() and get broadcast on the CEC bus, and it6625_parse_endpoint() pre-set the fwnode bus_type to D-PHY, which made the parser reject every IT6626 C-PHY device tree outright. One other v5 review finding (a concurrent-ioctl-vs-remove() race) was checked and is not being acted on -- see the patch's own changelog for the reasoning. - Patch 1 (dt-bindings): unchanged -- the v5 review concern was checked and dismissed; see the patch's own changelog for the reasoning. - Link to v5: https://lore.kernel.org/r/20260722-upstream-it6625-it6626-v5-0-387e7584f2c9@ite.com.tw Changes in v5: - Patch 2 (driver): fix further correctness issues found by automated review of v4 -- two debugfs read handlers could leak uninitialized kernel stack memory to userspace on an I2C read failure, TRY-format state could go stale or leak active hardware state into what should be an isolated buffer, a concurrent S_FMT ioctl could interleave with hardware programming because the lock was released too early, S_EDID's blocks==0 case didn't actually disable the EDID per spec and a failed EDID write left HPD disabled permanently, the mipi_reg debugfs write was unlocked, and the HDMI info control returned a hardcoded string instead of real telemetry; also wire up the previously-unused reset-gpios line during probe. See the patch's own changelog for the full list, including three other v4 review findings that were checked (one against the hardware author's own knowledge of the chip) and are not being acted on -- see the patch's own changelog for the reasoning. - Patch 1 (dt-bindings): unchanged -- both v4 review concerns for this patch were self-dismissed by the automated review itself with sound reasoning (the endpoint/patternProperties gap matches an established single-endpoint convention already used elsewhere in-tree, and dropping port@2 doesn't weaken validation since graph.yaml's own generic port pattern still covers it). - Link to v4: https://lore.kernel.org/r/20260722-upstream-it6625-it6626-v4-0-c03362eb55ca@ite.com.tw Changes in v4: - Patch 2 (driver): fix further correctness issues found by automated review of v3 -- a CEC RX interrupt left unacknowledged on an invalid message length could storm the IRQ line, set_fmt returned -EINVAL for an unrecognized format code instead of falling back to a default, get_fmt's active-path reads of the format state were unlocked against a concurrent set_fmt, g_edid/s_edid didn't follow the VIDIOC_G/S_EDID spec for over-capacity requests, and the two mipi_reg debugfs handlers were missing locking/offset handling; also mark the HDMI info control volatile so it is actually refreshed. See the patch's own changelog for the full list. Separately (not a review finding), drop a redundant FW-start register trigger from probe -- the firmware starts automatically on power-on. - Patch 1 (dt-bindings): drop the unused port@2 (HDMI input) graph port entirely rather than the review's suggested fix of adding it to required, and require bus-type on port@0/port@1 endpoints for "ite,it6626" specifically, since only that variant's D-PHY default can silently mismatch actual board wiring. Both found by the automated review of v3, which also flagged one other dt-bindings issue that was checked and is not being acted on -- see the patch's own changelog for the reasoning. - Link to v3: https://lore.kernel.org/r/20260722-upstream-it6625-it6626-v3-0-947e50f01764@ite.com.tw Changes in v3: - Patch 2 (driver): fix further correctness issues found by automated review of v2 -- two banked-register reads still missing a lock, a CEC RX length TOCTOU and a dropped CEC TX interrupt on invalid RX length, it6625_s_edid() treating a successful write as a failure, missing V4L2_SUBDEV_FORMAT_TRY support, an unlocked active-format commit, and detected timings lumping the whole blanking interval into hsync/vsync instead of reading the real porch registers. See the patch's own changelog for the full list, including four other v2 review findings that were checked and are not being acted on (with reasoning). - Patch 1 (dt-bindings): unchanged. - Link to v2: https://lore.kernel.org/r/20260721-upstream-it6625-it6626-v2-0-7c4f3564ca38@ite.com.tw Changes in v2: - Patch 2 (driver): fix several correctness issues found by automated review of v1 -- premature V4L2 async subdev registration/teardown ordering, a missing-lock race on banked register reads, a status-poll interval bug, a dropped CEC interrupt, an unacked CEC transmit, set_fmt mutating active state on TRY, EDID blocks=0 handling, and a couple of uninitialized-stack hardening fixes. See the patch's own changelog for the full list. - Patch 1 (dt-bindings): restrict port@0/port@1 bus-type to D-PHY (4) only for "ite,it6625", since that chip doesn't support C-PHY (also found by the automated review of v1). - Link to v1: https://lore.kernel.org/r/20260720-upstream-it6625-it6626-v1-0-475912f74459@ite.com.tw --- Hermes Wu (2): dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding media: i2c: add driver for ITE IT6625/IT6626 .../devicetree/bindings/media/i2c/ite,it6625.yaml | 169 ++ MAINTAINERS | 7 + drivers/media/i2c/Kconfig | 18 + drivers/media/i2c/Makefile | 1 + drivers/media/i2c/it6625.c | 2216 ++++++++++++++++++++ 5 files changed, 2411 insertions(+) --- base-commit: 8dac27bfa2f994ecb11f01a63641527d17d48fc1 change-id: 20260716-upstream-it6625-it6626-6874ab8a5305 Best regards, -- Hermes Wu <Hermes.wu@ite.com.tw> ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v6 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding 2026-07-22 11:18 ` Hermes Wu @ 2026-07-22 11:18 ` Hermes Wu -1 siblings, 0 replies; 8+ messages in thread From: Hermes Wu via B4 Relay @ 2026-07-22 11:18 UTC (permalink / raw) To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski, Conor Dooley Cc: linux-media, devicetree, linux-kernel, Hermes Wu From: Hermes Wu <Hermes.wu@ite.com.tw> Document the devicetree binding for the ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge. The device exposes two graph ports: port@0 (MIPI0) and port@1 (MIPI1), the two selectable CSI-2 D-PHY/C-PHY outputs. Only port@0 is required, since a board only needs to wire up as many of the bridge's outputs as it actually uses. Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw> --- Changes in v6: - Unchanged. The v5 automated review flagged that the it6626 conditional's `required: [bus-type]` constraint, expressed under `properties: { endpoint: {...} }`, doesn't match an `endpoint@N` unit-addressed node and could in principle be bypassed. Checked against mipi-ccs.yaml, an actively-maintained upstream binding that requires bus-type (among other properties) via the exact same `properties.endpoint.required` idiom with no patternProperties -- direct evidence this is accepted convention, not a defect, so no fix was made. Changes in v4: - Drop port@2 (the HDMI connector input graph port) entirely: it added no constraints beyond graph.yaml's generic endpoint-base, and the driver doesn't use this graph port for anything schema-relevant, so there was nothing for a consumer devicetree to usefully validate against it. Found by the automated review of v3, which suggested adding it to `required` instead -- dropping it entirely was judged the better fix since the port carried no real constraints to begin with. - Require bus-type on port@0/port@1 endpoints when compatible contains "ite,it6626": the driver defaults to D-PHY when bus-type is omitted, which is always correct for it6625 (D-PHY only) but would silently misconfigure an it6626 board actually wired for C-PHY. Also found by the automated review of v3. One other v3 review finding was checked and is not being acted on: - The plain `properties: { endpoint: {...} }` override used for each port technically doesn't reach graph.yaml's separate `patternProperties` matcher, so an `endpoint@0`-style node could in principle bypass the local data-lanes/bus-type constraints. This is the same convention already used by other single-endpoint-per-port bindings in-tree (e.g. thine,thp7312.yaml), not a defect unique to this binding. Changes in v2: - Restrict port@0/port@1 endpoint bus-type to D-PHY (4) only when compatible is "ite,it6625", via an allOf/if/then constraint. IT6625 only supports MIPI CSI-2 D-PHY output; the schema previously allowed both C-PHY (1) and D-PHY (4) unconditionally, so a devicetree claiming "ite,it6625" with bus-type = C-PHY passed schema validation despite being invalid on that hardware. IT6626, which supports both PHY types, is unaffected. Found by the sashiko.dev automated review of v1. --- .../devicetree/bindings/media/i2c/ite,it6625.yaml | 169 +++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fed732e4428ba0098b0a5580cfaf4b704af21a8e --- /dev/null +++ b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/media/i2c/ite,it6625.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: ITE IT6625/IT6626 HDMI to dual MIPI CSI-2 bridge + +maintainers: + - Hermes Wu <Hermes.wu@ite.com.tw> + +description: |- + The ITE IT6625 and IT6626 are HDMI to MIPI CSI-2 bridge devices. + IT6625 supports an HDMI 2.0 input and converts it to one or two D-PHY CSI + outputs, while IT6626 supports an HDMI 2.1 input and converts it to one or + two C/D-PHY CSI outputs. The bridges are programmable through I2C and + expose two selectable CSI-2 output ports. The bridge can operate in + split mode or clone mode. + +properties: + compatible: + enum: + - ite,it6625 + - ite,it6626 + + reg: + maxItems: 1 + + reset-gpios: + description: + GPIO connected to the active-low reset line. + maxItems: 1 + + ports: + $ref: /schemas/graph.yaml#/properties/ports + properties: + port@0: + $ref: /schemas/graph.yaml#/$defs/port-base + unevaluatedProperties: false + description: CSI-2 output port MIPI0 + + properties: + endpoint: + $ref: /schemas/media/video-interfaces.yaml# + unevaluatedProperties: false + + properties: + data-lanes: + minItems: 1 + maxItems: 4 + + bus-type: + enum: + - 1 # MEDIA_BUS_TYPE_CSI2_CPHY + - 4 # MEDIA_BUS_TYPE_CSI2_DPHY + + clock-noncontinuous: true + link-frequencies: true + + required: + - data-lanes + + port@1: + $ref: /schemas/graph.yaml#/$defs/port-base + unevaluatedProperties: false + description: CSI-2 output port MIPI1 + + properties: + endpoint: + $ref: /schemas/media/video-interfaces.yaml# + unevaluatedProperties: false + + properties: + data-lanes: + minItems: 1 + maxItems: 4 + + bus-type: + enum: + - 1 # MEDIA_BUS_TYPE_CSI2_CPHY + - 4 # MEDIA_BUS_TYPE_CSI2_DPHY + + clock-noncontinuous: true + link-frequencies: true + + required: + - data-lanes + + required: + - port@0 +required: + - compatible + - reg + - ports + +allOf: + - if: + properties: + compatible: + contains: + const: ite,it6625 + then: + properties: + ports: + properties: + port@0: + properties: + endpoint: + properties: + bus-type: + const: 4 + port@1: + properties: + endpoint: + properties: + bus-type: + const: 4 + + - if: + properties: + compatible: + contains: + const: ite,it6626 + then: + properties: + ports: + properties: + port@0: + properties: + endpoint: + required: + - bus-type + port@1: + properties: + endpoint: + required: + - bus-type + +additionalProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + hdmi-bridge@4c { + compatible = "ite,it6625"; + reg = <0x4c>; + + reset-gpios = <&gpio 2 GPIO_ACTIVE_LOW>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + csi_out0: endpoint { + remote-endpoint = <&csi2_rx0>; + bus-type = <4>; /* MEDIA_BUS_TYPE_CSI2_DPHY */ + data-lanes = <1 2 3 4>; + }; + }; + }; + }; + }; -- 2.34.1 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v6 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding @ 2026-07-22 11:18 ` Hermes Wu 0 siblings, 0 replies; 8+ messages in thread From: Hermes Wu @ 2026-07-22 11:18 UTC (permalink / raw) To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski, Conor Dooley Cc: linux-media, devicetree, linux-kernel, Hermes Wu Document the devicetree binding for the ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge. The device exposes two graph ports: port@0 (MIPI0) and port@1 (MIPI1), the two selectable CSI-2 D-PHY/C-PHY outputs. Only port@0 is required, since a board only needs to wire up as many of the bridge's outputs as it actually uses. Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw> --- Changes in v6: - Unchanged. The v5 automated review flagged that the it6626 conditional's `required: [bus-type]` constraint, expressed under `properties: { endpoint: {...} }`, doesn't match an `endpoint@N` unit-addressed node and could in principle be bypassed. Checked against mipi-ccs.yaml, an actively-maintained upstream binding that requires bus-type (among other properties) via the exact same `properties.endpoint.required` idiom with no patternProperties -- direct evidence this is accepted convention, not a defect, so no fix was made. Changes in v4: - Drop port@2 (the HDMI connector input graph port) entirely: it added no constraints beyond graph.yaml's generic endpoint-base, and the driver doesn't use this graph port for anything schema-relevant, so there was nothing for a consumer devicetree to usefully validate against it. Found by the automated review of v3, which suggested adding it to `required` instead -- dropping it entirely was judged the better fix since the port carried no real constraints to begin with. - Require bus-type on port@0/port@1 endpoints when compatible contains "ite,it6626": the driver defaults to D-PHY when bus-type is omitted, which is always correct for it6625 (D-PHY only) but would silently misconfigure an it6626 board actually wired for C-PHY. Also found by the automated review of v3. One other v3 review finding was checked and is not being acted on: - The plain `properties: { endpoint: {...} }` override used for each port technically doesn't reach graph.yaml's separate `patternProperties` matcher, so an `endpoint@0`-style node could in principle bypass the local data-lanes/bus-type constraints. This is the same convention already used by other single-endpoint-per-port bindings in-tree (e.g. thine,thp7312.yaml), not a defect unique to this binding. Changes in v2: - Restrict port@0/port@1 endpoint bus-type to D-PHY (4) only when compatible is "ite,it6625", via an allOf/if/then constraint. IT6625 only supports MIPI CSI-2 D-PHY output; the schema previously allowed both C-PHY (1) and D-PHY (4) unconditionally, so a devicetree claiming "ite,it6625" with bus-type = C-PHY passed schema validation despite being invalid on that hardware. IT6626, which supports both PHY types, is unaffected. Found by the sashiko.dev automated review of v1. --- .../devicetree/bindings/media/i2c/ite,it6625.yaml | 169 +++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fed732e4428ba0098b0a5580cfaf4b704af21a8e --- /dev/null +++ b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/media/i2c/ite,it6625.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: ITE IT6625/IT6626 HDMI to dual MIPI CSI-2 bridge + +maintainers: + - Hermes Wu <Hermes.wu@ite.com.tw> + +description: |- + The ITE IT6625 and IT6626 are HDMI to MIPI CSI-2 bridge devices. + IT6625 supports an HDMI 2.0 input and converts it to one or two D-PHY CSI + outputs, while IT6626 supports an HDMI 2.1 input and converts it to one or + two C/D-PHY CSI outputs. The bridges are programmable through I2C and + expose two selectable CSI-2 output ports. The bridge can operate in + split mode or clone mode. + +properties: + compatible: + enum: + - ite,it6625 + - ite,it6626 + + reg: + maxItems: 1 + + reset-gpios: + description: + GPIO connected to the active-low reset line. + maxItems: 1 + + ports: + $ref: /schemas/graph.yaml#/properties/ports + properties: + port@0: + $ref: /schemas/graph.yaml#/$defs/port-base + unevaluatedProperties: false + description: CSI-2 output port MIPI0 + + properties: + endpoint: + $ref: /schemas/media/video-interfaces.yaml# + unevaluatedProperties: false + + properties: + data-lanes: + minItems: 1 + maxItems: 4 + + bus-type: + enum: + - 1 # MEDIA_BUS_TYPE_CSI2_CPHY + - 4 # MEDIA_BUS_TYPE_CSI2_DPHY + + clock-noncontinuous: true + link-frequencies: true + + required: + - data-lanes + + port@1: + $ref: /schemas/graph.yaml#/$defs/port-base + unevaluatedProperties: false + description: CSI-2 output port MIPI1 + + properties: + endpoint: + $ref: /schemas/media/video-interfaces.yaml# + unevaluatedProperties: false + + properties: + data-lanes: + minItems: 1 + maxItems: 4 + + bus-type: + enum: + - 1 # MEDIA_BUS_TYPE_CSI2_CPHY + - 4 # MEDIA_BUS_TYPE_CSI2_DPHY + + clock-noncontinuous: true + link-frequencies: true + + required: + - data-lanes + + required: + - port@0 +required: + - compatible + - reg + - ports + +allOf: + - if: + properties: + compatible: + contains: + const: ite,it6625 + then: + properties: + ports: + properties: + port@0: + properties: + endpoint: + properties: + bus-type: + const: 4 + port@1: + properties: + endpoint: + properties: + bus-type: + const: 4 + + - if: + properties: + compatible: + contains: + const: ite,it6626 + then: + properties: + ports: + properties: + port@0: + properties: + endpoint: + required: + - bus-type + port@1: + properties: + endpoint: + required: + - bus-type + +additionalProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + hdmi-bridge@4c { + compatible = "ite,it6625"; + reg = <0x4c>; + + reset-gpios = <&gpio 2 GPIO_ACTIVE_LOW>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + csi_out0: endpoint { + remote-endpoint = <&csi2_rx0>; + bus-type = <4>; /* MEDIA_BUS_TYPE_CSI2_DPHY */ + data-lanes = <1 2 3 4>; + }; + }; + }; + }; + }; -- 2.34.1 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v6 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding 2026-07-22 11:18 ` Hermes Wu (?) @ 2026-07-22 11:25 ` sashiko-bot -1 siblings, 0 replies; 8+ messages in thread From: sashiko-bot @ 2026-07-22 11:25 UTC (permalink / raw) To: Hermes Wu; +Cc: robh, devicetree, media-ci, conor+dt Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] The device tree binding intentionally omits the HDMI input port, violating the fundamental rule that Device Tree describes the physical hardware independent of driver implementation. -- --- Patch [1]: [PATCH v6 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding --- commit e9e6db2f5d5e1b4ac21c4df62354e0ec71117d3a Author: Hermes Wu <Hermes.wu@ite.com.tw> dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding This commit documents the devicetree binding for the ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge. It exposes two selectable CSI-2 output graph ports, port@0 and port@1. > diff --git a/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml > new file mode 100644 > index 0000000000000..fed732e4428ba > --- /dev/null > +++ b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml [ ... ] > + ports: > + $ref: /schemas/graph.yaml#/properties/ports > + properties: > + port@0: > + $ref: /schemas/graph.yaml#/$defs/port-base > + unevaluatedProperties: false > + description: CSI-2 output port MIPI0 [Severity: Medium] The commit message mentions dropping the HDMI input graph port because the driver does not use it. Does omitting this input port conflict with the principle that devicetree schemas should describe the physical hardware independent of driver implementation? Because the physical hardware acts as an HDMI bridge, device tree authors would need a documented port index under properties.ports to accurately link an upstream HDMI source to the bridge. Without an explicitly defined input port, will this prevent boards from accurately describing their video graph topology, potentially leading to inconsistent or missing routing connections across different devices? -- Sashiko AI review · https://sashiko.dev/#/patchset/20260722-upstream-it6625-it6626-v6-0-6117d4d96574@ite.com.tw?part=1 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v6 2/2] media: i2c: add driver for ITE IT6625/IT6626 2026-07-22 11:18 ` Hermes Wu @ 2026-07-22 11:18 ` Hermes Wu -1 siblings, 0 replies; 8+ messages in thread From: Hermes Wu via B4 Relay @ 2026-07-22 11:18 UTC (permalink / raw) To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski, Conor Dooley Cc: linux-media, devicetree, linux-kernel, Hermes Wu From: Hermes Wu <Hermes.wu@ite.com.tw> Add a V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI-to-MIPI CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an HDMI 2.1 input, converting it to a D-PHY (or C/D-PHY on IT6626) MIPI CSI-2 output. The bridge is configured over I2C, exposes an HDMI CEC adapter, and supports EDID read/write, DV timings detection/configuration, and HPD control via the standard V4L2 subdevice pad and video ops. Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw> --- Changes in v6: - Fix two correctness issues found by the automated review of v5: it6625_s_edid() called v4l2_phys_addr_validate() but discarded its return value, so a CEC physical address that failed topology validation (e.g. a gap like 1.0.1.0) still reached cec_s_phys_addr() and got broadcast on the CEC bus instead of being rejected; and it6625_parse_endpoint() pre-set the fwnode endpoint's bus_type to D-PHY before parsing, which (per include/media/v4l2-fwnode.h) makes the parser treat it as a hard requirement and reject any endpoint whose DT bus-type disagrees -- every IT6626 C-PHY device tree failed to probe as a result. One other v5 review finding was checked and is not being acted on: - A concurrent VIDIOC_SUBDEV_S_EDID ioctl racing it6625_remove()'s cancel_delayed_work_sync() could in principle schedule hpd_delayed_work after teardown frees the driver state. Real in the abstract -- no lock serializes an in-flight ioctl against video_unregister_device() -- but the identical unlocked pattern (delayed work scheduled from the s_edid path, remove() doing only cancel_delayed_work_sync()) exists unfixed in tc358743.c and adv7604.c, and this is the same residual risk already dismissed in the v2/v3/v4 rounds of this series for the same reason: no peer driver adds extra synchronization for it either. Changes in v5: - Fix a kernel-stack-memory leak in it6625_edid_ram_show() and it6625_mipi_reg_show(): both printed an uninitialized on-stack register buffer to userspace via debugfs without checking whether the preceding I2C bulk read actually succeeded. - Fix two related format-negotiation bugs: it6625_set_fmt()'s V4L2_SUBDEV_FORMAT_TRY branch never updated fmt->colorspace to match the new fmt->code, and it6625_get_fmt()'s TRY branch computed the returned colorspace from the active hardware state instead of the TRY state's own format, leaking active state into what should be an isolated TRY buffer -- store and read code and colorspace together in sd_state instead. Separately, it6625_set_fmt()'s active-format path released it6625_lock before programming the hardware, letting two concurrent S_FMT calls interleave and leave the hardware programmed for the wrong thread's format; add _locked variants of it6625_enable_stream()/it6625_set_mipi_config() and call them from inside set_fmt()'s existing lock scope so the whole update is now atomic. - Fix it6625_s_edid(): per the VIDIOC_S_EDID spec, blocks == 0 must leave the EDID disabled ("no longer available"), but the driver disabled HPD and then fell through to the normal completion path, which unconditionally re-enabled it -- return immediately instead. Also stop a failed it6625_write_edid() from skipping the HPD re-enable entirely, which left HPD disabled permanently after any single failed S_EDID call. - Take it6625_lock around it6625_mipi_reg_write()'s I2C write, matching every other register access in the driver. - Derive IT6625_CID_HDMI_INFO's string from real state (it6625->csi_format and fps_from_bt_timings()) instead of returning a hardcoded "RGB444 10bit @ 60Hz" regardless of actual signal. - Wire up the previously-unused reset-gpios line in probe(): acquire it via devm_gpiod_get_optional() and toggle it (assert briefly, then deassert and let it settle) before the first I2C access. All found by the sashiko.dev automated review of v4. Three other v4 review findings were checked and are not being acted on: - it6625_mipi_reg_show()'s raw register dump could destructively consume a pending CEC RX message via REG_CEC_RX_DATA (0x20) -- checked with the hardware author: reading REG_CEC_RX_DATA is not destructive, only the explicit write-0 to REG_CEC_RX_DATA_LEN clears state. The v4 changelog's "likely clear-on-read" assumption for this same register was wrong. - devm_kzalloc'd state + subdev devnode UAF-in-the-abstract on unbind -- same reasoning as the v2/v3 rounds: matches tc358743.c/adv7604.c/ adv7842.c exactly, an accepted V4L2-subsystem-wide pattern, not a defect unique to this driver. - Concurrent VIDIOC_SUBDEV_S_EDID racing it6625_remove()'s cancel_delayed_work_sync()/mutex_destroy() -- the same residual risk dismissed in the v3 round: real in the abstract, but no peer driver adds extra synchronization for it either, and the v2 round's fix (unregister the subdev first in remove()) already narrowed this to the same residual risk level every peer driver carries. Changes in v4: - Always acknowledge the CEC RX length register once the RX interrupt branch is entered, even when the reported length is invalid -- previously the ack write only happened on valid lengths, so an invalid length left the interrupt asserted, which could storm the IRQ line. - Fall back to a default format instead of returning -EINVAL from it6625_set_fmt() when the requested mbus code isn't recognized, matching the V4L2 subdev set_fmt convention (cf. adv7604.c). Take it6625_lock around it6625_get_fmt()'s ACTIVE-path reads of csi_format/mbus_fmt_code, which could otherwise observe a torn update from a concurrent set_fmt(). - Fix EDID ioctl spec conformance: it6625_g_edid() now clamps the requested block count and returns success instead of propagating an error when the request exceeds capacity; it6625_s_edid() now returns -E2BIG (not -EINVAL) with blocks set to the maximum, without attempting the write, when the request exceeds capacity; and it6625_read_edid()/it6625_write_edid()'s low-level bounds check now tests start_block and num_blocks individually before summing them, since the sum could otherwise overflow given the unsigned __u32 fields of struct v4l2_edid. - Harden the mipi_reg debugfs handlers: it6625_mipi_reg_write() now always parses from a local offset that starts at 0 instead of the real file offset, since this is a stateless one-write-one-command interface, not a seekable stream; it6625_mipi_reg_show() now takes it6625_lock around its raw register-bank read, matching every other banked-register access in the driver (the dumped range also includes REG_CEC_RX_DATA, which the CEC handler treats as clear-on-read, so an unlocked read could silently steal a pending CEC RX message). - Mark the HDMI info control (IT6625_CID_HDMI_INFO) volatile: it6625_g_volatile_ctrl() already had a live case for it, but the control's v4l2_ctrl_config was missing V4L2_CTRL_FLAG_VOLATILE, so that case was never actually reached and the control's value was never refreshed. All found by the sashiko.dev automated review of v3. Separately, not a review finding: remove a redundant FW-start register trigger from it6625_initial_setup() -- the chip's firmware starts automatically on power-on, so the explicit software trigger was dead weight. Changes in v3: - Finish the banked-register locking from v2: it6625_irq_infoframe_latch() and get_audio_sampling_rate() also read banked registers without it6625_lock; take the lock inside the CEC RX branch across the length read, data read and length-clear together (closing a TOCTOU where the length could go stale under lock contention), and stop the RX branch from using an early return that skipped a simultaneous CEC TX interrupt. - Fix it6625_s_edid() incorrectly treating a successful EDID write as a failure: it6625_write_edid() returns a positive block count on success, not 0, so `if (err)` skipped the CEC phys-addr update and HPD re-enable on every successful write. Pre-existing since v1, not introduced by the v2 blocks==0 handling. - Add real V4L2_SUBDEV_FORMAT_TRY support in get_fmt/set_fmt via sd_state, matching the pattern used by adv7604.c, and lock the csi_format/mbus_fmt_code commit in the ACTIVE path against concurrent S_FMT calls. - Read the chip's real porch/sync-width registers (REG_H_FP_1..REG_V_BP_0) in it6625_get_detected_timings() instead of lumping the whole blanking interval into hsync/vsync, so detected timings can actually match the standard CEA/VESA tables. All found by the sashiko.dev automated review of v2. Four other v2 review findings were checked against the code and are not being acted on: - "timer_container_of() doesn't exist" -- it does (include/linux/timer.h), the driver already builds clean with it. - "devm_kzalloc'd state + subdev devnode risks a UAF on unbind" -- true in the abstract, but it's the exact pattern used by every comparable in-tree driver (tc358743.c, adv7604.c, adv7842.c all devm_kzalloc their main state struct despite setting V4L2_SUBDEV_FL_HAS_DEVNODE too). - "unregistering the subdev before disabling IRQ/work in remove() leaves sd->devnode dangling" -- checked the core: v4l2_device_unregister_subdev() only marks sd->devnode unregistered via video_unregister_device(), it does not free it; the struct is freed later via kref on last close, so the pointer stays valid. - the residual "ioctl reschedules hpd_delayed_work concurrently with remove()" race -- real in the abstract (requires an ioctl already in flight at the exact moment of unbind), but no peer driver adds extra synchronization for this either (none set sd->devnode->lock), and the v2 fix (unregister first in remove()) already narrowed this down to the same residual risk level every peer driver carries. Changes in v2: - Fix premature v4l2_async_register_subdev(): move it to the end of probe() (after ctrl handler, CEC and initial hardware setup), and mirror the unregister order at the top of remove(), closing a use-after-free window where a concurrent ioctl could reschedule hpd_delayed_work during teardown. - Take it6625_lock around the banked register reads in it6625_get_detected_timings() and the CEC RX path in it6625_cec_handler(), which could otherwise race with an in-progress EDID read/write and observe the wrong bank. - Fix it6625_wait_for_status()'s poll interval exceeding the timeout budget, split the CEC RX/TX interrupt handling out of an else-if so simultaneous events aren't dropped, and wait for hardware to latch the CEC transmit trigger bit before releasing the lock. - Stop it6625_set_fmt() from mutating active state on V4L2_SUBDEV_FORMAT_TRY, and handle edid->blocks == 0 in g_edid/s_edid per the V4L2 spec (capacity query / EDID clear). - Zero-initialize the CEC rxmsg and debugfs mipi_reg write buffer to avoid leaking uninitialized stack bytes, and use designated initializers in the i2c_device_id table. All found by the sashiko.dev automated review of v1. --- MAINTAINERS | 7 + drivers/media/i2c/Kconfig | 18 + drivers/media/i2c/Makefile | 1 + drivers/media/i2c/it6625.c | 2216 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 2242 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 15011f5752a994cf1b354f490d6c4e411588df88..2d5b0ee107f9e3255a5365ec0a25167a716b1e6e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13847,6 +13847,13 @@ T: git https://gitlab.freedesktop.org/drm/misc/kernel.git F: Documentation/devicetree/bindings/display/bridge/ite,it66121.yaml F: drivers/gpu/drm/bridge/ite-it66121.c +ITE IT6625 HDMI to MIPI MEDIA DRIVER +M: Hermes Wu <Hermes.Wu@ite.com.tw> +S: Maintained +T: git git://linuxtv.org/media.git +F: Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml +F: drivers/media/i2c/it6625.c + IVTV VIDEO4LINUX DRIVER M: Andy Walls <awalls@md.metrocast.net> L: linux-media@vger.kernel.org diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig index 5d173e0ecf424f2f204f8d426be818e44357f8e4..fb4b24684f7710f90eb399976d11f57701488d26 100644 --- a/drivers/media/i2c/Kconfig +++ b/drivers/media/i2c/Kconfig @@ -1278,6 +1278,24 @@ config VIDEO_ISL7998X Support for Intersil ISL7998x analog to MIPI-CSI2 or BT.656 decoder. +config VIDEO_IT6625 + tristate "IT6625 HDMI to MIPI CSI bridge" + depends on VIDEO_DEV && I2C + depends on OF + select CEC_CORE + select MEDIA_CONTROLLER + select REGMAP_I2C + select V4L2_FWNODE + select VIDEO_V4L2_SUBDEV_API + help + V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI to MIPI + CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and + IT6626 an HDMI 2.1 input, converting it to a MIPI CSI-2 + output. The driver also exposes an HDMI CEC adapter. + + To compile this driver as a module, choose M here: the + module will be called it6625. + config VIDEO_LT6911UXE tristate "Lontium LT6911UXE decoder" depends on ACPI && VIDEO_DEV && I2C diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile index e45359efe0e41e13e3c0869e5ead7d6cf4aca3a7..9bbceaa3d07573958edb8a36bc7f0942f1410a39 100644 --- a/drivers/media/i2c/Makefile +++ b/drivers/media/i2c/Makefile @@ -63,6 +63,7 @@ obj-$(CONFIG_VIDEO_IMX412) += imx412.o obj-$(CONFIG_VIDEO_IMX415) += imx415.o obj-$(CONFIG_VIDEO_IR_I2C) += ir-kbd-i2c.o obj-$(CONFIG_VIDEO_ISL7998X) += isl7998x.o +obj-$(CONFIG_VIDEO_IT6625) += it6625.o obj-$(CONFIG_VIDEO_KS0127) += ks0127.o obj-$(CONFIG_VIDEO_LM3560) += lm3560.o obj-$(CONFIG_VIDEO_LM3646) += lm3646.o diff --git a/drivers/media/i2c/it6625.c b/drivers/media/i2c/it6625.c new file mode 100644 index 0000000000000000000000000000000000000000..a14d36179f426090f1f00d512d1afbef622062e3 --- /dev/null +++ b/drivers/media/i2c/it6625.c @@ -0,0 +1,2216 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * it6625 - ite HDMI to MIPI bridge + */ +#include <linux/bitfield.h> +#include <linux/clk.h> +#include <linux/debugfs.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/hdmi.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_graph.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/timer.h> +#include <linux/v4l2-dv-timings.h> +#include <linux/videodev2.h> +#include <linux/workqueue.h> + +#include <media/cec.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-device.h> +#include <media/v4l2-dv-timings.h> +#include <media/v4l2-event.h> +#include <media/v4l2-fwnode.h> + +static int debug = 3; +module_param(debug, int, 0644); +MODULE_PARM_DESC(debug, "debug level (0-3)"); + +#define REG_CHIP_ID_0 0x00 +#define REG_CHIP_ID_1 0x01 +#define REG_FW_VER_MAJOR 0x03 +#define REG_FW_VER_MINOR 0x04 +#define REG_PROTOCOL_VERSION 0x05 +#define B_PVER_MINOR BIT(0) +#define B_PVER_MAJOR BIT(4) + +#define REG_CMD_SET 0x10 +#define CMD_SET_CEC_LA 0xC0 +#define CMD_SET_CEC_ENABLE 0xC1 + +#define REG_EDID_START 0x20 +#define REG_CEC_RX_DATA 0x20 +#define REG_CEC_TX_DATA 0x30 + +#define REG_H_ACTIVE_1 0x50 +#define REG_H_ACTIVE_0 0x51 +#define REG_V_ACTIVE_1 0x52 +#define REG_V_ACTIVE_0 0x53 +#define REG_H_TOTAL_1 0x54 +#define REG_H_TOTAL_0 0x55 +#define REG_V_TOTAL_1 0x56 +#define REG_V_TOTAL_0 0x57 +#define REG_VID_PCLK 0x58 +#define REG_H_FP_1 0x5C +#define REG_H_FP_0 0x5D +#define REG_H_SW_1 0x5E +#define REG_H_SW_0 0x5F +#define REG_H_BP_1 0x60 +#define REG_H_BP_0 0x61 +#define REG_V_FP_1 0x62 +#define REG_V_FP_0 0x63 +#define REG_V_SW_1 0x64 +#define REG_V_SW_0 0x65 +#define REG_V_BP_1 0x66 +#define REG_V_BP_0 0x67 +#define REG_VID_INFO 0x68 +#define B_INTERLACE BIT(0) +#define B_HSWPOL BIT(1) +#define B_VSWPOL BIT(2) +#define B_PIXEL_REP BIT(4) + +#define REG_VIC 0x69 +#define REG_HDMI_VIDEO_INFO 0x6A +#define B_COLOR_MODE BIT(0) +#define B_COLOR_DEPTH BIT(4) +#define REG_HDMI_AUDIO_INFO1 0x6B +#define B_AUD_FS BIT(0) + +#define REG_HDMI_AUDIO_INFO2 0x6C +#define B_AUD_CH BIT(0) +#define B_AUD_WL BIT(4) + +#define REG_HDMI_AUDIO_INFO3 0x6D +#define B_AUD_TYPE BIT(0) +#define B_AUD_MS BIT(2) +#define B_AUD_3D BIT(3) + +#define REG_AUDIO_FMT 0x6E +#define B_I2S_WL BIT(0) +#define B_I2S_ALN BIT(2) +#define B_I2S_DLY BIT(3) +#define B_I2S_LR BIT(4) +#define B_I2S_SFT BIT(5) +#define B_AUD_OUT_IF BIT(6) + +#define REG_IF_LATCH_HB 0x6F +#define REG_IF_DATA 0x70 +#define REG_EMP_DATA 0xA0 +#define REG_AVI_DATA 0xAE + +#define REG_TX_STATUS 0xE0 + +#define REG_RX_STATUS 0xE2 +#define B_RX_5V BIT(0) +#define B_RX_HPD BIT(1) +#define B_RX_STABLE BIT(2) +#define B_RX_HDMI BIT(3) +#define B_RX_AVMUTE BIT(4) +#define B_RX_AUD_ON BIT(5) + +#define REG_RX_HDCP_STS 0xE3 +#define B_HDCP1_AUTH_START BIT(0) +#define B_HDCP2_AUTH_START BIT(1) +#define B_HDCP_AUTH_DONE BIT(2) +#define B_HDCP_ENC BIT(3) + +#define REG_CEC_STATUS 0xE4 +#define B_CEC_TX_DONE BIT(0) +#define B_CEC_TX_NACK BIT(1) + +#define REG_CEC_RX_DATA_LEN 0xE5 +#define REG_CEC_TX_DATA_LEN 0xE6 + +#define REG_SYS_MIPI_INT 0xEB +#define B_MIPI_OUTPUT_ENABLE BIT(0) +#define B_MIPI_VIDEO_UNSTABLE BIT(1) + +#define REG_RX_INT_STATUS1 0xEC +#define B_HDMI_5V_CHG BIT(0) +#define B_HDMI_VID_CHG BIT(1) +#define B_HDMI_AUD_CHG BIT(2) +#define B_HDMI_CP_CHG BIT(3) +#define B_HDMI_IF_LATCH BIT(4) +#define B_HDMI_NO_IF_LATCH BIT(5) +#define B_HDMI_EMP BIT(6) +#define B_HDMI_NO_EMP BIT(7) + +#define REG_RX_INT_STATUS2 0xED +#define B_HDMI_AVI BIT(0) +#define B_HDMI_NO_AVI BIT(1) +#define B_HDMI_VSIF BIT(2) +#define B_HDMI_NO_VSIF BIT(3) +#define B_HDMI_AVMUTE_CHG BIT(4) + +#define REG_CHIP_CONTROL 0xF0 +#define B_HDMI_RESET BIT(5) +#define B_FW_START BIT(6) + +#define REG_MIPI_CFG 0xF1 +#define M_MIPI_LANE 0x03 +#define B_MIPI_SPLIT BIT(2) +#define B_MIPI_SPLIT_CFG BIT(3) +#define B_MIPI_DPHY BIT(4) +#define B_MIPI_USE_DSI BIT(5) +#define B_MIPI_CONTINU_CLK BIT(6) + +#define REG_MIPI_DATA_TYPE 0xF2 +#define CSI_RGB444 0x20 +#define CSI_RGB555 0x21 +#define CSI_RGB565 0x22 +#define CSI_RGB666 0x23 +#define CSI_RGB888 0x24 +#define CSI_YUV420_8b_L 0x1A +#define CSI_YUV420_8b 0x1C +#define CSI_YUV420_10b 0x1D +#define CSI_YUV422_8b 0x1E +#define CSI_YUV422_10b 0x1F +#define CSI_RGB_10b 0x30 +#define CSI_RGB_12b 0x31 +#define CSI_YUV422_12b 0x32 +#define CSI_YUV420_10b_L 0x33 +#define CSI_YUV420_12b 0x34 +#define CSI_YUV444_8b 0x35 +#define CSI_YUV444_10b 0x36 +#define CSI_YUV444_12b 0x37 + +#define REG_MIPI_CONTROL 0xF3 +#define B_MIPI_OUTPUT BIT(0) + +#define REG_RX_CFG 0xF4 +#define B_MANUAL_HPD BIT(0) +#define B_HPD_HIGH BIT(1) +#define B_HPD_TOGGLE BIT(3) + +#define REG_CSC_CFG 0xF5 +#define B_DYNAMIC_RANGE BIT(0) + +#define REG_MISC_CFG 0xF6 +#define B_BAUD_RATE BIT(0) +#define B_DEBUG_MSG BIT(1) + +#define REG_HPD_DELAY 0xF7 +#define B_DELAY_COUNT BIT(0) +#define B_DELAY_UNIT BIT(7) + +#define REG_INFO_BANK_SEL 0xFD +#define CTL_BANK_EDID_READ 1 +#define CTL_BANK_EDID_WRITE 5 + +#define REG_HOST_CTRL_INT 0xFE +#define B_CMD_SET BIT(4) +#define B_CEC_SEND_DATA BIT(5) +#define B_CONFIG_UPDATE BIT(6) +#define B_IF_BANK BIT(7) + +#define REG_MCU_INTERRUPT 0xFF +#define B_SYS_INT_ACTIVE BIT(0) +#define B_CEC_RX_RECEIVED BIT(1) +#define B_CEC_TX_UPDATE BIT(2) + +#define EDID_NUM_BLOCKS_MAX 4 +#define EDID_BLOCK_SIZE 128 + +#define I2C_MAX_XFER_SIZE 8 +#define POLL_INTERVAL_CEC_MS 10 +#define POLL_INTERVAL_MS 40 + +#define AUD32K 0x03 +#define AUD44K 0x00 +#define AUD48K 0x02 +#define AUD64K 0x0B +#define AUD88K 0x08 +#define AUD96K 0x0A +#define AUD128K 0x2B +#define AUD176K 0x0C +#define AUD192K 0x0E +#define AUD256K 0x1B +#define AUD352K 0x0D +#define AUD384K 0x05 +#define AUD512K 0x3B +#define AUD705K 0x2D +#define AUD768K 0x09 +#define AUD1024K 0x35 +#define AUD1411K 0x1D +#define AUD1536K 0x15 + +enum it6625_chip_type { + IT6625_CHIP = 0, + IT6626_CHIP = 1, +}; + +struct it6625 { + struct device *dev; + struct i2c_client *i2c_client; + struct regmap *it6625_regmap; + enum it6625_chip_type chip_type; + + /* protects concurrent access to the chip's registers and state */ + struct mutex it6625_lock; + + struct v4l2_subdev sd; + struct v4l2_mbus_config_mipi_csi2 bus; + struct video_device *vdev; + struct media_pad pad; + struct v4l2_ctrl_handler hdl; + + /* controls */ + struct v4l2_ctrl *ctrl_5v_detect; + struct v4l2_ctrl *ctrl_audio_sampling_rate; + struct v4l2_ctrl *ctrl_audio_present; + struct v4l2_ctrl *ctrl_link_freq; + struct v4l2_ctrl *ctrl_get_hdmi_info; + struct v4l2_ctrl *ctrl_mipi_config; + + struct delayed_work hpd_delayed_work; + + struct timer_list timer; + struct work_struct polling_work; + + struct v4l2_dv_timings timings; + + u8 csi_lanes; + u8 port_num; + enum v4l2_mbus_type bus_type; + u8 csi_format; + u32 mbus_fmt_code; + + struct gpio_desc *reset_gpio; + + struct cec_adapter *cec_adap; + + struct dentry *debugfs_dir; +}; + +static const s64 it6625_link_freq[] = { 445500000 }; + +static const struct v4l2_dv_timings_cap it6625_timings_cap = { + .type = V4L2_DV_BT_656_1120, + /* keep this initialization for compatibility with GCC < 4.4.6 */ + .reserved = { 0 }, + + V4L2_INIT_BT_TIMINGS(640, 3840, 480, 2160, 27000000, 300000000, + V4L2_DV_BT_STD_CEA861 | V4L2_DV_BT_STD_DMT | + V4L2_DV_BT_STD_GTF | V4L2_DV_BT_STD_CVT, + V4L2_DV_BT_CAP_PROGRESSIVE | V4L2_DV_BT_CAP_INTERLACED | + V4L2_DV_BT_CAP_REDUCED_BLANKING | V4L2_DV_BT_CAP_CUSTOM) +}; + +static const struct it6625_format_info { + u8 csi_format; + u32 mbus_fmt_code; +} it6625_formats[] = { + { CSI_YUV422_8b, MEDIA_BUS_FMT_UYVY8_1X16 }, + { CSI_RGB888, MEDIA_BUS_FMT_RGB888_1X24 }, + { CSI_YUV444_8b, MEDIA_BUS_FMT_YUV8_1X24 }, +}; + +static inline int it6625_csi_format_idx(u8 csi_format) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(it6625_formats); i++) { + if (it6625_formats[i].csi_format == csi_format) + return i; + } + + return -EINVAL; +} + +static inline int it6625_csi_mbus_code_idx(u32 mbus_fmt_code) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(it6625_formats); i++) { + if (it6625_formats[i].mbus_fmt_code == mbus_fmt_code) + return i; + } + + return -EINVAL; +} + +static inline struct it6625 *sd_to_6625(struct v4l2_subdev *sd) +{ + return container_of(sd, struct it6625, sd); +} + +static inline struct it6625 *hdl_to_6625(struct v4l2_ctrl_handler *hdl) +{ + return container_of(hdl, struct it6625, hdl); +} + +static const struct regmap_config it6625_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0xff, + .cache_type = REGCACHE_NONE, + .max_raw_read = I2C_MAX_XFER_SIZE, + .max_raw_write = I2C_MAX_XFER_SIZE, +}; + +static int it6625_regmap_i2c_init(struct i2c_client *client, + struct it6625 *it6625) +{ + it6625->i2c_client = client; + it6625->dev = &client->dev; + + it6625->it6625_regmap = devm_regmap_init_i2c(it6625->i2c_client, + &it6625_regmap_config); + if (IS_ERR(it6625->it6625_regmap)) + return PTR_ERR(it6625->it6625_regmap); + + return 0; +} + +static int it6625_read_byte(struct it6625 *it6625, u8 reg) +{ + unsigned int val; + int err; + struct device *dev = it6625->dev; + + err = regmap_read(it6625->it6625_regmap, reg, &val); + if (err < 0) { + dev_err(dev, "reg[0x%x] read failed err: %d", reg, err); + return err; + } + + return val; +} + +static int it6625_write_byte(struct it6625 *it6625, u8 reg, u8 val) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_write(it6625->it6625_regmap, reg, val); + if (err < 0) { + dev_err(dev, "reg[0x%x] write failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_set_bits(struct it6625 *it6625, u8 reg, u8 mask, u8 val) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_update_bits(it6625->it6625_regmap, reg, mask, val); + if (err < 0) { + dev_err(dev, "reg[0x%x] set bits failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_read_bytes(struct it6625 *it6625, u8 reg, u8 *buf, int len) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_bulk_read(it6625->it6625_regmap, reg, buf, len); + if (err < 0) { + dev_err(dev, "reg[0x%x] read failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_write_bytes(struct it6625 *it6625, u8 reg, u8 *buf, int len) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_bulk_write(it6625->it6625_regmap, reg, buf, len); + if (err < 0) { + dev_err(dev, "reg[0x%x] write failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_wait_for_status(struct it6625 *it6625, u8 reg, u8 val, + int timeout_ms) +{ + struct device *dev = it6625->dev; + int status; + int rval; + int sleep_ms = 10; + int timeout_round_ms = DIV_ROUND_UP(timeout_ms, sleep_ms) * sleep_ms; + + status = read_poll_timeout(it6625_read_byte, rval, rval == val, + sleep_ms * 1000, + timeout_round_ms * 1000, + false, it6625, reg); + + dev_info(dev, "%s status = %d %d", __func__, status, (int)rval); + if (status < 0) { + dev_err(dev, "%s err status = %d", __func__, status); + return -ETIMEDOUT; + } + + return 0; +} + +static void it6625_write_command(struct it6625 *it6625, u8 *cmds, int cmd_len) +{ + it6625_write_bytes(it6625, REG_CMD_SET, cmds, cmd_len); + it6625_write_byte(it6625, REG_HOST_CTRL_INT, B_CMD_SET); + it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static int it6625_update_config(struct it6625 *it6625) +{ + int err; + + err = it6625_set_bits(it6625, REG_HOST_CTRL_INT, + B_CONFIG_UPDATE, B_CONFIG_UPDATE); + if (err < 0) + return err; + + return it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static int it6625_set_bank(struct it6625 *it6625, u8 bank) +{ + int err; + + err = it6625_write_byte(it6625, REG_INFO_BANK_SEL, bank); + if (err < 0) + return err; + + err = it6625_write_byte(it6625, REG_HOST_CTRL_INT, B_IF_BANK); + if (err < 0) + return err; + + return it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static inline bool is_hdmi(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? false : (val & B_RX_HDMI); +} + +static inline bool hdmi_5v_power_present(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? false : (val & B_RX_5V); +} + +static inline bool no_signal(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? true : !(val & B_RX_STABLE); +} + +static inline bool audio_present(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? false : (val & B_RX_AUD_ON); +} + +static int get_audio_sampling_rate(struct it6625 *it6625) +{ + u8 fs_id; + int i, freq = 0; + const struct fs_id_map { + u8 fs_id; + u16 freq; + } s_fsid_map[] = { + { AUD32K, 32 }, + { AUD44K, 44 }, + { AUD48K, 48 }, + { AUD64K, 64 }, + { AUD88K, 88 }, + { AUD96K, 96 }, + + { AUD128K, 128 }, + { AUD176K, 176 }, + { AUD192K, 192 }, + { AUD256K, 256 }, + { AUD352K, 352 }, + { AUD384K, 384 }, + + { AUD512K, 512 }, + { AUD705K, 705 }, + { AUD768K, 768 }, + { AUD1024K, 1024 }, + { AUD1411K, 1411 }, + { AUD1536K, 1536 }, + }; + + if (no_signal(it6625) || !audio_present(it6625)) + return 0; + + guard(mutex)(&it6625->it6625_lock); + + fs_id = it6625_read_byte(it6625, REG_HDMI_AUDIO_INFO1); + + for (i = 0; i < ARRAY_SIZE(s_fsid_map); i++) { + if (s_fsid_map[i].fs_id == fs_id) { + freq = s_fsid_map[i].freq * 1000; + break; + } + } + + return freq; +} + +static u64 it6625_get_pclk(struct it6625 *it6625) +{ + u32 pclk; + u8 ck[4]; + int ret; + + ret = it6625_read_bytes(it6625, REG_VID_PCLK, ck, 4); + if (ret < 0) { + dev_err(it6625->dev, "failed to read pixel clock"); + return 0; + } + + pclk = ck[0]; + pclk <<= 8; + pclk |= ck[1]; + pclk <<= 8; + pclk |= ck[2]; + pclk <<= 8; + pclk |= ck[3]; + + v4l2_dbg(1, debug, &it6625->sd, "%s: pclk=%u (%08x)", + __func__, pclk, pclk); + + return (u64)pclk * 1000; +} + +static int it6625_read_edid(struct it6625 *it6625, u8 *edid, int start_block, + int num_blocks) +{ + int i, bank_ctrl, err = 0; + struct device *dev = it6625->dev; + + if (!edid) { + dev_err(dev, "edid buffer is NULL"); + return -EINVAL; + } + + if (start_block < 0 || num_blocks <= 0 || + start_block > EDID_NUM_BLOCKS_MAX || + num_blocks > EDID_NUM_BLOCKS_MAX || + start_block + num_blocks > EDID_NUM_BLOCKS_MAX) { + dev_err(dev, + "invalid block range: start_block=%d, num_blocks=%d", + start_block, num_blocks); + return -EINVAL; + } + + guard(mutex)(&it6625->it6625_lock); + for (i = 0; i < num_blocks; i++) { + bank_ctrl = CTL_BANK_EDID_READ + start_block + i; + err = it6625_set_bank(it6625, bank_ctrl); + if (err < 0) + break; + + err = it6625_read_bytes(it6625, REG_EDID_START, + edid + (i * 128), 128); + if (err < 0) + break; + } + + it6625_set_bank(it6625, 0); + + return err < 0 ? err : num_blocks; +} + +static int it6625_write_edid(struct it6625 *it6625, u8 *edid, int start_block, + int num_blocks) +{ + int i, bank_ctrl, err = 0; + struct device *dev = it6625->dev; + + if (start_block < 0 || num_blocks <= 0 || + start_block > EDID_NUM_BLOCKS_MAX || + num_blocks > EDID_NUM_BLOCKS_MAX || + start_block + num_blocks > EDID_NUM_BLOCKS_MAX) { + dev_err(dev, + "invalid block range: start_block=%d, num_blocks=%d", + start_block, num_blocks); + return -EINVAL; + } + + guard(mutex)(&it6625->it6625_lock); + for (i = 0; i < num_blocks; i++) { + bank_ctrl = CTL_BANK_EDID_WRITE + start_block + i; + err = it6625_set_bank(it6625, bank_ctrl); + if (err < 0) + break; + + err = it6625_write_bytes(it6625, REG_EDID_START, + edid + (i * 128), 128); + if (err < 0) + break; + + err = it6625_update_config(it6625); + if (err < 0) + break; + } + + it6625_set_bank(it6625, 0); + + return err < 0 ? err : num_blocks; +} + +static void it6625_enable_auto_hpd(struct it6625 *it6625) +{ + dev_dbg(it6625->dev, "%s: auto HPD", __func__); + guard(mutex)(&it6625->it6625_lock); + it6625_set_bits(it6625, REG_RX_CFG, 0x03, 0x00); + it6625_update_config(it6625); +} + +static void it6625_disable_hpd(struct it6625 *it6625) +{ + cancel_delayed_work_sync(&it6625->hpd_delayed_work); + + guard(mutex)(&it6625->it6625_lock); + it6625_set_bits(it6625, REG_RX_CFG, 0x03, 0x01); + it6625_update_config(it6625); +} + +static void it6625_enable_hpd(struct it6625 *it6625) +{ + schedule_delayed_work(&it6625->hpd_delayed_work, + msecs_to_jiffies(100)); +} + +static void it6625_hpd_delayed_work(struct work_struct *work) +{ + struct it6625 *it6625 = container_of(work, + struct it6625, hpd_delayed_work.work); + int val = 0; + + guard(mutex)(&it6625->it6625_lock); + it6625_set_bits(it6625, REG_RX_CFG, 0x03, val); + it6625_update_config(it6625); +} + +static int it6625_get_detected_timings(struct it6625 *it6625, + struct v4l2_dv_timings *timings) +{ + struct v4l2_bt_timings *bt = &timings->bt; + int val; + unsigned int width, height; + u8 buffer[4]; + u8 buffer2[12]; + + if (no_signal(it6625)) { + dev_err(it6625->dev, "no signal detected"); + return -ENOLINK; + } + + guard(mutex)(&it6625->it6625_lock); + + memset(timings, 0, sizeof(struct v4l2_dv_timings)); + timings->type = V4L2_DV_BT_656_1120; + val = it6625_read_byte(it6625, REG_VID_INFO); + if (val < 0) { + dev_err(it6625->dev, "failed to read video info"); + return -EIO; + } + + bt->interlaced = val & B_INTERLACE ? + V4L2_DV_INTERLACED : V4L2_DV_PROGRESSIVE; + + if (it6625_read_bytes(it6625, REG_H_ACTIVE_1, buffer, 4) < 0) + return -EIO; + + width = ((buffer[0] & 0xff) << 8) + buffer[1]; + height = ((buffer[2] & 0xff) << 8) + buffer[3]; + + bt->width = width; + bt->height = height; + + if (it6625_read_bytes(it6625, REG_H_FP_1, buffer2, 12) < 0) + return -EIO; + + bt->hfrontporch = ((buffer2[0] & 0xff) << 8) + buffer2[1]; + bt->hsync = ((buffer2[2] & 0xff) << 8) + buffer2[3]; + bt->hbackporch = ((buffer2[4] & 0xff) << 8) + buffer2[5]; + bt->vfrontporch = ((buffer2[6] & 0xff) << 8) + buffer2[7]; + bt->vsync = ((buffer2[8] & 0xff) << 8) + buffer2[9]; + bt->vbackporch = ((buffer2[10] & 0xff) << 8) + buffer2[11]; + + bt->pixelclock = it6625_get_pclk(it6625); + if (bt->interlaced == V4L2_DV_INTERLACED) { + bt->height *= 2; + bt->il_vsync = bt->vsync + 1; + bt->pixelclock /= 2; + } + + return 0; +} + +static void it6625_show_avi_infoframe(struct it6625 *it6625) +{ + struct device *dev = it6625->dev; + union hdmi_infoframe frame; + u8 buffer[HDMI_INFOFRAME_SIZE(AVI)]; + u8 ver, len; + int ret; + + if (!is_hdmi(it6625)) { + dev_err(dev, "not HDMI signal, skip AVI infoframe log"); + return; + } + + ret = it6625_read_bytes(it6625, REG_AVI_DATA, buffer + 1, + HDMI_INFOFRAME_SIZE(AVI) - 1); + if (ret < 0) { + dev_err(dev, "failed to read AVI infoframe data"); + return; + } + + len = buffer[1]; + ver = buffer[2]; + + buffer[0] = HDMI_INFOFRAME_TYPE_AVI; + buffer[1] = ver; + buffer[2] = len; + + ret = hdmi_infoframe_unpack(&frame, buffer, sizeof(buffer)); + if (ret < 0) { + dev_err(dev, "unpack of AVI infoframe failed"); + return; + } + + hdmi_infoframe_log(KERN_INFO, dev, &frame); +} + +static int it6625_s_ctrl_detect_hdmi_5v(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + return v4l2_ctrl_s_ctrl(it6625->ctrl_5v_detect, + hdmi_5v_power_present(it6625)); +} + +static int it6625_s_ctrl_audio_sampling_rate(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + return v4l2_ctrl_s_ctrl(it6625->ctrl_audio_sampling_rate, + get_audio_sampling_rate(it6625)); +} + +static int it6625_s_ctrl_audio_present(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + return v4l2_ctrl_s_ctrl(it6625->ctrl_audio_present, + audio_present(it6625)); +} + +static int it6625_v4l2_sd_ctrl_update(struct v4l2_subdev *sd) +{ + int ret = 0; + + ret |= it6625_s_ctrl_detect_hdmi_5v(sd); + ret |= it6625_s_ctrl_audio_sampling_rate(sd); + ret |= it6625_s_ctrl_audio_present(sd); + + return 0; +} + +static void it6625_enable_stream_locked(struct it6625 *it6625, bool enable) +{ + struct v4l2_subdev *sd = &it6625->sd; + int val; + + lockdep_assert_held(&it6625->it6625_lock); + + v4l2_dbg(3, debug, sd, "%s: %sable", + __func__, enable ? "en" : "dis"); + + val = enable ? B_MIPI_OUTPUT : 0; + it6625_set_bits(it6625, REG_MIPI_CONTROL, B_MIPI_OUTPUT, val); + it6625_update_config(it6625); +} + +static void it6625_enable_stream(struct it6625 *it6625, bool enable) +{ + guard(mutex)(&it6625->it6625_lock); + it6625_enable_stream_locked(it6625, enable); +} + +static void it6625_set_mipi_config_locked(struct it6625 *it6625, u32 cfg_val) +{ + u8 mipi_data_type; + + lockdep_assert_held(&it6625->it6625_lock); + + dev_dbg(it6625->dev, "mipi_data_type = 0x%x", cfg_val); + + mipi_data_type = cfg_val & 0xFF; + it6625_write_byte(it6625, REG_MIPI_DATA_TYPE, mipi_data_type); + it6625_update_config(it6625); +} + +static void it6625_set_mipi_config(struct it6625 *it6625, u32 cfg_val) +{ + guard(mutex)(&it6625->it6625_lock); + it6625_set_mipi_config_locked(it6625, cfg_val); +} + +static inline unsigned int fps_from_bt_timings(const struct v4l2_bt_timings *t) +{ + if (!V4L2_DV_BT_FRAME_HEIGHT(t) || !V4L2_DV_BT_FRAME_WIDTH(t)) + return 0; + + return DIV_ROUND_CLOSEST((unsigned int)t->pixelclock, + V4L2_DV_BT_FRAME_HEIGHT(t) * + V4L2_DV_BT_FRAME_WIDTH(t)); +} + +static void it6625_initial_setup(struct it6625 *it6625) +{ + int val = 0; + + /* + * REG_MIPI_CFG[0:2] lane count field: 1 lane -> 0, 2 lanes -> 1, + * 3 lanes (C-PHY only) -> 3, 4 lanes (D-PHY only) -> 3. + */ + switch (it6625->csi_lanes) { + case 1: + val = FIELD_PREP(M_MIPI_LANE, 0); + break; + case 2: + val = FIELD_PREP(M_MIPI_LANE, 1); + break; + default: + val = FIELD_PREP(M_MIPI_LANE, 3); + break; + } + + if (it6625->bus_type == V4L2_MBUS_CSI2_DPHY) + val |= FIELD_PREP(B_MIPI_DPHY, 1); + + if (it6625->port_num == 2) + val |= FIELD_PREP(B_MIPI_SPLIT, 1); + + it6625_write_byte(it6625, REG_MIPI_CFG, val); + it6625_write_byte(it6625, REG_MIPI_DATA_TYPE, it6625->csi_format); + it6625_write_byte(it6625, REG_MIPI_CONTROL, 0x00); + it6625_write_byte(it6625, REG_RX_CFG, 0x00); + + it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CONFIG_UPDATE, B_CONFIG_UPDATE); + it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static void dump_cec_msg(struct it6625 *it6625, struct cec_msg *msg, char *str) +{ + int i; + + dev_dbg(it6625->dev, "%s: from %d to %d len=%d", str, + msg->msg[0] >> 4, msg->msg[0] & 0x0f, msg->len); + for (i = 1; i < msg->len; i++) + dev_dbg(it6625->dev, " %02x", msg->msg[i]); +} + +static int it6625_cec_adap_enable(struct cec_adapter *adap, bool enable) +{ + struct it6625 *it6625 = adap->priv; + u8 cmds[2]; + + cmds[0] = CMD_SET_CEC_ENABLE; + cmds[1] = enable ? 1 : 0; + guard(mutex)(&it6625->it6625_lock); + it6625_write_command(it6625, cmds, sizeof(cmds)); + + return 0; +} + +static int it6625_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr) +{ + struct it6625 *it6625 = adap->priv; + + dev_dbg(it6625->dev, "%s: la=%d", __func__, log_addr); + + if (log_addr != CEC_LOG_ADDR_INVALID) { + u8 cmds[2] = {CMD_SET_CEC_LA, log_addr}; + + guard(mutex)(&it6625->it6625_lock); + it6625_write_command(it6625, cmds, sizeof(cmds)); + } + + return 0; +} + +static int it6625_cec_adap_transmit(struct cec_adapter *adap, u8 attempts, + u32 signal_free_time, struct cec_msg *msg) +{ + struct it6625 *it6625 = adap->priv; + + dump_cec_msg(it6625, msg, "CEC_TX"); + + guard(mutex)(&it6625->it6625_lock); + it6625_write_bytes(it6625, REG_CEC_TX_DATA, msg->msg, msg->len); + it6625_write_byte(it6625, REG_CEC_TX_DATA_LEN, msg->len); + it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CEC_SEND_DATA, B_CEC_SEND_DATA); + it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); + + return 0; +} + +static const struct cec_adap_ops it6625_cec_adap_ops = { + .adap_enable = it6625_cec_adap_enable, + .adap_log_addr = it6625_cec_adap_log_addr, + .adap_transmit = it6625_cec_adap_transmit, +}; + +static void it6625_cec_handler(struct it6625 *it6625, u8 intstatus) +{ + struct cec_msg rxmsg = {}; + int val = 0; + + if (intstatus & B_CEC_RX_RECEIVED) { + scoped_guard(mutex, &it6625->it6625_lock) { + val = it6625_read_byte(it6625, REG_CEC_RX_DATA_LEN); + if (val > 0 && val <= 16) + it6625_read_bytes(it6625, REG_CEC_RX_DATA, &rxmsg.msg[0], val); + it6625_write_byte(it6625, REG_CEC_RX_DATA_LEN, 0); + } + + if (val > 0 && val <= 16) { + rxmsg.len = val; + cec_received_msg(it6625->cec_adap, &rxmsg); + dump_cec_msg(it6625, &rxmsg, "CEC_RX"); + } else { + dev_err(it6625->dev, "invalid CEC RX length %d", val); + } + } + + if (intstatus & B_CEC_TX_UPDATE) { + val = it6625_read_byte(it6625, REG_CEC_STATUS); + if (val < 0) { + dev_err(it6625->dev, "read CEC status failed"); + return; + } + + if (val & BIT(0)) { + cec_transmit_attempt_done(it6625->cec_adap, + CEC_TX_STATUS_OK); + } else if (val & BIT(1)) { + cec_transmit_attempt_done(it6625->cec_adap, + CEC_TX_STATUS_NACK); + } else { + dev_err(it6625->dev, "unknown CEC status %02X", val); + cec_transmit_attempt_done(it6625->cec_adap, + CEC_TX_STATUS_NACK); + } + } +} + +static void it6625_irq_format_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + struct v4l2_dv_timings timings; + const struct v4l2_event it6625_ev_fmt = { + .type = V4L2_EVENT_SOURCE_CHANGE, + .u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION, + }; + int ret; + + if (no_signal(it6625)) + return; + + ret = it6625_get_detected_timings(it6625, &timings); + if (ret < 0) { + v4l2_dbg(1, debug, sd, "Failed to get detected timings"); + return; + } + + if (sd->devnode) + v4l2_subdev_notify_event(sd, &it6625_ev_fmt); +} + +static void it6625_irq_hdmi_audio_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + + it6625_s_ctrl_audio_sampling_rate(sd); + it6625_s_ctrl_audio_present(sd); +} + +static void it6625_get_timings(struct it6625 *it6625, + struct v4l2_dv_timings *timings) +{ + guard(mutex)(&it6625->it6625_lock); + *timings = it6625->timings; +} + +static void it6625_clear_timings(struct it6625 *it6625) +{ + guard(mutex)(&it6625->it6625_lock); + memset(&it6625->timings, 0, sizeof(it6625->timings)); +} + +static void it6625_irq_hdmi_5v_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + + it6625_clear_timings(it6625); + it6625_v4l2_sd_ctrl_update(sd); +} + +static void it6625_irq_hdcp_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + u8 cp_sts; + + cp_sts = it6625_read_byte(it6625, REG_RX_HDCP_STS); + v4l2_info(sd, "HDCP change to %02X", cp_sts); +} + +static void it6625_irq_infoframe_latch(struct it6625 *it6625) +{ + u8 info_frame[32]; + + guard(mutex)(&it6625->it6625_lock); + + it6625_read_bytes(it6625, REG_IF_DATA, info_frame, 31); +} + +static void it6625_irq_emata_packet_latch(struct it6625 *it6625) +{ + u8 extend_packet[14]; + + it6625_read_bytes(it6625, REG_EMP_DATA, extend_packet, 13); +} + +static void it6625_irq_avmute_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + int val; + u8 avmute; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + if (val < 0) + return; + + avmute = (val & B_RX_AVMUTE) ? 1 : 0; + v4l2_info(sd, "AVMute change to %d", avmute); +} + +static int it6625_interrupt_handler(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + int val, int_sts1 = 0, int_sts2 = 0; + + val = it6625_read_byte(it6625, REG_MCU_INTERRUPT); + if (val <= 0) + return 0; + + it6625_write_byte(it6625, REG_MCU_INTERRUPT, val); + + v4l2_dbg(1, debug, sd, "%s: INT = 0x%02X", __func__, val); + + if (val & (B_CEC_RX_RECEIVED | B_CEC_TX_UPDATE) && it6625->cec_adap) + it6625_cec_handler(it6625, val & (B_CEC_RX_RECEIVED | B_CEC_TX_UPDATE)); + + if (val & B_SYS_INT_ACTIVE) { + int_sts1 = it6625_read_byte(it6625, REG_RX_INT_STATUS1); + int_sts2 = it6625_read_byte(it6625, REG_RX_INT_STATUS2); + v4l2_dbg(1, debug, sd, "%s: hdmi_int = 0x%02X 0x%02X", + __func__, int_sts1, int_sts2); + + if (int_sts1 >= 0) { + it6625_write_byte(it6625, REG_RX_INT_STATUS1, 0x00); + if (int_sts1 & B_HDMI_5V_CHG) + it6625_irq_hdmi_5v_change(it6625); + + if (int_sts1 & B_HDMI_VID_CHG) + it6625_irq_format_change(it6625); + + if (int_sts1 & B_HDMI_AUD_CHG) + it6625_irq_hdmi_audio_change(it6625); + + if (int_sts1 & B_HDMI_CP_CHG) + it6625_irq_hdcp_change(it6625); + + if (int_sts1 & B_HDMI_IF_LATCH) + it6625_irq_infoframe_latch(it6625); + + if (int_sts1 & B_HDMI_EMP) + it6625_irq_emata_packet_latch(it6625); + } + + if (int_sts2 >= 0) { + it6625_write_byte(it6625, REG_RX_INT_STATUS2, 0x00); + + if (int_sts2 & B_HDMI_AVI) + it6625_show_avi_infoframe(it6625); + + if (int_sts2 & B_HDMI_NO_AVI) + v4l2_info(sd, "AVI-info stopped"); + + if (int_sts2 & B_HDMI_AVMUTE_CHG) + it6625_irq_avmute_change(it6625); + } + } + + return 0; +} + +static irqreturn_t it6625_irq_handler(int unused, void *data) +{ + struct it6625 *it6625 = data; + + it6625_interrupt_handler(it6625); + return IRQ_HANDLED; +} + +static void it6625_irq_poll_timer(struct timer_list *t) +{ + struct it6625 *it6625 = timer_container_of(it6625, t, timer); + unsigned int msecs; + + schedule_work(&it6625->polling_work); + /* + * If CEC is present, then we need to poll more frequently, + * otherwise we will miss CEC messages. + */ + msecs = it6625->cec_adap ? POLL_INTERVAL_CEC_MS : POLL_INTERVAL_MS; + mod_timer(&it6625->timer, jiffies + msecs_to_jiffies(msecs)); +} + +static void it6625_polling_work(struct work_struct *work) +{ + struct it6625 *it6625 = container_of(work, struct it6625, + polling_work); + + it6625_interrupt_handler(it6625); +} + +static int it6625_log_status(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + struct v4l2_dv_timings timings, configured_timings; + + if (it6625_get_detected_timings(it6625, &timings)) + v4l2_info(sd, "No video detected"); + else + v4l2_print_dv_timings(sd->name, "Detected format: ", &timings, + true); + + it6625_get_timings(it6625, &configured_timings); + v4l2_print_dv_timings(sd->name, "Configured format: ", + &configured_timings, true); + + it6625_show_avi_infoframe(it6625); + + return 0; +} + +static int it6625_isr(struct v4l2_subdev *sd, u32 status, bool *handled) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + schedule_work(&it6625->polling_work); + *handled = true; + + return 0; +} + +static int it6625_subscribe_event(struct v4l2_subdev *sd, struct v4l2_fh *fh, + struct v4l2_event_subscription *sub) +{ + switch (sub->type) { + case V4L2_EVENT_SOURCE_CHANGE: + return v4l2_src_change_event_subdev_subscribe(sd, fh, sub); + case V4L2_EVENT_CTRL: + return v4l2_ctrl_subdev_subscribe_event(sd, fh, sub); + default: + return -EINVAL; + } +} + +static int it6625_g_input_status(struct v4l2_subdev *sd, u32 *status) +{ + struct it6625 *it6625 = sd_to_6625(sd); + bool val; + + val = no_signal(it6625); + *status = 0; + *status |= val ? V4L2_IN_ST_NO_SIGNAL : 0; + *status |= val ? V4L2_IN_ST_NO_SYNC : 0; + + v4l2_dbg(1, debug, sd, "%s: status = 0x%x", __func__, *status); + + return 0; +} + +static int +it6625_update_timings_if_changed(struct it6625 *it6625, + const struct v4l2_dv_timings *timings) +{ + int ret; + + guard(mutex)(&it6625->it6625_lock); + if (v4l2_match_dv_timings(&it6625->timings, timings, 0, false)) { + ret = 0; + } else if (!v4l2_valid_dv_timings(timings, &it6625_timings_cap, + NULL, NULL)) { + ret = -ERANGE; + } else { + it6625->timings = *timings; + ret = 1; + } + + return ret; +} + +static int it6625_s_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int ret; + + if (!timings) + return -EINVAL; + + if (debug) + v4l2_print_dv_timings(sd->name, __func__, timings, false); + + ret = it6625_update_timings_if_changed(it6625, timings); + if (ret == -ERANGE) { + v4l2_dbg(1, debug, sd, "%s: timings out of range", __func__); + return ret; + } + + if (ret == 0) { + v4l2_dbg(1, debug, sd, "%s: no change", __func__); + return 0; + } + + it6625_enable_stream(it6625, false); + + return 0; +} + +static int it6625_g_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + it6625_get_timings(it6625, timings); + + return 0; +} + +static int it6625_enum_dv_timings(struct v4l2_subdev *sd, + struct v4l2_enum_dv_timings *timings) +{ + if (timings->pad != 0) + return -EINVAL; + + return v4l2_enum_dv_timings_cap(timings, + &it6625_timings_cap, NULL, NULL); +} + +static int it6625_query_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int ret; + + ret = it6625_get_detected_timings(it6625, timings); + if (ret) + return ret; + + if (debug) + v4l2_print_dv_timings(sd->name, __func__, timings, false); + + if (!v4l2_valid_dv_timings(timings, &it6625_timings_cap, NULL, NULL)) { + v4l2_dbg(1, debug, sd, "%s: timings out of range", __func__); + return -ERANGE; + } + + return 0; +} + +static int it6625_dv_timings_cap(struct v4l2_subdev *sd, + struct v4l2_dv_timings_cap *cap) +{ + if (cap->pad != 0) + return -EINVAL; + + *cap = it6625_timings_cap; + + return 0; +} + +static int it6625_s_stream(struct v4l2_subdev *sd, int enable) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + it6625_enable_stream(it6625, enable); + return 0; +} + +static int it6625_enum_mbus_code(struct v4l2_subdev *sd, + struct v4l2_subdev_state *sd_state, + struct v4l2_subdev_mbus_code_enum *code) +{ + dev_dbg(sd->dev, "%s: index=%d", __func__, code->index); + + if (code->index >= ARRAY_SIZE(it6625_formats)) + return -EINVAL; + + dev_dbg(sd->dev, "%s: code=0x%08x", __func__, + it6625_formats[code->index].mbus_fmt_code); + code->code = it6625_formats[code->index].mbus_fmt_code; + + return 0; +} + +static int it6625_get_mbus_config(struct v4l2_subdev *sd, + unsigned int pad, + struct v4l2_mbus_config *cfg) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + if (pad != 0) + return -EINVAL; + + cfg->type = it6625->bus_type; + cfg->bus.mipi_csi2.flags = 0; + cfg->bus.mipi_csi2.num_data_lanes = it6625->csi_lanes; + + return 0; +} + +static int it6625_pad_s_dv_timings(struct v4l2_subdev *sd, unsigned int pad, + struct v4l2_dv_timings *timings) +{ + if (pad != 0) + return -EINVAL; + + return it6625_s_dv_timings(sd, timings); +} + +static int it6625_pad_g_dv_timings(struct v4l2_subdev *sd, unsigned int pad, + struct v4l2_dv_timings *timings) +{ + if (pad != 0) + return -EINVAL; + + return it6625_g_dv_timings(sd, timings); +} + +static int it6625_pad_query_dv_timings(struct v4l2_subdev *sd, + unsigned int pad, + struct v4l2_dv_timings *timings) +{ + if (pad != 0) + return -EINVAL; + + return it6625_query_dv_timings(sd, timings); +} + +static inline u32 format_to_colorspace(u8 csi_format) +{ + switch (csi_format) { + case CSI_RGB444: + case CSI_RGB555: + case CSI_RGB565: + case CSI_RGB666: + case CSI_RGB888: + case CSI_RGB_10b: + case CSI_RGB_12b: + return V4L2_COLORSPACE_SRGB; + case CSI_YUV420_8b_L: + case CSI_YUV420_8b: + case CSI_YUV420_10b: + case CSI_YUV422_8b: + case CSI_YUV422_10b: + case CSI_YUV422_12b: + case CSI_YUV420_10b_L: + case CSI_YUV420_12b: + case CSI_YUV444_8b: + case CSI_YUV444_10b: + case CSI_YUV444_12b: + return V4L2_COLORSPACE_REC709; + default: + return 0; + } +} + +static const char *it6625_csi_format_name(u8 csi_format) +{ + switch (csi_format) { + case CSI_YUV422_8b: + return "YUV422 8bit"; + case CSI_RGB888: + return "RGB888 8bit"; + case CSI_YUV444_8b: + return "YUV444 8bit"; + default: + return "unknown"; + } +} + +static int it6625_get_fmt(struct v4l2_subdev *sd, + struct v4l2_subdev_state *sd_state, + struct v4l2_subdev_format *format) +{ + struct it6625 *it6625 = sd_to_6625(sd); + struct v4l2_dv_timings timings; + + if (format->pad != 0) + return -EINVAL; + + it6625_get_timings(it6625, &timings); + format->format.width = timings.bt.width; + format->format.height = timings.bt.height; + format->format.field = timings.bt.interlaced == V4L2_DV_INTERLACED ? + V4L2_FIELD_INTERLACED : V4L2_FIELD_NONE; + + if (format->which == V4L2_SUBDEV_FORMAT_TRY) { + struct v4l2_mbus_framefmt *fmt; + + fmt = v4l2_subdev_state_get_format(sd_state, format->pad); + format->format.code = fmt->code; + format->format.colorspace = fmt->colorspace; + } else { + scoped_guard(mutex, &it6625->it6625_lock) { + format->format.colorspace = + format_to_colorspace(it6625->csi_format); + format->format.code = it6625->mbus_fmt_code; + } + } + + return 0; +} + +static int it6625_set_fmt(struct v4l2_subdev *sd, + struct v4l2_subdev_state *sd_state, + struct v4l2_subdev_format *format) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int ret; + u32 mbus_fmt_code = format->format.code; + + ret = it6625_get_fmt(sd, sd_state, format); + format->format.code = mbus_fmt_code; + + if (ret) + return ret; + + ret = it6625_csi_mbus_code_idx(mbus_fmt_code); + + if (ret < 0) { + v4l2_dbg(1, debug, sd, + "%s: unsupported format code 0x%x, falling back to default", + __func__, mbus_fmt_code); + ret = 0; + mbus_fmt_code = it6625_formats[ret].mbus_fmt_code; + format->format.code = mbus_fmt_code; + } + + if (format->which == V4L2_SUBDEV_FORMAT_TRY) { + struct v4l2_mbus_framefmt *fmt; + + fmt = v4l2_subdev_state_get_format(sd_state, format->pad); + fmt->code = format->format.code; + fmt->colorspace = format_to_colorspace(it6625_formats[ret].csi_format); + format->format.colorspace = fmt->colorspace; + v4l2_dbg(1, debug, sd, "%s: try format code = 0x%x", + __func__, format->format.code); + return 0; + } + + scoped_guard(mutex, &it6625->it6625_lock) { + it6625->csi_format = it6625_formats[ret].csi_format; + it6625->mbus_fmt_code = format->format.code; + it6625_enable_stream_locked(it6625, false); + it6625_set_mipi_config_locked(it6625, it6625->csi_format); + } + + return 0; +} + +static int it6625_g_edid(struct v4l2_subdev *sd, + struct v4l2_subdev_edid *edid) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int err; + + if (edid->pad != 0) + return -EINVAL; + + memset(edid->reserved, 0, sizeof(edid->reserved)); + + if (edid->blocks == 0) { + edid->blocks = EDID_NUM_BLOCKS_MAX; + return 0; + } + + if (edid->start_block >= EDID_NUM_BLOCKS_MAX) { + edid->blocks = 0; + return 0; + } + + if (edid->blocks > EDID_NUM_BLOCKS_MAX - edid->start_block) + edid->blocks = EDID_NUM_BLOCKS_MAX - edid->start_block; + + err = it6625_read_edid(it6625, edid->edid, edid->start_block, + edid->blocks); + + return (err < 0) ? err : 0; +} + +static int it6625_s_edid(struct v4l2_subdev *sd, + struct v4l2_subdev_edid *edid) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int err = 0; + u16 pa; + int pa_err; + + if (edid->pad != 0) { + v4l2_err(sd, "invalid pad %d", edid->pad); + return -EINVAL; + } + + memset(edid->reserved, 0, sizeof(edid->reserved)); + + if (edid->blocks > EDID_NUM_BLOCKS_MAX) { + v4l2_err(sd, "too many edid blocks: %d", edid->blocks); + edid->blocks = EDID_NUM_BLOCKS_MAX; + return -E2BIG; + } + + it6625_disable_hpd(it6625); + + if (edid->blocks == 0) { + cec_phys_addr_invalidate(it6625->cec_adap); + return 0; + } + + err = it6625_write_edid(it6625, edid->edid, + edid->start_block, edid->blocks); + if (err < 0) { + v4l2_err(sd, "write edid failed"); + } else { + pa = cec_get_edid_phys_addr(edid->edid, edid->blocks * 128, NULL); + pa_err = pa == CEC_PHYS_ADDR_INVALID ? -EINVAL : + v4l2_phys_addr_validate(pa, NULL, NULL); + if (pa_err) { + v4l2_warn(sd, "invalid CEC physical address in EDID"); + cec_phys_addr_invalidate(it6625->cec_adap); + } else { + cec_s_phys_addr(it6625->cec_adap, pa, false); + } + } + + if (hdmi_5v_power_present(it6625)) { + it6625_enable_hpd(it6625); + it6625_s_ctrl_detect_hdmi_5v(sd); + } else { + it6625_enable_auto_hpd(it6625); + } + + return err < 0 ? err : 0; +} + +#define V4L2_CID_USER_IT6625_BASE (V4L2_CID_USER_BASE + 0x11b0) +#define IT6625_CID_AUDIO_SAMPLING_RATE (V4L2_CID_USER_IT6625_BASE + 0) +#define IT6625_CID_AUDIO_PRESENT (V4L2_CID_USER_IT6625_BASE + 1) +#define IT6625_CID_HDMI_INFO (V4L2_CID_USER_IT6625_BASE + 2) +#define IT6625_CID_MIPI_CONFIG (V4L2_CID_USER_IT6625_BASE + 3) +static int it6625_g_volatile_ctrl(struct v4l2_ctrl *ctrl) +{ + struct it6625 *it6625 = hdl_to_6625(ctrl->handler); + struct v4l2_subdev *sd = &it6625->sd; + + v4l2_dbg(1, debug, sd, "%s, id=0x%x, val=%d", + __func__, ctrl->id, ctrl->val); + + switch (ctrl->id) { + case IT6625_CID_HDMI_INFO: + scoped_guard(mutex, &it6625->it6625_lock) { + snprintf(ctrl->p_new.p_char, ctrl->maximum, "%s @ %uHz", + it6625_csi_format_name(it6625->csi_format), + fps_from_bt_timings(&it6625->timings.bt)); + } + break; + case IT6625_CID_MIPI_CONFIG: + ctrl->val = it6625_read_byte(it6625, REG_MIPI_DATA_TYPE); + break; + default: + break; + } + + return 0; +} + +static int it6625_s_ctrl(struct v4l2_ctrl *ctrl) +{ + struct it6625 *it6625 = hdl_to_6625(ctrl->handler); + struct v4l2_subdev *sd = &it6625->sd; + + v4l2_dbg(1, debug, sd, "%s, id=0x%x, val=%d", + __func__, ctrl->id, ctrl->val); + + switch (ctrl->id) { + case IT6625_CID_MIPI_CONFIG: + it6625_set_mipi_config(it6625, ctrl->val); + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct v4l2_subdev_core_ops it6625_core_ops = { + .log_status = it6625_log_status, + .interrupt_service_routine = it6625_isr, + .subscribe_event = it6625_subscribe_event, + .unsubscribe_event = v4l2_event_subdev_unsubscribe, +}; + +static const struct v4l2_subdev_video_ops it6625_video_ops = { + .g_input_status = it6625_g_input_status, + .s_stream = it6625_s_stream, +}; + +static const struct v4l2_subdev_pad_ops it6625_pad_ops = { + .enum_mbus_code = it6625_enum_mbus_code, + .set_fmt = it6625_set_fmt, + .get_fmt = it6625_get_fmt, + .get_edid = it6625_g_edid, + .set_edid = it6625_s_edid, + .enum_dv_timings = it6625_enum_dv_timings, + .dv_timings_cap = it6625_dv_timings_cap, + .get_mbus_config = it6625_get_mbus_config, + .s_dv_timings = it6625_pad_s_dv_timings, + .g_dv_timings = it6625_pad_g_dv_timings, + .query_dv_timings = it6625_pad_query_dv_timings, +}; + +static const struct v4l2_subdev_ops it6625_ops = { + .core = &it6625_core_ops, + .video = &it6625_video_ops, + .pad = &it6625_pad_ops, +}; + +static const struct v4l2_ctrl_ops it6625_ctrl_ops = { + .g_volatile_ctrl = it6625_g_volatile_ctrl, + .s_ctrl = it6625_s_ctrl, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_audio_sampling_rate = { + .id = IT6625_CID_AUDIO_SAMPLING_RATE, + .name = "Audio sampling rate", + .type = V4L2_CTRL_TYPE_INTEGER, + .min = 0, + .max = 768000, + .step = 1, + .def = 0, + .flags = V4L2_CTRL_FLAG_READ_ONLY, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_audio_present = { + .id = IT6625_CID_AUDIO_PRESENT, + .name = "Audio present", + .type = V4L2_CTRL_TYPE_BOOLEAN, + .min = 0, + .max = 1, + .step = 1, + .def = 0, + .flags = V4L2_CTRL_FLAG_READ_ONLY, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_get_hdmi_info = { + .ops = &it6625_ctrl_ops, + .id = IT6625_CID_HDMI_INFO, + .name = "HDMI Info", + .type = V4L2_CTRL_TYPE_STRING, + .max = 128, + .step = 1, + .flags = V4L2_CTRL_FLAG_READ_ONLY | V4L2_CTRL_FLAG_VOLATILE, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_mipi_config = { + .ops = &it6625_ctrl_ops, + .id = IT6625_CID_MIPI_CONFIG, + .name = "MIPI config", + .type = V4L2_CTRL_TYPE_INTEGER, + .min = 0, + .max = 0x7FFFFFFF, + .step = 1, + .def = 0x3E, + .flags = V4L2_CTRL_FLAG_VOLATILE, +}; + +static int it6625_v4l2_init_controls(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + struct v4l2_ctrl_handler *hdl = &it6625->hdl; + + v4l2_ctrl_handler_init(hdl, 4); + it6625->ctrl_5v_detect = + v4l2_ctrl_new_std(hdl, NULL, V4L2_CID_DV_RX_POWER_PRESENT, + 0, 1, 0, 0); + + it6625->ctrl_audio_sampling_rate = + v4l2_ctrl_new_custom(hdl, + &it6625_ctrl_audio_sampling_rate, + NULL); + it6625->ctrl_audio_present = + v4l2_ctrl_new_custom(hdl, &it6625_ctrl_audio_present, NULL); + it6625->ctrl_link_freq = + v4l2_ctrl_new_int_menu(hdl, NULL, V4L2_CID_LINK_FREQ, + ARRAY_SIZE(it6625_link_freq) - 1, + 0, it6625_link_freq); + it6625->ctrl_get_hdmi_info = + v4l2_ctrl_new_custom(hdl, &it6625_ctrl_get_hdmi_info, NULL); + it6625->ctrl_mipi_config = + v4l2_ctrl_new_custom(hdl, &it6625_ctrl_mipi_config, NULL); + if (hdl->error) { + v4l2_err(sd, "Failed to initialize controls"); + v4l2_ctrl_handler_free(hdl); + return hdl->error; + } + + sd->ctrl_handler = hdl; + + return 0; +} + +static void it6625_regdump_print(struct seq_file *s, const u8 *reg_buf) +{ + int i; + + seq_puts(s, " 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F\n"); + + for (i = 0; i < 256; i++) { + if (i % 16 == 0) + seq_printf(s, "[%02X] ", i & 0xF0); + seq_printf(s, "0x%02X ", reg_buf[i]); + if (i % 16 == 15) + seq_putc(s, '\n'); + } +} + +static int it6625_edid_ram_show(struct seq_file *s, void *data) +{ + struct it6625 *it6625 = s->private; + u8 reg_buf[256]; + int ret; + + ret = it6625_read_edid(it6625, reg_buf, 0, 2); + if (ret < 0) + return ret; + it6625_regdump_print(s, reg_buf); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(it6625_edid_ram); + +static int it6625_mipi_reg_show(struct seq_file *s, void *data) +{ + struct it6625 *it6625 = s->private; + u8 reg_buf[256]; + int ret; + + scoped_guard(mutex, &it6625->it6625_lock) + ret = it6625_read_bytes(it6625, 0x00, reg_buf, sizeof(reg_buf)); + if (ret < 0) + return ret; + it6625_regdump_print(s, reg_buf); + + return 0; +} + +static int it6625_mipi_reg_open(struct inode *inode, struct file *file) +{ + return single_open(file, it6625_mipi_reg_show, inode->i_private); +} + +static ssize_t it6625_mipi_reg_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct it6625 *it6625 = file_inode(file)->i_private; + char buf[32] = {}; + unsigned int addr, val; + ssize_t len; + loff_t pos = 0; + + len = simple_write_to_buffer(buf, sizeof(buf) - 1, &pos, user_buf, count); + if (len < 0) + return len; + buf[len] = '\0'; + + if (sscanf(buf, "%X %X", &addr, &val) != 2) + return -EINVAL; + + scoped_guard(mutex, &it6625->it6625_lock) + it6625_write_byte(it6625, addr, val); + + return count; +} + +static const struct file_operations it6625_mipi_reg_fops = { + .owner = THIS_MODULE, + .open = it6625_mipi_reg_open, + .read = seq_read, + .write = it6625_mipi_reg_write, + .llseek = seq_lseek, + .release = single_release, +}; + +static void it6625_debugfs_init(struct it6625 *it6625, struct i2c_client *client) +{ + it6625->debugfs_dir = debugfs_create_dir(dev_name(&client->dev), NULL); + + debugfs_create_file("mipi_reg", 0600, it6625->debugfs_dir, it6625, + &it6625_mipi_reg_fops); + debugfs_create_file("edid_ram", 0400, it6625->debugfs_dir, it6625, + &it6625_edid_ram_fops); +} + +static void it6625_init_data(struct it6625 *it6625) +{ + static struct v4l2_dv_timings default_timing = + V4L2_DV_BT_CEA_1920X1080P60; + + it6625->csi_lanes = 4; + it6625->port_num = 1; + it6625->bus_type = V4L2_MBUS_CSI2_DPHY; + it6625->csi_format = it6625_formats[0].csi_format; + it6625->mbus_fmt_code = it6625_formats[0].mbus_fmt_code; + it6625->timings = default_timing; +} + +static int it6625_parse_endpoint(struct it6625 *it6625) +{ + struct device *dev = it6625->dev; + /* + * Pre-setting bus_type here makes v4l2_fwnode_endpoint_alloc_parse() + * treat it as a hard requirement and reject any endpoint whose DT + * bus-type disagrees, so this must stay V4L2_MBUS_UNKNOWN to let it + * autodetect C-PHY vs D-PHY from the endpoint itself. + */ + struct v4l2_fwnode_endpoint endpoint = { .bus_type = V4L2_MBUS_UNKNOWN }; + struct device_node *ep = NULL; + unsigned int max_lanes; + unsigned int port; + int ret; + + /* + * port@0 and port@1 are the two CSI-2 output ports MIPI0/MIPI1 + * (port@2 is the HDMI input). This chip series can drive both + * simultaneously in split or mirror mode, so port_num counts how + * many of MIPI0/MIPI1 have an endpoint wired up. This driver only + * wires up a single source pad, so lane/bus-type configuration is + * parsed from whichever of the two is found first. + */ + it6625->port_num = 0; + for (port = 0; port < 2; port++) { + struct device_node *port_ep = + of_graph_get_endpoint_by_regs(dev->of_node, port, -1); + + if (!port_ep) + continue; + + it6625->port_num++; + if (!ep) + ep = port_ep; + else + of_node_put(port_ep); + } + + if (!ep) { + it6625->port_num = 1; + dev_dbg(dev, "no CSI-2 endpoint node found, using default %u CSI lanes", + it6625->csi_lanes); + return 0; + } + + ret = v4l2_fwnode_endpoint_alloc_parse(of_fwnode_handle(ep), &endpoint); + of_node_put(ep); + if (ret) { + dev_err(dev, "failed to parse endpoint: %d", ret); + return ret; + } + + if (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY && + it6625->chip_type != IT6626_CHIP) { + dev_err(dev, "IT6625 does not support C-PHY, only IT6626 does"); + v4l2_fwnode_endpoint_free(&endpoint); + return -EINVAL; + } + + max_lanes = (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY) ? 3 : 4; + + if (endpoint.bus.mipi_csi2.num_data_lanes == 0 || + endpoint.bus.mipi_csi2.num_data_lanes > max_lanes) { + dev_err(dev, + "invalid number of CSI data lanes: %u (max %u for this bus type)", + endpoint.bus.mipi_csi2.num_data_lanes, max_lanes); + v4l2_fwnode_endpoint_free(&endpoint); + return -EINVAL; + } + + it6625->csi_lanes = endpoint.bus.mipi_csi2.num_data_lanes; + it6625->bus_type = endpoint.bus_type; + v4l2_fwnode_endpoint_free(&endpoint); + + return 0; +} + +static int it6625_parse_dt(struct it6625 *it6625) +{ + return it6625_parse_endpoint(it6625); +} + +static int it6625_init_v4l2_subdev(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + int err; + + sd->dev = it6625->dev; + + v4l2_i2c_subdev_init(sd, it6625->i2c_client, &it6625_ops); + sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS; + if (it6625_v4l2_init_controls(sd)) { + dev_err(it6625->dev, "Failed to initialize v4l2 controls"); + return -ENOMEM; + } + + it6625->pad.flags = MEDIA_PAD_FL_SOURCE; + sd->entity.function = MEDIA_ENT_F_CAM_SENSOR; + err = media_entity_pads_init(&sd->entity, 1, &it6625->pad); + if (err < 0) { + dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err); + v4l2_ctrl_handler_free(sd->ctrl_handler); + return err; + } + + return 0; +} + +static int it6625_check_device(struct it6625 *it6625) +{ + static const u8 chip_ids[][2] = { + { 0x66, 0x25 }, + { 0x66, 0x26 }, + }; + int chip_id0, chip_id1; + + chip_id0 = it6625_read_byte(it6625, REG_CHIP_ID_0); + chip_id1 = it6625_read_byte(it6625, REG_CHIP_ID_1); + if (chip_id0 != chip_ids[it6625->chip_type][0] || + chip_id1 != chip_ids[it6625->chip_type][1]) { + dev_err(it6625->dev, + "chip ID mismatch: got 0x%02x%02x, expected 0x%02x%02x", + chip_id0, chip_id1, + chip_ids[it6625->chip_type][0], + chip_ids[it6625->chip_type][1]); + return -ENODEV; + } + + return 0; +} + +static int it6625_probe(struct i2c_client *client) +{ + struct it6625 *it6625; + struct v4l2_subdev *sd; + int err; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) + return -EIO; + + it6625 = devm_kzalloc(&client->dev, sizeof(struct it6625), GFP_KERNEL); + if (!it6625) + return -ENOMEM; + + it6625->chip_type = (uintptr_t)i2c_get_match_data(client); + + it6625->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", + GPIOD_OUT_HIGH); + if (IS_ERR(it6625->reset_gpio)) + return PTR_ERR(it6625->reset_gpio); + + if (it6625->reset_gpio) { + usleep_range(1000, 2000); + gpiod_set_value_cansleep(it6625->reset_gpio, 0); + usleep_range(10000, 11000); + } + + err = it6625_regmap_i2c_init(client, it6625); + if (err) + return err; + + err = it6625_check_device(it6625); + if (err) + return err; + + it6625_init_data(it6625); + + err = it6625_parse_dt(it6625); + if (err) + return err; + + mutex_init(&it6625->it6625_lock); + INIT_DELAYED_WORK(&it6625->hpd_delayed_work, it6625_hpd_delayed_work); + INIT_WORK(&it6625->polling_work, it6625_polling_work); + + if (client->irq) { + err = devm_request_threaded_irq(&client->dev, client->irq, + NULL, it6625_irq_handler, + IRQF_TRIGGER_LOW | + IRQF_ONESHOT | + IRQF_NO_AUTOEN, + "it6625", it6625); + if (err) + return err; + } else { + dev_info(it6625->dev, "no IRQ, falling back to polling"); + timer_setup(&it6625->timer, it6625_irq_poll_timer, 0); + } + + sd = &it6625->sd; + err = it6625_init_v4l2_subdev(it6625); + if (err) + goto err_clean_work_queues; + + err = v4l2_ctrl_handler_setup(sd->ctrl_handler); + if (err) + goto err_clean_hdl; + + it6625->cec_adap = cec_allocate_adapter(&it6625_cec_adap_ops, + it6625, dev_name(it6625->dev), + CEC_CAP_DEFAULTS | + CEC_CAP_MONITOR_ALL | + CEC_CAP_PHYS_ADDR, + CEC_MAX_LOG_ADDRS); + if (IS_ERR(it6625->cec_adap)) { + err = PTR_ERR(it6625->cec_adap); + dev_err(it6625->dev, "%s %d", __func__, __LINE__); + goto err_clean_hdl; + } + + err = cec_register_adapter(it6625->cec_adap, &client->dev); + if (err < 0) { + dev_err(it6625->dev, "%s: failed to register the cec device", __func__); + cec_delete_adapter(it6625->cec_adap); + it6625->cec_adap = NULL; + goto err_clean_hdl; + } + + it6625_debugfs_init(it6625, client); + + it6625_initial_setup(it6625); + it6625_v4l2_sd_ctrl_update(sd); + + err = v4l2_async_register_subdev(sd); + if (err < 0) { + dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err); + goto err_clean_debugfs; + } + + if (client->irq) + enable_irq(client->irq); + else + mod_timer(&it6625->timer, jiffies + msecs_to_jiffies(POLL_INTERVAL_MS)); + + return 0; + +err_clean_debugfs: + debugfs_remove_recursive(it6625->debugfs_dir); + cec_unregister_adapter(it6625->cec_adap); +err_clean_hdl: + media_entity_cleanup(&sd->entity); + v4l2_ctrl_handler_free(&it6625->hdl); + +err_clean_work_queues: + if (!client->irq) + timer_shutdown_sync(&it6625->timer); + cancel_work_sync(&it6625->polling_work); + cancel_delayed_work_sync(&it6625->hpd_delayed_work); + mutex_destroy(&it6625->it6625_lock); + return err; +} + +static void it6625_remove(struct i2c_client *client) +{ + struct v4l2_subdev *sd = i2c_get_clientdata(client); + struct it6625 *it6625 = sd_to_6625(sd); + + v4l2_async_unregister_subdev(sd); + v4l2_device_unregister_subdev(sd); + + if (client->irq) + disable_irq(client->irq); + else + timer_shutdown_sync(&it6625->timer); + + cancel_work_sync(&it6625->polling_work); + cancel_delayed_work_sync(&it6625->hpd_delayed_work); + debugfs_remove_recursive(it6625->debugfs_dir); + cec_unregister_adapter(it6625->cec_adap); + mutex_destroy(&it6625->it6625_lock); + media_entity_cleanup(&sd->entity); + v4l2_ctrl_handler_free(&it6625->hdl); +} + +static const struct i2c_device_id it6625_id[] = { + { .name = "it6625", .driver_data = IT6625_CHIP }, + { .name = "it6626", .driver_data = IT6626_CHIP }, + {} +}; +MODULE_DEVICE_TABLE(i2c, it6625_id); + +static const struct of_device_id it6625_of_match[] = { + { .compatible = "ite,it6625", .data = (void *)IT6625_CHIP }, + { .compatible = "ite,it6626", .data = (void *)IT6626_CHIP }, + {}, +}; +MODULE_DEVICE_TABLE(of, it6625_of_match); + +static struct i2c_driver it6625_driver = { + .driver = { + .name = "it6625", + .of_match_table = of_match_ptr(it6625_of_match), + }, + .probe = it6625_probe, + .remove = it6625_remove, + .id_table = it6625_id, +}; +module_i2c_driver(it6625_driver); + +MODULE_DESCRIPTION("iTE it6625/it6626 HDMI to MIPI CSI bridge driver"); +MODULE_AUTHOR("Hermes Wu <Hermes.wu@ite.com.tw>"); +MODULE_LICENSE("GPL"); -- 2.34.1 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v6 2/2] media: i2c: add driver for ITE IT6625/IT6626 @ 2026-07-22 11:18 ` Hermes Wu 0 siblings, 0 replies; 8+ messages in thread From: Hermes Wu @ 2026-07-22 11:18 UTC (permalink / raw) To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski, Conor Dooley Cc: linux-media, devicetree, linux-kernel, Hermes Wu Add a V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI-to-MIPI CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an HDMI 2.1 input, converting it to a D-PHY (or C/D-PHY on IT6626) MIPI CSI-2 output. The bridge is configured over I2C, exposes an HDMI CEC adapter, and supports EDID read/write, DV timings detection/configuration, and HPD control via the standard V4L2 subdevice pad and video ops. Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw> --- Changes in v6: - Fix two correctness issues found by the automated review of v5: it6625_s_edid() called v4l2_phys_addr_validate() but discarded its return value, so a CEC physical address that failed topology validation (e.g. a gap like 1.0.1.0) still reached cec_s_phys_addr() and got broadcast on the CEC bus instead of being rejected; and it6625_parse_endpoint() pre-set the fwnode endpoint's bus_type to D-PHY before parsing, which (per include/media/v4l2-fwnode.h) makes the parser treat it as a hard requirement and reject any endpoint whose DT bus-type disagrees -- every IT6626 C-PHY device tree failed to probe as a result. One other v5 review finding was checked and is not being acted on: - A concurrent VIDIOC_SUBDEV_S_EDID ioctl racing it6625_remove()'s cancel_delayed_work_sync() could in principle schedule hpd_delayed_work after teardown frees the driver state. Real in the abstract -- no lock serializes an in-flight ioctl against video_unregister_device() -- but the identical unlocked pattern (delayed work scheduled from the s_edid path, remove() doing only cancel_delayed_work_sync()) exists unfixed in tc358743.c and adv7604.c, and this is the same residual risk already dismissed in the v2/v3/v4 rounds of this series for the same reason: no peer driver adds extra synchronization for it either. Changes in v5: - Fix a kernel-stack-memory leak in it6625_edid_ram_show() and it6625_mipi_reg_show(): both printed an uninitialized on-stack register buffer to userspace via debugfs without checking whether the preceding I2C bulk read actually succeeded. - Fix two related format-negotiation bugs: it6625_set_fmt()'s V4L2_SUBDEV_FORMAT_TRY branch never updated fmt->colorspace to match the new fmt->code, and it6625_get_fmt()'s TRY branch computed the returned colorspace from the active hardware state instead of the TRY state's own format, leaking active state into what should be an isolated TRY buffer -- store and read code and colorspace together in sd_state instead. Separately, it6625_set_fmt()'s active-format path released it6625_lock before programming the hardware, letting two concurrent S_FMT calls interleave and leave the hardware programmed for the wrong thread's format; add _locked variants of it6625_enable_stream()/it6625_set_mipi_config() and call them from inside set_fmt()'s existing lock scope so the whole update is now atomic. - Fix it6625_s_edid(): per the VIDIOC_S_EDID spec, blocks == 0 must leave the EDID disabled ("no longer available"), but the driver disabled HPD and then fell through to the normal completion path, which unconditionally re-enabled it -- return immediately instead. Also stop a failed it6625_write_edid() from skipping the HPD re-enable entirely, which left HPD disabled permanently after any single failed S_EDID call. - Take it6625_lock around it6625_mipi_reg_write()'s I2C write, matching every other register access in the driver. - Derive IT6625_CID_HDMI_INFO's string from real state (it6625->csi_format and fps_from_bt_timings()) instead of returning a hardcoded "RGB444 10bit @ 60Hz" regardless of actual signal. - Wire up the previously-unused reset-gpios line in probe(): acquire it via devm_gpiod_get_optional() and toggle it (assert briefly, then deassert and let it settle) before the first I2C access. All found by the sashiko.dev automated review of v4. Three other v4 review findings were checked and are not being acted on: - it6625_mipi_reg_show()'s raw register dump could destructively consume a pending CEC RX message via REG_CEC_RX_DATA (0x20) -- checked with the hardware author: reading REG_CEC_RX_DATA is not destructive, only the explicit write-0 to REG_CEC_RX_DATA_LEN clears state. The v4 changelog's "likely clear-on-read" assumption for this same register was wrong. - devm_kzalloc'd state + subdev devnode UAF-in-the-abstract on unbind -- same reasoning as the v2/v3 rounds: matches tc358743.c/adv7604.c/ adv7842.c exactly, an accepted V4L2-subsystem-wide pattern, not a defect unique to this driver. - Concurrent VIDIOC_SUBDEV_S_EDID racing it6625_remove()'s cancel_delayed_work_sync()/mutex_destroy() -- the same residual risk dismissed in the v3 round: real in the abstract, but no peer driver adds extra synchronization for it either, and the v2 round's fix (unregister the subdev first in remove()) already narrowed this to the same residual risk level every peer driver carries. Changes in v4: - Always acknowledge the CEC RX length register once the RX interrupt branch is entered, even when the reported length is invalid -- previously the ack write only happened on valid lengths, so an invalid length left the interrupt asserted, which could storm the IRQ line. - Fall back to a default format instead of returning -EINVAL from it6625_set_fmt() when the requested mbus code isn't recognized, matching the V4L2 subdev set_fmt convention (cf. adv7604.c). Take it6625_lock around it6625_get_fmt()'s ACTIVE-path reads of csi_format/mbus_fmt_code, which could otherwise observe a torn update from a concurrent set_fmt(). - Fix EDID ioctl spec conformance: it6625_g_edid() now clamps the requested block count and returns success instead of propagating an error when the request exceeds capacity; it6625_s_edid() now returns -E2BIG (not -EINVAL) with blocks set to the maximum, without attempting the write, when the request exceeds capacity; and it6625_read_edid()/it6625_write_edid()'s low-level bounds check now tests start_block and num_blocks individually before summing them, since the sum could otherwise overflow given the unsigned __u32 fields of struct v4l2_edid. - Harden the mipi_reg debugfs handlers: it6625_mipi_reg_write() now always parses from a local offset that starts at 0 instead of the real file offset, since this is a stateless one-write-one-command interface, not a seekable stream; it6625_mipi_reg_show() now takes it6625_lock around its raw register-bank read, matching every other banked-register access in the driver (the dumped range also includes REG_CEC_RX_DATA, which the CEC handler treats as clear-on-read, so an unlocked read could silently steal a pending CEC RX message). - Mark the HDMI info control (IT6625_CID_HDMI_INFO) volatile: it6625_g_volatile_ctrl() already had a live case for it, but the control's v4l2_ctrl_config was missing V4L2_CTRL_FLAG_VOLATILE, so that case was never actually reached and the control's value was never refreshed. All found by the sashiko.dev automated review of v3. Separately, not a review finding: remove a redundant FW-start register trigger from it6625_initial_setup() -- the chip's firmware starts automatically on power-on, so the explicit software trigger was dead weight. Changes in v3: - Finish the banked-register locking from v2: it6625_irq_infoframe_latch() and get_audio_sampling_rate() also read banked registers without it6625_lock; take the lock inside the CEC RX branch across the length read, data read and length-clear together (closing a TOCTOU where the length could go stale under lock contention), and stop the RX branch from using an early return that skipped a simultaneous CEC TX interrupt. - Fix it6625_s_edid() incorrectly treating a successful EDID write as a failure: it6625_write_edid() returns a positive block count on success, not 0, so `if (err)` skipped the CEC phys-addr update and HPD re-enable on every successful write. Pre-existing since v1, not introduced by the v2 blocks==0 handling. - Add real V4L2_SUBDEV_FORMAT_TRY support in get_fmt/set_fmt via sd_state, matching the pattern used by adv7604.c, and lock the csi_format/mbus_fmt_code commit in the ACTIVE path against concurrent S_FMT calls. - Read the chip's real porch/sync-width registers (REG_H_FP_1..REG_V_BP_0) in it6625_get_detected_timings() instead of lumping the whole blanking interval into hsync/vsync, so detected timings can actually match the standard CEA/VESA tables. All found by the sashiko.dev automated review of v2. Four other v2 review findings were checked against the code and are not being acted on: - "timer_container_of() doesn't exist" -- it does (include/linux/timer.h), the driver already builds clean with it. - "devm_kzalloc'd state + subdev devnode risks a UAF on unbind" -- true in the abstract, but it's the exact pattern used by every comparable in-tree driver (tc358743.c, adv7604.c, adv7842.c all devm_kzalloc their main state struct despite setting V4L2_SUBDEV_FL_HAS_DEVNODE too). - "unregistering the subdev before disabling IRQ/work in remove() leaves sd->devnode dangling" -- checked the core: v4l2_device_unregister_subdev() only marks sd->devnode unregistered via video_unregister_device(), it does not free it; the struct is freed later via kref on last close, so the pointer stays valid. - the residual "ioctl reschedules hpd_delayed_work concurrently with remove()" race -- real in the abstract (requires an ioctl already in flight at the exact moment of unbind), but no peer driver adds extra synchronization for this either (none set sd->devnode->lock), and the v2 fix (unregister first in remove()) already narrowed this down to the same residual risk level every peer driver carries. Changes in v2: - Fix premature v4l2_async_register_subdev(): move it to the end of probe() (after ctrl handler, CEC and initial hardware setup), and mirror the unregister order at the top of remove(), closing a use-after-free window where a concurrent ioctl could reschedule hpd_delayed_work during teardown. - Take it6625_lock around the banked register reads in it6625_get_detected_timings() and the CEC RX path in it6625_cec_handler(), which could otherwise race with an in-progress EDID read/write and observe the wrong bank. - Fix it6625_wait_for_status()'s poll interval exceeding the timeout budget, split the CEC RX/TX interrupt handling out of an else-if so simultaneous events aren't dropped, and wait for hardware to latch the CEC transmit trigger bit before releasing the lock. - Stop it6625_set_fmt() from mutating active state on V4L2_SUBDEV_FORMAT_TRY, and handle edid->blocks == 0 in g_edid/s_edid per the V4L2 spec (capacity query / EDID clear). - Zero-initialize the CEC rxmsg and debugfs mipi_reg write buffer to avoid leaking uninitialized stack bytes, and use designated initializers in the i2c_device_id table. All found by the sashiko.dev automated review of v1. --- MAINTAINERS | 7 + drivers/media/i2c/Kconfig | 18 + drivers/media/i2c/Makefile | 1 + drivers/media/i2c/it6625.c | 2216 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 2242 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 15011f5752a994cf1b354f490d6c4e411588df88..2d5b0ee107f9e3255a5365ec0a25167a716b1e6e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13847,6 +13847,13 @@ T: git https://gitlab.freedesktop.org/drm/misc/kernel.git F: Documentation/devicetree/bindings/display/bridge/ite,it66121.yaml F: drivers/gpu/drm/bridge/ite-it66121.c +ITE IT6625 HDMI to MIPI MEDIA DRIVER +M: Hermes Wu <Hermes.Wu@ite.com.tw> +S: Maintained +T: git git://linuxtv.org/media.git +F: Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml +F: drivers/media/i2c/it6625.c + IVTV VIDEO4LINUX DRIVER M: Andy Walls <awalls@md.metrocast.net> L: linux-media@vger.kernel.org diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig index 5d173e0ecf424f2f204f8d426be818e44357f8e4..fb4b24684f7710f90eb399976d11f57701488d26 100644 --- a/drivers/media/i2c/Kconfig +++ b/drivers/media/i2c/Kconfig @@ -1278,6 +1278,24 @@ config VIDEO_ISL7998X Support for Intersil ISL7998x analog to MIPI-CSI2 or BT.656 decoder. +config VIDEO_IT6625 + tristate "IT6625 HDMI to MIPI CSI bridge" + depends on VIDEO_DEV && I2C + depends on OF + select CEC_CORE + select MEDIA_CONTROLLER + select REGMAP_I2C + select V4L2_FWNODE + select VIDEO_V4L2_SUBDEV_API + help + V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI to MIPI + CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and + IT6626 an HDMI 2.1 input, converting it to a MIPI CSI-2 + output. The driver also exposes an HDMI CEC adapter. + + To compile this driver as a module, choose M here: the + module will be called it6625. + config VIDEO_LT6911UXE tristate "Lontium LT6911UXE decoder" depends on ACPI && VIDEO_DEV && I2C diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile index e45359efe0e41e13e3c0869e5ead7d6cf4aca3a7..9bbceaa3d07573958edb8a36bc7f0942f1410a39 100644 --- a/drivers/media/i2c/Makefile +++ b/drivers/media/i2c/Makefile @@ -63,6 +63,7 @@ obj-$(CONFIG_VIDEO_IMX412) += imx412.o obj-$(CONFIG_VIDEO_IMX415) += imx415.o obj-$(CONFIG_VIDEO_IR_I2C) += ir-kbd-i2c.o obj-$(CONFIG_VIDEO_ISL7998X) += isl7998x.o +obj-$(CONFIG_VIDEO_IT6625) += it6625.o obj-$(CONFIG_VIDEO_KS0127) += ks0127.o obj-$(CONFIG_VIDEO_LM3560) += lm3560.o obj-$(CONFIG_VIDEO_LM3646) += lm3646.o diff --git a/drivers/media/i2c/it6625.c b/drivers/media/i2c/it6625.c new file mode 100644 index 0000000000000000000000000000000000000000..a14d36179f426090f1f00d512d1afbef622062e3 --- /dev/null +++ b/drivers/media/i2c/it6625.c @@ -0,0 +1,2216 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * it6625 - ite HDMI to MIPI bridge + */ +#include <linux/bitfield.h> +#include <linux/clk.h> +#include <linux/debugfs.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/hdmi.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_graph.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/timer.h> +#include <linux/v4l2-dv-timings.h> +#include <linux/videodev2.h> +#include <linux/workqueue.h> + +#include <media/cec.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-device.h> +#include <media/v4l2-dv-timings.h> +#include <media/v4l2-event.h> +#include <media/v4l2-fwnode.h> + +static int debug = 3; +module_param(debug, int, 0644); +MODULE_PARM_DESC(debug, "debug level (0-3)"); + +#define REG_CHIP_ID_0 0x00 +#define REG_CHIP_ID_1 0x01 +#define REG_FW_VER_MAJOR 0x03 +#define REG_FW_VER_MINOR 0x04 +#define REG_PROTOCOL_VERSION 0x05 +#define B_PVER_MINOR BIT(0) +#define B_PVER_MAJOR BIT(4) + +#define REG_CMD_SET 0x10 +#define CMD_SET_CEC_LA 0xC0 +#define CMD_SET_CEC_ENABLE 0xC1 + +#define REG_EDID_START 0x20 +#define REG_CEC_RX_DATA 0x20 +#define REG_CEC_TX_DATA 0x30 + +#define REG_H_ACTIVE_1 0x50 +#define REG_H_ACTIVE_0 0x51 +#define REG_V_ACTIVE_1 0x52 +#define REG_V_ACTIVE_0 0x53 +#define REG_H_TOTAL_1 0x54 +#define REG_H_TOTAL_0 0x55 +#define REG_V_TOTAL_1 0x56 +#define REG_V_TOTAL_0 0x57 +#define REG_VID_PCLK 0x58 +#define REG_H_FP_1 0x5C +#define REG_H_FP_0 0x5D +#define REG_H_SW_1 0x5E +#define REG_H_SW_0 0x5F +#define REG_H_BP_1 0x60 +#define REG_H_BP_0 0x61 +#define REG_V_FP_1 0x62 +#define REG_V_FP_0 0x63 +#define REG_V_SW_1 0x64 +#define REG_V_SW_0 0x65 +#define REG_V_BP_1 0x66 +#define REG_V_BP_0 0x67 +#define REG_VID_INFO 0x68 +#define B_INTERLACE BIT(0) +#define B_HSWPOL BIT(1) +#define B_VSWPOL BIT(2) +#define B_PIXEL_REP BIT(4) + +#define REG_VIC 0x69 +#define REG_HDMI_VIDEO_INFO 0x6A +#define B_COLOR_MODE BIT(0) +#define B_COLOR_DEPTH BIT(4) +#define REG_HDMI_AUDIO_INFO1 0x6B +#define B_AUD_FS BIT(0) + +#define REG_HDMI_AUDIO_INFO2 0x6C +#define B_AUD_CH BIT(0) +#define B_AUD_WL BIT(4) + +#define REG_HDMI_AUDIO_INFO3 0x6D +#define B_AUD_TYPE BIT(0) +#define B_AUD_MS BIT(2) +#define B_AUD_3D BIT(3) + +#define REG_AUDIO_FMT 0x6E +#define B_I2S_WL BIT(0) +#define B_I2S_ALN BIT(2) +#define B_I2S_DLY BIT(3) +#define B_I2S_LR BIT(4) +#define B_I2S_SFT BIT(5) +#define B_AUD_OUT_IF BIT(6) + +#define REG_IF_LATCH_HB 0x6F +#define REG_IF_DATA 0x70 +#define REG_EMP_DATA 0xA0 +#define REG_AVI_DATA 0xAE + +#define REG_TX_STATUS 0xE0 + +#define REG_RX_STATUS 0xE2 +#define B_RX_5V BIT(0) +#define B_RX_HPD BIT(1) +#define B_RX_STABLE BIT(2) +#define B_RX_HDMI BIT(3) +#define B_RX_AVMUTE BIT(4) +#define B_RX_AUD_ON BIT(5) + +#define REG_RX_HDCP_STS 0xE3 +#define B_HDCP1_AUTH_START BIT(0) +#define B_HDCP2_AUTH_START BIT(1) +#define B_HDCP_AUTH_DONE BIT(2) +#define B_HDCP_ENC BIT(3) + +#define REG_CEC_STATUS 0xE4 +#define B_CEC_TX_DONE BIT(0) +#define B_CEC_TX_NACK BIT(1) + +#define REG_CEC_RX_DATA_LEN 0xE5 +#define REG_CEC_TX_DATA_LEN 0xE6 + +#define REG_SYS_MIPI_INT 0xEB +#define B_MIPI_OUTPUT_ENABLE BIT(0) +#define B_MIPI_VIDEO_UNSTABLE BIT(1) + +#define REG_RX_INT_STATUS1 0xEC +#define B_HDMI_5V_CHG BIT(0) +#define B_HDMI_VID_CHG BIT(1) +#define B_HDMI_AUD_CHG BIT(2) +#define B_HDMI_CP_CHG BIT(3) +#define B_HDMI_IF_LATCH BIT(4) +#define B_HDMI_NO_IF_LATCH BIT(5) +#define B_HDMI_EMP BIT(6) +#define B_HDMI_NO_EMP BIT(7) + +#define REG_RX_INT_STATUS2 0xED +#define B_HDMI_AVI BIT(0) +#define B_HDMI_NO_AVI BIT(1) +#define B_HDMI_VSIF BIT(2) +#define B_HDMI_NO_VSIF BIT(3) +#define B_HDMI_AVMUTE_CHG BIT(4) + +#define REG_CHIP_CONTROL 0xF0 +#define B_HDMI_RESET BIT(5) +#define B_FW_START BIT(6) + +#define REG_MIPI_CFG 0xF1 +#define M_MIPI_LANE 0x03 +#define B_MIPI_SPLIT BIT(2) +#define B_MIPI_SPLIT_CFG BIT(3) +#define B_MIPI_DPHY BIT(4) +#define B_MIPI_USE_DSI BIT(5) +#define B_MIPI_CONTINU_CLK BIT(6) + +#define REG_MIPI_DATA_TYPE 0xF2 +#define CSI_RGB444 0x20 +#define CSI_RGB555 0x21 +#define CSI_RGB565 0x22 +#define CSI_RGB666 0x23 +#define CSI_RGB888 0x24 +#define CSI_YUV420_8b_L 0x1A +#define CSI_YUV420_8b 0x1C +#define CSI_YUV420_10b 0x1D +#define CSI_YUV422_8b 0x1E +#define CSI_YUV422_10b 0x1F +#define CSI_RGB_10b 0x30 +#define CSI_RGB_12b 0x31 +#define CSI_YUV422_12b 0x32 +#define CSI_YUV420_10b_L 0x33 +#define CSI_YUV420_12b 0x34 +#define CSI_YUV444_8b 0x35 +#define CSI_YUV444_10b 0x36 +#define CSI_YUV444_12b 0x37 + +#define REG_MIPI_CONTROL 0xF3 +#define B_MIPI_OUTPUT BIT(0) + +#define REG_RX_CFG 0xF4 +#define B_MANUAL_HPD BIT(0) +#define B_HPD_HIGH BIT(1) +#define B_HPD_TOGGLE BIT(3) + +#define REG_CSC_CFG 0xF5 +#define B_DYNAMIC_RANGE BIT(0) + +#define REG_MISC_CFG 0xF6 +#define B_BAUD_RATE BIT(0) +#define B_DEBUG_MSG BIT(1) + +#define REG_HPD_DELAY 0xF7 +#define B_DELAY_COUNT BIT(0) +#define B_DELAY_UNIT BIT(7) + +#define REG_INFO_BANK_SEL 0xFD +#define CTL_BANK_EDID_READ 1 +#define CTL_BANK_EDID_WRITE 5 + +#define REG_HOST_CTRL_INT 0xFE +#define B_CMD_SET BIT(4) +#define B_CEC_SEND_DATA BIT(5) +#define B_CONFIG_UPDATE BIT(6) +#define B_IF_BANK BIT(7) + +#define REG_MCU_INTERRUPT 0xFF +#define B_SYS_INT_ACTIVE BIT(0) +#define B_CEC_RX_RECEIVED BIT(1) +#define B_CEC_TX_UPDATE BIT(2) + +#define EDID_NUM_BLOCKS_MAX 4 +#define EDID_BLOCK_SIZE 128 + +#define I2C_MAX_XFER_SIZE 8 +#define POLL_INTERVAL_CEC_MS 10 +#define POLL_INTERVAL_MS 40 + +#define AUD32K 0x03 +#define AUD44K 0x00 +#define AUD48K 0x02 +#define AUD64K 0x0B +#define AUD88K 0x08 +#define AUD96K 0x0A +#define AUD128K 0x2B +#define AUD176K 0x0C +#define AUD192K 0x0E +#define AUD256K 0x1B +#define AUD352K 0x0D +#define AUD384K 0x05 +#define AUD512K 0x3B +#define AUD705K 0x2D +#define AUD768K 0x09 +#define AUD1024K 0x35 +#define AUD1411K 0x1D +#define AUD1536K 0x15 + +enum it6625_chip_type { + IT6625_CHIP = 0, + IT6626_CHIP = 1, +}; + +struct it6625 { + struct device *dev; + struct i2c_client *i2c_client; + struct regmap *it6625_regmap; + enum it6625_chip_type chip_type; + + /* protects concurrent access to the chip's registers and state */ + struct mutex it6625_lock; + + struct v4l2_subdev sd; + struct v4l2_mbus_config_mipi_csi2 bus; + struct video_device *vdev; + struct media_pad pad; + struct v4l2_ctrl_handler hdl; + + /* controls */ + struct v4l2_ctrl *ctrl_5v_detect; + struct v4l2_ctrl *ctrl_audio_sampling_rate; + struct v4l2_ctrl *ctrl_audio_present; + struct v4l2_ctrl *ctrl_link_freq; + struct v4l2_ctrl *ctrl_get_hdmi_info; + struct v4l2_ctrl *ctrl_mipi_config; + + struct delayed_work hpd_delayed_work; + + struct timer_list timer; + struct work_struct polling_work; + + struct v4l2_dv_timings timings; + + u8 csi_lanes; + u8 port_num; + enum v4l2_mbus_type bus_type; + u8 csi_format; + u32 mbus_fmt_code; + + struct gpio_desc *reset_gpio; + + struct cec_adapter *cec_adap; + + struct dentry *debugfs_dir; +}; + +static const s64 it6625_link_freq[] = { 445500000 }; + +static const struct v4l2_dv_timings_cap it6625_timings_cap = { + .type = V4L2_DV_BT_656_1120, + /* keep this initialization for compatibility with GCC < 4.4.6 */ + .reserved = { 0 }, + + V4L2_INIT_BT_TIMINGS(640, 3840, 480, 2160, 27000000, 300000000, + V4L2_DV_BT_STD_CEA861 | V4L2_DV_BT_STD_DMT | + V4L2_DV_BT_STD_GTF | V4L2_DV_BT_STD_CVT, + V4L2_DV_BT_CAP_PROGRESSIVE | V4L2_DV_BT_CAP_INTERLACED | + V4L2_DV_BT_CAP_REDUCED_BLANKING | V4L2_DV_BT_CAP_CUSTOM) +}; + +static const struct it6625_format_info { + u8 csi_format; + u32 mbus_fmt_code; +} it6625_formats[] = { + { CSI_YUV422_8b, MEDIA_BUS_FMT_UYVY8_1X16 }, + { CSI_RGB888, MEDIA_BUS_FMT_RGB888_1X24 }, + { CSI_YUV444_8b, MEDIA_BUS_FMT_YUV8_1X24 }, +}; + +static inline int it6625_csi_format_idx(u8 csi_format) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(it6625_formats); i++) { + if (it6625_formats[i].csi_format == csi_format) + return i; + } + + return -EINVAL; +} + +static inline int it6625_csi_mbus_code_idx(u32 mbus_fmt_code) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(it6625_formats); i++) { + if (it6625_formats[i].mbus_fmt_code == mbus_fmt_code) + return i; + } + + return -EINVAL; +} + +static inline struct it6625 *sd_to_6625(struct v4l2_subdev *sd) +{ + return container_of(sd, struct it6625, sd); +} + +static inline struct it6625 *hdl_to_6625(struct v4l2_ctrl_handler *hdl) +{ + return container_of(hdl, struct it6625, hdl); +} + +static const struct regmap_config it6625_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0xff, + .cache_type = REGCACHE_NONE, + .max_raw_read = I2C_MAX_XFER_SIZE, + .max_raw_write = I2C_MAX_XFER_SIZE, +}; + +static int it6625_regmap_i2c_init(struct i2c_client *client, + struct it6625 *it6625) +{ + it6625->i2c_client = client; + it6625->dev = &client->dev; + + it6625->it6625_regmap = devm_regmap_init_i2c(it6625->i2c_client, + &it6625_regmap_config); + if (IS_ERR(it6625->it6625_regmap)) + return PTR_ERR(it6625->it6625_regmap); + + return 0; +} + +static int it6625_read_byte(struct it6625 *it6625, u8 reg) +{ + unsigned int val; + int err; + struct device *dev = it6625->dev; + + err = regmap_read(it6625->it6625_regmap, reg, &val); + if (err < 0) { + dev_err(dev, "reg[0x%x] read failed err: %d", reg, err); + return err; + } + + return val; +} + +static int it6625_write_byte(struct it6625 *it6625, u8 reg, u8 val) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_write(it6625->it6625_regmap, reg, val); + if (err < 0) { + dev_err(dev, "reg[0x%x] write failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_set_bits(struct it6625 *it6625, u8 reg, u8 mask, u8 val) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_update_bits(it6625->it6625_regmap, reg, mask, val); + if (err < 0) { + dev_err(dev, "reg[0x%x] set bits failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_read_bytes(struct it6625 *it6625, u8 reg, u8 *buf, int len) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_bulk_read(it6625->it6625_regmap, reg, buf, len); + if (err < 0) { + dev_err(dev, "reg[0x%x] read failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_write_bytes(struct it6625 *it6625, u8 reg, u8 *buf, int len) +{ + int err; + struct device *dev = it6625->dev; + + err = regmap_bulk_write(it6625->it6625_regmap, reg, buf, len); + if (err < 0) { + dev_err(dev, "reg[0x%x] write failed err: %d", reg, err); + return err; + } + + return 0; +} + +static int it6625_wait_for_status(struct it6625 *it6625, u8 reg, u8 val, + int timeout_ms) +{ + struct device *dev = it6625->dev; + int status; + int rval; + int sleep_ms = 10; + int timeout_round_ms = DIV_ROUND_UP(timeout_ms, sleep_ms) * sleep_ms; + + status = read_poll_timeout(it6625_read_byte, rval, rval == val, + sleep_ms * 1000, + timeout_round_ms * 1000, + false, it6625, reg); + + dev_info(dev, "%s status = %d %d", __func__, status, (int)rval); + if (status < 0) { + dev_err(dev, "%s err status = %d", __func__, status); + return -ETIMEDOUT; + } + + return 0; +} + +static void it6625_write_command(struct it6625 *it6625, u8 *cmds, int cmd_len) +{ + it6625_write_bytes(it6625, REG_CMD_SET, cmds, cmd_len); + it6625_write_byte(it6625, REG_HOST_CTRL_INT, B_CMD_SET); + it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static int it6625_update_config(struct it6625 *it6625) +{ + int err; + + err = it6625_set_bits(it6625, REG_HOST_CTRL_INT, + B_CONFIG_UPDATE, B_CONFIG_UPDATE); + if (err < 0) + return err; + + return it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static int it6625_set_bank(struct it6625 *it6625, u8 bank) +{ + int err; + + err = it6625_write_byte(it6625, REG_INFO_BANK_SEL, bank); + if (err < 0) + return err; + + err = it6625_write_byte(it6625, REG_HOST_CTRL_INT, B_IF_BANK); + if (err < 0) + return err; + + return it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static inline bool is_hdmi(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? false : (val & B_RX_HDMI); +} + +static inline bool hdmi_5v_power_present(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? false : (val & B_RX_5V); +} + +static inline bool no_signal(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? true : !(val & B_RX_STABLE); +} + +static inline bool audio_present(struct it6625 *it6625) +{ + int val; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + return (val < 0) ? false : (val & B_RX_AUD_ON); +} + +static int get_audio_sampling_rate(struct it6625 *it6625) +{ + u8 fs_id; + int i, freq = 0; + const struct fs_id_map { + u8 fs_id; + u16 freq; + } s_fsid_map[] = { + { AUD32K, 32 }, + { AUD44K, 44 }, + { AUD48K, 48 }, + { AUD64K, 64 }, + { AUD88K, 88 }, + { AUD96K, 96 }, + + { AUD128K, 128 }, + { AUD176K, 176 }, + { AUD192K, 192 }, + { AUD256K, 256 }, + { AUD352K, 352 }, + { AUD384K, 384 }, + + { AUD512K, 512 }, + { AUD705K, 705 }, + { AUD768K, 768 }, + { AUD1024K, 1024 }, + { AUD1411K, 1411 }, + { AUD1536K, 1536 }, + }; + + if (no_signal(it6625) || !audio_present(it6625)) + return 0; + + guard(mutex)(&it6625->it6625_lock); + + fs_id = it6625_read_byte(it6625, REG_HDMI_AUDIO_INFO1); + + for (i = 0; i < ARRAY_SIZE(s_fsid_map); i++) { + if (s_fsid_map[i].fs_id == fs_id) { + freq = s_fsid_map[i].freq * 1000; + break; + } + } + + return freq; +} + +static u64 it6625_get_pclk(struct it6625 *it6625) +{ + u32 pclk; + u8 ck[4]; + int ret; + + ret = it6625_read_bytes(it6625, REG_VID_PCLK, ck, 4); + if (ret < 0) { + dev_err(it6625->dev, "failed to read pixel clock"); + return 0; + } + + pclk = ck[0]; + pclk <<= 8; + pclk |= ck[1]; + pclk <<= 8; + pclk |= ck[2]; + pclk <<= 8; + pclk |= ck[3]; + + v4l2_dbg(1, debug, &it6625->sd, "%s: pclk=%u (%08x)", + __func__, pclk, pclk); + + return (u64)pclk * 1000; +} + +static int it6625_read_edid(struct it6625 *it6625, u8 *edid, int start_block, + int num_blocks) +{ + int i, bank_ctrl, err = 0; + struct device *dev = it6625->dev; + + if (!edid) { + dev_err(dev, "edid buffer is NULL"); + return -EINVAL; + } + + if (start_block < 0 || num_blocks <= 0 || + start_block > EDID_NUM_BLOCKS_MAX || + num_blocks > EDID_NUM_BLOCKS_MAX || + start_block + num_blocks > EDID_NUM_BLOCKS_MAX) { + dev_err(dev, + "invalid block range: start_block=%d, num_blocks=%d", + start_block, num_blocks); + return -EINVAL; + } + + guard(mutex)(&it6625->it6625_lock); + for (i = 0; i < num_blocks; i++) { + bank_ctrl = CTL_BANK_EDID_READ + start_block + i; + err = it6625_set_bank(it6625, bank_ctrl); + if (err < 0) + break; + + err = it6625_read_bytes(it6625, REG_EDID_START, + edid + (i * 128), 128); + if (err < 0) + break; + } + + it6625_set_bank(it6625, 0); + + return err < 0 ? err : num_blocks; +} + +static int it6625_write_edid(struct it6625 *it6625, u8 *edid, int start_block, + int num_blocks) +{ + int i, bank_ctrl, err = 0; + struct device *dev = it6625->dev; + + if (start_block < 0 || num_blocks <= 0 || + start_block > EDID_NUM_BLOCKS_MAX || + num_blocks > EDID_NUM_BLOCKS_MAX || + start_block + num_blocks > EDID_NUM_BLOCKS_MAX) { + dev_err(dev, + "invalid block range: start_block=%d, num_blocks=%d", + start_block, num_blocks); + return -EINVAL; + } + + guard(mutex)(&it6625->it6625_lock); + for (i = 0; i < num_blocks; i++) { + bank_ctrl = CTL_BANK_EDID_WRITE + start_block + i; + err = it6625_set_bank(it6625, bank_ctrl); + if (err < 0) + break; + + err = it6625_write_bytes(it6625, REG_EDID_START, + edid + (i * 128), 128); + if (err < 0) + break; + + err = it6625_update_config(it6625); + if (err < 0) + break; + } + + it6625_set_bank(it6625, 0); + + return err < 0 ? err : num_blocks; +} + +static void it6625_enable_auto_hpd(struct it6625 *it6625) +{ + dev_dbg(it6625->dev, "%s: auto HPD", __func__); + guard(mutex)(&it6625->it6625_lock); + it6625_set_bits(it6625, REG_RX_CFG, 0x03, 0x00); + it6625_update_config(it6625); +} + +static void it6625_disable_hpd(struct it6625 *it6625) +{ + cancel_delayed_work_sync(&it6625->hpd_delayed_work); + + guard(mutex)(&it6625->it6625_lock); + it6625_set_bits(it6625, REG_RX_CFG, 0x03, 0x01); + it6625_update_config(it6625); +} + +static void it6625_enable_hpd(struct it6625 *it6625) +{ + schedule_delayed_work(&it6625->hpd_delayed_work, + msecs_to_jiffies(100)); +} + +static void it6625_hpd_delayed_work(struct work_struct *work) +{ + struct it6625 *it6625 = container_of(work, + struct it6625, hpd_delayed_work.work); + int val = 0; + + guard(mutex)(&it6625->it6625_lock); + it6625_set_bits(it6625, REG_RX_CFG, 0x03, val); + it6625_update_config(it6625); +} + +static int it6625_get_detected_timings(struct it6625 *it6625, + struct v4l2_dv_timings *timings) +{ + struct v4l2_bt_timings *bt = &timings->bt; + int val; + unsigned int width, height; + u8 buffer[4]; + u8 buffer2[12]; + + if (no_signal(it6625)) { + dev_err(it6625->dev, "no signal detected"); + return -ENOLINK; + } + + guard(mutex)(&it6625->it6625_lock); + + memset(timings, 0, sizeof(struct v4l2_dv_timings)); + timings->type = V4L2_DV_BT_656_1120; + val = it6625_read_byte(it6625, REG_VID_INFO); + if (val < 0) { + dev_err(it6625->dev, "failed to read video info"); + return -EIO; + } + + bt->interlaced = val & B_INTERLACE ? + V4L2_DV_INTERLACED : V4L2_DV_PROGRESSIVE; + + if (it6625_read_bytes(it6625, REG_H_ACTIVE_1, buffer, 4) < 0) + return -EIO; + + width = ((buffer[0] & 0xff) << 8) + buffer[1]; + height = ((buffer[2] & 0xff) << 8) + buffer[3]; + + bt->width = width; + bt->height = height; + + if (it6625_read_bytes(it6625, REG_H_FP_1, buffer2, 12) < 0) + return -EIO; + + bt->hfrontporch = ((buffer2[0] & 0xff) << 8) + buffer2[1]; + bt->hsync = ((buffer2[2] & 0xff) << 8) + buffer2[3]; + bt->hbackporch = ((buffer2[4] & 0xff) << 8) + buffer2[5]; + bt->vfrontporch = ((buffer2[6] & 0xff) << 8) + buffer2[7]; + bt->vsync = ((buffer2[8] & 0xff) << 8) + buffer2[9]; + bt->vbackporch = ((buffer2[10] & 0xff) << 8) + buffer2[11]; + + bt->pixelclock = it6625_get_pclk(it6625); + if (bt->interlaced == V4L2_DV_INTERLACED) { + bt->height *= 2; + bt->il_vsync = bt->vsync + 1; + bt->pixelclock /= 2; + } + + return 0; +} + +static void it6625_show_avi_infoframe(struct it6625 *it6625) +{ + struct device *dev = it6625->dev; + union hdmi_infoframe frame; + u8 buffer[HDMI_INFOFRAME_SIZE(AVI)]; + u8 ver, len; + int ret; + + if (!is_hdmi(it6625)) { + dev_err(dev, "not HDMI signal, skip AVI infoframe log"); + return; + } + + ret = it6625_read_bytes(it6625, REG_AVI_DATA, buffer + 1, + HDMI_INFOFRAME_SIZE(AVI) - 1); + if (ret < 0) { + dev_err(dev, "failed to read AVI infoframe data"); + return; + } + + len = buffer[1]; + ver = buffer[2]; + + buffer[0] = HDMI_INFOFRAME_TYPE_AVI; + buffer[1] = ver; + buffer[2] = len; + + ret = hdmi_infoframe_unpack(&frame, buffer, sizeof(buffer)); + if (ret < 0) { + dev_err(dev, "unpack of AVI infoframe failed"); + return; + } + + hdmi_infoframe_log(KERN_INFO, dev, &frame); +} + +static int it6625_s_ctrl_detect_hdmi_5v(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + return v4l2_ctrl_s_ctrl(it6625->ctrl_5v_detect, + hdmi_5v_power_present(it6625)); +} + +static int it6625_s_ctrl_audio_sampling_rate(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + return v4l2_ctrl_s_ctrl(it6625->ctrl_audio_sampling_rate, + get_audio_sampling_rate(it6625)); +} + +static int it6625_s_ctrl_audio_present(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + return v4l2_ctrl_s_ctrl(it6625->ctrl_audio_present, + audio_present(it6625)); +} + +static int it6625_v4l2_sd_ctrl_update(struct v4l2_subdev *sd) +{ + int ret = 0; + + ret |= it6625_s_ctrl_detect_hdmi_5v(sd); + ret |= it6625_s_ctrl_audio_sampling_rate(sd); + ret |= it6625_s_ctrl_audio_present(sd); + + return 0; +} + +static void it6625_enable_stream_locked(struct it6625 *it6625, bool enable) +{ + struct v4l2_subdev *sd = &it6625->sd; + int val; + + lockdep_assert_held(&it6625->it6625_lock); + + v4l2_dbg(3, debug, sd, "%s: %sable", + __func__, enable ? "en" : "dis"); + + val = enable ? B_MIPI_OUTPUT : 0; + it6625_set_bits(it6625, REG_MIPI_CONTROL, B_MIPI_OUTPUT, val); + it6625_update_config(it6625); +} + +static void it6625_enable_stream(struct it6625 *it6625, bool enable) +{ + guard(mutex)(&it6625->it6625_lock); + it6625_enable_stream_locked(it6625, enable); +} + +static void it6625_set_mipi_config_locked(struct it6625 *it6625, u32 cfg_val) +{ + u8 mipi_data_type; + + lockdep_assert_held(&it6625->it6625_lock); + + dev_dbg(it6625->dev, "mipi_data_type = 0x%x", cfg_val); + + mipi_data_type = cfg_val & 0xFF; + it6625_write_byte(it6625, REG_MIPI_DATA_TYPE, mipi_data_type); + it6625_update_config(it6625); +} + +static void it6625_set_mipi_config(struct it6625 *it6625, u32 cfg_val) +{ + guard(mutex)(&it6625->it6625_lock); + it6625_set_mipi_config_locked(it6625, cfg_val); +} + +static inline unsigned int fps_from_bt_timings(const struct v4l2_bt_timings *t) +{ + if (!V4L2_DV_BT_FRAME_HEIGHT(t) || !V4L2_DV_BT_FRAME_WIDTH(t)) + return 0; + + return DIV_ROUND_CLOSEST((unsigned int)t->pixelclock, + V4L2_DV_BT_FRAME_HEIGHT(t) * + V4L2_DV_BT_FRAME_WIDTH(t)); +} + +static void it6625_initial_setup(struct it6625 *it6625) +{ + int val = 0; + + /* + * REG_MIPI_CFG[0:2] lane count field: 1 lane -> 0, 2 lanes -> 1, + * 3 lanes (C-PHY only) -> 3, 4 lanes (D-PHY only) -> 3. + */ + switch (it6625->csi_lanes) { + case 1: + val = FIELD_PREP(M_MIPI_LANE, 0); + break; + case 2: + val = FIELD_PREP(M_MIPI_LANE, 1); + break; + default: + val = FIELD_PREP(M_MIPI_LANE, 3); + break; + } + + if (it6625->bus_type == V4L2_MBUS_CSI2_DPHY) + val |= FIELD_PREP(B_MIPI_DPHY, 1); + + if (it6625->port_num == 2) + val |= FIELD_PREP(B_MIPI_SPLIT, 1); + + it6625_write_byte(it6625, REG_MIPI_CFG, val); + it6625_write_byte(it6625, REG_MIPI_DATA_TYPE, it6625->csi_format); + it6625_write_byte(it6625, REG_MIPI_CONTROL, 0x00); + it6625_write_byte(it6625, REG_RX_CFG, 0x00); + + it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CONFIG_UPDATE, B_CONFIG_UPDATE); + it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); +} + +static void dump_cec_msg(struct it6625 *it6625, struct cec_msg *msg, char *str) +{ + int i; + + dev_dbg(it6625->dev, "%s: from %d to %d len=%d", str, + msg->msg[0] >> 4, msg->msg[0] & 0x0f, msg->len); + for (i = 1; i < msg->len; i++) + dev_dbg(it6625->dev, " %02x", msg->msg[i]); +} + +static int it6625_cec_adap_enable(struct cec_adapter *adap, bool enable) +{ + struct it6625 *it6625 = adap->priv; + u8 cmds[2]; + + cmds[0] = CMD_SET_CEC_ENABLE; + cmds[1] = enable ? 1 : 0; + guard(mutex)(&it6625->it6625_lock); + it6625_write_command(it6625, cmds, sizeof(cmds)); + + return 0; +} + +static int it6625_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr) +{ + struct it6625 *it6625 = adap->priv; + + dev_dbg(it6625->dev, "%s: la=%d", __func__, log_addr); + + if (log_addr != CEC_LOG_ADDR_INVALID) { + u8 cmds[2] = {CMD_SET_CEC_LA, log_addr}; + + guard(mutex)(&it6625->it6625_lock); + it6625_write_command(it6625, cmds, sizeof(cmds)); + } + + return 0; +} + +static int it6625_cec_adap_transmit(struct cec_adapter *adap, u8 attempts, + u32 signal_free_time, struct cec_msg *msg) +{ + struct it6625 *it6625 = adap->priv; + + dump_cec_msg(it6625, msg, "CEC_TX"); + + guard(mutex)(&it6625->it6625_lock); + it6625_write_bytes(it6625, REG_CEC_TX_DATA, msg->msg, msg->len); + it6625_write_byte(it6625, REG_CEC_TX_DATA_LEN, msg->len); + it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CEC_SEND_DATA, B_CEC_SEND_DATA); + it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25); + + return 0; +} + +static const struct cec_adap_ops it6625_cec_adap_ops = { + .adap_enable = it6625_cec_adap_enable, + .adap_log_addr = it6625_cec_adap_log_addr, + .adap_transmit = it6625_cec_adap_transmit, +}; + +static void it6625_cec_handler(struct it6625 *it6625, u8 intstatus) +{ + struct cec_msg rxmsg = {}; + int val = 0; + + if (intstatus & B_CEC_RX_RECEIVED) { + scoped_guard(mutex, &it6625->it6625_lock) { + val = it6625_read_byte(it6625, REG_CEC_RX_DATA_LEN); + if (val > 0 && val <= 16) + it6625_read_bytes(it6625, REG_CEC_RX_DATA, &rxmsg.msg[0], val); + it6625_write_byte(it6625, REG_CEC_RX_DATA_LEN, 0); + } + + if (val > 0 && val <= 16) { + rxmsg.len = val; + cec_received_msg(it6625->cec_adap, &rxmsg); + dump_cec_msg(it6625, &rxmsg, "CEC_RX"); + } else { + dev_err(it6625->dev, "invalid CEC RX length %d", val); + } + } + + if (intstatus & B_CEC_TX_UPDATE) { + val = it6625_read_byte(it6625, REG_CEC_STATUS); + if (val < 0) { + dev_err(it6625->dev, "read CEC status failed"); + return; + } + + if (val & BIT(0)) { + cec_transmit_attempt_done(it6625->cec_adap, + CEC_TX_STATUS_OK); + } else if (val & BIT(1)) { + cec_transmit_attempt_done(it6625->cec_adap, + CEC_TX_STATUS_NACK); + } else { + dev_err(it6625->dev, "unknown CEC status %02X", val); + cec_transmit_attempt_done(it6625->cec_adap, + CEC_TX_STATUS_NACK); + } + } +} + +static void it6625_irq_format_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + struct v4l2_dv_timings timings; + const struct v4l2_event it6625_ev_fmt = { + .type = V4L2_EVENT_SOURCE_CHANGE, + .u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION, + }; + int ret; + + if (no_signal(it6625)) + return; + + ret = it6625_get_detected_timings(it6625, &timings); + if (ret < 0) { + v4l2_dbg(1, debug, sd, "Failed to get detected timings"); + return; + } + + if (sd->devnode) + v4l2_subdev_notify_event(sd, &it6625_ev_fmt); +} + +static void it6625_irq_hdmi_audio_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + + it6625_s_ctrl_audio_sampling_rate(sd); + it6625_s_ctrl_audio_present(sd); +} + +static void it6625_get_timings(struct it6625 *it6625, + struct v4l2_dv_timings *timings) +{ + guard(mutex)(&it6625->it6625_lock); + *timings = it6625->timings; +} + +static void it6625_clear_timings(struct it6625 *it6625) +{ + guard(mutex)(&it6625->it6625_lock); + memset(&it6625->timings, 0, sizeof(it6625->timings)); +} + +static void it6625_irq_hdmi_5v_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + + it6625_clear_timings(it6625); + it6625_v4l2_sd_ctrl_update(sd); +} + +static void it6625_irq_hdcp_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + u8 cp_sts; + + cp_sts = it6625_read_byte(it6625, REG_RX_HDCP_STS); + v4l2_info(sd, "HDCP change to %02X", cp_sts); +} + +static void it6625_irq_infoframe_latch(struct it6625 *it6625) +{ + u8 info_frame[32]; + + guard(mutex)(&it6625->it6625_lock); + + it6625_read_bytes(it6625, REG_IF_DATA, info_frame, 31); +} + +static void it6625_irq_emata_packet_latch(struct it6625 *it6625) +{ + u8 extend_packet[14]; + + it6625_read_bytes(it6625, REG_EMP_DATA, extend_packet, 13); +} + +static void it6625_irq_avmute_change(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + int val; + u8 avmute; + + val = it6625_read_byte(it6625, REG_RX_STATUS); + if (val < 0) + return; + + avmute = (val & B_RX_AVMUTE) ? 1 : 0; + v4l2_info(sd, "AVMute change to %d", avmute); +} + +static int it6625_interrupt_handler(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + int val, int_sts1 = 0, int_sts2 = 0; + + val = it6625_read_byte(it6625, REG_MCU_INTERRUPT); + if (val <= 0) + return 0; + + it6625_write_byte(it6625, REG_MCU_INTERRUPT, val); + + v4l2_dbg(1, debug, sd, "%s: INT = 0x%02X", __func__, val); + + if (val & (B_CEC_RX_RECEIVED | B_CEC_TX_UPDATE) && it6625->cec_adap) + it6625_cec_handler(it6625, val & (B_CEC_RX_RECEIVED | B_CEC_TX_UPDATE)); + + if (val & B_SYS_INT_ACTIVE) { + int_sts1 = it6625_read_byte(it6625, REG_RX_INT_STATUS1); + int_sts2 = it6625_read_byte(it6625, REG_RX_INT_STATUS2); + v4l2_dbg(1, debug, sd, "%s: hdmi_int = 0x%02X 0x%02X", + __func__, int_sts1, int_sts2); + + if (int_sts1 >= 0) { + it6625_write_byte(it6625, REG_RX_INT_STATUS1, 0x00); + if (int_sts1 & B_HDMI_5V_CHG) + it6625_irq_hdmi_5v_change(it6625); + + if (int_sts1 & B_HDMI_VID_CHG) + it6625_irq_format_change(it6625); + + if (int_sts1 & B_HDMI_AUD_CHG) + it6625_irq_hdmi_audio_change(it6625); + + if (int_sts1 & B_HDMI_CP_CHG) + it6625_irq_hdcp_change(it6625); + + if (int_sts1 & B_HDMI_IF_LATCH) + it6625_irq_infoframe_latch(it6625); + + if (int_sts1 & B_HDMI_EMP) + it6625_irq_emata_packet_latch(it6625); + } + + if (int_sts2 >= 0) { + it6625_write_byte(it6625, REG_RX_INT_STATUS2, 0x00); + + if (int_sts2 & B_HDMI_AVI) + it6625_show_avi_infoframe(it6625); + + if (int_sts2 & B_HDMI_NO_AVI) + v4l2_info(sd, "AVI-info stopped"); + + if (int_sts2 & B_HDMI_AVMUTE_CHG) + it6625_irq_avmute_change(it6625); + } + } + + return 0; +} + +static irqreturn_t it6625_irq_handler(int unused, void *data) +{ + struct it6625 *it6625 = data; + + it6625_interrupt_handler(it6625); + return IRQ_HANDLED; +} + +static void it6625_irq_poll_timer(struct timer_list *t) +{ + struct it6625 *it6625 = timer_container_of(it6625, t, timer); + unsigned int msecs; + + schedule_work(&it6625->polling_work); + /* + * If CEC is present, then we need to poll more frequently, + * otherwise we will miss CEC messages. + */ + msecs = it6625->cec_adap ? POLL_INTERVAL_CEC_MS : POLL_INTERVAL_MS; + mod_timer(&it6625->timer, jiffies + msecs_to_jiffies(msecs)); +} + +static void it6625_polling_work(struct work_struct *work) +{ + struct it6625 *it6625 = container_of(work, struct it6625, + polling_work); + + it6625_interrupt_handler(it6625); +} + +static int it6625_log_status(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + struct v4l2_dv_timings timings, configured_timings; + + if (it6625_get_detected_timings(it6625, &timings)) + v4l2_info(sd, "No video detected"); + else + v4l2_print_dv_timings(sd->name, "Detected format: ", &timings, + true); + + it6625_get_timings(it6625, &configured_timings); + v4l2_print_dv_timings(sd->name, "Configured format: ", + &configured_timings, true); + + it6625_show_avi_infoframe(it6625); + + return 0; +} + +static int it6625_isr(struct v4l2_subdev *sd, u32 status, bool *handled) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + schedule_work(&it6625->polling_work); + *handled = true; + + return 0; +} + +static int it6625_subscribe_event(struct v4l2_subdev *sd, struct v4l2_fh *fh, + struct v4l2_event_subscription *sub) +{ + switch (sub->type) { + case V4L2_EVENT_SOURCE_CHANGE: + return v4l2_src_change_event_subdev_subscribe(sd, fh, sub); + case V4L2_EVENT_CTRL: + return v4l2_ctrl_subdev_subscribe_event(sd, fh, sub); + default: + return -EINVAL; + } +} + +static int it6625_g_input_status(struct v4l2_subdev *sd, u32 *status) +{ + struct it6625 *it6625 = sd_to_6625(sd); + bool val; + + val = no_signal(it6625); + *status = 0; + *status |= val ? V4L2_IN_ST_NO_SIGNAL : 0; + *status |= val ? V4L2_IN_ST_NO_SYNC : 0; + + v4l2_dbg(1, debug, sd, "%s: status = 0x%x", __func__, *status); + + return 0; +} + +static int +it6625_update_timings_if_changed(struct it6625 *it6625, + const struct v4l2_dv_timings *timings) +{ + int ret; + + guard(mutex)(&it6625->it6625_lock); + if (v4l2_match_dv_timings(&it6625->timings, timings, 0, false)) { + ret = 0; + } else if (!v4l2_valid_dv_timings(timings, &it6625_timings_cap, + NULL, NULL)) { + ret = -ERANGE; + } else { + it6625->timings = *timings; + ret = 1; + } + + return ret; +} + +static int it6625_s_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int ret; + + if (!timings) + return -EINVAL; + + if (debug) + v4l2_print_dv_timings(sd->name, __func__, timings, false); + + ret = it6625_update_timings_if_changed(it6625, timings); + if (ret == -ERANGE) { + v4l2_dbg(1, debug, sd, "%s: timings out of range", __func__); + return ret; + } + + if (ret == 0) { + v4l2_dbg(1, debug, sd, "%s: no change", __func__); + return 0; + } + + it6625_enable_stream(it6625, false); + + return 0; +} + +static int it6625_g_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + it6625_get_timings(it6625, timings); + + return 0; +} + +static int it6625_enum_dv_timings(struct v4l2_subdev *sd, + struct v4l2_enum_dv_timings *timings) +{ + if (timings->pad != 0) + return -EINVAL; + + return v4l2_enum_dv_timings_cap(timings, + &it6625_timings_cap, NULL, NULL); +} + +static int it6625_query_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int ret; + + ret = it6625_get_detected_timings(it6625, timings); + if (ret) + return ret; + + if (debug) + v4l2_print_dv_timings(sd->name, __func__, timings, false); + + if (!v4l2_valid_dv_timings(timings, &it6625_timings_cap, NULL, NULL)) { + v4l2_dbg(1, debug, sd, "%s: timings out of range", __func__); + return -ERANGE; + } + + return 0; +} + +static int it6625_dv_timings_cap(struct v4l2_subdev *sd, + struct v4l2_dv_timings_cap *cap) +{ + if (cap->pad != 0) + return -EINVAL; + + *cap = it6625_timings_cap; + + return 0; +} + +static int it6625_s_stream(struct v4l2_subdev *sd, int enable) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + it6625_enable_stream(it6625, enable); + return 0; +} + +static int it6625_enum_mbus_code(struct v4l2_subdev *sd, + struct v4l2_subdev_state *sd_state, + struct v4l2_subdev_mbus_code_enum *code) +{ + dev_dbg(sd->dev, "%s: index=%d", __func__, code->index); + + if (code->index >= ARRAY_SIZE(it6625_formats)) + return -EINVAL; + + dev_dbg(sd->dev, "%s: code=0x%08x", __func__, + it6625_formats[code->index].mbus_fmt_code); + code->code = it6625_formats[code->index].mbus_fmt_code; + + return 0; +} + +static int it6625_get_mbus_config(struct v4l2_subdev *sd, + unsigned int pad, + struct v4l2_mbus_config *cfg) +{ + struct it6625 *it6625 = sd_to_6625(sd); + + if (pad != 0) + return -EINVAL; + + cfg->type = it6625->bus_type; + cfg->bus.mipi_csi2.flags = 0; + cfg->bus.mipi_csi2.num_data_lanes = it6625->csi_lanes; + + return 0; +} + +static int it6625_pad_s_dv_timings(struct v4l2_subdev *sd, unsigned int pad, + struct v4l2_dv_timings *timings) +{ + if (pad != 0) + return -EINVAL; + + return it6625_s_dv_timings(sd, timings); +} + +static int it6625_pad_g_dv_timings(struct v4l2_subdev *sd, unsigned int pad, + struct v4l2_dv_timings *timings) +{ + if (pad != 0) + return -EINVAL; + + return it6625_g_dv_timings(sd, timings); +} + +static int it6625_pad_query_dv_timings(struct v4l2_subdev *sd, + unsigned int pad, + struct v4l2_dv_timings *timings) +{ + if (pad != 0) + return -EINVAL; + + return it6625_query_dv_timings(sd, timings); +} + +static inline u32 format_to_colorspace(u8 csi_format) +{ + switch (csi_format) { + case CSI_RGB444: + case CSI_RGB555: + case CSI_RGB565: + case CSI_RGB666: + case CSI_RGB888: + case CSI_RGB_10b: + case CSI_RGB_12b: + return V4L2_COLORSPACE_SRGB; + case CSI_YUV420_8b_L: + case CSI_YUV420_8b: + case CSI_YUV420_10b: + case CSI_YUV422_8b: + case CSI_YUV422_10b: + case CSI_YUV422_12b: + case CSI_YUV420_10b_L: + case CSI_YUV420_12b: + case CSI_YUV444_8b: + case CSI_YUV444_10b: + case CSI_YUV444_12b: + return V4L2_COLORSPACE_REC709; + default: + return 0; + } +} + +static const char *it6625_csi_format_name(u8 csi_format) +{ + switch (csi_format) { + case CSI_YUV422_8b: + return "YUV422 8bit"; + case CSI_RGB888: + return "RGB888 8bit"; + case CSI_YUV444_8b: + return "YUV444 8bit"; + default: + return "unknown"; + } +} + +static int it6625_get_fmt(struct v4l2_subdev *sd, + struct v4l2_subdev_state *sd_state, + struct v4l2_subdev_format *format) +{ + struct it6625 *it6625 = sd_to_6625(sd); + struct v4l2_dv_timings timings; + + if (format->pad != 0) + return -EINVAL; + + it6625_get_timings(it6625, &timings); + format->format.width = timings.bt.width; + format->format.height = timings.bt.height; + format->format.field = timings.bt.interlaced == V4L2_DV_INTERLACED ? + V4L2_FIELD_INTERLACED : V4L2_FIELD_NONE; + + if (format->which == V4L2_SUBDEV_FORMAT_TRY) { + struct v4l2_mbus_framefmt *fmt; + + fmt = v4l2_subdev_state_get_format(sd_state, format->pad); + format->format.code = fmt->code; + format->format.colorspace = fmt->colorspace; + } else { + scoped_guard(mutex, &it6625->it6625_lock) { + format->format.colorspace = + format_to_colorspace(it6625->csi_format); + format->format.code = it6625->mbus_fmt_code; + } + } + + return 0; +} + +static int it6625_set_fmt(struct v4l2_subdev *sd, + struct v4l2_subdev_state *sd_state, + struct v4l2_subdev_format *format) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int ret; + u32 mbus_fmt_code = format->format.code; + + ret = it6625_get_fmt(sd, sd_state, format); + format->format.code = mbus_fmt_code; + + if (ret) + return ret; + + ret = it6625_csi_mbus_code_idx(mbus_fmt_code); + + if (ret < 0) { + v4l2_dbg(1, debug, sd, + "%s: unsupported format code 0x%x, falling back to default", + __func__, mbus_fmt_code); + ret = 0; + mbus_fmt_code = it6625_formats[ret].mbus_fmt_code; + format->format.code = mbus_fmt_code; + } + + if (format->which == V4L2_SUBDEV_FORMAT_TRY) { + struct v4l2_mbus_framefmt *fmt; + + fmt = v4l2_subdev_state_get_format(sd_state, format->pad); + fmt->code = format->format.code; + fmt->colorspace = format_to_colorspace(it6625_formats[ret].csi_format); + format->format.colorspace = fmt->colorspace; + v4l2_dbg(1, debug, sd, "%s: try format code = 0x%x", + __func__, format->format.code); + return 0; + } + + scoped_guard(mutex, &it6625->it6625_lock) { + it6625->csi_format = it6625_formats[ret].csi_format; + it6625->mbus_fmt_code = format->format.code; + it6625_enable_stream_locked(it6625, false); + it6625_set_mipi_config_locked(it6625, it6625->csi_format); + } + + return 0; +} + +static int it6625_g_edid(struct v4l2_subdev *sd, + struct v4l2_subdev_edid *edid) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int err; + + if (edid->pad != 0) + return -EINVAL; + + memset(edid->reserved, 0, sizeof(edid->reserved)); + + if (edid->blocks == 0) { + edid->blocks = EDID_NUM_BLOCKS_MAX; + return 0; + } + + if (edid->start_block >= EDID_NUM_BLOCKS_MAX) { + edid->blocks = 0; + return 0; + } + + if (edid->blocks > EDID_NUM_BLOCKS_MAX - edid->start_block) + edid->blocks = EDID_NUM_BLOCKS_MAX - edid->start_block; + + err = it6625_read_edid(it6625, edid->edid, edid->start_block, + edid->blocks); + + return (err < 0) ? err : 0; +} + +static int it6625_s_edid(struct v4l2_subdev *sd, + struct v4l2_subdev_edid *edid) +{ + struct it6625 *it6625 = sd_to_6625(sd); + int err = 0; + u16 pa; + int pa_err; + + if (edid->pad != 0) { + v4l2_err(sd, "invalid pad %d", edid->pad); + return -EINVAL; + } + + memset(edid->reserved, 0, sizeof(edid->reserved)); + + if (edid->blocks > EDID_NUM_BLOCKS_MAX) { + v4l2_err(sd, "too many edid blocks: %d", edid->blocks); + edid->blocks = EDID_NUM_BLOCKS_MAX; + return -E2BIG; + } + + it6625_disable_hpd(it6625); + + if (edid->blocks == 0) { + cec_phys_addr_invalidate(it6625->cec_adap); + return 0; + } + + err = it6625_write_edid(it6625, edid->edid, + edid->start_block, edid->blocks); + if (err < 0) { + v4l2_err(sd, "write edid failed"); + } else { + pa = cec_get_edid_phys_addr(edid->edid, edid->blocks * 128, NULL); + pa_err = pa == CEC_PHYS_ADDR_INVALID ? -EINVAL : + v4l2_phys_addr_validate(pa, NULL, NULL); + if (pa_err) { + v4l2_warn(sd, "invalid CEC physical address in EDID"); + cec_phys_addr_invalidate(it6625->cec_adap); + } else { + cec_s_phys_addr(it6625->cec_adap, pa, false); + } + } + + if (hdmi_5v_power_present(it6625)) { + it6625_enable_hpd(it6625); + it6625_s_ctrl_detect_hdmi_5v(sd); + } else { + it6625_enable_auto_hpd(it6625); + } + + return err < 0 ? err : 0; +} + +#define V4L2_CID_USER_IT6625_BASE (V4L2_CID_USER_BASE + 0x11b0) +#define IT6625_CID_AUDIO_SAMPLING_RATE (V4L2_CID_USER_IT6625_BASE + 0) +#define IT6625_CID_AUDIO_PRESENT (V4L2_CID_USER_IT6625_BASE + 1) +#define IT6625_CID_HDMI_INFO (V4L2_CID_USER_IT6625_BASE + 2) +#define IT6625_CID_MIPI_CONFIG (V4L2_CID_USER_IT6625_BASE + 3) +static int it6625_g_volatile_ctrl(struct v4l2_ctrl *ctrl) +{ + struct it6625 *it6625 = hdl_to_6625(ctrl->handler); + struct v4l2_subdev *sd = &it6625->sd; + + v4l2_dbg(1, debug, sd, "%s, id=0x%x, val=%d", + __func__, ctrl->id, ctrl->val); + + switch (ctrl->id) { + case IT6625_CID_HDMI_INFO: + scoped_guard(mutex, &it6625->it6625_lock) { + snprintf(ctrl->p_new.p_char, ctrl->maximum, "%s @ %uHz", + it6625_csi_format_name(it6625->csi_format), + fps_from_bt_timings(&it6625->timings.bt)); + } + break; + case IT6625_CID_MIPI_CONFIG: + ctrl->val = it6625_read_byte(it6625, REG_MIPI_DATA_TYPE); + break; + default: + break; + } + + return 0; +} + +static int it6625_s_ctrl(struct v4l2_ctrl *ctrl) +{ + struct it6625 *it6625 = hdl_to_6625(ctrl->handler); + struct v4l2_subdev *sd = &it6625->sd; + + v4l2_dbg(1, debug, sd, "%s, id=0x%x, val=%d", + __func__, ctrl->id, ctrl->val); + + switch (ctrl->id) { + case IT6625_CID_MIPI_CONFIG: + it6625_set_mipi_config(it6625, ctrl->val); + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct v4l2_subdev_core_ops it6625_core_ops = { + .log_status = it6625_log_status, + .interrupt_service_routine = it6625_isr, + .subscribe_event = it6625_subscribe_event, + .unsubscribe_event = v4l2_event_subdev_unsubscribe, +}; + +static const struct v4l2_subdev_video_ops it6625_video_ops = { + .g_input_status = it6625_g_input_status, + .s_stream = it6625_s_stream, +}; + +static const struct v4l2_subdev_pad_ops it6625_pad_ops = { + .enum_mbus_code = it6625_enum_mbus_code, + .set_fmt = it6625_set_fmt, + .get_fmt = it6625_get_fmt, + .get_edid = it6625_g_edid, + .set_edid = it6625_s_edid, + .enum_dv_timings = it6625_enum_dv_timings, + .dv_timings_cap = it6625_dv_timings_cap, + .get_mbus_config = it6625_get_mbus_config, + .s_dv_timings = it6625_pad_s_dv_timings, + .g_dv_timings = it6625_pad_g_dv_timings, + .query_dv_timings = it6625_pad_query_dv_timings, +}; + +static const struct v4l2_subdev_ops it6625_ops = { + .core = &it6625_core_ops, + .video = &it6625_video_ops, + .pad = &it6625_pad_ops, +}; + +static const struct v4l2_ctrl_ops it6625_ctrl_ops = { + .g_volatile_ctrl = it6625_g_volatile_ctrl, + .s_ctrl = it6625_s_ctrl, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_audio_sampling_rate = { + .id = IT6625_CID_AUDIO_SAMPLING_RATE, + .name = "Audio sampling rate", + .type = V4L2_CTRL_TYPE_INTEGER, + .min = 0, + .max = 768000, + .step = 1, + .def = 0, + .flags = V4L2_CTRL_FLAG_READ_ONLY, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_audio_present = { + .id = IT6625_CID_AUDIO_PRESENT, + .name = "Audio present", + .type = V4L2_CTRL_TYPE_BOOLEAN, + .min = 0, + .max = 1, + .step = 1, + .def = 0, + .flags = V4L2_CTRL_FLAG_READ_ONLY, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_get_hdmi_info = { + .ops = &it6625_ctrl_ops, + .id = IT6625_CID_HDMI_INFO, + .name = "HDMI Info", + .type = V4L2_CTRL_TYPE_STRING, + .max = 128, + .step = 1, + .flags = V4L2_CTRL_FLAG_READ_ONLY | V4L2_CTRL_FLAG_VOLATILE, +}; + +static const struct v4l2_ctrl_config it6625_ctrl_mipi_config = { + .ops = &it6625_ctrl_ops, + .id = IT6625_CID_MIPI_CONFIG, + .name = "MIPI config", + .type = V4L2_CTRL_TYPE_INTEGER, + .min = 0, + .max = 0x7FFFFFFF, + .step = 1, + .def = 0x3E, + .flags = V4L2_CTRL_FLAG_VOLATILE, +}; + +static int it6625_v4l2_init_controls(struct v4l2_subdev *sd) +{ + struct it6625 *it6625 = sd_to_6625(sd); + struct v4l2_ctrl_handler *hdl = &it6625->hdl; + + v4l2_ctrl_handler_init(hdl, 4); + it6625->ctrl_5v_detect = + v4l2_ctrl_new_std(hdl, NULL, V4L2_CID_DV_RX_POWER_PRESENT, + 0, 1, 0, 0); + + it6625->ctrl_audio_sampling_rate = + v4l2_ctrl_new_custom(hdl, + &it6625_ctrl_audio_sampling_rate, + NULL); + it6625->ctrl_audio_present = + v4l2_ctrl_new_custom(hdl, &it6625_ctrl_audio_present, NULL); + it6625->ctrl_link_freq = + v4l2_ctrl_new_int_menu(hdl, NULL, V4L2_CID_LINK_FREQ, + ARRAY_SIZE(it6625_link_freq) - 1, + 0, it6625_link_freq); + it6625->ctrl_get_hdmi_info = + v4l2_ctrl_new_custom(hdl, &it6625_ctrl_get_hdmi_info, NULL); + it6625->ctrl_mipi_config = + v4l2_ctrl_new_custom(hdl, &it6625_ctrl_mipi_config, NULL); + if (hdl->error) { + v4l2_err(sd, "Failed to initialize controls"); + v4l2_ctrl_handler_free(hdl); + return hdl->error; + } + + sd->ctrl_handler = hdl; + + return 0; +} + +static void it6625_regdump_print(struct seq_file *s, const u8 *reg_buf) +{ + int i; + + seq_puts(s, " 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F\n"); + + for (i = 0; i < 256; i++) { + if (i % 16 == 0) + seq_printf(s, "[%02X] ", i & 0xF0); + seq_printf(s, "0x%02X ", reg_buf[i]); + if (i % 16 == 15) + seq_putc(s, '\n'); + } +} + +static int it6625_edid_ram_show(struct seq_file *s, void *data) +{ + struct it6625 *it6625 = s->private; + u8 reg_buf[256]; + int ret; + + ret = it6625_read_edid(it6625, reg_buf, 0, 2); + if (ret < 0) + return ret; + it6625_regdump_print(s, reg_buf); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(it6625_edid_ram); + +static int it6625_mipi_reg_show(struct seq_file *s, void *data) +{ + struct it6625 *it6625 = s->private; + u8 reg_buf[256]; + int ret; + + scoped_guard(mutex, &it6625->it6625_lock) + ret = it6625_read_bytes(it6625, 0x00, reg_buf, sizeof(reg_buf)); + if (ret < 0) + return ret; + it6625_regdump_print(s, reg_buf); + + return 0; +} + +static int it6625_mipi_reg_open(struct inode *inode, struct file *file) +{ + return single_open(file, it6625_mipi_reg_show, inode->i_private); +} + +static ssize_t it6625_mipi_reg_write(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct it6625 *it6625 = file_inode(file)->i_private; + char buf[32] = {}; + unsigned int addr, val; + ssize_t len; + loff_t pos = 0; + + len = simple_write_to_buffer(buf, sizeof(buf) - 1, &pos, user_buf, count); + if (len < 0) + return len; + buf[len] = '\0'; + + if (sscanf(buf, "%X %X", &addr, &val) != 2) + return -EINVAL; + + scoped_guard(mutex, &it6625->it6625_lock) + it6625_write_byte(it6625, addr, val); + + return count; +} + +static const struct file_operations it6625_mipi_reg_fops = { + .owner = THIS_MODULE, + .open = it6625_mipi_reg_open, + .read = seq_read, + .write = it6625_mipi_reg_write, + .llseek = seq_lseek, + .release = single_release, +}; + +static void it6625_debugfs_init(struct it6625 *it6625, struct i2c_client *client) +{ + it6625->debugfs_dir = debugfs_create_dir(dev_name(&client->dev), NULL); + + debugfs_create_file("mipi_reg", 0600, it6625->debugfs_dir, it6625, + &it6625_mipi_reg_fops); + debugfs_create_file("edid_ram", 0400, it6625->debugfs_dir, it6625, + &it6625_edid_ram_fops); +} + +static void it6625_init_data(struct it6625 *it6625) +{ + static struct v4l2_dv_timings default_timing = + V4L2_DV_BT_CEA_1920X1080P60; + + it6625->csi_lanes = 4; + it6625->port_num = 1; + it6625->bus_type = V4L2_MBUS_CSI2_DPHY; + it6625->csi_format = it6625_formats[0].csi_format; + it6625->mbus_fmt_code = it6625_formats[0].mbus_fmt_code; + it6625->timings = default_timing; +} + +static int it6625_parse_endpoint(struct it6625 *it6625) +{ + struct device *dev = it6625->dev; + /* + * Pre-setting bus_type here makes v4l2_fwnode_endpoint_alloc_parse() + * treat it as a hard requirement and reject any endpoint whose DT + * bus-type disagrees, so this must stay V4L2_MBUS_UNKNOWN to let it + * autodetect C-PHY vs D-PHY from the endpoint itself. + */ + struct v4l2_fwnode_endpoint endpoint = { .bus_type = V4L2_MBUS_UNKNOWN }; + struct device_node *ep = NULL; + unsigned int max_lanes; + unsigned int port; + int ret; + + /* + * port@0 and port@1 are the two CSI-2 output ports MIPI0/MIPI1 + * (port@2 is the HDMI input). This chip series can drive both + * simultaneously in split or mirror mode, so port_num counts how + * many of MIPI0/MIPI1 have an endpoint wired up. This driver only + * wires up a single source pad, so lane/bus-type configuration is + * parsed from whichever of the two is found first. + */ + it6625->port_num = 0; + for (port = 0; port < 2; port++) { + struct device_node *port_ep = + of_graph_get_endpoint_by_regs(dev->of_node, port, -1); + + if (!port_ep) + continue; + + it6625->port_num++; + if (!ep) + ep = port_ep; + else + of_node_put(port_ep); + } + + if (!ep) { + it6625->port_num = 1; + dev_dbg(dev, "no CSI-2 endpoint node found, using default %u CSI lanes", + it6625->csi_lanes); + return 0; + } + + ret = v4l2_fwnode_endpoint_alloc_parse(of_fwnode_handle(ep), &endpoint); + of_node_put(ep); + if (ret) { + dev_err(dev, "failed to parse endpoint: %d", ret); + return ret; + } + + if (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY && + it6625->chip_type != IT6626_CHIP) { + dev_err(dev, "IT6625 does not support C-PHY, only IT6626 does"); + v4l2_fwnode_endpoint_free(&endpoint); + return -EINVAL; + } + + max_lanes = (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY) ? 3 : 4; + + if (endpoint.bus.mipi_csi2.num_data_lanes == 0 || + endpoint.bus.mipi_csi2.num_data_lanes > max_lanes) { + dev_err(dev, + "invalid number of CSI data lanes: %u (max %u for this bus type)", + endpoint.bus.mipi_csi2.num_data_lanes, max_lanes); + v4l2_fwnode_endpoint_free(&endpoint); + return -EINVAL; + } + + it6625->csi_lanes = endpoint.bus.mipi_csi2.num_data_lanes; + it6625->bus_type = endpoint.bus_type; + v4l2_fwnode_endpoint_free(&endpoint); + + return 0; +} + +static int it6625_parse_dt(struct it6625 *it6625) +{ + return it6625_parse_endpoint(it6625); +} + +static int it6625_init_v4l2_subdev(struct it6625 *it6625) +{ + struct v4l2_subdev *sd = &it6625->sd; + int err; + + sd->dev = it6625->dev; + + v4l2_i2c_subdev_init(sd, it6625->i2c_client, &it6625_ops); + sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS; + if (it6625_v4l2_init_controls(sd)) { + dev_err(it6625->dev, "Failed to initialize v4l2 controls"); + return -ENOMEM; + } + + it6625->pad.flags = MEDIA_PAD_FL_SOURCE; + sd->entity.function = MEDIA_ENT_F_CAM_SENSOR; + err = media_entity_pads_init(&sd->entity, 1, &it6625->pad); + if (err < 0) { + dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err); + v4l2_ctrl_handler_free(sd->ctrl_handler); + return err; + } + + return 0; +} + +static int it6625_check_device(struct it6625 *it6625) +{ + static const u8 chip_ids[][2] = { + { 0x66, 0x25 }, + { 0x66, 0x26 }, + }; + int chip_id0, chip_id1; + + chip_id0 = it6625_read_byte(it6625, REG_CHIP_ID_0); + chip_id1 = it6625_read_byte(it6625, REG_CHIP_ID_1); + if (chip_id0 != chip_ids[it6625->chip_type][0] || + chip_id1 != chip_ids[it6625->chip_type][1]) { + dev_err(it6625->dev, + "chip ID mismatch: got 0x%02x%02x, expected 0x%02x%02x", + chip_id0, chip_id1, + chip_ids[it6625->chip_type][0], + chip_ids[it6625->chip_type][1]); + return -ENODEV; + } + + return 0; +} + +static int it6625_probe(struct i2c_client *client) +{ + struct it6625 *it6625; + struct v4l2_subdev *sd; + int err; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) + return -EIO; + + it6625 = devm_kzalloc(&client->dev, sizeof(struct it6625), GFP_KERNEL); + if (!it6625) + return -ENOMEM; + + it6625->chip_type = (uintptr_t)i2c_get_match_data(client); + + it6625->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", + GPIOD_OUT_HIGH); + if (IS_ERR(it6625->reset_gpio)) + return PTR_ERR(it6625->reset_gpio); + + if (it6625->reset_gpio) { + usleep_range(1000, 2000); + gpiod_set_value_cansleep(it6625->reset_gpio, 0); + usleep_range(10000, 11000); + } + + err = it6625_regmap_i2c_init(client, it6625); + if (err) + return err; + + err = it6625_check_device(it6625); + if (err) + return err; + + it6625_init_data(it6625); + + err = it6625_parse_dt(it6625); + if (err) + return err; + + mutex_init(&it6625->it6625_lock); + INIT_DELAYED_WORK(&it6625->hpd_delayed_work, it6625_hpd_delayed_work); + INIT_WORK(&it6625->polling_work, it6625_polling_work); + + if (client->irq) { + err = devm_request_threaded_irq(&client->dev, client->irq, + NULL, it6625_irq_handler, + IRQF_TRIGGER_LOW | + IRQF_ONESHOT | + IRQF_NO_AUTOEN, + "it6625", it6625); + if (err) + return err; + } else { + dev_info(it6625->dev, "no IRQ, falling back to polling"); + timer_setup(&it6625->timer, it6625_irq_poll_timer, 0); + } + + sd = &it6625->sd; + err = it6625_init_v4l2_subdev(it6625); + if (err) + goto err_clean_work_queues; + + err = v4l2_ctrl_handler_setup(sd->ctrl_handler); + if (err) + goto err_clean_hdl; + + it6625->cec_adap = cec_allocate_adapter(&it6625_cec_adap_ops, + it6625, dev_name(it6625->dev), + CEC_CAP_DEFAULTS | + CEC_CAP_MONITOR_ALL | + CEC_CAP_PHYS_ADDR, + CEC_MAX_LOG_ADDRS); + if (IS_ERR(it6625->cec_adap)) { + err = PTR_ERR(it6625->cec_adap); + dev_err(it6625->dev, "%s %d", __func__, __LINE__); + goto err_clean_hdl; + } + + err = cec_register_adapter(it6625->cec_adap, &client->dev); + if (err < 0) { + dev_err(it6625->dev, "%s: failed to register the cec device", __func__); + cec_delete_adapter(it6625->cec_adap); + it6625->cec_adap = NULL; + goto err_clean_hdl; + } + + it6625_debugfs_init(it6625, client); + + it6625_initial_setup(it6625); + it6625_v4l2_sd_ctrl_update(sd); + + err = v4l2_async_register_subdev(sd); + if (err < 0) { + dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err); + goto err_clean_debugfs; + } + + if (client->irq) + enable_irq(client->irq); + else + mod_timer(&it6625->timer, jiffies + msecs_to_jiffies(POLL_INTERVAL_MS)); + + return 0; + +err_clean_debugfs: + debugfs_remove_recursive(it6625->debugfs_dir); + cec_unregister_adapter(it6625->cec_adap); +err_clean_hdl: + media_entity_cleanup(&sd->entity); + v4l2_ctrl_handler_free(&it6625->hdl); + +err_clean_work_queues: + if (!client->irq) + timer_shutdown_sync(&it6625->timer); + cancel_work_sync(&it6625->polling_work); + cancel_delayed_work_sync(&it6625->hpd_delayed_work); + mutex_destroy(&it6625->it6625_lock); + return err; +} + +static void it6625_remove(struct i2c_client *client) +{ + struct v4l2_subdev *sd = i2c_get_clientdata(client); + struct it6625 *it6625 = sd_to_6625(sd); + + v4l2_async_unregister_subdev(sd); + v4l2_device_unregister_subdev(sd); + + if (client->irq) + disable_irq(client->irq); + else + timer_shutdown_sync(&it6625->timer); + + cancel_work_sync(&it6625->polling_work); + cancel_delayed_work_sync(&it6625->hpd_delayed_work); + debugfs_remove_recursive(it6625->debugfs_dir); + cec_unregister_adapter(it6625->cec_adap); + mutex_destroy(&it6625->it6625_lock); + media_entity_cleanup(&sd->entity); + v4l2_ctrl_handler_free(&it6625->hdl); +} + +static const struct i2c_device_id it6625_id[] = { + { .name = "it6625", .driver_data = IT6625_CHIP }, + { .name = "it6626", .driver_data = IT6626_CHIP }, + {} +}; +MODULE_DEVICE_TABLE(i2c, it6625_id); + +static const struct of_device_id it6625_of_match[] = { + { .compatible = "ite,it6625", .data = (void *)IT6625_CHIP }, + { .compatible = "ite,it6626", .data = (void *)IT6626_CHIP }, + {}, +}; +MODULE_DEVICE_TABLE(of, it6625_of_match); + +static struct i2c_driver it6625_driver = { + .driver = { + .name = "it6625", + .of_match_table = of_match_ptr(it6625_of_match), + }, + .probe = it6625_probe, + .remove = it6625_remove, + .id_table = it6625_id, +}; +module_i2c_driver(it6625_driver); + +MODULE_DESCRIPTION("iTE it6625/it6626 HDMI to MIPI CSI bridge driver"); +MODULE_AUTHOR("Hermes Wu <Hermes.wu@ite.com.tw>"); +MODULE_LICENSE("GPL"); -- 2.34.1 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v6 2/2] media: i2c: add driver for ITE IT6625/IT6626 2026-07-22 11:18 ` Hermes Wu (?) @ 2026-07-22 11:31 ` sashiko-bot -1 siblings, 0 replies; 8+ messages in thread From: sashiko-bot @ 2026-07-22 11:31 UTC (permalink / raw) To: Hermes Wu; +Cc: devicetree, robh, media-ci, conor+dt Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Medium] Accessing MIPI CSI-2 specific union fields without verifying the parsed bus type. - [Medium] `it6625_set_fmt()` fails to update `format->format.colorspace` for the `V4L2_SUBDEV_FORMAT_ACTIVE` branch. - [Medium] `it6625_s_edid` violates the V4L2 `VIDIOC_SUBDEV_S_EDID` specification for `start_block` bounds handling. -- --- Patch [2]: [PATCH v6 2/2] media: i2c: add driver for ITE IT6625/IT6626 --- commit 9017f8e5343a61b6c557bf4b67bcc1fe372b54d2 Author: Hermes Wu <Hermes.wu@ite.com.tw> Subject: media: i2c: add driver for ITE IT6625/IT6626 This commit adds a new V4L2 subdevice driver for the ITE IT6625 and IT6626 HDMI-to-MIPI CSI-2 bridge chips. The driver supports configuring the bridge over I2C, handles EDID reading and writing, exposes an HDMI CEC adapter, and supports DV timings and HPD control. diff --git a/drivers/media/i2c/it6625.c b/drivers/media/i2c/it6625.c --- a/drivers/media/i2c/it6625.c +++ b/drivers/media/i2c/it6625.c [ ... ] > + if (format->which == V4L2_SUBDEV_FORMAT_TRY) { > + struct v4l2_mbus_framefmt *fmt; > + > + fmt = v4l2_subdev_state_get_format(sd_state, format->pad); > + fmt->code = format->format.code; > + fmt->colorspace = format_to_colorspace(it6625_formats[ret].csi_format); > + format->format.colorspace = fmt->colorspace; > + v4l2_dbg(1, debug, sd, "%s: try format code = 0x%x", > + __func__, format->format.code); > + return 0; > + } > + > + scoped_guard(mutex, &it6625->it6625_lock) { > + it6625->csi_format = it6625_formats[ret].csi_format; > + it6625->mbus_fmt_code = format->format.code; > + it6625_enable_stream_locked(it6625, false); > + it6625_set_mipi_config_locked(it6625, it6625->csi_format); > + } > + > + return 0; > +} [Severity: Medium] Does this branch need to update format->format.colorspace before returning? The V4L2_SUBDEV_FORMAT_TRY branch above updates format->format.colorspace, but the active format branch only updates the internal hardware state variables. This leaves the userspace format object with the old colorspace value that was populated earlier in the function by it6625_get_fmt(). [ ... ] > +static int it6625_s_edid(struct v4l2_subdev *sd, > + struct v4l2_subdev_edid *edid) > +{ [ ... ] > + memset(edid->reserved, 0, sizeof(edid->reserved)); > + > + if (edid->blocks > EDID_NUM_BLOCKS_MAX) { > + v4l2_err(sd, "too many edid blocks: %d", edid->blocks); > + edid->blocks = EDID_NUM_BLOCKS_MAX; > + return -E2BIG; > + } > + > + it6625_disable_hpd(it6625); [Severity: Medium] Does this logic correctly handle out-of-bounds start blocks according to the V4L2 specification? If start_block is out of bounds, the spec expects blocks to be set to 0. Additionally, if start_block + blocks exceeds the maximum, shouldn't blocks be clamped to the remaining capacity (EDID_NUM_BLOCKS_MAX - edid->start_block) before returning -E2BIG? [ ... ] > +static int it6625_parse_endpoint(struct it6625 *it6625) > +{ [ ... ] > + if (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY && > + it6625->chip_type != IT6626_CHIP) { > + dev_err(dev, "IT6625 does not support C-PHY, only IT6626 does"); > + v4l2_fwnode_endpoint_free(&endpoint); > + return -EINVAL; > + } > + > + max_lanes = (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY) ? 3 : 4; > + > + if (endpoint.bus.mipi_csi2.num_data_lanes == 0 || > + endpoint.bus.mipi_csi2.num_data_lanes > max_lanes) { > + dev_err(dev, > + "invalid number of CSI data lanes: %u (max %u for this bus type)", > + endpoint.bus.mipi_csi2.num_data_lanes, max_lanes); [Severity: Medium] Can we read from the mipi_csi2 union member here without first verifying the parsed bus type? If the device tree is misconfigured and causes v4l2_fwnode_endpoint_alloc_parse() to infer a different bus type (like V4L2_MBUS_PARALLEL), reading from the mipi_csi2 struct could yield arbitrary data for the lane count. -- Sashiko AI review · https://sashiko.dev/#/patchset/20260722-upstream-it6625-it6626-v6-0-6117d4d96574@ite.com.tw?part=2 ^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-07-22 11:31 UTC | newest] Thread overview: 8+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-22 11:18 [PATCH v6 0/2] media: i2c: add support for ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge Hermes Wu via B4 Relay 2026-07-22 11:18 ` Hermes Wu 2026-07-22 11:18 ` [PATCH v6 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding Hermes Wu via B4 Relay 2026-07-22 11:18 ` Hermes Wu 2026-07-22 11:25 ` sashiko-bot 2026-07-22 11:18 ` [PATCH v6 2/2] media: i2c: add driver for ITE IT6625/IT6626 Hermes Wu via B4 Relay 2026-07-22 11:18 ` Hermes Wu 2026-07-22 11:31 ` sashiko-bot
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.