* [PATCH 5/7] soc: renesas: Identify Renesas R-Car R8A779MD M3Le SoC
From: Marek Vasut @ 2026-04-19 19:35 UTC (permalink / raw)
To: linux-arm-kernel
Cc: Marek Vasut, Conor Dooley, David Airlie, Geert Uytterhoeven,
Kieran Bingham, Krzysztof Kozlowski, Kuninori Morimoto,
Laurent Pinchart, Magnus Damm, Maxime Ripard, Michael Turquette,
Rob Herring, Simona Vetter, Stephen Boyd, Thomas Zimmermann,
Tomi Valkeinen, devicetree, dri-devel, linux-clk, linux-kernel,
linux-renesas-soc
In-Reply-To: <20260419193718.133174-1-marek.vasut+renesas@mailbox.org>
Add support for identifying the R-Car M3Le (R8A779MD) SoC.
The Renesas R-Car R8A779MD M3Le SoC is a variant of the
already supported R-Car M3-N SoC with reduced peripherals.
Enable support for the M3Le SoC through already existing
ARCH_R8A77965 configuration symbol. PRR reads 0x67c05501 .
Signed-off-by: Marek Vasut <marek.vasut+renesas@mailbox.org>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: David Airlie <airlied@gmail.com>
Cc: Geert Uytterhoeven <geert+renesas@glider.be>
Cc: Kieran Bingham <kieran.bingham+renesas@ideasonboard.com>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>
Cc: Laurent Pinchart <laurent.pinchart+renesas@ideasonboard.com>
Cc: Magnus Damm <magnus.damm@gmail.com>
Cc: Maxime Ripard <mripard@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Rob Herring <robh@kernel.org>
Cc: Simona Vetter <simona@ffwll.ch>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: Thomas Zimmermann <tzimmermann@suse.de>
Cc: Tomi Valkeinen <tomi.valkeinen+renesas@ideasonboard.com>
Cc: devicetree@vger.kernel.org
Cc: dri-devel@lists.freedesktop.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-renesas-soc@vger.kernel.org
---
drivers/soc/renesas/renesas-soc.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/soc/renesas/renesas-soc.c b/drivers/soc/renesas/renesas-soc.c
index 38ff0b823bdaf..c82835cf6d8e9 100644
--- a/drivers/soc/renesas/renesas-soc.c
+++ b/drivers/soc/renesas/renesas-soc.c
@@ -361,6 +361,7 @@ static const struct of_device_id renesas_socs[] __initconst __maybe_unused = {
{ .compatible = "renesas,r8a77965", .data = &soc_rcar_m3_n },
{ .compatible = "renesas,r8a779m4", .data = &soc_rcar_m3_n },
{ .compatible = "renesas,r8a779m5", .data = &soc_rcar_m3_n },
+ { .compatible = "renesas,r8a779md", .data = &soc_rcar_m3_n },
#endif
#ifdef CONFIG_ARCH_R8A77970
{ .compatible = "renesas,r8a77970", .data = &soc_rcar_v3m },
--
2.53.0
^ permalink raw reply related
* [PATCH 6/7] arm64: dts: renesas: r8a779md: Add Renesas R-Car R8A779MD M3Le DTs
From: Marek Vasut @ 2026-04-19 19:35 UTC (permalink / raw)
To: linux-arm-kernel
Cc: Nguyen Tran, Marek Vasut, Conor Dooley, David Airlie,
Geert Uytterhoeven, Kieran Bingham, Krzysztof Kozlowski,
Kuninori Morimoto, Laurent Pinchart, Magnus Damm, Maxime Ripard,
Michael Turquette, Rob Herring, Simona Vetter, Stephen Boyd,
Thomas Zimmermann, Tomi Valkeinen, devicetree, dri-devel,
linux-clk, linux-kernel, linux-renesas-soc
In-Reply-To: <20260419193718.133174-1-marek.vasut+renesas@mailbox.org>
From: Nguyen Tran <nguyen.tran.pz@bp.renesas.com>
Add support for the Renesas R-Car M3Le (R8A779MD) SoC, a variant of the
R-Car M3-N (R8A77965) SoC. The Renesas M3Le SoC is a register-compatible
variant of the R8A77965 (M3-N) with reduced set of peripherals.
Signed-off-by: Nguyen Tran <nguyen.tran.pz@bp.renesas.com>
Signed-off-by: Marek Vasut <marek.vasut+renesas@mailbox.org>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: David Airlie <airlied@gmail.com>
Cc: Geert Uytterhoeven <geert+renesas@glider.be>
Cc: Kieran Bingham <kieran.bingham+renesas@ideasonboard.com>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>
Cc: Laurent Pinchart <laurent.pinchart+renesas@ideasonboard.com>
Cc: Magnus Damm <magnus.damm@gmail.com>
Cc: Maxime Ripard <mripard@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Rob Herring <robh@kernel.org>
Cc: Simona Vetter <simona@ffwll.ch>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: Thomas Zimmermann <tzimmermann@suse.de>
Cc: Tomi Valkeinen <tomi.valkeinen+renesas@ideasonboard.com>
Cc: devicetree@vger.kernel.org
Cc: dri-devel@lists.freedesktop.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-renesas-soc@vger.kernel.org
---
arch/arm64/boot/dts/renesas/r8a779md.dtsi | 48 +++++++++++++++++++++++
1 file changed, 48 insertions(+)
create mode 100644 arch/arm64/boot/dts/renesas/r8a779md.dtsi
diff --git a/arch/arm64/boot/dts/renesas/r8a779md.dtsi b/arch/arm64/boot/dts/renesas/r8a779md.dtsi
new file mode 100644
index 0000000000000..7e0f5fe4cd439
--- /dev/null
+++ b/arch/arm64/boot/dts/renesas/r8a779md.dtsi
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+/*
+ * Device Tree Source for the R-Car M3Le (R8A779MD) SoC
+ *
+ * Copyright (C) 2025-2026 Renesas Electronics Corp.
+ */
+
+#include "r8a77965.dtsi"
+
+/ {
+ compatible = "renesas,r8a779md", "renesas,r8a77965";
+};
+
+/delete-node/ &csi20;
+/delete-node/ &drif00;
+/delete-node/ &drif01;
+/delete-node/ &drif10;
+/delete-node/ &drif11;
+/delete-node/ &drif20;
+/delete-node/ &drif21;
+/delete-node/ &drif30;
+/delete-node/ &drif31;
+/delete-node/ &hdmi0;
+/delete-node/ &mlp;
+/delete-node/ &pciec1;
+/delete-node/ &sata;
+/delete-node/ &sdhi3;
+/delete-node/ &usb3_peri0;
+/delete-node/ &usb3_phy0;
+/delete-node/ &vin0csi20;
+/delete-node/ &vin1csi20;
+/delete-node/ &vin2csi20;
+/delete-node/ &vin3csi20;
+/delete-node/ &vin4csi20;
+/delete-node/ &vin5csi20;
+/delete-node/ &vin6csi20;
+/delete-node/ &vin7csi20;
+/delete-node/ &xhci0;
+
+&du {
+ compatible = "renesas,du-r8a779md";
+ renesas,cmms = <&cmm0>, <&cmm3>;
+ renesas,vsps = <&vspd0 0>, <&vspd1 0>;
+
+ ports {
+ /delete-node/ port@1;
+ };
+};
--
2.53.0
^ permalink raw reply related
* [PATCH 7/7] arm64: dts: renesas: r8a779md: Add support for R-Car M3Le R8A779MD Geist
From: Marek Vasut @ 2026-04-19 19:35 UTC (permalink / raw)
To: linux-arm-kernel
Cc: Nguyen Tran, Marek Vasut, Conor Dooley, David Airlie,
Geert Uytterhoeven, Kieran Bingham, Krzysztof Kozlowski,
Kuninori Morimoto, Laurent Pinchart, Magnus Damm, Maxime Ripard,
Michael Turquette, Rob Herring, Simona Vetter, Stephen Boyd,
Thomas Zimmermann, Tomi Valkeinen, devicetree, dri-devel,
linux-clk, linux-kernel, linux-renesas-soc
In-Reply-To: <20260419193718.133174-1-marek.vasut+renesas@mailbox.org>
From: Nguyen Tran <nguyen.tran.pz@bp.renesas.com>
Add support for the Geist board based on the Renesas R-Car R8A779MD (M3Le)
SoC, a register-compatible variant of the R8A77965 (M3-N) with reduced set
of peripherals. The Geist board design references the Renesas Salvator-X/XS
boards, adapting their configuration for the R8A779MD SoC.
Signed-off-by: Nguyen Tran <nguyen.tran.pz@bp.renesas.com>
Signed-off-by: Marek Vasut <marek.vasut+renesas@mailbox.org>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: David Airlie <airlied@gmail.com>
Cc: Geert Uytterhoeven <geert+renesas@glider.be>
Cc: Kieran Bingham <kieran.bingham+renesas@ideasonboard.com>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>
Cc: Laurent Pinchart <laurent.pinchart+renesas@ideasonboard.com>
Cc: Magnus Damm <magnus.damm@gmail.com>
Cc: Maxime Ripard <mripard@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Rob Herring <robh@kernel.org>
Cc: Simona Vetter <simona@ffwll.ch>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: Thomas Zimmermann <tzimmermann@suse.de>
Cc: Tomi Valkeinen <tomi.valkeinen+renesas@ideasonboard.com>
Cc: devicetree@vger.kernel.org
Cc: dri-devel@lists.freedesktop.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-renesas-soc@vger.kernel.org
---
arch/arm64/boot/dts/renesas/Makefile | 3 +
.../dts/renesas/geist-panel-aa104xd12.dtso | 17 +
.../arm64/boot/dts/renesas/r8a779md-geist.dts | 832 ++++++++++++++++++
3 files changed, 852 insertions(+)
create mode 100644 arch/arm64/boot/dts/renesas/geist-panel-aa104xd12.dtso
create mode 100644 arch/arm64/boot/dts/renesas/r8a779md-geist.dts
diff --git a/arch/arm64/boot/dts/renesas/Makefile b/arch/arm64/boot/dts/renesas/Makefile
index ca45d2857ea7f..0b8fbc7b00c6e 100644
--- a/arch/arm64/boot/dts/renesas/Makefile
+++ b/arch/arm64/boot/dts/renesas/Makefile
@@ -60,6 +60,9 @@ r8a77965-salvator-xs-panel-aa104xd12-dtbs := r8a77965-salvator-xs.dtb salvator-p
dtb-$(CONFIG_ARCH_R8A77965) += r8a77965-salvator-xs-panel-aa104xd12.dtb
dtb-$(CONFIG_ARCH_R8A77965) += r8a77965-ulcb.dtb
dtb-$(CONFIG_ARCH_R8A77965) += r8a77965-ulcb-kf.dtb
+dtb-$(CONFIG_ARCH_R8A77965) += r8a779md-geist.dtb
+r8a779md-geist-panel-aa104xd12-dtbs := r8a779md-geist.dtb geist-panel-aa104xd12.dtbo
+dtb-$(CONFIG_ARCH_R8A77965) += r8a779md-geist-panel-aa104xd12.dtb
dtb-$(CONFIG_ARCH_R8A77970) += r8a77970-eagle.dtb
dtb-$(CONFIG_ARCH_R8A77970) += r8a77970-eagle-function-expansion.dtbo
diff --git a/arch/arm64/boot/dts/renesas/geist-panel-aa104xd12.dtso b/arch/arm64/boot/dts/renesas/geist-panel-aa104xd12.dtso
new file mode 100644
index 0000000000000..c8e39811eb051
--- /dev/null
+++ b/arch/arm64/boot/dts/renesas/geist-panel-aa104xd12.dtso
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+/*
+ * Device Tree overlay for the AA104XD12 panel connected to LVDS0 on a Geist board
+ *
+ * Copyright 2026 Marek Vasut
+ */
+
+/dts-v1/;
+/plugin/;
+
+#include <dt-bindings/gpio/gpio.h>
+
+#include "salvator-panel-aa104xd12.dtso"
+
+&{/panel} {
+ data-mapping = "jeida-24";
+};
diff --git a/arch/arm64/boot/dts/renesas/r8a779md-geist.dts b/arch/arm64/boot/dts/renesas/r8a779md-geist.dts
new file mode 100644
index 0000000000000..1a25acf638ea1
--- /dev/null
+++ b/arch/arm64/boot/dts/renesas/r8a779md-geist.dts
@@ -0,0 +1,832 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+/*
+ * Device Tree Source for the Geist board with R-Car M3Le
+ *
+ * Copyright (C) 2025-2026 Renesas Electronics Corp.
+ */
+
+/dts-v1/;
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/input.h>
+#include "r8a779md.dtsi"
+
+/ {
+ model = "Renesas Geist board based on r8a779md";
+ compatible = "renesas,geist", "renesas,r8a779md", "renesas,r8a77965";
+
+ aliases {
+ serial0 = &scif2;
+ serial1 = &hscif1;
+ ethernet0 = &avb;
+ mmc0 = &sdhi2;
+ mmc1 = &sdhi0;
+ };
+
+ chosen {
+ bootargs = "ignore_loglevel rw root=/dev/nfs ip=on";
+ stdout-path = "serial0:115200n8";
+ };
+
+ memory@48000000 {
+ device_type = "memory";
+ /* first 128MB is reserved for secure area. */
+ reg = <0x0 0x48000000 0x0 0x78000000>;
+ };
+
+ reserved-memory {
+ #address-cells = <2>;
+ #size-cells = <2>;
+ ranges;
+
+ /* Device specific region for Lossy Decompression */
+ lossy_decompress: linux,lossy_decompress@54000000 {
+ no-map;
+ reg = <0x00000000 0x54000000 0x0 0x03000000>;
+ };
+ };
+
+ audio_clkout: audio-clkout {
+ /*
+ * FIXME
+ * This is same as <&rcar_sound 0>
+ * but needed to avoid cs2500/rcar_sound probe dead-lock
+ */
+ compatible = "fixed-clock";
+ #clock-cells = <0>;
+ clock-frequency = <12288000>;
+ };
+
+ backlight: backlight {
+ compatible = "pwm-backlight";
+ pwms = <&pwm1 0 50000>;
+
+ brightness-levels = <256 128 64 16 8 4 0>;
+ default-brightness-level = <6>;
+
+ power-supply = <®_12v>;
+ enable-gpios = <&gpio6 7 GPIO_ACTIVE_HIGH>;
+ };
+
+ cvbs-in {
+ compatible = "composite-video-connector";
+ label = "CVBS IN";
+
+ port {
+ cvbs_con: endpoint {
+ remote-endpoint = <&adv7482_ain7>;
+ };
+ };
+ };
+
+ hdmi-in {
+ compatible = "hdmi-connector";
+ label = "HDMI IN";
+ type = "a";
+
+ port {
+ hdmi_in_con: endpoint {
+ remote-endpoint = <&adv7482_hdmi>;
+ };
+ };
+ };
+
+ keys {
+ compatible = "gpio-keys";
+
+ pinctrl-0 = <&keys_pins>;
+ pinctrl-names = "default";
+
+ key-1 {
+ gpios = <&gpio5 17 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_1>;
+ label = "SW4-1";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+
+ key-2 {
+ gpios = <&gpio5 20 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_2>;
+ label = "SW4-2";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+
+ key-3 {
+ gpios = <&gpio5 22 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_3>;
+ label = "SW4-3";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+
+ key-4 {
+ gpios = <&gpio5 23 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_4>;
+ label = "SW4-4";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+
+ key-a {
+ gpios = <&gpio6 11 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_A>;
+ label = "TSW0";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+
+ key-b {
+ gpios = <&gpio6 12 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_B>;
+ label = "TSW1";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+
+ key-c {
+ gpios = <&gpio6 13 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_C>;
+ label = "TSW2";
+ wakeup-source;
+ debounce-interval = <20>;
+ };
+ };
+
+ reg_1p8v: regulator0 {
+ compatible = "regulator-fixed";
+ regulator-name = "fixed-1.8V";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ regulator-boot-on;
+ regulator-always-on;
+ };
+
+ reg_3p3v: regulator1 {
+ compatible = "regulator-fixed";
+ regulator-name = "fixed-3.3V";
+ regulator-min-microvolt = <3300000>;
+ regulator-max-microvolt = <3300000>;
+ regulator-boot-on;
+ regulator-always-on;
+ };
+
+ reg_12v: regulator2 {
+ compatible = "regulator-fixed";
+ regulator-name = "fixed-12V";
+ regulator-min-microvolt = <12000000>;
+ regulator-max-microvolt = <12000000>;
+ regulator-boot-on;
+ regulator-always-on;
+ };
+
+ sound_card: sound {
+ compatible = "audio-graph-card";
+
+ label = "rcar-sound";
+ dais = <&rsnd_port0>; /* AK4619 Audio Codec */
+ };
+
+ vbus0_usb2: regulator-vbus0-usb2 {
+ compatible = "regulator-fixed";
+
+ regulator-name = "USB20_VBUS0";
+ regulator-min-microvolt = <5000000>;
+ regulator-max-microvolt = <5000000>;
+
+ gpio = <&gpio6 16 GPIO_ACTIVE_HIGH>;
+ enable-active-high;
+ };
+
+ vcc_sdhi0: regulator-vcc-sdhi0 {
+ compatible = "regulator-fixed";
+
+ regulator-name = "SDHI0 Vcc";
+ regulator-min-microvolt = <3300000>;
+ regulator-max-microvolt = <3300000>;
+
+ gpio = <&gpio5 2 GPIO_ACTIVE_HIGH>;
+ enable-active-high;
+ };
+
+ vccq_sdhi0: regulator-vccq-sdhi0 {
+ compatible = "regulator-gpio";
+
+ regulator-name = "SDHI0 VccQ";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <3300000>;
+
+ gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
+ gpios-states = <1>;
+ states = <3300000 1>, <1800000 0>;
+ };
+
+ vga {
+ compatible = "vga-connector";
+
+ port {
+ vga_in: endpoint {
+ remote-endpoint = <&adv7123_out>;
+ };
+ };
+ };
+
+ vga-encoder {
+ compatible = "adi,adv7123";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ adv7123_in: endpoint {
+ remote-endpoint = <&du_out_rgb>;
+ };
+ };
+ port@1 {
+ reg = <1>;
+ adv7123_out: endpoint {
+ remote-endpoint = <&vga_in>;
+ };
+ };
+ };
+ };
+
+ x12_clk: x12 {
+ compatible = "fixed-clock";
+ #clock-cells = <0>;
+ clock-frequency = <24576000>;
+ };
+
+ /* External DU dot clocks */
+ x21_clk: x21-clock {
+ compatible = "fixed-clock";
+ #clock-cells = <0>;
+ clock-frequency = <33000000>;
+ };
+
+ x22_clk: x22-clock {
+ compatible = "fixed-clock";
+ #clock-cells = <0>;
+ clock-frequency = <33000000>;
+ };
+
+ x23_clk: x23-clock {
+ compatible = "fixed-clock";
+ #clock-cells = <0>;
+ clock-frequency = <25000000>;
+ };
+
+ x3013_clk: x3013-clock {
+ compatible = "fixed-clock";
+ #clock-cells = <0>;
+ clock-frequency = <25000000>;
+ };
+};
+
+&audio_clk_a {
+ clock-frequency = <22579200>;
+};
+
+&avb {
+ pinctrl-0 = <&avb_pins>;
+ pinctrl-names = "default";
+ phy-handle = <&phy0>;
+ tx-internal-delay-ps = <2000>;
+ status = "okay";
+
+ phy0: ethernet-phy@0 {
+ rxc-skew-ps = <1500>;
+ reg = <0>;
+ interrupt-parent = <&gpio2>;
+ interrupts = <11 IRQ_TYPE_LEVEL_LOW>;
+ reset-gpios = <&gpio2 10 GPIO_ACTIVE_LOW>;
+ reset-assert-us = <100>;
+ reset-deassert-us = <100>;
+ };
+};
+
+&csi40 {
+ status = "okay";
+
+ ports {
+ port@0 {
+ csi40_in: endpoint {
+ clock-lanes = <0>;
+ data-lanes = <1 2 3 4>;
+ remote-endpoint = <&adv7482_txa>;
+ };
+ };
+ };
+};
+
+&du {
+ pinctrl-0 = <&du_pins>;
+ pinctrl-names = "default";
+ clocks = <&cpg CPG_MOD 724>,
+ <&cpg CPG_MOD 723>,
+ <&cpg CPG_MOD 721>,
+ <&versaclock5 1>,
+ <&x21_clk>,
+ <&versaclock5 2>;
+ clock-names = "du.0", "du.1", "du.3",
+ "dclkin.0", "dclkin.1", "dclkin.3";
+ status = "okay";
+
+ ports {
+ port@0 {
+ du_out_rgb: endpoint {
+ remote-endpoint = <&adv7123_in>;
+ };
+ };
+ };
+};
+
+&ehci0 {
+ dr_mode = "otg";
+ status = "okay";
+};
+
+&ehci1 {
+ status = "okay";
+};
+
+&extalr_clk {
+ clock-frequency = <32768>;
+};
+
+&extal_clk {
+ clock-frequency = <16666666>;
+};
+
+&hscif1 {
+ pinctrl-0 = <&hscif1_pins>;
+ pinctrl-names = "default";
+
+ uart-has-rtscts;
+ /* Please only enable hscif1 or scif1 */
+ status = "okay";
+};
+
+&hsusb {
+ dr_mode = "otg";
+ status = "okay";
+};
+
+&i2c2 {
+ pinctrl-0 = <&i2c2_pins>;
+ pinctrl-names = "default";
+ clock-frequency = <100000>;
+ status = "okay";
+
+ ak4619: codec@10 {
+ compatible = "asahi-kasei,ak4619";
+ reg = <0x10>;
+ clocks = <&rcar_sound 3>;
+ clock-names = "mclk";
+ #sound-dai-cells = <0>;
+
+ port {
+ ak4619_endpoint: endpoint {
+ remote-endpoint = <&rsnd_endpoint0>;
+ };
+ };
+ };
+
+ /* Pin-to-pin, register map, and control compatible with CS2000 and CS2200 */
+ cs2500: clk_multiplier@4f {
+ #clock-cells = <0>;
+ compatible = "cirrus,cs2500-cp", "cirrus,cs2000-cp";
+ reg = <0x4f>;
+ clocks = <&audio_clkout>, <&x12_clk>;
+ clock-names = "clk_in", "ref_clk";
+
+ assigned-clocks = <&cs2500>;
+ assigned-clock-rates = <24576000>; /* 1/1 divide */
+ };
+};
+
+&i2c4 {
+ clock-frequency = <400000>;
+ status = "okay";
+
+ versaclock3: clock-generator@68 {
+ compatible = "renesas,5p35023";
+ reg = <0x68>;
+ #clock-cells = <1>;
+ clocks = <&x3013_clk>;
+ assigned-clocks = <&versaclock3 4>, <&versaclock3 5>;
+ assigned-clock-rates = <100000000>, <100000000>;
+ };
+
+ versaclock5: clock-generator@6a {
+ compatible = "idt,5p49v5923";
+ reg = <0x6a>;
+ #clock-cells = <1>;
+ clocks = <&x23_clk>;
+ clock-names = "xin";
+ };
+
+ video-receiver@70 {
+ compatible = "adi,adv7482";
+ reg = <0x70 0x71 0x72 0x73 0x74 0x75
+ 0x60 0x61 0x62 0x63 0x64 0x65>;
+ reg-names = "main", "dpll", "cp", "hdmi", "edid", "repeater",
+ "infoframe", "cbus", "cec", "sdp", "txa", "txb" ;
+
+ interrupt-parent = <&gpio6>;
+ interrupts = <30 IRQ_TYPE_LEVEL_LOW>,
+ <31 IRQ_TYPE_LEVEL_LOW>;
+ interrupt-names = "intrq1", "intrq2";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@7 {
+ reg = <7>;
+
+ adv7482_ain7: endpoint {
+ remote-endpoint = <&cvbs_con>;
+ };
+ };
+
+ port@8 {
+ reg = <8>;
+
+ adv7482_hdmi: endpoint {
+ remote-endpoint = <&hdmi_in_con>;
+ };
+ };
+
+ port@a {
+ reg = <10>;
+
+ adv7482_txa: endpoint {
+ clock-lanes = <0>;
+ data-lanes = <1 2 3 4>;
+ remote-endpoint = <&csi40_in>;
+ };
+ };
+ };
+ };
+
+ csa_vdd: adc@7c {
+ compatible = "maxim,max9611";
+ reg = <0x7c>;
+
+ shunt-resistor-micro-ohms = <5000>;
+ };
+
+ csa_dvfs: adc@7f {
+ compatible = "maxim,max9611";
+ reg = <0x7f>;
+
+ shunt-resistor-micro-ohms = <5000>;
+ };
+};
+
+&i2c_dvfs {
+ status = "okay";
+
+ clock-frequency = <400000>;
+
+ eeprom@50 {
+ compatible = "rohm,br24t01", "atmel,24c01";
+ reg = <0x50>;
+ pagesize = <8>;
+ };
+};
+
+&ohci0 {
+ dr_mode = "otg";
+ status = "okay";
+};
+
+&ohci1 {
+ status = "okay";
+};
+
+&pcie_bus_clk {
+ status = "disabled";
+};
+
+&pciec0 {
+ clocks = <&cpg CPG_MOD 319>, <&versaclock3 4>;
+ status = "okay";
+};
+
+&pciec0_rp {
+ clocks = <&versaclock3 5>;
+};
+
+&pfc {
+ pinctrl-0 = <&scif_clk_pins>;
+ pinctrl-names = "default";
+
+ avb_pins: avb {
+ mux {
+ groups = "avb_link", "avb_mdio", "avb_mii";
+ function = "avb";
+ };
+
+ pins_mdio {
+ groups = "avb_mdio";
+ drive-strength = <24>;
+ };
+
+ pins_mii_tx {
+ pins = "PIN_AVB_TX_CTL", "PIN_AVB_TXC", "PIN_AVB_TD0",
+ "PIN_AVB_TD1", "PIN_AVB_TD2", "PIN_AVB_TD3";
+ drive-strength = <12>;
+ };
+ };
+
+ du_pins: du {
+ groups = "du_rgb888", "du_sync", "du_oddf", "du_clk_out_0";
+ function = "du";
+ };
+
+ hscif1_pins: hscif1 {
+ groups = "hscif1_data_a", "hscif1_ctrl_a";
+ function = "hscif1";
+ };
+
+ i2c2_pins: i2c2 {
+ groups = "i2c2_a";
+ function = "i2c2";
+ };
+
+ irq0_pins: irq0 {
+ groups = "intc_ex_irq0";
+ function = "intc_ex";
+ };
+
+ keys_pins: keys {
+ pins = "GP_5_17", "GP_5_20", "GP_5_22";
+ bias-pull-up;
+ };
+
+ pwm1_pins: pwm1 {
+ groups = "pwm1_a";
+ function = "pwm1";
+ };
+
+ pwm2_pins: pwm2 {
+ groups = "pwm2_a";
+ function = "pwm2";
+ };
+
+ scif1_pins: scif1 {
+ groups = "scif1_data_a", "scif1_ctrl";
+ function = "scif1";
+ };
+
+ scif2_pins: scif2 {
+ groups = "scif2_data_a";
+ function = "scif2";
+ };
+
+ scif_clk_pins: scif_clk {
+ groups = "scif_clk_a";
+ function = "scif_clk";
+ };
+
+ sdhi0_pins: sd0 {
+ groups = "sdhi0_data4", "sdhi0_ctrl";
+ function = "sdhi0";
+ power-source = <3300>;
+ };
+
+ sdhi0_pins_uhs: sd0_uhs {
+ groups = "sdhi0_data4", "sdhi0_ctrl";
+ function = "sdhi0";
+ power-source = <1800>;
+ };
+
+ sdhi2_pins: sd2 {
+ groups = "sdhi2_data8", "sdhi2_ctrl", "sdhi2_ds";
+ function = "sdhi2";
+ power-source = <1800>;
+ };
+
+ sound_pins: sound {
+ groups = "ssi01239_ctrl", "ssi0_data", "ssi1_data_a";
+ function = "ssi";
+ };
+
+ sound_clk_pins: sound_clk {
+ groups = "audio_clk_a_a", "audio_clk_b_a", "audio_clk_c_a",
+ "audio_clkout_a", "audio_clkout3_a";
+ function = "audio_clk";
+ };
+
+ usb0_pins: usb0 {
+ groups = "usb0";
+ function = "usb0";
+ };
+
+ usb1_pins: usb1 {
+ mux {
+ groups = "usb1";
+ function = "usb1";
+ };
+
+ ovc {
+ pins = "GP_6_27";
+ bias-pull-up;
+ };
+
+ pwen {
+ pins = "GP_6_26";
+ bias-pull-down;
+ };
+ };
+};
+
+&pwm1 {
+ pinctrl-0 = <&pwm1_pins>;
+ pinctrl-names = "default";
+
+ status = "okay";
+};
+
+&pwm2 {
+ pinctrl-0 = <&pwm2_pins>;
+ pinctrl-names = "default";
+
+ status = "okay";
+};
+
+&rcar_sound {
+ pinctrl-0 = <&sound_pins>, <&sound_clk_pins>;
+ pinctrl-names = "default";
+
+ /* Single DAI */
+ #sound-dai-cells = <0>;
+
+ /* audio_clkout0/1/2/3 */
+ #clock-cells = <1>;
+ clock-frequency = <12288000 11289600>;
+
+ status = "okay";
+
+ /* update <audio_clk_b> to <cs2500> */
+ clocks = <&cpg CPG_MOD 1005>,
+ <&cpg CPG_MOD 1006>, <&cpg CPG_MOD 1007>,
+ <&cpg CPG_MOD 1008>, <&cpg CPG_MOD 1009>,
+ <&cpg CPG_MOD 1010>, <&cpg CPG_MOD 1011>,
+ <&cpg CPG_MOD 1012>, <&cpg CPG_MOD 1013>,
+ <&cpg CPG_MOD 1014>, <&cpg CPG_MOD 1015>,
+ <&cpg CPG_MOD 1022>, <&cpg CPG_MOD 1023>,
+ <&cpg CPG_MOD 1024>, <&cpg CPG_MOD 1025>,
+ <&cpg CPG_MOD 1026>, <&cpg CPG_MOD 1027>,
+ <&cpg CPG_MOD 1028>, <&cpg CPG_MOD 1029>,
+ <&cpg CPG_MOD 1030>, <&cpg CPG_MOD 1031>,
+ <&cpg CPG_MOD 1020>, <&cpg CPG_MOD 1021>,
+ <&cpg CPG_MOD 1020>, <&cpg CPG_MOD 1021>,
+ <&cpg CPG_MOD 1019>, <&cpg CPG_MOD 1018>,
+ <&audio_clk_a>, <&cs2500>,
+ <&audio_clk_c>,
+ <&cpg CPG_MOD 922>;
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ rsnd_port0: port {
+ rsnd_endpoint0: endpoint {
+ remote-endpoint = <&ak4619_endpoint>;
+ dai-format = "left_j";
+ bitclock-master = <&rsnd_endpoint0>;
+ frame-master = <&rsnd_endpoint0>;
+ playback = <&ssi0>, <&src0>, <&dvc0>;
+ capture = <&ssi1>, <&src1>, <&dvc1>;
+ };
+ };
+ };
+};
+
+&rwdt {
+ timeout-sec = <60>;
+ status = "okay";
+};
+
+&scif1 {
+ pinctrl-0 = <&scif1_pins>;
+ pinctrl-names = "default";
+
+ uart-has-rtscts;
+ /* Please only enable hscif1 or scif1 */
+ /* status = "okay"; */
+};
+
+&scif2 {
+ pinctrl-0 = <&scif2_pins>;
+ pinctrl-names = "default";
+
+ status = "okay";
+};
+
+&scif_clk {
+ clock-frequency = <14745600>;
+};
+
+&sdhi0 {
+ pinctrl-0 = <&sdhi0_pins>;
+ pinctrl-1 = <&sdhi0_pins_uhs>;
+ pinctrl-names = "default", "state_uhs";
+
+ vmmc-supply = <&vcc_sdhi0>;
+ vqmmc-supply = <&vccq_sdhi0>;
+ cd-gpios = <&gpio3 12 GPIO_ACTIVE_LOW>;
+ wp-gpios = <&gpio3 13 GPIO_ACTIVE_HIGH>;
+ bus-width = <4>;
+ sd-uhs-sdr50;
+ sd-uhs-sdr104;
+ status = "okay";
+};
+
+&sdhi2 {
+ /* used for on-board 8bit eMMC */
+ pinctrl-0 = <&sdhi2_pins>;
+ pinctrl-1 = <&sdhi2_pins>;
+ pinctrl-names = "default", "state_uhs";
+
+ iommus = <&ipmmu_ds1 34>;
+
+ vmmc-supply = <®_3p3v>;
+ vqmmc-supply = <®_1p8v>;
+ bus-width = <8>;
+ mmc-hs200-1_8v;
+ no-sd;
+ no-sdio;
+ non-removable;
+ fixed-emmc-driver-type = <1>;
+ full-pwr-cycle-in-suspend;
+ status = "okay";
+};
+
+&ssi1 {
+ shared-pin;
+};
+
+&usb_extal_clk {
+ clock-frequency = <50000000>;
+};
+
+&usb2_phy0 {
+ pinctrl-0 = <&usb0_pins>;
+ pinctrl-names = "default";
+
+ vbus-supply = <&vbus0_usb2>;
+ status = "okay";
+};
+
+&usb2_phy1 {
+ pinctrl-0 = <&usb1_pins>;
+ pinctrl-names = "default";
+
+ status = "okay";
+};
+
+&vin0 {
+ status = "okay";
+};
+
+&vin1 {
+ status = "okay";
+};
+
+&vin2 {
+ status = "okay";
+};
+
+&vin3 {
+ status = "okay";
+};
+
+&vin4 {
+ status = "okay";
+};
+
+&vin5 {
+ status = "okay";
+};
+
+&vin6 {
+ status = "okay";
+};
+
+&vin7 {
+ status = "okay";
+};
+
+&vspb {
+ status = "okay";
+};
+
+&vspi0 {
+ status = "okay";
+};
--
2.53.0
^ permalink raw reply related
* Re: [PATCH 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding
From: Wadim Mueller @ 2026-04-19 19:50 UTC (permalink / raw)
To: Conor Dooley
Cc: wbg, robh, krzk+dt, conor+dt, linux-iio, devicetree, linux-kernel
In-Reply-To: <20260417-banjo-uncross-fbec3af75617@spud>
On 2026-04-17 17:13, Conor Dooley wrote:
Thanks for the review
> On Thu, Apr 16, 2026 at 10:48:17PM +0200, Wadim Mueller wrote:
> > Add devicetree binding documentation for the GPIO-based quadrature
> > encoder counter driver. The driver reads A/B quadrature signals and
> > an optional index pulse via edge-triggered GPIO interrupts, supporting
> > X1, X2, X4 quadrature decoding and pulse-direction mode.
> >
> > This is useful on SoCs that lack a dedicated hardware quadrature
> > decoder or where the encoder is wired to generic GPIO pins.
> >
> > Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
> > ---
> > .../counter/gpio-quadrature-encoder.yaml | 69 +++++++++++++++++++
> > 1 file changed, 69 insertions(+)
> > create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
> >
> > diff --git a/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
> > new file mode 100644
> > index 000000000..a52deaab6
> > --- /dev/null
> > +++ b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
> > @@ -0,0 +1,69 @@
> > +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
> > +%YAML 1.2
> > +---
> > +$id: http://devicetree.org/schemas/counter/gpio-quadrature-encoder.yaml#
> > +$schema: http://devicetree.org/meta-schemas/core.yaml#
> > +
> > +title: GPIO-based Quadrature Encoder
> > +
> > +maintainers:
> > + - Wadim Mueller <wadim.mueller@cmblu.de>
> > +
> > +description: |
> > + A generic GPIO-based quadrature encoder counter. Reads A/B quadrature
> > + signals and an optional index pulse via edge-triggered GPIO interrupts.
> > + Supports X1, X2, X4 quadrature decoding and pulse-direction mode.
> > +
>
> > + This driver is useful on SoCs that lack a dedicated hardware quadrature
> > + decoder (eQEP, QEI, etc.) or where the encoder is wired to generic GPIO
> > + pins rather than to a dedicated peripheral.
>
> Idea seems okay to me. Please rephrase this section to avoid talking
> about drivers...
Thanks, will fix that in v2.
>
> > +
> > +properties:
> > + compatible:
> > + const: gpio-quadrature-encoder
> > +
> > + encoder-a-gpios:
> > + maxItems: 1
> > + description:
> > + GPIO connected to the encoder's A (phase A) output.
> > +
> > + encoder-b-gpios:
> > + maxItems: 1
> > + description:
> > + GPIO connected to the encoder's B (phase B) output.
> > +
> > + encoder-index-gpios:
> > + maxItems: 1
> > + description:
> > + Optional GPIO connected to the encoder's index (Z) output.
> > + When the index input is enabled via sysfs, the count resets
> > + to zero on each index pulse.
>
> ...and this to stop talking about sysfs and driver behaviour though.
> Bindings are about hardware.
> pw-bot: changes-requested
>
Agreed, will rephrase to mention hardware signals only
> > +
> > +required:
> > + - compatible
> > + - encoder-a-gpios
> > + - encoder-b-gpios
> > +
> > +additionalProperties: false
> > +
> > +examples:
> > + - |
> > + #include <dt-bindings/gpio/gpio.h>
> > +
> > + quadrature-encoder-0 {
> > + compatible = "gpio-quadrature-encoder";
> > + encoder-a-gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>;
> > + encoder-b-gpios = <&gpio0 11 GPIO_ACTIVE_HIGH>;
> > + };
> > +
> > + - |
> > + #include <dt-bindings/gpio/gpio.h>
> > +
> > + quadrature-encoder-1 {
> > + compatible = "gpio-quadrature-encoder";
> > + encoder-a-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;
> > + encoder-b-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>;
> > + encoder-index-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
> > + };
>
> I think this example alone is sufficient btw.
>
Ack, will drop the first example
> Cheers,
> Conor.
> > +
> > +...
> > --
> > 2.52.0
> >
^ permalink raw reply
* [PATCH v2 0/3] counter: add GPIO-based quadrature encoder driver
From: Wadim Mueller @ 2026-04-19 19:59 UTC (permalink / raw)
To: linux-iio, devicetree
Cc: wbg, conor+dt, krzk+dt, robh, linux-kernel, Wadim Mueller
This series adds a new counter subsystem driver that implements
quadrature encoder position tracking using plain GPIO pins with
edge-triggered interrupts.
The driver is intended for low to medium speed rotary encoders where
hardware counter peripherals (eQEP, FTM, etc.) are unavailable or
already in use. It targets the same use-cases as interrupt-cnt.c but
provides full quadrature decoding instead of simple pulse counting.
Features:
- X1, X2, X4 quadrature decoding and pulse-direction mode
- Optional index signal for zero-reset
- Configurable ceiling (position clamping)
- Standard counter subsystem sysfs + chrdev interface
- Enable/disable via sysfs with IRQ gating
Tested on TI AM64x (Cortex-A53) with a motor-driven rotary encoder
at up to 2 kHz quadrature edge rate.
Changes in v2:
- DT binding: rephrase description to describe hardware, not
driver/sysfs behaviour (Conor Dooley)
- DT binding: drop redundant example without index GPIO (Conor Dooley)
Wadim Mueller (3):
dt-bindings: counter: add gpio-quadrature-encoder binding
counter: add GPIO-based quadrature encoder driver
MAINTAINERS: add entry for GPIO quadrature encoder counter driver
.../counter/gpio-quadrature-encoder.yaml | 60 ++
MAINTAINERS | 7 +
drivers/counter/Kconfig | 15 +
drivers/counter/Makefile | 1 +
drivers/counter/gpio-quadrature-encoder.c | 710 ++++++++++++++++++
5 files changed, 793 insertions(+)
create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
create mode 100644 drivers/counter/gpio-quadrature-encoder.c
--
2.52.0
^ permalink raw reply
* [PATCH v2 1/3] dt-bindings: counter: add gpio-quadrature-encoder binding
From: Wadim Mueller @ 2026-04-19 19:59 UTC (permalink / raw)
To: linux-iio, devicetree
Cc: wbg, conor+dt, krzk+dt, robh, linux-kernel, Wadim Mueller
In-Reply-To: <20260419195908.12202-1-wafgo01@gmail.com>
Add devicetree binding documentation for the GPIO-based quadrature
encoder counter driver. The driver reads A/B quadrature signals and
an optional index pulse via edge-triggered GPIO interrupts, supporting
X1, X2, X4 quadrature decoding and pulse-direction mode.
This is useful on SoCs that lack a dedicated hardware quadrature
decoder or where the encoder is wired to generic GPIO pins.
Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
---
.../counter/gpio-quadrature-encoder.yaml | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
create mode 100644 Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
diff --git a/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
new file mode 100644
index 000000000..741396b29
--- /dev/null
+++ b/Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/counter/gpio-quadrature-encoder.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: GPIO-based Quadrature Encoder
+
+maintainers:
+ - Wadim Mueller <wadim.mueller@cmblu.de>
+
+description: |
+ A generic GPIO-based quadrature encoder counter. Reads A/B quadrature
+ signals and an optional index pulse via edge-triggered GPIO interrupts.
+ Supports X1, X2, X4 quadrature decoding and pulse-direction mode.
+
+ This is useful on SoCs that lack a dedicated hardware quadrature
+ decoder (eQEP, QEI, etc.) or where the encoder is wired to generic
+ GPIO pins rather than to a dedicated peripheral.
+
+properties:
+ compatible:
+ const: gpio-quadrature-encoder
+
+ encoder-a-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the encoder's A (phase A) output.
+
+ encoder-b-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the encoder's B (phase B) output.
+
+ encoder-index-gpios:
+ maxItems: 1
+ description:
+ Optional GPIO connected to the encoder's index (Z) output.
+ The index signal pulses once per revolution and can be used
+ as a reference point for absolute position tracking.
+
+required:
+ - compatible
+ - encoder-a-gpios
+ - encoder-b-gpios
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+
+ quadrature-encoder {
+ compatible = "gpio-quadrature-encoder";
+ encoder-a-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;
+ encoder-b-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>;
+ encoder-index-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
+ };
+
+...
--
2.52.0
^ permalink raw reply related
* [PATCH v2 3/3] MAINTAINERS: add entry for GPIO quadrature encoder counter driver
From: Wadim Mueller @ 2026-04-19 19:59 UTC (permalink / raw)
To: linux-iio, devicetree
Cc: wbg, conor+dt, krzk+dt, robh, linux-kernel, Wadim Mueller
In-Reply-To: <20260419195908.12202-1-wafgo01@gmail.com>
Add myself as maintainer for the new gpio-quadrature-encoder counter
driver and its devicetree binding.
Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 06a8c7457..fca62baa7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11018,6 +11018,13 @@ F: Documentation/dev-tools/gpio-sloppy-logic-analyzer.rst
F: drivers/gpio/gpio-sloppy-logic-analyzer.c
F: tools/gpio/gpio-sloppy-logic-analyzer.sh
+GPIO QUADRATURE ENCODER COUNTER DRIVER
+M: Wadim Mueller <wafgo01@gmail.com>
+L: linux-iio@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/counter/gpio-quadrature-encoder.yaml
+F: drivers/counter/gpio-quadrature-encoder.c
+
GPIO SUBSYSTEM
M: Linus Walleij <linusw@kernel.org>
M: Bartosz Golaszewski <brgl@kernel.org>
--
2.52.0
^ permalink raw reply related
* [PATCH v2 2/3] counter: add GPIO-based quadrature encoder driver
From: Wadim Mueller @ 2026-04-19 19:59 UTC (permalink / raw)
To: linux-iio, devicetree
Cc: wbg, conor+dt, krzk+dt, robh, linux-kernel, Wadim Mueller
In-Reply-To: <20260419195908.12202-1-wafgo01@gmail.com>
Add a platform driver that turns ordinary GPIOs into a quadrature
encoder counter device. The driver requests edge-triggered interrupts
on the A and B (and optional Index) GPIOs and decodes the quadrature
signal in software using a classic state-table approach.
Supported counting modes:
- Quadrature X1 (count on A rising edge only)
- Quadrature X2 (count on both A edges)
- Quadrature X4 (count on every A and B edge)
- Pulse-direction (A = pulse, B = direction)
An optional index signal resets the count to zero on its rising edge
when enabled through sysfs. A configurable ceiling clamps the count
to [0, ceiling].
Signed-off-by: Wadim Mueller <wafgo01@gmail.com>
---
drivers/counter/Kconfig | 15 +
drivers/counter/Makefile | 1 +
drivers/counter/gpio-quadrature-encoder.c | 710 ++++++++++++++++++++++
3 files changed, 726 insertions(+)
create mode 100644 drivers/counter/gpio-quadrature-encoder.c
diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig
index d30d22dfe..72c5c8159 100644
--- a/drivers/counter/Kconfig
+++ b/drivers/counter/Kconfig
@@ -68,6 +68,21 @@ config INTEL_QEP
To compile this driver as a module, choose M here: the module
will be called intel-qep.
+config GPIO_QUADRATURE_ENCODER
+ tristate "GPIO-based quadrature encoder counter driver"
+ depends on GPIOLIB
+ help
+ Select this option to enable the GPIO-based quadrature encoder
+ counter driver. It reads A/B quadrature signals and an optional
+ index pulse via edge-triggered GPIO interrupts, supporting X1, X2,
+ X4 quadrature decoding and pulse-direction mode.
+
+ This is useful on SoCs that lack a dedicated hardware quadrature
+ decoder or where the encoder is wired to generic GPIO pins.
+
+ To compile this driver as a module, choose M here: the
+ module will be called gpio-quadrature-encoder.
+
config INTERRUPT_CNT
tristate "Interrupt counter driver"
depends on GPIOLIB
diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile
index fa3c1d08f..2bef64d10 100644
--- a/drivers/counter/Makefile
+++ b/drivers/counter/Makefile
@@ -14,6 +14,7 @@ obj-$(CONFIG_STM32_TIMER_CNT) += stm32-timer-cnt.o
obj-$(CONFIG_STM32_LPTIMER_CNT) += stm32-lptimer-cnt.o
obj-$(CONFIG_TI_EQEP) += ti-eqep.o
obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec.o
+obj-$(CONFIG_GPIO_QUADRATURE_ENCODER) += gpio-quadrature-encoder.o
obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o
obj-$(CONFIG_INTEL_QEP) += intel-qep.o
obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o
diff --git a/drivers/counter/gpio-quadrature-encoder.c b/drivers/counter/gpio-quadrature-encoder.c
new file mode 100644
index 000000000..0822f0a8a
--- /dev/null
+++ b/drivers/counter/gpio-quadrature-encoder.c
@@ -0,0 +1,710 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * GPIO-based Quadrature Encoder Counter Driver
+ *
+ * Reads quadrature encoder signals (A, B, and optional Index) via GPIOs.
+ * Supports X1, X2, X4 quadrature decoding and pulse-direction mode.
+ *
+ * Copyright (C) 2026 CMBlu Energy AG
+ * Author: Wadim Mueller <wafgo01@gmail.com>
+ */
+
+#include <linux/counter.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+enum gpio_qenc_function {
+ GPIO_QENC_FUNC_QUAD_X1 = 0,
+ GPIO_QENC_FUNC_QUAD_X2,
+ GPIO_QENC_FUNC_QUAD_X4,
+ GPIO_QENC_FUNC_PULSE_DIR,
+};
+
+enum gpio_qenc_signal_id {
+ GPIO_QENC_SIGNAL_A = 0,
+ GPIO_QENC_SIGNAL_B,
+ GPIO_QENC_SIGNAL_INDEX,
+};
+
+struct gpio_qenc_priv {
+ struct gpio_desc *gpio_a;
+ struct gpio_desc *gpio_b;
+ struct gpio_desc *gpio_index;
+
+ int irq_a;
+ int irq_b;
+ int irq_index;
+
+ spinlock_t lock;
+
+ s64 count;
+ u64 ceiling;
+ bool enabled;
+ enum counter_count_direction direction;
+ enum gpio_qenc_function function;
+
+ int prev_a;
+ int prev_b;
+
+ bool index_enabled;
+
+ struct counter_signal signals[3];
+ struct counter_synapse synapses[3];
+ struct counter_count cnts;
+};
+
+/*
+ * Quadrature state table for X4 decoding.
+ * Rows = previous state (A<<1 | B), Columns = new state (A<<1 | B).
+ * Values: 0 = no change, +1 = forward, -1 = backward, 2 = error (skip).
+ */
+static const int quad_table[4][4] = {
+ /* 00 01 10 11 <- new */
+ /* 00 */ { 0, -1, 1, 2 },
+ /* 01 */ { 1, 0, 2, -1 },
+ /* 10 */ { -1, 2, 0, 1 },
+ /* 11 */ { 2, 1, -1, 0 },
+};
+
+static void gpio_qenc_update_count(struct gpio_qenc_priv *priv, int delta)
+{
+ s64 new_count;
+
+ if (!delta)
+ return;
+
+ new_count = priv->count + delta;
+
+ if (priv->ceiling) {
+ if (new_count < 0)
+ new_count = 0;
+ else if (new_count > (s64)priv->ceiling)
+ new_count = priv->ceiling;
+ }
+
+ priv->count = new_count;
+ priv->direction = (delta > 0) ? COUNTER_COUNT_DIRECTION_FORWARD
+ : COUNTER_COUNT_DIRECTION_BACKWARD;
+}
+
+static irqreturn_t gpio_qenc_a_isr(int irq, void *dev_id)
+{
+ struct counter_device *counter = dev_id;
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+ int a, b, prev_state, new_state, delta;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ if (!priv->enabled)
+ goto out;
+
+ a = gpiod_get_value(priv->gpio_a);
+ b = gpiod_get_value(priv->gpio_b);
+
+ prev_state = (priv->prev_a << 1) | priv->prev_b;
+ new_state = (a << 1) | b;
+
+ switch (priv->function) {
+ case GPIO_QENC_FUNC_QUAD_X4:
+ delta = quad_table[prev_state][new_state];
+ if (delta == 2)
+ delta = 0;
+ gpio_qenc_update_count(priv, delta);
+ break;
+
+ case GPIO_QENC_FUNC_QUAD_X2:
+ delta = quad_table[prev_state][new_state];
+ if (delta == 2)
+ delta = 0;
+ gpio_qenc_update_count(priv, delta);
+ break;
+
+ case GPIO_QENC_FUNC_QUAD_X1:
+ if (!priv->prev_a && a) {
+ delta = b ? -1 : 1;
+ gpio_qenc_update_count(priv, delta);
+ }
+ break;
+
+ case GPIO_QENC_FUNC_PULSE_DIR:
+ if (!priv->prev_a && a) {
+ delta = b ? -1 : 1;
+ gpio_qenc_update_count(priv, delta);
+ }
+ break;
+ }
+
+ priv->prev_a = a;
+ priv->prev_b = b;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ counter_push_event(counter, COUNTER_EVENT_CHANGE_OF_STATE, 0);
+
+ return IRQ_HANDLED;
+
+out:
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t gpio_qenc_b_isr(int irq, void *dev_id)
+{
+ struct counter_device *counter = dev_id;
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+ int a, b, prev_state, new_state, delta;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ if (!priv->enabled)
+ goto out;
+
+ a = gpiod_get_value(priv->gpio_a);
+ b = gpiod_get_value(priv->gpio_b);
+
+ prev_state = (priv->prev_a << 1) | priv->prev_b;
+ new_state = (a << 1) | b;
+
+ switch (priv->function) {
+ case GPIO_QENC_FUNC_QUAD_X4:
+ delta = quad_table[prev_state][new_state];
+ if (delta == 2)
+ delta = 0;
+ gpio_qenc_update_count(priv, delta);
+ break;
+
+ case GPIO_QENC_FUNC_QUAD_X2:
+ /* X2: only A-channel edges update count */
+ break;
+
+ case GPIO_QENC_FUNC_QUAD_X1:
+ case GPIO_QENC_FUNC_PULSE_DIR:
+ break;
+ }
+
+ priv->prev_a = a;
+ priv->prev_b = b;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return IRQ_HANDLED;
+
+out:
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t gpio_qenc_index_isr(int irq, void *dev_id)
+{
+ struct counter_device *counter = dev_id;
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ if (priv->enabled && priv->index_enabled)
+ priv->count = 0;
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ counter_push_event(counter, COUNTER_EVENT_INDEX, 0);
+
+ return IRQ_HANDLED;
+}
+
+static int gpio_qenc_count_read(struct counter_device *counter,
+ struct counter_count *count, u64 *val)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ *val = (u64)priv->count;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return 0;
+}
+
+static int gpio_qenc_count_write(struct counter_device *counter,
+ struct counter_count *count, const u64 val)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ if (priv->ceiling && val > priv->ceiling) {
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return -EINVAL;
+ }
+
+ priv->count = (s64)val;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return 0;
+}
+
+static const enum counter_function gpio_qenc_functions[] = {
+ COUNTER_FUNCTION_QUADRATURE_X1_A,
+ COUNTER_FUNCTION_QUADRATURE_X2_A,
+ COUNTER_FUNCTION_QUADRATURE_X4,
+ COUNTER_FUNCTION_PULSE_DIRECTION,
+};
+
+static int gpio_qenc_function_read(struct counter_device *counter,
+ struct counter_count *count,
+ enum counter_function *function)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ switch (priv->function) {
+ case GPIO_QENC_FUNC_QUAD_X1:
+ *function = COUNTER_FUNCTION_QUADRATURE_X1_A;
+ break;
+ case GPIO_QENC_FUNC_QUAD_X2:
+ *function = COUNTER_FUNCTION_QUADRATURE_X2_A;
+ break;
+ case GPIO_QENC_FUNC_QUAD_X4:
+ *function = COUNTER_FUNCTION_QUADRATURE_X4;
+ break;
+ case GPIO_QENC_FUNC_PULSE_DIR:
+ *function = COUNTER_FUNCTION_PULSE_DIRECTION;
+ break;
+ }
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return 0;
+}
+
+static int gpio_qenc_function_write(struct counter_device *counter,
+ struct counter_count *count,
+ enum counter_function function)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ switch (function) {
+ case COUNTER_FUNCTION_QUADRATURE_X1_A:
+ priv->function = GPIO_QENC_FUNC_QUAD_X1;
+ break;
+ case COUNTER_FUNCTION_QUADRATURE_X2_A:
+ priv->function = GPIO_QENC_FUNC_QUAD_X2;
+ break;
+ case COUNTER_FUNCTION_QUADRATURE_X4:
+ priv->function = GPIO_QENC_FUNC_QUAD_X4;
+ break;
+ case COUNTER_FUNCTION_PULSE_DIRECTION:
+ priv->function = GPIO_QENC_FUNC_PULSE_DIR;
+ break;
+ default:
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return -EINVAL;
+ }
+
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return 0;
+}
+
+static const enum counter_synapse_action gpio_qenc_synapse_actions[] = {
+ COUNTER_SYNAPSE_ACTION_BOTH_EDGES,
+ COUNTER_SYNAPSE_ACTION_RISING_EDGE,
+ COUNTER_SYNAPSE_ACTION_NONE,
+};
+
+static int gpio_qenc_action_read(struct counter_device *counter,
+ struct counter_count *count,
+ struct counter_synapse *synapse,
+ enum counter_synapse_action *action)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ enum gpio_qenc_signal_id signal_id = synapse->signal->id;
+
+ switch (priv->function) {
+ case GPIO_QENC_FUNC_QUAD_X4:
+ if (signal_id == GPIO_QENC_SIGNAL_A ||
+ signal_id == GPIO_QENC_SIGNAL_B)
+ *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES;
+ else
+ *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+ return 0;
+
+ case GPIO_QENC_FUNC_QUAD_X2:
+ if (signal_id == GPIO_QENC_SIGNAL_A)
+ *action = COUNTER_SYNAPSE_ACTION_BOTH_EDGES;
+ else if (signal_id == GPIO_QENC_SIGNAL_B)
+ *action = COUNTER_SYNAPSE_ACTION_NONE;
+ else
+ *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+ return 0;
+
+ case GPIO_QENC_FUNC_QUAD_X1:
+ if (signal_id == GPIO_QENC_SIGNAL_A)
+ *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+ else if (signal_id == GPIO_QENC_SIGNAL_B)
+ *action = COUNTER_SYNAPSE_ACTION_NONE;
+ else
+ *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+ return 0;
+
+ case GPIO_QENC_FUNC_PULSE_DIR:
+ if (signal_id == GPIO_QENC_SIGNAL_A)
+ *action = COUNTER_SYNAPSE_ACTION_RISING_EDGE;
+ else
+ *action = COUNTER_SYNAPSE_ACTION_NONE;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static int gpio_qenc_signal_read(struct counter_device *counter,
+ struct counter_signal *signal,
+ enum counter_signal_level *level)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ struct gpio_desc *gpio;
+ int ret;
+
+ switch (signal->id) {
+ case GPIO_QENC_SIGNAL_A:
+ gpio = priv->gpio_a;
+ break;
+ case GPIO_QENC_SIGNAL_B:
+ gpio = priv->gpio_b;
+ break;
+ case GPIO_QENC_SIGNAL_INDEX:
+ gpio = priv->gpio_index;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (!gpio)
+ return -EINVAL;
+
+ ret = gpiod_get_value(gpio);
+ if (ret < 0)
+ return ret;
+
+ *level = ret ? COUNTER_SIGNAL_LEVEL_HIGH : COUNTER_SIGNAL_LEVEL_LOW;
+ return 0;
+}
+
+static int gpio_qenc_events_configure(struct counter_device *counter)
+{
+ return 0;
+}
+
+static int gpio_qenc_watch_validate(struct counter_device *counter,
+ const struct counter_watch *watch)
+{
+ if (watch->channel != 0)
+ return -EINVAL;
+
+ switch (watch->event) {
+ case COUNTER_EVENT_CHANGE_OF_STATE:
+ case COUNTER_EVENT_INDEX:
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct counter_ops gpio_qenc_ops = {
+ .count_read = gpio_qenc_count_read,
+ .count_write = gpio_qenc_count_write,
+ .function_read = gpio_qenc_function_read,
+ .function_write = gpio_qenc_function_write,
+ .action_read = gpio_qenc_action_read,
+ .signal_read = gpio_qenc_signal_read,
+ .events_configure = gpio_qenc_events_configure,
+ .watch_validate = gpio_qenc_watch_validate,
+};
+
+static int gpio_qenc_ceiling_read(struct counter_device *counter,
+ struct counter_count *count, u64 *val)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ *val = priv->ceiling;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return 0;
+}
+
+static int gpio_qenc_ceiling_write(struct counter_device *counter,
+ struct counter_count *count, const u64 val)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ priv->ceiling = val;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return 0;
+}
+
+static int gpio_qenc_enable_read(struct counter_device *counter,
+ struct counter_count *count, u8 *enable)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+
+ *enable = priv->enabled;
+ return 0;
+}
+
+static int gpio_qenc_enable_write(struct counter_device *counter,
+ struct counter_count *count, u8 enable)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+
+ if (priv->enabled == !!enable) {
+ spin_unlock_irqrestore(&priv->lock, flags);
+ return 0;
+ }
+
+ if (enable) {
+ priv->enabled = true;
+ spin_unlock_irqrestore(&priv->lock, flags);
+ enable_irq(priv->irq_a);
+ enable_irq(priv->irq_b);
+ if (priv->irq_index)
+ enable_irq(priv->irq_index);
+ } else {
+ priv->enabled = false;
+ spin_unlock_irqrestore(&priv->lock, flags);
+ disable_irq(priv->irq_a);
+ disable_irq(priv->irq_b);
+ if (priv->irq_index)
+ disable_irq(priv->irq_index);
+ }
+
+ return 0;
+}
+
+static int gpio_qenc_direction_read(struct counter_device *counter,
+ struct counter_count *count, u32 *direction)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ *direction = priv->direction;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return 0;
+}
+
+static int gpio_qenc_index_enable_read(struct counter_device *counter,
+ struct counter_count *count, u8 *val)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+
+ *val = priv->index_enabled;
+ return 0;
+}
+
+static int gpio_qenc_index_enable_write(struct counter_device *counter,
+ struct counter_count *count, u8 val)
+{
+ struct gpio_qenc_priv *priv = counter_priv(counter);
+ unsigned long flags;
+
+ spin_lock_irqsave(&priv->lock, flags);
+ priv->index_enabled = !!val;
+ spin_unlock_irqrestore(&priv->lock, flags);
+
+ return 0;
+}
+
+static struct counter_comp gpio_qenc_count_ext[] = {
+ COUNTER_COMP_CEILING(gpio_qenc_ceiling_read, gpio_qenc_ceiling_write),
+ COUNTER_COMP_ENABLE(gpio_qenc_enable_read, gpio_qenc_enable_write),
+ COUNTER_COMP_DIRECTION(gpio_qenc_direction_read),
+ COUNTER_COMP_COUNT_BOOL("index_enabled",
+ gpio_qenc_index_enable_read,
+ gpio_qenc_index_enable_write),
+};
+
+static int gpio_qenc_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct counter_device *counter;
+ struct gpio_qenc_priv *priv;
+ bool has_index;
+ int num_signals;
+ int num_synapses;
+ int ret;
+
+ counter = devm_counter_alloc(dev, sizeof(*priv));
+ if (!counter)
+ return -ENOMEM;
+
+ priv = counter_priv(counter);
+ spin_lock_init(&priv->lock);
+
+ priv->gpio_a = devm_gpiod_get(dev, "encoder-a", GPIOD_IN);
+ if (IS_ERR(priv->gpio_a))
+ return dev_err_probe(dev, PTR_ERR(priv->gpio_a),
+ "failed to get encoder-a GPIO\n");
+
+ priv->gpio_b = devm_gpiod_get(dev, "encoder-b", GPIOD_IN);
+ if (IS_ERR(priv->gpio_b))
+ return dev_err_probe(dev, PTR_ERR(priv->gpio_b),
+ "failed to get encoder-b GPIO\n");
+
+ priv->gpio_index = devm_gpiod_get_optional(dev, "encoder-index",
+ GPIOD_IN);
+ if (IS_ERR(priv->gpio_index))
+ return dev_err_probe(dev, PTR_ERR(priv->gpio_index),
+ "failed to get encoder-index GPIO\n");
+
+ has_index = !!priv->gpio_index;
+
+ priv->irq_a = gpiod_to_irq(priv->gpio_a);
+ if (priv->irq_a < 0)
+ return dev_err_probe(dev, priv->irq_a,
+ "failed to get IRQ for encoder-a\n");
+
+ priv->irq_b = gpiod_to_irq(priv->gpio_b);
+ if (priv->irq_b < 0)
+ return dev_err_probe(dev, priv->irq_b,
+ "failed to get IRQ for encoder-b\n");
+
+ if (has_index) {
+ priv->irq_index = gpiod_to_irq(priv->gpio_index);
+ if (priv->irq_index < 0)
+ return dev_err_probe(dev, priv->irq_index,
+ "failed to get IRQ for encoder-index\n");
+ }
+
+ priv->prev_a = gpiod_get_value(priv->gpio_a);
+ priv->prev_b = gpiod_get_value(priv->gpio_b);
+
+ priv->function = GPIO_QENC_FUNC_QUAD_X4;
+ priv->direction = COUNTER_COUNT_DIRECTION_FORWARD;
+
+ num_signals = has_index ? 3 : 2;
+
+ priv->signals[GPIO_QENC_SIGNAL_A].id = GPIO_QENC_SIGNAL_A;
+ priv->signals[GPIO_QENC_SIGNAL_A].name = "Signal A";
+
+ priv->signals[GPIO_QENC_SIGNAL_B].id = GPIO_QENC_SIGNAL_B;
+ priv->signals[GPIO_QENC_SIGNAL_B].name = "Signal B";
+
+ if (has_index) {
+ priv->signals[GPIO_QENC_SIGNAL_INDEX].id =
+ GPIO_QENC_SIGNAL_INDEX;
+ priv->signals[GPIO_QENC_SIGNAL_INDEX].name = "Index";
+ }
+
+ num_synapses = num_signals;
+
+ priv->synapses[0].actions_list = gpio_qenc_synapse_actions;
+ priv->synapses[0].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions);
+ priv->synapses[0].signal = &priv->signals[GPIO_QENC_SIGNAL_A];
+
+ priv->synapses[1].actions_list = gpio_qenc_synapse_actions;
+ priv->synapses[1].num_actions = ARRAY_SIZE(gpio_qenc_synapse_actions);
+ priv->synapses[1].signal = &priv->signals[GPIO_QENC_SIGNAL_B];
+
+ if (has_index) {
+ priv->synapses[2].actions_list = gpio_qenc_synapse_actions;
+ priv->synapses[2].num_actions =
+ ARRAY_SIZE(gpio_qenc_synapse_actions);
+ priv->synapses[2].signal =
+ &priv->signals[GPIO_QENC_SIGNAL_INDEX];
+ }
+
+ priv->cnts.id = 0;
+ priv->cnts.name = "Position";
+ priv->cnts.functions_list = gpio_qenc_functions;
+ priv->cnts.num_functions = ARRAY_SIZE(gpio_qenc_functions);
+ priv->cnts.synapses = priv->synapses;
+ priv->cnts.num_synapses = num_synapses;
+ priv->cnts.ext = gpio_qenc_count_ext;
+ priv->cnts.num_ext = ARRAY_SIZE(gpio_qenc_count_ext);
+
+ counter->name = dev_name(dev);
+ counter->parent = dev;
+ counter->ops = &gpio_qenc_ops;
+ counter->signals = priv->signals;
+ counter->num_signals = num_signals;
+ counter->counts = &priv->cnts;
+ counter->num_counts = 1;
+
+ irq_set_status_flags(priv->irq_a, IRQ_NOAUTOEN);
+ ret = devm_request_irq(dev, priv->irq_a, gpio_qenc_a_isr,
+ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
+ "gpio-qenc-a", counter);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to request IRQ for encoder-a\n");
+
+ irq_set_status_flags(priv->irq_b, IRQ_NOAUTOEN);
+ ret = devm_request_irq(dev, priv->irq_b, gpio_qenc_b_isr,
+ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
+ "gpio-qenc-b", counter);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to request IRQ for encoder-b\n");
+
+ if (has_index) {
+ irq_set_status_flags(priv->irq_index, IRQ_NOAUTOEN);
+ ret = devm_request_irq(dev, priv->irq_index,
+ gpio_qenc_index_isr,
+ IRQF_TRIGGER_RISING,
+ "gpio-qenc-index", counter);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to request IRQ for encoder-index\n");
+ }
+
+ ret = devm_counter_add(dev, counter);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to add counter\n");
+
+ dev_info(dev, "GPIO quadrature encoder registered (signals: A, B%s)\n",
+ has_index ? ", Index" : "");
+
+ return 0;
+}
+
+static const struct of_device_id gpio_qenc_of_match[] = {
+ { .compatible = "gpio-quadrature-encoder" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, gpio_qenc_of_match);
+
+static struct platform_driver gpio_qenc_driver = {
+ .probe = gpio_qenc_probe,
+ .driver = {
+ .name = "gpio-quadrature-encoder",
+ .of_match_table = gpio_qenc_of_match,
+ },
+};
+module_platform_driver(gpio_qenc_driver);
+
+MODULE_ALIAS("platform:gpio-quadrature-encoder");
+MODULE_AUTHOR("Wadim Mueller <wafgo01@gmail.com>");
+MODULE_DESCRIPTION("GPIO-based quadrature encoder counter driver");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("COUNTER");
--
2.52.0
^ permalink raw reply related
* Re: [PATCH v2 14/21] drm/panel: jadard-jd9365da-h3: support Waveshare round DSI panels
From: Linus Walleij @ 2026-04-19 20:16 UTC (permalink / raw)
To: Dmitry Baryshkov
Cc: Neil Armstrong, Jessica Zhang, David Airlie, Simona Vetter,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Cong Yang, Ondrej Jirman,
Javier Martinez Canillas, Jagan Teki, Liam Girdwood, Mark Brown,
Bartosz Golaszewski, dri-devel, devicetree, linux-kernel,
linux-gpio, Riccardo Mereu
In-Reply-To: <20260411-waveshare-dsi-touch-v2-14-75cdbeac5156@oss.qualcomm.com>
On Sat, Apr 11, 2026 at 2:11 PM Dmitry Baryshkov
<dmitry.baryshkov@oss.qualcomm.com> wrote:
> Add configuration for Waveshare 3.4" and 4.0" round DSI panels using
> JD9365 controller.
>
> Tested-by: Riccardo Mereu <r.mereu@arduino.cc>
> Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Yours,
Linus Walleij
^ permalink raw reply
* Re: [PATCH v2 15/21] drm/panel: jadard-jd9365da-h3: support Waveshare WXGA DSI panels
From: Linus Walleij @ 2026-04-19 20:16 UTC (permalink / raw)
To: Dmitry Baryshkov
Cc: Neil Armstrong, Jessica Zhang, David Airlie, Simona Vetter,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Cong Yang, Ondrej Jirman,
Javier Martinez Canillas, Jagan Teki, Liam Girdwood, Mark Brown,
Bartosz Golaszewski, dri-devel, devicetree, linux-kernel,
linux-gpio, Riccardo Mereu
In-Reply-To: <20260411-waveshare-dsi-touch-v2-15-75cdbeac5156@oss.qualcomm.com>
On Sat, Apr 11, 2026 at 2:11 PM Dmitry Baryshkov
<dmitry.baryshkov@oss.qualcomm.com> wrote:
> Add configuration for several Waveshare 8.0" and 10.1" WXGA DSI panels
> using JD9365 controller
>
> Tested-by: Riccardo Mereu <r.mereu@arduino.cc>
> Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Yours,
Linus Walleij
^ permalink raw reply
* Re: [PATCH v2 16/21] drm/panel: jadard-jd9365da-h3: support Waveshare 720p DSI panels
From: Linus Walleij @ 2026-04-19 20:16 UTC (permalink / raw)
To: Dmitry Baryshkov
Cc: Neil Armstrong, Jessica Zhang, David Airlie, Simona Vetter,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Cong Yang, Ondrej Jirman,
Javier Martinez Canillas, Jagan Teki, Liam Girdwood, Mark Brown,
Bartosz Golaszewski, dri-devel, devicetree, linux-kernel,
linux-gpio, Riccardo Mereu
In-Reply-To: <20260411-waveshare-dsi-touch-v2-16-75cdbeac5156@oss.qualcomm.com>
On Sat, Apr 11, 2026 at 2:11 PM Dmitry Baryshkov
<dmitry.baryshkov@oss.qualcomm.com> wrote:
> Add configuration for Waveshare 9.0" and 10.1" 720p DSI panels using
> JD9365 controller.
>
> Tested-by: Riccardo Mereu <r.mereu@arduino.cc>
> Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Yours,
Linus Walleij
^ permalink raw reply
* Re: [PATCH v2 17/21] drm/panel: ilitek-ili9881c: support Waveshare 7.0" DSI panel
From: Linus Walleij @ 2026-04-19 20:17 UTC (permalink / raw)
To: Dmitry Baryshkov
Cc: Neil Armstrong, Jessica Zhang, David Airlie, Simona Vetter,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Cong Yang, Ondrej Jirman,
Javier Martinez Canillas, Jagan Teki, Liam Girdwood, Mark Brown,
Bartosz Golaszewski, dri-devel, devicetree, linux-kernel,
linux-gpio
In-Reply-To: <20260411-waveshare-dsi-touch-v2-17-75cdbeac5156@oss.qualcomm.com>
On Sat, Apr 11, 2026 at 2:11 PM Dmitry Baryshkov
<dmitry.baryshkov@oss.qualcomm.com> wrote:
> Enable support for Waveshare 7.0" DSI TOUCH-A panel. It requires
> additional voltage regulator, iovcc.
>
> Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Yours,
Linus Walleij
^ permalink raw reply
* Re: [PATCH v2 07/21] drm/panel: himax-hx83102: support Waveshare 12.3" DSI panel
From: Linus Walleij @ 2026-04-19 20:18 UTC (permalink / raw)
To: Dmitry Baryshkov
Cc: Neil Armstrong, Jessica Zhang, David Airlie, Simona Vetter,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Cong Yang, Ondrej Jirman,
Javier Martinez Canillas, Jagan Teki, Liam Girdwood, Mark Brown,
Bartosz Golaszewski, dri-devel, devicetree, linux-kernel,
linux-gpio
In-Reply-To: <20260411-waveshare-dsi-touch-v2-7-75cdbeac5156@oss.qualcomm.com>
On Sat, Apr 11, 2026 at 2:11 PM Dmitry Baryshkov
<dmitry.baryshkov@oss.qualcomm.com> wrote:
> Add support for the Waveshare 12.3" DSI TOUCH-A panel. According to the
> vendor driver, it uses different mode_flags, so let the panel
> descriptions override driver-wide defaults.
>
> Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Acked-by: Linus Walleij <linusw@kernel.org>
Yours,
Linus Walleij
^ permalink raw reply
* Re: [PATCH RFC 06/10] arm64: dts: qcom: msm8939-asus-z00t: add Venus
From: André Apitzsch @ 2026-04-19 20:26 UTC (permalink / raw)
To: Dmitry Baryshkov, Erikas Bitovtas
Cc: Konrad Dybcio, Bryan O'Donoghue, Vikash Garodia,
Dikshita Agarwal, Mauro Carvalho Chehab, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson, Konrad Dybcio,
Michael Turquette, Stephen Boyd, linux-media, linux-arm-msm,
devicetree, linux-kernel, linux-clk, ~postmarketos/upstreaming,
phone-devel
In-Reply-To: <37poakqgqhsuavvrm2dyzwk36syyq44o4cfdsylkzwsupbh2yt@ycdvyrxgnrcs>
Hi Dmitry,
Am Samstag, dem 18.04.2026 um 02:40 +0300 schrieb Dmitry Baryshkov:
> On Thu, Apr 16, 2026 at 07:57:30PM +0300, Erikas Bitovtas wrote:
> >
> >
> > On 4/16/26 6:17 PM, Konrad Dybcio wrote:
> > > On 4/16/26 3:43 PM, Erikas Bitovtas wrote:
> > > > Enable Venus video encoder/decoder for Asus ZenFone 2
> > > > Laser/Selfie.
> > > >
> > > > Signed-off-by: Erikas Bitovtas <xerikasxx@gmail.com>
> > > > ---
> > > > arch/arm64/boot/dts/qcom/msm8939-asus-z00t.dts | 8 ++++++++
> > > > 1 file changed, 8 insertions(+)
> > > >
> > > > diff --git a/arch/arm64/boot/dts/qcom/msm8939-asus-z00t.dts
> > > > b/arch/arm64/boot/dts/qcom/msm8939-asus-z00t.dts
> > > > index 90e966242720..231a3e9c1929 100644
> > > > --- a/arch/arm64/boot/dts/qcom/msm8939-asus-z00t.dts
> > > > +++ b/arch/arm64/boot/dts/qcom/msm8939-asus-z00t.dts
> > > > @@ -267,6 +267,14 @@ &usb_hs_phy {
> > > > extcon = <&usb_id>;
> > > > };
> > > >
> > > > +&venus {
> > > > + status = "okay";
> > >
> > > You need a firmware path here
> >
> > When I tested Venus on my device, it loaded without one specified -
> > msm-firmware-loader creates a symbolic link from modem partition
> > for firmware. Additionally, none of the MSM8916 devices seem to
> > include a firmware name. Has something changed since then?
>
> Us becoming more strict? Or more caring? The default file paths are
> supposed to be used for unfused devices. So if they don't work with
> yours (most likely they don't), please add firmware-name:
>
> firmware-name = "qcom/msm8916/Asus/z00t/venus.mbn";
For BQ M5 (msm8939-longcheer-l9100), venus works with the firmware
provided by linux-firmware [1] and the default path (here "qcom/venus-
1.8/venus.mbn").
Just to be clear, should the firmware-name still be added in this case,
even if it is the default one?
Best regards,
André
[1] https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/tree/qcom/venus-1.8
^ permalink raw reply
* Re: [PATCH 0/4] Add hstimer support for H616 and T113-S3
From: Andre Przywara @ 2026-04-19 20:55 UTC (permalink / raw)
To: Michal Piekos
Cc: Daniel Lezcano, Thomas Gleixner, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
Maxime Ripard, linux-kernel, devicetree, linux-arm-kernel,
linux-sunxi
In-Reply-To: <20260419-h616-t113s-hstimer-v1-0-1af74ebef7c5@mmpsystems.pl>
On Sun, 19 Apr 2026 14:46:06 +0200
Michal Piekos <michal.piekos@mmpsystems.pl> wrote:
Hi Michal,
> Add support for Allwinner H616 high speed timer in sun5i hstimer driver
> and describe corresponding nodes in dts for H616 and T113-S3.
>
> H616 uses same model as existing driver except register shift compared
> to older variants.
>
> Added register layout abstraction in the driver, extended the binding
> with new compatibles and wired up dts nodes for H616 and T113-S3 which
> uses H616 as fallback compatible.
Can you say *why* we need this? IIUC Linux only ever uses one clock
source, and selects the (non-optional) Generic Timer (aka arch timer)
for that? So can you say what this hstimer clock source adds? I guess
higher resolution, but what is your use case, so why would you need the
200 MHz? And does this offset the higher access cost of an MMIO
access, compared to the arch timer's sysreg based access? Also, IIUC,
people would need to manually select this as the clocksource, why and
when would they do so? (Given they even know about it in the first
place).
Also the hstimer hasn't been used since the A20, so nobody seemed to
have missed it meanwhile?
Cheers,
Andre
>
> Signed-off-by: Michal Piekos <michal.piekos@mmpsystems.pl>
> ---
> Michal Piekos (4):
> dt-bindings: timer: allwinner,sun5i-a13-hstimer: add H616 and T113-S3
> clocksource/drivers/sun5i: add H616 hstimer support
> arm64: dts: allwinner: h616: add hstimer node
> arm: dts: allwinner: t113s: add hstimer node
>
> .../timer/allwinner,sun5i-a13-hstimer.yaml | 8 +++-
> arch/arm/boot/dts/allwinner/sun8i-t113s.dtsi | 12 +++++
> arch/arm64/boot/dts/allwinner/sun50i-h616.dtsi | 9 ++++
> drivers/clocksource/timer-sun5i.c | 56 +++++++++++++++++++---
> 4 files changed, 78 insertions(+), 7 deletions(-)
> ---
> base-commit: faeab166167f5787719eb8683661fd41a3bb1514
> change-id: 20260413-h616-t113s-hstimer-62939948f91c
>
> Best regards,
^ permalink raw reply
* Re: [PATCH v2 3/4] gpio: realtek: Add driver for Realtek DHC RTD1625 SoC
From: Linus Walleij @ 2026-04-19 21:19 UTC (permalink / raw)
To: Yu-Chun Lin [林祐君], Michael Walle
Cc: Bartosz Golaszewski, linux-gpio@vger.kernel.org,
devicetree@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-arm-kernel@lists.infradead.org,
linux-realtek-soc@lists.infradead.org,
CY_Huang[黃鉦晏],
Stanley Chang[昌育德],
James Tai [戴志峰], robh@kernel.org,
krzk+dt@kernel.org, conor+dt@kernel.org, afaerber@suse.com,
TY_Chang[張子逸]
In-Reply-To: <52bf9ce2b7754af8af69b0afee0d07b2@realtek.com>
Hi Yu-Chun,
On Fri, Apr 10, 2026 at 11:39 AM Yu-Chun Lin [林祐君]
<eleanor.lin@realtek.com> wrote:
> We did look into gpio-mmio and gpio-regmap, but they are not quite suitable for
> our platform due to the specific hardware design:
>
> 1. Per-GPIO Dedicated Registers: Unlike typical GPIO controllers that pack 32 pins
> into a single 32-bit register (1 bit per pin), our hardware uses a dedicated 32-bit
> register for each individual GPIO. This single register controls the
> input/output state, direction, and interrupt trigger type for that specific pin.
Isn't that attainable by:
- setting .ngpio_per_reg to 1 in struct gpio_regmap_config
- extend .reg_mask_xlate callback with an enum for each operation
(need to change all users of the .reg_mask_xlate callback but
who cares, they are not many):
e.g.
enum gpio_regmap_operation {
GPIO_REGMAP_GET_OP,
GPIO_REGMAP_SET_OP,
GPIO_REGMAP_SET_WITH_CLEAR_OP,
GPIO_REGMAP_GET_DIR_OP,
GPIO_REGMAP_SET_DIR_OP,
};
int (*reg_mask_xlate)(struct gpio_regmap *gpio,
enum_gpio_regmap_operation op,
unsigned int base,
unsigned int offset, unsigned int *reg,
unsigned int *mask);
This way .reg_mask_xlate() can hit different bits in the returned
*mask depending on operation and it will be find to pack all of
the bits into one 32bit register.
Added Michael Walle to the the thread, he will know if this is a
good idea.
> 2. Write-Enable (WREN) Mask Mechanism: Our hardware requires a specific Write-Enable
> mask to be written simultaneously when updating the register values.
Which is to just set bit 31.
With the above scheme your .reg_mask_xlate callback can just set bit 31
no matter what operating you're doing. Piece of cake.
> 3. Hardware Debounce: We also need to support hardware debounce settings per pin,
> which requires custom configuration via set_config mapped to these specific per-pin
> registers.
Just add a version of an optional .set_config() call to gpio-regmap.c
to handle this using .reg_mask_xlate() per above and add a new
GPIO_REGMAP_CONFIG_OP to the above enum, problem solved.
If it seems too hard I can write patch 1 & 2 adding this infrastructure
but I bet you can easily see what can be done with gpio-regmap.c
here provided Michael W approves the idea.
Yours,
Linus Walleij
^ permalink raw reply
* Re: [PATCH 1/4] dt-bindings: timer: allwinner,sun5i-a13-hstimer: add H616 and T113-S3
From: Andre Przywara @ 2026-04-19 21:21 UTC (permalink / raw)
To: Michal Piekos
Cc: Daniel Lezcano, Thomas Gleixner, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
Maxime Ripard, linux-kernel, devicetree, linux-arm-kernel,
linux-sunxi
In-Reply-To: <20260419-h616-t113s-hstimer-v1-1-1af74ebef7c5@mmpsystems.pl>
On Sun, 19 Apr 2026 14:46:07 +0200
Michal Piekos <michal.piekos@mmpsystems.pl> wrote:
> H616 is compatible with the existing sun5i binding, but
> require its own compatible string to differentiate register offsets.
Just a nit: if the register offsets are different, then it's not
compatible, not even with the binding. So just say something like "they
are similar, but with different register offsets".
> T113-S3 uses same offsets as H616.
So it looks like (somewhat naturally) this is true for D1 as well? And
since that SoC was the first, we use "sun20i-d1" as the compatible
string prefix for this SoC's devices. I think we should follow suit
here and name that similarly.
>
> Add allwinner,sun50i-h616-hstimer
> Add allwinner,sun8i-t113s-hstimer with fallback to
> allwinner,sun50i-h616-hstimer
> Extend schema condition for interrupts to cover H616 compatible variant.
>
> Signed-off-by: Michal Piekos <michal.piekos@mmpsystems.pl>
> ---
> .../devicetree/bindings/timer/allwinner,sun5i-a13-hstimer.yaml | 8 +++++++-
> 1 file changed, 7 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/devicetree/bindings/timer/allwinner,sun5i-a13-hstimer.yaml b/Documentation/devicetree/bindings/timer/allwinner,sun5i-a13-hstimer.yaml
> index f1853daec2f9..bb60a85dc34b 100644
> --- a/Documentation/devicetree/bindings/timer/allwinner,sun5i-a13-hstimer.yaml
> +++ b/Documentation/devicetree/bindings/timer/allwinner,sun5i-a13-hstimer.yaml
> @@ -15,9 +15,13 @@ properties:
> oneOf:
> - const: allwinner,sun5i-a13-hstimer
> - const: allwinner,sun7i-a20-hstimer
> + - const: allwinner,sun50i-h616-hstimer
> - items:
> - const: allwinner,sun6i-a31-hstimer
> - const: allwinner,sun7i-a20-hstimer
> + - items:
> + - const: allwinner,sun8i-t113s-hstimer
> + - const: allwinner,sun50i-h616-hstimer
>
> reg:
> maxItems: 1
> @@ -45,7 +49,9 @@ required:
> if:
> properties:
> compatible:
> - const: allwinner,sun5i-a13-hstimer
> + enum:
> + - allwinner,sun5i-a13-hstimer
> + - allwinner,sun50i-h616-hstimer
IIUC this just matches the H616, but wouldn't match the T113/D1? And
there is some construct with "contains" to match for fallback
compatibles?
Cheers,
Andre
>
> then:
> properties:
>
^ permalink raw reply
* [PATCH v3 0/5] iio: magnetometer: add driver for QST QMC5883P
From: Hardik Phalet @ 2026-04-19 22:32 UTC (permalink / raw)
To: gregkh, jic23
Cc: andy, conor+dt, devicetree, dlechner, krzk+dt, linux-iio,
linux-kernel, linux-staging, me, nuno.sa, robh, skhan,
Hardik Phalet, Hardik Phalet
This series adds an IIO driver for the QST QMC5883P, a 3-axis
anisotropic magneto-resistive (AMR) magnetometer with a 16-bit ADC,
communicating over I2C. To my knowledge there is no existing
upstream driver for this device (see "Prior-art register-map check"
below).
The driver supports:
- Raw magnetic field readings on X, Y and Z axes
- Four full-scale ranges (+/-2 G, +/-8 G, +/-12 G, +/-30 G),
selectable via IIO_CHAN_INFO_SCALE
- Four output data rates (10, 50, 100, 200 Hz), selectable via
IIO_CHAN_INFO_SAMP_FREQ
- Four oversampling ratios (1, 2, 4, 8), selectable via
IIO_CHAN_INFO_OVERSAMPLING_RATIO
- Runtime PM with a 2 s autosuspend delay
- System suspend/resume delegated to the runtime callbacks
Regmap with an rbtree cache is used throughout. CTRL_1 and CTRL_2
bit fields are accessed via regmap_field to avoid read-modify-write
races. The STATUS register is marked precious so regmap never reads
it speculatively and clears the DRDY/OVFL bits unexpectedly.
The probe-time init sequence is: soft reset, wait 300 us for POR
to complete, deassert reset, drop the register cache so subsequent
RMW writes read fresh values, then enter normal mode. 300 us
comfortably covers the 250 us POR time given in the datasheet.
Patches:
1/5 - dt-bindings: vendor-prefixes: Add QST Corporation
2/5 - dt-bindings: iio: magnetometer: QSTCORP QMC5883P
3/5 - iio: magnetometer: add driver for QST QMC5883P
4/5 - iio: magnetometer: qmc5883p: add oversampling ratio support
5/5 - iio: magnetometer: qmc5883p: add PM support
Patches 4 and 5 are split out from the main driver so that the core
(1-3) can be reviewed and picked independently, per review feedback
on v2. 4/5 exposes the CTRL_1 OSR field via
IIO_CHAN_INFO_OVERSAMPLING_RATIO. 5/5 adds runtime PM that puts the
chip into MODE_SUSPEND when idle and wakes it to MODE_NORMAL on
demand.
Changes in v3
=============
Addressing review feedback on v2:
- Moved the driver out of staging and into drivers/iio/magnetometer/
(Greg Kroah-Hartman).
- Changed the vendor prefix from "qst" to "qstcorp" to match the
manufacturer's domain (qstcorp.com) (Krzysztof Kozlowski).
- Subject of the binding patch no longer says "Add binding for";
"dt-bindings:" already conveys that (Krzysztof Kozlowski).
- Dropped the redundant last sentence of the binding commit message
(Krzysztof Kozlowski).
- VDD supply is now made required (Krzysztof Kozlowski).
- Split runtime PM + system sleep handling out of the core driver
patch into its own patch (5/5), so the core driver can be reviewed
independently (David Lechner).
- Split oversampling-ratio support out into its own patch (4/5)
(David Lechner).
- Dropped the custom downsampling_ratio sysfs attribute entirely.
The datasheet describes OSR2 only as "another filter ... depth can
be adjusted through OSR2", with no further characterisation, and
no application note clarifying it. Without a precise definition
of what the filter actually does it is not possible to map OSR2
to any existing IIO ABI, so support for it is dropped from this
series (David Lechner).
- qmc5883p_verify_chip_id() -> qmc5883p_read_chip_id() no longer
treats an ID mismatch as a probe failure; the chip-ID check is
informational only (David Lechner).
- qmc5883p_chip_init() no longer programs driver-chosen defaults
for RNG/OSR/DSR/ODR. The hardware defaults are sufficient and the
explicit writes were a development artifact (David Lechner).
- Post-reset delay in qmc5883p_chip_init() uses fsleep() with a
comment citing the 250 us POR time from the datasheet
(David Lechner).
- Timeout in regmap_read_poll_timeout() written as
150 * (MICRO / MILLI) instead of 150000 (David Lechner).
- Channel spec duplication collapsed behind a QMC5883P_CHAN(ch)
macro (David Lechner).
- qmc5883p_rf_init() moved up in probe, before the regulator and
chip-ID reads, so the regmap fields are available by the time
they are needed (David Lechner).
- Trailing comma and extra whitespace in the of_device_id and
i2c_device_id sentinel entries cleaned up (David Lechner).
- Verified that there is no existing driver in drivers/iio/,
drivers/hwmon/, drivers/input/, drivers/staging/iio/ or
drivers/misc/ that matches the QMC5883P register map. Summary
of candidates inspected is included in the "Testing" section
below (Andy Shevchenko).
- Waited ~10 days before sending v3 to allow time for review
(Andy Shevchenko).
Additional v3 changes not directly from review:
- Scale encoding changed from IIO_VAL_FRACTIONAL to
IIO_VAL_INT_PLUS_NANO with a matching write_raw_get_fmt(),
because the IIO core defaults sysfs writes to
IIO_VAL_INT_PLUS_MICRO and was silently truncating nano-precision
writes. The truncation on the 8 G and 2 G entries is documented
in a comment above the scale table.
- STATUS register marked precious (in addition to volatile) so
regmap never reads it speculatively and clears DRDY/OVFL.
- Added regcache_drop_region() after the soft-reset deassert, so
subsequent RMW writes read fresh values rather than cached
pre-reset values.
Changes in v2
=============
- Use get_unaligned_le16() from <linux/unaligned.h> instead of
manual byte-shifting for deserialising axis data (review feedback).
- Fix pm_runtime_* calls in downsampling_ratio_store() to use
data->dev (the i2c parent device) instead of dev (the iio
device), avoiding PM refcount imbalances (review feedback).
- Replace manual pm_runtime_disable() devm action with
devm_pm_runtime_enable(), which avoids a kcfi-violating function
pointer cast (review feedback).
- Move chip suspend into a devm action (qmc5883p_suspend_action)
registered before devm_iio_device_register() so that devres LIFO
ordering guarantees the IIO interface is fully unregistered
before the hardware is put to sleep, closing a race window on
removal (review feedback).
- Drop qmc5883p_remove() and the .remove hook entirely, as the
above devm action subsumes it (review feedback).
- Remove the empty qmc5883p_runtime_idle() stub; passing NULL in
RUNTIME_PM_OPS already provides the correct default behaviour.
- Add regulator support: use devm_regulator_get_enable_optional()
for the vdd-supply documented in the dt-binding, with a 50 ms
post-enable delay per datasheet section 5.3 (PSUP ramp + POR
time) (review feedback).
- Reinitialise the chip in qmc5883p_system_resume() via
qmc5883p_chip_init() followed by regcache_mark_dirty() +
regcache_sync(), so that the driver recovers correctly if the
regulator was physically cut during system suspend and POR
reset all registers (review feedback).
Links
=====
v1: https://lore.kernel.org/all/20260409162308.2590385-1-hardik.phalet@pm.me/
v2: https://lore.kernel.org/all/20260409210639.3197576-1-hardik.phalet@pm.me/
Testing
=======
Hardware
A GY-271 HM-246 breakout (this board is a QMC5883P, not a QMC5883L,
despite what some vendors put on the silkscreen), connected over
I2C bus 1 to a Raspberry Pi 4B running a mainline aarch64 kernel.
The chip enumerates at address 0x2C via i2cdetect, and CHIP_ID
(register 0x00) reads back 0x80 as expected.
Prior-art register-map check (for Andy)
I grepped drivers/iio/magnetometer/, drivers/hwmon/,
drivers/input/misc/, drivers/staging/iio/ and drivers/misc/ for
the distinctive offsets 0x09 (STATUS) and 0x0A (CTRL_1), narrowed
to files containing both, and manually compared each candidate's
register layout and control-bit encoding against the QMC5883P.
Closest candidates:
ak8975.c - four register offsets coincide (0x00, 0x09, 0x0A,
0x0B) but the data registers sit at 0x03-0x08
(shifted +2 vs QMC5883P's 0x01-0x06), DRDY is in
ST1 at 0x02 rather than STATUS at 0x09, and CNTL
encodes a 4-bit mode only - no packed ODR/OSR/range
fields.
hmc5843.c - STATUS matches at 0x09, but 0x0A is a read-only ID
register, configuration spans 0x00-0x02 rather than
a single CTRL_1 byte, and data is MSB-first at 0x03
in X/Z/Y order.
af8133j.c - 0x0A and 0x0B carry mode and range, but STATUS is
at 0x02, data starts at 0x03, and the mode field is
2-valued (standby/work) rather than 4-valued.
mmc35240.c - data at 0x00-0x05 overlaps, but STATUS and control
land at 0x06-0x08.
No overlap worth discussing: mmc5633, mag3110, tlv493d, tmag5273,
bmc150_magn, rm3100, yamaha-yas530, st_magn, si7210, als31300. No
magnetometer driver under drivers/hwmon/, drivers/input/misc/ or
drivers/staging/.
Conclusion: no existing driver can be extended to cover the
QMC5883P without restructuring its register addressing and
control-bit model. A new driver is warranted.
Functional testing on v3
- Chip ID read: 0x80 (matches datasheet).
- Raw axis reads: in_magn_{x,y,z}_raw return stable s16 values
and track manual reorientation of the board.
- Scale: all four ranges (+/-2/8/12/30 G) selectable via
in_magn_scale; in_magn_scale_available lists all four; sysfs
write-back round-trips cleanly at nano precision.
- Sampling frequency: 10/50/100/200 Hz all selectable via
in_magn_sampling_frequency; _available lists all four.
- Oversampling ratio (patch 4): 1/2/4/8 selectable via
in_magn_oversampling_ratio; _available lists all four.
- DRDY polling: verified STATUS.DRDY asserts and clears on read,
and that OVFL is captured in the same read as DRDY.
- Soft reset: register state after qmc5883p_chip_init() matches
the datasheet defaults; regcache_drop_region() confirmed by
observing fresh values being read on the first post-reset RMW.
- Runtime PM (patch 5): power/runtime_status transitions to
"suspended" after the 2 s autosuspend delay (MODE_SUSPEND on
the wire, verified by i2cdump); next sysfs read resumes the
device and returns valid data.
- System sleep: echo mem > /sys/power/state (s2idle on the Pi)
followed by wake; readings are valid after resume.
- Unbind: echo <dev> > /sys/bus/i2c/drivers/qmc5883p/unbind
leaves the chip in MODE_SUSPEND, confirming the devm LIFO
teardown order.
- Build: CONFIG_QMC5883P=y and =m both clean; W=1 clean on
aarch64; sparse clean; checkpatch --strict clean.
- dt_binding_check: passes for patch 2/5.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
Hardik Phalet (5):
dt-bindings: vendor-prefixes: Add QST Corporation
dt-bindings: iio: magnetometer: QSTCORP QMC5883P
iio: magnetometer: add driver for QST QMC5883P
iio: magnetometer: qmc5883p: add oversampling ratio support
iio: magnetometer: qmc5883p: add PM support
.../iio/magnetometer/qstcorp,qmc5883p.yaml | 48 ++
.../devicetree/bindings/vendor-prefixes.yaml | 2 +
MAINTAINERS | 7 +
drivers/iio/magnetometer/Kconfig | 11 +
drivers/iio/magnetometer/Makefile | 2 +
drivers/iio/magnetometer/qmc5883p.c | 673 +++++++++++++++++++++
6 files changed, 743 insertions(+)
---
base-commit: d2a4ec19d2a2e54c23b5180e939994d3da4a6b91
change-id: 20260418-qmc5883p-driver-dcc74bd4a789
Best regards,
--
Hardik Phalet <hardik.phalet@pm.me>
^ permalink raw reply
* [PATCH v3 1/5] dt-bindings: vendor-prefixes: Add QST Corporation
From: Hardik Phalet @ 2026-04-19 22:32 UTC (permalink / raw)
To: gregkh, jic23
Cc: andy, conor+dt, devicetree, dlechner, krzk+dt, linux-iio,
linux-kernel, linux-staging, me, nuno.sa, robh, skhan,
Hardik Phalet, Hardik Phalet
In-Reply-To: <20260420-qmc5883p-driver-v3-0-da1e97088f8b@pm.me>
Add the vendor prefix 'qstcorp' for QST Corporation, a manufacturer of
MEMS sensors.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index ee7fd3cfe203..4ecf438f1a4a 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1337,6 +1337,8 @@ patternProperties:
description: Shenzhen QiShenglong Industrialist Co., Ltd.
"^qnap,.*":
description: QNAP Systems, Inc.
+ "^qstcorp,.*":
+ description: QST Corporation
"^quanta,.*":
description: Quanta Computer Inc.
"^radxa,.*":
--
2.53.0
^ permalink raw reply related
* [PATCH v3 2/5] dt-bindings: iio: magnetometer: QSTCORP QMC5883P
From: Hardik Phalet @ 2026-04-19 22:32 UTC (permalink / raw)
To: gregkh, jic23
Cc: andy, conor+dt, devicetree, dlechner, krzk+dt, linux-iio,
linux-kernel, linux-staging, me, nuno.sa, robh, skhan,
Hardik Phalet, Hardik Phalet
In-Reply-To: <20260420-qmc5883p-driver-v3-0-da1e97088f8b@pm.me>
Add the device tree binding document for the QST QMC5883P, a 3-axis
anisotropic magneto-resistive (AMR) sensor with a 16-bit ADC that
communicates over I2C.
Add a MAINTAINERS entry for the QSTCORP QMC5883P devicetree binding.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
.../iio/magnetometer/qstcorp,qmc5883p.yaml | 48 ++++++++++++++++++++++
MAINTAINERS | 6 +++
2 files changed, 54 insertions(+)
diff --git a/Documentation/devicetree/bindings/iio/magnetometer/qstcorp,qmc5883p.yaml b/Documentation/devicetree/bindings/iio/magnetometer/qstcorp,qmc5883p.yaml
new file mode 100644
index 000000000000..72cc3fef2226
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/magnetometer/qstcorp,qmc5883p.yaml
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/magnetometer/qstcorp,qmc5883p.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: QSTCORP QMC5883P 3-axis magnetometer
+
+maintainers:
+ - Hardik Phalet <hardik.phalet@pm.me>
+
+description:
+ The QMC5883P is a 3-axis anisotropic magneto-resistive (AMR) sensor with a
+ 16-bit ADC. It communicates over I2C (standard and fast modes) and is
+ targeted at compass, navigation, and industrial applications.
+
+properties:
+ compatible:
+ const: qstcorp,qmc5883p
+
+ reg:
+ maxItems: 1
+ description: I2C address of the device; the default address is 0x2c
+
+ vdd-supply:
+ description:
+ VDD power supply (2.5 V to 3.6 V). Powers all internal analog and
+ digital functional blocks.
+
+required:
+ - compatible
+ - reg
+ - vdd-supply
+
+additionalProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ magnetometer@2c {
+ compatible = "qstcorp,qmc5883p";
+ reg = <0x2c>;
+ vdd-supply = <&vdd_3v3>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 48fda1f8332e..d41f6b33d0e5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21554,6 +21554,12 @@ F: Documentation/networking/device_drivers/ethernet/freescale/dpaa2/overview.rst
F: drivers/bus/fsl-mc/
F: include/uapi/linux/fsl_mc.h
+QSTCORP QMC5883P MAGNETOMETER DRIVER
+M: Hardik Phalet <hardik.phalet@pm.me>
+L: linux-iio@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/iio/magnetometer/qstcorp,qmc5883p.yaml
+
QT1010 MEDIA DRIVER
L: linux-media@vger.kernel.org
S: Orphan
--
2.53.0
^ permalink raw reply related
* [PATCH v3 3/5] iio: magnetometer: add driver for QST QMC5883P
From: Hardik Phalet @ 2026-04-19 22:32 UTC (permalink / raw)
To: gregkh, jic23
Cc: andy, conor+dt, devicetree, dlechner, krzk+dt, linux-iio,
linux-kernel, linux-staging, me, nuno.sa, robh, skhan,
Hardik Phalet, Hardik Phalet
In-Reply-To: <20260420-qmc5883p-driver-v3-0-da1e97088f8b@pm.me>
Add an IIO driver for the QST QMC5883P, a 3-axis anisotropic
magneto-resistive (AMR) magnetometer with a 16-bit ADC, communicating
over I2C. There is no existing upstream driver for this device.
The driver supports:
- Raw magnetic field readings on X, Y and Z axes
- Four full-scale ranges (+/-2 G, +/-8 G, +/-12 G, +/-30 G) selectable
via IIO_CHAN_INFO_SCALE
- Output data rate configurable via IIO_CHAN_INFO_SAMP_FREQ (10, 50,
100, 200 Hz)
- vdd-supply regulator management
Regmap with an rbtree cache is used throughout. CTRL_1 and CTRL_2 bit
fields are accessed via regmap_field to avoid read-modify-write races.
The STATUS register is marked precious so regmap never reads it
speculatively and clears the DRDY/OVFL bits unexpectedly.
The probe-time init sequence is: soft reset, wait 300 us for POR
completion, deassert reset, then drop the register cache so subsequent
RMW writes read fresh values from the device. After reset the chip is in
MODE_SUSPEND per datasheet §6.2.4, and is left there; the first
userspace access will wake it via runtime PM (added in a follow-up
patch).
Cleanup is fully devm-managed via devm_regulator_get_enable() and
devm_iio_device_register().
Oversampling ratio and runtime PM are added in follow-up patches.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
MAINTAINERS | 1 +
drivers/iio/magnetometer/Kconfig | 11 +
drivers/iio/magnetometer/Makefile | 2 +
drivers/iio/magnetometer/qmc5883p.c | 574 ++++++++++++++++++++++++++++++++++++
4 files changed, 588 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index d41f6b33d0e5..2fbbe8831a7c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21559,6 +21559,7 @@ M: Hardik Phalet <hardik.phalet@pm.me>
L: linux-iio@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/iio/magnetometer/qstcorp,qmc5883p.yaml
+F: drivers/iio/magnetometer/qmc5883p.c
QT1010 MEDIA DRIVER
L: linux-media@vger.kernel.org
diff --git a/drivers/iio/magnetometer/Kconfig b/drivers/iio/magnetometer/Kconfig
index fb313e591e85..333c5e6f231d 100644
--- a/drivers/iio/magnetometer/Kconfig
+++ b/drivers/iio/magnetometer/Kconfig
@@ -298,4 +298,15 @@ config YAMAHA_YAS530
To compile this driver as a module, choose M here: the module
will be called yamaha-yas.
+config QMC5883P
+ tristate "QMC5883P 3-Axis Magnetometer"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Say yes here to build support for QMC5883P I2C-based
+ 3-axis magnetometer chip.
+
+ To compile this driver as a module, choose M here: the
+ module will be called qmc5883p.
+
endmenu
diff --git a/drivers/iio/magnetometer/Makefile b/drivers/iio/magnetometer/Makefile
index 5bd227f8c120..ff519a055d77 100644
--- a/drivers/iio/magnetometer/Makefile
+++ b/drivers/iio/magnetometer/Makefile
@@ -39,3 +39,5 @@ obj-$(CONFIG_SI7210) += si7210.o
obj-$(CONFIG_TI_TMAG5273) += tmag5273.o
obj-$(CONFIG_YAMAHA_YAS530) += yamaha-yas530.o
+
+obj-$(CONFIG_QMC5883P) += qmc5883p.o
diff --git a/drivers/iio/magnetometer/qmc5883p.c b/drivers/iio/magnetometer/qmc5883p.c
new file mode 100644
index 000000000000..e4a76ae7c2cf
--- /dev/null
+++ b/drivers/iio/magnetometer/qmc5883p.c
@@ -0,0 +1,574 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * qmc5883p.c - QMC5883P magnetometer driver
+ *
+ * Copyright 2026 Hardik Phalet <hardik.phalet@pm.me>
+ *
+ * TODO: add triggered buffer support, PM, OSR, DSR
+ *
+ */
+
+#include <linux/array_size.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/units.h>
+#include <linux/unaligned.h>
+
+/*
+ * Register definition
+ */
+#define QMC5883P_REG_CHIP_ID 0x00
+#define QMC5883P_REG_X_LSB 0x01
+#define QMC5883P_REG_X_MSB 0x02
+#define QMC5883P_REG_Y_LSB 0x03
+#define QMC5883P_REG_Y_MSB 0x04
+#define QMC5883P_REG_Z_LSB 0x05
+#define QMC5883P_REG_Z_MSB 0x06
+#define QMC5883P_REG_STATUS 0x09
+#define QMC5883P_REG_CTRL_1 0x0A
+#define QMC5883P_REG_CTRL_2 0x0B
+
+/*
+ * Value definition
+ */
+#define QMC5883P_MODE_SUSPEND 0x00
+#define QMC5883P_MODE_NORMAL 0x01
+#define QMC5883P_MODE_SINGLE 0x02
+#define QMC5883P_MODE_CONTINUOUS 0x03
+
+/*
+ * Output data rate
+ */
+#define QMC5883P_ODR_10 0x00
+#define QMC5883P_ODR_50 0x01
+#define QMC5883P_ODR_100 0x02
+#define QMC5883P_ODR_200 0x03
+
+/*
+ * Oversampling rate
+ */
+#define QMC5883P_OSR_8 0x00
+#define QMC5883P_OSR_4 0x01
+#define QMC5883P_OSR_2 0x02
+#define QMC5883P_OSR_1 0x03
+
+#define QMC5883P_RNG_30G 0x00
+#define QMC5883P_RNG_12G 0x01
+#define QMC5883P_RNG_08G 0x02
+#define QMC5883P_RNG_02G 0x03
+
+#define QMC5883P_DRDY_POLL_US 1000
+
+#define QMC5883P_CHIP_ID 0x80
+
+#define QMC5883P_STATUS_DRDY BIT(0)
+#define QMC5883P_STATUS_OVFL BIT(1)
+
+struct qmc5883p_rf {
+ struct regmap_field *osr;
+ struct regmap_field *odr;
+ struct regmap_field *mode;
+ struct regmap_field *rng;
+ struct regmap_field *sftrst;
+ struct regmap_field *chip_id;
+};
+
+struct qmc5883p_data {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex mutex; /* protects regmap and rf field accesses */
+ struct qmc5883p_rf rf;
+};
+
+enum qmc5883p_channels {
+ AXIS_X = 0,
+ AXIS_Y,
+ AXIS_Z,
+};
+
+/*
+ * Scale factors in nT/LSB for IIO_VAL_INT_PLUS_NANO, derived from datasheet
+ * Table 2 sensitivities (LSB/G) converted to LSB/T (1 G = 1e-4 T):
+ * sensitivity_T = sensitivity_G * 10000
+ * scale_nT = 1e9 / sensitivity_T
+ *
+ * The 8G and 2G entries truncate 26.666... and 6.666... nT/LSB respectively;
+ * IIO_VAL_INT_PLUS_NANO cannot carry the exact rationals, but the chosen
+ * values match what IIO_VAL_FRACTIONAL would have rendered and therefore
+ * round-trip cleanly through sysfs write back.
+ *
+ * Index matches register value: RNG<1:0> = 0b00..0b11
+ */
+static const int qmc5883p_scale[][2] = {
+ [QMC5883P_RNG_30G] = { 0, 100 },
+ [QMC5883P_RNG_12G] = { 0, 40 },
+ [QMC5883P_RNG_08G] = { 0, 26 },
+ [QMC5883P_RNG_02G] = { 0, 6 },
+};
+
+static const int qmc5883p_odr[] = {
+ [QMC5883P_ODR_10] = 10,
+ [QMC5883P_ODR_50] = 50,
+ [QMC5883P_ODR_100] = 100,
+ [QMC5883P_ODR_200] = 200,
+};
+
+static const struct regmap_range qmc5883p_readable_ranges[] = {
+ regmap_reg_range(QMC5883P_REG_CHIP_ID, QMC5883P_REG_Z_MSB),
+ regmap_reg_range(QMC5883P_REG_STATUS, QMC5883P_REG_CTRL_2),
+};
+
+static const struct regmap_range qmc5883p_writable_ranges[] = {
+ regmap_reg_range(QMC5883P_REG_CTRL_1, QMC5883P_REG_CTRL_2),
+};
+
+/*
+ * Volatile registers: hardware updates these independently of the driver.
+ * regmap will never serve these from cache.
+ */
+static const struct regmap_range qmc5883p_volatile_ranges[] = {
+ regmap_reg_range(QMC5883P_REG_X_LSB, QMC5883P_REG_Z_MSB),
+ regmap_reg_range(QMC5883P_REG_STATUS, QMC5883P_REG_STATUS),
+ regmap_reg_range(QMC5883P_REG_CTRL_2, QMC5883P_REG_CTRL_2),
+};
+
+/*
+ * Precious registers: reading has a side effect (clears DRDY/OVFL bits).
+ * regmap will never read these speculatively.
+ */
+static const struct regmap_range qmc5883p_precious_ranges[] = {
+ regmap_reg_range(QMC5883P_REG_STATUS, QMC5883P_REG_STATUS),
+};
+
+static const struct regmap_access_table qmc5883p_readable_table = {
+ .yes_ranges = qmc5883p_readable_ranges,
+ .n_yes_ranges = ARRAY_SIZE(qmc5883p_readable_ranges),
+};
+
+static const struct regmap_access_table qmc5883p_writable_table = {
+ .yes_ranges = qmc5883p_writable_ranges,
+ .n_yes_ranges = ARRAY_SIZE(qmc5883p_writable_ranges),
+};
+
+static const struct regmap_access_table qmc5883p_volatile_table = {
+ .yes_ranges = qmc5883p_volatile_ranges,
+ .n_yes_ranges = ARRAY_SIZE(qmc5883p_volatile_ranges),
+};
+
+static const struct regmap_access_table qmc5883p_precious_table = {
+ .yes_ranges = qmc5883p_precious_ranges,
+ .n_yes_ranges = ARRAY_SIZE(qmc5883p_precious_ranges),
+};
+
+static const struct regmap_config qmc5883p_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x0B,
+ .cache_type = REGCACHE_RBTREE,
+ .rd_table = &qmc5883p_readable_table,
+ .wr_table = &qmc5883p_writable_table,
+ .volatile_table = &qmc5883p_volatile_table,
+ .precious_table = &qmc5883p_precious_table,
+};
+
+static const struct reg_field qmc5883p_rf_osr =
+ REG_FIELD(QMC5883P_REG_CTRL_1, 4, 5);
+static const struct reg_field qmc5883p_rf_odr =
+ REG_FIELD(QMC5883P_REG_CTRL_1, 2, 3);
+static const struct reg_field qmc5883p_rf_mode =
+ REG_FIELD(QMC5883P_REG_CTRL_1, 0, 1);
+static const struct reg_field qmc5883p_rf_rng =
+ REG_FIELD(QMC5883P_REG_CTRL_2, 2, 3);
+static const struct reg_field qmc5883p_rf_sftrst =
+ REG_FIELD(QMC5883P_REG_CTRL_2, 7, 7);
+static const struct reg_field qmc5883p_rf_chip_id =
+ REG_FIELD(QMC5883P_REG_CHIP_ID, 0, 7);
+
+/*
+ * qmc5883p_get_measure - read all three axes.
+ * Must be called with data->mutex held.
+ */
+static int qmc5883p_get_measure(struct qmc5883p_data *data, s16 *x, s16 *y,
+ s16 *z)
+{
+ int ret;
+ u8 reg_data[6];
+ unsigned int status;
+
+ /*
+ * Poll the status register until DRDY is set or timeout.
+ * Read the whole register in one shot so that OVFL is captured from
+ * the same read: reading 0x09 clears both DRDY and OVFL, so a second
+ * read would always see OVFL=0.
+ * At ODR=10Hz one period is 100ms; use 150ms as a safe upper bound.
+ */
+ ret = regmap_read_poll_timeout(data->regmap, QMC5883P_REG_STATUS,
+ status, status & QMC5883P_STATUS_DRDY,
+ QMC5883P_DRDY_POLL_US,
+ 150 * (MICRO / MILLI));
+ if (ret)
+ return ret;
+
+ if (status & QMC5883P_STATUS_OVFL) {
+ dev_warn_ratelimited(data->dev,
+ "data overflow, consider reducing field range\n");
+ ret = -ERANGE;
+ return ret;
+ }
+
+ ret = regmap_bulk_read(data->regmap, QMC5883P_REG_X_LSB, reg_data,
+ ARRAY_SIZE(reg_data));
+ if (ret)
+ return ret;
+
+ *x = (s16)get_unaligned_le16(®_data[0]);
+ *y = (s16)get_unaligned_le16(®_data[2]);
+ *z = (s16)get_unaligned_le16(®_data[4]);
+
+ return ret;
+}
+
+static int qmc5883p_read_raw(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan, int *val,
+ int *val2, long mask)
+{
+ s16 x, y, z;
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+ int ret;
+ unsigned int regval;
+
+ guard(mutex)(&data->mutex);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ ret = qmc5883p_get_measure(data, &x, &y, &z);
+ if (ret < 0)
+ return ret;
+ switch (chan->address) {
+ case AXIS_X:
+ *val = x;
+ break;
+ case AXIS_Y:
+ *val = y;
+ break;
+ case AXIS_Z:
+ *val = z;
+ break;
+ }
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE:
+ ret = regmap_field_read(data->rf.rng, ®val);
+ if (ret < 0)
+ return ret;
+ *val = qmc5883p_scale[regval][0];
+ *val2 = qmc5883p_scale[regval][1];
+ return IIO_VAL_INT_PLUS_NANO;
+
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ ret = regmap_field_read(data->rf.odr, ®val);
+ if (ret < 0)
+ return ret;
+ *val = qmc5883p_odr[regval];
+ return IIO_VAL_INT;
+ }
+
+ return -EINVAL;
+}
+
+static int qmc5883p_write_scale(struct qmc5883p_data *data, int val, int val2)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(qmc5883p_scale); i++) {
+ if (qmc5883p_scale[i][0] == val && qmc5883p_scale[i][1] == val2)
+ return regmap_field_write(data->rf.rng, i);
+ }
+
+ return -EINVAL;
+}
+
+static int qmc5883p_write_odr(struct qmc5883p_data *data, int val)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(qmc5883p_odr); i++) {
+ if (qmc5883p_odr[i] == val)
+ return regmap_field_write(data->rf.odr, i);
+ }
+
+ return -EINVAL;
+}
+
+static int qmc5883p_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int val,
+ int val2, long mask)
+{
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+ int ret, restore;
+
+ guard(mutex)(&data->mutex);
+
+ ret = regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+ if (ret)
+ return ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ ret = qmc5883p_write_odr(data, val);
+ break;
+ case IIO_CHAN_INFO_SCALE:
+ ret = qmc5883p_write_scale(data, val, val2);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ restore = regmap_field_write(data->rf.mode, QMC5883P_MODE_NORMAL);
+ if (restore && !ret)
+ ret = restore;
+
+ return ret;
+}
+
+/*
+ * qmc5883p_read_avail - expose available values to userspace.
+ *
+ * Creates the _available sysfs attributes automatically:
+ * in_magn_sampling_frequency_available
+ * in_magn_scale_available
+ */
+static int qmc5883p_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type, int *length,
+ long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *vals = qmc5883p_odr;
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(qmc5883p_odr);
+ return IIO_AVAIL_LIST;
+
+ case IIO_CHAN_INFO_SCALE:
+ *vals = (const int *)qmc5883p_scale;
+ *type = IIO_VAL_INT_PLUS_NANO;
+ *length = ARRAY_SIZE(qmc5883p_scale) * 2;
+ return IIO_AVAIL_LIST;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+/*
+ * Tell the IIO core how to parse sysfs writes. Without this, the core
+ * defaults to IIO_VAL_INT_PLUS_MICRO (6 fractional digits), which would
+ * silently truncate nano-scale writes like "0.000000040" to 0.
+ */
+static int qmc5883p_write_raw_get_fmt(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SCALE:
+ return IIO_VAL_INT_PLUS_NANO;
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return IIO_VAL_INT;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct iio_info qmc5883p_info = {
+ .read_raw = qmc5883p_read_raw,
+ .write_raw = qmc5883p_write_raw,
+ .write_raw_get_fmt = qmc5883p_write_raw_get_fmt,
+ .read_avail = qmc5883p_read_avail,
+};
+
+static int qmc5883p_rf_init(struct qmc5883p_data *data)
+{
+ struct regmap *regmap = data->regmap;
+ struct device *dev = data->dev;
+ struct qmc5883p_rf *rf = &data->rf;
+
+ rf->osr = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_osr);
+ if (IS_ERR(rf->osr))
+ return PTR_ERR(rf->osr);
+
+ rf->odr = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_odr);
+ if (IS_ERR(rf->odr))
+ return PTR_ERR(rf->odr);
+
+ rf->mode = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_mode);
+ if (IS_ERR(rf->mode))
+ return PTR_ERR(rf->mode);
+
+ rf->rng = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_rng);
+ if (IS_ERR(rf->rng))
+ return PTR_ERR(rf->rng);
+
+ rf->sftrst = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_sftrst);
+ if (IS_ERR(rf->sftrst))
+ return PTR_ERR(rf->sftrst);
+
+ rf->chip_id = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_chip_id);
+ if (IS_ERR(rf->chip_id))
+ return PTR_ERR(rf->chip_id);
+
+ return 0;
+}
+
+static int qmc5883p_read_chip_id(struct qmc5883p_data *data)
+{
+ int ret, regval;
+
+ ret = regmap_field_read(data->rf.chip_id, ®val);
+ if (ret)
+ return dev_err_probe(data->dev, ret,
+ "failed to read chip ID\n");
+
+ if (regval != QMC5883P_CHIP_ID)
+ dev_info(data->dev, "unexpected chip ID %#x, expected %#x\n",
+ regval, QMC5883P_CHIP_ID);
+
+ return 0;
+}
+
+#define QMC5883P_CHAN(ch) \
+ { \
+ .type = IIO_MAGN, \
+ .channel2 = IIO_MOD_##ch, \
+ .modified = 1, \
+ .address = AXIS_##ch, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SCALE), \
+ .info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE), \
+ .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_type_available = \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ }
+
+static const struct iio_chan_spec qmc5883p_channels[] = {
+ QMC5883P_CHAN(X),
+ QMC5883P_CHAN(Y),
+ QMC5883P_CHAN(Z),
+};
+
+static int qmc5883p_chip_init(struct qmc5883p_data *data)
+{
+ int ret;
+
+ ret = regmap_field_write(data->rf.sftrst, 1);
+ if (ret)
+ return ret;
+
+ /*
+ * The datasheet does not specify a post-reset delay, but POR
+ * completion takes up to 250 microseconds. Use 300 microseconds
+ * to be safe.
+ */
+ fsleep(300);
+
+ ret = regmap_field_write(data->rf.sftrst, 0);
+ if (ret)
+ return ret;
+
+ /*
+ * Soft reset restored every register to its default. Drop the cache
+ * so subsequent RMW writes read fresh values from the device.
+ */
+ regcache_drop_region(data->regmap, QMC5883P_REG_CHIP_ID,
+ QMC5883P_REG_CTRL_2);
+
+ /* Chip is now in MODE_SUSPEND per datasheet §6.2.4. Leave it there. */
+ return 0;
+}
+
+static int qmc5883p_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct qmc5883p_data *data;
+ struct iio_dev *indio_dev;
+ struct regmap *regmap;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ regmap = devm_regmap_init_i2c(client, &qmc5883p_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(dev, PTR_ERR(regmap),
+ "regmap initialization failed\n");
+
+ data = iio_priv(indio_dev);
+ i2c_set_clientdata(client, indio_dev);
+ data->dev = dev;
+ data->regmap = regmap;
+
+ mutex_init(&data->mutex);
+
+ ret = qmc5883p_rf_init(data);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to initialize regmap fields\n");
+
+ indio_dev->name = "qmc5883p";
+ indio_dev->info = &qmc5883p_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+ indio_dev->channels = qmc5883p_channels;
+ indio_dev->num_channels = ARRAY_SIZE(qmc5883p_channels);
+
+ ret = devm_regulator_get_enable(dev, "vdd");
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to initialize vdd regulator\n");
+
+ /* Datasheet specifies up to 50 ms supply ramp + 250 us POR time. */
+ fsleep(50 * (MICRO / MILLI) + 250);
+
+ ret = qmc5883p_read_chip_id(data);
+ if (ret)
+ return ret;
+
+ ret = qmc5883p_chip_init(data);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to initialize chip\n");
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct of_device_id qmc5883p_of_match[] = {
+ { .compatible = "qstcorp,qmc5883p" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, qmc5883p_of_match);
+
+static const struct i2c_device_id qmc5883p_id[] = {
+ { "qmc5883p", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, qmc5883p_id);
+
+static struct i2c_driver qmc5883p_driver = {
+ .driver = {
+ .name = "qmc5883p",
+ .of_match_table = qmc5883p_of_match,
+ },
+ .probe = qmc5883p_probe,
+ .id_table = qmc5883p_id,
+};
+module_i2c_driver(qmc5883p_driver);
+
+MODULE_AUTHOR("Hardik Phalet <hardik.phalet@pm.me>");
+MODULE_DESCRIPTION("QST QMC5883P 3-axis magnetometer driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH v3 4/5] iio: magnetometer: qmc5883p: add oversampling ratio support
From: Hardik Phalet @ 2026-04-19 22:32 UTC (permalink / raw)
To: gregkh, jic23
Cc: andy, conor+dt, devicetree, dlechner, krzk+dt, linux-iio,
linux-kernel, linux-staging, me, nuno.sa, robh, skhan,
Hardik Phalet, Hardik Phalet
In-Reply-To: <20260420-qmc5883p-driver-v3-0-da1e97088f8b@pm.me>
Expose the CTRL_1 OSR field through IIO_CHAN_INFO_OVERSAMPLING_RATIO so
userspace can select among the four oversampling settings (1, 2, 4, 8)
supported by the device. Read, write and available handlers mirror the
existing SAMP_FREQ plumbing and use the already-present rf.osr regmap
field.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
drivers/iio/magnetometer/qmc5883p.c | 46 ++++++++++++++++++++++++++++++++++---
1 file changed, 43 insertions(+), 3 deletions(-)
diff --git a/drivers/iio/magnetometer/qmc5883p.c b/drivers/iio/magnetometer/qmc5883p.c
index e4a76ae7c2cf..d0e4a1a600b6 100644
--- a/drivers/iio/magnetometer/qmc5883p.c
+++ b/drivers/iio/magnetometer/qmc5883p.c
@@ -4,7 +4,7 @@
*
* Copyright 2026 Hardik Phalet <hardik.phalet@pm.me>
*
- * TODO: add triggered buffer support, PM, OSR, DSR
+ * TODO: add triggered buffer support, PM, DSR
*
*/
@@ -119,6 +119,13 @@ static const int qmc5883p_odr[] = {
[QMC5883P_ODR_200] = 200,
};
+static const int qmc5883p_osr[] = {
+ [QMC5883P_OSR_1] = 1,
+ [QMC5883P_OSR_2] = 2,
+ [QMC5883P_OSR_4] = 4,
+ [QMC5883P_OSR_8] = 8,
+};
+
static const struct regmap_range qmc5883p_readable_ranges[] = {
regmap_reg_range(QMC5883P_REG_CHIP_ID, QMC5883P_REG_Z_MSB),
regmap_reg_range(QMC5883P_REG_STATUS, QMC5883P_REG_CTRL_2),
@@ -277,6 +284,13 @@ static int qmc5883p_read_raw(struct iio_dev *indio_dev,
return ret;
*val = qmc5883p_odr[regval];
return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ ret = regmap_field_read(data->rf.osr, ®val);
+ if (ret < 0)
+ return ret;
+ *val = qmc5883p_osr[regval];
+ return IIO_VAL_INT;
}
return -EINVAL;
@@ -306,6 +320,18 @@ static int qmc5883p_write_odr(struct qmc5883p_data *data, int val)
return -EINVAL;
}
+static int qmc5883p_write_osr(struct qmc5883p_data *data, int val)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(qmc5883p_osr); i++) {
+ if (qmc5883p_osr[i] == val)
+ return regmap_field_write(data->rf.osr, i);
+ }
+
+ return -EINVAL;
+}
+
static int qmc5883p_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan, int val,
int val2, long mask)
@@ -323,6 +349,9 @@ static int qmc5883p_write_raw(struct iio_dev *indio_dev,
case IIO_CHAN_INFO_SAMP_FREQ:
ret = qmc5883p_write_odr(data, val);
break;
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ ret = qmc5883p_write_osr(data, val);
+ break;
case IIO_CHAN_INFO_SCALE:
ret = qmc5883p_write_scale(data, val, val2);
break;
@@ -357,6 +386,12 @@ static int qmc5883p_read_avail(struct iio_dev *indio_dev,
*length = ARRAY_SIZE(qmc5883p_odr);
return IIO_AVAIL_LIST;
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ *vals = qmc5883p_osr;
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(qmc5883p_osr);
+ return IIO_AVAIL_LIST;
+
case IIO_CHAN_INFO_SCALE:
*vals = (const int *)qmc5883p_scale;
*type = IIO_VAL_INT_PLUS_NANO;
@@ -382,6 +417,8 @@ static int qmc5883p_write_raw_get_fmt(struct iio_dev *indio_dev,
return IIO_VAL_INT_PLUS_NANO;
case IIO_CHAN_INFO_SAMP_FREQ:
return IIO_VAL_INT;
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ return IIO_VAL_INT;
default:
return -EINVAL;
}
@@ -452,9 +489,12 @@ static int qmc5883p_read_chip_id(struct qmc5883p_data *data)
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
BIT(IIO_CHAN_INFO_SCALE), \
.info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE), \
- .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_type = \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) | \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
.info_mask_shared_by_type_available = \
- BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) | \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
}
static const struct iio_chan_spec qmc5883p_channels[] = {
--
2.53.0
^ permalink raw reply related
* [PATCH v3 5/5] iio: magnetometer: qmc5883p: add PM support
From: Hardik Phalet @ 2026-04-19 22:33 UTC (permalink / raw)
To: gregkh, jic23
Cc: andy, conor+dt, devicetree, dlechner, krzk+dt, linux-iio,
linux-kernel, linux-staging, me, nuno.sa, robh, skhan,
Hardik Phalet, Hardik Phalet
In-Reply-To: <20260420-qmc5883p-driver-v3-0-da1e97088f8b@pm.me>
Add runtime PM with a 2 s autosuspend delay. Per datasheet §6.2.1
the chip continuously samples in MODE_NORMAL; putting it into
MODE_SUSPEND when idle drops current from up to 1180 uA to ~22 uA
(datasheet Table 2).
Wrap qmc5883p_get_measure() and qmc5883p_write_raw() with
pm_runtime_resume_and_get() / pm_runtime_put_autosuspend(), converting
early returns to a goto so the put is always paired.
System sleep is delegated to the runtime callbacks via
SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume).
A devm action is registered before devm_pm_runtime_enable() so that
LIFO teardown on unbind runs pm_runtime_disable() first (freezing PM
state) and then suspends the hardware via MODE_SUSPEND.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
drivers/iio/magnetometer/qmc5883p.c | 69 ++++++++++++++++++++++++++++++++++---
1 file changed, 64 insertions(+), 5 deletions(-)
diff --git a/drivers/iio/magnetometer/qmc5883p.c b/drivers/iio/magnetometer/qmc5883p.c
index d0e4a1a600b6..0ff635924abf 100644
--- a/drivers/iio/magnetometer/qmc5883p.c
+++ b/drivers/iio/magnetometer/qmc5883p.c
@@ -4,7 +4,7 @@
*
* Copyright 2026 Hardik Phalet <hardik.phalet@pm.me>
*
- * TODO: add triggered buffer support, PM, DSR
+ * TODO: add triggered buffer support, DSR
*
*/
@@ -15,6 +15,7 @@
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/mutex.h>
+#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/units.h>
@@ -208,6 +209,10 @@ static int qmc5883p_get_measure(struct qmc5883p_data *data, s16 *x, s16 *y,
u8 reg_data[6];
unsigned int status;
+ ret = pm_runtime_resume_and_get(data->dev);
+ if (ret < 0)
+ return ret;
+
/*
* Poll the status register until DRDY is set or timeout.
* Read the whole register in one shot so that OVFL is captured from
@@ -220,24 +225,26 @@ static int qmc5883p_get_measure(struct qmc5883p_data *data, s16 *x, s16 *y,
QMC5883P_DRDY_POLL_US,
150 * (MICRO / MILLI));
if (ret)
- return ret;
+ goto out;
if (status & QMC5883P_STATUS_OVFL) {
dev_warn_ratelimited(data->dev,
"data overflow, consider reducing field range\n");
ret = -ERANGE;
- return ret;
+ goto out;
}
ret = regmap_bulk_read(data->regmap, QMC5883P_REG_X_LSB, reg_data,
ARRAY_SIZE(reg_data));
if (ret)
- return ret;
+ goto out;
*x = (s16)get_unaligned_le16(®_data[0]);
*y = (s16)get_unaligned_le16(®_data[2]);
*z = (s16)get_unaligned_le16(®_data[4]);
+out:
+ pm_runtime_put_autosuspend(data->dev);
return ret;
}
@@ -341,10 +348,14 @@ static int qmc5883p_write_raw(struct iio_dev *indio_dev,
guard(mutex)(&data->mutex);
- ret = regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+ ret = pm_runtime_resume_and_get(data->dev);
if (ret)
return ret;
+ ret = regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+ if (ret)
+ goto out;
+
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
ret = qmc5883p_write_odr(data, val);
@@ -364,6 +375,8 @@ static int qmc5883p_write_raw(struct iio_dev *indio_dev,
if (restore && !ret)
ret = restore;
+out:
+ pm_runtime_put_autosuspend(data->dev);
return ret;
}
@@ -533,6 +546,18 @@ static int qmc5883p_chip_init(struct qmc5883p_data *data)
return 0;
}
+static void qmc5883p_suspend_action(void *arg)
+{
+ struct qmc5883p_data *data = arg;
+
+ /*
+ * PM is already disabled at this point (devm LIFO); put the hardware
+ * into MODE_SUSPEND directly so the chip is not left sampling after
+ * unbind.
+ */
+ regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+}
+
static int qmc5883p_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
@@ -584,9 +609,42 @@ static int qmc5883p_probe(struct i2c_client *client)
if (ret)
return dev_err_probe(dev, ret, "failed to initialize chip\n");
+ ret = devm_add_action_or_reset(dev, qmc5883p_suspend_action, data);
+ if (ret)
+ return ret;
+
+ pm_runtime_set_autosuspend_delay(dev, 2000);
+ pm_runtime_use_autosuspend(dev);
+
+ ret = devm_pm_runtime_enable(dev);
+ if (ret)
+ return ret;
+
return devm_iio_device_register(dev, indio_dev);
}
+static int qmc5883p_runtime_suspend(struct device *dev)
+{
+ struct iio_dev *indio_dev = dev_get_drvdata(dev);
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+
+ return regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+}
+
+static int qmc5883p_runtime_resume(struct device *dev)
+{
+ struct iio_dev *indio_dev = dev_get_drvdata(dev);
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+
+ return regmap_field_write(data->rf.mode, QMC5883P_MODE_NORMAL);
+}
+
+static const struct dev_pm_ops qmc5883p_dev_pm_ops = {
+ SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume)
+ RUNTIME_PM_OPS(qmc5883p_runtime_suspend,
+ qmc5883p_runtime_resume, NULL)
+};
+
static const struct of_device_id qmc5883p_of_match[] = {
{ .compatible = "qstcorp,qmc5883p" },
{ }
@@ -603,6 +661,7 @@ static struct i2c_driver qmc5883p_driver = {
.driver = {
.name = "qmc5883p",
.of_match_table = qmc5883p_of_match,
+ .pm = pm_ptr(&qmc5883p_dev_pm_ops),
},
.probe = qmc5883p_probe,
.id_table = qmc5883p_id,
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v4 1/4] arm64: dts: qcom: ipq9574: Add gpio details for eMMC
From: Alex G. @ 2026-04-19 22:38 UTC (permalink / raw)
To: andersson, konradybcio, robh, krzk+dt, conor+dt, linux-arm-msm,
devicetree, linux-kernel, Varadarajan Narayanan
Cc: sumit.garg, dmitry.baryshkov, Varadarajan Narayanan,
Konrad Dybcio
In-Reply-To: <20260202073322.259534-2-varadarajan.narayanan@oss.qualcomm.com>
On Monday, February 2, 2026 1:33:19 AM Central Daylight Time Varadarajan
Narayanan wrote:
Hi Varadarajan,
> The RDP433 has NAND and eMMC variants. Presently, only NAND variant is
> supported. To enable support for eMMC variant, add the relevant GPIO
> related information.
>
> Do not enable NAND by default here. Enable it in board specific DTS.
>
This commit references sdc_default_state in the .dtsi file, without defining it.
It creates a silent dependency on the board .dts, who must now define the pins.
This makes no sense to me for boards that don't have eMMC. As an example, it
breaks most downstream OpenWRT boards.
Was this new dependency accidental?
Alex
> Reviewed-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
> Signed-off-by: Varadarajan Narayanan
> <varadarajan.narayanan@oss.qualcomm.com> ---
> v4: Move sdhc properties from emmc dts to SoC dtsi
>
> v3: Disable nand in ipq9574-rdp-common.dtsi and enable it where required.
> Add 'Reviewed-by: Konrad Dybcio'
> ---
> .../boot/dts/qcom/ipq9574-rdp-common.dtsi | 32 +++++++++++++++++++
> arch/arm64/boot/dts/qcom/ipq9574.dtsi | 9 ++++++
> 2 files changed, 41 insertions(+)
>
> diff --git a/arch/arm64/boot/dts/qcom/ipq9574-rdp-common.dtsi
> b/arch/arm64/boot/dts/qcom/ipq9574-rdp-common.dtsi index
> bdb396afb992..e4ae79b2fcd9 100644
> --- a/arch/arm64/boot/dts/qcom/ipq9574-rdp-common.dtsi
> +++ b/arch/arm64/boot/dts/qcom/ipq9574-rdp-common.dtsi
> @@ -169,6 +169,38 @@ data-pins {
> bias-disable;
> };
> };
> +
> + sdc_default_state: sdc-default-state {
> + clk-pins {
> + pins = "gpio5";
> + function = "sdc_clk";
> + drive-strength = <8>;
> + bias-disable;
> + };
> +
> + cmd-pins {
> + pins = "gpio4";
> + function = "sdc_cmd";
> + drive-strength = <8>;
> + bias-pull-up;
> + };
> +
> + data-pins {
> + pins = "gpio0", "gpio1", "gpio2",
> + "gpio3", "gpio6", "gpio7",
> + "gpio8", "gpio9";
> + function = "sdc_data";
> + drive-strength = <8>;
> + bias-pull-up;
> + };
> +
> + rclk-pins {
> + pins = "gpio10";
> + function = "sdc_rclk";
> + drive-strength = <8>;
> + bias-pull-down;
> + };
> + };
> };
>
> &qpic_bam {
> diff --git a/arch/arm64/boot/dts/qcom/ipq9574.dtsi
> b/arch/arm64/boot/dts/qcom/ipq9574.dtsi index 86c9cb9fffc9..4b8c58982869
> 100644
> --- a/arch/arm64/boot/dts/qcom/ipq9574.dtsi
> +++ b/arch/arm64/boot/dts/qcom/ipq9574.dtsi
> @@ -467,6 +467,15 @@ sdhc_1: mmc@7804000 {
> clock-names = "iface", "core", "xo", "ice";
> non-removable;
> supports-cqe;
> + pinctrl-0 = <&sdc_default_state>;
> + pinctrl-names = "default";
> + mmc-ddr-1_8v;
> + mmc-hs200-1_8v;
> + mmc-hs400-1_8v;
> + mmc-hs400-enhanced-strobe;
> + max-frequency = <384000000>;
> + bus-width = <8>;
> +
> status = "disabled";
> };
^ permalink raw reply
* Re: [PATCH 2/4] clocksource/drivers/sun5i: add H616 hstimer support
From: Andre Przywara @ 2026-04-19 22:39 UTC (permalink / raw)
To: Michal Piekos
Cc: Daniel Lezcano, Thomas Gleixner, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Chen-Yu Tsai, Jernej Skrabec, Samuel Holland,
Maxime Ripard, linux-kernel, devicetree, linux-arm-kernel,
linux-sunxi
In-Reply-To: <20260419-h616-t113s-hstimer-v1-2-1af74ebef7c5@mmpsystems.pl>
On Sun, 19 Apr 2026 14:46:08 +0200
Michal Piekos <michal.piekos@mmpsystems.pl> wrote:
Hi,
> H616 high speed timer differs from existing timer-sun5i by register base
> offset.
>
> Add selectable register layout structures.
> Add H616 compatible string to OF match table.
>
> Signed-off-by: Michal Piekos <michal.piekos@mmpsystems.pl>
> ---
> drivers/clocksource/timer-sun5i.c | 56 ++++++++++++++++++++++++++++++++++-----
> 1 file changed, 50 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/clocksource/timer-sun5i.c b/drivers/clocksource/timer-sun5i.c
> index f827d3f98f60..125abc11c3c3 100644
> --- a/drivers/clocksource/timer-sun5i.c
> +++ b/drivers/clocksource/timer-sun5i.c
> @@ -21,18 +21,52 @@
> #define TIMER_IRQ_EN_REG 0x00
> #define TIMER_IRQ_EN(val) BIT(val)
> #define TIMER_IRQ_ST_REG 0x04
> -#define TIMER_CTL_REG(val) (0x20 * (val) + 0x10)
> #define TIMER_CTL_ENABLE BIT(0)
> #define TIMER_CTL_RELOAD BIT(1)
> -#define TIMER_CTL_CLK_PRES(val) (((val) & 0x7) << 4)
> #define TIMER_CTL_ONESHOT BIT(7)
> -#define TIMER_INTVAL_LO_REG(val) (0x20 * (val) + 0x14)
> -#define TIMER_INTVAL_HI_REG(val) (0x20 * (val) + 0x18)
> -#define TIMER_CNTVAL_LO_REG(val) (0x20 * (val) + 0x1c)
> -#define TIMER_CNTVAL_HI_REG(val) (0x20 * (val) + 0x20)
> +#define TIMER_CTL_CLK_PRES(val) (((val) & 0x7) << 4)
> +#define TIMER_CTL_REG(val) \
> + (soc_base->stride * (val) + soc_base->ctl_base)
> +#define TIMER_INTVAL_LO_REG(val) \
> + (soc_base->stride * (val) + soc_base->intval_lo_base)
> +#define TIMER_INTVAL_HI_REG(val) \
> + (soc_base->stride * (val) + soc_base->intval_hi_base)
> +#define TIMER_CNTVAL_LO_REG(val) \
> + (soc_base->stride * (val) + soc_base->cntval_lo_base)
> +#define TIMER_CNTVAL_HI_REG(val) \
> + (soc_base->stride * (val) + soc_base->cntval_hi_base)
>
> #define TIMER_SYNC_TICKS 3
>
> +struct sunxi_timer_base {
> + u32 ctl_base;
> + u32 intval_lo_base;
> + u32 intval_hi_base;
> + u32 cntval_lo_base;
> + u32 cntval_hi_base;
> + u32 stride;
> +};
> +
> +static const struct sunxi_timer_base sun5i_base = {
> + .ctl_base = 0x10,
> + .intval_lo_base = 0x14,
> + .intval_hi_base = 0x18,
> + .cntval_lo_base = 0x1c,
> + .cntval_hi_base = 0x20,
Mmmh, why all these members? Aren't those all the same, just offset by
0x10? So we just need a single value reg_offs, being either 0x0 or 0x10?
> + .stride = 0x20
What it this about? It's the same stride for both versions, so why is
this a field?
> +};
> +
> +static const struct sunxi_timer_base sun50i_base = {
> + .ctl_base = 0x20,
> + .intval_lo_base = 0x24,
> + .intval_hi_base = 0x28,
> + .cntval_lo_base = 0x2c,
> + .cntval_hi_base = 0x30,
> + .stride = 0x20
> +};
> +
> +static const struct sunxi_timer_base *soc_base;
This doesn't look right. Differentiating between slightly different
hardware revision via the compatible string is a common pattern, look
at for instance drivers/media/rc/sunxi-cir.c and its usage of quirks for
an example how to handle this more nicely.
Cheers,
Andre
> +
> struct sun5i_timer {
> void __iomem *base;
> struct clk *clk;
> @@ -238,6 +272,7 @@ static int sun5i_setup_clockevent(struct platform_device *pdev,
> static int sun5i_timer_probe(struct platform_device *pdev)
> {
> struct device *dev = &pdev->dev;
> + struct device_node *node = dev_of_node(&pdev->dev);
> struct sun5i_timer *st;
> struct reset_control *rstc;
> void __iomem *timer_base;
> @@ -251,6 +286,14 @@ static int sun5i_timer_probe(struct platform_device *pdev)
>
> platform_set_drvdata(pdev, st);
>
> + if (!node)
> + return -EINVAL;
> +
> + if (of_device_is_compatible(node, "allwinner,sun50i-h616-hstimer"))
> + soc_base = &sun50i_base;
> + else
> + soc_base = &sun5i_base;
> +
> timer_base = devm_platform_ioremap_resource(pdev, 0);
> if (IS_ERR(timer_base)) {
> dev_err(dev, "Can't map registers\n");
> @@ -314,6 +357,7 @@ static void sun5i_timer_remove(struct platform_device *pdev)
> static const struct of_device_id sun5i_timer_of_match[] = {
> { .compatible = "allwinner,sun5i-a13-hstimer" },
> { .compatible = "allwinner,sun7i-a20-hstimer" },
> + { .compatible = "allwinner,sun50i-h616-hstimer" },
> {},
> };
> MODULE_DEVICE_TABLE(of, sun5i_timer_of_match);
>
^ permalink raw reply
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