PWM2 Module

Since Origin / Contributor Maintainer Source
2019-02-12 fikin fikin pwm2.c

Module to generate PWM impulses to any of the GPIO pins.

PWM is being generated by software using soft-interrupt TIMER1 FRC1. This module is using the timer in exclusive mode. See understanding timer use for more.

Supported frequencies are roughly from 120kHZ (with 50% duty) up to pulse/53sec (or 250kHz and 26 sec for CPU160). See understanding frequencies for more.

Supported are also frequency fractions even for integer-only firmware builds.

Supported are all of the GPIO pins except pin 0.

One can generate different PWM signals to any of them at the same time. See working with multiple frequencies for more.

This module supports CPU80MHz as well as CPU160MHz. Frequency boundaries are same but by using CPU160MHz one can hope of doing more work meantime.

Typical usage is as following:

pwm2.setup_pin_hz(3,250000,2,1) -- pin 3, PWM freq of 250kHz, pulse period of 2 steps, initial duty 1 period step 
pwm2.setup_pin_hz(4,1,2,1) -- pin 4, PWM freq of 1Hz, pulse period of 2 steps, initial duty 1 period step 
pwm2.start() -- starts pwm, internal led will blink with 0.5sec interval
...
pwm2.set_duty(4, 2) -- led full off (pin is high)
...
pwm2.set_duty(4, 0) -- led full on (pin is low)
...
pwm2.stop() -- PWM stopped, gpio pin released, timer1 released

Understand frequencies

All frequencines and periods internally are expressed as CPU ticks using following formula: cpuTicksPerSecond / (frequencyHz * period). For example, 1kHz with 1000 period for CPU80MHz results in 80 CPU ticks per period i.e. period is 1uS long.

In order to allow for better tunning, I've added an optional frequencyDivisor argument when setting pins up. With it one can express the frequency as division between two values : frequency / divisor. For example to model 100,1Hz frequency one has to specify frequency of 1001 and divisor 10.

An easy way to express sub-Hz frequencies, i.e. the ones taking seconds to complete one impulse, is to use setup in seconds methods. For them formula to compute CPU ticks is cpuTicksPerSecond * frequencySec / period. Max pulse duration is limited by roll-over of the ESP's internal CPU 32bits ticks counter. For CPU80 that would be every 53 seconds, for CPU160 that would be half.

Frequency precision and limits

ESP's TIMER1 FRC1 is operating at fixed, own frequency of 5MHz. Therefore the precision of individual interrupt is 200ns. But that limit cannot be attained.

OS timer interrupt handler code itself has internal overhead. For auto-loaded interrupts it is about 50CPUTicks. For short periods of time one can interrupt at approximately 1MHz but then watchdog will intervene.

PWM2 own interrupt handler has an overhead of 162CPUTicks + 12CPUTicks per each used pin.

With the fastest setup i.e. 1 pin, 50% duty cycle (pulse period of 2) and CPU80 one could expect to achive PWM frequency of 125kHz. For 12 pins that would drop to about 100kHz. With CPU160 one could reach 220kHz with 1 pin.

Frequencies internally are expressed as CPU ticks first then to TIMER1 ticks. Because TIMER1 frequency is 1/16 of CPU frequency, some frequency precision is lost when converting from CPU to TIMER ticks. One can inspect exact values used via pwm2.get_timer_data(). Value of interruptTimerCPUTicks represents desired interrupt period in CPUTicks. And interruptTimerTicks represents actually used interrupt period as TIMER1 ticks (1/16 of CPU).

Working with multiple frequencies

When working with multiple pins, this module auto-discovers what would be the right underlying interrupt frequency. It does so by computing the greatest common frequency divisor and use it as common frequency for all pins.

When using same frequency for many pins, tunning frequency of single pin is enough to ensure precision.

When using different frequencies, one has to pay close attention at their greates common divisor when expressed as CPU ticks. For example, mixing 100kHz with period 2 and 0.5Hz with period 2 results in underlying interrupt period of 800CPU ticks. But changing to 100kHz+1 will easily result to divisor of 1. This is clearly non-working combination. Another example is frequency of 120kHz with period 2, which results in period of 333CPU ticks. If combined with even-resulting frequency like 1Hz with period of 2, this will lead to common divisor of 1, which is clearly a non-working setup either. For the moment best would be to use pwm2.get_timer_data() and observe how interruptTimerCPUTicks and interruptTimerTicks change with given input.

Understanding timer use

This module is using soft-interrupt TIMER1 FRC1 to generate PWM signal. Since its interrupts can be masked, as some part of OS are doing it, it is possible to have some impact on the quality of generated PWM signal. As a general principle, one should not expect high precision signal with this module. Also note that interrupt masking is dependent on other activities happening within the ESP besides pwm2 module.

Additionally this timer is used by other modules like pwm, pcm, ws2812 and etc. Since an exclusive lock is maintained on the timer, simultaneous use of such modules would not be possible.

Troubleshooting watchdog timeouts

Watchdog interrupt typically will occur if choosen frequency (and period) is too big i.e. too small timer ticks value. For CPU80MHz I guess threshold is around 125kHz with period of 2 and single pin (CPU80), given not much other load on the system. For CPU160 threshold is 225kHz.

Another reason for watchdog interrupt to occur is due to mixing otherwise not very compatible frequencies when multiple pins are used. See working with multiple frequencies for more.

Both cases are best anlyzed using pwm2.get_timer_data() watching values of interruptTimerCPUTicks and interruptTimerTicks. For interruptTimerCPUTicks with CPU80 anything below (330/630) for (1/12) pins would be cause for special attention.

Differences with PWM module

PWM and PWM2 are modules doing similar job and have much in common. Here are few PWM2 highlights compared to PWM module:

  • PWM2 is using TIMER1 exclusively, which allows for possibly a better quality PWM signal
  • PWM2 can generate PWM frequencies in the range of 1pulse/53 seconds up to 125kHz (26sec/225kHz for CPU160)
  • PWM2 can generate PWM frequencies with fractions i.e. 1001kHz
  • PWM2 supports CPU160
  • PWM2 supports virtually all GPIO ports at the same time

Unlike PWM2, PWM can:

  • generate PWM pulse with a little bit bigger duty cycle i.e. 1kHz at 1000 pulse period
  • can be used at the same time with some other modules like gpio.pulse

pwm2.setup_pin_hz()

Assigns PWM frequency expressed as Hz to given pin. This method is suitable for setting up frequencies in the range of >= 1Hz.

Syntax

pwm2.setup_pin_hz(pin,frequencyAsHz,pulsePeriod,initialDuty [,frequencyDivisor])

Parameters

  • pin 1-12
  • frequencyAsHz desired frequency in Hz, for example 1000 for 1KHz
  • pulsePeriod discreet steps in single PWM pulse, for example 100
  • initialDuty initial duty in pulse period steps i.e. 50 for 50% pulse of 100 resolution
  • frequencyDivisor an integer to divide product of frequency and pulsePeriod. Used to form frequency fractions. By default not required.

Returns

nil

See also

pwm2.setup_pin_sec()

Assigns PWM frequency expressed as one impulse per second(s) to given pin. This method is suitable for setting up frequencies in the range of 0 < 1Hz but expressed as seconds instead. For example 0.5Hz are expressed as 2 seconds impulse.

Syntax

pwm2.setup_pin_sec(pin,frequencyAsSec,pulsePeriod,initialDuty [,frequencyDivisor])

Parameters

  • pin 1-12
  • frequencyAsSec desired frequency as one impulse for given seconds, for example 2 means PWM with impulse long 2 seconds.
  • pulsePeriod discreet steps in single PWM pulse, for example 100
  • initialDuty initial duty in pulse period steps i.e. 50 for 50% pulse of 100 resolution
  • frequencyDivisor an integer to divide product of frequency and pulsePeriod. Used to form frequency fractions. By default not required.

Returns

nil

See also

pwm2.start()

Starts PWM for all setup pins. At this moment GPIO pins are marked as output and TIMER1 is being reserved for this module. If the TIMER1 is already reserved by another module this method reports a Lua error and returns false.

Syntax

pwm2.start()

Parameters

nil

Returns

  • bool true if PWM started ok, false of TIMER1 is reserved by another module.

See also

pwm2.stop()

Stops PWM for all pins. All GPIO pins and TIMER1 are being released. One can resume PWM with previous pin settings by calling pwm2.start() right after stop.

Syntax

pwm2.stop()

Parameters

nil

Returns

nil

See also

pwm2.set_duty()

Sets duty cycle for one or more a pins. This method takes immediate effect to ongoing PWM generation.

Syntax

pwm2.set_duty(pin, duty [,pin,duty]*)

Parameters

  • pin 1~12, IO index
  • duty 0~period, pwm duty cycle

Returns

nil

See also

pwm2.release_pin()

Releases given pin from previously done setup. This method is applicable when PWM is stopped and given pin is not needed anymore. Releasing pins is not strictly needed. This method is useful for start-stop-start situations when pins do change.

Syntax

pwm2.release_pin(pin)

Parameters

  • pin 1~12, IO index

Returns

nil

See also

pwm2.get_timer_data()

Prints internal data structures related to the timer. This method is usefull for people troubleshooting frequency side effects.

Syntax

pwm2.get_timer_data()

Parameters

nil

Returns

  • isStarted bool, if true PWM2 has been started
  • interruptTimerCPUTicks int, desired timer interrupt period in CPU ticks
  • interruptTimerTicks int, actual timer interrupt period in timer ticks

Example

isStarted, interruptTimerCPUTicks, interruptTimerTicks = pwm2.get_timer_data()

See also

pwm2.get_pin_data()

Prints internal data structures related to given GPIO pin. This method is usefull for people troubleshooting frequency side effects.

Syntax

pwm2.get_pin_data(pin)

Parameters

  • pin 1~12, IO index

Returns

  • isPinSetup bool, if 1 pin is setup
  • duty int, assigned duty
  • pulseResolutions int, assigned pulse periods
  • divisableFrequency int, assigned frequency
  • frequencyDivisor int, assigned frequency divisor
  • resolutionCPUTicks int, calculated one pulse period in CPU ticks
  • resolutionInterruptCounterMultiplier int, how many timer interrupts constitute one pulse period

Example

isPinSetup, duty, pulseResolutions, divisableFrequency, frequencyDivisor, resolutionCPUTicks, resolutionInterruptCounterMultiplier = pwm2..get_pin_data(4)

See also