[PATCH 07/10] iio: frequency: ad9910: support drctl control

From: Rodrigo Alencar via B4 Relay

Date: Thu Oct 15 2026 - 14:21:32 EST


From: Rodrigo Alencar <rodrigo.alencar@xxxxxxxxxx>

The DRCTL pin controls the slope direction of the AD9910 digital ramp
generator (DRG). When dwell is enabled at a ramp limit, the DRG holds
that limit until the next DRCTL transition, so the sweep pattern is
defined by the waveform applied to that pin, which can be generated by
a PWM.

Get the optional "drctl" PWM and expose the integration_time attribute
in the DRG ramp up and ramp down channels, which sets the time interval
assigned to each ramp direction while the sweep parameter is accumulating.

The PWM is disabled when the resulting period is zero. The applied state
is read back, so that integration_time reports the intervals achieved
by the PWM provider. Writing to integration_time fails with -EOPNOTSUPP
when no DRCTL PWM is available.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@xxxxxxxxxx>
---
drivers/iio/frequency/ad9910.c | 132 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 126 insertions(+), 6 deletions(-)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index dc13986dff3d..3d517879dee3 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -21,6 +21,7 @@
#include <linux/math64.h>
#include <linux/module.h>
#include <linux/property.h>
+#include <linux/pwm.h>
#include <linux/regulator/consumer.h>
#include <linux/reset.h>
#include <linux/seq_file.h>
@@ -324,12 +325,16 @@ union ad9910_reg {
struct ad9910_state {
struct spi_device *spi;
struct clk *refclk;
+ struct pwm_device *drctl_pwm;
struct fw_upload *ram_fwu;

struct gpio_desc *gpio_pwdown;
struct gpio_desc *gpio_update;
struct gpio_descs *gpio_profile;

+ s64 ramp_up_time_ns;
+ s64 ramp_down_time_ns;
+
/* cached registers */
union ad9910_reg reg[AD9910_REG_NUM_CACHED];

@@ -609,6 +614,66 @@ static int ad9910_sw_powerdown_set(struct ad9910_state *st, bool enable)
AD9910_CFR1_SW1_POWER_DOWN_MSK, 0, true);
}

+static int ad9910_drctl_sync(struct ad9910_state *st)
+{
+ struct pwm_state state;
+ bool ndh, ndl;
+ int ret;
+
+ if (!st->drctl_pwm)
+ return 0;
+
+ ndh = FIELD_GET(AD9910_CFR2_DRG_NO_DWELL_HIGH_MSK,
+ st->reg[AD9910_REG_CFR2].val32);
+ ndl = FIELD_GET(AD9910_CFR2_DRG_NO_DWELL_LOW_MSK,
+ st->reg[AD9910_REG_CFR2].val32);
+
+ pwm_init_state(st->drctl_pwm, &state);
+
+ if (!ndh && ndl) {
+ state.duty_cycle = st->ramp_down_time_ns / 2;
+ state.period = st->ramp_down_time_ns;
+ state.polarity = PWM_POLARITY_INVERSED;
+ } else if (ndh && !ndl) {
+ state.duty_cycle = st->ramp_up_time_ns / 2;
+ state.period = st->ramp_up_time_ns;
+ state.polarity = PWM_POLARITY_NORMAL;
+ } else if (!ndh && !ndl) {
+ state.duty_cycle = st->ramp_up_time_ns;
+ state.period = st->ramp_up_time_ns + st->ramp_down_time_ns;
+ state.polarity = PWM_POLARITY_NORMAL;
+ } else {
+ state.duty_cycle = 0;
+ state.period = st->ramp_up_time_ns + st->ramp_down_time_ns;
+ state.polarity = PWM_POLARITY_NORMAL;
+ }
+
+ if (!state.period) {
+ pwm_disable(st->drctl_pwm);
+ return 0;
+ }
+
+ state.enabled = true;
+ ret = pwm_apply_might_sleep(st->drctl_pwm, &state);
+ if (ret)
+ return ret;
+
+ ret = pwm_get_state_hw(st->drctl_pwm, &state);
+ if (ret)
+ return ret;
+
+ if (!ndh && ndl) {
+ st->ramp_down_time_ns = state.period;
+ } else if (ndh && !ndl) {
+ st->ramp_up_time_ns = state.period;
+ } else if (!ndh && !ndl) {
+ st->ramp_up_time_ns = state.duty_cycle;
+ st->ramp_down_time_ns = state.period - st->ramp_up_time_ns;
+ }
+
+ return 0;
+}
+
static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
uintptr_t private,
const struct iio_chan_spec *chan,
@@ -675,6 +740,13 @@ static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
if (ret)
return ret;
}
+
+ /*
+ * DRCTL waveform depends on the dwell configuration, but the
+ * previous integration time values may not suit the new
+ * waveform, which should not prevent dwell_en from changing.
+ */
+ ad9910_drctl_sync(st);
break;
default:
return -EINVAL;
@@ -1054,7 +1126,8 @@ static const struct iio_chan_spec ad9910_channels[] = {
.channel = AD9910_CHANNEL_DRG_RAMP_UP,
.address = AD9910_CHAN_IDX_DRG_FREQ_RAMP_UP,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
- BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_INT_TIME),
.ext_info = ad9910_drg_ramp_ext_info,
.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_FREQ],
},
@@ -1065,7 +1138,8 @@ static const struct iio_chan_spec ad9910_channels[] = {
.channel = AD9910_CHANNEL_DRG_RAMP_DOWN,
.address = AD9910_CHAN_IDX_DRG_FREQ_RAMP_DOWN,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
- BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_INT_TIME),
.ext_info = ad9910_drg_ramp_ext_info,
.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_FREQ],
},
@@ -1076,7 +1150,8 @@ static const struct iio_chan_spec ad9910_channels[] = {
.channel = AD9910_CHANNEL_DRG_RAMP_UP,
.address = AD9910_CHAN_IDX_DRG_PHASE_RAMP_UP,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
- BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_INT_TIME),
.ext_info = ad9910_drg_ramp_ext_info,
.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_PHASE],
},
@@ -1087,7 +1162,8 @@ static const struct iio_chan_spec ad9910_channels[] = {
.channel = AD9910_CHANNEL_DRG_RAMP_DOWN,
.address = AD9910_CHAN_IDX_DRG_PHASE_RAMP_DOWN,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
- BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_INT_TIME),
.ext_info = ad9910_drg_ramp_ext_info,
.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_PHASE],
},
@@ -1098,7 +1174,8 @@ static const struct iio_chan_spec ad9910_channels[] = {
.channel = AD9910_CHANNEL_DRG_RAMP_UP,
.address = AD9910_CHAN_IDX_DRG_AMP_RAMP_UP,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
- BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_INT_TIME),
.ext_info = ad9910_drg_ramp_ext_info,
.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_AMP],
},
@@ -1109,7 +1186,8 @@ static const struct iio_chan_spec ad9910_channels[] = {
.channel = AD9910_CHANNEL_DRG_RAMP_DOWN,
.address = AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
- BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) |
+ BIT(IIO_CHAN_INFO_INT_TIME),
.ext_info = ad9910_drg_ramp_ext_info,
.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_AMP],
},
@@ -1337,6 +1415,17 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
default:
return -EINVAL;
}
+ case IIO_CHAN_INFO_INT_TIME:
+ switch (chan->channel) {
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ iio_val_s64_decompose(st->ramp_up_time_ns, val, val2);
+ return IIO_VAL_DECIMAL64_NANO;
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ iio_val_s64_decompose(st->ramp_down_time_ns, val, val2);
+ return IIO_VAL_DECIMAL64_NANO;
+ default:
+ return -EINVAL;
+ }
default:
return -EINVAL;
}
@@ -1637,6 +1726,26 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
return -EINVAL;
}
}
+ case IIO_CHAN_INFO_INT_TIME: {
+ s64 val64 = iio_val_s64_compose(val, val2);
+
+ if (!st->drctl_pwm)
+ return -EOPNOTSUPP;
+ if (val64 < 0)
+ return -EINVAL;
+
+ switch (chan->channel) {
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ st->ramp_up_time_ns = val64;
+ break;
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ st->ramp_down_time_ns = val64;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return ad9910_drctl_sync(st);
+ }
default:
return -EINVAL;
}
@@ -1671,6 +1780,8 @@ static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
return IIO_VAL_INT_PLUS_NANO;
case IIO_CHAN_INFO_OFFSET:
return IIO_VAL_DECIMAL64_MICRO;
+ case IIO_CHAN_INFO_INT_TIME:
+ return IIO_VAL_DECIMAL64_NANO;
default:
return -EINVAL;
}
@@ -2249,6 +2360,15 @@ static int ad9910_probe(struct spi_device *spi)

fsleep(AD9910_WAKEUP_DELAY_us);

+ st->drctl_pwm = devm_pwm_get(dev, "drctl");
+ if (IS_ERR(st->drctl_pwm)) {
+ ret = PTR_ERR(st->drctl_pwm);
+ if (ret != -ENOENT && ret != -ENODEV)
+ return dev_err_probe(dev, PTR_ERR(st->drctl_pwm),
+ "failed to get drctl pwm\n");
+ st->drctl_pwm = NULL;
+ }
+
ret = ad9910_parse_fw(st);
if (ret)
return ret;

--
2.43.0