diff --git a/hash/gbcolor.xml b/hash/gbcolor.xml
index b461c2c6375..1ecd062d98c 100644
--- a/hash/gbcolor.xml
+++ b/hash/gbcolor.xml
@@ -15950,7 +15950,7 @@ license:CC0
-
+
diff --git a/src/devices/bus/gameboy/carts.cpp b/src/devices/bus/gameboy/carts.cpp
index b2ea83788d8..98ca0f40d95 100644
--- a/src/devices/bus/gameboy/carts.cpp
+++ b/src/devices/bus/gameboy/carts.cpp
@@ -37,6 +37,7 @@ char const *const GB_GBCK003 = "rom_gbck003";
char const *const GB_MBC1 = "rom_mbc1";
char const *const GB_MBC2 = "rom_mbc2";
char const *const GB_MBC3 = "rom_mbc3";
+char const *const GB_MBC30 = "rom_mbc30";
char const *const GB_MBC5 = "rom_mbc5";
char const *const GB_MBC6 = "rom_mbc6";
char const *const GB_MBC7_2K = "rom_mbc7_2k";
@@ -76,6 +77,7 @@ void gameboy_cartridges(device_slot_interface &device)
device.option_add_internal(slotoptions::GB_MBC1, GB_ROM_MBC1);
device.option_add_internal(slotoptions::GB_MBC2, GB_ROM_MBC2);
device.option_add_internal(slotoptions::GB_MBC3, GB_ROM_MBC3);
+ device.option_add_internal(slotoptions::GB_MBC30, GB_ROM_MBC3); // MBC3 and MBC30 treated as the same thing for now
device.option_add_internal(slotoptions::GB_MBC5, GB_ROM_MBC5);
device.option_add_internal(slotoptions::GB_MBC6, GB_ROM_MBC6);
device.option_add_internal(slotoptions::GB_MBC7_2K, GB_ROM_MBC7_2K);
diff --git a/src/devices/bus/gameboy/carts.h b/src/devices/bus/gameboy/carts.h
index 02340527bd1..2d330f104ff 100644
--- a/src/devices/bus/gameboy/carts.h
+++ b/src/devices/bus/gameboy/carts.h
@@ -31,6 +31,7 @@ extern char const *const GB_MBC1;
extern char const *const GB_MBC2;
extern char const *const GB_MBC3;
extern char const *const GB_MBC3;
+extern char const *const GB_MBC30;
extern char const *const GB_MBC5;
extern char const *const GB_MBC6;
extern char const *const GB_MBC7_2K;
diff --git a/src/devices/bus/gameboy/gbslot.cpp b/src/devices/bus/gameboy/gbslot.cpp
index 43f7b7931f7..be5548bd426 100644
--- a/src/devices/bus/gameboy/gbslot.cpp
+++ b/src/devices/bus/gameboy/gbslot.cpp
@@ -484,6 +484,32 @@ bool is_wisdom_tree(std::string_view tag, util::random_read &file, u64 length, u
}
+bool is_mbc30(std::string_view tag, util::random_read &file, u64 length, u64 offset, u8 const *header)
+{
+ // MBC30 supposedly has an additional ROM bank output
+ if ((u32(0x4000) << 7) < length)
+ {
+ osd_printf_verbose(
+ "[%s] Assuming 0x%06X-byte cartridge declaring MBC3 controller uses MBC30\n",
+ tag,
+ length);
+ return true;
+ }
+
+ // MBC30 has three RAM bank outputs, supporting up to 64 KiB static RAM
+ if (cartheader::RAM_SIZE_64K == header[cartheader::OFFSET_RAM_SIZE - 0x100])
+ {
+ osd_printf_verbose(
+ "[%s] Assuming cartridge declaring MBC3 controller with 64 KiB RAM uses MBC30\n",
+ tag);
+ return true;
+ }
+
+ // MBC3 should be fine
+ return false;
+}
+
+
bool is_m161(std::string_view tag, util::random_read &file, u64 length, u64 offset, u8 const *header)
{
// supports eight 32 KiB banks at most, doesn't make sense without at least two banks
@@ -667,7 +693,10 @@ std::optional probe_gbx_footer(std::string_view tag, util::random_
result = slotoptions::GB_MBC2;
break;
case gbxfile::TYPE_MBC3:
- result = slotoptions::GB_MBC3;
+ if (((u32(0x4000) << 7) < leader.rom_bytes) || ((u32(0x2000) << 2) < leader.ram_bytes))
+ result = slotoptions::GB_MBC30;
+ else
+ result = slotoptions::GB_MBC3;
break;
case gbxfile::TYPE_MBC5:
result = slotoptions::GB_MBC5;
@@ -845,14 +874,20 @@ char const *guess_cart_type(std::string_view tag, util::random_read &file, u64 l
return slotoptions::GB_MMM01;
// 0x0e
case cartheader::TYPE_MBC3_RTC_BATT:
+ if (is_mbc30(tag, file, length, offset, header))
+ return slotoptions::GB_MBC30;
return slotoptions::GB_MBC3;
case cartheader::TYPE_MBC3_RTC_RAM_BATT:
if (is_m161(tag, file, length, offset, header))
return slotoptions::GB_M161;
+ else if (is_mbc30(tag, file, length, offset, header))
+ return slotoptions::GB_MBC30;
return slotoptions::GB_MBC3;
case cartheader::TYPE_MBC3:
case cartheader::TYPE_MBC3_RAM:
case cartheader::TYPE_MBC3_RAM_BATT:
+ if (is_mbc30(tag, file, length, offset, header))
+ return slotoptions::GB_MBC30;
return slotoptions::GB_MBC3;
// 0x14
case cartheader::TYPE_MBC5:
diff --git a/src/devices/bus/gameboy/huc3.cpp b/src/devices/bus/gameboy/huc3.cpp
index 1ab47ce6da0..e1326b3a9d1 100644
--- a/src/devices/bus/gameboy/huc3.cpp
+++ b/src/devices/bus/gameboy/huc3.cpp
@@ -69,17 +69,18 @@
when it has completed the command and is ready to execute another command.
Five of the eight possible commands are used by the games:
- 0x1 - Read register and increment address (value put in bits 3-0 of 0xC)
- 0x3 - Write register and increment address (value from bits 3-0 of 0xB)
- 0x4 - Set register address low nybble
- 0x5 - Set register address high nybble
- 0x6 - Execute extended command (selector from bits 3-0 of 0xB)
+ 0x1 - Read register and increment address (value put in bits 3-0 of 0xC).
+ 0x3 - Write register and increment address (value from bits 3-0 of 0xB).
+ 0x4 - Set register address low nybble.
+ 0x5 - Set register address high nybble.
+ 0x6 - Execute extended command (selector from bits 3-0 of 0xB).
The games use four of the sixteen possible extended commands:
- 0x0 - Atomically read real-time clock to registers 0-6
- 0x1 - Atomically write real-time clock from registers 0-6
- 0x2 - Some kind of handshake/status request - sets result to 0x1
- 0xe - Sent twice to trigger melody generator
+ 0x0 - Atomically read real-time clock to registers 0x00-0x06.
+ 0x1 - Atomically write real-time clock from registers 0x00-0x06.
+ Also updates event time in registers 0x58-0x5D.
+ 0x2 - Some kind of handshake/status request - sets result to 0x1.
+ 0xe - Sent twice to trigger melody generator.
Registers are likely a window into the microcontroller's memory. Known
registers:
@@ -89,11 +90,12 @@
0x13-15 - Day counter (least significant nybble low)
0x26 - Bits 1-0 select melody
0x27 - Enable (0x1) or disable (not 0x1) melody
+ 0x58-5A - Event time minutes (least significant nybble low)
+ 0x5B-5D - Event time days (least significant nybble low)
TODO:
- * Implement real-time clock.
+ * Simulate more microcontroller functionality as it's discovered.
* Simulate melody generator?
- * Which of the internal registers are battery-backed?
* What is the default state for banking and infrared select on reset?
* Does ROM bank 0 map to bank 1 like MBC1?
* How many RAM page lines are there? No games use more than 2.
@@ -104,10 +106,16 @@
#include "huc3.h"
#include "cartbase.ipp"
+#include "gbxfile.h"
+
+#include "dirtc.h"
#include
+#include
#include
+#include
#include
+#include
//#define VERBOSE 1
//#define LOG_OUTPUT_FUNC osd_printf_info
@@ -118,7 +126,10 @@ namespace bus::gameboy {
namespace {
-class huc3_device : public mbc_ram_device_base
+class huc3_device :
+ public mbc_ram_device_base,
+ public device_rtc_interface,
+ public device_nvram_interface
{
public:
static constexpr feature_type unemulated_features() { return feature::SOUND | feature::COMMS; }
@@ -130,6 +141,13 @@ protected:
virtual void device_start() override ATTR_COLD;
virtual void device_reset() override ATTR_COLD;
+ virtual void rtc_clock_updated(int year, int month, int day, int day_of_week, int hour, int minute, int second) override ATTR_COLD;
+
+ virtual void nvram_default() override ATTR_COLD;
+ virtual bool nvram_read(util::read_stream &file) override ATTR_COLD;
+ virtual bool nvram_write(util::write_stream &file) override ATTR_COLD;
+ virtual bool nvram_can_write() const override ATTR_COLD;
+
private:
void io_select(u8 data);
void bank_switch_fine(u8 data);
@@ -141,6 +159,8 @@ private:
u8 read_ir(address_space &space);
void write_ir(u8 data);
+ TIMER_CALLBACK_MEMBER(rtc_advance_seconds);
+
void execute_instruction()
{
switch (m_ctrl_data & 0x0f)
@@ -150,8 +170,35 @@ private:
std::copy_n(&m_registers[0x10], 7, &m_registers[0x00]);
break;
case 0x1:
- LOG("Instruction 0x2 - atomic RTC write\n");
- std::copy_n(&m_registers[0x00], 7, &m_registers[0x10]);
+ {
+ LOG("Instruction 0x2 - atomic RTC write\n");
+
+ s16 const newminutes(read_12bit(0x00));
+ s16 const newdays(read_12bit(0x03));
+ s16 const oldminutes(read_12bit(0x10));
+ s16 const olddays(read_12bit(0x13));
+ s16 const eventminutes(read_12bit(0x58));
+ s16 const eventdays(read_12bit(0x5b));
+
+ s16 minutesdelta(newminutes - oldminutes);
+ s16 daysdelta(newdays - olddays);
+ while ((60 * 24) <= (eventminutes + minutesdelta))
+ {
+ minutesdelta -= 60 * 24;
+ ++daysdelta;
+ }
+ while (0 > (eventminutes + minutesdelta))
+ {
+ minutesdelta += 60 * 24;
+ --daysdelta;
+ }
+
+ assert(0 <= (eventminutes + minutesdelta));
+ assert((60 * 24) > (eventminutes + minutesdelta));
+ std::copy_n(&m_registers[0x00], 7, &m_registers[0x10]);
+ write_12bit(0x58, s16(eventminutes + minutesdelta));
+ write_12bit(0x5b, s16(eventdays + daysdelta));
+ }
break;
case 0x2:
logerror("Instruction 0x2 - setting data to 0x1\n");
@@ -168,8 +215,27 @@ private:
}
}
- memory_view m_view_io;
+ u16 read_12bit(u8 offset) const
+ {
+ return
+ (u16(m_registers[(offset + 0) & 0xff] & 0x0f) << 0) |
+ (u16(m_registers[(offset + 1) & 0xff] & 0x0f) << 4) |
+ (u16(m_registers[(offset + 2) & 0xff] & 0x0f) << 8);
+ }
+ void write_12bit(u8 offset, u16 data)
+ {
+ m_registers[(offset + 0) & 0xff] = (data >> 0) & 0x0f;
+ m_registers[(offset + 1) & 0xff] = (data >> 4) & 0x0f;
+ m_registers[(offset + 2) & 0xff] = (data >> 8) & 0x0f;
+ }
+
+ memory_view m_view_io;
+ emu_timer *m_timer_rtc;
+ s64 m_machine_seconds;
+ bool m_has_battery;
+
+ u8 m_seconds;
u8 m_ctrl_cmd;
u8 m_ctrl_data;
u8 m_ctrl_addr;
@@ -183,7 +249,13 @@ huc3_device::huc3_device(
device_t *owner,
u32 clock) :
mbc_ram_device_base(mconfig, GB_ROM_HUC3, tag, owner, clock),
+ device_rtc_interface(mconfig, *this),
+ device_nvram_interface(mconfig, *this),
m_view_io(*this, "io"),
+ m_timer_rtc(nullptr),
+ m_machine_seconds(0),
+ m_has_battery(false),
+ m_seconds(0U),
m_ctrl_cmd(0U),
m_ctrl_data(0U),
m_ctrl_addr(0U)
@@ -193,6 +265,41 @@ huc3_device::huc3_device(
image_init_result huc3_device::load(std::string &message)
{
+ // check for backup battery
+ if (loaded_through_softlist())
+ {
+ // if there's an NVRAM region, there must be a backup battery
+ if (cart_nvram_region())
+ {
+ logerror("Found 'nvram' region, backup battery must be present\n");
+ m_has_battery = true;
+ }
+ else
+ {
+ logerror("No 'nvram' region found, assuming no backup battery present\n");
+ m_has_battery = true;
+ }
+ }
+ else
+ {
+ gbxfile::leader_1_0 leader;
+ u8 const *extra;
+ u32 extralen;
+ if (gbxfile::get_data(gbx_footer_region(), leader, extra, extralen))
+ {
+ m_has_battery = bool(leader.batt);
+ logerror(
+ "GBX format image specifies %sbackup battery present\n",
+ m_has_battery ? "" : "no ");
+ }
+ else
+ {
+ // just assume the coin cell is present - every known game has it
+ logerror("Assuming backup battery present\n");
+ m_has_battery = true;
+ }
+ }
+
// check for valid ROM/RAM regions
set_bank_bits_rom(2, 7);
set_bank_bits_ram(2);
@@ -244,12 +351,18 @@ void huc3_device::device_start()
{
mbc_ram_device_base::device_start();
+ m_seconds = 0U;
std::fill(std::begin(m_registers), std::end(m_registers), 0U);
+ m_timer_rtc = timer_alloc(FUNC(huc3_device::rtc_advance_seconds), this);
+
+ save_item(NAME(m_seconds));
save_item(NAME(m_ctrl_cmd));
save_item(NAME(m_ctrl_data));
save_item(NAME(m_ctrl_addr));
save_item(NAME(m_registers));
+
+ m_timer_rtc->adjust(attotime(1, 0), 0, attotime(1, 0));
}
@@ -270,6 +383,149 @@ void huc3_device::device_reset()
}
+void huc3_device::rtc_clock_updated(
+ int year,
+ int month,
+ int day,
+ int day_of_week,
+ int hour,
+ int minute,
+ int second)
+{
+ if (!m_has_battery)
+ {
+ logerror("No battery present, not updating for elapsed time\n");
+ }
+ else if (std::numeric_limits::min() == m_machine_seconds)
+ {
+ logerror("Failed to load machine time from previous session, not updating for elapsed time\n");
+ }
+ else
+ {
+ // do a simple seconds elapsed since last run calculation
+ system_time current;
+ machine().current_datetime(current);
+ s64 delta(std::make_signed_t(current.time) - m_machine_seconds);
+ logerror("Previous session time, %d current time %d, delta %d\n", current.time, m_machine_seconds, delta);
+ if (0 > delta)
+ {
+ // This happens if the user runs the emulation faster
+ // than real time, exits, and then starts again without
+ // waiting for the difference between emulated and real
+ // time to elapse.
+ logerror("Previous session ended in the future, not updating for elapsed time\n");
+ }
+ else
+ {
+ // combine the counter nybbles for convenience
+ u16 minutes(read_12bit(0x10));
+ u16 days(read_12bit(0x13));
+ logerror(
+ "Time before applying delta %u %02u:%02u:%02u\n",
+ days,
+ minutes / 60,
+ minutes % 60,
+ m_seconds);
+
+ // deal with seconds
+ unsigned s(delta % 60);
+ delta /= 60;
+ if (64 <= m_seconds)
+ {
+ m_seconds = 0U;
+ --s;
+ ++delta;
+ }
+ if (60 <= (m_seconds + s))
+ ++delta;
+ m_seconds = (m_seconds + s) % 60;
+
+ // update the minute counter value
+ unsigned m(delta % (60 * 24));
+ delta /= 60 * 24;
+ if ((60 * 24) <= minutes)
+ {
+ minutes = 0U;
+ --m;
+ ++delta;
+ }
+ if ((60 * 24) <= (minutes + m))
+ ++delta;
+ minutes = (minutes + m) % (60 * 24);
+
+ // no special handling for day counter
+ days += delta;
+
+ // write the counter nybbles back to registers
+ write_12bit(0x10, minutes);
+ write_12bit(0x13, days);
+ logerror(
+ "Time after applying delta %u %02u:%02u:%02u\n",
+ days,
+ minutes / 60,
+ minutes % 60,
+ m_seconds);
+ }
+ }
+}
+
+
+void huc3_device::nvram_default()
+{
+ // TODO: proper cold RTC state
+ m_machine_seconds = std::numeric_limits::min();
+ m_seconds = 0U;
+ std::fill(std::begin(m_registers), std::end(m_registers), 0U);
+}
+
+
+bool huc3_device::nvram_read(util::read_stream &file)
+{
+ if (m_has_battery)
+ {
+ // read previous machine time (seconds since epoch), seconds counter, and register contents
+ u64 machinesecs;
+ std::size_t actual;
+ if (file.read(&machinesecs, sizeof(machinesecs), actual) || (sizeof(machinesecs) != actual))
+ return false;
+ m_machine_seconds = big_endianize_int64(machinesecs);
+
+ if (file.read(&m_seconds, sizeof(m_seconds), actual) || (sizeof(m_seconds) != actual))
+ return false;
+ if (file.read(&m_registers[0], sizeof(m_registers), actual) || (sizeof(m_registers) != actual))
+ return false;
+ }
+ else
+ {
+ logerror("No battery present, not loading real-time clock register contents\n");
+ }
+ return true;
+}
+
+
+bool huc3_device::nvram_write(util::write_stream &file)
+{
+ // save current machine time as seconds since epoch, seconds counter, and register contents
+ system_time current;
+ machine().current_datetime(current);
+ u64 const machinesecs(big_endianize_int64(s64(std::make_signed_t(current.time))));
+ std::size_t written;
+ if (file.write(&machinesecs, sizeof(machinesecs), written) || (sizeof(machinesecs) != written))
+ return false;
+ if (file.write(&m_seconds, sizeof(m_seconds), written) || (sizeof(m_seconds) != written))
+ return false;
+ if (file.write(&m_registers[0], sizeof(m_registers), written) || (sizeof(m_registers) != written))
+ return false;
+ return true;
+}
+
+
+bool huc3_device::nvram_can_write() const
+{
+ return m_has_battery;
+}
+
+
void huc3_device::io_select(u8 data)
{
switch (data & 0x0f)
@@ -402,6 +658,27 @@ void huc3_device::write_ir(u8 data)
LOG("%s: Infrared write 0x%02X\n", machine().describe_context(), data);
}
+
+TIMER_CALLBACK_MEMBER(huc3_device::rtc_advance_seconds)
+{
+ if ((60 - 1) > m_seconds)
+ {
+ ++m_seconds;
+ return;
+ }
+
+ m_seconds = 0U;
+ u16 const minutes(read_12bit(0x10));
+ if (((60 * 24) - 1) > minutes)
+ {
+ write_12bit(0x10, minutes + 1);
+ return;
+ }
+
+ write_12bit(0x10, 0);
+ write_12bit(0x13, read_12bit(0x13) + 1);
+}
+
} // anonymous namespace
} // namespace bus::gameboy
diff --git a/src/devices/bus/gameboy/mbc.cpp b/src/devices/bus/gameboy/mbc.cpp
index 4b705e1cc63..4edb3bdd2cc 100644
--- a/src/devices/bus/gameboy/mbc.cpp
+++ b/src/devices/bus/gameboy/mbc.cpp
@@ -105,8 +105,8 @@
#include
#include
-#define VERBOSE 1
-#define LOG_OUTPUT_FUNC osd_printf_info
+//#define VERBOSE 1
+//#define LOG_OUTPUT_FUNC osd_printf_info
#include "logmacro.h"
@@ -754,7 +754,7 @@ protected:
int day_of_week,
int hour,
int minute,
- int second) override
+ int second) override ATTR_COLD
{
if (!m_has_rtc_xtal && !m_has_battery)
{
@@ -960,7 +960,7 @@ private:
// TODO: what happens with the RAM bank outputs when the RTC is selected?
// TODO: what happens for 4-7?
// TODO: is the high nybble ignored altogether?
- bank_switch_coarse(data & 0x03);
+ bank_switch_coarse(data & 0x07);
m_rtc_select = data;
if (m_rtc_enable)
{