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.
This commit is contained in:
Vas Crabb 2022-09-14 02:41:30 +10:00
parent da7bdd575c
commit 3c49020bab
6 changed files with 336 additions and 21 deletions

View File

@ -15950,7 +15950,7 @@ license:CC0
<feature name="u3" value="U3 RAM [D431000AGW-70LL]" />
<feature name="u4" value="U4 MM1134 [6735]" />
<feature name="battery" value="CR2025" />
<feature name="slot" value="rom_mbc3" />
<feature name="slot" value="rom_mbc30" />
<feature name="rtc" value="yes" />
<dataarea name="rom" size="2097152">
<rom name="cgb-bxtj-0.u1" size="2097152" crc="270c4ecc" sha1="95127b901bbce2407daf43cce9f45d4c27ef635d"/>

View File

@ -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);

View File

@ -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;

View File

@ -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<char const *> 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:

View File

@ -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 <algorithm>
#include <cassert>
#include <iterator>
#include <limits>
#include <string>
#include <type_traits>
//#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<mbc_dual_device_base>
class huc3_device :
public mbc_ram_device_base<mbc_dual_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<mbc_dual_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<mbc_dual_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<s64>::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<decltype(current.time)>(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<s64>::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<decltype(current.time)>(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

View File

@ -105,8 +105,8 @@
#include <sstream>
#include <type_traits>
#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)
{