* Re: [PATCH v3 1/2] leds: ltc3208: add driver
From: Lee Jones @ 2026-04-09 16:59 UTC (permalink / raw)
To: Jan Carlo Roleda
Cc: Pavel Machek, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
linux-kernel, linux-leds, devicetree
In-Reply-To: <20260406-upstream-ltc3208-v3-1-7f0b1d20ee7a@analog.com>
"Add driver" is not a good subject line.
> Kernel driver implementation for LTC3208 Multidisplay LED Driver
A one line commit messages is not suitable fore a 300 line driver!
What is the LTC3208 Multidisplay LED?
What does it do?
How does it operate?
What's special about it?
Any quirks?
> Signed-off-by: Jan Carlo Roleda <jancarlo.roleda@analog.com>
> ---
> MAINTAINERS | 7 ++
> drivers/leds/Kconfig | 11 ++
> drivers/leds/Makefile | 1 +
> drivers/leds/leds-ltc3208.c | 298 ++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 317 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 55af015174a5..48bae02057d5 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -15126,6 +15126,13 @@ W: https://ez.analog.com/linux-software-drivers
> F: Documentation/devicetree/bindings/iio/temperature/adi,ltc2983.yaml
> F: drivers/iio/temperature/ltc2983.c
>
> +LTC3208 LED DRIVER
> +M: Jan Carlo Roleda <jancarlo.roleda@analog.com>
> +L: linux-leds@vger.kernel.org
> +S: Maintained
> +W: https://ez.analog.com/linux-software-drivers
> +F: drivers/leds/leds-ltc3208.c
> +
> LTC4282 HARDWARE MONITOR DRIVER
> M: Nuno Sa <nuno.sa@analog.com>
> L: linux-hwmon@vger.kernel.org
> diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
> index 597d7a79c988..867b120ea8ba 100644
> --- a/drivers/leds/Kconfig
> +++ b/drivers/leds/Kconfig
> @@ -1029,6 +1029,17 @@ config LEDS_ACER_A500
> This option enables support for the Power Button LED of
> Acer Iconia Tab A500.
>
> +config LEDS_LTC3208
> + tristate "LED Driver for Analog Devices LTC3208"
> + depends on LEDS_CLASS && I2C
> + select REGMAP_I2C
> + help
> + Say Y to enable the LTC3208 LED driver.
> + This supports the LED device LTC3208.
You can do better!
> + To compile this driver as a module, choose M here: the module will
> + be called ltc3208.
> +
> source "drivers/leds/blink/Kconfig"
>
> comment "Flash and Torch LED drivers"
> diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
> index 8fdb45d5b439..b08b539112b6 100644
> --- a/drivers/leds/Makefile
> +++ b/drivers/leds/Makefile
> @@ -61,6 +61,7 @@ obj-$(CONFIG_LEDS_LP8788) += leds-lp8788.o
> obj-$(CONFIG_LEDS_LP8860) += leds-lp8860.o
> obj-$(CONFIG_LEDS_LP8864) += leds-lp8864.o
> obj-$(CONFIG_LEDS_LT3593) += leds-lt3593.o
> +obj-$(CONFIG_LEDS_LTC3208) += leds-ltc3208.o
> obj-$(CONFIG_LEDS_MAX5970) += leds-max5970.o
> obj-$(CONFIG_LEDS_MAX77650) += leds-max77650.o
> obj-$(CONFIG_LEDS_MAX77705) += leds-max77705.o
> diff --git a/drivers/leds/leds-ltc3208.c b/drivers/leds/leds-ltc3208.c
> new file mode 100644
> index 000000000000..65e65cd73d73
> --- /dev/null
> +++ b/drivers/leds/leds-ltc3208.c
> @@ -0,0 +1,298 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * LED driver for Analog Devices LTC3208 Multi-Display Driver
> + *
> + * Copyright 2026 Analog Devices Inc.
> + *
> + * Author: Jan Carlo Roleda <jancarlo.roleda@analog.com>
> + */
> +#include <linux/bitfield.h>
> +#include <linux/errno.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/i2c.h>
> +#include <linux/init.h>
> +#include <linux/leds.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <linux/property.h>
> +#include <linux/regmap.h>
> +#include <linux/types.h>
> +#include <linux/workqueue.h>
Are all of these headers strictly necessary? For instance, it doesn't appear we
are using GPIOs, platform devices, or workqueues in this driver.
> +#define LTC3208_SET_HIGH_BYTE_DATA(x) FIELD_PREP(GENMASK(7, 4), (x))
> +
> +/* Registers */
> +#define LTC3208_REG_A_GRNRED 0x1 /* Green (High half-byte) and Red (Low half-byte) current DAC*/
> +#define LTC3208_REG_B_AUXBLU 0x2 /* AUX (High half-byte) and Blue (Low half-byte) current DAC*/
> +#define LTC3208_REG_C_MAIN 0x3 /* Main current DAC */
> +#define LTC3208_REG_D_SUB 0x4 /* Sub current DAC */
> +#define LTC3208_REG_E_AUX 0x5 /* AUX DAC Select */
> +#define LTC3208_REG_F_CAM 0x6 /* CAM (High half-byte and Low half-byte) current DAC*/
> +#define LTC3208_REG_G_OPT 0x7 /* Device Options */
> +
> +/* Device Options register */
> +#define LTC3208_OPT_CPO_MASK GENMASK(7, 6)
> +#define LTC3208_OPT_DIS_RGBDROP BIT(3)
> +#define LTC3208_OPT_DIS_CAMHILO BIT(2)
> +#define LTC3208_OPT_EN_RGBS BIT(1)
Nit: This can look nicer nested:
#define LTC3208_REG_A_GRNRED 0x1 /* Green (High half-byte) and Red (Low half-byte) current DAC*/
#define LTC3208_REG_B_AUXBLU 0x2 /* AUX (High half-byte) and Blue (Low half-byte) current DAC*/
#define LTC3208_REG_C_MAIN 0x3 /* Main current DAC */
#define LTC3208_REG_D_SUB 0x4 /* Sub current DAC */
#define LTC3208_REG_E_AUX 0x5 /* AUX DAC Select */
#define LTC3208_AUX1_MASK GENMASK(1, 0)
#define LTC3208_AUX2_MASK GENMASK(3, 2)
#define LTC3208_AUX3_MASK GENMASK(5, 4)
#define LTC3208_AUX4_MASK GENMASK(7, 6)
#define LTC3208_REG_F_CAM 0x6 /* CAM (High half-byte and Low half-byte) current DAC*/
#define LTC3208_REG_G_OPT 0x7 /* Device Options */
#define LTC3208_OPT_CPO_MASK GENMASK(7, 6)
#define LTC3208_OPT_DIS_RGBDROP BIT(3)
#define LTC3208_OPT_DIS_CAMHILO BIT(2)
#define LTC3208_OPT_EN_RGBS BIT(1)
> +#define LTC3208_MAX_BRIGHTNESS_4BIT 0xF
> +#define LTC3208_MAX_BRIGHTNESS_8BIT 0xFF
> +
> +#define LTC3208_NUM_LED_GRPS 8
> +#define LTC3208_NUM_AUX_LEDS 4
> +
> +#define LTC3208_NUM_AUX_OPT 4
> +#define LTC3208_MAX_CPO_OPT 3
Nit: Can we have _all_ of the values line up nicely?
#define LTC3208_NUM_AUX_OPT 4
#define LTC3208_MAX_CPO_OPT 3
> +enum ltc3208_aux_channel {
> + LTC3208_AUX_CHAN_AUX = 0,
> + LTC3208_AUX_CHAN_MAIN,
> + LTC3208_AUX_CHAN_SUB,
> + LTC3208_AUX_CHAN_CAM
> +};
> +
> +enum ltc3208_channel {
> + LTC3208_CHAN_MAIN = 0,
> + LTC3208_CHAN_SUB,
> + LTC3208_CHAN_AUX,
> + LTC3208_CHAN_CAML,
> + LTC3208_CHAN_CAMH,
> + LTC3208_CHAN_RED,
> + LTC3208_CHAN_BLUE,
> + LTC3208_CHAN_GREEN
> +};
> +
> +static const char * const ltc3208_dt_aux_channels[] = {
> + "adi,aux1-channel", "adi,aux2-channel",
> + "adi,aux3-channel", "adi,aux4-channel"
> +};
> +
> +static const char * const ltc3208_aux_opt[] = {
> + "aux", "main", "sub", "cam"
> +};
> +
> +
?
> +struct ltc3208_led {
> + struct led_classdev cdev;
> + struct i2c_client *client;
> + enum ltc3208_channel channel;
> +};
> +
> +struct ltc3208_dev {
> + struct i2c_client *client;
> + struct regmap *map;
> + struct ltc3208_led *leds;
> +};
> +
> +static const struct regmap_config ltc3208_regmap_cfg = {
> + .reg_bits = 8,
> + .val_bits = 8,
> +};
> +
> +static int ltc3208_led_set_brightness(struct led_classdev *led_cdev,
> + enum led_brightness brightness)
> +{
> + struct ltc3208_led *led = container_of(led_cdev,
> + struct ltc3208_led, cdev);
You can use 100-chars to avoid this awkwardness.
> + struct i2c_client *client = led->client;
> + struct ltc3208_dev *dev = i2c_get_clientdata(client);
> + struct regmap *map = dev->map;
> + u8 current_level = brightness;
> +
> + /*
> + * For registers with 4-bit splits (CAM, AUX/BLUE, GREEN/RED), the other
> + * half of the byte will be retrieved from the stored DAC value before
> + * updating the register.
> + */
> + switch (led->channel) {
> + case LTC3208_CHAN_MAIN:
> + return regmap_write(map, LTC3208_REG_C_MAIN, current_level);
> + case LTC3208_CHAN_SUB:
> + return regmap_write(map, LTC3208_REG_D_SUB, current_level);
> + case LTC3208_CHAN_AUX:
> + /* combine both low and high halves of byte */
> + current_level = LTC3208_SET_HIGH_BYTE_DATA(current_level);
> + current_level |= dev->leds[LTC3208_CHAN_BLUE].cdev.brightness;
> + return regmap_write(map, LTC3208_REG_B_AUXBLU, current_level);
Should we be using 'regmap_update_bits()' or 'regmap_field' here instead?
Constructing the register value by reading the software state of another LED
instance could lead to races.
> + case LTC3208_CHAN_BLUE:
> + /* apply high bits stored in other led */
> + current_level |= LTC3208_SET_HIGH_BYTE_DATA(dev->leds[LTC3208_CHAN_AUX].cdev.brightness);
> + return regmap_write(map, LTC3208_REG_B_AUXBLU, current_level);
> + case LTC3208_CHAN_CAMH:
> + current_level = LTC3208_SET_HIGH_BYTE_DATA(current_level);
> + current_level |= dev->leds[LTC3208_CHAN_CAML].cdev.brightness;
> + return regmap_write(map, LTC3208_REG_F_CAM, current_level);
> + case LTC3208_CHAN_CAML:
> + current_level |= LTC3208_SET_HIGH_BYTE_DATA(dev->leds[LTC3208_CHAN_CAMH].cdev.brightness);
> + return regmap_write(map, LTC3208_REG_F_CAM, current_level);
> + case LTC3208_CHAN_GREEN:
> + current_level = LTC3208_SET_HIGH_BYTE_DATA(current_level);
> + current_level |= dev->leds[LTC3208_CHAN_RED].cdev.brightness;
> + return regmap_write(map, LTC3208_REG_A_GRNRED, current_level);
> + case LTC3208_CHAN_RED:
> + current_level |= LTC3208_SET_HIGH_BYTE_DATA(dev->leds[LTC3208_CHAN_GREEN].cdev.brightness);
> + return regmap_write(map, LTC3208_REG_A_GRNRED, current_level);
This lot is begging for a sub function:
static int ltc3208_led_set_current_level(struct regmap *regmap, u8 reg, u8 low, u8 high) {
{
return regmap_write(regmap, reg, SET_HIGH_BYTE(high) | low);
}
> + default:
> + dev_err(&client->dev, "Invalid LED Channel\n");
> + return -EINVAL;
> + }
> +}
> +
> +static int ltc3208_update_options(struct ltc3208_dev *dev,
> + bool is_sub, bool is_cam_hi, bool is_rgb_drop)
> +{
> + struct regmap *map = dev->map;
> + u8 val = FIELD_PREP(LTC3208_OPT_EN_RGBS, is_sub) |
> + FIELD_PREP(LTC3208_OPT_DIS_CAMHILO, is_cam_hi) |
> + FIELD_PREP(LTC3208_OPT_DIS_RGBDROP, is_rgb_drop);
> +
That tabbing is awkward. In these cases it's better to do the
allocation after the declaration.
> + return regmap_write(map, LTC3208_REG_G_OPT, val);
> +}
> +
> +static int ltc3208_update_aux_dac(struct ltc3208_dev *dev,
> + enum ltc3208_aux_channel aux_1, enum ltc3208_aux_channel aux_2,
> + enum ltc3208_aux_channel aux_3, enum ltc3208_aux_channel aux_4)
These should sit under the '('.
> +{
> + struct regmap *map = dev->map;
> + u8 val = FIELD_PREP(LTC3208_AUX1_MASK, aux_1) |
> + FIELD_PREP(LTC3208_AUX2_MASK, aux_2) |
> + FIELD_PREP(LTC3208_AUX3_MASK, aux_3) |
> + FIELD_PREP(LTC3208_AUX4_MASK, aux_4);
As above.
> + return regmap_write(map, LTC3208_REG_E_AUX, val);
> +}
> +
> +static int ltc3208_probe(struct i2c_client *client)
> +{
> + enum ltc3208_aux_channel aux_channels[LTC3208_NUM_AUX_LEDS];
> + struct ltc3208_dev *data;
'data' is a terrible variable name.
> + struct ltc3208_led *leds;
> + struct regmap *map;
'regmap'
> + int ret, i;
> + u32 val;
> + bool dropdis_rgb_aux4;
> + bool dis_camhl;
> + bool en_rgbs;
> +
> + map = devm_regmap_init_i2c(client, <c3208_regmap_cfg);
> + if (IS_ERR(map))
> + return dev_err_probe(&client->dev, PTR_ERR(map),
> + "Failed to initialize regmap\n");
> +
> + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
> + if (!data)
> + return -ENOMEM;
> +
> + leds = devm_kcalloc(&client->dev, LTC3208_NUM_LED_GRPS,
> + sizeof(struct ltc3208_led), GFP_KERNEL);
> + if (!leds)
> + return -ENOMEM;
> +
> + data->client = client;
> + data->map = map;
> +
> + /* initialize options from devicetree */
Capitalise comments and it's "Device Tree", although honestly, I think
the whole comment is superfluous.
> + dis_camhl = device_property_read_bool(&client->dev,
> + "adi,disable-camhl-pin");
> + en_rgbs = device_property_read_bool(&client->dev,
> + "adi,cfg-enrgbs-pin");
> + dropdis_rgb_aux4 = device_property_read_bool(&client->dev,
> + "adi,disable-rgb-aux4-dropout");
Use 100-chars.
> + ret = ltc3208_update_options(data, en_rgbs, dis_camhl,
> + dropdis_rgb_aux4);
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "error writing to options register\n");
Capitalise.
> + /* initialize aux channel configurations from devicetree */
As above and throughout.
> + for (i = 0; i < LTC3208_NUM_AUX_LEDS; i++) {
for (int i = 0; ...
> + ret = device_property_match_property_string(&client->dev,
> + ltc3208_dt_aux_channels[i],
> + ltc3208_aux_opt,
> + LTC3208_NUM_AUX_OPT);
> + /* use default value if absent in devicetree */
> + if (ret == -EINVAL)
> + aux_channels[i] = LTC3208_AUX_CHAN_AUX;
> + else if (ret >= 0)
> + aux_channels[i] = ret;
> + else
> + return dev_err_probe(&client->dev, ret,
> + "Failed getting aux-channel.\n");
> + }
> +
> + ret = ltc3208_update_aux_dac(data, aux_channels[0], aux_channels[1],
> + aux_channels[2], aux_channels[3]);
Why not just aux_channels and pull the values out in the function.
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "error writing to aux %u channel register.\n", i);
When is 'i' not 'LTC3208_NUM_AUX_LEDS'?
> + i2c_set_clientdata(client, data);
> +
> + device_for_each_child_node_scoped(&client->dev, child) {
> + struct ltc3208_led *led;
> + struct led_init_data init_data = {};
> +
> + ret = fwnode_property_read_u32(child, "reg", &val);
> + if (ret || val >= LTC3208_NUM_LED_GRPS)
> + return dev_err_probe(&client->dev, -EINVAL,
> + "Invalid reg property for LED\n");
Why aren't we propagating the real error?
> +
> + led = &leds[val];
> + led->client = client;
> + led->channel = val;
> + led->cdev.brightness_set_blocking = ltc3208_led_set_brightness;
> + led->cdev.max_brightness = LTC3208_MAX_BRIGHTNESS_4BIT;
> + if (val == LTC3208_CHAN_MAIN || val == LTC3208_CHAN_SUB)
> + led->cdev.max_brightness = LTC3208_MAX_BRIGHTNESS_8BIT;
> +
> + init_data.fwnode = child;
> +
> + ret = devm_led_classdev_register_ext(&client->dev, &led->cdev,
> + &init_data);
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "Failed to register LED %u\n", val);
> + }
> +
> + data->leds = leds;
> +
> + return 0;
> +}
> +
> +static const struct of_device_id ltc3208_match_table[] = {
> + {.compatible = "adi,ltc3208"},
> + { }
> +};
> +MODULE_DEVICE_TABLE(of, ltc3208_match_table);
> +
> +static const struct i2c_device_id ltc3208_idtable[] = {
> + { "ltc3208" },
> + { }
> +};
> +MODULE_DEVICE_TABLE(i2c, ltc3208_idtable);
> +
> +static struct i2c_driver ltc3208_driver = {
> + .driver = {
> + .name = "ltc3208",
> + .of_match_table = ltc3208_match_table,
> + },
> + .id_table = ltc3208_idtable,
> + .probe = ltc3208_probe,
> +};
> +module_i2c_driver(ltc3208_driver);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Jan Carlo Roleda <jancarlo.roleda@analog.com>");
> +MODULE_DESCRIPTION("LTC3208 LED Driver");
>
> --
> 2.43.0
>
--
Lee Jones [李琼斯]
^ permalink raw reply
* Re: [PATCH V11 02/12] PCI: host-generic: Add common helpers for parsing Root Port properties
From: Manivannan Sadhasivam @ 2026-04-09 16:58 UTC (permalink / raw)
To: Sherry Sun
Cc: robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
Frank Li, s.hauer@pengutronix.de, kernel@pengutronix.de,
festevam@gmail.com, lpieralisi@kernel.org, kwilczynski@kernel.org,
bhelgaas@google.com, Hongxing Zhu, l.stach@pengutronix.de,
imx@lists.linux.dev, linux-pci@vger.kernel.org,
linux-arm-kernel@lists.infradead.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org
In-Reply-To: <VI0PR04MB1211419BB996B4790AE04170F92582@VI0PR04MB12114.eurprd04.prod.outlook.com>
On Thu, Apr 09, 2026 at 02:58:21AM +0000, Sherry Sun wrote:
> > On Wed, Apr 08, 2026 at 06:34:02AM +0000, Sherry Sun wrote:
> >
> > [...]
> >
> > > > > +/**
> > > > > + * pci_host_common_parse_port - Parse a single Root Port node
> > > > > + * @dev: Device pointer
> > > > > + * @bridge: PCI host bridge
> > > > > + * @node: Device tree node of the Root Port
> > > > > + *
> > > > > + * Returns: 0 on success, negative error code on failure */
> > > > > +static int pci_host_common_parse_port(struct device *dev,
> > > > > + struct pci_host_bridge *bridge,
> > > > > + struct device_node *node) {
> > > > > + struct pci_host_port *port;
> > > > > + struct gpio_desc *reset;
> > > > > +
> > > > > + reset = devm_fwnode_gpiod_get(dev, of_fwnode_handle(node),
> > > > > + "reset", GPIOD_ASIS, "PERST#");
> > > >
> > > > Sorry, I missed this earlier.
> > > >
> > > > Since PERST# is optional, you cannot reliably detect whether the
> > > > Root Port binding intentionally skipped the PERST# GPIO or legacy
> > > > binding is used, just by checking for PERST# in Root Port node.
> > > >
> > > > So this helper should do 3 things:
> > > >
> > > > 1. If PERST# is found in Root Port node, use it.
> > > > 2. If not, check the RC node and if present, return -ENOENT to
> > > > fallback to the legacy binding.
> > > > 3. If not found in both nodes, assume that the PERST# is not present
> > > > in the design, and proceed with parsing Root Port binding further.
> > >
> > > Hi Mani, understand, does the following code looks ok for above three
> > cases?
> > >
> > > /* Check if PERST# is present in Root Port node */
> > > reset = devm_fwnode_gpiod_get(dev, of_fwnode_handle(node),
> > > "reset", GPIOD_ASIS, "PERST#");
> > > if (IS_ERR(reset)) {
> > > /* If error is not -ENOENT, it's a real error */
> > > if (PTR_ERR(reset) != -ENOENT)
> > > return PTR_ERR(reset);
> > >
> > > /* PERST# not found in Root Port node, check RC node */
> > > rc_has_reset = of_property_read_bool(dev->of_node, "reset-gpios") ||
> > > of_property_read_bool(dev->of_node, "reset-gpio");
> >
> > Just:
> > if (of_property_read_bool(dev->of_node, "reset-gpios") ||
> > of_property_read_bool(dev->of_node, "reset-gpio")) {
> > return -ENOENT;
> > }
>
> Ok, will do.
>
> >
> > > if (rc_has_reset)
> > > return -ENOENT;
> > >
> > > /* No PERST# in either node, assume not present in design */
> > > reset = NULL;
> > > }
> > >
> > > port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
> > > if (!port)
> > > return -ENOMEM;
> > > ...
> > >
> > > >
> > > > But there is one more important limitation here. Right now, this API
> > > > only handles PERST#. But if another vendor tries to use it and if
> > > > they need other properties such as PHY, clocks etc... those
> > > > resources should be fetched optionally only by this helper. But if
> > > > the controller has a hard dependency on those resources, the driver will
> > fail to operate.
> > > >
> > > > I don't think we can fix this limitation though and those platforms
> > > > should ensure that the resource dependency is correctly modeled in
> > > > DT binding and the DTS is validated properly. It'd be good to
> > > > mention this in the kernel doc of this API.
> > >
> > > Ok, I will add a NOTE for this in this API description.
> > >
> > > >
> > > > > + if (IS_ERR(reset))
> > > > > + return PTR_ERR(reset);
> > > > > +
> > > > > + port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
> > > > > + if (!port)
> > > > > + return -ENOMEM;
> > > > > +
> > > > > + port->reset = reset;
> > > > > + INIT_LIST_HEAD(&port->list);
> > > > > + list_add_tail(&port->list, &bridge->ports);
> > > > > +
> > > > > + return 0;
> > > > > +}
> > > > > +
> > > > > +/**
> > > > > + * pci_host_common_parse_ports - Parse Root Port nodes from
> > > > > +device tree
> > > > > + * @dev: Device pointer
> > > > > + * @bridge: PCI host bridge
> > > > > + *
> > > > > + * This function iterates through child nodes of the host bridge
> > > > > +and parses
> > > > > + * Root Port properties (currently only reset GPIO).
> > > > > + *
> > > > > + * Returns: 0 on success, -ENOENT if no ports found, other
> > > > > +negative error codes
> > > > > + * on failure
> > > > > + */
> > > > > +int pci_host_common_parse_ports(struct device *dev, struct
> > > > > +pci_host_bridge *bridge) {
> > > > > + int ret = -ENOENT;
> > > > > +
> > > > > + for_each_available_child_of_node_scoped(dev->of_node, of_port) {
> > > > > + if (!of_node_is_type(of_port, "pci"))
> > > > > + continue;
> > > > > + ret = pci_host_common_parse_port(dev, bridge, of_port);
> > > > > + if (ret)
> > > > > + return ret;
> > > >
> > > > As Sashiko flagged, you need to make sure that
> > > > devm_add_action_or_reset() is added even during the error path:
> > >
> > > Yes, it needs to be fixed. We can handle it with the following two methods, I
> > am not sure which method is better or more preferable?
> > >
> > > #1: register cleanup action after first successful port parse and use
> > cleanup_registered flag to avoid duplicate register.
> > > int ret = -ENOENT;
> > > bool cleanup_registered = false;
> > >
> > > for_each_available_child_of_node_scoped(dev->of_node, of_port) {
> > > if (!of_node_is_type(of_port, "pci"))
> > > continue;
> > > ret = pci_host_common_parse_port(dev, bridge, of_port);
> > > if (ret)
> > > return ret;
> > >
> > > /* Register cleanup action after first successful port parse */
> > > if (!cleanup_registered) {
> > > ret = devm_add_action_or_reset(dev,
> > > pci_host_common_delete_ports,
> > > &bridge->ports);
> >
> > Even if you register devm_add_action_or_reset(), it won't be called when
> > pci_host_common_parse_port() fails since the legacy fallback will be used.
> >
> > So you need to manually call pci_host_common_delete_ports() in the error
> > path.
>
> Get your point, so seems I should just add the err_cleanup handle path like this, right?
>
> for_each_available_child_of_node_scoped(dev->of_node, of_port) {
> if (!of_node_is_type(of_port, "pci"))
> continue;
> ret = pci_host_common_parse_port(dev, bridge, of_port);
> if (ret)
> goto err_cleanup;
> }
>
> if (ret)
> return ret;
>
> return devm_add_action_or_reset(dev, pci_host_common_delete_ports,
> &bridge->ports);
>
> err_cleanup:
> pci_host_common_delete_ports(&bridge->ports);
> return ret;
>
Yes!
- Mani
--
மணிவண்ணன் சதாசிவம்
^ permalink raw reply
* [PATCH 4/4] MAINTAINERS: Add entry for QST QMC5883P magnetometer driver
From: Hardik Phalet @ 2026-04-09 16:24 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: Jonathan Cameron, David Lechner, Nuno Sá, Andy Shevchenko,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Brigham Campbell,
Shuah Khan, linux-iio, devicetree, linux-kernel, linux-staging,
Hardik Phalet
In-Reply-To: <20260409162308.2590385-1-hardik.phalet@pm.me>
Add a MAINTAINERS entry for the QST QMC5883P staging IIO driver,
covering the driver source and its device tree binding.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index a92290fffa16..d0b9bfceb283 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20274,6 +20274,13 @@ F: Documentation/networking/device_drivers/ethernet/freescale/dpaa2/overview.rst
F: drivers/bus/fsl-mc/
F: include/uapi/linux/fsl_mc.h
+QST QMC5883P MAGNETOMETER DRIVER
+M: Hardik Phalet <hardik.phalet@pm.me>
+L: linux-iio@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/iio/magnetometer/qst,qmc5883p.yaml
+F: drivers/staging/iio/magnetometer/
+
QT1010 MEDIA DRIVER
L: linux-media@vger.kernel.org
S: Orphan
--
2.53.0
^ permalink raw reply related
* [PATCH 3/4] staging: iio: magnetometer: Add QST QMC5883P driver
From: Hardik Phalet @ 2026-04-09 16:24 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: Jonathan Cameron, David Lechner, Nuno Sá, Andy Shevchenko,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Brigham Campbell,
Shuah Khan, linux-iio, devicetree, linux-kernel, linux-staging,
Hardik Phalet
In-Reply-To: <20260409162308.2590385-1-hardik.phalet@pm.me>
Add an IIO driver for the QST QMC5883P 3-axis magnetometer. The device
communicates over I2C and is managed via regmap with an rbtree cache.
Regmap fields are used to access the individual bit fields in CTRL_1 and
CTRL_2 registers.
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)
- Configurable output data rate (10, 50, 100, 200 Hz)
- Configurable oversampling ratio (1, 2, 4, 8)
- Configurable downsampling ratio (1, 2, 4, 8) via a custom sysfs attr
- Runtime PM with a 2 s autosuspend delay
Known limitations tracked in TODO:
- No triggered buffer support
- No DRDY interrupt support
- Self-test register field is unused
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
drivers/staging/iio/Kconfig | 1 +
drivers/staging/iio/Makefile | 1 +
drivers/staging/iio/magnetometer/Kconfig | 20 +
drivers/staging/iio/magnetometer/Makefile | 7 +
drivers/staging/iio/magnetometer/TODO | 5 +
drivers/staging/iio/magnetometer/qmc5883p.c | 819 ++++++++++++++++++++
6 files changed, 853 insertions(+)
create mode 100644 drivers/staging/iio/magnetometer/Kconfig
create mode 100644 drivers/staging/iio/magnetometer/Makefile
create mode 100644 drivers/staging/iio/magnetometer/TODO
create mode 100644 drivers/staging/iio/magnetometer/qmc5883p.c
diff --git a/drivers/staging/iio/Kconfig b/drivers/staging/iio/Kconfig
index a60631c1f449..d363e163d248 100644
--- a/drivers/staging/iio/Kconfig
+++ b/drivers/staging/iio/Kconfig
@@ -10,5 +10,6 @@ source "drivers/staging/iio/adc/Kconfig"
source "drivers/staging/iio/addac/Kconfig"
source "drivers/staging/iio/frequency/Kconfig"
source "drivers/staging/iio/impedance-analyzer/Kconfig"
+source "drivers/staging/iio/magnetometer/Kconfig"
endmenu
diff --git a/drivers/staging/iio/Makefile b/drivers/staging/iio/Makefile
index 628583535393..7dcbb75d43f0 100644
--- a/drivers/staging/iio/Makefile
+++ b/drivers/staging/iio/Makefile
@@ -8,3 +8,4 @@ obj-y += adc/
obj-y += addac/
obj-y += frequency/
obj-y += impedance-analyzer/
+obj-y += magnetometer/
diff --git a/drivers/staging/iio/magnetometer/Kconfig b/drivers/staging/iio/magnetometer/Kconfig
new file mode 100644
index 000000000000..d631da9578a1
--- /dev/null
+++ b/drivers/staging/iio/magnetometer/Kconfig
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Magnetometer sensors
+#
+# When adding new entries keep the list in alphabetical order
+
+menu "Magnetometer sensors"
+
+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/staging/iio/magnetometer/Makefile b/drivers/staging/iio/magnetometer/Makefile
new file mode 100644
index 000000000000..8e650f2e3b02
--- /dev/null
+++ b/drivers/staging/iio/magnetometer/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for staging industrial I/O Magnetometer sensor devices
+#
+# When adding new entries keep the list in alphabetical order
+
+obj-$(CONFIG_QMC5883P) += qmc5883p.o
diff --git a/drivers/staging/iio/magnetometer/TODO b/drivers/staging/iio/magnetometer/TODO
new file mode 100644
index 000000000000..6a8084c0dded
--- /dev/null
+++ b/drivers/staging/iio/magnetometer/TODO
@@ -0,0 +1,5 @@
+TODO
+====
+- Implement triggered buffer support (iio_triggered_buffer_setup)
+- Add interrupt (DRDY) support
+- Implement self-test (selftest regmap field is unused)
diff --git a/drivers/staging/iio/magnetometer/qmc5883p.c b/drivers/staging/iio/magnetometer/qmc5883p.c
new file mode 100644
index 000000000000..6a71dc47efb9
--- /dev/null
+++ b/drivers/staging/iio/magnetometer/qmc5883p.c
@@ -0,0 +1,819 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * qmc5883p.c - QMC5883P magnetometer driver
+ *
+ * Copyright 2026 Hardik Phalet <hardik.phalet@pm.me>
+ */
+
+#include <linux/array_size.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/sysfs.h>
+#include <linux/iio/types.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.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
+
+/* Downsampling rate */
+#define QMC5883P_DSR_1 0x00
+#define QMC5883P_DSR_2 0x01
+#define QMC5883P_DSR_4 0x02
+#define QMC5883P_DSR_8 0x03
+
+#define QMC5883P_RSTCTRL_SET_RESET \
+ 0x00 /* Set and reset on, i.e. the offset of device is renewed */
+#define QMC5883P_RSTCTRL_SET_ONLY 0x01 /* Set only on */
+#define QMC5883P_RSTCTRL_OFF 0x02 /* Set and reset off */
+
+#define QMC5883P_RNG_30G 0x00
+#define QMC5883P_RNG_12G 0x01
+#define QMC5883P_RNG_08G 0x02
+#define QMC5883P_RNG_02G 0x03
+
+#define QMC5883P_DEFAULT_ODR QMC5883P_ODR_100
+#define QMC5883P_DEFAULT_OSR QMC5883P_OSR_4
+#define QMC5883P_DEFAULT_DSR QMC5883P_DSR_4
+#define QMC5883P_DEFAULT_RNG QMC5883P_RNG_08G
+
+#define QMC5883P_DRDY_POLL_US 1000
+
+#define QMC5883P_CHIP_ID 0x80
+
+#define QMC5883P_STATUS_DRDY BIT(0)
+#define QMC5883P_STATUS_OVFL BIT(1)
+
+/*
+ * Scale factors in T/LSB for IIO_VAL_FRACTIONAL (val/val2), derived from
+ * datasheet Table 2 sensitivities (LSB/G) converted to LSB/T (1 G = 1e-4 T):
+ * sensitivity_T = sensitivity_G * 10000
+ * scale = 1 / sensitivity_T
+ *
+ * Index matches register value: RNG<1:0> = 0b00..0b11
+ */
+static const int qmc5883p_scale[][2] = {
+ [QMC5883P_RNG_30G] = { 1, 10000000 },
+ [QMC5883P_RNG_12G] = { 1, 25000000 },
+ [QMC5883P_RNG_08G] = { 1, 37500000 },
+ [QMC5883P_RNG_02G] = { 1, 150000000 },
+};
+
+static const int qmc5883p_odr[] = {
+ [QMC5883P_ODR_10] = 10,
+ [QMC5883P_ODR_50] = 50,
+ [QMC5883P_ODR_100] = 100,
+ [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 unsigned int qmc5883p_dsr[] = {
+ [QMC5883P_DSR_1] = 1,
+ [QMC5883P_DSR_2] = 2,
+ [QMC5883P_DSR_4] = 4,
+ [QMC5883P_DSR_8] = 8,
+};
+
+struct qmc5883p_rf {
+ struct regmap_field *osr;
+ struct regmap_field *dsr;
+ struct regmap_field *odr;
+ struct regmap_field *mode;
+ struct regmap_field *rng;
+ struct regmap_field *rstctrl;
+ struct regmap_field *sftrst;
+ struct regmap_field *selftest;
+ struct regmap_field *chip_id;
+};
+
+static const struct regmap_range qmc5883p_readable_ranges[] = {
+ regmap_reg_range(QMC5883P_REG_CHIP_ID, QMC5883P_REG_STATUS),
+ regmap_reg_range(QMC5883P_REG_CTRL_1, 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),
+};
+
+/*
+ * 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,
+};
+
+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,
+};
+
+static const struct reg_field qmc5883p_rf_osr =
+ REG_FIELD(QMC5883P_REG_CTRL_1, 4, 5);
+static const struct reg_field qmc5883p_rf_dsr =
+ REG_FIELD(QMC5883P_REG_CTRL_1, 6, 7);
+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_rstctrl =
+ REG_FIELD(QMC5883P_REG_CTRL_2, 0, 1);
+static const struct reg_field qmc5883p_rf_sftrst =
+ REG_FIELD(QMC5883P_REG_CTRL_2, 7, 7);
+static const struct reg_field qmc5883p_rf_selftest =
+ REG_FIELD(QMC5883P_REG_CTRL_2, 6, 6);
+static const struct reg_field qmc5883p_rf_chip_id =
+ REG_FIELD(QMC5883P_REG_CHIP_ID, 0, 7);
+
+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->dsr = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_dsr);
+ if (IS_ERR(rf->dsr))
+ return PTR_ERR(rf->dsr);
+
+ 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->rstctrl = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_rstctrl);
+ if (IS_ERR(rf->rstctrl))
+ return PTR_ERR(rf->rstctrl);
+
+ rf->sftrst = devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_sftrst);
+ if (IS_ERR(rf->sftrst))
+ return PTR_ERR(rf->sftrst);
+
+ rf->selftest =
+ devm_regmap_field_alloc(dev, regmap, qmc5883p_rf_selftest);
+ if (IS_ERR(rf->selftest))
+ return PTR_ERR(rf->selftest);
+
+ 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_verify_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)
+ return dev_err_probe(data->dev, -ENODEV,
+ "unexpected chip ID 0x%02x, expected 0x%02x\n",
+ regval, QMC5883P_CHIP_ID);
+ return ret;
+}
+
+static int qmc5883p_chip_init(struct qmc5883p_data *data)
+{
+ int ret;
+
+ ret = regmap_field_write(data->rf.sftrst, 1);
+ if (ret)
+ return ret;
+
+ usleep_range(1000, 2000);
+
+ ret = regmap_field_write(data->rf.sftrst, 0);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_write(data->rf.rstctrl, QMC5883P_RSTCTRL_SET_RESET);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_write(data->rf.rng, QMC5883P_DEFAULT_RNG);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_write(data->rf.osr, QMC5883P_DEFAULT_OSR);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_write(data->rf.dsr, QMC5883P_DEFAULT_DSR);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_write(data->rf.odr, QMC5883P_DEFAULT_ODR);
+ if (ret)
+ return ret;
+
+ return regmap_field_write(data->rf.mode, QMC5883P_MODE_NORMAL);
+}
+
+/*
+ * qmc5883p_get_measure - read all three axes.
+ * Must be called with data->mutex held.
+ * Handles PM internally: resumes device, reads data, schedules autosuspend.
+ */
+static int qmc5883p_get_measure(struct qmc5883p_data *data, s16 *x, s16 *y,
+ s16 *z)
+{
+ int ret;
+ 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
+ * 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, 150000);
+ if (ret)
+ goto out;
+
+ if (status & QMC5883P_STATUS_OVFL) {
+ dev_warn_ratelimited(data->dev,
+ "data overflow, consider reducing field range\n");
+ ret = -ERANGE;
+ goto out;
+ }
+
+ ret = regmap_bulk_read(data->regmap, QMC5883P_REG_X_LSB, reg_data,
+ ARRAY_SIZE(reg_data));
+ if (ret)
+ goto out;
+
+ *x = (s16)((reg_data[1] << 8) | reg_data[0]);
+ *y = (s16)((reg_data[3] << 8) | reg_data[2]);
+ *z = (s16)((reg_data[5] << 8) | reg_data[4]);
+
+out:
+ pm_runtime_mark_last_busy(data->dev);
+ pm_runtime_put_autosuspend(data->dev);
+ return ret;
+}
+
+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_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 ssize_t downsampling_ratio_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct iio_dev *indio_dev = dev_get_drvdata(dev);
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+ unsigned int regval;
+ int ret;
+
+ guard(mutex)(&data->mutex);
+
+ ret = regmap_field_read(data->rf.dsr, ®val);
+ if (ret)
+ return ret;
+
+ return sysfs_emit(buf, "%u\n", qmc5883p_dsr[regval]);
+}
+
+static ssize_t downsampling_ratio_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct iio_dev *indio_dev = dev_get_drvdata(dev);
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+ unsigned int val;
+ int i, ret, restore;
+
+ ret = kstrtouint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&data->mutex);
+
+ ret = pm_runtime_resume_and_get(dev);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+ if (ret)
+ goto out;
+
+ ret = -EINVAL;
+ for (i = 0; i < ARRAY_SIZE(qmc5883p_dsr); i++) {
+ if (qmc5883p_dsr[i] == val) {
+ ret = regmap_field_write(data->rf.dsr, i);
+ break;
+ }
+ }
+
+ restore = regmap_field_write(data->rf.mode, QMC5883P_MODE_NORMAL);
+ if (restore && !ret)
+ ret = restore;
+
+out:
+ pm_runtime_mark_last_busy(dev);
+ pm_runtime_put_autosuspend(dev);
+ return ret ? ret : (ssize_t)len;
+}
+
+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_FRACTIONAL;
+
+ 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;
+
+ 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;
+}
+
+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 = 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);
+ 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;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ restore = regmap_field_write(data->rf.mode, QMC5883P_MODE_NORMAL);
+ if (restore && !ret)
+ ret = restore;
+
+out:
+ pm_runtime_mark_last_busy(data->dev);
+ pm_runtime_put_autosuspend(data->dev);
+ return ret;
+}
+
+/*
+ * qmc5883p_read_avail - expose available values to userspace.
+ *
+ * Creates the _available sysfs attributes automatically:
+ * in_magn_sampling_frequency_available
+ * in_magn_oversampling_ratio_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_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_FRACTIONAL;
+ *length = ARRAY_SIZE(qmc5883p_scale) * 2;
+ return IIO_AVAIL_LIST;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static IIO_DEVICE_ATTR(downsampling_ratio, 0644, downsampling_ratio_show,
+ downsampling_ratio_store, 0);
+static IIO_CONST_ATTR(downsampling_ratio_available, "1 2 4 8");
+
+static struct attribute *qmc5883p_attributes[] = {
+ &iio_dev_attr_downsampling_ratio.dev_attr.attr,
+ &iio_const_attr_downsampling_ratio_available.dev_attr.attr, NULL
+};
+
+static const struct attribute_group qmc5883p_attribute_group = {
+ .attrs = qmc5883p_attributes,
+};
+
+static const struct iio_info qmc5883p_info = {
+ .attrs = &qmc5883p_attribute_group,
+ .read_raw = qmc5883p_read_raw,
+ .write_raw = qmc5883p_write_raw,
+ .read_avail = qmc5883p_read_avail,
+};
+
+static const struct iio_chan_spec qmc5883p_channels[] = {
+ {
+ .type = IIO_MAGN,
+ .channel2 = IIO_MOD_X,
+ .modified = 1,
+ .address = AXIS_X,
+ .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) |
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
+ .info_mask_shared_by_type_available =
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
+ },
+ {
+ .type = IIO_MAGN,
+ .channel2 = IIO_MOD_Y,
+ .modified = 1,
+ .address = AXIS_Y,
+ .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) |
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
+ .info_mask_shared_by_type_available =
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
+ },
+ {
+ .type = IIO_MAGN,
+ .channel2 = IIO_MOD_Z,
+ .modified = 1,
+ .address = AXIS_Z,
+ .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) |
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
+ .info_mask_shared_by_type_available =
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
+ },
+};
+
+static void qmc5883p_runtime_pm_disable(void *dev)
+{
+ pm_runtime_disable(dev);
+ pm_runtime_set_suspended(dev);
+}
+
+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);
+ data->dev = dev;
+ data->regmap = regmap;
+ mutex_init(&data->mutex);
+
+ i2c_set_clientdata(client, indio_dev);
+
+ ret = qmc5883p_rf_init(data);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to initialize regmap fields\n");
+
+ ret = qmc5883p_verify_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");
+
+ 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);
+
+ pm_runtime_set_autosuspend_delay(dev, 2000);
+ pm_runtime_use_autosuspend(dev);
+
+ pm_runtime_set_active(dev);
+ pm_runtime_enable(dev);
+
+ ret = devm_add_action_or_reset(dev,
+ (void (*)(void *))qmc5883p_runtime_pm_disable,
+ dev);
+ if (ret)
+ return ret;
+
+ pm_runtime_mark_last_busy(dev);
+
+ ret = devm_iio_device_register(dev, indio_dev);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to register IIO device\n");
+ return 0;
+}
+
+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);
+ int ret;
+
+ ret = regmap_field_write(data->rf.mode, QMC5883P_MODE_NORMAL);
+ if (ret)
+ return ret;
+
+ usleep_range(10000, 11000);
+ return 0;
+}
+
+static int qmc5883p_runtime_idle(struct device *dev)
+{
+ return 0;
+}
+
+static int qmc5883p_system_suspend(struct device *dev)
+{
+ return pm_runtime_force_suspend(dev);
+}
+
+static int qmc5883p_system_resume(struct device *dev)
+{
+ return pm_runtime_force_resume(dev);
+}
+
+static void qmc5883p_remove(struct i2c_client *client)
+{
+ struct iio_dev *indio_dev = i2c_get_clientdata(client);
+ struct qmc5883p_data *data = iio_priv(indio_dev);
+
+ /*
+ * Best effort: put device to sleep on removal.
+ * Ignore error since we cannot do anything useful with it here.
+ * Runtime PM disable is handled by the devm cleanup action registered
+ * in probe.
+ */
+ regmap_field_write(data->rf.mode, QMC5883P_MODE_SUSPEND);
+}
+
+static const struct dev_pm_ops qmc5883p_dev_pm_ops = {
+ SYSTEM_SLEEP_PM_OPS(qmc5883p_system_suspend, qmc5883p_system_resume)
+ RUNTIME_PM_OPS(qmc5883p_runtime_suspend, qmc5883p_runtime_resume, NULL)
+};
+
+static const struct of_device_id qmc5883p_of_match[] = {
+ { .compatible = "qst,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,
+ .pm = pm_ptr(&qmc5883p_dev_pm_ops),
+ },
+ .probe = qmc5883p_probe,
+ .remove = qmc5883p_remove,
+ .id_table = qmc5883p_id,
+};
+module_i2c_driver(qmc5883p_driver);
+
+MODULE_AUTHOR("Hardik Phalet <hardik.phalet@pm.me>");
+MODULE_DESCRIPTION("QMC5883P magnetic sensor driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH 2/4] dt-bindings: iio: magnetometer: Add binding for QST QMC5883P
From: Hardik Phalet @ 2026-04-09 16:24 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: Jonathan Cameron, David Lechner, Nuno Sá, Andy Shevchenko,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Brigham Campbell,
Shuah Khan, linux-iio, devicetree, linux-kernel, linux-staging,
Hardik Phalet
In-Reply-To: <20260409162308.2590385-1-hardik.phalet@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. The binding exposes the required 'compatible'
and 'reg' properties along with an optional 'vdd-supply' for the
2.5 V–3.6 V VDD rail.
Signed-off-by: Hardik Phalet <hardik.phalet@pm.me>
---
.../iio/magnetometer/qst,qmc5883p.yaml | 48 +++++++++++++++++++
1 file changed, 48 insertions(+)
create mode 100644 Documentation/devicetree/bindings/iio/magnetometer/qst,qmc5883p.yaml
diff --git a/Documentation/devicetree/bindings/iio/magnetometer/qst,qmc5883p.yaml b/Documentation/devicetree/bindings/iio/magnetometer/qst,qmc5883p.yaml
new file mode 100644
index 000000000000..84fec10d8b9a
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/magnetometer/qst,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/qst,qmc5883p.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: QST 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: qst,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
+
+additionalProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ magnetometer@2c {
+ compatible = "qst,qmc5883p";
+ reg = <0x2c>;
+ vdd-supply = <&vdd_3v3>;
+ };
+ };
+...
--
2.53.0
^ permalink raw reply related
* [PATCH 1/4] dt-bindings: vendor-prefixes: Add QST Corporation
From: Hardik Phalet @ 2026-04-09 16:23 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: Jonathan Cameron, David Lechner, Nuno Sá, Andy Shevchenko,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Brigham Campbell,
Shuah Khan, linux-iio, devicetree, linux-kernel, linux-staging,
Hardik Phalet
In-Reply-To: <20260409162308.2590385-1-hardik.phalet@pm.me>
Add the vendor prefix 'qst' 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 5d2a7a8d3ac6..71a1b9087c5e 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1244,6 +1244,8 @@ patternProperties:
description: Shenzhen QiShenglong Industrialist Co., Ltd.
"^qnap,.*":
description: QNAP Systems, Inc.
+ "^qst,.*":
+ description: QST Corporation
"^quanta,.*":
description: Quanta Computer Inc.
"^radxa,.*":
--
2.53.0
^ permalink raw reply related
* [PATCH 0/4] Add QST QMC5883P magnetometer driver
From: Hardik Phalet @ 2026-04-09 16:23 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: Jonathan Cameron, David Lechner, Nuno Sá, Andy Shevchenko,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Brigham Campbell,
Shuah Khan, linux-iio, devicetree, linux-kernel, linux-staging,
Hardik Phalet
This series adds initial Linux support for the QST QMC5883P, a 3-axis
anisotropic magneto-resistive (AMR) magnetometer with a 16-bit ADC that
communicates over I2C. To my knowledge there is no existing upstream
driver for this device.
The driver supports:
- Raw magnetic field readings on X, Y, and Z axes
- Four selectable full-scale ranges (±2 G, ±8 G, ±12 G, ±30 G)
- Configurable output data rate (10, 50, 100, 200 Hz)
- Configurable oversampling ratio (1, 2, 4, 8)
- Configurable downsampling ratio (1, 2, 4, 8) via a custom sysfs
attribute
- Runtime PM with a 2 s autosuspend delay
- System suspend/resume via pm_runtime_force_suspend/resume
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 init sequence on probe is: soft reset → wait 1 ms → deassert
reset → configure SET/RESET control → apply default ODR/OSR/DSR/RNG
→ enter normal mode. This ordering was determined empirically on
hardware to produce reliable, non-zero axis readings.
The driver is placed under drivers/staging/iio/magnetometer/ with a
TODO file tracking the remaining work before it can graduate:
- Triggered buffer support (iio_triggered_buffer_setup)
- DRDY interrupt support
- Self-test implementation
Patches:
1/4 - dt-bindings: vendor-prefixes: Add 'qst' for QST Corporation
2/4 - dt-bindings: iio: magnetometer: Add binding for QST QMC5883P
3/4 - staging: iio: magnetometer: Add QST QMC5883P driver
4/4 - MAINTAINERS: Add entry for QST QMC5883P magnetometer driver
Testing
-------
Tested on a Raspberry Pi 4B running a mainline kernel (aarch64) with a
GY-271 HM-246 board connected via I2C bus 1. The chip was confirmed to
enumerate at address 0x2C via i2cdetect.
The driver was cross-compiled from Fedora (x86_64) targeting aarch64
and loaded as a module (qmc5883p.ko) with the Device Tree overlay
pointing at i2c1:0x2c.
Verification steps performed:
- Chip ID register (0x00) reads back 0x80 on probe, confirming the
correct device is present
- All three axes (in_magn_x_raw, in_magn_y_raw, in_magn_z_raw) return
non-zero, stable values when the board is held still and change
appropriately when the board is rotated
- in_magn_x_scale (and Y, Z) returns the expected fractional value for
the default ±8 G range (1/37500000)
- in_magn_sampling_frequency / _available, in_magn_oversampling_ratio /
_available, and downsampling_ratio / downsampling_ratio_available all
read and write correctly; the chip responds without error to each
valid setting
- Runtime PM: after 2 s of inactivity the device enters suspend mode
(MODE = 0x00 confirmed via i2cdump); the next sysfs read correctly
resumes the device and returns valid data
- System suspend/resume (echo mem > /sys/power/state) leaves the
driver in a consistent state; readings remain valid after resume
- dt_binding_check passes for patch 2/4
- Kernel builds cleanly with W=1 and no new warnings
Hardik Phalet (4):
dt-bindings: vendor-prefixes: Add QST Corporation
dt-bindings: iio: magnetometer: Add binding for QST QMC5883P
Subject: [PATCH 3/4] staging: iio: magnetometer: Add QST QMC5883P
driver
MAINTAINERS: Add entry for QST QMC5883P magnetometer driver
.../iio/magnetometer/qst,qmc5883p.yaml | 48 +
.../devicetree/bindings/vendor-prefixes.yaml | 2 +
MAINTAINERS | 7 +
drivers/staging/iio/Kconfig | 1 +
drivers/staging/iio/Makefile | 1 +
drivers/staging/iio/magnetometer/Kconfig | 20 +
drivers/staging/iio/magnetometer/Makefile | 7 +
drivers/staging/iio/magnetometer/TODO | 5 +
drivers/staging/iio/magnetometer/qmc5883p.c | 819 ++++++++++++++++++
9 files changed, 910 insertions(+)
create mode 100644 Documentation/devicetree/bindings/iio/magnetometer/qst,qmc5883p.yaml
create mode 100644 drivers/staging/iio/magnetometer/Kconfig
create mode 100644 drivers/staging/iio/magnetometer/Makefile
create mode 100644 drivers/staging/iio/magnetometer/TODO
create mode 100644 drivers/staging/iio/magnetometer/qmc5883p.c
--
2.53.0
^ permalink raw reply
* Re: [PATCH] dt-bindings: i2c: nxp,pca9564: convert to DT schema
From: Akhila YS @ 2026-04-09 16:21 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: Andi Shyti, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Peter Rosin, linux-i2c, devicetree, linux-kernel
In-Reply-To: <7930789e-6930-45b9-997b-0e5c850db194@gmail.com>
On 09-04-2026 20:32, Akhila YS wrote:
> On 09-04-2026 13:56, Krzysztof Kozlowski wrote:
>> On Wed, Apr 08, 2026 at 08:23:31AM +0000, Akhila YS wrote:
>>> Convert NXP PCA PCA9564/PCA9665 I2C controller to YAML format.
>> DT schema, not YAML format. Look at your subject.
>
> Okay, i will change it.
>
>> ...
>>
>>> + reg:
>>> + maxItems: 1
>>> +
>>> + "#address-cells":
>>> + const: 1
>>> +
>>> + "#size-cells":
>>> + const: 0
>>> +
>>> + interrupts:
>>> + maxItems: 1
>>> +
>>> + reset-gpios:
>>> + maxItems: 1
>>> +
>>> + clock-frequency:
>>> + default: 100000
>>> +
>>> +required:
>>> + - compatible
>>> + - reg
>>> +
>>> +additionalProperties: false
>> And if you tested any DTS with this, you would see this cannot work.
>> Look at other bindings - you miss ref to i2c-controller and
>> unevaluatedProps. But the problem is that you are doing something which
>> would never work, so I have doubts that you know what you are doing. One
>> thing is to make a mistake, other thing is to post something can never
>> work thus putting quite noticeable requirements on review.
>
> You are right, i missed referencing the common i2c controller schema and
> did not handle unevaluated properties correctly,which makes binding invalid.
There is no file named i2c-controller.yaml to take ref to this yaml. I
tried testing the yaml file with the dts in my local machine and its
working as expected.
I even checked the status in patchwork and there it was marked as
successful.
>
>> Please first learn how DTS and DT bindings work, before you post new
>> patches.
>
> Thank you for the review, i will work on it.
>
>> Best regards,
>> Krzysztof
>>
--
Best Regards,
Akhila.
^ permalink raw reply
* RE: [PATCH v2 24/24] arm64: dts: renesas: r9a09g047e57-smarc: add DA7212 audio codec support
From: John Madieu @ 2026-04-09 16:14 UTC (permalink / raw)
To: geert
Cc: Kuninori Morimoto, Vinod Koul, Mark Brown, Rob Herring,
Krzysztof Kozlowski, Michael Turquette, Stephen Boyd,
Conor Dooley, Frank Li, Liam Girdwood, magnus.damm,
Thomas Gleixner, Jaroslav Kysela, Takashi Iwai, Philipp Zabel,
Claudiu.Beznea, Biju Das, Fabrizio Castro, Prabhakar Mahadev Lad,
John Madieu, linux-renesas-soc@vger.kernel.org,
linux-clk@vger.kernel.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, dmaengine@vger.kernel.org,
linux-sound@vger.kernel.org
In-Reply-To: <CAMuHMdVLb3Wj=4qK_5jLsiN28i2LDYPVH9ch91Y6e8XyT+yjjA@mail.gmail.com>
Hi Geert,
Thank you for your review.
> -----Original Message-----
> From: Geert Uytterhoeven <geert@linux-m68k.org>
> Sent: Wednesday, April 8, 2026 11:41 AM
> To: John Madieu <john.madieu.xa@bp.renesas.com>
> Subject: Re: [PATCH v2 24/24] arm64: dts: renesas: r9a09g047e57-smarc: add
> DA7212 audio codec support
>
> Hi John,
>
> On Thu, 2 Apr 2026 at 11:10, John Madieu <john.madieu.xa@bp.renesas.com>
> wrote:
> > RZ/G3E SMARC board has a DA7212 audio codec connected via I2C1 for
> > sound input/output using SSI3/SSI4 where:
> >
> > - The codec receives its master clock from the Versa3 clock
> > generator present on the SoM
> > - SSI4 shares clock pins with SSI3 to provide a separate data
> > line for full-duplex audio capture.
> >
> > Enable audio support on RZ/G3E SMARC2 EVK boards with a DA7212 audio
> codec.
> >
> > Signed-off-by: John Madieu <john.madieu.xa@bp.renesas.com>
>
> Thanks for your patch!
>
> > --- a/arch/arm64/boot/dts/renesas/r9a09g047e57-smarc.dts
> > +++ b/arch/arm64/boot/dts/renesas/r9a09g047e57-smarc.dts
>
> > @@ -280,6 +358,42 @@ &sdhi1 {
> > vqmmc-supply = <&vqmmc_sd1_pvdd>; };
> >
> > +&snd_rzg3e {
>
> Please preserve sort order (alphabetical, by label).
Noted for v3.
Regards,
John
>
> Gr{oetje,eeting}s,
>
> Geert
>
> --
> Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-
> m68k.org
>
> In personal conversations with technical people, I call myself a hacker.
> But when I'm talking to journalists I just say "programmer" or something
> like that.
> -- Linus Torvalds
^ permalink raw reply
* Re: [PATCH 2/3] pwm: rp1: Add RP1 PWM controller driver
From: Andrea della Porta @ 2026-04-09 16:16 UTC (permalink / raw)
To: Uwe Kleine-König
Cc: Andrea della Porta, linux-pwm, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Florian Fainelli,
Broadcom internal kernel review list, devicetree,
linux-rpi-kernel, linux-arm-kernel, linux-kernel, Naushir Patuck,
Stanimir Varbanov
In-Reply-To: <adLTwOTbkJ0VQXy6@monoceros>
Hi Uwe,
On 23:45 Sun 05 Apr , Uwe Kleine-König wrote:
> Hello Andrea,
>
> On Fri, Apr 03, 2026 at 04:31:55PM +0200, Andrea della Porta wrote:
> > From: Naushir Patuck <naush@raspberrypi.com>
> >
> > The Raspberry Pi RP1 southbridge features an embedded PWM
> > controller with 4 output channels, alongside an RPM interface
> > to read the fan speed on the Raspberry Pi 5.
> >
> > Add the supporting driver.
> >
> > Signed-off-by: Naushir Patuck <naush@raspberrypi.com>
> > Co-developed-by: Stanimir Varbanov <svarbanov@suse.de>
> > Signed-off-by: Stanimir Varbanov <svarbanov@suse.de>
> > Signed-off-by: Andrea della Porta <andrea.porta@suse.com>
> > ---
> > drivers/pwm/Kconfig | 10 ++
> > drivers/pwm/Makefile | 1 +
> > drivers/pwm/pwm-rp1.c | 244 ++++++++++++++++++++++++++++++++++++++++++
> > 3 files changed, 255 insertions(+)
> > create mode 100644 drivers/pwm/pwm-rp1.c
> >
> > diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> > index 6f3147518376a..22e4fc6385da2 100644
> > --- a/drivers/pwm/Kconfig
> > +++ b/drivers/pwm/Kconfig
> > @@ -625,6 +625,16 @@ config PWM_ROCKCHIP
> > Generic PWM framework driver for the PWM controller found on
> > Rockchip SoCs.
> >
> > +config PWM_RP1
>
> I prefer PWM_RASPBERRYPI1, or PWM_RASPBERRYPI_RP1 here.
Ack.
>
> > + tristate "RP1 PWM support"
> > + depends on MISC_RP1 || COMPILE_TEST
> > + depends on HWMON
> > + help
> > + PWM framework driver for Raspberry Pi RP1 controller
> > +
> > + To compile this driver as a module, choose M here: the module
> > + will be called pwm-rp1.
> > +
> > config PWM_SAMSUNG
> > tristate "Samsung PWM support"
> > depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST
> > diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
> > index 0dc0d2b69025d..895a7c42fe9c0 100644
> > --- a/drivers/pwm/Makefile
> > +++ b/drivers/pwm/Makefile
> > @@ -56,6 +56,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o
> > obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o
> > obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
> > obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
> > +obj-$(CONFIG_PWM_RP1) += pwm-rp1.o
> > obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
> > obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
> > obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
> > diff --git a/drivers/pwm/pwm-rp1.c b/drivers/pwm/pwm-rp1.c
> > new file mode 100644
> > index 0000000000000..0a1c1c1dd27e9
> > --- /dev/null
> > +++ b/drivers/pwm/pwm-rp1.c
> > @@ -0,0 +1,244 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * pwm-rp1.c
> > + *
> > + * Raspberry Pi RP1 PWM.
> > + *
> > + * Copyright © 2026 Raspberry Pi Ltd.
> > + *
> > + * Author: Naushir Patuck (naush@raspberrypi.com)
> > + *
> > + * Based on the pwm-bcm2835 driver by:
> > + * Bart Tanghe <bart.tanghe@thomasmore.be>
> > + */
>
> Please add a paragraph here named "Limitations" in the same format as
> several other drivers describing how the driver behaves on disable and
> configuration changes (can glitches occur? Is the currently running
> period completed or aborted?)
Ack.
>
> > +#include <linux/bitops.h>
> > +#include <linux/clk.h>
> > +#include <linux/err.h>
> > +#include <linux/hwmon.h>
> > +#include <linux/io.h>
> > +#include <linux/module.h>
> > +#include <linux/of.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/pwm.h>
> > +
> > +#define PWM_GLOBAL_CTRL 0x000
> > +#define PWM_CHANNEL_CTRL(x) (0x014 + ((x) * 0x10))
> > +#define PWM_RANGE(x) (0x018 + ((x) * 0x10))
> > +#define PWM_PHASE(x) (0x01C + ((x) * 0x10))
> > +#define PWM_DUTY(x) (0x020 + ((x) * 0x10))
> > +
> > +/* 8:FIFO_POP_MASK + 0:Trailing edge M/S modulation */
> > +#define PWM_CHANNEL_DEFAULT (BIT(8) + BIT(0))
> > +#define PWM_CHANNEL_ENABLE(x) BIT(x)
> > +#define PWM_POLARITY BIT(3)
> > +#define SET_UPDATE BIT(31)
> > +#define PWM_MODE_MASK GENMASK(1, 0)
> > +
> > +#define NUM_PWMS 4
>
> Please prefix all #defines by something driver specific (e.g. RP1_PWM_).
Ack.
>
> > +
> > +struct rp1_pwm {
> > + void __iomem *base;
> > + struct clk *clk;
> > +};
> > +
> > +static const struct hwmon_channel_info * const rp1_fan_hwmon_info[] = {
> > + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT),
> > + NULL
> > +};
> > +
> > +static umode_t rp1_fan_hwmon_is_visible(const void *data, enum hwmon_sensor_types type,
> > + u32 attr, int channel)
> > +{
> > + umode_t mode = 0;
> > +
> > + if (type == hwmon_fan && attr == hwmon_fan_input)
> > + mode = 0444;
> > +
> > + return mode;
> > +}
> > +
> > +static int rp1_fan_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
> > + u32 attr, int channel, long *val)
> > +{
> > + struct rp1_pwm *rp1 = dev_get_drvdata(dev);
> > +
> > + if (type != hwmon_fan || attr != hwmon_fan_input)
> > + return -EOPNOTSUPP;
> > +
> > + *val = readl(rp1->base + PWM_PHASE(2));
> > +
> > + return 0;
> > +}
>
> I don't like having hwmon bits in pwm drivers. Is the PWM only usable
> for a fan? I guess the hwmon parts should be dropped and a pwm-fan
> defined in dt.
The pwm-fan generic driver expects an interrupt to count the RPM, while
on RP1 this data is passed via a register filled by the RP1 fw running
on the internal core. Instead of changing the generic pwm-fan driver,
I'll add a syscon to export this register which will be read by a new
device/driver registering an hwmon device.
>
> > +static const struct hwmon_ops rp1_fan_hwmon_ops = {
> > + .is_visible = rp1_fan_hwmon_is_visible,
> > + .read = rp1_fan_hwmon_read,
> > +};
> > +
> > +static const struct hwmon_chip_info rp1_fan_hwmon_chip_info = {
> > + .ops = &rp1_fan_hwmon_ops,
> > + .info = rp1_fan_hwmon_info,
> > +};
> > +
> > +static void rp1_pwm_apply_config(struct pwm_chip *chip, struct pwm_device *pwm)
> > +{
> > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip);
> > + u32 value;
> > +
> > + value = readl(rp1->base + PWM_GLOBAL_CTRL);
> > + value |= SET_UPDATE;
> > + writel(value, rp1->base + PWM_GLOBAL_CTRL);
> > +}
> > +
> > +static int rp1_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm)
> > +{
> > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip);
> > +
> > + writel(PWM_CHANNEL_DEFAULT, rp1->base + PWM_CHANNEL_CTRL(pwm->hwpwm));
>
> Please add a comment about what this does.
Ack.
>
> > + return 0;
> > +}
> > +
> > +static void rp1_pwm_free(struct pwm_chip *chip, struct pwm_device *pwm)
> > +{
> > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip);
> > + u32 value;
> > +
> > + value = readl(rp1->base + PWM_CHANNEL_CTRL(pwm->hwpwm));
> > + value &= ~PWM_MODE_MASK;
> > + writel(value, rp1->base + PWM_CHANNEL_CTRL(pwm->hwpwm));
> > +
> > + rp1_pwm_apply_config(chip, pwm);
>
> What is the purpose of this call?
To update the configuration on the next PWM strobe in order to avoid
glitches. I'll add a short comment in the code.
>
> > +}
> > +
> > +static int rp1_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
> > + const struct pwm_state *state)
> > +{
> > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip);
> > + unsigned long clk_rate = clk_get_rate(rp1->clk);
> > + unsigned long clk_period;
> > + u32 value;
> > +
> > + if (!clk_rate) {
> > + dev_err(&chip->dev, "failed to get clock rate\n");
> > + return -EINVAL;
> > + }
> > +
> > + /* set period and duty cycle */
> > + clk_period = DIV_ROUND_CLOSEST(NSEC_PER_SEC, clk_rate);
>
> DIV_ROUND_CLOSEST is wrong here. (I don't go into details as .apply()
> should be dropped.)
Ack.
>
> > + writel(DIV_ROUND_CLOSEST(state->duty_cycle, clk_period),
>
> Dividing by the result of a division loses precision.
>
> > + rp1->base + PWM_DUTY(pwm->hwpwm));
> > +
> > + writel(DIV_ROUND_CLOSEST(state->period, clk_period),
> > + rp1->base + PWM_RANGE(pwm->hwpwm));
> > +
> > + /* set polarity */
> > + value = readl(rp1->base + PWM_CHANNEL_CTRL(pwm->hwpwm));
> > + if (state->polarity == PWM_POLARITY_NORMAL)
> > + value &= ~PWM_POLARITY;
> > + else
> > + value |= PWM_POLARITY;
> > + writel(value, rp1->base + PWM_CHANNEL_CTRL(pwm->hwpwm));
> > +
> > + /* enable/disable */
> > + value = readl(rp1->base + PWM_GLOBAL_CTRL);
> > + if (state->enabled)
> > + value |= PWM_CHANNEL_ENABLE(pwm->hwpwm);
> > + else
> > + value &= ~PWM_CHANNEL_ENABLE(pwm->hwpwm);
> > + writel(value, rp1->base + PWM_GLOBAL_CTRL);
> > +
> > + rp1_pwm_apply_config(chip, pwm);
> > +
> > + return 0;
> > +}
> > +
> > +static const struct pwm_ops rp1_pwm_ops = {
> > + .request = rp1_pwm_request,
> > + .free = rp1_pwm_free,
> > + .apply = rp1_pwm_apply,
>
> Please implement the waveform callbacks instead of .apply().
Ack.
>
> > +};
> > +
> > +static int rp1_pwm_probe(struct platform_device *pdev)
> > +{
> > + struct device *dev = &pdev->dev;
> > + struct device *hwmon_dev;
> > + struct pwm_chip *chip;
> > + struct rp1_pwm *rp1;
> > + int ret;
> > +
> > + chip = devm_pwmchip_alloc(dev, NUM_PWMS, sizeof(*rp1));
> > + if (IS_ERR(chip))
> > + return PTR_ERR(chip);
> > +
> > + rp1 = pwmchip_get_drvdata(chip);
> > +
> > + rp1->base = devm_platform_ioremap_resource(pdev, 0);
> > + if (IS_ERR(rp1->base))
> > + return PTR_ERR(rp1->base);
> > +
> > + rp1->clk = devm_clk_get_enabled(dev, NULL);
> > + if (IS_ERR(rp1->clk))
> > + return dev_err_probe(dev, PTR_ERR(rp1->clk), "clock not found\n");
>
> Please start error messages with a capital letter.
Ack.
>
> > +
> > + ret = devm_clk_rate_exclusive_get(dev, rp1->clk);
>
> After this call you can determine the rate just once and fail if it's ==
> 0.
Ack.
>
> > + if (ret)
> > + return dev_err_probe(dev, ret, "fail to get exclusive rate\n");
> > +
> > + chip->ops = &rp1_pwm_ops;
> > +
> > + platform_set_drvdata(pdev, chip);
> > +
> > + ret = devm_pwmchip_add(dev, chip);
> > + if (ret)
> > + return dev_err_probe(dev, ret, "failed to register PWM chip\n");
> > +
> > + hwmon_dev = devm_hwmon_device_register_with_info(dev, "rp1_fan_tach", rp1,
> > + &rp1_fan_hwmon_chip_info,
> > + NULL);
> > +
> > + if (IS_ERR(hwmon_dev))
> > + return dev_err_probe(dev, PTR_ERR(hwmon_dev),
> > + "failed to register hwmon fan device\n");
> > +
> > + return 0;
> > +}
> > +
> > +static int rp1_pwm_suspend(struct device *dev)
> > +{
> > + struct rp1_pwm *rp1 = dev_get_drvdata(dev);
> > +
> > + clk_disable_unprepare(rp1->clk);
> > +
> > + return 0;
> > +}
> > +
> > +static int rp1_pwm_resume(struct device *dev)
> > +{
> > + struct rp1_pwm *rp1 = dev_get_drvdata(dev);
> > +
> > + return clk_prepare_enable(rp1->clk);
>
> Hmm, if this fails and then the driver is unbound, the clk operations
> are not balanced.
I'll add some flags to check if the clock is really enabled or not.
Regards,
Andrea
>
> > +}
> > +
> > +static DEFINE_SIMPLE_DEV_PM_OPS(rp1_pwm_pm_ops, rp1_pwm_suspend, rp1_pwm_resume);
> > +
> > +static const struct of_device_id rp1_pwm_of_match[] = {
> > + { .compatible = "raspberrypi,rp1-pwm" },
> > + { /* sentinel */ }
> > +};
> > +MODULE_DEVICE_TABLE(of, rp1_pwm_of_match);
> > +
> > +static struct platform_driver rp1_pwm_driver = {
> > + .probe = rp1_pwm_probe,
> > + .driver = {
> > + .name = "rp1-pwm",
> > + .of_match_table = rp1_pwm_of_match,
> > + .pm = pm_ptr(&rp1_pwm_pm_ops),
> > + },
> > +};
> > +module_platform_driver(rp1_pwm_driver);
> > +
> > +MODULE_DESCRIPTION("RP1 PWM driver");
> > +MODULE_AUTHOR("Naushir Patuck <naush@raspberrypi.com>");
> > +MODULE_LICENSE("GPL");
> > --
> > 2.35.3
> >
>
> Best regards
> Uwe
^ permalink raw reply
* RE: [PATCH v2 02/24] clk: renesas: r9a09g047: Add audio clock and reset support
From: John Madieu @ 2026-04-09 16:12 UTC (permalink / raw)
To: geert
Cc: Geert Uytterhoeven, Kuninori Morimoto, Vinod Koul, Mark Brown,
Rob Herring, Krzysztof Kozlowski, Michael Turquette, Stephen Boyd,
Conor Dooley, Frank Li, Liam Girdwood, magnus.damm,
Thomas Gleixner, Jaroslav Kysela, Takashi Iwai, Philipp Zabel,
Claudiu.Beznea, Biju Das, Fabrizio Castro, Prabhakar Mahadev Lad,
John Madieu, linux-renesas-soc@vger.kernel.org,
linux-clk@vger.kernel.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, dmaengine@vger.kernel.org,
linux-sound@vger.kernel.org
In-Reply-To: <CAMuHMdU+a7cXRY=yEmXQW9=rYnyMCifhZs+je8LDHL6r=mBDMw@mail.gmail.com>
Hi Geert,
Thanks for the review.
> -----Original Message-----
> From: Geert Uytterhoeven <geert@linux-m68k.org>
> Sent: Wednesday, April 8, 2026 11:38 AM
> To: John Madieu <john.madieu.xa@bp.renesas.com>
> Subject: Re: [PATCH v2 02/24] clk: renesas: r9a09g047: Add audio clock and
> reset support
>
> Hi John,
>
> On Thu, 2 Apr 2026 at 11:07, John Madieu <john.madieu.xa@bp.renesas.com>
> wrote:
> > Add clock and reset entries for audio-related modules on the RZ/G3E SoC.
> >
> > Target modules are:
> > - SSIU (Serial Sound Interface Unit) with SSI ch0-ch9
> > - SCU (Sampling Rate Converter Unit) with SRC ch0-ch9, DVC ch0-ch1,
> > CTU/MIX ch0-ch1
> > - ADMAC (Audio DMA Controller)
> > - ADG (Audio Clock Generator) with divider input clocks and audio
> > master clock outputs
> >
> > While at it, reorder plldty_div16 to group it with other plldty fixed
> > dividers.
> >
> > Signed-off-by: John Madieu <john.madieu.xa@bp.renesas.com>
>
> Thanks for your patch!
>
> > --- a/drivers/clk/renesas/r9a09g047-cpg.c
> > +++ b/drivers/clk/renesas/r9a09g047-cpg.c
>
> > @@ -460,6 +483,96 @@ static const struct rzv2h_mod_clk
> r9a09g047_mod_clks[] __initconst = {
> > BUS_MSTOP(3, BIT(4))),
> > DEF_MOD("tsu_1_pclk", CLK_QEXTAL, 16, 10, 8,
> 10,
> > BUS_MSTOP(2,
> > BIT(15))),
> > + DEF_MOD("ssif_clk", CLK_PLLCLN_DIV8, 15, 5,
> 7, 21,
>
> Please preserve sort order (by _onindex, _onbit);
I'll address this in v3.
Regards,
John
>
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("scu_clk", CLK_PLLCLN_DIV8, 15, 6,
> 7, 22,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("scu_clkx2", CLK_PLLCLN_DIV4, 15, 7,
> 7, 23,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("admac_clk", CLK_PLLCLN_DIV8, 15, 8,
> 7, 24,
> > + BUS_MSTOP(2, BIT(5))),
> > + DEF_MOD("adg_clks1", CLK_PLLCLN_DIV8, 15, 9,
> 7, 25,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_clk_200m", CLK_PLLCLN_DIV8, 15, 10,
> 7, 26,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_audio_clka", CLK_AUDIO_CLKA, 15, 11,
> 7, 27,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_audio_clkb", CLK_AUDIO_CLKB, 15, 12,
> 7, 28,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_audio_clkc", CLK_AUDIO_CLKC, 15, 13,
> 7, 29,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi0_clk", CLK_PLLCLN_DIV8, 22, 0,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi1_clk", CLK_PLLCLN_DIV8, 22, 1,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi2_clk", CLK_PLLCLN_DIV8, 22, 2,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi3_clk", CLK_PLLCLN_DIV8, 22, 3,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi4_clk", CLK_PLLCLN_DIV8, 22, 4,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi5_clk", CLK_PLLCLN_DIV8, 22, 5,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi6_clk", CLK_PLLCLN_DIV8, 22, 6,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi7_clk", CLK_PLLCLN_DIV8, 22, 7,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi8_clk", CLK_PLLCLN_DIV8, 22, 8,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("adg_ssi9_clk", CLK_PLLCLN_DIV8, 22, 9,
> -1, -1,
> > + BUS_MSTOP(2, BIT(2))),
> > + DEF_MOD("dvc0_clk", CLK_PLLCLN_DIV8, 23, 0,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("dvc1_clk", CLK_PLLCLN_DIV8, 23, 1,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("ctu0_mix0_clk", CLK_PLLCLN_DIV8, 23, 2,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("ctu1_mix1_clk", CLK_PLLCLN_DIV8, 23, 3,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src0_clk", CLK_PLLCLN_DIV8, 23, 4,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src1_clk", CLK_PLLCLN_DIV8, 23, 5,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src2_clk", CLK_PLLCLN_DIV8, 23, 6,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src3_clk", CLK_PLLCLN_DIV8, 23, 7,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src4_clk", CLK_PLLCLN_DIV8, 23, 8,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src5_clk", CLK_PLLCLN_DIV8, 23, 9,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src6_clk", CLK_PLLCLN_DIV8, 23, 10,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src7_clk", CLK_PLLCLN_DIV8, 23, 11,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src8_clk", CLK_PLLCLN_DIV8, 23, 12,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("src9_clk", CLK_PLLCLN_DIV8, 23, 13,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("scu_supply_clk", CLK_PLLCLN_DIV8, 23, 14,
> -1, -1,
> > + BUS_MSTOP(2, BIT(0) |
> BIT(1))),
> > + DEF_MOD("ssif_supply_clk", CLK_PLLCLN_DIV8, 24, 0,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi0_clk", CLK_PLLCLN_DIV8, 24, 1,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi1_clk", CLK_PLLCLN_DIV8, 24, 2,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi2_clk", CLK_PLLCLN_DIV8, 24, 3,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi3_clk", CLK_PLLCLN_DIV8, 24, 4,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi4_clk", CLK_PLLCLN_DIV8, 24, 5,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi5_clk", CLK_PLLCLN_DIV8, 24, 6,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi6_clk", CLK_PLLCLN_DIV8, 24, 7,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi7_clk", CLK_PLLCLN_DIV8, 24, 8,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi8_clk", CLK_PLLCLN_DIV8, 24, 9,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> BIT(4))),
> > + DEF_MOD("ssi9_clk", CLK_PLLCLN_DIV8, 24, 10,
> -1, -1,
> > + BUS_MSTOP(2, BIT(3) |
> > + BIT(4))),
> > };
> >
>
> Gr{oetje,eeting}s,
>
> Geert
>
> --
> Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-
> m68k.org
>
> In personal conversations with technical people, I call myself a hacker.
> But when I'm talking to journalists I just say "programmer" or something
> like that.
> -- Linus Torvalds
^ permalink raw reply
* Re: [PATCH v3 09/11] dt-bindings: input: Document hid-over-spi DT schema
From: Conor Dooley @ 2026-04-09 16:02 UTC (permalink / raw)
To: Jingyuan Liang
Cc: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
linux-input, linux-doc, linux-kernel, linux-spi,
linux-trace-kernel, devicetree, hbarnor, tfiga, Dmitry Antipov,
Jarrett Schultz
In-Reply-To: <20260402-send-upstream-v3-9-6091c458d357@chromium.org>
[-- Attachment #1: Type: text/plain, Size: 5441 bytes --]
On Thu, Apr 02, 2026 at 01:59:46AM +0000, Jingyuan Liang wrote:
> Documentation describes the required and optional properties for
> implementing Device Tree for a Microsoft G6 Touch Digitizer that
> supports HID over SPI Protocol 1.0 specification.
>
> The properties are common to HID over SPI.
>
> Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
> Signed-off-by: Jarrett Schultz <jaschultz@microsoft.com>
> Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
> ---
> .../devicetree/bindings/input/hid-over-spi.yaml | 126 +++++++++++++++++++++
> 1 file changed, 126 insertions(+)
>
> diff --git a/Documentation/devicetree/bindings/input/hid-over-spi.yaml b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
> new file mode 100644
> index 000000000000..d1b0a2e26c32
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
> @@ -0,0 +1,126 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/input/hid-over-spi.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: HID over SPI Devices
> +
> +maintainers:
> + - Benjamin Tissoires <benjamin.tissoires@redhat.com>
> + - Jiri Kosina <jkosina@suse.cz>
Why them and not you, the developers of the series?
> +
> +description: |+
> + HID over SPI provides support for various Human Interface Devices over the
> + SPI bus. These devices can be for example touchpads, keyboards, touch screens
> + or sensors.
> +
> + The specification has been written by Microsoft and is currently available
> + here: https://www.microsoft.com/en-us/download/details.aspx?id=103325
> +
> + If this binding is used, the kernel module spi-hid will handle the
> + communication with the device and the generic hid core layer will handle the
> + protocol.
This is not relevant to the binding, please remove it.
> +
> +allOf:
> + - $ref: /schemas/input/touchscreen/touchscreen.yaml#
> +
> +properties:
> + compatible:
> + oneOf:
> + - items:
> + - enum:
> + - microsoft,g6-touch-digitizer
> + - const: hid-over-spi
> + - description: Just "hid-over-spi" alone is allowed, but not recommended.
> + const: hid-over-spi
Why is it allowed but not recommended? Seems to me like we should
require device-specific compatibles.
> +
> + reg:
> + maxItems: 1
> +
> + interrupts:
> + maxItems: 1
> +
> + reset-gpios:
> + maxItems: 1
> + description:
> + GPIO specifier for the digitizer's reset pin (active low). The line must
> + be flagged with GPIO_ACTIVE_LOW.
> +
> + vdd-supply:
> + description:
> + Regulator for the VDD supply voltage.
> +
> + input-report-header-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 0xffffff
> + description:
> + A value to be included in the Read Approval packet, listing an address of
> + the input report header to be put on the SPI bus. This address has 24
> + bits.
> +
> + input-report-body-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 0xffffff
> + description:
> + A value to be included in the Read Approval packet, listing an address of
> + the input report body to be put on the SPI bus. This address has 24 bits.
> +
> + output-report-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 0xffffff
> + description:
> + A value to be included in the Output Report sent by the host, listing an
> + address where the output report on the SPI bus is to be written to. This
> + address has 24 bits.
> +
> + read-opcode:
> + $ref: /schemas/types.yaml#/definitions/uint8
> + description:
> + Value to be used in Read Approval packets. 1 byte.
> +
> + write-opcode:
> + $ref: /schemas/types.yaml#/definitions/uint8
> + description:
> + Value to be used in Write Approval packets. 1 byte.
Why can none of these things be determined from the device's compatible?
On the surface, they like the kinds of things that could/should be.
Cheers,
Conor.
> +
> +required:
> + - compatible
> + - interrupts
> + - reset-gpios
> + - vdd-supply
> + - input-report-header-address
> + - input-report-body-address
> + - output-report-address
> + - read-opcode
> + - write-opcode
> +
> +additionalProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/irq.h>
> + #include <dt-bindings/gpio/gpio.h>
> +
> + spi {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + hid@0 {
> + compatible = "microsoft,g6-touch-digitizer", "hid-over-spi";
> + reg = <0x0>;
> + interrupts-extended = <&gpio 42 IRQ_TYPE_EDGE_FALLING>;
> + reset-gpios = <&gpio 27 GPIO_ACTIVE_LOW>;
> + vdd-supply = <&pm8350c_l3>;
> + pinctrl-names = "default";
> + pinctrl-0 = <&ts_d6_int_bias>;
> + input-report-header-address = <0x1000>;
> + input-report-body-address = <0x1004>;
> + output-report-address = <0x2000>;
> + read-opcode = /bits/ 8 <0x0b>;
> + write-opcode = /bits/ 8 <0x02>;
> + };
> + };
>
> --
> 2.53.0.1185.g05d4b7b318-goog
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH v7 1/6] dt-bindings: iio: adc: add AD4691 family
From: Conor Dooley @ 2026-04-09 15:57 UTC (permalink / raw)
To: radu.sabau
Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan, linux-iio, devicetree,
linux-kernel, linux-pwm, linux-gpio, linux-doc
In-Reply-To: <20260409-ad4692-multichannel-sar-adc-driver-v7-1-be375d4df2c5@analog.com>
[-- Attachment #1: Type: text/plain, Size: 3184 bytes --]
On Thu, Apr 09, 2026 at 06:28:22PM +0300, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
>
> Add DT bindings for the Analog Devices AD4691 family of multichannel
> SAR ADCs (AD4691, AD4692, AD4693, AD4694).
>
> The binding describes the hardware connections:
>
> - Power domains: avdd-supply (required), vio-supply, ref-supply or
> refin-supply (external reference; the REFIN path enables the
> internal reference buffer), and an optional ldo-in-supply, that if
> absent, means the on-chip internal LDO will be used.
>
> - Optional PWM on the CNV pin selects CNV Burst Mode; when absent,
> Manual Mode is assumed with CNV tied to SPI CS.
>
> - An optional reset GPIO (reset-gpios) for hardware reset.
>
> - Up to four GP pins (gp0..gp3) usable as interrupt sources,
> identified in firmware via interrupt-names "gp0".."gp3".
>
> - gpio-controller with #gpio-cells = <2> for GP pin GPIO usage.
>
> - #trigger-source-cells = <1>: one cell selecting the GP pin number
> (0-3) used as the SPI offload trigger source.
>
> Two binding examples are provided: CNV Burst Mode with SPI offload
> (DMA data acquisition driven by DATA_READY on a GP pin), and Manual
> Mode for CPU-driven triggered-buffer or single-shot capture.
>
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
> .../devicetree/bindings/iio/adc/adi,ad4691.yaml | 162 +++++++++++++++++++++
> MAINTAINERS | 7 +
> 2 files changed, 169 insertions(+)
>
> diff --git a/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml b/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
> new file mode 100644
> index 000000000000..81d2ca4e0e22
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
> @@ -0,0 +1,162 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/iio/adc/adi,ad4691.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: Analog Devices AD4691 Family Multichannel SAR ADCs
> +
> +maintainers:
> + - Radu Sabau <radu.sabau@analog.com>
> +
> +description: |
> + The AD4691 family are high-speed, low-power, multichannel successive
> + approximation register (SAR) analog-to-digital converters (ADCs) with
> + an SPI-compatible serial interface. The ADC supports CNV Burst Mode,
> + where an external PWM drives the CNV pin, and Manual Mode, where CNV
> + is directly tied to the SPI chip-select.
> +
> + Datasheets:
> + * https://www.analog.com/en/products/ad4691.html
> + * https://www.analog.com/en/products/ad4692.html
> + * https://www.analog.com/en/products/ad4693.html
> + * https://www.analog.com/en/products/ad4694.html
> +
> +$ref: /schemas/spi/spi-peripheral-props.yaml#
> +
> +properties:
> + compatible:
> + enum:
> + - adi,ad4691
> + - adi,ad4692
> + - adi,ad4693
> + - adi,ad4694
Please add a note to the commit message about why these devices are not
compatible. With that,
Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: not-applicable
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH 1/2] dt-bindings: pwm: clk-pwm: add optional GPIO and pinctrl properties
From: Conor Dooley @ 2026-04-09 15:51 UTC (permalink / raw)
To: Xilin Wu
Cc: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Nikita Travkin, linux-pwm, devicetree, linux-kernel,
linux-arm-msm
In-Reply-To: <20260406-clk-pwm-gpio-v1-1-40d2f3a20aff@radxa.com>
[-- Attachment #1: Type: text/plain, Size: 2939 bytes --]
On Mon, Apr 06, 2026 at 11:50:01PM +0800, Xilin Wu wrote:
> The clk-pwm driver cannot produce constant output levels (0% or 100%
> duty cycle, or disabled state) through the clock hardware alone - the
> actual pin level when the clock is off is undefined and
> hardware-dependent.
>
> Document optional gpios, pinctrl-names, pinctrl-0, and pinctrl-1
> properties that allow the driver to switch the pin between clock
> function mux (for normal PWM output) and GPIO mode (to drive a
> deterministic constant level).
>
> Signed-off-by: Xilin Wu <sophon@radxa.com>
This seems reasonable enough to me, but the PWM maintainers should
defintely make sure this fits the ideology of the device.
Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: not-applicable
> ---
> Documentation/devicetree/bindings/pwm/clk-pwm.yaml | 36 +++++++++++++++++++++-
> 1 file changed, 35 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/devicetree/bindings/pwm/clk-pwm.yaml b/Documentation/devicetree/bindings/pwm/clk-pwm.yaml
> index ec1768291503..2a0e3e02d27b 100644
> --- a/Documentation/devicetree/bindings/pwm/clk-pwm.yaml
> +++ b/Documentation/devicetree/bindings/pwm/clk-pwm.yaml
> @@ -15,6 +15,11 @@ description: |
> It's often possible to control duty-cycle of such clocks which makes them
> suitable for generating PWM signal.
>
> + Optionally, a GPIO and pinctrl states can be provided. When a constant
> + output level is needed (0%, 100%, or disabled), the pin is switched to
> + GPIO mode to drive the level directly. For normal PWM output the pin is
> + switched back to its clock function mux.
> +
> allOf:
> - $ref: pwm.yaml#
>
> @@ -29,6 +34,26 @@ properties:
> "#pwm-cells":
> const: 2
>
> + gpios:
> + description:
> + Optional GPIO used to drive a constant level when the PWM output is
> + disabled or set to 0% / 100% duty cycle. When provided, pinctrl states
> + "default" (clock mux) and "gpio" must also be defined.
> + maxItems: 1
> +
> + pinctrl-names: true
> +
> + pinctrl-0:
> + description: Pin configuration for clock function mux (normal PWM).
> + maxItems: 1
> +
> + pinctrl-1:
> + description: Pin configuration for GPIO mode (constant level output).
> + maxItems: 1
> +
> +dependencies:
> + gpios: [ pinctrl-0, pinctrl-1 ]
> +
> unevaluatedProperties: false
>
> required:
> @@ -41,6 +66,15 @@ examples:
> compatible = "clk-pwm";
> #pwm-cells = <2>;
> clocks = <&gcc 0>;
> - pinctrl-names = "default";
> + };
> +
> + - |
> + pwm {
> + compatible = "clk-pwm";
> + #pwm-cells = <2>;
> + clocks = <&gcc 0>;
> + pinctrl-names = "default", "gpio";
> pinctrl-0 = <&pwm_clk_flash_default>;
> + pinctrl-1 = <&pwm_clk_flash_gpio>;
> + gpios = <&tlmm 32 0>;
> };
>
> --
> 2.53.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH 1/2] dt-bindings: pwm: marvell,pxa-pwm: Add SpacemiT K3 PWM support
From: Conor Dooley @ 2026-04-09 15:41 UTC (permalink / raw)
To: Yixun Lan
Cc: Uwe Kleine-König, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Duje Mihanović, linux-pwm, devicetree,
linux-kernel, linux-riscv, spacemit
In-Reply-To: <20260409-03-k3-pwm-drv-v1-1-1307a06fba38@kernel.org>
[-- Attachment #1: Type: text/plain, Size: 2845 bytes --]
On Thu, Apr 09, 2026 at 12:45:11AM +0000, Yixun Lan wrote:
> The PWM controller in SpacemiT K3 SoC reuse the same IP as previous K1
> generation, while the difference is that one additional bus clock is
> added.
>
> Signed-off-by: Yixun Lan <dlan@kernel.org>
> ---
> .../devicetree/bindings/pwm/marvell,pxa-pwm.yaml | 53 ++++++++++++++++++++--
> 1 file changed, 50 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/devicetree/bindings/pwm/marvell,pxa-pwm.yaml b/Documentation/devicetree/bindings/pwm/marvell,pxa-pwm.yaml
> index 8df327e52810..3427c8ef3945 100644
> --- a/Documentation/devicetree/bindings/pwm/marvell,pxa-pwm.yaml
> +++ b/Documentation/devicetree/bindings/pwm/marvell,pxa-pwm.yaml
> @@ -15,7 +15,9 @@ allOf:
> properties:
> compatible:
> contains:
> - const: spacemit,k1-pwm
> + enum:
> + - spacemit,k1-pwm
> + - spacemit,k3-pwm
> then:
> properties:
> "#pwm-cells":
> @@ -26,6 +28,38 @@ allOf:
> const: 1
> description: |
> Used for specifying the period length in nanoseconds.
> + - if:
> + properties:
> + compatible:
> + contains:
> + enum:
> + - spacemit,k3-pwm
> + then:
> + properties:
> + clock-names:
> + items:
> + - const: func
> + - const: bus
This condition here doesn't appear to do anything? It just repeats
what's already done unconditonally below?
> + - if:
> + properties:
> + compatible:
> + contains:
> + enum:
> + - spacemit,k3-pwm
> + then:
> + required:
> + - clock-names
> + properties:
> + clocks:
> + minItems: 2
> + clock-names:
> + minItems: 2
> + else:
> + properties:
> + clocks:
> + maxItems: 1
> + clock-names:
> + maxItems: 1
>
> properties:
> compatible:
> @@ -36,7 +70,9 @@ properties:
> - marvell,pxa168-pwm
> - marvell,pxa910-pwm
> - items:
> - - const: spacemit,k1-pwm
> + - enum:
> + - spacemit,k1-pwm
> + - spacemit,k3-pwm
> - const: marvell,pxa910-pwm
>
> reg:
> @@ -47,7 +83,18 @@ properties:
> description: Number of cells in a pwm specifier.
>
> clocks:
> - maxItems: 1
> + minItems: 1
> + items:
> + - description: The function clock
> + - description: An optional bus clock
> +
> + clock-names:
> + minItems: 1
> + maxItems: 2
> + oneOf:
> + - items:
> + - const: func
> + - const: bus
>
> resets:
> maxItems: 1
>
> --
> 2.53.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH v4 2/4] ASoC: codecs: Add TAS67524 quad-channel audio amplifier driver
From: Mark Brown @ 2026-04-09 15:41 UTC (permalink / raw)
To: Wang, Sen
Cc: linux-sound, lgirdwood, robh, krzk+dt, conor+dt, devicetree,
perex, tiwai, shenghao-ding, kevin-lu, baojun.xu, niranjan.hy,
l-badrinarayanan, devarsht, v-singh1, linux-kernel
In-Reply-To: <6d0fa8ee-b189-4ccb-a120-d7d42300168a@ti.com>
[-- Attachment #1: Type: text/plain, Size: 401 bytes --]
On Thu, Apr 09, 2026 at 10:26:55AM -0500, Wang, Sen wrote:
> On 4/8/2026 10:41 AM, Mark Brown wrote:
> > On Wed, Apr 08, 2026 at 12:31:46AM -0500, Sen Wang wrote:
> Would you suggest a regmap/cache mechanism better suited for this two-level
> hierarchy? Happy to rework it if there's a cleaner approach.
I hadn't noticed that you'd got both books and pages - what you've got
already should be fine.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply
* Re: [PATCH v1 1/2] dt-bindings: usb: dwc3: add support for StarFive JHB100
From: Conor Dooley @ 2026-04-09 15:37 UTC (permalink / raw)
To: Minda Chen
Cc: Greg Kroah-Hartman, Thinh Nguyen, Rob Herring,
Krzysztof Kozlowski, linux-usb, linux-kernel, devicetree
In-Reply-To: <20260409101227.39417-2-minda.chen@starfivetech.com>
[-- Attachment #1: Type: text/plain, Size: 75 bytes --]
Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: not-applicable
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH v2 1/3] dt-bindings: arm: aspeed: add Anacapa EVT1 EVT2 board
From: Conor Dooley @ 2026-04-09 15:36 UTC (permalink / raw)
To: Colin Huang
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Joel Stanley,
Andrew Jeffery, devicetree, linux-arm-kernel, linux-aspeed,
linux-kernel, colin.huang2
In-Reply-To: <20260409-anacapa-devlop-phase-devicetree-v2-1-68f328671653@gmail.com>
[-- Attachment #1: Type: text/plain, Size: 247 bytes --]
On Thu, Apr 09, 2026 at 07:40:26PM +0800, Colin Huang wrote:
> Document Anacapa BMC EVT1 and EVT2 compatibles.
>
> Signed-off-by: Colin Huang <u8813345@gmail.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: not-applicable
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH v2 1/3] dt-bindings: net: dsa: nxp,sja1105: make spi-cpol optional for sja1110
From: Conor Dooley @ 2026-04-09 15:36 UTC (permalink / raw)
To: Josua Mayer
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Shawn Guo,
Frank Li, Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam,
Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Yazan Shhady, Mikhail Anikin,
Alexander Dahl, devicetree, linux-kernel, imx, linux-arm-kernel,
Vladimir Oltean, Conor Dooley, Krzysztof Kozlowski, netdev
In-Reply-To: <20260409-imx8dxl-sr-som-v2-1-83ff20629ba0@solid-run.com>
[-- Attachment #1: Type: text/plain, Size: 668 bytes --]
On Thu, Apr 09, 2026 at 02:34:33PM +0200, Josua Mayer wrote:
> Currently, the binding requires 'spi-cpha' for SJA1105 and 'spi-cpol'
> for SJA1110.
>
> However, the SJA1110 supports both SPI modes 0 and 2. Mode 2
> (cpha=0, cpol=1) is used by the NXP LX2160 Bluebox 3.
>
> On the SolidRun i.MX8DXL HummingBoard Telematics, mode 0 is stable,
> while forcing mode 2 introduces CRC errors especially during bursts.
>
> Drop the requirement on spi-cpol for SJA1110.
>
> Fixes: af2eab1a8243 ("dt-bindings: net: nxp,sja1105: document spi-cpol/cpha")
> Signed-off-by: Josua Mayer <josua@solid-run.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH v1 1/2] dt-bindings: i2c: ls2x-i2c: Add clock- related properties
From: Yao Zi @ 2026-04-09 15:29 UTC (permalink / raw)
To: Hongliang Wang, Krzysztof Kozlowski
Cc: Binbin Zhou, Andi Shyti, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, linux-i2c, devicetree, loongarch
In-Reply-To: <e10017ce-cda0-5b5e-8cd8-3488aef4cdb0@loongson.cn>
On Thu, Apr 09, 2026 at 08:03:47PM +0800, Hongliang Wang wrote:
> Hi, Krzysztof
>
> On 2026/3/31 下午3:11, Hongliang Wang wrote:
> >
> > On 2026/3/30 下午3:23, Krzysztof Kozlowski wrote:
> > > On 30/03/2026 09:18, Hongliang Wang wrote:
> > > > On 2026/3/27 下午2:39, Krzysztof Kozlowski wrote:
> > > > > On 27/03/2026 04:09, Hongliang Wang wrote:
> > > > > > The initial idea was that this patch could be used for
> > > > > > both ACPI and DTS.
> > > > > > > > > The i2c-ls2x driver is compatible with both Loongson 2K and 3A+7A
> > > > > > > > > platform, parse
> > > > > > > > > the same parameters regardless of dts or
> > > > > > > > > acpi parameter passing, So
> > > > > > > > > clock-input
> > > > > > > > > and clock-div attributes are defined to
> > > > > > > > > describe input clock of i2c
> > > > > > > > > controller and
> > > > > > > > > divisor of input clock. It can be used on
> > > > > > > > > both 2K and 3A+7A platform.
> > > > > > > > And you cannot use them in DTS.
> > > > > > OK
> > > > > > > I need to keep guessing what you want to achieve,
> > > > > > > because neither your
> > > > > > > message nor commit text was explicit
> > > > > > What I want to achieve is to describe the input clock
> > > > > > and divisor of I2C
> > > > > > controller
> > > > > Input clocks are defined as clock inputs obviously in DT, not as
> > > > > integers. Bindings need to describe the hardware, so start with that.
> > > > I can describe the hardware in loongson,ls2x-i2c.yaml, and I
> > > > would like to
> > > > confirm with you what final implementation plan you agree to? clock
> > > > framework
> > > > or custom clock-input an clock-div attributes? if clock framework, how
> > > > can it
> > > > also be used for ACPI?
> > > And you ask DT maintainer for that? It's not relevant. You sent DT
> > > bindings patch, so this patch must be correct and we discuss this patch
> > > here.
> > I don't. My idea is that if the clock input attribute can't be used for
> > both
> > dts and acpi, then clock framework will be used for dts and new define
> > attribute
> > will be used for acpi. I will first implement the hardware description
> > and clock
> > framework in Bindings.
> > > Best regards,
> > > Krzysztof
> >
> > Best regards,
> > Hongliang Wang
> >
>
> I have a question, the input clock of i2c controller can be described by
> "clocks",
> but there is no existing attribute can describe the divisor of the input
> clock,
From the description of 7A1000's user manual (section 2.3
"时钟功能描述"), it seems the divider isn't part of the I2C controller,
but instead is an on-chip divider with fixed 1/2 factor, feeding both
"MISC" block (including I2C) and SPI.
> Can I define a new attribute named "clock-div" to describe it in DT
> bindings?
> or do you have any standard solutions for the divisor problem? Thank you.
If these devicetree-based Loongson platforms follow a similar pattern as
the bridge chip, then the divisor shouldn't be described in the I2C
controller node. You may want to include a "fixed-factor-clock" node to
match the hardware.
> Best regards,
> Hongliang Wang
>
>
>
Regards,
Yao Zi
^ permalink raw reply
* [PATCH v7 6/6] docs: iio: adc: ad4691: add driver documentation
From: Radu Sabau via B4 Relay @ 2026-04-09 15:28 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add RST documentation for the AD4691 family ADC driver covering
supported devices, IIO channels, operating modes, oversampling,
reference voltage, LDO supply, reset, GP pins, SPI offload support,
and buffer data format.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
Documentation/iio/ad4691.rst | 283 +++++++++++++++++++++++++++++++++++++++++++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 285 insertions(+)
diff --git a/Documentation/iio/ad4691.rst b/Documentation/iio/ad4691.rst
new file mode 100644
index 000000000000..a1012c8b78a3
--- /dev/null
+++ b/Documentation/iio/ad4691.rst
@@ -0,0 +1,283 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+=============
+AD4691 driver
+=============
+
+ADC driver for Analog Devices Inc. AD4691 family of multichannel SAR ADCs.
+The module name is ``ad4691``.
+
+
+Supported devices
+=================
+
+The following chips are supported by this driver:
+
+* `AD4691 <https://www.analog.com/en/products/ad4691.html>`_ — 16-channel, 500 kSPS
+* `AD4692 <https://www.analog.com/en/products/ad4692.html>`_ — 16-channel, 1 MSPS
+* `AD4693 <https://www.analog.com/en/products/ad4693.html>`_ — 8-channel, 500 kSPS
+* `AD4694 <https://www.analog.com/en/products/ad4694.html>`_ — 8-channel, 1 MSPS
+
+
+IIO channels
+============
+
+Each physical ADC input maps to one IIO voltage channel. The AD4691 and AD4692
+expose 16 channels (``voltage0`` through ``voltage15``); the AD4693 and AD4694
+expose 8 channels (``voltage0`` through ``voltage7``).
+
+All channels share a common scale (``in_voltage_scale``), derived from the
+reference voltage. Each channel independently exposes:
+
+* ``in_voltageN_raw`` — single-shot ADC result
+* ``in_voltageN_sampling_frequency`` — per-channel effective conversion rate.
+ In CNV Burst Mode this equals the internal oscillator frequency divided by
+ the channel's oversampling ratio. In Manual Mode (where oversampling is not
+ supported) it equals the oscillator frequency directly.
+* ``in_voltageN_sampling_frequency_available`` — list of valid oscillator
+ frequencies
+
+The following attributes are only available in CNV Burst Mode:
+
+* ``in_voltageN_oversampling_ratio`` — per-channel hardware accumulation depth
+* ``in_voltageN_oversampling_ratio_available`` — list of valid ratios: 1, 2, 4,
+ 8, 16, 32
+
+
+Operating modes
+===============
+
+The driver supports two operating modes, auto-detected from the device tree at
+probe time. Both modes transition to and from an internal Autonomous Mode idle
+state when the IIO buffer is enabled and disabled.
+
+Manual Mode
+-----------
+
+Selected when no ``pwms`` property is present in the device tree. The CNV pin
+is tied to the SPI chip-select: every CS assertion both triggers a new
+conversion and returns the result of the previous one (pipelined N+1 scheme).
+
+To read N channels the driver issues N+1 SPI transfers in a single optimised
+message:
+
+* Transfers 0 to N-1 each carry ``AD4691_ADC_CHAN(n)`` in the TX byte to
+ select the next channel; the RX byte of transfer ``k+1`` contains the result
+ of the channel selected in transfer ``k``.
+* Transfer N is a NOOP (0x00) to flush the last conversion result out of the
+ pipeline.
+
+A user-defined IIO trigger (e.g. hrtimer trigger) drives the trigger handler,
+which executes the pre-built SPI message and pushes the scan to the buffer.
+
+Oversampling is not supported in Manual Mode.
+
+CNV Burst Mode
+--------------
+
+Selected when a ``pwms`` property is present in the device tree. The PWM drives
+the CNV pin independently of SPI at the configured conversion rate, and a GP
+pin (identified by ``interrupt-names``) asserts DATA_READY at end-of-burst to
+signal that the AVG_IN result registers are ready to be read.
+
+The IRQ handler fires the IIO trigger without stopping the PWM, then disables
+itself to prevent a second DATA_READY assertion while the trigger handler is
+running. The trigger handler reads all active ``AVG_IN(n)`` registers in a
+single optimised SPI message, pushes the scan to the buffer, and re-enables
+the IRQ.
+
+The buffer sampling frequency (i.e. the PWM rate) is controlled by the
+``sampling_frequency`` attribute on the IIO buffer. Valid values span from the
+chip's minimum oscillator rate up to its maximum conversion rate (500 kSPS for
+AD4691/AD4693, 1 MSPS for AD4692/AD4694). In practice, without SPI offload,
+the SPI read overhead between DATA_READY and the start of the next PWM period
+limits the achievable rate; the PWM frequency should be set low enough to
+accommodate the SPI transfer time.
+
+Autonomous Mode (idle / single-shot)
+-------------------------------------
+
+The chip idles in Autonomous Mode whenever the IIO buffer is disabled. In this
+state, ``read_raw`` requests (``in_voltageN_raw``) use the internal oscillator
+to perform a single conversion on the requested channel and read back the
+result from the ``AVG_IN(N)`` register. The oscillator is started and stopped
+for each read to save power.
+
+
+Oversampling
+============
+
+In CNV Burst Mode each channel has an independent hardware accumulator
+(ACC_DEPTH_IN) that averages a configurable number of successive conversions
+before DATA_READY asserts. The result is always returned as a 16-bit mean from
+the ``AVG_IN`` register, so the IIO ``realbits`` and ``storagebits`` are
+unaffected by the oversampling ratio.
+
+Valid ratios are 1, 2, 4, 8, 16 and 32. The default is 1 (no averaging).
+Oversampling is not supported in Manual Mode.
+
+.. code-block:: bash
+
+ # Set oversampling ratio to 16 on channel 0
+ echo 16 > /sys/bus/iio/devices/iio:device0/in_voltage0_oversampling_ratio
+
+When OSR > 1 the effective conversion rate reported by
+``in_voltageN_sampling_frequency`` and used for ``read_raw`` is reduced
+accordingly, since each output sample requires OSR successive conversions.
+
+
+Reference voltage
+=================
+
+The driver supports two reference configurations, mutually exclusive:
+
+* **External reference** (``ref-supply``): a voltage between 2.4 V and 5.25 V
+ supplied externally. The internal reference buffer is disabled.
+* **Buffered internal reference** (``refin-supply``): An internal reference
+ buffer is used. The driver enables ``REFBUF_EN`` in the REF_CTRL register
+ when this supply is used.
+
+Exactly one of ``ref-supply`` or ``refin-supply`` must be present in the
+device tree.
+
+The reference voltage determines the full-scale range:
+
+.. code-block::
+
+ full-scale = Vref / 2^16 (per LSB)
+
+
+LDO supply
+==========
+
+The chip contains an internal LDO that powers part of the analog front-end.
+The LDO input can be driven externally via the ``ldo-in-supply`` regulator. If
+that supply is absent, the driver enables the internal LDO path (``LDO_EN``
+bit in DEVICE_SETUP).
+
+
+Reset
+=====
+
+The driver supports two reset mechanisms:
+
+* **Hardware reset** (``reset-gpios`` in device tree): the GPIO is already
+ asserted at driver probe by the reset controller framework. The driver waits
+ for the required 300 µs reset pulse width and then deasserts.
+* **Software reset** (fallback when ``reset-gpios`` is absent): the driver
+ writes the software-reset pattern to the SPI_CONFIG_A register.
+
+
+GP pins and interrupts
+======================
+
+The chip exposes up to four general-purpose (GP) pins that can be configured as
+interrupt or trigger-source outputs. In CNV Burst Mode (non-offload), one GP
+pin must be wired to an interrupt-capable SoC input and declared in the device
+tree using the ``interrupts`` and ``interrupt-names`` properties.
+
+The ``interrupt-names`` value identifies which GP pin is used (``"gp0"``
+through ``"gp3"``). The driver configures that pin as a DATA_READY output in
+the GPIO_MODE register.
+
+Example device tree fragment::
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ ...
+ interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-parent = <&gpio0>;
+ interrupt-names = "gp0";
+ };
+
+
+SPI offload support
+===================
+
+When a SPI offload engine (e.g. the AXI SPI Engine) is present, the driver
+uses DMA-backed transfers for CPU-independent, high-throughput data capture.
+SPI offload is detected automatically at probe via ``devm_spi_offload_get()``;
+if no offload hardware is available the driver falls back to the software
+triggered-buffer path.
+
+Two SPI offload sub-modes exist, corresponding to the two operating modes:
+
+CNV Burst offload
+-----------------
+
+Used when a ``pwms`` property is present and SPI offload is available.
+
+The PWM drives CNV at the configured rate. On DATA_READY the SPI offload
+engine automatically executes a pre-built message that reads all active
+``AVG_IN`` registers and streams the data directly to an IIO DMA buffer with
+no CPU involvement. A final state-reset transfer re-arms DATA_READY for the
+next burst.
+
+The GP pin used as DATA_READY trigger is supplied by the trigger-source
+consumer (via ``#trigger-source-cells``) at buffer enable time; no
+``interrupt-names`` entry is required in this path.
+
+The buffer sampling frequency is controlled by the ``sampling_frequency``
+attribute on the IIO buffer (same as the non-offload CNV Burst path).
+
+Manual offload
+--------------
+
+Used when no ``pwms`` property is present and SPI offload is available.
+
+A periodic SPI offload trigger controls the conversion rate. On each trigger
+period, the SPI engine executes an N+1 transfer message (same pipelined scheme
+as software Manual Mode) and streams the 16-bit ADC results directly to the
+IIO DMA buffer. Each transfer sends a 16-bit TX word (channel command byte in
+the upper byte, zero in the lower byte); the chip returns the 16-bit ADC
+result on MISO. The first transfer's RX is discarded (pipeline flush); results
+from transfers 1 through N are streamed to the DMA buffer.
+
+The ``sampling_frequency`` attribute on the IIO buffer controls the trigger
+rate (in Hz). The initial rate is 100 kHz — a conservative default chosen
+because the N+1 SPI transfer overhead in this mode limits the achievable rate
+relative to the chip's maximum conversion rate.
+
+Oversampling is not supported in Manual Mode.
+
+
+Buffer data format
+==================
+
+The IIO buffer data format (``in_voltageN_type``) is the same across all
+paths: 16-bit unsigned big-endian samples with no shift.
+
++-------------------------+-------------+----------+-------+
+| Path | storagebits | realbits | shift |
++=========================+=============+==========+=======+
+| Triggered buffer | 16 | 16 | 0 |
++-------------------------+-------------+----------+-------+
+| CNV Burst offload (DMA) | 16 | 16 | 0 |
++-------------------------+-------------+----------+-------+
+| Manual offload (DMA) | 16 | 16 | 0 |
++-------------------------+-------------+----------+-------+
+
+In the triggered-buffer path the SPI rx_buf for each transfer points directly
+into the scan buffer, so the 16-bit big-endian result is written in place with
+no additional copying.
+
+In the DMA offload paths the SPI offload engine streams 16-bit words (one per
+active channel) directly into the IIO DMA buffer:
+
+* **CNV Burst offload**: each channel read uses a 16-bit address phase followed
+ by a 16-bit data phase; only the data phase is flagged for DMA streaming, so
+ the 16-bit result lands directly in the buffer.
+* **Manual offload**: each 16-bit SPI transfer carries the channel command on
+ TX and receives the 16-bit ADC result on RX; results are streamed to the
+ DMA buffer one 16-bit word per channel.
+
+The ``in_voltageN_type`` sysfs attribute reflects the active scan type.
+
+
+Unimplemented features
+======================
+
+* GPIO controller functionality of the GP pins
+* Clamp status and overrange events
+* Raw accumulator (ACC_IN) and accumulator status registers
+* ADC_BUSY and overrun status interrupts
diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
index ba3e609c6a13..007e0a1fcc5a 100644
--- a/Documentation/iio/index.rst
+++ b/Documentation/iio/index.rst
@@ -23,6 +23,7 @@ Industrial I/O Kernel Drivers
ad4000
ad4030
ad4062
+ ad4691
ad4695
ad7191
ad7380
diff --git a/MAINTAINERS b/MAINTAINERS
index 24e4502b8292..819d8b6eb6bb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1491,6 +1491,7 @@ S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
F: drivers/iio/adc/ad4691.c
+F: drivers/iio/adc/ad4691.rst
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
--
2.43.0
^ permalink raw reply related
* [PATCH v7 4/6] iio: adc: ad4691: add SPI offload support
From: Radu Sabau via B4 Relay @ 2026-04-09 15:28 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add SPI offload support to enable DMA-based, CPU-independent data
acquisition using the SPI Engine offload framework.
When an SPI offload is available (devm_spi_offload_get() succeeds),
the driver registers a DMA engine IIO buffer and uses dedicated buffer
setup operations. If no offload is available the existing software
triggered buffer path is used unchanged.
Both CNV Burst Mode and Manual Mode support offload, but use different
trigger mechanisms:
CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
signal on the GP pin specified by the trigger-source consumer reference
in the device tree (one cell = GP pin number 0-3). For this mode the
driver acts as both an SPI offload consumer (DMA RX stream, message
optimization) and a trigger source provider: it registers the
GP/DATA_READY output via devm_spi_offload_trigger_register() so the
offload framework can match the '#trigger-source-cells' phandle and
automatically fire the SPI Engine DMA transfer at end-of-conversion.
Manual Mode: the SPI Engine is triggered by a periodic trigger at
the configured sampling frequency. The pre-built SPI message uses
the pipelined CNV-on-CS protocol: N+1 16-bit transfers are issued
for N active channels (the first result is discarded as garbage from
the pipeline flush) and the remaining N results are captured by DMA.
All offload transfers use 16-bit frames (bits_per_word=16, len=2).
The channel scan_type (storagebits=16, shift=0, IIO_BE) is shared
between the software triggered-buffer and offload paths; no separate
scan_type or channel array is needed for the offload case. The
ad4691_manual_channels[] array introduced in the triggered-buffer
commit is reused here: it hides the IIO_CHAN_INFO_OVERSAMPLING_RATIO
attribute, which is not applicable in Manual Mode.
Kconfig gains a dependency on IIO_BUFFER_DMAENGINE.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 398 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 395 insertions(+), 5 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index d498f16c0816..fdc6565933c5 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -143,8 +143,10 @@ config AD4691
tristate "Analog Devices AD4691 Family ADC Driver"
depends on SPI
select IIO_BUFFER
+ select IIO_BUFFER_DMAENGINE
select IIO_TRIGGERED_BUFFER
select REGMAP
+ select SPI_OFFLOAD
help
Say yes here to build support for Analog Devices AD4691 Family MuxSAR
SPI analog to digital converters (ADC).
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index 3e5caa0972eb..839ea7f44c78 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -22,6 +22,8 @@
#include <linux/regulator/consumer.h>
#include <linux/reset.h>
#include <linux/spi/spi.h>
+#include <linux/spi/offload/consumer.h>
+#include <linux/spi/offload/provider.h>
#include <linux/units.h>
#include <linux/unaligned.h>
@@ -43,6 +45,11 @@
#define AD4691_CNV_DUTY_CYCLE_NS 380
#define AD4691_CNV_HIGH_TIME_NS 430
+/*
+ * Conservative default for the manual offload periodic trigger. Low enough
+ * to work safely out of the box across all OSR and channel count combinations.
+ */
+#define AD4691_OFFLOAD_INITIAL_TRIGGER_HZ (100 * HZ_PER_KHZ)
#define AD4691_SPI_CONFIG_A_REG 0x000
#define AD4691_SW_RESET (BIT(7) | BIT(0))
@@ -95,6 +102,8 @@
#define AD4691_ACC_IN(n) (0x252 + (3 * (n)))
#define AD4691_ACC_STS_DATA(n) (0x283 + (4 * (n)))
+#define AD4691_OFFLOAD_BITS_PER_WORD 16
+
static const char * const ad4691_supplies[] = { "avdd", "vio" };
enum ad4691_ref_ctrl {
@@ -114,6 +123,7 @@ struct ad4691_chip_info {
const char *name;
unsigned int max_rate;
const struct ad4691_channel_info *sw_info;
+ const struct ad4691_channel_info *offload_info;
};
#define AD4691_CHANNEL(ch) \
@@ -177,6 +187,18 @@ static const struct ad4691_channel_info ad4693_sw_info = {
.num_channels = ARRAY_SIZE(ad4693_channels),
};
+static const struct ad4691_channel_info ad4691_offload_info = {
+ .channels = ad4691_channels,
+ /* No soft timestamp; num_channels caps access to 16. */
+ .num_channels = 16,
+};
+
+static const struct ad4691_channel_info ad4693_offload_info = {
+ .channels = ad4693_channels,
+ /* No soft timestamp; num_channels caps access to 8. */
+ .num_channels = 8,
+};
+
/*
* Internal oscillator frequency table. Index is the OSC_FREQ_REG[3:0] value.
* Index 0 (1 MHz) is only valid for AD4692/AD4694; AD4691/AD4693 support
@@ -207,24 +229,36 @@ static const struct ad4691_chip_info ad4691_chip_info = {
.name = "ad4691",
.max_rate = 500 * HZ_PER_KHZ,
.sw_info = &ad4691_sw_info,
+ .offload_info = &ad4691_offload_info,
};
static const struct ad4691_chip_info ad4692_chip_info = {
.name = "ad4692",
.max_rate = 1 * HZ_PER_MHZ,
.sw_info = &ad4691_sw_info,
+ .offload_info = &ad4691_offload_info,
};
static const struct ad4691_chip_info ad4693_chip_info = {
.name = "ad4693",
.max_rate = 500 * HZ_PER_KHZ,
.sw_info = &ad4693_sw_info,
+ .offload_info = &ad4693_offload_info,
};
static const struct ad4691_chip_info ad4694_chip_info = {
.name = "ad4694",
.max_rate = 1 * HZ_PER_MHZ,
.sw_info = &ad4693_sw_info,
+ .offload_info = &ad4693_offload_info,
+};
+
+struct ad4691_offload_state {
+ struct spi_offload *spi;
+ struct spi_offload_trigger *trigger;
+ u64 trigger_hz;
+ u8 tx_cmd[17][2];
+ u8 tx_reset[4];
};
struct ad4691_state {
@@ -253,7 +287,10 @@ struct ad4691_state {
* transfers in one go.
*/
struct spi_message scan_msg;
- /* max 16 + 1 NOOP (manual) or 2*16 + 2 (CNV burst). */
+ /*
+ * Non-offload: max 16 + 1 NOOP (manual) or 2*16 + 2 (CNV burst).
+ * Offload reuses this array — both paths are mutually exclusive.
+ */
struct spi_transfer scan_xfers[34];
/*
* CNV burst: 16 AVG_IN addresses + state-reset address + state-reset
@@ -265,6 +302,8 @@ struct ad4691_state {
__be16 vals[16];
aligned_s64 ts;
} scan;
+ /* NULL when no SPI offload hardware is present */
+ struct ad4691_offload_state *offload;
};
/*
@@ -284,6 +323,46 @@ static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
AD4691_GP_MODE_DATA_READY << shift);
}
+static const struct spi_offload_config ad4691_offload_config = {
+ .capability_flags = SPI_OFFLOAD_CAP_TRIGGER |
+ SPI_OFFLOAD_CAP_RX_STREAM_DMA,
+};
+
+static bool ad4691_offload_trigger_match(struct spi_offload_trigger *trigger,
+ enum spi_offload_trigger_type type,
+ u64 *args, u32 nargs)
+{
+ return type == SPI_OFFLOAD_TRIGGER_DATA_READY &&
+ nargs == 1 && args[0] <= 3;
+}
+
+static int ad4691_offload_trigger_request(struct spi_offload_trigger *trigger,
+ enum spi_offload_trigger_type type,
+ u64 *args, u32 nargs)
+{
+ struct ad4691_state *st = spi_offload_trigger_get_priv(trigger);
+
+ if (nargs != 1)
+ return -EINVAL;
+
+ return ad4691_gpio_setup(st, args[0]);
+}
+
+static int ad4691_offload_trigger_validate(struct spi_offload_trigger *trigger,
+ struct spi_offload_trigger_config *config)
+{
+ if (config->type != SPI_OFFLOAD_TRIGGER_DATA_READY)
+ return -EINVAL;
+
+ return 0;
+}
+
+static const struct spi_offload_trigger_ops ad4691_offload_trigger_ops = {
+ .match = ad4691_offload_trigger_match,
+ .request = ad4691_offload_trigger_request,
+ .validate = ad4691_offload_trigger_validate,
+};
+
static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct spi_device *spi = context;
@@ -807,6 +886,214 @@ static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
.postdisable = &ad4691_cnv_burst_buffer_postdisable,
};
+static int ad4691_manual_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ struct device *dev = regmap_get_device(st->regmap);
+ struct spi_device *spi = to_spi_device(dev);
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_PERIODIC,
+ };
+ unsigned int bit, k;
+ int ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+
+ /*
+ * N+1 transfers for N channels. Each CS-low period triggers
+ * a conversion AND returns the previous result (pipelined).
+ * TX: [AD4691_ADC_CHAN(n), 0x00]
+ * RX: [data_hi, data_lo] (storagebits=16, shift=0)
+ * Transfer 0 RX is garbage; transfers 1..N carry real data.
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ offload->tx_cmd[k][0] = AD4691_ADC_CHAN(bit);
+ st->scan_xfers[k].tx_buf = offload->tx_cmd[k];
+ st->scan_xfers[k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
+ st->scan_xfers[k].cs_change = 1;
+ st->scan_xfers[k].cs_change_delay.value = AD4691_CNV_HIGH_TIME_NS;
+ st->scan_xfers[k].cs_change_delay.unit = SPI_DELAY_UNIT_NSECS;
+ /* First transfer RX is garbage — skip it. */
+ if (k > 0)
+ st->scan_xfers[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ k++;
+ }
+
+ /* Final NOOP to flush pipeline and capture last channel. */
+ offload->tx_cmd[k][0] = AD4691_NOOP;
+ st->scan_xfers[k].tx_buf = offload->tx_cmd[k];
+ st->scan_xfers[k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
+ st->scan_xfers[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ k++;
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, k);
+ st->scan_msg.offload = offload->spi;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ config.periodic.frequency_hz = offload->trigger_hz;
+ ret = spi_offload_trigger_enable(offload->spi, offload->trigger, &config);
+ if (ret)
+ goto err_unoptimize;
+
+ return 0;
+
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+err_exit_conversion:
+ ad4691_exit_conversion_mode(st);
+ return ret;
+}
+
+static int ad4691_manual_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+
+ spi_offload_trigger_disable(offload->spi, offload->trigger);
+ spi_unoptimize_message(&st->scan_msg);
+
+ return ad4691_exit_conversion_mode(st);
+}
+
+static const struct iio_buffer_setup_ops ad4691_manual_offload_buffer_setup_ops = {
+ .postenable = &ad4691_manual_offload_buffer_postenable,
+ .predisable = &ad4691_manual_offload_buffer_predisable,
+};
+
+static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ struct device *dev = regmap_get_device(st->regmap);
+ struct spi_device *spi = to_spi_device(dev);
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_DATA_READY,
+ };
+ unsigned int n_active;
+ unsigned int bit, k;
+ int ret;
+
+ n_active = bitmap_weight(indio_dev->active_scan_mask, iio_get_masklength(indio_dev));
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
+ ~bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0));
+ if (ret)
+ return ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+
+ /*
+ * Each AVG_IN register read uses two 16-bit transfers:
+ * TX: [reg_hi | 0x80, reg_lo] (address, CS stays asserted)
+ * RX: [data_hi, data_lo] (data, storagebits=16, shift=0)
+ * The state reset is also split into two 16-bit transfers
+ * (address then value) to keep bits_per_word uniform throughout.
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ put_unaligned_be16(0x8000 | AD4691_AVG_IN(bit), offload->tx_cmd[k]);
+
+ /* TX: address phase, CS stays asserted into data phase */
+ st->scan_xfers[2 * k].tx_buf = offload->tx_cmd[k];
+ st->scan_xfers[2 * k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
+
+ /* RX: data phase, CS toggles after to delimit the next register op */
+ st->scan_xfers[2 * k + 1].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k + 1].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
+ st->scan_xfers[2 * k + 1].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ k++;
+ }
+
+ /* State reset to re-arm DATA_READY for the next scan. */
+ put_unaligned_be16(AD4691_STATE_RESET_REG, offload->tx_reset);
+ offload->tx_reset[2] = AD4691_STATE_RESET_ALL;
+
+ st->scan_xfers[2 * k].tx_buf = offload->tx_reset;
+ st->scan_xfers[2 * k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
+
+ st->scan_xfers[2 * k + 1].tx_buf = &offload->tx_reset[2];
+ st->scan_xfers[2 * k + 1].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k + 1].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, 2 * k + 2);
+ st->scan_msg.offload = offload->spi;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = spi_offload_trigger_enable(offload->spi, offload->trigger, &config);
+ if (ret)
+ goto err_sampling_disable;
+
+ return 0;
+
+err_sampling_disable:
+ ad4691_sampling_enable(st, false);
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+err_exit_conversion:
+ ad4691_exit_conversion_mode(st);
+ return ret;
+}
+
+static int ad4691_cnv_burst_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ int ret;
+
+ spi_offload_trigger_disable(offload->spi, offload->trigger);
+
+ ret = ad4691_sampling_enable(st, false);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ AD4691_SEQ_ALL_CHANNELS_OFF);
+ if (ret)
+ return ret;
+
+ spi_unoptimize_message(&st->scan_msg);
+
+ return ad4691_exit_conversion_mode(st);
+}
+
+static const struct iio_buffer_setup_ops ad4691_cnv_burst_offload_buffer_setup_ops = {
+ .postenable = &ad4691_cnv_burst_offload_buffer_postenable,
+ .predisable = &ad4691_cnv_burst_offload_buffer_predisable,
+};
+
static ssize_t sampling_frequency_show(struct device *dev,
struct device_attribute *attr,
char *buf)
@@ -814,7 +1101,10 @@ static ssize_t sampling_frequency_show(struct device *dev,
struct iio_dev *indio_dev = dev_to_iio_dev(dev);
struct ad4691_state *st = iio_priv(indio_dev);
- return sysfs_emit(buf, "%u\n", NSEC_PER_SEC / st->cnv_period_ns);
+ if (st->manual_mode && st->offload)
+ return sysfs_emit(buf, "%llu\n", st->offload->trigger_hz);
+
+ return sysfs_emit(buf, "%lu\n", NSEC_PER_SEC / st->cnv_period_ns);
}
static ssize_t sampling_frequency_store(struct device *dev,
@@ -833,6 +1123,23 @@ static ssize_t sampling_frequency_store(struct device *dev,
if (ret)
return ret;
+ if (st->manual_mode && st->offload) {
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_PERIODIC,
+ .periodic = { .frequency_hz = freq },
+ };
+
+ ret = spi_offload_trigger_validate(st->offload->trigger, &config);
+ if (ret) {
+ iio_device_release_direct(indio_dev);
+ return ret;
+ }
+
+ st->offload->trigger_hz = config.periodic.frequency_hz;
+ iio_device_release_direct(indio_dev);
+ return len;
+ }
+
ret = ad4691_set_pwm_freq(st, freq);
iio_device_release_direct(indio_dev);
if (ret)
@@ -1132,9 +1439,75 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
ad4691_buffer_attrs);
}
+static int ad4691_setup_offload(struct iio_dev *indio_dev,
+ struct ad4691_state *st,
+ struct spi_offload *spi_offload)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct ad4691_offload_state *offload;
+ struct dma_chan *rx_dma;
+ int ret;
+
+ offload = devm_kzalloc(dev, sizeof(*offload), GFP_KERNEL);
+ if (!offload)
+ return -ENOMEM;
+
+ offload->spi = spi_offload;
+ st->offload = offload;
+
+ if (st->manual_mode) {
+ offload->trigger =
+ devm_spi_offload_trigger_get(dev, offload->spi,
+ SPI_OFFLOAD_TRIGGER_PERIODIC);
+ if (IS_ERR(offload->trigger))
+ return dev_err_probe(dev, PTR_ERR(offload->trigger),
+ "Failed to get periodic offload trigger\n");
+
+ offload->trigger_hz = AD4691_OFFLOAD_INITIAL_TRIGGER_HZ;
+ } else {
+ struct spi_offload_trigger_info trigger_info = {
+ .fwnode = dev_fwnode(dev),
+ .ops = &ad4691_offload_trigger_ops,
+ .priv = st,
+ };
+
+ ret = devm_spi_offload_trigger_register(dev, &trigger_info);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to register offload trigger\n");
+
+ offload->trigger =
+ devm_spi_offload_trigger_get(dev, offload->spi,
+ SPI_OFFLOAD_TRIGGER_DATA_READY);
+ if (IS_ERR(offload->trigger))
+ return dev_err_probe(dev, PTR_ERR(offload->trigger),
+ "Failed to get DATA_READY offload trigger\n");
+ }
+
+ rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, offload->spi);
+ if (IS_ERR(rx_dma))
+ return dev_err_probe(dev, PTR_ERR(rx_dma),
+ "Failed to get offload RX DMA channel\n");
+
+ if (st->manual_mode)
+ indio_dev->setup_ops = &ad4691_manual_offload_buffer_setup_ops;
+ else
+ indio_dev->setup_ops = &ad4691_cnv_burst_offload_buffer_setup_ops;
+
+ ret = devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma,
+ IIO_BUFFER_DIRECTION_IN);
+ if (ret)
+ return ret;
+
+ indio_dev->buffer->attrs = ad4691_buffer_attrs;
+
+ return 0;
+}
+
static int ad4691_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
+ struct spi_offload *spi_offload;
struct iio_dev *indio_dev;
struct ad4691_state *st;
int ret;
@@ -1168,13 +1541,26 @@ static int ad4691_probe(struct spi_device *spi)
if (ret)
return ret;
+ spi_offload = devm_spi_offload_get(dev, spi, &ad4691_offload_config);
+ ret = PTR_ERR_OR_ZERO(spi_offload);
+ if (ret == -ENODEV)
+ spi_offload = NULL;
+ else if (ret)
+ return dev_err_probe(dev, ret, "Failed to get SPI offload\n");
+
indio_dev->name = st->info->name;
indio_dev->info = &ad4691_info;
indio_dev->modes = INDIO_DIRECT_MODE;
- indio_dev->channels = st->info->sw_info->channels;
- indio_dev->num_channels = st->info->sw_info->num_channels;
- ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (spi_offload) {
+ indio_dev->channels = st->info->offload_info->channels;
+ indio_dev->num_channels = st->info->offload_info->num_channels;
+ ret = ad4691_setup_offload(indio_dev, st, spi_offload);
+ } else {
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ }
if (ret)
return ret;
@@ -1212,3 +1598,5 @@ module_spi_driver(ad4691_driver);
MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("IIO_DMA_BUFFER");
+MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER");
--
2.43.0
^ permalink raw reply related
* [PATCH v7 5/6] iio: adc: ad4691: add oversampling support
From: Radu Sabau via B4 Relay @ 2026-04-09 15:28 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add per-channel oversampling ratio (OSR) support for CNV burst mode.
The accumulator depth register (ACC_DEPTH_IN) is programmed with the
selected OSR at buffer enable time and before each single-shot read.
Supported OSR values: 1, 2, 4, 8, 16, 32.
Introduce AD4691_MANUAL_CHANNEL() for manual mode channels, which do
not expose the oversampling ratio attribute since OSR is not applicable
in that mode. A separate manual_channels array is added to
struct ad4691_channel_info and selected at probe time; offload paths
reuse the same arrays with num_channels capping access before the soft
timestamp entry.
The reported sampling frequency accounts for the active OSR:
effective_freq = oscillator_freq / osr
OSR defaults to 1 (no accumulation) for all channels.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/ad4691.c | 137 +++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 128 insertions(+), 9 deletions(-)
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index 839ea7f44c78..ef96d736996e 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -116,6 +116,7 @@ enum ad4691_ref_ctrl {
struct ad4691_channel_info {
const struct iio_chan_spec *channels;
+ const struct iio_chan_spec *manual_channels;
unsigned int num_channels;
};
@@ -126,7 +127,34 @@ struct ad4691_chip_info {
const struct ad4691_channel_info *offload_info;
};
+/* CNV burst mode channel — exposes oversampling ratio. */
#define AD4691_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ .endianness = IIO_BE, \
+ }, \
+ }
+
+/*
+ * Manual mode channel — no oversampling ratio attribute. OSR is not
+ * supported in manual mode; ACC_DEPTH_IN is not configured during manual
+ * buffer enable.
+ */
+#define AD4691_MANUAL_CHANNEL(ch) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
@@ -177,25 +205,65 @@ static const struct iio_chan_spec ad4693_channels[] = {
IIO_CHAN_SOFT_TIMESTAMP(8),
};
+static const struct iio_chan_spec ad4691_manual_channels[] = {
+ AD4691_MANUAL_CHANNEL(0),
+ AD4691_MANUAL_CHANNEL(1),
+ AD4691_MANUAL_CHANNEL(2),
+ AD4691_MANUAL_CHANNEL(3),
+ AD4691_MANUAL_CHANNEL(4),
+ AD4691_MANUAL_CHANNEL(5),
+ AD4691_MANUAL_CHANNEL(6),
+ AD4691_MANUAL_CHANNEL(7),
+ AD4691_MANUAL_CHANNEL(8),
+ AD4691_MANUAL_CHANNEL(9),
+ AD4691_MANUAL_CHANNEL(10),
+ AD4691_MANUAL_CHANNEL(11),
+ AD4691_MANUAL_CHANNEL(12),
+ AD4691_MANUAL_CHANNEL(13),
+ AD4691_MANUAL_CHANNEL(14),
+ AD4691_MANUAL_CHANNEL(15),
+ IIO_CHAN_SOFT_TIMESTAMP(16),
+};
+
+static const struct iio_chan_spec ad4693_manual_channels[] = {
+ AD4691_MANUAL_CHANNEL(0),
+ AD4691_MANUAL_CHANNEL(1),
+ AD4691_MANUAL_CHANNEL(2),
+ AD4691_MANUAL_CHANNEL(3),
+ AD4691_MANUAL_CHANNEL(4),
+ AD4691_MANUAL_CHANNEL(5),
+ AD4691_MANUAL_CHANNEL(6),
+ AD4691_MANUAL_CHANNEL(7),
+ IIO_CHAN_SOFT_TIMESTAMP(8),
+};
+
+static const int ad4691_oversampling_ratios[] = { 1, 2, 4, 8, 16, 32 };
+
static const struct ad4691_channel_info ad4691_sw_info = {
.channels = ad4691_channels,
+ .manual_channels = ad4691_manual_channels,
.num_channels = ARRAY_SIZE(ad4691_channels),
};
static const struct ad4691_channel_info ad4693_sw_info = {
.channels = ad4693_channels,
+ .manual_channels = ad4693_manual_channels,
.num_channels = ARRAY_SIZE(ad4693_channels),
};
static const struct ad4691_channel_info ad4691_offload_info = {
.channels = ad4691_channels,
- /* No soft timestamp; num_channels caps access to 16. */
+ /*
+ * Offload paths share the SW channel arrays. num_channels caps access
+ * before the soft timestamp entry, so no separate array is needed.
+ */
+ .manual_channels = ad4691_manual_channels,
.num_channels = 16,
};
static const struct ad4691_channel_info ad4693_offload_info = {
.channels = ad4693_channels,
- /* No soft timestamp; num_channels caps access to 8. */
+ .manual_channels = ad4693_manual_channels,
.num_channels = 8,
};
@@ -270,6 +338,8 @@ struct ad4691_state {
int irq;
int vref_uV;
u32 cnv_period_ns;
+ /* Per-channel oversampling ratio; always 1 in manual mode. */
+ u8 osr[16];
bool manual_mode;
bool refbuf_en;
@@ -490,7 +560,8 @@ static const struct regmap_config ad4691_regmap_config = {
.cache_type = REGCACHE_MAPLE,
};
-static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
+static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val,
+ unsigned int osr)
{
unsigned int reg_val;
int ret;
@@ -499,7 +570,7 @@ static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
if (ret)
return ret;
- *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)];
+ *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)] / osr;
return IIO_VAL_INT;
}
@@ -536,6 +607,11 @@ static int ad4691_read_avail(struct iio_dev *indio_dev,
*type = IIO_VAL_INT;
*length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
return IIO_AVAIL_LIST;
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ *vals = ad4691_oversampling_ratios;
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(ad4691_oversampling_ratios);
+ return IIO_AVAIL_LIST;
default:
return -EINVAL;
}
@@ -566,6 +642,11 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
if (ret)
return ret;
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(chan->channel),
+ st->osr[chan->channel]);
+ if (ret)
+ return ret;
+
ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
if (ret)
return ret;
@@ -575,8 +656,9 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
return ret;
osc_idx = FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val);
- /* Wait 2 oscillator periods for the conversion to complete. */
- period_us = DIV_ROUND_UP(2UL * USEC_PER_SEC, ad4691_osc_freqs_Hz[osc_idx]);
+ /* Wait osr oscillator periods for all accumulator samples to complete. */
+ period_us = DIV_ROUND_UP((unsigned long)st->osr[chan->channel] * USEC_PER_SEC,
+ ad4691_osc_freqs_Hz[osc_idx]);
fsleep(period_us);
ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 0);
@@ -611,7 +693,10 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
return ad4691_single_shot_read(indio_dev, chan, val);
}
case IIO_CHAN_INFO_SAMP_FREQ:
- return ad4691_get_sampling_freq(st, val);
+ return ad4691_get_sampling_freq(st, val, st->osr[chan->channel]);
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ *val = st->osr[chan->channel];
+ return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
*val = st->vref_uV / (MICRO / MILLI);
*val2 = chan->scan_type.realbits;
@@ -625,9 +710,24 @@ static int ad4691_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
return ad4691_set_sampling_freq(indio_dev, val);
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_oversampling_ratios); i++) {
+ if (ad4691_oversampling_ratios[i] != val)
+ continue;
+ st->osr[chan->channel] = val;
+ return 0;
+ }
+ return -EINVAL;
+ }
default:
return -EINVAL;
}
@@ -842,6 +942,12 @@ static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
if (ret)
goto err_unoptimize;
+ iio_for_each_active_channel(indio_dev, i) {
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(i), st->osr[i]);
+ if (ret)
+ goto err_unoptimize;
+ }
+
ret = ad4691_enter_conversion_mode(st);
if (ret)
goto err_unoptimize;
@@ -998,6 +1104,12 @@ static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
if (ret)
return ret;
+ iio_for_each_active_channel(indio_dev, bit) {
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit), st->osr[bit]);
+ if (ret)
+ return ret;
+ }
+
ret = ad4691_enter_conversion_mode(st);
if (ret)
return ret;
@@ -1519,6 +1631,7 @@ static int ad4691_probe(struct spi_device *spi)
st = iio_priv(indio_dev);
st->spi = spi;
st->info = spi_get_device_match_data(spi);
+ memset(st->osr, 1, sizeof(st->osr));
ret = devm_mutex_init(dev, &st->lock);
if (ret)
@@ -1553,11 +1666,17 @@ static int ad4691_probe(struct spi_device *spi)
indio_dev->modes = INDIO_DIRECT_MODE;
if (spi_offload) {
- indio_dev->channels = st->info->offload_info->channels;
+ if (st->manual_mode)
+ indio_dev->channels = st->info->offload_info->manual_channels;
+ else
+ indio_dev->channels = st->info->offload_info->channels;
indio_dev->num_channels = st->info->offload_info->num_channels;
ret = ad4691_setup_offload(indio_dev, st, spi_offload);
} else {
- indio_dev->channels = st->info->sw_info->channels;
+ if (st->manual_mode)
+ indio_dev->channels = st->info->sw_info->manual_channels;
+ else
+ indio_dev->channels = st->info->sw_info->channels;
indio_dev->num_channels = st->info->sw_info->num_channels;
ret = ad4691_setup_triggered_buffer(indio_dev, st);
}
--
2.43.0
^ permalink raw reply related
* [PATCH v7 3/6] iio: adc: ad4691: add triggered buffer support
From: Radu Sabau via B4 Relay @ 2026-04-09 15:28 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add buffered capture support using the IIO triggered buffer framework.
CNV Burst Mode: the GP pin identified by interrupt-names in the device
tree is configured as DATA_READY output. The IRQ handler stops
conversions and fires the IIO trigger; the trigger handler executes a
pre-built SPI message that reads all active channels from the AVG_IN
accumulator registers and then resets accumulator state and restarts
conversions for the next cycle.
Manual Mode: CNV is tied to SPI CS so each transfer simultaneously
reads the previous result and starts the next conversion (pipelined
N+1 scheme). At preenable time a pre-built, optimised SPI message of
N+1 transfers is constructed (N channel reads plus one NOOP to drain
the pipeline). The trigger handler executes the message in a single
spi_sync() call and collects the results. An external trigger (e.g.
iio-trig-hrtimer) is required to drive the trigger at the desired
sample rate.
Both modes share the same trigger handler and push a complete scan —
one u16 slot per channel at its scan_index position, followed by a
timestamp — to the IIO buffer via iio_push_to_buffers_with_ts().
The CNV Burst Mode sampling frequency (PWM period) is exposed as a
buffer-level attribute via IIO_DEVICE_ATTR.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 553 +++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 540 insertions(+), 15 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 3685a03aa8dc..d498f16c0816 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -142,6 +142,8 @@ config AD4170_4
config AD4691
tristate "Analog Devices AD4691 Family ADC Driver"
depends on SPI
+ select IIO_BUFFER
+ select IIO_TRIGGERED_BUFFER
select REGMAP
help
Say yes here to build support for Analog Devices AD4691 Family MuxSAR
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index 43bd408c3d11..3e5caa0972eb 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -5,15 +5,19 @@
*/
#include <linux/array_size.h>
#include <linux/bitfield.h>
-#include <linux/bitops.h>
+#include <linux/bitmap.h>
#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/device/devres.h>
+#include <linux/dmaengine.h>
#include <linux/err.h>
+#include <linux/interrupt.h>
#include <linux/math.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
+#include <linux/property.h>
+#include <linux/pwm.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/reset.h>
@@ -21,7 +25,14 @@
#include <linux/units.h>
#include <linux/unaligned.h>
+#include <linux/iio/buffer.h>
+#include <linux/iio/buffer-dma.h>
+#include <linux/iio/buffer-dmaengine.h>
#include <linux/iio/iio.h>
+#include <linux/iio/sysfs.h>
+#include <linux/iio/trigger.h>
+#include <linux/iio/triggered_buffer.h>
+#include <linux/iio/trigger_consumer.h>
#define AD4691_VREF_uV_MIN 2400000
#define AD4691_VREF_uV_MAX 5250000
@@ -30,6 +41,9 @@
#define AD4691_VREF_3P3_uV_MAX 3750000
#define AD4691_VREF_4P096_uV_MAX 4500000
+#define AD4691_CNV_DUTY_CYCLE_NS 380
+#define AD4691_CNV_HIGH_TIME_NS 430
+
#define AD4691_SPI_CONFIG_A_REG 0x000
#define AD4691_SW_RESET (BIT(7) | BIT(0))
@@ -37,6 +51,7 @@
#define AD4691_CLAMP_STATUS1_REG 0x01A
#define AD4691_CLAMP_STATUS2_REG 0x01B
#define AD4691_DEVICE_SETUP 0x020
+#define AD4691_MANUAL_MODE BIT(2)
#define AD4691_LDO_EN BIT(4)
#define AD4691_REF_CTRL 0x021
#define AD4691_REF_CTRL_MASK GENMASK(4, 2)
@@ -44,13 +59,18 @@
#define AD4691_OSC_FREQ_REG 0x023
#define AD4691_OSC_FREQ_MASK GENMASK(3, 0)
#define AD4691_STD_SEQ_CONFIG 0x025
+#define AD4691_SEQ_ALL_CHANNELS_OFF 0x00
#define AD4691_SPARE_CONTROL 0x02A
+#define AD4691_NOOP 0x00
+#define AD4691_ADC_CHAN(ch) ((0x10 + (ch)) << 3)
+
#define AD4691_OSC_EN_REG 0x180
#define AD4691_STATE_RESET_REG 0x181
#define AD4691_STATE_RESET_ALL 0x01
#define AD4691_ADC_SETUP 0x182
#define AD4691_ADC_MODE_MASK GENMASK(1, 0)
+#define AD4691_CNV_BURST_MODE 0x01
#define AD4691_AUTONOMOUS_MODE 0x02
/*
* ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
@@ -60,6 +80,8 @@
#define AD4691_ACC_DEPTH_IN(n) (0x186 + (n))
#define AD4691_GPIO_MODE1_REG 0x196
#define AD4691_GPIO_MODE2_REG 0x197
+#define AD4691_GP_MODE_MASK GENMASK(3, 0)
+#define AD4691_GP_MODE_DATA_READY 0x06
#define AD4691_GPIO_READ 0x1A0
#define AD4691_ACC_STATUS_FULL1_REG 0x1B0
#define AD4691_ACC_STATUS_FULL2_REG 0x1B1
@@ -83,19 +105,23 @@ enum ad4691_ref_ctrl {
AD4691_VREF_5P0 = 4,
};
-struct ad4691_chip_info {
+struct ad4691_channel_info {
const struct iio_chan_spec *channels;
- const char *name;
unsigned int num_channels;
+};
+
+struct ad4691_chip_info {
+ const char *name;
unsigned int max_rate;
+ const struct ad4691_channel_info *sw_info;
};
#define AD4691_CHANNEL(ch) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
- .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
- | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_separate_available = \
BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
@@ -105,6 +131,7 @@ struct ad4691_chip_info {
.sign = 'u', \
.realbits = 16, \
.storagebits = 16, \
+ .endianness = IIO_BE, \
}, \
}
@@ -125,6 +152,7 @@ static const struct iio_chan_spec ad4691_channels[] = {
AD4691_CHANNEL(13),
AD4691_CHANNEL(14),
AD4691_CHANNEL(15),
+ IIO_CHAN_SOFT_TIMESTAMP(16),
};
static const struct iio_chan_spec ad4693_channels[] = {
@@ -136,6 +164,17 @@ static const struct iio_chan_spec ad4693_channels[] = {
AD4691_CHANNEL(5),
AD4691_CHANNEL(6),
AD4691_CHANNEL(7),
+ IIO_CHAN_SOFT_TIMESTAMP(8),
+};
+
+static const struct ad4691_channel_info ad4691_sw_info = {
+ .channels = ad4691_channels,
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+};
+
+static const struct ad4691_channel_info ad4693_sw_info = {
+ .channels = ad4693_channels,
+ .num_channels = ARRAY_SIZE(ad4693_channels),
};
/*
@@ -162,38 +201,43 @@ static const int ad4691_osc_freqs_Hz[] = {
[0xF] = 1250,
};
+static const char * const ad4691_gp_names[] = { "gp0", "gp1", "gp2", "gp3" };
+
static const struct ad4691_chip_info ad4691_chip_info = {
- .channels = ad4691_channels,
.name = "ad4691",
- .num_channels = ARRAY_SIZE(ad4691_channels),
.max_rate = 500 * HZ_PER_KHZ,
+ .sw_info = &ad4691_sw_info,
};
static const struct ad4691_chip_info ad4692_chip_info = {
- .channels = ad4691_channels,
.name = "ad4692",
- .num_channels = ARRAY_SIZE(ad4691_channels),
.max_rate = 1 * HZ_PER_MHZ,
+ .sw_info = &ad4691_sw_info,
};
static const struct ad4691_chip_info ad4693_chip_info = {
- .channels = ad4693_channels,
.name = "ad4693",
- .num_channels = ARRAY_SIZE(ad4693_channels),
.max_rate = 500 * HZ_PER_KHZ,
+ .sw_info = &ad4693_sw_info,
};
static const struct ad4691_chip_info ad4694_chip_info = {
- .channels = ad4693_channels,
.name = "ad4694",
- .num_channels = ARRAY_SIZE(ad4693_channels),
.max_rate = 1 * HZ_PER_MHZ,
+ .sw_info = &ad4693_sw_info,
};
struct ad4691_state {
const struct ad4691_chip_info *info;
struct regmap *regmap;
+ struct spi_device *spi;
+
+ struct pwm_device *conv_trigger;
+ int irq;
int vref_uV;
+ u32 cnv_period_ns;
+
+ bool manual_mode;
bool refbuf_en;
bool ldo_en;
/*
@@ -201,8 +245,45 @@ struct ad4691_state {
* atomicity of consecutive SPI operations.
*/
struct mutex lock;
+ /*
+ * Per-buffer-enable lifetime resources:
+ * Manual Mode - a pre-built SPI message that clocks out N+1
+ * transfers in one go.
+ * CNV Burst Mode - a pre-built SPI message that clocks out 2*N
+ * transfers in one go.
+ */
+ struct spi_message scan_msg;
+ /* max 16 + 1 NOOP (manual) or 2*16 + 2 (CNV burst). */
+ struct spi_transfer scan_xfers[34];
+ /*
+ * CNV burst: 16 AVG_IN addresses + state-reset address + state-reset
+ * value = 18. Manual: 16 channel cmds + 1 NOOP = 17.
+ */
+ __be16 scan_tx[18];
+ /* Scan buffer: one BE16 slot per channel (rx'd directly), plus timestamp */
+ struct {
+ __be16 vals[16];
+ aligned_s64 ts;
+ } scan;
};
+/*
+ * Configure the given GP pin (0-3) as DATA_READY output.
+ * GP0/GP1 → GPIO_MODE1_REG, GP2/GP3 → GPIO_MODE2_REG.
+ * Even pins occupy bits [3:0], odd pins bits [7:4].
+ */
+static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
+{
+ unsigned int bit_off = gp_num % 2;
+ unsigned int reg_off = gp_num / 2;
+ unsigned int shift = 4 * bit_off;
+
+ return regmap_update_bits(st->regmap,
+ AD4691_GPIO_MODE1_REG + reg_off,
+ AD4691_GP_MODE_MASK << shift,
+ AD4691_GP_MODE_DATA_READY << shift);
+}
+
static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct spi_device *spi = context;
@@ -486,6 +567,346 @@ static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
return regmap_write(st->regmap, reg, writeval);
}
+static int ad4691_set_pwm_freq(struct ad4691_state *st, int freq)
+{
+ if (!freq)
+ return -EINVAL;
+
+ st->cnv_period_ns = DIV_ROUND_UP(NSEC_PER_SEC, freq);
+ return 0;
+}
+
+static int ad4691_sampling_enable(struct ad4691_state *st, bool enable)
+{
+ struct pwm_state conv_state = {
+ .period = st->cnv_period_ns,
+ .duty_cycle = AD4691_CNV_DUTY_CYCLE_NS,
+ .polarity = PWM_POLARITY_NORMAL,
+ .enabled = enable,
+ };
+
+ return pwm_apply_might_sleep(st->conv_trigger, &conv_state);
+}
+
+/*
+ * ad4691_enter_conversion_mode - Switch the chip to its buffer conversion mode.
+ *
+ * Configures the ADC hardware registers for the mode selected at probe
+ * (CNV_BURST or MANUAL). Called from buffer preenable before starting
+ * sampling. The chip is in AUTONOMOUS mode during idle (for read_raw).
+ */
+static int ad4691_enter_conversion_mode(struct ad4691_state *st)
+{
+ int ret;
+
+ if (st->manual_mode)
+ return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
+
+ ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
+ if (ret)
+ return ret;
+
+ return regmap_write(st->regmap, AD4691_STATE_RESET_REG,
+ AD4691_STATE_RESET_ALL);
+}
+
+/*
+ * ad4691_exit_conversion_mode - Return the chip to AUTONOMOUS mode.
+ *
+ * Called from buffer postdisable to restore the chip to the
+ * idle state used by read_raw. Clears the sequencer and resets state.
+ */
+static int ad4691_exit_conversion_mode(struct ad4691_state *st)
+{
+ if (st->manual_mode)
+ return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_MANUAL_MODE, 0);
+
+ return regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
+}
+
+static int ad4691_manual_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int n_active;
+ unsigned int n_xfers;
+ unsigned int prev_i, k, i;
+ bool first;
+ int ret;
+
+ n_active = bitmap_weight(indio_dev->active_scan_mask, iio_get_masklength(indio_dev));
+ n_xfers = n_active + 1;
+
+ memset(st->scan_xfers, 0, n_xfers * sizeof(st->scan_xfers[0]));
+ memset(st->scan_tx, 0, n_xfers * sizeof(st->scan_tx[0]));
+
+ spi_message_init(&st->scan_msg);
+
+ first = true;
+ prev_i = 0;
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ st->scan_tx[k] = cpu_to_be16(AD4691_ADC_CHAN(i));
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k];
+ /*
+ * The pipeline means xfer[0] receives the residual from the
+ * previous sequence, not a valid sample for channel i. Point
+ * it at vals[i] anyway; xfer[1] (or the NOOP when only one
+ * channel is active) will overwrite that slot with the real
+ * result, so no separate dummy buffer is needed.
+ */
+ if (first) {
+ st->scan_xfers[k].rx_buf = &st->scan.vals[i];
+ first = false;
+ } else {
+ st->scan_xfers[k].rx_buf = &st->scan.vals[prev_i];
+ }
+ st->scan_xfers[k].len = sizeof(__be16);
+ st->scan_xfers[k].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+ prev_i = i;
+ k++;
+ }
+
+ /* Final NOOP transfer retrieves the last channel's result. */
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k]; /* scan_tx[k] == 0 == NOOP */
+ st->scan_xfers[k].rx_buf = &st->scan.vals[prev_i];
+ st->scan_xfers[k].len = sizeof(__be16);
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret) {
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ad4691_manual_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ ret = ad4691_exit_conversion_mode(st);
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static const struct iio_buffer_setup_ops ad4691_manual_buffer_setup_ops = {
+ .preenable = &ad4691_manual_buffer_preenable,
+ .postdisable = &ad4691_manual_buffer_postdisable,
+};
+
+static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int n_active;
+ unsigned int k, i;
+ int ret;
+
+ n_active = bitmap_weight(indio_dev->active_scan_mask, iio_get_masklength(indio_dev));
+
+ memset(st->scan_xfers, 0, (2 * n_active + 2) * sizeof(st->scan_xfers[0]));
+ memset(st->scan_tx, 0, (n_active + 2) * sizeof(st->scan_tx[0]));
+
+ spi_message_init(&st->scan_msg);
+
+ /*
+ * Each AVG_IN read needs two transfers: a 2-byte address write phase
+ * followed by a 2-byte data read phase. CS toggles between channels
+ * (cs_change=1 on the read phase of all but the last channel).
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ st->scan_tx[k] = cpu_to_be16(0x8000 | AD4691_AVG_IN(i));
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(__be16);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+ st->scan_xfers[2 * k + 1].rx_buf = &st->scan.vals[i];
+ st->scan_xfers[2 * k + 1].len = sizeof(__be16);
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
+ k++;
+ }
+
+ st->scan_tx[k] = cpu_to_be16(AD4691_STATE_RESET_REG);
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(__be16);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+ st->scan_tx[k + 1] = cpu_to_be16(AD4691_STATE_RESET_ALL << 8);
+ st->scan_xfers[2 * k + 1].tx_buf = &st->scan_tx[k + 1];
+ st->scan_xfers[2 * k + 1].len = sizeof(__be16);
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)));
+ if (ret)
+ goto err_unoptimize;
+
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
+ ~bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0));
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ goto err_exit_conv;
+
+ enable_irq(st->irq);
+ return 0;
+
+err_exit_conv:
+ ad4691_exit_conversion_mode(st);
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static int ad4691_cnv_burst_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ disable_irq(st->irq);
+
+ ret = ad4691_sampling_enable(st, false);
+ if (ret)
+ return ret;
+
+ ret = ad4691_exit_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ AD4691_SEQ_ALL_CHANNELS_OFF);
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
+ .preenable = &ad4691_cnv_burst_buffer_preenable,
+ .postdisable = &ad4691_cnv_burst_buffer_postdisable,
+};
+
+static ssize_t sampling_frequency_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ return sysfs_emit(buf, "%u\n", NSEC_PER_SEC / st->cnv_period_ns);
+}
+
+static ssize_t sampling_frequency_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int freq, ret;
+
+ ret = kstrtoint(buf, 10, &freq);
+ if (ret)
+ return ret;
+
+ ret = iio_device_claim_direct(indio_dev);
+ if (ret)
+ return ret;
+
+ ret = ad4691_set_pwm_freq(st, freq);
+ iio_device_release_direct(indio_dev);
+ if (ret)
+ return ret;
+
+ return len;
+}
+
+static IIO_DEVICE_ATTR_RW(sampling_frequency, 0);
+
+static const struct iio_dev_attr *ad4691_buffer_attrs[] = {
+ &iio_dev_attr_sampling_frequency,
+ NULL
+};
+
+static irqreturn_t ad4691_irq(int irq, void *private)
+{
+ struct iio_dev *indio_dev = private;
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ iio_trigger_poll(indio_dev->trig);
+ /*
+ * Keep the DATA_READY IRQ disabled until the trigger handler has
+ * finished reading the scan, to prevent a new assertion mid-transfer.
+ * The PWM continues running uninterrupted; the IRQ is re-enabled in
+ * ad4691_trigger_handler once spi_sync completes.
+ *
+ * IRQF_ONESHOT already masks the hardware line during this threaded
+ * handler, so disable_irq_nosync here ensures the IRQ stays disabled
+ * even after IRQF_ONESHOT unmasks on return.
+ */
+ disable_irq_nosync(st->irq);
+
+ return IRQ_HANDLED;
+}
+
+static const struct iio_trigger_ops ad4691_trigger_ops = {
+ .validate_device = iio_trigger_validate_own_device,
+};
+
+static int ad4691_read_scan(struct iio_dev *indio_dev, s64 timestamp)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ guard(mutex)(&st->lock);
+
+ ret = spi_sync(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ /*
+ * rx_buf pointers in scan_xfers point directly into scan.vals, so no
+ * copy is needed. The scan_msg already includes a STATE_RESET at the
+ * end (appended in preenable), so no explicit reset is needed here.
+ */
+ iio_push_to_buffers_with_ts(indio_dev, &st->scan, sizeof(st->scan),
+ timestamp);
+ return 0;
+}
+
+static irqreturn_t ad4691_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ ad4691_read_scan(indio_dev, pf->timestamp);
+ if (!st->manual_mode)
+ enable_irq(st->irq);
+ iio_trigger_notify_done(indio_dev->trig);
+ return IRQ_HANDLED;
+}
+
static const struct iio_info ad4691_info = {
.read_raw = &ad4691_read_raw,
.write_raw = &ad4691_write_raw,
@@ -493,6 +914,18 @@ static const struct iio_info ad4691_info = {
.debugfs_reg_access = &ad4691_reg_access,
};
+static int ad4691_pwm_setup(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+
+ st->conv_trigger = devm_pwm_get(dev, "cnv");
+ if (IS_ERR(st->conv_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->conv_trigger),
+ "Failed to get cnv pwm\n");
+
+ return ad4691_set_pwm_freq(st, st->info->max_rate);
+}
+
static int ad4691_regulator_setup(struct ad4691_state *st)
{
struct device *dev = regmap_get_device(st->regmap);
@@ -558,6 +991,22 @@ static int ad4691_config(struct ad4691_state *st)
unsigned int val;
int ret;
+ /*
+ * Determine buffer conversion mode from DT: if a PWM is provided it
+ * drives the CNV pin (CNV_BURST_MODE); otherwise CNV is tied to CS
+ * and each SPI transfer triggers a conversion (MANUAL_MODE).
+ * Both modes idle in AUTONOMOUS mode so that read_raw can use the
+ * internal oscillator without disturbing the hardware configuration.
+ */
+ if (device_property_present(dev, "pwms")) {
+ st->manual_mode = false;
+ ret = ad4691_pwm_setup(st);
+ if (ret)
+ return ret;
+ } else {
+ st->manual_mode = true;
+ }
+
switch (st->vref_uV) {
case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
ref_val = AD4691_VREF_2P5;
@@ -613,6 +1062,76 @@ static int ad4691_config(struct ad4691_state *st)
return 0;
}
+static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
+ struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct iio_trigger *trig;
+ unsigned int i;
+ int irq, ret;
+
+ trig = devm_iio_trigger_alloc(dev, "%s-dev%d",
+ indio_dev->name,
+ iio_device_id(indio_dev));
+ if (!trig)
+ return -ENOMEM;
+
+ trig->ops = &ad4691_trigger_ops;
+ iio_trigger_set_drvdata(trig, st);
+
+ ret = devm_iio_trigger_register(dev, trig);
+ if (ret)
+ return dev_err_probe(dev, ret, "IIO trigger register failed\n");
+
+ indio_dev->trig = iio_trigger_get(trig);
+
+ if (st->manual_mode)
+ return devm_iio_triggered_buffer_setup(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ &ad4691_trigger_handler,
+ &ad4691_manual_buffer_setup_ops);
+
+ /*
+ * The GP pin named in interrupt-names asserts at end-of-conversion.
+ * The IRQ handler stops conversions and fires the IIO trigger so
+ * the trigger handler can read and push the sample to the buffer.
+ * The IRQ is kept disabled until the buffer is enabled.
+ */
+ irq = -ENXIO;
+ for (i = 0; i < ARRAY_SIZE(ad4691_gp_names); i++) {
+ irq = fwnode_irq_get_byname(dev_fwnode(dev),
+ ad4691_gp_names[i]);
+ if (irq > 0)
+ break;
+ }
+ if (irq < 0)
+ return dev_err_probe(dev, irq, "failed to get GP interrupt\n");
+
+ st->irq = irq;
+
+ ret = ad4691_gpio_setup(st, i);
+ if (ret)
+ return ret;
+
+ /*
+ * IRQ is kept disabled until the buffer is enabled to prevent
+ * spurious DATA_READY events before the SPI message is set up.
+ */
+ ret = devm_request_threaded_irq(dev, irq, NULL,
+ &ad4691_irq,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ indio_dev->name, indio_dev);
+ if (ret)
+ return ret;
+
+ return devm_iio_triggered_buffer_setup_ext(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ &ad4691_trigger_handler,
+ IIO_BUFFER_DIRECTION_IN,
+ &ad4691_cnv_burst_buffer_setup_ops,
+ ad4691_buffer_attrs);
+}
+
static int ad4691_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
@@ -625,6 +1144,7 @@ static int ad4691_probe(struct spi_device *spi)
return -ENOMEM;
st = iio_priv(indio_dev);
+ st->spi = spi;
st->info = spi_get_device_match_data(spi);
ret = devm_mutex_init(dev, &st->lock);
@@ -652,8 +1172,11 @@ static int ad4691_probe(struct spi_device *spi)
indio_dev->info = &ad4691_info;
indio_dev->modes = INDIO_DIRECT_MODE;
- indio_dev->channels = st->info->channels;
- indio_dev->num_channels = st->info->num_channels;
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (ret)
+ return ret;
return devm_iio_device_register(dev, indio_dev);
}
--
2.43.0
^ permalink raw reply related
* [PATCH v7 2/6] iio: adc: ad4691: add initial driver for AD4691 family
From: Radu Sabau via B4 Relay @ 2026-04-09 15:28 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add support for the Analog Devices AD4691 family of high-speed,
low-power multichannel SAR ADCs: AD4691 (16-ch, 500 kSPS),
AD4692 (16-ch, 1 MSPS), AD4693 (8-ch, 500 kSPS) and
AD4694 (8-ch, 1 MSPS).
The driver implements a custom regmap layer over raw SPI to handle the
device's mixed 1/2/3/4-byte register widths and uses the standard IIO
read_raw/write_raw interface for single-channel reads.
The chip idles in Autonomous Mode so that single-shot read_raw can use
the internal oscillator without disturbing the hardware configuration.
Three voltage supply domains are managed: avdd (required), vio, and a
reference supply on either the REF pin (ref-supply, external buffer)
or the REFIN pin (refin-supply, uses the on-chip reference buffer;
REFBUF_EN is set accordingly). Hardware reset is performed via
the reset controller framework; a software reset through SPI_CONFIG_A
is used as fallback when no hardware reset is available.
Accumulator channel masking for single-shot reads uses ACC_MASK_REG via
an ADDR_DESCENDING SPI write, which covers both mask bytes in a single
16-bit transfer.
Reviewed-by: David Lechner <dlechner@baylibre.com>
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 11 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 691 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 704 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 438ca850fa1c..24e4502b8292 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1490,6 +1490,7 @@ L: linux-iio@vger.kernel.org
S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
+F: drivers/iio/adc/ad4691.c
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 60038ae8dfc4..3685a03aa8dc 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -139,6 +139,17 @@ config AD4170_4
To compile this driver as a module, choose M here: the module will be
called ad4170-4.
+config AD4691
+ tristate "Analog Devices AD4691 Family ADC Driver"
+ depends on SPI
+ select REGMAP
+ help
+ Say yes here to build support for Analog Devices AD4691 Family MuxSAR
+ SPI analog to digital converters (ADC).
+
+ To compile this driver as a module, choose M here: the module will be
+ called ad4691.
+
config AD4695
tristate "Analog Device AD4695 ADC Driver"
depends on SPI
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index c76550415ff1..4ac1ea09d773 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_AD4080) += ad4080.o
obj-$(CONFIG_AD4130) += ad4130.o
obj-$(CONFIG_AD4134) += ad4134.o
obj-$(CONFIG_AD4170_4) += ad4170-4.o
+obj-$(CONFIG_AD4691) += ad4691.o
obj-$(CONFIG_AD4695) += ad4695.o
obj-$(CONFIG_AD4851) += ad4851.o
obj-$(CONFIG_AD7091R) += ad7091r-base.o
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
new file mode 100644
index 000000000000..43bd408c3d11
--- /dev/null
+++ b/drivers/iio/adc/ad4691.c
@@ -0,0 +1,691 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024-2026 Analog Devices, Inc.
+ * Author: Radu Sabau <radu.sabau@analog.com>
+ */
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device/devres.h>
+#include <linux/err.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/spi/spi.h>
+#include <linux/units.h>
+#include <linux/unaligned.h>
+
+#include <linux/iio/iio.h>
+
+#define AD4691_VREF_uV_MIN 2400000
+#define AD4691_VREF_uV_MAX 5250000
+#define AD4691_VREF_2P5_uV_MAX 2750000
+#define AD4691_VREF_3P0_uV_MAX 3250000
+#define AD4691_VREF_3P3_uV_MAX 3750000
+#define AD4691_VREF_4P096_uV_MAX 4500000
+
+#define AD4691_SPI_CONFIG_A_REG 0x000
+#define AD4691_SW_RESET (BIT(7) | BIT(0))
+
+#define AD4691_STATUS_REG 0x014
+#define AD4691_CLAMP_STATUS1_REG 0x01A
+#define AD4691_CLAMP_STATUS2_REG 0x01B
+#define AD4691_DEVICE_SETUP 0x020
+#define AD4691_LDO_EN BIT(4)
+#define AD4691_REF_CTRL 0x021
+#define AD4691_REF_CTRL_MASK GENMASK(4, 2)
+#define AD4691_REFBUF_EN BIT(0)
+#define AD4691_OSC_FREQ_REG 0x023
+#define AD4691_OSC_FREQ_MASK GENMASK(3, 0)
+#define AD4691_STD_SEQ_CONFIG 0x025
+#define AD4691_SPARE_CONTROL 0x02A
+
+#define AD4691_OSC_EN_REG 0x180
+#define AD4691_STATE_RESET_REG 0x181
+#define AD4691_STATE_RESET_ALL 0x01
+#define AD4691_ADC_SETUP 0x182
+#define AD4691_ADC_MODE_MASK GENMASK(1, 0)
+#define AD4691_AUTONOMOUS_MODE 0x02
+/*
+ * ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
+ * 16-bit BE value to 0x185 auto-decrements to 0x184 for the second byte.
+ */
+#define AD4691_ACC_MASK_REG 0x185
+#define AD4691_ACC_DEPTH_IN(n) (0x186 + (n))
+#define AD4691_GPIO_MODE1_REG 0x196
+#define AD4691_GPIO_MODE2_REG 0x197
+#define AD4691_GPIO_READ 0x1A0
+#define AD4691_ACC_STATUS_FULL1_REG 0x1B0
+#define AD4691_ACC_STATUS_FULL2_REG 0x1B1
+#define AD4691_ACC_STATUS_OVERRUN1_REG 0x1B2
+#define AD4691_ACC_STATUS_OVERRUN2_REG 0x1B3
+#define AD4691_ACC_STATUS_SAT1_REG 0x1B4
+#define AD4691_ACC_STATUS_SAT2_REG 0x1BE
+#define AD4691_ACC_SAT_OVR_REG(n) (0x1C0 + (n))
+#define AD4691_AVG_IN(n) (0x201 + (2 * (n)))
+#define AD4691_AVG_STS_IN(n) (0x222 + (3 * (n)))
+#define AD4691_ACC_IN(n) (0x252 + (3 * (n)))
+#define AD4691_ACC_STS_DATA(n) (0x283 + (4 * (n)))
+
+static const char * const ad4691_supplies[] = { "avdd", "vio" };
+
+enum ad4691_ref_ctrl {
+ AD4691_VREF_2P5 = 0,
+ AD4691_VREF_3P0 = 1,
+ AD4691_VREF_3P3 = 2,
+ AD4691_VREF_4P096 = 3,
+ AD4691_VREF_5P0 = 4,
+};
+
+struct ad4691_chip_info {
+ const struct iio_chan_spec *channels;
+ const char *name;
+ unsigned int num_channels;
+ unsigned int max_rate;
+};
+
+#define AD4691_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
+ | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
+static const struct iio_chan_spec ad4691_channels[] = {
+ AD4691_CHANNEL(0),
+ AD4691_CHANNEL(1),
+ AD4691_CHANNEL(2),
+ AD4691_CHANNEL(3),
+ AD4691_CHANNEL(4),
+ AD4691_CHANNEL(5),
+ AD4691_CHANNEL(6),
+ AD4691_CHANNEL(7),
+ AD4691_CHANNEL(8),
+ AD4691_CHANNEL(9),
+ AD4691_CHANNEL(10),
+ AD4691_CHANNEL(11),
+ AD4691_CHANNEL(12),
+ AD4691_CHANNEL(13),
+ AD4691_CHANNEL(14),
+ AD4691_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_channels[] = {
+ AD4691_CHANNEL(0),
+ AD4691_CHANNEL(1),
+ AD4691_CHANNEL(2),
+ AD4691_CHANNEL(3),
+ AD4691_CHANNEL(4),
+ AD4691_CHANNEL(5),
+ AD4691_CHANNEL(6),
+ AD4691_CHANNEL(7),
+};
+
+/*
+ * Internal oscillator frequency table. Index is the OSC_FREQ_REG[3:0] value.
+ * Index 0 (1 MHz) is only valid for AD4692/AD4694; AD4691/AD4693 support
+ * up to 500 kHz and use index 1 as their highest valid rate.
+ */
+static const int ad4691_osc_freqs_Hz[] = {
+ [0x0] = 1000000,
+ [0x1] = 500000,
+ [0x2] = 400000,
+ [0x3] = 250000,
+ [0x4] = 200000,
+ [0x5] = 167000,
+ [0x6] = 133000,
+ [0x7] = 125000,
+ [0x8] = 100000,
+ [0x9] = 50000,
+ [0xA] = 25000,
+ [0xB] = 12500,
+ [0xC] = 10000,
+ [0xD] = 5000,
+ [0xE] = 2500,
+ [0xF] = 1250,
+};
+
+static const struct ad4691_chip_info ad4691_chip_info = {
+ .channels = ad4691_channels,
+ .name = "ad4691",
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+ .max_rate = 500 * HZ_PER_KHZ,
+};
+
+static const struct ad4691_chip_info ad4692_chip_info = {
+ .channels = ad4691_channels,
+ .name = "ad4692",
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+ .max_rate = 1 * HZ_PER_MHZ,
+};
+
+static const struct ad4691_chip_info ad4693_chip_info = {
+ .channels = ad4693_channels,
+ .name = "ad4693",
+ .num_channels = ARRAY_SIZE(ad4693_channels),
+ .max_rate = 500 * HZ_PER_KHZ,
+};
+
+static const struct ad4691_chip_info ad4694_chip_info = {
+ .channels = ad4693_channels,
+ .name = "ad4694",
+ .num_channels = ARRAY_SIZE(ad4693_channels),
+ .max_rate = 1 * HZ_PER_MHZ,
+};
+
+struct ad4691_state {
+ const struct ad4691_chip_info *info;
+ struct regmap *regmap;
+ int vref_uV;
+ bool refbuf_en;
+ bool ldo_en;
+ /*
+ * Synchronize access to members of the driver state, and ensure
+ * atomicity of consecutive SPI operations.
+ */
+ struct mutex lock;
+};
+
+static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct spi_device *spi = context;
+ u8 tx[2], rx[4];
+ int ret;
+
+ /* Set bit 15 to mark the operation as READ. */
+ put_unaligned_be16(0x8000 | reg, tx);
+
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_SAT_OVR_REG(15):
+ ret = spi_write_then_read(spi, tx, 2, rx, 1);
+ if (ret)
+ return ret;
+ *val = rx[0];
+ return 0;
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ ret = spi_write_then_read(spi, tx, 2, rx, 2);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be16(rx);
+ return 0;
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ ret = spi_write_then_read(spi, tx, 2, rx, 3);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be24(rx);
+ return 0;
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ ret = spi_write_then_read(spi, tx, 2, rx, 4);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be32(rx);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_reg_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct spi_device *spi = context;
+ u8 tx[4];
+
+ put_unaligned_be16(reg, tx);
+
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_MASK_REG - 1:
+ case AD4691_ACC_MASK_REG + 1 ... AD4691_GPIO_MODE2_REG:
+ if (val > 0xFF)
+ return -EINVAL;
+ tx[2] = val;
+ return spi_write_then_read(spi, tx, 3, NULL, 0);
+ case AD4691_ACC_MASK_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ if (val > 0xFFFF)
+ return -EINVAL;
+ put_unaligned_be16(val, &tx[2]);
+ return spi_write_then_read(spi, tx, 4, NULL, 0);
+ default:
+ return -EINVAL;
+ }
+}
+
+static bool ad4691_volatile_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case AD4691_STATUS_REG:
+ case AD4691_CLAMP_STATUS1_REG:
+ case AD4691_CLAMP_STATUS2_REG:
+ case AD4691_GPIO_READ:
+ case AD4691_ACC_STATUS_FULL1_REG ... AD4691_ACC_STATUS_SAT2_REG:
+ case AD4691_ACC_SAT_OVR_REG(0) ... AD4691_ACC_SAT_OVR_REG(15):
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool ad4691_readable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_SAT_OVR_REG(15):
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool ad4691_writeable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_SPARE_CONTROL ... AD4691_GPIO_MODE2_REG:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct regmap_config ad4691_regmap_config = {
+ .reg_bits = 16,
+ .val_bits = 32,
+ .reg_read = ad4691_reg_read,
+ .reg_write = ad4691_reg_write,
+ .volatile_reg = ad4691_volatile_reg,
+ .readable_reg = ad4691_readable_reg,
+ .writeable_reg = ad4691_writeable_reg,
+ .max_register = AD4691_ACC_STS_DATA(15),
+ .cache_type = REGCACHE_MAPLE,
+};
+
+static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
+{
+ unsigned int reg_val;
+ int ret;
+
+ ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ if (ret)
+ return ret;
+
+ *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)];
+ return IIO_VAL_INT;
+}
+
+static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int start = (st->info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] != freq)
+ continue;
+ return regmap_update_bits(st->regmap, AD4691_OSC_FREQ_REG,
+ AD4691_OSC_FREQ_MASK, i);
+ }
+
+ return -EINVAL;
+}
+
+static int ad4691_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type,
+ int *length, long mask)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int start = (st->info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *vals = &ad4691_osc_freqs_Hz[start];
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
+ return IIO_AVAIL_LIST;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_single_shot_read(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int reg_val, osc_idx, period_us;
+ int ret;
+
+ guard(mutex)(&st->lock);
+
+ /* Use AUTONOMOUS mode for single-shot reads. */
+ ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG,
+ AD4691_STATE_RESET_ALL);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ BIT(chan->channel));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
+ ~BIT(chan->channel) & GENMASK(15, 0));
+ if (ret)
+ return ret;
+
+ ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 1);
+ if (ret)
+ return ret;
+
+ osc_idx = FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val);
+ /* Wait 2 oscillator periods for the conversion to complete. */
+ period_us = DIV_ROUND_UP(2UL * USEC_PER_SEC, ad4691_osc_freqs_Hz[osc_idx]);
+ fsleep(period_us);
+
+ ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 0);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(st->regmap, AD4691_AVG_IN(chan->channel), ®_val);
+ if (ret)
+ return ret;
+
+ *val = reg_val;
+
+ ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG, AD4691_STATE_RESET_ALL);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT;
+}
+
+static int ad4691_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2, long info)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ switch (info) {
+ case IIO_CHAN_INFO_RAW: {
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ return ad4691_single_shot_read(indio_dev, chan, val);
+ }
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_get_sampling_freq(st, val);
+ case IIO_CHAN_INFO_SCALE:
+ *val = st->vref_uV / (MICRO / MILLI);
+ *val2 = chan->scan_type.realbits;
+ return IIO_VAL_FRACTIONAL_LOG2;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_set_sampling_freq(indio_dev, val);
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
+ unsigned int writeval, unsigned int *readval)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ guard(mutex)(&st->lock);
+
+ if (readval)
+ return regmap_read(st->regmap, reg, readval);
+
+ return regmap_write(st->regmap, reg, writeval);
+}
+
+static const struct iio_info ad4691_info = {
+ .read_raw = &ad4691_read_raw,
+ .write_raw = &ad4691_write_raw,
+ .read_avail = &ad4691_read_avail,
+ .debugfs_reg_access = &ad4691_reg_access,
+};
+
+static int ad4691_regulator_setup(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ int ret;
+
+ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(ad4691_supplies),
+ ad4691_supplies);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to get and enable supplies\n");
+
+ ret = devm_regulator_get_enable(dev, "ldo-in");
+ if (ret == -ENODEV)
+ st->ldo_en = true;
+ else if (ret)
+ return dev_err_probe(dev, ret, "Failed to get and enable LDO-IN\n");
+
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "ref");
+ if (st->vref_uV == -ENODEV) {
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "refin");
+ st->refbuf_en = true;
+ }
+ if (st->vref_uV < 0)
+ return dev_err_probe(dev, st->vref_uV,
+ "Failed to get reference supply\n");
+
+ if (st->vref_uV < AD4691_VREF_uV_MIN || st->vref_uV > AD4691_VREF_uV_MAX)
+ return dev_err_probe(dev, -EINVAL,
+ "vref(%d) must be in the range [%u...%u]\n",
+ st->vref_uV, AD4691_VREF_uV_MIN,
+ AD4691_VREF_uV_MAX);
+
+ return 0;
+}
+
+static int ad4691_reset(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct reset_control *rst;
+
+ rst = devm_reset_control_get_optional_exclusive(dev, NULL);
+ if (IS_ERR(rst))
+ return dev_err_probe(dev, PTR_ERR(rst), "Failed to get reset\n");
+
+ if (rst) {
+ /*
+ * The GPIO is already asserted by reset_gpio_probe().
+ * Wait for the reset pulse width required by the chip.
+ * See datasheet Table 5.
+ */
+ fsleep(300);
+ return reset_control_deassert(rst);
+ }
+
+ /* No hardware reset available, fall back to software reset. */
+ return regmap_write(st->regmap, AD4691_SPI_CONFIG_A_REG,
+ AD4691_SW_RESET);
+}
+
+static int ad4691_config(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ enum ad4691_ref_ctrl ref_val;
+ unsigned int val;
+ int ret;
+
+ switch (st->vref_uV) {
+ case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
+ ref_val = AD4691_VREF_2P5;
+ break;
+ case AD4691_VREF_2P5_uV_MAX + 1 ... AD4691_VREF_3P0_uV_MAX:
+ ref_val = AD4691_VREF_3P0;
+ break;
+ case AD4691_VREF_3P0_uV_MAX + 1 ... AD4691_VREF_3P3_uV_MAX:
+ ref_val = AD4691_VREF_3P3;
+ break;
+ case AD4691_VREF_3P3_uV_MAX + 1 ... AD4691_VREF_4P096_uV_MAX:
+ ref_val = AD4691_VREF_4P096;
+ break;
+ case AD4691_VREF_4P096_uV_MAX + 1 ... AD4691_VREF_uV_MAX:
+ ref_val = AD4691_VREF_5P0;
+ break;
+ default:
+ return dev_err_probe(dev, -EINVAL,
+ "Unsupported vref voltage: %d uV\n",
+ st->vref_uV);
+ }
+
+ val = FIELD_PREP(AD4691_REF_CTRL_MASK, ref_val);
+ if (st->refbuf_en)
+ val |= AD4691_REFBUF_EN;
+
+ ret = regmap_update_bits(st->regmap, AD4691_REF_CTRL,
+ AD4691_REF_CTRL_MASK | AD4691_REFBUF_EN, val);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write REF_CTRL\n");
+
+ ret = regmap_assign_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_LDO_EN, st->ldo_en);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write DEVICE_SETUP\n");
+
+ /*
+ * Set the internal oscillator to the highest rate this chip supports.
+ * Index 0 (1 MHz) exceeds the 500 kHz max of AD4691/AD4693, so those
+ * chips start at index 1 (500 kHz).
+ */
+ ret = regmap_assign_bits(st->regmap, AD4691_OSC_FREQ_REG,
+ AD4691_OSC_FREQ_MASK,
+ (st->info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write OSC_FREQ\n");
+
+ ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write ADC_SETUP\n");
+
+ return 0;
+}
+
+static int ad4691_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct iio_dev *indio_dev;
+ struct ad4691_state *st;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*st));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ st = iio_priv(indio_dev);
+ st->info = spi_get_device_match_data(spi);
+
+ ret = devm_mutex_init(dev, &st->lock);
+ if (ret)
+ return ret;
+
+ st->regmap = devm_regmap_init(dev, NULL, spi, &ad4691_regmap_config);
+ if (IS_ERR(st->regmap))
+ return dev_err_probe(dev, PTR_ERR(st->regmap),
+ "Failed to initialize regmap\n");
+
+ ret = ad4691_regulator_setup(st);
+ if (ret)
+ return ret;
+
+ ret = ad4691_reset(st);
+ if (ret)
+ return ret;
+
+ ret = ad4691_config(st);
+ if (ret)
+ return ret;
+
+ indio_dev->name = st->info->name;
+ indio_dev->info = &ad4691_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ indio_dev->channels = st->info->channels;
+ indio_dev->num_channels = st->info->num_channels;
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct of_device_id ad4691_of_match[] = {
+ { .compatible = "adi,ad4691", .data = &ad4691_chip_info },
+ { .compatible = "adi,ad4692", .data = &ad4692_chip_info },
+ { .compatible = "adi,ad4693", .data = &ad4693_chip_info },
+ { .compatible = "adi,ad4694", .data = &ad4694_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ad4691_of_match);
+
+static const struct spi_device_id ad4691_id[] = {
+ { "ad4691", (kernel_ulong_t)&ad4691_chip_info },
+ { "ad4692", (kernel_ulong_t)&ad4692_chip_info },
+ { "ad4693", (kernel_ulong_t)&ad4693_chip_info },
+ { "ad4694", (kernel_ulong_t)&ad4694_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ad4691_id);
+
+static struct spi_driver ad4691_driver = {
+ .driver = {
+ .name = "ad4691",
+ .of_match_table = ad4691_of_match,
+ },
+ .probe = ad4691_probe,
+ .id_table = ad4691_id,
+};
+module_spi_driver(ad4691_driver);
+
+MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
+MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox