renode: Initial implementation of Matcha I2S driver Change-Id: Id1f04d2b8bd64cddc08531d40ccc2903b0fa719b
diff --git a/platforms/shodan.repl b/platforms/shodan.repl index 07c8135..b848959 100644 --- a/platforms/shodan.repl +++ b/platforms/shodan.repl
@@ -41,3 +41,12 @@ // ISP [‘h4200_0000 - ‘h4200_FFFF) 64KB ISP registers // DMA Ctrl [‘h4201_0000 - ‘h4201_FFFF) 64KB DMA control interface // DSP Ctrl [‘h4202_0000 - ‘h4202_FFFF) 64KB Audio DSP control interface + +// TOP_MATCHA_I2S0_BASE_ADDR @ top_matcha.h +// I2S0 ['h5410_0000 - 'h5410_0040) 64B registers +i2s0 : Sound.MatchaI2S @ sysbus 0x54100000 + TxWatermarkIRQ -> smc_plic@39 // kTopMatchaPlicIrqIdI2s0TxWatermark @ top_matcha.h + RxWatermarkIRQ -> smc_plic@40 // kTopMatchaPlicIrqIdI2s0RxWatermark @ top_matcha.h + TxEmptyIRQ -> smc_plic@41 // kTopMatchaPlicIrqIdI2s0TxEmpty @ top_matcha.h + RxOverflowIRQ -> smc_plic@42 // kTopMatchaPlicIrqIdI2s0RxOverflow @ top_matcha.h +
diff --git a/platforms/smc.repl b/platforms/smc.repl index e28f711..c9835f9 100644 --- a/platforms/smc.repl +++ b/platforms/smc.repl
@@ -43,8 +43,8 @@ // Bit 11 corresponds MEIP and bit 9 to SEIP. 0 -> cpu1@11 1 -> cpu1@9 - // Must be >= kTopMatchaPlicIrqIdLastSmc in top_matcha.h - numberOfSources: 34 + // Must be kTopMatchaPlicIrqIdLastSmc + 1 from top_matcha.h + numberOfSources: 43 numberOfContexts: 2 // MEIP and SEIP uart5: UART.OpenTitan_UART @ sysbus 0x50000000
diff --git a/shodan.resc b/shodan.resc index 1c9a0c7..4892d82 100644 --- a/shodan.resc +++ b/shodan.resc
@@ -16,6 +16,8 @@ include @sim/config/shodan_infrastructure/Mailbox.cs include @sim/renode/tools/sel4_extensions/seL4Extensions.cs include @sim/config/shodan_infrastructure/AddressRangeStub.cs +include @sim/config/shodan_infrastructure/MatchaI2S.cs +EnsureTypeIsLoaded "Antmicro.Renode.Peripherals.Sound.MatchaI2S" $repl_file ?= @sim/config/platforms/shodan.repl @@ -58,6 +60,16 @@ sysbus LoadBinary $sc_bin $eflash_address sysbus LoadSymbolsFrom $kernel +# If we have a I2S peripheral, setup an audio file that we can use +# for sampling data. Note that the format for these files is raw +# sample data, left channel followed by right. +$i2s_mic_audio_file ?= @sim/config/shodan_infrastructure/test.raw +$i2s_speaker_audio_file ?= @out/speaker.raw + +# Uncomment these lines to enable audio +# i2s0 InputFile $i2s_mic_audio_file +# i2s0 OutputFile $i2s_speaker_audio_file + # Start cpu0 at the bootrom reset vector, which is stored immediately after the # bootrom interrupt vector table at 0x8080. # (see https://ibex-core.readthedocs.io/en/latest/03_reference/exception_interrupts.html for details)
diff --git a/shodan_infrastructure/MatchaI2S.cs b/shodan_infrastructure/MatchaI2S.cs new file mode 100644 index 0000000..56def9d --- /dev/null +++ b/shodan_infrastructure/MatchaI2S.cs
@@ -0,0 +1,330 @@ +// +// Copyright (c) 2010-2023 Google +// +// This file is licensed under the MIT License. +// Full license text is available in 'licenses/MIT.txt'. +// +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Exceptions; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Miscellaneous; +using Antmicro.Renode.Peripherals.Bus; +using Antmicro.Renode.Utilities; +using Antmicro.Renode.Debugging; +using Antmicro.Renode.Sound; + +namespace Antmicro.Renode.Peripherals.Sound +{ + public class Fifo + { + public void Write(ulong value) + { + lock (synclock) + { + if (!Full()) + { + fifo[cursor_w] = value; + cursor_w = (cursor_w + 1) & FIFO_MASK; + count++; + } + } + } + + public ulong Read() + { + ulong result = 0; + + lock (synclock) + { + if (!Empty()) + { + result = fifo[cursor_r]; + cursor_r = (cursor_r + 1) & FIFO_MASK; + count--; + } + + return result; + } + } + + public bool Empty() + { + return count == 0; + } + + public bool Full() + { + return count == FIFO_SIZE; + } + + public void Flush() + { + lock (synclock) + { + cursor_r = 0; + cursor_w = 0; + count = 0; + for (int i = 0; i < FIFO_SIZE; i++) + fifo[i] = 0; + } + } + + const uint FIFO_SIZE = 64 * 2; + const uint FIFO_MASK = FIFO_SIZE - 1; + + public uint cursor_r = 0; + public uint cursor_w = 0; + public uint count = 0; + public ulong[] fifo = new ulong[FIFO_SIZE]; + + private readonly object synclock = new object(); + } + + public class MatchaI2S : BasicDoubleWordPeripheral, IDisposable, IKnownSize, INumberedGPIOOutput + { + public MatchaI2S(Machine machine) : base(machine) + { + CreateRegisters(); + + TxWatermarkIRQ = new GPIO(); + RxWatermarkIRQ = new GPIO(); + TxEmptyIRQ = new GPIO(); + RxOverflowIRQ = new GPIO(); + + var irqs = new Dictionary<int, IGPIO>(); + irqs[(int)Events.TxWatermarkIRQ] = TxWatermarkIRQ; + irqs[(int)Events.RxWatermarkIRQ] = RxWatermarkIRQ; + irqs[(int)Events.TxEmptyIRQ] = TxEmptyIRQ; + irqs[(int)Events.RxOverflowIRQ] = RxOverflowIRQ; + Connections = new ReadOnlyDictionary<int, IGPIO>(irqs); + + Reset(); + } + + public override void Reset() + { + base.Reset(); + InputFile = ""; + OutputFile = ""; + + ResetTx(); + ResetRx(); + } + + private void ResetTx() + { + encoder?.FlushBuffer(); + txThread = null; + txBuffer = new Fifo(); + } + + private void ResetRx() + { + decoder?.Reset(); + rxThread = null; + rxBuffer = new Fifo(); + + rxTriggerLevel.Value = 0b010; + mappedRxTriggerLevel = 8; + } + + public void Dispose() + { + encoder?.Dispose(); + } + + public string InputFile { get; set; } + public string OutputFile { get; set; } + + public const uint SampleFrequencyHz = 16_000; + public const uint SampleSizeBits = 16; + public const uint NumChannels = 2; + public const uint BufferSizeWords = 64; + + private Fifo rxBuffer; + private Fifo txBuffer; + + public IReadOnlyDictionary<int, IGPIO> Connections { get; } + + private void StartRx() + { + if (InputFile == "") { + this.Log(LogLevel.Error, "Starting recording without an input file!"); + return; + } + + decoder = new PCMDecoder(SampleSizeBits, SampleFrequencyHz, NumChannels, false, this); + decoder.LoadFile(InputFile); + + this.Log(LogLevel.Info, "Starting recording."); + rxThread = machine.ObtainManagedThread(InputFrames, SampleFrequencyHz); + rxThread.Start(); + } + + private void StartTx() + { + } + + private void StopThread(ref IManagedThread thread) + { + thread?.Stop(); + thread = null; + } + + private void OutputFrames() + { + } + + private void InputFrames() + { + while (!rxBuffer.Full()) + { + // Decode left on the upper half of the sample + ulong sample = (decoder.GetSingleSample() << 16); + sample |= decoder.GetSingleSample(); + + if (!rxBuffer.Full()) { + rxBuffer.Write(sample); + + if (rxBuffer.count == mappedRxTriggerLevel) + { + this.Log(LogLevel.Debug, "I2S RX FIFO Watermark."); + Connections[(int)Events.RxWatermarkIRQ].Blink(); + } + } + } + + Connections[(int)Events.RxOverflowIRQ].Blink(); + } + + private void CreateRegisters() + { + Registers.Control.Define(this) + .WithFlag(0, out txEnable, name: "TX", + writeCallback: (_, val) => { + if (val) { + StartTx(); + } else { + StopThread(ref txThread); + } + }) + .WithFlag(1, out rxEnable, name: "RX", + writeCallback: (_, val) => { + if (val) { + StartRx(); + } else { + StopThread(ref rxThread); + } + }) + .WithFlag(2, out loopbackEnable, name: "SLPBK") + .WithReservedBits(3, 15) + .WithValueField(18, 6, out rxClkDivide, name: "NCO_RX") + .WithValueField(25, 6, out txClkDivide, name: "NCO_TX"); + Registers.Status.Define32(this) + .WithFlag(0, name: "TXFULL", valueProviderCallback: _ => { return txBuffer.Full(); }) + .WithFlag(1, name: "RXFULL", valueProviderCallback: _ => { return rxBuffer.Full(); }) + .WithFlag(2, name: "TXEMPTY", valueProviderCallback: _ => { return txBuffer.Empty(); }) + .WithFlag(3, name: "RXEMPTY", valueProviderCallback: _ => { return rxBuffer.Empty(); }) + .WithReservedBits(4, 27); + Registers.ReadData.Define32(this) + .WithValueField(0, 31, FieldMode.Read, name: "RDATA", + valueProviderCallback: _ => { return rxBuffer.Read(); }); + Registers.WriteData.Define32(this) + .WithValueField(0, 31, name: "WDATA", + writeCallback: (_, val) => { txBuffer.Write(val); }); + Registers.FifoControl.Define32(this) + .WithFlag(0, name: "RXRST", writeCallback: (_, val) => { if (val) ResetRx(); }) + .WithFlag(1, name: "TXRST", writeCallback: (_, val) => { if (val) ResetTx(); }) + .WithValueField(2, 3, out rxTriggerLevel, + writeCallback: (_, val) => { + switch (val) { + case 0b000: mappedRxTriggerLevel = 1; break; + case 0b001: mappedRxTriggerLevel = 4; break; + case 0b010: mappedRxTriggerLevel = 8; break; + case 0b011: mappedRxTriggerLevel = 16; break; + case 0b100: mappedRxTriggerLevel = 30; break; + default: + this.Log(LogLevel.Error, "Trying to set invalid RX trigger level. Setting to default"); + val = 0b010; + mappedRxTriggerLevel = 8; + break; + } + }, + name: "RXILVL") + .WithValueField(5, 2, out txTriggerLevel, + writeCallback: (_, val) => { + val = 0b01; + }, + name: "TXILVL") + .WithReservedBits(7, 24); + Registers.FifoStatus.Define32(this) + .WithValueField(0, 6, name: "TXLVL", + valueProviderCallback: _ => { return txBuffer.count; }) + .WithReservedBits(7, 9) + .WithValueField(16, 6, name: "RXLVL", + valueProviderCallback: _ => { return rxBuffer.count; }) + .WithReservedBits(22, 10); + } + + private IFlagRegisterField txEnable; + private IFlagRegisterField rxEnable; + private IFlagRegisterField loopbackEnable; + + private IValueRegisterField rxClkDivide; + private IValueRegisterField txClkDivide; + private IValueRegisterField rxTriggerLevel; + private IValueRegisterField txTriggerLevel; + + private int mappedRxTriggerLevel; + private int mappedTxTriggerLevel; + + private IManagedThread rxThread; + private IManagedThread txThread; + private PCMDecoder decoder; + private PCMEncoder encoder; + + public GPIO TxWatermarkIRQ { get; } + public GPIO RxWatermarkIRQ { get; } + public GPIO TxEmptyIRQ { get; } + public GPIO RxOverflowIRQ { get; } + + private enum Events + { + TxWatermarkIRQ = 0, // raised if the xmit FIFO is below the high watermark + RxWatermarkIRQ = 1, // raised if the recv FIFO is above the high watermark + TxEmptyIRQ = 2, // raised if the xmit FIFO is empty + RxOverflowIRQ = 3, // raised if the recv FIFO has overflowed + } + + private enum Registers + { + Control = 0x0, // Control register + Status = 0x1, // I2S Live Status register + // bit 0: TX buffer full + // bit 1: RX buffer full + // bit 2: TX FIFO empty + // bit 3: RX FIFO empty + ReadData = 0x2, // I2S read data + WriteData = 0x3, // I2S write data + FifoControl = 0x4, // I2S FIFO control register + // bit 0: RX FIFO reset (write 1 to reset) + // bit 1: TX FIFO reset (write 1 to reset) + // bit 4-2: Trigger level for RX interrupts + // bit 6-5: Trigger level for TX interrupts + FifoStatus = 0x5, // I2S FIFO status register + // bit 5:0: Current fill level of TX fifo + // bit 21:16: Current fill level of RX fifo. + } + + public long Size => 0x40; + } +}
diff --git a/shodan_infrastructure/test.raw b/shodan_infrastructure/test.raw new file mode 100644 index 0000000..d868cec --- /dev/null +++ b/shodan_infrastructure/test.raw Binary files differ