rp2040: hardware PWM support

This implements hardware PWM support for the rp2040. The maximum
value(100% duty) is set to 255 to match the other controllers. Cycle
time is clamped automatically, and uses the full 8.4 fractional range of
the rp2040 PWM block. This allows a maximum PWM frequency of 490kHz and
a minimum frequency of 1915 Hz.

Signed-off-by: Lasse Dalegaard <dalegaard@gmail.com>
This commit is contained in:
Lasse Dalegaard 2021-07-04 21:30:24 +02:00 committed by KevinOConnor
parent 4802c6d86a
commit 28f60f7ef6
4 changed files with 111 additions and 0 deletions

View File

@ -10,6 +10,7 @@ config RP2040_SELECT
select HAVE_GPIO_BITBANGING select HAVE_GPIO_BITBANGING
select HAVE_STRICT_TIMING select HAVE_STRICT_TIMING
select HAVE_CHIPID select HAVE_CHIPID
select HAVE_GPIO_HARD_PWM
config BOARD_DIRECTORY config BOARD_DIRECTORY
string string

View File

@ -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/usbserial.c generic/usb_cdc.c
src-$(CONFIG_USBSERIAL) += rp2040/chipid.c src-$(CONFIG_USBSERIAL) += rp2040/chipid.c
src-$(CONFIG_SERIAL) += rp2040/serial.c generic/serial_irq.c src-$(CONFIG_SERIAL) += rp2040/serial.c generic/serial_irq.c
src-$(CONFIG_HAVE_GPIO_HARD_PWM) += rp2040/hard_pwm.c
# rp2040 stage2 building # rp2040 stage2 building
$(OUT)stage2.o: lib/rp2040/boot_stage2/boot2_w25q080.S $(OUT)stage2.o: lib/rp2040/boot_stage2/boot2_w25q080.S

View File

@ -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); void gpio_in_reset(struct gpio_in g, int8_t pull_up);
uint8_t gpio_in_read(struct gpio_in g); 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 { struct gpio_adc {
uint8_t chan; uint8_t chan;
}; };

101
src/rp2040/hard_pwm.c Normal file
View File

@ -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);
}