Partial implementation of the I2S

Only covers the DSi mic (with ARM7 interface)
NDMA mode 0x0C is not implemented
SPU / DSP sampling should be done here, but this is not implemented (would need some discussion since it deeply affects how the frontend handles audio, also especially since the sample rate can change here)
Some things are a complete guess (only some things have been hardware tested)
This commit is contained in:
CasualPokePlayer 2024-11-01 12:29:21 -07:00
parent 1b8daa0465
commit 44e6dec81e
8 changed files with 360 additions and 36 deletions

View File

@ -21,6 +21,7 @@ add_library(core STATIC
DSi_Camera.cpp
DSi_DSP.cpp
DSi_I2C.cpp
DSi_I2S.cpp
DSi_NAND.cpp
DSi_NDMA.cpp
DSi_NWifi.cpp

View File

@ -35,6 +35,7 @@
#include "DSi_NDMA.h"
#include "DSi_I2C.h"
#include "DSi_I2S.h"
#include "DSi_SD.h"
#include "DSi_AES.h"
#include "DSi_NAND.h"
@ -108,6 +109,7 @@ DSi::DSi(DSiArgs&& args, void* userdata) noexcept :
SDMMC(*this, std::move(args.NANDImage), std::move(args.DSiSDCard)),
SDIO(*this),
I2C(*this),
I2S(*this),
CamModule(*this),
AES(*this)
{
@ -141,6 +143,7 @@ void DSi::Reset()
for (int i = 0; i < 8; i++) NDMAs[i].Reset();
I2C.Reset();
I2S.Reset();
CamModule.Reset();
DSP.Reset();
@ -210,6 +213,13 @@ void DSi::CamInputFrame(int cam, const u32* data, int width, int height, bool rg
}
}
void DSi::MicInputFrame(s16* data, int samples)
{
SPI.GetTSC()->MicInputFrame(data, samples);
I2S.MicInputFrame(data, samples);
// TODO: Need to send the mic samples to the DSP!
}
void DSi::DoSavestateExtra(Savestate* file)
{
file->Section("DSIG");
@ -285,6 +295,7 @@ void DSi::DoSavestateExtra(Savestate* file)
CamModule.DoSavestate(file);
DSP.DoSavestate(file);
I2C.DoSavestate(file);
I2S.DoSavestate(file);
SDMMC.DoSavestate(file);
SDIO.DoSavestate(file);
}
@ -644,6 +655,8 @@ void DSi::SetupDirectBoot()
SPI.GetFirmwareMem()->SetupDirectBoot();
I2S.WriteSndExCnt(0x8008, 0xFFFF);
ARM9.CP15Write(0x100, 0x00056078);
ARM9.CP15Write(0x200, 0x0000004A);
ARM9.CP15Write(0x201, 0x0000004A);
@ -690,6 +703,9 @@ void DSi::SoftReset()
NDS::MapSharedWRAM(3);
// TODO: is this actually reset?
I2S.Reset();
// TODO: does the DSP get reset? NWRAM doesn't, so I'm assuming no
// *HOWEVER*, the bootrom (which does get rerun) does remap NWRAM, and thus
// the DSP most likely gets reset
@ -2707,8 +2723,16 @@ u8 DSi::ARM7IORead8(u32 addr)
case 0x04004D07: if (SCFG_BIOS & (1<<10)) return 0; return SDMMC.GetNAND()->GetConsoleID() >> 56;
case 0x04004D08: return 0;
case 0x4004700: return DSP.ReadSNDExCnt() & 0xFF;
case 0x4004701: return DSP.ReadSNDExCnt() >> 8;
case 0x4004600: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt() & 0xFF;
case 0x4004601: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt() >> 8;
case 0x4004602: return 0;
case 0x4004603: return 0;
case 0x4004604: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() & 0xFF;
case 0x4004605: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return (I2S.ReadMicData() >> 8) & 0xFF;
case 0x4004606: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return (I2S.ReadMicData() >> 16) & 0xFF;
case 0x4004607: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() >> 24;
case 0x4004700: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt() & 0xFF;
case 0x4004701: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt() >> 8;
case 0x04004C00: return GPIO_Data;
case 0x04004C01: return GPIO_Dir;
@ -2751,7 +2775,11 @@ u16 DSi::ARM7IORead16(u32 addr)
case 0x04004D06: if (SCFG_BIOS & (1<<10)) return 0; return SDMMC.GetNAND()->GetConsoleID() >> 48;
case 0x04004D08: return 0;
case 0x4004700: return DSP.ReadSNDExCnt();
case 0x4004600: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt();
case 0x4004602: return 0;
case 0x4004604: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() >> 16;
case 0x4004606: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() & 0xFFFF;
case 0x4004700: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt();
case 0x04004C00: return GPIO_Data | ((u16)GPIO_Dir << 8);
case 0x04004C02: return GPIO_IEdgeSel | ((u16)GPIO_IE << 8);
@ -2829,9 +2857,9 @@ u32 DSi::ARM7IORead32(u32 addr)
case 0x04004D04: if (SCFG_BIOS & (1<<10)) return 0; return SDMMC.GetNAND()->GetConsoleID() >> 32;
case 0x04004D08: return 0;
case 0x4004700:
Log(LogLevel::Debug, "32-Bit SNDExCnt read? %08X\n", ARM7.R[15]);
return DSP.ReadSNDExCnt();
case 0x4004600: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt();
case 0x4004604: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData();
case 0x4004700: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt();
}
if (addr >= 0x04004800 && addr < 0x04004A00)
@ -2884,11 +2912,25 @@ void DSi::ARM7IOWrite8(u32 addr, u8 val)
case 0x04004500: I2C.WriteData(val); return;
case 0x04004501: I2C.WriteCnt(val); return;
case 0x4004600:
if (!(SCFG_EXT[1] & (1 << 20)))
return;
I2S.WriteMicCnt((u16)val, 0xFF);
return;
case 0x4004601:
if (!(SCFG_EXT[1] & (1 << 20)))
return;
I2S.WriteMicCnt(((u16)val << 8), 0xFF00);
return;
case 0x4004700:
DSP.WriteSNDExCnt((u16)val, 0xFF);
if (!(SCFG_EXT[1] & (1 << 21)))
return;
I2S.WriteSndExCnt((u16)val, 0xFF);
return;
case 0x4004701:
DSP.WriteSNDExCnt(((u16)val << 8), 0xFF00);
if (!(SCFG_EXT[1] & (1 << 21)))
return;
I2S.WriteSndExCnt(((u16)val << 8), 0xFF00);
return;
case 0x04004C00:
@ -2987,8 +3029,15 @@ void DSi::ARM7IOWrite16(u32 addr, u16 val)
AES.WriteBlkCnt(val<<16);
return;
case 0x4004600:
if (!(SCFG_EXT[1] & (1 << 20)))
return;
I2S.WriteMicCnt(val, 0xFFFF);
return;
case 0x4004700:
DSP.WriteSNDExCnt(val, 0xFFFF);
if (!(SCFG_EXT[1] & (1 << 21)))
return;
I2S.WriteSndExCnt(val, 0xFFFF);
return;
case 0x04004C00:
@ -3136,9 +3185,15 @@ void DSi::ARM7IOWrite32(u32 addr, u32 val)
case 0x04004404: AES.WriteBlkCnt(val); return;
case 0x04004408: AES.WriteInputFIFO(val); return;
case 0x4004600:
if (!(SCFG_EXT[1] & (1 << 20)))
return;
I2S.WriteMicCnt(val, 0xFFFF);
return;
case 0x4004700:
Log(LogLevel::Debug, "32-Bit SNDExCnt write? %08X %08X\n", val, ARM7.R[15]);
DSP.WriteSNDExCnt(val, 0xFFFF);
if (!(SCFG_EXT[1] & (1 << 21)))
return;
I2S.WriteSndExCnt(val, 0xFFFF);
return;
}

View File

@ -21,6 +21,7 @@
#include "NDS.h"
#include "DSi_NDMA.h"
#include "DSi_I2S.h"
#include "DSi_SD.h"
#include "DSi_DSP.h"
#include "DSi_AES.h"
@ -30,6 +31,7 @@
namespace melonDS
{
class DSi_I2CHost;
class DSi_I2S;
class DSi_CamModule;
class DSi_AES;
class DSi_DSP;
@ -69,6 +71,7 @@ public:
u32 NWRAMMask[2][3];
DSi_I2CHost I2C;
DSi_I2S I2S;
DSi_CamModule CamModule;
DSi_AES AES;
DSi_DSP DSP;
@ -155,6 +158,7 @@ public:
void SetSDCard(std::optional<FATStorage>&& sdcard) noexcept { SDMMC.SetSDCard(std::move(sdcard)); }
void CamInputFrame(int cam, const u32* data, int width, int height, bool rgb) override;
void MicInputFrame(s16* data, int samples) override;
bool DMAsInMode(u32 cpu, u32 mode) const override;
bool DMAsRunning(u32 cpu) const override;
void StopDMAs(u32 cpu, u32 mode) override;

View File

@ -178,8 +178,6 @@ void DSi_DSP::Reset()
TeakraCore->Reset();
DSi.CancelEvent(Event_DSi_DSP);
SNDExCnt = 0;
}
bool DSi_DSP::IsRstReleased() const
@ -536,23 +534,6 @@ void DSi_DSP::Write32(u32 addr, u32 val)
Write16(addr, val & 0xFFFF);
}
void DSi_DSP::WriteSNDExCnt(u16 val, u16 mask)
{
val = (val & mask) | (SNDExCnt & ~mask);
// it can be written even in NDS mode
// mic frequency can only be changed if it was disabled
// before the write
if (SNDExCnt & 0x8000)
{
val &= ~0x2000;
val |= SNDExCnt & 0x2000;
}
SNDExCnt = val & 0xE00F;
}
void DSi_DSP::Run(u32 cycles)
{
if (!IsDSPCoreEnabled())

View File

@ -54,9 +54,6 @@ public:
u32 Read32(u32 addr);
void Write32(u32 addr, u32 val);
u16 ReadSNDExCnt() const { return SNDExCnt; }
void WriteSNDExCnt(u16 val, u16 mask);
// NOTE: checks SCFG_CLK9
void Run(u32 cycles);
@ -70,8 +67,6 @@ public:
private:
melonDS::DSi& DSi;
// not sure whether to not rather put it somewhere else
u16 SNDExCnt;
Teakra::Teakra* TeakraCore;

219
src/DSi_I2S.cpp Normal file
View File

@ -0,0 +1,219 @@
/*
Copyright 2016-2024 melonDS team
This file is part of melonDS.
melonDS is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with melonDS. If not, see http://www.gnu.org/licenses/.
*/
#include <string.h>
#include "DSi.h"
#include "DSi_I2S.h"
#include "Platform.h"
namespace melonDS
{
using Platform::Log;
using Platform::LogLevel;
DSi_I2S::DSi_I2S(melonDS::DSi& dsi) : DSi(dsi)
{
DSi.RegisterEventFunc(Event_DSi_I2S, 0, MemberEventFunc(DSi_I2S, Clock));
MicCnt = 0;
SndExCnt = 0;
MicClockDivider = 0;
MicBufferLen = 0;
}
DSi_I2S::~DSi_I2S()
{
DSi.UnregisterEventFunc(Event_DSi_I2S, 0);
}
void DSi_I2S::Reset()
{
MicCnt = 0;
SndExCnt = 0;
MicClockDivider = 0;
MicFifo.Clear();
MicBufferLen = 0;
DSi.ScheduleEvent(Event_DSi_I2S, false, 1024, 0, I2S_Freq_32728Hz);
}
void DSi_I2S::DoSavestate(Savestate* file)
{
file->Section("I2Si");
file->Var16(&MicCnt);
file->Var16(&SndExCnt);
file->Var8(&MicClockDivider);
MicFifo.DoSavestate(file);
}
void DSi_I2S::MicInputFrame(const s16* data, int samples)
{
if (!data)
{
MicBufferLen = 0;
return;
}
if (samples > 1024) samples = 1024;
memcpy(MicBuffer, data, samples * sizeof(s16));
MicBufferLen = samples;
}
u16 DSi_I2S::ReadMicCnt()
{
u16 ret = MicCnt;
if (MicFifo.Level() == 0) ret |= 1 << 8;
if (MicFifo.Level() >= 16) ret |= 1 << 9;
if (MicFifo.Level() >= 32) ret |= 1 << 10;
return ret;
}
void DSi_I2S::WriteMicCnt(u16 val, u16 mask)
{
val = (val & mask) | (MicCnt & ~mask);
// FIFO clear can only happen if the mic was disabled before the write
if (!(MicCnt & (1 << 15)) && (val & (1 << 12)))
{
MicCnt &= ~(1 << 11);
MicFifo.Clear();
}
MicCnt = (val & 0xE00F) | (MicCnt & (1 << 11));
}
u32 DSi_I2S::ReadMicData()
{
// CHECKME: This is a complete guess on how mic data reads work
// gbatek states the FIFO is 16 words large, with 1 word having 2 samples
u32 ret = MicFifo.IsEmpty() ? 0 : (u16)MicFifo.Read();
ret |= (MicFifo.IsEmpty() ? 0 : (u16)MicFifo.Read()) << 16;
return ret;
}
u16 DSi_I2S::ReadSndExCnt()
{
return SndExCnt;
}
void DSi_I2S::WriteSndExCnt(u16 val, u16 mask)
{
val = (val & mask) | (SndExCnt & ~mask);
// Note: SNDEXCNT can be accessed in "NDS mode"
// This is due to the corresponding SCFG_EXT flag not being disabled
// If it is disabled (with homebrew), SNDEXCNT cannot be accessed
// This is more purely a software mistake on the DSi menu's part
// I2S frequency can only be changed if it was disabled before the write
if (SndExCnt & (1 << 15))
{
val &= ~(1 << 13);
val |= SndExCnt & (1 << 13);
}
if ((SndExCnt ^ val) & (1 << 13))
{
Log(LogLevel::Debug, "Changed I2S frequency to %dHz\n", (SndExCnt & (1 << 13)) ? 47605 : 32728);
}
SndExCnt = val & 0xE00F;
}
void DSi_I2S::Clock(u32 freq)
{
if (SndExCnt & (1 << 15))
{
// CHECKME (from gbatek)
// "The Sampling Rate becomes zero (no data arriving) when SNDEXCNT.Bit15=0, or when MIC_CNT.bit0-1=3, or when MIC_CNT.bit15=0, or when Overrun has occurred."
// This likely means on any of these conditions the mic completely ignores any I2S clocks
// Although perhaps it might still acknowledge the clocks in some capacity (maybe affecting below clock division)
if ((MicCnt & (1 << 15)) && !(MicCnt & (1 << 11)) && (MicCnt & 3) != 3)
{
// CHECKME (from gbatek)
// "2-3 Sampling Rate (0..3=F/1, F/2, F/3, F/4)"
// This likely works with an internal counter compared with the sampling rate
// This counter is then likely reset on mic sample
// But this is completely untested
MicClockDivider++;
u8 micRate = (MicCnt >> 2) & 3;
if (MicClockDivider > micRate)
{
MicClockDivider = 0;
s16 sample = 0;
if (MicBufferLen > 0)
{
// 560190 cycles per frame
u32 cyclepos = (u32)DSi.GetSysClockCycles(2);
u32 samplepos = (cyclepos * MicBufferLen) / 560190;
if (samplepos >= MicBufferLen) samplepos = MicBufferLen - 1;
sample = MicBuffer[samplepos];
}
u32 oldLevel = MicFifo.Level();
if ((MicCnt & 3) == 0)
{
// stereo (this just duplicates the sample, as the mic itself is mono)
if (MicFifo.IsFull()) MicCnt |= 1 << 11;
MicFifo.Write(sample);
if (MicFifo.IsFull()) MicCnt |= 1 << 11;
MicFifo.Write(sample);
}
else
{
// mono
if (MicFifo.IsFull()) MicCnt |= 1 << 11;
MicFifo.Write(sample);
}
// if bit 13 is set, an IRQ is generated when the mic FIFO is half full
if (MicCnt & (1 << 13))
{
if (oldLevel < 16 && MicFifo.Level() >= 16) DSi.SetIRQ2(IRQ2_DSi_MicExt);
}
// if bit 13 is not set and bit 14 is set, an IRQ is generated when the mic FIFO is full
else if (MicCnt & (1 << 14))
{
if (oldLevel < 32 && MicFifo.Level() >= 32) DSi.SetIRQ2(IRQ2_DSi_MicExt);
}
}
}
// TODO: SPU and DSP sampling should happen here
// use passed freq to know how much to advance SPU by?
}
if (SndExCnt & (1 << 13))
{
DSi.ScheduleEvent(Event_DSi_I2S, false, 704, 0, I2S_Freq_47605Hz);
}
else
{
DSi.ScheduleEvent(Event_DSi_I2S, false, 1024, 0, I2S_Freq_32728Hz);
}
}
}

68
src/DSi_I2S.h Normal file
View File

@ -0,0 +1,68 @@
/*
Copyright 2016-2024 melonDS team
This file is part of melonDS.
melonDS is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with melonDS. If not, see http://www.gnu.org/licenses/.
*/
#ifndef DSI_I2S_H
#define DSI_I2S_H
#include "FIFO.h"
#include "types.h"
#include "Savestate.h"
namespace melonDS
{
class DSi;
class DSi_I2S
{
public:
DSi_I2S(melonDS::DSi& dsi);
~DSi_I2S();
void Reset();
void DoSavestate(Savestate* file);
void MicInputFrame(const s16* data, int samples);
u16 ReadMicCnt();
void WriteMicCnt(u16 val, u16 mask);
u32 ReadMicData();
u16 ReadSndExCnt();
void WriteSndExCnt(u16 val, u16 mask);
private:
melonDS::DSi& DSi;
u16 MicCnt;
u16 SndExCnt;
u8 MicClockDivider;
FIFO<s16, 32> MicFifo;
s16 MicBuffer[1024];
int MicBufferLen;
enum
{
I2S_Freq_32728Hz,
I2S_Freq_47605Hz
};
void Clock(u32 freq);
};
}
#endif // DSI_I2S_H

View File

@ -72,6 +72,7 @@ enum
Event_DSi_CamIRQ,
Event_DSi_CamTransfer,
Event_DSi_DSP,
Event_DSi_I2S,
Event_MAX
};
@ -400,7 +401,7 @@ public: // TODO: Encapsulate the rest of these members
void SetLidClosed(bool closed);
virtual void CamInputFrame(int cam, const u32* data, int width, int height, bool rgb) {}
void MicInputFrame(s16* data, int samples);
virtual void MicInputFrame(s16* data, int samples);
void RegisterEventFunc(u32 id, u32 funcid, EventFunc func);
void UnregisterEventFunc(u32 id, u32 funcid);