/* $NetBSD: meson_pwm.c,v 1.6 2024/02/07 04:20:26 msaitoh Exp $ */ /* * Copyright (c) 2021 Ryo Shimizu * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include __KERNEL_RCSID(0, "$NetBSD: meson_pwm.c,v 1.6 2024/02/07 04:20:26 msaitoh Exp $"); #include #include #include #include #include #include /*#define MESON_PWM_DEBUG*/ #define MESON_PWM_DUTYCYCLE_A_REG 0x00 #define MESON_PWM_DUTYCYCLE_B_REG 0x01 #define MESON_PWM_DUTYCYCLE_HIGH __BITS(31,16) #define MESON_PWM_DUTYCYCLE_LOW __BITS(15,0) #define MESON_PWM_MISC_AB_REG 0x02 #define MESON_PWM_MISC_AB_B_CLK_EN __BIT(23) #define MESON_PWM_MISC_AB_B_CLK_DIV __BITS(22,16) #define MESON_PWM_MISC_AB_A_CLK_EN __BIT(15) #define MESON_PWM_MISC_AB_A_CLK_DIV __BITS(14,8) #define MESON_PWM_MISC_AB_B_CLK_SEL __BITS(7,6) #define MESON_PWM_MISC_AB_A_CLK_SEL __BITS(5,4) #define MESON_PWM_MISC_AB_DS_B_EN __BIT(3) #define MESON_PWM_MISC_AB_DS_A_EN __BIT(2) #define MESON_PWM_MISC_AB_PWM_B_EN __BIT(1) #define MESON_PWM_MISC_AB_PWM_A_EN __BIT(0) #define MESON_PWM_DELTASIGMA_A_B_REG 0x03 #define MESON_PWM_TIME_AB_REG 0x04 #define MESON_PWM_A2_REG 0x05 #define MESON_PWM_B2_REG 0x06 #define MESON_PWM_BLINK_AB_REG 0x07 #define PWM_READ_REG(sc, reg) \ bus_space_read_4((sc)->sc_bst, (sc)->sc_bsh, (reg) * 4) #define PWM_WRITE_REG(sc, reg, val) \ bus_space_write_4((sc)->sc_bst, (sc)->sc_bsh, (reg) * 4, (val)) static const struct device_compatible_entry compat_data[] = { { .compat = "amlogic,meson-g12a-ao-pwm-ab" }, { .compat = "amlogic,meson-g12a-ao-pwm-cd" }, { .compat = "amlogic,meson-g12a-ee-pwm" }, DEVICE_COMPAT_EOL }; #define MESON_PWM_NCHAN 2 struct meson_pwm_channel { struct meson_pwm_softc *mpc_sc; struct pwm_controller mpc_pwm; struct pwm_config mpc_conf; u_int mpc_index; }; struct meson_pwm_softc { device_t sc_dev; bus_space_tag_t sc_bst; bus_space_handle_t sc_bsh; kmutex_t sc_reglock; /* for PWM_A vs. PWM_B */ int sc_phandle; u_int sc_clkfreq; u_int sc_clksource; struct meson_pwm_channel sc_pwmchan[MESON_PWM_NCHAN]; }; static int meson_pwm_enable(pwm_tag_t pwm, bool enable) { struct meson_pwm_channel * const pwmchan = pwm->pwm_priv; struct meson_pwm_softc * const sc = device_private(pwm->pwm_dev); uint32_t val; mutex_enter(&sc->sc_reglock); switch (pwmchan->mpc_index) { case 0: /* A */ val = PWM_READ_REG(sc, MESON_PWM_MISC_AB_REG); val &= ~MESON_PWM_MISC_AB_DS_A_EN; val |= MESON_PWM_MISC_AB_PWM_A_EN; PWM_WRITE_REG(sc, MESON_PWM_MISC_AB_REG, val); break; case 1: /* B */ val = PWM_READ_REG(sc, MESON_PWM_MISC_AB_REG); val &= ~MESON_PWM_MISC_AB_DS_B_EN; val |= MESON_PWM_MISC_AB_PWM_B_EN; PWM_WRITE_REG(sc, MESON_PWM_MISC_AB_REG, val); break; } mutex_exit(&sc->sc_reglock); return 0; } static void meson_pwm_get_current(struct meson_pwm_softc *sc, int chan, struct pwm_config *conf) { uint64_t period_hz, duty_hz; uint32_t val; u_int period, duty, clk_div, hi, lo; memset(conf, 0, sizeof(*conf)); mutex_enter(&sc->sc_reglock); switch (chan) { case 0: /* A */ val = PWM_READ_REG(sc, MESON_PWM_MISC_AB_REG); clk_div = __SHIFTOUT(val, MESON_PWM_MISC_AB_A_CLK_DIV); val = PWM_READ_REG(sc, MESON_PWM_DUTYCYCLE_A_REG); hi = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_HIGH); lo = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_LOW); break; case 1: /* B */ val = PWM_READ_REG(sc, MESON_PWM_MISC_AB_REG); clk_div = __SHIFTOUT(val, MESON_PWM_MISC_AB_B_CLK_DIV); val = PWM_READ_REG(sc, MESON_PWM_DUTYCYCLE_B_REG); hi = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_HIGH); lo = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_LOW); break; default: mutex_exit(&sc->sc_reglock); return; } mutex_exit(&sc->sc_reglock); clk_div += 1; duty_hz = (uint64_t)hi * clk_div; period_hz = (uint64_t)(hi + lo) * clk_div; period = period_hz * 1000000000ULL / sc->sc_clkfreq; duty = duty_hz * 1000000000ULL / sc->sc_clkfreq; conf->polarity = PWM_ACTIVE_HIGH; conf->period = period; conf->duty_cycle = duty; } static int meson_pwm_get_config(pwm_tag_t pwm, struct pwm_config *conf) { struct meson_pwm_channel * const pwmchan = pwm->pwm_priv; *conf = pwmchan->mpc_conf; return 0; } static int meson_pwm_set_config(pwm_tag_t pwm, const struct pwm_config *conf) { struct meson_pwm_channel * const pwmchan = pwm->pwm_priv; struct meson_pwm_softc * const sc = device_private(pwm->pwm_dev); uint64_t period_hz, duty_hz; uint32_t val; u_int period, duty, clk_div, hi, lo; #ifdef MESON_PWM_DEBUG u_int old_div = 0, old_hi = 0, old_lo = 0; #endif period = conf->period; duty = conf->duty_cycle; if (period == 0) return EINVAL; KASSERT(period >= duty); if (conf->polarity == PWM_ACTIVE_LOW) duty = period - duty; /* calculate the period to be within the maximum value (0xffff) */ #define MESON_PWMTIME_MAX 0xffff #define MESON_CLKDIV_MAX 127 clk_div = 1; period_hz = ((uint64_t)sc->sc_clkfreq * period + 500000000ULL) / 1000000000ULL; duty_hz = ((uint64_t)sc->sc_clkfreq * duty + 500000000ULL) / 1000000000ULL; if (period_hz > MESON_PWMTIME_MAX) { clk_div = (period_hz + 0x7fff) / 0xffff; period_hz /= clk_div; duty_hz /= clk_div; } clk_div -= 1; /* the divider is N+1 */ if (clk_div > MESON_CLKDIV_MAX) return EINVAL; hi = duty_hz; lo = period_hz - duty_hz; mutex_enter(&sc->sc_reglock); switch (pwmchan->mpc_index) { case 0: /* A */ val = PWM_READ_REG(sc, MESON_PWM_MISC_AB_REG); val &= ~MESON_PWM_MISC_AB_A_CLK_DIV; #ifdef MESON_PWM_DEBUG old_div = __SHIFTOUT(val, MESON_PWM_MISC_AB_A_CLK_DIV); #endif val |= __SHIFTIN(clk_div, MESON_PWM_MISC_AB_A_CLK_DIV); val &= ~MESON_PWM_MISC_AB_A_CLK_SEL; val |= __SHIFTIN(sc->sc_clksource, MESON_PWM_MISC_AB_A_CLK_SEL); val |= MESON_PWM_MISC_AB_A_CLK_EN; PWM_WRITE_REG(sc, MESON_PWM_MISC_AB_REG, val); val = PWM_READ_REG(sc, MESON_PWM_DUTYCYCLE_A_REG); #ifdef MESON_PWM_DEBUG old_hi = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_HIGH); old_lo = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_LOW); #endif PWM_WRITE_REG(sc, MESON_PWM_DUTYCYCLE_A_REG, __SHIFTIN(hi, MESON_PWM_DUTYCYCLE_HIGH) | __SHIFTIN(lo, MESON_PWM_DUTYCYCLE_LOW)); break; case 1: /* B */ val = PWM_READ_REG(sc, MESON_PWM_MISC_AB_REG); val &= ~MESON_PWM_MISC_AB_B_CLK_DIV; #ifdef MESON_PWM_DEBUG old_div = __SHIFTOUT(val, MESON_PWM_MISC_AB_B_CLK_DIV); #endif val |= __SHIFTIN(clk_div, MESON_PWM_MISC_AB_B_CLK_DIV); val &= ~MESON_PWM_MISC_AB_B_CLK_SEL; val |= __SHIFTIN(sc->sc_clksource, MESON_PWM_MISC_AB_B_CLK_SEL); val |= MESON_PWM_MISC_AB_B_CLK_EN; PWM_WRITE_REG(sc, MESON_PWM_MISC_AB_REG, val); val = PWM_READ_REG(sc, MESON_PWM_DUTYCYCLE_B_REG); #ifdef MESON_PWM_DEBUG old_hi = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_HIGH); old_lo = __SHIFTOUT(val, MESON_PWM_DUTYCYCLE_LOW); #endif PWM_WRITE_REG(sc, MESON_PWM_DUTYCYCLE_B_REG, __SHIFTIN(hi, MESON_PWM_DUTYCYCLE_HIGH) | __SHIFTIN(lo, MESON_PWM_DUTYCYCLE_LOW)); break; } mutex_exit(&sc->sc_reglock); pwmchan->mpc_conf = *conf; #ifdef MESON_PWM_DEBUG device_printf(sc->sc_dev, "%s: %s: polarity=%s, DutuCycle/Period=%uns/%uns(%u%%) : " "%uHz, HIGH:LOW=%u:%u(%u%%) -> " "%uHz, HIGH:LOW=%u:%u(%u%%)\n", __func__, pwmchan->mpc_index ? "A" : "B", (conf->polarity == PWM_ACTIVE_LOW) ? "LOW" : "HIGH", conf->duty_cycle, conf->period, conf->duty_cycle * 100 / conf->period, sc->sc_clkfreq / (old_div + 1), old_hi, old_lo, old_hi * 100 / (old_hi + old_lo), sc->sc_clkfreq / (clk_div + 1), hi, lo, hi * 100 / (hi + lo)); #endif return 0; } static pwm_tag_t meson_pwm_get_tag(device_t dev, const void *data, size_t len) { struct meson_pwm_softc * const sc = device_private(dev); struct meson_pwm_channel *pwmchan; const u_int *pwm = data; if (len != 16) return NULL; const u_int index = be32toh(pwm[1]); if (index >= MESON_PWM_NCHAN) return NULL; const u_int period = be32toh(pwm[2]); const u_int polarity = (pwm[3] == 0) ? PWM_ACTIVE_HIGH : PWM_ACTIVE_LOW; pwmchan = &sc->sc_pwmchan[index]; /* * if polarity or period in pwm-tag is different from the copy of * config it holds, the content returned by pwm_get_conf() should * also be according to the tag. * this is because the caller may only set_conf() if necessary. */ if (pwmchan->mpc_conf.polarity != polarity) { pwmchan->mpc_conf.duty_cycle = pwmchan->mpc_conf.period - pwmchan->mpc_conf.duty_cycle; pwmchan->mpc_conf.polarity = polarity; } if (pwmchan->mpc_conf.period != period) { if (pwmchan->mpc_conf.period == 0) { pwmchan->mpc_conf.duty_cycle = 0; } else { pwmchan->mpc_conf.duty_cycle = (uint64_t)pwmchan->mpc_conf.duty_cycle * period / pwmchan->mpc_conf.period; } pwmchan->mpc_conf.period = period; } return &pwmchan->mpc_pwm; } static struct fdtbus_pwm_controller_func meson_pwm_funcs = { .get_tag = meson_pwm_get_tag }; static int meson_pwm_match(device_t parent, cfdata_t cf, void *aux) { struct fdt_attach_args * const faa = aux; return of_compatible_match(faa->faa_phandle, compat_data); } static void meson_pwm_attach(device_t parent, device_t self, void *aux) { struct meson_pwm_softc * const sc = device_private(self); struct fdt_attach_args * const faa = aux; bus_addr_t addr; bus_size_t size; struct clk *clk; int phandle, i; sc->sc_dev = self; sc->sc_bst = faa->faa_bst; sc->sc_phandle = phandle = faa->faa_phandle; clk = fdtbus_clock_get_index(phandle, 0); if (clk == NULL) { aprint_error(": couldn't get clock\n"); return; } sc->sc_clkfreq = clk_get_rate(clk); if (clk == fdtbus_clock_byname("vid_pll")) sc->sc_clksource = 1; else if (clk == fdtbus_clock_byname("fclk_div4")) sc->sc_clksource = 2; else if (clk == fdtbus_clock_byname("fclk_div3")) sc->sc_clksource = 3; else sc->sc_clksource = 0; /* default: "xtal" */ if (fdtbus_get_reg(phandle, 0, &addr, &size) != 0) { aprint_error(": couldn't get registers\n"); return; } if (bus_space_map(sc->sc_bst, addr, size, 0, &sc->sc_bsh) != 0) { aprint_error(": couldn't map registers\n"); return; } aprint_naive("\n"); aprint_normal(": Pulse-Width Modulation\n"); mutex_init(&sc->sc_reglock, MUTEX_DEFAULT, IPL_NONE); for (i = 0; i < __arraycount(sc->sc_pwmchan); i++) { sc->sc_pwmchan[i].mpc_sc = sc; sc->sc_pwmchan[i].mpc_index = i; sc->sc_pwmchan[i].mpc_pwm.pwm_enable = meson_pwm_enable; sc->sc_pwmchan[i].mpc_pwm.pwm_get_config = meson_pwm_get_config; sc->sc_pwmchan[i].mpc_pwm.pwm_set_config = meson_pwm_set_config; sc->sc_pwmchan[i].mpc_pwm.pwm_dev = self; sc->sc_pwmchan[i].mpc_pwm.pwm_priv = &sc->sc_pwmchan[i]; meson_pwm_get_current(sc, i, &sc->sc_pwmchan[i].mpc_conf); } fdtbus_register_pwm_controller(self, phandle, &meson_pwm_funcs); } CFATTACH_DECL_NEW(meson_pwm, sizeof(struct meson_pwm_softc), meson_pwm_match, meson_pwm_attach, NULL, NULL);