/*	$OpenBSD: qcpwm.c,v 1.2 2022/11/30 09:52:13 patrick Exp $	*/
/*
 * Copyright (c) 2022 Patrick Wildt <patrick@blueri.se>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/device.h>
#include <sys/sysctl.h>

#include <machine/bus.h>
#include <machine/fdt.h>

#include <dev/fdt/spmivar.h>

#include <dev/ofw/openfirm.h>
#include <dev/ofw/fdt.h>
#include <dev/ofw/ofw_clock.h>
#include <dev/ofw/ofw_pinctrl.h>
#include <dev/ofw/ofw_misc.h>

#define PWM_CHAN_OFF(x)			(0x100 * (x))
#define PWM_SUBTYPE			0x05
#define  PWM_SUBTYPE_LPG			0x2
#define  PWM_SUBTYPE_PWM			0xb
#define  PWM_SUBTYPE_LPG_LITE			0x11
#define PWM_SIZE_CLK			0x41
#define  PWM_SIZE_CLK_SELECT_SHIFT		0
#define  PWM_SIZE_CLK_SELECT_MASK		0x3
#define  PWM_SIZE_CLK_LPG_9BIT			(3 << 4)
#define  PWM_SIZE_CLK_PWM_9BIT			(1 << 2)
#define  PWM_SIZE_CLK_LPG_LITE_9BIT		(1 << 4)
#define PWM_PREDIV_CLK			0x42
#define  PWM_PREDIV_CLK_EXP_SHIFT		0
#define  PWM_PREDIV_CLK_EXP_MASK		0x7
#define  PWM_PREDIV_CLK_PREDIV_SHIFT		5
#define  PWM_PREDIV_CLK_PREDIV_MASK		0x3
#define PWM_TYPE_CONFIG			0x43
#define  PWM_TYPE_CONFIG_GLITCH_REMOVAL		(1 << 5)
#define PWM_VALUE			0x44
#define PWM_ENABLE_CONTROL		0x46
#define  PWM_ENABLE_CONTROL_OUTPUT		(1U << 7)
#define  PWM_ENABLE_CONTROL_BUFFER_TRISTATE	(1 << 5)
#define  PWM_ENABLE_CONTROL_SRC_PWM		(1 << 2)
#define  PWM_ENABLE_CONTROL_RAMP_GEN		(1 << 1)
#define PWM_SYNC			0x47
#define  PWM_SYNC_PWM				(1 << 0)

#define PWM_RESOLUTION			512

#define NS_PER_S			1000000000LLU

uint64_t qcpwm_clk_rates[] = { 0, 1024, 32768, 19200000 };
uint64_t qcpwm_pre_divs[] = { 1, 3, 5, 6 };

struct qcpwm_softc {
	struct device		sc_dev;
	int			sc_node;

	spmi_tag_t		sc_tag;
	int8_t			sc_sid;
	uint16_t		sc_addr;

	int			sc_nchannel;

	struct pwm_device	sc_pd;
};

int	qcpwm_match(struct device *, void *, void *);
void	qcpwm_attach(struct device *, struct device *, void *);

const struct cfattach qcpwm_ca = {
	sizeof(struct qcpwm_softc), qcpwm_match, qcpwm_attach
};

struct cfdriver qcpwm_cd = {
	NULL, "qcpwm", DV_DULL
};

uint8_t	qcpwm_read(struct qcpwm_softc *, uint16_t);
void	qcpwm_write(struct qcpwm_softc *, uint16_t, uint8_t);

int	qcpwm_get_state(void *, uint32_t *, struct pwm_state *);
int	qcpwm_set_state(void *, uint32_t *, struct pwm_state *);

int
qcpwm_match(struct device *parent, void *match, void *aux)
{
	struct spmi_attach_args *saa = aux;

	return OF_is_compatible(saa->sa_node, "qcom,pm8350c-pwm");
}

void
qcpwm_attach(struct device *parent, struct device *self, void *aux)
{
	struct qcpwm_softc *sc = (struct qcpwm_softc *)self;
	struct spmi_attach_args *saa = aux;

	sc->sc_addr = OF_getpropint(saa->sa_node, "reg", 0xe800);
	sc->sc_node = saa->sa_node;
	sc->sc_tag = saa->sa_tag;
	sc->sc_sid = saa->sa_sid;

	sc->sc_nchannel = 4;

	printf("\n");

	pinctrl_byname(saa->sa_node, "default");

	clock_enable_all(saa->sa_node);
	reset_deassert_all(saa->sa_node);

	sc->sc_pd.pd_node = saa->sa_node;
	sc->sc_pd.pd_cookie = sc;
	sc->sc_pd.pd_get_state = qcpwm_get_state;
	sc->sc_pd.pd_set_state = qcpwm_set_state;

	pwm_register(&sc->sc_pd);
}

int
qcpwm_get_state(void *cookie, uint32_t *cells, struct pwm_state *ps)
{
	struct qcpwm_softc *sc = cookie;
	int chan = cells[0];
	uint64_t refclk, prediv, exp;
	uint64_t pcycles, dcycles;
	uint16_t pwmval;
	uint8_t reg;

	if (chan >= sc->sc_nchannel)
		return ENXIO;

	memset(ps, 0, sizeof(struct pwm_state));

	ps->ps_enabled = !!(qcpwm_read(sc, PWM_CHAN_OFF(chan) +
	    PWM_ENABLE_CONTROL) & PWM_ENABLE_CONTROL_OUTPUT);

	reg = qcpwm_read(sc, PWM_CHAN_OFF(chan) + PWM_SIZE_CLK);
	refclk = (reg >> PWM_SIZE_CLK_SELECT_SHIFT) &
	    PWM_SIZE_CLK_SELECT_MASK;
	refclk = qcpwm_clk_rates[refclk];
	if (refclk == 0)
		return 0;

	reg = qcpwm_read(sc, PWM_CHAN_OFF(chan) + PWM_PREDIV_CLK);
	exp = (reg >> PWM_PREDIV_CLK_EXP_SHIFT) &
	    PWM_PREDIV_CLK_EXP_MASK;
	prediv = (reg >> PWM_PREDIV_CLK_PREDIV_SHIFT) &
	    PWM_PREDIV_CLK_PREDIV_MASK;
	prediv = qcpwm_pre_divs[prediv];

	spmi_cmd_read(sc->sc_tag, sc->sc_sid, SPMI_CMD_EXT_READL,
	    sc->sc_addr + PWM_CHAN_OFF(chan) + PWM_VALUE,
	    &pwmval, sizeof(pwmval));

	pcycles = (NS_PER_S * PWM_RESOLUTION * prediv * (1 << exp));
	pcycles = (pcycles + refclk - 1) / refclk;
	dcycles = (NS_PER_S * pwmval * prediv * (1 << exp));
	dcycles = (dcycles + refclk - 1) / refclk;

	ps->ps_period = pcycles;
	ps->ps_pulse_width = dcycles;
	return 0;
}

int
qcpwm_set_state(void *cookie, uint32_t *cells, struct pwm_state *ps)
{
	struct qcpwm_softc *sc = cookie;
	uint64_t dcycles, pcycles, acycles, diff = UINT64_MAX;
	int chan = cells[0];
	int clksel, divsel, exp;
	uint16_t pwmval;
	uint8_t reg;
	int i, j, m;

	if (chan >= sc->sc_nchannel)
		return ENXIO;

	pcycles = ps->ps_period;
	dcycles = ps->ps_pulse_width;

	for (i = 1; i < nitems(qcpwm_clk_rates); i++) {
		for (j = 0; j < nitems(qcpwm_pre_divs); j++) {
			if (pcycles * qcpwm_clk_rates[i] <
			    NS_PER_S * qcpwm_pre_divs[j] * PWM_RESOLUTION)
				continue;

			m = flsl((pcycles * qcpwm_clk_rates[i]) /
			    (NS_PER_S * qcpwm_pre_divs[j] * PWM_RESOLUTION))
			    - 1;
			if (m > PWM_PREDIV_CLK_EXP_MASK)
				m = PWM_PREDIV_CLK_EXP_MASK;

			acycles = (NS_PER_S * qcpwm_pre_divs[j] *
			    PWM_RESOLUTION * (1 << m)) /
			    qcpwm_clk_rates[i];
			if (pcycles - acycles < diff) {
				diff = pcycles - acycles;
				ps->ps_period = acycles;
				clksel = i;
				divsel = j;
				exp = m;
			}
		}
	}

	ps->ps_pulse_width = (dcycles * ps->ps_period) / pcycles;
	if (ps->ps_pulse_width > ps->ps_period)
		ps->ps_pulse_width = ps->ps_period;

	pcycles = ps->ps_period;
	dcycles = ps->ps_pulse_width;
	pwmval = ((uint64_t)ps->ps_pulse_width * qcpwm_clk_rates[clksel]) /
	    (NS_PER_S * qcpwm_pre_divs[divsel] * (1 << exp));

	reg = qcpwm_read(sc, PWM_CHAN_OFF(chan) + PWM_TYPE_CONFIG);
	reg |= PWM_TYPE_CONFIG_GLITCH_REMOVAL;
	qcpwm_write(sc, PWM_CHAN_OFF(chan) + PWM_TYPE_CONFIG, reg);

	reg = clksel << PWM_SIZE_CLK_SELECT_SHIFT;
	switch (qcpwm_read(sc, PWM_CHAN_OFF(chan) + PWM_SUBTYPE)) {
	case PWM_SUBTYPE_LPG:
		reg |= PWM_SIZE_CLK_LPG_9BIT;
		break;
	case PWM_SUBTYPE_PWM:
		reg |= PWM_SIZE_CLK_PWM_9BIT;
		break;
	case PWM_SUBTYPE_LPG_LITE:
	default:
		reg |= PWM_SIZE_CLK_LPG_LITE_9BIT;
		break;
	}
	qcpwm_write(sc, PWM_CHAN_OFF(chan) + PWM_SIZE_CLK, reg);
	qcpwm_write(sc, PWM_CHAN_OFF(chan) + PWM_PREDIV_CLK,
	    divsel << PWM_PREDIV_CLK_PREDIV_SHIFT |
	    exp << PWM_PREDIV_CLK_EXP_SHIFT);

	if (ps->ps_enabled) {
		spmi_cmd_write(sc->sc_tag, sc->sc_sid, SPMI_CMD_EXT_WRITEL,
		    sc->sc_addr + PWM_CHAN_OFF(chan) + PWM_VALUE,
		    &pwmval, sizeof(pwmval));
	}

	reg = PWM_ENABLE_CONTROL_BUFFER_TRISTATE;
	if (ps->ps_enabled)
		reg |= PWM_ENABLE_CONTROL_OUTPUT;
	reg |= PWM_ENABLE_CONTROL_SRC_PWM;
	qcpwm_write(sc, PWM_CHAN_OFF(chan) + PWM_ENABLE_CONTROL, reg);

	/* HW quirk: rewrite after enable */
	if (ps->ps_enabled) {
		spmi_cmd_write(sc->sc_tag, sc->sc_sid, SPMI_CMD_EXT_WRITEL,
		    sc->sc_addr + PWM_CHAN_OFF(chan) + PWM_VALUE,
		    &pwmval, sizeof(pwmval));
	}

	qcpwm_write(sc, PWM_CHAN_OFF(chan) + PWM_SYNC, PWM_SYNC_PWM);

	reg = qcpwm_read(sc, PWM_CHAN_OFF(chan) + PWM_TYPE_CONFIG);
	reg &= ~PWM_TYPE_CONFIG_GLITCH_REMOVAL;
	qcpwm_write(sc, PWM_CHAN_OFF(chan) + PWM_TYPE_CONFIG, reg);

	return 0;
}

uint8_t
qcpwm_read(struct qcpwm_softc *sc, uint16_t reg)
{
	uint8_t val = 0;
	int error;

	error = spmi_cmd_read(sc->sc_tag, sc->sc_sid, SPMI_CMD_EXT_READL,
	    sc->sc_addr + reg, &val, sizeof(val));
	if (error)
		printf("%s: error reading\n", sc->sc_dev.dv_xname);

	return val;
}

void
qcpwm_write(struct qcpwm_softc *sc, uint16_t reg, uint8_t val)
{
	int error;

	error = spmi_cmd_write(sc->sc_tag, sc->sc_sid, SPMI_CMD_EXT_WRITEL,
	    sc->sc_addr + reg, &val, sizeof(val));
	if (error)
		printf("%s: error writing\n", sc->sc_dev.dv_xname);
}
