Skip to content

Commit

Permalink
fc: Initial EPSM implementation. (#1274)
Browse files Browse the repository at this point in the history
More information:
https://www.nesdev.org/wiki/Expansion_Port_Sound_Module

While this is a homebrew device, it has already shipped to consumers in
this manner.

TODO (I'm not sure how to fix these issues myself):

- [x] Switching from EPSM back to another peripheral (None or Family
Keyboard) unloads not just the additional EPSM-provided audio streams,
but seems to break *all* audio from the NES core.
- [x] NES 2.0 cartridges can declare that they wish to have the EPSM
peripheral installed by default (Extended Console Type 0x04), but I'm
not sure how to hook this nicely from the pak data to the expansion port
node.
- [ ] Missing serialization, but this affects the Family Keyboard to
some limited extent already, so...

Not urgent at all. It has shipped to consumers, but the amount of
software using it is still lacking.

---------

Co-authored-by: Luke Usher <[email protected]>
  • Loading branch information
asiekierka and LukeUsher authored Nov 7, 2023
1 parent 020410e commit c27f369
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 7 deletions.
7 changes: 6 additions & 1 deletion ares/fc/cpu/memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ inline auto CPU::readBus(n16 address) -> n8 {
}

inline auto CPU::writeBus(n16 address, n8 data) -> void {
// The EPSM can be mapped via $4016 writes, but also using a "passthrough"
// cartridge mapping at f.e. $401C-$401F. This kludge allows both types of
// writes to go through, which is required for most EPSM software around.
expansionPort.writeIO(address, data);

cartridge.writePRG(address, data);
if(address <= 0x1fff) return ram.write(address, data);
if(address <= 0x3fff) return ppu.writeIO(address, data);
Expand Down Expand Up @@ -66,7 +71,7 @@ auto CPU::writeIO(n16 address, n8 data) -> void {
case 0x4016: {
controllerPort1.latch(data.bit(0));
controllerPort2.latch(data.bit(0));
expansionPort.write(data.bit(0,2));
expansionPort.write(data);
return;
}

Expand Down
88 changes: 88 additions & 0 deletions ares/fc/expansion/epsm/epsm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
EPSM::EPSM(Node::Port parent) : ymf288(interface) {
node = parent->append<Node::Peripheral>("EPSM");
ioAddress = 0x401c;

streamFM = node->append<Node::Audio::Stream>("EPSM FM");
streamFM->setChannels(2);
streamFM->setFrequency(ymf288.sample_rate(8_MHz));
streamFM->addHighPassFilter( 20.0, 1);
streamFM->addLowPassFilter(2840.0, 1);

streamSSG = node->append<Node::Audio::Stream>("EPSM SSG");
streamSSG->setChannels(1);
streamSSG->setFrequency(ymf288.sample_rate(8_MHz));

ymf288.reset();
clocksPerSample = clocksPerSample = 8_MHz / ymf288.sample_rate(8_MHz);
Thread::create(8_MHz, {&EPSM::main, this});
}

EPSM::~EPSM() {
node->remove(streamFM);
node->remove(streamSSG);
node.reset();
Thread::destroy();
}

auto EPSM::main() -> void {
ymfm::ymf288::output_data output;
ymf288.generate(&output);

streamFM->frame(output.data[0] / 32768.0, output.data[1] / 32768.0);
streamSSG->frame(output.data[2] / (4.0 * 32768.0));

step(clocksPerSample);
}

auto EPSM::step(u32 clocks) -> void {
if(busyCyclesRemaining) {
busyCyclesRemaining -= clocks;
if(busyCyclesRemaining <= 0) {
busyCyclesRemaining = 0;
}
}

for(u32 timer : range(2)) {
if(timerCyclesRemaining[timer]) {
timerCyclesRemaining[timer] -= clocks;
if(timerCyclesRemaining[timer] <= 0) {
timerCyclesRemaining[timer] = 0;
interface.timerCallback(timer);
}
}
}

Thread::step(clocks);
Thread::synchronize();
}

auto EPSM::read1() -> n1 {
return 0;
}

auto EPSM::read2() -> n5 {
return 0b00000;
}

auto EPSM::write(n8 data) -> void {
if (data.bit(1) != latch) {
latch = data.bit(1);
if (latch) {
ymAddress.bit(0,1) = data.bit(2,3);
ymData.bit(4,7) = data.bit(4,7);
} else {
ymData.bit(0,3) = data.bit(4,7);
ymf288.write(ymAddress, ymData);
}
}
}

auto EPSM::writeIO(n16 address, n8 data) -> void {
if ((address & 0xFFFC) != ioAddress) return;
ymf288.write(address, data);
}

void EPSM::Interface::ymfm_update_irq(bool asserted) {
// TODO: Handle conflicts between cartridge and EPSM IRQ.
cpu.irqLine(asserted ? 1 : 0);
}
47 changes: 47 additions & 0 deletions ares/fc/expansion/epsm/epsm.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
struct EPSM : Expansion, Thread {
Node::Audio::Stream streamFM;
Node::Audio::Stream streamSSG;

EPSM(Node::Port);
~EPSM();

auto main() -> void;
auto step(u32 clocks) -> void;

auto read1() -> n1 override;
auto read2() -> n5 override;
auto write(n8 data) -> void override;

auto writeIO(n16 address, n8 data) -> void override;

private:
class Interface : public ymfm::ymfm_interface {
public:
Interface(EPSM& self) : self{self} {}

void timerCallback(uint32_t timer) { m_engine->engine_timer_expired(timer); }
void ymfm_set_busy_end(uint32_t clocks) override { self.busyCyclesRemaining = clocks; }
bool ymfm_is_busy() override { return self.busyCyclesRemaining > 0; }
void ymfm_update_irq(bool asserted) override;

void ymfm_set_timer(uint32_t timer, int32_t duration) override {
if (duration < 0) {
self.timerCyclesRemaining[timer] = 0;
} else {
self.timerCyclesRemaining[timer] = duration;
}
}

EPSM& self;
} interface{*this};

n1 latch;
n2 ymAddress;
n8 ymData;
n16 ioAddress;

ymfm::ymf288 ymf288;
s32 busyCyclesRemaining = 0;
s32 timerCyclesRemaining[2] = {0, 0};
s32 clocksPerSample = 0;
};
1 change: 1 addition & 0 deletions ares/fc/expansion/expansion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ares::Famicom {

#include "port.cpp"
#include "epsm/epsm.cpp"
#include "family-keyboard/family-keyboard.cpp"

}
9 changes: 8 additions & 1 deletion ares/fc/expansion/expansion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,15 @@ struct Expansion {

virtual auto read1() -> n1 { return 0; }
virtual auto read2() -> n5 { return 0; }
virtual auto write(n3 data) -> void {}

// The Famicom/NES expansion port only exposes three bits for $4016 writes:
// OUT0, OUT1, and OUT2. However, the NES additionally exposes the CPU's
// data bus, allowing reading all eight bits written to $4016. This is
// required for emulating the EPSM.
virtual auto write(n8 data) -> void {}
virtual auto writeIO(n16 address, n8 data) -> void {}
};

#include "port.hpp"
#include "epsm/epsm.hpp"
#include "family-keyboard/family-keyboard.hpp"
4 changes: 2 additions & 2 deletions ares/fc/expansion/family-keyboard/family-keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ auto FamilyKeyboard::read2() -> n5 {
return data;
}

auto FamilyKeyboard::write(n3 data) -> void {
latch = data;
auto FamilyKeyboard::write(n8 data) -> void {
latch = data.bit(0,2);
if(column && !latch.bit(1)) row = (row + 1) % 10;
column = latch.bit(1);
if(latch.bit(0)) row = 0;
Expand Down
2 changes: 1 addition & 1 deletion ares/fc/expansion/family-keyboard/family-keyboard.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct FamilyKeyboard : Expansion {
FamilyKeyboard(Node::Port);
auto read1() -> n1 override;
auto read2() -> n5 override;
auto write(n3 data) -> void override;
auto write(n8 data) -> void override;

private:
n3 latch;
Expand Down
3 changes: 2 additions & 1 deletion ares/fc/expansion/port.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ auto ExpansionPort::load(Node::Object parent) -> void {
port->setHotSwappable(true);
port->setAllocate([&](auto name) { return allocate(name); });
port->setDisconnect([&] { device.reset(); });
port->setSupported({"Family Keyboard"});
port->setSupported({"EPSM", "Family Keyboard"});
}

auto ExpansionPort::unload() -> void {
Expand All @@ -19,6 +19,7 @@ auto ExpansionPort::unload() -> void {
}

auto ExpansionPort::allocate(string name) -> Node::Peripheral {
if(name == "EPSM") device = new EPSM(port);
if(name == "Family Keyboard") device = new FamilyKeyboard(port);
if(device) return device->node;
return {};
Expand Down
3 changes: 2 additions & 1 deletion ares/fc/expansion/port.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ struct ExpansionPort {

auto read1() -> n1 { if(device) return device->read1(); return 0; }
auto read2() -> n5 { if(device) return device->read2(); return 0; }
auto write(n3 data) -> void { if(device) return device->write(data); }
auto write(n8 data) -> void { if(device) return device->write(data); }
auto writeIO(n16 address, n8 data) -> void { if(device) return device->writeIO(address, data); }

auto serialize(serializer&) -> void;

Expand Down
1 change: 1 addition & 0 deletions ares/fc/fc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <component/audio/ym2413/ym2413.hpp>
#include <component/eeprom/m24c/m24c.hpp>
#include <component/flash/sst39sf0x0/sst39sf0x0.hpp>
#include "ymfm_opn.h"

namespace ares::Famicom {
#include <ares/inline.hpp>
Expand Down
7 changes: 7 additions & 0 deletions desktop-ui/emulator/famicom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ auto Famicom::load() -> bool {
port->connect();
}

if(game->pak->attribute("system") == "EPSM") {
if(auto port = root->find<ares::Node::Port>("Expansion Port")) {
port->allocate("EPSM");
port->connect();
}
}

return true;
}

Expand Down
13 changes: 13 additions & 0 deletions mia/medium/famicom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ auto Famicom::load(string location) -> bool {
pak->setAttribute("region", document["game/region"].string());
pak->setAttribute("board", document["game/board"].string());
pak->setAttribute("mirror", document["game/board/mirror/mode"].string());
pak->setAttribute("system", document["game/system"].string());
pak->setAttribute("chip", document["game/board/chip/type"].string());
pak->setAttribute("chip/key", document["game/board/chip/key"].natural());
pak->setAttribute("pinout/a0", document["game/board/chip/pinout/a0"].natural());
Expand Down Expand Up @@ -172,6 +173,7 @@ auto Famicom::analyzeINES(vector<u8>& data) -> string {
u32 chrram = chrrom == 0u ? 8192u : 0u;
u32 chrnvram = 0u;
u32 submapper = 0u;
string system = "Regular";
bool battery = (data[6] & 0x02) != 0;
bool eepromMapper = false;
bool prgromFlash = false;
Expand All @@ -182,6 +184,16 @@ auto Famicom::analyzeINES(vector<u8>& data) -> string {
if(iNes2) {
mapper |= ((data[8] & 0xf) << 8);
submapper = data[8] >> 4;
u32 consoleType = data[7] & 0x3;
if(consoleType == 3) {
consoleType = data[13]& 0xf;
}

string types[16] = {
"Regular", "Vs. System", "PlayChoice-10", "BCD", "EPSM", "VT01", "VT02", "VT03",
"VT09", "VT32", "VT360", "UMC UM6578", "Network System", "Reserved", "Reserved", "Reserved"
};
system = types[consoleType];

prgrom = calculateNes2RomSize(data[4], data[9] & 0xf, 0x4000);
chrrom = calculateNes2RomSize(data[5], data[9] >> 4, 0x2000);
Expand Down Expand Up @@ -209,6 +221,7 @@ auto Famicom::analyzeINES(vector<u8>& data) -> string {
s +={" name: ", Medium::name(location), "\n"};
s +={" title: ", Medium::name(location), "\n"};
s +={" region: ", region, "\n"};
s +={" system: ", system, "\n"};

switch(mapper) {

Expand Down

0 comments on commit c27f369

Please sign in to comment.