From f5b678b330f41ee8a8a9837fa3ea3912ce184308 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 29 Sep 2026 16:33:47 +0200 Subject: [PATCH 1/2] machine/esp32xx: add I2S support Adds I2S for the ESP32-C3, ESP32-C6 and ESP32-S3 (I2S0 only). Samples are copied into a ring of RAM buffers. A GDMA EOF interrupt points each of the three circular descriptors at the next full buffer or at silence, so playback has no gaps between writes. The API is the same as on nRF52. The CPU interrupt lines do not overlap the ones espradio uses for WiFi and BLE. Tested on a xiao-esp32c3 with a PCM5102 DAC at 22.05, 44.1 and 48 kHz, in mono and stereo, also while BLE advertises and while WiFi scans. The C6 and S3 build but are not tested on hardware. --- src/machine/i2s.go | 2 +- src/machine/machine_esp32c3_i2s.go | 78 ++++ src/machine/machine_esp32c6_i2s.go | 83 ++++ src/machine/machine_esp32s3_i2s.go | 79 ++++ src/machine/machine_esp32xx_i2s.go | 609 +++++++++++++++++++++++++++++ 5 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 src/machine/machine_esp32c3_i2s.go create mode 100644 src/machine/machine_esp32c6_i2s.go create mode 100644 src/machine/machine_esp32s3_i2s.go create mode 100644 src/machine/machine_esp32xx_i2s.go diff --git a/src/machine/i2s.go b/src/machine/i2s.go index 1b4bbfc6ee..63209fa1eb 100644 --- a/src/machine/i2s.go +++ b/src/machine/i2s.go @@ -1,4 +1,4 @@ -//go:build (sam && atsamd21) || nrf52 || nrf52833 || nrf52840 +//go:build (sam && atsamd21) || nrf52 || nrf52833 || nrf52840 || esp32c3 || esp32c6 || esp32s3 // This is the definition for I2S bus functions. // Actual implementations if available for any given hardware diff --git a/src/machine/machine_esp32c3_i2s.go b/src/machine/machine_esp32c3_i2s.go new file mode 100644 index 0000000000..9eb981d750 --- /dev/null +++ b/src/machine/machine_esp32c3_i2s.go @@ -0,0 +1,78 @@ +//go:build esp32c3 + +package machine + +import ( + "device/esp" + "runtime/interrupt" +) + +// GPIO matrix signals (IDF soc/esp32c3/include/soc/gpio_sig_map.h). +const ( + i2sSignalTxBCK = 13 + i2sSignalTxWS = 14 + i2sSignalTxSD = 15 + i2sSignalRxSD = 15 + i2sSignalRxBCK = 16 + i2sSignalRxWS = 17 +) + +const cpuInterruptFromI2S = 11 + +// The C3 has one interrupt register for both directions of a channel. +const ( + gdmaOutEOF = esp.DMA_INT_RAW_CH0_OUT_EOF + gdmaInSucEOF = esp.DMA_INT_RAW_CH0_IN_SUC_EOF +) + +var i2sGDMARegs = i2sGDMA{ + outConf0: &esp.DMA.OUT_CONF0_CH0, + outLink: &esp.DMA.OUT_LINK_CH0, + outPeriSel: &esp.DMA.OUT_PERI_SEL_CH0, + outIntEna: &esp.DMA.INT_ENA_CH0, + outIntClr: &esp.DMA.INT_CLR_CH0, + outIntSt: &esp.DMA.INT_ST_CH0, + outEOFAddr: &esp.DMA.OUT_EOF_DES_ADDR_CH0, + inConf0: &esp.DMA.IN_CONF0_CH0, + inLink: &esp.DMA.IN_LINK_CH0, + inPeriSel: &esp.DMA.IN_PERI_SEL_CH0, + inIntEna: &esp.DMA.INT_ENA_CH0, + inIntClr: &esp.DMA.INT_CLR_CH0, + inIntSt: &esp.DMA.INT_ST_CH0, + inEOFAddr: &esp.DMA.IN_SUC_EOF_DES_ADDR_CH0, +} + +// i2sEnableClocks enables and resets I2S and enables GDMA. The one I2S block +// uses the I2S1 bits (IDF hal/esp32c3/include/hal/clk_gate_ll.h line 36). +func i2sEnableClocks() { + esp.SYSTEM.SetPERIP_CLK_EN0_I2S1_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_I2S1_RST(1) + esp.SYSTEM.SetPERIP_RST_EN0_I2S1_RST(0) + if esp.SYSTEM.GetPERIP_CLK_EN1_DMA_CLK_EN() == 0 { + esp.SYSTEM.SetPERIP_CLK_EN1_DMA_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN1_DMA_RST(1) + esp.SYSTEM.SetPERIP_RST_EN1_DMA_RST(0) + } + esp.DMA.MISC_CONF.SetBits(esp.DMA_MISC_CONF_CLK_EN) + esp.I2S0.SetTX_CLKM_CONF_CLK_EN(1) +} + +func i2sSetTxClock(div, x, y, z, yn1 uint32) { + i2sSetClockDiv(&esp.I2S0.TX_CLKM_CONF, &esp.I2S0.TX_CLKM_DIV_CONF, 0, div, x, y, z, yn1) + esp.I2S0.SetTX_CLKM_CONF_TX_CLK_SEL(i2sPLL160MClockSel) + esp.I2S0.SetTX_CLKM_CONF_TX_CLK_ACTIVE(1) + esp.I2S0.SetRX_CLKM_CONF_MCLK_SEL(0) +} + +func i2sSetRxClock(div, x, y, z, yn1 uint32) { + i2sSetClockDiv(&esp.I2S0.RX_CLKM_CONF, &esp.I2S0.RX_CLKM_DIV_CONF, 0, div, x, y, z, yn1) + esp.I2S0.SetRX_CLKM_CONF_RX_CLK_SEL(i2sPLL160MClockSel) + esp.I2S0.SetRX_CLKM_CONF_RX_CLK_ACTIVE(1) +} + +func i2sEnableInterrupt() { + esp.INTERRUPT_CORE0.SetDMA_CH0_INT_MAP(cpuInterruptFromI2S) + interrupt.New(cpuInterruptFromI2S, func(interrupt.Interrupt) { + I2S0.handleInterrupt() + }).Enable() +} diff --git a/src/machine/machine_esp32c6_i2s.go b/src/machine/machine_esp32c6_i2s.go new file mode 100644 index 0000000000..93d1422d26 --- /dev/null +++ b/src/machine/machine_esp32c6_i2s.go @@ -0,0 +1,83 @@ +//go:build esp32c6 + +package machine + +import ( + "device/esp" + "runtime/interrupt" + "runtime/volatile" + "unsafe" +) + +// GPIO matrix signals (IDF soc/esp32c6/include/soc/gpio_sig_map.h). +const ( + i2sSignalTxBCK = 13 + i2sSignalTxWS = 14 + i2sSignalTxSD = 15 + i2sSignalRxSD = 15 + i2sSignalRxBCK = 16 + i2sSignalRxWS = 17 +) + +const cpuInterruptFromI2S = 11 + +// Interrupt bits from IDF soc/esp32c6/include/soc/gdma_struct.h. +const ( + gdmaOutEOF = 1 << 1 + gdmaInSucEOF = 1 << 1 +) + +// The generated DMA_Type puts OUT_CONF0_CH0 at 0x190, but it is at 0xD0 +// (IDF soc/esp32c6/include/soc/gdma_reg.h line 1650). +var i2sGDMARegs = i2sGDMA{ + outConf0: (*volatile.Register32)(unsafe.Add(unsafe.Pointer(esp.DMA), 0xD0)), + outLink: &esp.DMA.OUT_LINK_CH0, + outPeriSel: &esp.DMA.OUT_PERI_SEL_CH0, + outIntEna: &esp.DMA.OUT_INT_ENA_CH0, + outIntClr: &esp.DMA.OUT_INT_CLR_CH0, + outIntSt: &esp.DMA.OUT_INT_ST_CH0, + outEOFAddr: &esp.DMA.OUT_EOF_DES_ADDR_CH0, + inConf0: &esp.DMA.IN_CONF0_CH0, + inLink: &esp.DMA.IN_LINK_CH0, + inPeriSel: &esp.DMA.IN_PERI_SEL_CH0, + inIntEna: &esp.DMA.IN_INT_ENA_CH0, + inIntClr: &esp.DMA.IN_INT_CLR_CH0, + inIntSt: &esp.DMA.IN_INT_ST_CH0, + inEOFAddr: &esp.DMA.IN_SUC_EOF_DES_ADDR_CH0, +} + +func i2sEnableClocks() { + esp.PCR.SetI2S_CONF_I2S_CLK_EN(1) + esp.PCR.SetI2S_CONF_I2S_RST_EN(1) + esp.PCR.SetI2S_CONF_I2S_RST_EN(0) + if esp.PCR.GetGDMA_CONF_GDMA_CLK_EN() == 0 { + esp.PCR.SetGDMA_CONF_GDMA_CLK_EN(1) + esp.PCR.SetGDMA_CONF_GDMA_RST_EN(1) + esp.PCR.SetGDMA_CONF_GDMA_RST_EN(0) + } + esp.DMA.MISC_CONF.SetBits(esp.DMA_MISC_CONF_CLK_EN) +} + +// The C6 I2S clocks are in PCR (IDF hal/esp32c6/include/hal/i2s_ll.h). +func i2sSetTxClock(div, x, y, z, yn1 uint32) { + i2sSetClockDiv(&esp.PCR.I2S_TX_CLKM_CONF, &esp.PCR.I2S_TX_CLKM_DIV_CONF, + esp.PCR_I2S_TX_CLKM_CONF_I2S_TX_CLKM_DIV_NUM_Pos, div, x, y, z, yn1) + esp.PCR.SetI2S_TX_CLKM_CONF_I2S_TX_CLKM_SEL(i2sPLL160MClockSel) + esp.PCR.SetI2S_TX_CLKM_CONF_I2S_TX_CLKM_EN(1) + esp.PCR.SetI2S_RX_CLKM_CONF_I2S_MCLK_SEL(0) +} + +func i2sSetRxClock(div, x, y, z, yn1 uint32) { + i2sSetClockDiv(&esp.PCR.I2S_RX_CLKM_CONF, &esp.PCR.I2S_RX_CLKM_DIV_CONF, + esp.PCR_I2S_RX_CLKM_CONF_I2S_RX_CLKM_DIV_NUM_Pos, div, x, y, z, yn1) + esp.PCR.SetI2S_RX_CLKM_CONF_I2S_RX_CLKM_SEL(i2sPLL160MClockSel) + esp.PCR.SetI2S_RX_CLKM_CONF_I2S_RX_CLKM_EN(1) +} + +func i2sEnableInterrupt() { + esp.INTERRUPT_CORE0.DMA_OUT_CH0_INTR_MAP.Set(cpuInterruptFromI2S) + esp.INTERRUPT_CORE0.DMA_IN_CH0_INTR_MAP.Set(cpuInterruptFromI2S) + interrupt.New(cpuInterruptFromI2S, func(interrupt.Interrupt) { + I2S0.handleInterrupt() + }).Enable() +} diff --git a/src/machine/machine_esp32s3_i2s.go b/src/machine/machine_esp32s3_i2s.go new file mode 100644 index 0000000000..b558b216e0 --- /dev/null +++ b/src/machine/machine_esp32s3_i2s.go @@ -0,0 +1,79 @@ +//go:build esp32s3 + +package machine + +import ( + "device/esp" + "runtime/interrupt" +) + +// GPIO matrix signals for I2S0 (IDF soc/esp32s3/include/soc/gpio_sig_map.h). +const ( + i2sSignalTxBCK = 22 + i2sSignalTxWS = 24 + i2sSignalTxSD = 25 + i2sSignalRxSD = 25 + i2sSignalRxBCK = 26 + i2sSignalRxWS = 27 +) + +// Line 18 is a free level 1 interrupt. espradio uses 12, 13 and 17 +// (ESP32-S3 TRM, table "CPU Interrupts"). +const cpuInterruptFromI2S = 18 + +// Interrupt bits from IDF soc/esp32s3/include/soc/gdma_struct.h. +const ( + gdmaOutEOF = 1 << 1 + gdmaInSucEOF = 1 << 1 +) + +var i2sGDMARegs = i2sGDMA{ + outConf0: &esp.DMA.OUT_CONF0_CH0, + outLink: &esp.DMA.OUT_LINK_CH0, + outPeriSel: &esp.DMA.OUT_PERI_SEL_CH0, + outIntEna: &esp.DMA.OUT_INT_ENA_CH0, + outIntClr: &esp.DMA.OUT_INT_CLR_CH0, + outIntSt: &esp.DMA.OUT_INT_ST_CH0, + outEOFAddr: &esp.DMA.OUT_EOF_DES_ADDR_CH0, + inConf0: &esp.DMA.IN_CONF0_CH0, + inLink: &esp.DMA.IN_LINK_CH0, + inPeriSel: &esp.DMA.IN_PERI_SEL_CH0, + inIntEna: &esp.DMA.IN_INT_ENA_CH0, + inIntClr: &esp.DMA.IN_INT_CLR_CH0, + inIntSt: &esp.DMA.IN_INT_ST_CH0, + inEOFAddr: &esp.DMA.IN_SUC_EOF_DES_ADDR_CH0, +} + +func i2sEnableClocks() { + esp.SYSTEM.SetPERIP_CLK_EN0_I2S0_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_I2S0_RST(1) + esp.SYSTEM.SetPERIP_RST_EN0_I2S0_RST(0) + if esp.SYSTEM.GetPERIP_CLK_EN1_DMA_CLK_EN() == 0 { + esp.SYSTEM.SetPERIP_CLK_EN1_DMA_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN1_DMA_RST(1) + esp.SYSTEM.SetPERIP_RST_EN1_DMA_RST(0) + } + esp.DMA.MISC_CONF.SetBits(esp.DMA_MISC_CONF_CLK_EN) + esp.I2S0.SetTX_CLKM_CONF_CLK_EN(1) +} + +func i2sSetTxClock(div, x, y, z, yn1 uint32) { + i2sSetClockDiv(&esp.I2S0.TX_CLKM_CONF, &esp.I2S0.TX_CLKM_DIV_CONF, 0, div, x, y, z, yn1) + esp.I2S0.SetTX_CLKM_CONF_TX_CLK_SEL(i2sPLL160MClockSel) + esp.I2S0.SetTX_CLKM_CONF_TX_CLK_ACTIVE(1) + esp.I2S0.SetRX_CLKM_CONF_MCLK_SEL(0) +} + +func i2sSetRxClock(div, x, y, z, yn1 uint32) { + i2sSetClockDiv(&esp.I2S0.RX_CLKM_CONF, &esp.I2S0.RX_CLKM_DIV_CONF, 0, div, x, y, z, yn1) + esp.I2S0.SetRX_CLKM_CONF_RX_CLK_SEL(i2sPLL160MClockSel) + esp.I2S0.SetRX_CLKM_CONF_RX_CLK_ACTIVE(1) +} + +func i2sEnableInterrupt() { + esp.INTERRUPT_CORE0.SetDMA_OUT_CH0_INT_MAP(cpuInterruptFromI2S) + esp.INTERRUPT_CORE0.SetDMA_IN_CH0_INT_MAP(cpuInterruptFromI2S) + interrupt.New(cpuInterruptFromI2S, func(interrupt.Interrupt) { + I2S0.handleInterrupt() + }).Enable() +} diff --git a/src/machine/machine_esp32xx_i2s.go b/src/machine/machine_esp32xx_i2s.go new file mode 100644 index 0000000000..9cd9481490 --- /dev/null +++ b/src/machine/machine_esp32xx_i2s.go @@ -0,0 +1,609 @@ +//go:build esp32c3 || esp32c6 || esp32s3 + +package machine + +import ( + "device/esp" + "errors" + "runtime/interrupt" + "runtime/volatile" + "unsafe" +) + +// I2S on the ESP32-C3, ESP32-C6 and ESP32-S3 (I2S0 only). +// +// Samples are copied into a ring of RAM buffers that GDMA channel 0 plays or +// fills in the background, so audio keeps running between calls. +type I2S struct { + Bus *esp.I2S_Type + + config I2SConfig + configured bool + running bool + + tx i2sRing + rx i2sRing + silence []uint32 + scratch []uint32 + writing volatile.Register8 + + txDesc [i2sDescCount]i2sDescriptor + rxDesc [i2sDescCount]i2sDescriptor + txData [i2sDescCount]bool + rxData [i2sDescCount]bool + txNext int + rxNext int +} + +// i2sRing tracks buffers by counters that only increase. [done, queued) is +// owned by the hardware and [queued, filled) is ready for the hardware (TX) +// or for Read (RX). +type i2sRing struct { + buf []uint32 + done uint32 + queued uint32 + filled uint32 + pos int +} + +// i2sDescriptor is a GDMA link descriptor (IDF hal/include/hal/dma_types.h). +type i2sDescriptor struct { + dw0 uint32 + buf uint32 + next uint32 +} + +const ( + i2sBufferWords = 128 + i2sBufferCount = 4 + i2sBufferBytes = i2sBufferWords * 4 + + // GDMA can read the next descriptor early, so three descriptors leave one + // buffer of time to update a finished descriptor. + i2sDescCount = 3 + + i2sDescOwnerDMA = 1 << 31 + i2sDescSucEOF = 1 << 30 + i2sDescLenPos = 12 +) + +// GDMA channel bits, the same on C3, C6 and S3 (IDF soc/*/include/soc/gdma_struct.h). +const ( + gdmaConf0Rst = 1 << 0 + gdmaInConf0Burst = 1<<2 | 1<<3 + gdmaOutConf0Burst = 1<<4 | 1<<5 + gdmaLinkAddrMask = 0xfffff + gdmaOutLinkStop = 1 << 20 + gdmaOutLinkStart = 1 << 21 + gdmaInLinkStop = 1 << 21 + gdmaInLinkStart = 1 << 22 + gdmaPeriSelI2S0 = 3 +) + +// I2S clock values from IDF hal/esp32c3/include/hal/i2s_ll.h. +const ( + i2sPLL160MClockSel = 2 + i2sPLL160MClockFreq = 160000000 + i2sMCLKMultiple = 256 + i2sMCLKDividerMax = 511 + i2sMCLKDivNumMax = 255 + i2sUpdateTimeoutLoop = 100000 +) + +// i2sGDMA holds the GDMA channel 0 registers. The layout differs per chip. +type i2sGDMA struct { + outConf0, outLink, outPeriSel, outIntEna, outIntClr, outIntSt, outEOFAddr *volatile.Register32 + inConf0, inLink, inPeriSel, inIntEna, inIntClr, inIntSt, inEOFAddr *volatile.Register32 +} + +var I2S0 = I2S{Bus: esp.I2S0} + +var ( + errI2SPins = errors.New("i2s: missing pin") + errI2SUnsupported = errors.New("i2s: unsupported configuration") + errI2SNotRunning = errors.New("i2s: not running") +) + +// Configure sets up the I2S peripheral and starts it. +func (i2s *I2S) Configure(config I2SConfig) error { + if i2s.configured { + i2s.stop() + i2s.configured = false + } + + if config.DataFormat == I2SDataFormatDefault { + config.DataFormat = I2SDataFormat16bit + } + if config.DataFormat != I2SDataFormat16bit && config.DataFormat != I2SDataFormat32bit { + return errI2SUnsupported + } + if config.AudioFrequency == 0 { + config.AudioFrequency = 44100 + } + if config.Mode == I2SModePDM || config.MainClockOutput { + return errI2SUnsupported + } + + tx := config.Mode == I2SModeSource || config.Mode == I2SModeSourceReceiver + rx := config.Mode == I2SModeReceiver || config.Mode == I2SModeSourceReceiver + if config.SCK == NoPin || config.WS == NoPin || (tx && config.SDO == NoPin) || (rx && config.SDI == NoPin) { + return errI2SPins + } + slave := config.ClockSource == I2SClockSourceExternal + + i2sEnableClocks() + + bits := uint32(config.DataFormat) - 1 + txConf := uint32(esp.I2S_TX_CONF_TX_TDM_EN | esp.I2S_TX_CONF_TX_PCM_BYPASS | esp.I2S_TX_CONF_TX_STOP_EN | + esp.I2S_TX_CONF_TX_LEFT_ALIGN | esp.I2S_TX_CONF_TX_MONO_FST_VLD) + rxConf := uint32(esp.I2S_RX_CONF_RX_TDM_EN | esp.I2S_RX_CONF_RX_PCM_BYPASS | esp.I2S_RX_CONF_RX_LEFT_ALIGN | + esp.I2S_RX_CONF_RX_MONO_FST_VLD | 2< ma { + diff = mb - ma + } + if diff < best { + denom, numer, best = a, b, diff + } + if diff == 0 { + break + } + } + } else { + integ++ + } + } + if integ < 2 || integ > i2sMCLKDivNumMax { + return ErrInvalidSampleFrequency + } + + // Same as i2s_ll_tx_set_mclk in IDF hal/esp32c3/include/hal/i2s_ll.h. + var x, y, z, yn1 uint64 + if denom != 0 && numer != 0 { + if numer*2 > denom { + yn1 = 1 + z = denom - numer + } else { + z = numer + } + x = denom/z - 1 + y = denom % z + } + + bckDiv := uint32(i2sMCLKMultiple/(2*uint32(i2s.config.DataFormat))) - 1 + i2sSetTxClock(uint32(integ), uint32(x), uint32(y), uint32(z), uint32(yn1)) + i2sSetRxClock(uint32(integ), uint32(x), uint32(y), uint32(z), uint32(yn1)) + i2s.Bus.SetTX_CONF1_TX_BCK_DIV_NUM(bckDiv) + i2s.Bus.SetRX_CONF1_RX_BCK_DIV_NUM(bckDiv) + return nil +} + +// WriteMono plays each sample on both channels. It returns once all samples +// are queued. Only 16-bit samples are supported. +func (i2s *I2S) WriteMono(b []uint16) (int, error) { + if i2s.config.DataFormat != I2SDataFormat16bit { + return 0, errI2SUnsupported + } + return i2sWrite(i2s, b, true) +} + +// WriteStereo queues stereo frames. At 16-bit each value is one frame with +// left in the low half. At 32-bit each value is one sample, left then right. +func (i2s *I2S) WriteStereo(b []uint32) (int, error) { + return i2sWrite(i2s, b, false) +} + +// ReadMono reads the left channel. Only 16-bit samples are supported. +func (i2s *I2S) ReadMono(b []uint16) (int, error) { + if i2s.config.DataFormat != I2SDataFormat16bit { + return 0, errI2SUnsupported + } + return i2sRead(i2s, b) +} + +// ReadStereo reads frames in the same layout WriteStereo uses. +func (i2s *I2S) ReadStereo(b []uint32) (int, error) { + return i2sRead(i2s, b) +} + +func i2sWrite[T uint16 | uint32](i2s *I2S, b []T, mono bool) (int, error) { + if !i2s.txEnabled() { + return 0, errI2SUnsupported + } + if !i2s.running { + return 0, errI2SNotRunning + } + r := &i2s.tx + i2s.writing.Set(1) + defer i2s.writing.Set(0) + for i := 0; i < len(b); { + if r.pos == 0 { + for volatile.LoadUint32(&r.filled)-volatile.LoadUint32(&r.done) >= i2sBufferCount { + if !i2s.running { + return i, errI2SNotRunning + } + gosched() + } + } + buf := r.slot(r.filled) + n := min(len(buf)-r.pos, len(b)-i) + for j := 0; j < n; j++ { + v := uint32(b[i+j]) + if mono { + v |= v << 16 + } + buf[r.pos+j] = v + } + i += n + r.pos += n + if r.pos == len(buf) { + mask := interrupt.Disable() + r.pos = 0 + r.filled++ + interrupt.Restore(mask) + } + } + return len(b), nil +} + +func i2sRead[T uint16 | uint32](i2s *I2S, b []T) (int, error) { + if !i2s.rxEnabled() { + return 0, errI2SUnsupported + } + if !i2s.running { + return 0, errI2SNotRunning + } + r := &i2s.rx + for i := 0; i < len(b); { + for volatile.LoadUint32(&r.filled) == volatile.LoadUint32(&r.queued) { + if !i2s.running { + return i, errI2SNotRunning + } + gosched() + } + buf := r.slot(r.queued) + n := min(len(buf)-r.pos, len(b)-i) + for j := 0; j < n; j++ { + b[i+j] = T(buf[r.pos+j]) + } + i += n + r.pos += n + if r.pos == len(buf) { + mask := interrupt.Disable() + r.pos = 0 + r.queued++ + interrupt.Restore(mask) + } + } + return len(b), nil +} + +func (i2s *I2S) txEnabled() bool { + return i2s.config.Mode == I2SModeSource || i2s.config.Mode == I2SModeSourceReceiver +} + +func (i2s *I2S) rxEnabled() bool { + return i2s.config.Mode == I2SModeReceiver || i2s.config.Mode == I2SModeSourceReceiver +} + +func (r *i2sRing) slot(n uint32) []uint32 { + start := (n % i2sBufferCount) * i2sBufferWords + return r.buf[start : start+i2sBufferWords] +} + +func (r *i2sRing) reset() { + r.done, r.queued, r.filled, r.pos = 0, 0, 0, 0 +} + +// TX and RX share one edge-triggered CPU interrupt on C3 and C6. Loop until both +// are clear, otherwise a late event keeps the line high and no new edge comes. +func (i2s *I2S) handleInterrupt() { + g := &i2sGDMARegs + for { + out := g.outIntSt.Get()&gdmaOutEOF != 0 + in := g.inIntSt.Get()&gdmaInSucEOF != 0 + if !out && !in { + return + } + if out { + g.outIntClr.Set(gdmaOutEOF) + last := i2sDescIndex(&i2s.txDesc, g.outEOFAddr.Get()) + for last >= 0 { + d := i2s.txNext + i2s.txNext = (d + 1) % i2sDescCount + i2s.nextTx(d) + if d == last { + break + } + } + } + if in { + g.inIntClr.Set(gdmaInSucEOF) + last := i2sDescIndex(&i2s.rxDesc, g.inEOFAddr.Get()) + for last >= 0 { + d := i2s.rxNext + i2s.rxNext = (d + 1) % i2sDescCount + i2s.nextRx(d) + if d == last { + break + } + } + } + } +} + +func i2sDescIndex(desc *[i2sDescCount]i2sDescriptor, addr uint32) int { + for i := range desc { + if uint32(uintptr(unsafe.Pointer(&desc[i]))) == addr { + return i + } + } + return -1 +} + +// nextTx runs when GDMA has read all of descriptor d. The descriptor gets +// the next full buffer or silence. +func (i2s *I2S) nextTx(d int) { + r := &i2s.tx + if i2s.txData[d] { + r.done++ + } + if r.queued == r.filled && r.pos > 0 && i2s.writing.Get() == 0 { + clear(r.slot(r.filled)[r.pos:]) + r.pos = 0 + r.filled++ + } + buf := i2s.silence + i2s.txData[d] = r.queued != r.filled + if i2s.txData[d] { + buf = r.slot(r.queued) + r.queued++ + } + i2s.setTxDesc(d, buf) +} + +// nextRx runs when GDMA has filled descriptor d. RX counters use done for +// buffers given to the hardware, filled for buffers ready to read and queued +// for buffers already read. +func (i2s *I2S) nextRx(d int) { + r := &i2s.rx + if i2s.rxData[d] { + r.filled++ + } + // Read is too slow when all buffers are in use, so drop these samples. + buf := i2s.scratch + i2s.rxData[d] = r.done-r.queued < i2sBufferCount + if i2s.rxData[d] { + buf = r.slot(r.done) + r.done++ + } + i2s.setRxDesc(d, buf) +} + +func (i2s *I2S) setTxDesc(d int, buf []uint32) { + volatile.StoreUint32(&i2s.txDesc[d].buf, uint32(uintptr(unsafe.Pointer(&buf[0])))) + volatile.StoreUint32(&i2s.txDesc[d].dw0, i2sDescOwnerDMA|i2sDescSucEOF|i2sBufferBytes< Date: Tue, 29 Sep 2026 16:33:47 +0200 Subject: [PATCH 2/2] examples: add esp32xx pins to i2s-tone Uses GPIO3 for BCK, GPIO4 for WS and GPIO5 for DIN. Also adds i2s-tone to the smoke tests for xiao-esp32c3, xiao-esp32c6 and xiao-esp32s3. --- make/smoketest.mk | 6 ++++++ src/examples/i2s-tone/esp32xx.go | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/examples/i2s-tone/esp32xx.go diff --git a/make/smoketest.mk b/make/smoketest.mk index 1985373c94..dde87e1810 100644 --- a/make/smoketest.mk +++ b/make/smoketest.mk @@ -516,6 +516,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32c6 examples/blinkm @$(MD5SUM) $(SMOKE_OUT).bin + $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32c6 examples/i2s-tone + @$(MD5SUM) $(SMOKE_OUT).bin # esp32h2-devkitm-1 $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32h2-devkitm-1 examples/machinetest @$(MD5SUM) $(SMOKE_OUT).bin @@ -532,6 +534,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32s3 examples/adc @$(MD5SUM) $(SMOKE_OUT).bin + $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32s3 examples/i2s-tone + @$(MD5SUM) $(SMOKE_OUT).bin # esp32s3-supermini $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32s3-supermini examples/blinky1 @$(MD5SUM) $(SMOKE_OUT).bin @@ -567,6 +571,8 @@ smoketest-riscv: | build/smoke @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32c3 examples/machinetest @$(MD5SUM) $(SMOKE_OUT).bin + $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32c3 examples/i2s-tone + @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32-c3-devkit-rust-1 examples/blinky1 @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32c3-12f examples/blinky1 diff --git a/src/examples/i2s-tone/esp32xx.go b/src/examples/i2s-tone/esp32xx.go new file mode 100644 index 0000000000..b51a61458c --- /dev/null +++ b/src/examples/i2s-tone/esp32xx.go @@ -0,0 +1,11 @@ +//go:build esp32c3 || esp32c6 || esp32s3 + +package main + +import "machine" + +const ( + pinSCK = machine.GPIO3 + pinWS = machine.GPIO4 + pinSDO = machine.GPIO5 +)