From patchwork Sat May 18 03:54:45 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Watts X-Patchwork-Id: 1936656 X-Patchwork-Delegate: andre.przywara@arm.com Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=pass (2048-bit key; unprotected) header.d=jookia.org header.i=@jookia.org header.a=rsa-sha256 header.s=key1 header.b=UYxn4qxS; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.denx.de (client-ip=2a01:238:438b:c500:173d:9f52:ddab:ee01; helo=phobos.denx.de; envelope-from=u-boot-bounces@lists.denx.de; receiver=patchwork.ozlabs.org) Received: from phobos.denx.de (phobos.denx.de [IPv6:2a01:238:438b:c500:173d:9f52:ddab:ee01]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4Vh93Y5xdJz20KF for ; Sat, 18 May 2024 13:55:53 +1000 (AEST) Received: from h2850616.stratoserver.net (localhost [IPv6:::1]) by phobos.denx.de (Postfix) with ESMTP id A1DEC885E2; Sat, 18 May 2024 05:55:34 +0200 (CEST) Authentication-Results: phobos.denx.de; dmarc=pass (p=quarantine dis=none) header.from=jookia.org Authentication-Results: phobos.denx.de; spf=pass smtp.mailfrom=u-boot-bounces@lists.denx.de Authentication-Results: phobos.denx.de; dkim=pass (2048-bit key; unprotected) header.d=jookia.org header.i=@jookia.org header.b="UYxn4qxS"; dkim-atps=neutral Received: by phobos.denx.de (Postfix, from userid 109) id C0C74885E2; Sat, 18 May 2024 05:55:33 +0200 (CEST) X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on phobos.denx.de X-Spam-Level: X-Spam-Status: No, score=-2.1 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,SPF_HELO_NONE,SPF_NONE autolearn=ham autolearn_force=no version=3.4.2 Received: from out-183.mta0.migadu.com (out-183.mta0.migadu.com [IPv6:2001:41d0:1004:224b::b7]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) (No client certificate requested) by phobos.denx.de (Postfix) with ESMTPS id 7EFE28858C for ; Sat, 18 May 2024 05:55:31 +0200 (CEST) Authentication-Results: phobos.denx.de; dmarc=pass (p=quarantine dis=none) header.from=jookia.org Authentication-Results: phobos.denx.de; spf=none smtp.mailfrom=contact@jookia.org X-Envelope-To: contact@jookia.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=jookia.org; s=key1; t=1716004530; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=Qk3SNdNBg2M/QY93M1A9mO2QJ2c+bB4dG80J2aMOAsY=; b=UYxn4qxS8n2V3suezsWAXh90NZDRl7ms2hv6+YULbE6p/iWcFtadfwxHQdF/EwHCyYALhD blHsKJ45Fimp34m2+ZOpRb1YUrty5MMBtfHx0po1Ug7ph4CNpFQTVyAg1E4+tJBF5ot1m3 18k9qarQiZTxj594Ti5B//wBuN59xisjiZml5k/8QKOIaeEkGOUpdBBuRltpTAMldt0loA vMu61YhYvbZW5eJ3BjayW0kjyX+Y/rN3P90zOJcm9gSQighmlzD3ogiS/54ZZF+p/1BTCT G2fV+Wo41VaTi+zCJLGxyrH7PrOsc3RHGa5j4UbvuDhZmTsibDxm2txxFZRTvg== X-Envelope-To: lukma@denx.de X-Envelope-To: rick@andestech.com X-Envelope-To: seanga2@gmail.com X-Envelope-To: ycliang@andestech.com X-Envelope-To: sumit.garg@linaro.org X-Envelope-To: u-boot@lists.denx.de X-Envelope-To: trini@konsulko.com X-Envelope-To: andre.przywara@arm.com X-Envelope-To: jagan@amarulasolutions.com X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. From: John Watts Date: Sat, 18 May 2024 13:54:45 +1000 Subject: [PATCH RFC 3/4] pwm: sunxi: Add support Allwinner D1 PWM MIME-Version: 1.0 Message-Id: <20240518-pwm_d1-v1-3-311fc5fe2248@jookia.org> References: <20240518-pwm_d1-v1-0-311fc5fe2248@jookia.org> In-Reply-To: <20240518-pwm_d1-v1-0-311fc5fe2248@jookia.org> To: Jagan Teki , Andre Przywara , Lukasz Majewski , Sean Anderson , Tom Rini , Rick Chen , Leo , Sumit Garg Cc: u-boot@lists.denx.de, John Watts X-Developer-Signature: v=1; a=openssh-sha256; t=1716004494; l=16510; i=contact@jookia.org; h=from:subject:message-id; bh=rF1ho91g+ytJviWcTmGLMSq9UeQjQQYtDiUc7OseDnk=; b=U1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgpGuA3uho2 8zVxm554DVLHyl4gq5/nBHglU5WIWN8/zYAAAAEc3NoOgAAAAZwYXRhdHQAAAAAAAAABnNoYTUx MgAAAGcAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAABABwi6tykqSKPfjxmV3diN1+j Ckfw9oiD1oeJ/aVOZ1JSAMMRuoSMadWTvPX4NW+bYMd59BIJTcyeNhQqPTBdvAgUAAPRa X-Developer-Key: i=contact@jookia.org; a=openssh; fpr=SHA256:6LBQmZH5u7i/edmEZXzCTwCrpSevs/ZshZxNmlD1thY X-Migadu-Flow: FLOW_OUT X-BeenThere: u-boot@lists.denx.de X-Mailman-Version: 2.1.39 Precedence: list List-Id: U-Boot discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: u-boot-bounces@lists.denx.de Sender: "U-Boot" X-Virus-Scanned: clamav-milter 0.103.8 at phobos.denx.de X-Virus-Status: Clean This driver documents and handles setting up PWM on the D1. Signed-off-by: John Watts --- drivers/pwm/Kconfig | 6 + drivers/pwm/Makefile | 1 + drivers/pwm/sunxi_pwm_d1.c | 542 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 549 insertions(+) diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 6e79868d0e..8c4c910ea7 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -112,6 +112,12 @@ config PWM_SUNXI This PWM is found on H3, A64 and other Allwinner SoCs. It supports a programmable period and duty cycle. A 16-bit counter is used. +config PWM_SUNXI_D1 + bool "Enable support for the Allwinner D1 Sunxi PWM" + depends on DM_PWM + help + This PWM is found on D1, T113-S3 and R329 SoCs. + config PWM_TI_EHRPWM bool "Enable support for EHRPWM PWM" depends on DM_PWM && ARCH_OMAP2PLUS diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index e4d10c8dc3..ea96e7159b 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -23,4 +23,5 @@ obj-$(CONFIG_PWM_SANDBOX) += sandbox_pwm.o obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o obj-$(CONFIG_PWM_TEGRA) += tegra_pwm.o obj-$(CONFIG_PWM_SUNXI) += sunxi_pwm.o +obj-$(CONFIG_PWM_SUNXI_D1) += sunxi_pwm_d1.o obj-$(CONFIG_PWM_TI_EHRPWM) += pwm-ti-ehrpwm.o diff --git a/drivers/pwm/sunxi_pwm_d1.c b/drivers/pwm/sunxi_pwm_d1.c new file mode 100644 index 0000000000..6c57bc6e85 --- /dev/null +++ b/drivers/pwm/sunxi_pwm_d1.c @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright 2022 Jookia +/* + * The Allwinner D1's PWM channels are 16-bit counters with up to + * 65537 cycles (yes, you read that correctly). + * + * Each channel must be programmed using three variables: + * - The entire cycle count (used for the period) + * - The active cycle count (the count of inactive cycles) + * - The polarity (specifies if the signal is active high or low) + * The cycle counts are at minimum 1 and at maximum 65536. + * + * The controller will output the number of entire cycles plus one + * extra, with any cycles after the active cycle output as active. + * + * Consider a PWM period of 128 nanoseconds and a cycle period of 32. + * Setting the entire cycle count to 3 and active cycle count to 4 + * gives an output like so: + * + * - Cycle 1 runs 0 to 32 nanoseconds, inactive + * - Cycle 2 runs 32 to 64 nanoseconds, inactive + * - Cycle 3 runs 64 to 96 nanoseconds, inactive + * - Cycle 4 runs 96 to 128 nanoseconds, inactive + * - Cycle 5 is skipped but would run 128 to 160 nanoseconds, active + * + * If we set the entire count to 4, cycle 5 would run and we wouldn't be + * able to specify it as inactive as the active count only goes up to 4. + * + * In practice this means we want to set the entire cycle to be one less + * then the actual number of cycles we want, so we can set the number of + * active cycles to be up to maximum for a fully inactive signal. + * + * The PWM channels are paired and clocked together, resulting in a + * cycle time found using the following formula: + * + * PWM0_CYCLE_NS = 1000000000 / (BUS_CLOCK / COMMON_DIV / PWM0_PRESCALER_K) + * PWM1_CYCLE_NS = 1000000000 / (BUS_CLOCK / COMMON_DIV / PWM1_PRESCALER_K) + * + * This means both clocks should ideally be set at the same time and not + * impact each other too much. + */ + +#include +#include +#include +#include +#include +#include +#include + +/* PWM channel information */ +struct pwm_channel { + uint period_ns; + uint duty_ns; + bool polarity; + bool enable; + bool updated; +}; + +/* Timings found for a PWM channel */ +struct pwm_timings { + uint cycle_ns; + uint period_ns; + uint duty_ns; + uint clock_id; + uint common_div; + uint prescale_k; + uint entire_cycles; + uint active_cycles; + uint polarity; +}; + +/* Driver state */ +struct sunxi_pwm_d1_priv { + void *base; + struct clk *clk_bus; + struct clk *clk_srcs[3]; /* Last value must be NULL */ + struct reset_ctl *reset; + int npwm; + struct pwm_channel *channels; +}; + +/* Divides a nanosecond value, rounding up for very low values */ +uint div_ns(uint ns, uint div) +{ + uint result = (ns / div); + + /* If the number is less than 1000, round it to the nearest digit */ + if (result < 1000) + result = (ns + (div - 1)) / div; + + if (result < 1) + result = 1; + + return result; +} + +/* Checks if an error is relatively too large */ +int error_too_large(uint actual, uint target) +{ + /* For a target of zero we want zero */ + if (target == 0) + return (actual == 0); + + /* Don't overflow large numbers when we multiply by 100 */ + while (actual > 1000) { + actual /= 100; + target /= 100; + } + + int error_percent = (actual * 100) / target; + + return (error_percent < 80 || 120 < error_percent); +} + +/* Calculates the cycle nanoseconds from clock parameters */ +int get_cycle_ns(uint parent_hz, uint common_div, uint prescaler) +{ + return 1000000000 / ((parent_hz / common_div) / prescaler); +} + +int find_channel_dividers(uint period_ns, + uint parent_hz, + struct pwm_timings *out) +{ + uint ideal_cycle_ns = div_ns(period_ns, 65536); + int common_div = out->common_div; + int prescaler = 1; + uint cycle_ns = 0; + + for (;;) { + cycle_ns = get_cycle_ns(parent_hz, common_div, prescaler); + if (cycle_ns >= ideal_cycle_ns) + break; + + prescaler *= 2; + if (prescaler > 256) { + if (common_div < 256) { + prescaler = 1; + common_div *= 2; + } else { + return -1; + } + } + } + + out->common_div = common_div; + out->prescale_k = prescaler; + out->cycle_ns = cycle_ns; + + return 0; +} + +int find_channel_timings(const struct pwm_channel *in, + struct pwm_timings *out, + uint parent_hz) +{ + struct pwm_timings new = *out; + + if (find_channel_dividers(in->period_ns, parent_hz, &new)) + return -1; + + new.entire_cycles = (in->period_ns / new.cycle_ns) - 1; + new.active_cycles = (in->duty_ns / new.cycle_ns); + new.period_ns = (new.entire_cycles + 1) * new.cycle_ns; + new.duty_ns = new.active_cycles * new.cycle_ns; + new.polarity = in->polarity; + + if (error_too_large(new.period_ns, in->period_ns)) + return -1; + + if (in->duty_ns && error_too_large(new.duty_ns, in->duty_ns)) + return -1; + + *out = new; + + return 0; +} + +int find_pair_timings(const struct pwm_channel *channel0, + const struct pwm_channel *channel1, + struct pwm_timings *timings0, + struct pwm_timings *timings1, + int clock_hz) +{ + struct pwm_timings new0 = *timings0; + struct pwm_timings new1 = *timings1; + int err0 = 0; + int err1 = 0; + + new0.common_div = 1; + new1.common_div = 1; + + if (channel0->enable) { + err0 = find_channel_timings(channel0, &new0, clock_hz); + new1.common_div = new0.common_div; + } + + if (channel1->enable) { + err1 = find_channel_timings(channel1, &new1, clock_hz); + new0 = *timings0; + new0.common_div = new1.common_div; + } + + if (channel0->enable && channel1->enable) { + err0 = find_channel_timings(channel0, &new0, clock_hz); + + if (new0.common_div != new1.common_div) + return -1; + } + + if (err0 || err1) + return -1; + + *timings0 = new0; + *timings1 = new1; + + return 0; +} + +int find_pair_timings_clocked(struct clk **clk_srcs, + const struct pwm_channel *channel0, + const struct pwm_channel *channel1, + struct pwm_timings *timings0, + struct pwm_timings *timings1) +{ + struct clk *clock = *clk_srcs; + + for (int clock_id = 0; clock; clock = clk_srcs[++clock_id]) { + int clock_hz = clk_get_rate(clock); + + if (clock_hz == 0 || IS_ERR_VALUE(clock_hz)) + continue; + + timings0->clock_id = clock_id; + timings1->clock_id = clock_id; + + if (find_pair_timings(channel0, channel1, + timings0, timings1, + clock_hz)) + continue; + + return 0; + } + + return -1; +} + +#define PCGR(base) ((base) + 0x40) +#define PCGR_CLK_GATE(channel) BIT(channel) + +#define PER(base) ((base) + 0x80) +#define PER_ENABLE_PWM(channel) BIT(channel) + +#define PCCR(base, pair) ((base) + 0x20 + ((pair) * 2)) +#define PCCR_CLK_SRC(src) ((src) << 7) +#define PCCR_CLK_SRC_MASK GENMASK(8, 7) +#define PCCR_CLK_DIV_M(m) (m) +#define PCCR_CLK_DIV_M_MASK GENMASK(3, 0) + +#define PCR(base, channel) ((base) + 0x100 + ((channel) * 0x20)) +#define PCR_PRESCAL_K(k) (k) +#define PCR_PRESCAL_K_MASK GENMASK(7, 0) +#define PCR_PWM_ACTIVE BIT(8) + +#define PPR(base, channel) ((base) + 0x104 + ((channel) * 0x20)) +#define PPR_ENTIRE_CYCLE(n) ((n) << 16) +#define PPR_ENTIRE_CYCLE_MASK GENMASK(31, 16) +#define PPR_ACT_CYCLE(n) (n) +#define PPR_ACT_CYCLE_MASK GENMASK(15, 0) + +/* Like clrsetbits_le32 but with memory barriers */ +void clrsetreg(void *addr, u32 clear, u32 set) +{ + u32 val = readl(addr); + + val &= ~clear; + val |= set; + + writel(val, addr); +} + +void disable_pair(void *base, int pair) +{ + u32 PER_clear = (PER_ENABLE_PWM(pair) | PER_ENABLE_PWM(pair + 1)); + u32 PCGR_clear = (PCGR_CLK_GATE(pair) | PCGR_CLK_GATE(pair + 1)); + + clrsetreg(PER(base), PER_clear, 0); + clrsetreg(PCGR(base), PCGR_clear, 0); + + log_debug("%s: pair %i, PCGR 0x%x, PER 0x%x\n", + __func__, pair, readl(PCGR(base)), readl(PER(base))); +} + +void enable_pair(void *base, int pair, int clk_src, int clk_div) +{ + int div_m = fls(clk_div) - 1; + + u32 PCGR_set = (PCGR_CLK_GATE(pair) | PCGR_CLK_GATE(pair + 1)); + u32 PCCR_clear = (PCCR_CLK_SRC_MASK | PCCR_CLK_DIV_M_MASK); + u32 PCCR_set = (PCCR_CLK_SRC(clk_src) | PCCR_CLK_DIV_M(div_m)); + + clrsetreg(PCGR(base), 0, PCGR_set); + clrsetreg(PCCR(base, pair), PCCR_clear, PCCR_set); + + log_debug("%s: pair %i, clk_src %i, div_m %i, PCCR 0x%x\n", + __func__, pair, clk_src, div_m, readl(PCCR(base, pair))); +} + +void enable_channel(void *base, int channel, struct pwm_timings *timings) +{ + u32 pwm_active = (timings->polarity) ? 0 : PCR_PWM_ACTIVE; + u32 prescale = (timings->prescale_k - 1); + u32 entire_cycles = timings->entire_cycles; + u32 active_cycles = timings->active_cycles; + + u32 PCR_clear = (PCR_PRESCAL_K_MASK | PCR_PWM_ACTIVE); + u32 PCR_set = (PCR_PRESCAL_K(prescale) | pwm_active); + u32 PPR_clear = (PPR_ENTIRE_CYCLE_MASK | PPR_ACT_CYCLE_MASK); + u32 PPR_set = (PPR_ENTIRE_CYCLE(entire_cycles) | PPR_ACT_CYCLE(active_cycles)); + u32 PER_set = PER_ENABLE_PWM(channel); + + clrsetreg(PCR(base, channel), PCR_clear, PCR_set); + clrsetreg(PPR(base, channel), PPR_clear, PPR_set); + clrsetreg(PER(base), 0, PER_set); + + log_debug("%s: channel %u, clock_id %u, period_ns %u, duty_ns %u, common_div %u, prescale_k %u, entire_cycles %u, active_cycles %u, polarity %u, PCGR 0x%x, PCR 0x%x, PPR 0x%x, PER 0x%x\n", + __func__, channel, timings->clock_id, timings->period_ns, + timings->duty_ns, timings->common_div, timings->prescale_k, + timings->entire_cycles, timings->active_cycles, + timings->polarity, readl(PCGR(base)), + readl(PCR(base, channel)), readl(PPR(base, channel)), + readl(PER(base))); +} + +int update_channel_pair(struct sunxi_pwm_d1_priv *priv, int pair) +{ + struct pwm_timings timings0 = {0}; + struct pwm_timings timings1 = {0}; + struct pwm_channel *channel0 = &priv->channels[pair + 0]; + struct pwm_channel *channel1 = &priv->channels[pair + 1]; + void *base = priv->base; + + if (channel0->updated && channel1->updated) + return 0; + + disable_pair(base, pair); + + if (find_pair_timings_clocked(priv->clk_srcs, channel0, channel1, &timings0, &timings1)) + return -1; + + if (channel0->enable || channel1->enable) + enable_pair(base, pair, timings0.clock_id, timings0.common_div); + + if (channel0->enable) + enable_channel(base, pair + 0, &timings0); + + if (channel1->enable) + enable_channel(base, pair + 1, &timings1); + + channel0->updated = true; + channel1->updated = true; + + return 0; +} + +static int update_channels(struct udevice *dev) +{ + struct sunxi_pwm_d1_priv *priv = dev_get_priv(dev); + int i; + + for (i = 0; i < priv->npwm; i += 2) { + int ret = update_channel_pair(priv, i); + + if (ret != 0) + return ret; + } + + return 0; +} + +static int sunxi_pwm_d1_set_invert(struct udevice *dev, uint channel_num, + bool polarity) +{ + struct sunxi_pwm_d1_priv *priv = dev_get_priv(dev); + struct pwm_channel *channel; + + if (channel_num >= priv->npwm) + return -EINVAL; + + channel = &priv->channels[channel_num]; + channel->updated = (channel->polarity == polarity); + channel->polarity = polarity; + + return update_channels(dev); +} + +static int sunxi_pwm_d1_set_config(struct udevice *dev, uint channel_num, + uint period_ns, uint duty_ns) +{ + struct sunxi_pwm_d1_priv *priv = dev_get_priv(dev); + struct pwm_channel *channel; + + if (channel_num >= priv->npwm) + return -EINVAL; + + channel = &priv->channels[channel_num]; + channel->updated = (channel->period_ns == period_ns && channel->duty_ns == duty_ns); + channel->period_ns = period_ns; + channel->duty_ns = duty_ns; + + return update_channels(dev); +} + +static int sunxi_pwm_d1_set_enable(struct udevice *dev, uint channel_num, bool enable) +{ + struct sunxi_pwm_d1_priv *priv = dev_get_priv(dev); + struct pwm_channel *channel; + + if (channel_num >= priv->npwm) + return -EINVAL; + + channel = &priv->channels[channel_num]; + channel->updated = (channel->enable == enable); + channel->enable = enable; + + return update_channels(dev); +} + +static int sunxi_pwm_d1_of_to_plat(struct udevice *dev) +{ + struct sunxi_pwm_d1_priv *priv = dev_get_priv(dev); + struct clk *clk_hosc; + struct clk *clk_apb0; + int ret; + + priv->base = dev_read_addr_ptr(dev); + + if (!priv->base) { + dev_err(dev, "Unset device tree offset?\n"); + return -EINVAL; + } + + priv->clk_bus = devm_clk_get(dev, "bus"); + + if (IS_ERR(priv->clk_bus)) { + dev_err(dev, "failed to get bus clock: %ld", + PTR_ERR(priv->clk_bus)); + return PTR_ERR(priv->clk_bus); + } + + ret = clk_enable(priv->clk_bus); + + if (ret) { + dev_err(dev, "failed to enable bus clk: %d", ret); + return ret; + } + + clk_hosc = devm_clk_get(dev, "hosc"); + + if (IS_ERR(clk_hosc)) { + dev_err(dev, "failed to get hosc clock: %ld", + PTR_ERR(clk_hosc)); + return PTR_ERR(clk_hosc); + } + + clk_apb0 = devm_clk_get(dev, "apb0"); + + if (IS_ERR(clk_apb0)) { + dev_err(dev, "failed to get apb0 clock: %ld", + PTR_ERR(clk_apb0)); + return PTR_ERR(clk_apb0); + } + + priv->clk_srcs[0] = clk_hosc; + priv->clk_srcs[1] = clk_apb0; + priv->clk_srcs[2] = NULL; + + priv->reset = devm_reset_control_get(dev, NULL); + + if (IS_ERR(priv->reset)) { + dev_err(dev, "failed to get reset: %ld", + PTR_ERR(priv->reset)); + return PTR_ERR(priv->reset); + } + + priv->npwm = 8; + ret = dev_read_u32(dev, "allwinner,pwm-channels", &priv->npwm); + + if (ret < 0 && ret != -EINVAL) { + dev_err(dev, "failed to read allwinner,pwm-channels: %d", + ret); + return ret; + } + + priv->channels = devm_kzalloc(dev, + sizeof(struct pwm_channel) * priv->npwm, + GFP_KERNEL); + + if (!priv->channels) { + dev_err(dev, "failed to read allocate pwm channels"); + return -ENOMEM; + } + + return 0; +} + +static int sunxi_pwm_d1_probe(struct udevice *dev) +{ + struct sunxi_pwm_d1_priv *priv = dev_get_priv(dev); + int ret; + + ret = reset_deassert(priv->reset); + + if (ret < 0) { + dev_err(dev, "failed to deassert reset: %d", ret); + return ret; + } + + return update_channels(dev); +} + +static const struct pwm_ops sunxi_pwm_d1_ops = { + .set_invert = sunxi_pwm_d1_set_invert, + .set_config = sunxi_pwm_d1_set_config, + .set_enable = sunxi_pwm_d1_set_enable, +}; + +static const struct udevice_id sunxi_pwm_d1_ids[] = { + { .compatible = "allwinner,sun20i-d1-pwm" }, + { } +}; + +U_BOOT_DRIVER(sunxi_pwm_d1) = { + .name = "sunxi_pwm_d1", + .id = UCLASS_PWM, + .of_match = sunxi_pwm_d1_ids, + .ops = &sunxi_pwm_d1_ops, + .of_to_plat = sunxi_pwm_d1_of_to_plat, + .probe = sunxi_pwm_d1_probe, + .priv_auto = sizeof(struct sunxi_pwm_d1_priv), +};