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