/*
 * Copyright (c) 2022 Microchip Technololgy Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT microchip_xec_pwmbbled

#include <errno.h>
#include <stdlib.h>
#include <stdint.h>

#include <zephyr/device.h>
#include <zephyr/drivers/pwm.h>
#ifdef CONFIG_SOC_SERIES_MEC172X
#include <zephyr/drivers/clock_control/mchp_xec_clock_control.h>
#include <zephyr/drivers/interrupt_controller/intc_mchp_xec_ecia.h>
#endif
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>

#include <soc.h>

LOG_MODULE_REGISTER(pwmbbled_mchp_xec, CONFIG_PWM_LOG_LEVEL);

#define XEC_PWM_BBLED_MAX_FREQ_DIV	256U

/* We will choose frequency from Device Tree */
#define XEC_PWM_BBLED_INPUT_FREQ_HI	48000000
#define XEC_PWM_BBLED_INPUT_FREQ_LO	32768

/* Hardware blink mode equation is Fpwm = Fin / (256 * (LD + 1))
 * The maximum Fpwm is actually Fin / 256
 * LD in [0, 4095]
 */
#define XEC_PWM_BBLED_MAX_PWM_FREQ_HI	(XEC_PWM_BBLED_INPUT_FREQ_HI / \
		XEC_PWM_BBLED_MAX_FREQ_DIV)
#define XEC_PWM_BBLED_MAX_PWM_FREQ_LO	(XEC_PWM_BBLED_INPUT_FREQ_LO / \
		XEC_PWM_BBLED_MAX_FREQ_DIV)
#define XEC_PWM_BBLED_LD_MAX 4095
#define XEC_PWM_BBLED_DC_MIN 1u /* 0 full off */
#define XEC_PWM_BBLED_DC_MAX 254u /* 255 is full on */

/* BBLED PWM mode uses the duty cycle to set the PWM frequency:
 * Fpwm = Fclock / (256 * (LD + 1)) OR
 * Tpwm = (256 * (LD + 1)) / Fclock
 * Fclock is 48MHz or 32KHz
 * LD = Delay register, LOW_DELAY field: bits[11:0]
 * Pulse_ON_width = (1/Fpwm) * (duty_cycle/256) seconds
 * Puse_OFF_width = (1/Fpwm) * (256 - duty_cycle) seconds
 * where duty_cycle is an 8-bit value 0 to 255.
 * Prescale is derived from DELAY register LOW_DELAY 12-bit field
 * Duty cycle is derived from LIMITS register MINIMUM 8-bit field
 *
 * Fc in Hz, Tp in seconds
 * Fc / Fp = 256 * (LD+1)
 * Tp / Tc = 256 * (LD+1)
 *
 * API passes pulse period and pulse width in nanoseconds.
 * BBLED PWM mode duty cycle specified by 8-bit MIN field of the LIMITS register
 * MIN=0 is OFF, pin driven low
 * MIN=255 is ON, pin driven high
 */

/* Same BBLED hardware block in MEC15xx and MEC172x families
 * Config register
 */
#define XEC_PWM_BBLED_CFG_MSK			0x1ffffu
#define XEC_PWM_BBLED_CFG_MODE_POS		0
#define XEC_PWM_BBLED_CFG_MODE_MSK		0x3u
#define XEC_PWM_BBLED_CFG_MODE_OFF		0
#define XEC_PWM_BBLED_CFG_MODE_PWM		0x2u
#define XEC_PWM_BBLED_CFG_MODE_ALWAYS_ON	0x3u
#define XEC_PWM_BBLED_CFG_CLK_SRC_48M_POS	2
#define XEC_PWM_BBLED_CFG_EN_UPDATE_POS		6
#define XEC_PWM_BBLED_CFG_RST_PWM_POS		7
#define XEC_PWM_BBLED_CFG_WDT_RLD_POS		8
#define XEC_PWM_BBLED_CFG_WDT_RLD_MSK0		0xffu
#define XEC_PWM_BBLED_CFG_WDT_RLD_MSK		0xff00u
#define XEC_PWM_BBLED_CFG_WDT_RLD_DFLT		0x1400u

/* Limits register */
#define XEC_PWM_BBLED_LIM_MSK			0xffffu
#define XEC_PWM_BBLED_LIM_MIN_POS		0
#define XEC_PWM_BBLED_LIM_MIN_MSK		0xffu
#define XEC_PWM_BBLED_LIM_MAX_POS		8
#define XEC_PWM_BBLED_LIM_MAX_MSK		0xff00u

/* Delay register */
#define XEC_PWM_BBLED_DLY_MSK			0xffffffu
#define XEC_PWM_BBLED_DLY_LO_POS		0
#define XEC_PWM_BBLED_DLY_LO_MSK		0xfffu
#define XEC_PWM_BBLED_DLY_HI_POS		12
#define XEC_PWM_BBLED_DLY_HI_MSK		0xfff000u

/* Output delay in clocks for initial enable and enable on resume from sleep
 * Clocks are either 48MHz or 32KHz selected in CONFIG register.
 */
#define XEC_PWM_BBLED_OUT_DLY_MSK		0xffu

/* DT enum values */
#define XEC_PWM_BBLED_CLKSEL_32K	0
#define XEC_PWM_BBLED_CLKSEL_AHB_48M	1

#define XEC_PWM_BBLED_CLKSEL_0		XEC_PWM_BBLED_CLKSEL_32K
#define XEC_PWM_BBLED_CLKSEL_1		XEC_PWM_BBLED_CLKSEL_AHB_48M


struct bbled_regs {
	volatile uint32_t config;
	volatile uint32_t limits;
	volatile uint32_t delay;
	volatile uint32_t update_step_size;
	volatile uint32_t update_interval;
	volatile uint32_t output_delay;
};

#define XEC_PWM_BBLED_CLK_SEL_48M	0
#define XEC_PWM_BBLED_CLK_SEL_32K	1

struct pwm_bbled_xec_config {
	struct bbled_regs * const regs;
	const struct pinctrl_dev_config *pcfg;
	uint8_t girq;
	uint8_t girq_pos;
	uint8_t pcr_idx;
	uint8_t pcr_pos;
	uint8_t clk_sel;
	bool enable_low_power_32K;
};

struct bbled_xec_data {
	uint32_t config;
};

/* Issue: two separate registers must be updated.
 * LIMITS.MIN = duty cycle = [1, 254]
 * LIMITS register update takes effect immediately.
 * DELAY.LO = pre-scaler = [0, 4095]
 * Writing DELAY stores value in an internal holding register.
 * Writing bit[6]=1 causes HW to update DELAY at the beginning of
 * the next HW PWM period.
 */
static void xec_pwmbb_progam_pwm(const struct device *dev, uint32_t ld, uint32_t dc)
{
	const struct pwm_bbled_xec_config * const cfg = dev->config;
	struct bbled_regs * const regs = cfg->regs;
	uint32_t val;

	val = regs->limits & ~(XEC_PWM_BBLED_LIM_MIN_MSK);
	val |= ((dc << XEC_PWM_BBLED_LIM_MIN_POS) & XEC_PWM_BBLED_LIM_MIN_MSK);
	regs->limits = val;

	val = regs->delay & ~(XEC_PWM_BBLED_DLY_LO_MSK);
	val |= ((ld  << XEC_PWM_BBLED_DLY_LO_POS) & XEC_PWM_BBLED_DLY_LO_MSK);
	regs->delay = val;

	 /* transfer new delay value from holding register */
	regs->config |= BIT(XEC_PWM_BBLED_CFG_EN_UPDATE_POS);

	val = regs->config & ~(XEC_PWM_BBLED_CFG_MODE_MSK);
	val |= XEC_PWM_BBLED_CFG_MODE_PWM;
	regs->config = val;
}

/* API implementation: Get the clock rate (cycles per second) for a single PWM output.
 * BBLED in PWM mode (same as blink mode) PWM frequency = Source Frequency / (256 * (LP + 1))
 * where Source Frequency is either 48 MHz or 32768 Hz and LP is the 12-bit low delay
 * field of the DELAY register. We return the maximum PWM frequency which is configured
 * hardware input frequency (32K or 48M) divided by 256.
 */
static int pwm_bbled_xec_get_cycles_per_sec(const struct device *dev,
					    uint32_t channel, uint64_t *cycles)
{
	const struct pwm_bbled_xec_config * const cfg = dev->config;
	struct bbled_regs * const regs = cfg->regs;

	if (channel > 0) {
		return -EIO;
	}

	if (cycles) {
		if (regs->config & BIT(XEC_PWM_BBLED_CFG_CLK_SRC_48M_POS)) {
			*cycles = XEC_PWM_BBLED_MAX_PWM_FREQ_HI; /* 187,500 Hz */
		} else {
			*cycles = XEC_PWM_BBLED_MAX_PWM_FREQ_LO; /* 128 Hz */
		}
	}

	return 0;
}

/* API PWM set cycles:
 * pulse == 0 -> pin should be constant inactive level
 * pulse >= period -> pin should be constant active level
 * hardware PWM (blink) mode: Fpwm = Fin_actual / (LD + 1)
 * Fin_actual = XEC_PWM_BBLED_MAX_PWM_FREQ_HI or XEC_PWM_BBLED_MAX_PWM_FREQ_LO.
 *  period cycles and pulse cycles both zero is OFF
 *  pulse cycles == 0 is OFF
 *  pulse cycles > 0 and period cycles == 0 is OFF
 *  otherwise
 *    compute duty cycle = 256 * (pulse_cycles / period_cycles).
 *    compute (LD + 1) = Fin_actual / Fpwm
 *    program LD into bits[11:0] of Delay register
 *    program duty cycle info bits[7:0] of Limits register
 * NOTE: flags parameter is currently used for pin invert and PWM capture.
 * The BBLED HW does not support pin invert or PWM capture.
 * NOTE 2: Pin invert is possible by using the MCHP function invert feature
 * of the GPIO pin. This property can be set using PINCTRL at build time.
 */
static int pwm_bbled_xec_set_cycles(const struct device *dev, uint32_t channel,
				    uint32_t period_cycles, uint32_t pulse_cycles,
				    pwm_flags_t flags)
{
	const struct pwm_bbled_xec_config * const cfg = dev->config;
	struct bbled_regs * const regs = cfg->regs;
	uint32_t dc, ld;

	if (channel > 0) {
		LOG_ERR("Invalid channel: %u", channel);
		return -EIO;
	}

	if (flags) {
		return -ENOTSUP;
	}

	LOG_DBG("period_cycles = %u  pulse_cycles = %u", period_cycles, pulse_cycles);

	if (pulse_cycles == 0u) {
		/* drive pin to inactive state */
		regs->config = (regs->config & ~XEC_PWM_BBLED_CFG_MODE_MSK)
			       | XEC_PWM_BBLED_CFG_MODE_OFF;
		regs->limits &= ~XEC_PWM_BBLED_LIM_MIN_MSK;
		regs->delay &= ~(XEC_PWM_BBLED_DLY_LO_MSK);
	} else if (pulse_cycles >= period_cycles) {
		/* drive pin to active state */
		regs->config = (regs->config & ~XEC_PWM_BBLED_CFG_MODE_MSK)
			       | XEC_PWM_BBLED_CFG_MODE_ALWAYS_ON;
		regs->limits &= ~XEC_PWM_BBLED_LIM_MIN_MSK;
		regs->delay &= ~(XEC_PWM_BBLED_DLY_LO_MSK);
	} else {
		ld = period_cycles;
		if (ld) {
			ld--;
			if (ld > XEC_PWM_BBLED_LD_MAX) {
				ld = XEC_PWM_BBLED_LD_MAX;
			}
		}

		dc = ((XEC_PWM_BBLED_DC_MAX + 1) * pulse_cycles / period_cycles);
		if (dc < XEC_PWM_BBLED_DC_MIN) {
			dc = XEC_PWM_BBLED_DC_MIN;
		} else if (dc > XEC_PWM_BBLED_DC_MAX) {
			dc = XEC_PWM_BBLED_DC_MAX;
		}

		LOG_DBG("Program: ld = 0x%0x  dc = 0x%0x", ld, dc);

		xec_pwmbb_progam_pwm(dev, ld, dc);
	}

	return 0;
}


#ifdef CONFIG_PM_DEVICE
static int pwm_bbled_xec_pm_action(const struct device *dev, enum pm_device_action action)
{
	const struct pwm_bbled_xec_config *const devcfg = dev->config;
	struct bbled_regs * const regs = devcfg->regs;
	struct bbled_xec_data * const data = dev->data;
	int ret = 0;

	/* 32K core clock is not gated by PCR in sleep, so BBLED can blink the LED even
	 * in sleep, if it is configured to use 32K clock. If we want to control it
	 * we shall use flag "enable_low_power_32K".
	 * This flag dont have effect on 48M clock. Since it is gated by PCR in sleep, BBLED
	 * will not get clock during sleep.
	 */
	if ((!devcfg->enable_low_power_32K) &&
			(!(regs->config & BIT(XEC_PWM_BBLED_CFG_CLK_SRC_48M_POS)))) {
		return ret;
	}

	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		ret = pinctrl_apply_state(devcfg->pcfg, PINCTRL_STATE_DEFAULT);
		if (ret != 0) {
			LOG_ERR("XEC BBLED pinctrl setup failed (%d)", ret);
		}

		/* Turn on BBLED only if it is ON before sleep */
		if ((data->config & XEC_PWM_BBLED_CFG_MODE_MSK) != XEC_PWM_BBLED_CFG_MODE_OFF) {

			regs->config |= (data->config & XEC_PWM_BBLED_CFG_MODE_MSK);
			regs->config |= BIT(XEC_PWM_BBLED_CFG_EN_UPDATE_POS);

			data->config = XEC_PWM_BBLED_CFG_MODE_OFF;
		}
	break;
	case PM_DEVICE_ACTION_SUSPEND:
		if ((regs->config & XEC_PWM_BBLED_CFG_MODE_MSK) != XEC_PWM_BBLED_CFG_MODE_OFF) {
			/* Do copy first, then clear mode. */
			data->config = regs->config;

			regs->config &= ~(XEC_PWM_BBLED_CFG_MODE_MSK);
		}

		ret = pinctrl_apply_state(devcfg->pcfg, PINCTRL_STATE_SLEEP);
		/* pinctrl-1 does not exist. */
		if (ret == -ENOENT) {
			ret = 0;
		}
	break;
	default:
	ret = -ENOTSUP;
	}
	return ret;
}
#endif /* CONFIG_PM_DEVICE */

static const struct pwm_driver_api pwm_bbled_xec_driver_api = {
	.set_cycles = pwm_bbled_xec_set_cycles,
	.get_cycles_per_sec = pwm_bbled_xec_get_cycles_per_sec,
};

static int pwm_bbled_xec_init(const struct device *dev)
{
	const struct pwm_bbled_xec_config * const cfg = dev->config;
	struct bbled_regs * const regs = cfg->regs;
	int ret = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT);

	if (ret != 0) {
		LOG_ERR("XEC PWM-BBLED pinctrl init failed (%d)", ret);
		return ret;
	}

	/* BBLED PWM WDT is enabled by default. Disable it and select 32KHz */
	regs->config = BIT(XEC_PWM_BBLED_CFG_RST_PWM_POS);
	regs->config = 0U;
	if (cfg->clk_sel == XEC_PWM_BBLED_CLKSEL_AHB_48M) {
		regs->config |= BIT(XEC_PWM_BBLED_CFG_CLK_SRC_48M_POS);
	}

	return 0;
}

#define XEC_PWM_BBLED_CLKSEL(n)							\
	COND_CODE_1(DT_INST_NODE_HAS_PROP(n, clock_select),			\
		    (DT_INST_ENUM_IDX(n, clock_select)), (0))

#define XEC_PWM_BBLED_CONFIG(inst)						\
	static struct pwm_bbled_xec_config pwm_bbled_xec_config_##inst = {	\
		.regs = (struct bbled_regs * const)DT_INST_REG_ADDR(inst),	\
		.girq = (uint8_t)(DT_INST_PROP_BY_IDX(0, girqs, 0)),		\
		.girq_pos = (uint8_t)(DT_INST_PROP_BY_IDX(0, girqs, 1)),	\
		.pcr_idx = (uint8_t)DT_INST_PROP_BY_IDX(inst, pcrs, 0),		\
		.pcr_pos = (uint8_t)DT_INST_PROP_BY_IDX(inst, pcrs, 1),		\
		.clk_sel = UTIL_CAT(XEC_PWM_BBLED_CLKSEL_, XEC_PWM_BBLED_CLKSEL(inst)),	\
		.enable_low_power_32K = DT_INST_PROP(inst, enable_low_power_32k),\
		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst),			\
	};

#define XEC_PWM_BBLED_DEVICE_INIT(index)				\
									\
	static struct bbled_xec_data bbled_xec_data_##index;	\
									\
	PINCTRL_DT_INST_DEFINE(index);					\
									\
	XEC_PWM_BBLED_CONFIG(index);					\
									\
	PM_DEVICE_DT_INST_DEFINE(index, pwm_bbled_xec_pm_action);	\
									\
	DEVICE_DT_INST_DEFINE(index, &pwm_bbled_xec_init,		\
			      PM_DEVICE_DT_INST_GET(index),		\
			      &bbled_xec_data_##index,			\
			      &pwm_bbled_xec_config_##index, POST_KERNEL,	\
			      CONFIG_PWM_INIT_PRIORITY,			\
			      &pwm_bbled_xec_driver_api);

DT_INST_FOREACH_STATUS_OKAY(XEC_PWM_BBLED_DEVICE_INIT)
