Add ADC driver support for Renesas RZ/G2L A/D converter in SW trigger mode. A/D Converter block is a successive approximation analog-to-digital converter with a 12-bit accuracy and supports a maximum of 8 input channels. Signed-off-by: Lad Prabhakar <prabhakar.mahadev-lad.rj@xxxxxxxxxxxxxx> Reviewed-by: Biju Das <biju.das.jz@xxxxxxxxxxxxxx> --- MAINTAINERS | 8 + drivers/iio/adc/Kconfig | 10 + drivers/iio/adc/Makefile | 1 + drivers/iio/adc/rzg2l_adc.c | 489 ++++++++++++++++++++++++++++++++++++ 4 files changed, 508 insertions(+) create mode 100644 drivers/iio/adc/rzg2l_adc.c diff --git a/MAINTAINERS b/MAINTAINERS index 81e1edeceae4..bee4c3847e01 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15622,6 +15622,14 @@ L: linux-renesas-soc@xxxxxxxxxxxxxxx S: Maintained F: drivers/phy/renesas/phy-rcar-gen3-usb*.c +RENESAS RZ/G2L A/D DRIVER +M: Lad Prabhakar <prabhakar.mahadev-lad.rj@xxxxxxxxxxxxxx> +L: linux-iio@xxxxxxxxxxxxxxx +L: linux-renesas-soc@xxxxxxxxxxxxxxx +S: Supported +F: Documentation/devicetree/bindings/iio/adc/renesas,rzg2l-adc.yaml +F: drivers/iio/adc/rzg2l_adc.c + RESET CONTROLLER FRAMEWORK M: Philipp Zabel <p.zabel@xxxxxxxxxxxxxx> S: Maintained diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig index c7946c439612..9408cbf97acc 100644 --- a/drivers/iio/adc/Kconfig +++ b/drivers/iio/adc/Kconfig @@ -887,6 +887,16 @@ config ROCKCHIP_SARADC To compile this driver as a module, choose M here: the module will be called rockchip_saradc. +config RZG2L_ADC + tristate "Renesas RZ/G2L ADC driver" + depends on ARCH_R9A07G044 || COMPILE_TEST + help + Say yes here to build support for the ADC found in Renesas + RZ/G2L family. + + To compile this driver as a module, choose M here: the + module will be called rzg2l_adc. + config SC27XX_ADC tristate "Spreadtrum SC27xx series PMICs ADC" depends on MFD_SC27XX_PMIC || COMPILE_TEST diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile index a226657d19c0..d92bcc9c5fbb 100644 --- a/drivers/iio/adc/Makefile +++ b/drivers/iio/adc/Makefile @@ -82,6 +82,7 @@ obj-$(CONFIG_QCOM_PM8XXX_XOADC) += qcom-pm8xxx-xoadc.o obj-$(CONFIG_RCAR_GYRO_ADC) += rcar-gyroadc.o obj-$(CONFIG_RN5T618_ADC) += rn5t618-adc.o obj-$(CONFIG_ROCKCHIP_SARADC) += rockchip_saradc.o +obj-$(CONFIG_RZG2L_ADC) += rzg2l_adc.o obj-$(CONFIG_SC27XX_ADC) += sc27xx_adc.o obj-$(CONFIG_SPEAR_ADC) += spear_adc.o obj-$(CONFIG_STX104) += stx104.o diff --git a/drivers/iio/adc/rzg2l_adc.c b/drivers/iio/adc/rzg2l_adc.c new file mode 100644 index 000000000000..1c58eb8ae1ec --- /dev/null +++ b/drivers/iio/adc/rzg2l_adc.c @@ -0,0 +1,489 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * RZ/G2L A/D Converter driver + * + * Copyright (c) 2021 Renesas Electronics Europe GmbH + * + * Author: Lad Prabhakar <prabhakar.mahadev-lad.rj@xxxxxxxxxxxxxx> + */ + +#include <linux/clk.h> +#include <linux/completion.h> +#include <linux/delay.h> +#include <linux/iio/iio.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/reset.h> + +#define ADM(n) ((n) * 0x4) +#define ADM0_ADCE BIT(0) +#define ADM0_ADBSY BIT(1) +#define ADM0_PWDWNB BIT(2) +#define ADM0_SRESB BIT(15) +#define ADM1_TRG BIT(0) +#define ADM1_MS BIT(2) +#define ADM1_BS BIT(4) +#define ADM1_EGA_CLEAR ~GENMASK(13, 12) +#define ADM2_CHSEL_CLEAR ~GENMASK(7, 0) +#define ADM3_ADSMP 0x578 +#define ADM3_ADCMP (0xe << 16) +#define ADM3_ADIL_CLEAR ~GENMASK(31, 24) + +#define ADINT 0x20 +#define ADINT_CH_CLEAR ~GENMASK(7, 0) +#define ADINT_CSEEN BIT(16) +#define ADINT_INTS BIT(31) +#define ADSTS 0x24 +#define ADINT_INTST_MASK GENMASK(7, 0) +#define ADSTS_CSEST BIT(16) +#define ADIVC 0x28 +#define ADIVC_DIVADC_CLEAR ~GENMASK(8, 0) +#define ADIVC_DIVADC_4 0x4 +#define ADFIL 0x2c +#define ADCR(n) (0x30 + ((n) * 0x4)) +#define ADCR_AD_MASK GENMASK(11, 0) + +#define ADC_MAX_CHANNELS 8 +#define ADC_CHN_MASK 0x7 +#define ADC_TIMEOUT usecs_to_jiffies(1 * 4) + +enum trigger_mode { + SW_TRIGGER = 0, + SYNC_TRIGGER, + ASYNC_TRIGGER, +}; + +struct rzg2l_adc_data { + const struct iio_chan_spec *channels; + u8 num_channels; + u8 trigger; +}; + +struct rzg2l_adc { + void __iomem *base; + struct clk *pclk; + struct clk *adclk; + struct reset_control *presetn; + struct reset_control *adrstn; + struct completion completion; + const struct rzg2l_adc_data *data; + bool adc_disabled; /* protected with mlock mutex from indio_dev */ + u16 last_val[ADC_MAX_CHANNELS]; +}; + +static unsigned int rzg2l_adc_readl(struct rzg2l_adc *adc, u32 reg) +{ + return readl(adc->base + reg); +} + +static void rzg2l_adc_writel(struct rzg2l_adc *adc, unsigned int reg, u32 val) +{ + writel(val, adc->base + reg); +} + +static int rzg2l_adc_adclk(struct rzg2l_adc *adc, bool prepare) +{ + if (prepare) + return clk_prepare_enable(adc->adclk); + + clk_disable_unprepare(adc->adclk); + return 0; +} + +static void rzg2l_adc_pwr(struct rzg2l_adc *adc, bool on) +{ + u32 reg; + + reg = rzg2l_adc_readl(adc, ADM(0)); + if (on) + reg |= ADM0_PWDWNB; + else + reg &= ~ADM0_PWDWNB; + rzg2l_adc_writel(adc, ADM(0), reg); + udelay(2); +} + +static void rzg2l_adc_conversion(struct rzg2l_adc *adc, bool start) +{ + int timeout = 5; + u32 reg; + + /* stop A/D conversion */ + reg = rzg2l_adc_readl(adc, ADM(0)); + if (start) + reg |= ADM0_ADCE; + else + reg &= ~ADM0_ADCE; + rzg2l_adc_writel(adc, ADM(0), reg); + + if (start) + return; + + do { + usleep_range(100, 200); + reg = rzg2l_adc_readl(adc, ADM(0)); + timeout--; + if (!timeout) { + pr_err("%s stopping ADC timed out\n", __func__); + break; + } + } while (((reg & ADM0_ADBSY) || (reg & ADM0_ADCE))); +} + +static int rzg2l_adc_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int *val, int *val2, long mask) +{ + struct rzg2l_adc *adc = iio_priv(indio_dev); + u32 reg; + int ret; + u8 ch; + + switch (mask) { + case IIO_CHAN_INFO_RAW: + mutex_lock(&indio_dev->mlock); + + if (adc->adc_disabled) { + mutex_unlock(&indio_dev->mlock); + return -EBUSY; + } + + if (rzg2l_adc_readl(adc, ADM(0)) & ADM0_ADBSY) { + mutex_unlock(&indio_dev->mlock); + return -EBUSY; + } + + ch = chan->channel & ADC_CHN_MASK; + /* SW trigger */ + reg = rzg2l_adc_readl(adc, ADM(1)); + reg &= ADM1_EGA_CLEAR; + reg &= ~ADM1_BS; + reg |= ADM1_MS; + reg &= ~ADM1_TRG; + rzg2l_adc_writel(adc, ADM(1), reg); + + /* select channel */ + reg = rzg2l_adc_readl(adc, ADM(2)); + reg &= ADM2_CHSEL_CLEAR; + reg |= BIT(ch); + rzg2l_adc_writel(adc, ADM(2), reg); + + reg = rzg2l_adc_readl(adc, ADM(3)); + reg &= ADM3_ADIL_CLEAR; + reg |= ADM3_ADCMP; + reg |= ADM3_ADSMP; + rzg2l_adc_writel(adc, ADM(3), reg); + + reg = rzg2l_adc_readl(adc, ADIVC); + reg &= ADIVC_DIVADC_CLEAR; + reg |= ADIVC_DIVADC_4; + rzg2l_adc_writel(adc, ADIVC, reg); + + reg = rzg2l_adc_readl(adc, ADINT); + reg &= ~ADINT_INTS; + reg &= ADINT_CH_CLEAR; + reg |= ADINT_CSEEN; + reg |= BIT(ch); + rzg2l_adc_writel(adc, ADINT, reg); + + rzg2l_adc_pwr(adc, true); + + ret = rzg2l_adc_adclk(adc, true); + if (ret) { + rzg2l_adc_pwr(adc, false); + mutex_unlock(&indio_dev->mlock); + return -EINVAL; + } + + reinit_completion(&adc->completion); + + rzg2l_adc_conversion(adc, true); + + if (!wait_for_completion_timeout(&adc->completion, ADC_TIMEOUT)) { + reg &= ADINT_CH_CLEAR; + rzg2l_adc_writel(adc, ADINT, reg); + rzg2l_adc_conversion(adc, false); + rzg2l_adc_adclk(adc, false); + rzg2l_adc_pwr(adc, false); + mutex_unlock(&indio_dev->mlock); + return -ETIMEDOUT; + } + + *val = adc->last_val[ch]; + rzg2l_adc_conversion(adc, false); + rzg2l_adc_adclk(adc, false); + rzg2l_adc_pwr(adc, false); + mutex_unlock(&indio_dev->mlock); + return IIO_VAL_INT; + + default: + return -EINVAL; + } +} + +static irqreturn_t rzg2l_adc_isr(int irq, void *dev_id) +{ + struct rzg2l_adc *adc = (struct rzg2l_adc *)dev_id; + u8 intst; + u32 reg; + u8 i; + + reg = rzg2l_adc_readl(adc, ADSTS); + if (reg & ADSTS_CSEST) { + rzg2l_adc_writel(adc, ADSTS, reg); + return IRQ_HANDLED; + } + + intst = reg & ADINT_INTST_MASK; + if (!intst) + return IRQ_HANDLED; + + for (i = 0; i < ADC_MAX_CHANNELS; i++) { + if (intst & BIT(i)) + adc->last_val[i] = rzg2l_adc_readl(adc, ADCR(i)) & ADCR_AD_MASK; + } + + rzg2l_adc_writel(adc, ADSTS, reg); + + complete(&adc->completion); + + return IRQ_HANDLED; +} + +static const struct iio_info rzg2l_adc_iio_info = { + .read_raw = rzg2l_adc_read_raw, +}; + +static const char * const rzg2l_adc_channel_name[] = { + "adc0", + "adc1", + "adc2", + "adc3", + "adc4", + "adc5", + "adc6", + "adc7", +}; + +static int rzg2l_adc_parse_of(struct platform_device *pdev, struct rzg2l_adc *adc) +{ + struct device_node *node = pdev->dev.of_node; + struct iio_chan_spec *chan_array; + u8 channels[ADC_MAX_CHANNELS]; + struct rzg2l_adc_data *data; + int num_channels; + int ret; + u8 i; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + num_channels = of_property_count_u8_elems(node, "renesas-rzg2l,adc-channels"); + if (num_channels <= 0 || num_channels > ADC_MAX_CHANNELS) + return -EINVAL; + + ret = of_property_read_u8_array(node, "renesas-rzg2l,adc-channels", + channels, num_channels); + if (ret) + return ret; + + chan_array = devm_kcalloc(&pdev->dev, num_channels, sizeof(*chan_array), + GFP_KERNEL); + if (!chan_array) + return -ENOMEM; + + for (i = 0; i < num_channels; i++) { + chan_array[i].type = IIO_VOLTAGE; + chan_array[i].indexed = 1; + chan_array[i].channel = channels[i]; + chan_array[i].info_mask_separate = BIT(IIO_CHAN_INFO_RAW); + chan_array[i].info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE); + chan_array[i].datasheet_name = rzg2l_adc_channel_name[i]; + } + + ret = of_property_read_u8(node, "renesas-rzg2l,adc-trigger-mode", + &data->trigger); + if (ret) + data->trigger = SW_TRIGGER; + + /* we support SW_TRIGGER as of now */ + if (data->trigger != SW_TRIGGER) + return -EINVAL; + + data->num_channels = num_channels; + data->channels = chan_array; + adc->data = data; + + return 0; +} + +static int rzg2l_adc_sw_reset(struct rzg2l_adc *adc) +{ + int timeout = 5; + u32 val; + + val = rzg2l_adc_readl(adc, ADM(0)); + val |= ADM0_SRESB; + rzg2l_adc_writel(adc, ADM(0), val); + + while (!(rzg2l_adc_readl(adc, ADM(0)) & ADM0_SRESB)) { + if (!timeout) + return -EINVAL; + timeout--; + usleep_range(100, 200); + } + + return 0; +} + +static int rzg2l_adc_probe(struct platform_device *pdev) +{ + struct iio_dev *indio_dev; + struct rzg2l_adc *adc; + int ret; + int irq; + + indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*adc)); + if (!indio_dev) { + dev_err(&pdev->dev, "failed allocating iio device\n"); + return -ENOMEM; + } + + adc = iio_priv(indio_dev); + if (!adc) + return -ENOMEM; + + ret = rzg2l_adc_parse_of(pdev, adc); + if (ret) + return -ENOMEM; + + adc->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(adc->base)) { + dev_err(&pdev->dev, "missing mem resource"); + return PTR_ERR(adc->base); + } + + irq = platform_get_irq(pdev, 0); + if (irq < 0) { + dev_err(&pdev->dev, "no irq resource\n"); + return irq; + } + + adc->pclk = devm_clk_get(&pdev->dev, "pclk"); + if (IS_ERR(adc->pclk)) { + dev_err(&pdev->dev, "Failed to get pclk"); + return PTR_ERR(adc->pclk); + } + + adc->adclk = devm_clk_get(&pdev->dev, "adclk"); + if (IS_ERR(adc->adclk)) { + dev_err(&pdev->dev, "Failed to get adclk"); + return PTR_ERR(adc->adclk); + } + + adc->adrstn = devm_reset_control_get_exclusive(&pdev->dev, "adrst-n"); + if (IS_ERR(adc->adrstn)) { + dev_err(&pdev->dev, "failed to get adrstn\n"); + return PTR_ERR(adc->adrstn); + } + + adc->presetn = devm_reset_control_get_exclusive(&pdev->dev, "presetn"); + if (IS_ERR(adc->presetn)) { + dev_err(&pdev->dev, "failed to get presetn\n"); + return PTR_ERR(adc->presetn); + } + + ret = reset_control_deassert(adc->adrstn); + if (ret) + return ret; + + ret = reset_control_deassert(adc->presetn); + if (ret) + goto assert_adrstn; + + ret = clk_prepare_enable(adc->pclk); + if (ret) + goto assert_presetn; + + ret = rzg2l_adc_sw_reset(adc); + if (ret) + goto unprepare_pclk; + + init_completion(&adc->completion); + + platform_set_drvdata(pdev, indio_dev); + + ret = devm_request_irq(&pdev->dev, irq, rzg2l_adc_isr, + 0, dev_name(&pdev->dev), adc); + if (ret < 0) + goto unprepare_pclk; + + adc->adc_disabled = false; + indio_dev->name = dev_name(&pdev->dev); + indio_dev->dev.parent = &pdev->dev; + indio_dev->dev.of_node = pdev->dev.of_node; + indio_dev->info = &rzg2l_adc_iio_info; + indio_dev->modes = INDIO_DIRECT_MODE; + indio_dev->channels = adc->data->channels; + indio_dev->num_channels = adc->data->num_channels; + + ret = iio_device_register(indio_dev); + if (ret) + goto unprepare_pclk; + + return 0; + +unprepare_pclk: + clk_disable_unprepare(adc->pclk); +assert_presetn: + reset_control_assert(adc->presetn); +assert_adrstn: + reset_control_assert(adc->adrstn); + return ret; +} + +static int rzg2l_adc_remove(struct platform_device *pdev) +{ + struct iio_dev *indio_dev = platform_get_drvdata(pdev); + struct rzg2l_adc *adc = iio_priv(indio_dev); + + mutex_lock(&indio_dev->mlock); + adc->adc_disabled = true; + mutex_unlock(&indio_dev->mlock); + + iio_device_unregister(indio_dev); + + clk_disable_unprepare(adc->pclk); + reset_control_assert(adc->presetn); + reset_control_assert(adc->adrstn); + + return 0; +} + +static const struct of_device_id rzg2l_adc_match[] = { + { + .compatible = "renesas,rzg2l-adc", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, rzg2l_adc_match); + +static struct platform_driver rzg2l_adc_driver = { + .probe = rzg2l_adc_probe, + .remove = rzg2l_adc_remove, + .driver = { + .name = "rzg2l-adc", + .of_match_table = rzg2l_adc_match, + }, +}; + +module_platform_driver(rzg2l_adc_driver); + +MODULE_AUTHOR("Lad Prabhakar <prabhakar.mahadev-lad.rj@xxxxxxxxxxxxxx>"); +MODULE_DESCRIPTION("Renesas RZ/G2L ADC driver"); +MODULE_LICENSE("GPL v2"); -- 2.17.1