diff --git a/src/rp2040/Kconfig b/src/rp2040/Kconfig index 3f178d3f..937d8f24 100644 --- a/src/rp2040/Kconfig +++ b/src/rp2040/Kconfig @@ -10,6 +10,7 @@ config RP2040_SELECT select HAVE_GPIO_BITBANGING select HAVE_STRICT_TIMING select HAVE_CHIPID + select HAVE_GPIO_HARD_PWM config BOARD_DIRECTORY string diff --git a/src/rp2040/Makefile b/src/rp2040/Makefile index 648f121d..1d419a07 100644 --- a/src/rp2040/Makefile +++ b/src/rp2040/Makefile @@ -19,6 +19,7 @@ src-y += generic/timer_irq.c rp2040/timer.c rp2040/bootrom.c src-$(CONFIG_USBSERIAL) += rp2040/usbserial.c generic/usb_cdc.c src-$(CONFIG_USBSERIAL) += rp2040/chipid.c src-$(CONFIG_SERIAL) += rp2040/serial.c generic/serial_irq.c +src-$(CONFIG_HAVE_GPIO_HARD_PWM) += rp2040/hard_pwm.c # rp2040 stage2 building $(OUT)stage2.o: lib/rp2040/boot_stage2/boot2_w25q080.S diff --git a/src/rp2040/gpio.h b/src/rp2040/gpio.h index 50b6302c..5f6a836a 100644 --- a/src/rp2040/gpio.h +++ b/src/rp2040/gpio.h @@ -19,6 +19,14 @@ struct gpio_in gpio_in_setup(uint8_t pin, int8_t pull_up); void gpio_in_reset(struct gpio_in g, int8_t pull_up); uint8_t gpio_in_read(struct gpio_in g); +struct gpio_pwm { + void *reg; + uint8_t shift; + uint32_t mask; +}; +struct gpio_pwm gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint8_t val); +void gpio_pwm_write(struct gpio_pwm g, uint32_t val); + struct gpio_adc { uint8_t chan; }; diff --git a/src/rp2040/hard_pwm.c b/src/rp2040/hard_pwm.c new file mode 100644 index 00000000..8b7c96b0 --- /dev/null +++ b/src/rp2040/hard_pwm.c @@ -0,0 +1,101 @@ +// Hardware PWM support on rp2040 +// +// Copyright (C) 2021 Lasse Dalegaard +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "autoconf.h" // CONFIG_CLOCK_FREQ +#include "command.h" // DECL_CONSTANT +#include "gpio.h" // gpio_pwm_write +#include "sched.h" // sched_shutdown +#include "internal.h" // get_pclock_frequency +#include "hardware/structs/pwm.h" // pwm_hw +#include "hardware/structs/iobank0.h" // iobank0_hw +#include "hardware/regs/resets.h" // RESETS_RESET_PWM_BITS + +#define MAX_PWM 255 +DECL_CONSTANT("PWM_MAX", MAX_PWM); + +struct gpio_pwm +gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint8_t val) { + if(pin >= 30) + shutdown("invalid gpio pin"); + + // All pins on the rp2040 can be used for PWM, there are 8 PWM + // slices and each has two channels. + pwm_slice_hw_t * slice = &pwm_hw->slice[(pin >> 1) & 0x7]; + uint8_t channel = pin & 1; + + // Map cycle_time to clock divider + // The rp2040 has an 8.4 fractional divider, so we'll map the requested + // cycle time into that. The cycle_time we receive from Klippy is in + // relation to the crystal frequency and so we need to scale it up to match + // the PWM clock. + // For better precision, we introduce a scale factor such that pclk * scale + // doesn't overflow. We then multiply by this scale factor at the beginning + // and divide by it at the end. + uint32_t pclk = get_pclock_frequency(RESETS_RESET_PWM_BITS); + uint32_t scale = 1 << __builtin_clz(pclk); + uint32_t clock_mult = (scale * get_pclock_frequency(RESETS_RESET_PWM_BITS)) + / CONFIG_CLOCK_FREQ; + uint32_t cycle_clocks = clock_mult * cycle_time; + uint32_t div_int = cycle_clocks / MAX_PWM / scale; + uint32_t div_frac = (cycle_clocks - div_int * MAX_PWM * scale) * 16 + / MAX_PWM / scale; + + // Clamp range of the divider + if(div_int > 255) { + div_int = 255; + div_frac = 15; + } else if(div_int < 1) { + div_int = 1; + div_frac = 0; + } + + uint32_t pwm_div = div_int << 4 | div_frac; + + // Enable clock + if (!is_enabled_pclock(RESETS_RESET_PWM_BITS)) + enable_pclock(RESETS_RESET_PWM_BITS); + + // If this PWM slice hasn't been set up yet, we do the full set + // up cycle. If it's already been set up however, we check that + // the cycle time requested now matches the cycle time already + // set on the slice. This allows both channels to be utilized, + // as long as their cycle times are the same. + if(!(slice->csr & PWM_CH0_CSR_EN_BITS)) { + slice->div = pwm_div; + slice->top = MAX_PWM - 1; + slice->ctr = PWM_CH0_CTR_RESET; + slice->cc = PWM_CH0_CC_RESET; + slice->csr = PWM_CH0_CSR_EN_BITS; + } else { + if (slice->div != pwm_div) + shutdown("PWM pin has different cycle time from another in " + "the same slice"); + + // PWM is already enabled on this slice, we'll check if the + // aliasing GPIO pin is already set up for PWM function. If it + // is, then we need to bail out. + uint32_t alias_pin = (~pin & 0x10) | (pin & 0xF); + uint32_t alias_ctrl = iobank0_hw->io[alias_pin].ctrl; + uint32_t alias_func = alias_ctrl & IO_BANK0_GPIO0_CTRL_FUNCSEL_BITS; + if (alias_func == IO_BANK0_GPIO0_CTRL_FUNCSEL_VALUE_PWM_A_0) + shutdown("Aliasing PWM pin already has PWM enabled"); + } + + struct gpio_pwm out; + out.reg = (void*)&slice->cc; + out.shift = channel ? PWM_CH0_CC_B_LSB : PWM_CH0_CC_A_LSB; + out.mask = channel ? PWM_CH0_CC_B_BITS : PWM_CH0_CC_A_BITS; + + gpio_peripheral(pin, IO_BANK0_GPIO0_CTRL_FUNCSEL_VALUE_PWM_A_0, 0); + gpio_pwm_write(out, val); + + return out; +} + +void +gpio_pwm_write(struct gpio_pwm g, uint32_t val) { + hw_write_masked((uint32_t*)g.reg, val << g.shift, g.mask); +}