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 +) 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<