Add qcpwm(4), a driver for the PWM found on Qualcomm PMICs. This is used
authorpatrick <patrick@openbsd.org>
Wed, 9 Nov 2022 19:59:38 +0000 (19:59 +0000)
committerpatrick <patrick@openbsd.org>
Wed, 9 Nov 2022 19:59:38 +0000 (19:59 +0000)
on the Lenovo x13s to control the display backlight brightness.

ok kettenis@ mlarkin@

sys/dev/fdt/files.fdt
sys/dev/fdt/qcpwm.c [new file with mode: 0644]

index eb7a42e..9117640 100644 (file)
@@ -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 (file)
index 0000000..fc29fd1
--- /dev/null
@@ -0,0 +1,310 @@
+/*     $OpenBSD: qcpwm.c,v 1.1 2022/11/09 19:59:38 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", -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);
+}