| // |
| // 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; |
| } |
| } |