Add reflashing, fix bugs and traceid generation

This commit is contained in:
spicyjpeg 2023-06-25 06:49:39 +02:00
parent 63651e7114
commit 51ac032198
No known key found for this signature in database
GPG Key ID: 5CC87404C01DF393
18 changed files with 517 additions and 132 deletions

View File

@ -8,7 +8,11 @@
},
"cartEraseWorker": {
"erase": "Performing cartridge erase...",
"error": "An error occurred while erasing the cartridge's EEPROM.\n\nPress the Test button to view debug logs."
"error": "An error occurred while erasing the cartridge's EEPROM. The unlocking key may or may not have been successfully changed.\n\nPress the Test button to view debug logs."
},
"cartReflashWorker": {
"flash": "Performing cartridge reflash...",
"error": "An error occurred while reflashing the cartridge. The unlocking key may or may not have been successfully changed.\n\nPress the Test button to view debug logs."
},
"cartUnlockWorker": {
"read": "Dumping cartridge...",
@ -56,22 +60,31 @@
"prompt": "Save the contents of the cartridge's EEPROM to a file on the IDE hard drive or CF card connected as secondary drive (if any)."
},
"hexdump": {
"name": "[UNIMPLEMENTED] View cartridge hexdump",
"prompt": "Display the raw contents of the cartridge's EEPROM."
"name": "View cartridge hexdump",
"prompt": "Display the raw contents of the cartridge's EEPROM in hexadecimal format."
},
"resetSystemID": {
"name": "Reset system identifier (unpair cartridge)",
"prompt": "Delete any previously saved system identifier, allowing the cartridge to be used to reinstall the game on any system.",
"confirm": "The system identifier will be cleared, allowing the cartridge to be used on a system with any digital I/O board. The game will have to be reinstalled in order to pair the cartridge to the new board.\n\nDo you wish to proceed?"
"confirm": "The system identifier will be cleared, allowing the cartridge to be used on a system with any digital I/O board. The game will have to be reinstalled in order to pair the cartridge to the new board.\n\nDo you wish to proceed?",
"error": "The system identifier is not present or has already been cleared on this cartridge."
},
"matchSystemID": {
"name": "Copy system identifier (pair to this system)",
"prompt": "Set the saved system identifier to allow the cartridge to be used on this system without having to reinstall the game first.",
"confirm": "The system identifier will be changed, allowing the cartridge to be used on this system. If already installed here, the game will not have to be reinstalled.\n\nDo you wish to proceed?",
"error": "No digital I/O board found. Copying the system identifier requires a digital I/O board to be installed in this system."
},
"editSystemID": {
"name": "Edit system identifier (pair to another I/O board)",
"prompt": "Edit the saved system identifier to allow the cartridge to be used on a specific system without having to reinstall the game first.",
"confirm": "The system identifier will be changed, allowing the cartridge to be used on the respective system. If already installed on the new system, the game will not have to be reinstalled.\n\nDo you wish to proceed?"
"confirm": "The system identifier will be changed, allowing the cartridge to be used on the respective system. If already installed on the new system, the game will not have to be reinstalled.\n\nDo you wish to proceed?",
"error": "The system identifier entered is invalid. Make sure all digits are correct and try again."
},
"reflash": {
"name": "[UNIMPLEMENTED] Erase and convert to another game",
"prompt": "Wipe all data and flash the cartridge with another game's identifiers. All cartridges can be converted for use with any other game that uses the same cartridge type."
"name": "Erase and convert cartridge to another game",
"prompt": "Wipe all data and flash the cartridge with another game's identifiers. All cartridges can be converted for use with any other game that uses the same cartridge type.",
"confirm": "The contents of the cartridge's EEPROM will be replaced, the system identifier (if any) will be cleared and the unlocking key will be updated to match the new game.\n\nDo you wish to proceed?"
},
"erase": {
"name": "Erase cartridge",
@ -151,6 +164,11 @@
"ok": "Continue"
},
"HexdumpScreen": {
"title": "{CART_ICON} Cartridge dump",
"prompt": "Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back."
},
"KeyEntryScreen": {
"title": "Enter unlocking key",
"body": "Enter the 8-byte key this cartridge was last locked with.\n\nUse {LEFT_BUTTON}{RIGHT_BUTTON} to move between digits, hold {START_BUTTON} and use {LEFT_BUTTON}{RIGHT_BUTTON} to edit the highlighted digit.",
@ -163,8 +181,14 @@
"prompt": "Scan this code and paste the resulting string into the decodeDump.py script provided alongside this tool to obtain a dump of the cartridge. Press {START_BUTTON} to go back."
},
"ReflashGameScreen": {
"title": "{CART_ICON} Select game to convert cartridge to",
"prompt": "Make sure you select the correct region. Note that cartridges can only be converted for use with games that accept the same cartridge type.",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to confirm, hold {LEFT_BUTTON} + {RIGHT_BUTTON} to go back"
},
"SystemIDEntryScreen": {
"title": "Enter system ID",
"title": "Edit system identifier",
"body": "Enter the new digital I/O board's identifier. To obtain the ID of another board, run this tool on its respective system.\n\nUse {LEFT_BUTTON}{RIGHT_BUTTON} to move between digits, hold {START_BUTTON} and use {LEFT_BUTTON}{RIGHT_BUTTON} to edit the highlighted digit.",
"cancel": "Cancel",
"ok": "Confirm"

Binary file not shown.

View File

@ -13,7 +13,7 @@ public:
void (CartActionsScreen::*target)(ui::Context &ctx);
};
static constexpr int _NUM_SYSTEM_ID_ACTIONS = 7;
static constexpr int _NUM_SYSTEM_ID_ACTIONS = 8;
static constexpr int _NUM_NO_SYSTEM_ID_ACTIONS = 5;
static const Action _ACTIONS[]{
@ -41,6 +41,10 @@ static const Action _ACTIONS[]{
.name = "CartActionsScreen.resetSystemID.name"_h,
.prompt = "CartActionsScreen.resetSystemID.prompt"_h,
.target = &CartActionsScreen::resetSystemID
}, {
.name = "CartActionsScreen.matchSystemID.name"_h,
.prompt = "CartActionsScreen.matchSystemID.prompt"_h,
.target = &CartActionsScreen::matchSystemID
}, {
.name = "CartActionsScreen.editSystemID.name"_h,
.prompt = "CartActionsScreen.editSystemID.prompt"_h,
@ -63,9 +67,11 @@ void CartActionsScreen::hddDump(ui::Context &ctx) {
}
void CartActionsScreen::hexdump(ui::Context &ctx) {
ctx.show(APP->_hexdumpScreen, false, true);
}
void CartActionsScreen::reflash(ui::Context &ctx) {
ctx.show(APP->_reflashGameScreen, false, true);
}
void CartActionsScreen::erase(ui::Context &ctx) {
@ -82,24 +88,58 @@ void CartActionsScreen::erase(ui::Context &ctx) {
}
void CartActionsScreen::resetSystemID(ui::Context &ctx) {
APP->_confirmScreen.setMessage(
*this,
[](ui::Context &ctx) {
APP->_parser->getIdentifiers()->systemID.clear();
APP->_parser->flush();
if (!(APP->_parser->getIdentifiers()->systemID.isEmpty())) {
APP->_confirmScreen.setMessage(
*this,
[](ui::Context &ctx) {
APP->_parser->getIdentifiers()->systemID.clear();
APP->_parser->flush();
APP->_setupWorker(&App::_cartWriteWorker);
ctx.show(APP->_workerStatusScreen, false, true);
},
STR("CartActionsScreen.resetSystemID.confirm")
);
APP->_setupWorker(&App::_cartWriteWorker);
ctx.show(APP->_workerStatusScreen, false, true);
},
STR("CartActionsScreen.resetSystemID.confirm")
);
ctx.show(APP->_confirmScreen, false, true);
ctx.show(APP->_confirmScreen, false, true);
} else {
APP->_errorScreen.setMessage(
*this, STR("CartActionsScreen.resetSystemID.error")
);
ctx.show(APP->_errorScreen, false, true);
}
}
void CartActionsScreen::matchSystemID(ui::Context &ctx) {
if (APP->_dump.flags & cart::DUMP_SYSTEM_ID_OK) {
APP->_confirmScreen.setMessage(
*this,
[](ui::Context &ctx) {
APP->_parser->getIdentifiers()->systemID.copyFrom(
APP->_dump.systemID.data
);
APP->_parser->flush();
APP->_setupWorker(&App::_cartWriteWorker);
ctx.show(APP->_workerStatusScreen, false, true);
},
STR("CartActionsScreen.matchSystemID.confirm")
);
ctx.show(APP->_confirmScreen, false, true);
} else {
APP->_errorScreen.setMessage(
*this, STR("CartActionsScreen.matchSystemID.error")
);
ctx.show(APP->_errorScreen, false, true);
}
}
void CartActionsScreen::editSystemID(ui::Context &ctx) {
APP->_confirmScreen.setMessage(
*this,
APP->_systemIDEntryScreen,
[](ui::Context &ctx) {
APP->_systemIDEntryScreen.setSystemID(*(APP->_parser));
@ -109,6 +149,10 @@ void CartActionsScreen::editSystemID(ui::Context &ctx) {
STR("CartActionsScreen.editSystemID.confirm")
);
APP->_errorScreen.setMessage(
APP->_systemIDEntryScreen, STR("CartActionsScreen.editSystemID.error")
);
ctx.show(APP->_systemIDEntryScreen, false, true);
}
@ -135,7 +179,10 @@ void CartActionsScreen::update(ui::Context &ctx) {
if (ctx.buttons.pressed(ui::BTN_START))
(this->*action.target)(ctx);
if (ctx.buttons.held(ui::BTN_LEFT) && ctx.buttons.held(ui::BTN_RIGHT))
if (
(ctx.buttons.held(ui::BTN_LEFT) && ctx.buttons.pressed(ui::BTN_RIGHT)) ||
(ctx.buttons.pressed(ui::BTN_LEFT) && ctx.buttons.held(ui::BTN_RIGHT))
)
ctx.show(APP->_cartInfoScreen, true, true);
}
@ -158,6 +205,69 @@ void QRCodeScreen::update(ui::Context &ctx) {
ctx.show(APP->_cartInfoScreen, true, true);
}
void HexdumpScreen::show(ui::Context &ctx, bool goBack) {
_title = STR("HexdumpScreen.title");
_body = _bodyText;
_prompt = STR("HexdumpScreen.prompt");
size_t length = APP->_dump.getChipSize().dataLength;
char *ptr = _bodyText, *end = &_bodyText[sizeof(_bodyText)];
for (size_t i = 0; i < length; i += 16) {
ptr += snprintf(ptr, end - ptr, "%04x: ", i);
ptr += util::hexToString(ptr, &APP->_dump.data[i], 16, ' ');
*(ptr++) = '\n';
}
TextScreen::show(ctx, goBack);
}
void HexdumpScreen::update(ui::Context &ctx) {
if (ctx.buttons.pressed(ui::BTN_START))
ctx.show(APP->_cartInfoScreen, true, true);
}
const char *ReflashGameScreen::_getItemName(ui::Context &ctx, int index) const {
static char name[96]; // TODO: get rid of this ugly crap
APP->_db.get(index)->getDisplayName(name, sizeof(name));
return name;
}
void ReflashGameScreen::show(ui::Context &ctx, bool goBack) {
_title = STR("ReflashGameScreen.title");
_prompt = STR("ReflashGameScreen.prompt");
_itemPrompt = STR("ReflashGameScreen.itemPrompt");
_listLength = APP->_db.getNumEntries();
ListScreen::show(ctx, goBack);
}
void ReflashGameScreen::update(ui::Context &ctx) {
ListScreen::update(ctx);
if (ctx.buttons.pressed(ui::BTN_START)) {
APP->_confirmScreen.setMessage(
*this,
[](ui::Context &ctx) {
APP->_setupWorker(&App::_cartReflashWorker);
ctx.show(APP->_workerStatusScreen, false, true);
},
STR("CartActionsScreen.reflash.confirm")
);
APP->_reflashEntry = APP->_db.get(_activeItem);
ctx.show(APP->_confirmScreen, false, true);
} else if (
(ctx.buttons.held(ui::BTN_LEFT) && ctx.buttons.pressed(ui::BTN_RIGHT)) ||
(ctx.buttons.pressed(ui::BTN_LEFT) && ctx.buttons.held(ui::BTN_RIGHT))
) {
ctx.show(APP->_cartActionsScreen, true, true);
}
}
void SystemIDEntryScreen::show(ui::Context &ctx, bool goBack) {
_title = STR("SystemIDEntryScreen.title");
_body = STR("SystemIDEntryScreen.body");
@ -171,15 +281,21 @@ void SystemIDEntryScreen::show(ui::Context &ctx, bool goBack) {
_separator = '-';
HexEntryScreen::show(ctx, goBack);
APP->_parser->getIdentifiers()->systemID.copyTo(_buffer);
}
void SystemIDEntryScreen::update(ui::Context &ctx) {
HexEntryScreen::update(ctx);
if (ctx.buttons.pressed(ui::BTN_START)) {
if (_activeButton == _buttonIndexOffset)
if (_activeButton == _buttonIndexOffset) {
ctx.show(APP->_cartActionsScreen, true, true);
else if (_activeButton == (_buttonIndexOffset + 1))
ctx.show(APP->_confirmScreen, false, true);
} else if (_activeButton == (_buttonIndexOffset + 1)) {
if (util::dsCRC8(_buffer, 7) == _buffer[7])
ctx.show(APP->_confirmScreen, false, true);
else
ctx.show(APP->_errorScreen, false, true);
}
}
}

View File

@ -21,6 +21,7 @@ public:
void reflash(ui::Context &ctx);
void erase(ui::Context &ctx);
void resetSystemID(ui::Context &ctx);
void matchSystemID(ui::Context &ctx);
void editSystemID(ui::Context &ctx);
void show(ui::Context &ctx, bool goBack = false);
@ -40,6 +41,24 @@ public:
void update(ui::Context &ctx);
};
class HexdumpScreen : public ui::TextScreen {
private:
char _bodyText[2048];
public:
void show(ui::Context &ctx, bool goBack = false);
void update(ui::Context &ctx);
};
class ReflashGameScreen : public ui::ListScreen {
protected:
const char *_getItemName(ui::Context &ctx, int index) const;
public:
void show(ui::Context &ctx, bool goBack = false);
void update(ui::Context &ctx);
};
class SystemIDEntryScreen : public ui::HexEntryScreen {
public:
inline void setSystemID(cart::Parser &parser) const {

View File

@ -90,8 +90,16 @@ void App::_cartDetectWorker(void) {
LOG("cart driver @ 0x%08x", _driver);
_workerStatus.update(1, 4, WSTR("App.cartDetectWorker.readCart"));
_driver->readCartID();
if (!_driver->readPublicData())
auto error = _driver->readCartID();
if (error)
LOG("cart ID error, code=%d", error);
error = _driver->readPublicData();
if (error)
LOG("public data error, code=%d", error);
else
_parser = cart::newCartParser(_dump);
LOG("cart parser @ 0x%08x", _parser);
@ -136,14 +144,21 @@ _cartInitDone:
// This must be outside of the if block above to make sure the system ID
// gets read with the dummy driver.
_driver->readSystemID();
auto error = _driver->readSystemID();
if (error)
LOG("system ID error, code=%d", error);
}
void App::_cartUnlockWorker(void) {
_workerStatus.setNextScreen(_cartInfoScreen, true);
_workerStatus.update(0, 2, WSTR("App.cartUnlockWorker.read"));
if (_driver->readPrivateData()) {
auto error = _driver->readPrivateData();
if (error) {
LOG("private data error, code=%d", error);
/*_errorScreen.setMessage(
_cartInfoScreen, WSTR("App.cartUnlockWorker.error")
);*/
@ -155,6 +170,7 @@ void App::_cartUnlockWorker(void) {
delete _parser;
_parser = cart::newCartParser(_dump);
if (!_parser)
return;
@ -204,51 +220,102 @@ void App::_hddDumpWorker(void) {
void App::_cartWriteWorker(void) {
_workerStatus.update(0, 1, WSTR("App.cartWriteWorker.write"));
if (_driver->writeData()) {
auto error = _driver->writeData();
_cartDetectWorker();
if (!error && _identified) {
_identified->copyKeyTo(_dump.dataKey);
_cartUnlockWorker();
}
if (error) {
LOG("write error, code=%d", error);
_errorScreen.setMessage(
_cartInfoScreen, WSTR("App.cartWriteWorker.error")
);
_workerStatus.setNextScreen(_errorScreen);
return;
}
}
void App::_cartReflashWorker(void) {
_workerStatus.update(0, 1, WSTR("App.cartWriteWorker.flash"));
if (_parser)
delete _parser;
_parser = cart::newCartParser(
_dump, _reflashEntry->formatType, _reflashEntry->flags
);
auto pri = _parser->getIdentifiers();
auto pub = _parser->getPublicIdentifiers();
_dump.clearData();
pri->clear();
pri->cartID.copyFrom(_dump.cartID.data);
pri->updateTraceID(_reflashEntry->traceIDType, _reflashEntry->traceIDParam);
// The private installation ID seems to be unused on carts with a public
// data section.
if (pub) {
pri->installID.clear();
pub->setInstallID(_reflashEntry->installIDPrefix);
} else {
pri->setInstallID(_reflashEntry->installIDPrefix);
}
_parser->setCode(_reflashEntry->code);
_parser->setRegion(_reflashEntry->region);
_parser->setYear(_reflashEntry->year);
_parser->flush();
uint8_t key[8];
auto error = _driver->writeData();
if (!error) {
_reflashEntry->copyKeyTo(key);
error = _driver->setDataKey(key);
}
_cartDetectWorker();
_cartUnlockWorker();
if (!error) {
_dump.copyKeyFrom(key);
_cartUnlockWorker();
}
if (error) {
LOG("write error, code=%d", error);
_errorScreen.setMessage(
_cartInfoScreen, WSTR("App.cartReflashWorker.error")
);
_workerStatus.setNextScreen(_errorScreen);
}
}
void App::_cartEraseWorker(void) {
_workerStatus.update(0, 1, WSTR("App.cartEraseWorker.erase"));
if (_driver->erase()) {
auto error = _driver->erase();
_cartDetectWorker();
if (!error) {
_dump.clearKey();
_cartUnlockWorker();
}
if (error) {
LOG("erase error, code=%d", error);
_errorScreen.setMessage(
_cartInfoScreen, WSTR("App.cartEraseWorker.error")
);
_workerStatus.setNextScreen(_errorScreen);
return;
}
_cartDetectWorker();
_cartUnlockWorker();
}
void App::_rebootWorker(void) {
int startTime = _ctx->time;
int duration = _ctx->gpuCtx.refreshRate * 3;
int elapsed;
// Stop clearing the watchdog for a few seconds.
_allowWatchdogClear = false;
do {
elapsed = _ctx->time - startTime;
_workerStatus.update(elapsed, duration, WSTR("App.rebootWorker.reboot"));
delayMicroseconds(10000);
} while (elapsed < duration);
// If for some reason the watchdog fails to reboot the system, fall back to
// a soft reboot.
softReset();
}
/* Misc. functions */
@ -267,9 +334,8 @@ void App::_worker(void) {
void App::_interruptHandler(void) {
if (acknowledgeInterrupt(IRQ_VBLANK)) {
_ctx->tick();
io::clearWatchdog();
if (_allowWatchdogClear)
io::clearWatchdog();
if (gpu::isIdle() && (_workerStatus.status != WORKER_BUSY_SUSPEND))
switchThread(nullptr);
}

View File

@ -95,6 +95,8 @@ class App {
friend class KeyEntryScreen;
friend class CartActionsScreen;
friend class QRCodeScreen;
friend class HexdumpScreen;
friend class ReflashGameScreen;
friend class SystemIDEntryScreen;
private:
@ -108,6 +110,8 @@ private:
KeyEntryScreen _keyEntryScreen;
CartActionsScreen _cartActionsScreen;
QRCodeScreen _qrCodeScreen;
HexdumpScreen _hexdumpScreen;
ReflashGameScreen _reflashGameScreen;
SystemIDEntryScreen _systemIDEntryScreen;
ui::Context *_ctx;
@ -123,9 +127,7 @@ private:
uint8_t *_workerStack;
cart::Driver *_driver;
cart::Parser *_parser;
const cart::DBEntry *_identified;
bool _allowWatchdogClear;
const cart::DBEntry *_identified, *_reflashEntry;
void _unloadCartData(void);
void _setupWorker(void (App::*func)(void));
@ -136,16 +138,15 @@ private:
void _qrCodeWorker(void);
void _hddDumpWorker(void);
void _cartWriteWorker(void);
void _cartReflashWorker(void);
void _cartEraseWorker(void);
void _rebootWorker(void);
void _worker(void);
void _interruptHandler(void);
public:
inline App(void)
: _driver(nullptr), _parser(nullptr), _identified(nullptr),
_allowWatchdogClear(true) {
: _driver(nullptr), _parser(nullptr), _identified(nullptr) {
_workerStack = new uint8_t[WORKER_STACK_SIZE];
}
inline ~App(void) {

View File

@ -142,8 +142,6 @@ void CartInfoScreen::show(ui::Context &ctx, bool goBack) {
state = IDENTIFIED;
APP->_identified->getDisplayName(name, sizeof(name));
//
auto ids = APP->_parser->getIdentifiers();
if (!(APP->_identified->flags & cart::DATA_HAS_SYSTEM_ID)) {
@ -320,7 +318,10 @@ void UnlockKeyScreen::update(ui::Context &ctx) {
);
ctx.show(APP->_confirmScreen, false, true);
}
} else if (ctx.buttons.held(ui::BTN_LEFT) && ctx.buttons.held(ui::BTN_RIGHT)) {
} else if (
(ctx.buttons.held(ui::BTN_LEFT) && ctx.buttons.pressed(ui::BTN_RIGHT)) ||
(ctx.buttons.pressed(ui::BTN_LEFT) && ctx.buttons.held(ui::BTN_RIGHT))
) {
ctx.show(APP->_cartInfoScreen, true, true);
}
}

View File

@ -68,25 +68,28 @@ void IdentifierSet::updateTraceID(TraceIDType type, int param) {
uint16_t checksum = 0;
if (type == TID_81) {
// TODO: reverse engineer this TID format
// This format seems to be an arbitrary unique identifier not tied to
// anything in particular (perhaps RTC RAM?), ignored by the game.
traceID.data[0] = 0x81;
traceID.data[2] = 0;
traceID.data[5] = 0;
traceID.data[6] = 0;
traceID.data[2] = 5;
traceID.data[5] = 7;
traceID.data[6] = 3;
LOG("prefix=0x81");
goto _done;
}
for (size_t i = 0; i < (sizeof(cartID.data) - 2); i++) {
for (size_t i = 0; i < ((sizeof(cartID.data) - 2) * 8); i += 8) {
uint8_t value = *(input++);
for (int j = 0; j < 8; j++, value >>= 1) {
if (value % 2)
checksum ^= 1 << (i % param);
for (size_t j = i; j < (i + 8); j++, value >>= 1) {
if (value & 1)
checksum ^= 1 << (j % param);
}
}
traceID.data[0] = 0x82;
if (type == TID_82_BIG_ENDIAN) {
traceID.data[1] = checksum >> 8;
traceID.data[2] = checksum & 0xff;
@ -101,6 +104,24 @@ _done:
traceID.updateChecksum();
}
uint8_t PublicIdentifierSet::getFlags(void) const {
uint8_t flags = 0;
if (!installID.isEmpty())
flags |= DATA_HAS_INSTALL_ID;
if (!systemID.isEmpty())
flags |= DATA_HAS_SYSTEM_ID;
return flags;
}
void PublicIdentifierSet::setInstallID(uint8_t prefix) {
installID.clear();
installID.data[0] = prefix;
installID.updateChecksum();
}
void BasicHeader::updateChecksum(bool invert) {
auto value = util::sum(reinterpret_cast<const uint8_t *>(this), 4);
uint8_t mask = invert ? 0xff : 0x00;

View File

@ -109,11 +109,14 @@ public:
class [[gnu::packed]] PublicIdentifierSet {
public:
Identifier traceID, cartID; // aka TID, SID
Identifier installID, systemID; // aka MID, XID
inline void clear(void) {
__builtin_memset(this, 0, sizeof(PublicIdentifierSet));
}
uint8_t getFlags(void) const;
void setInstallID(uint8_t prefix);
};
class [[gnu::packed]] SimpleHeader {

View File

@ -51,6 +51,22 @@ size_t SimpleParser::getRegion(char *output) const {
return __builtin_strlen(output);
}
void SimpleParser::setRegion(const char *input) {
auto header = _getHeader();
__builtin_strncpy(header->region, input, sizeof(header->region));
}
void BasicParser::setCode(const char *input) {
if (!(flags & DATA_HAS_CODE_PREFIX))
return;
auto header = _getHeader();
header->region[2] = input[0];
header->region[3] = input[1];
}
size_t BasicParser::getRegion(char *output) const {
auto header = _getHeader();
@ -61,10 +77,21 @@ size_t BasicParser::getRegion(char *output) const {
return 2;
}
void BasicParser::setRegion(const char *input) {
auto header = _getHeader();
header->region[0] = input[0];
header->region[1] = input[1];
}
IdentifierSet *BasicParser::getIdentifiers(void) {
return reinterpret_cast<IdentifierSet *>(&_dump.data[sizeof(BasicHeader)]);
}
void BasicParser::flush(void) {
_getHeader()->updateChecksum(flags & DATA_CHECKSUM_INVERTED);
}
bool BasicParser::validate(void) {
if (!Parser::validate())
return false;
@ -84,6 +111,15 @@ size_t ExtendedParser::getCode(char *output) const {
return __builtin_strlen(output);
}
void ExtendedParser::setCode(const char *input) {
auto header = _getHeader();
__builtin_strncpy(header->code, input, sizeof(header->code));
if (flags & DATA_GX706_WORKAROUND)
header->region[1] = 'E';
}
size_t ExtendedParser::getRegion(char *output) const {
auto header = _getHeader();
@ -93,6 +129,20 @@ size_t ExtendedParser::getRegion(char *output) const {
return __builtin_strlen(output);
}
void ExtendedParser::setRegion(const char *input) {
auto header = _getHeader();
__builtin_strncpy(header->region, input, sizeof(header->region));
}
uint16_t ExtendedParser::getYear(void) const {
return _getHeader()->year;
}
void ExtendedParser::setYear(uint16_t value) {
_getHeader()->year = value;
}
IdentifierSet *ExtendedParser::getIdentifiers(void) {
if (!(flags & DATA_HAS_PUBLIC_SECTION))
return nullptr;
@ -118,8 +168,11 @@ void ExtendedParser::flush(void) {
auto pri = getIdentifiers();
auto pub = getPublicIdentifiers();
pub->traceID.copyFrom(pri->traceID.data);
pub->cartID.copyFrom(pri->cartID.data);
// The private installation ID seems to always go unused and zeroed out...
//pub->installID.copyFrom(pri->installID.data);
pub->systemID.copyFrom(pri->systemID.data);
_getHeader()->updateChecksum(flags & DATA_CHECKSUM_INVERTED);
}
bool ExtendedParser::validate(void) {
@ -272,18 +325,22 @@ Parser *newCartParser(Dump &dump) {
const DBEntry *CartDB::lookup(const char *code, const char *region) const {
// Perform a binary search. This assumes all entries in the DB are sorted by
// their code and region.
auto offset = reinterpret_cast<const DBEntry *>(ptr);
auto low = reinterpret_cast<const DBEntry *>(ptr);
auto high = &low[getNumEntries() - 1];
for (size_t step = getNumEntries() / 2; step; step /= 2) {
auto entry = &offset[step];
while (low <= high) {
auto entry = &low[(high - low) / 2];
int diff = entry->compare(code, region);
if (!diff) {
LOG("%s %s found, entry=0x%08x", code, region, entry);
return entry;
} else if (diff < 0) {
offset = entry;
}
if (diff < 0)
low = &entry[1];
else
high = &entry[-1];
}
LOG("%s %s not found", code, region);

View File

@ -34,7 +34,11 @@ public:
virtual ~Parser(void) {}
virtual size_t getCode(char *output) const { return 0; }
virtual void setCode(const char *input) {}
virtual size_t getRegion(char *output) const { return 0; }
virtual void setRegion(const char *input) {}
virtual uint16_t getYear(void) const { return 0; }
virtual void setYear(uint16_t value) {}
virtual IdentifierSet *getIdentifiers(void) { return nullptr; }
virtual PublicIdentifierSet *getPublicIdentifiers(void) { return nullptr; }
virtual void flush(void) {}
@ -52,6 +56,7 @@ public:
: Parser(dump, flags | DATA_HAS_PUBLIC_SECTION) {}
size_t getRegion(char *output) const;
void setRegion(const char *input);
};
class BasicParser : public Parser {
@ -64,8 +69,11 @@ public:
inline BasicParser(Dump &dump, uint8_t flags = 0)
: Parser(dump, flags) {}
void setCode(const char *input);
size_t getRegion(char *output) const;
void setRegion(const char *input);
IdentifierSet *getIdentifiers(void);
void flush(void);
bool validate(void);
};
@ -80,7 +88,11 @@ public:
: Parser(dump, flags | DATA_HAS_CODE_PREFIX) {}
size_t getCode(char *output) const;
void setCode(const char *input);
size_t getRegion(char *output) const;
void setRegion(const char *input);
uint16_t getYear(void) const;
void setYear(uint16_t value);
IdentifierSet *getIdentifiers(void);
PublicIdentifierSet *getPublicIdentifiers(void);
void flush(void);
@ -118,6 +130,9 @@ public:
inline int getDisplayName(char *output, size_t length) const {
return snprintf(output, length, "%s %s\t%s", code, region, name);
}
inline void copyKeyTo(uint8_t *dest) const {
__builtin_memcpy(dest, dataKey, sizeof(dataKey));
}
};
class CartDB : public util::Data {

View File

@ -110,6 +110,7 @@ DriverError DummyDriver::setDataKey(const uint8_t *key) {
static constexpr int _X76_MAX_ACK_POLLS = 5;
static constexpr int _X76_WRITE_DELAY = 12000;
static constexpr int _X76_PACKET_DELAY = 12000;
static constexpr int _ZS01_SEND_DELAY = 30000;
static constexpr int _ZS01_PACKET_DELAY = 30000;
DriverError CartDriver::readSystemID(void) {
@ -377,6 +378,7 @@ DriverError X76F100Driver::setDataKey(const uint8_t *key) {
DriverError ZS01Driver::_transact(
zs01::Packet &request, zs01::Packet &response
) {
delayMicroseconds(_ZS01_PACKET_DELAY);
io::i2cStart();
#ifdef ENABLE_I2C_LOGGING
@ -387,7 +389,7 @@ DriverError ZS01Driver::_transact(
#endif
if (!io::i2cWriteBytes(
&request.command, sizeof(zs01::Packet), _ZS01_PACKET_DELAY
&request.command, sizeof(zs01::Packet), _ZS01_SEND_DELAY
)) {
io::i2cStop();
LOG("NACK while sending request packet");
@ -402,14 +404,16 @@ DriverError ZS01Driver::_transact(
LOG("R: %s", buffer);
#endif
if (!response.decodeResponse())
return ZS01_CRC_MISMATCH;
bool ok = response.decodeResponse();
#ifdef ENABLE_I2C_LOGGING
util::hexToString(buffer, &response.command, sizeof(zs01::Packet), ' ');
LOG("D: %s", buffer);
#endif
//if (!ok)
//return ZS01_CRC_MISMATCH;
_encoderState = response.address;
if (response.command != zs01::RESP_NO_ERROR) {

View File

@ -232,7 +232,7 @@ DeviceError Device::enumerate(void) {
_copyString(revision, block.revision, sizeof(revision));
_copyString(serialNumber, block.serialNumber, sizeof(serialNumber));
LOG("%s: %s", (flags & DEVICE_PRIMARY) ? "primary" : "secondary", model);
LOG("%s: %s", (flags & DEVICE_SECONDARY) ? "secondary" : "primary", model);
// Find out the fastest PIO transfer mode supported and enable it.
int mode = 1;

View File

@ -171,15 +171,18 @@ void initKonamiBitstream(void) {
/* I2C driver */
static constexpr int _I2C_BUS_DELAY = 50;
static constexpr int _I2C_RESET_DELAY = 500;
// SDA is open-drain so it is toggled by changing pin direction.
#define _SDA(value) setCartSDADir(!(value))
#define SDA(value) _SDA(value), delayMicroseconds(20)
#define SDA(value) _SDA(value), delayMicroseconds(_I2C_BUS_DELAY)
#define _SCL(value) setCartOutput(OUT_SCL, value)
#define SCL(value) _SCL(value), delayMicroseconds(20)
#define SCL(value) _SCL(value), delayMicroseconds(_I2C_BUS_DELAY)
#define _CS(value) setCartOutput(OUT_CS, value)
#define CS(value) _CS(value), delayMicroseconds(20)
#define CS(value) _CS(value), delayMicroseconds(_I2C_BUS_DELAY)
#define _RESET(value) setCartOutput(OUT_RESET, value)
#define RESET(value) _RESET(value), delayMicroseconds(20)
#define RESET(value) _RESET(value), delayMicroseconds(_I2C_BUS_DELAY)
void i2cStart(void) {
_SDA(true);
@ -226,7 +229,7 @@ uint8_t i2cReadByte(void) {
SCL(false);
}
delayMicroseconds(20);
delayMicroseconds(_I2C_BUS_DELAY);
return value;
}
@ -248,12 +251,12 @@ void i2cSendACK(bool ack) {
}
bool i2cGetACK(void) {
delayMicroseconds(20); // Required for ZS01
delayMicroseconds(_I2C_BUS_DELAY); // Required for ZS01
SCL(true);
bool ack = !getCartSDA();
SCL(false);
delayMicroseconds(20);
delayMicroseconds(_I2C_BUS_DELAY);
return ack;
}
@ -291,6 +294,7 @@ uint32_t i2cResetX76(void) {
SCL(true);
SCL(false);
RESET(false);
delayMicroseconds(_I2C_RESET_DELAY);
for (int bit = 0; bit < 32; bit++) { // LSB first
SCL(true);
@ -317,7 +321,7 @@ uint32_t i2cResetZS01(void) {
RESET(false);
RESET(true);
delayMicroseconds(100);
delayMicroseconds(_I2C_RESET_DELAY);
SCL(true);
SCL(false);
@ -335,12 +339,14 @@ uint32_t i2cResetZS01(void) {
/* 1-wire driver */
static constexpr int _DS_RESET_DELAY = 480;
#define _CART1WIRE(value) setCartOutput(OUT_1WIRE, !(value))
#define _DIO1WIRE(value) setDIO1Wire(value)
bool dsCartReset(void) {
_CART1WIRE(false);
delayMicroseconds(480);
delayMicroseconds(_DS_RESET_DELAY);
_CART1WIRE(true);
delayMicroseconds(60);
@ -353,7 +359,7 @@ bool dsCartReset(void) {
bool dsDIOReset(void) {
_DIO1WIRE(false);
delayMicroseconds(480);
delayMicroseconds(_DS_RESET_DELAY);
_DIO1WIRE(true);
delayMicroseconds(60);

View File

@ -79,25 +79,35 @@ void MessageScreen::update(Context &ctx) {
if (_locked)
return;
if (ctx.buttons.pressed(ui::BTN_LEFT)) {
if (_activeButton > 0) {
_activeButton--;
int numButtons = _buttonIndexOffset + _numButtons;
_buttonAnim.setValue(ctx.time, 0, _getButtonWidth(), SPEED_FASTEST);
ctx.sounds[SOUND_MOVE].play();
} else {
if (
ctx.buttons.pressed(ui::BTN_LEFT) ||
(ctx.buttons.repeating(ui::BTN_LEFT) && (_activeButton > 0))
) {
_activeButton--;
if (_activeButton < 0) {
_activeButton += numButtons;
ctx.sounds[SOUND_CLICK].play();
} else {
ctx.sounds[SOUND_MOVE].play();
}
_buttonAnim.setValue(ctx.time, 0, _getButtonWidth(), SPEED_FASTEST);
}
if (ctx.buttons.pressed(ui::BTN_RIGHT)) {
if (_activeButton < (_buttonIndexOffset + _numButtons - 1)) {
_activeButton++;
_buttonAnim.setValue(ctx.time, 0, _getButtonWidth(), SPEED_FASTEST);
ctx.sounds[SOUND_MOVE].play();
} else {
if (
ctx.buttons.pressed(ui::BTN_RIGHT) ||
(ctx.buttons.repeating(ui::BTN_RIGHT) && (_activeButton < (numButtons - 1)))
) {
_activeButton++;
if (_activeButton >= numButtons) {
_activeButton -= numButtons;
ctx.sounds[SOUND_CLICK].play();
} else {
ctx.sounds[SOUND_MOVE].play();
}
_buttonAnim.setValue(ctx.time, 0, _getButtonWidth(), SPEED_FASTEST);
}
}
@ -141,8 +151,9 @@ void HexEntryScreen::draw(Context &ctx, bool active) const {
// Cursor
if (_activeButton < _buttonIndexOffset)
ctx.gpuCtx.drawGradientRectV(
textOffset + _cursorAnim.getValue(ctx.time), boxY, digitWidth,
BUTTON_HEIGHT, COLOR_BOX1, COLOR_HIGHLIGHT1
textOffset + _cursorAnim.getValue(ctx.time),
boxY + BUTTON_HEIGHT / 2, digitWidth, BUTTON_HEIGHT / 2, COLOR_BOX1,
COLOR_HIGHLIGHT1
);
// Text

View File

@ -18,8 +18,10 @@ static const Key _COMMAND_KEY{
// Konami's driver generates a pseudorandom key for each transaction, but it can
// be a fixed key as well.
static const Key _RESPONSE_KEY{
.add = { 0, 0, 0, 0, 0, 0, 0, 0 },
.shift = { 0, 0, 0, 0, 0, 0, 0, 0 }
.add = { 237, 8, 16, 11, 6, 4, 8, 30 },
.shift = { 0, 3, 2, 2, 6, 2, 2, 1 }
//.add = { 0, 0, 0, 0, 0, 0, 0, 0 },
//.shift = { 0, 0, 0, 0, 0, 0, 0, 0 }
};
/* Packet encoding/decoding */

View File

@ -30,15 +30,15 @@ enum RequestFlag : uint8_t {
enum ResponseCode : uint8_t {
// The meaning of these codes is currently unknown. Presumably:
// - one of the "security errors" is a CRC validation failure, the other
// could be data key related
// could be data key related, the third one could be DS2401 related
// - one of the unknown errors is for invalid commands or addresses
// - one or two of the unknown errors are for actual read/write failures
// - one of the unknown errors is for actual read/write failures
RESP_NO_ERROR = 0x00,
RESP_SECURITY_ERROR1 = 0x01,
RESP_UNKNOWN_ERROR1 = 0x02,
RESP_UNKNOWN_ERROR2 = 0x03,
RESP_SECURITY_ERROR2 = 0x04,
RESP_UNKNOWN_ERROR3 = 0x05
RESP_UNKNOWN_ERROR1 = 0x01,
RESP_SECURITY_ERROR1 = 0x02,
RESP_SECURITY_ERROR2 = 0x03,
RESP_UNKNOWN_ERROR2 = 0x04,
RESP_SECURITY_ERROR3 = 0x05
};
/* Packet encoding/decoding */

View File

@ -6,7 +6,7 @@ __author__ = "spicyjpeg"
import re
from dataclasses import dataclass
from enum import IntEnum, IntFlag
from struct import Struct
from struct import Struct, unpack
from typing import Any, Iterable, Iterator, Mapping, Sequence
## Definitions
@ -100,7 +100,24 @@ class IdentifierSet:
return flags
def getTraceIDType(self) -> TraceIDType:
def getCartIDChecksum(self, param: int) -> int:
if self.cartID is None:
return 0
checksum: int = 0
for i in range(6):
value: int = self.cartID[i + 1]
for j in range(i * 8, (i + 1) * 8):
if value & 1:
checksum ^= 1 << (j % param)
value >>= 1
return checksum & 0xffff
def getTraceIDType(self, param: int) -> TraceIDType:
if self.traceID is None:
return TraceIDType.TID_NONE
@ -109,7 +126,17 @@ class IdentifierSet:
return TraceIDType.TID_81
case 0x82:
return TraceIDType.TID_82_BIG_ENDIAN # TODO
print(self.cartID,self.traceID)
checksum: int = self.getCartIDChecksum(param)
big: int = unpack("> H", self.traceID[1:3])[0]
little: int = unpack("< H", self.traceID[1:3])[0]
if checksum == big:
return TraceIDType.TID_82_BIG_ENDIAN
elif checksum == little:
return TraceIDType.TID_82_LITTLE_ENDIAN
raise ValueError(f"trace ID mismatch, exp=0x{checksum:04x}, big=0x{big:04x}, little=0x{little:04x}")
case prefix:
raise ValueError(f"unknown trace ID prefix: 0x{prefix:02x}")
@ -272,6 +299,7 @@ class ExtendedParser(Parser):
## Cartridge database
DB_ENTRY_STRUCT: Struct = Struct("< 6B H 8s 8s 8s 96s")
TRACE_ID_PARAMS: Sequence[int] = 16, 14
@dataclass
class GameEntry:
@ -312,21 +340,32 @@ class DBEntry:
year: int = 0
def __init__(self, game: GameEntry, dump: Dump, parser: Parser):
# Find the correct parameters for the trace ID heuristically.
_type: TraceIDType | None = None
for self.traceIDParam in TRACE_ID_PARAMS:
try:
_type = parser.identifiers.getTraceIDType(self.traceIDParam)
except ValueError:
continue
break
if _type is None:
raise RuntimeError("failed to determine trace ID parameters")
self.game = game
self.dataKey = dump.dataKey
self.chipType = dump.chipType
self.formatType = parser.formatType
self.traceIDType = parser.identifiers.getTraceIDType()
self.traceIDType = _type
self.flags = parser.flags
self.year = parser.year or 0
# TODO: implement this properly
self.traceIDParam = 16
if parser.identifiers.installID:
self.installIDPrefix = parser.identifiers.installID[0]
else:
if parser.identifiers.installID is None:
self.installIDPrefix = 0
else:
self.installIDPrefix = parser.identifiers.installID[0]
def __lt__(self, entry: Any) -> bool:
return (self.game < entry.game)