linux/drivers/iio/frequency/admv4420.c

// SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause
/*
 * ADMV4420
 *
 * Copyright 2021 Analog Devices Inc.
 */

#include <linux/bitfield.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/spi/spi.h>
#include <linux/units.h>

#include <linux/unaligned.h>

/* ADMV4420 Register Map */
#define ADMV4420_SPI_CONFIG_1			0x00
#define ADMV4420_SPI_CONFIG_2			0x01
#define ADMV4420_CHIPTYPE			0x03
#define ADMV4420_PRODUCT_ID_L			0x04
#define ADMV4420_PRODUCT_ID_H			0x05
#define ADMV4420_SCRATCHPAD			0x0A
#define ADMV4420_SPI_REV			0x0B
#define ADMV4420_ENABLES			0x103
#define ADMV4420_SDO_LEVEL			0x108
#define ADMV4420_INT_L				0x200
#define ADMV4420_INT_H				0x201
#define ADMV4420_FRAC_L				0x202
#define ADMV4420_FRAC_M				0x203
#define ADMV4420_FRAC_H				0x204
#define ADMV4420_MOD_L				0x208
#define ADMV4420_MOD_M				0x209
#define ADMV4420_MOD_H				0x20A
#define ADMV4420_R_DIV_L			0x20C
#define ADMV4420_R_DIV_H			0x20D
#define ADMV4420_REFERENCE			0x20E
#define ADMV4420_VCO_DATA_READBACK1		0x211
#define ADMV4420_VCO_DATA_READBACK2		0x212
#define ADMV4420_PLL_MUX_SEL			0x213
#define ADMV4420_LOCK_DETECT			0x214
#define ADMV4420_BAND_SELECT			0x215
#define ADMV4420_VCO_ALC_TIMEOUT		0x216
#define ADMV4420_VCO_MANUAL			0x217
#define ADMV4420_ALC				0x219
#define ADMV4420_VCO_TIMEOUT1			0x21C
#define ADMV4420_VCO_TIMEOUT2			0x21D
#define ADMV4420_VCO_BAND_DIV			0x21E
#define ADMV4420_VCO_READBACK_SEL		0x21F
#define ADMV4420_AUTOCAL			0x226
#define ADMV4420_CP_STATE			0x22C
#define ADMV4420_CP_BLEED_EN			0x22D
#define ADMV4420_CP_CURRENT			0x22E
#define ADMV4420_CP_BLEED			0x22F

#define ADMV4420_SPI_CONFIG_1_SDOACTIVE		(BIT(4) | BIT(3))
#define ADMV4420_SPI_CONFIG_1_ENDIAN		(BIT(5) | BIT(2))
#define ADMV4420_SPI_CONFIG_1_SOFTRESET		(BIT(7) | BIT(1))

#define ADMV4420_REFERENCE_DIVIDE_BY_2_MASK	BIT(0)
#define ADMV4420_REFERENCE_MODE_MASK		BIT(1)
#define ADMV4420_REFERENCE_DOUBLER_MASK		BIT(2)

#define ADMV4420_REF_DIVIDER_MAX_VAL		GENMASK(9, 0)
#define ADMV4420_N_COUNTER_INT_MAX		GENMASK(15, 0)
#define ADMV4420_N_COUNTER_FRAC_MAX		GENMASK(23, 0)
#define ADMV4420_N_COUNTER_MOD_MAX		GENMASK(23, 0)

#define ENABLE_PLL				BIT(6)
#define ENABLE_LO				BIT(5)
#define ENABLE_VCO				BIT(3)
#define ENABLE_IFAMP				BIT(2)
#define ENABLE_MIXER				BIT(1)
#define ENABLE_LNA				BIT(0)

#define ADMV4420_SCRATCH_PAD_VAL_1              0xAD
#define ADMV4420_SCRATCH_PAD_VAL_2              0xEA

#define ADMV4420_REF_FREQ_HZ                    50000000
#define MAX_N_COUNTER                           655360UL
#define MAX_R_DIVIDER                           1024
#define ADMV4420_DEFAULT_LO_FREQ_HZ		16750000000ULL

enum admv4420_mux_sel {
	ADMV4420_LOW = 0,
	ADMV4420_LOCK_DTCT = 1,
	ADMV4420_R_COUNTER_PER_2 = 4,
	ADMV4420_N_CONUTER_PER_2 = 5,
	ADMV4420_HIGH = 8,
};

struct admv4420_reference_block {
	bool doubler_en;
	bool divide_by_2_en;
	bool ref_single_ended;
	u32 divider;
};

struct admv4420_n_counter {
	u32 int_val;
	u32 frac_val;
	u32 mod_val;
	u32 n_counter;
};

struct admv4420_state {
	struct spi_device		*spi;
	struct regmap			*regmap;
	u64				vco_freq_hz;
	u64				lo_freq_hz;
	struct admv4420_reference_block ref_block;
	struct admv4420_n_counter	n_counter;
	enum admv4420_mux_sel		mux_sel;
	struct mutex			lock;
	u8				transf_buf[4] __aligned(IIO_DMA_MINALIGN);
};

static const struct regmap_config admv4420_regmap_config = {
	.reg_bits = 16,
	.val_bits = 8,
	.read_flag_mask = BIT(7),
};

static int admv4420_reg_access(struct iio_dev *indio_dev,
			       u32 reg, u32 writeval,
			       u32 *readval)
{
	struct admv4420_state *st = iio_priv(indio_dev);

	if (readval)
		return regmap_read(st->regmap, reg, readval);
	else
		return regmap_write(st->regmap, reg, writeval);
}

static int admv4420_set_n_counter(struct admv4420_state *st, u32 int_val,
				  u32 frac_val, u32 mod_val)
{
	int ret;

	put_unaligned_le32(frac_val, st->transf_buf);
	ret = regmap_bulk_write(st->regmap, ADMV4420_FRAC_L, st->transf_buf, 3);
	if (ret)
		return ret;

	put_unaligned_le32(mod_val, st->transf_buf);
	ret = regmap_bulk_write(st->regmap, ADMV4420_MOD_L, st->transf_buf, 3);
	if (ret)
		return ret;

	put_unaligned_le32(int_val, st->transf_buf);
	return regmap_bulk_write(st->regmap, ADMV4420_INT_L, st->transf_buf, 2);
}

static int admv4420_read_raw(struct iio_dev *indio_dev,
			     struct iio_chan_spec const *chan,
			     int *val, int *val2, long info)
{
	struct admv4420_state *st = iio_priv(indio_dev);

	switch (info) {
	case IIO_CHAN_INFO_FREQUENCY:

		*val = div_u64_rem(st->lo_freq_hz, MICRO, val2);

		return IIO_VAL_INT_PLUS_MICRO;
	default:
		return -EINVAL;
	}
}

static const struct iio_info admv4420_info = {
	.read_raw = admv4420_read_raw,
	.debugfs_reg_access = &admv4420_reg_access,
};

static const struct iio_chan_spec admv4420_channels[] = {
	{
		.type = IIO_ALTVOLTAGE,
		.output = 0,
		.indexed = 1,
		.channel = 0,
		.info_mask_separate = BIT(IIO_CHAN_INFO_FREQUENCY),
	},
};

static void admv4420_fw_parse(struct admv4420_state *st)
{
	struct device *dev = &st->spi->dev;
	u32 tmp;
	int ret;

	ret = device_property_read_u32(dev, "adi,lo-freq-khz", &tmp);
	if (!ret)
		st->lo_freq_hz = (u64)tmp * KILO;

	st->ref_block.ref_single_ended = device_property_read_bool(dev,
								   "adi,ref-ext-single-ended-en");
}

static inline uint64_t admv4420_calc_pfd_vco(struct admv4420_state *st)
{
	return div_u64(st->vco_freq_hz * 10, st->n_counter.n_counter);
}

static inline uint32_t admv4420_calc_pfd_ref(struct admv4420_state *st)
{
	uint32_t tmp;
	u8 doubler, divide_by_2;

	doubler = st->ref_block.doubler_en ? 2 : 1;
	divide_by_2 = st->ref_block.divide_by_2_en ? 2 : 1;
	tmp = ADMV4420_REF_FREQ_HZ * doubler;

	return (tmp / (st->ref_block.divider * divide_by_2));
}

static int admv4420_calc_parameters(struct admv4420_state *st)
{
	u64 pfd_ref, pfd_vco;
	bool sol_found = false;

	st->ref_block.doubler_en = false;
	st->ref_block.divide_by_2_en = false;
	st->vco_freq_hz = div_u64(st->lo_freq_hz, 2);

	for (st->ref_block.divider = 1; st->ref_block.divider < MAX_R_DIVIDER;
	    st->ref_block.divider++) {
		pfd_ref = admv4420_calc_pfd_ref(st);
		for (st->n_counter.n_counter = 1; st->n_counter.n_counter < MAX_N_COUNTER;
		    st->n_counter.n_counter++) {
			pfd_vco = admv4420_calc_pfd_vco(st);
			if (pfd_ref == pfd_vco) {
				sol_found = true;
				break;
			}
		}

		if (sol_found)
			break;

		st->n_counter.n_counter = 1;
	}
	if (!sol_found)
		return -1;

	st->n_counter.int_val = div_u64_rem(st->n_counter.n_counter, 10, &st->n_counter.frac_val);
	st->n_counter.mod_val = 10;

	return 0;
}

static int admv4420_setup(struct iio_dev *indio_dev)
{
	struct admv4420_state *st = iio_priv(indio_dev);
	struct device *dev = indio_dev->dev.parent;
	u32 val;
	int ret;

	ret = regmap_write(st->regmap, ADMV4420_SPI_CONFIG_1,
			   ADMV4420_SPI_CONFIG_1_SOFTRESET);
	if (ret)
		return ret;

	ret = regmap_write(st->regmap, ADMV4420_SPI_CONFIG_1,
			   ADMV4420_SPI_CONFIG_1_SDOACTIVE |
			   ADMV4420_SPI_CONFIG_1_ENDIAN);
	if (ret)
		return ret;

	ret = regmap_write(st->regmap,
			   ADMV4420_SCRATCHPAD,
			   ADMV4420_SCRATCH_PAD_VAL_1);
	if (ret)
		return ret;

	ret = regmap_read(st->regmap, ADMV4420_SCRATCHPAD, &val);
	if (ret)
		return ret;

	if (val != ADMV4420_SCRATCH_PAD_VAL_1) {
		dev_err(dev, "Failed ADMV4420 to read/write scratchpad %x ", val);
		return -EIO;
	}

	ret = regmap_write(st->regmap,
			   ADMV4420_SCRATCHPAD,
			   ADMV4420_SCRATCH_PAD_VAL_2);
	if (ret)
		return ret;

	ret = regmap_read(st->regmap, ADMV4420_SCRATCHPAD, &val);
	if (ret)
		return ret;

	if (val != ADMV4420_SCRATCH_PAD_VAL_2) {
		dev_err(dev, "Failed to read/write scratchpad %x ", val);
		return -EIO;
	}

	st->mux_sel = ADMV4420_LOCK_DTCT;
	st->lo_freq_hz = ADMV4420_DEFAULT_LO_FREQ_HZ;

	admv4420_fw_parse(st);

	ret = admv4420_calc_parameters(st);
	if (ret) {
		dev_err(dev, "Failed calc parameters for %lld ", st->vco_freq_hz);
		return ret;
	}

	ret = regmap_write(st->regmap, ADMV4420_R_DIV_L,
			   FIELD_GET(0xFF, st->ref_block.divider));
	if (ret)
		return ret;

	ret = regmap_write(st->regmap, ADMV4420_R_DIV_H,
			   FIELD_GET(0xFF00, st->ref_block.divider));
	if (ret)
		return ret;

	ret = regmap_write(st->regmap, ADMV4420_REFERENCE,
			   st->ref_block.divide_by_2_en |
			   FIELD_PREP(ADMV4420_REFERENCE_MODE_MASK, st->ref_block.ref_single_ended) |
			   FIELD_PREP(ADMV4420_REFERENCE_DOUBLER_MASK, st->ref_block.doubler_en));
	if (ret)
		return ret;

	ret = admv4420_set_n_counter(st, st->n_counter.int_val,
				     st->n_counter.frac_val,
				     st->n_counter.mod_val);
	if (ret)
		return ret;

	ret = regmap_write(st->regmap, ADMV4420_PLL_MUX_SEL, st->mux_sel);
	if (ret)
		return ret;

	return regmap_write(st->regmap, ADMV4420_ENABLES,
			    ENABLE_PLL | ENABLE_LO | ENABLE_VCO |
			    ENABLE_IFAMP | ENABLE_MIXER | ENABLE_LNA);
}

static int admv4420_probe(struct spi_device *spi)
{
	struct iio_dev *indio_dev;
	struct admv4420_state *st;
	struct regmap *regmap;
	int ret;

	indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*st));
	if (!indio_dev)
		return -ENOMEM;

	regmap = devm_regmap_init_spi(spi, &admv4420_regmap_config);
	if (IS_ERR(regmap))
		return dev_err_probe(&spi->dev, PTR_ERR(regmap),
				     "Failed to initializing spi regmap\n");

	st = iio_priv(indio_dev);
	st->spi = spi;
	st->regmap = regmap;

	indio_dev->name = "admv4420";
	indio_dev->info = &admv4420_info;
	indio_dev->channels = admv4420_channels;
	indio_dev->num_channels = ARRAY_SIZE(admv4420_channels);

	ret = admv4420_setup(indio_dev);
	if (ret) {
		dev_err(&spi->dev, "Setup ADMV4420 failed (%d)\n", ret);
		return ret;
	}

	return devm_iio_device_register(&spi->dev, indio_dev);
}

static const struct of_device_id admv4420_of_match[] = {
	{ .compatible = "adi,admv4420" },
	{ }
};

MODULE_DEVICE_TABLE(of, admv4420_of_match);

static struct spi_driver admv4420_driver = {
	.driver = {
		.name = "admv4420",
		.of_match_table = admv4420_of_match,
	},
	.probe = admv4420_probe,
};

module_spi_driver(admv4420_driver);

MODULE_AUTHOR("Cristian Pop <[email protected]>");
MODULE_DESCRIPTION("Analog Devices ADMV44200 K Band Downconverter");
MODULE_LICENSE("Dual BSD/GPL");