aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/rp2040/Kconfig1
-rw-r--r--src/rp2040/Makefile1
-rw-r--r--src/rp2040/gpio.h8
-rw-r--r--src/rp2040/hard_pwm.c101
4 files changed, 111 insertions, 0 deletions
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 <dalegaard@gmail.com>
+//
+// 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);
+}