Files
tinygo/src/machine/machine_esp32xx_pwm.go
T
Dima 47282be619 esp3s3 & esp32c3 PWM implements (#5215)
* esp3s3 pwm

* esp32s3-pwm: added spi alias for esp32s3 for tests

* esp32s3-pwm: fix linters

* esp32s3-pwm: fix rename

* esp32s3-pwm: replace if to switch

* esp32s3-pwm: chip specific registry

* esp32s3-pwm: fix linters
2026-04-05 14:15:40 +02:00

180 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build esp32c3 || esp32s3
// PWM on ESP32-C3/S3 uses the LEDC (LED Control) peripheral, low-speed mode only.
// One timer drives multiple channels; each channel has its own duty, shared frequency.
// Pin routing is via GPIO matrix (SigOutBase + channel index).
//
// Channel config (chanOp) follows the hardware contract from:
// - ESP-IDF: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html
// (timer config → channel config → duty + update_duty).
// - SVD (e.g. lib/cmsis-svd/data/Espressif/esp32s3.svd): CONF0.PARA_UP "updates
// HPOINT, DUTY_START, SIG_OUT_EN, TIMER_SEL, DUTY_NUM, DUTY_CYCLE, DUTY_SCALE,
// DUTY_INC for channel and is auto-cleared by hardware"; CONF1.DUTY_START "other
// CONF1 fields take effect when this bit is set to 1".
package machine
import (
"device/esp"
"errors"
)
const ledcApbClock = 80_000000
const ledcDutyFracBits = 4 // DUTY register has 4 fractional bits; write value<<4
const ledcDividerFracBits = 8 // Clock divider register = actual_divider * 256
var errPWMNoChannel = errors.New("pwm: no free channel")
type LEDCPWM struct {
SigOutBase uint32 // GPIO matrix signal index for channel 0 (e.g. 73 on S3, 45 on C3)
NumChannels uint8
timerNum uint8 // 03: which LEDC timer (frequency) this PWM uses
dutyRes uint8
configured bool
channelPin [8]Pin
}
type ledcChanOp uint8
const (
ledcChanOpInit ledcChanOp = iota // initial per-channel setup (timer, enable, HPOINT/DUTY/CONF1, PARA_UP)
ledcChanOpSetDuty // update duty and latch it (DUTY + CONF1 + PARA_UP)
ledcChanOpSetInvert // change idle level (IDLE_LV)
)
func (pwm *LEDCPWM) Configure(config PWMConfig) error {
// Enable LEDC clock and release reset (SYSTEM perip_clk_en0 / perip_rst_en0).
esp.SYSTEM.SetPERIP_RST_EN0_LEDC_RST(1)
esp.SYSTEM.SetPERIP_CLK_EN0_LEDC_CLK_EN(1)
esp.SYSTEM.SetPERIP_RST_EN0_LEDC_RST(0)
// LEDC global: APB clock source, enable internal clock.
esp.LEDC.SetCONF_APB_CLK_SEL(1)
esp.LEDC.SetCONF_CLK_EN(1)
period := config.Period
if period == 0 {
period = 1_000_000
}
freq := uint64(1e9) / period
dutyRes := uint8(10)
switch {
case freq < 100:
dutyRes = 14
case freq < 1000:
dutyRes = 12
case freq > 100_000:
dutyRes = 8
}
// Timer divider: period_ns = (2^dutyRes * divActual/256) / 80MHz * 1e9 => divReg = divActual<<8.
divActual := ledcApbClock / (uint32(freq) * (1 << dutyRes))
if divActual == 0 {
divActual = 1
}
divReg := divActual << ledcDividerFracBits
if divReg > 0x3ffff {
return ErrPWMPeriodTooLong
}
// Selected timer: resolution, divider, no pause, reset then latch config with PARA_UP.
pwm.setTimerConf(dutyRes, divReg)
pwm.dutyRes = dutyRes
pwm.configured = true
for i := range pwm.channelPin {
pwm.channelPin[i] = NoPin
}
return nil
}
func (pwm *LEDCPWM) setTimerConf(dutyRes uint8, divReg uint32) {
t := pwm.timerNum
switch t {
case 0:
esp.LEDC.SetTIMER0_CONF_DUTY_RES(uint32(dutyRes))
esp.LEDC.SetTIMER0_CONF_CLK_DIV(divReg)
esp.LEDC.SetTIMER0_CONF_TICK_SEL(0)
esp.LEDC.SetTIMER0_CONF_PAUSE(0)
esp.LEDC.SetTIMER0_CONF_RST(1)
esp.LEDC.SetTIMER0_CONF_RST(0)
esp.LEDC.SetTIMER0_CONF_PARA_UP(1)
case 1:
esp.LEDC.SetTIMER1_CONF_DUTY_RES(uint32(dutyRes))
esp.LEDC.SetTIMER1_CONF_CLK_DIV(divReg)
esp.LEDC.SetTIMER1_CONF_TICK_SEL(0)
esp.LEDC.SetTIMER1_CONF_PAUSE(0)
esp.LEDC.SetTIMER1_CONF_RST(1)
esp.LEDC.SetTIMER1_CONF_RST(0)
esp.LEDC.SetTIMER1_CONF_PARA_UP(1)
case 2:
esp.LEDC.SetTIMER2_CONF_DUTY_RES(uint32(dutyRes))
esp.LEDC.SetTIMER2_CONF_CLK_DIV(divReg)
esp.LEDC.SetTIMER2_CONF_TICK_SEL(0)
esp.LEDC.SetTIMER2_CONF_PAUSE(0)
esp.LEDC.SetTIMER2_CONF_RST(1)
esp.LEDC.SetTIMER2_CONF_RST(0)
esp.LEDC.SetTIMER2_CONF_PARA_UP(1)
case 3:
esp.LEDC.SetTIMER3_CONF_DUTY_RES(uint32(dutyRes))
esp.LEDC.SetTIMER3_CONF_CLK_DIV(divReg)
esp.LEDC.SetTIMER3_CONF_TICK_SEL(0)
esp.LEDC.SetTIMER3_CONF_PAUSE(0)
esp.LEDC.SetTIMER3_CONF_RST(1)
esp.LEDC.SetTIMER3_CONF_RST(0)
esp.LEDC.SetTIMER3_CONF_PARA_UP(1)
}
}
func (pwm *LEDCPWM) Channel(pin Pin) (uint8, error) {
if !pwm.configured {
return 0, errors.New("pwm: not configured")
}
if pin == NoPin {
return 0, ErrInvalidOutputPin
}
var ch uint8
for ch = 0; ch < pwm.NumChannels; ch++ {
if pwm.channelPin[ch] == NoPin {
break
}
}
if ch >= pwm.NumChannels {
return 0, errPWMNoChannel
}
pwm.channelPin[ch] = pin
signal := pwm.SigOutBase + uint32(ch)
pin.configure(PinConfig{Mode: PinOutput}, signal) // GPIO matrix: pin <- LEDC_LS_SIG_OUTn
pwm.chanOp(ch, ledcChanOpInit, 0, false)
return ch, nil
}
func (pwm *LEDCPWM) Set(channel uint8, value uint32) {
if channel >= pwm.NumChannels {
return
}
top := uint32(1<<pwm.dutyRes) - 1
if value > top {
value = top
}
dutyVal := value << ledcDutyFracBits
pwm.chanOp(channel, ledcChanOpSetDuty, dutyVal, false)
}
func (pwm *LEDCPWM) Top() uint32 {
if !pwm.configured {
return 0
}
return uint32(1<<pwm.dutyRes) - 1
}
func (pwm *LEDCPWM) SetInverting(channel uint8, inverting bool) {
if channel >= pwm.NumChannels {
return
}
pwm.chanOp(channel, ledcChanOpSetInvert, 0, inverting)
}