// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2016-2017 Socionext Inc.
 *   Author: Masahiro Yamada <yamada.masahiro@socionext.com>
 */

#include <dm.h>
#include <linux/bitops.h>
#include <linux/io.h>
#include <linux/sizes.h>
#include <linux/errno.h>
#include <asm/global_data.h>
#include <asm/gpio.h>
#include <dt-bindings/gpio/uniphier-gpio.h>

#define UNIPHIER_GPIO_PORT_DATA		0x0	/* data */
#define UNIPHIER_GPIO_PORT_DIR		0x4	/* direction (1:in, 0:out) */
#define UNIPHIER_GPIO_IRQ_EN		0x90	/* irq enable */

struct uniphier_gpio_priv {
	void __iomem *regs;
};

static unsigned int uniphier_gpio_bank_to_reg(unsigned int bank)
{
	unsigned int reg;

	reg = (bank + 1) * 8;

	/*
	 * Unfortunately, the GPIO port registers are not contiguous because
	 * offset 0x90-0x9f is used for IRQ.  Add 0x10 when crossing the region.
	 */
	if (reg >= UNIPHIER_GPIO_IRQ_EN)
		reg += 0x10;

	return reg;
}

static void uniphier_gpio_get_bank_and_mask(unsigned int offset,
					    unsigned int *bank, u32 *mask)
{
	*bank = offset / UNIPHIER_GPIO_LINES_PER_BANK;
	*mask = BIT(offset % UNIPHIER_GPIO_LINES_PER_BANK);
}

static void uniphier_gpio_reg_update(struct uniphier_gpio_priv *priv,
				     unsigned int reg, u32 mask, u32 val)
{
	u32 tmp;

	tmp = readl(priv->regs + reg);
	tmp &= ~mask;
	tmp |= mask & val;
	writel(tmp, priv->regs + reg);
}

static void uniphier_gpio_bank_write(struct udevice *dev, unsigned int bank,
				     unsigned int reg, u32 mask, u32 val)
{
	struct uniphier_gpio_priv *priv = dev_get_priv(dev);

	if (!mask)
		return;

	uniphier_gpio_reg_update(priv, uniphier_gpio_bank_to_reg(bank) + reg,
				 mask, val);
}

static void uniphier_gpio_offset_write(struct udevice *dev, unsigned int offset,
				       unsigned int reg, int val)
{
	unsigned int bank;
	u32 mask;

	uniphier_gpio_get_bank_and_mask(offset, &bank, &mask);

	uniphier_gpio_bank_write(dev, bank, reg, mask, val ? mask : 0);
}

static int uniphier_gpio_offset_read(struct udevice *dev,
				     unsigned int offset, unsigned int reg)
{
	struct uniphier_gpio_priv *priv = dev_get_priv(dev);
	unsigned int bank, reg_offset;
	u32 mask;

	uniphier_gpio_get_bank_and_mask(offset, &bank, &mask);
	reg_offset = uniphier_gpio_bank_to_reg(bank) + reg;

	return !!(readl(priv->regs + reg_offset) & mask);
}

static int uniphier_gpio_get_function(struct udevice *dev, unsigned int offset)
{
	return uniphier_gpio_offset_read(dev, offset, UNIPHIER_GPIO_PORT_DIR) ?
						GPIOF_INPUT : GPIOF_OUTPUT;
}

static int uniphier_gpio_direction_input(struct udevice *dev,
					 unsigned int offset)
{
	uniphier_gpio_offset_write(dev, offset, UNIPHIER_GPIO_PORT_DIR, 1);

	return 0;
}

static int uniphier_gpio_direction_output(struct udevice *dev,
					  unsigned int offset, int value)
{
	uniphier_gpio_offset_write(dev, offset, UNIPHIER_GPIO_PORT_DATA, value);
	uniphier_gpio_offset_write(dev, offset, UNIPHIER_GPIO_PORT_DIR, 0);

	return 0;
}

static int uniphier_gpio_get_value(struct udevice *dev, unsigned int offset)
{
	return uniphier_gpio_offset_read(dev, offset, UNIPHIER_GPIO_PORT_DATA);
}

static int uniphier_gpio_set_value(struct udevice *dev,
				   unsigned int offset, int value)
{
	uniphier_gpio_offset_write(dev, offset, UNIPHIER_GPIO_PORT_DATA, value);

	return 0;
}

static const struct dm_gpio_ops uniphier_gpio_ops = {
	.direction_input	= uniphier_gpio_direction_input,
	.direction_output	= uniphier_gpio_direction_output,
	.get_value		= uniphier_gpio_get_value,
	.set_value		= uniphier_gpio_set_value,
	.get_function		= uniphier_gpio_get_function,
};

static int uniphier_gpio_probe(struct udevice *dev)
{
	struct uniphier_gpio_priv *priv = dev_get_priv(dev);
	struct gpio_dev_priv *uc_priv = dev_get_uclass_priv(dev);
	fdt_addr_t addr;

	addr = dev_read_addr(dev);
	if (addr == FDT_ADDR_T_NONE)
		return -EINVAL;

	priv->regs = devm_ioremap(dev, addr, SZ_512);
	if (!priv->regs)
		return -ENOMEM;

	uc_priv->gpio_count = fdtdec_get_uint(gd->fdt_blob, dev_of_offset(dev),
					      "ngpios", 0);

	return 0;
}

static const struct udevice_id uniphier_gpio_match[] = {
	{ .compatible = "socionext,uniphier-gpio" },
	{ /* sentinel */ }
};

U_BOOT_DRIVER(uniphier_gpio) = {
	.name	= "uniphier-gpio",
	.id	= UCLASS_GPIO,
	.of_match = uniphier_gpio_match,
	.probe	= uniphier_gpio_probe,
	.priv_auto	= sizeof(struct uniphier_gpio_priv),
	.ops	= &uniphier_gpio_ops,
};
