From a335f5a9f6679999888d7f9634245f41d79feea1 Mon Sep 17 00:00:00 2001 From: zombieleet Date: Fri, 11 Sep 2026 00:56:08 +0200 Subject: [PATCH 1/2] machine/esp32: add PWM support using the LEDC peripheral The classic ESP32 had no PWM. This adds it. It uses the high speed half of the LEDC peripheral. This gives 4 timers (PWM0 to PWM3) and 8 channels. You can use any pin, because the signal goes through the GPIO matrix. The ESP32-C3 and the ESP32-S3 have LEDC PWM. Most of that code is the same for all of these chips. This change uses that code again. It does not add a second driver for the same peripheral. Three functions in the shared file work only on the newer chips. They move to a file for each chip. enableClock The C3 and S3 turn on the clock in SYSTEM. This chip uses DPORT. It also has no LEDC.CONF_CLK_EN bit. setTimerConf The timer registers here are HSTIMER0_CONF. On the other chips they are TIMER0_CONF. chanOp The channel registers here are HSCH0_CONF0. On the other chips they are CH0_CONF0. The files are now: machine_esp32xx_pwm.go The shared part. It builds for esp32 also. It calls enableClock and setTimerConf. machine_esp32xx_ls_pwm.go New. It holds the two low speed functions. The code moved without a change. It builds for esp32c3 and esp32s3 only. machine_esp32_pwm.go New. The versions for the classic ESP32. The channel and timer registers repeat at a fixed distance, so chanOp and setTimerConf use pointer arithmetic to find them. Pin.outFunc in machine_esp32.go does the same. Channel registers repeat every 0x14 bytes and timer registers every 0x8. Two hardware details are important. TICK_SEL has the opposite meaning on this chip. Here 1 selects APB_CLK at 80MHz and 0 selects REF_TICK at 1MHz. The low speed timers on the C3 and the S3 write 0. A 0 here makes all frequencies 80 times too slow. There is no PARA_UP bit. On the C3 and the S3 you set PARA_UP to apply a change to a channel. High speed channels apply the change themselves at the end of the period. The reset of the LEDC block happens one time only. The reset clears all timers and all channels, so a Configure of a second timer would erase the first one. Tests: The examples/pwm binaries for esp32c3-supermini and xiao-esp32s3 are the same byte for byte before and after this change. The move of the code did not change it. Tested on an ESP32-WROVER board. A program wrote a different value to all 4 timers and all 8 channels through the pointer arithmetic, then read the values back through the named registers. All of them agree. A servo on GPIO15 turns from 0 to 180 degrees and back. The movement is smooth. This shows that the timing is correct. A servo moves only with pulses between 0.5ms and 2.5ms that repeat near 50Hz. Motor control PWM (the MCPWM peripheral) is not in this change. It needs a different API. --- make/smoketest.mk | 2 + src/examples/pwm/esp32-coreboard-v2.go | 11 ++ src/machine/machine_esp32_pwm.go | 148 +++++++++++++++++++++++++ src/machine/machine_esp32xx_ls_pwm.go | 69 ++++++++++++ src/machine/machine_esp32xx_pwm.go | 86 +++++--------- 5 files changed, 255 insertions(+), 61 deletions(-) create mode 100644 src/examples/pwm/esp32-coreboard-v2.go create mode 100644 src/machine/machine_esp32_pwm.go create mode 100644 src/machine/machine_esp32xx_ls_pwm.go diff --git a/make/smoketest.mk b/make/smoketest.mk index ac42496be4..0316dd6f67 100644 --- a/make/smoketest.mk +++ b/make/smoketest.mk @@ -477,6 +477,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32-coreboard-v2 examples/adc @$(MD5SUM) $(SMOKE_OUT).bin + $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32-coreboard-v2 examples/pwm + @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32c3-generic examples/machinetest @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32s3-generic examples/machinetest diff --git a/src/examples/pwm/esp32-coreboard-v2.go b/src/examples/pwm/esp32-coreboard-v2.go new file mode 100644 index 0000000000..7980431bde --- /dev/null +++ b/src/examples/pwm/esp32-coreboard-v2.go @@ -0,0 +1,11 @@ +//go:build esp32_coreboard_v2 + +package main + +import "machine" + +var ( + pwm = machine.PWM0 + pinA = machine.GPIO18 + pinB = machine.GPIO19 +) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go new file mode 100644 index 0000000000..4e90a0302c --- /dev/null +++ b/src/machine/machine_esp32_pwm.go @@ -0,0 +1,148 @@ +//go:build esp32 + +// PWM on the classic ESP32 uses the LEDC peripheral. +// +// LEDC here has two halves: a high-speed one and a low-speed one. This file uses +// the high-speed half. It gives 4 timers (PWM0-PWM3) and 8 channels. +// +// The code that is the same on every ESP32 chip is in machine_esp32xx_pwm.go. +// This file adds only the parts that are different here. +// +// Three things are different from the ESP32-C3 and the ESP32-S3: +// +// - The clock is switched on in DPORT. The newer chips renamed that block to +// SYSTEM. This chip also has no LEDC.CONF_CLK_EN bit. +// - Register names start with HS, for example HSTIMER0_CONF and HSCH0_CONF0. +// - There is no PARA_UP bit. On the C3 and S3 you set PARA_UP to say "apply my +// changes now". Here the channel applies them by itself, at the end of the +// period it is in. That is what makes it "high speed". +// +// Register addresses and bit positions come from the ESP32 TRM, chapter 14 +// "LED PWM Controller", section 14.4 "Register Summary". + +package machine + +import ( + "device/esp" + "runtime/volatile" + "unsafe" +) + +// The GPIO matrix can send an internal signal to almost any pin. Each signal has +// a number. High-speed LEDC channel 0 is number 71, and the rest follow on from +// there, so channels 0 to 7 are 71 to 78. +// (From Espressif's soc/gpio_sig_map.h: LEDC_HS_SIG_OUT0_IDX.) +const LEDC_HS_SIG_OUT0_IDX = 71 + +const ledcChannelsESP32 = 8 + +// Each channel has 5 registers and each timer has 2, so the blocks repeat at a +// fixed distance. HSCH0_CONF0 is at 0x0 and HSCH1_CONF0 is at 0x14. +// HSTIMER0_CONF is at 0x140 and HSTIMER1_CONF is at 0x148. +const ( + ledcChannelStride = 0x14 + ledcTimerStride = 0x8 +) + +// Bit positions in HSCHn_CONF0, HSCHn_CONF1 and HSTIMERn_CONF. +const ( + ledcTimerSelPos = 0 // CONF0, which timer the channel follows + ledcSigOutEn = 1 << 2 // CONF0, let the channel drive the pin + ledcIdleLv = 1 << 3 // CONF0, pin level when the channel is off + ledcDutyCyclePos = 10 // CONF1 + ledcDutyNumPos = 20 // CONF1 + ledcDutyInc = 1 << 30 // CONF1 + ledcDutyStart = 1 << 31 // CONF1, apply the other CONF1 fields + ledcDivNumPos = 5 // TIMER CONF, clock divider + ledcTimerRst = 1 << 24 // TIMER CONF + ledcTickSelAPB = 1 << 25 // TIMER CONF, 1 is APB_CLK and 0 is REF_TICK +) + +var ( + PWM0 = &LEDCPWM{SigOutBase: LEDC_HS_SIG_OUT0_IDX, NumChannels: ledcChannelsESP32, timerNum: 0} + PWM1 = &LEDCPWM{SigOutBase: LEDC_HS_SIG_OUT0_IDX, NumChannels: ledcChannelsESP32, timerNum: 1} + PWM2 = &LEDCPWM{SigOutBase: LEDC_HS_SIG_OUT0_IDX, NumChannels: ledcChannelsESP32, timerNum: 2} + PWM3 = &LEDCPWM{SigOutBase: LEDC_HS_SIG_OUT0_IDX, NumChannels: ledcChannelsESP32, timerNum: 3} +) + +// ledcStarted is true after the LEDC block came out of reset. The reset clears +// every timer and every channel, so it must happen one time only. Without this +// a second Configure would erase the settings of the first one. +var ledcStarted bool + +// chanReg returns a register of channel ch, given the register of channel 0. +func chanReg(channel0 *volatile.Register32, ch uint8) *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(channel0), uintptr(ch)*ledcChannelStride)) +} + +// timerReg returns a register of timer t, given the register of timer 0. +func timerReg(timer0 *volatile.Register32, t uint8) *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(timer0), uintptr(t)*ledcTimerStride)) +} + +// enableClock turns the LEDC hardware on and picks its clock. +func (pwm *LEDCPWM) enableClock() { + // Every peripheral starts switched off, to save power. These registers are + // in DPORT on this chip. The C3 and S3 call the same block SYSTEM. + if !ledcStarted { + esp.DPORT.SetPERIP_CLK_EN_LEDC_CLK_EN(1) + esp.DPORT.SetPERIP_RST_EN_LEDC_RST(1) + esp.DPORT.SetPERIP_RST_EN_LEDC_RST(0) + ledcStarted = true + } + + // Use APB_CLK, which runs at 80MHz. This chip has no CONF_CLK_EN bit, so + // there is nothing more to switch on. + esp.LEDC.SetCONF_APB_CLK_SEL(1) +} + +// setTimerConf writes the resolution and the divider that Configure worked out +// into one timer. +// +// Watch TICK_SEL. It is 1 here, not 0. On this chip: +// +// 1 = APB_CLK, 80MHz <- what we want +// 0 = REF_TICK, 1MHz +// +// The low speed timers on the C3 and S3 use the opposite meaning, so their code +// writes 0. Writing 0 here would make every frequency 80 times too slow. +// +// These timers have no PARA_UP bit. The short reset pulse at the end is what +// makes the new values take effect. +func (pwm *LEDCPWM) setTimerConf(dutyRes uint8, divReg uint32) { + conf := timerReg(&esp.LEDC.HSTIMER0_CONF, pwm.timerNum) + value := uint32(dutyRes) | divReg< Date: Sat, 19 Sep 2026 14:58:57 +0200 Subject: [PATCH 2/2] machine/esp32xx: share the LEDC channels and fix SetInverting Several problems in the shared LEDC code. They are older than the PWM support for the classic ESP32, but that change makes them reachable. The four timers share one set of 8 channels in the hardware, yet each LEDCPWM had a table of its own that starts at index 0. PWM0.Channel(a) and PWM1.Channel(b) both returned channel 0 and both wrote hardware channel 0, so only one timer worked at a time. The table now belongs to the peripheral and records the timer that owns each entry. Configure cleared that whole table, which with a shared table erases the channels of the other timers, so it now releases only its own. Releasing a channel also disables its output, because the table alone does not stop the hardware from driving the pin. Set and SetInverting took a channel number without checking who owns it. One timer could reprogram another timer's channel, with the duty scaled for the wrong resolution. Both now go through LEDCPWM.owns. SetInverting did not invert. It wrote IDLE_LV, which only sets the pin level when SIG_OUT_EN is 0, so it has no effect on a running signal. The GPIO matrix does the inversion with INV_SEL in FUNCn_OUT_SEL_CFG, and the SVD already gives that bit for each chip. chanOp has no invert operation now, so the operation and its parameter are gone from all three chips. enableClock pulses the LEDC reset, which clears every timer and channel. It now runs once for the C3 and S3 as well as for the classic ESP32. Before this, a Configure of one timer erased all the others, and a call that failed the period check destroyed running outputs on its way out. A period above one second makes the frequency truncate to zero, and the divider then divides by zero. Configure returns ErrPWMPeriodTooLong. Tested on an ESP32-WROVER board. PWM0 to PWM3 gave channels 0, 1, 2 and 3 for 4 pins, where before all four gave channel 0. A pin driven at full duty read 100 percent high, and 0 percent after a second Configure released its channel. A Set from a timer that does not own the channel left the output at 100 percent, so it was refused. SetInverting took the same pin from 100 percent to 0 percent, and back to 99 percent when switched off. Configure with a period of 2 seconds returned "pwm: period too long" instead of a panic. The reads use Pin.Get on the output pin, which works because Pin.configure always sets FUN_IE. --- src/machine/machine_esp32_pwm.go | 21 +++---- src/machine/machine_esp32c3_pwm.go | 42 +++++++------ src/machine/machine_esp32s3_pwm.go | 52 +++++++++------- src/machine/machine_esp32xx_ls_pwm.go | 11 +++- src/machine/machine_esp32xx_pwm.go | 85 +++++++++++++++++++++------ 5 files changed, 139 insertions(+), 72 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 4e90a0302c..0c2bf95098 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -48,7 +48,6 @@ const ( const ( ledcTimerSelPos = 0 // CONF0, which timer the channel follows ledcSigOutEn = 1 << 2 // CONF0, let the channel drive the pin - ledcIdleLv = 1 << 3 // CONF0, pin level when the channel is off ledcDutyCyclePos = 10 // CONF1 ledcDutyNumPos = 20 // CONF1 ledcDutyInc = 1 << 30 // CONF1 @@ -65,11 +64,6 @@ var ( PWM3 = &LEDCPWM{SigOutBase: LEDC_HS_SIG_OUT0_IDX, NumChannels: ledcChannelsESP32, timerNum: 3} ) -// ledcStarted is true after the LEDC block came out of reset. The reset clears -// every timer and every channel, so it must happen one time only. Without this -// a second Configure would erase the settings of the first one. -var ledcStarted bool - // chanReg returns a register of channel ch, given the register of channel 0. func chanReg(channel0 *volatile.Register32, ch uint8) *volatile.Register32 { return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(channel0), uintptr(ch)*ledcChannelStride)) @@ -116,12 +110,17 @@ func (pwm *LEDCPWM) setTimerConf(dutyRes uint8, divReg uint32) { conf.Set(value) } +// chanDisable stops a channel from driving its pin. +func chanDisable(ch uint8) { + chanReg(&esp.LEDC.HSCH0_CONF0, ch).ClearBits(ledcSigOutEn) +} + // chanOp does the work for one channel, numbered 0 to 7. It either sets the -// channel up, changes its duty, or flips its idle level. +// channel up or changes its duty. // // DUTY_SCALE stays 0. LEDC can fade slowly from one duty to the next, and 0 // turns that off, so the duty changes in a single step. -func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) { +func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32) { conf0 := chanReg(&esp.LEDC.HSCH0_CONF0, ch) // DUTY_NUM and DUTY_CYCLE are 1 step of 1 period, which is the smallest @@ -138,11 +137,5 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) chanReg(&esp.LEDC.HSCH0_DUTY, ch).Set(duty) chanReg(&esp.LEDC.HSCH0_CONF1, ch).Set(conf1) conf0.SetBits(ledcSigOutEn) - case ledcChanOpSetInvert: - if inverting { - conf0.SetBits(ledcIdleLv) - } else { - conf0.ClearBits(ledcIdleLv) - } } } diff --git a/src/machine/machine_esp32c3_pwm.go b/src/machine/machine_esp32c3_pwm.go index d7e575e8d9..e43140b9a7 100644 --- a/src/machine/machine_esp32c3_pwm.go +++ b/src/machine/machine_esp32c3_pwm.go @@ -23,12 +23,32 @@ var ( PWM3 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsC3, timerNum: 3} ) -// chanOp implements LEDC low-speed channel ops for ESP32-C3 (channels 0–5 only). -func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) { - invVal := uint32(0) - if inverting { - invVal = 1 +// chanDisable stops a channel from driving its pin. +func chanDisable(ch uint8) { + switch ch { + case 0: + esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH0_CONF0_PARA_UP(1) + case 1: + esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH1_CONF0_PARA_UP(1) + case 2: + esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH2_CONF0_PARA_UP(1) + case 3: + esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH3_CONF0_PARA_UP(1) + case 4: + esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH4_CONF0_PARA_UP(1) + case 5: + esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH5_CONF0_PARA_UP(1) } +} + +// chanOp implements LEDC low-speed channel ops for ESP32-C3 (channels 0–5 only). +func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32) { switch ch { case 0: switch op { @@ -51,8 +71,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH0_CONF1_DUTY_START(1) esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH0_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH0_CONF0_IDLE_LV(invVal) } case 1: switch op { @@ -75,8 +93,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH1_CONF1_DUTY_START(1) esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH1_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH1_CONF0_IDLE_LV(invVal) } case 2: switch op { @@ -99,8 +115,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH2_CONF1_DUTY_START(1) esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH2_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH2_CONF0_IDLE_LV(invVal) } case 3: switch op { @@ -123,8 +137,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH3_CONF1_DUTY_START(1) esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH3_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH3_CONF0_IDLE_LV(invVal) } case 4: switch op { @@ -147,8 +159,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH4_CONF1_DUTY_START(1) esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH4_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH4_CONF0_IDLE_LV(invVal) } case 5: switch op { @@ -171,8 +181,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH5_CONF1_DUTY_START(1) esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH5_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH5_CONF0_IDLE_LV(invVal) } } } diff --git a/src/machine/machine_esp32s3_pwm.go b/src/machine/machine_esp32s3_pwm.go index f4015a5f95..8b8f3cff4b 100644 --- a/src/machine/machine_esp32s3_pwm.go +++ b/src/machine/machine_esp32s3_pwm.go @@ -23,12 +23,38 @@ var ( PWM3 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsS3, timerNum: 3} ) -// chanOp implements LEDC low-speed channel ops for ESP32-S3 (channels 0–7). -func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) { - invVal := uint32(0) - if inverting { - invVal = 1 +// chanDisable stops a channel from driving its pin. +func chanDisable(ch uint8) { + switch ch { + case 0: + esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH0_CONF0_PARA_UP(1) + case 1: + esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH1_CONF0_PARA_UP(1) + case 2: + esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH2_CONF0_PARA_UP(1) + case 3: + esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH3_CONF0_PARA_UP(1) + case 4: + esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH4_CONF0_PARA_UP(1) + case 5: + esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH5_CONF0_PARA_UP(1) + case 6: + esp.LEDC.SetCH6_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH6_CONF0_PARA_UP(1) + case 7: + esp.LEDC.SetCH7_CONF0_SIG_OUT_EN(0) + esp.LEDC.SetCH7_CONF0_PARA_UP(1) } +} + +// chanOp implements LEDC low-speed channel ops for ESP32-S3 (channels 0–7). +func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32) { switch ch { case 0: switch op { @@ -51,8 +77,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH0_CONF1_DUTY_START(1) esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH0_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH0_CONF0_IDLE_LV(invVal) } case 1: switch op { @@ -75,8 +99,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH1_CONF1_DUTY_START(1) esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH1_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH1_CONF0_IDLE_LV(invVal) } case 2: switch op { @@ -99,8 +121,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH2_CONF1_DUTY_START(1) esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH2_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH2_CONF0_IDLE_LV(invVal) } case 3: switch op { @@ -123,8 +143,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH3_CONF1_DUTY_START(1) esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH3_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH3_CONF0_IDLE_LV(invVal) } case 4: switch op { @@ -147,8 +165,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH4_CONF1_DUTY_START(1) esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH4_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH4_CONF0_IDLE_LV(invVal) } case 5: switch op { @@ -171,8 +187,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH5_CONF1_DUTY_START(1) esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH5_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH5_CONF0_IDLE_LV(invVal) } case 6: switch op { @@ -195,8 +209,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH6_CONF1_DUTY_START(1) esp.LEDC.SetCH6_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH6_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH6_CONF0_IDLE_LV(invVal) } case 7: switch op { @@ -219,8 +231,6 @@ func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) esp.LEDC.SetCH7_CONF1_DUTY_START(1) esp.LEDC.SetCH7_CONF0_SIG_OUT_EN(1) esp.LEDC.SetCH7_CONF0_PARA_UP(1) - case ledcChanOpSetInvert: - esp.LEDC.SetCH7_CONF0_IDLE_LV(invVal) } } } diff --git a/src/machine/machine_esp32xx_ls_pwm.go b/src/machine/machine_esp32xx_ls_pwm.go index a1b7138c47..b3927e1cc7 100644 --- a/src/machine/machine_esp32xx_ls_pwm.go +++ b/src/machine/machine_esp32xx_ls_pwm.go @@ -21,9 +21,14 @@ import "device/esp" // enableClock turns the LEDC hardware on and picks APB_CLK as its clock. func (pwm *LEDCPWM) enableClock() { // 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) + // The reset clears every timer and channel, so it runs only once. Without + // the guard a Configure of one timer would erase all the others. + if !ledcStarted { + esp.SYSTEM.SetPERIP_CLK_EN0_LEDC_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_LEDC_RST(1) + esp.SYSTEM.SetPERIP_RST_EN0_LEDC_RST(0) + ledcStarted = true + } // LEDC global: APB clock source, enable internal clock. esp.LEDC.SetCONF_APB_CLK_SEL(1) diff --git a/src/machine/machine_esp32xx_pwm.go b/src/machine/machine_esp32xx_pwm.go index 558e720153..e98487b189 100644 --- a/src/machine/machine_esp32xx_pwm.go +++ b/src/machine/machine_esp32xx_pwm.go @@ -8,12 +8,13 @@ // signal reaches a pin through the GPIO matrix, using signal number // SigOutBase + channel. // -// This file holds the part that is the same on every chip. Three functions are +// This file holds the part that is the same on every chip. These functions are // different per chip and live in other files: // // enableClock turn the LEDC hardware on and pick its clock // setTimerConf program one timer -// chanOp set up a channel, change its duty, or invert it +// chanOp set up a channel or change its duty +// chanDisable stop a channel from driving its pin // // The classic ESP32 has them in machine_esp32_pwm.go. The C3 and S3 have them in // machine_esp32xx_ls_pwm.go and machine_esp32{c3,s3}_pwm.go. @@ -24,7 +25,10 @@ package machine -import "errors" +import ( + "device/esp" + "errors" +) const ledcApbClock = 80_000000 @@ -34,21 +38,31 @@ const ledcDividerFracBits = 8 // Clock divider register = actual_divider * 256 var errPWMNoChannel = errors.New("pwm: no free channel") +// ledcStarted is true once the LEDC block has come out of reset. The reset +// clears every timer and channel, so it must happen only once. +var ledcStarted bool + type LEDCPWM struct { SigOutBase uint32 // GPIO matrix signal index for channel 0 (e.g. 73 on S3, 45 on C3) NumChannels uint8 timerNum uint8 // 0–3: which LEDC timer (frequency) this PWM uses dutyRes uint8 configured bool - channelPin [8]Pin +} + +// The timers share one set of channels, so this table is for the whole +// peripheral. A table in LEDCPWM would give channel 0 to every timer. +var ledcChannels [8]struct { + pin Pin + timer uint8 + inUse bool } 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) + ledcChanOpInit ledcChanOp = iota // initial per-channel setup (timer, enable, HPOINT, DUTY, CONF1) + ledcChanOpSetDuty // write the duty and make the hardware use it ) func (pwm *LEDCPWM) Configure(config PWMConfig) error { @@ -61,6 +75,11 @@ func (pwm *LEDCPWM) Configure(config PWMConfig) error { period = 1_000_000 } freq := uint64(1e9) / period + if freq == 0 { + // A period above one second cannot be reached, and it would make the + // divider below a division by zero. + return ErrPWMPeriodTooLong + } dutyRes := uint8(10) switch { case freq < 100: @@ -81,13 +100,22 @@ func (pwm *LEDCPWM) Configure(config PWMConfig) error { return ErrPWMPeriodTooLong } - // Selected timer: resolution, divider, no pause, reset then latch config with PARA_UP. + // Program the selected timer with the resolution and the divider. How the + // new values take effect differs per chip, so setTimerConf does that part. pwm.setTimerConf(dutyRes, divReg) pwm.dutyRes = dutyRes pwm.configured = true - for i := range pwm.channelPin { - pwm.channelPin[i] = NoPin + + // Free the channels of this timer only. The other timers keep theirs. + // Each one must also stop driving its pin, because the table alone does + // not stop the hardware. + for i := range ledcChannels { + if ledcChannels[i].inUse && ledcChannels[i].timer == pwm.timerNum { + chanDisable(uint8(i)) + ledcChannels[i].pin = NoPin + ledcChannels[i].inUse = false + } } return nil } @@ -101,7 +129,7 @@ func (pwm *LEDCPWM) Channel(pin Pin) (uint8, error) { } var ch uint8 for ch = 0; ch < pwm.NumChannels; ch++ { - if pwm.channelPin[ch] == NoPin { + if !ledcChannels[ch].inUse { break } } @@ -109,15 +137,17 @@ func (pwm *LEDCPWM) Channel(pin Pin) (uint8, error) { return 0, errPWMNoChannel } - pwm.channelPin[ch] = pin + ledcChannels[ch].pin = pin + ledcChannels[ch].timer = pwm.timerNum + ledcChannels[ch].inUse = true signal := pwm.SigOutBase + uint32(ch) pin.configure(PinConfig{Mode: PinOutput}, signal) // GPIO matrix: pin <- LEDC_LS_SIG_OUTn - pwm.chanOp(ch, ledcChanOpInit, 0, false) + pwm.chanOp(ch, ledcChanOpInit, 0) return ch, nil } func (pwm *LEDCPWM) Set(channel uint8, value uint32) { - if channel >= pwm.NumChannels { + if !pwm.owns(channel) { return } top := uint32(1<= pwm.NumChannels { + if !pwm.owns(channel) { return } - pwm.chanOp(channel, ledcChanOpSetInvert, 0, inverting) + reg := ledcChannels[channel].pin.outFunc() + if inverting { + reg.SetBits(esp.GPIO_FUNC_OUT_SEL_CFG_INV_SEL) + } else { + reg.ClearBits(esp.GPIO_FUNC_OUT_SEL_CFG_INV_SEL) + } +} + +// owns reports whether this timer holds the channel. The channels are shared, +// so a number on its own does not say which timer programmed it. +func (pwm *LEDCPWM) owns(channel uint8) bool { + return channel < pwm.NumChannels && + ledcChannels[channel].inUse && + ledcChannels[channel].timer == pwm.timerNum }