diff --git a/ports/raspberrypi/common-hal/pwmio/PWMOut.c b/ports/raspberrypi/common-hal/pwmio/PWMOut.c index 07591012c587..e0572eccd9e4 100644 --- a/ports/raspberrypi/common-hal/pwmio/PWMOut.c +++ b/ports/raspberrypi/common-hal/pwmio/PWMOut.c @@ -176,6 +176,10 @@ extern void common_hal_pwmio_pwmout_set_duty_cycle(pwmio_pwmout_obj_t *self, uin } else { compare_count = ((uint32_t)duty * self->top + MAX_TOP / 2) / MAX_TOP; } + // do not allow count to be 0 (due to rounding) unless duty 0 was requested + if (compare_count == 0 && duty != 0) { + compare_count = 1; + } // compare_count is the CC register value, which should be TOP+1 for 100% duty cycle. pwm_set_chan_level(self->slice, self->ab_channel, compare_count); } @@ -218,8 +222,8 @@ void common_hal_pwmio_pwmout_set_frequency(pwmio_pwmout_obj_t *self, uint32_t fr pwm_set_clkdiv_int_frac(self->slice, div16 / 16, div16 % 16); pwm_set_wrap(self->slice, self->top); } else { - uint32_t top = common_hal_mcu_processor_get_frequency() / frequency; - self->actual_frequency = common_hal_mcu_processor_get_frequency() / top; + uint32_t top = common_hal_mcu_processor_get_frequency() / frequency - 1; + self->actual_frequency = common_hal_mcu_processor_get_frequency() / (top + 1); self->top = MIN(MAX_TOP, top); pwm_set_clkdiv_int_frac(self->slice, 1, 0); // Set TOP register. For 100% duty cycle, CC must be set to TOP+1. diff --git a/tests/circuitpython-manual/pwmio/README.md b/tests/circuitpython-manual/pwmio/README.md index 11bcd4de6651..511424c8f6c6 100644 --- a/tests/circuitpython-manual/pwmio/README.md +++ b/tests/circuitpython-manual/pwmio/README.md @@ -2,7 +2,7 @@ This directory contains tools for testing CircuitPython's PWM API. Running the tests involves three steps: -1. [CircuitPython PWM test code `code.py`](code.py) is run on the board to be tested. +1. [CircuitPython PWM test code `code_ramps.py`](code_ramps.py) is run on the board to be tested. 2. As the code runs, the state of the PWM signal is logged by a logic analyzer (I used a Saleae Logic Pro 8). 3. Data collected by the logic analyzer is analyzed and plotted into a PNG image by [CPython code `duty.py`](duty.py). @@ -37,3 +37,9 @@ These tests can be used to assess how well the PWM API delivers expected behavio The plot at the top of this page depicts data PWM gathered from a device with an API that displays all of the expected behavior listed above. The plots below show how the tools reveal flaws in the behavior of PWM APIs that are not as complete. + +## Testing always-off and always-on PWM extremes + +The procedure described above does not test item 2 above, i.e. the ability of the API to support duty cycles of 0% and 100%. A different code file, (code_extremes.py) provides for this. This code cycles through PWM duty cycles of 32767, 65535, 1, 65534, and 0, repeating the sequence at six frequencies from 100 Hz to 10MHz. When viewed on a logic analyzer, the PWM output should look like the figure below. 100% and 0% PWM result from duty cycle settings of 65535 and 0, (and only from those settings, in accordance with item 3 above.) + + diff --git a/tests/circuitpython-manual/pwmio/code_extremes.py b/tests/circuitpython-manual/pwmio/code_extremes.py new file mode 100644 index 000000000000..13dd5c90ad1a --- /dev/null +++ b/tests/circuitpython-manual/pwmio/code_extremes.py @@ -0,0 +1,87 @@ +import board +import pwmio +import random +import time +import microcontroller +import os +import sys +import random + +exponents = [ + 2, + 3, + 4, + 5, + 6, + 7, +] + +freqs = [int(10**f) for f in exponents] + +top = 65536 +den = 10 +duties = [32767, 65535, 1, 65534, 0, 0] +freq_duration = 1.2 +duty_duration = 0.2 + +print("\n\n") +board_name = sys.implementation[2] + +pins = { + "Feather RP2040": (("D4", ""),), + "RP2040-Zero": (("GP15", ""),), + "Grand Central": (("D51", "TCC"), ("A15", "TC")), + "Metro M0": (("A2", "TC"), ("A3", "TCC")), + "ESP32-S3-DevKit": (("IO6", ""),), # marked D5 on board for XIAO-ESP32-s3 + "Feather ESP32-S2": (("D9", ""),), + "XIAO nRF52840": (("D9", ""),), +} + +for board_key in pins: + if board_key in board_name: + pins_to_use = pins[board_key] + break + +while True: + for pin_name, pin_type in pins_to_use: + pin = getattr(board, pin_name) + print('title="', end="") + print(f"{board_name} at {microcontroller.cpu.frequency} Hz, pin {pin_name}", end="") + if len(pin_type) > 0: + print(f" ({pin_type})", end="") + print('",') + print(f'subtitle="{freq_duration:0.1f}s per frequency",') + print(f'version="{sys.version}",') + print("freq_calls=", end="") + pwm = pwmio.PWMOut(pin, variable_frequency=True) + t0 = time.monotonic() + duty_time = t0 + duty_duration + print("(", end="") + offset = 0 + increment = 1 + for freq in freqs: + i = 0 + try: + pwm.frequency = freq + except ValueError: + break + freq_time = t0 + freq_duration + duty_time = t0 + duty_duration + j = 0 + while time.monotonic() < freq_time: + duty = duties[j] + pwm.duty_cycle = duty + while time.monotonic() < duty_time and time.monotonic() < freq_time: + pass + j += 1 + j = min(j, len(duties) - 1) + duty_time += duty_duration + i += 1 + if time.monotonic() > freq_time: + break + t0 = freq_time + print(f"({freq}, {i/freq_duration:.0f}), ", end="") + print(")") + print("done.") + pwm.deinit() + time.sleep(5) diff --git a/tests/circuitpython-manual/pwmio/code.py b/tests/circuitpython-manual/pwmio/code_ramps.py similarity index 98% rename from tests/circuitpython-manual/pwmio/code.py rename to tests/circuitpython-manual/pwmio/code_ramps.py index b7f10670e8be..6a06e4eff6bb 100644 --- a/tests/circuitpython-manual/pwmio/code.py +++ b/tests/circuitpython-manual/pwmio/code_ramps.py @@ -39,6 +39,7 @@ board_name = sys.implementation[2] pins = { + "Feather RP2040": (("D4", ""),), "RP2040-Zero": (("GP15", ""),), "Grand Central": (("D51", "TCC"), ("A15", "TC")), "Metro M0": (("A2", "TC"), ("A3", "TCC")), diff --git a/tests/circuitpython-manual/pwmio/pwm_extremes_explainer.png b/tests/circuitpython-manual/pwmio/pwm_extremes_explainer.png new file mode 100644 index 000000000000..25f30251976d Binary files /dev/null and b/tests/circuitpython-manual/pwmio/pwm_extremes_explainer.png differ