Skip to content

esp32/PWM: Reduce inconsitencies between ports. #10854

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

IhorNehrutsa
Copy link
Contributor

@IhorNehrutsa IhorNehrutsa commented Feb 25, 2023

This PR is developed according to the: PWM: Reduce inconsistencies between ports. #10817

  1. duty_u16() high value is 2**16-1 == 65535

  2. Invert PWM wave with invert=1 parameter

  3. Enable PWM in light sleep mode like in
    esp32/machine_pwm: Enhancement of PWM: Add features light_sleep_enable. #16102
    Test code 1
    Test code 2

  4. Allows PWM output and the pulse-input is the same pin.

pwm26 = PWM(Pin(26)) # PWM wave starts on pin 26 immediately.
print(pwm26) 
>>> PWM(Pin(26), freq=5000, duty=512)

pwm26.duty_u16(0) # LOW level on the pin
pwm26.duty_u16(2**16-1) # HIGH level on the pin

pwm26.duty_ns(100)

pwm26 = PWM(Pin(26), freq=10_000, duty_u16=16384)            # The output is at a high level 25% of the time.
pwm23 = PWM(Pin(23), freq=10_000, duty_u16=16384, invert=1)  # The output is at a low level 25% of the time.

pwm4 = PWM(Pin(4), light_sleep_enable=True) # Allow PWM during light sleep mode

@IhorNehrutsa IhorNehrutsa marked this pull request as draft February 25, 2023 15:20
@github-actions
Copy link

github-actions bot commented Feb 28, 2023

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +9 +0.002% VIRT_RV32

@robert-hh
Copy link
Contributor

Wouldn't it be better to put DEBUG printing into a separate PR?

@IhorNehrutsa
Copy link
Contributor Author

Wouldn't it be better to put DEBUG printing into a separate PR?
Yes, DEBUG will be removed in filal.

@codecov-commenter
Copy link

codecov-commenter commented Mar 2, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (4a2e510) 98.36% compared to head (6022240) 98.36%.
Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #10854   +/-   ##
=======================================
  Coverage   98.36%   98.36%           
=======================================
  Files         159      159           
  Lines       21090    21090           
=======================================
  Hits        20745    20745           
  Misses        345      345           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@IhorNehrutsa
Copy link
Contributor Author

V30303-161405.mp4

@robert-hh
Copy link
Contributor

robert-hh commented Mar 3, 2023

I fetched the PR, compiled & loaded it. What I found somewhat unexpected was the error message after the sequence:

from machine import PWM, Pip
p = PWM(Pin(4))

Which said: ValueError: frequency must be from 1Hz to 40MHz, since I did not enter a frequency. I recall well that you did not want to implement the above case, and I know what caused the message, Still it's confusing. And you still should consider harmonizing the behavior to the other ports with machine.PWM in that respect.

What has to be discussed beside that is the method duty(). The range is different for the ports which support it. 1024 for ESP32, 256 for ESP8266, 100 for nrf and 10000 for CC3200. The duty() method was intentionally not implemented any more at the newer ports. And i may be dropped in a release later than 1.20 (nickname Godot).

@IhorNehrutsa
Copy link
Contributor Author

  • This PR is in a draft state under construction. WIP - work in process.
from machine import PWM, Pin

p = PWM(Pin(4))
print(p)

output is

PWM(Pin(4), freq=5000, duty=512)  # resolution=13, (duty=50.00%, resolution=0.012%), mode=0, channel=0, timer=0

The duty cycle is between 0 and 1023 inclusive.

Earlier in a discussion, someone said that 1023 is too small for his application case, so duty=100 in nrf port is small too.
duty=0..10000 breaks backward compatibility for ESP32 and ESP8266.

0..10000 is attractive for noobs
but 10000 will recalc to 2^16 and one step in range 0..10000 will produce dissimilar, inregular 6 or 7 steps in range 0..2^16.

print(' 0..10_000   0..2**16       step')
x_prev = 0
for i in range(0, 10_000 + 1):
    x = round(i * 2**16 / 10_000)
    step = x - x_prev
    print(f'{i:10} {x:10} {step:10}')
    x_prev = x

output is

 0..10_000   0..2**16       step
         0          0          0
         1          7          7
         2         13          6
         3         20          7
         4         26          6
         5         33          7
         6         39          6
         7         46          7
         8         52          6
         9         59          7
        10         66          7
        11         72          6
        12         79          7
...

I vote for the duty [0..1023] range.

@robert-hh
Copy link
Contributor

pwm.duty() is kept for legacy only in the ports that had it before duty_us() was introduced. It will be dropped some day. For instance with the announced version 2.x, which will have breaking changes. So no reason to change it at the moment.

IhorNehrutsa referenced this pull request Mar 5, 2023
If setting the frequency to a value used already by an existing timer, this
timer will be used.  But still, the duty cycle for that channel may have to
be changed.

Fixes issues #8306 and #8345.
@IhorNehrutsa
Copy link
Contributor Author

from utime import sleep
from machine import Pin, PWM

F_MIN = 100
F_MAX = 1000

f = F_MIN
delta_f = 100

p = PWM(Pin(27), f)

while True:
    p.freq(f)
    print(p)

    sleep(.2)

    f += delta_f
    if f > F_MAX or f < F_MIN:
        delta_f = -delta_f
        print()
        if f > F_MAX:
            f = F_MAX
        elif f < F_MIN:
            f = F_MIN
V30309-134257.mp4

@IhorNehrutsa
Copy link
Contributor Author

from utime import sleep
from machine import Pin, PWM

DUTY_MAX = 2**16 - 1

duty_u16 = 0
delta_d = 16

p = PWM(Pin(5), 1000, duty_u16=duty_u16)
print(p)

while True:
    p.duty_u16(duty_u16)

    sleep(1 / 1000)

    duty_u16 += delta_d
    if duty_u16 >= DUTY_MAX:
        duty_u16 = DUTY_MAX
        delta_d = -delta_d
    elif duty_u16 <= 0:
        duty_u16 = 0
        delta_d = -delta_d
V30309-141247.mp4

@IhorNehrutsa IhorNehrutsa force-pushed the pwm_reduce_inconsist branch from 983b093 to eb33120 Compare March 9, 2023 13:41
@IhorNehrutsa IhorNehrutsa marked this pull request as ready for review March 9, 2023 14:47
@IhorNehrutsa IhorNehrutsa force-pushed the pwm_reduce_inconsist branch from 7d821bb to 265b82e Compare March 10, 2023 07:44
@robert-hh
Copy link
Contributor

@IhorNehrutsa I have a question regarding the PWM module. Reading the various ESP32 reference manuals I learned, the the ESP32 has 3 to 8 timers, which can be assigned to 6 to 16 PWM channels, which then can be assigned to GPIO pins. Pretty flexible. Since the timer and channel to be used cannot be selected in the constructor, the selection of timer an channel is kind of automatic. What is the policy you have implemented? Is there some way to control this assignment?

@IhorNehrutsa
Copy link
Contributor Author

=======================================================  ========  ========  ========
Hardware specification                                      ESP32  ESP32-S2  ESP32-C3
                                                                   ESP32-S3  ESP32-H2
-------------------------------------------------------  --------  --------  --------
Number of groups (speed modes)                                  2         1         1
Number of timers per group                                      4         4         4
Number of channels per group                                    8         8         6
-------------------------------------------------------  --------  --------  --------
Different PWM frequencies = (groups * timers)                   8         4         4
Total PWM channels (Pins, duties) = (groups * channels)        16         8         6
=======================================================  ========  ========  ========

Try to find a timer with the same frequency in the current mode, otherwise in the next mode.
If no existing timer and channel were found, then try to find a free timer in any mode.
If the mode or channel is changed, release the current channel and select(bind) a new channel in this mode.

    from time import sleep
    from machine import Pin, PWM
    try:
        f = 100  # Hz
        d = 2**16 // 16  # 6.25%
        pins = (2, 4, 12, 13, 14, 15, 16, 18, 19, 22, 23, 25, 26, 27, 32, 33)
        pwms = []
        for i, pin in enumerate(pins):
            pwms.append(PWM(Pin(pin), freq=f, duty_u16=min(2**16 - 1, d * (i + 1))))
            print(pwms[i])
        sleep(60)
    finally:
        for pwm in pwms:
            try:
                pwm.deinit()
            except:
                pass

Output is::

    PWM(Pin(2), freq=100, duty_u16=4096)  # resolution=16, (duty=6.25%, resolution=0.002%), mode=0, channel=0, timer=0
    PWM(Pin(4), freq=100, duty_u16=8192)  # resolution=16, (duty=12.50%, resolution=0.002%), mode=0, channel=1, timer=0
    PWM(Pin(12), freq=100, duty_u16=12288)  # resolution=16, (duty=18.75%, resolution=0.002%), mode=0, channel=2, timer=0
    PWM(Pin(13), freq=100, duty_u16=16384)  # resolution=16, (duty=25.00%, resolution=0.002%), mode=0, channel=3, timer=0
    PWM(Pin(14), freq=100, duty_u16=20480)  # resolution=16, (duty=31.25%, resolution=0.002%), mode=0, channel=4, timer=0
    PWM(Pin(15), freq=100, duty_u16=24576)  # resolution=16, (duty=37.50%, resolution=0.002%), mode=0, channel=5, timer=0
    PWM(Pin(16), freq=100, duty_u16=28672)  # resolution=16, (duty=43.75%, resolution=0.002%), mode=0, channel=6, timer=0
    PWM(Pin(18), freq=100, duty_u16=32768)  # resolution=16, (duty=50.00%, resolution=0.002%), mode=0, channel=7, timer=0
    PWM(Pin(19), freq=100, duty_u16=36864)  # resolution=16, (duty=56.25%, resolution=0.002%), mode=1, channel=0, timer=0
    PWM(Pin(22), freq=100, duty_u16=40960)  # resolution=16, (duty=62.50%, resolution=0.002%), mode=1, channel=1, timer=0
    PWM(Pin(23), freq=100, duty_u16=45056)  # resolution=16, (duty=68.75%, resolution=0.002%), mode=1, channel=2, timer=0
    PWM(Pin(25), freq=100, duty_u16=49152)  # resolution=16, (duty=75.00%, resolution=0.002%), mode=1, channel=3, timer=0
    PWM(Pin(26), freq=100, duty_u16=53248)  # resolution=16, (duty=81.25%, resolution=0.002%), mode=1, channel=4, timer=0
    PWM(Pin(27), freq=100, duty_u16=57344)  # resolution=16, (duty=87.50%, resolution=0.002%), mode=1, channel=5, timer=0
    PWM(Pin(32), freq=100, duty_u16=61440)  # resolution=16, (duty=93.75%, resolution=0.002%), mode=1, channel=6, timer=0
    PWM(Pin(33), freq=100, duty_u16=65535)  # resolution=16, (duty=100.00%, resolution=0.002%), mode=1, channel=7, timer=0

See https://github.com/IhorNehrutsa/micropython/blob/pwm_reduce_inconsist/docs/esp32/tutorial/pwm.rst

@robert-hh
Copy link
Contributor

Thank you for the explanation and the example code. It is very convenient. The strategy looks fine for an automatic assignment. It may as well be helpful to specify timer and channel in the constructor replacing the automatic search, such that one can have synchronous channels at a timer.

@robert-hh
Copy link
Contributor

robert-hh commented Mar 15, 2023

@IhorNehrutsa In order to make invert=x effective even with pwm.init(invert=x), I tried two changes.

  1. Saving the previous state.
    #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
    int save_output_invert = self->output_invert;
    self->output_invert = args[ARG_invert].u_int == 0 ? 0 : 1;
    #endif

  1. Test, whether it has been changed further down in the code.
    if ((chans[mode][channel].pin < 0)
        || (save_mode != self->mode)  // && (save_mode >= 0))
        || (save_channel != self->channel)  // && (save_channel >= 0))
        || (save_timer != self->timer)  // && (save_timer >= 0)))
    #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
        || (save_output_invert != self->output_invert)
    #endif
    ) {
        configure_channel(self);
    }

You could consider whether you skip the test for changes and just configure the channel always when you call init().

@IhorNehrutsa IhorNehrutsa force-pushed the pwm_reduce_inconsist branch from 265b82e to 490b8ea Compare March 24, 2023 13:33
@robert-hh
Copy link
Contributor

The esp32 board build fails with esp-idf v4.0.2 because of this include in machine_pwm.c:
#include "soc/soc_caps.h"
When I comment this line, I can build with versions v4.0.2 and as well with v4.2.2 and v4.4.2. Other versions should work as well, but I did not try. Since the code can be built without that include, it seems not to be required.

@robert-hh
Copy link
Contributor

Please restrict the changes in this PR to the machine.PWM module and do not add unrelated code like for debug printing. These affect all other ports as well. I you consider that useful for everyone, please open a separate PR for it.

@IhorNehrutsa
Copy link
Contributor Author

IhorNehrutsa commented Jul 17, 2023

Please restrict the changes in this PR to the machine.PWM module and do not add unrelated code like for debug printing.

MP_PRN() will removed in release

@robert-hh
Copy link
Contributor

At the moment I can only use ESP IDF 5.0.2. Switching back to 4.4.2 is not possible. Since you PR fails to build with IDF 5.0.2, I cannot buils & test it.

@IhorNehrutsa
Copy link
Contributor Author

Tested ESP IDF 5.0.2, 5.0.3, 5.1
image

@IhorNehrutsa
Copy link
Contributor Author

less infos are printed out...

See commit

Update

#define MICROPY_ERROR_REPORTING             (MICROPY_ERROR_REPORTING_NORMAL+1)

to get

PWM(Pin(4), freq=5001, duty=256, invert=True, lightsleep=True)  # duty=25.00%, raw_duty=512, resolution=11, mode=0, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK

@IhorNehrutsa
Copy link
Contributor Author

lightsleep test output:

=====================
ESP32-C3 detected
=====================

Test #1: Create all available channels
PWM(Pin(2), freq=100, duty=64)  # duty=6.25%, raw_duty=512, resolution=13, mode=0, timer=0, channel=0, clk_cfg=4=APB_CLK
PWM(Pin(3), freq=100, duty=128)  # duty=12.50%, raw_duty=1024, resolution=13, mode=0, timer=0, channel=1, clk_cfg=4=APB_CLK
PWM(Pin(4), freq=200, duty=192)  # duty=18.75%, raw_duty=1536, resolution=13, mode=0, timer=1, channel=2, clk_cfg=4=APB_CLK
PWM(Pin(5), freq=200, duty=256)  # duty=25.00%, raw_duty=2048, resolution=13, mode=0, timer=1, channel=3, clk_cfg=4=APB_CLK
PWM(Pin(6), freq=300, duty=320)  # duty=31.25%, raw_duty=2560, resolution=13, mode=0, timer=2, channel=4, clk_cfg=4=APB_CLK
PWM(Pin(7), freq=300, duty=384)  # duty=37.50%, raw_duty=3072, resolution=13, mode=0, timer=2, channel=5, clk_cfg=4=APB_CLK
finally:
PWM(Pin(7))
PWM(Pin(6))
PWM(Pin(5))
PWM(Pin(4))
PWM(Pin(3))
PWM(Pin(2))
[]

Test #2: Create all available channels in sleep mode
PWM(Pin(2), freq=100, duty=64, lightsleep=True)  # duty=6.25%, raw_duty=512, resolution=13, mode=0, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK
PWM(Pin(3), freq=100, duty=128, lightsleep=True)  # duty=12.50%, raw_duty=1024, resolution=13, mode=0, timer=0, channel=1, clk_cfg=8=RC_FAST_CLK
PWM(Pin(4), freq=200, duty=192, lightsleep=True)  # duty=18.75%, raw_duty=1536, resolution=13, mode=0, timer=1, channel=2, clk_cfg=8=RC_FAST_CLK
PWM(Pin(5), freq=200, duty=256, lightsleep=True)  # duty=25.00%, raw_duty=2048, resolution=13, mode=0, timer=1, channel=3, clk_cfg=8=RC_FAST_CLK
PWM(Pin(6), freq=300, duty=320, lightsleep=True)  # duty=31.25%, raw_duty=2560, resolution=13, mode=0, timer=2, channel=4, clk_cfg=8=RC_FAST_CLK
PWM(Pin(7), freq=300, duty=384, lightsleep=True)  # duty=37.50%, raw_duty=3072, resolution=13, mode=0, timer=2, channel=5, clk_cfg=8=RC_FAST_CLK
finally:
PWM(Pin(7))
PWM(Pin(6))
PWM(Pin(5))
PWM(Pin(4))
PWM(Pin(3))
PWM(Pin(2))
[]

Test #3: Create 2 channel, one in light_sleep mode, second default both shloud run with clock=RC_FAST_CLK
PWM(Pin(2), freq=10002, duty=512, lightsleep=True)  # duty=50.00%, raw_duty=512, resolution=10, mode=0, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK
PWM(Pin(3), freq=10002, duty=512)  # duty=50.00%, raw_duty=512, resolution=10, mode=0, timer=0, channel=1, clk_cfg=8=RC_FAST_CLK
finally:
PWM(Pin(2))
PWM(Pin(3))

Test #4: Create 2 channel, one default, second in light_sleep mode shloud generate an exception on the second one
PWM(Pin(2), freq=10000, duty=512)  # duty=50.00%, raw_duty=2048, resolution=12, mode=0, timer=0, channel=0, clk_cfg=4=APB_CLK
Traceback (most recent call last):
  File "<stdin>", line 126, in <module>
RuntimeError: one or more active timers use a different clock source, not supported by the current SoC.
Clock conflict detected !
finally:
PWM(Pin(2))
PWM(Pin(3))

@yoann-darche
Copy link

MICROPY_ERROR_REPORTING

Ok understand, but from my point of view, these information are not errors, but can be usefull for electronic developers. Shall we keep it as normal reporting ?

@IhorNehrutsa
Copy link
Contributor Author

At one of the iterations of the two-year code review, one of the reviewers suggested just such a solution. (I am not sure.)

Users use compiled bin files, and developers increase the MICROPY_ERROR_REPORTING and compile the code as they need.

@robert-hh
Copy link
Contributor

robert-hh commented Feb 20, 2025

At one of the iterations of the two-year code review, one of the reviewers suggested just such a solution. (I am not sure.)

I did not make that comment, but as general rule the information printed for an object should be able to be fed into a init() method. Needless to say that there is much legacy stuff that does not meet this rule.

@yoann-darche
Copy link

yoann-darche commented Feb 20, 2025

I've a use case that fails due to the mixture of Clocks :

from machine import PWM, Pin, lightsleep

a=PWM(Pin(4), freq=3000, duty=256, lightsleep=True)
print(a)

f=PWM(Pin(2), freq=2000, duty=64)
print(f)

lightsleep(20*1000) # Led goes off

The excepted functionality is the Pin(4) should be set in order to continue to run under lighsleep(), and the pin(2) should to stop under lightsleep().

but I got this error du too the miss selecting of the active clock:

MPY: soft reboot
PWM(Pin(4), freq=2998, duty=512, lightsleep=True)  # duty=50.00%, raw_duty=1024, resolution=12, mode=0, timer=0, channel=0, clk_cfg=9=RC_FAST_CLK
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
RuntimeError: one or more active timers use a different clock source, not supported by the current SoC.

The thing that I've proposed : if an timer is already set with RC_FAST_CLK clock, all other time need to use it either if lightsleep is true or not, except if the SOC support High speed mode where each group can use a separate clock source.

EDIT: After several tests, this case happend when the frequency differs so when it is need to create a new timer source.

@IhorNehrutsa
Copy link
Contributor Author

general rule the information printed for an object should be able to be fed into a init() method.

And even more

pwm1 = PWM(Pin(XXX), freq=500, duty_u16=327)
pwm
>>> PWM(Pin(XXX), freq=500, duty_u16=327)
s = str(pwm)
do STORE/LOAD s to/from drive   or   SEND/RECIEVE s via serial 
pwm2 = eval(s)
pwm2
>>> PWM(Pin(XXX), freq=500, duty_u16=327)

@IhorNehrutsa
Copy link
Contributor Author

MPY: soft reboot
MicroPython v1.25.0-preview.306.g117fedc6a.dirty on 2025-02-21; ESP32C3 module with ESP32C3

Type "help()" for more information.

>>> %run -c $EDITOR_CONTENT
PWM(Pin(4), freq=3002, duty=256, lightsleep=True)
PWM(Pin(5), freq=2001, duty=64)
>>> 

@yoann-darche
Copy link

yoann-darche commented Feb 21, 2025

Perfect now it is working as expected on ESP32-S3, I've also check the output signal with a scope, and it is working. @IhorNehrutsa great job !

@IhorNehrutsa
Copy link
Contributor Author

Thank you very much @yoann-darche @robert-hh

@yoann-darche
Copy link

yoann-darche commented Feb 21, 2025

Test on ESP32-WROVER-E:

=====================
ESP32-GENERIC detected
=====================

Test #1: Create all available channels
PWM(Pin(15), freq=100, duty=64)  # duty=6.25%, raw_duty=4096, resolution=16, mode=0, timer=0, channel=0, clk_cfg=4=APB_CLK
PWM(Pin(2), freq=100, duty=128)  # duty=12.50%, raw_duty=8192, resolution=16, mode=0, timer=0, channel=1, clk_cfg=4=APB_CLK
PWM(Pin(4), freq=200, duty=192)  # duty=18.75%, raw_duty=12288, resolution=16, mode=0, timer=1, channel=2, clk_cfg=4=APB_CLK
PWM(Pin(16), freq=200, duty=256)  # duty=25.00%, raw_duty=16384, resolution=16, mode=0, timer=1, channel=3, clk_cfg=4=APB_CLK
PWM(Pin(18), freq=300, duty=320)  # duty=31.25%, raw_duty=20480, resolution=16, mode=0, timer=2, channel=4, clk_cfg=4=APB_CLK
PWM(Pin(19), freq=300, duty=384)  # duty=37.50%, raw_duty=24576, resolution=16, mode=0, timer=2, channel=5, clk_cfg=4=APB_CLK
PWM(Pin(22), freq=400, duty=448)  # duty=43.75%, raw_duty=28672, resolution=16, mode=0, timer=3, channel=6, clk_cfg=4=APB_CLK
PWM(Pin(23), freq=400, duty=512)  # duty=50.00%, raw_duty=32768, resolution=16, mode=0, timer=3, channel=7, clk_cfg=4=APB_CLK
PWM(Pin(25), freq=500, duty=576)  # duty=56.25%, raw_duty=36864, resolution=16, mode=1, timer=0, channel=0, clk_cfg=4=APB_CLK
PWM(Pin(26), freq=500, duty=640)  # duty=62.50%, raw_duty=40960, resolution=16, mode=1, timer=0, channel=1, clk_cfg=4=APB_CLK
PWM(Pin(27), freq=600, duty=704)  # duty=68.75%, raw_duty=45056, resolution=16, mode=1, timer=1, channel=2, clk_cfg=4=APB_CLK
PWM(Pin(14), freq=600, duty=768)  # duty=75.00%, raw_duty=49152, resolution=16, mode=1, timer=1, channel=3, clk_cfg=4=APB_CLK
PWM(Pin(12), freq=701, duty=832)  # duty=81.25%, raw_duty=53248, resolution=16, mode=1, timer=2, channel=4, clk_cfg=4=APB_CLK
PWM(Pin(13), freq=701, duty=896)  # duty=87.50%, raw_duty=57344, resolution=16, mode=1, timer=2, channel=5, clk_cfg=4=APB_CLK
PWM(Pin(32), freq=799, duty=960)  # duty=93.75%, raw_duty=61440, resolution=16, mode=1, timer=3, channel=6, clk_cfg=4=APB_CLK
PWM(Pin(33), freq=799, duty=1023)  # duty=100.00%, raw_duty=65536, resolution=16, mode=1, timer=3, channel=7, clk_cfg=4=APB_CLK
finally:
PWM(Pin(33))
PWM(Pin(32))
PWM(Pin(13))
PWM(Pin(12))
PWM(Pin(14))
PWM(Pin(27))
PWM(Pin(26))
PWM(Pin(25))
PWM(Pin(23))
PWM(Pin(22))
PWM(Pin(19))
PWM(Pin(18))
PWM(Pin(16))
PWM(Pin(4))
PWM(Pin(2))
PWM(Pin(15))
[]

Test #2: Create all available channels in sleep mode
PWM(Pin(15), freq=100, duty=64, lightsleep=True)  # duty=6.25%, raw_duty=4096, resolution=16, mode=1, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK
PWM(Pin(2), freq=100, duty=128, lightsleep=True)  # duty=12.50%, raw_duty=8192, resolution=16, mode=1, timer=0, channel=1, clk_cfg=8=RC_FAST_CLK
PWM(Pin(4), freq=200, duty=192, lightsleep=True)  # duty=18.75%, raw_duty=6144, resolution=15, mode=1, timer=1, channel=2, clk_cfg=8=RC_FAST_CLK
PWM(Pin(16), freq=200, duty=256, lightsleep=True)  # duty=25.00%, raw_duty=8192, resolution=15, mode=1, timer=1, channel=3, clk_cfg=8=RC_FAST_CLK
PWM(Pin(18), freq=300, duty=320, lightsleep=True)  # duty=31.25%, raw_duty=5120, resolution=14, mode=1, timer=2, channel=4, clk_cfg=8=RC_FAST_CLK
PWM(Pin(19), freq=300, duty=384, lightsleep=True)  # duty=37.50%, raw_duty=6144, resolution=14, mode=1, timer=2, channel=5, clk_cfg=8=RC_FAST_CLK
PWM(Pin(22), freq=400, duty=448, lightsleep=True)  # duty=43.75%, raw_duty=7168, resolution=14, mode=1, timer=3, channel=6, clk_cfg=8=RC_FAST_CLK
PWM(Pin(23), freq=400, duty=512, lightsleep=True)  # duty=50.00%, raw_duty=8192, resolution=14, mode=1, timer=3, channel=7, clk_cfg=8=RC_FAST_CLK
Traceback (most recent call last):
  File "<stdin>", line 71, in <module>
RuntimeError: out of light sleep capable PWM channels:8
In case of ESP32, only low speed mode can use RC_FAST_CLK, so only 8 channels available.
finally:
PWM(Pin(23))
PWM(Pin(22))
PWM(Pin(19))
PWM(Pin(18))
PWM(Pin(16))
PWM(Pin(4))
PWM(Pin(2))
PWM(Pin(15))
[]

Test #3: Create 2 channel, one in light_sleep mode, second default both shloud run with clock=RC_FAST_CLK
PWM(Pin(15), freq=9991, duty=512, lightsleep=True)  # duty=50.00%, raw_duty=256, resolution=9, mode=1, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK
PWM(Pin(2), freq=9991, duty=512)  # duty=50.00%, raw_duty=256, resolution=9, mode=1, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK
finally:
PWM(Pin(15))
PWM(Pin(2))

Test #4: Create 2 channel, one default, second in light_sleep mode shloud generate an exception on the second one
PWM(Pin(15), freq=10000, duty=512)  # duty=50.00%, raw_duty=2048, resolution=12, mode=0, timer=0, channel=0, clk_cfg=4=APB_CLK
PWM(Pin(2), freq=9991, duty=512, lightsleep=True)  # duty=50.00%, raw_duty=256, resolution=9, mode=1, timer=0, channel=0, clk_cfg=8=RC_FAST_CLK
finally:
PWM(Pin(15))
PWM(Pin(2))

I've also test the lightsleep mode and again it is working as expected 👍. Please note the last test (#4) that exploit the double clock source available on ESP32... nice !

@IhorNehrutsa
Copy link
Contributor Author

IMHO. This PR is ready for merging.

@projectgus
Copy link
Contributor

IMHO. This PR is ready for merging.

Thanks @IhorNehrutsa. This is marked for the 1.26 release. The 1.25 release is being finalised now, and we won't merge such a big change so late in the release. However, expect to see some movement on this after 1.25 is out.

@IhorNehrutsa
Copy link
Contributor Author

The 2nd anniversary since the opening of PR! Congratulations!

@kdschlosser
Copy link

as general rule the information printed for an object should be able to be fed into a init() method. Needless to say that there is much legacy stuff that does not meet this rule.

This should be something that is wrapped by a macro to enable or disable it. Those functions take up space when they may never get used. As MicroPython grows in size from added features so does the program size and this is one of those things that really should be an option and not a requirement if only for the sole purpose of decreasing the program size.

If it could be eliminated from the back end removing the field where the function pointer is stored that would also decrease memory use as well. Granted it's not a lot of memory saved per object but if there are lots of objects then the memory use does add up very quickly.

@IhorNehrutsa
Copy link
Contributor Author

@DavideRossi
Please give feedback for the #10854

@DavideRossi
Copy link

I applied the PR and the invert PWM parameter worked as expected (on an ESP32-S3).
The ability to produce antiphase PWM signals can be very useful and, given the fact that this is already supported for other microcontrollers, I only see good reasons to accept the PR.

@IhorNehrutsa
Copy link
Contributor Author

@DavideRossi
If you are interested in complementary PWM with dead time look to the
esp32/MCPWM: Add motor control MCPWM driver. #12345

IhorNehrutsa and others added 3 commits March 13, 2025 09:23
According to the: micropython#10817.

Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
esp32/machine_pwm.c: Reduce PWM inconsistencies between ports.

1. duty_u16() high value is 2**16-1 == 65535
2. Invert PWM wave with invert=1 parameter
3. Enable PWM in light sleep mode
4. Allows to output PWM and read pulse input
simultaneously on the same Pin()
5. Code refactoring

Co-Authored-By: Angus Gratton <angus@redyak.com.au>
Co-Authored-By: robert-hh <robert@hammelrath.com>
Co-Authored-By: Andrew Leech <andrew.leech@planetinnovation.com.au>
Co-Authored-By: Yoann Darche <yoannd@hotmail.com>

Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
Debugging Note: Increase the MICROPY_ERROR_REPORTING level
to view _FUNCTION__, __LINE__, __FILE__ in check_esp_err() exceptions.

Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
@IhorNehrutsa IhorNehrutsa force-pushed the pwm_reduce_inconsist branch from 10e53b5 to 9ea0573 Compare March 13, 2025 07:27
@yoann-darche
Copy link

@IhorNehrutsa : We may need to add an example of the light_sleep mode in the documentation. We can reuse our test code used upper in this thread.

docs/esp32/quickref.rst: Add lightsleep() PWM example.

Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.