/*
 * Copyright (c) 2020 Henrik Brix Andersen <henrik@brixandersen.dk>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT xlnx_xps_timer_1_00_a_pwm

#include <zephyr/arch/cpu.h>
#include <zephyr/device.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/sys/sys_io.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(xlnx_axi_timer_pwm, CONFIG_PWM_LOG_LEVEL);

/* AXI Timer v2.0 registers offsets (See Xilinx PG079 for details) */
#define TCSR0_OFFSET 0x00
#define TLR0_OFFSET  0x04
#define TCR0_OFFSET  0x08
#define TCSR1_OFFSET 0x10
#define TLR1_OFFSET  0x14
#define TCR1_OFFSET  0x18

/* TCSRx bit definitions */
#define TCSR_MDT   BIT(0)
#define TCSR_UDT   BIT(1)
#define TCSR_GENT  BIT(2)
#define TCSR_CAPT  BIT(3)
#define TCSR_ARHT  BIT(4)
#define TCSR_LOAD  BIT(5)
#define TCSR_ENIT  BIT(6)
#define TCSR_ENT   BIT(7)
#define TCSR_TINT  BIT(8)
#define TCSR_PWMA  BIT(9)
#define TCSR_ENALL BIT(10)
#define TCSR_CASC  BIT(11)

/* Generate PWM mode, count-down, auto-reload */
#define TCSR_PWM (TCSR_UDT | TCSR_GENT | TCSR_ARHT | TCSR_PWMA)

struct xlnx_axi_timer_config {
	mm_reg_t base;
	uint32_t cycles_max;
	uint32_t freq;
};

static inline uint32_t xlnx_axi_timer_read32(const struct device *dev,
					     mm_reg_t offset)
{
	const struct xlnx_axi_timer_config *config = dev->config;

	return sys_read32(config->base + offset);
}

static inline void xlnx_axi_timer_write32(const struct device *dev,
					  uint32_t value,
					  mm_reg_t offset)
{
	const struct xlnx_axi_timer_config *config = dev->config;

	sys_write32(value, config->base + offset);
}

static int xlnx_axi_timer_set_cycles(const struct device *dev, uint32_t channel,
				     uint32_t period_cycles,
				     uint32_t pulse_cycles, pwm_flags_t flags)
{
	const struct xlnx_axi_timer_config *config = dev->config;
	uint32_t tcsr0 = TCSR_PWM;
	uint32_t tcsr1 = TCSR_PWM;
	uint32_t tlr0;
	uint32_t tlr1;

	if (channel != 0) {
		return -ENOTSUP;
	}

	LOG_DBG("period = 0x%08x, pulse = 0x%08x", period_cycles, pulse_cycles);

	if (pulse_cycles == 0) {
		LOG_DBG("setting constant inactive level");

		if (flags & PWM_POLARITY_INVERTED) {
			tcsr0 |= TCSR_ENT;
		} else {
			tcsr1 |= TCSR_ENT;
		}
	} else if (pulse_cycles == period_cycles) {
		LOG_DBG("setting constant active level");

		if (flags & PWM_POLARITY_INVERTED) {
			tcsr1 |= TCSR_ENT;
		} else {
			tcsr0 |= TCSR_ENT;
		}
	} else {
		LOG_DBG("setting normal pwm");

		if (period_cycles < 2) {
			LOG_ERR("period cycles too narrow");
			return -ENOTSUP;
		}

		/* PWM_PERIOD = (TLR0 + 2) * AXI_CLOCK_PERIOD */
		tlr0 = period_cycles - 2;

		if (tlr0 > config->cycles_max) {
			LOG_ERR("tlr0 out of range (0x%08x > 0x%08x)", tlr0,
				config->cycles_max);
			return -ENOTSUP;
		}

		if (flags & PWM_POLARITY_INVERTED) {
			/*
			 * Since this is a single-channel PWM controller (with
			 * no other channels to phase align with) inverse
			 * polarity can be achieved simply by inverting the
			 * pulse.
			 */

			if ((period_cycles - pulse_cycles) < 2) {
				LOG_ERR("pulse cycles too narrow");
				return -ENOTSUP;
			}

			/* PWM_HIGH_TIME = (TLR1 + 2) * AXI_CLOCK_PERIOD */
			tlr1 = period_cycles - pulse_cycles - 2;
		} else {
			if (pulse_cycles < 2) {
				LOG_ERR("pulse cycles too narrow");
				return -ENOTSUP;
			}

			/* PWM_HIGH_TIME = (TLR1 + 2) * AXI_CLOCK_PERIOD */
			tlr1 = pulse_cycles - 2;
		}

		LOG_DBG("tlr0 = 0x%08x, tlr1 = 0x%08x", tlr0, tlr1);

		/* Stop both timers */
		xlnx_axi_timer_write32(dev, TCSR_PWM, TCSR0_OFFSET);
		xlnx_axi_timer_write32(dev, TCSR_PWM, TCSR1_OFFSET);

		/* Load period cycles */
		xlnx_axi_timer_write32(dev, tlr0, TLR0_OFFSET);
		xlnx_axi_timer_write32(dev, TCSR_PWM | TCSR_LOAD, TCSR0_OFFSET);

		/* Load pulse cycles */
		xlnx_axi_timer_write32(dev, tlr1, TLR1_OFFSET);
		xlnx_axi_timer_write32(dev, TCSR_PWM | TCSR_LOAD, TCSR1_OFFSET);

		/* Start both timers */
		tcsr1 |= TCSR_ENALL;
	}

	xlnx_axi_timer_write32(dev, tcsr0, TCSR0_OFFSET);
	xlnx_axi_timer_write32(dev, tcsr1, TCSR1_OFFSET);

	return 0;
}

static int xlnx_axi_timer_get_cycles_per_sec(const struct device *dev,
					     uint32_t channel, uint64_t *cycles)
{
	const struct xlnx_axi_timer_config *config = dev->config;

	ARG_UNUSED(channel);

	*cycles = config->freq;

	return 0;
}

static const struct pwm_driver_api xlnx_axi_timer_driver_api = {
	.set_cycles = xlnx_axi_timer_set_cycles,
	.get_cycles_per_sec = xlnx_axi_timer_get_cycles_per_sec,
};

#define XLNX_AXI_TIMER_ASSERT_PROP_VAL(n, prop, val, str)	\
	BUILD_ASSERT(DT_INST_PROP(n, prop) == val, str)

#define XLNX_AXI_TIMER_INIT(n)						\
	XLNX_AXI_TIMER_ASSERT_PROP_VAL(n, xlnx_gen0_assert, 1,		\
				   "xlnx,gen0-assert must be 1 for pwm"); \
	XLNX_AXI_TIMER_ASSERT_PROP_VAL(n, xlnx_gen1_assert, 1,		\
				   "xlnx,gen1-assert must be 1 for pwm"); \
	XLNX_AXI_TIMER_ASSERT_PROP_VAL(n, xlnx_one_timer_only, 0,	\
				   "xlnx,one-timer-only must be 0 for pwm"); \
									\
	static struct xlnx_axi_timer_config xlnx_axi_timer_config_##n = { \
		.base = DT_INST_REG_ADDR(n),				\
		.freq = DT_INST_PROP(n, clock_frequency),		\
		.cycles_max =						\
			GENMASK(DT_INST_PROP(n, xlnx_count_width) - 1, 0), \
	};								\
									\
	DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL,			\
			    &xlnx_axi_timer_config_##n,			\
			    POST_KERNEL,				\
			    CONFIG_PWM_INIT_PRIORITY,			\
			    &xlnx_axi_timer_driver_api)

DT_INST_FOREACH_STATUS_OKAY(XLNX_AXI_TIMER_INIT);
