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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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 f0256cab18469c394147fed815dfa19a840e16e1 Mon Sep 17 00:00:00 2001 From: robo Date: Wed, 21 Jan 2026 20:59:17 -0300 Subject: [PATCH 08/81] Fix syntax for building with TinyGo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 518dcdad18..4c8a2c69be 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ tinygo build -buildmode=c-shared -o add.wasm -target=wasip1 add.go You can also use the same syntax as Go 1.24+: ```shell -GOARCH=wasip1 GOOS=wasm tinygo build -buildmode=c-shared -o add.wasm add.go +GOOS=wasip1 GOARCH=wasm tinygo build -buildmode=c-shared -o add.wasm add.go ``` ## Installation From e79cdc1122acecf7d830749bcbda717d0201c1e6 Mon Sep 17 00:00:00 2001 From: Yaj <51419431+YajTPG@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:34:22 +0530 Subject: [PATCH 09/81] targets: Add Shrike Lite board (#5170) * feat: Add Vicharak Shrike Lite * Add shrike-lite to smoketest --- GNUmakefile | 2 + src/machine/board_vicharak_shrike-lite.go | 118 ++++++++++++++++++++++ targets/vicharak_shrike-lite.json | 14 +++ 3 files changed, 134 insertions(+) create mode 100644 src/machine/board_vicharak_shrike-lite.go create mode 100644 targets/vicharak_shrike-lite.json diff --git a/GNUmakefile b/GNUmakefile index fc8d71d998..f4c3dc533b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -814,6 +814,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=waveshare-rp2040-tiny examples/echo @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=vicharak_shrike-lite examples/echo + @$(MD5SUM) test.hex # test pwm $(TINYGO) build -size short -o test.hex -target=itsybitsy-m0 examples/pwm @$(MD5SUM) test.hex diff --git a/src/machine/board_vicharak_shrike-lite.go b/src/machine/board_vicharak_shrike-lite.go new file mode 100644 index 0000000000..8899e7125c --- /dev/null +++ b/src/machine/board_vicharak_shrike-lite.go @@ -0,0 +1,118 @@ +//go:build vicharak_shrike_lite + +// Pin mappings for Vicharak Shrike-Lite. +// +// Reference: https://vicharak-in.github.io/shrike/shrike_pinouts.html + +package machine + +// Digital +const ( + IO0 Pin = GPIO0 + IO1 Pin = GPIO1 + IO2 Pin = GPIO2 + IO3 Pin = GPIO3 + IO4 Pin = GPIO4 + IO5 Pin = GPIO5 + IO6 Pin = GPIO6 + IO7 Pin = GPIO7 + IO8 Pin = GPIO8 + IO9 Pin = GPIO9 + IO10 Pin = GPIO10 + IO11 Pin = GPIO11 + IO12 Pin = GPIO12 + IO13 Pin = GPIO13 + IO14 Pin = GPIO14 + IO15 Pin = GPIO15 + IO16 Pin = GPIO16 + IO17 Pin = GPIO17 + IO18 Pin = GPIO18 + IO19 Pin = GPIO19 + IO20 Pin = GPIO20 + IO21 Pin = GPIO21 + IO22 Pin = GPIO22 + IO23 Pin = GPIO23 + IO24 Pin = GPIO24 + IO25 Pin = GPIO25 + IO26 Pin = GPIO26 + IO27 Pin = GPIO27 + IO28 Pin = GPIO28 + IO29 Pin = GPIO29 +) + +// FPGA Pins +const ( + FPGA_EN Pin = IO13 + FPGA_PWR Pin = IO12 + // SPI_SCLK + F3 Pin = IO2 + // SPI_SS + F4 Pin = IO1 + // SPI_SI (MOSI) + F5 Pin = IO3 + // SPI_SO (MISO) / CONFIG + F6 Pin = IO0 + F18 Pin = IO14 + F17 Pin = IO15 +) + +// Analog pins +const ( + A0 Pin = IO26 + A1 Pin = IO27 + A2 Pin = IO28 + A3 Pin = IO29 +) + +// LED +const ( + LED = IO4 +) + +// I2C pins +const ( + I2C0_SDA_PIN Pin = IO24 + I2C0_SCL_PIN Pin = IO25 + + I2C1_SDA_PIN Pin = IO6 + I2C1_SCL_PIN Pin = IO7 +) + +// SPI pins +const ( + SPI0_SCK_PIN Pin = IO18 + SPI0_SDO_PIN Pin = IO19 + SPI0_SDI_PIN Pin = IO20 + + SPI1_SCK_PIN Pin = IO10 + SPI1_SDO_PIN Pin = IO11 + SPI1_SDI_PIN Pin = IO8 +) + +// Onboard crystal oscillator frequency, in MHz. +const ( + xoscFreq = 12 // MHz +) + +// UART pins +const ( + UART0_TX_PIN = IO28 + UART0_RX_PIN = IO29 + UART_TX_PIN = UART0_TX_PIN + UART_RX_PIN = UART0_RX_PIN + UART1_TX_PIN = IO24 + UART1_RX_PIN = IO25 +) + +var DefaultUART = UART0 + +// USB CDC identifiers +const ( + usb_STRING_PRODUCT = "Shrike-Lite" + usb_STRING_MANUFACTURER = "Vicharak" +) + +var ( + usb_VID uint16 = 0x2e8a + usb_PID uint16 = 0x0003 +) diff --git a/targets/vicharak_shrike-lite.json b/targets/vicharak_shrike-lite.json new file mode 100644 index 0000000000..c801b2c27b --- /dev/null +++ b/targets/vicharak_shrike-lite.json @@ -0,0 +1,14 @@ +{ + "inherits": [ + "rp2040" + ], + "serial-port": ["2e8a:0003"], + "default-stack-size": 8192, + "build-tags": ["vicharak_shrike_lite"], + "ldflags": [ + "--defsym=__flash_size=4M" + ], + "extra-files": [ + "targets/pico-boot-stage2.S" + ] +} From bef0dc5d219aede409526e6c3ea54ba214888ade Mon Sep 17 00:00:00 2001 From: Matthew Hiles <15929821+sparques@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:09:34 -0500 Subject: [PATCH 10/81] Add per-byte timeout budget for rp2 I2C (#5189) * Add per-byte timeout budget for rp2 I2C * run goimports --- src/machine/machine_rp2_i2c.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_rp2_i2c.go b/src/machine/machine_rp2_i2c.go index 50e2e8a277..e4de7a783b 100644 --- a/src/machine/machine_rp2_i2c.go +++ b/src/machine/machine_rp2_i2c.go @@ -280,8 +280,6 @@ func (i2c *I2C) deinit() (resetVal uint32) { // tx performs blocking write followed by read to I2C bus. func (i2c *I2C) tx(addr uint8, tx, rx []byte) (err error) { - const timeout_us = 4_000 - deadline := ticks() + timeout_us if addr >= 0x80 || isReservedI2CAddr(addr) { return errInvalidTgtAddr } @@ -292,6 +290,14 @@ func (i2c *I2C) tx(addr uint8, tx, rx []byte) (err error) { return nil } + // Base 4ms for small register pokes. + // Add per-byte budget. 100us/byte is conservative at 400kHz and still ok at 100kHz for modest sizes. + timeout_us := uint64(4_000) + uint64(txlen+rxlen)*100 + // Cap so it doesn't go insane: + timeout_us = min(timeout_us, 500_000) + + deadline := ticks() + timeout_us + err = i2c.disable() if err != nil { return err From 66d7099c966079ded9e92299b071def3f35c20ab Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 4 Feb 2026 17:33:45 +0100 Subject: [PATCH 11/81] build: update CI builds to use latest Go 1.25.7 release Signed-off-by: deadprogram --- .github/workflows/build-macos.yml | 4 ++-- .github/workflows/linux.yml | 6 +++--- .github/workflows/windows.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 1ff0be8def..1db9de1b48 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -40,7 +40,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Restore LLVM source cache uses: actions/cache/restore@v4 @@ -135,7 +135,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Build TinyGo (LLVM ${{ matrix.version }}) run: go install -tags=llvm${{ matrix.version }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index d5a6619fbc..e11d8193af 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -137,7 +137,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Install wasmtime uses: bytecodealliance/actions/wasmtime/setup@v1 @@ -181,7 +181,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Install Node.js uses: actions/setup-node@v4 @@ -298,7 +298,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Restore LLVM source cache uses: actions/cache/restore@v4 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 60b0d8cb5d..0cfeb518be 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -41,7 +41,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Restore cached LLVM source uses: actions/cache/restore@v4 @@ -147,7 +147,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Download TinyGo build uses: actions/download-artifact@v4 @@ -177,7 +177,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Download TinyGo build uses: actions/download-artifact@v4 @@ -213,7 +213,7 @@ jobs: - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.25.5' + go-version: '1.25.7' cache: true - name: Download TinyGo build uses: actions/download-artifact@v4 From f23e18a8de41dfe26a7208f99205f8421795ba54 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 6 Feb 2026 15:53:52 +0100 Subject: [PATCH 12/81] flake.*: bump to nixpkgs 25.11 Bump the GitHub Actions Nix install as well; nixpkgs 25.11 requires a newer nix command. --- .github/workflows/nix.yml | 2 +- flake.lock | 8 ++++---- flake.nix | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index e65ae3193a..6c5b9f9a41 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} path: | llvm-project/compiler-rt - - uses: cachix/install-nix-action@v22 + - uses: cachix/install-nix-action@v31 - name: Test run: | nix develop --ignore-environment --keep HOME --command bash -c "go install && ~/go/bin/tinygo version && ~/go/bin/tinygo build -o test ./testdata/cgo" diff --git a/flake.lock b/flake.lock index 877c18b461..9ff701357d 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1747953325, - "narHash": "sha256-y2ZtlIlNTuVJUZCqzZAhIw5rrKP4DOSklev6c8PyCkQ=", + "lastModified": 1770136044, + "narHash": "sha256-tlFqNG/uzz2++aAmn4v8J0vAkV3z7XngeIIB3rM3650=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "55d1f923c480dadce40f5231feb472e81b0bab48", + "rev": "e576e3c9cf9bad747afcddd9e34f51d18c855b4e", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-25.05", + "ref": "nixos-25.11", "type": "indirect" } }, diff --git a/flake.nix b/flake.nix index 4feea0a4b8..85ab404940 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ inputs = { # Use a recent stable release, but fix the version to make it reproducible. # This version should be updated from time to time. - nixpkgs.url = "nixpkgs/nixos-25.05"; + nixpkgs.url = "nixpkgs/nixos-25.11"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: From f5b2a08b15c523a41f4a7eca963ad25031ce7539 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 6 Feb 2026 15:16:21 +0100 Subject: [PATCH 13/81] builder: order embedded files deterministically --- builder/build.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/builder/build.go b/builder/build.go index a598f01965..44f41eb233 100644 --- a/builder/build.go +++ b/builder/build.go @@ -19,6 +19,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "sort" "strconv" "strings" @@ -281,9 +282,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe allFiles[file.Name] = append(allFiles[file.Name], file) } } - for name, files := range allFiles { - name := name - files := files + // Sort embedded files by name to maintain output determinism. + embedNames := make([]string, 0, len(allFiles)) + for _, files := range allFiles { + embedNames = append(embedNames, files[0].Name) + } + slices.Sort(embedNames) + for _, name := range embedNames { job := &compileJob{ description: "make object file for " + name, run: func(job *compileJob) error { @@ -298,7 +303,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe sum := sha256.Sum256(data) hexSum := hex.EncodeToString(sum[:16]) - for _, file := range files { + for _, file := range allFiles[name] { file.Size = uint64(len(data)) file.Hash = hexSum if file.NeedsData { From 32378537b8563b07c691cb5fb2e258db10ad9b14 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 11 Feb 2026 09:45:32 +0100 Subject: [PATCH 14/81] sponsorship: add explicit callout/link in README to help out TinyGo Signed-off-by: deadprogram --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4c8a2c69be..86955a2d9d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ TinyGo is a Go compiler intended for use in small places such as microcontroller It reuses libraries used by the [Go language tools](https://golang.org/pkg/go/) alongside [LLVM](http://llvm.org) to provide an alternative way to compile programs written in the Go programming language. +> [!IMPORTANT] +> You can help TinyGo with a financial contribution using OpenCollective. Please see https://opencollective.com/tinygo for more information. Thank you! + ## Embedded Here is an example program that blinks the built-in LED when run directly on any supported board with onboard LED: From 4b0e858964a27c893c199783aec1278d370866b2 Mon Sep 17 00:00:00 2001 From: Dima Date: Sun, 15 Feb 2026 15:50:39 +0200 Subject: [PATCH 15/81] Esp32s3 implement spi (#5169) * esp32s3 spi * stabilization freq cpu * cheange clacl freq for spi * fix linters * esp32s3-spi: change default pins for esp32s3 xiao * set default configuration * esp32s3-spi: extends smoketests for esp32s3 --- GNUmakefile | 6 + src/machine/board_xiao-esp32s3.go | 12 +- src/machine/machine_esp32s3-wroom1.go | 15 + src/machine/machine_esp32s3_spi.go | 460 ++++++++++++++++++++++++++ src/runtime/runtime_esp32s3.go | 14 + targets/esp32s3-wroom1.json | 4 + 6 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 src/machine/machine_esp32s3-wroom1.go create mode 100644 src/machine/machine_esp32s3_spi.go create mode 100644 targets/esp32s3-wroom1.json diff --git a/GNUmakefile b/GNUmakefile index f4c3dc533b..de77ac41f0 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -923,6 +923,12 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/blinky1 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/mcp3008 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/mcp3008 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3 examples/mcp3008 + @$(MD5SUM) test.bin endif $(TINYGO) build -size short -o test.bin -target=esp-c3-32s-kit examples/blinky1 @$(MD5SUM) test.bin diff --git a/src/machine/board_xiao-esp32s3.go b/src/machine/board_xiao-esp32s3.go index 9181bffc2e..6e1e67b832 100644 --- a/src/machine/board_xiao-esp32s3.go +++ b/src/machine/board_xiao-esp32s3.go @@ -47,9 +47,15 @@ const ( // SPI pins const ( - SPI_SCK_PIN = GPIO7 - SPI_SDI_PIN = GPIO9 - SPI_SDO_PIN = GPIO8 + SPI1_SCK_PIN = GPIO7 // D8 + SPI1_MISO_PIN = GPIO8 // D9 + SPI1_MOSI_PIN = GPIO9 // D10 + SPI1_CS_PIN = NoPin + + SPI2_SCK_PIN = NoPin + SPI2_MOSI_PIN = NoPin + SPI2_MISO_PIN = NoPin + SPI2_CS_PIN = NoPin ) // Onboard LEDs diff --git a/src/machine/machine_esp32s3-wroom1.go b/src/machine/machine_esp32s3-wroom1.go new file mode 100644 index 0000000000..8ecfb75098 --- /dev/null +++ b/src/machine/machine_esp32s3-wroom1.go @@ -0,0 +1,15 @@ +//go:build esp32s3 && !xiao_esp32s3 + +package machine + +const ( + SPI1_SCK_PIN = GPIO12 // SCK + SPI1_MOSI_PIN = GPIO11 // SDO (MOSI) + SPI1_MISO_PIN = GPIO13 // SDI (MISO) + SPI1_CS_PIN = GPIO10 // CS + + SPI2_SCK_PIN = GPIO36 // SCK + SPI2_MOSI_PIN = GPIO35 // SDO (MOSI) + SPI2_MISO_PIN = GPIO37 // SDI (MISO) + SPI2_CS_PIN = GPIO34 // CS +) diff --git a/src/machine/machine_esp32s3_spi.go b/src/machine/machine_esp32s3_spi.go new file mode 100644 index 0000000000..8c1618fc39 --- /dev/null +++ b/src/machine/machine_esp32s3_spi.go @@ -0,0 +1,460 @@ +//go:build esp32s3 + +package machine + +// ESP32-S3 SPI support based on ESP-IDF HAL +// Simple but correct implementation following spi_ll.h +// SPI0 = hardware SPI2 (FSPI), SPI1 = hardware SPI3 (HSPI) +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/spi_master.html + +import ( + "device/esp" + "errors" + "runtime/volatile" + "unsafe" +) + +const ( + SPI_MODE0 = uint8(0) + SPI_MODE1 = uint8(1) + SPI_MODE2 = uint8(2) + SPI_MODE3 = uint8(3) + + // ESP32-S3 PLL clock frequency (same as ESP32-C3) + pplClockFreq = 80e6 + + // Default SPI frequency - maximum safe speed + SPI_DEFAULT_FREQUENCY = 80e6 // 80MHz +) + +const ( + // IO MUX function number for SPI direct connection + SPI_IOMUX_FUNC = 4 +) + +// ESP32-S3 GPIO Matrix signal indices for SPI - CORRECTED from ESP-IDF gpio_sig_map.h +const ( + // SPI2 (FSPI) signals - Hardware SPI2 - CORRECT VALUES from ESP-IDF + SPI2_CLK_OUT_IDX = uint32(101) // FSPICLK_OUT_IDX + SPI2_CLK_IN_IDX = uint32(101) // FSPICLK_IN_IDX + SPI2_Q_OUT_IDX = uint32(102) // FSPIQ_OUT_IDX (MISO) + SPI2_Q_IN_IDX = uint32(102) // FSPIQ_IN_IDX + SPI2_D_OUT_IDX = uint32(103) // FSPID_OUT_IDX (MOSI) + SPI2_D_IN_IDX = uint32(103) // FSPID_IN_IDX + SPI2_CS0_OUT_IDX = uint32(110) // FSPICS0_OUT_IDX + + // SPI3 (HSPI) signals - Hardware SPI3 - CORRECTED from ESP-IDF gpio_sig_map.h + // Source: /esp-idf/components/soc/esp32s3/include/soc/gpio_sig_map.h + SPI3_CLK_OUT_IDX = uint32(66) // Line 136: SPI3_CLK_OUT_IDX + SPI3_CLK_IN_IDX = uint32(66) // Line 135: SPI3_CLK_IN_IDX + SPI3_Q_OUT_IDX = uint32(67) // Line 138: SPI3_Q_OUT_IDX (MISO) + SPI3_Q_IN_IDX = uint32(67) // Line 137: SPI3_Q_IN_IDX + SPI3_D_OUT_IDX = uint32(68) // Line 140: SPI3_D_OUT_IDX (MOSI) + SPI3_D_IN_IDX = uint32(68) // Line 139: SPI3_D_IN_IDX + SPI3_CS0_OUT_IDX = uint32(71) // Line 146: SPI3_CS0_OUT_IDX +) + +type SPI struct { + Bus interface{} + busID uint8 +} + +var ( + SPI0 = &SPI{Bus: esp.SPI2, busID: 2} // Primary SPI (FSPI) + SPI1 = &SPI{Bus: esp.SPI3, busID: 3} // Secondary SPI (HSPI) +) + +type SPIConfig struct { + Frequency uint32 + SCK Pin // Serial Clock + SDO Pin // Serial Data Out (MOSI) + SDI Pin // Serial Data In (MISO) + CS Pin // Chip Select (optional) + LSBFirst bool // MSB is default + Mode uint8 // SPI_MODE0 is default +} + +// Configure and make the SPI peripheral ready to use. +// Implementation following ESP-IDF HAL with GPIO Matrix routing +func (spi *SPI) Configure(config SPIConfig) error { + // Set default + if config.Frequency == 0 { + config.Frequency = SPI_DEFAULT_FREQUENCY + } + + switch spi.busID { + case 2: // SPI2 (FSPI) + if config.SCK == 0 { + config.SCK = SPI1_SCK_PIN + } + if config.SDO == 0 { + config.SDO = SPI1_MOSI_PIN + } + if config.SDI == 0 { + config.SDI = SPI1_MISO_PIN + } + case 3: // SPI3 (HSPI) + if config.SCK == 0 { + config.SCK = SPI2_SCK_PIN + } + if config.SDO == 0 { + config.SDO = SPI2_MOSI_PIN + } + if config.SDI == 0 { + config.SDI = SPI2_MISO_PIN + } + default: + } + + // Get GPIO Matrix signal indices for this SPI bus + var sckOutIdx, mosiOutIdx, misoInIdx, csOutIdx uint32 + switch spi.busID { + case 2: // SPI2 (FSPI) + sckOutIdx = SPI2_CLK_OUT_IDX + mosiOutIdx = SPI2_D_OUT_IDX + misoInIdx = SPI2_Q_IN_IDX + csOutIdx = SPI2_CS0_OUT_IDX + case 3: // SPI3 (HSPI) + sckOutIdx = SPI3_CLK_OUT_IDX + mosiOutIdx = SPI3_D_OUT_IDX + misoInIdx = SPI3_Q_IN_IDX + csOutIdx = SPI3_CS0_OUT_IDX + default: + return ErrInvalidSPIBus + } + + // Check if we can use IO MUX direct connection for better performance + if isDefaultSPIPins(spi.busID, config) { + // Use IO MUX direct connection - better signal quality and performance + // Configure pins using IO MUX direct connection (SPI function) + if config.SCK != NoPin { + config.SCK.configure(PinConfig{Mode: PinOutput}, SPI_IOMUX_FUNC) + } + if config.SDO != NoPin { + config.SDO.configure(PinConfig{Mode: PinOutput}, SPI_IOMUX_FUNC) + } + if config.SDI != NoPin { + config.SDI.configure(PinConfig{Mode: PinInput}, SPI_IOMUX_FUNC) + } + if config.CS != NoPin { + config.CS.configure(PinConfig{Mode: PinOutput}, SPI_IOMUX_FUNC) + } + } else { + // Use GPIO Matrix routing - more flexible but slightly slower + // Configure SDI (MISO) pin + if config.SDI != NoPin { + config.SDI.Configure(PinConfig{Mode: PinInput}) + inFunc(misoInIdx).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(config.SDI)) + } + + // Configure SDO (MOSI) pin + if config.SDO != NoPin { + config.SDO.Configure(PinConfig{Mode: PinOutput}) + config.SDO.outFunc().Set(mosiOutIdx) + } + + // Configure SCK (Clock) pin + if config.SCK != NoPin { + config.SCK.Configure(PinConfig{Mode: PinOutput}) + config.SCK.outFunc().Set(sckOutIdx) + } + + // Configure CS (Chip Select) pin + if config.CS != NoPin { + config.CS.Configure(PinConfig{Mode: PinOutput}) + config.CS.outFunc().Set(csOutIdx) + } + } + + // Enable peripheral clock and reset + // Without bootloader, we need to be more explicit about clock initialization + switch spi.busID { + case 2: // Hardware SPI2 (FSPI) + esp.SYSTEM.SetPERIP_CLK_EN0_SPI2_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_SPI2_RST(1) + esp.SYSTEM.SetPERIP_RST_EN0_SPI2_RST(0) + case 3: // Hardware SPI3 (HSPI) + esp.SYSTEM.SetPERIP_CLK_EN0_SPI3_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_SPI3_RST(1) + esp.SYSTEM.SetPERIP_RST_EN0_SPI3_RST(0) + } + + // Get bus handle - both SPI2 and SPI3 use SPI2_Type + bus, ok := spi.Bus.(*esp.SPI2_Type) + if !ok { + return ErrInvalidSPIBus + } + + // Reset timing: cs_setup_time = 0, cs_hold_time = 0 + bus.USER1.Set(0) + + // Use all 64 bytes of the buffer + bus.SetUSER_USR_MISO_HIGHPART(0) + bus.SetUSER_USR_MOSI_HIGHPART(0) + + // Disable unneeded interrupts and clear all USER bits first + bus.SLAVE.Set(0) + bus.USER.Set(0) + + // Clear other important registers like ESP32-C3 + bus.MISC.Set(0) + bus.CTRL.Set(0) + bus.CLOCK.Set(0) + + // Clear data buffers like ESP32-C3 + bus.W0.Set(0) + bus.W1.Set(0) + bus.W2.Set(0) + bus.W3.Set(0) + + // Configure master clock gate - CRITICAL: need CLK_EN bit! + bus.SetCLK_GATE_CLK_EN(1) // Enable basic SPI clock (bit 0) + bus.SetCLK_GATE_MST_CLK_ACTIVE(1) // Enable master clock (bit 1) + bus.SetCLK_GATE_MST_CLK_SEL(1) // Select master clock (bit 2) + + // Configure DMA following ESP-IDF HAL + // Reset DMA configuration + bus.DMA_CONF.Set(0) + // Set DMA segment transaction clear enable bits + bus.SetDMA_CONF_SLV_TX_SEG_TRANS_CLR_EN(1) + bus.SetDMA_CONF_SLV_RX_SEG_TRANS_CLR_EN(1) + // dma_seg_trans_en = 0 (already 0 from DMA_CONF.Set(0)) + + // Configure master mode + bus.SetUSER_USR_MOSI(1) // Enable MOSI + bus.SetUSER_USR_MISO(1) // Enable MISO + bus.SetUSER_DOUTDIN(1) // Full-duplex mode + bus.SetCTRL_WR_BIT_ORDER(0) // MSB first + bus.SetCTRL_RD_BIT_ORDER(0) // MSB first + + // CRITICAL: Enable clock output (from working test) + bus.SetMISC_CK_DIS(0) // Enable CLK output - THIS IS KEY! + + // Configure SPI mode (CPOL/CPHA) following ESP-IDF HAL + switch config.Mode { + case SPI_MODE0: + // CPOL=0, CPHA=0 (default) + case SPI_MODE1: + bus.SetUSER_CK_OUT_EDGE(1) // CPHA=1 + case SPI_MODE2: + bus.SetMISC_CK_IDLE_EDGE(1) // CPOL=1 + bus.SetUSER_CK_OUT_EDGE(1) // CPHA=1 + case SPI_MODE3: + bus.SetMISC_CK_IDLE_EDGE(1) // CPOL=1 + } + + // Configure SPI bus clock using ESP32-C3 algorithm for better accuracy + bus.CLOCK.Set(freqToClockDiv(config.Frequency)) + + return nil +} + +// Transfer writes/reads a single byte using the SPI interface. +// Implementation following ESP-IDF HAL spi_ll_user_start with proper USER register setup +func (spi *SPI) Transfer(w byte) (byte, error) { + // Both SPI2 and SPI3 use SPI2_Type + bus, ok := spi.Bus.(*esp.SPI2_Type) + if !ok { + return 0, errors.New("invalid SPI bus type") + } + + // Set transfer length (8 bits = 7 in register) + bus.SetMS_DLEN_MS_DATA_BITLEN(7) + + // Clear any pending interrupt flags BEFORE starting transaction + bus.SetDMA_INT_CLR_TRANS_DONE_INT_CLR(1) + + // Write data to buffer (use W0 register) + bus.W0.Set(uint32(w)) + + // CRITICAL: Apply configuration before transmission (like ESP-IDF spi_ll_apply_config) + bus.SetCMD_UPDATE(1) + for bus.GetCMD_UPDATE() != 0 { + // Wait for config to be applied + } + + // Start transaction following ESP-IDF HAL spi_ll_user_start + bus.SetCMD_USR(1) + + // Wait for completion using CMD_USR flag (like ESP32-C3 approach) + // Hardware clears CMD_USR when transaction is complete + timeout := 100000 + for bus.GetCMD_USR() != 0 && timeout > 0 { + timeout-- + // Wait for CMD_USR to be cleared by hardware + } + + if timeout == 0 { + return 0, errors.New("SPI transfer timeout") + } + + // Read received data from W0 register + result := byte(bus.W0.Get() & 0xFF) + return result, nil +} + +// Tx handles read/write operation for SPI interface. Since SPI is a synchronous write/read +// interface, there must always be the same number of bytes written as bytes read. +// This is accomplished by sending zero bits if r is bigger than w or discarding +// the incoming data if w is bigger than r. +// Optimized implementation ported from ESP32-C3 for better performance. +func (spi *SPI) Tx(w, r []byte) error { + toTransfer := len(w) + if len(r) > toTransfer { + toTransfer = len(r) + } + + // Get bus handle - both SPI2 and SPI3 use SPI2_Type + bus, ok := spi.Bus.(*esp.SPI2_Type) + if !ok { + return ErrInvalidSPIBus + } + + for toTransfer > 0 { + // Chunk 64 bytes at a time. + chunkSize := toTransfer + if chunkSize > 64 { + chunkSize = 64 + } + + // Fill tx buffer. + transferWords := (*[16]volatile.Register32)(unsafe.Add(unsafe.Pointer(&bus.W0), 0)) + if len(w) >= 64 { + // We can fill the entire 64-byte transfer buffer with data. + // This loop is slightly faster than the loop below. + for i := 0; i < 16; i++ { + word := uint32(w[i*4]) | uint32(w[i*4+1])<<8 | uint32(w[i*4+2])<<16 | uint32(w[i*4+3])<<24 + transferWords[i].Set(word) + } + } else { + // We can't fill the entire transfer buffer, so we need to be a bit + // more careful. + // Note that parts of the transfer buffer that aren't used still + // need to be set to zero, otherwise we might be transferring + // garbage from a previous transmission if w is smaller than r. + for i := 0; i < 16; i++ { + var word uint32 + if i*4+3 < len(w) { + word |= uint32(w[i*4+3]) << 24 + } + if i*4+2 < len(w) { + word |= uint32(w[i*4+2]) << 16 + } + if i*4+1 < len(w) { + word |= uint32(w[i*4+1]) << 8 + } + if i*4+0 < len(w) { + word |= uint32(w[i*4+0]) << 0 + } + transferWords[i].Set(word) + } + } + + // Do the transfer. + bus.SetMS_DLEN_MS_DATA_BITLEN(uint32(chunkSize)*8 - 1) + + bus.SetCMD_UPDATE(1) + for bus.GetCMD_UPDATE() != 0 { + } + + bus.SetCMD_USR(1) + for bus.GetCMD_USR() != 0 { + } + + // Read rx buffer. + rxSize := chunkSize + if rxSize > len(r) { + rxSize = len(r) + } + for i := 0; i < rxSize; i++ { + r[i] = byte(transferWords[i/4].Get() >> ((i % 4) * 8)) + } + + // Cut off some part of the output buffer so the next iteration we will + // only send the remaining bytes. + if len(w) < chunkSize { + w = nil + } else { + w = w[chunkSize:] + } + if len(r) < chunkSize { + r = nil + } else { + r = r[chunkSize:] + } + toTransfer -= chunkSize + } + + return nil +} + +// Compute the SPI bus frequency from the APB clock frequency. +// Note: APB clock is always 80MHz on ESP32-S3, independent of CPU frequency. +// Ported from ESP32-C3 implementation for better accuracy. +func freqToClockDiv(hz uint32) uint32 { + // Use APB clock frequency (80MHz), not CPU frequency! + // SPI peripheral is connected to APB bus which stays at 80MHz + const apbFreq = pplClockFreq // 80MHz + + if hz >= apbFreq { // maximum frequency + return 1 << 31 + } + if hz < (apbFreq / (16 * 64)) { // minimum frequency + return 15<<18 | 63<<12 | 31<<6 | 63 // pre=15, n=63 + } + + // iterate looking for an exact match + // or iterate all 16 prescaler options + // looking for the smallest error + var bestPre, bestN, bestErr uint32 + bestN = 1 + bestErr = 0xffffffff + q := uint32(float32(apbFreq)/float32(hz) + float32(0.5)) + for p := uint32(0); p < 16; p++ { + n := q/(p+1) - 1 + if n < 1 { // prescaler became too large, stop enum + break + } + if n > 63 { // prescaler too small, skip to next + continue + } + + freq := apbFreq / ((p + 1) * (n + 1)) + if freq == hz { // exact match + return p<<18 | n<<12 | (n/2)<<6 | n + } + + var err uint32 + if freq < hz { + err = hz - freq + } else { + err = freq - hz + } + if err < bestErr { + bestErr = err + bestPre = p + bestN = n + } + } + + return bestPre<<18 | bestN<<12 | (bestN/2)<<6 | bestN +} + +// isDefaultSPIPins checks if the given pins match the default SPI pin configuration +// that supports IO MUX direct connection for better performance +func isDefaultSPIPins(busID uint8, config SPIConfig) bool { + switch busID { + case 2: // SPI2 (FSPI) + return config.SCK == SPI1_SCK_PIN && + config.SDO == SPI1_MOSI_PIN && + config.SDI == SPI1_MISO_PIN && + (config.CS == SPI1_CS_PIN || config.CS == NoPin) + case 3: // SPI3 (HSPI) + return config.SCK == SPI2_SCK_PIN && + config.SDO == SPI2_MOSI_PIN && + config.SDI == SPI2_MISO_PIN && + (config.CS == SPI2_CS_PIN || config.CS == NoPin) + default: + return false + } +} diff --git a/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index 35cd26da85..5a612d6df8 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -49,8 +49,22 @@ func main() { // Change CPU frequency from 80MHz to 240MHz by setting SYSTEM_PLL_FREQ_SEL to // 1 and SYSTEM_CPUPERIOD_SEL to 2 (see table "CPU Clock Frequency" in the // reference manual). + // We do this gradually to allow PLL and system to stabilize. esp.SYSTEM.SetCPU_PER_CONF_PLL_FREQ_SEL(1) + + // First switch to 160MHz (intermediate step) + esp.SYSTEM.SetCPU_PER_CONF_CPUPERIOD_SEL(1) + // Small delay to let PLL stabilize at 160MHz + for i := 0; i < 1000; i++ { + _ = esp.SYSTEM.CPU_PER_CONF.Get() + } + + // Now switch to 240MHz esp.SYSTEM.SetCPU_PER_CONF_CPUPERIOD_SEL(2) + // Small delay to let PLL stabilize at 240MHz + for i := 0; i < 1000; i++ { + _ = esp.SYSTEM.CPU_PER_CONF.Get() + } // Clear bss. Repeat many times while we wait for cpu/clock to stabilize for x := 0; x < 30; x++ { diff --git a/targets/esp32s3-wroom1.json b/targets/esp32s3-wroom1.json new file mode 100644 index 0000000000..56f8d57d21 --- /dev/null +++ b/targets/esp32s3-wroom1.json @@ -0,0 +1,4 @@ +{ + "inherits": ["esp32s3"], + "build-tags": ["esp32s3_wroom1"] +} \ No newline at end of file From 934d5f41bf5830bb01402f06c82d679c280aa562 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 15 Feb 2026 19:24:16 +0100 Subject: [PATCH 16/81] fix: init heap before random number seed on wasm platforms This changes the order for initialization of the random number seed generation on wasm platforms until after the heap has been initialized. Should fix #5198 Signed-off-by: deadprogram --- src/runtime/runtime_wasmentry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/runtime_wasmentry.go b/src/runtime/runtime_wasmentry.go index 005b58641e..59cacb3b04 100644 --- a/src/runtime/runtime_wasmentry.go +++ b/src/runtime/runtime_wasmentry.go @@ -34,8 +34,8 @@ func wasmEntryReactor() { // Initialize the heap. heapStart = uintptr(unsafe.Pointer(&heapStartSymbol)) heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) - initRand() initHeap() + initRand() if hasScheduler { // A package initializer might do funky stuff like start a goroutine and From b6b723aeec5b972c4018efb2808f4d66f5bd0b6a Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 16 Feb 2026 09:49:12 +0100 Subject: [PATCH 17/81] targets: correct name/tag use for esp32s3-wroom1 board Signed-off-by: deadprogram --- .../{machine_esp32s3-wroom1.go => board_esp32s3-wroom1.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/machine/{machine_esp32s3-wroom1.go => board_esp32s3-wroom1.go} (89%) diff --git a/src/machine/machine_esp32s3-wroom1.go b/src/machine/board_esp32s3-wroom1.go similarity index 89% rename from src/machine/machine_esp32s3-wroom1.go rename to src/machine/board_esp32s3-wroom1.go index 8ecfb75098..649745398d 100644 --- a/src/machine/machine_esp32s3-wroom1.go +++ b/src/machine/board_esp32s3-wroom1.go @@ -1,4 +1,4 @@ -//go:build esp32s3 && !xiao_esp32s3 +//go:build esp32s3_wroom1 package machine From 24f965425d616310a3d867b30d8ad35f9d1c7bd6 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 16 Feb 2026 09:53:20 +0100 Subject: [PATCH 18/81] make: remove machine without board from smoketest Signed-off-by: deadprogram --- GNUmakefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index de77ac41f0..368fedc910 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -927,8 +927,6 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/mcp3008 @$(MD5SUM) test.bin - $(TINYGO) build -size short -o test.bin -target=esp32s3 examples/mcp3008 - @$(MD5SUM) test.bin endif $(TINYGO) build -size short -o test.bin -target=esp-c3-32s-kit examples/blinky1 @$(MD5SUM) test.bin From 9bbad6700b3b17ad4fe9709f1d4138cd17111b48 Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Sun, 15 Feb 2026 21:11:41 +0200 Subject: [PATCH 19/81] esp32s3-usbserial: added usbserial printing --- src/machine/machine_esp32s3.go | 88 +++++++++++++++++++++++++++++++++- src/runtime/runtime_esp32s3.go | 4 ++ targets/esp32s3.json | 2 +- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_esp32s3.go b/src/machine/machine_esp32s3.go index 65261bd3c8..9c2bd90017 100644 --- a/src/machine/machine_esp32s3.go +++ b/src/machine/machine_esp32s3.go @@ -309,4 +309,90 @@ func (uart *UART) writeByte(b byte) error { func (uart *UART) flush() {} -// TODO: SPI +// USB Serial/JTAG Controller for ESP32-S3 +type USB_DEVICE struct { + Bus *esp.USB_DEVICE_Type +} + +var ( + _USBCDC = &USB_DEVICE{ + Bus: esp.USB_DEVICE, + } + + USBCDC Serialer = _USBCDC +) + +func initUSB() { + USBCDC.Configure(UARTConfig{BaudRate: 115200}) +} + +type Serialer interface { + WriteByte(c byte) error + Write(data []byte) (n int, err error) + Configure(config UARTConfig) error + Buffered() int + ReadByte() (byte, error) + DTR() bool + RTS() bool +} + +func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { + return nil +} + +func (usbdev *USB_DEVICE) WriteByte(c byte) error { + // Host is connected - wait for TX FIFO space with reasonable timeout + timeout := 10000 // Generous timeout for connected host + for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 && timeout > 0 { + timeout-- + } + + // Even with host connected, don't hang forever + if timeout == 0 { + return nil + } + + // Write byte to USB Serial/JTAG endpoint + usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) + + // Trigger transmission + usbdev.Bus.SetEP1_CONF_WR_DONE(1) + + return nil +} + +func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { + for _, c := range data { + err = usbdev.WriteByte(c) + if err != nil { + return n, err + } + n++ + } + return n, nil +} + +func (usbdev *USB_DEVICE) ReadByte() (byte, error) { + // TODO: Implement USB Serial/JTAG input reading + return 0, errors.New("ReadByte not implemented") +} + +func (usbdev *USB_DEVICE) Buffered() int { + // Return number of bytes available to read + return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) +} + +func (usbdev *USB_DEVICE) DTR() bool { + // Data Terminal Ready - not applicable for USB Serial/JTAG + return false +} + +func (usbdev *USB_DEVICE) RTS() bool { + // Request To Send - not applicable for USB Serial/JTAG + return false +} + +func (usbdev *USB_DEVICE) flush() { + // Force transmission of any buffered data + usbdev.Bus.SetEP1_CONF_WR_DONE(1) +} diff --git a/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index 5a612d6df8..ab0d85d733 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -4,6 +4,7 @@ package runtime import ( "device/esp" + "machine" ) // This is the function called on startup after the flash (IROM/DROM) is @@ -71,6 +72,9 @@ func main() { clearbss() } + // Initialize UART. + machine.InitSerial() + // Initialize main system timer used for time.Now. initTimer() diff --git a/targets/esp32s3.json b/targets/esp32s3.json index f245b82ab8..051cc85420 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -4,7 +4,7 @@ "features": "+atomctl,+bool,+clamps,+coprocessor,+debug,+density,+div32,+esp32s3,+exception,+fp,+highpriinterrupts,+interrupt,+loop,+mac16,+memctl,+minmax,+miscsr,+mul32,+mul32high,+nsa,+prid,+regprotect,+rvector,+s32c1i,+sext,+threadptr,+timerint,+windowed", "build-tags": ["esp32s3", "esp"], "scheduler": "tasks", - "serial": "uart", + "serial": "usb", "linker": "ld.lld", "default-stack-size": 2048, "rtlib": "compiler-rt", From c1cddffbe96f9f7d32ae256b85a8491917114bb8 Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Mon, 16 Feb 2026 21:59:02 +0200 Subject: [PATCH 20/81] esp32s3-usbserial: split usb --- src/machine/machine_esp32_usb.go | 45 ++++++++++++++ src/machine/machine_esp32c3.go | 96 ------------------------------ src/machine/machine_esp32c3_usb.go | 52 ++++++++++++++++ src/machine/machine_esp32s3.go | 88 --------------------------- src/machine/machine_esp32s3_usb.go | 41 +++++++++++++ 5 files changed, 138 insertions(+), 184 deletions(-) create mode 100644 src/machine/machine_esp32_usb.go create mode 100644 src/machine/machine_esp32c3_usb.go create mode 100644 src/machine/machine_esp32s3_usb.go diff --git a/src/machine/machine_esp32_usb.go b/src/machine/machine_esp32_usb.go new file mode 100644 index 0000000000..6f40a48fc2 --- /dev/null +++ b/src/machine/machine_esp32_usb.go @@ -0,0 +1,45 @@ +//go:build esp32s3 || esp32c3 + +package machine + +import ( + "device/esp" +) + +type USB_DEVICE struct { + Bus *esp.USB_DEVICE_Type +} + +var ( + _USBCDC = &USB_DEVICE{ + Bus: esp.USB_DEVICE, + } + + USBCDC Serialer = _USBCDC +) + +type Serialer interface { + WriteByte(c byte) error + Write(data []byte) (n int, err error) + Configure(config UARTConfig) error + Buffered() int + ReadByte() (byte, error) + DTR() bool + RTS() bool +} + +func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { + return nil +} + +func (usbdev *USB_DEVICE) Buffered() int { + return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) +} + +func (usbdev *USB_DEVICE) DTR() bool { + return false +} + +func (usbdev *USB_DEVICE) RTS() bool { + return false +} diff --git a/src/machine/machine_esp32c3.go b/src/machine/machine_esp32c3.go index eb2a18b5ee..f413a8c143 100644 --- a/src/machine/machine_esp32c3.go +++ b/src/machine/machine_esp32c3.go @@ -509,102 +509,6 @@ func (uart *UART) writeByte(b byte) error { func (uart *UART) flush() {} -type Serialer interface { - WriteByte(c byte) error - Write(data []byte) (n int, err error) - Configure(config UARTConfig) error - Buffered() int - ReadByte() (byte, error) - DTR() bool - RTS() bool -} - -func initUSB() { - // nothing to do here -} - -// USB Serial/JTAG Controller -// See esp32-c3_technical_reference_manual_en.pdf -// pg. 736 -type USB_DEVICE struct { - Bus *esp.USB_DEVICE_Type -} - -var ( - _USBCDC = &USB_DEVICE{ - Bus: esp.USB_DEVICE, - } - - USBCDC Serialer = _USBCDC -) - -var ( - errUSBWrongSize = errors.New("USB: invalid write size") - errUSBCouldNotWriteAllData = errors.New("USB: could not write all data") - errUSBBufferEmpty = errors.New("USB: read buffer empty") -) - -func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { - return nil -} - -func (usbdev *USB_DEVICE) WriteByte(c byte) error { - if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { - return errUSBCouldNotWriteAllData - } - - usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) - usbdev.flush() - - return nil -} - -func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { - if len(data) == 0 || len(data) > 64 { - return 0, errUSBWrongSize - } - - for i, c := range data { - if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { - if i > 0 { - usbdev.flush() - } - - return i, errUSBCouldNotWriteAllData - } - usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) - } - - usbdev.flush() - return len(data), nil -} - -func (usbdev *USB_DEVICE) Buffered() int { - return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) -} - -func (usbdev *USB_DEVICE) ReadByte() (byte, error) { - if usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { - return byte(usbdev.Bus.GetEP1_RDWR_BYTE()), nil - } - - return 0, nil -} - -func (usbdev *USB_DEVICE) DTR() bool { - return false -} - -func (usbdev *USB_DEVICE) RTS() bool { - return false -} - -func (usbdev *USB_DEVICE) flush() { - usbdev.Bus.SetEP1_CONF_WR_DONE(1) - for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { - } -} - // GetRNG returns 32-bit random numbers using the ESP32-C3 true random number generator, // Random numbers are generated based on the thermal noise in the system and the // asynchronous clock mismatch. diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32c3_usb.go new file mode 100644 index 0000000000..7309673129 --- /dev/null +++ b/src/machine/machine_esp32c3_usb.go @@ -0,0 +1,52 @@ +//go:build esp32c3 + +package machine + +import "errors" + +var ( + errUSBWrongSize = errors.New("USB: invalid write size") + errUSBCouldNotWriteAllData = errors.New("USB: could not write all data") + errUSBBufferEmpty = errors.New("USB: read buffer empty") +) + +func initUSB() {} + +func (usbdev *USB_DEVICE) WriteByte(c byte) error { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return errUSBCouldNotWriteAllData + } + usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) + usbdev.flush() + return nil +} + +func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { + if len(data) == 0 || len(data) > 64 { + return 0, errUSBWrongSize + } + for i, c := range data { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + if i > 0 { + usbdev.flush() + } + return i, errUSBCouldNotWriteAllData + } + usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) + } + usbdev.flush() + return len(data), nil +} + +func (usbdev *USB_DEVICE) ReadByte() (byte, error) { + if usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { + return byte(usbdev.Bus.GetEP1_RDWR_BYTE()), nil + } + return 0, nil +} + +func (usbdev *USB_DEVICE) flush() { + usbdev.Bus.SetEP1_CONF_WR_DONE(1) + for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + } +} diff --git a/src/machine/machine_esp32s3.go b/src/machine/machine_esp32s3.go index 9c2bd90017..f3ac520d2b 100644 --- a/src/machine/machine_esp32s3.go +++ b/src/machine/machine_esp32s3.go @@ -308,91 +308,3 @@ func (uart *UART) writeByte(b byte) error { } func (uart *UART) flush() {} - -// USB Serial/JTAG Controller for ESP32-S3 -type USB_DEVICE struct { - Bus *esp.USB_DEVICE_Type -} - -var ( - _USBCDC = &USB_DEVICE{ - Bus: esp.USB_DEVICE, - } - - USBCDC Serialer = _USBCDC -) - -func initUSB() { - USBCDC.Configure(UARTConfig{BaudRate: 115200}) -} - -type Serialer interface { - WriteByte(c byte) error - Write(data []byte) (n int, err error) - Configure(config UARTConfig) error - Buffered() int - ReadByte() (byte, error) - DTR() bool - RTS() bool -} - -func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { - return nil -} - -func (usbdev *USB_DEVICE) WriteByte(c byte) error { - // Host is connected - wait for TX FIFO space with reasonable timeout - timeout := 10000 // Generous timeout for connected host - for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 && timeout > 0 { - timeout-- - } - - // Even with host connected, don't hang forever - if timeout == 0 { - return nil - } - - // Write byte to USB Serial/JTAG endpoint - usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) - - // Trigger transmission - usbdev.Bus.SetEP1_CONF_WR_DONE(1) - - return nil -} - -func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { - for _, c := range data { - err = usbdev.WriteByte(c) - if err != nil { - return n, err - } - n++ - } - return n, nil -} - -func (usbdev *USB_DEVICE) ReadByte() (byte, error) { - // TODO: Implement USB Serial/JTAG input reading - return 0, errors.New("ReadByte not implemented") -} - -func (usbdev *USB_DEVICE) Buffered() int { - // Return number of bytes available to read - return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) -} - -func (usbdev *USB_DEVICE) DTR() bool { - // Data Terminal Ready - not applicable for USB Serial/JTAG - return false -} - -func (usbdev *USB_DEVICE) RTS() bool { - // Request To Send - not applicable for USB Serial/JTAG - return false -} - -func (usbdev *USB_DEVICE) flush() { - // Force transmission of any buffered data - usbdev.Bus.SetEP1_CONF_WR_DONE(1) -} diff --git a/src/machine/machine_esp32s3_usb.go b/src/machine/machine_esp32s3_usb.go new file mode 100644 index 0000000000..c9bfbe7ed2 --- /dev/null +++ b/src/machine/machine_esp32s3_usb.go @@ -0,0 +1,41 @@ +//go:build esp32s3 + +package machine + +import "errors" + +func initUSB() { + USBCDC.Configure(UARTConfig{BaudRate: 115200}) +} + +func (usbdev *USB_DEVICE) WriteByte(c byte) error { + timeout := 10000 + for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 && timeout > 0 { + timeout-- + } + if timeout == 0 { + return nil + } + usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) + usbdev.Bus.SetEP1_CONF_WR_DONE(1) + return nil +} + +func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { + for _, c := range data { + err = usbdev.WriteByte(c) + if err != nil { + return n, err + } + n++ + } + return n, nil +} + +func (usbdev *USB_DEVICE) ReadByte() (byte, error) { + return 0, errors.New("ReadByte not implemented") +} + +func (usbdev *USB_DEVICE) flush() { + usbdev.Bus.SetEP1_CONF_WR_DONE(1) +} From 44ca224056d13a11e7c676660bda2ccb87e399ac Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Tue, 17 Feb 2026 10:44:37 +0200 Subject: [PATCH 21/81] esp32s3-usbserial: common usbserial for both esp32c3 & esp32s3 --- src/machine/machine_esp32_usb.go | 45 ---------------- src/machine/machine_esp32s3_usb.go | 41 -------------- ..._esp32c3_usb.go => machine_esp32xx_usb.go} | 54 ++++++++++++++++++- 3 files changed, 52 insertions(+), 88 deletions(-) delete mode 100644 src/machine/machine_esp32_usb.go delete mode 100644 src/machine/machine_esp32s3_usb.go rename src/machine/{machine_esp32c3_usb.go => machine_esp32xx_usb.go} (59%) diff --git a/src/machine/machine_esp32_usb.go b/src/machine/machine_esp32_usb.go deleted file mode 100644 index 6f40a48fc2..0000000000 --- a/src/machine/machine_esp32_usb.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build esp32s3 || esp32c3 - -package machine - -import ( - "device/esp" -) - -type USB_DEVICE struct { - Bus *esp.USB_DEVICE_Type -} - -var ( - _USBCDC = &USB_DEVICE{ - Bus: esp.USB_DEVICE, - } - - USBCDC Serialer = _USBCDC -) - -type Serialer interface { - WriteByte(c byte) error - Write(data []byte) (n int, err error) - Configure(config UARTConfig) error - Buffered() int - ReadByte() (byte, error) - DTR() bool - RTS() bool -} - -func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { - return nil -} - -func (usbdev *USB_DEVICE) Buffered() int { - return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) -} - -func (usbdev *USB_DEVICE) DTR() bool { - return false -} - -func (usbdev *USB_DEVICE) RTS() bool { - return false -} diff --git a/src/machine/machine_esp32s3_usb.go b/src/machine/machine_esp32s3_usb.go deleted file mode 100644 index c9bfbe7ed2..0000000000 --- a/src/machine/machine_esp32s3_usb.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build esp32s3 - -package machine - -import "errors" - -func initUSB() { - USBCDC.Configure(UARTConfig{BaudRate: 115200}) -} - -func (usbdev *USB_DEVICE) WriteByte(c byte) error { - timeout := 10000 - for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 && timeout > 0 { - timeout-- - } - if timeout == 0 { - return nil - } - usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) - usbdev.Bus.SetEP1_CONF_WR_DONE(1) - return nil -} - -func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { - for _, c := range data { - err = usbdev.WriteByte(c) - if err != nil { - return n, err - } - n++ - } - return n, nil -} - -func (usbdev *USB_DEVICE) ReadByte() (byte, error) { - return 0, errors.New("ReadByte not implemented") -} - -func (usbdev *USB_DEVICE) flush() { - usbdev.Bus.SetEP1_CONF_WR_DONE(1) -} diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32xx_usb.go similarity index 59% rename from src/machine/machine_esp32c3_usb.go rename to src/machine/machine_esp32xx_usb.go index 7309673129..bdd1281a81 100644 --- a/src/machine/machine_esp32c3_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -1,8 +1,26 @@ -//go:build esp32c3 +//go:build esp32s3 || esp32c3 package machine -import "errors" +import ( + "device/esp" + "errors" +) + +// USB Serial/JTAG Controller +// See esp32-c3_technical_reference_manual_en.pdf +// pg. 736 +type USB_DEVICE struct { + Bus *esp.USB_DEVICE_Type +} + +var ( + _USBCDC = &USB_DEVICE{ + Bus: esp.USB_DEVICE, + } + + USBCDC Serialer = _USBCDC +) var ( errUSBWrongSize = errors.New("USB: invalid write size") @@ -10,14 +28,30 @@ var ( errUSBBufferEmpty = errors.New("USB: read buffer empty") ) +type Serialer interface { + WriteByte(c byte) error + Write(data []byte) (n int, err error) + Configure(config UARTConfig) error + Buffered() int + ReadByte() (byte, error) + DTR() bool + RTS() bool +} + func initUSB() {} +func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { + return nil +} + func (usbdev *USB_DEVICE) WriteByte(c byte) error { if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { return errUSBCouldNotWriteAllData } + usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) usbdev.flush() + return nil } @@ -25,26 +59,42 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { if len(data) == 0 || len(data) > 64 { return 0, errUSBWrongSize } + for i, c := range data { if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { if i > 0 { usbdev.flush() } + return i, errUSBCouldNotWriteAllData } usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) } + usbdev.flush() return len(data), nil } +func (usbdev *USB_DEVICE) Buffered() int { + return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) +} + func (usbdev *USB_DEVICE) ReadByte() (byte, error) { if usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { return byte(usbdev.Bus.GetEP1_RDWR_BYTE()), nil } + return 0, nil } +func (usbdev *USB_DEVICE) DTR() bool { + return false +} + +func (usbdev *USB_DEVICE) RTS() bool { + return false +} + func (usbdev *USB_DEVICE) flush() { usbdev.Bus.SetEP1_CONF_WR_DONE(1) for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { From 610dd19c40830b32c739792d16ecf88e76b136cb Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Tue, 17 Feb 2026 19:31:48 +0200 Subject: [PATCH 22/81] esp32s3-usbserial: move InitSerial to init method --- src/runtime/runtime_esp32c3.go | 9 +++++---- src/runtime/runtime_esp32s3.go | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/runtime/runtime_esp32c3.go b/src/runtime/runtime_esp32c3.go index 013c939246..f85f7dec78 100644 --- a/src/runtime/runtime_esp32c3.go +++ b/src/runtime/runtime_esp32c3.go @@ -54,10 +54,6 @@ func main() { // Configure interrupt handler interruptInit() - // Initialize UART. - machine.USBCDC.Configure(machine.UARTConfig{}) - machine.InitSerial() - // Initialize main system timer used for time.Now. initTimer() @@ -68,6 +64,11 @@ func main() { exit(0) } +func init() { + // Initialize UART. + machine.InitSerial() +} + func abort() { // lock up forever for { diff --git a/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index ab0d85d733..b469ddf9e9 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -72,9 +72,6 @@ func main() { clearbss() } - // Initialize UART. - machine.InitSerial() - // Initialize main system timer used for time.Now. initTimer() @@ -85,6 +82,11 @@ func main() { exit(0) } +func init() { + // Initialize UART. + machine.InitSerial() +} + func abort() { // lock up forever print("abort called\n") From 5c37d1ba61fa8eaedeabfe2866d78c5d7dbbffdc Mon Sep 17 00:00:00 2001 From: Nia Waldvogel Date: Mon, 9 Feb 2026 14:21:44 -0500 Subject: [PATCH 23/81] runtime: implement fminimum/fmaximum The compiler may generate calls to fminimum/fmaximum on some platforms. Neither of the libm implementations we statically link against have these functions yet. Implement them ourselves. --- src/runtime/float.go | 127 +++++++++++++++++++++ src/runtime/float_test.go | 227 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 src/runtime/float_test.go diff --git a/src/runtime/float.go b/src/runtime/float.go index c80c8b7abf..b5fee4c5c4 100644 --- a/src/runtime/float.go +++ b/src/runtime/float.go @@ -52,3 +52,130 @@ func float64bits(f float64) uint64 { func float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) } + +// The fmimimum/fmaximum are missing from most libm implementations. +// Just define them ourselves. + +//export fminimum +func fminimum(x, y float64) float64 { + return minimumFloat64(x, y) +} + +//export fminimumf +func fminimumf(x, y float32) float32 { + return minimumFloat32(x, y) +} + +//export fmaximum +func fmaximum(x, y float64) float64 { + return maximumFloat64(x, y) +} + +//export fmaximumf +func fmaximumf(x, y float32) float32 { + return maximumFloat32(x, y) +} + +// Create seperate copies of the function that are not exported. +// This is necessary so that LLVM does not recognize them as builtins. +// If tests called the builtins, LLVM would just override them on most platforms. + +func minimumFloat32(x, y float32) float32 { + return minimumFloat[float32, int32](x, y, minPosNaN32, magMask32) +} + +func minimumFloat64(x, y float64) float64 { + return minimumFloat[float64, int64](x, y, minPosNaN64, magMask64) +} + +func maximumFloat32(x, y float32) float32 { + return maximumFloat[float32, int32](x, y, minPosNaN32, magMask32) +} + +func maximumFloat64(x, y float64) float64 { + return maximumFloat[float64, int64](x, y, minPosNaN64, magMask64) +} + +// minimumFloat is a generic implementation of the floating-point minimum operation. +// This implementation uses integer operations because this is mainly used for platforms without an FPU. +func minimumFloat[T float, I floatInt](x, y T, minPosNaN, magMask I) T { + xBits := *(*I)(unsafe.Pointer(&x)) + yBits := *(*I)(unsafe.Pointer(&y)) + + // Handle the special case of a positive NaN value. + switch { + case xBits >= minPosNaN: + return x + case yBits >= minPosNaN: + return y + } + + // The exponent-mantissa portion of the float is comparable via unsigned comparison (excluding the NaN case). + // We can turn a float into a signed-comparable value by reversing the comparison order of negative values. + // We can reverse the order by inverting the bits. + // This also ensures that positive zero compares greater than negative zero (as required by the spec). + // Negative NaN values will compare less than any other value, so they require no special handling to propogate. + if xBits < 0 { + xBits ^= magMask + } + if yBits < 0 { + yBits ^= magMask + } + if xBits <= yBits { + return x + } else { + return y + } +} + +// maximumFloat is a generic implementation of the floating-point maximum operation. +// This implementation uses integer operations because this is mainly used for platforms without an FPU. +func maximumFloat[T float, I floatInt](x, y T, minPosNaN, magMask I) T { + xBits := *(*I)(unsafe.Pointer(&x)) + yBits := *(*I)(unsafe.Pointer(&y)) + + // The exponent-mantissa portion of the float is comparable via unsigned comparison (excluding the NaN case). + // We can turn a float into a signed-comparable value by reversing the comparison order of negative values. + // We can reverse the order by inverting the bits. + // This also ensures that positive zero compares greater than negative zero (as required by the spec). + // Positive NaN values will compare greater than any other value, so they require no special handling to propogate. + if xBits < 0 { + xBits ^= magMask + } + if yBits < 0 { + yBits ^= magMask + } + // Handle the special case of a negative NaN value. + maxNegNaN := ^minPosNaN + switch { + case xBits <= maxNegNaN: + return x + case yBits <= maxNegNaN: + return y + } + if xBits >= yBits { + return x + } else { + return y + } +} + +const ( + signPos64 = 63 + exponentPos64 = 52 + minPosNaN64 = ((1 << signPos64) - (1 << exponentPos64)) + 1 + magMask64 = 1< Date: Tue, 17 Feb 2026 10:46:57 -0800 Subject: [PATCH 24/81] export UART0 and pins --- src/machine/board_feather-m0.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/machine/board_feather-m0.go b/src/machine/board_feather-m0.go index f38d8ec889..15ec77d37a 100644 --- a/src/machine/board_feather-m0.go +++ b/src/machine/board_feather-m0.go @@ -43,6 +43,15 @@ const ( USBCDC_DP_PIN = PA25 ) +// UART0 pins +const ( + UART0_TX_PIN = D1 + UART0_RX_PIN = D0 +) + +// UART0 on the Feather M0. +var UART0 = &sercomUSART0 + // UART1 pins const ( UART_TX_PIN = D10 From e0425edb0083165d522de32bb7ab8b415c62a88e Mon Sep 17 00:00:00 2001 From: Dima Date: Sat, 21 Feb 2026 21:23:03 +0200 Subject: [PATCH 25/81] Esp32s3 i2c interface (#5210) * esp32s3-i2c implement interface * esp32s3-i2c: disable m5stamp_c3 * added simple tests without Listen * replace smoke tests * esp32s3-i2c: fix allocation in tx --- GNUmakefile | 6 + src/machine/board_esp32s3-wroom1.go | 3 + src/machine/i2c.go | 2 +- src/machine/machine_esp32c3.go | 15 ++ src/machine/machine_esp32c3_i2c.go | 348 +--------------------------- src/machine/machine_esp32s3.go | 4 + src/machine/machine_esp32s3_i2c.go | 35 +++ src/machine/machine_esp32xx_i2c.go | 336 +++++++++++++++++++++++++++ 8 files changed, 408 insertions(+), 341 deletions(-) create mode 100644 src/machine/machine_esp32s3_i2c.go create mode 100644 src/machine/machine_esp32xx_i2c.go diff --git a/GNUmakefile b/GNUmakefile index 368fedc910..536a3e6c02 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -909,6 +909,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinky1 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinkm + @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=nodemcu examples/blinky1 @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target m5stack-core2 examples/machinetest @@ -923,10 +925,14 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/blinky1 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/blinkm + @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/mcp3008 @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/mcp3008 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/blinkm + @$(MD5SUM) test.bin endif $(TINYGO) build -size short -o test.bin -target=esp-c3-32s-kit examples/blinky1 @$(MD5SUM) test.bin diff --git a/src/machine/board_esp32s3-wroom1.go b/src/machine/board_esp32s3-wroom1.go index 649745398d..20c8883183 100644 --- a/src/machine/board_esp32s3-wroom1.go +++ b/src/machine/board_esp32s3-wroom1.go @@ -3,6 +3,9 @@ package machine const ( + SCL_PIN = GPIO17 + SDA_PIN = GPIO18 + SPI1_SCK_PIN = GPIO12 // SCK SPI1_MOSI_PIN = GPIO11 // SDO (MOSI) SPI1_MISO_PIN = GPIO13 // SDI (MISO) diff --git a/src/machine/i2c.go b/src/machine/i2c.go index 016f1a969b..ff6813856e 100644 --- a/src/machine/i2c.go +++ b/src/machine/i2c.go @@ -1,4 +1,4 @@ -//go:build !baremetal || atmega || nrf || sam || stm32 || fe310 || k210 || rp2040 || rp2350 || mimxrt1062 || (esp32c3 && !m5stamp_c3) || esp32 +//go:build !baremetal || atmega || nrf || sam || stm32 || fe310 || k210 || rp2040 || rp2350 || mimxrt1062 || (esp32c3 && !m5stamp_c3) || esp32 || esp32s3 package machine diff --git a/src/machine/machine_esp32c3.go b/src/machine/machine_esp32c3.go index f413a8c143..7214acbb22 100644 --- a/src/machine/machine_esp32c3.go +++ b/src/machine/machine_esp32c3.go @@ -105,6 +105,21 @@ func (p Pin) Configure(config PinConfig) { } } +// configure is the same as Configure, but allows setting a specific GPIO matrix signal. +func (p Pin) configure(config PinConfig, signal uint32) { + p.Configure(config) + if signal == 256 { + return + } + if config.Mode == PinOutput { + p.outFunc().Set(signal) + } else { + inFunc(signal).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(p)) + } +} + +func initI2CExt1Clock() {} + // outFunc returns the FUNCx_OUT_SEL_CFG register used for configuring the // output function selection. func (p Pin) outFunc() *volatile.Register32 { diff --git a/src/machine/machine_esp32c3_i2c.go b/src/machine/machine_esp32c3_i2c.go index dd334b0db7..222c107f4d 100644 --- a/src/machine/machine_esp32c3_i2c.go +++ b/src/machine/machine_esp32c3_i2c.go @@ -4,350 +4,18 @@ package machine import ( "device/esp" - "runtime/volatile" - "unsafe" ) -var ( - I2C0 = &I2C{} -) - -type I2C struct{} - -// I2CConfig is used to store config info for I2C. -type I2CConfig struct { - Frequency uint32 // in Hz - SCL Pin - SDA Pin -} - const ( - clkXTAL = 0 - clkFOSC = 1 - clkXTALFrequency = uint32(40e6) - clkFOSCFrequency = uint32(17.5e6) - i2cClkSourceFrequency = clkXTALFrequency - i2cClkSource = clkXTAL + I2CEXT0_SCL_OUT_IDX = 53 + I2CEXT0_SDA_OUT_IDX = 54 ) -func (i2c *I2C) Configure(config I2CConfig) error { - if config.Frequency == 0 { - config.Frequency = 400 * KHz - } - if config.SCL == 0 { - config.SCL = SCL_PIN - } - if config.SDA == 0 { - config.SDA = SDA_PIN - } - - i2c.initClock(config) - i2c.initNoiseFilter() - i2c.initPins(config) - i2c.initFrequency(config) - i2c.startMaster() - return nil -} - -//go:inline -func (i2c *I2C) initClock(config I2CConfig) { - // reset I2C clock - esp.SYSTEM.SetPERIP_RST_EN0_I2C_EXT0_RST(1) - esp.SYSTEM.SetPERIP_CLK_EN0_I2C_EXT0_CLK_EN(1) - esp.SYSTEM.SetPERIP_RST_EN0_I2C_EXT0_RST(0) - // disable interrupts - esp.I2C0.INT_ENA.ClearBits(0x3fff) - esp.I2C0.INT_CLR.ClearBits(0x3fff) - - esp.I2C0.SetCLK_CONF_SCLK_SEL(i2cClkSource) - esp.I2C0.SetCLK_CONF_SCLK_ACTIVE(1) - esp.I2C0.SetCLK_CONF_SCLK_DIV_NUM(i2cClkSourceFrequency / (config.Frequency * 1024)) - esp.I2C0.SetCTR_CLK_EN(1) -} - -//go:inline -func (i2c *I2C) initNoiseFilter() { - esp.I2C0.FILTER_CFG.Set(0x377) -} - -//go:inline -func (i2c *I2C) initPins(config I2CConfig) { - var muxConfig uint32 - const function = 1 // function 1 is just GPIO - - // SDA - muxConfig = function << esp.IO_MUX_GPIO_MCU_SEL_Pos - // Make this pin an input pin (always). - muxConfig |= esp.IO_MUX_GPIO_FUN_IE - // Set drive strength: 0 is lowest, 3 is highest. - muxConfig |= 1 << esp.IO_MUX_GPIO_FUN_DRV_Pos - config.SDA.mux().Set(muxConfig) - config.SDA.outFunc().Set(54) - inFunc(54).Set(uint32(esp.GPIO_FUNC_IN_SEL_CFG_SEL | config.SDA)) - config.SDA.Set(true) - // Configure the pad with the given IO mux configuration. - config.SDA.pinReg().SetBits(esp.GPIO_PIN_PAD_DRIVER) - - esp.GPIO.ENABLE.SetBits(1 << int(config.SDA)) - esp.I2C0.SetCTR_SDA_FORCE_OUT(1) - - // SCL - muxConfig = function << esp.IO_MUX_GPIO_MCU_SEL_Pos - // Make this pin an input pin (always). - muxConfig |= esp.IO_MUX_GPIO_FUN_IE - // Set drive strength: 0 is lowest, 3 is highest. - muxConfig |= 1 << esp.IO_MUX_GPIO_FUN_DRV_Pos - config.SCL.mux().Set(muxConfig) - config.SCL.outFunc().Set(53) - inFunc(53).Set(uint32(config.SCL)) - config.SCL.Set(true) - // Configure the pad with the given IO mux configuration. - config.SCL.pinReg().SetBits(esp.GPIO_PIN_PAD_DRIVER) - - esp.GPIO.ENABLE.SetBits(1 << int(config.SCL)) - esp.I2C0.SetCTR_SCL_FORCE_OUT(1) -} - -//go:inline -func (i2c *I2C) initFrequency(config I2CConfig) { - - clkmDiv := i2cClkSourceFrequency/(config.Frequency*1024) + 1 - sclkFreq := i2cClkSourceFrequency / clkmDiv - halfCycle := sclkFreq / config.Frequency / 2 - //SCL - sclLow := halfCycle - sclWaitHigh := uint32(0) - if config.Frequency > 50000 { - sclWaitHigh = halfCycle / 8 // compensate the time when freq > 50K - } - sclHigh := halfCycle - sclWaitHigh - // SDA - sdaHold := halfCycle / 4 - sda_sample := halfCycle / 2 - setup := halfCycle - hold := halfCycle - - esp.I2C0.SetSCL_LOW_PERIOD(sclLow - 1) - esp.I2C0.SetSCL_HIGH_PERIOD(sclHigh) - esp.I2C0.SetSCL_HIGH_PERIOD_SCL_WAIT_HIGH_PERIOD(25) - esp.I2C0.SetSCL_RSTART_SETUP_TIME(setup) - esp.I2C0.SetSCL_STOP_SETUP_TIME(setup) - esp.I2C0.SetSCL_START_HOLD_TIME(hold - 1) - esp.I2C0.SetSCL_STOP_HOLD_TIME(hold - 1) - esp.I2C0.SetSDA_SAMPLE_TIME(sda_sample) - esp.I2C0.SetSDA_HOLD_TIME(sdaHold) -} - -//go:inline -func (i2c *I2C) startMaster() { - // FIFO mode for data - esp.I2C0.SetFIFO_CONF_NONFIFO_EN(0) - // Reset TX & RX buffers - esp.I2C0.SetFIFO_CONF_RX_FIFO_RST(1) - esp.I2C0.SetFIFO_CONF_RX_FIFO_RST(0) - esp.I2C0.SetFIFO_CONF_TX_FIFO_RST(1) - esp.I2C0.SetFIFO_CONF_TX_FIFO_RST(0) - // set timeout value - esp.I2C0.TO.Set(0x10) - // enable master mode - esp.I2C0.CTR.Set(0x113) - esp.I2C0.SetCTR_CONF_UPGATE(1) - resetMaster() -} - -//go:inline -func resetMaster() { - // reset FSM - esp.I2C0.SetCTR_FSM_RST(1) - // clear the bus - esp.I2C0.SetSCL_SP_CONF_SCL_RST_SLV_NUM(9) - esp.I2C0.SetSCL_SP_CONF_SCL_RST_SLV_EN(1) - esp.I2C0.SetSCL_STRETCH_CONF_SLAVE_SCL_STRETCH_EN(1) - esp.I2C0.SetCTR_CONF_UPGATE(1) - esp.I2C0.FILTER_CFG.Set(0x377) - // wait for SCL_RST_SLV_EN - for esp.I2C0.GetSCL_SP_CONF_SCL_RST_SLV_EN() != 0 { +var ( + I2C0 = &I2C{ + Bus: esp.I2C0, + funcSCL: I2CEXT0_SCL_OUT_IDX, + funcSDA: I2CEXT0_SDA_OUT_IDX, + useExt1: false, } - esp.I2C0.SetSCL_SP_CONF_SCL_RST_SLV_NUM(0) -} - -type i2cCommandType = uint32 -type i2cAck = uint32 - -const ( - i2cCMD_RSTART i2cCommandType = 6 << 11 - i2cCMD_WRITE i2cCommandType = 1<<11 | 1<<8 // WRITE + ack_check_en - i2cCMD_READ i2cCommandType = 3<<11 | 1<<8 // READ + ack_check_en - i2cCMD_READLAST i2cCommandType = 3<<11 | 5<<8 // READ + ack_check_en + NACK - i2cCMD_STOP i2cCommandType = 2 << 11 - i2cCMD_END i2cCommandType = 4 << 11 ) - -type i2cCommand struct { - cmd i2cCommandType - data []byte - head int -} - -//go:linkname nanotime runtime.nanotime -func nanotime() int64 - -func (i2c *I2C) transmit(addr uint16, cmd []i2cCommand, timeoutMS int) error { - const intMask = esp.I2C_INT_STATUS_END_DETECT_INT_ST_Msk | esp.I2C_INT_STATUS_TRANS_COMPLETE_INT_ST_Msk | esp.I2C_INT_STATUS_TIME_OUT_INT_ST_Msk | esp.I2C_INT_STATUS_NACK_INT_ST_Msk - esp.I2C0.INT_CLR.SetBits(intMask) - esp.I2C0.INT_ENA.SetBits(intMask) - esp.I2C0.SetCTR_CONF_UPGATE(1) - - defer func() { - esp.I2C0.INT_CLR.SetBits(intMask) - esp.I2C0.INT_ENA.ClearBits(intMask) - }() - - timeoutNS := int64(timeoutMS) * 1000000 - needAddress := true - needRestart := false - readLast := false - var readTo []byte - for cmdIdx, reg := 0, &esp.I2C0.COMD0; cmdIdx < len(cmd); { - c := &cmd[cmdIdx] - - switch c.cmd { - case i2cCMD_RSTART: - reg.Set(i2cCMD_RSTART) - reg = nextAddress(reg) - cmdIdx++ - - case i2cCMD_WRITE: - count := 32 - if needAddress { - needAddress = false - esp.I2C0.SetDATA_FIFO_RDATA((uint32(addr) & 0x7f) << 1) - count-- - esp.I2C0.SLAVE_ADDR.Set(uint32(addr)) - esp.I2C0.SetCTR_CONF_UPGATE(1) - } - for ; count > 0 && c.head < len(c.data); count, c.head = count-1, c.head+1 { - esp.I2C0.SetDATA_FIFO_RDATA(uint32(c.data[c.head])) - } - reg.Set(i2cCMD_WRITE | uint32(32-count)) - reg = nextAddress(reg) - - if c.head < len(c.data) { - reg.Set(i2cCMD_END) - reg = nil - } else { - cmdIdx++ - } - needRestart = true - - case i2cCMD_READ: - if needAddress { - needAddress = false - esp.I2C0.SetDATA_FIFO_RDATA((uint32(addr)&0x7f)<<1 | 1) - esp.I2C0.SLAVE_ADDR.Set(uint32(addr)) - reg.Set(i2cCMD_WRITE | 1) - reg = nextAddress(reg) - } - if needRestart { - // We need to send RESTART again after i2cCMD_WRITE. - reg.Set(i2cCMD_RSTART) - - reg = nextAddress(reg) - reg.Set(i2cCMD_WRITE | 1) - - reg = nextAddress(reg) - esp.I2C0.SetDATA_FIFO_RDATA((uint32(addr)&0x7f)<<1 | 1) - needRestart = false - } - count := 32 - bytes := len(c.data) - c.head - // Only last byte in sequence must be sent with ACK set to 1 to indicate end of data. - split := bytes <= count - if split { - bytes-- - } - if bytes > 32 { - bytes = 32 - } - reg.Set(i2cCMD_READ | uint32(bytes)) - reg = nextAddress(reg) - - if split { - readLast = true - reg.Set(i2cCMD_READLAST | 1) - reg = nextAddress(reg) - readTo = c.data[c.head : c.head+bytes+1] // read bytes + 1 last byte - cmdIdx++ - } else { - reg.Set(i2cCMD_END) - readTo = c.data[c.head : c.head+bytes] - reg = nil - } - - case i2cCMD_STOP: - reg.Set(i2cCMD_STOP) - reg = nil - cmdIdx++ - } - if reg == nil { - // transmit now - esp.I2C0.SetCTR_CONF_UPGATE(1) - esp.I2C0.SetCTR_TRANS_START(1) - end := nanotime() + timeoutNS - var mask uint32 - for mask = esp.I2C0.INT_STATUS.Get(); mask&intMask == 0; mask = esp.I2C0.INT_STATUS.Get() { - if nanotime() > end { - if readTo != nil { - return errI2CReadTimeout - } - return errI2CWriteTimeout - } - } - switch { - case mask&esp.I2C_INT_STATUS_NACK_INT_ST_Msk != 0 && !readLast: - return errI2CAckExpected - case mask&esp.I2C_INT_STATUS_TIME_OUT_INT_ST_Msk != 0: - if readTo != nil { - return errI2CReadTimeout - } - return errI2CWriteTimeout - } - esp.I2C0.INT_CLR.SetBits(intMask) - for i := 0; i < len(readTo); i++ { - readTo[i] = byte(esp.I2C0.GetDATA_FIFO_RDATA() & 0xff) - c.head++ - } - readTo = nil - reg = &esp.I2C0.COMD0 - } - } - return nil -} - -// Tx does a single I2C transaction at the specified address. -// It clocks out the given address, writes the bytes in w, reads back len(r) -// bytes and stores them in r, and generates a stop condition on the bus. -func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) { - // timeout in microseconds. - const timeout = 40 // 40ms is a reasonable time for a real-time system. - - cmd := make([]i2cCommand, 0, 8) - cmd = append(cmd, i2cCommand{cmd: i2cCMD_RSTART}) - if len(w) > 0 { - cmd = append(cmd, i2cCommand{cmd: i2cCMD_WRITE, data: w}) - } - if len(r) > 0 { - cmd = append(cmd, i2cCommand{cmd: i2cCMD_READ, data: r}) - } - cmd = append(cmd, i2cCommand{cmd: i2cCMD_STOP}) - - return i2c.transmit(addr, cmd, timeout) -} - -func (i2c *I2C) SetBaudRate(br uint32) error { - return nil -} - -func nextAddress(reg *volatile.Register32) *volatile.Register32 { - return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(reg), 4)) -} diff --git a/src/machine/machine_esp32s3.go b/src/machine/machine_esp32s3.go index f3ac520d2b..fdb3dbd374 100644 --- a/src/machine/machine_esp32s3.go +++ b/src/machine/machine_esp32s3.go @@ -273,6 +273,10 @@ func (p Pin) Get() bool { } } +func (p Pin) pinReg() *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.GPIO.PIN0), uintptr(p)*4)) +} + var DefaultUART = UART0 var ( diff --git a/src/machine/machine_esp32s3_i2c.go b/src/machine/machine_esp32s3_i2c.go new file mode 100644 index 0000000000..ce1337e899 --- /dev/null +++ b/src/machine/machine_esp32s3_i2c.go @@ -0,0 +1,35 @@ +//go:build esp32s3 + +package machine + +import ( + "device/esp" +) + +const ( + I2CEXT0_SCL_OUT_IDX = 89 + I2CEXT0_SDA_OUT_IDX = 90 + I2CEXT1_SCL_OUT_IDX = 91 + I2CEXT1_SDA_OUT_IDX = 92 +) + +var ( + I2C0 = &I2C{ + Bus: esp.I2C0, + funcSCL: I2CEXT0_SCL_OUT_IDX, + funcSDA: I2CEXT0_SDA_OUT_IDX, + useExt1: false, + } + I2C1 = &I2C{ + Bus: esp.I2C1, + funcSCL: I2CEXT1_SCL_OUT_IDX, + funcSDA: I2CEXT1_SDA_OUT_IDX, + useExt1: true, + } +) + +func initI2CExt1Clock() { + esp.SYSTEM.SetPERIP_RST_EN0_I2C_EXT1_RST(1) + esp.SYSTEM.SetPERIP_CLK_EN0_I2C_EXT1_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_I2C_EXT1_RST(0) +} diff --git a/src/machine/machine_esp32xx_i2c.go b/src/machine/machine_esp32xx_i2c.go new file mode 100644 index 0000000000..0ccae5ccee --- /dev/null +++ b/src/machine/machine_esp32xx_i2c.go @@ -0,0 +1,336 @@ +//go:build (esp32c3 || esp32s3) && !m5stamp_c3 + +package machine + +import ( + "device/esp" + "runtime/volatile" + "unsafe" +) + +type I2C struct { + Bus *esp.I2C_Type + funcSCL, funcSDA uint32 + useExt1 bool + txCmdBuf [8]i2cCommand +} + +// I2CConfig is used to store config info for I2C. +type I2CConfig struct { + Frequency uint32 // in Hz + SCL Pin + SDA Pin +} + +const ( + clkXTAL = 0 + clkFOSC = 1 + clkXTALFrequency = uint32(40e6) + clkFOSCFrequency = uint32(17.5e6) + i2cClkSourceFrequency = clkXTALFrequency + i2cClkSource = clkXTAL +) + +func (i2c *I2C) Configure(config I2CConfig) error { + if config.Frequency == 0 { + config.Frequency = 400 * KHz + } + if config.SCL == 0 { + config.SCL = SCL_PIN + } + if config.SDA == 0 { + config.SDA = SDA_PIN + } + + i2c.initClock(config) + i2c.initNoiseFilter() + i2c.initPins(config) + i2c.initFrequency(config) + i2c.startMaster() + return nil +} + +//go:inline +func (i2c *I2C) initClock(config I2CConfig) { + if !i2c.useExt1 { + esp.SYSTEM.SetPERIP_RST_EN0_I2C_EXT0_RST(1) + esp.SYSTEM.SetPERIP_CLK_EN0_I2C_EXT0_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_I2C_EXT0_RST(0) + } else { + initI2CExt1Clock() + } + // disable interrupts + i2c.Bus.INT_CLR.Set(0x3fff) + i2c.Bus.INT_ENA.ClearBits(0x3fff) + + i2c.Bus.SetCLK_CONF_SCLK_SEL(i2cClkSource) + i2c.Bus.SetCLK_CONF_SCLK_ACTIVE(1) + i2c.Bus.SetCLK_CONF_SCLK_DIV_NUM(i2cClkSourceFrequency / (config.Frequency * 1024)) + i2c.Bus.SetCTR_CLK_EN(1) +} + +//go:inline +func (i2c *I2C) initNoiseFilter() { + i2c.Bus.FILTER_CFG.Set(0x377) +} + +//go:inline +func (i2c *I2C) initPins(config I2CConfig) { + config.SDA.configure(PinConfig{Mode: PinOutput}, i2c.funcSDA) + inFunc(i2c.funcSDA).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(config.SDA)< 50000 { + sclWaitHigh = halfCycle / 8 // compensate the time when freq > 50K + } + sclHigh := halfCycle - sclWaitHigh + // SDA + sdaHold := halfCycle / 4 + sda_sample := halfCycle / 2 + setup := halfCycle + hold := halfCycle + + i2c.Bus.SetSCL_LOW_PERIOD(sclLow - 1) + i2c.Bus.SetSCL_HIGH_PERIOD(sclHigh) + i2c.Bus.SetSCL_HIGH_PERIOD_SCL_WAIT_HIGH_PERIOD(25) + i2c.Bus.SetSCL_RSTART_SETUP_TIME(setup) + i2c.Bus.SetSCL_STOP_SETUP_TIME(setup) + i2c.Bus.SetSCL_START_HOLD_TIME(hold - 1) + i2c.Bus.SetSCL_STOP_HOLD_TIME(hold - 1) + i2c.Bus.SetSDA_SAMPLE_TIME(sda_sample) + i2c.Bus.SetSDA_HOLD_TIME(sdaHold) +} + +//go:inline +func (i2c *I2C) startMaster() { + // FIFO mode for data + i2c.Bus.SetFIFO_CONF_NONFIFO_EN(0) + // Reset TX & RX buffers + i2c.Bus.SetFIFO_CONF_RX_FIFO_RST(1) + i2c.Bus.SetFIFO_CONF_RX_FIFO_RST(0) + i2c.Bus.SetFIFO_CONF_TX_FIFO_RST(1) + i2c.Bus.SetFIFO_CONF_TX_FIFO_RST(0) + // set timeout value + i2c.Bus.TO.Set(0x10) + // enable master mode + i2c.Bus.CTR.Set(0x113) + i2c.Bus.SetCTR_CONF_UPGATE(1) + i2c.resetMaster() +} + +//go:inline +func (i2c *I2C) resetMaster() { + // reset FSM + i2c.Bus.SetCTR_FSM_RST(1) + // clear the bus + i2c.Bus.SetSCL_SP_CONF_SCL_RST_SLV_NUM(9) + i2c.Bus.SetSCL_SP_CONF_SCL_RST_SLV_EN(1) + i2c.Bus.SetSCL_STRETCH_CONF_SLAVE_SCL_STRETCH_EN(1) + i2c.Bus.SetCTR_CONF_UPGATE(1) + i2c.Bus.FILTER_CFG.Set(0x377) + // wait for SCL_RST_SLV_EN + for i2c.Bus.GetSCL_SP_CONF_SCL_RST_SLV_EN() != 0 { + } + i2c.Bus.SetSCL_SP_CONF_SCL_RST_SLV_NUM(0) +} + +type i2cCommandType = uint32 +type i2cAck = uint32 + +const ( + i2cCMD_RSTART i2cCommandType = 6 << 11 + i2cCMD_WRITE i2cCommandType = 1<<11 | 1<<8 // WRITE + ack_check_en + i2cCMD_READ i2cCommandType = 3<<11 | 1<<8 // READ + ack_check_en + i2cCMD_READLAST i2cCommandType = 3<<11 | 5<<8 // READ + ack_check_en + NACK + i2cCMD_STOP i2cCommandType = 2 << 11 + i2cCMD_END i2cCommandType = 4 << 11 +) + +type i2cCommand struct { + cmd i2cCommandType + data []byte + head int +} + +//go:linkname nanotime runtime.nanotime +func nanotime() int64 + +func (i2c *I2C) transmit(addr uint16, cmd []i2cCommand, timeoutMS int) error { + const intMask = esp.I2C_INT_STATUS_END_DETECT_INT_ST_Msk | esp.I2C_INT_STATUS_TRANS_COMPLETE_INT_ST_Msk | esp.I2C_INT_STATUS_TIME_OUT_INT_ST_Msk | esp.I2C_INT_STATUS_NACK_INT_ST_Msk + i2c.Bus.INT_CLR.SetBits(intMask) + i2c.Bus.INT_ENA.SetBits(intMask) + i2c.Bus.SetCTR_CONF_UPGATE(1) + + defer func() { + i2c.Bus.INT_CLR.SetBits(intMask) + i2c.Bus.INT_ENA.ClearBits(intMask) + }() + + timeoutNS := int64(timeoutMS) * 1000000 + needAddress := true + needRestart := false + readLast := false + var readTo []byte + for cmdIdx, reg := 0, &i2c.Bus.COMD0; cmdIdx < len(cmd); { + c := &cmd[cmdIdx] + + switch c.cmd { + case i2cCMD_RSTART: + reg.Set(i2cCMD_RSTART) + reg = nextAddress(reg) + cmdIdx++ + + case i2cCMD_WRITE: + count := 32 + if needAddress { + needAddress = false + i2c.Bus.SetDATA_FIFO_RDATA((uint32(addr) & 0x7f) << 1) + count-- + i2c.Bus.SLAVE_ADDR.Set(uint32(addr)) + i2c.Bus.SetCTR_CONF_UPGATE(1) + } + for ; count > 0 && c.head < len(c.data); count, c.head = count-1, c.head+1 { + i2c.Bus.SetDATA_FIFO_RDATA(uint32(c.data[c.head])) + } + reg.Set(i2cCMD_WRITE | uint32(32-count)) + reg = nextAddress(reg) + + if c.head < len(c.data) { + reg.Set(i2cCMD_END) + reg = nil + } else { + cmdIdx++ + } + needRestart = true + + case i2cCMD_READ: + if needAddress { + needAddress = false + i2c.Bus.SetDATA_FIFO_RDATA((uint32(addr)&0x7f)<<1 | 1) + i2c.Bus.SLAVE_ADDR.Set(uint32(addr)) + reg.Set(i2cCMD_WRITE | 1) + reg = nextAddress(reg) + } + if needRestart { + // We need to send RESTART again after i2cCMD_WRITE. + reg.Set(i2cCMD_RSTART) + + reg = nextAddress(reg) + reg.Set(i2cCMD_WRITE | 1) + + reg = nextAddress(reg) + i2c.Bus.SetDATA_FIFO_RDATA((uint32(addr)&0x7f)<<1 | 1) + needRestart = false + } + count := 32 + bytes := len(c.data) - c.head + // Only last byte in sequence must be sent with ACK set to 1 to indicate end of data. + split := bytes <= count + if split { + bytes-- + } + if bytes > 32 { + bytes = 32 + } + if bytes > 0 { + reg.Set(i2cCMD_READ | uint32(bytes)) + reg = nextAddress(reg) + } + if split { + readLast = true + reg.Set(i2cCMD_READLAST | 1) + reg = nextAddress(reg) + readTo = c.data[c.head : c.head+bytes+1] // read bytes + 1 last byte + cmdIdx++ + } else { + reg.Set(i2cCMD_END) + readTo = c.data[c.head : c.head+bytes] + reg = nil + } + + case i2cCMD_STOP: + reg.Set(i2cCMD_STOP) + reg = nil + cmdIdx++ + } + if reg == nil { + // transmit now + i2c.Bus.SetCTR_CONF_UPGATE(1) + i2c.Bus.SetCTR_TRANS_START(1) + end := nanotime() + timeoutNS + var mask uint32 + for mask = i2c.Bus.INT_STATUS.Get(); mask&intMask == 0; mask = i2c.Bus.INT_STATUS.Get() { + if nanotime() > end { + if readTo != nil { + return errI2CReadTimeout + } + return errI2CWriteTimeout + } + } + switch { + case mask&esp.I2C_INT_STATUS_NACK_INT_ST_Msk != 0 && !readLast: + return errI2CAckExpected + case mask&esp.I2C_INT_STATUS_TIME_OUT_INT_ST_Msk != 0: + if readTo != nil { + return errI2CReadTimeout + } + return errI2CWriteTimeout + } + i2c.Bus.INT_CLR.SetBits(intMask) + for i := 0; i < len(readTo); i++ { + readTo[i] = byte(i2c.Bus.GetDATA_FIFO_RDATA() & 0xff) + c.head++ + } + readTo = nil + reg = &i2c.Bus.COMD0 + } + } + return nil +} + +// Tx does a single I2C transaction at the specified address. +// It clocks out the given address, writes the bytes in w, reads back len(r) +// bytes and stores them in r, and generates a stop condition on the bus. +func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) { + // timeout in milliseconds. + const timeout = 40 // 40ms is a reasonable time for a real-time system. + + cmd := i2c.txCmdBuf[:0] + cmd = append(cmd, i2cCommand{cmd: i2cCMD_RSTART}) + if len(w) > 0 { + cmd = append(cmd, i2cCommand{cmd: i2cCMD_WRITE, data: w}) + } + if len(r) > 0 { + cmd = append(cmd, i2cCommand{cmd: i2cCMD_READ, data: r}) + } + cmd = append(cmd, i2cCommand{cmd: i2cCMD_STOP}) + + return i2c.transmit(addr, cmd, timeout) +} + +func (i2c *I2C) SetBaudRate(br uint32) error { + return nil +} + +func nextAddress(reg *volatile.Register32) *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(reg), 4)) +} From 9a1be25f3d923e2098d1d14f43d5e8cbc8e8b5d4 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 24 Feb 2026 14:34:13 +0100 Subject: [PATCH 26/81] transform: modify output format from the -print-allocs flag to be the same as expected by the go coverage tool Signed-off-by: deadprogram --- transform/allocs.go | 38 ++++++++++++++++++++++++++++++++++- transform/allocs_test.go | 17 +++++++++------- transform/optimizer.go | 6 +++++- transform/testdata/allocs2.go | 20 +++++++++--------- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/transform/allocs.go b/transform/allocs.go index 870faa5b75..d9c7e47096 100644 --- a/transform/allocs.go +++ b/transform/allocs.go @@ -6,8 +6,10 @@ package transform // interprocedural escape analysis. import ( + "bufio" "fmt" "go/token" + "os" "regexp" "tinygo.org/x/go-llvm" @@ -37,6 +39,10 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u complex128Type := ctx.StructType([]llvm.Type{ctx.DoubleType(), ctx.DoubleType()}, false) maxAlign := int64(targetData.ABITypeAlignment(complex128Type)) + if printAllocs != nil { + fmt.Fprintln(os.Stderr, "mode: set") + } + for _, heapalloc := range getUses(allocator) { logAllocs := printAllocs != nil && printAllocs.MatchString(heapalloc.InstructionParent().Parent().Name()) if heapalloc.Operand(0).IsAConstantInt().IsNil() { @@ -173,5 +179,35 @@ func valueEscapesAt(value llvm.Value) llvm.Value { // logAlloc prints a message to stderr explaining why the given object had to be // allocated on the heap. func logAlloc(logger func(token.Position, string), allocCall llvm.Value, reason string) { - logger(getPosition(allocCall), "object allocated on the heap: "+reason) + pos := getPosition(allocCall) + if pos.Filename == "" || pos.Line <= 0 { + logger(pos, "") + return + } + + endCol := lineLengthAt(pos.Filename, pos.Line) + if endCol < 1 { + endCol = 1 + } + + // Only emit the coverprofile line, without position prefix. + logger(token.Position{}, fmt.Sprintf("%s:%d.1,%d.%d 1 0", pos.Filename, pos.Line, pos.Line, endCol)) +} + +func lineLengthAt(filename string, lineNumber int) int { + f, err := os.Open(filename) + if err != nil { + return 0 + } + defer f.Close() + + scanner := bufio.NewScanner(f) + line := 1 + for scanner.Scan() { + if line == lineNumber { + return len(scanner.Text()) + } + line++ + } + return 0 } diff --git a/transform/allocs_test.go b/transform/allocs_test.go index 7f7ff5b75e..df39605b16 100644 --- a/transform/allocs_test.go +++ b/transform/allocs_test.go @@ -57,9 +57,9 @@ func TestAllocs2(t *testing.T) { sort.Slice(testOutputs, func(i, j int) bool { return testOutputs[i].line < testOutputs[j].line }) - testOutput := "" + testOutput := make([]string, 0) for _, out := range testOutputs { - testOutput += out.String() + "\n" + testOutput = append(testOutput, out.String()) } // Load expected test output (the OUT: lines). @@ -67,15 +67,18 @@ func TestAllocs2(t *testing.T) { if err != nil { t.Fatal("could not read test input:", err) } - var expectedTestOutput string - for i, line := range strings.Split(strings.ReplaceAll(string(testInput), "\r\n", "\n"), "\n") { + var expectedTestOutput []string + for _, line := range strings.Split(strings.ReplaceAll(string(testInput), "\r\n", "\n"), "\n") { if idx := strings.Index(line, " // OUT: "); idx > 0 { msg := line[idx+len(" // OUT: "):] - expectedTestOutput += "allocs2.go:" + strconv.Itoa(i+1) + ": " + msg + "\n" + expectedTestOutput = append(expectedTestOutput, msg) } } - if testOutput != expectedTestOutput { - t.Errorf("output does not match expected output:\n%s", testOutput) + for i := range testOutput { + if !strings.HasSuffix(testOutput[i], expectedTestOutput[i]) { + t.Errorf("output does not match expected output:\n%s\n%s\n", testOutput[i], expectedTestOutput[i]) + return + } } } diff --git a/transform/optimizer.go b/transform/optimizer.go index 54f9762bc4..1209754310 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -88,7 +88,11 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { // Run TinyGo-specific interprocedural optimizations. OptimizeAllocs(mod, config.Options.PrintAllocs, maxStackSize, func(pos token.Position, msg string) { - fmt.Fprintln(os.Stderr, pos.String()+": "+msg) + if pos.Filename != "" { + fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", pos.Filename, pos.Line, pos.Column, msg) + } else { + fmt.Fprintln(os.Stderr, msg) // No prefix! + } }) OptimizeStringToBytes(mod) OptimizeStringEqual(mod) diff --git a/transform/testdata/allocs2.go b/transform/testdata/allocs2.go index 9fcebb212f..2b0306db10 100644 --- a/transform/testdata/allocs2.go +++ b/transform/testdata/allocs2.go @@ -10,7 +10,7 @@ func main() { derefInt(&n1) // This should eventually be modified to not escape. - n2 := 6 // OUT: object allocated on the heap: escapes at line 14 + n2 := 6 // OUT: allocs2.go:52.1,52.42 1 0 returnIntPtr(&n2) s1 := make([]int, 3) @@ -20,22 +20,22 @@ func main() { readIntSlice(s2[:]) // This should also be modified to not escape. - s3 := make([]int, 3) // OUT: object allocated on the heap: escapes at line 24 + s3 := make([]int, 3) // OUT: allocs2.go:51.1,51.42 1 0 returnIntSlice(s3) - useSlice(make([]int, getUnknownNumber())) // OUT: object allocated on the heap: size is not constant + useSlice(make([]int, getUnknownNumber())) // OUT: allocs2.go:48.1,48.55 1 0 - s4 := make([]byte, 300) // OUT: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256 + s4 := make([]byte, 300) // OUT: allocs2.go:46.1,46.56 1 0 readByteSlice(s4) - s5 := make([]int, 4) // OUT: object allocated on the heap: escapes at line 32 + s5 := make([]int, 4) // OUT: allocs2.go:38.1,38.56 1 0 _ = append(s5, 5) s6 := make([]int, 3) s7 := []int{1, 2, 3} copySlice(s6, s7) - c1 := getComplex128() // OUT: object allocated on the heap: escapes at line 39 + c1 := getComplex128() // OUT: allocs2.go:31.1,31.55 1 0 useInterface(c1) n3 := 5 @@ -43,13 +43,13 @@ func main() { return n3 }() - callVariadic(3, 5, 8) // OUT: object allocated on the heap: escapes at line 46 + callVariadic(3, 5, 8) // OUT: allocs2.go:28.1,28.58 1 0 - s8 := []int{3, 5, 8} // OUT: object allocated on the heap: escapes at line 49 + s8 := []int{3, 5, 8} // OUT: allocs2.go:26.1,26.76 1 0 callVariadic(s8...) - n4 := 3 // OUT: object allocated on the heap: escapes at line 53 - n5 := 7 // OUT: object allocated on the heap: escapes at line 53 + n4 := 3 // OUT: allocs2.go:23.1,23.55 1 0 + n5 := 7 // OUT: allocs2.go:13.1,13.42 1 0 func() { n4 = n5 }() From 4afce2bb138997496af3679d4a4cc278b1a50215 Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 25 Feb 2026 10:36:56 +0200 Subject: [PATCH 27/81] esp3s3 & esp32c3 PWM implements (#5215) * esp3s3 pwm * esp32s3-pwm: added spi alias for esp32s3 for tests * esp32s3-pwm: fix linters * esp32s3-pwm: fix rename * esp32s3-pwm: replace if to switch * esp32s3-pwm: chip specific registry * esp32s3-pwm: fix linters --- GNUmakefile | 20 ++- src/examples/pwm/esp32c3-supermini.go | 11 ++ src/examples/pwm/esp32s3-wroom1.go | 11 ++ src/examples/pwm/xiao_esp32s3.go | 11 ++ src/machine/machine_esp32c3_pwm.go | 178 ++++++++++++++++++++ src/machine/machine_esp32c3_spi.go | 1 + src/machine/machine_esp32s3_pwm.go | 226 ++++++++++++++++++++++++++ src/machine/machine_esp32xx_pwm.go | 179 ++++++++++++++++++++ 8 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 src/examples/pwm/esp32c3-supermini.go create mode 100644 src/examples/pwm/esp32s3-wroom1.go create mode 100644 src/examples/pwm/xiao_esp32s3.go create mode 100644 src/machine/machine_esp32c3_pwm.go create mode 100644 src/machine/machine_esp32s3_pwm.go create mode 100644 src/machine/machine_esp32xx_pwm.go diff --git a/GNUmakefile b/GNUmakefile index 536a3e6c02..3eed5b9419 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -923,17 +923,34 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target mch2022 examples/machinetest @$(MD5SUM) test.bin + # xiao-esp32s3 $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/blinky1 @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/blinkm @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/mcp3008 @$(MD5SUM) test.bin - $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/mcp3008 + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/pwm @$(MD5SUM) test.bin + + # esp32s3-wroom1 $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/blinkm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/mcp3008 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/pwm + @$(MD5SUM) test.bin endif + # esp32c3-supermini + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinky1 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinkm + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/mcp3008 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/pwm + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp-c3-32s-kit examples/blinky1 @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=qtpy-esp32c3 examples/machinetest @@ -946,6 +963,7 @@ endif @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32c3-12f examples/blinky1 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=makerfabs-esp32c3spi35 examples/machinetest @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.hex -target=hifive1b examples/blinky1 diff --git a/src/examples/pwm/esp32c3-supermini.go b/src/examples/pwm/esp32c3-supermini.go new file mode 100644 index 0000000000..edca88b97e --- /dev/null +++ b/src/examples/pwm/esp32c3-supermini.go @@ -0,0 +1,11 @@ +//go:build esp32c3_supermini + +package main + +import "machine" + +var ( + pwm = machine.PWM0 + pinA = machine.GPIO5 + pinB = machine.GPIO6 +) diff --git a/src/examples/pwm/esp32s3-wroom1.go b/src/examples/pwm/esp32s3-wroom1.go new file mode 100644 index 0000000000..b3e5dcb584 --- /dev/null +++ b/src/examples/pwm/esp32s3-wroom1.go @@ -0,0 +1,11 @@ +//go:build esp32s3_wroom1 + +package main + +import "machine" + +var ( + pwm = machine.PWM0 + pinA = machine.GPIO17 + pinB = machine.GPIO18 +) diff --git a/src/examples/pwm/xiao_esp32s3.go b/src/examples/pwm/xiao_esp32s3.go new file mode 100644 index 0000000000..c614abc861 --- /dev/null +++ b/src/examples/pwm/xiao_esp32s3.go @@ -0,0 +1,11 @@ +//go:build xiao_esp32s3 + +package main + +import "machine" + +var ( + pwm = machine.PWM0 + pinA = machine.D0 + pinB = machine.D1 +) diff --git a/src/machine/machine_esp32c3_pwm.go b/src/machine/machine_esp32c3_pwm.go new file mode 100644 index 0000000000..d7e575e8d9 --- /dev/null +++ b/src/machine/machine_esp32c3_pwm.go @@ -0,0 +1,178 @@ +//go:build esp32c3 + +package machine + +import "device/esp" + +// LEDC PWM for ESP32-C3: 4 timers (PWM0–PWM3), 6 channels per timer; each timer has its own frequency. +// Range: frequency from a few Hz up to ~40 MHz (at 1-bit resolution); duty resolution 1–15 bits +// (higher frequency gives lower resolution). Clock source: APB 80 MHz. Low-speed mode only. +// See ESP-IDF LEDC driver, TRM LED PWM Controller. + +// GPIO matrix output signal indices for LEDC (soc/gpio_sig_map.h) +const ( + LEDC_LS_SIG_OUT0_IDX = 45 +) + +const ledcChannelsC3 = 6 + +var ( + PWM0 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsC3, timerNum: 0} + PWM1 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsC3, timerNum: 1} + PWM2 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsC3, timerNum: 2} + PWM3 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsC3, timerNum: 3} +) + +// chanOp implements LEDC low-speed channel ops for ESP32-C3 (channels 0–5 only). +func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) { + invVal := uint32(0) + if inverting { + invVal = 1 + } + switch ch { + case 0: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH0_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH0_CONF0_IDLE_LV(0) + esp.LEDC.SetCH0_HPOINT_HPOINT(0) + esp.LEDC.SetCH0_DUTY_DUTY(0) + esp.LEDC.SetCH0_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH0_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH0_CONF1_DUTY_INC(1) + esp.LEDC.SetCH0_CONF1_DUTY_START(1) + esp.LEDC.SetCH0_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH0_DUTY_DUTY(duty) + esp.LEDC.SetCH0_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH0_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH0_CONF1_DUTY_INC(1) + esp.LEDC.SetCH0_CONF1_DUTY_START(1) + esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH0_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH0_CONF0_IDLE_LV(invVal) + } + case 1: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH1_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH1_CONF0_IDLE_LV(0) + esp.LEDC.SetCH1_HPOINT_HPOINT(0) + esp.LEDC.SetCH1_DUTY_DUTY(0) + esp.LEDC.SetCH1_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH1_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH1_CONF1_DUTY_INC(1) + esp.LEDC.SetCH1_CONF1_DUTY_START(1) + esp.LEDC.SetCH1_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH1_DUTY_DUTY(duty) + esp.LEDC.SetCH1_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH1_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH1_CONF1_DUTY_INC(1) + esp.LEDC.SetCH1_CONF1_DUTY_START(1) + esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH1_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH1_CONF0_IDLE_LV(invVal) + } + case 2: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH2_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH2_CONF0_IDLE_LV(0) + esp.LEDC.SetCH2_HPOINT_HPOINT(0) + esp.LEDC.SetCH2_DUTY_DUTY(0) + esp.LEDC.SetCH2_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH2_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH2_CONF1_DUTY_INC(1) + esp.LEDC.SetCH2_CONF1_DUTY_START(1) + esp.LEDC.SetCH2_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH2_DUTY_DUTY(duty) + esp.LEDC.SetCH2_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH2_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH2_CONF1_DUTY_INC(1) + esp.LEDC.SetCH2_CONF1_DUTY_START(1) + esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH2_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH2_CONF0_IDLE_LV(invVal) + } + case 3: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH3_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH3_CONF0_IDLE_LV(0) + esp.LEDC.SetCH3_HPOINT_HPOINT(0) + esp.LEDC.SetCH3_DUTY_DUTY(0) + esp.LEDC.SetCH3_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH3_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH3_CONF1_DUTY_INC(1) + esp.LEDC.SetCH3_CONF1_DUTY_START(1) + esp.LEDC.SetCH3_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH3_DUTY_DUTY(duty) + esp.LEDC.SetCH3_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH3_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH3_CONF1_DUTY_INC(1) + esp.LEDC.SetCH3_CONF1_DUTY_START(1) + esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH3_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH3_CONF0_IDLE_LV(invVal) + } + case 4: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH4_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH4_CONF0_IDLE_LV(0) + esp.LEDC.SetCH4_HPOINT_HPOINT(0) + esp.LEDC.SetCH4_DUTY_DUTY(0) + esp.LEDC.SetCH4_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH4_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH4_CONF1_DUTY_INC(1) + esp.LEDC.SetCH4_CONF1_DUTY_START(1) + esp.LEDC.SetCH4_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH4_DUTY_DUTY(duty) + esp.LEDC.SetCH4_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH4_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH4_CONF1_DUTY_INC(1) + esp.LEDC.SetCH4_CONF1_DUTY_START(1) + esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH4_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH4_CONF0_IDLE_LV(invVal) + } + case 5: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH5_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH5_CONF0_IDLE_LV(0) + esp.LEDC.SetCH5_HPOINT_HPOINT(0) + esp.LEDC.SetCH5_DUTY_DUTY(0) + esp.LEDC.SetCH5_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH5_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH5_CONF1_DUTY_INC(1) + esp.LEDC.SetCH5_CONF1_DUTY_START(1) + esp.LEDC.SetCH5_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH5_DUTY_DUTY(duty) + esp.LEDC.SetCH5_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH5_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH5_CONF1_DUTY_INC(1) + esp.LEDC.SetCH5_CONF1_DUTY_START(1) + esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH5_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH5_CONF0_IDLE_LV(invVal) + } + } +} diff --git a/src/machine/machine_esp32c3_spi.go b/src/machine/machine_esp32c3_spi.go index aec3ca77a8..edb0218ac4 100644 --- a/src/machine/machine_esp32c3_spi.go +++ b/src/machine/machine_esp32c3_spi.go @@ -53,6 +53,7 @@ type SPI struct { var ( // SPI0 and SPI1 are reserved for use by the caching system etc. SPI2 = &SPI{esp.SPI2} + SPI0 = SPI2 ) // SPIConfig is used to store config info for SPI. diff --git a/src/machine/machine_esp32s3_pwm.go b/src/machine/machine_esp32s3_pwm.go new file mode 100644 index 0000000000..f4015a5f95 --- /dev/null +++ b/src/machine/machine_esp32s3_pwm.go @@ -0,0 +1,226 @@ +//go:build esp32s3 + +package machine + +import "device/esp" + +// LEDC PWM for ESP32-S3: 4 timers (PWM0–PWM3), 8 channels per timer; each timer has its own frequency. +// Range: frequency from a few Hz up to ~40 MHz (at 1-bit resolution); duty resolution 1–15 bits +// (higher frequency gives lower resolution). Clock source: APB 80 MHz. Low-speed mode only. +// Duty must not equal 2^resolution (overflow risk). See ESP-IDF LEDC driver, TRM LED PWM Controller. + +// GPIO matrix output signal indices for LEDC (soc/gpio_sig_map.h) +const ( + LEDC_LS_SIG_OUT0_IDX = 73 +) + +const ledcChannelsS3 = 8 + +var ( + PWM0 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsS3, timerNum: 0} + PWM1 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsS3, timerNum: 1} + PWM2 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsS3, timerNum: 2} + PWM3 = &LEDCPWM{SigOutBase: LEDC_LS_SIG_OUT0_IDX, NumChannels: ledcChannelsS3, timerNum: 3} +) + +// chanOp implements LEDC low-speed channel ops for ESP32-S3 (channels 0–7). +func (pwm *LEDCPWM) chanOp(ch uint8, op ledcChanOp, duty uint32, inverting bool) { + invVal := uint32(0) + if inverting { + invVal = 1 + } + switch ch { + case 0: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH0_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH0_CONF0_IDLE_LV(0) + esp.LEDC.SetCH0_HPOINT_HPOINT(0) + esp.LEDC.SetCH0_DUTY_DUTY(0) + esp.LEDC.SetCH0_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH0_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH0_CONF1_DUTY_INC(1) + esp.LEDC.SetCH0_CONF1_DUTY_START(1) + esp.LEDC.SetCH0_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH0_DUTY_DUTY(duty) + esp.LEDC.SetCH0_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH0_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH0_CONF1_DUTY_INC(1) + esp.LEDC.SetCH0_CONF1_DUTY_START(1) + esp.LEDC.SetCH0_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH0_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH0_CONF0_IDLE_LV(invVal) + } + case 1: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH1_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH1_CONF0_IDLE_LV(0) + esp.LEDC.SetCH1_HPOINT_HPOINT(0) + esp.LEDC.SetCH1_DUTY_DUTY(0) + esp.LEDC.SetCH1_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH1_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH1_CONF1_DUTY_INC(1) + esp.LEDC.SetCH1_CONF1_DUTY_START(1) + esp.LEDC.SetCH1_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH1_DUTY_DUTY(duty) + esp.LEDC.SetCH1_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH1_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH1_CONF1_DUTY_INC(1) + esp.LEDC.SetCH1_CONF1_DUTY_START(1) + esp.LEDC.SetCH1_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH1_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH1_CONF0_IDLE_LV(invVal) + } + case 2: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH2_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH2_CONF0_IDLE_LV(0) + esp.LEDC.SetCH2_HPOINT_HPOINT(0) + esp.LEDC.SetCH2_DUTY_DUTY(0) + esp.LEDC.SetCH2_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH2_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH2_CONF1_DUTY_INC(1) + esp.LEDC.SetCH2_CONF1_DUTY_START(1) + esp.LEDC.SetCH2_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH2_DUTY_DUTY(duty) + esp.LEDC.SetCH2_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH2_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH2_CONF1_DUTY_INC(1) + esp.LEDC.SetCH2_CONF1_DUTY_START(1) + esp.LEDC.SetCH2_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH2_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH2_CONF0_IDLE_LV(invVal) + } + case 3: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH3_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH3_CONF0_IDLE_LV(0) + esp.LEDC.SetCH3_HPOINT_HPOINT(0) + esp.LEDC.SetCH3_DUTY_DUTY(0) + esp.LEDC.SetCH3_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH3_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH3_CONF1_DUTY_INC(1) + esp.LEDC.SetCH3_CONF1_DUTY_START(1) + esp.LEDC.SetCH3_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH3_DUTY_DUTY(duty) + esp.LEDC.SetCH3_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH3_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH3_CONF1_DUTY_INC(1) + esp.LEDC.SetCH3_CONF1_DUTY_START(1) + esp.LEDC.SetCH3_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH3_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH3_CONF0_IDLE_LV(invVal) + } + case 4: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH4_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH4_CONF0_IDLE_LV(0) + esp.LEDC.SetCH4_HPOINT_HPOINT(0) + esp.LEDC.SetCH4_DUTY_DUTY(0) + esp.LEDC.SetCH4_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH4_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH4_CONF1_DUTY_INC(1) + esp.LEDC.SetCH4_CONF1_DUTY_START(1) + esp.LEDC.SetCH4_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH4_DUTY_DUTY(duty) + esp.LEDC.SetCH4_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH4_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH4_CONF1_DUTY_INC(1) + esp.LEDC.SetCH4_CONF1_DUTY_START(1) + esp.LEDC.SetCH4_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH4_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH4_CONF0_IDLE_LV(invVal) + } + case 5: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH5_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH5_CONF0_IDLE_LV(0) + esp.LEDC.SetCH5_HPOINT_HPOINT(0) + esp.LEDC.SetCH5_DUTY_DUTY(0) + esp.LEDC.SetCH5_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH5_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH5_CONF1_DUTY_INC(1) + esp.LEDC.SetCH5_CONF1_DUTY_START(1) + esp.LEDC.SetCH5_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH5_DUTY_DUTY(duty) + esp.LEDC.SetCH5_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH5_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH5_CONF1_DUTY_INC(1) + esp.LEDC.SetCH5_CONF1_DUTY_START(1) + esp.LEDC.SetCH5_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH5_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH5_CONF0_IDLE_LV(invVal) + } + case 6: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH6_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH6_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH6_CONF0_IDLE_LV(0) + esp.LEDC.SetCH6_HPOINT_HPOINT(0) + esp.LEDC.SetCH6_DUTY_DUTY(0) + esp.LEDC.SetCH6_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH6_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH6_CONF1_DUTY_INC(1) + esp.LEDC.SetCH6_CONF1_DUTY_START(1) + esp.LEDC.SetCH6_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH6_DUTY_DUTY(duty) + esp.LEDC.SetCH6_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH6_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH6_CONF1_DUTY_INC(1) + esp.LEDC.SetCH6_CONF1_DUTY_START(1) + esp.LEDC.SetCH6_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH6_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH6_CONF0_IDLE_LV(invVal) + } + case 7: + switch op { + case ledcChanOpInit: + esp.LEDC.SetCH7_CONF0_TIMER_SEL(uint32(pwm.timerNum)) + esp.LEDC.SetCH7_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH7_CONF0_IDLE_LV(0) + esp.LEDC.SetCH7_HPOINT_HPOINT(0) + esp.LEDC.SetCH7_DUTY_DUTY(0) + esp.LEDC.SetCH7_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH7_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH7_CONF1_DUTY_INC(1) + esp.LEDC.SetCH7_CONF1_DUTY_START(1) + esp.LEDC.SetCH7_CONF0_PARA_UP(1) + case ledcChanOpSetDuty: + esp.LEDC.SetCH7_DUTY_DUTY(duty) + esp.LEDC.SetCH7_CONF1_DUTY_CYCLE(1) + esp.LEDC.SetCH7_CONF1_DUTY_NUM(1) + esp.LEDC.SetCH7_CONF1_DUTY_INC(1) + esp.LEDC.SetCH7_CONF1_DUTY_START(1) + esp.LEDC.SetCH7_CONF0_SIG_OUT_EN(1) + esp.LEDC.SetCH7_CONF0_PARA_UP(1) + case ledcChanOpSetInvert: + esp.LEDC.SetCH7_CONF0_IDLE_LV(invVal) + } + } +} diff --git a/src/machine/machine_esp32xx_pwm.go b/src/machine/machine_esp32xx_pwm.go new file mode 100644 index 0000000000..03a63d60be --- /dev/null +++ b/src/machine/machine_esp32xx_pwm.go @@ -0,0 +1,179 @@ +//go:build esp32c3 || esp32s3 + +// PWM on ESP32-C3/S3 uses the LEDC (LED Control) peripheral, low-speed mode only. +// One timer drives multiple channels; each channel has its own duty, shared frequency. +// Pin routing is via GPIO matrix (SigOutBase + channel index). +// +// Channel config (chanOp) follows the hardware contract from: +// - ESP-IDF: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html +// (timer config → channel config → duty + update_duty). +// - SVD (e.g. lib/cmsis-svd/data/Espressif/esp32s3.svd): CONF0.PARA_UP "updates +// HPOINT, DUTY_START, SIG_OUT_EN, TIMER_SEL, DUTY_NUM, DUTY_CYCLE, DUTY_SCALE, +// DUTY_INC for channel and is auto-cleared by hardware"; CONF1.DUTY_START "other +// CONF1 fields take effect when this bit is set to 1". + +package machine + +import ( + "device/esp" + "errors" +) + +const ledcApbClock = 80_000000 + +const ledcDutyFracBits = 4 // DUTY register has 4 fractional bits; write value<<4 + +const ledcDividerFracBits = 8 // Clock divider register = actual_divider * 256 + +var errPWMNoChannel = errors.New("pwm: no free channel") + +type LEDCPWM struct { + SigOutBase uint32 // GPIO matrix signal index for channel 0 (e.g. 73 on S3, 45 on C3) + NumChannels uint8 + timerNum uint8 // 0–3: which LEDC timer (frequency) this PWM uses + dutyRes uint8 + configured bool + channelPin [8]Pin +} + +type ledcChanOp uint8 + +const ( + ledcChanOpInit ledcChanOp = iota // initial per-channel setup (timer, enable, HPOINT/DUTY/CONF1, PARA_UP) + ledcChanOpSetDuty // update duty and latch it (DUTY + CONF1 + PARA_UP) + ledcChanOpSetInvert // change idle level (IDLE_LV) +) + +func (pwm *LEDCPWM) Configure(config PWMConfig) error { + // Enable LEDC clock and release reset (SYSTEM perip_clk_en0 / perip_rst_en0). + esp.SYSTEM.SetPERIP_RST_EN0_LEDC_RST(1) + esp.SYSTEM.SetPERIP_CLK_EN0_LEDC_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_LEDC_RST(0) + + // LEDC global: APB clock source, enable internal clock. + esp.LEDC.SetCONF_APB_CLK_SEL(1) + esp.LEDC.SetCONF_CLK_EN(1) + + period := config.Period + if period == 0 { + period = 1_000_000 + } + freq := uint64(1e9) / period + dutyRes := uint8(10) + switch { + case freq < 100: + dutyRes = 14 + case freq < 1000: + dutyRes = 12 + case freq > 100_000: + dutyRes = 8 + } + + // Timer divider: period_ns = (2^dutyRes * divActual/256) / 80MHz * 1e9 => divReg = divActual<<8. + divActual := ledcApbClock / (uint32(freq) * (1 << dutyRes)) + if divActual == 0 { + divActual = 1 + } + divReg := divActual << ledcDividerFracBits + if divReg > 0x3ffff { + return ErrPWMPeriodTooLong + } + + // Selected timer: resolution, divider, no pause, reset then latch config with PARA_UP. + pwm.setTimerConf(dutyRes, divReg) + + pwm.dutyRes = dutyRes + pwm.configured = true + for i := range pwm.channelPin { + pwm.channelPin[i] = NoPin + } + return nil +} + +func (pwm *LEDCPWM) setTimerConf(dutyRes uint8, divReg uint32) { + t := pwm.timerNum + switch t { + case 0: + esp.LEDC.SetTIMER0_CONF_DUTY_RES(uint32(dutyRes)) + esp.LEDC.SetTIMER0_CONF_CLK_DIV(divReg) + esp.LEDC.SetTIMER0_CONF_TICK_SEL(0) + esp.LEDC.SetTIMER0_CONF_PAUSE(0) + esp.LEDC.SetTIMER0_CONF_RST(1) + esp.LEDC.SetTIMER0_CONF_RST(0) + esp.LEDC.SetTIMER0_CONF_PARA_UP(1) + case 1: + esp.LEDC.SetTIMER1_CONF_DUTY_RES(uint32(dutyRes)) + esp.LEDC.SetTIMER1_CONF_CLK_DIV(divReg) + esp.LEDC.SetTIMER1_CONF_TICK_SEL(0) + esp.LEDC.SetTIMER1_CONF_PAUSE(0) + esp.LEDC.SetTIMER1_CONF_RST(1) + esp.LEDC.SetTIMER1_CONF_RST(0) + esp.LEDC.SetTIMER1_CONF_PARA_UP(1) + case 2: + esp.LEDC.SetTIMER2_CONF_DUTY_RES(uint32(dutyRes)) + esp.LEDC.SetTIMER2_CONF_CLK_DIV(divReg) + esp.LEDC.SetTIMER2_CONF_TICK_SEL(0) + esp.LEDC.SetTIMER2_CONF_PAUSE(0) + esp.LEDC.SetTIMER2_CONF_RST(1) + esp.LEDC.SetTIMER2_CONF_RST(0) + esp.LEDC.SetTIMER2_CONF_PARA_UP(1) + case 3: + esp.LEDC.SetTIMER3_CONF_DUTY_RES(uint32(dutyRes)) + esp.LEDC.SetTIMER3_CONF_CLK_DIV(divReg) + esp.LEDC.SetTIMER3_CONF_TICK_SEL(0) + esp.LEDC.SetTIMER3_CONF_PAUSE(0) + esp.LEDC.SetTIMER3_CONF_RST(1) + esp.LEDC.SetTIMER3_CONF_RST(0) + esp.LEDC.SetTIMER3_CONF_PARA_UP(1) + } +} + +func (pwm *LEDCPWM) Channel(pin Pin) (uint8, error) { + if !pwm.configured { + return 0, errors.New("pwm: not configured") + } + if pin == NoPin { + return 0, ErrInvalidOutputPin + } + var ch uint8 + for ch = 0; ch < pwm.NumChannels; ch++ { + if pwm.channelPin[ch] == NoPin { + break + } + } + if ch >= pwm.NumChannels { + return 0, errPWMNoChannel + } + + pwm.channelPin[ch] = pin + signal := pwm.SigOutBase + uint32(ch) + pin.configure(PinConfig{Mode: PinOutput}, signal) // GPIO matrix: pin <- LEDC_LS_SIG_OUTn + pwm.chanOp(ch, ledcChanOpInit, 0, false) + return ch, nil +} + +func (pwm *LEDCPWM) Set(channel uint8, value uint32) { + if channel >= pwm.NumChannels { + return + } + top := uint32(1< top { + value = top + } + dutyVal := value << ledcDutyFracBits + pwm.chanOp(channel, ledcChanOpSetDuty, dutyVal, false) +} + +func (pwm *LEDCPWM) Top() uint32 { + if !pwm.configured { + return 0 + } + return uint32(1<= pwm.NumChannels { + return + } + pwm.chanOp(channel, ledcChanOpSetInvert, 0, inverting) +} From 72e7040329788366a3cff563c2c1688cedd6e8d8 Mon Sep 17 00:00:00 2001 From: knieriem Date: Wed, 25 Feb 2026 18:05:31 +0100 Subject: [PATCH 28/81] update tools/gen-device-svd w.r.t. recent changes to the structure of stm32-svd files (#5212) * tools/gen-device-svd: orderPeripherals: prevent skipping base peripherals derived by name Recent SVDs from stm32-rs, like the one for stm32u595, define derivedFrom attributes that do not refer to a peripheral group name, but to a peripheral name. For instance, the peripheral I2C5 may be derived from "I2C1", not from "I2C". The previous algorithm records, in case the group name is non-empty, only the group name in the knownBasePeripherals map, not the name of the peripheral itself. So in case of the base peripheral I2C1 with group name I2C: although the peripheral gets added to the sorted list, it would be added to knownBasePeripherals with the group name "I2C" as key, not with "I2C1". A following peripheral, I2C5, derived from I2C1, with an empty group name, would be recorded as known with key "I2C5", but omitted from the sorted list, because "I2C1" is not recognized as known. The following peripheral SEC_I2C5, derived from I2C5, with empty group name, would be added to both the knownPeripherals map and the sorted list. So if, later, the sorted list is examined, it would find SEC_I2C5 earlier than its base peripheral I2C5, which would be missing from "peripheralDict", resulting in a nil pointer access. This patch makes sure that, to stay with the example, that "I2C1" is recorded as known too (not only the group name "I2C"), so that "I2C5" won't be skipped anymore, preventing the program from crashing. * tools/gen-device-svd: orderPeripherals: ensure ordered content of missingBasePeripherals After the first run, missingBasePeripherals may contain peripherals with dependencies that are not guaranteed to be in proper order. This change implements additional loop runs that try to reduce the size of the missingBasePeripherals as far as possible. [With recent SVDs from stm32-rs this change will not produce different results, though (since these source files contain already properly ordered peripherals).] * tools/gen-device-svd: Register: move dim array decoding to utility type dimArray This allows encoding of dim increment and array indices to be re-used by other elements supporting dim arrays. This change just restructures parts of register specific code, it does not change the output of the program. * tools/gen-device-svd: parseBitfields: support field dim arrays Patched SVD files from stm32-rs recently contain many fields with dim array parameters and names containing %s (like "CC%sIF"). This change adjusts parseBitfields so that these field elements get resolved. * tools/gen-device-svd: SVDField: allow multiple enumeratedValues In recent patched SVD files from stm32-rs there may be two enumeratedValues elements per SVDField, not just one. The SVD specification allows up to two entries (they may be used to define different enums for read and write access). This change extends SVDField and parseBitfields so that two enumeratedValues are processed like a single one. * tools/gen-device-svd: orderPeripherals: sort peripherals of same group with larger number of registers/bitfields first In group "TIM" there may be general purpose timers like TIM16 and advanced timers like TIM1. The advanced peripheral may contain a larger number of registers than the general purpose ones. TIM1 may contain CCR1..CCR4, SMCR and OR1, while TIM16 only knows about CCR1. Unfortunately in some SVDs, like the one for stm32g031, TIM16 is defined before TIM1. Since register and bitfield constants are generated taking only the first peripheral of a group into account, the resulting .go file may lack definitions for e.g. CCR2..CCR4, SMCR, and OR1. This change adjusts orderPeripherals so that, to stay with the example, a peripheral like TIM1 will be moved in front of TIM16, resulting in an output file containing the larger set of definitions. * tools/gen-device-svd: SVDEnumeration: support isDefault Recent SVD files created by stm32-rs use "isDefault" in enumeratedValue elements for purposes like the Div1 enum for clock prescaler registers without specifying a specific value. Previously, these enumeratedValues would be skipped because of the enumEl.Value == 0 condition, and the corresponding const definitions like "RCC_CFGR2_PPRE2_Div1 = 0x0" would be missing from the resulting .go files, so existing code relying on these constants would not compile anymore. This change adds a utility type enumDefaultResolver that helps finding an actual value that is unused by the enumeratedValues that are defined for the field. More examples for values marked as "isDefault", along with their resolved values: DAC_CR_WAVE1_Triangle => 2 IWDG_PR_PR_DivideBy256 => 6 DAC_CR_MAMP2_Amp4095 => 0xb * tools/gen-device-svd: support derivedFrom attribute at field level This ensures that some more constants are included in the .go files that would otherwise be skipped (like e.g. ADC_SMPR2_SMP1_Cycles* of some STM32 devices), which prevented compilation of some programs. To avoid extending a lot of func argument lists, and since there is no context.Context in use yet, this change introduces a global derivationContext. * tools/gen-device-svd: tweak: stm32: ensure USART_ISR_TXE/TXFNF are present * tools/gen-device-svd: stm32: ensure CCMR*_Output alternate registers are sorted first * tools/gen-device-svd: stm32: add IWDG peripheral alias if SVD defines IWDG1 --- tools/gen-device-svd/gen-device-svd.go | 703 ++++++++++++++++++------- tools/gen-device-svd/tweak.go | 124 +++++ 2 files changed, 648 insertions(+), 179 deletions(-) create mode 100644 tools/gen-device-svd/tweak.go diff --git a/tools/gen-device-svd/gen-device-svd.go b/tools/gen-device-svd/gen-device-svd.go index 1a3d539621..ef9dedff97 100755 --- a/tools/gen-device-svd/gen-device-svd.go +++ b/tools/gen-device-svd/gen-device-svd.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strconv" "strings" @@ -18,6 +19,7 @@ import ( ) var validName = regexp.MustCompile("^[a-zA-Z0-9_]+$") +var validDimableName = regexp.MustCompile(`^((%s)|(%s)[_A-Za-z]{1}[_A-Za-z0-9]*)|([_A-Za-z]{1}[_A-Za-z0-9]*(\[%s\])?)|([_A-Za-z]{1}[_A-Za-z0-9]*(%s)?[_A-Za-z0-9]*)$`) var enumBitSpecifier = regexp.MustCompile("^#x*[01]+[01x]*$") type SVDFile struct { @@ -60,22 +62,18 @@ type SVDRegister struct { } type SVDField struct { - Name string `xml:"name"` - Description string `xml:"description"` - Lsb *uint32 `xml:"lsb"` - Msb *uint32 `xml:"msb"` - BitOffset *uint32 `xml:"bitOffset"` - BitWidth *uint32 `xml:"bitWidth"` - BitRange *string `xml:"bitRange"` - EnumeratedValues struct { - DerivedFrom string `xml:"derivedFrom,attr"` - Name string `xml:"name"` - EnumeratedValue []struct { - Name string `xml:"name"` - Description string `xml:"description"` - Value string `xml:"value"` - } `xml:"enumeratedValue"` - } `xml:"enumeratedValues"` + DerivedFrom string `xml:"derivedFrom,attr"` + Name string `xml:"name"` + Description string `xml:"description"` + Dim *string `xml:"dim"` + DimIndex *string `xml:"dimIndex"` + DimIncrement string `xml:"dimIncrement"` + Lsb *uint32 `xml:"lsb"` + Msb *uint32 `xml:"msb"` + BitOffset *uint32 `xml:"bitOffset"` + BitWidth *uint32 `xml:"bitWidth"` + BitRange *string `xml:"bitRange"` + EnumeratedValues []SVDEnumeration `xml:"enumeratedValues"` } type SVDCluster struct { @@ -89,6 +87,17 @@ type SVDCluster struct { AddressOffset string `xml:"addressOffset"` } +type SVDEnumeration struct { + DerivedFrom string `xml:"derivedFrom,attr"` + Name string `xml:"name"` + EnumeratedValue []struct { + Name string `xml:"name"` + Description string `xml:"description"` + Value string `xml:"value"` + IsDefault bool `xml:"isDefault"` + } `xml:"enumeratedValue"` +} + type Device struct { Metadata *Metadata Interrupts []*Interrupt @@ -120,6 +129,7 @@ type Interrupt struct { type Peripheral struct { Name string + Alias string GroupName string BaseAddress uint64 Description string @@ -173,23 +183,36 @@ func splitLine(s string) []string { // Replace characters that are not allowed in a symbol name with a '_'. This is // useful to be able to process SVD files with errors. func cleanName(text string) string { - if !validName.MatchString(text) { - result := make([]rune, 0, len(text)) - for _, c := range text { + return cleanIdentifier(text, validName) +} + +func cleanDimableName(text string) string { + return cleanIdentifier(text, validDimableName) +} + +func cleanIdentifier(text string, valid *regexp.Regexp) string { + text = cleanString(text, valid) + if len(text) != 0 && (text[0] >= '0' && text[0] <= '9') { + // Identifiers may not start with a number. + // Add an underscore instead. + text = "_" + text + } + return text +} + +func cleanString(s string, valid *regexp.Regexp) string { + if !valid.MatchString(s) { + result := make([]rune, 0, len(s)) + for _, c := range s { if validName.MatchString(string(c)) { result = append(result, c) } else { result = append(result, '_') } } - text = string(result) + s = string(result) } - if len(text) != 0 && (text[0] >= '0' && text[0] <= '9') { - // Identifiers may not start with a number. - // Add an underscore instead. - text = "_" + text - } - return text + return s } func processSubCluster(p *Peripheral, cluster *SVDCluster, clusterOffset uint64, clusterName string, peripheralDict map[string]*Peripheral) []*Peripheral { @@ -353,6 +376,7 @@ func readSVD(path, sourceURL string) (*Device, error) { // comes later in the file. To make sure this works, sort the peripherals if // needed. orderedPeripherals := orderPeripherals(device.Peripherals) + globalDerivationCtx.peripherals = orderedPeripherals for _, periphEl := range orderedPeripherals { description := formatText(periphEl.Description) @@ -490,21 +514,42 @@ func orderPeripherals(input []SVDPeripheral) []*SVDPeripheral { var sortedPeripherals []*SVDPeripheral var missingBasePeripherals []*SVDPeripheral knownBasePeripherals := map[string]struct{}{} - for i := range input { - p := &input[i] + + tryProcess := func(p *SVDPeripheral) { groupName := p.GroupName - if groupName == "" { - groupName = p.Name + if groupName != "" { + knownBasePeripherals[groupName] = struct{}{} } - knownBasePeripherals[groupName] = struct{}{} + knownBasePeripherals[p.Name] = struct{}{} if p.DerivedFrom != "" { if _, ok := knownBasePeripherals[p.DerivedFrom]; !ok { missingBasePeripherals = append(missingBasePeripherals, p) - continue + return } } sortedPeripherals = append(sortedPeripherals, p) } + for i := range input { + tryProcess(&input[i]) + } + orderPeripheralsByNumBitfields(sortedPeripherals) + + // missingBasePeripherals may still contain unordered entries; + // repeat the process until missingBasePeripheral does not change anymore. + prevNumPending := 0 + for { + pending := missingBasePeripherals + if len(pending) == prevNumPending { + break + } + // reuse the same slice as input and for keeping track of + // missing base periphal + missingBasePeripherals = missingBasePeripherals[:0] + for _, p := range pending { + tryProcess(p) + } + prevNumPending = len(pending) + } // Let's hope all base peripherals are now included. sortedPeripherals = append(sortedPeripherals, missingBasePeripherals...) @@ -512,6 +557,51 @@ func orderPeripherals(input []SVDPeripheral) []*SVDPeripheral { return sortedPeripherals } +func orderPeripheralsByNumBitfields(list []*SVDPeripheral) { + seenGroup := make(map[string]struct{}) + for i, p := range list { + groupName := p.GroupName + if groupName == "" || p.DerivedFrom != "" { + continue + } + if _, ok := seenGroup[groupName]; ok { + continue + } + iMax, nMax := -1, p.bitfieldCount() + for j, p2 := range list[i+1:] { + if p2.GroupName != groupName || p2.DerivedFrom != "" { + continue + } + if n2 := p2.bitfieldCount(); n2 > nMax { + iMax = i + 1 + j + nMax = n2 + } + } + if iMax != -1 { + pMax := list[iMax] + // swap peripherals + copy(list[i+1:iMax+1], list[i:iMax]) + list[i] = pMax + seenGroup[groupName] = struct{}{} + } + } +} + +func (p *SVDPeripheral) bitfieldCount() int { + n := 0 + for _, r := range p.Registers { + for _, f := range r.Fields { + dim := decodeDim(f.Dim) + if dim > 0 { + n += dim + } else { + n++ + } + } + } + return n +} + func addInterrupt(interrupts map[string]*Interrupt, name, interruptName string, index int, description string) { if _, ok := interrupts[name]; ok { if interrupts[name].Value != index { @@ -544,14 +634,23 @@ func addInterrupt(interrupts map[string]*Interrupt, name, interruptName string, func parseBitfields(groupName, regName string, fieldEls []*SVDField, bitfieldPrefix string) ([]Constant, []Bitfield) { var fields []Constant var bitfields []Bitfield + var enumDefault enumDefaultResolver enumSeen := map[string]int64{} for _, fieldEl := range fieldEls { + + if fieldEl.DerivedFrom != "" { + err := globalDerivationCtx.deriveField(fieldEl, fieldEls) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to derive field %q from %q: %v\n", fieldEl.Name, fieldEl.DerivedFrom, err.Error()) + } + } + // Some bitfields (like the STM32H7x7) contain invalid bitfield // names like "CNT[31]". Replace invalid characters with "_" when // needed. - fieldName := cleanName(fieldEl.Name) - if !unicode.IsUpper(rune(fieldName[0])) && !unicode.IsDigit(rune(fieldName[0])) { - fieldName = strings.ToUpper(fieldName) + fieldNameTpl := cleanDimableName(fieldEl.Name) + if !unicode.IsUpper(rune(fieldNameTpl[0])) && !unicode.IsDigit(rune(fieldNameTpl[0])) { + fieldNameTpl = strings.ReplaceAll(strings.ToUpper(fieldNameTpl), "%S", "%s") } // Find the lsb/msb that is encoded in various ways. @@ -581,129 +680,332 @@ func parseBitfields(groupName, regName string, fieldEls []*SVDField, bitfieldPre msb = uint32(m) } else { // this is an error. what to do? - fmt.Fprintln(os.Stderr, "unable to find lsb/msb in field:", fieldName) + fmt.Fprintln(os.Stderr, "unable to find lsb/msb in field:", fieldNameTpl) continue } - // The enumerated values can be the same as another field, so to avoid - // duplication SVD files can simply refer to another set of enumerated - // values in the same register. - // See: https://www.keil.com/pack/doc/CMSIS/SVD/html/elem_registers.html#elem_enumeratedValues - enumeratedValues := fieldEl.EnumeratedValues - if enumeratedValues.DerivedFrom != "" { - parts := strings.Split(enumeratedValues.DerivedFrom, ".") - if len(parts) == 1 { - found := false - for _, otherFieldEl := range fieldEls { - if otherFieldEl.EnumeratedValues.Name == parts[0] { - found = true - enumeratedValues = otherFieldEl.EnumeratedValues + da := decodeDimArray(fieldEl.Dim, fieldEl.DimIndex, fieldEl.DimIncrement, "field", fieldNameTpl) + da.rangeElems(func(ia int, _ uint32) bool { + if da != nil { + lsb += da.incr + msb += da.incr + } + fieldName := da.replace(fieldNameTpl, ia) + + // The enumerated values can be the same as another field, so to avoid + // duplication SVD files can simply refer to another set of enumerated + // values in the same register. + // See: https://www.keil.com/pack/doc/CMSIS/SVD/html/elem_registers.html#elem_enumeratedValues + for i := range fieldEl.EnumeratedValues { + enumeratedValues := &fieldEl.EnumeratedValues[i] + if enumeratedValues.DerivedFrom != "" { + parts := strings.Split(enumeratedValues.DerivedFrom, ".") + if len(parts) == 1 { + found := false + for _, otherFieldEl := range fieldEls { + for i := range otherFieldEl.EnumeratedValues { + otherEnum := &otherFieldEl.EnumeratedValues[i] + if otherEnum.Name == parts[0] { + found = true + *enumeratedValues = *otherEnum + } + } + } + if !found { + fmt.Fprintf(os.Stderr, "Warning: could not find enumeratedValue.derivedFrom of %s for register field %s\n", enumeratedValues.DerivedFrom, fieldName) + } + } else { + // The derivedFrom attribute may also point to enumerated values + // in other registers and even peripherals, but this feature + // isn't often used in SVD files. + fmt.Fprintf(os.Stderr, "TODO: enumeratedValue.derivedFrom to a different register: %s\n", enumeratedValues.DerivedFrom) } } - if !found { - fmt.Fprintf(os.Stderr, "Warning: could not find enumeratedValue.derivedFrom of %s for register field %s\n", enumeratedValues.DerivedFrom, fieldName) - } - } else { - // The derivedFrom attribute may also point to enumerated values - // in other registers and even peripherals, but this feature - // isn't often used in SVD files. - fmt.Fprintf(os.Stderr, "TODO: enumeratedValue.derivedFrom to a different register: %s\n", enumeratedValues.DerivedFrom) } - } - bitfields = append(bitfields, Bitfield{ - Name: fieldName, - Offset: lsb, - Mask: (0xffffffff >> (31 - (msb - lsb))) << lsb, - }) - fields = append(fields, Constant{ - Name: fmt.Sprintf("%s_%s%s_%s_Pos", groupName, bitfieldPrefix, regName, fieldName), - Description: fmt.Sprintf("Position of %s field.", fieldName), - Value: uint64(lsb), - }) - fields = append(fields, Constant{ - Name: fmt.Sprintf("%s_%s%s_%s_Msk", groupName, bitfieldPrefix, regName, fieldName), - Description: fmt.Sprintf("Bit mask of %s field.", fieldName), - Value: (0xffffffffffffffff >> (63 - (msb - lsb))) << lsb, - }) - if lsb == msb { // single bit + bitfields = append(bitfields, Bitfield{ + Name: fieldName, + Offset: lsb, + Mask: (0xffffffff >> (31 - (msb - lsb))) << lsb, + }) fields = append(fields, Constant{ - Name: fmt.Sprintf("%s_%s%s_%s", groupName, bitfieldPrefix, regName, fieldName), - Description: fmt.Sprintf("Bit %s.", fieldName), - Value: 1 << lsb, + Name: fmt.Sprintf("%s_%s%s_%s_Pos", groupName, bitfieldPrefix, regName, fieldName), + Description: fmt.Sprintf("Position of %s field.", fieldName), + Value: uint64(lsb), }) - } - for _, enumEl := range enumeratedValues.EnumeratedValue { - enumName := enumEl.Name - // Renesas has enum without actual values that we have to skip - if enumEl.Value == "" { - continue + fields = append(fields, Constant{ + Name: fmt.Sprintf("%s_%s%s_%s_Msk", groupName, bitfieldPrefix, regName, fieldName), + Description: fmt.Sprintf("Bit mask of %s field.", fieldName), + Value: (0xffffffffffffffff >> (63 - (msb - lsb))) << lsb, + }) + if lsb == msb { // single bit + fields = append(fields, Constant{ + Name: fmt.Sprintf("%s_%s%s_%s", groupName, bitfieldPrefix, regName, fieldName), + Description: fmt.Sprintf("Bit %s.", fieldName), + Value: 1 << lsb, + }) } + for i := range fieldEl.EnumeratedValues { + enumDefault.reset(1<<(msb+1-lsb) - 1) + fields0Pos := len(fields) + for _, enumEl := range fieldEl.EnumeratedValues[i].EnumeratedValue { + enumName := enumEl.Name + + if strings.EqualFold(enumName, "reserved") || !validName.MatchString(enumName) { + continue + } + if !unicode.IsUpper(rune(enumName[0])) && !unicode.IsDigit(rune(enumName[0])) { + enumName = strings.ToUpper(enumName) + } + enumName = fmt.Sprintf("%s_%s%s_%s_%s", groupName, bitfieldPrefix, regName, fieldName, enumName) + enumDescription := formatText(enumEl.Description) + + if enumEl.IsDefault { + enumDefault.setDefaultAction(func(value uint64) { + if value == 0 { + // put zero value in front of other constants + fields = slices.Insert(fields, fields0Pos, Constant{}) + appendConstant(fields[:fields0Pos], enumName, enumDescription, value, enumSeen) + } else { + fields = appendConstant(fields, enumName, enumDescription, value, enumSeen) + } + }) + continue + } - if strings.EqualFold(enumName, "reserved") || !validName.MatchString(enumName) { - continue + // Renesas has enum without actual values that we have to skip + if enumEl.Value == "" { + continue + } + + var enumValue uint64 + var err error + if strings.HasPrefix(enumEl.Value, "0b") { + val := strings.TrimPrefix(enumEl.Value, "0b") + enumValue, err = strconv.ParseUint(val, 2, 64) + } else { + enumValue, err = strconv.ParseUint(enumEl.Value, 0, 64) + } + if err != nil { + if enumBitSpecifier.MatchString(enumEl.Value) { + // NXP and Renesas SVDs use the form #xx1x, #x0xx, etc for values + enumValue, err = strconv.ParseUint(strings.ReplaceAll(enumEl.Value[1:], "x", "0"), 2, 64) + if err != nil { + panic(err) + } + } else { + panic(err) + } + } + enumDefault.collectValue(enumValue) + fields = appendConstant(fields, enumName, enumDescription, enumValue, enumSeen) + } + enumDefault.resolve() } - if !unicode.IsUpper(rune(enumName[0])) && !unicode.IsDigit(rune(enumName[0])) { - enumName = strings.ToUpper(enumName) + return true + }) + } + return fields, bitfields +} + +var globalDerivationCtx derivationContext + +type derivationContext struct { + peripherals []*SVDPeripheral +} + +func (ctx *derivationContext) deriveField(fieldEl *SVDField, localFieldEls []*SVDField) error { + from := fieldEl.DerivedFrom + parts := strings.Split(from, ".") + srcName := parts[0] + var srcFieldEl *SVDField + switch len(parts) { + case 3, 4: + src, err := ctx.lookupGlobal(parts) + if err != nil { + return err + } + srcFieldEl = src + case 1: + // resolve locally, in current register + for _, f := range localFieldEls { + if f == fieldEl { + continue } - enumDescription := formatText(enumEl.Description) - var enumValue uint64 - var err error - if strings.HasPrefix(enumEl.Value, "0b") { - val := strings.TrimPrefix(enumEl.Value, "0b") - enumValue, err = strconv.ParseUint(val, 2, 64) - } else { - enumValue, err = strconv.ParseUint(enumEl.Value, 0, 64) + if f.Name == srcName { + srcFieldEl = f + break } - if err != nil { - if enumBitSpecifier.MatchString(enumEl.Value) { - // NXP and Renesas SVDs use the form #xx1x, #x0xx, etc for values - enumValue, err = strconv.ParseUint(strings.ReplaceAll(enumEl.Value[1:], "x", "0"), 2, 64) - if err != nil { - panic(err) + } + if srcFieldEl == nil { + return fmt.Errorf("not found") + } + default: + return fmt.Errorf("cannot decode source path") + } + + // copy enumeratedValues from source to current field + if fieldEl.DimIndex == nil && strings.Contains(fieldEl.Name, "%s") { + fieldEl.DimIndex = srcFieldEl.DimIndex + fieldEl.DimIncrement = srcFieldEl.DimIncrement + fieldEl.Dim = srcFieldEl.Dim + } + if fieldEl.Description == "" { + fieldEl.Description = srcFieldEl.Description + } + if fieldEl.BitWidth == nil { + fieldEl.BitWidth = srcFieldEl.BitWidth + } + if fieldEl.BitOffset == nil { + fieldEl.BitOffset = srcFieldEl.BitOffset + } + if fieldEl.BitRange == nil { + fieldEl.BitRange = srcFieldEl.BitRange + } + + fieldEl.EnumeratedValues = srcFieldEl.EnumeratedValues + return nil +} + +func (ctx *derivationContext) lookupGlobal(path []string) (*SVDField, error) { + curPath := path[:1] + for _, p := range ctx.peripherals { + if p.Name == path[0] { + if len(path) == 4 { + curPath = path[:2] + for _, c := range p.Clusters { + if c.Name == path[1] { + return ctx.lookupFieldInRegs(path[2:], c.Registers, curPath) } - } else { - panic(err) } + return nil, fmt.Errorf("cluster not found: %q", path[2]) + } - enumName = fmt.Sprintf("%s_%s%s_%s_%s", groupName, bitfieldPrefix, regName, fieldName, enumName) - - // Avoid duplicate values. Duplicate names with the same value are - // allowed, but the same name with a different value is not. Instead - // of trying to work around those cases, remove the value entirely - // as there is probably not one correct answer in such a case. - // For example, SVD files from NXP have enums limited to 20 - // characters, leading to lots of duplicates when these enum names - // are long. Nothing here can really fix those cases. - previousEnumValue, seenBefore := enumSeen[enumName] - if seenBefore { - if previousEnumValue < 0 { - // There was a mismatch before, ignore all equally named fields. - continue + return ctx.lookupFieldInRegs(path[1:], p.Registers, curPath) + } + } + return nil, fmt.Errorf("peripheral not found: %s", path[0]) +} + +func (ctx *derivationContext) lookupFieldInRegs(path []string, registers []*SVDRegister, curPath []string) (*SVDField, error) { + curPath = curPath[:len(curPath)+1] + for _, r := range registers { + if r.Name == path[0] { + curPath = curPath[:len(curPath)+1] + for _, f := range r.Fields { + if f.Name == path[1] { + return f, nil } - if int64(enumValue) != previousEnumValue { - // There is a mismatch. Mark it as such, and remove the - // existing enum bitfield value. - enumSeen[enumName] = -1 - for i, field := range fields { - if field.Name == enumName { - fields = append(fields[:i], fields[i+1:]...) - break - } - } + } + return nil, fmt.Errorf("field not found: %q", strings.Join(curPath, ".")) + } + } + return nil, fmt.Errorf("register not found: %q", strings.Join(curPath, ".")) +} + +func appendConstant(fields []Constant, enumName, enumDescription string, enumValue uint64, enumSeen map[string]int64) []Constant { + // Avoid duplicate values. Duplicate names with the same value are + // allowed, but the same name with a different value is not. Instead + // of trying to work around those cases, remove the value entirely + // as there is probably not one correct answer in such a case. + // For example, SVD files from NXP have enums limited to 20 + // characters, leading to lots of duplicates when these enum names + // are long. Nothing here can really fix those cases. + previousEnumValue, seenBefore := enumSeen[enumName] + if seenBefore { + if previousEnumValue < 0 { + // There was a mismatch before, ignore all equally named fields. + return fields + } + if int64(enumValue) != previousEnumValue { + // There is a mismatch. Mark it as such, and remove the + // existing enum bitfield value. + enumSeen[enumName] = -1 + for i, field := range fields { + if field.Name == enumName { + fields = append(fields[:i], fields[i+1:]...) + break } - continue } - enumSeen[enumName] = int64(enumValue) + } + return fields + } + enumSeen[enumName] = int64(enumValue) - fields = append(fields, Constant{ - Name: enumName, - Description: enumDescription, - Value: enumValue, - }) + fields = append(fields, Constant{ + Name: enumName, + Description: enumDescription, + Value: enumValue, + }) + return fields +} + +// enumDefaultResolver helps determine the actual numeric value for an +// enumeratedValue marked as the default (i.e., where "isDefault" is set). +// +// Some SVD files use "isDefault" to indicate a fallback value (e.g., Div1 in +// clock prescaler registers) without specifying the exact value when it's not +// critical. This type is used to collect all defined enumValues, and once +// collection is complete, derive a sensible default value that does not conflict +// with any explicitly defined ones. +// +// Typically, it prefers zero as a default if available; otherwise, it will +// choose a suitable unused value below the field's maximum. +type enumDefaultResolver struct { + values []uint64 + maxValue uint64 + handleDefault func(value uint64) +} + +func (dr *enumDefaultResolver) reset(maxValue uint64) { + dr.values = dr.values[:0] + dr.maxValue = maxValue + dr.handleDefault = nil +} + +func (dr *enumDefaultResolver) setDefaultAction(action func(v uint64)) { + dr.handleDefault = action +} + +func (dr *enumDefaultResolver) collectValue(value uint64) { + dr.values = append(dr.values, value) +} + +// resolve tries to find an actual value for the enumerated Value +// marked as default. +func (dr *enumDefaultResolver) resolve() { + if dr.handleDefault == nil { + return + } + list := dr.values + n := len(list) + if n == 0 { + return + } + slices.Sort(list) + + var value uint64 + // try to use zero as default value + if list[0] == 0 { + // not available, now try the highest value +1 + largest := list[n-1] + if largest < dr.maxValue { + value = largest + 1 + } else { + value = 1 + // not available, now lookup the first free value + for _, enumValue := range list[1:] { + if value < enumValue { + break + } + value = enumValue + 1 + if value == dr.maxValue { + return + } + } } } - return fields, bitfields + dr.handleDefault(value) } type Register struct { @@ -739,38 +1041,57 @@ func (r *Register) address() uint64 { } func (r *Register) dim() int { - if r.element.Dim == nil { + return decodeDim(r.element.Dim) +} + +func decodeDim(s *string) int { + if s == nil { return -1 // no dim elements } - dim, err := strconv.ParseInt(*r.element.Dim, 0, 32) + dim, err := strconv.ParseInt(*s, 0, 32) if err != nil { panic(err) } return int(dim) } -func (r *Register) dimIndex() []string { +type dimArray struct { + dim int + idx []string + incr uint32 +} + +func decodeDimArray(dimSpec, dimIndex *string, dimIncr, elType, elName string) *dimArray { + dim := decodeDim(dimSpec) + if dim <= 0 { + return nil + } + a := new(dimArray) + a.dim = dim + defer func() { if err := recover(); err != nil { - fmt.Println("register", r.name()) + fmt.Println(elType, elName) panic(err) } }() - dim := r.dim() - if r.element.DimIndex == nil { - if dim <= 0 { - return nil - } + incr, err := strconv.ParseUint(dimIncr, 0, 32) + if err != nil { + panic(err) + } + a.incr = uint32(incr) + if dimIndex == nil { idx := make([]string, dim) for i := range idx { idx[i] = strconv.FormatInt(int64(i), 10) } - return idx + a.idx = idx + return a } - t := strings.Split(*r.element.DimIndex, "-") + t := strings.Split(*dimIndex, "-") if len(t) == 2 { // renesas uses hex letters e.g. A-B if strings.Contains("ABCDEFabcdef", t[0]) { @@ -797,17 +1118,40 @@ func (r *Register) dimIndex() []string { for i := x; i <= y; i++ { idx[i-x] = strconv.FormatInt(i, 10) } - return idx + a.idx = idx + return a } else if len(t) > 2 { panic("invalid dimIndex") } - s := strings.Split(*r.element.DimIndex, ",") + s := strings.Split(*dimIndex, ",") if len(s) != dim { panic("invalid dimIndex") } + a.idx = s + return a +} - return s +func (da *dimArray) replace(s string, i int) string { + if da == nil { + return s + } + if i >= len(da.idx) { + return s + } + return strings.ReplaceAll(s, "%s", da.idx[i]) +} + +func (da *dimArray) rangeElems(yield func(i int, incr uint32) bool) { + if da == nil { + yield(0, 0) + return + } + for i := range da.dim { + if !yield(i, uint32(i)*da.incr) { + return + } + } } func (r *Register) size() int { @@ -823,37 +1167,32 @@ func (r *Register) size() int { func parseRegister(groupName string, regEl *SVDRegister, baseAddress uint64, bitfieldPrefix string) []*PeripheralField { reg := NewRegister(regEl, baseAddress) - - if reg.dim() != -1 { - dimIncrement, err := strconv.ParseUint(regEl.DimIncrement, 0, 32) - if err != nil { - panic(err) + name := reg.name() + da := decodeDimArray(regEl.Dim, regEl.DimIndex, regEl.DimIncrement, "register", name) + if da != nil && strings.Contains(name, "%s") { + // a "spaced array" of registers, special processing required + // we need to generate a separate register for each "element" + var results []*PeripheralField + shortName := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, "_%s", ""), "%s", "")) + for i := range da.idx { + regAddress := reg.address() + (uint64(i) * uint64(da.incr)) + results = append(results, &PeripheralField{ + Name: strings.ToUpper(da.replace(name, i)), + Address: regAddress, + Description: reg.description(), + Array: -1, + ElementSize: reg.size(), + ShortName: shortName, + }) } - if strings.Contains(reg.name(), "%s") { - // a "spaced array" of registers, special processing required - // we need to generate a separate register for each "element" - var results []*PeripheralField - shortName := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(reg.name(), "_%s", ""), "%s", "")) - for i, j := range reg.dimIndex() { - regAddress := reg.address() + (uint64(i) * dimIncrement) - results = append(results, &PeripheralField{ - Name: strings.ToUpper(strings.ReplaceAll(reg.name(), "%s", j)), - Address: regAddress, - Description: reg.description(), - Array: -1, - ElementSize: reg.size(), - ShortName: shortName, - }) - } - // set first result bitfield - results[0].Constants, results[0].Bitfields = parseBitfields(groupName, shortName, regEl.Fields, bitfieldPrefix) - results[0].HasBitfields = len(results[0].Bitfields) > 0 - for i := 1; i < len(results); i++ { - results[i].Bitfields = results[0].Bitfields - results[i].HasBitfields = results[0].HasBitfields - } - return results + // set first result bitfield + results[0].Constants, results[0].Bitfields = parseBitfields(groupName, shortName, regEl.Fields, bitfieldPrefix) + results[0].HasBitfields = len(results[0].Bitfields) > 0 + for i := 1; i < len(results); i++ { + results[i].Bitfields = results[0].Bitfields + results[i].HasBitfields = results[0].HasBitfields } + return results } regName := reg.name() if !unicode.IsUpper(rune(regName[0])) && !unicode.IsDigit(rune(regName[0])) { @@ -979,14 +1318,20 @@ var ( {{- end}} {{- end}} {{.Name}} = (*{{.GroupName}}_Type)(unsafe.Pointer(uintptr(0x{{printf "%x" .BaseAddress}}))) + {{- if .Alias}} + {{.Alias}} = {{.Name}} + {{- end}} {{- "\n"}} {{- end}} ) `)) + pkgName := filepath.Base(strings.TrimRight(outdir, "/")) + tweakDevice(device, pkgName) + err = t.Execute(w, map[string]interface{}{ "device": device, - "pkgName": filepath.Base(strings.TrimRight(outdir, "/")), + "pkgName": pkgName, "interruptMax": maxInterruptValue, "interruptSystem": interruptSystem, "interruptHandlers": interruptHandlers, diff --git a/tools/gen-device-svd/tweak.go b/tools/gen-device-svd/tweak.go new file mode 100644 index 0000000000..8ee505da14 --- /dev/null +++ b/tools/gen-device-svd/tweak.go @@ -0,0 +1,124 @@ +package main + +import ( + "slices" + "strings" +) + +func tweakDevice(d *Device, pkgName string) { + if pkgName != "stm32" { + // no-op for device types that do not need tweaks + return + } + + // Source file machine_stm32_iwdg.go relies on the presence of + // a register IWDG. On some devices, though, like the h723, + // there are two registers, IWDG1 and IWDG2. In this case we + // define an alias IWDG for IWDG1. + addUnnumberedAlias(d, "IWDG", "IWDG1") + + for _, p := range d.Peripherals { + switch p.GroupName { + case "TIM": + // SVDs like stm32l4r5.svd define CCMR*_Input and _Output + // alternate registers, with _Input sorted before _Output. + // This would result in the _Output fields missing from the + // TIM_type struct definition, hence compilation would fail. + // Therefore we adjust the order of these alternate registers + // accordingly. + stm32EnsureCCMROrder(p.Registers) + + case "USART": + isr := p.lookupRegister("ISR") + if isr == nil { + continue + } + + // Some of the upstream SVD files, like the one for stm32wl5x_cm4, + // lack FIFO enabled variants of the USART ISR register, + // even if the register manual defines them. To make sure + // that TXFNF is not missing from the generated .go files, + // we add TXFNF here in case FIFOEN is present. + if p.lookupRegister("CR1").hasBitfield("FIFOEN") { + stm32EnsureBit(isr, "TXFNF", "TXE", "USART_ISR_") + } + + // Svdtools handles the presence of alternate USART ISR registers, + // like in case of the stm32l4r5, adjusting names like "ISR_enabled" + // to "ISR", deleting "ISR_disabled" or "ISR_ALTERNATE" register definitions + // from the SVD. + // As this would result in USART_ISR_TXE definitions missing in the + // generated .go file, a constant for TXE is added here + // in case TXFNF is defined. + stm32EnsureBit(isr, "TXE", "TXFNF", "USART_ISR_") + } + } +} + +func addUnnumberedAlias(d *Device, dest, src string) { + if _, ok := d.PeripheralDict[dest]; !ok { + if p := d.PeripheralDict[src]; p != nil { + p.Alias = dest + } + } +} + +func stm32EnsureCCMROrder(registers []*PeripheralField) { + for i, r := range registers { + if i > 0 { + prev := registers[i-1] + if r.Address == prev.Address { + // alternate field + if strings.HasPrefix(prev.Name, "CCMR") && strings.HasPrefix(r.Name, "CCMR") && strings.HasSuffix(r.Name, "_Output") { + // swap register pointers + registers[i-1], registers[i] = r, prev + } + } + } + } +} + +func stm32EnsureBit(reg *PeripheralField, want, have, prefix string) { + iWant := -1 + iHave := -1 + wantConst := prefix + want + haveConst := prefix + have + for i := range reg.Constants { + f := ®.Constants[i] + if f.Name == wantConst { + iWant = i + break + } + if f.Name == haveConst { + iHave = i + break + } + } + if iHave != -1 && iWant == -1 { + iWant = iHave + 1 + reg.Constants = slices.Insert(reg.Constants, iWant, reg.Constants[iHave]) + reg.Constants[iWant].Name = wantConst + reg.Constants[iWant].Description = "Bit " + want + ". (added by gen-device-svd)" + } +} + +func (p *Peripheral) lookupRegister(name string) *PeripheralField { + for _, r := range p.Registers { + if r.Name == name { + return r + } + } + return nil +} + +func (r *PeripheralField) hasBitfield(name string) bool { + if r == nil { + return false + } + for i := range r.Bitfields { + if r.Bitfields[i].Name == name { + return true + } + } + return false +} From 8c2fcac862234a11c45b71543ae41e0fd1a8dd15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Teichgr=C3=A4ber?= Date: Wed, 25 Feb 2026 11:17:32 +0100 Subject: [PATCH 29/81] lib/stm32-svd: update submodule --- lib/stm32-svd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stm32-svd b/lib/stm32-svd index e6db8e32d5..bfed180b63 160000 --- a/lib/stm32-svd +++ b/lib/stm32-svd @@ -1 +1 @@ -Subproject commit e6db8e32d5d42293a528434ec12e7f88479a8649 +Subproject commit bfed180b639863db414221bdb6aa77e198f464f5 From 6607df5fd8616717a480ec845ba44ef0d9ac8405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Teichgr=C3=A4ber?= Date: Fri, 28 Nov 2025 21:26:16 +0100 Subject: [PATCH 30/81] targets/nucleo-f722ze.json: add build-tag stm32f722, change stm32f7x2.s to ..722.s In stm32-rs, stm32f7x2.svd got replaced by stm32f722.svd and stm32f732.svd. This change adjusts the target definition where stm32f7x2 is used, --- targets/nucleo-f722ze.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/targets/nucleo-f722ze.json b/targets/nucleo-f722ze.json index f426b332d7..88d7aed3ec 100644 --- a/targets/nucleo-f722ze.json +++ b/targets/nucleo-f722ze.json @@ -1,10 +1,10 @@ { "inherits": ["cortex-m7"], - "build-tags": ["nucleof722ze", "stm32f7x2", "stm32f7", "stm32"], + "build-tags": ["nucleof722ze", "stm32f722", "stm32f7x2", "stm32f7", "stm32"], "serial": "uart", "linkerscript": "targets/stm32f7x2zetx.ld", "extra-files": [ - "src/device/stm32/stm32f7x2.s" + "src/device/stm32/stm32f722.s" ], "flash-method": "openocd", "openocd-interface": "stlink-v2-1", From 4140cb0535224c9e0c4575437cb3ebde3e6acd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Teichgr=C3=A4ber?= Date: Mon, 23 Feb 2026 16:35:07 +0100 Subject: [PATCH 31/81] targets/swan: rename group build tag stm32l4x5 => stm32l4y5 to avoid clash with stm32 device name Previously, there was no specific stm32f4r5.svd in lib/stm32-svd, just stm32f4x5.svd was used; now, both files are present. This means that the existing build-tag stm32f4r5 will include the device/stm32/stm32f4r5.go file, and the additional build-tag stm32f4x5 would include the device/stm32/stm32f4x5.go file as well, resulting in build conflicts. Renaming just the tag, which is used in src/machine, and src/runtime, into stm32f4y5 solves this issue. --- src/machine/machine_stm32l4x5.go | 2 +- src/runtime/runtime_stm32l4x5.go | 2 +- targets/swan.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/machine/machine_stm32l4x5.go b/src/machine/machine_stm32l4x5.go index c8c550c3da..28fb5da334 100644 --- a/src/machine/machine_stm32l4x5.go +++ b/src/machine/machine_stm32l4x5.go @@ -1,4 +1,4 @@ -//go:build stm32l4x5 +//go:build stm32l4y5 package machine diff --git a/src/runtime/runtime_stm32l4x5.go b/src/runtime/runtime_stm32l4x5.go index 273eb726f5..c7c242975f 100644 --- a/src/runtime/runtime_stm32l4x5.go +++ b/src/runtime/runtime_stm32l4x5.go @@ -1,4 +1,4 @@ -//go:build stm32 && stm32l4x5 +//go:build stm32 && stm32l4y5 package runtime diff --git a/targets/swan.json b/targets/swan.json index fbb46a5569..92587690e8 100644 --- a/targets/swan.json +++ b/targets/swan.json @@ -1,6 +1,6 @@ { "inherits": ["cortex-m4"], - "build-tags": ["swan", "stm32l4r5", "stm32l4x5", "stm32l4", "stm32"], + "build-tags": ["swan", "stm32l4r5", "stm32l4y5", "stm32l4", "stm32"], "serial": "uart", "linkerscript": "targets/stm32l4x5.ld", "extra-files": [ From 3bd5d727eaa4dbd1cc3cf7027f646d566c4ef41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Teichgr=C3=A4ber?= Date: Mon, 23 Feb 2026 16:50:34 +0100 Subject: [PATCH 32/81] machine: sync stm32g0 with updated stm32 device files USART.ISR_FIFO_ENABLED => USART.ISR IRQ_TIM6_DAC => IRQ_TIM6_DAC_LPTIM1 ADC.CHSELR => ADC.CHSELR0 (alternate registers 0/1) --- src/machine/machine_stm32_adc_g0.go | 2 +- src/machine/machine_stm32_uart.go | 2 +- src/machine/machine_stm32g0.go | 6 +- src/machine/machine_stm32g0_uart.go | 86 ----------------------------- 4 files changed, 5 insertions(+), 91 deletions(-) delete mode 100644 src/machine/machine_stm32g0_uart.go diff --git a/src/machine/machine_stm32_adc_g0.go b/src/machine/machine_stm32_adc_g0.go index d8cca3c8df..7d7150ade1 100644 --- a/src/machine/machine_stm32_adc_g0.go +++ b/src/machine/machine_stm32_adc_g0.go @@ -123,7 +123,7 @@ func (a ADC) Get() uint16 { // Select the channel to convert using CHSELR // CHSELR uses a bitfield where bit N = 1 enables channel N - stm32.ADC.CHSELR.Set(1 << ch) + stm32.ADC.CHSELR0.Set(1 << ch) // Wait for channel configuration ready for stm32.ADC.GetISR_CCRDY() == 0 { diff --git a/src/machine/machine_stm32_uart.go b/src/machine/machine_stm32_uart.go index 6d37582448..6beafe4e1c 100644 --- a/src/machine/machine_stm32_uart.go +++ b/src/machine/machine_stm32_uart.go @@ -1,4 +1,4 @@ -//go:build stm32 && !stm32g0 +//go:build stm32 package machine diff --git a/src/machine/machine_stm32g0.go b/src/machine/machine_stm32g0.go index b338faef1a..530c77aade 100644 --- a/src/machine/machine_stm32g0.go +++ b/src/machine/machine_stm32g0.go @@ -230,7 +230,7 @@ func (uart *UART) getBaudRateDivisor(baudRate uint32) uint32 { func (uart *UART) setRegisters() { uart.rxReg = &uart.Bus.RDR uart.txReg = &uart.Bus.TDR - uart.statusReg = &uart.Bus.ISR_FIFO_ENABLED + uart.statusReg = &uart.Bus.ISR uart.txEmptyFlag = stm32.USART_ISR_TXE } @@ -508,7 +508,7 @@ func (t *TIM) registerUPInterrupt() interrupt.Interrupt { case &TIM3: return interrupt.New(stm32.IRQ_TIM3_TIM4, TIM3.handleUPInterrupt) case &TIM6: - return interrupt.New(stm32.IRQ_TIM6_DAC, TIM6.handleUPInterrupt) + return interrupt.New(stm32.IRQ_TIM6_DAC_LPTIM1, TIM6.handleUPInterrupt) case &TIM7: return interrupt.New(stm32.IRQ_TIM7, TIM7.handleUPInterrupt) case &TIM14: @@ -533,7 +533,7 @@ func (t *TIM) registerOCInterrupt() interrupt.Interrupt { case &TIM3: return interrupt.New(stm32.IRQ_TIM3_TIM4, TIM3.handleOCInterrupt) case &TIM6: - return interrupt.New(stm32.IRQ_TIM6_DAC, TIM6.handleOCInterrupt) + return interrupt.New(stm32.IRQ_TIM6_DAC_LPTIM1, TIM6.handleOCInterrupt) case &TIM7: return interrupt.New(stm32.IRQ_TIM7, TIM7.handleOCInterrupt) case &TIM14: diff --git a/src/machine/machine_stm32g0_uart.go b/src/machine/machine_stm32g0_uart.go deleted file mode 100644 index 18544c3ecd..0000000000 --- a/src/machine/machine_stm32g0_uart.go +++ /dev/null @@ -1,86 +0,0 @@ -//go:build stm32g0 - -package machine - -// Peripheral abstraction layer for UARTs on the stm32g0 family. - -import ( - "device/stm32" - "runtime/interrupt" - "runtime/volatile" - "unsafe" -) - -// UART representation -type UART struct { - Buffer *RingBuffer - Bus *stm32.USART_Type - Interrupt interrupt.Interrupt - TxAltFuncSelector uint8 - RxAltFuncSelector uint8 - - // Registers specific to the chip - rxReg *volatile.Register32 - txReg *volatile.Register32 - statusReg *volatile.Register32 - txEmptyFlag uint32 -} - -// Configure the UART. -func (uart *UART) Configure(config UARTConfig) { - // Default baud rate to 115200. - if config.BaudRate == 0 { - config.BaudRate = 115200 - } - - // Set the GPIO pins to defaults if they're not set - if config.TX == 0 && config.RX == 0 { - config.TX = UART_TX_PIN - config.RX = UART_RX_PIN - } - - // STM32 families have different, but compatible, registers for - // basic UART functions. For each family populate the registers - // into `uart`. - uart.setRegisters() - - // Enable USART clock - enableAltFuncClock(unsafe.Pointer(uart.Bus)) - - uart.configurePins(config) - - // Set baud rate - uart.SetBaudRate(config.BaudRate) - - // Enable USART port, tx, rx and rx interrupts - // STM32G0 uses CR1_FIFO_ENABLED register - uart.Bus.CR1_FIFO_ENABLED.Set(stm32.USART_CR1_TE | stm32.USART_CR1_RE | stm32.USART_CR1_RXNEIE | stm32.USART_CR1_UE) - - // Enable RX IRQ - uart.Interrupt.SetPriority(0xc0) - uart.Interrupt.Enable() -} - -// handleInterrupt should be called from the appropriate interrupt handler for -// this UART instance. -func (uart *UART) handleInterrupt(interrupt.Interrupt) { - uart.Receive(byte((uart.rxReg.Get() & 0xFF))) -} - -// SetBaudRate sets the communication speed for the UART. Defer to chip-specific -// routines for calculation -func (uart *UART) SetBaudRate(br uint32) { - divider := uart.getBaudRateDivisor(br) - uart.Bus.BRR.Set(divider) -} - -// WriteByte writes a byte of data to the UART. -func (uart *UART) writeByte(c byte) error { - uart.txReg.Set(uint32(c)) - - for !uart.statusReg.HasBits(uart.txEmptyFlag) { - } - return nil -} - -func (uart *UART) flush() {} From e4c4b6736f021b82ba41b497eff33d9e7aceacaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Teichgr=C3=A4ber?= Date: Mon, 23 Feb 2026 17:01:16 +0100 Subject: [PATCH 33/81] machine: stm32l0x1,l0x2: TIM: adapt to 32-bit register access Recent changes in stm32-svd result in a change from previous 16-bit register access to 32-bit access. Both access types are allowed, according to the register manuals. --- src/machine/machine_stm32l0x1.go | 4 ++-- src/machine/machine_stm32l0x2.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/machine/machine_stm32l0x1.go b/src/machine/machine_stm32l0x1.go index f0d23ca495..292b81f462 100644 --- a/src/machine/machine_stm32l0x1.go +++ b/src/machine/machine_stm32l0x1.go @@ -188,8 +188,8 @@ func (t *TIM) enableMainOutput() { // nothing to do - no BDTR register } -type arrtype = uint16 -type arrRegType = volatile.Register16 +type arrtype = uint32 +type arrRegType = volatile.Register32 const ( ARR_MAX = 0x10000 diff --git a/src/machine/machine_stm32l0x2.go b/src/machine/machine_stm32l0x2.go index 2a747920be..fe1ba3a229 100644 --- a/src/machine/machine_stm32l0x2.go +++ b/src/machine/machine_stm32l0x2.go @@ -245,8 +245,8 @@ func (t *TIM) enableMainOutput() { // nothing to do - no BDTR register } -type arrtype = uint16 -type arrRegType = volatile.Register16 +type arrtype = uint32 +type arrRegType = volatile.Register32 const ( ARR_MAX = 0x10000 From bbca04739a6a9e24a0ceeb6af34f748546785fea Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 26 Feb 2026 19:36:58 +0100 Subject: [PATCH 34/81] targets: add support for Seeedstudio Xiao-RP2350 board. Signed-off-by: deadprogram --- GNUmakefile | 2 + src/machine/board_xiao-rp2350.go | 93 ++++++++++++++++++++++++++++++++ targets/xiao-rp2350.json | 8 +++ 3 files changed, 103 insertions(+) create mode 100644 src/machine/board_xiao-rp2350.go create mode 100644 targets/xiao-rp2350.json diff --git a/GNUmakefile b/GNUmakefile index 3eed5b9419..6a2b8c1cd7 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -816,6 +816,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=vicharak_shrike-lite examples/echo @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=xiao-rp2350 examples/blinky1 + @$(MD5SUM) test.hex # test pwm $(TINYGO) build -size short -o test.hex -target=itsybitsy-m0 examples/pwm @$(MD5SUM) test.hex diff --git a/src/machine/board_xiao-rp2350.go b/src/machine/board_xiao-rp2350.go new file mode 100644 index 0000000000..30003d4600 --- /dev/null +++ b/src/machine/board_xiao-rp2350.go @@ -0,0 +1,93 @@ +//go:build xiao_rp2350 + +// This file contains the pin mappings for the Seeed XIAO RP2350 boards. +// +// XIAO RP2350 is a microcontroller using the Raspberry Pi RP2350 chip. +// +// - https://wiki.seeedstudio.com/XIAO-RP2350/ +package machine + +// Digital Pins +const ( + D0 Pin = GPIO26 + D1 Pin = GPIO27 + D2 Pin = GPIO28 + D3 Pin = GPIO5 + D4 Pin = GPIO6 + D5 Pin = GPIO7 + D6 Pin = GPIO0 + D7 Pin = GPIO1 + D8 Pin = GPIO2 + D9 Pin = GPIO4 + D10 Pin = GPIO3 + D11 Pin = GPIO21 + D12 Pin = GPIO20 + D13 Pin = GPIO17 + D14 Pin = GPIO16 + D15 Pin = GPIO11 + D16 Pin = GPIO12 + D17 Pin = GPIO10 +) + +// Analog pins +const ( + A0 Pin = D0 + A1 Pin = D1 + A2 Pin = D2 +) + +// Onboard LEDs +const ( + NEOPIXEL = GPIO22 + WS2812 = GPIO22 + NEO_PWR = GPIO23 + NEOPIXEL_POWER = GPIO23 + + LED = GPIO25 +) + +// I2C pins +const ( + I2C0_SDA_PIN Pin = D14 + I2C0_SCL_PIN Pin = D13 + + I2C1_SDA_PIN Pin = D4 + I2C1_SCL_PIN Pin = D5 +) + +// SPI pins +const ( + SPI0_SCK_PIN Pin = D8 + SPI0_SDO_PIN Pin = D10 + SPI0_SDI_PIN Pin = D9 + + SPI1_SCK_PIN Pin = D17 + SPI1_SDO_PIN Pin = D15 + SPI1_SDI_PIN Pin = D16 +) + +// Onboard crystal oscillator frequency, in MHz. +const ( + xoscFreq = 12 // MHz +) + +// UART pins +const ( + UART0_TX_PIN = GPIO0 + UART0_RX_PIN = GPIO1 + UART_TX_PIN = UART0_TX_PIN + UART_RX_PIN = UART0_RX_PIN +) + +var DefaultUART = UART0 + +// USB CDC identifiers +const ( + usb_STRING_PRODUCT = "XIAO RP2350" + usb_STRING_MANUFACTURER = "Seeed" +) + +var ( + usb_VID uint16 = 0x2e8a + usb_PID uint16 = 0x000a +) diff --git a/targets/xiao-rp2350.json b/targets/xiao-rp2350.json new file mode 100644 index 0000000000..dd65bbcbb9 --- /dev/null +++ b/targets/xiao-rp2350.json @@ -0,0 +1,8 @@ +{ + "inherits": [ + "rp2350" + ], + "serial-port": ["2e8a:000a"], + "build-tags": ["xiao_rp2350"], + "default-stack-size": 8192 +} From 5343bcf48c777b9cb17ed5e7c15f59eb874c8853 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 27 Feb 2026 12:17:10 +0100 Subject: [PATCH 35/81] feature: add esp32flash flash method for esp32s3/esp32c3/esp32/esp8266 This adds usage of the new espflash package to perform flashing on ESP32, ESP32S3, ESP32C3, & ESP8266 boards. This means that you no longer have to install esptool.py in order to flash ESP32 based boards. Signed-off-by: deadprogram --- go.mod | 3 ++- go.sum | 6 ++++-- main.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7d4ea5f189..677f506386 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,12 @@ require ( github.com/mattn/go-tty v0.0.4 github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 github.com/tetratelabs/wazero v1.6.0 - go.bug.st/serial v1.6.0 + go.bug.st/serial v1.6.2 golang.org/x/net v0.35.0 golang.org/x/sys v0.30.0 golang.org/x/tools v0.30.0 gopkg.in/yaml.v2 v2.4.0 + tinygo.org/x/espflash v0.1.0 tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 ) diff --git a/go.sum b/go.sum index 8c2330c3c5..301225e24f 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g= github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= -go.bug.st/serial v1.6.0 h1:mAbRGN4cKE2J5gMwsMHC2KQisdLRQssO9WSM+rbZJ8A= -go.bug.st/serial v1.6.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= @@ -58,5 +58,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tinygo.org/x/espflash v0.1.0 h1:3OsU8BwQ8glikAY9J0OLWOYpP3baef/OpW5BqjPu+MU= +tinygo.org/x/espflash v0.1.0/go.mod h1:n4sWxsjN4wjHN1lKfEYjXke6/eSeLTq/lRRLJS5FNfg= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 h1:ovavgTdIBWCH8YWlcfq9gkpoyT1+IxMKSn+Df27QwE8= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/main.go b/main.go index e2736fd17b..80c2d35c7b 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ import ( "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/loader" "golang.org/x/tools/go/buildutil" + "tinygo.org/x/espflash" "tinygo.org/x/go-llvm" "go.bug.st/serial" @@ -385,6 +386,8 @@ func Flash(pkgName, port, outpath string, options *compileopts.Options) error { fileExt = ".hex" case "bmp": fileExt = ".elf" + case "esp32flash": + fileExt = ".bin" case "native": return errors.New("unknown flash method \"native\" - did you miss a -target flag?") default: @@ -519,6 +522,15 @@ func Flash(pkgName, port, outpath string, options *compileopts.Options) error { if err != nil { return &commandError{"failed to flash", result.Binary, err} } + case "esp32flash": + port, err := getDefaultPort(port, config.Target.SerialPort) + if err != nil { + return &commandError{"failed to find port", port, err} + } + + if err := flashBinUsingEsp32(port, result.Binary, config.Options); err != nil { + return &commandError{"failed to flash", result.Binary, err} + } default: return fmt.Errorf("unknown flash method: %s", flashMethod) } @@ -1019,6 +1031,43 @@ func flashHexUsingMSD(volumes []string, tmppath string, options *compileopts.Opt return errors.New("unable to locate any volume: [" + strings.Join(volumes, ",") + "]") } +func flashBinUsingEsp32(port, tmppath string, options *compileopts.Options) error { + flasher, err := espflash.NewFlasher(port, nil) + if err != nil { + return err + } + defer flasher.Close() + + chipName := flasher.ChipName() + fmt.Printf("Connected to %s\n", chipName) + + offset := uint32(0x0) + if chipName == "ESP32" { + offset = 0x1000 + } + + // Read the firmware binary + data, err := os.ReadFile(tmppath) + if err != nil { + return err + } + + // Flash with progress reporting + err = flasher.FlashImage(data, offset, func(current, total int) { + fmt.Printf("\rFlashing: %d/%d bytes (%.0f%%)", current, total, + float64(current)/float64(total)*100) + }) + if err != nil { + return err + } + fmt.Println() + + // Reset the device to run the new firmware + flasher.Reset() + + return nil +} + type mountPoint struct { name string path string From 1385c97c5d70beba8bdeb596db7c7968abb89774 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 27 Feb 2026 12:21:11 +0100 Subject: [PATCH 36/81] targets: modify esp32, esp32s3, esp32c3, & esp8266 targets to use built-in esp32flash Signed-off-by: deadprogram --- targets/esp32.json | 2 +- targets/esp32c3.json | 2 +- targets/esp32s3.json | 2 +- targets/esp8266.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/targets/esp32.json b/targets/esp32.json index f49282fdd5..7aad3da4e9 100644 --- a/targets/esp32.json +++ b/targets/esp32.json @@ -15,7 +15,7 @@ "src/internal/task/task_stack_esp32.S" ], "binary-format": "esp32", - "flash-command": "esptool.py --chip=esp32 --port {port} write_flash 0x1000 {bin} -ff 80m -fm dout", + "flash-method": "esp32flash", "emulator": "qemu-system-xtensa -machine esp32 -nographic -drive file={img},if=mtd,format=raw", "gdb": ["xtensa-esp32-elf-gdb"] } diff --git a/targets/esp32c3.json b/targets/esp32c3.json index fd993b8bb8..3336f3c18e 100644 --- a/targets/esp32c3.json +++ b/targets/esp32c3.json @@ -13,7 +13,7 @@ "src/device/esp/esp32c3.S" ], "binary-format": "esp32c3", - "flash-command": "esptool.py --chip=esp32c3 --port {port} write_flash 0x0 {bin}", + "flash-method": "esp32flash", "serial-port": ["303a:1001"], "openocd-interface": "esp_usb_jtag", "openocd-target": "esp32c3", diff --git a/targets/esp32s3.json b/targets/esp32s3.json index 051cc85420..8f534e2890 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -15,7 +15,7 @@ "src/internal/task/task_stack_esp32.S" ], "binary-format": "esp32s3", - "flash-command": "esptool.py --chip=esp32s3 --port {port} write_flash 0x0000 {bin} -ff 80m -fm dout", + "flash-method": "esp32flash", "emulator": "qemu-system-xtensa -machine esp32 -nographic -drive file={img},if=mtd,format=raw", "gdb": ["xtensa-esp32-elf-gdb"] } diff --git a/targets/esp8266.json b/targets/esp8266.json index bb02c3db29..b6e4a3500f 100644 --- a/targets/esp8266.json +++ b/targets/esp8266.json @@ -14,5 +14,5 @@ "src/internal/task/task_stack_esp8266.S" ], "binary-format": "esp8266", - "flash-command": "esptool.py --chip=esp8266 --port {port} write_flash 0x00000 {bin} -fm qio" + "flash-method": "esp32flash" } From 4f9857ad429f8f848764e32ed1d4c1839a980677 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 28 Feb 2026 10:25:01 +0100 Subject: [PATCH 37/81] modules: update espflash to 0.2.0 for esp8266 fixes Signed-off-by: deadprogram --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 677f506386..d6c4fea966 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sys v0.30.0 golang.org/x/tools v0.30.0 gopkg.in/yaml.v2 v2.4.0 - tinygo.org/x/espflash v0.1.0 + tinygo.org/x/espflash v0.2.0 tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 ) diff --git a/go.sum b/go.sum index 301225e24f..f5bdc84c87 100644 --- a/go.sum +++ b/go.sum @@ -58,7 +58,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -tinygo.org/x/espflash v0.1.0 h1:3OsU8BwQ8glikAY9J0OLWOYpP3baef/OpW5BqjPu+MU= -tinygo.org/x/espflash v0.1.0/go.mod h1:n4sWxsjN4wjHN1lKfEYjXke6/eSeLTq/lRRLJS5FNfg= +tinygo.org/x/espflash v0.2.0 h1:sHDeMVaiJ0kEosCYgkiAHjSCV9+/mgQZMKy7nOXWVoQ= +tinygo.org/x/espflash v0.2.0/go.mod h1:n4sWxsjN4wjHN1lKfEYjXke6/eSeLTq/lRRLJS5FNfg= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 h1:ovavgTdIBWCH8YWlcfq9gkpoyT1+IxMKSn+Df27QwE8= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= From a17ffae0a8d6fd7e901cc76eb18884ee4d689137 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 27 Feb 2026 19:05:47 +0100 Subject: [PATCH 38/81] machine: implement RNG for esp32s3 based on the onboard hardware random number generator. Signed-off-by: deadprogram --- src/crypto/rand/rand_baremetal.go | 2 +- src/machine/machine_esp32s3.go | 35 +++++++++++++++++++++++++++++++ src/runtime/rand_hwrng.go | 2 +- src/runtime/rand_norng.go | 2 +- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/crypto/rand/rand_baremetal.go b/src/crypto/rand/rand_baremetal.go index 5711f23eb0..b39eee17e7 100644 --- a/src/crypto/rand/rand_baremetal.go +++ b/src/crypto/rand/rand_baremetal.go @@ -1,4 +1,4 @@ -//go:build nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt) +//go:build nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || esp32s3 || tkey || (tinygo.riscv32 && virt) // If you update the above build constraint, you'll probably also need to update // src/runtime/rand_hwrng.go. diff --git a/src/machine/machine_esp32s3.go b/src/machine/machine_esp32s3.go index fdb3dbd374..d1ddcc5e64 100644 --- a/src/machine/machine_esp32s3.go +++ b/src/machine/machine_esp32s3.go @@ -312,3 +312,38 @@ func (uart *UART) writeByte(b byte) error { } func (uart *UART) flush() {} + +// GetRNG returns 32-bit random numbers using the ESP32-S3 true random number generator, +// Random numbers are generated based on the thermal noise in the system and the +// asynchronous clock mismatch. +// For maximum entropy also make sure that the SAR_ADC is enabled. +// See esp32-s3_technical_reference_manual_en.pdf p.920 +func GetRNG() (ret uint32, err error) { + // ensure ADC clock is initialized + initADCClock() + + // ensure fast RTC clock is enabled + if esp.RTC_CNTL.GetCLK_CONF_DIG_CLK8M_EN() == 0 { + esp.RTC_CNTL.SetCLK_CONF_DIG_CLK8M_EN(1) + } + + return esp.RNG.DATA.Get(), nil +} + +func initADCClock() { + if esp.APB_SARADC.GetCLKM_CONF_CLK_EN() == 1 { + return + } + + // only support ADC_CTRL_CLK set to 1 + esp.APB_SARADC.SetCLKM_CONF_CLK_SEL(1) + + esp.APB_SARADC.SetCTRL_SARADC_SAR_CLK_GATED(1) + + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_NUM(15) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_B(1) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_A(0) + + esp.APB_SARADC.SetCTRL_SARADC_SAR_CLK_DIV(1) + esp.APB_SARADC.SetCLKM_CONF_CLK_EN(1) +} diff --git a/src/runtime/rand_hwrng.go b/src/runtime/rand_hwrng.go index bb5b2e8f85..5f95caf386 100644 --- a/src/runtime/rand_hwrng.go +++ b/src/runtime/rand_hwrng.go @@ -1,4 +1,4 @@ -//go:build baremetal && (nrf || (stm32 && !(stm32f103 || stm32l0x1 || stm32g0)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) +//go:build baremetal && (nrf || (stm32 && !(stm32f103 || stm32l0x1 || stm32g0)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || esp32s3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) // If you update the above build constraint, you'll probably also need to update // src/crypto/rand/rand_baremetal.go. diff --git a/src/runtime/rand_norng.go b/src/runtime/rand_norng.go index 0e3b6d09b3..b9ab475c76 100644 --- a/src/runtime/rand_norng.go +++ b/src/runtime/rand_norng.go @@ -1,4 +1,4 @@ -//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1 || stm32g0)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) +//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1 || stm32g0)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || esp32s3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) package runtime From 0952eb4eb62a892cc373304c038d0ac5ce8bef5d Mon Sep 17 00:00:00 2001 From: Dima Date: Sun, 1 Mar 2026 23:45:01 +0200 Subject: [PATCH 39/81] esp32s3 + c3 ADC (#5231) * save * esp32s3: save * adc * worker on esp32s3 * worker after flash arduino * save * fix * simple adc * added adc * esp32s3-adc: rm debug * esp32s3-adc: clear * last refactor * linters * esp32s3-adc: recover example * esp32s3-adc: reuse fuse for esp32c3 * esp32s3-adc: refactor bugs * esp32s3-adc: fix adc2 for esp32c3 * esp32s3-adc: group to adc files * esp32s3-adc: revert changing board * esp32s3-adc: recover example adc * esp32s3-adc: fix edge values adc & added smoketests * esp32s3-adc: rename methods * esp32s3-adc: extends adc tests * esp32s3-adc: drop debug * esp32s3-adc: added ADCX const * esp32s3-adc: change adc tests * esp32s3-adc: added comment for esp32c3 * esp32s3-adc: drop debug empty loops * esp32s3-adc: drop duplicate gpio * esp32s3-adc: change return values to 0..65520 --- GNUmakefile | 6 + src/examples/adc/adc.go | 14 +- src/machine/board_esp32c3-12f.go | 3 - src/machine/machine_esp32c3.go | 37 +- src/machine/machine_esp32c3_adc.go | 414 +++++++++++++++++++ src/machine/machine_esp32s3.go | 36 +- src/machine/machine_esp32s3_adc.go | 644 +++++++++++++++++++++++++++++ 7 files changed, 1123 insertions(+), 31 deletions(-) create mode 100644 src/machine/machine_esp32c3_adc.go create mode 100644 src/machine/machine_esp32s3_adc.go diff --git a/GNUmakefile b/GNUmakefile index 6a2b8c1cd7..9a6dc7b8e5 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -934,6 +934,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/pwm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/adc + @$(MD5SUM) test.bin # esp32s3-wroom1 $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/blinkm @@ -942,6 +944,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/pwm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/adc + @$(MD5SUM) test.bin endif # esp32c3-supermini $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinky1 @@ -952,6 +956,8 @@ endif @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/pwm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/adc + @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp-c3-32s-kit examples/blinky1 @$(MD5SUM) test.bin diff --git a/src/examples/adc/adc.go b/src/examples/adc/adc.go index de8a7f8859..3ec213f41a 100644 --- a/src/examples/adc/adc.go +++ b/src/examples/adc/adc.go @@ -5,25 +5,15 @@ import ( "time" ) -// This example assumes that an analog sensor such as a rotary dial is connected to pin ADC0. -// When the dial is turned past the midway point, the built-in LED will light up. - func main() { machine.InitADC() - led := machine.LED - led.Configure(machine.PinConfig{Mode: machine.PinOutput}) - sensor := machine.ADC{machine.ADC2} sensor.Configure(machine.ADCConfig{}) for { val := sensor.Get() - if val < 0x8000 { - led.Low() - } else { - led.High() - } - time.Sleep(time.Millisecond * 100) + println(val) + time.Sleep(time.Millisecond * 500) } } diff --git a/src/machine/board_esp32c3-12f.go b/src/machine/board_esp32c3-12f.go index f023bb9d61..0988c7adcb 100644 --- a/src/machine/board_esp32c3-12f.go +++ b/src/machine/board_esp32c3-12f.go @@ -30,9 +30,6 @@ const ( // ADC pins const ( - ADC0 Pin = ADC1_0 - ADC1 Pin = ADC2_0 - ADC1_0 Pin = IO0 ADC1_1 Pin = IO1 ADC1_2 Pin = IO2 diff --git a/src/machine/machine_esp32c3.go b/src/machine/machine_esp32c3.go index 7214acbb22..e5e10c326b 100644 --- a/src/machine/machine_esp32c3.go +++ b/src/machine/machine_esp32c3.go @@ -27,16 +27,29 @@ const ( PinInput PinInputPullup PinInputPulldown + PinAnalog +) + +const ( + GPIO0 Pin = 0 + GPIO1 Pin = 1 + GPIO2 Pin = 2 + GPIO3 Pin = 3 + GPIO4 Pin = 4 + GPIO5 Pin = 5 + GPIO6 Pin = 6 +) + +const ( + ADC0 Pin = GPIO0 + ADC1 Pin = GPIO1 + ADC2 Pin = GPIO2 + ADC3 Pin = GPIO3 + ADC4 Pin = GPIO4 + ADC5 Pin = GPIO5 // avoid when WiFi is used. ) const ( - GPIO0 Pin = 0 - GPIO1 Pin = 1 - GPIO2 Pin = 2 - GPIO3 Pin = 3 - GPIO4 Pin = 4 - GPIO5 Pin = 5 - GPIO6 Pin = 6 GPIO7 Pin = 7 GPIO8 Pin = 8 GPIO9 Pin = 9 @@ -76,13 +89,15 @@ func (p Pin) Configure(config PinConfig) { const function = 1 // function 1 is GPIO for every pin muxConfig |= function << esp.IO_MUX_GPIO_MCU_SEL_Pos - // Make this pin an input pin (always). - muxConfig |= esp.IO_MUX_GPIO_FUN_IE + // FUN_IE: disable for PinAnalog (high-Z for ADC) + if config.Mode != PinAnalog { + muxConfig |= esp.IO_MUX_GPIO_FUN_IE + } // Set drive strength: 0 is lowest, 3 is highest. muxConfig |= 2 << esp.IO_MUX_GPIO_FUN_DRV_Pos - // Select pull mode. + // Select pull mode (no pulls for PinAnalog). if config.Mode == PinInputPullup { muxConfig |= esp.IO_MUX_GPIO_FUN_WPU } else if config.Mode == PinInputPulldown { @@ -99,7 +114,7 @@ func (p Pin) Configure(config PinConfig) { case PinOutput: // Set the 'output enable' bit. esp.GPIO.ENABLE_W1TS.Set(1 << p) - case PinInput, PinInputPullup, PinInputPulldown: + case PinInput, PinInputPullup, PinInputPulldown, PinAnalog: // Clear the 'output enable' bit. esp.GPIO.ENABLE_W1TC.Set(1 << p) } diff --git a/src/machine/machine_esp32c3_adc.go b/src/machine/machine_esp32c3_adc.go new file mode 100644 index 0000000000..ec23637edc --- /dev/null +++ b/src/machine/machine_esp32c3_adc.go @@ -0,0 +1,414 @@ +//go:build esp32c3 && !m5stamp_c3 + +package machine + +import ( + "device/esp" + "errors" + "runtime/volatile" + "unsafe" +) + +const ( + // ADC attenuation values for ESP32-C3 APB_SARADC. + // 0 dB : ~0 .. 1.1 V + // 11 dB : ~0 .. 3.3 V (matches typical VDD) + atten0dB = 0 + atten11dB = 3 +) + +func InitADC() { + esp.SYSTEM.SetPERIP_RST_EN0_APB_SARADC_RST(1) + esp.SYSTEM.SetPERIP_CLK_EN0_APB_SARADC_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_APB_SARADC_RST(0) + + esp.RTC_CNTL.SetANA_CONF_SAR_I2C_PU(1) + esp.RTC_CNTL.SetSENSOR_CTRL_FORCE_XPD_SAR(1) + esp.APB_SARADC.SetCTRL_SARADC_XPD_SAR_FORCE(1) + esp.APB_SARADC.SetFSM_WAIT_SARADC_XPD_WAIT(8) + esp.APB_SARADC.SetFSM_WAIT_SARADC_RSTB_WAIT(8) + esp.APB_SARADC.SetFSM_WAIT_SARADC_STANDBY_WAIT(100) + esp.APB_SARADC.SetCLKM_CONF_CLK_SEL(2) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_NUM(1) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_B(0) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_A(0) + esp.APB_SARADC.SetCLKM_CONF_CLK_EN(1) + + var c adcSelfCalibration + c.calibrate() +} + +// ESP32-C3: ADC1 = GPIO0–GPIO4 (ch 0–4), ADC2 = GPIO5 (ch 0). ADC2 shares with Wi‑Fi; +// readings may be noisy when Wi‑Fi is active. +func (a ADC) Configure(config ADCConfig) error { + if a.Pin > 5 { + return errors.New("invalid ADC pin for ESP32-C3") + } + a.Pin.Configure(PinConfig{Mode: PinAnalog}) + return nil +} + +func (a ADC) Get() uint16 { + if a.Pin > 5 { + return 0 + } + adc1 := a.Pin <= 4 + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) + esp.APB_SARADC.SetINT_CLR_APB_SARADC1_DONE_INT_CLR(1) + esp.APB_SARADC.SetINT_CLR_APB_SARADC2_DONE_INT_CLR(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + var raw uint32 + if adc1 { + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(uint32(a.Pin)) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC1_DONE_INT_RAW() == 0 { + } + raw = esp.APB_SARADC.GetSAR1DATA_STATUS_APB_SARADC1_DATA() + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(0) + } else { + // ADC2: GPIO5 = channel 0. Grant arbiter to ADC2 first, then set channel and start. + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(8) // (1<<3)|0 for ADC2 channel 0 + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC2_ONETIME_SAMPLE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC2_DONE_INT_RAW() == 0 { + } + raw = esp.APB_SARADC.GetSAR2DATA_STATUS_APB_SARADC2_DATA() + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC2_ONETIME_SAMPLE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(0) + } + return uint16(raw&0xfff) << 4 +} + +// adcSelfCalibration +const ( + adcCalTimesC3 = 15 + adcCalOffsetRangeC3 = uint32(4096) + adcCalRtcMagicC3 = uint32(0xADC1C401) + adcCalInitMinC3 = uint32(1000) + adcCalInitMaxC3 = uint32(4096) + adcGndOffsetCompC3 = uint32(0) +) + +type adcSelfCalibration struct { + digiRefMv uint32 +} + +// calibrate sets ADC1/ADC2 init code from RTC or runs self-calibration (GND). +// eFuse is not used: on ESP32-C3 the ADC calibration fields in BLK2 are often unprogrammed. +func (c *adcSelfCalibration) calibrate() { + reg := regI2C{} + reg.sarEnable() + + var adc1Code uint32 + if saved, ok := c.restoreFromRTC(); ok { + adc1Code = saved + } else { + c.calSetupADC1() + reg.adc1CalibrationInit(0) + reg.adc1CalibrationPrepare(0) + adc1Code = c.calibrateUnit(reg, 0, c.readADC1) + c.saveToRTC(adc1Code) + reg.adc1CalibrationFinish(0) + } + + c.applyADC1Code(reg, adc1Code) + c.applyADC2Code(reg, adc1Code) +} + +// calSetupADC1 configures APB_SARADC for oneshot sampling on ADC1 channel 0 +// with fixed attenuation. This is used only during self‑calibration. +func (c *adcSelfCalibration) calSetupADC1() { + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(1) +} + +// calSetupADC2 configures APB_SARADC for oneshot sampling on ADC2 (GPIO5, ch 0). +// On C3, onetime_channel = (unit<<3)|channel → ADC2 ch0 = 8. +func (c *adcSelfCalibration) calSetupADC2() { + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(8) // (1<<3)|0 for ADC2 + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC2_ONETIME_SAMPLE(1) +} + +// readADC1 performs a single ADC1 conversion using the APB_SARADC +// oneshot path and returns the raw 12‑bit result (0..4095). +func (c *adcSelfCalibration) readADC1() uint32 { + esp.APB_SARADC.SetINT_CLR_APB_SARADC1_DONE_INT_CLR(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC1_DONE_INT_RAW() == 0 { + } + raw := esp.APB_SARADC.GetSAR1DATA_STATUS_APB_SARADC1_DATA() & 0xfff + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + return uint32(raw) +} + +// readADC2 performs a single ADC2 conversion and returns the raw 12‑bit result (0..4095). +func (c *adcSelfCalibration) readADC2() uint32 { + esp.APB_SARADC.SetINT_CLR_APB_SARADC2_DONE_INT_CLR(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC2_DONE_INT_RAW() == 0 { + } + raw := esp.APB_SARADC.GetSAR2DATA_STATUS_APB_SARADC2_DATA() & 0xfff + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(0) + return uint32(raw) +} + +func (c *adcSelfCalibration) restoreFromRTC() (uint32, bool) { + if esp.RTC_CNTL.GetSTORE0() != adcCalRtcMagicC3 { + return 0, false + } + code := esp.RTC_CNTL.GetSTORE1() + if code < adcCalInitMinC3 || code > adcCalInitMaxC3 { + return 0, false + } + return code, true +} + +func (c *adcSelfCalibration) saveToRTC(code uint32) { + if code < adcCalInitMinC3 || code > adcCalInitMaxC3 { + return + } + esp.RTC_CNTL.SetSTORE0(adcCalRtcMagicC3) + esp.RTC_CNTL.SetSTORE1(code) +} + +// applyADC1Code sets ADC1 init code and finishes calibration. +func (c *adcSelfCalibration) applyADC1Code(reg regI2C, code uint32) { + c.calSetupADC1() + reg.adc1CalibrationInit(0) + reg.adc1CalibrationPrepare(0) + reg.adc1SetCalibrationParam(0, code) + reg.adc1CalibrationFinish(0) +} + +// applyADC2Code sets ADC2 init code and finishes calibration. On C3 eFuse V1 +// there is no separate ADC2 calibration; IDF uses ADC1 init code for both units. +func (c *adcSelfCalibration) applyADC2Code(reg regI2C, code uint32) { + reg.adc1CalibrationInit(1) + reg.adc1CalibrationPrepare(1) + reg.adc1SetCalibrationParam(1, code) + reg.adc1CalibrationFinish(1) +} + +func (c *adcSelfCalibration) calibrateUnit(reg regI2C, adcN uint8, readADC func() uint32) uint32 { + var codeList [adcCalTimesC3]uint32 + var codeSum uint32 + + for rpt := 0; rpt < adcCalTimesC3; rpt++ { + codeH := adcCalOffsetRangeC3 + codeL := uint32(0) + chkCode := (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(adcN, chkCode) + selfCal := readADC() + + for codeH-codeL > 1 { + if selfCal == 0 { + codeH = chkCode + } else { + codeL = chkCode + } + chkCode = (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(adcN, chkCode) + selfCal = readADC() + if codeH-codeL == 1 { + chkCode++ + reg.adc1SetCalibrationParam(adcN, chkCode) + selfCal = readADC() + } + } + codeList[rpt] = chkCode + codeSum += chkCode + } + + codeL := codeList[0] + codeH := codeList[0] + for i := 0; i < adcCalTimesC3; i++ { + if codeList[i] < codeL { + codeL = codeList[i] + } + if codeList[i] > codeH { + codeH = codeList[i] + } + } + excluded := codeH + codeL + remaining := codeSum - excluded + finalCode := remaining / (adcCalTimesC3 - 2) + if remaining%(adcCalTimesC3-2) >= 4 { + finalCode++ + } + if finalCode < adcCalInitMinC3 { + finalCode = adcCalInitMinC3 + } + if finalCode > adcCalInitMaxC3 { + finalCode = adcCalInitMaxC3 + } + + reg.adc1SetCalibrationParam(adcN, finalCode) + return finalCode +} + +// regi2c + +// regI2C on ESP32‑C3 exposes the internal analog I2C bus that controls +// SAR ADC trim registers. Constants below mirror the layout from +// ESP‑IDF's soc/regi2c_saradc.h and TRM (I2C_RTC_CONFIG2 block). +const ( + // i2cSarADC/i2cSarADCHostID select the SAR ADC block on the internal bus. + i2cSarADC = uint8(0x69) + i2cSarADCHostID = uint8(0) + + // adc*_Dref* define the DREF (reference) bitfields for ADC1/ADC2. + adc1DrefAddr = uint8(0x2) + adc1DrefMSB = uint8(6) + adc1DrefLSB = uint8(4) + + adc2DrefAddr = uint8(0x5) + adc2DrefMSB = uint8(6) + adc2DrefLSB = uint8(4) + + // adc*_EncalGnd* control ENCAL_GND: route internal ground to ADC input + // during self‑calibration so that the pin is effectively disconnected. + adc1EncalGndAddr = uint8(0x7) + adc1EncalGndMSB = uint8(5) + adc1EncalGndLSB = uint8(5) + + adc2EncalGndAddr = uint8(0x7) + adc2EncalGndMSB = uint8(7) + adc2EncalGndLSB = uint8(7) + + // adc*_InitCode* hold the INIT_CODE (offset) that hardware uses to + // compensate ADC1/ADC2 offset error. + adc1InitCodeHighAddr = uint8(0x1) + adc1InitCodeHighMSB = uint8(3) + adc1InitCodeHighLSB = uint8(0) + adc1InitCodeLowAddr = uint8(0x0) + adc1InitCodeLowMSB = uint8(7) + adc1InitCodeLowLSB = uint8(0) + + adc2InitCodeHighAddr = uint8(0x4) + adc2InitCodeHighMSB = uint8(3) + adc2InitCodeHighLSB = uint8(0) + adc2InitCodeLowAddr = uint8(0x3) + adc2InitCodeLowMSB = uint8(7) + adc2InitCodeLowLSB = uint8(0) + + // ANA_CONFIG/ANA_CONFIG2: enable analog SAR I2C domain before regI2C access. + anaConfigReg = uintptr(0x6000E044) + i2cSarEnMask = uint32(1 << 18) + anaConfig2Reg = uintptr(0x6000E048) + anaSarCfg2En = uint32(1 << 16) + + // I2C_RTC_CONFIG2 master control register used by regI2C operations. + i2cMstCtrlHost = uintptr(0x6000E000) + i2cMstBusyBit = uint32(1 << 25) + i2cMstWrCntl = uint32(1 << 24) + i2cMstDataMask = uint32(0xFF << 16) + i2cMstDataShift = 16 + i2cMstTimeout = 10000 +) + +type regI2C struct{} + +// sarEnable enables the SAR analog I2C domain before any regI2C access. +func (r *regI2C) sarEnable() { + cfg := (*volatile.Register32)(unsafe.Pointer(anaConfigReg)) + cfg2 := (*volatile.Register32)(unsafe.Pointer(anaConfig2Reg)) + esp.RTC_CNTL.SetANA_CONF_SAR_I2C_PU(1) + cfg.Set(cfg.Get() &^ i2cSarEnMask) + cfg2.Set(cfg2.Get() | anaSarCfg2En) +} + +// adc1CalibrationInit sets DREF for the selected ADC unit +// before running the self‑calibration procedure. +func (r *regI2C) adc1CalibrationInit(adcN uint8) { + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1DrefAddr, adc1DrefMSB, adc1DrefLSB, 1) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2DrefAddr, adc2DrefMSB, adc2DrefLSB, 1) + } +} + +// adc1CalibrationPrepare enables ENCAL_GND so that the ADC input +// is internally shorted to ground during self‑calibration. +func (r *regI2C) adc1CalibrationPrepare(adcN uint8) { + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1EncalGndAddr, adc1EncalGndMSB, adc1EncalGndLSB, 1) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2EncalGndAddr, adc2EncalGndMSB, adc2EncalGndLSB, 1) + } +} + +// adc1CalibrationFinish clears ENCAL_GND and reconnects the ADC +// input back to the external pad after self‑calibration. +func (r *regI2C) adc1CalibrationFinish(adcN uint8) { + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1EncalGndAddr, adc1EncalGndMSB, adc1EncalGndLSB, 0) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2EncalGndAddr, adc2EncalGndMSB, adc2EncalGndLSB, 0) + } +} + +// adc1SetCalibrationParam writes the INIT_CODE (offset trim) for +// the selected ADC unit using the regI2C bitfields. +func (r *regI2C) adc1SetCalibrationParam(adcN uint8, param uint32) { + msb := uint8(param >> 8) + lsb := uint8(param & 0xFF) + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) + } +} + +// waitIdle polls the REGI2C master BUSY bit until it clears or the +// simple software timeout expires. This matches the busy‑wait helper +// used in ESP‑IDF's regi2c_ctrl.c. +func (r *regI2C) waitIdle(reg *volatile.Register32) bool { + for i := 0; i < i2cMstTimeout; i++ { + if reg.Get()&i2cMstBusyBit == 0 { + return true + } + } + return false +} + +// writeMask is a software implementation of REGI2C_WRITE_MASK macro: +// 1. select block + regAddr, +// 2. read current byte, +// 3. update only [msb:lsb] bitfield, +// 4. write it back via internal I2C master. +func (r *regI2C) writeMask(block, hostID, regAddr, msb, lsb, data uint8) { + if hostID != i2cSarADCHostID { + return + } + reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlHost)) + if !r.waitIdle(reg) { + return + } + reg.Set(uint32(block) | uint32(regAddr)<<8) + if !r.waitIdle(reg) { + return + } + cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift + mask := uint32(1<<(msb-lsb+1)-1) << lsb + cur &^= mask + cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb + reg.Set(uint32(block) | uint32(regAddr)<<8 | i2cMstWrCntl | (cur< 20 { + return errors.New("invalid ADC pin for ESP32-S3") + } + a.Pin.Configure(PinConfig{Mode: PinAnalog}) + InitADC() + + return nil +} + +func (a ADC) Get() uint16 { + if a.Pin < 1 || a.Pin > 20 { + return 0 + } + + var ch uint32 + var raw uint32 + if a.Pin <= 10 { + ch = uint32(a.Pin - 1) // GPIO1→ch0 … GPIO10→ch9 + esp.SENS.SetSAR_MEAS1_MUX_SAR1_DIG_FORCE(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) + setSensAtten1(ch, attenDefault) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD(1 << ch) + for esp.SENS.GetSAR_SLAVE_ADDR1_SAR_SARADC_MEAS_STATUS() != 0 { + } + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(1) + for esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DONE_SAR() == 0 { + } + raw = esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DATA_SAR() + } else { + ch = uint32(a.Pin - 11) // GPIO11→ch0 … GPIO20→ch9 + // SENS.SAR_MEAS2_CTRL2: force SW control, select channel + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_FORCE(1) + esp.SENS.SetSAR_MEAS2_CTRL2_SAR2_EN_PAD_FORCE(1) + esp.SENS.SetSAR_MEAS2_CTRL2_SAR2_EN_PAD(1 << ch) + setSensAtten2(ch, attenDefault) + // APB_SARADC.ARB_CTRL: grant ADC2 to APB for oneshot + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(1) + // SENS.SAR_MEAS2_CTRL2.MEAS2_START_SAR: one-shot start + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_SAR(0) + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_SAR(1) + for esp.SENS.GetSAR_MEAS2_CTRL2_MEAS2_DONE_SAR() == 0 { + } + raw = esp.SENS.GetSAR_MEAS2_CTRL2_MEAS2_DATA_SAR() + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(0) + } + + return uint16(raw&0xfff) << 4 +} + +func (a ADC) GetVoltage() (raw uint32, v float64) { + const samples = 4 + var sum uint32 + for i := 0; i < samples; i++ { + sum += uint32(a.Get()) + } + raw = sum / samples + + // Default full-scale for 11 dB is approximately 3.3 V assuming + // Vref ≈ 1.1 V and gain ≈ 3. If eFuse provided a per-chip DIGI_REF + // (Vref in mV) via adcCalibration, use it to adjust the + // full-scale range instead. + scale := 3.3 + if adcDigiRefMv != 0 { + scale = 3.0 * float64(adcDigiRefMv) / 1000.0 + } + + v = float64(raw) / 65520.0 * scale + return raw, v +} + +// ADC hardware self-calibration for ESP32-S3. +// +// Mapping to ESP-IDF (adc_hal_common.c, hal/esp32s3/adc_ll.h): +// - adc_hal_self_calibration() → ADCSelfCalibrate() +// - adc_ll_calibration_init() → regI2C.ADC1CalibrationInit (DREF=4); +// in IDF it is not called from self_cal, we call it explicitly. +// - adc_ll_calibration_prepare() → SarEnable + ADC1CalibrationPrepare (ENCAL_GND=1) +// - adc_ll_calibration_finish() → ADC1CalibrationFinish (ENCAL_GND=0) +// - adc_ll_set_calibration_param() → ADC1SetCalibrationParam() +// - read_cal_channel() → adcCalibration.readADC1(): +// wait for meas_status==0, start 0→1, wait done, read data +// (similar to adc_oneshot_ll_start + get_raw_result). +// - Loop: 10 iterations, code 0..4096, binary search on self_cal==0; drop min/max; +// rounding (remainder%8 < 4 without +1, otherwise +1) — same as in adc_hal_common.c. +// - raw_check_valid: for ADC1 in IDF always true — we do not check it. +// +// Differences: +// - regI2C: not ROM helper but direct access to 0x6000E000 (protocol like I2C_RTC_CONFIG2). +// - cal_setup: same SENS/atten/controller fields, but through our registers. +// - Result is stored only in hardware for the current session (not in eFuse). +// - eFuse V1: init_code and digi_ref are taken from eFuse — same idea as Arduino/IDF. + +const ( + adcCalTimes = 10 + adcCalOffsetMax = uint32(4096) + adcCalRtcMagic = uint32(0xADC1C401) + adcCalInitMin = uint32(2000) + adcCalInitMax = uint32(3900) + adcDigiRefMinMv = uint32(920) + adcDigiRefMaxMv = uint32(1150) +) + +// adcCalibration encapsulates the self-calibration flow for ADC1 +// and remembers per-chip calibration data (such as DIGI_REF) when it is +// available from eFuse. +type adcCalibration struct { + digiRefMv uint32 +} + +func (c *adcCalibration) calibrate() { + reg := regI2C{} + f := fuse{} + + if vref, ok := f.adc1DigiRefAtten3(); ok { + c.digiRefMv = vref + } + + if saved, ok := c.restoreFromRTC(); ok { + reg.sarEnable() + reg.adc1CalibrationInit(0) + c.adc1CalibrateHigh(reg, saved) + return + } + + initCode, useEfuse := f.adc1InitCodeAtten3() + c.adc1CalibrationSetup(reg) + + if useEfuse { + c.saveToRTC(initCode) + c.adc1CalibrateHigh(reg, initCode) + return + } + + finalCode := c.adc1CalibrateLow(reg) + c.saveToRTC(finalCode) + c.adc1CalibrateHigh(reg, finalCode) +} + +func (c *adcCalibration) getDigiRef() uint32 { + return c.digiRefMv +} + +func (c *adcCalibration) adc1CalibrationSetup(reg regI2C) { + reg.sarEnable() + + esp.SENS.SetSAR_MEAS1_MUX_SAR1_DIG_FORCE(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(0) + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_FORCE(0) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD(0) + setSensAtten1(0, attenDefault) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) + + reg.adc1CalibrationInit(0) + reg.adc1CalibrationPrepare(0) +} + +func (c *adcCalibration) adc1CalibrateLow(reg regI2C) uint32 { + var codeList [adcCalTimes]uint32 + var codeSum uint32 + + for rpt := 0; rpt < adcCalTimes; rpt++ { + codeH := adcCalOffsetMax + codeL := uint32(0) + chkCode := (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(0, chkCode) + selfCal := c.readADC1() + + for codeH-codeL > 1 { + if selfCal == 0 { + codeH = chkCode + } else { + codeL = chkCode + } + chkCode = (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(0, chkCode) + selfCal = c.readADC1() + if codeH-codeL == 1 { + chkCode++ + reg.adc1SetCalibrationParam(0, chkCode) + selfCal = c.readADC1() + } + } + codeList[rpt] = chkCode + codeSum += chkCode + } + + codeL := codeList[0] + codeH := codeList[0] + for i := 0; i < adcCalTimes; i++ { + if codeList[i] < codeL { + codeL = codeList[i] + } + if codeList[i] > codeH { + codeH = codeList[i] + } + } + excluded := codeH + codeL + remaining := codeSum - excluded + finalCode := remaining / (adcCalTimes - 2) + if remaining%(adcCalTimes-2) >= 4 { + finalCode++ + } + + return finalCode +} + +func (c *adcCalibration) adc1CalibrateHigh(reg regI2C, code uint32) { + reg.adc1SetCalibrationParam(0, code) + reg.adc1CalibrationFinish(0) + c.adc1StartWithPadForce() +} + +func (c *adcCalibration) adc1StartWithPadForce() { + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) +} + +// readADC1 performs one ADC1 conversion via RTC path (used during calibration). +// Internal GND is connected via ENCAL_GND, so the pin input is disconnected. +// Matches IDF: wait conversion idle (meas_status==0), then start 0→1, wait done, read data. +func (c *adcCalibration) readADC1() uint32 { + for esp.SENS.GetSAR_SLAVE_ADDR1_SAR_SARADC_MEAS_STATUS() != 0 { + } + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(1) + for esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DONE_SAR() == 0 { + } + return uint32(esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DATA_SAR() & 0xfff) +} + +func (c *adcCalibration) restoreFromRTC() (uint32, bool) { + if esp.RTC_CNTL.GetSTORE0() != adcCalRtcMagic { + return 0, false + } + code := esp.RTC_CNTL.GetSTORE1() + if code < adcCalInitMin || code > adcCalInitMax { + return 0, false + } + return code, true +} + +func (c *adcCalibration) saveToRTC(code uint32) { + esp.RTC_CNTL.SetSTORE0(adcCalRtcMagic) + esp.RTC_CNTL.SetSTORE1(code) +} + +// regI2C — internal I2C for SAR ADC (ESP32-S2 I2C_RTC_CONFIG2, reg 0x6000E000). +// Source: idf-source/components/soc/esp32s3/include/soc/regi2c_saradc.h + +const ( + // I2C_SAR_ADC / I2C_SAR_ADC_HOSTID in regi2c_saradc.h + i2cSarADC = uint8(0x69) // I2C_SAR_ADC + i2cSarADCHostID = uint8(1) // I2C_SAR_ADC_HOSTID + + // ADC_SAR1_DREF_ADDR(_MSB/_LSB) + adc1DrefAddr = uint8(0x2) // ADC_SAR1_DREF_ADDR + adc1DrefMSB = uint8(6) // ADC_SAR1_DREF_ADDR_MSB + adc1DrefLSB = uint8(4) // ADC_SAR1_DREF_ADDR_LSB + + // ADC_SAR2_DREF_ADDR(_MSB/_LSB) + adc2DrefAddr = uint8(0x5) // ADC_SAR2_DREF_ADDR + adc2DrefMSB = uint8(6) // ADC_SAR2_DREF_ADDR_MSB + adc2DrefLSB = uint8(4) // ADC_SAR2_DREF_ADDR_LSB + + // ADC_SAR1_ENCAL_GND_ADDR(_MSB/_LSB) + adc1EncalGndAddr = uint8(0x7) // ADC_SAR1_ENCAL_GND_ADDR + adc1EncalGndMSB = uint8(5) // ADC_SAR1_ENCAL_GND_ADDR_MSB + adc1EncalGndLSB = uint8(5) // ADC_SAR1_ENCAL_GND_ADDR_LSB + + // ADC_SAR2_ENCAL_GND_ADDR(_MSB/_LSB) + adc2EncalGndAddr = uint8(0x7) // ADC_SAR2_ENCAL_GND_ADDR + adc2EncalGndMSB = uint8(7) // ADC_SAR2_ENCAL_GND_ADDR_MSB + adc2EncalGndLSB = uint8(7) // ADC_SAR2_ENCAL_GND_ADDR_LSB + + // ADC_SAR1_INITIAL_CODE_HIGH/LOW_ADDR(_MSB/_LSB) + adc1InitCodeHighAddr = uint8(0x1) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR + adc1InitCodeHighMSB = uint8(3) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR_MSB + adc1InitCodeHighLSB = uint8(0) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR_LSB + adc1InitCodeLowAddr = uint8(0x0) // ADC_SAR1_INITIAL_CODE_LOW_ADDR + adc1InitCodeLowMSB = uint8(7) // ADC_SAR1_INITIAL_CODE_LOW_ADDR_MSB + adc1InitCodeLowLSB = uint8(0) // ADC_SAR1_INITIAL_CODE_LOW_ADDR_LSB + + // ADC_SAR2_INITIAL_CODE_HIGH/LOW_ADDR(_MSB/_LSB) + adc2InitCodeHighAddr = uint8(0x4) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR + adc2InitCodeHighMSB = uint8(3) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR_MSB + adc2InitCodeHighLSB = uint8(0) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR_LSB + adc2InitCodeLowAddr = uint8(0x3) // ADC_SAR2_INITIAL_CODE_LOW_ADDR + adc2InitCodeLowMSB = uint8(7) // ADC_SAR2_INITIAL_CODE_LOW_ADDR_MSB + adc2InitCodeLowLSB = uint8(0) // ADC_SAR2_INITIAL_CODE_LOW_ADDR_LSB + + // Analog config registers for regI2C block (RTC/ANA config in TRM). + anaConfigReg = uintptr(0x6000E044) + i2cSarEnMask = uint32(1 << 18) + anaConfig2Reg = uintptr(0x6000E048) + anaSarCfg2En = uint32(1 << 16) + + // REGI2C master control register and helper masks. + i2cMstCtrlHost1 = uintptr(0x6000E000) + i2cMstBusyBit = uint32(1 << 25) + i2cMstWrCntlBit = uint32(1 << 24) + i2cMstDataMask = uint32(0xFF << 16) + i2cMstDataShift = 16 + i2cMstBusyTimeout = 10000 +) + +type regI2C struct{} + +// waitIdle mimics the IDF regi2c busy-wait helper (see regi2c_ctrl.c). +// It polls the REGI2C master control register until the BUSY bit clears +// or a small timeout expires, to avoid writing while a previous transfer +// is still in progress. +func (r *regI2C) waitIdle(reg *volatile.Register32) bool { + for i := 0; i < i2cMstBusyTimeout; i++ { + if reg.Get()&i2cMstBusyBit == 0 { + return true + } + } + return false +} + +// writeMask is a software implementation of the REGI2C_WRITE_MASK macro +// from IDF (see soc/regi2c_saradc.h). It: +// - selects the regI2C SAR ADC block + register address, +// - reads the current byte, +// - updates only the [msb:lsb] bitfield, +// - writes the new value back via the internal I2C master. +func (r *regI2C) writeMask(block, hostID, regAddr, msb, lsb, data uint8) { + if hostID != i2cSarADCHostID { + return + } + reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlHost1)) + if !r.waitIdle(reg) { + return + } + reg.Set(uint32(block) | uint32(regAddr)<<8) + if !r.waitIdle(reg) { + return + } + cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift + mask := uint32(1<<(msb-lsb+1)-1) << lsb + cur &^= mask + cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb + reg.Set(uint32(block) | uint32(regAddr)<<8 | i2cMstWrCntlBit | (cur<> 8) + lsb := uint8(param & 0xFF) + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) + } +} + +// fuse +const ( + // Base address for eFuse controller (EFUSE_BLKx region in TRM). + efuseBase = uintptr(0x60007000) + + // EFUSE_*_REG offsets mirror ESP-IDF's efuse_reg.h layout. + efuseClkReg = efuseBase + 0x1c8 + efuseConfReg = efuseBase + 0x1cc + efuseCmdReg = efuseBase + 0x1d4 + efuseDacConfReg = efuseBase + 0x1e8 + efuseWrTimConf1Reg = efuseBase + 0x1f4 + efuseWrTimConf2Reg = efuseBase + 0x1f8 + efuseRdData4Reg = efuseBase + 0x6c // EFUSE_RD_WR_DIS_REG / RD_DATA4 + efuseRdData5Reg = efuseBase + 0x70 // EFUSE_RD_REPEAT_DATA1_REG / RD_DATA5 + efuseRdData7Reg = efuseBase + 0x78 // EFUSE_RD_REPEAT_DATA3_REG / RD_DATA7 + + // Read opcode and clock enable bit used by EFUSE HAL (see efuse_ll). + efuseReadOpCode = uint32(0x5AA5) + efuseClkEnBit = uint32(1 << 16) + efuseBlkVersionV1 = 1 // EFUSE_BLK_VERSION major version = 1 + + // SYSTEM_PERIP_CLK_EN0 register and EFUSE clock gate bit. + systemPeripClkEn0 = uintptr(0x600C0018) + systemEfuseClkEnBit = uint32(1 << 14) +) + +type fuse struct{} + +// adc1InitCodeAtten3 extracts the ADC1 INIT_CODE (offset trim) for +// attenuation index 3 (typically 11 dB) from EFUSE_BLK2. This mirrors +// the logic used by ESP-IDF's ADC calibration HAL for ESP32-S3. +// +// The code is built from four differential eFuse fields (diff0..diff3) +// and constant offsets (1850, 90, 70) as described in Espressif's +// internal calibration formulas. +func (f *fuse) adc1InitCodeAtten3() (uint32, bool) { + for try := 0; try < 2; try++ { + f.triggerReadSequence() + data4, data5, blkVer := f.readBlock2Data4Data5() + if blkVer != efuseBlkVersionV1 { + continue + } + diff0 := (data4 >> 21) & 0xFF + diff1 := (data4 >> 29) | ((data5 & 7) << 3) + diff2 := (data5 >> 3) & 0x3F + diff3 := (data5 >> 9) & 0x3F + icode0 := diff0 + 1850 + icode1 := diff1 + icode0 + 90 + icode2 := diff2 + icode1 + icode3 := diff3 + icode2 + 70 + if icode3 >= adcCalInitMin && icode3 <= adcCalInitMax { + return icode3, true + } + } + return 0, false +} + +// adc1DigiRefAtten3 reads the digital reference (DIGI_REF) for +// ADC1 at attenuation index 3 from EFUSE_BLK2 / RD_DATA7. This is +// similar to what the ESP-IDF ADC calibration HAL uses when present. +func (f *fuse) adc1DigiRefAtten3() (uint32, bool) { + f.triggerReadSequence() + _, _, blkVer := f.readBlock2Data4Data5() + if blkVer != efuseBlkVersionV1 { + return 0, false + } + data7 := f.readBlock2Data7() + diff3 := (data7 >> 1) & 0xFF + digiRef := diff3 + 900 + if digiRef < adcDigiRefMinMv || digiRef > adcDigiRefMaxMv { + return 0, false + } + return digiRef, true +} + +// triggerReadSequence performs one eFuse read operation using the +// controller's timing/opcode sequence. This roughly corresponds to +// the low-level logic in the ESP-IDF eFuse HAL (see efuse_ll_* in +// the IDF sources and the "eFuse Manager" docs: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/system/efuse.html). +func (f *fuse) triggerReadSequence() { + clk := (*volatile.Register32)(unsafe.Pointer(systemPeripClkEn0)) + clk.Set(clk.Get() | systemEfuseClkEnBit) + efuseClk := (*volatile.Register32)(unsafe.Pointer(efuseClkReg)) + efuseClk.Set(efuseClk.Get() | efuseClkEnBit) + dac := (*volatile.Register32)(unsafe.Pointer(efuseDacConfReg)) + dac.Set(0x28 | (0xFF << 9)) + (*volatile.Register32)(unsafe.Pointer(efuseWrTimConf1Reg)).Set(0x3000 << 8) + (*volatile.Register32)(unsafe.Pointer(efuseWrTimConf2Reg)).Set(0x190) + (*volatile.Register32)(unsafe.Pointer(efuseConfReg)).Set(efuseReadOpCode) + cmd := (*volatile.Register32)(unsafe.Pointer(efuseCmdReg)) + cmd.Set(1) + for cmd.Get()&1 != 0 { + } +} + +// readBlock2Data4Data5 reads the EFUSE_BLK2 data words that contain +// ADC calibration and version information. It returns RD_DATA4, +// RD_DATA5 and the decoded block version (BLK_VERSION). +// +// Layout is derived from the ESP32-S3 TRM and IDF eFuse tables. +func (f *fuse) readBlock2Data4Data5() (data4, data5 uint32, blkVer uint8) { + data4 = (*volatile.Register32)(unsafe.Pointer(efuseRdData4Reg)).Get() + data5 = (*volatile.Register32)(unsafe.Pointer(efuseRdData5Reg)).Get() + blkVer = uint8(data4 & 3) + return data4, data5, blkVer +} + +// readBlock2Data7 reads RD_DATA7 from EFUSE_BLK2, which for ADC +// calibration contains additional reference (DIGI_REF) data fields. +func (f *fuse) readBlock2Data7() uint32 { + return (*volatile.Register32)(unsafe.Pointer(efuseRdData7Reg)).Get() +} + +// readAdcCalibBlock2 triggers an eFuse read and returns the raw +// EFUSE_BLK2 words used for ADC calibration (RD_DATA4/5) along +// with the decoded block version. This is a small helper similar +// in spirit to the internal IDF helpers around EFUSE_BLK2. +func (f *fuse) readAdcCalibBlock2() (data4, data5 uint32, blkVer uint8) { + f.triggerReadSequence() + return f.readBlock2Data4Data5() +} From f195d09f25b17e29397b257975103df63e9e2c92 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Mon, 2 Mar 2026 00:25:56 +0100 Subject: [PATCH 40/81] usb/cdc: Better ring buffer implementation (#5209) * begin adding ring512 implementation * refactor to make operation driven fuzz test * refactor USBCDC.Read to use ring512 * try txhandler separate to flush * working USBCDC with large packets * fix binary size * remove comment * documentation improvements --- builder/sizes_test.go | 2 +- src/examples/serial-stress/main.go | 29 ++ src/machine/machine_atsamd51.go | 1 + src/machine/usb/cdc/buffer.go | 119 ------ src/machine/usb/cdc/cdc.go | 7 +- src/machine/usb/cdc/ring.go | 98 +++++ src/machine/usb/cdc/ring_test.go | 658 +++++++++++++++++++++++++++++ src/machine/usb/cdc/usbcdc.go | 142 ++++--- 8 files changed, 875 insertions(+), 181 deletions(-) create mode 100644 src/examples/serial-stress/main.go delete mode 100644 src/machine/usb/cdc/buffer.go create mode 100644 src/machine/usb/cdc/ring.go create mode 100644 src/machine/usb/cdc/ring_test.go diff --git a/builder/sizes_test.go b/builder/sizes_test.go index 0f52f3c6be..4b0d6301ea 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -44,7 +44,7 @@ func TestBinarySize(t *testing.T) { // microcontrollers {"hifive1b", "examples/echo", 3668, 280, 0, 2244}, {"microbit", "examples/serial", 2694, 342, 8, 2248}, - {"wioterminal", "examples/pininterrupt", 6837, 1491, 120, 6888}, + {"wioterminal", "examples/pininterrupt", 7074, 1510, 120, 7248}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/src/examples/serial-stress/main.go b/src/examples/serial-stress/main.go new file mode 100644 index 0000000000..cdda2cfa7c --- /dev/null +++ b/src/examples/serial-stress/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "machine" + "strconv" + "time" +) + +func main() { + time.Sleep(2 * time.Second) // connect via serial + buf1 := makeBuffer('|', 600) + buf2 := makeBuffer('/', 600) + println("start") + serialWrite(buf1) + serialWrite(buf2) +} + +func makeBuffer(sep byte, size int) []byte { + buf := make([]byte, size) + for i := 0; i < size-5; i += 5 { + buf[i] = sep + strconv.AppendInt(buf[i+1:i+1:i+5], int64(i), 10) + } + return buf +} + +func serialWrite(b []byte) { + machine.Serial.Write(b) +} diff --git a/src/machine/machine_atsamd51.go b/src/machine/machine_atsamd51.go index 20dc41b8de..158be4911b 100644 --- a/src/machine/machine_atsamd51.go +++ b/src/machine/machine_atsamd51.go @@ -11,6 +11,7 @@ import ( "device/sam" "errors" "internal/binary" + "runtime/interrupt" "unsafe" ) diff --git a/src/machine/usb/cdc/buffer.go b/src/machine/usb/cdc/buffer.go deleted file mode 100644 index ad5eb3645b..0000000000 --- a/src/machine/usb/cdc/buffer.go +++ /dev/null @@ -1,119 +0,0 @@ -package cdc - -import ( - "runtime/volatile" -) - -const rxRingBufferSize = 128 - -// rxRingBuffer is ring buffer implementation inspired by post at -// https://www.embeddedrelated.com/showthread/comp.arch.embedded/77084-1.php -type rxRingBuffer struct { - buffer [rxRingBufferSize]volatile.Register8 - head volatile.Register8 - tail volatile.Register8 -} - -// NewRxRingBuffer returns a new ring buffer. -func NewRxRingBuffer() *rxRingBuffer { - return &rxRingBuffer{} -} - -// Used returns how many bytes in buffer have been used. -func (rb *rxRingBuffer) Used() uint8 { - return uint8(rb.head.Get() - rb.tail.Get()) -} - -// Put stores a byte in the buffer. If the buffer is already -// full, the method will return false. -func (rb *rxRingBuffer) Put(val byte) bool { - if rb.Used() != rxRingBufferSize { - rb.head.Set(rb.head.Get() + 1) - rb.buffer[rb.head.Get()%rxRingBufferSize].Set(val) - return true - } - return false -} - -// Get returns a byte from the buffer. If the buffer is empty, -// the method will return a false as the second value. -func (rb *rxRingBuffer) Get() (byte, bool) { - if rb.Used() != 0 { - rb.tail.Set(rb.tail.Get() + 1) - return rb.buffer[rb.tail.Get()%rxRingBufferSize].Get(), true - } - return 0, false -} - -// Clear resets the head and tail pointer to zero. -func (rb *rxRingBuffer) Clear() { - rb.head.Set(0) - rb.tail.Set(0) -} - -const txRingBufferSize = 8 - -// txRingBuffer is ring buffer implementation inspired by post at -// https://www.embeddedrelated.com/showthread/comp.arch.embedded/77084-1.php -type txRingBuffer struct { - buffer [txRingBufferSize]struct { - buf [64]byte - size int - } - head volatile.Register8 - tail volatile.Register8 -} - -// NewTxRingBuffer returns a new ring buffer. -func NewTxRingBuffer() *txRingBuffer { - return &txRingBuffer{} -} - -// Used returns how many bytes in buffer have been used. -func (rb *txRingBuffer) Used() uint8 { - return uint8(rb.head.Get() - rb.tail.Get()) -} - -// Put stores a byte in the buffer. If the buffer is already -// full, the method will return false. -func (rb *txRingBuffer) Put(val []byte) bool { - if rb.Used() == txRingBufferSize { - return false - } - - if rb.Used() == 0 { - rb.head.Set(rb.head.Get() + 1) - rb.buffer[rb.head.Get()%txRingBufferSize].size = 0 - } - buf := &rb.buffer[rb.head.Get()%txRingBufferSize] - - for i := 0; i < len(val); i++ { - if buf.size == 64 { - // next - // TODO: Make sure that data is not corrupted even when the buffer is full - rb.head.Set(rb.head.Get() + 1) - buf = &rb.buffer[rb.head.Get()%txRingBufferSize] - rb.buffer[rb.head.Get()%txRingBufferSize].size = 0 - } - buf.buf[buf.size] = val[i] - buf.size++ - } - return true -} - -// Get returns a byte from the buffer. If the buffer is empty, -// the method will return a false as the second value. -func (rb *txRingBuffer) Get() ([]byte, bool) { - if rb.Used() != 0 { - rb.tail.Set(rb.tail.Get() + 1) - size := rb.buffer[rb.tail.Get()%txRingBufferSize].size - return rb.buffer[rb.tail.Get()%txRingBufferSize].buf[:size], true - } - return nil, false -} - -// Clear resets the head and tail pointer to zero. -func (rb *txRingBuffer) Clear() { - rb.head.Set(0) - rb.tail.Set(0) -} diff --git a/src/machine/usb/cdc/cdc.go b/src/machine/usb/cdc/cdc.go index f180535df1..44bd7f7e0e 100644 --- a/src/machine/usb/cdc/cdc.go +++ b/src/machine/usb/cdc/cdc.go @@ -1,3 +1,5 @@ +//go:build baremetal + package cdc const ( @@ -9,10 +11,7 @@ const ( // New returns USBCDC struct. func New() *USBCDC { if USB == nil { - USB = &USBCDC{ - rxBuffer: NewRxRingBuffer(), - txBuffer: NewTxRingBuffer(), - } + USB = &USBCDC{} } return USB } diff --git a/src/machine/usb/cdc/ring.go b/src/machine/usb/cdc/ring.go new file mode 100644 index 0000000000..ca8ce4b175 --- /dev/null +++ b/src/machine/usb/cdc/ring.go @@ -0,0 +1,98 @@ +package cdc + +import "sync/atomic" + +// ring512 is an interrupt/concurrent-safe ring buffer for a single-producer, +// single-consumer (SPSC) pair. The writer calls Put, the reader calls +// Peek/Discard. Reset may only be called when neither side is active. +// +// Implementation uses monotonic counters (head/tail) instead of bounded +// offsets. Unsigned subtraction (head - tail) always yields +// correct used count regardless of uint32 overflow. +type ring512 struct { + buf [ringBufLen]byte // power of 2 so compiler can optimize & mask. + // head counts total bytes written. Only the writer stores to head. + head atomic.Uint32 + // tail counts total bytes read. Only the reader stores to tail. + tail atomic.Uint32 +} + +const ( + ringBufLen = 512 + ringMask = ringBufLen - 1 // 0x1FF +) + +// Reset empties the ring buffer. Must not be called concurrently with +// Put, Peek, or Discard. +func (r *ring512) Reset() { + r.head.Store(0) + r.tail.Store(0) +} + +// Free returns number of bytes that can be written via Put. +func (r *ring512) Free() uint32 { + return ringBufLen - r.Used() +} + +// Used returns number of bytes ready to be peeked/discarded. +func (r *ring512) Used() uint32 { + return r.head.Load() - r.tail.Load() +} + +// Peek returns contiguous views into the readable portions of the buffer +// without advancing the read position. When data wraps around the end of +// the internal buffer, two segments are returned. Second data2 is nil on fully contiguous buffer. +// Returns nil,nil when empty. +func (r *ring512) Peek() (data1, data2 []byte) { + head, tail := r.lims() + used := head - tail + if used == 0 { + return nil, nil + } + pos := tail & ringMask + contig := ringBufLen - pos + if contig >= used { + return r.buf[pos : pos+used], nil + } + return r.buf[pos:], r.buf[:used-contig] +} + +// Discard marks numBytes as read, advancing the read position. +// Panics if numBytes exceeds Used (indicates a race violating SPSC) +func (r *ring512) Discard(numBytes uint32) { + if numBytes == 0 { + return + } + head, tail := r.lims() + used := head - tail + if numBytes > used { + panic("ring: discard exceeds used") + } + r.tail.Store(tail + numBytes) +} + +// Put writes data into the ring buffer. Returns true if all data was +// written, false if insufficient free space (no partial writes). +func (r *ring512) Put(data []byte) bool { + wlen := uint32(len(data)) + if wlen == 0 { + return true + } + head, tail := r.lims() + used := head - tail + free := uint32(ringBufLen) - used + if wlen > free { + return false + } + pos := head & ringMask + n := uint32(copy(r.buf[pos:], data)) + if n < wlen { + copy(r.buf[:], data[n:]) + } + r.head.Store(head + wlen) + return true +} + +func (r *ring512) lims() (head, tail uint32) { + return r.head.Load(), r.tail.Load() +} diff --git a/src/machine/usb/cdc/ring_test.go b/src/machine/usb/cdc/ring_test.go new file mode 100644 index 0000000000..e79581a5b4 --- /dev/null +++ b/src/machine/usb/cdc/ring_test.go @@ -0,0 +1,658 @@ +package cdc + +import ( + "bytes" + "fmt" + "math/rand" + "sync" + "testing" +) + +// peekAll returns all readable data by concatenating both Peek segments. +func peekAll(r *ring512) []byte { + d1, d2 := r.Peek() + if len(d2) == 0 { + return d1 + } + out := make([]byte, len(d1)+len(d2)) + copy(out, d1) + copy(out[len(d1):], d2) + return out +} + +// drain reads all data from the ring, verifying Peek length matches Used. +func drain(t *testing.T, r *ring512) []byte { + t.Helper() + d1, d2 := r.Peek() + n := uint32(len(d1) + len(d2)) + if n != r.Used() { + t.Fatalf("Peek returned %d bytes but Used()=%d", n, r.Used()) + } + var out []byte + out = append(out, d1...) + out = append(out, d2...) + r.Discard(n) + return out +} + +// --- Basic Functionality --- + +func TestRing512_PutPeekDiscard(t *testing.T) { + var r ring512 + data := []byte("hello world") + if !r.Put(data) { + t.Fatal("Put failed on empty buffer") + } + got := peekAll(&r) + if !bytes.Equal(got, data) { + t.Fatalf("Peek = %q, want %q", got, data) + } + if r.Used() != uint32(len(data)) { + t.Fatalf("Used = %d, want %d", r.Used(), len(data)) + } + r.Discard(uint32(len(data))) + if r.Used() != 0 { + t.Fatalf("Used after full discard = %d, want 0", r.Used()) + } + d1, d2 := r.Peek() + if d1 != nil || d2 != nil { + t.Fatalf("Peek after full discard = (%v, %v), want (nil, nil)", d1, d2) + } +} + +func TestRing512_Reset(t *testing.T) { + var r ring512 + r.Put([]byte("data")) + r.Reset() + if r.Used() != 0 { + t.Fatalf("Used after Reset = %d", r.Used()) + } + if r.Free() != 512 { + t.Fatalf("Free after Reset = %d", r.Free()) + } +} + +func TestRing512_PutEmpty(t *testing.T) { + var r ring512 + if !r.Put(nil) { + t.Fatal("Put nil should succeed") + } + if !r.Put([]byte{}) { + t.Fatal("Put empty slice should succeed") + } + if r.Used() != 0 { + t.Fatalf("Used = %d after empty puts", r.Used()) + } +} + +func TestRing512_PutFull(t *testing.T) { + var r ring512 + data := make([]byte, 512) + for i := range data { + data[i] = byte(i) + } + if !r.Put(data) { + t.Fatal("Put 512 bytes failed on empty buffer") + } + if r.Free() != 0 { + t.Fatalf("Free after filling = %d", r.Free()) + } + if r.Put([]byte{0x42}) { + t.Fatal("Put on full buffer should fail") + } + got := peekAll(&r) + if !bytes.Equal(got, data) { + t.Fatalf("Peek full buffer: got len %d, want 512", len(got)) + } +} + +func TestRing512_PutExactFit(t *testing.T) { + var r ring512 + data := make([]byte, 512) + for i := range data { + data[i] = byte(i) + } + if !r.Put(data) { + t.Fatal("Put exact fit failed") + } + if r.Used() != 512 { + t.Fatalf("Used = %d, want 512", r.Used()) + } + r.Discard(512) + if r.Used() != 0 { + t.Fatal("buffer not empty after discard all") + } +} + +// --- Full buffer with wrapped position --- + +func TestRing512_FullBufferWrapped(t *testing.T) { + var r ring512 + + r.Put(make([]byte, 200)) + r.Discard(100) // tail=100, head=200, used=100 + + free := r.Free() + if free != 412 { + t.Fatalf("Free = %d, want 412", free) + } + fill := make([]byte, free) + for i := range fill { + fill[i] = byte(i) + } + if !r.Put(fill) { + t.Fatalf("Put(%d) into %d free space failed", free, free) + } + if r.Used() != 512 { + t.Fatalf("Used = %d, want 512 (full)", r.Used()) + } + if r.Free() != 0 { + t.Fatalf("Free = %d, want 0 (full)", r.Free()) + } + drained := drain(t, &r) + if len(drained) != 512 { + t.Fatalf("drained %d bytes, want 512", len(drained)) + } +} + +// --- Wrapping Tests --- + +func TestRing512_Wrap(t *testing.T) { + var r ring512 + r.Put(make([]byte, 500)) + r.Discard(490) // tail=490, head=500, used=10 + + wrapData := make([]byte, 30) + for i := range wrapData { + wrapData[i] = byte(i + 100) + } + if !r.Put(wrapData) { + t.Fatal("wrapped Put failed") + } + if r.Used() != 40 { + t.Fatalf("Used = %d, want 40", r.Used()) + } + + d1, d2 := r.Peek() + if len(d1)+len(d2) != 40 { + t.Fatalf("Peek total = %d, want 40", len(d1)+len(d2)) + } + if d2 == nil { + t.Fatal("expected wrapped data in d2") + } + drained := drain(t, &r) + if len(drained) != 40 { + t.Fatalf("drained %d bytes, want 40", len(drained)) + } +} + +func TestRing512_WrapDataIntegrity(t *testing.T) { + var r ring512 + r.Put(make([]byte, 500)) + r.Discard(500) + + data := make([]byte, 100) + for i := range data { + data[i] = byte(i) + } + if !r.Put(data) { + t.Fatal("wrapped put failed") + } + got := drain(t, &r) + if !bytes.Equal(got, data) { + t.Fatal("data integrity failure across wrap") + } +} + +// --- Edge Cases --- + +func TestRing512_DiscardPartial(t *testing.T) { + var r ring512 + r.Put([]byte("abcdefgh")) + r.Discard(3) + got := peekAll(&r) + if !bytes.Equal(got, []byte("defgh")) { + t.Fatalf("after partial discard, Peek = %q, want %q", got, "defgh") + } +} + +func TestRing512_DiscardZero(t *testing.T) { + var r ring512 + r.Discard(0) + r.Put([]byte("hi")) + r.Discard(0) + if r.Used() != 2 { + t.Fatalf("Used = %d after zero discard", r.Used()) + } +} + +func TestRing512_DiscardPanicOnOverread(t *testing.T) { + var r ring512 + r.Put([]byte("hi")) + defer func() { + if rec := recover(); rec == nil { + t.Fatal("expected panic on over-discard, got none") + } + }() + r.Discard(100) +} + +func TestRing512_FreeUsedInvariant(t *testing.T) { + var r ring512 + check := func(label string) { + if r.Free()+r.Used() != 512 { + t.Fatalf("%s: Free(%d) + Used(%d) != 512", label, r.Free(), r.Used()) + } + } + check("empty") + r.Put(make([]byte, 200)) + check("after put 200") + r.Discard(50) + check("after discard 50") + r.Put(make([]byte, 362)) + check("after fill to full") + r.Discard(512) + check("after drain") +} + +func TestRing512_PutOversize(t *testing.T) { + var r ring512 + if r.Put(make([]byte, 513)) { + t.Fatal("Put(513) should fail on empty 512 buffer") + } + r.Put(make([]byte, 1)) + if r.Put(make([]byte, 512)) { + t.Fatal("Put(512) should fail with 1 byte used") + } +} + +func TestRing512_MultiplePutPeekDiscard(t *testing.T) { + var r ring512 + for i := 0; i < 2000; i++ { + msg := []byte(fmt.Sprintf("msg%04d", i)) + if !r.Put(msg) { + t.Fatalf("Put failed at iteration %d, Free=%d, Used=%d", i, r.Free(), r.Used()) + } + got := drain(t, &r) + if !bytes.Equal(got, msg) { + t.Fatalf("iter %d: got %q, want %q", i, got, msg) + } + } +} + +func TestRing512_HeadTailOverflow(t *testing.T) { + var r ring512 + near := uint32(0xFFFFFFFF - 100) + r.head.Store(near) + r.tail.Store(near) + + if r.Used() != 0 || r.Free() != 512 { + t.Fatalf("Used=%d Free=%d, want 0/512", r.Used(), r.Free()) + } + + for i := 0; i < 300; i++ { + data := []byte{byte(i), byte(i + 1), byte(i + 2)} + if !r.Put(data) { + t.Fatalf("Put failed at iter %d (head=%d tail=%d)", i, r.head.Load(), r.tail.Load()) + } + got := drain(t, &r) + if !bytes.Equal(got, data) { + t.Fatalf("iter %d: data mismatch", i) + } + } +} + +// --- Peek two-segment tests --- + +func TestRing512_PeekNoWrap(t *testing.T) { + var r ring512 + r.Put([]byte("hello")) + d1, d2 := r.Peek() + if !bytes.Equal(d1, []byte("hello")) { + t.Fatalf("d1 = %q, want %q", d1, "hello") + } + if d2 != nil { + t.Fatalf("d2 = %v, want nil", d2) + } +} + +func TestRing512_PeekWrapped(t *testing.T) { + var r ring512 + r.Put(make([]byte, 508)) + r.Discard(508) // tail=508, head=508 + + data := []byte("abcdefghij") // 10 bytes: 4 at end, 6 at start + if !r.Put(data) { + t.Fatal("put failed") + } + d1, d2 := r.Peek() + if len(d1) != 4 { + t.Fatalf("d1 len = %d, want 4", len(d1)) + } + if len(d2) != 6 { + t.Fatalf("d2 len = %d, want 6", len(d2)) + } + var got []byte + got = append(got, d1...) + got = append(got, d2...) + if !bytes.Equal(got, data) { + t.Fatalf("got %q, want %q", got, data) + } +} + +func TestRing512_PeekEmpty(t *testing.T) { + var r ring512 + d1, d2 := r.Peek() + if d1 != nil || d2 != nil { + t.Fatalf("Peek on empty = (%v, %v), want (nil, nil)", d1, d2) + } +} + +func TestRing512_PeekTotalEqualsUsed(t *testing.T) { + var r ring512 + // Test at many wrap positions. + for offset := 0; offset < 512; offset += 37 { + r.Reset() + if offset > 0 { + r.Put(make([]byte, offset)) + r.Discard(uint32(offset)) + } + sz := 200 + r.Put(make([]byte, sz)) + d1, d2 := r.Peek() + total := len(d1) + len(d2) + if total != sz { + t.Fatalf("offset=%d: Peek total=%d, want %d", offset, total, sz) + } + } +} + +// --- Concurrent SPSC Test --- + +func TestRing512_SPSC(t *testing.T) { + for trial := 0; trial < 20; trial++ { + var r ring512 + const totalBytes = 1 << 18 + produced := make([]byte, totalBytes) + for i := range produced { + produced[i] = byte(i + trial) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + sent := 0 + for sent < totalBytes { + chunkSize := 1 + rand.Intn(128) + if sent+chunkSize > totalBytes { + chunkSize = totalBytes - sent + } + if r.Put(produced[sent : sent+chunkSize]) { + sent += chunkSize + } + } + }() + + consumed := make([]byte, 0, totalBytes) + go func() { + defer wg.Done() + for len(consumed) < totalBytes { + d1, d2 := r.Peek() + n := len(d1) + len(d2) + if n == 0 { + continue + } + consumed = append(consumed, d1...) + consumed = append(consumed, d2...) + r.Discard(uint32(n)) + } + }() + + wg.Wait() + if !bytes.Equal(consumed, produced) { + for i := range consumed { + if i >= len(produced) || consumed[i] != produced[i] { + t.Fatalf("trial %d: mismatch at byte %d", trial, i) + } + } + t.Fatalf("trial %d: length mismatch: got %d want %d", trial, len(consumed), len(produced)) + } + } +} + +func TestRing512_SPSCSmallChunks(t *testing.T) { + var r ring512 + const totalBytes = 1 << 16 + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + for i := 0; i < totalBytes; i++ { + for !r.Put([]byte{byte(i)}) { + } + } + }() + + consumed := make([]byte, 0, totalBytes) + go func() { + defer wg.Done() + for len(consumed) < totalBytes { + d1, d2 := r.Peek() + n := len(d1) + len(d2) + if n == 0 { + continue + } + consumed = append(consumed, d1...) + consumed = append(consumed, d2...) + r.Discard(uint32(n)) + } + }() + + wg.Wait() + for i, b := range consumed { + if b != byte(i) { + t.Fatalf("mismatch at %d: got %d want %d", i, b, byte(i)) + } + } +} + +// --- Fuzz Testing --- + +type refRing struct{ data []byte } + +func (r *refRing) Put(d []byte) bool { + if len(r.data)+len(d) > 512 { + return false + } + r.data = append(r.data, d...) + return true +} +func (r *refRing) Discard(n uint32) { r.data = r.data[n:] } +func (r *refRing) Used() uint32 { return uint32(len(r.data)) } + +// FuzzRing512 compares ring512 against a trivially correct reference. +func FuzzRing512(f *testing.F) { + f.Add([]byte{0, 10, 1, 5, 2, 0, 10, 1, 10}) + f.Add([]byte{0, 0}) + f.Add([]byte{0, 255, 0, 255, 1, 255, 1, 255}) + f.Add(bytes.Repeat([]byte{0, 64, 1, 64}, 50)) + f.Add([]byte{0, 200, 1, 100, 0, 156, 0, 156}) + + f.Fuzz(func(t *testing.T, ops []byte) { + var ring ring512 + var ref refRing + + i := 0 + for i+1 < len(ops) { + op := ops[i] % 4 + arg := ops[i+1] + i += 2 + + switch op { + case 0: // Put + size := int(arg) + if size > 512 { + size = 512 + } + data := make([]byte, size) + for j := range data { + data[j] = byte(j) + } + gotOK := ring.Put(data) + refOK := ref.Put(data) + if gotOK != refOK { + t.Fatalf("Put(%d): ring=%v ref=%v", size, gotOK, refOK) + } + + case 1: // Discard + used := ring.Used() + if used != ref.Used() { + t.Fatalf("Used mismatch: ring=%d ref=%d", used, ref.Used()) + } + if used == 0 { + continue + } + n := uint32(arg) % (used + 1) + ring.Discard(n) + ref.Discard(n) + + case 2: // Peek + verify + rUsed := ring.Used() + if rUsed != ref.Used() { + t.Fatalf("Used mismatch: ring=%d ref=%d", rUsed, ref.Used()) + } + if rUsed == 0 { + d1, d2 := ring.Peek() + if d1 != nil || d2 != nil { + t.Fatal("Peek non-nil on empty ring") + } + continue + } + got := peekAll(&ring) + if uint32(len(got)) != rUsed { + t.Fatalf("Peek returned %d bytes, Used=%d", len(got), rUsed) + } + if !bytes.Equal(got, ref.data) { + t.Fatal("Peek data mismatch") + } + + case 3: // Invariant + if ring.Free()+ring.Used() != 512 { + t.Fatalf("invariant: Free(%d)+Used(%d)!=512", ring.Free(), ring.Used()) + } + } + } + + if ring.Used() != ref.Used() { + t.Fatalf("final Used mismatch: ring=%d ref=%d", ring.Used(), ref.Used()) + } + }) +} + +// FuzzRing512_Op2 uses raw fuzz bytes as an operation stream with +// data integrity tracking. +func FuzzRing512_Op2(f *testing.F) { + f.Add([]byte{0, 255, 0, 255, 0, 2, 1, 255}) + f.Add([]byte{0, 200, 1, 100, 0, 255, 0, 157, 1, 255, 1, 255}) + seed := make([]byte, 40) + for i := range seed { + if i%4 < 2 { + seed[i] = 0 + } else { + seed[i] = 1 + } + if i%2 == 1 { + seed[i] = byte(3 + i%13) + } + } + f.Add(seed) + + f.Fuzz(func(t *testing.T, ops []byte) { + const buflen = 512 + const maxOps = 128 + var ring ring512 + var written []byte + totalRead := 0 + + i := 0 + nops := 0 + for i+1 < len(ops) && nops < maxOps { + op := ops[i] % 3 + sz := int(ops[i+1]) + i += 2 + nops++ + + switch op { + case 0: // Put + if sz == 0 { + continue + } + free := int(ring.Free()) + if sz > free { + sz = free + } + if sz == 0 { + continue + } + data := make([]byte, sz) + for j := range data { + data[j] = byte(len(written) + j) + } + if !ring.Put(data) { + t.Fatalf("Put(%d) failed with Free()=%d", sz, free) + } + written = append(written, data...) + + case 1: // Read + used := int(ring.Used()) + if used == 0 || sz == 0 { + continue + } + if sz > used { + sz = used + } + d1, d2 := ring.Peek() + // Concatenate and take sz bytes. + var got []byte + if sz <= len(d1) { + got = d1[:sz] + } else { + got = make([]byte, sz) + copy(got, d1) + copy(got[len(d1):], d2) + } + expect := written[totalRead : totalRead+sz] + if !bytes.Equal(got, expect) { + t.Fatalf("data mismatch at read offset %d", totalRead) + } + ring.Discard(uint32(sz)) + totalRead += sz + + case 2: // Reset + ring.Reset() + totalRead = len(written) + } + + if ring.Free()+ring.Used() != buflen { + t.Fatalf("invariant: Free(%d)+Used(%d)!=%d", ring.Free(), ring.Used(), buflen) + } + if int(ring.Used()) != len(written)-totalRead { + t.Fatalf("Used()=%d expected %d", ring.Used(), len(written)-totalRead) + } + } + + // Final drain. + d1, d2 := ring.Peek() + var remaining []byte + remaining = append(remaining, d1...) + remaining = append(remaining, d2...) + expect := written[totalRead:] + if !bytes.Equal(remaining, expect) { + t.Fatalf("final drain mismatch: got %d bytes, want %d", len(remaining), len(expect)) + } + }) +} diff --git a/src/machine/usb/cdc/usbcdc.go b/src/machine/usb/cdc/usbcdc.go index 5b5ffbf7c4..b9e68369a2 100644 --- a/src/machine/usb/cdc/usbcdc.go +++ b/src/machine/usb/cdc/usbcdc.go @@ -1,10 +1,13 @@ +//go:build baremetal + package cdc import ( "errors" "machine" "machine/usb" - "runtime/interrupt" + "sync/atomic" + _ "unsafe" ) var ( @@ -21,64 +24,56 @@ type cdcLineInfo struct { lineState uint8 } -// Read from the RX buffer. -func (usbcdc *USBCDC) Read(data []byte) (n int, err error) { - // check if RX buffer is empty - size := usbcdc.Buffered() - if size == 0 { - return 0, nil - } +// USBCDC is the USB CDC aka serial over USB interface. +type USBCDC struct { + tx ring512 + rx ring512 + inflight atomic.Uint32 + rbuf [1]byte + wbuf [1]byte +} - // Make sure we do not read more from buffer than the data slice can hold. - if len(data) < size { - size = len(data) - } +var ( + // USB is a USB CDC interface. + USB *USBCDC - // only read number of bytes used from buffer - for i := 0; i < size; i++ { - v, _ := usbcdc.ReadByte() - data[i] = v - } + usbLineInfo = cdcLineInfo{115200, 0x00, 0x00, 0x08, 0x00} +) - return size, nil +// Read from the RX buffer. +func (usbcdc *USBCDC) Read(data []byte) (n int, err error) { + data1, data2 := usbcdc.rx.Peek() + n += copy(data, data1) + n += copy(data[n:], data2) + usbcdc.rx.Discard(uint32(n)) + return n, nil } // ReadByte reads a single byte from the RX buffer. // If there is no data in the buffer, returns an error. func (usbcdc *USBCDC) ReadByte() (byte, error) { // check if RX buffer is empty - buf, ok := usbcdc.rxBuffer.Get() - if !ok { - return 0, ErrBufferEmpty + b, _ := usbcdc.rx.Peek() + if len(b) > 0 { + c := b[0] + usbcdc.rx.Discard(1) + return c, nil } - return buf, nil + return 0, ErrBufferEmpty } // Buffered returns the number of bytes currently stored in the RX buffer. func (usbcdc *USBCDC) Buffered() int { - return int(usbcdc.rxBuffer.Used()) + return int(usbcdc.rx.Used()) } // Receive handles adding data to the UART's data buffer. // Usually called by the IRQ handler for a machine. func (usbcdc *USBCDC) Receive(data byte) { - usbcdc.rxBuffer.Put(data) + usbcdc.rbuf[0] = data + usbcdc.rx.Put(usbcdc.rbuf[:]) } -// USBCDC is the USB CDC aka serial over USB interface. -type USBCDC struct { - rxBuffer *rxRingBuffer - txBuffer *txRingBuffer - waitTxc bool -} - -var ( - // USB is a USB CDC interface. - USB *USBCDC - - usbLineInfo = cdcLineInfo{115200, 0x00, 0x00, 0x08, 0x00} -) - // Configure the USB CDC interface. The config is here for compatibility with the UART interface. func (usbcdc *USBCDC) Configure(config machine.UARTConfig) error { return nil @@ -86,32 +81,63 @@ func (usbcdc *USBCDC) Configure(config machine.UARTConfig) error { // Flush flushes buffered data. func (usbcdc *USBCDC) Flush() { - mask := interrupt.Disable() - if b, ok := usbcdc.txBuffer.Get(); ok { - machine.SendUSBInPacket(cdcEndpointIn, b) - } else { - usbcdc.waitTxc = false + for usbcdc.tx.Used() > 0 { + gosched() } - interrupt.Restore(mask) } // Write data to the USBCDC. func (usbcdc *USBCDC) Write(data []byte) (n int, err error) { - if usbLineInfo.lineState > 0 { - mask := interrupt.Disable() - usbcdc.txBuffer.Put(data) - if !usbcdc.waitTxc { - usbcdc.waitTxc = true - usbcdc.Flush() + n = len(data) + if usbLineInfo.lineState <= 0 { + return n, nil + } + for len(data) > 0 { + tosend := min(len(data), int(usbcdc.tx.Free())) + if tosend == 0 { + gosched() + continue } - interrupt.Restore(mask) + usbcdc.tx.Put(data[:tosend]) + data = data[tosend:] + usbcdc.kickTx() } - return len(data), nil + return n, nil +} + +// kickTx starts a transfer if none is in flight. Called from main context only. +func (usbcdc *USBCDC) kickTx() { + if usbcdc.inflight.Load() > 0 { + return // txhandler will chain the next packet. + } + usbcdc.sendFromRing() +} + +func (usbcdc *USBCDC) txhandler() { + inflight := usbcdc.inflight.Load() + usbcdc.inflight.Store(0) + usbcdc.tx.Discard(inflight) + usbcdc.sendFromRing() +} + +// sendFromRing sends one USB packet from the ring and sets inflight. +// Called from kickTx (main) or txhandler (ISR), but never concurrently +// because kickTx only runs when inflight==0 and txhandler only runs +// when inflight>0. +func (usbcdc *USBCDC) sendFromRing() { + d1, _ := usbcdc.tx.Peek() + if len(d1) == 0 { + return + } + chunk := d1[:min(usb.EndpointPacketSize, len(d1))] + usbcdc.inflight.Store(uint32(len(chunk))) + machine.SendUSBInPacket(cdcEndpointIn, chunk) } // WriteByte writes a byte of data to the USB CDC interface. func (usbcdc *USBCDC) WriteByte(c byte) error { - usbcdc.Write([]byte{c}) + usbcdc.wbuf[0] = c + usbcdc.Write(usbcdc.wbuf[:]) return nil } @@ -124,9 +150,8 @@ func (usbcdc *USBCDC) RTS() bool { } func cdcCallbackRx(b []byte) { - for i := range b { - USB.Receive(b[i]) - } + free := USB.rx.Free() + USB.rx.Put(b[:min(len(b), int(free))]) } var cdcSetupBuff [cdcLineInfoSize]byte @@ -187,5 +212,8 @@ func cdcSetup(setup usb.Setup) bool { func EnableUSBCDC() { machine.USBCDC = New() - machine.EnableCDC(USB.Flush, cdcCallbackRx, cdcSetup) + machine.EnableCDC(USB.txhandler, cdcCallbackRx, cdcSetup) } + +//go:linkname gosched runtime.Gosched +func gosched() From f50eccf832cd207f1de8e51e397296eadd8dd000 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 1 Mar 2026 23:21:34 +0100 Subject: [PATCH 41/81] esp32c3/esp32s3: refactor ADC implementation to reduce code duplication. This refactoring reduces code duplication from the esp32c3/esp32s3 ADC implementation, by reusing the register/efuse calibration code since the same basic procedures are used by both processors. Signed-off-by: deadprogram --- src/machine/machine_esp32c3_adc.go | 295 +++++------------------------ src/machine/machine_esp32s3_adc.go | 280 ++++----------------------- src/machine/machine_esp32xx_adc.go | 235 +++++++++++++++++++++++ 3 files changed, 312 insertions(+), 498 deletions(-) create mode 100644 src/machine/machine_esp32xx_adc.go diff --git a/src/machine/machine_esp32c3_adc.go b/src/machine/machine_esp32c3_adc.go index ec23637edc..a908666264 100644 --- a/src/machine/machine_esp32c3_adc.go +++ b/src/machine/machine_esp32c3_adc.go @@ -5,10 +5,11 @@ package machine import ( "device/esp" "errors" - "runtime/volatile" - "unsafe" ) +// newRegI2C returns the regI2C configured for ESP32-C3: hostID=0, drefInit=1. +func newRegI2C() regI2C { return regI2C{hostID: 0, drefInit: 1} } + const ( // ADC attenuation values for ESP32-C3 APB_SARADC. // 0 dB : ~0 .. 1.1 V @@ -34,8 +35,7 @@ func InitADC() { esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_A(0) esp.APB_SARADC.SetCLKM_CONF_CLK_EN(1) - var c adcSelfCalibration - c.calibrate() + adcSelfCalibrate() } // ESP32-C3: ADC1 = GPIO0–GPIO4 (ch 0–4), ADC2 = GPIO5 (ch 0). ADC2 shares with Wi‑Fi; @@ -88,43 +88,43 @@ func (a ADC) Get() uint16 { // adcSelfCalibration const ( - adcCalTimesC3 = 15 - adcCalOffsetRangeC3 = uint32(4096) - adcCalRtcMagicC3 = uint32(0xADC1C401) - adcCalInitMinC3 = uint32(1000) - adcCalInitMaxC3 = uint32(4096) - adcGndOffsetCompC3 = uint32(0) + adcCalTimesC3 = 15 + adcCalRtcMagicC3 = uint32(0xADC1C401) + adcCalInitMinC3 = uint32(1000) + adcCalInitMaxC3 = uint32(4096) ) -type adcSelfCalibration struct { - digiRefMv uint32 -} - -// calibrate sets ADC1/ADC2 init code from RTC or runs self-calibration (GND). +// selfCalibrate sets ADC1/ADC2 init code from RTC or runs self-calibration (GND). // eFuse is not used: on ESP32-C3 the ADC calibration fields in BLK2 are often unprogrammed. -func (c *adcSelfCalibration) calibrate() { - reg := regI2C{} +func adcSelfCalibrate() { + reg := newRegI2C() reg.sarEnable() var adc1Code uint32 - if saved, ok := c.restoreFromRTC(); ok { + if saved, ok := restoreFromRTC(); ok { adc1Code = saved } else { - c.calSetupADC1() - reg.adc1CalibrationInit(0) - reg.adc1CalibrationPrepare(0) - adc1Code = c.calibrateUnit(reg, 0, c.readADC1) - c.saveToRTC(adc1Code) - reg.adc1CalibrationFinish(0) + calSetupADC1() + reg.calibrationInit(0) + reg.calibrationPrepare(0) + adc1Code = reg.calibrateBinarySearch(0, adcCalTimesC3, readADC1) + if adc1Code < adcCalInitMinC3 { + adc1Code = adcCalInitMinC3 + } + if adc1Code > adcCalInitMaxC3 { + adc1Code = adcCalInitMaxC3 + } + saveToRTC(adc1Code) + reg.calibrationFinish(0) } - c.applyADC1Code(reg, adc1Code) - c.applyADC2Code(reg, adc1Code) + applyADC1Code(reg, adc1Code) + applyADC2Code(reg, adc1Code) } // calSetupADC1 configures APB_SARADC for oneshot sampling on ADC1 channel 0 // with fixed attenuation. This is used only during self‑calibration. -func (c *adcSelfCalibration) calSetupADC1() { +func calSetupADC1() { esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(0) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(1) @@ -132,7 +132,7 @@ func (c *adcSelfCalibration) calSetupADC1() { // calSetupADC2 configures APB_SARADC for oneshot sampling on ADC2 (GPIO5, ch 0). // On C3, onetime_channel = (unit<<3)|channel → ADC2 ch0 = 8. -func (c *adcSelfCalibration) calSetupADC2() { +func calSetupADC2() { esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(8) // (1<<3)|0 for ADC2 esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) @@ -142,7 +142,7 @@ func (c *adcSelfCalibration) calSetupADC2() { // readADC1 performs a single ADC1 conversion using the APB_SARADC // oneshot path and returns the raw 12‑bit result (0..4095). -func (c *adcSelfCalibration) readADC1() uint32 { +func readADC1() uint32 { esp.APB_SARADC.SetINT_CLR_APB_SARADC1_DONE_INT_CLR(1) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) @@ -154,7 +154,7 @@ func (c *adcSelfCalibration) readADC1() uint32 { } // readADC2 performs a single ADC2 conversion and returns the raw 12‑bit result (0..4095). -func (c *adcSelfCalibration) readADC2() uint32 { +func readADC2() uint32 { esp.APB_SARADC.SetINT_CLR_APB_SARADC2_DONE_INT_CLR(1) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) @@ -167,7 +167,7 @@ func (c *adcSelfCalibration) readADC2() uint32 { return uint32(raw) } -func (c *adcSelfCalibration) restoreFromRTC() (uint32, bool) { +func restoreFromRTC() (uint32, bool) { if esp.RTC_CNTL.GetSTORE0() != adcCalRtcMagicC3 { return 0, false } @@ -178,7 +178,7 @@ func (c *adcSelfCalibration) restoreFromRTC() (uint32, bool) { return code, true } -func (c *adcSelfCalibration) saveToRTC(code uint32) { +func saveToRTC(code uint32) { if code < adcCalInitMinC3 || code > adcCalInitMaxC3 { return } @@ -187,228 +187,19 @@ func (c *adcSelfCalibration) saveToRTC(code uint32) { } // applyADC1Code sets ADC1 init code and finishes calibration. -func (c *adcSelfCalibration) applyADC1Code(reg regI2C, code uint32) { - c.calSetupADC1() - reg.adc1CalibrationInit(0) - reg.adc1CalibrationPrepare(0) - reg.adc1SetCalibrationParam(0, code) - reg.adc1CalibrationFinish(0) +func applyADC1Code(reg regI2C, code uint32) { + calSetupADC1() + reg.calibrationInit(0) + reg.calibrationPrepare(0) + reg.setCalibrationParam(0, code) + reg.calibrationFinish(0) } // applyADC2Code sets ADC2 init code and finishes calibration. On C3 eFuse V1 // there is no separate ADC2 calibration; IDF uses ADC1 init code for both units. -func (c *adcSelfCalibration) applyADC2Code(reg regI2C, code uint32) { - reg.adc1CalibrationInit(1) - reg.adc1CalibrationPrepare(1) - reg.adc1SetCalibrationParam(1, code) - reg.adc1CalibrationFinish(1) -} - -func (c *adcSelfCalibration) calibrateUnit(reg regI2C, adcN uint8, readADC func() uint32) uint32 { - var codeList [adcCalTimesC3]uint32 - var codeSum uint32 - - for rpt := 0; rpt < adcCalTimesC3; rpt++ { - codeH := adcCalOffsetRangeC3 - codeL := uint32(0) - chkCode := (codeH + codeL) / 2 - reg.adc1SetCalibrationParam(adcN, chkCode) - selfCal := readADC() - - for codeH-codeL > 1 { - if selfCal == 0 { - codeH = chkCode - } else { - codeL = chkCode - } - chkCode = (codeH + codeL) / 2 - reg.adc1SetCalibrationParam(adcN, chkCode) - selfCal = readADC() - if codeH-codeL == 1 { - chkCode++ - reg.adc1SetCalibrationParam(adcN, chkCode) - selfCal = readADC() - } - } - codeList[rpt] = chkCode - codeSum += chkCode - } - - codeL := codeList[0] - codeH := codeList[0] - for i := 0; i < adcCalTimesC3; i++ { - if codeList[i] < codeL { - codeL = codeList[i] - } - if codeList[i] > codeH { - codeH = codeList[i] - } - } - excluded := codeH + codeL - remaining := codeSum - excluded - finalCode := remaining / (adcCalTimesC3 - 2) - if remaining%(adcCalTimesC3-2) >= 4 { - finalCode++ - } - if finalCode < adcCalInitMinC3 { - finalCode = adcCalInitMinC3 - } - if finalCode > adcCalInitMaxC3 { - finalCode = adcCalInitMaxC3 - } - - reg.adc1SetCalibrationParam(adcN, finalCode) - return finalCode -} - -// regi2c - -// regI2C on ESP32‑C3 exposes the internal analog I2C bus that controls -// SAR ADC trim registers. Constants below mirror the layout from -// ESP‑IDF's soc/regi2c_saradc.h and TRM (I2C_RTC_CONFIG2 block). -const ( - // i2cSarADC/i2cSarADCHostID select the SAR ADC block on the internal bus. - i2cSarADC = uint8(0x69) - i2cSarADCHostID = uint8(0) - - // adc*_Dref* define the DREF (reference) bitfields for ADC1/ADC2. - adc1DrefAddr = uint8(0x2) - adc1DrefMSB = uint8(6) - adc1DrefLSB = uint8(4) - - adc2DrefAddr = uint8(0x5) - adc2DrefMSB = uint8(6) - adc2DrefLSB = uint8(4) - - // adc*_EncalGnd* control ENCAL_GND: route internal ground to ADC input - // during self‑calibration so that the pin is effectively disconnected. - adc1EncalGndAddr = uint8(0x7) - adc1EncalGndMSB = uint8(5) - adc1EncalGndLSB = uint8(5) - - adc2EncalGndAddr = uint8(0x7) - adc2EncalGndMSB = uint8(7) - adc2EncalGndLSB = uint8(7) - - // adc*_InitCode* hold the INIT_CODE (offset) that hardware uses to - // compensate ADC1/ADC2 offset error. - adc1InitCodeHighAddr = uint8(0x1) - adc1InitCodeHighMSB = uint8(3) - adc1InitCodeHighLSB = uint8(0) - adc1InitCodeLowAddr = uint8(0x0) - adc1InitCodeLowMSB = uint8(7) - adc1InitCodeLowLSB = uint8(0) - - adc2InitCodeHighAddr = uint8(0x4) - adc2InitCodeHighMSB = uint8(3) - adc2InitCodeHighLSB = uint8(0) - adc2InitCodeLowAddr = uint8(0x3) - adc2InitCodeLowMSB = uint8(7) - adc2InitCodeLowLSB = uint8(0) - - // ANA_CONFIG/ANA_CONFIG2: enable analog SAR I2C domain before regI2C access. - anaConfigReg = uintptr(0x6000E044) - i2cSarEnMask = uint32(1 << 18) - anaConfig2Reg = uintptr(0x6000E048) - anaSarCfg2En = uint32(1 << 16) - - // I2C_RTC_CONFIG2 master control register used by regI2C operations. - i2cMstCtrlHost = uintptr(0x6000E000) - i2cMstBusyBit = uint32(1 << 25) - i2cMstWrCntl = uint32(1 << 24) - i2cMstDataMask = uint32(0xFF << 16) - i2cMstDataShift = 16 - i2cMstTimeout = 10000 -) - -type regI2C struct{} - -// sarEnable enables the SAR analog I2C domain before any regI2C access. -func (r *regI2C) sarEnable() { - cfg := (*volatile.Register32)(unsafe.Pointer(anaConfigReg)) - cfg2 := (*volatile.Register32)(unsafe.Pointer(anaConfig2Reg)) - esp.RTC_CNTL.SetANA_CONF_SAR_I2C_PU(1) - cfg.Set(cfg.Get() &^ i2cSarEnMask) - cfg2.Set(cfg2.Get() | anaSarCfg2En) -} - -// adc1CalibrationInit sets DREF for the selected ADC unit -// before running the self‑calibration procedure. -func (r *regI2C) adc1CalibrationInit(adcN uint8) { - if adcN == 0 { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1DrefAddr, adc1DrefMSB, adc1DrefLSB, 1) - } else { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2DrefAddr, adc2DrefMSB, adc2DrefLSB, 1) - } -} - -// adc1CalibrationPrepare enables ENCAL_GND so that the ADC input -// is internally shorted to ground during self‑calibration. -func (r *regI2C) adc1CalibrationPrepare(adcN uint8) { - if adcN == 0 { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1EncalGndAddr, adc1EncalGndMSB, adc1EncalGndLSB, 1) - } else { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2EncalGndAddr, adc2EncalGndMSB, adc2EncalGndLSB, 1) - } -} - -// adc1CalibrationFinish clears ENCAL_GND and reconnects the ADC -// input back to the external pad after self‑calibration. -func (r *regI2C) adc1CalibrationFinish(adcN uint8) { - if adcN == 0 { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1EncalGndAddr, adc1EncalGndMSB, adc1EncalGndLSB, 0) - } else { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2EncalGndAddr, adc2EncalGndMSB, adc2EncalGndLSB, 0) - } -} - -// adc1SetCalibrationParam writes the INIT_CODE (offset trim) for -// the selected ADC unit using the regI2C bitfields. -func (r *regI2C) adc1SetCalibrationParam(adcN uint8, param uint32) { - msb := uint8(param >> 8) - lsb := uint8(param & 0xFF) - if adcN == 0 { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) - } else { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) - } -} - -// waitIdle polls the REGI2C master BUSY bit until it clears or the -// simple software timeout expires. This matches the busy‑wait helper -// used in ESP‑IDF's regi2c_ctrl.c. -func (r *regI2C) waitIdle(reg *volatile.Register32) bool { - for i := 0; i < i2cMstTimeout; i++ { - if reg.Get()&i2cMstBusyBit == 0 { - return true - } - } - return false -} - -// writeMask is a software implementation of REGI2C_WRITE_MASK macro: -// 1. select block + regAddr, -// 2. read current byte, -// 3. update only [msb:lsb] bitfield, -// 4. write it back via internal I2C master. -func (r *regI2C) writeMask(block, hostID, regAddr, msb, lsb, data uint8) { - if hostID != i2cSarADCHostID { - return - } - reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlHost)) - if !r.waitIdle(reg) { - return - } - reg.Set(uint32(block) | uint32(regAddr)<<8) - if !r.waitIdle(reg) { - return - } - cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift - mask := uint32(1<<(msb-lsb+1)-1) << lsb - cur &^= mask - cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb - reg.Set(uint32(block) | uint32(regAddr)<<8 | i2cMstWrCntl | (cur< 1 { - if selfCal == 0 { - codeH = chkCode - } else { - codeL = chkCode - } - chkCode = (codeH + codeL) / 2 - reg.adc1SetCalibrationParam(0, chkCode) - selfCal = c.readADC1() - if codeH-codeL == 1 { - chkCode++ - reg.adc1SetCalibrationParam(0, chkCode) - selfCal = c.readADC1() - } - } - codeList[rpt] = chkCode - codeSum += chkCode - } - - codeL := codeList[0] - codeH := codeList[0] - for i := 0; i < adcCalTimes; i++ { - if codeList[i] < codeL { - codeL = codeList[i] - } - if codeList[i] > codeH { - codeH = codeList[i] - } - } - excluded := codeH + codeL - remaining := codeSum - excluded - finalCode := remaining / (adcCalTimes - 2) - if remaining%(adcCalTimes-2) >= 4 { - finalCode++ - } - - return finalCode + reg.calibrationInit(0) + reg.calibrationPrepare(0) } -func (c *adcCalibration) adc1CalibrateHigh(reg regI2C, code uint32) { - reg.adc1SetCalibrationParam(0, code) - reg.adc1CalibrationFinish(0) - c.adc1StartWithPadForce() +func adc1CalibrateHigh(reg regI2C, code uint32) { + reg.setCalibrationParam(0, code) + reg.calibrationFinish(0) + adc1StartWithPadForce() } -func (c *adcCalibration) adc1StartWithPadForce() { +func adc1StartWithPadForce() { esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) } @@ -336,7 +282,7 @@ func (c *adcCalibration) adc1StartWithPadForce() { // readADC1 performs one ADC1 conversion via RTC path (used during calibration). // Internal GND is connected via ENCAL_GND, so the pin input is disconnected. // Matches IDF: wait conversion idle (meas_status==0), then start 0→1, wait done, read data. -func (c *adcCalibration) readADC1() uint32 { +func readADC1() uint32 { for esp.SENS.GetSAR_SLAVE_ADDR1_SAR_SARADC_MEAS_STATUS() != 0 { } esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(0) @@ -346,7 +292,7 @@ func (c *adcCalibration) readADC1() uint32 { return uint32(esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DATA_SAR() & 0xfff) } -func (c *adcCalibration) restoreFromRTC() (uint32, bool) { +func restoreFromRTC() (uint32, bool) { if esp.RTC_CNTL.GetSTORE0() != adcCalRtcMagic { return 0, false } @@ -357,169 +303,11 @@ func (c *adcCalibration) restoreFromRTC() (uint32, bool) { return code, true } -func (c *adcCalibration) saveToRTC(code uint32) { +func saveToRTC(code uint32) { esp.RTC_CNTL.SetSTORE0(adcCalRtcMagic) esp.RTC_CNTL.SetSTORE1(code) } -// regI2C — internal I2C for SAR ADC (ESP32-S2 I2C_RTC_CONFIG2, reg 0x6000E000). -// Source: idf-source/components/soc/esp32s3/include/soc/regi2c_saradc.h - -const ( - // I2C_SAR_ADC / I2C_SAR_ADC_HOSTID in regi2c_saradc.h - i2cSarADC = uint8(0x69) // I2C_SAR_ADC - i2cSarADCHostID = uint8(1) // I2C_SAR_ADC_HOSTID - - // ADC_SAR1_DREF_ADDR(_MSB/_LSB) - adc1DrefAddr = uint8(0x2) // ADC_SAR1_DREF_ADDR - adc1DrefMSB = uint8(6) // ADC_SAR1_DREF_ADDR_MSB - adc1DrefLSB = uint8(4) // ADC_SAR1_DREF_ADDR_LSB - - // ADC_SAR2_DREF_ADDR(_MSB/_LSB) - adc2DrefAddr = uint8(0x5) // ADC_SAR2_DREF_ADDR - adc2DrefMSB = uint8(6) // ADC_SAR2_DREF_ADDR_MSB - adc2DrefLSB = uint8(4) // ADC_SAR2_DREF_ADDR_LSB - - // ADC_SAR1_ENCAL_GND_ADDR(_MSB/_LSB) - adc1EncalGndAddr = uint8(0x7) // ADC_SAR1_ENCAL_GND_ADDR - adc1EncalGndMSB = uint8(5) // ADC_SAR1_ENCAL_GND_ADDR_MSB - adc1EncalGndLSB = uint8(5) // ADC_SAR1_ENCAL_GND_ADDR_LSB - - // ADC_SAR2_ENCAL_GND_ADDR(_MSB/_LSB) - adc2EncalGndAddr = uint8(0x7) // ADC_SAR2_ENCAL_GND_ADDR - adc2EncalGndMSB = uint8(7) // ADC_SAR2_ENCAL_GND_ADDR_MSB - adc2EncalGndLSB = uint8(7) // ADC_SAR2_ENCAL_GND_ADDR_LSB - - // ADC_SAR1_INITIAL_CODE_HIGH/LOW_ADDR(_MSB/_LSB) - adc1InitCodeHighAddr = uint8(0x1) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR - adc1InitCodeHighMSB = uint8(3) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR_MSB - adc1InitCodeHighLSB = uint8(0) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR_LSB - adc1InitCodeLowAddr = uint8(0x0) // ADC_SAR1_INITIAL_CODE_LOW_ADDR - adc1InitCodeLowMSB = uint8(7) // ADC_SAR1_INITIAL_CODE_LOW_ADDR_MSB - adc1InitCodeLowLSB = uint8(0) // ADC_SAR1_INITIAL_CODE_LOW_ADDR_LSB - - // ADC_SAR2_INITIAL_CODE_HIGH/LOW_ADDR(_MSB/_LSB) - adc2InitCodeHighAddr = uint8(0x4) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR - adc2InitCodeHighMSB = uint8(3) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR_MSB - adc2InitCodeHighLSB = uint8(0) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR_LSB - adc2InitCodeLowAddr = uint8(0x3) // ADC_SAR2_INITIAL_CODE_LOW_ADDR - adc2InitCodeLowMSB = uint8(7) // ADC_SAR2_INITIAL_CODE_LOW_ADDR_MSB - adc2InitCodeLowLSB = uint8(0) // ADC_SAR2_INITIAL_CODE_LOW_ADDR_LSB - - // Analog config registers for regI2C block (RTC/ANA config in TRM). - anaConfigReg = uintptr(0x6000E044) - i2cSarEnMask = uint32(1 << 18) - anaConfig2Reg = uintptr(0x6000E048) - anaSarCfg2En = uint32(1 << 16) - - // REGI2C master control register and helper masks. - i2cMstCtrlHost1 = uintptr(0x6000E000) - i2cMstBusyBit = uint32(1 << 25) - i2cMstWrCntlBit = uint32(1 << 24) - i2cMstDataMask = uint32(0xFF << 16) - i2cMstDataShift = 16 - i2cMstBusyTimeout = 10000 -) - -type regI2C struct{} - -// waitIdle mimics the IDF regi2c busy-wait helper (see regi2c_ctrl.c). -// It polls the REGI2C master control register until the BUSY bit clears -// or a small timeout expires, to avoid writing while a previous transfer -// is still in progress. -func (r *regI2C) waitIdle(reg *volatile.Register32) bool { - for i := 0; i < i2cMstBusyTimeout; i++ { - if reg.Get()&i2cMstBusyBit == 0 { - return true - } - } - return false -} - -// writeMask is a software implementation of the REGI2C_WRITE_MASK macro -// from IDF (see soc/regi2c_saradc.h). It: -// - selects the regI2C SAR ADC block + register address, -// - reads the current byte, -// - updates only the [msb:lsb] bitfield, -// - writes the new value back via the internal I2C master. -func (r *regI2C) writeMask(block, hostID, regAddr, msb, lsb, data uint8) { - if hostID != i2cSarADCHostID { - return - } - reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlHost1)) - if !r.waitIdle(reg) { - return - } - reg.Set(uint32(block) | uint32(regAddr)<<8) - if !r.waitIdle(reg) { - return - } - cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift - mask := uint32(1<<(msb-lsb+1)-1) << lsb - cur &^= mask - cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb - reg.Set(uint32(block) | uint32(regAddr)<<8 | i2cMstWrCntlBit | (cur<> 8) - lsb := uint8(param & 0xFF) - if adcN == 0 { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) - r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) - } else { - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) - r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) - } -} - // fuse const ( // Base address for eFuse controller (EFUSE_BLKx region in TRM). diff --git a/src/machine/machine_esp32xx_adc.go b/src/machine/machine_esp32xx_adc.go new file mode 100644 index 0000000000..30fec86cbe --- /dev/null +++ b/src/machine/machine_esp32xx_adc.go @@ -0,0 +1,235 @@ +//go:build esp32s3 || (esp32c3 && !m5stamp_c3) + +// Shared regI2C-based ADC calibration helpers for ESP32-S3 and ESP32-C3. +// +// The internal I2C bus ("regI2C") and SAR ADC trim register layout are +// identical across both chips; chip-specific differences (host ID, DREF +// init value, calibration iterations) are captured in the regI2C struct +// fields, keeping each target file free of duplicated low-level code. + +package machine + +import ( + "device/esp" + "runtime/volatile" + "unsafe" +) + +// regI2C wraps the internal I2C bus used for SAR ADC calibration registers. +// Fields hold chip-specific parameters that differ between ESP32-S3 and ESP32-C3. +type regI2C struct { + // hostID is the I2C_SAR_ADC_HOSTID (1 for ESP32-S3, 0 for ESP32-C3). + hostID uint8 + // drefInit is the DREF reference value written during calibrationInit + // (4 for ESP32-S3, 1 for ESP32-C3). + drefInit uint8 +} + +// SAR ADC I2C register layout constants shared across ESP32-S3 and ESP32-C3. +// Source: ESP-IDF soc/regi2c_saradc.h +const ( + // i2cSarADC is the I2C_SAR_ADC block address on the internal bus. + i2cSarADC = uint8(0x69) + + // DREF (reference) bitfields for ADC1 and ADC2. + adc1DrefAddr = uint8(0x2) + adc1DrefMSB = uint8(6) + adc1DrefLSB = uint8(4) + adc2DrefAddr = uint8(0x5) + adc2DrefMSB = uint8(6) + adc2DrefLSB = uint8(4) + + // ENCAL_GND: routes internal ground to ADC input during self-calibration. + adc1EncalGndAddr = uint8(0x7) + adc1EncalGndMSB = uint8(5) + adc1EncalGndLSB = uint8(5) + adc2EncalGndAddr = uint8(0x7) + adc2EncalGndMSB = uint8(7) + adc2EncalGndLSB = uint8(7) + + // INIT_CODE (offset) high/low for ADC1 and ADC2. + adc1InitCodeHighAddr = uint8(0x1) + adc1InitCodeHighMSB = uint8(3) + adc1InitCodeHighLSB = uint8(0) + adc1InitCodeLowAddr = uint8(0x0) + adc1InitCodeLowMSB = uint8(7) + adc1InitCodeLowLSB = uint8(0) + adc2InitCodeHighAddr = uint8(0x4) + adc2InitCodeHighMSB = uint8(3) + adc2InitCodeHighLSB = uint8(0) + adc2InitCodeLowAddr = uint8(0x3) + adc2InitCodeLowMSB = uint8(7) + adc2InitCodeLowLSB = uint8(0) + + // ANA_CONFIG / ANA_CONFIG2: enable analog SAR I2C domain. + anaConfigReg = uintptr(0x6000E044) + i2cSarEnMask = uint32(1 << 18) + anaConfig2Reg = uintptr(0x6000E048) + anaSarCfg2En = uint32(1 << 16) + + // REGI2C master control register and helper masks. + i2cMstCtrlReg = uintptr(0x6000E000) + i2cMstBusyBit = uint32(1 << 25) + i2cMstWrCntlBit = uint32(1 << 24) + i2cMstDataMask = uint32(0xFF << 16) + i2cMstDataShift = 16 + i2cMstBusyTimeout = 10000 + + // adcCalOffsetRange is the binary search upper bound (12-bit full scale). + adcCalOffsetRange = uint32(4096) + + // adcCalMaxIterations is the maximum number of calibration iterations + // supported by calibrateBinarySearch. Must be >= max(S3=10, C3=15). + adcCalMaxIterations = 16 +) + +// waitIdle polls the REGI2C master BUSY bit until it clears or a +// timeout expires, matching the busy-wait helper in ESP-IDF's regi2c_ctrl.c. +func (r regI2C) waitIdle(reg *volatile.Register32) bool { + for i := 0; i < i2cMstBusyTimeout; i++ { + if reg.Get()&i2cMstBusyBit == 0 { + return true + } + } + return false +} + +// writeMask is a software implementation of the IDF REGI2C_WRITE_MASK macro. +// It reads the current byte at regAddr on the SAR ADC I2C block, updates +// only the [msb:lsb] bitfield, and writes it back via the internal I2C master. +func (r regI2C) writeMask(regAddr, msb, lsb, data uint8) { + reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlReg)) + if !r.waitIdle(reg) { + return + } + reg.Set(uint32(i2cSarADC) | uint32(regAddr)<<8) + if !r.waitIdle(reg) { + return + } + cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift + mask := uint32(1<<(msb-lsb+1)-1) << lsb + cur &^= mask + cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb + reg.Set(uint32(i2cSarADC) | uint32(regAddr)<<8 | i2cMstWrCntlBit | (cur<> 8) + lsb := uint8(param & 0xFF) + if adcN == 0 { + r.writeMask(adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) + r.writeMask(adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) + } else { + r.writeMask(adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) + r.writeMask(adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) + } +} + +// calibrateBinarySearch runs the ADC self-calibration binary search loop. +// It performs 'iterations' rounds of binary search to find the optimal offset +// code, drops the min/max outliers, and returns the rounded mean of the +// remaining values. This matches adc_hal_self_calibration() in ESP-IDF. +// +// The readADC callback must perform a single conversion using the target's +// oneshot path (SENS or APB_SARADC) and return the raw 12-bit result. +// During calibration, ENCAL_GND is active so the ADC reads its internal ground. +func (r regI2C) calibrateBinarySearch(adcN uint8, iterations int, readADC func() uint32) uint32 { + if iterations > adcCalMaxIterations { + iterations = adcCalMaxIterations + } + var codeList [adcCalMaxIterations]uint32 + var codeSum uint32 + + for rpt := 0; rpt < iterations; rpt++ { + codeH := adcCalOffsetRange + codeL := uint32(0) + chkCode := (codeH + codeL) / 2 + r.setCalibrationParam(adcN, chkCode) + selfCal := readADC() + + for codeH-codeL > 1 { + if selfCal == 0 { + codeH = chkCode + } else { + codeL = chkCode + } + chkCode = (codeH + codeL) / 2 + r.setCalibrationParam(adcN, chkCode) + selfCal = readADC() + if codeH-codeL == 1 { + chkCode++ + r.setCalibrationParam(adcN, chkCode) + selfCal = readADC() + } + } + codeList[rpt] = chkCode + codeSum += chkCode + } + + // Drop min and max outliers, then average with IDF-style rounding. + codeMin := codeList[0] + codeMax := codeList[0] + for i := 0; i < iterations; i++ { + if codeList[i] < codeMin { + codeMin = codeList[i] + } + if codeList[i] > codeMax { + codeMax = codeList[i] + } + } + remaining := codeSum - codeMax - codeMin + divisor := uint32(iterations - 2) + finalCode := remaining / divisor + if remaining%divisor >= 4 { + finalCode++ + } + + return finalCode +} From 0abc5142b0d2cdea20bf1d4a55ef9dffd1776b5e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 2 Mar 2026 15:23:29 +0100 Subject: [PATCH 42/81] targets: switch rp2040/rp2350 to default to tasks scheduler This switches the rp2040 and rp2350 to use the tasks scheduler by default instead of using the cores scheduler. Too many race conditions at present, we need to look into exactly why. In the meantime, going back to the tasks as default will address a lot of these intermittent problems. Signed-off-by: deadprogram --- targets/rp2040.json | 2 +- targets/rp2350.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/targets/rp2040.json b/targets/rp2040.json index 3f9fea4590..6470d57f50 100644 --- a/targets/rp2040.json +++ b/targets/rp2040.json @@ -1,7 +1,7 @@ { "inherits": ["cortex-m0plus"], "build-tags": ["rp2040", "rp"], - "scheduler": "cores", + "scheduler": "tasks", "flash-1200-bps-reset": "true", "flash-method": "msd", "serial": "usb", diff --git a/targets/rp2350.json b/targets/rp2350.json index ebffcbdece..e3a6549fd2 100644 --- a/targets/rp2350.json +++ b/targets/rp2350.json @@ -1,7 +1,7 @@ { "inherits": ["cortex-m33"], "build-tags": ["rp2350", "rp"], - "scheduler": "cores", + "scheduler": "tasks", "flash-1200-bps-reset": "true", "flash-method": "msd", "serial": "usb", From 1f4e30318c961f646cda557650e5f27771553e84 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 2 Mar 2026 10:53:25 +0100 Subject: [PATCH 43/81] esp32c3/esp32s3: refactoring and corrections for SPI implementation This refactors and corrects the SPI implentation for the ESP32C3 and ESP32S3 processors. There was a lot of duplicated code, as well as some errors such as incorrectly calculating speed on the esp32c3 implementation. This will also be helpful when adding additional processors that use very similar peripheral registers. Signed-off-by: deadprogram --- src/machine/machine_esp32c3_spi.go | 102 ++------------------------- src/machine/machine_esp32s3_spi.go | 106 ++--------------------------- src/machine/machine_esp32xx_spi.go | 101 +++++++++++++++++++++++++++ src/machine/spi.go | 2 +- 4 files changed, 112 insertions(+), 199 deletions(-) create mode 100644 src/machine/machine_esp32xx_spi.go diff --git a/src/machine/machine_esp32c3_spi.go b/src/machine/machine_esp32c3_spi.go index edb0218ac4..5fd0a853dd 100644 --- a/src/machine/machine_esp32c3_spi.go +++ b/src/machine/machine_esp32c3_spi.go @@ -16,11 +16,6 @@ import ( ) const ( - SPI_MODE0 = uint8(0) - SPI_MODE1 = uint8(1) - SPI_MODE2 = uint8(2) - SPI_MODE3 = uint8(3) - FSPICLK_IN_IDX = uint32(63) FSPICLK_OUT_IDX = uint32(63) FSPIQ_IN_IDX = uint32(64) @@ -56,64 +51,6 @@ var ( SPI0 = SPI2 ) -// SPIConfig is used to store config info for SPI. -type SPIConfig struct { - Frequency uint32 - SCK Pin // Serial Clock - SDO Pin // Serial Data Out (MOSI) - SDI Pin // Serial Data In (MISO) - CS Pin // Chip Select (optional) - LSBFirst bool // MSB is default - Mode uint8 // SPI_MODE0 is default -} - -// Compute the SPI bus frequency from the CPU frequency. -func freqToClockDiv(hz uint32) uint32 { - fcpu := CPUFrequency() - if hz >= fcpu { // maximum frequency - return 1 << 31 - } - if hz < (fcpu / (16 * 64)) { // minimum frequency - return 15<<18 | 63<<12 | 31<<6 | 63 // pre=15, n=63 - } - - // iterate looking for an exact match - // or iterate all 16 prescaler options - // looking for the smallest error - var bestPre, bestN, bestErr uint32 - bestN = 1 - bestErr = 0xffffffff - q := uint32(float32(pplClockFreq)/float32(hz) + float32(0.5)) - for p := uint32(0); p < 16; p++ { - n := q/(p+1) - 1 - if n < 1 { // prescaler became too large, stop enum - break - } - if n > 63 { // prescaler too small, skip to next - continue - } - - freq := fcpu / ((p + 1) * (n + 1)) - if freq == hz { // exact match - return p<<18 | n<<12 | (n/2)<<6 | n - } - - var err uint32 - if freq < hz { - err = hz - freq - } else { - err = freq - hz - } - if err < bestErr { - bestErr = err - bestPre = p - bestN = n - } - } - - return bestPre<<18 | bestN<<12 | (bestN/2)<<6 | bestN -} - // Configure and make the SPI peripheral ready to use. func (spi *SPI) Configure(config SPIConfig) error { // right now this is only setup to work for the esp32c3 spi2 bus @@ -172,16 +109,16 @@ func (spi *SPI) Configure(config SPIConfig) error { // set spi2 data mode switch config.Mode { - case SPI_MODE0: + case Mode0: spi.Bus.SetMISC_CK_IDLE_EDGE(0) spi.Bus.SetUSER_CK_OUT_EDGE(0) - case SPI_MODE1: + case Mode1: spi.Bus.SetMISC_CK_IDLE_EDGE(0) spi.Bus.SetUSER_CK_OUT_EDGE(1) - case SPI_MODE2: + case Mode2: spi.Bus.SetMISC_CK_IDLE_EDGE(1) spi.Bus.SetUSER_CK_OUT_EDGE(1) - case SPI_MODE3: + case Mode3: spi.Bus.SetMISC_CK_IDLE_EDGE(1) spi.Bus.SetUSER_CK_OUT_EDGE(0) default: @@ -254,36 +191,7 @@ func (spi *SPI) Tx(w, r []byte) error { // Fill tx buffer. transferWords := (*[16]volatile.Register32)(unsafe.Pointer(uintptr(unsafe.Pointer(&spi.Bus.W0)))) - if len(w) >= 64 { - // We can fill the entire 64-byte transfer buffer with data. - // This loop is slightly faster than the loop below. - for i := 0; i < 16; i++ { - word := uint32(w[i*4]) | uint32(w[i*4+1])<<8 | uint32(w[i*4+2])<<16 | uint32(w[i*4+3])<<24 - transferWords[i].Set(word) - } - } else { - // We can't fill the entire transfer buffer, so we need to be a bit - // more careful. - // Note that parts of the transfer buffer that aren't used still - // need to be set to zero, otherwise we might be transferring - // garbage from a previous transmission if w is smaller than r. - for i := 0; i < 16; i++ { - var word uint32 - if i*4+3 < len(w) { - word |= uint32(w[i*4+3]) << 24 - } - if i*4+2 < len(w) { - word |= uint32(w[i*4+2]) << 16 - } - if i*4+1 < len(w) { - word |= uint32(w[i*4+1]) << 8 - } - if i*4+0 < len(w) { - word |= uint32(w[i*4+0]) << 0 - } - transferWords[i].Set(word) - } - } + spiTxFillBuffer(transferWords, w) // Do the transfer. spi.Bus.SetMS_DLEN_MS_DATA_BITLEN(uint32(chunkSize)*8 - 1) diff --git a/src/machine/machine_esp32s3_spi.go b/src/machine/machine_esp32s3_spi.go index 8c1618fc39..fb320f5ed1 100644 --- a/src/machine/machine_esp32s3_spi.go +++ b/src/machine/machine_esp32s3_spi.go @@ -15,11 +15,6 @@ import ( ) const ( - SPI_MODE0 = uint8(0) - SPI_MODE1 = uint8(1) - SPI_MODE2 = uint8(2) - SPI_MODE3 = uint8(3) - // ESP32-S3 PLL clock frequency (same as ESP32-C3) pplClockFreq = 80e6 @@ -64,16 +59,6 @@ var ( SPI1 = &SPI{Bus: esp.SPI3, busID: 3} // Secondary SPI (HSPI) ) -type SPIConfig struct { - Frequency uint32 - SCK Pin // Serial Clock - SDO Pin // Serial Data Out (MOSI) - SDI Pin // Serial Data In (MISO) - CS Pin // Chip Select (optional) - LSBFirst bool // MSB is default - Mode uint8 // SPI_MODE0 is default -} - // Configure and make the SPI peripheral ready to use. // Implementation following ESP-IDF HAL with GPIO Matrix routing func (spi *SPI) Configure(config SPIConfig) error { @@ -232,14 +217,14 @@ func (spi *SPI) Configure(config SPIConfig) error { // Configure SPI mode (CPOL/CPHA) following ESP-IDF HAL switch config.Mode { - case SPI_MODE0: + case Mode0: // CPOL=0, CPHA=0 (default) - case SPI_MODE1: + case Mode1: bus.SetUSER_CK_OUT_EDGE(1) // CPHA=1 - case SPI_MODE2: + case Mode2: bus.SetMISC_CK_IDLE_EDGE(1) // CPOL=1 bus.SetUSER_CK_OUT_EDGE(1) // CPHA=1 - case SPI_MODE3: + case Mode3: bus.SetMISC_CK_IDLE_EDGE(1) // CPOL=1 } @@ -319,36 +304,7 @@ func (spi *SPI) Tx(w, r []byte) error { // Fill tx buffer. transferWords := (*[16]volatile.Register32)(unsafe.Add(unsafe.Pointer(&bus.W0), 0)) - if len(w) >= 64 { - // We can fill the entire 64-byte transfer buffer with data. - // This loop is slightly faster than the loop below. - for i := 0; i < 16; i++ { - word := uint32(w[i*4]) | uint32(w[i*4+1])<<8 | uint32(w[i*4+2])<<16 | uint32(w[i*4+3])<<24 - transferWords[i].Set(word) - } - } else { - // We can't fill the entire transfer buffer, so we need to be a bit - // more careful. - // Note that parts of the transfer buffer that aren't used still - // need to be set to zero, otherwise we might be transferring - // garbage from a previous transmission if w is smaller than r. - for i := 0; i < 16; i++ { - var word uint32 - if i*4+3 < len(w) { - word |= uint32(w[i*4+3]) << 24 - } - if i*4+2 < len(w) { - word |= uint32(w[i*4+2]) << 16 - } - if i*4+1 < len(w) { - word |= uint32(w[i*4+1]) << 8 - } - if i*4+0 < len(w) { - word |= uint32(w[i*4+0]) << 0 - } - transferWords[i].Set(word) - } - } + spiTxFillBuffer(transferWords, w) // Do the transfer. bus.SetMS_DLEN_MS_DATA_BITLEN(uint32(chunkSize)*8 - 1) @@ -388,58 +344,6 @@ func (spi *SPI) Tx(w, r []byte) error { return nil } -// Compute the SPI bus frequency from the APB clock frequency. -// Note: APB clock is always 80MHz on ESP32-S3, independent of CPU frequency. -// Ported from ESP32-C3 implementation for better accuracy. -func freqToClockDiv(hz uint32) uint32 { - // Use APB clock frequency (80MHz), not CPU frequency! - // SPI peripheral is connected to APB bus which stays at 80MHz - const apbFreq = pplClockFreq // 80MHz - - if hz >= apbFreq { // maximum frequency - return 1 << 31 - } - if hz < (apbFreq / (16 * 64)) { // minimum frequency - return 15<<18 | 63<<12 | 31<<6 | 63 // pre=15, n=63 - } - - // iterate looking for an exact match - // or iterate all 16 prescaler options - // looking for the smallest error - var bestPre, bestN, bestErr uint32 - bestN = 1 - bestErr = 0xffffffff - q := uint32(float32(apbFreq)/float32(hz) + float32(0.5)) - for p := uint32(0); p < 16; p++ { - n := q/(p+1) - 1 - if n < 1 { // prescaler became too large, stop enum - break - } - if n > 63 { // prescaler too small, skip to next - continue - } - - freq := apbFreq / ((p + 1) * (n + 1)) - if freq == hz { // exact match - return p<<18 | n<<12 | (n/2)<<6 | n - } - - var err uint32 - if freq < hz { - err = hz - freq - } else { - err = freq - hz - } - if err < bestErr { - bestErr = err - bestPre = p - bestN = n - } - } - - return bestPre<<18 | bestN<<12 | (bestN/2)<<6 | bestN -} - // isDefaultSPIPins checks if the given pins match the default SPI pin configuration // that supports IO MUX direct connection for better performance func isDefaultSPIPins(busID uint8, config SPIConfig) bool { diff --git a/src/machine/machine_esp32xx_spi.go b/src/machine/machine_esp32xx_spi.go new file mode 100644 index 0000000000..755b4bec67 --- /dev/null +++ b/src/machine/machine_esp32xx_spi.go @@ -0,0 +1,101 @@ +//go:build esp32s3 || esp32c3 + +package machine + +import ( + "runtime/volatile" +) + +// SPIConfig is used to store config info for SPI. +type SPIConfig struct { + Frequency uint32 + SCK Pin // Serial Clock + SDO Pin // Serial Data Out (MOSI) + SDI Pin // Serial Data In (MISO) + CS Pin // Chip Select (optional) + LSBFirst bool // MSB is default + Mode uint8 // Mode0 is default +} + +// freqToClockDiv computes the SPI bus clock divider register value. +// SPI peripherals on ESP32-C3 and ESP32-S3 are clocked from the APB bus +// (pplClockFreq, 80 MHz on both chips). +func freqToClockDiv(hz uint32) uint32 { + if hz >= pplClockFreq { // maximum frequency + return 1 << 31 + } + if hz < (pplClockFreq / (16 * 64)) { // minimum frequency + return 15<<18 | 63<<12 | 31<<6 | 63 // pre=15, n=63 + } + + // Iterate all 16 prescaler options looking for an exact match + // or the smallest error. + var bestPre, bestN, bestErr uint32 + bestN = 1 + bestErr = 0xffffffff + q := uint32(float32(pplClockFreq)/float32(hz) + float32(0.5)) + for p := uint32(0); p < 16; p++ { + n := q/(p+1) - 1 + if n < 1 { // prescaler became too large, stop enum + break + } + if n > 63 { // prescaler too small, skip to next + continue + } + + freq := pplClockFreq / ((p + 1) * (n + 1)) + if freq == hz { // exact match + return p<<18 | n<<12 | (n/2)<<6 | n + } + + var err uint32 + if freq < hz { + err = hz - freq + } else { + err = freq - hz + } + if err < bestErr { + bestErr = err + bestPre = p + bestN = n + } + } + + return bestPre<<18 | bestN<<12 | (bestN/2)<<6 | bestN +} + +// spiTxFillBuffer writes data from w into the 16-word (64-byte) SPI +// hardware transfer buffer. Unused words are zeroed so that no stale +// data from a previous transfer is sent when w is shorter than 64 bytes. +func spiTxFillBuffer(buf *[16]volatile.Register32, w []byte) { + if len(w) >= 64 { + // We can fill the entire 64-byte transfer buffer with data. + // This loop is slightly faster than the loop below. + for i := 0; i < 16; i++ { + word := uint32(w[i*4]) | uint32(w[i*4+1])<<8 | uint32(w[i*4+2])<<16 | uint32(w[i*4+3])<<24 + buf[i].Set(word) + } + } else { + // We can't fill the entire transfer buffer, so we need to be a bit + // more careful. + // Note that parts of the transfer buffer that aren't used still + // need to be set to zero, otherwise we might be transferring + // garbage from a previous transmission if w is smaller than r. + for i := 0; i < 16; i++ { + var word uint32 + if i*4+3 < len(w) { + word |= uint32(w[i*4+3]) << 24 + } + if i*4+2 < len(w) { + word |= uint32(w[i*4+2]) << 16 + } + if i*4+1 < len(w) { + word |= uint32(w[i*4+1]) << 8 + } + if i*4+0 < len(w) { + word |= uint32(w[i*4+0]) << 0 + } + buf[i].Set(word) + } + } +} diff --git a/src/machine/spi.go b/src/machine/spi.go index fa507b961d..83fc33dae4 100644 --- a/src/machine/spi.go +++ b/src/machine/spi.go @@ -1,4 +1,4 @@ -//go:build !baremetal || atmega || attiny85 || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2) +//go:build !baremetal || atmega || attiny85 || esp32 || esp32c3 || esp32s3 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2) package machine From dfb257884ef748f5b34583979d0cc3768273b0ca Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 4 Mar 2026 15:01:22 +0100 Subject: [PATCH 44/81] modules: update to latest serial and espflash packages Signed-off-by: deadprogram --- go.mod | 5 ++--- go.sum | 8 ++++---- main.go | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index d6c4fea966..2b3d51d1cd 100644 --- a/go.mod +++ b/go.mod @@ -13,18 +13,17 @@ require ( github.com/mattn/go-tty v0.0.4 github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 github.com/tetratelabs/wazero v1.6.0 - go.bug.st/serial v1.6.2 + go.bug.st/serial v1.6.4 golang.org/x/net v0.35.0 golang.org/x/sys v0.30.0 golang.org/x/tools v0.30.0 gopkg.in/yaml.v2 v2.4.0 - tinygo.org/x/espflash v0.2.0 + tinygo.org/x/espflash v0.3.0 tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 ) require ( github.com/creack/goselect v0.1.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/stretchr/testify v1.8.4 // indirect golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index f5bdc84c87..6af085b56e 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g= github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= -go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= -go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= @@ -58,7 +58,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -tinygo.org/x/espflash v0.2.0 h1:sHDeMVaiJ0kEosCYgkiAHjSCV9+/mgQZMKy7nOXWVoQ= -tinygo.org/x/espflash v0.2.0/go.mod h1:n4sWxsjN4wjHN1lKfEYjXke6/eSeLTq/lRRLJS5FNfg= +tinygo.org/x/espflash v0.3.0 h1:uhTOWZb1FyQ4MZTZWORh/bOLFQfaFr2nuM9t3pJExRM= +tinygo.org/x/espflash v0.3.0/go.mod h1:n4sWxsjN4wjHN1lKfEYjXke6/eSeLTq/lRRLJS5FNfg= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 h1:ovavgTdIBWCH8YWlcfq9gkpoyT1+IxMKSn+Df27QwE8= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/main.go b/main.go index 80c2d35c7b..4df12695e7 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ import ( "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/loader" "golang.org/x/tools/go/buildutil" - "tinygo.org/x/espflash" + "tinygo.org/x/espflash/pkg/espflash" "tinygo.org/x/go-llvm" "go.bug.st/serial" From ba88db0a5bb5ee94508f3d55a998e2575ac27146 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 4 Mar 2026 15:03:49 +0100 Subject: [PATCH 45/81] flashing: introduce flash method 'esp32jtag' for esp32c3/esp32s3 targets This adds a new flash method 'esp32jtag' to explicitly define when this reset method is needed. When flashing esp32c3/esp32s3 boards on Windows this method of board reset is required, otherwise the reset does not take place and the board cannot be flashed. Signed-off-by: deadprogram --- main.go | 29 +++++++++++++++++++++++++---- targets/esp32c3.json | 2 +- targets/esp32s3.json | 2 +- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 4df12695e7..425d12574a 100644 --- a/main.go +++ b/main.go @@ -386,7 +386,7 @@ func Flash(pkgName, port, outpath string, options *compileopts.Options) error { fileExt = ".hex" case "bmp": fileExt = ".elf" - case "esp32flash": + case "esp32flash", "esp32jtag": fileExt = ".bin" case "native": return errors.New("unknown flash method \"native\" - did you miss a -target flag?") @@ -528,7 +528,16 @@ func Flash(pkgName, port, outpath string, options *compileopts.Options) error { return &commandError{"failed to find port", port, err} } - if err := flashBinUsingEsp32(port, result.Binary, config.Options); err != nil { + if err := flashBinUsingEsp32(port, classicReset, result.Binary, config.Options); err != nil { + return &commandError{"failed to flash", result.Binary, err} + } + case "esp32jtag": + port, err := getDefaultPort(port, config.Target.SerialPort) + if err != nil { + return &commandError{"failed to find port", port, err} + } + + if err := flashBinUsingEsp32(port, jtagReset, result.Binary, config.Options); err != nil { return &commandError{"failed to flash", result.Binary, err} } default: @@ -1031,8 +1040,20 @@ func flashHexUsingMSD(volumes []string, tmppath string, options *compileopts.Opt return errors.New("unable to locate any volume: [" + strings.Join(volumes, ",") + "]") } -func flashBinUsingEsp32(port, tmppath string, options *compileopts.Options) error { - flasher, err := espflash.NewFlasher(port, nil) +const ( + classicReset = "classic" + jtagReset = "jtag" +) + +func flashBinUsingEsp32(port, resetMode, tmppath string, options *compileopts.Options) error { + var opts *espflash.FlasherOptions + // On Windows, we have to explicitly specify the reset mode to use USB JTAG. + if runtime.GOOS == "windows" && resetMode == jtagReset { + opts = espflash.DefaultOptions() + opts.ResetMode = espflash.ResetUSBJTAG + } + + flasher, err := espflash.NewFlasher(port, opts) if err != nil { return err } diff --git a/targets/esp32c3.json b/targets/esp32c3.json index 3336f3c18e..96af15d515 100644 --- a/targets/esp32c3.json +++ b/targets/esp32c3.json @@ -13,7 +13,7 @@ "src/device/esp/esp32c3.S" ], "binary-format": "esp32c3", - "flash-method": "esp32flash", + "flash-method": "esp32jtag", "serial-port": ["303a:1001"], "openocd-interface": "esp_usb_jtag", "openocd-target": "esp32c3", diff --git a/targets/esp32s3.json b/targets/esp32s3.json index 8f534e2890..1b9159230a 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -15,7 +15,7 @@ "src/internal/task/task_stack_esp32.S" ], "binary-format": "esp32s3", - "flash-method": "esp32flash", + "flash-method": "esp32jtag", "emulator": "qemu-system-xtensa -machine esp32 -nographic -drive file={img},if=mtd,format=raw", "gdb": ["xtensa-esp32-elf-gdb"] } From 9966dc3e97d70e463eabf6608b824fc46fdb67a7 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 5 Mar 2026 20:31:32 +0100 Subject: [PATCH 46/81] fix: ESP32-C3 needs to clear MCAUSE after handling interrupt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCAUSE was never being cleared after handling an interrupt. On RISC-V, mret does NOT zero MCAUSE — it retains the last trap cause. Every other TinyGo RISC-V target (FE310, K210, QEMU) explicitly does riscv.MCAUSE.Set(0) after handling. The ESP32-C3 was missing this. Signed-off-by: deadprogram --- src/runtime/interrupt/interrupt_esp32c3.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/runtime/interrupt/interrupt_esp32c3.go b/src/runtime/interrupt/interrupt_esp32c3.go index 5725cd3524..27b7ae1b2c 100644 --- a/src/runtime/interrupt/interrupt_esp32c3.go +++ b/src/runtime/interrupt/interrupt_esp32c3.go @@ -201,6 +201,11 @@ func handleInterrupt() { reg.Set(thresholdSave) riscv.Asm("fence") + // Zero MCAUSE so that interrupt.In() returns false once we + // return to normal (non-interrupt) code. Other RISC-V targets + // (FE310, K210) do the same. + riscv.MCAUSE.Set(0) + // restore MSTATUS & MEPC riscv.MSTATUS.Set(mstatus) riscv.MEPC.Set(mepc) From 6424b09af63165fd61135e245ebe5e04476b1ffa Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 8 Mar 2026 15:49:43 +0100 Subject: [PATCH 47/81] chore: update espflash to espflasher Signed-off-by: deadprogram --- go.mod | 2 +- go.sum | 4 ++-- main.go | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 2b3d51d1cd..035527189e 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sys v0.30.0 golang.org/x/tools v0.30.0 gopkg.in/yaml.v2 v2.4.0 - tinygo.org/x/espflash v0.3.0 + tinygo.org/x/espflasher v0.4.0 tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 ) diff --git a/go.sum b/go.sum index 6af085b56e..a3b398191d 100644 --- a/go.sum +++ b/go.sum @@ -58,7 +58,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -tinygo.org/x/espflash v0.3.0 h1:uhTOWZb1FyQ4MZTZWORh/bOLFQfaFr2nuM9t3pJExRM= -tinygo.org/x/espflash v0.3.0/go.mod h1:n4sWxsjN4wjHN1lKfEYjXke6/eSeLTq/lRRLJS5FNfg= +tinygo.org/x/espflasher v0.4.0 h1:0N+Ei+0qT/wGkGIoMaY2g+oI519VA5G4kUVUYHedTv8= +tinygo.org/x/espflasher v0.4.0/go.mod h1:a3hRV9EETPUkfPE6P14p4A6jKKth+oD5gtQz3nmij+8= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74 h1:ovavgTdIBWCH8YWlcfq9gkpoyT1+IxMKSn+Df27QwE8= tinygo.org/x/go-llvm v0.0.0-20250422114502-b8f170971e74/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/main.go b/main.go index 425d12574a..30985022e1 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ import ( "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/loader" "golang.org/x/tools/go/buildutil" - "tinygo.org/x/espflash/pkg/espflash" + "tinygo.org/x/espflasher/pkg/espflasher" "tinygo.org/x/go-llvm" "go.bug.st/serial" @@ -1046,14 +1046,14 @@ const ( ) func flashBinUsingEsp32(port, resetMode, tmppath string, options *compileopts.Options) error { - var opts *espflash.FlasherOptions + var opts *espflasher.FlasherOptions // On Windows, we have to explicitly specify the reset mode to use USB JTAG. if runtime.GOOS == "windows" && resetMode == jtagReset { - opts = espflash.DefaultOptions() - opts.ResetMode = espflash.ResetUSBJTAG + opts = espflasher.DefaultOptions() + opts.ResetMode = espflasher.ResetUSBJTAG } - flasher, err := espflash.NewFlasher(port, opts) + flasher, err := espflasher.New(port, opts) if err != nil { return err } From 9e2288b61272c3f651674ee7b663a41f533d0b24 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 5 Mar 2026 20:22:12 +0100 Subject: [PATCH 48/81] esp32c3: implement USBDevice using interrupts for more proper operation. This improves the ESP32-C3 USBDevice implementation by using interrupts to properly handle data RX/TX. It also stubs out USB interfaces for HID functions, since those cannot be implemented on ESP32-C3 due to using a JTAG-USB interface. Signed-off-by: deadprogram --- src/machine/machine_esp32c3_usb.go | 241 +++++++++++++++++++++++++++++ src/machine/machine_esp32xx_usb.go | 2 +- 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/machine/machine_esp32c3_usb.go diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32c3_usb.go new file mode 100644 index 0000000000..06fdca913a --- /dev/null +++ b/src/machine/machine_esp32c3_usb.go @@ -0,0 +1,241 @@ +//go:build esp32c3 + +package machine + +import ( + "device/esp" + "errors" + "machine/usb" + "machine/usb/descriptor" + "runtime/interrupt" +) + +// USB Serial/JTAG Controller +// See esp32-c3_technical_reference_manual_en.pdf pg. 736 +// +// The ESP32-C3 has a built-in USB Serial/JTAG controller that provides a +// CDC-ACM serial port. The USB protocol and enumeration are handled entirely +// in hardware; software only reads/writes the EP1 FIFO. + +const cpuInterruptFromUSB = 8 + +// flushTimeout is the maximum number of busy-wait iterations in flush(). +// Prevents hanging when no USB host is connected. +const flushTimeout = 200000 + +type USB_DEVICE struct { + Bus *esp.USB_DEVICE_Type + Buffer *RingBuffer +} + +var ( + _USBCDC = &USB_DEVICE{ + Bus: esp.USB_DEVICE, + Buffer: NewRingBuffer(), + } + + USBCDC Serialer = _USBCDC +) + +var ( + errUSBWrongSize = errors.New("USB: invalid write size") + errUSBCouldNotWriteAllData = errors.New("USB: could not write all data") +) + +type Serialer interface { + WriteByte(c byte) error + Write(data []byte) (n int, err error) + Configure(config UARTConfig) error + Buffered() int + ReadByte() (byte, error) + DTR() bool + RTS() bool +} + +var usbConfigured bool + +// USBDevice provides a stub USB device for the ESP32-C3. The hardware +// only supports a fixed-function CDC-ACM serial port, so the programmable +// USB device features are no-ops. +type USBDevice struct { + initcomplete bool + InitEndpointComplete bool +} + +var USBDev = &USBDevice{} + +func (dev *USBDevice) SetStallEPIn(ep uint32) {} +func (dev *USBDevice) SetStallEPOut(ep uint32) {} +func (dev *USBDevice) ClearStallEPIn(ep uint32) {} +func (dev *USBDevice) ClearStallEPOut(ep uint32) {} + +// initUSB is intentionally empty — the interp phase evaluates init() +// functions at compile time and cannot access hardware registers. +// Actual hardware setup is deferred to the first Configure() call. +func initUSB() {} + +// Configure initialises the USB Serial/JTAG controller clock, pads, and +// interrupt so that received data is buffered automatically. +func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { + if usbConfigured { + return nil + } + usbConfigured = true + + // Enable the USB_DEVICE peripheral clock. + // Do NOT reset the peripheral — the ROM bootloader has already + // configured the USB Serial/JTAG controller and the host may + // already be connected. Resetting would drop the USB link. + esp.SYSTEM.SetPERIP_CLK_EN0_USB_DEVICE_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_USB_DEVICE_RST(0) + + // Ensure internal PHY is selected and USB pads are enabled. + usbdev.Bus.SetCONF0_PHY_SEL(0) + usbdev.Bus.SetCONF0_USB_PAD_ENABLE(1) + usbdev.Bus.SetCONF0_DP_PULLUP(1) + + // Clear any pending interrupts. + usbdev.Bus.INT_CLR.Set(0xFFFFFFFF) + + // Enable the RX-packet-received interrupt. + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) + + // Map the USB peripheral interrupt to CPU interrupt cpuInterruptFromUSB. + esp.INTERRUPT_CORE0.SetUSB_INTR_MAP(cpuInterruptFromUSB) + + _ = interrupt.New(cpuInterruptFromUSB, func(interrupt.Interrupt) { + _USBCDC.handleInterrupt() + }).Enable() + + return nil +} + +// ensureConfigured triggers lazy initialization on first use. +func (usbdev *USB_DEVICE) ensureConfigured() { + if !usbConfigured { + usbdev.Configure(UARTConfig{}) + } +} + +// handleInterrupt drains the hardware RX FIFO into the software ring buffer. +func (usbdev *USB_DEVICE) handleInterrupt() { + // Read INT_ST while INT_ENA is still set (INT_ST = INT_RAW & INT_ENA). + intStatus := usbdev.Bus.INT_ST.Get() + + // Disable the RX interrupt to prevent re-triggering while we drain. + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0) + + if intStatus&esp.USB_DEVICE_INT_ST_SERIAL_OUT_RECV_PKT_INT_ST != 0 { + // Drain all available bytes from the EP1 OUT FIFO. + // Use EP1.Get() directly — the generated GetEP1_RDWR_BYTE is + // functionally identical, but a direct load makes the FIFO-pop + // intent explicit. + for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { + b := byte(usbdev.Bus.EP1.Get()) + usbdev.Buffer.Put(b) + } + // Clear the interrupt. + usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1) + } + + // Re-enable the RX interrupt. + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) +} + +func (usbdev *USB_DEVICE) WriteByte(c byte) error { + usbdev.ensureConfigured() + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + // FIFO full — try flushing first, then recheck. + usbdev.flush() + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return errUSBCouldNotWriteAllData + } + } + + // Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which + // does a read-modify-write — the read side-effect pops a byte from + // the RX FIFO. + usbdev.Bus.EP1.Set(uint32(c)) + usbdev.flush() + + return nil +} + +func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { + usbdev.ensureConfigured() + if len(data) == 0 { + return 0, nil + } + + for i, c := range data { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + if i > 0 { + usbdev.flush() + } + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return i, errUSBCouldNotWriteAllData + } + } + usbdev.Bus.EP1.Set(uint32(c)) + } + + usbdev.flush() + return len(data), nil +} + +// Buffered returns the number of bytes waiting in the receive ring buffer. +func (usbdev *USB_DEVICE) Buffered() int { + usbdev.ensureConfigured() + return int(usbdev.Buffer.Used()) +} + +// ReadByte returns a byte from the receive ring buffer. +func (usbdev *USB_DEVICE) ReadByte() (byte, error) { + b, ok := usbdev.Buffer.Get() + if !ok { + return 0, nil + } + return b, nil +} + +func (usbdev *USB_DEVICE) DTR() bool { + return false +} + +func (usbdev *USB_DEVICE) RTS() bool { + return false +} + +// flush signals WR_DONE and waits (with timeout) for the hardware to +// consume the data. A timeout prevents hanging when no USB host is present. +func (usbdev *USB_DEVICE) flush() { + usbdev.Bus.SetEP1_CONF_WR_DONE(1) + for i := 0; i < flushTimeout; i++ { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 { + return + } + } +} + +// The ESP32-C3 USB Serial/JTAG controller is fixed-function hardware. +// It only provides a CDC-ACM serial port; the USB protocol and endpoint +// configuration are handled entirely in silicon. The functions below +// are no-op stubs so that higher-level USB packages (HID, MIDI, …) +// compile, but they cannot add real endpoints on this hardware. + +// ConfigureUSBEndpoint is a no-op on ESP32-C3 — the hardware does not +// support programmable USB endpoints. +func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) { +} + +// SendZlp is a no-op on ESP32-C3 — the hardware handles control +// transfers internally. +func SendZlp() { +} + +// SendUSBInPacket is a no-op on ESP32-C3 — the hardware does not +// support arbitrary IN endpoints. Returns false to indicate the +// packet was not sent. +func SendUSBInPacket(ep uint32, data []byte) bool { + return false +} diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index bdd1281a81..a2e79dc0c1 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -1,4 +1,4 @@ -//go:build esp32s3 || esp32c3 +//go:build esp32s3 package machine From adb95d8d847387ce557b631f3ed2da254c15b0de Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Tue, 17 Mar 2026 18:58:01 +0200 Subject: [PATCH 49/81] WiFi/BLE ROM --- src/internal/task/task_stack_tinygoriscv.go | 5 +++++ targets/esp32c3.ld | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/internal/task/task_stack_tinygoriscv.go b/src/internal/task/task_stack_tinygoriscv.go index 541dc96a4c..27e6542474 100644 --- a/src/internal/task/task_stack_tinygoriscv.go +++ b/src/internal/task/task_stack_tinygoriscv.go @@ -71,3 +71,8 @@ func (s *state) pause() { func SystemStack() uintptr { return *runtime_systemStackPtr() } + +//export tinygo_task_current +func tinygo_task_current() unsafe.Pointer { + return unsafe.Pointer(Current()) +} diff --git a/targets/esp32c3.ld b/targets/esp32c3.ld index 81a8c4f2ae..18bde26044 100644 --- a/targets/esp32c3.ld +++ b/targets/esp32c3.ld @@ -29,9 +29,12 @@ MEMORY { - /* Note: DRAM and IRAM below are actually in the same 384K address space. */ - DRAM (rw) : ORIGIN = 0x3FC80000, LENGTH = 384K /* Internal SRAM 1 (data bus) */ - IRAM (x) : ORIGIN = 0x40380000, LENGTH = 384K /* Internal SRAM 1 (instruction bus) */ + /* Note: DRAM and IRAM below are actually in the same 384K address space. + * WiFi/BLE ROM variables occupy 0x3FCCE710..0x3FCDFFFC, so DRAM must + * stop before that boundary to prevent the Go heap from overwriting them. + */ + DRAM (rw) : ORIGIN = 0x3FC80000, LENGTH = 0x4E710 /* ends at 0x3FCCE710 — WiFi/ROM boundary */ + IRAM (x) : ORIGIN = 0x40380000, LENGTH = 0x4E710 /* Note: DROM and IROM below are actually in the same 8M address space. */ DROM (r) : ORIGIN = 0x3C000000, LENGTH = 8M /* Data bus (read-only) */ @@ -59,6 +62,7 @@ SECTIONS .rodata : ALIGN(4) { *(.rodata*) + *(.srodata*) . = ALIGN (4); } >DROM @@ -83,6 +87,7 @@ SECTIONS . = ALIGN (4); _sbss = ABSOLUTE(.); *(.sbss) + *(.sbss.*) *(.bss .bss.*) . = ALIGN (4); _ebss = ABSOLUTE(.); From 755ce413bc4d315b60a4bbc36f82f583b5f4186d Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 15 Mar 2026 15:43:56 +0100 Subject: [PATCH 50/81] machine/stm32: add STM32U585 target definition and runtime Add processor target (cortex-m33), linker script (2048K flash, 768K RAM), and arduino-uno-q board target. Runtime initializes 160MHz clock via PLL1 from HSI16, with PWR Range 1 and EPOD booster. Also add psctype abstraction for timer PSC register width, needed because the U5 SVD defines PSC as a 16-bit register. --- src/machine/machine_stm32_tim.go | 2 +- src/machine/machine_stm32f103.go | 1 + src/machine/machine_stm32f4.go | 1 + src/machine/machine_stm32f7.go | 1 + src/machine/machine_stm32g0.go | 1 + src/machine/machine_stm32l0x1.go | 1 + src/machine/machine_stm32l0x2.go | 1 + src/machine/machine_stm32l4.go | 1 + src/machine/machine_stm32l5.go | 1 + src/machine/machine_stm32wlx.go | 1 + src/runtime/runtime_stm32u5.go | 101 +++++++++++++++++++++++++++++++ src/runtime/runtime_stm32u585.go | 15 +++++ targets/arduino-uno-q.json | 7 +++ targets/stm32u585.json | 16 +++++ targets/stm32u585.ld | 10 +++ 15 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/runtime/runtime_stm32u5.go create mode 100644 src/runtime/runtime_stm32u585.go create mode 100644 targets/arduino-uno-q.json create mode 100644 targets/stm32u585.json create mode 100644 targets/stm32u585.ld diff --git a/src/machine/machine_stm32_tim.go b/src/machine/machine_stm32_tim.go index 4898ed9f6f..da11b44e1f 100644 --- a/src/machine/machine_stm32_tim.go +++ b/src/machine/machine_stm32_tim.go @@ -150,7 +150,7 @@ func (t *TIM) setPeriod(period uint64, updatePrescaler bool) error { psc = ceil(top, ARR_MAX) top = top / psc - t.Device.PSC.Set(uint32(psc - 1)) + t.Device.PSC.Set(psctype(psc - 1)) } else { psc = uint64(t.Device.PSC.Get()) + 1 top = top / psc diff --git a/src/machine/machine_stm32f103.go b/src/machine/machine_stm32f103.go index 9e7bb347c9..d946ef4aae 100644 --- a/src/machine/machine_stm32f103.go +++ b/src/machine/machine_stm32f103.go @@ -731,6 +731,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32f4.go b/src/machine/machine_stm32f4.go index 31f1d2c635..2a989b10a8 100644 --- a/src/machine/machine_stm32f4.go +++ b/src/machine/machine_stm32f4.go @@ -607,6 +607,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32f7.go b/src/machine/machine_stm32f7.go index 11eff11081..7944769de0 100644 --- a/src/machine/machine_stm32f7.go +++ b/src/machine/machine_stm32f7.go @@ -712,6 +712,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32g0.go b/src/machine/machine_stm32g0.go index 530c77aade..7390aed6cf 100644 --- a/src/machine/machine_stm32g0.go +++ b/src/machine/machine_stm32g0.go @@ -554,6 +554,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32l0x1.go b/src/machine/machine_stm32l0x1.go index 292b81f462..3157f947e8 100644 --- a/src/machine/machine_stm32l0x1.go +++ b/src/machine/machine_stm32l0x1.go @@ -189,6 +189,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32l0x2.go b/src/machine/machine_stm32l0x2.go index fe1ba3a229..d8873fc4be 100644 --- a/src/machine/machine_stm32l0x2.go +++ b/src/machine/machine_stm32l0x2.go @@ -246,6 +246,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32l4.go b/src/machine/machine_stm32l4.go index b5babc0b2a..8453b28299 100644 --- a/src/machine/machine_stm32l4.go +++ b/src/machine/machine_stm32l4.go @@ -532,6 +532,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32l5.go b/src/machine/machine_stm32l5.go index faa583c9c3..89b3a1f14a 100644 --- a/src/machine/machine_stm32l5.go +++ b/src/machine/machine_stm32l5.go @@ -542,6 +542,7 @@ func (t *TIM) enableMainOutput() { } type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/machine/machine_stm32wlx.go b/src/machine/machine_stm32wlx.go index 80ca791e6a..b94e551ffe 100644 --- a/src/machine/machine_stm32wlx.go +++ b/src/machine/machine_stm32wlx.go @@ -433,6 +433,7 @@ func initRNG() { //---------- type arrtype = uint32 +type psctype = uint32 type arrRegType = volatile.Register32 const ( diff --git a/src/runtime/runtime_stm32u5.go b/src/runtime/runtime_stm32u5.go new file mode 100644 index 0000000000..be55b5e371 --- /dev/null +++ b/src/runtime/runtime_stm32u5.go @@ -0,0 +1,101 @@ +//go:build stm32u5 + +package runtime + +import ( + "device/stm32" + "machine" +) + +func putchar(c byte) { + machine.Serial.WriteByte(c) +} + +func getchar() byte { + for machine.Serial.Buffered() == 0 { + Gosched() + } + v, _ := machine.Serial.ReadByte() + return v +} + +func buffered() int { + return machine.Serial.Buffered() +} + +func initCLK() { + // Initialize clock to 160MHz using PLL1 with HSI16 as source. + // PLL1 configuration: HSI16 (16MHz) / PLLM(1) * PLLN(10) / PLLR(1) = 160MHz + // VCO = 16 * 10 = 160MHz, PLLR = /1 -> 160MHz SYSCLK + + // Enable PWR clock + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_CRSEN) // CRS enable not needed but ensures APB1 is clocked + _ = stm32.RCC.APB1ENR1.Get() + + // Set voltage scaling to Range 1 for 160MHz operation + // On U5, voltage scaling is in PWR.VOSR register + // VOS = 11 (Range 1, up to 160MHz) + stm32.PWR.VOSR.ReplaceBits(stm32.PWR_VOSR_VOS_Range1, stm32.PWR_VOSR_VOS_Msk, 0) + // Wait for VOS ready + for !stm32.PWR.VOSR.HasBits(stm32.PWR_VOSR_VOSRDY) { + } + + // Enable EPOD booster for high performance (required for Range 1 >100MHz) + stm32.PWR.VOSR.SetBits(stm32.PWR_VOSR_BOOSTEN) + for !stm32.PWR.VOSR.HasBits(stm32.PWR_VOSR_BOOSTRDY) { + } + + // Enable HSI16 + stm32.RCC.CR.SetBits(stm32.RCC_CR_HSION) + for !stm32.RCC.CR.HasBits(stm32.RCC_CR_HSIRDY) { + } + + // Disable PLL1 before configuration + stm32.RCC.CR.ClearBits(stm32.RCC_CR_PLL1ON) + for stm32.RCC.CR.HasBits(stm32.RCC_CR_PLL1RDY) { + } + + // Configure PLL1: + // Source = HSI16 (0x2) + // PLLM = 0 (divide by 1) + // PLL1RGE = 0x3 (input range 8-16MHz) + // Enable PLLR output + stm32.RCC.PLL1CFGR.Set( + (stm32.RCC_PLL1CFGR_PLL1SRC_HSI16 << stm32.RCC_PLL1CFGR_PLL1SRC_Pos) | + (stm32.RCC_PLL1CFGR_PLL1M_Div1 << stm32.RCC_PLL1CFGR_PLL1M_Pos) | + (stm32.RCC_PLL1CFGR_PLL1RGE_Range2 << stm32.RCC_PLL1CFGR_PLL1RGE_Pos)) + + // Enable PLL1R output + stm32.RCC.PLL1CFGR.SetBits(1 << stm32.RCC_PLL1CFGR_PLL1REN_Pos) + + // Set PLL1 dividers in PLL1DIVR: + // PLL1N = 10 (value - 1 = 9 in register) + // PLL1R = 1 (value - 1 = 0 in register) + // VCO = HSI16/1 * 10 = 160MHz + // PLLR output = 160MHz / 1 = 160MHz + stm32.RCC.SetPLL1DIVR_PLL1N(9) // N = 10, register value = N-1 = 9 + stm32.RCC.SetPLL1DIVR_PLL1R(0) // R = 1, register value = R-1 = 0 + + // Enable PLL1 + stm32.RCC.CR.SetBits(stm32.RCC_CR_PLL1ON) + for !stm32.RCC.CR.HasBits(stm32.RCC_CR_PLL1RDY) { + } + + // Set flash latency to 4 wait states (required for 160MHz in Range 1) + const FLASH_LATENCY_4 = 4 + stm32.FLASH.ACR.ReplaceBits(FLASH_LATENCY_4, stm32.Flash_ACR_LATENCY_Msk, 0) + for (stm32.FLASH.ACR.Get() & stm32.Flash_ACR_LATENCY_Msk) != FLASH_LATENCY_4 { + } + + // Set AHB prescaler to 1 (no division) in CFGR2 + stm32.RCC.CFGR2.ReplaceBits(stm32.RCC_CFGR2_HPRE_Div1, stm32.RCC_CFGR2_HPRE_Msk, 0) + + // Set APB1 and APB2 prescalers to 1 (no division) in CFGR2 + stm32.RCC.CFGR2.ReplaceBits(stm32.RCC_CFGR2_PPRE1_Div1, stm32.RCC_CFGR2_PPRE1_Msk, 0) + stm32.RCC.CFGR2.ReplaceBits(stm32.RCC_CFGR2_PPRE2_Div1, stm32.RCC_CFGR2_PPRE2_Msk, 0) + + // Switch system clock to PLL1 (SW = 11 = PLL) + stm32.RCC.CFGR1.ReplaceBits(stm32.RCC_CFGR1_SW_PLL, stm32.RCC_CFGR1_SW_Msk, 0) + for (stm32.RCC.CFGR1.Get() & stm32.RCC_CFGR1_SWS_Msk) != (stm32.RCC_CFGR1_SWS_PLL << stm32.RCC_CFGR1_SWS_Pos) { + } +} diff --git a/src/runtime/runtime_stm32u585.go b/src/runtime/runtime_stm32u585.go new file mode 100644 index 0000000000..be878cc116 --- /dev/null +++ b/src/runtime/runtime_stm32u585.go @@ -0,0 +1,15 @@ +//go:build stm32u585 + +package runtime + +import ( + "machine" +) + +func init() { + initCLK() + + machine.InitSerial() + + initTickTimer(&machine.TIM16) +} diff --git a/targets/arduino-uno-q.json b/targets/arduino-uno-q.json new file mode 100644 index 0000000000..89b0689f7f --- /dev/null +++ b/targets/arduino-uno-q.json @@ -0,0 +1,7 @@ +{ + "inherits": ["stm32u585"], + "build-tags": ["arduino_uno_q"], + "serial": "uart", + "openocd-interface": "stlink", + "openocd-target": "stm32u5x" +} diff --git a/targets/stm32u585.json b/targets/stm32u585.json new file mode 100644 index 0000000000..2b6d6eeb38 --- /dev/null +++ b/targets/stm32u585.json @@ -0,0 +1,16 @@ +{ + "inherits": [ + "cortex-m33" + ], + "build-tags": [ + "stm32u585", + "stm32u5", + "stm32" + ], + "extra-files": [ + "src/device/stm32/stm32u585.s" + ], + "linkerscript": "targets/stm32u585.ld", + "flash-method": "openocd", + "openocd-target": "stm32u5x" +} diff --git a/targets/stm32u585.ld b/targets/stm32u585.ld new file mode 100644 index 0000000000..c2ffd3c003 --- /dev/null +++ b/targets/stm32u585.ld @@ -0,0 +1,10 @@ + +MEMORY +{ + FLASH_TEXT (rw) : ORIGIN = 0x08000000, LENGTH = 2048K + RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 768K +} + +_stack_size = 4K; + +INCLUDE "targets/arm.ld" From fe07f817dc44f14eb34edfce169d92cf0e51ca23 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 15 Mar 2026 15:44:54 +0100 Subject: [PATCH 51/81] machine/stm32: add STM32U5 GPIO, pin interrupts, and UART support Add machine-level support for STM32U5 family: GPIO ports A-I with clock enables via AHB2ENR1, EXTI pin interrupts (individual EXTI0-15 IRQs), timer definitions (TIM1-8, TIM15-17), RNG, and alternate function clock enables for all peripherals. Add UART support with STM32U585 baud rate and register configuration. Add arduino-uno-q board pin definitions and USART2 serial. Update shared build tags: gpio_revb, gpio_reva, exti_exti, exti_syscfg. --- src/machine/board_arduino_uno_q.go | 73 +++ src/machine/machine_stm32_exti_exti.go | 2 +- src/machine/machine_stm32_exti_syscfg.go | 2 +- src/machine/machine_stm32_gpio_reva.go | 2 +- src/machine/machine_stm32_gpio_revb.go | 2 +- src/machine/machine_stm32u5.go | 577 +++++++++++++++++++++++ src/machine/machine_stm32u585.go | 47 ++ 7 files changed, 701 insertions(+), 4 deletions(-) create mode 100644 src/machine/board_arduino_uno_q.go create mode 100644 src/machine/machine_stm32u5.go create mode 100644 src/machine/machine_stm32u585.go diff --git a/src/machine/board_arduino_uno_q.go b/src/machine/board_arduino_uno_q.go new file mode 100644 index 0000000000..8c2412cdfd --- /dev/null +++ b/src/machine/board_arduino_uno_q.go @@ -0,0 +1,73 @@ +package tmp +//go:build arduino_uno_q + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} UART1.Interrupt = interrupt.New(stm32.IRQ_USART2, _UART1.handleInterrupt)func init() {) DefaultUART = UART1 } RxAltFuncSelector: AF7_USART1_2_3, TxAltFuncSelector: AF7_USART1_2_3, Bus: stm32.USART2, Buffer: NewRingBuffer(), _UART1 = UART{ UART1 = &_UART1 // debugger to be exposed as virtual COM port over USB. // USART2 is the hardware serial port connected to the onboard ST-LINKvar () UART_RX_PIN = PA3 UART_TX_PIN = PA2 // PA2 and PA3 are connected to the ST-Link Virtual Com Port (VCP) // UART pinsconst () BUTTON = PC13const () LED_GREEN = PA5 LED_BUILTIN = LED_GREEN LED = LED_BUILTINconst () D15 = PB8 D14 = PB9 D13 = PA5 D12 = PA6 D11 = PA7 D10 = PB0 D9 = PC7 D8 = PA9 D7 = PA8 D6 = PB10 D5 = PB4 D4 = PB5 D3 = PB3 D2 = PA10 D1 = PB6 D0 = PB7 A5 = PB12 A4 = PB11 A3 = PB1 A2 = PA4 A1 = PA1 A0 = PA0 // Arduino Pinsconst () "runtime/interrupt" "device/stm32"import (package machine// Arduino UNO Q board with STM32U585 processor. \ No newline at end of file diff --git a/src/machine/machine_stm32_exti_exti.go b/src/machine/machine_stm32_exti_exti.go index 874a043832..b332bfb070 100644 --- a/src/machine/machine_stm32_exti_exti.go +++ b/src/machine/machine_stm32_exti_exti.go @@ -1,4 +1,4 @@ -//go:build stm32l5 +//go:build stm32l5 || stm32u5 package machine diff --git a/src/machine/machine_stm32_exti_syscfg.go b/src/machine/machine_stm32_exti_syscfg.go index 51e1e1a571..4eaa6c4ca8 100644 --- a/src/machine/machine_stm32_exti_syscfg.go +++ b/src/machine/machine_stm32_exti_syscfg.go @@ -1,4 +1,4 @@ -//go:build stm32 && !stm32f1 && !stm32l5 && !stm32wlx && !stm32g0 +//go:build stm32 && !stm32f1 && !stm32l5 && !stm32wlx && !stm32g0 && !stm32u5 package machine diff --git a/src/machine/machine_stm32_gpio_reva.go b/src/machine/machine_stm32_gpio_reva.go index d7eaad0ac4..27839c4918 100644 --- a/src/machine/machine_stm32_gpio_reva.go +++ b/src/machine/machine_stm32_gpio_reva.go @@ -1,4 +1,4 @@ -//go:build stm32 && !stm32l4 && !stm32l5 && !stm32wlx && !stm32g0 +//go:build stm32 && !stm32l4 && !stm32l5 && !stm32wlx && !stm32g0 && !stm32u5 package machine diff --git a/src/machine/machine_stm32_gpio_revb.go b/src/machine/machine_stm32_gpio_revb.go index 49094c730c..38278f3e31 100644 --- a/src/machine/machine_stm32_gpio_revb.go +++ b/src/machine/machine_stm32_gpio_revb.go @@ -1,4 +1,4 @@ -//go:build stm32l4 || stm32l5 +//go:build stm32l4 || stm32l5 || stm32u5 package machine diff --git a/src/machine/machine_stm32u5.go b/src/machine/machine_stm32u5.go new file mode 100644 index 0000000000..720e26ca2e --- /dev/null +++ b/src/machine/machine_stm32u5.go @@ -0,0 +1,577 @@ +//go:build stm32u5 + +package machine + +// Peripheral abstraction layer for the stm32u5 + +import ( + "device/stm32" + "runtime/interrupt" + "runtime/volatile" + "unsafe" +) + +var deviceIDAddr = []uintptr{0x0BFA0590, 0x0BFA0594, 0x0BFA0598} + +const ( + AF0_SYSTEM = 0 + AF1_TIM1_2_5_8_LPTIM1 = 1 + AF2_TIM1_2_3_4_5_LPTIM2 = 2 + AF3_SAI1_SPI2_TIM1_8_USART2_OCTOSPI1 = 3 + AF4_I2C1_2_3_4 = 4 + AF5_SPI1_2_3_OCTOSPI1_OCTOSPI2 = 5 + AF6_SPI3_I2C3_DFSDM1_COMP1 = 6 + AF7_USART1_2_3 = 7 + AF8_UART4_5_LPUART1_SDMMC1 = 8 + AF9_FDCAN1_TSC = 9 + AF10_USB_OCTOSPI1_OCTOSPI2 = 10 + AF11_UCPD1 = 11 + AF12_SDMMC1_COMP1_2_TIM1_8_FMC = 12 + AF13_SAI1_2_TIM8 = 13 + AF14_TIM2_8_15_16_17_LPTIM2 = 14 + AF15_EVENTOUT = 15 +) + +const ( + PA0 = portA + 0 + PA1 = portA + 1 + PA2 = portA + 2 + PA3 = portA + 3 + PA4 = portA + 4 + PA5 = portA + 5 + PA6 = portA + 6 + PA7 = portA + 7 + PA8 = portA + 8 + PA9 = portA + 9 + PA10 = portA + 10 + PA11 = portA + 11 + PA12 = portA + 12 + PA13 = portA + 13 + PA14 = portA + 14 + PA15 = portA + 15 + + PB0 = portB + 0 + PB1 = portB + 1 + PB2 = portB + 2 + PB3 = portB + 3 + PB4 = portB + 4 + PB5 = portB + 5 + PB6 = portB + 6 + PB7 = portB + 7 + PB8 = portB + 8 + PB9 = portB + 9 + PB10 = portB + 10 + PB11 = portB + 11 + PB12 = portB + 12 + PB13 = portB + 13 + PB14 = portB + 14 + PB15 = portB + 15 + + PC0 = portC + 0 + PC1 = portC + 1 + PC2 = portC + 2 + PC3 = portC + 3 + PC4 = portC + 4 + PC5 = portC + 5 + PC6 = portC + 6 + PC7 = portC + 7 + PC8 = portC + 8 + PC9 = portC + 9 + PC10 = portC + 10 + PC11 = portC + 11 + PC12 = portC + 12 + PC13 = portC + 13 + PC14 = portC + 14 + PC15 = portC + 15 + + PD0 = portD + 0 + PD1 = portD + 1 + PD2 = portD + 2 + PD3 = portD + 3 + PD4 = portD + 4 + PD5 = portD + 5 + PD6 = portD + 6 + PD7 = portD + 7 + PD8 = portD + 8 + PD9 = portD + 9 + PD10 = portD + 10 + PD11 = portD + 11 + PD12 = portD + 12 + PD13 = portD + 13 + PD14 = portD + 14 + PD15 = portD + 15 + + PE0 = portE + 0 + PE1 = portE + 1 + PE2 = portE + 2 + PE3 = portE + 3 + PE4 = portE + 4 + PE5 = portE + 5 + PE6 = portE + 6 + PE7 = portE + 7 + PE8 = portE + 8 + PE9 = portE + 9 + PE10 = portE + 10 + PE11 = portE + 11 + PE12 = portE + 12 + PE13 = portE + 13 + PE14 = portE + 14 + PE15 = portE + 15 + + PF0 = portF + 0 + PF1 = portF + 1 + PF2 = portF + 2 + PF3 = portF + 3 + PF4 = portF + 4 + PF5 = portF + 5 + PF6 = portF + 6 + PF7 = portF + 7 + PF8 = portF + 8 + PF9 = portF + 9 + PF10 = portF + 10 + PF11 = portF + 11 + PF12 = portF + 12 + PF13 = portF + 13 + PF14 = portF + 14 + PF15 = portF + 15 + + PG0 = portG + 0 + PG1 = portG + 1 + PG2 = portG + 2 + PG3 = portG + 3 + PG4 = portG + 4 + PG5 = portG + 5 + PG6 = portG + 6 + PG7 = portG + 7 + PG8 = portG + 8 + PG9 = portG + 9 + PG10 = portG + 10 + PG11 = portG + 11 + PG12 = portG + 12 + PG13 = portG + 13 + PG14 = portG + 14 + PG15 = portG + 15 + + PH0 = portH + 0 + PH1 = portH + 1 + PH2 = portH + 2 + PH3 = portH + 3 + PH4 = portH + 4 + PH5 = portH + 5 + PH6 = portH + 6 + PH7 = portH + 7 + PH8 = portH + 8 + PH9 = portH + 9 + PH10 = portH + 10 + PH11 = portH + 11 + PH12 = portH + 12 + PH13 = portH + 13 + PH14 = portH + 14 + PH15 = portH + 15 + + PI0 = portI + 0 + PI1 = portI + 1 + PI2 = portI + 2 + PI3 = portI + 3 + PI4 = portI + 4 + PI5 = portI + 5 + PI6 = portI + 6 + PI7 = portI + 7 + PI8 = portI + 8 + PI9 = portI + 9 + PI10 = portI + 10 + PI11 = portI + 11 + PI12 = portI + 12 + PI13 = portI + 13 + PI14 = portI + 14 + PI15 = portI + 15 +) + +func (p Pin) getPort() *stm32.GPIO_Type { + switch p / 16 { + case 0: + return stm32.GPIOA + case 1: + return stm32.GPIOB + case 2: + return stm32.GPIOC + case 3: + return stm32.GPIOD + case 4: + return stm32.GPIOE + case 5: + return stm32.GPIOF + case 6: + return stm32.GPIOG + case 7: + return stm32.GPIOH + case 8: + return stm32.GPIOI + default: + panic("machine: unknown port") + } +} + +// enableClock enables the clock for this desired GPIO port. +func (p Pin) enableClock() { + switch p / 16 { + case 0: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOAEN) + case 1: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOBEN) + case 2: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOCEN) + case 3: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIODEN) + case 4: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOEEN) + case 5: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOFEN) + case 6: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOGEN) + case 7: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOHEN) + case 8: + stm32.RCC.AHB2ENR1.SetBits(stm32.RCC_AHB2ENR1_GPIOIEN) + default: + panic("machine: unknown port") + } +} + +// Enable peripheral clock +func enableAltFuncClock(bus unsafe.Pointer) { + switch bus { + case unsafe.Pointer(stm32.PWR): // Power interface clock enable + // PWR clock is always enabled on U5 (no dedicated enable bit needed) + case unsafe.Pointer(stm32.I2C1): // I2C1 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_I2C1EN) + case unsafe.Pointer(stm32.I2C2): // I2C2 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_I2C2EN) + case unsafe.Pointer(stm32.I2C3): // I2C3 clock enable + stm32.RCC.APB3ENR.SetBits(stm32.RCC_APB3ENR_I2C3EN) + case unsafe.Pointer(stm32.I2C4): // I2C4 clock enable + stm32.RCC.APB1ENR2.SetBits(stm32.RCC_APB1ENR2_I2C4EN) + case unsafe.Pointer(stm32.USART1): // USART1 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_USART1EN) + case unsafe.Pointer(stm32.USART2): // USART2 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_USART2EN) + case unsafe.Pointer(stm32.USART3): // USART3 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_USART3EN) + case unsafe.Pointer(stm32.UART4): // UART4 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_UART4EN) + case unsafe.Pointer(stm32.UART5): // UART5 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_UART5EN) + case unsafe.Pointer(stm32.LPUART1): // LPUART1 clock enable + stm32.RCC.APB3ENR.SetBits(stm32.RCC_APB3ENR_LPUART1EN) + case unsafe.Pointer(stm32.SPI1): // SPI1 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_SPI1EN) + case unsafe.Pointer(stm32.SPI2): // SPI2 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_SPI2EN) + case unsafe.Pointer(stm32.SPI3): // SPI3 clock enable + stm32.RCC.APB3ENR.SetBits(stm32.RCC_APB3ENR_SPI3EN) + case unsafe.Pointer(stm32.WWDG): // Window watchdog clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_WWDGEN) + case unsafe.Pointer(stm32.TIM1): // TIM1 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_TIM1EN) + case unsafe.Pointer(stm32.TIM2): // TIM2 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_TIM2EN) + case unsafe.Pointer(stm32.TIM3): // TIM3 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_TIM3EN) + case unsafe.Pointer(stm32.TIM4): // TIM4 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_TIM4EN) + case unsafe.Pointer(stm32.TIM5): // TIM5 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_TIM5EN) + case unsafe.Pointer(stm32.TIM6): // TIM6 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_TIM6EN) + case unsafe.Pointer(stm32.TIM7): // TIM7 clock enable + stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_TIM7EN) + case unsafe.Pointer(stm32.TIM8): // TIM8 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_TIM8EN) + case unsafe.Pointer(stm32.TIM15): // TIM15 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_TIM15EN) + case unsafe.Pointer(stm32.TIM16): // TIM16 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_TIM16EN) + case unsafe.Pointer(stm32.TIM17): // TIM17 clock enable + stm32.RCC.APB2ENR.SetBits(stm32.RCC_APB2ENR_TIM17EN) + case unsafe.Pointer(stm32.SYSCFG): // System configuration controller clock enable + stm32.RCC.APB3ENR.SetBits(stm32.RCC_APB3ENR_SYSCFGEN) + case unsafe.Pointer(stm32.FDCAN1): // FDCAN1 clock enable + stm32.RCC.APB1ENR2.SetBits(stm32.RCC_APB1ENR2_FDCAN1EN) + } +} + +func (p Pin) registerInterrupt() interrupt.Interrupt { + pin := uint8(p) % 16 + + switch pin { + case 0: + return interrupt.New(stm32.IRQ_EXTI0, func(interrupt.Interrupt) { handlePinInterrupt(0) }) + case 1: + return interrupt.New(stm32.IRQ_EXTI1, func(interrupt.Interrupt) { handlePinInterrupt(1) }) + case 2: + return interrupt.New(stm32.IRQ_EXTI2, func(interrupt.Interrupt) { handlePinInterrupt(2) }) + case 3: + return interrupt.New(stm32.IRQ_EXTI3, func(interrupt.Interrupt) { handlePinInterrupt(3) }) + case 4: + return interrupt.New(stm32.IRQ_EXTI4, func(interrupt.Interrupt) { handlePinInterrupt(4) }) + case 5: + return interrupt.New(stm32.IRQ_EXTI5, func(interrupt.Interrupt) { handlePinInterrupt(5) }) + case 6: + return interrupt.New(stm32.IRQ_EXTI6, func(interrupt.Interrupt) { handlePinInterrupt(6) }) + case 7: + return interrupt.New(stm32.IRQ_EXTI7, func(interrupt.Interrupt) { handlePinInterrupt(7) }) + case 8: + return interrupt.New(stm32.IRQ_EXTI8, func(interrupt.Interrupt) { handlePinInterrupt(8) }) + case 9: + return interrupt.New(stm32.IRQ_EXTI9, func(interrupt.Interrupt) { handlePinInterrupt(9) }) + case 10: + return interrupt.New(stm32.IRQ_EXTI10, func(interrupt.Interrupt) { handlePinInterrupt(10) }) + case 11: + return interrupt.New(stm32.IRQ_EXTI11, func(interrupt.Interrupt) { handlePinInterrupt(11) }) + case 12: + return interrupt.New(stm32.IRQ_EXTI12, func(interrupt.Interrupt) { handlePinInterrupt(12) }) + case 13: + return interrupt.New(stm32.IRQ_EXTI13, func(interrupt.Interrupt) { handlePinInterrupt(13) }) + case 14: + return interrupt.New(stm32.IRQ_EXTI14, func(interrupt.Interrupt) { handlePinInterrupt(14) }) + case 15: + return interrupt.New(stm32.IRQ_EXTI15, func(interrupt.Interrupt) { handlePinInterrupt(15) }) + } + + return interrupt.Interrupt{} +} + +func handlePinInterrupt(pin uint8) { + if stm32.EXTI.RPR1.HasBits(1< Date: Sun, 15 Mar 2026 15:45:38 +0100 Subject: [PATCH 52/81] machine/stm32: add STM32U5 I2C support Add I2C timing values for STM32U585 at 160MHz PCLK1 (10/100/400/500 KHz). Add I2C1 peripheral and pin definitions to arduino-uno-q board. Update i2c_revb build tag to include stm32u5. --- src/machine/board_arduino_uno_q.go | 13 ++++++++++++- src/machine/machine_stm32_i2c_revb.go | 2 +- src/machine/machine_stm32u585.go | 23 ++++++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/machine/board_arduino_uno_q.go b/src/machine/board_arduino_uno_q.go index 8c2412cdfd..364529e58c 100644 --- a/src/machine/board_arduino_uno_q.go +++ b/src/machine/board_arduino_uno_q.go @@ -70,4 +70,15 @@ package tmp -} UART1.Interrupt = interrupt.New(stm32.IRQ_USART2, _UART1.handleInterrupt)func init() {) DefaultUART = UART1 } RxAltFuncSelector: AF7_USART1_2_3, TxAltFuncSelector: AF7_USART1_2_3, Bus: stm32.USART2, Buffer: NewRingBuffer(), _UART1 = UART{ UART1 = &_UART1 // debugger to be exposed as virtual COM port over USB. // USART2 is the hardware serial port connected to the onboard ST-LINKvar () UART_RX_PIN = PA3 UART_TX_PIN = PA2 // PA2 and PA3 are connected to the ST-Link Virtual Com Port (VCP) // UART pinsconst () BUTTON = PC13const () LED_GREEN = PA5 LED_BUILTIN = LED_GREEN LED = LED_BUILTINconst () D15 = PB8 D14 = PB9 D13 = PA5 D12 = PA6 D11 = PA7 D10 = PB0 D9 = PC7 D8 = PA9 D7 = PA8 D6 = PB10 D5 = PB4 D4 = PB5 D3 = PB3 D2 = PA10 D1 = PB6 D0 = PB7 A5 = PB12 A4 = PB11 A3 = PB1 A2 = PA4 A1 = PA1 A0 = PA0 // Arduino Pinsconst () "runtime/interrupt" "device/stm32"import (package machine// Arduino UNO Q board with STM32U585 processor. \ No newline at end of file + + + + + + + + + + + +} UART1.Interrupt = interrupt.New(stm32.IRQ_USART2, _UART1.handleInterrupt)func init() {) I2C0 = I2C1 } AltFuncSelector: AF4_I2C1_2_3_4, Bus: stm32.I2C1, I2C1 = &I2C{ // I2C1 is documented, alias to I2C0 as well DefaultUART = UART1 } RxAltFuncSelector: AF7_USART1_2_3, TxAltFuncSelector: AF7_USART1_2_3, Bus: stm32.USART2, Buffer: NewRingBuffer(), _UART1 = UART{ UART1 = &_UART1 // debugger to be exposed as virtual COM port over USB. // USART2 is the hardware serial port connected to the onboard ST-LINKvar () I2C0_SDA_PIN = PB9 I2C0_SCL_PIN = PB8 // I2C pins UART_RX_PIN = PA3 UART_TX_PIN = PA2 // PA2 and PA3 are connected to the ST-Link Virtual Com Port (VCP) // UART pinsconst () BUTTON = PC13const () LED_GREEN = PA5 LED_BUILTIN = LED_GREEN LED = LED_BUILTINconst () D15 = PB8 D14 = PB9 D13 = PA5 D12 = PA6 D11 = PA7 D10 = PB0 D9 = PC7 D8 = PA9 D7 = PA8 D6 = PB10 D5 = PB4 D4 = PB5 D3 = PB3 D2 = PA10 D1 = PB6 D0 = PB7 A5 = PB12 A4 = PB11 A3 = PB1 A2 = PA4 A1 = PA1 A0 = PA0 // Arduino Pinsconst () "runtime/interrupt" "device/stm32"import (package machine// Arduino UNO Q board with STM32U585 processor. \ No newline at end of file diff --git a/src/machine/machine_stm32_i2c_revb.go b/src/machine/machine_stm32_i2c_revb.go index 9e184e1be8..644c7e38ab 100644 --- a/src/machine/machine_stm32_i2c_revb.go +++ b/src/machine/machine_stm32_i2c_revb.go @@ -1,4 +1,4 @@ -//go:build stm32l5 || stm32f7 || stm32l4 || stm32l0 || stm32wlx || stm32g0 +//go:build stm32l5 || stm32f7 || stm32l4 || stm32l0 || stm32wlx || stm32g0 || stm32u5 package machine diff --git a/src/machine/machine_stm32u585.go b/src/machine/machine_stm32u585.go index 9b92365ed3..d8d9719401 100644 --- a/src/machine/machine_stm32u585.go +++ b/src/machine/machine_stm32u585.go @@ -44,4 +44,25 @@ package tmp -} uart.txEmptyFlag = stm32.USART_ISR_TXE uart.statusReg = &uart.Bus.ISR uart.txReg = &uart.Bus.TDR uart.rxReg = &uart.Bus.RDRfunc (uart *UART) setRegisters() {// Register names vary by ST processor, these are for STM U5} return CPUFrequency() / baudRatefunc (uart *UART) getBaudRateDivisor(baudRate uint32) uint32 {// NOTE: keep this in sync with the runtime/runtime_stm32u5.go clock init code// UART baudrate calc based on the bus and clockspeed} config.RX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTRX}, uart.RxAltFuncSelector) config.TX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTTX}, uart.TxAltFuncSelector) // enable the alternate functions on the TX and RX pins } stm32.PWR.SetSVMCR_IO2SV(1) // Enable VDDIO2 power supply for PGx pins if config.RX.getPort() == stm32.GPIOG || config.TX.getPort() == stm32.GPIOG {func (uart *UART) configurePins(config UARTConfig) {// Configure the UART.//---------- UART related codeconst APB2_TIM_FREQ = 160e6 // 160MHzconst APB1_TIM_FREQ = 160e6 // 160MHz// and clock frequencies// in sync with any changes to runtime package which configures the oscillators// Internal use: configured speed of the APB1 and APB2 timers, this should be kept} return 160000000func CPUFrequency() uint32 {) "device/stm32"import (package machine \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + +} } return 0 default: return 0x00802172 case 500 * KHz: return 0x10802D9B case 400 * KHz: return 0x30A0A7FB case 100 * KHz: return 0xF010F3FE case 10 * KHz: switch br { // TODO: Do calculations based on PCLK1 // for 160MHz PCLK1. // These are 'magic' values calculated by STM32CubeMXfunc (i2c *I2C) getFreqRange(br uint32) uint32 {// Gets the value for TIMINGR register//---------- I2C related code} uart.txEmptyFlag = stm32.USART_ISR_TXE uart.statusReg = &uart.Bus.ISR uart.txReg = &uart.Bus.TDR uart.rxReg = &uart.Bus.RDRfunc (uart *UART) setRegisters() {// Register names vary by ST processor, these are for STM U5} return CPUFrequency() / baudRatefunc (uart *UART) getBaudRateDivisor(baudRate uint32) uint32 {// NOTE: keep this in sync with the runtime/runtime_stm32u5.go clock init code// UART baudrate calc based on the bus and clockspeed} config.RX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTRX}, uart.RxAltFuncSelector) config.TX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTTX}, uart.TxAltFuncSelector) // enable the alternate functions on the TX and RX pins } stm32.PWR.SetSVMCR_IO2SV(1) // Enable VDDIO2 power supply for PGx pins if config.RX.getPort() == stm32.GPIOG || config.TX.getPort() == stm32.GPIOG {func (uart *UART) configurePins(config UARTConfig) {// Configure the UART.//---------- UART related codeconst APB2_TIM_FREQ = 160e6 // 160MHzconst APB1_TIM_FREQ = 160e6 // 160MHz// and clock frequencies// in sync with any changes to runtime package which configures the oscillators// Internal use: configured speed of the APB1 and APB2 timers, this should be kept} return 160000000func CPUFrequency() uint32 {) "device/stm32"import (package machine \ No newline at end of file From ea166174ae6f011b9c5308e307bae33fd9fe6993 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 15 Mar 2026 15:46:03 +0100 Subject: [PATCH 53/81] machine/stm32: add STM32U5 SPI support Add SPIv2 driver for STM32U5 using TXDR/RXDR and CFG1/CFG2 registers, which differ from the classic SPI peripheral on older STM32 families. Implements Configure() and single-byte Transfer(). Add SPI1 peripheral and pin definitions to arduino-uno-q board. Exclude stm32u5 from the shared classic SPI build tag. --- src/machine/board_arduino_uno_q.go | 177 ++++++++++++++------------- src/machine/machine_stm32_spi.go | 2 +- src/machine/machine_stm32u585.go | 187 +++++++++++++++++++---------- src/machine/machine_stm32u5_spi.go | 119 ++++++++++++++++++ 4 files changed, 336 insertions(+), 149 deletions(-) create mode 100644 src/machine/machine_stm32u5_spi.go diff --git a/src/machine/board_arduino_uno_q.go b/src/machine/board_arduino_uno_q.go index 364529e58c..bf26eadb5f 100644 --- a/src/machine/board_arduino_uno_q.go +++ b/src/machine/board_arduino_uno_q.go @@ -1,84 +1,97 @@ -package tmp //go:build arduino_uno_q - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} UART1.Interrupt = interrupt.New(stm32.IRQ_USART2, _UART1.handleInterrupt)func init() {) I2C0 = I2C1 } AltFuncSelector: AF4_I2C1_2_3_4, Bus: stm32.I2C1, I2C1 = &I2C{ // I2C1 is documented, alias to I2C0 as well DefaultUART = UART1 } RxAltFuncSelector: AF7_USART1_2_3, TxAltFuncSelector: AF7_USART1_2_3, Bus: stm32.USART2, Buffer: NewRingBuffer(), _UART1 = UART{ UART1 = &_UART1 // debugger to be exposed as virtual COM port over USB. // USART2 is the hardware serial port connected to the onboard ST-LINKvar () I2C0_SDA_PIN = PB9 I2C0_SCL_PIN = PB8 // I2C pins UART_RX_PIN = PA3 UART_TX_PIN = PA2 // PA2 and PA3 are connected to the ST-Link Virtual Com Port (VCP) // UART pinsconst () BUTTON = PC13const () LED_GREEN = PA5 LED_BUILTIN = LED_GREEN LED = LED_BUILTINconst () D15 = PB8 D14 = PB9 D13 = PA5 D12 = PA6 D11 = PA7 D10 = PB0 D9 = PC7 D8 = PA9 D7 = PA8 D6 = PB10 D5 = PB4 D4 = PB5 D3 = PB3 D2 = PA10 D1 = PB6 D0 = PB7 A5 = PB12 A4 = PB11 A3 = PB1 A2 = PA4 A1 = PA1 A0 = PA0 // Arduino Pinsconst () "runtime/interrupt" "device/stm32"import (package machine// Arduino UNO Q board with STM32U585 processor. \ No newline at end of file +// Arduino UNO Q board with STM32U585 processor. + +package machine + +import ( + "device/stm32" + "runtime/interrupt" +) + +const ( + // Arduino Pins + A0 = PA0 + A1 = PA1 + A2 = PA4 + A3 = PB1 + A4 = PB11 + A5 = PB12 + + D0 = PB7 + D1 = PB6 + D2 = PA10 + D3 = PB3 + D4 = PB5 + D5 = PB4 + D6 = PB10 + D7 = PA8 + D8 = PA9 + D9 = PC7 + D10 = PB0 + D11 = PA7 + D12 = PA6 + D13 = PA5 + D14 = PB9 + D15 = PB8 +) + +const ( + LED = LED_BUILTIN + LED_BUILTIN = LED_GREEN + LED_GREEN = PA5 +) + +const ( + BUTTON = PC13 +) + +const ( + // UART pins + // PA2 and PA3 are connected to the ST-Link Virtual Com Port (VCP) + UART_TX_PIN = PA2 + UART_RX_PIN = PA3 + + // I2C pins + I2C0_SCL_PIN = PB8 + I2C0_SDA_PIN = PB9 + + // SPI pins + SPI1_SCK_PIN = PA5 + SPI1_SDI_PIN = PA6 + SPI1_SDO_PIN = PA7 + SPI0_SCK_PIN = SPI1_SCK_PIN + SPI0_SDI_PIN = SPI1_SDI_PIN + SPI0_SDO_PIN = SPI1_SDO_PIN +) + +var ( + // USART2 is the hardware serial port connected to the onboard ST-LINK + // debugger to be exposed as virtual COM port over USB. + UART1 = &_UART1 + _UART1 = UART{ + Buffer: NewRingBuffer(), + Bus: stm32.USART2, + TxAltFuncSelector: AF7_USART1_2_3, + RxAltFuncSelector: AF7_USART1_2_3, + } + DefaultUART = UART1 + + // I2C1 is documented, alias to I2C0 as well + I2C1 = &I2C{ + Bus: stm32.I2C1, + AltFuncSelector: AF4_I2C1_2_3_4, + } + I2C0 = I2C1 + + // SPI1 is documented, alias to SPI0 as well + SPI1 = &SPI{ + Bus: stm32.SPI1, + AltFuncSelector: AF5_SPI1_2_3_OCTOSPI1_OCTOSPI2, + } + SPI0 = SPI1 +) + +func init() { + UART1.Interrupt = interrupt.New(stm32.IRQ_USART2, _UART1.handleInterrupt) +} diff --git a/src/machine/machine_stm32_spi.go b/src/machine/machine_stm32_spi.go index 3fb9af7fb8..d246391be8 100644 --- a/src/machine/machine_stm32_spi.go +++ b/src/machine/machine_stm32_spi.go @@ -1,4 +1,4 @@ -//go:build stm32 && !stm32f7x2 && !stm32l5x2 && !stm32g0 +//go:build stm32 && !stm32f7x2 && !stm32l5x2 && !stm32g0 && !stm32u5 package machine diff --git a/src/machine/machine_stm32u585.go b/src/machine/machine_stm32u585.go index d8d9719401..901bec5e83 100644 --- a/src/machine/machine_stm32u585.go +++ b/src/machine/machine_stm32u585.go @@ -1,68 +1,123 @@ -package tmp //go:build stm32u585 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} } return 0 default: return 0x00802172 case 500 * KHz: return 0x10802D9B case 400 * KHz: return 0x30A0A7FB case 100 * KHz: return 0xF010F3FE case 10 * KHz: switch br { // TODO: Do calculations based on PCLK1 // for 160MHz PCLK1. // These are 'magic' values calculated by STM32CubeMXfunc (i2c *I2C) getFreqRange(br uint32) uint32 {// Gets the value for TIMINGR register//---------- I2C related code} uart.txEmptyFlag = stm32.USART_ISR_TXE uart.statusReg = &uart.Bus.ISR uart.txReg = &uart.Bus.TDR uart.rxReg = &uart.Bus.RDRfunc (uart *UART) setRegisters() {// Register names vary by ST processor, these are for STM U5} return CPUFrequency() / baudRatefunc (uart *UART) getBaudRateDivisor(baudRate uint32) uint32 {// NOTE: keep this in sync with the runtime/runtime_stm32u5.go clock init code// UART baudrate calc based on the bus and clockspeed} config.RX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTRX}, uart.RxAltFuncSelector) config.TX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTTX}, uart.TxAltFuncSelector) // enable the alternate functions on the TX and RX pins } stm32.PWR.SetSVMCR_IO2SV(1) // Enable VDDIO2 power supply for PGx pins if config.RX.getPort() == stm32.GPIOG || config.TX.getPort() == stm32.GPIOG {func (uart *UART) configurePins(config UARTConfig) {// Configure the UART.//---------- UART related codeconst APB2_TIM_FREQ = 160e6 // 160MHzconst APB1_TIM_FREQ = 160e6 // 160MHz// and clock frequencies// in sync with any changes to runtime package which configures the oscillators// Internal use: configured speed of the APB1 and APB2 timers, this should be kept} return 160000000func CPUFrequency() uint32 {) "device/stm32"import (package machine \ No newline at end of file +package machine + +import ( + "device/stm32" +) + +func CPUFrequency() uint32 { + return 160000000 +} + +// Internal use: configured speed of the APB1 and APB2 timers, this should be kept +// in sync with any changes to runtime package which configures the oscillators +// and clock frequencies +const APB1_TIM_FREQ = 160e6 // 160MHz +const APB2_TIM_FREQ = 160e6 // 160MHz + +//---------- UART related code + +// Configure the UART. +func (uart *UART) configurePins(config UARTConfig) { + if config.RX.getPort() == stm32.GPIOG || config.TX.getPort() == stm32.GPIOG { + // Enable VDDIO2 power supply for PGx pins + stm32.PWR.SetSVMCR_IO2SV(1) + } + + // enable the alternate functions on the TX and RX pins + config.TX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTTX}, uart.TxAltFuncSelector) + config.RX.ConfigureAltFunc(PinConfig{Mode: PinModeUARTRX}, uart.RxAltFuncSelector) +} + +// UART baudrate calc based on the bus and clockspeed +// NOTE: keep this in sync with the runtime/runtime_stm32u5.go clock init code +func (uart *UART) getBaudRateDivisor(baudRate uint32) uint32 { + return CPUFrequency() / baudRate +} + +// Register names vary by ST processor, these are for STM U5 +func (uart *UART) setRegisters() { + uart.rxReg = &uart.Bus.RDR + uart.txReg = &uart.Bus.TDR + uart.statusReg = &uart.Bus.ISR + uart.txEmptyFlag = stm32.USART_ISR_TXE +} + +//---------- SPI related types and code + +// SPI on the STM32U5 using the new SPIv2 peripheral +type SPI struct { + Bus *stm32.SPI_Type + AltFuncSelector uint8 +} + +func (spi *SPI) config8Bits() { + // U5 SPI has DSIZE field in CFG1, set to 7 for 8-bit frames (DSIZE = bits-1) + spi.Bus.CFG1.ReplaceBits(7, 0x1f, 0) // DSIZE[4:0] = 0x7 = 8 bits +} + +// Set baud rate for SPI +func (spi *SPI) getBaudRate(config SPIConfig) uint32 { + var conf uint32 + + localFrequency := config.Frequency + + // Default + if localFrequency == 0 { + localFrequency = 4e6 + } + + // Set frequency dependent on PCLK prescaler + // MBR field in CFG1 register, bits [30:28] + switch { + case localFrequency < 625000: + conf = 7 // Div256 + case localFrequency < 1250000: + conf = 6 // Div128 + case localFrequency < 2500000: + conf = 5 // Div64 + case localFrequency < 5000000: + conf = 4 // Div32 + case localFrequency < 10000000: + conf = 3 // Div16 + case localFrequency < 20000000: + conf = 2 // Div8 + case localFrequency < 40000000: + conf = 1 // Div4 + case localFrequency < 80000000: + conf = 0 // Div2 + default: + conf = 7 // Div256 (safest) + } + + return conf << 28 // MBR position in CFG1 +} + +// Configure SPI pins for input output and clock +func (spi *SPI) configurePins(config SPIConfig) { + config.SCK.ConfigureAltFunc(PinConfig{Mode: PinModeSPICLK}, spi.AltFuncSelector) + config.SDO.ConfigureAltFunc(PinConfig{Mode: PinModeSPISDO}, spi.AltFuncSelector) + config.SDI.ConfigureAltFunc(PinConfig{Mode: PinModeSPISDI}, spi.AltFuncSelector) +} + +//---------- I2C related code + +// Gets the value for TIMINGR register +func (i2c *I2C) getFreqRange(br uint32) uint32 { + // These are 'magic' values calculated by STM32CubeMX + // for 160MHz PCLK1. + // TODO: Do calculations based on PCLK1 + switch br { + case 10 * KHz: + return 0xF010F3FE + case 100 * KHz: + return 0x30A0A7FB + case 400 * KHz: + return 0x10802D9B + case 500 * KHz: + return 0x00802172 + default: + return 0 + } +} diff --git a/src/machine/machine_stm32u5_spi.go b/src/machine/machine_stm32u5_spi.go new file mode 100644 index 0000000000..2b8ca474a9 --- /dev/null +++ b/src/machine/machine_stm32u5_spi.go @@ -0,0 +1,119 @@ +//go:build stm32u5 + +package machine + +// SPI on STM32U5 uses the new SPIv2 peripheral with separate TXDR/RXDR registers. + +import ( + "unsafe" +) + +// SPIConfig is used to store config info for SPI. +type SPIConfig struct { + Frequency uint32 + SCK Pin + SDO Pin + SDI Pin + LSBFirst bool + Mode uint8 +} + +// Configure is intended to setup the STM32U5 SPI peripheral. +func (spi *SPI) Configure(config SPIConfig) error { + // Disable SPI interface before any configuration changes + spi.Bus.CR1.ClearBits(1) // Clear SPE (bit 0) + + // Enable clock for SPI + enableAltFuncClock(unsafe.Pointer(spi.Bus)) + + // Init pins - use defaults if not specified + if config.SCK == 0 && config.SDO == 0 && config.SDI == 0 { + config.SCK = SPI0_SCK_PIN + config.SDO = SPI0_SDO_PIN + config.SDI = SPI0_SDI_PIN + } + spi.configurePins(config) + + // Configure CFG1: baud rate and data size + var cfg1 uint32 + + // Set baud rate (MBR bits [30:28]) + cfg1 |= spi.getBaudRate(config) + + // Set data size to 8 bits (DSIZE[4:0] = 7 = 8-1) + cfg1 |= 7 + + spi.Bus.CFG1.Set(cfg1) + + // Configure CFG2: master mode, SS output, polarity, phase + var cfg2 uint32 + const ( + cfg2_MASTER = 1 << 22 // MASTER bit + cfg2_SSM = 1 << 26 // Software SS management + cfg2_SSOE = 1 << 29 // SS output enable + cfg2_AFCNTR = 1 << 31 // Alternate function GPIO control always active + cfg2_CPOL = 1 << 25 + cfg2_CPHA = 1 << 24 + cfg2_LSBFRST = 1 << 23 + ) + + cfg2 |= cfg2_MASTER | cfg2_SSM | cfg2_AFCNTR + + // Set polarity and phase + switch config.Mode { + case Mode1: + cfg2 |= cfg2_CPHA + case Mode2: + cfg2 |= cfg2_CPOL + case Mode3: + cfg2 |= cfg2_CPOL | cfg2_CPHA + } + + // Set bit transfer order + if config.LSBFirst { + cfg2 |= cfg2_LSBFRST + } + + spi.Bus.CFG2.Set(cfg2) + + // Enable SPI + spi.Bus.CR1.SetBits(1) // Set SPE (bit 0) + + return nil +} + +// Transfer writes/reads a single byte using the SPI interface. +func (spi *SPI) Transfer(w byte) (byte, error) { + // Set transfer size to 1 frame + spi.Bus.CR2.Set(1) + + // Start the transfer + spi.Bus.CR1.SetBits(1 << 9) // CSTART bit + + // Wait until TX FIFO is ready (TXP bit in SR) + const sr_TXP = 1 << 1 + const sr_RXP = 1 << 0 + const sr_EOT = 1 << 3 + + for !spi.Bus.SR.HasBits(sr_TXP) { + } + + // Write byte to TXDR + spi.Bus.TXDR.Set(uint32(w)) + + // Wait for RX data available (RXP bit in SR) + for !spi.Bus.SR.HasBits(sr_RXP) { + } + + // Read received byte + data := byte(spi.Bus.RXDR.Get()) + + // Wait for end of transfer + for !spi.Bus.SR.HasBits(sr_EOT) { + } + + // Clear EOT flag + spi.Bus.IFCR.Set(sr_EOT) + + return data, nil +} From 02bfdf119f211c08560c42304e5010ef3a2900ba Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 16 Mar 2026 15:40:58 +0100 Subject: [PATCH 54/81] machine/stm32: add Arduino UNO Q to smoketests Signed-off-by: deadprogram --- GNUmakefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GNUmakefile b/GNUmakefile index 9a6dc7b8e5..2c640e403b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -877,6 +877,12 @@ ifneq ($(STM32), 0) @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=stm32l0x1 examples/serial @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=arduino-uno-q examples/blinky1 + @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=arduino-uno-q examples/serial + @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=arduino-uno-q examples/blinkm + @$(MD5SUM) test.hex endif $(TINYGO) build -size short -o test.hex -target=atmega328pb examples/blinkm @$(MD5SUM) test.hex From b92f29b386e040b2dc73d4a880e59dcdc95c0ad2 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 17 Mar 2026 18:59:21 +0100 Subject: [PATCH 55/81] fix: set stm32u5x clock rate to default The complex 160MHz PLL initialization was hanging the MCU. The fix: Replaced the entire PLL-based clock init with the MSI 4MHz default Signed-off-by: deadprogram --- src/machine/machine_stm32u585.go | 6 +-- src/runtime/runtime_stm32u5.go | 80 ++------------------------------ 2 files changed, 8 insertions(+), 78 deletions(-) diff --git a/src/machine/machine_stm32u585.go b/src/machine/machine_stm32u585.go index 901bec5e83..f5e568bb30 100644 --- a/src/machine/machine_stm32u585.go +++ b/src/machine/machine_stm32u585.go @@ -7,14 +7,14 @@ import ( ) func CPUFrequency() uint32 { - return 160000000 + return 4_000_000 } // Internal use: configured speed of the APB1 and APB2 timers, this should be kept // in sync with any changes to runtime package which configures the oscillators // and clock frequencies -const APB1_TIM_FREQ = 160e6 // 160MHz -const APB2_TIM_FREQ = 160e6 // 160MHz +const APB1_TIM_FREQ = 4e6 // 4MHz (MSI default) +const APB2_TIM_FREQ = 4e6 // 4MHz (MSI default) //---------- UART related code diff --git a/src/runtime/runtime_stm32u5.go b/src/runtime/runtime_stm32u5.go index be55b5e371..cd97531d10 100644 --- a/src/runtime/runtime_stm32u5.go +++ b/src/runtime/runtime_stm32u5.go @@ -3,7 +3,6 @@ package runtime import ( - "device/stm32" "machine" ) @@ -24,78 +23,9 @@ func buffered() int { } func initCLK() { - // Initialize clock to 160MHz using PLL1 with HSI16 as source. - // PLL1 configuration: HSI16 (16MHz) / PLLM(1) * PLLN(10) / PLLR(1) = 160MHz - // VCO = 16 * 10 = 160MHz, PLLR = /1 -> 160MHz SYSCLK - - // Enable PWR clock - stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_CRSEN) // CRS enable not needed but ensures APB1 is clocked - _ = stm32.RCC.APB1ENR1.Get() - - // Set voltage scaling to Range 1 for 160MHz operation - // On U5, voltage scaling is in PWR.VOSR register - // VOS = 11 (Range 1, up to 160MHz) - stm32.PWR.VOSR.ReplaceBits(stm32.PWR_VOSR_VOS_Range1, stm32.PWR_VOSR_VOS_Msk, 0) - // Wait for VOS ready - for !stm32.PWR.VOSR.HasBits(stm32.PWR_VOSR_VOSRDY) { - } - - // Enable EPOD booster for high performance (required for Range 1 >100MHz) - stm32.PWR.VOSR.SetBits(stm32.PWR_VOSR_BOOSTEN) - for !stm32.PWR.VOSR.HasBits(stm32.PWR_VOSR_BOOSTRDY) { - } - - // Enable HSI16 - stm32.RCC.CR.SetBits(stm32.RCC_CR_HSION) - for !stm32.RCC.CR.HasBits(stm32.RCC_CR_HSIRDY) { - } - - // Disable PLL1 before configuration - stm32.RCC.CR.ClearBits(stm32.RCC_CR_PLL1ON) - for stm32.RCC.CR.HasBits(stm32.RCC_CR_PLL1RDY) { - } - - // Configure PLL1: - // Source = HSI16 (0x2) - // PLLM = 0 (divide by 1) - // PLL1RGE = 0x3 (input range 8-16MHz) - // Enable PLLR output - stm32.RCC.PLL1CFGR.Set( - (stm32.RCC_PLL1CFGR_PLL1SRC_HSI16 << stm32.RCC_PLL1CFGR_PLL1SRC_Pos) | - (stm32.RCC_PLL1CFGR_PLL1M_Div1 << stm32.RCC_PLL1CFGR_PLL1M_Pos) | - (stm32.RCC_PLL1CFGR_PLL1RGE_Range2 << stm32.RCC_PLL1CFGR_PLL1RGE_Pos)) - - // Enable PLL1R output - stm32.RCC.PLL1CFGR.SetBits(1 << stm32.RCC_PLL1CFGR_PLL1REN_Pos) - - // Set PLL1 dividers in PLL1DIVR: - // PLL1N = 10 (value - 1 = 9 in register) - // PLL1R = 1 (value - 1 = 0 in register) - // VCO = HSI16/1 * 10 = 160MHz - // PLLR output = 160MHz / 1 = 160MHz - stm32.RCC.SetPLL1DIVR_PLL1N(9) // N = 10, register value = N-1 = 9 - stm32.RCC.SetPLL1DIVR_PLL1R(0) // R = 1, register value = R-1 = 0 - - // Enable PLL1 - stm32.RCC.CR.SetBits(stm32.RCC_CR_PLL1ON) - for !stm32.RCC.CR.HasBits(stm32.RCC_CR_PLL1RDY) { - } - - // Set flash latency to 4 wait states (required for 160MHz in Range 1) - const FLASH_LATENCY_4 = 4 - stm32.FLASH.ACR.ReplaceBits(FLASH_LATENCY_4, stm32.Flash_ACR_LATENCY_Msk, 0) - for (stm32.FLASH.ACR.Get() & stm32.Flash_ACR_LATENCY_Msk) != FLASH_LATENCY_4 { - } - - // Set AHB prescaler to 1 (no division) in CFGR2 - stm32.RCC.CFGR2.ReplaceBits(stm32.RCC_CFGR2_HPRE_Div1, stm32.RCC_CFGR2_HPRE_Msk, 0) - - // Set APB1 and APB2 prescalers to 1 (no division) in CFGR2 - stm32.RCC.CFGR2.ReplaceBits(stm32.RCC_CFGR2_PPRE1_Div1, stm32.RCC_CFGR2_PPRE1_Msk, 0) - stm32.RCC.CFGR2.ReplaceBits(stm32.RCC_CFGR2_PPRE2_Div1, stm32.RCC_CFGR2_PPRE2_Msk, 0) - - // Switch system clock to PLL1 (SW = 11 = PLL) - stm32.RCC.CFGR1.ReplaceBits(stm32.RCC_CFGR1_SW_PLL, stm32.RCC_CFGR1_SW_Msk, 0) - for (stm32.RCC.CFGR1.Get() & stm32.RCC_CFGR1_SWS_Msk) != (stm32.RCC_CFGR1_SWS_PLL << stm32.RCC_CFGR1_SWS_Pos) { - } + // Use MSI at 4MHz — the reset default clock configuration. + // This matches the known-working bare-metal C configuration for + // the Arduino Uno Q (STM32U585). The MCU boots with MSI at 4MHz, + // VOS Range 4, and 0 flash wait states. No additional configuration + // is needed. } From 7565e09fadbc45638bf512d7610d878874f2cc5b Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 17 Mar 2026 20:16:03 +0100 Subject: [PATCH 56/81] targets: correct pin mapping for Arduino UNO Q STM32 MCU Signed-off-by: deadprogram machine/stm32u585: fix PWR peripheral clock was never enabled On STM32U5, PWR is on AHB3 and requires RCC.AHB3ENR.PWREN - unlike some other STM32 families where PWR is always clocked. Without this, all writes to PWR registers (including IO2SV for VDDIO2) were silently dropped, so GPIOG pins could never drive. Signed-off-by: deadprogram target: correct I2C pin mapping for Arduino UNO Q Signed-off-by: deadprogram --- src/machine/board_arduino_uno_q.go | 101 +++++++++++++++++------------ src/machine/machine_stm32u5.go | 5 +- src/machine/machine_stm32u585.go | 27 +++++++- src/runtime/runtime_stm32u5.go | 10 +-- 4 files changed, 96 insertions(+), 47 deletions(-) diff --git a/src/machine/board_arduino_uno_q.go b/src/machine/board_arduino_uno_q.go index bf26eadb5f..4d11ca2174 100644 --- a/src/machine/board_arduino_uno_q.go +++ b/src/machine/board_arduino_uno_q.go @@ -7,54 +7,64 @@ package machine import ( "device/stm32" "runtime/interrupt" + "unsafe" ) const ( // Arduino Pins - A0 = PA0 - A1 = PA1 - A2 = PA4 - A3 = PB1 - A4 = PB11 - A5 = PB12 + A0 = PA4 + A1 = PA5 + A2 = PA6 + A3 = PA7 + A4 = PC1 + A5 = PC0 D0 = PB7 D1 = PB6 - D2 = PA10 - D3 = PB3 - D4 = PB5 - D5 = PB4 - D6 = PB10 - D7 = PA8 - D8 = PA9 - D9 = PC7 - D10 = PB0 - D11 = PA7 - D12 = PA6 - D13 = PA5 - D14 = PB9 - D15 = PB8 + D2 = PB3 + D3 = PB0 + D4 = PA12 + D5 = PA11 + D6 = PB1 + D7 = PB2 + D8 = PB4 + D9 = PB8 + D10 = PB9 + D11 = PB15 + D12 = PB14 + D13 = PB13 + D18 = PC1 + D19 = PC0 + D20 = PB10 + D21 = PB11 ) const ( - LED = LED_BUILTIN - LED_BUILTIN = LED_GREEN - LED_GREEN = PA5 + LED = LED3_R + LED3_R = PH10 + LED3_G = PH11 + LED3_B = PH12 + LED4_R = PH13 + LED4_G = PH14 + LED4_B = PH15 ) const ( - BUTTON = PC13 -) + // Default UART pins (LPUART1 active via ST-LINK virtual COM port) + UART_TX_PIN = PG7 + UART_RX_PIN = PG8 -const ( - // UART pins - // PA2 and PA3 are connected to the ST-Link Virtual Com Port (VCP) - UART_TX_PIN = PA2 - UART_RX_PIN = PA3 + // USART1 pins (Arduino header D1/D0) + UART1_TX_PIN = D1 + UART1_RX_PIN = D0 + + // LPUART1 pins (active via ST-LINK virtual COM port) + UART2_TX_PIN = PG7 + UART2_RX_PIN = PG8 // I2C pins - I2C0_SCL_PIN = PB8 - I2C0_SDA_PIN = PB9 + I2C0_SCL_PIN = D20 + I2C0_SDA_PIN = D21 // SPI pins SPI1_SCK_PIN = PA5 @@ -66,23 +76,31 @@ const ( ) var ( - // USART2 is the hardware serial port connected to the onboard ST-LINK - // debugger to be exposed as virtual COM port over USB. + // USART1 on PB6/PB7 (Arduino header D1/D0). UART1 = &_UART1 _UART1 = UART{ Buffer: NewRingBuffer(), - Bus: stm32.USART2, + Bus: stm32.USART1, TxAltFuncSelector: AF7_USART1_2_3, RxAltFuncSelector: AF7_USART1_2_3, } - DefaultUART = UART1 - // I2C1 is documented, alias to I2C0 as well - I2C1 = &I2C{ - Bus: stm32.I2C1, + // LPUART1 on PG7/PG8 (active via ST-LINK virtual COM port). + UART2 = &_UART2 + _UART2 = UART{ + Buffer: NewRingBuffer(), + Bus: (*stm32.USART_Type)(unsafe.Pointer(stm32.LPUART1)), + TxAltFuncSelector: AF8_UART4_5_LPUART1_SDMMC1, + RxAltFuncSelector: AF8_UART4_5_LPUART1_SDMMC1, + } + DefaultUART = UART2 + + // I2C2 is documented, alias to I2C0 as well + I2C2 = &I2C{ + Bus: stm32.I2C2, AltFuncSelector: AF4_I2C1_2_3_4, } - I2C0 = I2C1 + I2C0 = I2C2 // SPI1 is documented, alias to SPI0 as well SPI1 = &SPI{ @@ -93,5 +111,6 @@ var ( ) func init() { - UART1.Interrupt = interrupt.New(stm32.IRQ_USART2, _UART1.handleInterrupt) + UART1.Interrupt = interrupt.New(stm32.IRQ_USART1, _UART1.handleInterrupt) + UART2.Interrupt = interrupt.New(stm32.IRQ_LPUART1, _UART2.handleInterrupt) } diff --git a/src/machine/machine_stm32u5.go b/src/machine/machine_stm32u5.go index 720e26ca2e..19efa18cec 100644 --- a/src/machine/machine_stm32u5.go +++ b/src/machine/machine_stm32u5.go @@ -236,13 +236,16 @@ func (p Pin) enableClock() { default: panic("machine: unknown port") } + // Dummy read-back to ensure the clock enable takes effect before + // accessing GPIO registers (2 AHB cycle delay per STM32 reference manual). + _ = stm32.RCC.AHB2ENR1.Get() } // Enable peripheral clock func enableAltFuncClock(bus unsafe.Pointer) { switch bus { case unsafe.Pointer(stm32.PWR): // Power interface clock enable - // PWR clock is always enabled on U5 (no dedicated enable bit needed) + stm32.RCC.AHB3ENR.SetBits(stm32.RCC_AHB3ENR_PWREN) case unsafe.Pointer(stm32.I2C1): // I2C1 clock enable stm32.RCC.APB1ENR1.SetBits(stm32.RCC_APB1ENR1_I2C1EN) case unsafe.Pointer(stm32.I2C2): // I2C2 clock enable diff --git a/src/machine/machine_stm32u585.go b/src/machine/machine_stm32u585.go index f5e568bb30..5492e14e63 100644 --- a/src/machine/machine_stm32u585.go +++ b/src/machine/machine_stm32u585.go @@ -4,6 +4,7 @@ package machine import ( "device/stm32" + "unsafe" ) func CPUFrequency() uint32 { @@ -20,8 +21,23 @@ const APB2_TIM_FREQ = 4e6 // 4MHz (MSI default) // Configure the UART. func (uart *UART) configurePins(config UARTConfig) { + if uart.isLPUART1() { + // LPUART1 is on APB3. Explicitly enable its peripheral clock. + stm32.RCC.APB3ENR.SetBits(stm32.RCC_APB3ENR_LPUART1EN) + _ = stm32.RCC.APB3ENR.Get() // delay for clock stabilization + + // Select PCLK3 as LPUART1 kernel clock source. + stm32.RCC.CCIPR3.ReplaceBits( + stm32.RCC_CCIPR3_LPUART1SEL_PCLK3< Date: Wed, 18 Mar 2026 09:15:35 +0100 Subject: [PATCH 57/81] targets: add stm32u585 openocd commands for flashing on Arduino UNO Q This adds the specific OpenOCD commands needed for flashing the onboard STM32U585 MCU directly from the Arduino UNO Q board. Thanks to https://github.com/jesdev-io/arduino-uno-q-pio-toolchain for the infor on how to do this. Signed-off-by: deadprogram --- targets/arduino-uno-q.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/targets/arduino-uno-q.json b/targets/arduino-uno-q.json index 89b0689f7f..8309d2ca6e 100644 --- a/targets/arduino-uno-q.json +++ b/targets/arduino-uno-q.json @@ -3,5 +3,6 @@ "build-tags": ["arduino_uno_q"], "serial": "uart", "openocd-interface": "stlink", - "openocd-target": "stm32u5x" + "openocd-target": "stm32u5x", + "openocd-commands": ["adapter driver linuxgpiod adapter gpio swclk -chip 1 26 adapter gpio swdio -chip 1 25 adapter gpio srst -chip 1 38 transport select swd adapter speed 1000 reset_config srst_only srst_push_pull"] } From 8ff2bea0648cddcb5d6300b32a62c7965c5331d5 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 18 Mar 2026 12:29:53 +0100 Subject: [PATCH 58/81] machine/stm32u585: implement ADC Signed-off-by: deadprogram --- src/machine/board_arduino_uno_q.go | 8 ++ src/machine/machine_stm32_adc_u5.go | 131 ++++++++++++++++++++++++++++ src/machine/machine_stm32u5.go | 4 + src/runtime/runtime_stm32u5.go | 8 ++ 4 files changed, 151 insertions(+) create mode 100644 src/machine/machine_stm32_adc_u5.go diff --git a/src/machine/board_arduino_uno_q.go b/src/machine/board_arduino_uno_q.go index 4d11ca2174..9e8996d9f4 100644 --- a/src/machine/board_arduino_uno_q.go +++ b/src/machine/board_arduino_uno_q.go @@ -19,6 +19,14 @@ const ( A4 = PC1 A5 = PC0 + // ADC pin aliases + ADC0 = A0 + ADC1 = A1 + ADC2 = A2 + ADC3 = A3 + ADC4 = A4 + ADC5 = A5 + D0 = PB7 D1 = PB6 D2 = PB3 diff --git a/src/machine/machine_stm32_adc_u5.go b/src/machine/machine_stm32_adc_u5.go new file mode 100644 index 0000000000..6620185ffb --- /dev/null +++ b/src/machine/machine_stm32_adc_u5.go @@ -0,0 +1,131 @@ +//go:build stm32u5 + +package machine + +import ( + "device/stm32" + "unsafe" +) + +// InitADC initializes the registers needed for ADC1. +func InitADC() { + // Enable ADC bus clock. + enableAltFuncClock(unsafe.Pointer(stm32.ADC1)) + + // Declare VDDA analog supply valid. Without ASV the ADC LDO cannot start. + stm32.PWR.SVMCR.SetBits(stm32.PWR_SVMCR_ASV) + + // Set ADC12_Common CCR: synchronous clock HCLK/1. + // ADC12_Common is mapped as ADC_Type; CCR at offset 0x08 = .CR field. + stm32.ADC12_Common.CR.Set(1 << 16) // CKMODE=01 + + // Exit deep power-down mode. + stm32.ADC1.CR.ClearBits(stm32.ADC_CR_DEEPPWD) + + // Enable internal voltage regulator and wait for LDO ready. + stm32.ADC1.CR.SetBits(stm32.ADC_CR_ADVREGEN) + for !stm32.ADC1.ISR.HasBits(stm32.ADC_ISR_LDORDY) { + } + + // Calibrate ADC (single-ended). + stm32.ADC1.CR.SetBits(stm32.ADC_CR_ADCAL) + for stm32.ADC1.CR.HasBits(stm32.ADC_CR_ADCAL) { + } + + // 12-bit resolution, overwrite DR on overrun. + stm32.ADC1.CFGR1.Set(stm32.ADC_CFGR1_RES_TwelveBit< Date: Sat, 21 Mar 2026 10:11:11 +0100 Subject: [PATCH 59/81] fix: correctly handle interrupt disable on esp32-c3 Priority 0 disables an interrupt on ESP32-C3. This corrects the code to actually use that. Signed-off-by: deadprogram --- builder/sizes_test.go | 2 +- src/device/riscv/handleinterrupt.S | 11 +++++++++++ src/runtime/interrupt/interrupt_esp32c3.go | 12 +++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/builder/sizes_test.go b/builder/sizes_test.go index 4b0d6301ea..fd985a8977 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -42,7 +42,7 @@ func TestBinarySize(t *testing.T) { // This is a small number of very diverse targets that we want to test. tests := []sizeTest{ // microcontrollers - {"hifive1b", "examples/echo", 3668, 280, 0, 2244}, + {"hifive1b", "examples/echo", 3680, 280, 0, 2252}, {"microbit", "examples/serial", 2694, 342, 8, 2248}, {"wioterminal", "examples/pininterrupt", 7074, 1510, 120, 7248}, diff --git a/src/device/riscv/handleinterrupt.S b/src/device/riscv/handleinterrupt.S index c206c01595..79031423fb 100644 --- a/src/device/riscv/handleinterrupt.S +++ b/src/device/riscv/handleinterrupt.S @@ -75,6 +75,10 @@ handleInterruptASM: SFREG f30,(30 + 16)*REGSIZE(sp) SFREG f31,(31 + 16)*REGSIZE(sp) #endif + // Save ra to a global so handleException can print the caller of the NULL + // function pointer. + la t0, tinygo_saved_ra + SREG ra, 0(t0) call handleInterrupt #ifdef __riscv_flen LFREG f0, (31 + 16)*REGSIZE(sp) @@ -128,3 +132,10 @@ handleInterruptASM: LREG ra, 0*REGSIZE(sp) addi sp, sp, NREG*REGSIZE mret + +.section .bss.tinygo_saved_ra +.global tinygo_saved_ra +.type tinygo_saved_ra,@object +.align 2 +tinygo_saved_ra: + .space REGSIZE diff --git a/src/runtime/interrupt/interrupt_esp32c3.go b/src/runtime/interrupt/interrupt_esp32c3.go index 27b7ae1b2c..82e3d13eac 100644 --- a/src/runtime/interrupt/interrupt_esp32c3.go +++ b/src/runtime/interrupt/interrupt_esp32c3.go @@ -10,6 +10,9 @@ import ( "unsafe" ) +//go:extern tinygo_saved_ra +var tinygo_saved_ra uintptr + // Enable register CPU interrupt with interrupt.Interrupt. // The ESP32-C3 has 31 CPU independent interrupts. // The Interrupt.New(x, f) (x = [1..31]) attaches CPU interrupt to function f. @@ -88,7 +91,13 @@ const ( const ( defaultThreshold = 5 - disableThreshold = 10 + // Priority 0 disables an interrupt on ESP32-C3 (per the TRM, + // priority 0 = masked). During handling we set the current + // interrupt's priority to 0 so it cannot re-fire while MIE is + // re-enabled for nesting. This is critical for level-triggered + // interrupts where the hardware line stays asserted until the + // peripheral source is serviced by the handler. + disableThreshold = 0 ) //go:inline @@ -225,6 +234,7 @@ func handleException(mcause uintptr) { println("*** Exception: pc:", riscv.MEPC.Get()) println("*** Exception: code:", uint32(mcause&0x1f)) println("*** Exception: mcause:", mcause) + println("*** Exception: ra:", tinygo_saved_ra) switch uint32(mcause & 0x1f) { case riscv.InstructionAccessFault: println("*** virtual address:", riscv.MTVAL.Get()) From 10fca3fc5c8de05f699baf8d59369703e72d5ac8 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 22 Mar 2026 20:34:44 +0100 Subject: [PATCH 60/81] runtime/esp32c3: use TIMG0 alarm interrupt for sleepTicks instead of busy-waiting Signed-off-by: deadprogram --- src/machine/machine_esp32c3_usb.go | 2 +- src/runtime/interrupt/interrupt_esp32c3.go | 6 ++ src/runtime/runtime_esp32.go | 8 +++ src/runtime/runtime_esp32c3.go | 74 ++++++++++++++++++++++ src/runtime/runtime_esp32xx.go | 8 --- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32c3_usb.go index 06fdca913a..6480348243 100644 --- a/src/machine/machine_esp32c3_usb.go +++ b/src/machine/machine_esp32c3_usb.go @@ -17,7 +17,7 @@ import ( // CDC-ACM serial port. The USB protocol and enumeration are handled entirely // in hardware; software only reads/writes the EP1 FIFO. -const cpuInterruptFromUSB = 8 +const cpuInterruptFromUSB = 10 // flushTimeout is the maximum number of busy-wait iterations in flush(). // Prevents hanging when no USB host is connected. diff --git a/src/runtime/interrupt/interrupt_esp32c3.go b/src/runtime/interrupt/interrupt_esp32c3.go index 82e3d13eac..86a4db4cfb 100644 --- a/src/runtime/interrupt/interrupt_esp32c3.go +++ b/src/runtime/interrupt/interrupt_esp32c3.go @@ -55,6 +55,9 @@ func (i Interrupt) Enable() error { //go:linkname callHandlers runtime/interrupt.callHandlers func callHandlers(num int) +//go:linkname signalInterrupt runtime.signalInterrupt +func signalInterrupt() + const ( IRQNUM_1 = 1 + iota IRQNUM_2 @@ -203,6 +206,9 @@ func handleInterrupt() { // Call registered interrupt handler(s) callHandler(int(interruptNumber)) + // Signal to sleepTicks that an interrupt has occurred. + signalInterrupt() + // disable CPU interrupts riscv.MSTATUS.ClearBits(riscv.MSTATUS_MIE) diff --git a/src/runtime/runtime_esp32.go b/src/runtime/runtime_esp32.go index 39219bb031..2e734c5e54 100644 --- a/src/runtime/runtime_esp32.go +++ b/src/runtime/runtime_esp32.go @@ -65,6 +65,14 @@ var _sbss [0]byte //go:extern _ebss var _ebss [0]byte +// sleepTicks busy-waits until the given number of ticks have passed. +func sleepTicks(d timeUnit) { + sleepUntil := ticks() + d + for ticks() < sleepUntil { + // TODO: suspend the CPU to not burn power here unnecessarily. + } +} + func abort() { for { device.Asm("waiti 0") diff --git a/src/runtime/runtime_esp32c3.go b/src/runtime/runtime_esp32c3.go index f85f7dec78..abae59533a 100644 --- a/src/runtime/runtime_esp32c3.go +++ b/src/runtime/runtime_esp32c3.go @@ -6,6 +6,7 @@ import ( "device/esp" "device/riscv" "machine" + "runtime/interrupt" "runtime/volatile" "unsafe" ) @@ -57,6 +58,9 @@ func main() { // Initialize main system timer used for time.Now. initTimer() + // Initialize timer alarm interrupt for the scheduler. + initTimerInterrupt() + // Initialize the heap, call main.main, etc. run() @@ -98,5 +102,75 @@ func interruptInit() { riscv.EnableInterrupts(mie) } +// CPU interrupt number used for the TIMG0 timer alarm. +const timerAlarmCPUInterrupt = 9 + +var interruptPending volatile.Register8 + +func signalInterrupt() { + interruptPending.Set(1) +} + +// initTimerInterrupt routes the TIMG0 timer 0 alarm interrupt to a CPU +// interrupt and registers a handler that signals timerWakeup. +func initTimerInterrupt() { + // Map the TIMG0 T0 peripheral interrupt to a CPU interrupt line. + esp.INTERRUPT_CORE0.TG_T0_INT_MAP.Set(timerAlarmCPUInterrupt) + + // Enable T0 interrupt at the timer group level. + esp.TIMG0.INT_ENA_TIMERS.SetBits(1) + + // Register the interrupt handler (compile-time wiring). + interrupt.New(timerAlarmCPUInterrupt, func(interrupt.Interrupt) { + // Clear the timer interrupt at the peripheral level. + esp.TIMG0.INT_CLR_TIMERS.Set(1) + }) + + // Manually enable the CPU interrupt with correct ordering: + // 1) clear any stale pending bit first + // 2) set edge-triggered + // 3) set priority above threshold + // 4) enable the interrupt last + mie := riscv.DisableInterrupts() + + esp.INTERRUPT_CORE0.CPU_INT_CLEAR.SetBits(1 << timerAlarmCPUInterrupt) + esp.INTERRUPT_CORE0.CPU_INT_CLEAR.ClearBits(1 << timerAlarmCPUInterrupt) + + esp.INTERRUPT_CORE0.CPU_INT_TYPE.SetBits(1 << timerAlarmCPUInterrupt) + + priReg := (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.INTERRUPT_CORE0.CPU_INT_PRI_0), timerAlarmCPUInterrupt*4)) + priReg.Set(10) + + riscv.Asm("fence") + + esp.INTERRUPT_CORE0.CPU_INT_ENABLE.SetBits(1 << timerAlarmCPUInterrupt) + + riscv.EnableInterrupts(mie) +} + +// sleepTicks spins until the given number of ticks have elapsed, using the +// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration. +func sleepTicks(d timeUnit) { + target := ticks() + d + for ticks() < target { + // Set the alarm to fire at the target tick count (or as close + // as the 54-bit counter allows). + interruptPending.Set(0) + + esp.TIMG0.T0ALARMLO.Set(uint32(target)) + esp.TIMG0.T0ALARMHI.Set(uint32(target >> 32)) + + // Enable the alarm (auto-clears when alarm fires). + esp.TIMG0.T0CONFIG.SetBits(esp.TIMG_T0CONFIG_ALARM_EN) + + // Wait for any interrupt (timer alarm or other) or a timeout. + for interruptPending.Get() == 0 { + if ticks() >= target { + return + } + } + } +} + //go:extern _vector_table var _vector_table [0]uintptr diff --git a/src/runtime/runtime_esp32xx.go b/src/runtime/runtime_esp32xx.go index f1c62243f1..cde65b8cb9 100644 --- a/src/runtime/runtime_esp32xx.go +++ b/src/runtime/runtime_esp32xx.go @@ -55,14 +55,6 @@ func ticksToNanoseconds(ticks timeUnit) int64 { return int64(ticks) * 25 } -// sleepTicks busy-waits until the given number of ticks have passed. -func sleepTicks(d timeUnit) { - sleepUntil := ticks() + d - for ticks() < sleepUntil { - // TODO: suspend the CPU to not burn power here unnecessarily. - } -} - func exit(code int) { abort() } From 25423a61138677fc36f3662b805ea5566d0a97d3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 21 Mar 2026 10:29:22 +0100 Subject: [PATCH 61/81] targets: change default stack size for esp32c3 and esp32s3 to 8196. This changes the default stack size for the esp32-c3 and esp32-s3 to 8kb. Since they have more onboard memory and a larger stack size is needed for any networking etc this default seems like a more sensible one, in similar way to the rp2040/rp2350 defaults. Signed-off-by: deadprogram --- targets/esp32c3.json | 1 + targets/esp32s3.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/targets/esp32c3.json b/targets/esp32c3.json index 96af15d515..ea46d07882 100644 --- a/targets/esp32c3.json +++ b/targets/esp32c3.json @@ -5,6 +5,7 @@ "serial": "usb", "rtlib": "compiler-rt", "libc": "picolibc", + "default-stack-size": 8192, "cflags": [ "-march=rv32imc" ], diff --git a/targets/esp32s3.json b/targets/esp32s3.json index 1b9159230a..e47c6784d2 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -6,7 +6,7 @@ "scheduler": "tasks", "serial": "usb", "linker": "ld.lld", - "default-stack-size": 2048, + "default-stack-size": 8192, "rtlib": "compiler-rt", "libc": "picolibc", "linkerscript": "targets/esp32s3.ld", From 97d4eece99cf07249e497fface5647a2c58b2ebf Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 18 Mar 2026 21:32:47 +0100 Subject: [PATCH 62/81] machine/stm32: fix PWM problem due to register shifting. The SVD-generated TIM constants in the STM32 device files have all per-channel fields shifted by one channel position (e.g., CC1E is at the hardware position of CC2E, OC1M_Pos is at the OC2M hardware position). This caused Set(), Unset(), SetInverting(), and interrupt handlers to write to wrong bit positions. Signed-off-by: deadprogram --- src/machine/machine_stm32_tim.go | 47 ++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/machine/machine_stm32_tim.go b/src/machine/machine_stm32_tim.go index da11b44e1f..ddb975a814 100644 --- a/src/machine/machine_stm32_tim.go +++ b/src/machine/machine_stm32_tim.go @@ -13,6 +13,23 @@ import ( const PWM_MODE1 = 0x6 +// Hardware bit positions for TIM registers. The SVD-generated constants +// (stm32.TIM_CCMR1_Output_OC1M_Pos, stm32.TIM_CCER_CC1E, etc.) are +// systematically shifted by one channel in some STM32 device files, +// so we define the correct hardware positions here. +const ( + // OC1M[2:0] field position within each CCMR half (bits [6:4]). + tim_CCMR_OCxM_Pos = 4 + + // Per-channel bit positions within CCER (4 bits per channel). + tim_CCER_CCxE = 0x1 // CCxE at bit 0 of channel group + tim_CCER_CCxP = 0x2 // CCxP at bit 1 of channel group + + // Per-channel bit positions within SR, EGR, and DIER (1 bit per channel, + // starting at bit 1 for CC1). Use as: tim_CC1_bit << channel. + tim_CC1_bit = 0x2 // CC1G/CC1IF/CC1IE at bit 1 +) + type TimerCallback func() type ChannelCallback func(channel uint8) @@ -99,14 +116,14 @@ func (t *TIM) SetMatchInterrupt(channel uint8, callback ChannelCallback) error { t.OCInterrupt = t.registerOCInterrupt() // Clear the interrupt flag - t.Device.SR.ClearBits(stm32.TIM_SR_CC1IF << channel) + t.Device.SR.ClearBits(tim_CC1_bit << channel) // Enable the interrupt t.OCInterrupt.SetPriority(0xc1) t.OCInterrupt.Enable() // Enable the hardware interrupt - t.Device.DIER.SetBits(stm32.TIM_DIER_CC1IE << channel) + t.Device.DIER.SetBits(tim_CC1_bit << channel) return nil } @@ -210,20 +227,20 @@ func (t *TIM) Set(channel uint8, value uint32) { // Set the PWM to Mode 1 (active below set value, inactive above) // Preload is disabled so we can change OC value within one update period. var ccmrVal uint32 - ccmrVal |= PWM_MODE1 << stm32.TIM_CCMR1_Output_OC1M_Pos + ccmrVal |= PWM_MODE1 << tim_CCMR_OCxM_Pos ccmr.ReplaceBits(ccmrVal, 0xFF, offset) // Set the compare value ccr.Set(arrtype(value)) // Enable the channel (if not already) - t.Device.CCER.ReplaceBits(stm32.TIM_CCER_CC1E, 0xD, channel*4) + t.Device.CCER.ReplaceBits(tim_CCER_CCxE, 0xD, channel*4) // Force update - t.Device.EGR.SetBits(stm32.TIM_EGR_CC1G << channel) + t.Device.EGR.SetBits(tim_CC1_bit << channel) // Reset Interrupt Flag - t.Device.SR.ClearBits(stm32.TIM_SR_CC1IF << channel) + t.Device.SR.ClearBits(tim_CC1_bit << channel) // Restore interrupts interrupt.Restore(mask) @@ -242,10 +259,10 @@ func (t *TIM) Unset(channel uint8) { ccr.Set(0) // Disable the hardware interrupt - t.Device.DIER.ClearBits(stm32.TIM_DIER_CC1IE << channel) + t.Device.DIER.ClearBits(tim_CC1_bit << channel) // Clear the interrupt flag - t.Device.SR.ClearBits(stm32.TIM_SR_CC1IF << channel) + t.Device.SR.ClearBits(tim_CC1_bit << channel) // Restore interrupts interrupt.Restore(mask) @@ -261,10 +278,10 @@ func (t *TIM) SetInverting(channel uint8, inverting bool) { var val = uint32(0) if inverting { - val |= stm32.TIM_CCER_CC1P + val |= tim_CCER_CCxP } - t.Device.CCER.ReplaceBits(val, stm32.TIM_CCER_CC1P_Msk, channel*4) + t.Device.CCER.ReplaceBits(val, tim_CCER_CCxP, channel*4) } func (t *TIM) handleUPInterrupt(interrupt.Interrupt) { @@ -279,29 +296,29 @@ func (t *TIM) handleUPInterrupt(interrupt.Interrupt) { } func (t *TIM) handleOCInterrupt(interrupt.Interrupt) { - if t.Device.SR.HasBits(stm32.TIM_SR_CC1IF) { + if t.Device.SR.HasBits(tim_CC1_bit << 0) { if t.channelCallbacks[0] != nil { t.channelCallbacks[0](0) } } - if t.Device.SR.HasBits(stm32.TIM_SR_CC2IF) { + if t.Device.SR.HasBits(tim_CC1_bit << 1) { if t.channelCallbacks[1] != nil { t.channelCallbacks[1](1) } } - if t.Device.SR.HasBits(stm32.TIM_SR_CC3IF) { + if t.Device.SR.HasBits(tim_CC1_bit << 2) { if t.channelCallbacks[2] != nil { t.channelCallbacks[2](2) } } - if t.Device.SR.HasBits(stm32.TIM_SR_CC4IF) { + if t.Device.SR.HasBits(tim_CC1_bit << 3) { if t.channelCallbacks[3] != nil { t.channelCallbacks[3](3) } } // Reset interrupt flags - t.Device.SR.ClearBits(stm32.TIM_SR_CC1IF | stm32.TIM_SR_CC2IF | stm32.TIM_SR_CC3IF | stm32.TIM_SR_CC4IF) + t.Device.SR.ClearBits(tim_CC1_bit<<0 | tim_CC1_bit<<1 | tim_CC1_bit<<2 | tim_CC1_bit<<3) } func (t *TIM) channelCCR(channel uint8) *arrRegType { From c97ad03abc71abdb50c2fc79036a85f4e0b1c33e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 18 Mar 2026 21:35:07 +0100 Subject: [PATCH 63/81] target: add PWM constant values for Arduino UNO Q Signed-off-by: deadprogram --- src/examples/pwm/arduino-uno-q.go | 11 +++++++++++ src/machine/board_arduino_uno_q.go | 26 +++++++++++++------------- 2 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 src/examples/pwm/arduino-uno-q.go diff --git a/src/examples/pwm/arduino-uno-q.go b/src/examples/pwm/arduino-uno-q.go new file mode 100644 index 0000000000..e0c2c53a52 --- /dev/null +++ b/src/examples/pwm/arduino-uno-q.go @@ -0,0 +1,11 @@ +//go:build arduino_uno_q + +package main + +import "machine" + +var ( + pwm = &machine.TIM3 + pinA = machine.D3 // PB0 = TIM3_CH3 + pinB = machine.D6 // PB1 = TIM3_CH4 +) diff --git a/src/machine/board_arduino_uno_q.go b/src/machine/board_arduino_uno_q.go index 9e8996d9f4..9c2296bc89 100644 --- a/src/machine/board_arduino_uno_q.go +++ b/src/machine/board_arduino_uno_q.go @@ -27,24 +27,24 @@ const ( ADC4 = A4 ADC5 = A5 - D0 = PB7 - D1 = PB6 - D2 = PB3 - D3 = PB0 + D0 = PB7 // USART1 RX, PWM TIM4_CH2 + D1 = PB6 // USART1 TX, PWM TIM4_CH1 + D2 = PB3 // PWM TIM2_CH2 + D3 = PB0 // PWM TIM3_CH3 D4 = PA12 - D5 = PA11 - D6 = PB1 + D5 = PA11 // PWM TIM1_CH4 + D6 = PB1 // PWM TIM3_CH4 D7 = PB2 - D8 = PB4 - D9 = PB8 - D10 = PB9 - D11 = PB15 - D12 = PB14 + D8 = PB4 // PWM TIM3_CH1 + D9 = PB8 // PWM TIM4_CH3 / TIM16_CH1 + D10 = PB9 // PWM TIM4_CH4 / TIM17_CH1 + D11 = PB15 // PWM TIM15_CH2 + D12 = PB14 // PWM TIM15_CH1 D13 = PB13 D18 = PC1 D19 = PC0 - D20 = PB10 - D21 = PB11 + D20 = PB10 // I2C2 SCL, PWM TIM2_CH3 + D21 = PB11 // I2C2 SDA, PWM TIM2_CH4 ) const ( From e665b3214eaa9461bd5fbacb5b897f678aa829e3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 24 Mar 2026 10:37:36 +0100 Subject: [PATCH 64/81] targets/esp32c3: fix to use PROVIDE() for WiFi/BLE ROM function addresses to allow blob overrides Signed-off-by: deadprogram --- targets/esp32c3.ld | 584 ++++++++++++++++++++++----------------------- 1 file changed, 292 insertions(+), 292 deletions(-) diff --git a/targets/esp32c3.ld b/targets/esp32c3.ld index 18bde26044..4bdc72c83f 100644 --- a/targets/esp32c3.ld +++ b/targets/esp32c3.ld @@ -1811,154 +1811,154 @@ r_sch_plan_offset_req_hook = 0x40001ce4; ***************************************/ /* Functions */ -esp_pp_rom_version_get = 0x400015b0; -RC_GetBlockAckTime = 0x400015b4; -ebuf_list_remove = 0x400015b8; +PROVIDE(esp_pp_rom_version_get = 0x400015b0); +PROVIDE(RC_GetBlockAckTime = 0x400015b4); +PROVIDE(ebuf_list_remove = 0x400015b8); /*esf_buf_alloc = 0x400015bc;*/ -GetAccess = 0x400015c8; -hal_mac_is_low_rate_enabled = 0x400015cc; -hal_mac_tx_get_blockack = 0x400015d0; +PROVIDE(GetAccess = 0x400015c8); +PROVIDE(hal_mac_is_low_rate_enabled = 0x400015cc); +PROVIDE(hal_mac_tx_get_blockack = 0x400015d0); /* hal_mac_tx_set_ppdu = 0x400015d4;*/ -ic_get_trc = 0x400015d8; +PROVIDE(ic_get_trc = 0x400015d8); /* ic_mac_deinit = 0x400015dc; */ -ic_mac_init = 0x400015e0; -ic_interface_enabled = 0x400015e4; -is_lmac_idle = 0x400015e8; +PROVIDE(ic_mac_init = 0x400015e0); +PROVIDE(ic_interface_enabled = 0x400015e4); +PROVIDE(is_lmac_idle = 0x400015e8); /*lmacAdjustTimestamp = 0x400015ec;*/ -lmacDiscardAgedMSDU = 0x400015f0; +PROVIDE(lmacDiscardAgedMSDU = 0x400015f0); /*lmacDiscardMSDU = 0x400015f4;*/ -lmacEndFrameExchangeSequence = 0x400015f8; -lmacIsIdle = 0x400015fc; -lmacIsLongFrame = 0x40001600; +PROVIDE(lmacEndFrameExchangeSequence = 0x400015f8); +PROVIDE(lmacIsIdle = 0x400015fc); +PROVIDE(lmacIsLongFrame = 0x40001600); /*lmacMSDUAged = 0x40001604;*/ -lmacPostTxComplete = 0x40001608; -lmacProcessAllTxTimeout = 0x4000160c; -lmacProcessCollisions = 0x40001610; -lmacProcessRxSucData = 0x40001614; -lmacReachLongLimit = 0x40001618; -lmacReachShortLimit = 0x4000161c; -lmacRecycleMPDU = 0x40001620; -lmacRxDone = 0x40001624; +PROVIDE(lmacPostTxComplete = 0x40001608); +PROVIDE(lmacProcessAllTxTimeout = 0x4000160c); +PROVIDE(lmacProcessCollisions = 0x40001610); +PROVIDE(lmacProcessRxSucData = 0x40001614); +PROVIDE(lmacReachLongLimit = 0x40001618); +PROVIDE(lmacReachShortLimit = 0x4000161c); +PROVIDE(lmacRecycleMPDU = 0x40001620); +PROVIDE(lmacRxDone = 0x40001624); /*lmacSetTxFrame = 0x40001628;*/ /*lmacTxFrame = 0x40001630;*/ -mac_tx_set_duration = 0x40001634; +PROVIDE(mac_tx_set_duration = 0x40001634); /* mac_tx_set_htsig = 0x40001638;*/ -mac_tx_set_plcp0 = 0x4000163c; +PROVIDE(mac_tx_set_plcp0 = 0x4000163c); /* mac_tx_set_plcp1 = 0x40001640;*/ -mac_tx_set_plcp2 = 0x40001644; +PROVIDE(mac_tx_set_plcp2 = 0x40001644); /* pm_check_state = 0x40001648; */ -pm_disable_dream_timer = 0x4000164c; -pm_disable_sleep_delay_timer = 0x40001650; -pm_dream = 0x40001654; -pm_mac_wakeup = 0x40001658; -pm_mac_sleep = 0x4000165c; -pm_enable_active_timer = 0x40001660; -pm_enable_sleep_delay_timer = 0x40001664; -pm_local_tsf_process = 0x40001668; -pm_set_beacon_filter = 0x4000166c; -pm_is_in_wifi_slice_threshold = 0x40001670; -pm_is_waked = 0x40001674; -pm_keep_alive = 0x40001678; +PROVIDE(pm_disable_dream_timer = 0x4000164c); +PROVIDE(pm_disable_sleep_delay_timer = 0x40001650); +PROVIDE(pm_dream = 0x40001654); +PROVIDE(pm_mac_wakeup = 0x40001658); +PROVIDE(pm_mac_sleep = 0x4000165c); +PROVIDE(pm_enable_active_timer = 0x40001660); +PROVIDE(pm_enable_sleep_delay_timer = 0x40001664); +PROVIDE(pm_local_tsf_process = 0x40001668); +PROVIDE(pm_set_beacon_filter = 0x4000166c); +PROVIDE(pm_is_in_wifi_slice_threshold = 0x40001670); +PROVIDE(pm_is_waked = 0x40001674); +PROVIDE(pm_keep_alive = 0x40001678); /* pm_on_beacon_rx = 0x4000167c; */ -pm_on_data_rx = 0x40001680; -pm_on_tbtt = 0x40001684; +PROVIDE(pm_on_data_rx = 0x40001680); +PROVIDE(pm_on_tbtt = 0x40001684); /* pm_parse_beacon = 0x40001688;*/ /* pm_process_tim = 0x4000168c; */ /*pm_rx_beacon_process = 0x40001690;*/ /* pm_rx_data_process = 0x40001694; */ /*pm_sleep = 0x40001698;*/ -pm_sleep_for = 0x4000169c; +PROVIDE(pm_sleep_for = 0x4000169c); /* pm_tbtt_process = 0x400016a0; */ -ppAMPDU2Normal = 0x400016a4; +PROVIDE(ppAMPDU2Normal = 0x400016a4); /*ppAssembleAMPDU = 0x400016a8;*/ -ppCalFrameTimes = 0x400016ac; -ppCalSubFrameLength = 0x400016b0; +PROVIDE(ppCalFrameTimes = 0x400016ac); +PROVIDE(ppCalSubFrameLength = 0x400016b0); /*ppCalTxAMPDULength = 0x400016b4;*/ -ppCheckTxAMPDUlength = 0x400016b8; -ppDequeueRxq_Locked = 0x400016bc; -ppDequeueTxQ = 0x400016c0; -ppEmptyDelimiterLength = 0x400016c4; -ppEnqueueRxq = 0x400016c8; -ppEnqueueTxDone = 0x400016cc; -ppGetTxQFirstAvail_Locked = 0x400016d0; -ppGetTxframe = 0x400016d4; -ppProcessRxPktHdr = 0x400016e0; -ppProcessTxQ = 0x400016e4; -ppRecordBarRRC = 0x400016e8; -lmacRequestTxopQueue = 0x400016ec; -lmacReleaseTxopQueue = 0x400016f0; -ppRecycleAmpdu = 0x400016f4; -ppRecycleRxPkt = 0x400016f8; -ppResortTxAMPDU = 0x400016fc; -ppResumeTxAMPDU = 0x40001700; +PROVIDE(ppCheckTxAMPDUlength = 0x400016b8); +PROVIDE(ppDequeueRxq_Locked = 0x400016bc); +PROVIDE(ppDequeueTxQ = 0x400016c0); +PROVIDE(ppEmptyDelimiterLength = 0x400016c4); +PROVIDE(ppEnqueueRxq = 0x400016c8); +PROVIDE(ppEnqueueTxDone = 0x400016cc); +PROVIDE(ppGetTxQFirstAvail_Locked = 0x400016d0); +PROVIDE(ppGetTxframe = 0x400016d4); +PROVIDE(ppProcessRxPktHdr = 0x400016e0); +PROVIDE(ppProcessTxQ = 0x400016e4); +PROVIDE(ppRecordBarRRC = 0x400016e8); +PROVIDE(lmacRequestTxopQueue = 0x400016ec); +PROVIDE(lmacReleaseTxopQueue = 0x400016f0); +PROVIDE(ppRecycleAmpdu = 0x400016f4); +PROVIDE(ppRecycleRxPkt = 0x400016f8); +PROVIDE(ppResortTxAMPDU = 0x400016fc); +PROVIDE(ppResumeTxAMPDU = 0x40001700); /* ppRxFragmentProc = 0x40001704; */ /* ppRxPkt = 0x40001708; */ -ppRxProtoProc = 0x4000170c; -ppSearchTxQueue = 0x40001710; -ppSearchTxframe = 0x40001714; -ppSelectNextQueue = 0x40001718; -ppSubFromAMPDU = 0x4000171c; -ppTask = 0x40001720; -ppTxPkt = 0x40001724; -ppTxProtoProc = 0x40001728; -ppTxqUpdateBitmap = 0x4000172c; -pp_coex_tx_request = 0x40001730; -pp_hdrsize = 0x40001734; -pp_post = 0x40001738; -pp_process_hmac_waiting_txq = 0x4000173c; -rcGetAmpduSched = 0x40001740; -rcUpdateRxDone = 0x40001744; -rc_get_trc = 0x40001748; -rc_get_trc_by_index = 0x4000174c; -rcAmpduLowerRate = 0x40001750; -rcampduuprate = 0x40001754; -rcClearCurAMPDUSched = 0x40001758; -rcClearCurSched = 0x4000175c; -rcClearCurStat = 0x40001760; -rcLowerSched = 0x40001768; -rcSetTxAmpduLimit = 0x4000176c; +PROVIDE(ppRxProtoProc = 0x4000170c); +PROVIDE(ppSearchTxQueue = 0x40001710); +PROVIDE(ppSearchTxframe = 0x40001714); +PROVIDE(ppSelectNextQueue = 0x40001718); +PROVIDE(ppSubFromAMPDU = 0x4000171c); +PROVIDE(ppTask = 0x40001720); +PROVIDE(ppTxPkt = 0x40001724); +PROVIDE(ppTxProtoProc = 0x40001728); +PROVIDE(ppTxqUpdateBitmap = 0x4000172c); +PROVIDE(pp_coex_tx_request = 0x40001730); +PROVIDE(pp_hdrsize = 0x40001734); +PROVIDE(pp_post = 0x40001738); +PROVIDE(pp_process_hmac_waiting_txq = 0x4000173c); +PROVIDE(rcGetAmpduSched = 0x40001740); +PROVIDE(rcUpdateRxDone = 0x40001744); +PROVIDE(rc_get_trc = 0x40001748); +PROVIDE(rc_get_trc_by_index = 0x4000174c); +PROVIDE(rcAmpduLowerRate = 0x40001750); +PROVIDE(rcampduuprate = 0x40001754); +PROVIDE(rcClearCurAMPDUSched = 0x40001758); +PROVIDE(rcClearCurSched = 0x4000175c); +PROVIDE(rcClearCurStat = 0x40001760); +PROVIDE(rcLowerSched = 0x40001768); +PROVIDE(rcSetTxAmpduLimit = 0x4000176c); /* rcTxUpdatePer = 0x40001770;*/ -rcUpdateAckSnr = 0x40001774; +PROVIDE(rcUpdateAckSnr = 0x40001774); /*rcUpdateRate = 0x40001778;*/ /* rcUpdateTxDone = 0x4000177c; */ -rcUpdateTxDoneAmpdu2 = 0x40001780; -rcUpSched = 0x40001784; -rssi_margin = 0x40001788; -rx11NRate2AMPDULimit = 0x4000178c; -TRC_AMPDU_PER_DOWN_THRESHOLD = 0x40001790; -TRC_AMPDU_PER_UP_THRESHOLD = 0x40001794; -trc_calc_duration = 0x40001798; -trc_isTxAmpduOperational = 0x4000179c; -trc_onAmpduOp = 0x400017a0; -TRC_PER_IS_GOOD = 0x400017a4; -trc_SetTxAmpduState = 0x400017a8; -trc_tid_isTxAmpduOperational = 0x400017ac; -trcAmpduSetState = 0x400017b0; -wDev_AppendRxBlocks = 0x400017b8; -wDev_DiscardFrame = 0x400017bc; -wDev_GetNoiseFloor = 0x400017c0; -wDev_IndicateAmpdu = 0x400017c4; +PROVIDE(rcUpdateTxDoneAmpdu2 = 0x40001780); +PROVIDE(rcUpSched = 0x40001784); +PROVIDE(rssi_margin = 0x40001788); +PROVIDE(rx11NRate2AMPDULimit = 0x4000178c); +PROVIDE(TRC_AMPDU_PER_DOWN_THRESHOLD = 0x40001790); +PROVIDE(TRC_AMPDU_PER_UP_THRESHOLD = 0x40001794); +PROVIDE(trc_calc_duration = 0x40001798); +PROVIDE(trc_isTxAmpduOperational = 0x4000179c); +PROVIDE(trc_onAmpduOp = 0x400017a0); +PROVIDE(TRC_PER_IS_GOOD = 0x400017a4); +PROVIDE(trc_SetTxAmpduState = 0x400017a8); +PROVIDE(trc_tid_isTxAmpduOperational = 0x400017ac); +PROVIDE(trcAmpduSetState = 0x400017b0); +PROVIDE(wDev_AppendRxBlocks = 0x400017b8); +PROVIDE(wDev_DiscardFrame = 0x400017bc); +PROVIDE(wDev_GetNoiseFloor = 0x400017c0); +PROVIDE(wDev_IndicateAmpdu = 0x400017c4); /*wDev_IndicateFrame = 0x400017c8;*/ -wdev_bank_store = 0x400017cc; -wdev_bank_load = 0x400017d0; -wdev_mac_reg_load = 0x400017d4; -wdev_mac_reg_store = 0x400017d8; -wdev_mac_special_reg_load = 0x400017dc; -wdev_mac_special_reg_store = 0x400017e0; -wdev_mac_wakeup = 0x400017e4; -wdev_mac_sleep = 0x400017e8; -hal_mac_is_dma_enable = 0x400017ec; +PROVIDE(wdev_bank_store = 0x400017cc); +PROVIDE(wdev_bank_load = 0x400017d0); +PROVIDE(wdev_mac_reg_load = 0x400017d4); +PROVIDE(wdev_mac_reg_store = 0x400017d8); +PROVIDE(wdev_mac_special_reg_load = 0x400017dc); +PROVIDE(wdev_mac_special_reg_store = 0x400017e0); +PROVIDE(wdev_mac_wakeup = 0x400017e4); +PROVIDE(wdev_mac_sleep = 0x400017e8); +PROVIDE(hal_mac_is_dma_enable = 0x400017ec); /*wDev_ProcessFiq = 0x400017f0;*/ /*wDev_ProcessRxSucData = 0x400017f4;*/ -wdevProcessRxSucDataAll = 0x400017f8; -wdev_csi_len_align = 0x400017fc; -ppDequeueTxDone_Locked = 0x40001800; +PROVIDE(wdevProcessRxSucDataAll = 0x400017f8); +PROVIDE(wdev_csi_len_align = 0x400017fc); +PROVIDE(ppDequeueTxDone_Locked = 0x40001800); /*pm_tx_data_done_process = 0x40001808;*/ -config_is_cache_tx_buf_enabled = 0x4000180c; +PROVIDE(config_is_cache_tx_buf_enabled = 0x4000180c); //ppMapWaitTxq = 0x40001810; -ppProcessWaitingQueue = 0x40001814; -ppDisableQueue = 0x40001818; -pm_allow_tx = 0x4000181c; +PROVIDE(ppProcessWaitingQueue = 0x40001814); +PROVIDE(ppDisableQueue = 0x40001818); +PROVIDE(pm_allow_tx = 0x4000181c); /* Data (.data, .bss, .rodata) */ our_instances_ptr = 0x3ff1ee44; pTxRx = 0x3fcdf968; @@ -2015,41 +2015,41 @@ s_encap_amsdu_func = 0x3fcdf870; ***************************************/ /* Functions */ -esp_net80211_rom_version_get = 0x40001820; -ampdu_dispatch = 0x40001824; -ampdu_dispatch_all = 0x40001828; -ampdu_dispatch_as_many_as_possible = 0x4000182c; -ampdu_dispatch_movement = 0x40001830; -ampdu_dispatch_upto = 0x40001834; -chm_is_at_home_channel = 0x40001838; -cnx_node_is_existing = 0x4000183c; -cnx_node_search = 0x40001840; -ic_ebuf_recycle_rx = 0x40001844; -ic_ebuf_recycle_tx = 0x40001848; -ic_reset_rx_ba = 0x4000184c; -ieee80211_align_eb = 0x40001850; -ieee80211_ampdu_reorder = 0x40001854; -ieee80211_ampdu_start_age_timer = 0x40001858; +PROVIDE(esp_net80211_rom_version_get = 0x40001820); +PROVIDE(ampdu_dispatch = 0x40001824); +PROVIDE(ampdu_dispatch_all = 0x40001828); +PROVIDE(ampdu_dispatch_as_many_as_possible = 0x4000182c); +PROVIDE(ampdu_dispatch_movement = 0x40001830); +PROVIDE(ampdu_dispatch_upto = 0x40001834); +PROVIDE(chm_is_at_home_channel = 0x40001838); +PROVIDE(cnx_node_is_existing = 0x4000183c); +PROVIDE(cnx_node_search = 0x40001840); +PROVIDE(ic_ebuf_recycle_rx = 0x40001844); +PROVIDE(ic_ebuf_recycle_tx = 0x40001848); +PROVIDE(ic_reset_rx_ba = 0x4000184c); +PROVIDE(ieee80211_align_eb = 0x40001850); +PROVIDE(ieee80211_ampdu_reorder = 0x40001854); +PROVIDE(ieee80211_ampdu_start_age_timer = 0x40001858); /*ieee80211_encap_esfbuf = 0x4000185c;*/ -ieee80211_is_tx_allowed = 0x40001860; -ieee80211_output_pending_eb = 0x40001864; +PROVIDE(ieee80211_is_tx_allowed = 0x40001860); +PROVIDE(ieee80211_output_pending_eb = 0x40001864); /*ieee80211_output_process = 0x40001868;*/ -ieee80211_set_tx_desc = 0x4000186c; -rom_sta_input = 0x40001870; -wifi_get_macaddr = 0x40001874; -wifi_rf_phy_disable = 0x40001878; -wifi_rf_phy_enable = 0x4000187c; -ic_ebuf_alloc = 0x40001880; -ieee80211_classify = 0x40001884; -ieee80211_copy_eb_header = 0x40001888; -ieee80211_recycle_cache_eb = 0x4000188c; -ieee80211_search_node = 0x40001890; -roundup2 = 0x40001894; -ieee80211_crypto_encap = 0x40001898; +PROVIDE(ieee80211_set_tx_desc = 0x4000186c); +PROVIDE(rom_sta_input = 0x40001870); +PROVIDE(wifi_get_macaddr = 0x40001874); +PROVIDE(wifi_rf_phy_disable = 0x40001878); +PROVIDE(wifi_rf_phy_enable = 0x4000187c); +PROVIDE(ic_ebuf_alloc = 0x40001880); +PROVIDE(ieee80211_classify = 0x40001884); +PROVIDE(ieee80211_copy_eb_header = 0x40001888); +PROVIDE(ieee80211_recycle_cache_eb = 0x4000188c); +PROVIDE(ieee80211_search_node = 0x40001890); +PROVIDE(roundup2 = 0x40001894); +PROVIDE(ieee80211_crypto_encap = 0x40001898); /* ieee80211_crypto_decap = 0x4000189c; */ /* ieee80211_decap = 0x400018a0; */ -ieee80211_set_tx_pti = 0x400018a4; -wifi_is_started = 0x400018a8; +PROVIDE(ieee80211_set_tx_pti = 0x400018a4); +PROVIDE(wifi_is_started = 0x400018a8); /* Data (.data, .bss, .rodata) */ net80211_funcs = 0x3fcdf86c; g_scan = 0x3fcdf868; @@ -2067,26 +2067,26 @@ sta_rxcb = 0x3fcdf84c; ***************************************/ /* Functions */ -esp_coex_rom_version_get = 0x400018ac; -coex_bt_release = 0x400018b0; -coex_bt_request = 0x400018b4; -coex_core_ble_conn_dyn_prio_get = 0x400018b8; -coex_core_event_duration_get = 0x400018bc; -coex_core_pti_get = 0x400018c0; -coex_core_release = 0x400018c4; -coex_core_request = 0x400018c8; -coex_core_status_get = 0x400018cc; +PROVIDE(esp_coex_rom_version_get = 0x400018ac); +PROVIDE(coex_bt_release = 0x400018b0); +PROVIDE(coex_bt_request = 0x400018b4); +PROVIDE(coex_core_ble_conn_dyn_prio_get = 0x400018b8); +PROVIDE(coex_core_event_duration_get = 0x400018bc); +PROVIDE(coex_core_pti_get = 0x400018c0); +PROVIDE(coex_core_release = 0x400018c4); +PROVIDE(coex_core_request = 0x400018c8); +PROVIDE(coex_core_status_get = 0x400018cc); /*coex_core_timer_idx_get = 0x400018d0;*/ -coex_event_duration_get = 0x400018d4; -coex_hw_timer_disable = 0x400018d8; -coex_hw_timer_enable = 0x400018dc; -coex_hw_timer_set = 0x400018e0; -coex_schm_interval_set = 0x400018e4; -coex_schm_lock = 0x400018e8; -coex_schm_unlock = 0x400018ec; -coex_status_get = 0x400018f0; -coex_wifi_release = 0x400018f4; -esp_coex_ble_conn_dynamic_prio_get = 0x400018f8; +PROVIDE(coex_event_duration_get = 0x400018d4); +PROVIDE(coex_hw_timer_disable = 0x400018d8); +PROVIDE(coex_hw_timer_enable = 0x400018dc); +PROVIDE(coex_hw_timer_set = 0x400018e0); +PROVIDE(coex_schm_interval_set = 0x400018e4); +PROVIDE(coex_schm_lock = 0x400018e8); +PROVIDE(coex_schm_unlock = 0x400018ec); +PROVIDE(coex_status_get = 0x400018f0); +PROVIDE(coex_wifi_release = 0x400018f4); +PROVIDE(esp_coex_ble_conn_dynamic_prio_get = 0x400018f8); /* Data (.data, .bss, .rodata) */ coex_env_ptr = 0x3fcdf848; coex_pti_tab_ptr = 0x3fcdf844; @@ -2101,143 +2101,143 @@ g_coex_param_ptr = 0x3fcdf834; ***************************************/ /* Functions */ -phy_get_romfuncs = 0x400018fc; -rom_abs_temp = 0x40001900; -rom_bb_bss_cbw40_dig = 0x40001904; -rom_bb_wdg_test_en = 0x40001908; -rom_bb_wdt_get_status = 0x4000190c; -rom_bb_wdt_int_enable = 0x40001910; -rom_bb_wdt_rst_enable = 0x40001914; -rom_bb_wdt_timeout_clear = 0x40001918; -rom_cbw2040_cfg = 0x4000191c; -rom_check_noise_floor = 0x40001920; -rom_chip_i2c_readReg = 0x40001924; -rom_chip_i2c_writeReg = 0x40001928; -rom_correct_rf_ana_gain = 0x4000192c; -rom_dc_iq_est = 0x40001930; -rom_disable_agc = 0x40001934; -rom_en_pwdet = 0x40001938; -rom_enable_agc = 0x4000193c; -rom_get_bbgain_db = 0x40001940; -rom_get_data_sat = 0x40001944; -rom_get_i2c_read_mask = 0x40001948; -rom_get_pwctrl_correct = 0x4000194c; -rom_get_rf_gain_qdb = 0x40001950; -rom_i2c_readReg = 0x40001954; -rom_i2c_readReg_Mask = 0x40001958; -rom_i2c_writeReg = 0x4000195c; -rom_i2c_writeReg_Mask = 0x40001960; +PROVIDE(phy_get_romfuncs = 0x400018fc); +PROVIDE(rom_abs_temp = 0x40001900); +PROVIDE(rom_bb_bss_cbw40_dig = 0x40001904); +PROVIDE(rom_bb_wdg_test_en = 0x40001908); +PROVIDE(rom_bb_wdt_get_status = 0x4000190c); +PROVIDE(rom_bb_wdt_int_enable = 0x40001910); +PROVIDE(rom_bb_wdt_rst_enable = 0x40001914); +PROVIDE(rom_bb_wdt_timeout_clear = 0x40001918); +PROVIDE(rom_cbw2040_cfg = 0x4000191c); +PROVIDE(rom_check_noise_floor = 0x40001920); +PROVIDE(rom_chip_i2c_readReg = 0x40001924); +PROVIDE(rom_chip_i2c_writeReg = 0x40001928); +PROVIDE(rom_correct_rf_ana_gain = 0x4000192c); +PROVIDE(rom_dc_iq_est = 0x40001930); +PROVIDE(rom_disable_agc = 0x40001934); +PROVIDE(rom_en_pwdet = 0x40001938); +PROVIDE(rom_enable_agc = 0x4000193c); +PROVIDE(rom_get_bbgain_db = 0x40001940); +PROVIDE(rom_get_data_sat = 0x40001944); +PROVIDE(rom_get_i2c_read_mask = 0x40001948); +PROVIDE(rom_get_pwctrl_correct = 0x4000194c); +PROVIDE(rom_get_rf_gain_qdb = 0x40001950); +PROVIDE(rom_i2c_readReg = 0x40001954); +PROVIDE(rom_i2c_readReg_Mask = 0x40001958); +PROVIDE(rom_i2c_writeReg = 0x4000195c); +PROVIDE(rom_i2c_writeReg_Mask = 0x40001960); /* rom_index_to_txbbgain = 0x40001964; */ -rom_iq_est_disable = 0x40001968; -rom_iq_est_enable = 0x4000196c; -rom_linear_to_db = 0x40001970; -rom_loopback_mode_en = 0x40001974; -rom_mhz2ieee = 0x40001978; -rom_noise_floor_auto_set = 0x4000197c; -rom_pbus_debugmode = 0x40001980; -rom_pbus_force_mode = 0x40001984; -rom_pbus_force_test = 0x40001988; -rom_pbus_rd = 0x4000198c; -rom_pbus_rd_addr = 0x40001990; -rom_pbus_rd_shift = 0x40001994; -rom_pbus_set_dco = 0x40001998; -rom_pbus_set_rxgain = 0x4000199c; -rom_pbus_workmode = 0x400019a0; -rom_pbus_xpd_rx_off = 0x400019a4; -rom_pbus_xpd_rx_on = 0x400019a8; -rom_pbus_xpd_tx_off = 0x400019ac; +PROVIDE(rom_iq_est_disable = 0x40001968); +PROVIDE(rom_iq_est_enable = 0x4000196c); +PROVIDE(rom_linear_to_db = 0x40001970); +PROVIDE(rom_loopback_mode_en = 0x40001974); +PROVIDE(rom_mhz2ieee = 0x40001978); +PROVIDE(rom_noise_floor_auto_set = 0x4000197c); +PROVIDE(rom_pbus_debugmode = 0x40001980); +PROVIDE(rom_pbus_force_mode = 0x40001984); +PROVIDE(rom_pbus_force_test = 0x40001988); +PROVIDE(rom_pbus_rd = 0x4000198c); +PROVIDE(rom_pbus_rd_addr = 0x40001990); +PROVIDE(rom_pbus_rd_shift = 0x40001994); +PROVIDE(rom_pbus_set_dco = 0x40001998); +PROVIDE(rom_pbus_set_rxgain = 0x4000199c); +PROVIDE(rom_pbus_workmode = 0x400019a0); +PROVIDE(rom_pbus_xpd_rx_off = 0x400019a4); +PROVIDE(rom_pbus_xpd_rx_on = 0x400019a8); +PROVIDE(rom_pbus_xpd_tx_off = 0x400019ac); /* rom_pbus_xpd_tx_on = 0x400019b0; */ -rom_phy_byte_to_word = 0x400019b4; -rom_phy_disable_cca = 0x400019b8; -rom_phy_enable_cca = 0x400019bc; -rom_phy_get_noisefloor = 0x400019c0; -rom_phy_get_rx_freq = 0x400019c4; -rom_phy_set_bbfreq_init = 0x400019c8; -rom_pow_usr = 0x400019cc; -rom_pwdet_sar2_init = 0x400019d0; -rom_read_hw_noisefloor = 0x400019d4; -rom_read_sar_dout = 0x400019d8; -rom_set_cal_rxdc = 0x400019dc; -rom_set_chan_cal_interp = 0x400019e0; -rom_set_loopback_gain = 0x400019e4; -rom_set_noise_floor = 0x400019e8; -rom_set_rxclk_en = 0x400019ec; +PROVIDE(rom_phy_byte_to_word = 0x400019b4); +PROVIDE(rom_phy_disable_cca = 0x400019b8); +PROVIDE(rom_phy_enable_cca = 0x400019bc); +PROVIDE(rom_phy_get_noisefloor = 0x400019c0); +PROVIDE(rom_phy_get_rx_freq = 0x400019c4); +PROVIDE(rom_phy_set_bbfreq_init = 0x400019c8); +PROVIDE(rom_pow_usr = 0x400019cc); +PROVIDE(rom_pwdet_sar2_init = 0x400019d0); +PROVIDE(rom_read_hw_noisefloor = 0x400019d4); +PROVIDE(rom_read_sar_dout = 0x400019d8); +PROVIDE(rom_set_cal_rxdc = 0x400019dc); +PROVIDE(rom_set_chan_cal_interp = 0x400019e0); +PROVIDE(rom_set_loopback_gain = 0x400019e4); +PROVIDE(rom_set_noise_floor = 0x400019e8); +PROVIDE(rom_set_rxclk_en = 0x400019ec); /* rom_set_tx_dig_gain = 0x400019f0; */ /* rom_set_txcap_reg = 0x400019f4; */ -rom_set_txclk_en = 0x400019f8; -rom_spur_cal = 0x400019fc; -rom_spur_reg_write_one_tone = 0x40001a00; -rom_target_power_add_backoff = 0x40001a04; -rom_tx_pwctrl_bg_init = 0x40001a08; +PROVIDE(rom_set_txclk_en = 0x400019f8); +PROVIDE(rom_spur_cal = 0x400019fc); +PROVIDE(rom_spur_reg_write_one_tone = 0x40001a00); +PROVIDE(rom_target_power_add_backoff = 0x40001a04); +PROVIDE(rom_tx_pwctrl_bg_init = 0x40001a08); /* rom_txbbgain_to_index = 0x40001a0c; */ -rom_wifi_11g_rate_chg = 0x40001a10; -rom_write_gain_mem = 0x40001a14; -chip726_phyrom_version = 0x40001a18; -rom_disable_wifi_agc = 0x40001a1c; -rom_enable_wifi_agc = 0x40001a20; -rom_set_tx_gain_table = 0x40001a24; -rom_bt_index_to_bb = 0x40001a28; -rom_bt_bb_to_index = 0x40001a2c; -rom_wr_bt_tx_atten = 0x40001a30; -rom_wr_bt_tx_gain_mem = 0x40001a34; -rom_spur_coef_cfg = 0x40001a38; -rom_bb_bss_cbw40 = 0x40001a3c; -rom_set_cca = 0x40001a40; -rom_tx_paon_set = 0x40001a44; -rom_i2cmst_reg_init = 0x40001a48; -rom_iq_corr_enable = 0x40001a4c; -rom_fe_reg_init = 0x40001a50; +PROVIDE(rom_wifi_11g_rate_chg = 0x40001a10); +PROVIDE(rom_write_gain_mem = 0x40001a14); +PROVIDE(chip726_phyrom_version = 0x40001a18); +PROVIDE(rom_disable_wifi_agc = 0x40001a1c); +PROVIDE(rom_enable_wifi_agc = 0x40001a20); +PROVIDE(rom_set_tx_gain_table = 0x40001a24); +PROVIDE(rom_bt_index_to_bb = 0x40001a28); +PROVIDE(rom_bt_bb_to_index = 0x40001a2c); +PROVIDE(rom_wr_bt_tx_atten = 0x40001a30); +PROVIDE(rom_wr_bt_tx_gain_mem = 0x40001a34); +PROVIDE(rom_spur_coef_cfg = 0x40001a38); +PROVIDE(rom_bb_bss_cbw40 = 0x40001a3c); +PROVIDE(rom_set_cca = 0x40001a40); +PROVIDE(rom_tx_paon_set = 0x40001a44); +PROVIDE(rom_i2cmst_reg_init = 0x40001a48); +PROVIDE(rom_iq_corr_enable = 0x40001a4c); +PROVIDE(rom_fe_reg_init = 0x40001a50); /* rom_agc_reg_init = 0x40001a54; */ /* rom_bb_reg_init = 0x40001a58; */ -rom_mac_enable_bb = 0x40001a5c; -rom_bb_wdg_cfg = 0x40001a60; -rom_force_txon = 0x40001a64; -rom_fe_txrx_reset = 0x40001a68; -rom_set_rx_comp = 0x40001a6c; +PROVIDE(rom_mac_enable_bb = 0x40001a5c); +PROVIDE(rom_bb_wdg_cfg = 0x40001a60); +PROVIDE(rom_force_txon = 0x40001a64); +PROVIDE(rom_fe_txrx_reset = 0x40001a68); +PROVIDE(rom_set_rx_comp = 0x40001a6c); /* rom_set_pbus_reg = 0x40001a70; */ -rom_write_chan_freq = 0x40001a74; +PROVIDE(rom_write_chan_freq = 0x40001a74); /* rom_phy_xpd_rf = 0x40001a78; */ -rom_set_xpd_sar = 0x40001a7c; -rom_write_dac_gain2 = 0x40001a80; -rom_rtc_sar2_init = 0x40001a84; -rom_get_target_power_offset = 0x40001a88; +PROVIDE(rom_set_xpd_sar = 0x40001a7c); +PROVIDE(rom_write_dac_gain2 = 0x40001a80); +PROVIDE(rom_rtc_sar2_init = 0x40001a84); +PROVIDE(rom_get_target_power_offset = 0x40001a88); /* rom_write_txrate_power_offset = 0x40001a8c; */ -rom_get_rate_fcc_index = 0x40001a90; -rom_get_rate_target_power = 0x40001a94; -rom_write_wifi_dig_gain = 0x40001a98; -rom_bt_correct_rf_ana_gain = 0x40001a9c; -rom_pkdet_vol_start = 0x40001aa0; -rom_read_sar2_code = 0x40001aa4; -rom_get_sar2_vol = 0x40001aa8; -rom_get_pll_vol = 0x40001aac; -rom_get_phy_target_power = 0x40001ab0; +PROVIDE(rom_get_rate_fcc_index = 0x40001a90); +PROVIDE(rom_get_rate_target_power = 0x40001a94); +PROVIDE(rom_write_wifi_dig_gain = 0x40001a98); +PROVIDE(rom_bt_correct_rf_ana_gain = 0x40001a9c); +PROVIDE(rom_pkdet_vol_start = 0x40001aa0); +PROVIDE(rom_read_sar2_code = 0x40001aa4); +PROVIDE(rom_get_sar2_vol = 0x40001aa8); +PROVIDE(rom_get_pll_vol = 0x40001aac); +PROVIDE(rom_get_phy_target_power = 0x40001ab0); /* rom_temp_to_power = 0x40001ab4; */ -rom_phy_track_pll_cap = 0x40001ab8; -rom_phy_pwdet_always_en = 0x40001abc; -rom_phy_pwdet_onetime_en = 0x40001ac0; -rom_get_i2c_mst0_mask = 0x40001ac4; -rom_get_i2c_hostid = 0x40001ac8; -rom_enter_critical_phy = 0x40001acc; -rom_exit_critical_phy = 0x40001ad0; -rom_chip_i2c_readReg_org = 0x40001ad4; -rom_i2c_paral_set_mst0 = 0x40001ad8; -rom_i2c_paral_set_read = 0x40001adc; -rom_i2c_paral_read = 0x40001ae0; -rom_i2c_paral_write = 0x40001ae4; -rom_i2c_paral_write_num = 0x40001ae8; -rom_i2c_paral_write_mask = 0x40001aec; -rom_bb_bss_cbw40_ana = 0x40001af0; -rom_chan_to_freq = 0x40001af4; +PROVIDE(rom_phy_track_pll_cap = 0x40001ab8); +PROVIDE(rom_phy_pwdet_always_en = 0x40001abc); +PROVIDE(rom_phy_pwdet_onetime_en = 0x40001ac0); +PROVIDE(rom_get_i2c_mst0_mask = 0x40001ac4); +PROVIDE(rom_get_i2c_hostid = 0x40001ac8); +PROVIDE(rom_enter_critical_phy = 0x40001acc); +PROVIDE(rom_exit_critical_phy = 0x40001ad0); +PROVIDE(rom_chip_i2c_readReg_org = 0x40001ad4); +PROVIDE(rom_i2c_paral_set_mst0 = 0x40001ad8); +PROVIDE(rom_i2c_paral_set_read = 0x40001adc); +PROVIDE(rom_i2c_paral_read = 0x40001ae0); +PROVIDE(rom_i2c_paral_write = 0x40001ae4); +PROVIDE(rom_i2c_paral_write_num = 0x40001ae8); +PROVIDE(rom_i2c_paral_write_mask = 0x40001aec); +PROVIDE(rom_bb_bss_cbw40_ana = 0x40001af0); +PROVIDE(rom_chan_to_freq = 0x40001af4); /* rom_open_i2c_xpd = 0x40001af8; */ -rom_dac_rate_set = 0x40001afc; +PROVIDE(rom_dac_rate_set = 0x40001afc); /* rom_tsens_read_init = 0x40001b00; */ /* rom_tsens_code_read = 0x40001b04; */ -rom_tsens_index_to_dac = 0x40001b08; -rom_tsens_index_to_offset = 0x40001b0c; +PROVIDE(rom_tsens_index_to_dac = 0x40001b08); +PROVIDE(rom_tsens_index_to_offset = 0x40001b0c); /* rom_tsens_dac_cal = 0x40001b10; */ -rom_code_to_temp = 0x40001b14; -rom_write_pll_cap_mem = 0x40001b18; -rom_pll_correct_dcap = 0x40001b1c; -rom_phy_en_hw_set_freq = 0x40001b20; -rom_phy_dis_hw_set_freq = 0x40001b24; +PROVIDE(rom_code_to_temp = 0x40001b14); +PROVIDE(rom_write_pll_cap_mem = 0x40001b18); +PROVIDE(rom_pll_correct_dcap = 0x40001b1c); +PROVIDE(rom_phy_en_hw_set_freq = 0x40001b20); +PROVIDE(rom_phy_dis_hw_set_freq = 0x40001b24); /* rom_pll_vol_cal = 0x40001b28; */ From d52d1632a164ba81a28da2362ed4f0349e293926 Mon Sep 17 00:00:00 2001 From: rdon Date: Thu, 26 Mar 2026 02:10:05 +0900 Subject: [PATCH 65/81] fix: replace ! with ~ for register mask --- src/machine/machine_rp2040_rom.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/machine/machine_rp2040_rom.go b/src/machine/machine_rp2040_rom.go index 5541e2a9bf..0f7c6819a5 100644 --- a/src/machine/machine_rp2040_rom.go +++ b/src/machine/machine_rp2040_rom.go @@ -115,7 +115,7 @@ void ram_func flash_cs_force(bool high) { // &ioqspi_hw->io[1].ctrl uint32_t *addr = (uint32_t*)(IO_QSPI_BASE + (1 * 8) + 4); - *addr = ((*addr) & !IO_QSPI_GPIO_QSPI_SS_CTRL_OUTOVER_BITS) + *addr = ((*addr) & ~IO_QSPI_GPIO_QSPI_SS_CTRL_OUTOVER_BITS) | (field_val << IO_QSPI_GPIO_QSPI_SS_CTRL_OUTOVER_LSB); } From 468da8050e2a0c7330cfcbcf03fb7b389edd84d4 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 29 Mar 2026 10:47:33 +0200 Subject: [PATCH 66/81] machine/esp32c3: implement BlockDevice for esp32c3 flash - Add `machine_esp32c3_flash.go` to implement the `BlockDevice` interface for ESP32-C3. - Map internal ESP ROM SPI flash and cache invalidation functions via CGo. - Update `targets/esp32c3.ld` to expose `__flash_data_start` and `__flash_data_end` linker variables. - Add `esp32c3` to build tags in `src/machine/flash.go`. - Ensure atomic flash operations by disabling interrupts and invalidating cache to prevent stale reads. Signed-off-by: deadprogram --- src/machine/flash.go | 2 +- src/machine/machine_esp32c3_flash.go | 137 +++++++++++++++++++++++++++ targets/esp32c3.ld | 4 + 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/machine/machine_esp32c3_flash.go diff --git a/src/machine/flash.go b/src/machine/flash.go index bfbb28794a..bcbc64e12e 100644 --- a/src/machine/flash.go +++ b/src/machine/flash.go @@ -1,4 +1,4 @@ -//go:build nrf || nrf51 || nrf52 || nrf528xx || stm32f4 || stm32l0 || stm32l4 || stm32wlx || atsamd21 || atsamd51 || atsame5x || rp2040 || rp2350 +//go:build esp32c3 || nrf || nrf51 || nrf52 || nrf528xx || stm32f4 || stm32l0 || stm32l4 || stm32wlx || atsamd21 || atsamd51 || atsame5x || rp2040 || rp2350 package machine diff --git a/src/machine/machine_esp32c3_flash.go b/src/machine/machine_esp32c3_flash.go new file mode 100644 index 0000000000..5229abfb47 --- /dev/null +++ b/src/machine/machine_esp32c3_flash.go @@ -0,0 +1,137 @@ +//go:build esp32c3 + +package machine + +import ( + "runtime/interrupt" + "unsafe" +) + +/* +#include +extern int esp_rom_spiflash_read(uint32_t src_addr, uint32_t *data, uint32_t len); +extern int esp_rom_spiflash_write(uint32_t dest_addr, const uint32_t *data, uint32_t len); +extern int esp_rom_spiflash_erase_sector(uint32_t sector_num); +extern int esp_rom_spiflash_unlock(void); +extern void Cache_Invalidate_Addr(uint32_t addr, uint32_t size); +*/ +import "C" + +// compile-time check for ensuring we fulfill BlockDevice interface +var _ BlockDevice = flashBlockDevice{} + +var Flash flashBlockDevice + +type flashBlockDevice struct { +} + +// ReadAt reads the given number of bytes from the block device. +func (f flashBlockDevice) ReadAt(p []byte, off int64) (n int, err error) { + if readAddress(off)+uintptr(len(p)) > FlashDataEnd() { + return 0, errFlashCannotReadPastEOF + } + + data := unsafe.Slice((*byte)(unsafe.Add(unsafe.Pointer(FlashDataStart()), off)), len(p)) + copy(p, data) + + return len(p), nil +} + +// WriteAt writes the given number of bytes to the block device. +// Only word (32 bits) length data can be programmed. +// If the length of p is not long enough it will be padded with 0xFF bytes. +// This method assumes that the destination is already erased. +func (f flashBlockDevice) WriteAt(p []byte, off int64) (n int, err error) { + return f.writeAt(p, off) +} + +// Size returns the number of bytes in this block device. +func (f flashBlockDevice) Size() int64 { + return int64(FlashDataEnd() - FlashDataStart()) +} + +const writeBlockSize = 4 + +// WriteBlockSize returns the block size in which data can be written to +// memory. It can be used by a client to optimize writes, non-aligned writes +// should always work correctly. +func (f flashBlockDevice) WriteBlockSize() int64 { + return writeBlockSize +} + +const eraseBlockSizeValue = 1 << 12 + +func eraseBlockSize() int64 { + return eraseBlockSizeValue +} + +// EraseBlockSize returns the smallest erasable area on this particular chip +// in bytes. This is used for the block size in EraseBlocks. +func (f flashBlockDevice) EraseBlockSize() int64 { + return eraseBlockSize() +} + +// EraseBlocks erases the given number of blocks. An implementation may +// transparently coalesce ranges of blocks into larger bundles if the chip +// supports this. The start and len parameters are in block numbers, use +// EraseBlockSize to map addresses to blocks. +func (f flashBlockDevice) EraseBlocks(start, length int64) error { + return f.eraseBlocks(start, length) +} + +// return the correct address to be used for reads +func readAddress(off int64) uintptr { + return FlashDataStart() + uintptr(off) +} + +const flashDROMStart = 0x3C000000 + +// return the correct physical address to be used for write/erase +func writeAddress(off int64) uint32 { + // DROM maps 1:1 with flash physical offset, starting at 0x3C000000. + return uint32(readAddress(off) - flashDROMStart) +} + +func (f flashBlockDevice) writeAt(p []byte, off int64) (n int, err error) { + if readAddress(off)+uintptr(len(p)) > FlashDataEnd() { + return 0, errFlashCannotWritePastEOF + } + + address := writeAddress(off) + padded := flashPad(p, int(f.WriteBlockSize())) + + state := interrupt.Disable() + defer interrupt.Restore(state) + + C.esp_rom_spiflash_unlock() + res := C.esp_rom_spiflash_write(C.uint32_t(address), (*C.uint32_t)(unsafe.Pointer(&padded[0])), C.uint32_t(len(padded))) + C.Cache_Invalidate_Addr(C.uint32_t(readAddress(off)), C.uint32_t(len(padded))) + if res != 0 { + return 0, errFlashCannotWriteData + } + + return len(padded), nil +} + +func (f flashBlockDevice) eraseBlocks(start, length int64) error { + address := writeAddress(start * f.EraseBlockSize()) + if uintptr(unsafe.Add(unsafe.Pointer(uintptr(address)+flashDROMStart), length*f.EraseBlockSize())) > FlashDataEnd() { + return errFlashCannotErasePastEOF + } + + state := interrupt.Disable() + defer interrupt.Restore(state) + + C.esp_rom_spiflash_unlock() + sector := address / uint32(f.EraseBlockSize()) + + for i := int64(0); i < length; i++ { + res := C.esp_rom_spiflash_erase_sector(C.uint32_t(sector + uint32(i))) + C.Cache_Invalidate_Addr(C.uint32_t(readAddress((start+i)*f.EraseBlockSize())), C.uint32_t(f.EraseBlockSize())) + if res != 0 { + return errFlashCannotErasePage + } + } + + return nil +} diff --git a/targets/esp32c3.ld b/targets/esp32c3.ld index 4bdc72c83f..a5c09182a3 100644 --- a/targets/esp32c3.ld +++ b/targets/esp32c3.ld @@ -161,8 +161,12 @@ SECTIONS *(.text.exception_vectors) . = ALIGN (4); *(.text .text.*) + _irom_end = .; } >IROM + PROVIDE(__flash_data_start = ALIGN(_irom_end - 0x42000000 + 0x3C000000, 4096)); + PROVIDE(__flash_data_end = 0x3C000000 + 4M); + /DISCARD/ : { *(.eh_frame) /* causes 'no memory region specified' error in lld */ From c22a58b322f20284324ebb336004657495cbca12 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 29 Mar 2026 12:50:59 +0200 Subject: [PATCH 67/81] fix(esp32c3): map missing IRAM sections in linker script Adds `.coexiram*`, `.wifiorslpiram*`, and `.iram1*` to the main `.iram` segment in `esp32c3.ld`. This resolves the "Invalid image block, can't boot" error on the ESP32-C3 by ensuring these precompiled ESP-IDF blob sections are correctly aligned in memory. Signed-off-by: deadprogram --- targets/esp32c3.ld | 3 +++ 1 file changed, 3 insertions(+) diff --git a/targets/esp32c3.ld b/targets/esp32c3.ld index a5c09182a3..5fe8cc89d2 100644 --- a/targets/esp32c3.ld +++ b/targets/esp32c3.ld @@ -132,6 +132,9 @@ SECTIONS *(.wifi0iram*) *(.wifislpiram*) *(.wifirxiram*) + *(.wifiorslpiram*) + *(.iram1*) + *(.coexiram*) __init_start = .; *(.init) __init_end = .; From a6c9ac6d67c029744b758987ae6af6fe1d32e26b Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 29 Mar 2026 12:52:41 +0200 Subject: [PATCH 68/81] main: add extra second of delay when flashing esp32 boards before reset Signed-off-by: deadprogram --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 30985022e1..5316798a1c 100644 --- a/main.go +++ b/main.go @@ -1083,6 +1083,8 @@ func flashBinUsingEsp32(port, resetMode, tmppath string, options *compileopts.Op } fmt.Println() + time.Sleep(time.Second) + // Reset the device to run the new firmware flasher.Reset() From e98f7786dd71de9f53b98e14b07329c0801ce26a Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 29 Mar 2026 11:08:49 +0200 Subject: [PATCH 69/81] builder: fix panic in findPackagePath when using -size short This fixes an "index out of range" panic that occurs when calculating binary sizes. The strings.SplitN operation in findPackagePath can sometimes return a slice with only one element, so we now verify the length of the slice before attempting to access the second element. Signed-off-by: deadprogram --- builder/sizes.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/builder/sizes.go b/builder/sizes.go index 57fb36df67..0b407f96f7 100644 --- a/builder/sizes.go +++ b/builder/sizes.go @@ -954,7 +954,11 @@ func findPackagePath(path string, packagePathMap map[string]string) (packagePath libPath := strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")+string(os.PathSeparator)) parts := strings.SplitN(libPath, string(os.PathSeparator), 2) packagePath = "C " + parts[0] - filename = parts[1] + if len(parts) > 1 { + filename = parts[1] + } else { + filename = parts[0] + } } else if prefix := filepath.Join(goenv.Get("TINYGOROOT"), "llvm-project", "compiler-rt"); strings.HasPrefix(path, prefix) { packagePath = "C compiler-rt" filename = strings.TrimPrefix(path, prefix+string(os.PathSeparator)) From 675dc7f3382a2f50b0ffeadd87e5318107a9f86d Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 31 Mar 2026 18:34:57 +0200 Subject: [PATCH 70/81] build: let scoop run updates to resolve binaryen install failures on Windows Signed-off-by: deadprogram --- .github/workflows/windows.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 0cfeb518be..98e1ea8f8b 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -24,8 +24,6 @@ jobs: maximum-size: 24GB disk-root: "C:" - uses: MinoruSekine/setup-scoop@v4 - with: - scoop_update: 'false' - name: Install Dependencies shell: bash run: | @@ -136,8 +134,6 @@ jobs: maximum-size: 24GB disk-root: "C:" - uses: MinoruSekine/setup-scoop@v4 - with: - scoop_update: 'false' - name: Install Dependencies shell: bash run: | @@ -202,8 +198,6 @@ jobs: maximum-size: 24GB disk-root: "C:" - uses: MinoruSekine/setup-scoop@v4 - with: - scoop_update: 'false' - name: Install Dependencies shell: bash run: | From 8315c89731aaf32c6c1f40d47c20935e75e60e7e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 1 Apr 2026 10:46:06 +0200 Subject: [PATCH 71/81] interrupt/esp32c3: add Enable stub for QEMU test target Signed-off-by: deadprogram --- src/runtime/interrupt/interrupt_esp32c3_qemu.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/runtime/interrupt/interrupt_esp32c3_qemu.go diff --git a/src/runtime/interrupt/interrupt_esp32c3_qemu.go b/src/runtime/interrupt/interrupt_esp32c3_qemu.go new file mode 100644 index 0000000000..039daf0274 --- /dev/null +++ b/src/runtime/interrupt/interrupt_esp32c3_qemu.go @@ -0,0 +1,7 @@ +//go:build tinygo.riscv && esp32c3_qemu_target + +package interrupt + +// Enable is a no-op stub for the QEMU esp32c3 test target. +// Hardware interrupts are not needed when running tests under qemu-system-riscv32. +func (i Interrupt) Enable() error { return nil } From 61cc02f39166eb0d8fc0759b39252b89b51863be Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 1 Apr 2026 09:47:19 +0200 Subject: [PATCH 72/81] chore: update all github actions to latest versions Signed-off-by: deadprogram --- .github/workflows/build-macos.yml | 14 ++++----- .github/workflows/docker.yml | 12 ++++---- .github/workflows/linux.yml | 48 +++++++++++++++---------------- .github/workflows/llvm.yml | 10 +++---- .github/workflows/nix.yml | 6 ++-- .github/workflows/sizediff.yml | 6 ++-- .github/workflows/windows.yml | 20 ++++++------- 7 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 1db9de1b48..ac65536291 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -31,7 +31,7 @@ jobs: run: | HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu binaryen - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: true - name: Extract TinyGo version @@ -43,7 +43,7 @@ jobs: go-version: '1.25.7' cache: true - name: Restore LLVM source cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-source with: key: llvm-source-20-${{ matrix.os }}-v1 @@ -57,7 +57,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Save LLVM source cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-source.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} @@ -68,7 +68,7 @@ jobs: llvm-project/lld/include llvm-project/llvm/include - name: Restore LLVM build cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-build with: key: llvm-build-20-${{ matrix.os }}-v2 @@ -85,7 +85,7 @@ jobs: make llvm-build find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; - name: Save LLVM build cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-build.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-build.outputs.cache-primary-key }} @@ -107,7 +107,7 @@ jobs: # - have a double-zipped artifact when downloaded from the UI # - have a very slow artifact upload # We're doing the former here, to keep artifact uploads fast. - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: darwin-${{ matrix.goarch }}-double-zipped-${{ steps.version.outputs.version }} path: build/tinygo${{ steps.version.outputs.version }}.darwin-${{ matrix.goarch }}.tar.gz @@ -131,7 +131,7 @@ jobs: run: | brew install llvm@${{ matrix.version }} - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 18fd5127d4..f1e8be695b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -31,14 +31,14 @@ jobs: sudo rm -rf /usr/local/share/boost df -h - name: Check out the repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | tinygo/tinygo-dev @@ -47,18 +47,18 @@ jobs: type=sha,format=long type=raw,value=latest - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Log in to Github Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: true diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index e11d8193af..4d161cc5f1 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -23,7 +23,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Install apk dependencies - # tar: needed for actions/cache@v4 + # tar: needed for actions/cache@v5 # git+openssh: needed for checkout (I think?) # ruby: needed to install fpm run: apk add tar git openssh make g++ ruby-dev mold @@ -31,21 +31,21 @@ jobs: # We're not on a multi-user machine, so this is safe. run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: true - name: Extract TinyGo version id: version run: ./.github/workflows/tinygo-extract-version.sh | tee -a "$GITHUB_OUTPUT" - name: Cache Go - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: go-cache-linux-alpine-v1-${{ hashFiles('go.mod') }} path: | ~/.cache/go-build ~/go/pkg/mod - name: Restore LLVM source cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-source with: key: llvm-source-20-linux-alpine-v1 @@ -59,7 +59,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Save LLVM source cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-source.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} @@ -70,7 +70,7 @@ jobs: llvm-project/lld/include llvm-project/llvm/include - name: Restore LLVM build cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-build with: key: llvm-build-20-linux-alpine-v1 @@ -88,13 +88,13 @@ jobs: # Remove unnecessary object files (to reduce cache size). find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; - name: Save LLVM build cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-build.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-build.outputs.cache-primary-key }} path: llvm-build - name: Cache Binaryen - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-binaryen with: key: binaryen-linux-alpine-v1 @@ -119,7 +119,7 @@ jobs: cp -p build/release.tar.gz /tmp/tinygo${{ steps.version.outputs.version }}.linux-amd64.tar.gz cp -p build/release.deb /tmp/tinygo_${{ steps.version.outputs.version }}_amd64.deb - name: Publish release artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: linux-amd64-double-zipped-${{ steps.version.outputs.version }} path: | @@ -131,7 +131,7 @@ jobs: needs: build-linux steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: true - name: Install Go @@ -164,7 +164,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: true - name: Install apt dependencies @@ -184,7 +184,7 @@ jobs: go-version: '1.25.7' cache: true - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '18' - name: Install wasmtime @@ -194,7 +194,7 @@ jobs: - name: Setup `wasm-tools` uses: bytecodealliance/actions/wasm-tools/setup@v1 - name: Restore LLVM source cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-source with: key: llvm-source-20-linux-asserts-v1 @@ -208,7 +208,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Save LLVM source cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-source.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} @@ -219,7 +219,7 @@ jobs: llvm-project/lld/include llvm-project/llvm/include - name: Restore LLVM build cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-build with: key: llvm-build-20-linux-asserts-v1 @@ -235,13 +235,13 @@ jobs: # Remove unnecessary object files (to reduce cache size). find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; - name: Save LLVM build cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-build.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-build.outputs.cache-primary-key }} path: llvm-build - name: Cache Binaryen - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-binaryen with: key: binaryen-linux-asserts-v1 @@ -284,7 +284,7 @@ jobs: needs: build-linux steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get TinyGo version id: version run: ./.github/workflows/tinygo-extract-version.sh | tee -a "$GITHUB_OUTPUT" @@ -301,7 +301,7 @@ jobs: go-version: '1.25.7' cache: true - name: Restore LLVM source cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-source with: key: llvm-source-20-linux-v1 @@ -315,7 +315,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Save LLVM source cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-source.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} @@ -326,7 +326,7 @@ jobs: llvm-project/lld/include llvm-project/llvm/include - name: Restore LLVM build cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-build with: key: llvm-build-20-linux-${{ matrix.goarch }}-v1 @@ -344,13 +344,13 @@ jobs: # Remove unnecessary object files (to reduce cache size). find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; - name: Save LLVM build cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-build.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-build.outputs.cache-primary-key }} path: llvm-build - name: Cache Binaryen - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-binaryen with: key: binaryen-linux-${{ matrix.goarch }}-v4 @@ -387,7 +387,7 @@ jobs: cp -p build/release.tar.gz /tmp/tinygo${{ steps.version.outputs.version }}.linux-${{ matrix.goarch }}.tar.gz cp -p build/release.deb /tmp/tinygo_${{ steps.version.outputs.version }}_${{ matrix.libc }}.deb - name: Publish release artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: linux-${{ matrix.goarch }}-double-zipped-${{ steps.version.outputs.version }} path: | diff --git a/.github/workflows/llvm.yml b/.github/workflows/llvm.yml index ec45d587e4..fac80561ed 100644 --- a/.github/workflows/llvm.yml +++ b/.github/workflows/llvm.yml @@ -25,14 +25,14 @@ jobs: contents: read steps: - name: Check out the repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | tinygo/llvm-20 @@ -46,13 +46,13 @@ jobs: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Log in to Github Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: target: tinygo-llvm-build context: . diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 6c5b9f9a41..0d3abc2b62 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -21,12 +21,12 @@ jobs: # See: https://github.com/tinygo-org/tinygo/pull/4516#issuecomment-2416363668 run: sudo apt-get remove llvm-18 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Pull musl, bdwgc run: | git submodule update --init lib/musl lib/bdwgc - name: Restore LLVM source cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-source with: key: llvm-source-20-linux-nix-v1 @@ -36,7 +36,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Save LLVM source cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-source.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} diff --git a/.github/workflows/sizediff.yml b/.github/workflows/sizediff.yml index 08130a78c9..8243306c16 100644 --- a/.github/workflows/sizediff.yml +++ b/.github/workflows/sizediff.yml @@ -20,14 +20,14 @@ jobs: run: | echo "$HOME/go/bin" >> $GITHUB_PATH - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 # fetch all history (no sparse checkout) submodules: true - name: Install apt dependencies run: ./.github/workflows/sizediff-install-pkgs.sh - name: Restore LLVM source cache - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-llvm-source with: key: llvm-source-20-sizediff-v1 @@ -37,7 +37,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Cache Go - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: go-cache-linux-sizediff-v2-${{ hashFiles('go.mod') }} path: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 98e1ea8f8b..45cf4409b6 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -29,7 +29,7 @@ jobs: run: | scoop install ninja binaryen - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: true - name: Extract TinyGo version @@ -42,7 +42,7 @@ jobs: go-version: '1.25.7' cache: true - name: Restore cached LLVM source - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-source with: key: llvm-source-20-windows-v1 @@ -56,7 +56,7 @@ jobs: if: steps.cache-llvm-source.outputs.cache-hit != 'true' run: make llvm-source - name: Save cached LLVM source - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-source.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-source.outputs.cache-primary-key }} @@ -67,7 +67,7 @@ jobs: llvm-project/lld/include llvm-project/llvm/include - name: Restore cached LLVM build - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: cache-llvm-build with: key: llvm-build-20-windows-v2 @@ -84,13 +84,13 @@ jobs: # Remove unnecessary object files (to reduce cache size). find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; - name: Save cached LLVM build - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: steps.cache-llvm-build.outputs.cache-hit != 'true' with: key: ${{ steps.cache-llvm-build.outputs.cache-primary-key }} path: llvm-build - name: Cache Go cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: go-cache-windows-v1-${{ hashFiles('go.mod') }} path: | @@ -118,7 +118,7 @@ jobs: # - have a dobule-zipped artifact when downloaded from the UI # - have a very slow artifact upload # We're doing the former here, to keep artifact uploads fast. - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: windows-amd64-double-zipped-${{ steps.version.outputs.version }} path: build/release/tinygo${{ steps.version.outputs.version }}.windows-amd64.zip @@ -139,7 +139,7 @@ jobs: run: | scoop install binaryen - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: @@ -169,7 +169,7 @@ jobs: maximum-size: 24GB disk-root: "C:" - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: @@ -203,7 +203,7 @@ jobs: run: | scoop install binaryen && scoop install wasmtime@29.0.1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: From e3e36f2d319a27f08de37273ea2fc88467308ac4 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 1 Apr 2026 11:10:19 +0200 Subject: [PATCH 73/81] fix: use external 7zip for scoop installs on Windows Signed-off-by: deadprogram --- .github/workflows/windows.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 45cf4409b6..a989435c64 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -27,6 +27,7 @@ jobs: - name: Install Dependencies shell: bash run: | + scoop config use_external_7zip true scoop install ninja binaryen - name: Checkout uses: actions/checkout@v6 @@ -98,6 +99,7 @@ jobs: C:/Users/runneradmin/go/pkg/mod - name: Install wasmtime run: | + scoop config use_external_7zip true scoop install wasmtime@29.0.1 - name: make gen-device run: make -j3 gen-device @@ -137,6 +139,7 @@ jobs: - name: Install Dependencies shell: bash run: | + scoop config use_external_7zip true scoop install binaryen - name: Checkout uses: actions/checkout@v6 @@ -201,7 +204,8 @@ jobs: - name: Install Dependencies shell: bash run: | - scoop install binaryen && scoop install wasmtime@29.0.1 + scoop config use_external_7zip true + scoop install binaryen wasmtime@29.0.1 - name: Checkout uses: actions/checkout@v6 - name: Install Go From d338bf66195531de64a097208c799134c391e663 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Thu, 2 Apr 2026 13:49:26 -0700 Subject: [PATCH 74/81] reflect: fix reflect.TyepAssert for structs --- src/internal/reflectlite/value.go | 2 +- src/reflect/value_test.go | 46 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index b8ec96d55f..3c2af94f72 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -138,7 +138,7 @@ func TypeAssert[T any](v Value) (T, bool) { var zero T return zero, false } - if !v.isIndirect() { + if !v.isIndirect() && v.typecode.Size() <= unsafe.Sizeof(uintptr(0)) { return *(*T)(unsafe.Pointer(&v.value)), true } return *(*T)(v.value), true diff --git a/src/reflect/value_test.go b/src/reflect/value_test.go index 4b818100b5..999c115cb7 100644 --- a/src/reflect/value_test.go +++ b/src/reflect/value_test.go @@ -924,6 +924,52 @@ func testTypeAssert[T comparable, V any](t *testing.T, val V, wantVal T, wantOk } } +func TestTypeAssertStruct(t *testing.T) { + type taStruct struct { + i int + b bool + } + + var a any + + // struct + a = taStruct{3, true} + if s, ok := a.(taStruct); ok { + if s.i != 3 || s.b != true { + t.Errorf("a.(S) failed: got s.i=%v, s.b=%v\n", s.i, s.b) + } + } else { + t.Errorf("a.(S) failed: got ok=false") + } + + if s, ok := TypeAssert[taStruct](ValueOf(a)); ok { + if s.i != 3 || s.b != true { + t.Errorf("TypeAssert[S] failed: got s.i=%v, s.b=%v\n", s.i, s.b) + } + } else { + t.Errorf("TypeAssert[S] failed: got ok=false") + } + + // struct ptr + a = &taStruct{3, true} + if s, ok := a.(*taStruct); ok { + if s.i != 3 || s.b != true { + t.Errorf("a.(*S) failed: got s.i=%v, s.b=%v\n", s.i, s.b) + } + } else { + t.Errorf("a.(*S) failed: got ok=false") + } + + if s, ok := TypeAssert[*taStruct](ValueOf(a)); ok { + if s.i != 3 || s.b != true { + t.Errorf("TypeAssert[*S] failed: got s.i=%v, s.b=%v\n", s.i, s.b) + } + } else { + t.Errorf("TypeAssert[*S] failed: got ok=false") + } + +} + type testTypeWithMethod struct{ val string } func (v testTypeWithMethod) String() string { return v.val } From c498659b00ea85b107b32ce7fa2afe40ec55deb0 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Fri, 3 Apr 2026 16:32:06 +0200 Subject: [PATCH 75/81] esp32s3: add interrupt support (#5244) * esp32s3: add interrupt support This finally adds the long awaited support for interrupts on the Xtensa arch. Initially just for the ESP32-S3 but then others. Signed-off-by: deadprogram * esp32s3: get interrupts working correctly There were a number of needed changes in order to get interrupts correctly working on the esp32s3 processor: - PS.UM=1 in interruptInit() - routed interrupts to user exception vector (0x340) instead of kernel (0x300) - Inline ISR in the vector slot - external handlers via j/call0 crashed (likely clang Xtensa literal pool issue with large movi constants in separate sections) - Disable INTENABLE (not just INT_CLR) - the USB RX interrupt is level-triggered; clearing INT_CLR alone causes infinite re-entry since data is still in the FIFO - Buffered() re-enables INTENABLE after draining the hardware FIFO Signed-off-by: deadprogram --------- Signed-off-by: deadprogram --- src/runtime/interrupt/interrupt_esp32s3.go | 223 +++++++++++++++++++++ src/runtime/interrupt/interrupt_xtensa.go | 2 +- src/runtime/runtime_esp32s3.go | 41 ++++ targets/esp32s3-interrupts.S | 202 +++++++++++++++++++ targets/esp32s3.json | 1 + targets/esp32s3.ld | 13 ++ 6 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 src/runtime/interrupt/interrupt_esp32s3.go create mode 100644 targets/esp32s3-interrupts.S diff --git a/src/runtime/interrupt/interrupt_esp32s3.go b/src/runtime/interrupt/interrupt_esp32s3.go new file mode 100644 index 0000000000..4f7b8b1111 --- /dev/null +++ b/src/runtime/interrupt/interrupt_esp32s3.go @@ -0,0 +1,223 @@ +//go:build esp32s3 + +package interrupt + +import ( + "device" + "device/esp" + "runtime/volatile" + "unsafe" +) + +// State represents the previous global interrupt state. +type State uintptr + +// Disable disables all interrupts and returns the previous interrupt state. It +// can be used in a critical section like this: +// +// state := interrupt.Disable() +// // critical section +// interrupt.Restore(state) +// +// Critical sections can be nested. Make sure to call Restore in the same order +// as you called Disable (this happens naturally with the pattern above). +func Disable() (state State) { + return State(device.AsmFull("rsil {}, 15", nil)) +} + +// Restore restores interrupts to what they were before. Give the previous state +// returned by Disable as a parameter. If interrupts were disabled before +// calling Disable, this will not re-enable interrupts, allowing for nested +// critical sections. +func Restore(state State) { + device.AsmFull("wsr {state}, PS", map[string]interface{}{ + "state": state, + }) +} + +// The ESP32-S3 (Xtensa LX7) interrupt model: +// +// 1. The **interrupt matrix** (INTERRUPT_CORE0) maps each peripheral source +// (0-98) to one of 32 CPU interrupt lines via a 5-bit mapping register. +// 2. The CPU's INTENABLE special register (SR 228) enables/disables each of +// the 32 CPU interrupt lines independently. +// 3. When an enabled CPU interrupt fires, the processor vectors to the +// level-1 exception vector (offset 0x180 from VECBASE). +// 4. The INTERRUPT special register (SR 226) shows which CPU interrupts are +// currently pending. +// +// We allocate CPU interrupt lines 6..30 for use by peripherals via +// interrupt.New(). Lines 0-5 are reserved (timer, software, etc.) and +// line 31 is avoided because some hardware treats it specially. + +const ( + // First / last allocatable CPU interrupt for peripherals. + firstCPUInt = 6 + lastCPUInt = 30 +) + +// cpuIntUsed tracks which CPU interrupt lines have been allocated. +var cpuIntUsed [32]bool + +// cpuIntToPeripheral maps CPU interrupt number → peripheral IRQ source, +// so that handleInterrupt can dispatch to the correct Go handler. +var cpuIntToPeripheral [32]int + +// inInterrupt is set while we're inside the interrupt handler so that +// interrupt.In() returns the correct value. +var inInterrupt bool + +// Enable enables a CPU interrupt for the ESP32-S3. The caller must first +// map the peripheral to a CPU interrupt line using the interrupt matrix, +// e.g.: +// +// esp.INTERRUPT_CORE0.SetGPIO_INTERRUPT_PRO_MAP(cpuInt) +// interrupt.New(cpuInt, handler).Enable() +func (i Interrupt) Enable() error { + if i.num < firstCPUInt || i.num > lastCPUInt { + return errInterruptRange + } + + // Mark as used. + cpuIntUsed[i.num] = true + + // Read current INTENABLE, set the bit for this CPU interrupt. + cur := readINTENABLE() + cur |= 1 << uint(i.num) + writeINTENABLE(cur) + + return nil +} + +// In returns whether the CPU is currently inside an interrupt handler. +func In() bool { + return inInterrupt +} + +// handleInterrupt is called from the assembly vector code in esp32s3.S. +// It determines which CPU interrupt(s) fired and dispatches to the +// registered Go handlers. +// +//export handleInterrupt +func handleInterrupt() { + inInterrupt = true + + // INTERRUPT register shows pending + enabled CPU interrupts. + pending := readINTERRUPT() + enabled := readINTENABLE() + active := pending & enabled + + for i := firstCPUInt; i <= lastCPUInt; i++ { + if active&(1<IRAM AT >DRAM +/* Xtensa exception/interrupt vector table — must be 0x400-aligned. */ +.text.exception_vectors : ALIGN(0x400) +{ + *(.text.exception_vectors) +} >IRAM AT >DRAM + +/* Level-1 interrupt handler (called from the vector stub). */ +.text._handle_level1 : ALIGN(4) +{ + *(.literal._handle_level1) + *(.text._handle_level1) +} >IRAM AT >DRAM + /* All other code and literals */ .text : ALIGN(4) { From 75422927e0b50fab40dc7c133423ac967249ddca Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 2 Apr 2026 12:56:28 +0200 Subject: [PATCH 76/81] esp32s3: switch USB implementation to use interrupts instead of polling The previous implementation for USB was using polling instead of using interrupts. This changes that. Signed-off-by: deadprogram --- src/machine/machine_esp32xx_usb.go | 161 +++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 18 deletions(-) diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index a2e79dc0c1..2e36b9b2c9 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -5,18 +5,33 @@ package machine import ( "device/esp" "errors" + "machine/usb" + "machine/usb/descriptor" + "runtime/interrupt" ) // USB Serial/JTAG Controller -// See esp32-c3_technical_reference_manual_en.pdf -// pg. 736 +// See esp32-s3_technical_reference_manual_en.pdf +// +// The ESP32-S3 has a built-in USB Serial/JTAG controller that provides a +// CDC-ACM serial port. The USB protocol and enumeration are handled entirely +// in hardware; software only reads/writes the EP1 FIFO. + +const cpuInterruptFromUSB = 8 + +// flushTimeout is the maximum number of busy-wait iterations in flush(). +// Prevents hanging when no USB host is connected. +const flushTimeout = 200000 + type USB_DEVICE struct { - Bus *esp.USB_DEVICE_Type + Bus *esp.USB_DEVICE_Type + Buffer *RingBuffer } var ( _USBCDC = &USB_DEVICE{ - Bus: esp.USB_DEVICE, + Bus: esp.USB_DEVICE, + Buffer: NewRingBuffer(), } USBCDC Serialer = _USBCDC @@ -25,7 +40,6 @@ var ( var ( errUSBWrongSize = errors.New("USB: invalid write size") errUSBCouldNotWriteAllData = errors.New("USB: could not write all data") - errUSBBufferEmpty = errors.New("USB: read buffer empty") ) type Serialer interface { @@ -38,26 +52,96 @@ type Serialer interface { RTS() bool } +var usbConfigured bool + +// USBDevice provides a stub USB device for the ESP32-S3. The hardware +// only supports a fixed-function CDC-ACM serial port, so the programmable +// USB device features are no-ops. +type USBDevice struct { + initcomplete bool + InitEndpointComplete bool +} + +var USBDev = &USBDevice{} + +func (dev *USBDevice) SetStallEPIn(ep uint32) {} +func (dev *USBDevice) SetStallEPOut(ep uint32) {} +func (dev *USBDevice) ClearStallEPIn(ep uint32) {} +func (dev *USBDevice) ClearStallEPOut(ep uint32) {} + +// initUSB is intentionally empty — the interp phase evaluates init() +// functions at compile time and cannot access hardware registers. +// Actual hardware setup is deferred to the first Configure() call. func initUSB() {} +// usbHandleInterrupt is the top-level interrupt handler passed to +// interrupt.New. It must be a plain function (not a closure) because +// interrupt.New is a compiler intrinsic that does not support closures. +func usbHandleInterrupt(interrupt.Interrupt) { + _USBCDC.handleInterrupt() +} + +// Configure initialises the USB Serial/JTAG controller. func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { + if usbConfigured { + return nil + } + usbConfigured = true + + // Enable the USB_DEVICE peripheral clock. + esp.SYSTEM.SetPERIP_CLK_EN1_USB_DEVICE_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN1_USB_DEVICE_RST(0) + + // Clear any pending interrupts, then enable the RX interrupt. + usbdev.Bus.INT_CLR.Set(0xFFFFFFFF) + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) + + // Map the USB_DEVICE peripheral interrupt to CPU interrupt line. + esp.INTERRUPT_CORE0.SetUSB_DEVICE_INT_MAP(cpuInterruptFromUSB) + + _ = interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable() + return nil } +// ensureConfigured triggers lazy initialization on first use. +func (usbdev *USB_DEVICE) ensureConfigured() { + if !usbConfigured { + usbdev.Configure(UARTConfig{}) + } +} + +// handleInterrupt is called from the CPU interrupt vector when the USB +// peripheral raises an interrupt. For now, just clear the interrupt flag. +// The actual data drain happens in Buffered() via polling — once the ISR +// mechanism is proven, we can move the drain here. +func (usbdev *USB_DEVICE) handleInterrupt() { + usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1) +} + func (usbdev *USB_DEVICE) WriteByte(c byte) error { + usbdev.ensureConfigured() if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { - return errUSBCouldNotWriteAllData + // FIFO full — try flushing first, then recheck. + usbdev.flush() + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return errUSBCouldNotWriteAllData + } } - usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) + // Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which + // does a read-modify-write — the read side-effect pops a byte from + // the RX FIFO. + usbdev.Bus.EP1.Set(uint32(c)) usbdev.flush() return nil } func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { - if len(data) == 0 || len(data) > 64 { - return 0, errUSBWrongSize + usbdev.ensureConfigured() + if len(data) == 0 { + return 0, nil } for i, c := range data { @@ -65,26 +149,43 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { if i > 0 { usbdev.flush() } - - return i, errUSBCouldNotWriteAllData + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return i, errUSBCouldNotWriteAllData + } } - usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c)) + usbdev.Bus.EP1.Set(uint32(c)) } usbdev.flush() return len(data), nil } +// Buffered returns the number of bytes waiting in the receive ring buffer. +// It drains any data sitting in the hardware FIFO and re-enables the +// USB interrupt (which the ISR disables via INTENABLE to prevent a +// level-triggered interrupt storm). func (usbdev *USB_DEVICE) Buffered() int { - return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL()) + usbdev.ensureConfigured() + // Drain the hardware FIFO into the ring buffer. + for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { + b := byte(usbdev.Bus.EP1.Get()) + usbdev.Buffer.Put(b) + } + // Clear pending flags and re-enable the RX interrupt. + usbdev.Bus.INT_CLR.Set(0xFFFFFFFF) + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) + // Re-enable CPU interrupt 8 in INTENABLE (the ISR clears all bits). + interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable() + return int(usbdev.Buffer.Used()) } +// ReadByte returns a byte from the receive ring buffer. func (usbdev *USB_DEVICE) ReadByte() (byte, error) { - if usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { - return byte(usbdev.Bus.GetEP1_RDWR_BYTE()), nil + b, ok := usbdev.Buffer.Get() + if !ok { + return 0, nil } - - return 0, nil + return b, nil } func (usbdev *USB_DEVICE) DTR() bool { @@ -95,8 +196,32 @@ func (usbdev *USB_DEVICE) RTS() bool { return false } +// flush signals WR_DONE and waits (with timeout) for the hardware to +// consume the data. A timeout prevents hanging when no USB host is present. func (usbdev *USB_DEVICE) flush() { usbdev.Bus.SetEP1_CONF_WR_DONE(1) - for usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + for i := 0; i < flushTimeout; i++ { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 { + return + } } } + +// The ESP32-S3 USB Serial/JTAG controller is fixed-function hardware. +// It only provides a CDC-ACM serial port; the USB protocol and endpoint +// configuration are handled entirely in silicon. The functions below +// are no-op stubs so that higher-level USB packages (HID, MIDI, …) +// compile, but they cannot add real endpoints on this hardware. + +// ConfigureUSBEndpoint is a no-op on ESP32-S3. +func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) { +} + +// SendZlp is a no-op on ESP32-S3. +func SendZlp() { +} + +// SendUSBInPacket is a no-op on ESP32-S3. +func SendUSBInPacket(ep uint32, data []byte) bool { + return false +} From 24f012720490049be54342c296de5689425b8c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= <782854+gandarez@users.noreply.github.com> Date: Sat, 4 Apr 2026 06:57:49 -0300 Subject: [PATCH 77/81] feat: add inheritable-only field to filter processor-level targets (#5270) * feat: add inheritable-only field to hide processor-level targets from listing Processor-level targets like esp32, rp2040, etc. have flash-method set so they leak through the existing heuristic filter in GetTargetSpecs and appear in `tinygo targets` output. Add an "inheritable-only" JSON field that is checked on raw JSON before inheritance resolution, preventing these non-board targets from being listed while keeping them loadable for direct builds and inheritance. Ref: tinygo-org/tinygo#5178 * fix: prevent inheritable-only from propagating to child targets The InheritableOnly bool field was propagating from parent to child targets through overrideProperties, which would hide board targets from GetTargetSpecs. Fix by preserving/restoring the field across resolveInherits. Also simplify GetTargetSpecs by removing the raw JSON pre-check workaround and remove nonexistent esp32c6 from tests. * prevent build commands to use inheritable only targets --- compileopts/target.go | 16 ++++++++++++++++ compileopts/target_test.go | 31 +++++++++++++++++++++++++++++++ main.go | 14 ++++++++++++++ targets/esp32.json | 1 + targets/esp32c3.json | 1 + targets/esp32s3.json | 1 + targets/esp8266.json | 1 + targets/rp2040.json | 1 + targets/rp2350.json | 1 + targets/rp2350b.json | 1 + 10 files changed, 68 insertions(+) diff --git a/compileopts/target.go b/compileopts/target.go index 16d21f46c5..29b3733bd9 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -24,6 +24,7 @@ import ( // https://github.com/shepmaster/rust-arduino-blink-led-no-core-with-cargo/blob/master/blink/arduino.json type TargetSpec struct { Inherits []string `json:"inherits,omitempty"` + InheritableOnly bool `json:"inheritable-only"` // this target is only meant to be inherited from, not used directly Triple string `json:"llvm-target,omitempty"` CPU string `json:"cpu,omitempty"` ABI string `json:"target-abi,omitempty"` // roughly equivalent to -mabi= flag @@ -148,6 +149,11 @@ func (spec *TargetSpec) loadFromGivenStr(str string) error { // resolveInherits loads inherited targets, recursively. func (spec *TargetSpec) resolveInherits() error { + // Save InheritableOnly before resolving, since it must not propagate + // from parent to child (a board target should not become inheritable-only + // just because its parent processor target is). + inheritableOnly := spec.InheritableOnly + // First create a new spec with all the inherited properties. newSpec := &TargetSpec{} for _, name := range spec.Inherits { @@ -173,6 +179,9 @@ func (spec *TargetSpec) resolveInherits() error { } *spec = *newSpec + // Restore InheritableOnly from the original spec, not from parents. + spec.InheritableOnly = inheritableOnly + return nil } @@ -239,10 +248,17 @@ func GetTargetSpecs() (map[string]*TargetSpec, error) { continue } path := filepath.Join(dir, entry.Name()) + spec, err := LoadTarget(&Options{Target: path}) if err != nil { return nil, fmt.Errorf("could not list target: %w", err) } + + if spec.InheritableOnly { + // Skip targets that are only meant to be inherited from, not used directly. + continue + } + if spec.FlashMethod == "" && spec.FlashCommand == "" && spec.Emulator == "" { // This doesn't look like a regular target file, but rather like // a parent target (such as targets/cortex-m.json). diff --git a/compileopts/target_test.go b/compileopts/target_test.go index 579c54f67b..d8a17a5e34 100644 --- a/compileopts/target_test.go +++ b/compileopts/target_test.go @@ -23,6 +23,37 @@ func TestLoadTarget(t *testing.T) { } } +func TestGetTargetSpecs_InheritableOnlyTargetsExcluded(t *testing.T) { + specs, err := GetTargetSpecs() + if err != nil { + t.Fatal("GetTargetSpecs failed:", err) + } + + // Inheritable-only processor-level targets should not appear in the listing. + inheritableOnlyTargets := []string{"esp32", "esp32c3", "esp32s3", "esp8266", "rp2040", "rp2350", "rp2350b"} + for _, name := range inheritableOnlyTargets { + if _, ok := specs[name]; ok { + t.Errorf("inheritable-only target %q should not appear in GetTargetSpecs", name) + } + } + + // Board targets that inherit from inheritable-only targets should still appear. + boardTargets := []string{"esp32-coreboard-v2", "pico"} + for _, name := range boardTargets { + if _, ok := specs[name]; !ok { + t.Errorf("board target %q should appear in GetTargetSpecs", name) + } + } +} + +func TestLoadTarget_InheritableOnlyTargetStillLoadable(t *testing.T) { + // Inheritable-only targets should still be loadable directly (for building). + _, err := LoadTarget(&Options{Target: "esp32"}) + if err != nil { + t.Errorf("LoadTarget should still load inheritable-only target esp32: %v", err) + } +} + func TestOverrideProperties(t *testing.T) { baseAutoStackSize := true base := &TargetSpec{ diff --git a/main.go b/main.go index 5316798a1c..a798b9fb4c 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,8 @@ import ( "go.bug.st/serial/enumerator" ) +var errInheritableOnly = errors.New("target is inheritable-only, which means it cannot be used directly for building or flashing") + // commandError is an error type to wrap os/exec.Command errors. This provides // some more information regarding what went wrong while running a command. type commandError struct { @@ -140,6 +142,10 @@ func printCommand(cmd string, args ...string) { // Build compiles and links the given package and writes it to outpath. func Build(pkgName, outpath string, config *compileopts.Config) error { + if config.Target != nil && config.Target.InheritableOnly { + return errInheritableOnly + } + // Create a temporary directory for intermediary files. tmpdir, err := os.MkdirTemp("", "tinygo") if err != nil { @@ -357,6 +363,10 @@ func Flash(pkgName, port, outpath string, options *compileopts.Options) error { return err } + if config.Target != nil && config.Target.InheritableOnly { + return errInheritableOnly + } + // determine the type of file to compile var fileExt string @@ -786,6 +796,10 @@ func Run(pkgName string, options *compileopts.Options, cmdArgs []string) error { return err } + if config.Target != nil && config.Target.InheritableOnly { + return errInheritableOnly + } + _, err = buildAndRun(pkgName, config, os.Stdout, cmdArgs, nil, 0, func(cmd *exec.Cmd, result builder.BuildResult) error { return cmd.Run() }) diff --git a/targets/esp32.json b/targets/esp32.json index 7aad3da4e9..2c7abd6993 100644 --- a/targets/esp32.json +++ b/targets/esp32.json @@ -1,5 +1,6 @@ { "inherits": ["xtensa"], + "inheritable-only": true, "cpu": "esp32", "features": "+atomctl,+bool,+clamps,+coprocessor,+debug,+density,+dfpaccel,+div32,+exception,+fp,+highpriinterrupts,+interrupt,+loop,+mac16,+memctl,+minmax,+miscsr,+mul32,+mul32high,+nsa,+prid,+regprotect,+rvector,+s32c1i,+sext,+threadptr,+timerint,+windowed", "build-tags": ["esp32", "esp"], diff --git a/targets/esp32c3.json b/targets/esp32c3.json index ea46d07882..583506c19f 100644 --- a/targets/esp32c3.json +++ b/targets/esp32c3.json @@ -1,5 +1,6 @@ { "inherits": ["riscv32"], + "inheritable-only": true, "features": "+32bit,+c,+m,+zmmul,-a,-b,-d,-e,-experimental-sdext,-experimental-sdtrig,-experimental-smctr,-experimental-ssctr,-experimental-svukte,-experimental-xqcia,-experimental-xqciac,-experimental-xqcicli,-experimental-xqcicm,-experimental-xqcics,-experimental-xqcicsr,-experimental-xqciint,-experimental-xqcilo,-experimental-xqcilsm,-experimental-xqcisls,-experimental-zalasr,-experimental-zicfilp,-experimental-zicfiss,-experimental-zvbc32e,-experimental-zvkgs,-f,-h,-relax,-sha,-shcounterenw,-shgatpa,-shtvala,-shvsatpa,-shvstvala,-shvstvecd,-smaia,-smcdeleg,-smcsrind,-smdbltrp,-smepmp,-smmpm,-smnpm,-smrnmi,-smstateen,-ssaia,-ssccfg,-ssccptr,-sscofpmf,-sscounterenw,-sscsrind,-ssdbltrp,-ssnpm,-sspm,-ssqosid,-ssstateen,-ssstrict,-sstc,-sstvala,-sstvecd,-ssu64xl,-supm,-svade,-svadu,-svbare,-svinval,-svnapot,-svpbmt,-svvptc,-v,-xcvalu,-xcvbi,-xcvbitmanip,-xcvelw,-xcvmac,-xcvmem,-xcvsimd,-xesppie,-xmipscmove,-xmipslsp,-xsfcease,-xsfvcp,-xsfvfnrclipxfqf,-xsfvfwmaccqqq,-xsfvqmaccdod,-xsfvqmaccqoq,-xsifivecdiscarddlone,-xsifivecflushdlone,-xtheadba,-xtheadbb,-xtheadbs,-xtheadcmo,-xtheadcondmov,-xtheadfmemidx,-xtheadmac,-xtheadmemidx,-xtheadmempair,-xtheadsync,-xtheadvdot,-xventanacondops,-xwchc,-za128rs,-za64rs,-zaamo,-zabha,-zacas,-zalrsc,-zama16b,-zawrs,-zba,-zbb,-zbc,-zbkb,-zbkc,-zbkx,-zbs,-zca,-zcb,-zcd,-zce,-zcf,-zcmop,-zcmp,-zcmt,-zdinx,-zfa,-zfbfmin,-zfh,-zfhmin,-zfinx,-zhinx,-zhinxmin,-zic64b,-zicbom,-zicbop,-zicboz,-ziccamoa,-ziccif,-zicclsm,-ziccrse,-zicntr,-zicond,-zicsr,-zifencei,-zihintntl,-zihintpause,-zihpm,-zimop,-zk,-zkn,-zknd,-zkne,-zknh,-zkr,-zks,-zksed,-zksh,-zkt,-ztso,-zvbb,-zvbc,-zve32f,-zve32x,-zve64d,-zve64f,-zve64x,-zvfbfmin,-zvfbfwma,-zvfh,-zvfhmin,-zvkb,-zvkg,-zvkn,-zvknc,-zvkned,-zvkng,-zvknha,-zvknhb,-zvks,-zvksc,-zvksed,-zvksg,-zvksh,-zvkt,-zvl1024b,-zvl128b,-zvl16384b,-zvl2048b,-zvl256b,-zvl32768b,-zvl32b,-zvl4096b,-zvl512b,-zvl64b,-zvl65536b,-zvl8192b", "build-tags": ["esp32c3", "esp"], "serial": "usb", diff --git a/targets/esp32s3.json b/targets/esp32s3.json index 281b477df1..9eac6dfbd6 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -1,5 +1,6 @@ { "inherits": ["xtensa"], + "inheritable-only": true, "cpu": "esp32s3", "features": "+atomctl,+bool,+clamps,+coprocessor,+debug,+density,+div32,+esp32s3,+exception,+fp,+highpriinterrupts,+interrupt,+loop,+mac16,+memctl,+minmax,+miscsr,+mul32,+mul32high,+nsa,+prid,+regprotect,+rvector,+s32c1i,+sext,+threadptr,+timerint,+windowed", "build-tags": ["esp32s3", "esp"], diff --git a/targets/esp8266.json b/targets/esp8266.json index b6e4a3500f..83d5bcd1bb 100644 --- a/targets/esp8266.json +++ b/targets/esp8266.json @@ -1,5 +1,6 @@ { "inherits": ["xtensa"], + "inheritable-only": true, "cpu": "esp8266", "features": "+debug,+density,+exception,+extendedl32r,+highpriinterrupts,+interrupt,+mul32,+nsa,+prid,+regprotect,+rvector,+timerint", "build-tags": ["esp8266", "esp"], diff --git a/targets/rp2040.json b/targets/rp2040.json index 6470d57f50..e2969fea5a 100644 --- a/targets/rp2040.json +++ b/targets/rp2040.json @@ -1,5 +1,6 @@ { "inherits": ["cortex-m0plus"], + "inheritable-only": true, "build-tags": ["rp2040", "rp"], "scheduler": "tasks", "flash-1200-bps-reset": "true", diff --git a/targets/rp2350.json b/targets/rp2350.json index e3a6549fd2..f4567652da 100644 --- a/targets/rp2350.json +++ b/targets/rp2350.json @@ -1,5 +1,6 @@ { "inherits": ["cortex-m33"], + "inheritable-only": true, "build-tags": ["rp2350", "rp"], "scheduler": "tasks", "flash-1200-bps-reset": "true", diff --git a/targets/rp2350b.json b/targets/rp2350b.json index 5db4d48492..6e628c6b77 100644 --- a/targets/rp2350b.json +++ b/targets/rp2350b.json @@ -1,5 +1,6 @@ { "inherits": ["rp2350"], + "inheritable-only": true, "build-tags": ["rp2350b"], "serial-port": ["2e8a:000f"] } \ No newline at end of file From 3bd565c9896dfdc762e18a66b8bb4022f4b6281f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= Date: Fri, 3 Apr 2026 15:28:46 -0300 Subject: [PATCH 78/81] targets: add esp32s3-supermini --- GNUmakefile | 10 ++- src/machine/board_esp32c3-supermini.go | 2 +- src/machine/board_esp32s3-supermini.go | 88 ++++++++++++++++++++++++++ targets/esp32s3-supermini.json | 4 ++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/machine/board_esp32s3-supermini.go create mode 100644 targets/esp32s3-supermini.json diff --git a/GNUmakefile b/GNUmakefile index 2c640e403b..b0840b6814 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -942,7 +942,15 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/adc @$(MD5SUM) test.bin - + # esp32s3-supermini + $(TINYGO) build -size short -o test.bin -target=esp32s3-supermini examples/blinky1 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-supermini examples/blinkm + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-supermini examples/mcp3008 + @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-supermini examples/adc + @$(MD5SUM) test.bin # esp32s3-wroom1 $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/blinkm @$(MD5SUM) test.bin diff --git a/src/machine/board_esp32c3-supermini.go b/src/machine/board_esp32c3-supermini.go index c180ff0e3e..51fee55c36 100644 --- a/src/machine/board_esp32c3-supermini.go +++ b/src/machine/board_esp32c3-supermini.go @@ -1,6 +1,6 @@ //go:build esp32c3_supermini -// This file contains the pin mappings for the ESP32 supermini boards. +// This file contains the pin mappings for the ESP32C3 supermini board. // // - https://web.archive.org/web/20240805232453/https://dl.artronshop.co.th/ESP32-C3%20SuperMini%20datasheet.pdf diff --git a/src/machine/board_esp32s3-supermini.go b/src/machine/board_esp32s3-supermini.go new file mode 100644 index 0000000000..c377dda75f --- /dev/null +++ b/src/machine/board_esp32s3-supermini.go @@ -0,0 +1,88 @@ +//go:build esp32s3_supermini + +// This file contains the pin mappings for the ESP32S3 supermini board. +// +// - https://www.nologo.tech/product/esp32/esp32s3/esp32s3supermini/esp32S3SuperMini.html + +package machine + +// Digital Pins +const ( + IO1 = GPIO1 + IO2 = GPIO2 + IO3 = GPIO3 + IO4 = GPIO4 + IO5 = GPIO5 + IO6 = GPIO6 + IO7 = GPIO7 + IO8 = GPIO8 + IO9 = GPIO9 + IO10 = GPIO10 + IO11 = GPIO11 + IO12 = GPIO12 + IO13 = GPIO13 + IO14 = GPIO14 + IO15 = GPIO15 + IO16 = GPIO16 + IO17 = GPIO17 + IO18 = GPIO18 + IO21 = GPIO21 + IO33 = GPIO33 + IO34 = GPIO34 + IO35 = GPIO35 + IO36 = GPIO36 + IO37 = GPIO37 + IO38 = GPIO38 + IO39 = GPIO39 + IO40 = GPIO40 + IO41 = GPIO41 + IO42 = GPIO42 + IO43 = GPIO43 + IO44 = GPIO44 + IO45 = GPIO45 + IO46 = GPIO46 + IO47 = GPIO47 + IO48 = GPIO48 +) + +// Built-in LED +const LED = GPIO48 + +// Analog pins +const ( + A1 = GPIO1 + A2 = GPIO2 + A3 = GPIO3 + A4 = GPIO4 + A5 = GPIO5 + A6 = GPIO6 + A7 = GPIO7 + A8 = GPIO8 + A9 = GPIO9 + A10 = GPIO10 + A11 = GPIO11 + A12 = GPIO12 + A13 = GPIO13 + A14 = GPIO14 + A15 = GPIO15 + A16 = GPIO16 +) + +// I2C pins +const ( + SDA_PIN = GPIO8 + SCL_PIN = GPIO9 +) + +// SPI pins +const ( + SPI1_SCK_PIN = GPIO4 + SPI1_MISO_PIN = GPIO5 + SPI1_MOSI_PIN = GPIO6 + SPI1_CS_PIN = GPIO7 + + SPI2_SCK_PIN = NoPin + SPI2_MOSI_PIN = NoPin + SPI2_MISO_PIN = NoPin + SPI2_CS_PIN = NoPin +) diff --git a/targets/esp32s3-supermini.json b/targets/esp32s3-supermini.json new file mode 100644 index 0000000000..bec908eb8b --- /dev/null +++ b/targets/esp32s3-supermini.json @@ -0,0 +1,4 @@ +{ + "inherits": ["esp32s3"], + "build-tags": ["esp32s3_supermini"] +} From b6d974340bcafcab6b5e155735608464870273a1 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 2 Apr 2026 14:09:31 +0200 Subject: [PATCH 79/81] esp32s3: replace inline ISR with full interrupt vector handler Replace the minimal inline ISR (which only disabled INTENABLE) with a full level-1 interrupt handler that saves/restores the interrupted context and dispatches to Go's handleInterrupt. The handler uses callx4 (not callx0) to call into Go code because: - callx0 does not set PS.CALLINC, so the Go function's entry instruction uses stale CALLINC from the interrupted code, causing wrong window rotation and a garbage stack pointer. - callx4 explicitly sets CALLINC=1, and our frame pointer (a1) is outside the callee's register window so it is preserved. Also updates the USB Serial/JTAG ISR to disable INT_ENA (peripheral level) instead of relying on INTENABLE, and adds signalInterrupt to the dispatcher so sleepTicks can be woken by any interrupt. --- src/machine/machine_esp32xx_usb.go | 13 +- src/runtime/interrupt/interrupt_esp32s3.go | 6 + targets/esp32s3-interrupts.S | 135 +++++++++++++++++++-- 3 files changed, 139 insertions(+), 15 deletions(-) diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index 2e36b9b2c9..5066fab677 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -112,10 +112,11 @@ func (usbdev *USB_DEVICE) ensureConfigured() { } // handleInterrupt is called from the CPU interrupt vector when the USB -// peripheral raises an interrupt. For now, just clear the interrupt flag. -// The actual data drain happens in Buffered() via polling — once the ISR -// mechanism is proven, we can move the drain here. +// peripheral raises an interrupt. Disable INT_ENA to prevent the +// level-triggered interrupt from re-asserting immediately (data may +// still be in the FIFO). Buffered() re-enables after draining. func (usbdev *USB_DEVICE) handleInterrupt() { + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0) usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1) } @@ -162,7 +163,7 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { // Buffered returns the number of bytes waiting in the receive ring buffer. // It drains any data sitting in the hardware FIFO and re-enables the -// USB interrupt (which the ISR disables via INTENABLE to prevent a +// peripheral-level USB interrupt (which the ISR disables to prevent a // level-triggered interrupt storm). func (usbdev *USB_DEVICE) Buffered() int { usbdev.ensureConfigured() @@ -171,11 +172,9 @@ func (usbdev *USB_DEVICE) Buffered() int { b := byte(usbdev.Bus.EP1.Get()) usbdev.Buffer.Put(b) } - // Clear pending flags and re-enable the RX interrupt. + // Clear pending flags and re-enable the RX interrupt at the peripheral level. usbdev.Bus.INT_CLR.Set(0xFFFFFFFF) usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) - // Re-enable CPU interrupt 8 in INTENABLE (the ISR clears all bits). - interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable() return int(usbdev.Buffer.Used()) } diff --git a/src/runtime/interrupt/interrupt_esp32s3.go b/src/runtime/interrupt/interrupt_esp32s3.go index 4f7b8b1111..bfb55cb81f 100644 --- a/src/runtime/interrupt/interrupt_esp32s3.go +++ b/src/runtime/interrupt/interrupt_esp32s3.go @@ -115,6 +115,9 @@ func handleInterrupt() { } } + // Signal to sleepTicks that an interrupt has occurred. + signalInterrupt() + inInterrupt = false } @@ -180,6 +183,9 @@ func callHandler(n int) { //go:linkname callHandlers runtime/interrupt.callHandlers func callHandlers(num int) +//go:linkname signalInterrupt runtime.signalInterrupt +func signalInterrupt() + var errInterruptRange = constError("interrupt for ESP32-S3 must be in range 6 through 30") type constError string diff --git a/targets/esp32s3-interrupts.S b/targets/esp32s3-interrupts.S index 9236b5e970..bdc731fd12 100644 --- a/targets/esp32s3-interrupts.S +++ b/targets/esp32s3-interrupts.S @@ -181,18 +181,13 @@ _kernel_vector: // ----------------------------------------------------------------------- // Offset 0x340 — User exception / level-1 interrupt // -// Entire handler is inline — no jump, no stack access, no memory loads. -// Just disable all CPU interrupts via INTENABLE and return. -// Buffered() re-enables INTENABLE after draining the hardware FIFO. +// Save a0 and jump to the full handler below the vector table. // ----------------------------------------------------------------------- .org _vector_table + 0x340 .global _level1_vector _level1_vector: - wsr a0, EXCSAVE1 // save a0 - movi a0, 0 - wsr a0, INTENABLE // disable ALL CPU interrupts - rsr a0, EXCSAVE1 // restore a0 - rfe // return from exception + wsr a0, EXCSAVE1 // save a0 — only scratch register available + j _handle_level1 // jump to full handler (PC-relative, no literal pool) // ----------------------------------------------------------------------- // Offset 0x3C0 — Double exception (stub — loops forever) @@ -200,3 +195,127 @@ _level1_vector: .org _vector_table + 0x3C0 _double_vector: j _double_vector + +// ----------------------------------------------------------------------- +// Level-1 interrupt handler — lives outside the vector table so there +// is no 64-byte size constraint. +// +// Saves the interrupted context on the current stack, clears PS.EXCM +// (so window overflow/underflow work), calls the Go handleInterrupt +// dispatcher, restores context, and returns via rfe. +// +// We call handleInterrupt via callx4 (window rotation by 4). This is +// required because: +// - callx0 does not set PS.CALLINC, so the Go function's "entry" +// instruction would use whatever CALLINC the interrupted code left, +// causing incorrect window rotation and a garbage stack pointer. +// - callx0 puts the return address in a0 with the raw PC (0x42xxx for +// flash), whose top 2 bits (01) cause retw to decrement WindowBase +// by 1 even though nothing was incremented. +// +// With callx4, CALLINC is explicitly set to 1 and the return address +// in a4 has the top 2 bits set to 01 — matching the window rotation +// that entry performs. After retw, WindowBase is correctly restored. +// Our a0..a3 (including a1, the frame pointer) are NOT in the callee's +// register window (callee uses physical regs +4..+19), so a1 is +// preserved across the call without needing EXCSAVE1. +// ----------------------------------------------------------------------- +// Literal data for l32r (must be at a lower address than the l32r). + .balign 4 +.LhandleInterrupt_addr: + .word handleInterrupt + + .global _handle_level1 +_handle_level1: + // --- allocate 96-byte exception frame on the interrupted stack --- + // Layout (offsets from a1 after adjustment): + // 0: a0 4: a1(orig) 8: a2 12: a3 16: a4 20: a5 + // 24: a6 28: a7 32: a8 36: a9 40: a10 44: a11 + // 48: a12 52: a13 56: a14 60: a15 + // 64: SAR 68: EPC1 72: PS + addi a0, a1, -96 // a0 = new frame pointer + s32i a1, a0, 4 // save original a1 (SP) + mov a1, a0 // a1 = frame pointer + + rsr a0, EXCSAVE1 // recover original a0 + s32i a0, a1, 0 // save original a0 + + // Save general registers a2..a15. + s32i a2, a1, 8 + s32i a3, a1, 12 + s32i a4, a1, 16 + s32i a5, a1, 20 + s32i a6, a1, 24 + s32i a7, a1, 28 + s32i a8, a1, 32 + s32i a9, a1, 36 + s32i a10, a1, 40 + s32i a11, a1, 44 + s32i a12, a1, 48 + s32i a13, a1, 52 + s32i a14, a1, 56 + s32i a15, a1, 60 + + // Save special registers. + rsr a2, SAR + s32i a2, a1, 64 + rsr a2, EPC1 + s32i a2, a1, 68 + + // Clear PS.EXCM (bit 4) so window overflow/underflow exceptions work + // during the Go call. Set PS.INTLEVEL=1 to prevent re-entry of + // level-1 interrupts. + rsr a2, PS + s32i a2, a1, 72 // save PS (with EXCM=1 set by hardware) + movi a3, ~0x1F // mask: clear INTLEVEL (bits 0-3) + EXCM (bit 4) + and a2, a2, a3 + movi a3, 1 // INTLEVEL = 1 + or a2, a2, a3 + wsr a2, PS + rsync + + // Call the Go interrupt dispatcher via callx4. + // callx4 explicitly sets PS.CALLINC=1 and puts the return address + // (with top 2 bits = 01) in a4. After entry rotates the window by + // 4, the callee sees: a0 = our a4 (return addr), a1 = our a5 - N. + // We set a5 = our frame pointer so the callee gets a valid stack. + mov a5, a1 + l32r a2, .LhandleInterrupt_addr + callx4 a2 + // After retw, WindowBase is restored. a0..a3 are preserved because + // they are outside the callee's register window. + + // --- restore context --- + + // Restore PS (restores EXCM=1). + l32i a2, a1, 72 + wsr a2, PS + rsync + + // Restore special registers. + l32i a2, a1, 64 + wsr a2, SAR + l32i a2, a1, 68 + wsr a2, EPC1 + + // Restore general registers a15..a2. + l32i a15, a1, 60 + l32i a14, a1, 56 + l32i a13, a1, 52 + l32i a12, a1, 48 + l32i a11, a1, 44 + l32i a10, a1, 40 + l32i a9, a1, 36 + l32i a8, a1, 32 + l32i a7, a1, 28 + l32i a6, a1, 24 + l32i a5, a1, 20 + l32i a4, a1, 16 + l32i a3, a1, 12 + l32i a2, a1, 8 + + // Restore a0 and a1 (a1 must be last since it is the frame pointer). + l32i a0, a1, 0 + l32i a1, a1, 4 // restores original SP (deallocates frame) + + rfe From d82f5cb2fafe8c0e93037cf0d78773aa7e078efe Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 2 Apr 2026 14:09:47 +0200 Subject: [PATCH 80/81] esp32s3: use TIMG0 alarm interrupt for sleepTicks Replace the busy-wait sleepTicks with an interrupt-driven version that sets a TIMG0 timer alarm and waits for the interrupt to fire. The timer alarm handler disables INT_ENA at the peripheral level to prevent level-triggered re-assertion; sleepTicks re-enables it after each wake. This avoids burning CPU cycles during time.Sleep and similar delays. --- src/runtime/runtime_esp32s3.go | 3 ++ src/runtime/runtime_esp32sx.go | 61 +++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index 56b1283fca..474c5a8fad 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -80,6 +80,9 @@ func main() { // Set up the Xtensa interrupt vector table. interruptInit() + // Initialize timer alarm interrupt for the scheduler. + initTimerInterrupt() + // Initialize the heap, call main.main, etc. run() diff --git a/src/runtime/runtime_esp32sx.go b/src/runtime/runtime_esp32sx.go index b30dc37e97..2a92783559 100644 --- a/src/runtime/runtime_esp32sx.go +++ b/src/runtime/runtime_esp32sx.go @@ -5,6 +5,8 @@ package runtime import ( "device/esp" "machine" + "runtime/interrupt" + "runtime/volatile" "unsafe" ) @@ -73,11 +75,62 @@ func ticksToNanoseconds(ticks timeUnit) int64 { return int64(ticks) * 25 } -// sleepTicks busy-waits until the given number of ticks have passed. +// CPU interrupt number used for the TIMG0 timer alarm. +const timerAlarmCPUInterrupt = 9 + +var interruptPending volatile.Register8 + +func signalInterrupt() { + interruptPending.Set(1) +} + +var timerAlarmInterrupt interrupt.Interrupt + +// timerAlarmHandler clears the timer interrupt at the peripheral level +// and disables INT_ENA to prevent level-triggered re-assertion. +func timerAlarmHandler(interrupt.Interrupt) { + esp.TIMG0.INT_ENA_TIMERS.ClearBits(1) + esp.TIMG0.INT_CLR_TIMERS.Set(1) +} + +// initTimerInterrupt routes the TIMG0 timer 0 alarm interrupt to a CPU +// interrupt and registers a handler that clears the alarm flag. +func initTimerInterrupt() { + // Clear any stale timer interrupt before enabling. + esp.TIMG0.INT_CLR_TIMERS.Set(1) + + // Map the TIMG0 T0 peripheral interrupt to a CPU interrupt line. + esp.INTERRUPT_CORE0.SetTG_T0_INT_MAP(timerAlarmCPUInterrupt) + + // Register the interrupt handler and enable it once. + timerAlarmInterrupt = interrupt.New(timerAlarmCPUInterrupt, timerAlarmHandler) + timerAlarmInterrupt.Enable() +} + +// sleepTicks spins until the given number of ticks have elapsed, using the +// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration. func sleepTicks(d timeUnit) { - sleepUntil := ticks() + d - for ticks() < sleepUntil { - // TODO: suspend the CPU to not burn power here unnecessarily. + target := ticks() + d + for ticks() < target { + // Set the alarm to fire at the target tick count. + interruptPending.Set(0) + + esp.TIMG0.T0ALARMLO.Set(uint32(target)) + esp.TIMG0.T0ALARMHI.Set(uint32(target >> 32)) + + // Enable the alarm (auto-clears when alarm fires). + esp.TIMG0.T0CONFIG.SetBits(esp.TIMG_TCONFIG_ALARM_EN) + + // Re-enable the timer interrupt (handler disables INT_ENA). + esp.TIMG0.INT_CLR_TIMERS.Set(1) + esp.TIMG0.INT_ENA_TIMERS.SetBits(1) + + // Wait for any interrupt (timer alarm or other) or timeout. + for interruptPending.Get() == 0 { + if ticks() >= target { + return + } + } } } From 8348b08d6b59b065ada0c1452dd2e50e08e0fbb3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 4 Apr 2026 19:40:21 +0200 Subject: [PATCH 81/81] llvm: update llvm 20 to use latest espressif patches for Xtensa Signed-off-by: deadprogram --- .github/workflows/build-macos.yml | 4 ++-- .github/workflows/linux.yml | 12 ++++++------ .github/workflows/nix.yml | 2 +- .github/workflows/sizediff.yml | 2 +- .github/workflows/windows.yml | 4 ++-- GNUmakefile | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index ac65536291..23d41a61c7 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -46,7 +46,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-source with: - key: llvm-source-20-${{ matrix.os }}-v1 + key: llvm-source-20-${{ matrix.os }}-v2 path: | llvm-project/clang/lib/Headers llvm-project/clang/include @@ -71,7 +71,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-build with: - key: llvm-build-20-${{ matrix.os }}-v2 + key: llvm-build-20-${{ matrix.os }}-v3 path: llvm-build - name: Build LLVM if: steps.cache-llvm-build.outputs.cache-hit != 'true' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 4d161cc5f1..37a3fbc0c8 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -48,7 +48,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-source with: - key: llvm-source-20-linux-alpine-v1 + key: llvm-source-20-linux-alpine-v2 path: | llvm-project/clang/lib/Headers llvm-project/clang/include @@ -73,7 +73,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-build with: - key: llvm-build-20-linux-alpine-v1 + key: llvm-build-20-linux-alpine-v2 path: llvm-build - name: Build LLVM if: steps.cache-llvm-build.outputs.cache-hit != 'true' @@ -197,7 +197,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-source with: - key: llvm-source-20-linux-asserts-v1 + key: llvm-source-20-linux-asserts-v2 path: | llvm-project/clang/lib/Headers llvm-project/clang/include @@ -222,7 +222,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-build with: - key: llvm-build-20-linux-asserts-v1 + key: llvm-build-20-linux-asserts-v2 path: llvm-build - name: Build LLVM if: steps.cache-llvm-build.outputs.cache-hit != 'true' @@ -304,7 +304,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-source with: - key: llvm-source-20-linux-v1 + key: llvm-source-20-linux-v2 path: | llvm-project/clang/lib/Headers llvm-project/clang/include @@ -329,7 +329,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-build with: - key: llvm-build-20-linux-${{ matrix.goarch }}-v1 + key: llvm-build-20-linux-${{ matrix.goarch }}-v2 path: llvm-build - name: Build LLVM if: steps.cache-llvm-build.outputs.cache-hit != 'true' diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 0d3abc2b62..ca8f16500e 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -29,7 +29,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-source with: - key: llvm-source-20-linux-nix-v1 + key: llvm-source-20-linux-nix-v2 path: | llvm-project/compiler-rt - name: Download LLVM source diff --git a/.github/workflows/sizediff.yml b/.github/workflows/sizediff.yml index 8243306c16..155208dfc9 100644 --- a/.github/workflows/sizediff.yml +++ b/.github/workflows/sizediff.yml @@ -30,7 +30,7 @@ jobs: uses: actions/cache@v5 id: cache-llvm-source with: - key: llvm-source-20-sizediff-v1 + key: llvm-source-20-sizediff-v2 path: | llvm-project/compiler-rt - name: Download LLVM source diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a989435c64..06eccd9fa2 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -46,7 +46,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-source with: - key: llvm-source-20-windows-v1 + key: llvm-source-20-windows-v2 path: | llvm-project/clang/lib/Headers llvm-project/clang/include @@ -71,7 +71,7 @@ jobs: uses: actions/cache/restore@v5 id: cache-llvm-build with: - key: llvm-build-20-windows-v2 + key: llvm-build-20-windows-v3 path: llvm-build - name: Build LLVM if: steps.cache-llvm-build.outputs.cache-hit != 'true' diff --git a/GNUmakefile b/GNUmakefile index b0840b6814..f3961a65d7 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -263,7 +263,7 @@ gen-device-renesas: build/gen-device-svd GO111MODULE=off $(GO) fmt ./src/device/renesas $(LLVM_PROJECTDIR)/llvm: - git clone -b tinygo_20.x --depth=1 https://github.com/tinygo-org/llvm-project $(LLVM_PROJECTDIR) + git clone -b tinygo_20.x.1 --depth=1 https://github.com/tinygo-org/llvm-project $(LLVM_PROJECTDIR) llvm-source: $(LLVM_PROJECTDIR)/llvm ## Get LLVM sources # Configure LLVM.