310 lines
12 KiB
Markdown
310 lines
12 KiB
Markdown
# PWM2 Module
|
|
| Since | Origin / Contributor | Maintainer | Source |
|
|
| :----- | :-------------------- | :---------- | :------ |
|
|
| 2019-02-12 | [fikin](https://github.com/fikin) | [fikin](https://github.com/fikin) | [pwm2.c](../../../app/modules/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](#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](#understand-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](#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:
|
|
```lua
|
|
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()](#pwm2get_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()](#pwm2get_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](#working-with-multiple-frequencies) for more.
|
|
|
|
Both cases are best anlyzed using [pwm2.get_timer_data()](#pwm2get_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()](#pwm2setup_pin_sec)
|
|
- [pwm2.start()](#pwm2start)
|
|
- [pwm2.release_pin()](#pwm2release_pin)
|
|
- [understanding frequencies](#understand-frequencies)
|
|
- [working with multiple frequencies](#working-with-multiple-frequencies)
|
|
- [pwm2.get_timer_data()](#pwm2get_timer_data)
|
|
|
|
## 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.setup_pin_hz()](#pwm2setup_pin_hz)
|
|
- [pwm2.start()](#pwm2start)
|
|
- [pwm2.release_pin()](#pwm2release_pin)
|
|
- [understanding frequencies](#understand-frequencies)
|
|
- [working with multiple frequencies](#working-with-multiple-frequencies)
|
|
- [pwm2.get_timer_data()](#pwm2get_timer_data)
|
|
|
|
## 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.setup_pin_hz()](#pwm2setup_pin_hz)
|
|
- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec)
|
|
- [pwm2.set_duty()](#pwm2set_duty)
|
|
- [pwm2.stop()](#pwm2stop)
|
|
|
|
## 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()](#pwm2start) right after stop.
|
|
|
|
### Syntax
|
|
|
|
`pwm2.stop()`
|
|
|
|
### Parameters
|
|
|
|
`nil`
|
|
|
|
### Returns
|
|
|
|
`nil`
|
|
|
|
### See also
|
|
|
|
- [pwm2.start()](#pwm2start)
|
|
- [pwm2.release_pin()](#pwm2release_pin)
|
|
|
|
## 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.stop()](#pwm2stop)
|
|
|
|
## 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.setup_pin_hz()](#pwm2setup_pin_hz)
|
|
- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec)
|
|
- [pwm2.stop()](#pwm2stop)
|
|
|
|
## 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.setup_pin_hz()](#pwm2setup_pin_hz)
|
|
- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec)
|
|
- [pwm2.get_pin_data()](#pwm2get_pin_data)
|
|
|
|
## 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
|
|
|
|
- [pwm2.setup_pin_hz()](#pwm2setup_pin_hz)
|
|
- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec)
|
|
- [pwm2.get_timer_data()](#pwm2get_timer_data)
|