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 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

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.

Note: MicroPython-lib PR Add Stepper Motor PWM-Counter driver. requires this PR.

@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
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS

@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 marked this pull request as ready for review March 9, 2023 14:47
@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().

@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

@florentbr
Copy link

I noticed the change from 65535 to 65536 to support the 100% duty by using the overflow value.
From the doc:

On ESP32-S3, when channel's binded timer selects its maximum duty resolution, the duty cycle value cannot be set to (2 ** duty_resolution). Otherwise, the internal duty counter in the hardware will overflow and be messed up.

Since you capped the resolution to 16bits, it's not going to be an issue with the 20 bits counter of the ESP32.
However, other boards like the ESP32 S3 and ESP32 C3 have a 14 bits counter.
I could be wrong, but I think the 100% duty will fail with those boards at maximum resolution.

@robert-hh
Copy link
Contributor

but I think the 100% duty will fail with those boards at maximum resolution.

It works as expected. At 65536 the output is constant high, at 0 it's constant low.
The duty_u16 resolution of the API is not related to the internal resolution, which may very drastically with the frequency.

@IhorNehrutsa
Copy link
Contributor Author

@florentbr
When duty_u16 is 65536, then duty set to 0 and invert output to high level.

    int max_duty = 1 << timer->duty_resolution;
    if (self->channel_duty == max_duty) {
        cfg.duty = 0;
        cfg.flags.output_invert = self->output_invert ^ 1;
    }

@florentbr
Copy link

@robert-hh

but I think the 100% duty will fail with those boards at maximum resolution.

It works as expected. At 65536 the output is constant high, at 0 it's constant low. The duty_u16 resolution of the API is not related to the internal resolution, which may very drastically with the frequency.

I was referring to the internal resolution which is related to duty_u16 by the scaling:

>>> hex(65535 * ((1 << 14) - 1) // 65535)
0x3FFF   # 14 bits max value, small pulse at 100%
>>> hex(65536 * (1 << 14) // 65536)
0x4000   # 14 bits overflow, unreachable, giving 100% duty if 15bits supported

@IhorNehrutsa

When duty_u16 is 65536, then duty set to 0 and invert output to high level.

Thanks, so the 100% overflow is not used but converted a 0% inverted.
That's a clever trick which deserves a comment in the code.

@robert-hh
Copy link
Contributor

robert-hh commented Jan 10, 2024

@IhorNehrutsa It may be better to hold back the last commit until the esp-idf version is changed and the build succeeds. Otherwise it cannot be merged.
P.S.: I know that this PR is pending since a long time (like many others), even if it a major improvement, and I do not know if and when it would be merged.

@robert-hh
Copy link
Contributor

robert-hh commented Jan 10, 2024

The last update works down to 3Hz on as ESP32S3 with esp-idf 5.0.4. Still formatting errors at the code and commit message,

@beyonlo
Copy link

beyonlo commented Jan 10, 2024

The last update works down to 3Hz on as ESP32S3 with esp-idf 5.0.4. Still formatting errors at the code and commit message,

@robert-hh Thank you for testing. When used with esp-idf 5.1.2 will the ESP32-S3 reach >= 1.04Hz?

@IhorNehrutsa
Copy link
Contributor Author

ESP32-S3 with esp-idf 5.1.2 allows 2Hz

@IhorNehrutsa IhorNehrutsa force-pushed the pwm_reduce_inconsist branch 2 times, most recently from 1cdc34a to e3fdc79 Compare January 10, 2024 18:01
@robert-hh
Copy link
Contributor

Thank you for testing. When used with esp-idf 5.1.2 will the ESP32-S3 reach >= 1.04Hz?

The frequency must be an integer. It is easily possible to change the code accepting floating point numbers for Frequency which would allow a finer granularity at low frequencies. I had implemented that, but never made a PR. There are too many PRs outstanding, and there were not many requests for that higher frequency granularity (1).

@beyonlo
Copy link

beyonlo commented Jan 11, 2024

The frequency must be an integer. It is easily possible to change the code accepting floating point numbers for Frequency which would allow a finer granularity at low frequencies. I had implemented that, but never made a PR. There are too many PRs outstanding, and there were not many requests for that higher frequency granularity (1).

@robert-hh I think that a finer granularity at low frequencies is a very good feature. I wrote 1.04Hz because, as @florentbr wrote, is the minimum frequency allowed closer to 1Hz on the ESP32-S3. I believe that 2Hz will works fine in my application, but if 2Hz will not enough I would like to request a PR to accept floating point numbers to have 1.04Hz (closer to 1Hz) :)

Thank you very much!

@florentbr
Copy link

@robert-hh

The frequency must be an integer. It is easily possible to change the code accepting floating point numbers for Frequency which would allow a finer granularity at low frequencies.

In this case there's no need for floating point.
Since the requested frequency 1Hz equals the integer of the minimum possible frequency (1.04Hz), the implementation should allow the requested frequency of 1Hz by maximizing the divider/resolution instead of raising an error. At 1Hz (14bits counter 17.5MHz clock), the frequency error would be around 5% which is less compared to some higher frequencies. It would make the documented 1Hz-40MHz true for all the ESP boards.

The IDF ledc driver also takes an integer for the frequency but it has the ledc_timer_set function to set directly the divider/resolution.

@robert-hh
Copy link
Contributor

robert-hh commented Jan 11, 2024

@IhorNehrutsa Do you like to add that check to just emit the lowest supported frequency if the requested frequency is below it. AFAIK, other ports raise an error.

@IhorNehrutsa IhorNehrutsa force-pushed the pwm_reduce_inconsist branch 3 times, most recently from de194c4 to 114b067 Compare January 12, 2024 14:09
@IhorNehrutsa
Copy link
Contributor Author

ESP32-S3 with esp-idf 5.1.2 allows ~1Hz

IhorNehrutsa added 3 commits January 26, 2024 15:36
Signed-off-by: IhorNehrutsa <Ihor.Nehrutsa@gmail.com>
Signed-off-by: IhorNehrutsa <Ihor.Nehrutsa@gmail.com>
Signed-off-by: IhorNehrutsa <Ihor.Nehrutsa@gmail.com>
Signed-off-by: IhorNehrutsa <Ihor.Nehrutsa@gmail.com>
@projectgus
Copy link
Contributor

This is an automated heads-up that we've just merged a Pull Request
that removes the STATIC macro from MicroPython's C API.

See #13763

A search suggests this PR might apply the STATIC macro to some C code. If it
does, then next time you rebase the PR (or merge from master) then you should
please replace all the STATIC keywords with static.

Although this is an automated message, feel free to @-reply to me directly if
you have any questions about this.

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.

None yet

8 participants