Skip to content

PWM: Reduce inconsitencies between ports. #10817

Description

@robert-hh

Since we talked about PWM:
It seems that the ports behave different with respect to PWM. The esp32 and mimxrt ports start as soon as the PWM object is instantiated, albeit with different frequencies. The rp2, SAMD and ESP8266 ports for instance require frequency an duty cycle to be set for providing output. The nrf implementation looks kind of broken, being not able to change frequency or period and requiring period to be set even if freq was provided. No machine.PWM at the STM32 port. Not to forget the little things like no keyword arguments at the RP2 PWM constructor. Seems it's time to tidy up things. I can work on that topic, but first of all we should agree on a behavior. So the kind of common behavior would be:

  • No output until freq (or period) and duty cycle are set.
  • The constructor accepts keyword arguments.
  • deinit() stops the PWM but does not release the PWM object, and init() or setting a new freq/duty cycle restarts it again.
  • Fix the nrf port. Looks too strange.
  • Adapt the quickref doc examples to set freq and duty_u16.

Activity

  1. dpgeorge commented on Feb 22, 2023

    @dpgeorge
    Member

    No output until freq (or period) and duty cycle are set

    That makes sense. The alternative would be to have a default freq/duty which it uses on construction, but that's likely to lead to more confusion than actually be useful.

    We could also add a .start() method... but I don't think it's necessary. As you say, it can start once the freq and duty have been configured and if the user wants to delay the start (and potentially stop and restart) they can just use the .duty() method to set the duty to 0% (or 100%) to "stop" the output.

    But still, we need to define what it actually means for a PWM object to be created and not yet started, because then it has two (possibly three) states: created but not yet running, running, and deinit'd. Eg, when the PWM is created on a pin but not yet running (no freq and/or duty) does it make that pin an output? High or low? And what exactly does .deinit() do, what state is the pin in?

    The constructor accepts keyword arguments

    Yes, that would be consistent with all the other machine classes.

  2. added
    extmodRelates to extmod/ directory in source
    on Feb 22, 2023
  3. robert-hh commented on Feb 23, 2023

    @robert-hh
    ContributorAuthor

    We could also add a .start() method...

    init() without arguments can (and does on some ports) restart PWM again, after it has been stopped by deinit().
    And you are right that the state of the Pin has to be defined when the PWM is not running - if it can be set. I cannot imagine a per se useful state for any possible application. That depends on the circuitry attached to it. So we either have to grab a value (like OUT, level low) or make it configurable with a keyword argument of the constructor/init.

  4. th3w commented on Feb 23, 2023

    @th3w

    A PWM.init() method to start the PWM output and PWM.deinit() to stop it would make this API almost equivalent to the machine.Timer() API, where the timer can also be restarted/reconfigured with Timer.init() and stopped with Timer.denit(). (at least for the rp2 port)

  5. rkompass commented on Feb 23, 2023

    @rkompass

    From my standpoint it would make sense to have deinit() completely reverse the effects of init(). This would mean the Pin goes back to the state it had before init(), i.e. if it was OUT, level low, then it again would be OUT, low.
    If you wanted to go it back to IN automatically, you could set it to IN before init().

    Another problem coming into mind:
    PWM channels share common timers (slices on RP2). So if you set frequency on one channel you already have it set on another channel. I can imagine 2 approaches to this complication:

    1. Be completely agnostic to it. The programmer has to know it's hardware. This would suggest that the setting of duty in init() is sufficient to start the PWM. Frequency should have a documented default value which is common and compatible with LED and motor control, say 1 kHz (something between 100 Hz and 10 kHz). If a freq value is set, the programmer is aware of the channels that he has set also.
    2. Let MP control the resources somewhat. Once the frequency is set for a channel it is registered for this timer/slice and another attempt to set it to a different value for another channel on this timer raises an exception. This may be avoided with an optional force argument which the programmer uses in awareness of the hardware dependencies.
      This approach would allow the programmer to probe which channels depend on the same timer/slice without studying the hardware datasheets: He could set one frequency for a channel and then another frequency for other channels he is interested in and catch the exceptions. When he had not set the duty this would mean not disturbance on the outputs.
  6. robert-hh commented on Feb 23, 2023

    @robert-hh
    ContributorAuthor

    From my standpoint it would make sense to have deinit() completely reverse the effects of init(). This would mean the Pin goes back to the state it had before init(), i.e. if it was OUT, level low, then it again would be OUT, low.
    If you wanted to go it back to IN automatically, you could set it to IN before init().

    That can and should be done by the programmer's code. The pin could have been for all possible uses, not only as machine.Pin object.

    PWM channels share common timers (slices on RP2).

    Yes, and I see it as a feature that all channels on this timer change the frequency simultaneously if it is changed for one channel. So I went & go for option 1.

  7. dpgeorge commented on Feb 23, 2023

    @dpgeorge
    Member

    When thinking about the uninitialised state it's worth also considering how other peripherals work (or should be made to work). Eg I2C and SPi will configure their pins when constructed and the bus will be idle. ADC will configure it's given pin. DAC is more similar to PWM and I'm not sure exactly how that works.

  8. robert-hh commented on Feb 24, 2023

    @robert-hh
    ContributorAuthor

    As far as I recall, all of the modules you mention configure the pins according to the required mode. For DAC the pin is connected to the driver with an initial value, which might be set or not.
    For PWM it might be the same, but I have to check all ports. When PWM is not started, I expect output mode and low for normal PWM pins and high for inverted pins. To avoid any confusion, having a start and stop method (or on and off) seems to be more clear. Then init or the constructor would configure and optionally start the PWM, and deinit() releases the resources. The latter may detach the output as well, if the port lib supports it. But setting the pin to a different mode can also be left to the Python script. Having a start method is also helpful to start several outputs of pins connected to the same PWM device with maybe different polarity and duty rate at the same time, like the two channels of a slice of rp2.

    P.S.: As the first step I modified already the MIMXRT implementation to not start before freq and duty rate are deliberately set. Maybe @IhorNehrutsa could look at that for the ESP32 port. Next steps would be adding keyword options to the rp2 constructor, adding init() to SAMD, fixing the nrf PWM, and check the behavior of the ESP8266 port. Then the major ports should be comparable. I do not know if init() is required, once a start/stop mechanism exists. Having the constructor with options and init() was always a source of confusion, especially if init() sets default value. But removing init() would be a breaking change. So better let it behave reasonable for now.

  9. IhorNehrutsa commented on Feb 24, 2023

    @IhorNehrutsa
    Contributor

    I can review the PWM of the ESP32 port.

    I am against to add start()/on(), stop()/off() methods.

    pwm = PWM(pin, duty_u16=0)    #   If the duty is 0 or 100%, the output has stable level
    pwm.start()    #   start() does not change the output level
    pwm.stop()     #   and stop() does not change the output level
    pwm.start()    #   the output level stay same
    pwm.stop()     #   the output level stay same
    
  10. IhorNehrutsa commented on Feb 24, 2023

    @IhorNehrutsa
    Contributor

    When a novice user starts PWM,

    pwm = PWM(pin)
    

    they expect something to happen.
    The ESP32 use 5kHz and 50% duty as default.

  11. robert-hh commented on Feb 24, 2023

    @robert-hh
    ContributorAuthor

    Hi @IhorNehrutsa Thank you for the answer. The first step would be to make the implementations consistent in a way, that PWM does not start until freq and duty are deliberately set. Everything else it t.b.d..
    The behavior for deinit()/init() may be next.
    About start()/stop() resp. init()/deinit(): Something which is implemented already by chance in the SAMD port and could easily be added to the rp2 port: If deinit() is called, the pin is switched back to GPIO mode, and init() sets it to PWM mode. That way, the programmer can define the inactivity mode & level. The state with duty=0 or 100% is something different.

  12. IhorNehrutsa commented on Feb 24, 2023

    @IhorNehrutsa
    Contributor

    Returning to GPIO mode when deinit() is not advisable, because you do not know from which mode you came to PWM. It could be, for example, the DAC or something else in general (you won't guess).

  13. robert-hh commented on Feb 24, 2023

    @robert-hh
    ContributorAuthor

    It's not any mode, and the code does not have to guess. It is the mode that was set for the GPIO before by the program. And for me it looks like a good method to control the mode the Pin must have, depending on what is connected.

  14. IhorNehrutsa commented on Feb 25, 2023

    @IhorNehrutsa
    Contributor

    Setting (and restoring in deinit()) the PWM/DAC Pin(26) as GPIO Pin.IN or Pin.OUT is redundant in this scenario.

    from time import sleep
    from machine import Pin, PWM, DAC
    
    pin = Pin(26)
    while True:
        pwm = PWM(pin, freq=30_000, duty_u16=32768)  # 50% duty ≈ 50% of output voltage after filter
        print(pwm)
        sleep(5)
        pwm.deinit()
        
        dac = DAC(pin)
        print(dac)
        dac.write(127)  # 50% of output voltage
        sleep(5)
        dac.deinit()
    

    Ideally, this code must hold 50% of the output voltage at all times.

    P.S.Use ESP32: Add DAC.deinit() method. #10852

  15. 58 remaining items

  16. robert-hh commented on Mar 16, 2023

    @robert-hh
    ContributorAuthor

    Do you have an idea, how to proceed with__repr__()

    No. this is a big mess not only for the PWM class and need to be harmonized. MIMXRT is worse since there are two types of PWM peripherals: FLEXPWM and QTMR. Even for RP2 the pin number you get may be wrong, because the same slice/channel is available at two pins. So maybe it's better to supply the Pin object instead of the PWM objects.

  17. rkompass commented on Mar 16, 2023

    @rkompass

    I switched to PWM objects because then there is no need of deinit(). Deiniting the PWM objects is left to the user. The HBridge class only specifies parameters of pins and PWM.
    Also I combined two H-Bridge interface modes by checking the type of argument Pin or PWM.
    See the code at #10983.
    I feel like being in need of a feedback whether this is o.k.. I have to test the code yet, of course.
    The __repr__() will not be available.

    In general I would opt for a pin_id() function for pins as well as PWM.

  18. robert-hh commented on Mar 17, 2023

    @robert-hh
    ContributorAuthor

    The output of print for any object has to be reconsidered. There is total chaos. It should be uniform. One idea I had is to make it in the form, an object would be created. So repr of a pin for RP2 would be:
    2, mode=Pin.OUT, pull=Pin.PULL_NONE
    For PWM (since we're at it):
    Pin(2), freq=1234, duty_u16=8192, invert=0
    Whether optional information is to be supplied has to be discussed. But that will be quite a bit of work across the ports. And as a breaking change that is to be done for a version 2.0.0.

  19. rkompass commented on Mar 17, 2023

    @rkompass
  20. IhorNehrutsa commented on Jul 19, 2023

    @IhorNehrutsa
    Contributor

    Where is the updated PWM API documentation located?
    Is https://docs.micropython.org/en/latest/library/machine.PWM.html out of date?

    Can someone explain the pin states(input/output mode/attached/detached/levels/???) and pwm timer states(inited/deinited/???) at this sequence in new PWM API?

    from machine import PWM, Pin  # PWM timer deinited,  Pin detached, Pin hold previous state(input no pull-up, no pull-down) after reset
    pwm.PWM(Pin(19))  # PWM timer ???, Pin ???
    pwm.PWM(Pin(19), freq=1250, duty_u16=2**16//4, invert=1)  # PWM timer inited,  Pin output work
    pwm.PWM(Pin(19))  # PWM timer ???, Pin ???
    pwm.deinit()   # PWM timer deinited,  Pin detached, Pin state is indeterminate(undefined)
    
  21. added a commit that references this issue on Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementFeature requests, new feature implementationsextmodRelates to extmod/ directory in source

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions