diff --git a/targets/TARGET_RASPBERRYPI/TARGET_RP2040/analogin_api.c b/targets/TARGET_RASPBERRYPI/TARGET_RP2040/analogin_api.c index a89123f0b0a..9e8729f20f5 100644 --- a/targets/TARGET_RASPBERRYPI/TARGET_RP2040/analogin_api.c +++ b/targets/TARGET_RASPBERRYPI/TARGET_RP2040/analogin_api.c @@ -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) { diff --git a/targets/TARGET_RASPBERRYPI/TARGET_RP2040/objects.h b/targets/TARGET_RASPBERRYPI/TARGET_RP2040/objects.h index 6c793dcdd1e..578a5628cc2 100644 --- a/targets/TARGET_RASPBERRYPI/TARGET_RP2040/objects.h +++ b/targets/TARGET_RASPBERRYPI/TARGET_RP2040/objects.h @@ -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; }; diff --git a/targets/TARGET_RASPBERRYPI/TARGET_RP2040/pwmout_api.c b/targets/TARGET_RASPBERRYPI/TARGET_RP2040/pwmout_api.c index e4bc4daa806..dd08c7abc2d 100644 --- a/targets/TARGET_RASPBERRYPI/TARGET_RP2040/pwmout_api.c +++ b/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" @@ -45,7 +25,77 @@ #include "hardware/clocks.h" #include "mbed_assert.h" -const uint count_top = 1000; +#include + +// 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 +#include +#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 * @@ -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); @@ -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); } @@ -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 @@ -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 @@ -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. @@ -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. @@ -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. @@ -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() diff --git a/targets/targets.json5 b/targets/targets.json5 index 57df8503104..af4e3d0c27e 100644 --- a/targets/targets.json5 +++ b/targets/targets.json5 @@ -4876,7 +4876,12 @@ "device_name": "STM32U575ZITx", "detect_code": [ "886" - ] + ], + "overrides": { + // As shipped, this nucleo board connects VREFP to VDD_MCU, and connects VDD_MCU to 3.3V. + // Jumper JP4 can be used to switch VDD_MCU to 1.8V in which case you should override this setting to 1.8. + "default-adc-vref": 3.3 + } }, "MCU_STM32U585xI": { "inherits": [ @@ -9758,7 +9763,7 @@ "RTC" ] }, - "RASPBERRY_PI_PICO_SWD": { + "RASPBERRY_PI_PICO": { "inherits": ["RP2040"], "macros_add": [ "PICO_RP2040_USB_DEVICE_ENUMERATION_FIX=1", @@ -9766,13 +9771,24 @@ "PICO_TIME_DEFAULT_ALARM_POOL_DISABLED", "PICO_ON_DEVICE=1", "PICO_UART_ENABLE_CRLF_SUPPORT=0" - ] - }, - "RASPBERRY_PI_PICO": { - "inherits": ["RASPBERRY_PI_PICO_SWD"], + ], "overrides": { "console-usb": true, - "console-uart": false + "console-uart": false, + + // ADC_VDD sets the ADC reference voltage on this chip. + // Most RP2040 boards set this pin to 3.3V. + // Note that if the I/O voltage is set to less than the ADC reference voltage, + // voltages higher than the I/O voltage are illegal for the analog pins + // (so the ADC can never read 100%). + "default-adc-vref": 3.3 + } + }, + "RASPBERRY_PI_PICO_SWD": { + "inherits": ["RASPBERRY_PI_PICO"], + "overrides": { + "console-usb": false, + "console-uart": true } } }