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

esp32/PWM: Reduce inconsitencies between ports. #10854

Open
wants to merge 3 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

@robert-hh
Copy link
Contributor

robert-hh commented Feb 19, 2025

Indeed. With light_sleep_enable=True PWM continues during lightsleep. Maybe it would match better the machine method if the PWM flag is called lightsleep_enable.

Edit: Or lightsleep_keep, lightsleep_stay or just lightsleep`.

@yoann-darche
Copy link

yoann-darche commented Feb 19, 2025

@robert-hh @yoann-darche I am not completely confident in the PWM light sleep mode. May you check it in your apps?

I would be happy to help you to test it, but how can rebranch on this code (not very confident with git...) ?
What is the PR code ? I've try :
git fetch origin pull/10854/head:IhorNehrutsa:pwm_reduce_inconsist, but not working...

@robert-hh
Copy link
Contributor

Good start. Then you have to checkout the branch.

git fetch origin pull/10854/head:pwm_reduce_inconsist
git checkout pwm_reduce_inconsist

@IhorNehrutsa
Copy link
Contributor Author

Or lightsleep_keep, lightsleep_stay or just lightsleep`.

Actually, I wanted to suggest lightsleep=True|False myself.

@yoann-darche
Copy link

yoann-darche commented Feb 19, 2025

I have run a basic test that create all available channel with or wthout lightsleep enbale, and I don't get same result (especially when there is clock conflict). This is the output of the test programe (to compare with the trace in the #16102 (comment) :

=====================
ESP32-S3 detected
=====================

Test #1: Create all available channels
PWM(Pin(2), freq=100, duty=64)
PWM(Pin(3), freq=100, duty=128)
PWM(Pin(4), freq=200, duty=192)
PWM(Pin(5), freq=200, duty=256)
PWM(Pin(6), freq=300, duty=320)
PWM(Pin(7), freq=300, duty=384)
PWM(Pin(8), freq=400, duty=448)
PWM(Pin(9), freq=400, duty=512)

Test #2: Create all available channels in sleep mode
PWM(Pin(2), freq=100, duty=64, light_sleep_enable=True)
PWM(Pin(3), freq=100, duty=128, light_sleep_enable=True)
PWM(Pin(4), freq=200, duty=192, light_sleep_enable=True)
PWM(Pin(5), freq=200, duty=256, light_sleep_enable=True)
PWM(Pin(6), freq=300, duty=320, light_sleep_enable=True)
PWM(Pin(7), freq=300, duty=384, light_sleep_enable=True)
PWM(Pin(8), freq=400, duty=448, light_sleep_enable=True)
PWM(Pin(9), freq=400, duty=512, light_sleep_enable=True)

Test #3: Create 2 channel, one in light_sleep mode, second default both shloud run with clock=RC_FAST_CLK
PWM(Pin(2), freq=9999, duty=512, light_sleep_enable=True)
PWM(Pin(3), freq=9999, duty=512)

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)

We can see that the exception is not rise, and less infos are printed out...

@IhorNehrutsa
Copy link
Contributor Author

@yoann-darche
Lately, I've been mostly using GitHub Desktop on Windows and Ubuntu.
It fully meets my needs: checkout branch/PR, create branch, commit/amend, pull/push/force push, fetch, squash/cherry peek, rebase, merge, etc.

@IhorNehrutsa
Copy link
Contributor Author

Updated version of lightsleep test: #16102 comment

import os
from sys import print_exception
from time import sleep_ms
from machine import PWM, Pin, lightsleep

IS_ESP32GEN = "ESP32 " in os.uname().machine
IS_ESP32S2 = "ESP32-S2 " in os.uname().machine
IS_ESP32S3 = "ESP32S3 " in os.uname().machine
IS_ESP32C3 = "ESP32C3 " in os.uname().machine

# List available pin in function of the SoC

print("\n=====================")
if IS_ESP32S2:
    # Support only 8 channels
    pinList = (15, 2, 3, 4, 5, 6, 7, 8)
    print("ESP32-S2 detected")
elif IS_ESP32S3:
    # Support only 8 channels
    pinList = (2, 3, 4, 5, 6, 7, 8, 9)
    print("ESP32-S3 detected")
elif IS_ESP32C3:
    # Support only 6 channels
    pinList = (2, 3, 4, 5, 6, 7)
    print("ESP32-C3 detected")
elif IS_ESP32GEN:
    # support 16 channels (8 in LowSpeed and 8 in HightSpeed)
    pinList = (15, 2, 4, 16, 18, 19, 22, 23, 25, 26, 27, 14, 12, 13, 32, 33)
    print("ESP32-GENERIC detected")
else:
    raise RuntimeError

if pinList == None:
    print("Test Fail: Board not detetected !")
    exit(1)
print("=====================")
pwms = []
pLS = None
pDEF = None
 
print("\nTest #1: Create all available channels")
# From test in the documentation
try:
    f = 100  # Hz
    d = 1024 // 16  # 6.25%

    for i, pin in enumerate(pinList):
        pwms.append(PWM(Pin(pin), freq=f * (i // 2 + 1), duty=1023 if i == 15 else d * (i + 1)))
        sleep_ms(200)
        print(pwms[i])
finally:
    print("finally:")
    while len(pwms) > 0:
        p = pwms.pop()
        try:
            p.deinit()
        except Exception as e:
            print_exception(e)
            print("Error while deinit :", p)
        print(p)
    print(pwms)


print("\nTest #2: Create all available channels in sleep mode")
try:
    for i, pin in enumerate(pinList):
        pwms.append(
            PWM(
                Pin(pin),
                freq=f * (i // 2 + 1),
                duty=1023 if i == 15 else d * (i + 1),
                lightsleep=True,
            )
        )
        sleep_ms(200)
        print(pwms[i])
except Exception as e:
    print_exception(e)
    if i > 7:
        print(
            "In case of ESP32, only low speed mode can use RC_FAST_CLK, so only 8 channels available."
        )
finally:
    print("finally:")
    while len(pwms) > 0:
        p = pwms.pop()
        try:
            p.deinit()
        except Exception as e:
            print_exception(e)
            print("Error while deinit :", p)
        print(p)
    print(pwms)

print(
    "\nTest #3: Create 2 channel, one in light_sleep mode, second default both shloud run with clock=RC_FAST_CLK"
)
try:
    pLS = PWM(Pin(pinList[0]), freq=10_000, duty=512, lightsleep=True)
    sleep_ms(200)
    print(pLS)
    pDEF = PWM(Pin(pinList[1]), freq=10_000, duty=512)
    sleep_ms(200)
    print(pDEF)
finally:
    print("finally:")
    try:
        pLS.deinit()
    except:
        pass
    print(pLS)
    try:
        pDEF.deinit()
    except:
        pass
    print(pDEF)

print(
    "\nTest #4: Create 2 channel, one default, second in light_sleep mode shloud generate an exception on the second one"
)
try:
    pLS = PWM(Pin(pinList[0]), freq=10_000, duty=512)
    sleep_ms(200)
    print(pLS)
    try:
        pDEF = PWM(Pin(pinList[1]), freq=10_000, duty=512, lightsleep=True)
        sleep_ms(200)
        print(pDEF)
    except Exception as e:
        print_exception(e)
        print("Clock conflict detected !")
        if pDEF:
            pDEF.deinit()
finally:
    print("finally:")
    try:
        pLS.deinit()
    except:
        pass
    print(pLS)
    try:
        pDEF.deinit()
    except:
        pass
    print(pDEF)

@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

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 <[email protected]>
Co-Authored-By: robert-hh <[email protected]>
Co-Authored-By: Andrew Leech <[email protected]>
Co-Authored-By: Yoann Darche <[email protected]>

Signed-off-by: Ihor Nehrutsa <[email protected]>
Debugging Note: Increase the MICROPY_ERROR_REPORTING level
to view _FUNCTION__, __LINE__, __FILE__ in check_esp_err() exceptions.

Signed-off-by: Ihor Nehrutsa <[email protected]>
@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!

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.

9 participants