* [PATCH 2/2] arm64: tegra: Add Lenovo ThinkEdge SE70 carrier board DTS
From: Jiqi Li @ 2026-06-26 1:09 UTC (permalink / raw)
To: linux-tegra
Cc: devicetree, robh+dt, krzk+dt, conor+dt, jonathanh, thierry.reding,
mpearson-lenovo, Jiqi Li
In-Reply-To: <20260626010950.459899-1-lijq9@lenovo.com>
ThinkEdge SE70 is a fanless industrial edge gateway built around
NVIDIA Tegra194 Xavier NX SOM. This patch adds a standalone device
tree file describing core carrier board peripherals:
- Custom 40-pin header pinmux configuration
- External SD card slot with dedicated 3.3V fixed regulator
No fan, PWM, tachometer, extra camera/spi peripherals are present
on this passively cooled platform, so unused nodes are explicitly
disabled following mainline device tree best practices.
Static verification passed: dt_binding_check and dtbs compilation
complete without errors.
Signed-off-by: Jiqi Li <lijq9@lenovo.com>
---
arch/arm64/boot/dts/nvidia/Makefile | 1 +
.../nvidia/tegra194-lenovo-thinkedge-se70.dts | 167 ++++++++++++++++++
2 files changed, 168 insertions(+)
create mode 100644 arch/arm64/boot/dts/nvidia/tegra194-lenovo-thinkedge-se70.dts
diff --git a/arch/arm64/boot/dts/nvidia/Makefile b/arch/arm64/boot/dts/nvidia/Makefile
index 72c0cb5efa47..736a3f8a923f 100644
--- a/arch/arm64/boot/dts/nvidia/Makefile
+++ b/arch/arm64/boot/dts/nvidia/Makefile
@@ -29,6 +29,7 @@ dtb-$(CONFIG_ARCH_TEGRA_186_SOC) += tegra186-p3509-0000+p3636-0001.dtb
dtb-$(CONFIG_ARCH_TEGRA_194_SOC) += tegra194-p2972-0000.dtb
dtb-$(CONFIG_ARCH_TEGRA_194_SOC) += tegra194-p3509-0000+p3668-0000.dtb
dtb-$(CONFIG_ARCH_TEGRA_194_SOC) += tegra194-p3509-0000+p3668-0001.dtb
+dtb-$(CONFIG_ARCH_TEGRA_194_SOC) += tegra194-lenovo-thinkedge-se70.dtb
dtb-$(CONFIG_ARCH_TEGRA_234_SOC) += tegra234-sim-vdk.dtb
dtb-$(CONFIG_ARCH_TEGRA_234_SOC) += tegra234-p3737-0000+p3701-0000.dtb
dtb-$(CONFIG_ARCH_TEGRA_234_SOC) += tegra234-p3737-0000+p3701-0008.dtb
diff --git a/arch/arm64/boot/dts/nvidia/tegra194-lenovo-thinkedge-se70.dts b/arch/arm64/boot/dts/nvidia/tegra194-lenovo-thinkedge-se70.dts
new file mode 100644
index 000000000000..075e057c9095
--- /dev/null
+++ b/arch/arm64/boot/dts/nvidia/tegra194-lenovo-thinkedge-se70.dts
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-2.0
+/dts-v1/;
+
+#include "tegra194-p3668-0001.dtsi"
+#include "tegra194-p3509-0000.dtsi"
+
+/ {
+ model = "Lenovo ThinkEdge SE70";
+ compatible = "lenovo,thinkedge-se70", "nvidia,p3509-0000+p3668-0001", "nvidia,tegra194";
+
+ chosen {
+ bootargs = "console=ttyTCU0,115200";
+ };
+
+ /* Fixed 2.3V regulator for external SD card slot */
+ fixed-regulators {
+ ap2306gn_3v3_sd: ap2306gn_3v3_sd {
+ compatible = "regulator-fixed";
+ regulator-name = "ap2306gn-3v3-sd";
+ regulator-min-microvolt = <3300000>;
+ regulator-max-microvolt = <3300000>;
+ gpio = <&gpio TEGRA194_MAIN_GPIO(Q, 5) GPIO_ACTIVE_HIGH>;
+ enable-active-high;
+ status = "okay";
+ };
+ };
+
+ /* Custom pinmux configurations for 40-pin expansion header */
+ pinmux@c302000 {
+ status = "okay";
+ touch_clk_pcc4 {
+ nvidia,pins = "touch_clk_pcc4";
+ nvidia,function = "rsvd2";
+ nvidia,pull = <TEGRA_PIN_PULL_UP>;
+ nvidia,tristate = <TEGRA_PIN_ENABLE>;
+ nvidia,enable-input = <TEGRA_PIN_ENABLE>;
+ nvidia,lpdr = <TEGRA_PIN_DISABLE>;
+ };
+ };
+
+ pinmux@2430028 {
+ status = "okay";
+ soc_gpio41_pq5 {
+ nvidia,pins = "soc_gpio41_pq5";
+ nvidia,function = "rsvd2";
+ nvidia,pull = <TEGRA_PIN_PULL_NONE>;
+ nvidia,tristate = <TEGRA_PIN_DISABLE>;
+ nvidia,enable-input = <TEGRA_PIN_DISABLE>;
+ nvidia,lpdr = <TEGRA_PIN_DISABLE>;
+ nvidia,io-high-voltage = <TEGRA_PIN_DISABLE>;
+ };
+ };
+
+ pinmux@2430000 {
+ status = "okay";
+ pinctrl-names = "default";
+ pinctrl-0 = <&hdr40_pinmux>;
+
+ hdr40_pinmux: header-40pin-pinmux {
+ pin7 {
+ nvidia,pins = "aud_mclk_ps4";
+ nvidia,function = "aud";
+ nvidia,pull = <TEGRA_PIN_PULL_NONE>;
+ nvidia,tristate = <TEGRA_PIN_DISABLE>;
+ nvidia,enable-input = <TEGRA_PIN_DISABLE>;
+ };
+ pin11 {
+ nvidia,pins = "uart1_rts_pr4";
+ nvidia,function = "uarta";
+ nvidia,pull = <TEGRA_PIN_PULL_NONE>;
+ nvidia,tristate = <TEGRA_PIN_DISABLE>;
+ nvidia,enable-input = <TEGRA_PIN_DISABLE>;
+ nvidia,lpdr = <0x0>;
+ };
+ pin12 {
+ nvidia,pins = "dap5_sclk_pt5";
+ nvidia,function = "i2s5";
+ nvidia,pull = <TEGRA_PIN_PULL_DOWN>;
+ nvidia,tristate = <TEGRA_PIN_DISABLE>;
+ nvidia,enable-input = <TEGRA_PIN_ENABLE>;
+ };
+ pin35 {
+ nvidia,pins = "dap5_fs_pu0";
+ nvidia,function = "i2s5";
+ nvidia,pull = <TEGRA_PIN_PULL_DOWN>;
+ nvidia,tristate = <TEGRA_PIN_DISABLE>;
+ nvidia,enable-input = <TEGRA_PIN_ENABLE>;
+ };
+ pin36 {
+ nvidia,pins = "uart1_cts_pr5";
+ nvidia,function = "uarta";
+ nvidia,pull = <TEGRA_PIN_PULL_UP>;
+ nvidia,tristate = <TEGRA_PIN_ENABLE>;
+ nvidia,enable-input = <TEGRA_PIN_ENABLE>;
+ nvidia,lpdr = <0x0>;
+ };
+ pin38 {
+ nvidia,pins = "dap5_din_pt7";
+ nvidia,function = "i2s5";
+ nvidia,pull = <TEGRA_PIN_PULL_DOWN>;
+ nvidia,tristate = <TEGRA_PIN_ENABLE>;
+ nvidia,enable-input = <TEGRA_PIN_ENABLE>;
+ };
+ pin40 {
+ nvidia,pins = "dap5_dout_pt6";
+ nvidia,function = "i2s5";
+ nvidia,pull = <TEGRA_PIN_PULL_DOWN>;
+ nvidia,tristate = <TEGRA_PIN_DISABLE>;
+ nvidia,enable-input = <TEGRA_PIN_DISABLE>;
+ };
+ };
+ };
+
+ /* Configure i2c bus clock to 400kHz for carrier board peripherals */
+ i2c@3160000 {
+ clock-frequency = <400000>;
+ status = "okay";
+ };
+
+ /* SDMMC3 for external user SD card slot with dedicated 3.3V power */
+ sdhci@3440000 {
+ uhs-mask = <0x78>;
+ mmc-ocr-mask = <0x0>;
+ cd-inverted;
+ cd-gpios = <&gpio_aon TEGRA194_AON_GPIO(CC, 4) GPIO_ACTIVE_LOW>;
+ nvidia,cd-wakeup-capable;
+ vmmc-supply = <&ap2306gn_3v3_sd>;
+ status = "okay";
+ };
+
+ /* Disable fan and tachometer hardware not populated on SE70 carrier board */
+ pwm-fan {
+ status = "disabled";
+ };
+ thermal-fan-est {
+ status = "disabled";
+ };
+ tachometer@39c0000 {
+ status = "disabled";
+ };
+ pwm@c340000 {
+ status = "disabled";
+ };
+ pwm@3280000 {
+ status = "disabled";
+ };
+ pwm@32c0000 {
+ status = "disabled";
+ };
+ pwm@32d0000 {
+ status = "disabled";
+ };
+ pwm@32f0000 {
+ status = "disabled";
+ };
+ generic_pwm_tachometer {
+ status = "disabled";
+ };
+
+ /* Disable unused SPI interfaces on 40-pin header */
+ spi@3210000 {
+ status = "disabled";
+ };
+ spi@3230000 {
+ status = "disabled";
+ };
+};
--
2.43.0
^ permalink raw reply related
* [PATCH 1/2] dt-bindings: arm: tegra: Add lenovo,thinkedge-se70 compatible string
From: Jiqi Li @ 2026-06-26 1:09 UTC (permalink / raw)
To: linux-tegra
Cc: devicetree, robh+dt, krzk+dt, conor+dt, jonathanh, thierry.reding,
mpearson-lenovo, Jiqi Li
In-Reply-To: <20260626010950.459899-1-lijq9@lenovo.com>
Lenovo ThinkEdge SE70 is a fanless industrial edge gateway carrier
board based on NVIDIA Tegra194 (Xavier NX) SOM.
Add the corresponding compatible string for device tree validation.
Signed-off-by: Jiqi Li <lijq9@lenovo.com>
---
Documentation/devicetree/bindings/arm/tegra.yaml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Documentation/devicetree/bindings/arm/tegra.yaml b/Documentation/devicetree/bindings/arm/tegra.yaml
index 033a63f6c068..960c604ef9a3 100644
--- a/Documentation/devicetree/bindings/arm/tegra.yaml
+++ b/Documentation/devicetree/bindings/arm/tegra.yaml
@@ -268,6 +268,10 @@ properties:
items:
- const: nvidia,p3509-0000+p3668-0001
- const: nvidia,tegra194
+ - description: Lenovo ThinkEdge SE70
+ items:
+ - const: lenovo,thinkedge-se70
+ - const: nvidia,tegra194
- items:
- const: nvidia,tegra234-vdk
- const: nvidia,tegra234
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v7 02/13] dt-bindings: display: renesas,rzg2l-du: Add support for RZ/G3E SoC
From: Tommaso Merciai @ 2026-06-26 0:59 UTC (permalink / raw)
To: Rob Herring
Cc: tomm.merciai, geert, laurent.pinchart, linux-renesas-soc,
biju.das.jz, David Airlie, Simona Vetter, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, Krzysztof Kozlowski,
Conor Dooley, Philipp Zabel, Geert Uytterhoeven, Magnus Damm,
Laurent Pinchart, dri-devel, devicetree, linux-kernel
In-Reply-To: <aiqtSWTTa4ZIThrp@tom-desktop>
Hi Rob,
On Thu, Jun 11, 2026 at 02:42:49PM +0200, Tommaso Merciai wrote:
> Hi Rob,
> Thanks for your review.
>
> On Wed, May 13, 2026 at 05:27:25PM -0500, Rob Herring wrote:
> > On Thu, May 07, 2026 at 11:21:30AM +0200, Tommaso Merciai wrote:
> > > The RZ/G3E SoC integrates two LCD controllers (LCDC0 and LCDC1), each
> > > containing a FCPVD, VSPD, and Display Unit (DU).
> > >
> > > - LCDC0 supports DSI and LVDS (single or dual-channel) outputs.
> > > - LCDC1 supports DSI, LVDS (single-channel), and RGB outputs.
> > >
> > > Add compatible string 'renesas,r9a09g047-du' and extend the binding to
> > > support two DU instances: add reg-names ('du0'/'du1'), extend reg,
> > > interrupts, and resets to maxItems: 2, and extend clocks/clock-names to
> > > six entries (aclk/pclk/vclk per instance, minItems: 3).
> >
> > Don't write what the diff has. I can read the diff for that.
>
> Ouch, thanks.
>
> >
> > >
> > > Drop the "Each port shall have a single endpoint." constraint since
> > > RZ/G3E ports expose multiple endpoints.
> > >
> > > Add a RZ/G3E-specific allOf rule mapping two DU instances to two ports:
> > >
> > > - port@0 (DU0): endpoint@0 DSI, endpoint@2 LVDS ch0, endpoint@3 LVDS ch1
> > > - port@1 (DU1): endpoint@0 DSI, endpoint@1 RGB (DPAD), endpoint@3 LVDS ch1
> > >
> > > Signed-off-by: Tommaso Merciai <tommaso.merciai.xr@bp.renesas.com>
> > > ---
> > > v6->v7:
> > > - Rebased on top of [1]
> > > [1] https://lore.kernel.org/all/20260429170012.366537-1-prabhakar.mahadev-lad.rj@bp.renesas.com/
> > > - Use single DRM device aggregating both DU instances (1 DU dt node),
> > > modelling single port for each DU0, DU1 and multiple endpoints for
> > > outputs.
> >
> > That seems like the completely wrong thing to do and you've given no
> > reason why you think it is the right choice.
>
>
> We had a discussion with Laurent at [1] about this topic.
> In particular:
>
> DSI ip can select at runtime input data path or DU0 or DU1
> via DSI_LINK_GPO0R VICH register. This can be done by managing the
> 2 DUs as single DRM device aggregating both DU instances that will spawn
> 2 crtcs. In this way at runtime we can select the output for DSI ip
> via the following commands:
>
> modetest -M rzg2l-du -s 58@55:800x600-56.25@XR24 (DU0 -> DSI)
> modetest -M rzg2l-du -s 58@56:800x600-56.25@XR24 (DU1 -> DSI)
>
> This can be done using option [B] (single drm device that spawn 2 crtc).
>
> Using option [A] we will have 2 drm devices 1 for DU0 and 1 for DU1
> that each will spawn a single CRTC and the above feature will be not
> achievable.
>
> In the end we need a way to have single DRM device that spawn 2 CRTCs.
>
> A) Two device tree nodes rapresenting DU0 and DU1 design [v6]:
>
> du0: display@16460000 {
> compatible = "renesas,r9a09g047-du";
> reg = <0 0x16460000 0 0x10000>;
> interrupts = <GIC_SPI 882 IRQ_TYPE_LEVEL_HIGH>;
> clocks = <&cpg CPG_MOD 0xed>,
> <&cpg CPG_MOD 0xee>,
> <&cpg CPG_MOD 0xef>;
> clock-names = "aclk", "pclk", "vclk";
> power-domains = <&cpg>;
> resets = <&cpg 0xdc>;
> renesas,vsps = <&vspd0 0>;
> status = "disabled";
>
> ports {
> #address-cells = <1>;
> #size-cells = <0>;
>
> port@0 {
> reg = <0>;
> du0_out_dsi: endpoint {
> };
> };
>
> port@2 {
> reg = <2>;
> du0_out_lvds0: endpoint {
> };
> };
>
> port@3 {
> reg = <3>;
> du0_out_lvds1: endpoint {
> };
> };
> };
> };
>
> du1: display@16490000 {
> compatible = "renesas,r9a09g047-du";
> reg = <0 0x16490000 0 0x10000>;
> interrupts = <GIC_SPI 922 IRQ_TYPE_LEVEL_HIGH>;
> clocks = <&cpg CPG_MOD 0x1a8>,
> <&cpg CPG_MOD 0x1a9>,
> <&cpg CPG_MOD 0x1aa>;
> clock-names = "aclk", "pclk", "vclk";
> power-domains = <&cpg>;
> resets = <&cpg 0x11e>;
> renesas,vsps = <&vspd1 0>;
> status = "disabled";
>
> ports {
> #address-cells = <1>;
> #size-cells = <0>;
>
> port@0 {
> reg = <0>;
> du1_out_dsi: endpoint {
> };
> };
>
> port@1 {
> reg = <1>;
> du1_out_rgb: endpoint {
> };
> };
>
> port@3 {
> reg = <3>;
> du1_out_lvds1: endpoint {
> };
> };
> };
> };
>
> ---
>
> B) Single device tree node design aggregating both DU instances [v7]:
>
> du: display@16460000 {
> compatible = "renesas,r9a09g047-du";
> reg = <0 0x16460000 0 0x10000>,
> <0 0x16490000 0 0x10000>;
> reg-names = "du0", "du1";
> interrupts = <GIC_SPI 882 IRQ_TYPE_LEVEL_HIGH>,
> <GIC_SPI 922 IRQ_TYPE_LEVEL_HIGH>;
> clocks = <&cpg CPG_MOD 0xed>,
> <&cpg CPG_MOD 0xee>,
> <&cpg CPG_MOD 0xef>,
> <&cpg CPG_MOD 0x1a8>,
> <&cpg CPG_MOD 0x1a9>,
> <&cpg CPG_MOD 0x1aa>;
> clock-names = "aclk", "pclk", "vclk",
> "aclk1", "pclk1", "vclk1";
> power-domains = <&cpg>;
> resets = <&cpg 0xdc>, <&cpg 0x11e>;
> reset-names = "resetn", "resetn1";
> renesas,vsps = <&vspd0 0>, <&vspd1 0>;
> status = "disabled";
>
> ports {
> #address-cells = <1>;
> #size-cells = <0>;
>
> port@0 {
> #address-cells = <1>;
> #size-cells = <0>;
>
> reg = <0>;
>
> du0_out_dsi: endpoint@0 {
> reg = <0>;
> };
>
> du0_out_lvds0: endpoint@2 {
> reg = <2>;
> };
>
> du0_out_lvds1: endpoint@3 {
> reg = <3>;
> };
> };
>
> port@1 {
> #address-cells = <1>;
> #size-cells = <0>;
>
> reg = <1>;
>
> du1_out_dsi: endpoint@0 {
> reg = <0>;
> };
>
> du1_out_rgb: endpoint@1 {
> reg = <1>;
> };
>
> du1_out_lvds1: endpoint@3 {
> reg = <3>;
> };
> };
> };
> };
>
> ---
Gentle ping on how to proceed on this patch?
Kind Regards,
Tommaso
>
> Please Biju, Laurent correct me if I'm missing something.
>
> [1] https://patchwork.kernel.org/project/linux-renesas-soc/patch/8f814f22ff62dcde6153260e2c8c29a5415c9a89.1775636898.git.tommaso.merciai.xr@bp.renesas.com/
> [v6] https://patchwork.kernel.org/project/linux-renesas-soc/patch/8f814f22ff62dcde6153260e2c8c29a5415c9a89.1775636898.git.tommaso.merciai.xr@bp.renesas.com/
> [v7] https://patchwork.kernel.org/project/linux-renesas-soc/patch/ff8e401a0667970a42a55420dcb071e34730a923.1778141145.git.tommaso.merciai.xr@bp.renesas.com/
>
> Kind Regards,
> Tommaso
>
> >
> > Rob
^ permalink raw reply
* Re: [PATCH 2/4] clk: en7523: add support for dedicated PCIe PERSTOUT reset
From: sashiko-bot @ 2026-06-26 0:20 UTC (permalink / raw)
To: Christian Marangi; +Cc: robh, conor+dt, linux-pci, devicetree
In-Reply-To: <20260625215741.3253212-3-ansuelsmth@gmail.com>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [High] Data race between clock enable/disable and reset assert/deassert on the shared PCIC register (0x88).
- [Medium] The reset status callback does not account for inverted reset logic, reporting incorrect status for PCIC resets.
Pre-existing issues:
- [High] Missing synchronization in the reset controller allows concurrent reset operations to corrupt shared reset registers.
- [High] The `en7581_pci_disable` function calls `usleep_range()`, sleeping in an atomic context.
--
commit 82bdae998b98b49b0cf96f540c945b593f30a899
Author: Christian Marangi <ansuelsmth@gmail.com>
clk: en7523: add support for dedicated PCIe PERSTOUT reset
Add support for resetting the PCIe lines with the PERSTOUT reset. These
special reset are controlled by the PCIC register and are specific to each
of the 3 PCIe lines.
Notice that reset logic is inverted for these bit where 0 is assert and 1
deassert. This is intenrally handled in the reset function.
PCI enable/disable are updated to drop PERSTOUT bits in favor dedicated
reset handling.
> diff --git a/drivers/clk/clk-en7523.c b/drivers/clk/clk-en7523.c
> index 1ab0e2eca5d32..a33cf2e1b76f8 100644
> --- a/drivers/clk/clk-en7523.c
> +++ b/drivers/clk/clk-en7523.c
[ ... ]
> @@ -650,9 +654,7 @@ static void en7581_pci_disable(struct clk_hw *hw)
> void __iomem *np_base = cg->base;
> u32 val, mask;
>
> - mask = REG_PCI_CONTROL_REFCLK_EN0 | REG_PCI_CONTROL_REFCLK_EN1 |
> - REG_PCI_CONTROL_PERSTOUT1 | REG_PCI_CONTROL_PERSTOUT2 |
> - REG_PCI_CONTROL_PERSTOUT;
> + mask = REG_PCI_CONTROL_REFCLK_EN0 | REG_PCI_CONTROL_REFCLK_EN1;
> val = readl(np_base + REG_PCI_CONTROL);
> writel(val & ~mask, np_base + REG_PCI_CONTROL);
> usleep_range(1000, 2000);
[Severity: High]
This is a pre-existing issue, but the Common Clock Framework calls the
.disable callback with the enable_lock spinlock held and hardware interrupts
disabled.
Does calling usleep_range() here sleep in an atomic context and lead to a bug?
[ ... ]
> @@ -754,14 +756,21 @@ static int en7523_reset_update(struct reset_controller_dev *rcdev,
> unsigned long id, bool assert)
> {
> struct en_rst_data *rst_data = container_of(rcdev, struct en_rst_data, rcdev);
> - void __iomem *addr = rst_data->base + rst_data->bank_ofs[id / RST_NR_PER_BANK];
> + u32 offset = rst_data->bank_ofs[id / RST_NR_PER_BANK];
> + void __iomem *addr = rst_data->base + offset;
> + bool inverted = false;
> u32 val;
>
> + /* For PCIC reset logic is inverted, 0:assert 1:deassert*/
> + if (offset == REG_NP_SCU_PCIC)
> + inverted = true;
[Severity: Medium]
It appears en7523_reset_status() wasn't updated to handle the inverted logic:
drivers/clk/clk-en7523.c:en7523_reset_status() {
...
return !!(readl(addr) & BIT(id % RST_NR_PER_BANK));
}
Will callers of reset_control_status() receive incorrect status for these
resets, returning 0 for asserted and 1 for deasserted when querying PCIC
resets?
> +
> val = readl(addr);
> + val &= ~BIT(id % RST_NR_PER_BANK);
> if (assert)
> - val |= BIT(id % RST_NR_PER_BANK);
> + val |= inverted ? 0 : BIT(id % RST_NR_PER_BANK);
> else
> - val &= ~BIT(id % RST_NR_PER_BANK);
> + val |= inverted ? BIT(id % RST_NR_PER_BANK) : 0;
> writel(val, addr);
[Severity: High]
This is a pre-existing issue, but multiple drivers could concurrently probe or
change power states, causing their reset operations on the same register bank
to interleave.
Could this lockless read-modify-write operation allow concurrent reset
operations to corrupt the shared reset registers?
[Severity: High]
en7581_pci_enable() modifies REG_PCI_CONTROL using a lockless
read-modify-write:
drivers/clk/clk-en7523.c:en7581_pci_enable() {
...
val = readl(np_base + REG_PCI_CONTROL);
writel(val | mask, np_base + REG_PCI_CONTROL);
...
}
If REG_NP_SCU_PCIC and REG_PCI_CONTROL point to the same register, a
concurrent clk_enable() and reset_control_assert() could silently corrupt
each other's bits since they belong to different lock domains.
Will this read-modify-write race with the clock framework?
>
> return 0;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625215741.3253212-1-ansuelsmth@gmail.com?part=2
^ permalink raw reply
* [PATCH RFC 4/4] arm64: dts: imx8mq-librem5: Add rear camera
From: Vincent Cloutier @ 2026-06-26 0:07 UTC (permalink / raw)
To: linux-media, devicetree, linux-arm-kernel
Cc: linux-kernel, linux-imx, kernel, Vincent Cloutier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Frank Li, Sascha Hauer,
Pengutronix Kernel Team, Fabio Estevam, imx
In-Reply-To: <20260626000715.1111803-1-vincent.cloutier@icloud.com>
From: Vincent Cloutier <vincent@cloutier.co>
Add the Librem 5 rear camera sensor node and enable the second CSI-2
capture path that receives data from it.
Describe the Samsung S5K3L6 sensor with the upstream binding property
names, including reset-gpios and the link frequencies used by the initial
two-lane RAW8/RAW10 driver modes.
Signed-off-by: Vincent Cloutier <vincent@cloutier.co>
Assisted-by: OpenCode:gpt-5.5
---
arch/arm64/boot/dts/freescale/imx8mq-librem5.dtsi | 51 +++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/arch/arm64/boot/dts/freescale/imx8mq-librem5.dtsi b/arch/arm64/boot/dts/freescale/imx8mq-librem5.dtsi
index f5d529c5baf3..12d5fb3440c5 100644
--- a/arch/arm64/boot/dts/freescale/imx8mq-librem5.dtsi
+++ b/arch/arm64/boot/dts/freescale/imx8mq-librem5.dtsi
@@ -318,6 +318,10 @@ &csi1 {
status = "okay";
};
+&csi2 {
+ status = "okay";
+};
+
&ddrc {
operating-points-v2 = <&ddrc_opp_table>;
status = "okay";
@@ -434,6 +438,13 @@ MX8MQ_IOMUXC_ENET_RXC_GPIO1_IO25 0x83
>;
};
+ pinctrl_csi2: csi2grp {
+ fsl,pins = <
+ /* CSI2_NRST */
+ MX8MQ_IOMUXC_ENET_RD0_GPIO1_IO26 0x83
+ >;
+ };
+
pinctrl_charger_in: chargeringrp {
fsl,pins = <
/* CHRG_INT */
@@ -1175,6 +1186,31 @@ vcm@c {
vcc-supply = <®_csi_1v8>;
};
+ camera_rear: camera@2d {
+ compatible = "samsung,s5k3l6";
+ reg = <0x2d>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_csi2>;
+ clocks = <&clk IMX8MQ_CLK_CLKO2>;
+ assigned-clocks = <&clk IMX8MQ_CLK_CLKO2>;
+ assigned-clock-rates = <25000000>;
+ reset-gpios = <&gpio1 26 GPIO_ACTIVE_LOW>;
+ vdda-supply = <®_vcam_2v8>;
+ vddd-supply = <®_vcam_1v2>;
+ vddio-supply = <®_csi_1v8>;
+ rotation = <270>;
+ orientation = <1>;
+
+ port {
+ camera2_ep: endpoint {
+ data-lanes = <1 2>;
+ link-frequencies = /bits/ 64
+ <537500000 600000000 625000000>;
+ remote-endpoint = <&mipi2_sensor_ep>;
+ };
+ };
+ };
+
bat: fuel-gauge@36 {
compatible = "maxim,max17055";
reg = <0x36>;
@@ -1226,6 +1262,21 @@ mipi1_sensor_ep: endpoint {
};
};
+&mipi_csi2 {
+ status = "okay";
+
+ ports {
+ port@0 {
+ reg = <0>;
+
+ mipi2_sensor_ep: endpoint {
+ remote-endpoint = <&camera2_ep>;
+ data-lanes = <1 2>;
+ };
+ };
+ };
+};
+
&mipi_dsi {
#address-cells = <1>;
#size-cells = <0>;
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 3/4] media: i2c: Add Samsung S5K3L6 image sensor driver
From: Vincent Cloutier @ 2026-06-26 0:06 UTC (permalink / raw)
To: linux-media, devicetree, linux-arm-kernel
Cc: linux-kernel, linux-imx, kernel, Vincent Cloutier,
Mauro Carvalho Chehab, Sakari Ailus, Hans Verkuil, Hans de Goede,
Vladimir Zapolskiy, Mehdi Djait, Laurent Pinchart, Xiaolei Wang,
Walter Werner Schneider, Kate Hsuan, Hardevsinh Palaniya,
Himanshu Bhavani, Svyatoslav Ryhel
In-Reply-To: <20260626000715.1111803-1-vincent.cloutier@icloud.com>
From: Vincent Cloutier <vincent@cloutier.co>
Add a V4L2 sub-device driver for the Samsung S5K3L6 raw Bayer image
sensor.
The initial driver supports the production path used by the Librem 5 rear
camera: a 25 MHz input clock, two MIPI CSI-2 data lanes, RAW8 and RAW10
Bayer modes derived from the full 4208x3120 active array, runtime PM, CCI
register access, basic exposure/gain/blanking controls, test patterns, and
fwnode orientation controls.
This driver is derived from the Librem 5 downstream S5K3L6 carry, initially
authored by Martin Kepplinger and substantially developed by Dorota
Czaplejewicz and Sebastian Krzyszkowiak. The upstream submission rewrites
and collapses that history into a current V4L2 sensor driver shape.
Signed-off-by: Vincent Cloutier <vincent@cloutier.co>
Assisted-by: OpenCode:gpt-5.5
---
drivers/media/i2c/Kconfig | 10 +
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/s5k3l6.c | 1055 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1066 insertions(+)
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 5d173e0ecf42..5ec4db05920a 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -781,6 +781,16 @@ config VIDEO_S5K3M5
To compile this driver as a module, choose M here: the
module will be called s5k3m5.
+config VIDEO_S5K3L6
+ tristate "Samsung S5K3L6 sensor support"
+ select V4L2_CCI_I2C
+ help
+ This is a V4L2 sensor driver for Samsung S5K3L6 13MP raw
+ camera sensor.
+
+ To compile this driver as a module, choose M here: the
+ module will be called s5k3l6.
+
config VIDEO_S5K5BAF
tristate "Samsung S5K5BAF sensor support"
help
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index e45359efe0e4..f3360e97aa38 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -128,6 +128,7 @@ obj-$(CONFIG_VIDEO_RDACM21) += rdacm21.o
obj-$(CONFIG_VIDEO_RJ54N1) += rj54n1cb0c.o
obj-$(CONFIG_VIDEO_S5C73M3) += s5c73m3/
obj-$(CONFIG_VIDEO_S5K3M5) += s5k3m5.o
+obj-$(CONFIG_VIDEO_S5K3L6) += s5k3l6.o
obj-$(CONFIG_VIDEO_S5K5BAF) += s5k5baf.o
obj-$(CONFIG_VIDEO_S5K6A3) += s5k6a3.o
obj-$(CONFIG_VIDEO_S5KJN1) += s5kjn1.o
diff --git a/drivers/media/i2c/s5k3l6.c b/drivers/media/i2c/s5k3l6.c
new file mode 100644
index 000000000000..f70f83e9de17
--- /dev/null
+++ b/drivers/media/i2c/s5k3l6.c
@@ -0,0 +1,1055 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (C) 2020-2021 Purism SPC
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/regulator/consumer.h>
+#include <linux/units.h>
+
+#include <media/v4l2-cci.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-fwnode.h>
+
+#define S5K3L6_MCLK_FREQ (25 * HZ_PER_MHZ)
+#define S5K3L6_DATA_LANES 2
+
+#define S5K3L6_LINK_FREQ_537P5MHZ (537500ULL * HZ_PER_KHZ)
+#define S5K3L6_LINK_FREQ_600MHZ (600ULL * HZ_PER_MHZ)
+#define S5K3L6_LINK_FREQ_625MHZ (625ULL * HZ_PER_MHZ)
+
+/* Downstream mode tables use a fixed 480 MHz VT pixel clock. */
+#define S5K3L6_PIXEL_RATE (480ULL * HZ_PER_MHZ)
+
+#define S5K3L6_PIXEL_ARRAY_WIDTH 4224
+#define S5K3L6_PIXEL_ARRAY_HEIGHT 3136
+#define S5K3L6_ACTIVE_LEFT 8
+#define S5K3L6_ACTIVE_TOP 8
+#define S5K3L6_ACTIVE_WIDTH 4208
+#define S5K3L6_ACTIVE_HEIGHT 3120
+
+#define S5K3L6_CHIP_ID 0x30c6
+#define S5K3L6_REVISION 0xb0
+
+#define S5K3L6_REG_CHIP_ID CCI_REG16(0x0000)
+#define S5K3L6_REG_REVISION CCI_REG8(0x0002)
+#define S5K3L6_REG_MODE_SELECT CCI_REG8(0x0100)
+#define S5K3L6_MODE_STANDBY 0x00
+#define S5K3L6_MODE_STREAMING 0x01
+
+#define S5K3L6_REG_LANE_MODE CCI_REG8(0x0114)
+#define S5K3L6_REG_DATA_FORMAT CCI_REG16(0x0112)
+#define S5K3L6_DATA_FORMAT_RAW8 0x0808
+#define S5K3L6_DATA_FORMAT_RAW10 0x0a0a
+
+#define S5K3L6_REG_OP_PLL_MULTIPLIER CCI_REG16(0x030e)
+#define S5K3L6_REG_REQUESTED_LINK_RATE CCI_REG16(0x0820)
+#define S5K3L6_REG_FRAME_LENGTH CCI_REG16(0x0340)
+#define S5K3L6_REG_LINE_LENGTH CCI_REG16(0x0342)
+#define S5K3L6_REG_X_ADDR_START CCI_REG16(0x0344)
+#define S5K3L6_REG_Y_ADDR_START CCI_REG16(0x0346)
+#define S5K3L6_REG_X_ADDR_END CCI_REG16(0x0348)
+#define S5K3L6_REG_Y_ADDR_END CCI_REG16(0x034a)
+#define S5K3L6_REG_X_OUTPUT_SIZE CCI_REG16(0x034c)
+#define S5K3L6_REG_Y_OUTPUT_SIZE CCI_REG16(0x034e)
+
+#define S5K3L6_REG_EXPOSURE CCI_REG16(0x0202)
+#define S5K3L6_EXPOSURE_MIN 2
+#define S5K3L6_EXPOSURE_MARGIN 2
+
+#define S5K3L6_REG_ANALOG_GAIN CCI_REG16(0x0204)
+#define S5K3L6_ANALOG_GAIN_MIN 0x20
+#define S5K3L6_ANALOG_GAIN_MAX 0x200
+#define S5K3L6_ANALOG_GAIN_DEFAULT 0x20
+
+#define S5K3L6_REG_DIGITAL_GAIN CCI_REG16(0x020e)
+#define S5K3L6_DIGITAL_GAIN_MIN 0x100
+#define S5K3L6_DIGITAL_GAIN_MAX 0x300
+#define S5K3L6_DIGITAL_GAIN_DEFAULT 0x100
+
+#define S5K3L6_REG_TEST_PATTERN CCI_REG8(0x0601)
+
+#define S5K3L6_REG_BPC CCI_REG8(0x3403)
+#define S5K3L6_BPC_FILTER BIT(0)
+#define S5K3L6_BPC_AF_FILTER BIT(2)
+
+#define S5K3L6_REG_PLL_PD CCI_REG8(0x3c1e)
+#define S5K3L6_REG_MIPI_CONTINUOUS CCI_REG8(0x38a1)
+
+#define to_s5k3l6(_sd) container_of(_sd, struct s5k3l6, sd)
+
+static const s64 s5k3l6_link_freq_menu[] = {
+ S5K3L6_LINK_FREQ_537P5MHZ,
+ S5K3L6_LINK_FREQ_600MHZ,
+ S5K3L6_LINK_FREQ_625MHZ,
+};
+
+static const char *const s5k3l6_supply_names[] = {
+ "vddio",
+ "vdda",
+ "vddd",
+};
+
+#define S5K3L6_NUM_SUPPLIES ARRAY_SIZE(s5k3l6_supply_names)
+
+struct s5k3l6_reg_list {
+ const struct cci_reg_sequence *regs;
+ unsigned int num_regs;
+};
+
+struct s5k3l6_mode {
+ u32 width;
+ u32 height;
+ u32 code;
+ u32 hts;
+ u32 vts;
+ u32 data_format;
+ u16 op_pll_multiplier;
+ u8 binning;
+ u8 link_freq_index;
+
+ const struct s5k3l6_reg_list reg_list;
+};
+
+struct s5k3l6 {
+ struct device *dev;
+ struct regmap *regmap;
+ struct clk *mclk;
+ struct gpio_desc *reset_gpio;
+ struct regulator_bulk_data supplies[S5K3L6_NUM_SUPPLIES];
+
+ struct v4l2_subdev sd;
+ struct media_pad pad;
+
+ struct v4l2_ctrl_handler ctrl_handler;
+ struct v4l2_ctrl *link_freq;
+ struct v4l2_ctrl *pixel_rate;
+ struct v4l2_ctrl *hblank;
+ struct v4l2_ctrl *vblank;
+ struct v4l2_ctrl *exposure;
+
+ const struct s5k3l6_mode *mode;
+};
+
+static const struct cci_reg_sequence s5k3l6_binning_4x4[] = {
+ { CCI_REG8(0x0900), 0x01 },
+ { CCI_REG8(0x0901), 0x44 },
+ { CCI_REG8(0x0387), 0x07 },
+ { CCI_REG16(0x3090), 0x8000 },
+ { CCI_REG16(0x314a), 0x5f02 },
+ { CCI_REG16(0x32b2), 0x0006 },
+ { CCI_REG16(0x32b4), 0x0006 },
+ { CCI_REG16(0x32b6), 0x0006 },
+ { CCI_REG16(0x32b8), 0x0006 },
+ { CCI_REG16(0x3238), 0x000a },
+ { CCI_REG16(0x380c), 0x003b },
+};
+
+static const struct cci_reg_sequence s5k3l6_binning_2x2[] = {
+ { CCI_REG8(0x0900), 0x01 },
+ { CCI_REG8(0x0901), 0x22 },
+ { CCI_REG8(0x0387), 0x03 },
+ { CCI_REG16(0x3090), 0x8000 },
+ { CCI_REG16(0x314a), 0x5f02 },
+ { CCI_REG16(0x32b2), 0x0003 },
+ { CCI_REG16(0x32b4), 0x0003 },
+ { CCI_REG16(0x32b6), 0x0003 },
+ { CCI_REG16(0x32b8), 0x0003 },
+ { CCI_REG16(0x3238), 0x000b },
+ { CCI_REG16(0x380c), 0x0049 },
+};
+
+static const struct cci_reg_sequence s5k3l6_no_binning[] = {
+ { CCI_REG8(0x0900), 0x00 },
+ { CCI_REG8(0x0387), 0x01 },
+ { CCI_REG16(0x3090), 0x8800 },
+ { CCI_REG16(0x314a), 0x5f00 },
+ { CCI_REG16(0x32b2), 0x0000 },
+ { CCI_REG16(0x32b4), 0x0000 },
+ { CCI_REG16(0x32b6), 0x0000 },
+ { CCI_REG16(0x32b8), 0x0000 },
+ { CCI_REG16(0x3238), 0x000c },
+ { CCI_REG16(0x380c), 0x0090 },
+};
+
+static const struct cci_reg_sequence s5k3l6_common_regs[] = {
+ { CCI_REG16(0x0136), 0x1900 },
+ { CCI_REG16(0x0300), 0x0005 },
+ { CCI_REG16(0x0304), 0x0004 },
+ { CCI_REG16(0x0306), 0x0060 },
+ { CCI_REG16(0x030c), 0x0004 },
+ { CCI_REG16(0x3002), 0x0e00 },
+ { CCI_REG16(0x3006), 0x1000 },
+ { CCI_REG16(0x300a), 0x0c00 },
+ { CCI_REG16(0x3018), 0xc500 },
+ { CCI_REG16(0x3024), 0x0016 },
+ { CCI_REG16(0x306a), 0x2f4c },
+ { CCI_REG16(0x3070), 0x3d00 },
+ { CCI_REG16(0x3072), 0x0013 },
+ { CCI_REG16(0x3074), 0x0977 },
+ { CCI_REG16(0x3076), 0x9411 },
+ { CCI_REG16(0x307a), 0x0d20 },
+ { CCI_REG16(0x3084), 0x1314 },
+ { CCI_REG16(0x309c), 0x0640 },
+ { CCI_REG16(0x309e), 0x002d },
+ { CCI_REG16(0x3266), 0x0001 },
+ { CCI_REG16(0x3452), 0x0000 },
+ { CCI_REG16(0x345a), 0x0000 },
+ { CCI_REG16(0x345c), 0x0000 },
+ { CCI_REG16(0x345e), 0x0000 },
+ { CCI_REG16(0x3460), 0x0000 },
+ { CCI_REG16(0x38da), 0x000a },
+ { CCI_REG16(0x38dc), 0x000b },
+ { CCI_REG16(0x38d6), 0x000a },
+ { CCI_REG16(0x3932), 0x1000 },
+ { CCI_REG16(0x393e), 0x4000 },
+ { CCI_REG16(0x3c36), 0x2800 },
+ { CCI_REG16(0x3c38), 0x0028 },
+ { S5K3L6_REG_MIPI_CONTINUOUS, 0x7e },
+ { S5K3L6_REG_BPC,
+ 0x42 | S5K3L6_BPC_FILTER | S5K3L6_BPC_AF_FILTER },
+};
+
+static const struct s5k3l6_mode s5k3l6_supported_modes[] = {
+ {
+ .width = 1052,
+ .height = 780,
+ .code = MEDIA_BUS_FMT_SGRBG8_1X8,
+ .hts = 0x1320,
+ .vts = 0x0331,
+ .data_format = S5K3L6_DATA_FORMAT_RAW8,
+ .op_pll_multiplier = 0x56,
+ .binning = 4,
+ .link_freq_index = 0,
+ .reg_list = {
+ .regs = s5k3l6_binning_4x4,
+ .num_regs = ARRAY_SIZE(s5k3l6_binning_4x4),
+ },
+ }, {
+ .width = 2104,
+ .height = 1560,
+ .code = MEDIA_BUS_FMT_SGRBG8_1X8,
+ .hts = 0x1320,
+ .vts = 0x0662,
+ .data_format = S5K3L6_DATA_FORMAT_RAW8,
+ .op_pll_multiplier = 0x56,
+ .binning = 2,
+ .link_freq_index = 0,
+ .reg_list = {
+ .regs = s5k3l6_binning_2x2,
+ .num_regs = ARRAY_SIZE(s5k3l6_binning_2x2),
+ },
+ }, {
+ .width = 4208,
+ .height = 3120,
+ .code = MEDIA_BUS_FMT_SGRBG8_1X8,
+ .hts = 0x1ce0,
+ .vts = 0x0cbc,
+ .data_format = S5K3L6_DATA_FORMAT_RAW8,
+ .op_pll_multiplier = 0x64,
+ .binning = 1,
+ .link_freq_index = 2,
+ .reg_list = {
+ .regs = s5k3l6_no_binning,
+ .num_regs = ARRAY_SIZE(s5k3l6_no_binning),
+ },
+ }, {
+ .width = 1052,
+ .height = 780,
+ .code = MEDIA_BUS_FMT_SGRBG10_1X10,
+ .hts = 0x1320,
+ .vts = 0x0331,
+ .data_format = S5K3L6_DATA_FORMAT_RAW10,
+ .op_pll_multiplier = 0x56,
+ .binning = 4,
+ .link_freq_index = 0,
+ .reg_list = {
+ .regs = s5k3l6_binning_4x4,
+ .num_regs = ARRAY_SIZE(s5k3l6_binning_4x4),
+ },
+ }, {
+ .width = 2104,
+ .height = 1560,
+ .code = MEDIA_BUS_FMT_SGRBG10_1X10,
+ .hts = 0x1320,
+ .vts = 0x0662,
+ .data_format = S5K3L6_DATA_FORMAT_RAW10,
+ .op_pll_multiplier = 0x60,
+ .binning = 2,
+ .link_freq_index = 1,
+ .reg_list = {
+ .regs = s5k3l6_binning_2x2,
+ .num_regs = ARRAY_SIZE(s5k3l6_binning_2x2),
+ },
+ }, {
+ .width = 4208,
+ .height = 3120,
+ .code = MEDIA_BUS_FMT_SGRBG10_1X10,
+ .hts = 0x2650,
+ .vts = 0x0cbc,
+ .data_format = S5K3L6_DATA_FORMAT_RAW10,
+ .op_pll_multiplier = 0x64,
+ .binning = 1,
+ .link_freq_index = 2,
+ .reg_list = {
+ .regs = s5k3l6_no_binning,
+ .num_regs = ARRAY_SIZE(s5k3l6_no_binning),
+ },
+ },
+};
+
+static const char *const s5k3l6_test_pattern_menu[] = {
+ "Disabled",
+ "Solid color",
+ "Color bars",
+};
+
+static bool s5k3l6_code_supported(u32 code)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(s5k3l6_supported_modes); i++) {
+ if (s5k3l6_supported_modes[i].code == code)
+ return true;
+ }
+
+ return false;
+}
+
+static const struct s5k3l6_mode *s5k3l6_find_mode(u32 code, u32 width,
+ u32 height)
+{
+ const struct s5k3l6_mode *best = NULL;
+ u32 best_delta = U32_MAX;
+ unsigned int i;
+
+ if (!s5k3l6_code_supported(code))
+ code = s5k3l6_supported_modes[0].code;
+
+ for (i = 0; i < ARRAY_SIZE(s5k3l6_supported_modes); i++) {
+ const struct s5k3l6_mode *mode = &s5k3l6_supported_modes[i];
+ u32 delta;
+
+ if (mode->code != code)
+ continue;
+
+ delta = abs((int)mode->width - (int)width) +
+ abs((int)mode->height - (int)height);
+ if (!best || delta < best_delta) {
+ best = mode;
+ best_delta = delta;
+ }
+ }
+
+ return best;
+}
+
+static void s5k3l6_update_pad_format(const struct s5k3l6_mode *mode,
+ struct v4l2_mbus_framefmt *fmt)
+{
+ fmt->code = mode->code;
+ fmt->width = mode->width;
+ fmt->height = mode->height;
+ fmt->field = V4L2_FIELD_NONE;
+ fmt->colorspace = V4L2_COLORSPACE_RAW;
+ fmt->ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT;
+ fmt->quantization = V4L2_QUANTIZATION_FULL_RANGE;
+ fmt->xfer_func = V4L2_XFER_FUNC_NONE;
+}
+
+static void s5k3l6_update_controls(struct s5k3l6 *s5k3l6,
+ const struct s5k3l6_mode *mode)
+{
+ u32 hblank = mode->hts - mode->width;
+ u32 vblank = mode->vts - mode->height;
+ u32 exposure_max = mode->vts - S5K3L6_EXPOSURE_MARGIN;
+ u32 exposure_default = min_t(u32, 0x03de, exposure_max);
+
+ __v4l2_ctrl_s_ctrl(s5k3l6->link_freq, mode->link_freq_index);
+ __v4l2_ctrl_modify_range(s5k3l6->hblank, hblank, hblank, 1, hblank);
+ __v4l2_ctrl_modify_range(s5k3l6->vblank, vblank,
+ 0xffff - mode->height, 1, vblank);
+ __v4l2_ctrl_s_ctrl(s5k3l6->vblank, vblank);
+ __v4l2_ctrl_modify_range(s5k3l6->exposure, S5K3L6_EXPOSURE_MIN,
+ exposure_max, 1, exposure_default);
+ __v4l2_ctrl_s_ctrl(s5k3l6->exposure, exposure_default);
+}
+
+static int s5k3l6_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct s5k3l6 *s5k3l6 =
+ container_of(ctrl->handler, struct s5k3l6, ctrl_handler);
+ const struct s5k3l6_mode *mode = s5k3l6->mode;
+ u32 frame_length;
+ u32 exposure_max;
+ int active;
+ int ret;
+
+ if (ctrl->id == V4L2_CID_VBLANK) {
+ exposure_max =
+ mode->height + ctrl->val - S5K3L6_EXPOSURE_MARGIN;
+ __v4l2_ctrl_modify_range(s5k3l6->exposure,
+ S5K3L6_EXPOSURE_MIN, exposure_max, 1,
+ s5k3l6->exposure->default_value);
+ }
+
+ active = pm_runtime_get_if_active(s5k3l6->dev);
+ if (!active)
+ return 0;
+ if (active < 0)
+ return active;
+
+ switch (ctrl->id) {
+ case V4L2_CID_ANALOGUE_GAIN:
+ ret = cci_write(s5k3l6->regmap, S5K3L6_REG_ANALOG_GAIN,
+ ctrl->val, NULL);
+ break;
+ case V4L2_CID_DIGITAL_GAIN:
+ ret = cci_write(s5k3l6->regmap, S5K3L6_REG_DIGITAL_GAIN,
+ ctrl->val, NULL);
+ break;
+ case V4L2_CID_EXPOSURE:
+ ret = cci_write(s5k3l6->regmap, S5K3L6_REG_EXPOSURE,
+ ctrl->val, NULL);
+ break;
+ case V4L2_CID_VBLANK:
+ frame_length = mode->height + ctrl->val;
+ ret = cci_write(s5k3l6->regmap, S5K3L6_REG_FRAME_LENGTH,
+ frame_length, NULL);
+ break;
+ case V4L2_CID_TEST_PATTERN:
+ ret = cci_write(s5k3l6->regmap, S5K3L6_REG_TEST_PATTERN,
+ ctrl->val, NULL);
+ break;
+ default:
+ ret = 0;
+ break;
+ }
+
+ pm_runtime_put(s5k3l6->dev);
+
+ return ret;
+}
+
+static const struct v4l2_ctrl_ops s5k3l6_ctrl_ops = {
+ .s_ctrl = s5k3l6_set_ctrl,
+};
+
+static int s5k3l6_init_controls(struct s5k3l6 *s5k3l6)
+{
+ struct v4l2_ctrl_handler *ctrl_hdlr = &s5k3l6->ctrl_handler;
+ const struct v4l2_ctrl_ops *ops = &s5k3l6_ctrl_ops;
+ const struct s5k3l6_mode *mode = s5k3l6->mode;
+ struct v4l2_fwnode_device_properties props;
+ u32 hblank = mode->hts - mode->width;
+ u32 vblank = mode->vts - mode->height;
+ u32 exposure_max = mode->vts - S5K3L6_EXPOSURE_MARGIN;
+ u32 exposure_default = min_t(u32, 0x03de, exposure_max);
+ int ret;
+
+ v4l2_ctrl_handler_init(ctrl_hdlr, 10);
+
+ s5k3l6->link_freq =
+ v4l2_ctrl_new_int_menu(ctrl_hdlr, &s5k3l6_ctrl_ops,
+ V4L2_CID_LINK_FREQ,
+ ARRAY_SIZE(s5k3l6_link_freq_menu) - 1,
+ mode->link_freq_index,
+ s5k3l6_link_freq_menu);
+ if (s5k3l6->link_freq)
+ s5k3l6->link_freq->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+ s5k3l6->pixel_rate =
+ v4l2_ctrl_new_std(ctrl_hdlr, &s5k3l6_ctrl_ops,
+ V4L2_CID_PIXEL_RATE, S5K3L6_PIXEL_RATE,
+ S5K3L6_PIXEL_RATE, 1, S5K3L6_PIXEL_RATE);
+ if (s5k3l6->pixel_rate)
+ s5k3l6->pixel_rate->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+ s5k3l6->hblank = v4l2_ctrl_new_std(ctrl_hdlr, &s5k3l6_ctrl_ops,
+ V4L2_CID_HBLANK, hblank, hblank, 1,
+ hblank);
+ if (s5k3l6->hblank)
+ s5k3l6->hblank->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+
+ s5k3l6->vblank = v4l2_ctrl_new_std(ctrl_hdlr, &s5k3l6_ctrl_ops,
+ V4L2_CID_VBLANK, vblank,
+ 0xffff - mode->height, 1, vblank);
+
+ v4l2_ctrl_new_std(ctrl_hdlr, &s5k3l6_ctrl_ops, V4L2_CID_ANALOGUE_GAIN,
+ S5K3L6_ANALOG_GAIN_MIN, S5K3L6_ANALOG_GAIN_MAX, 1,
+ S5K3L6_ANALOG_GAIN_DEFAULT);
+
+ v4l2_ctrl_new_std(ctrl_hdlr, &s5k3l6_ctrl_ops, V4L2_CID_DIGITAL_GAIN,
+ S5K3L6_DIGITAL_GAIN_MIN, S5K3L6_DIGITAL_GAIN_MAX,
+ 1, S5K3L6_DIGITAL_GAIN_DEFAULT);
+
+ s5k3l6->exposure =
+ v4l2_ctrl_new_std(ctrl_hdlr, ops, V4L2_CID_EXPOSURE,
+ S5K3L6_EXPOSURE_MIN, exposure_max, 1,
+ exposure_default);
+
+ v4l2_ctrl_new_std_menu_items(ctrl_hdlr, &s5k3l6_ctrl_ops,
+ V4L2_CID_TEST_PATTERN,
+ ARRAY_SIZE(s5k3l6_test_pattern_menu) - 1,
+ 0, 0, s5k3l6_test_pattern_menu);
+
+ if (ctrl_hdlr->error) {
+ ret = ctrl_hdlr->error;
+ goto error_free_hdlr;
+ }
+
+ ret = v4l2_fwnode_device_parse(s5k3l6->dev, &props);
+ if (ret)
+ goto error_free_hdlr;
+
+ ret = v4l2_ctrl_new_fwnode_properties(ctrl_hdlr, &s5k3l6_ctrl_ops,
+ &props);
+ if (ret)
+ goto error_free_hdlr;
+
+ s5k3l6->sd.ctrl_handler = ctrl_hdlr;
+
+ return 0;
+
+error_free_hdlr:
+ v4l2_ctrl_handler_free(ctrl_hdlr);
+
+ return ret;
+}
+
+static int s5k3l6_write_mode(struct s5k3l6 *s5k3l6)
+{
+ const struct s5k3l6_mode *mode = s5k3l6->mode;
+ u64 link_freq = s5k3l6_link_freq_menu[mode->link_freq_index];
+ /*
+ * Match downstream integer-MHz programming. The 537.5 MHz mode uses
+ * 1074 here, not the rounded 1075 Mb/s DDR lane rate.
+ */
+ u16 link_rate = div_u64(link_freq, HZ_PER_MHZ) * 2;
+ u32 crop_width = mode->width * mode->binning;
+ u32 crop_height = mode->height * mode->binning;
+ u32 x_start = S5K3L6_ACTIVE_LEFT;
+ u32 y_start = S5K3L6_ACTIVE_TOP;
+ u32 x_end = x_start + crop_width - 1;
+ u32 y_end = y_start + crop_height - 1;
+ struct cci_reg_sequence crop_regs[] = {
+ { S5K3L6_REG_X_OUTPUT_SIZE, mode->width },
+ { S5K3L6_REG_Y_OUTPUT_SIZE, mode->height },
+ { S5K3L6_REG_Y_ADDR_START, y_start },
+ { S5K3L6_REG_Y_ADDR_END, y_end },
+ { S5K3L6_REG_X_ADDR_START, x_start },
+ { S5K3L6_REG_X_ADDR_END, x_end },
+ };
+ struct cci_reg_sequence format_regs[] = {
+ { S5K3L6_REG_DATA_FORMAT, mode->data_format },
+ { S5K3L6_REG_LANE_MODE, S5K3L6_DATA_LANES - 1 },
+ { S5K3L6_REG_OP_PLL_MULTIPLIER, mode->op_pll_multiplier },
+ { S5K3L6_REG_REQUESTED_LINK_RATE, link_rate },
+ { S5K3L6_REG_LINE_LENGTH, mode->hts },
+ };
+ int ret = 0;
+
+ cci_multi_reg_write(s5k3l6->regmap, crop_regs, ARRAY_SIZE(crop_regs),
+ &ret);
+ cci_multi_reg_write(s5k3l6->regmap, mode->reg_list.regs,
+ mode->reg_list.num_regs, &ret);
+ cci_multi_reg_write(s5k3l6->regmap, s5k3l6_common_regs,
+ ARRAY_SIZE(s5k3l6_common_regs), &ret);
+ cci_multi_reg_write(s5k3l6->regmap, format_regs,
+ ARRAY_SIZE(format_regs), &ret);
+
+ return ret;
+}
+
+static int s5k3l6_enable_streams(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state, u32 pad,
+ u64 streams_mask)
+{
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+ int ret;
+
+ ret = pm_runtime_resume_and_get(s5k3l6->dev);
+ if (ret)
+ return ret;
+
+ cci_write(s5k3l6->regmap, S5K3L6_REG_PLL_PD, 0x01, &ret);
+ if (ret)
+ goto error;
+
+ ret = s5k3l6_write_mode(s5k3l6);
+ if (ret)
+ goto error;
+
+ ret = __v4l2_ctrl_handler_setup(s5k3l6->sd.ctrl_handler);
+ if (ret)
+ goto error;
+
+ cci_write(s5k3l6->regmap, S5K3L6_REG_MODE_SELECT,
+ S5K3L6_MODE_STREAMING, &ret);
+ cci_write(s5k3l6->regmap, S5K3L6_REG_PLL_PD, 0x00, &ret);
+ if (ret)
+ goto error;
+
+ return 0;
+
+error:
+ dev_err(s5k3l6->dev, "failed to start streaming: %d\n", ret);
+ pm_runtime_put_autosuspend(s5k3l6->dev);
+
+ return ret;
+}
+
+static int s5k3l6_disable_streams(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state, u32 pad,
+ u64 streams_mask)
+{
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+ int ret;
+
+ ret = cci_write(s5k3l6->regmap, S5K3L6_REG_MODE_SELECT,
+ S5K3L6_MODE_STANDBY, NULL);
+ if (ret)
+ dev_err(s5k3l6->dev, "failed to stop streaming: %d\n", ret);
+
+ pm_runtime_put_autosuspend(s5k3l6->dev);
+
+ return ret;
+}
+
+static int s5k3l6_set_pad_format(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_format *fmt)
+{
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+ const struct s5k3l6_mode *mode;
+
+ mode = s5k3l6_find_mode(fmt->format.code, fmt->format.width,
+ fmt->format.height);
+ s5k3l6_update_pad_format(mode, &fmt->format);
+
+ if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE && s5k3l6->mode != mode) {
+ s5k3l6->mode = mode;
+ s5k3l6_update_controls(s5k3l6, mode);
+ }
+
+ *v4l2_subdev_state_get_format(state, 0) = fmt->format;
+
+ return 0;
+}
+
+static int s5k3l6_enum_mbus_code(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_mbus_code_enum *code)
+{
+ static const u32 codes[] = {
+ MEDIA_BUS_FMT_SGRBG8_1X8,
+ MEDIA_BUS_FMT_SGRBG10_1X10,
+ };
+
+ if (code->index >= ARRAY_SIZE(codes))
+ return -EINVAL;
+
+ code->code = codes[code->index];
+
+ return 0;
+}
+
+static int s5k3l6_enum_frame_size(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_frame_size_enum *fse)
+{
+ unsigned int index = 0;
+ unsigned int i;
+
+ if (!s5k3l6_code_supported(fse->code))
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(s5k3l6_supported_modes); i++) {
+ const struct s5k3l6_mode *mode = &s5k3l6_supported_modes[i];
+
+ if (mode->code != fse->code)
+ continue;
+
+ if (index++ != fse->index)
+ continue;
+
+ fse->min_width = mode->width;
+ fse->max_width = mode->width;
+ fse->min_height = mode->height;
+ fse->max_height = mode->height;
+
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static int s5k3l6_get_selection(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_selection *sel)
+{
+ switch (sel->target) {
+ case V4L2_SEL_TGT_NATIVE_SIZE:
+ sel->r.left = 0;
+ sel->r.top = 0;
+ sel->r.width = S5K3L6_PIXEL_ARRAY_WIDTH;
+ sel->r.height = S5K3L6_PIXEL_ARRAY_HEIGHT;
+ return 0;
+ case V4L2_SEL_TGT_CROP_BOUNDS:
+ case V4L2_SEL_TGT_CROP_DEFAULT:
+ case V4L2_SEL_TGT_CROP:
+ sel->r.left = S5K3L6_ACTIVE_LEFT;
+ sel->r.top = S5K3L6_ACTIVE_TOP;
+ sel->r.width = S5K3L6_ACTIVE_WIDTH;
+ sel->r.height = S5K3L6_ACTIVE_HEIGHT;
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int s5k3l6_get_mbus_config(struct v4l2_subdev *sd, unsigned int pad,
+ struct v4l2_mbus_config *config)
+{
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+ const struct s5k3l6_mode *mode = s5k3l6->mode;
+
+ if (pad)
+ return -EINVAL;
+
+ *config = (struct v4l2_mbus_config) {
+ .type = V4L2_MBUS_CSI2_DPHY,
+ .link_freq = s5k3l6_link_freq_menu[mode->link_freq_index],
+ };
+ config->bus.mipi_csi2.num_data_lanes = S5K3L6_DATA_LANES;
+
+ return 0;
+}
+
+static int s5k3l6_init_state(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state)
+{
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+ struct v4l2_subdev_format fmt = {
+ .which = V4L2_SUBDEV_FORMAT_TRY,
+ .pad = 0,
+ .format = {
+ .code = s5k3l6->mode->code,
+ .width = s5k3l6->mode->width,
+ .height = s5k3l6->mode->height,
+ },
+ };
+
+ s5k3l6_set_pad_format(sd, state, &fmt);
+
+ return 0;
+}
+
+static const struct v4l2_subdev_video_ops s5k3l6_video_ops = {
+ .s_stream = v4l2_subdev_s_stream_helper,
+};
+
+static const struct v4l2_subdev_pad_ops s5k3l6_pad_ops = {
+ .set_fmt = s5k3l6_set_pad_format,
+ .get_fmt = v4l2_subdev_get_fmt,
+ .get_selection = s5k3l6_get_selection,
+ .get_mbus_config = s5k3l6_get_mbus_config,
+ .enum_mbus_code = s5k3l6_enum_mbus_code,
+ .enum_frame_size = s5k3l6_enum_frame_size,
+ .enable_streams = s5k3l6_enable_streams,
+ .disable_streams = s5k3l6_disable_streams,
+};
+
+static const struct v4l2_subdev_ops s5k3l6_subdev_ops = {
+ .video = &s5k3l6_video_ops,
+ .pad = &s5k3l6_pad_ops,
+};
+
+static const struct v4l2_subdev_internal_ops s5k3l6_internal_ops = {
+ .init_state = s5k3l6_init_state,
+};
+
+static const struct media_entity_operations s5k3l6_entity_ops = {
+ .link_validate = v4l2_subdev_link_validate,
+};
+
+static int s5k3l6_identify_sensor(struct s5k3l6 *s5k3l6)
+{
+ u64 val;
+ int ret;
+
+ ret = cci_read(s5k3l6->regmap, S5K3L6_REG_CHIP_ID, &val, NULL);
+ if (ret)
+ return dev_err_probe(s5k3l6->dev, ret,
+ "failed to read chip id\n");
+
+ if (val != S5K3L6_CHIP_ID)
+ return dev_err_probe(s5k3l6->dev, -ENODEV,
+ "chip id mismatch: %x != %llx\n",
+ S5K3L6_CHIP_ID, val);
+
+ ret = cci_read(s5k3l6->regmap, S5K3L6_REG_REVISION, &val, NULL);
+ if (ret)
+ return dev_err_probe(s5k3l6->dev, ret,
+ "failed to read revision\n");
+
+ if (val != S5K3L6_REVISION)
+ return dev_err_probe(s5k3l6->dev, -ENODEV,
+ "revision mismatch: %x != %llx\n",
+ S5K3L6_REVISION, val);
+
+ return 0;
+}
+
+static int s5k3l6_check_hwcfg(struct s5k3l6 *s5k3l6)
+{
+ struct fwnode_handle *fwnode = dev_fwnode(s5k3l6->dev);
+ struct v4l2_fwnode_endpoint bus_cfg = {
+ .bus = {
+ .mipi_csi2 = {
+ .num_data_lanes = S5K3L6_DATA_LANES,
+ },
+ },
+ .bus_type = V4L2_MBUS_CSI2_DPHY,
+ };
+ struct fwnode_handle *ep;
+ unsigned long freq_bitmap;
+ unsigned long expected_bitmap;
+ int ret;
+
+ if (!fwnode)
+ return -ENODEV;
+
+ ep = fwnode_graph_get_next_endpoint(fwnode, NULL);
+ if (!ep)
+ return -EINVAL;
+
+ ret = v4l2_fwnode_endpoint_alloc_parse(ep, &bus_cfg);
+ fwnode_handle_put(ep);
+ if (ret)
+ return ret;
+
+ if (bus_cfg.bus.mipi_csi2.num_data_lanes != S5K3L6_DATA_LANES) {
+ dev_err(s5k3l6->dev, "only %u data lanes are supported\n",
+ S5K3L6_DATA_LANES);
+ ret = -EINVAL;
+ goto endpoint_free;
+ }
+
+ ret = v4l2_link_freq_to_bitmap(s5k3l6->dev, bus_cfg.link_frequencies,
+ bus_cfg.nr_of_link_frequencies,
+ s5k3l6_link_freq_menu,
+ ARRAY_SIZE(s5k3l6_link_freq_menu),
+ &freq_bitmap);
+ if (ret)
+ goto endpoint_free;
+
+ expected_bitmap = GENMASK(ARRAY_SIZE(s5k3l6_link_freq_menu) - 1, 0);
+ if (freq_bitmap != expected_bitmap) {
+ dev_err(s5k3l6->dev, "not all link frequencies are listed\n");
+ ret = -EINVAL;
+ }
+
+endpoint_free:
+ v4l2_fwnode_endpoint_free(&bus_cfg);
+
+ return ret;
+}
+
+static int s5k3l6_power_on(struct device *dev)
+{
+ struct v4l2_subdev *sd = dev_get_drvdata(dev);
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+ int ret;
+
+ ret = regulator_bulk_enable(S5K3L6_NUM_SUPPLIES, s5k3l6->supplies);
+ if (ret)
+ return ret;
+
+ usleep_range(10, 20);
+
+ ret = clk_prepare_enable(s5k3l6->mclk);
+ if (ret)
+ goto disable_regulators;
+
+ gpiod_set_value_cansleep(s5k3l6->reset_gpio, 0);
+ usleep_range(USEC_PER_MSEC, 1200);
+
+ gpiod_set_value_cansleep(s5k3l6->reset_gpio, 1);
+ usleep_range(400, 800);
+ gpiod_set_value_cansleep(s5k3l6->reset_gpio, 0);
+
+ usleep_range(10 * USEC_PER_MSEC, 11 * USEC_PER_MSEC);
+
+ return 0;
+
+disable_regulators:
+ regulator_bulk_disable(S5K3L6_NUM_SUPPLIES, s5k3l6->supplies);
+
+ return ret;
+}
+
+static int s5k3l6_power_off(struct device *dev)
+{
+ struct v4l2_subdev *sd = dev_get_drvdata(dev);
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+
+ gpiod_set_value_cansleep(s5k3l6->reset_gpio, 1);
+ clk_disable_unprepare(s5k3l6->mclk);
+
+ return regulator_bulk_disable(S5K3L6_NUM_SUPPLIES,
+ s5k3l6->supplies);
+}
+
+static int s5k3l6_probe(struct i2c_client *client)
+{
+ struct s5k3l6 *s5k3l6;
+ unsigned long freq;
+ unsigned int i;
+ int ret;
+
+ s5k3l6 = devm_kzalloc(&client->dev, sizeof(*s5k3l6), GFP_KERNEL);
+ if (!s5k3l6)
+ return -ENOMEM;
+
+ s5k3l6->dev = &client->dev;
+ v4l2_i2c_subdev_init(&s5k3l6->sd, client, &s5k3l6_subdev_ops);
+
+ s5k3l6->regmap = devm_cci_regmap_init_i2c(client, 16);
+ if (IS_ERR(s5k3l6->regmap))
+ return dev_err_probe(s5k3l6->dev, PTR_ERR(s5k3l6->regmap),
+ "failed to init CCI\n");
+
+ s5k3l6->mclk = devm_v4l2_sensor_clk_get(s5k3l6->dev, NULL);
+ if (IS_ERR(s5k3l6->mclk))
+ return dev_err_probe(s5k3l6->dev, PTR_ERR(s5k3l6->mclk),
+ "failed to get clock\n");
+
+ freq = clk_get_rate(s5k3l6->mclk);
+ if (freq != S5K3L6_MCLK_FREQ)
+ return dev_err_probe(s5k3l6->dev, -EINVAL,
+ "clock frequency %lu is not supported\n",
+ freq);
+
+ ret = s5k3l6_check_hwcfg(s5k3l6);
+ if (ret)
+ return dev_err_probe(s5k3l6->dev, ret,
+ "failed to check HW configuration\n");
+
+ s5k3l6->reset_gpio =
+ devm_gpiod_get_optional(s5k3l6->dev, "reset", GPIOD_OUT_HIGH);
+ if (IS_ERR(s5k3l6->reset_gpio))
+ return dev_err_probe(s5k3l6->dev,
+ PTR_ERR(s5k3l6->reset_gpio),
+ "failed to get reset GPIO\n");
+
+ for (i = 0; i < S5K3L6_NUM_SUPPLIES; i++)
+ s5k3l6->supplies[i].supply = s5k3l6_supply_names[i];
+
+ ret = devm_regulator_bulk_get(s5k3l6->dev, S5K3L6_NUM_SUPPLIES,
+ s5k3l6->supplies);
+ if (ret)
+ return dev_err_probe(s5k3l6->dev, ret,
+ "failed to get supplies\n");
+
+ ret = s5k3l6_power_on(s5k3l6->dev);
+ if (ret)
+ return ret;
+
+ ret = s5k3l6_identify_sensor(s5k3l6);
+ if (ret)
+ goto power_off;
+
+ s5k3l6->mode = &s5k3l6_supported_modes[0];
+ ret = s5k3l6_init_controls(s5k3l6);
+ if (ret)
+ goto power_off;
+
+ s5k3l6->sd.state_lock = s5k3l6->ctrl_handler.lock;
+ s5k3l6->sd.internal_ops = &s5k3l6_internal_ops;
+ s5k3l6->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+ s5k3l6->sd.entity.ops = &s5k3l6_entity_ops;
+ s5k3l6->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+ s5k3l6->pad.flags = MEDIA_PAD_FL_SOURCE;
+
+ ret = media_entity_pads_init(&s5k3l6->sd.entity, 1, &s5k3l6->pad);
+ if (ret)
+ goto free_ctrls;
+
+ ret = v4l2_subdev_init_finalize(&s5k3l6->sd);
+ if (ret)
+ goto cleanup_entity;
+
+ pm_runtime_set_active(s5k3l6->dev);
+ pm_runtime_enable(s5k3l6->dev);
+
+ ret = v4l2_async_register_subdev_sensor(&s5k3l6->sd);
+ if (ret)
+ goto cleanup_subdev;
+
+ pm_runtime_set_autosuspend_delay(s5k3l6->dev, 1000);
+ pm_runtime_use_autosuspend(s5k3l6->dev);
+ pm_runtime_idle(s5k3l6->dev);
+
+ return 0;
+
+cleanup_subdev:
+ v4l2_subdev_cleanup(&s5k3l6->sd);
+ pm_runtime_disable(s5k3l6->dev);
+ pm_runtime_set_suspended(s5k3l6->dev);
+
+cleanup_entity:
+ media_entity_cleanup(&s5k3l6->sd.entity);
+
+free_ctrls:
+ v4l2_ctrl_handler_free(s5k3l6->sd.ctrl_handler);
+
+power_off:
+ s5k3l6_power_off(s5k3l6->dev);
+
+ return ret;
+}
+
+static void s5k3l6_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct s5k3l6 *s5k3l6 = to_s5k3l6(sd);
+
+ v4l2_async_unregister_subdev(sd);
+ v4l2_subdev_cleanup(sd);
+ media_entity_cleanup(&sd->entity);
+ v4l2_ctrl_handler_free(sd->ctrl_handler);
+ pm_runtime_disable(s5k3l6->dev);
+
+ if (!pm_runtime_status_suspended(s5k3l6->dev)) {
+ s5k3l6_power_off(s5k3l6->dev);
+ pm_runtime_set_suspended(s5k3l6->dev);
+ }
+}
+
+static const struct dev_pm_ops s5k3l6_pm_ops = {
+ SET_RUNTIME_PM_OPS(s5k3l6_power_off, s5k3l6_power_on, NULL)
+};
+
+static const struct of_device_id s5k3l6_of_match[] = {
+ { .compatible = "samsung,s5k3l6" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, s5k3l6_of_match);
+
+static struct i2c_driver s5k3l6_i2c_driver = {
+ .driver = {
+ .name = "s5k3l6",
+ .pm = &s5k3l6_pm_ops,
+ .of_match_table = s5k3l6_of_match,
+ },
+ .probe = s5k3l6_probe,
+ .remove = s5k3l6_remove,
+};
+
+module_i2c_driver(s5k3l6_i2c_driver);
+
+MODULE_DESCRIPTION("Samsung S5K3L6 image sensor driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 2/4] dt-bindings: media: i2c: Add Samsung S5K3L6 image sensor
From: Vincent Cloutier @ 2026-06-26 0:06 UTC (permalink / raw)
To: linux-media, devicetree, linux-arm-kernel
Cc: linux-kernel, linux-imx, kernel, Vincent Cloutier,
Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
Conor Dooley
In-Reply-To: <20260626000715.1111803-1-vincent.cloutier@icloud.com>
From: Vincent Cloutier <vincent@cloutier.co>
Add a devicetree binding for the Samsung S5K3L6 13MP raw Bayer image
sensor.
This starts the upstreaming work for the Librem 5 rear camera path. The
binding describes the validated two-lane MIPI CSI-2 configuration.
Signed-off-by: Vincent Cloutier <vincent@cloutier.co>
Assisted-by: OpenCode:gpt-5.5
---
.../bindings/media/i2c/samsung,s5k3l6.yaml | 117 +++++++++++++++++++++
1 file changed, 117 insertions(+)
diff --git a/Documentation/devicetree/bindings/media/i2c/samsung,s5k3l6.yaml b/Documentation/devicetree/bindings/media/i2c/samsung,s5k3l6.yaml
new file mode 100644
index 000000000000..96150764b341
--- /dev/null
+++ b/Documentation/devicetree/bindings/media/i2c/samsung,s5k3l6.yaml
@@ -0,0 +1,117 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/media/i2c/samsung,s5k3l6.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Samsung S5K3L6 13MP MIPI CSI-2 image sensor
+
+maintainers:
+ - Vincent Cloutier <vincent@cloutier.co>
+ - Purism Kernel Team <kernel@puri.sm>
+
+description: |-
+ The Samsung S5K3L6 is a raw Bayer image sensor with a MIPI CSI-2 image
+ data interface and an I2C-compatible control bus.
+
+allOf:
+ - $ref: /schemas/media/video-interface-devices.yaml#
+
+properties:
+ compatible:
+ const: samsung,s5k3l6
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ description: Reference to the sensor input clock.
+ maxItems: 1
+
+ reset-gpios:
+ description: Active-low reset GPIO.
+ maxItems: 1
+
+ vdda-supply:
+ description: Analog power supply.
+
+ vddd-supply:
+ description: Digital core power supply.
+
+ vddio-supply:
+ description: Digital I/O power supply.
+
+ port:
+ $ref: /schemas/graph.yaml#/$defs/port-base
+ unevaluatedProperties: false
+
+ properties:
+ endpoint:
+ $ref: /schemas/media/video-interfaces.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ data-lanes:
+ items:
+ - const: 1
+ - const: 2
+
+ link-frequencies:
+ minItems: 3
+ maxItems: 3
+ items:
+ - const: 537500000
+ - const: 600000000
+ - const: 625000000
+
+ required:
+ - data-lanes
+ - link-frequencies
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - vdda-supply
+ - vddd-supply
+ - vddio-supply
+ - port
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/imx8mq-clock.h>
+ #include <dt-bindings/gpio/gpio.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ camera@2d {
+ compatible = "samsung,s5k3l6";
+ reg = <0x2d>;
+
+ clocks = <&clk IMX8MQ_CLK_CLKO2>;
+ assigned-clocks = <&clk IMX8MQ_CLK_CLKO2>;
+ assigned-clock-rates = <25000000>;
+
+ reset-gpios = <&gpio1 26 GPIO_ACTIVE_LOW>;
+ vdda-supply = <®_camera_vdda>;
+ vddd-supply = <®_camera_vddd>;
+ vddio-supply = <®_camera_vddio>;
+
+ orientation = <1>;
+ rotation = <270>;
+
+ port {
+ camera_out: endpoint {
+ data-lanes = <1 2>;
+ link-frequencies = /bits/ 64
+ <537500000 600000000 625000000>;
+ remote-endpoint = <&mipi_csi2_in>;
+ };
+ };
+ };
+ };
+...
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 1/4] media: imx8mq-mipi-csi2: Make reset release SoC-specific
From: Vincent Cloutier @ 2026-06-26 0:06 UTC (permalink / raw)
To: linux-media, devicetree, linux-arm-kernel
Cc: linux-kernel, linux-imx, kernel, Vincent Cloutier,
Laurent Pinchart, Frank Li, Martin Kepplinger-Novakovic,
Rui Miguel Silva, Mauro Carvalho Chehab, Sascha Hauer,
Pengutronix Kernel Team, Fabio Estevam, Philipp Zabel, imx
In-Reply-To: <20260626000715.1111803-1-vincent.cloutier@icloud.com>
From: Vincent Cloutier <vincent@cloutier.co>
The CSI-2 software reset helper currently asserts the reset control and
then releases it again unconditionally.
That release step is required by the i.MX8QXP path, but it changes the
reset sequence used by i.MX8MQ. On Librem 5r4, which is i.MX8MQ-based,
the unconditional release step prevents the camera pipeline from producing
frames after reset; captures time out waiting for EOF from the CSI bridge.
This series enables the Librem 5 rear camera on the second i.MX8MQ CSI-2
receiver. Keep the i.MX8MQ path on the known-working assert-only software
reset sequence while preserving the explicit release step for i.MX8QXP.
Make reset release opt-in through platform data.
Tested on Librem 5r4 with the existing HI846 front camera and the S5K3L6
rear camera added by this series.
Signed-off-by: Vincent Cloutier <vincent@cloutier.co>
Assisted-by: OpenCode:gpt-5.5
---
drivers/media/platform/nxp/imx8mq-mipi-csi2.c | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/drivers/media/platform/nxp/imx8mq-mipi-csi2.c b/drivers/media/platform/nxp/imx8mq-mipi-csi2.c
index 950793297496..xxxxxxxxxxxx 100644
--- a/drivers/media/platform/nxp/imx8mq-mipi-csi2.c
+++ b/drivers/media/platform/nxp/imx8mq-mipi-csi2.c
@@ -76,6 +76,7 @@ struct imx8mq_plat_data {
int (*enable)(struct csi_state *state, u32 hs_settle);
void (*disable)(struct csi_state *state);
bool use_reg_csr;
+ bool needs_reset_deassert;
};
/*
@@ -244,6 +245,7 @@ static const struct imx8mq_plat_data imx8qxp_data = {
.enable = imx8qxp_gpr_enable,
.disable = imx8qxp_gpr_disable,
.use_reg_csr = true,
+ .needs_reset_deassert = true,
};
static const struct csi2_pix_format imx8mq_mipi_csi_formats[] = {
@@ -363,8 +365,12 @@ static int imx8mq_mipi_csi_sw_reset(struct csi_state *state)
return ret;
}
- /* Explicitly release reset to make sure reset bits are cleared. */
- return reset_control_deassert(state->rst);
+ /*
+ * Some SoC integrations require an explicit release after reset
+ * assertion. Keep this SoC-specific so i.MX8MQ retains its
+ * known-working assert-only sequence.
+ */
+ if (!state->pdata->needs_reset_deassert)
+ return 0;
+
+ return reset_control_deassert(state->rst);
}
static void imx8mq_mipi_csi_set_params(struct csi_state *state)
--
2.53.0
^ permalink raw reply
* [PATCH RFC 0/4] media: i2c: Add Samsung S5K3L6 and Librem 5 rear camera
From: Vincent Cloutier @ 2026-06-26 0:06 UTC (permalink / raw)
To: linux-media, devicetree, linux-arm-kernel
Cc: linux-kernel, linux-imx, kernel, Vincent Cloutier
From: Vincent Cloutier <vincent@cloutier.co>
This RFC fixes the i.MX8MQ CSI-2 reset path needed by Librem 5 camera
capture, adds initial upstream support for the Samsung S5K3L6 image
sensor, and wires it up as the rear camera on the Purism Librem 5.
This is intentionally a rewrite, not a direct forwarding of the Librem 5
downstream S5K3L6 carry. The downstream history is a long bring-up
series with debugfs register overrides, debug-frame plumbing, commented-out
old subdev code, FIXME/TODO scaffolding, and several API-era migrations.
This RFC collapses the usable production path into a current V4L2 sensor
driver using CCI regmap, runtime PM, fwnode endpoint validation, and the
current subdev stream API.
Many thanks to Martin Kepplinger, Dorota Czaplejewicz, and Sebastian
Krzyszkowiak for the original Librem 5 S5K3L6 driver work and rear-camera
bring-up. Martin authored the initial downstream driver, and Dorota and
Sebastian substantially developed the mode tables, controls, power-up
sequence, and sensor tuning that this rewrite is based on.
This series covers:
- 25 MHz input clock, matching the Librem 5 downstream configuration
- two MIPI CSI-2 data lanes
- RAW8 and RAW10 SGRBG modes at 1052x780, 2104x1560, and 4208x3120
- exposure, analogue gain, digital gain, blanking, pixel-rate,
link-frequency, test-pattern, orientation, and rotation controls
- Librem 5 DTS integration for the rear sensor and second CSI-2 path
The rewritten driver and DTS path have now been tested on Librem 5r4
hardware in a v7.1.1 carry build, with the i.MX8MQ CSI-2 reset fix from this
series applied.
Patch 1 keeps the i.MX8MQ CSI-2 software reset sequence compatible with
the Librem 5 camera pipeline by making the post-assert reset release
SoC-specific. This is included as an RFC prerequisite for the Librem 5
rear-camera enablement; if preferred, it can be split out and handled as
a separate media/platform fix.
Vincent Cloutier (4):
media: imx8mq-mipi-csi2: Keep i.MX8MQ reset assert-only
dt-bindings: media: i2c: Add Samsung S5K3L6 image sensor
media: i2c: Add Samsung S5K3L6 image sensor driver
arm64: dts: imx8mq-librem5: Add rear camera
.../bindings/media/i2c/samsung,s5k3l6.yaml | 117 ++
arch/arm64/boot/dts/freescale/imx8mq-librem5.dtsi | 51 +
drivers/media/i2c/Kconfig | 10 +
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/s5k3l6.c | 1055 +++++++++++++++++
drivers/media/platform/nxp/imx8mq-mipi-csi2.c | 9 +-
6 files changed, 1241 insertions(+), 2 deletions(-)
--
2.53.0
^ permalink raw reply
* Re: [PATCH v6 2/2] Input: isa1200 - new driver for Imagis ISA1200
From: Dmitry Torokhov @ 2026-06-25 23:45 UTC (permalink / raw)
To: Svyatoslav Ryhel
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Linus Walleij,
linux-input, devicetree, linux-kernel
In-Reply-To: <20260617070528.35006-3-clamor95@gmail.com>
Hi Svyatoslav,
On Wed, Jun 17, 2026 at 10:05:27AM +0300, Svyatoslav Ryhel wrote:
> From: Linus Walleij <linusw@kernel.org>
>
> The ISA1200 is a haptic feedback unit from Imagis Technology using two
> motors for haptic feedback in mobile phones. Used in many mobile devices
> c. 2012 including Samsung Galxy S Advance GT-I9070 (Janice), Samsung Beam
> GT-I8350 (Gavini), LG Optimus 4X P880 and LG Optimus Vu P895.
>
> The exact datasheet for the ISA1200 is not available; all data was modeled
> based on available downstream kernel sources for various devices and
> fragments of information scattered across the internet.
>
> Tested-by: Linus Walleij <linusw@kernel.org> # GT-I9070 Janice
> Signed-off-by: Linus Walleij <linusw@kernel.org>
> Co-developed-by: Svyatoslav Ryhel <clamor95@gmail.com>
> Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
Sashiko convinced me that using mutex_trylock() in the playback work
handler will result in dropping requests and that we are safe not taking
the lock there at all. Can you please try the following modification?
diff --git a/drivers/input/misc/isa1200.c b/drivers/input/misc/isa1200.c
index c61adc4b605c..fb7f68fa0a2b 100644
--- a/drivers/input/misc/isa1200.c
+++ b/drivers/input/misc/isa1200.c
@@ -253,15 +253,12 @@ static void isa1200_stop(struct isa1200 *isa)
static void isa1200_play_work(struct work_struct *work)
{
struct isa1200 *isa = container_of(work, struct isa1200, play_work);
- struct input_dev *input = isa->input;
-
- scoped_guard(mutex_try, &input->mutex) {
- if (!isa->suspended) {
- if (isa->level)
- isa1200_start(isa);
- else
- isa1200_stop(isa);
- }
+
+ if (!READ_ONCE(isa->suspended)) {
+ if (isa->level)
+ isa1200_start(isa);
+ else
+ isa1200_stop(isa);
}
}
If this works no need to resend, I'll fold on my side.
Thanks.
--
Dmitry
^ permalink raw reply related
* Re: [PATCH v14 4/5] gpio: rpmsg: add generic rpmsg GPIO driver
From: Julian Braha @ 2026-06-25 22:17 UTC (permalink / raw)
To: Shenwei Wang, Linus Walleij, Bartosz Golaszewski, Jonathan Corbet,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
Mathieu Poirier, Frank Li, Sascha Hauer
Cc: Shuah Khan, linux-gpio, linux-doc, linux-kernel,
Pengutronix Kernel Team, Fabio Estevam, Shenwei Wang, Peng Fan,
devicetree, linux-remoteproc, imx, linux-arm-kernel, linux-imx,
Arnaud POULIQUEN, b-padhi, Andrew Lunn, Bartosz Golaszewski
In-Reply-To: <20260625155432.815185-5-shenwei.wang@oss.nxp.com>
Hi Shenwei,
On 6/25/26 16:54, Shenwei Wang wrote:
> +config GPIO_RPMSG
> + tristate "Generic RPMSG GPIO support"
> + depends on OF && REMOTEPROC
> + select GPIOLIB_IRQCHIP
> + default REMOTEPROC
> + help
> + Say yes here to support the generic GPIO functions over the RPMSG
> + bus. Currently supported devices: i.MX7ULP, i.MX8ULP, i.MX8x, and
> + i.MX9x.
> +
> + If unsure, say N.
You've got GPIO_RPMSG defaulting to REMOTEPROC, which is also a
dependency, so it's effectively the same as using 'default y'.
But then you've got "If unsure, say N" in the help text. Usually this is
used when the default is N.
- Julian Braha
^ permalink raw reply
* Re: [PATCH v2 5/8] dt-bindings: iio: adc: add ti,ads122c14
From: sashiko-bot @ 2026-06-25 22:12 UTC (permalink / raw)
Cc: robh, conor+dt, devicetree
In-Reply-To: <20260625-iio-adc-ti-ads122c14-v2-5-ceb9b0b561cb@baylibre.com>
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] The pattern property regex for channel nodes `^channel@[0-7a-f]$` omits digits 8 and 9, rejecting valid node addresses.
- [High] The `burn-out-current-nanoamp` property applies an `enum` constraint directly to an array, which will fail DT validation.
- [Medium] The commit message explicitly claims to reuse the custom 'ti,vref-source' property, but the code actually implements the standard 'reference-sources' property instead.
- [Medium] The JSON schema reference `$ref: adc.yaml` is missing the `#` fragment or the absolute path.
- [Medium] The `reference-sources` property lacks a `maxItems` constraint and leaves subsequent elements unvalidated.
- [Low] Strict index ordering in `interrupts` clashes with unordered `interrupt-names`, causing invalid semantics if only one interrupt is used.
--
commit de052c7080b8c89f1a3c4b730b190d119eacb414
Author: David Lechner (TI) <dlechner@baylibre.com>
dt-bindings: iio: adc: add ti,ads122c14
Add new bindings for ti,ads122c14 and similar devices primarily intended for
use with temperature sensors. These bindings introduce channel-specific
properties to handle per-measurement reference voltage and current output
requirements.
> ti,vref-source is reused from ti,tlv320adcx140.yaml (otherwise might
> have preferred an enum of strings).
[Severity: Medium]
The commit message explicitly states it reuses the 'ti,vref-source' property,
but the schema actually implements the standard 'reference-sources' property
instead. Should the commit message be updated to match the implementation?
> diff --git a/Documentation/devicetree/bindings/iio/adc/ti,ads112c14.yaml b/Documentation/devicetree/bindings/iio/adc/ti,ads112c14.yaml
> new file mode 100644
> index 0000000000000..59ee4d652a087
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/adc/ti,ads112c14.yaml
[ ... ]
> + interrupts:
> + minItems: 1
> + items:
> + - description: FAULT interrupt (GPIO2 pin)
> + - description: DRDY interrupt (GPIO3 pin)
> +
> + interrupt-names:
> + minItems: 1
> + maxItems: 2
> + items:
> + enum: [fault, drdy]
[Severity: Low]
If a device tree specifies only the DRDY interrupt at index 0, does the strict
ordering in the interrupts list (which expects FAULT at index 0) clash with
the unordered interrupt-names definition?
[ ... ]
> +patternProperties:
> + ^channel@[0-7a-f]$:
> + $ref: adc.yaml
[Severity: Medium]
Does the JSON schema reference here need a '#' fragment or an absolute path
to ensure it resolves properly across all tooling versions during validation?
[Severity: High]
Does the regex pattern ^channel@[0-7a-f]$ omit digits 8 and 9? The reg
property below has a maximum of 15, so a device tree legitimately using
channel@8 or channel@9 would fail schema validation.
> +
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + items:
> + - maximum: 15 # arbitrary limit, channel@ can be any combination of AIN0-AIN7
[ ... ]
> + burn-out-current-nanoamp:
> + enum: [200, 1000, 10000]
[Severity: High]
Because device tree properties are parsed as arrays, will applying an enum
constraint directly to the property without using the 'items' keyword cause
dt_binding_check to always fail?
> +
> + reference-sources:
> + items:
> + - enum: [internal-2.5v, internal-1.25v, external, avdd]
> + default: internal-2.5v
[Severity: Medium]
Is a maxItems constraint needed for this array? Without it, JSON Schema only
applies the list validation to the first element, meaning additional items
in the array might silently pass without being checked.
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625-iio-adc-ti-ads122c14-v2-0-ceb9b0b561cb@baylibre.com?part=5
^ permalink raw reply
* Re: [PATCH v2 8/8] iio: adc: ti-ads112c14: add measurement channel support
From: sashiko-bot @ 2026-06-25 22:11 UTC (permalink / raw)
Cc: conor+dt, devicetree, robh
In-Reply-To: <20260625-iio-adc-ti-ads122c14-v2-8-ceb9b0b561cb@baylibre.com>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [Medium] The parsed burnout current configuration (`measurement->burnout`) is never applied to the hardware.
- [Medium] Directly checking `fwnode_find_reference` for `avdd-supply` bypasses the regulator framework's dummy fallback, causing probe to fail if the supply is omitted from the devicetree.
- [Medium] Calculation of `ext_ref_uV` can underflow, leading to garbage scaling values and bypassing validation checks.
Pre-existing issues:
- [High] Short reads from `i2c_smbus_read_i2c_block_data()` are not correctly identified as errors, exposing uninitialized kernel stack memory to userspace.
--
commit 61a17369383e7866b0adf6d21770ae99f4aca379
Author: David Lechner (TI) <dlechner@baylibre.com>
iio: adc: ti-ads112c14: add measurement channel support
Add support for parsing devicetree properties for measurement channels
and doing direct reads on these.
Channels are created dynamically since the number of possibilities is
unreasonably large.
> diff --git a/drivers/iio/adc/ti-ads112c14.c b/drivers/iio/adc/ti-ads112c14.c
> --- a/drivers/iio/adc/ti-ads112c14.c
> +++ b/drivers/iio/adc/ti-ads112c14.c
> @@ -308,106 +463,6 @@ static int ads112c14_prepare_measurement_channel(struct ads112c14_data *data,
> const struct iio_chan_spec *chan)
> {
> struct ads112c14_measurement *measurement = &data->measurements[chan->scan_index];
> u32 refp_buf_en, refn_buf_en, ref_val, ref_sel;
> int ret;
>
[ ... ]
> ret = regmap_update_bits(data->regmap, ADS112C14_REG_DATA_RATE_CFG,
> ADS112C14_DATA_RATE_CFG_GC_EN,
> FIELD_PREP(ADS112C14_DATA_RATE_CFG_GC_EN,
> measurement->global_chop));
> if (ret)
> return ret;
[Severity: Medium]
Is the burnout configuration supposed to be applied to the hardware here?
The parsed burn-out-current-nanoamp is saved to measurement->burnout in
ads112c14_parse_channels(), but this function does not appear to write it
to the ADS112C14_REG_DEVICE_CFG register. This might cause the feature to
be silently ignored even when configured in the devicetree.
[ ... ]
> @@ -494,7 +670,6 @@ static int ads112c14_read_raw(struct iio_dev *indio_dev,
> case IIO_CHAN_INFO_RAW: {
> u8 buf[3];
> int ret;
>
> if (!iio_device_claim_direct(indio_dev))
> return -EBUSY;
>
> ret = ads112c14_single_conversion(data, chan, buf);
> iio_device_release_direct(indio_dev);
> if (ret < 0)
> return ret;
>
> switch (data->chip_info->resolution_bits) {
> case 16:
> *val = get_unaligned_be16(buf);
[Severity: High]
This isn't a bug introduced by this patch, but does this adequately handle
short reads?
The i2c_smbus_read_i2c_block_data() function called by
ads112c14_single_conversion() returns the positive number of bytes read on
success. If a short read occurs, ret will be positive and bypass the
ret < 0 check.
Could this proceed to process a partially uninitialized buf array and expose
kernel stack memory to userspace?
[ ... ]
> @@ -1021,76 +1142,6 @@ static int ads112c14_probe(struct i2c_client *client)
> if (device_property_present(dev, "refp-supply")) {
> ret = devm_regulator_get_enable_read_voltage(dev, "refp");
> if (ret < 0)
> return dev_err_probe(dev, ret, "failed to get refp voltage\n");
>
> refp_uV = ret;
>
> struct fwnode_handle *refp_fwnode __free(fwnode_handle) =
> fwnode_find_reference(dev->fwnode, "refp-supply", 0);
> if (IS_ERR(refp_fwnode))
> return dev_err_probe(dev, PTR_ERR(refp_fwnode),
> "failed to get refp fwnode\n");
>
> struct fwnode_handle *avdd_fwnode __free(fwnode_handle) =
> fwnode_find_reference(dev->fwnode, "avdd-supply", 0);
> if (IS_ERR(avdd_fwnode))
> return dev_err_probe(dev, PTR_ERR(avdd_fwnode),
> "failed to get avdd fwnode\n");
[Severity: Medium]
Will this rigidly fail the probe if the avdd-supply is absent?
By directly using fwnode_find_reference(), it seems we bypass the regulator
framework's dummy fallback. If the devicetree omits avdd-supply, this
appears to return -ENOENT and fail unconditionally rather than treating
the missing reference as simply not matching refp_fwnode.
>
> /* REFP buffer should not be enabled when connected to AVDD */
> data->refp_is_avdd = refp_fwnode == avdd_fwnode;
> }
>
> if (device_property_present(dev, "refn-supply")) {
> ret = devm_regulator_get_enable_read_voltage(dev, "refn");
> if (ret < 0)
> return dev_err_probe(dev, ret, "failed to get refn voltage\n");
>
> refn_uV = ret;
> } else {
> data->refn_is_gnd = true;
> }
>
> data->ext_ref_uV = refp_uV - refn_uV;
>
> if (data->ext_ref_uV && data->ext_ref_ohms)
> return dev_err_probe(dev, -EINVAL,
> "ti,refp-refn-resistor-ohms property should not be present when refp-supply or refn-supply is present\n");
>
> if (need_ext_ref && !data->ext_ref_uV && !data->ext_ref_ohms)
> return dev_err_probe(dev, -EINVAL,
> "external reference measurements require either refp-supply or ti,refp-refn-resistor-ohms property\n");
[Severity: Medium]
Can this subtraction underflow if refp-supply is missing but refn-supply is
present?
If refp_uV is 0 and refn_uV is greater than 0, subtracting them as unsigned
32-bit integers will wrap to a large positive number. This seems like it
would bypass the subsequent !data->ext_ref_uV validation check and lead to
incorrect values when computing full_scale in ads112c14_populate_tables().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625-iio-adc-ti-ads122c14-v2-0-ceb9b0b561cb@baylibre.com?part=8
^ permalink raw reply
* Re: [PATCH v2 7/8] iio: adc: ti-ads112c14: implement gain on internal short SYS_MON channel
From: sashiko-bot @ 2026-06-25 22:08 UTC (permalink / raw)
Cc: robh, conor+dt, devicetree
In-Reply-To: <20260625-iio-adc-ti-ads122c14-v2-7-ceb9b0b561cb@baylibre.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Unlocked concurrent accesses to `gain_val` variables can result in data races and potentially corrupted scale values being returned to userspace.
--
commit d2600e3b47039aca86a7fafcbbec65c4180a38f5
Author: David Lechner (TI) <dlechner@baylibre.com>
iio: adc: ti-ads112c14: implement gain on internal short SYS_MON channel
Implement support for the programmable gain amplifier on the internal short
SYS_MON channel. This channel is used for calibration, so it is useful to be
able to set the PGA to the same gain as the external channels.
> diff --git a/drivers/iio/adc/ti-ads112c14.c b/drivers/iio/adc/ti-ads112c14.c
> index c61d472447325..0e775dbc8d50b 100644
> --- a/drivers/iio/adc/ti-ads112c14.c
> +++ b/drivers/iio/adc/ti-ads112c14.c
[ ... ]
> @@ -371,6 +401,17 @@ static int ads112c14_read_raw(struct iio_dev *indio_dev,
> return IIO_VAL_FRACTIONAL_LOG2;
> }
>
> + if (chan->channel == ADS112C14_SYS_MON_CHANNEL_SHORT) {
> + u8 idx = data->sys_mon_chan_short_gain_val;
> +
> + scale_avail = data->sys_mon_chan_short_scale_available[idx];
> +
> + *val = scale_avail[0];
> + *val2 = scale_avail[1];
> +
> + return IIO_VAL_INT_PLUS_NANO;
> + }
> +
[Severity: Medium]
Is it possible for a concurrent write from ads112c14_write_raw() to cause a
data race here?
Since this read side in ads112c14_read_raw() does not acquire the IIO direct
mode lock or use READ_ONCE(), the compiler could reload
data->sys_mon_chan_short_gain_val.
If the value gets reloaded between the assignments of *val and *val2, the
function might fetch the integer and fractional parts from different gain
configurations, returning a corrupted scale to userspace.
[ ... ]
> +static int ads112c14_write_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan, int val,
> + int val2, long mask)
> +{
[ ... ]
> + IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
> + if (IIO_DEV_ACQUIRE_FAILED(claim))
> + return -EBUSY;
> +
> + for (u32 i = 0; i < ARRAY_SIZE(ads112c14_pga_gains_x10); i++) {
> + if (val == scale_avail[i][0] && val2 == scale_avail[i][1]) {
> + *gain_val = i;
> + return 0;
> + }
> + }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625-iio-adc-ti-ads122c14-v2-0-ceb9b0b561cb@baylibre.com?part=7
^ permalink raw reply
* Re: [PATCH 4/4] PCI: mediatek-gen3: Add 2-lanes mode support for Airoha AN7581
From: sashiko-bot @ 2026-06-25 22:07 UTC (permalink / raw)
To: Christian Marangi; +Cc: linux-pci, conor+dt, robh, devicetree
In-Reply-To: <20260625215741.3253212-5-ansuelsmth@gmail.com>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [High] Calling devm_ioremap_resource() inside the .power_up() callback breaks system resume with -EBUSY.
- [High] Unvalidated num_lanes > 2 causes a NULL pointer dereference in the hardware configuration loop.
- [High] Missing clock disable on error path in mtk_pcie_en7581_power_up().
Pre-existing issues:
- [High] The return value of pm_runtime_get_sync() is ignored, leading to hardware access on suspended devices.
--
commit c4659641288856a22fb57ae4fe5c48d43375cff0
Author: Christian Marangi <ansuelsmth@gmail.com>
PCI: mediatek-gen3: Add 2-lanes mode support for Airoha AN7581
This commit adds 2-lanes mode support for the Airoha AN7581 SoC by
configuring the PCIe MUX in the SCU register. It introduces secondary PCIe
MAC memory mapping, SCU regmap parsing, and additional PERSTOUT reset logic.
> diff --git a/drivers/pci/controller/pcie-mediatek-gen3.c b/drivers/pci/controller/pcie-mediatek-gen3.c
> index b0accd8285892..f750759bbc1d8 100644
> --- a/drivers/pci/controller/pcie-mediatek-gen3.c
> +++ b/drivers/pci/controller/pcie-mediatek-gen3.c
[ ... ]
> @@ -955,12 +974,29 @@ static int mtk_pcie_parse_port(struct mtk_gen3_pcie *pcie)
> static int mtk_pcie_en7581_power_up(struct mtk_gen3_pcie *pcie)
> {
> struct pci_host_bridge *host = pci_host_bridge_from_priv(pcie);
> + unsigned int num_lanes = max(1, pcie->num_lanes);
> struct device *dev = pcie->dev;
> struct resource_entry *entry;
> struct regmap *pbus_regmap;
> u32 val, args[2], size;
> resource_size_t addr;
> - int err;
> + int i, err;
> +
> + if (num_lanes == 2) {
> + struct platform_device *pdev = to_platform_device(dev);
> + struct resource *regs;
> +
> + regs = platform_get_resource_byname(pdev, IORESOURCE_MEM, "sec-pcie-mac");
> + if (!regs)
> + return -EINVAL;
> + pcie->sec_base = devm_ioremap_resource(dev, regs);
[Severity: High]
Since mtk_pcie_en7581_power_up() is assigned as the .power_up callback and
executes during both probe and resume_noirq(), won't calling
devm_ioremap_resource() here cause a conflict on system resume?
The devm-managed resources persist across suspend/resume cycles, so
devm_request_mem_region() will detect a conflict and cause
devm_ioremap_resource() to return -EBUSY. Could this mapping be moved
to the probe path, such as mtk_pcie_parse_port(), to avoid resume failures?
> + if (IS_ERR(pcie->sec_base))
> + return dev_err_probe(dev, PTR_ERR(pcie->sec_base), "failed to map secondary register base\n");
[ ... ]
> @@ -1024,34 +1060,60 @@ static int mtk_pcie_en7581_power_up(struct mtk_gen3_pcie *pcie)
> pm_runtime_enable(dev);
> pm_runtime_get_sync(dev);
[Severity: High]
This is a pre-existing issue, but should the return value
of pm_runtime_get_sync() be checked?
If the device power domain fails to transition to an active state,
pm_runtime_get_sync() will return a negative error code. Proceeding to access
registers on suspended hardware could trigger a synchronous external abort or
system crash.
> - val = FIELD_PREP(PCIE_VAL_LN0_DOWNSTREAM, 0x47) |
> - FIELD_PREP(PCIE_VAL_LN1_DOWNSTREAM, 0x47) |
> - FIELD_PREP(PCIE_VAL_LN0_UPSTREAM, 0x41) |
> - FIELD_PREP(PCIE_VAL_LN1_UPSTREAM, 0x41);
> - writel_relaxed(val, pcie->base + PCIE_EQ_PRESET_01_REG);
[ ... ]
> + /* Wait for refclk to stabilize */
> msleep(PCIE_T_PVPERL_MS);
>
> + /* Configure all the lines to the same EQ config */
> + for (i = 0; i < num_lanes; i++) {
> + void __iomem *base = pcie->base;
> +
> + if (i == 1)
> + base = pcie->sec_base;
> +
> + val = FIELD_PREP(PCIE_VAL_LN0_DOWNSTREAM, 0x47) |
> + FIELD_PREP(PCIE_VAL_LN1_DOWNSTREAM, 0x47) |
> + FIELD_PREP(PCIE_VAL_LN0_UPSTREAM, 0x41) |
> + FIELD_PREP(PCIE_VAL_LN1_UPSTREAM, 0x41);
> + writel_relaxed(val, base + PCIE_EQ_PRESET_01_REG);
[Severity: High]
If the Device Tree specifies a valid num-lanes value greater than 2 (e.g.,
4 or 8), will this loop dereference a NULL pointer?
Earlier in this function, pcie->sec_base is exclusively mapped when
num_lanes == 2. If num_lanes is greater than 2, pcie->sec_base remains NULL.
When i == 1, base becomes NULL, and writel_relaxed() will panic. Should
there be explicit bounds validation before writing to sec_base?
> + val = PCIE_K_PHYPARAM_QUERY | PCIE_K_QUERY_TIMEOUT |
> + FIELD_PREP(PCIE_K_PRESET_TO_USE_16G, 0x80) |
> + FIELD_PREP(PCIE_K_PRESET_TO_USE, 0x2) |
> + FIELD_PREP(PCIE_K_FINETUNE_MAX, 0xf);
> + writel_relaxed(val, base + PCIE_PIPE4_PIE8_REG);
> + }
> +
> + /* Deassert PERSTOUT for all relevant lines */
> + err = reset_control_bulk_deassert(MAX_NUM_PERSTOUT_RESETS,
> + pcie->perstout_resets);
> + if (err) {
> + dev_err(dev, "failed to deassert PERSTOUTs\n");
> + goto err_perstout_assert;
> + }
> +
> return 0;
>
> -err_clk_prepare_enable:
> +err_perstout_assert:
> pm_runtime_put_sync(dev);
> pm_runtime_disable(dev);
[Severity: High]
If reset_control_bulk_deassert() fails, won't jumping to this label leak
the clock prepare/enable references?
The function successfully enables clocks earlier with
clk_bulk_prepare_enable(), but err_perstout_assert entirely omits calling
clk_bulk_disable_unprepare(), which will leave the hardware clocks
permanently running.
> reset_control_bulk_assert(pcie->soc->phy_resets.num_resets,
> pcie->phy_resets);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625215741.3253212-1-ansuelsmth@gmail.com?part=4
^ permalink raw reply
* Re: [PATCH 1/4] dt-bindings: clock: airoha: Add additional reset for PCIe PERSTOUT
From: sashiko-bot @ 2026-06-25 22:07 UTC (permalink / raw)
To: Christian Marangi; +Cc: conor+dt, linux-pci, robh, devicetree
In-Reply-To: <20260625215741.3253212-2-ansuelsmth@gmail.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Data race between direct MMIO and regmap accesses on the SCU_PCIC register.
--
commit 900fc8364d6744e062fdb68cdff6066cb3ba875d
Author: Christian Marangi <ansuelsmth@gmail.com>
dt-bindings: clock: airoha: Add additional reset for PCIe PERSTOUT
Add additional reset to control PCIe PERSTOUT reset line for each of the 3
PCIe lines.
> diff --git a/include/dt-bindings/reset/airoha,en7581-reset.h b/include/dt-bindings/reset/airoha,en7581-reset.h
> --- a/include/dt-bindings/reset/airoha,en7581-reset.h
> +++ b/include/dt-bindings/reset/airoha,en7581-reset.h
[ ... ]
> @@ -62,5 +62,9 @@
> #define EN7581_CPU_TIMER_RST 50
> #define EN7581_PCIE_HB_RST 51
> #define EN7581_XPON_MAC_RST 52
> +/* RST_PCIC */
> +#define EN7581_PCIC_PERSTOUT0_RST 53
> +#define EN7581_PCIC_PERSTOUT1_RST 54
> +#define EN7581_PCIC_PERSTOUT2_RST 55
[Severity: High]
Will adding these resets expose a data race between the reset controller and
the PCIe controller during link bring-up?
When these resets are asserted, en7523_reset_update() in clk-en7523.c
performs an unlocked direct MMIO read-modify-write on the SCU_PCIC register:
drivers/clk/clk-en7523.c:en7523_reset_update() {
...
val = readl(addr);
val &= ~BIT(id % RST_NR_PER_BANK);
...
writel(val, addr);
}
At the same time, mtk_pcie_en7581_power_up() in pcie-mediatek-gen3.c
modifies the same SCU_PCIC register using a regmap:
drivers/pci/controller/pcie-mediatek-gen3.c:mtk_pcie_en7581_power_up() {
...
regmap_update_bits(pcie->scu, SCU_PCIC, SCU_PCIC_PCIE_CTRL,
FIELD_PREP(SCU_PCIC_PCIE_CTRL, BIT(1)));
}
Since the regmap internal lock does not protect against direct MMIO accesses
from clk-en7523.c, could concurrent operations (like one port triggering a
reset while another powers up) silently corrupt the SCU_PCIC hardware
register state and cause PCIe links to fail?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625215741.3253212-1-ansuelsmth@gmail.com?part=1
^ permalink raw reply
* Re: [PATCH v2 6/8] iio: adc: add ti-ads112c14 driver
From: sashiko-bot @ 2026-06-25 22:07 UTC (permalink / raw)
Cc: conor+dt, robh, devicetree
In-Reply-To: <20260625-iio-adc-ti-ads122c14-v2-6-ceb9b0b561cb@baylibre.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Short I2C reads lead to the use of uninitialized stack memory for ADC readings.
- [High] The driver inadvertently sets the device into Continuous Conversion mode instead of the intended Single-Shot mode.
--
commit 610f816911f47ce29d3c83f300222d6ab72f8820
Author: David Lechner (TI) <dlechner@baylibre.com>
iio: adc: add ti-ads112c14 driver
Add a new driver for the TI ADS112C14/ADS122C14 ADC chips.
This first step is adding a very basic driver that only supports power
on/reset and reading the system monitor channels.
> diff --git a/drivers/iio/adc/ti-ads112c14.c b/drivers/iio/adc/ti-ads112c14.c
> new file mode 100644
> index 0000000000000..c61d472447325
> --- /dev/null
> +++ b/drivers/iio/adc/ti-ads112c14.c
[ ... ]
> +static int ads112c14_single_conversion(struct ads112c14_data *data,
> + const struct iio_chan_spec *chan,
> + u8 *buf)
> +{
[ ... ]
> + return i2c_smbus_read_i2c_block_data(client, ADS112C14_CMD_RDATA,
> + BITS_TO_BYTES(data->chip_info->resolution_bits),
> + buf);
> +}
> +
> +static int ads112c14_read_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan, int *val,
> + int *val2, long mask)
> +{
[ ... ]
> + switch (mask) {
> + case IIO_CHAN_INFO_RAW: {
> + u8 buf[3];
> + int ret;
> +
> + if (!iio_device_claim_direct(indio_dev))
> + return -EBUSY;
> +
> + ret = ads112c14_single_conversion(data, chan, buf);
> + iio_device_release_direct(indio_dev);
> + if (ret < 0)
> + return ret;
[Severity: High]
Does this code handle short I2C reads correctly?
i2c_smbus_read_i2c_block_data() returns the positive byte count on success.
If the device returns fewer bytes than requested, ret will be positive but
less than BITS_TO_BYTES(data->chip_info->resolution_bits).
Since ads112c14_read_raw() only checks if (ret < 0), won't a short read
bypass error handling and cause it to process uninitialized stack memory from
buf, leading to an information leak and corrupted ADC readings?
> +
> + switch (data->chip_info->resolution_bits) {
> + case 16:
> + *val = get_unaligned_be16(buf);
[ ... ]
> +static int ads112c14_probe(struct i2c_client *client)
> +{
[ ... ]
> + /* Place in single-shot conversion mode to make ready for raw read. */
> + ret = regmap_set_bits(data->regmap, ADS112C14_REG_DEVICE_CFG,
> + ADS112C14_DEVICE_CFG_CONV_MODE);
[Severity: High]
Does setting ADS112C14_DEVICE_CFG_CONV_MODE actually put the device into
continuous conversion mode instead of the intended single-shot mode?
According to the comment, the intention is to use single-shot mode (which is
typically the default value of 0). Setting the bit to 1 appears to enable
continuous conversion mode, which could cause hardware race conditions and
corrupted data when multiplexer settings are changed for new sysfs readings.
> + if (ret)
> + return ret;
> +
> + indio_dev->name = info->name;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625-iio-adc-ti-ads122c14-v2-0-ceb9b0b561cb@baylibre.com?part=6
^ permalink raw reply
* Re: [PATCH 3/4] dt-bindings: PCI: mediatek-gen3: Split Airoha schema and document 2-lanes
From: sashiko-bot @ 2026-06-25 22:04 UTC (permalink / raw)
To: Christian Marangi; +Cc: devicetree, robh, conor+dt, linux-pci
In-Reply-To: <20260625215741.3253212-4-ansuelsmth@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The schema incorrectly uses `regs` instead of `reg` in the `allOf` conditional block for `num-lanes == 2`, failing to enforce the requirement for a secondary register region.
- [Medium] Typographical error `phy-lan2` instead of `phy-lane2` in the `reset-names` enum constraint for the 1-lane fallback block.
--
commit bb5683e00f2f10ba9133fc862deba2acbfb939ed
Author: Christian Marangi <ansuelsmth@gmail.com>
dt-bindings: PCI: mediatek-gen3: Split Airoha schema and document 2-lanes
This commit splits the Airoha PCIe schema from the mediatek-gen3 schema into
a dedicated file. This allows documenting the required properties for PCIe
configured in 2-lanes mode, while cleaning up the mediatek-gen3 schema.
> diff --git a/Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml b/Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml
> new file mode 100644
> index 0000000000000..977c1816572cc
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml
[ ... ]
> +allOf:
> + - $ref: /schemas/pci/pci-host-bridge.yaml#
> + - if:
> + properties:
> + num-lanes:
> + const: 2
> + then:
> + properties:
> + regs:
> + minItems: 2
[Severity: Medium]
Does this schema enforce the minItems constraint as intended? It looks like
there's a typo where "regs" is used instead of "reg". This might allow a
node with num-lanes = <2> but only a single register region to silently pass
validation.
> +
> + reg-names:
> + items:
> + - const: pcie-mac
> + - const: sec-pcie-mac
[ ... ]
> + else:
> + properties:
> + reg:
> + maxItems: 1
> +
> + reg-names:
> + items:
> + - const: pcie-mac
> +
> + resets:
> + minItems: 2
> + maxItems: 3
> +
> + reset-names:
> + minItems: 2
> + items:
> + - enum: [ phy-lane0, phy-lane1, phy-lan2 ]
[Severity: Medium]
Will this properly validate phy-lane2? It appears there is a typo here
(phy-lan2 instead of phy-lane2) which would cause valid configurations
to fail schema validation while accepting the misspelled name.
> + - enum: [ phy-lane1, perstout ]
> + - const: phy-lane2
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260625215741.3253212-1-ansuelsmth@gmail.com?part=3
^ permalink raw reply
* [PATCH 4/4] PCI: mediatek-gen3: Add 2-lanes mode support for Airoha AN7581
From: Christian Marangi @ 2026-06-25 21:57 UTC (permalink / raw)
To: Lorenzo Pieralisi, Krzysztof Wilczyński,
Manivannan Sadhasivam, Rob Herring, Bjorn Helgaas,
Krzysztof Kozlowski, Conor Dooley, Ryder Lee, Michael Turquette,
Stephen Boyd, Brian Masney, Philipp Zabel, Matthias Brugger,
AngeloGioacchino Del Regno, Christian Marangi, Jianjun Wang,
linux-pci, devicetree, linux-kernel, linux-mediatek, linux-clk,
linux-arm-kernel
In-Reply-To: <20260625215741.3253212-1-ansuelsmth@gmail.com>
The Airoha AN7581 SoC supports configuring the first PCIe0 line to 2-lanes
mode by bonding it with the second PCIe line. This is done by configuring
the PCIe MUX in the SCU register.
To correctly configure the line for 2-lanes mode, it's required to define
in DT an additional reg, 'sec-pcie-mac' for the secondary PCIe.
It's also needed to define the additional reset and the PERSTOUT reset.
Also 'airoha,scu' property is mandatory to correctly configure the SCU
register for the PCIe MUX.
Finally to toggle 2-lanes mode, it's needed to define in DT 'num-lanes' as
2.
In such configuration the EQ preset are configured to the same values.
To permit correct configuration of the PCIe line, additional logic is added
to assert and deassert the PERSTOUT resets.
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
---
drivers/pci/controller/pcie-mediatek-gen3.c | 98 +++++++++++++++++----
1 file changed, 80 insertions(+), 18 deletions(-)
diff --git a/drivers/pci/controller/pcie-mediatek-gen3.c b/drivers/pci/controller/pcie-mediatek-gen3.c
index b0accd828589..f750759bbc1d 100644
--- a/drivers/pci/controller/pcie-mediatek-gen3.c
+++ b/drivers/pci/controller/pcie-mediatek-gen3.c
@@ -32,6 +32,11 @@
#include "../pci.h"
+/* AN7581 SCU register */
+#define SCU_PCIC 0x88
+#define SCU_PCIC_PCIE_CTRL GENMASK(7, 0)
+
+/* PCIe register */
#define PCIE_BASE_CFG_REG 0x14
#define PCIE_BASE_CFG_SPEED GENMASK(15, 8)
@@ -131,6 +136,7 @@
#define PCIE_ATR_TLP_TYPE_IO PCIE_ATR_TLP_TYPE(2)
#define MAX_NUM_PHY_RESETS 3
+#define MAX_NUM_PERSTOUT_RESETS 2
#define PCIE_MTK_RESET_TIME_US 10
@@ -203,9 +209,11 @@ struct mtk_msi_set {
struct mtk_gen3_pcie {
struct device *dev;
void __iomem *base;
+ void __iomem *sec_base;
phys_addr_t reg_base;
struct reset_control *mac_reset;
struct reset_control_bulk_data phy_resets[MAX_NUM_PHY_RESETS];
+ struct reset_control_bulk_data perstout_resets[MAX_NUM_PERSTOUT_RESETS];
struct phy *phy;
struct clk_bulk_data *clks;
int num_clks;
@@ -222,6 +230,9 @@ struct mtk_gen3_pcie {
DECLARE_BITMAP(msi_irq_in_use, PCIE_MSI_IRQS_NUM);
const struct mtk_gen3_pcie_pdata *soc;
+
+ /* AN7581 specific */
+ struct regmap *scu;
};
/* LTSSM state in PCIE_LTSSM_STATUS_REG bit[28:24] */
@@ -928,6 +939,14 @@ static int mtk_pcie_parse_port(struct mtk_gen3_pcie *pcie)
if (ret)
return dev_err_probe(dev, ret, "failed to get PHY bulk reset\n");
+ pcie->perstout_resets[0].id = "perstout";
+ pcie->perstout_resets[1].id = "sec-perstout";
+
+ ret = devm_reset_control_bulk_get_optional_exclusive(dev, MAX_NUM_PERSTOUT_RESETS,
+ pcie->perstout_resets);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get PERSTOUT bulk reset\n");
+
pcie->mac_reset = devm_reset_control_get_optional_exclusive(dev, "mac");
if (IS_ERR(pcie->mac_reset))
return dev_err_probe(dev, PTR_ERR(pcie->mac_reset), "failed to get MAC reset\n");
@@ -955,12 +974,29 @@ static int mtk_pcie_parse_port(struct mtk_gen3_pcie *pcie)
static int mtk_pcie_en7581_power_up(struct mtk_gen3_pcie *pcie)
{
struct pci_host_bridge *host = pci_host_bridge_from_priv(pcie);
+ unsigned int num_lanes = max(1, pcie->num_lanes);
struct device *dev = pcie->dev;
struct resource_entry *entry;
struct regmap *pbus_regmap;
u32 val, args[2], size;
resource_size_t addr;
- int err;
+ int i, err;
+
+ if (num_lanes == 2) {
+ struct platform_device *pdev = to_platform_device(dev);
+ struct resource *regs;
+
+ regs = platform_get_resource_byname(pdev, IORESOURCE_MEM, "sec-pcie-mac");
+ if (!regs)
+ return -EINVAL;
+ pcie->sec_base = devm_ioremap_resource(dev, regs);
+ if (IS_ERR(pcie->sec_base))
+ return dev_err_probe(dev, PTR_ERR(pcie->sec_base), "failed to map secondary register base\n");
+
+ pcie->scu = syscon_regmap_lookup_by_phandle(dev->of_node, "airoha,scu");
+ if (IS_ERR(pcie->scu))
+ return dev_err_probe(dev, PTR_ERR(pcie->scu), "failed to map SCU regmap\n");
+ }
/*
* The controller may have been left out of reset by the bootloader
@@ -1024,34 +1060,60 @@ static int mtk_pcie_en7581_power_up(struct mtk_gen3_pcie *pcie)
pm_runtime_enable(dev);
pm_runtime_get_sync(dev);
- val = FIELD_PREP(PCIE_VAL_LN0_DOWNSTREAM, 0x47) |
- FIELD_PREP(PCIE_VAL_LN1_DOWNSTREAM, 0x47) |
- FIELD_PREP(PCIE_VAL_LN0_UPSTREAM, 0x41) |
- FIELD_PREP(PCIE_VAL_LN1_UPSTREAM, 0x41);
- writel_relaxed(val, pcie->base + PCIE_EQ_PRESET_01_REG);
+ /* Assert PERSTOUT for all relevant lines */
+ err = reset_control_bulk_assert(MAX_NUM_PERSTOUT_RESETS,
+ pcie->perstout_resets);
+ if (err) {
+ dev_err(dev, "failed to assert PERSTOUTs\n");
+ goto err_perstout_assert;
+ }
+
+ /* Configure SCU MUX to disable PCIE1 for 2 lines mode */
+ if (num_lanes == 2)
+ regmap_update_bits(pcie->scu, SCU_PCIC, SCU_PCIC_PCIE_CTRL,
+ FIELD_PREP(SCU_PCIC_PCIE_CTRL, BIT(1)));
- val = PCIE_K_PHYPARAM_QUERY | PCIE_K_QUERY_TIMEOUT |
- FIELD_PREP(PCIE_K_PRESET_TO_USE_16G, 0x80) |
- FIELD_PREP(PCIE_K_PRESET_TO_USE, 0x2) |
- FIELD_PREP(PCIE_K_FINETUNE_MAX, 0xf);
- writel_relaxed(val, pcie->base + PCIE_PIPE4_PIE8_REG);
err = clk_bulk_prepare_enable(pcie->num_clks, pcie->clks);
if (err) {
dev_err(dev, "failed to prepare clock\n");
- goto err_clk_prepare_enable;
+ goto err_perstout_assert;
}
- /*
- * Airoha EN7581 performs PCIe reset via clk callbacks since it has a
- * hw issue with PCIE_PE_RSTB signal. Add wait for the time needed to
- * complete the PCIe reset.
- */
+ /* Wait for refclk to stabilize */
msleep(PCIE_T_PVPERL_MS);
+ /* Configure all the lines to the same EQ config */
+ for (i = 0; i < num_lanes; i++) {
+ void __iomem *base = pcie->base;
+
+ if (i == 1)
+ base = pcie->sec_base;
+
+ val = FIELD_PREP(PCIE_VAL_LN0_DOWNSTREAM, 0x47) |
+ FIELD_PREP(PCIE_VAL_LN1_DOWNSTREAM, 0x47) |
+ FIELD_PREP(PCIE_VAL_LN0_UPSTREAM, 0x41) |
+ FIELD_PREP(PCIE_VAL_LN1_UPSTREAM, 0x41);
+ writel_relaxed(val, base + PCIE_EQ_PRESET_01_REG);
+
+ val = PCIE_K_PHYPARAM_QUERY | PCIE_K_QUERY_TIMEOUT |
+ FIELD_PREP(PCIE_K_PRESET_TO_USE_16G, 0x80) |
+ FIELD_PREP(PCIE_K_PRESET_TO_USE, 0x2) |
+ FIELD_PREP(PCIE_K_FINETUNE_MAX, 0xf);
+ writel_relaxed(val, base + PCIE_PIPE4_PIE8_REG);
+ }
+
+ /* Deassert PERSTOUT for all relevant lines */
+ err = reset_control_bulk_deassert(MAX_NUM_PERSTOUT_RESETS,
+ pcie->perstout_resets);
+ if (err) {
+ dev_err(dev, "failed to deassert PERSTOUTs\n");
+ goto err_perstout_assert;
+ }
+
return 0;
-err_clk_prepare_enable:
+err_perstout_assert:
pm_runtime_put_sync(dev);
pm_runtime_disable(dev);
reset_control_bulk_assert(pcie->soc->phy_resets.num_resets,
--
2.53.0
^ permalink raw reply related
* [PATCH 3/4] dt-bindings: PCI: mediatek-gen3: Split Airoha schema and document 2-lanes
From: Christian Marangi @ 2026-06-25 21:57 UTC (permalink / raw)
To: Lorenzo Pieralisi, Krzysztof Wilczyński,
Manivannan Sadhasivam, Rob Herring, Bjorn Helgaas,
Krzysztof Kozlowski, Conor Dooley, Ryder Lee, Michael Turquette,
Stephen Boyd, Brian Masney, Philipp Zabel, Matthias Brugger,
AngeloGioacchino Del Regno, Christian Marangi, Jianjun Wang,
linux-pci, devicetree, linux-kernel, linux-mediatek, linux-clk,
linux-arm-kernel
In-Reply-To: <20260625215741.3253212-1-ansuelsmth@gmail.com>
To permit proper documentation of required property to support PCIe
configured for 2-lanes mode, split the Airoha schema part from the
mediatek-gen3 schema to a dedicated schema.
A PCIe configured for 2-lanes mode require an additional reg for the
secondary PCIe to be configured and the airoha,scu phandle to correctly
configure the PCIe MUX.
Rework the mediatek-gen3 schema to drop any redundant constraint previsouly
introduced for Airoha PCIe properties.
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
---
.../bindings/pci/airoha,en7581-pcie.yaml | 251 ++++++++++++++++++
.../bindings/pci/mediatek-pcie-gen3.yaml | 77 +-----
2 files changed, 256 insertions(+), 72 deletions(-)
create mode 100644 Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml
diff --git a/Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml b/Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml
new file mode 100644
index 000000000000..977c1816572c
--- /dev/null
+++ b/Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml
@@ -0,0 +1,251 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pci/airoha,en7581-pcie.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Gen3 PCIe controller on Airoha SoCs
+
+maintainers:
+ - Christian Marangi <ansuelsmth@gmail.com>
+
+description: |+
+ PCIe Gen3 MAC controller for Airoha SoCs, it supports Gen3 speed
+ and compatible with Gen2, Gen1 speed.
+
+ This PCIe controller supports up to 256 MSI vectors, the MSI hardware
+ block diagram is as follows:
+
+ +-----+
+ | GIC |
+ +-----+
+ ^
+ |
+ port->irq
+ |
+ +-+-+-+-+-+-+-+-+
+ |0|1|2|3|4|5|6|7| (PCIe intc)
+ +-+-+-+-+-+-+-+-+
+ ^ ^ ^
+ | | ... |
+ +-------+ +------+ +-----------+
+ | | |
+ +-+-+---+--+--+ +-+-+---+--+--+ +-+-+---+--+--+
+ |0|1|...|30|31| |0|1|...|30|31| |0|1|...|30|31| (MSI sets)
+ +-+-+---+--+--+ +-+-+---+--+--+ +-+-+---+--+--+
+ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
+ | | | | | | | | | | | | (MSI vectors)
+ | | | | | | | | | | | |
+
+ (MSI SET0) (MSI SET1) ... (MSI SET7)
+
+ With 256 MSI vectors supported, the MSI vectors are composed of 8 sets,
+ each set has its own address for MSI message, and supports 32 MSI vectors
+ to generate interrupt.
+
+properties:
+ compatible:
+ const: airoha,en7581-pcie
+
+ reg:
+ minItems: 1
+ maxItems: 2
+
+ reg-names:
+ minItems: 1
+ maxItems: 2
+
+ interrupts:
+ maxItems: 1
+
+ ranges:
+ minItems: 1
+ maxItems: 8
+
+ iommu-map:
+ maxItems: 1
+
+ iommu-map-mask:
+ const: 0
+
+ resets:
+ minItems: 1
+ maxItems: 4
+
+ reset-names:
+ minItems: 1
+ maxItems: 4
+
+ clocks:
+ maxItems: 1
+
+ clock-names:
+ items:
+ - const: sys-ck
+
+ phys:
+ maxItems: 1
+
+ phy-names:
+ items:
+ - const: pcie-phy
+
+ num-lanes:
+ enum: [1, 2]
+
+ mediatek,pbus-csr:
+ $ref: /schemas/types.yaml#/definitions/phandle-array
+ items:
+ - items:
+ - description: phandle to pbus-csr syscon
+ - description: offset of pbus-csr base address register
+ - description: offset of pbus-csr base address mask register
+ description:
+ Phandle with two arguments to the syscon node used to detect if
+ a given address is accessible on PCIe controller.
+
+ airoha,scu:
+ $ref: /schemas/types.yaml#/definitions/phandle-array
+ items:
+ - items:
+ - description: phandle to airoha SCU syscon
+ description:
+ Phandle to SCU syscon to configure PCIe MUX for 2 lines support.
+
+ '#interrupt-cells':
+ const: 1
+
+ interrupt-controller:
+ description: Interrupt controller node for handling legacy PCI interrupts.
+ type: object
+ properties:
+ '#address-cells':
+ const: 0
+ '#interrupt-cells':
+ const: 1
+ interrupt-controller: true
+
+ required:
+ - '#address-cells'
+ - '#interrupt-cells'
+ - interrupt-controller
+
+ additionalProperties: false
+
+required:
+ - compatible
+ - reg
+ - reg-names
+ - interrupts
+ - ranges
+ - clocks
+ - clock-names
+ - '#interrupt-cells'
+ - interrupt-controller
+
+allOf:
+ - $ref: /schemas/pci/pci-host-bridge.yaml#
+ - if:
+ properties:
+ num-lanes:
+ const: 2
+ then:
+ properties:
+ regs:
+ minItems: 2
+
+ reg-names:
+ items:
+ - const: pcie-mac
+ - const: sec-pcie-mac
+
+ resets:
+ minItems: 4
+
+ reset-names:
+ items:
+ - const: phy-lane0
+ - const: phy-lane1
+ - const: perstout
+ - const: sec-perstout
+
+ required:
+ - airoha,scu
+
+ else:
+ properties:
+ reg:
+ maxItems: 1
+
+ reg-names:
+ items:
+ - const: pcie-mac
+
+ resets:
+ minItems: 2
+ maxItems: 3
+
+ reset-names:
+ minItems: 2
+ items:
+ - enum: [ phy-lane0, phy-lane1, phy-lan2 ]
+ - enum: [ phy-lane1, perstout ]
+ - const: phy-lane2
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ bus {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ pcie@1fc00000 {
+ compatible = "airoha,en7581-pcie";
+ device_type = "pci";
+ #address-cells = <3>;
+ #size-cells = <2>;
+
+ reg = <0x0 0x1fc00000 0x0 0x1670>,
+ <0x0 0x1fc20000 0x0 0x1670>;
+ reg-names = "pcie-mac", "sec-pcie-mac";
+
+ clocks = <&scuclk 7>;
+ clock-names = "sys-ck";
+
+ phys = <&pciephy>;
+ phy-names = "pcie-phy";
+
+ ranges = <0x02000000 0 0x20000000 0x0 0x20000000 0 0x4000000>;
+
+ resets = <&scuclk 48>,
+ <&scuclk 49>,
+ <&scuclk 53>,
+ <&scuclk 54>;
+ reset-names = "phy-lane0", "phy-lane1",
+ "perstout", "sec-perstout";
+
+ num-lanes = <2>;
+
+ mediatek,pbus-csr = <&pbus_csr 0x0 0x4>;
+
+ airoha,scu = <&scuclk>;
+
+ interrupts = <GIC_SPI 39 IRQ_TYPE_LEVEL_HIGH>;
+ bus-range = <0x00 0xff>;
+ #interrupt-cells = <1>;
+ interrupt-map-mask = <0 0 0 0x7>;
+ interrupt-map = <0 0 0 1 &pcie_intc 0>,
+ <0 0 0 2 &pcie_intc 1>,
+ <0 0 0 3 &pcie_intc 2>,
+ <0 0 0 4 &pcie_intc 3>;
+ pcie_intc: interrupt-controller {
+ #address-cells = <0>;
+ #interrupt-cells = <1>;
+ interrupt-controller;
+ };
+ };
+ };
diff --git a/Documentation/devicetree/bindings/pci/mediatek-pcie-gen3.yaml b/Documentation/devicetree/bindings/pci/mediatek-pcie-gen3.yaml
index 4db700fc36ba..510f1f2b1c5a 100644
--- a/Documentation/devicetree/bindings/pci/mediatek-pcie-gen3.yaml
+++ b/Documentation/devicetree/bindings/pci/mediatek-pcie-gen3.yaml
@@ -59,7 +59,6 @@ properties:
- const: mediatek,mt8196-pcie
- const: mediatek,mt8192-pcie
- const: mediatek,mt8196-pcie
- - const: airoha,en7581-pcie
reg:
maxItems: 1
@@ -83,20 +82,20 @@ properties:
resets:
minItems: 1
- maxItems: 3
+ maxItems: 2
reset-names:
minItems: 1
- maxItems: 3
+ maxItems: 2
items:
- enum: [ phy, mac, phy-lane0, phy-lane1, phy-lane2 ]
+ enum: [ phy, mac ]
clocks:
- minItems: 1
+ minItems: 4
maxItems: 6
clock-names:
- minItems: 1
+ minItems: 4
maxItems: 6
assigned-clocks:
@@ -115,17 +114,6 @@ properties:
power-domains:
maxItems: 1
- mediatek,pbus-csr:
- $ref: /schemas/types.yaml#/definitions/phandle-array
- items:
- - items:
- - description: phandle to pbus-csr syscon
- - description: offset of pbus-csr base address register
- - description: offset of pbus-csr base address mask register
- description:
- Phandle with two arguments to the syscon node used to detect if
- a given address is accessible on PCIe controller.
-
'#interrupt-cells':
const: 1
@@ -177,16 +165,6 @@ allOf:
- const: peri_26m
- const: top_133m
- resets:
- minItems: 1
- maxItems: 2
-
- reset-names:
- minItems: 1
- maxItems: 2
-
- mediatek,pbus-csr: false
-
- if:
properties:
compatible:
@@ -208,16 +186,6 @@ allOf:
- const: peri_26m
- const: peri_mem
- resets:
- minItems: 1
- maxItems: 2
-
- reset-names:
- minItems: 1
- maxItems: 2
-
- mediatek,pbus-csr: false
-
- if:
properties:
compatible:
@@ -246,8 +214,6 @@ allOf:
- const: phy
- const: mac
- mediatek,pbus-csr: false
-
- if:
properties:
compatible:
@@ -257,7 +223,6 @@ allOf:
then:
properties:
clocks:
- minItems: 4
maxItems: 4
clock-names:
@@ -267,38 +232,6 @@ allOf:
- const: peri_26m
- const: top_133m
- resets:
- minItems: 1
- maxItems: 2
-
- reset-names:
- minItems: 1
- maxItems: 2
-
- mediatek,pbus-csr: false
-
- - if:
- properties:
- compatible:
- const: airoha,en7581-pcie
- then:
- properties:
- clocks:
- maxItems: 1
-
- clock-names:
- items:
- - const: sys-ck
-
- resets:
- minItems: 3
-
- reset-names:
- items:
- - const: phy-lane0
- - const: phy-lane1
- - const: phy-lane2
-
unevaluatedProperties: false
examples:
--
2.53.0
^ permalink raw reply related
* [PATCH 2/4] clk: en7523: add support for dedicated PCIe PERSTOUT reset
From: Christian Marangi @ 2026-06-25 21:57 UTC (permalink / raw)
To: Lorenzo Pieralisi, Krzysztof Wilczyński,
Manivannan Sadhasivam, Rob Herring, Bjorn Helgaas,
Krzysztof Kozlowski, Conor Dooley, Ryder Lee, Michael Turquette,
Stephen Boyd, Brian Masney, Philipp Zabel, Matthias Brugger,
AngeloGioacchino Del Regno, Christian Marangi, Jianjun Wang,
linux-pci, devicetree, linux-kernel, linux-mediatek, linux-clk,
linux-arm-kernel
In-Reply-To: <20260625215741.3253212-1-ansuelsmth@gmail.com>
Add support for resetting the PCIe lines with the PERSTOUT reset. These
special reset are controlled by the PCIC register and are specific to each
of the 3 PCIe lines.
Notice that reset logic is inverted for these bit where 0 is assert and 1
deassert. This is intenrally handled in the reset function.
PCI enable/disable are updated to drop PERSTOUT bits in favor dedicated
reset handling.
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
---
drivers/clk/clk-en7523.c | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/drivers/clk/clk-en7523.c b/drivers/clk/clk-en7523.c
index 1ab0e2eca5d3..a33cf2e1b76f 100644
--- a/drivers/clk/clk-en7523.c
+++ b/drivers/clk/clk-en7523.c
@@ -338,6 +338,7 @@ static const struct en_clk_desc en7581_base_clks[] = {
static const u16 en7581_rst_ofs[] = {
REG_RST_CTRL2,
REG_RST_CTRL1,
+ REG_NP_SCU_PCIC,
};
static const u16 en751221_rst_ofs[] = {
@@ -450,6 +451,11 @@ static const u16 en7581_rst_map[] = {
[EN7581_CPU_TIMER_RST] = RST_NR_PER_BANK + 28,
[EN7581_PCIE_HB_RST] = RST_NR_PER_BANK + 29,
[EN7581_XPON_MAC_RST] = RST_NR_PER_BANK + 31,
+
+ /* RST_PCIC */
+ [EN7581_PCIC_PERSTOUT0_RST] = 2 * RST_NR_PER_BANK + 29,
+ [EN7581_PCIC_PERSTOUT1_RST] = 2 * RST_NR_PER_BANK + 26,
+ [EN7581_PCIC_PERSTOUT2_RST] = 2 * RST_NR_PER_BANK + 16,
};
static const u16 en751221_rst_map[] = {
@@ -635,9 +641,7 @@ static int en7581_pci_enable(struct clk_hw *hw)
void __iomem *np_base = cg->base;
u32 val, mask;
- mask = REG_PCI_CONTROL_REFCLK_EN0 | REG_PCI_CONTROL_REFCLK_EN1 |
- REG_PCI_CONTROL_PERSTOUT1 | REG_PCI_CONTROL_PERSTOUT2 |
- REG_PCI_CONTROL_PERSTOUT;
+ mask = REG_PCI_CONTROL_REFCLK_EN0 | REG_PCI_CONTROL_REFCLK_EN1;
val = readl(np_base + REG_PCI_CONTROL);
writel(val | mask, np_base + REG_PCI_CONTROL);
@@ -650,9 +654,7 @@ static void en7581_pci_disable(struct clk_hw *hw)
void __iomem *np_base = cg->base;
u32 val, mask;
- mask = REG_PCI_CONTROL_REFCLK_EN0 | REG_PCI_CONTROL_REFCLK_EN1 |
- REG_PCI_CONTROL_PERSTOUT1 | REG_PCI_CONTROL_PERSTOUT2 |
- REG_PCI_CONTROL_PERSTOUT;
+ mask = REG_PCI_CONTROL_REFCLK_EN0 | REG_PCI_CONTROL_REFCLK_EN1;
val = readl(np_base + REG_PCI_CONTROL);
writel(val & ~mask, np_base + REG_PCI_CONTROL);
usleep_range(1000, 2000);
@@ -754,14 +756,21 @@ static int en7523_reset_update(struct reset_controller_dev *rcdev,
unsigned long id, bool assert)
{
struct en_rst_data *rst_data = container_of(rcdev, struct en_rst_data, rcdev);
- void __iomem *addr = rst_data->base + rst_data->bank_ofs[id / RST_NR_PER_BANK];
+ u32 offset = rst_data->bank_ofs[id / RST_NR_PER_BANK];
+ void __iomem *addr = rst_data->base + offset;
+ bool inverted = false;
u32 val;
+ /* For PCIC reset logic is inverted, 0:assert 1:deassert*/
+ if (offset == REG_NP_SCU_PCIC)
+ inverted = true;
+
val = readl(addr);
+ val &= ~BIT(id % RST_NR_PER_BANK);
if (assert)
- val |= BIT(id % RST_NR_PER_BANK);
+ val |= inverted ? 0 : BIT(id % RST_NR_PER_BANK);
else
- val &= ~BIT(id % RST_NR_PER_BANK);
+ val |= inverted ? BIT(id % RST_NR_PER_BANK) : 0;
writel(val, addr);
return 0;
--
2.53.0
^ permalink raw reply related
* [PATCH 1/4] dt-bindings: clock: airoha: Add additional reset for PCIe PERSTOUT
From: Christian Marangi @ 2026-06-25 21:57 UTC (permalink / raw)
To: Lorenzo Pieralisi, Krzysztof Wilczyński,
Manivannan Sadhasivam, Rob Herring, Bjorn Helgaas,
Krzysztof Kozlowski, Conor Dooley, Ryder Lee, Michael Turquette,
Stephen Boyd, Brian Masney, Philipp Zabel, Matthias Brugger,
AngeloGioacchino Del Regno, Christian Marangi, Jianjun Wang,
linux-pci, devicetree, linux-kernel, linux-mediatek, linux-clk,
linux-arm-kernel
In-Reply-To: <20260625215741.3253212-1-ansuelsmth@gmail.com>
Add additional reset to control PCIe PERSTOUT reset line for each of the 3
PCIe lines.
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
---
include/dt-bindings/reset/airoha,en7581-reset.h | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/include/dt-bindings/reset/airoha,en7581-reset.h b/include/dt-bindings/reset/airoha,en7581-reset.h
index 6544a1790b83..25e75534daa9 100644
--- a/include/dt-bindings/reset/airoha,en7581-reset.h
+++ b/include/dt-bindings/reset/airoha,en7581-reset.h
@@ -62,5 +62,9 @@
#define EN7581_CPU_TIMER_RST 50
#define EN7581_PCIE_HB_RST 51
#define EN7581_XPON_MAC_RST 52
+/* RST_PCIC */
+#define EN7581_PCIC_PERSTOUT0_RST 53
+#define EN7581_PCIC_PERSTOUT1_RST 54
+#define EN7581_PCIC_PERSTOUT2_RST 55
#endif /* __DT_BINDINGS_RESET_CONTROLLER_AIROHA_EN7581_H_ */
--
2.53.0
^ permalink raw reply related
* [PATCH 0/4] PCI: mediatek-gen3: Add 2-lanes mode support + clock
From: Christian Marangi @ 2026-06-25 21:57 UTC (permalink / raw)
To: Lorenzo Pieralisi, Krzysztof Wilczyński,
Manivannan Sadhasivam, Rob Herring, Bjorn Helgaas,
Krzysztof Kozlowski, Conor Dooley, Ryder Lee, Michael Turquette,
Stephen Boyd, Brian Masney, Philipp Zabel, Matthias Brugger,
AngeloGioacchino Del Regno, Christian Marangi, Jianjun Wang,
linux-pci, devicetree, linux-kernel, linux-mediatek, linux-clk,
linux-arm-kernel
This small series introduce support for 2-lanes mode for Airoha AN7581
SoC. This is needed for correctly functionality of Eagle WiFi Card
normally attached to this SoC that require a 2-line PCIe card to
correctly work (and give the proper performance)
The first 2 patch address a limitation of the PCIe implementation
where the PERSTOUT reset were indirectly asserted and deasserted
all at the same time (for all the 3 PCIe card) with PCIe
enable and disable.
The 2 patch address this and introduce correct reset to control
reset line for the relevant PCIe line.
The last 2 patch add additional logic and support to assert
and deassert the PERSTOUT and also apply the required configuration
for 2-lanes mode.
2-lanes mode is implemented in DT by adding the required property
and by defining the "num-lanes" to 2.
Christian Marangi (4):
dt-bindings: clock: airoha: Add additional reset for PCIe PERSTOUT
clk: en7523: add support for dedicated PCIe PERSTOUT reset
dt-bindings: PCI: mediatek-gen3: Split Airoha schema and document
2-lanes
PCI: mediatek-gen3: Add 2-lanes mode support for Airoha AN7581
.../bindings/pci/airoha,en7581-pcie.yaml | 251 ++++++++++++++++++
.../bindings/pci/mediatek-pcie-gen3.yaml | 77 +-----
drivers/clk/clk-en7523.c | 27 +-
drivers/pci/controller/pcie-mediatek-gen3.c | 98 +++++--
.../dt-bindings/reset/airoha,en7581-reset.h | 4 +
5 files changed, 358 insertions(+), 99 deletions(-)
create mode 100644 Documentation/devicetree/bindings/pci/airoha,en7581-pcie.yaml
--
2.53.0
^ permalink raw reply
* [PATCH v2 8/8] iio: adc: ti-ads112c14: add measurement channel support
From: David Lechner (TI) @ 2026-06-25 21:55 UTC (permalink / raw)
To: Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: Chris Hall, Patrick Edwards, Kurt Borja, Nguyen Minh Tien,
linux-iio, devicetree, linux-kernel, David Lechner
In-Reply-To: <20260625-iio-adc-ti-ads122c14-v2-0-ceb9b0b561cb@baylibre.com>
Add support for parsing devicetree properties for measurement channels
and doing direct reads on these.
There are quite a lot of conditions that have to be met for each
measurement to be made, so quite a bit of state and algorithms are
required to handle it.
Channels are created dynamically since the number of possibilities is
unreasonably large.
Signed-off-by: David Lechner (TI) <dlechner@baylibre.com>
---
v2 changes:
* Adapted for changes in DT bindings.
* Fixed bug in IDAC current register value calculation.
* Fix uninitialized variable bug.
* Fix bug in data->num_measurements calculation.
* Use IIO_RESISTANCE instead of IIO_VOLTAGE when external reference is
used and it is a resistor rather than a voltage source.
* Fix bug with negative input mux selection on single-ended measurements.
* Fixed return checks of devm_regulator_get_enable_read_voltage().
---
drivers/iio/adc/ti-ads112c14.c | 523 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 512 insertions(+), 11 deletions(-)
diff --git a/drivers/iio/adc/ti-ads112c14.c b/drivers/iio/adc/ti-ads112c14.c
index 0e775dbc8d50..05d9670c72a4 100644
--- a/drivers/iio/adc/ti-ads112c14.c
+++ b/drivers/iio/adc/ti-ads112c14.c
@@ -9,11 +9,14 @@
*/
#include <linux/bitfield.h>
+#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/dev_printk.h>
+#include <linux/device/devres.h>
#include <linux/i2c.h>
#include <linux/iio/iio.h>
#include <linux/math64.h>
+#include <linux/minmax.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/property.h>
@@ -62,6 +65,10 @@
#define ADS112C14_DEVICE_CFG_PWDN BIT(7)
#define ADS112C14_DEVICE_CFG_STBY_MODE BIT(6)
#define ADS112C14_DEVICE_CFG_BOCS GENMASK(5, 4)
+#define ADS112C14_DEVICE_CFG_BOCS_DISABLED 0
+#define ADS112C14_DEVICE_CFG_BOCS_200_nA 1
+#define ADS112C14_DEVICE_CFG_BOCS_1_uA 2
+#define ADS112C14_DEVICE_CFG_BOCS_10_uA 3
#define ADS112C14_DEVICE_CFG_CLK_SEL BIT(3)
#define ADS112C14_DEVICE_CFG_CONV_MODE BIT(2)
#define ADS112C14_DEVICE_CFG_SPEED_MODE GENMASK(1, 0)
@@ -74,6 +81,7 @@
#define ADS112C14_REG_MUX_CFG 0x07
#define ADS112C14_MUX_CFG_AINP GENMASK(7, 4)
#define ADS112C14_MUX_CFG_AINN GENMASK(3, 0)
+#define ADS112C14_MUX_CFG_AIN_GND 0x8
#define ADS112C14_REG_GAIN_CFG 0x08
#define ADS112C14_GAIN_CFG_SPARE BIT(7)
@@ -88,6 +96,9 @@
#define ADS112C14_REFERENCE_CFG_REF_VAL_1_25V 0
#define ADS112C14_REFERENCE_CFG_REF_VAL_2_5V 1
#define ADS112C14_REFERENCE_CFG_REF_SEL GENMASK(1, 0)
+#define ADS112C14_REFERENCE_CFG_REF_SEL_INTERNAL 0
+#define ADS112C14_REFERENCE_CFG_REF_SEL_EXTERNAL 1
+#define ADS112C14_REFERENCE_CFG_REF_SEL_AVDD 2
#define ADS112C14_REG_DIGITAL_CFG 0x0A
#define ADS112C14_DIGITAL_CFG_REG_MAP_CRC_EN BIT(6)
@@ -124,6 +135,20 @@
#define ADS112C14_INT_REF0_mV 1250
#define ADS112C14_INT_REF1_mV 2500
+enum {
+ ADS112C14_VREF_SOURCE_INTERNAL_2_5V,
+ ADS112C14_VREF_SOURCE_INTERNAL_1_25V,
+ ADS112C14_VREF_SOURCE_EXTERNAL,
+ ADS112C14_VREF_SOURCE_AVDD,
+};
+
+static const char * const ads112c14_vref_source_names[] = {
+ [ADS112C14_VREF_SOURCE_INTERNAL_2_5V] = "internal-2.5v",
+ [ADS112C14_VREF_SOURCE_INTERNAL_1_25V] = "internal-1.25v",
+ [ADS112C14_VREF_SOURCE_EXTERNAL] = "external",
+ [ADS112C14_VREF_SOURCE_AVDD] = "avdd",
+};
+
/* Available gains as tenths (x10) */
static const u32 ads112c14_pga_gains_x10[] = {
5, /* 0.5 */
@@ -199,9 +224,32 @@ struct ads112c14_chip_info {
u32 resolution_bits;
};
+struct ads112c14_measurement {
+ const char *label;
+ u32 vref_source;
+ u8 iunit;
+ u8 idac1_mag;
+ u8 idac2_mag;
+ u8 idac1_mux;
+ u8 idac2_mux;
+ u8 iadc_count;
+ u8 gain_val;
+ u8 burnout;
+ bool global_chop;
+ bool bipolar;
+ int scale_available[ARRAY_SIZE(ads112c14_pga_gains_x10)][2];
+};
+
struct ads112c14_data {
const struct ads112c14_chip_info *chip_info;
struct regmap *regmap;
+ u32 avdd_uV;
+ u32 ext_ref_uV;
+ bool refp_is_avdd;
+ bool refn_is_gnd;
+ u32 ext_ref_ohms;
+ struct ads112c14_measurement *measurements;
+ u32 num_measurements;
u8 sys_mon_chan_short_gain_val;
int sys_mon_chan_short_scale_available[ARRAY_SIZE(ads112c14_pga_gains_x10)][2];
};
@@ -260,12 +308,106 @@ static const struct iio_chan_spec ads112c14_sys_mon_channels[] = {
},
};
+static int ads112c14_prepare_measurement_channel(struct ads112c14_data *data,
+ const struct iio_chan_spec *chan)
+{
+ struct ads112c14_measurement *measurement = &data->measurements[chan->scan_index];
+ u32 refp_buf_en, refn_buf_en, ref_val, ref_sel;
+ int ret;
+
+ ret = regmap_write(data->regmap, ADS112C14_REG_MUX_CFG,
+ FIELD_PREP(ADS112C14_MUX_CFG_AINP, chan->channel) |
+ FIELD_PREP(ADS112C14_MUX_CFG_AINN, chan->channel2));
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(data->regmap, ADS112C14_REG_DIGITAL_CFG,
+ ADS112C14_DIGITAL_CFG_CODING,
+ FIELD_PREP(ADS112C14_DIGITAL_CFG_CODING,
+ measurement->bipolar ? 0 : 1));
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(data->regmap, ADS112C14_REG_GAIN_CFG,
+ ADS112C14_GAIN_CFG_SYS_MON | ADS112C14_GAIN_CFG_GAIN,
+ FIELD_PREP(ADS112C14_GAIN_CFG_SYS_MON, 0) |
+ FIELD_PREP(ADS112C14_GAIN_CFG_GAIN,
+ measurement->gain_val));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(data->regmap, ADS112C14_REG_IDAC_MAG_CFG,
+ FIELD_PREP(ADS112C14_IDAC_MAG_CFG_I2MAG,
+ measurement->idac2_mag) |
+ FIELD_PREP(ADS112C14_IDAC_MAG_CFG_I1MAG,
+ measurement->idac1_mag));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(data->regmap, ADS112C14_REG_IDAC_MUX_CFG,
+ FIELD_PREP(ADS112C14_IDAC_MUX_CFG_IUNIT,
+ measurement->iunit) |
+ FIELD_PREP(ADS112C14_IDAC_MUX_CFG_I2MUX,
+ measurement->idac2_mux) |
+ FIELD_PREP(ADS112C14_IDAC_MUX_CFG_I1MUX,
+ measurement->idac1_mux));
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(data->regmap, ADS112C14_REG_DATA_RATE_CFG,
+ ADS112C14_DATA_RATE_CFG_GC_EN,
+ FIELD_PREP(ADS112C14_DATA_RATE_CFG_GC_EN,
+ measurement->global_chop));
+ if (ret)
+ return ret;
+
+ refp_buf_en = !data->refp_is_avdd &&
+ measurement->vref_source == ADS112C14_VREF_SOURCE_EXTERNAL;
+ refn_buf_en = !data->refn_is_gnd &&
+ measurement->vref_source == ADS112C14_VREF_SOURCE_EXTERNAL;
+
+ ref_val = measurement->vref_source == ADS112C14_VREF_SOURCE_INTERNAL_2_5V ?
+ ADS112C14_REFERENCE_CFG_REF_VAL_2_5V :
+ ADS112C14_REFERENCE_CFG_REF_VAL_1_25V;
+
+ switch (measurement->vref_source) {
+ case ADS112C14_VREF_SOURCE_AVDD:
+ ref_sel = ADS112C14_REFERENCE_CFG_REF_SEL_AVDD;
+ break;
+ case ADS112C14_VREF_SOURCE_EXTERNAL:
+ ref_sel = ADS112C14_REFERENCE_CFG_REF_SEL_EXTERNAL;
+ break;
+ default:
+ ref_sel = ADS112C14_REFERENCE_CFG_REF_SEL_INTERNAL;
+ break;
+ }
+
+ return regmap_update_bits(data->regmap, ADS112C14_REG_REFERENCE_CFG,
+ ADS112C14_REFERENCE_CFG_REFP_BUF_EN |
+ ADS112C14_REFERENCE_CFG_REFN_BUF_EN |
+ ADS112C14_REFERENCE_CFG_REF_VAL |
+ ADS112C14_REFERENCE_CFG_REF_SEL,
+ FIELD_PREP(ADS112C14_REFERENCE_CFG_REFP_BUF_EN,
+ refp_buf_en) |
+ FIELD_PREP(ADS112C14_REFERENCE_CFG_REFN_BUF_EN,
+ refn_buf_en) |
+ FIELD_PREP(ADS112C14_REFERENCE_CFG_REF_VAL,
+ ref_val) |
+ FIELD_PREP(ADS112C14_REFERENCE_CFG_REF_SEL,
+ ref_sel));
+}
+
static int ads112c14_prepare_sys_mon_channel(struct ads112c14_data *data,
const struct iio_chan_spec *chan)
{
u32 gain_val;
int ret;
+ /*
+ * NB: IDAC registers are left as-is in case they are generating current
+ * needed for the external reference measurement.
+ */
+
/*
* All SYS_MON channels use GAIN of 1 to keep it simple. Other than
* the internal short channel, where it is useful in practice.
@@ -321,8 +463,9 @@ static int ads112c14_single_conversion(struct ads112c14_data *data,
int ret;
if (chan->channel < 100) {
- /* Not implemented yet. */
- return -EINVAL;
+ ret = ads112c14_prepare_measurement_channel(data, chan);
+ if (ret)
+ return ret;
} else {
ret = ads112c14_prepare_sys_mon_channel(data, chan);
if (ret)
@@ -351,6 +494,7 @@ static int ads112c14_read_raw(struct iio_dev *indio_dev,
int *val2, long mask)
{
struct ads112c14_data *data = iio_priv(indio_dev);
+ struct ads112c14_measurement *measurement = NULL;
u32 vref_uV, fsr_bits;
int *scale_avail;
@@ -358,8 +502,8 @@ static int ads112c14_read_raw(struct iio_dev *indio_dev,
vref_uV = ADS112C14_INT_REF1_mV * (MICRO / MILLI);
if (chan->channel < 100) {
- /* Not implemented yet. */
- return -EINVAL;
+ measurement = &data->measurements[chan->scan_index];
+ fsr_bits = data->chip_info->resolution_bits - measurement->bipolar;
} else {
/* All SYS_MON channels are using signed coding. */
fsr_bits = data->chip_info->resolution_bits - 1;
@@ -389,7 +533,8 @@ static int ads112c14_read_raw(struct iio_dev *indio_dev,
return -EINVAL;
}
- *val = sign_extend32(*val, fsr_bits);
+ if (!measurement || measurement->bipolar)
+ *val = sign_extend32(*val, fsr_bits);
return IIO_VAL_INT;
}
@@ -401,6 +546,14 @@ static int ads112c14_read_raw(struct iio_dev *indio_dev,
return IIO_VAL_FRACTIONAL_LOG2;
}
+ if (chan->channel < 100) {
+ scale_avail = measurement->scale_available[measurement->gain_val];
+ *val = scale_avail[0];
+ *val2 = scale_avail[1];
+
+ return IIO_VAL_INT_PLUS_NANO;
+ }
+
if (chan->channel == ADS112C14_SYS_MON_CHANNEL_SHORT) {
u8 idx = data->sys_mon_chan_short_gain_val;
@@ -441,6 +594,16 @@ static int ads112c14_read_avail(struct iio_dev *indio_dev,
{
struct ads112c14_data *data = iio_priv(indio_dev);
+ if (chan->channel < 100) {
+ struct ads112c14_measurement *measurement;
+
+ measurement = &data->measurements[chan->scan_index];
+ *vals = (const int *)measurement->scale_available;
+ *length = 2 * ARRAY_SIZE(measurement->scale_available);
+ *type = IIO_VAL_INT_PLUS_NANO;
+ return IIO_AVAIL_LIST;
+ }
+
if (chan->channel == ADS112C14_SYS_MON_CHANNEL_SHORT) {
*vals = (const int *)data->sys_mon_chan_short_scale_available;
*length = 2 * ARRAY_SIZE(data->sys_mon_chan_short_scale_available);
@@ -461,7 +624,13 @@ static int ads112c14_write_raw(struct iio_dev *indio_dev,
switch (mask) {
case IIO_CHAN_INFO_SCALE: {
- if (chan->channel == ADS112C14_SYS_MON_CHANNEL_SHORT) {
+ if (chan->channel < 100) {
+ struct ads112c14_measurement *measurement;
+
+ measurement = &data->measurements[chan->scan_index];
+ scale_avail = measurement->scale_available;
+ gain_val = &measurement->gain_val;
+ } else if (chan->channel == ADS112C14_SYS_MON_CHANNEL_SHORT) {
scale_avail = data->sys_mon_chan_short_scale_available;
gain_val = &data->sys_mon_chan_short_gain_val;
} else {
@@ -501,8 +670,21 @@ static int ads112c14_write_raw_get_fmt(struct iio_dev *indio_dev,
static int ads112c14_read_label(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan, char *label)
{
+ struct ads112c14_data *data = iio_priv(indio_dev);
const char *label_source;
+ /* measurement channels */
+ if (chan->channel < 100) {
+ struct ads112c14_measurement *measurement;
+
+ measurement = &data->measurements[chan->scan_index];
+
+ if (!measurement->label)
+ return -EINVAL;
+
+ return sysfs_emit(label, "%s\n", measurement->label);
+ }
+
/* System monitor channels. */
switch (chan->channel) {
case ADS112C14_SYS_MON_CHANNEL_TEMP:
@@ -535,6 +717,234 @@ static const struct iio_info ads112c14_info = {
.read_label = ads112c14_read_label,
};
+static int ads112c14_populate_idac_mag(u32 current_nA, u8 *idac_mag)
+{
+ u32 current_uA = current_nA / (NANO / MICRO);
+
+ /* Convert microamps to IMAG bits */
+ if (current_uA == 1)
+ *idac_mag = 1;
+ else if (in_range(current_uA, 10, 100) && current_uA % 10 == 0)
+ *idac_mag = current_uA / 10 + 1;
+ else
+ return dev_err_probe(NULL, -EINVAL,
+ "invalid excitation-current-nanoamp value\n");
+
+ return 0;
+}
+
+static int ads112c14_parse_channels(struct iio_dev *indio_dev,
+ bool *need_avdd_ref, bool *need_ext_ref)
+{
+ struct ads112c14_data *data = iio_priv(indio_dev);
+ struct device *dev = indio_dev->dev.parent;
+ struct iio_chan_spec *channels;
+ u32 num_child_nodes, i, pair[2];
+ int ret;
+
+ *need_avdd_ref = false;
+ *need_ext_ref = false;
+
+ num_child_nodes = device_get_named_child_node_count(dev, "channel");
+
+ data->measurements = devm_kcalloc(dev, num_child_nodes,
+ sizeof(*data->measurements), GFP_KERNEL);
+ if (!data->measurements)
+ return -ENOMEM;
+
+ channels = devm_kcalloc(dev, num_child_nodes +
+ ARRAY_SIZE(ads112c14_sys_mon_channels),
+ sizeof(*channels), GFP_KERNEL);
+ if (!channels)
+ return -ENOMEM;
+
+ i = 0;
+ device_for_each_named_child_node_scoped(dev, child, "channel") {
+ struct ads112c14_measurement *measurement = &data->measurements[i];
+ struct iio_chan_spec *spec = &channels[i];
+
+ if (!fwnode_device_is_available(child))
+ continue;
+
+ spec->indexed = 1;
+ spec->scan_index = i;
+ measurement->gain_val = 1;
+
+ fwnode_property_read_string(child, "label", &measurement->label);
+
+ if (fwnode_property_present(child, "single-channel")) {
+ ret = fwnode_property_read_u32(child, "single-channel",
+ &pair[0]);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read single-channel property\n");
+
+ if (pair[0] >= 8)
+ return dev_err_probe(dev, -EINVAL,
+ "single-channel value must be between 0 and 7\n");
+
+ spec->channel = pair[0];
+ /* NB: channel2 is unused by iio core code in this case. */
+ spec->channel2 = ADS112C14_MUX_CFG_AIN_GND;
+ } else if (fwnode_property_present(child, "diff-channels")) {
+ ret = fwnode_property_read_u32_array(child, "diff-channels",
+ pair, ARRAY_SIZE(pair));
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read diff-channels property\n");
+
+ if (pair[0] >= 8 || pair[1] >= 8)
+ return dev_err_probe(dev, -EINVAL,
+ "diff-channels values must be between 0 and 7\n");
+
+ spec->differential = 1;
+ spec->channel = pair[0];
+ spec->channel2 = pair[1];
+ } else {
+ return dev_err_probe(dev, -EINVAL,
+ "channel node missing channel type property\n");
+ }
+
+ if (fwnode_property_present(child, "excitation-channels")) {
+ ret = fwnode_property_count_u32(child, "excitation-channels");
+ if (ret < 0)
+ return dev_err_probe(dev, ret,
+ "failed to read excitation-channels property\n");
+
+ if (ret < 1 || ret > 2)
+ return dev_err_probe(dev, -EINVAL,
+ "excitation-channels property must have 1 or 2 values\n");
+
+ measurement->iadc_count = ret;
+ pair[1] = 0;
+
+ ret = fwnode_property_read_u32_array(child, "excitation-channels",
+ pair, measurement->iadc_count);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read excitation-channels property\n");
+
+ if (pair[0] >= 8 || pair[1] >= 8)
+ return dev_err_probe(dev, -EINVAL,
+ "excitation-channels values must be between 0 and 7\n");
+
+ measurement->idac1_mux = pair[0];
+ measurement->idac2_mux = measurement->iadc_count > 1 ? pair[1] : 0;
+
+ ret = fwnode_property_read_u32_array(child, "excitation-current-nanoamp",
+ pair, measurement->iadc_count);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read excitation-current-nanoamp property\n");
+
+ if (pair[0] <= 100000 && (measurement->iadc_count == 1 || pair[1] <= 100000)) {
+ /*
+ * If both values are 100uA or less, then we can
+ * use IUNIT = 1uA for better precision.
+ */
+ ret = ads112c14_populate_idac_mag(pair[0],
+ &measurement->idac1_mag);
+ if (ret)
+ return ret;
+
+ if (measurement->iadc_count > 1) {
+ ret = ads112c14_populate_idac_mag(pair[1],
+ &measurement->idac2_mag);
+ if (ret)
+ return ret;
+ }
+ } else {
+ /*
+ * Otherwise, IUINT is 10uA (flag set) and so
+ * IxMAG is 1/10 of the actual current.
+ */
+ measurement->iunit = 1;
+
+ ret = ads112c14_populate_idac_mag(pair[0] / 10,
+ &measurement->idac1_mag);
+ if (ret)
+ return ret;
+
+ if (measurement->iadc_count > 1) {
+ ret = ads112c14_populate_idac_mag(pair[1] / 10,
+ &measurement->idac2_mag);
+ if (ret)
+ return ret;
+ }
+ }
+ }
+
+ measurement->global_chop = fwnode_property_read_bool(child,
+ "input-channel-rotation");
+
+ if (fwnode_property_present(child, "burn-out-current-nanoamp")) {
+ u32 burnout_nA;
+
+ ret = fwnode_property_read_u32(child, "burn-out-current-nanoamp",
+ &burnout_nA);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read burn-out-current-nanoamp property\n");
+
+ switch (burnout_nA) {
+ case 200:
+ measurement->burnout = ADS112C14_DEVICE_CFG_BOCS_200_nA;
+ break;
+ case 1000:
+ measurement->burnout = ADS112C14_DEVICE_CFG_BOCS_1_uA;
+ break;
+ case 10000:
+ measurement->burnout = ADS112C14_DEVICE_CFG_BOCS_10_uA;
+ break;
+ default:
+ return dev_err_probe(dev, -EINVAL,
+ "invalid burn-out-current-nanoamp value\n");
+ }
+ }
+
+ measurement->bipolar = fwnode_property_read_bool(child, "bipolar");
+
+ if (fwnode_property_present(child, "reference-sources")) {
+ ret = fwnode_property_match_property_string(child,
+ "reference-sources", ads112c14_vref_source_names,
+ ARRAY_SIZE(ads112c14_vref_source_names));
+ if (ret < 0)
+ return dev_err_probe(dev, ret,
+ "invalid reference-sources value\n");
+
+ measurement->vref_source = ret;
+ }
+
+ if (measurement->vref_source == ADS112C14_VREF_SOURCE_AVDD)
+ *need_avdd_ref = true;
+ if (measurement->vref_source == ADS112C14_VREF_SOURCE_EXTERNAL)
+ *need_ext_ref = true;
+
+ spec->info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE);
+ spec->info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE);
+
+ /*
+ * If reference source is resistor rather than voltage supply,
+ * then the measurement is effectively a resistance measurement.
+ */
+ spec->type = (measurement->vref_source == ADS112C14_VREF_SOURCE_EXTERNAL &&
+ data->ext_ref_ohms) ? IIO_RESISTANCE : IIO_VOLTAGE;
+
+ if (spec->type == IIO_RESISTANCE)
+ spec->differential = 0;
+
+ i++;
+ }
+
+ data->num_measurements = i;
+ memcpy(channels + i, ads112c14_sys_mon_channels, sizeof(ads112c14_sys_mon_channels));
+
+ indio_dev->channels = channels;
+ indio_dev->num_channels = i + ARRAY_SIZE(ads112c14_sys_mon_channels);
+
+ return 0;
+}
+
static void ads112c14_populate_scale_available(int scale_avail[][2],
u32 full_scale, u32 fsr_bits)
{
@@ -553,6 +963,33 @@ static void ads112c14_populate_tables(struct ads112c14_data *data)
{
u32 full_scale, fsr_bits;
+ for (u32 i = 0; i < data->num_measurements; i++) {
+ struct ads112c14_measurement *measurement = &data->measurements[i];
+
+ switch (measurement->vref_source) {
+ case ADS112C14_VREF_SOURCE_EXTERNAL:
+ if (data->ext_ref_ohms)
+ full_scale = data->ext_ref_ohms;
+ else
+ full_scale = data->ext_ref_uV / (MICRO / MILLI);
+ break;
+ case ADS112C14_VREF_SOURCE_AVDD:
+ full_scale = data->avdd_uV / (MICRO / MILLI);
+ break;
+ case ADS112C14_VREF_SOURCE_INTERNAL_1_25V:
+ full_scale = ADS112C14_INT_REF0_mV;
+ break;
+ default:
+ full_scale = ADS112C14_INT_REF1_mV;
+ break;
+ }
+
+ fsr_bits = data->chip_info->resolution_bits - measurement->bipolar;
+
+ ads112c14_populate_scale_available(measurement->scale_available,
+ full_scale, fsr_bits);
+ }
+
/* For now, assuming all sys_mon channels are using 2.5V reference. */
full_scale = ADS112C14_INT_REF1_mV;
fsr_bits = data->chip_info->resolution_bits - 1;
@@ -567,6 +1004,9 @@ static int ads112c14_probe(struct i2c_client *client)
const struct ads112c14_chip_info *info;
struct iio_dev *indio_dev;
struct ads112c14_data *data;
+ bool need_avdd_ref, need_ext_ref;
+ u32 refp_uV = 0;
+ u32 refn_uV = 0;
u32 reg_val;
int ret;
@@ -581,13 +1021,76 @@ static int ads112c14_probe(struct i2c_client *client)
data = iio_priv(indio_dev);
data->chip_info = info;
+ if (device_property_present(dev, "ti,refp-refn-resistor-ohms")) {
+ ret = device_property_read_u32(dev, "ti,refp-refn-resistor-ohms",
+ &data->ext_ref_ohms);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read ti,refp-refn-resistor-ohms property\n");
+ }
+
+ ret = ads112c14_parse_channels(indio_dev, &need_avdd_ref, &need_ext_ref);
+ if (ret)
+ return ret;
+
ret = devm_regulator_get_enable(dev, "dvdd");
if (ret)
return dev_err_probe(dev, ret, "failed to get dvdd regulator\n");
- ret = devm_regulator_get_enable(dev, "avdd");
- if (ret)
- return dev_err_probe(dev, ret, "failed to get avdd regulator\n");
+ if (need_avdd_ref) {
+ ret = devm_regulator_get_enable_read_voltage(dev, "avdd");
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to get avdd voltage\n");
+
+ data->avdd_uV = ret;
+ } else {
+ ret = devm_regulator_get_enable(dev, "avdd");
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get avdd regulator\n");
+ }
+
+ if (device_property_present(dev, "refp-supply")) {
+ ret = devm_regulator_get_enable_read_voltage(dev, "refp");
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to get refp voltage\n");
+
+ refp_uV = ret;
+
+ struct fwnode_handle *refp_fwnode __free(fwnode_handle) =
+ fwnode_find_reference(dev->fwnode, "refp-supply", 0);
+ if (IS_ERR(refp_fwnode))
+ return dev_err_probe(dev, PTR_ERR(refp_fwnode),
+ "failed to get refp fwnode\n");
+
+ struct fwnode_handle *avdd_fwnode __free(fwnode_handle) =
+ fwnode_find_reference(dev->fwnode, "avdd-supply", 0);
+ if (IS_ERR(avdd_fwnode))
+ return dev_err_probe(dev, PTR_ERR(avdd_fwnode),
+ "failed to get avdd fwnode\n");
+
+ /* REFP buffer should not be enabled when connected to AVDD */
+ data->refp_is_avdd = refp_fwnode == avdd_fwnode;
+ }
+
+ if (device_property_present(dev, "refn-supply")) {
+ ret = devm_regulator_get_enable_read_voltage(dev, "refn");
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to get refn voltage\n");
+
+ refn_uV = ret;
+ } else {
+ data->refn_is_gnd = true;
+ }
+
+ data->ext_ref_uV = refp_uV - refn_uV;
+
+ if (data->ext_ref_uV && data->ext_ref_ohms)
+ return dev_err_probe(dev, -EINVAL,
+ "ti,refp-refn-resistor-ohms property should not be present when refp-supply or refn-supply is present\n");
+
+ if (need_ext_ref && !data->ext_ref_uV && !data->ext_ref_ohms)
+ return dev_err_probe(dev, -EINVAL,
+ "external reference measurements require either refp-supply or ti,refp-refn-resistor-ohms property\n");
data->regmap = devm_regmap_init_i2c(client, &ads112c14_regmap_config);
if (IS_ERR(data->regmap))
@@ -639,8 +1142,6 @@ static int ads112c14_probe(struct i2c_client *client)
indio_dev->name = info->name;
indio_dev->modes = INDIO_DIRECT_MODE;
- indio_dev->channels = ads112c14_sys_mon_channels;
- indio_dev->num_channels = ARRAY_SIZE(ads112c14_sys_mon_channels);
indio_dev->info = &ads112c14_info;
return devm_iio_device_register(dev, indio_dev);
--
2.43.0
^ permalink raw reply related
* [PATCH v2 6/8] iio: adc: add ti-ads112c14 driver
From: David Lechner (TI) @ 2026-06-25 21:55 UTC (permalink / raw)
To: Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: Chris Hall, Patrick Edwards, Kurt Borja, Nguyen Minh Tien,
linux-iio, devicetree, linux-kernel, David Lechner
In-Reply-To: <20260625-iio-adc-ti-ads122c14-v2-0-ceb9b0b561cb@baylibre.com>
Add a new driver for the TI ADS112C14/ADS122C14 ADC chips.
This first step is adding a very basic driver that only supports power
on/reset and reading the system monitor channels.
ADS112C14_SYS_MON_CHANNEL_SHORT is the last channel rather than being in
logical order by address to keep the voltage channels together and in
case we find we need to add variants of this channel with different
voltage reference later.
Signed-off-by: David Lechner (TI) <dlechner@baylibre.com>
---
A few other notes for review that didn't seem worth putting in the
commit message:
* I intentionally did not use bulk regmap because later we may need to
get the voltage of the avdd supply.
* I left some comments in the code where the code might look funny (e.g.
to reduce future diff) or does not exactly match the datasheet, in
which case later changes will address that.
v2 changes:
* Add some indent to register field macro defines.
* Drop ads112c14_internal_ref_uV array (use macros instead).
* Don't keep pointer to i2c client.
* Consistently check channel < 100 instead of >= 100 sometimes.
* Check return of i2c_get_match_data().
* Fix bug on checking -EREMOTEIO.
* Don't use &client->dev when we already have dev local variable.
* Make some changes to reduce diff in later patches.
---
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 12 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ti-ads112c14.c | 547 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 561 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 0ac9f44c7096..92746892f6b3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26940,6 +26940,7 @@ M: David Lechner <dlechner@baylibre.com>
L: linux-iio@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/iio/adc/ti,ads112c14.yaml
+F: drivers/iio/adc/ti-ads112c14.c
TI ADS1018 ADC DRIVER
M: Kurt Borja <kuurtb@gmail.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 6fb0766ca27a..bfcaecc85f24 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -1787,6 +1787,18 @@ config TI_ADS1119
This driver can also be built as a module. If so, the module will be
called ti-ads1119.
+config TI_ADS112C14
+ tristate "Texas Instruments ADS112C14/ADS122C14"
+ depends on I2C
+ select REGMAP
+ select REGMAP_I2C
+ help
+ If you say yes here you get support for Texas Instruments ADS112C14,
+ ADS122C14 ADC chips.
+
+ This driver can also be built as a module. If so, the module will be
+ called ti-ads112c14.
+
config TI_ADS124S08
tristate "Texas Instruments ADS124S08"
depends on SPI
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index 707dd708912f..72f4c6b33ca8 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -153,6 +153,7 @@ obj-$(CONFIG_TI_ADS1015) += ti-ads1015.o
obj-$(CONFIG_TI_ADS1018) += ti-ads1018.o
obj-$(CONFIG_TI_ADS1100) += ti-ads1100.o
obj-$(CONFIG_TI_ADS1119) += ti-ads1119.o
+obj-$(CONFIG_TI_ADS112C14) += ti-ads112c14.o
obj-$(CONFIG_TI_ADS124S08) += ti-ads124s08.o
obj-$(CONFIG_TI_ADS1298) += ti-ads1298.o
obj-$(CONFIG_TI_ADS131E08) += ti-ads131e08.o
diff --git a/drivers/iio/adc/ti-ads112c14.c b/drivers/iio/adc/ti-ads112c14.c
new file mode 100644
index 000000000000..c61d47244732
--- /dev/null
+++ b/drivers/iio/adc/ti-ads112c14.c
@@ -0,0 +1,547 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * IIO driver for Texas Instruments ADS112C14 and similar ADCs.
+ *
+ * Copyright (C) 2026 Texas Instruments Incorporated - https://www.ti.com/
+ * Copyright (C) 2026 Baylibre Inc.
+ *
+ * Datasheet: https://www.ti.com/lit/ds/symlink/ads122c14.pdf
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/math64.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/sysfs.h>
+#include <linux/time64.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/units.h>
+
+/* Datasheet t_d(RST) - time to wait after reset before next I2C use. */
+#define ADS112C14_DELAY_RESET_US 500
+
+#define ADS112C14_CMD_RDATA 0x00
+#define ADS112C14_CMD_RREG 0x40
+#define ADS112C14_CMD_WREG 0x80
+
+#define ADS112C14_REG_DEVICE_ID 0x00
+#define ADS112C14_DEVICE_ID_BITS GENMASK(3, 0)
+
+#define ADS112C14_REG_REVISION_ID 0x01
+
+#define ADS112C14_REG_STATUS_MSB 0x02
+#define ADS112C14_STATUS_MSB_RESETN BIT(7)
+#define ADS112C14_STATUS_MSB_AVDD_UVN BIT(6)
+#define ADS112C14_STATUS_MSB_REF_UVN BIT(5)
+#define ADS112C14_STATUS_MSB_REG_MAP_CRC_FAULTN BIT(3)
+#define ADS112C14_STATUS_MSB_MEM_FAULTN BIT(2)
+#define ADS112C14_STATUS_MSB_REG_WRITE_FAULTN BIT(1)
+#define ADS112C14_STATUS_MSB_DRDY BIT(0)
+
+#define ADS112C14_REG_STATUS_LSB 0x03
+#define ADS112C14_STATUS_LSB_CONV_COUNT GENMASK(7, 4)
+#define ADS112C14_STATUS_LSB_GPIO3_DAT_IN BIT(3)
+#define ADS112C14_STATUS_LSB_GPIO2_DAT_IN BIT(2)
+#define ADS112C14_STATUS_LSB_GPIO1_DAT_IN BIT(1)
+#define ADS112C14_STATUS_LSB_GPIO0_DAT_IN BIT(0)
+
+#define ADS112C14_REG_CONVERSION_CTRL 0x04
+#define ADS112C14_CONVERSION_CTRL_RESET GENMASK(7, 2)
+#define ADS112C14_CONVERSION_CTRL_START BIT(1)
+#define ADS112C14_CONVERSION_CTRL_STOP BIT(0)
+
+#define ADS112C14_REG_DEVICE_CFG 0x05
+#define ADS112C14_DEVICE_CFG_PWDN BIT(7)
+#define ADS112C14_DEVICE_CFG_STBY_MODE BIT(6)
+#define ADS112C14_DEVICE_CFG_BOCS GENMASK(5, 4)
+#define ADS112C14_DEVICE_CFG_CLK_SEL BIT(3)
+#define ADS112C14_DEVICE_CFG_CONV_MODE BIT(2)
+#define ADS112C14_DEVICE_CFG_SPEED_MODE GENMASK(1, 0)
+
+#define ADS112C14_REG_DATA_RATE_CFG 0x06
+#define ADS112C14_DATA_RATE_CFG_DELAY GENMASK(7, 4)
+#define ADS112C14_DATA_RATE_CFG_GC_EN BIT(3)
+#define ADS112C14_DATA_RATE_CFG_FLTR_OSR GENMASK(2, 0)
+
+#define ADS112C14_REG_MUX_CFG 0x07
+#define ADS112C14_MUX_CFG_AINP GENMASK(7, 4)
+#define ADS112C14_MUX_CFG_AINN GENMASK(3, 0)
+
+#define ADS112C14_REG_GAIN_CFG 0x08
+#define ADS112C14_GAIN_CFG_SPARE BIT(7)
+#define ADS112C14_GAIN_CFG_SYS_MON GENMASK(6, 4)
+#define ADS112C14_GAIN_CFG_GAIN GENMASK(3, 0)
+
+#define ADS112C14_REG_REFERENCE_CFG 0x09
+#define ADS112C14_REFERENCE_CFG_REF_UV_EN BIT(7)
+#define ADS112C14_REFERENCE_CFG_REFP_BUF_EN BIT(5)
+#define ADS112C14_REFERENCE_CFG_REFN_BUF_EN BIT(4)
+#define ADS112C14_REFERENCE_CFG_REF_VAL BIT(2)
+#define ADS112C14_REFERENCE_CFG_REF_VAL_1_25V 0
+#define ADS112C14_REFERENCE_CFG_REF_VAL_2_5V 1
+#define ADS112C14_REFERENCE_CFG_REF_SEL GENMASK(1, 0)
+
+#define ADS112C14_REG_DIGITAL_CFG 0x0A
+#define ADS112C14_DIGITAL_CFG_REG_MAP_CRC_EN BIT(6)
+#define ADS112C14_DIGITAL_CFG_I2C_CRC_EN BIT(5)
+#define ADS112C14_DIGITAL_CFG_STATUS_EN BIT(4)
+#define ADS112C14_DIGITAL_CFG_FAULT_PIN_BEHAVIOR BIT(3)
+#define ADS112C14_DIGITAL_CFG_CODING BIT(1)
+
+#define ADS112C14_REG_GPIO_CFG 0x0B
+#define ADS112C14_GPIO_CFG_GPIO3_CFG GENMASK(7, 6)
+#define ADS112C14_GPIO_CFG_GPIO2_CFG GENMASK(5, 4)
+#define ADS112C14_GPIO_CFG_GPIO1_CFG GENMASK(3, 2)
+#define ADS112C14_GPIO_CFG_GPIO0_CFG GENMASK(1, 0)
+
+#define ADS112C14_REG_GPIO_DATA_OUTPUT 0x0C
+#define ADS112C14_GPIO_DATA_OUTPUT_GPIO3_SRC BIT(7)
+#define ADS112C14_GPIO_DATA_OUTPUT_GPIO2_SRC BIT(6)
+#define ADS112C14_GPIO_DATA_OUTPUT_GPIO3_DAT_OUT BIT(3)
+#define ADS112C14_GPIO_DATA_OUTPUT_GPIO2_DAT_OUT BIT(2)
+#define ADS112C14_GPIO_DATA_OUTPUT_GPIO1_DAT_OUT BIT(1)
+#define ADS112C14_GPIO_DATA_OUTPUT_GPIO0_DAT_OUT BIT(0)
+
+#define ADS112C14_REG_IDAC_MAG_CFG 0x0D
+#define ADS112C14_IDAC_MAG_CFG_I2MAG GENMASK(7, 4)
+#define ADS112C14_IDAC_MAG_CFG_I1MAG GENMASK(3, 0)
+
+#define ADS112C14_REG_IDAC_MUX_CFG 0x0E
+#define ADS112C14_IDAC_MUX_CFG_IUNIT BIT(7)
+#define ADS112C14_IDAC_MUX_CFG_I2MUX GENMASK(6, 4)
+#define ADS112C14_IDAC_MUX_CFG_I1MUX GENMASK(2, 0)
+
+#define ADS112C14_REG_REG_MAP_CRC 0x0F
+
+#define ADS112C14_INT_REF0_mV 1250
+#define ADS112C14_INT_REF1_mV 2500
+
+static bool ads112c14_writeable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case ADS112C14_REG_DEVICE_ID:
+ case ADS112C14_REG_REVISION_ID:
+ case ADS112C14_REG_STATUS_LSB:
+ return false;
+ default:
+ return true;
+ }
+}
+
+static bool ads112c14_volatile_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case ADS112C14_REG_STATUS_MSB:
+ case ADS112C14_REG_STATUS_LSB:
+ case ADS112C14_REG_CONVERSION_CTRL:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct reg_default ads112c14_reg_defaults[] = {
+ { ADS112C14_REG_DEVICE_CFG, 0 },
+ { ADS112C14_REG_DATA_RATE_CFG, 0 },
+ { ADS112C14_REG_MUX_CFG, 0 },
+ { ADS112C14_REG_GAIN_CFG, FIELD_PREP_CONST(ADS112C14_GAIN_CFG_GAIN, 1) },
+ { ADS112C14_REG_REFERENCE_CFG, 0 },
+ { ADS112C14_REG_DIGITAL_CFG, 0 },
+ { ADS112C14_REG_GPIO_CFG, 0 },
+ { ADS112C14_REG_GPIO_DATA_OUTPUT, 0 },
+ { ADS112C14_REG_IDAC_MAG_CFG, 0 },
+ { ADS112C14_REG_IDAC_MUX_CFG, FIELD_PREP_CONST(ADS112C14_IDAC_MUX_CFG_I2MUX, 1) },
+};
+
+static const struct regmap_config ads112c14_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .read_flag_mask = ADS112C14_CMD_RREG,
+ .write_flag_mask = ADS112C14_CMD_WREG,
+ .max_register = ADS112C14_REG_REG_MAP_CRC,
+ .writeable_reg = ads112c14_writeable_reg,
+ .volatile_reg = ads112c14_volatile_reg,
+ .reg_defaults = ads112c14_reg_defaults,
+ .num_reg_defaults = ARRAY_SIZE(ads112c14_reg_defaults),
+ .cache_type = REGCACHE_MAPLE,
+};
+
+struct ads112c14_chip_info {
+ const char *name;
+ u32 resolution_bits;
+};
+
+struct ads112c14_data {
+ const struct ads112c14_chip_info *chip_info;
+ struct regmap *regmap;
+};
+
+/* Fixed channels for system monitor measurements. */
+#define ADS112C14_SYS_MON_CHANNEL_TEMP 100
+#define ADS112C14_SYS_MON_CHANNEL_EXT_REF 101
+#define ADS112C14_SYS_MON_CHANNEL_AVDD 102
+#define ADS112C14_SYS_MON_CHANNEL_DVDD 103
+#define ADS112C14_SYS_MON_CHANNEL_SHORT 104
+
+static const struct iio_chan_spec ads112c14_sys_mon_channels[] = {
+ {
+ .type = IIO_TEMP,
+ .indexed = 1,
+ .channel = ADS112C14_SYS_MON_CHANNEL_TEMP,
+ .address = 2,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
+ | BIT(IIO_CHAN_INFO_SCALE)
+ | BIT(IIO_CHAN_INFO_OFFSET),
+ },
+ {
+ .type = IIO_VOLTAGE,
+ .indexed = 1,
+ .channel = ADS112C14_SYS_MON_CHANNEL_EXT_REF,
+ .address = 3,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
+ | BIT(IIO_CHAN_INFO_SCALE),
+ },
+ {
+ .type = IIO_VOLTAGE,
+ .indexed = 1,
+ .channel = ADS112C14_SYS_MON_CHANNEL_AVDD,
+ .address = 4,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
+ | BIT(IIO_CHAN_INFO_SCALE),
+ },
+ {
+ .type = IIO_VOLTAGE,
+ .indexed = 1,
+ .channel = ADS112C14_SYS_MON_CHANNEL_DVDD,
+ .address = 5,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
+ | BIT(IIO_CHAN_INFO_SCALE),
+ },
+ {
+ .type = IIO_VOLTAGE,
+ .indexed = 1,
+ .channel = ADS112C14_SYS_MON_CHANNEL_SHORT,
+ .channel2 = ADS112C14_SYS_MON_CHANNEL_SHORT,
+ .differential = 1,
+ .address = 1,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW)
+ | BIT(IIO_CHAN_INFO_SCALE),
+ },
+};
+
+static int ads112c14_prepare_sys_mon_channel(struct ads112c14_data *data,
+ const struct iio_chan_spec *chan)
+{
+ int ret;
+
+ /* TODO: GAIN is useful for shorted PGA inputs. */
+ /* All SYS_MON channels use GAIN of 1 to keep it simple. */
+ ret = regmap_update_bits(data->regmap, ADS112C14_REG_GAIN_CFG,
+ ADS112C14_GAIN_CFG_SYS_MON |
+ ADS112C14_GAIN_CFG_GAIN,
+ FIELD_PREP(ADS112C14_GAIN_CFG_SYS_MON, chan->address) |
+ FIELD_PREP(ADS112C14_GAIN_CFG_GAIN, 1));
+ if (ret)
+ return ret;
+
+ /* All SYS_MON channels use signed data to keep it simple. */
+ ret = regmap_clear_bits(data->regmap, ADS112C14_REG_DIGITAL_CFG,
+ ADS112C14_DIGITAL_CFG_CODING);
+ if (ret)
+ return ret;
+
+ /*
+ * REVISIT: if we implement regulator support for the REFOUT pin, we
+ * might need to make this voltage match what is required by that. In
+ * that case, we could also adjust GAIN so that we still get the same
+ * range.
+ */
+ /*
+ * NB: SYS_MON channels ignore REF_SEL except for the shorted input
+ * channel, so we set it here to internal reference to be consistent.
+ * If we ever need to make a measurement of shorted input with other
+ * reference source, we could add additional channels for that.
+ */
+ ret = regmap_update_bits(data->regmap, ADS112C14_REG_REFERENCE_CFG,
+ ADS112C14_REFERENCE_CFG_REF_VAL |
+ ADS112C14_REFERENCE_CFG_REF_SEL,
+ FIELD_PREP(ADS112C14_REFERENCE_CFG_REF_VAL,
+ ADS112C14_REFERENCE_CFG_REF_VAL_2_5V) |
+ FIELD_PREP(ADS112C14_REFERENCE_CFG_REF_SEL,
+ ADS112C14_REFERENCE_CFG_REF_SEL_INTERNAL));
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int ads112c14_single_conversion(struct ads112c14_data *data,
+ const struct iio_chan_spec *chan,
+ u8 *buf)
+{
+ struct i2c_client *client = to_i2c_client(regmap_get_device(data->regmap));
+ u32 reg_val;
+ int ret;
+
+ if (chan->channel < 100) {
+ /* Not implemented yet. */
+ return -EINVAL;
+ } else {
+ ret = ads112c14_prepare_sys_mon_channel(data, chan);
+ if (ret)
+ return ret;
+ }
+
+ ret = regmap_write(data->regmap, ADS112C14_REG_CONVERSION_CTRL,
+ ADS112C14_CONVERSION_CTRL_START);
+ if (ret)
+ return ret;
+
+ ret = regmap_read_poll_timeout(data->regmap,
+ ADS112C14_REG_STATUS_MSB, reg_val,
+ FIELD_GET(ADS112C14_STATUS_MSB_DRDY, reg_val),
+ 1 * USEC_PER_MSEC, 100 * USEC_PER_MSEC);
+ if (ret)
+ return ret;
+
+ return i2c_smbus_read_i2c_block_data(client, ADS112C14_CMD_RDATA,
+ BITS_TO_BYTES(data->chip_info->resolution_bits),
+ buf);
+}
+
+static int ads112c14_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2, long mask)
+{
+ struct ads112c14_data *data = iio_priv(indio_dev);
+ u32 vref_uV, fsr_bits;
+
+ /* Selecting V_REF source is not implemented yet. */
+ vref_uV = ADS112C14_INT_REF1_mV * (MICRO / MILLI);
+
+ if (chan->channel < 100) {
+ /* Not implemented yet. */
+ return -EINVAL;
+ } else {
+ /* All SYS_MON channels are using signed coding. */
+ fsr_bits = data->chip_info->resolution_bits - 1;
+ }
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW: {
+ u8 buf[3];
+ int ret;
+
+ if (!iio_device_claim_direct(indio_dev))
+ return -EBUSY;
+
+ ret = ads112c14_single_conversion(data, chan, buf);
+ iio_device_release_direct(indio_dev);
+ if (ret < 0)
+ return ret;
+
+ switch (data->chip_info->resolution_bits) {
+ case 16:
+ *val = get_unaligned_be16(buf);
+ break;
+ case 24:
+ *val = get_unaligned_be24(buf);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ *val = sign_extend32(*val, fsr_bits);
+
+ return IIO_VAL_INT;
+ }
+ case IIO_CHAN_INFO_SCALE:
+ if (chan->type == IIO_TEMP) {
+ /* TS_TC (typical) = 405 uV/°C */
+ *val = MILLI * vref_uV / 405;
+ *val2 = fsr_bits;
+ return IIO_VAL_FRACTIONAL_LOG2;
+ }
+
+ *val = vref_uV / (MICRO / MILLI);
+ /*
+ * Last 3 SYS_MON channels (ext ref, AVDD, DVDD) need to be
+ * multiplied by 8 to account for internal attenuation of / 8.
+ */
+ *val2 = fsr_bits - (chan->address >= 3 ? 3 : 0);
+ return IIO_VAL_FRACTIONAL_LOG2;
+ case IIO_CHAN_INFO_OFFSET:
+ /* Only the temperature channel has an offset. */
+ if (chan->type != IIO_TEMP)
+ return -EINVAL;
+ /*
+ * Die temperature [°C] = 25°C + (Measured voltage – TS_Offset) / TS_TC
+ * TS_TC (typical) = 405 uV/°C
+ * TS_Offset (typical) = 119.5 mV
+ */
+ *val = div_s64((s64)(25 * 405 - 119500) * BIT(fsr_bits), vref_uV);
+ return IIO_VAL_INT;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ads112c14_read_label(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, char *label)
+{
+ const char *label_source;
+
+ /* System monitor channels. */
+ switch (chan->channel) {
+ case ADS112C14_SYS_MON_CHANNEL_TEMP:
+ label_source = "Internal temperature sensor";
+ break;
+ case ADS112C14_SYS_MON_CHANNEL_EXT_REF:
+ label_source = "External reference";
+ break;
+ case ADS112C14_SYS_MON_CHANNEL_AVDD:
+ label_source = "AVDD";
+ break;
+ case ADS112C14_SYS_MON_CHANNEL_DVDD:
+ label_source = "DVDD";
+ break;
+ case ADS112C14_SYS_MON_CHANNEL_SHORT:
+ label_source = "Internal short (internal reference source)";
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return sysfs_emit(label, "%s\n", label_source);
+}
+
+static const struct iio_info ads112c14_info = {
+ .read_raw = ads112c14_read_raw,
+ .read_label = ads112c14_read_label,
+};
+
+static int ads112c14_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ const struct ads112c14_chip_info *info;
+ struct iio_dev *indio_dev;
+ struct ads112c14_data *data;
+ u32 reg_val;
+ int ret;
+
+ info = i2c_get_match_data(client);
+ if (!info)
+ return dev_err_probe(dev, -EINVAL, "missing match data\n");
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ data = iio_priv(indio_dev);
+ data->chip_info = info;
+
+ ret = devm_regulator_get_enable(dev, "dvdd");
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get dvdd regulator\n");
+
+ ret = devm_regulator_get_enable(dev, "avdd");
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get avdd regulator\n");
+
+ data->regmap = devm_regmap_init_i2c(client, &ads112c14_regmap_config);
+ if (IS_ERR(data->regmap))
+ return dev_err_probe(dev, PTR_ERR(data->regmap),
+ "failed to init regmap\n");
+
+ /* Write magic reset value (0x16) to ensure known state.*/
+ ret = regmap_write(data->regmap, ADS112C14_REG_CONVERSION_CTRL,
+ FIELD_PREP(ADS112C14_CONVERSION_CTRL_RESET, 0x16));
+ /*
+ * The reset may cause an -EREMOTEIO error because of failing to get the
+ * I2C ACK at the end of the message. The device still gets reset so it
+ * is safe to ignore this error.
+ */
+ if (ret == -EREMOTEIO)
+ ret = 0;
+ if (ret)
+ return ret;
+
+ fsleep(ADS112C14_DELAY_RESET_US);
+
+ ret = regmap_read(data->regmap, ADS112C14_REG_STATUS_MSB, ®_val);
+ if (ret)
+ return ret;
+
+ if (FIELD_GET(ADS112C14_STATUS_MSB_RESETN, reg_val))
+ return dev_err_probe(dev, -EIO, "reset failed\n");
+
+ /*
+ * Clear reset bit to prepare for next probe. And clear AVDD fault since
+ * that happens on every reset.
+ */
+ ret = regmap_write(data->regmap, ADS112C14_REG_STATUS_MSB,
+ ADS112C14_STATUS_MSB_RESETN |
+ ADS112C14_STATUS_MSB_AVDD_UVN);
+ if (ret)
+ return ret;
+
+ /* Place in single-shot conversion mode to make ready for raw read. */
+ ret = regmap_set_bits(data->regmap, ADS112C14_REG_DEVICE_CFG,
+ ADS112C14_DEVICE_CFG_CONV_MODE);
+ if (ret)
+ return ret;
+
+ indio_dev->name = info->name;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+ indio_dev->channels = ads112c14_sys_mon_channels;
+ indio_dev->num_channels = ARRAY_SIZE(ads112c14_sys_mon_channels);
+ indio_dev->info = &ads112c14_info;
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct ads112c14_chip_info ads112c14_chip_info = {
+ .name = "ads112c14",
+ .resolution_bits = 16,
+};
+
+static const struct ads112c14_chip_info ads122c14_chip_info = {
+ .name = "ads122c14",
+ .resolution_bits = 24,
+};
+
+static const struct of_device_id ads112c14_of_match[] = {
+ { .compatible = "ti,ads112c14", .data = &ads112c14_chip_info },
+ { .compatible = "ti,ads122c14", .data = &ads122c14_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ads112c14_of_match);
+
+static const struct i2c_device_id ads112c14_id[] = {
+ { .name = "ads112c14", .driver_data = (kernel_ulong_t)&ads112c14_chip_info },
+ { .name = "ads122c14", .driver_data = (kernel_ulong_t)&ads122c14_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, ads112c14_id);
+
+static struct i2c_driver ads112c14_driver = {
+ .driver = {
+ .name = "ads112c14",
+ .of_match_table = ads112c14_of_match,
+ },
+ .probe = ads112c14_probe,
+ .id_table = ads112c14_id,
+};
+module_i2c_driver(ads112c14_driver);
+
+MODULE_AUTHOR("David Lechner (TI) <dlechner@baylibre.com>");
+MODULE_DESCRIPTION("TI ADS112C14 I2C ADC driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox