From 3c49020babd1d5805af22d1cbc44f9fc22a99c5f Mon Sep 17 00:00:00 2001 From: Vas Crabb Date: Wed, 14 Sep 2022 02:41:30 +1000 Subject: [PATCH] bus/gameboy: Added basic HuC-3 real-time clock simulation, and cleanup. * Added MBC30 as a distinct slot option for documentation purposes. * Added heuristics to detect MBC30 for GBX and plain ROM dump files. * mbc.cpp: Disabled noisy logging. --- hash/gbcolor.xml | 2 +- src/devices/bus/gameboy/carts.cpp | 2 + src/devices/bus/gameboy/carts.h | 1 + src/devices/bus/gameboy/gbslot.cpp | 37 +++- src/devices/bus/gameboy/huc3.cpp | 307 +++++++++++++++++++++++++++-- src/devices/bus/gameboy/mbc.cpp | 8 +- 6 files changed, 336 insertions(+), 21 deletions(-) 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) {