From ff58c501188972db4d2522acbac56579d5ab2464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 12 Jan 2026 13:38:20 +0100 Subject: [PATCH 01/16] machine: add attiny85 pwm support (#5171) * machine/attiny85: add PWM support for Timer0 and Timer1 Add complete PWM implementation for ATtiny85, supporting both Timer0 and Timer1 with their respective output channels: - Timer0: 8-bit timer for pins PB0 (OC0A) and PB1 (OC0B) - Timer1: 8-bit high-speed timer for pins PB1 (OC1A) and PB4 (OC1B) Timer1 provides more flexible period control with configurable top value (OCR1C) and extended prescaler options (1-16384), making it well-suited for LED PWM control and other applications requiring variable frequencies. Implements full PWM interface including Configure, SetPeriod, Channel, Set, SetInverting, Top, Counter, and Period methods. Co-Authored-By: Claude Sonnet 4.5 * machine/digispark: document PWM support on pins Add documentation to the Digispark board file indicating which pins support PWM output: - P0 (PB0): Timer0 channel A - P1 (PB1): Timer0 channel B or Timer1 channel A - P4 (PB4): Timer1 channel B Includes package comment explaining Timer0 vs Timer1 capabilities, with Timer1 recommended for more flexible frequency control. Co-Authored-By: Claude Sonnet 4.5 * machine/attiny85: optimize PWM prescaler lookups Replace verbose switch statements with more efficient implementations: - SetPeriod: Use bit shift (top >>= prescaler-1) instead of 15-case switch for dividing uint64 by power-of-2 prescaler values - Period: Replace switch statements with compact uint16 lookup tables for both Timer0 and Timer1, casting to uint64 only when needed This addresses review feedback about inefficient switch-based lookups. On AVR, this approach is significantly smaller: - Bit shifts for uint64 division: ~34 bytes vs ~140 bytes - uint16 tables: 22 bytes code + 32/16 bytes data vs ~140 bytes - Total savings: ~190 bytes (68% reduction) Co-Authored-By: Claude Sonnet 4.5 * examples/pwm: add digispark support and smoketest Add digispark.go configuration for PWM example using Timer1 with pins P1 (LED) and P4. Also add digispark PWM example to GNUmakefile smoketests. Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Sonnet 4.5 --- GNUmakefile | 2 + src/examples/pwm/digispark.go | 12 ++ src/machine/board_digispark.go | 15 +- src/machine/machine_attiny85.go | 354 ++++++++++++++++++++++++++++++++ 4 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 src/examples/pwm/digispark.go diff --git a/GNUmakefile b/GNUmakefile index 99a654ca7f..322c0b7cff 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -896,6 +896,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark examples/blinky1 @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=digispark examples/pwm + @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark -gc=leaking examples/blinky1 @$(MD5SUM) test.hex ifneq ($(XTENSA), 0) diff --git a/src/examples/pwm/digispark.go b/src/examples/pwm/digispark.go new file mode 100644 index 0000000000..848d518546 --- /dev/null +++ b/src/examples/pwm/digispark.go @@ -0,0 +1,12 @@ +//go:build digispark + +package main + +import "machine" + +var ( + // Use Timer1 for PWM (recommended for ATtiny85) + pwm = machine.Timer1 + pinA = machine.P1 // PB1, Timer1 channel A (LED pin) + pinB = machine.P4 // PB4, Timer1 channel B +) diff --git a/src/machine/board_digispark.go b/src/machine/board_digispark.go index f380aae85c..d7106a5544 100644 --- a/src/machine/board_digispark.go +++ b/src/machine/board_digispark.go @@ -2,17 +2,26 @@ package machine +// Digispark is a tiny ATtiny85-based board with 6 I/O pins. +// +// PWM is available on the following pins: +// - P0 (PB0): Timer0 channel A +// - P1 (PB1): Timer0 channel B or Timer1 channel A (LED pin) +// - P4 (PB4): Timer1 channel B +// +// Timer1 is recommended for PWM as it provides more flexible frequency control. + // Return the current CPU frequency in hertz. func CPUFrequency() uint32 { return 16000000 } const ( - P0 Pin = PB0 - P1 Pin = PB1 + P0 Pin = PB0 // PWM available (Timer0 OC0A) + P1 Pin = PB1 // PWM available (Timer0 OC0B or Timer1 OC1A) P2 Pin = PB2 P3 Pin = PB3 - P4 Pin = PB4 + P4 Pin = PB4 // PWM available (Timer1 OC1B) P5 Pin = PB5 LED = P1 diff --git a/src/machine/machine_attiny85.go b/src/machine/machine_attiny85.go index 33424c6052..27adaf948c 100644 --- a/src/machine/machine_attiny85.go +++ b/src/machine/machine_attiny85.go @@ -21,3 +21,357 @@ func (p Pin) getPortMask() (*volatile.Register8, uint8) { // Very simple for the attiny85, which only has a single port. return avr.PORTB, 1 << uint8(p) } + +// PWM is one PWM peripheral, which consists of a counter and two output +// channels (that can be connected to two fixed pins). You can set the frequency +// using SetPeriod, but only for all the channels in this PWM peripheral at +// once. +type PWM struct { + num uint8 +} + +var ( + Timer0 = PWM{0} // 8 bit timer for PB0 and PB1 + Timer1 = PWM{1} // 8 bit high-speed timer for PB1 and PB4 +) + +// GTCCR bits for Timer1 that are not defined in the device file +const ( + gtccrPWM1B = 0x40 // Pulse Width Modulator B Enable + gtccrCOM1B0 = 0x10 // Comparator B Output Mode bit 0 + gtccrCOM1B1 = 0x20 // Comparator B Output Mode bit 1 +) + +// Configure enables and configures this PWM. +// +// For Timer0, there is only a limited number of periods available, namely the +// CPU frequency divided by 256 and again divided by 1, 8, 64, 256, or 1024. +// For a MCU running at 8MHz, this would be a period of 32µs, 256µs, 2048µs, +// 8192µs, or 32768µs. +// +// For Timer1, the period is more flexible as it uses OCR1C as the top value. +// Timer1 also supports more prescaler values (1 to 16384). +func (pwm PWM) Configure(config PWMConfig) error { + switch pwm.num { + case 0: // Timer/Counter 0 (8-bit) + // Calculate the timer prescaler. + var prescaler uint8 + switch config.Period { + case 0, (uint64(1e9) * 256 * 1) / uint64(CPUFrequency()): + prescaler = 1 + case (uint64(1e9) * 256 * 8) / uint64(CPUFrequency()): + prescaler = 2 + case (uint64(1e9) * 256 * 64) / uint64(CPUFrequency()): + prescaler = 3 + case (uint64(1e9) * 256 * 256) / uint64(CPUFrequency()): + prescaler = 4 + case (uint64(1e9) * 256 * 1024) / uint64(CPUFrequency()): + prescaler = 5 + default: + return ErrPWMPeriodTooLong + } + + avr.TCCR0B.Set(prescaler) + // Set the PWM mode to fast PWM (mode = 3). + avr.TCCR0A.Set(avr.TCCR0A_WGM00 | avr.TCCR0A_WGM01) + + case 1: // Timer/Counter 1 (8-bit high-speed) + // Timer1 on ATtiny85 is different from ATmega328: + // - It's 8-bit with configurable top (OCR1C) + // - Has more prescaler options (1-16384) + // - PWM mode is enabled per-channel via PWM1A/PWM1B bits + var top uint64 + if config.Period == 0 { + // Use a top appropriate for LEDs. + top = 0xff + } else { + // Calculate top value: top = period * (CPUFrequency / 1e9) + top = config.Period * (uint64(CPUFrequency()) / 1000000) / 1000 + } + + // Timer1 prescaler values: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 + const maxTop = 256 + var prescaler uint8 + switch { + case top <= maxTop: + prescaler = 1 // prescaler 1 + case top/2 <= maxTop: + prescaler = 2 // prescaler 2 + top /= 2 + case top/4 <= maxTop: + prescaler = 3 // prescaler 4 + top /= 4 + case top/8 <= maxTop: + prescaler = 4 // prescaler 8 + top /= 8 + case top/16 <= maxTop: + prescaler = 5 // prescaler 16 + top /= 16 + case top/32 <= maxTop: + prescaler = 6 // prescaler 32 + top /= 32 + case top/64 <= maxTop: + prescaler = 7 // prescaler 64 + top /= 64 + case top/128 <= maxTop: + prescaler = 8 // prescaler 128 + top /= 128 + case top/256 <= maxTop: + prescaler = 9 // prescaler 256 + top /= 256 + case top/512 <= maxTop: + prescaler = 10 // prescaler 512 + top /= 512 + case top/1024 <= maxTop: + prescaler = 11 // prescaler 1024 + top /= 1024 + case top/2048 <= maxTop: + prescaler = 12 // prescaler 2048 + top /= 2048 + case top/4096 <= maxTop: + prescaler = 13 // prescaler 4096 + top /= 4096 + case top/8192 <= maxTop: + prescaler = 14 // prescaler 8192 + top /= 8192 + case top/16384 <= maxTop: + prescaler = 15 // prescaler 16384 + top /= 16384 + default: + return ErrPWMPeriodTooLong + } + + // Set prescaler (CS1[3:0] bits) + avr.TCCR1.Set(prescaler) + // Set top value + avr.OCR1C.Set(uint8(top - 1)) + } + return nil +} + +// SetPeriod updates the period of this PWM peripheral. +// To set a particular frequency, use the following formula: +// +// period = 1e9 / frequency +// +// If you use a period of 0, a period that works well for LEDs will be picked. +// +// SetPeriod will not change the prescaler, but also won't change the current +// value in any of the channels. This means that you may need to update the +// value for the particular channel. +// +// Note that you cannot pick any arbitrary period after the PWM peripheral has +// been configured. If you want to switch between frequencies, pick the lowest +// frequency (longest period) once when calling Configure and adjust the +// frequency here as needed. +func (pwm PWM) SetPeriod(period uint64) error { + if pwm.num == 0 { + return ErrPWMPeriodTooLong // Timer0 doesn't support dynamic period + } + + // Timer1 can adjust period via OCR1C + var top uint64 + if period == 0 { + top = 0xff + } else { + top = period * (uint64(CPUFrequency()) / 1000000) / 1000 + } + + // Get current prescaler + prescaler := avr.TCCR1.Get() & 0x0f + // Timer1 prescaler values follow a power-of-2 pattern: + // prescaler n maps to divisor 2^(n-1), so we can use a simple shift + if prescaler > 0 && prescaler <= 15 { + top >>= (prescaler - 1) + } + + if top > 256 { + return ErrPWMPeriodTooLong + } + + avr.OCR1C.Set(uint8(top - 1)) + avr.TCNT1.Set(0) + + return nil +} + +// Top returns the current counter top, for use in duty cycle calculation. It +// will only change with a call to Configure or SetPeriod, otherwise it is +// constant. +// +// The value returned here is hardware dependent. In general, it's best to treat +// it as an opaque value that can be divided by some number and passed to Set +// (see Set documentation for more information). +func (pwm PWM) Top() uint32 { + if pwm.num == 1 { + // Timer1 has configurable top via OCR1C + return uint32(avr.OCR1C.Get()) + 1 + } + // Timer0 goes from 0 to 0xff (256 in total) + return 256 +} + +// Counter returns the current counter value of the timer in this PWM +// peripheral. It may be useful for debugging. +func (pwm PWM) Counter() uint32 { + switch pwm.num { + case 0: + return uint32(avr.TCNT0.Get()) + case 1: + return uint32(avr.TCNT1.Get()) + } + return 0 +} + +// Prescaler lookup tables using uint16 (more efficient than uint64 on AVR) +// Timer0 prescaler lookup table (index 0-7 maps to prescaler bits) +var timer0Prescalers = [8]uint16{0, 1, 8, 64, 256, 1024, 0, 0} + +// Timer1 prescaler lookup table (index 0-15 maps to prescaler bits) +var timer1Prescalers = [16]uint16{0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384} + +// Period returns the used PWM period in nanoseconds. It might deviate slightly +// from the configured period due to rounding. +func (pwm PWM) Period() uint64 { + var prescaler uint64 + switch pwm.num { + case 0: + prescalerBits := avr.TCCR0B.Get() & 0x7 + prescaler = uint64(timer0Prescalers[prescalerBits]) + if prescaler == 0 { + return 0 + } + case 1: + prescalerBits := avr.TCCR1.Get() & 0x0f + prescaler = uint64(timer1Prescalers[prescalerBits]) + if prescaler == 0 { + return 0 + } + } + top := uint64(pwm.Top()) + return prescaler * top * 1000 / uint64(CPUFrequency()/1e6) +} + +// Channel returns a PWM channel for the given pin. +func (pwm PWM) Channel(pin Pin) (uint8, error) { + pin.Configure(PinConfig{Mode: PinOutput}) + pin.Low() + switch pwm.num { + case 0: + switch pin { + case PB0: // OC0A + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) + return 0, nil + case PB1: // OC0B + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) + return 1, nil + } + case 1: + switch pin { + case PB1: // OC1A + // Enable PWM on channel A + avr.TCCR1.SetBits(avr.TCCR1_PWM1A | avr.TCCR1_COM1A1) + return 0, nil + case PB4: // OC1B + // Enable PWM on channel B (controlled via GTCCR) + avr.GTCCR.SetBits(gtccrPWM1B | gtccrCOM1B1) + return 1, nil + } + } + return 0, ErrInvalidOutputPin +} + +// SetInverting sets whether to invert the output of this channel. +// Without inverting, a 25% duty cycle would mean the output is high for 25% of +// the time and low for the rest. Inverting flips the output as if a NOT gate +// was placed at the output, meaning that the output would be 25% low and 75% +// high with a duty cycle of 25%. +func (pwm PWM) SetInverting(channel uint8, inverting bool) { + switch pwm.num { + case 0: + switch channel { + case 0: // channel A, PB0 + if inverting { + avr.PORTB.SetBits(1 << 0) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A0) + } else { + avr.PORTB.ClearBits(1 << 0) + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0A0) + } + case 1: // channel B, PB1 + if inverting { + avr.PORTB.SetBits(1 << 1) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B0) + } else { + avr.PORTB.ClearBits(1 << 1) + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0B0) + } + } + case 1: + switch channel { + case 0: // channel A, PB1 + if inverting { + avr.PORTB.SetBits(1 << 1) + avr.TCCR1.SetBits(avr.TCCR1_COM1A0) + } else { + avr.PORTB.ClearBits(1 << 1) + avr.TCCR1.ClearBits(avr.TCCR1_COM1A0) + } + case 1: // channel B, PB4 + if inverting { + avr.PORTB.SetBits(1 << 4) + avr.GTCCR.SetBits(gtccrCOM1B0) + } else { + avr.PORTB.ClearBits(1 << 4) + avr.GTCCR.ClearBits(gtccrCOM1B0) + } + } + } +} + +// Set updates the channel value. This is used to control the channel duty +// cycle, in other words the fraction of time the channel output is high (or low +// when inverted). For example, to set it to a 25% duty cycle, use: +// +// pwm.Set(channel, pwm.Top() / 4) +// +// pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, +// pwm.Top()) will set the output to high, assuming the output isn't inverted. +func (pwm PWM) Set(channel uint8, value uint32) { + switch pwm.num { + case 0: + switch channel { + case 0: // channel A, PB0 + if value == 0 { + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0A1) + } else { + avr.OCR0A.Set(uint8(value - 1)) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) + } + case 1: // channel B, PB1 + if value == 0 { + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0B1) + } else { + avr.OCR0B.Set(uint8(value - 1)) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) + } + } + case 1: + switch channel { + case 0: // channel A, PB1 + if value == 0 { + avr.TCCR1.ClearBits(avr.TCCR1_COM1A1) + } else { + avr.OCR1A.Set(uint8(value - 1)) + avr.TCCR1.SetBits(avr.TCCR1_COM1A1) + } + case 1: // channel B, PB4 + if value == 0 { + avr.GTCCR.ClearBits(gtccrCOM1B1) + } else { + avr.OCR1B.Set(uint8(value - 1)) + avr.GTCCR.SetBits(gtccrCOM1B1) + } + } + } +} From 8bd2233b57f03ec2ef699786fb4f6256b4b3dbae Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 13 Jan 2026 01:22:20 +0100 Subject: [PATCH 02/16] machine/rp: use the blockReset() and unresetBlockWait() helper functions for all peripheral reset/unreset operations Signed-off-by: deadprogram --- src/machine/machine_rp2_adc.go | 6 ++---- src/machine/machine_rp2_i2c.go | 7 ++----- src/machine/machine_rp2_spi.go | 7 ++----- src/machine/machine_rp2_uart.go | 6 ++---- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/machine/machine_rp2_adc.go b/src/machine/machine_rp2_adc.go index e0d6a459a9..12ff152dc9 100644 --- a/src/machine/machine_rp2_adc.go +++ b/src/machine/machine_rp2_adc.go @@ -19,10 +19,8 @@ var adcAref uint32 // InitADC resets the ADC peripheral. func InitADC() { - rp.RESETS.RESET.SetBits(rp.RESETS_RESET_ADC) - rp.RESETS.RESET.ClearBits(rp.RESETS_RESET_ADC) - for !rp.RESETS.RESET_DONE.HasBits(rp.RESETS_RESET_ADC) { - } + resetBlock(rp.RESETS_RESET_ADC) + unresetBlockWait(rp.RESETS_RESET_ADC) // enable ADC rp.ADC.CS.Set(rp.ADC_CS_EN) adcAref = 3300 diff --git a/src/machine/machine_rp2_i2c.go b/src/machine/machine_rp2_i2c.go index 54a5e5357b..50e2e8a277 100644 --- a/src/machine/machine_rp2_i2c.go +++ b/src/machine/machine_rp2_i2c.go @@ -259,10 +259,7 @@ func (i2c *I2C) init(config I2CConfig) error { //go:inline func (i2c *I2C) reset() { resetVal := i2c.deinit() - rp.RESETS.RESET.ClearBits(resetVal) - // Wait until reset is done. - for !rp.RESETS.RESET_DONE.HasBits(resetVal) { - } + unresetBlockWait(resetVal) } // deinit sets reset bit for I2C. Must call reset to reenable I2C after deinit. @@ -276,7 +273,7 @@ func (i2c *I2C) deinit() (resetVal uint32) { resetVal = rp.RESETS_RESET_I2C1 } // Perform I2C reset. - rp.RESETS.RESET.SetBits(resetVal) + resetBlock(resetVal) return resetVal } diff --git a/src/machine/machine_rp2_spi.go b/src/machine/machine_rp2_spi.go index 75e4f86b7b..f3fb256f61 100644 --- a/src/machine/machine_rp2_spi.go +++ b/src/machine/machine_rp2_spi.go @@ -212,10 +212,7 @@ func (spi *SPI) setFormat(mode uint8) { //go:inline func (spi *SPI) reset() { resetVal := spi.deinit() - rp.RESETS.RESET.ClearBits(resetVal) - // Wait until reset is done. - for !rp.RESETS.RESET_DONE.HasBits(resetVal) { - } + unresetBlockWait(resetVal) } //go:inline @@ -227,7 +224,7 @@ func (spi *SPI) deinit() (resetVal uint32) { resetVal = rp.RESETS_RESET_SPI1 } // Perform SPI reset. - rp.RESETS.RESET.SetBits(resetVal) + resetBlock(resetVal) return resetVal } diff --git a/src/machine/machine_rp2_uart.go b/src/machine/machine_rp2_uart.go index 872418a766..5961d3a869 100644 --- a/src/machine/machine_rp2_uart.go +++ b/src/machine/machine_rp2_uart.go @@ -148,10 +148,8 @@ func initUART(uart *UART) { } // reset UART - rp.RESETS.RESET.SetBits(resetVal) - rp.RESETS.RESET.ClearBits(resetVal) - for !rp.RESETS.RESET_DONE.HasBits(resetVal) { - } + resetBlock(resetVal) + unresetBlockWait(resetVal) } // handleInterrupt should be called from the appropriate interrupt handler for From 1fe934e8a8a9779d966d60dd91868f761be8bf91 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 13 Jan 2026 17:33:37 +0100 Subject: [PATCH 03/16] machine/rp: add Close function to UART to allow for removing all system resources/power usage Signed-off-by: deadprogram --- src/machine/machine_rp2_uart.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/machine/machine_rp2_uart.go b/src/machine/machine_rp2_uart.go index 5961d3a869..37e2ca9c2a 100644 --- a/src/machine/machine_rp2_uart.go +++ b/src/machine/machine_rp2_uart.go @@ -73,6 +73,27 @@ func (uart *UART) Configure(config UARTConfig) error { return nil } +// Close the UART and disable its interrupt/power use. +func (uart *UART) Close() error { + uart.Interrupt.Disable() + + // Disable UART. + uart.Bus.UARTCR.ClearBits(rp.UART0_UARTCR_UARTEN) + + var resetVal uint32 + switch { + case uart.Bus == rp.UART0: + resetVal = rp.RESETS_RESET_UART0 + case uart.Bus == rp.UART1: + resetVal = rp.RESETS_RESET_UART1 + } + + // reset UART + resetBlock(resetVal) + + return nil +} + // SetBaudRate sets the baudrate to be used for the UART. func (uart *UART) SetBaudRate(br uint32) { div := 8 * CPUFrequency() / br From 1876b65b18f9f9e5f3a4fe9f407b2a9d7562a7b4 Mon Sep 17 00:00:00 2001 From: Nia Waldvogel Date: Sun, 28 Dec 2025 14:59:13 -0500 Subject: [PATCH 04/16] compiler: simplify createObjectLayout This simplifies the process of constructing and encoding layout bitmaps. Instead of creating big integers and merging them, we can create a pre-sized bitmap and set positions within it. This also changes the encoding logic to allow larger layouts to be encoded inline. We would previously not encode a layout inline unless the size was less than the width of the data field. This is overly conservative. A layout can be encoded inline as long as: 1. The size fits within the size field. 2. All set bits in the bitmap fit into the data field. --- compiler/gc.go | 3 + compiler/llvm.go | 182 ++++++++++++++++++++-------------------- compiler/testdata/gc.go | 8 ++ compiler/testdata/gc.ll | 12 ++- 4 files changed, 110 insertions(+), 95 deletions(-) diff --git a/compiler/gc.go b/compiler/gc.go index fc0e6e687f..5ca79b91ba 100644 --- a/compiler/gc.go +++ b/compiler/gc.go @@ -99,6 +99,9 @@ func typeHasPointers(t llvm.Type) bool { } return false case llvm.ArrayTypeKind: + if t.ArrayLength() == 0 { + return false + } if typeHasPointers(t.ElementType()) { return true } diff --git a/compiler/llvm.go b/compiler/llvm.go index de387b39c0..7ce6c7d615 100644 --- a/compiler/llvm.go +++ b/compiler/llvm.go @@ -1,10 +1,10 @@ package compiler import ( + "encoding/binary" "fmt" "go/token" "go/types" - "math/big" "strings" "github.com/tinygo-org/tinygo/compileopts" @@ -231,6 +231,12 @@ func (c *compilerContext) makeGlobalArray(buf []byte, name string, elementType l // // For details on what's in this value, see src/runtime/gc_precise.go. func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Value { + if !typeHasPointers(t) { + // There are no pointers in this type, so we can simplify the layout. + layout := (uint64(1) << 1) | 1 + return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) + } + // Use the element type for arrays. This works even for nested arrays. for { kind := t.TypeKind() @@ -248,54 +254,29 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va break } - // Do a few checks to see whether we need to generate any object layout - // information at all. + // Create the pointer bitmap. objectSizeBytes := c.targetData.TypeAllocSize(t) - pointerSize := c.targetData.TypeAllocSize(c.dataPtrType) - pointerAlignment := c.targetData.PrefTypeAlignment(c.dataPtrType) - if objectSizeBytes < pointerSize { - // Too small to contain a pointer. - layout := (uint64(1) << 1) | 1 - return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) - } - bitmap := c.getPointerBitmap(t, pos) - if bitmap.BitLen() == 0 { - // There are no pointers in this type, so we can simplify the layout. - // TODO: this can be done in many other cases, e.g. when allocating an - // array (like [4][]byte, which repeats a slice 4 times). - layout := (uint64(1) << 1) | 1 - return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) - } - if objectSizeBytes%uint64(pointerAlignment) != 0 { - // This shouldn't happen except for packed structs, which aren't - // currently used. - c.addError(pos, "internal error: unexpected object size for object with pointer field") - return llvm.ConstNull(c.dataPtrType) - } - objectSizeWords := objectSizeBytes / uint64(pointerAlignment) + pointerAlignment := uint64(c.targetData.PrefTypeAlignment(c.dataPtrType)) + bitmapLen := objectSizeBytes / pointerAlignment + bitmapBytes := (bitmapLen + 7) / 8 + bitmap := make([]byte, bitmapBytes, max(bitmapBytes, 8)) + c.buildPointerBitmap(bitmap, pointerAlignment, pos, t, 0) + // Try to encode the layout inline. + pointerSize := c.targetData.TypeAllocSize(c.dataPtrType) pointerBits := pointerSize * 8 - var sizeFieldBits uint64 - switch pointerBits { - case 16: - sizeFieldBits = 4 - case 32: - sizeFieldBits = 5 - case 64: - sizeFieldBits = 6 - default: - panic("unknown pointer size") - } - layoutFieldBits := pointerBits - 1 - sizeFieldBits - - // Try to emit the value as an inline integer. This is possible in most - // cases. - if objectSizeWords < layoutFieldBits { - // If it can be stored directly in the pointer value, do so. - // The runtime knows that if the least significant bit of the pointer is - // set, the pointer contains the value itself. - layout := bitmap.Uint64()<<(sizeFieldBits+1) | (objectSizeWords << 1) | 1 - return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) + if bitmapLen < pointerBits { + rawMask := binary.LittleEndian.Uint64(bitmap[0:8]) + layout := rawMask*pointerBits + bitmapLen + layout <<= 1 + layout |= 1 + + // Check if the layout fits. + layout &= 1<>1)/pointerBits == rawMask { + // No set bits were shifted off. + return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) + } } // Unfortunately, the object layout is too big to fit in a pointer-sized @@ -303,25 +284,24 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va // Try first whether the global already exists. All objects with a // particular name have the same type, so this is possible. - globalName := "runtime/gc.layout:" + fmt.Sprintf("%d-%0*x", objectSizeWords, (objectSizeWords+15)/16, bitmap) + globalName := "runtime/gc.layout:" + fmt.Sprintf("%d-%0*x", bitmapLen, (bitmapLen+15)/16, bitmap) global := c.mod.NamedGlobal(globalName) if !global.IsNil() { return global } // Create the global initializer. - bitmapBytes := make([]byte, int(objectSizeWords+7)/8) - bitmap.FillBytes(bitmapBytes) - reverseBytes(bitmapBytes) // big-endian to little-endian - var bitmapByteValues []llvm.Value - for _, b := range bitmapBytes { - bitmapByteValues = append(bitmapByteValues, llvm.ConstInt(c.ctx.Int8Type(), uint64(b), false)) + bitmapByteValues := make([]llvm.Value, bitmapBytes) + i8 := c.ctx.Int8Type() + for i, b := range bitmap { + bitmapByteValues[i] = llvm.ConstInt(i8, uint64(b), false) } initializer := c.ctx.ConstStruct([]llvm.Value{ - llvm.ConstInt(c.uintptrType, objectSizeWords, false), - llvm.ConstArray(c.ctx.Int8Type(), bitmapByteValues), + llvm.ConstInt(c.uintptrType, bitmapLen, false), + llvm.ConstArray(i8, bitmapByteValues), }, false) + // Create the actual global. global = llvm.AddGlobal(c.mod, initializer.Type(), globalName) global.SetInitializer(initializer) global.SetUnnamedAddr(true) @@ -329,6 +309,7 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va global.SetLinkage(llvm.LinkOnceODRLinkage) if c.targetData.PrefTypeAlignment(c.uintptrType) < 2 { // AVR doesn't have alignment by default. + // The lowest bit must be unset to distinguish this from an inline layout. global.SetAlignment(2) } if c.Debug && pos != token.NoPos { @@ -360,52 +341,71 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va return global } -// getPointerBitmap scans the given LLVM type for pointers and sets bits in a -// bigint at the word offset that contains a pointer. This scan is recursive. -func (c *compilerContext) getPointerBitmap(typ llvm.Type, pos token.Pos) *big.Int { - alignment := c.targetData.PrefTypeAlignment(c.dataPtrType) - switch typ.TypeKind() { +// buildPointerBitmap scans the given LLVM type for pointers and sets bits in a +// bitmap at the word offset that contains a pointer. This scan is recursive. +func (c *compilerContext) buildPointerBitmap( + dst []byte, + ptrAlign uint64, + pos token.Pos, + t llvm.Type, + offset uint64, +) { + switch t.TypeKind() { case llvm.IntegerTypeKind, llvm.FloatTypeKind, llvm.DoubleTypeKind: - return big.NewInt(0) + // These types do not contain pointers. + case llvm.PointerTypeKind: - return big.NewInt(1) + // Set the corresponding position in the bitmap. + dst[offset/8] |= 1 << (offset % 8) + case llvm.StructTypeKind: - ptrs := big.NewInt(0) - for i, subtyp := range typ.StructElementTypes() { - subptrs := c.getPointerBitmap(subtyp, pos) - if subptrs.BitLen() == 0 { - continue - } - offset := c.targetData.ElementOffset(typ, i) - if offset%uint64(alignment) != 0 { - // This error will let the compilation fail, but by continuing - // the error can still easily be shown. - c.addError(pos, "internal error: allocated struct contains unaligned pointer") + // Recurse over struct elements. + for i, et := range t.StructElementTypes() { + eo := c.targetData.ElementOffset(t, i) + if eo%uint64(ptrAlign) != 0 { + if typeHasPointers(et) { + // This error will let the compilation fail, but by continuing + // the error can still easily be shown. + c.addError(pos, "internal error: allocated struct contains unaligned pointer") + } continue } - subptrs.Lsh(subptrs, uint(offset)/uint(alignment)) - ptrs.Or(ptrs, subptrs) + c.buildPointerBitmap( + dst, + ptrAlign, + pos, + et, + offset+(eo/ptrAlign), + ) } - return ptrs + case llvm.ArrayTypeKind: - subtyp := typ.ElementType() - subptrs := c.getPointerBitmap(subtyp, pos) - ptrs := big.NewInt(0) - if subptrs.BitLen() == 0 { - return ptrs + // Recurse over array elements. + len := t.ArrayLength() + if len <= 0 { + return } - elementSize := c.targetData.TypeAllocSize(subtyp) - if elementSize%uint64(alignment) != 0 { - // This error will let the compilation fail (but continues so that - // other errors can be shown). - c.addError(pos, "internal error: allocated array contains unaligned pointer") - return ptrs + et := t.ElementType() + elementSize := c.targetData.TypeAllocSize(et) + if elementSize%ptrAlign != 0 { + if typeHasPointers(et) { + // This error will let the compilation fail (but continues so that + // other errors can be shown). + c.addError(pos, "internal error: allocated array contains unaligned pointer") + } + return } - for i := 0; i < typ.ArrayLength(); i++ { - ptrs.Lsh(ptrs, uint(elementSize)/uint(alignment)) - ptrs.Or(ptrs, subptrs) + elementSize /= ptrAlign + for i := 0; i < len; i++ { + c.buildPointerBitmap( + dst, + ptrAlign, + pos, + et, + offset+uint64(i)*elementSize, + ) } - return ptrs + default: // Should not happen. panic("unknown LLVM type") diff --git a/compiler/testdata/gc.go b/compiler/testdata/gc.go index 20e5967028..9aa00a4c6f 100644 --- a/compiler/testdata/gc.go +++ b/compiler/testdata/gc.go @@ -24,6 +24,10 @@ var ( x *byte y [61]uintptr } + struct5 *struct { + x *byte + y [30]uintptr + } slice1 []byte slice2 []*int @@ -58,6 +62,10 @@ func newStruct() { x *byte y [61]uintptr }) + struct5 = new(struct { + x *byte + y [30]uintptr + }) } func newFuncValue() *func() { diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll index d2be74cbcf..42a278b66e 100644 --- a/compiler/testdata/gc.ll +++ b/compiler/testdata/gc.ll @@ -16,11 +16,12 @@ target triple = "wasm32-unknown-wasi" @main.struct2 = hidden global ptr null, align 4 @main.struct3 = hidden global ptr null, align 4 @main.struct4 = hidden global ptr null, align 4 +@main.struct5 = hidden global ptr null, align 4 @main.slice1 = hidden global { ptr, i32, i32 } zeroinitializer, align 4 @main.slice2 = hidden global { ptr, i32, i32 } zeroinitializer, align 4 @main.slice3 = hidden global { ptr, i32, i32 } zeroinitializer, align 4 -@"runtime/gc.layout:62-2000000000000001" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00 " } -@"runtime/gc.layout:62-0001" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00\00" } +@"runtime/gc.layout:62-0100000000000020" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00 " } +@"runtime/gc.layout:62-0100000000000000" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00\00" } @"reflect/types.type:basic:complex128" = linkonce_odr constant { i8, ptr } { i8 80, ptr @"reflect/types.type:pointer:basic:complex128" }, align 4 @"reflect/types.type:pointer:basic:complex128" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:basic:complex128" }, align 4 @@ -80,12 +81,15 @@ entry: %new1 = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new1, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new1, ptr @main.struct2, align 4 - %new2 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-2000000000000001", ptr undef) #3 + %new2 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-0100000000000020", ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new2, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new2, ptr @main.struct3, align 4 - %new3 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-0001", ptr undef) #3 + %new3 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-0100000000000000", ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new3, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new3, ptr @main.struct4, align 4 + %new4 = call align 4 dereferenceable(124) ptr @runtime.alloc(i32 124, ptr nonnull inttoptr (i32 127 to ptr), ptr undef) #3 + call void @runtime.trackPointer(ptr nonnull %new4, ptr nonnull %stackalloc, ptr undef) #3 + store ptr %new4, ptr @main.struct5, align 4 ret void } From 707d37a4c1bbc279c7e3859cfddb6435b7bb4282 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 14 Jan 2026 09:55:22 +0100 Subject: [PATCH 05/16] chore: update version to 0.41.0-dev Signed-off-by: deadprogram --- goenv/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goenv/version.go b/goenv/version.go index 423f95906e..9ade0e0b79 100644 --- a/goenv/version.go +++ b/goenv/version.go @@ -10,7 +10,7 @@ import ( // Version of TinyGo. // Update this value before release of new version of software. -const version = "0.40.1" +const version = "0.41.0-dev" // Return TinyGo version, either in the form 0.30.0 or as a development version // (like 0.30.0-dev-abcd012). From a0069b6282164081f044eda45143e98c4371419d Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Sat, 17 Jan 2026 08:59:13 -0800 Subject: [PATCH 06/16] testdata: more corpus entries (#5182) * testdata: more corpus entries * testdata: remove skipwasi for dchest/siphash build issues --- testdata/corpus.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/testdata/corpus.yaml b/testdata/corpus.yaml index 0ed29adbe4..36ac337cf6 100644 --- a/testdata/corpus.yaml +++ b/testdata/corpus.yaml @@ -29,8 +29,7 @@ - repo: github.com/dgryski/go-camellia - repo: github.com/dgryski/go-change - repo: github.com/dgryski/go-chaskey - tags: appengine noasm - skipwasi: true # siphash has build tag issues + tags: appengine noasm # for dchest/siphash - repo: github.com/dgryski/go-clefia - repo: github.com/dgryski/go-clockpro - repo: github.com/dgryski/go-cobs @@ -56,7 +55,6 @@ - repo: github.com/dgryski/go-linlog - repo: github.com/dgryski/go-maglev tags: appengine # for dchest/siphash - skipwasi: true - repo: github.com/dgryski/go-marvin32 - repo: github.com/dgryski/go-md5crypt - repo: github.com/dgryski/go-metro @@ -66,7 +64,6 @@ tags: noasm - repo: github.com/dgryski/go-mpchash tags: appengine # for dchest/siphash - skipwasi: true - repo: github.com/dgryski/go-neeva - repo: github.com/dgryski/go-nibz - repo: github.com/dgryski/go-nibblesort @@ -289,3 +286,8 @@ - repo: github.com/philhofer/fwd - repo: github.com/blevesearch/sear - repo: github.com/steveyen/gtreap +- repo: github.com/orsinium-labs/tinymath +- repo: github.com/orsinium-labs/jsony +- repo: github.com/tidwall/gjson +- repo: github.com/dchest/siphash + tags: appengine From 5d8e071bfb195872263631f66474a7cf7dfb2a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sat, 17 Jan 2026 21:22:15 +0100 Subject: [PATCH 07/16] machine/attiny85: add USI-based SPI support (#5181) * machine/attiny85: add USI-based SPI support Implement SPI communication for ATTiny85 using the USI (Universal Serial Interface) hardware in three-wire mode. The ATTiny85 lacks dedicated SPI hardware but can emulate SPI using the USI module with software clock strobing. Implementation details: - Configure USI in three-wire mode for SPI operation - Use clock strobing technique to shift data in/out - Pin mapping: PB2 (SCK), PB1 (MOSI/DO), PB0 (MISO/DI) - Support both Transfer() and Tx() methods The implementation uses the USI control register (USICR) to toggle the clock pin, which triggers automatic bit shifting in hardware. This is more efficient than pure software bit-banging. Current limitations: - Frequency configuration not yet implemented (runs at max software speed) - Only SPI Mode 0 (CPOL=0, CPHA=0) supported - Only MSB-first bit order supported Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: Ona * machine/attiny85: add SPI frequency configuration support Add software-based frequency control for USI SPI. The ATtiny85 USI lacks hardware prescalers, so frequency is controlled via delay loops between clock toggles. - Calculate delay cycles based on requested frequency and CPU clock - Fast path (no delay) when frequency is 0 or max speed requested - Delay loop uses nop instructions for timing control Co-authored-by: Ona * machine/attiny85: add SPI mode configuration support Add support for all 4 SPI modes (Mode 0-3) using USI hardware: - Mode 0 (CPOL=0, CPHA=0): Clock idle low, sample on rising edge - Mode 1 (CPOL=0, CPHA=1): Clock idle low, sample on falling edge - Mode 2 (CPOL=1, CPHA=0): Clock idle high, sample on falling edge - Mode 3 (CPOL=1, CPHA=1): Clock idle high, sample on rising edge CPOL is controlled by setting the clock pin idle state. CPHA is controlled via the USICS0 bit in USICR. Co-authored-by: Ona * machine/attiny85: add LSB-first bit order support Add software-based LSB-first support for USI SPI. The USI hardware only supports MSB-first, so bit reversal is done in software before sending and after receiving. Uses an efficient parallel bit swap algorithm (3 operations) to reverse the byte. Co-authored-by: Ona * GNUmakefile: add mcp3008 SPI example to digispark smoketest Test the USI-based SPI implementation for ATtiny85/digispark. Co-authored-by: Ona * machine/attiny85: minimize SPI RAM footprint Reduce SPI struct from ~14 bytes to 1 byte to fit in ATtiny85's limited 512 bytes of RAM. Changes: - Remove register pointers (use avr.USIDR/USISR/USICR directly) - Remove pin fields (USI pins are fixed: PB0/PB1/PB2) - Remove CS pin management (user must handle CS) - Remove frequency control (runs at max speed) - Remove LSBFirst support The SPI struct now only stores the USICR configuration byte. Co-authored-by: Ona * Revert "machine/attiny85: minimize SPI RAM footprint" This reverts commit 387ccad494a5f0419c1e274d228eb100284f28fd. Co-authored-by: Ona * machine/attiny85: reduce SPI RAM usage by 10 bytes Remove unnecessary fields from SPI struct while keeping all functionality: - Remove register pointers (use avr.USIDR/USISR/USICR directly) - Remove pin fields (USI pins are fixed: PB0/PB1/PB2) - Remove CS pin (user must manage it, standard practice) Kept functional fields: - delayCycles for frequency control - usicrValue for SPI mode support - lsbFirst for bit order support SPI struct reduced from 14 bytes to 4 bytes. Co-authored-by: Ona --------- Co-authored-by: Ona --- GNUmakefile | 2 + src/machine/machine_attiny85.go | 167 ++++++++++++++++++++++++++++++++ src/machine/spi.go | 2 +- src/machine/spi_tx.go | 2 +- 4 files changed, 171 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 322c0b7cff..fc8d71d998 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -898,6 +898,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark examples/pwm @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=digispark examples/mcp3008 + @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark -gc=leaking examples/blinky1 @$(MD5SUM) test.hex ifneq ($(XTENSA), 0) diff --git a/src/machine/machine_attiny85.go b/src/machine/machine_attiny85.go index 27adaf948c..6d31846b5a 100644 --- a/src/machine/machine_attiny85.go +++ b/src/machine/machine_attiny85.go @@ -375,3 +375,170 @@ func (pwm PWM) Set(channel uint8, value uint32) { } } } + +// SPIConfig is used to store config info for SPI. +type SPIConfig struct { + Frequency uint32 + LSBFirst bool + Mode uint8 +} + +// SPI is the USI-based SPI implementation for ATTiny85. +// The ATTiny85 doesn't have dedicated SPI hardware, but uses the USI +// (Universal Serial Interface) in three-wire mode. +// +// Fixed pin mapping (directly controlled by USI hardware): +// - PB2: SCK (clock) +// - PB1: DO/MOSI (data out) +// - PB0: DI/MISO (data in) +// +// Note: CS pin must be managed by the user. +type SPI struct { + // Delay cycles for frequency control (0 = max speed) + delayCycles uint16 + + // USICR value configured for the selected SPI mode + usicrValue uint8 + + // LSB-first mode (requires software bit reversal) + lsbFirst bool +} + +// SPI0 is the USI-based SPI interface on the ATTiny85 +var SPI0 = SPI{} + +// Configure sets up the USI for SPI communication. +// Note: The user must configure and control the CS pin separately. +func (s *SPI) Configure(config SPIConfig) error { + // Configure USI pins (fixed by hardware) + // PB1 (DO/MOSI) -> OUTPUT + // PB2 (USCK/SCK) -> OUTPUT + // PB0 (DI/MISO) -> INPUT + PB1.Configure(PinConfig{Mode: PinOutput}) + PB2.Configure(PinConfig{Mode: PinOutput}) + PB0.Configure(PinConfig{Mode: PinInput}) + + // Reset USI registers + avr.USIDR.Set(0) + avr.USISR.Set(0) + + // Configure USI for SPI mode: + // - USIWM0: Three-wire mode (SPI) + // - USICS1: External clock source (software controlled via USITC) + // - USICLK: Clock strobe - enables counter increment on USITC toggle + // - USICS0: Controls clock phase (CPHA) + // + // SPI Modes: + // Mode 0 (CPOL=0, CPHA=0): Clock idle low, sample on rising edge + // Mode 1 (CPOL=0, CPHA=1): Clock idle low, sample on falling edge + // Mode 2 (CPOL=1, CPHA=0): Clock idle high, sample on falling edge + // Mode 3 (CPOL=1, CPHA=1): Clock idle high, sample on rising edge + // + // For USI, USICS0 controls the sampling edge when USICS1=1: + // USICS0=0: Positive edge (rising) + // USICS0=1: Negative edge (falling) + switch config.Mode { + case Mode0: // CPOL=0, CPHA=0: idle low, sample rising + PB2.Low() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK + case Mode1: // CPOL=0, CPHA=1: idle low, sample falling + PB2.Low() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK + case Mode2: // CPOL=1, CPHA=0: idle high, sample falling + PB2.High() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK + case Mode3: // CPOL=1, CPHA=1: idle high, sample rising + PB2.High() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK + default: // Default to Mode 0 + PB2.Low() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK + } + avr.USICR.Set(s.usicrValue) + + // Calculate delay cycles for frequency control + // Each bit transfer requires 2 clock toggles (rising + falling edge) + // The loop overhead is approximately 10-15 cycles per toggle on AVR + // We calculate additional delay cycles needed to achieve the target frequency + if config.Frequency > 0 && config.Frequency < CPUFrequency()/2 { + // Cycles per half-period = CPUFrequency / (2 * Frequency) + // Subtract loop overhead (~15 cycles) to get delay cycles + cyclesPerHalfPeriod := CPUFrequency() / (2 * config.Frequency) + const loopOverhead = 15 + if cyclesPerHalfPeriod > loopOverhead { + s.delayCycles = uint16(cyclesPerHalfPeriod - loopOverhead) + } else { + s.delayCycles = 0 + } + } else { + // Max speed - no delay + s.delayCycles = 0 + } + + // Store LSBFirst setting for use in Transfer + s.lsbFirst = config.LSBFirst + + return nil +} + +// reverseByte reverses the bit order of a byte (MSB <-> LSB) +// Used for LSB-first SPI mode since USI hardware only supports MSB-first +func reverseByte(b byte) byte { + b = (b&0xF0)>>4 | (b&0x0F)<<4 + b = (b&0xCC)>>2 | (b&0x33)<<2 + b = (b&0xAA)>>1 | (b&0x55)<<1 + return b +} + +// Transfer performs a single byte SPI transfer (send and receive simultaneously) +// This implements the USI-based SPI transfer using the "clock strobing" technique +func (s *SPI) Transfer(b byte) (byte, error) { + // For LSB-first mode, reverse the bits before sending + // USI hardware only supports MSB-first, so we do it in software + if s.lsbFirst { + b = reverseByte(b) + } + + // Load the byte to transmit into the USI Data Register + avr.USIDR.Set(b) + + // Clear the counter overflow flag by writing 1 to it (AVR quirk) + // This also resets the 4-bit counter to 0 + avr.USISR.Set(avr.USISR_USIOIF) + + // Clock the data out/in + // We need 16 clock toggles (8 bits × 2 edges per bit) + // The USI counter counts each clock edge, so it overflows at 16 + // After 16 toggles, the clock returns to its idle state (set by CPOL in Configure) + // + // IMPORTANT: Only toggle USITC here! + // - USITC toggles the clock pin + // - The USICR mode bits (USIWM0, USICS1, USICS0, USICLK) were set in Configure() + // - SetBits preserves those bits and only sets USITC + if s.delayCycles == 0 { + // Fast path: no delay, run at maximum speed + for !avr.USISR.HasBits(avr.USISR_USIOIF) { + avr.USICR.SetBits(avr.USICR_USITC) + } + } else { + // Frequency-controlled path: add delay between clock toggles + for !avr.USISR.HasBits(avr.USISR_USIOIF) { + avr.USICR.SetBits(avr.USICR_USITC) + // Delay loop for frequency control + // Each iteration is approximately 3 cycles on AVR (dec, brne) + for i := s.delayCycles; i > 0; i-- { + avr.Asm("nop") + } + } + } + + // Get the received byte + result := avr.USIDR.Get() + + // For LSB-first mode, reverse the received bits + if s.lsbFirst { + result = reverseByte(result) + } + + return result, nil +} diff --git a/src/machine/spi.go b/src/machine/spi.go index 9a1033ca7d..fa507b961d 100644 --- a/src/machine/spi.go +++ b/src/machine/spi.go @@ -1,4 +1,4 @@ -//go:build !baremetal || atmega || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2) +//go:build !baremetal || atmega || attiny85 || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2) package machine diff --git a/src/machine/spi_tx.go b/src/machine/spi_tx.go index 97385bb596..aec3f52fe1 100644 --- a/src/machine/spi_tx.go +++ b/src/machine/spi_tx.go @@ -1,4 +1,4 @@ -//go:build atmega || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2) +//go:build atmega || attiny85 || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2) // This file implements the SPI Tx function for targets that don't have a custom // (faster) implementation for it. From ee33b25ff0f11571cd58792b31b9d9be94ffdb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 19 Jan 2026 15:04:27 +0000 Subject: [PATCH 08/16] machine/esp32: add PWM support using LEDC peripheral Add PWM support for ESP32 using the LEDC (LED Control) peripheral. Features: - 4 high-speed timers (PWM0-PWM3) with glitch-free duty updates - Up to 8 channels shared across timers - Configurable duty resolution (1-20 bits) - Flexible frequency control via period setting - Any GPIO can be used via GPIO matrix routing The implementation follows the standard TinyGo PWM interface with Configure, Channel, Set, SetPeriod, Top, and SetInverting methods. Co-authored-by: Ona --- src/examples/pwm/esp32.go | 12 + src/machine/machine_esp32_pwm.go | 415 +++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 src/examples/pwm/esp32.go create mode 100644 src/machine/machine_esp32_pwm.go diff --git a/src/examples/pwm/esp32.go b/src/examples/pwm/esp32.go new file mode 100644 index 0000000000..3b96a4de2d --- /dev/null +++ b/src/examples/pwm/esp32.go @@ -0,0 +1,12 @@ +//go:build esp32 +// +build esp32 + +package main + +import "machine" + +var ( + pwm = machine.PWM0 // Use high-speed timer 0 + pinA = machine.GPIO2 // Built-in LED on many ESP32 boards + pinB = machine.GPIO4 // Another GPIO for testing +) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go new file mode 100644 index 0000000000..bdbe08562d --- /dev/null +++ b/src/machine/machine_esp32_pwm.go @@ -0,0 +1,415 @@ +//go:build esp32 +// +build esp32 + +package machine + +import ( + "device/esp" + "runtime/volatile" + "unsafe" +) + +// PWM is one PWM peripheral, which consists of a timer and the associated +// channels. There are 4 high-speed timers available (PWM0-PWM3), each can +// drive up to 8 channels total (shared across all timers). +// +// The ESP32 LEDC peripheral is used for PWM generation. It provides flexible +// frequency and duty cycle control with configurable resolution (1-20 bits). +type PWM struct { + num uint8 // Timer number (0-3 for high-speed) +} + +// pwmChannelPins tracks which pin is assigned to each channel (0-7) +// Initialized to NoPin (0xff) to indicate unused channels +var pwmChannelPins [8]Pin + +// Hardware PWM peripherals available on ESP32. +// These use the high-speed LEDC timers for glitch-free PWM updates. +var ( + PWM0 = &PWM{num: 0} + PWM1 = &PWM{num: 1} + PWM2 = &PWM{num: 2} + PWM3 = &PWM{num: 3} +) + +// LEDC peripheral constants +const ( + // Clock enable bit in DPORT.PERIP_CLK_EN for LEDC + ledcClockEnable = 1 << 11 + + // GPIO matrix output signal numbers for LEDC high-speed channels + ledcHSSignalOut0 = 71 + ledcHSSignalOut1 = 72 + ledcHSSignalOut2 = 73 + ledcHSSignalOut3 = 74 + ledcHSSignalOut4 = 75 + ledcHSSignalOut5 = 76 + ledcHSSignalOut6 = 77 + ledcHSSignalOut7 = 78 + + // APB clock frequency (used by high-speed LEDC) + apbClockFreq = 80000000 // 80 MHz + + // Maximum values + maxDivider = 0x3FFFF // 18-bit divider + maxResolution = 20 // Maximum duty resolution in bits + minResolution = 1 // Minimum duty resolution in bits +) + +// LEDC register bit positions and masks for timer configuration +const ( + // HSTIMER_CONF register + timerDivNumPos = 5 // DIV_NUM position (bits 5-22) + timerDivNumMask = 0x3FFFF << timerDivNumPos + timerLimPos = 0 // LIM position (bits 0-4), resolution = LIM + 1 + timerLimMask = 0x1F + timerPausePos = 23 // PAUSE bit + timerPauseMask = 1 << timerPausePos + timerRstPos = 24 // RST bit + timerRstMask = 1 << timerRstPos + timerTickSelPos = 25 // TICK_SEL bit (0=REF_TICK, 1=APB_CLK) + timerTickSelMask = 1 << timerTickSelPos +) + +// LEDC register bit positions and masks for channel configuration +const ( + // HSCH_CONF0 register + chanTimerSelPos = 0 // TIMER_SEL position (bits 0-1) + chanTimerSelMask = 0x3 + chanSigOutEnPos = 2 // SIG_OUT_EN bit + chanSigOutEnMask = 1 << chanSigOutEnPos + chanIdleLvPos = 3 // IDLE_LV bit + chanIdleLvMask = 1 << chanIdleLvPos + chanClkEnPos = 31 // CLK_EN bit + chanClkEnMask = 1 << chanClkEnPos + + // HSCH_CONF1 register + chanDutyStartPos = 31 // DUTY_START bit + chanDutyStartMask = 1 << chanDutyStartPos + + // HSCH_DUTY register - duty value uses bits 0-24 (25 bits total) + // The lower 4 bits are fractional, upper 20 bits are integer + chanDutyFracBits = 4 +) + +// pwmState holds the current configuration for each PWM timer +type pwmState struct { + resolution uint8 // Current duty resolution in bits + configured bool // Whether the timer has been configured +} + +var pwmStates [4]pwmState + +// Configure enables and configures this PWM peripheral. +// The period is specified in nanoseconds. A period of 0 will select a default +// period suitable for LED dimming (~1kHz). +func (pwm *PWM) Configure(config PWMConfig) error { + // Enable LEDC peripheral clock + esp.DPORT.PERIP_CLK_EN.SetBits(ledcClockEnable) + // Clear reset bit + esp.DPORT.PERIP_RST_EN.ClearBits(ledcClockEnable) + + // Calculate timer configuration + divider, resolution, err := pwm.calculateConfig(config.Period) + if err != nil { + return err + } + + // Store the resolution for duty cycle calculations + pwmStates[pwm.num].resolution = resolution + pwmStates[pwm.num].configured = true + + // Get timer configuration register + timerConf := pwm.timerConf() + + // Reset the timer first + timerConf.SetBits(timerRstMask) + + // Configure timer: + // - Use APB_CLK (80MHz) as clock source + // - Set divider + // - Set resolution (LIM = resolution - 1) + var conf uint32 + conf |= timerTickSelMask // APB_CLK + conf |= (divider << timerDivNumPos) & timerDivNumMask // Divider + conf |= uint32(resolution-1) & timerLimMask // Resolution + timerConf.Set(conf) + + return nil +} + +// calculateConfig determines the optimal divider and resolution for a given period. +// Returns divider, resolution, and any error. +func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { + if period == 0 { + // Default: ~1kHz with 13-bit resolution (good for LEDs) + // period = 1e9 / 1000 = 1,000,000 ns + period = 1000000 + } + + // Formula: period_ns = (2^resolution * divider) / 80MHz * 1e9 + // period_ns = (2^resolution * divider) * 12.5 + // divider = period_ns / (2^resolution * 12.5) + // divider = period_ns * 80 / (2^resolution * 1000) + + // Try to find the highest resolution that gives a valid divider + for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { + // Calculate divider for this resolution + // divider = period * 80MHz / (2^resolution * 1e9) + // To avoid overflow: divider = period * 80 / (2^resolution * 1000) + resolutionValue := uint64(1) << resolution + divider := (period * 80) / (resolutionValue * 1000) + + if divider == 0 { + // Period too short for this resolution, try lower resolution + continue + } + + if divider <= maxDivider { + return uint32(divider), resolution, nil + } + } + + return 0, 0, ErrPWMPeriodTooLong +} + +// Channel returns a PWM channel for the given pin. If the pin is already +// configured for this PWM peripheral, the same channel is returned. +// The pin is configured for PWM output. +func (pwm *PWM) Channel(pin Pin) (uint8, error) { + if !pwmStates[pwm.num].configured { + // Timer not configured, configure with default period + if err := pwm.Configure(PWMConfig{}); err != nil { + return 0, err + } + } + + // Check if this pin is already assigned to a channel + for ch := uint8(0); ch < 8; ch++ { + if pwmChannelPins[ch] == pin { + return ch, nil + } + } + + // Find an available channel + for ch := uint8(0); ch < 8; ch++ { + if pwmChannelPins[ch] == NoPin || pwmChannelPins[ch] == 0 { + // Found an available channel + pwmChannelPins[ch] = pin + + // Configure the GPIO for PWM output through GPIO matrix + signal := uint32(ledcHSSignalOut0 + ch) + pin.configure(PinConfig{Mode: PinOutput}, signal) + + // Configure the channel + chanConf0 := pwm.channelConf0(ch) + + // Set channel configuration: + // - Enable clock + // - Select this timer + // - Enable signal output + // - Idle level low + var conf uint32 + conf |= chanClkEnMask // Enable clock + conf |= uint32(pwm.num) & chanTimerSelMask // Select timer + conf |= chanSigOutEnMask // Enable output + chanConf0.Set(conf) + + // Initialize duty to 0 + pwm.channelDuty(ch).Set(0) + + // Set HPOINT to 0 (start of cycle) + pwm.channelHpoint(ch).Set(0) + + // Trigger duty update + pwm.channelConf1(ch).SetBits(chanDutyStartMask) + + return ch, nil + } + } + + return 0, ErrInvalidOutputPin +} + +// Set updates the channel value. This is used to control the channel duty +// cycle. For example, to set it to a 25% duty cycle, use: +// +// pwm.Set(channel, pwm.Top() / 4) +// +// pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, +// pwm.Top()) will set the output to high, assuming the output isn't inverted. +func (pwm *PWM) Set(channel uint8, value uint32) { + if channel >= 8 { + return + } + + resolution := pwmStates[pwm.num].resolution + maxValue := uint32((1 << resolution) - 1) + + // Clamp value to valid range + // Note: Setting duty to exactly 2^resolution causes hardware overflow + if value > maxValue { + value = maxValue + } + + // The duty register uses 4 fractional bits, so shift left by 4 + dutyValue := value << chanDutyFracBits + + // Set the duty value + pwm.channelDuty(channel).Set(dutyValue) + + // Trigger duty update by setting DUTY_START bit + pwm.channelConf1(channel).SetBits(chanDutyStartMask) +} + +// SetPeriod updates the period of this PWM peripheral in nanoseconds. +// To set a particular frequency, use the following formula: +// +// period = 1e9 / frequency +// +// SetPeriod will try to maintain the current duty cycle ratio when changing +// the period. +func (pwm *PWM) SetPeriod(period uint64) error { + // Calculate new configuration + divider, resolution, err := pwm.calculateConfig(period) + if err != nil { + return err + } + + oldResolution := pwmStates[pwm.num].resolution + pwmStates[pwm.num].resolution = resolution + + // Update timer configuration + timerConf := pwm.timerConf() + + // Read current config, update divider and resolution + conf := timerConf.Get() + conf &^= timerDivNumMask | timerLimMask + conf |= (divider << timerDivNumPos) & timerDivNumMask + conf |= uint32(resolution-1) & timerLimMask + timerConf.Set(conf) + + // If resolution changed, we may need to scale duty values + if resolution != oldResolution { + // Scale all active channel duty values + for ch := uint8(0); ch < 8; ch++ { + if pwmChannelPins[ch] != NoPin && pwmChannelPins[ch] != 0 { + // Read current duty (includes fractional bits) + currentDuty := pwm.channelDuty(ch).Get() >> chanDutyFracBits + + // Scale to new resolution + if oldResolution > resolution { + currentDuty >>= (oldResolution - resolution) + } else { + currentDuty <<= (resolution - oldResolution) + } + + // Apply new duty + pwm.Set(ch, currentDuty) + } + } + } + + return nil +} + +// Top returns the current counter top, for use in duty cycle calculation. +// The value returned is (2^resolution - 1), which is the maximum value +// that can be passed to Set(). +func (pwm *PWM) Top() uint32 { + resolution := pwmStates[pwm.num].resolution + if resolution == 0 { + resolution = 13 // Default resolution + } + return (1 << resolution) - 1 +} + +// Counter returns the current counter value of the timer. +// This may be useful for debugging. +func (pwm *PWM) Counter() uint32 { + return pwm.timerValue().Get() +} + +// Period returns the current period in nanoseconds. +func (pwm *PWM) Period() uint64 { + conf := pwm.timerConf().Get() + divider := (conf & timerDivNumMask) >> timerDivNumPos + resolution := (conf & timerLimMask) + 1 + + // period_ns = (2^resolution * divider) * 12.5 + // period_ns = (2^resolution * divider) * 1000 / 80 + resolutionValue := uint64(1) << resolution + return resolutionValue * uint64(divider) * 1000 / 80 +} + +// SetInverting sets whether to invert the output of this channel. +// Without inverting, a 25% duty cycle would mean the output is high for 25% of +// the time and low for the rest. Inverting flips the output as if a NOT gate +// was placed at the output, meaning that the output would be 25% low and 75% +// high with a duty cycle of 25%. +func (pwm *PWM) SetInverting(channel uint8, inverting bool) { + if channel >= 8 { + return + } + + chanConf0 := pwm.channelConf0(channel) + if inverting { + chanConf0.SetBits(chanIdleLvMask) + } else { + chanConf0.ClearBits(chanIdleLvMask) + } +} + +// Enable enables or disables the PWM output for all channels on this timer. +func (pwm *PWM) Enable(enable bool) { + timerConf := pwm.timerConf() + if enable { + timerConf.ClearBits(timerPauseMask) + } else { + timerConf.SetBits(timerPauseMask) + } +} + +// Register access helpers + +// timerConf returns the configuration register for this timer. +func (pwm *PWM) timerConf() *volatile.Register32 { + // HSTIMER0_CONF is at offset 0x140, each timer is 0x8 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_CONF)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) +} + +// timerValue returns the value register for this timer. +func (pwm *PWM) timerValue() *volatile.Register32 { + // HSTIMER0_VALUE is at offset 0x144, each timer is 0x8 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_VALUE)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) +} + +// channelConf0 returns the CONF0 register for the given channel. +func (pwm *PWM) channelConf0(ch uint8) *volatile.Register32 { + // HSCH0_CONF0 is at offset 0x0, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF0)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} + +// channelHpoint returns the HPOINT register for the given channel. +func (pwm *PWM) channelHpoint(ch uint8) *volatile.Register32 { + // HSCH0_HPOINT is at offset 0x4, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_HPOINT)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} + +// channelDuty returns the DUTY register for the given channel. +func (pwm *PWM) channelDuty(ch uint8) *volatile.Register32 { + // HSCH0_DUTY is at offset 0x8, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} + +// channelConf1 returns the CONF1 register for the given channel. +func (pwm *PWM) channelConf1(ch uint8) *volatile.Register32 { + // HSCH0_CONF1 is at offset 0xC, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF1)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} From a58f5559dd7751d0107b05ec973ee566a756c789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:38:29 +0000 Subject: [PATCH 09/16] machine/esp32: fix PWM duty cycle not working Fix several issues with the LEDC PWM implementation: 1. Timer reset: The reset bit must be set then cleared (not just set) 2. CONF1 register: For non-fading operation, duty_cycle and duty_num must be set to 1, and duty_inc must be enabled 3. Duty update: Properly configure CONF1 when updating duty values These fixes align with the ESP-IDF LEDC driver implementation. Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index bdbe08562d..bfc4ccf7e8 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -84,6 +84,14 @@ const ( chanClkEnMask = 1 << chanClkEnPos // HSCH_CONF1 register + chanDutyScalePos = 0 // DUTY_SCALE position (bits 0-9) + chanDutyScaleMask = 0x3FF + chanDutyCyclePos = 10 // DUTY_CYCLE position (bits 10-19) + chanDutyCycleMask = 0x3FF << chanDutyCyclePos + chanDutyNumPos = 20 // DUTY_NUM position (bits 20-29) + chanDutyNumMask = 0x3FF << chanDutyNumPos + chanDutyIncPos = 30 // DUTY_INC bit + chanDutyIncMask = 1 << chanDutyIncPos chanDutyStartPos = 31 // DUTY_START bit chanDutyStartMask = 1 << chanDutyStartPos @@ -122,9 +130,6 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Get timer configuration register timerConf := pwm.timerConf() - // Reset the timer first - timerConf.SetBits(timerRstMask) - // Configure timer: // - Use APB_CLK (80MHz) as clock source // - Set divider @@ -133,6 +138,9 @@ func (pwm *PWM) Configure(config PWMConfig) error { conf |= timerTickSelMask // APB_CLK conf |= (divider << timerDivNumPos) & timerDivNumMask // Divider conf |= uint32(resolution-1) & timerLimMask // Resolution + + // Reset the timer (set rst=1, then rst=0) + timerConf.Set(conf | timerRstMask) timerConf.Set(conf) return nil @@ -221,8 +229,14 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { // Set HPOINT to 0 (start of cycle) pwm.channelHpoint(ch).Set(0) - // Trigger duty update - pwm.channelConf1(ch).SetBits(chanDutyStartMask) + // Configure CONF1 for non-fading operation and trigger duty update + // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + var conf1 uint32 + conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 + conf1 |= 1 << chanDutyNumPos // duty_num = 1 + conf1 |= chanDutyIncMask // duty_inc = 1 + conf1 |= chanDutyStartMask // duty_start = 1 + pwm.channelConf1(ch).Set(conf1) return ch, nil } @@ -258,8 +272,14 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Set the duty value pwm.channelDuty(channel).Set(dutyValue) - // Trigger duty update by setting DUTY_START bit - pwm.channelConf1(channel).SetBits(chanDutyStartMask) + // Configure CONF1 for non-fading operation and trigger duty update + // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + var conf1 uint32 + conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 + conf1 |= 1 << chanDutyNumPos // duty_num = 1 + conf1 |= chanDutyIncMask // duty_inc = 1 + conf1 |= chanDutyStartMask // duty_start = 1 + pwm.channelConf1(channel).Set(conf1) } // SetPeriod updates the period of this PWM peripheral in nanoseconds. From 0cb1af7967e46f860774efb87645a999465b4801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:43:47 +0000 Subject: [PATCH 10/16] machine/esp32: fix PWM clock divider calculation The LEDC clock divider register uses 8 fractional bits, meaning the register value is actual_divider * 256. The previous implementation was setting the divider directly without accounting for this, resulting in a divider that was 256x smaller than intended. Also added: - Set LEDC.CONF.APB_CLK_SEL = 1 to select APB clock source - Updated Period() calculation to account for fractional bits Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 49 ++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index bfc4ccf7e8..f671f805b6 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -50,10 +50,15 @@ const ( // APB clock frequency (used by high-speed LEDC) apbClockFreq = 80000000 // 80 MHz + // Clock divider has 8 fractional bits + // So divider register value = actual_divider * 256 + dividerFractionalBits = 8 + // Maximum values - maxDivider = 0x3FFFF // 18-bit divider - maxResolution = 20 // Maximum duty resolution in bits - minResolution = 1 // Minimum duty resolution in bits + maxDivider = 0x3FFFF // 18-bit divider register value (actual max divider ~1024) + minDivider = 1 << dividerFractionalBits // Minimum divider = 256 (represents 1.0) + maxResolution = 20 // Maximum duty resolution in bits + minResolution = 1 // Minimum duty resolution in bits ) // LEDC register bit positions and masks for timer configuration @@ -117,6 +122,9 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Clear reset bit esp.DPORT.PERIP_RST_EN.ClearBits(ledcClockEnable) + // Select APB clock (80MHz) for LEDC + esp.LEDC.CONF.Set(1) // APB_CLK_SEL = 1 + // Calculate timer configuration divider, resolution, err := pwm.calculateConfig(config.Period) if err != nil { @@ -147,7 +155,7 @@ func (pwm *PWM) Configure(config PWMConfig) error { } // calculateConfig determines the optimal divider and resolution for a given period. -// Returns divider, resolution, and any error. +// Returns divider (with 8 fractional bits), resolution, and any error. func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { if period == 0 { // Default: ~1kHz with 13-bit resolution (good for LEDs) @@ -156,25 +164,30 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { } // Formula: period_ns = (2^resolution * divider) / 80MHz * 1e9 - // period_ns = (2^resolution * divider) * 12.5 - // divider = period_ns / (2^resolution * 12.5) - // divider = period_ns * 80 / (2^resolution * 1000) + // Where divider is the actual divider (not the register value) + // Register value = actual_divider * 256 (8 fractional bits) + // + // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 + // divider_reg = period_ns * 80MHz * 256 / (2^resolution * 1e9) + // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) + // divider_reg = period_ns * 20480 / (2^resolution * 1000) + // divider_reg = period_ns * 256 * 80 / (2^resolution * 1000) // Try to find the highest resolution that gives a valid divider for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { - // Calculate divider for this resolution - // divider = period * 80MHz / (2^resolution * 1e9) - // To avoid overflow: divider = period * 80 / (2^resolution * 1000) resolutionValue := uint64(1) << resolution - divider := (period * 80) / (resolutionValue * 1000) - if divider == 0 { + // Calculate divider register value (includes 8 fractional bits) + // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) + dividerReg := (period * 80 * 256) / (resolutionValue * 1000) + + if dividerReg < minDivider { // Period too short for this resolution, try lower resolution continue } - if divider <= maxDivider { - return uint32(divider), resolution, nil + if dividerReg <= maxDivider { + return uint32(dividerReg), resolution, nil } } @@ -353,13 +366,13 @@ func (pwm *PWM) Counter() uint32 { // Period returns the current period in nanoseconds. func (pwm *PWM) Period() uint64 { conf := pwm.timerConf().Get() - divider := (conf & timerDivNumMask) >> timerDivNumPos + dividerReg := (conf & timerDivNumMask) >> timerDivNumPos resolution := (conf & timerLimMask) + 1 - // period_ns = (2^resolution * divider) * 12.5 - // period_ns = (2^resolution * divider) * 1000 / 80 + // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 + // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) resolutionValue := uint64(1) << resolution - return resolutionValue * uint64(divider) * 1000 / 80 + return resolutionValue * uint64(dividerReg) * 1000 / (80 * 256) } // SetInverting sets whether to invert the output of this channel. From 69fba70f59bf315bff85e362ff2146aae91e7a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:48:17 +0000 Subject: [PATCH 11/16] machine/esp32: improve PWM duty update reliability - Always write full CONF1 register value when updating duty - Re-enable sig_out_en on each duty update (matching ESP-IDF behavior) This should improve the smoothness of duty cycle transitions. Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index f671f805b6..f48dd12992 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -285,14 +285,19 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Set the duty value pwm.channelDuty(channel).Set(dutyValue) - // Configure CONF1 for non-fading operation and trigger duty update - // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + // Configure CONF1 and trigger duty update + // For non-fading operation: duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1 + // Then set duty_start=1 to apply the new duty value + // We write the full register value each time to ensure consistent state var conf1 uint32 conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 conf1 |= 1 << chanDutyNumPos // duty_num = 1 conf1 |= chanDutyIncMask // duty_inc = 1 conf1 |= chanDutyStartMask // duty_start = 1 pwm.channelConf1(channel).Set(conf1) + + // Ensure signal output is enabled (as ESP-IDF does in _ledc_update_duty) + pwm.channelConf0(channel).SetBits(chanSigOutEnMask) } // SetPeriod updates the period of this PWM peripheral in nanoseconds. From 4f31a6a27e44bbd0a590d9d44074b71f96e61ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 15:54:32 +0000 Subject: [PATCH 12/16] machine/esp32: fix PWM resolution register value The duty_resolution register stores the bit width directly, not bit width - 1. Setting resolution-1 caused the timer counter to wrap at half the expected value, resulting in duty cycle glitches when the duty value exceeded the actual counter range. For example, with 20-bit resolution intended: - Before: register = 19, counter wraps at 524287 - After: register = 20, counter wraps at 1048575 This fixes the 'reset in the middle' behavior during fades. Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index f48dd12992..301c80ad76 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -66,7 +66,7 @@ const ( // HSTIMER_CONF register timerDivNumPos = 5 // DIV_NUM position (bits 5-22) timerDivNumMask = 0x3FFFF << timerDivNumPos - timerLimPos = 0 // LIM position (bits 0-4), resolution = LIM + 1 + timerLimPos = 0 // duty_resolution position (bits 0-4), stores bit width directly timerLimMask = 0x1F timerPausePos = 23 // PAUSE bit timerPauseMask = 1 << timerPausePos @@ -141,11 +141,11 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Configure timer: // - Use APB_CLK (80MHz) as clock source // - Set divider - // - Set resolution (LIM = resolution - 1) + // - Set resolution (duty_resolution field stores the bit width directly) var conf uint32 conf |= timerTickSelMask // APB_CLK conf |= (divider << timerDivNumPos) & timerDivNumMask // Divider - conf |= uint32(resolution-1) & timerLimMask // Resolution + conf |= uint32(resolution) & timerLimMask // Resolution (bit width) // Reset the timer (set rst=1, then rst=0) timerConf.Set(conf | timerRstMask) @@ -372,7 +372,7 @@ func (pwm *PWM) Counter() uint32 { func (pwm *PWM) Period() uint64 { conf := pwm.timerConf().Get() dividerReg := (conf & timerDivNumMask) >> timerDivNumPos - resolution := (conf & timerLimMask) + 1 + resolution := conf & timerLimMask // duty_resolution stores bit width directly // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) From 952dc77f327ac826130bef0086f1aaee3082854f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:02:42 +0000 Subject: [PATCH 13/16] machine/esp32: improve PWM implementation Improvements: - Fix SetPeriod using resolution-1 instead of resolution - Refactor channel tracking to properly associate channels with timers - Add pwmChannelInfo struct to track pin, timer, and usage state - Prevent same pin from being used on different timers - Add ReleaseChannel() method to free channels - Add Get() method to read current duty value - Fix SetInverting to use GPIO matrix inversion (bit 9) - SetPeriod now only scales channels bound to this timer - Remove unused constants, add named constants for defaults - Add bounds checking in Period() for zero values Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 175 ++++++++++++++++++++----------- 1 file changed, 112 insertions(+), 63 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 301c80ad76..63b856c522 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -11,7 +11,7 @@ import ( // PWM is one PWM peripheral, which consists of a timer and the associated // channels. There are 4 high-speed timers available (PWM0-PWM3), each can -// drive up to 8 channels total (shared across all timers). +// use any of the 8 high-speed channels. // // The ESP32 LEDC peripheral is used for PWM generation. It provides flexible // frequency and duty cycle control with configurable resolution (1-20 bits). @@ -19,9 +19,15 @@ type PWM struct { num uint8 // Timer number (0-3 for high-speed) } -// pwmChannelPins tracks which pin is assigned to each channel (0-7) -// Initialized to NoPin (0xff) to indicate unused channels -var pwmChannelPins [8]Pin +// pwmChannelInfo tracks the state of each LEDC channel +type pwmChannelInfo struct { + pin Pin // The pin assigned to this channel (NoPin if unused) + timer uint8 // The timer this channel is bound to + inUse bool // Whether this channel is currently in use +} + +// pwmChannels tracks which pin and timer is assigned to each channel (0-7) +var pwmChannels [8]pwmChannelInfo // Hardware PWM peripherals available on ESP32. // These use the high-speed LEDC timers for glitch-free PWM updates. @@ -38,17 +44,7 @@ const ( ledcClockEnable = 1 << 11 // GPIO matrix output signal numbers for LEDC high-speed channels - ledcHSSignalOut0 = 71 - ledcHSSignalOut1 = 72 - ledcHSSignalOut2 = 73 - ledcHSSignalOut3 = 74 - ledcHSSignalOut4 = 75 - ledcHSSignalOut5 = 76 - ledcHSSignalOut6 = 77 - ledcHSSignalOut7 = 78 - - // APB clock frequency (used by high-speed LEDC) - apbClockFreq = 80000000 // 80 MHz + ledcHSSignalBase = 71 // LEDC_HS_SIG_OUT0, channels are consecutive (71-78) // Clock divider has 8 fractional bits // So divider register value = actual_divider * 256 @@ -59,6 +55,10 @@ const ( minDivider = 1 << dividerFractionalBits // Minimum divider = 256 (represents 1.0) maxResolution = 20 // Maximum duty resolution in bits minResolution = 1 // Minimum duty resolution in bits + + // Default values + defaultPeriodNs = 1_000_000 // 1ms = 1kHz, good for LEDs + defaultResolution = 13 // 13-bit resolution for default period ) // LEDC register bit positions and masks for timer configuration @@ -66,39 +66,25 @@ const ( // HSTIMER_CONF register timerDivNumPos = 5 // DIV_NUM position (bits 5-22) timerDivNumMask = 0x3FFFF << timerDivNumPos - timerLimPos = 0 // duty_resolution position (bits 0-4), stores bit width directly - timerLimMask = 0x1F - timerPausePos = 23 // PAUSE bit - timerPauseMask = 1 << timerPausePos - timerRstPos = 24 // RST bit - timerRstMask = 1 << timerRstPos - timerTickSelPos = 25 // TICK_SEL bit (0=REF_TICK, 1=APB_CLK) - timerTickSelMask = 1 << timerTickSelPos + timerLimMask = 0x1F // duty_resolution (bits 0-4), stores bit width directly + timerPauseMask = 1 << 23 + timerRstMask = 1 << 24 + timerTickSelMask = 1 << 25 // TICK_SEL bit (0=REF_TICK, 1=APB_CLK) ) // LEDC register bit positions and masks for channel configuration const ( // HSCH_CONF0 register - chanTimerSelPos = 0 // TIMER_SEL position (bits 0-1) - chanTimerSelMask = 0x3 - chanSigOutEnPos = 2 // SIG_OUT_EN bit - chanSigOutEnMask = 1 << chanSigOutEnPos - chanIdleLvPos = 3 // IDLE_LV bit - chanIdleLvMask = 1 << chanIdleLvPos - chanClkEnPos = 31 // CLK_EN bit - chanClkEnMask = 1 << chanClkEnPos + chanTimerSelMask = 0x3 // TIMER_SEL (bits 0-1) + chanSigOutEnMask = 1 << 2 // SIG_OUT_EN bit + chanIdleLvMask = 1 << 3 // IDLE_LV bit + chanClkEnMask = 1 << 31 // CLK_EN bit // HSCH_CONF1 register - chanDutyScalePos = 0 // DUTY_SCALE position (bits 0-9) - chanDutyScaleMask = 0x3FF chanDutyCyclePos = 10 // DUTY_CYCLE position (bits 10-19) - chanDutyCycleMask = 0x3FF << chanDutyCyclePos chanDutyNumPos = 20 // DUTY_NUM position (bits 20-29) - chanDutyNumMask = 0x3FF << chanDutyNumPos - chanDutyIncPos = 30 // DUTY_INC bit - chanDutyIncMask = 1 << chanDutyIncPos - chanDutyStartPos = 31 // DUTY_START bit - chanDutyStartMask = 1 << chanDutyStartPos + chanDutyIncMask = 1 << 30 + chanDutyStartMask = 1 << 31 // HSCH_DUTY register - duty value uses bits 0-24 (25 bits total) // The lower 4 bits are fractional, upper 20 bits are integer @@ -158,9 +144,7 @@ func (pwm *PWM) Configure(config PWMConfig) error { // Returns divider (with 8 fractional bits), resolution, and any error. func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { if period == 0 { - // Default: ~1kHz with 13-bit resolution (good for LEDs) - // period = 1e9 / 1000 = 1,000,000 ns - period = 1000000 + period = defaultPeriodNs } // Formula: period_ns = (2^resolution * divider) / 80MHz * 1e9 @@ -168,17 +152,13 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { // Register value = actual_divider * 256 (8 fractional bits) // // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 - // divider_reg = period_ns * 80MHz * 256 / (2^resolution * 1e9) // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) - // divider_reg = period_ns * 20480 / (2^resolution * 1000) - // divider_reg = period_ns * 256 * 80 / (2^resolution * 1000) // Try to find the highest resolution that gives a valid divider for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { resolutionValue := uint64(1) << resolution // Calculate divider register value (includes 8 fractional bits) - // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) dividerReg := (period * 80 * 256) / (resolutionValue * 1000) if dividerReg < minDivider { @@ -205,21 +185,35 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } } - // Check if this pin is already assigned to a channel + // Check if this pin is already assigned to a channel on THIS timer for ch := uint8(0); ch < 8; ch++ { - if pwmChannelPins[ch] == pin { + if pwmChannels[ch].inUse && + pwmChannels[ch].pin == pin && + pwmChannels[ch].timer == pwm.num { return ch, nil } } + // Check if pin is used by a different timer (error case) + for ch := uint8(0); ch < 8; ch++ { + if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin { + // Pin is already used by a different timer + return 0, ErrInvalidOutputPin + } + } + // Find an available channel for ch := uint8(0); ch < 8; ch++ { - if pwmChannelPins[ch] == NoPin || pwmChannelPins[ch] == 0 { + if !pwmChannels[ch].inUse { // Found an available channel - pwmChannelPins[ch] = pin + pwmChannels[ch] = pwmChannelInfo{ + pin: pin, + timer: pwm.num, + inUse: true, + } // Configure the GPIO for PWM output through GPIO matrix - signal := uint32(ledcHSSignalOut0 + ch) + signal := uint32(ledcHSSignalBase + ch) pin.configure(PinConfig{Mode: PinOutput}, signal) // Configure the channel @@ -258,6 +252,27 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { return 0, ErrInvalidOutputPin } +// ReleaseChannel releases a PWM channel, making it available for other uses. +// The pin is not reconfigured; call pin.Configure() to change its function. +func (pwm *PWM) ReleaseChannel(channel uint8) error { + if channel >= 8 { + return ErrInvalidOutputPin + } + + if !pwmChannels[channel].inUse || pwmChannels[channel].timer != pwm.num { + // Channel not in use or belongs to different timer + return ErrInvalidOutputPin + } + + // Disable signal output + pwm.channelConf0(channel).ClearBits(chanSigOutEnMask) + + // Clear channel tracking + pwmChannels[channel] = pwmChannelInfo{} + + return nil +} + // Set updates the channel value. This is used to control the channel duty // cycle. For example, to set it to a 25% duty cycle, use: // @@ -288,7 +303,6 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Configure CONF1 and trigger duty update // For non-fading operation: duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1 // Then set duty_start=1 to apply the new duty value - // We write the full register value each time to ensure consistent state var conf1 uint32 conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 conf1 |= 1 << chanDutyNumPos // duty_num = 1 @@ -296,17 +310,27 @@ func (pwm *PWM) Set(channel uint8, value uint32) { conf1 |= chanDutyStartMask // duty_start = 1 pwm.channelConf1(channel).Set(conf1) - // Ensure signal output is enabled (as ESP-IDF does in _ledc_update_duty) + // Ensure signal output is enabled pwm.channelConf0(channel).SetBits(chanSigOutEnMask) } +// Get returns the current duty cycle value for the given channel. +func (pwm *PWM) Get(channel uint8) uint32 { + if channel >= 8 { + return 0 + } + + // Read from the duty read register and remove fractional bits + return pwm.channelDutyR(channel).Get() >> chanDutyFracBits +} + // SetPeriod updates the period of this PWM peripheral in nanoseconds. // To set a particular frequency, use the following formula: // // period = 1e9 / frequency // // SetPeriod will try to maintain the current duty cycle ratio when changing -// the period. +// the period for channels bound to this timer. func (pwm *PWM) SetPeriod(period uint64) error { // Calculate new configuration divider, resolution, err := pwm.calculateConfig(period) @@ -324,14 +348,13 @@ func (pwm *PWM) SetPeriod(period uint64) error { conf := timerConf.Get() conf &^= timerDivNumMask | timerLimMask conf |= (divider << timerDivNumPos) & timerDivNumMask - conf |= uint32(resolution-1) & timerLimMask + conf |= uint32(resolution) & timerLimMask timerConf.Set(conf) - // If resolution changed, we may need to scale duty values + // If resolution changed, scale duty values for channels bound to THIS timer if resolution != oldResolution { - // Scale all active channel duty values for ch := uint8(0); ch < 8; ch++ { - if pwmChannelPins[ch] != NoPin && pwmChannelPins[ch] != 0 { + if pwmChannels[ch].inUse && pwmChannels[ch].timer == pwm.num { // Read current duty (includes fractional bits) currentDuty := pwm.channelDuty(ch).Get() >> chanDutyFracBits @@ -357,7 +380,7 @@ func (pwm *PWM) SetPeriod(period uint64) error { func (pwm *PWM) Top() uint32 { resolution := pwmStates[pwm.num].resolution if resolution == 0 { - resolution = 13 // Default resolution + resolution = defaultResolution } return (1 << resolution) - 1 } @@ -372,7 +395,11 @@ func (pwm *PWM) Counter() uint32 { func (pwm *PWM) Period() uint64 { conf := pwm.timerConf().Get() dividerReg := (conf & timerDivNumMask) >> timerDivNumPos - resolution := conf & timerLimMask // duty_resolution stores bit width directly + resolution := conf & timerLimMask + + if resolution == 0 || dividerReg == 0 { + return 0 + } // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) @@ -390,11 +417,26 @@ func (pwm *PWM) SetInverting(channel uint8, inverting bool) { return } - chanConf0 := pwm.channelConf0(channel) + // Get the pin for this channel to configure GPIO matrix inversion + if !pwmChannels[channel].inUse || pwmChannels[channel].timer != pwm.num { + return + } + + pin := pwmChannels[channel].pin + + // Reconfigure the GPIO with inversion setting through GPIO matrix + // The GPIO matrix FUNC_OUT_SEL_CFG register has an inversion bit + signal := uint32(ledcHSSignalBase + channel) + + // Get the GPIO function output select register + outFunc := pin.outFunc() + if inverting { - chanConf0.SetBits(chanIdleLvMask) + // Set signal with inversion enabled (bit 9 is the invert bit) + outFunc.Set(signal | (1 << 9)) } else { - chanConf0.ClearBits(chanIdleLvMask) + // Set signal without inversion + outFunc.Set(signal) } } @@ -451,3 +493,10 @@ func (pwm *PWM) channelConf1(ch uint8) *volatile.Register32 { base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF1)) return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) } + +// channelDutyR returns the DUTY_R (read) register for the given channel. +func (pwm *PWM) channelDutyR(ch uint8) *volatile.Register32 { + // HSCH0_DUTY_R is at offset 0x10, each channel is 0x14 bytes apart + base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY_R)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) +} From 971ef70e16f93aa4ed01198fedad7a8d22559c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:07:06 +0000 Subject: [PATCH 14/16] machine/esp32: second pass improvements for PWM Additional improvements: - Add errPWMPeriodTooShort error for periods that are too short - Add pwmChannelCount constant and use it consistently - Add gpioMatrixInvertBit constant for clarity - Use dividerFracBits constant in calculations instead of magic 256 - Add isValidChannel() helper for centralized validation - Add IsConnected() method to check if channel is in use - Add SetCounter() method for timer synchronization (API compatibility) - Handle unconfigured timer in Set() by using default resolution - Improve Enable() documentation - Remove unused chanIdleLvMask constant Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 88 ++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 63b856c522..5284868adc 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -5,10 +5,16 @@ package machine import ( "device/esp" + "errors" "runtime/volatile" "unsafe" ) +// PWM peripheral errors +var ( + errPWMPeriodTooShort = errors.New("pwm: period too short") +) + // PWM is one PWM peripheral, which consists of a timer and the associated // channels. There are 4 high-speed timers available (PWM0-PWM3), each can // use any of the 8 high-speed channels. @@ -38,6 +44,9 @@ var ( PWM3 = &PWM{num: 3} ) +// Number of available PWM channels +const pwmChannelCount = 8 + // LEDC peripheral constants const ( // Clock enable bit in DPORT.PERIP_CLK_EN for LEDC @@ -46,15 +55,17 @@ const ( // GPIO matrix output signal numbers for LEDC high-speed channels ledcHSSignalBase = 71 // LEDC_HS_SIG_OUT0, channels are consecutive (71-78) - // Clock divider has 8 fractional bits - // So divider register value = actual_divider * 256 - dividerFractionalBits = 8 + // GPIO matrix output inversion bit + gpioMatrixInvertBit = 1 << 9 + + // Clock divider fractional bits (register value = actual_divider * 256) + dividerFracBits = 8 // Maximum values - maxDivider = 0x3FFFF // 18-bit divider register value (actual max divider ~1024) - minDivider = 1 << dividerFractionalBits // Minimum divider = 256 (represents 1.0) - maxResolution = 20 // Maximum duty resolution in bits - minResolution = 1 // Minimum duty resolution in bits + maxDivider = 0x3FFFF // 18-bit divider register value + minDivider = 1 << dividerFracBits // Minimum divider = 256 (represents 1.0) + maxResolution = 20 // Maximum duty resolution in bits + minResolution = 1 // Minimum duty resolution in bits // Default values defaultPeriodNs = 1_000_000 // 1ms = 1kHz, good for LEDs @@ -77,7 +88,6 @@ const ( // HSCH_CONF0 register chanTimerSelMask = 0x3 // TIMER_SEL (bits 0-1) chanSigOutEnMask = 1 << 2 // SIG_OUT_EN bit - chanIdleLvMask = 1 << 3 // IDLE_LV bit chanClkEnMask = 1 << 31 // CLK_EN bit // HSCH_CONF1 register @@ -154,12 +164,15 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 // divider_reg = period_ns * 80 * 256 / (2^resolution * 1000) + var lastDividerReg uint64 + // Try to find the highest resolution that gives a valid divider for resolution := uint8(maxResolution); resolution >= minResolution; resolution-- { resolutionValue := uint64(1) << resolution // Calculate divider register value (includes 8 fractional bits) - dividerReg := (period * 80 * 256) / (resolutionValue * 1000) + dividerReg := (period * 80 * (1 << dividerFracBits)) / (resolutionValue * 1000) + lastDividerReg = dividerReg if dividerReg < minDivider { // Period too short for this resolution, try lower resolution @@ -171,6 +184,10 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { } } + // Determine which error to return + if lastDividerReg < minDivider { + return 0, 0, errPWMPeriodTooShort + } return 0, 0, ErrPWMPeriodTooLong } @@ -186,7 +203,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } // Check if this pin is already assigned to a channel on THIS timer - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin && pwmChannels[ch].timer == pwm.num { @@ -195,7 +212,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } // Check if pin is used by a different timer (error case) - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin { // Pin is already used by a different timer return 0, ErrInvalidOutputPin @@ -203,7 +220,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } // Find an available channel - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if !pwmChannels[ch].inUse { // Found an available channel pwmChannels[ch] = pwmChannelInfo{ @@ -255,7 +272,7 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { // ReleaseChannel releases a PWM channel, making it available for other uses. // The pin is not reconfigured; call pin.Configure() to change its function. func (pwm *PWM) ReleaseChannel(channel uint8) error { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return ErrInvalidOutputPin } @@ -273,6 +290,19 @@ func (pwm *PWM) ReleaseChannel(channel uint8) error { return nil } +// IsConnected returns true if the given channel is in use by this PWM peripheral. +func (pwm *PWM) IsConnected(channel uint8) bool { + if !pwm.isValidChannel(channel) { + return false + } + return pwmChannels[channel].inUse && pwmChannels[channel].timer == pwm.num +} + +// isValidChannel returns true if the channel number is valid. +func (pwm *PWM) isValidChannel(channel uint8) bool { + return channel < pwmChannelCount +} + // Set updates the channel value. This is used to control the channel duty // cycle. For example, to set it to a 25% duty cycle, use: // @@ -281,11 +311,14 @@ func (pwm *PWM) ReleaseChannel(channel uint8) error { // pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, // pwm.Top()) will set the output to high, assuming the output isn't inverted. func (pwm *PWM) Set(channel uint8, value uint32) { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return } resolution := pwmStates[pwm.num].resolution + if resolution == 0 { + resolution = defaultResolution + } maxValue := uint32((1 << resolution) - 1) // Clamp value to valid range @@ -316,7 +349,7 @@ func (pwm *PWM) Set(channel uint8, value uint32) { // Get returns the current duty cycle value for the given channel. func (pwm *PWM) Get(channel uint8) uint32 { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return 0 } @@ -353,7 +386,7 @@ func (pwm *PWM) SetPeriod(period uint64) error { // If resolution changed, scale duty values for channels bound to THIS timer if resolution != oldResolution { - for ch := uint8(0); ch < 8; ch++ { + for ch := uint8(0); ch < pwmChannelCount; ch++ { if pwmChannels[ch].inUse && pwmChannels[ch].timer == pwm.num { // Read current duty (includes fractional bits) currentDuty := pwm.channelDuty(ch).Get() >> chanDutyFracBits @@ -413,7 +446,7 @@ func (pwm *PWM) Period() uint64 { // was placed at the output, meaning that the output would be 25% low and 75% // high with a duty cycle of 25%. func (pwm *PWM) SetInverting(channel uint8, inverting bool) { - if channel >= 8 { + if !pwm.isValidChannel(channel) { return } @@ -425,22 +458,21 @@ func (pwm *PWM) SetInverting(channel uint8, inverting bool) { pin := pwmChannels[channel].pin // Reconfigure the GPIO with inversion setting through GPIO matrix - // The GPIO matrix FUNC_OUT_SEL_CFG register has an inversion bit + // The GPIO matrix FUNC_OUT_SEL_CFG register has an inversion bit (bit 9) signal := uint32(ledcHSSignalBase + channel) // Get the GPIO function output select register outFunc := pin.outFunc() if inverting { - // Set signal with inversion enabled (bit 9 is the invert bit) - outFunc.Set(signal | (1 << 9)) + outFunc.Set(signal | gpioMatrixInvertBit) } else { - // Set signal without inversion outFunc.Set(signal) } } -// Enable enables or disables the PWM output for all channels on this timer. +// Enable enables or disables this PWM timer. When disabled (paused), the timer +// stops counting and all channels using this timer will hold their current state. func (pwm *PWM) Enable(enable bool) { timerConf := pwm.timerConf() if enable { @@ -450,6 +482,18 @@ func (pwm *PWM) Enable(enable bool) { } } +// SetCounter sets the counter value of this PWM timer. This can be used to +// synchronize multiple PWM timers. +func (pwm *PWM) SetCounter(value uint32) { + // The counter is reset by setting the RST bit, then writing the value + // Note: ESP32 LEDC doesn't support directly writing counter value, + // so we reset to 0 instead. This method is provided for API compatibility. + timerConf := pwm.timerConf() + conf := timerConf.Get() + timerConf.Set(conf | timerRstMask) + timerConf.Set(conf) +} + // Register access helpers // timerConf returns the configuration register for this timer. From 2d3b1b22384ab7bded23f7ad3a6902ad6743ca70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:16:08 +0000 Subject: [PATCH 15/16] machine/esp32: third pass improvements for PWM Additional improvements: - Add pwmTimerCount constant and use it for pwmStates array - Use pwmChannelCount constant for pwmChannels array declaration - Add IsEnabled() method to check if timer is running - Add Frequency() method to get current PWM frequency in Hz - Add Resolution() method to get current duty resolution in bits - Add GetPin() method to get pin assigned to a channel - Rename SetCounter() to ResetCounter() (ESP32 only supports reset to 0) - Fix Period() to use dividerFracBits constant instead of magic 256 - Remove misleading 'Idle level low' comment in Channel() Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 60 +++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index 5284868adc..d038fbfe56 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -32,8 +32,14 @@ type pwmChannelInfo struct { inUse bool // Whether this channel is currently in use } +// Number of PWM timers and channels +const ( + pwmTimerCount = 4 // Number of high-speed timers (0-3) + pwmChannelCount = 8 // Number of high-speed channels (0-7) +) + // pwmChannels tracks which pin and timer is assigned to each channel (0-7) -var pwmChannels [8]pwmChannelInfo +var pwmChannels [pwmChannelCount]pwmChannelInfo // Hardware PWM peripherals available on ESP32. // These use the high-speed LEDC timers for glitch-free PWM updates. @@ -44,9 +50,6 @@ var ( PWM3 = &PWM{num: 3} ) -// Number of available PWM channels -const pwmChannelCount = 8 - // LEDC peripheral constants const ( // Clock enable bit in DPORT.PERIP_CLK_EN for LEDC @@ -107,7 +110,7 @@ type pwmState struct { configured bool // Whether the timer has been configured } -var pwmStates [4]pwmState +var pwmStates [pwmTimerCount]pwmState // Configure enables and configures this PWM peripheral. // The period is specified in nanoseconds. A period of 0 will select a default @@ -240,7 +243,6 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { // - Enable clock // - Select this timer // - Enable signal output - // - Idle level low var conf uint32 conf |= chanClkEnMask // Enable clock conf |= uint32(pwm.num) & chanTimerSelMask // Select timer @@ -435,9 +437,27 @@ func (pwm *PWM) Period() uint64 { } // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 - // period_ns = (2^resolution * divider_reg * 1000) / (80 * 256) + // period_ns = (2^resolution * divider_reg * 1000) / (80 * (1 << dividerFracBits)) resolutionValue := uint64(1) << resolution - return resolutionValue * uint64(dividerReg) * 1000 / (80 * 256) + return resolutionValue * uint64(dividerReg) * 1000 / (80 << dividerFracBits) +} + +// Frequency returns the current PWM frequency in Hz. +func (pwm *PWM) Frequency() uint32 { + period := pwm.Period() + if period == 0 { + return 0 + } + return uint32(1_000_000_000 / period) +} + +// Resolution returns the current duty cycle resolution in bits. +func (pwm *PWM) Resolution() uint8 { + resolution := pwmStates[pwm.num].resolution + if resolution == 0 { + return defaultResolution + } + return resolution } // SetInverting sets whether to invert the output of this channel. @@ -482,18 +502,32 @@ func (pwm *PWM) Enable(enable bool) { } } -// SetCounter sets the counter value of this PWM timer. This can be used to +// IsEnabled returns true if this PWM timer is running (not paused). +func (pwm *PWM) IsEnabled() bool { + return (pwm.timerConf().Get() & timerPauseMask) == 0 +} + +// ResetCounter resets the timer counter to 0. This can be used to // synchronize multiple PWM timers. -func (pwm *PWM) SetCounter(value uint32) { - // The counter is reset by setting the RST bit, then writing the value - // Note: ESP32 LEDC doesn't support directly writing counter value, - // so we reset to 0 instead. This method is provided for API compatibility. +func (pwm *PWM) ResetCounter() { timerConf := pwm.timerConf() conf := timerConf.Get() timerConf.Set(conf | timerRstMask) timerConf.Set(conf) } +// GetPin returns the pin assigned to the given channel, or NoPin if the +// channel is not in use by this PWM peripheral. +func (pwm *PWM) GetPin(channel uint8) Pin { + if !pwm.isValidChannel(channel) { + return NoPin + } + if !pwmChannels[channel].inUse || pwmChannels[channel].timer != pwm.num { + return NoPin + } + return pwmChannels[channel].pin +} + // Register access helpers // timerConf returns the configuration register for this timer. From 39b6261379e9ff1dc9a2024f44bfbe7b568ba2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 20 Jan 2026 16:21:14 +0000 Subject: [PATCH 16/16] machine/esp32: fourth pass improvements for PWM Final improvements: - Add apbClockMHz constant (80) instead of hardcoded values - Add timerRegisterStride and channelRegisterStride constants - Add SetFrequency() method for convenience - Add ChannelCount() method to query available channels - Optimize Channel() to use single-pass loop instead of three loops - Add error documentation to Channel() function comment - Simplify register access helper comments Co-authored-by: Ona --- src/machine/machine_esp32_pwm.go | 163 +++++++++++++++++-------------- 1 file changed, 92 insertions(+), 71 deletions(-) diff --git a/src/machine/machine_esp32_pwm.go b/src/machine/machine_esp32_pwm.go index d038fbfe56..9b43ea56fd 100644 --- a/src/machine/machine_esp32_pwm.go +++ b/src/machine/machine_esp32_pwm.go @@ -61,6 +61,9 @@ const ( // GPIO matrix output inversion bit gpioMatrixInvertBit = 1 << 9 + // APB clock frequency in MHz (used by high-speed LEDC) + apbClockMHz = 80 + // Clock divider fractional bits (register value = actual_divider * 256) dividerFracBits = 8 @@ -73,6 +76,10 @@ const ( // Default values defaultPeriodNs = 1_000_000 // 1ms = 1kHz, good for LEDs defaultResolution = 13 // 13-bit resolution for default period + + // Register offsets + timerRegisterStride = 0x8 // Bytes between timer registers + channelRegisterStride = 0x14 // Bytes between channel registers ) // LEDC register bit positions and masks for timer configuration @@ -174,7 +181,7 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { resolutionValue := uint64(1) << resolution // Calculate divider register value (includes 8 fractional bits) - dividerReg := (period * 80 * (1 << dividerFracBits)) / (resolutionValue * 1000) + dividerReg := (period * apbClockMHz * (1 << dividerFracBits)) / (resolutionValue * 1000) lastDividerReg = dividerReg if dividerReg < minDivider { @@ -197,6 +204,9 @@ func (pwm *PWM) calculateConfig(period uint64) (uint32, uint8, error) { // Channel returns a PWM channel for the given pin. If the pin is already // configured for this PWM peripheral, the same channel is returned. // The pin is configured for PWM output. +// +// Returns ErrInvalidOutputPin if the pin is already used by a different timer +// or if no channels are available. func (pwm *PWM) Channel(pin Pin) (uint8, error) { if !pwmStates[pwm.num].configured { // Timer not configured, configure with default period @@ -205,70 +215,73 @@ func (pwm *PWM) Channel(pin Pin) (uint8, error) { } } - // Check if this pin is already assigned to a channel on THIS timer + // Single pass: find existing assignment, check for conflicts, find free channel + var freeChannel int8 = -1 for ch := uint8(0); ch < pwmChannelCount; ch++ { - if pwmChannels[ch].inUse && - pwmChannels[ch].pin == pin && - pwmChannels[ch].timer == pwm.num { - return ch, nil + if !pwmChannels[ch].inUse { + if freeChannel < 0 { + freeChannel = int8(ch) + } + continue } - } - // Check if pin is used by a different timer (error case) - for ch := uint8(0); ch < pwmChannelCount; ch++ { - if pwmChannels[ch].inUse && pwmChannels[ch].pin == pin { - // Pin is already used by a different timer + if pwmChannels[ch].pin == pin { + if pwmChannels[ch].timer == pwm.num { + // Already assigned to this timer + return ch, nil + } + // Pin is used by a different timer return 0, ErrInvalidOutputPin } } - // Find an available channel - for ch := uint8(0); ch < pwmChannelCount; ch++ { - if !pwmChannels[ch].inUse { - // Found an available channel - pwmChannels[ch] = pwmChannelInfo{ - pin: pin, - timer: pwm.num, - inUse: true, - } + // No existing assignment found, use free channel if available + if freeChannel < 0 { + return 0, ErrInvalidOutputPin + } - // Configure the GPIO for PWM output through GPIO matrix - signal := uint32(ledcHSSignalBase + ch) - pin.configure(PinConfig{Mode: PinOutput}, signal) - - // Configure the channel - chanConf0 := pwm.channelConf0(ch) - - // Set channel configuration: - // - Enable clock - // - Select this timer - // - Enable signal output - var conf uint32 - conf |= chanClkEnMask // Enable clock - conf |= uint32(pwm.num) & chanTimerSelMask // Select timer - conf |= chanSigOutEnMask // Enable output - chanConf0.Set(conf) - - // Initialize duty to 0 - pwm.channelDuty(ch).Set(0) - - // Set HPOINT to 0 (start of cycle) - pwm.channelHpoint(ch).Set(0) - - // Configure CONF1 for non-fading operation and trigger duty update - // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 - var conf1 uint32 - conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 - conf1 |= 1 << chanDutyNumPos // duty_num = 1 - conf1 |= chanDutyIncMask // duty_inc = 1 - conf1 |= chanDutyStartMask // duty_start = 1 - pwm.channelConf1(ch).Set(conf1) - - return ch, nil - } + ch := uint8(freeChannel) + + // Configure the new channel + pwmChannels[ch] = pwmChannelInfo{ + pin: pin, + timer: pwm.num, + inUse: true, } - return 0, ErrInvalidOutputPin + // Configure the GPIO for PWM output through GPIO matrix + signal := uint32(ledcHSSignalBase + ch) + pin.configure(PinConfig{Mode: PinOutput}, signal) + + // Configure the channel + chanConf0 := pwm.channelConf0(ch) + + // Set channel configuration: + // - Enable clock + // - Select this timer + // - Enable signal output + var conf uint32 + conf |= chanClkEnMask // Enable clock + conf |= uint32(pwm.num) & chanTimerSelMask // Select timer + conf |= chanSigOutEnMask // Enable output + chanConf0.Set(conf) + + // Initialize duty to 0 + pwm.channelDuty(ch).Set(0) + + // Set HPOINT to 0 (start of cycle) + pwm.channelHpoint(ch).Set(0) + + // Configure CONF1 for non-fading operation and trigger duty update + // duty_scale=0, duty_cycle=1, duty_num=1, duty_inc=1, duty_start=1 + var conf1 uint32 + conf1 |= 1 << chanDutyCyclePos // duty_cycle = 1 + conf1 |= 1 << chanDutyNumPos // duty_num = 1 + conf1 |= chanDutyIncMask // duty_inc = 1 + conf1 |= chanDutyStartMask // duty_start = 1 + pwm.channelConf1(ch).Set(conf1) + + return ch, nil } // ReleaseChannel releases a PWM channel, making it available for other uses. @@ -437,9 +450,9 @@ func (pwm *PWM) Period() uint64 { } // period_ns = (2^resolution * divider_reg / 256) / 80MHz * 1e9 - // period_ns = (2^resolution * divider_reg * 1000) / (80 * (1 << dividerFracBits)) + // period_ns = (2^resolution * divider_reg * 1000) / (apbClockMHz * (1 << dividerFracBits)) resolutionValue := uint64(1) << resolution - return resolutionValue * uint64(dividerReg) * 1000 / (80 << dividerFracBits) + return resolutionValue * uint64(dividerReg) * 1000 / (apbClockMHz << dividerFracBits) } // Frequency returns the current PWM frequency in Hz. @@ -451,6 +464,15 @@ func (pwm *PWM) Frequency() uint32 { return uint32(1_000_000_000 / period) } +// SetFrequency sets the PWM frequency in Hz. +// This is a convenience method equivalent to SetPeriod(1e9 / frequency). +func (pwm *PWM) SetFrequency(frequency uint32) error { + if frequency == 0 { + return ErrPWMPeriodTooLong + } + return pwm.SetPeriod(1_000_000_000 / uint64(frequency)) +} + // Resolution returns the current duty cycle resolution in bits. func (pwm *PWM) Resolution() uint8 { resolution := pwmStates[pwm.num].resolution @@ -460,6 +482,12 @@ func (pwm *PWM) Resolution() uint8 { return resolution } +// ChannelCount returns the number of channels available for this PWM peripheral. +// Note: Channels are shared across all PWM timers on ESP32. +func (pwm *PWM) ChannelCount() uint8 { + return pwmChannelCount +} + // SetInverting sets whether to invert the output of this channel. // Without inverting, a 25% duty cycle would mean the output is high for 25% of // the time and low for the rest. Inverting flips the output as if a NOT gate @@ -532,49 +560,42 @@ func (pwm *PWM) GetPin(channel uint8) Pin { // timerConf returns the configuration register for this timer. func (pwm *PWM) timerConf() *volatile.Register32 { - // HSTIMER0_CONF is at offset 0x140, each timer is 0x8 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_CONF)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*timerRegisterStride)) } // timerValue returns the value register for this timer. func (pwm *PWM) timerValue() *volatile.Register32 { - // HSTIMER0_VALUE is at offset 0x144, each timer is 0x8 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSTIMER0_VALUE)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*0x8)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(pwm.num)*timerRegisterStride)) } // channelConf0 returns the CONF0 register for the given channel. func (pwm *PWM) channelConf0(ch uint8) *volatile.Register32 { - // HSCH0_CONF0 is at offset 0x0, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF0)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelHpoint returns the HPOINT register for the given channel. func (pwm *PWM) channelHpoint(ch uint8) *volatile.Register32 { - // HSCH0_HPOINT is at offset 0x4, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_HPOINT)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelDuty returns the DUTY register for the given channel. func (pwm *PWM) channelDuty(ch uint8) *volatile.Register32 { - // HSCH0_DUTY is at offset 0x8, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelConf1 returns the CONF1 register for the given channel. func (pwm *PWM) channelConf1(ch uint8) *volatile.Register32 { - // HSCH0_CONF1 is at offset 0xC, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_CONF1)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) } // channelDutyR returns the DUTY_R (read) register for the given channel. func (pwm *PWM) channelDutyR(ch uint8) *volatile.Register32 { - // HSCH0_DUTY_R is at offset 0x10, each channel is 0x14 bytes apart base := uintptr(unsafe.Pointer(&esp.LEDC.HSCH0_DUTY_R)) - return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*0x14)) + return (*volatile.Register32)(unsafe.Pointer(base + uintptr(ch)*channelRegisterStride)) }