[PATCH v9 3/6] iio: adc: ltc2378: Enable high-speed data capture
Marcelo Schmitt <[email protected]>
| Newsgroups | org.kernel.vger.linux-iio,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <e4eabdd9833e377c3fa1afd1ac089c5b37bb7928.1785186980.git.marcelo.schmitt@analog.com> |
Make use of SPI transfer offloading to speed up data capture, enabling data acquisition at faster sample rates (up to 2 MSPS). Reviewed-by: Andy Shevchenko <[email protected]> Reviewed-by: David Lechner <[email protected]> Signed-off-by: Marcelo Schmitt <[email protected]> --- Change log v8 -> v9: No changes in v9. drivers/iio/adc/Kconfig | 6 + drivers/iio/adc/ltc2378.c | 404 +++++++++++++++++++++++++++++++++++++- 2 files changed, 407 insertions(+), 3 deletions(-) diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig index 220c6739a8f2..96df8453b826 100644 --- a/drivers/iio/adc/Kconfig +++ b/drivers/iio/adc/Kconfig @@ -948,6 +948,12 @@ config LTC2378 depends on SPI depends on REGULATOR || COMPILE_TEST depends on GPIOLIB + depends on PWM + select IIO_BUFFER + select IIO_BUFFER_DMA + select IIO_BUFFER_DMAENGINE + select SPI_OFFLOAD + select SPI_OFFLOAD_TRIGGER_PWM help Say yes here to build support for Analog Devices LTC2378-20 and similar analog to digital converters. diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c index d161435fc3a3..48380a2d7dc6 100644 --- a/drivers/iio/adc/ltc2378.c +++ b/drivers/iio/adc/ltc2378.c @@ -16,16 +16,27 @@ #include <linux/device-id/of.h> #include <linux/err.h> #include <linux/gpio/consumer.h> +#include <linux/math64.h> +#include <linux/minmax.h> #include <linux/module.h> #include <linux/mutex.h> #include <linux/regulator/consumer.h> +#include <linux/pwm.h> #include <linux/spi/spi.h> +#include <linux/spi/offload/consumer.h> +#include <linux/spi/offload/types.h> +#include <linux/time64.h> #include <linux/types.h> #include <linux/units.h> +#include <linux/iio/buffer.h> +#include <linux/iio/buffer-dmaengine.h> #include <linux/iio/iio.h> #include <linux/iio/types.h> +#define LTC2378_TDSDOBUSYL_NS 5 +#define LTC2378_TBUSYLH_NS 13 +#define LTC2378_TCNV_HIGH_NS 20 #define LTC2378_MAX_DATA_WAIT_US 4 /* max(TBUSYLH + TCONV + TDSDOBUSYL) */ #define LTC2378_CHANNEL(_sign, _real_bits, _storage_bits) \ @@ -54,9 +65,49 @@ #define LTC2378_PSEUDO_DIFF_CHANNEL(_real_bits) \ LTC2378_CHANNEL(0, _real_bits, (((_real_bits) > 16) ? 32 : 16)) +#define LTC2378_OFFLOAD_CHANNEL(_sign, _real_bits, _storage_bits) \ +{ \ + .type = IIO_VOLTAGE, \ + .indexed = 1, \ + .differential = _sign, \ + .channel = 0, \ + .channel2 = _sign ? 1 : 0, \ + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \ + BIT(IIO_CHAN_INFO_SCALE) | \ + BIT(IIO_CHAN_INFO_SAMP_FREQ), \ + .info_mask_separate_available = BIT(IIO_CHAN_INFO_SAMP_FREQ), \ + .scan_index = 0, \ + .scan_type = { \ + .format = _sign ? IIO_SCAN_FORMAT_SIGNED_INT : \ + IIO_SCAN_FORMAT_UNSIGNED_INT, \ + .realbits = _real_bits, \ + .storagebits = _storage_bits, \ + .shift = 0, \ + .endianness = IIO_CPU, \ + }, \ +} + +/* + * Currently, the available offload hardware + DMA configuration only supports + * pushing 32-bit data elements to DMA IIO buffers in CPU endianness. For 16-bit + * precision parts, those 32-bit elements (in CPU endianness) contain 2 bytes + * with data and 2 bytes always zeroed out. Nevertheless, for the offload use + * case, the IIO buffer is configured for 32 storage bits in CPU endianness so + * data is correctly aligned in user space despite 2 out of the 4 bytes being + * zeros. + */ +#define LTC2378_OFFLOAD_DIFF_CHANNEL(_real_bits) \ + LTC2378_OFFLOAD_CHANNEL(1, (_real_bits), 32) + +#define LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(_real_bits) \ + LTC2378_OFFLOAD_CHANNEL(0, (_real_bits), 32) + struct ltc2378_chip_info { const char *name; struct iio_chan_spec chan; + struct iio_chan_spec offload_chan; + unsigned int max_sample_rate_Hz; + unsigned int tconv_ns; }; struct ltc2378_state { @@ -66,6 +117,15 @@ struct ltc2378_state { struct mutex lock; /* Protect data acquisition cycle */ int ref_uV; struct spi_transfer xfer; + struct spi_transfer offload_xfer; + struct spi_offload *offload; + struct spi_offload_trigger *offload_trigger; + struct pwm_waveform cnv_wf; + struct spi_message offload_msg; + struct spi_offload_trigger_config offload_trigger_config; + struct pwm_device *cnv_trigger; + unsigned int cnv_Hz; + unsigned int sample_freq_range[3]; /* * DMA (thus cache coherency maintenance) requires the transfer buffers @@ -85,96 +145,153 @@ struct ltc2378_state { static const struct ltc2378_chip_info ltc2364_16_chip_info = { .name = "ltc2364-16", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 250 * HZ_PER_KHZ, + .tconv_ns = 3000, }; static const struct ltc2378_chip_info ltc2364_18_chip_info = { .name = "ltc2364-18", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 250 * HZ_PER_KHZ, + .tconv_ns = 3000, }; static const struct ltc2378_chip_info ltc2367_16_chip_info = { .name = "ltc2367-16", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 500 * HZ_PER_KHZ, + .tconv_ns = 1500, }; static const struct ltc2378_chip_info ltc2367_18_chip_info = { .name = "ltc2367-18", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 500 * HZ_PER_KHZ, + .tconv_ns = 1500, }; static const struct ltc2378_chip_info ltc2368_16_chip_info = { .name = "ltc2368-16", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 1 * HZ_PER_MHZ, + .tconv_ns = 527, }; static const struct ltc2378_chip_info ltc2368_18_chip_info = { .name = "ltc2368-18", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 1 * HZ_PER_MHZ, + .tconv_ns = 527, }; static const struct ltc2378_chip_info ltc2369_18_chip_info = { .name = "ltc2369-18", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 1600 * HZ_PER_KHZ, + .tconv_ns = 412, }; static const struct ltc2378_chip_info ltc2370_16_chip_info = { .name = "ltc2370-16", .chan = LTC2378_PSEUDO_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_PSEUDO_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 2 * HZ_PER_MHZ, + .tconv_ns = 322, }; static const struct ltc2378_chip_info ltc2376_16_chip_info = { .name = "ltc2376-16", .chan = LTC2378_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 250 * HZ_PER_KHZ, + .tconv_ns = 3000, }; static const struct ltc2378_chip_info ltc2376_18_chip_info = { .name = "ltc2376-18", .chan = LTC2378_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 250 * HZ_PER_KHZ, + .tconv_ns = 3000, }; static const struct ltc2378_chip_info ltc2376_20_chip_info = { .name = "ltc2376-20", .chan = LTC2378_DIFF_CHANNEL(20), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(20), + .max_sample_rate_Hz = 250 * HZ_PER_KHZ, + .tconv_ns = 3000, }; static const struct ltc2378_chip_info ltc2377_16_chip_info = { .name = "ltc2377-16", .chan = LTC2378_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 500 * HZ_PER_KHZ, + .tconv_ns = 1500, }; static const struct ltc2378_chip_info ltc2377_18_chip_info = { .name = "ltc2377-18", .chan = LTC2378_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 500 * HZ_PER_KHZ, + .tconv_ns = 1500, }; static const struct ltc2378_chip_info ltc2377_20_chip_info = { .name = "ltc2377-20", .chan = LTC2378_DIFF_CHANNEL(20), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(20), + .max_sample_rate_Hz = 500 * HZ_PER_KHZ, + .tconv_ns = 1500, }; static const struct ltc2378_chip_info ltc2378_16_chip_info = { .name = "ltc2378-16", .chan = LTC2378_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 1 * HZ_PER_MHZ, + .tconv_ns = 527, }; static const struct ltc2378_chip_info ltc2378_18_chip_info = { .name = "ltc2378-18", .chan = LTC2378_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 1 * HZ_PER_MHZ, + .tconv_ns = 527, }; static const struct ltc2378_chip_info ltc2378_20_chip_info = { .name = "ltc2378-20", .chan = LTC2378_DIFF_CHANNEL(20), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(20), + .max_sample_rate_Hz = 1 * HZ_PER_MHZ, + .tconv_ns = 675, }; static const struct ltc2378_chip_info ltc2379_18_chip_info = { .name = "ltc2379-18", .chan = LTC2378_DIFF_CHANNEL(18), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(18), + .max_sample_rate_Hz = 1600 * HZ_PER_KHZ, + .tconv_ns = 412, }; static const struct ltc2378_chip_info ltc2380_16_chip_info = { .name = "ltc2380-16", .chan = LTC2378_DIFF_CHANNEL(16), + .offload_chan = LTC2378_OFFLOAD_DIFF_CHANNEL(16), + .max_sample_rate_Hz = 2 * HZ_PER_MHZ, + .tconv_ns = 322, }; static int ltc2378_convert_and_acquire(struct ltc2378_state *st) @@ -257,7 +374,137 @@ static int ltc2378_read_raw(struct iio_dev *indio_dev, *val2 = chan->scan_type.realbits; return IIO_VAL_FRACTIONAL_LOG2; + case IIO_CHAN_INFO_SAMP_FREQ: + *val = st->cnv_Hz; + return IIO_VAL_INT; + default: + return -EINVAL; + } +} +static int ltc2378_read_avail(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + const int **vals, int *type, int *length, long mask) +{ + struct ltc2378_state *st = iio_priv(indio_dev); + + switch (mask) { + case IIO_CHAN_INFO_SAMP_FREQ: + *vals = st->sample_freq_range; + *type = IIO_VAL_INT; + return IIO_AVAIL_RANGE; + default: + return -EINVAL; + } +} + +/* + * SPI offload wiring schema + * + * +-------------+ +-------------+ + * | CNV |<-----+--| GPIO | + * | | +--| PWM0 | + * | | | | + * | | +--| PWM1 | + * | | | +-------------+ + * | | +->| TRIGGER | + * | | | | + * | ADC | | SPI | + * | | | controller | + * | | | | + * | SDI |<--------| SDO | + * | SDO |-------->| SDI | + * | SCLK |<--------| SCLK | + * +-------------+ +-------------+ + * + */ +static int ltc2378_update_conversion_rate(struct ltc2378_state *st, int freq_Hz) +{ + struct spi_offload_trigger_config config = st->offload_trigger_config; + unsigned int min_read_offset, offload_period_ns; + struct pwm_waveform cnv_wf = { }; + u64 target = LTC2378_TCNV_HIGH_NS; + unsigned int count; + u64 offload_offset_ns; + int ret; + + if (freq_Hz == 0) + return -EINVAL; + + if (!in_range(freq_Hz, 1, st->info->max_sample_rate_Hz)) + return -ERANGE; + + /* Configure CNV PWM waveform */ + cnv_wf.period_length_ns = DIV_ROUND_CLOSEST(NSEC_PER_SEC, freq_Hz); + + /* + * Ensure CNV high time meets minimum requirement (20ns). The PWM + * hardware may round the duty cycle, so iterate until we get at least + * the minimum required high time (or reach a try count limit). + */ + count = 100; + do { + cnv_wf.duty_length_ns = target; + ret = pwm_round_waveform_might_sleep(st->cnv_trigger, &cnv_wf); + if (ret) + return ret; + target += 10; /* Increment by PWM duty cycle period */ + } while (count-- && cnv_wf.duty_length_ns < LTC2378_TCNV_HIGH_NS); + + /* Check the minimum CNV high time is met */ + if (cnv_wf.duty_length_ns < LTC2378_TCNV_HIGH_NS) + return -EDOM; + + /* + * Configure SPI offload PWM trigger. + * The trigger should fire after tBUSYLH + tCONV + tDSDOBUSYL. + * Minimum time needed: TBUSYLH (13ns) + TCONV (part-specific) + TDSDOBUSYL (5ns) + * + * Use the same period as CNV PWM to avoid timing issues. + * Convert back from period to frequency for the SPI offload API. + */ + offload_period_ns = cnv_wf.period_length_ns; + config.periodic.frequency_hz = div_u64(HZ_PER_GHZ, offload_period_ns); + min_read_offset = LTC2378_TBUSYLH_NS + st->info->tconv_ns + LTC2378_TDSDOBUSYL_NS; + offload_offset_ns = min_read_offset; + count = 100; + do { + config.periodic.offset_ns = offload_offset_ns; + ret = spi_offload_trigger_validate(st->offload_trigger, &config); + if (ret) + return ret; + offload_offset_ns += 10; + } while (count-- && config.periodic.offset_ns < min_read_offset); + + /* Check the minimum CNV to SCLK delay is met */ + if (config.periodic.offset_ns < min_read_offset) + return -EDOM; + + /* Check the PWM periods remain the same */ + offload_period_ns = div64_u64(HZ_PER_GHZ, config.periodic.frequency_hz); + if (cnv_wf.period_length_ns != offload_period_ns) + return -EDOM; + + st->offload_trigger_config = config; + st->cnv_wf = cnv_wf; + st->cnv_Hz = DIV_ROUND_CLOSEST_ULL(HZ_PER_GHZ, cnv_wf.period_length_ns); + + return 0; +} + +static int ltc2378_write_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int val, int val2, long mask) +{ + struct ltc2378_state *st = iio_priv(indio_dev); + + IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim); + if (IIO_DEV_ACQUIRE_FAILED(claim)) + return -EBUSY; + + switch (mask) { + case IIO_CHAN_INFO_SAMP_FREQ: + return ltc2378_update_conversion_rate(st, val); default: return -EINVAL; } @@ -267,6 +514,111 @@ static const struct iio_info ltc2378_iio_info = { .read_raw = <c2378_read_raw, }; +static const struct iio_info ltc2378_offload_iio_info = { + .read_raw = <c2378_read_raw, + .read_avail = <c2378_read_avail, + .write_raw = <c2378_write_raw, +}; + +static int ltc2378_offload_buffer_postenable(struct iio_dev *indio_dev) +{ + struct ltc2378_state *st = iio_priv(indio_dev); + int ret; + + ret = pwm_set_waveform_might_sleep(st->cnv_trigger, &st->cnv_wf, true); + if (ret) + return ret; + + ret = spi_offload_trigger_enable(st->offload, st->offload_trigger, + &st->offload_trigger_config); + if (ret) + goto out_pwm_disable; + + return 0; + +out_pwm_disable: + pwm_disable(st->cnv_trigger); + return ret; +} + +static int ltc2378_offload_buffer_predisable(struct iio_dev *indio_dev) +{ + struct ltc2378_state *st = iio_priv(indio_dev); + + spi_offload_trigger_disable(st->offload, st->offload_trigger); + pwm_disable(st->cnv_trigger); + + return 0; +} + +static const struct iio_buffer_setup_ops ltc2378_offload_buffer_ops = { + .postenable = <c2378_offload_buffer_postenable, + .predisable = <c2378_offload_buffer_predisable, +}; + +static int ltc2378_prepare_offload_message(struct device *dev, + struct ltc2378_state *st) +{ + unsigned int resolution = st->info->offload_chan.scan_type.realbits; + + st->offload_xfer.bits_per_word = resolution; + st->offload_xfer.len = spi_bpw_to_bytes(resolution); + st->offload_xfer.offload_flags = SPI_OFFLOAD_XFER_RX_STREAM; + + /* Initialize message with offload */ + spi_message_init_with_transfers(&st->offload_msg, &st->offload_xfer, 1); + st->offload_msg.offload = st->offload; + + return devm_spi_optimize_message(dev, st->spi, &st->offload_msg); +} + +static int ltc2378_spi_offload_setup(struct iio_dev *indio_dev, + struct ltc2378_state *st) +{ + struct device *dev = &st->spi->dev; + struct dma_chan *rx_dma; + + indio_dev->setup_ops = <c2378_offload_buffer_ops; + + st->offload_trigger = devm_spi_offload_trigger_get(dev, st->offload, + SPI_OFFLOAD_TRIGGER_PERIODIC); + if (IS_ERR(st->offload_trigger)) + return dev_err_probe(dev, PTR_ERR(st->offload_trigger), + "failed to get offload trigger\n"); + + st->offload_trigger_config.type = SPI_OFFLOAD_TRIGGER_PERIODIC; + + rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, st->offload); + if (IS_ERR(rx_dma)) + return dev_err_probe(dev, PTR_ERR(rx_dma), "failed to get offload RX DMA\n"); + + return devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma, + IIO_BUFFER_DIRECTION_IN); +} + +static int ltc2378_pwm_get(struct ltc2378_state *st) +{ + struct device *dev = &st->spi->dev; + + st->cnv_trigger = devm_pwm_get(dev, NULL); + if (IS_ERR(st->cnv_trigger)) + return dev_err_probe(dev, PTR_ERR(st->cnv_trigger), + "failed to get cnv pwm\n"); + + /* + * Disable the PWM connected to CNV in case it was left running by + * something else. + */ + pwm_disable(st->cnv_trigger); + + return 0; +} + +static const struct spi_offload_config ltc2378_offload_config = { + .capability_flags = SPI_OFFLOAD_CAP_TRIGGER | + SPI_OFFLOAD_CAP_RX_STREAM_DMA, +}; + static int ltc2378_ref_setup(struct device *dev, struct ltc2378_state *st) { int ret; @@ -307,7 +659,6 @@ static int ltc2378_probe(struct spi_device *spi) return ret; indio_dev->name = st->info->name; - indio_dev->info = <c2378_iio_info; indio_dev->modes = INDIO_DIRECT_MODE; st->cnv_gpio = devm_gpiod_get(dev, "cnv", GPIOD_OUT_LOW); @@ -315,8 +666,53 @@ static int ltc2378_probe(struct spi_device *spi) return dev_err_probe(dev, PTR_ERR(st->cnv_gpio), "failed to get CNV GPIO"); - indio_dev->channels = &st->info->chan; - indio_dev->num_channels = 1; + st->offload = devm_spi_offload_get(dev, spi, <c2378_offload_config); + ret = PTR_ERR_OR_ZERO(st->offload); + /* Fall back to low speed usage when no SPI offload is available. */ + if (ret == -ENODEV) { + indio_dev->info = <c2378_iio_info; + indio_dev->channels = &st->info->chan; + indio_dev->num_channels = 1; + } else if (ret) { + return dev_err_probe(dev, ret, "failed to get offload\n"); + } else { + indio_dev->info = <c2378_offload_iio_info; + indio_dev->channels = &st->info->offload_chan; + indio_dev->num_channels = 1; + ret = ltc2378_spi_offload_setup(indio_dev, st); + if (ret) + return dev_err_probe(dev, ret, + "failed to setup SPI offload\n"); + + ret = ltc2378_pwm_get(st); + if (ret) + return dev_err_probe(dev, ret, "failed to get PWM\n"); + + st->sample_freq_range[0] = 1; /* min */ + st->sample_freq_range[1] = 1; /* step */ + st->sample_freq_range[2] = st->info->max_sample_rate_Hz; /* max */ + + /* + * Start with a slower sampling rate so there is some room for + * adjusting the sample averaging and the sampling frequency + * without hitting the maximum conversion rate. + */ + ret = ltc2378_update_conversion_rate(st, st->info->max_sample_rate_Hz >> 4); + if (ret) + return dev_err_probe(dev, ret, + "failed to set offload samp freq\n"); + + ret = ltc2378_prepare_offload_message(&spi->dev, st); + if (ret) + return dev_err_probe(dev, ret, "failed to optimize SPI message\n"); + + /* + * Set single-read transfer bits_per_word so the SPI subsystem + * rearranges data to CPU endianness, enabling us to reuse + * offload_chan specifications for single-shot reads. + */ + st->xfer.bits_per_word = st->info->offload_chan.scan_type.realbits; + } st->xfer.rx_buf = &st->scan.data; st->xfer.len = spi_bpw_to_bytes(indio_dev->channels[0].scan_type.realbits); @@ -385,3 +781,5 @@ module_spi_driver(ltc2378_driver); MODULE_AUTHOR("Marcelo Schmitt <[email protected]>"); MODULE_DESCRIPTION("Analog Devices LTC2378 ADC series driver"); MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER"); +MODULE_IMPORT_NS("SPI_OFFLOAD"); -- 2.53.0