Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase RPi Pico PWM range and resolution to the max supported by HW #203

Merged
merged 4 commits into from Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions targets/TARGET_RASPBERRYPI/TARGET_RP2040/analogin_api.c
Expand Up @@ -22,9 +22,8 @@
#include "pinmap.h"
#include "PeripheralPins.h"

static float const ADC_VREF_VOLTAGE = 3.3f; /* 3.3V */
static uint16_t const ADC_RESOLUTION_BITS = 12;
static float const ADC_CONVERSION_FACTOR = ADC_VREF_VOLTAGE / (1 << 16);
static float const ADC_CONVERSION_FACTOR = 1.0f / (1 << 16);

void analogin_init(analogin_t *obj, PinName pin)
{
Expand Down
23 changes: 21 additions & 2 deletions targets/TARGET_RASPBERRYPI/TARGET_RP2040/objects.h
Expand Up @@ -114,12 +114,31 @@ struct spi_s {
spi_inst_t * dev;
};

struct pwmout_s {
struct pwmout_s
{
/// Pin that the PWM is being sent out on
PinName pin;

/// Slice number of this PWM (0-7). Each slice contains two channels.
/// Each slice must have the same period but can have an independent duty cycle.
uint8_t slice;

/// Channel of this PWM output on the slice (0 or 1)
uint8_t channel;
uint16_t period;

/// Value after which this PWM channel will reset to 0. This plus the clock divider controls the PWM period.
uint16_t top_count;

/// Current clock divider value that the channel is set to (hardware accepts 1-255.9375)
float clock_divider;

/// Current duty cycle percent
float percent;

/// Current period setting in floating point seconds
float period;

/// Pico HAL config structure
pwm_config cfg;
};

Expand Down
234 changes: 173 additions & 61 deletions targets/TARGET_RASPBERRYPI/TARGET_RP2040/pwmout_api.c
@@ -1,41 +1,21 @@
/*
* Copyright (c) 2018 Nordic Semiconductor ASA
* All rights reserved.
/* mbed Microcontroller Library
* Copyright (c) 2024, Arm Limited and affiliates.
* SPDX-License-Identifier: Apache-2.0
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 1. Redistributions of source code must retain the above copyright notice, this list
* of conditions and the following disclaimer.
*
* 2. Redistributions in binary form, except as embedded into a Nordic Semiconductor ASA
* integrated circuit in a product or a software update for such product, 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.
*
* 3. Neither the name of Nordic Semiconductor ASA nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* 4. This software, with or without modification, must only be used with a
* Nordic Semiconductor ASA integrated circuit.
*
* 5. Any software provided in binary or object form under this license must not be reverse
* engineered, decompiled, modified and/or disassembled.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


#if DEVICE_PWMOUT

#include "hal/pwmout_api.h"
Expand All @@ -45,7 +25,77 @@
#include "hardware/clocks.h"
#include "mbed_assert.h"

const uint count_top = 1000;
#include <math.h>

// Change to 1 to enable debug prints of what's being calculated.
// Must comment out the critical section calls in PwmOut to use.
#define RP2040_PWMOUT_DEBUG 0

#if RP2040_PWMOUT_DEBUG
#include <stdio.h>
#include <inttypes.h>
#endif

/// Largest top count value supported by hardware. Using this value will provide the highest duty cycle resolution,
/// but will limit the period to a maximum of (1 / (125 MHz / (65534 + 1)) =) 524 us
const uint16_t MAX_TOP_COUNT = 65534;

/// Value for PWM_CHn_DIV register that produces a division of 1
const uint16_t PWM_CHn_DIV_1 = 0x010;

/// Calculate the effective PWM period (in floating point seconds) based on a divider and top_count value
static float calc_effective_pwm_period(float divider, uint16_t top_count)
{
// Note: The hardware counts to top_count *inclusively*, so we have to add 1
// to get the number of clock cycles that a given top_count value will produce
return 1.0f / ((clock_get_hz(clk_sys) / divider) / (top_count + 1));
}

/// Calculate the best possible top_count value (rounding up) for a divider and a desired pwm period
static uint16_t calc_top_count_for_period(float divider, float desired_pwm_period)
{
// Derivation:
// desired_pwm_period = 1.0f / ((clock_get_hz(clk_sys) / divider) / (top_count + 1))
// desired_pwm_period = (top_count + 1) / (clock_get_hz(clk_sys) / divider)
// desired_pwm_period * (clock_get_hz(clk_sys) / divider) - 1 = top_count

long top_count_float = lroundf(desired_pwm_period * (clock_get_hz(clk_sys) / divider) - 1);
MBED_ASSERT(top_count_float <= MAX_TOP_COUNT);
return (uint16_t)top_count_float;
}

/// Calculate the best possible floating point divider value for a desired pwm period.
/// This function assumes that top_count is set to MAX_TOP_COUNT.
static float calc_divider_for_period(float desired_pwm_period)
{
// Derivation:
// (desired_pwm_period * clock_get_hz(clk_sys)) / divider - 1 = top_count
// (desired_pwm_period * clock_get_hz(clk_sys)) / divider = top_count + 1
// divider = (desired_pwm_period * clock_get_hz(clk_sys)) / (top_count + 1)

return (desired_pwm_period * clock_get_hz(clk_sys)) / (MAX_TOP_COUNT + 1);
}

/// Convert PWM divider from floating point to a fixed point number (rounding up).
/// The divider is returned as an 8.4 bit fixed point number, which is what the Pico registers use.
static uint16_t pwm_divider_float_to_fixed(float divider_float)
{
// To convert to a fixed point number, multiply by 16 and then round up
uint16_t divider_exact = ceil(divider_float * 16);

// Largest supported divider is 255 and 15/16
if(divider_exact > 0xFFF)
{
divider_exact = 0xFFF;
}
return divider_exact;
}

/// Convert PWM divider from the fixed point hardware value (8.4 bits) to a float.
static float pwm_divider_fixed_to_float(uint16_t divider_fixed)
{
return divider_fixed / 16.0f;
}

/** Initialize the pwm out peripheral and configure the pin
*
Expand All @@ -60,11 +110,10 @@ void pwmout_init(pwmout_t *obj, PinName pin)
obj->slice = pwm_gpio_to_slice_num(pin);
obj->channel = pwm_gpio_to_channel(pin);
obj->pin = pin;
obj->period = 0;
obj->top_count = MAX_TOP_COUNT;
obj->percent = 0.5f;

obj->cfg = pwm_get_default_config();
pwm_config_set_wrap(&(obj->cfg), count_top);
pwm_config_set_wrap(&(obj->cfg), obj->top_count);

pwm_init(obj->slice, &(obj->cfg), false);
gpio_set_function(pin, GPIO_FUNC_PWM);
Expand All @@ -89,7 +138,25 @@ void pwmout_free(pwmout_t *obj)
void pwmout_write(pwmout_t *obj, float percent)
{
obj->percent = percent;
pwm_set_gpio_level(obj->pin, percent * (count_top + 1));

// Per datasheet section 4.5.2.2, a period value of top_count + 1 produces 100% duty cycle
int32_t new_reset_counts = lroundf((obj->top_count + 1) * percent);

// Clamp to valid values
if(new_reset_counts > obj->top_count + 1)
{
new_reset_counts = obj->top_count + 1;
}
else if(new_reset_counts < 0)
{
new_reset_counts = 0;
}

#if RP2040_PWMOUT_DEBUG
printf("new_reset_counts: %" PRIu32 "\n", new_reset_counts);
#endif

pwm_set_chan_level(obj->slice, obj->channel, new_reset_counts);
pwm_set_enabled(obj->slice, true);
}

Expand All @@ -114,8 +181,61 @@ float pwmout_read(pwmout_t *obj)
*/
void pwmout_period(pwmout_t *obj, float period)
{
/* Set new period. */
pwmout_period_us(obj, period * 1000000);
// Two possibilities here:
// - If the period is relatively short (< about 524 us), we want to keep the clock divider at 1
// and reduce top_count to match the period
// - If the period is larger than what we can achieve with a clock divider of 1, we need to
// use a higher clock divider, then recalculate the top_count to match

// Note: For math this complex, I wasn't able to avoid using floating point values.
// This function won't be too efficient, but for now I just want something that works and
// can access the full PWM range.

if(period <= calc_effective_pwm_period(1, MAX_TOP_COUNT))
{
// Short period. Leave divider at 1 and reduce top_count to match the expected period
obj->clock_divider = 1.0f;
obj->cfg.div = PWM_CHn_DIV_1;
obj->top_count = calc_top_count_for_period(obj->clock_divider, period);
}
else
{
// Long period, need to use divider.

// Step 1: Calculate exact desired divider such that top_count would equal MAX_TOP_COUNT
float desired_divider = calc_divider_for_period(period);

// Step 2: Round desired divider upwards to the next value the hardware can do.
// We go upwards so that the top_count value can be trimmed downwards for the best period accuracy.
uint16_t divider_fixed_point = pwm_divider_float_to_fixed(desired_divider);
obj->cfg.div = divider_fixed_point;

// Step 3: Get the divider we'll actually be using as a float
obj->clock_divider = pwm_divider_fixed_to_float(divider_fixed_point);

// Step 4: For best accuracy, recalculate the top_count value using the divider.
obj->top_count = calc_top_count_for_period(obj->clock_divider, period);

#if RP2040_PWMOUT_DEBUG
printf("period = %f, desired_divider = %f\n",
period,
desired_divider);
#endif
}

// Save period for later
obj->period = period;

#if RP2040_PWMOUT_DEBUG
printf("obj->clock_divider = %f, obj->cfg.div = %" PRIu32 ", obj->top_count = %" PRIu16 "\n",
obj->clock_divider,
obj->cfg.div,
obj->top_count);
#endif

// Set the new divider and top_count values.
pwm_config_set_wrap(&(obj->cfg), obj->top_count);
pwm_init(obj->slice, &(obj->cfg), false);
}

/** Set the PWM period specified in miliseconds, keeping the duty cycle the same
Expand All @@ -126,7 +246,7 @@ void pwmout_period(pwmout_t *obj, float period)
void pwmout_period_ms(pwmout_t *obj, int period)
{
/* Set new period. */
pwmout_period_us(obj, period * 1000);
pwmout_period(obj, period / 1000.0f);
}

/** Set the PWM period specified in microseconds, keeping the duty cycle the same
Expand All @@ -136,18 +256,18 @@ void pwmout_period_ms(pwmout_t *obj, int period)
*/
void pwmout_period_us(pwmout_t *obj, int period)
{
obj->period = period;

// min_period should be 8us
uint32_t min_period = 1000000 * count_top / clock_get_hz(clk_sys);

pwm_config_set_clkdiv(&(obj->cfg), (float)period / (float)min_period);
pwm_init(obj->slice, &(obj->cfg), false);
/* Set new period. */
pwmout_period(obj, period / 1000000.0f);
}

/** Read the PWM period specified in microseconds
*
* @param obj The pwmout object
* @return A int output period
*/
int pwmout_read_period_us(pwmout_t *obj)
{
return obj->period;
return lroundf(1000000 * calc_effective_pwm_period(obj->clock_divider, obj->top_count));
}

/** Set the PWM pulsewidth specified in seconds, keeping the period the same.
Expand All @@ -157,7 +277,7 @@ int pwmout_read_period_us(pwmout_t *obj)
*/
void pwmout_pulsewidth(pwmout_t *obj, float pulse)
{
pwmout_pulsewidth_us(obj, pulse * 1000000);
pwmout_write(obj, pulse / obj->period);
}

/** Set the PWM pulsewidth specified in miliseconds, keeping the period the same.
Expand All @@ -167,7 +287,7 @@ void pwmout_pulsewidth(pwmout_t *obj, float pulse)
*/
void pwmout_pulsewidth_ms(pwmout_t *obj, int pulse)
{
pwmout_pulsewidth_us(obj, pulse * 1000);
pwmout_write(obj, (pulse * .001f) / obj->period);
}

/** Set the PWM pulsewidth specified in microseconds, keeping the period the same.
Expand All @@ -177,19 +297,11 @@ void pwmout_pulsewidth_ms(pwmout_t *obj, int pulse)
*/
void pwmout_pulsewidth_us(pwmout_t *obj, int pulse)
{
/* Cap pulsewidth to period. */
if (pulse > obj->period) {
pulse = obj->period;
}

obj->percent = (float) pulse / (float) obj->period;

/* Restart instance with new values. */
pwmout_write(obj, obj->percent);
pwmout_write(obj, (pulse * .000001f) / obj->period);
}

int pwmout_read_pulsewidth_us(pwmout_t *obj) {
return (obj->period) * (obj->percent);
return lroundf(obj->period * obj->percent * 1000000);
}

const PinMap *pwmout_pinmap()
Expand Down