From 1c9ad52eeb2029ce5e5c6cab2d52fc0d300be612 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 9 Nov 2022 19:59:38 +0000 Subject: [PATCH] Add qcpwm(4), a driver for the PWM found on Qualcomm PMICs. This is used on the Lenovo x13s to control the display backlight brightness. ok kettenis@ mlarkin@ --- sys/dev/fdt/files.fdt | 7 +- sys/dev/fdt/qcpwm.c | 310 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 sys/dev/fdt/qcpwm.c diff --git a/sys/dev/fdt/files.fdt b/sys/dev/fdt/files.fdt index eb7a42e0da0..9117640b46c 100644 --- a/sys/dev/fdt/files.fdt +++ b/sys/dev/fdt/files.fdt @@ -1,4 +1,4 @@ -# $OpenBSD: files.fdt,v 1.169 2022/11/08 19:49:34 patrick Exp $ +# $OpenBSD: files.fdt,v 1.170 2022/11/09 19:59:38 patrick Exp $ # # Config file and device description for machine-independent FDT code. # Included by ports that need it. @@ -659,6 +659,11 @@ device qcpon attach qcpon at spmi file dev/fdt/qcpon.c qcpon +# Qualcomm PMIC PWM +device qcpwm +attach qcpwm at spmi +file dev/fdt/qcpwm.c qcpwm + # Qualcomm PMIC RTC device qcrtc attach qcrtc at spmi diff --git a/sys/dev/fdt/qcpwm.c b/sys/dev/fdt/qcpwm.c new file mode 100644 index 00000000000..fc29fd1bb50 --- /dev/null +++ b/sys/dev/fdt/qcpwm.c @@ -0,0 +1,310 @@ +/* $OpenBSD: qcpwm.c,v 1.1 2022/11/09 19:59:38 patrick Exp $ */ +/* + * Copyright (c) 2022 Patrick Wildt + * + * 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 +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#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", -1); + if (sc->sc_addr < 0) { + printf(": can't find registers\n"); + return; + } + + 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); +} -- 2.20.1