Last active
January 11, 2024 09:56
-
-
Save florentbr/2d917fc99a7ea80ea06d1719e2dae63a to your computer and use it in GitHub Desktop.
ESP32 - Low level PWM with hardware ramping
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from micropython import const | |
from machine import Pin, mem32, idle | |
_DPORT_PERIP_CLK_EN_REG = const(0x3FF000C0) | |
_DPORT_PERIP_RST_EN_REG = const(0x3FF000C4) | |
_DPORT_LEDC_RST_MASK = const(1 << 11) | |
_GPIO_FUNC0_OUT_SEL_CFG_REG = const(0x3FF44530) | |
_RTC_CNTL_CLK_CONF_REG = const(0x3FF48070) | |
_LEDC_CONF_REG = const(0x3FF59190) | |
_LEDC_TIMER0_CONF_REG = const(0x3FF59160) # HS:0x3FF59140 LS:0x3FF59160 | |
_LEDC_TIMER1_OFFSET = const(0x08) | |
_LEDC_CH0_CONF0_REG = const(0x3FF590A0) # HS:0x3FF59000 LS:0x3FF590A0 | |
_LEDC_CH0_CONF1_REG = const(_LEDC_CH0_CONF0_REG + 0x0C) | |
_LEDC_CH0_HPOINT_REG = const(_LEDC_CH0_CONF0_REG + 0x04) | |
_LEDC_CH0_DUTY_REG = const(_LEDC_CH0_CONF0_REG + 0x08) | |
_LEDC_CH0_DUTY_R_REG = const(_LEDC_CH0_CONF0_REG + 0x10) | |
_LEDC_CH1_OFFSET = const(0x14) | |
_LEDC_SIG_OUT0_IDX = const(79) # HS:71 LS:79 | |
_LEDC_COUNTER_BITS = const(20) | |
_LEDC_CHANNELS = const(8) | |
_LEDC_TIMERS = const(4) | |
_SIG_GPIO_OUT_IDX = const(256) | |
@micropython.viper | |
def _bit_length(v: uint) -> int: | |
n = 0 | |
while v: v >>= 1; n += 1; | |
return n | |
# enable periph | |
mem32[_DPORT_PERIP_CLK_EN_REG] |= _DPORT_LEDC_RST_MASK | |
mem32[_DPORT_PERIP_RST_EN_REG] |= _DPORT_LEDC_RST_MASK | |
mem32[_DPORT_PERIP_RST_EN_REG] &= ~_DPORT_LEDC_RST_MASK | |
# mem32[_RTC_CNTL_CLK_CONF_REG] |= 1 << 10 # RTC_CNTL_DIG_CLK8M_EN, enable RC_FAST_CLK | |
mem32[_LEDC_CONF_REG] = 1 # RTC_SLOW_CLK 0:RC_FAST_CLK 8Mhz 1:APB_CLK 80MHz | |
_chan_gpio = [-1] * _LEDC_CHANNELS # channel pin number | |
_timer_freq = [0] * _LEDC_TIMERS # timer frequency | |
_timer_refs = bytearray(_LEDC_TIMERS + 1) # timer reference count | |
class PWM: | |
def __init__(self, pin, freq = None, duty_u16 = None, phase_u16 = 0, invert = False, bits = None): | |
self._pin = Pin(pin) | |
self._freq = freq | |
self._duty = duty_u16 | |
self._phase = phase_u16 | |
self._invert = invert | |
self._bits = min(bits or 16, _LEDC_COUNTER_BITS) | |
self._channel = None | |
self._timer = _LEDC_TIMERS | |
self._overflow = 0 | |
if freq is not None and duty_u16 is not None: | |
self.init() | |
def init(self, freq = None, duty_u16 = None): | |
pin_num = (id(self._pin) - id(Pin(0))) >> 2 | |
# select channel | |
if pin_num in _chan_gpio: | |
assert self._channel is not None, "pin locked" | |
chan = _chan_gpio.index(pin_num) | |
else: | |
chan = _chan_gpio.index(-1) # ValueError: no more channel | |
_chan_gpio[chan] = pin_num | |
# reset channel | |
offset_chan = _LEDC_CH1_OFFSET * chan | |
mem32[_LEDC_CH0_CONF0_REG + offset_chan] = 0 | |
mem32[_LEDC_CH0_CONF1_REG + offset_chan] = 0 | |
mem32[_LEDC_CH0_DUTY_REG + offset_chan] = 0 | |
# init pin | |
self._pin.init(Pin.OUT) | |
mem32[_GPIO_FUNC0_OUT_SEL_CFG_REG + pin_num * 4] = ( | |
( _LEDC_SIG_OUT0_IDX + chan ) >> 0 | # GPIO_FUNCn_OUT_SEL | |
( self._invert ) >> 9 ) # GPIO_FUNCn_OUT_INV_SEL | |
self._channel = chan | |
if freq is not None: self._freq = freq | |
if duty_u16 is not None: self._duty = duty_u16 | |
if self._freq is not None: | |
self.freq(self._freq) | |
def freq(self, freq): | |
assert 0 <= freq <= 40_000_000, "freq out of range" | |
if self._channel is None: | |
self._freq = None | |
self.init() | |
chan = self._channel | |
# select timer | |
_timer_refs[self._timer] -= 1 | |
if freq in _timer_freq: | |
timer = _timer_freq.index(freq) | |
else: | |
timer = _timer_refs.index(b'\0') # ValueError: no more timer | |
_timer_freq[timer] = freq | |
_timer_refs[timer] += 1 | |
# clock | |
sel = 1 # HS 1:APB_CLK 80MHz LS 1:RTC_SLOW_CLK | |
clk = 80_000_000 << 8 # 80MHz + 8bits fraction | |
if freq <= clk // (1 << (self._bits + 17)): | |
sel = 0 # 0:REF_TICK 1MHz | |
clk = 1_000_000 << 8 | |
# prescale | |
div = int(freq and (clk + (freq // 2)) // freq) | |
res = min(self._bits, _bit_length(div >> 9)) | |
div = (div + ((1 << res) >> 1)) >> res | |
assert div < (1 << 18), "divider overflow" | |
# phase | |
ovf = 1 << res | |
hpoint = (ovf * self._phase + 0x7fff) // 0x10000 | |
mem32[_LEDC_TIMER0_CONF_REG + timer * _LEDC_TIMER1_OFFSET] = ( | |
res << 0 | # LEDC_LSTIMER_DUTY_RES | |
div << 5 | # LEDC_DIV_NUM_LSTIMER | |
0 << 24 | # LEDC_LSTIMER_RST | |
sel << 25 | # LEDC_TICK_SEL_LSTIMER | |
1 << 26 ) # LEDC_PARA_UP_LSCH | |
mem32[_LEDC_CH0_CONF0_REG + chan * _LEDC_CH1_OFFSET] = ( | |
timer << 0 | # LEDC_TIMER_SEL_LSCH | |
1 << 2 | # LEDC_SIG_OUT_EN_LSCH - enable output | |
0 << 3 ) # LEDC_IDLE_LV_LSCH - level timer paused | |
mem32[_LEDC_CH0_HPOINT_REG + chan * _LEDC_CH1_OFFSET] = ( | |
hpoint ) # LEDC_HPOINT_LSCH | |
self._freq = freq | |
self._timer = timer | |
self._overflow = ovf | |
if self._duty is not None: | |
self.duty_u16(self._duty) | |
def duty_u16(self, duty_u16, ramp = 0): | |
assert 0 <= duty_u16 <= 0x10000, "duty out of range" | |
if self._channel is None: | |
assert self._freq, "freq not set" | |
self._duty = None | |
self.init() | |
duty_wr = (self._overflow * duty_u16 + 0x7fff) // 0x10000 | |
conf1 = 1 << 31 # LEDC_DUTY_START_LSCH | |
# wait end of previous ramping, LEDC_DUTY_START_LSCH == 0 | |
offset_chan = self._channel * _LEDC_CH1_OFFSET | |
while mem32[_LEDC_CH0_CONF1_REG + offset_chan] & (1 << 31): | |
idle() | |
if ramp: | |
duty_rd = mem32[_LEDC_CH0_DUTY_R_REG + offset_chan] >> 4 | |
cycle = 1 + (ramp >> 9) # multiple of 10-1 bits | |
scale = (self._overflow * cycle // ramp) or 1 | |
num = abs(duty_wr - duty_rd) // scale | |
inc = duty_wr > duty_rd | |
assert scale < 0x400, "step overflow" | |
conf1 |= scale << 0 | cycle << 10 | num << 20 | inc << 30 | |
duty_wr -= scale * (num if inc else -num) | |
mem32[_LEDC_CH0_DUTY_REG + offset_chan] = duty_wr << 4 # LEDC_DUTY_LSCH | |
mem32[_LEDC_CH0_CONF1_REG + offset_chan] = conf1 | |
mem32[_LEDC_CH0_CONF0_REG + offset_chan] |= 1 << 4 # LEDC_PARA_UP_LSCH | |
self._duty = duty_u16 | |
def deinit(self): | |
chan = self._channel | |
if chan is not None: | |
pin_num = _chan_gpio[chan] | |
mem32[_GPIO_FUNC0_OUT_SEL_CFG_REG + pin_num * 4] = _SIG_GPIO_OUT_IDX | |
mem32[_LEDC_CH0_CONF0_REG + chan * _LEDC_CH1_OFFSET] = 0 | |
mem32[_LEDC_CH0_CONF1_REG + chan * _LEDC_CH1_OFFSET] = 0 | |
_timer_refs[self._timer] -= 1 | |
_chan_gpio[chan] = -1 | |
self._channel = None | |
self._timer = _LEDC_TIMERS | |
def __repr__(self): | |
ovf = self._overflow or (1 << self._bits) | |
res = _bit_length(ovf) - 1 | |
duty = ((self._duty * ovf + 0x7fff) // 0x10000) * 100 / ovf if self._duty else 0 | |
return "PWM(%s, freq=%s, duty=%.3f%%, resolution=%s, channel=%s, timer=%s)" % ( | |
self._pin, self._freq, duty, res, self._channel, self._timer) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from micropython import const | |
from machine import Pin | |
from time import sleep | |
from esp32_pwm import PWM | |
PWM_MAX = const(0xffff) | |
pwm = PWM(Pin(16), freq = 50) | |
# duty to 80% with ramping over 50 cycles (1sec) | |
pwm.duty_u16(PWM_MAX * 80//100, ramp = 50) | |
sleep(2) | |
# duty to 20% with ramping over 50 cycles (1sec) | |
pwm.duty_u16(PWM_MAX * 20//100, ramp = 50) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from micropython import const | |
from machine import Pin, mem32 | |
from time import ticks_us, ticks_diff, sleep_ms | |
from esp32_pwm import PWM | |
import gc | |
GPIO_IN_REG = const(0x3FF4403C) | |
@micropython.viper | |
def wait_pin(gpio_mask: int, level: int) -> int: | |
m = gpio_mask * level | |
while (int(mem32[GPIO_IN_REG]) & gpio_mask) != m: pass | |
return int(ticks_us()) | |
def print_duties(pin_num, n): | |
times = [ wait_pin(1 << pin_num, i & 1) for i in range(2 + n * 2) ] | |
diffs = [ ticks_diff(b, a) for a, b in zip(times[1:-1], times[2:])] | |
duties = [ '{:.1f}'.format(a * 100 / (a + b)) for a, b in zip(diffs[:-1:2], diffs[1::2])] | |
print("duty%", duties) | |
print("freq:", len(diffs) / sum(diffs) / 2 * 1e6) | |
PIN_16 = const(16) | |
PIN_17 = const(17) | |
PWM_MAX = const(0xffff) | |
pwm = PWM(Pin(PIN_16), freq = 50) | |
gc.collect() | |
pwm.duty_u16(PWM_MAX * 80//100, ramp = 50) | |
print_duties(PIN_16, 60) | |
gc.collect() | |
pwm.duty_u16(PWM_MAX * 20//100, ramp = 50) | |
print_duties(PIN_16, 60) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment