|  | // | 
|  | // Copyright (c) 2023 Google LLC | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     https://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | using System; | 
|  | using System.Collections.Generic; | 
|  | using System.Collections.ObjectModel; | 
|  | using System.Diagnostics; | 
|  | 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() | 
|  | { | 
|  | lock (synclock) | 
|  | { | 
|  | return count == 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | public bool Full() | 
|  | { | 
|  | lock (synclock) | 
|  | { | 
|  | 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; | 
|  | } | 
|  | } | 
|  |  | 
|  | public override string ToString() | 
|  | { | 
|  | string result = "["; | 
|  |  | 
|  | for (uint i = 0; i < FIFO_SIZE; i++) { | 
|  | result += (i == cursor_r) ? "r" : " "; | 
|  | result += (i == cursor_w) ? "w" : " "; | 
|  | result += String.Format("{0:X8} ", fifo[i]); | 
|  | } | 
|  |  | 
|  | return result + "]"; | 
|  | } | 
|  |  | 
|  | public uint Count { get { lock(synclock) { return count; } } } | 
|  |  | 
|  | const uint FIFO_SIZE = 32; | 
|  | const uint FIFO_MASK = FIFO_SIZE - 1; | 
|  |  | 
|  | private uint cursor_r = 0; | 
|  | private uint cursor_w = 0; | 
|  | private uint count = 0; | 
|  | private 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(ReceiveFrame, SampleFrequencyHz, name: "i2s rx"); | 
|  | rxThread.Start(); | 
|  | } | 
|  |  | 
|  | private void StartTx() | 
|  | { | 
|  | if (OutputFile == "") { | 
|  | this.Log(LogLevel.Error, "Starting playback without an output file!"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | this.Log(LogLevel.Info, "Starting playback."); | 
|  |  | 
|  | encoder = new PCMEncoder(SampleSizeBits, SampleFrequencyHz, NumChannels, false); | 
|  | encoder.Output = OutputFile + String.Format(".{0}.{1:d4}", | 
|  | Process.GetCurrentProcess().Id, outputFileIdx++); | 
|  | encoder.SetBufferingBySamplesCount(1); | 
|  |  | 
|  | txThread = machine.ObtainManagedThread(TransmitFrame, SampleFrequencyHz, name: "i2s tx"); | 
|  | txThread.Start(); | 
|  | } | 
|  |  | 
|  | private void StopThread(ref IManagedThread thread) | 
|  | { | 
|  | thread?.Stop(); | 
|  | thread = null; | 
|  | } | 
|  |  | 
|  | // Note: this is not a real thread! This is simply a function that is | 
|  | // called at a known frequency from the machine simulation. It must | 
|  | // always complete and never loop! | 
|  | private void TransmitFrame() | 
|  | { | 
|  | if (txBuffer.Empty()) { | 
|  | // FIFO is empty -- nothing to do except set the IRQ and return. | 
|  | this.Log(LogLevel.Debug, "I2S TX FIFO Empty.", txBuffer.Count, mappedTxTriggerLevel); | 
|  | TxEmptyIRQ.Set(txEmptyIrqEnabled.Value); | 
|  | return; | 
|  | } | 
|  |  | 
|  | ulong sample = txBuffer.Read(); | 
|  | encoder.AcceptSample((uint)((sample & 0xFFFF0000) >> 16)); // left | 
|  | encoder.AcceptSample((uint)(sample & 0x0000FFFF));         // right | 
|  | this.Log(LogLevel.Noisy, "Removed sample. Level: {0}", txBuffer.Count); | 
|  |  | 
|  | if (txBuffer.Count < mappedTxTriggerLevel) | 
|  | { | 
|  | this.Log(LogLevel.Debug, "I2S TX FIFO Watermark (count {0} < {1}).", txBuffer.Count, mappedTxTriggerLevel); | 
|  | TxWatermarkIRQ.Set(txWatermarkIrqEnabled.Value); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Note: this is not a real thread! This is simply a function that is | 
|  | // called at a known frequency from the machine simulation. It must | 
|  | // always complete and never loop! | 
|  | private void ReceiveFrame() | 
|  | { | 
|  | // Decode left on the upper half of the sample | 
|  | ulong sample = (decoder.GetSingleSample() << 16); | 
|  | sample |= decoder.GetSingleSample(); | 
|  |  | 
|  | if (rxBuffer.Full()) | 
|  | { | 
|  | this.Log(LogLevel.Noisy, "RX buffer full"); | 
|  | if (rxOverflowIrqEnabled.Value) | 
|  | { | 
|  | this.Log(LogLevel.Debug, "I2S RX FIFO Overflow (count {0}).", rxBuffer.Count); | 
|  | RxOverflowIRQ.Set(rxOverflowIrqEnabled.Value); | 
|  | } | 
|  |  | 
|  | // FIFO is full -- drop the sample | 
|  | return; | 
|  | } | 
|  |  | 
|  | rxBuffer.Write(sample); | 
|  | this.Log(LogLevel.Noisy, "Added sample ({1:X}). Level: {0}", rxBuffer.Count, sample); | 
|  | this.Log(LogLevel.Noisy, rxBuffer.ToString()); | 
|  |  | 
|  | if (rxBuffer.Count >= mappedRxTriggerLevel) | 
|  | { | 
|  | this.Log(LogLevel.Debug, "I2S RX FIFO Watermark (count {0} == {1}).", rxBuffer.Count, mappedRxTriggerLevel); | 
|  | RxWatermarkIRQ.Set(rxWatermarkIrqEnabled.Value); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void CreateRegisters() | 
|  | { | 
|  | Registers.IrqState.Define(this) | 
|  | .WithFlag(0, name: "TX_WATERMARK", | 
|  | valueProviderCallback: _ => { return txBuffer.Count >= mappedTxTriggerLevel; }, | 
|  | writeCallback: (_, val) => { if (val) TxWatermarkIRQ.Set(false); }) | 
|  | .WithFlag(1, name: "RX_WATERMARK", | 
|  | valueProviderCallback: _ => { return rxBuffer.Count >= mappedRxTriggerLevel; }, | 
|  | writeCallback: (_, val) => { if (val) RxWatermarkIRQ.Set(false); }) | 
|  | .WithFlag(2, name: "TX_EMPTY", | 
|  | valueProviderCallback: _ => { return txBuffer.Empty(); }, | 
|  | writeCallback: (_, val) => { if (val) TxEmptyIRQ.Set(false); }) | 
|  | .WithFlag(3, name: "RX_OVERFLOW", | 
|  | valueProviderCallback: _ => { return rxBuffer.Full(); }, | 
|  | writeCallback: (_, val) => { if (val) RxOverflowIRQ.Set(false); }); | 
|  |  | 
|  | Registers.IrqEnable.Define(this) | 
|  | .WithFlag(0, out txWatermarkIrqEnabled, name: "ENABLE_TX_WATERMARK") | 
|  | .WithFlag(1, out rxWatermarkIrqEnabled, name: "ENABLE_RX_WATERMARK") | 
|  | .WithFlag(2, out txEmptyIrqEnabled, name: "ENABLE_TX_EMPTY") | 
|  | .WithFlag(3, out rxOverflowIrqEnabled, name: "ENABLE_RX_OVERFLOW"); | 
|  |  | 
|  | Registers.IrqTest.Define(this) | 
|  | .WithFlag(0, name: "TEST_TX_WATERMARK", | 
|  | writeCallback: (_, val) => { TxWatermarkIRQ.Set(val); }) | 
|  | .WithFlag(1, name: "TEST_RX_WATERMARK", | 
|  | writeCallback: (_, val) => { RxWatermarkIRQ.Set(val); }) | 
|  | .WithFlag(2, name: "TEST_TX_EMPTY", | 
|  | writeCallback: (_, val) => { TxEmptyIRQ.Set(val); }) | 
|  | .WithFlag(3, name: "TEST_RX_OVERFLOW", | 
|  | writeCallback: (_, val) => { RxOverflowIRQ.Set(val); }); | 
|  |  | 
|  | 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, 32, FieldMode.Read, name: "RDATA", | 
|  | valueProviderCallback: _ => { | 
|  | ulong sample = rxBuffer.Read(); | 
|  | if (rxBuffer.Count < mappedRxTriggerLevel) { | 
|  | RxWatermarkIRQ.Set(false); | 
|  | this.Log(LogLevel.Noisy, "RX Watermark cleared"); | 
|  | } | 
|  | RxOverflowIRQ.Set(false); | 
|  | this.Log(LogLevel.Noisy, "Removed sample ({1:X}). Level: {0}", rxBuffer.Count, sample); | 
|  |  | 
|  | return sample; | 
|  | }); | 
|  |  | 
|  | Registers.WriteData.Define32(this) | 
|  | .WithValueField(0, 32, name: "WDATA", | 
|  | writeCallback: (_, val) => { | 
|  | txBuffer.Write(val); | 
|  | if (txBuffer.Count > mappedTxTriggerLevel) { | 
|  | TxWatermarkIRQ.Set(false); | 
|  | this.Log(LogLevel.Noisy, "TX Watermark cleared"); | 
|  | } | 
|  | TxEmptyIRQ.Set(false); | 
|  | this.Log(LogLevel.Noisy, "Added sample ({1:X}). Level: {0}", txBuffer.Count, 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) => { | 
|  | switch (val) { | 
|  | case 0b00: mappedTxTriggerLevel = 1; break; | 
|  | case 0b01: mappedTxTriggerLevel = 4; break; | 
|  | case 0b10: mappedTxTriggerLevel = 8; break; | 
|  | case 0b11: mappedTxTriggerLevel = 16; break; | 
|  | default: | 
|  | this.Log(LogLevel.Error, "Trying to set invalid TX trigger level. Setting to default"); | 
|  | val = 0b00; | 
|  | mappedTxTriggerLevel = 8; | 
|  | break; | 
|  | } | 
|  | }, | 
|  | 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 IFlagRegisterField txWatermarkIrqEnabled; | 
|  | private IFlagRegisterField rxWatermarkIrqEnabled; | 
|  | private IFlagRegisterField txEmptyIrqEnabled; | 
|  | private IFlagRegisterField rxOverflowIrqEnabled; | 
|  |  | 
|  | private IValueRegisterField rxClkDivide; | 
|  | private IValueRegisterField txClkDivide; | 
|  | private IValueRegisterField rxTriggerLevel; | 
|  | private IValueRegisterField txTriggerLevel; | 
|  |  | 
|  | private int mappedRxTriggerLevel; | 
|  | private int mappedTxTriggerLevel; | 
|  | private int outputFileIdx; | 
|  |  | 
|  | 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; } | 
|  |  | 
|  | public string DumpFifos() { | 
|  | return rxBuffer.ToString(); | 
|  | } | 
|  |  | 
|  | 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 | 
|  | { | 
|  | IrqState    = 0x00, // Current interrupt states | 
|  | //   bit 0: TX watermark | 
|  | //   bit 1: RX watermark | 
|  | //   bit 2: TX empty | 
|  | //   bit 3: RX overflow | 
|  | IrqEnable   = 0x04, // Whether or not interrupts are enabled | 
|  | //   bit 0: TX watermark | 
|  | //   bit 1: RX watermark | 
|  | //   bit 2: TX empty | 
|  | //   bit 3: RX overflow | 
|  | IrqTest     = 0x08, // IRQ testing | 
|  | //   bit 0: TX watermark | 
|  | //   bit 1: RX watermark | 
|  | //   bit 2: TX empty | 
|  | //   bit 3: RX overflow | 
|  | Control     = 0x0c, // Control register | 
|  | Status      = 0x10, // 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    = 0x14, // I2S read data | 
|  | WriteData   = 0x18, // I2S write data | 
|  | FifoControl = 0x1c, // 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  = 0x20, // 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; | 
|  | } | 
|  | } |